From 4d2e4172a229f4d5d176f5241646291c709e333b Mon Sep 17 00:00:00 2001 From: David Volovskiy Date: Tue, 8 Jul 2025 07:44:06 -0400 Subject: [PATCH 001/179] Moved Wordle from legacy to main folder --- movement_faces.h | 1 + watch-faces.mk | 1 + {legacy/watch_faces => watch-faces}/complication/wordle_face.c | 0 {legacy/watch_faces => watch-faces}/complication/wordle_face.h | 0 .../watch_faces => watch-faces}/complication/wordle_face_dict.h | 0 5 files changed, 2 insertions(+) rename {legacy/watch_faces => watch-faces}/complication/wordle_face.c (100%) rename {legacy/watch_faces => watch-faces}/complication/wordle_face.h (100%) rename {legacy/watch_faces => watch-faces}/complication/wordle_face_dict.h (100%) diff --git a/movement_faces.h b/movement_faces.h index f5c087bc..d849636b 100644 --- a/movement_faces.h +++ b/movement_faces.h @@ -61,4 +61,5 @@ #include "totp_face.h" #include "tally_face.h" #include "probability_face.h" +#include "wordle_face.h" // New includes go above this line. diff --git a/watch-faces.mk b/watch-faces.mk index 665ec4b7..a390a3bf 100644 --- a/watch-faces.mk +++ b/watch-faces.mk @@ -16,6 +16,7 @@ SRCS += \ ./watch-faces/complication/squash_face.c \ ./watch-faces/complication/totp_face.c \ ./watch-faces/complication/tally_face.c \ + ./watch-faces/complication/wordle_face.c \ ./watch-faces/demo/all_segments_face.c \ ./watch-faces/demo/character_set_face.c \ ./watch-faces/demo/light_sensor_face.c \ diff --git a/legacy/watch_faces/complication/wordle_face.c b/watch-faces/complication/wordle_face.c similarity index 100% rename from legacy/watch_faces/complication/wordle_face.c rename to watch-faces/complication/wordle_face.c diff --git a/legacy/watch_faces/complication/wordle_face.h b/watch-faces/complication/wordle_face.h similarity index 100% rename from legacy/watch_faces/complication/wordle_face.h rename to watch-faces/complication/wordle_face.h diff --git a/legacy/watch_faces/complication/wordle_face_dict.h b/watch-faces/complication/wordle_face_dict.h similarity index 100% rename from legacy/watch_faces/complication/wordle_face_dict.h rename to watch-faces/complication/wordle_face_dict.h From 07116222108a8bb3b602998acea4644c91642f68 Mon Sep 17 00:00:00 2001 From: David Volovskiy Date: Tue, 8 Jul 2025 07:51:36 -0400 Subject: [PATCH 002/179] Add delays before registering btns, don't repeat words, cleaned up enum names --- movement_config.h | 1 + watch-faces/complication/wordle_face.c | 152 +++++++++++++++---------- watch-faces/complication/wordle_face.h | 50 +++++--- 3 files changed, 129 insertions(+), 74 deletions(-) diff --git a/movement_config.h b/movement_config.h index 0da8da43..c4cb8c36 100644 --- a/movement_config.h +++ b/movement_config.h @@ -29,6 +29,7 @@ const watch_face_t watch_faces[] = { clock_face, + wordle_face, world_clock_face, sunrise_sunset_face, moon_phase_face, diff --git a/watch-faces/complication/wordle_face.c b/watch-faces/complication/wordle_face.c index a93a0e0e..4873b5a1 100644 --- a/watch-faces/complication/wordle_face.c +++ b/watch-faces/complication/wordle_face.c @@ -35,7 +35,7 @@ static uint32_t get_random(uint32_t max) { #endif } -static uint8_t get_first_pos(WordleLetterResult *word_elements_result) { +static uint8_t get_first_pos(wordle_letter_result *word_elements_result) { for (size_t i = 0; i < WORDLE_LENGTH; i++) { if (word_elements_result[i] != WORDLE_LETTER_CORRECT) return i; @@ -43,7 +43,7 @@ static uint8_t get_first_pos(WordleLetterResult *word_elements_result) { return 0; } -static uint8_t get_next_pos(uint8_t curr_pos, WordleLetterResult *word_elements_result) { +static uint8_t get_next_pos(uint8_t curr_pos, wordle_letter_result *word_elements_result) { for (size_t pos = curr_pos; pos < WORDLE_LENGTH;) { if (word_elements_result[++pos] != WORDLE_LETTER_CORRECT) return pos; @@ -51,7 +51,7 @@ static uint8_t get_next_pos(uint8_t curr_pos, WordleLetterResult *word_elements_ return WORDLE_LENGTH; } -static uint8_t get_prev_pos(uint8_t curr_pos, WordleLetterResult *word_elements_result) { +static uint8_t get_prev_pos(uint8_t curr_pos, wordle_letter_result *word_elements_result) { if (curr_pos == 0) return 0; for (int8_t pos = curr_pos; pos >= 0;) { if (word_elements_result[--pos] != WORDLE_LETTER_CORRECT) @@ -99,13 +99,15 @@ static void display_all_letters(wordle_state_t *state) { #if !WORDLE_ALLOW_NON_WORD_AND_REPEAT_GUESSES static void display_not_in_dict(wordle_state_t *state) { - state->curr_screen = SCREEN_NO_DICT; + state->curr_screen = WORDLE_SCREEN_NO_DICT; watch_display_string("nodict", 4); + state->ignore_btn_ticks = WORDLE_TICK_BAD_GUESS; } static void display_already_guessed(wordle_state_t *state) { - state->curr_screen = SCREEN_ALREADY_GUESSED; + state->curr_screen = WORDLE_SCREEN_ALREADY_GUESSED; watch_display_string("GUESSD", 4); + state->ignore_btn_ticks = WORDLE_TICK_BAD_GUESS; } static uint32_t check_word_in_dict(uint8_t *word_elements) { @@ -164,8 +166,8 @@ static bool check_word(wordle_state_t *state) { return false; } -static void show_skip_wrong_letter_indicator(bool skipping, WordleScreen curr_screen) { - if (curr_screen >= SCREEN_PLAYING) return; +static void show_skip_wrong_letter_indicator(bool skipping, wordle_screen curr_screen) { + if (curr_screen >= WORDLE_SCREEN_PLAYING) return; if (skipping) watch_display_string("H", 3); else @@ -201,7 +203,7 @@ static void display_attempt(uint8_t attempt) { } static void display_playing(wordle_state_t *state) { - state->curr_screen = SCREEN_PLAYING; + state->curr_screen = WORDLE_SCREEN_PLAYING; display_attempt(state->attempt); display_all_letters(state); } @@ -230,9 +232,18 @@ static void reset_incorrect_elements(wordle_state_t *state) { } } +static bool is_in_do_not_use_list(uint16_t guess, const uint16_t *not_to_use, uint8_t max_repeats) { + for (size_t i = 0; i < max_repeats; i++) { + if (guess == not_to_use[i]) return true; + } + return false; +} + static void reset_board(wordle_state_t *state) { reset_all_elements(state); - state->curr_answer = get_random(WORDLE_NUM_WORDS); + do { + state->curr_answer = get_random(WORDLE_NUM_WORDS); + } while (is_in_do_not_use_list(state->curr_answer, state->not_to_use, WORDLE_MAX_BETWEEN_REPEATS)); watch_clear_colon(); state->position = get_first_pos(state->word_elements_result); display_playing(state); @@ -243,7 +254,7 @@ static void reset_board(wordle_state_t *state) { } static void display_title(wordle_state_t *state) { - state->curr_screen = SCREEN_TITLE; + state->curr_screen = WORDLE_SCREEN_TITLE; watch_display_string("WO WordLE", 0); show_skip_wrong_letter_indicator(state->skip_wrong_letter, state->curr_screen); } @@ -254,7 +265,7 @@ static void display_continue_result(bool continuing) { } static void display_continue(wordle_state_t *state) { - state->curr_screen = SCREEN_CONTINUE; + state->curr_screen = WORDLE_SCREEN_CONTINUE; watch_display_string("Cont ", 4); show_skip_wrong_letter_indicator(state->skip_wrong_letter, state->curr_screen); display_continue_result(state->continuing); @@ -263,7 +274,7 @@ static void display_continue(wordle_state_t *state) { static void display_streak(wordle_state_t *state) { char buf[12]; - state->curr_screen = SCREEN_STREAK; + state->curr_screen = WORDLE_SCREEN_STREAK; #if WORDLE_USE_DAILY_STREAK == 2 if (state->streak > 99) sprintf(buf, "WO St--dy"); @@ -279,7 +290,7 @@ static void display_streak(wordle_state_t *state) { #if WORDLE_USE_DAILY_STREAK == 2 static void display_wait(wordle_state_t *state) { - state->curr_screen = SCREEN_WAIT; + state->curr_screen = WORDLE_SCREEN_WAIT; if (state->streak < 40) { char buf[13]; sprintf(buf,"WO%2d WaIt ", state->streak); @@ -302,14 +313,14 @@ static uint32_t get_day_unix_time(void) { static void display_lose(wordle_state_t *state, uint8_t subsecond) { char buf[WORDLE_LENGTH + 6]; - sprintf(buf," L %s", subsecond % 2 ? _valid_words[state->curr_answer] : " "); + sprintf(buf,"L %s", subsecond % 2 ? _valid_words[state->curr_answer] : " "); watch_display_string(buf, 0); } static void display_win(wordle_state_t *state, uint8_t subsecond) { (void) state; char buf[13]; - sprintf(buf," W %s ", subsecond % 2 ? "NICE" : "JOb "); + sprintf(buf,"W %s ", subsecond % 2 ? "NICE" : "JOb "); watch_display_string(buf, 0); } @@ -346,15 +357,16 @@ static void display_result(wordle_state_t *state, uint8_t subsecond) { watch_display_string(buf, 5); } -static bool act_on_btn(wordle_state_t *state, const uint8_t pin) { +static bool act_on_btn(wordle_state_t *state, const wordle_pin_enum pin) { + if (state->ignore_btn_ticks > 0) return true; switch (state->curr_screen) { - case SCREEN_RESULT: + case WORDLE_SCREEN_RESULT: reset_incorrect_elements(state); state->position = get_first_pos(state->word_elements_result); display_playing(state); return true; - case SCREEN_TITLE: + case WORDLE_SCREEN_TITLE: #if WORDLE_USE_DAILY_STREAK == 2 if (state->day_last_game_started == get_day_unix_time()) { display_wait(state); @@ -372,26 +384,26 @@ static bool act_on_btn(wordle_state_t *state, const uint8_t pin) { display_streak(state); #endif return true; - case SCREEN_STREAK: + case WORDLE_SCREEN_STREAK: state->day_last_game_started = get_day_unix_time(); reset_board(state); return true; - case SCREEN_WIN: - case SCREEN_LOSE: + case WORDLE_SCREEN_WIN: + case WORDLE_SCREEN_LOSE: display_title(state); return true; - case SCREEN_NO_DICT: - case SCREEN_ALREADY_GUESSED: + case WORDLE_SCREEN_NO_DICT: + case WORDLE_SCREEN_ALREADY_GUESSED: state->position = get_first_pos(state->word_elements_result); display_playing(state); return true; #if WORDLE_USE_DAILY_STREAK == 2 - case SCREEN_WAIT: + case WORDLE_SCREEN_WAIT: (void) pin; display_title(state); return true; #else - case SCREEN_CONTINUE: + case WORDLE_SCREEN_CONTINUE: switch (pin) { case BTN_ALARM: @@ -407,6 +419,8 @@ static bool act_on_btn(wordle_state_t *state, const uint8_t pin) { state->continuing = !state->continuing; display_continue_result(state->continuing); break; + default: + break; } return true; #endif @@ -416,6 +430,13 @@ static bool act_on_btn(wordle_state_t *state, const uint8_t pin) { return false; } +static void win_lose_shared(wordle_state_t *state) { + reset_all_elements(state); + state->ignore_btn_ticks = WORDLE_TICK_WIN_LOSE; + state->not_to_use[state->not_to_use_position] = state->curr_answer; + state->not_to_use_position = (state->not_to_use_position + 1) % WORDLE_MAX_BETWEEN_REPEATS; +} + static void get_result(wordle_state_t *state) { #if !WORDLE_ALLOW_NON_WORD_AND_REPEAT_GUESSES // Check if it's in the dict @@ -437,8 +458,8 @@ static void get_result(wordle_state_t *state) { #endif bool exact_match = check_word(state); if (exact_match) { - reset_all_elements(state); - state->curr_screen = SCREEN_WIN; + state->curr_screen = WORDLE_SCREEN_WIN; + win_lose_shared(state); if (state->streak < 0x7F) state->streak++; #if WORDLE_USE_DAILY_STREAK == 2 @@ -447,13 +468,14 @@ static void get_result(wordle_state_t *state) { return; } if (++state->attempt >= WORDLE_MAX_ATTEMPTS) { - reset_all_elements(state); - state->curr_screen = SCREEN_LOSE; + state->curr_screen = WORDLE_SCREEN_LOSE; + win_lose_shared(state); state->streak = 0; return; } update_known_wrong_letters(state); - state->curr_screen = SCREEN_RESULT; + state->curr_screen = WORDLE_SCREEN_RESULT; + state->ignore_btn_ticks = WORDLE_TICKS_RESULT; return; } @@ -476,21 +498,7 @@ static void insert_random_guess(wordle_state_t *state) { } #endif -void wordle_face_setup(uint8_t watch_face_index, void ** context_ptr) { - (void) watch_face_index; - if (*context_ptr == NULL) { - *context_ptr = malloc(sizeof(wordle_state_t)); - memset(*context_ptr, 0, sizeof(wordle_state_t)); - wordle_state_t *state = (wordle_state_t *)*context_ptr; - state->curr_screen = SCREEN_TITLE; - state->skip_wrong_letter = false; - reset_all_elements(state); - } - // Do any pin or peripheral setup here; this will be called whenever the watch wakes from deep sleep. -} - -void wordle_face_activate(void *context) { - wordle_state_t *state = (wordle_state_t *)context; +static void _activate(wordle_state_t *state) { #if WORDLE_USE_DAILY_STREAK != 0 uint32_t now = get_day_unix_time(); uint32_t one_day = 60 *60 * 24; @@ -501,35 +509,58 @@ void wordle_face_activate(void *context) { } #endif state->using_random_guess = false; - if (is_playing(state) && state->curr_screen >= SCREEN_RESULT) { + if (is_playing(state) && state->curr_screen >= WORDLE_SCREEN_RESULT) { reset_incorrect_elements(state); state->position = get_first_pos(state->word_elements_result); } - movement_request_tick_frequency(2); + movement_request_tick_frequency(WORDLE_FREQ); + watch_clear_all_indicators(); + watch_clear_colon(); display_title(state); } +void wordle_face_setup(uint8_t watch_face_index, void ** context_ptr) { + (void) watch_face_index; + if (*context_ptr == NULL) { + *context_ptr = malloc(sizeof(wordle_state_t)); + memset(*context_ptr, 0, sizeof(wordle_state_t)); + wordle_state_t *state = (wordle_state_t *)*context_ptr; + state->curr_screen = WORDLE_SCREEN_TITLE; + state->skip_wrong_letter = true; + reset_all_elements(state); + memset(state->not_to_use, 0xff, sizeof(state->not_to_use)); + } + // Do any pin or peripheral setup here; this will be called whenever the watch wakes from deep sleep. +} + +void wordle_face_activate(void *context) { + wordle_state_t *state = (wordle_state_t *)context; + _activate(state); + +} + bool wordle_face_loop(movement_event_t event, void *context) { wordle_state_t *state = (wordle_state_t *)context; switch (event.event_type) { case EVENT_TICK: + if (state->ignore_btn_ticks > 0) state->ignore_btn_ticks--; switch (state->curr_screen) { - case SCREEN_PLAYING: + case WORDLE_SCREEN_PLAYING: if (event.subsecond % 2) { display_letter(state, true); } else { watch_display_string(" ", state->position + 5); } break; - case SCREEN_RESULT: + case WORDLE_SCREEN_RESULT: display_result(state, event.subsecond); break; - case SCREEN_LOSE: + case WORDLE_SCREEN_LOSE: display_lose(state, event.subsecond); break; - case SCREEN_WIN: + case WORDLE_SCREEN_WIN: display_win(state, event.subsecond); break; default: @@ -542,12 +573,12 @@ bool wordle_face_loop(movement_event_t event, void *context) { display_letter(state, true); break; case EVENT_LIGHT_LONG_PRESS: - if (state->curr_screen < SCREEN_PLAYING) { + if (state->curr_screen < WORDLE_SCREEN_PLAYING) { state->skip_wrong_letter = !state->skip_wrong_letter; show_skip_wrong_letter_indicator(state->skip_wrong_letter, state->curr_screen); break; } - if (state->curr_screen != SCREEN_PLAYING) break; + if (state->curr_screen != WORDLE_SCREEN_PLAYING) break; get_prev_letter(state->position, state->word_elements, state->known_wrong_letters, state->skip_wrong_letter); display_letter(state, true); break; @@ -569,7 +600,7 @@ bool wordle_face_loop(movement_event_t event, void *context) { } break; case EVENT_ALARM_LONG_PRESS: - if (state->curr_screen != SCREEN_PLAYING) break; + if (state->curr_screen != WORDLE_SCREEN_PLAYING) break; display_letter(state, true); state->position = get_prev_pos(state->position, state->word_elements_result); break; @@ -577,17 +608,24 @@ bool wordle_face_loop(movement_event_t event, void *context) { case EVENT_ACTIVATE: break; case EVENT_TIMEOUT: - if (state->curr_screen >= SCREEN_RESULT) { + if (state->curr_screen >= WORDLE_SCREEN_RESULT) { reset_incorrect_elements(state); state->position = get_first_pos(state->word_elements_result); display_title(state); } break; case EVENT_LOW_ENERGY_UPDATE: - if (state->curr_screen != SCREEN_TITLE) + if (state->curr_screen != WORDLE_SCREEN_TITLE) display_title(state); break; - default: + case EVENT_MODE_LONG_PRESS: + if (state->curr_screen >= WORDLE_SCREEN_PLAYING) { + _activate(state); + } else { + movement_move_to_face(0); + } + break; + default: return movement_default_loop_handler(event); } return true; diff --git a/watch-faces/complication/wordle_face.h b/watch-faces/complication/wordle_face.h index 5cc2f09a..e4f7d0f9 100644 --- a/watch-faces/complication/wordle_face.h +++ b/watch-faces/complication/wordle_face.h @@ -83,7 +83,15 @@ * 2 = Allow using a random guess of any value that can be an answer where all of its letters are unique * 3 = Allow using a random guess of any value that can be an answer, and it's considered one of the best initial choices. */ -#define WORDLE_USE_RANDOM_GUESS 2 +#define WORDLE_USE_RANDOM_GUESS 3 +#define WORDLE_FREQ 2 +// To avoid a button press immedietly skipping a screen, we wait this many ticks +#define WORDLE_TICKS_RESULT 4 +#define WORDLE_TICK_WIN_LOSE 2 +#define WORDLE_TICK_BAD_GUESS 0 + +// Store this many words in our list of words that were already used to avoid too much repetition of guesses +#define WORDLE_MAX_BETWEEN_REPEATS 50 #include "wordle_face_dict.h" #define WORDLE_NUM_WORDS (sizeof(_valid_words) / sizeof(_valid_words[0])) @@ -95,28 +103,34 @@ typedef enum { WORDLE_LETTER_WRONG_LOC, WORDLE_LETTER_CORRECT, WORDLE_LETTER_COUNT -} WordleLetterResult; +} wordle_letter_result; typedef enum { - SCREEN_TITLE = 0, - SCREEN_STREAK, - SCREEN_CONTINUE, + WORDLE_SCREEN_TITLE = 0, + WORDLE_SCREEN_STREAK, + WORDLE_SCREEN_CONTINUE, #if WORDLE_USE_DAILY_STREAK - SCREEN_WAIT, + WORDLE_SCREEN_WAIT, #endif - SCREEN_PLAYING, - SCREEN_RESULT, - SCREEN_WIN, - SCREEN_LOSE, - SCREEN_NO_DICT, - SCREEN_ALREADY_GUESSED, - SCREEN_COUNT -} WordleScreen; + WORDLE_SCREEN_PLAYING, + WORDLE_SCREEN_RESULT, + WORDLE_SCREEN_WIN, + WORDLE_SCREEN_LOSE, + WORDLE_SCREEN_NO_DICT, + WORDLE_SCREEN_ALREADY_GUESSED, + WORDLE_SCREEN_COUNT +} wordle_screen; + +typedef enum { + BTN_MODE = 0, + BTN_ALARM, + BTN_LIGHT, +} wordle_pin_enum; typedef struct { // Anything you need to keep track of, put it here! uint8_t word_elements[WORDLE_LENGTH]; - WordleLetterResult word_elements_result[WORDLE_LENGTH]; + wordle_letter_result word_elements_result[WORDLE_LENGTH]; #if !WORDLE_ALLOW_NON_WORD_AND_REPEAT_GUESSES uint16_t guessed_words[WORDLE_MAX_ATTEMPTS]; #endif @@ -127,9 +141,12 @@ typedef struct { bool continuing : 1; bool skip_wrong_letter : 1; uint8_t streak; - WordleScreen curr_screen; + wordle_screen curr_screen; bool known_wrong_letters[WORDLE_NUM_VALID_LETTERS]; uint32_t day_last_game_started; + uint8_t ignore_btn_ticks; + uint16_t not_to_use[WORDLE_MAX_BETWEEN_REPEATS]; + uint8_t not_to_use_position; } wordle_state_t; void wordle_face_setup(uint8_t watch_face_index, void ** context_ptr); @@ -146,4 +163,3 @@ void wordle_face_resign(void *context); }) #endif // WORDLE_FACE_H_ - From befa570a9a3f59ce1a3011734d33b8e340c0fd4f Mon Sep 17 00:00:00 2001 From: David Volovskiy Date: Wed, 9 Jul 2025 07:48:59 -0400 Subject: [PATCH 003/179] Updated print logic for second movement --- watch-faces/complication/wordle_face.c | 82 +++++++++++++++----------- 1 file changed, 46 insertions(+), 36 deletions(-) diff --git a/watch-faces/complication/wordle_face.c b/watch-faces/complication/wordle_face.c index 4873b5a1..c3a2e69a 100644 --- a/watch-faces/complication/wordle_face.c +++ b/watch-faces/complication/wordle_face.c @@ -26,6 +26,7 @@ #include #include "wordle_face.h" #include "watch_utility.h" +#include "watch_common_display.h" static uint32_t get_random(uint32_t max) { #if __EMSCRIPTEN__ @@ -75,21 +76,21 @@ static void get_prev_letter(const uint8_t curr_pos, uint8_t *word_elements, cons } static void display_letter(wordle_state_t *state, bool display_dash) { - char buf[1 + 1]; + char buf[3]; if (state->word_elements[state->position] >= WORDLE_NUM_VALID_LETTERS) { if (display_dash) - watch_display_string("-", state->position + 5); + watch_display_character('-', state->position + 5); else - watch_display_string(" ", state->position + 5); + watch_display_character(' ', state->position + 5); return; } sprintf(buf, "%c", _valid_letters[state->word_elements[state->position]]); - watch_display_string(buf, state->position + 5); + watch_display_character(buf[0], state->position + 5); } static void display_all_letters(wordle_state_t *state) { uint8_t prev_pos = state->position; - watch_display_string(" ", 4); + watch_display_character(' ', 4); for (size_t i = 0; i < WORDLE_LENGTH; i++) { state->position = i; display_letter(state, false); @@ -100,13 +101,13 @@ static void display_all_letters(wordle_state_t *state) { #if !WORDLE_ALLOW_NON_WORD_AND_REPEAT_GUESSES static void display_not_in_dict(wordle_state_t *state) { state->curr_screen = WORDLE_SCREEN_NO_DICT; - watch_display_string("nodict", 4); + watch_display_text(WATCH_POSITION_BOTTOM, "nodict"); state->ignore_btn_ticks = WORDLE_TICK_BAD_GUESS; } static void display_already_guessed(wordle_state_t *state) { state->curr_screen = WORDLE_SCREEN_ALREADY_GUESSED; - watch_display_string("GUESSD", 4); + watch_display_text(WATCH_POSITION_BOTTOM, "GUESSD"); state->ignore_btn_ticks = WORDLE_TICK_BAD_GUESS; } @@ -169,9 +170,9 @@ static bool check_word(wordle_state_t *state) { static void show_skip_wrong_letter_indicator(bool skipping, wordle_screen curr_screen) { if (curr_screen >= WORDLE_SCREEN_PLAYING) return; if (skipping) - watch_display_string("H", 3); + watch_display_character('H', 3); else - watch_display_string(" ", 3); + watch_display_character(' ', 3); } static void update_known_wrong_letters(wordle_state_t *state) { @@ -199,7 +200,7 @@ static void update_known_wrong_letters(wordle_state_t *state) { static void display_attempt(uint8_t attempt) { char buf[3]; sprintf(buf, "%d", attempt+1); - watch_display_string(buf, 3); + watch_display_character(buf[0], 3); } static void display_playing(wordle_state_t *state) { @@ -247,7 +248,7 @@ static void reset_board(wordle_state_t *state) { watch_clear_colon(); state->position = get_first_pos(state->word_elements_result); display_playing(state); - watch_display_string(" -", 4); + watch_display_character('-', 5); #if __EMSCRIPTEN__ printf("ANSWER: %s\r\n", _valid_words[state->curr_answer]); #endif @@ -255,35 +256,39 @@ static void reset_board(wordle_state_t *state) { static void display_title(wordle_state_t *state) { state->curr_screen = WORDLE_SCREEN_TITLE; - watch_display_string("WO WordLE", 0); + watch_display_text(WATCH_POSITION_TOP_LEFT, "WO"); + watch_display_text(WATCH_POSITION_TOP_RIGHT, " "); + watch_display_text(WATCH_POSITION_BOTTOM, "WordLE"); show_skip_wrong_letter_indicator(state->skip_wrong_letter, state->curr_screen); } #if WORDLE_USE_DAILY_STREAK != 2 static void display_continue_result(bool continuing) { - watch_display_string(continuing ? "y" : "n", 9); + watch_display_character(continuing ? 'y' : 'n', 9); } static void display_continue(wordle_state_t *state) { state->curr_screen = WORDLE_SCREEN_CONTINUE; - watch_display_string("Cont ", 4); + watch_display_text(WATCH_POSITION_BOTTOM, "Cont "); show_skip_wrong_letter_indicator(state->skip_wrong_letter, state->curr_screen); display_continue_result(state->continuing); } #endif static void display_streak(wordle_state_t *state) { - char buf[12]; + char buf[10]; state->curr_screen = WORDLE_SCREEN_STREAK; #if WORDLE_USE_DAILY_STREAK == 2 if (state->streak > 99) - sprintf(buf, "WO St--dy"); + sprintf(buf, "St--dy"); else - sprintf(buf, "WO St%2ddy", state->streak); + sprintf(buf, "St%2ddy", state->streak); #else - sprintf(buf, "WO St%4d", state->streak); + sprintf(buf, "St%4d", state->streak); #endif - watch_display_string(buf, 0); + watch_display_text(WATCH_POSITION_TOP_LEFT, "WO"); + watch_display_text(WATCH_POSITION_TOP_RIGHT, " "); + watch_display_text(WATCH_POSITION_BOTTOM, buf); watch_set_colon(); show_skip_wrong_letter_indicator(state->skip_wrong_letter, state->curr_screen); } @@ -292,13 +297,15 @@ static void display_streak(wordle_state_t *state) { static void display_wait(wordle_state_t *state) { state->curr_screen = WORDLE_SCREEN_WAIT; if (state->streak < 40) { - char buf[13]; - sprintf(buf,"WO%2d WaIt ", state->streak); - watch_display_string(buf, 0); + char buf[5]; + sprintf(buf,"%2d", state->streak); + watch_display_text(WATCH_POSITION_TOP_RIGHT, buf); } else { // Streak too long to display in top-right - watch_display_string("WO WaIt ", 0); + watch_display_text(WATCH_POSITION_TOP_RIGHT, " "); } + watch_display_text(WATCH_POSITION_TOP_LEFT, "WO"); + watch_display_text(WATCH_POSITION_BOTTOM, " WaIt "); show_skip_wrong_letter_indicator(state->skip_wrong_letter, state->curr_screen); } #endif @@ -312,16 +319,18 @@ static uint32_t get_day_unix_time(void) { } static void display_lose(wordle_state_t *state, uint8_t subsecond) { - char buf[WORDLE_LENGTH + 6]; - sprintf(buf,"L %s", subsecond % 2 ? _valid_words[state->curr_answer] : " "); - watch_display_string(buf, 0); + char buf[10]; + sprintf(buf," %s", subsecond % 2 ? _valid_words[state->curr_answer] : " "); + watch_display_text(WATCH_POSITION_TOP, "L "); + watch_display_text(WATCH_POSITION_BOTTOM, buf); } static void display_win(wordle_state_t *state, uint8_t subsecond) { (void) state; - char buf[13]; - sprintf(buf,"W %s ", subsecond % 2 ? "NICE" : "JOb "); - watch_display_string(buf, 0); + char buf[10]; + sprintf(buf," %s ", subsecond % 2 ? "NICE" : "JOb "); + watch_display_text(WATCH_POSITION_TOP, "W "); + watch_display_text(WATCH_POSITION_BOTTOM, buf); } static bool is_playing(const wordle_state_t *state) { @@ -334,27 +343,28 @@ static bool is_playing(const wordle_state_t *state) { } static void display_result(wordle_state_t *state, uint8_t subsecond) { - char buf[WORDLE_LENGTH + 1]; + char buf[10]; + buf[0] = ' '; for (size_t i = 0; i < WORDLE_LENGTH; i++) { switch (state->word_elements_result[i]) { case WORDLE_LETTER_WRONG: - buf[i] = '-'; + buf[i+1] = '-'; break; case WORDLE_LETTER_CORRECT: - buf[i] = _valid_letters[state->word_elements[i]]; + buf[i+1] = _valid_letters[state->word_elements[i]]; break; case WORDLE_LETTER_WRONG_LOC: if (subsecond % 2) - buf[i] = ' '; + buf[i+1] = ' '; else - buf[i] = _valid_letters[state->word_elements[i]]; + buf[i+1] = _valid_letters[state->word_elements[i]]; default: break; } } - watch_display_string(buf, 5); + watch_display_text(WATCH_POSITION_BOTTOM, buf); } static bool act_on_btn(wordle_state_t *state, const wordle_pin_enum pin) { @@ -551,7 +561,7 @@ bool wordle_face_loop(movement_event_t event, void *context) { if (event.subsecond % 2) { display_letter(state, true); } else { - watch_display_string(" ", state->position + 5); + watch_display_character(' ', state->position + 5); } break; case WORDLE_SCREEN_RESULT: From 7c4a498a96e02069b7677825c45e85ea5d703af3 Mon Sep 17 00:00:00 2001 From: David Volovskiy Date: Wed, 9 Jul 2025 07:52:42 -0400 Subject: [PATCH 004/179] Removed from watch_faces --- movement_config.h | 1 - 1 file changed, 1 deletion(-) diff --git a/movement_config.h b/movement_config.h index c4cb8c36..0da8da43 100644 --- a/movement_config.h +++ b/movement_config.h @@ -29,7 +29,6 @@ const watch_face_t watch_faces[] = { clock_face, - wordle_face, world_clock_face, sunrise_sunset_face, moon_phase_face, From d0ce60111eb74e0965b1ae3dbf6bee7308778205 Mon Sep 17 00:00:00 2001 From: Eli Dowling Date: Fri, 8 Aug 2025 22:23:33 +1000 Subject: [PATCH 005/179] use 0 contrast for custom lcd this greatly improves off axis viewing, for an extremely slightly reduction in actual contrast when viewed on axis. --- watch-library/hardware/watch/watch_slcd.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/watch-library/hardware/watch/watch_slcd.c b/watch-library/hardware/watch/watch_slcd.c index db6b1942..4ba2421b 100644 --- a/watch-library/hardware/watch/watch_slcd.c +++ b/watch-library/hardware/watch/watch_slcd.c @@ -252,7 +252,7 @@ void watch_enable_display(void) { slcd_clear(); if (_installed_display == WATCH_LCD_TYPE_CUSTOM) { - slcd_set_contrast(4); + slcd_set_contrast(0); } else { slcd_set_contrast(9); } From 647457f27ac361964ad47ba847825ae1ac5ecc46 Mon Sep 17 00:00:00 2001 From: Daniel Bergman Date: Sat, 9 Aug 2025 19:52:35 +0200 Subject: [PATCH 006/179] Add a defensive check for negative values - silences compiler warnings --- watch-faces/complication/pulsometer_face.c | 1 + 1 file changed, 1 insertion(+) diff --git a/watch-faces/complication/pulsometer_face.c b/watch-faces/complication/pulsometer_face.c index 42f445af..dcef39c9 100644 --- a/watch-faces/complication/pulsometer_face.c +++ b/watch-faces/complication/pulsometer_face.c @@ -80,6 +80,7 @@ static void pulsometer_display_measurement(pulsometer_state_t *pulsometer) { char buf[5]; int16_t value = pulsometer->pulses; + if (value < 0) value = 0; if (value > 9999) value = 9999; snprintf(buf, sizeof(buf), "%-4hd", value); From 2a714c74362937530aed750d26170919f8501ff9 Mon Sep 17 00:00:00 2001 From: David Volovskiy Date: Mon, 11 Aug 2025 20:47:01 -0400 Subject: [PATCH 007/179] Moved face --- movement_faces.h | 1 + watch-faces.mk | 1 + .../complication/endless_runner_face.c | 0 .../complication/endless_runner_face.h | 0 4 files changed, 2 insertions(+) rename {legacy/watch_faces => watch-faces}/complication/endless_runner_face.c (100%) rename {legacy/watch_faces => watch-faces}/complication/endless_runner_face.h (100%) diff --git a/movement_faces.h b/movement_faces.h index fc43716d..bcd82330 100644 --- a/movement_faces.h +++ b/movement_faces.h @@ -73,4 +73,5 @@ #include "wareki_face.h" #include "deadline_face.h" #include "wordle_face.h" +#include "endless_runner_face.h" // New includes go above this line. diff --git a/watch-faces.mk b/watch-faces.mk index 22e262cf..95d81875 100644 --- a/watch-faces.mk +++ b/watch-faces.mk @@ -48,4 +48,5 @@ SRCS += \ ./watch-faces/sensor/lis2dw_monitor_face.c \ ./watch-faces/complication/wareki_face.c \ ./watch-faces/complication/deadline_face.c \ + ./watch-faces/complication/endless_runner_face.c \ # New watch faces go above this line. diff --git a/legacy/watch_faces/complication/endless_runner_face.c b/watch-faces/complication/endless_runner_face.c similarity index 100% rename from legacy/watch_faces/complication/endless_runner_face.c rename to watch-faces/complication/endless_runner_face.c diff --git a/legacy/watch_faces/complication/endless_runner_face.h b/watch-faces/complication/endless_runner_face.h similarity index 100% rename from legacy/watch_faces/complication/endless_runner_face.h rename to watch-faces/complication/endless_runner_face.h From d9466cb2d1180057acbbd9a4d0c22be26dbba305 Mon Sep 17 00:00:00 2001 From: David Volovskiy Date: Mon, 11 Aug 2025 21:05:04 -0400 Subject: [PATCH 008/179] Modified for custom display --- .../complication/endless_runner_face.c | 175 ++++++++---------- 1 file changed, 74 insertions(+), 101 deletions(-) diff --git a/watch-faces/complication/endless_runner_face.c b/watch-faces/complication/endless_runner_face.c index 1a173293..e1368707 100644 --- a/watch-faces/complication/endless_runner_face.c +++ b/watch-faces/complication/endless_runner_face.c @@ -77,6 +77,23 @@ typedef struct { uint8_t fuel; } game_state_t; +// always-on, left, right, bottom, jump-top, jump-left, jump-right +int8_t classic_ball_arr_com[] = {1, 0, 1, 0, 2, 1, 2}; +int8_t classic_ball_arr_seg[] = {20, 20, 21, 21, 20, 17, 21}; +int8_t custom_ball_arr_com[] = {2, 1, 1, 0, 3, 3, 2}; +int8_t custom_ball_arr_seg[] = {15, 15, 14, 15, 14, 15, 14}; + +// obstacle 0-11 +int8_t classic_obstacle_arr_com[] = {0, 0, 0, 1, 1, 0, 1, 0, 0, 0, 0, 0}; +int8_t classic_obstacle_arr_seg[] = {18, 19, 20, 21, 22, 23, 0, 1, 2, 4, 5, 6}; +int8_t custom_obstacle_arr_com[] = {1, 1, 1, 1, 1, 0, 1, 0, 3, 0, 0, 2}; +int8_t custom_obstacle_arr_seg[] = {22, 16, 15, 14, 1, 2, 3, 4, 4, 5, 6, 7}; + +int8_t *ball_arr_com; +int8_t *ball_arr_seg; +int8_t *obstacle_arr_com; +int8_t *obstacle_arr_seg; + static game_state_t game_state; static const uint8_t _num_bits_obst_pattern = sizeof(game_state.obst_pattern) * 8; @@ -84,7 +101,7 @@ static void print_binary(uint32_t value, int bits) { #if __EMSCRIPTEN__ for (int i = bits - 1; i >= 0; i--) { // Print each bit - printf("%lu", (value >> i) & 1); + printf("%u", (value >> i) & 1); // Optional: add a space every 4 bits for readability if (i % 4 == 0 && i != 0) { printf(" "); @@ -188,22 +205,22 @@ static uint32_t get_random_legal(uint32_t prev_val, uint16_t difficulty) { static void display_ball(bool jumping) { if (!jumping) { - watch_set_pixel(0, 21); - watch_set_pixel(1, 21); - watch_set_pixel(0, 20); - watch_set_pixel(1, 20); - watch_clear_pixel(1, 17); - watch_clear_pixel(2, 20); - watch_clear_pixel(2, 21); + watch_set_pixel(ball_arr_com[3], ball_arr_seg[3]); + watch_set_pixel(ball_arr_com[2], ball_arr_seg[2]); + watch_set_pixel(ball_arr_com[1], ball_arr_seg[1]); + watch_set_pixel(ball_arr_com[0], ball_arr_seg[0]); + watch_clear_pixel(ball_arr_com[6], ball_arr_seg[6]); + watch_clear_pixel(ball_arr_com[5], ball_arr_seg[5]); + watch_clear_pixel(ball_arr_com[4], ball_arr_seg[4]); } else { - watch_clear_pixel(0, 21); - watch_clear_pixel(1, 21); - watch_clear_pixel(0, 20); - watch_set_pixel(1, 20); - watch_set_pixel(1, 17); - watch_set_pixel(2, 20); - watch_set_pixel(2, 21); + watch_clear_pixel(ball_arr_com[3], ball_arr_seg[3]); + watch_clear_pixel(ball_arr_com[2], ball_arr_seg[2]); + watch_clear_pixel(ball_arr_com[1], ball_arr_seg[1]); + watch_set_pixel(ball_arr_com[0], ball_arr_seg[0]); + watch_set_pixel(ball_arr_com[6], ball_arr_seg[6]); + watch_set_pixel(ball_arr_com[5], ball_arr_seg[5]); + watch_set_pixel(ball_arr_com[4], ball_arr_seg[4]); } } @@ -212,12 +229,12 @@ static void display_score(uint8_t score) { if (game_state.fuel_mode) { score %= (MAX_DISP_SCORE_FUEL + 1); sprintf(buf, "%1d", score); - watch_display_string(buf, 0); + watch_display_text(WATCH_POSITION_TOP_LEFT, buf); } else { score %= (MAX_DISP_SCORE + 1); sprintf(buf, "%2d", score); - watch_display_string(buf, 2); + watch_display_text(WATCH_POSITION_TOP_RIGHT, buf); } } @@ -234,11 +251,11 @@ static void add_to_score(endless_runner_state_t *state) { static void display_fuel(uint8_t subsecond, uint8_t difficulty) { char buf[4]; if (difficulty == DIFF_FUEL_1 && game_state.fuel == 0 && subsecond % (FREQ/2) == 0) { - watch_display_string(" ", 2); // Blink the 0 fuel to show it cannot be refilled. + watch_display_text(WATCH_POSITION_TOP_RIGHT, " "); // Blink the 0 fuel to show it cannot be refilled. return; } sprintf(buf, "%2d", game_state.fuel); - watch_display_string(buf, 2); + watch_display_text(WATCH_POSITION_TOP_RIGHT, buf); } static void check_and_reset_hi_score(endless_runner_state_t *state) { @@ -255,28 +272,15 @@ static void check_and_reset_hi_score(endless_runner_state_t *state) { } static void display_difficulty(uint16_t difficulty) { - switch (difficulty) - { - case DIFF_BABY: - watch_display_string(" b", 2); - break; - case DIFF_EASY: - watch_display_string(" E", 2); - break; - case DIFF_HARD: - watch_display_string(" H", 2); - break; - case DIFF_FUEL: - watch_display_string(" F", 2); - break; - case DIFF_FUEL_1: - watch_display_string("1F", 2); - break; - case DIFF_NORM: - default: - watch_display_string(" N", 2); - break; - } + static const char *labels[] = { + [DIFF_BABY] = " b", + [DIFF_EASY] = " E", + [DIFF_HARD] = " H", + [DIFF_FUEL] = " F", + [DIFF_FUEL_1] = "1F", + [DIFF_NORM] = " N" + }; + watch_display_text(WATCH_POSITION_TOP_RIGHT, labels[difficulty]); game_state.fuel_mode = difficulty >= DIFF_FUEL && difficulty <= DIFF_FUEL_1; } @@ -309,13 +313,15 @@ static void display_title(endless_runner_state_t *state) { game_state.sec_before_moves = 1; // The first obstacles will all be 0s, which is about an extra second of delay. if (sound_on) game_state.sec_before_moves--; // Start chime is about 1 second watch_set_colon(); + watch_display_text_with_fallback(WATCH_POSITION_TOP, "RUN", "ER"); + watch_display_text(WATCH_POSITION_TOP_RIGHT, " "); if (hi_score > MAX_HI_SCORE) { - watch_display_string("ER HS --", 0); + watch_display_text(WATCH_POSITION_BOTTOM, "HS --"); } else { - char buf[14]; - sprintf(buf, "ER HS%4d", hi_score); - watch_display_string(buf, 0); + char buf[10]; + sprintf(buf, "HS%4d", hi_score); + watch_display_text(WATCH_POSITION_BOTTOM, buf); } display_difficulty(difficulty); } @@ -337,17 +343,12 @@ static void display_time(watch_date_time_t date_time, bool clock_mode_24h) { } watch_set_colon(); sprintf( buf, "%2d%02d ", hour, date_time.unit.minute); - watch_display_string(buf, 4); + watch_display_text(WATCH_POSITION_BOTTOM, buf); } - // If both digits of the minute need updating - else if ((date_time.unit.minute / 10) != (previous_date_time.unit.minute / 10)) { - sprintf( buf, "%02d ", date_time.unit.minute); - watch_display_string(buf, 6); - } - // If only the ones-place of the minute needs updating. - else if (date_time.unit.minute != previous_date_time.unit.minute) { - sprintf( buf, "%d ", date_time.unit.minute % 10); - watch_display_string(buf, 7); + // If only the minute need updating + else { + sprintf( buf, "%02d", date_time.unit.minute); + watch_display_text(WATCH_POSITION_MINUTES, buf); } previous_date_time.reg = date_time.reg; } @@ -358,14 +359,15 @@ static void begin_playing(endless_runner_state_t *state) { watch_clear_colon(); movement_request_tick_frequency((state -> difficulty == DIFF_BABY) ? FREQ_SLOW : FREQ); if (game_state.fuel_mode) { - watch_display_string(" ", 0); + watch_clear_display(); game_state.obst_pattern = get_random_fuel(0); if ((16 * JUMP_FRAMES_FUEL_RECHARGE) < JUMP_FRAMES_FUEL) // 16 frames of zeros at the start of a level game_state.fuel = JUMP_FRAMES_FUEL - (16 * JUMP_FRAMES_FUEL_RECHARGE); // Have it below its max to show it counting up when starting. if (game_state.fuel < JUMP_FRAMES_FUEL_RECHARGE) game_state.fuel = JUMP_FRAMES_FUEL_RECHARGE; } else { - watch_display_string(" ", 2); + watch_display_text(WATCH_POSITION_TOP_RIGHT, " "); + watch_display_text(WATCH_POSITION_BOTTOM, " "); game_state.obst_pattern = get_random_legal(0, difficulty); } game_state.jump_state = NOT_JUMPING; @@ -381,7 +383,8 @@ static void begin_playing(endless_runner_state_t *state) { static void display_lose_screen(endless_runner_state_t *state) { game_state.curr_screen = SCREEN_LOSE; game_state.curr_score = 0; - watch_display_string(" LOSE ", 0); + watch_clear_display(); + watch_display_text(WATCH_POSITION_BOTTOM, " LOSE "); if (state -> soundOn) watch_buzzer_play_note(BUZZER_NOTE_A1, 600); else @@ -395,9 +398,9 @@ static void display_obstacle(bool obstacle, int grid_loc, endless_runner_state_t case 2: game_state.loc_2_on = obstacle; if (obstacle) - watch_set_pixel(0, 20); + watch_set_pixel(obstacle_arr_com[grid_loc], obstacle_arr_seg[grid_loc]); else if (game_state.jump_state != NOT_JUMPING) { - watch_clear_pixel(0, 20); + watch_clear_pixel(obstacle_arr_com[grid_loc], obstacle_arr_seg[grid_loc]); if (game_state.fuel_mode && prev_obst_pos_two) add_to_score(state); } @@ -406,55 +409,20 @@ static void display_obstacle(bool obstacle, int grid_loc, endless_runner_state_t case 3: game_state.loc_3_on = obstacle; if (obstacle) - watch_set_pixel(1, 21); + watch_set_pixel(obstacle_arr_com[grid_loc], obstacle_arr_seg[grid_loc]); else if (game_state.jump_state != NOT_JUMPING) - watch_clear_pixel(1, 21); + watch_clear_pixel(obstacle_arr_com[grid_loc], obstacle_arr_seg[grid_loc]); break; case 1: if (!game_state.fuel_mode && obstacle) // If an obstacle is here, it means the ball cleared it add_to_score(state); //fall through - case 0: - case 5: - if (obstacle) - watch_set_pixel(0, 18 + grid_loc); - else - watch_clear_pixel(0, 18 + grid_loc); - break; - case 4: - if (obstacle) - watch_set_pixel(1, 22); - else - watch_clear_pixel(1, 22); - break; - case 6: - if (obstacle) - watch_set_pixel(1, 0); - else - watch_clear_pixel(1, 0); - break; - case 7: - case 8: - if (obstacle) - watch_set_pixel(0, grid_loc - 6); - else - watch_clear_pixel(0, grid_loc - 6); - break; - case 9: - case 10: - if (obstacle) - watch_set_pixel(0, grid_loc - 5); - else - watch_clear_pixel(0, grid_loc - 5); - break; - case 11: - if (obstacle) - watch_set_pixel(1, 6); - else - watch_clear_pixel(1, 6); - break; default: + if (obstacle) + watch_set_pixel(obstacle_arr_com[grid_loc], obstacle_arr_seg[grid_loc]); + else + watch_clear_pixel(obstacle_arr_com[grid_loc], obstacle_arr_seg[grid_loc]); break; } } @@ -551,6 +519,11 @@ void endless_runner_face_setup(uint8_t watch_face_index, void ** context_ptr) { void endless_runner_face_activate(void *context) { (void) context; + bool is_custom_lcd = watch_get_lcd_type() == WATCH_LCD_TYPE_CUSTOM; + ball_arr_com = is_custom_lcd ? custom_ball_arr_com : classic_ball_arr_com; + ball_arr_seg = is_custom_lcd ? custom_ball_arr_seg : classic_ball_arr_seg; + obstacle_arr_com = is_custom_lcd ? custom_obstacle_arr_com : classic_obstacle_arr_com; + obstacle_arr_seg = is_custom_lcd ? custom_obstacle_arr_seg : classic_obstacle_arr_seg; } bool endless_runner_face_loop(movement_event_t event, void *context) { From 89b58cbb795eed7082ab6446f547011fc1f98116 Mon Sep 17 00:00:00 2001 From: David Volovskiy Date: Mon, 11 Aug 2025 21:12:27 -0400 Subject: [PATCH 009/179] Fixed time display --- watch-faces/complication/endless_runner_face.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/watch-faces/complication/endless_runner_face.c b/watch-faces/complication/endless_runner_face.c index e1368707..b7e78667 100644 --- a/watch-faces/complication/endless_runner_face.c +++ b/watch-faces/complication/endless_runner_face.c @@ -573,7 +573,7 @@ bool endless_runner_face_loop(movement_event_t event, void *context) { display_title(state); break; case EVENT_LOW_ENERGY_UPDATE: - display_time(watch_rtc_get_date_time(), movement_clock_mode_24h()); + display_time(movement_get_local_date_time(), movement_clock_mode_24h()); break; default: return movement_default_loop_handler(event); From 553572db5f8c844b727ef19c1b415c69197fb3be Mon Sep 17 00:00:00 2001 From: David Volovskiy Date: Mon, 11 Aug 2025 21:15:44 -0400 Subject: [PATCH 010/179] included delay.h --- watch-faces/complication/endless_runner_face.c | 1 + 1 file changed, 1 insertion(+) diff --git a/watch-faces/complication/endless_runner_face.c b/watch-faces/complication/endless_runner_face.c index b7e78667..d3985d40 100644 --- a/watch-faces/complication/endless_runner_face.c +++ b/watch-faces/complication/endless_runner_face.c @@ -25,6 +25,7 @@ #include #include #include "endless_runner_face.h" +#include "delay.h" typedef enum { JUMPING_FINAL_FRAME = 0, From 1b503336c523a9529c1a5561c299da235faa90dc Mon Sep 17 00:00:00 2001 From: David Volovskiy Date: Mon, 11 Aug 2025 22:03:15 -0400 Subject: [PATCH 011/179] Moved face --- .../complication/higher_lower_game_face.c | 0 .../complication/higher_lower_game_face.h | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename {legacy/watch_faces => watch-faces}/complication/higher_lower_game_face.c (100%) rename {legacy/watch_faces => watch-faces}/complication/higher_lower_game_face.h (100%) diff --git a/legacy/watch_faces/complication/higher_lower_game_face.c b/watch-faces/complication/higher_lower_game_face.c similarity index 100% rename from legacy/watch_faces/complication/higher_lower_game_face.c rename to watch-faces/complication/higher_lower_game_face.c diff --git a/legacy/watch_faces/complication/higher_lower_game_face.h b/watch-faces/complication/higher_lower_game_face.h similarity index 100% rename from legacy/watch_faces/complication/higher_lower_game_face.h rename to watch-faces/complication/higher_lower_game_face.h From fc9e6c388e2794c32414ba6480d189c6bd33eb9f Mon Sep 17 00:00:00 2001 From: David Volovskiy Date: Tue, 12 Aug 2025 18:57:15 -0400 Subject: [PATCH 012/179] Fixed segmapping --- movement_faces.h | 1 + watch-faces.mk | 1 + watch-faces/complication/higher_lower_game_face.c | 13 +++++++++---- 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/movement_faces.h b/movement_faces.h index fc43716d..24509a66 100644 --- a/movement_faces.h +++ b/movement_faces.h @@ -73,4 +73,5 @@ #include "wareki_face.h" #include "deadline_face.h" #include "wordle_face.h" +#include "higher_lower_game_face.h" // New includes go above this line. diff --git a/watch-faces.mk b/watch-faces.mk index 22e262cf..11dab1d8 100644 --- a/watch-faces.mk +++ b/watch-faces.mk @@ -48,4 +48,5 @@ SRCS += \ ./watch-faces/sensor/lis2dw_monitor_face.c \ ./watch-faces/complication/wareki_face.c \ ./watch-faces/complication/deadline_face.c \ + ./watch-faces/complication/higher_lower_game_face.c \ # New watch faces go above this line. diff --git a/watch-faces/complication/higher_lower_game_face.c b/watch-faces/complication/higher_lower_game_face.c index 3491b5bd..260412d9 100755 --- a/watch-faces/complication/higher_lower_game_face.c +++ b/watch-faces/complication/higher_lower_game_face.c @@ -30,7 +30,7 @@ #include #include #include "higher_lower_game_face.h" -#include "watch_private_display.h" +#include "watch_common_display.h" #define TITLE_TEXT "Hi-Lo" #define GAME_BOARD_SIZE 6 @@ -151,9 +151,14 @@ static void init_game(void) { } static void set_segment_at_position(segment_t segment, uint8_t position) { - const uint64_t position_segment_data = (Segment_Map[position] >> (8 * (uint8_t) segment)) & 0xFF; - const uint8_t com_pin = position_segment_data >> 6; - const uint8_t seg = position_segment_data & 0x3F; + digit_mapping_t segmap; + if (watch_get_lcd_type() == WATCH_LCD_TYPE_CUSTOM) { + segmap = Custom_LCD_Display_Mapping[position]; + } else { + segmap = Classic_LCD_Display_Mapping[position]; + } + const uint8_t com_pin = segmap.segment[segment].address.com; + const uint8_t seg = segmap.segment[segment].address.seg; watch_set_pixel(com_pin, seg); } From f7e7482c491584966dbc77c2926a57b938100b78 Mon Sep 17 00:00:00 2001 From: David Volovskiy Date: Tue, 12 Aug 2025 19:43:28 -0400 Subject: [PATCH 013/179] Moved away from watch_display_string --- .../complication/higher_lower_game_face.c | 36 +++++++++---------- 1 file changed, 17 insertions(+), 19 deletions(-) diff --git a/watch-faces/complication/higher_lower_game_face.c b/watch-faces/complication/higher_lower_game_face.c index 260412d9..cfbbb98e 100755 --- a/watch-faces/complication/higher_lower_game_face.c +++ b/watch-faces/complication/higher_lower_game_face.c @@ -37,8 +37,6 @@ #define MAX_BOARDS 40 #define GUESSES_PER_SCREEN 5 #define WIN_SCORE (MAX_BOARDS * GUESSES_PER_SCREEN) -#define STATUS_DISPLAY_START 0 -#define BOARD_SCORE_DISPLAY_START 2 #define BOARD_DISPLAY_START 4 #define BOARD_DISPLAY_END 9 #define MIN_CARD_VALUE 2 @@ -141,8 +139,8 @@ static void reset_board(bool first_round) { static void init_game(void) { watch_clear_display(); - watch_display_string(TITLE_TEXT, BOARD_DISPLAY_START); - watch_display_string("GA", STATUS_DISPLAY_START); + watch_display_text(WATCH_POSITION_BOTTOM, TITLE_TEXT); + watch_display_text(WATCH_POSITION_TOP_LEFT, "HL"); reset_deck(); reset_board(true); score = 0; @@ -214,16 +212,16 @@ static void render_board_count(void) { // Render completed boards (screens) char buf[3] = {0}; snprintf(buf, sizeof(buf), "%2hhu", completed_board_count); - watch_display_string(buf, BOARD_SCORE_DISPLAY_START); + watch_display_text(WATCH_POSITION_TOP_RIGHT, buf); } static void render_final_score(void) { - watch_display_string("SC", STATUS_DISPLAY_START); + watch_display_text_with_fallback(WATCH_POSITION_TOP_LEFT, "SCO", "SC"); char buf[7] = {0}; const uint8_t complete_boards = score / GUESSES_PER_SCREEN; snprintf(buf, sizeof(buf), "%2hu %03hu", complete_boards, score); watch_set_colon(); - watch_display_string(buf, BOARD_DISPLAY_START); + watch_display_text(WATCH_POSITION_BOTTOM, buf); } static guess_t get_answer(void) { @@ -256,13 +254,13 @@ static void do_game_loop(guess_t user_guess) { // Render answer indicator switch (answer) { case HL_GUESS_EQUAL: - watch_display_string("==", STATUS_DISPLAY_START); + watch_display_text(WATCH_POSITION_TOP_LEFT, "=="); break; case HL_GUESS_HIGHER: - watch_display_string("HI", STATUS_DISPLAY_START); + watch_display_text(WATCH_POSITION_TOP_LEFT, "HI"); break; case HL_GUESS_LOWER: - watch_display_string("LO", STATUS_DISPLAY_START); + watch_display_text(WATCH_POSITION_TOP_LEFT, "LO"); break; } @@ -273,7 +271,7 @@ static void do_game_loop(guess_t user_guess) { // No score for two consecutive identical cards } else { // Incorrect guess, game over - watch_display_string("GO", STATUS_DISPLAY_START); + watch_display_text_with_fallback(WATCH_POSITION_TOP_LEFT, "End", "GO"); game_board[guess_position].revealed = true; render_board_position(guess_position); game_state = HL_GS_LOSE; @@ -282,9 +280,9 @@ static void do_game_loop(guess_t user_guess) { if (score >= WIN_SCORE) { // Win, perhaps some kind of animation sequence? - watch_display_string("WI", STATUS_DISPLAY_START); - watch_display_string(" ", BOARD_SCORE_DISPLAY_START); - watch_display_string("------", BOARD_DISPLAY_START); + watch_display_text_with_fallback(WATCH_POSITION_TOP_LEFT, "WIN", "WI"); + watch_display_text(WATCH_POSITION_TOP_RIGHT, " "); + watch_display_text(WATCH_POSITION_BOTTOM, "------"); game_state = HL_GS_WIN; return; } @@ -314,12 +312,12 @@ static void do_game_loop(guess_t user_guess) { break; case HL_GS_SHOW_SCORE: watch_clear_display(); - watch_display_string(TITLE_TEXT, BOARD_DISPLAY_START); - watch_display_string("GA", STATUS_DISPLAY_START); + watch_display_text(WATCH_POSITION_BOTTOM, TITLE_TEXT); + watch_display_text(WATCH_POSITION_TOP_LEFT, "HL"); game_state = HL_GS_TITLE_SCREEN; break; default: - watch_display_string("ERROR", BOARD_DISPLAY_START); + watch_display_text(WATCH_POSITION_BOTTOM, "ERROR"); break; } } @@ -358,8 +356,8 @@ bool higher_lower_game_face_loop(movement_event_t event, void *context) { switch (event.event_type) { case EVENT_ACTIVATE: // Show your initial UI here. - watch_display_string(TITLE_TEXT, BOARD_DISPLAY_START); - watch_display_string("GA", STATUS_DISPLAY_START); + watch_display_text(WATCH_POSITION_BOTTOM, TITLE_TEXT); + watch_display_text(WATCH_POSITION_TOP_LEFT, "HL"); break; case EVENT_TICK: // If needed, update your display here. From c803ba83b5ac6bd9672d37d0fc88b7417366cda1 Mon Sep 17 00:00:00 2001 From: David Volovskiy Date: Tue, 12 Aug 2025 20:07:03 -0400 Subject: [PATCH 014/179] Removed Aces; Added --- across screen when losing --- .../complication/higher_lower_game_face.c | 29 +++++++++++++------ 1 file changed, 20 insertions(+), 9 deletions(-) diff --git a/watch-faces/complication/higher_lower_game_face.c b/watch-faces/complication/higher_lower_game_face.c index cfbbb98e..762c462a 100755 --- a/watch-faces/complication/higher_lower_game_face.c +++ b/watch-faces/complication/higher_lower_game_face.c @@ -32,6 +32,11 @@ #include "higher_lower_game_face.h" #include "watch_common_display.h" + +#define KING 12 +#define QUEEN 11 +#define JACK 10 + #define TITLE_TEXT "Hi-Lo" #define GAME_BOARD_SIZE 6 #define MAX_BOARDS 40 @@ -40,7 +45,7 @@ #define BOARD_DISPLAY_START 4 #define BOARD_DISPLAY_END 9 #define MIN_CARD_VALUE 2 -#define MAX_CARD_VALUE 14 +#define MAX_CARD_VALUE KING #define CARD_RANK_COUNT (MAX_CARD_VALUE - MIN_CARD_VALUE + 1) #define CARD_SUIT_COUNT 4 #define DECK_SIZE (CARD_SUIT_COUNT * CARD_RANK_COUNT) @@ -160,10 +165,12 @@ static void set_segment_at_position(segment_t segment, uint8_t position) { watch_set_pixel(com_pin, seg); } +static inline size_t get_display_position(size_t board_position) { + return FLIP_BOARD_DIRECTION ? BOARD_DISPLAY_START + board_position : BOARD_DISPLAY_END - board_position; +} + static void render_board_position(size_t board_position) { - const size_t display_position = FLIP_BOARD_DIRECTION - ? BOARD_DISPLAY_START + board_position - : BOARD_DISPLAY_END - board_position; + const size_t display_position = get_display_position(board_position); const bool revealed = game_board[board_position].revealed; //// Current position indicator spot @@ -181,18 +188,18 @@ static void render_board_position(size_t board_position) { const uint8_t value = game_board[board_position].value; switch (value) { - case 14: // A (≡) + case KING: // K (≡) watch_display_character(' ', display_position); set_segment_at_position(A, display_position); set_segment_at_position(D, display_position); set_segment_at_position(G, display_position); break; - case 13: // K (=) + case QUEEN: // Q (=) watch_display_character(' ', display_position); set_segment_at_position(A, display_position); set_segment_at_position(D, display_position); break; - case 12: // Q (-) + case JACK: // J (-) watch_display_character('-', display_position); break; default: { @@ -216,7 +223,7 @@ static void render_board_count(void) { } static void render_final_score(void) { - watch_display_text_with_fallback(WATCH_POSITION_TOP_LEFT, "SCO", "SC"); + watch_display_text_with_fallback(WATCH_POSITION_TOP, "SCORE", "SC "); char buf[7] = {0}; const uint8_t complete_boards = score / GUESSES_PER_SCREEN; snprintf(buf, sizeof(buf), "%2hu %03hu", complete_boards, score); @@ -273,7 +280,11 @@ static void do_game_loop(guess_t user_guess) { // Incorrect guess, game over watch_display_text_with_fallback(WATCH_POSITION_TOP_LEFT, "End", "GO"); game_board[guess_position].revealed = true; + watch_display_text(WATCH_POSITION_BOTTOM, "------"); + render_board_position(guess_position - 1); render_board_position(guess_position); + if (game_board[guess_position].value == JACK && guess_position < GAME_BOARD_SIZE) // Adds a space in case the revealed option is '-' + watch_display_character(' ', get_display_position(guess_position + 1)); game_state = HL_GS_LOSE; return; } @@ -282,7 +293,7 @@ static void do_game_loop(guess_t user_guess) { // Win, perhaps some kind of animation sequence? watch_display_text_with_fallback(WATCH_POSITION_TOP_LEFT, "WIN", "WI"); watch_display_text(WATCH_POSITION_TOP_RIGHT, " "); - watch_display_text(WATCH_POSITION_BOTTOM, "------"); + watch_display_text_with_fallback(WATCH_POSITION_BOTTOM, "WINNER", "winnEr"); game_state = HL_GS_WIN; return; } From 9fbaadeaac81fa77b040a2bc99112d3b4a614ba8 Mon Sep 17 00:00:00 2001 From: David Volovskiy Date: Tue, 12 Aug 2025 21:22:58 -0400 Subject: [PATCH 015/179] Added lander face --- movement_faces.h | 1 + watch-faces.mk | 1 + watch-faces/complication/lander_face.c | 572 +++++++++++++++++++++++++ watch-faces/complication/lander_face.h | 152 +++++++ 4 files changed, 726 insertions(+) create mode 100644 watch-faces/complication/lander_face.c create mode 100644 watch-faces/complication/lander_face.h diff --git a/movement_faces.h b/movement_faces.h index fc43716d..7108c5e0 100644 --- a/movement_faces.h +++ b/movement_faces.h @@ -73,4 +73,5 @@ #include "wareki_face.h" #include "deadline_face.h" #include "wordle_face.h" +#include "lander_face.h" // New includes go above this line. diff --git a/watch-faces.mk b/watch-faces.mk index 22e262cf..f60a827c 100644 --- a/watch-faces.mk +++ b/watch-faces.mk @@ -48,4 +48,5 @@ SRCS += \ ./watch-faces/sensor/lis2dw_monitor_face.c \ ./watch-faces/complication/wareki_face.c \ ./watch-faces/complication/deadline_face.c \ + ./watch-faces/complication/lander_face.c \ # New watch faces go above this line. diff --git a/watch-faces/complication/lander_face.c b/watch-faces/complication/lander_face.c new file mode 100644 index 00000000..1cf81039 --- /dev/null +++ b/watch-faces/complication/lander_face.c @@ -0,0 +1,572 @@ +/* + * MIT License + * + * Copyright (c) 2024 Klingon Jane + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +// Emulator only: need time() to seed the random number generator. +#if __EMSCRIPTEN__ +#include +#endif + +#include +#include +#include +#include "lander_face.h" + +#ifndef max +#define max(x, y) ((y) > (x) ? (y) : (x)) +#endif + +#ifndef min +#define min(x, y) ((x) > (y) ? (y) : (x)) +#endif + +#define LANDER_TICK_FREQUENCY 8 +#define MONSTER_DISPLAY_TICKS 9 +#define ENGINE_THRUST 11 +#define MODE_WAITING_TO_START 0 +#define MODE_DISPLAY_SKILL_LEVEL 1 +#define MODE_PLAYING 2 +#define MODE_TOUCHDOWN_BLANK 3 +#define MODE_DISPLAY_FINAL_STATUS 4 +#define MODE_MONSTER 5 +#define MODE_FIND_EARTH_MESSAGE 6 +#define CREWS_COMPLIMENT 13 +// Granularity is divisions per foot - height display +#define GRANUL 40 +// Next lines for repeat heroes only. +#define PROMOTION_INTERVAL 3 +#define LEVEL_ACE 8 +#define LEVEL_STARBUCK 11 +#define HARD_EARTH_INCREMENTS 11 +#define MAX_HARD_EARTH_CHANCE 6 + +// The gory final result calculations: +#define SPEED_FATALITY_ALL 41 +#define SPEED_FATALITY_NONE 26 +#define SPEED_NO_DAMAGE 21 +#define SPEED_LEVEL_INCREMENTS 2 +#define SPEED_MAJOR_CRASH 73 +#define MAJOR_CRASH_INCREMENTS 65 +#define SPEED_INJURY_NONE 20 +#define SPEED_INJURY_FULCRUM 32 +#define INJURY_FULCRUM_PROB 65 +#define FUEL_SCORE_GOOD 145 +#define FUEL_SCORE_GREAT 131 +#define FUEL_SCORE_FANTASTIC 125 + +// Joey Castillo to oversee storage allocation row +#define LANDER_STORAGE_ROW 2 +#define STORAGE_KEY_NUMBER 110 + +#define DIFFICULTY_LEVELS 3 +char lander_difficulty_names[DIFFICULTY_LEVELS][7] = { + "NOrMAL", "HArd ", "HArdEr" +}; +#define MONSTER_TYPES 4 +char lander_monster_names[MONSTER_TYPES][7] = { + "mOnStr", "6Erbil", "HAmStr", "Rabbit" +}; +#define MONSTER_ACTIONS 8 +char lander_monster_actions[MONSTER_ACTIONS][7] = { + "HUn6ry", " EAtS", "6Reedy", "annoYd", "nASty ", "SAVOry", "HO66SH", " pI66Y" +}; + +// -------------- +// Custom methods +// -------------- + + +static int gen_random_int (int16_t lower, int16_t upper) { + int range; + int retVal; + range = upper - lower + 1; + if ( range < 2 ) range = 2; + // Emulator: use rand. Hardware: use arc4random. + #if __EMSCRIPTEN__ + retVal = rand() % range; + #else + retVal = arc4random_uniform(range); + #endif + retVal += lower; + return retVal; +} + +static uint8_t assignProb ( uint8_t lowerProb, uint8_t upperProb, int16_t lowerSpeed, int16_t upperSpeed, int16_t actSpeed ) { + float probRange, speedRange; + float ratio, probFloat; + int probInt; + speedRange = upperSpeed - lowerSpeed; + if (speedRange<1.0) speedRange = 1.0; + probRange = upperProb - lowerProb; + ratio = ( (float) actSpeed - (float) lowerSpeed ) / speedRange; + probFloat = (float) lowerProb + ( ratio * probRange ); + probInt = (int) ( probFloat + 0.5 ); + probInt = min ( probInt, upperProb ); + probInt = max ( probInt, lowerProb ); + return (uint8_t) probInt; +} + +static void write_to_lander_EEPROM(lander_state_t *state) { + uint8_t output_array [ 3 ]; + output_array [ 0 ] = STORAGE_KEY_NUMBER; + output_array [ 1 ] = state->hero_counter; + output_array [ 2 ] = state->legend_counter; + watch_storage_erase ( LANDER_STORAGE_ROW ); + watch_storage_sync ( ); + watch_storage_write ( LANDER_STORAGE_ROW, 0, output_array, 3 ); +} + +// --------------------------- +// Standard watch face methods +// --------------------------- +void lander_face_setup(uint8_t watch_face_index, void ** context_ptr) { + (void) watch_face_index; + if (*context_ptr == NULL) { + *context_ptr = malloc(sizeof(lander_state_t)); + memset(*context_ptr, 0, sizeof(lander_state_t)); + lander_state_t *state = (lander_state_t *)*context_ptr; + state->led_enabled = false; + } + // Emulator only: Seed random number generator + #if __EMSCRIPTEN__ + srand(time(NULL)); + #endif +} + +void lander_face_activate(void *context) { + lander_state_t *state = (lander_state_t *)context; + char buf [ 7 ]; + state->mode = MODE_WAITING_TO_START; + state->led_active = false; + state->reset_counter = 0; + watch_clear_all_indicators ( ); + uint32_t offset = 0; + uint32_t size = 3; + uint8_t stored_data [ size ]; + // See if the hero_counter was ever written to EEPROM storage + watch_storage_read (LANDER_STORAGE_ROW, offset, stored_data, size); + if (stored_data[0] == STORAGE_KEY_NUMBER ) + { + state->hero_counter = stored_data [1]; // There's real data in there. + state->legend_counter = stored_data [2]; + } + else + { + state->hero_counter = 0; // Nope. Nothing there. + state->legend_counter = 0; + write_to_lander_EEPROM(state); // Initial EEPROM tracking data. + } + state->difficulty_level = state->hero_counter / PROMOTION_INTERVAL; + state->difficulty_level = min ( state->difficulty_level, DIFFICULTY_LEVELS - 1 ); // Upper limit + // Fancy intro + if ( state->legend_counter == 0 ) watch_display_string("LA", 0); + else watch_display_string("LE", 0); + if ( ( state->hero_counter == 0 ) || ( state->hero_counter >= 40 ) ) watch_display_string ( " ", 2); + else + { + sprintf ( buf, "%2d", state->hero_counter ); + watch_display_string(buf, 2); + } + if ( state->hero_counter >= 100 ) sprintf ( buf, "Str%3d", state->hero_counter ); + else if ( state->hero_counter >= 40 ) sprintf ( buf, "Strb%2d", state->hero_counter ); + else if ( state->hero_counter >= LEVEL_STARBUCK ) sprintf ( buf, "StrbUC" ); + else if ( state->hero_counter >= LEVEL_ACE ) sprintf ( buf, " ACE " ); // This human is good + else if ( state->difficulty_level == 0 ) sprintf ( buf, " " ); + else sprintf ( buf, "%s", lander_difficulty_names[state->difficulty_level] ); + watch_display_string ( buf, 4); + if (state->led_enabled) watch_set_indicator(WATCH_INDICATOR_SIGNAL); + else watch_clear_indicator(WATCH_INDICATOR_SIGNAL); +} + +bool lander_face_loop(movement_event_t event, void *context) { + lander_state_t *state = (lander_state_t *)context; + char buf [ 20 ]; // [11] is more correct and works; compiler too helpful. + + switch (event.event_type) { + case EVENT_TICK: + state->tick_counter++; + if ( state->mode == MODE_PLAYING ) { + int16_t accel = state->gravity; + bool gas_pedal_on = HAL_GPIO_BTN_ALARM_read() || HAL_GPIO_BTN_LIGHT_read(); + if ( gas_pedal_on && ( state->fuel_remaining > 0 ) ) { + accel = ENGINE_THRUST + state->gravity; // Gravity is negative + state->fuel_remaining--; // Used 1 fuel unit + watch_set_indicator ( WATCH_INDICATOR_LAP ); + // Low fuel warning indicators + if ( state->fuel_remaining == ( 3 * LANDER_TICK_FREQUENCY ) ) { // 3 seconds of fuel left + watch_set_indicator ( WATCH_INDICATOR_SIGNAL ); + watch_set_indicator ( WATCH_INDICATOR_BELL ); + watch_set_indicator ( WATCH_INDICATOR_PM ); + watch_set_indicator ( WATCH_INDICATOR_24H ); + } + else if ( state->fuel_remaining == 0 ) { // 0 seconds of fuel left, empty! + watch_clear_all_indicators ( ); + } + } + else { + watch_clear_indicator ( WATCH_INDICATOR_LAP ); + } + state->speed += accel; + state->height += state->speed; + if ( state->height > 971 * 80 ) { // Escape height + watch_clear_all_indicators (); + watch_display_string ( "ESCAPE", 4 ); + state->tick_counter = 0; + state->mode = MODE_WAITING_TO_START; + } + else if ( state->height <= 0 ) { // Touchdown + state->tick_counter = 0; + state->mode = MODE_TOUCHDOWN_BLANK; + } + else { + // Update height display + sprintf ( buf, "%4d", (int) ( state->height / GRANUL ) ); + watch_display_string ( buf, 4 ); + } + } + else if ( state->mode == MODE_TOUCHDOWN_BLANK ) { + // Blank display on touchdown + if ( state->tick_counter == 1 ) { + watch_clear_all_indicators (); + watch_display_string ( " ", 4 ); + + // Also calc fuel score now. + float fuel_score_float; + uint16_t fuel_used; + fuel_used = state->fuel_start - state->fuel_remaining; + fuel_score_float = (float) fuel_used / (float) state->fuel_tpl; + state->fuel_score = (int) (fuel_score_float * 100.0 + 0.5); + if ( state->legend_counter == 0 ) state->fuel_score -= 8; // First Earth is easier + // Monitor reset_counter + if ( fuel_used >= 1 ) state->reset_counter = 0; + else state->reset_counter++; + if ( state->reset_counter >= 3 ) { + state->hero_counter = 0; + state->difficulty_level = 0; + if ( state->reset_counter >= 6 ) state->legend_counter = 0; + watch_display_string ( "rESET ", 4 ); + write_to_lander_EEPROM(state); + } + } + // Wait until time for next display + if ( state->tick_counter >= ( 1 * LANDER_TICK_FREQUENCY ) ) { + state->tick_counter = 0; + state->mode = MODE_DISPLAY_FINAL_STATUS; + } + } + else if ( state->mode == MODE_DISPLAY_FINAL_STATUS ) { + bool last_pass = false; + if ( state->tick_counter >= LANDER_TICK_FREQUENCY ) last_pass = true; + + // Show final status + if ( state->tick_counter == 1 ) { + // Calculate many attributes + // 1) Major crash: bug, crater, vaporized (gone). + // 2) Rank ship's health 0 to 8 + // 3) Crew fatalities and injuries + // 4) Special conditions: hero + // 5) Set fuel conservation indicators as appropriate + // 6) Set coffee maker OK indicator as appropriate + // 7) Green light if ship intact + // 8) Set standard display if not preempted. + bool allDone; + int16_t finalSpeed, boostedSpeed, levelsDamage; + int8_t shipsHealth, myRand; + uint8_t fatalities, probFatal, probInjury; + uint8_t i; + + allDone = false; + // Easiest implementation for difficulty_level is to increase touchdown speed above actual. + finalSpeed = abs ( state->speed ) + state->difficulty_level * 4; + // First Earth is a bit easier than all the others + if ( state->legend_counter == 0 ) finalSpeed -= 2; + + // 1) Major crash: bug, crater, vaporized (gone). + if ( finalSpeed >= SPEED_MAJOR_CRASH ) { + allDone = true; + shipsHealth = -1; + if ( finalSpeed >= ( SPEED_MAJOR_CRASH + 2 * MAJOR_CRASH_INCREMENTS ) ) sprintf ( buf, "GOnE " ); + else if ( finalSpeed >= ( SPEED_MAJOR_CRASH + MAJOR_CRASH_INCREMENTS ) ) sprintf ( buf, " CrAtr" ); + else sprintf ( buf, " bU6" ); + } + // 2) Rank ship's health 0 to 8 + if (!allDone) { + boostedSpeed = finalSpeed + SPEED_LEVEL_INCREMENTS - 1; + levelsDamage = (int) ( ( boostedSpeed - SPEED_NO_DAMAGE ) / SPEED_LEVEL_INCREMENTS ); + shipsHealth = 8 - levelsDamage; + shipsHealth = min ( shipsHealth, 8 ); // Keep between 0 and 8 + shipsHealth = max ( shipsHealth, 0 ); + } + state->ships_health = shipsHealth; // Remember ships health + // 3) Crew fatalities and injuries + if (!allDone) { + // Fatalies + probFatal = assignProb ( 0, 92, SPEED_FATALITY_NONE, SPEED_FATALITY_ALL, finalSpeed ); + // Injuries + if ( finalSpeed <= SPEED_INJURY_FULCRUM ) { + probInjury = assignProb ( 0, INJURY_FULCRUM_PROB, SPEED_INJURY_NONE, SPEED_INJURY_FULCRUM, finalSpeed ); + } else { + probInjury = assignProb ( INJURY_FULCRUM_PROB, 96, SPEED_INJURY_FULCRUM, SPEED_FATALITY_ALL, finalSpeed ); + } + fatalities = 0; + state->injured = 0; + for ( i = 0; i < CREWS_COMPLIMENT; i++ ) { + myRand = gen_random_int ( 1, 100 ); + if ( myRand <= probFatal ) fatalities++; + else if ( myRand <= probInjury ) state->injured++; + } + state->uninjured = CREWS_COMPLIMENT - fatalities - state->injured; + } + // 4) Special conditions: hero + if (!allDone) { + if ( (shipsHealth>=8) && ( state->fuel_score <= FUEL_SCORE_FANTASTIC ) ) { + state->hero_counter++; + if ( state->hero_counter==1 ) sprintf ( buf, "HErO " ); + else if ( state->hero_counter == LEVEL_ACE ) sprintf ( buf, " ACE " ); + else if ( state->hero_counter == LEVEL_STARBUCK ) sprintf ( buf, "STrbUC" ); + else if ( state->hero_counter>99 ) sprintf ( buf, "HEr%3d", state->hero_counter ); + else sprintf ( buf, "HErO%2d", state->hero_counter ); // Typical case + allDone = true; + // Two rule sets for finding Earth. Alternate between easy and hard. + int8_t my_odds, temp; + if ( state->legend_counter %2 == 0 ) my_odds = (int8_t) state->hero_counter - LEVEL_STARBUCK; // Easy + else { + temp = ( state->hero_counter - LEVEL_STARBUCK ) + HARD_EARTH_INCREMENTS - 1; + my_odds = temp / HARD_EARTH_INCREMENTS; + my_odds = min ( my_odds, MAX_HARD_EARTH_CHANCE ); + } + // Display odds in weekday region if positive value + if ( my_odds > 0 ) { + char buff3 [ 5 ]; + sprintf ( buff3, "%2d", my_odds ); + watch_display_string ( buff3, 2 ); + } else watch_display_string ( " ", 2 ); + if ( my_odds >= gen_random_int ( 1, 200 ) ) { // EARTH!!!! The final objective. + sprintf ( buf, "EArTH " ); // 17% within 8, 50% by 16, 79% by 24, 94% by 32 <- easy mode + state->hero_counter = 0; + state->legend_counter++; + } + // Recalculate difficulty level base on new hero_counter. + state->difficulty_level = state->hero_counter / PROMOTION_INTERVAL; + state->difficulty_level = min ( state->difficulty_level, DIFFICULTY_LEVELS - 1 ); // Upper limit + // Write to EEPROM + write_to_lander_EEPROM(state); + } + } + // 5) Set fuel conservation indicators as appropriate + if ( shipsHealth >= 1 && ( state->fuel_score <= FUEL_SCORE_FANTASTIC ) ) watch_set_indicator ( WATCH_INDICATOR_LAP ); + if ( shipsHealth >= 1 && ( state->fuel_score <= FUEL_SCORE_GREAT ) ) watch_set_indicator ( WATCH_INDICATOR_24H ); + if ( shipsHealth >= 1 && ( state->fuel_score <= FUEL_SCORE_GOOD ) ) watch_set_indicator ( WATCH_INDICATOR_PM ); + // 6) Set coffee maker OK indicator as appropriate + if ( shipsHealth >= 5 || ( shipsHealth >= 0 && ( gen_random_int ( 0, 3 ) != 1 ) ) ){ + watch_set_indicator ( WATCH_INDICATOR_SIGNAL ); + } + // 7) Green light if ship intact + if ( shipsHealth >= 8 && state->led_enabled) { + watch_set_led_green ( ); + state->led_active = true; + } + // 8) Set standard display if not preempted. + if (!allDone) { + if ( ( state->injured > 0 ) || ( state->uninjured == 0 ) ) { + sprintf ( buf, "%d %2d%2d", shipsHealth, state->uninjured, state->injured ); + } + else { + sprintf ( buf, "%d %2d ", shipsHealth, state->uninjured ); + } + } + // Display final status. + watch_display_string ( buf, 4 ); + } // End if tick_counter == 1 + + // Major crash - ship burning with red LED. + if ( state->ships_health < 0 && state->led_enabled) { + if ( ( gen_random_int ( 0, 1 ) != 1 ) && !last_pass ) { // Always off on last pass + // Turn on red LED. + watch_set_led_red ( ); + state->led_active = true; + } else { + watch_set_led_off ( ); + } + } + // Wait long enough, then allow waiting for next game. + if ( last_pass ) { + watch_set_led_off ( ); + // No change to display text, allow new game to start. + state->mode = MODE_WAITING_TO_START; + // Unless it's time for monsters + uint8_t survivors = state->injured + state->uninjured; + if ( ( state->ships_health >= 0 ) && ( survivors > 0 ) && + ( gen_random_int ( -1, 3 ) >= state->ships_health ) ) { + state->mode = MODE_MONSTER; + state->tick_counter = 0; + state->monster_type = gen_random_int ( 0, MONSTER_TYPES - 1 ); + } + } + } // End if MODE_DISPLAY_FINAL_STATUS + else if ( state->mode == MODE_DISPLAY_SKILL_LEVEL ) { + // Display skill level + if ( state->tick_counter == 1 ) { + sprintf ( buf, " %d %d ", state->skill_level, state->skill_level ); + watch_display_string ( buf, 2 ); + } + // Wait long enough, then start game. + if ( state->tick_counter >= ( 2.0 * LANDER_TICK_FREQUENCY ) ) { + state->tick_counter = 0; + // Houston, WE ARE LAUNCHING NOW.... + state->mode = MODE_PLAYING; + } + } + else if ( state->mode == MODE_FIND_EARTH_MESSAGE ) { + // Display "Find" then "Earth" + if ( state->tick_counter == 1 ) { + sprintf ( buf, " FInd " ); + watch_display_string ( buf, 2 ); + } + if ( state->tick_counter == (int) ( 1.5 * LANDER_TICK_FREQUENCY + 1 ) ) { + sprintf ( buf, " EArTH " ); + watch_display_string ( buf, 2 ); + } + // Wait long enough, then display skill level. + if ( state->tick_counter >= ( 3 * LANDER_TICK_FREQUENCY ) ) { + state->tick_counter = 0; + state->mode = MODE_DISPLAY_SKILL_LEVEL; + } + } + else if ( state->mode == MODE_MONSTER ) { + if ( state->tick_counter == 1 ) watch_display_string ( lander_monster_names[state->monster_type], 4 ); + else if ( state->tick_counter == MONSTER_DISPLAY_TICKS + 1 ) { + uint8_t my_rand; + my_rand = gen_random_int ( 0 , MONSTER_ACTIONS - 1 ); + watch_display_string ( lander_monster_actions[my_rand], 4 ); + } + else if ( state->tick_counter == MONSTER_DISPLAY_TICKS * 2 ) { // Display 1st monster character + sprintf ( buf, "%s", lander_monster_names[state->monster_type] ); + buf [1] = 0; + watch_display_string(buf,4); + } + else if ( state->tick_counter == MONSTER_DISPLAY_TICKS * 2 + 1 ) { // Display current population, close mouth + sprintf ( buf, "c%2d%2d", state->uninjured, state->injured ); + watch_display_string ( buf, 5 ); + } + else if ( state->tick_counter == MONSTER_DISPLAY_TICKS * 2 + 3 ) watch_display_string ( "C", 5 ); // Open mouth + else if ( state->tick_counter == MONSTER_DISPLAY_TICKS * 2 + 5 ) { + // Decision to: continue loop, end loop or eat astronaut + uint8_t survivors = state->injured + state->uninjured; + uint8_t myRand = gen_random_int ( 0, 16 ); + if ( survivors == 0 ) state->mode = MODE_WAITING_TO_START; + else if ( myRand <= 1 ) { // Leave loop with survivors + sprintf ( buf, "%d %2d%2d", state->ships_health, state->uninjured, state->injured ); + watch_display_string ( buf, 4 ); + state->mode = MODE_WAITING_TO_START; + } else if ( myRand <= 11 ) state->tick_counter = MONSTER_DISPLAY_TICKS * 2; // Do nothing, loop continues + else { // Eat an astronaut - welcome to the space program! + if ( state->injured > 0 && state->uninjured > 0 ) { + if ( gen_random_int ( 0,1 ) == 0 ) state->injured--; + else state->uninjured--; + } + else if ( state->injured > 0 ) state->injured--; + else state->uninjured--; + state->tick_counter = MONSTER_DISPLAY_TICKS * 2; // Re-display + } + } + else if ( state->tick_counter >= MONSTER_DISPLAY_TICKS * 4 ) state->mode = MODE_WAITING_TO_START; // Safety + } // End if MODE_MONSTER + break; // End case EVENT_TICK + case EVENT_ALARM_BUTTON_DOWN: + if ( state->mode == MODE_WAITING_TO_START ) { + // That was the go signal - start a new game!! + float numerator, denominator, timeSquared; + int16_t gravity, thrust; + float myTime, distToTop, fuel_mult; + uint8_t skill_level; + int32_t tplTop; // Top lander height for TPL calculations + movement_request_tick_frequency(LANDER_TICK_FREQUENCY); + watch_set_led_off ( ); // Safety + watch_clear_all_indicators ( ); + // Randomize starting parameters + state->height = gen_random_int ( 131, 181 ) * 80; + // Per line below; see Mars Orbiter September 23, 1999 + if ( gen_random_int ( 0, 8 ) == 5 ) state->height = gen_random_int ( 240, 800 ) * 80; + state->speed = gen_random_int ( -120, 35 ); // Positive is up + state->gravity = gen_random_int ( -3, -2 ) * 2; // negative downwards value + skill_level = gen_random_int ( 1, 4 ); // Precursor to fuel allocation + // Theoretical Perfect Landing (TPL) calculations start here. + myTime = (float) state->speed / (float) state->gravity; // How long to reach this speed? Don't care which way sign is. + distToTop = fabs ( 0.5 * state->gravity * myTime * myTime ); + tplTop = (int) ( state->height + distToTop + 0.5 ); // Theoretical highest point based on all of speed, height and gravity. + // Time squared = ( 2 * grav * height ) / ( t*t + g*t ), where t is net acceleration with thrust on. + gravity = abs ( state->gravity ); + thrust = ENGINE_THRUST + state->gravity; + numerator = 2.0 * (float) gravity * (float) tplTop; + denominator = thrust * thrust + thrust * gravity; + timeSquared = numerator / denominator; + state->fuel_tpl = (int) ( sqrt ( timeSquared ) + 0.5 ); // Fuel required for theoretical perfect landing (TPL). + if ( skill_level == 1 ) fuel_mult = 4.0; // TPL + 300% + else if ( skill_level == 2 ) fuel_mult = 2.5; // TPL + 150% + else if ( skill_level == 3 ) fuel_mult = 1.6; // TPL + 60% + else fuel_mult = 1.3; // TPL + 30% + state->fuel_start = state->fuel_tpl * fuel_mult; + state->fuel_remaining = state->fuel_start; + state->skill_level = skill_level; + state->tick_counter = 0; + if ( gen_random_int ( 1, 109 ) != 37 ) { + // Houston, approaching launch.... + state->mode = MODE_DISPLAY_SKILL_LEVEL; + } + else state->mode = MODE_FIND_EARTH_MESSAGE; + } + break; + case EVENT_LIGHT_BUTTON_DOWN: + if ( state->mode == MODE_WAITING_TO_START ) { + // Display difficulty level + watch_display_string ( lander_difficulty_names [state->difficulty_level], 4 ); + } + break; + case EVENT_LIGHT_LONG_PRESS: + if ( state->mode != MODE_WAITING_TO_START ) break; + state->led_enabled = !state->led_enabled; + if (state->led_enabled) watch_set_indicator(WATCH_INDICATOR_SIGNAL); + else watch_clear_indicator(WATCH_INDICATOR_SIGNAL); + break; + case EVENT_LIGHT_LONG_UP: + if ( ( state->mode == MODE_WAITING_TO_START ) && ( state->legend_counter > 0 ) ) { + if ( state->legend_counter > 9 ) sprintf (buf,"EArt%2d", state->legend_counter ); + else sprintf (buf,"EArth%d", state->legend_counter ); + // Display legend counter + watch_display_string ( buf, 4 ); + } + break; + + default: + movement_default_loop_handler(event); + break; + } + if ( !state->led_active ) return true; + else return false; +} + +void lander_face_resign(void *context) { + (void) context; + watch_set_led_off ( ); +} \ No newline at end of file diff --git a/watch-faces/complication/lander_face.h b/watch-faces/complication/lander_face.h new file mode 100644 index 00000000..5e4c344f --- /dev/null +++ b/watch-faces/complication/lander_face.h @@ -0,0 +1,152 @@ +/* + * MIT License + * + * Copyright (c) 2024 Klingon Jane + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +#ifndef LANDER_FACE_H_ +#define LANDER_FACE_H_ + +#include "movement.h" + +/* + +My remake of a classic planet landing game. + +Objective: Safely land the Cringeworthy. +Use your limited fuel supply to achieve a soft touch-down. + +End scenarios and ship's health: + +Hero They name this planet after you. +8 Life is very cozy. +7 +6 +5 Life is tolerable, plus some creature comforts +4 +3 Marooned. +2 +1 +0 Ship destroyed. Life is harsh, no shelter. Giant hamsters are cute. ** +Bug As in squished. +Crater They name this crater after you. +Gone As in vapourized. + +Landing display format is: +Ship's health, intact crewmen, injured crewmen. + +Additional data: +Crew's compliment: 13. +Low fuel warning icons: activates when 3 seconds of full thrust remains. +** Yes, hamsters are very cute. However; some eating of astronauts may occur. + +Starting velocity, height and gravity are randomized each scenario. +Fuel levels randomly assigned from 1 to 4 (hardest) to match starting parameters. + +A safe landing is always possible. + +End of game icons: +LAP - Fantastic budgeting of fuel supply ( Required for heroic landing status. ) +24H - Great budgeting of fuel supply +PM - Good budgeting of fuel supply +SIGNAL - The combination coffee and tea maker survived + +Landings get progressively harder with the number of heroic landings made. +Number of heroic landings are remembered. + +Heroic +Landings Status + 0 Normal + 3 Hard ( first difficulty increase ) + 6 Harder ( final difficulty increase ) + 8 Ace + 11 ?????? + +Save yourself. Save the coffee maker. + +END of standard training manual + +*/ + + +/* + +What is really going on here? +The fleet is lost. You are a newbie pilot making a name for yourself. + +Objective: Find Earth. + +After reaching ?????? status, future heroic sorties will have 'some' chance in 200 +of finding Earth. + +Your chances improve by 1 chance in 200 for each subsequent Heroic Landing (HL). + +Completing HL 12 will give you 1 chance in 200, for that landing. +HL 13 will give you 2 chances in 200, for that landing. +HL 14 will give you 3 chances in 200, for that landing. +HL 20 will give you 9 chances in 200, for that landing, and so on. + +At these higher levels, your chances in 200 are displayed in the upper right corner on a heroic landing. + +For wannabe pilots only: The HL counter can be reset by crashing three consecutive +missions without touching the thrust button. ( 6 to reset Earth-found counter ) + +Find Earth. Save Humanity. + +*/ + +typedef struct { + int32_t height; + int16_t speed; // Positive is up + uint16_t tick_counter; // For minimum delays + uint16_t fuel_start; + uint16_t fuel_remaining; + uint16_t fuel_tpl; // Fuel required for theoretical perfect landing + uint16_t fuel_score; // 100 is perfect; higher is less perfect + int8_t gravity; // negative downwards value + bool led_enabled; // Can the led be turned on? + bool led_active; // Did we use it this scenario? + uint8_t mode; // 0 Pre-launch waiting, 1 show level, 2 playing, 3 touchdown blank, 4 final display, 5 monster + uint8_t skill_level; // 1 thru 4. Dictates fuel alloted + int8_t ships_health; // 0 thru 8. -1 = major crash + uint8_t hero_counter; // Total heroic landings ever + uint8_t legend_counter; // Historic events counter ( Earth ) + uint8_t difficulty_level; // Based on hero_counter + uint8_t reset_counter; // Can reset hero_counter by crashing using zero fuel several consecutive scenarios + uint8_t monster_type; // Which monster is hungry? + uint8_t uninjured; // OK survivors + uint8_t injured; // Hurt survivors +} lander_state_t; + +void lander_face_setup(uint8_t watch_face_index, void ** context_ptr); +void lander_face_activate(void *context); +bool lander_face_loop(movement_event_t event, void *context); +void lander_face_resign(void *context); + +#define lander_face ((const watch_face_t){ \ + lander_face_setup, \ + lander_face_activate, \ + lander_face_loop, \ + lander_face_resign, \ + NULL, \ +}) + +#endif // LANDER_FACE_H_ From b8260718540cee021a26940d60558f9d78220cef Mon Sep 17 00:00:00 2001 From: David Volovskiy Date: Tue, 12 Aug 2025 22:08:28 -0400 Subject: [PATCH 016/179] Changed the text logic to watch_display_text --- watch-faces/complication/lander_face.c | 59 ++++++++++++++------------ 1 file changed, 32 insertions(+), 27 deletions(-) diff --git a/watch-faces/complication/lander_face.c b/watch-faces/complication/lander_face.c index 1cf81039..8381ff0a 100644 --- a/watch-faces/complication/lander_face.c +++ b/watch-faces/complication/lander_face.c @@ -31,6 +31,7 @@ #include #include #include "lander_face.h" +#include "watch_common_display.h" #ifndef max #define max(x, y) ((y) > (x) ? (y) : (x)) @@ -179,13 +180,13 @@ void lander_face_activate(void *context) { state->difficulty_level = state->hero_counter / PROMOTION_INTERVAL; state->difficulty_level = min ( state->difficulty_level, DIFFICULTY_LEVELS - 1 ); // Upper limit // Fancy intro - if ( state->legend_counter == 0 ) watch_display_string("LA", 0); - else watch_display_string("LE", 0); - if ( ( state->hero_counter == 0 ) || ( state->hero_counter >= 40 ) ) watch_display_string ( " ", 2); + if ( state->legend_counter == 0 ) watch_display_text(WATCH_POSITION_TOP_LEFT, "LA"); + else watch_display_text(WATCH_POSITION_TOP_LEFT, "LE"); + if ( ( state->hero_counter == 0 ) || ( state->hero_counter >= 40 ) ) watch_display_text ( WATCH_POSITION_TOP_RIGHT, " "); else { sprintf ( buf, "%2d", state->hero_counter ); - watch_display_string(buf, 2); + watch_display_text ( WATCH_POSITION_TOP_RIGHT, buf); } if ( state->hero_counter >= 100 ) sprintf ( buf, "Str%3d", state->hero_counter ); else if ( state->hero_counter >= 40 ) sprintf ( buf, "Strb%2d", state->hero_counter ); @@ -193,7 +194,7 @@ void lander_face_activate(void *context) { else if ( state->hero_counter >= LEVEL_ACE ) sprintf ( buf, " ACE " ); // This human is good else if ( state->difficulty_level == 0 ) sprintf ( buf, " " ); else sprintf ( buf, "%s", lander_difficulty_names[state->difficulty_level] ); - watch_display_string ( buf, 4); + watch_display_text ( WATCH_POSITION_BOTTOM, buf); if (state->led_enabled) watch_set_indicator(WATCH_INDICATOR_SIGNAL); else watch_clear_indicator(WATCH_INDICATOR_SIGNAL); } @@ -230,7 +231,7 @@ bool lander_face_loop(movement_event_t event, void *context) { state->height += state->speed; if ( state->height > 971 * 80 ) { // Escape height watch_clear_all_indicators (); - watch_display_string ( "ESCAPE", 4 ); + watch_display_text( WATCH_POSITION_BOTTOM, "ESCAPE" ); state->tick_counter = 0; state->mode = MODE_WAITING_TO_START; } @@ -241,14 +242,14 @@ bool lander_face_loop(movement_event_t event, void *context) { else { // Update height display sprintf ( buf, "%4d", (int) ( state->height / GRANUL ) ); - watch_display_string ( buf, 4 ); + watch_display_text( WATCH_POSITION_BOTTOM, buf ); } } else if ( state->mode == MODE_TOUCHDOWN_BLANK ) { // Blank display on touchdown if ( state->tick_counter == 1 ) { watch_clear_all_indicators (); - watch_display_string ( " ", 4 ); + watch_display_text( WATCH_POSITION_BOTTOM, " " ); // Also calc fuel score now. float fuel_score_float; @@ -264,7 +265,7 @@ bool lander_face_loop(movement_event_t event, void *context) { state->hero_counter = 0; state->difficulty_level = 0; if ( state->reset_counter >= 6 ) state->legend_counter = 0; - watch_display_string ( "rESET ", 4 ); + watch_display_text(WATCH_POSITION_BOTTOM, "rESET "); write_to_lander_EEPROM(state); } } @@ -359,8 +360,8 @@ bool lander_face_loop(movement_event_t event, void *context) { if ( my_odds > 0 ) { char buff3 [ 5 ]; sprintf ( buff3, "%2d", my_odds ); - watch_display_string ( buff3, 2 ); - } else watch_display_string ( " ", 2 ); + watch_display_text( WATCH_POSITION_TOP_RIGHT, buff3 ); + } else watch_display_text( WATCH_POSITION_TOP_RIGHT, " " ); if ( my_odds >= gen_random_int ( 1, 200 ) ) { // EARTH!!!! The final objective. sprintf ( buf, "EArTH " ); // 17% within 8, 50% by 16, 79% by 24, 94% by 32 <- easy mode state->hero_counter = 0; @@ -396,7 +397,7 @@ bool lander_face_loop(movement_event_t event, void *context) { } } // Display final status. - watch_display_string ( buf, 4 ); + watch_display_text(WATCH_POSITION_BOTTOM, buf ); } // End if tick_counter == 1 // Major crash - ship burning with red LED. @@ -427,8 +428,10 @@ bool lander_face_loop(movement_event_t event, void *context) { else if ( state->mode == MODE_DISPLAY_SKILL_LEVEL ) { // Display skill level if ( state->tick_counter == 1 ) { - sprintf ( buf, " %d %d ", state->skill_level, state->skill_level ); - watch_display_string ( buf, 2 ); + sprintf ( buf, " %d", state->skill_level ); + watch_display_text ( WATCH_POSITION_TOP_RIGHT, buf ); + sprintf ( buf, " %d ", state->skill_level ); + watch_display_text ( WATCH_POSITION_BOTTOM, buf ); } // Wait long enough, then start game. if ( state->tick_counter >= ( 2.0 * LANDER_TICK_FREQUENCY ) ) { @@ -440,12 +443,14 @@ bool lander_face_loop(movement_event_t event, void *context) { else if ( state->mode == MODE_FIND_EARTH_MESSAGE ) { // Display "Find" then "Earth" if ( state->tick_counter == 1 ) { - sprintf ( buf, " FInd " ); - watch_display_string ( buf, 2 ); + sprintf ( buf, " FInd " ); + watch_display_text ( WATCH_POSITION_TOP_RIGHT, " " ); + watch_display_text ( WATCH_POSITION_BOTTOM, buf ); } if ( state->tick_counter == (int) ( 1.5 * LANDER_TICK_FREQUENCY + 1 ) ) { - sprintf ( buf, " EArTH " ); - watch_display_string ( buf, 2 ); + sprintf ( buf, "EArTH " ); + watch_display_text ( WATCH_POSITION_TOP_RIGHT, " " ); + watch_display_text ( WATCH_POSITION_BOTTOM, buf ); } // Wait long enough, then display skill level. if ( state->tick_counter >= ( 3 * LANDER_TICK_FREQUENCY ) ) { @@ -454,22 +459,22 @@ bool lander_face_loop(movement_event_t event, void *context) { } } else if ( state->mode == MODE_MONSTER ) { - if ( state->tick_counter == 1 ) watch_display_string ( lander_monster_names[state->monster_type], 4 ); + if ( state->tick_counter == 1 ) watch_display_text ( WATCH_POSITION_BOTTOM, lander_monster_names[state->monster_type] ); else if ( state->tick_counter == MONSTER_DISPLAY_TICKS + 1 ) { uint8_t my_rand; my_rand = gen_random_int ( 0 , MONSTER_ACTIONS - 1 ); - watch_display_string ( lander_monster_actions[my_rand], 4 ); + watch_display_text ( WATCH_POSITION_BOTTOM, lander_monster_actions[my_rand] ); } else if ( state->tick_counter == MONSTER_DISPLAY_TICKS * 2 ) { // Display 1st monster character sprintf ( buf, "%s", lander_monster_names[state->monster_type] ); buf [1] = 0; - watch_display_string(buf,4); + watch_display_text(WATCH_POSITION_BOTTOM, buf); } else if ( state->tick_counter == MONSTER_DISPLAY_TICKS * 2 + 1 ) { // Display current population, close mouth - sprintf ( buf, "c%2d%2d", state->uninjured, state->injured ); - watch_display_string ( buf, 5 ); + sprintf ( buf, " c%2d%2d", state->uninjured, state->injured ); + watch_display_text ( WATCH_POSITION_BOTTOM, buf ); } - else if ( state->tick_counter == MONSTER_DISPLAY_TICKS * 2 + 3 ) watch_display_string ( "C", 5 ); // Open mouth + else if ( state->tick_counter == MONSTER_DISPLAY_TICKS * 2 + 3 ) watch_display_character ( 'C', 5 ); // Open mouth else if ( state->tick_counter == MONSTER_DISPLAY_TICKS * 2 + 5 ) { // Decision to: continue loop, end loop or eat astronaut uint8_t survivors = state->injured + state->uninjured; @@ -477,7 +482,7 @@ bool lander_face_loop(movement_event_t event, void *context) { if ( survivors == 0 ) state->mode = MODE_WAITING_TO_START; else if ( myRand <= 1 ) { // Leave loop with survivors sprintf ( buf, "%d %2d%2d", state->ships_health, state->uninjured, state->injured ); - watch_display_string ( buf, 4 ); + watch_display_text ( WATCH_POSITION_BOTTOM, buf); state->mode = MODE_WAITING_TO_START; } else if ( myRand <= 11 ) state->tick_counter = MONSTER_DISPLAY_TICKS * 2; // Do nothing, loop continues else { // Eat an astronaut - welcome to the space program! @@ -540,7 +545,7 @@ bool lander_face_loop(movement_event_t event, void *context) { case EVENT_LIGHT_BUTTON_DOWN: if ( state->mode == MODE_WAITING_TO_START ) { // Display difficulty level - watch_display_string ( lander_difficulty_names [state->difficulty_level], 4 ); + watch_display_text(WATCH_POSITION_BOTTOM, lander_difficulty_names [state->difficulty_level]); } break; case EVENT_LIGHT_LONG_PRESS: @@ -554,7 +559,7 @@ bool lander_face_loop(movement_event_t event, void *context) { if ( state->legend_counter > 9 ) sprintf (buf,"EArt%2d", state->legend_counter ); else sprintf (buf,"EArth%d", state->legend_counter ); // Display legend counter - watch_display_string ( buf, 4 ); + watch_display_text(WATCH_POSITION_BOTTOM, buf); } break; From b68ec8410f2f1364016269014b3dda23b8e7ebf6 Mon Sep 17 00:00:00 2001 From: David Volovskiy Date: Sat, 16 Aug 2025 11:33:33 -0400 Subject: [PATCH 017/179] Fixed win/lose text --- watch-faces/complication/wordle_face.c | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/watch-faces/complication/wordle_face.c b/watch-faces/complication/wordle_face.c index c3a2e69a..3ea9981b 100644 --- a/watch-faces/complication/wordle_face.c +++ b/watch-faces/complication/wordle_face.c @@ -321,15 +321,17 @@ static uint32_t get_day_unix_time(void) { static void display_lose(wordle_state_t *state, uint8_t subsecond) { char buf[10]; sprintf(buf," %s", subsecond % 2 ? _valid_words[state->curr_answer] : " "); - watch_display_text(WATCH_POSITION_TOP, "L "); + watch_display_text(WATCH_POSITION_TOP_RIGHT, " "); + watch_display_text_with_fallback(WATCH_POSITION_TOP, "LOSE", "L "); watch_display_text(WATCH_POSITION_BOTTOM, buf); } static void display_win(wordle_state_t *state, uint8_t subsecond) { (void) state; char buf[10]; - sprintf(buf," %s ", subsecond % 2 ? "NICE" : "JOb "); - watch_display_text(WATCH_POSITION_TOP, "W "); + sprintf(buf," %s ", subsecond % 2 ? "NICE" : "JOb "); + watch_display_text(WATCH_POSITION_TOP_RIGHT, " "); + watch_display_text_with_fallback(WATCH_POSITION_TOP_LEFT, "WIN", "W "); watch_display_text(WATCH_POSITION_BOTTOM, buf); } From 5d5ac6facb84aaff2303476b7333f685a7c4dbf3 Mon Sep 17 00:00:00 2001 From: "Eirik S. Morland" Date: Mon, 18 Aug 2025 21:54:53 +0200 Subject: [PATCH 018/179] Support blue led in sim --- watch-library/simulator/shell.html | 30 +++++++++++------------ watch-library/simulator/watch/watch_tcc.c | 28 ++++++++------------- 2 files changed, 25 insertions(+), 33 deletions(-) diff --git a/watch-library/simulator/shell.html b/watch-library/simulator/shell.html index 8306b919..51d289cb 100644 --- a/watch-library/simulator/shell.html +++ b/watch-library/simulator/shell.html @@ -64,22 +64,22 @@

Sensor Watch Emulator

- - - - - - - + + + + + + + - - - - + + + diff --git a/watch-library/simulator/watch/watch_tcc.c b/watch-library/simulator/watch/watch_tcc.c index f0046849..5466eebb 100644 --- a/watch-library/simulator/watch/watch_tcc.c +++ b/watch-library/simulator/watch/watch_tcc.c @@ -190,37 +190,29 @@ void watch_enable_leds(void) {} void watch_disable_leds(void) {} -void watch_set_led_color(uint8_t red, uint8_t green) { +void watch_set_led_color_rgb(uint8_t red, uint8_t green, uint8_t blue) { EM_ASM({ - // the watch svg contains an feColorMatrix filter with id ledcolor - // and a green svg gradient that mimics the led being on - // https://developer.mozilla.org/en-US/docs/Web/SVG/Element/feColorMatrix - // this changes the color of the gradient to match the red+green combination let filter = document.getElementById("ledcolor"); let color_matrix = filter.children[0].values.baseVal; - color_matrix[1].value = $0 / 255; // red value - color_matrix[6].value = $1 / 255; // green value - document.getElementById('light').style.opacity = Math.min(255, $0 + $1) / 255; - }, red, green); -} - -void watch_set_led_color_rgb(uint8_t red, uint8_t green, uint8_t blue) { - (void) blue; - watch_set_led_color(red, green); + color_matrix[0].value = $0 / 255; // red + color_matrix[6].value = $1 / 255; // green + color_matrix[12].value = $2 / 255; // blue + document.getElementById('light').style.opacity = Math.min(255, $0 + $1 + $2) / 255; + }, red, green, blue); } void watch_set_led_red(void) { - watch_set_led_color(255, 0); + watch_set_led_color_rgb(255, 0, 0); } void watch_set_led_green(void) { - watch_set_led_color(0, 255); + watch_set_led_color_rgb(0, 255, 0); } void watch_set_led_yellow(void) { - watch_set_led_color(255, 255); + watch_set_led_color_rgb(255, 255, 0); } void watch_set_led_off(void) { - watch_set_led_color(0, 0); + watch_set_led_color_rgb(0, 0, 0); } From 47c62f66fdd1687bc71037c056246a4f45dc0f20 Mon Sep 17 00:00:00 2001 From: David Volovskiy Date: Sun, 17 Aug 2025 12:54:33 -0400 Subject: [PATCH 019/179] Added tapping to endless runner --- watch-faces/complication/endless_runner_face.c | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/watch-faces/complication/endless_runner_face.c b/watch-faces/complication/endless_runner_face.c index d3985d40..6084ef61 100644 --- a/watch-faces/complication/endless_runner_face.c +++ b/watch-faces/complication/endless_runner_face.c @@ -557,6 +557,10 @@ bool endless_runner_face_loop(movement_event_t event, void *context) { if (game_state.curr_screen == SCREEN_TITLE) change_difficulty(state); break; + case EVENT_SINGLE_TAP: + case EVENT_DOUBLE_TAP: + if (state->difficulty > DIFF_HARD) break; // Don't do this on fuel modes + //fall through case EVENT_LIGHT_BUTTON_DOWN: case EVENT_ALARM_BUTTON_DOWN: if (game_state.curr_screen == SCREEN_PLAYING && game_state.jump_state == NOT_JUMPING){ From 4fe6a7bbb183271763c757d4cabf372a38de1bb1 Mon Sep 17 00:00:00 2001 From: David Volovskiy Date: Mon, 18 Aug 2025 22:10:49 -0400 Subject: [PATCH 020/179] Moved Simon face --- {legacy/watch_faces => watch-faces}/complication/simon_face.c | 0 {legacy/watch_faces => watch-faces}/complication/simon_face.h | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename {legacy/watch_faces => watch-faces}/complication/simon_face.c (100%) rename {legacy/watch_faces => watch-faces}/complication/simon_face.h (100%) diff --git a/legacy/watch_faces/complication/simon_face.c b/watch-faces/complication/simon_face.c similarity index 100% rename from legacy/watch_faces/complication/simon_face.c rename to watch-faces/complication/simon_face.c diff --git a/legacy/watch_faces/complication/simon_face.h b/watch-faces/complication/simon_face.h similarity index 100% rename from legacy/watch_faces/complication/simon_face.h rename to watch-faces/complication/simon_face.h From eeeb9865ac4692d40ec876215fb3990e02e11d40 Mon Sep 17 00:00:00 2001 From: David Volovskiy Date: Tue, 19 Aug 2025 06:59:59 -0400 Subject: [PATCH 021/179] Refactored text display --- movement_faces.h | 1 + watch-faces.mk | 1 + watch-faces/complication/simon_face.c | 42 ++++++++++++++------------- watch-faces/complication/simon_face.h | 4 +-- 4 files changed, 26 insertions(+), 22 deletions(-) diff --git a/movement_faces.h b/movement_faces.h index fc43716d..d86e72d4 100644 --- a/movement_faces.h +++ b/movement_faces.h @@ -73,4 +73,5 @@ #include "wareki_face.h" #include "deadline_face.h" #include "wordle_face.h" +#include "simon_face.h" // New includes go above this line. diff --git a/watch-faces.mk b/watch-faces.mk index 22e262cf..3f1f8946 100644 --- a/watch-faces.mk +++ b/watch-faces.mk @@ -48,4 +48,5 @@ SRCS += \ ./watch-faces/sensor/lis2dw_monitor_face.c \ ./watch-faces/complication/wareki_face.c \ ./watch-faces/complication/deadline_face.c \ + ./watch-faces/complication/simon_face.c \ # New watch faces go above this line. diff --git a/watch-faces/complication/simon_face.c b/watch-faces/complication/simon_face.c index 41bc5c3d..55f484bf 100644 --- a/watch-faces/complication/simon_face.c +++ b/watch-faces/complication/simon_face.c @@ -47,18 +47,19 @@ static inline uint8_t _simon_get_rand_num(uint8_t num_values) { } static void _simon_clear_display(simon_state_t *state) { - if (state->playing_state == SIMON_NOT_PLAYING) { - watch_display_string(" ", 0); - } else { - sprintf(_simon_display_buf, " %2d ", state->sequence_length); - watch_display_string(_simon_display_buf, 0); + watch_clear_display(); + if (state->playing_state != SIMON_NOT_PLAYING) { + sprintf(_simon_display_buf, "%2d", state->sequence_length); + watch_display_text(WATCH_POSITION_TOP_RIGHT, _simon_display_buf); } } static void _simon_not_playing_display(simon_state_t *state) { _simon_clear_display(state); - sprintf(_simon_display_buf, "SI %d", state->best_score); + watch_display_text(WATCH_POSITION_TOP, "SI"); + sprintf(_simon_display_buf, "%d", state->best_score); + watch_display_text(WATCH_POSITION_BOTTOM, _simon_display_buf); if (!state->soundOff) watch_set_indicator(WATCH_INDICATOR_BELL); else @@ -67,14 +68,13 @@ static void _simon_not_playing_display(simon_state_t *state) { watch_set_indicator(WATCH_INDICATOR_SIGNAL); else watch_clear_indicator(WATCH_INDICATOR_SIGNAL); - watch_display_string(_simon_display_buf, 0); switch (state->mode) { case SIMON_MODE_EASY: - watch_display_string("E", 9); + watch_display_text(WATCH_POSITION_SECONDS, " E"); break; case SIMON_MODE_HARD: - watch_display_string("H", 9); + watch_display_text(WATCH_POSITION_SECONDS, " H"); break; default: break; @@ -90,24 +90,27 @@ static void _simon_reset(simon_state_t *state) { static void _simon_display_note(SimonNote note, simon_state_t *state) { - char *ndtemplate = NULL; - + watch_clear_display(); + if (note == SIMON_WRONG_NOTE) { + watch_display_text(WATCH_POSITION_TOP_LEFT, "OH"); + watch_display_text(WATCH_POSITION_BOTTOM, "NOOOOO"); + return; + } + sprintf(_simon_display_buf, "%2d", state->sequence_length); + watch_display_text(WATCH_POSITION_TOP_RIGHT, _simon_display_buf); switch (note) { case SIMON_LED_NOTE: - ndtemplate = "LI%2d "; + watch_display_text(WATCH_POSITION_TOP_LEFT, "LI"); break; case SIMON_ALARM_NOTE: - ndtemplate = " %2d AL"; + watch_display_text(WATCH_POSITION_SECONDS, "AL"); break; case SIMON_MODE_NOTE: - ndtemplate = " %2dDE "; + watch_display_text(WATCH_POSITION_HOURS, DE"); + break; + default: break; - case SIMON_WRONG_NOTE: - ndtemplate = "OH NOOOOO"; } - - sprintf(_simon_display_buf, ndtemplate, state->sequence_length); - watch_display_string(_simon_display_buf, 0); } static void _simon_play_note(SimonNote note, simon_state_t *state, bool skip_rest) { @@ -220,7 +223,6 @@ void simon_face_setup(uint8_t watch_face_index, } void simon_face_activate(void *context) { - (void) settings; (void) context; simon_state_t *state = (simon_state_t *)context; _simon_change_speed(state); diff --git a/watch-faces/complication/simon_face.h b/watch-faces/complication/simon_face.h index 63e83895..a3dd743b 100644 --- a/watch-faces/complication/simon_face.h +++ b/watch-faces/complication/simon_face.h @@ -95,8 +95,8 @@ void simon_face_activate(void *context); bool simon_face_loop(movement_event_t event, void *context); void simon_face_resign(void *context); -#define simon_face \ - ((const watch_face_t){ \ +#define simon_face \ + ((const watch_face_t){ \ simon_face_setup, \ simon_face_activate, \ simon_face_loop, \ From 3220ab4a3abd11dc493b5eb83dd4e1b8f7c532c5 Mon Sep 17 00:00:00 2001 From: David Volovskiy Date: Tue, 19 Aug 2025 07:00:35 -0400 Subject: [PATCH 022/179] Added fallbacks on custom dispaly texts --- watch-faces/complication/simon_face.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/watch-faces/complication/simon_face.c b/watch-faces/complication/simon_face.c index 55f484bf..ef8b0095 100644 --- a/watch-faces/complication/simon_face.c +++ b/watch-faces/complication/simon_face.c @@ -57,7 +57,7 @@ static void _simon_clear_display(simon_state_t *state) { static void _simon_not_playing_display(simon_state_t *state) { _simon_clear_display(state); - watch_display_text(WATCH_POSITION_TOP, "SI"); + watch_display_text_with_fallback(WATCH_POSITION_TOP, "SIMON", "SI"); sprintf(_simon_display_buf, "%d", state->best_score); watch_display_text(WATCH_POSITION_BOTTOM, _simon_display_buf); if (!state->soundOff) @@ -106,7 +106,7 @@ static void _simon_display_note(SimonNote note, simon_state_t *state) { watch_display_text(WATCH_POSITION_SECONDS, "AL"); break; case SIMON_MODE_NOTE: - watch_display_text(WATCH_POSITION_HOURS, DE"); + watch_display_text_with_fallback(WATCH_POSITION_HOURS, "Md", "DE"); break; default: break; From fb6978fde87a2634827f02d6860c68f5212c9ff6 Mon Sep 17 00:00:00 2001 From: David Volovskiy Date: Tue, 19 Aug 2025 08:41:18 -0400 Subject: [PATCH 023/179] Added default temp of 25C in simulator --- movement.c | 3 +++ 1 file changed, 3 insertions(+) diff --git a/movement.c b/movement.c index 0046a489..976d811d 100644 --- a/movement.c +++ b/movement.c @@ -587,6 +587,9 @@ bool movement_set_accelerometer_motion_threshold(uint8_t new_threshold) { } float movement_get_temperature(void) { +#if __EMSCRIPTEN__ + return 25; +#endif float temperature_c = (float)0xFFFFFFFF; if (movement_state.has_thermistor) { From 4ee1c9ec2e3979aa1a1b687a2750c797ea2f60c0 Mon Sep 17 00:00:00 2001 From: David Volovskiy Date: Mon, 18 Aug 2025 20:42:22 -0400 Subject: [PATCH 024/179] Moon face and Activity face loop --- watch-faces/complication/moon_phase_face.c | 15 ++++++++++++--- watch-faces/sensor/activity_logging_face.c | 7 +++++++ 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/watch-faces/complication/moon_phase_face.c b/watch-faces/complication/moon_phase_face.c index 9e9591d3..c7a550dc 100644 --- a/watch-faces/complication/moon_phase_face.c +++ b/watch-faces/complication/moon_phase_face.c @@ -184,10 +184,19 @@ bool moon_phase_face_loop(movement_event_t event, void *context) { state->offset += 86400; _update(state, state->offset); break; - case EVENT_ALARM_LONG_PRESS: - state->offset = 0; + case EVENT_ALARM_LONG_PRESS: + state->offset = 0; _update(state, state->offset); - break; + break; + case EVENT_LIGHT_BUTTON_DOWN: + break; + case EVENT_LIGHT_BUTTON_UP: + state->offset -= 86400; + _update(state, state->offset); + break; + case EVENT_LIGHT_LONG_PRESS: + movement_illuminate_led(); + break; case EVENT_TIMEOUT: // QUESTION: Should timeout reset offset to 0? break; diff --git a/watch-faces/sensor/activity_logging_face.c b/watch-faces/sensor/activity_logging_face.c index 3d5eb1cf..65d717f1 100644 --- a/watch-faces/sensor/activity_logging_face.c +++ b/watch-faces/sensor/activity_logging_face.c @@ -86,6 +86,13 @@ void activity_logging_face_activate(void *context) { bool activity_logging_face_loop(movement_event_t event, void *context) { activity_logging_state_t *state = (activity_logging_state_t *)context; switch (event.event_type) { + case EVENT_LIGHT_LONG_PRESS: + movement_illuminate_led(); + break; + case EVENT_LIGHT_BUTTON_DOWN: + state->display_index = (state->display_index + ACTIVITY_LOGGING_NUM_DAYS - 1) % ACTIVITY_LOGGING_NUM_DAYS; + _activity_logging_face_update_display(state); + break; case EVENT_ALARM_BUTTON_DOWN: state->display_index = (state->display_index + 1) % ACTIVITY_LOGGING_NUM_DAYS; // fall through From e5c5d9f067ccc3a7337d35c788fdef74a0b2af29 Mon Sep 17 00:00:00 2001 From: David Volovskiy Date: Tue, 19 Aug 2025 08:46:11 -0400 Subject: [PATCH 025/179] Documented UI changes --- watch-faces/complication/moon_phase_face.h | 3 +++ watch-faces/sensor/activity_logging_face.h | 2 ++ 2 files changed, 5 insertions(+) diff --git a/watch-faces/complication/moon_phase_face.h b/watch-faces/complication/moon_phase_face.h index b0209829..2a32ed6b 100644 --- a/watch-faces/complication/moon_phase_face.h +++ b/watch-faces/complication/moon_phase_face.h @@ -47,6 +47,9 @@ * each button press, and both the text and the graphical representation will * display the moon phase for that day. Try pressing the Alarm button 27 times * now, just to visualize what the moon will look like over the next month. + * Pressing the Light button will move back in time. + * + * Holding the Light button will illuminate the display. */ #include "movement.h" diff --git a/watch-faces/sensor/activity_logging_face.h b/watch-faces/sensor/activity_logging_face.h index 6c5b74fb..f4f58b6e 100644 --- a/watch-faces/sensor/activity_logging_face.h +++ b/watch-faces/sensor/activity_logging_face.h @@ -40,6 +40,8 @@ * * A short press of the Alarm button moves backwards in the data log, showing yesterday's active minutes, * then the day before, etc. going back 14 days. + * A short press of the Light button moves forward in the data log, looping around if we're on the most-recent day. + * Holding the Light button will illuminate the display. * */ From b7abbc77b560d9995077e3dfd22a7833b7107caa Mon Sep 17 00:00:00 2001 From: David Volovskiy Date: Mon, 18 Aug 2025 20:27:58 -0400 Subject: [PATCH 026/179] Tarot remembers if we have major_arcana_only on or not along with number of cards to draw --- watch-faces/complication/tarot_face.c | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/watch-faces/complication/tarot_face.c b/watch-faces/complication/tarot_face.c index c10b7710..ba50ed6d 100644 --- a/watch-faces/complication/tarot_face.c +++ b/watch-faces/complication/tarot_face.c @@ -280,6 +280,9 @@ void tarot_face_setup(uint8_t watch_face_index, void ** context_ptr) { if (*context_ptr == NULL) { *context_ptr = malloc(sizeof(tarot_state_t)); memset(*context_ptr, 0, sizeof(tarot_state_t)); + tarot_state_t *state = (tarot_state_t *)*context_ptr; + state->major_arcana_only = true; + state->num_cards_to_draw = 3; } // Emulator only: Seed random number generator #if __EMSCRIPTEN__ @@ -292,8 +295,6 @@ void tarot_face_activate(void *context) { watch_display_text_with_fallback(WATCH_POSITION_TOP, "Tarot", "TA"); init_deck(state); - state->num_cards_to_draw = 3; - state->major_arcana_only = true; } bool tarot_face_loop(movement_event_t event, void *context) { From c5a71e0450cd1fdd17fb0349e075b20be2d26fce Mon Sep 17 00:00:00 2001 From: David Volovskiy Date: Tue, 19 Aug 2025 20:09:21 -0400 Subject: [PATCH 027/179] Fixed the Worlde text refresh --- watch-faces/complication/wordle_face.c | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/watch-faces/complication/wordle_face.c b/watch-faces/complication/wordle_face.c index 3ea9981b..c71c0842 100644 --- a/watch-faces/complication/wordle_face.c +++ b/watch-faces/complication/wordle_face.c @@ -256,7 +256,7 @@ static void reset_board(wordle_state_t *state) { static void display_title(wordle_state_t *state) { state->curr_screen = WORDLE_SCREEN_TITLE; - watch_display_text(WATCH_POSITION_TOP_LEFT, "WO"); + watch_display_text_with_fallback(WATCH_POSITION_TOP_LEFT, "WO ", "WO"); watch_display_text(WATCH_POSITION_TOP_RIGHT, " "); watch_display_text(WATCH_POSITION_BOTTOM, "WordLE"); show_skip_wrong_letter_indicator(state->skip_wrong_letter, state->curr_screen); @@ -286,7 +286,7 @@ static void display_streak(wordle_state_t *state) { #else sprintf(buf, "St%4d", state->streak); #endif - watch_display_text(WATCH_POSITION_TOP_LEFT, "WO"); + watch_display_text_with_fallback(WATCH_POSITION_TOP_LEFT, "WO ", "WO"); watch_display_text(WATCH_POSITION_TOP_RIGHT, " "); watch_display_text(WATCH_POSITION_BOTTOM, buf); watch_set_colon(); @@ -304,7 +304,7 @@ static void display_wait(wordle_state_t *state) { else { // Streak too long to display in top-right watch_display_text(WATCH_POSITION_TOP_RIGHT, " "); } - watch_display_text(WATCH_POSITION_TOP_LEFT, "WO"); + watch_display_text_with_fallback(WATCH_POSITION_TOP_LEFT, "WO ", "WO"); watch_display_text(WATCH_POSITION_BOTTOM, " WaIt "); show_skip_wrong_letter_indicator(state->skip_wrong_letter, state->curr_screen); } From d87bf0d73b5db32729b7d7c14c0bcb0ccb832f2e Mon Sep 17 00:00:00 2001 From: David Volovskiy Date: Tue, 19 Aug 2025 20:10:37 -0400 Subject: [PATCH 028/179] Changed custom display to Wdl --- watch-faces/complication/wordle_face.c | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/watch-faces/complication/wordle_face.c b/watch-faces/complication/wordle_face.c index c71c0842..4bc9fa15 100644 --- a/watch-faces/complication/wordle_face.c +++ b/watch-faces/complication/wordle_face.c @@ -256,7 +256,7 @@ static void reset_board(wordle_state_t *state) { static void display_title(wordle_state_t *state) { state->curr_screen = WORDLE_SCREEN_TITLE; - watch_display_text_with_fallback(WATCH_POSITION_TOP_LEFT, "WO ", "WO"); + watch_display_text_with_fallback(WATCH_POSITION_TOP_LEFT, "Wdl", "WO"); watch_display_text(WATCH_POSITION_TOP_RIGHT, " "); watch_display_text(WATCH_POSITION_BOTTOM, "WordLE"); show_skip_wrong_letter_indicator(state->skip_wrong_letter, state->curr_screen); @@ -286,7 +286,7 @@ static void display_streak(wordle_state_t *state) { #else sprintf(buf, "St%4d", state->streak); #endif - watch_display_text_with_fallback(WATCH_POSITION_TOP_LEFT, "WO ", "WO"); + watch_display_text_with_fallback(WATCH_POSITION_TOP_LEFT, "Wdl", "WO"); watch_display_text(WATCH_POSITION_TOP_RIGHT, " "); watch_display_text(WATCH_POSITION_BOTTOM, buf); watch_set_colon(); @@ -304,7 +304,7 @@ static void display_wait(wordle_state_t *state) { else { // Streak too long to display in top-right watch_display_text(WATCH_POSITION_TOP_RIGHT, " "); } - watch_display_text_with_fallback(WATCH_POSITION_TOP_LEFT, "WO ", "WO"); + watch_display_text_with_fallback(WATCH_POSITION_TOP_LEFT, "Wdl", "WO"); watch_display_text(WATCH_POSITION_BOTTOM, " WaIt "); show_skip_wrong_letter_indicator(state->skip_wrong_letter, state->curr_screen); } From 95df9a66838ae2ae3dd6dc492a0bd3ffb05a18ad Mon Sep 17 00:00:00 2001 From: David Volovskiy Date: Tue, 19 Aug 2025 20:16:43 -0400 Subject: [PATCH 029/179] Enable tap functionality --- watch-faces/complication/endless_runner_face.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/watch-faces/complication/endless_runner_face.c b/watch-faces/complication/endless_runner_face.c index 6084ef61..c749cc9d 100644 --- a/watch-faces/complication/endless_runner_face.c +++ b/watch-faces/complication/endless_runner_face.c @@ -525,6 +525,7 @@ void endless_runner_face_activate(void *context) { ball_arr_seg = is_custom_lcd ? custom_ball_arr_seg : classic_ball_arr_seg; obstacle_arr_com = is_custom_lcd ? custom_obstacle_arr_com : classic_obstacle_arr_com; obstacle_arr_seg = is_custom_lcd ? custom_obstacle_arr_seg : classic_obstacle_arr_seg; + movement_enable_tap_detection_if_available(); } bool endless_runner_face_loop(movement_event_t event, void *context) { @@ -588,5 +589,6 @@ bool endless_runner_face_loop(movement_event_t event, void *context) { void endless_runner_face_resign(void *context) { (void) context; + movement_disable_tap_detection_if_available(); } From 7b4f491db6d62ec6c17edcc67e9edf6e16577bf4 Mon Sep 17 00:00:00 2001 From: David Volovskiy Date: Tue, 19 Aug 2025 20:18:20 -0400 Subject: [PATCH 030/179] Able to start runner with a tap --- watch-faces/complication/endless_runner_face.c | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/watch-faces/complication/endless_runner_face.c b/watch-faces/complication/endless_runner_face.c index c749cc9d..1f245e8d 100644 --- a/watch-faces/complication/endless_runner_face.c +++ b/watch-faces/complication/endless_runner_face.c @@ -547,6 +547,10 @@ bool endless_runner_face_loop(movement_event_t event, void *context) { break; } break; + case EVENT_SINGLE_TAP: + case EVENT_DOUBLE_TAP: + if (state->difficulty > DIFF_HARD) break; // Don't do this on fuel modes + //fall through case EVENT_LIGHT_BUTTON_UP: case EVENT_ALARM_BUTTON_UP: if (game_state.curr_screen == SCREEN_TITLE) From 0e13674a79e717cbf6eeb7eb5ae17beceb38e2f3 Mon Sep 17 00:00:00 2001 From: David Volovskiy Date: Tue, 19 Aug 2025 21:47:51 -0400 Subject: [PATCH 031/179] Fixed duplicate cases --- watch-faces/complication/endless_runner_face.c | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/watch-faces/complication/endless_runner_face.c b/watch-faces/complication/endless_runner_face.c index 1f245e8d..7f6837ba 100644 --- a/watch-faces/complication/endless_runner_face.c +++ b/watch-faces/complication/endless_runner_face.c @@ -547,10 +547,6 @@ bool endless_runner_face_loop(movement_event_t event, void *context) { break; } break; - case EVENT_SINGLE_TAP: - case EVENT_DOUBLE_TAP: - if (state->difficulty > DIFF_HARD) break; // Don't do this on fuel modes - //fall through case EVENT_LIGHT_BUTTON_UP: case EVENT_ALARM_BUTTON_UP: if (game_state.curr_screen == SCREEN_TITLE) @@ -565,6 +561,15 @@ bool endless_runner_face_loop(movement_event_t event, void *context) { case EVENT_SINGLE_TAP: case EVENT_DOUBLE_TAP: if (state->difficulty > DIFF_HARD) break; // Don't do this on fuel modes + // Allow starting a new game by tapping. + if (game_state.curr_screen == SCREEN_TITLE) { + begin_playing(state); + break; + } + else if (game_state.curr_screen == SCREEN_LOSE) { + display_title(state); + break; + } //fall through case EVENT_LIGHT_BUTTON_DOWN: case EVENT_ALARM_BUTTON_DOWN: From 7ec68ad2189035401b7f59cd0411ed77686c265b Mon Sep 17 00:00:00 2001 From: David Volovskiy Date: Tue, 19 Aug 2025 22:00:03 -0400 Subject: [PATCH 032/179] Included delay library --- watch-faces/complication/simon_face.c | 1 + 1 file changed, 1 insertion(+) diff --git a/watch-faces/complication/simon_face.c b/watch-faces/complication/simon_face.c index ef8b0095..642b32a2 100644 --- a/watch-faces/complication/simon_face.c +++ b/watch-faces/complication/simon_face.c @@ -23,6 +23,7 @@ */ #include "simon_face.h" +#include "delay.h" #include #include #include From c0c78411df515cf44bc68a6efb4b29977248fe6f Mon Sep 17 00:00:00 2001 From: David Volovskiy Date: Sat, 23 Aug 2025 07:37:24 -0400 Subject: [PATCH 033/179] Tap enabe and disable added; fixed soundOn icon --- .../complication/endless_runner_face.c | 31 +++++++++++++++---- .../complication/endless_runner_face.h | 5 ++- 2 files changed, 29 insertions(+), 7 deletions(-) diff --git a/watch-faces/complication/endless_runner_face.c b/watch-faces/complication/endless_runner_face.c index 7f6837ba..390f54c7 100644 --- a/watch-faces/complication/endless_runner_face.c +++ b/watch-faces/complication/endless_runner_face.c @@ -305,6 +305,20 @@ static void toggle_sound(endless_runner_state_t *state) { } } +static void enable_tap_control(endless_runner_state_t *state) { + if (!state->tap_control_on) { + movement_enable_tap_detection_if_available(); + state->tap_control_on = true; + } +} + +static void disable_tap_control(endless_runner_state_t *state) { + if (state->tap_control_on) { + movement_disable_tap_detection_if_available(); + state->tap_control_on = false; + } +} + static void display_title(endless_runner_state_t *state) { uint16_t hi_score = state -> hi_score; uint8_t difficulty = state -> difficulty; @@ -325,6 +339,7 @@ static void display_title(endless_runner_state_t *state) { watch_display_text(WATCH_POSITION_BOTTOM, buf); } display_difficulty(difficulty); + if (state -> soundOn) watch_set_indicator(WATCH_INDICATOR_BELL); } static void display_time(watch_date_time_t date_time, bool clock_mode_24h) { @@ -532,8 +547,8 @@ bool endless_runner_face_loop(movement_event_t event, void *context) { endless_runner_state_t *state = (endless_runner_state_t *)context; switch (event.event_type) { case EVENT_ACTIVATE: + disable_tap_control(state); check_and_reset_hi_score(state); - if (state -> soundOn) watch_set_indicator(WATCH_INDICATOR_BELL); display_title(state); break; case EVENT_TICK: @@ -549,10 +564,13 @@ bool endless_runner_face_loop(movement_event_t event, void *context) { break; case EVENT_LIGHT_BUTTON_UP: case EVENT_ALARM_BUTTON_UP: - if (game_state.curr_screen == SCREEN_TITLE) + if (game_state.curr_screen == SCREEN_TITLE) { + enable_tap_control(state); begin_playing(state); - else if (game_state.curr_screen == SCREEN_LOSE) + } + else if (game_state.curr_screen == SCREEN_LOSE) { display_title(state); + } break; case EVENT_LIGHT_LONG_PRESS: if (game_state.curr_screen == SCREEN_TITLE) @@ -580,10 +598,11 @@ bool endless_runner_face_loop(movement_event_t event, void *context) { } break; case EVENT_ALARM_LONG_PRESS: - if (game_state.curr_screen != SCREEN_PLAYING) + if (game_state.curr_screen == SCREEN_TITLE) toggle_sound(state); break; case EVENT_TIMEOUT: + disable_tap_control(state); if (game_state.curr_screen != SCREEN_TITLE) display_title(state); break; @@ -597,7 +616,7 @@ bool endless_runner_face_loop(movement_event_t event, void *context) { } void endless_runner_face_resign(void *context) { - (void) context; - movement_disable_tap_detection_if_available(); + endless_runner_state_t *state = (endless_runner_state_t *)context; + disable_tap_control(state); } diff --git a/watch-faces/complication/endless_runner_face.h b/watch-faces/complication/endless_runner_face.h index 3cfa6814..3a17a611 100644 --- a/watch-faces/complication/endless_runner_face.h +++ b/watch-faces/complication/endless_runner_face.h @@ -33,6 +33,8 @@ This is a basic endless-runner, like the [Chrome Dino game](https://en.wikipedia.org/wiki/Dinosaur_Game). On the title screen, you can select a difficulty by long-pressing LIGHT or toggle sound by long-pressing ALARM. LED or ALARM are used to jump. + If the accelerometer is installed, you can tap the screen to jump and move through the menus after using the + buttons to go into the first game. High-score is displayed on the top-right on the title screen. During a game, the current score is displayed. */ @@ -42,7 +44,8 @@ typedef struct { uint8_t month_last_hi_score : 4; uint8_t year_last_hi_score : 6; uint8_t soundOn : 1; - /* 24 bits, likely aligned to 32 bits = 4 bytes */ + uint8_t tap_control_on : 1; + uint8_t unused : 7; } endless_runner_state_t; void endless_runner_face_setup(uint8_t watch_face_index, void ** context_ptr); From a769b2968775cae34b3fba875ea101e4d4fe7a92 Mon Sep 17 00:00:00 2001 From: David Volovskiy Date: Sat, 23 Aug 2025 07:55:12 -0400 Subject: [PATCH 034/179] Fixed up start chime --- watch-faces/complication/endless_runner_face.c | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/watch-faces/complication/endless_runner_face.c b/watch-faces/complication/endless_runner_face.c index 390f54c7..4588c2a3 100644 --- a/watch-faces/complication/endless_runner_face.c +++ b/watch-faces/complication/endless_runner_face.c @@ -369,10 +369,18 @@ static void display_time(watch_date_time_t date_time, bool clock_mode_24h) { previous_date_time.reg = date_time.reg; } +int8_t start_tune[] = { + BUZZER_NOTE_C5, 15, + BUZZER_NOTE_E5, 15, + BUZZER_NOTE_G5, 15, + 0 +}; + static void begin_playing(endless_runner_state_t *state) { uint8_t difficulty = state -> difficulty; game_state.curr_screen = SCREEN_PLAYING; watch_clear_colon(); + watch_clear_indicator(WATCH_INDICATOR_BELL); movement_request_tick_frequency((state -> difficulty == DIFF_BABY) ? FREQ_SLOW : FREQ); if (game_state.fuel_mode) { watch_clear_display(); @@ -390,9 +398,7 @@ static void begin_playing(endless_runner_state_t *state) { display_ball(game_state.jump_state != NOT_JUMPING); display_score( game_state.curr_score); if (state -> soundOn){ - watch_buzzer_play_note(BUZZER_NOTE_C5, 200); - watch_buzzer_play_note(BUZZER_NOTE_E5, 200); - watch_buzzer_play_note(BUZZER_NOTE_G5, 200); + watch_buzzer_play_sequence(start_tune, NULL); } } From 3dada3e9b5d551612658ad23ea57822be4b153cc Mon Sep 17 00:00:00 2001 From: David Volovskiy Date: Sat, 23 Aug 2025 08:04:09 -0400 Subject: [PATCH 035/179] Referenicng local time function --- watch-faces/complication/endless_runner_face.c | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/watch-faces/complication/endless_runner_face.c b/watch-faces/complication/endless_runner_face.c index 4588c2a3..d4aebf69 100644 --- a/watch-faces/complication/endless_runner_face.c +++ b/watch-faces/complication/endless_runner_face.c @@ -261,7 +261,7 @@ static void display_fuel(uint8_t subsecond, uint8_t difficulty) { static void check_and_reset_hi_score(endless_runner_state_t *state) { // Resets the hi score at the beginning of each month. - watch_date_time_t date_time = watch_rtc_get_date_time(); + watch_date_time_t date_time = movement_get_local_date_time(); if ((state -> year_last_hi_score != date_time.unit.year) || (state -> month_last_hi_score != date_time.unit.month)) { @@ -536,6 +536,7 @@ void endless_runner_face_setup(uint8_t watch_face_index, void ** context_ptr) { memset(*context_ptr, 0, sizeof(endless_runner_state_t)); endless_runner_state_t *state = (endless_runner_state_t *)*context_ptr; state->difficulty = DIFF_NORM; + state->tap_control_on = false; } } @@ -576,7 +577,7 @@ bool endless_runner_face_loop(movement_event_t event, void *context) { } else if (game_state.curr_screen == SCREEN_LOSE) { display_title(state); - } + } break; case EVENT_LIGHT_LONG_PRESS: if (game_state.curr_screen == SCREEN_TITLE) @@ -625,4 +626,3 @@ void endless_runner_face_resign(void *context) { endless_runner_state_t *state = (endless_runner_state_t *)context; disable_tap_control(state); } - From a71d48d5d3681259b5f00a5967e9be8113175c5f Mon Sep 17 00:00:00 2001 From: David Volovskiy Date: Sat, 23 Aug 2025 09:41:42 -0400 Subject: [PATCH 036/179] Corrected obstacle on classic face --- watch-faces/complication/endless_runner_face.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/watch-faces/complication/endless_runner_face.c b/watch-faces/complication/endless_runner_face.c index d4aebf69..aff3fd69 100644 --- a/watch-faces/complication/endless_runner_face.c +++ b/watch-faces/complication/endless_runner_face.c @@ -85,7 +85,7 @@ int8_t custom_ball_arr_com[] = {2, 1, 1, 0, 3, 3, 2}; int8_t custom_ball_arr_seg[] = {15, 15, 14, 15, 14, 15, 14}; // obstacle 0-11 -int8_t classic_obstacle_arr_com[] = {0, 0, 0, 1, 1, 0, 1, 0, 0, 0, 0, 0}; +int8_t classic_obstacle_arr_com[] = {0, 0, 0, 1, 1, 0, 1, 0, 0, 0, 0, 1}; int8_t classic_obstacle_arr_seg[] = {18, 19, 20, 21, 22, 23, 0, 1, 2, 4, 5, 6}; int8_t custom_obstacle_arr_com[] = {1, 1, 1, 1, 1, 0, 1, 0, 3, 0, 0, 2}; int8_t custom_obstacle_arr_seg[] = {22, 16, 15, 14, 1, 2, 3, 4, 4, 5, 6, 7}; From 358ddeaea9882dc4d52690d8778035b2aeb44c7b Mon Sep 17 00:00:00 2001 From: David Volovskiy Date: Sat, 23 Aug 2025 11:31:59 -0400 Subject: [PATCH 037/179] Added watch_disable_display --- watch-library/hardware/watch/watch_slcd.c | 4 ++++ watch-library/shared/watch/watch_slcd.h | 4 ++++ watch-library/simulator/watch/watch_slcd.c | 14 ++++++++++---- 3 files changed, 18 insertions(+), 4 deletions(-) diff --git a/watch-library/hardware/watch/watch_slcd.c b/watch-library/hardware/watch/watch_slcd.c index db6b1942..457962cb 100644 --- a/watch-library/hardware/watch/watch_slcd.c +++ b/watch-library/hardware/watch/watch_slcd.c @@ -260,6 +260,10 @@ void watch_enable_display(void) { slcd_enable(); } +void watch_disable_display(void) { + slcd_disable(); +} + inline void watch_set_pixel(uint8_t com, uint8_t seg) { slcd_set_segment(com, seg); } diff --git a/watch-library/shared/watch/watch_slcd.h b/watch-library/shared/watch/watch_slcd.h index b94b3f80..43a4f849 100644 --- a/watch-library/shared/watch/watch_slcd.h +++ b/watch-library/shared/watch/watch_slcd.h @@ -98,6 +98,10 @@ typedef enum { */ void watch_enable_display(void); +/** @brief Disables the Segment LCD display. + */ +void watch_disable_display(void); + /** @brief Sets a pixel. Use this to manually set a pixel with a given common and segment number. * See segmap.html. * @param com the common pin, numbered from 0-2. diff --git a/watch-library/simulator/watch/watch_slcd.c b/watch-library/simulator/watch/watch_slcd.c index 34d8878c..1990e90c 100644 --- a/watch-library/simulator/watch/watch_slcd.c +++ b/watch-library/simulator/watch/watch_slcd.c @@ -50,17 +50,23 @@ void watch_enable_display(void) { _watch_update_indicator_segments(); #endif - EM_ASM({ #if defined(FORCE_CUSTOM_LCD_TYPE) - document.getElementById("classic").style.display = "none"; + EM_ASM({document.getElementById("custom").style.display = "";}); + EM_ASM({document.getElementById("classic").style.display = "none";}); #else - document.getElementById("custom").style.display = "none"; + EM_ASM({document.getElementById("custom").style.display = "none";}); + EM_ASM({document.getElementById("classic").style.display = "";}); #endif - }); watch_clear_display(); } +void watch_disable_display(void) { + watch_clear_display(); + EM_ASM({document.getElementById("classic").style.display = "none";}); + EM_ASM({document.getElementById("custom").style.display = "none";}); +} + void watch_set_pixel(uint8_t com, uint8_t seg) { EM_ASM({ document.querySelectorAll("[data-com='" + $0 + "'][data-seg='" + $1 + "']") From 877d9721903916680d42e6dd21a0b4cd9534eec6 Mon Sep 17 00:00:00 2001 From: David Volovskiy Date: Sat, 23 Aug 2025 11:45:53 -0400 Subject: [PATCH 038/179] Temperature now can be set by simulator --- movement.c | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/movement.c b/movement.c index 976d811d..dac3c08c 100644 --- a/movement.c +++ b/movement.c @@ -588,7 +588,10 @@ bool movement_set_accelerometer_motion_threshold(uint8_t new_threshold) { float movement_get_temperature(void) { #if __EMSCRIPTEN__ - return 25; +#include + return EM_ASM_DOUBLE({ + return temp_c || 25.0; + }); #endif float temperature_c = (float)0xFFFFFFFF; From 018c94f23d9407cc99bae030137bc0391aa11966 Mon Sep 17 00:00:00 2001 From: David Volovskiy Date: Sun, 24 Aug 2025 08:58:49 -0400 Subject: [PATCH 039/179] movement_get_temperature no longer returns early in case logic after reading will ever be needed --- movement.c | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/movement.c b/movement.c index dac3c08c..cf64f90e 100644 --- a/movement.c +++ b/movement.c @@ -587,13 +587,13 @@ bool movement_set_accelerometer_motion_threshold(uint8_t new_threshold) { } float movement_get_temperature(void) { + float temperature_c = (float)0xFFFFFFFF; #if __EMSCRIPTEN__ #include - return EM_ASM_DOUBLE({ + temperature_c = EM_ASM_DOUBLE({ return temp_c || 25.0; }); -#endif - float temperature_c = (float)0xFFFFFFFF; +#else if (movement_state.has_thermistor) { thermistor_driver_enable(); @@ -604,6 +604,7 @@ float movement_get_temperature(void) { val = val >> 4; temperature_c = 25 + (float)val / 16.0; } +#endif return temperature_c; } From d5eb32bc7466495fcb5e873213a9dd6c3c105d81 Mon Sep 17 00:00:00 2001 From: David Volovskiy Date: Wed, 27 Aug 2025 07:42:54 -0400 Subject: [PATCH 040/179] Removed duplicate includes --- legacy/watch_faces/clock/world_clock2_face.c | 1 - movement.c | 2 -- 2 files changed, 3 deletions(-) diff --git a/legacy/watch_faces/clock/world_clock2_face.c b/legacy/watch_faces/clock/world_clock2_face.c index e79f2df7..e47bc44d 100644 --- a/legacy/watch_faces/clock/world_clock2_face.c +++ b/legacy/watch_faces/clock/world_clock2_face.c @@ -28,7 +28,6 @@ #include "world_clock2_face.h" #include "watch.h" #include "watch_utility.h" -#include "watch_utility.h" static bool refresh_face; diff --git a/movement.c b/movement.c index ca0b0f36..4b4a1676 100644 --- a/movement.c +++ b/movement.c @@ -27,9 +27,7 @@ #include #include #include -#include #include -#include #include "app.h" #include "watch.h" #include "watch_utility.h" From 6374045c690fe2d244eb85cac2966712b9050423 Mon Sep 17 00:00:00 2001 From: David Volovskiy Date: Wed, 27 Aug 2025 07:44:59 -0400 Subject: [PATCH 041/179] Removed duplicate include --- movement.c | 1 - 1 file changed, 1 deletion(-) diff --git a/movement.c b/movement.c index cf64f90e..b35621f7 100644 --- a/movement.c +++ b/movement.c @@ -589,7 +589,6 @@ bool movement_set_accelerometer_motion_threshold(uint8_t new_threshold) { float movement_get_temperature(void) { float temperature_c = (float)0xFFFFFFFF; #if __EMSCRIPTEN__ -#include temperature_c = EM_ASM_DOUBLE({ return temp_c || 25.0; }); From ba98aab4bd3dd73ae2d7aa23412826727e874087 Mon Sep 17 00:00:00 2001 From: David Volovskiy Date: Wed, 27 Aug 2025 08:06:52 -0400 Subject: [PATCH 042/179] Blink colon on showing time in custom display --- watch-faces/complication/endless_runner_face.c | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/watch-faces/complication/endless_runner_face.c b/watch-faces/complication/endless_runner_face.c index aff3fd69..0849c27b 100644 --- a/watch-faces/complication/endless_runner_face.c +++ b/watch-faces/complication/endless_runner_face.c @@ -346,6 +346,9 @@ static void display_time(watch_date_time_t date_time, bool clock_mode_24h) { static watch_date_time_t previous_date_time; char buf[6 + 1]; + if (!watch_sleep_animation_is_running()) { + watch_start_indicator_blink_if_possible(WATCH_INDICATOR_COLON, 500); + } // If the hour needs updating or it's the first time displaying the time if ((game_state.curr_screen != SCREEN_TIME) || (date_time.unit.hour != previous_date_time.unit.hour)) { uint8_t hour = date_time.unit.hour; @@ -358,7 +361,7 @@ static void display_time(watch_date_time_t date_time, bool clock_mode_24h) { if (hour == 0) hour = 12; } watch_set_colon(); - sprintf( buf, "%2d%02d ", hour, date_time.unit.minute); + sprintf( buf, movement_clock_mode_24h() == MOVEMENT_CLOCK_MODE_024H ? "%02d%02d " : "%2d%02d ", hour, date_time.unit.minute); watch_display_text(WATCH_POSITION_BOTTOM, buf); } // If only the minute need updating From 61b65c1d1768692592ca304f68a9fa4e0c69b773 Mon Sep 17 00:00:00 2001 From: David Volovskiy Date: Wed, 27 Aug 2025 08:45:16 -0400 Subject: [PATCH 043/179] MOVEMENT_CLOCK_MODE_12H works on the time display --- .../complication/endless_runner_face.c | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/watch-faces/complication/endless_runner_face.c b/watch-faces/complication/endless_runner_face.c index 0849c27b..6fb9b9c5 100644 --- a/watch-faces/complication/endless_runner_face.c +++ b/watch-faces/complication/endless_runner_face.c @@ -342,26 +342,27 @@ static void display_title(endless_runner_state_t *state) { if (state -> soundOn) watch_set_indicator(WATCH_INDICATOR_BELL); } -static void display_time(watch_date_time_t date_time, bool clock_mode_24h) { +static void display_time(void) { static watch_date_time_t previous_date_time; + watch_date_time_t date_time = movement_get_local_date_time(); + movement_clock_mode_t clock_mode_24h = movement_clock_mode_24h(); char buf[6 + 1]; - if (!watch_sleep_animation_is_running()) { - watch_start_indicator_blink_if_possible(WATCH_INDICATOR_COLON, 500); - } // If the hour needs updating or it's the first time displaying the time if ((game_state.curr_screen != SCREEN_TIME) || (date_time.unit.hour != previous_date_time.unit.hour)) { uint8_t hour = date_time.unit.hour; game_state.curr_screen = SCREEN_TIME; - - if (clock_mode_24h) watch_set_indicator(WATCH_INDICATOR_24H); + if (!watch_sleep_animation_is_running()) { + watch_set_colon(); + watch_start_indicator_blink_if_possible(WATCH_INDICATOR_COLON, 500); + } + if (clock_mode_24h != MOVEMENT_CLOCK_MODE_12H) watch_set_indicator(WATCH_INDICATOR_24H); else { if (hour >= 12) watch_set_indicator(WATCH_INDICATOR_PM); hour %= 12; if (hour == 0) hour = 12; } - watch_set_colon(); - sprintf( buf, movement_clock_mode_24h() == MOVEMENT_CLOCK_MODE_024H ? "%02d%02d " : "%2d%02d ", hour, date_time.unit.minute); + sprintf( buf, clock_mode_24h == MOVEMENT_CLOCK_MODE_024H ? "%02d%02d " : "%2d%02d ", hour, date_time.unit.minute); watch_display_text(WATCH_POSITION_BOTTOM, buf); } // If only the minute need updating @@ -617,7 +618,7 @@ bool endless_runner_face_loop(movement_event_t event, void *context) { display_title(state); break; case EVENT_LOW_ENERGY_UPDATE: - display_time(movement_get_local_date_time(), movement_clock_mode_24h()); + display_time(); break; default: return movement_default_loop_handler(event); From 71d7b4495b5063ff37c52a414b978dc379569468 Mon Sep 17 00:00:00 2001 From: David Volovskiy Date: Wed, 27 Aug 2025 09:03:31 -0400 Subject: [PATCH 044/179] Stop blink animation when leaving sleep mode --- watch-faces/complication/endless_runner_face.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/watch-faces/complication/endless_runner_face.c b/watch-faces/complication/endless_runner_face.c index 6fb9b9c5..f43a831b 100644 --- a/watch-faces/complication/endless_runner_face.c +++ b/watch-faces/complication/endless_runner_face.c @@ -551,7 +551,6 @@ void endless_runner_face_activate(void *context) { ball_arr_seg = is_custom_lcd ? custom_ball_arr_seg : classic_ball_arr_seg; obstacle_arr_com = is_custom_lcd ? custom_obstacle_arr_com : classic_obstacle_arr_com; obstacle_arr_seg = is_custom_lcd ? custom_obstacle_arr_seg : classic_obstacle_arr_seg; - movement_enable_tap_detection_if_available(); } bool endless_runner_face_loop(movement_event_t event, void *context) { @@ -559,6 +558,7 @@ bool endless_runner_face_loop(movement_event_t event, void *context) { switch (event.event_type) { case EVENT_ACTIVATE: disable_tap_control(state); + clock_stop_tick_tock_animation(); check_and_reset_hi_score(state); display_title(state); break; From 2770b8b924cb46308f17114cdc4c4aebad8a1d3f Mon Sep 17 00:00:00 2001 From: David Volovskiy Date: Wed, 27 Aug 2025 09:09:44 -0400 Subject: [PATCH 045/179] Fixed proper usage of blinkng --- watch-faces/complication/endless_runner_face.c | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/watch-faces/complication/endless_runner_face.c b/watch-faces/complication/endless_runner_face.c index f43a831b..a3f7e371 100644 --- a/watch-faces/complication/endless_runner_face.c +++ b/watch-faces/complication/endless_runner_face.c @@ -558,7 +558,9 @@ bool endless_runner_face_loop(movement_event_t event, void *context) { switch (event.event_type) { case EVENT_ACTIVATE: disable_tap_control(state); - clock_stop_tick_tock_animation(); + if (watch_sleep_animation_is_running()) { + watch_stop_blink(); + } check_and_reset_hi_score(state); display_title(state); break; From db2d5cbf68e3585342ff361e3f277d4a6c9a3bb1 Mon Sep 17 00:00:00 2001 From: David Volovskiy Date: Wed, 27 Aug 2025 17:17:11 -0400 Subject: [PATCH 046/179] Moved sleep animation check into the function rather than the switch statement --- watch-faces/complication/endless_runner_face.c | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/watch-faces/complication/endless_runner_face.c b/watch-faces/complication/endless_runner_face.c index a3f7e371..a0dc85db 100644 --- a/watch-faces/complication/endless_runner_face.c +++ b/watch-faces/complication/endless_runner_face.c @@ -551,6 +551,9 @@ void endless_runner_face_activate(void *context) { ball_arr_seg = is_custom_lcd ? custom_ball_arr_seg : classic_ball_arr_seg; obstacle_arr_com = is_custom_lcd ? custom_obstacle_arr_com : classic_obstacle_arr_com; obstacle_arr_seg = is_custom_lcd ? custom_obstacle_arr_seg : classic_obstacle_arr_seg; + if (watch_sleep_animation_is_running()) { + watch_stop_blink(); + } } bool endless_runner_face_loop(movement_event_t event, void *context) { @@ -558,9 +561,6 @@ bool endless_runner_face_loop(movement_event_t event, void *context) { switch (event.event_type) { case EVENT_ACTIVATE: disable_tap_control(state); - if (watch_sleep_animation_is_running()) { - watch_stop_blink(); - } check_and_reset_hi_score(state); display_title(state); break; From d24ebce9da1d38957be37321bffe7cd9e1c03649 Mon Sep 17 00:00:00 2001 From: David Volovskiy Date: Sat, 30 Aug 2025 10:45:06 -0400 Subject: [PATCH 047/179] Added lose_tune --- .../complication/endless_runner_face.c | 23 ++++++++++++------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/watch-faces/complication/endless_runner_face.c b/watch-faces/complication/endless_runner_face.c index a0dc85db..cd4f7249 100644 --- a/watch-faces/complication/endless_runner_face.c +++ b/watch-faces/complication/endless_runner_face.c @@ -98,6 +98,20 @@ int8_t *obstacle_arr_seg; static game_state_t game_state; static const uint8_t _num_bits_obst_pattern = sizeof(game_state.obst_pattern) * 8; +int8_t start_tune[] = { + BUZZER_NOTE_C5, 15, + BUZZER_NOTE_E5, 15, + BUZZER_NOTE_G5, 15, + 0 +}; + +int8_t lose_tune[] = { + BUZZER_NOTE_D3, 10, + BUZZER_NOTE_C3SHARP_D3FLAT, 10, + BUZZER_NOTE_C3, 10, + 0 +}; + static void print_binary(uint32_t value, int bits) { #if __EMSCRIPTEN__ for (int i = bits - 1; i >= 0; i--) { @@ -373,13 +387,6 @@ static void display_time(void) { previous_date_time.reg = date_time.reg; } -int8_t start_tune[] = { - BUZZER_NOTE_C5, 15, - BUZZER_NOTE_E5, 15, - BUZZER_NOTE_G5, 15, - 0 -}; - static void begin_playing(endless_runner_state_t *state) { uint8_t difficulty = state -> difficulty; game_state.curr_screen = SCREEN_PLAYING; @@ -412,7 +419,7 @@ static void display_lose_screen(endless_runner_state_t *state) { watch_clear_display(); watch_display_text(WATCH_POSITION_BOTTOM, " LOSE "); if (state -> soundOn) - watch_buzzer_play_note(BUZZER_NOTE_A1, 600); + watch_buzzer_play_sequence(lose_tune, NULL); else delay_ms(600); } From 8fbb2d48b0686f33a95dcb4eed584fbca7f3ba9d Mon Sep 17 00:00:00 2001 From: David Volovskiy Date: Sat, 30 Aug 2025 10:03:40 -0400 Subject: [PATCH 048/179] Added title screen to endless runner --- .../complication/endless_runner_face.c | 32 ++++++++++++------- 1 file changed, 21 insertions(+), 11 deletions(-) diff --git a/watch-faces/complication/endless_runner_face.c b/watch-faces/complication/endless_runner_face.c index cd4f7249..9f951d31 100644 --- a/watch-faces/complication/endless_runner_face.c +++ b/watch-faces/complication/endless_runner_face.c @@ -35,6 +35,7 @@ typedef enum { typedef enum { SCREEN_TITLE = 0, + SCREEN_SCORE, SCREEN_PLAYING, SCREEN_LOSE, SCREEN_TIME, @@ -334,11 +335,19 @@ static void disable_tap_control(endless_runner_state_t *state) { } static void display_title(endless_runner_state_t *state) { + game_state.curr_screen = SCREEN_TITLE; + watch_clear_colon(); + watch_display_text_with_fallback(WATCH_POSITION_TOP, "ENdLS", "ER "); + watch_display_text(WATCH_POSITION_BOTTOM, "RUNNER"); + if (state -> soundOn) watch_set_indicator(WATCH_INDICATOR_BELL); +} + +static void display_score_screen(endless_runner_state_t *state) { uint16_t hi_score = state -> hi_score; uint8_t difficulty = state -> difficulty; bool sound_on = state -> soundOn; - game_state.curr_screen = SCREEN_TITLE; memset(&game_state, 0, sizeof(game_state)); + game_state.curr_screen = SCREEN_SCORE; game_state.sec_before_moves = 1; // The first obstacles will all be 0s, which is about an extra second of delay. if (sound_on) game_state.sec_before_moves--; // Start chime is about 1 second watch_set_colon(); @@ -353,7 +362,7 @@ static void display_title(endless_runner_state_t *state) { watch_display_text(WATCH_POSITION_BOTTOM, buf); } display_difficulty(difficulty); - if (state -> soundOn) watch_set_indicator(WATCH_INDICATOR_BELL); + if (sound_on) watch_set_indicator(WATCH_INDICATOR_BELL); } static void display_time(void) { @@ -575,6 +584,7 @@ bool endless_runner_face_loop(movement_event_t event, void *context) { switch (game_state.curr_screen) { case SCREEN_TITLE: + case SCREEN_SCORE: case SCREEN_LOSE: break; default: @@ -584,28 +594,28 @@ bool endless_runner_face_loop(movement_event_t event, void *context) { break; case EVENT_LIGHT_BUTTON_UP: case EVENT_ALARM_BUTTON_UP: - if (game_state.curr_screen == SCREEN_TITLE) { + if (game_state.curr_screen == SCREEN_SCORE) { enable_tap_control(state); begin_playing(state); } - else if (game_state.curr_screen == SCREEN_LOSE) { - display_title(state); + else if (game_state.curr_screen == SCREEN_TITLE || game_state.curr_screen == SCREEN_LOSE) { + display_score_screen(state); } break; case EVENT_LIGHT_LONG_PRESS: - if (game_state.curr_screen == SCREEN_TITLE) + if (game_state.curr_screen == SCREEN_SCORE) change_difficulty(state); break; case EVENT_SINGLE_TAP: case EVENT_DOUBLE_TAP: if (state->difficulty > DIFF_HARD) break; // Don't do this on fuel modes // Allow starting a new game by tapping. - if (game_state.curr_screen == SCREEN_TITLE) { + if (game_state.curr_screen == SCREEN_SCORE) { begin_playing(state); break; } else if (game_state.curr_screen == SCREEN_LOSE) { - display_title(state); + display_score_screen(state); break; } //fall through @@ -618,13 +628,13 @@ bool endless_runner_face_loop(movement_event_t event, void *context) { } break; case EVENT_ALARM_LONG_PRESS: - if (game_state.curr_screen == SCREEN_TITLE) + if (game_state.curr_screen == SCREEN_TITLE || game_state.curr_screen == SCREEN_SCORE) toggle_sound(state); break; case EVENT_TIMEOUT: disable_tap_control(state); - if (game_state.curr_screen != SCREEN_TITLE) - display_title(state); + if (game_state.curr_screen != SCREEN_SCORE) + display_score_screen(state); break; case EVENT_LOW_ENERGY_UPDATE: display_time(); From 8c5921538502c86b88e81503a6676d123407943b Mon Sep 17 00:00:00 2001 From: David Volovskiy Date: Sun, 31 Aug 2025 09:35:36 -0400 Subject: [PATCH 049/179] Better enterring of time mode and sound indicator during game --- .../complication/endless_runner_face.c | 28 +++++++++++++------ 1 file changed, 19 insertions(+), 9 deletions(-) diff --git a/watch-faces/complication/endless_runner_face.c b/watch-faces/complication/endless_runner_face.c index 9f951d31..4b22262f 100644 --- a/watch-faces/complication/endless_runner_face.c +++ b/watch-faces/complication/endless_runner_face.c @@ -309,14 +309,19 @@ static void change_difficulty(endless_runner_state_t *state) { } } +static void display_sound_indicator(bool soundOn) { + if (soundOn){ + watch_set_indicator(WATCH_INDICATOR_BELL); + } else { + watch_clear_indicator(WATCH_INDICATOR_BELL); + } +} + static void toggle_sound(endless_runner_state_t *state) { state -> soundOn = !state -> soundOn; + display_sound_indicator(state -> soundOn); if (state -> soundOn){ watch_buzzer_play_note(BUZZER_NOTE_C5, 30); - watch_set_indicator(WATCH_INDICATOR_BELL); - } - else { - watch_clear_indicator(WATCH_INDICATOR_BELL); } } @@ -339,7 +344,7 @@ static void display_title(endless_runner_state_t *state) { watch_clear_colon(); watch_display_text_with_fallback(WATCH_POSITION_TOP, "ENdLS", "ER "); watch_display_text(WATCH_POSITION_BOTTOM, "RUNNER"); - if (state -> soundOn) watch_set_indicator(WATCH_INDICATOR_BELL); + display_sound_indicator(state -> soundOn); } static void display_score_screen(endless_runner_state_t *state) { @@ -351,8 +356,7 @@ static void display_score_screen(endless_runner_state_t *state) { game_state.sec_before_moves = 1; // The first obstacles will all be 0s, which is about an extra second of delay. if (sound_on) game_state.sec_before_moves--; // Start chime is about 1 second watch_set_colon(); - watch_display_text_with_fallback(WATCH_POSITION_TOP, "RUN", "ER"); - watch_display_text(WATCH_POSITION_TOP_RIGHT, " "); + watch_display_text_with_fallback(WATCH_POSITION_TOP, "RUN ", "ER "); if (hi_score > MAX_HI_SCORE) { watch_display_text(WATCH_POSITION_BOTTOM, "HS --"); } @@ -362,7 +366,7 @@ static void display_score_screen(endless_runner_state_t *state) { watch_display_text(WATCH_POSITION_BOTTOM, buf); } display_difficulty(difficulty); - if (sound_on) watch_set_indicator(WATCH_INDICATOR_BELL); + display_sound_indicator(sound_on); } static void display_time(void) { @@ -400,7 +404,7 @@ static void begin_playing(endless_runner_state_t *state) { uint8_t difficulty = state -> difficulty; game_state.curr_screen = SCREEN_PLAYING; watch_clear_colon(); - watch_clear_indicator(WATCH_INDICATOR_BELL); + display_sound_indicator(state -> soundOn); movement_request_tick_frequency((state -> difficulty == DIFF_BABY) ? FREQ_SLOW : FREQ); if (game_state.fuel_mode) { watch_clear_display(); @@ -586,6 +590,7 @@ bool endless_runner_face_loop(movement_event_t event, void *context) { case SCREEN_TITLE: case SCREEN_SCORE: case SCREEN_LOSE: + case SCREEN_TIME: break; default: update_game(state, event.subsecond); @@ -637,6 +642,11 @@ bool endless_runner_face_loop(movement_event_t event, void *context) { display_score_screen(state); break; case EVENT_LOW_ENERGY_UPDATE: + if (game_state.curr_screen != SCREEN_TIME) { + watch_display_text_with_fallback(WATCH_POSITION_TOP, "RUN ", "ER "); + display_sound_indicator(state -> soundOn); + display_difficulty(state->difficulty); + } display_time(); break; default: From e6b8be467630137d9890f0c096f00c869a8f1d24 Mon Sep 17 00:00:00 2001 From: David Volovskiy Date: Sun, 31 Aug 2025 09:44:46 -0400 Subject: [PATCH 050/179] Lose chime plays fully when sound is on --- watch-faces/complication/endless_runner_face.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/watch-faces/complication/endless_runner_face.c b/watch-faces/complication/endless_runner_face.c index 4b22262f..5d479b9a 100644 --- a/watch-faces/complication/endless_runner_face.c +++ b/watch-faces/complication/endless_runner_face.c @@ -431,10 +431,10 @@ static void display_lose_screen(endless_runner_state_t *state) { game_state.curr_score = 0; watch_clear_display(); watch_display_text(WATCH_POSITION_BOTTOM, " LOSE "); - if (state -> soundOn) + if (state -> soundOn) { watch_buzzer_play_sequence(lose_tune, NULL); - else delay_ms(600); + } } static void display_obstacle(bool obstacle, int grid_loc, endless_runner_state_t *state) { From 2bce9bba65e1bb75b0b94e2961064cce31c9a37d Mon Sep 17 00:00:00 2001 From: David Volovskiy Date: Sun, 31 Aug 2025 16:28:59 -0400 Subject: [PATCH 051/179] Enable endless runner tapping right after leaving the title screen --- watch-faces/complication/endless_runner_face.c | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/watch-faces/complication/endless_runner_face.c b/watch-faces/complication/endless_runner_face.c index 5d479b9a..8a4b7a3b 100644 --- a/watch-faces/complication/endless_runner_face.c +++ b/watch-faces/complication/endless_runner_face.c @@ -599,12 +599,18 @@ bool endless_runner_face_loop(movement_event_t event, void *context) { break; case EVENT_LIGHT_BUTTON_UP: case EVENT_ALARM_BUTTON_UP: - if (game_state.curr_screen == SCREEN_SCORE) { - enable_tap_control(state); - begin_playing(state); - } - else if (game_state.curr_screen == SCREEN_TITLE || game_state.curr_screen == SCREEN_LOSE) { - display_score_screen(state); + switch (game_state.curr_screen) { + case SCREEN_SCORE: + enable_tap_control(state); + begin_playing(state); + break; + case SCREEN_TITLE: + enable_tap_control(state); + // fall through + case SCREEN_TIME: + case SCREEN_LOSE: + watch_clear_display(); + display_score_screen(state); } break; case EVENT_LIGHT_LONG_PRESS: From 4da0e4bd6d82c822c5287e29ac59e342bce88410 Mon Sep 17 00:00:00 2001 From: David Volovskiy Date: Mon, 1 Sep 2025 09:41:51 -0400 Subject: [PATCH 052/179] Fixed Sunday text --- watch-faces/complication/advanced_alarm_face.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/watch-faces/complication/advanced_alarm_face.c b/watch-faces/complication/advanced_alarm_face.c index 39739bc4..26f6eb5e 100644 --- a/watch-faces/complication/advanced_alarm_face.c +++ b/watch-faces/complication/advanced_alarm_face.c @@ -40,7 +40,7 @@ typedef enum { alarm_setting_idx_beeps } alarm_setting_idx_t; -static const char _dow_strings[ALARM_DAY_STATES + 1][2] ={"AL", "MO", "TU", "WE", "TH", "FR", "SA", "SO", "ED", "1t", "MF", "WN"}; +static const char _dow_strings[ALARM_DAY_STATES + 1][2] ={"AL", "MO", "TU", "WE", "TH", "FR", "SA", "SU", "ED", "1t", "MF", "WN"}; static const uint8_t _blink_idx[ALARM_SETTING_STATES] = {2, 0, 4, 6, 8, 9}; static const uint8_t _blink_idx2[ALARM_SETTING_STATES] = {3, 1, 5, 7, 8, 9}; static const watch_buzzer_note_t _buzzer_notes[3] = {BUZZER_NOTE_B6, BUZZER_NOTE_C8, BUZZER_NOTE_A8}; From f43dd33febe0639f9c7fd56cd8f638e907c1b7b7 Mon Sep 17 00:00:00 2001 From: David Volovskiy Date: Mon, 1 Sep 2025 09:43:23 -0400 Subject: [PATCH 053/179] Added capability to display longer days on custom display --- .../complication/advanced_alarm_face.c | 35 ++++++++++++++----- 1 file changed, 26 insertions(+), 9 deletions(-) diff --git a/watch-faces/complication/advanced_alarm_face.c b/watch-faces/complication/advanced_alarm_face.c index 26f6eb5e..146386c0 100644 --- a/watch-faces/complication/advanced_alarm_face.c +++ b/watch-faces/complication/advanced_alarm_face.c @@ -40,7 +40,8 @@ typedef enum { alarm_setting_idx_beeps } alarm_setting_idx_t; -static const char _dow_strings[ALARM_DAY_STATES + 1][2] ={"AL", "MO", "TU", "WE", "TH", "FR", "SA", "SU", "ED", "1t", "MF", "WN"}; +static const char _dow_strings_classic[ALARM_DAY_STATES + 1][2] ={"AL", "MO", "TU", "WE", "TH", "FR", "SA", "SU", "ED", "1t", "MF", "WN"}; +static const char _dow_strings_custom[ALARM_DAY_STATES + 1][3] ={ "AL ", "MON", "TUE", "WED", "THU", "FRI", "SAT", "SUN", "DAY", "1t ", "M-F", "WKD"}; static const uint8_t _blink_idx[ALARM_SETTING_STATES] = {2, 0, 4, 6, 8, 9}; static const uint8_t _blink_idx2[ALARM_SETTING_STATES] = {3, 1, 5, 7, 8, 9}; static const watch_buzzer_note_t _buzzer_notes[3] = {BUZZER_NOTE_B6, BUZZER_NOTE_C8, BUZZER_NOTE_A8}; @@ -90,18 +91,34 @@ static void _advanced_alarm_face_draw(alarm_state_t *state, uint8_t subsecond) { watch_set_indicator(WATCH_INDICATOR_24H); } - sprintf(buf, set_leading_zero? "%c%c%2d%02d%02d " : "%c%c%2d%2d%02d ", - _dow_strings[i][0], _dow_strings[i][1], - (state->alarm_idx + 1), - h, - state->alarm[state->alarm_idx].minute); // blink items if in settings mode - if (state->is_setting && subsecond % 2 && state->setting_state < alarm_setting_idx_pitch && !state->alarm_quick_ticks) { - buf[_blink_idx[state->setting_state]] = buf[_blink_idx2[state->setting_state]] = ' '; + bool blinking = state->is_setting && subsecond % 2 && state->setting_state < alarm_setting_idx_pitch && !state->alarm_quick_ticks; + if (state->setting_state == alarm_setting_idx_alarm && blinking) { + watch_display_text(WATCH_POSITION_TOP_RIGHT, " "); + } else { + sprintf(buf, "%2d", (state->alarm_idx + 1)); + watch_display_text(WATCH_POSITION_TOP_RIGHT, buf); + } + if (state->setting_state == alarm_setting_idx_day && blinking) { + watch_display_text_with_fallback(WATCH_POSITION_TOP_LEFT, " ", " "); + } else { + watch_display_text_with_fallback(WATCH_POSITION_TOP_LEFT, _dow_strings_custom[i], _dow_strings_classic[i]); + } + if (state->setting_state == alarm_setting_idx_hour && blinking) { + watch_display_text(WATCH_POSITION_HOURS, " "); + } else { + sprintf(buf, set_leading_zero? "%02d" : "%2d", h); + watch_display_text(WATCH_POSITION_HOURS, buf); + } + if (state->setting_state == alarm_setting_idx_minute && blinking) { + watch_display_text(WATCH_POSITION_MINUTES, " "); + } else { + sprintf(buf, "%02d", state->alarm[state->alarm_idx].minute); + watch_display_text(WATCH_POSITION_MINUTES, buf); } - watch_display_text(WATCH_POSITION_FULL, buf); if (state->is_setting) { + watch_display_text(WATCH_POSITION_SECONDS, " "); // draw pitch level indicator if ((subsecond % 2) == 0 || (state->setting_state != alarm_setting_idx_pitch)) { for (i = 0; i <= state->alarm[state->alarm_idx].pitch && i < 3; i++) From 562ed5158d070a1cc9ad90417cbad95c658607b0 Mon Sep 17 00:00:00 2001 From: David Volovskiy Date: Mon, 1 Sep 2025 09:43:56 -0400 Subject: [PATCH 054/179] Made showing which alarms are on in the same way as the Gshocks --- watch-faces/complication/advanced_alarm_face.c | 3 +++ 1 file changed, 3 insertions(+) diff --git a/watch-faces/complication/advanced_alarm_face.c b/watch-faces/complication/advanced_alarm_face.c index 146386c0..ae79f343 100644 --- a/watch-faces/complication/advanced_alarm_face.c +++ b/watch-faces/complication/advanced_alarm_face.c @@ -136,6 +136,9 @@ static void _advanced_alarm_face_draw(alarm_state_t *state, uint8_t subsecond) { } } } + else { + watch_display_text(WATCH_POSITION_SECONDS, state->alarm[state->alarm_idx].enabled ? "on" : "--"); + } // set alarm indicator _alarm_set_signal(state); From 92bae2016f9520674e763a80535f569e86a53a1a Mon Sep 17 00:00:00 2001 From: David Volovskiy Date: Mon, 1 Sep 2025 09:56:24 -0400 Subject: [PATCH 055/179] Removed unneeded arrays --- watch-faces/complication/advanced_alarm_face.c | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/watch-faces/complication/advanced_alarm_face.c b/watch-faces/complication/advanced_alarm_face.c index ae79f343..cfb83a95 100644 --- a/watch-faces/complication/advanced_alarm_face.c +++ b/watch-faces/complication/advanced_alarm_face.c @@ -42,8 +42,7 @@ typedef enum { static const char _dow_strings_classic[ALARM_DAY_STATES + 1][2] ={"AL", "MO", "TU", "WE", "TH", "FR", "SA", "SU", "ED", "1t", "MF", "WN"}; static const char _dow_strings_custom[ALARM_DAY_STATES + 1][3] ={ "AL ", "MON", "TUE", "WED", "THU", "FRI", "SAT", "SUN", "DAY", "1t ", "M-F", "WKD"}; -static const uint8_t _blink_idx[ALARM_SETTING_STATES] = {2, 0, 4, 6, 8, 9}; -static const uint8_t _blink_idx2[ALARM_SETTING_STATES] = {3, 1, 5, 7, 8, 9}; +static const uint8_t _beeps_blink_idx = 9; static const watch_buzzer_note_t _buzzer_notes[3] = {BUZZER_NOTE_B6, BUZZER_NOTE_C8, BUZZER_NOTE_A8}; // Volume is indicated by the three segments 5D, 5G and 5A @@ -127,12 +126,12 @@ static void _advanced_alarm_face_draw(alarm_state_t *state, uint8_t subsecond) { // draw beep rounds indicator if ((subsecond % 2) == 0 || (state->setting_state != alarm_setting_idx_beeps)) { if (state->alarm[state->alarm_idx].beeps == ALARM_MAX_BEEP_ROUNDS - 1) - watch_display_character('L', _blink_idx[alarm_setting_idx_beeps]); + watch_display_character('L', _beeps_blink_idx); else { if (state->alarm[state->alarm_idx].beeps == 0) - watch_display_character('o', _blink_idx[alarm_setting_idx_beeps]); + watch_display_character('o', _beeps_blink_idx); else - watch_display_character(state->alarm[state->alarm_idx].beeps + 48, _blink_idx[alarm_setting_idx_beeps]); + watch_display_character(state->alarm[state->alarm_idx].beeps + 48, _beeps_blink_idx); } } } From 61294d557c28d78c52ddc050141b77bce29936ae Mon Sep 17 00:00:00 2001 From: David Volovskiy Date: Mon, 1 Sep 2025 10:09:53 -0400 Subject: [PATCH 056/179] _alarm_show_alarm_on_text is now its own function --- watch-faces/complication/advanced_alarm_face.c | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/watch-faces/complication/advanced_alarm_face.c b/watch-faces/complication/advanced_alarm_face.c index cfb83a95..16bfdbce 100644 --- a/watch-faces/complication/advanced_alarm_face.c +++ b/watch-faces/complication/advanced_alarm_face.c @@ -67,6 +67,10 @@ static void _alarm_set_signal(alarm_state_t *state) { watch_clear_indicator(WATCH_INDICATOR_SIGNAL); } +static void _alarm_show_alarm_on_text(alarm_state_t *state) { + watch_display_text(WATCH_POSITION_SECONDS, state->alarm[state->alarm_idx].enabled ? "on" : "--"); +} + static void _advanced_alarm_face_draw(alarm_state_t *state, uint8_t subsecond) { char buf[12]; bool set_leading_zero = movement_clock_mode_24h() == MOVEMENT_CLOCK_MODE_024H; @@ -136,7 +140,7 @@ static void _advanced_alarm_face_draw(alarm_state_t *state, uint8_t subsecond) { } } else { - watch_display_text(WATCH_POSITION_SECONDS, state->alarm[state->alarm_idx].enabled ? "on" : "--"); + _alarm_show_alarm_on_text(state); } // set alarm indicator @@ -322,6 +326,7 @@ bool advanced_alarm_face_loop(movement_event_t event, void *context) { // revert change of enabled flag and show it briefly state->alarm[state->alarm_idx].enabled ^= 1; _alarm_set_signal(state); + _alarm_show_alarm_on_text(state); delay_ms(275); state->alarm_idx = 0; } From 838aa1e6f701e2d8b832f64c083261aae749a368 Mon Sep 17 00:00:00 2001 From: David Volovskiy Date: Tue, 2 Sep 2025 21:34:29 -0400 Subject: [PATCH 057/179] initial commit of working code --- movement_faces.h | 1 + watch-faces.mk | 1 + watch-faces/complication/blackjack_face.c | 326 ++++++++++++++++++++++ watch-faces/complication/blackjack_face.h | 55 ++++ 4 files changed, 383 insertions(+) create mode 100755 watch-faces/complication/blackjack_face.c create mode 100755 watch-faces/complication/blackjack_face.h diff --git a/movement_faces.h b/movement_faces.h index fc43716d..fab9ee31 100644 --- a/movement_faces.h +++ b/movement_faces.h @@ -73,4 +73,5 @@ #include "wareki_face.h" #include "deadline_face.h" #include "wordle_face.h" +#include "blackjack_face.h" // New includes go above this line. diff --git a/watch-faces.mk b/watch-faces.mk index 22e262cf..52919f53 100644 --- a/watch-faces.mk +++ b/watch-faces.mk @@ -48,4 +48,5 @@ SRCS += \ ./watch-faces/sensor/lis2dw_monitor_face.c \ ./watch-faces/complication/wareki_face.c \ ./watch-faces/complication/deadline_face.c \ + ./watch-faces/complication/blackjack_face.c \ # New watch faces go above this line. diff --git a/watch-faces/complication/blackjack_face.c b/watch-faces/complication/blackjack_face.c new file mode 100755 index 00000000..ee3d274a --- /dev/null +++ b/watch-faces/complication/blackjack_face.c @@ -0,0 +1,326 @@ +/* + * MIT License + * + * Copyright (c) 2025 David Volovskiy + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +// Emulator only: need time() to seed the random number generator. +#if __EMSCRIPTEN__ +#include +#endif + +#include +#include +#include "blackjack_face.h" +#include "watch_common_display.h" + +#define KING 12 +#define QUEEN 11 +#define JACK 10 + +#define MIN_CARD_VALUE 1 +#define MAX_CARD_VALUE KING +#define CARD_RANK_COUNT (MAX_CARD_VALUE - MIN_CARD_VALUE + 1) +#define CARD_SUIT_COUNT 4 +#define DECK_SIZE (CARD_SUIT_COUNT * CARD_RANK_COUNT) + +#define BLACKJACK_MAX_HAND_SIZE 11 // 4*1 + 4*2 + 3*3 = 21; 11 cards total +#define MAX_PLAYER_CARDS_DISPLAY 4 +#define BOARD_DISPLAY_START 4 + +uint8_t score_player = 0; +uint8_t score_dealer = 0; +uint8_t hand_player[BLACKJACK_MAX_HAND_SIZE] = {0xFF}; +uint8_t hand_dealer[BLACKJACK_MAX_HAND_SIZE] = {0xFF}; +uint8_t idx_player = 0; +uint8_t idx_dealer = 0; + +typedef enum { + BJ_HIT, + BJ_STAND, + BJ_BUST, +} guess_t; + +typedef enum { + BJ_TITLE_SCREEN, + BJ_PLAYING, + BJ_DEALER_PLAYING, + BJ_RESULT, +} game_state_t; + +static game_state_t game_state = BJ_TITLE_SCREEN; +static uint8_t deck[DECK_SIZE] = {0}; +static uint8_t current_card = 0; + +static uint8_t generate_random_number(uint8_t num_values) { + // Emulator: use rand. Hardware: use arc4random. +#if __EMSCRIPTEN__ + return rand() % num_values; +#else + return arc4random_uniform(num_values); +#endif +} + +static void stack_deck(void) { + for (size_t i = 0; i < CARD_RANK_COUNT; i++) { + for (size_t j = 0; j < CARD_SUIT_COUNT; j++) + deck[(i * CARD_SUIT_COUNT) + j] = MIN_CARD_VALUE + i; + } +} + +static void shuffle_deck(void) { + // Randomize shuffle with Fisher Yates + size_t i; + size_t j; + uint8_t tmp; + + for (i = DECK_SIZE - 1; i > 0; i--) { + j = generate_random_number(0xFF) % (i + 1); + tmp = deck[j]; + deck[j] = deck[i]; + deck[i] = tmp; + } +} + +static void reset_deck(void) { + current_card = 0; + stack_deck(); + shuffle_deck(); +} + +static uint8_t get_next_card(void) { + if (current_card >= DECK_SIZE) + reset_deck(); + uint8_t card = deck[current_card++]; + if (card > 10) return 10; + return card; +} + +static void reset_hands(void) { + score_player = 0; + score_dealer = 0; + idx_player = 0; + idx_dealer = 0; + memset(hand_player, 0xFF, sizeof(hand_player)); + memset(hand_dealer, 0xFF, sizeof(hand_dealer)); + reset_deck(); +} + +static void give_player_card(void) { + uint8_t card = get_next_card(); + hand_player[idx_player++] = card; + score_player += card; +} + +static void give_dealer_card(void) { + uint8_t card = get_next_card(); + hand_dealer[idx_dealer++] = card; + score_dealer += card; +} + +static void display_player_hand(void) { + uint8_t cards_to_display = idx_player > MAX_PLAYER_CARDS_DISPLAY ? MAX_PLAYER_CARDS_DISPLAY : idx_player; + for (uint8_t i=0; i 21) { + display_bust(); + } else { + display_player_hand(); + display_player_score(); + } +} + +static void perform_stand(void) { + game_state = BJ_DEALER_PLAYING; + watch_display_text(WATCH_POSITION_BOTTOM, "Stnd"); + display_player_score(); +} + +static void dealer_performs_hits(void) { + give_dealer_card(); + display_dealer_hand(); + if (score_dealer > 21) { + display_win(); + } else if (score_dealer > score_player) { + display_lose(); + } else { + display_dealer_hand(); + display_dealer_score(); + } +} + +static void see_if_dealer_hits(void) { + if (score_dealer > 16) { + if (score_dealer > score_player) { + display_lose(); + } else if (score_dealer < score_player) { + display_win(); + } else { + display_tie(); + } + } else { + dealer_performs_hits(); + } +} + +void handle_button_presses(bool hit) { + switch (game_state) + { + case BJ_TITLE_SCREEN: + begin_playing(); + break; + case BJ_PLAYING: + if (hit) { + perform_hit(); + } else { + perform_stand(); + } + break; + case BJ_DEALER_PLAYING: + see_if_dealer_hits(); + break; + case BJ_RESULT: + display_title(); + break; + } +} + +void blackjack_face_setup(uint8_t watch_face_index, void **context_ptr) { + (void) watch_face_index; + + if (*context_ptr == NULL) { + *context_ptr = malloc(sizeof(blackjack_face_state_t)); + memset(*context_ptr, 0, sizeof(blackjack_face_state_t)); + } +} + +void blackjack_face_activate(void *context) { + blackjack_face_state_t *state = (blackjack_face_state_t *) context; + (void) state; + display_title(); +} + +bool blackjack_face_loop(movement_event_t event, void *context) { + blackjack_face_state_t *state = (blackjack_face_state_t *) context; + (void) state; + + switch (event.event_type) { + case EVENT_ACTIVATE: + break; + case EVENT_TICK: + if (game_state == BJ_DEALER_PLAYING) { + see_if_dealer_hits(); + } + break; + case EVENT_LIGHT_BUTTON_UP: + handle_button_presses(false); + case EVENT_LIGHT_BUTTON_DOWN: + break; + case EVENT_ALARM_BUTTON_UP: + handle_button_presses(true); + break; + case EVENT_TIMEOUT: + break; + default: + return movement_default_loop_handler(event); + } + return true; +} + +void blackjack_face_resign(void *context) { + (void) context; +} diff --git a/watch-faces/complication/blackjack_face.h b/watch-faces/complication/blackjack_face.h new file mode 100755 index 00000000..36eb4425 --- /dev/null +++ b/watch-faces/complication/blackjack_face.h @@ -0,0 +1,55 @@ +/* + * MIT License + * + * Copyright (c) 2023 Chris Ellis + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +#ifndef BLACKJACK_FACE_H_ +#define BLACKJACK_FACE_H_ + +#include "movement.h" + +/* + * Blackjack face + * ====================== + * + */ + + + +typedef struct { + // Anything you need to keep track of, put it here! +} blackjack_face_state_t; + +void blackjack_face_setup(uint8_t watch_face_index, void ** context_ptr); +void blackjack_face_activate(void *context); +bool blackjack_face_loop(movement_event_t event, void *context); +void blackjack_face_resign(void *context); + +#define blackjack_face ((const watch_face_t){ \ + blackjack_face_setup, \ + blackjack_face_activate, \ + blackjack_face_loop, \ + blackjack_face_resign, \ + NULL, \ +}) + +#endif // blackjack_FACE_H_ From 3760aeb947306de9cf70d8b74b25db3d53b43c4c Mon Sep 17 00:00:00 2001 From: David Volovskiy Date: Tue, 2 Sep 2025 21:57:42 -0400 Subject: [PATCH 058/179] Changed order of how cards are displayed for player --- watch-faces/complication/blackjack_face.c | 20 +++++++------------- 1 file changed, 7 insertions(+), 13 deletions(-) diff --git a/watch-faces/complication/blackjack_face.c b/watch-faces/complication/blackjack_face.c index ee3d274a..b0f78b09 100755 --- a/watch-faces/complication/blackjack_face.c +++ b/watch-faces/complication/blackjack_face.c @@ -53,12 +53,6 @@ uint8_t hand_dealer[BLACKJACK_MAX_HAND_SIZE] = {0xFF}; uint8_t idx_player = 0; uint8_t idx_dealer = 0; -typedef enum { - BJ_HIT, - BJ_STAND, - BJ_BUST, -} guess_t; - typedef enum { BJ_TITLE_SCREEN, BJ_PLAYING, @@ -66,7 +60,7 @@ typedef enum { BJ_RESULT, } game_state_t; -static game_state_t game_state = BJ_TITLE_SCREEN; +static game_state_t game_state; static uint8_t deck[DECK_SIZE] = {0}; static uint8_t current_card = 0; @@ -138,11 +132,11 @@ static void give_dealer_card(void) { static void display_player_hand(void) { uint8_t cards_to_display = idx_player > MAX_PLAYER_CARDS_DISPLAY ? MAX_PLAYER_CARDS_DISPLAY : idx_player; - for (uint8_t i=0; i0; i--) { + uint8_t card = hand_player[idx_player-i]; if (card == 10) card = 0; const char display_char = card + '0'; - watch_display_character(display_char, BOARD_DISPLAY_START + i); + watch_display_character(display_char, BOARD_DISPLAY_START + cards_to_display - i); } } @@ -167,7 +161,7 @@ static void display_dealer_score(void) { static void display_win(void) { game_state = BJ_RESULT; - watch_display_text(WATCH_POSITION_BOTTOM, " WIN"); + watch_display_text_with_fallback(WATCH_POSITION_BOTTOM, "WlN ", " WIN"); display_player_score(); display_dealer_score(); } @@ -181,13 +175,13 @@ static void display_lose(void) { static void display_tie(void) { game_state = BJ_RESULT; - watch_display_text(WATCH_POSITION_BOTTOM, " TIE"); + watch_display_text_with_fallback(WATCH_POSITION_BOTTOM, "TlE ", " TIE"); display_player_score(); } static void display_bust(void) { game_state = BJ_RESULT; - watch_display_text(WATCH_POSITION_BOTTOM, "BUST"); + watch_display_text_with_fallback(WATCH_POSITION_BOTTOM, "8UST ", " BUST"); display_player_score(); } From 8a831f5cfded756822c51a25870eac12746fd72a Mon Sep 17 00:00:00 2001 From: David Volovskiy Date: Tue, 2 Sep 2025 22:15:06 -0400 Subject: [PATCH 059/179] Added tap controls --- watch-faces/complication/blackjack_face.c | 28 +++++++++++++++++++++-- watch-faces/complication/blackjack_face.h | 2 +- 2 files changed, 27 insertions(+), 3 deletions(-) diff --git a/watch-faces/complication/blackjack_face.c b/watch-faces/complication/blackjack_face.c index b0f78b09..9b0de5a5 100755 --- a/watch-faces/complication/blackjack_face.c +++ b/watch-faces/complication/blackjack_face.c @@ -273,12 +273,28 @@ void handle_button_presses(bool hit) { } } +static void toggle_tap_control(blackjack_face_state_t *state) { + if (state->tap_control_on) { + movement_disable_tap_detection_if_available(); + state->tap_control_on = false; + watch_clear_indicator(WATCH_INDICATOR_SIGNAL); + } else { + bool tap_could_enable = movement_enable_tap_detection_if_available(); + if (tap_could_enable) { + state->tap_control_on = true; + watch_set_indicator(WATCH_INDICATOR_SIGNAL); + } + } +} + void blackjack_face_setup(uint8_t watch_face_index, void **context_ptr) { (void) watch_face_index; if (*context_ptr == NULL) { *context_ptr = malloc(sizeof(blackjack_face_state_t)); memset(*context_ptr, 0, sizeof(blackjack_face_state_t)); + blackjack_face_state_t *state = (blackjack_face_state_t *)*context_ptr; + state->tap_control_on = false; } } @@ -290,10 +306,11 @@ void blackjack_face_activate(void *context) { bool blackjack_face_loop(movement_event_t event, void *context) { blackjack_face_state_t *state = (blackjack_face_state_t *) context; - (void) state; - switch (event.event_type) { case EVENT_ACTIVATE: + if (state->tap_control_on) { + movement_enable_tap_detection_if_available(); + } break; case EVENT_TICK: if (game_state == BJ_DEALER_PLAYING) { @@ -301,12 +318,19 @@ bool blackjack_face_loop(movement_event_t event, void *context) { } break; case EVENT_LIGHT_BUTTON_UP: + case EVENT_DOUBLE_TAP: handle_button_presses(false); case EVENT_LIGHT_BUTTON_DOWN: break; case EVENT_ALARM_BUTTON_UP: + case EVENT_SINGLE_TAP: handle_button_presses(true); break; + case EVENT_ALARM_LONG_PRESS: + if (game_state == BJ_TITLE_SCREEN) { + toggle_tap_control(state); + } + break; case EVENT_TIMEOUT: break; default: diff --git a/watch-faces/complication/blackjack_face.h b/watch-faces/complication/blackjack_face.h index 36eb4425..ff52d79f 100755 --- a/watch-faces/complication/blackjack_face.h +++ b/watch-faces/complication/blackjack_face.h @@ -36,7 +36,7 @@ typedef struct { - // Anything you need to keep track of, put it here! + bool tap_control_on; } blackjack_face_state_t; void blackjack_face_setup(uint8_t watch_face_index, void ** context_ptr); From 3b3ecffd3e55064a81fde9a71675d3e187756e2d Mon Sep 17 00:00:00 2001 From: David Volovskiy Date: Tue, 2 Sep 2025 22:23:36 -0400 Subject: [PATCH 060/179] Fixed a few warnings. --- watch-faces/complication/blackjack_face.c | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/watch-faces/complication/blackjack_face.c b/watch-faces/complication/blackjack_face.c index 9b0de5a5..5234d02a 100755 --- a/watch-faces/complication/blackjack_face.c +++ b/watch-faces/complication/blackjack_face.c @@ -148,13 +148,13 @@ static void display_dealer_hand(void) { } static void display_player_score(void) { - char buf[3]; + char buf[4]; sprintf(buf, "%2d", score_player); watch_display_text(WATCH_POSITION_SECONDS, buf); } static void display_dealer_score(void) { - char buf[3]; + char buf[4]; sprintf(buf, "%2d", score_dealer); watch_display_text(WATCH_POSITION_TOP_RIGHT, buf); } @@ -251,7 +251,7 @@ static void see_if_dealer_hits(void) { } } -void handle_button_presses(bool hit) { +static void handle_button_presses(bool hit) { switch (game_state) { case BJ_TITLE_SCREEN: From b66e10406e44152c38307cf1d94da4389db0a80b Mon Sep 17 00:00:00 2001 From: David Volovskiy Date: Wed, 3 Sep 2025 06:45:14 -0400 Subject: [PATCH 061/179] Made hand info into a struct --- watch-faces/complication/blackjack_face.c | 53 +++++++++++------------ 1 file changed, 25 insertions(+), 28 deletions(-) diff --git a/watch-faces/complication/blackjack_face.c b/watch-faces/complication/blackjack_face.c index 5234d02a..c403579a 100755 --- a/watch-faces/complication/blackjack_face.c +++ b/watch-faces/complication/blackjack_face.c @@ -46,12 +46,11 @@ #define MAX_PLAYER_CARDS_DISPLAY 4 #define BOARD_DISPLAY_START 4 -uint8_t score_player = 0; -uint8_t score_dealer = 0; -uint8_t hand_player[BLACKJACK_MAX_HAND_SIZE] = {0xFF}; -uint8_t hand_dealer[BLACKJACK_MAX_HAND_SIZE] = {0xFF}; -uint8_t idx_player = 0; -uint8_t idx_dealer = 0; +typedef struct { + uint8_t score; + uint8_t idx_hand; + uint8_t hand[BLACKJACK_MAX_HAND_SIZE]; +} hand_info_t; typedef enum { BJ_TITLE_SCREEN, @@ -63,6 +62,8 @@ typedef enum { static game_state_t game_state; static uint8_t deck[DECK_SIZE] = {0}; static uint8_t current_card = 0; +hand_info_t player; +hand_info_t dealer; static uint8_t generate_random_number(uint8_t num_values) { // Emulator: use rand. Hardware: use arc4random. @@ -109,31 +110,27 @@ static uint8_t get_next_card(void) { } static void reset_hands(void) { - score_player = 0; - score_dealer = 0; - idx_player = 0; - idx_dealer = 0; - memset(hand_player, 0xFF, sizeof(hand_player)); - memset(hand_dealer, 0xFF, sizeof(hand_dealer)); + memset(&player, 0, sizeof(player)); + memset(&dealer, 0, sizeof(dealer)); reset_deck(); } static void give_player_card(void) { uint8_t card = get_next_card(); - hand_player[idx_player++] = card; - score_player += card; + player.hand[player.idx_hand++] = card; + player.score += card; } static void give_dealer_card(void) { uint8_t card = get_next_card(); - hand_dealer[idx_dealer++] = card; - score_dealer += card; + dealer.hand[dealer.idx_hand++] = card; + dealer.score += card; } static void display_player_hand(void) { - uint8_t cards_to_display = idx_player > MAX_PLAYER_CARDS_DISPLAY ? MAX_PLAYER_CARDS_DISPLAY : idx_player; + uint8_t cards_to_display = player.idx_hand > MAX_PLAYER_CARDS_DISPLAY ? MAX_PLAYER_CARDS_DISPLAY : player.idx_hand; for (uint8_t i=cards_to_display; i>0; i--) { - uint8_t card = hand_player[idx_player-i]; + uint8_t card = player.hand[player.idx_hand-i]; if (card == 10) card = 0; const char display_char = card + '0'; watch_display_character(display_char, BOARD_DISPLAY_START + cards_to_display - i); @@ -141,7 +138,7 @@ static void display_player_hand(void) { } static void display_dealer_hand(void) { - uint8_t card = hand_dealer[idx_dealer - 1]; + uint8_t card = dealer.hand[dealer.idx_hand - 1]; if (card == 10) card = 0; const char display_char = card + '0'; watch_display_character(display_char, 0); @@ -149,13 +146,13 @@ static void display_dealer_hand(void) { static void display_player_score(void) { char buf[4]; - sprintf(buf, "%2d", score_player); + sprintf(buf, "%2d", player.score); watch_display_text(WATCH_POSITION_SECONDS, buf); } static void display_dealer_score(void) { char buf[4]; - sprintf(buf, "%2d", score_dealer); + sprintf(buf, "%2d", dealer.score); watch_display_text(WATCH_POSITION_TOP_RIGHT, buf); } @@ -206,11 +203,11 @@ static void begin_playing(void) { } static void perform_hit(void) { - if (score_player == 21) { + if (player.score == 21) { return; // Assume hitting on 21 is a mistake and ignore } give_player_card(); - if (score_player > 21) { + if (player.score > 21) { display_bust(); } else { display_player_hand(); @@ -227,9 +224,9 @@ static void perform_stand(void) { static void dealer_performs_hits(void) { give_dealer_card(); display_dealer_hand(); - if (score_dealer > 21) { + if (dealer.score > 21) { display_win(); - } else if (score_dealer > score_player) { + } else if (dealer.score > player.score) { display_lose(); } else { display_dealer_hand(); @@ -238,10 +235,10 @@ static void dealer_performs_hits(void) { } static void see_if_dealer_hits(void) { - if (score_dealer > 16) { - if (score_dealer > score_player) { + if (dealer.score > 16) { + if (dealer.score > player.score) { display_lose(); - } else if (score_dealer < score_player) { + } else if (dealer.score < player.score) { display_win(); } else { display_tie(); From 1d40c384cb63bd81e7f950a05e8f2f6514c8e42a Mon Sep 17 00:00:00 2001 From: David Volovskiy Date: Wed, 3 Sep 2025 07:07:51 -0400 Subject: [PATCH 062/179] Added logic for supporting Ace --- watch-faces/complication/blackjack_face.c | 55 +++++++++++++++-------- 1 file changed, 36 insertions(+), 19 deletions(-) diff --git a/watch-faces/complication/blackjack_face.c b/watch-faces/complication/blackjack_face.c index c403579a..beb14446 100755 --- a/watch-faces/complication/blackjack_face.c +++ b/watch-faces/complication/blackjack_face.c @@ -32,12 +32,13 @@ #include "blackjack_face.h" #include "watch_common_display.h" +#define ACE 13 #define KING 12 #define QUEEN 11 #define JACK 10 -#define MIN_CARD_VALUE 1 -#define MAX_CARD_VALUE KING +#define MIN_CARD_VALUE 2 +#define MAX_CARD_VALUE ACE #define CARD_RANK_COUNT (MAX_CARD_VALUE - MIN_CARD_VALUE + 1) #define CARD_SUIT_COUNT 4 #define DECK_SIZE (CARD_SUIT_COUNT * CARD_RANK_COUNT) @@ -49,6 +50,7 @@ typedef struct { uint8_t score; uint8_t idx_hand; + int8_t high_aces_in_hand; uint8_t hand[BLACKJACK_MAX_HAND_SIZE]; } hand_info_t; @@ -101,30 +103,45 @@ static void reset_deck(void) { shuffle_deck(); } -static uint8_t get_next_card(void) { +static uint8_t get_next_card(hand_info_t *hand_info) { if (current_card >= DECK_SIZE) reset_deck(); uint8_t card = deck[current_card++]; - if (card > 10) return 10; + switch (card) + { + case ACE: + card = 11; + hand_info->high_aces_in_hand++; + break; + case KING: + case QUEEN: + case JACK: + card = 10; + + default: + break; + } return card; } +static void modify_score_from_aces(hand_info_t *hand_info) { + while (hand_info->score > 21 && hand_info->high_aces_in_hand > 0) { + hand_info->score -= 10; + hand_info->high_aces_in_hand--; + } +} + static void reset_hands(void) { memset(&player, 0, sizeof(player)); memset(&dealer, 0, sizeof(dealer)); reset_deck(); } -static void give_player_card(void) { - uint8_t card = get_next_card(); - player.hand[player.idx_hand++] = card; - player.score += card; -} - -static void give_dealer_card(void) { - uint8_t card = get_next_card(); - dealer.hand[dealer.idx_hand++] = card; - dealer.score += card; +static void give_card(hand_info_t *hand_info) { + uint8_t card = get_next_card(hand_info); + hand_info->hand[hand_info->idx_hand++] = card; + hand_info->score += card; + modify_score_from_aces(hand_info); } static void display_player_hand(void) { @@ -193,11 +210,11 @@ static void begin_playing(void) { game_state = BJ_PLAYING; reset_hands(); // Give player their first 2 cards - give_player_card(); - give_player_card(); + give_card(&player); + give_card(&player); display_player_hand(); display_player_score(); - give_dealer_card(); + give_card(&dealer); display_dealer_hand(); display_dealer_score(); } @@ -206,7 +223,7 @@ static void perform_hit(void) { if (player.score == 21) { return; // Assume hitting on 21 is a mistake and ignore } - give_player_card(); + give_card(&player); if (player.score > 21) { display_bust(); } else { @@ -222,7 +239,7 @@ static void perform_stand(void) { } static void dealer_performs_hits(void) { - give_dealer_card(); + give_card(&dealer); display_dealer_hand(); if (dealer.score > 21) { display_win(); From 9ac62e8e53a39aab692212396fd2df9663158c44 Mon Sep 17 00:00:00 2001 From: David Volovskiy Date: Wed, 3 Sep 2025 07:28:38 -0400 Subject: [PATCH 063/179] Display of face cards and Ace --- watch-faces/complication/blackjack_face.c | 102 +++++++++++++++------- 1 file changed, 69 insertions(+), 33 deletions(-) diff --git a/watch-faces/complication/blackjack_face.c b/watch-faces/complication/blackjack_face.c index beb14446..ee490426 100755 --- a/watch-faces/complication/blackjack_face.c +++ b/watch-faces/complication/blackjack_face.c @@ -61,6 +61,10 @@ typedef enum { BJ_RESULT, } game_state_t; +typedef enum { + A, B, C, D, E, F, G +} segment_t; + static game_state_t game_state; static uint8_t deck[DECK_SIZE] = {0}; static uint8_t current_card = 0; @@ -106,22 +110,23 @@ static void reset_deck(void) { static uint8_t get_next_card(hand_info_t *hand_info) { if (current_card >= DECK_SIZE) reset_deck(); - uint8_t card = deck[current_card++]; + return deck[current_card++]; +} + +static uint8_t get_card_value(uint8_t card) { + uint8_t card_value; switch (card) { case ACE: - card = 11; - hand_info->high_aces_in_hand++; + return 11; break; case KING: case QUEEN: case JACK: card = 10; - default: - break; + return card; } - return card; } static void modify_score_from_aces(hand_info_t *hand_info) { @@ -139,64 +144,95 @@ static void reset_hands(void) { static void give_card(hand_info_t *hand_info) { uint8_t card = get_next_card(hand_info); + if (card == ACE) hand_info->high_aces_in_hand++; hand_info->hand[hand_info->idx_hand++] = card; - hand_info->score += card; + uint8_t card_value = get_card_value(card); + hand_info->score += card_value; modify_score_from_aces(hand_info); } +static void set_segment_at_position(segment_t segment, uint8_t position) { + digit_mapping_t segmap; + if (watch_get_lcd_type() == WATCH_LCD_TYPE_CUSTOM) { + segmap = Custom_LCD_Display_Mapping[position]; + } else { + segmap = Classic_LCD_Display_Mapping[position]; + } + const uint8_t com_pin = segmap.segment[segment].address.com; + const uint8_t seg = segmap.segment[segment].address.seg; + watch_set_pixel(com_pin, seg); +} + +static void display_card_at_position(uint8_t card, uint8_t display_position) { + switch (card) { + case KING: + watch_display_character(' ', display_position); + set_segment_at_position(A, display_position); + set_segment_at_position(D, display_position); + set_segment_at_position(G, display_position); + break; + case QUEEN: + watch_display_character(' ', display_position); + set_segment_at_position(A, display_position); + set_segment_at_position(D, display_position); + break; + case JACK: + watch_display_character('-', display_position); + break; + case ACE: + watch_display_character('A', display_position); + break; + default: { + const char display_char = card + '0'; + watch_display_character(display_char, display_position); + break; + } + } +} + static void display_player_hand(void) { uint8_t cards_to_display = player.idx_hand > MAX_PLAYER_CARDS_DISPLAY ? MAX_PLAYER_CARDS_DISPLAY : player.idx_hand; for (uint8_t i=cards_to_display; i>0; i--) { uint8_t card = player.hand[player.idx_hand-i]; - if (card == 10) card = 0; - const char display_char = card + '0'; - watch_display_character(display_char, BOARD_DISPLAY_START + cards_to_display - i); + display_card_at_position(card, BOARD_DISPLAY_START + cards_to_display - i); } } static void display_dealer_hand(void) { uint8_t card = dealer.hand[dealer.idx_hand - 1]; - if (card == 10) card = 0; - const char display_char = card + '0'; - watch_display_character(display_char, 0); + display_card_at_position(card, 0); } -static void display_player_score(void) { +display_score(uint8_t score, watch_position_t pos) { char buf[4]; - sprintf(buf, "%2d", player.score); - watch_display_text(WATCH_POSITION_SECONDS, buf); -} - -static void display_dealer_score(void) { - char buf[4]; - sprintf(buf, "%2d", dealer.score); - watch_display_text(WATCH_POSITION_TOP_RIGHT, buf); + sprintf(buf, "%2d", score); + watch_display_text(pos, buf); } static void display_win(void) { game_state = BJ_RESULT; watch_display_text_with_fallback(WATCH_POSITION_BOTTOM, "WlN ", " WIN"); - display_player_score(); - display_dealer_score(); + display_score(player.score, WATCH_POSITION_SECONDS); + display_score(dealer.score, WATCH_POSITION_TOP_RIGHT); } static void display_lose(void) { game_state = BJ_RESULT; watch_display_text_with_fallback(WATCH_POSITION_BOTTOM, "LOSE", "lOSE"); - display_player_score(); - display_dealer_score(); + display_score(player.score, WATCH_POSITION_SECONDS); + display_score(dealer.score, WATCH_POSITION_TOP_RIGHT); } static void display_tie(void) { game_state = BJ_RESULT; watch_display_text_with_fallback(WATCH_POSITION_BOTTOM, "TlE ", " TIE"); - display_player_score(); + display_score(player.score, WATCH_POSITION_SECONDS); } static void display_bust(void) { game_state = BJ_RESULT; watch_display_text_with_fallback(WATCH_POSITION_BOTTOM, "8UST ", " BUST"); - display_player_score(); + display_score(player.score, WATCH_POSITION_SECONDS); } static void display_title(void) { @@ -213,10 +249,10 @@ static void begin_playing(void) { give_card(&player); give_card(&player); display_player_hand(); - display_player_score(); + display_score(player.score, WATCH_POSITION_SECONDS); give_card(&dealer); display_dealer_hand(); - display_dealer_score(); + display_score(dealer.score, WATCH_POSITION_TOP_RIGHT); } static void perform_hit(void) { @@ -228,14 +264,14 @@ static void perform_hit(void) { display_bust(); } else { display_player_hand(); - display_player_score(); + display_score(player.score, WATCH_POSITION_SECONDS); } } static void perform_stand(void) { game_state = BJ_DEALER_PLAYING; watch_display_text(WATCH_POSITION_BOTTOM, "Stnd"); - display_player_score(); + display_score(player.score, WATCH_POSITION_SECONDS); } static void dealer_performs_hits(void) { @@ -247,7 +283,7 @@ static void dealer_performs_hits(void) { display_lose(); } else { display_dealer_hand(); - display_dealer_score(); + display_score(dealer.score, WATCH_POSITION_TOP_RIGHT); } } From 93a2a5a5eef056d40c142ff7495181ac83397cc5 Mon Sep 17 00:00:00 2001 From: David Volovskiy Date: Wed, 3 Sep 2025 07:46:38 -0400 Subject: [PATCH 064/179] Documented face --- watch-faces/complication/blackjack_face.c | 13 ++++++---- watch-faces/complication/blackjack_face.h | 29 +++++++++++++++++++++++ 2 files changed, 37 insertions(+), 5 deletions(-) diff --git a/watch-faces/complication/blackjack_face.c b/watch-faces/complication/blackjack_face.c index ee490426..c9c7a3cd 100755 --- a/watch-faces/complication/blackjack_face.c +++ b/watch-faces/complication/blackjack_face.c @@ -32,10 +32,10 @@ #include "blackjack_face.h" #include "watch_common_display.h" -#define ACE 13 -#define KING 12 -#define QUEEN 11 -#define JACK 10 +#define ACE 14 +#define KING 13 +#define QUEEN 12 +#define JACK 11 #define MIN_CARD_VALUE 2 #define MAX_CARD_VALUE ACE @@ -180,7 +180,10 @@ static void display_card_at_position(uint8_t card, uint8_t display_position) { watch_display_character('-', display_position); break; case ACE: - watch_display_character('A', display_position); + watch_display_character(watch_get_lcd_type() == WATCH_LCD_TYPE_CUSTOM ? 'A' : 'a', display_position); + break; + case 10: + watch_display_character('0', display_position); break; default: { const char display_char = card + '0'; diff --git a/watch-faces/complication/blackjack_face.h b/watch-faces/complication/blackjack_face.h index ff52d79f..8771561c 100755 --- a/watch-faces/complication/blackjack_face.h +++ b/watch-faces/complication/blackjack_face.h @@ -31,6 +31,35 @@ * Blackjack face * ====================== * + * Simple blackjack game. + * + * Aces are 11 unless you'd but, and if so, they become 1. + * King, Queen, and jack are all 10 points. + * Dealer deals to themselves until they get at least 17. + * The game plays with one shuffled deck that gets reshuffled with every game. + * + * Press either ALARM or LIGHT to begin playing. + * Your score is in the Seconds position. + * The dealer's score is in the Top-Right position. + * The dealer's last-shown card is in the Top-Left position. + * Your cards are in the Bottom row. From left to right, they are oldest to newest. Up to four cards will be dislayed. + * + * To hit, press the ALARM button. + * To stand, press the LIGHT button. + * If you're at 21, you cannoy hit, since we just assume it's a mispress on the button. + * + * Once you stand, the dealer will deal out to themselves once per second (or immidietly when you press the LIGHT or ALARM buttons). + * The game results are: + * WIN: You have a higher score than the dealer, but no more than 21. Or the dealer's score is over 21. + * LOSE: Your score is lower than the dealer's. + * BUST: Your score is above 21. + * TIE: Your score matches the dealer's final score + * + * | Cards | | + * |---------|--------------------------| + * | Value |2|3|4|5|6|7|8|9|10|J|Q|K|A| + * | Display |2|3|4|5|6|7|8|9| 0|-|=|≡|a| + * If you're using a custom display, Ace will display as 'A', not 'a' */ From d6e4a1ad441f86ffb586c62db2dea4ff88b91515 Mon Sep 17 00:00:00 2001 From: David Volovskiy Date: Wed, 3 Sep 2025 07:52:18 -0400 Subject: [PATCH 065/179] Added delay before showing Bust --- watch-faces/complication/blackjack_face.c | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/watch-faces/complication/blackjack_face.c b/watch-faces/complication/blackjack_face.c index c9c7a3cd..78270bf0 100755 --- a/watch-faces/complication/blackjack_face.c +++ b/watch-faces/complication/blackjack_face.c @@ -58,6 +58,7 @@ typedef enum { BJ_TITLE_SCREEN, BJ_PLAYING, BJ_DEALER_PLAYING, + BJ_BUST, BJ_RESULT, } game_state_t; @@ -206,7 +207,7 @@ static void display_dealer_hand(void) { display_card_at_position(card, 0); } -display_score(uint8_t score, watch_position_t pos) { +static void display_score(uint8_t score, watch_position_t pos) { char buf[4]; sprintf(buf, "%2d", score); watch_display_text(pos, buf); @@ -235,7 +236,6 @@ static void display_tie(void) { static void display_bust(void) { game_state = BJ_RESULT; watch_display_text_with_fallback(WATCH_POSITION_BOTTOM, "8UST ", " BUST"); - display_score(player.score, WATCH_POSITION_SECONDS); } static void display_title(void) { @@ -264,11 +264,10 @@ static void perform_hit(void) { } give_card(&player); if (player.score > 21) { - display_bust(); - } else { - display_player_hand(); - display_score(player.score, WATCH_POSITION_SECONDS); + game_state = BJ_BUST; } + display_player_hand(); + display_score(player.score, WATCH_POSITION_SECONDS); } static void perform_stand(void) { @@ -320,6 +319,8 @@ static void handle_button_presses(bool hit) { case BJ_DEALER_PLAYING: see_if_dealer_hits(); break; + case BJ_BUST: + display_bust(); case BJ_RESULT: display_title(); break; @@ -369,6 +370,9 @@ bool blackjack_face_loop(movement_event_t event, void *context) { if (game_state == BJ_DEALER_PLAYING) { see_if_dealer_hits(); } + else if (game_state == BJ_BUST) { + display_bust(); + } break; case EVENT_LIGHT_BUTTON_UP: case EVENT_DOUBLE_TAP: From 20bb59f02f44ecdc337b576a48993ef8b7300633 Mon Sep 17 00:00:00 2001 From: David Volovskiy Date: Wed, 3 Sep 2025 07:56:33 -0400 Subject: [PATCH 066/179] Compiler warning fixes --- watch-faces/complication/blackjack_face.c | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/watch-faces/complication/blackjack_face.c b/watch-faces/complication/blackjack_face.c index 78270bf0..2545fd7a 100755 --- a/watch-faces/complication/blackjack_face.c +++ b/watch-faces/complication/blackjack_face.c @@ -108,23 +108,21 @@ static void reset_deck(void) { shuffle_deck(); } -static uint8_t get_next_card(hand_info_t *hand_info) { +static uint8_t get_next_card(void) { if (current_card >= DECK_SIZE) reset_deck(); return deck[current_card++]; } static uint8_t get_card_value(uint8_t card) { - uint8_t card_value; switch (card) { case ACE: return 11; - break; case KING: case QUEEN: case JACK: - card = 10; + return 10; default: return card; } @@ -144,7 +142,7 @@ static void reset_hands(void) { } static void give_card(hand_info_t *hand_info) { - uint8_t card = get_next_card(hand_info); + uint8_t card = get_next_card(); if (card == ACE) hand_info->high_aces_in_hand++; hand_info->hand[hand_info->idx_hand++] = card; uint8_t card_value = get_card_value(card); @@ -321,6 +319,7 @@ static void handle_button_presses(bool hit) { break; case BJ_BUST: display_bust(); + break; case BJ_RESULT: display_title(); break; From eb1b551e7bd86e5e4e2aebf3fd0162f25c332127 Mon Sep 17 00:00:00 2001 From: David Volovskiy Date: Wed, 3 Sep 2025 08:06:03 -0400 Subject: [PATCH 067/179] Added documentation on tap controls and retained the tap indicator --- watch-faces/complication/blackjack_face.c | 20 ++++++++++++++------ watch-faces/complication/blackjack_face.h | 5 +++++ 2 files changed, 19 insertions(+), 6 deletions(-) diff --git a/watch-faces/complication/blackjack_face.c b/watch-faces/complication/blackjack_face.c index 2545fd7a..1497b7d6 100755 --- a/watch-faces/complication/blackjack_face.c +++ b/watch-faces/complication/blackjack_face.c @@ -242,8 +242,11 @@ static void display_title(void) { watch_display_text_with_fallback(WATCH_POSITION_BOTTOM, " JACK ", "BLaKJK"); } -static void begin_playing(void) { +static void begin_playing(bool tap_control_on) { watch_clear_display(); + if (tap_control_on) { + watch_set_indicator(WATCH_INDICATOR_SIGNAL); + } game_state = BJ_PLAYING; reset_hands(); // Give player their first 2 cards @@ -301,11 +304,11 @@ static void see_if_dealer_hits(void) { } } -static void handle_button_presses(bool hit) { +static void handle_button_presses(bool tap_control_on, bool hit) { switch (game_state) { case BJ_TITLE_SCREEN: - begin_playing(); + begin_playing(tap_control_on); break; case BJ_PLAYING: if (hit) { @@ -362,7 +365,12 @@ bool blackjack_face_loop(movement_event_t event, void *context) { switch (event.event_type) { case EVENT_ACTIVATE: if (state->tap_control_on) { - movement_enable_tap_detection_if_available(); + bool tap_could_enable = movement_enable_tap_detection_if_available(); + if (tap_could_enable) { + watch_set_indicator(WATCH_INDICATOR_SIGNAL); + } else { + state->tap_control_on = false; + } } break; case EVENT_TICK: @@ -375,12 +383,12 @@ bool blackjack_face_loop(movement_event_t event, void *context) { break; case EVENT_LIGHT_BUTTON_UP: case EVENT_DOUBLE_TAP: - handle_button_presses(false); + handle_button_presses(state->tap_control_on, false); case EVENT_LIGHT_BUTTON_DOWN: break; case EVENT_ALARM_BUTTON_UP: case EVENT_SINGLE_TAP: - handle_button_presses(true); + handle_button_presses(state->tap_control_on, true); break; case EVENT_ALARM_LONG_PRESS: if (game_state == BJ_TITLE_SCREEN) { diff --git a/watch-faces/complication/blackjack_face.h b/watch-faces/complication/blackjack_face.h index 8771561c..84041517 100755 --- a/watch-faces/complication/blackjack_face.h +++ b/watch-faces/complication/blackjack_face.h @@ -55,6 +55,11 @@ * BUST: Your score is above 21. * TIE: Your score matches the dealer's final score * + * On a watch that has the accelerometer, long-pressing the ALARM button will turn on the ability to play by tapping. + * The SIGNAL indicator will display when tapping is enabled. + * Tapping once will behave like the ALARM button and hit. + * Tapping twice behave like the LIGHT button and stand. + * * | Cards | | * |---------|--------------------------| * | Value |2|3|4|5|6|7|8|9|10|J|Q|K|A| From a2c400ff8711f5bc90eb9e2c6c620079f7e55eec Mon Sep 17 00:00:00 2001 From: David Volovskiy Date: Wed, 3 Sep 2025 18:18:03 -0400 Subject: [PATCH 068/179] Added Win percentage face --- watch-faces/complication/blackjack_face.c | 62 +++++++++++++++++++++-- watch-faces/complication/blackjack_face.h | 2 + 2 files changed, 60 insertions(+), 4 deletions(-) diff --git a/watch-faces/complication/blackjack_face.c b/watch-faces/complication/blackjack_face.c index 1497b7d6..3d549dca 100755 --- a/watch-faces/complication/blackjack_face.c +++ b/watch-faces/complication/blackjack_face.c @@ -60,6 +60,7 @@ typedef enum { BJ_DEALER_PLAYING, BJ_BUST, BJ_RESULT, + BJ_WIN_RATIO, } game_state_t; typedef enum { @@ -69,6 +70,8 @@ typedef enum { static game_state_t game_state; static uint8_t deck[DECK_SIZE] = {0}; static uint8_t current_card = 0; +static bool add_to_games_played = false; +static bool add_to_games_won = false; hand_info_t player; hand_info_t dealer; @@ -193,10 +196,15 @@ static void display_card_at_position(uint8_t card, uint8_t display_position) { } static void display_player_hand(void) { - uint8_t cards_to_display = player.idx_hand > MAX_PLAYER_CARDS_DISPLAY ? MAX_PLAYER_CARDS_DISPLAY : player.idx_hand; - for (uint8_t i=cards_to_display; i>0; i--) { - uint8_t card = player.hand[player.idx_hand-i]; - display_card_at_position(card, BOARD_DISPLAY_START + cards_to_display - i); + uint8_t card; + if (player.idx_hand <= MAX_PLAYER_CARDS_DISPLAY) { + card = player.hand[player.idx_hand - 1]; + display_card_at_position(card, BOARD_DISPLAY_START + player.idx_hand - 1); + } else { + for (uint8_t i=0; igames_played > 0) { // Avoid dividing by zero + win_ratio = (uint8_t)(100 * state->games_won) / state->games_played; + } + watch_display_text_with_fallback(WATCH_POSITION_TOP, "WINS ", "WR "); + sprintf(buf, "%3dPct", win_ratio); + watch_display_text(WATCH_POSITION_BOTTOM, buf); +} + static void begin_playing(bool tap_control_on) { watch_clear_display(); if (tap_control_on) { @@ -251,6 +276,7 @@ static void begin_playing(bool tap_control_on) { reset_hands(); // Give player their first 2 cards give_card(&player); + display_player_hand(); give_card(&player); display_player_hand(); display_score(player.score, WATCH_POSITION_SECONDS); @@ -304,6 +330,25 @@ static void see_if_dealer_hits(void) { } } +static void add_to_game_scores(blackjack_face_state_t *state) { + if (add_to_games_played) { + add_to_games_played = false; + state->games_played++; + if (state->games_played == 0) { + // Overflow + state->games_won = 0; + } + } + if (add_to_games_won) { + add_to_games_won = false; + state->games_won++; + if (state->games_won == 0) { + // Overflow + state->games_played = 0; + } + } +} + static void handle_button_presses(bool tap_control_on, bool hit) { switch (game_state) { @@ -324,6 +369,7 @@ static void handle_button_presses(bool tap_control_on, bool hit) { display_bust(); break; case BJ_RESULT: + case BJ_WIN_RATIO: display_title(); break; } @@ -362,6 +408,9 @@ void blackjack_face_activate(void *context) { bool blackjack_face_loop(movement_event_t event, void *context) { blackjack_face_state_t *state = (blackjack_face_state_t *) context; + if (game_state == BJ_RESULT) { + add_to_game_scores(state); + } switch (event.event_type) { case EVENT_ACTIVATE: if (state->tap_control_on) { @@ -390,6 +439,11 @@ bool blackjack_face_loop(movement_event_t event, void *context) { case EVENT_SINGLE_TAP: handle_button_presses(state->tap_control_on, true); break; + case EVENT_LIGHT_LONG_PRESS: + if (game_state == BJ_TITLE_SCREEN) { + display_win_ratio(state); + } + break; case EVENT_ALARM_LONG_PRESS: if (game_state == BJ_TITLE_SCREEN) { toggle_tap_control(state); diff --git a/watch-faces/complication/blackjack_face.h b/watch-faces/complication/blackjack_face.h index 84041517..c93143b9 100755 --- a/watch-faces/complication/blackjack_face.h +++ b/watch-faces/complication/blackjack_face.h @@ -71,6 +71,8 @@ typedef struct { bool tap_control_on; + uint16_t games_played; + uint16_t games_won; } blackjack_face_state_t; void blackjack_face_setup(uint8_t watch_face_index, void ** context_ptr); From a7c2cede060577ce716a3650aebc2bb0f593ffb8 Mon Sep 17 00:00:00 2001 From: David Volovskiy Date: Thu, 4 Sep 2025 18:29:07 -0400 Subject: [PATCH 069/179] Typo fix --- watch-faces/complication/blackjack_face.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/watch-faces/complication/blackjack_face.c b/watch-faces/complication/blackjack_face.c index 3d549dca..9db6db88 100755 --- a/watch-faces/complication/blackjack_face.c +++ b/watch-faces/complication/blackjack_face.c @@ -246,7 +246,7 @@ static void display_tie(void) { static void display_bust(void) { game_state = BJ_RESULT; add_to_games_played = true; - watch_display_text_with_fallback(WATCH_POSITION_BOTTOM, "8UST ", " BUST"); + watch_display_text_with_fallback(WATCH_POSITION_BOTTOM, "8UST", " BUST"); } static void display_title(void) { From 30c378a3ce2950f39532838a15427bfac220d213 Mon Sep 17 00:00:00 2001 From: David Volovskiy Date: Fri, 5 Sep 2025 17:07:24 -0400 Subject: [PATCH 070/179] Text fixes for classic display --- watch-faces/complication/blackjack_face.c | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/watch-faces/complication/blackjack_face.c b/watch-faces/complication/blackjack_face.c index 9db6db88..1ff1c2ed 100755 --- a/watch-faces/complication/blackjack_face.c +++ b/watch-faces/complication/blackjack_face.c @@ -246,12 +246,13 @@ static void display_tie(void) { static void display_bust(void) { game_state = BJ_RESULT; add_to_games_played = true; - watch_display_text_with_fallback(WATCH_POSITION_BOTTOM, "8UST", " BUST"); + watch_display_text_with_fallback(WATCH_POSITION_BOTTOM, "8UST", "BUST"); } static void display_title(void) { game_state = BJ_TITLE_SCREEN; - watch_display_text_with_fallback(WATCH_POSITION_TOP, "BLACK ", "21 "); + watch_display_text(WATCH_POSITION_TOP_RIGHT, " "); + watch_display_text_with_fallback(WATCH_POSITION_TOP, "BLACK ", "21"); watch_display_text_with_fallback(WATCH_POSITION_BOTTOM, " JACK ", "BLaKJK"); } @@ -262,7 +263,8 @@ static void display_win_ratio(blackjack_face_state_t *state) { if (state->games_played > 0) { // Avoid dividing by zero win_ratio = (uint8_t)(100 * state->games_won) / state->games_played; } - watch_display_text_with_fallback(WATCH_POSITION_TOP, "WINS ", "WR "); + watch_display_text(WATCH_POSITION_TOP_RIGHT, " "); + watch_display_text_with_fallback(WATCH_POSITION_TOP, "WINS ", "WR"); sprintf(buf, "%3dPct", win_ratio); watch_display_text(WATCH_POSITION_BOTTOM, buf); } From 2c4d3d7f503a26b32effb680bcbddde97f875519 Mon Sep 17 00:00:00 2001 From: David Volovskiy Date: Sat, 6 Sep 2025 12:07:17 -0400 Subject: [PATCH 071/179] Can now turn on LED with long press --- watch-faces/complication/blackjack_face.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/watch-faces/complication/blackjack_face.c b/watch-faces/complication/blackjack_face.c index 1ff1c2ed..4f7c89db 100755 --- a/watch-faces/complication/blackjack_face.c +++ b/watch-faces/complication/blackjack_face.c @@ -444,6 +444,8 @@ bool blackjack_face_loop(movement_event_t event, void *context) { case EVENT_LIGHT_LONG_PRESS: if (game_state == BJ_TITLE_SCREEN) { display_win_ratio(state); + } else { + movement_illuminate_led(); } break; case EVENT_ALARM_LONG_PRESS: From 1b805a273709b9db241e6f37364f230740b11cfc Mon Sep 17 00:00:00 2001 From: David Volovskiy Date: Sun, 7 Sep 2025 08:48:42 -0400 Subject: [PATCH 072/179] No longer remake deck before each shuffle --- watch-faces/complication/higher_lower_game_face.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/watch-faces/complication/higher_lower_game_face.c b/watch-faces/complication/higher_lower_game_face.c index 762c462a..d47e1f61 100755 --- a/watch-faces/complication/higher_lower_game_face.c +++ b/watch-faces/complication/higher_lower_game_face.c @@ -114,7 +114,6 @@ static void shuffle_deck(void) { static void reset_deck(void) { current_card = 0; - stack_deck(); shuffle_deck(); } @@ -358,6 +357,7 @@ void higher_lower_game_face_activate(void *context) { (void) state; // Handle any tasks related to your watch face coming on screen. game_state = HL_GS_TITLE_SCREEN; + stack_deck(); } bool higher_lower_game_face_loop(movement_event_t event, void *context) { From 9764c0f84dc8d23ebc7f09b070295f6a9815b0e4 Mon Sep 17 00:00:00 2001 From: David Volovskiy Date: Sun, 7 Sep 2025 08:51:47 -0400 Subject: [PATCH 073/179] We only stack the deck on activate and not before every new shuffle --- watch-faces/complication/blackjack_face.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/watch-faces/complication/blackjack_face.c b/watch-faces/complication/blackjack_face.c index 4f7c89db..7b538e6b 100755 --- a/watch-faces/complication/blackjack_face.c +++ b/watch-faces/complication/blackjack_face.c @@ -107,7 +107,6 @@ static void shuffle_deck(void) { static void reset_deck(void) { current_card = 0; - stack_deck(); shuffle_deck(); } @@ -406,6 +405,7 @@ void blackjack_face_activate(void *context) { blackjack_face_state_t *state = (blackjack_face_state_t *) context; (void) state; display_title(); + stack_deck(); } bool blackjack_face_loop(movement_event_t event, void *context) { From 320e7c13f97e90c80c651c9c47d4c7b1b82cfc92 Mon Sep 17 00:00:00 2001 From: David Volovskiy Date: Sun, 7 Sep 2025 15:10:35 -0400 Subject: [PATCH 074/179] Set time face isn't blank at the beginning of displaying it --- watch-faces/settings/set_time_face.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/watch-faces/settings/set_time_face.c b/watch-faces/settings/set_time_face.c index 63fc34fd..5d1a8abe 100644 --- a/watch-faces/settings/set_time_face.c +++ b/watch-faces/settings/set_time_face.c @@ -91,6 +91,8 @@ bool set_time_face_loop(movement_event_t event, void *context) { watch_date_time_t date_time = movement_get_local_date_time(); switch (event.event_type) { + case EVENT_ACTIVATE: + break; case EVENT_TICK: if (_quick_ticks_running) { if (HAL_GPIO_BTN_ALARM_read()) _handle_alarm_button(date_time, current_page); From 639f42c5d8d843fd91279d7828ab55916fb56cf4 Mon Sep 17 00:00:00 2001 From: David Volovskiy Date: Mon, 8 Sep 2025 19:44:14 -0400 Subject: [PATCH 075/179] Tapping turned off when not being used since tap control uses 90uA --- watch-faces/complication/blackjack_face.c | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/watch-faces/complication/blackjack_face.c b/watch-faces/complication/blackjack_face.c index 7b538e6b..ff4d73a4 100755 --- a/watch-faces/complication/blackjack_face.c +++ b/watch-faces/complication/blackjack_face.c @@ -67,6 +67,7 @@ typedef enum { A, B, C, D, E, F, G } segment_t; +static bool tap_turned_on = false; static game_state_t game_state; static uint8_t deck[DECK_SIZE] = {0}; static uint8_t current_card = 0; @@ -354,6 +355,9 @@ static void handle_button_presses(bool tap_control_on, bool hit) { switch (game_state) { case BJ_TITLE_SCREEN: + if (!tap_turned_on && tap_control_on) { + if (movement_enable_tap_detection_if_available()) tap_turned_on = true; + } begin_playing(tap_control_on); break; case BJ_PLAYING: @@ -415,14 +419,7 @@ bool blackjack_face_loop(movement_event_t event, void *context) { } switch (event.event_type) { case EVENT_ACTIVATE: - if (state->tap_control_on) { - bool tap_could_enable = movement_enable_tap_detection_if_available(); - if (tap_could_enable) { - watch_set_indicator(WATCH_INDICATOR_SIGNAL); - } else { - state->tap_control_on = false; - } - } + if (state->tap_control_on) watch_set_indicator(WATCH_INDICATOR_SIGNAL); break; case EVENT_TICK: if (game_state == BJ_DEALER_PLAYING) { @@ -454,6 +451,10 @@ bool blackjack_face_loop(movement_event_t event, void *context) { } break; case EVENT_TIMEOUT: + case EVENT_LOW_ENERGY_UPDATE: + if (tap_turned_on) { + movement_disable_tap_detection_if_available(); + } break; default: return movement_default_loop_handler(event); @@ -463,4 +464,8 @@ bool blackjack_face_loop(movement_event_t event, void *context) { void blackjack_face_resign(void *context) { (void) context; + if (tap_turned_on) { + tap_turned_on = false; + movement_disable_tap_detection_if_available(); + } } From af0051a160d9c41535b915314e18fee6820944a0 Mon Sep 17 00:00:00 2001 From: Alessandro Genova Date: Mon, 22 Sep 2025 23:42:47 -0400 Subject: [PATCH 076/179] fix int32 overflow when setting a year past 2067 --- watch-faces/settings/set_time_face.c | 2 +- watch-library/shared/watch/watch_utility.c | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/watch-faces/settings/set_time_face.c b/watch-faces/settings/set_time_face.c index 63fc34fd..f394347d 100644 --- a/watch-faces/settings/set_time_face.c +++ b/watch-faces/settings/set_time_face.c @@ -46,7 +46,7 @@ static void _handle_alarm_button(watch_date_time_t date_time, uint8_t current_pa current_offset = movement_get_current_timezone_offset_for_zone(movement_get_timezone_index()); return; case 0: // year - date_time.unit.year = ((date_time.unit.year % 60) + 1); + date_time.unit.year = (date_time.unit.year + 1) % 60; break; case 1: // month date_time.unit.month = (date_time.unit.month % 12) + 1; diff --git a/watch-library/shared/watch/watch_utility.c b/watch-library/shared/watch/watch_utility.c index 6d024032..7179aa72 100644 --- a/watch-library/shared/watch/watch_utility.c +++ b/watch-library/shared/watch/watch_utility.c @@ -205,7 +205,8 @@ uint32_t watch_utility_date_time_to_unix_time(watch_date_time_t date_time, int32 watch_date_time_t watch_utility_date_time_from_unix_time(uint32_t timestamp, int32_t utc_offset) { watch_date_time_t retval; retval.reg = 0; - int32_t days, secs; + uint32_t secs; + int32_t days; int32_t remdays, remsecs, remyears; int32_t qc_cycles, c_cycles, q_cycles; int32_t years, months; From 796f83a19f95dddcac565e23d1f9129d90a08cae Mon Sep 17 00:00:00 2001 From: David Volovskiy Date: Fri, 26 Sep 2025 10:02:20 -0400 Subject: [PATCH 077/179] Attempt at fixing blackjack win rate counter --- watch-faces/complication/blackjack_face.c | 51 ++++++++++------------- 1 file changed, 23 insertions(+), 28 deletions(-) diff --git a/watch-faces/complication/blackjack_face.c b/watch-faces/complication/blackjack_face.c index ff4d73a4..654b883e 100755 --- a/watch-faces/complication/blackjack_face.c +++ b/watch-faces/complication/blackjack_face.c @@ -71,8 +71,7 @@ static bool tap_turned_on = false; static game_state_t game_state; static uint8_t deck[DECK_SIZE] = {0}; static uint8_t current_card = 0; -static bool add_to_games_played = false; -static bool add_to_games_won = false; +static blackjack_face_state_t *g_state = NULL; hand_info_t player; hand_info_t dealer; @@ -219,10 +218,27 @@ static void display_score(uint8_t score, watch_position_t pos) { watch_display_text(pos, buf); } +static void add_to_game_scores(bool add_to_wins) { + g_state->games_played++; + if (g_state->games_played == 0) { + // Overflow + g_state->games_played = 1; + g_state->games_won = add_to_wins ? 1 : 0; + return; + } + if (add_to_wins) { + g_state->games_won++; + if (g_state->games_won == 0) { + // Overflow + g_state->games_played = 1; + g_state->games_won = 1; + } + } +} + static void display_win(void) { game_state = BJ_RESULT; - add_to_games_played = true; - add_to_games_won = true; + add_to_game_scores(true); watch_display_text_with_fallback(WATCH_POSITION_BOTTOM, "WlN ", " WIN"); display_score(player.score, WATCH_POSITION_SECONDS); display_score(dealer.score, WATCH_POSITION_TOP_RIGHT); @@ -230,7 +246,7 @@ static void display_win(void) { static void display_lose(void) { game_state = BJ_RESULT; - add_to_games_played = true; + add_to_game_scores(false); watch_display_text_with_fallback(WATCH_POSITION_BOTTOM, "LOSE", "lOSE"); display_score(player.score, WATCH_POSITION_SECONDS); display_score(dealer.score, WATCH_POSITION_TOP_RIGHT); @@ -245,7 +261,7 @@ static void display_tie(void) { static void display_bust(void) { game_state = BJ_RESULT; - add_to_games_played = true; + add_to_game_scores(false); watch_display_text_with_fallback(WATCH_POSITION_BOTTOM, "8UST", "BUST"); } @@ -332,25 +348,6 @@ static void see_if_dealer_hits(void) { } } -static void add_to_game_scores(blackjack_face_state_t *state) { - if (add_to_games_played) { - add_to_games_played = false; - state->games_played++; - if (state->games_played == 0) { - // Overflow - state->games_won = 0; - } - } - if (add_to_games_won) { - add_to_games_won = false; - state->games_won++; - if (state->games_won == 0) { - // Overflow - state->games_played = 0; - } - } -} - static void handle_button_presses(bool tap_control_on, bool hit) { switch (game_state) { @@ -403,6 +400,7 @@ void blackjack_face_setup(uint8_t watch_face_index, void **context_ptr) { blackjack_face_state_t *state = (blackjack_face_state_t *)*context_ptr; state->tap_control_on = false; } + g_state = (blackjack_face_state_t *)*context_ptr; } void blackjack_face_activate(void *context) { @@ -414,9 +412,6 @@ void blackjack_face_activate(void *context) { bool blackjack_face_loop(movement_event_t event, void *context) { blackjack_face_state_t *state = (blackjack_face_state_t *) context; - if (game_state == BJ_RESULT) { - add_to_game_scores(state); - } switch (event.event_type) { case EVENT_ACTIVATE: if (state->tap_control_on) watch_set_indicator(WATCH_INDICATOR_SIGNAL); From 870fd8d01f60987c7815922c695b9c28ccece467 Mon Sep 17 00:00:00 2001 From: David Volovskiy Date: Sun, 19 Oct 2025 12:16:15 -0400 Subject: [PATCH 078/179] Included return value of I2C calls --- watch-library/hardware/watch/watch_i2c.c | 48 ++++++++++++++--------- watch-library/shared/watch/watch_i2c.h | 10 +++-- watch-library/simulator/watch/watch_i2c.c | 12 ++++-- 3 files changed, 46 insertions(+), 24 deletions(-) diff --git a/watch-library/hardware/watch/watch_i2c.c b/watch-library/hardware/watch/watch_i2c.c index 709c63b7..ef898799 100644 --- a/watch-library/hardware/watch/watch_i2c.c +++ b/watch-library/hardware/watch/watch_i2c.c @@ -23,7 +23,6 @@ */ #include "watch_i2c.h" -#include "i2c.h" #ifdef I2C_SERCOM @@ -38,27 +37,31 @@ void watch_disable_i2c(void) { i2c_disable(); } -void watch_i2c_send(int16_t addr, uint8_t *buf, uint16_t length) { - i2c_write(addr, buf, length); +i2c_result_t watch_i2c_send(int16_t addr, uint8_t *buf, uint16_t length) { + return i2c_write(addr, buf, length); } -void watch_i2c_receive(int16_t addr, uint8_t *buf, uint16_t length) { - i2c_read(addr, buf, length); +i2c_result_t watch_i2c_receive(int16_t addr, uint8_t *buf, uint16_t length) { + return i2c_read(addr, buf, length); } -void watch_i2c_write8(int16_t addr, uint8_t reg, uint8_t data) { +i2c_result_t watch_i2c_write8(int16_t addr, uint8_t reg, uint8_t data) { uint8_t buf[2]; buf[0] = reg; buf[1] = data; - watch_i2c_send(addr, (uint8_t *)&buf, 2); + return watch_i2c_send(addr, (uint8_t *)&buf, 2); } uint8_t watch_i2c_read8(int16_t addr, uint8_t reg) { uint8_t data; - watch_i2c_send(addr, (uint8_t *)®, 1); - watch_i2c_receive(addr, (uint8_t *)&data, 1); + if (watch_i2c_send(addr, (uint8_t *)®, 1) != I2C_RESULT_SUCCESS) { + return 0; + } + if (watch_i2c_receive(addr, (uint8_t *)&data, 1) != I2C_RESULT_SUCCESS) { + return 0; + } return data; } @@ -66,9 +69,12 @@ uint8_t watch_i2c_read8(int16_t addr, uint8_t reg) { uint16_t watch_i2c_read16(int16_t addr, uint8_t reg) { uint16_t data; - watch_i2c_send(addr, (uint8_t *)®, 1); - watch_i2c_receive(addr, (uint8_t *)&data, 2); - + if (watch_i2c_send(addr, (uint8_t *)®, 1) != I2C_RESULT_SUCCESS) { + return 0; + } + if (watch_i2c_receive(addr, (uint8_t *)&data, 2) != I2C_RESULT_SUCCESS) { + return 0; + } return data; } @@ -76,18 +82,24 @@ uint32_t watch_i2c_read24(int16_t addr, uint8_t reg) { uint32_t data; data = 0; - watch_i2c_send(addr, (uint8_t *)®, 1); - watch_i2c_receive(addr, (uint8_t *)&data, 3); - + if (watch_i2c_send(addr, (uint8_t *)®, 1) != I2C_RESULT_SUCCESS) { + return 0; + } + if (watch_i2c_receive(addr, (uint8_t *)&data, 3) != I2C_RESULT_SUCCESS) { + return 0; + } return data << 8; } uint32_t watch_i2c_read32(int16_t addr, uint8_t reg) { uint32_t data; - watch_i2c_send(addr, (uint8_t *)®, 1); - watch_i2c_receive(addr, (uint8_t *)&data, 4); - + if (watch_i2c_send(addr, (uint8_t *)®, 1) != I2C_RESULT_SUCCESS) { + return 0; + } + if (watch_i2c_receive(addr, (uint8_t *)&data, 4) != I2C_RESULT_SUCCESS) { + return 0; + } return data; } diff --git a/watch-library/shared/watch/watch_i2c.h b/watch-library/shared/watch/watch_i2c.h index fbcc1a92..db131eb1 100644 --- a/watch-library/shared/watch/watch_i2c.h +++ b/watch-library/shared/watch/watch_i2c.h @@ -26,6 +26,7 @@ ////< @file watch_i2c.h #include "watch.h" +#include "i2c.h" /** @addtogroup i2c I2C Controller Driver * @brief This section covers functions related to the SAM L22's built-I2C driver, including @@ -45,22 +46,25 @@ void watch_disable_i2c(void); * @param addr The address of the device you wish to talk to. * @param buf A series of unsigned bytes; the data you wish to transmit. * @param length The number of bytes in buf that you wish to send. + * @return 0 if no error code, otherwise a code via i2c_result_t */ -void watch_i2c_send(int16_t addr, uint8_t *buf, uint16_t length); +i2c_result_t watch_i2c_send(int16_t addr, uint8_t *buf, uint16_t length); /** @brief Receives a series of values from a device on the I2C bus. * @param addr The address of the device you wish to hear from. * @param buf Storage for the incoming bytes; on return, it will contain the received data. * @param length The number of bytes that you wish to receive. + * @return 0 if no error code, otherwise a code via i2c_result_t */ -void watch_i2c_receive(int16_t addr, uint8_t *buf, uint16_t length); +i2c_result_t watch_i2c_receive(int16_t addr, uint8_t *buf, uint16_t length); /** @brief Writes a byte to a register in an I2C device. * @param addr The address of the device you wish to address. * @param reg The register on the device that you wish to set. * @param data The value that you wish to set the register to. + * @return 0 if no error code, otherwise a code via i2c_result_t */ -void watch_i2c_write8(int16_t addr, uint8_t reg, uint8_t data); +i2c_result_t watch_i2c_write8(int16_t addr, uint8_t reg, uint8_t data); /** @brief Reads a byte from a register in an I2C device. * @param addr The address of the device you wish to address. diff --git a/watch-library/simulator/watch/watch_i2c.c b/watch-library/simulator/watch/watch_i2c.c index 09339888..fea9de5c 100644 --- a/watch-library/simulator/watch/watch_i2c.c +++ b/watch-library/simulator/watch/watch_i2c.c @@ -28,11 +28,17 @@ void watch_enable_i2c(void) {} void watch_disable_i2c(void) {} -void watch_i2c_send(int16_t addr, uint8_t *buf, uint16_t length) {} +i2c_result_t watch_i2c_send(int16_t addr, uint8_t *buf, uint16_t length) { + return I2C_RESULT_SUCCESS; +} -void watch_i2c_receive(int16_t addr, uint8_t *buf, uint16_t length) {} +i2c_result_t watch_i2c_receive(int16_t addr, uint8_t *buf, uint16_t length) { + return I2C_RESULT_SUCCESS; +} -void watch_i2c_write8(int16_t addr, uint8_t reg, uint8_t data) {} +i2c_result_t watch_i2c_write8(int16_t addr, uint8_t reg, uint8_t data) { + return I2C_RESULT_SUCCESS; +} uint8_t watch_i2c_read8(int16_t addr, uint8_t reg) { return 0; From 85742166126fd850092c0ef61fcca3ab9eff8c8a Mon Sep 17 00:00:00 2001 From: David Volovskiy Date: Sun, 19 Oct 2025 12:58:07 -0400 Subject: [PATCH 079/179] Moved away from i2c_result_t to int8_t --- watch-library/hardware/watch/watch_i2c.c | 29 ++++++++++++----------- watch-library/shared/watch/watch_i2c.h | 7 +++--- watch-library/simulator/watch/watch_i2c.c | 12 +++++----- 3 files changed, 24 insertions(+), 24 deletions(-) diff --git a/watch-library/hardware/watch/watch_i2c.c b/watch-library/hardware/watch/watch_i2c.c index ef898799..1a89f80c 100644 --- a/watch-library/hardware/watch/watch_i2c.c +++ b/watch-library/hardware/watch/watch_i2c.c @@ -23,6 +23,7 @@ */ #include "watch_i2c.h" +#include "i2c.h" #ifdef I2C_SERCOM @@ -37,29 +38,29 @@ void watch_disable_i2c(void) { i2c_disable(); } -i2c_result_t watch_i2c_send(int16_t addr, uint8_t *buf, uint16_t length) { - return i2c_write(addr, buf, length); +int8_t watch_i2c_send(int16_t addr, uint8_t *buf, uint16_t length) { + return (int8_t)i2c_write(addr, buf, length); } -i2c_result_t watch_i2c_receive(int16_t addr, uint8_t *buf, uint16_t length) { - return i2c_read(addr, buf, length); +int8_t watch_i2c_receive(int16_t addr, uint8_t *buf, uint16_t length) { + return (int8_t)i2c_read(addr, buf, length); } -i2c_result_t watch_i2c_write8(int16_t addr, uint8_t reg, uint8_t data) { +int8_t watch_i2c_write8(int16_t addr, uint8_t reg, uint8_t data) { uint8_t buf[2]; buf[0] = reg; buf[1] = data; - return watch_i2c_send(addr, (uint8_t *)&buf, 2); + return (int8_t)watch_i2c_send(addr, (uint8_t *)&buf, 2); } uint8_t watch_i2c_read8(int16_t addr, uint8_t reg) { uint8_t data; - if (watch_i2c_send(addr, (uint8_t *)®, 1) != I2C_RESULT_SUCCESS) { + if (watch_i2c_send(addr, (uint8_t *)®, 1) != 0) { return 0; } - if (watch_i2c_receive(addr, (uint8_t *)&data, 1) != I2C_RESULT_SUCCESS) { + if (watch_i2c_receive(addr, (uint8_t *)&data, 1) != 0) { return 0; } @@ -69,10 +70,10 @@ uint8_t watch_i2c_read8(int16_t addr, uint8_t reg) { uint16_t watch_i2c_read16(int16_t addr, uint8_t reg) { uint16_t data; - if (watch_i2c_send(addr, (uint8_t *)®, 1) != I2C_RESULT_SUCCESS) { + if (watch_i2c_send(addr, (uint8_t *)®, 1) != 0) { return 0; } - if (watch_i2c_receive(addr, (uint8_t *)&data, 2) != I2C_RESULT_SUCCESS) { + if (watch_i2c_receive(addr, (uint8_t *)&data, 2) != 0) { return 0; } return data; @@ -82,10 +83,10 @@ uint32_t watch_i2c_read24(int16_t addr, uint8_t reg) { uint32_t data; data = 0; - if (watch_i2c_send(addr, (uint8_t *)®, 1) != I2C_RESULT_SUCCESS) { + if (watch_i2c_send(addr, (uint8_t *)®, 1) != 0) { return 0; } - if (watch_i2c_receive(addr, (uint8_t *)&data, 3) != I2C_RESULT_SUCCESS) { + if (watch_i2c_receive(addr, (uint8_t *)&data, 3) != 0) { return 0; } return data << 8; @@ -94,10 +95,10 @@ uint32_t watch_i2c_read24(int16_t addr, uint8_t reg) { uint32_t watch_i2c_read32(int16_t addr, uint8_t reg) { uint32_t data; - if (watch_i2c_send(addr, (uint8_t *)®, 1) != I2C_RESULT_SUCCESS) { + if (watch_i2c_send(addr, (uint8_t *)®, 1) != 0) { return 0; } - if (watch_i2c_receive(addr, (uint8_t *)&data, 4) != I2C_RESULT_SUCCESS) { + if (watch_i2c_receive(addr, (uint8_t *)&data, 4) != 0) { return 0; } return data; diff --git a/watch-library/shared/watch/watch_i2c.h b/watch-library/shared/watch/watch_i2c.h index db131eb1..18d07b38 100644 --- a/watch-library/shared/watch/watch_i2c.h +++ b/watch-library/shared/watch/watch_i2c.h @@ -26,7 +26,6 @@ ////< @file watch_i2c.h #include "watch.h" -#include "i2c.h" /** @addtogroup i2c I2C Controller Driver * @brief This section covers functions related to the SAM L22's built-I2C driver, including @@ -48,7 +47,7 @@ void watch_disable_i2c(void); * @param length The number of bytes in buf that you wish to send. * @return 0 if no error code, otherwise a code via i2c_result_t */ -i2c_result_t watch_i2c_send(int16_t addr, uint8_t *buf, uint16_t length); +int8_t watch_i2c_send(int16_t addr, uint8_t *buf, uint16_t length); /** @brief Receives a series of values from a device on the I2C bus. * @param addr The address of the device you wish to hear from. @@ -56,7 +55,7 @@ i2c_result_t watch_i2c_send(int16_t addr, uint8_t *buf, uint16_t length); * @param length The number of bytes that you wish to receive. * @return 0 if no error code, otherwise a code via i2c_result_t */ -i2c_result_t watch_i2c_receive(int16_t addr, uint8_t *buf, uint16_t length); +int8_t watch_i2c_receive(int16_t addr, uint8_t *buf, uint16_t length); /** @brief Writes a byte to a register in an I2C device. * @param addr The address of the device you wish to address. @@ -64,7 +63,7 @@ i2c_result_t watch_i2c_receive(int16_t addr, uint8_t *buf, uint16_t length); * @param data The value that you wish to set the register to. * @return 0 if no error code, otherwise a code via i2c_result_t */ -i2c_result_t watch_i2c_write8(int16_t addr, uint8_t reg, uint8_t data); +int8_t watch_i2c_write8(int16_t addr, uint8_t reg, uint8_t data); /** @brief Reads a byte from a register in an I2C device. * @param addr The address of the device you wish to address. diff --git a/watch-library/simulator/watch/watch_i2c.c b/watch-library/simulator/watch/watch_i2c.c index fea9de5c..8086fb75 100644 --- a/watch-library/simulator/watch/watch_i2c.c +++ b/watch-library/simulator/watch/watch_i2c.c @@ -28,16 +28,16 @@ void watch_enable_i2c(void) {} void watch_disable_i2c(void) {} -i2c_result_t watch_i2c_send(int16_t addr, uint8_t *buf, uint16_t length) { - return I2C_RESULT_SUCCESS; +int8_t watch_i2c_send(int16_t addr, uint8_t *buf, uint16_t length) { + return 0; } -i2c_result_t watch_i2c_receive(int16_t addr, uint8_t *buf, uint16_t length) { - return I2C_RESULT_SUCCESS; +int8_t watch_i2c_receive(int16_t addr, uint8_t *buf, uint16_t length) { + return 0; } -i2c_result_t watch_i2c_write8(int16_t addr, uint8_t reg, uint8_t data) { - return I2C_RESULT_SUCCESS; +int8_t watch_i2c_write8(int16_t addr, uint8_t reg, uint8_t data) { + return 0; } uint8_t watch_i2c_read8(int16_t addr, uint8_t reg) { From 6609b3f79a6a45ace613f9d803bf6b3d686a5f47 Mon Sep 17 00:00:00 2001 From: David Volovskiy Date: Sat, 15 Nov 2025 12:48:16 -0500 Subject: [PATCH 080/179] LIS2DW tap mode uses low-power mode --- movement.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/movement.c b/movement.c index ca0b0f36..0a41b306 100644 --- a/movement.c +++ b/movement.c @@ -521,7 +521,7 @@ bool movement_enable_tap_detection_if_available(void) { // ramp data rate up to 400 Hz and high performance mode lis2dw_set_low_noise_mode(true); lis2dw_set_data_rate(LIS2DW_DATA_RATE_HP_400_HZ); - lis2dw_set_mode(LIS2DW_MODE_HIGH_PERFORMANCE); + lis2dw_set_mode(LIS2DW_MODE_LOW_POWER); // Settling time (1 sample duration, i.e. 1/400Hz) delay_ms(3); From eaae42cca99c84ace0cd8d2cf3989725bc56eef7 Mon Sep 17 00:00:00 2001 From: David Volovskiy Date: Sat, 15 Nov 2025 12:47:53 -0500 Subject: [PATCH 081/179] Added double-tap capabilities to LIS2DW --- movement.c | 6 ++++-- watch-library/shared/driver/lis2dw.c | 14 ++++++++++++++ watch-library/shared/driver/lis2dw.h | 4 ++++ 3 files changed, 22 insertions(+), 2 deletions(-) diff --git a/movement.c b/movement.c index ca0b0f36..5f2c5475 100644 --- a/movement.c +++ b/movement.c @@ -516,18 +516,19 @@ bool movement_enable_tap_detection_if_available(void) { if (movement_state.has_lis2dw) { // configure tap duration threshold and enable Z axis lis2dw_configure_tap_threshold(0, 0, 12, LIS2DW_REG_TAP_THS_Z_Z_AXIS_ENABLE); - lis2dw_configure_tap_duration(10, 2, 2); + lis2dw_configure_tap_duration(2, 2, 2); // ramp data rate up to 400 Hz and high performance mode lis2dw_set_low_noise_mode(true); lis2dw_set_data_rate(LIS2DW_DATA_RATE_HP_400_HZ); lis2dw_set_mode(LIS2DW_MODE_HIGH_PERFORMANCE); + lis2dw_enable_double_tap(); // Settling time (1 sample duration, i.e. 1/400Hz) delay_ms(3); // enable tap detection on INT1/A3. - lis2dw_configure_int1(LIS2DW_CTRL4_INT1_SINGLE_TAP | LIS2DW_CTRL4_INT1_6D); + lis2dw_configure_int1(LIS2DW_CTRL4_INT1_SINGLE_TAP | LIS2DW_CTRL4_INT1_DOUBLE_TAP | LIS2DW_CTRL4_INT1_6D); return true; } @@ -541,6 +542,7 @@ bool movement_disable_tap_detection_if_available(void) { lis2dw_set_low_noise_mode(false); lis2dw_set_data_rate(movement_state.accelerometer_background_rate); lis2dw_set_mode(LIS2DW_MODE_LOW_POWER); + lis2dw_disable_double_tap(); // ...disable Z axis (not sure if this is needed, does this save power?)... lis2dw_configure_tap_threshold(0, 0, 0, 0); diff --git a/watch-library/shared/driver/lis2dw.c b/watch-library/shared/driver/lis2dw.c index b716f790..1c89f244 100644 --- a/watch-library/shared/driver/lis2dw.c +++ b/watch-library/shared/driver/lis2dw.c @@ -302,6 +302,20 @@ void lis2dw_clear_fifo(void) { #endif } +void lis2dw_enable_double_tap(void) { +#ifdef I2C_SERCOM + uint8_t configuration = watch_i2c_read8(LIS2DW_ADDRESS, LIS2DW_REG_WAKE_UP_THS); + watch_i2c_write8(LIS2DW_ADDRESS, LIS2DW_REG_WAKE_UP_THS, configuration | LIS2DW_WAKE_UP_THS_ENABLE_DOUBLE_TAP); +#endif +} + +void lis2dw_disable_double_tap(void) { +#ifdef I2C_SERCOM + uint8_t configuration = watch_i2c_read8(LIS2DW_ADDRESS, LIS2DW_REG_WAKE_UP_THS); + watch_i2c_write8(LIS2DW_ADDRESS, LIS2DW_REG_WAKE_UP_THS, configuration & ~LIS2DW_WAKE_UP_THS_ENABLE_DOUBLE_TAP); +#endif +} + void lis2dw_enable_sleep(void) { #ifdef I2C_SERCOM uint8_t configuration = watch_i2c_read8(LIS2DW_ADDRESS, LIS2DW_REG_WAKE_UP_THS); diff --git a/watch-library/shared/driver/lis2dw.h b/watch-library/shared/driver/lis2dw.h index 57ff05b2..3b017c65 100644 --- a/watch-library/shared/driver/lis2dw.h +++ b/watch-library/shared/driver/lis2dw.h @@ -343,6 +343,10 @@ bool lis2dw_read_fifo(lis2dw_fifo_t *fifo_data); void lis2dw_clear_fifo(void); +void lis2dw_enable_double_tap(void); + +void lis2dw_disable_double_tap(void); + void lis2dw_enable_sleep(void); void lis2dw_disable_sleep(void); From 437e513d1f416b2df2901e636263bd7d5488fec4 Mon Sep 17 00:00:00 2001 From: David Volovskiy Date: Sun, 16 Nov 2025 08:28:46 -0500 Subject: [PATCH 082/179] Stand on 21 if we ask for a hit --- watch-faces/complication/blackjack_face.c | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/watch-faces/complication/blackjack_face.c b/watch-faces/complication/blackjack_face.c index 654b883e..375e0435 100755 --- a/watch-faces/complication/blackjack_face.c +++ b/watch-faces/complication/blackjack_face.c @@ -303,9 +303,16 @@ static void begin_playing(bool tap_control_on) { display_score(dealer.score, WATCH_POSITION_TOP_RIGHT); } +static void perform_stand(void) { + game_state = BJ_DEALER_PLAYING; + watch_display_text(WATCH_POSITION_BOTTOM, "Stnd"); + display_score(player.score, WATCH_POSITION_SECONDS); +} + static void perform_hit(void) { if (player.score == 21) { - return; // Assume hitting on 21 is a mistake and ignore + perform_stand(); + return; // Assume hitting on 21 is a mistake and stand } give_card(&player); if (player.score > 21) { @@ -315,12 +322,6 @@ static void perform_hit(void) { display_score(player.score, WATCH_POSITION_SECONDS); } -static void perform_stand(void) { - game_state = BJ_DEALER_PLAYING; - watch_display_text(WATCH_POSITION_BOTTOM, "Stnd"); - display_score(player.score, WATCH_POSITION_SECONDS); -} - static void dealer_performs_hits(void) { give_card(&dealer); display_dealer_hand(); From 6f08545e127d353ecdfec2d7415b40cf8f7001ee Mon Sep 17 00:00:00 2001 From: Daniel Bergman Date: Sat, 9 Aug 2025 20:20:10 +0200 Subject: [PATCH 083/179] Fix compiler warnings: missing prototype, unused variables, implicit declaration --- watch-faces/complication/simple_coin_flip_face.c | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/watch-faces/complication/simple_coin_flip_face.c b/watch-faces/complication/simple_coin_flip_face.c index 446832be..635c70ff 100644 --- a/watch-faces/complication/simple_coin_flip_face.c +++ b/watch-faces/complication/simple_coin_flip_face.c @@ -26,6 +26,7 @@ #include #include #include "simple_coin_flip_face.h" +#include "delay.h" void simple_coin_flip_face_setup(uint8_t watch_face_index, void ** context_ptr) { (void) watch_face_index; @@ -36,7 +37,7 @@ void simple_coin_flip_face_setup(uint8_t watch_face_index, void ** context_ptr) } void simple_coin_flip_face_activate(void *context) { - simple_coin_flip_face_state_t *state = (simple_coin_flip_face_state_t *)context; + (void) context; } static uint32_t get_random(uint32_t max) { @@ -48,7 +49,7 @@ static uint32_t get_random(uint32_t max) { } -void draw_start_face() { +static void draw_start_face(void) { watch_clear_display(); if (watch_get_lcd_type() == WATCH_LCD_TYPE_CLASSIC) { watch_display_text(WATCH_POSITION_BOTTOM, " Flip"); @@ -57,7 +58,7 @@ void draw_start_face() { } } -void set_pixels(int pixels[3][4][2], int j_len) { +static void set_pixels(int pixels[3][4][2], int j_len) { for(int loopruns = 0; loopruns<2; loopruns++) { for(int i = 0; i<3; i++) { watch_clear_display(); @@ -69,7 +70,7 @@ void set_pixels(int pixels[3][4][2], int j_len) { } } -void load_animation() { +static void load_animation(void) { if (watch_get_lcd_type() == WATCH_LCD_TYPE_CLASSIC) { int j_len = 2; int pixels[3][4][2] = { @@ -114,6 +115,7 @@ void load_animation() { } static void _blink_face_update_lcd(simple_coin_flip_face_state_t *state) { + (void) state; watch_clear_display(); load_animation(); watch_clear_display(); From 3e4c6f23637d113df4fd9e2c1a0ebd95bb03f646 Mon Sep 17 00:00:00 2001 From: Ante Vukorepa Date: Fri, 14 Nov 2025 19:32:51 +0100 Subject: [PATCH 084/179] Make deadline face respect button volumes Just blindly replaced watch_buzzer_play_note with watch_buzzer_play_note_with_volume --- watch-faces/complication/deadline_face.c | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/watch-faces/complication/deadline_face.c b/watch-faces/complication/deadline_face.c index 41240e76..d3204f20 100644 --- a/watch-faces/complication/deadline_face.c +++ b/watch-faces/complication/deadline_face.c @@ -161,19 +161,19 @@ static inline void _beep(beep_type_t beep_type) switch (beep_type) { case BEEP_BUTTON: - watch_buzzer_play_note(BUZZER_NOTE_C7, 50); + watch_buzzer_play_note_with_volume(BUZZER_NOTE_C7, 50, movement_button_volume()); break; case BEEP_ENABLE: - watch_buzzer_play_note(BUZZER_NOTE_G7, 50); + watch_buzzer_play_note_with_volume(BUZZER_NOTE_G7, 50, movement_button_volume()); watch_buzzer_play_note(BUZZER_NOTE_REST, 75); - watch_buzzer_play_note(BUZZER_NOTE_C8, 75); + watch_buzzer_play_note_with_volume(BUZZER_NOTE_C8, 50, movement_button_volume()); break; case BEEP_DISABLE: - watch_buzzer_play_note(BUZZER_NOTE_C8, 50); + watch_buzzer_play_note_with_volume(BUZZER_NOTE_C8, 50, movement_button_volume()); watch_buzzer_play_note(BUZZER_NOTE_REST, 75); - watch_buzzer_play_note(BUZZER_NOTE_G7, 75); + watch_buzzer_play_note_with_volume(BUZZER_NOTE_G7, 50, movement_button_volume()); break; } } From 998a1350788908ae7c5cc5f727c78e0433e91ae2 Mon Sep 17 00:00:00 2001 From: michael-lachmann <43858037+michael-lachmann@users.noreply.github.com> Date: Fri, 21 Nov 2025 20:28:41 -0700 Subject: [PATCH 085/179] Avoid out-of-range character When character is out of range, this will access illegal memory areas. In watch_display_text() it also makes sense to stop on char='\0' even when in position 0. --- watch-library/shared/watch/watch_common_display.c | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/watch-library/shared/watch/watch_common_display.c b/watch-library/shared/watch/watch_common_display.c index 86ab352d..1e24a5b1 100644 --- a/watch-library/shared/watch/watch_common_display.c +++ b/watch-library/shared/watch/watch_common_display.c @@ -43,6 +43,8 @@ uint8_t IndicatorSegments[8] = { }; void watch_display_character(uint8_t character, uint8_t position) { + if((character-0x20 < 0) | (character-0x20 >= sizeof(Classic_LCD_Character_Set)) return ; + if (watch_get_lcd_type() == WATCH_LCD_TYPE_CUSTOM) { if (character == 'R' && position > 1 && position < 8) character = 'r'; // We can't display uppercase R in these positions else if (character == 'T' && position > 1 && position < 8) character = 't'; // lowercase t is the only option for these positions @@ -125,9 +127,9 @@ void watch_display_character(uint8_t character, uint8_t position) { void watch_display_character_lp_seconds(uint8_t character, uint8_t position) { // Will only work for digits and for positions 8 and 9 - but less code & checks to reduce power consumption - digit_mapping_t segmap; uint8_t segdata; + if(character < 20) return ; /// TODO: See optimization note above. @@ -169,6 +171,7 @@ void watch_display_string(const char *string, uint8_t position) { } void watch_display_text(watch_position_t location, const char *string) { + if(!string[0]) return ; switch (location) { case WATCH_POSITION_TOP: case WATCH_POSITION_TOP_LEFT: From a139dc81b100040908dbcaf2b8d0bafbf26453f7 Mon Sep 17 00:00:00 2001 From: David Volovskiy Date: Sat, 22 Nov 2025 07:44:36 -0500 Subject: [PATCH 086/179] Resized baby kick buffers to get rid of truncation warnings --- watch-faces/complication/baby_kicks_face.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/watch-faces/complication/baby_kicks_face.c b/watch-faces/complication/baby_kicks_face.c index bfbc0756..cc0dd513 100644 --- a/watch-faces/complication/baby_kicks_face.c +++ b/watch-faces/complication/baby_kicks_face.c @@ -210,7 +210,7 @@ static inline void _display_counts(baby_kicks_state_t *state) { watch_display_text(WATCH_POSITION_BOTTOM, "baby "); watch_clear_colon(); } else { - char buf[7]; + char buf[9]; snprintf( buf, @@ -259,7 +259,7 @@ static void _display_elapsed_minutes(baby_kicks_state_t *state) { * the total elapsed minutes. */ - char buf[3]; + char buf[5]; uint32_t elapsed_minutes = _elapsed_minutes(state); uint8_t multiple = elapsed_minutes / 30; uint8_t remainder = elapsed_minutes % 30; From d5704891ef349d6224196270ec397f6eb4f430d1 Mon Sep 17 00:00:00 2001 From: voloved <36523934+voloved@users.noreply.github.com> Date: Sat, 22 Nov 2025 08:24:58 -0500 Subject: [PATCH 087/179] Revert "Avoid out-of-range character" --- watch-library/shared/watch/watch_common_display.c | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/watch-library/shared/watch/watch_common_display.c b/watch-library/shared/watch/watch_common_display.c index 1e24a5b1..86ab352d 100644 --- a/watch-library/shared/watch/watch_common_display.c +++ b/watch-library/shared/watch/watch_common_display.c @@ -43,8 +43,6 @@ uint8_t IndicatorSegments[8] = { }; void watch_display_character(uint8_t character, uint8_t position) { - if((character-0x20 < 0) | (character-0x20 >= sizeof(Classic_LCD_Character_Set)) return ; - if (watch_get_lcd_type() == WATCH_LCD_TYPE_CUSTOM) { if (character == 'R' && position > 1 && position < 8) character = 'r'; // We can't display uppercase R in these positions else if (character == 'T' && position > 1 && position < 8) character = 't'; // lowercase t is the only option for these positions @@ -127,9 +125,9 @@ void watch_display_character(uint8_t character, uint8_t position) { void watch_display_character_lp_seconds(uint8_t character, uint8_t position) { // Will only work for digits and for positions 8 and 9 - but less code & checks to reduce power consumption + digit_mapping_t segmap; uint8_t segdata; - if(character < 20) return ; /// TODO: See optimization note above. @@ -171,7 +169,6 @@ void watch_display_string(const char *string, uint8_t position) { } void watch_display_text(watch_position_t location, const char *string) { - if(!string[0]) return ; switch (location) { case WATCH_POSITION_TOP: case WATCH_POSITION_TOP_LEFT: From cee51b8595cc6f0c6fb310ddb3f5836a3615b18c Mon Sep 17 00:00:00 2001 From: David Volovskiy Date: Sat, 22 Nov 2025 08:41:38 -0500 Subject: [PATCH 088/179] Removed extra newline at end --- watch-faces/complication/wordle_face.c | 1 - 1 file changed, 1 deletion(-) diff --git a/watch-faces/complication/wordle_face.c b/watch-faces/complication/wordle_face.c index 4bc9fa15..d63ef467 100644 --- a/watch-faces/complication/wordle_face.c +++ b/watch-faces/complication/wordle_face.c @@ -646,4 +646,3 @@ bool wordle_face_loop(movement_event_t event, void *context) { void wordle_face_resign(void *context) { (void) context; } - From 5b6b415b869ade7c2d286aa775d9e4e00f61859b Mon Sep 17 00:00:00 2001 From: David Volovskiy Date: Sat, 22 Nov 2025 08:49:54 -0500 Subject: [PATCH 089/179] Removed 6D interrupt --- movement.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/movement.c b/movement.c index 5f2c5475..c23cf455 100644 --- a/movement.c +++ b/movement.c @@ -528,7 +528,7 @@ bool movement_enable_tap_detection_if_available(void) { delay_ms(3); // enable tap detection on INT1/A3. - lis2dw_configure_int1(LIS2DW_CTRL4_INT1_SINGLE_TAP | LIS2DW_CTRL4_INT1_DOUBLE_TAP | LIS2DW_CTRL4_INT1_6D); + lis2dw_configure_int1(LIS2DW_CTRL4_INT1_SINGLE_TAP | LIS2DW_CTRL4_INT1_DOUBLE_TAP); return true; } From cb344dea222004e30830fd2db258c5ada3e8a928 Mon Sep 17 00:00:00 2001 From: ciuffo Date: Sun, 23 Nov 2025 13:32:05 -0800 Subject: [PATCH 090/179] Fix for wrong sunrise/sunset in some timezones --- watch-faces/complication/sunrise_sunset_face.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/watch-faces/complication/sunrise_sunset_face.c b/watch-faces/complication/sunrise_sunset_face.c index 7dcb3350..d19dcb61 100644 --- a/watch-faces/complication/sunrise_sunset_face.c +++ b/watch-faces/complication/sunrise_sunset_face.c @@ -84,7 +84,7 @@ static void _sunrise_sunset_face_update(sunrise_sunset_state_t *state) { watch_date_time_t date_time = movement_get_local_date_time(); // the current local date / time watch_date_time_t utc_now = watch_utility_date_time_convert_zone(date_time, movement_get_current_timezone_offset(), 0); // the current date / time in UTC watch_date_time_t scratch_time; // scratchpad, contains different values at different times - scratch_time.reg = utc_now.reg; + scratch_time.reg = date_time.reg; // Weird quirky unsigned things were happening when I tried to cast these directly to doubles below. // it looks redundant, but extracting them to local int16's seemed to fix it. @@ -200,7 +200,7 @@ static void _sunrise_sunset_face_update(sunrise_sunset_state_t *state) { } // it's after sunset. we need to display sunrise/sunset for tomorrow. - uint32_t timestamp = watch_utility_date_time_to_unix_time(utc_now, 0); + uint32_t timestamp = watch_utility_date_time_to_unix_time(date_time, 0); timestamp += 86400; scratch_time = watch_utility_date_time_from_unix_time(timestamp, 0); } From ab7018e44c9472b11665af404fe10bdc6c7ff987 Mon Sep 17 00:00:00 2001 From: David Volovskiy Date: Wed, 26 Nov 2025 12:07:37 -0500 Subject: [PATCH 091/179] Updated documentation --- watch-faces/complication/blackjack_face.h | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/watch-faces/complication/blackjack_face.h b/watch-faces/complication/blackjack_face.h index c93143b9..4a02cd0b 100755 --- a/watch-faces/complication/blackjack_face.h +++ b/watch-faces/complication/blackjack_face.h @@ -46,7 +46,7 @@ * * To hit, press the ALARM button. * To stand, press the LIGHT button. - * If you're at 21, you cannoy hit, since we just assume it's a mispress on the button. + * If you're at 21, you will stand if you try to hit, since we just assume it's a mispress on the button. * * Once you stand, the dealer will deal out to themselves once per second (or immidietly when you press the LIGHT or ALARM buttons). * The game results are: @@ -55,10 +55,14 @@ * BUST: Your score is above 21. * TIE: Your score matches the dealer's final score * - * On a watch that has the accelerometer, long-pressing the ALARM button will turn on the ability to play by tapping. + * On a watch that has the accelerometer, long-pressing the ALARM button on the Title Screen will turn on the ability to play by tapping. * The SIGNAL indicator will display when tapping is enabled. * Tapping once will behave like the ALARM button and hit. - * Tapping twice behave like the LIGHT button and stand. + * Tapping twice behave like the LIGHT button and stand. Warning: if you're using the LIS2DW board, it cannot register a double-tapping + * without seeing a single-tap first. + * + * Long-pressing the LIGHT button on the Title Screen will display your win rate as a percentage of games finished. + * It displays as games won / (games won + games lost) it does not include incomplete nor tied games. * * | Cards | | * |---------|--------------------------| From f95cd68b2587e1372e8e9cd237ffb7bc6dc1b98f Mon Sep 17 00:00:00 2001 From: oliverpool Date: Mon, 24 Nov 2025 19:26:38 +0100 Subject: [PATCH 092/179] deadline_face: move description to .h file See #152 --- watch-faces/complication/deadline_face.c | 74 ------------------------ watch-faces/complication/deadline_face.h | 74 ++++++++++++++++++++++++ 2 files changed, 74 insertions(+), 74 deletions(-) diff --git a/watch-faces/complication/deadline_face.c b/watch-faces/complication/deadline_face.c index d3204f20..b145a618 100644 --- a/watch-faces/complication/deadline_face.c +++ b/watch-faces/complication/deadline_face.c @@ -23,80 +23,6 @@ * THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -/* - * # Deadline Face - * - * This is a watch face for tracking deadlines. It draws inspiration from - * other watch faces of the project but focuses on keeping track of - * deadlines. You can enter and monitor up to four different deadlines by - * providing their respective date and time. The face has two modes: - * *running mode* and *settings mode*. - * - * ## Running Mode - * - * When the watch face is activated, it defaults to running mode. The top - * right corner shows the current deadline number, and the main display - * presents the time left until the deadline. The format of the display - * varies depending on the remaining time. - * - * - When less than a day is left, the display shows the remaining hours, - * minutes, and seconds in the form `HH:MM:SS`. - * - * - When less than a month is left, the display shows the remaining days - * and hours in the form `DD:HH` with the unit `dy` for days. - * - * - When less than a year is left, the display shows the remaining months - * and days in the form `MM:DD` with the unit `mo` for months. - * - * - When more than a year is left, the years and months are displayed in - * the form `YY:MM` with the unit `yr` for years. - * - * - When a deadline has passed in the last 24 hours, the display shows - * `over` to indicate that the deadline has just recently been reached. - * - * - When no deadline is set for a particular slot, or if a deadline has - * already passed by more than 24 hours, `--:--` is displayed. - * - * The user can navigate in running mode using the following buttons: - * - * - The *alarm button* moves the next deadline. There are currently four - * slots available for deadlines. When the last slot has been reached, - * pressing the button moves to the first slot. - * - * - A *long press* on the *alarm button* activates settings mode and - * enables configuring the currently selected deadline. - * - * - A *long press* on the *light button* activates a deadline alarm. The - * bell icon is displayed, and the alarm will ring upon reaching any of - * the deadlines set. It is important to note that the watch will not - * enter low-energy sleep mode while the alarm is enabled. - * - * - * ## Settings Mode - * - * In settings mode, the currently selected slot for a deadline can be - * configured by providing the date and the time. Like running mode, the - * top right corner of the display indicates the current deadline number. - * The main display shows the date and, on the next page, the time to be - * configured. - * - * The user can use the following buttons in settings mode. - * - * - The *light button* navigates through the different date and time - * settings, going from year, month, day, hour, to minute. The selected - * position is blinking. - * - * - A *long press* on the light button resets the date and time to the next - * day at midnight. This is the default deadline. - * - * - The *alarm button* increments the currently selected position. A *long - * press* on the *alarm button* changes the value faster. - * - * - The *mode button* exists setting mode and returns to *running mode*. - * Here the selected deadline slot can be changed. - * - */ - #include #include #include "deadline_face.h" diff --git a/watch-faces/complication/deadline_face.h b/watch-faces/complication/deadline_face.h index 98a50d2d..6da3c893 100644 --- a/watch-faces/complication/deadline_face.h +++ b/watch-faces/complication/deadline_face.h @@ -26,6 +26,80 @@ #ifndef DEADLINE_FACE_H_ #define DEADLINE_FACE_H_ +/* + * # Deadline Face + * + * This is a watch face for tracking deadlines. It draws inspiration from + * other watch faces of the project but focuses on keeping track of + * deadlines. You can enter and monitor up to four different deadlines by + * providing their respective date and time. The face has two modes: + * *running mode* and *settings mode*. + * + * ## Running Mode + * + * When the watch face is activated, it defaults to running mode. The top + * right corner shows the current deadline number, and the main display + * presents the time left until the deadline. The format of the display + * varies depending on the remaining time. + * + * - When less than a day is left, the display shows the remaining hours, + * minutes, and seconds in the form `HH:MM:SS`. + * + * - When less than a month is left, the display shows the remaining days + * and hours in the form `DD:HH` with the unit `dy` for days. + * + * - When less than a year is left, the display shows the remaining months + * and days in the form `MM:DD` with the unit `mo` for months. + * + * - When more than a year is left, the years and months are displayed in + * the form `YY:MM` with the unit `yr` for years. + * + * - When a deadline has passed in the last 24 hours, the display shows + * `over` to indicate that the deadline has just recently been reached. + * + * - When no deadline is set for a particular slot, or if a deadline has + * already passed by more than 24 hours, `--:--` is displayed. + * + * The user can navigate in running mode using the following buttons: + * + * - The *alarm button* moves the next deadline. There are currently four + * slots available for deadlines. When the last slot has been reached, + * pressing the button moves to the first slot. + * + * - A *long press* on the *alarm button* activates settings mode and + * enables configuring the currently selected deadline. + * + * - A *long press* on the *light button* activates a deadline alarm. The + * bell icon is displayed, and the alarm will ring upon reaching any of + * the deadlines set. It is important to note that the watch will not + * enter low-energy sleep mode while the alarm is enabled. + * + * + * ## Settings Mode + * + * In settings mode, the currently selected slot for a deadline can be + * configured by providing the date and the time. Like running mode, the + * top right corner of the display indicates the current deadline number. + * The main display shows the date and, on the next page, the time to be + * configured. + * + * The user can use the following buttons in settings mode. + * + * - The *light button* navigates through the different date and time + * settings, going from year, month, day, hour, to minute. The selected + * position is blinking. + * + * - A *long press* on the light button resets the date and time to the next + * day at midnight. This is the default deadline. + * + * - The *alarm button* increments the currently selected position. A *long + * press* on the *alarm button* changes the value faster. + * + * - The *mode button* exists setting mode and returns to *running mode*. + * Here the selected deadline slot can be changed. + * + */ + #include "movement.h" /* Modes of face */ From 5dbcba181e539169a500cd8345abd290637ef199 Mon Sep 17 00:00:00 2001 From: oliverpool Date: Mon, 24 Nov 2025 19:32:24 +0100 Subject: [PATCH 093/179] wareki_face: move description to .h file and add license notice See #152 --- watch-faces/complication/wareki_face.c | 36 ++++++++++++++-------- watch-faces/complication/wareki_face.h | 41 ++++++++++++++++++++++++++ 2 files changed, 64 insertions(+), 13 deletions(-) diff --git a/watch-faces/complication/wareki_face.c b/watch-faces/complication/wareki_face.c index 103a503d..6dec4b31 100644 --- a/watch-faces/complication/wareki_face.c +++ b/watch-faces/complication/wareki_face.c @@ -1,17 +1,27 @@ /* - -The displayed Japanese Era can be changed by the buttons on the watch, making it also usable as a converter between the Gregorian calendar and the Japanese Era. - -Light button: Subtract one year from the Japanese Era. -Start/Stop button: Add one year to the Japanese Era. -Button operations support long-press functionality. - -Japanese Era Notations: - -r : REIWA (令和) -h : HEISEI (平成) -s : SHOWA(昭和) -*/ + * MIT License + * + * Copyright (c) 2025 kbc-yam + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to permit + * persons to whom the Software is furnished to do so, subject to the + * following conditions: + * + * The above copyright notice and this permission notice shall be included + * in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS + * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY + * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT + * OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR + * THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ #include #include diff --git a/watch-faces/complication/wareki_face.h b/watch-faces/complication/wareki_face.h index 070642e8..dfa3e405 100644 --- a/watch-faces/complication/wareki_face.h +++ b/watch-faces/complication/wareki_face.h @@ -1,6 +1,47 @@ +/* + * MIT License + * + * Copyright (c) 2025 kbc-yam + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to permit + * persons to whom the Software is furnished to do so, subject to the + * following conditions: + * + * The above copyright notice and this permission notice shall be included + * in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS + * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY + * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT + * OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR + * THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + #ifndef WAREKI_FACE_H_ #define WAREKI_FACE_H_ +/* +Display Japanese era names (Wareki) + +The displayed Japanese Era can be changed by the buttons on the watch, making it also usable as a converter between the Gregorian calendar and the Japanese Era. + +Light button: Subtract one year from the Japanese Era. +Start/Stop button: Add one year to the Japanese Era. +Button operations support long-press functionality. + +Japanese Era Notations: + +r : REIWA (令和) +h : HEISEI (平成) +s : SHOWA(昭和) +*/ + #include "movement.h" #define REIWA_LIMIT 2018 + 99 From 9b93db7fa6f5ca88afb5ec612d380a064afa1755 Mon Sep 17 00:00:00 2001 From: voloved <36523934+voloved@users.noreply.github.com> Date: Mon, 1 Dec 2025 08:19:05 -0500 Subject: [PATCH 094/179] Revert "Fix for wrong date being shown on sunrise/sunset complication." --- watch-faces/complication/sunrise_sunset_face.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/watch-faces/complication/sunrise_sunset_face.c b/watch-faces/complication/sunrise_sunset_face.c index d19dcb61..7dcb3350 100644 --- a/watch-faces/complication/sunrise_sunset_face.c +++ b/watch-faces/complication/sunrise_sunset_face.c @@ -84,7 +84,7 @@ static void _sunrise_sunset_face_update(sunrise_sunset_state_t *state) { watch_date_time_t date_time = movement_get_local_date_time(); // the current local date / time watch_date_time_t utc_now = watch_utility_date_time_convert_zone(date_time, movement_get_current_timezone_offset(), 0); // the current date / time in UTC watch_date_time_t scratch_time; // scratchpad, contains different values at different times - scratch_time.reg = date_time.reg; + scratch_time.reg = utc_now.reg; // Weird quirky unsigned things were happening when I tried to cast these directly to doubles below. // it looks redundant, but extracting them to local int16's seemed to fix it. @@ -200,7 +200,7 @@ static void _sunrise_sunset_face_update(sunrise_sunset_state_t *state) { } // it's after sunset. we need to display sunrise/sunset for tomorrow. - uint32_t timestamp = watch_utility_date_time_to_unix_time(date_time, 0); + uint32_t timestamp = watch_utility_date_time_to_unix_time(utc_now, 0); timestamp += 86400; scratch_time = watch_utility_date_time_from_unix_time(timestamp, 0); } From 3bd1254b5fbb53ce84bddd1fb806da3237a8f245 Mon Sep 17 00:00:00 2001 From: voloved <36523934+voloved@users.noreply.github.com> Date: Mon, 1 Dec 2025 20:09:34 -0500 Subject: [PATCH 095/179] Revert "Added double-tap capabilities to LIS2DW" --- movement.c | 5 ++--- watch-library/shared/driver/lis2dw.c | 14 -------------- watch-library/shared/driver/lis2dw.h | 4 ---- 3 files changed, 2 insertions(+), 21 deletions(-) diff --git a/movement.c b/movement.c index e81ccb94..ec54a9ba 100644 --- a/movement.c +++ b/movement.c @@ -514,7 +514,7 @@ bool movement_enable_tap_detection_if_available(void) { if (movement_state.has_lis2dw) { // configure tap duration threshold and enable Z axis lis2dw_configure_tap_threshold(0, 0, 12, LIS2DW_REG_TAP_THS_Z_Z_AXIS_ENABLE); - lis2dw_configure_tap_duration(2, 2, 2); + lis2dw_configure_tap_duration(10, 2, 2); // ramp data rate up to 400 Hz and high performance mode lis2dw_set_low_noise_mode(true); @@ -525,7 +525,7 @@ bool movement_enable_tap_detection_if_available(void) { delay_ms(3); // enable tap detection on INT1/A3. - lis2dw_configure_int1(LIS2DW_CTRL4_INT1_SINGLE_TAP | LIS2DW_CTRL4_INT1_DOUBLE_TAP); + lis2dw_configure_int1(LIS2DW_CTRL4_INT1_SINGLE_TAP | LIS2DW_CTRL4_INT1_6D); return true; } @@ -539,7 +539,6 @@ bool movement_disable_tap_detection_if_available(void) { lis2dw_set_low_noise_mode(false); lis2dw_set_data_rate(movement_state.accelerometer_background_rate); lis2dw_set_mode(LIS2DW_MODE_LOW_POWER); - lis2dw_disable_double_tap(); // ...disable Z axis (not sure if this is needed, does this save power?)... lis2dw_configure_tap_threshold(0, 0, 0, 0); diff --git a/watch-library/shared/driver/lis2dw.c b/watch-library/shared/driver/lis2dw.c index 1c89f244..b716f790 100644 --- a/watch-library/shared/driver/lis2dw.c +++ b/watch-library/shared/driver/lis2dw.c @@ -302,20 +302,6 @@ void lis2dw_clear_fifo(void) { #endif } -void lis2dw_enable_double_tap(void) { -#ifdef I2C_SERCOM - uint8_t configuration = watch_i2c_read8(LIS2DW_ADDRESS, LIS2DW_REG_WAKE_UP_THS); - watch_i2c_write8(LIS2DW_ADDRESS, LIS2DW_REG_WAKE_UP_THS, configuration | LIS2DW_WAKE_UP_THS_ENABLE_DOUBLE_TAP); -#endif -} - -void lis2dw_disable_double_tap(void) { -#ifdef I2C_SERCOM - uint8_t configuration = watch_i2c_read8(LIS2DW_ADDRESS, LIS2DW_REG_WAKE_UP_THS); - watch_i2c_write8(LIS2DW_ADDRESS, LIS2DW_REG_WAKE_UP_THS, configuration & ~LIS2DW_WAKE_UP_THS_ENABLE_DOUBLE_TAP); -#endif -} - void lis2dw_enable_sleep(void) { #ifdef I2C_SERCOM uint8_t configuration = watch_i2c_read8(LIS2DW_ADDRESS, LIS2DW_REG_WAKE_UP_THS); diff --git a/watch-library/shared/driver/lis2dw.h b/watch-library/shared/driver/lis2dw.h index 3b017c65..57ff05b2 100644 --- a/watch-library/shared/driver/lis2dw.h +++ b/watch-library/shared/driver/lis2dw.h @@ -343,10 +343,6 @@ bool lis2dw_read_fifo(lis2dw_fifo_t *fifo_data); void lis2dw_clear_fifo(void); -void lis2dw_enable_double_tap(void); - -void lis2dw_disable_double_tap(void); - void lis2dw_enable_sleep(void); void lis2dw_disable_sleep(void); From a907760cc19e7e161f0a7476c9ad62d68e9a0785 Mon Sep 17 00:00:00 2001 From: David Volovskiy Date: Mon, 1 Dec 2025 20:11:40 -0500 Subject: [PATCH 096/179] We forgot to enable the actual double tap --- movement.c | 1 + 1 file changed, 1 insertion(+) diff --git a/movement.c b/movement.c index e81ccb94..a37e8df2 100644 --- a/movement.c +++ b/movement.c @@ -520,6 +520,7 @@ bool movement_enable_tap_detection_if_available(void) { lis2dw_set_low_noise_mode(true); lis2dw_set_data_rate(LIS2DW_DATA_RATE_HP_400_HZ); lis2dw_set_mode(LIS2DW_MODE_LOW_POWER); + lis2dw_enable_double_tap(); // Settling time (1 sample duration, i.e. 1/400Hz) delay_ms(3); From 23038c3f7480dacfe9862cfa9d3e65c537804d0c Mon Sep 17 00:00:00 2001 From: David Volovskiy Date: Mon, 1 Dec 2025 20:14:54 -0500 Subject: [PATCH 097/179] Reapply "Added double-tap capabilities to LIS2DW" This reverts commit 3bd1254b5fbb53ce84bddd1fb806da3237a8f245. --- movement.c | 5 +++-- watch-library/shared/driver/lis2dw.c | 14 ++++++++++++++ watch-library/shared/driver/lis2dw.h | 4 ++++ 3 files changed, 21 insertions(+), 2 deletions(-) diff --git a/movement.c b/movement.c index eb478ec9..a37e8df2 100644 --- a/movement.c +++ b/movement.c @@ -514,7 +514,7 @@ bool movement_enable_tap_detection_if_available(void) { if (movement_state.has_lis2dw) { // configure tap duration threshold and enable Z axis lis2dw_configure_tap_threshold(0, 0, 12, LIS2DW_REG_TAP_THS_Z_Z_AXIS_ENABLE); - lis2dw_configure_tap_duration(10, 2, 2); + lis2dw_configure_tap_duration(2, 2, 2); // ramp data rate up to 400 Hz and high performance mode lis2dw_set_low_noise_mode(true); @@ -526,7 +526,7 @@ bool movement_enable_tap_detection_if_available(void) { delay_ms(3); // enable tap detection on INT1/A3. - lis2dw_configure_int1(LIS2DW_CTRL4_INT1_SINGLE_TAP | LIS2DW_CTRL4_INT1_6D); + lis2dw_configure_int1(LIS2DW_CTRL4_INT1_SINGLE_TAP | LIS2DW_CTRL4_INT1_DOUBLE_TAP); return true; } @@ -540,6 +540,7 @@ bool movement_disable_tap_detection_if_available(void) { lis2dw_set_low_noise_mode(false); lis2dw_set_data_rate(movement_state.accelerometer_background_rate); lis2dw_set_mode(LIS2DW_MODE_LOW_POWER); + lis2dw_disable_double_tap(); // ...disable Z axis (not sure if this is needed, does this save power?)... lis2dw_configure_tap_threshold(0, 0, 0, 0); diff --git a/watch-library/shared/driver/lis2dw.c b/watch-library/shared/driver/lis2dw.c index b716f790..1c89f244 100644 --- a/watch-library/shared/driver/lis2dw.c +++ b/watch-library/shared/driver/lis2dw.c @@ -302,6 +302,20 @@ void lis2dw_clear_fifo(void) { #endif } +void lis2dw_enable_double_tap(void) { +#ifdef I2C_SERCOM + uint8_t configuration = watch_i2c_read8(LIS2DW_ADDRESS, LIS2DW_REG_WAKE_UP_THS); + watch_i2c_write8(LIS2DW_ADDRESS, LIS2DW_REG_WAKE_UP_THS, configuration | LIS2DW_WAKE_UP_THS_ENABLE_DOUBLE_TAP); +#endif +} + +void lis2dw_disable_double_tap(void) { +#ifdef I2C_SERCOM + uint8_t configuration = watch_i2c_read8(LIS2DW_ADDRESS, LIS2DW_REG_WAKE_UP_THS); + watch_i2c_write8(LIS2DW_ADDRESS, LIS2DW_REG_WAKE_UP_THS, configuration & ~LIS2DW_WAKE_UP_THS_ENABLE_DOUBLE_TAP); +#endif +} + void lis2dw_enable_sleep(void) { #ifdef I2C_SERCOM uint8_t configuration = watch_i2c_read8(LIS2DW_ADDRESS, LIS2DW_REG_WAKE_UP_THS); diff --git a/watch-library/shared/driver/lis2dw.h b/watch-library/shared/driver/lis2dw.h index 57ff05b2..3b017c65 100644 --- a/watch-library/shared/driver/lis2dw.h +++ b/watch-library/shared/driver/lis2dw.h @@ -343,6 +343,10 @@ bool lis2dw_read_fifo(lis2dw_fifo_t *fifo_data); void lis2dw_clear_fifo(void); +void lis2dw_enable_double_tap(void); + +void lis2dw_disable_double_tap(void); + void lis2dw_enable_sleep(void); void lis2dw_disable_sleep(void); From ad093f4f67fc687c2ece465bac86fa5693a27252 Mon Sep 17 00:00:00 2001 From: David Volovskiy Date: Wed, 3 Dec 2025 07:43:47 -0500 Subject: [PATCH 098/179] Fix on typecasting win rate --- watch-faces/complication/blackjack_face.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/watch-faces/complication/blackjack_face.c b/watch-faces/complication/blackjack_face.c index 375e0435..09cf9e4e 100755 --- a/watch-faces/complication/blackjack_face.c +++ b/watch-faces/complication/blackjack_face.c @@ -277,7 +277,7 @@ static void display_win_ratio(blackjack_face_state_t *state) { game_state = BJ_WIN_RATIO; uint8_t win_ratio = 0; if (state->games_played > 0) { // Avoid dividing by zero - win_ratio = (uint8_t)(100 * state->games_won) / state->games_played; + win_ratio = (uint8_t)((100 * state->games_won) / state->games_played); } watch_display_text(WATCH_POSITION_TOP_RIGHT, " "); watch_display_text_with_fallback(WATCH_POSITION_TOP, "WINS ", "WR"); From 8198d6c832925a78500aeaca3ef4bc19bc973272 Mon Sep 17 00:00:00 2001 From: David Volovskiy Date: Wed, 3 Dec 2025 07:50:38 -0500 Subject: [PATCH 099/179] Able to reset the win-lose rate --- watch-faces/complication/blackjack_face.c | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/watch-faces/complication/blackjack_face.c b/watch-faces/complication/blackjack_face.c index 09cf9e4e..c5dda6b6 100755 --- a/watch-faces/complication/blackjack_face.c +++ b/watch-faces/complication/blackjack_face.c @@ -444,6 +444,11 @@ bool blackjack_face_loop(movement_event_t event, void *context) { case EVENT_ALARM_LONG_PRESS: if (game_state == BJ_TITLE_SCREEN) { toggle_tap_control(state); + } else if (game_state == BJ_WIN_RATIO) { + // Reset the win-lose ratio + state->games_won = 0; + state->games_played = 0; + watch_display_text(WATCH_POSITION_BOTTOM, " 0Pct"); } break; case EVENT_TIMEOUT: From 3b4b70814b3529482bf15ca073bd065edf96fd7d Mon Sep 17 00:00:00 2001 From: David Volovskiy Date: Wed, 3 Dec 2025 07:55:48 -0500 Subject: [PATCH 100/179] Doc update on the win-rate reset --- watch-faces/complication/blackjack_face.h | 1 + 1 file changed, 1 insertion(+) diff --git a/watch-faces/complication/blackjack_face.h b/watch-faces/complication/blackjack_face.h index 4a02cd0b..8b10d069 100755 --- a/watch-faces/complication/blackjack_face.h +++ b/watch-faces/complication/blackjack_face.h @@ -63,6 +63,7 @@ * * Long-pressing the LIGHT button on the Title Screen will display your win rate as a percentage of games finished. * It displays as games won / (games won + games lost) it does not include incomplete nor tied games. + * You can reset the win rate on that screen by long-pressing the ALARM button. * * | Cards | | * |---------|--------------------------| From 802d64cb8e0d2b891016df6d3a69264afc037feb Mon Sep 17 00:00:00 2001 From: David Volovskiy Date: Thu, 4 Dec 2025 08:33:14 -0500 Subject: [PATCH 101/179] Fix for wrong date being shown on sunrise/sunset complication --- watch-faces/complication/sunrise_sunset_face.c | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/watch-faces/complication/sunrise_sunset_face.c b/watch-faces/complication/sunrise_sunset_face.c index 7dcb3350..58514848 100644 --- a/watch-faces/complication/sunrise_sunset_face.c +++ b/watch-faces/complication/sunrise_sunset_face.c @@ -82,9 +82,8 @@ static void _sunrise_sunset_face_update(sunrise_sunset_state_t *state) { } watch_date_time_t date_time = movement_get_local_date_time(); // the current local date / time - watch_date_time_t utc_now = watch_utility_date_time_convert_zone(date_time, movement_get_current_timezone_offset(), 0); // the current date / time in UTC watch_date_time_t scratch_time; // scratchpad, contains different values at different times - scratch_time.reg = utc_now.reg; + scratch_time.reg = date_time.reg; // Weird quirky unsigned things were happening when I tried to cast these directly to doubles below. // it looks redundant, but extracting them to local int16's seemed to fix it. @@ -200,7 +199,7 @@ static void _sunrise_sunset_face_update(sunrise_sunset_state_t *state) { } // it's after sunset. we need to display sunrise/sunset for tomorrow. - uint32_t timestamp = watch_utility_date_time_to_unix_time(utc_now, 0); + uint32_t timestamp = watch_utility_date_time_to_unix_time(date_time, 0); timestamp += 86400; scratch_time = watch_utility_date_time_from_unix_time(timestamp, 0); } From e3239ec47d81493deac2fa3d5296940d49cea717 Mon Sep 17 00:00:00 2001 From: David Volovskiy Date: Sun, 21 Dec 2025 09:03:19 -0500 Subject: [PATCH 102/179] Began adding ping face --- movement_faces.h | 1 + watch-faces.mk | 1 + watch-faces/complication/ping_face.c | 619 +++++++++++++++++++++++++++ watch-faces/complication/ping_face.h | 58 +++ 4 files changed, 679 insertions(+) create mode 100644 watch-faces/complication/ping_face.c create mode 100644 watch-faces/complication/ping_face.h diff --git a/movement_faces.h b/movement_faces.h index eef0e3ac..b75d5d7e 100644 --- a/movement_faces.h +++ b/movement_faces.h @@ -78,4 +78,5 @@ #include "higher_lower_game_face.h" #include "lander_face.h" #include "simon_face.h" +#include "ping_face.h" // New includes go above this line. diff --git a/watch-faces.mk b/watch-faces.mk index 3c424923..5fd366a0 100644 --- a/watch-faces.mk +++ b/watch-faces.mk @@ -53,4 +53,5 @@ SRCS += \ ./watch-faces/complication/higher_lower_game_face.c \ ./watch-faces/complication/lander_face.c \ ./watch-faces/complication/simon_face.c \ + ./watch-faces/complication/ping_face.c \ # New watch faces go above this line. diff --git a/watch-faces/complication/ping_face.c b/watch-faces/complication/ping_face.c new file mode 100644 index 00000000..cb45600b --- /dev/null +++ b/watch-faces/complication/ping_face.c @@ -0,0 +1,619 @@ +/* + * MIT License + * + * Copyright (c) 2024 + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +#include +#include +#include "ping_face.h" +#include "delay.h" +#include "watch_common_display.h" + +typedef enum { + PADDLE_RETRACTED = 0, + PADDLE_EXTENDING, + PADDLE_EXTENDED, + PADDLE_RETRACTING, +} PingPaddleState; + +typedef enum { + SCREEN_TITLE = 0, + SCREEN_SCORE, + SCREEN_PLAYING, + SCREEN_LOSE, + SCREEN_TIME, + SCREEN_COUNT +} PingCurrScreen; + +typedef enum { + DIFF_BABY = 0, // FREQ_SLOW FPS + DIFF_EASY, // FREQ FPS + DIFF_NORM, // FREQ FPS + DIFF_HARD, // FREQ FPS + DIFF_COUNT +} PingDifficulty; + +typedef enum { + RESULT_LOSE = -1, + RESULT_NONE = 0, + RESULT_HIT = 1 +} PingResult; + +#define BALL_POS_MAX 11 +#define BALL_OFF_SCREEN 100 +#define FREQ_BABY 2 +#define FREQ_EASY 4 +#define FREQ_NORM 8 +#define FREQ_HARD 16 +#define MAX_HI_SCORE 9999 // Max hi score to store and display on the title screen. +#define MAX_DISP_SCORE 39 // The top-right digits can't properly display above 39 + +typedef struct { + uint8_t ball_pos; // 0 to 11; 0 is the bottom-right and 11 is the top right. + // | 6 | 7 | 8 | 9 | 10 | 11 | + // | 5 | 4 | 3 | 2 | 1 | 0 | + PingPaddleState paddle_pos; + uint8_t ball_char_pos; // Derived from ball_pos + bool ball_is_clockwise; + bool ball_is_moving; + uint16_t curr_score; + PingCurrScreen curr_screen; + bool paddle_hit; + bool paddle_released; + uint8_t curr_freq; +} game_state_t; + +static game_state_t game_state; +static int8_t _ticks_show_title = 0; +static bool _is_custom_lcd; + +static int8_t start_tune[] = { + BUZZER_NOTE_C5, 15, + BUZZER_NOTE_E5, 15, + BUZZER_NOTE_G5, 15, + 0 +}; + +static int8_t lose_tune[] = { + BUZZER_NOTE_D3, 10, + BUZZER_NOTE_C3SHARP_D3FLAT, 10, + BUZZER_NOTE_C3, 10, + 0 +}; + +static uint8_t ball_pos_to_char_pos(uint8_t ball_pos) { + switch (ball_pos) + { + case 5: + case 6: + return 4; + case 4: + case 7: + return 5; + case 3: + case 8: + return 6; + case 2: + case 9: + return 7; + case 1: + case 10: + return 8; + case 0: + case 11: + return 9; + default: + return BALL_OFF_SCREEN; + } +} + +static bool paddle_and_ball_on_same_segment(void) { + if (game_state.paddle_pos == PADDLE_EXTENDED) { + if (game_state.ball_pos == 9 || game_state.ball_pos == 2) { + return true; + } + } + else if (game_state.paddle_pos == PADDLE_EXTENDING || game_state.paddle_pos == PADDLE_RETRACTING) { + if (game_state.ball_pos == 10 || game_state.ball_pos == 1) { + return true; + } + } + else if (game_state.paddle_pos == PADDLE_RETRACTED) { + if (game_state.ball_pos == 11 || game_state.ball_pos == 0) { + return true; + } + } + return false; +} + +static bool paddle_hit_ball(void) { + if (game_state.paddle_pos == PADDLE_EXTENDED) { + if (game_state.ball_pos >= 9 && game_state.ball_is_clockwise) { + return true; + } + if (game_state.ball_pos <= 2 && !game_state.ball_is_clockwise) { + return true; + } + } + else if (game_state.paddle_pos == PADDLE_EXTENDING) { + if (game_state.ball_pos >= 10 && game_state.ball_is_clockwise) { + return true; + } + if (game_state.ball_pos <= 1 && !game_state.ball_is_clockwise) { + return true; + } + } + else if (game_state.paddle_pos == PADDLE_EXTENDING) { + if (game_state.ball_pos >= 11 && game_state.ball_is_clockwise) { + return true; + } + if (game_state.ball_pos <= 0 && !game_state.ball_is_clockwise) { + return true; + } + } + return false; +} + +static uint8_t get_next_ball_pos(bool ball_hit) { + int8_t offset_next; + if (ball_hit) { + game_state.ball_is_clockwise = game_state.ball_pos < 6; + } + if (game_state.ball_is_clockwise) { + offset_next = 1; + } else { + offset_next = -1; + } + int8_t next_pos = game_state.ball_pos + offset_next; + if (next_pos > BALL_POS_MAX || next_pos < 0) { + return BALL_OFF_SCREEN; + } + return next_pos; +} + +static void display_ball(void) { + uint8_t char_pos = ball_pos_to_char_pos(game_state.ball_pos); + uint8_t char_display; + bool overlap = paddle_and_ball_on_same_segment(); + if (game_state.ball_pos > 5) { + if (overlap) { + char_display = 'q'; + } else { + char_display = '#'; + } + } else { + if (_is_custom_lcd || char_pos == 4 || char_pos == 6) { + char_display = 'n'; // No need to check for overlap on these segments + } else { + if (overlap) { + char_display = 'd'; + } else { + char_display = 'o'; + } + } + } + watch_display_character(char_display, char_pos); +} + +static PingResult update_ball(void) { + bool ball_hit = paddle_hit_ball(); + if (!game_state.ball_is_moving) { + if (ball_hit) { + game_state.ball_is_moving = true; + } else { + return RESULT_NONE; + } + } + watch_display_character(' ', ball_pos_to_char_pos(game_state.ball_pos)); // remove the old ball. + game_state.ball_pos = get_next_ball_pos(ball_hit); + if (game_state.ball_pos == BALL_OFF_SCREEN) { + return RESULT_LOSE; + } + display_ball(); + if (ball_hit) { + return RESULT_HIT; + } else { + return RESULT_NONE; + } +} + +static void display_paddle(void) { + switch (game_state.paddle_pos) + { + case PADDLE_EXTENDING: + case PADDLE_RETRACTING: + watch_display_character('-', 9); + watch_display_character('1', 8); + break; + case PADDLE_EXTENDED: + watch_display_character('-', 9); + watch_display_character('-', 8); + watch_display_character('1', 7); + break; + case PADDLE_RETRACTED: + default: + watch_display_character('1', 9); + break; + } +} + +static void update_paddle(void) { + switch (game_state.paddle_pos) + { + case PADDLE_RETRACTED: + if (game_state.paddle_hit) { + game_state.paddle_pos = PADDLE_EXTENDING; + } + break; + case PADDLE_EXTENDING: + if (game_state.paddle_released) { + game_state.paddle_pos = PADDLE_RETRACTED; + } else { + game_state.paddle_pos = PADDLE_EXTENDED; + } + break; + case PADDLE_EXTENDED: + game_state.paddle_pos = PADDLE_RETRACTING; + watch_display_character(' ', 7); + break; + case PADDLE_RETRACTING: + game_state.paddle_pos = PADDLE_RETRACTED; + watch_display_character(' ', 8); + break; + default: + break; + } + game_state.paddle_hit = false; + game_state.paddle_released = false; + display_paddle(); +} + +static inline bool paddle_is_extending(void) { + return game_state.paddle_pos == PADDLE_EXTENDING || game_state.paddle_pos == PADDLE_EXTENDED; +} + +static void display_score(uint8_t score) { + char buf[3]; + score %= (MAX_DISP_SCORE + 1); + sprintf(buf, "%2d", score); + watch_display_text(WATCH_POSITION_TOP_RIGHT, buf); +} + +static void add_to_score(ping_state_t *state) { + if (game_state.curr_score <= MAX_HI_SCORE) { + game_state.curr_score++; + if (game_state.curr_score > state -> hi_score) + state -> hi_score = game_state.curr_score; + } + display_score(game_state.curr_score); +} + +static void check_and_reset_hi_score(ping_state_t *state) { + // Resets the hi score at the beginning of each month. + watch_date_time_t date_time = movement_get_local_date_time(); + if ((state -> year_last_hi_score != date_time.unit.year) || + (state -> month_last_hi_score != date_time.unit.month)) + { + // The high score resets itself every new month. + state -> hi_score = 0; + state -> year_last_hi_score = date_time.unit.year; + state -> month_last_hi_score = date_time.unit.month; + } +} + +static void display_difficulty(uint16_t difficulty) { + static const char *labels[] = { + [DIFF_BABY] = " b", + [DIFF_EASY] = " E", + [DIFF_NORM] = " N", + [DIFF_HARD] = " H" + }; + watch_display_text(WATCH_POSITION_TOP_RIGHT, labels[difficulty]); +} + +static void change_difficulty(ping_state_t *state) { + state -> difficulty = (state -> difficulty + 1) % DIFF_COUNT; + display_difficulty(state -> difficulty); + if (state -> soundOn) { + if (state -> difficulty == 0) watch_buzzer_play_note(BUZZER_NOTE_B4, 30); + else watch_buzzer_play_note(BUZZER_NOTE_C5, 30); + } +} + +static void display_sound_indicator(bool soundOn) { + if (soundOn){ + watch_set_indicator(WATCH_INDICATOR_BELL); + } else { + watch_clear_indicator(WATCH_INDICATOR_BELL); + } +} + +static void toggle_sound(ping_state_t *state) { + state -> soundOn = !state -> soundOn; + display_sound_indicator(state -> soundOn); + if (state -> soundOn){ + watch_buzzer_play_note(BUZZER_NOTE_C5, 30); + } +} + +static void enable_tap_control(ping_state_t *state) { + if (!state->tap_control_on) { + movement_enable_tap_detection_if_available(); + state->tap_control_on = true; + } +} + +static void disable_tap_control(ping_state_t *state) { + if (state->tap_control_on) { + movement_disable_tap_detection_if_available(); + state->tap_control_on = false; + } +} + +static void display_title(ping_state_t *state) { + movement_request_tick_frequency(1); + game_state.curr_screen = SCREEN_TITLE; + watch_clear_colon(); + watch_display_text_with_fallback(WATCH_POSITION_TOP, "PING", "PI "); + watch_display_text(WATCH_POSITION_BOTTOM, " Ping "); + display_sound_indicator(state -> soundOn); + _ticks_show_title = 1; +} + +static void display_score_screen(ping_state_t *state) { + uint16_t hi_score = state -> hi_score; + uint8_t difficulty = state -> difficulty; + movement_request_tick_frequency(1); + bool sound_on = state -> soundOn; + memset(&game_state, 0, sizeof(game_state)); + game_state.curr_screen = SCREEN_SCORE; + watch_set_colon(); + watch_display_text_with_fallback(WATCH_POSITION_TOP, "PING ", "PI "); + if (hi_score > MAX_HI_SCORE) { + watch_display_text(WATCH_POSITION_BOTTOM, "HS --"); + } + else { + char buf[10]; + sprintf(buf, "HS%4d", hi_score); + watch_display_text(WATCH_POSITION_BOTTOM, buf); + } + display_difficulty(difficulty); + display_sound_indicator(sound_on); +} + +static void display_time(void) { + static watch_date_time_t previous_date_time; + watch_date_time_t date_time = movement_get_local_date_time(); + char buf[6 + 1]; + + // If the hour needs updating or it's the first time displaying the time + if ((game_state.curr_screen != SCREEN_TIME) || (date_time.unit.hour != previous_date_time.unit.hour)) { + uint8_t hour = date_time.unit.hour; + game_state.curr_screen = SCREEN_TIME; + if (!watch_sleep_animation_is_running()) { + watch_set_colon(); + watch_start_indicator_blink_if_possible(WATCH_INDICATOR_COLON, 500); + } + if (movement_clock_is_24h()) watch_set_indicator(WATCH_INDICATOR_24H); + else { + if (hour >= 12) watch_set_indicator(WATCH_INDICATOR_PM); + hour %= 12; + if (hour == 0) hour = 12; + } + sprintf( buf, movement_clock_has_leading_zeroes() ? "%02d%02d " : "%2d%02d ", hour, date_time.unit.minute); + watch_display_text(WATCH_POSITION_BOTTOM, buf); + } + // If only the minute need updating + else { + sprintf( buf, "%02d", date_time.unit.minute); + watch_display_text(WATCH_POSITION_MINUTES, buf); + } + previous_date_time.reg = date_time.reg; +} + +static void begin_playing(ping_state_t *state) { + game_state.curr_screen = SCREEN_PLAYING; + watch_clear_colon(); + display_sound_indicator(state -> soundOn); + switch (state -> difficulty) + { + case DIFF_BABY: + game_state.curr_freq = FREQ_BABY; + break; + case DIFF_EASY: + game_state.curr_freq = FREQ_EASY; + break; + case DIFF_HARD: + game_state.curr_freq = FREQ_HARD; + break; + case DIFF_NORM: + default: + game_state.curr_freq = FREQ_NORM; + break; + } + movement_request_tick_frequency(game_state.curr_freq); + watch_display_text(WATCH_POSITION_TOP_RIGHT, " "); + watch_display_text(WATCH_POSITION_BOTTOM, " "); + game_state.paddle_pos = PADDLE_RETRACTED; + game_state.ball_pos = 1; + game_state.paddle_hit = false; + game_state.paddle_released = false; + game_state.ball_is_moving = false; + game_state.ball_is_clockwise = false; + game_state.curr_score = 0; + display_paddle(); + display_ball(); + display_score( game_state.curr_score); + if (state -> soundOn){ + watch_buzzer_play_sequence(start_tune, NULL); + } +} + +static void display_lose_screen(ping_state_t *state) { + game_state.curr_screen = SCREEN_LOSE; + game_state.curr_score = 0; + watch_clear_display(); + watch_display_text(WATCH_POSITION_BOTTOM, " LOSE "); + if (state -> soundOn) { + watch_buzzer_play_sequence(lose_tune, NULL); + delay_ms(600); + } +} + +static void update_game(ping_state_t *state) { + update_paddle(); + bool can_earn_point = game_state.ball_is_moving; + int game_result = update_ball(); + if (game_result == RESULT_LOSE) { + display_lose_screen(state); + } else if (game_result == RESULT_HIT && can_earn_point) { + add_to_score(state); + if (game_state.curr_score % 10 == 0) { // Up the speed every 10 points + game_state.curr_freq *= 2; + movement_request_tick_frequency(game_state.curr_freq); + } + } + printf("freq %d\r\n", game_state.curr_freq); +} + +void ping_face_setup(uint8_t watch_face_index, void ** context_ptr) { + (void) watch_face_index; + if (*context_ptr == NULL) { + *context_ptr = malloc(sizeof(ping_state_t)); + memset(*context_ptr, 0, sizeof(ping_state_t)); + ping_state_t *state = (ping_state_t *)*context_ptr; + state->difficulty = DIFF_NORM; + state->tap_control_on = false; + } +} + +void ping_face_activate(void *context) { + (void) context; + _is_custom_lcd = watch_get_lcd_type() == WATCH_LCD_TYPE_CUSTOM; + if (watch_sleep_animation_is_running()) { + watch_stop_blink(); + } +} + +bool ping_face_loop(movement_event_t event, void *context) { + ping_state_t *state = (ping_state_t *)context; + switch (event.event_type) { + case EVENT_ACTIVATE: + disable_tap_control(state); + check_and_reset_hi_score(state); + if (game_state.curr_screen != SCREEN_TIME) { + display_title(state); + } + break; + case EVENT_TICK: + switch (game_state.curr_screen) + { + case SCREEN_TITLE: + if (_ticks_show_title > 0) {_ticks_show_title--;} + else { + watch_clear_display(); + display_score_screen(state); + } + case SCREEN_SCORE: + case SCREEN_LOSE: + case SCREEN_TIME: + break; + case SCREEN_PLAYING: + default: + update_game(state); + break; + } + break; + case EVENT_ALARM_BUTTON_UP: + if (game_state.curr_screen == SCREEN_PLAYING){ + game_state.paddle_released = true; + break; + } + // fall-through + case EVENT_LIGHT_BUTTON_UP: + switch (game_state.curr_screen) { + case SCREEN_SCORE: + enable_tap_control(state); + begin_playing(state); + break; + case SCREEN_TITLE: + enable_tap_control(state); + // fall through + case SCREEN_TIME: + case SCREEN_LOSE: + watch_clear_display(); + display_score_screen(state); + default: + break; + } + break; + case EVENT_LIGHT_LONG_PRESS: + if (game_state.curr_screen == SCREEN_SCORE) + change_difficulty(state); + break; + case EVENT_SINGLE_TAP: + case EVENT_DOUBLE_TAP: + // Allow starting a new game by tapping. + if (game_state.curr_screen == SCREEN_SCORE) { + begin_playing(state); + break; + } + else if (game_state.curr_screen == SCREEN_LOSE) { + display_score_screen(state); + break; + } + //fall through + case EVENT_ALARM_BUTTON_DOWN: + if (game_state.curr_screen == SCREEN_PLAYING){ + game_state.paddle_hit = true; + } + break; + case EVENT_ALARM_LONG_PRESS: + if (game_state.curr_screen == SCREEN_TITLE || game_state.curr_screen == SCREEN_SCORE) + toggle_sound(state); + break; + case EVENT_TIMEOUT: + disable_tap_control(state); + if (game_state.curr_screen != SCREEN_SCORE) + display_score_screen(state); + break; + case EVENT_LOW_ENERGY_UPDATE: + if (game_state.curr_screen != SCREEN_TIME) { + movement_request_tick_frequency(1); + watch_display_text_with_fallback(WATCH_POSITION_TOP, "PING ", "PI "); + display_sound_indicator(state -> soundOn); + display_difficulty(state->difficulty); + } + display_time(); + break; + case EVENT_LIGHT_BUTTON_DOWN: + break; + default: + return movement_default_loop_handler(event); + } + return true; +} + +void ping_face_resign(void *context) { + ping_state_t *state = (ping_state_t *)context; + disable_tap_control(state); +} diff --git a/watch-faces/complication/ping_face.h b/watch-faces/complication/ping_face.h new file mode 100644 index 00000000..96b6b636 --- /dev/null +++ b/watch-faces/complication/ping_face.h @@ -0,0 +1,58 @@ +/* + * MIT License + * + * Copyright (c) 2024 + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +#ifndef PING_FACE_H_ +#define PING_FACE_H_ + +#include "movement.h" + +/* + PING face +*/ + +typedef struct { + uint16_t hi_score : 10; + uint8_t difficulty : 3; + uint8_t month_last_hi_score : 4; + uint8_t year_last_hi_score : 6; + uint8_t soundOn : 1; + uint8_t tap_control_on : 1; + uint8_t unused : 7; +} ping_state_t; + +void ping_face_setup(uint8_t watch_face_index, void ** context_ptr); +void ping_face_activate(void *context); +bool ping_face_loop(movement_event_t event, void *context); +void ping_face_resign(void *context); + +#define ping_face ((const watch_face_t){ \ + ping_face_setup, \ + ping_face_activate, \ + ping_face_loop, \ + ping_face_resign, \ + NULL, \ +}) + +#endif // ping_FACE_H_ + From d785419912e618d75abef77a4f0bec0b995ff9e5 Mon Sep 17 00:00:00 2001 From: David Volovskiy Date: Sun, 21 Dec 2025 10:41:16 -0500 Subject: [PATCH 103/179] bug fixes to ping face --- watch-faces/complication/ping_face.c | 79 ++++++++++++++-------------- 1 file changed, 39 insertions(+), 40 deletions(-) diff --git a/watch-faces/complication/ping_face.c b/watch-faces/complication/ping_face.c index cb45600b..98665fe0 100644 --- a/watch-faces/complication/ping_face.c +++ b/watch-faces/complication/ping_face.c @@ -45,10 +45,11 @@ typedef enum { } PingCurrScreen; typedef enum { - DIFF_BABY = 0, // FREQ_SLOW FPS - DIFF_EASY, // FREQ FPS - DIFF_NORM, // FREQ FPS - DIFF_HARD, // FREQ FPS + DIFF_BABY = 0, // FREQ_BABY FPS + DIFF_EASY, // FREQ_EASY FPS + DIFF_NORM, // FREQ_NORM FPS + DIFF_HARD, // FREQ_NORM FPS, smaller travel-distance for ball + DIFF_FAST, // FREQ_FAST FPS DIFF_COUNT } PingDifficulty; @@ -58,12 +59,13 @@ typedef enum { RESULT_HIT = 1 } PingResult; -#define BALL_POS_MAX 11 -#define BALL_OFF_SCREEN 100 #define FREQ_BABY 2 #define FREQ_EASY 4 #define FREQ_NORM 8 -#define FREQ_HARD 16 +#define FREQ_FAST 16 + +#define BALL_POS_MAX 11 +#define BALL_OFF_SCREEN 100 #define MAX_HI_SCORE 9999 // Max hi score to store and display on the title screen. #define MAX_DISP_SCORE 39 // The top-right digits can't properly display above 39 @@ -162,21 +164,20 @@ static bool paddle_hit_ball(void) { return true; } } - else if (game_state.paddle_pos == PADDLE_EXTENDING) { - if (game_state.ball_pos >= 11 && game_state.ball_is_clockwise) { - return true; - } - if (game_state.ball_pos <= 0 && !game_state.ball_is_clockwise) { - return true; - } - } return false; } -static uint8_t get_next_ball_pos(bool ball_hit) { +static uint8_t get_next_ball_pos(bool ball_hit, uint8_t difficulty) { int8_t offset_next; if (ball_hit) { - game_state.ball_is_clockwise = game_state.ball_pos < 6; + bool ball_on_top = game_state.ball_pos > 5; + game_state.ball_is_clockwise = !ball_on_top; + // ball is at the same frame as the paddle + if (game_state.paddle_pos == PADDLE_EXTENDED) { + return ball_on_top ? 9 : 2; + } else if (game_state.paddle_pos == PADDLE_EXTENDING) { + return ball_on_top ? 10 : 1; + } } if (game_state.ball_is_clockwise) { offset_next = 1; @@ -187,6 +188,13 @@ static uint8_t get_next_ball_pos(bool ball_hit) { if (next_pos > BALL_POS_MAX || next_pos < 0) { return BALL_OFF_SCREEN; } + if (difficulty == DIFF_HARD) { + if (next_pos == 4) { + next_pos = 8; + } else if (next_pos == 7) { + next_pos = 3; + } + } return next_pos; } @@ -214,7 +222,7 @@ static void display_ball(void) { watch_display_character(char_display, char_pos); } -static PingResult update_ball(void) { +static PingResult update_ball(uint8_t difficulty) { bool ball_hit = paddle_hit_ball(); if (!game_state.ball_is_moving) { if (ball_hit) { @@ -223,8 +231,7 @@ static PingResult update_ball(void) { return RESULT_NONE; } } - watch_display_character(' ', ball_pos_to_char_pos(game_state.ball_pos)); // remove the old ball. - game_state.ball_pos = get_next_ball_pos(ball_hit); + game_state.ball_pos = get_next_ball_pos(ball_hit, difficulty); if (game_state.ball_pos == BALL_OFF_SCREEN) { return RESULT_LOSE; } @@ -265,7 +272,7 @@ static void update_paddle(void) { } break; case PADDLE_EXTENDING: - if (game_state.paddle_released) { + if (!HAL_GPIO_BTN_ALARM_read()) { game_state.paddle_pos = PADDLE_RETRACTED; } else { game_state.paddle_pos = PADDLE_EXTENDED; @@ -283,7 +290,6 @@ static void update_paddle(void) { break; } game_state.paddle_hit = false; - game_state.paddle_released = false; display_paddle(); } @@ -325,7 +331,8 @@ static void display_difficulty(uint16_t difficulty) { [DIFF_BABY] = " b", [DIFF_EASY] = " E", [DIFF_NORM] = " N", - [DIFF_HARD] = " H" + [DIFF_HARD] = " H", + [DIFF_FAST] = " F" }; watch_display_text(WATCH_POSITION_TOP_RIGHT, labels[difficulty]); } @@ -442,10 +449,11 @@ static void begin_playing(ping_state_t *state) { case DIFF_EASY: game_state.curr_freq = FREQ_EASY; break; - case DIFF_HARD: - game_state.curr_freq = FREQ_HARD; + case DIFF_FAST: + game_state.curr_freq = FREQ_FAST; break; case DIFF_NORM: + case DIFF_HARD: default: game_state.curr_freq = FREQ_NORM; break; @@ -456,7 +464,6 @@ static void begin_playing(ping_state_t *state) { game_state.paddle_pos = PADDLE_RETRACTED; game_state.ball_pos = 1; game_state.paddle_hit = false; - game_state.paddle_released = false; game_state.ball_is_moving = false; game_state.ball_is_clockwise = false; game_state.curr_score = 0; @@ -480,19 +487,16 @@ static void display_lose_screen(ping_state_t *state) { } static void update_game(ping_state_t *state) { + if (game_state.ball_is_moving) { + watch_display_character(' ', ball_pos_to_char_pos(game_state.ball_pos)); // remove the old ball. + } update_paddle(); - bool can_earn_point = game_state.ball_is_moving; - int game_result = update_ball(); + int game_result = update_ball(state -> difficulty); if (game_result == RESULT_LOSE) { display_lose_screen(state); - } else if (game_result == RESULT_HIT && can_earn_point) { + } else if (game_result == RESULT_HIT) { add_to_score(state); - if (game_state.curr_score % 10 == 0) { // Up the speed every 10 points - game_state.curr_freq *= 2; - movement_request_tick_frequency(game_state.curr_freq); - } } - printf("freq %d\r\n", game_state.curr_freq); } void ping_face_setup(uint8_t watch_face_index, void ** context_ptr) { @@ -501,7 +505,7 @@ void ping_face_setup(uint8_t watch_face_index, void ** context_ptr) { *context_ptr = malloc(sizeof(ping_state_t)); memset(*context_ptr, 0, sizeof(ping_state_t)); ping_state_t *state = (ping_state_t *)*context_ptr; - state->difficulty = DIFF_NORM; + state->difficulty = DIFF_BABY; state->tap_control_on = false; } } @@ -544,11 +548,6 @@ bool ping_face_loop(movement_event_t event, void *context) { } break; case EVENT_ALARM_BUTTON_UP: - if (game_state.curr_screen == SCREEN_PLAYING){ - game_state.paddle_released = true; - break; - } - // fall-through case EVENT_LIGHT_BUTTON_UP: switch (game_state.curr_screen) { case SCREEN_SCORE: From fe085f81fd08a51b591307e66e3b7dbea58d7f34 Mon Sep 17 00:00:00 2001 From: David Volovskiy Date: Sun, 21 Dec 2025 10:50:11 -0500 Subject: [PATCH 104/179] Adding description --- watch-faces/complication/ping_face.h | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/watch-faces/complication/ping_face.h b/watch-faces/complication/ping_face.h index 96b6b636..bdd9e395 100644 --- a/watch-faces/complication/ping_face.h +++ b/watch-faces/complication/ping_face.h @@ -1,7 +1,7 @@ /* * MIT License * - * Copyright (c) 2024 + * Copyright (c) 2025 * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -29,6 +29,12 @@ /* PING face + I saw the face made on the Ollee watch and thought it'd be fun to have on my Sensorwatch. + https://www.instagram.com/reel/DNlTb-ERE1F/ + On the title screen, you can select a difficulty by long-pressing LIGHT or toggle sound by long-pressing ALARM. + ALARM are used to paddle. Holding the ALARM button longer makes the paddle travel further. + If the accelerometer is installed, you can tap the screen to move the paddle. + High-score is displayed on the top-right on the title screen. During a game, the current score is displayed. */ typedef struct { From 6627ff1fbec75f676d7240d404d4fab2a166e8fe Mon Sep 17 00:00:00 2001 From: David Volovskiy Date: Sun, 21 Dec 2025 10:50:59 -0500 Subject: [PATCH 105/179] accelerometer moving paddle fully out --- watch-faces/complication/ping_face.c | 11 +++++++++-- watch-faces/complication/ping_face.h | 2 +- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/watch-faces/complication/ping_face.c b/watch-faces/complication/ping_face.c index 98665fe0..e35762e9 100644 --- a/watch-faces/complication/ping_face.c +++ b/watch-faces/complication/ping_face.c @@ -82,6 +82,7 @@ typedef struct { bool paddle_hit; bool paddle_released; uint8_t curr_freq; + bool moving_from_tap; } game_state_t; static game_state_t game_state; @@ -272,7 +273,7 @@ static void update_paddle(void) { } break; case PADDLE_EXTENDING: - if (!HAL_GPIO_BTN_ALARM_read()) { + if (!game_state.moving_from_tap && !HAL_GPIO_BTN_ALARM_read()) { game_state.paddle_pos = PADDLE_RETRACTED; } else { game_state.paddle_pos = PADDLE_EXTENDED; @@ -285,6 +286,7 @@ static void update_paddle(void) { case PADDLE_RETRACTING: game_state.paddle_pos = PADDLE_RETRACTED; watch_display_character(' ', 8); + game_state.moving_from_tap = false; break; default: break; @@ -580,9 +582,14 @@ bool ping_face_loop(movement_event_t event, void *context) { display_score_screen(state); break; } - //fall through + else if (game_state.curr_screen == SCREEN_PLAYING){ + game_state.moving_from_tap = true; + game_state.paddle_hit = true; + } + break; case EVENT_ALARM_BUTTON_DOWN: if (game_state.curr_screen == SCREEN_PLAYING){ + game_state.moving_from_tap = false; game_state.paddle_hit = true; } break; diff --git a/watch-faces/complication/ping_face.h b/watch-faces/complication/ping_face.h index bdd9e395..5aa5a62f 100644 --- a/watch-faces/complication/ping_face.h +++ b/watch-faces/complication/ping_face.h @@ -33,7 +33,7 @@ https://www.instagram.com/reel/DNlTb-ERE1F/ On the title screen, you can select a difficulty by long-pressing LIGHT or toggle sound by long-pressing ALARM. ALARM are used to paddle. Holding the ALARM button longer makes the paddle travel further. - If the accelerometer is installed, you can tap the screen to move the paddle. + If the accelerometer is installed, you can tap the screen to move the paddle. Paddle will travel its full distance when tapping is used. High-score is displayed on the top-right on the title screen. During a game, the current score is displayed. */ From 66257c523274637f7593329f2a8db1421c9e2702 Mon Sep 17 00:00:00 2001 From: David Volovskiy Date: Sun, 21 Dec 2025 11:04:31 -0500 Subject: [PATCH 106/179] Removed time display on ping face --- watch-faces/complication/ping_face.c | 47 ++-------------------------- 1 file changed, 2 insertions(+), 45 deletions(-) diff --git a/watch-faces/complication/ping_face.c b/watch-faces/complication/ping_face.c index e35762e9..96d3f4f8 100644 --- a/watch-faces/complication/ping_face.c +++ b/watch-faces/complication/ping_face.c @@ -40,7 +40,6 @@ typedef enum { SCREEN_SCORE, SCREEN_PLAYING, SCREEN_LOSE, - SCREEN_TIME, SCREEN_COUNT } PingCurrScreen; @@ -409,36 +408,6 @@ static void display_score_screen(ping_state_t *state) { display_sound_indicator(sound_on); } -static void display_time(void) { - static watch_date_time_t previous_date_time; - watch_date_time_t date_time = movement_get_local_date_time(); - char buf[6 + 1]; - - // If the hour needs updating or it's the first time displaying the time - if ((game_state.curr_screen != SCREEN_TIME) || (date_time.unit.hour != previous_date_time.unit.hour)) { - uint8_t hour = date_time.unit.hour; - game_state.curr_screen = SCREEN_TIME; - if (!watch_sleep_animation_is_running()) { - watch_set_colon(); - watch_start_indicator_blink_if_possible(WATCH_INDICATOR_COLON, 500); - } - if (movement_clock_is_24h()) watch_set_indicator(WATCH_INDICATOR_24H); - else { - if (hour >= 12) watch_set_indicator(WATCH_INDICATOR_PM); - hour %= 12; - if (hour == 0) hour = 12; - } - sprintf( buf, movement_clock_has_leading_zeroes() ? "%02d%02d " : "%2d%02d ", hour, date_time.unit.minute); - watch_display_text(WATCH_POSITION_BOTTOM, buf); - } - // If only the minute need updating - else { - sprintf( buf, "%02d", date_time.unit.minute); - watch_display_text(WATCH_POSITION_MINUTES, buf); - } - previous_date_time.reg = date_time.reg; -} - static void begin_playing(ping_state_t *state) { game_state.curr_screen = SCREEN_PLAYING; watch_clear_colon(); @@ -526,9 +495,7 @@ bool ping_face_loop(movement_event_t event, void *context) { case EVENT_ACTIVATE: disable_tap_control(state); check_and_reset_hi_score(state); - if (game_state.curr_screen != SCREEN_TIME) { - display_title(state); - } + display_title(state); break; case EVENT_TICK: switch (game_state.curr_screen) @@ -541,7 +508,6 @@ bool ping_face_loop(movement_event_t event, void *context) { } case SCREEN_SCORE: case SCREEN_LOSE: - case SCREEN_TIME: break; case SCREEN_PLAYING: default: @@ -559,7 +525,6 @@ bool ping_face_loop(movement_event_t event, void *context) { case SCREEN_TITLE: enable_tap_control(state); // fall through - case SCREEN_TIME: case SCREEN_LOSE: watch_clear_display(); display_score_screen(state); @@ -599,17 +564,9 @@ bool ping_face_loop(movement_event_t event, void *context) { break; case EVENT_TIMEOUT: disable_tap_control(state); - if (game_state.curr_screen != SCREEN_SCORE) + if (game_state.curr_screen != SCREEN_SCORE) { display_score_screen(state); - break; - case EVENT_LOW_ENERGY_UPDATE: - if (game_state.curr_screen != SCREEN_TIME) { - movement_request_tick_frequency(1); - watch_display_text_with_fallback(WATCH_POSITION_TOP, "PING ", "PI "); - display_sound_indicator(state -> soundOn); - display_difficulty(state->difficulty); } - display_time(); break; case EVENT_LIGHT_BUTTON_DOWN: break; From 2935f0b6042606f3cb4c2c2a042dad618df591db Mon Sep 17 00:00:00 2001 From: David Volovskiy Date: Sun, 21 Dec 2025 11:23:29 -0500 Subject: [PATCH 107/179] Changed default difficulty --- watch-faces/complication/ping_face.c | 2 +- watch-faces/complication/ping_face.h | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/watch-faces/complication/ping_face.c b/watch-faces/complication/ping_face.c index 96d3f4f8..ba650b55 100644 --- a/watch-faces/complication/ping_face.c +++ b/watch-faces/complication/ping_face.c @@ -476,7 +476,7 @@ void ping_face_setup(uint8_t watch_face_index, void ** context_ptr) { *context_ptr = malloc(sizeof(ping_state_t)); memset(*context_ptr, 0, sizeof(ping_state_t)); ping_state_t *state = (ping_state_t *)*context_ptr; - state->difficulty = DIFF_BABY; + state->difficulty = DIFF_NORM; state->tap_control_on = false; } } diff --git a/watch-faces/complication/ping_face.h b/watch-faces/complication/ping_face.h index 5aa5a62f..a816b5d3 100644 --- a/watch-faces/complication/ping_face.h +++ b/watch-faces/complication/ping_face.h @@ -35,6 +35,14 @@ ALARM are used to paddle. Holding the ALARM button longer makes the paddle travel further. If the accelerometer is installed, you can tap the screen to move the paddle. Paddle will travel its full distance when tapping is used. High-score is displayed on the top-right on the title screen. During a game, the current score is displayed. + + Difficulties: + Baby: 2 FPS + Easy: 4 FPS + Normal: 8 FPS + Hard: 8 FPS and the ball travels half the half the board. + Fast: 16 FPS + */ typedef struct { From 4ef85c6f2c54761d692fc377c217a2f439ed477d Mon Sep 17 00:00:00 2001 From: David Volovskiy Date: Sun, 21 Dec 2025 11:28:40 -0500 Subject: [PATCH 108/179] Watch's refresh can't handle Fast difficulty --- watch-faces/complication/ping_face.c | 8 +------- watch-faces/complication/ping_face.h | 1 - 2 files changed, 1 insertion(+), 8 deletions(-) diff --git a/watch-faces/complication/ping_face.c b/watch-faces/complication/ping_face.c index ba650b55..414ab8e8 100644 --- a/watch-faces/complication/ping_face.c +++ b/watch-faces/complication/ping_face.c @@ -48,7 +48,6 @@ typedef enum { DIFF_EASY, // FREQ_EASY FPS DIFF_NORM, // FREQ_NORM FPS DIFF_HARD, // FREQ_NORM FPS, smaller travel-distance for ball - DIFF_FAST, // FREQ_FAST FPS DIFF_COUNT } PingDifficulty; @@ -61,7 +60,6 @@ typedef enum { #define FREQ_BABY 2 #define FREQ_EASY 4 #define FREQ_NORM 8 -#define FREQ_FAST 16 #define BALL_POS_MAX 11 #define BALL_OFF_SCREEN 100 @@ -332,8 +330,7 @@ static void display_difficulty(uint16_t difficulty) { [DIFF_BABY] = " b", [DIFF_EASY] = " E", [DIFF_NORM] = " N", - [DIFF_HARD] = " H", - [DIFF_FAST] = " F" + [DIFF_HARD] = " H" }; watch_display_text(WATCH_POSITION_TOP_RIGHT, labels[difficulty]); } @@ -420,9 +417,6 @@ static void begin_playing(ping_state_t *state) { case DIFF_EASY: game_state.curr_freq = FREQ_EASY; break; - case DIFF_FAST: - game_state.curr_freq = FREQ_FAST; - break; case DIFF_NORM: case DIFF_HARD: default: diff --git a/watch-faces/complication/ping_face.h b/watch-faces/complication/ping_face.h index a816b5d3..2207bec3 100644 --- a/watch-faces/complication/ping_face.h +++ b/watch-faces/complication/ping_face.h @@ -41,7 +41,6 @@ Easy: 4 FPS Normal: 8 FPS Hard: 8 FPS and the ball travels half the half the board. - Fast: 16 FPS */ From 2c3259b2e372c7a360752aaa71e7870d80527f05 Mon Sep 17 00:00:00 2001 From: David Volovskiy Date: Sun, 21 Dec 2025 13:22:43 -0500 Subject: [PATCH 109/179] sound effect on hit --- watch-faces/complication/ping_face.c | 1 + 1 file changed, 1 insertion(+) diff --git a/watch-faces/complication/ping_face.c b/watch-faces/complication/ping_face.c index 414ab8e8..4f80c8d7 100644 --- a/watch-faces/complication/ping_face.c +++ b/watch-faces/complication/ping_face.c @@ -461,6 +461,7 @@ static void update_game(ping_state_t *state) { display_lose_screen(state); } else if (game_result == RESULT_HIT) { add_to_score(state); + watch_buzzer_play_note(BUZZER_NOTE_C5, 60); } } From 90e99f89575aaacb419178367cdaf947c6ccd836 Mon Sep 17 00:00:00 2001 From: David Volovskiy Date: Sun, 21 Dec 2025 13:39:16 -0500 Subject: [PATCH 110/179] First hit not counted; bugfixes on sound --- watch-faces/complication/ping_face.c | 30 ++++++++++++++++------------ 1 file changed, 17 insertions(+), 13 deletions(-) diff --git a/watch-faces/complication/ping_face.c b/watch-faces/complication/ping_face.c index 4f80c8d7..56165215 100644 --- a/watch-faces/complication/ping_face.c +++ b/watch-faces/complication/ping_face.c @@ -54,7 +54,8 @@ typedef enum { typedef enum { RESULT_LOSE = -1, RESULT_NONE = 0, - RESULT_HIT = 1 + RESULT_HIT = 1, + RESULT_FIRST_HIT = 2, } PingResult; #define FREQ_BABY 2 @@ -207,7 +208,7 @@ static void display_ball(void) { char_display = '#'; } } else { - if (_is_custom_lcd || char_pos == 4 || char_pos == 6) { + if (!_is_custom_lcd && (char_pos == 4 || char_pos == 6)) { char_display = 'n'; // No need to check for overlap on these segments } else { if (overlap) { @@ -222,9 +223,11 @@ static void display_ball(void) { static PingResult update_ball(uint8_t difficulty) { bool ball_hit = paddle_hit_ball(); + bool first_hit = false; if (!game_state.ball_is_moving) { if (ball_hit) { game_state.ball_is_moving = true; + first_hit = true; } else { return RESULT_NONE; } @@ -235,7 +238,7 @@ static PingResult update_ball(uint8_t difficulty) { } display_ball(); if (ball_hit) { - return RESULT_HIT; + return first_hit ? RESULT_FIRST_HIT : RESULT_HIT; } else { return RESULT_NONE; } @@ -345,7 +348,7 @@ static void change_difficulty(ping_state_t *state) { } static void display_sound_indicator(bool soundOn) { - if (soundOn){ + if (soundOn) { watch_set_indicator(WATCH_INDICATOR_BELL); } else { watch_clear_indicator(WATCH_INDICATOR_BELL); @@ -355,7 +358,7 @@ static void display_sound_indicator(bool soundOn) { static void toggle_sound(ping_state_t *state) { state -> soundOn = !state -> soundOn; display_sound_indicator(state -> soundOn); - if (state -> soundOn){ + if (state -> soundOn) { watch_buzzer_play_note(BUZZER_NOTE_C5, 30); } } @@ -378,7 +381,7 @@ static void display_title(ping_state_t *state) { movement_request_tick_frequency(1); game_state.curr_screen = SCREEN_TITLE; watch_clear_colon(); - watch_display_text_with_fallback(WATCH_POSITION_TOP, "PING", "PI "); + watch_display_text_with_fallback(WATCH_POSITION_TOP, "Ping", "PI "); watch_display_text(WATCH_POSITION_BOTTOM, " Ping "); display_sound_indicator(state -> soundOn); _ticks_show_title = 1; @@ -392,7 +395,7 @@ static void display_score_screen(ping_state_t *state) { memset(&game_state, 0, sizeof(game_state)); game_state.curr_screen = SCREEN_SCORE; watch_set_colon(); - watch_display_text_with_fallback(WATCH_POSITION_TOP, "PING ", "PI "); + watch_display_text_with_fallback(WATCH_POSITION_TOP, "PI ", "PI "); if (hi_score > MAX_HI_SCORE) { watch_display_text(WATCH_POSITION_BOTTOM, "HS --"); } @@ -435,9 +438,6 @@ static void begin_playing(ping_state_t *state) { display_paddle(); display_ball(); display_score( game_state.curr_score); - if (state -> soundOn){ - watch_buzzer_play_sequence(start_tune, NULL); - } } static void display_lose_screen(ping_state_t *state) { @@ -461,7 +461,11 @@ static void update_game(ping_state_t *state) { display_lose_screen(state); } else if (game_result == RESULT_HIT) { add_to_score(state); - watch_buzzer_play_note(BUZZER_NOTE_C5, 60); + if (state -> soundOn) { + watch_buzzer_play_note(BUZZER_NOTE_C5, 60); + } + } else if (game_result == RESULT_FIRST_HIT && state -> soundOn) { + watch_buzzer_play_sequence(start_tune, NULL); } } @@ -542,13 +546,13 @@ bool ping_face_loop(movement_event_t event, void *context) { display_score_screen(state); break; } - else if (game_state.curr_screen == SCREEN_PLAYING){ + else if (game_state.curr_screen == SCREEN_PLAYING) { game_state.moving_from_tap = true; game_state.paddle_hit = true; } break; case EVENT_ALARM_BUTTON_DOWN: - if (game_state.curr_screen == SCREEN_PLAYING){ + if (game_state.curr_screen == SCREEN_PLAYING) { game_state.moving_from_tap = false; game_state.paddle_hit = true; } From cac1f50e8d0f3e82dbf90a556ec5aa7cfb2df049 Mon Sep 17 00:00:00 2001 From: David Volovskiy Date: Thu, 25 Dec 2025 09:25:47 -0500 Subject: [PATCH 111/179] Fixed clearing paddle on fast press --- watch-faces/complication/ping_face.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/watch-faces/complication/ping_face.c b/watch-faces/complication/ping_face.c index 56165215..a0591ba3 100644 --- a/watch-faces/complication/ping_face.c +++ b/watch-faces/complication/ping_face.c @@ -275,6 +275,8 @@ static void update_paddle(void) { case PADDLE_EXTENDING: if (!game_state.moving_from_tap && !HAL_GPIO_BTN_ALARM_read()) { game_state.paddle_pos = PADDLE_RETRACTED; + watch_display_character(' ', 8); + game_state.moving_from_tap = false; } else { game_state.paddle_pos = PADDLE_EXTENDED; } From eb9ec8659c42e34dc1b31e3ff591ac2bf842271f Mon Sep 17 00:00:00 2001 From: Alessandro Genova Date: Sat, 26 Jul 2025 21:40:22 -0400 Subject: [PATCH 112/179] Initial seemingly stable version of movement using the RTC COUNTER32 mode --- Makefile | 4 + movement.c | 765 +++++++++++++-------- movement.h | 39 +- watch-library/hardware/watch/rtc32.c | 138 ++++ watch-library/hardware/watch/rtc32.h | 98 +++ watch-library/hardware/watch/watch_rtc.c | 187 +++-- watch-library/hardware/watch/watch_tcc.c | 119 +++- watch-library/shared/watch/watch_rtc.h | 68 +- watch-library/shared/watch/watch_tcc.h | 30 +- watch-library/shared/watch/watch_utility.c | 4 + watch-library/shared/watch/watch_utility.h | 10 + watch-library/simulator/watch/watch_rtc.c | 320 +++++---- watch-library/simulator/watch/watch_tcc.c | 67 +- 13 files changed, 1335 insertions(+), 514 deletions(-) create mode 100644 watch-library/hardware/watch/rtc32.c create mode 100644 watch-library/hardware/watch/rtc32.h diff --git a/Makefile b/Makefile index cae2b943..3c4436f3 100644 --- a/Makefile +++ b/Makefile @@ -20,6 +20,9 @@ TINYUSB_CDC=1 # Now we're all set to include gossamer's make rules. include $(GOSSAMER_PATH)/make.mk +# Don't add gossamer's rtc.c since we are using our own rtc32.c +SRCS := $(filter-out $(GOSSAMER_PATH)/peripherals/rtc.c,$(SRCS)) + CFLAGS+=-D_POSIX_C_SOURCE=200112L define n @@ -136,6 +139,7 @@ INCLUDES += \ -I./watch-library/hardware/watch \ SRCS += \ + ./watch-library/hardware/watch/rtc32.c \ ./watch-library/hardware/watch/watch.c \ ./watch-library/hardware/watch/watch_adc.c \ ./watch-library/hardware/watch/watch_deepsleep.c \ diff --git a/movement.c b/movement.c index a37e8df2..adc01871 100644 --- a/movement.c +++ b/movement.c @@ -2,6 +2,7 @@ * MIT License * * Copyright (c) 2022 Joey Castillo + * Copyright (c) 2025 Alessandro Genova * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -59,7 +60,55 @@ void * watch_face_contexts[MOVEMENT_NUM_FACES]; watch_date_time_t scheduled_tasks[MOVEMENT_NUM_FACES]; const int32_t movement_le_inactivity_deadlines[8] = {INT_MAX, 600, 3600, 7200, 21600, 43200, 86400, 604800}; const int16_t movement_timeout_inactivity_deadlines[4] = {60, 120, 300, 1800}; -movement_event_t event; + +typedef struct { + movement_event_type_t down_event; + watch_cb_t cb_longpress; + movement_timeout_index_t timeout_index; + volatile bool is_down; + volatile rtc_counter_t down_timestamp; +} movement_button_t; + +/* Pieces of state that can be modified by the various interrupt callbacks. + The interrupt writes state changes here, and it will be acted upon on the next app_loop invokation. +*/ +typedef struct { + volatile uint32_t pending_events; + volatile bool turn_led_off; + volatile bool has_pending_sequence; + volatile bool enter_sleep_mode; + volatile bool exit_sleep_mode; + volatile bool is_sleeping; + volatile uint8_t subsecond; + volatile rtc_counter_t minute_counter; + volatile bool minute_alarm_fired; + volatile bool is_buzzing; + volatile uint8_t pending_sequence_priority; + + // button tracking for long press + movement_button_t mode_button; + movement_button_t light_button; + movement_button_t alarm_button; +} movement_volatile_state_t; + +movement_volatile_state_t movement_volatile_state; + +// The last sequence that we have been asked to play while the watch was in deep sleep +static int8_t *_pending_sequence; + +// The note sequence of the default alarm +int8_t alarm_tune[] = { + BUZZER_NOTE_C8, 4, + BUZZER_NOTE_REST, 4, + BUZZER_NOTE_C8, 4, + BUZZER_NOTE_REST, 4, + BUZZER_NOTE_C8, 4, + BUZZER_NOTE_REST, 4, + BUZZER_NOTE_C8, 6, + BUZZER_NOTE_REST, 18, + -8, 9, + 0 +}; int8_t _movement_dst_offset_cache[NUM_ZONE_NAMES] = {0}; #define TIMEZONE_DOES_NOT_OBSERVE (-127) @@ -68,9 +117,16 @@ void cb_mode_btn_interrupt(void); void cb_light_btn_interrupt(void); void cb_alarm_btn_interrupt(void); void cb_alarm_btn_extwake(void); -void cb_alarm_fired(void); -void cb_fast_tick(void); +void cb_minute_alarm_fired(void); void cb_tick(void); +void cb_mode_btn_timeout_interrupt(void); +void cb_light_btn_timeout_interrupt(void); +void cb_alarm_btn_timeout_interrupt(void); +void cb_led_timeout_interrupt(void); +void cb_resign_timeout_interrupt(void); +void cb_sleep_timeout_interrupt(void); +void cb_buzzer_start(void); +void cb_buzzer_stop(void); void cb_accelerometer_event(void); void cb_accelerometer_wake(void); @@ -97,6 +153,21 @@ static udatetime_t _movement_convert_date_time_to_udate(watch_date_time_t date_t }; } +static void _movement_set_top_of_minute_alarm() { + uint32_t counter = watch_rtc_get_counter(); + watch_date_time_t date_time = watch_rtc_get_date_time(); + uint32_t freq = watch_rtc_get_frequency(); + + // remove subsecond from counter + counter &= ~(freq - 1); + // counter at the next top of the minute + counter += (60 - date_time.unit.second) * freq; + + movement_volatile_state.minute_counter = counter; + + watch_rtc_register_comp_callback(cb_minute_alarm_fired, counter, MINUTE_TIMEOUT); +} + static bool _movement_update_dst_offset_cache(void) { uzone_t local_zone; udatetime_t udate_time; @@ -127,25 +198,27 @@ static bool _movement_update_dst_offset_cache(void) { } static inline void _movement_reset_inactivity_countdown(void) { - movement_state.le_mode_ticks = movement_le_inactivity_deadlines[movement_state.settings.bit.le_interval]; - movement_state.timeout_ticks = movement_timeout_inactivity_deadlines[movement_state.settings.bit.to_interval]; + rtc_counter_t counter = watch_rtc_get_counter(); + uint32_t freq = watch_rtc_get_frequency(); + + watch_rtc_register_comp_callback( + cb_resign_timeout_interrupt, + counter + movement_timeout_inactivity_deadlines[movement_state.settings.bit.to_interval] * freq, + RESIGN_TIMEOUT + ); + + movement_volatile_state.enter_sleep_mode = false; + + watch_rtc_register_comp_callback( + cb_sleep_timeout_interrupt, + counter + movement_le_inactivity_deadlines[movement_state.settings.bit.le_interval] * freq, + SLEEP_TIMEOUT + ); } -static inline void _movement_enable_fast_tick_if_needed(void) { - if (!movement_state.fast_tick_enabled) { - movement_state.fast_ticks = 0; - watch_rtc_register_periodic_callback(cb_fast_tick, 128); - movement_state.fast_tick_enabled = true; - } -} - -static inline void _movement_disable_fast_tick_if_possible(void) { - if ((movement_state.light_ticks == -1) && - (movement_state.alarm_ticks == -1) && - ((movement_state.light_down_timestamp + movement_state.mode_down_timestamp + movement_state.alarm_down_timestamp) == 0)) { - movement_state.fast_tick_enabled = false; - watch_rtc_disable_periodic_callback(128); - } +static inline void _movement_disable_inactivity_countdown(void) { + watch_rtc_disable_comp_callback(RESIGN_TIMEOUT); + watch_rtc_disable_comp_callback(SLEEP_TIMEOUT); } static void _movement_handle_top_of_minute(void) { @@ -172,7 +245,6 @@ static void _movement_handle_top_of_minute(void) { // TODO: handle other advisory types } } - movement_state.woke_from_alarm_handler = false; } static void _movement_handle_scheduled_tasks(void) { @@ -203,45 +275,59 @@ static void _movement_handle_scheduled_tasks(void) { } void movement_request_tick_frequency(uint8_t freq) { - // Movement uses the 128 Hz tick internally - if (freq == 128) return; - // Movement requires at least a 1 Hz tick. // If we are asked for an invalid frequency, default back to 1 Hz. if (freq == 0 || __builtin_popcount(freq) != 1) freq = 1; - // disable all callbacks except the 128 Hz one - watch_rtc_disable_matching_periodic_callbacks(0xFE); + // disable all periodic callbacks + watch_rtc_disable_matching_periodic_callbacks(0xFF); + + // this left-justifies the period in a 32-bit integer. + uint32_t tmp = (freq & 0xFF) << 24; + // now we can count the leading zeroes to get the value we need. + // 0x01 (1 Hz) will have 7 leading zeros for PER7. 0x80 (128 Hz) will have no leading zeroes for PER0. + uint8_t per_n = __builtin_clz(tmp); - movement_state.subsecond = 0; movement_state.tick_frequency = freq; + movement_state.tick_pern = per_n; + watch_rtc_register_periodic_callback(cb_tick, freq); } void movement_illuminate_led(void) { if (movement_state.settings.bit.led_duration != 0b111) { + movement_state.light_on = true; watch_set_led_color_rgb(movement_state.settings.bit.led_red_color | movement_state.settings.bit.led_red_color << 4, movement_state.settings.bit.led_green_color | movement_state.settings.bit.led_green_color << 4, movement_state.settings.bit.led_blue_color | movement_state.settings.bit.led_blue_color << 4); if (movement_state.settings.bit.led_duration == 0) { - movement_state.light_ticks = 1; + // Do nothing it'll be turned off on button release } else { - movement_state.light_ticks = (movement_state.settings.bit.led_duration * 2 - 1) * 128; + // Set a timeout to turn off the light + rtc_counter_t counter = watch_rtc_get_counter(); + uint32_t freq = watch_rtc_get_frequency(); + watch_rtc_register_comp_callback( + cb_led_timeout_interrupt, + counter + (movement_state.settings.bit.led_duration * 2 - 1) * freq, + LED_TIMEOUT + ); } - _movement_enable_fast_tick_if_needed(); } } void movement_force_led_on(uint8_t red, uint8_t green, uint8_t blue) { // this is hacky, we need a way for watch faces to set an arbitrary color and prevent Movement from turning it right back off. + movement_state.light_on = true; watch_set_led_color_rgb(red, green, blue); - movement_state.light_ticks = 32767; + rtc_counter_t counter = watch_rtc_get_counter(); + watch_rtc_register_comp_callback(cb_led_timeout_interrupt, counter + 32767, LED_TIMEOUT); } void movement_force_led_off(void) { + movement_state.light_on = false; + // The led timeout probably already triggered, but still disable just in case we are switching off the light by other means + watch_rtc_disable_comp_callback(LED_TIMEOUT); watch_set_led_off(); - movement_state.light_ticks = -1; - _movement_disable_fast_tick_if_possible(); } bool movement_default_loop_handler(movement_event_t event) { @@ -315,63 +401,98 @@ void movement_cancel_background_task_for_face(uint8_t watch_face_index) { } void movement_request_sleep(void) { - /// FIXME: for #SecondMovement: This was a feature request to allow watch faces to request sleep. - /// Setting the ticks to 1 means the watch will sleep after the next tick. I'd like to say let's - /// set it to 0, have the watch face loop return false, and then we'll fall asleep immediately. - /// But could this lead to a race condition where the callback decrements to -1 before the loop? - /// This is the safest way but consider more testing here. - movement_state.le_mode_ticks = 1; + movement_volatile_state.enter_sleep_mode = true; } void movement_request_wake() { - movement_state.needs_wake = true; + movement_volatile_state.exit_sleep_mode = true; _movement_reset_inactivity_countdown(); } -static void end_buzzing() { - movement_state.is_buzzing = false; +void cb_buzzer_start(void) { + movement_volatile_state.is_buzzing = true; } -static void end_buzzing_and_disable_buzzer(void) { - end_buzzing(); - watch_disable_buzzer(); +void cb_buzzer_stop(void) { + movement_volatile_state.is_buzzing = false; + movement_volatile_state.pending_sequence_priority = 0; +} + +void movement_play_note(watch_buzzer_note_t note, uint16_t duration_ms) { + static int8_t single_note_sequence[3]; + + single_note_sequence[0] = note; + // 48 ticks per second for the tc0? + // Each tick is approximately 20ms + uint16_t duration = duration_ms / 20; + if (duration > 127) duration = 127; + single_note_sequence[1] = (int8_t)duration; + single_note_sequence[2] = 0; + + movement_play_sequence(single_note_sequence, 0); } void movement_play_signal(void) { - void *maybe_disable_buzzer = end_buzzing_and_disable_buzzer; - if (watch_is_buzzer_or_led_enabled()) { - maybe_disable_buzzer = end_buzzing; - } else { - watch_enable_buzzer(); - } - movement_state.is_buzzing = true; - watch_buzzer_play_sequence(signal_tune, maybe_disable_buzzer); - if (movement_state.le_mode_ticks == -1) { - // the watch is asleep. wake it up for "1" round through the main loop. - // the sleep_mode_app_loop will notice the is_buzzing and note that it - // only woke up to beep and then it will spinlock until the callback - // turns off the is_buzzing flag. - movement_state.needs_wake = true; - movement_state.le_mode_ticks = 1; - } + movement_play_sequence(signal_tune, 1); } void movement_play_alarm(void) { - movement_play_alarm_beeps(5, BUZZER_NOTE_C8); + movement_play_sequence(alarm_tune, 2); } void movement_play_alarm_beeps(uint8_t rounds, watch_buzzer_note_t alarm_note) { + // Ugly but necessary to avoid breaking backward compatibility with some faces. + // Create an alarm tune on the fly with the specified note and repetition. + static int8_t custom_alarm_tune[19]; + if (rounds == 0) rounds = 1; if (rounds > 20) rounds = 20; - movement_request_wake(); - movement_state.alarm_note = alarm_note; - // our tone is 0.375 seconds of beep and 0.625 of silence, repeated as given. - movement_state.alarm_ticks = 128 * rounds - 75; - _movement_enable_fast_tick_if_needed(); + + for (uint8_t i = 0; i < 9; i++) { + uint8_t note_idx = i * 2; + uint8_t duration_idx = note_idx + 1; + + int8_t note = alarm_tune[note_idx]; + int8_t duration = alarm_tune[duration_idx]; + + if (note == BUZZER_NOTE_C8) { + note = alarm_note; + } else if (note < 0) { + duration = rounds; + } + + custom_alarm_tune[note_idx] = note; + custom_alarm_tune[duration_idx] = duration; + } + + custom_alarm_tune[18] = 0; + + movement_play_sequence(custom_alarm_tune, 2); +} + +void movement_play_sequence(int8_t *note_sequence, uint8_t priority) { + // Priority is used to ensure that lower priority sequences don't cancel higher priority ones + // Priotity order: alarm(2) > signal(1) > note(0) + if (priority < movement_volatile_state.pending_sequence_priority) { + return; + } + + movement_volatile_state.pending_sequence_priority = priority; + + // The tcc is off during sleep, we can't play immediately. + // Ask to wake up the watch. + if (movement_volatile_state.is_sleeping) { + _pending_sequence = note_sequence; + movement_volatile_state.has_pending_sequence = true; + movement_volatile_state.exit_sleep_mode = true; + } else { + watch_buzzer_play_sequence_with_volume(note_sequence, NULL, movement_button_volume()); + } } uint8_t movement_claim_backup_register(void) { - if (movement_state.next_available_backup_register >= 8) return 0; + // We use backup register 7 in watch_rtc to keep track of the reference time + if (movement_state.next_available_backup_register >= 7) return 0; return movement_state.next_available_backup_register++; } @@ -405,18 +526,20 @@ watch_date_time_t movement_get_utc_date_time(void) { watch_date_time_t movement_get_date_time_in_zone(uint8_t zone_index) { int32_t offset = movement_get_current_timezone_offset_for_zone(zone_index); - return watch_utility_date_time_convert_zone(watch_rtc_get_date_time(), 0, offset); + unix_timestamp_t timestamp = watch_rtc_get_unix_time(); + return watch_utility_date_time_from_unix_time(timestamp, offset); } watch_date_time_t movement_get_local_date_time(void) { - watch_date_time_t date_time = watch_rtc_get_date_time(); - return watch_utility_date_time_convert_zone(date_time, 0, movement_get_current_timezone_offset()); + unix_timestamp_t timestamp = watch_rtc_get_unix_time(); + return watch_utility_date_time_from_unix_time(timestamp, movement_get_current_timezone_offset()); } -void movement_set_local_date_time(watch_date_time_t date_time) { - int32_t current_offset = movement_get_current_timezone_offset(); - watch_date_time_t utc_date_time = watch_utility_date_time_convert_zone(date_time, current_offset, 0); - watch_rtc_set_date_time(utc_date_time); +void movement_set_utc_date_time(watch_date_time_t date_time) { + watch_rtc_set_date_time(date_time); + + // If the time was changed, the top of the minute alarm needs to be reset accordingly + _movement_set_top_of_minute_alarm(); // this may seem wasteful, but if the user's local time is in a zone that observes DST, // they may have just crossed a DST boundary, which means the next call to this function @@ -424,6 +547,12 @@ void movement_set_local_date_time(watch_date_time_t date_time) { _movement_update_dst_offset_cache(); } +void movement_set_local_date_time(watch_date_time_t date_time) { + int32_t current_offset = movement_get_current_timezone_offset(); + watch_date_time_t utc_date_time = watch_utility_date_time_convert_zone(date_time, current_offset, 0); + movement_set_utc_date_time(utc_date_time); +} + bool movement_button_should_sound(void) { return movement_state.settings.bit.button_should_sound; } @@ -625,6 +754,38 @@ void app_init(void) { memset((void *)&movement_state, 0, sizeof(movement_state)); + movement_volatile_state.pending_events = 0; + movement_volatile_state.turn_led_off = false; + + movement_volatile_state.minute_alarm_fired = false; + movement_volatile_state.minute_counter = 0; + + movement_volatile_state.enter_sleep_mode = false; + movement_volatile_state.exit_sleep_mode = false; + movement_volatile_state.has_pending_sequence = false; + movement_volatile_state.is_sleeping = false; + + movement_volatile_state.is_buzzing = false; + movement_volatile_state.pending_sequence_priority = 0; + + movement_volatile_state.mode_button.down_event = EVENT_MODE_BUTTON_DOWN; + movement_volatile_state.mode_button.is_down = false; + movement_volatile_state.mode_button.down_timestamp = 0; + movement_volatile_state.mode_button.timeout_index = MODE_BUTTON_TIMEOUT; + movement_volatile_state.mode_button.cb_longpress = cb_mode_btn_timeout_interrupt; + + movement_volatile_state.light_button.down_event = EVENT_LIGHT_BUTTON_DOWN; + movement_volatile_state.light_button.is_down = false; + movement_volatile_state.light_button.down_timestamp = 0; + movement_volatile_state.light_button.timeout_index = LIGHT_BUTTON_TIMEOUT; + movement_volatile_state.light_button.cb_longpress = cb_light_btn_timeout_interrupt; + + movement_volatile_state.alarm_button.down_event = EVENT_ALARM_BUTTON_DOWN; + movement_volatile_state.alarm_button.is_down = false; + movement_volatile_state.alarm_button.down_timestamp = 0; + movement_volatile_state.alarm_button.timeout_index = ALARM_BUTTON_TIMEOUT; + movement_volatile_state.alarm_button.cb_longpress = cb_alarm_btn_timeout_interrupt; + movement_state.has_thermistor = thermistor_driver_init(); bool settings_file_exists = filesystem_file_exists("settings.u32"); @@ -680,13 +841,19 @@ void app_init(void) { watch_rtc_set_date_time(date_time); } + // set up the 1 minute alarm (for background tasks and low power updates) + _movement_set_top_of_minute_alarm(); + + // register callbacks to be notified when buzzer starts/stops playing. + // this is so movement can be notified even when triggered by a face bypassing movement + watch_buzzer_register_global_callbacks(cb_buzzer_start, cb_buzzer_stop); + // populate the DST offset cache _movement_update_dst_offset_cache(); if (movement_state.accelerometer_motion_threshold == 0) movement_state.accelerometer_motion_threshold = 32; - movement_state.light_ticks = -1; - movement_state.alarm_ticks = -1; + movement_state.light_on = false; movement_state.next_available_backup_register = 2; _movement_reset_inactivity_countdown(); } @@ -721,17 +888,12 @@ void app_setup(void) { } } #endif - - // set up the 1 minute alarm (for background tasks and low power updates) - watch_date_time_t alarm_time; - alarm_time.reg = 0; - watch_rtc_register_alarm_callback(cb_alarm_fired, alarm_time, ALARM_MATCH_SS); } // LCD autodetect uses the buttons as a a failsafe, so we should run it before we enable the button interrupts watch_enable_display(); - if (movement_state.le_mode_ticks != -1) { + if (!movement_volatile_state.is_sleeping) { watch_disable_extwake_interrupt(HAL_GPIO_BTN_ALARM_pin()); watch_enable_external_interrupts(); @@ -808,150 +970,186 @@ void app_setup(void) { } watch_faces[movement_state.current_face_idx].activate(watch_face_contexts[movement_state.current_face_idx]); - event.subsecond = 0; - event.event_type = EVENT_ACTIVATE; + movement_volatile_state.pending_events |= 1 << EVENT_ACTIVATE; } } #ifndef MOVEMENT_LOW_ENERGY_MODE_FORBIDDEN static void _sleep_mode_app_loop(void) { - movement_state.needs_wake = false; - // as long as le_mode_ticks is -1 (i.e. we are in low energy mode), we wake up here, update the screen, and go right back to sleep. - while (movement_state.le_mode_ticks == -1) { - // we also have to handle top-of-the-minute tasks here in the mini-runloop - if (movement_state.woke_from_alarm_handler) _movement_handle_top_of_minute(); + // as long as we are in low energy mode, we wake up here, update the screen, and go right back to sleep. + while (movement_volatile_state.is_sleeping) { + // if we need to wake immediately, do it! + if (movement_volatile_state.exit_sleep_mode) { + movement_volatile_state.exit_sleep_mode = false; + movement_volatile_state.is_sleeping = false; + return; + } + + // we also have to handle top-of-the-minute tasks here in the mini-runloop + if (movement_volatile_state.minute_alarm_fired) { + movement_volatile_state.minute_alarm_fired = false; + _movement_handle_top_of_minute(); + } + + movement_event_t event; event.event_type = EVENT_LOW_ENERGY_UPDATE; + event.subsecond = 0; watch_faces[movement_state.current_face_idx].loop(event, watch_face_contexts[movement_state.current_face_idx]); - // if we need to wake immediately, do it! - if (movement_state.needs_wake) return; - // otherwise enter sleep mode, and when the extwake handler is called, it will reset le_mode_ticks and force us out at the next loop. - else watch_enter_sleep_mode(); + // If any of the previous loops requested to wake up, do it! + if (movement_volatile_state.exit_sleep_mode) { + movement_volatile_state.exit_sleep_mode = false; + movement_volatile_state.is_sleeping = false; + + return; + } + + // otherwise enter sleep mode, until either the top of the minute interrupt or extwake wakes us up. + watch_enter_sleep_mode(); } } #endif +static bool _switch_face(void) { + const watch_face_t *wf = &watch_faces[movement_state.current_face_idx]; + + wf->resign(watch_face_contexts[movement_state.current_face_idx]); + movement_state.current_face_idx = movement_state.next_face_idx; + // we have just updated the face idx, so we must recache the watch face pointer. + wf = &watch_faces[movement_state.current_face_idx]; + watch_clear_display(); + movement_request_tick_frequency(1); + + if (movement_state.settings.bit.button_should_sound) { + // low note for nonzero case, high note for return to watch_face 0 + movement_play_note(movement_state.next_face_idx ? BUZZER_NOTE_C7 : BUZZER_NOTE_C8, 50); + } + + wf->activate(watch_face_contexts[movement_state.current_face_idx]); + + movement_event_t event; + event.subsecond = 0; + event.event_type = EVENT_ACTIVATE; + movement_state.watch_face_changed = false; + bool can_sleep = wf->loop(event, watch_face_contexts[movement_state.current_face_idx]); + + return can_sleep; +} + bool app_loop(void) { const watch_face_t *wf = &watch_faces[movement_state.current_face_idx]; - bool woke_up_for_buzzer = false; - - if (movement_state.watch_face_changed) { - if (movement_state.settings.bit.button_should_sound) { - // low note for nonzero case, high note for return to watch_face 0 - watch_buzzer_play_note_with_volume(movement_state.next_face_idx ? BUZZER_NOTE_C7 : BUZZER_NOTE_C8, 50, movement_state.settings.bit.button_volume); - } - wf->resign(watch_face_contexts[movement_state.current_face_idx]); - movement_state.current_face_idx = movement_state.next_face_idx; - // we have just updated the face idx, so we must recache the watch face pointer. - wf = &watch_faces[movement_state.current_face_idx]; - watch_clear_display(); - movement_request_tick_frequency(1); - wf->activate(watch_face_contexts[movement_state.current_face_idx]); - event.subsecond = 0; - event.event_type = EVENT_ACTIVATE; - movement_state.watch_face_changed = false; - } - - // if the LED should be off, turn it off - if (movement_state.light_ticks == 0) { - // unless the user is holding down the LIGHT button, in which case, give them more time. - if (HAL_GPIO_BTN_LIGHT_read()) { - movement_state.light_ticks = 1; - } else { - movement_force_led_off(); - } - } - - // handle top-of-minute tasks, if the alarm handler told us we need to - if (movement_state.woke_from_alarm_handler) _movement_handle_top_of_minute(); - - // if we have a scheduled background task, handle that here: - if (event.event_type == EVENT_TICK && movement_state.has_scheduled_background_task) _movement_handle_scheduled_tasks(); - -#ifndef MOVEMENT_LOW_ENERGY_MODE_FORBIDDEN - // if we have timed out of our low energy mode countdown, enter low energy mode. - if (movement_state.le_mode_ticks == 0) { - movement_state.le_mode_ticks = -1; - watch_register_extwake_callback(HAL_GPIO_BTN_ALARM_pin(), cb_alarm_btn_extwake, true); - event.event_type = EVENT_NONE; - event.subsecond = 0; - - // _sleep_mode_app_loop takes over at this point and loops until le_mode_ticks is reset by the extwake handler, - // or wake is requested using the movement_request_wake function. - _sleep_mode_app_loop(); - // as soon as _sleep_mode_app_loop returns, we prepare to reactivate - // ourselves, but first, we check to see if we woke up for the buzzer: - if (movement_state.is_buzzing) { - woke_up_for_buzzer = true; - } - event.event_type = EVENT_ACTIVATE; - // this is a hack tho: waking from sleep mode, app_setup does get called, but it happens before we have reset our ticks. - // need to figure out if there's a better heuristic for determining how we woke up. - app_setup(); - } -#endif // default to being allowed to sleep by the face. bool can_sleep = true; - if (event.event_type) { - event.subsecond = movement_state.subsecond; - // the first trip through the loop overrides the can_sleep state - can_sleep = wf->loop(event, watch_face_contexts[movement_state.current_face_idx]); + // Any events that have been added by the various interrupts in between app_loop invokations + uint32_t pending_events = movement_volatile_state.pending_events; + movement_volatile_state.pending_events = 0; - // Keep light on if user is still interacting with the watch. - if (movement_state.light_ticks > 0) { - switch (event.event_type) { - case EVENT_LIGHT_BUTTON_DOWN: - case EVENT_MODE_BUTTON_DOWN: - case EVENT_ALARM_BUTTON_DOWN: - movement_illuminate_led(); - } + movement_event_t event; + event.event_type = EVENT_NONE; + // Subsecond is determined by the TICK event, if concurrent events have happened, + // they will all have the same subsecond as they should to keep backward compatibility. + event.subsecond = movement_volatile_state.subsecond; + + // if the LED should be off, turn it off + if (movement_volatile_state.turn_led_off) { + // unless the user is holding down the LIGHT button, in which case, give them more time. + if (movement_volatile_state.light_button.is_down) { + } else { + movement_volatile_state.turn_led_off = false; + movement_force_led_off(); } + } - event.event_type = EVENT_NONE; + // actually play the note sequence we were asked to play while in deep sleep. + if (movement_volatile_state.has_pending_sequence) { + movement_volatile_state.has_pending_sequence = false; + watch_buzzer_play_sequence_with_volume(_pending_sequence, movement_request_sleep, movement_button_volume()); + // When this sequence is done playing, movement_request_sleep is invoked and the watch will go, + // back to sleep (unless the user interacts with it in the meantime) + _pending_sequence = NULL; } - // if we have timed out of our timeout countdown, give the app a hint that they can resign. - if (movement_state.timeout_ticks == 0 && movement_state.current_face_idx != 0) { - movement_state.timeout_ticks = -1; + // handle top-of-minute tasks, if the alarm handler told us we need to + if (movement_volatile_state.minute_alarm_fired) { + movement_volatile_state.minute_alarm_fired = false; + _movement_handle_top_of_minute(); + } + + // if we have a scheduled background task, handle that here: + if ( + (pending_events & (1 << EVENT_TICK)) + && event.subsecond == 0 + && movement_state.has_scheduled_background_task + ) { + _movement_handle_scheduled_tasks(); + } + + // Delay auto light off if the user is still interacting with the watch. + if (movement_state.light_on) { + if (pending_events & ( + (1 << EVENT_LIGHT_BUTTON_DOWN) | + (1 << EVENT_MODE_BUTTON_DOWN) | + (1 << EVENT_ALARM_BUTTON_DOWN) + )) { + movement_illuminate_led(); + } + } + + // Pop the EVENT_TIMEOUT out of the pending_events so it can be handled separately + bool resign_timeout = (pending_events & (1 << EVENT_TIMEOUT)) != 0; + if (resign_timeout) { + pending_events &= ~(1 << EVENT_TIMEOUT); + } + + // Consume all the pending events + movement_event_type_t event_type = 0; + while (pending_events) { + if (pending_events & 1) { + event.event_type = event_type; + can_sleep = wf->loop(event, watch_face_contexts[movement_state.current_face_idx]) && can_sleep; + } + pending_events = pending_events >> 1; + event_type++; + } + + // Now handle the EVENT_TIMEOUT + if (resign_timeout && movement_state.current_face_idx != 0) { event.event_type = EVENT_TIMEOUT; - event.subsecond = movement_state.subsecond; - // if we run through the loop again to time out, we need to reconsider whether or not we can sleep. - // if the first trip said true, but this trip said false, we need the false to override, thus - // we will be using boolean AND: - // - // first trip | can sleep | cannot sleep | can sleep | cannot sleep - // second trip | can sleep | cannot sleep | cannot sleep | can sleep - // && | can sleep | cannot sleep | cannot sleep | cannot sleep - bool can_sleep2 = wf->loop(event, watch_face_contexts[movement_state.current_face_idx]); - can_sleep = can_sleep && can_sleep2; - event.event_type = EVENT_NONE; + can_sleep = wf->loop(event, watch_face_contexts[movement_state.current_face_idx]) && can_sleep; } - // Now that we've handled all display update tasks, handle the alarm. - if (movement_state.alarm_ticks >= 0) { - uint8_t buzzer_phase = (movement_state.alarm_ticks + 80) % 128; - if(buzzer_phase == 127) { - // failsafe: buzzer could have been disabled in the meantime - if (!watch_is_buzzer_or_led_enabled()) watch_enable_buzzer(); - // play 4 beeps plus pause - for(uint8_t i = 0; i < 4; i++) { - // TODO: This method of playing the buzzer blocks the UI while it's beeping. - // It might be better to time it with the fast tick. - watch_buzzer_play_note(movement_state.alarm_note, (i != 3) ? 50 : 75); - if (i != 3) watch_buzzer_play_note(BUZZER_NOTE_REST, 50); - } - } - if (movement_state.alarm_ticks == 0) { - movement_state.alarm_ticks = -1; - _movement_disable_fast_tick_if_possible(); - } + // The watch_face_changed flag might be set again by the face loop, so check it again + if (movement_state.watch_face_changed) { + can_sleep = _switch_face() && can_sleep; } +#ifndef MOVEMENT_LOW_ENERGY_MODE_FORBIDDEN + // if we have timed out of our low energy mode countdown, enter low energy mode. + if (movement_volatile_state.enter_sleep_mode && !movement_volatile_state.is_buzzing) { + movement_volatile_state.enter_sleep_mode = false; + movement_volatile_state.is_sleeping = true; + + // No need to fire resign and sleep interrupts while in sleep mode + _movement_disable_inactivity_countdown(); + + watch_register_extwake_callback(HAL_GPIO_BTN_ALARM_pin(), cb_alarm_btn_extwake, true); + + // _sleep_mode_app_loop takes over at this point and loops until exit_sleep_mode is set by the extwake handler, + // or wake is requested using the movement_request_wake function. + _sleep_mode_app_loop(); + // as soon as _sleep_mode_app_loop returns, we prepare to reactivate + + // // this is a hack tho: waking from sleep mode, app_setup does get called, but it happens before we have reset our ticks. + // // need to figure out if there's a better heuristic for determining how we woke up. + app_setup(); + } +#endif + #if __EMSCRIPTEN__ shell_task(); #else @@ -961,19 +1159,6 @@ bool app_loop(void) { } #endif - event.subsecond = 0; - - // if the watch face changed, we can't sleep because we need to update the display. - if (movement_state.watch_face_changed) can_sleep = false; - - // if we woke up for the buzzer, stay awake until it's finished. - if (woke_up_for_buzzer) { - while(watch_is_buzzer_or_led_enabled()); - } - - // if the LED is on, we need to stay awake to keep the TCC running. - if (movement_state.light_ticks != -1) can_sleep = false; - // if we are plugged into USB, we can't sleep because we need to keep the serial shell running. if (usb_is_enabled()) { yield(); @@ -983,114 +1168,152 @@ bool app_loop(void) { return can_sleep; } -static movement_event_type_t _figure_out_button_event(bool pin_level, movement_event_type_t button_down_event_type, volatile uint16_t *down_timestamp) { - // force alarm off if the user pressed a button. - if (movement_state.alarm_ticks) movement_state.alarm_ticks = 0; +static movement_event_type_t _process_button_event(bool pin_level, movement_button_t* button) { + // This shouldn't happen normally + if (pin_level == button->is_down) { + return EVENT_NONE; + } + + uint32_t counter = watch_rtc_get_counter(); + + button->is_down = pin_level; if (pin_level) { - // handle rising edge - _movement_enable_fast_tick_if_needed(); - *down_timestamp = movement_state.fast_ticks + 1; - return button_down_event_type; + // We schedule a timeout to fire the longpress event + button->down_timestamp = counter; + watch_rtc_register_comp_callback(button->cb_longpress, counter + MOVEMENT_LONG_PRESS_TICKS, button->timeout_index); + // force alarm off if the user pressed a button. + watch_buzzer_abort_sequence(); + return button->down_event; } else { - // this line is hack but it handles the situation where the light button was held for more than 20 seconds. - // fast tick is disabled by then, and the LED would get stuck on since there's no one left decrementing light_ticks. - if (movement_state.light_ticks == 1) movement_state.light_ticks = 0; - // now that that's out of the way, handle falling edge - uint16_t diff = movement_state.fast_ticks - *down_timestamp; - *down_timestamp = 0; - _movement_disable_fast_tick_if_possible(); - // any press over a half second is considered a long press. Fire the long-up event - if (diff > MOVEMENT_LONG_PRESS_TICKS) return button_down_event_type + 3; - else return button_down_event_type + 1; + // We cancel the timeout if it hasn't fired yet + watch_rtc_disable_comp_callback(button->timeout_index); + if ((counter - button->down_timestamp) >= MOVEMENT_LONG_PRESS_TICKS) { + return button->down_event + 3; + } else { + return button->down_event + 1; + } } } void cb_light_btn_interrupt(void) { bool pin_level = HAL_GPIO_BTN_LIGHT_read(); + + movement_volatile_state.pending_events |= 1 << _process_button_event(pin_level, &movement_volatile_state.light_button); + _movement_reset_inactivity_countdown(); - event.event_type = _figure_out_button_event(pin_level, EVENT_LIGHT_BUTTON_DOWN, &movement_state.light_down_timestamp); } void cb_mode_btn_interrupt(void) { bool pin_level = HAL_GPIO_BTN_MODE_read(); + + movement_volatile_state.pending_events |= 1 << _process_button_event(pin_level, &movement_volatile_state.mode_button); + _movement_reset_inactivity_countdown(); - event.event_type = _figure_out_button_event(pin_level, EVENT_MODE_BUTTON_DOWN, &movement_state.mode_down_timestamp); } void cb_alarm_btn_interrupt(void) { bool pin_level = HAL_GPIO_BTN_ALARM_read(); + + movement_volatile_state.pending_events |= 1 << _process_button_event(pin_level, &movement_volatile_state.alarm_button); + _movement_reset_inactivity_countdown(); - event.event_type = _figure_out_button_event(pin_level, EVENT_ALARM_BUTTON_DOWN, &movement_state.alarm_down_timestamp); +} + +static movement_event_type_t _process_button_longpress_timeout(movement_button_t* button) { + // Looks like all these checks are not needed for the longpress detection to work reliably. + // Keep the code around for now in case problems arise long-term. + + // if (!button->is_down) { + // return EVENT_NONE; + // } + + // movement_event_type_t up_event = button->down_event + 1; + + // if (movement_volatile_state.pending_events & 1 << up_event) { + // return EVENT_NONE; + // } + + // uint32_t counter = watch_rtc_get_counter(); + // if ((counter - button->down_timestamp) < MOVEMENT_LONG_PRESS_TICKS) { + // return EVENT_NONE; + // } + + movement_event_type_t longpress_event = button->down_event + 2; + + return longpress_event; +} + +void cb_light_btn_timeout_interrupt(void) { + movement_button_t* button = &movement_volatile_state.light_button; + + movement_volatile_state.pending_events |= 1 << _process_button_longpress_timeout(button); +} + +void cb_mode_btn_timeout_interrupt(void) { + movement_button_t* button = &movement_volatile_state.mode_button; + + movement_volatile_state.pending_events |= 1 << _process_button_longpress_timeout(button); +} + +void cb_alarm_btn_timeout_interrupt(void) { + movement_button_t* button = &movement_volatile_state.alarm_button; + + movement_volatile_state.pending_events |= 1 << _process_button_longpress_timeout(button); +} + +void cb_led_timeout_interrupt(void) { + movement_volatile_state.turn_led_off = true; +} + +void cb_resign_timeout_interrupt(void) { + movement_volatile_state.pending_events |= 1 << EVENT_TIMEOUT; +} + +void cb_sleep_timeout_interrupt(void) { + movement_request_sleep(); } void cb_alarm_btn_extwake(void) { // wake up! - _movement_reset_inactivity_countdown(); + movement_request_wake(); } -void cb_alarm_fired(void) { +void cb_minute_alarm_fired(void) { + movement_volatile_state.minute_alarm_fired = true; + #if __EMSCRIPTEN__ _wake_up_simulator(); #endif - movement_state.woke_from_alarm_handler = true; -} - -void cb_fast_tick(void) { - movement_state.fast_ticks++; - if (movement_state.light_ticks > 0) movement_state.light_ticks--; - if (movement_state.alarm_ticks > 0) movement_state.alarm_ticks--; - // check timestamps and auto-fire the long-press events - // Notice: is it possible that two or more buttons have an identical timestamp? In this case - // only one of these buttons would receive the long press event. Don't bother for now... - if (movement_state.light_down_timestamp > 0) - if (movement_state.fast_ticks - movement_state.light_down_timestamp == MOVEMENT_LONG_PRESS_TICKS + 1) - event.event_type = EVENT_LIGHT_LONG_PRESS; - if (movement_state.mode_down_timestamp > 0) - if (movement_state.fast_ticks - movement_state.mode_down_timestamp == MOVEMENT_LONG_PRESS_TICKS + 1) - event.event_type = EVENT_MODE_LONG_PRESS; - if (movement_state.alarm_down_timestamp > 0) - if (movement_state.fast_ticks - movement_state.alarm_down_timestamp == MOVEMENT_LONG_PRESS_TICKS + 1) - event.event_type = EVENT_ALARM_LONG_PRESS; - // this is just a fail-safe; fast tick should be disabled as soon as the button is up, the LED times out, and/or the alarm finishes. - // but if for whatever reason it isn't, this forces the fast tick off after 20 seconds. - if (movement_state.fast_ticks >= 128 * 20) { - watch_rtc_disable_periodic_callback(128); - movement_state.fast_tick_enabled = false; - } + // Renew the alarm for a minute from the previous one (ensures no drift) + movement_volatile_state.minute_counter += watch_rtc_get_ticks_per_minute(); + watch_rtc_register_comp_callback(cb_minute_alarm_fired, movement_volatile_state.minute_counter, MINUTE_TIMEOUT); } void cb_tick(void) { - event.event_type = EVENT_TICK; - watch_date_time_t date_time = watch_rtc_get_date_time(); - if (date_time.unit.second != movement_state.last_second) { - // TODO: can we consolidate these two ticks? - if (movement_state.le_mode_ticks > 0) movement_state.le_mode_ticks--; - if (movement_state.timeout_ticks > 0) movement_state.timeout_ticks--; - - movement_state.last_second = date_time.unit.second; - movement_state.subsecond = 0; - } else { - movement_state.subsecond++; - } + rtc_counter_t counter = watch_rtc_get_counter(); + uint32_t freq = watch_rtc_get_frequency(); + uint32_t subsecond_mask = freq - 1; + movement_volatile_state.pending_events |= 1 << EVENT_TICK; + movement_volatile_state.subsecond = (counter & subsecond_mask) >> movement_state.tick_pern; } void cb_accelerometer_event(void) { uint8_t int_src = lis2dw_get_interrupt_source(); if (int_src & LIS2DW_REG_ALL_INT_SRC_DOUBLE_TAP) { - event.event_type = EVENT_DOUBLE_TAP; + movement_volatile_state.pending_events |= 1 << EVENT_DOUBLE_TAP; printf("Double tap!\n"); } if (int_src & LIS2DW_REG_ALL_INT_SRC_SINGLE_TAP) { - event.event_type = EVENT_SINGLE_TAP; + movement_volatile_state.pending_events |= 1 << EVENT_SINGLE_TAP; printf("Single tap!\n"); } } void cb_accelerometer_wake(void) { - event.event_type = EVENT_ACCELEROMETER_WAKE; + movement_volatile_state.pending_events |= 1 << EVENT_ACCELEROMETER_WAKE; // also: wake up! _movement_reset_inactivity_countdown(); } diff --git a/movement.h b/movement.h index 56f67a49..0288c9c9 100644 --- a/movement.h +++ b/movement.h @@ -134,6 +134,17 @@ typedef enum { EVENT_DOUBLE_TAP, // Accelerometer detected a double tap. This event is not yet implemented. } movement_event_type_t; +// Each different timeout type will use a different index when invoking watch_rtc_register_comp_callback +typedef enum { + LIGHT_BUTTON_TIMEOUT = 0, // Light button longpress timeout + MODE_BUTTON_TIMEOUT, // Mode button longpress timeout + ALARM_BUTTON_TIMEOUT, // Alarm button longpress timeout + LED_TIMEOUT, // LED off timeout + RESIGN_TIMEOUT, // Resign active face timeout + SLEEP_TIMEOUT, // Low-energy begin timeout + MINUTE_TIMEOUT, // Top of the Minute timeout +} movement_timeout_index_t; + typedef struct { uint8_t event_type; uint8_t subsecond; @@ -249,37 +260,16 @@ typedef struct { int16_t current_face_idx; int16_t next_face_idx; bool watch_face_changed; - bool fast_tick_enabled; - int16_t fast_ticks; // LED stuff - int16_t light_ticks; - - // alarm stuff - int16_t alarm_ticks; - bool is_buzzing; - watch_buzzer_note_t alarm_note; - - // button tracking for long press - uint16_t light_down_timestamp; - uint16_t mode_down_timestamp; - uint16_t alarm_down_timestamp; + bool light_on; // background task handling - bool woke_from_alarm_handler; bool has_scheduled_background_task; - bool needs_wake; - - // low energy mode countdown - int32_t le_mode_ticks; - - // app resignation countdown (TODO: consolidate with LE countdown?) - int16_t timeout_ticks; // stuff for subsecond tracking uint8_t tick_frequency; - uint8_t last_second; - uint8_t subsecond; + uint8_t tick_pern; // backup register stuff uint8_t next_available_backup_register; @@ -324,9 +314,11 @@ void movement_cancel_background_task_for_face(uint8_t watch_face_index); void movement_request_sleep(void); void movement_request_wake(void); +void movement_play_note(watch_buzzer_note_t note, uint16_t duration_ms); void movement_play_signal(void); void movement_play_alarm(void); void movement_play_alarm_beeps(uint8_t rounds, watch_buzzer_note_t alarm_note); +void movement_play_sequence(int8_t *note_sequence, uint8_t priority); uint8_t movement_claim_backup_register(void); @@ -340,6 +332,7 @@ watch_date_time_t movement_get_utc_date_time(void); watch_date_time_t movement_get_local_date_time(void); watch_date_time_t movement_get_date_time_in_zone(uint8_t zone_index); +void movement_set_utc_date_time(watch_date_time_t date_time); void movement_set_local_date_time(watch_date_time_t date_time); bool movement_button_should_sound(void); diff --git a/watch-library/hardware/watch/rtc32.c b/watch-library/hardware/watch/rtc32.c new file mode 100644 index 00000000..934564bb --- /dev/null +++ b/watch-library/hardware/watch/rtc32.c @@ -0,0 +1,138 @@ +/* + * MIT License + * + * Copyright (c) 2022 Joey Castillo + * Copyright (c) 2025 Alessandro Genova + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +#include +#include "rtc32.h" +#include "sam.h" + +rtc_cb_t _rtc_callback = NULL; + +#if defined(_SAMD21_) || defined(_SAMD11_) +#define CTRLREG (RTC->MODE0.CTRL) +#define MODE_SETTING (RTC_MODE0_CTRL_MODE_COUNT32_Val) // Mode 0 Count32 +#define PRESCALER_SETTING (RTC_MODE0_CTRL_PRESCALER_DIV8_Val) +#else +#define CTRLREG (RTC->MODE0.CTRLA) +#define MODE_SETTING (RTC_MODE0_CTRLA_MODE_COUNT32_Val) // Mode 0 Count32 +#define PRESCALER_SETTING (RTC_MODE0_CTRLA_PRESCALER_DIV8_Val) +#endif + +bool rtc_is_enabled(void) { + return CTRLREG.bit.ENABLE; +} + +static void _rtc_sync(void) { +#if defined(_SAMD21_) || defined(_SAMD11_) + while (RTC->MODE0.STATUS.bit.SYNCBUSY); +#else + while (RTC->MODE0.SYNCBUSY.reg & RTC_MODE0_SYNCBUSY_MASK); +#endif +} + +void rtc_init(void) { +#if defined(_SAMD21_) || defined(_SAMD11_) + // enable the RTC + PM->APBAMASK.reg |= PM_APBAMASK_RTC; + // clock RTC with GCLK3 (prescaled 1024 Hz output from the external crystal) + GCLK->CLKCTRL.reg = GCLK_CLKCTRL_GEN(3) | GCLK_CLKCTRL_ID(RTC_GCLK_ID) | GCLK_CLKCTRL_CLKEN; +#else + MCLK->APBAMASK.reg |= MCLK_APBAMASK_RTC; +#endif + + // if (rtc_is_enabled()) return; // don't reset the RTC if it's already set up. + // Reset everything, once things are stabilized we can think about preserving some state + CTRLREG.bit.ENABLE = 0; + + _rtc_sync(); + CTRLREG.bit.SWRST = 1; + _rtc_sync(); + + CTRLREG.bit.MODE = MODE_SETTING; + CTRLREG.bit.PRESCALER = PRESCALER_SETTING; + +#if defined(_SAML21_) || defined(_SAML22_) || defined(_SAMD51_) + CTRLREG.bit.COUNTSYNC = 1; +#endif + + RTC->MODE0.INTENSET.reg = RTC_MODE0_INTENSET_OVF; +} + +void rtc_enable(void) { + if (rtc_is_enabled()) return; + CTRLREG.bit.ENABLE = 1; + _rtc_sync(); +} + +void rtc_set_counter(rtc_counter_t counter) { + // // syncing before and after was found to increase reliability on Sensor Watch + _rtc_sync(); + RTC->MODE0.COUNT.reg = counter; + _rtc_sync(); +} + +rtc_counter_t rtc_get_counter(void) { + rtc_counter_t counter; + +#if defined(_SAML21_) || defined(_SAML22_) || defined(_SAMD51_) + CTRLREG.bit.COUNTSYNC = 1; +#endif + _rtc_sync(); + counter = RTC->MODE0.COUNT.reg; + + return counter; +} + +void rtc_enable_compare_interrupt(uint32_t compare_time) { + RTC->MODE0.COMP[0].reg = compare_time; + _rtc_sync(); + RTC->MODE0.INTENSET.reg = RTC_MODE0_INTENSET_CMP0; + // NVIC_ClearPendingIRQ(RTC_IRQn); + // NVIC_EnableIRQ(RTC_IRQn); +} + +void rtc_configure_callback(rtc_cb_t callback) { + _rtc_callback = callback; +} + +void rtc_disable_compare_interrupt(void){ + RTC->MODE0.INTENCLR.reg = RTC_MODE0_INTENCLR_CMP0; + // NVIC_ClearPendingIRQ(RTC_IRQn); + // NVIC_DisableIRQ(RTC_IRQn); +} + +void irq_handler_rtc(void); + +void irq_handler_rtc(void) { + uint16_t int_cause = (uint16_t)RTC->MODE0.INTFLAG.reg; + RTC->MODE0.INTFLAG.reg = RTC_MODE0_INTFLAG_MASK; + (void)RTC->MODE0.INTFLAG.reg; + + /* Invoke registered Callback function */ + if (_rtc_callback != NULL) { + _rtc_callback(int_cause); + } + + // NVIC_ClearPendingIRQ(RTC_IRQn); +} diff --git a/watch-library/hardware/watch/rtc32.h b/watch-library/hardware/watch/rtc32.h new file mode 100644 index 00000000..52f20d68 --- /dev/null +++ b/watch-library/hardware/watch/rtc32.h @@ -0,0 +1,98 @@ +////< @file rtc32.h +/* + * MIT License + * + * Copyright (c) 2020 Joey Castillo + * Copyright (c) 2025 Alessandro Genova + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +#pragma once + +#include +#include + +/** + * @addtogroup rtc Real-Time Clock + * @brief Functions for configuring and using the Real-Time Clock peripheral. + * @details This is the rtc implementation for MODE0 (counter32) + * @{ + */ + +#define RTC_REFERENCE_YEAR (2020) + +typedef union { + struct { + uint32_t second : 6; // 0-59 + uint32_t minute : 6; // 0-59 + uint32_t hour : 5; // 0-23 + uint32_t day : 5; // 1-31 + uint32_t month : 4; // 1-12 + uint32_t year : 6; // 0-63 (representing 2020-2083) + } unit; + uint32_t reg; // the bit-packed value as expected by the RTC peripheral's CLOCK register. +} rtc_date_time_t; + +typedef enum rtc_alarm_match_t { + ALARM_MATCH_DISABLED = 0, + ALARM_MATCH_SS, + ALARM_MATCH_MMSS, + ALARM_MATCH_HHMMSS, +} rtc_alarm_match_t; + +typedef uint32_t rtc_counter_t; + +typedef void (*rtc_cb_t)(uint16_t intflag); + +/** @brief Initializes the RTC. + * @details Configures the RTC for COUNT32 mode, with a 1 Hz + * tick derived from the 1024 Hz clock on GCLK3 (for SAM D devices) + * or OSC32KCTRL's most accurate 1024 Hz output (for SAM L devices). + */ +void rtc_init(void); + +/** @brief Enables the RTC. + */ +void rtc_enable(void); + +/** @brief Checks if the RTC is enabled. + * @return true if the RTC is enabled; false if not. + */ +bool rtc_is_enabled(void); + +/** @brief Set the value of the counter register. + */ +void rtc_set_counter(rtc_counter_t counter); + +/** @brief Returns the value of the counter register. + */ +rtc_counter_t rtc_get_counter(void); + +/** @brief Configures the RTC alarm callback. + * @param callback The function to call when an RTC interrupt occurs. The callback + * will be passed a bitmask of the interrupt flags, the full contents + * of the RTC peripheral's INTFLAG register. + */ +void rtc_configure_callback(rtc_cb_t callback); + +void rtc_enable_compare_interrupt(uint32_t compare_time); +void rtc_disable_compare_interrupt(void); + +/** @} */ diff --git a/watch-library/hardware/watch/watch_rtc.c b/watch-library/hardware/watch/watch_rtc.c index babae30d..61bc5d1f 100644 --- a/watch-library/hardware/watch/watch_rtc.c +++ b/watch-library/hardware/watch/watch_rtc.c @@ -2,6 +2,7 @@ * MIT License * * Copyright (c) 2020 Joey Castillo + * Copyright (c) 2025 Alessandro Genova * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -23,11 +24,32 @@ */ #include +#include #include "watch_rtc.h" #include "watch_private.h" +#include "watch_utility.h" + +static const uint32_t RTC_OSC_DIV = 10; +static const uint32_t RTC_OSC_HZ = 1 << RTC_OSC_DIV; // 2^10 = 1024 +static const uint32_t RTC_PRESCALER_DIV = 3; +static const uint32_t RTC_CNT_HZ = RTC_OSC_HZ >> RTC_PRESCALER_DIV; // 1024 / 2^3 = 128 +static const uint32_t RTC_CNT_DIV = RTC_OSC_DIV - RTC_PRESCALER_DIV; // 7 +static const uint32_t RTC_CNT_TICKS_PER_MINUTE = RTC_CNT_HZ * 60; +static const uint32_t RTC_CNT_TICKS_PER_HOUR = RTC_CNT_TICKS_PER_MINUTE * 60; + +static const int TB_BKUP_REG = 7; + +#define WATCH_RTC_N_COMP_CB 8 + +typedef struct { + volatile uint32_t counter; + volatile watch_cb_t callback; + volatile bool enabled; +} comp_cb_t; watch_cb_t tick_callbacks[8]; +comp_cb_t comp_callbacks[WATCH_RTC_N_COMP_CB]; watch_cb_t alarm_callback; watch_cb_t btn_alarm_callback; watch_cb_t a2_callback; @@ -46,14 +68,49 @@ void _watch_rtc_init(void) { #endif rtc_enable(); rtc_configure_callback(watch_rtc_callback); + + for (uint8_t index = 0; index < WATCH_RTC_N_COMP_CB; ++index) { + comp_callbacks[index].counter = 0; + comp_callbacks[index].callback = NULL; + comp_callbacks[index].enabled = false; + } + + NVIC_ClearPendingIRQ(RTC_IRQn); + NVIC_EnableIRQ(RTC_IRQn); } void watch_rtc_set_date_time(rtc_date_time_t date_time) { - rtc_set_date_time(date_time); + watch_rtc_set_unix_time(watch_utility_date_time_to_unix_time(date_time, 0)); } rtc_date_time_t watch_rtc_get_date_time(void) { - return rtc_get_date_time(); + return watch_utility_date_time_from_unix_time(watch_rtc_get_unix_time(), 0); +} + +void watch_rtc_set_unix_time(unix_timestamp_t unix_time) { + // time_backup + counter / RTC_CNT_HZ = unix_time + rtc_counter_t counter = rtc_get_counter(); + unix_timestamp_t tb = unix_time - (counter >> RTC_CNT_DIV); + watch_store_backup_data(tb, TB_BKUP_REG); +} + +unix_timestamp_t watch_rtc_get_unix_time(void) { + // time_backup + counter / RTC_CNT_HZ = unix_time + rtc_counter_t counter = rtc_get_counter(); + unix_timestamp_t tb = watch_get_backup_data(TB_BKUP_REG); + return tb + (counter >> RTC_CNT_DIV); +} + +rtc_counter_t watch_rtc_get_counter(void) { + return rtc_get_counter(); +} + +uint32_t watch_rtc_get_frequency(void) { + return RTC_CNT_HZ; +} + +uint32_t watch_rtc_get_ticks_per_minute(void) { + return RTC_CNT_TICKS_PER_MINUTE; } rtc_date_time_t watch_get_init_date_time(void) { @@ -103,57 +160,98 @@ void watch_rtc_register_periodic_callback(watch_cb_t callback, uint8_t frequency // this also maps nicely to an index for our list of tick callbacks. tick_callbacks[per_n] = callback; - NVIC_ClearPendingIRQ(RTC_IRQn); - NVIC_EnableIRQ(RTC_IRQn); - RTC->MODE2.INTENSET.reg = 1 << per_n; + // NVIC_ClearPendingIRQ(RTC_IRQn); + // NVIC_EnableIRQ(RTC_IRQn); + RTC->MODE0.INTENSET.reg = 1 << per_n; } void watch_rtc_disable_periodic_callback(uint8_t frequency) { if (__builtin_popcount(frequency) != 1) return; uint8_t per_n = __builtin_clz((frequency & 0xFF) << 24); - RTC->MODE2.INTENCLR.reg = 1 << per_n; + RTC->MODE0.INTENCLR.reg = 1 << per_n; } void watch_rtc_disable_matching_periodic_callbacks(uint8_t mask) { - RTC->MODE2.INTENCLR.reg = mask; + RTC->MODE0.INTENCLR.reg = mask; } void watch_rtc_disable_all_periodic_callbacks(void) { watch_rtc_disable_matching_periodic_callbacks(0xFF); } -void watch_rtc_register_alarm_callback(watch_cb_t callback, rtc_date_time_t alarm_time, rtc_alarm_match_t mask) { - RTC->MODE2.Mode2Alarm[0].ALARM.reg = alarm_time.reg; - RTC->MODE2.Mode2Alarm[0].MASK.reg = mask; - RTC->MODE2.INTENSET.reg = RTC_MODE2_INTENSET_ALARM0; - alarm_callback = callback; - NVIC_ClearPendingIRQ(RTC_IRQn); - NVIC_EnableIRQ(RTC_IRQn); - RTC->MODE2.INTENSET.reg = RTC_MODE2_INTENSET_ALARM0; +static void _watch_rtc_schedule_next_comp(void) { + rtc_disable_compare_interrupt(); + + // The soonest we can schedule is the next tick + rtc_counter_t curr_counter = watch_rtc_get_counter() + 1; + + bool schedule_any = false; + rtc_counter_t comp_counter; + rtc_counter_t min_diff = UINT_MAX; + + for (uint8_t index = 0; index < WATCH_RTC_N_COMP_CB; ++index) { + if (comp_callbacks[index].enabled) { + rtc_counter_t diff = comp_callbacks[index].counter - curr_counter; + if (diff <= min_diff) { + min_diff = diff; + comp_counter = comp_callbacks[index].counter; + schedule_any = true; + } + } + } + + if (schedule_any) { + rtc_enable_compare_interrupt(comp_counter); + } } -void watch_rtc_disable_alarm_callback(void) { - RTC->MODE2.INTENCLR.reg = RTC_MODE2_INTENCLR_ALARM0; +void watch_rtc_register_comp_callback(watch_cb_t callback, rtc_counter_t counter, uint8_t index) { + if (index >= WATCH_RTC_N_COMP_CB) { + return; + } + + rtc_disable_compare_interrupt(); + + comp_callbacks[index].counter = counter; + comp_callbacks[index].callback = callback; + comp_callbacks[index].enabled = true; + + _watch_rtc_schedule_next_comp(); } -void watch_rtc_callback(uint16_t interrupt_status) { - uint16_t interrupt_enabled = RTC->MODE2.INTENSET.reg; +void watch_rtc_disable_comp_callback(uint8_t index) { + if (index >= WATCH_RTC_N_COMP_CB) { + return; + } - if ((interrupt_status & interrupt_enabled) & RTC_MODE2_INTFLAG_PER_Msk) { + rtc_disable_compare_interrupt(); + + comp_callbacks[index].enabled = false; + + _watch_rtc_schedule_next_comp(); +} + +void watch_rtc_callback(uint16_t interrupt_cause) { + // First read all relevant registers, to ensure no changes occurr during the callbacks + uint16_t interrupt_enabled = RTC->MODE0.INTENSET.reg; + rtc_counter_t comp_counter = RTC->MODE0.COMP[0].reg; + + + if ((interrupt_cause & interrupt_enabled) & RTC_MODE0_INTFLAG_PER_Msk) { // handle the tick callback first, it's what we do the most. // start from PER7, the 1 Hz tick. for(int8_t i = 7; i >= 0; i--) { - if ((interrupt_status & interrupt_enabled) & (1 << i)) { + if ((interrupt_cause & interrupt_enabled) & (1 << i)) { if (tick_callbacks[i] != NULL) { tick_callbacks[i](); } - RTC->MODE2.INTFLAG.reg = 1 << i; -// break; Uncertain if this fix is requried. We were discussing in discord. Might slightly increase power consumption. } } - } else if ((interrupt_status & interrupt_enabled) & RTC_MODE2_INTFLAG_TAMPER) { + } + + if ((interrupt_cause & interrupt_enabled) & RTC_MODE0_INTFLAG_TAMPER) { // handle the extwake interrupts next. - uint8_t reason = RTC->MODE2.TAMPID.reg; + uint8_t reason = RTC->MODE0.TAMPID.reg; if (reason & RTC_TAMPID_TAMPID2) { if (btn_alarm_callback != NULL) btn_alarm_callback(); } else if (reason & RTC_TAMPID_TAMPID1) { @@ -161,25 +259,37 @@ void watch_rtc_callback(uint16_t interrupt_status) { } else if (reason & RTC_TAMPID_TAMPID0) { if (a4_callback != NULL) a4_callback(); } - RTC->MODE2.TAMPID.reg = reason; - RTC->MODE2.INTFLAG.reg = RTC_MODE2_INTFLAG_TAMPER; - } else if ((interrupt_status & interrupt_enabled) & RTC_MODE2_INTFLAG_ALARM0) { - // finally handle the alarm. - if (alarm_callback != NULL) { - alarm_callback(); + RTC->MODE0.TAMPID.reg = reason; + } + + if ((interrupt_cause & interrupt_enabled) & RTC_MODE0_INTFLAG_CMP0) { + // The comp interrupt is generated one tick after the matched counter + // rtc_counter_t comp_counter = watch_rtc_get_counter() - 1; + + for (uint8_t index = 0; index < WATCH_RTC_N_COMP_CB; ++index) { + if (comp_callbacks[index].enabled && comp_counter == comp_callbacks[index].counter) { + comp_callbacks[index].enabled = false; + comp_callbacks[index].callback(); + } } - RTC->MODE2.INTFLAG.reg = RTC_MODE2_INTFLAG_ALARM0; + _watch_rtc_schedule_next_comp(); + } + + if ((interrupt_cause & interrupt_enabled) & RTC_MODE0_INTFLAG_OVF) { + // Handle the overflow of the counter. All we need to do is reset the reference time. + unix_timestamp_t tb = watch_get_backup_data(TB_BKUP_REG); + watch_store_backup_data(tb + (UINT_MAX >> RTC_CNT_DIV), TB_BKUP_REG); } } void watch_rtc_enable(bool en) { // Writing it twice - as it's quite dangerous operation. // If write fails - we might hang with RTC off, which means no recovery possible - while (RTC->MODE2.SYNCBUSY.reg); - RTC->MODE2.CTRLA.bit.ENABLE = en ? 1 : 0; - while (RTC->MODE2.SYNCBUSY.reg); - RTC->MODE2.CTRLA.bit.ENABLE = en ? 1 : 0; - while (RTC->MODE2.SYNCBUSY.reg); + while (RTC->MODE0.SYNCBUSY.reg); + RTC->MODE0.CTRLA.bit.ENABLE = en ? 1 : 0; + while (RTC->MODE0.SYNCBUSY.reg); + RTC->MODE0.CTRLA.bit.ENABLE = en ? 1 : 0; + while (RTC->MODE0.SYNCBUSY.reg); } void watch_rtc_freqcorr_write(int16_t value, int16_t sign) { @@ -188,8 +298,7 @@ void watch_rtc_freqcorr_write(int16_t value, int16_t sign) { data.bit.VALUE = value; data.bit.SIGN = sign; - RTC->MODE2.FREQCORR.reg = data.reg; // Setting correction in single write operation + RTC->MODE0.FREQCORR.reg = data.reg; // Setting correction in single write operation // We do not sycnronize. We are not in a hurry } - diff --git a/watch-library/hardware/watch/watch_tcc.c b/watch-library/hardware/watch/watch_tcc.c index b5d50f94..06cee5d5 100644 --- a/watch-library/hardware/watch/watch_tcc.c +++ b/watch-library/hardware/watch/watch_tcc.c @@ -32,9 +32,13 @@ void cb_watch_buzzer_seq(void); static uint16_t _seq_position; static int8_t _tone_ticks, _repeat_counter; -static bool _callback_running = false; static int8_t *_sequence; +static uint8_t _volume; static void (*_cb_finished)(void); +static watch_cb_t _cb_start_global = NULL; +static watch_cb_t _cb_stop_global = NULL; +static volatile bool _led_is_active = false; +static volatile bool _buzzer_is_active = false; static void _tcc_write_RUNSTDBY(bool value) { // enables or disables RUNSTDBY of the tcc @@ -46,13 +50,11 @@ static void _tcc_write_RUNSTDBY(bool value) { static inline void _tc0_start() { // start the TC0 timer tc_enable(0); - _callback_running = true; } static inline void _tc0_stop() { // stop the TC0 timer tc_disable(0); - _callback_running = false; } static void _tc0_initialize() { @@ -68,19 +70,32 @@ static void _tc0_initialize() { } void watch_buzzer_play_sequence(int8_t *note_sequence, void (*callback_on_end)(void)) { - if (_callback_running) _tc0_stop(); + watch_buzzer_play_sequence_with_volume(note_sequence, callback_on_end, WATCH_BUZZER_VOLUME_LOUD); +} + +void watch_buzzer_play_sequence_with_volume(int8_t *note_sequence, void (*callback_on_end)(void), watch_buzzer_volume_t volume) { + // Abort any previous sequence + watch_buzzer_abort_sequence(); + + _buzzer_is_active = true; + + if (_cb_start_global) { + _cb_start_global(); + } + + watch_enable_buzzer_and_leds(); + watch_set_buzzer_off(); _sequence = note_sequence; _cb_finished = callback_on_end; + _volume = volume == WATCH_BUZZER_VOLUME_SOFT ? 5 : 25; _seq_position = 0; _tone_ticks = 0; _repeat_counter = -1; // prepare buzzer - watch_enable_buzzer(); + // setup TC0 timer _tc0_initialize(); - // TCC should run in standby mode - _tcc_write_RUNSTDBY(true); // start the timer (for the 64 hz callback) _tc0_start(); } @@ -110,7 +125,7 @@ void cb_watch_buzzer_seq(void) { // read note watch_buzzer_note_t note = _sequence[_seq_position]; if (note != BUZZER_NOTE_REST) { - watch_set_buzzer_period_and_duty_cycle(NotePeriods[note], 25); + watch_set_buzzer_period_and_duty_cycle(NotePeriods[note], _volume); watch_set_buzzer_on(); } else watch_set_buzzer_off(); // set duration ticks and move to next tone @@ -119,17 +134,37 @@ void cb_watch_buzzer_seq(void) { } else { // end the sequence watch_buzzer_abort_sequence(); - if (_cb_finished) _cb_finished(); } } else _tone_ticks--; } void watch_buzzer_abort_sequence(void) { // ends/aborts the sequence - if (_callback_running) _tc0_stop(); + if (!_buzzer_is_active) { + return; + } + + _buzzer_is_active = false; + + _tc0_stop(); + watch_set_buzzer_off(); - // disable standby mode for TCC - _tcc_write_RUNSTDBY(false); + + // disable TCC + watch_maybe_disable_buzzer_and_leds(); + + if (_cb_stop_global) { + _cb_stop_global(); + } + + if (_cb_finished) { + _cb_finished(); + } +} + +void watch_buzzer_register_global_callbacks(watch_cb_t cb_start, watch_cb_t cb_stop) { + _cb_stop_global = cb_start; + _cb_stop_global = cb_stop; } void irq_handler_tc0(void) { @@ -142,19 +177,41 @@ bool watch_is_buzzer_or_led_enabled(void){ return tcc_is_enabled(0); } -inline void watch_enable_buzzer(void) { +void watch_enable_buzzer_and_leds(void) { if (!tcc_is_enabled(0)) { + // tcc_set_run_in_standby(0, true); _watch_enable_tcc(); + // TCC should run in standby mode + _tcc_write_RUNSTDBY(true); } } +void watch_disable_buzzer_and_leds(void) { + if (tcc_is_enabled(0)) { + _tcc_write_RUNSTDBY(false); + _watch_disable_tcc(); + } +} + +void watch_maybe_disable_buzzer_and_leds(void) { + if (_buzzer_is_active || _led_is_active) { + return; + } + + watch_disable_buzzer_and_leds(); +} + +void watch_enable_buzzer(void) { + watch_enable_buzzer_and_leds(); +} + void watch_set_buzzer_period_and_duty_cycle(uint32_t period, uint8_t duty) { tcc_set_period(0, period, true); tcc_set_cc(0, (WATCH_BUZZER_TCC_CHANNEL) % 4, period / (100 / duty), true); } void watch_disable_buzzer(void) { - _watch_disable_tcc(); + watch_maybe_disable_buzzer_and_leds(); } inline void watch_set_buzzer_on(void) { @@ -172,14 +229,17 @@ void watch_buzzer_play_note(watch_buzzer_note_t note, uint16_t duration_ms) { } void watch_buzzer_play_note_with_volume(watch_buzzer_note_t note, uint16_t duration_ms, watch_buzzer_volume_t volume) { - if (note == BUZZER_NOTE_REST) { - watch_set_buzzer_off(); - } else { - watch_set_buzzer_period_and_duty_cycle(NotePeriods[note], volume == WATCH_BUZZER_VOLUME_SOFT ? 5 : 25); - watch_set_buzzer_on(); - } - delay_ms(duration_ms); - watch_set_buzzer_off(); + static int8_t single_note_sequence[3]; + + single_note_sequence[0] = note; + // 48 ticks per second for the tc0? + // Each tick is approximately 20ms + uint16_t duration = duration_ms / 20; + if (duration > 127) duration = 127; + single_note_sequence[1] = (int8_t)duration; + single_note_sequence[2] = 0; + + watch_buzzer_play_sequence_with_volume(single_note_sequence, NULL, volume); } void _watch_enable_tcc(void) { @@ -263,7 +323,7 @@ void watch_enable_leds(void) { } void watch_disable_leds(void) { - _watch_disable_tcc(); + watch_maybe_disable_buzzer_and_leds(); } void watch_set_led_color(uint8_t red, uint8_t green) { @@ -275,6 +335,15 @@ void watch_set_led_color(uint8_t red, uint8_t green) { } void watch_set_led_color_rgb(uint8_t red, uint8_t green, uint8_t blue) { + bool turning_on = (red | green | blue) != 0; + + if (turning_on) { + _led_is_active = true; + watch_enable_buzzer_and_leds(); + } else { + _led_is_active = false; + } + if (tcc_is_enabled(0)) { uint32_t period = tcc_get_period(0); tcc_set_cc(0, (WATCH_RED_TCC_CHANNEL) % 4, ((period * (uint32_t)red * 1000ull) / 255000ull), true); @@ -289,6 +358,10 @@ void watch_set_led_color_rgb(uint8_t red, uint8_t green, uint8_t blue) { (void) blue; // silence warning #endif } + + if (!turning_on) { + watch_maybe_disable_buzzer_and_leds(); + } } void watch_set_led_red(void) { diff --git a/watch-library/shared/watch/watch_rtc.h b/watch-library/shared/watch/watch_rtc.h index a51fc826..e268ad28 100644 --- a/watch-library/shared/watch/watch_rtc.h +++ b/watch-library/shared/watch/watch_rtc.h @@ -27,7 +27,7 @@ ////< @file watch_rtc.h #include "watch.h" -#include "rtc.h" +#include "rtc32.h" /** @addtogroup rtc Real-Time Clock * @brief This section covers functions related to the SAM L22's real-time clock peripheral, including @@ -42,17 +42,20 @@ extern watch_cb_t btn_alarm_callback; extern watch_cb_t a2_callback; extern watch_cb_t a4_callback; +extern watch_cb_t comp_callback; #define WATCH_RTC_REFERENCE_YEAR (2020) #define watch_date_time_t rtc_date_time_t +typedef rtc_counter_t watch_counter_t; +typedef uint32_t unix_timestamp_t; /** @brief Called by main.c to check if the RTC is enabled. * You may call this function, but outside of app_init, it should always return true. */ bool _watch_rtc_is_enabled(void); -/** @brief Sets the date and time. +/** @brief Sets the date and time. Calls watch_rtc_set_unix_time internally. * @param date_time The date and time you wish to set, with a year value from 0-63 representing 2020-2083. * @note The SAM L22 stores the year as six bits representing a value from 0 to 63. It treats this as a year * offset from a reference year, which must be a leap year. Since 2020 was a leap year, and it allows @@ -62,7 +65,7 @@ bool _watch_rtc_is_enabled(void); */ void watch_rtc_set_date_time(rtc_date_time_t date_time); -/** @brief Returns the date and time. +/** @brief Returns the date and time. Calls watch_rtc_get_unix_time internally. * @return A rtc_date_time_t with the current date and time, with a year value from 0-63 representing 2020-2083. * @see watch_rtc_set_date_time for notes about how the year is stored. */ @@ -73,26 +76,51 @@ rtc_date_time_t watch_rtc_get_date_time(void); */ rtc_date_time_t watch_get_init_date_time(void); -/** @brief Registers an alarm callback that will be called when the RTC time matches the target time, as masked - * by the provided mask. - * @param callback The function you wish to have called when the alarm fires. If this value is NULL, the alarm +/** @brief Set the current UTC date and time using a unix timestamp + */ +void watch_rtc_set_unix_time(unix_timestamp_t unix_time); + +/** @brief Get the current UTC date and time using a unix timestamp + */ +unix_timestamp_t watch_rtc_get_unix_time(void); + +/** @brief Get the current value of the internal hardware counter + * @details The counter starts at 0 and it increases at a 128Hz rate until it overflows and starts over. + * We never manually set the counter. Doing so allows us to calculate absolute elapsed and more. + * When the user sets the time, what is modified is the reference time (i.e. the date and time when + * the counter is 0). + */ +rtc_counter_t watch_rtc_get_counter(void); + +/** @brief Get the RTC counter frequency. + */ +uint32_t watch_rtc_get_frequency(void); + +/** @brief Get how many counter ticks are in one minute. + */ +uint32_t watch_rtc_get_ticks_per_minute(void); + +/** @brief Registers a callback that will be called when the RTC counter matches the target counter. + * @param callback The function you wish to have called when the target counter is reached. If this value is NULL, the comp * interrupt will still be enabled, but no callback function will be called. - * @param alarm_time The time that you wish to match. The date is currently ignored. - * @param mask One of the values in rtc_alarm_match_t indicating which values to check. - * @details The alarm interrupt is a versatile tool for scheduling events in the future, especially since it can - * wake the device from all sleep modes. The key to its versatility is the mask parameter. - * Suppose we set an alarm for midnight, 00:00:00. - * * if mask is ALARM_MATCH_SS, the alarm will fire every minute when the clock ticks to seconds == 0. - * * with ALARM_MATCH_MMSS, the alarm will once an hour, at the top of each hour. - * * with ALARM_MATCH_HHMMSS, the alarm will fire at midnight every day. - * In theory the SAM L22's alarm function can match on days, months and even years, but I have not had - * success with this yet; as such, I am omitting these options for now. + * @param counter The time that you wish to match. The date is currently ignored. + * @param index We can have up to 8 active callbacks at a time. This parameter specifies which of the 8 callbacks should be set. + * @details The hardware RTC provides us with single interrupt that fires when the RTC counter matches a target counter COMP0. + * With a little bit of logic, we can provide multiple active compare callbacks. Every time a comp callback is + * registered/disabled/fired we iterate over all the active comp callbacks and set the hardware COMP0 counter + * to the next occurring one. + * With this very simple API, movement can implement one-shot timers to turn off the led and determine button longpresses + * as well as the inactivity timeouts for resigning and sleeping, as well as emulating the top of the minute alarm. */ -void watch_rtc_register_alarm_callback(watch_cb_t callback, rtc_date_time_t alarm_time, rtc_alarm_match_t mask); +void watch_rtc_register_comp_callback(watch_cb_t callback, rtc_counter_t counter, uint8_t index); + +/** @brief Disables the specified comp callback. + */ +void watch_rtc_disable_comp_callback(uint8_t index); /** @brief Disables the alarm callback. */ -void watch_rtc_disable_alarm_callback(void); +// void watch_rtc_disable_alarm_callback(void); /** @brief Registers a "tick" callback that will be called once per second. * @param callback The function you wish to have called when the clock ticks. If you pass in NULL, the tick @@ -117,10 +145,6 @@ void watch_rtc_disable_tick_callback(void); * tick at 16 or 32 Hz to update the screen more quickly. Just remember that the more frequent the tick, the more * power your app will consume. Ideally you should enable the fast tick only when the user requires it (i.e. in * response to an input event), and move back to the slow tick after some time. - * - * Also note that the RTC peripheral does not have sub-second resolution, so even if you set a 2 or 4 Hz interval, - * the system will not have any way of telling you where you are within a given second; watch_rtc_get_date_time - * will return the exact same timestamp until the second ticks over. */ void watch_rtc_register_periodic_callback(watch_cb_t callback, uint8_t frequency); diff --git a/watch-library/shared/watch/watch_tcc.h b/watch-library/shared/watch/watch_tcc.h index aff88f7f..99c56dcc 100644 --- a/watch-library/shared/watch/watch_tcc.h +++ b/watch-library/shared/watch/watch_tcc.h @@ -170,8 +170,6 @@ void watch_set_buzzer_off(void); /** @brief Plays the given note for a set duration at the loudest possible volume. * @param note The note you wish to play, or BUZZER_NOTE_REST to disable output for the given duration. * @param duration_ms The duration of the note. - * @note Note that this will block your UI for the duration of the note's play time, and it will - * after this call, the buzzer period will be set to the period of this note. */ void watch_buzzer_play_note(watch_buzzer_note_t note, uint16_t duration_ms); @@ -179,8 +177,6 @@ void watch_buzzer_play_note(watch_buzzer_note_t note, uint16_t duration_ms); * @param note The note you wish to play, or BUZZER_NOTE_REST to disable output for the given duration. * @param duration_ms The duration of the note. * @param volume either WATCH_BUZZER_VOLUME_SOFT or WATCH_BUZZER_VOLUME_LOUD - * @note This will block your UI for the duration of the note's play time, and after this call, the - * buzzer will stop sounding, but the TCC period will remain set to the period of this note. */ void watch_buzzer_play_note_with_volume(watch_buzzer_note_t note, uint16_t duration_ms, watch_buzzer_volume_t volume); @@ -202,10 +198,36 @@ extern const uint16_t NotePeriods[108]; */ void watch_buzzer_play_sequence(int8_t *note_sequence, void (*callback_on_end)(void)); +/** @brief Plays the given sequence of notes in a non-blocking way. + * @param note_sequence A pointer to the sequence of buzzer note & duration tuples, ending with a zero. A simple + * RLE logic is implemented: a negative number instead of a buzzer note means that the sequence + * is rewound by the given number of notes. The byte following a negative number determines the number + * of loops. I.e. if you want to repeat the last three notes of the sequence one time, you should provide + * the tuple -3, 1. The repeated notes must not contain any other repeat markers, or you will end up with + * an eternal loop. + * @param callback_on_end A pointer to a callback function to be invoked when the sequence has finished playing. + * @param volume either WATCH_BUZZER_VOLUME_SOFT or WATCH_BUZZER_VOLUME_LOUD + */ +void watch_buzzer_play_sequence_with_volume(int8_t *note_sequence, void (*callback_on_end)(void), watch_buzzer_volume_t volume); + /** @brief Aborts a playing sequence. */ void watch_buzzer_abort_sequence(void); +void watch_buzzer_register_global_callbacks(watch_cb_t cb_start, watch_cb_t cb_stop); + +/** @brief Enables the TCC peripheral, which drives the buzzer and the leds. +*/ +void watch_enable_buzzer_and_leds(void); + +/** @brief Disables the TCC peripheral that drives the buzzer and the leds. + */ +void watch_disable_buzzer_and_leds(void); + +/** @brief Disables the TCC peripheral that drives the buzzer and the leds if neither is currently active + */ +void watch_maybe_disable_buzzer_and_leds(void); + #ifndef __EMSCRIPTEN__ void irq_handler_tc0(void); #endif diff --git a/watch-library/shared/watch/watch_utility.c b/watch-library/shared/watch/watch_utility.c index 7179aa72..d592fe56 100644 --- a/watch-library/shared/watch/watch_utility.c +++ b/watch-library/shared/watch/watch_utility.c @@ -278,6 +278,10 @@ watch_date_time_t watch_utility_date_time_convert_zone(watch_date_time_t date_ti return watch_utility_date_time_from_unix_time(timestamp, destination_utc_offset); } +uint32_t watch_utility_unix_time_convert_zone(uint32_t timestamp, uint32_t origin_utc_offset, uint32_t destination_utc_offset) { + return timestamp - origin_utc_offset + destination_utc_offset; +} + watch_duration_t watch_utility_seconds_to_duration(uint32_t seconds) { watch_duration_t retval; diff --git a/watch-library/shared/watch/watch_utility.h b/watch-library/shared/watch/watch_utility.h index 61c292b4..9b1b3a04 100644 --- a/watch-library/shared/watch/watch_utility.h +++ b/watch-library/shared/watch/watch_utility.h @@ -144,6 +144,16 @@ bool watch_utility_convert_to_12_hour(watch_date_time_t *date_time); */ watch_date_time_t watch_utility_date_time_convert_zone(watch_date_time_t date_time, uint32_t origin_utc_offset, uint32_t destination_utc_offset); +/** @brief Converts a unix time from a given time zone to another time zone. + * @param timestamp The unix time that you wish to convert + * @param origin_utc_offset The number of seconds from UTC in the origin time zone + * @param destination_utc_offset The number of seconds from UTC in the destination time zone + * @return A unix time for the given UNIX timestamp and UTC offset. + * @note Adapted from MIT-licensed code from musl, Copyright © 2005-2014 Rich Felker, et al.: + * https://github.com/esmil/musl/blob/1cc81f5cb0df2b66a795ff0c26d7bbc4d16e13c6/src/time/__secs_to_tm.c + */ +uint32_t watch_utility_unix_time_convert_zone(uint32_t timestamp, uint32_t origin_utc_offset, uint32_t destination_utc_offset); + /** @brief Returns a temperature in degrees Celsius for a given thermistor voltage divider circuit. * @param value The raw analog reading from the thermistor pin (0-65535) * @param highside True if the thermistor is connected to VCC and the series resistor is connected diff --git a/watch-library/simulator/watch/watch_rtc.c b/watch-library/simulator/watch/watch_rtc.c index 40487dc6..77463f74 100644 --- a/watch-library/simulator/watch/watch_rtc.c +++ b/watch-library/simulator/watch/watch_rtc.c @@ -21,6 +21,7 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ +#include #include "watch_rtc.h" #include "watch_main_loop.h" @@ -29,8 +30,28 @@ #include #include +static const uint32_t RTC_CNT_HZ = 128; +static const uint32_t RTC_CNT_DIV = 7; +static const uint32_t RTC_CNT_TICKS_PER_MINUTE = RTC_CNT_HZ * 60; +static const uint32_t RTC_CNT_TICKS_PER_HOUR = RTC_CNT_TICKS_PER_MINUTE * 60; + +static uint32_t counter_interval; +static uint32_t counter; +static uint32_t reference_timestamp; + +#define WATCH_RTC_N_COMP_CB 8 + +typedef struct { + volatile uint32_t counter; + volatile watch_cb_t callback; + volatile bool enabled; +} comp_cb_t; + static double time_offset = 0; -static long tick_callbacks[8] = { -1, -1, -1, -1, -1, -1, -1, -1 }; +watch_cb_t tick_callbacks[8]; +comp_cb_t comp_callbacks[WATCH_RTC_N_COMP_CB]; + +static uint32_t scheduled_comp_counter; static long alarm_interval_id = -1; static long alarm_timeout_id = -1; @@ -40,41 +61,73 @@ watch_cb_t btn_alarm_callback; watch_cb_t a2_callback; watch_cb_t a4_callback; +static void _watch_increase_counter(void *userData); +static void _watch_process_periodic_callbacks(void); +static void _watch_process_comp_callbacks(void); + bool _watch_rtc_is_enabled(void) { - return true; + return counter_interval; } void _watch_rtc_init(void) { -#if EMSCRIPTEN - // Shifts the timezone so our local time is converted to UTC and set + for (uint8_t index = 0; index < 8; ++index) { + tick_callbacks[index] = NULL; + } + + for (uint8_t index = 0; index < WATCH_RTC_N_COMP_CB; ++index) { + comp_callbacks[index].counter = 0; + comp_callbacks[index].callback = NULL; + comp_callbacks[index].enabled = false; + } + + scheduled_comp_counter = 0; + counter = 0; + counter_interval = 0; + + watch_rtc_set_date_time(watch_get_init_date_time()); + watch_rtc_enable(true); +} + +void watch_rtc_set_date_time(rtc_date_time_t date_time) { + watch_rtc_set_unix_time(watch_utility_date_time_to_unix_time(date_time, 0)); +} + +rtc_date_time_t watch_rtc_get_date_time(void) { + return watch_utility_date_time_from_unix_time(watch_rtc_get_unix_time(), 0); +} + +void watch_rtc_set_unix_time(unix_timestamp_t unix_time) { + // time_backup + counter / RTC_CNT_HZ = unix_time + rtc_counter_t counter = watch_rtc_get_counter(); + reference_timestamp = unix_time - (counter >> RTC_CNT_DIV); +} + +unix_timestamp_t watch_rtc_get_unix_time(void) { + // time_backup + counter / RTC_CNT_HZ = unix_time + rtc_counter_t counter = watch_rtc_get_counter(); + return reference_timestamp + (counter >> RTC_CNT_DIV); +} + +rtc_counter_t watch_rtc_get_counter(void) { + return counter; +} + +uint32_t watch_rtc_get_frequency(void) { + return RTC_CNT_HZ; +} + +uint32_t watch_rtc_get_ticks_per_minute(void) { + return RTC_CNT_TICKS_PER_MINUTE; +} + +rtc_date_time_t watch_get_init_date_time(void) { + rtc_date_time_t date_time = {0}; + int32_t time_zone_offset = EM_ASM_INT({ - return -new Date().getTimezoneOffset() * 60; + return new Date().getTimezoneOffset() * 60 * 1000; // ms }); -#endif -#ifdef BUILD_YEAR - watch_date_time_t date_time = watch_get_init_date_time(); -#else - watch_date_time_t date_time = watch_rtc_get_date_time(); -#endif - watch_rtc_set_date_time(watch_utility_date_time_convert_zone(date_time, time_zone_offset, 0)); -} -void watch_rtc_set_date_time(watch_date_time_t date_time) { - time_offset = EM_ASM_DOUBLE({ - const year = 2020 + (($0 >> 26) & 0x3f); - const month = ($0 >> 22) & 0xf; - const day = ($0 >> 17) & 0x1f; - const hour = ($0 >> 12) & 0x1f; - const minute = ($0 >> 6) & 0x3f; - const second = $0 & 0x3f; - const date = new Date(year, month - 1, day, hour, minute, second); - return date - Date.now(); - }, date_time.reg); -} - -watch_date_time_t watch_rtc_get_date_time(void) { - watch_date_time_t retval; - retval.reg = EM_ASM_INT({ + date_time.reg = EM_ASM_INT({ const date = new Date(Date.now() + $0); return date.getSeconds() | (date.getMinutes() << 6) | @@ -82,27 +135,16 @@ watch_date_time_t watch_rtc_get_date_time(void) { (date.getDate() << 17) | ((date.getMonth() + 1) << 22) | ((date.getFullYear() - 2020) << 26); - }, time_offset); - return retval; -} - -rtc_date_time_t watch_get_init_date_time(void) { - rtc_date_time_t date_time = {0}; + }, time_zone_offset); #ifdef BUILD_YEAR date_time.unit.year = BUILD_YEAR; -#else - date_time.unit.year = 5; #endif #ifdef BUILD_MONTH date_time.unit.month = BUILD_MONTH; -#else - date_time.unit.month = 1; #endif #ifdef BUILD_DAY date_time.unit.day = BUILD_DAY; -#else - date_time.unit.day = 1; #endif #ifdef BUILD_HOUR date_time.unit.hour = BUILD_HOUR; @@ -122,12 +164,72 @@ void watch_rtc_disable_tick_callback(void) { watch_rtc_disable_periodic_callback(1); } -static void watch_invoke_periodic_callback(void *userData) { - watch_cb_t callback = userData; - callback(); +static void _watch_increase_counter(void *userData) { + (void) userData; + + counter += 1; + // Fire the periodic callbacks that match this counter + _watch_process_periodic_callbacks(); + // Fire the comp callbacks that match this counter + _watch_process_comp_callbacks(); + resume_main_loop(); } +static void _watch_process_periodic_callbacks(void) { + /* It looks weird but it follows the way the hardware triggers periodic interrupts. + * For 128hz counter periodic interrupts fire at these tick values: + * 1Hz: 64 + * 2Hz: 32, 96 + * 4Hz: 16, 48, 80, 112 + * 8Hz: 8, 24, 40, 56, 72, 88, 104, 120 + * 16Hz: 4, 12, 20, ..., 124 + * 32Hz: 2, 6, 10, ..., 126 + * 64Hz: 1, 3, 5, ..., 127 + * 128Hz: 0, 1, 2, ..., 127 + * + * Which means that only one periodic interrupt can fire for a given counter value + * (except 128Hz which can always fire) + */ + + uint32_t freq = watch_rtc_get_frequency(); + uint32_t subsecond_mask = freq - 1; + uint32_t subseconds = counter & subsecond_mask; + + // Find the firs non-zero bit in the counter, which can be used to determine the appropriate period (see table above). + uint8_t per_n = 0; + + for (uint8_t i = 0; i < 7; i++) { + if (subseconds & (1 << i)) { + per_n = i + 1; + break; + } + } + + if (tick_callbacks[per_n]) { + tick_callbacks[per_n](); + } + + // 128Hz is always a match + if (per_n != 0 && tick_callbacks[0]) { + tick_callbacks[0](); + } +} + +static void _watch_process_comp_callbacks(void) { + // In hardware the interrupt fires one tick after the matching counter + if (counter == (scheduled_comp_counter + 1)) { + for (uint8_t index = 0; index < WATCH_RTC_N_COMP_CB; ++index) { + if (comp_callbacks[index].enabled && scheduled_comp_counter == comp_callbacks[index].counter) { + comp_callbacks[index].enabled = false; + comp_callbacks[index].callback(); + } + } + + _watch_rtc_schedule_next_comp(); + } +} + void watch_rtc_register_periodic_callback(watch_cb_t callback, uint8_t frequency) { // we told them, it has to be a power of 2. if (__builtin_popcount(frequency) != 1) return; @@ -138,26 +240,19 @@ void watch_rtc_register_periodic_callback(watch_cb_t callback, uint8_t frequency // 0x01 (1 Hz) will have 7 leading zeros for PER7. 0xF0 (128 Hz) will have no leading zeroes for PER0. uint8_t per_n = __builtin_clz(tmp); - double interval = 1000.0 / frequency; // in msec - - if (tick_callbacks[per_n] != -1) emscripten_clear_interval(tick_callbacks[per_n]); - tick_callbacks[per_n] = emscripten_set_interval(watch_invoke_periodic_callback, interval, (void *)callback); + tick_callbacks[per_n] = callback; } void watch_rtc_disable_periodic_callback(uint8_t frequency) { if (__builtin_popcount(frequency) != 1) return; uint8_t per_n = __builtin_clz((frequency & 0xFF) << 24); - if (tick_callbacks[per_n] != -1) { - emscripten_clear_interval(tick_callbacks[per_n]); - tick_callbacks[per_n] = -1; - } + tick_callbacks[per_n] = NULL; } void watch_rtc_disable_matching_periodic_callbacks(uint8_t mask) { for (int i = 0; i < 8; i++) { - if (tick_callbacks[i] != -1 && (mask & (1 << i)) != 0) { - emscripten_clear_interval(tick_callbacks[i]); - tick_callbacks[i] = -1; + if (tick_callbacks[i] && (mask & (1 << i)) != 0) { + tick_callbacks[i] = NULL; } } } @@ -166,81 +261,70 @@ void watch_rtc_disable_all_periodic_callbacks(void) { watch_rtc_disable_matching_periodic_callbacks(0xFF); } -static void watch_invoke_alarm_interval_callback(void *userData) { - if (alarm_callback) alarm_callback(); -} - -static void watch_invoke_alarm_callback(void *userData) { - if (alarm_callback) alarm_callback(); - alarm_interval_id = emscripten_set_interval(watch_invoke_alarm_interval_callback, alarm_interval, NULL); -} - -void watch_rtc_register_alarm_callback(watch_cb_t callback, watch_date_time_t alarm_time, rtc_alarm_match_t mask) { - watch_rtc_disable_alarm_callback(); - - switch (mask) { - case ALARM_MATCH_DISABLED: - return; - case ALARM_MATCH_SS: - alarm_interval = 60 * 1000; - break; - case ALARM_MATCH_MMSS: - alarm_interval = 60 * 60 * 1000; - break; - case ALARM_MATCH_HHMMSS: - alarm_interval = 60 * 60 * 60 * 1000; - break; +void watch_rtc_register_comp_callback(watch_cb_t callback, rtc_counter_t counter, uint8_t index) { + if (index >= WATCH_RTC_N_COMP_CB) { + return; } - double timeout = EM_ASM_DOUBLE({ - const now = Date.now(); - const date = new Date(now); + comp_callbacks[index].counter = counter; + comp_callbacks[index].callback = callback; + comp_callbacks[index].enabled = true; - const hour = ($0 >> 12) & 0x1f; - const minute = ($0 >> 6) & 0x3f; - const second = $0 & 0x3f; + _watch_rtc_schedule_next_comp(); +} - if ($1 == 1) { // SS - if (second < date.getSeconds()) date.setMinutes(date.getMinutes() + 1); - date.setSeconds(second); - } else if ($1 == 2) { // MMSS - if (second < date.getSeconds()) date.setMinutes(date.getMinutes() + 1); - if (minute < date.getMinutes()) date.setHours(date.getHours() + 1); - date.setMinutes(minute, second); - } else if ($1 == 3) { // HHMMSS - if (second < date.getSeconds()) date.setMinutes(date.getMinutes() + 1); - if (minute < date.getMinutes()) date.setHours(date.getHours() + 1); - if (hour < date.getHours()) date.setDate(date.getDate() + 1); - date.setHours(hour, minute, second); - } else { - throw 'Invalid alarm match mask'; +void watch_rtc_disable_comp_callback(uint8_t index) { + if (index >= WATCH_RTC_N_COMP_CB) { + return; + } + + comp_callbacks[index].enabled = false; + + _watch_rtc_schedule_next_comp(); +} + +void _watch_rtc_schedule_next_comp(void) { + scheduled_comp_counter = 0; + + // The soonest we can schedule is the next tick + rtc_counter_t curr_counter = watch_rtc_get_counter() + 1; + + bool schedule_any = false; + rtc_counter_t comp_counter; + rtc_counter_t min_diff = UINT_MAX; + + for (uint8_t index = 0; index < WATCH_RTC_N_COMP_CB; ++index) { + // rtc_counter_t diff = + if (comp_callbacks[index].enabled) { + rtc_counter_t diff = comp_callbacks[index].counter - curr_counter; + if (diff <= min_diff) { + min_diff = diff; + comp_counter = comp_callbacks[index].counter; + schedule_any = true; + } } - - return date - now; - }, alarm_time.reg, mask); - - alarm_callback = callback; - alarm_timeout_id = emscripten_set_timeout(watch_invoke_alarm_callback, timeout, NULL); -} - -void watch_rtc_disable_alarm_callback(void) { - alarm_callback = NULL; - alarm_interval = 0; - - if (alarm_timeout_id != -1) { - emscripten_clear_timeout(alarm_timeout_id); - alarm_timeout_id = -1; } - if (alarm_interval_id != -1) { - emscripten_clear_interval(alarm_interval_id); - alarm_interval_id = -1; + if (schedule_any) { + scheduled_comp_counter = comp_counter; } } void watch_rtc_enable(bool en) { - //Not simulated + // Nothing to do cases + if ((en && counter_interval) || (!en && !counter_interval)) { + return; + } + + if (en) { + // Very bad way to keep time, but okay way to emulates the hardware. + double ms = 1000.0 / (double)RTC_CNT_HZ; // in msec + counter_interval = emscripten_set_interval(_watch_increase_counter, ms, NULL); + } else { + emscripten_clear_interval(counter_interval); + counter_interval = 0; + } } void watch_rtc_freqcorr_write(int16_t value, int16_t sign) diff --git a/watch-library/simulator/watch/watch_tcc.c b/watch-library/simulator/watch/watch_tcc.c index 5466eebb..5e5dd013 100644 --- a/watch-library/simulator/watch/watch_tcc.c +++ b/watch-library/simulator/watch/watch_tcc.c @@ -28,16 +28,20 @@ #include #include -static bool buzzer_enabled = false; +static volatile bool buzzer_enabled = false; static uint32_t buzzer_period; void cb_watch_buzzer_seq(void *userData); static uint16_t _seq_position; static int8_t _tone_ticks, _repeat_counter; -static long _em_interval_id = 0; +static volatile long _em_interval_id = 0; static int8_t *_sequence; +static uint8_t _volume; static void (*_cb_finished)(void); +static watch_cb_t _cb_start_global = NULL; +static watch_cb_t _cb_stop_global = NULL; +static volatile bool _buzzer_is_active = false; void _watch_enable_tcc(void) {} @@ -47,15 +51,27 @@ static inline void _em_interval_stop() { } void watch_buzzer_play_sequence(int8_t *note_sequence, void (*callback_on_end)(void)) { - if (_em_interval_id) _em_interval_stop(); - watch_set_buzzer_off(); + watch_buzzer_play_sequence_with_volume(note_sequence, callback_on_end, WATCH_BUZZER_VOLUME_LOUD); +} + +void watch_buzzer_play_sequence_with_volume(int8_t *note_sequence, void (*callback_on_end)(void), watch_buzzer_volume_t volume) { + watch_buzzer_abort_sequence(); + + _buzzer_is_active = true; + + if (_cb_start_global) { + _cb_start_global(); + } + _sequence = note_sequence; _cb_finished = callback_on_end; + _volume = volume == WATCH_BUZZER_VOLUME_SOFT ? 5 : 25; _seq_position = 0; _tone_ticks = 0; _repeat_counter = -1; // prepare buzzer watch_enable_buzzer(); + watch_set_buzzer_off(); // initiate 64 hz callback _em_interval_id = emscripten_set_interval(cb_watch_buzzer_seq, (double)(1000/64), (void *)NULL); } @@ -88,7 +104,7 @@ void cb_watch_buzzer_seq(void *userData) { if (note == BUZZER_NOTE_REST) { watch_set_buzzer_off(); } else { - watch_set_buzzer_period_and_duty_cycle(NotePeriods[note], 25); + watch_set_buzzer_period_and_duty_cycle(NotePeriods[note], _volume); watch_set_buzzer_on(); } // set duration ticks and move to next tone @@ -97,7 +113,6 @@ void cb_watch_buzzer_seq(void *userData) { } else { // end the sequence watch_buzzer_abort_sequence(); - if (_cb_finished) _cb_finished(); } } else _tone_ticks--; } @@ -105,10 +120,32 @@ void cb_watch_buzzer_seq(void *userData) { void watch_buzzer_abort_sequence(void) { // ends/aborts the sequence if (_em_interval_id) _em_interval_stop(); + watch_set_buzzer_off(); + watch_disable_buzzer(); + + if (!_buzzer_is_active) { + return; + } + + _buzzer_is_active = false; + + if (_cb_stop_global) { + _cb_stop_global(); + } + + if (_cb_finished) { + _cb_finished(); + } +} + +void watch_buzzer_register_global_callbacks(watch_cb_t cb_start, watch_cb_t cb_stop) { + _cb_stop_global = cb_start; + _cb_stop_global = cb_stop; } void watch_enable_buzzer(void) { + watch_buzzer_abort_sequence(); buzzer_enabled = true; buzzer_period = NotePeriods[BUZZER_NOTE_A4]; @@ -175,15 +212,17 @@ void watch_buzzer_play_note(watch_buzzer_note_t note, uint16_t duration_ms) { } void watch_buzzer_play_note_with_volume(watch_buzzer_note_t note, uint16_t duration_ms, watch_buzzer_volume_t volume) { - if (note == BUZZER_NOTE_REST) { - watch_set_buzzer_off(); - } else { - watch_set_buzzer_period_and_duty_cycle(NotePeriods[note], volume == WATCH_BUZZER_VOLUME_SOFT ? 5 : 25); - watch_set_buzzer_on(); - } + static int8_t single_note_sequence[3]; - main_loop_sleep(duration_ms); - watch_set_buzzer_off(); + single_note_sequence[0] = note; + // 64 ticks per second for the tc0? + // Each tick is approximately 15ms + uint16_t duration = duration_ms / 15; + if (duration > 127) duration = 127; + single_note_sequence[1] = (int8_t)duration; + single_note_sequence[2] = 0; + + watch_buzzer_play_sequence_with_volume(single_note_sequence, NULL, volume); } void watch_enable_leds(void) {} From e942f6768517085849daf4981d62c2c0ddcc9e9b Mon Sep 17 00:00:00 2001 From: Alessandro Genova Date: Sat, 26 Jul 2025 21:42:31 -0400 Subject: [PATCH 113/179] Minor fixes to settings faces to work with the new rtc mode --- watch-faces/settings/finetune_face.c | 4 ++-- watch-faces/settings/set_time_face.c | 6 +---- watch-faces/settings/settings_face.c | 4 ++-- .../hardware/watch/watch_deepsleep.c | 22 +++++++++---------- .../{hardware => shared}/watch/rtc32.h | 0 watch-library/simulator/watch/watch_rtc.c | 3 ++- 6 files changed, 18 insertions(+), 21 deletions(-) rename watch-library/{hardware => shared}/watch/rtc32.h (100%) diff --git a/watch-faces/settings/finetune_face.c b/watch-faces/settings/finetune_face.c index 7567f9fc..4c77c4ea 100644 --- a/watch-faces/settings/finetune_face.c +++ b/watch-faces/settings/finetune_face.c @@ -106,7 +106,7 @@ static void finetune_adjust_subseconds(int delta) { watch_rtc_enable(false); delay_ms(delta); if (delta > 500) { - watch_date_time_t date_time = watch_rtc_get_date_time(); + watch_date_time_t date_time = movement_get_utc_date_time(); date_time.unit.second = (date_time.unit.second + 1) % 60; if (date_time.unit.second == 0) { // Overflow date_time.unit.minute = (date_time.unit.minute + 1) % 60; @@ -116,7 +116,7 @@ static void finetune_adjust_subseconds(int delta) { date_time.unit.day++; } } - watch_rtc_set_date_time(date_time); + movement_set_utc_date_time(date_time); } watch_rtc_enable(true); } diff --git a/watch-faces/settings/set_time_face.c b/watch-faces/settings/set_time_face.c index e273e61d..f52c7179 100644 --- a/watch-faces/settings/set_time_face.c +++ b/watch-faces/settings/set_time_face.c @@ -108,10 +108,6 @@ bool set_time_face_loop(movement_event_t event, void *context) { case EVENT_ALARM_LONG_UP: _abort_quick_ticks(); break; - case EVENT_MODE_BUTTON_UP: - _abort_quick_ticks(); - movement_move_to_next_face(); - return false; case EVENT_LIGHT_BUTTON_DOWN: current_page = (current_page + 1) % SET_TIME_FACE_NUM_SETTINGS; *((uint8_t *)context) = current_page; @@ -187,6 +183,6 @@ bool set_time_face_loop(movement_event_t event, void *context) { void set_time_face_resign(void *context) { (void) context; - watch_set_led_off(); movement_store_settings(); + movement_request_tick_frequency(1); } diff --git a/watch-faces/settings/settings_face.c b/watch-faces/settings/settings_face.c index 0894ebe5..44d70f1b 100644 --- a/watch-faces/settings/settings_face.c +++ b/watch-faces/settings/settings_face.c @@ -322,7 +322,7 @@ bool settings_face_loop(movement_event_t event, void *context) { case EVENT_MODE_BUTTON_UP: movement_force_led_off(); movement_move_to_next_face(); - return false; + return true; case EVENT_ALARM_BUTTON_UP: state->settings_screens[state->current_page].advance(); break; @@ -339,7 +339,7 @@ bool settings_face_loop(movement_event_t event, void *context) { movement_force_led_on(color.red | color.red << 4, color.green | color.green << 4, color.blue | color.blue << 4); - return false; + return true; } else { movement_force_led_off(); return true; diff --git a/watch-library/hardware/watch/watch_deepsleep.c b/watch-library/hardware/watch/watch_deepsleep.c index a0abe5eb..e90150ee 100644 --- a/watch-library/hardware/watch/watch_deepsleep.c +++ b/watch-library/hardware/watch/watch_deepsleep.c @@ -41,7 +41,7 @@ void sleep(const uint8_t mode) { } void watch_register_extwake_callback(uint8_t pin, watch_cb_t callback, bool level) { - uint32_t config = RTC->MODE2.TAMPCTRL.reg; + uint32_t config = RTC->MODE0.TAMPCTRL.reg; if (pin == HAL_GPIO_BTN_ALARM_pin()) { HAL_GPIO_BTN_ALARM_in(); @@ -71,22 +71,22 @@ void watch_register_extwake_callback(uint8_t pin, watch_cb_t callback, bool leve } // disable the RTC - RTC->MODE2.CTRLA.bit.ENABLE = 0; - while (RTC->MODE2.SYNCBUSY.bit.ENABLE); // wait for RTC to be disabled + RTC->MODE0.CTRLA.bit.ENABLE = 0; + while (RTC->MODE0.SYNCBUSY.bit.ENABLE); // wait for RTC to be disabled // update the configuration - RTC->MODE2.TAMPCTRL.reg = config; + RTC->MODE0.TAMPCTRL.reg = config; // re-enable the RTC - RTC->MODE2.CTRLA.bit.ENABLE = 1; + RTC->MODE0.CTRLA.bit.ENABLE = 1; NVIC_ClearPendingIRQ(RTC_IRQn); NVIC_EnableIRQ(RTC_IRQn); - RTC->MODE2.INTENSET.reg = RTC_MODE2_INTENSET_TAMPER; + RTC->MODE0.INTENSET.reg = RTC_MODE0_INTENSET_TAMPER; } void watch_disable_extwake_interrupt(uint8_t pin) { - uint32_t config = RTC->MODE2.TAMPCTRL.reg; + uint32_t config = RTC->MODE0.TAMPCTRL.reg; if (pin == HAL_GPIO_BTN_ALARM_pin()) { btn_alarm_callback = NULL; @@ -101,14 +101,14 @@ void watch_disable_extwake_interrupt(uint8_t pin) { } // disable the RTC - RTC->MODE2.CTRLA.bit.ENABLE = 0; - while (RTC->MODE2.SYNCBUSY.bit.ENABLE); // wait for RTC to be disabled + RTC->MODE0.CTRLA.bit.ENABLE = 0; + while (RTC->MODE0.SYNCBUSY.bit.ENABLE); // wait for RTC to be disabled // update the configuration - RTC->MODE2.TAMPCTRL.reg = config; + RTC->MODE0.TAMPCTRL.reg = config; // re-enable the RTC - RTC->MODE2.CTRLA.bit.ENABLE = 1; + RTC->MODE0.CTRLA.bit.ENABLE = 1; } void watch_store_backup_data(uint32_t data, uint8_t reg) { diff --git a/watch-library/hardware/watch/rtc32.h b/watch-library/shared/watch/rtc32.h similarity index 100% rename from watch-library/hardware/watch/rtc32.h rename to watch-library/shared/watch/rtc32.h diff --git a/watch-library/simulator/watch/watch_rtc.c b/watch-library/simulator/watch/watch_rtc.c index 77463f74..ee410c44 100644 --- a/watch-library/simulator/watch/watch_rtc.c +++ b/watch-library/simulator/watch/watch_rtc.c @@ -64,6 +64,7 @@ watch_cb_t a4_callback; static void _watch_increase_counter(void *userData); static void _watch_process_periodic_callbacks(void); static void _watch_process_comp_callbacks(void); +static void _watch_rtc_schedule_next_comp(void); bool _watch_rtc_is_enabled(void) { return counter_interval; @@ -283,7 +284,7 @@ void watch_rtc_disable_comp_callback(uint8_t index) { _watch_rtc_schedule_next_comp(); } -void _watch_rtc_schedule_next_comp(void) { +static void _watch_rtc_schedule_next_comp(void) { scheduled_comp_counter = 0; // The soonest we can schedule is the next tick From 6e23421df4c676b240012c88f5f1ae435bdbc6a3 Mon Sep 17 00:00:00 2001 From: Alessandro Genova Date: Tue, 5 Aug 2025 23:39:47 -0400 Subject: [PATCH 114/179] Fix a bug that was causing 10x power usage from boot until the first sound played --- movement.c | 3 --- watch-library/hardware/watch/watch_tcc.c | 4 +--- 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/movement.c b/movement.c index adc01871..186aaeb5 100644 --- a/movement.c +++ b/movement.c @@ -960,9 +960,6 @@ void app_setup(void) { } #endif - watch_enable_buzzer(); - watch_enable_leds(); - movement_request_tick_frequency(1); for(uint8_t i = 0; i < MOVEMENT_NUM_FACES; i++) { diff --git a/watch-library/hardware/watch/watch_tcc.c b/watch-library/hardware/watch/watch_tcc.c index 06cee5d5..0e1fdd9d 100644 --- a/watch-library/hardware/watch/watch_tcc.c +++ b/watch-library/hardware/watch/watch_tcc.c @@ -317,9 +317,7 @@ void _watch_disable_tcc(void) { } void watch_enable_leds(void) { - if (!tcc_is_enabled(0)) { - _watch_enable_tcc(); - } + watch_enable_buzzer_and_leds(); } void watch_disable_leds(void) { From e2d13e076eb4f0e3893be2d5a95a74803223696d Mon Sep 17 00:00:00 2001 From: Alessandro Genova Date: Sat, 2 Aug 2025 23:06:45 -0400 Subject: [PATCH 115/179] Optimize finetune and nanosec faces to work with rtc-counter32 --- movement.c | 21 +++++++++++++++------ movement.h | 2 ++ watch-faces/settings/finetune_face.c | 23 +++++++---------------- watch-faces/settings/finetune_face.h | 5 ----- watch-faces/settings/nanosec_face.c | 10 ++++------ 5 files changed, 28 insertions(+), 33 deletions(-) diff --git a/movement.c b/movement.c index 186aaeb5..70bd63b7 100644 --- a/movement.c +++ b/movement.c @@ -535,8 +535,22 @@ watch_date_time_t movement_get_local_date_time(void) { return watch_utility_date_time_from_unix_time(timestamp, movement_get_current_timezone_offset()); } +uint32_t movement_get_utc_timestamp(void) { + return watch_rtc_get_unix_time(); +} + void movement_set_utc_date_time(watch_date_time_t date_time) { - watch_rtc_set_date_time(date_time); + movement_set_utc_timestamp(watch_utility_date_time_to_unix_time(date_time, 0)); +} + +void movement_set_local_date_time(watch_date_time_t date_time) { + int32_t current_offset = movement_get_current_timezone_offset(); + watch_date_time_t utc_date_time = watch_utility_date_time_convert_zone(date_time, current_offset, 0); + movement_set_utc_date_time(utc_date_time); +} + +void movement_set_utc_timestamp(uint32_t timestamp) { + watch_rtc_set_unix_time(timestamp); // If the time was changed, the top of the minute alarm needs to be reset accordingly _movement_set_top_of_minute_alarm(); @@ -547,11 +561,6 @@ void movement_set_utc_date_time(watch_date_time_t date_time) { _movement_update_dst_offset_cache(); } -void movement_set_local_date_time(watch_date_time_t date_time) { - int32_t current_offset = movement_get_current_timezone_offset(); - watch_date_time_t utc_date_time = watch_utility_date_time_convert_zone(date_time, current_offset, 0); - movement_set_utc_date_time(utc_date_time); -} bool movement_button_should_sound(void) { return movement_state.settings.bit.button_should_sound; diff --git a/movement.h b/movement.h index 0288c9c9..c1ac756f 100644 --- a/movement.h +++ b/movement.h @@ -331,9 +331,11 @@ void movement_set_timezone_index(uint8_t value); watch_date_time_t movement_get_utc_date_time(void); watch_date_time_t movement_get_local_date_time(void); watch_date_time_t movement_get_date_time_in_zone(uint8_t zone_index); +uint32_t movement_get_utc_timestamp(void); void movement_set_utc_date_time(watch_date_time_t date_time); void movement_set_local_date_time(watch_date_time_t date_time); +void movement_set_utc_timestamp(uint32_t timestamp); bool movement_button_should_sound(void); void movement_set_button_should_sound(bool value); diff --git a/watch-faces/settings/finetune_face.c b/watch-faces/settings/finetune_face.c index 4c77c4ea..fe75d8fa 100644 --- a/watch-faces/settings/finetune_face.c +++ b/watch-faces/settings/finetune_face.c @@ -27,7 +27,6 @@ #include #include "finetune_face.h" #include "nanosec_face.h" -#include "watch_utility.h" #include "delay.h" extern nanosec_state_t nanosec_state; @@ -51,7 +50,7 @@ void finetune_face_activate(void *context) { } static float finetune_get_hours_passed(void) { - uint32_t current_time = watch_utility_date_time_to_unix_time(watch_rtc_get_date_time(), 0); + uint32_t current_time = movement_get_utc_timestamp(); return (current_time - nanosec_state.last_correction_time) / 3600.0f; } @@ -64,7 +63,7 @@ static void finetune_update_display(void) { if (finetune_page == 0) { watch_display_text_with_fallback(WATCH_POSITION_TOP_LEFT, "FTU", "FT"); - watch_date_time_t date_time = watch_rtc_get_date_time(); + watch_date_time_t date_time = movement_get_utc_date_time(); sprintf(buf, "%04d%02d", abs(total_adjustment), date_time.unit.second); watch_display_text(WATCH_POSITION_BOTTOM, buf); @@ -106,17 +105,9 @@ static void finetune_adjust_subseconds(int delta) { watch_rtc_enable(false); delay_ms(delta); if (delta > 500) { - watch_date_time_t date_time = movement_get_utc_date_time(); - date_time.unit.second = (date_time.unit.second + 1) % 60; - if (date_time.unit.second == 0) { // Overflow - date_time.unit.minute = (date_time.unit.minute + 1) % 60; - if (date_time.unit.minute == 0) { // Overflow - date_time.unit.hour = (date_time.unit.hour + 1) % 24; - if (date_time.unit.hour == 0) // Overflow - date_time.unit.day++; - } - } - movement_set_utc_date_time(date_time); + uint32_t timestamp = movement_get_utc_timestamp(); + timestamp += 1; + movement_set_utc_timestamp(timestamp); } watch_rtc_enable(true); } @@ -126,7 +117,7 @@ static void finetune_update_correction_time(void) { nanosec_state.freq_correction += roundf(nanosec_get_aging() * 100); // Remember when we last corrected time - nanosec_state.last_correction_time = watch_utility_date_time_to_unix_time(watch_rtc_get_date_time(), 0); + nanosec_state.last_correction_time = movement_get_utc_timestamp(); nanosec_save(); movement_move_to_face(0); // Go to main face after saving settings } @@ -146,7 +137,7 @@ bool finetune_face_loop(movement_event_t event, void *context) { // We flash green LED once per minute to measure clock error, when we are not on first screen if (finetune_page!=0) { watch_date_time_t date_time; - date_time = watch_rtc_get_date_time(); + date_time = movement_get_utc_date_time(); if (date_time.unit.second == 0) { watch_set_led_green(); #ifndef __EMSCRIPTEN__ diff --git a/watch-faces/settings/finetune_face.h b/watch-faces/settings/finetune_face.h index 905df760..76ae7e23 100644 --- a/watch-faces/settings/finetune_face.h +++ b/watch-faces/settings/finetune_face.h @@ -44,11 +44,6 @@ * worry about aging only on second/third years of watch calibration (if you * are really looking at less than 10 seconds per year of error). * - * Warning, do not use at the first second of a month, as you might stay at - * the same month and it will surprise you. Just wait 1 second...We are not - * fully replicating RTC timer behavior when RTC is off. - * Simulating months and years is... too much complexity. - * * For full usage instructions, please refer to the wiki: * https://www.sensorwatch.net/docs/watchfaces/nanosec/ */ diff --git a/watch-faces/settings/nanosec_face.c b/watch-faces/settings/nanosec_face.c index d4d7b9f5..f49b22ad 100644 --- a/watch-faces/settings/nanosec_face.c +++ b/watch-faces/settings/nanosec_face.c @@ -27,7 +27,6 @@ #include #include "nanosec_face.h" #include "filesystem.h" -#include "watch_utility.h" int16_t freq_correction_residual = 0; // Dithering 0.1ppm correction, does not need to be configured. int16_t freq_correction_previous = -30000; @@ -44,8 +43,7 @@ const float voltage_coefficient = 0.241666667 * dithering; // 10 * ppm/V. Nomina static void nanosec_init_profile(void) { nanosec_changed = true; nanosec_state.correction_cadence = 10; - watch_date_time_t date_time = watch_rtc_get_date_time(); - nanosec_state.last_correction_time = watch_utility_date_time_to_unix_time(date_time, 0); + nanosec_state.last_correction_time = movement_get_utc_timestamp(); // init data after changing profile - do that once per profile selection switch (nanosec_state.correction_profile) { @@ -265,8 +263,8 @@ static void nanosec_next_edit_screen(void) { float nanosec_get_aging() // Returns aging correction in ppm { - watch_date_time_t date_time = watch_rtc_get_date_time(); - float years = (watch_utility_date_time_to_unix_time(date_time, 0) - nanosec_state.last_correction_time) / 31536000.0f; // Years passed since finetune + uint32_t timestamp = movement_get_utc_timestamp(); + float years = (timestamp - nanosec_state.last_correction_time) / 31536000.0f; // Years passed since finetune return years*nanosec_state.aging_ppm_pa/100.0f; } @@ -377,7 +375,7 @@ movement_watch_face_advisory_t nanosec_face_advise(void *context) { // No need for background correction if we are on profile 0 - static hardware correction. if (nanosec_state.correction_profile != 0) { - watch_date_time_t date_time = watch_rtc_get_date_time(); + watch_date_time_t date_time = movement_get_utc_date_time(); retval.wants_background_task = date_time.unit.minute % nanosec_state.correction_cadence == 0; } From 7acc9cc4142acf025705c256de2105e9c65c25f8 Mon Sep 17 00:00:00 2001 From: Alessandro Genova Date: Sun, 3 Aug 2025 01:13:19 -0400 Subject: [PATCH 116/179] Align the top of the second with the 1Hz periodic interrupt --- movement.c | 68 +++++++++++++++-------- watch-library/hardware/watch/watch_rtc.c | 23 ++++++-- watch-library/simulator/watch/watch_rtc.c | 9 +-- 3 files changed, 68 insertions(+), 32 deletions(-) diff --git a/movement.c b/movement.c index 70bd63b7..4f49fb8a 100644 --- a/movement.c +++ b/movement.c @@ -155,17 +155,36 @@ static udatetime_t _movement_convert_date_time_to_udate(watch_date_time_t date_t static void _movement_set_top_of_minute_alarm() { uint32_t counter = watch_rtc_get_counter(); + uint32_t next_minute_counter; watch_date_time_t date_time = watch_rtc_get_date_time(); uint32_t freq = watch_rtc_get_frequency(); + uint32_t half_freq = freq >> 1; + uint32_t subsecond_mask = freq - 1; + uint32_t ticks_per_minute = watch_rtc_get_ticks_per_minute(); - // remove subsecond from counter - counter &= ~(freq - 1); + // get the counter at the last second tick + next_minute_counter = counter & (~subsecond_mask); + // add/subtract half second shift to sync up second tick with the 1Hz interrupt + next_minute_counter += (counter & subsecond_mask) >= half_freq ? half_freq : -half_freq; // counter at the next top of the minute - counter += (60 - date_time.unit.second) * freq; + next_minute_counter += (60 - date_time.unit.second) * freq; - movement_volatile_state.minute_counter = counter; + // Since the minute alarm is very important, double/triple check to make sure that it will fire. + // These are theoretical corner cases that probably can't even happen, but since we do a subtraction + // above I wanna be certain that we don't schedule the next alarm at a counter value just before the + // current counter, which would result in the alarm firing after more than one year. + // This should be robust to the counter overflow, and we should ever iterate once at most. + if (next_minute_counter == counter) { + next_minute_counter += ticks_per_minute; + } - watch_rtc_register_comp_callback(cb_minute_alarm_fired, counter, MINUTE_TIMEOUT); + while ((next_minute_counter - counter) > ticks_per_minute) { + next_minute_counter += ticks_per_minute; + } + + movement_volatile_state.minute_counter = next_minute_counter; + + watch_rtc_register_comp_callback(cb_minute_alarm_fired, next_minute_counter, MINUTE_TIMEOUT); } static bool _movement_update_dst_offset_cache(void) { @@ -850,9 +869,6 @@ void app_init(void) { watch_rtc_set_date_time(date_time); } - // set up the 1 minute alarm (for background tasks and low power updates) - _movement_set_top_of_minute_alarm(); - // register callbacks to be notified when buzzer starts/stops playing. // this is so movement can be notified even when triggered by a face bypassing movement watch_buzzer_register_global_callbacks(cb_buzzer_start, cb_buzzer_stop); @@ -865,6 +881,9 @@ void app_init(void) { movement_state.light_on = false; movement_state.next_available_backup_register = 2; _movement_reset_inactivity_countdown(); + + // set up the 1 minute alarm (for background tasks and low power updates) + _movement_set_top_of_minute_alarm(); } void app_wake_from_backup(void) { @@ -1069,21 +1088,6 @@ bool app_loop(void) { movement_volatile_state.turn_led_off = false; movement_force_led_off(); } - } - - // actually play the note sequence we were asked to play while in deep sleep. - if (movement_volatile_state.has_pending_sequence) { - movement_volatile_state.has_pending_sequence = false; - watch_buzzer_play_sequence_with_volume(_pending_sequence, movement_request_sleep, movement_button_volume()); - // When this sequence is done playing, movement_request_sleep is invoked and the watch will go, - // back to sleep (unless the user interacts with it in the meantime) - _pending_sequence = NULL; - } - - // handle top-of-minute tasks, if the alarm handler told us we need to - if (movement_volatile_state.minute_alarm_fired) { - movement_volatile_state.minute_alarm_fired = false; - _movement_handle_top_of_minute(); } // if we have a scheduled background task, handle that here: @@ -1123,6 +1127,12 @@ bool app_loop(void) { event_type++; } + // handle top-of-minute tasks, if the alarm handler told us we need to + if (movement_volatile_state.minute_alarm_fired) { + movement_volatile_state.minute_alarm_fired = false; + _movement_handle_top_of_minute(); + } + // Now handle the EVENT_TIMEOUT if (resign_timeout && movement_state.current_face_idx != 0) { event.event_type = EVENT_TIMEOUT; @@ -1153,6 +1163,15 @@ bool app_loop(void) { // // this is a hack tho: waking from sleep mode, app_setup does get called, but it happens before we have reset our ticks. // // need to figure out if there's a better heuristic for determining how we woke up. app_setup(); + + // If we woke up to play a note sequence, actually play the note sequence we were asked to play while in deep sleep. + if (movement_volatile_state.has_pending_sequence) { + movement_volatile_state.has_pending_sequence = false; + watch_buzzer_play_sequence_with_volume(_pending_sequence, movement_request_sleep, movement_button_volume()); + // When this sequence is done playing, movement_request_sleep is invoked and the watch will go, + // back to sleep (unless the user interacts with it in the meantime) + _pending_sequence = NULL; + } } #endif @@ -1300,9 +1319,10 @@ void cb_minute_alarm_fired(void) { void cb_tick(void) { rtc_counter_t counter = watch_rtc_get_counter(); uint32_t freq = watch_rtc_get_frequency(); + uint32_t half_freq = freq >> 1; uint32_t subsecond_mask = freq - 1; movement_volatile_state.pending_events |= 1 << EVENT_TICK; - movement_volatile_state.subsecond = (counter & subsecond_mask) >> movement_state.tick_pern; + movement_volatile_state.subsecond = ((counter + half_freq) & subsecond_mask) >> movement_state.tick_pern; } void cb_accelerometer_event(void) { diff --git a/watch-library/hardware/watch/watch_rtc.c b/watch-library/hardware/watch/watch_rtc.c index 61bc5d1f..f81f10ff 100644 --- a/watch-library/hardware/watch/watch_rtc.c +++ b/watch-library/hardware/watch/watch_rtc.c @@ -34,6 +34,7 @@ static const uint32_t RTC_OSC_DIV = 10; static const uint32_t RTC_OSC_HZ = 1 << RTC_OSC_DIV; // 2^10 = 1024 static const uint32_t RTC_PRESCALER_DIV = 3; static const uint32_t RTC_CNT_HZ = RTC_OSC_HZ >> RTC_PRESCALER_DIV; // 1024 / 2^3 = 128 +static const uint32_t RTC_CNT_SUBSECOND_MASK = RTC_CNT_HZ - 1; static const uint32_t RTC_CNT_DIV = RTC_OSC_DIV - RTC_PRESCALER_DIV; // 7 static const uint32_t RTC_CNT_TICKS_PER_MINUTE = RTC_CNT_HZ * 60; static const uint32_t RTC_CNT_TICKS_PER_HOUR = RTC_CNT_TICKS_PER_MINUTE * 60; @@ -88,17 +89,31 @@ rtc_date_time_t watch_rtc_get_date_time(void) { } void watch_rtc_set_unix_time(unix_timestamp_t unix_time) { - // time_backup + counter / RTC_CNT_HZ = unix_time + /* unix_time = time_backup + counter / RTC_CNT_HZ - 0.5 + * + * Because of the way the hardware is designed, the periodic interrupts fire at the subsecond tick values + * according to the table below (for a 128Hz counter). + * since the 1Hz periodic interrupt is the most important, we shift the conversion from counter to timestamp by 64 ticks, + * so that the second changes at the top of the 1Hz interrupt. Hence the 0.5 factor in the equation above. + * 1Hz: 64 + * 2Hz: 32, 96 + * 4Hz: 16, 48, 80, 112 + * 8Hz: 8, 24, 40, 56, 72, 88, 104, 120 + * 16Hz: 4, 12, 20, ..., 124 + * 32Hz: 2, 6, 10, ..., 126 + * 64Hz: 1, 3, 5, ..., 127 + * 128Hz: 0, 1, 2, ..., 127 + */ rtc_counter_t counter = rtc_get_counter(); - unix_timestamp_t tb = unix_time - (counter >> RTC_CNT_DIV); + unix_timestamp_t tb = unix_time - (counter >> RTC_CNT_DIV) - ((counter & RTC_CNT_SUBSECOND_MASK) >> (RTC_CNT_DIV - 1)) + 1; watch_store_backup_data(tb, TB_BKUP_REG); } unix_timestamp_t watch_rtc_get_unix_time(void) { - // time_backup + counter / RTC_CNT_HZ = unix_time + // unix_time = time_backup + counter / RTC_CNT_HZ - 0.5 rtc_counter_t counter = rtc_get_counter(); unix_timestamp_t tb = watch_get_backup_data(TB_BKUP_REG); - return tb + (counter >> RTC_CNT_DIV); + return tb + (counter >> RTC_CNT_DIV) + ((counter & RTC_CNT_SUBSECOND_MASK) >> (RTC_CNT_DIV - 1)) - 1; } rtc_counter_t watch_rtc_get_counter(void) { diff --git a/watch-library/simulator/watch/watch_rtc.c b/watch-library/simulator/watch/watch_rtc.c index ee410c44..b58b6bfa 100644 --- a/watch-library/simulator/watch/watch_rtc.c +++ b/watch-library/simulator/watch/watch_rtc.c @@ -31,6 +31,7 @@ #include static const uint32_t RTC_CNT_HZ = 128; +static const uint32_t RTC_CNT_SUBSECOND_MASK = RTC_CNT_HZ - 1; static const uint32_t RTC_CNT_DIV = 7; static const uint32_t RTC_CNT_TICKS_PER_MINUTE = RTC_CNT_HZ * 60; static const uint32_t RTC_CNT_TICKS_PER_HOUR = RTC_CNT_TICKS_PER_MINUTE * 60; @@ -98,15 +99,15 @@ rtc_date_time_t watch_rtc_get_date_time(void) { } void watch_rtc_set_unix_time(unix_timestamp_t unix_time) { - // time_backup + counter / RTC_CNT_HZ = unix_time + // unix_time = time_backup + counter / RTC_CNT_HZ - 0.5 rtc_counter_t counter = watch_rtc_get_counter(); - reference_timestamp = unix_time - (counter >> RTC_CNT_DIV); + reference_timestamp = unix_time - (counter >> RTC_CNT_DIV) - ((counter & RTC_CNT_SUBSECOND_MASK) >> (RTC_CNT_DIV - 1)) + 1; } unix_timestamp_t watch_rtc_get_unix_time(void) { - // time_backup + counter / RTC_CNT_HZ = unix_time + // unix_time = time_backup + counter / RTC_CNT_HZ - 0.5 rtc_counter_t counter = watch_rtc_get_counter(); - return reference_timestamp + (counter >> RTC_CNT_DIV); + return reference_timestamp + (counter >> RTC_CNT_DIV) + ((counter & RTC_CNT_SUBSECOND_MASK) >> (RTC_CNT_DIV - 1)) - 1; } rtc_counter_t watch_rtc_get_counter(void) { From a616ac6cc4865b8569a7f1bfb218638903532075 Mon Sep 17 00:00:00 2001 From: Alessandro Genova Date: Thu, 7 Aug 2025 23:34:13 -0400 Subject: [PATCH 117/179] Fix corner case that could cause the top of minute alarm to stop firing --- watch-library/hardware/watch/watch_rtc.c | 28 ++++++++++++++++------- watch-library/simulator/watch/watch_rtc.c | 11 +++++++-- 2 files changed, 29 insertions(+), 10 deletions(-) diff --git a/watch-library/hardware/watch/watch_rtc.c b/watch-library/hardware/watch/watch_rtc.c index f81f10ff..1fee9917 100644 --- a/watch-library/hardware/watch/watch_rtc.c +++ b/watch-library/hardware/watch/watch_rtc.c @@ -49,6 +49,8 @@ typedef struct { volatile bool enabled; } comp_cb_t; +volatile uint32_t scheduled_comp_counter; + watch_cb_t tick_callbacks[8]; comp_cb_t comp_callbacks[WATCH_RTC_N_COMP_CB]; watch_cb_t alarm_callback; @@ -76,6 +78,8 @@ void _watch_rtc_init(void) { comp_callbacks[index].enabled = false; } + scheduled_comp_counter = 0; + NVIC_ClearPendingIRQ(RTC_IRQn); NVIC_EnableIRQ(RTC_IRQn); } @@ -195,10 +199,15 @@ void watch_rtc_disable_all_periodic_callbacks(void) { } static void _watch_rtc_schedule_next_comp(void) { - rtc_disable_compare_interrupt(); + rtc_counter_t curr_counter = watch_rtc_get_counter(); + // If there is already a pending comp interrupt for this very tick, let it fire + // And this function will be called again as soon as the interrupt fires. + if (curr_counter == scheduled_comp_counter) { + return; + } - // The soonest we can schedule is the next tick - rtc_counter_t curr_counter = watch_rtc_get_counter() + 1; + // Because of the hardware, the soonest we can schedule is the next tick + curr_counter +=1; bool schedule_any = false; rtc_counter_t comp_counter; @@ -216,7 +225,14 @@ static void _watch_rtc_schedule_next_comp(void) { } if (schedule_any) { - rtc_enable_compare_interrupt(comp_counter); + // If we are changing the comp counter at the front of the line + if (comp_counter != scheduled_comp_counter) { + scheduled_comp_counter = comp_counter; + rtc_enable_compare_interrupt(comp_counter); + } + } else { + scheduled_comp_counter = curr_counter - 2; + rtc_disable_compare_interrupt(); } } @@ -225,8 +241,6 @@ void watch_rtc_register_comp_callback(watch_cb_t callback, rtc_counter_t counter return; } - rtc_disable_compare_interrupt(); - comp_callbacks[index].counter = counter; comp_callbacks[index].callback = callback; comp_callbacks[index].enabled = true; @@ -239,8 +253,6 @@ void watch_rtc_disable_comp_callback(uint8_t index) { return; } - rtc_disable_compare_interrupt(); - comp_callbacks[index].enabled = false; _watch_rtc_schedule_next_comp(); diff --git a/watch-library/simulator/watch/watch_rtc.c b/watch-library/simulator/watch/watch_rtc.c index b58b6bfa..d9cfb9f2 100644 --- a/watch-library/simulator/watch/watch_rtc.c +++ b/watch-library/simulator/watch/watch_rtc.c @@ -286,10 +286,15 @@ void watch_rtc_disable_comp_callback(uint8_t index) { } static void _watch_rtc_schedule_next_comp(void) { - scheduled_comp_counter = 0; + rtc_counter_t curr_counter = watch_rtc_get_counter(); + // If there is already a pending comp interrupt for this very tick, let it fire + // And this function will be called again as soon as the interrupt fires. + if (curr_counter == scheduled_comp_counter) { + return; + } // The soonest we can schedule is the next tick - rtc_counter_t curr_counter = watch_rtc_get_counter() + 1; + curr_counter +=1; bool schedule_any = false; rtc_counter_t comp_counter; @@ -309,6 +314,8 @@ static void _watch_rtc_schedule_next_comp(void) { if (schedule_any) { scheduled_comp_counter = comp_counter; + } else { + scheduled_comp_counter = curr_counter - 2; } } From 9770ad4fe97d5e6a8b6fa83ec4f5b183a940fa36 Mon Sep 17 00:00:00 2001 From: Alessandro Genova Date: Fri, 8 Aug 2025 09:19:14 -0400 Subject: [PATCH 118/179] Fix more corner case that could cause the top of minute alarm to stop firing --- movement.c | 36 +++++++++++++---------- watch-library/hardware/watch/watch_rtc.c | 35 +++++++++++++++++----- watch-library/shared/watch/watch_rtc.h | 28 ++++++++++++++++++ watch-library/simulator/watch/watch_rtc.c | 27 +++++++++++++---- 4 files changed, 98 insertions(+), 28 deletions(-) diff --git a/movement.c b/movement.c index 4f49fb8a..7e4a23bf 100644 --- a/movement.c +++ b/movement.c @@ -220,7 +220,7 @@ static inline void _movement_reset_inactivity_countdown(void) { rtc_counter_t counter = watch_rtc_get_counter(); uint32_t freq = watch_rtc_get_frequency(); - watch_rtc_register_comp_callback( + watch_rtc_register_comp_callback_no_schedule( cb_resign_timeout_interrupt, counter + movement_timeout_inactivity_deadlines[movement_state.settings.bit.to_interval] * freq, RESIGN_TIMEOUT @@ -228,16 +228,19 @@ static inline void _movement_reset_inactivity_countdown(void) { movement_volatile_state.enter_sleep_mode = false; - watch_rtc_register_comp_callback( + watch_rtc_register_comp_callback_no_schedule( cb_sleep_timeout_interrupt, counter + movement_le_inactivity_deadlines[movement_state.settings.bit.le_interval] * freq, SLEEP_TIMEOUT ); + + watch_rtc_schedule_next_comp(); } static inline void _movement_disable_inactivity_countdown(void) { - watch_rtc_disable_comp_callback(RESIGN_TIMEOUT); - watch_rtc_disable_comp_callback(SLEEP_TIMEOUT); + watch_rtc_disable_comp_callback_no_schedule(RESIGN_TIMEOUT); + watch_rtc_disable_comp_callback_no_schedule(SLEEP_TIMEOUT); + watch_rtc_schedule_next_comp(); } static void _movement_handle_top_of_minute(void) { @@ -1194,9 +1197,11 @@ bool app_loop(void) { } static movement_event_type_t _process_button_event(bool pin_level, movement_button_t* button) { + movement_event_type_t event_type = EVENT_NONE; + // This shouldn't happen normally if (pin_level == button->is_down) { - return EVENT_NONE; + return event_type; } uint32_t counter = watch_rtc_get_counter(); @@ -1206,43 +1211,42 @@ static movement_event_type_t _process_button_event(bool pin_level, movement_butt if (pin_level) { // We schedule a timeout to fire the longpress event button->down_timestamp = counter; - watch_rtc_register_comp_callback(button->cb_longpress, counter + MOVEMENT_LONG_PRESS_TICKS, button->timeout_index); + watch_rtc_register_comp_callback_no_schedule(button->cb_longpress, counter + MOVEMENT_LONG_PRESS_TICKS, button->timeout_index); // force alarm off if the user pressed a button. watch_buzzer_abort_sequence(); - return button->down_event; + event_type = button->down_event; } else { // We cancel the timeout if it hasn't fired yet - watch_rtc_disable_comp_callback(button->timeout_index); + watch_rtc_disable_comp_callback_no_schedule(button->timeout_index); if ((counter - button->down_timestamp) >= MOVEMENT_LONG_PRESS_TICKS) { - return button->down_event + 3; + event_type = button->down_event + 3; } else { - return button->down_event + 1; + event_type = button->down_event + 1; } } + + // This will also schedule the comp callbacks above + _movement_reset_inactivity_countdown(); + + return event_type; } void cb_light_btn_interrupt(void) { bool pin_level = HAL_GPIO_BTN_LIGHT_read(); movement_volatile_state.pending_events |= 1 << _process_button_event(pin_level, &movement_volatile_state.light_button); - - _movement_reset_inactivity_countdown(); } void cb_mode_btn_interrupt(void) { bool pin_level = HAL_GPIO_BTN_MODE_read(); movement_volatile_state.pending_events |= 1 << _process_button_event(pin_level, &movement_volatile_state.mode_button); - - _movement_reset_inactivity_countdown(); } void cb_alarm_btn_interrupt(void) { bool pin_level = HAL_GPIO_BTN_ALARM_read(); movement_volatile_state.pending_events |= 1 << _process_button_event(pin_level, &movement_volatile_state.alarm_button); - - _movement_reset_inactivity_countdown(); } static movement_event_type_t _process_button_longpress_timeout(movement_button_t* button) { diff --git a/watch-library/hardware/watch/watch_rtc.c b/watch-library/hardware/watch/watch_rtc.c index 1fee9917..8d5255ad 100644 --- a/watch-library/hardware/watch/watch_rtc.c +++ b/watch-library/hardware/watch/watch_rtc.c @@ -198,7 +198,7 @@ void watch_rtc_disable_all_periodic_callbacks(void) { watch_rtc_disable_matching_periodic_callbacks(0xFF); } -static void _watch_rtc_schedule_next_comp(void) { +void watch_rtc_schedule_next_comp(void) { rtc_counter_t curr_counter = watch_rtc_get_counter(); // If there is already a pending comp interrupt for this very tick, let it fire // And this function will be called again as soon as the interrupt fires. @@ -245,7 +245,17 @@ void watch_rtc_register_comp_callback(watch_cb_t callback, rtc_counter_t counter comp_callbacks[index].callback = callback; comp_callbacks[index].enabled = true; - _watch_rtc_schedule_next_comp(); + watch_rtc_schedule_next_comp(); +} + +void watch_rtc_register_comp_callback_no_schedule(watch_cb_t callback, rtc_counter_t counter, uint8_t index) { + if (index >= WATCH_RTC_N_COMP_CB) { + return; + } + + comp_callbacks[index].counter = counter; + comp_callbacks[index].callback = callback; + comp_callbacks[index].enabled = true; } void watch_rtc_disable_comp_callback(uint8_t index) { @@ -255,13 +265,21 @@ void watch_rtc_disable_comp_callback(uint8_t index) { comp_callbacks[index].enabled = false; - _watch_rtc_schedule_next_comp(); + watch_rtc_schedule_next_comp(); +} + +void watch_rtc_disable_comp_callback_no_schedule(uint8_t index) { + if (index >= WATCH_RTC_N_COMP_CB) { + return; + } + + comp_callbacks[index].enabled = false; } void watch_rtc_callback(uint16_t interrupt_cause) { // First read all relevant registers, to ensure no changes occurr during the callbacks uint16_t interrupt_enabled = RTC->MODE0.INTENSET.reg; - rtc_counter_t comp_counter = RTC->MODE0.COMP[0].reg; + rtc_counter_t counter = watch_rtc_get_counter(); if ((interrupt_cause & interrupt_enabled) & RTC_MODE0_INTFLAG_PER_Msk) { @@ -291,15 +309,18 @@ void watch_rtc_callback(uint16_t interrupt_cause) { if ((interrupt_cause & interrupt_enabled) & RTC_MODE0_INTFLAG_CMP0) { // The comp interrupt is generated one tick after the matched counter - // rtc_counter_t comp_counter = watch_rtc_get_counter() - 1; + rtc_counter_t comp_counter = counter - 1; for (uint8_t index = 0; index < WATCH_RTC_N_COMP_CB; ++index) { - if (comp_callbacks[index].enabled && comp_counter == comp_callbacks[index].counter) { + // Give it a little bit of wiggle room, if a comp callback is enabled and is just passed + if (comp_callbacks[index].enabled && + (comp_counter - comp_callbacks[index].counter) <= RTC_CNT_HZ + ) { comp_callbacks[index].enabled = false; comp_callbacks[index].callback(); } } - _watch_rtc_schedule_next_comp(); + watch_rtc_schedule_next_comp(); } if ((interrupt_cause & interrupt_enabled) & RTC_MODE0_INTFLAG_OVF) { diff --git a/watch-library/shared/watch/watch_rtc.h b/watch-library/shared/watch/watch_rtc.h index e268ad28..504c37f3 100644 --- a/watch-library/shared/watch/watch_rtc.h +++ b/watch-library/shared/watch/watch_rtc.h @@ -114,10 +114,38 @@ uint32_t watch_rtc_get_ticks_per_minute(void); */ void watch_rtc_register_comp_callback(watch_cb_t callback, rtc_counter_t counter, uint8_t index); +/** @brief Just like watch_rtc_register_comp_callback but doesn't actually schedule the callback + * + * Useful if you need register multiple callbacks at once, avoids multiple calls to the expensive watch_rtc_schedule_next_comp: + * Usage: + * watch_rtc_register_comp_callback_no_schedule(cb0, counter0, index0); + * watch_rtc_register_comp_callback_no_schedule(cb1, counter1, index1); + * watch_rtc_schedule_next_comp(); + */ +void watch_rtc_register_comp_callback_no_schedule(watch_cb_t callback, rtc_counter_t counter, uint8_t index); + /** @brief Disables the specified comp callback. */ void watch_rtc_disable_comp_callback(uint8_t index); +/** @brief Just like watch_rtc_disable_comp_callback but doesn't actually schedule the callback + * + * Useful if you need disable multiple callbacks at once, avoids multiple calls to the expensive watch_rtc_schedule_next_comp: + * Usage: + * watch_rtc_disable_comp_callback_no_schedule(index0); + * watch_rtc_disable_comp_callback_no_schedule(index1); + * watch_rtc_schedule_next_comp(); + */ +/** @brief Disables the specified comp callback. + */ +void watch_rtc_disable_comp_callback_no_schedule(uint8_t index); + +/** @brief Determines the first comp callback that should fire and schedule it with the RTC + * + * You would never need to call this manually, unless you used the 'no_schedule' functions above. + */ +void watch_rtc_schedule_next_comp(void); + /** @brief Disables the alarm callback. */ // void watch_rtc_disable_alarm_callback(void); diff --git a/watch-library/simulator/watch/watch_rtc.c b/watch-library/simulator/watch/watch_rtc.c index d9cfb9f2..1347d797 100644 --- a/watch-library/simulator/watch/watch_rtc.c +++ b/watch-library/simulator/watch/watch_rtc.c @@ -65,7 +65,6 @@ watch_cb_t a4_callback; static void _watch_increase_counter(void *userData); static void _watch_process_periodic_callbacks(void); static void _watch_process_comp_callbacks(void); -static void _watch_rtc_schedule_next_comp(void); bool _watch_rtc_is_enabled(void) { return counter_interval; @@ -228,7 +227,7 @@ static void _watch_process_comp_callbacks(void) { } } - _watch_rtc_schedule_next_comp(); + watch_rtc_schedule_next_comp(); } } @@ -272,7 +271,17 @@ void watch_rtc_register_comp_callback(watch_cb_t callback, rtc_counter_t counter comp_callbacks[index].callback = callback; comp_callbacks[index].enabled = true; - _watch_rtc_schedule_next_comp(); + watch_rtc_schedule_next_comp(); +} + +void watch_rtc_register_comp_callback_no_schedule(watch_cb_t callback, rtc_counter_t counter, uint8_t index) { + if (index >= WATCH_RTC_N_COMP_CB) { + return; + } + + comp_callbacks[index].counter = counter; + comp_callbacks[index].callback = callback; + comp_callbacks[index].enabled = true; } void watch_rtc_disable_comp_callback(uint8_t index) { @@ -282,10 +291,18 @@ void watch_rtc_disable_comp_callback(uint8_t index) { comp_callbacks[index].enabled = false; - _watch_rtc_schedule_next_comp(); + watch_rtc_schedule_next_comp(); } -static void _watch_rtc_schedule_next_comp(void) { +void watch_rtc_disable_comp_callback_no_schedule(uint8_t index) { + if (index >= WATCH_RTC_N_COMP_CB) { + return; + } + + comp_callbacks[index].enabled = false; +} + +void watch_rtc_schedule_next_comp(void) { rtc_counter_t curr_counter = watch_rtc_get_counter(); // If there is already a pending comp interrupt for this very tick, let it fire // And this function will be called again as soon as the interrupt fires. From a71967a0156a2f09f4eb4a706c9e5f6c345be4cb Mon Sep 17 00:00:00 2001 From: Alessandro Genova Date: Sat, 9 Aug 2025 11:47:57 -0400 Subject: [PATCH 119/179] Perform as little work as possible in the interrupt callbacks --- movement.c | 111 +++++++++++++++++------ watch-library/hardware/watch/watch_rtc.c | 9 +- 2 files changed, 85 insertions(+), 35 deletions(-) diff --git a/movement.c b/movement.c index 7e4a23bf..49df5023 100644 --- a/movement.c +++ b/movement.c @@ -84,6 +84,7 @@ typedef struct { volatile bool minute_alarm_fired; volatile bool is_buzzing; volatile uint8_t pending_sequence_priority; + volatile bool schedule_next_comp; // button tracking for long press movement_button_t mode_button; @@ -184,7 +185,8 @@ static void _movement_set_top_of_minute_alarm() { movement_volatile_state.minute_counter = next_minute_counter; - watch_rtc_register_comp_callback(cb_minute_alarm_fired, next_minute_counter, MINUTE_TIMEOUT); + watch_rtc_register_comp_callback_no_schedule(cb_minute_alarm_fired, next_minute_counter, MINUTE_TIMEOUT); + movement_volatile_state.schedule_next_comp = true; } static bool _movement_update_dst_offset_cache(void) { @@ -234,13 +236,66 @@ static inline void _movement_reset_inactivity_countdown(void) { SLEEP_TIMEOUT ); - watch_rtc_schedule_next_comp(); + movement_volatile_state.schedule_next_comp = true; } static inline void _movement_disable_inactivity_countdown(void) { watch_rtc_disable_comp_callback_no_schedule(RESIGN_TIMEOUT); watch_rtc_disable_comp_callback_no_schedule(SLEEP_TIMEOUT); - watch_rtc_schedule_next_comp(); + movement_volatile_state.schedule_next_comp = true; +} + +static void _movement_renew_top_of_minute_alarm(void) { + // Renew the alarm for a minute from the previous one (ensures no drift) + movement_volatile_state.minute_counter += watch_rtc_get_ticks_per_minute(); + watch_rtc_register_comp_callback_no_schedule(cb_minute_alarm_fired, movement_volatile_state.minute_counter, MINUTE_TIMEOUT); + movement_volatile_state.schedule_next_comp = true; +} + +static void _movement_handle_button_presses(uint32_t pending_events) { + bool any_up = false; + bool any_down = false; + + movement_button_t* buttons[3] = { + &movement_volatile_state.mode_button, + &movement_volatile_state.light_button, + &movement_volatile_state.alarm_button + }; + + for (uint8_t i = 0; i < 3; i++) { + movement_button_t* button = buttons[i]; + + // If a button down occurred + if (pending_events & (1 << button->down_event)) { + watch_rtc_register_comp_callback_no_schedule(button->cb_longpress, button->down_timestamp + MOVEMENT_LONG_PRESS_TICKS, button->timeout_index); + any_down = true; + } + + // If a button up or button long up occurred + if (pending_events & ( + (1 << (button->down_event + 1)) | + (1 << (button->down_event + 3)) + )) { + // We cancel the timeout if it hasn't fired yet + watch_rtc_disable_comp_callback_no_schedule(button->timeout_index); + any_up = true; + } + } + + if (any_down) { + // force alarm off if the user pressed a button. + watch_buzzer_abort_sequence(); + + // Delay auto light off if the user is still interacting with the watch. + if (movement_state.light_on) { + movement_illuminate_led(); + } + } + + if (any_down || any_up) { + _movement_reset_inactivity_countdown(); + movement_volatile_state.schedule_next_comp = true; + } } static void _movement_handle_top_of_minute(void) { @@ -328,11 +383,12 @@ void movement_illuminate_led(void) { // Set a timeout to turn off the light rtc_counter_t counter = watch_rtc_get_counter(); uint32_t freq = watch_rtc_get_frequency(); - watch_rtc_register_comp_callback( + watch_rtc_register_comp_callback_no_schedule( cb_led_timeout_interrupt, counter + (movement_state.settings.bit.led_duration * 2 - 1) * freq, LED_TIMEOUT ); + movement_volatile_state.schedule_next_comp = true; } } } @@ -342,13 +398,15 @@ void movement_force_led_on(uint8_t red, uint8_t green, uint8_t blue) { movement_state.light_on = true; watch_set_led_color_rgb(red, green, blue); rtc_counter_t counter = watch_rtc_get_counter(); - watch_rtc_register_comp_callback(cb_led_timeout_interrupt, counter + 32767, LED_TIMEOUT); + watch_rtc_register_comp_callback_no_schedule(cb_led_timeout_interrupt, counter + 32767, LED_TIMEOUT); + movement_volatile_state.schedule_next_comp = true; } void movement_force_led_off(void) { movement_state.light_on = false; // The led timeout probably already triggered, but still disable just in case we are switching off the light by other means - watch_rtc_disable_comp_callback(LED_TIMEOUT); + watch_rtc_disable_comp_callback_no_schedule(LED_TIMEOUT); + movement_volatile_state.schedule_next_comp = true; watch_set_led_off(); } @@ -1018,6 +1076,7 @@ static void _sleep_mode_app_loop(void) { // we also have to handle top-of-the-minute tasks here in the mini-runloop if (movement_volatile_state.minute_alarm_fired) { movement_volatile_state.minute_alarm_fired = false; + _movement_renew_top_of_minute_alarm(); _movement_handle_top_of_minute(); } @@ -1034,6 +1093,12 @@ static void _sleep_mode_app_loop(void) { return; } + // If we have made changes to any of the RTC comp timers, schedule the next one in the queue + if (movement_volatile_state.schedule_next_comp) { + movement_volatile_state.schedule_next_comp = false; + watch_rtc_schedule_next_comp(); + } + // otherwise enter sleep mode, until either the top of the minute interrupt or extwake wakes us up. watch_enter_sleep_mode(); } @@ -1093,6 +1158,9 @@ bool app_loop(void) { } } + // handle any button up/down events that occurred, e.g. schedule longpress timeouts, reset inactivity, etc. + _movement_handle_button_presses(pending_events); + // if we have a scheduled background task, handle that here: if ( (pending_events & (1 << EVENT_TICK)) @@ -1102,17 +1170,6 @@ bool app_loop(void) { _movement_handle_scheduled_tasks(); } - // Delay auto light off if the user is still interacting with the watch. - if (movement_state.light_on) { - if (pending_events & ( - (1 << EVENT_LIGHT_BUTTON_DOWN) | - (1 << EVENT_MODE_BUTTON_DOWN) | - (1 << EVENT_ALARM_BUTTON_DOWN) - )) { - movement_illuminate_led(); - } - } - // Pop the EVENT_TIMEOUT out of the pending_events so it can be handled separately bool resign_timeout = (pending_events & (1 << EVENT_TIMEOUT)) != 0; if (resign_timeout) { @@ -1133,6 +1190,7 @@ bool app_loop(void) { // handle top-of-minute tasks, if the alarm handler told us we need to if (movement_volatile_state.minute_alarm_fired) { movement_volatile_state.minute_alarm_fired = false; + _movement_renew_top_of_minute_alarm(); _movement_handle_top_of_minute(); } @@ -1178,6 +1236,12 @@ bool app_loop(void) { } #endif + // If we have made changes to any of the RTC comp timers, schedule the next one in the queue + if (movement_volatile_state.schedule_next_comp) { + movement_volatile_state.schedule_next_comp = false; + watch_rtc_schedule_next_comp(); + } + #if __EMSCRIPTEN__ shell_task(); #else @@ -1209,15 +1273,9 @@ static movement_event_type_t _process_button_event(bool pin_level, movement_butt button->is_down = pin_level; if (pin_level) { - // We schedule a timeout to fire the longpress event button->down_timestamp = counter; - watch_rtc_register_comp_callback_no_schedule(button->cb_longpress, counter + MOVEMENT_LONG_PRESS_TICKS, button->timeout_index); - // force alarm off if the user pressed a button. - watch_buzzer_abort_sequence(); event_type = button->down_event; } else { - // We cancel the timeout if it hasn't fired yet - watch_rtc_disable_comp_callback_no_schedule(button->timeout_index); if ((counter - button->down_timestamp) >= MOVEMENT_LONG_PRESS_TICKS) { event_type = button->down_event + 3; } else { @@ -1225,9 +1283,6 @@ static movement_event_type_t _process_button_event(bool pin_level, movement_butt } } - // This will also schedule the comp callbacks above - _movement_reset_inactivity_countdown(); - return event_type; } @@ -1314,10 +1369,6 @@ void cb_minute_alarm_fired(void) { #if __EMSCRIPTEN__ _wake_up_simulator(); #endif - - // Renew the alarm for a minute from the previous one (ensures no drift) - movement_volatile_state.minute_counter += watch_rtc_get_ticks_per_minute(); - watch_rtc_register_comp_callback(cb_minute_alarm_fired, movement_volatile_state.minute_counter, MINUTE_TIMEOUT); } void cb_tick(void) { diff --git a/watch-library/hardware/watch/watch_rtc.c b/watch-library/hardware/watch/watch_rtc.c index 8d5255ad..8bdc1c50 100644 --- a/watch-library/hardware/watch/watch_rtc.c +++ b/watch-library/hardware/watch/watch_rtc.c @@ -207,7 +207,7 @@ void watch_rtc_schedule_next_comp(void) { } // Because of the hardware, the soonest we can schedule is the next tick - curr_counter +=1; + // curr_counter +=1; bool schedule_any = false; rtc_counter_t comp_counter; @@ -278,9 +278,8 @@ void watch_rtc_disable_comp_callback_no_schedule(uint8_t index) { void watch_rtc_callback(uint16_t interrupt_cause) { // First read all relevant registers, to ensure no changes occurr during the callbacks - uint16_t interrupt_enabled = RTC->MODE0.INTENSET.reg; rtc_counter_t counter = watch_rtc_get_counter(); - + uint16_t interrupt_enabled = (uint16_t)RTC->MODE0.INTENSET.reg; if ((interrupt_cause & interrupt_enabled) & RTC_MODE0_INTFLAG_PER_Msk) { // handle the tick callback first, it's what we do the most. @@ -309,12 +308,12 @@ void watch_rtc_callback(uint16_t interrupt_cause) { if ((interrupt_cause & interrupt_enabled) & RTC_MODE0_INTFLAG_CMP0) { // The comp interrupt is generated one tick after the matched counter - rtc_counter_t comp_counter = counter - 1; + // rtc_counter_t comp_counter = counter - 1; for (uint8_t index = 0; index < WATCH_RTC_N_COMP_CB; ++index) { // Give it a little bit of wiggle room, if a comp callback is enabled and is just passed if (comp_callbacks[index].enabled && - (comp_counter - comp_callbacks[index].counter) <= RTC_CNT_HZ + (counter - comp_callbacks[index].counter) <= RTC_CNT_HZ ) { comp_callbacks[index].enabled = false; comp_callbacks[index].callback(); From 96682a513b18ac27874b05d60cd3ad771c890e70 Mon Sep 17 00:00:00 2001 From: Alessandro Genova Date: Sat, 9 Aug 2025 20:21:10 -0400 Subject: [PATCH 120/179] Undo some workarounds implemented to fix race conditions that are no longer needed --- watch-library/hardware/watch/watch_rtc.c | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/watch-library/hardware/watch/watch_rtc.c b/watch-library/hardware/watch/watch_rtc.c index 8bdc1c50..b3c61efb 100644 --- a/watch-library/hardware/watch/watch_rtc.c +++ b/watch-library/hardware/watch/watch_rtc.c @@ -207,7 +207,7 @@ void watch_rtc_schedule_next_comp(void) { } // Because of the hardware, the soonest we can schedule is the next tick - // curr_counter +=1; + curr_counter += 1; bool schedule_any = false; rtc_counter_t comp_counter; @@ -278,7 +278,7 @@ void watch_rtc_disable_comp_callback_no_schedule(uint8_t index) { void watch_rtc_callback(uint16_t interrupt_cause) { // First read all relevant registers, to ensure no changes occurr during the callbacks - rtc_counter_t counter = watch_rtc_get_counter(); + rtc_counter_t comp_counter = RTC->MODE0.COMP[0].reg; uint16_t interrupt_enabled = (uint16_t)RTC->MODE0.INTENSET.reg; if ((interrupt_cause & interrupt_enabled) & RTC_MODE0_INTFLAG_PER_Msk) { @@ -307,14 +307,8 @@ void watch_rtc_callback(uint16_t interrupt_cause) { } if ((interrupt_cause & interrupt_enabled) & RTC_MODE0_INTFLAG_CMP0) { - // The comp interrupt is generated one tick after the matched counter - // rtc_counter_t comp_counter = counter - 1; - for (uint8_t index = 0; index < WATCH_RTC_N_COMP_CB; ++index) { - // Give it a little bit of wiggle room, if a comp callback is enabled and is just passed - if (comp_callbacks[index].enabled && - (counter - comp_callbacks[index].counter) <= RTC_CNT_HZ - ) { + if (comp_callbacks[index].enabled && comp_counter == comp_callbacks[index].counter) { comp_callbacks[index].enabled = false; comp_callbacks[index].callback(); } From 42fdad78d5c157f6c8e1c8191e088c56bed15860 Mon Sep 17 00:00:00 2001 From: Alessandro Genova Date: Tue, 12 Aug 2025 22:28:17 -0400 Subject: [PATCH 121/179] Add more theoretical checks to ensure we don't miss out on any comp timers Even without this "fix" the watch seems to be extremely stable and haven't seen any missed timers over several days test. So this might not actually be needed, but it should make things even safer. --- watch-library/hardware/watch/watch_rtc.c | 28 ++++++++++++++---------- 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/watch-library/hardware/watch/watch_rtc.c b/watch-library/hardware/watch/watch_rtc.c index b3c61efb..1b0b5f49 100644 --- a/watch-library/hardware/watch/watch_rtc.c +++ b/watch-library/hardware/watch/watch_rtc.c @@ -39,6 +39,8 @@ static const uint32_t RTC_CNT_DIV = RTC_OSC_DIV - RTC_PRESCALER_DIV; // 7 static const uint32_t RTC_CNT_TICKS_PER_MINUTE = RTC_CNT_HZ * 60; static const uint32_t RTC_CNT_TICKS_PER_HOUR = RTC_CNT_TICKS_PER_MINUTE * 60; +static const uint32_t RTC_COMP_GRACE_PERIOD = 4; + static const int TB_BKUP_REG = 7; #define WATCH_RTC_N_COMP_CB 8 @@ -200,14 +202,10 @@ void watch_rtc_disable_all_periodic_callbacks(void) { void watch_rtc_schedule_next_comp(void) { rtc_counter_t curr_counter = watch_rtc_get_counter(); - // If there is already a pending comp interrupt for this very tick, let it fire - // And this function will be called again as soon as the interrupt fires. - if (curr_counter == scheduled_comp_counter) { - return; - } - // Because of the hardware, the soonest we can schedule is the next tick - curr_counter += 1; + // We want to ensure we never miss any registered callbacks, + // so if a callback counter has just passed but didn't fire, give it a chance to fire. + rtc_counter_t lax_curr_counter = curr_counter - RTC_COMP_GRACE_PERIOD; bool schedule_any = false; rtc_counter_t comp_counter; @@ -215,7 +213,7 @@ void watch_rtc_schedule_next_comp(void) { for (uint8_t index = 0; index < WATCH_RTC_N_COMP_CB; ++index) { if (comp_callbacks[index].enabled) { - rtc_counter_t diff = comp_callbacks[index].counter - curr_counter; + rtc_counter_t diff = comp_callbacks[index].counter - lax_curr_counter; if (diff <= min_diff) { min_diff = diff; comp_counter = comp_callbacks[index].counter; @@ -225,13 +223,17 @@ void watch_rtc_schedule_next_comp(void) { } if (schedule_any) { - // If we are changing the comp counter at the front of the line + // If we are changing the comp counter at the front of the line, don't schedule a comp interrupt for a counter that is too close to now if (comp_counter != scheduled_comp_counter) { + rtc_counter_t earliest_comp_counter = curr_counter + RTC_COMP_GRACE_PERIOD; + if ((earliest_comp_counter - lax_curr_counter) > (comp_counter - lax_curr_counter)) { + comp_counter = earliest_comp_counter; + } scheduled_comp_counter = comp_counter; rtc_enable_compare_interrupt(comp_counter); } } else { - scheduled_comp_counter = curr_counter - 2; + scheduled_comp_counter = lax_curr_counter - RTC_COMP_GRACE_PERIOD; rtc_disable_compare_interrupt(); } } @@ -278,7 +280,7 @@ void watch_rtc_disable_comp_callback_no_schedule(uint8_t index) { void watch_rtc_callback(uint16_t interrupt_cause) { // First read all relevant registers, to ensure no changes occurr during the callbacks - rtc_counter_t comp_counter = RTC->MODE0.COMP[0].reg; + rtc_counter_t curr_counter = watch_rtc_get_counter(); uint16_t interrupt_enabled = (uint16_t)RTC->MODE0.INTENSET.reg; if ((interrupt_cause & interrupt_enabled) & RTC_MODE0_INTFLAG_PER_Msk) { @@ -308,7 +310,9 @@ void watch_rtc_callback(uint16_t interrupt_cause) { if ((interrupt_cause & interrupt_enabled) & RTC_MODE0_INTFLAG_CMP0) { for (uint8_t index = 0; index < WATCH_RTC_N_COMP_CB; ++index) { - if (comp_callbacks[index].enabled && comp_counter == comp_callbacks[index].counter) { + if (comp_callbacks[index].enabled && + (curr_counter - comp_callbacks[index].counter) < (RTC_COMP_GRACE_PERIOD * 4) + ) { comp_callbacks[index].enabled = false; comp_callbacks[index].callback(); } From a34a266903303fa4afaeb07c98c13396b7501b39 Mon Sep 17 00:00:00 2001 From: Alessandro Genova Date: Tue, 12 Aug 2025 22:25:47 -0400 Subject: [PATCH 122/179] Add a face to debug some counter32 metrics --- movement_config.h | 5 +- movement_faces.h | 1 + watch-faces.mk | 1 + watch-faces/demo/rtccount_face.c | 196 +++++++++++++++++++++++++++++++ watch-faces/demo/rtccount_face.h | 47 ++++++++ 5 files changed, 248 insertions(+), 2 deletions(-) create mode 100644 watch-faces/demo/rtccount_face.c create mode 100644 watch-faces/demo/rtccount_face.h diff --git a/movement_config.h b/movement_config.h index 0da8da43..f5f6b2b4 100644 --- a/movement_config.h +++ b/movement_config.h @@ -38,7 +38,8 @@ const watch_face_t watch_faces[] = { temperature_display_face, voltage_face, settings_face, - set_time_face + set_time_face, + rtccount_face, }; #define MOVEMENT_NUM_FACES (sizeof(watch_faces) / sizeof(watch_face_t)) @@ -49,7 +50,7 @@ const watch_face_t watch_faces[] = { * Some folks also like to use this to hide the preferences and time set faces from the normal rotation. * If you don't want any faces to be excluded, set this to 0 and a long Mode press will have no effect. */ -#define MOVEMENT_SECONDARY_FACE_INDEX (MOVEMENT_NUM_FACES - 4) +#define MOVEMENT_SECONDARY_FACE_INDEX (MOVEMENT_NUM_FACES - 5) /* Custom hourly chime tune. Check movement_custom_signal_tunes.h for options. */ #define SIGNAL_TUNE_DEFAULT diff --git a/movement_faces.h b/movement_faces.h index b75d5d7e..164dbb4c 100644 --- a/movement_faces.h +++ b/movement_faces.h @@ -79,4 +79,5 @@ #include "lander_face.h" #include "simon_face.h" #include "ping_face.h" +#include "rtccount_face.h" // New includes go above this line. diff --git a/watch-faces.mk b/watch-faces.mk index 5fd366a0..69ec4e9c 100644 --- a/watch-faces.mk +++ b/watch-faces.mk @@ -22,6 +22,7 @@ SRCS += \ ./watch-faces/demo/character_set_face.c \ ./watch-faces/demo/light_sensor_face.c \ ./watch-faces/demo/peek_memory_face.c \ + ./watch-faces/demo/rtccount_face.c \ ./watch-faces/sensor/accelerometer_status_face.c \ ./watch-faces/sensor/temperature_display_face.c \ ./watch-faces/sensor/temperature_logging_face.c \ diff --git a/watch-faces/demo/rtccount_face.c b/watch-faces/demo/rtccount_face.c new file mode 100644 index 00000000..f08cde5b --- /dev/null +++ b/watch-faces/demo/rtccount_face.c @@ -0,0 +1,196 @@ +/* + * MIT License + * + * Copyright (c) 2022 Joey Castillo + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +#include +#include +#include "rtccount_face.h" +#include "watch.h" +#include "sam.h" +#include "watch_utility.h" +#include "watch_common_display.h" +#include "watch_rtc.h" + +typedef enum { + RTCCOUNT_STATUS_COUNTER = 0, + RTCCOUNT_STATUS_COUNTER_SUB, + RTCCOUNT_STATUS_MINUTES, + RTCCOUNT_STATUS_MINUTES_DIFF, + RTCCOUNT_STATUS_NUMBER +} rtccount_face_status_t; + +typedef struct { + rtccount_face_status_t status; + uint8_t frequency; + uint32_t n_top_of_minute; + uint32_t ref_timestamp; +} rtccount_state_t; + +static const uint32_t COUNTER_MASK = (1 << 19) - 1; + +static void _rtccount_face_draw(movement_event_t event, rtccount_state_t* state) { + uint32_t counter = watch_rtc_get_counter(); + + char buf[11] = " 000000\0"; + switch (state->status) { + case RTCCOUNT_STATUS_COUNTER: { + buf[0] = 'C'; + break; + } + + case RTCCOUNT_STATUS_COUNTER_SUB: { + buf[0] = 'S'; + break; + } + + case RTCCOUNT_STATUS_MINUTES: { + buf[0] = 'M'; + break; + } + + case RTCCOUNT_STATUS_MINUTES_DIFF: { + buf[0] = 'D'; + break; + } + + default: + break; + } + + watch_display_string(buf, 0); + + snprintf(buf, sizeof(buf), "%u", event.subsecond); + uint32_t len = strlen(buf); + watch_display_string(buf, 4 - len); + + switch (state->status) { + case RTCCOUNT_STATUS_COUNTER: { + snprintf(buf, sizeof(buf), "%lu", counter & COUNTER_MASK); + + size_t len = strlen(buf); + + watch_display_string(buf, 10 - len); + break; + } + + case RTCCOUNT_STATUS_COUNTER_SUB: { + snprintf(buf, sizeof(buf), "%lu", counter & 127); + + size_t len = strlen(buf); + + watch_display_string(buf, 10 - len); + break; + } + + case RTCCOUNT_STATUS_MINUTES: { + snprintf(buf, sizeof(buf), "%lu", state->n_top_of_minute & COUNTER_MASK); + + size_t len = strlen(buf); + + watch_display_string(buf, 10 - len); + break; + } + + case RTCCOUNT_STATUS_MINUTES_DIFF: { + uint32_t elapsed_minutes = (movement_get_utc_timestamp() - state->ref_timestamp) / 60; + + snprintf(buf, sizeof(buf), "%lu", (elapsed_minutes - state->n_top_of_minute) & COUNTER_MASK); + + size_t len = strlen(buf); + + watch_display_string(buf, 10 - len); + break; + } + + default: + break; + } +} + +void rtccount_face_setup(uint8_t watch_face_index, void ** context_ptr) { + (void) watch_face_index; + if (*context_ptr == NULL) { + *context_ptr = malloc(sizeof(rtccount_state_t)); + memset(*context_ptr, 0, sizeof(rtccount_state_t)); + rtccount_state_t *state = (rtccount_state_t *) *context_ptr; + state->status = RTCCOUNT_STATUS_COUNTER; + state->frequency = 1; + state->n_top_of_minute = 0; + rtc_date_time_t datetime = movement_get_utc_date_time(); + state->ref_timestamp = movement_get_utc_timestamp() - datetime.unit.second; + } +} + +void rtccount_face_activate(void *context) { + rtccount_state_t* state = (rtccount_state_t*)context; + movement_request_tick_frequency(state->frequency); +} + +bool rtccount_face_loop(movement_event_t event, void *context) { + rtccount_state_t* state = (rtccount_state_t*)context; + + switch (event.event_type) { + case EVENT_BACKGROUND_TASK: + state->n_top_of_minute += 1; + break; + case EVENT_ALARM_BUTTON_UP: + if (state->frequency == 128) { + state->frequency = 1; + } else { + state->frequency *= 2; + } + + movement_request_tick_frequency(state->frequency); + break; + case EVENT_ALARM_LONG_PRESS: + state->n_top_of_minute = 0; + rtc_date_time_t datetime = movement_get_utc_date_time(); + state->ref_timestamp = movement_get_utc_timestamp() - datetime.unit.second; + break; + case EVENT_LIGHT_BUTTON_DOWN: + state->status = (state->status + 1) % RTCCOUNT_STATUS_NUMBER; + _rtccount_face_draw(event, state); + break; + case EVENT_ACTIVATE: + case EVENT_TICK: + _rtccount_face_draw(event, state); + break; + default: + movement_default_loop_handler(event); + break; + } + + return true; +} + +void rtccount_face_resign(void *context) { + (void) context; + movement_request_tick_frequency(1); +} + +movement_watch_face_advisory_t rtccount_face_advise(void *context) { + (void) context; + movement_watch_face_advisory_t retval = { 0 }; + retval.wants_background_task = true; + return retval; +} diff --git a/watch-faces/demo/rtccount_face.h b/watch-faces/demo/rtccount_face.h new file mode 100644 index 00000000..59f11929 --- /dev/null +++ b/watch-faces/demo/rtccount_face.h @@ -0,0 +1,47 @@ +/* + * MIT License + * + * Copyright (c) 2022 Joey Castillo + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +#pragma once + +/* + * RTCCOUNT FACE + * + * A test face to inspect some metrics of the rtc-counter32 mode. + */ + +#include "movement.h" + +void rtccount_face_setup(uint8_t watch_face_index, void ** context_ptr); +void rtccount_face_activate(void *context); +bool rtccount_face_loop(movement_event_t event, void *context); +void rtccount_face_resign(void *context); +movement_watch_face_advisory_t rtccount_face_advise(void *context); + +#define rtccount_face ((const watch_face_t){ \ + rtccount_face_setup, \ + rtccount_face_activate, \ + rtccount_face_loop, \ + rtccount_face_resign, \ + rtccount_face_advise, \ +}) From c37d40d086bf5d6d8694d6b94b30f9bb6dc8bda6 Mon Sep 17 00:00:00 2001 From: Alessandro Genova Date: Thu, 14 Aug 2025 21:40:02 -0400 Subject: [PATCH 123/179] Make the simulator play beeps a lot more reliably. --- watch-library/simulator/watch/watch_tcc.c | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/watch-library/simulator/watch/watch_tcc.c b/watch-library/simulator/watch/watch_tcc.c index 5e5dd013..c7b1182d 100644 --- a/watch-library/simulator/watch/watch_tcc.c +++ b/watch-library/simulator/watch/watch_tcc.c @@ -150,7 +150,11 @@ void watch_enable_buzzer(void) { buzzer_period = NotePeriods[BUZZER_NOTE_A4]; EM_ASM({ - Module['audioContext'] = new (window.AudioContext || window.webkitAudioContext)(); + // "It's recommended to create one AudioContext and reuse it instead of initializing a new one each time." + // https://developer.mozilla.org/en-US/docs/Web/API/AudioContext + if (!Module['audioContext']) { + Module['audioContext'] = new (window.AudioContext || window.webkitAudioContext)(); + } }); } @@ -163,13 +167,6 @@ void watch_set_buzzer_period_and_duty_cycle(uint32_t period, uint8_t duty_cycle) void watch_disable_buzzer(void) { buzzer_enabled = false; buzzer_period = NotePeriods[BUZZER_NOTE_A4]; - - EM_ASM({ - if (Module['audioContext']) { - Module['audioContext'].close(); - Module['audioContext'] = null; - } - }); } void watch_set_buzzer_on(void) { From 3caef587df7416108c04f9431a1bddf7f5b5ccd2 Mon Sep 17 00:00:00 2001 From: Alessandro Genova Date: Fri, 15 Aug 2025 00:53:23 -0400 Subject: [PATCH 124/179] Play an arbitrary stream on the buzzer without blocking --- watch-library/hardware/watch/watch_tcc.c | 70 ++++++++++++++++++++++- watch-library/shared/watch/watch_tcc.h | 49 ++++++++++++++++ watch-library/simulator/watch/watch_tcc.c | 68 +++++++++++++++++++++- 3 files changed, 183 insertions(+), 4 deletions(-) diff --git a/watch-library/hardware/watch/watch_tcc.c b/watch-library/hardware/watch/watch_tcc.c index 0e1fdd9d..8624ffcc 100644 --- a/watch-library/hardware/watch/watch_tcc.c +++ b/watch-library/hardware/watch/watch_tcc.c @@ -28,11 +28,15 @@ #include "tc.h" void _watch_enable_tcc(void); +static void (*_cb_tc0)(void) = NULL; void cb_watch_buzzer_seq(void); +void cb_watch_buzzer_raw_source(void); static uint16_t _seq_position; static int8_t _tone_ticks, _repeat_counter; static int8_t *_sequence; +static watch_buzzer_raw_source_t _raw_source; +static void* _userdata; static uint8_t _volume; static void (*_cb_finished)(void); static watch_cb_t _cb_start_global = NULL; @@ -94,6 +98,7 @@ void watch_buzzer_play_sequence_with_volume(int8_t *note_sequence, void (*callba _repeat_counter = -1; // prepare buzzer + _cb_tc0 = cb_watch_buzzer_seq; // setup TC0 timer _tc0_initialize(); // start the timer (for the 64 hz callback) @@ -138,6 +143,67 @@ void cb_watch_buzzer_seq(void) { } else _tone_ticks--; } +void watch_buzzer_play_raw_source(watch_buzzer_raw_source_t raw_source, void* userdata, watch_cb_t callback_on_end) { + watch_buzzer_play_raw_source_with_volume(raw_source, userdata, callback_on_end, WATCH_BUZZER_VOLUME_LOUD); +} + +void watch_buzzer_play_raw_source_with_volume(watch_buzzer_raw_source_t raw_source, void* userdata, watch_cb_t callback_on_end, watch_buzzer_volume_t volume) { + // Abort any previous sequence + watch_buzzer_abort_sequence(); + + _buzzer_is_active = true; + + if (_cb_start_global) { + _cb_start_global(); + } + + watch_enable_buzzer_and_leds(); + + watch_set_buzzer_off(); + _raw_source = raw_source; + _userdata = userdata; + _cb_finished = callback_on_end; + _volume = volume == WATCH_BUZZER_VOLUME_SOFT ? 5 : 25; + _seq_position = 0; + _tone_ticks = 0; + // prepare buzzer + + _cb_tc0 = cb_watch_buzzer_raw_source; + // setup TC0 timer + _tc0_initialize(); + // start the timer (for the 64 hz callback) + _tc0_start(); +} + +void cb_watch_buzzer_raw_source(void) { + // callback for reading the note sequence + uint16_t period; + uint16_t duration; + bool done; + + if (_tone_ticks == 0) { + done = _raw_source(_seq_position, _userdata, &period, &duration); + + if (done) { + // end the sequence + watch_buzzer_abort_sequence(); + } else { + if (period == WATCH_BUZZER_PERIOD_REST) { + watch_set_buzzer_off(); + } else { + watch_set_buzzer_period_and_duty_cycle(period, _volume); + watch_set_buzzer_on(); + } + + // set duration ticks and move to next tone + _tone_ticks = duration; + _seq_position += 1; + } + } else { + _tone_ticks--; + } +} + void watch_buzzer_abort_sequence(void) { // ends/aborts the sequence if (!_buzzer_is_active) { @@ -169,7 +235,9 @@ void watch_buzzer_register_global_callbacks(watch_cb_t cb_start, watch_cb_t cb_s void irq_handler_tc0(void) { // interrupt handler for TC0 (globally!) - cb_watch_buzzer_seq(); + if (_cb_tc0) { + _cb_tc0(); + } TC0->COUNT8.INTFLAG.reg |= TC_INTFLAG_OVF; } diff --git a/watch-library/shared/watch/watch_tcc.h b/watch-library/shared/watch/watch_tcc.h index 99c56dcc..e87e53bf 100644 --- a/watch-library/shared/watch/watch_tcc.h +++ b/watch-library/shared/watch/watch_tcc.h @@ -126,6 +126,10 @@ typedef enum { BUZZER_NOTE_REST ///< no sound } watch_buzzer_note_t; +#define WATCH_BUZZER_PERIOD_REST 0 + +typedef bool (*watch_buzzer_raw_source_t)(uint16_t position, void* userdata, uint16_t* period, uint16_t* duration); + /** @brief Returns true if either the buzzer or the LED driver is enabled. * @details Both the buzzer and the LED use the TCC peripheral to drive their behavior. This function returns true if that * peripheral is enabled. You can use this function to determine whether you need to call the watch_enable_leds or @@ -210,6 +214,51 @@ void watch_buzzer_play_sequence(int8_t *note_sequence, void (*callback_on_end)(v */ void watch_buzzer_play_sequence_with_volume(int8_t *note_sequence, void (*callback_on_end)(void), watch_buzzer_volume_t volume); +/** @brief Plays the given raw buzzer source function in a non-blocking way. + * + * @details This function plays audio data generated by a raw source callback function, + * allowing for precise control over buzzer timing and frequency. The raw source + * function is called repeatedly to generate audio samples, each containing a + * period and duration for the buzzer tone. + * Useful for applications such as chirpy, so that they won't need to allocate a + * long note sequence, and we will also take care of all the timing logic. + * + * @param raw_source Pointer to the callback function that generates raw buzzer data. + * The function should take a position parameter and return true if + * more data is available, false if end of sequence is reached. + * Parameters: + * - position: Current position in the audio sequence (0-based) + * - userdata: User-provided data passed through to the callback + * - period: Pointer to store the period (in microseconds) for the tone + * - duration: Pointer to store the duration (in microseconds) for the tone + * @param userdata Pointer to user data that will be passed to the raw_source callback + * @param callback_on_end A pointer to a callback function to be invoked when the sequence has finished playing. + */ +void watch_buzzer_play_raw_source(watch_buzzer_raw_source_t raw_source, void* userdata, watch_cb_t callback_on_end); + +/** @brief Plays the given raw buzzer source function in a non-blocking way. + * + * @details This function plays audio data generated by a raw source callback function, + * allowing for precise control over buzzer timing and frequency. The raw source + * function is called repeatedly to generate audio samples, each containing a + * period and duration for the buzzer tone. + * Useful for applications such as chirpy, so that they won't need to allocate a + * long note sequence, and we will also take care of all the timing logic. + * + * @param raw_source Pointer to the callback function that generates raw buzzer data. + * The function should take a position parameter and return true if + * more data is available, false if end of sequence is reached. + * Parameters: + * - position: Current position in the audio sequence (0-based) + * - userdata: User-provided data passed through to the callback + * - period: Pointer to store the period (in microseconds) for the tone + * - duration: Pointer to store the duration (in microseconds) for the tone + * @param userdata Pointer to user data that will be passed to the raw_source callback + * @param callback_on_end A pointer to a callback function to be invoked when the sequence has finished playing. + * @param volume either WATCH_BUZZER_VOLUME_SOFT or WATCH_BUZZER_VOLUME_LOUD + */ +void watch_buzzer_play_raw_source_with_volume(watch_buzzer_raw_source_t raw_source, void* userdata, watch_cb_t callback_on_end, watch_buzzer_volume_t volume); + /** @brief Aborts a playing sequence. */ void watch_buzzer_abort_sequence(void); diff --git a/watch-library/simulator/watch/watch_tcc.c b/watch-library/simulator/watch/watch_tcc.c index c7b1182d..1ca4bbf0 100644 --- a/watch-library/simulator/watch/watch_tcc.c +++ b/watch-library/simulator/watch/watch_tcc.c @@ -32,11 +32,14 @@ static volatile bool buzzer_enabled = false; static uint32_t buzzer_period; void cb_watch_buzzer_seq(void *userData); +void cb_watch_buzzer_raw_source(void *userData); static uint16_t _seq_position; static int8_t _tone_ticks, _repeat_counter; static volatile long _em_interval_id = 0; static int8_t *_sequence; +static watch_buzzer_raw_source_t _raw_source; +static void* _userdata; static uint8_t _volume; static void (*_cb_finished)(void); static watch_cb_t _cb_start_global = NULL; @@ -57,6 +60,10 @@ void watch_buzzer_play_sequence(int8_t *note_sequence, void (*callback_on_end)(v void watch_buzzer_play_sequence_with_volume(int8_t *note_sequence, void (*callback_on_end)(void), watch_buzzer_volume_t volume) { watch_buzzer_abort_sequence(); + // prepare buzzer + watch_enable_buzzer(); + watch_set_buzzer_off(); + _buzzer_is_active = true; if (_cb_start_global) { @@ -69,9 +76,6 @@ void watch_buzzer_play_sequence_with_volume(int8_t *note_sequence, void (*callba _seq_position = 0; _tone_ticks = 0; _repeat_counter = -1; - // prepare buzzer - watch_enable_buzzer(); - watch_set_buzzer_off(); // initiate 64 hz callback _em_interval_id = emscripten_set_interval(cb_watch_buzzer_seq, (double)(1000/64), (void *)NULL); } @@ -117,6 +121,64 @@ void cb_watch_buzzer_seq(void *userData) { } else _tone_ticks--; } +void watch_buzzer_play_raw_source(watch_buzzer_raw_source_t raw_source, void* userdata, watch_cb_t callback_on_end) { + watch_buzzer_play_raw_source_with_volume(raw_source, userdata, callback_on_end, WATCH_BUZZER_VOLUME_LOUD); +} + +void watch_buzzer_play_raw_source_with_volume(watch_buzzer_raw_source_t raw_source, void* userdata, watch_cb_t callback_on_end, watch_buzzer_volume_t volume) { + watch_buzzer_abort_sequence(); + + // prepare buzzer + watch_enable_buzzer(); + watch_set_buzzer_off(); + + _buzzer_is_active = true; + + if (_cb_start_global) { + _cb_start_global(); + } + + _raw_source = raw_source; + _userdata = userdata; + _cb_finished = callback_on_end; + _volume = volume == WATCH_BUZZER_VOLUME_SOFT ? 5 : 25; + _seq_position = 0; + _tone_ticks = 0; + + // initiate 64 hz callback + _em_interval_id = emscripten_set_interval(cb_watch_buzzer_raw_source, (double)(1000/64), (void *)NULL); +} + +void cb_watch_buzzer_raw_source(void *userData) { + // callback for reading the note sequence + (void) userData; + uint16_t period; + uint16_t duration; + bool done; + + if (_tone_ticks == 0) { + done = _raw_source(_seq_position, _userdata, &period, &duration); + + if (done) { + // end the sequence + watch_buzzer_abort_sequence(); + } else { + if (period == WATCH_BUZZER_PERIOD_REST) { + watch_set_buzzer_off(); + } else { + watch_set_buzzer_period_and_duty_cycle(period, _volume); + watch_set_buzzer_on(); + } + + // set duration ticks and move to next tone + _tone_ticks = duration; + _seq_position += 1; + } + } else { + _tone_ticks--; + } +} + void watch_buzzer_abort_sequence(void) { // ends/aborts the sequence if (_em_interval_id) _em_interval_stop(); From 557a785b1a091e5bf04b848b7909497b98f3cc57 Mon Sep 17 00:00:00 2001 From: Alessandro Genova Date: Fri, 15 Aug 2025 00:54:48 -0400 Subject: [PATCH 125/179] Fix chirpy to work with rtc-counter32 --- watch-faces/io/chirpy_demo_face.c | 167 ++++++++++-------------------- 1 file changed, 54 insertions(+), 113 deletions(-) diff --git a/watch-faces/io/chirpy_demo_face.c b/watch-faces/io/chirpy_demo_face.c index 673da201..3a7ff300 100644 --- a/watch-faces/io/chirpy_demo_face.c +++ b/watch-faces/io/chirpy_demo_face.c @@ -47,9 +47,6 @@ typedef struct { // Selected program chirpy_demo_program_t program; - // Helps us handle 1/64 ticks during transmission; including countdown timer - chirpy_tick_state_t tick_state; - // Used by chirpy encoder during transmission chirpy_encoder_state_t encoder_state; @@ -150,46 +147,10 @@ static void _cdf_update_lcd(chirpy_demo_state_t *state) { } } -static void _cdf_quit_chirping(chirpy_demo_state_t *state) { - state->mode = CDM_CHOOSE; - watch_set_buzzer_off(); - watch_clear_indicator(WATCH_INDICATOR_BELL); - movement_request_tick_frequency(1); -} - -static void _cdf_scale_tick(void *context) { - chirpy_demo_state_t *state = (chirpy_demo_state_t *)context; - chirpy_tick_state_t *tick_state = &state->tick_state; - - // Scale goes in 200Hz increments from 700 Hz to 12.3 kHz -> 58 steps - if (tick_state->seq_pos == 58) { - _cdf_quit_chirping(state); - return; - } - uint32_t freq = 700 + tick_state->seq_pos * 200; - uint32_t period = 1000000 / freq; - watch_set_buzzer_period_and_duty_cycle(period, 25); - watch_set_buzzer_on(); - ++tick_state->seq_pos; -} - -static void _cdf_data_tick(void *context) { - chirpy_demo_state_t *state = (chirpy_demo_state_t *)context; - - uint8_t tone = chirpy_get_next_tone(&state->encoder_state); - // Transmission over? - if (tone == 255) { - _cdf_quit_chirping(state); - return; - } - uint16_t period = chirpy_get_tone_period(tone); - watch_set_buzzer_period_and_duty_cycle(period, 25); - watch_set_buzzer_on(); -} - static uint8_t *curr_data_ptr; static uint16_t curr_data_ix; static uint16_t curr_data_len; +static chirpy_demo_state_t *curr_state; static uint8_t _cdf_get_next_byte(uint8_t *next_byte) { if (curr_data_ix == curr_data_len) @@ -199,59 +160,60 @@ static uint8_t _cdf_get_next_byte(uint8_t *next_byte) { return 1; } -static void _cdf_countdown_tick(void *context) { - chirpy_demo_state_t *state = (chirpy_demo_state_t *)context; - chirpy_tick_state_t *tick_state = &state->tick_state; - - // Countdown over: start actual broadcast - if (tick_state->seq_pos == 8 * 3) { - tick_state->tick_compare = 3; - tick_state->tick_count = -1; - tick_state->seq_pos = 0; - // We'll be chirping out a scale - if (false) { // state->program == CDP_CLEAR) { - tick_state->tick_fun = _cdf_scale_tick; - } - // We'll be chirping out data - else { - // Set up the encoder - chirpy_init_encoder(&state->encoder_state, _cdf_get_next_byte); - tick_state->tick_fun = _cdf_data_tick; - // Set up the data - curr_data_ix = 0; - if (state->program == CDP_INFO_SHORT) { - curr_data_ptr = short_data; - curr_data_len = short_data_len; - } else if (state->program == CDP_INFO_LONG) { - curr_data_ptr = long_data_str; - curr_data_len = strlen((const char *)long_data_str); - } else if (state->program == CDP_INFO_NANOSEC) { - curr_data_ptr = activity_buffer; - curr_data_len = activity_buffer_size; - } - } - return; +static void _cdf_on_chirping_done(void) { + if (curr_state) { + curr_state->mode = CDM_CHOOSE; } - // Sound or turn off buzzer - if ((tick_state->seq_pos % 8) == 0) { - watch_set_buzzer_period_and_duty_cycle(NotePeriods[BUZZER_NOTE_A5], 25); - watch_set_buzzer_on(); - } else if ((tick_state->seq_pos % 8) == 1) { - watch_set_buzzer_off(); - } - ++tick_state->seq_pos; + watch_clear_indicator(WATCH_INDICATOR_BELL); } -static void _cdm_setup_chirp(chirpy_demo_state_t *state) { - // We want frequent callbacks from now on - movement_request_tick_frequency(64); +static bool _cdm_raw_source_fn(uint16_t position, void* userdata, uint16_t* period, uint16_t* duration) { + // Beep countdown + if (position < 6) { + if (position % 2) { + *period = WATCH_BUZZER_PERIOD_REST; + *duration = 56; + } else { + *period = NotePeriods[BUZZER_NOTE_A5]; + *duration = 8; + } + return false; + } + + chirpy_demo_state_t *state = (chirpy_demo_state_t *)userdata; + + uint8_t tone = chirpy_get_next_tone(&state->encoder_state); + // Transmission over? + if (tone == 255) { + return true; + } + + *period = chirpy_get_tone_period(tone); + *duration = 3; + + return false; +} + +static void _cdm_start_transmission(chirpy_demo_state_t *state) { watch_set_indicator(WATCH_INDICATOR_BELL); state->mode = CDM_CHIRPING; - // Set up tick state; start with countdown - state->tick_state.tick_count = -1; - state->tick_state.tick_compare = 8; - state->tick_state.seq_pos = 0; - state->tick_state.tick_fun = _cdf_countdown_tick; + + // Set up the data + curr_state = state; + curr_data_ix = 0; + if (state->program == CDP_INFO_SHORT) { + curr_data_ptr = short_data; + curr_data_len = short_data_len; + } else if (state->program == CDP_INFO_LONG) { + curr_data_ptr = long_data_str; + curr_data_len = strlen((const char *)long_data_str); + } else if (state->program == CDP_INFO_NANOSEC) { + curr_data_ptr = activity_buffer; + curr_data_len = activity_buffer_size; + } + + chirpy_init_encoder(&state->encoder_state, _cdf_get_next_byte); + watch_buzzer_play_raw_source(_cdm_raw_source_fn, state, _cdf_on_chirping_done); } bool chirpy_demo_face_loop(movement_event_t event, void *context) { @@ -261,12 +223,7 @@ bool chirpy_demo_face_loop(movement_event_t event, void *context) { case EVENT_ACTIVATE: _cdf_update_lcd(state); break; - case EVENT_MODE_BUTTON_UP: - // Do not exit face while we're chirping - if (state->mode != CDM_CHIRPING) { - movement_move_to_next_face(); - } - break; + case EVENT_LIGHT_BUTTON_DOWN: case EVENT_LIGHT_BUTTON_UP: // We don't do light. break; @@ -286,10 +243,6 @@ bool chirpy_demo_face_loop(movement_event_t event, void *context) { state->program = CDP_CLEAR; _cdf_update_lcd(state); } - // If chirping: stoppit - else if (state->mode == CDM_CHIRPING) { - _cdf_quit_chirping(state); - } break; case EVENT_ALARM_LONG_PRESS: // If in choose mode: start chirping @@ -299,16 +252,7 @@ bool chirpy_demo_face_loop(movement_event_t event, void *context) { movement_force_led_off(); movement_move_to_next_face(); } else { - _cdm_setup_chirp(state); - } - } - break; - case EVENT_TICK: - if (state->mode == CDM_CHIRPING) { - ++state->tick_state.tick_count; - if (state->tick_state.tick_count == state->tick_state.tick_compare) { - state->tick_state.tick_count = 0; - state->tick_state.tick_fun(context); + _cdm_start_transmission(state); } } break; @@ -318,14 +262,11 @@ bool chirpy_demo_face_loop(movement_event_t event, void *context) { movement_move_to_face(0); } default: + movement_default_loop_handler(event); break; } - // Return true if the watch can enter standby mode. False needed when chirping. - if (state->mode == CDM_CHIRPING) - return false; - else - return true; + return true; } void chirpy_demo_face_resign(void *context) { From bf9d89106f6f0db838b90627185a0fd17bd5cd13 Mon Sep 17 00:00:00 2001 From: Alessandro Genova Date: Fri, 15 Aug 2025 21:34:29 -0400 Subject: [PATCH 126/179] Fix LED flickering while the buzzer is playing --- watch-library/hardware/watch/watch_tcc.c | 37 +++++++++++++++++------- 1 file changed, 26 insertions(+), 11 deletions(-) diff --git a/watch-library/hardware/watch/watch_tcc.c b/watch-library/hardware/watch/watch_tcc.c index 8624ffcc..fa81d074 100644 --- a/watch-library/hardware/watch/watch_tcc.c +++ b/watch-library/hardware/watch/watch_tcc.c @@ -43,6 +43,9 @@ static watch_cb_t _cb_start_global = NULL; static watch_cb_t _cb_stop_global = NULL; static volatile bool _led_is_active = false; static volatile bool _buzzer_is_active = false; +static volatile uint8_t _current_led_color[3] = {0, 0, 0}; + +static void _watch_set_led_duty_cycle(uint32_t period, uint8_t red, uint8_t green, uint8_t blue); static void _tcc_write_RUNSTDBY(bool value) { // enables or disables RUNSTDBY of the tcc @@ -276,6 +279,11 @@ void watch_enable_buzzer(void) { void watch_set_buzzer_period_and_duty_cycle(uint32_t period, uint8_t duty) { tcc_set_period(0, period, true); tcc_set_cc(0, (WATCH_BUZZER_TCC_CHANNEL) % 4, period / (100 / duty), true); + // The buzzer determines the period, which means that if the LED was active before it will flicker + // Update the LED duty cycle to match the new period required by the buzzer. + if (_led_is_active) { + _watch_set_led_duty_cycle(period, _current_led_color[0], _current_led_color[1], _current_led_color[2]); + } } void watch_disable_buzzer(void) { @@ -400,10 +408,27 @@ void watch_set_led_color(uint8_t red, uint8_t green) { #endif } +static void _watch_set_led_duty_cycle(uint32_t period, uint8_t red, uint8_t green, uint8_t blue) { + tcc_set_cc(0, (WATCH_RED_TCC_CHANNEL) % 4, ((period * (uint32_t)red * 1000ull) / 255000ull), true); +#ifdef WATCH_GREEN_TCC_CHANNEL + tcc_set_cc(0, (WATCH_GREEN_TCC_CHANNEL) % 4, ((period * (uint32_t)green * 1000ull) / 255000ull), true); +#else + (void) green; // silence warning +#endif +#ifdef WATCH_BLUE_TCC_CHANNEL + tcc_set_cc(0, (WATCH_BLUE_TCC_CHANNEL) % 4, ((period * (uint32_t)blue * 1000ull) / 255000ull), true); +#else + (void) blue; // silence warning +#endif +} + void watch_set_led_color_rgb(uint8_t red, uint8_t green, uint8_t blue) { bool turning_on = (red | green | blue) != 0; if (turning_on) { + _current_led_color[0] = red; + _current_led_color[1] = green; + _current_led_color[2] = blue; _led_is_active = true; watch_enable_buzzer_and_leds(); } else { @@ -412,17 +437,7 @@ void watch_set_led_color_rgb(uint8_t red, uint8_t green, uint8_t blue) { if (tcc_is_enabled(0)) { uint32_t period = tcc_get_period(0); - tcc_set_cc(0, (WATCH_RED_TCC_CHANNEL) % 4, ((period * (uint32_t)red * 1000ull) / 255000ull), true); -#ifdef WATCH_GREEN_TCC_CHANNEL - tcc_set_cc(0, (WATCH_GREEN_TCC_CHANNEL) % 4, ((period * (uint32_t)green * 1000ull) / 255000ull), true); -#else - (void) green; // silence warning -#endif -#ifdef WATCH_BLUE_TCC_CHANNEL - tcc_set_cc(0, (WATCH_BLUE_TCC_CHANNEL) % 4, ((period * (uint32_t)blue * 1000ull) / 255000ull), true); -#else - (void) blue; // silence warning -#endif + _watch_set_led_duty_cycle(period, red, green, blue); } if (!turning_on) { From 9d91ff3090bd6008afbd78261d824681998ff0f5 Mon Sep 17 00:00:00 2001 From: Alessandro Genova Date: Wed, 6 Aug 2025 21:29:44 -0400 Subject: [PATCH 127/179] Add an optional implementation of button debounce --- movement.c | 15 +++++++++++++++ movement_config.h | 6 ++++++ 2 files changed, 21 insertions(+) diff --git a/movement.c b/movement.c index 49df5023..aa90f034 100644 --- a/movement.c +++ b/movement.c @@ -67,6 +67,9 @@ typedef struct { movement_timeout_index_t timeout_index; volatile bool is_down; volatile rtc_counter_t down_timestamp; +#if MOVEMENT_DEBOUNCE_TICKS + volatile rtc_counter_t up_timestamp; +#endif } movement_button_t; /* Pieces of state that can be modified by the various interrupt callbacks. @@ -1270,12 +1273,24 @@ static movement_event_type_t _process_button_event(bool pin_level, movement_butt uint32_t counter = watch_rtc_get_counter(); +#if MOVEMENT_DEBOUNCE_TICKS + if ( + (counter - button->up_timestamp) <= MOVEMENT_DEBOUNCE_TICKS && + (counter - button->down_timestamp) <= MOVEMENT_DEBOUNCE_TICKS + ) { + return event_type; + } +#endif + button->is_down = pin_level; if (pin_level) { button->down_timestamp = counter; event_type = button->down_event; } else { +#if MOVEMENT_DEBOUNCE_TICKS + button->up_timestamp = counter; +#endif if ((counter - button->down_timestamp) >= MOVEMENT_LONG_PRESS_TICKS) { event_type = button->down_event + 3; } else { diff --git a/movement_config.h b/movement_config.h index f5f6b2b4..9633de87 100644 --- a/movement_config.h +++ b/movement_config.h @@ -101,4 +101,10 @@ const watch_face_t watch_faces[] = { */ #define MOVEMENT_DEFAULT_LED_DURATION 1 +/* Optionally debounce button presses (disable by default). + * A value of 4 is a good starting point if you have issues + * with multiple button presses firing. +*/ +#define MOVEMENT_DEBOUNCE_TICKS 0 + #endif // MOVEMENT_CONFIG_H_ From 6fe1b236a4854c0062930f52e27433e4242e9db4 Mon Sep 17 00:00:00 2001 From: Alessandro Genova Date: Fri, 15 Aug 2025 22:50:10 -0400 Subject: [PATCH 128/179] Minor improvement to movement_force_led_on logic --- movement.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/movement.c b/movement.c index aa90f034..f52050e0 100644 --- a/movement.c +++ b/movement.c @@ -400,8 +400,8 @@ void movement_force_led_on(uint8_t red, uint8_t green, uint8_t blue) { // this is hacky, we need a way for watch faces to set an arbitrary color and prevent Movement from turning it right back off. movement_state.light_on = true; watch_set_led_color_rgb(red, green, blue); - rtc_counter_t counter = watch_rtc_get_counter(); - watch_rtc_register_comp_callback_no_schedule(cb_led_timeout_interrupt, counter + 32767, LED_TIMEOUT); + // The led will stay on until movement_force_led_off is called, so disable the led timeout in case we were in the middle of it. + watch_rtc_disable_comp_callback_no_schedule(LED_TIMEOUT); movement_volatile_state.schedule_next_comp = true; } From 1b9624d042dc241ee08c9d290e8b199420183f93 Mon Sep 17 00:00:00 2001 From: Alessandro Genova Date: Sat, 26 Jul 2025 22:31:53 -0400 Subject: [PATCH 129/179] Make fast_stopwatch power efficiend by using the new counter32 --- .../complication/fast_stopwatch_face.c | 412 ++++++++---------- .../complication/fast_stopwatch_face.h | 11 +- 2 files changed, 193 insertions(+), 230 deletions(-) diff --git a/watch-faces/complication/fast_stopwatch_face.c b/watch-faces/complication/fast_stopwatch_face.c index ce998102..a4af13f3 100644 --- a/watch-faces/complication/fast_stopwatch_face.c +++ b/watch-faces/complication/fast_stopwatch_face.c @@ -2,6 +2,7 @@ * MIT License * * Copyright (c) 2022 Andreas Nebinger + * Copyright (c) 2025 Alessandro Genova * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -40,173 +41,203 @@ turns on on each button press or it doesn't. */ -#if __EMSCRIPTEN__ -#include -#include -#else -#include "tc.h" -#endif - -// distant future for background task: January 1, 2083 -static const watch_date_time_t distant_future = { - .unit = {0, 0, 0, 1, 1, 63} -}; - -static uint32_t _ticks; -static uint32_t _lap_ticks; -static uint8_t _blink_ticks; -static uint32_t _old_seconds; -static uint8_t _old_minutes; -static uint8_t _hours; -static bool _colon; -static bool _is_running; - -#if __EMSCRIPTEN__ - -static long _em_interval_id = 0; - -void em_cb_handler(void *userData) { - // interrupt handler for emscripten 128 Hz callbacks - (void) userData; - _ticks++; -} - -static void _cb_initialize() { } - -static inline void _cb_stop() { - emscripten_clear_interval(_em_interval_id); - _em_interval_id = 0; - _is_running = false; -} - -static inline void _cb_start() { - // initiate 128 hz callback - _em_interval_id = emscripten_set_interval(em_cb_handler, (double)(1000/128), (void *)NULL); -} - -#else - -static inline void _cb_start() { - // start the TC1 timer - tc_enable(1); - _is_running = true; -} - -static inline void _cb_stop() { - // stop the TC1 timer - tc_disable(1); - _is_running = false; -} - -static void _cb_initialize() { - tc_init(1, GENERIC_CLOCK_3, TC_PRESCALER_DIV4); - tc_set_counter_mode(1, TC_COUNTER_MODE_8BIT); - tc_set_run_in_standby(1, true); - _cb_stop(); - tc_count8_set_period(1, 1); // 1024 Hz divided by 4 divided by 2 results in a 128 Hz interrupt - /// FIXME: #SecondMovement, we need a gossamer wrapper for interrupts. - TC1->COUNT8.INTENSET.bit.OVF = 1; - NVIC_ClearPendingIRQ(TC1_IRQn); - NVIC_EnableIRQ (TC1_IRQn); -} - -void irq_handler_tc1(void); -void irq_handler_tc1(void) { - // interrupt handler for TC1 (globally!) - _ticks++; - TC1->COUNT8.INTFLAG.reg |= TC_INTFLAG_OVF; -} - -#endif +// Loosely implement the watch as a state machine +typedef enum { + SW_STATUS_IDLE = 0, + SW_STATUS_RUNNING, + SW_STATUS_RUNNING_LAPPING, + SW_STATUS_STOPPED, + SW_STATUS_STOPPED_LAPPING +} stopwatch_status_t; static inline void _button_beep() { // play a beep as confirmation for a button press (if applicable) if (movement_button_should_sound()) watch_buzzer_play_note_with_volume(BUZZER_NOTE_C7, 50, movement_button_volume()); } +// How quickly should the elapsing time be displayed? +// This is just for looks, timekeeping is always accurate to 128Hz +static const uint8_t DISPLAY_RUNNING_RATE = 32; + +static const uint32_t TWENTY_FOUR_HOURS = 24 * 60 * 60 * 128; + /// @brief Display minutes, seconds and fractions derived from 128 Hz tick counter /// on the lcd. /// @param ticks -static void _display_ticks(uint32_t ticks) { +static void _display_elapsed(uint32_t ticks) { + ticks = ticks % TWENTY_FOUR_HOURS; char buf[14]; uint8_t sec_100 = (ticks & 0x7F) * 100 / 128; uint32_t seconds = ticks >> 7; uint32_t minutes = seconds / 60; - if (_hours) { - sprintf(buf, "%2u", _hours); + uint32_t hours = minutes / 60; + if (hours) { + sprintf(buf, "%2u", hours); watch_display_text(WATCH_POSITION_TOP_RIGHT, buf); } else { watch_display_text(WATCH_POSITION_TOP_RIGHT, " "); } - sprintf(buf, "%02lu%02lu%02u", minutes, (seconds % 60), sec_100); + sprintf(buf, "%02lu%02lu%02u", (minutes % 60), (seconds % 60), sec_100); watch_display_text(WATCH_POSITION_BOTTOM, buf); } -/// @brief Displays the current stopwatch time on the LCD (more optimized than _display_ticks()) -static void _draw() { - if (_lap_ticks == 0) { - char buf[14]; - uint8_t sec_100 = (_ticks & 0x7F) * 100 / 128; - if (_is_running) { - uint32_t seconds = _ticks >> 7; - if (seconds != _old_seconds) { - // seconds have changed - _old_seconds = seconds; - uint8_t minutes = seconds / 60; - seconds %= 60; - if (minutes != _old_minutes) { - // minutes have changed, draw everything - _old_minutes = minutes; - minutes %= 60; - if (_hours) { - // with hour indicator - sprintf(buf, "%2u", _hours); - watch_display_text(WATCH_POSITION_TOP_RIGHT, buf); - } else { - // no hour indicator - watch_display_text(WATCH_POSITION_TOP_RIGHT, " "); - } - sprintf(buf, "%02u%02lu%02u", minutes, seconds, sec_100); - watch_display_text(WATCH_POSITION_BOTTOM, buf); - } else { - // just draw seconds - sprintf(buf, "%02lu", seconds); - // note that we're drawing the seconds in the "minutes" position, since this - // watch face uses the "seconds" position for hundredths of seconds - watch_display_text(WATCH_POSITION_MINUTES, buf); - watch_display_character_lp_seconds('0' + sec_100 / 10, 8); - watch_display_character_lp_seconds('0' + sec_100 % 10, 9); - } +static void _draw_indicators(fast_stopwatch_state_t *state, movement_event_t event, uint32_t elapsed) { + uint8_t subsecond; + bool tock; + + switch (state->status) { + case SW_STATUS_RUNNING: + subsecond = elapsed & 127; + tock = subsecond >= 64; + + watch_clear_indicator(WATCH_INDICATOR_LAP); + if (tock) { + watch_clear_colon(); } else { - // only draw 100ths of seconds - watch_display_character_lp_seconds('0' + sec_100 / 10, 8); - watch_display_character_lp_seconds('0' + sec_100 % 10, 9); + watch_set_colon(); } - } else { - _display_ticks(_ticks); - } - } - if (_is_running) { - // blink the colon every half second - uint8_t blink_ticks = ((_ticks >> 6) & 1); - if (blink_ticks != _blink_ticks) { - _blink_ticks = blink_ticks; - _colon = !_colon; - if (_colon) watch_set_colon(); - else watch_clear_colon(); - } + + return; + + case SW_STATUS_RUNNING_LAPPING: + tock = event.subsecond > 0; + + if (tock) { + watch_clear_indicator(WATCH_INDICATOR_LAP); + watch_clear_colon(); + } else { + watch_set_indicator(WATCH_INDICATOR_LAP); + watch_set_colon(); + } + + return; + + case SW_STATUS_STOPPED_LAPPING: + watch_set_indicator(WATCH_INDICATOR_LAP); + watch_set_colon(); + + return; + + case SW_STATUS_STOPPED: + case SW_STATUS_IDLE: + default: + watch_clear_indicator(WATCH_INDICATOR_LAP); + watch_set_colon(); + return; } } -static inline void _update_lap_indicator() { - if (_lap_ticks) watch_set_indicator(WATCH_INDICATOR_LAP); - else watch_clear_indicator(WATCH_INDICATOR_LAP); +static uint8_t get_refresh_rate(fast_stopwatch_state_t *state) { + switch (state->status) { + case SW_STATUS_RUNNING: + return DISPLAY_RUNNING_RATE; + case SW_STATUS_RUNNING_LAPPING: + return 2; + case SW_STATUS_STOPPED: + case SW_STATUS_IDLE: + default: + return 1; + } } -static inline void _set_colon() { - watch_set_colon(); - _colon = true; +static void state_transition(fast_stopwatch_state_t *state, rtc_counter_t counter, movement_event_type_t event_type) { + switch (state->status) { + case SW_STATUS_IDLE: + switch (event_type) { + case EVENT_ALARM_BUTTON_DOWN: + state->status = SW_STATUS_RUNNING; + state->start_counter = counter; + movement_request_tick_frequency(DISPLAY_RUNNING_RATE); + return; + default: + return; + } + + case SW_STATUS_RUNNING: + switch (event_type) { + case EVENT_ALARM_BUTTON_DOWN: + state->status = SW_STATUS_STOPPED; + state->stop_counter = counter; + movement_request_tick_frequency(1); + return; + case EVENT_LIGHT_BUTTON_DOWN: + state->status = SW_STATUS_RUNNING_LAPPING; + state->lap_counter = counter; + movement_request_tick_frequency(2); + return; + default: + return; + } + + case SW_STATUS_RUNNING_LAPPING: + switch (event_type) { + case EVENT_ALARM_BUTTON_DOWN: + state->status = SW_STATUS_STOPPED_LAPPING; + state->stop_counter = counter; + movement_request_tick_frequency(1); + return; + case EVENT_LIGHT_BUTTON_DOWN: + state->status = SW_STATUS_RUNNING; + state->lap_counter = counter; + movement_request_tick_frequency(DISPLAY_RUNNING_RATE); + return; + default: + return; + } + + case SW_STATUS_STOPPED_LAPPING: + switch (event_type) { + case EVENT_ALARM_BUTTON_DOWN: + state->status = SW_STATUS_RUNNING_LAPPING; + state->start_counter = counter - state->stop_counter + state->start_counter; + state->lap_counter = counter - state->stop_counter + state->lap_counter; + movement_request_tick_frequency(2); + return; + case EVENT_LIGHT_BUTTON_DOWN: + state->status = SW_STATUS_STOPPED; + return; + default: + return; + } + + case SW_STATUS_STOPPED: + switch (event_type) { + case EVENT_ALARM_BUTTON_DOWN: + state->status = SW_STATUS_RUNNING; + state->start_counter = counter - state->stop_counter + state->start_counter; + movement_request_tick_frequency(DISPLAY_RUNNING_RATE); + return; + case EVENT_LIGHT_BUTTON_DOWN: + state->status = SW_STATUS_IDLE; + return; + default: + return; + } + + default: + return; + } +} + +static uint32_t elapsed_time(fast_stopwatch_state_t *state, rtc_counter_t counter) { + switch (state->status) { + case SW_STATUS_IDLE: + return 0; + + case SW_STATUS_RUNNING: + return counter - state->start_counter; + + case SW_STATUS_RUNNING_LAPPING: + case SW_STATUS_STOPPED_LAPPING: + return state->lap_counter - state->start_counter; + + case SW_STATUS_STOPPED: + return state->stop_counter - state->start_counter; + + default: + return 0; + } } void fast_stopwatch_face_setup(uint8_t watch_face_index, void ** context_ptr) { @@ -215,114 +246,49 @@ void fast_stopwatch_face_setup(uint8_t watch_face_index, void ** context_ptr) { *context_ptr = malloc(sizeof(fast_stopwatch_state_t)); memset(*context_ptr, 0, sizeof(fast_stopwatch_state_t)); fast_stopwatch_state_t *state = (fast_stopwatch_state_t *)*context_ptr; - _ticks = _lap_ticks = _blink_ticks = _old_minutes = _old_seconds = _hours = 0; - _is_running = _colon = false; - state->light_on_button = true; - } - if (!_is_running) { - // prepare the 128 Hz callback source - _cb_initialize(); + state->start_counter = 0; + state->stop_counter = 0; + state->lap_counter = 0; + state->status = SW_STATUS_IDLE; } } void fast_stopwatch_face_activate(void *context) { - (void) context; - if (_is_running) { - // The background task will keep the watch from entering low energy mode while the stopwatch is on screen. - movement_schedule_background_task(distant_future); - } + fast_stopwatch_state_t *state = (fast_stopwatch_state_t *) context; + movement_request_tick_frequency(get_refresh_rate(state)); } bool fast_stopwatch_face_loop(movement_event_t event, void *context) { fast_stopwatch_state_t *state = (fast_stopwatch_state_t *)context; - // handle overflow of fast ticks - while (_ticks >= (128 * 60 * 60)) { - _ticks -= (128 * 60 * 60); - _hours++; - if (_hours >= 24) _hours -= 24; - // initiate a re-draw - _old_minutes = 59; - } + rtc_counter_t counter = watch_rtc_get_counter(); + + state_transition(state, counter, event.event_type); + rtc_counter_t elapsed = elapsed_time(state, counter); switch (event.event_type) { case EVENT_ACTIVATE: - _set_colon(); watch_display_text_with_fallback(WATCH_POSITION_TOP_LEFT, "STW", "ST"); - _update_lap_indicator(); - if (_is_running) movement_request_tick_frequency(16); - _display_ticks(_lap_ticks ? _lap_ticks : _ticks); - break; - case EVENT_TICK: - _draw(); - break; - case EVENT_LIGHT_LONG_PRESS: - // kind od hidden feature: long press toggles light on or off - state->light_on_button = !state->light_on_button; - if (state->light_on_button) movement_illuminate_led(); - else watch_set_led_off(); + _draw_indicators(state, event, elapsed); + _display_elapsed(elapsed); break; case EVENT_ALARM_BUTTON_DOWN: - _is_running = !_is_running; - if (_is_running) { - // start or continue stopwatch - movement_request_tick_frequency(16); - // register 128 hz callback for time measuring - _cb_start(); - // schedule the keepalive task when running - movement_schedule_background_task(distant_future); - } else { - // stop the stopwatch - _cb_stop(); - movement_request_tick_frequency(1); - _set_colon(); - // cancel the keepalive task - movement_cancel_background_task(); - } - _draw(); - _button_beep(); - break; case EVENT_LIGHT_BUTTON_DOWN: - if (state->light_on_button) movement_illuminate_led(); - if (_is_running) { - if (_lap_ticks) { - // clear lap and continue running - _lap_ticks = 0; - movement_request_tick_frequency(16); - } else { - // set lap ticks and stop updating the display - _lap_ticks = _ticks; - movement_request_tick_frequency(2); - _set_colon(); - } - } else { - if (_lap_ticks) { - // clear lap and show running stopwatch - _lap_ticks = 0; - } else if (_ticks) { - // reset stopwatch - _ticks = _lap_ticks = _blink_ticks = _old_minutes = _old_seconds = _hours = 0; - _button_beep(); - } - } - _display_ticks(_ticks); - _update_lap_indicator(); - break; - case EVENT_TIMEOUT: - if (!_is_running) movement_move_to_face(0); - break; - case EVENT_LOW_ENERGY_UPDATE: - _draw(); + _button_beep(); + // Fall into the case below; + case EVENT_TICK: + _draw_indicators(state, event, elapsed); + _display_elapsed(elapsed); break; default: movement_default_loop_handler(event); break; } + return true; } void fast_stopwatch_face_resign(void *context) { (void) context; - // cancel the keepalive task - movement_cancel_background_task(); + movement_request_tick_frequency(1); } diff --git a/watch-faces/complication/fast_stopwatch_face.h b/watch-faces/complication/fast_stopwatch_face.h index eac56651..d99f6085 100644 --- a/watch-faces/complication/fast_stopwatch_face.h +++ b/watch-faces/complication/fast_stopwatch_face.h @@ -55,7 +55,10 @@ #include "movement.h" typedef struct { - bool light_on_button; // determines whether the light button actually triggers the led + rtc_counter_t start_counter; // rtc counter when the stopwatch was started + rtc_counter_t lap_counter; // rtc counter when the stopwatch was lapped + rtc_counter_t stop_counter; // rtc counter when the stopwatch was stopped + uint8_t status; // the status the stopwatch is in (idle, running, stopped) } fast_stopwatch_state_t; void fast_stopwatch_face_setup(uint8_t watch_face_index, void ** context_ptr); @@ -63,12 +66,6 @@ void fast_stopwatch_face_activate(void *context); bool fast_stopwatch_face_loop(movement_event_t event, void *context); void fast_stopwatch_face_resign(void *context); -#if __EMSCRIPTEN__ -void em_cb_handler(void *userData); -#else -void TC2_Handler(void); -#endif - #define fast_stopwatch_face ((const watch_face_t){ \ fast_stopwatch_face_setup, \ fast_stopwatch_face_activate, \ From 04276c1999e3db14ee0aebc680cef38a1bab0ba9 Mon Sep 17 00:00:00 2001 From: Alessandro Genova Date: Sun, 17 Aug 2025 23:14:25 -0400 Subject: [PATCH 130/179] Improve watch_tcc to decouple leds and buzzer as much as allowed As a positive side effect, the led will stop emitting the faintiest of blinks when the buzzer starts playing. --- .../hardware/watch/watch_deepsleep.c | 3 +- watch-library/hardware/watch/watch_tcc.c | 127 ++++++++++-------- watch-library/shared/watch/watch_tcc.h | 46 +------ watch-library/simulator/watch/watch.c | 4 - watch-library/simulator/watch/watch_private.c | 2 - watch-library/simulator/watch/watch_tcc.c | 2 - 6 files changed, 76 insertions(+), 108 deletions(-) diff --git a/watch-library/hardware/watch/watch_deepsleep.c b/watch-library/hardware/watch/watch_deepsleep.c index e90150ee..20254df4 100644 --- a/watch-library/hardware/watch/watch_deepsleep.c +++ b/watch-library/hardware/watch/watch_deepsleep.c @@ -151,7 +151,8 @@ static void _watch_disable_all_pins_except_rtc(void) { } static void _watch_disable_all_peripherals_except_slcd(void) { - _watch_disable_tcc(); + watch_disable_leds(); + watch_disable_buzzer(); watch_disable_adc(); watch_disable_external_interrupts(); diff --git a/watch-library/hardware/watch/watch_tcc.c b/watch-library/hardware/watch/watch_tcc.c index fa81d074..cde3c0dd 100644 --- a/watch-library/hardware/watch/watch_tcc.c +++ b/watch-library/hardware/watch/watch_tcc.c @@ -27,10 +27,15 @@ #include "tcc.h" #include "tc.h" -void _watch_enable_tcc(void); +static void _watch_enable_tcc(void); +static void _watch_disable_tcc(void); +static void _watch_maybe_enable_tcc(void); +static void _watch_maybe_disable_tcc(void); +static void _watch_enable_led_pins(void); +static void _watch_disable_led_pins(void); static void (*_cb_tc0)(void) = NULL; -void cb_watch_buzzer_seq(void); -void cb_watch_buzzer_raw_source(void); +static void cb_watch_buzzer_seq(void); +static void cb_watch_buzzer_raw_source(void); static uint16_t _seq_position; static int8_t _tone_ticks, _repeat_counter; @@ -84,14 +89,11 @@ void watch_buzzer_play_sequence_with_volume(int8_t *note_sequence, void (*callba // Abort any previous sequence watch_buzzer_abort_sequence(); - _buzzer_is_active = true; - if (_cb_start_global) { _cb_start_global(); } - watch_enable_buzzer_and_leds(); - + watch_enable_buzzer(); watch_set_buzzer_off(); _sequence = note_sequence; _cb_finished = callback_on_end; @@ -154,13 +156,11 @@ void watch_buzzer_play_raw_source_with_volume(watch_buzzer_raw_source_t raw_sour // Abort any previous sequence watch_buzzer_abort_sequence(); - _buzzer_is_active = true; - if (_cb_start_global) { _cb_start_global(); } - watch_enable_buzzer_and_leds(); + watch_enable_buzzer(); watch_set_buzzer_off(); _raw_source = raw_source; @@ -213,14 +213,12 @@ void watch_buzzer_abort_sequence(void) { return; } - _buzzer_is_active = false; - _tc0_stop(); watch_set_buzzer_off(); // disable TCC - watch_maybe_disable_buzzer_and_leds(); + watch_disable_buzzer(); if (_cb_stop_global) { _cb_stop_global(); @@ -244,11 +242,11 @@ void irq_handler_tc0(void) { TC0->COUNT8.INTFLAG.reg |= TC_INTFLAG_OVF; } -bool watch_is_buzzer_or_led_enabled(void){ - return tcc_is_enabled(0); -} +void _watch_maybe_enable_tcc(void) { + if (!_buzzer_is_active && !_led_is_active) { + return; + } -void watch_enable_buzzer_and_leds(void) { if (!tcc_is_enabled(0)) { // tcc_set_run_in_standby(0, true); _watch_enable_tcc(); @@ -257,23 +255,26 @@ void watch_enable_buzzer_and_leds(void) { } } -void watch_disable_buzzer_and_leds(void) { +void _watch_maybe_disable_tcc(void) { + if (_buzzer_is_active || _led_is_active) { + return; + } + if (tcc_is_enabled(0)) { _tcc_write_RUNSTDBY(false); _watch_disable_tcc(); } } -void watch_maybe_disable_buzzer_and_leds(void) { - if (_buzzer_is_active || _led_is_active) { - return; - } - - watch_disable_buzzer_and_leds(); +void watch_enable_buzzer(void) { + _buzzer_is_active = true; + _watch_maybe_enable_tcc(); } -void watch_enable_buzzer(void) { - watch_enable_buzzer_and_leds(); +void watch_disable_buzzer(void) { + _buzzer_is_active = false; + watch_set_buzzer_off(); + _watch_maybe_disable_tcc(); } void watch_set_buzzer_period_and_duty_cycle(uint32_t period, uint8_t duty) { @@ -286,10 +287,6 @@ void watch_set_buzzer_period_and_duty_cycle(uint32_t period, uint8_t duty) { } } -void watch_disable_buzzer(void) { - watch_maybe_disable_buzzer_and_leds(); -} - inline void watch_set_buzzer_on(void) { HAL_GPIO_BUZZER_out(); HAL_GPIO_BUZZER_pmuxen(HAL_GPIO_PMUX_TCC_ALT); @@ -356,21 +353,6 @@ void _watch_enable_tcc(void) { tcc_set_cc(0, (WATCH_BLUE_TCC_CHANNEL) % 4, 0, false); #endif - // enable LED PWM pins (the LED driver assumes if the TCC is on, the pins are enabled) - HAL_GPIO_RED_pmuxen(HAL_GPIO_PMUX_TCC_ALT); - HAL_GPIO_RED_drvstr(1); - HAL_GPIO_RED_out(); -#ifdef WATCH_GREEN_TCC_CHANNEL - HAL_GPIO_GREEN_pmuxen(HAL_GPIO_PMUX_TCC_ALT); - HAL_GPIO_GREEN_drvstr(1); - HAL_GPIO_GREEN_out(); -#endif -#ifdef WATCH_BLUE_TCC_CHANNEL - HAL_GPIO_BLUE_pmuxen(HAL_GPIO_PMUX_TCC_ALT); - HAL_GPIO_BLUE_drvstr(1); - HAL_GPIO_BLUE_out(); -#endif - // Enable the TCC tcc_enable(0); } @@ -393,11 +375,45 @@ void _watch_disable_tcc(void) { } void watch_enable_leds(void) { - watch_enable_buzzer_and_leds(); + _led_is_active = true; + _watch_enable_led_pins(); + _watch_maybe_enable_tcc(); } void watch_disable_leds(void) { - watch_maybe_disable_buzzer_and_leds(); + _led_is_active = false; + _watch_disable_led_pins(); + _watch_maybe_disable_tcc(); +} + +void _watch_enable_led_pins(void) { + // enable LED PWM pins (the LED driver assumes if the TCC is on, the pins are enabled) + HAL_GPIO_RED_pmuxen(HAL_GPIO_PMUX_TCC_ALT); + HAL_GPIO_RED_drvstr(1); + HAL_GPIO_RED_out(); +#ifdef WATCH_GREEN_TCC_CHANNEL + HAL_GPIO_GREEN_pmuxen(HAL_GPIO_PMUX_TCC_ALT); + HAL_GPIO_GREEN_drvstr(1); + HAL_GPIO_GREEN_out(); +#endif +#ifdef WATCH_BLUE_TCC_CHANNEL + HAL_GPIO_BLUE_pmuxen(HAL_GPIO_PMUX_TCC_ALT); + HAL_GPIO_BLUE_drvstr(1); + HAL_GPIO_BLUE_out(); +#endif +} + +void _watch_disable_led_pins(void) { + HAL_GPIO_RED_pmuxdis(); + HAL_GPIO_RED_off(); +#ifdef WATCH_GREEN_TCC_CHANNEL + HAL_GPIO_GREEN_pmuxdis(); + HAL_GPIO_GREEN_off(); +#endif +#ifdef WATCH_BLUE_TCC_CHANNEL + HAL_GPIO_BLUE_pmuxdis(); + HAL_GPIO_BLUE_off(); +#endif } void watch_set_led_color(uint8_t red, uint8_t green) { @@ -429,19 +445,14 @@ void watch_set_led_color_rgb(uint8_t red, uint8_t green, uint8_t blue) { _current_led_color[0] = red; _current_led_color[1] = green; _current_led_color[2] = blue; - _led_is_active = true; - watch_enable_buzzer_and_leds(); - } else { - _led_is_active = false; - } - - if (tcc_is_enabled(0)) { + watch_enable_leds(); uint32_t period = tcc_get_period(0); _watch_set_led_duty_cycle(period, red, green, blue); - } - - if (!turning_on) { - watch_maybe_disable_buzzer_and_leds(); + } else { + if (tcc_is_enabled(0)) { + _watch_set_led_duty_cycle(1, red, green, blue); + } + watch_disable_leds(); } } diff --git a/watch-library/shared/watch/watch_tcc.h b/watch-library/shared/watch/watch_tcc.h index e87e53bf..056844b3 100644 --- a/watch-library/shared/watch/watch_tcc.h +++ b/watch-library/shared/watch/watch_tcc.h @@ -130,13 +130,6 @@ typedef enum { typedef bool (*watch_buzzer_raw_source_t)(uint16_t position, void* userdata, uint16_t* period, uint16_t* duration); -/** @brief Returns true if either the buzzer or the LED driver is enabled. - * @details Both the buzzer and the LED use the TCC peripheral to drive their behavior. This function returns true if that - * peripheral is enabled. You can use this function to determine whether you need to call the watch_enable_leds or - * or watch_enable_buzzer functions before using these peripherals. - */ -bool watch_is_buzzer_or_led_enabled(void); - /** @addtogroup tcc Buzzer and LED Control (via the TCC peripheral) * @brief This section covers functions related to Timer Counter for Control peripheral, which drives the piezo buzzer * embedded in the F-91W's back plate as well as the LED that backlights the display. @@ -155,15 +148,13 @@ void watch_enable_buzzer(void); */ void watch_set_buzzer_period_and_duty_cycle(uint32_t period, uint8_t duty); -/** @brief Disables the TCC peripheral that drives the buzzer. - * @note If you are using PWM to set custom LED colors, this method will also disable the LED PWM driver, - * since the buzzer and LED both make use of the same peripheral to drive their PWM behavior. +/** @brief Disables the TCC peripheral that drives the buzzer (if LED not active). */ void watch_disable_buzzer(void); /** @brief Turns the buzzer output on. It will emit a continuous sound at the given frequency. - * @note The TCC peripheral that drives the buzzer does not run in standby mode; if you wish for buzzer - * output to continue, you should prevent your app from going to sleep. + * @note The TCC peripheral that drives the buzzer does run in standby mode; if you wish for buzzer + * output to continue, you don't need to prevent your app from going to sleep. */ void watch_set_buzzer_on(void); @@ -265,18 +256,6 @@ void watch_buzzer_abort_sequence(void); void watch_buzzer_register_global_callbacks(watch_cb_t cb_start, watch_cb_t cb_stop); -/** @brief Enables the TCC peripheral, which drives the buzzer and the leds. -*/ -void watch_enable_buzzer_and_leds(void); - -/** @brief Disables the TCC peripheral that drives the buzzer and the leds. - */ -void watch_disable_buzzer_and_leds(void); - -/** @brief Disables the TCC peripheral that drives the buzzer and the leds if neither is currently active - */ -void watch_maybe_disable_buzzer_and_leds(void); - #ifndef __EMSCRIPTEN__ void irq_handler_tc0(void); #endif @@ -294,26 +273,17 @@ void irq_handler_tc0(void); * so that watch_set_led_red sets the red LED, and watch_set_led_green sets the blue one. */ /// @{ -/** @brief Enables the bi-color LED. - * @note The TCC peripheral that drives the LEDs does not run in STANDBY mode — but the outputs do! This - * means that if you set either red, green or both LEDs to full power, they will shine even when - * your app is asleep. If, however, you set a custom color using watch_set_led_color, the color will - * not display correctly in STANDBY mode. You will need to keep your app running while the LED is on. +/** @brief Enables the TCC peripheral, which drives the LEDs. */ void watch_enable_leds(void); -/** @brief Disables the LEDs. - * @note This method will also disable the buzzer, since the buzzer and LED both make use of the same - * peripheral to drive their PWM behavior. +/** @brief Disables the TCC peripheral that drives the LEDs (if buzzer not active). */ void watch_disable_leds(void); /** @brief Sets the LED to a custom color by modulating each output's duty cycle. * @param red The red value from 0-255. * @param green The green value from 0-255. If your watch has a red/blue LED, this will be the blue value. - * @note If you are displaying a custom color, you will need to prevent your app from going to sleep - * while the LED is on; otherwise, the color will not display correctly. You can do this by - * returning false in your app_loop method. */ void watch_set_led_color(uint8_t red, uint8_t green); @@ -321,9 +291,6 @@ void watch_set_led_color(uint8_t red, uint8_t green); * @param red The red value from 0-255. * @param green The green value from 0-255. * @param blue The blue value from 0-255. - * @note If you are displaying a custom color, you will need to prevent your app from going to sleep - * while the LED is on; otherwise, the color will not display correctly. You can do this by - * returning false in your app_loop method. */ void watch_set_led_color_rgb(uint8_t red, uint8_t green, uint8_t blue); @@ -348,9 +315,6 @@ void watch_set_led_yellow(void); /** @brief Turns both the red and the green LEDs off. */ void watch_set_led_off(void); -/** @brief Disables the TCC peripheral. Should only be called internally. */ -void _watch_disable_tcc(void); - /// @brief An array of periods for all the notes on a piano, corresponding to the names in watch_buzzer_note_t. extern const uint16_t NotePeriods[108]; diff --git a/watch-library/simulator/watch/watch.c b/watch-library/simulator/watch/watch.c index 4e89f3d3..7584cd38 100644 --- a/watch-library/simulator/watch/watch.c +++ b/watch-library/simulator/watch/watch.c @@ -1,9 +1,5 @@ #include "watch.h" -bool watch_is_buzzer_or_led_enabled(void) { - return false; -} - bool watch_is_usb_enabled(void) { return true; } diff --git a/watch-library/simulator/watch/watch_private.c b/watch-library/simulator/watch/watch_private.c index 6f97490b..b94fbf12 100644 --- a/watch-library/simulator/watch/watch_private.c +++ b/watch-library/simulator/watch/watch_private.c @@ -51,8 +51,6 @@ int _gettimeofday(struct timeval *tv, void *tzvp) { return 0; } -void _watch_disable_tcc(void) {} - void _watch_enable_usb(void) {} void watch_disable_TRNG() {} diff --git a/watch-library/simulator/watch/watch_tcc.c b/watch-library/simulator/watch/watch_tcc.c index 1ca4bbf0..6cbf9822 100644 --- a/watch-library/simulator/watch/watch_tcc.c +++ b/watch-library/simulator/watch/watch_tcc.c @@ -46,8 +46,6 @@ static watch_cb_t _cb_start_global = NULL; static watch_cb_t _cb_stop_global = NULL; static volatile bool _buzzer_is_active = false; -void _watch_enable_tcc(void) {} - static inline void _em_interval_stop() { emscripten_clear_interval(_em_interval_id); _em_interval_id = 0; From cb05585c156efd3ff64cea0aeca844cc6c0cd02f Mon Sep 17 00:00:00 2001 From: Alessandro Genova Date: Sun, 17 Aug 2025 23:35:15 -0400 Subject: [PATCH 131/179] Fix faces that relied on watch_buzzer_play_note being blocking --- .../complication/advanced_alarm_face.c | 22 ++++++++-------- watch-faces/complication/deadline_face.c | 24 ++++++++++++----- watch-faces/complication/simon_face.c | 26 +++++++------------ watch-faces/complication/tally_face.c | 22 +++++++++++----- 4 files changed, 53 insertions(+), 41 deletions(-) diff --git a/watch-faces/complication/advanced_alarm_face.c b/watch-faces/complication/advanced_alarm_face.c index 16bfdbce..4e5d9eb9 100644 --- a/watch-faces/complication/advanced_alarm_face.c +++ b/watch-faces/complication/advanced_alarm_face.c @@ -202,9 +202,16 @@ static void _alarm_update_alarm_enabled(alarm_state_t *state) { static void _alarm_play_short_beep(uint8_t pitch_idx) { // play a short double beep - watch_buzzer_play_note(_buzzer_notes[pitch_idx], 50); - watch_buzzer_play_note(BUZZER_NOTE_REST, 50); - watch_buzzer_play_note(_buzzer_notes[pitch_idx], 70); + static int8_t beep_sequence[] = { + 0, 4, + BUZZER_NOTE_REST, 4, + 0, 6, + 0 + }; + beep_sequence[0] = _buzzer_notes[pitch_idx]; + beep_sequence[4] = _buzzer_notes[pitch_idx]; + + movement_play_sequence(beep_sequence, 0); } static void _alarm_indicate_beep(alarm_state_t *state) { @@ -437,14 +444,7 @@ bool advanced_alarm_face_loop(movement_event_t event, void *context) { // play alarm if (state->alarm[state->alarm_playing_idx].beeps == 0) { // short beep - if (watch_is_buzzer_or_led_enabled()) { - _alarm_play_short_beep(state->alarm[state->alarm_playing_idx].pitch); - } else { - // enable, play beep and disable buzzer again - watch_enable_buzzer(); - _alarm_play_short_beep(state->alarm[state->alarm_playing_idx].pitch); - watch_disable_buzzer(); - } + _alarm_play_short_beep(state->alarm[state->alarm_playing_idx].pitch); } else { // regular alarm beeps movement_play_alarm_beeps((state->alarm[state->alarm_playing_idx].beeps == (ALARM_MAX_BEEP_ROUNDS - 1) ? 20 : state->alarm[state->alarm_playing_idx].beeps), diff --git a/watch-faces/complication/deadline_face.c b/watch-faces/complication/deadline_face.c index b145a618..ebad0e8b 100644 --- a/watch-faces/complication/deadline_face.c +++ b/watch-faces/complication/deadline_face.c @@ -82,26 +82,36 @@ static inline int _days_in_month(int16_t month, int16_t year) /* Play beep sound based on type */ static inline void _beep(beep_type_t beep_type) { + static int8_t beep_sequence[] = { + 0, 4, + 0, 6, + 0, 6, + 0 + }; + if (!movement_button_should_sound()) return; switch (beep_type) { case BEEP_BUTTON: - watch_buzzer_play_note_with_volume(BUZZER_NOTE_C7, 50, movement_button_volume()); + beep_sequence[0] = BUZZER_NOTE_C7; + beep_sequence[2] = 0; break; case BEEP_ENABLE: - watch_buzzer_play_note_with_volume(BUZZER_NOTE_G7, 50, movement_button_volume()); - watch_buzzer_play_note(BUZZER_NOTE_REST, 75); - watch_buzzer_play_note_with_volume(BUZZER_NOTE_C8, 50, movement_button_volume()); + beep_sequence[0] = BUZZER_NOTE_G7; + beep_sequence[2] = BUZZER_NOTE_REST; + beep_sequence[4] = BUZZER_NOTE_C8; break; case BEEP_DISABLE: - watch_buzzer_play_note_with_volume(BUZZER_NOTE_C8, 50, movement_button_volume()); - watch_buzzer_play_note(BUZZER_NOTE_REST, 75); - watch_buzzer_play_note_with_volume(BUZZER_NOTE_G7, 50, movement_button_volume()); + beep_sequence[0] = BUZZER_NOTE_C8; + beep_sequence[2] = BUZZER_NOTE_REST; + beep_sequence[4] = BUZZER_NOTE_G7; break; } + + movement_play_sequence(beep_sequence, 0); } /* Change tick frequency */ diff --git a/watch-faces/complication/simon_face.c b/watch-faces/complication/simon_face.c index 642b32a2..7b54f12f 100644 --- a/watch-faces/complication/simon_face.c +++ b/watch-faces/complication/simon_face.c @@ -119,30 +119,22 @@ static void _simon_play_note(SimonNote note, simon_state_t *state, bool skip_res switch (note) { case SIMON_LED_NOTE: if (!state->lightOff) watch_set_led_yellow(); - if (state->soundOff) - delay_ms(_delay_beep); - else - watch_buzzer_play_note(BUZZER_NOTE_D3, _delay_beep); + if (!state->soundOff) watch_buzzer_play_note(BUZZER_NOTE_D3, _delay_beep); + delay_ms(_delay_beep); break; case SIMON_MODE_NOTE: if (!state->lightOff) watch_set_led_red(); - if (state->soundOff) - delay_ms(_delay_beep); - else - watch_buzzer_play_note(BUZZER_NOTE_E4, _delay_beep); + if (!state->soundOff) watch_buzzer_play_note(BUZZER_NOTE_E4, _delay_beep); + delay_ms(_delay_beep); break; case SIMON_ALARM_NOTE: if (!state->lightOff) watch_set_led_green(); - if (state->soundOff) - delay_ms(_delay_beep); - else - watch_buzzer_play_note(BUZZER_NOTE_C3, _delay_beep); + if (!state->soundOff) watch_buzzer_play_note(BUZZER_NOTE_C3, _delay_beep); + delay_ms(_delay_beep); break; case SIMON_WRONG_NOTE: - if (state->soundOff) - delay_ms(800); - else - watch_buzzer_play_note(BUZZER_NOTE_A1, 800); + if (!state->soundOff) watch_buzzer_play_note(BUZZER_NOTE_A1, 800); + delay_ms(800); break; } watch_set_led_off(); @@ -150,7 +142,7 @@ static void _simon_play_note(SimonNote note, simon_state_t *state, bool skip_res if (note != SIMON_WRONG_NOTE) { _simon_clear_display(state); if (!skip_rest) { - watch_buzzer_play_note(BUZZER_NOTE_REST, (_delay_beep * 2)/3); + delay_ms((_delay_beep * 2)/3); } } } diff --git a/watch-faces/complication/tally_face.c b/watch-faces/complication/tally_face.c index deb5b878..ec217954 100644 --- a/watch-faces/complication/tally_face.c +++ b/watch-faces/complication/tally_face.c @@ -110,6 +110,12 @@ static bool tally_face_should_move_back(tally_state_t *state) { bool tally_face_loop(movement_event_t event, void *context) { tally_state_t *state = (tally_state_t *)context; static bool using_led = false; + static int8_t beep_sequence[] = { + 0, 2, + BUZZER_NOTE_REST, 3, + 0, 2, + 0 + }; if (using_led) { if(!HAL_GPIO_BTN_MODE_read() && !HAL_GPIO_BTN_LIGHT_read() && !HAL_GPIO_BTN_ALARM_read()) @@ -148,9 +154,11 @@ bool tally_face_loop(movement_event_t event, void *context) { state->tally_idx = _tally_default[state->tally_default_idx]; // reset tally index _init_val = true; //play a reset tune - if (movement_button_should_sound()) watch_buzzer_play_note(BUZZER_NOTE_G6, 30); - if (movement_button_should_sound()) watch_buzzer_play_note(BUZZER_NOTE_REST, 30); - if (movement_button_should_sound()) watch_buzzer_play_note(BUZZER_NOTE_E6, 30); + if (movement_button_should_sound()) { + beep_sequence[0] = BUZZER_NOTE_G6; + beep_sequence[4] = BUZZER_NOTE_E6; + movement_play_sequence(beep_sequence, 0); + } print_tally(state, movement_button_should_sound()); } break; @@ -168,9 +176,11 @@ bool tally_face_loop(movement_event_t event, void *context) { if (TALLY_FACE_PRESETS_SIZE() > 1 && _init_val){ state->tally_default_idx = (state->tally_default_idx + 1) % TALLY_FACE_PRESETS_SIZE(); state->tally_idx = _tally_default[state->tally_default_idx]; - if (movement_button_should_sound()) watch_buzzer_play_note(BUZZER_NOTE_E6, 30); - if (movement_button_should_sound()) watch_buzzer_play_note(BUZZER_NOTE_REST, 30); - if (movement_button_should_sound()) watch_buzzer_play_note(BUZZER_NOTE_G6, 30); + if (movement_button_should_sound()) { + beep_sequence[0] = BUZZER_NOTE_E6; + beep_sequence[4] = BUZZER_NOTE_G6; + movement_play_sequence(beep_sequence, 0); + } print_tally(state, movement_button_should_sound()); } else{ From f534e7c202f08c0b666c78dd2fd5295cbec81bcd Mon Sep 17 00:00:00 2001 From: Alessandro Genova Date: Tue, 19 Aug 2025 00:00:51 -0400 Subject: [PATCH 132/179] Fix bug that was causing notes in a sequence to play too long --- movement.c | 16 ++++++++-------- watch-library/hardware/watch/watch_tcc.c | 12 ++++++------ watch-library/simulator/watch/watch_tcc.c | 8 ++++---- 3 files changed, 18 insertions(+), 18 deletions(-) diff --git a/movement.c b/movement.c index f52050e0..d13146e9 100644 --- a/movement.c +++ b/movement.c @@ -103,13 +103,13 @@ static int8_t *_pending_sequence; // The note sequence of the default alarm int8_t alarm_tune[] = { BUZZER_NOTE_C8, 4, - BUZZER_NOTE_REST, 4, + BUZZER_NOTE_REST, 7, BUZZER_NOTE_C8, 4, - BUZZER_NOTE_REST, 4, + BUZZER_NOTE_REST, 7, BUZZER_NOTE_C8, 4, - BUZZER_NOTE_REST, 4, - BUZZER_NOTE_C8, 6, - BUZZER_NOTE_REST, 18, + BUZZER_NOTE_REST, 7, + BUZZER_NOTE_C8, 4, + BUZZER_NOTE_REST, 27, -8, 9, 0 }; @@ -505,9 +505,9 @@ void movement_play_note(watch_buzzer_note_t note, uint16_t duration_ms) { static int8_t single_note_sequence[3]; single_note_sequence[0] = note; - // 48 ticks per second for the tc0? - // Each tick is approximately 20ms - uint16_t duration = duration_ms / 20; + // 64 ticks per second for the tc0 + // Each tick is approximately 15ms + uint16_t duration = duration_ms / 15; if (duration > 127) duration = 127; single_note_sequence[1] = (int8_t)duration; single_note_sequence[2] = 0; diff --git a/watch-library/hardware/watch/watch_tcc.c b/watch-library/hardware/watch/watch_tcc.c index cde3c0dd..07f3126c 100644 --- a/watch-library/hardware/watch/watch_tcc.c +++ b/watch-library/hardware/watch/watch_tcc.c @@ -139,7 +139,7 @@ void cb_watch_buzzer_seq(void) { watch_set_buzzer_on(); } else watch_set_buzzer_off(); // set duration ticks and move to next tone - _tone_ticks = _sequence[_seq_position + 1]; + _tone_ticks = _sequence[_seq_position + 1] - 1; _seq_position += 2; } else { // end the sequence @@ -187,7 +187,7 @@ void cb_watch_buzzer_raw_source(void) { if (_tone_ticks == 0) { done = _raw_source(_seq_position, _userdata, &period, &duration); - if (done) { + if (done || duration == 0) { // end the sequence watch_buzzer_abort_sequence(); } else { @@ -199,7 +199,7 @@ void cb_watch_buzzer_raw_source(void) { } // set duration ticks and move to next tone - _tone_ticks = duration; + _tone_ticks = duration - 1; _seq_position += 1; } } else { @@ -305,9 +305,9 @@ void watch_buzzer_play_note_with_volume(watch_buzzer_note_t note, uint16_t durat static int8_t single_note_sequence[3]; single_note_sequence[0] = note; - // 48 ticks per second for the tc0? - // Each tick is approximately 20ms - uint16_t duration = duration_ms / 20; + // 64 ticks per second for the tc0 + // Each tick is approximately 15ms + uint16_t duration = duration_ms / 15; if (duration > 127) duration = 127; single_note_sequence[1] = (int8_t)duration; single_note_sequence[2] = 0; diff --git a/watch-library/simulator/watch/watch_tcc.c b/watch-library/simulator/watch/watch_tcc.c index 6cbf9822..e90b7613 100644 --- a/watch-library/simulator/watch/watch_tcc.c +++ b/watch-library/simulator/watch/watch_tcc.c @@ -110,7 +110,7 @@ void cb_watch_buzzer_seq(void *userData) { watch_set_buzzer_on(); } // set duration ticks and move to next tone - _tone_ticks = _sequence[_seq_position + 1]; + _tone_ticks = _sequence[_seq_position + 1] - 1; _seq_position += 2; } else { // end the sequence @@ -157,7 +157,7 @@ void cb_watch_buzzer_raw_source(void *userData) { if (_tone_ticks == 0) { done = _raw_source(_seq_position, _userdata, &period, &duration); - if (done) { + if (done || duration == 0) { // end the sequence watch_buzzer_abort_sequence(); } else { @@ -169,7 +169,7 @@ void cb_watch_buzzer_raw_source(void *userData) { } // set duration ticks and move to next tone - _tone_ticks = duration; + _tone_ticks = duration - 1; _seq_position += 1; } } else { @@ -272,7 +272,7 @@ void watch_buzzer_play_note_with_volume(watch_buzzer_note_t note, uint16_t durat static int8_t single_note_sequence[3]; single_note_sequence[0] = note; - // 64 ticks per second for the tc0? + // 64 ticks per second for the tc0 // Each tick is approximately 15ms uint16_t duration = duration_ms / 15; if (duration > 127) duration = 127; From 996d5d6679618e309a117437606bcec4e4365315 Mon Sep 17 00:00:00 2001 From: Alessandro Genova Date: Tue, 26 Aug 2025 21:44:00 -0400 Subject: [PATCH 133/179] Convert from timestamp to date_time once per second at most --- movement.c | 13 ++++++++++++- watch-library/hardware/watch/watch_rtc.c | 14 +++++++++++++- 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/movement.c b/movement.c index d13146e9..bc8b12f9 100644 --- a/movement.c +++ b/movement.c @@ -614,8 +614,19 @@ watch_date_time_t movement_get_date_time_in_zone(uint8_t zone_index) { } watch_date_time_t movement_get_local_date_time(void) { + static struct { + unix_timestamp_t timestamp; + rtc_date_time_t datetime; + } cached_date_time = {.datetime.reg=0, .timestamp=0}; + unix_timestamp_t timestamp = watch_rtc_get_unix_time(); - return watch_utility_date_time_from_unix_time(timestamp, movement_get_current_timezone_offset()); + + if (timestamp != cached_date_time.timestamp) { + cached_date_time.timestamp = timestamp; + cached_date_time.datetime = watch_utility_date_time_from_unix_time(timestamp, movement_get_current_timezone_offset()); + } + + return cached_date_time.datetime; } uint32_t movement_get_utc_timestamp(void) { diff --git a/watch-library/hardware/watch/watch_rtc.c b/watch-library/hardware/watch/watch_rtc.c index 1b0b5f49..114c1ee4 100644 --- a/watch-library/hardware/watch/watch_rtc.c +++ b/watch-library/hardware/watch/watch_rtc.c @@ -91,7 +91,19 @@ void watch_rtc_set_date_time(rtc_date_time_t date_time) { } rtc_date_time_t watch_rtc_get_date_time(void) { - return watch_utility_date_time_from_unix_time(watch_rtc_get_unix_time(), 0); + static struct { + unix_timestamp_t timestamp; + rtc_date_time_t datetime; + } cached_date_time = {.datetime.reg=0, .timestamp=0}; + + unix_timestamp_t timestamp = watch_rtc_get_unix_time(); + + if (timestamp != cached_date_time.timestamp) { + cached_date_time.timestamp = timestamp; + cached_date_time.datetime = watch_utility_date_time_from_unix_time(timestamp, 0); + } + + return cached_date_time.datetime; } void watch_rtc_set_unix_time(unix_timestamp_t unix_time) { From 450100b90a3357c04dc28c11f5cfc8f69ca6ffef Mon Sep 17 00:00:00 2001 From: Alessandro Genova Date: Tue, 26 Aug 2025 22:15:01 -0400 Subject: [PATCH 134/179] make movement_set_local_date_time slightly more efficient --- movement.c | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/movement.c b/movement.c index bc8b12f9..23eeb401 100644 --- a/movement.c +++ b/movement.c @@ -639,8 +639,7 @@ void movement_set_utc_date_time(watch_date_time_t date_time) { void movement_set_local_date_time(watch_date_time_t date_time) { int32_t current_offset = movement_get_current_timezone_offset(); - watch_date_time_t utc_date_time = watch_utility_date_time_convert_zone(date_time, current_offset, 0); - movement_set_utc_date_time(utc_date_time); + movement_set_utc_timestamp(watch_utility_date_time_to_unix_time(date_time, current_offset)); } void movement_set_utc_timestamp(uint32_t timestamp) { From 5293b7983c034044c7a07f21c2eab6439143e862 Mon Sep 17 00:00:00 2001 From: Alessandro Genova Date: Tue, 26 Aug 2025 22:23:14 -0400 Subject: [PATCH 135/179] Remove a few unnecessary date_time to timestamp conversions --- watch-faces/complication/interval_face.c | 3 +-- watch-faces/complication/totp_face.c | 3 +-- watch-faces/complication/totp_lfs_face.c | 3 +-- 3 files changed, 3 insertions(+), 6 deletions(-) diff --git a/watch-faces/complication/interval_face.c b/watch-faces/complication/interval_face.c index 15593567..89f00b83 100644 --- a/watch-faces/complication/interval_face.c +++ b/watch-faces/complication/interval_face.c @@ -96,8 +96,7 @@ static inline void _inc_uint8(uint8_t *value, uint8_t step, uint8_t max) { static uint32_t _get_now_ts() { // returns the current date time as unix timestamp - watch_date_time_t now = watch_rtc_get_date_time(); - return watch_utility_date_time_to_unix_time(now, 0); + return movement_get_utc_timestamp(); } static inline void _button_beep() { diff --git a/watch-faces/complication/totp_face.c b/watch-faces/complication/totp_face.c index b45c62f6..666c3337 100644 --- a/watch-faces/complication/totp_face.c +++ b/watch-faces/complication/totp_face.c @@ -36,7 +36,6 @@ #include #include "totp_face.h" #include "watch.h" -#include "watch_utility.h" #include "TOTP.h" #include "base32.h" @@ -159,7 +158,7 @@ static void totp_generate_and_display(totp_state_t *totp_state) { } static inline uint32_t totp_compute_base_timestamp() { - return watch_utility_date_time_to_unix_time(movement_get_utc_date_time(), 0); + return movement_get_utc_timestamp(); } void totp_face_setup(uint8_t watch_face_index, void ** context_ptr) { diff --git a/watch-faces/complication/totp_lfs_face.c b/watch-faces/complication/totp_lfs_face.c index 8be3e28d..7d63bba4 100644 --- a/watch-faces/complication/totp_lfs_face.c +++ b/watch-faces/complication/totp_lfs_face.c @@ -30,7 +30,6 @@ #include "base32.h" #include "watch.h" -#include "watch_utility.h" #include "filesystem.h" #include "totp_lfs_face.h" @@ -253,7 +252,7 @@ void totp_lfs_face_activate(void *context) { } #endif - totp_state->timestamp = watch_utility_date_time_to_unix_time(movement_get_utc_date_time(), 0); + totp_state->timestamp = movement_get_utc_timestamp(); totp_face_set_record(totp_state, 0); } From 7d0f4d1a337f4e928d87cdf6ac8380a9ae82a070 Mon Sep 17 00:00:00 2001 From: Alessandro Genova Date: Tue, 26 Aug 2025 23:37:53 -0400 Subject: [PATCH 136/179] More efficient display draw for fast_stopwatch_face --- .../complication/fast_stopwatch_face.c | 54 ++++++++++++++----- .../complication/fast_stopwatch_face.h | 5 ++ 2 files changed, 47 insertions(+), 12 deletions(-) diff --git a/watch-faces/complication/fast_stopwatch_face.c b/watch-faces/complication/fast_stopwatch_face.c index a4af13f3..9baad1b7 100644 --- a/watch-faces/complication/fast_stopwatch_face.c +++ b/watch-faces/complication/fast_stopwatch_face.c @@ -25,6 +25,7 @@ #include #include +#include #include "fast_stopwatch_face.h" #include "watch.h" #include "watch_common_display.h" @@ -59,27 +60,52 @@ static inline void _button_beep() { // This is just for looks, timekeeping is always accurate to 128Hz static const uint8_t DISPLAY_RUNNING_RATE = 32; -static const uint32_t TWENTY_FOUR_HOURS = 24 * 60 * 60 * 128; - /// @brief Display minutes, seconds and fractions derived from 128 Hz tick counter /// on the lcd. /// @param ticks -static void _display_elapsed(uint32_t ticks) { - ticks = ticks % TWENTY_FOUR_HOURS; - char buf[14]; +static void _display_elapsed(fast_stopwatch_state_t *state, uint32_t ticks) { + char buf[3]; uint8_t sec_100 = (ticks & 0x7F) * 100 / 128; + + watch_display_character_lp_seconds('0' + sec_100 / 10, 8); + watch_display_character_lp_seconds('0' + sec_100 % 10, 9); + uint32_t seconds = ticks >> 7; + + if (seconds == state->old_display.seconds) { + return; + } + + state->old_display.seconds = seconds; + + sprintf(buf, "%02lu", seconds % 60); + watch_display_text(WATCH_POSITION_MINUTES, buf); + uint32_t minutes = seconds / 60; - uint32_t hours = minutes / 60; + + if (minutes == state->old_display.minutes) { + return; + } + + state->old_display.minutes = minutes; + + sprintf(buf, "%02lu", minutes % 60); + watch_display_text(WATCH_POSITION_HOURS, buf); + + uint32_t hours = (minutes / 60) % 24; + + if (hours == state->old_display.hours) { + return; + } + + state->old_display.hours = hours; + if (hours) { - sprintf(buf, "%2u", hours); + sprintf(buf, "%2lu", hours); watch_display_text(WATCH_POSITION_TOP_RIGHT, buf); } else { watch_display_text(WATCH_POSITION_TOP_RIGHT, " "); } - - sprintf(buf, "%02lu%02lu%02u", (minutes % 60), (seconds % 60), sec_100); - watch_display_text(WATCH_POSITION_BOTTOM, buf); } static void _draw_indicators(fast_stopwatch_state_t *state, movement_event_t event, uint32_t elapsed) { @@ -255,6 +281,10 @@ void fast_stopwatch_face_setup(uint8_t watch_face_index, void ** context_ptr) { void fast_stopwatch_face_activate(void *context) { fast_stopwatch_state_t *state = (fast_stopwatch_state_t *) context; + // force full re-draw + state->old_display.seconds = UINT_MAX; + state->old_display.minutes = UINT_MAX; + state->old_display.hours = UINT_MAX; movement_request_tick_frequency(get_refresh_rate(state)); } @@ -270,7 +300,7 @@ bool fast_stopwatch_face_loop(movement_event_t event, void *context) { case EVENT_ACTIVATE: watch_display_text_with_fallback(WATCH_POSITION_TOP_LEFT, "STW", "ST"); _draw_indicators(state, event, elapsed); - _display_elapsed(elapsed); + _display_elapsed(state, elapsed); break; case EVENT_ALARM_BUTTON_DOWN: case EVENT_LIGHT_BUTTON_DOWN: @@ -278,7 +308,7 @@ bool fast_stopwatch_face_loop(movement_event_t event, void *context) { // Fall into the case below; case EVENT_TICK: _draw_indicators(state, event, elapsed); - _display_elapsed(elapsed); + _display_elapsed(state, elapsed); break; default: movement_default_loop_handler(event); diff --git a/watch-faces/complication/fast_stopwatch_face.h b/watch-faces/complication/fast_stopwatch_face.h index d99f6085..70c78bef 100644 --- a/watch-faces/complication/fast_stopwatch_face.h +++ b/watch-faces/complication/fast_stopwatch_face.h @@ -59,6 +59,11 @@ typedef struct { rtc_counter_t lap_counter; // rtc counter when the stopwatch was lapped rtc_counter_t stop_counter; // rtc counter when the stopwatch was stopped uint8_t status; // the status the stopwatch is in (idle, running, stopped) + struct { + rtc_counter_t seconds; + rtc_counter_t minutes; + rtc_counter_t hours; + } old_display; // the digits currently being displayed on screen } fast_stopwatch_state_t; void fast_stopwatch_face_setup(uint8_t watch_face_index, void ** context_ptr); From edd3a5c3b45f3f524926141d0d2051c59b82c57a Mon Sep 17 00:00:00 2001 From: Alessandro Genova Date: Tue, 26 Aug 2025 23:56:50 -0400 Subject: [PATCH 137/179] Add a slow display refresh mode to fast_stopwatch --- .../complication/fast_stopwatch_face.c | 43 ++++++++++++++----- .../complication/fast_stopwatch_face.h | 1 + 2 files changed, 33 insertions(+), 11 deletions(-) diff --git a/watch-faces/complication/fast_stopwatch_face.c b/watch-faces/complication/fast_stopwatch_face.c index 9baad1b7..63ef7176 100644 --- a/watch-faces/complication/fast_stopwatch_face.c +++ b/watch-faces/complication/fast_stopwatch_face.c @@ -31,6 +31,7 @@ #include "watch_common_display.h" #include "watch_utility.h" #include "watch_rtc.h" +#include "slcd.h" /* This watch face implements the original F-91W stopwatch functionality @@ -59,16 +60,23 @@ static inline void _button_beep() { // How quickly should the elapsing time be displayed? // This is just for looks, timekeeping is always accurate to 128Hz static const uint8_t DISPLAY_RUNNING_RATE = 32; +static const uint8_t DISPLAY_RUNNING_RATE_SLOW = 2; /// @brief Display minutes, seconds and fractions derived from 128 Hz tick counter /// on the lcd. /// @param ticks static void _display_elapsed(fast_stopwatch_state_t *state, uint32_t ticks) { char buf[3]; - uint8_t sec_100 = (ticks & 0x7F) * 100 / 128; - watch_display_character_lp_seconds('0' + sec_100 / 10, 8); - watch_display_character_lp_seconds('0' + sec_100 % 10, 9); + if (state->slow_refresh && (state->status == SW_STATUS_RUNNING || state->status == SW_STATUS_IDLE)) { + watch_display_character_lp_seconds(' ', 8); + watch_display_character_lp_seconds(' ', 9); + } else { + uint8_t sec_100 = (ticks & 0x7F) * 100 / 128; + + watch_display_character_lp_seconds('0' + sec_100 / 10, 8); + watch_display_character_lp_seconds('0' + sec_100 % 10, 9); + } uint32_t seconds = ticks >> 7; @@ -157,7 +165,11 @@ static void _draw_indicators(fast_stopwatch_state_t *state, movement_event_t eve static uint8_t get_refresh_rate(fast_stopwatch_state_t *state) { switch (state->status) { case SW_STATUS_RUNNING: - return DISPLAY_RUNNING_RATE; + if (state->slow_refresh) { + return DISPLAY_RUNNING_RATE_SLOW; + } else { + return DISPLAY_RUNNING_RATE; + } case SW_STATUS_RUNNING_LAPPING: return 2; case SW_STATUS_STOPPED: @@ -174,7 +186,10 @@ static void state_transition(fast_stopwatch_state_t *state, rtc_counter_t counte case EVENT_ALARM_BUTTON_DOWN: state->status = SW_STATUS_RUNNING; state->start_counter = counter; - movement_request_tick_frequency(DISPLAY_RUNNING_RATE); + movement_request_tick_frequency(get_refresh_rate(state)); + return; + case EVENT_LIGHT_LONG_PRESS: + state->slow_refresh = !state->slow_refresh; return; default: return; @@ -185,12 +200,12 @@ static void state_transition(fast_stopwatch_state_t *state, rtc_counter_t counte case EVENT_ALARM_BUTTON_DOWN: state->status = SW_STATUS_STOPPED; state->stop_counter = counter; - movement_request_tick_frequency(1); + movement_request_tick_frequency(get_refresh_rate(state)); return; case EVENT_LIGHT_BUTTON_DOWN: state->status = SW_STATUS_RUNNING_LAPPING; state->lap_counter = counter; - movement_request_tick_frequency(2); + movement_request_tick_frequency(get_refresh_rate(state)); return; default: return; @@ -201,12 +216,17 @@ static void state_transition(fast_stopwatch_state_t *state, rtc_counter_t counte case EVENT_ALARM_BUTTON_DOWN: state->status = SW_STATUS_STOPPED_LAPPING; state->stop_counter = counter; - movement_request_tick_frequency(1); + movement_request_tick_frequency(get_refresh_rate(state)); return; case EVENT_LIGHT_BUTTON_DOWN: state->status = SW_STATUS_RUNNING; state->lap_counter = counter; - movement_request_tick_frequency(DISPLAY_RUNNING_RATE); + movement_request_tick_frequency(get_refresh_rate(state)); + return; + case EVENT_LIGHT_LONG_PRESS: + state->status = SW_STATUS_RUNNING; + state->slow_refresh = !state->slow_refresh; + movement_request_tick_frequency(get_refresh_rate(state)); return; default: return; @@ -218,7 +238,7 @@ static void state_transition(fast_stopwatch_state_t *state, rtc_counter_t counte state->status = SW_STATUS_RUNNING_LAPPING; state->start_counter = counter - state->stop_counter + state->start_counter; state->lap_counter = counter - state->stop_counter + state->lap_counter; - movement_request_tick_frequency(2); + movement_request_tick_frequency(get_refresh_rate(state)); return; case EVENT_LIGHT_BUTTON_DOWN: state->status = SW_STATUS_STOPPED; @@ -232,7 +252,7 @@ static void state_transition(fast_stopwatch_state_t *state, rtc_counter_t counte case EVENT_ALARM_BUTTON_DOWN: state->status = SW_STATUS_RUNNING; state->start_counter = counter - state->stop_counter + state->start_counter; - movement_request_tick_frequency(DISPLAY_RUNNING_RATE); + movement_request_tick_frequency(get_refresh_rate(state)); return; case EVENT_LIGHT_BUTTON_DOWN: state->status = SW_STATUS_IDLE; @@ -304,6 +324,7 @@ bool fast_stopwatch_face_loop(movement_event_t event, void *context) { break; case EVENT_ALARM_BUTTON_DOWN: case EVENT_LIGHT_BUTTON_DOWN: + case EVENT_LIGHT_LONG_PRESS: _button_beep(); // Fall into the case below; case EVENT_TICK: diff --git a/watch-faces/complication/fast_stopwatch_face.h b/watch-faces/complication/fast_stopwatch_face.h index 70c78bef..34cb4973 100644 --- a/watch-faces/complication/fast_stopwatch_face.h +++ b/watch-faces/complication/fast_stopwatch_face.h @@ -59,6 +59,7 @@ typedef struct { rtc_counter_t lap_counter; // rtc counter when the stopwatch was lapped rtc_counter_t stop_counter; // rtc counter when the stopwatch was stopped uint8_t status; // the status the stopwatch is in (idle, running, stopped) + bool slow_refresh; // update the display slowly (same 128Hz timekeeping accuracy) struct { rtc_counter_t seconds; rtc_counter_t minutes; From 3ea2f9c58ab6cb0108f93cbbc64d493c42ded9bc Mon Sep 17 00:00:00 2001 From: Alessandro Genova Date: Sat, 30 Aug 2025 23:46:34 -0400 Subject: [PATCH 138/179] Allow users to set independent buzzer volume for signal/alarm --- movement.c | 44 ++++++++++++++++--- movement.h | 18 +++++++- movement_config.h | 2 + watch-faces/settings/settings_face.c | 66 +++++++++++++++++++++++++++- watch-faces/settings/settings_face.h | 6 +++ 5 files changed, 127 insertions(+), 9 deletions(-) diff --git a/movement.c b/movement.c index 23eeb401..1bb0a4bb 100644 --- a/movement.c +++ b/movement.c @@ -157,6 +157,19 @@ static udatetime_t _movement_convert_date_time_to_udate(watch_date_time_t date_t }; } +static watch_buzzer_volume_t _movement_get_buzzer_volume(movement_buzzer_priority_t priority) { + switch (priority) { + case BUZZER_PRIORITY_BUTTON: + return movement_button_volume(); + case BUZZER_PRIORITY_SIGNAL: + return movement_signal_volume(); + case BUZZER_PRIORITY_ALARM: + return movement_alarm_volume(); + default: + return WATCH_BUZZER_VOLUME_LOUD; + } +} + static void _movement_set_top_of_minute_alarm() { uint32_t counter = watch_rtc_get_counter(); uint32_t next_minute_counter; @@ -512,15 +525,15 @@ void movement_play_note(watch_buzzer_note_t note, uint16_t duration_ms) { single_note_sequence[1] = (int8_t)duration; single_note_sequence[2] = 0; - movement_play_sequence(single_note_sequence, 0); + movement_play_sequence(single_note_sequence, BUZZER_PRIORITY_BUTTON); } void movement_play_signal(void) { - movement_play_sequence(signal_tune, 1); + movement_play_sequence(signal_tune, BUZZER_PRIORITY_SIGNAL); } void movement_play_alarm(void) { - movement_play_sequence(alarm_tune, 2); + movement_play_sequence(alarm_tune, BUZZER_PRIORITY_ALARM); } void movement_play_alarm_beeps(uint8_t rounds, watch_buzzer_note_t alarm_note) { @@ -550,10 +563,10 @@ void movement_play_alarm_beeps(uint8_t rounds, watch_buzzer_note_t alarm_note) { custom_alarm_tune[18] = 0; - movement_play_sequence(custom_alarm_tune, 2); + movement_play_sequence(custom_alarm_tune, BUZZER_PRIORITY_ALARM); } -void movement_play_sequence(int8_t *note_sequence, uint8_t priority) { +void movement_play_sequence(int8_t *note_sequence, movement_buzzer_priority_t priority) { // Priority is used to ensure that lower priority sequences don't cancel higher priority ones // Priotity order: alarm(2) > signal(1) > note(0) if (priority < movement_volatile_state.pending_sequence_priority) { @@ -569,7 +582,7 @@ void movement_play_sequence(int8_t *note_sequence, uint8_t priority) { movement_volatile_state.has_pending_sequence = true; movement_volatile_state.exit_sleep_mode = true; } else { - watch_buzzer_play_sequence_with_volume(note_sequence, NULL, movement_button_volume()); + watch_buzzer_play_sequence_with_volume(note_sequence, NULL, _movement_get_buzzer_volume(priority)); } } @@ -671,6 +684,21 @@ void movement_set_button_volume(watch_buzzer_volume_t value) { movement_state.settings.bit.button_volume = value; } +watch_buzzer_volume_t movement_signal_volume(void) { + return movement_state.signal_volume; +} +void movement_set_signal_volume(watch_buzzer_volume_t value) { + movement_state.signal_volume = value; +} + +watch_buzzer_volume_t movement_alarm_volume(void) { + return movement_state.alarm_volume; +} + +void movement_set_alarm_volume(watch_buzzer_volume_t value) { + movement_state.alarm_volume = value; +} + movement_clock_mode_t movement_clock_mode_24h(void) { return movement_state.settings.bit.clock_mode_24h ? MOVEMENT_CLOCK_MODE_24H : MOVEMENT_CLOCK_MODE_12H; } @@ -952,6 +980,8 @@ void app_init(void) { if (movement_state.accelerometer_motion_threshold == 0) movement_state.accelerometer_motion_threshold = 32; + movement_state.signal_volume = MOVEMENT_DEFAULT_SIGNAL_VOLUME; + movement_state.alarm_volume = MOVEMENT_DEFAULT_ALARM_VOLUME; movement_state.light_on = false; movement_state.next_available_backup_register = 2; _movement_reset_inactivity_countdown(); @@ -1241,7 +1271,7 @@ bool app_loop(void) { // If we woke up to play a note sequence, actually play the note sequence we were asked to play while in deep sleep. if (movement_volatile_state.has_pending_sequence) { movement_volatile_state.has_pending_sequence = false; - watch_buzzer_play_sequence_with_volume(_pending_sequence, movement_request_sleep, movement_button_volume()); + watch_buzzer_play_sequence_with_volume(_pending_sequence, movement_request_sleep, _movement_get_buzzer_volume(movement_volatile_state.pending_sequence_priority)); // When this sequence is done playing, movement_request_sleep is invoked and the watch will go, // back to sleep (unless the user interacts with it in the meantime) _pending_sequence = NULL; diff --git a/movement.h b/movement.h index c1ac756f..6e997d4a 100644 --- a/movement.h +++ b/movement.h @@ -145,6 +145,12 @@ typedef enum { MINUTE_TIMEOUT, // Top of the Minute timeout } movement_timeout_index_t; +typedef enum { + BUZZER_PRIORITY_BUTTON = 0, // Buzzer priority for button beeps (lowest priority). + BUZZER_PRIORITY_SIGNAL, // Buzzer priority for hourly chime (medium priority). + BUZZER_PRIORITY_ALARM, // Buzzer priority for hourly chime (highest priority). +} movement_buzzer_priority_t; + typedef struct { uint8_t event_type; uint8_t subsecond; @@ -286,6 +292,10 @@ typedef struct { lis2dw_data_rate_t accelerometer_background_rate; // threshold for considering the wearer is in motion uint8_t accelerometer_motion_threshold; + + // signal and alarm volumes + watch_buzzer_volume_t signal_volume; + watch_buzzer_volume_t alarm_volume; } movement_state_t; void movement_move_to_face(uint8_t watch_face_index); @@ -318,7 +328,7 @@ void movement_play_note(watch_buzzer_note_t note, uint16_t duration_ms); void movement_play_signal(void); void movement_play_alarm(void); void movement_play_alarm_beeps(uint8_t rounds, watch_buzzer_note_t alarm_note); -void movement_play_sequence(int8_t *note_sequence, uint8_t priority); +void movement_play_sequence(int8_t *note_sequence, movement_buzzer_priority_t priority); uint8_t movement_claim_backup_register(void); @@ -343,6 +353,12 @@ void movement_set_button_should_sound(bool value); watch_buzzer_volume_t movement_button_volume(void); void movement_set_button_volume(watch_buzzer_volume_t value); +watch_buzzer_volume_t movement_signal_volume(void); +void movement_set_signal_volume(watch_buzzer_volume_t value); + +watch_buzzer_volume_t movement_alarm_volume(void); +void movement_set_alarm_volume(watch_buzzer_volume_t value); + movement_clock_mode_t movement_clock_mode_24h(void); void movement_set_clock_mode_24h(movement_clock_mode_t value); diff --git a/movement_config.h b/movement_config.h index 9633de87..a20cd3c2 100644 --- a/movement_config.h +++ b/movement_config.h @@ -69,6 +69,8 @@ const watch_face_t watch_faces[] = { #define MOVEMENT_DEFAULT_BUTTON_SOUND true #define MOVEMENT_DEFAULT_BUTTON_VOLUME WATCH_BUZZER_VOLUME_SOFT +#define MOVEMENT_DEFAULT_SIGNAL_VOLUME WATCH_BUZZER_VOLUME_LOUD +#define MOVEMENT_DEFAULT_ALARM_VOLUME WATCH_BUZZER_VOLUME_LOUD /* Set the timeout before switching back to the main watch face * Valid values are: diff --git a/watch-faces/settings/settings_face.c b/watch-faces/settings/settings_face.c index 44d70f1b..73bdadc1 100644 --- a/watch-faces/settings/settings_face.c +++ b/watch-faces/settings/settings_face.c @@ -77,6 +77,64 @@ static void beep_setting_advance(void) { } } +static void signal_setting_display(uint8_t subsecond) { + watch_display_text_with_fallback(WATCH_POSITION_TOP_LEFT, "SIG", "SI"); + watch_display_text(WATCH_POSITION_BOTTOM, "SIGNAL"); + if (subsecond % 2) { + if (movement_signal_volume() == WATCH_BUZZER_VOLUME_LOUD) { + // H for HIGH + watch_display_text(WATCH_POSITION_TOP_RIGHT, " H"); + } + else { + // L for LOW + watch_display_text(WATCH_POSITION_TOP_RIGHT, " L"); + } + } +} + +static void signal_setting_advance(void) { + if (movement_signal_volume() == WATCH_BUZZER_VOLUME_SOFT) { + // was soft. make it loud. + movement_set_signal_volume(WATCH_BUZZER_VOLUME_LOUD); + } else { + // was loud. make it soft. + movement_set_signal_volume(WATCH_BUZZER_VOLUME_SOFT); + } + + signal_setting_display(1); + movement_play_signal(); +} + + +static void alarm_setting_display(uint8_t subsecond) { + watch_display_text_with_fallback(WATCH_POSITION_TOP_LEFT, "ALM", "AL"); + watch_display_text(WATCH_POSITION_BOTTOM, "ALARM "); + if (subsecond % 2) { + if (movement_alarm_volume() == WATCH_BUZZER_VOLUME_LOUD) { + // H for HIGH + watch_display_text(WATCH_POSITION_TOP_RIGHT, " H"); + } + else { + // L for LOW + watch_display_text(WATCH_POSITION_TOP_RIGHT, " L"); + } + } +} + +static void alarm_setting_advance(void) { + if (movement_alarm_volume() == WATCH_BUZZER_VOLUME_SOFT) { + // was soft. make it loud. + movement_set_alarm_volume(WATCH_BUZZER_VOLUME_LOUD); + } else { + // was loud. make it soft. + movement_set_alarm_volume(WATCH_BUZZER_VOLUME_SOFT); + + } + + alarm_setting_display(1); + movement_play_alarm(); +} + static void timeout_setting_display(uint8_t subsecond) { watch_display_text_with_fallback(WATCH_POSITION_TOP, "TMOUt", "TO"); if (subsecond % 2) { @@ -235,7 +293,7 @@ void settings_face_setup(uint8_t watch_face_index, void ** context_ptr) { settings_state_t *state = (settings_state_t *)*context_ptr; int8_t current_setting = 0; - state->num_settings = 5; // baseline, without LED settings + state->num_settings = 7; // baseline, without LED settings #ifdef BUILD_GIT_HASH state->num_settings++; #endif @@ -256,6 +314,12 @@ void settings_face_setup(uint8_t watch_face_index, void ** context_ptr) { state->settings_screens[current_setting].display = beep_setting_display; state->settings_screens[current_setting].advance = beep_setting_advance; current_setting++; + state->settings_screens[current_setting].display = signal_setting_display; + state->settings_screens[current_setting].advance = signal_setting_advance; + current_setting++; + state->settings_screens[current_setting].display = alarm_setting_display; + state->settings_screens[current_setting].advance = alarm_setting_advance; + current_setting++; state->settings_screens[current_setting].display = timeout_setting_display; state->settings_screens[current_setting].advance = timeout_setting_advance; current_setting++; diff --git a/watch-faces/settings/settings_face.h b/watch-faces/settings/settings_face.h index 49e96b98..fd9256ea 100644 --- a/watch-faces/settings/settings_face.h +++ b/watch-faces/settings/settings_face.h @@ -44,6 +44,12 @@ * This setting allows you to choose whether the Mode button should emit * a beep when pressed, and if so, how loud it should be. Options are * "Y" for yes and "N" for no. + * + * SI / SIG - Signal beep. + * This setting allows you to choose the hourly chime buzzer volume. + * + * AL / ALM - Alarm beep. + * This setting allows you to choose the hourly chime buzzer volume. * * TO / Tmout - Timeout. * Sets the time until screens that time out (like Settings and Time Set) From 961db3cc71a6e6eec77a34bfc23bf218b7335d15 Mon Sep 17 00:00:00 2001 From: Alessandro Genova Date: Fri, 12 Sep 2025 21:20:05 -0400 Subject: [PATCH 139/179] fix instant light not turning off after a long press --- movement.c | 1 + 1 file changed, 1 insertion(+) diff --git a/movement.c b/movement.c index 1bb0a4bb..1c9db617 100644 --- a/movement.c +++ b/movement.c @@ -435,6 +435,7 @@ bool movement_default_loop_handler(movement_event_t event) { movement_illuminate_led(); break; case EVENT_LIGHT_BUTTON_UP: + case EVENT_LIGHT_LONG_UP: if (movement_state.settings.bit.led_duration == 0) { movement_force_led_off(); } From e5ddbe3c10f2dfdd2ce11b5f74f97fb3758ad1d2 Mon Sep 17 00:00:00 2001 From: Alessandro Genova Date: Fri, 12 Sep 2025 21:26:02 -0400 Subject: [PATCH 140/179] immediately process events when coming out of deep sleep --- movement.c | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/movement.c b/movement.c index 1c9db617..14944bea 100644 --- a/movement.c +++ b/movement.c @@ -1277,6 +1277,10 @@ bool app_loop(void) { // back to sleep (unless the user interacts with it in the meantime) _pending_sequence = NULL; } + + // don't let the watch sleep when exiting deep sleep mode, + // so that app_loop will run again and process the events that may have fired. + can_sleep = false; } #endif From 13a516609716bb68ada905c1172b4b530ba17317 Mon Sep 17 00:00:00 2001 From: Alessandro Genova Date: Fri, 12 Sep 2025 21:41:59 -0400 Subject: [PATCH 141/179] use __builtin_ctz when processing pending events --- movement.c | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/movement.c b/movement.c index 14944bea..c1a5d087 100644 --- a/movement.c +++ b/movement.c @@ -1223,12 +1223,11 @@ bool app_loop(void) { // Consume all the pending events movement_event_type_t event_type = 0; while (pending_events) { - if (pending_events & 1) { - event.event_type = event_type; - can_sleep = wf->loop(event, watch_face_contexts[movement_state.current_face_idx]) && can_sleep; - } - pending_events = pending_events >> 1; - event_type++; + uint8_t next_event = __builtin_ctz(pending_events); + event.event_type = event_type + next_event; + can_sleep = wf->loop(event, watch_face_contexts[movement_state.current_face_idx]) && can_sleep; + pending_events = pending_events >> (next_event + 1); + event_type = event_type + next_event + 1; } // handle top-of-minute tasks, if the alarm handler told us we need to From 02faae3d25114300b7c96c5276d1e346804c1d92 Mon Sep 17 00:00:00 2001 From: Alessandro Genova Date: Mon, 15 Sep 2025 23:42:31 -0400 Subject: [PATCH 142/179] Events that follow a down event on the previous face should not be forwarded to the new face --- movement.c | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/movement.c b/movement.c index c1a5d087..34e5645e 100644 --- a/movement.c +++ b/movement.c @@ -61,6 +61,11 @@ watch_date_time_t scheduled_tasks[MOVEMENT_NUM_FACES]; const int32_t movement_le_inactivity_deadlines[8] = {INT_MAX, 600, 3600, 7200, 21600, 43200, 86400, 604800}; const int16_t movement_timeout_inactivity_deadlines[4] = {60, 120, 300, 1800}; +const uint32_t _movement_mode_button_events_mask = 0b1111 << EVENT_MODE_BUTTON_DOWN; +const uint32_t _movement_light_button_events_mask = 0b1111 << EVENT_LIGHT_BUTTON_DOWN; +const uint32_t _movement_alarm_button_events_mask = 0b1111 << EVENT_ALARM_BUTTON_DOWN; +const uint32_t _movement_button_events_mask = _movement_mode_button_events_mask | _movement_light_button_events_mask | _movement_alarm_button_events_mask; + typedef struct { movement_event_type_t down_event; watch_cb_t cb_longpress; @@ -93,6 +98,9 @@ typedef struct { movement_button_t mode_button; movement_button_t light_button; movement_button_t alarm_button; + + // button events that will not be passed to the current face loop, but will instead passed directly to the default loop handler. + volatile uint32_t passthrough_events; } movement_volatile_state_t; movement_volatile_state_t movement_volatile_state; @@ -278,6 +286,12 @@ static void _movement_handle_button_presses(uint32_t pending_events) { &movement_volatile_state.alarm_button }; + uint32_t button_events_masks[3] = { + _movement_mode_button_events_mask, + _movement_light_button_events_mask, + _movement_alarm_button_events_mask, + }; + for (uint8_t i = 0; i < 3; i++) { movement_button_t* button = buttons[i]; @@ -285,6 +299,8 @@ static void _movement_handle_button_presses(uint32_t pending_events) { if (pending_events & (1 << button->down_event)) { watch_rtc_register_comp_callback_no_schedule(button->cb_longpress, button->down_timestamp + MOVEMENT_LONG_PRESS_TICKS, button->timeout_index); any_down = true; + // this button's events will start getting passed to the face + movement_volatile_state.passthrough_events &= ~button_events_masks[i]; } // If a button up or button long up occurred @@ -1173,6 +1189,9 @@ static bool _switch_face(void) { movement_state.watch_face_changed = false; bool can_sleep = wf->loop(event, watch_face_contexts[movement_state.current_face_idx]); + // Button events that follow a down event that happened on the previous face should not be forwarded to the new face + movement_volatile_state.passthrough_events = _movement_button_events_mask; + return can_sleep; } @@ -1221,7 +1240,19 @@ bool app_loop(void) { } // Consume all the pending events + uint32_t passthrough_pending_events = pending_events & movement_volatile_state.passthrough_events; + pending_events = pending_events & ~movement_volatile_state.passthrough_events; + movement_event_type_t event_type = 0; + while (passthrough_pending_events) { + uint8_t next_event = __builtin_ctz(passthrough_pending_events); + event.event_type = event_type + next_event; + can_sleep = movement_default_loop_handler(event) && can_sleep; + passthrough_pending_events = passthrough_pending_events >> (next_event + 1); + event_type = event_type + next_event + 1; + } + + event_type = 0; while (pending_events) { uint8_t next_event = __builtin_ctz(pending_events); event.event_type = event_type + next_event; From dc6aebb4f8b5edf0dcb88edff64ab35c789ba7ad Mon Sep 17 00:00:00 2001 From: Alessandro Genova Date: Sat, 18 Oct 2025 22:53:12 -0400 Subject: [PATCH 143/179] add fast_stopwatch_face to default, and remove rtccount_face --- movement_config.h | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/movement_config.h b/movement_config.h index a20cd3c2..2f48d8fd 100644 --- a/movement_config.h +++ b/movement_config.h @@ -32,14 +32,13 @@ const watch_face_t watch_faces[] = { world_clock_face, sunrise_sunset_face, moon_phase_face, - stopwatch_face, + fast_stopwatch_face, countdown_face, alarm_face, temperature_display_face, voltage_face, settings_face, set_time_face, - rtccount_face, }; #define MOVEMENT_NUM_FACES (sizeof(watch_faces) / sizeof(watch_face_t)) From d9b65ed5c8a1696abcd336bb5106396b3d942661 Mon Sep 17 00:00:00 2001 From: Alessandro Genova Date: Sat, 18 Oct 2025 23:23:43 -0400 Subject: [PATCH 144/179] ensure a button up event is always emitted --- movement.c | 45 +++++++++++++++++++++------------------------ 1 file changed, 21 insertions(+), 24 deletions(-) diff --git a/movement.c b/movement.c index 34e5645e..2c9cda21 100644 --- a/movement.c +++ b/movement.c @@ -1394,46 +1394,43 @@ void cb_alarm_btn_interrupt(void) { movement_volatile_state.pending_events |= 1 << _process_button_event(pin_level, &movement_volatile_state.alarm_button); } -static movement_event_type_t _process_button_longpress_timeout(movement_button_t* button) { - // Looks like all these checks are not needed for the longpress detection to work reliably. - // Keep the code around for now in case problems arise long-term. +static movement_event_type_t _process_button_longpress_timeout(bool pin_level, movement_button_t* button) { + if (!button->is_down) { + return EVENT_NONE; + } - // if (!button->is_down) { - // return EVENT_NONE; - // } - - // movement_event_type_t up_event = button->down_event + 1; - - // if (movement_volatile_state.pending_events & 1 << up_event) { - // return EVENT_NONE; - // } - - // uint32_t counter = watch_rtc_get_counter(); - // if ((counter - button->down_timestamp) < MOVEMENT_LONG_PRESS_TICKS) { - // return EVENT_NONE; - // } - - movement_event_type_t longpress_event = button->down_event + 2; - - return longpress_event; + if (pin_level) { + return button->down_event + 2; // event_longpress + } else { + // hypotetical corner case: if the timeout fired but the pin level is actually up, we may have missed/rejected the up event, so fire it here +#if MOVEMENT_DEBOUNCE_TICKS + // we're in a corner case, we don't know when the up actually happened. + button->up_timestamp = button->down_timestamp; +#endif + button->is_down = false; + return button->down_event + 1; // event_up + } } void cb_light_btn_timeout_interrupt(void) { + bool pin_level = HAL_GPIO_BTN_LIGHT_read(); movement_button_t* button = &movement_volatile_state.light_button; - movement_volatile_state.pending_events |= 1 << _process_button_longpress_timeout(button); + movement_volatile_state.pending_events |= 1 << _process_button_longpress_timeout(pin_level, button); } void cb_mode_btn_timeout_interrupt(void) { + bool pin_level = HAL_GPIO_BTN_MODE_read(); movement_button_t* button = &movement_volatile_state.mode_button; - movement_volatile_state.pending_events |= 1 << _process_button_longpress_timeout(button); + movement_volatile_state.pending_events |= 1 << _process_button_longpress_timeout(pin_level, button); } void cb_alarm_btn_timeout_interrupt(void) { + bool pin_level = HAL_GPIO_BTN_ALARM_read(); movement_button_t* button = &movement_volatile_state.alarm_button; - movement_volatile_state.pending_events |= 1 << _process_button_longpress_timeout(button); + movement_volatile_state.pending_events |= 1 << _process_button_longpress_timeout(pin_level, button); } void cb_led_timeout_interrupt(void) { From 24400c53bbabd661b093832c208fef8db1c26950 Mon Sep 17 00:00:00 2001 From: Alessandro Genova Date: Sun, 19 Oct 2025 21:11:33 -0400 Subject: [PATCH 145/179] fix slow default alarm tune --- movement.c | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/movement.c b/movement.c index 2c9cda21..000490c5 100644 --- a/movement.c +++ b/movement.c @@ -110,14 +110,14 @@ static int8_t *_pending_sequence; // The note sequence of the default alarm int8_t alarm_tune[] = { - BUZZER_NOTE_C8, 4, - BUZZER_NOTE_REST, 7, - BUZZER_NOTE_C8, 4, - BUZZER_NOTE_REST, 7, - BUZZER_NOTE_C8, 4, - BUZZER_NOTE_REST, 7, - BUZZER_NOTE_C8, 4, - BUZZER_NOTE_REST, 27, + BUZZER_NOTE_C8, 3, + BUZZER_NOTE_REST, 4, + BUZZER_NOTE_C8, 3, + BUZZER_NOTE_REST, 4, + BUZZER_NOTE_C8, 3, + BUZZER_NOTE_REST, 4, + BUZZER_NOTE_C8, 5, + BUZZER_NOTE_REST, 38, -8, 9, 0 }; From 7526b30b3d72e8af97ba68e517a639ef9f0b8af2 Mon Sep 17 00:00:00 2001 From: Alessandro Genova Date: Mon, 27 Oct 2025 00:22:14 -0400 Subject: [PATCH 146/179] talk to accelerometer in the app loop rather than interrupt routine --- movement.c | 36 ++++++++++++++++++++++++++---------- 1 file changed, 26 insertions(+), 10 deletions(-) diff --git a/movement.c b/movement.c index 000490c5..f90aac93 100644 --- a/movement.c +++ b/movement.c @@ -93,6 +93,7 @@ typedef struct { volatile bool is_buzzing; volatile uint8_t pending_sequence_priority; volatile bool schedule_next_comp; + volatile bool has_pending_accelerometer; // button tracking for long press movement_button_t mode_button; @@ -276,6 +277,24 @@ static void _movement_renew_top_of_minute_alarm(void) { movement_volatile_state.schedule_next_comp = true; } +static uint32_t _movement_get_accelerometer_events() { + uint32_t accelerometer_events = 0; + + uint8_t int_src = lis2dw_get_interrupt_source(); + + if (int_src & LIS2DW_REG_ALL_INT_SRC_DOUBLE_TAP) { + accelerometer_events |= 1 << EVENT_DOUBLE_TAP; + printf("Double tap!\n"); + } + + if (int_src & LIS2DW_REG_ALL_INT_SRC_SINGLE_TAP) { + accelerometer_events |= 1 << EVENT_SINGLE_TAP; + printf("Single tap!\n"); + } + + return accelerometer_events; +} + static void _movement_handle_button_presses(uint32_t pending_events) { bool any_up = false; bool any_down = false; @@ -910,6 +929,7 @@ void app_init(void) { movement_volatile_state.enter_sleep_mode = false; movement_volatile_state.exit_sleep_mode = false; movement_volatile_state.has_pending_sequence = false; + movement_volatile_state.has_pending_accelerometer = false; movement_volatile_state.is_sleeping = false; movement_volatile_state.is_buzzing = false; @@ -1221,6 +1241,11 @@ bool app_loop(void) { } } + if (movement_volatile_state.has_pending_accelerometer) { + movement_volatile_state.has_pending_accelerometer = false; + pending_events |= _movement_get_accelerometer_events(); + } + // handle any button up/down events that occurred, e.g. schedule longpress timeouts, reset inactivity, etc. _movement_handle_button_presses(pending_events); @@ -1468,16 +1493,7 @@ void cb_tick(void) { } void cb_accelerometer_event(void) { - uint8_t int_src = lis2dw_get_interrupt_source(); - - if (int_src & LIS2DW_REG_ALL_INT_SRC_DOUBLE_TAP) { - movement_volatile_state.pending_events |= 1 << EVENT_DOUBLE_TAP; - printf("Double tap!\n"); - } - if (int_src & LIS2DW_REG_ALL_INT_SRC_SINGLE_TAP) { - movement_volatile_state.pending_events |= 1 << EVENT_SINGLE_TAP; - printf("Single tap!\n"); - } + movement_volatile_state.has_pending_accelerometer = true; } void cb_accelerometer_wake(void) { From c5fe350fa1ab16c03f304f21ee6d534ff2f5891e Mon Sep 17 00:00:00 2001 From: David Volovskiy Date: Sun, 16 Nov 2025 10:15:46 -0500 Subject: [PATCH 147/179] Explicitly set interrupt type to latched --- movement.c | 5 +++-- watch-library/shared/driver/lis2dw.c | 14 ++++++++++++++ watch-library/shared/driver/lis2dw.h | 4 ++++ 3 files changed, 21 insertions(+), 2 deletions(-) diff --git a/movement.c b/movement.c index f90aac93..345dcb34 100644 --- a/movement.c +++ b/movement.c @@ -284,12 +284,12 @@ static uint32_t _movement_get_accelerometer_events() { if (int_src & LIS2DW_REG_ALL_INT_SRC_DOUBLE_TAP) { accelerometer_events |= 1 << EVENT_DOUBLE_TAP; - printf("Double tap!\n"); + printf("Double tap!\r\n"); } if (int_src & LIS2DW_REG_ALL_INT_SRC_SINGLE_TAP) { accelerometer_events |= 1 << EVENT_SINGLE_TAP; - printf("Single tap!\n"); + printf("Single tap!\r\n"); } return accelerometer_events; @@ -1118,6 +1118,7 @@ void app_setup(void) { watch_register_interrupt_callback(HAL_GPIO_A3_pin(), cb_accelerometer_event, INTERRUPT_TRIGGER_RISING); // Enable the interrupts... + lis2dw_latched_interrupts(); lis2dw_enable_interrupts(); // At first boot, this next line sets the accelerometer's sampling rate to 0, which is LIS2DW_DATA_RATE_POWERDOWN. diff --git a/watch-library/shared/driver/lis2dw.c b/watch-library/shared/driver/lis2dw.c index 1c89f244..6e3cb59a 100644 --- a/watch-library/shared/driver/lis2dw.c +++ b/watch-library/shared/driver/lis2dw.c @@ -425,6 +425,20 @@ void lis2dw_disable_interrupts(void) { #endif } +void lis2dw_pulsed_interrupts(void) { +#ifdef I2C_SERCOM + uint8_t configuration = watch_i2c_read8(LIS2DW_ADDRESS, LIS2DW_REG_CTRL7); + watch_i2c_write8(LIS2DW_ADDRESS, LIS2DW_REG_CTRL7, configuration | LIS2DW_CTRL7_VAL_DRDY_PULSED); +#endif +} + +void lis2dw_latched_interrupts(void) { +#ifdef I2C_SERCOM + uint8_t configuration = watch_i2c_read8(LIS2DW_ADDRESS, LIS2DW_REG_CTRL7); + watch_i2c_write8(LIS2DW_ADDRESS, LIS2DW_REG_CTRL7, configuration & ~LIS2DW_CTRL7_VAL_DRDY_PULSED); +#endif +} + lis2dw_wakeup_source_t lis2dw_get_wakeup_source() { #ifdef I2C_SERCOM return (lis2dw_wakeup_source_t) watch_i2c_read8(LIS2DW_ADDRESS, LIS2DW_REG_WAKE_UP_SRC); diff --git a/watch-library/shared/driver/lis2dw.h b/watch-library/shared/driver/lis2dw.h index 3b017c65..2db57da1 100644 --- a/watch-library/shared/driver/lis2dw.h +++ b/watch-library/shared/driver/lis2dw.h @@ -375,6 +375,10 @@ void lis2dw_enable_interrupts(void); void lis2dw_disable_interrupts(void); +void lis2dw_pulsed_interrupts(void); + +void lis2dw_latched_interrupts(void); + lis2dw_interrupt_source_t lis2dw_get_interrupt_source(void); lis2dw_wakeup_source_t lis2dw_get_wakeup_source(void); From dd054efe8d6dc40a22dc7c7be26ba6eebbdca02b Mon Sep 17 00:00:00 2001 From: David Volovskiy Date: Sun, 16 Nov 2025 16:05:22 -0500 Subject: [PATCH 148/179] Added LIR latching on LIS2DW --- movement.c | 3 ++- watch-library/shared/driver/lis2dw.c | 31 ++++++++++++++++++++++++++-- watch-library/shared/driver/lis2dw.h | 14 +++++++++++-- 3 files changed, 43 insertions(+), 5 deletions(-) diff --git a/movement.c b/movement.c index 345dcb34..a3a61233 100644 --- a/movement.c +++ b/movement.c @@ -816,6 +816,7 @@ bool movement_enable_tap_detection_if_available(void) { lis2dw_set_data_rate(LIS2DW_DATA_RATE_HP_400_HZ); lis2dw_set_mode(LIS2DW_MODE_LOW_POWER); lis2dw_enable_double_tap(); + lis2dw12_int_notification_set(LIS2DW12_INT_LATCHED); // Settling time (1 sample duration, i.e. 1/400Hz) delay_ms(3); @@ -836,6 +837,7 @@ bool movement_disable_tap_detection_if_available(void) { lis2dw_set_data_rate(movement_state.accelerometer_background_rate); lis2dw_set_mode(LIS2DW_MODE_LOW_POWER); lis2dw_disable_double_tap(); + lis2dw12_int_notification_set(LIS2DW12_INT_PULSED); // ...disable Z axis (not sure if this is needed, does this save power?)... lis2dw_configure_tap_threshold(0, 0, 0, 0); @@ -1118,7 +1120,6 @@ void app_setup(void) { watch_register_interrupt_callback(HAL_GPIO_A3_pin(), cb_accelerometer_event, INTERRUPT_TRIGGER_RISING); // Enable the interrupts... - lis2dw_latched_interrupts(); lis2dw_enable_interrupts(); // At first boot, this next line sets the accelerometer's sampling rate to 0, which is LIS2DW_DATA_RATE_POWERDOWN. diff --git a/watch-library/shared/driver/lis2dw.c b/watch-library/shared/driver/lis2dw.c index 6e3cb59a..339af08d 100644 --- a/watch-library/shared/driver/lis2dw.c +++ b/watch-library/shared/driver/lis2dw.c @@ -411,6 +411,33 @@ void lis2dw_configure_int2(uint8_t sources) { #endif } +void lis2dw12_int_notification_set(lis2dw12_lir_t val) { +#ifdef I2C_SERCOM + uint8_t configuration = watch_i2c_read8(LIS2DW_ADDRESS, LIS2DW_REG_CTRL3); + if (val == LIS2DW12_INT_LATCHED) { + configuration |= LIS2DW_CTRL3_VAL_LIR; + } else { + configuration &= ~LIS2DW_CTRL7_VAL_DRDY_PULSED; + } + watch_i2c_write8(LIS2DW_ADDRESS, LIS2DW_REG_CTRL3, configuration); +#else + (void)val; +#endif +} + +lis2dw12_lir_t lis2dw12_int_notification_get(void) { +#ifdef I2C_SERCOM + uint8_t configuration = watch_i2c_read8(LIS2DW_ADDRESS, LIS2DW_REG_CTRL3); + if (configuration & LIS2DW12_INT_LATCHED) { + return LIS2DW12_INT_LATCHED; + } else { + return LIS2DW12_INT_PULSED; + } +#else + return LIS2DW12_INT_PULSED; +#endif +} + void lis2dw_enable_interrupts(void) { #ifdef I2C_SERCOM uint8_t configuration = watch_i2c_read8(LIS2DW_ADDRESS, LIS2DW_REG_CTRL7); @@ -425,14 +452,14 @@ void lis2dw_disable_interrupts(void) { #endif } -void lis2dw_pulsed_interrupts(void) { +void lis2dw_pulsed_drdy_interrupts(void) { #ifdef I2C_SERCOM uint8_t configuration = watch_i2c_read8(LIS2DW_ADDRESS, LIS2DW_REG_CTRL7); watch_i2c_write8(LIS2DW_ADDRESS, LIS2DW_REG_CTRL7, configuration | LIS2DW_CTRL7_VAL_DRDY_PULSED); #endif } -void lis2dw_latched_interrupts(void) { +void lis2dw_latched_drdy_interrupts(void) { #ifdef I2C_SERCOM uint8_t configuration = watch_i2c_read8(LIS2DW_ADDRESS, LIS2DW_REG_CTRL7); watch_i2c_write8(LIS2DW_ADDRESS, LIS2DW_REG_CTRL7, configuration & ~LIS2DW_CTRL7_VAL_DRDY_PULSED); diff --git a/watch-library/shared/driver/lis2dw.h b/watch-library/shared/driver/lis2dw.h index 2db57da1..f7b0a801 100644 --- a/watch-library/shared/driver/lis2dw.h +++ b/watch-library/shared/driver/lis2dw.h @@ -92,6 +92,12 @@ typedef enum { LIS2DW_FILTER_HIGH_PASS = 1, } lis2dw_filter_t; +typedef enum +{ + LIS2DW12_INT_PULSED = 0, + LIS2DW12_INT_LATCHED = 1, +} lis2dw12_lir_t; + typedef enum { LIS2DW_RANGE_16_G = 0b11, // +/- 16g LIS2DW_RANGE_8_G = 0b10, // +/- 8g @@ -367,6 +373,10 @@ void lis2dw_configure_tap_threshold(uint8_t threshold_x, uint8_t threshold_y, ui void lis2dw_configure_tap_duration(uint8_t latency, uint8_t quiet, uint8_t shock); +void lis2dw12_int_notification_set(lis2dw12_lir_t val); + +lis2dw12_lir_t lis2dw12_int_notification_get(void); + void lis2dw_configure_int1(uint8_t sources); void lis2dw_configure_int2(uint8_t sources); @@ -375,9 +385,9 @@ void lis2dw_enable_interrupts(void); void lis2dw_disable_interrupts(void); -void lis2dw_pulsed_interrupts(void); +void lis2dw_pulsed_drdy_interrupts(void); -void lis2dw_latched_interrupts(void); +void lis2dw_latched_drdy_interrupts(void); lis2dw_interrupt_source_t lis2dw_get_interrupt_source(void); From 359bf0df692e4d77d08010b7edef87e864bec2b7 Mon Sep 17 00:00:00 2001 From: David Volovskiy Date: Sun, 16 Nov 2025 16:06:08 -0500 Subject: [PATCH 149/179] Saw double-taps getting missed with latched LIR so moving back to pulsed --- movement.c | 2 -- 1 file changed, 2 deletions(-) diff --git a/movement.c b/movement.c index a3a61233..361c8c89 100644 --- a/movement.c +++ b/movement.c @@ -816,7 +816,6 @@ bool movement_enable_tap_detection_if_available(void) { lis2dw_set_data_rate(LIS2DW_DATA_RATE_HP_400_HZ); lis2dw_set_mode(LIS2DW_MODE_LOW_POWER); lis2dw_enable_double_tap(); - lis2dw12_int_notification_set(LIS2DW12_INT_LATCHED); // Settling time (1 sample duration, i.e. 1/400Hz) delay_ms(3); @@ -837,7 +836,6 @@ bool movement_disable_tap_detection_if_available(void) { lis2dw_set_data_rate(movement_state.accelerometer_background_rate); lis2dw_set_mode(LIS2DW_MODE_LOW_POWER); lis2dw_disable_double_tap(); - lis2dw12_int_notification_set(LIS2DW12_INT_PULSED); // ...disable Z axis (not sure if this is needed, does this save power?)... lis2dw_configure_tap_threshold(0, 0, 0, 0); From 372992e2594b47bd36b3da7646b2b5cb6f6cc51b Mon Sep 17 00:00:00 2001 From: David Volovskiy Date: Wed, 19 Nov 2025 08:06:59 -0500 Subject: [PATCH 150/179] Added print for LIS2DW interrupt --- movement.c | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/movement.c b/movement.c index 361c8c89..70249d60 100644 --- a/movement.c +++ b/movement.c @@ -277,10 +277,20 @@ static void _movement_renew_top_of_minute_alarm(void) { movement_volatile_state.schedule_next_comp = true; } +#define PRINT_LIS_EVENTS false static uint32_t _movement_get_accelerometer_events() { uint32_t accelerometer_events = 0; uint8_t int_src = lis2dw_get_interrupt_source(); +#if PRINT_LIS_EVENTS + printf("_movement_get_accelerometer_events\r\n"); + if (int_src & LIS2DW_REG_ALL_INT_SRC_SLEEP_CHANGE_IA) printf("Sleep Change IA\r\n"); + if (int_src & LIS2DW_REG_ALL_INT_SRC_6D_IA) printf("6D IA\r\n"); + if (int_src & LIS2DW_REG_ALL_INT_SRC_SINGLE_TAP) printf("Single Tap\r\n"); + if (int_src & LIS2DW_REG_ALL_INT_SRC_DOUBLE_TAP) printf("Double Tap\r\n"); + if (int_src & LIS2DW_REG_ALL_INT_SRC_WU_IA) printf("Wake Up\r\n"); + if (int_src & LIS2DW_REG_ALL_INT_SRC_FF_IA) printf("Free Fall\r\n"); +#endif if (int_src & LIS2DW_REG_ALL_INT_SRC_DOUBLE_TAP) { accelerometer_events |= 1 << EVENT_DOUBLE_TAP; From 8d551d26af14dc1c81f060b5806e7a6ba82dd5f2 Mon Sep 17 00:00:00 2001 From: David Volovskiy Date: Wed, 19 Nov 2025 08:08:01 -0500 Subject: [PATCH 151/179] Revert "Added print for LIS2DW interrupt" This reverts commit 0105fee2f176b38e51cd1da8118481b82bdb35ed. --- movement.c | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/movement.c b/movement.c index 70249d60..361c8c89 100644 --- a/movement.c +++ b/movement.c @@ -277,20 +277,10 @@ static void _movement_renew_top_of_minute_alarm(void) { movement_volatile_state.schedule_next_comp = true; } -#define PRINT_LIS_EVENTS false static uint32_t _movement_get_accelerometer_events() { uint32_t accelerometer_events = 0; uint8_t int_src = lis2dw_get_interrupt_source(); -#if PRINT_LIS_EVENTS - printf("_movement_get_accelerometer_events\r\n"); - if (int_src & LIS2DW_REG_ALL_INT_SRC_SLEEP_CHANGE_IA) printf("Sleep Change IA\r\n"); - if (int_src & LIS2DW_REG_ALL_INT_SRC_6D_IA) printf("6D IA\r\n"); - if (int_src & LIS2DW_REG_ALL_INT_SRC_SINGLE_TAP) printf("Single Tap\r\n"); - if (int_src & LIS2DW_REG_ALL_INT_SRC_DOUBLE_TAP) printf("Double Tap\r\n"); - if (int_src & LIS2DW_REG_ALL_INT_SRC_WU_IA) printf("Wake Up\r\n"); - if (int_src & LIS2DW_REG_ALL_INT_SRC_FF_IA) printf("Free Fall\r\n"); -#endif if (int_src & LIS2DW_REG_ALL_INT_SRC_DOUBLE_TAP) { accelerometer_events |= 1 << EVENT_DOUBLE_TAP; From b3c8f0f87a5bbb6bfd0485ca43a870614bbb51a4 Mon Sep 17 00:00:00 2001 From: Alessandro Genova Date: Fri, 21 Nov 2025 20:55:11 -0500 Subject: [PATCH 152/179] add missing copyright headers --- movement.h | 1 + watch-faces/demo/rtccount_face.c | 2 +- watch-faces/demo/rtccount_face.h | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/movement.h b/movement.h index 6e997d4a..94f9b83e 100644 --- a/movement.h +++ b/movement.h @@ -2,6 +2,7 @@ * MIT License * * Copyright (c) 2022 Joey Castillo + * Copyright (c) 2025 Alessandro Genova * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal diff --git a/watch-faces/demo/rtccount_face.c b/watch-faces/demo/rtccount_face.c index f08cde5b..e9bd2104 100644 --- a/watch-faces/demo/rtccount_face.c +++ b/watch-faces/demo/rtccount_face.c @@ -1,7 +1,7 @@ /* * MIT License * - * Copyright (c) 2022 Joey Castillo + * Copyright (c) 2025 Alessandro Genova * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal diff --git a/watch-faces/demo/rtccount_face.h b/watch-faces/demo/rtccount_face.h index 59f11929..a3478a2d 100644 --- a/watch-faces/demo/rtccount_face.h +++ b/watch-faces/demo/rtccount_face.h @@ -1,7 +1,7 @@ /* * MIT License * - * Copyright (c) 2022 Joey Castillo + * Copyright (c) 2025 Alessandro Genova * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal From fe4541d4ee11e93b3767b16be1a27e9f78111073 Mon Sep 17 00:00:00 2001 From: Alessandro Genova Date: Fri, 21 Nov 2025 21:02:07 -0500 Subject: [PATCH 153/179] fix compilation warnings in rtccount_face --- watch-faces/demo/rtccount_face.c | 28 ++++++++++++++++++---------- 1 file changed, 18 insertions(+), 10 deletions(-) diff --git a/watch-faces/demo/rtccount_face.c b/watch-faces/demo/rtccount_face.c index e9bd2104..72a3055a 100644 --- a/watch-faces/demo/rtccount_face.c +++ b/watch-faces/demo/rtccount_face.c @@ -48,6 +48,14 @@ typedef struct { static const uint32_t COUNTER_MASK = (1 << 19) - 1; +static void _rtccount_face_display_string(char* string, uint8_t pos) { + // watch_display_string is deprecated, but there is no alternative for this use-case + #pragma GCC diagnostic push + #pragma GCC diagnostic ignored "-Wdeprecated-declarations" + watch_display_string(string, pos); + #pragma GCC diagnostic pop +} + static void _rtccount_face_draw(movement_event_t event, rtccount_state_t* state) { uint32_t counter = watch_rtc_get_counter(); @@ -77,48 +85,48 @@ static void _rtccount_face_draw(movement_event_t event, rtccount_state_t* state) break; } - watch_display_string(buf, 0); + _rtccount_face_display_string(buf, 0); snprintf(buf, sizeof(buf), "%u", event.subsecond); uint32_t len = strlen(buf); - watch_display_string(buf, 4 - len); + _rtccount_face_display_string(buf, 4 - len); switch (state->status) { case RTCCOUNT_STATUS_COUNTER: { - snprintf(buf, sizeof(buf), "%lu", counter & COUNTER_MASK); + snprintf(buf, sizeof(buf), "%u", counter & COUNTER_MASK); size_t len = strlen(buf); - watch_display_string(buf, 10 - len); + _rtccount_face_display_string(buf, 10 - len); break; } case RTCCOUNT_STATUS_COUNTER_SUB: { - snprintf(buf, sizeof(buf), "%lu", counter & 127); + snprintf(buf, sizeof(buf), "%u", counter & 127); size_t len = strlen(buf); - watch_display_string(buf, 10 - len); + _rtccount_face_display_string(buf, 10 - len); break; } case RTCCOUNT_STATUS_MINUTES: { - snprintf(buf, sizeof(buf), "%lu", state->n_top_of_minute & COUNTER_MASK); + snprintf(buf, sizeof(buf), "%u", state->n_top_of_minute & COUNTER_MASK); size_t len = strlen(buf); - watch_display_string(buf, 10 - len); + _rtccount_face_display_string(buf, 10 - len); break; } case RTCCOUNT_STATUS_MINUTES_DIFF: { uint32_t elapsed_minutes = (movement_get_utc_timestamp() - state->ref_timestamp) / 60; - snprintf(buf, sizeof(buf), "%lu", (elapsed_minutes - state->n_top_of_minute) & COUNTER_MASK); + snprintf(buf, sizeof(buf), "%u", (elapsed_minutes - state->n_top_of_minute) & COUNTER_MASK); size_t len = strlen(buf); - watch_display_string(buf, 10 - len); + _rtccount_face_display_string(buf, 10 - len); break; } From c6270444f0400a97f10232a18fa0ebdf32cadff1 Mon Sep 17 00:00:00 2001 From: David Volovskiy Date: Sat, 6 Sep 2025 11:45:46 -0400 Subject: [PATCH 154/179] Typo fix --- watch-faces/settings/settings_face.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/watch-faces/settings/settings_face.h b/watch-faces/settings/settings_face.h index fd9256ea..b38e3d1a 100644 --- a/watch-faces/settings/settings_face.h +++ b/watch-faces/settings/settings_face.h @@ -49,7 +49,7 @@ * This setting allows you to choose the hourly chime buzzer volume. * * AL / ALM - Alarm beep. - * This setting allows you to choose the hourly chime buzzer volume. + * This setting allows you to choose the alarm buzzer volume. * * TO / Tmout - Timeout. * Sets the time until screens that time out (like Settings and Time Set) From c45fed8ea97818b4d291dab3ee57c9a1a4bd6fb9 Mon Sep 17 00:00:00 2001 From: David Volovskiy Date: Sat, 6 Sep 2025 11:49:00 -0400 Subject: [PATCH 155/179] Got rid of fallthrough warnings --- watch-faces/complication/fast_stopwatch_face.c | 2 +- watch-faces/io/chirpy_demo_face.c | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/watch-faces/complication/fast_stopwatch_face.c b/watch-faces/complication/fast_stopwatch_face.c index 63ef7176..70c1de96 100644 --- a/watch-faces/complication/fast_stopwatch_face.c +++ b/watch-faces/complication/fast_stopwatch_face.c @@ -326,7 +326,7 @@ bool fast_stopwatch_face_loop(movement_event_t event, void *context) { case EVENT_LIGHT_BUTTON_DOWN: case EVENT_LIGHT_LONG_PRESS: _button_beep(); - // Fall into the case below; + // fall through case EVENT_TICK: _draw_indicators(state, event, elapsed); _display_elapsed(state, elapsed); diff --git a/watch-faces/io/chirpy_demo_face.c b/watch-faces/io/chirpy_demo_face.c index 3a7ff300..f676e179 100644 --- a/watch-faces/io/chirpy_demo_face.c +++ b/watch-faces/io/chirpy_demo_face.c @@ -261,6 +261,7 @@ bool chirpy_demo_face_loop(movement_event_t event, void *context) { if (state->mode != CDM_CHIRPING) { movement_move_to_face(0); } + // fall through default: movement_default_loop_handler(event); break; From c6c97556ace998aaaa72cc18601e293347ea19d2 Mon Sep 17 00:00:00 2001 From: David Volovskiy Date: Tue, 7 Oct 2025 07:23:26 -0400 Subject: [PATCH 156/179] Added FIFO timeout to LIS2DW --- watch-library/shared/driver/lis2dw.c | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/watch-library/shared/driver/lis2dw.c b/watch-library/shared/driver/lis2dw.c index 339af08d..fbf1ac75 100644 --- a/watch-library/shared/driver/lis2dw.c +++ b/watch-library/shared/driver/lis2dw.c @@ -277,6 +277,7 @@ inline void lis2dw_disable_fifo(void) { #endif } +#define FIFO_TIMEOUT 102 // This is timeout seconds * 128[RTC_CNT_HZ]. So 800ms is 102 bool lis2dw_read_fifo(lis2dw_fifo_t *fifo_data) { #ifdef I2C_SERCOM uint8_t temp = watch_i2c_read8(LIS2DW_ADDRESS, LIS2DW_REG_FIFO_SAMPLE); @@ -284,7 +285,11 @@ bool lis2dw_read_fifo(lis2dw_fifo_t *fifo_data) { fifo_data->count = temp & LIS2DW_FIFO_SAMPLE_COUNT; + rtc_counter_t timeout_counter = watch_rtc_get_counter() + FIFO_TIMEOUT; for(int i = 0; i < fifo_data->count; i++) { + if (watch_rtc_get_counter() > timeout_counter) { + break; + } fifo_data->readings[i] = lis2dw_get_raw_reading(); } From 19f789e428e489290f6e05f663e4b8e7b7a7fb48 Mon Sep 17 00:00:00 2001 From: David Volovskiy Date: Tue, 7 Oct 2025 08:07:02 -0400 Subject: [PATCH 157/179] Made the timeout for lis2dw_read_fifo a variable --- .../sensor/accelerometer_data_acquisition_face.c | 4 ++-- watch-faces/sensor/lis2dw_monitor_face.c | 2 +- watch-library/shared/driver/lis2dw.c | 7 ++++--- watch-library/shared/driver/lis2dw.h | 2 +- 4 files changed, 8 insertions(+), 7 deletions(-) diff --git a/legacy/watch_faces/sensor/accelerometer_data_acquisition_face.c b/legacy/watch_faces/sensor/accelerometer_data_acquisition_face.c index 72584749..64a25a87 100644 --- a/legacy/watch_faces/sensor/accelerometer_data_acquisition_face.c +++ b/legacy/watch_faces/sensor/accelerometer_data_acquisition_face.c @@ -448,13 +448,13 @@ static void start_reading(accelerometer_data_acquisition_state_t *state) { state->records[state->pos++] = record; lis2dw_fifo_t fifo; - lis2dw_read_fifo(&fifo); // dump the fifo, this starts a fresh round of data in continue_reading + lis2dw_read_fifo(&fifo, 100); // dump the fifo, this starts a fresh round of data in continue_reading } static void continue_reading(accelerometer_data_acquisition_state_t *state) { printf("Continue reading\n"); lis2dw_fifo_t fifo; - lis2dw_read_fifo(&fifo); + lis2dw_read_fifo(&fifo, 100); fifo.count = min(fifo.count, 25); // hacky, but we need a consistent data rate; if we got a 26th data point, chuck it. uint8_t offset = 4 * (25 - fifo.count); // also hacky: we're sometimes short at the start. align to beginning of next second. diff --git a/watch-faces/sensor/lis2dw_monitor_face.c b/watch-faces/sensor/lis2dw_monitor_face.c index a717fe46..4459cf73 100644 --- a/watch-faces/sensor/lis2dw_monitor_face.c +++ b/watch-faces/sensor/lis2dw_monitor_face.c @@ -420,7 +420,7 @@ static void _monitor_update(lis2dw_monitor_state_t *state) lis2dw_fifo_t fifo; float x = 0, y = 0, z = 0; - lis2dw_read_fifo(&fifo); + lis2dw_read_fifo(&fifo, 25); if (fifo.count == 0) { return; } diff --git a/watch-library/shared/driver/lis2dw.c b/watch-library/shared/driver/lis2dw.c index fbf1ac75..6f77a94e 100644 --- a/watch-library/shared/driver/lis2dw.c +++ b/watch-library/shared/driver/lis2dw.c @@ -277,15 +277,15 @@ inline void lis2dw_disable_fifo(void) { #endif } -#define FIFO_TIMEOUT 102 // This is timeout seconds * 128[RTC_CNT_HZ]. So 800ms is 102 -bool lis2dw_read_fifo(lis2dw_fifo_t *fifo_data) { +bool lis2dw_read_fifo(lis2dw_fifo_t *fifo_data, uint32_t timeout) { + // timeout is in terms of 1/RTC_CNT_HZ seconds (likely 128 timeouts is one second) #ifdef I2C_SERCOM uint8_t temp = watch_i2c_read8(LIS2DW_ADDRESS, LIS2DW_REG_FIFO_SAMPLE); bool overrun = !!(temp & LIS2DW_FIFO_SAMPLE_OVERRUN); fifo_data->count = temp & LIS2DW_FIFO_SAMPLE_COUNT; - rtc_counter_t timeout_counter = watch_rtc_get_counter() + FIFO_TIMEOUT; + rtc_counter_t timeout_counter = watch_rtc_get_counter() + timeout; for(int i = 0; i < fifo_data->count; i++) { if (watch_rtc_get_counter() > timeout_counter) { break; @@ -296,6 +296,7 @@ bool lis2dw_read_fifo(lis2dw_fifo_t *fifo_data) { return overrun; #else (void) fifo_data; + (void) timeout; return false; #endif } diff --git a/watch-library/shared/driver/lis2dw.h b/watch-library/shared/driver/lis2dw.h index f7b0a801..d3a09dc4 100644 --- a/watch-library/shared/driver/lis2dw.h +++ b/watch-library/shared/driver/lis2dw.h @@ -345,7 +345,7 @@ void lis2dw_enable_fifo(void); void lis2dw_disable_fifo(void); -bool lis2dw_read_fifo(lis2dw_fifo_t *fifo_data); +bool lis2dw_read_fifo(lis2dw_fifo_t *fifo_data, uint32_t timeout); void lis2dw_clear_fifo(void); From 625e9209706650ca8ed2a2ab361c26a8ed1ff93c Mon Sep 17 00:00:00 2001 From: David Volovskiy Date: Tue, 7 Oct 2025 20:39:58 -0400 Subject: [PATCH 158/179] lis2dw_read_fifo(&fifo, 100 / DISPLAY_FREQUENCY)) --- watch-faces/sensor/lis2dw_monitor_face.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/watch-faces/sensor/lis2dw_monitor_face.c b/watch-faces/sensor/lis2dw_monitor_face.c index 4459cf73..46cc15f3 100644 --- a/watch-faces/sensor/lis2dw_monitor_face.c +++ b/watch-faces/sensor/lis2dw_monitor_face.c @@ -420,7 +420,7 @@ static void _monitor_update(lis2dw_monitor_state_t *state) lis2dw_fifo_t fifo; float x = 0, y = 0, z = 0; - lis2dw_read_fifo(&fifo, 25); + lis2dw_read_fifo(&fifo, 100 / DISPLAY_FREQUENCY); if (fifo.count == 0) { return; } From 757f1a1de758524bedf3a6447c9015134d60149c Mon Sep 17 00:00:00 2001 From: David Volovskiy Date: Wed, 8 Oct 2025 17:48:34 -0400 Subject: [PATCH 159/179] defined LIS2DW_FIFO_TIMEOUT_SECOND --- .../watch_faces/sensor/accelerometer_data_acquisition_face.c | 4 ++-- watch-faces/sensor/lis2dw_monitor_face.c | 2 +- watch-library/shared/driver/lis2dw.h | 2 ++ 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/legacy/watch_faces/sensor/accelerometer_data_acquisition_face.c b/legacy/watch_faces/sensor/accelerometer_data_acquisition_face.c index 64a25a87..968a7b81 100644 --- a/legacy/watch_faces/sensor/accelerometer_data_acquisition_face.c +++ b/legacy/watch_faces/sensor/accelerometer_data_acquisition_face.c @@ -448,13 +448,13 @@ static void start_reading(accelerometer_data_acquisition_state_t *state) { state->records[state->pos++] = record; lis2dw_fifo_t fifo; - lis2dw_read_fifo(&fifo, 100); // dump the fifo, this starts a fresh round of data in continue_reading + lis2dw_read_fifo(&fifo, LIS2DW_FIFO_TIMEOUT_SECOND); // dump the fifo, this starts a fresh round of data in continue_reading } static void continue_reading(accelerometer_data_acquisition_state_t *state) { printf("Continue reading\n"); lis2dw_fifo_t fifo; - lis2dw_read_fifo(&fifo, 100); + lis2dw_read_fifo(&fifo, LIS2DW_FIFO_TIMEOUT_SECOND); fifo.count = min(fifo.count, 25); // hacky, but we need a consistent data rate; if we got a 26th data point, chuck it. uint8_t offset = 4 * (25 - fifo.count); // also hacky: we're sometimes short at the start. align to beginning of next second. diff --git a/watch-faces/sensor/lis2dw_monitor_face.c b/watch-faces/sensor/lis2dw_monitor_face.c index 46cc15f3..ade0df55 100644 --- a/watch-faces/sensor/lis2dw_monitor_face.c +++ b/watch-faces/sensor/lis2dw_monitor_face.c @@ -420,7 +420,7 @@ static void _monitor_update(lis2dw_monitor_state_t *state) lis2dw_fifo_t fifo; float x = 0, y = 0, z = 0; - lis2dw_read_fifo(&fifo, 100 / DISPLAY_FREQUENCY); + lis2dw_read_fifo(&fifo, LIS2DW_FIFO_TIMEOUT_SECOND / DISPLAY_FREQUENCY); if (fifo.count == 0) { return; } diff --git a/watch-library/shared/driver/lis2dw.h b/watch-library/shared/driver/lis2dw.h index d3a09dc4..f81c7750 100644 --- a/watch-library/shared/driver/lis2dw.h +++ b/watch-library/shared/driver/lis2dw.h @@ -301,6 +301,8 @@ typedef enum { #define LIS2DW_CTRL7_VAL_HP_REF_MODE 0b00000010 #define LIS2DW_CTRL7_VAL_LPASS_ON6D 0b00000001 +#define LIS2DW_FIFO_TIMEOUT_SECOND 100 + bool lis2dw_begin(void); uint8_t lis2dw_get_device_id(void); From e8750713f8e9034c8ff3b4d4b6fbe323c27154cb Mon Sep 17 00:00:00 2001 From: David Volovskiy Date: Thu, 13 Nov 2025 08:19:22 -0500 Subject: [PATCH 160/179] Renamed LIS2DW_FIFO_TIMEOUT_SECOND to LIS2DW_FIFO_TIMEOUT --- .../watch_faces/sensor/accelerometer_data_acquisition_face.c | 4 ++-- watch-faces/sensor/lis2dw_monitor_face.c | 2 +- watch-library/shared/driver/lis2dw.h | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/legacy/watch_faces/sensor/accelerometer_data_acquisition_face.c b/legacy/watch_faces/sensor/accelerometer_data_acquisition_face.c index 968a7b81..cb49e018 100644 --- a/legacy/watch_faces/sensor/accelerometer_data_acquisition_face.c +++ b/legacy/watch_faces/sensor/accelerometer_data_acquisition_face.c @@ -448,13 +448,13 @@ static void start_reading(accelerometer_data_acquisition_state_t *state) { state->records[state->pos++] = record; lis2dw_fifo_t fifo; - lis2dw_read_fifo(&fifo, LIS2DW_FIFO_TIMEOUT_SECOND); // dump the fifo, this starts a fresh round of data in continue_reading + lis2dw_read_fifo(&fifo, LIS2DW_FIFO_TIMEOUT); // dump the fifo, this starts a fresh round of data in continue_reading } static void continue_reading(accelerometer_data_acquisition_state_t *state) { printf("Continue reading\n"); lis2dw_fifo_t fifo; - lis2dw_read_fifo(&fifo, LIS2DW_FIFO_TIMEOUT_SECOND); + lis2dw_read_fifo(&fifo, LIS2DW_FIFO_TIMEOUT); fifo.count = min(fifo.count, 25); // hacky, but we need a consistent data rate; if we got a 26th data point, chuck it. uint8_t offset = 4 * (25 - fifo.count); // also hacky: we're sometimes short at the start. align to beginning of next second. diff --git a/watch-faces/sensor/lis2dw_monitor_face.c b/watch-faces/sensor/lis2dw_monitor_face.c index ade0df55..8afb7d3e 100644 --- a/watch-faces/sensor/lis2dw_monitor_face.c +++ b/watch-faces/sensor/lis2dw_monitor_face.c @@ -420,7 +420,7 @@ static void _monitor_update(lis2dw_monitor_state_t *state) lis2dw_fifo_t fifo; float x = 0, y = 0, z = 0; - lis2dw_read_fifo(&fifo, LIS2DW_FIFO_TIMEOUT_SECOND / DISPLAY_FREQUENCY); + lis2dw_read_fifo(&fifo, LIS2DW_FIFO_TIMEOUT / DISPLAY_FREQUENCY); if (fifo.count == 0) { return; } diff --git a/watch-library/shared/driver/lis2dw.h b/watch-library/shared/driver/lis2dw.h index f81c7750..fc5947e0 100644 --- a/watch-library/shared/driver/lis2dw.h +++ b/watch-library/shared/driver/lis2dw.h @@ -301,7 +301,7 @@ typedef enum { #define LIS2DW_CTRL7_VAL_HP_REF_MODE 0b00000010 #define LIS2DW_CTRL7_VAL_LPASS_ON6D 0b00000001 -#define LIS2DW_FIFO_TIMEOUT_SECOND 100 +#define LIS2DW_FIFO_TIMEOUT 100 // timeout is in terms of 1/RTC_CNT_HZ seconds (likely 128 timeouts is one second) bool lis2dw_begin(void); From 994c12cc30748c4443f360e5b982186a0e180226 Mon Sep 17 00:00:00 2001 From: Alessandro Genova Date: Tue, 7 Oct 2025 22:26:24 -0400 Subject: [PATCH 161/179] add REALLY_LONG_PRESS event after a button is held down for 1.5s --- movement.c | 50 ++++++++++++++++++++++++++++++++++++++++++-------- movement.h | 6 ++++++ 2 files changed, 48 insertions(+), 8 deletions(-) diff --git a/movement.c b/movement.c index 361c8c89..f71ee58f 100644 --- a/movement.c +++ b/movement.c @@ -24,6 +24,8 @@ */ #define MOVEMENT_LONG_PRESS_TICKS 64 +#define MOVEMENT_REALLY_LONG_PRESS_TICKS 192 +#define MOVEMENT_MAX_LONG_PRESS_TICKS 1280 // get a chance to check if a button held down over 10 seconds is a glitch #include #include @@ -61,9 +63,9 @@ watch_date_time_t scheduled_tasks[MOVEMENT_NUM_FACES]; const int32_t movement_le_inactivity_deadlines[8] = {INT_MAX, 600, 3600, 7200, 21600, 43200, 86400, 604800}; const int16_t movement_timeout_inactivity_deadlines[4] = {60, 120, 300, 1800}; -const uint32_t _movement_mode_button_events_mask = 0b1111 << EVENT_MODE_BUTTON_DOWN; -const uint32_t _movement_light_button_events_mask = 0b1111 << EVENT_LIGHT_BUTTON_DOWN; -const uint32_t _movement_alarm_button_events_mask = 0b1111 << EVENT_ALARM_BUTTON_DOWN; +const uint32_t _movement_mode_button_events_mask = 0b111111 << EVENT_MODE_BUTTON_DOWN; +const uint32_t _movement_light_button_events_mask = 0b111111 << EVENT_LIGHT_BUTTON_DOWN; +const uint32_t _movement_alarm_button_events_mask = 0b111111 << EVENT_ALARM_BUTTON_DOWN; const uint32_t _movement_button_events_mask = _movement_mode_button_events_mask | _movement_light_button_events_mask | _movement_alarm_button_events_mask; typedef struct { @@ -298,6 +300,7 @@ static uint32_t _movement_get_accelerometer_events() { static void _movement_handle_button_presses(uint32_t pending_events) { bool any_up = false; bool any_down = false; + bool any_long = false; movement_button_t* buttons[3] = { &movement_volatile_state.mode_button, @@ -322,10 +325,23 @@ static void _movement_handle_button_presses(uint32_t pending_events) { movement_volatile_state.passthrough_events &= ~button_events_masks[i]; } + // If a long press occurred + if (pending_events & (1 << button->down_event + 2)) { + watch_rtc_register_comp_callback_no_schedule(button->cb_longpress, button->down_timestamp + MOVEMENT_REALLY_LONG_PRESS_TICKS, button->timeout_index); + any_long = true; + } + + // If a really long press occurred + if (pending_events & (1 << button->down_event + 4)) { + watch_rtc_register_comp_callback_no_schedule(button->cb_longpress, button->down_timestamp + MOVEMENT_MAX_LONG_PRESS_TICKS, button->timeout_index); + any_long = true; + } + // If a button up or button long up occurred if (pending_events & ( (1 << (button->down_event + 1)) | - (1 << (button->down_event + 3)) + (1 << (button->down_event + 3)) | + (1 << (button->down_event + 5)) )) { // We cancel the timeout if it hasn't fired yet watch_rtc_disable_comp_callback_no_schedule(button->timeout_index); @@ -343,7 +359,7 @@ static void _movement_handle_button_presses(uint32_t pending_events) { } } - if (any_down || any_up) { + if (any_down || any_up || any_long) { _movement_reset_inactivity_countdown(); movement_volatile_state.schedule_next_comp = true; } @@ -1391,7 +1407,9 @@ static movement_event_type_t _process_button_event(bool pin_level, movement_butt #if MOVEMENT_DEBOUNCE_TICKS button->up_timestamp = counter; #endif - if ((counter - button->down_timestamp) >= MOVEMENT_LONG_PRESS_TICKS) { + if ((counter - button->down_timestamp) >= MOVEMENT_REALLY_LONG_PRESS_TICKS) { + event_type = button->down_event + 5; + } else if ((counter - button->down_timestamp) >= MOVEMENT_LONG_PRESS_TICKS) { event_type = button->down_event + 3; } else { event_type = button->down_event + 1; @@ -1424,8 +1442,18 @@ static movement_event_type_t _process_button_longpress_timeout(bool pin_level, m return EVENT_NONE; } + uint32_t counter = watch_rtc_get_counter(); + bool max_long_press = (counter - button->down_timestamp) >= MOVEMENT_MAX_LONG_PRESS_TICKS; + bool really_long_press = (counter - button->down_timestamp) >= MOVEMENT_REALLY_LONG_PRESS_TICKS; + if (pin_level) { - return button->down_event + 2; // event_longpress + if (max_long_press) { + return EVENT_NONE; // no further events left to emit + } else if (really_long_press) { + return button->down_event + 4; // event_really_longpress + } else { + return button->down_event + 2; // event_longpress + } } else { // hypotetical corner case: if the timeout fired but the pin level is actually up, we may have missed/rejected the up event, so fire it here #if MOVEMENT_DEBOUNCE_TICKS @@ -1433,7 +1461,13 @@ static movement_event_type_t _process_button_longpress_timeout(bool pin_level, m button->up_timestamp = button->down_timestamp; #endif button->is_down = false; - return button->down_event + 1; // event_up + if (max_long_press) { + return button->down_event + 5; // event_really_long_up + } else if (really_long_press) { + return button->down_event + 3; // event_long_up + } else { + return button->down_event + 1; // event_up + } } } diff --git a/movement.h b/movement.h index 94f9b83e..305a2e8c 100644 --- a/movement.h +++ b/movement.h @@ -121,14 +121,20 @@ typedef enum { EVENT_LIGHT_BUTTON_UP, // The light button was pressed for less than half a second, and released. EVENT_LIGHT_LONG_PRESS, // The light button was held for over half a second, but not yet released. EVENT_LIGHT_LONG_UP, // The light button was held for over half a second, and released. + EVENT_LIGHT_REALLY_LONG_PRESS, // The light button was held for more than 1.5 second, note yet released. + EVENT_LIGHT_REALLY_LONG_UP, // The light button was held for more than 1.5 second, and released. EVENT_MODE_BUTTON_DOWN, // The mode button has been pressed, but not yet released. EVENT_MODE_BUTTON_UP, // The mode button was pressed for less than half a second, and released. EVENT_MODE_LONG_PRESS, // The mode button was held for over half a second, but not yet released. EVENT_MODE_LONG_UP, // The mode button was held for over half a second, and released. NOTE: your watch face will resign immediately after receiving this event. + EVENT_MODE_REALLY_LONG_PRESS, // The mode button was held for more than 1.5 second, note yet released. + EVENT_MODE_REALLY_LONG_UP, // The mode button was held for more than 1.5 second, and released. EVENT_ALARM_BUTTON_DOWN, // The alarm button has been pressed, but not yet released. EVENT_ALARM_BUTTON_UP, // The alarm button was pressed for less than half a second, and released. EVENT_ALARM_LONG_PRESS, // The alarm button was held for over half a second, but not yet released. EVENT_ALARM_LONG_UP, // The alarm button was held for over half a second, and released. + EVENT_ALARM_REALLY_LONG_PRESS, // The alarm button was held for more than 1.5 second, note yet released. + EVENT_ALARM_REALLY_LONG_UP, // The alarm button was held for more than 1.5 second, and released. EVENT_ACCELEROMETER_WAKE, // The accelerometer has detected motion and woken up. EVENT_SINGLE_TAP, // Accelerometer detected a single tap. This event is not yet implemented. From 51ca839f3bcf5da62f933bd4356a1d67815605a4 Mon Sep 17 00:00:00 2001 From: David Volovskiy Date: Sun, 23 Nov 2025 12:27:00 -0500 Subject: [PATCH 162/179] Removed all of the compiler warnings --- movement.c | 4 ++-- watch-faces/demo/rtccount_face.c | 8 ++++---- watch-library/hardware/watch/watch_rtc.c | 1 - watch-library/simulator/watch/watch_rtc.c | 1 - 4 files changed, 6 insertions(+), 8 deletions(-) diff --git a/movement.c b/movement.c index f71ee58f..bae267b3 100644 --- a/movement.c +++ b/movement.c @@ -326,13 +326,13 @@ static void _movement_handle_button_presses(uint32_t pending_events) { } // If a long press occurred - if (pending_events & (1 << button->down_event + 2)) { + if (pending_events & (1 << (button->down_event + 2))) { watch_rtc_register_comp_callback_no_schedule(button->cb_longpress, button->down_timestamp + MOVEMENT_REALLY_LONG_PRESS_TICKS, button->timeout_index); any_long = true; } // If a really long press occurred - if (pending_events & (1 << button->down_event + 4)) { + if (pending_events & (1 << (button->down_event + 4))) { watch_rtc_register_comp_callback_no_schedule(button->cb_longpress, button->down_timestamp + MOVEMENT_MAX_LONG_PRESS_TICKS, button->timeout_index); any_long = true; } diff --git a/watch-faces/demo/rtccount_face.c b/watch-faces/demo/rtccount_face.c index 72a3055a..6e7cb08c 100644 --- a/watch-faces/demo/rtccount_face.c +++ b/watch-faces/demo/rtccount_face.c @@ -93,7 +93,7 @@ static void _rtccount_face_draw(movement_event_t event, rtccount_state_t* state) switch (state->status) { case RTCCOUNT_STATUS_COUNTER: { - snprintf(buf, sizeof(buf), "%u", counter & COUNTER_MASK); + snprintf(buf, sizeof(buf), "%lu", counter & COUNTER_MASK); size_t len = strlen(buf); @@ -102,7 +102,7 @@ static void _rtccount_face_draw(movement_event_t event, rtccount_state_t* state) } case RTCCOUNT_STATUS_COUNTER_SUB: { - snprintf(buf, sizeof(buf), "%u", counter & 127); + snprintf(buf, sizeof(buf), "%lu", counter & 127); size_t len = strlen(buf); @@ -111,7 +111,7 @@ static void _rtccount_face_draw(movement_event_t event, rtccount_state_t* state) } case RTCCOUNT_STATUS_MINUTES: { - snprintf(buf, sizeof(buf), "%u", state->n_top_of_minute & COUNTER_MASK); + snprintf(buf, sizeof(buf), "%lu", state->n_top_of_minute & COUNTER_MASK); size_t len = strlen(buf); @@ -122,7 +122,7 @@ static void _rtccount_face_draw(movement_event_t event, rtccount_state_t* state) case RTCCOUNT_STATUS_MINUTES_DIFF: { uint32_t elapsed_minutes = (movement_get_utc_timestamp() - state->ref_timestamp) / 60; - snprintf(buf, sizeof(buf), "%u", (elapsed_minutes - state->n_top_of_minute) & COUNTER_MASK); + snprintf(buf, sizeof(buf), "%lu", (elapsed_minutes - state->n_top_of_minute) & COUNTER_MASK); size_t len = strlen(buf); diff --git a/watch-library/hardware/watch/watch_rtc.c b/watch-library/hardware/watch/watch_rtc.c index 114c1ee4..7cfa81c2 100644 --- a/watch-library/hardware/watch/watch_rtc.c +++ b/watch-library/hardware/watch/watch_rtc.c @@ -37,7 +37,6 @@ static const uint32_t RTC_CNT_HZ = RTC_OSC_HZ >> RTC_PRESCALER_DIV; // 1024 / 2^ static const uint32_t RTC_CNT_SUBSECOND_MASK = RTC_CNT_HZ - 1; static const uint32_t RTC_CNT_DIV = RTC_OSC_DIV - RTC_PRESCALER_DIV; // 7 static const uint32_t RTC_CNT_TICKS_PER_MINUTE = RTC_CNT_HZ * 60; -static const uint32_t RTC_CNT_TICKS_PER_HOUR = RTC_CNT_TICKS_PER_MINUTE * 60; static const uint32_t RTC_COMP_GRACE_PERIOD = 4; diff --git a/watch-library/simulator/watch/watch_rtc.c b/watch-library/simulator/watch/watch_rtc.c index 1347d797..4927ba14 100644 --- a/watch-library/simulator/watch/watch_rtc.c +++ b/watch-library/simulator/watch/watch_rtc.c @@ -34,7 +34,6 @@ static const uint32_t RTC_CNT_HZ = 128; static const uint32_t RTC_CNT_SUBSECOND_MASK = RTC_CNT_HZ - 1; static const uint32_t RTC_CNT_DIV = 7; static const uint32_t RTC_CNT_TICKS_PER_MINUTE = RTC_CNT_HZ * 60; -static const uint32_t RTC_CNT_TICKS_PER_HOUR = RTC_CNT_TICKS_PER_MINUTE * 60; static uint32_t counter_interval; static uint32_t counter; From e353d4760f951031723bc8e06d5a139b63c82c03 Mon Sep 17 00:00:00 2001 From: Alessandro Genova Date: Wed, 26 Nov 2025 13:23:42 -0500 Subject: [PATCH 163/179] disable REALLY_LONG_UP event for now --- movement.c | 16 +++++++++------- movement.h | 6 +++--- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/movement.c b/movement.c index bae267b3..f0fb9609 100644 --- a/movement.c +++ b/movement.c @@ -63,9 +63,9 @@ watch_date_time_t scheduled_tasks[MOVEMENT_NUM_FACES]; const int32_t movement_le_inactivity_deadlines[8] = {INT_MAX, 600, 3600, 7200, 21600, 43200, 86400, 604800}; const int16_t movement_timeout_inactivity_deadlines[4] = {60, 120, 300, 1800}; -const uint32_t _movement_mode_button_events_mask = 0b111111 << EVENT_MODE_BUTTON_DOWN; -const uint32_t _movement_light_button_events_mask = 0b111111 << EVENT_LIGHT_BUTTON_DOWN; -const uint32_t _movement_alarm_button_events_mask = 0b111111 << EVENT_ALARM_BUTTON_DOWN; +const uint32_t _movement_mode_button_events_mask = 0b11111 << EVENT_MODE_BUTTON_DOWN; +const uint32_t _movement_light_button_events_mask = 0b11111 << EVENT_LIGHT_BUTTON_DOWN; +const uint32_t _movement_alarm_button_events_mask = 0b11111 << EVENT_ALARM_BUTTON_DOWN; const uint32_t _movement_button_events_mask = _movement_mode_button_events_mask | _movement_light_button_events_mask | _movement_alarm_button_events_mask; typedef struct { @@ -340,8 +340,8 @@ static void _movement_handle_button_presses(uint32_t pending_events) { // If a button up or button long up occurred if (pending_events & ( (1 << (button->down_event + 1)) | - (1 << (button->down_event + 3)) | - (1 << (button->down_event + 5)) + (1 << (button->down_event + 3)) + // (1 << (button->down_event + 5)) )) { // We cancel the timeout if it hasn't fired yet watch_rtc_disable_comp_callback_no_schedule(button->timeout_index); @@ -1408,7 +1408,8 @@ static movement_event_type_t _process_button_event(bool pin_level, movement_butt button->up_timestamp = counter; #endif if ((counter - button->down_timestamp) >= MOVEMENT_REALLY_LONG_PRESS_TICKS) { - event_type = button->down_event + 5; + // event_type = button->down_event + 5; + event_type = button->down_event + 3; // TODO: swith to REALLY_LONG_UP } else if ((counter - button->down_timestamp) >= MOVEMENT_LONG_PRESS_TICKS) { event_type = button->down_event + 3; } else { @@ -1462,7 +1463,8 @@ static movement_event_type_t _process_button_longpress_timeout(bool pin_level, m #endif button->is_down = false; if (max_long_press) { - return button->down_event + 5; // event_really_long_up + // return button->down_event + 5; // event_really_long_up + return button->down_event + 3; // event_long_up TODO: use really_long_up } else if (really_long_press) { return button->down_event + 3; // event_long_up } else { diff --git a/movement.h b/movement.h index 305a2e8c..1bbd9638 100644 --- a/movement.h +++ b/movement.h @@ -122,19 +122,19 @@ typedef enum { EVENT_LIGHT_LONG_PRESS, // The light button was held for over half a second, but not yet released. EVENT_LIGHT_LONG_UP, // The light button was held for over half a second, and released. EVENT_LIGHT_REALLY_LONG_PRESS, // The light button was held for more than 1.5 second, note yet released. - EVENT_LIGHT_REALLY_LONG_UP, // The light button was held for more than 1.5 second, and released. + // EVENT_LIGHT_REALLY_LONG_UP, // The light button was held for more than 1.5 second, and released. EVENT_MODE_BUTTON_DOWN, // The mode button has been pressed, but not yet released. EVENT_MODE_BUTTON_UP, // The mode button was pressed for less than half a second, and released. EVENT_MODE_LONG_PRESS, // The mode button was held for over half a second, but not yet released. EVENT_MODE_LONG_UP, // The mode button was held for over half a second, and released. NOTE: your watch face will resign immediately after receiving this event. EVENT_MODE_REALLY_LONG_PRESS, // The mode button was held for more than 1.5 second, note yet released. - EVENT_MODE_REALLY_LONG_UP, // The mode button was held for more than 1.5 second, and released. + // EVENT_MODE_REALLY_LONG_UP, // The mode button was held for more than 1.5 second, and released. EVENT_ALARM_BUTTON_DOWN, // The alarm button has been pressed, but not yet released. EVENT_ALARM_BUTTON_UP, // The alarm button was pressed for less than half a second, and released. EVENT_ALARM_LONG_PRESS, // The alarm button was held for over half a second, but not yet released. EVENT_ALARM_LONG_UP, // The alarm button was held for over half a second, and released. EVENT_ALARM_REALLY_LONG_PRESS, // The alarm button was held for more than 1.5 second, note yet released. - EVENT_ALARM_REALLY_LONG_UP, // The alarm button was held for more than 1.5 second, and released. + // EVENT_ALARM_REALLY_LONG_UP, // The alarm button was held for more than 1.5 second, and released. EVENT_ACCELEROMETER_WAKE, // The accelerometer has detected motion and woken up. EVENT_SINGLE_TAP, // Accelerometer detected a single tap. This event is not yet implemented. From f6fad64eebe0b93b086c8bc0685fb049cef0e518 Mon Sep 17 00:00:00 2001 From: Alessandro Genova Date: Sun, 7 Dec 2025 21:09:39 -0500 Subject: [PATCH 164/179] don't force-reset the state of the RTC when rebooting --- watch-library/hardware/watch/rtc32.c | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/watch-library/hardware/watch/rtc32.c b/watch-library/hardware/watch/rtc32.c index 934564bb..d4ac9b27 100644 --- a/watch-library/hardware/watch/rtc32.c +++ b/watch-library/hardware/watch/rtc32.c @@ -61,9 +61,7 @@ void rtc_init(void) { MCLK->APBAMASK.reg |= MCLK_APBAMASK_RTC; #endif - // if (rtc_is_enabled()) return; // don't reset the RTC if it's already set up. - // Reset everything, once things are stabilized we can think about preserving some state - CTRLREG.bit.ENABLE = 0; + if (rtc_is_enabled()) return; // don't reset the RTC if it's already set up. _rtc_sync(); CTRLREG.bit.SWRST = 1; From 7d4a80a29dd7d2bc7954aea8adf19af05213a9d9 Mon Sep 17 00:00:00 2001 From: redraw Date: Wed, 21 Jan 2026 13:52:15 -0300 Subject: [PATCH 165/179] fixed simulator watch rtc counter --- watch-library/simulator/watch/watch_rtc.c | 52 ++++++++++++++++++++--- 1 file changed, 47 insertions(+), 5 deletions(-) diff --git a/watch-library/simulator/watch/watch_rtc.c b/watch-library/simulator/watch/watch_rtc.c index 4927ba14..7e23e6ee 100644 --- a/watch-library/simulator/watch/watch_rtc.c +++ b/watch-library/simulator/watch/watch_rtc.c @@ -38,6 +38,8 @@ static const uint32_t RTC_CNT_TICKS_PER_MINUTE = RTC_CNT_HZ * 60; static uint32_t counter_interval; static uint32_t counter; static uint32_t reference_timestamp; +static double next_tick_time; +static double rtc_start_time; #define WATCH_RTC_N_COMP_CB 8 @@ -109,7 +111,18 @@ unix_timestamp_t watch_rtc_get_unix_time(void) { } rtc_counter_t watch_rtc_get_counter(void) { - return counter; + if (!counter_interval) { + return counter; // RTC not running + } + + // Calculate current counter from high-precision time + double now = EM_ASM_DOUBLE({ return performance.now(); }); + double elapsed_ms = now - rtc_start_time; + + // Convert elapsed time to RTC ticks (RTC_CNT_HZ = 128 Hz) + double elapsed_ticks = (elapsed_ms * RTC_CNT_HZ) / 1000.0; + + return (rtc_counter_t)elapsed_ticks; } uint32_t watch_rtc_get_frequency(void) { @@ -164,6 +177,30 @@ void watch_rtc_disable_tick_callback(void) { watch_rtc_disable_periodic_callback(1); } +static void _watch_increase_counter(void *userData); + +static void _watch_schedule_next_tick(void) { + if (!counter_interval) return; + + double now = EM_ASM_DOUBLE({ return performance.now(); }); + double drift = now - next_tick_time; + + // Target interval in ms + double ms = 1000.0 / (double)RTC_CNT_HZ; + + // Schedule next tick, correcting for drift + next_tick_time += ms; + double delay = next_tick_time - now; + + // Ensure we don't schedule negative or zero delays + if (delay < 0.1) { + delay = 0.1; + next_tick_time = now + delay; + } + + emscripten_async_call(_watch_increase_counter, NULL, (int)delay); +} + static void _watch_increase_counter(void *userData) { (void) userData; @@ -174,6 +211,9 @@ static void _watch_increase_counter(void *userData) { _watch_process_comp_callbacks(); resume_main_loop(); + + // Schedule the next tick with drift correction + _watch_schedule_next_tick(); } static void _watch_process_periodic_callbacks(void) { @@ -343,11 +383,13 @@ void watch_rtc_enable(bool en) } if (en) { - // Very bad way to keep time, but okay way to emulates the hardware. - double ms = 1000.0 / (double)RTC_CNT_HZ; // in msec - counter_interval = emscripten_set_interval(_watch_increase_counter, ms, NULL); + // Use drift-correcting timer instead of fixed setInterval + counter_interval = 1; // Non-zero to indicate enabled + rtc_start_time = EM_ASM_DOUBLE({ return performance.now(); }); + next_tick_time = rtc_start_time; + counter = 0; + _watch_schedule_next_tick(); } else { - emscripten_clear_interval(counter_interval); counter_interval = 0; } } From a3664b0789f98fbef4f32e82decb241fdda25481 Mon Sep 17 00:00:00 2001 From: redraw Date: Thu, 22 Jan 2026 10:29:52 -0300 Subject: [PATCH 166/179] update simulator rtc counter improvements to match real hardware --- watch-library/simulator/watch/watch_rtc.c | 28 +++++------------------ 1 file changed, 6 insertions(+), 22 deletions(-) diff --git a/watch-library/simulator/watch/watch_rtc.c b/watch-library/simulator/watch/watch_rtc.c index 7e23e6ee..ad3e5db3 100644 --- a/watch-library/simulator/watch/watch_rtc.c +++ b/watch-library/simulator/watch/watch_rtc.c @@ -39,7 +39,6 @@ static uint32_t counter_interval; static uint32_t counter; static uint32_t reference_timestamp; static double next_tick_time; -static double rtc_start_time; #define WATCH_RTC_N_COMP_CB 8 @@ -111,18 +110,7 @@ unix_timestamp_t watch_rtc_get_unix_time(void) { } rtc_counter_t watch_rtc_get_counter(void) { - if (!counter_interval) { - return counter; // RTC not running - } - - // Calculate current counter from high-precision time - double now = EM_ASM_DOUBLE({ return performance.now(); }); - double elapsed_ms = now - rtc_start_time; - - // Convert elapsed time to RTC ticks (RTC_CNT_HZ = 128 Hz) - double elapsed_ticks = (elapsed_ms * RTC_CNT_HZ) / 1000.0; - - return (rtc_counter_t)elapsed_ticks; + return counter; } uint32_t watch_rtc_get_frequency(void) { @@ -183,7 +171,6 @@ static void _watch_schedule_next_tick(void) { if (!counter_interval) return; double now = EM_ASM_DOUBLE({ return performance.now(); }); - double drift = now - next_tick_time; // Target interval in ms double ms = 1000.0 / (double)RTC_CNT_HZ; @@ -192,13 +179,12 @@ static void _watch_schedule_next_tick(void) { next_tick_time += ms; double delay = next_tick_time - now; - // Ensure we don't schedule negative or zero delays - if (delay < 0.1) { - delay = 0.1; - next_tick_time = now + delay; + // If we're behind, schedule immediately + if (delay < 0) { + delay = 0; } - emscripten_async_call(_watch_increase_counter, NULL, (int)delay); + emscripten_async_call(_watch_increase_counter, NULL, delay); } static void _watch_increase_counter(void *userData) { @@ -385,9 +371,7 @@ void watch_rtc_enable(bool en) if (en) { // Use drift-correcting timer instead of fixed setInterval counter_interval = 1; // Non-zero to indicate enabled - rtc_start_time = EM_ASM_DOUBLE({ return performance.now(); }); - next_tick_time = rtc_start_time; - counter = 0; + next_tick_time = EM_ASM_DOUBLE({ return performance.now(); }); _watch_schedule_next_tick(); } else { counter_interval = 0; From d653cf6921ba49586a6efe3396d6b6f2eef3b90d Mon Sep 17 00:00:00 2001 From: redraw Date: Thu, 22 Jan 2026 13:18:18 -0300 Subject: [PATCH 167/179] renamed counter_interval to rtc_enabled --- watch-library/simulator/watch/watch_rtc.c | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/watch-library/simulator/watch/watch_rtc.c b/watch-library/simulator/watch/watch_rtc.c index ad3e5db3..cbda46b0 100644 --- a/watch-library/simulator/watch/watch_rtc.c +++ b/watch-library/simulator/watch/watch_rtc.c @@ -22,6 +22,7 @@ * SOFTWARE. */ #include +#include #include "watch_rtc.h" #include "watch_main_loop.h" @@ -35,7 +36,7 @@ static const uint32_t RTC_CNT_SUBSECOND_MASK = RTC_CNT_HZ - 1; static const uint32_t RTC_CNT_DIV = 7; static const uint32_t RTC_CNT_TICKS_PER_MINUTE = RTC_CNT_HZ * 60; -static uint32_t counter_interval; +static bool rtc_enabled; static uint32_t counter; static uint32_t reference_timestamp; static double next_tick_time; @@ -67,7 +68,7 @@ static void _watch_process_periodic_callbacks(void); static void _watch_process_comp_callbacks(void); bool _watch_rtc_is_enabled(void) { - return counter_interval; + return rtc_enabled; } void _watch_rtc_init(void) { @@ -83,7 +84,7 @@ void _watch_rtc_init(void) { scheduled_comp_counter = 0; counter = 0; - counter_interval = 0; + rtc_enabled = false; watch_rtc_set_date_time(watch_get_init_date_time()); watch_rtc_enable(true); @@ -168,7 +169,7 @@ void watch_rtc_disable_tick_callback(void) { static void _watch_increase_counter(void *userData); static void _watch_schedule_next_tick(void) { - if (!counter_interval) return; + if (!rtc_enabled) return; double now = EM_ASM_DOUBLE({ return performance.now(); }); @@ -364,17 +365,17 @@ void watch_rtc_schedule_next_comp(void) { void watch_rtc_enable(bool en) { // Nothing to do cases - if ((en && counter_interval) || (!en && !counter_interval)) { + if ((en && rtc_enabled) || (!en && !rtc_enabled)) { return; } if (en) { // Use drift-correcting timer instead of fixed setInterval - counter_interval = 1; // Non-zero to indicate enabled + rtc_enabled = true; next_tick_time = EM_ASM_DOUBLE({ return performance.now(); }); _watch_schedule_next_tick(); } else { - counter_interval = 0; + rtc_enabled = false; } } From 22293ed6932119aa428065068f87c59cb58c1b87 Mon Sep 17 00:00:00 2001 From: redraw Date: Thu, 22 Jan 2026 17:31:01 -0300 Subject: [PATCH 168/179] jump counter forward when tab resumes from background --- watch-library/simulator/watch/watch_rtc.c | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/watch-library/simulator/watch/watch_rtc.c b/watch-library/simulator/watch/watch_rtc.c index cbda46b0..3492032f 100644 --- a/watch-library/simulator/watch/watch_rtc.c +++ b/watch-library/simulator/watch/watch_rtc.c @@ -180,9 +180,12 @@ static void _watch_schedule_next_tick(void) { next_tick_time += ms; double delay = next_tick_time - now; - // If we're behind, schedule immediately + // If we're behind, jump counter forward and reset timing if (delay < 0) { - delay = 0; + uint32_t missed_ticks = (uint32_t)((-delay) / ms); + counter += missed_ticks; + next_tick_time = now + ms; + delay = ms; } emscripten_async_call(_watch_increase_counter, NULL, delay); From aba0aa97ec5b6b9416c80025e2f0a88b3e2684b5 Mon Sep 17 00:00:00 2001 From: redraw Date: Thu, 22 Jan 2026 23:55:55 -0300 Subject: [PATCH 169/179] don't jump missed ticks, forgot callbacks --- watch-library/simulator/watch/watch_rtc.c | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/watch-library/simulator/watch/watch_rtc.c b/watch-library/simulator/watch/watch_rtc.c index 3492032f..93da5e6b 100644 --- a/watch-library/simulator/watch/watch_rtc.c +++ b/watch-library/simulator/watch/watch_rtc.c @@ -166,8 +166,6 @@ void watch_rtc_disable_tick_callback(void) { watch_rtc_disable_periodic_callback(1); } -static void _watch_increase_counter(void *userData); - static void _watch_schedule_next_tick(void) { if (!rtc_enabled) return; @@ -180,10 +178,8 @@ static void _watch_schedule_next_tick(void) { next_tick_time += ms; double delay = next_tick_time - now; - // If we're behind, jump counter forward and reset timing + // If we're behind, reset timing if (delay < 0) { - uint32_t missed_ticks = (uint32_t)((-delay) / ms); - counter += missed_ticks; next_tick_time = now + ms; delay = ms; } From ff3c8a72fced39b858f464c9b5f573450d3e1645 Mon Sep 17 00:00:00 2001 From: Raffael Mancini Date: Sat, 21 Feb 2026 18:06:19 +0100 Subject: [PATCH 170/179] Implement local solar time face MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Full implementation of the local solar time complication using the pveducation.org formula set (LSTM, B, EoT, TC, LST, HRA). EoT and TC are cached by day-of-year and recomputed once at midnight rollover. Three display modes cycle with the Alarm button: SO HH:MM:SS — Local Solar Time nO HH:MM — Solar Noon in local clock time Hr ±DDD — Hour Angle in degrees Location is read from location.u32 on the filesystem. In the simulator the browser lat/lon globals are written to location.u32 on activation if not already set, fixing the "no Loc" issue in the emulator. --- movement_config.h | 5 +- movement_faces.h | 1 + watch-faces.mk | 1 + .../complication/local_solar_time_face.c | 227 ++++++++++++++++++ .../complication/local_solar_time_face.h | 84 +++++++ 5 files changed, 316 insertions(+), 2 deletions(-) create mode 100644 watch-faces/complication/local_solar_time_face.c create mode 100644 watch-faces/complication/local_solar_time_face.h diff --git a/movement_config.h b/movement_config.h index 2f48d8fd..bd23b810 100644 --- a/movement_config.h +++ b/movement_config.h @@ -28,8 +28,9 @@ #include "movement_faces.h" const watch_face_t watch_faces[] = { - clock_face, - world_clock_face, + local_solar_time_face, + clock_face, + /* world_clock_face, */ sunrise_sunset_face, moon_phase_face, fast_stopwatch_face, diff --git a/movement_faces.h b/movement_faces.h index 164dbb4c..4a346850 100644 --- a/movement_faces.h +++ b/movement_faces.h @@ -80,4 +80,5 @@ #include "simon_face.h" #include "ping_face.h" #include "rtccount_face.h" +#include "local_solar_time_face.h" // New includes go above this line. diff --git a/watch-faces.mk b/watch-faces.mk index 69ec4e9c..bb5d5aca 100644 --- a/watch-faces.mk +++ b/watch-faces.mk @@ -55,4 +55,5 @@ SRCS += \ ./watch-faces/complication/lander_face.c \ ./watch-faces/complication/simon_face.c \ ./watch-faces/complication/ping_face.c \ + ./watch-faces/complication/local_solar_time_face.c \ # New watch faces go above this line. diff --git a/watch-faces/complication/local_solar_time_face.c b/watch-faces/complication/local_solar_time_face.c new file mode 100644 index 00000000..4c8b2397 --- /dev/null +++ b/watch-faces/complication/local_solar_time_face.c @@ -0,0 +1,227 @@ +/* + * MIT License + * + * Copyright (c) 2025 Raffael Mancini + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + * Solar time formulas follow the notation from: + * https://www.pveducation.org/pvcdrom/properties-of-sunlight/solar-time + */ + +#include +#include +#include +#include "local_solar_time_face.h" +#include "watch.h" +#include "watch_utility.h" +#include "filesystem.h" + +#if __EMSCRIPTEN__ +#include +#endif + +#ifndef M_PI +#define M_PI 3.14159265358979323846f +#endif + +/* --------------------------------------------------------------------------- + * Solar time math (pveducation.org notation) + * --------------------------------------------------------------------------- + * + * LSTM = 15 * ΔTUTC [degrees] + * B = (360 / 365) * (d - 81) [degrees] d = day-of-year + * EoT = 9.87*sin(2B) - 7.53*cos(B) + * - 1.5*sin(B) [minutes] + * TC = 4 * (Longitude - LSTM) + EoT [minutes] + * LST = LT + TC/60 [hours] + * HRA = 15 * (LST - 12) [degrees] + * --------------------------------------------------------------------------- + */ + +static movement_location_t _load_location(void) { + movement_location_t loc = {0}; + filesystem_read_file("location.u32", (char *)&loc.reg, sizeof(loc.reg)); + return loc; +} + +/* Compute and cache EoT and TC. Call when d != state->last_calc_d. */ +static void _compute_daily(local_solar_time_state_t *state, uint16_t d) { + /* LSTM — movement_get_current_timezone_offset() returns seconds from UTC */ + float delta_T_UTC = (float)movement_get_current_timezone_offset() / 3600.0f; + float LSTM = 15.0f * delta_T_UTC; + + movement_location_t loc = _load_location(); + float longitude = (float)(int16_t)loc.bit.longitude / 100.0f; + + /* B in radians for sinf/cosf */ + float B = (360.0f / 365.0f) * ((float)d - 81.0f) * ((float)M_PI / 180.0f); + + state->EoT = 9.87f * sinf(2.0f * B) - 7.53f * cosf(B) - 1.5f * sinf(B); + state->TC = 4.0f * (longitude - LSTM) + state->EoT; + state->last_calc_d = d; +} + +/* Recompute if the day-of-year has rolled over. Returns current d. */ +static uint16_t _maybe_recompute(local_solar_time_state_t *state, watch_date_time_t dt) { + uint16_t d = watch_utility_days_since_new_year( + (uint16_t)(dt.unit.year + WATCH_RTC_REFERENCE_YEAR), + dt.unit.month, + dt.unit.day + ); + if (d != state->last_calc_d && _load_location().reg != 0) { + _compute_daily(state, d); + } + return d; +} + +/* LST as total seconds since midnight (0..86399). + * LST = LT + TC/60 => in seconds: LT_sec + TC*60 */ +static int32_t _lst_seconds(watch_date_time_t dt, float TC) { + int32_t lt = (int32_t)dt.unit.hour * 3600 + + (int32_t)dt.unit.minute * 60 + + (int32_t)dt.unit.second; + int32_t tc = (int32_t)(TC * 60.0f); + return ((lt + tc) % 86400 + 86400) % 86400; +} + +static void _update_display(local_solar_time_state_t *state, watch_date_time_t dt) { + char buf[14]; + + if (_load_location().reg == 0) { + watch_display_text(WATCH_POSITION_FULL, "SO no Loc"); + return; + } + + switch (state->mode) { + + case LOCAL_SOLAR_TIME_MODE_LST: { + int32_t s = _lst_seconds(dt, state->TC); + sprintf(buf, "SO %02d%02d%02d", + (int)(s / 3600), (int)((s % 3600) / 60), (int)(s % 60)); + watch_set_colon(); + break; + } + + case LOCAL_SOLAR_TIME_MODE_NOON: { + /* Solar noon: moment when LST = 12:00 → LT_noon = 12h - TC/60 */ + int32_t s = (int32_t)(( 12.0f - state->TC / 60.0f) * 3600.0f); + s = ((s % 86400) + 86400) % 86400; + sprintf(buf, "nO %02d%02d ", (int)(s / 3600), (int)((s % 3600) / 60)); + watch_set_colon(); + break; + } + + case LOCAL_SOLAR_TIME_MODE_HRA: { + /* HRA = 15 * (LST - 12); negative = morning, positive = afternoon */ + int32_t s = _lst_seconds(dt, state->TC); + int16_t hra = (int16_t)roundf(15.0f * ((float)s / 3600.0f - 12.0f)); + sprintf(buf, "Hr %+4d ", (int)hra); + watch_clear_colon(); + break; + } + + default: + return; + } + + watch_display_text(WATCH_POSITION_FULL, buf); +} + +/* ---- Movement callbacks -------------------------------------------------- */ + +void local_solar_time_face_setup(uint8_t watch_face_index, void **context_ptr) { + (void)watch_face_index; + if (*context_ptr == NULL) { + *context_ptr = malloc(sizeof(local_solar_time_state_t)); + memset(*context_ptr, 0, sizeof(local_solar_time_state_t)); + /* last_calc_d == 0 guarantees recomputation on first tick */ + } +} + +void local_solar_time_face_activate(void *context) { + local_solar_time_state_t *state = (local_solar_time_state_t *)context; + +#if __EMSCRIPTEN__ + /* In the simulator the browser exposes lat/lon as JS globals. + * Write them to location.u32 if not already set. */ + int16_t browser_lat = EM_ASM_INT({ return lat; }); + int16_t browser_lon = EM_ASM_INT({ return lon; }); + if (browser_lat || browser_lon) { + movement_location_t browser_loc = {0}; + filesystem_read_file("location.u32", (char *)&browser_loc.reg, sizeof(browser_loc.reg)); + if (browser_loc.reg == 0) { + browser_loc.bit.latitude = browser_lat; + browser_loc.bit.longitude = browser_lon; + filesystem_write_file("location.u32", (char *)&browser_loc.reg, sizeof(browser_loc.reg)); + } + } +#endif + + /* Force recompute on activation: timezone or location may have changed */ + state->last_calc_d = 0; + watch_date_time_t dt = movement_get_local_date_time(); + _maybe_recompute(state, dt); +} + +bool local_solar_time_face_loop(movement_event_t event, void *context) { + local_solar_time_state_t *state = (local_solar_time_state_t *)context; + + switch (event.event_type) { + case EVENT_ACTIVATE: + case EVENT_TICK: { + watch_date_time_t dt = movement_get_local_date_time(); + _maybe_recompute(state, dt); + _update_display(state, dt); + break; + } + + case EVENT_ALARM_BUTTON_UP: + state->mode = (local_solar_time_mode_t)((state->mode + 1) % LOCAL_SOLAR_TIME_NUM_MODES); + { + watch_date_time_t dt = movement_get_local_date_time(); + _update_display(state, dt); + } + break; + + case EVENT_LOW_ENERGY_UPDATE: { + if (!watch_sleep_animation_is_running()) watch_start_sleep_animation(1000); + watch_date_time_t dt = movement_get_local_date_time(); + _maybe_recompute(state, dt); + _update_display(state, dt); + break; + } + + case EVENT_TIMEOUT: + state->mode = LOCAL_SOLAR_TIME_MODE_LST; + if (_load_location().reg == 0) movement_move_to_face(0); + break; + + default: + return movement_default_loop_handler(event); + } + + return true; +} + +void local_solar_time_face_resign(void *context) { + local_solar_time_state_t *state = (local_solar_time_state_t *)context; + state->mode = LOCAL_SOLAR_TIME_MODE_LST; + watch_clear_colon(); +} diff --git a/watch-faces/complication/local_solar_time_face.h b/watch-faces/complication/local_solar_time_face.h new file mode 100644 index 00000000..dc677ed0 --- /dev/null +++ b/watch-faces/complication/local_solar_time_face.h @@ -0,0 +1,84 @@ +/* + * MIT License + * + * Copyright (c) 2025 Raffael Mancini + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +#pragma once + +/* + * LOCAL SOLAR TIME FACE + * + * Displays solar time information based on the user's location. + * Formulas follow the notation from: + * https://www.pveducation.org/pvcdrom/properties-of-sunlight/solar-time + * + * Variables (pveducation.org notation): + * LSTM - Local Standard Time Meridian [degrees] = 15 * ΔTUTC + * B - Seasonal angle [degrees] = (360/365) * (d - 81) + * EoT - Equation of Time [minutes] = 9.87*sin(2B) - 7.53*cos(B) - 1.5*sin(B) + * TC - Time Correction Factor [minutes] = 4*(Longitude - LSTM) + EoT + * LST - Local Solar Time [hours] = LT + TC/60 + * HRA - Hour Angle [degrees] = 15*(LST - 12) + * + * B, EoT and TC only depend on the day-of-year d, so they are cached + * in state and recomputed exactly once per day: the cache key is d itself + * (1-366). Zero-initialisation of state guarantees a recompute on first use. + * + * Requires the location to be set via the Sunrise/Sunset face, stored in + * "location.u32" on the filesystem. If no location is set, displays + * "SO no Loc". + * + * Display modes (cycle with the Alarm / start-stop button): + * SO HH:MM:SS — Local Solar Time (LST), live seconds display + * nO HH:MM — Solar Noon in local clock time + * Hr ±DDD — Hour Angle (HRA) in degrees; negative=morning, positive=afternoon + */ + +#include "movement.h" + +typedef enum { + LOCAL_SOLAR_TIME_MODE_LST = 0, /* Local Solar Time SO HH:MM:SS */ + LOCAL_SOLAR_TIME_MODE_NOON = 1, /* Solar Noon (local) nO HH:MM */ + LOCAL_SOLAR_TIME_MODE_HRA = 2, /* Hour Angle Hr ±DDD */ + LOCAL_SOLAR_TIME_NUM_MODES +} local_solar_time_mode_t; + +typedef struct { + local_solar_time_mode_t mode; + uint16_t last_calc_d; /* day-of-year (1-366) when EoT/TC were last computed; + 0 (zero-init) guarantees recomputation on first tick */ + float EoT; /* Equation of Time [minutes] */ + float TC; /* Time Correction Factor [minutes] */ +} local_solar_time_state_t; + +void local_solar_time_face_setup(uint8_t watch_face_index, void **context_ptr); +void local_solar_time_face_activate(void *context); +bool local_solar_time_face_loop(movement_event_t event, void *context); +void local_solar_time_face_resign(void *context); + +#define local_solar_time_face ((const watch_face_t){ \ + local_solar_time_face_setup, \ + local_solar_time_face_activate, \ + local_solar_time_face_loop, \ + local_solar_time_face_resign, \ + NULL, \ +}) From 7d3a217d91b1065322d8e3419668ae42665020f6 Mon Sep 17 00:00:00 2001 From: Wesley Ellis Date: Fri, 27 Feb 2026 20:36:32 -0500 Subject: [PATCH 171/179] port tomato face to second_movement --- movement_faces.h | 1 + watch-faces.mk | 1 + watch-faces/complication/tomato_face.c | 184 +++++++++++++++++++++++++ watch-faces/complication/tomato_face.h | 82 +++++++++++ 4 files changed, 268 insertions(+) create mode 100644 watch-faces/complication/tomato_face.c create mode 100644 watch-faces/complication/tomato_face.h diff --git a/movement_faces.h b/movement_faces.h index 164dbb4c..8419702c 100644 --- a/movement_faces.h +++ b/movement_faces.h @@ -80,4 +80,5 @@ #include "simon_face.h" #include "ping_face.h" #include "rtccount_face.h" +#include "tomato_face.h" // New includes go above this line. diff --git a/watch-faces.mk b/watch-faces.mk index 69ec4e9c..dd9c7f36 100644 --- a/watch-faces.mk +++ b/watch-faces.mk @@ -55,4 +55,5 @@ SRCS += \ ./watch-faces/complication/lander_face.c \ ./watch-faces/complication/simon_face.c \ ./watch-faces/complication/ping_face.c \ + ./watch-faces/complication/tomato_face.c \ # New watch faces go above this line. diff --git a/watch-faces/complication/tomato_face.c b/watch-faces/complication/tomato_face.c new file mode 100644 index 00000000..5729bdd7 --- /dev/null +++ b/watch-faces/complication/tomato_face.c @@ -0,0 +1,184 @@ +/* + * MIT License + * + * Copyright (c) 2026 Wesley Ellis + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +#include +#include +#include "tomato_face.h" +#include "watch.h" +#include "watch_utility.h" + +static const uint8_t focus_min = 25; +static const uint8_t break_min = 5; + +static uint8_t get_length(tomato_state_t *state) { + if (state->kind == tomato_focus) { + return focus_min; + } else { + return break_min; + } +} + +static void tomato_start(tomato_state_t *state) { + uint8_t length = get_length(state); + + state->mode = tomato_run; + state->now_ts = movement_get_utc_timestamp(); + state->target_ts = watch_utility_offset_timestamp(state->now_ts, 0, length, 0); + watch_date_time_t target_dt = watch_utility_date_time_from_unix_time(state->target_ts, movement_get_current_timezone_offset()); + movement_schedule_background_task_for_face(state->watch_face_index, target_dt); + watch_set_indicator(WATCH_INDICATOR_BELL); +} + +static void tomato_draw(tomato_state_t *state) { + char buf[16]; + + uint32_t delta; + div_t result; + uint8_t min = 0; + uint8_t sec = 0; + char kind; + + if (state->kind == tomato_break) { + kind = 'b'; + } else { + kind = 'f'; + } + + switch (state->mode) { + case tomato_run: + if (state->target_ts <= state->now_ts) + delta = 0; + else + delta = state->target_ts - state->now_ts; + result = div(delta, 60); + min = result.quot; + sec = result.rem; + break; + case tomato_ready: + min = get_length(state); + sec = 0; + break; + } + + sprintf(buf, " %c", kind); + watch_display_text(WATCH_POSITION_TOP_RIGHT, buf); + sprintf(buf, "%2d%02d%2d", min, sec, state->done_count); + watch_display_text(WATCH_POSITION_BOTTOM, buf); +} + +static void tomato_reset(tomato_state_t *state) { + state->mode = tomato_ready; + movement_cancel_background_task_for_face(state->watch_face_index); + watch_clear_indicator(WATCH_INDICATOR_BELL); +} + +static void tomato_ring(tomato_state_t *state) { + movement_play_signal(); + tomato_reset(state); + if (state->kind == tomato_focus) { + state->kind = tomato_break; + state->done_count++; + } else { + state->kind = tomato_focus; + } +} + +void tomato_face_setup(uint8_t watch_face_index, void ** context_ptr) { + if (*context_ptr == NULL) { + *context_ptr = malloc(sizeof(tomato_state_t)); + tomato_state_t *state = (tomato_state_t *)*context_ptr; + memset(*context_ptr, 0, sizeof(tomato_state_t)); + state->mode = tomato_ready; + state->kind = tomato_focus; + state->done_count = 0; + state->watch_face_index = watch_face_index; + } +} + +void tomato_face_activate(void *context) { + tomato_state_t *state = (tomato_state_t *)context; + if (state->mode == tomato_run) { + state->now_ts = movement_get_utc_timestamp(); + watch_set_indicator(WATCH_INDICATOR_BELL); + } + watch_set_colon(); +} + +bool tomato_face_loop(movement_event_t event, void *context) { + tomato_state_t *state = (tomato_state_t *)context; + + switch (event.event_type) { + case EVENT_ACTIVATE: + watch_display_text_with_fallback(WATCH_POSITION_TOP, "TOMATO", "TO"); + tomato_draw(state); + break; + case EVENT_TICK: + if (state->mode == tomato_run) { + state->now_ts++; + } + tomato_draw(state); + break; + case EVENT_LIGHT_BUTTON_DOWN: + movement_illuminate_led(); + if (state->mode == tomato_ready) { + if (state->kind == tomato_break) { + state->kind = tomato_focus; + } else { + state->kind = tomato_break; + } + } + tomato_draw(state); + break; + case EVENT_ALARM_BUTTON_UP: + switch(state->mode) { + case tomato_run: + tomato_reset(state); + break; + case tomato_ready: + tomato_start(state); + break; + } + tomato_draw(state); + break; + case EVENT_ALARM_LONG_PRESS: + state->done_count = 0; + break; + case EVENT_BACKGROUND_TASK: + tomato_ring(state); + tomato_draw(state); + break; + case EVENT_TIMEOUT: + movement_move_to_face(0); + break; + default: + movement_default_loop_handler(event); + break; + } + + return true; +} + +void tomato_face_resign(void *context) { + (void) context; +} diff --git a/watch-faces/complication/tomato_face.h b/watch-faces/complication/tomato_face.h new file mode 100644 index 00000000..913bd9eb --- /dev/null +++ b/watch-faces/complication/tomato_face.h @@ -0,0 +1,82 @@ +/* + * MIT License + * + * Copyright (c) 2026 Wesley Ellis + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +#ifndef TOMATO_FACE_H_ +#define TOMATO_FACE_H_ + +/* + * TOMATO TIMER face + * + * Add a "tomato" timer watch face that alternates between 25 and 5 minute + * timers as in the Pomodoro Technique. + * https://en.wikipedia.org/wiki/Pomodoro_Technique + * + * The top right letter shows mode (f for focus or b for break). + * The bottom right shows how many focus sessions you've completed. + * (You can reset the count with a long press of alarm) + * + * When you show up and it says 25 minutes, you can start it (alarm), + * switch to 5 minute (light) mode or leave (mode). + * + * When it's running you can reset (alarm), or leave (mode). + * + * When it's done, we beep and go back to step 1, changing switching + * mode from focus to break (or break to focus) + */ + +#include "movement.h" + +typedef enum { + tomato_ready, + tomato_run, +} tomato_mode; + +typedef enum { + tomato_break, + tomato_focus, +} tomato_kind; + +typedef struct { + uint32_t target_ts; + uint32_t now_ts; + tomato_mode mode; + tomato_kind kind; + uint8_t done_count; + uint8_t watch_face_index; +} tomato_state_t; + +void tomato_face_setup(uint8_t watch_face_index, void ** context_ptr); +void tomato_face_activate(void *context); +bool tomato_face_loop(movement_event_t event, void *context); +void tomato_face_resign(void *context); + +#define tomato_face ((const watch_face_t){ \ + tomato_face_setup, \ + tomato_face_activate, \ + tomato_face_loop, \ + tomato_face_resign, \ + NULL, \ +}) + +#endif // TOMATO_FACE_H_ From 1a33ae88f75eaeee5167a88e29d1164cbd77a992 Mon Sep 17 00:00:00 2001 From: Raffael Mancini Date: Wed, 4 Mar 2026 12:34:23 +0100 Subject: [PATCH 172/179] Move complications/local_solar_time_face -> clock/solar_time_face --- movement_faces.h | 2 +- watch-faces.mk | 2 +- .../solar_time_face.c} | 38 +++++++++---------- .../solar_time_face.h} | 38 +++++++++---------- 4 files changed, 40 insertions(+), 40 deletions(-) rename watch-faces/{complication/local_solar_time_face.c => clock/solar_time_face.c} (85%) rename watch-faces/{complication/local_solar_time_face.h => clock/solar_time_face.h} (74%) diff --git a/movement_faces.h b/movement_faces.h index 4a346850..cf940da4 100644 --- a/movement_faces.h +++ b/movement_faces.h @@ -80,5 +80,5 @@ #include "simon_face.h" #include "ping_face.h" #include "rtccount_face.h" -#include "local_solar_time_face.h" +#include "solar_time_face.h" // New includes go above this line. diff --git a/watch-faces.mk b/watch-faces.mk index bb5d5aca..95bd22ad 100644 --- a/watch-faces.mk +++ b/watch-faces.mk @@ -55,5 +55,5 @@ SRCS += \ ./watch-faces/complication/lander_face.c \ ./watch-faces/complication/simon_face.c \ ./watch-faces/complication/ping_face.c \ - ./watch-faces/complication/local_solar_time_face.c \ + ./watch-faces/clock/solar_time_face.c \ # New watch faces go above this line. diff --git a/watch-faces/complication/local_solar_time_face.c b/watch-faces/clock/solar_time_face.c similarity index 85% rename from watch-faces/complication/local_solar_time_face.c rename to watch-faces/clock/solar_time_face.c index 4c8b2397..f0b69ed0 100644 --- a/watch-faces/complication/local_solar_time_face.c +++ b/watch-faces/clock/solar_time_face.c @@ -28,7 +28,7 @@ #include #include #include -#include "local_solar_time_face.h" +#include "solar_time_face.h" #include "watch.h" #include "watch_utility.h" #include "filesystem.h" @@ -62,7 +62,7 @@ static movement_location_t _load_location(void) { } /* Compute and cache EoT and TC. Call when d != state->last_calc_d. */ -static void _compute_daily(local_solar_time_state_t *state, uint16_t d) { +static void _compute_daily(solar_time_state_t *state, uint16_t d) { /* LSTM — movement_get_current_timezone_offset() returns seconds from UTC */ float delta_T_UTC = (float)movement_get_current_timezone_offset() / 3600.0f; float LSTM = 15.0f * delta_T_UTC; @@ -79,7 +79,7 @@ static void _compute_daily(local_solar_time_state_t *state, uint16_t d) { } /* Recompute if the day-of-year has rolled over. Returns current d. */ -static uint16_t _maybe_recompute(local_solar_time_state_t *state, watch_date_time_t dt) { +static uint16_t _maybe_recompute(solar_time_state_t *state, watch_date_time_t dt) { uint16_t d = watch_utility_days_since_new_year( (uint16_t)(dt.unit.year + WATCH_RTC_REFERENCE_YEAR), dt.unit.month, @@ -101,7 +101,7 @@ static int32_t _lst_seconds(watch_date_time_t dt, float TC) { return ((lt + tc) % 86400 + 86400) % 86400; } -static void _update_display(local_solar_time_state_t *state, watch_date_time_t dt) { +static void _update_display(solar_time_state_t *state, watch_date_time_t dt) { char buf[14]; if (_load_location().reg == 0) { @@ -111,7 +111,7 @@ static void _update_display(local_solar_time_state_t *state, watch_date_time_t d switch (state->mode) { - case LOCAL_SOLAR_TIME_MODE_LST: { + case SOLAR_TIME_MODE_LST: { int32_t s = _lst_seconds(dt, state->TC); sprintf(buf, "SO %02d%02d%02d", (int)(s / 3600), (int)((s % 3600) / 60), (int)(s % 60)); @@ -119,7 +119,7 @@ static void _update_display(local_solar_time_state_t *state, watch_date_time_t d break; } - case LOCAL_SOLAR_TIME_MODE_NOON: { + case SOLAR_TIME_MODE_NOON: { /* Solar noon: moment when LST = 12:00 → LT_noon = 12h - TC/60 */ int32_t s = (int32_t)(( 12.0f - state->TC / 60.0f) * 3600.0f); s = ((s % 86400) + 86400) % 86400; @@ -128,7 +128,7 @@ static void _update_display(local_solar_time_state_t *state, watch_date_time_t d break; } - case LOCAL_SOLAR_TIME_MODE_HRA: { + case SOLAR_TIME_MODE_HRA: { /* HRA = 15 * (LST - 12); negative = morning, positive = afternoon */ int32_t s = _lst_seconds(dt, state->TC); int16_t hra = (int16_t)roundf(15.0f * ((float)s / 3600.0f - 12.0f)); @@ -146,17 +146,17 @@ static void _update_display(local_solar_time_state_t *state, watch_date_time_t d /* ---- Movement callbacks -------------------------------------------------- */ -void local_solar_time_face_setup(uint8_t watch_face_index, void **context_ptr) { +void solar_time_face_setup(uint8_t watch_face_index, void **context_ptr) { (void)watch_face_index; if (*context_ptr == NULL) { - *context_ptr = malloc(sizeof(local_solar_time_state_t)); - memset(*context_ptr, 0, sizeof(local_solar_time_state_t)); + *context_ptr = malloc(sizeof(solar_time_state_t)); + memset(*context_ptr, 0, sizeof(solar_time_state_t)); /* last_calc_d == 0 guarantees recomputation on first tick */ } } -void local_solar_time_face_activate(void *context) { - local_solar_time_state_t *state = (local_solar_time_state_t *)context; +void solar_time_face_activate(void *context) { + solar_time_state_t *state = (solar_time_state_t *)context; #if __EMSCRIPTEN__ /* In the simulator the browser exposes lat/lon as JS globals. @@ -180,8 +180,8 @@ void local_solar_time_face_activate(void *context) { _maybe_recompute(state, dt); } -bool local_solar_time_face_loop(movement_event_t event, void *context) { - local_solar_time_state_t *state = (local_solar_time_state_t *)context; +bool solar_time_face_loop(movement_event_t event, void *context) { + solar_time_state_t *state = (solar_time_state_t *)context; switch (event.event_type) { case EVENT_ACTIVATE: @@ -193,7 +193,7 @@ bool local_solar_time_face_loop(movement_event_t event, void *context) { } case EVENT_ALARM_BUTTON_UP: - state->mode = (local_solar_time_mode_t)((state->mode + 1) % LOCAL_SOLAR_TIME_NUM_MODES); + state->mode = (solar_time_mode_t)((state->mode + 1) % SOLAR_TIME_NUM_MODES); { watch_date_time_t dt = movement_get_local_date_time(); _update_display(state, dt); @@ -209,7 +209,7 @@ bool local_solar_time_face_loop(movement_event_t event, void *context) { } case EVENT_TIMEOUT: - state->mode = LOCAL_SOLAR_TIME_MODE_LST; + state->mode = SOLAR_TIME_MODE_LST; if (_load_location().reg == 0) movement_move_to_face(0); break; @@ -220,8 +220,8 @@ bool local_solar_time_face_loop(movement_event_t event, void *context) { return true; } -void local_solar_time_face_resign(void *context) { - local_solar_time_state_t *state = (local_solar_time_state_t *)context; - state->mode = LOCAL_SOLAR_TIME_MODE_LST; +void solar_time_face_resign(void *context) { + solar_time_state_t *state = (solar_time_state_t *)context; + state->mode = SOLAR_TIME_MODE_LST; watch_clear_colon(); } diff --git a/watch-faces/complication/local_solar_time_face.h b/watch-faces/clock/solar_time_face.h similarity index 74% rename from watch-faces/complication/local_solar_time_face.h rename to watch-faces/clock/solar_time_face.h index dc677ed0..48faf693 100644 --- a/watch-faces/complication/local_solar_time_face.h +++ b/watch-faces/clock/solar_time_face.h @@ -25,7 +25,7 @@ #pragma once /* - * LOCAL SOLAR TIME FACE + * SOLAR TIME FACE * * Displays solar time information based on the user's location. * Formulas follow the notation from: @@ -36,7 +36,7 @@ * B - Seasonal angle [degrees] = (360/365) * (d - 81) * EoT - Equation of Time [minutes] = 9.87*sin(2B) - 7.53*cos(B) - 1.5*sin(B) * TC - Time Correction Factor [minutes] = 4*(Longitude - LSTM) + EoT - * LST - Local Solar Time [hours] = LT + TC/60 + * LST - Solar Time [hours] = LT + TC/60 * HRA - Hour Angle [degrees] = 15*(LST - 12) * * B, EoT and TC only depend on the day-of-year d, so they are cached @@ -48,7 +48,7 @@ * "SO no Loc". * * Display modes (cycle with the Alarm / start-stop button): - * SO HH:MM:SS — Local Solar Time (LST), live seconds display + * SO HH:MM:SS — Solar Time (LST), live seconds display * nO HH:MM — Solar Noon in local clock time * Hr ±DDD — Hour Angle (HRA) in degrees; negative=morning, positive=afternoon */ @@ -56,29 +56,29 @@ #include "movement.h" typedef enum { - LOCAL_SOLAR_TIME_MODE_LST = 0, /* Local Solar Time SO HH:MM:SS */ - LOCAL_SOLAR_TIME_MODE_NOON = 1, /* Solar Noon (local) nO HH:MM */ - LOCAL_SOLAR_TIME_MODE_HRA = 2, /* Hour Angle Hr ±DDD */ - LOCAL_SOLAR_TIME_NUM_MODES -} local_solar_time_mode_t; + SOLAR_TIME_MODE_LST = 0, /* Solar Time SO HH:MM:SS */ + SOLAR_TIME_MODE_NOON = 1, /* Solar Noon (local) nO HH:MM */ + SOLAR_TIME_MODE_HRA = 2, /* Hour Angle Hr ±DDD */ + SOLAR_TIME_NUM_MODES +} solar_time_mode_t; typedef struct { - local_solar_time_mode_t mode; + solar_time_mode_t mode; uint16_t last_calc_d; /* day-of-year (1-366) when EoT/TC were last computed; 0 (zero-init) guarantees recomputation on first tick */ float EoT; /* Equation of Time [minutes] */ float TC; /* Time Correction Factor [minutes] */ -} local_solar_time_state_t; +} solar_time_state_t; -void local_solar_time_face_setup(uint8_t watch_face_index, void **context_ptr); -void local_solar_time_face_activate(void *context); -bool local_solar_time_face_loop(movement_event_t event, void *context); -void local_solar_time_face_resign(void *context); +void solar_time_face_setup(uint8_t watch_face_index, void **context_ptr); +void solar_time_face_activate(void *context); +bool solar_time_face_loop(movement_event_t event, void *context); +void solar_time_face_resign(void *context); -#define local_solar_time_face ((const watch_face_t){ \ - local_solar_time_face_setup, \ - local_solar_time_face_activate, \ - local_solar_time_face_loop, \ - local_solar_time_face_resign, \ +#define solar_time_face ((const watch_face_t){ \ + solar_time_face_setup, \ + solar_time_face_activate, \ + solar_time_face_loop, \ + solar_time_face_resign, \ NULL, \ }) From 0d6be495ef150f669d85663e517c0d17979f6a9b Mon Sep 17 00:00:00 2001 From: Raffael Mancini Date: Wed, 4 Mar 2026 13:33:26 +0100 Subject: [PATCH 173/179] Rollback config --- movement_config.h | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/movement_config.h b/movement_config.h index bd23b810..01d718b1 100644 --- a/movement_config.h +++ b/movement_config.h @@ -28,10 +28,10 @@ #include "movement_faces.h" const watch_face_t watch_faces[] = { - local_solar_time_face, - clock_face, - /* world_clock_face, */ + clock_face, + world_clock_face, sunrise_sunset_face, + solar_time_face, moon_phase_face, fast_stopwatch_face, countdown_face, @@ -50,7 +50,7 @@ const watch_face_t watch_faces[] = { * Some folks also like to use this to hide the preferences and time set faces from the normal rotation. * If you don't want any faces to be excluded, set this to 0 and a long Mode press will have no effect. */ -#define MOVEMENT_SECONDARY_FACE_INDEX (MOVEMENT_NUM_FACES - 5) +#define MOVEMENT_SECONDARY_FACE_INDEX 5 /* Custom hourly chime tune. Check movement_custom_signal_tunes.h for options. */ #define SIGNAL_TUNE_DEFAULT @@ -107,6 +107,6 @@ const watch_face_t watch_faces[] = { * A value of 4 is a good starting point if you have issues * with multiple button presses firing. */ -#define MOVEMENT_DEBOUNCE_TICKS 0 +#define MOVEMENT_DEBOUNCE_TICKS 4 #endif // MOVEMENT_CONFIG_H_ From c2cead4e3bfc4e84e8b9b5928f3b058ff2f0c778 Mon Sep 17 00:00:00 2001 From: Raffael Mancini Date: Wed, 4 Mar 2026 13:33:43 +0100 Subject: [PATCH 174/179] Support custom display --- watch-faces/clock/solar_time_face.c | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/watch-faces/clock/solar_time_face.c b/watch-faces/clock/solar_time_face.c index f0b69ed0..3f83e287 100644 --- a/watch-faces/clock/solar_time_face.c +++ b/watch-faces/clock/solar_time_face.c @@ -102,10 +102,13 @@ static int32_t _lst_seconds(watch_date_time_t dt, float TC) { } static void _update_display(solar_time_state_t *state, watch_date_time_t dt) { - char buf[14]; + char bottom[7]; if (_load_location().reg == 0) { - watch_display_text(WATCH_POSITION_FULL, "SO no Loc"); + watch_display_text_with_fallback(WATCH_POSITION_TOP_LEFT, "SOL", "SO"); + watch_display_text(WATCH_POSITION_TOP_RIGHT, " "); + watch_display_text(WATCH_POSITION_BOTTOM, "no Loc"); + watch_clear_colon(); return; } @@ -113,7 +116,8 @@ static void _update_display(solar_time_state_t *state, watch_date_time_t dt) { case SOLAR_TIME_MODE_LST: { int32_t s = _lst_seconds(dt, state->TC); - sprintf(buf, "SO %02d%02d%02d", + watch_display_text_with_fallback(WATCH_POSITION_TOP_LEFT, "SOL", "SO"); + sprintf(bottom, "%02d%02d%02d", (int)(s / 3600), (int)((s % 3600) / 60), (int)(s % 60)); watch_set_colon(); break; @@ -123,7 +127,8 @@ static void _update_display(solar_time_state_t *state, watch_date_time_t dt) { /* Solar noon: moment when LST = 12:00 → LT_noon = 12h - TC/60 */ int32_t s = (int32_t)(( 12.0f - state->TC / 60.0f) * 3600.0f); s = ((s % 86400) + 86400) % 86400; - sprintf(buf, "nO %02d%02d ", (int)(s / 3600), (int)((s % 3600) / 60)); + watch_display_text_with_fallback(WATCH_POSITION_TOP_LEFT, "NOO", "nO"); + sprintf(bottom, "%02d%02d ", (int)(s / 3600), (int)((s % 3600) / 60)); watch_set_colon(); break; } @@ -132,7 +137,8 @@ static void _update_display(solar_time_state_t *state, watch_date_time_t dt) { /* HRA = 15 * (LST - 12); negative = morning, positive = afternoon */ int32_t s = _lst_seconds(dt, state->TC); int16_t hra = (int16_t)roundf(15.0f * ((float)s / 3600.0f - 12.0f)); - sprintf(buf, "Hr %+4d ", (int)hra); + watch_display_text_with_fallback(WATCH_POSITION_TOP_LEFT, "HrA", "Hr"); + sprintf(bottom, "%+4d ", (int)hra); watch_clear_colon(); break; } @@ -141,7 +147,8 @@ static void _update_display(solar_time_state_t *state, watch_date_time_t dt) { return; } - watch_display_text(WATCH_POSITION_FULL, buf); + watch_display_text(WATCH_POSITION_TOP_RIGHT, " "); + watch_display_text(WATCH_POSITION_BOTTOM, bottom); } /* ---- Movement callbacks -------------------------------------------------- */ From eba8b7953e976b3d01f9ec9f69b7686833d669dc Mon Sep 17 00:00:00 2001 From: Raffael Mancini Date: Wed, 4 Mar 2026 14:24:27 +0100 Subject: [PATCH 175/179] Revert more config --- movement_config.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/movement_config.h b/movement_config.h index 01d718b1..adaf7cdb 100644 --- a/movement_config.h +++ b/movement_config.h @@ -50,7 +50,7 @@ const watch_face_t watch_faces[] = { * Some folks also like to use this to hide the preferences and time set faces from the normal rotation. * If you don't want any faces to be excluded, set this to 0 and a long Mode press will have no effect. */ -#define MOVEMENT_SECONDARY_FACE_INDEX 5 +#define MOVEMENT_SECONDARY_FACE_INDEX (MOVEMENT_NUM_FACES - 5) /* Custom hourly chime tune. Check movement_custom_signal_tunes.h for options. */ #define SIGNAL_TUNE_DEFAULT @@ -107,6 +107,6 @@ const watch_face_t watch_faces[] = { * A value of 4 is a good starting point if you have issues * with multiple button presses firing. */ -#define MOVEMENT_DEBOUNCE_TICKS 4 +#define MOVEMENT_DEBOUNCE_TICKS 0 #endif // MOVEMENT_CONFIG_H_ From 14d0ba73e0e1d3ca02f7b9b5f3f576970738ee61 Mon Sep 17 00:00:00 2001 From: Raffael Mancini Date: Wed, 4 Mar 2026 14:55:32 +0100 Subject: [PATCH 176/179] Better support for custom display --- watch-faces/clock/solar_time_face.c | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/watch-faces/clock/solar_time_face.c b/watch-faces/clock/solar_time_face.c index 3f83e287..54592975 100644 --- a/watch-faces/clock/solar_time_face.c +++ b/watch-faces/clock/solar_time_face.c @@ -117,6 +117,7 @@ static void _update_display(solar_time_state_t *state, watch_date_time_t dt) { case SOLAR_TIME_MODE_LST: { int32_t s = _lst_seconds(dt, state->TC); watch_display_text_with_fallback(WATCH_POSITION_TOP_LEFT, "SOL", "SO"); + watch_display_text(WATCH_POSITION_TOP_RIGHT, "Ar"); sprintf(bottom, "%02d%02d%02d", (int)(s / 3600), (int)((s % 3600) / 60), (int)(s % 60)); watch_set_colon(); @@ -127,7 +128,8 @@ static void _update_display(solar_time_state_t *state, watch_date_time_t dt) { /* Solar noon: moment when LST = 12:00 → LT_noon = 12h - TC/60 */ int32_t s = (int32_t)(( 12.0f - state->TC / 60.0f) * 3600.0f); s = ((s % 86400) + 86400) % 86400; - watch_display_text_with_fallback(WATCH_POSITION_TOP_LEFT, "NOO", "nO"); + watch_display_text_with_fallback(WATCH_POSITION_TOP_LEFT, "NOO", "NO"); + watch_display_text(WATCH_POSITION_TOP_RIGHT, "n "); sprintf(bottom, "%02d%02d ", (int)(s / 3600), (int)((s % 3600) / 60)); watch_set_colon(); break; @@ -138,6 +140,7 @@ static void _update_display(solar_time_state_t *state, watch_date_time_t dt) { int32_t s = _lst_seconds(dt, state->TC); int16_t hra = (int16_t)roundf(15.0f * ((float)s / 3600.0f - 12.0f)); watch_display_text_with_fallback(WATCH_POSITION_TOP_LEFT, "HrA", "Hr"); + watch_display_text(WATCH_POSITION_TOP_RIGHT, "n "); sprintf(bottom, "%+4d ", (int)hra); watch_clear_colon(); break; @@ -147,7 +150,6 @@ static void _update_display(solar_time_state_t *state, watch_date_time_t dt) { return; } - watch_display_text(WATCH_POSITION_TOP_RIGHT, " "); watch_display_text(WATCH_POSITION_BOTTOM, bottom); } From 0247ef43a954461a74aa4ee76e5fc58d69feae8a Mon Sep 17 00:00:00 2001 From: Wesley Ellis Date: Sat, 7 Mar 2026 16:53:35 -0500 Subject: [PATCH 177/179] fix timezone / unix time conversion bug --- watch-faces/complication/tomato_face.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/watch-faces/complication/tomato_face.c b/watch-faces/complication/tomato_face.c index 5729bdd7..321e5dea 100644 --- a/watch-faces/complication/tomato_face.c +++ b/watch-faces/complication/tomato_face.c @@ -45,7 +45,7 @@ static void tomato_start(tomato_state_t *state) { state->mode = tomato_run; state->now_ts = movement_get_utc_timestamp(); state->target_ts = watch_utility_offset_timestamp(state->now_ts, 0, length, 0); - watch_date_time_t target_dt = watch_utility_date_time_from_unix_time(state->target_ts, movement_get_current_timezone_offset()); + watch_date_time_t target_dt = watch_utility_date_time_from_unix_time(state->target_ts, 0); movement_schedule_background_task_for_face(state->watch_face_index, target_dt); watch_set_indicator(WATCH_INDICATOR_BELL); } From 67f63872f867d9dce79c7a30a85e20b523452deb Mon Sep 17 00:00:00 2001 From: Wesley Ellis Date: Sat, 7 Mar 2026 16:53:48 -0500 Subject: [PATCH 178/179] don't resign from tomato time if timer is running --- watch-faces/complication/tomato_face.c | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/watch-faces/complication/tomato_face.c b/watch-faces/complication/tomato_face.c index 321e5dea..7c419d55 100644 --- a/watch-faces/complication/tomato_face.c +++ b/watch-faces/complication/tomato_face.c @@ -169,7 +169,9 @@ bool tomato_face_loop(movement_event_t event, void *context) { tomato_draw(state); break; case EVENT_TIMEOUT: - movement_move_to_face(0); + if (state->mode != tomato_run) { + movement_move_to_face(0); + } break; default: movement_default_loop_handler(event); From 1243c57dccc1cfc825f529e70eff6002f09cafe1 Mon Sep 17 00:00:00 2001 From: voloved <36523934+voloved@users.noreply.github.com> Date: Sat, 14 Mar 2026 09:58:25 -0400 Subject: [PATCH 179/179] Remove solar_time_face from watch_faces array --- movement_config.h | 1 - 1 file changed, 1 deletion(-) diff --git a/movement_config.h b/movement_config.h index adaf7cdb..2f48d8fd 100644 --- a/movement_config.h +++ b/movement_config.h @@ -31,7 +31,6 @@ const watch_face_t watch_faces[] = { clock_face, world_clock_face, sunrise_sunset_face, - solar_time_face, moon_phase_face, fast_stopwatch_face, countdown_face,