From 991a4a1dc57a785c7d350049587c5dfa46e5efb0 Mon Sep 17 00:00:00 2001 From: Joey Castillo Date: Sun, 27 Jul 2025 11:30:49 -0400 Subject: [PATCH 01/92] add delay before sampling VBUS_DET --- movement.c | 1 + 1 file changed, 1 insertion(+) diff --git a/movement.c b/movement.c index 8e2c5031..b3a59e83 100644 --- a/movement.c +++ b/movement.c @@ -609,6 +609,7 @@ void app_init(void) { // check if we are plugged into USB power. HAL_GPIO_VBUS_DET_in(); HAL_GPIO_VBUS_DET_pulldown(); + delay_ms(10); if (HAL_GPIO_VBUS_DET_read()){ /// if so, enable USB functionality. _watch_enable_usb(); From 8c456c9b62590619d8811e1dd57e16eedfc4245d Mon Sep 17 00:00:00 2001 From: Joey Castillo Date: Sun, 27 Jul 2025 11:54:48 -0400 Subject: [PATCH 02/92] ke decimal time: silence warnings --- watch-faces/clock/ke_decimal_time_face.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/watch-faces/clock/ke_decimal_time_face.c b/watch-faces/clock/ke_decimal_time_face.c index 6737477c..83cd7137 100644 --- a/watch-faces/clock/ke_decimal_time_face.c +++ b/watch-faces/clock/ke_decimal_time_face.c @@ -36,14 +36,14 @@ static void _display_date(watch_date_time_t date_time) { } static void _display_time(ke_decimal_time_state_t *state, watch_date_time_t date_time, bool low_energy) { - char buf[7]; + char buf[8]; uint32_t value = date_time.unit.hour * 3600 + date_time.unit.minute * 60 + date_time.unit.second; if (value == state->previous_time) return; value = value * 100; value = value / 864; - snprintf(buf, sizeof(buf), "%04d#o", value); + snprintf(buf, sizeof(buf), "%04ld#o", value); // if under 10%, display 0.00 instead of 00.00 if (value < 1000) buf[0] = ' '; From 39d605204f628d10bc617025006b57eabb7164bf Mon Sep 17 00:00:00 2001 From: Joey Castillo Date: Sun, 27 Jul 2025 12:39:31 -0400 Subject: [PATCH 03/92] mark movement_state as volatile --- movement.c | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/movement.c b/movement.c index b3a59e83..18ec0ac2 100644 --- a/movement.c +++ b/movement.c @@ -55,7 +55,7 @@ #include "watch_usb_cdc.h" #endif -movement_state_t movement_state; +volatile movement_state_t movement_state; 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}; @@ -616,7 +616,7 @@ void app_init(void) { } HAL_GPIO_VBUS_DET_off(); - memset(&movement_state, 0, sizeof(movement_state)); + memset((void *)&movement_state, 0, sizeof(movement_state)); movement_state.has_thermistor = thermistor_driver_init(); @@ -972,7 +972,7 @@ 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, uint16_t *down_timestamp) { +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; From 78eb0c0c978997c106fa0aa7c8a350353cd4aa3a Mon Sep 17 00:00:00 2001 From: James Haggerty Date: Tue, 22 Jul 2025 20:37:56 +1000 Subject: [PATCH 04/92] Fix command line in simulator In the original movement, the 'usb enabled' check was overridden to true for the simulator such that shell_task() would always be executed. In the new 'dummy' device in gossamer used by the simulator, this usb check returns false. Seemed like a slightly cleaner thing to do was to call shell_task() regardless rather than to incorrectly pretend that USB was connected. --- movement.c | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/movement.c b/movement.c index 18ec0ac2..f932de0a 100644 --- a/movement.c +++ b/movement.c @@ -945,10 +945,14 @@ bool app_loop(void) { } } +#if __EMSCRIPTEN__ + shell_task(); +#else // if we are plugged into USB, handle the serial shell if (usb_is_enabled()) { shell_task(); } +#endif event.subsecond = 0; From 4b4735065fb33531b7262e6fb9d76cca1799ff2b Mon Sep 17 00:00:00 2001 From: voloved <36523934+voloved@users.noreply.github.com> Date: Sun, 3 Aug 2025 12:56:23 -0400 Subject: [PATCH 05/92] Wordle port (#45) * Moved Wordle from legacy to main folder * Add delays before registering btns, don't repeat words, cleaned up enum names * Updated print logic for second movement * Removed from watch_faces --- movement_faces.h | 1 + watch-faces.mk | 1 + .../complication/wordle_face.c | 230 +++++++++++------- .../complication/wordle_face.h | 50 ++-- .../complication/wordle_face_dict.h | 0 5 files changed, 174 insertions(+), 108 deletions(-) rename {legacy/watch_faces => watch-faces}/complication/wordle_face.c (75%) rename {legacy/watch_faces => watch-faces}/complication/wordle_face.h (85%) rename {legacy/watch_faces => watch-faces}/complication/wordle_face_dict.h (100%) diff --git a/movement_faces.h b/movement_faces.h index 3583e9a3..f895abd9 100644 --- a/movement_faces.h +++ b/movement_faces.h @@ -62,4 +62,5 @@ #include "tally_face.h" #include "probability_face.h" #include "ke_decimal_time_face.h" +#include "wordle_face.h" // New includes go above this line. diff --git a/watch-faces.mk b/watch-faces.mk index eff50d27..acdabd38 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 75% rename from legacy/watch_faces/complication/wordle_face.c rename to watch-faces/complication/wordle_face.c index a93a0e0e..c3a2e69a 100644 --- a/legacy/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__ @@ -35,7 +36,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 +44,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 +52,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) @@ -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); @@ -99,13 +100,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; - watch_display_string("nodict", 4); + state->curr_screen = WORDLE_SCREEN_NO_DICT; + 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 = SCREEN_ALREADY_GUESSED; - watch_display_string("GUESSD", 4); + state->curr_screen = WORDLE_SCREEN_ALREADY_GUESSED; + watch_display_text(WATCH_POSITION_BOTTOM, "GUESSD"); + state->ignore_btn_ticks = WORDLE_TICK_BAD_GUESS; } static uint32_t check_word_in_dict(uint8_t *word_elements) { @@ -164,12 +167,12 @@ 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); + watch_display_character('H', 3); else - watch_display_string(" ", 3); + watch_display_character(' ', 3); } static void update_known_wrong_letters(wordle_state_t *state) { @@ -197,11 +200,11 @@ 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) { - state->curr_screen = SCREEN_PLAYING; + state->curr_screen = WORDLE_SCREEN_PLAYING; display_attempt(state->attempt); display_all_letters(state); } @@ -230,64 +233,79 @@ 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); - watch_display_string(" -", 4); + watch_display_character('-', 5); #if __EMSCRIPTEN__ printf("ANSWER: %s\r\n", _valid_words[state->curr_answer]); #endif } static void display_title(wordle_state_t *state) { - state->curr_screen = SCREEN_TITLE; - watch_display_string("WO WordLE", 0); + state->curr_screen = WORDLE_SCREEN_TITLE; + 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 = SCREEN_CONTINUE; - watch_display_string("Cont ", 4); + state->curr_screen = WORDLE_SCREEN_CONTINUE; + 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]; - state->curr_screen = SCREEN_STREAK; + 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); } #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); - 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 @@ -301,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) { @@ -323,38 +343,40 @@ 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 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 +394,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 +429,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 +440,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 +468,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 +478,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 +508,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 +519,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); + watch_display_character(' ', 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 +583,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 +610,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 +618,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/legacy/watch_faces/complication/wordle_face.h b/watch-faces/complication/wordle_face.h similarity index 85% rename from legacy/watch_faces/complication/wordle_face.h rename to watch-faces/complication/wordle_face.h index 5cc2f09a..e4f7d0f9 100644 --- a/legacy/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_ - 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 1954944d8d284e6afc0f9ae4a8aa623ed6ff2573 Mon Sep 17 00:00:00 2001 From: Konrad Rieck Date: Sun, 3 Aug 2025 19:03:12 +0200 Subject: [PATCH 06/92] Port of deadline face (#48) * port of deadline face * removed beep type enum from header --------- Co-authored-by: Joey Castillo --- movement_faces.h | 1 + watch-faces.mk | 1 + watch-faces/complication/deadline_face.c | 637 +++++++++++++++++++++++ watch-faces/complication/deadline_face.h | 65 +++ 4 files changed, 704 insertions(+) create mode 100644 watch-faces/complication/deadline_face.c create mode 100644 watch-faces/complication/deadline_face.h diff --git a/movement_faces.h b/movement_faces.h index f895abd9..9d3083c5 100644 --- a/movement_faces.h +++ b/movement_faces.h @@ -62,5 +62,6 @@ #include "tally_face.h" #include "probability_face.h" #include "ke_decimal_time_face.h" +#include "deadline_face.h" #include "wordle_face.h" // New includes go above this line. diff --git a/watch-faces.mk b/watch-faces.mk index acdabd38..767e1839 100644 --- a/watch-faces.mk +++ b/watch-faces.mk @@ -38,4 +38,5 @@ SRCS += \ ./watch-faces/complication/kitchen_conversions_face.c \ ./watch-faces/complication/periodic_table_face.c \ ./watch-faces/clock/ke_decimal_time_face.c \ + ./watch-faces/complication/deadline_face.c \ # New watch faces go above this line. diff --git a/watch-faces/complication/deadline_face.c b/watch-faces/complication/deadline_face.c new file mode 100644 index 00000000..41240e76 --- /dev/null +++ b/watch-faces/complication/deadline_face.c @@ -0,0 +1,637 @@ +/* + * MIT License + * + * Copyright (c) 2023-2025 Konrad Rieck + * + * 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. + */ + +/* + * # 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" +#include "watch.h" +#include "watch_utility.h" + +/* Beep types */ +typedef enum { + BEEP_BUTTON, + BEEP_ENABLE, + BEEP_DISABLE +} beep_type_t; + +#define SETTINGS_NUM (5) +const char settings_titles[SETTINGS_NUM][6] = { "Year ", "Month", "Day ", "Hour ", "Minut" }; +const char settings_fallback_titles[SETTINGS_NUM][3] = { "YR", "MO", "DA", "HR", "M1" }; + +const char *running_title = "DUE"; +const char *running_fallback_title = "DL"; + +/* Local functions */ +static void _deadline_running_init(deadline_state_t * state); +static bool _deadline_running_loop(movement_event_t event, void *context); +static void _deadline_running_display(movement_event_t event, deadline_state_t * state); +static void _deadline_settings_init(deadline_state_t * state); +static bool _deadline_settings_loop(movement_event_t event, void *context); +static void _deadline_settings_display(movement_event_t event, deadline_state_t * state, + watch_date_time_t date); + +/* Check for leap year */ +static inline bool _is_leap(int16_t y) +{ + y += 1900; + return !(y % 4) && ((y % 100) || !(y % 400)); +} + +/* Modulo function */ +static inline unsigned int _mod(int a, int b) +{ + int r = a % b; + return r < 0 ? r + b : r; +} + +/* Return days in month */ +static inline int _days_in_month(int16_t month, int16_t year) +{ + uint8_t days[] = { 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 }; + month = _mod(month - 1, 12); + + if (month == 1 && _is_leap(year)) { + return days[month] + 1; + } else { + return days[month]; + } +} + +/* Play beep sound based on type */ +static inline void _beep(beep_type_t beep_type) +{ + if (!movement_button_should_sound()) + return; + + switch (beep_type) { + case BEEP_BUTTON: + watch_buzzer_play_note(BUZZER_NOTE_C7, 50); + break; + + case BEEP_ENABLE: + watch_buzzer_play_note(BUZZER_NOTE_G7, 50); + watch_buzzer_play_note(BUZZER_NOTE_REST, 75); + watch_buzzer_play_note(BUZZER_NOTE_C8, 75); + break; + + case BEEP_DISABLE: + watch_buzzer_play_note(BUZZER_NOTE_C8, 50); + watch_buzzer_play_note(BUZZER_NOTE_REST, 75); + watch_buzzer_play_note(BUZZER_NOTE_G7, 75); + break; + } +} + +/* Change tick frequency */ +static inline void _change_tick_freq(uint8_t freq, deadline_state_t *state) +{ + if (state->tick_freq != freq) { + movement_request_tick_frequency(freq); + state->tick_freq = freq; + } +} + +/* Determine index of closest deadline */ +static uint8_t _closest_deadline(deadline_state_t *state) +{ + watch_date_time_t now = movement_get_local_date_time(); + uint32_t now_ts = watch_utility_date_time_to_unix_time(now, 0); + uint32_t min_ts = UINT32_MAX; + uint8_t min_index = 0; + + for (uint8_t i = 0; i < DEADLINE_FACE_DATES; i++) { + /* Skip expired deadlines and those further in the future than current minimum */ + if (state->deadlines[i] < now_ts || state->deadlines[i] > min_ts) { + continue; + } + min_ts = state->deadlines[i]; + min_index = i; + } + + return min_index; +} + +/* Play background alarm */ +static void _background_alarm_play(deadline_state_t *state) +{ + movement_play_alarm(); + movement_move_to_face(state->face_idx); +} + +/* Reset deadline to tomorrow */ +static inline void _reset_deadline(deadline_state_t *state) +{ + /* Get current time and reset hours/minutes/seconds */ + watch_date_time_t date_time = movement_get_local_date_time(); + date_time.unit.second = 0; + date_time.unit.minute = 0; + date_time.unit.hour = 0; + + /* Add 24 hours to obtain first second of tomorrow */ + uint32_t ts = watch_utility_date_time_to_unix_time(date_time, 0); + ts += 24 * 60 * 60; + + state->deadlines[state->current_index] = ts; +} + +/* Calculate the naive difference between deadline and current time */ +static void _calculate_time_remaining(watch_date_time_t dl, watch_date_time_t now, int16_t *units) +{ + units[0] = dl.unit.second - now.unit.second; + units[1] = dl.unit.minute - now.unit.minute; + units[2] = dl.unit.hour - now.unit.hour; + units[3] = dl.unit.day - now.unit.day; + units[4] = dl.unit.month - now.unit.month; + units[5] = dl.unit.year - now.unit.year; +} + +/* Format the remaining time for display */ +static void _format_time_remaining(int16_t *units, char *buffer, size_t buffer_size) +{ + const int16_t years = units[5]; + const int16_t months = units[4]; + const int16_t days = units[3]; + const int16_t hours = units[2]; + const int16_t minutes = units[1]; + const int16_t seconds = units[0]; + + if (years > 0) { + /* years:months */ + snprintf(buffer, buffer_size, "%02d%02dYR", years % 100, months % 12); + } else if (months > 0) { + /* months:days */ + snprintf(buffer, buffer_size, "%02d%02dMO", (years * 12 + months) % 100, days % 32); + } else if (days > 0) { + /* days:hours */ + snprintf(buffer, buffer_size, "%02d%02ddY", days % 32, hours % 24); + } else { + /* hours:minutes:seconds */ + snprintf(buffer, buffer_size, "%02d%02d%02d", hours % 24, minutes % 60, seconds % 60); + } +} + +/* Correct the naive time difference calculation */ +static void _correct_time_difference(int16_t *units, watch_date_time_t deadline) +{ + const uint8_t range[] = { 60, 60, 24, 30, 12, 0 }; + + for (uint8_t i = 0; i < 6; i++) { + if (units[i] < 0) { + /* Correct remaining units */ + if (i == 3) { + units[i] += _days_in_month(deadline.unit.month - 1, deadline.unit.year); + } else { + units[i] += range[i]; + } + + /* Carry over change to next unit if non-zero */ + if (i < 5 && units[i + 1] != 0) { + units[i + 1] -= 1; + } + } + } +} + +/* Increment date in settings mode. Function taken from `set_time_face.c` */ +static void _increment_date(deadline_state_t *state, watch_date_time_t date_time) +{ + const uint8_t days_in_month[12] = { 31, 28, 31, 30, 31, 30, 30, 31, 30, 31, 30, 31 }; + + switch (state->current_page) { + case 0: + /* Only 10 years covered. Fix this sometime next decade */ + date_time.unit.year = ((date_time.unit.year % 10) + 1); + break; + case 1: + date_time.unit.month = (date_time.unit.month % 12) + 1; + break; + case 2: + date_time.unit.day = date_time.unit.day + 1; + + /* Check for leap years */ + int8_t days = days_in_month[date_time.unit.month - 1]; + if (date_time.unit.month == 2 && _is_leap(date_time.unit.year)) + days++; + + if (date_time.unit.day > days) + date_time.unit.day = 1; + break; + case 3: + date_time.unit.hour = (date_time.unit.hour + 1) % 24; + break; + case 4: + date_time.unit.minute = (date_time.unit.minute + 1) % 60; + break; + } + + uint32_t ts = watch_utility_date_time_to_unix_time(date_time, 0); + state->deadlines[state->current_index] = ts; +} + +/* Update display in running mode */ +static void _deadline_running_display(movement_event_t event, deadline_state_t *state) +{ + (void) event; + + /* Seconds, minutes, hours, days, months, years */ + int16_t units[] = { 0, 0, 0, 0, 0, 0 }; + char buf[16]; + + /* Top row with face name and deadline index */ + watch_display_text_with_fallback(WATCH_POSITION_TOP_LEFT, running_title, running_fallback_title); + sprintf(buf, "%2d", state->current_index + 1); + watch_display_text_with_fallback(WATCH_POSITION_TOP_RIGHT, buf, buf); + + /* Display indicators */ + if (state->alarm_enabled) + watch_set_indicator(WATCH_INDICATOR_BELL); + else + watch_clear_indicator(WATCH_INDICATOR_BELL); + + watch_date_time_t now = movement_get_local_date_time(); + uint32_t now_ts = watch_utility_date_time_to_unix_time(now, 0); + + /* Deadline expired */ + if (state->deadlines[state->current_index] < now_ts) { + if (state->deadlines[state->current_index] + 24 * 60 * 60 > now_ts) + sprintf(buf, "OVER "); + else + sprintf(buf, "---- "); + + watch_display_text_with_fallback(WATCH_POSITION_BOTTOM, buf, buf); + return; + } + + /* Get date time structs */ + uint32_t dl_ts = state->deadlines[state->current_index]; + watch_date_time_t deadline = watch_utility_date_time_from_unix_time(dl_ts, 0); + + /* Calculate and format time remaining */ + _calculate_time_remaining(deadline, now, units); + _correct_time_difference(units, deadline); + _format_time_remaining(units, buf, sizeof(buf)); + watch_display_text_with_fallback(WATCH_POSITION_BOTTOM, buf, buf); +} + +/* Init running mode */ +static void _deadline_running_init(deadline_state_t *state) +{ + (void) state; + + watch_clear_indicator(WATCH_INDICATOR_24H); + watch_clear_indicator(WATCH_INDICATOR_PM); + watch_set_colon(); + + /* Ensure 1Hz updates only */ + _change_tick_freq(1, state); +} + +/* Loop of running mode */ +static bool _deadline_running_loop(movement_event_t event, void *context) +{ + deadline_state_t *state = (deadline_state_t *) context; + + if (event.event_type != EVENT_BACKGROUND_TASK) + _deadline_running_display(event, state); + + switch (event.event_type) { + case EVENT_ALARM_BUTTON_UP: + _beep(BEEP_BUTTON); + state->current_index = (state->current_index + 1) % DEADLINE_FACE_DATES; + _deadline_running_display(event, state); + break; + case EVENT_ALARM_LONG_PRESS: + _beep(BEEP_ENABLE); + _deadline_settings_init(state); + state->mode = DEADLINE_SETTINGS; + break; + case EVENT_MODE_BUTTON_UP: + movement_move_to_next_face(); + return false; + case EVENT_LIGHT_BUTTON_DOWN: + break; + case EVENT_LIGHT_LONG_PRESS: + _beep(BEEP_BUTTON); + state->alarm_enabled = !state->alarm_enabled; + _deadline_running_display(event, state); + break; + case EVENT_TIMEOUT: + movement_move_to_face(0); + break; + case EVENT_BACKGROUND_TASK: + _background_alarm_play(state); + break; + case EVENT_LOW_ENERGY_UPDATE: + break; + default: + return movement_default_loop_handler(event); + } + + return true; +} + +/* Update display in settings mode */ +static void _deadline_settings_display(movement_event_t event, + deadline_state_t *state, watch_date_time_t date_time) +{ + char buf[7]; + + watch_display_text_with_fallback(WATCH_POSITION_TOP, settings_titles[state->current_page], + settings_fallback_titles[state->current_page]); + + if (state->current_page > 2) { + /* Time settings */ + watch_set_colon(); + if (movement_clock_mode_24h() == MOVEMENT_CLOCK_MODE_24H) { + /* 24h format */ + watch_set_indicator(WATCH_INDICATOR_24H); + sprintf(buf, "%2d%02d ", date_time.unit.hour, date_time.unit.minute); + } else { + /* 12h format */ + if (date_time.unit.hour < 12) + watch_clear_indicator(WATCH_INDICATOR_PM); + else + watch_set_indicator(WATCH_INDICATOR_PM); + uint8_t hour = date_time.unit.hour % 12; + sprintf(buf, "%2d%02d ", hour ? hour : 12, date_time.unit.minute); + } + } else { + /* Date settings */ + watch_clear_colon(); + watch_clear_indicator(WATCH_INDICATOR_24H); + watch_clear_indicator(WATCH_INDICATOR_PM); + sprintf(buf, "%2d%02d%02d", + date_time.unit.year + 20, date_time.unit.month, date_time.unit.day); + } + + /* Blink up the parameter we are setting */ + if (event.subsecond % 2) { + switch (state->current_page) { + case 0: + case 3: + buf[0] = buf[1] = ' '; + break; + case 1: + case 4: + buf[2] = buf[3] = ' '; + break; + case 2: + buf[4] = buf[5] = ' '; + break; + } + } + + watch_display_text_with_fallback(WATCH_POSITION_BOTTOM, buf, buf); +} + +/* Init setting mode */ +static void _deadline_settings_init(deadline_state_t *state) +{ + state->current_page = 0; + + /* Init fresh deadline to next day */ + if (state->deadlines[state->current_index] == 0) { + _reset_deadline(state); + } + + /* Ensure 1Hz updates only */ + _change_tick_freq(1, state); +} + +/* Loop of setting mode */ +static bool _deadline_settings_loop(movement_event_t event, void *context) +{ + deadline_state_t *state = (deadline_state_t *) context; + watch_date_time_t date_time; + date_time = watch_utility_date_time_from_unix_time(state->deadlines[state->current_index], 0); + + if (event.event_type != EVENT_BACKGROUND_TASK) + _deadline_settings_display(event, state, date_time); + + switch (event.event_type) { + case EVENT_TICK: + if (state->tick_freq == 8) { + if (HAL_GPIO_BTN_ALARM_read()) { + _increment_date(state, date_time); + _deadline_settings_display(event, state, date_time); + } else { + _change_tick_freq(4, state); + } + } + break; + case EVENT_ALARM_LONG_PRESS: + _change_tick_freq(8, state); + break; + case EVENT_ALARM_LONG_UP: + _change_tick_freq(4, state); + break; + case EVENT_LIGHT_LONG_PRESS: + _beep(BEEP_BUTTON); + _reset_deadline(state); + break; + case EVENT_LIGHT_BUTTON_DOWN: + break; + case EVENT_LIGHT_BUTTON_UP: + state->current_page = (state->current_page + 1) % SETTINGS_NUM; + _deadline_settings_display(event, state, date_time); + break; + case EVENT_ALARM_BUTTON_UP: + _change_tick_freq(4, state); + _increment_date(state, date_time); + _deadline_settings_display(event, state, date_time); + break; + case EVENT_TIMEOUT: + _beep(BEEP_BUTTON); + _change_tick_freq(1, state); + state->mode = DEADLINE_RUNNING; + movement_move_to_face(0); + break; + case EVENT_MODE_BUTTON_UP: + _beep(BEEP_DISABLE); + _deadline_running_init(state); + _deadline_running_display(event, state); + state->mode = DEADLINE_RUNNING; + break; + case EVENT_BACKGROUND_TASK: + _background_alarm_play(state); + break; + default: + return movement_default_loop_handler(event); + } + + return true; +} + +/* Setup face */ +void deadline_face_setup(uint8_t watch_face_index, void **context_ptr) +{ + (void) watch_face_index; + if (*context_ptr != NULL) + return; /* Skip setup if context available */ + + /* Allocate state */ + *context_ptr = malloc(sizeof(deadline_state_t)); + memset(*context_ptr, 0, sizeof(deadline_state_t)); + + /* Store face index for background tasks */ + deadline_state_t *state = (deadline_state_t *) * context_ptr; + state->face_idx = watch_face_index; +} + +/* Activate face */ +void deadline_face_activate(void *context) +{ + deadline_state_t *state = (deadline_state_t *) context; + + /* Set display options */ + _deadline_running_init(state); + state->mode = DEADLINE_RUNNING; + state->current_index = _closest_deadline(state); +} + +/* Loop face */ +bool deadline_face_loop(movement_event_t event, void *context) +{ + deadline_state_t *state = (deadline_state_t *) context; + switch (state->mode) { + case DEADLINE_SETTINGS: + _deadline_settings_loop(event, context); + break; + default: + case DEADLINE_RUNNING: + _deadline_running_loop(event, context); + break; + } + + return true; +} + +/* Resign face */ +void deadline_face_resign(void *context) +{ + (void) context; +} + + +/* Background task */ +movement_watch_face_advisory_t deadline_face_advise(void *context) +{ + deadline_state_t *state = (deadline_state_t *) context; + movement_watch_face_advisory_t retval = { 0 }; + + if (!state->alarm_enabled) + return retval; + + /* Determine closest deadline */ + watch_date_time_t now = movement_get_local_date_time(); + uint32_t now_ts = watch_utility_date_time_to_unix_time(now, 0); + uint32_t next_ts = state->deadlines[_closest_deadline(state)]; + + /* No active deadline */ + if (next_ts < now_ts) + return retval; + + /* No deadline within next 60 seconds */ + if (next_ts >= now_ts + 60) + return retval; + + /* Deadline within next minute. Let's set up an alarm */ + retval.wants_background_task = true; + return retval; +} diff --git a/watch-faces/complication/deadline_face.h b/watch-faces/complication/deadline_face.h new file mode 100644 index 00000000..98a50d2d --- /dev/null +++ b/watch-faces/complication/deadline_face.h @@ -0,0 +1,65 @@ +/* + * MIT License + * + * Copyright (c) 2023-2025 Konrad Rieck + * + * 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 DEADLINE_FACE_H_ +#define DEADLINE_FACE_H_ + +#include "movement.h" + +/* Modes of face */ +typedef enum { + DEADLINE_RUNNING = 0, + DEADLINE_SETTINGS +} deadline_mode_t; + +/* Number of deadline dates */ +#define DEADLINE_FACE_DATES (4) + +/* Deadline configuration */ +typedef struct { + deadline_mode_t mode:1; + uint8_t current_page:3; + uint8_t current_index:2; + uint8_t alarm_enabled:1; + uint8_t tick_freq; + uint8_t face_idx; + uint32_t deadlines[DEADLINE_FACE_DATES]; +} deadline_state_t; + +void deadline_face_setup(uint8_t watch_face_index, void **context_ptr); +void deadline_face_activate(void *context); +bool deadline_face_loop(movement_event_t event, void *context); +void deadline_face_resign(void *context); +movement_watch_face_advisory_t deadline_face_advise(void *context); + +#define deadline_face ((const watch_face_t){ \ + deadline_face_setup, \ + deadline_face_activate, \ + deadline_face_loop, \ + deadline_face_resign, \ + deadline_face_advise, \ +}) + +#endif // DEADLINE_FACE_H_ From bc0207225006b5f4931d8c7cb283963d8d9bc15c Mon Sep 17 00:00:00 2001 From: Daniel Bergman Date: Thu, 31 Jul 2025 22:34:11 +0200 Subject: [PATCH 07/92] Blank day field on reset --- watch-faces/complication/stopwatch_face.c | 1 + 1 file changed, 1 insertion(+) diff --git a/watch-faces/complication/stopwatch_face.c b/watch-faces/complication/stopwatch_face.c index e7e5ebb1..55186517 100644 --- a/watch-faces/complication/stopwatch_face.c +++ b/watch-faces/complication/stopwatch_face.c @@ -114,6 +114,7 @@ bool stopwatch_face_loop(movement_event_t event, void *context) { stopwatch_state->start_time.reg = 0; stopwatch_state->seconds_counted = 0; watch_display_text(WATCH_POSITION_BOTTOM, "000000"); + watch_display_text(WATCH_POSITION_TOP_RIGHT, " "); } break; case EVENT_ALARM_BUTTON_DOWN: From 5e0cc986596ee7c2fc398003705689b9ec206f50 Mon Sep 17 00:00:00 2001 From: kbc-yam <66929688+kbc-yam@users.noreply.github.com> Date: Mon, 4 Aug 2025 02:09:19 +0900 Subject: [PATCH 08/92] Add Japanese Era (Wareki) Display Feature (for Custom LCD) (#57) --- movement_faces.h | 1 + watch-faces.mk | 1 + .../complication/wareki_face.c | 115 +++++++++++++++--- .../complication/wareki_face.h | 5 +- 4 files changed, 100 insertions(+), 22 deletions(-) rename {legacy/watch_faces => watch-faces}/complication/wareki_face.c (58%) rename {legacy/watch_faces => watch-faces}/complication/wareki_face.h (93%) diff --git a/movement_faces.h b/movement_faces.h index 9d3083c5..e8fb0fe6 100644 --- a/movement_faces.h +++ b/movement_faces.h @@ -62,6 +62,7 @@ #include "tally_face.h" #include "probability_face.h" #include "ke_decimal_time_face.h" +#include "wareki_face.h" #include "deadline_face.h" #include "wordle_face.h" // New includes go above this line. diff --git a/watch-faces.mk b/watch-faces.mk index 767e1839..0c78fb47 100644 --- a/watch-faces.mk +++ b/watch-faces.mk @@ -38,5 +38,6 @@ SRCS += \ ./watch-faces/complication/kitchen_conversions_face.c \ ./watch-faces/complication/periodic_table_face.c \ ./watch-faces/clock/ke_decimal_time_face.c \ + ./watch-faces/complication/wareki_face.c \ ./watch-faces/complication/deadline_face.c \ # New watch faces go above this line. diff --git a/legacy/watch_faces/complication/wareki_face.c b/watch-faces/complication/wareki_face.c similarity index 58% rename from legacy/watch_faces/complication/wareki_face.c rename to watch-faces/complication/wareki_face.c index ec87659c..103a503d 100644 --- a/legacy/watch_faces/complication/wareki_face.c +++ b/watch-faces/complication/wareki_face.c @@ -1,9 +1,24 @@ +/* + +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 #include #include "wareki_face.h" #include "filesystem.h" #include "watch_utility.h" - +#include "watch.h" //Long press status flag static bool _alarm_button_press; @@ -14,41 +29,102 @@ void wareki_setup(uint8_t watch_face_index, void ** context_ptr) { (void) watch_face_index; //printf("wareki_setup() \n"); + if (*context_ptr == NULL) { *context_ptr = malloc(sizeof(wareki_state_t)); memset(*context_ptr, 0, sizeof(wareki_state_t)); - // Do any one-time tasks in here; the inside of this conditional happens only at boot. - } + //debug code + // watch_date_time datetime = watch_rtc_get_date_time(); + // datetime.unit.year = 2022 - WATCH_RTC_REFERENCE_YEAR; + // datetime.unit.month = 12; + // datetime.unit.day = 31; + // datetime.unit.hour = 23; + // datetime.unit.minute= 59; + // datetime.unit.second= 30; + // watch_rtc_set_date_time(datetime); + // settings->bit.clock_mode_24h = true; //24時間表記 + // settings->bit.to_interval = 1;//0=60sec 1=2m 2=5m 3=30m + // watch_store_backup_data(settings->reg, 0); + } } // splash view static void draw_wareki_splash(wareki_state_t *state) { (void) state; - char buf[11]; watch_clear_colon(); - sprintf(buf, "%s","wa ------"); - - watch_display_string(buf, 0); + watch_display_text_with_fallback(WATCH_POSITION_TOP_LEFT, "WA ", "wa"); + watch_display_text(WATCH_POSITION_TOP_RIGHT, " "); + watch_display_text(WATCH_POSITION_BOTTOM, " "); } //draw year and Japanese wareki static void draw_year_and_wareki(wareki_state_t *state) { - char buf[27]; + char buf[16]; - if(state->disp_year < REIWA_GANNEN){ + bool is_custom_lcd = watch_get_lcd_type() == WATCH_LCD_TYPE_CUSTOM; + + if(state->disp_year == REIWA_GANNEN){ + //The first year of Reiwa (2019) began on May 1. The period before May 1 is Heisei 31. + //In other words, 2019 is Heisei Year 31 and it is possible that it is Reiwa Year 1. + watch_display_text_with_fallback(WATCH_POSITION_TOP_LEFT, "H31", " r"); + + if (is_custom_lcd) + { + //For custom LCDs, display both Heisei and Reiwa. + watch_display_text(WATCH_POSITION_TOP_RIGHT, "r1"); + } + else{ + watch_display_text(WATCH_POSITION_TOP_RIGHT, " 1"); + } + } + else if(state->disp_year == HEISEI_GANNEN){ + //The year 1989 could be Showa 64 or it could be Heisei 1. + watch_display_text_with_fallback(WATCH_POSITION_TOP_LEFT, "S64", " h"); + + if (is_custom_lcd) + { + //For custom LCDs, display both Showa and Heisei. + watch_display_text(WATCH_POSITION_TOP_RIGHT, "h1"); + } + else{ + watch_display_text(WATCH_POSITION_TOP_RIGHT, " 1"); + } + } + else if(state->disp_year < HEISEI_GANNEN){ + //Showa + //sprintf(buf, " h%2d%4d ", (int)state->disp_year - HEISEI_GANNEN + 1, (int)state->disp_year); + + watch_display_text_with_fallback(WATCH_POSITION_TOP_LEFT, "Sho", "s "); + + sprintf(buf, "%2d", (int)state->disp_year - SHOWA_GANNEN + 1); + watch_display_text(WATCH_POSITION_TOP_RIGHT, buf); + } + else if(state->disp_year < REIWA_GANNEN){ //Heisei - sprintf(buf, " h%2d%4d ", (int)state->disp_year - HEISEI_GANNEN + 1, (int)state->disp_year); + //sprintf(buf, " h%2d%4d ", (int)state->disp_year - HEISEI_GANNEN + 1, (int)state->disp_year); + + watch_display_text_with_fallback(WATCH_POSITION_TOP_LEFT, "HEI", "h "); + + sprintf(buf, "%2d", (int)state->disp_year - HEISEI_GANNEN + 1); + watch_display_text(WATCH_POSITION_TOP_RIGHT, buf); } else{ //Reiwa - sprintf(buf, " r%2d%4d ", (int)state->disp_year - REIWA_GANNEN + 1 , (int)state->disp_year); + //sprintf(buf, " r%2d%4d ", (int)state->disp_year - REIWA_GANNEN + 1 , (int)state->disp_year); + + watch_display_text_with_fallback(WATCH_POSITION_TOP_LEFT, "REI", "r "); + + sprintf(buf, "%2d", (int)state->disp_year - REIWA_GANNEN + 1); + watch_display_text(WATCH_POSITION_TOP_RIGHT, buf); } - watch_display_string(buf, 0); + + sprintf(buf, "%4d ",(int)state->disp_year); + watch_display_text(WATCH_POSITION_BOTTOM, buf); } @@ -81,7 +157,6 @@ void addYear(wareki_state_t* state,int count){ state->disp_year = REIWA_LIMIT; } else{ - //watch_buzzer_play_note(BUZZER_NOTE_C8, 30); } } @@ -90,11 +165,10 @@ void subYear(wareki_state_t* state,int count){ state->disp_year = state->disp_year - count; - if(state->disp_year < 1989 ){ - state->disp_year = 1989; + if(state->disp_year < SHOWA_GANNEN ){ + state->disp_year = SHOWA_GANNEN; } else{ - //watch_buzzer_play_note(BUZZER_NOTE_C7, 30); } } @@ -123,7 +197,7 @@ bool wareki_loop(movement_event_t event, void *context) { //printf("tick %d\n",state->disp_year ); - if (_alarm_button_press && HAL_GPIO_BTN_ALARM_read()){ + if (_alarm_button_press && HAL_GPIO_BTN_ALARM_pin() ){ //printf("ALARM ON\n"); } else{ @@ -131,7 +205,8 @@ bool wareki_loop(movement_event_t event, void *context) { _alarm_button_press = false; } - if (_light_button_press && HAL_GPIO_BTN_LIGHT_read()){ + + if (_light_button_press && HAL_GPIO_BTN_LIGHT_pin()){ //printf("LIGHT ON\n"); } else{ @@ -192,13 +267,12 @@ bool wareki_loop(movement_event_t event, void *context) { //printf("time out ! \n"); movement_move_to_face(0); - break; //case EVENT_LOW_ENERGY_UPDATE: // If you did not resign in EVENT_TIMEOUT, you can use this event to update the display once a minute. // Avoid displaying fast-updating values like seconds, since the display won't update again for 60 seconds. // You should also consider starting the tick animation, to show the wearer that this is sleep mode: - // watch_start_sleep_animation(500); + // watch_start_tick_animation(500); //break; default: // Movement's default loop handler will step in for any cases you don't handle above: @@ -206,6 +280,7 @@ bool wareki_loop(movement_event_t event, void *context) { // * EVENT_MODE_BUTTON_UP moves to the next watch face in the list // * EVENT_MODE_LONG_PRESS returns to the first watch face (or skips to the secondary watch face, if configured) // You can override any of these behaviors by adding a case for these events to this switch statement. + //return movement_default_loop_handler(event, settings); return movement_default_loop_handler(event); } diff --git a/legacy/watch_faces/complication/wareki_face.h b/watch-faces/complication/wareki_face.h similarity index 93% rename from legacy/watch_faces/complication/wareki_face.h rename to watch-faces/complication/wareki_face.h index 4dffc46d..070642e8 100644 --- a/legacy/watch_faces/complication/wareki_face.h +++ b/watch-faces/complication/wareki_face.h @@ -3,9 +3,10 @@ #include "movement.h" -#define REIWA_LIMIT 2018 + 31 +#define REIWA_LIMIT 2018 + 99 #define REIWA_GANNEN 2019 #define HEISEI_GANNEN 1989 +#define SHOWA_GANNEN 1926 typedef struct { bool active; @@ -14,11 +15,11 @@ typedef struct { uint32_t real_year; //The actual current year } wareki_state_t; - void wareki_setup(uint8_t watch_face_index, void ** context_ptr); void wareki_activate(void *context); bool wareki_loop(movement_event_t event, void *context); void wareki_resign(void *context); + void addYear(wareki_state_t* state,int count); void subYear(wareki_state_t* state,int count); From 0eb96a637c95e12f32587c4ecba0e6e94e85d8aa Mon Sep 17 00:00:00 2001 From: James Haggerty Date: Tue, 22 Jul 2025 20:32:38 +1000 Subject: [PATCH 09/92] Move totp_lfs face from legacy Changes: - use 3 characters if possible for identifier - rename from totp_face_lfs to totp_lfs_face for consistency --- Makefile | 2 + movement_faces.h | 1 + watch-faces.mk | 2 +- .../complication/totp_lfs_face.c | 49 ++++++++++--------- .../complication/totp_lfs_face.h | 18 +++---- 5 files changed, 39 insertions(+), 33 deletions(-) rename legacy/watch_faces/complication/totp_face_lfs.c => watch-faces/complication/totp_lfs_face.c (87%) rename legacy/watch_faces/complication/totp_face_lfs.h => watch-faces/complication/totp_lfs_face.h (88%) diff --git a/Makefile b/Makefile index 5d1fbd2f..cae2b943 100644 --- a/Makefile +++ b/Makefile @@ -20,6 +20,8 @@ TINYUSB_CDC=1 # Now we're all set to include gossamer's make rules. include $(GOSSAMER_PATH)/make.mk +CFLAGS+=-D_POSIX_C_SOURCE=200112L + define n diff --git a/movement_faces.h b/movement_faces.h index e8fb0fe6..d0580fe4 100644 --- a/movement_faces.h +++ b/movement_faces.h @@ -59,6 +59,7 @@ #include "periodic_table_face.h" #include "squash_face.h" #include "totp_face.h" +#include "totp_lfs_face.h" #include "tally_face.h" #include "probability_face.h" #include "ke_decimal_time_face.h" diff --git a/watch-faces.mk b/watch-faces.mk index 0c78fb47..dbd4ebd3 100644 --- a/watch-faces.mk +++ b/watch-faces.mk @@ -14,7 +14,7 @@ SRCS += \ ./watch-faces/complication/days_since_face.c \ ./watch-faces/complication/breathing_face.c \ ./watch-faces/complication/squash_face.c \ - ./watch-faces/complication/totp_face.c \ + ./watch-faces/complication/totp_lfs_face.c \ ./watch-faces/complication/tally_face.c \ ./watch-faces/complication/wordle_face.c \ ./watch-faces/demo/all_segments_face.c \ diff --git a/legacy/watch_faces/complication/totp_face_lfs.c b/watch-faces/complication/totp_lfs_face.c similarity index 87% rename from legacy/watch_faces/complication/totp_face_lfs.c rename to watch-faces/complication/totp_lfs_face.c index 439aca3f..8be3e28d 100644 --- a/legacy/watch_faces/complication/totp_face_lfs.c +++ b/watch-faces/complication/totp_lfs_face.c @@ -33,7 +33,7 @@ #include "watch_utility.h" #include "filesystem.h" -#include "totp_face_lfs.h" +#include "totp_lfs_face.h" #define MAX_TOTP_RECORDS 30 #define MAX_TOTP_SECRET_SIZE 128 @@ -42,7 +42,7 @@ const char* TOTP_URI_START = "otpauth://totp/"; struct totp_record { - char label[2]; + char label[4]; hmac_alg algorithm; uint8_t period; uint8_t secret_size; @@ -67,19 +67,20 @@ static uint8_t num_totp_records = 0; static void init_totp_record(struct totp_record *totp_record) { totp_record->label[0] = 'A'; totp_record->label[1] = 'A'; + totp_record->label[2] = 'A'; + totp_record->label[3] = 0; totp_record->algorithm = SHA1; totp_record->period = 30; totp_record->secret_size = 0; } -static bool totp_face_lfs_read_param(struct totp_record *totp_record, char *param, char *value) { +static bool totp_lfs_face_read_param(struct totp_record *totp_record, char *param, char *value) { if (!strcmp(param, "issuer")) { - if (value[0] == '\0' || value[1] == '\0') { - printf("TOTP issuer must be >= 2 chars, got '%s'\n", value); + if (value[0] == '\0') { + printf("TOTP issuer must be a non-empty string\n"); return false; } - totp_record->label[0] = value[0]; - totp_record->label[1] = value[1]; + snprintf(totp_record->label, sizeof(totp_record->label), "%-3s", value); } else if (!strcmp(param, "secret")) { totp_record->file_secret_length = strlen(value); if (UNBASE32_LEN(totp_record->file_secret_length) > MAX_TOTP_SECRET_SIZE) { @@ -127,7 +128,7 @@ static bool totp_face_lfs_read_param(struct totp_record *totp_record, char *para return true; } -static void totp_face_lfs_read_file(char *filename) { +static void totp_lfs_face_read_file(char *filename) { // For 'format' of file, see comment at top. const size_t uri_start_len = strlen(TOTP_URI_START); @@ -166,7 +167,7 @@ static void totp_face_lfs_read_file(char *filename) { do { char *param_middle = strchr(param, '='); *param_middle = '\0'; - if (totp_face_lfs_read_param(&totp_records[num_totp_records], param, param_middle + 1)) { + if (totp_lfs_face_read_param(&totp_records[num_totp_records], param, param_middle + 1)) { if (!strcmp(param, "secret")) { totp_records[num_totp_records].file_secret_offset = old_offset + (param_middle + 1 - line); } @@ -189,7 +190,7 @@ static void totp_face_lfs_read_file(char *filename) { } } -void totp_face_lfs_setup(uint8_t watch_face_index, void ** context_ptr) { +void totp_lfs_face_setup(uint8_t watch_face_index, void ** context_ptr) { (void) watch_face_index; if (*context_ptr == NULL) { *context_ptr = malloc(sizeof(totp_lfs_state_t)); @@ -197,12 +198,12 @@ void totp_face_lfs_setup(uint8_t watch_face_index, void ** context_ptr) { #if !(__EMSCRIPTEN__) if (num_totp_records == 0) { - totp_face_lfs_read_file(TOTP_FILE); + totp_lfs_face_read_file(TOTP_FILE); } #endif } -static uint8_t *totp_face_lfs_get_file_secret(struct totp_record *record) { +static uint8_t *totp_lfs_face_get_file_secret(struct totp_record *record) { char buffer[BASE32_LEN(MAX_TOTP_SECRET_SIZE) + 1]; int32_t file_secret_offset = record->file_secret_offset; @@ -231,7 +232,7 @@ static void totp_face_set_record(totp_lfs_state_t *totp_state, int i) { record = &totp_records[i]; TOTP( - totp_face_lfs_get_file_secret(record), + totp_lfs_face_get_file_secret(record), record->secret_size, record->period, record->algorithm @@ -240,7 +241,7 @@ static void totp_face_set_record(totp_lfs_state_t *totp_state, int i) { totp_state->steps = totp_state->timestamp / record->period; } -void totp_face_lfs_activate(void *context) { +void totp_lfs_face_activate(void *context) { memset(context, 0, sizeof(totp_lfs_state_t)); totp_lfs_state_t *totp_state = (totp_lfs_state_t *)context; @@ -248,20 +249,20 @@ void totp_face_lfs_activate(void *context) { if (num_totp_records == 0) { // Doing this here rather than in setup makes things a bit more pleasant in the simulator, since there's no easy way to trigger // setup again after uploading the data. - totp_face_lfs_read_file(TOTP_FILE); + totp_lfs_face_read_file(TOTP_FILE); } #endif - totp_state->timestamp = watch_utility_date_time_to_unix_time(watch_rtc_get_date_time(), movement_get_current_timezone_offset()); + totp_state->timestamp = watch_utility_date_time_to_unix_time(movement_get_utc_date_time(), 0); totp_face_set_record(totp_state, 0); } static void totp_face_display(totp_lfs_state_t *totp_state) { uint8_t index = totp_state->current_index; - char buf[14]; + char buf[7]; if (num_totp_records == 0) { - watch_display_string("No2F Codes", 0); + watch_display_text(WATCH_POSITION_FULL, "No2F Codes"); return; } @@ -272,12 +273,14 @@ static void totp_face_display(totp_lfs_state_t *totp_state) { } uint8_t valid_for = totp_records[index].period - result.rem; - sprintf(buf, "%c%c%2d%06lu", totp_records[index].label[0], totp_records[index].label[1], valid_for, totp_state->current_code); - - watch_display_string(buf, 0); + watch_display_text_with_fallback(WATCH_POSITION_TOP_LEFT, totp_records[index].label, totp_records[index].label); + sprintf(buf, "%2d", valid_for); + watch_display_text_with_fallback(WATCH_POSITION_TOP_RIGHT, buf, buf); + sprintf(buf, "%06lu", totp_state->current_code); + watch_display_text_with_fallback(WATCH_POSITION_BOTTOM, buf, buf); } -bool totp_face_lfs_loop(movement_event_t event, void *context) { +bool totp_lfs_face_loop(movement_event_t event, void *context) { totp_lfs_state_t *totp_state = (totp_lfs_state_t *)context; @@ -315,6 +318,6 @@ bool totp_face_lfs_loop(movement_event_t event, void *context) { return true; } -void totp_face_lfs_resign(void *context) { +void totp_lfs_face_resign(void *context) { (void) context; } diff --git a/legacy/watch_faces/complication/totp_face_lfs.h b/watch-faces/complication/totp_lfs_face.h similarity index 88% rename from legacy/watch_faces/complication/totp_face_lfs.h rename to watch-faces/complication/totp_lfs_face.h index 16276a2e..3e18a6a8 100644 --- a/legacy/watch_faces/complication/totp_face_lfs.h +++ b/watch-faces/complication/totp_lfs_face.h @@ -61,16 +61,16 @@ typedef struct { uint8_t current_index; } totp_lfs_state_t; -void totp_face_lfs_setup(uint8_t watch_face_index, void ** context_ptr); -void totp_face_lfs_activate(void *context); -bool totp_face_lfs_loop(movement_event_t event, void *context); -void totp_face_lfs_resign(void *context); +void totp_lfs_face_setup(uint8_t watch_face_index, void ** context_ptr); +void totp_lfs_face_activate(void *context); +bool totp_lfs_face_loop(movement_event_t event, void *context); +void totp_lfs_face_resign(void *context); -#define totp_face_lfs ((const watch_face_t){ \ - totp_face_lfs_setup, \ - totp_face_lfs_activate, \ - totp_face_lfs_loop, \ - totp_face_lfs_resign, \ +#define totp_lfs_face ((const watch_face_t){ \ + totp_lfs_face_setup, \ + totp_lfs_face_activate, \ + totp_lfs_face_loop, \ + totp_lfs_face_resign, \ NULL, \ }) From 22b11de6aeb6afeb2bc0fd49c5be72640965ccc8 Mon Sep 17 00:00:00 2001 From: Konrad Rieck Date: Sun, 3 Aug 2025 19:12:43 +0200 Subject: [PATCH 10/92] Monitor for LIS2DW accelerometer (#61) This watch face displays the current reading of the LIS2DW12 accelerometer. The axis (x,y,z) can be selected using the alarm button. A long press on the light button allows to configure the sensor, including its mode, data rate, low power mode, bandwidth filtering, range, filter type, and low noise mode. The watch face is mainly designed for experimenting with the sensor and configuring it for other developing other watch faces. --- movement_faces.h | 1 + watch-faces.mk | 1 + watch-faces/sensor/lis2dw_monitor_face.c | 612 +++++++++++++++++++++++ watch-faces/sensor/lis2dw_monitor_face.h | 81 +++ 4 files changed, 695 insertions(+) create mode 100644 watch-faces/sensor/lis2dw_monitor_face.c create mode 100644 watch-faces/sensor/lis2dw_monitor_face.h diff --git a/movement_faces.h b/movement_faces.h index d0580fe4..ca270cc3 100644 --- a/movement_faces.h +++ b/movement_faces.h @@ -63,6 +63,7 @@ #include "tally_face.h" #include "probability_face.h" #include "ke_decimal_time_face.h" +#include "lis2dw_monitor_face.h" #include "wareki_face.h" #include "deadline_face.h" #include "wordle_face.h" diff --git a/watch-faces.mk b/watch-faces.mk index dbd4ebd3..91a0d9e9 100644 --- a/watch-faces.mk +++ b/watch-faces.mk @@ -38,6 +38,7 @@ SRCS += \ ./watch-faces/complication/kitchen_conversions_face.c \ ./watch-faces/complication/periodic_table_face.c \ ./watch-faces/clock/ke_decimal_time_face.c \ + ./watch-faces/sensor/lis2dw_monitor_face.c \ ./watch-faces/complication/wareki_face.c \ ./watch-faces/complication/deadline_face.c \ # New watch faces go above this line. diff --git a/watch-faces/sensor/lis2dw_monitor_face.c b/watch-faces/sensor/lis2dw_monitor_face.c new file mode 100644 index 00000000..a717fe46 --- /dev/null +++ b/watch-faces/sensor/lis2dw_monitor_face.c @@ -0,0 +1,612 @@ +/* + * MIT License + * + * Copyright (c) 2025 Konrad Rieck + * + * 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 "lis2dw_monitor_face.h" +#include "watch.h" +#include "watch_utility.h" + +/* Display frequency */ +#define DISPLAY_FREQUENCY 8 + +/* Settings */ +#define NUM_SETTINGS 7 + +static void _settings_title_display(lis2dw_monitor_state_t *state, char *buf1, char *buf2) +{ + char buf[10]; + watch_display_text_with_fallback(WATCH_POSITION_TOP, buf1, buf2); + if (watch_get_lcd_type() != WATCH_LCD_TYPE_CUSTOM) { + snprintf(buf, sizeof(buf), "%2d", state->settings_page + 1); + watch_display_text_with_fallback(WATCH_POSITION_TOP_RIGHT, buf, buf); + } +} + +static bool _settings_blink(uint8_t subsecond) +{ + if (subsecond % 2 == 0) { + watch_display_text_with_fallback(WATCH_POSITION_BOTTOM, " ", " "); + return true; + } + return false; +} + +static void _settings_mode_display(void *context, uint8_t subsecond) +{ + char buf[10]; + lis2dw_monitor_state_t *state = (lis2dw_monitor_state_t *) context; + + _settings_title_display(state, "MODE ", "MO"); + if (_settings_blink(subsecond)) + return; + + switch (state->ds.mode) { + case LIS2DW_MODE_LOW_POWER: + snprintf(buf, sizeof(buf), " LO "); + break; + case LIS2DW_MODE_HIGH_PERFORMANCE: + snprintf(buf, sizeof(buf), " HI "); + break; + case LIS2DW_MODE_ON_DEMAND: + snprintf(buf, sizeof(buf), " OD "); + break; + } + watch_display_text_with_fallback(WATCH_POSITION_BOTTOM, buf, buf); +} + +static void _settings_mode_advance(void *context) +{ + lis2dw_monitor_state_t *state = (lis2dw_monitor_state_t *) context; + + switch (state->ds.mode) { + case LIS2DW_MODE_LOW_POWER: + state->ds.mode = LIS2DW_MODE_HIGH_PERFORMANCE; + break; + case LIS2DW_MODE_HIGH_PERFORMANCE: + state->ds.mode = LIS2DW_MODE_ON_DEMAND; + break; + case LIS2DW_MODE_ON_DEMAND: + state->ds.mode = LIS2DW_MODE_LOW_POWER; + break; + } +} + +static void _settings_data_rate_display(void *context, uint8_t subsecond) +{ + char buf[10]; + lis2dw_monitor_state_t *state = (lis2dw_monitor_state_t *) context; + + _settings_title_display(state, "RATE ", "DR"); + if (_settings_blink(subsecond)) + return; + + switch (state->ds.data_rate) { + case LIS2DW_DATA_RATE_POWERDOWN: + snprintf(buf, sizeof(buf), " -- "); + break; + case LIS2DW_DATA_RATE_LOWEST: + snprintf(buf, sizeof(buf), " LO "); + break; + case LIS2DW_DATA_RATE_12_5_HZ: + snprintf(buf, sizeof(buf), " 12Hz"); + break; + case LIS2DW_DATA_RATE_25_HZ: + snprintf(buf, sizeof(buf), " 25Hz"); + break; + default: + snprintf(buf, sizeof(buf), " HI "); + break; + } + watch_display_text_with_fallback(WATCH_POSITION_BOTTOM, buf, buf); +} + +static void _settings_data_rate_advance(void *context) +{ + lis2dw_monitor_state_t *state = (lis2dw_monitor_state_t *) context; + + switch (state->ds.data_rate) { + case LIS2DW_DATA_RATE_POWERDOWN: + state->ds.data_rate = LIS2DW_DATA_RATE_LOWEST; + break; + case LIS2DW_DATA_RATE_LOWEST: + state->ds.data_rate = LIS2DW_DATA_RATE_12_5_HZ; + break; + case LIS2DW_DATA_RATE_12_5_HZ: + state->ds.data_rate = LIS2DW_DATA_RATE_25_HZ; + break; + case LIS2DW_DATA_RATE_25_HZ: + state->ds.data_rate = LIS2DW_DATA_RATE_POWERDOWN; + break; + default: + state->ds.data_rate = LIS2DW_DATA_RATE_POWERDOWN; + break; + } +} + +static void _settings_low_power_display(void *context, uint8_t subsecond) +{ + char buf[10]; + lis2dw_monitor_state_t *state = (lis2dw_monitor_state_t *) context; + + _settings_title_display(state, "LO PM", "LP"); + if (_settings_blink(subsecond)) + return; + + switch (state->ds.low_power) { + case LIS2DW_LP_MODE_1: + snprintf(buf, sizeof(buf), " L1 12"); + break; + case LIS2DW_LP_MODE_2: + snprintf(buf, sizeof(buf), " L2 14"); + break; + case LIS2DW_LP_MODE_3: + snprintf(buf, sizeof(buf), " L3 14"); + break; + case LIS2DW_LP_MODE_4: + snprintf(buf, sizeof(buf), " L4 14"); + break; + } + watch_display_text_with_fallback(WATCH_POSITION_BOTTOM, buf, buf); +} + +static void _settings_low_power_advance(void *context) +{ + lis2dw_monitor_state_t *state = (lis2dw_monitor_state_t *) context; + + switch (state->ds.low_power) { + case LIS2DW_LP_MODE_1: + state->ds.low_power = LIS2DW_LP_MODE_2; + break; + case LIS2DW_LP_MODE_2: + state->ds.low_power = LIS2DW_LP_MODE_3; + break; + case LIS2DW_LP_MODE_3: + state->ds.low_power = LIS2DW_LP_MODE_4; + break; + case LIS2DW_LP_MODE_4: + state->ds.low_power = LIS2DW_LP_MODE_1; + break; + } +} + +static void _settings_bwf_mode_display(void *context, uint8_t subsecond) +{ + char buf[10]; + lis2dw_monitor_state_t *state = (lis2dw_monitor_state_t *) context; + + _settings_title_display(state, "BWF ", "BW"); + if (_settings_blink(subsecond)) + return; + + switch (state->ds.bwf_mode) { + case LIS2DW_BANDWIDTH_FILTER_DIV2: + snprintf(buf, sizeof(buf), " 2 "); + break; + case LIS2DW_BANDWIDTH_FILTER_DIV4: + snprintf(buf, sizeof(buf), " 4 "); + break; + case LIS2DW_BANDWIDTH_FILTER_DIV10: + snprintf(buf, sizeof(buf), " 10 "); + break; + case LIS2DW_BANDWIDTH_FILTER_DIV20: + snprintf(buf, sizeof(buf), " 20 "); + break; + } + watch_display_text_with_fallback(WATCH_POSITION_BOTTOM, buf, buf); +} + +static void _settings_bwf_mode_advance(void *context) +{ + lis2dw_monitor_state_t *state = (lis2dw_monitor_state_t *) context; + + switch (state->ds.bwf_mode) { + case LIS2DW_BANDWIDTH_FILTER_DIV2: + state->ds.bwf_mode = LIS2DW_BANDWIDTH_FILTER_DIV4; + break; + case LIS2DW_BANDWIDTH_FILTER_DIV4: + state->ds.bwf_mode = LIS2DW_BANDWIDTH_FILTER_DIV10; + break; + case LIS2DW_BANDWIDTH_FILTER_DIV10: + state->ds.bwf_mode = LIS2DW_BANDWIDTH_FILTER_DIV20; + break; + case LIS2DW_BANDWIDTH_FILTER_DIV20: + state->ds.bwf_mode = LIS2DW_BANDWIDTH_FILTER_DIV2; + break; + } +} + +static void _settings_range_display(void *context, uint8_t subsecond) +{ + char buf[10]; + lis2dw_monitor_state_t *state = (lis2dw_monitor_state_t *) context; + + _settings_title_display(state, "RANGE", "RA"); + if (_settings_blink(subsecond)) + return; + + switch (state->ds.range) { + case LIS2DW_RANGE_2_G: + snprintf(buf, sizeof(buf), " 2g "); + break; + case LIS2DW_RANGE_4_G: + snprintf(buf, sizeof(buf), " 4g "); + break; + case LIS2DW_RANGE_8_G: + snprintf(buf, sizeof(buf), " 8g "); + break; + case LIS2DW_RANGE_16_G: + snprintf(buf, sizeof(buf), " 16g "); + break; + } + watch_display_text_with_fallback(WATCH_POSITION_BOTTOM, buf, buf); +} + +static void _settings_range_advance(void *context) +{ + lis2dw_monitor_state_t *state = (lis2dw_monitor_state_t *) context; + + switch (state->ds.range) { + case LIS2DW_RANGE_2_G: + state->ds.range = LIS2DW_RANGE_4_G; + break; + case LIS2DW_RANGE_4_G: + state->ds.range = LIS2DW_RANGE_8_G; + break; + case LIS2DW_RANGE_8_G: + state->ds.range = LIS2DW_RANGE_16_G; + break; + case LIS2DW_RANGE_16_G: + state->ds.range = LIS2DW_RANGE_2_G; + break; + } +} + +static void _settings_filter_display(void *context, uint8_t subsecond) +{ + char buf[10]; + lis2dw_monitor_state_t *state = (lis2dw_monitor_state_t *) context; + + _settings_title_display(state, "FLT ", "FL"); + if (_settings_blink(subsecond)) + return; + + switch (state->ds.filter) { + case LIS2DW_FILTER_LOW_PASS: + snprintf(buf, sizeof(buf), " LP "); + break; + case LIS2DW_FILTER_HIGH_PASS: + snprintf(buf, sizeof(buf), " HP "); + break; + } + watch_display_text_with_fallback(WATCH_POSITION_BOTTOM, buf, buf); +} + +static void _settings_filter_advance(void *context) +{ + lis2dw_monitor_state_t *state = (lis2dw_monitor_state_t *) context; + + switch (state->ds.filter) { + case LIS2DW_FILTER_LOW_PASS: + state->ds.filter = LIS2DW_FILTER_HIGH_PASS; + break; + case LIS2DW_FILTER_HIGH_PASS: + state->ds.filter = LIS2DW_FILTER_LOW_PASS; + break; + } +} + +static void _settings_low_noise_display(void *context, uint8_t subsecond) +{ + char buf[10]; + lis2dw_monitor_state_t *state = (lis2dw_monitor_state_t *) context; + + _settings_title_display(state, "LO NO", "LN"); + if (_settings_blink(subsecond)) + return; + + snprintf(buf, sizeof(buf), " %3s ", state->ds.low_noise ? "ON" : "OFF"); + watch_display_text_with_fallback(WATCH_POSITION_BOTTOM, buf, buf); +} + +static void _settings_low_noise_advance(void *context) +{ + lis2dw_monitor_state_t *state = (lis2dw_monitor_state_t *) context; + state->ds.low_noise = !state->ds.low_noise; +} + +/* Play beep sound */ +static inline void _beep() +{ + if (!movement_button_should_sound()) + return; + watch_buzzer_play_note(BUZZER_NOTE_C7, 50); +} + +/* Print lis2dw status to console. */ +static void _lis2dw_print_state(lis2dw_device_state_t *ds) +{ + printf("LIS2DW status:\n"); + printf(" Power mode:\t%x\n", ds->mode); + printf(" Data rate:\t%x\n", ds->data_rate); + printf(" LP mode:\t%x\n", ds->low_power); + printf(" BW filter:\t%x\n", ds->bwf_mode); + printf(" Range:\t%x \n", ds->range); + printf(" Filter type:\t%x\n", ds->filter); + printf(" Low noise:\t%x\n", ds->low_noise); + printf("\n"); +} + +static void _lis2dw_get_state(lis2dw_device_state_t *ds) +{ + ds->mode = lis2dw_get_mode(); + ds->data_rate = lis2dw_get_data_rate(); + ds->low_power = lis2dw_get_low_power_mode(); + ds->bwf_mode = lis2dw_get_bandwidth_filtering(); + ds->range = lis2dw_get_range(); + ds->filter = lis2dw_get_filter_type(); + ds->low_noise = lis2dw_get_low_noise_mode(); +} + +static void _lis2dw_set_state(lis2dw_device_state_t *ds) +{ + lis2dw_set_mode(ds->mode); + lis2dw_set_data_rate(ds->data_rate); + lis2dw_set_low_power_mode(ds->low_power); + lis2dw_set_bandwidth_filtering(ds->bwf_mode); + lis2dw_set_range(ds->range); + lis2dw_set_filter_type(ds->filter); + lis2dw_set_low_noise_mode(ds->low_noise); + + /* Additionally, set the background rate to the data rate. */ + movement_set_accelerometer_background_rate(ds->data_rate); +} + +static void _monitor_display(lis2dw_monitor_state_t *state) +{ + char buf[10]; + + snprintf(buf, sizeof(buf), " %C ", "XYZ"[state->axis]); + watch_display_text_with_fallback(WATCH_POSITION_TOP_LEFT, buf, buf); + + snprintf(buf, sizeof(buf), "%2d", state->axis + 1); + watch_display_text_with_fallback(WATCH_POSITION_TOP_RIGHT, buf, buf); + + if (state->show_title) { + snprintf(buf, sizeof(buf), "LIS2DW"); + watch_display_text_with_fallback(WATCH_POSITION_BOTTOM, buf, buf); + return; + } + + if (state->ds.data_rate == LIS2DW_DATA_RATE_POWERDOWN) { + /* No measurements available. */ + snprintf(buf, sizeof(buf), " -- "); + } else if (state->axis == 0) { + char sign = (state->reading.x) >= 0 ? ' ' : '-'; + snprintf(buf, sizeof(buf), "%c%.5d", sign, abs(state->reading.x)); + } else if (state->axis == 1) { + char sign = (state->reading.y) >= 0 ? ' ' : '-'; + snprintf(buf, sizeof(buf), "%c%.5d", sign, abs(state->reading.y)); + } else { + char sign = (state->reading.z) >= 0 ? ' ' : '-'; + snprintf(buf, sizeof(buf), "%c%.5d", sign, abs(state->reading.z)); + } + watch_display_text_with_fallback(WATCH_POSITION_BOTTOM, buf, buf); +} + +static void _monitor_update(lis2dw_monitor_state_t *state) +{ + lis2dw_fifo_t fifo; + float x = 0, y = 0, z = 0; + + lis2dw_read_fifo(&fifo); + if (fifo.count == 0) { + return; + } + + /* Add up samples in fifo */ + for (uint8_t i = 0; i < fifo.count; i++) { + x += fifo.readings[i].x; + y += fifo.readings[i].y; + z += fifo.readings[i].z; + } + + /* Divide by number of samples */ + state->reading.x = (int16_t) (x / fifo.count); + state->reading.y = (int16_t) (y / fifo.count); + state->reading.z = (int16_t) (z / fifo.count); + + lis2dw_clear_fifo(); +} + +static void _switch_to_monitor(lis2dw_monitor_state_t *state) +{ + /* Switch to recording page */ + movement_request_tick_frequency(DISPLAY_FREQUENCY); + state->page = PAGE_LIS2DW_MONITOR; + state->show_title = DISPLAY_FREQUENCY; + _monitor_display(state); +} + +static void _switch_to_settings(lis2dw_monitor_state_t *state) +{ + /* Switch to chirping page */ + movement_request_tick_frequency(4); + state->page = PAGE_LIS2DW_SETTINGS; + state->settings_page = 0; + state->settings[state->settings_page].display(state, 0); +} + +static bool _monitor_loop(movement_event_t event, void *context) +{ + lis2dw_monitor_state_t *state = (lis2dw_monitor_state_t *) context; + + switch (event.event_type) { + case EVENT_ACTIVATE: + watch_clear_colon(); + _monitor_update(state); + _monitor_display(state); + break; + case EVENT_TICK: + _monitor_update(state); + _monitor_display(state); + state->show_title = (state->show_title > 0) ? state->show_title - 1 : 0; + break; + case EVENT_ALARM_BUTTON_UP: + state->axis = (state->axis + 1) % 3; + _monitor_display(state); + break; + case EVENT_LIGHT_BUTTON_DOWN: + /* Do nothing. */ + break; + case EVENT_LIGHT_LONG_PRESS: + _switch_to_settings(state); + _beep(); + break; + default: + movement_default_loop_handler(event); + break; + } + + return true; +} + +static bool _settings_loop(movement_event_t event, void *context) +{ + lis2dw_monitor_state_t *state = (lis2dw_monitor_state_t *) context; + + switch (event.event_type) { + case EVENT_ACTIVATE: + case EVENT_TICK: + state->settings[state->settings_page].display(context, event.subsecond); + break; + case EVENT_LIGHT_BUTTON_UP: + state->settings_page = (state->settings_page + 1) % NUM_SETTINGS; + state->settings[state->settings_page].display(context, event.subsecond); + break; + case EVENT_MODE_BUTTON_UP: + _lis2dw_set_state(&state->ds); + _lis2dw_print_state(&state->ds); + _switch_to_monitor(state); + _beep(); + break; + case EVENT_LIGHT_BUTTON_DOWN: + /* Do nothing. */ + break; + case EVENT_ALARM_BUTTON_UP: + /* Advance current settings */ + state->settings[state->settings_page].advance(context); + state->settings[state->settings_page].display(context, event.subsecond); + break; + default: + _lis2dw_set_state(&state->ds); + movement_default_loop_handler(event); + break; + } + return true; +} + +void lis2dw_monitor_face_setup(uint8_t watch_face_index, void **context_ptr) +{ + (void) watch_face_index; + if (*context_ptr == NULL) { + *context_ptr = malloc(sizeof(lis2dw_monitor_state_t)); + memset(*context_ptr, 0, sizeof(lis2dw_monitor_state_t)); + } + lis2dw_monitor_state_t *state = (lis2dw_monitor_state_t *) * context_ptr; + + /* Default setup */ + state->axis = 0; + + /* Initialize settings */ + uint8_t settings_page = 0; + state->settings = malloc(NUM_SETTINGS * sizeof(lis2dw_settings_t)); + state->settings[settings_page].display = _settings_mode_display; + state->settings[settings_page].advance = _settings_mode_advance; + settings_page++; + state->settings[settings_page].display = _settings_data_rate_display; + state->settings[settings_page].advance = _settings_data_rate_advance; + settings_page++; + state->settings[settings_page].display = _settings_low_power_display; + state->settings[settings_page].advance = _settings_low_power_advance; + settings_page++; + state->settings[settings_page].display = _settings_bwf_mode_display; + state->settings[settings_page].advance = _settings_bwf_mode_advance; + settings_page++; + state->settings[settings_page].display = _settings_range_display; + state->settings[settings_page].advance = _settings_range_advance; + settings_page++; + state->settings[settings_page].display = _settings_filter_display; + state->settings[settings_page].advance = _settings_filter_advance; + settings_page++; + state->settings[settings_page].display = _settings_low_noise_display; + state->settings[settings_page].advance = _settings_low_noise_advance; + settings_page++; +} + +void lis2dw_monitor_face_activate(void *context) +{ + lis2dw_monitor_state_t *state = (lis2dw_monitor_state_t *) context; + + /* Setup lis2dw to run in background at 12.5 Hz sampling rate. */ + movement_set_accelerometer_background_rate(LIS2DW_DATA_RATE_12_5_HZ); + + /* Enable fifo and clear it. */ + lis2dw_enable_fifo(); + lis2dw_clear_fifo(); + + /* Print lis2dw status to console. */ + _lis2dw_get_state(&state->ds); + _lis2dw_print_state(&state->ds); + + /* Switch to monitor page. */ + _switch_to_monitor(state); +} + +bool lis2dw_monitor_face_loop(movement_event_t event, void *context) +{ + lis2dw_monitor_state_t *state = (lis2dw_monitor_state_t *) context; + + switch (state->page) { + default: + case PAGE_LIS2DW_MONITOR: + return _monitor_loop(event, context); + case PAGE_LIS2DW_SETTINGS: + return _settings_loop(event, context); + } +} + +void lis2dw_monitor_face_resign(void *context) +{ + (void) context; + lis2dw_clear_fifo(); + lis2dw_disable_fifo(); +} + +movement_watch_face_advisory_t lis2dw_monitor_face_advise(void *context) +{ + (void) context; + movement_watch_face_advisory_t retval = { 0 }; + return retval; +} diff --git a/watch-faces/sensor/lis2dw_monitor_face.h b/watch-faces/sensor/lis2dw_monitor_face.h new file mode 100644 index 00000000..51a98ecf --- /dev/null +++ b/watch-faces/sensor/lis2dw_monitor_face.h @@ -0,0 +1,81 @@ +/* + * MIT License + * + * Copyright (c) 2025 Konrad Rieck + * + * 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. + */ + +/* + * This watch face displays the current reading of the LIS2DW12 accelerometer. + * The axis (x,y,z) can be selected using the alarm button. + * + * A long press on the light button allows to configure the sensor, including + * its mode, data rate, low power mode, bandwidth filtering, range, filter type, + * and low noise mode. + * + * The watch face is mainly designed for experimenting with the sensor and + * configuring it for other developing other watch faces. + */ + +#include "movement.h" + +typedef enum { + PAGE_LIS2DW_MONITOR, + PAGE_LIS2DW_SETTINGS, +} lis2dw_monitor_page_t; + +typedef struct { + lis2dw_mode_t mode; + lis2dw_data_rate_t data_rate; + lis2dw_low_power_mode_t low_power; + lis2dw_bandwidth_filtering_mode_t bwf_mode; + lis2dw_range_t range; + lis2dw_filter_t filter; + bool low_noise; +} lis2dw_device_state_t; + +typedef struct { + void (*display)(void *, uint8_t); + void (*advance)(void *); +} lis2dw_settings_t; + +typedef struct { + uint8_t axis:2; /* Axis to display */ + lis2dw_reading_t reading; /* Current reading */ + lis2dw_monitor_page_t page; /* Displayed page */ + lis2dw_device_state_t ds; /* Device state */ + uint8_t settings_page:3; /* Subpage in settings */ + lis2dw_settings_t *settings; /* Settings config */ + uint8_t show_title:6; /* Display face title */ +} lis2dw_monitor_state_t; + +void lis2dw_monitor_face_setup(uint8_t watch_face_index, void **context_ptr); +void lis2dw_monitor_face_activate(void *context); +bool lis2dw_monitor_face_loop(movement_event_t event, void *context); +void lis2dw_monitor_face_resign(void *context); +movement_watch_face_advisory_t lis2dw_monitor_face_advise(void *context); + +#define lis2dw_monitor_face ((const watch_face_t){ \ + lis2dw_monitor_face_setup, \ + lis2dw_monitor_face_activate, \ + lis2dw_monitor_face_loop, \ + lis2dw_monitor_face_resign, \ + lis2dw_monitor_face_advise, \ +}) From 01b7f30cefd43bccb66e3f27c878dd671546e613 Mon Sep 17 00:00:00 2001 From: Vaipex <86567086+Vaipex@users.noreply.github.com> Date: Sun, 3 Aug 2025 17:16:34 +0000 Subject: [PATCH 11/92] Added simple coin flip watch face (#63) --- movement_faces.h | 1 + watch-faces.mk | 1 + .../complication/simple_coin_flip_face.c | 178 ++++++++++++++++++ .../complication/simple_coin_flip_face.h | 60 ++++++ 4 files changed, 240 insertions(+) create mode 100644 watch-faces/complication/simple_coin_flip_face.c create mode 100644 watch-faces/complication/simple_coin_flip_face.h diff --git a/movement_faces.h b/movement_faces.h index ca270cc3..ded8e789 100644 --- a/movement_faces.h +++ b/movement_faces.h @@ -63,6 +63,7 @@ #include "tally_face.h" #include "probability_face.h" #include "ke_decimal_time_face.h" +#include "simple_coin_flip_face.h" #include "lis2dw_monitor_face.h" #include "wareki_face.h" #include "deadline_face.h" diff --git a/watch-faces.mk b/watch-faces.mk index 91a0d9e9..6b326424 100644 --- a/watch-faces.mk +++ b/watch-faces.mk @@ -38,6 +38,7 @@ SRCS += \ ./watch-faces/complication/kitchen_conversions_face.c \ ./watch-faces/complication/periodic_table_face.c \ ./watch-faces/clock/ke_decimal_time_face.c \ + ./watch-faces/complication/simple_coin_flip_face.c \ ./watch-faces/sensor/lis2dw_monitor_face.c \ ./watch-faces/complication/wareki_face.c \ ./watch-faces/complication/deadline_face.c \ diff --git a/watch-faces/complication/simple_coin_flip_face.c b/watch-faces/complication/simple_coin_flip_face.c new file mode 100644 index 00000000..446832be --- /dev/null +++ b/watch-faces/complication/simple_coin_flip_face.c @@ -0,0 +1,178 @@ +/* + * MIT License + * + * Copyright (c) 2023 Wesley Aptekar-Cassels + * Copyright (c) 2025 Vaipex + * + * 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 "simple_coin_flip_face.h" + +void simple_coin_flip_face_setup(uint8_t watch_face_index, void ** context_ptr) { + (void) watch_face_index; + if (*context_ptr == NULL) { + *context_ptr = malloc(sizeof(simple_coin_flip_face_state_t)); + memset(*context_ptr, 0, sizeof(simple_coin_flip_face_state_t)); + } +} + +void simple_coin_flip_face_activate(void *context) { + simple_coin_flip_face_state_t *state = (simple_coin_flip_face_state_t *)context; +} + +static uint32_t get_random(uint32_t max) { + #if __EMSCRIPTEN__ + return rand() % max; + #else + return arc4random_uniform(max); + #endif + +} + +void draw_start_face() { + watch_clear_display(); + if (watch_get_lcd_type() == WATCH_LCD_TYPE_CLASSIC) { + watch_display_text(WATCH_POSITION_BOTTOM, " Flip"); + } else { + watch_display_text(WATCH_POSITION_BOTTOM, "Flip"); + } +} + +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(); + for(int j = 0; jis_start_face && !state->active){ + if(state->inactivity_ticks >= 15){ + state->is_start_face = false; + state->inactivity_ticks = 0; + draw_start_face(); + }else{ + state->inactivity_ticks++; + } + } else { + state->inactivity_ticks = 0; + } + break; + //execute same action for light and alarm button + case EVENT_LIGHT_BUTTON_UP: + case EVENT_ALARM_BUTTON_UP: + if (!state->active) { + state->active = true; + _blink_face_update_lcd(state); + state->active = false; + state->is_start_face = false; + state->inactivity_ticks = 0; + } + break; + case EVENT_TIMEOUT: + movement_move_to_face(0); + break; + case EVENT_LOW_ENERGY_UPDATE: + break; + default: + return movement_default_loop_handler(event); + } + + return true; +} + +void simple_coin_flip_face_resign(void *context) { + (void) context; +} + diff --git a/watch-faces/complication/simple_coin_flip_face.h b/watch-faces/complication/simple_coin_flip_face.h new file mode 100644 index 00000000..f5c95a4b --- /dev/null +++ b/watch-faces/complication/simple_coin_flip_face.h @@ -0,0 +1,60 @@ +/* + * MIT License + * + * Copyright (c) 2023 Wesley Aptekar-Cassels + * Copyright (c) 2025 Vaipex + * + * 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 "movement.h" + +/* + * A extremely simple coin flip face updated from Wesley Aptekar-Cassels version. + * + * Press ALARM or LIGHT to flip a coin, after a short animation it will display + * "Heads" or "Tails". Press ALARM or LIGHT to flip again. + * + * This is for people who want a simpler UI than probability_face or + * randonaut_face. While those have more features, this one is more immediately + * obvious - useful, for instance, if you are using a coin flip to agree on + * something with someone, and want the operation to be clear to someone who + * has not had anything explained to them. + */ + +typedef struct { + bool active; + bool is_start_face; + uint8_t inactivity_ticks; +} simple_coin_flip_face_state_t; + +void simple_coin_flip_face_setup(uint8_t watch_face_index, void ** context_ptr); +void simple_coin_flip_face_activate(void *context); +bool simple_coin_flip_face_loop(movement_event_t event, void *context); +void simple_coin_flip_face_resign(void *context); + +#define simple_coin_flip_face ((const watch_face_t){ \ + simple_coin_flip_face_setup, \ + simple_coin_flip_face_activate, \ + simple_coin_flip_face_loop, \ + simple_coin_flip_face_resign, \ + NULL, \ +}) From ad854fc9b816f895086af969f8538aa4249e93e8 Mon Sep 17 00:00:00 2001 From: Lorenzo Prosseda Date: Sun, 3 Aug 2025 17:17:57 +0000 Subject: [PATCH 12/92] Add "Evengelion" custom signal tune (#52) * Add "Evengelion" custom signal tune First notes from the opening "Cruel Angel Thesis" * Change tune to "staccato" tempo Draws less power while playing --- movement_custom_signal_tunes.h | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/movement_custom_signal_tunes.h b/movement_custom_signal_tunes.h index 96f083b4..728f68eb 100644 --- a/movement_custom_signal_tunes.h +++ b/movement_custom_signal_tunes.h @@ -200,3 +200,32 @@ int8_t signal_tune[] = { 0 }; #endif // SIGNAL_TUNE_HARRY_POTTER_LONG + +#ifdef SIGNAL_TUNE_EVANGELION +int8_t signal_tune[] = { + BUZZER_NOTE_C5, 13, + BUZZER_NOTE_REST, 13, + BUZZER_NOTE_D5SHARP_E5FLAT, 13, + BUZZER_NOTE_REST, 13, + BUZZER_NOTE_F5, 13, + BUZZER_NOTE_REST, 7, + BUZZER_NOTE_D5SHARP_E5FLAT, 13, + BUZZER_NOTE_REST, 7, + BUZZER_NOTE_F5, 7, + BUZZER_NOTE_REST, 7, + BUZZER_NOTE_F5, 7, + BUZZER_NOTE_REST, 7, + BUZZER_NOTE_F5, 7, + BUZZER_NOTE_REST, 7, + BUZZER_NOTE_A5SHARP_B5FLAT, 7, + BUZZER_NOTE_REST, 7, + BUZZER_NOTE_G5SHARP_A5FLAT, 7, + BUZZER_NOTE_REST, 7, + BUZZER_NOTE_G5, 3, + BUZZER_NOTE_REST, 3, + BUZZER_NOTE_F5, 7, + BUZZER_NOTE_REST, 7, + BUZZER_NOTE_G5, 13, + 0, +}; +#endif // SIGNAL_TUNE_EVANGELION From 39d2c4499ee624cb4f68031177d23b6175108148 Mon Sep 17 00:00:00 2001 From: Lorenzo Prosseda Date: Sun, 3 Aug 2025 17:19:26 +0000 Subject: [PATCH 13/92] Port of timer_face from legacy (#64) - Closes https://github.com/joeycastillo/second-movement/issues/15 - Fixed issue with colon disappearing if exiting settings mode on the CLEAR or LOOP strings - Use newer print function with fallback for classic display --- movement_faces.h | 1 + watch-faces.mk | 1 + .../complication/timer_face.c | 37 +++++++++++-------- .../complication/timer_face.h | 0 4 files changed, 23 insertions(+), 16 deletions(-) rename {legacy/watch_faces => watch-faces}/complication/timer_face.c (91%) rename {legacy/watch_faces => watch-faces}/complication/timer_face.h (100%) diff --git a/movement_faces.h b/movement_faces.h index ded8e789..542a0667 100644 --- a/movement_faces.h +++ b/movement_faces.h @@ -63,6 +63,7 @@ #include "tally_face.h" #include "probability_face.h" #include "ke_decimal_time_face.h" +#include "timer_face.h" #include "simple_coin_flip_face.h" #include "lis2dw_monitor_face.h" #include "wareki_face.h" diff --git a/watch-faces.mk b/watch-faces.mk index 6b326424..bf3c54de 100644 --- a/watch-faces.mk +++ b/watch-faces.mk @@ -38,6 +38,7 @@ SRCS += \ ./watch-faces/complication/kitchen_conversions_face.c \ ./watch-faces/complication/periodic_table_face.c \ ./watch-faces/clock/ke_decimal_time_face.c \ + ./watch-faces/complication/timer_face.c \ ./watch-faces/complication/simple_coin_flip_face.c \ ./watch-faces/sensor/lis2dw_monitor_face.c \ ./watch-faces/complication/wareki_face.c \ diff --git a/legacy/watch_faces/complication/timer_face.c b/watch-faces/complication/timer_face.c similarity index 91% rename from legacy/watch_faces/complication/timer_face.c rename to watch-faces/complication/timer_face.c index de48e356..aaf824c4 100644 --- a/legacy/watch_faces/complication/timer_face.c +++ b/watch-faces/complication/timer_face.c @@ -62,7 +62,8 @@ static void _start(timer_state_t *state, bool with_beep) { } static void _draw(timer_state_t *state, uint8_t subsecond) { - char buf[14]; + char bottom_time[10]; + char timer_id[3]; uint32_t delta; div_t result; uint8_t h, min, sec; @@ -84,37 +85,41 @@ static void _draw(timer_state_t *state, uint8_t subsecond) { result = div(result.quot, 60); min = result.rem; h = result.quot; - sprintf(buf, " %02u%02u%02u", h, min, sec); + sprintf(bottom_time, "%02u%02u%02u", h, min, sec); break; case setting: if (state->settings_state == 1) { // ask it the current timer shall be erased - sprintf(buf, " CLEAR%c", state->erase_timer_flag ? 'y' : 'n'); + sprintf(bottom_time, "CLEAR%c", state->erase_timer_flag ? 'y' : 'n'); watch_clear_colon(); } else if (state->settings_state == 5) { - sprintf(buf, " LOOP%c", state->timers[state->current_timer].unit.repeat ? 'y' : 'n'); + sprintf(bottom_time, " LOOP%c", state->timers[state->current_timer].unit.repeat ? 'y' : 'n'); watch_clear_colon(); } else { - sprintf(buf, " %02u%02u%02u", state->timers[state->current_timer].unit.hours, + sprintf(bottom_time, "%02u%02u%02u", state->timers[state->current_timer].unit.hours, state->timers[state->current_timer].unit.minutes, state->timers[state->current_timer].unit.seconds); watch_set_colon(); } break; case waiting: - sprintf(buf, " %02u%02u%02u", state->timers[state->current_timer].unit.hours, + sprintf(bottom_time, "%02u%02u%02u", state->timers[state->current_timer].unit.hours, state->timers[state->current_timer].unit.minutes, state->timers[state->current_timer].unit.seconds); + watch_set_colon(); break; } - buf[0] = 49 + state->current_timer; + + sprintf(timer_id, "%2u", state->current_timer + 1); if (state->mode == setting && subsecond % 2) { // blink the current settings value - if (state->settings_state == 0) buf[0] = ' '; - else if (state->settings_state == 1 || state->settings_state == 5) buf[6] = ' '; - else buf[(state->settings_state - 1) * 2 - 1] = buf[(state->settings_state - 1) * 2] = ' '; + if (state->settings_state == 0) timer_id[0] = timer_id[1] = ' '; + else if (state->settings_state == 1 || state->settings_state == 5) bottom_time[5] = ' '; + else bottom_time[(state->settings_state - 1) * 2 - 2] = bottom_time[(state->settings_state - 1) * 2 - 1] = ' '; } - watch_display_string(buf, 3); + watch_display_text_with_fallback(WATCH_POSITION_BOTTOM, bottom_time, bottom_time); + watch_display_text_with_fallback(WATCH_POSITION_TOP_RIGHT, timer_id, timer_id); + // set lap indicator when we have a looping timer if (state->timers[state->current_timer].unit.repeat) watch_set_indicator(WATCH_INDICATOR_LAP); else watch_clear_indicator(WATCH_INDICATOR_LAP); @@ -200,7 +205,7 @@ void timer_face_setup(uint8_t watch_face_index, void ** context_ptr) { void timer_face_activate(void *context) { timer_state_t *state = (timer_state_t *)context; - watch_display_string("TR", 0); + watch_display_text_with_fallback(WATCH_POSITION_TOP_LEFT, "TMR", "TR"); watch_set_colon(); if(state->mode == running) { watch_date_time_t now = watch_rtc_get_date_time(); @@ -265,14 +270,14 @@ bool timer_face_loop(movement_event_t event, void *context) { movement_cancel_background_task(); break; case pausing: - _start(statemovement_get_current_timezone_offset(), false); + _start(state, false); break; case waiting: { uint8_t last_timer = state->current_timer; state->current_timer = (state->current_timer + 1) % TIMER_SLOTS; _set_next_valid_timer(state); // start the time immediately if there is only one valid timer slot - if (last_timer == state->current_timer) _start(statemovement_get_current_timezone_offset(), true); + if (last_timer == state->current_timer) _start(state, true); break; } case setting: @@ -299,7 +304,7 @@ bool timer_face_loop(movement_event_t event, void *context) { _beeps_to_play = 4; watch_buzzer_play_sequence((int8_t *)_sound_seq_beep, _signal_callback); _reset(state); - if (state->timers[state->current_timer].unit.repeat) _start(statemovement_get_current_timezone_offset(), false); + if (state->timers[state->current_timer].unit.repeat) _start(state, false); break; case EVENT_ALARM_LONG_PRESS: switch(state->mode) { @@ -319,7 +324,7 @@ bool timer_face_loop(movement_event_t event, void *context) { } break; case waiting: - _start(statemovement_get_current_timezone_offset(), true); + _start(state, true); break; case pausing: case running: diff --git a/legacy/watch_faces/complication/timer_face.h b/watch-faces/complication/timer_face.h similarity index 100% rename from legacy/watch_faces/complication/timer_face.h rename to watch-faces/complication/timer_face.h From d903a827e9a95ec2472b3200c9fbb768a5cae3cd Mon Sep 17 00:00:00 2001 From: Alessandro Genova Date: Mon, 28 Jul 2025 23:58:03 -0400 Subject: [PATCH 14/92] Fix simulator deep sleep mode --- movement.c | 5 ++ .../simulator/watch/watch_deepsleep.c | 52 +++++++++++++++---- 2 files changed, 46 insertions(+), 11 deletions(-) diff --git a/movement.c b/movement.c index f932de0a..0046a489 100644 --- a/movement.c +++ b/movement.c @@ -51,6 +51,7 @@ #if __EMSCRIPTEN__ #include +void _wake_up_simulator(void); #else #include "watch_usb_cdc.h" #endif @@ -1023,6 +1024,10 @@ void cb_alarm_btn_extwake(void) { } void cb_alarm_fired(void) { +#if __EMSCRIPTEN__ + _wake_up_simulator(); +#endif + movement_state.woke_from_alarm_handler = true; } diff --git a/watch-library/simulator/watch/watch_deepsleep.c b/watch-library/simulator/watch/watch_deepsleep.c index 1bc4be73..88ac2da9 100644 --- a/watch-library/simulator/watch/watch_deepsleep.c +++ b/watch-library/simulator/watch/watch_deepsleep.c @@ -25,17 +25,37 @@ #include #include "watch_extint.h" #include "app.h" +#include + static uint32_t watch_backup_data[8]; +static bool _wake_up = false; +static watch_cb_t _callback = NULL; + + +void _wake_up_simulator(void) { + _wake_up = true; +} + +static void cb_extwake_wrapper(void) { + _wake_up_simulator(); + + if (_callback) { + _callback(); + } +} + void watch_register_extwake_callback(uint8_t pin, watch_cb_t callback, bool level) { if (pin == HAL_GPIO_BTN_ALARM_pin()) { + _callback = callback; watch_enable_external_interrupts(); - watch_register_interrupt_callback(pin, callback, level ? INTERRUPT_TRIGGER_RISING : INTERRUPT_TRIGGER_FALLING); + watch_register_interrupt_callback(pin, cb_extwake_wrapper, level ? INTERRUPT_TRIGGER_RISING : INTERRUPT_TRIGGER_FALLING); } } void watch_disable_extwake_interrupt(uint8_t pin) { if (pin == HAL_GPIO_BTN_ALARM_pin()) { + _callback = NULL; watch_register_interrupt_callback(pin, NULL, INTERRUPT_TRIGGER_NONE); } } @@ -57,23 +77,33 @@ uint32_t watch_get_backup_data(uint8_t reg) { void watch_enter_sleep_mode(void) { // TODO: (a2) hook to UI - // enter standby (4); we basically hang out here until an interrupt wakes us. - // sleep(4); + // disable tick interrupt + watch_rtc_disable_all_periodic_callbacks(); + + // // disable all buttons but alarm + watch_register_interrupt_callback(HAL_GPIO_BTN_MODE_pin(), NULL, INTERRUPT_TRIGGER_NONE); + watch_register_interrupt_callback(HAL_GPIO_BTN_LIGHT_pin(), NULL, INTERRUPT_TRIGGER_NONE); + + sleep(4); // call app_setup so the app can re-enable everything we disabled. app_setup(); } -void watch_enter_deep_sleep_mode(void) { - // identical to sleep mode except we disable the LCD first. - // TODO: (a2) hook to UI - - watch_enter_sleep_mode(); -} - void watch_enter_backup_mode(void) { // TODO: (a2) hook to UI // go into backup sleep mode (5). when we exit, the reset controller will take over. - // sleep(5); + sleep(5); +} + +void sleep(const uint8_t mode) { + (void) mode; + + // we basically hang out here until an interrupt wakes us. + while(!_wake_up) { + emscripten_sleep(100); + } + + _wake_up = false; } From 145fc168b1acbdb1517ad73e0c82a2fb454a47a0 Mon Sep 17 00:00:00 2001 From: Lorenzo Prosseda Date: Fri, 1 Aug 2025 18:44:03 +0200 Subject: [PATCH 15/92] Clear low energy animation when waking up --- watch-faces/clock/ke_decimal_time_face.c | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/watch-faces/clock/ke_decimal_time_face.c b/watch-faces/clock/ke_decimal_time_face.c index 83cd7137..a693aa5a 100644 --- a/watch-faces/clock/ke_decimal_time_face.c +++ b/watch-faces/clock/ke_decimal_time_face.c @@ -69,6 +69,10 @@ void ke_decimal_time_face_setup(uint8_t watch_face_index, void ** context_ptr) { void ke_decimal_time_face_activate(void *context) { ke_decimal_time_state_t *state = (ke_decimal_time_state_t *)context; + if (watch_sleep_animation_is_running()) { + watch_stop_sleep_animation(); + } + // force re-display of date and time in EVENT_ACTIVATE state->previous_day = 0xFF; state->previous_time = 0xFFFFFFFF; From 9121c0cfb85006165f7c73ab93a9b64d919d1295 Mon Sep 17 00:00:00 2001 From: Alessandro Genova Date: Tue, 29 Jul 2025 00:27:16 -0400 Subject: [PATCH 16/92] Fix set_time_face erroneously setting the local time when changing timezone --- watch-faces/settings/set_time_face.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/watch-faces/settings/set_time_face.c b/watch-faces/settings/set_time_face.c index 1bcf2123..63fc34fd 100644 --- a/watch-faces/settings/set_time_face.c +++ b/watch-faces/settings/set_time_face.c @@ -44,7 +44,7 @@ static void _handle_alarm_button(watch_date_time_t date_time, uint8_t current_pa movement_set_timezone_index(movement_get_timezone_index() + 1); if (movement_get_timezone_index() >= NUM_ZONE_NAMES) movement_set_timezone_index(0); current_offset = movement_get_current_timezone_offset_for_zone(movement_get_timezone_index()); - break; + return; case 0: // year date_time.unit.year = ((date_time.unit.year % 60) + 1); break; From 4eee544762c661d6b43aed34b07cead6e1010dc6 Mon Sep 17 00:00:00 2001 From: Lorenzo Prosseda Date: Sun, 3 Aug 2025 17:22:18 +0000 Subject: [PATCH 17/92] Port the interval_face complication to Second Movement (#66) * Port the interval_face complication to Second Movement - Compile inside Second Movement - Support custom display * Refactor display buffer name, enlarge index buffer Also removed now unused _blink_idx array * Fix Clear setting not showing, and its formatting * Rename work interval label to make it unique * Skip empty interval timers while cycling through --- movement_faces.h | 1 + watch-faces.mk | 1 + .../complication/interval_face.c | 139 +++++++++++++----- .../complication/interval_face.h | 0 4 files changed, 101 insertions(+), 40 deletions(-) rename {legacy/watch_faces => watch-faces}/complication/interval_face.c (82%) rename {legacy/watch_faces => watch-faces}/complication/interval_face.h (100%) diff --git a/movement_faces.h b/movement_faces.h index 542a0667..1e14d235 100644 --- a/movement_faces.h +++ b/movement_faces.h @@ -63,6 +63,7 @@ #include "tally_face.h" #include "probability_face.h" #include "ke_decimal_time_face.h" +#include "interval_face.h" #include "timer_face.h" #include "simple_coin_flip_face.h" #include "lis2dw_monitor_face.h" diff --git a/watch-faces.mk b/watch-faces.mk index bf3c54de..2b728e06 100644 --- a/watch-faces.mk +++ b/watch-faces.mk @@ -38,6 +38,7 @@ SRCS += \ ./watch-faces/complication/kitchen_conversions_face.c \ ./watch-faces/complication/periodic_table_face.c \ ./watch-faces/clock/ke_decimal_time_face.c \ + ./watch-faces/complication/interval_face.c \ ./watch-faces/complication/timer_face.c \ ./watch-faces/complication/simple_coin_flip_face.c \ ./watch-faces/sensor/lis2dw_monitor_face.c \ diff --git a/legacy/watch_faces/complication/interval_face.c b/watch-faces/complication/interval_face.c similarity index 82% rename from legacy/watch_faces/complication/interval_face.c rename to watch-faces/complication/interval_face.c index c354e722..15593567 100644 --- a/legacy/watch_faces/complication/interval_face.c +++ b/watch-faces/complication/interval_face.c @@ -28,8 +28,6 @@ #include "interval_face.h" #include "watch.h" #include "watch_utility.h" -#include "watch_private_display.h" -#include "watch_buzzer.h" typedef enum { interval_setting_0_timer_idx, @@ -48,10 +46,15 @@ typedef enum { } interval_setting_idx_t; #define INTERVAL_FACE_STATE_DEFAULT "IT" // Interval Timer +#define INTERVAL_FACE_STATE_DEFAULT_CD "INT" #define INTERVAL_FACE_STATE_WARMUP "PR" // PRepare / warm up +#define INTERVAL_FACE_STATE_WARMUP_CD "PRE" #define INTERVAL_FACE_STATE_WORK "WO" // WOrk +#define INTERVAL_FACE_STATE_WORK_CD "WOR" #define INTERVAL_FACE_STATE_BREAK "BR" // BReak +#define INTERVAL_FACE_STATE_BREAK_CD "BRK" #define INTERVAL_FACE_STATE_COOLDOWN "CD" // CoolDown +#define INTERVAL_FACE_STATE_COOLDOWN_CD "CLD" // Define some default timer settings. Each timer is described in an array like this: // 1. warm-up seconds, @@ -68,7 +71,7 @@ static const int8_t _default_timers[6][5] = {{0, 40, 20, 0, 0}, {0, -20, -5, 0, 0}}; static const uint8_t _intro_segdata[4][2] = {{1, 8}, {0, 8}, {0, 7}, {1, 7}}; -static const uint8_t _blink_idx[] = {3, 9, 4, 6, 4, 6, 8, 4, 6, 8, 4, 6}; +static const uint8_t _intro_segdata_cd[4][2] = {{1, 8}, {1, 9}, {0, 9}, {0, 8}}; static const uint8_t _setting_page_idx[] = {1, 0, 1, 1, 2, 2, 2, 3, 3, 3, 4, 4}; static const int8_t _sound_seq_warmup[] = {BUZZER_NOTE_F6, 8, BUZZER_NOTE_REST, 1, -2, 3, 0}; static const int8_t _sound_seq_work[] = {BUZZER_NOTE_F6, 8, BUZZER_NOTE_REST, 1, -2, 2, BUZZER_NOTE_C7, 24, 0}; @@ -102,41 +105,56 @@ static inline void _button_beep() { if (movement_button_should_sound()) watch_buzzer_play_note(BUZZER_NOTE_C7, 50); } -static void _timer_write_info(interval_face_state_t *state, char *buf, uint8_t timer_page) { +static void _timer_write_info(interval_face_state_t *state, char* bottom_row, char state_str[2][4], char* index_str, uint8_t timer_page) { // fill display string with requested timer information switch (timer_page) { case 0: // clear timer? - sprintf(buf, "%2s %1dCLEARn", INTERVAL_FACE_STATE_DEFAULT, state->timer_idx + 1); - if (_erase_timer_flag) buf[9] = 'y'; + sprintf(bottom_row, "CLEARn"); + sprintf(state_str[0], "%2s", INTERVAL_FACE_STATE_DEFAULT); + sprintf(state_str[1], "%3s", INTERVAL_FACE_STATE_DEFAULT_CD); + sprintf(index_str, " %1d", state->timer_idx + 1); + if (_erase_timer_flag) bottom_row[5] = 'y'; watch_clear_colon(); break; case 1: // warmup time info - sprintf(buf, "%2s %1d%02d%02d ", INTERVAL_FACE_STATE_WARMUP, state->timer_idx + 1, + sprintf(bottom_row, "%02d%02d ", state->timer[state->timer_idx].warmup_minutes, state->timer[state->timer_idx].warmup_seconds); + sprintf(state_str[0], "%2s", INTERVAL_FACE_STATE_WARMUP); + sprintf(state_str[1], "%3s", INTERVAL_FACE_STATE_WARMUP_CD); + sprintf(index_str, " %1d", state->timer_idx + 1); break; case 2: // work interval info - sprintf(buf, "%2s %1d%02d%02d%2d", INTERVAL_FACE_STATE_WORK, state->timer_idx + 1, + sprintf(bottom_row, "%02d%02d%2d", state->timer[state->timer_idx].work_minutes, state->timer[state->timer_idx].work_seconds, state->timer[state->timer_idx].work_rounds); + sprintf(state_str[0], "%2s", INTERVAL_FACE_STATE_WORK); + sprintf(state_str[1], "%3s", INTERVAL_FACE_STATE_WORK_CD); + sprintf(index_str, " %1d", state->timer_idx + 1); break; case 3: // break interval info - sprintf(buf, "%2s %1d%02d%02d%2d", INTERVAL_FACE_STATE_BREAK, state->timer_idx + 1, + sprintf(bottom_row, "%02d%02d%2d", state->timer[state->timer_idx].break_minutes, state->timer[state->timer_idx].break_seconds, state->timer[state->timer_idx].full_rounds); - if (!state->timer[state->timer_idx].full_rounds) buf[9] = '-'; + if (!state->timer[state->timer_idx].full_rounds) bottom_row[5] = '-'; + sprintf(state_str[0], "%2s", INTERVAL_FACE_STATE_BREAK); + sprintf(state_str[1], "%3s", INTERVAL_FACE_STATE_BREAK_CD); + sprintf(index_str, " %1d", state->timer_idx + 1); break; case 4: // cooldown time info - sprintf(buf, "%2s %1d%02d%02d ", INTERVAL_FACE_STATE_COOLDOWN ,state->timer_idx + 1, + sprintf(bottom_row, "%02d%02d ", state->timer[state->timer_idx].cooldown_minutes, state->timer[state->timer_idx].cooldown_seconds); + sprintf(state_str[0], "%2s", INTERVAL_FACE_STATE_COOLDOWN); + sprintf(state_str[1], "%3s", INTERVAL_FACE_STATE_COOLDOWN_CD); + sprintf(index_str, " %1d", state->timer_idx + 1); break; default: break; @@ -146,8 +164,10 @@ static void _timer_write_info(interval_face_state_t *state, char *buf, uint8_t t static void _face_draw(interval_face_state_t *state, uint8_t subsecond) { // draws current face state if (!state->is_active) return; - char buf[14]; - buf[0] = 0; + char bottom_row[10]; + char int_state_str[2][4]; + char int_index_str[5]; + bottom_row[0] = 0; uint8_t tmp; if (state->face_state == interval_state_waiting && _ticks >= 0) { // play info slideshow for current timer @@ -160,9 +180,9 @@ static void _face_draw(interval_face_state_t *state, uint8_t subsecond) { } } tmp = ticks / 3 + 1; - _timer_write_info(state, buf, tmp); + _timer_write_info(state, bottom_row, int_state_str, int_index_str, tmp); // don't show '1 round' when displaying workout time to avoid detail overload - if (tmp == 2 && state->timer[state->timer_idx].work_rounds == 1) buf[9] = ' '; + if (tmp == 2 && state->timer[state->timer_idx].work_rounds == 1) bottom_row[5] = ' '; // blink colon if (subsecond % 2 == 0 && _ticks < 24) watch_clear_colon(); else watch_set_colon(); @@ -175,38 +195,66 @@ static void _face_draw(interval_face_state_t *state, uint8_t subsecond) { } else { tmp = _setting_page_idx[_setting_idx]; } - _timer_write_info(state, buf, tmp); + _timer_write_info(state, bottom_row, int_state_str, int_index_str, tmp); // blink at cursor position if (subsecond % 2 && _ticks != -2) { - buf[_blink_idx[_setting_idx]] = ' '; - if (_blink_idx[_setting_idx] % 2 == 0) buf[_blink_idx[_setting_idx] + 1] = ' '; + switch (_setting_idx) { + case interval_setting_0_timer_idx: + int_index_str[0] = int_index_str[1] = ' '; + break; + case interval_setting_1_clear_yn: + bottom_row[5] = ' '; + break; + case interval_setting_2_warmup_minutes: + case interval_setting_4_work_minutes: + case interval_setting_7_break_minutes: + case interval_setting_10_cooldown_minutes: + bottom_row[0] = bottom_row[1] = ' '; + break; + case interval_setting_3_warmup_seconds: + case interval_setting_5_work_seconds: + case interval_setting_8_break_seconds: + case interval_setting_11_cooldown_seconds: + bottom_row[2] = bottom_row[3] = ' '; + break; + case interval_setting_6_work_rounds: + case interval_setting_9_full_rounds: + bottom_row[4] = bottom_row[5] = ' '; + break; + default: + break; + } } // show lap indicator only when rounds are set if (_setting_idx == interval_setting_6_work_rounds || _setting_idx == interval_setting_9_full_rounds) watch_set_indicator(WATCH_INDICATOR_LAP); - else + else watch_clear_indicator(WATCH_INDICATOR_LAP); } else if (state->face_state == interval_state_running || state->face_state == interval_state_pausing) { tmp = _timer_full_round; switch (_timer_run_state) { case 0: - sprintf(buf, INTERVAL_FACE_STATE_WARMUP); + sprintf(int_state_str[0], "%2s", INTERVAL_FACE_STATE_WARMUP); + sprintf(int_state_str[1], "%3s", INTERVAL_FACE_STATE_WARMUP_CD); break; case 1: - sprintf(buf, INTERVAL_FACE_STATE_WORK); + sprintf(int_state_str[0], "%2s", INTERVAL_FACE_STATE_WORK); + sprintf(int_state_str[1], "%3s", INTERVAL_FACE_STATE_WORK_CD); if (state->timer[state->timer_idx].work_rounds > 1) tmp = _timer_work_round; break; case 2: - sprintf(buf, INTERVAL_FACE_STATE_BREAK); + sprintf(int_state_str[0], "%2s", INTERVAL_FACE_STATE_BREAK); + sprintf(int_state_str[1], "%3s", INTERVAL_FACE_STATE_BREAK_CD); break; case 3: - sprintf(buf, INTERVAL_FACE_STATE_COOLDOWN); + sprintf(int_state_str[0], "%2s", INTERVAL_FACE_STATE_COOLDOWN); + sprintf(int_state_str[1], "%3s", INTERVAL_FACE_STATE_COOLDOWN_CD); break; default: break; } div_t delta; - + if (state->face_state == interval_state_pausing) { // pausing delta = div(_target_ts - _paused_ts, 60); @@ -216,16 +264,21 @@ static void _face_draw(interval_face_state_t *state, uint8_t subsecond) { } else // running delta = div(_target_ts - _now_ts, 60); - sprintf(&buf[2], " %1d%02d%02d%2d", state->timer_idx + 1, delta.quot, delta.rem, tmp + 1); + sprintf(bottom_row, "%02d%02d%2d", delta.quot, delta.rem, tmp + 1); + sprintf(int_index_str, " %1d", state->timer_idx + 1); } // write out to lcd - if (buf[0]) { - watch_display_character(buf[0], 0); - watch_display_character(buf[1], 1); + if (int_state_str[0][0]) { + watch_display_text_with_fallback(WATCH_POSITION_TOP_LEFT, int_state_str[1], int_state_str[0]); + watch_display_text_with_fallback(WATCH_POSITION_TOP_RIGHT, int_index_str, int_index_str); // set the bar for the i-like symbol on position 2 - watch_set_pixel(2, 9); + if (watch_get_lcd_type() == WATCH_LCD_TYPE_CLASSIC) { + watch_set_pixel(2, 9); + } else { + watch_set_pixel(2, 10); + } // display the rest of the string - watch_display_string(&buf[3], 3); + watch_display_text_with_fallback(WATCH_POSITION_BOTTOM, bottom_row, bottom_row); } } @@ -427,8 +480,12 @@ bool interval_face_loop(movement_event_t event, void *context) { _init_timer_info(state); _face_draw(state, event.subsecond); break; - } - watch_set_pixel(_intro_segdata[_ticks][0], _intro_segdata[_ticks][1]); + } + if (watch_get_lcd_type() == WATCH_LCD_TYPE_CLASSIC) { + watch_set_pixel(_intro_segdata[_ticks][0], _intro_segdata[_ticks][1]); + } else { + watch_set_pixel(_intro_segdata_cd[_ticks][0], _intro_segdata_cd[_ticks][1]); + } _ticks++; } else if (state->face_state == interval_state_waiting && _ticks >= 0) { // play information slideshow for current interval timer @@ -448,7 +505,7 @@ bool interval_face_loop(movement_event_t event, void *context) { } break; case EVENT_ACTIVATE: - watch_display_string(INTERVAL_FACE_STATE_DEFAULT, 0); + watch_display_text_with_fallback(WATCH_POSITION_TOP_LEFT, INTERVAL_FACE_STATE_DEFAULT_CD, INTERVAL_FACE_STATE_DEFAULT); if (state->face_state) _face_draw(state, event.subsecond); break; case EVENT_LIGHT_BUTTON_UP: @@ -479,7 +536,7 @@ bool interval_face_loop(movement_event_t event, void *context) { } break; case EVENT_LIGHT_LONG_PRESS: - _button_beep(settings); + _button_beep(); if (state->face_state == interval_state_setting) { _resume_setting(state, event.subsecond); } else { @@ -490,8 +547,10 @@ bool interval_face_loop(movement_event_t event, void *context) { case EVENT_ALARM_BUTTON_UP: switch (state->face_state) { case interval_state_waiting: - // cycle through timers - _inc_uint8(&state->timer_idx, 1, INTERVAL_TIMERS); + // cycle through timers, skipping empty ones + do { + _inc_uint8(&state->timer_idx, 1, INTERVAL_TIMERS); + } while (_is_timer_empty(&state->timer[state->timer_idx]) && state->timer_idx != 0); _ticks = 0; _face_draw(state, event.subsecond); break; @@ -502,7 +561,7 @@ bool interval_face_loop(movement_event_t event, void *context) { break; case interval_state_running: // pause timer - _button_beep(settings); + _button_beep(); _paused_ts = _get_now_ts(); state->face_state = interval_state_pausing; movement_cancel_background_task(); @@ -510,7 +569,7 @@ bool interval_face_loop(movement_event_t event, void *context) { break; case interval_state_pausing: // resume paused timer - _button_beep(settings); + _button_beep(); _resume_paused_timer(state); _face_draw(state, event.subsecond); break; @@ -527,7 +586,7 @@ bool interval_face_loop(movement_event_t event, void *context) { } else if (state->face_state <= interval_state_waiting) { if (_is_timer_empty(timer)) { // jump back to timer #1 - _button_beep(settings); + _button_beep(); state->timer_idx = 0; _init_timer_info(state); } else { @@ -551,7 +610,7 @@ bool interval_face_loop(movement_event_t event, void *context) { _init_timer_info(state); } else if (state->face_state == interval_state_pausing) { // resume paused timer - _button_beep(settings); + _button_beep(); _resume_paused_timer(state); } _face_draw(state, event.subsecond); diff --git a/legacy/watch_faces/complication/interval_face.h b/watch-faces/complication/interval_face.h similarity index 100% rename from legacy/watch_faces/complication/interval_face.h rename to watch-faces/complication/interval_face.h From 8ecf4ca3e64c6b26ce8093a1c62bebbc35371838 Mon Sep 17 00:00:00 2001 From: Lorenzo Prosseda Date: Sun, 3 Aug 2025 17:23:51 +0000 Subject: [PATCH 18/92] Add "Jurassic Park" custom signal tune (#53) * Add "Jurassic Park" custom signal tune * Change tune to "staccato" tempo Draws less power while playing --- movement_custom_signal_tunes.h | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/movement_custom_signal_tunes.h b/movement_custom_signal_tunes.h index 728f68eb..a654433c 100644 --- a/movement_custom_signal_tunes.h +++ b/movement_custom_signal_tunes.h @@ -201,6 +201,31 @@ int8_t signal_tune[] = { }; #endif // SIGNAL_TUNE_HARRY_POTTER_LONG +#ifdef SIGNAL_TUNE_JURASSIC_PARK +int8_t signal_tune[] = { + BUZZER_NOTE_B5, 7, + BUZZER_NOTE_REST, 7, + BUZZER_NOTE_A5SHARP_B5FLAT, 7, + BUZZER_NOTE_REST, 7, + BUZZER_NOTE_B5, 13, + BUZZER_NOTE_REST, 13, + BUZZER_NOTE_F5SHARP_G5FLAT, 13, + BUZZER_NOTE_REST, 13, + BUZZER_NOTE_E5, 13, + BUZZER_NOTE_REST, 13, + BUZZER_NOTE_B5, 7, + BUZZER_NOTE_REST, 7, + BUZZER_NOTE_A5SHARP_B5FLAT, 7, + BUZZER_NOTE_REST, 7, + BUZZER_NOTE_B5, 13, + BUZZER_NOTE_REST, 13, + BUZZER_NOTE_F5SHARP_G5FLAT, 13, + BUZZER_NOTE_REST, 13, + BUZZER_NOTE_E5, 13, + 0, +}; +#endif // SIGNAL_TUNE_JURASSIC_PARK + #ifdef SIGNAL_TUNE_EVANGELION int8_t signal_tune[] = { BUZZER_NOTE_C5, 13, From 6a422698573b01c364d64c45e7621e83fb48241b Mon Sep 17 00:00:00 2001 From: metrast <117864304+metrast@users.noreply.github.com> Date: Sun, 3 Aug 2025 19:26:17 +0200 Subject: [PATCH 19/92] ported pulsometer_face and counter_face from movement and optimized it for the custom lcd (#67) --- movement_faces.h | 2 + watch-faces.mk | 2 + watch-faces/complication/counter_face.c | 153 +++++++++++++++ watch-faces/complication/counter_face.h | 63 ++++++ watch-faces/complication/pulsometer_face.c | 215 +++++++++++++++++++++ watch-faces/complication/pulsometer_face.h | 87 +++++++++ 6 files changed, 522 insertions(+) create mode 100644 watch-faces/complication/counter_face.c create mode 100644 watch-faces/complication/counter_face.h create mode 100644 watch-faces/complication/pulsometer_face.c create mode 100644 watch-faces/complication/pulsometer_face.h diff --git a/movement_faces.h b/movement_faces.h index 1e14d235..230eae46 100644 --- a/movement_faces.h +++ b/movement_faces.h @@ -63,6 +63,8 @@ #include "tally_face.h" #include "probability_face.h" #include "ke_decimal_time_face.h" +#include "counter_face.h" +#include "pulsometer_face.h" #include "interval_face.h" #include "timer_face.h" #include "simple_coin_flip_face.h" diff --git a/watch-faces.mk b/watch-faces.mk index 2b728e06..c0ed9bf3 100644 --- a/watch-faces.mk +++ b/watch-faces.mk @@ -38,6 +38,8 @@ SRCS += \ ./watch-faces/complication/kitchen_conversions_face.c \ ./watch-faces/complication/periodic_table_face.c \ ./watch-faces/clock/ke_decimal_time_face.c \ + ./watch-faces/complication/counter_face.c \ + ./watch-faces/complication/pulsometer_face.c \ ./watch-faces/complication/interval_face.c \ ./watch-faces/complication/timer_face.c \ ./watch-faces/complication/simple_coin_flip_face.c \ diff --git a/watch-faces/complication/counter_face.c b/watch-faces/complication/counter_face.c new file mode 100644 index 00000000..9db86fc3 --- /dev/null +++ b/watch-faces/complication/counter_face.c @@ -0,0 +1,153 @@ +/* + * MIT License + * + * Copyright (c) 2022 Shogo Okamoto + * + * 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 "counter_face.h" +#include "watch.h" +#include "watch_utility.h" +#include "watch_common_display.h" + +static inline bool lcd_is_custom(void) { + return watch_get_lcd_type() == WATCH_LCD_TYPE_CUSTOM; +} + +void counter_face_setup(uint8_t watch_face_index, void ** context_ptr) { + (void) watch_face_index; + if (*context_ptr == NULL) { + *context_ptr = malloc(sizeof(counter_state_t)); + memset(*context_ptr, 0, sizeof(counter_state_t)); + counter_state_t *state = (counter_state_t *)*context_ptr; + state->beep_on = true; + } +} + +void counter_face_activate(void *context) { + counter_state_t *state = (counter_state_t *)context; + if (state->beep_on) { + watch_set_indicator(WATCH_INDICATOR_SIGNAL); + } +} + +bool counter_face_loop(movement_event_t event, void *context) { + + counter_state_t *state = (counter_state_t *)context; + + switch (event.event_type) { + case EVENT_ALARM_BUTTON_UP: + watch_buzzer_abort_sequence(); //abort running buzzer sequence when counting fast + state->counter_idx++; // increment counter index + if (state->counter_idx>99) { //0-99 + state->counter_idx=0;//reset counter index + } + print_counter(state); + if (state->beep_on) { + beep_counter(state); + } + break; + case EVENT_LIGHT_LONG_PRESS: + watch_buzzer_abort_sequence(); + state->beep_on = !state->beep_on; + if (state->beep_on) { + watch_set_indicator(WATCH_INDICATOR_SIGNAL); + } else { + watch_clear_indicator(WATCH_INDICATOR_SIGNAL); + } + break; + case EVENT_ALARM_LONG_PRESS: + state->counter_idx=0; // reset counter index + print_counter(state); + break; + case EVENT_ACTIVATE: + print_counter(state); + break; + case EVENT_TIMEOUT: + // ignore timeout + break; + default: + movement_default_loop_handler(event); + break; + } + + return true; +} + +// beep counter index times +void beep_counter(counter_state_t *state) { + int low_count = state->counter_idx/5; + int high_count = state->counter_idx - low_count * 5; + static int8_t sound_seq[15]; + memset(sound_seq, 0, 15); + int i = 0; + if (low_count > 0) { + sound_seq[i] = BUZZER_NOTE_A6; + i++; + sound_seq[i] = 3; + i++; + sound_seq[i] = BUZZER_NOTE_REST; + i++; + sound_seq[i] = 6; + i++; + if (low_count > 1) { + sound_seq[i] = -2; + i++; + sound_seq[i] = low_count-1; + i++; + } + sound_seq[i] = BUZZER_NOTE_REST; + i++; + sound_seq[i] = 6; + i++; + } + if (high_count > 0) { + sound_seq[i] = BUZZER_NOTE_B6; + i++; + sound_seq[i] = 3; + i++; + sound_seq[i] = BUZZER_NOTE_REST; + i++; + sound_seq[i] = 6; + i++; + } + if (high_count > 1) { + sound_seq[i] = -2; + i++; + sound_seq[i] = high_count-1; + } + watch_buzzer_play_sequence((int8_t *)sound_seq, NULL); +} + + +// print counter index at the center of display. +void print_counter(counter_state_t *state) { + char buf[14]; + watch_display_text_with_fallback(WATCH_POSITION_TOP, "COUNT", "CO"); + sprintf(buf, " %02d", state->counter_idx); // center of LCD display + watch_display_text_with_fallback(WATCH_POSITION_BOTTOM, buf, buf); + +} + +void counter_face_resign(void *context) { + (void) context; +} diff --git a/watch-faces/complication/counter_face.h b/watch-faces/complication/counter_face.h new file mode 100644 index 00000000..2ec68127 --- /dev/null +++ b/watch-faces/complication/counter_face.h @@ -0,0 +1,63 @@ +/* + * MIT License + * + * Copyright (c) 2022 Shogo Okamoto + * + * 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 COUNTER_FACE_H_ +#define COUNTER_FACE_H_ + +/* + * COUNTER face + * + * Counter face is designed to count the number of running laps during exercises. + * + * Usage: + * Short-press ALARM to increment the counter (loops at 99) + * Long-press ALARM to reset the counter. + * Long-press LIGHT to toggle sound. + */ + +#include "movement.h" + +typedef struct { + uint8_t counter_idx; + bool beep_on; +} counter_state_t; + + +void counter_face_setup(uint8_t watch_face_index, void ** context_ptr); +void counter_face_activate(void *context); +bool counter_face_loop(movement_event_t event, void *context); +void counter_face_resign(void *context); + +void print_counter(counter_state_t *state); +void beep_counter(counter_state_t *state); + +#define counter_face ((const watch_face_t){ \ + counter_face_setup, \ + counter_face_activate, \ + counter_face_loop, \ + counter_face_resign, \ + NULL, \ +}) + +#endif // COUNTER_FACE_H_ diff --git a/watch-faces/complication/pulsometer_face.c b/watch-faces/complication/pulsometer_face.c new file mode 100644 index 00000000..42f445af --- /dev/null +++ b/watch-faces/complication/pulsometer_face.c @@ -0,0 +1,215 @@ +/* SPDX-License-Identifier: MIT */ + +/* + * MIT License + * + * Copyright © 2021-2022 Joey Castillo + * Copyright © 2023 Jeremy O'Brien + * Copyright © 2024 Matheus Afonso Martins Moreira (https://www.matheusmoreira.com/) + * + * 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 "pulsometer_face.h" +#include "watch.h" +#include "watch_common_display.h" + +#ifndef PULSOMETER_FACE_CALIBRATION_DEFAULT +#define PULSOMETER_FACE_CALIBRATION_DEFAULT (30) +#endif + +#ifndef PULSOMETER_FACE_CALIBRATION_INCREMENT +#define PULSOMETER_FACE_CALIBRATION_INCREMENT (10) +#endif + +// tick frequency will be 2 to this power Hz (0 for 1 Hz, 2 for 4 Hz, etc.) +#ifndef PULSOMETER_FACE_FREQUENCY_FACTOR +#define PULSOMETER_FACE_FREQUENCY_FACTOR (4ul) +#endif + +#define PULSOMETER_FACE_FREQUENCY (1 << PULSOMETER_FACE_FREQUENCY_FACTOR) + +typedef struct { + bool measuring; + int16_t pulses; + int16_t ticks; + int8_t calibration; +} pulsometer_state_t; + +static inline bool lcd_is_custom(void) { + return watch_get_lcd_type() == WATCH_LCD_TYPE_CUSTOM; +} + +static void pulsometer_display_title(pulsometer_state_t *pulsometer) { + (void) pulsometer; + watch_display_text_with_fallback(WATCH_POSITION_TOP, "PULSE", "PL"); +} + +static void pulsometer_display_calibration(pulsometer_state_t *pulsometer) { + char buf[3]; + if (lcd_is_custom()) { + snprintf(buf, sizeof(buf), "%2hhd", pulsometer->calibration); + watch_display_text(WATCH_POSITION_SECONDS, buf); + } else { + snprintf(buf, sizeof(buf), "%2hhd", pulsometer->calibration); + watch_display_text(WATCH_POSITION_TOP_RIGHT, buf); + } +} + +static void pulsometer_display_measurement(pulsometer_state_t *pulsometer) { + if (lcd_is_custom()) { + char buf[5]; + int16_t value = pulsometer->pulses; + + if (value > 9999) value = 9999; + + snprintf(buf, sizeof(buf), "%-4hd", value); + watch_display_text(WATCH_POSITION_BOTTOM, buf); + } else { + char buf[7]; + snprintf(buf, sizeof(buf), "%-6hd", pulsometer->pulses); + watch_display_text(WATCH_POSITION_BOTTOM, buf); + } +} + +static void pulsometer_indicate(pulsometer_state_t *pulsometer) { + if (pulsometer->measuring) { + watch_set_indicator(WATCH_INDICATOR_LAP); + } else { + watch_clear_indicator(WATCH_INDICATOR_LAP); + } +} + +static void pulsometer_start_measurement(pulsometer_state_t *pulsometer) { + pulsometer->measuring = true; + pulsometer->pulses = INT16_MAX; + pulsometer->ticks = 0; + + pulsometer_indicate(pulsometer); + + movement_request_tick_frequency(PULSOMETER_FACE_FREQUENCY); +} + +static void pulsometer_measure(pulsometer_state_t *pulsometer) { + if (!pulsometer->measuring) { return; } + + pulsometer->ticks++; + + float ticks_per_minute = 60 << PULSOMETER_FACE_FREQUENCY_FACTOR; + float pulses_while_button_held = ticks_per_minute / pulsometer->ticks; + float calibrated_pulses = pulses_while_button_held * pulsometer->calibration; + calibrated_pulses += 0.5f; + + pulsometer->pulses = (int16_t) calibrated_pulses; + + pulsometer_display_measurement(pulsometer); +} + +static void pulsometer_stop_measurement(pulsometer_state_t *pulsometer) { + movement_request_tick_frequency(1); + + pulsometer->measuring = false; + + pulsometer_display_measurement(pulsometer); + pulsometer_indicate(pulsometer); +} + +static void pulsometer_cycle_calibration(pulsometer_state_t *pulsometer, int8_t increment) { + if (pulsometer->measuring) { return; } + + if (pulsometer->calibration <= 0) { + pulsometer->calibration = 1; + } + + int8_t last = pulsometer->calibration; + pulsometer->calibration += increment; + + if (pulsometer->calibration > 39) { + pulsometer->calibration = last == 39? 1 : 39; + } + + pulsometer_display_calibration(pulsometer); +} + +void pulsometer_face_setup(uint8_t watch_face_index, void ** context_ptr) { + (void) watch_face_index; + + if (*context_ptr == NULL) { + pulsometer_state_t *pulsometer = malloc(sizeof(pulsometer_state_t)); + + pulsometer->calibration = PULSOMETER_FACE_CALIBRATION_DEFAULT; + pulsometer->pulses = 0; + pulsometer->ticks = 0; + + *context_ptr = pulsometer; + } +} + +void pulsometer_face_activate(void *context) { + + pulsometer_state_t *pulsometer = context; + + pulsometer->measuring = false; + + pulsometer_display_title(pulsometer); + pulsometer_display_calibration(pulsometer); + pulsometer_display_measurement(pulsometer); +} + +bool pulsometer_face_loop(movement_event_t event,void *context) { + + pulsometer_state_t *pulsometer = (pulsometer_state_t *) context; + + switch (event.event_type) { + case EVENT_ALARM_BUTTON_DOWN: + pulsometer_start_measurement(pulsometer); + break; + case EVENT_ALARM_BUTTON_UP: + case EVENT_ALARM_LONG_UP: + pulsometer_stop_measurement(pulsometer); + break; + case EVENT_TICK: + pulsometer_measure(pulsometer); + break; + case EVENT_LIGHT_BUTTON_UP: + pulsometer_cycle_calibration(pulsometer, 1); + break; + case EVENT_LIGHT_LONG_UP: + pulsometer_cycle_calibration(pulsometer, PULSOMETER_FACE_CALIBRATION_INCREMENT); + break; + case EVENT_LIGHT_BUTTON_DOWN: + // Inhibit the LED + break; + case EVENT_TIMEOUT: + movement_move_to_face(0); + break; + default: + movement_default_loop_handler(event); + break; + } + + return true; +} + +void pulsometer_face_resign(void *context) { + (void) context; +} diff --git a/watch-faces/complication/pulsometer_face.h b/watch-faces/complication/pulsometer_face.h new file mode 100644 index 00000000..7adb9ca8 --- /dev/null +++ b/watch-faces/complication/pulsometer_face.h @@ -0,0 +1,87 @@ +/* SPDX-License-Identifier: MIT */ + +/* + * MIT License + * + * Copyright © 2021-2022 Joey Castillo + * Copyright © 2022 Alexsander Akers + * Copyright © 2023 Alex Utter + * Copyright © 2024 Matheus Afonso Martins Moreira (https://www.matheusmoreira.com/) + * + * 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 PULSOMETER_FACE_H_ +#define PULSOMETER_FACE_H_ + +/* + * PULSOMETER face + * + * The pulsometer implements a classic mechanical watch complication. + * A mechanical pulsometer involves a chronograph with a scale that + * allows the user to compute the number of heart beats per minute + * in less time. The scale is calibrated, or graduated, for a fixed + * number of heart beats, most often 30. The user starts the chronograph + * and simultaneously begins counting the heart beats. The movement of + * the chronograph's seconds hand over time automatically performs the + * computations required. When the calibrated number of heart beats + * is reached, the chronograph is stopped and the seconds hand shows + * the heart rate. + * + * The Sensor Watch pulsometer improves this design with user calibration: + * it can be graduated to any value between 1 and 39 pulsations per minute. + * The default is still 30, mirroring the classic pulsometer calibration. + * This feature allows the user to reconfigure the pulsometer to count + * many other types of periodic minutely events, making it more versatile. + * For example, it can be set to 5 respirations per minute to turn it into + * an asthmometer, a nearly identical mechanical watch complication + * that doctors might use to quickly measure respiratory rate. + * + * To use the pulsometer, hold the ALARM button and count the pulses. + * When the calibrated number of pulses is reached, release the button. + * The display will show the number of pulses per minute. + * + * In order to measure heart rate, feel for a pulse using the hand with + * the watch while holding the button down with the other. + * The pulse can be easily felt on the carotid artery of the neck. + * + * In order to measure breathing rate, simply hold the ALARM button + * and count the number of breaths. + * + * To calibrate the pulsometer, press LIGHT + * to cycle to the next integer calibration. + * Long press LIGHT to cycle it by 10. + */ + +#include "movement.h" + +void pulsometer_face_setup(uint8_t watch_face_index, void ** context_ptr); +void pulsometer_face_activate(void *context); +bool pulsometer_face_loop(movement_event_t event,void *context); +void pulsometer_face_resign(void *context); + +#define pulsometer_face ((const watch_face_t){ \ + pulsometer_face_setup, \ + pulsometer_face_activate, \ + pulsometer_face_loop, \ + pulsometer_face_resign, \ + NULL, \ +}) + +#endif // PULSOMETER_FACE_H_ From 3d86e14f0540008d165e8bdb3311e33bd2c5b606 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A1bor=20Ny=C3=A9ki?= <676249+gn0@users.noreply.github.com> Date: Sun, 3 Aug 2025 13:59:14 -0400 Subject: [PATCH 20/92] add Baby kicks face (#70) --- movement_faces.h | 1 + watch-faces.mk | 1 + watch-faces/complication/baby_kicks_face.c | 439 +++++++++++++++++++++ watch-faces/complication/baby_kicks_face.h | 132 +++++++ 4 files changed, 573 insertions(+) create mode 100644 watch-faces/complication/baby_kicks_face.c create mode 100644 watch-faces/complication/baby_kicks_face.h diff --git a/movement_faces.h b/movement_faces.h index 230eae46..fc43716d 100644 --- a/movement_faces.h +++ b/movement_faces.h @@ -63,6 +63,7 @@ #include "tally_face.h" #include "probability_face.h" #include "ke_decimal_time_face.h" +#include "baby_kicks_face.h" #include "counter_face.h" #include "pulsometer_face.h" #include "interval_face.h" diff --git a/watch-faces.mk b/watch-faces.mk index c0ed9bf3..906806d9 100644 --- a/watch-faces.mk +++ b/watch-faces.mk @@ -38,6 +38,7 @@ SRCS += \ ./watch-faces/complication/kitchen_conversions_face.c \ ./watch-faces/complication/periodic_table_face.c \ ./watch-faces/clock/ke_decimal_time_face.c \ + ./watch-faces/complication/baby_kicks_face.c \ ./watch-faces/complication/counter_face.c \ ./watch-faces/complication/pulsometer_face.c \ ./watch-faces/complication/interval_face.c \ diff --git a/watch-faces/complication/baby_kicks_face.c b/watch-faces/complication/baby_kicks_face.c new file mode 100644 index 00000000..bfbc0756 --- /dev/null +++ b/watch-faces/complication/baby_kicks_face.c @@ -0,0 +1,439 @@ +/* + * MIT License + * + * Copyright (c) 2025 Gábor Nyéki + * + * 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 "baby_kicks_face.h" +#include "watch.h" +#include "watch_utility.h" + +static inline void _play_failure_sound_if_beep_is_on() { + if (movement_button_should_sound()) { + watch_buzzer_play_note(BUZZER_NOTE_E7, 10); + } +} + +static inline void _play_successful_increment_sound_if_beep_is_on() { + if (movement_button_should_sound()) { + watch_buzzer_play_note(BUZZER_NOTE_E6, 10); + } +} + +static inline void _play_successful_decrement_sound_if_beep_is_on() { + if (movement_button_should_sound()) { + watch_buzzer_play_note(BUZZER_NOTE_D6, 10); + } +} + +static inline void _play_button_sound_if_beep_is_on() { + if (movement_button_should_sound()) { + watch_buzzer_play_note(BUZZER_NOTE_C7, 10); + } +} + +/** @brief Predicate for whether the counter has been started. + */ +static inline bool _is_running(baby_kicks_state_t *state) { + return state->start > 0; +} + +/** @brief Gets the current time, and caches it for re-use. + */ +static inline watch_date_time_t *_get_now(baby_kicks_state_t *state) { + if (state->now.unit.year == 0) { + state->now = movement_get_local_date_time(); + } + + return &state->now; +} + +/** @brief Clears the current time. Should only be called at the end of + * `baby_kicks_face_loop`. + */ +static inline void _clear_now(baby_kicks_state_t *state) { + if (state->now.unit.year > 0) { + memset(&state->now, 0, sizeof(state->now)); + } +} + +/** @brief Calculates the number of minutes since the timer was started. + * @return If the counter has been started, then the number of full + * minutes that have elapsed. If it has not been started, then + * 255. + */ +static inline uint32_t _elapsed_minutes(baby_kicks_state_t *state) { + if (!_is_running(state)) { + return 0xff; + } + + watch_date_time_t *now = _get_now(state); + + return ( + watch_utility_date_time_to_unix_time(*now, 0) - state->start + ) / 60; +} + +/** @brief Predicate for whether the counter has started but run for too + * long. + */ +static inline bool _has_timed_out(baby_kicks_state_t *state) { + return _elapsed_minutes(state) > BABY_KICKS_TIMEOUT; +} + +/** @brief Determines what we should display based on `state`. Should + * only be called from `baby_kicks_face_loop`. + */ +static void _update_display_mode(baby_kicks_state_t *state) { + if (watch_sleep_animation_is_running()) { + state->mode = BABY_KICKS_MODE_LE_MODE; + } else if (!_is_running(state)) { + state->mode = BABY_KICKS_MODE_SPLASH; + } else if (_has_timed_out(state)) { + state->mode = BABY_KICKS_MODE_TIMED_OUT; + } else { + state->mode = BABY_KICKS_MODE_ACTIVE; + } +} + +/** @brief Starts the counter. + * @details Sets the start time which will be used to calculate the + * elapsed minutes. + */ +static inline void _start(baby_kicks_state_t *state) { + watch_date_time_t *now = _get_now(state); + uint32_t now_unix = watch_utility_date_time_to_unix_time(*now, 0); + + state->start = now_unix; +} + +/** @brief Resets the counter. + * @details Zeros out the watch face state and clears the undo ring + * buffer. Effectively sets `state->mode` to + * `BABY_KICKS_MODE_SPLASH`. + */ +static void _reset(baby_kicks_state_t *state) { + memset(state, 0, sizeof(baby_kicks_state_t)); + memset( + state->undo_buffer.stretches, + 0xff, + sizeof(state->undo_buffer.stretches) + ); +} + +/** @brief Records a movement. + * @details Increments the movement counter, and along with it, if + * necessary, the counter of one-minute stretches. Also adds + * the movement to the undo buffer. + */ +static inline void _increment_counts(baby_kicks_state_t *state) { + watch_date_time_t *now = _get_now(state); + uint32_t now_unix = watch_utility_date_time_to_unix_time(*now, 0); + + /* Add movement to the undo ring buffer. */ + state->undo_buffer.stretches[state->undo_buffer.head] = + state->stretch_count; + state->undo_buffer.head++; + state->undo_buffer.head %= sizeof(state->undo_buffer.stretches); + + state->movement_count++; + + if (state->stretch_count == 0 + || state->latest_stretch_start + 60 < now_unix) { + /* Start new stretch. */ + state->latest_stretch_start = now_unix; + state->stretch_count++; + } +} + +/** @brief Undoes the last movement. + * @details Decrements the movement counter and, if necessary, the + * counter of one-minute stretches. + * @return True if and only if there was a movement to undo. + */ +static inline bool _successfully_undo(baby_kicks_state_t *state) { + uint8_t latest_mvmt, pre_undo_stretch_count; + + /* The latest movement is stored one position before `.head`. */ + if (state->undo_buffer.head == 0) { + latest_mvmt = sizeof(state->undo_buffer.stretches) - 1; + } else { + latest_mvmt = state->undo_buffer.head - 1; + } + + pre_undo_stretch_count = + state->undo_buffer.stretches[latest_mvmt]; + + if (pre_undo_stretch_count == 0xff) { + /* Nothing to undo. */ + return false; + } else if (pre_undo_stretch_count < state->stretch_count) { + state->latest_stretch_start = 0; + state->stretch_count--; + } + + state->movement_count--; + + state->undo_buffer.stretches[latest_mvmt] = 0xff; + state->undo_buffer.head = latest_mvmt; + + return true; +} + +/** @brief Updates the display with the movement counts if the counter + * has been started. + */ +static inline void _display_counts(baby_kicks_state_t *state) { + if (!_is_running(state)) { + watch_display_text(WATCH_POSITION_BOTTOM, "baby "); + watch_clear_colon(); + } else { + char buf[7]; + + snprintf( + buf, + sizeof(buf), + "%2d%4d", + state->stretch_count, + state->movement_count + ); + + watch_display_text(WATCH_POSITION_BOTTOM, buf); + watch_set_colon(); + } +} + +/** @brief Updates the display with the number of minutes since the + * timer was started. + * @details If more than `BABY_KICKS_TIMEOUT` minutes have elapsed, + * then it displays "TO". + */ +static void _display_elapsed_minutes(baby_kicks_state_t *state) { + if (!_is_running(state)) { + watch_display_text(WATCH_POSITION_TOP_LEFT, " "); + watch_display_text(WATCH_POSITION_TOP_RIGHT, " "); + } else if (_has_timed_out(state)) { + watch_display_text(WATCH_POSITION_TOP_LEFT, "TO"); + watch_display_text(WATCH_POSITION_TOP_RIGHT, " "); + } else { + /* NOTE We display the elapsed minutes in two parts. This is + * because on the classic LCD, neither the "weekday digits" nor + * the "day digits" position is suitable to display the elapsed + * minutes: + * + * - The classic LCD cannot display 2, 4, 5, 6, or 9 as the last + * digit in the "weekday digits" position. + * - It cannot display any number greater than 3 as the first + * digit in the "day digits" position. + * + * As a workaround, we split the elapsed minutes into 30-minute + * "laps." The elapsed minutes in the current "lap" are shown + * in the "day digits" position. This is any number between 0 + * and 29. The elapsed minutes in past "laps" are shown in the + * "weekday digits" position. This is either nothing, 30, 60, + * or 90. + * + * The sum of the numbers shown in the two positions is equal to + * the total elapsed minutes. + */ + + char buf[3]; + uint32_t elapsed_minutes = _elapsed_minutes(state); + uint8_t multiple = elapsed_minutes / 30; + uint8_t remainder = elapsed_minutes % 30; + + if (multiple == 0) { + watch_display_text(WATCH_POSITION_TOP_LEFT, " "); + } else { + snprintf(buf, sizeof(buf), "%2d", multiple * 30); + watch_display_text(WATCH_POSITION_TOP_LEFT, buf); + } + + snprintf(buf, sizeof(buf), "%2d", remainder); + watch_display_text(WATCH_POSITION_TOP_RIGHT, buf); + } +} + +static void _update_display(baby_kicks_state_t *state) { + _display_counts(state); + _display_elapsed_minutes(state); +} + +static inline void _start_sleep_face() { + if (!watch_sleep_animation_is_running()) { + watch_display_text(WATCH_POSITION_TOP_LEFT, " "); + watch_display_text(WATCH_POSITION_TOP_RIGHT, " "); + watch_display_text(WATCH_POSITION_BOTTOM, "baby "); + + watch_clear_colon(); + + watch_start_sleep_animation(500); + } +} + +static inline void _stop_sleep_face() { + if (watch_sleep_animation_is_running()) { + watch_stop_sleep_animation(); + } +} + +void baby_kicks_face_setup(uint8_t watch_face_index, + void **context_ptr) { + (void) watch_face_index; + + if (*context_ptr == NULL) { + *context_ptr = malloc(sizeof(baby_kicks_state_t)); + _reset(*context_ptr); + } +} + +void baby_kicks_face_activate(void *context) { + (void) context; + + _stop_sleep_face(); +} + +void baby_kicks_face_resign(void *context) { + baby_kicks_state_t *state = (baby_kicks_state_t *)context; + + state->currently_displayed = false; +} + +bool baby_kicks_face_loop(movement_event_t event, void *context) { + baby_kicks_state_t *state = (baby_kicks_state_t *)context; + + switch (event.event_type) { + case EVENT_ACTIVATE: + state->currently_displayed = true; + _update_display_mode(state); + _update_display(state); + break; + case EVENT_ALARM_BUTTON_UP: /* Start or increment. */ + /* Update `state->mode` in case we have a running counter + * that has just timed out. */ + _update_display_mode(state); + + switch (state->mode) { + case BABY_KICKS_MODE_SPLASH: + _start(state); + _update_display_mode(state); + _update_display(state); + _play_button_sound_if_beep_is_on(); + break; + case BABY_KICKS_MODE_ACTIVE: + _increment_counts(state); + _update_display(state); + _play_successful_increment_sound_if_beep_is_on(); + break; + case BABY_KICKS_MODE_TIMED_OUT: + _play_failure_sound_if_beep_is_on(); + break; + case BABY_KICKS_MODE_LE_MODE: /* fallthrough */ + default: + break; + } + break; + case EVENT_ALARM_LONG_PRESS: /* Undo. */ + _update_display_mode(state); + + switch (state->mode) { + case BABY_KICKS_MODE_ACTIVE: + if (!_successfully_undo(state)) { + _play_failure_sound_if_beep_is_on(); + } else { + _update_display(state); + _play_successful_decrement_sound_if_beep_is_on(); + } + break; + case BABY_KICKS_MODE_SPLASH: /* fallthrough */ + case BABY_KICKS_MODE_TIMED_OUT: + _play_failure_sound_if_beep_is_on(); + break; + case BABY_KICKS_MODE_LE_MODE: /* fallthrough */ + default: + break; + } + break; + case EVENT_MODE_LONG_PRESS: /* Reset. */ + _update_display_mode(state); + + switch (state->mode) { + case BABY_KICKS_MODE_ACTIVE: /* fallthrough */ + case BABY_KICKS_MODE_TIMED_OUT: + _reset(state); + + /* This shows the splash screen because `_reset` + * sets `state->mode` to `BABY_KICKS_MODE_SPLASH`. + */ + _update_display(state); + + _play_button_sound_if_beep_is_on(); + break; + case BABY_KICKS_MODE_SPLASH: + _play_failure_sound_if_beep_is_on(); + break; + case BABY_KICKS_MODE_LE_MODE: /* fallthrough */ + default: + break; + } + break; + case EVENT_BACKGROUND_TASK: /* Update minute display. */ + _update_display_mode(state); + + switch (state->mode) { + case BABY_KICKS_MODE_ACTIVE: /* fallthrough */ + case BABY_KICKS_MODE_TIMED_OUT: + if (state->currently_displayed) { + _display_elapsed_minutes(state); + } + break; + case BABY_KICKS_MODE_LE_MODE: /* fallthrough */ + case BABY_KICKS_MODE_SPLASH: /* fallthrough */ + default: + break; + } + break; + case EVENT_LOW_ENERGY_UPDATE: + _start_sleep_face(); + break; + default: + movement_default_loop_handler(event); + break; + } + + _clear_now(state); + + return true; +} + +movement_watch_face_advisory_t baby_kicks_face_advise(void *context) { + movement_watch_face_advisory_t retval = { 0 }; + baby_kicks_state_t *state = (baby_kicks_state_t *)context; + + retval.wants_background_task = + state->mode == BABY_KICKS_MODE_ACTIVE; + + return retval; +} diff --git a/watch-faces/complication/baby_kicks_face.h b/watch-faces/complication/baby_kicks_face.h new file mode 100644 index 00000000..c20a082a --- /dev/null +++ b/watch-faces/complication/baby_kicks_face.h @@ -0,0 +1,132 @@ +/* + * MIT License + * + * Copyright (c) 2025 Gábor Nyéki + * + * 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 + +/* + * Baby kicks face + * + * Count the movements of your in-utero baby. + * + * Background: + * + * This practice is recommended particularly in the third trimester + * (from week 28 onwards). The exact recommendations vary as to how to + * count the baby's movements. Some recommend drawing a chart with the + * number of "kicks" within a 12-hour period: + * + * - https://en.wikipedia.org/wiki/Kick_chart + * + * Others recommend measuring the time that it takes for the baby to + * "kick" 10 times: + * + * - https://my.clevelandclinic.org/health/articles/23497-kick-counts + * - https://healthy.kaiserpermanente.org/health-wellness/health-encyclopedia/he.pregnancy-kick-counts.aa107042 + * + * (Of course, not every movement that the baby makes is a kick, and we + * are interested in all movements, not only kicks.) + * + * This watch face follows the latter set of recommendations. When you + * start the counter, it measures the number of elapsed minutes, and it + * tracks the number of movements as you increment the counter. Since + * some consecutive movements made by the baby are actually part of a + * longer maneuver, the watch face also displays the number of + * one-minute stretches in which the baby moved at least once. + * + * Usage: + * + * - ALARM button, short press: + * * start the counter if it isn't running + * * increment the count otherwise + * - ALARM button, long press: undo the last count + * - MODE button, long press: reset the count to zero + * + * The watch face displays two numbers in the "clock digits" positions: + * + * 1. Count of movements (in the "second" and "minute" positions). + * 2. Count of one-minute stretches in which at least one movement + * occurred (in the "hour" position). + * + * The number of elapsed minutes, up to and including 29, is shown in + * the "day digits" position. Due to the limitations of the classic LCD + * display, completed 30-minute intervals are shown in the "weekday + * digits" position. The total number of elapsed minutes is the sum of + * these two numbers. + * + * The watch face times out after 99 minutes, since it cannot display + * more than 99 one-minute stretches in the "hour" position. When this + * happens, the "weekday digits" position shows "TO". + */ + +#include "movement.h" + +typedef enum { + BABY_KICKS_MODE_SPLASH = 0, + BABY_KICKS_MODE_ACTIVE, + BABY_KICKS_MODE_TIMED_OUT, + BABY_KICKS_MODE_LE_MODE, +} baby_kicks_mode_t; + +/* Stop counting after 99 minutes. The classic LCD cannot display any + * larger number in the "weekday digits" position. */ +#define BABY_KICKS_TIMEOUT 99 + +/* Ring buffer to store and allow undoing up to 10 movements. */ +typedef struct { + /* For each movement in the undo buffer, this array stores the value + * of `state->stretch_count` right before the movement was + * recorded. This is used for decrementing `state->stretch_count` + * as part of the undo operation if necessary. */ + uint8_t stretches[10]; + + /* Index of the next available slot in `.stretches`. */ + uint8_t head; +} baby_kicks_undo_buffer_t; + +typedef struct { + bool currently_displayed; + baby_kicks_mode_t mode; + watch_date_time_t now; + uint32_t start; + uint32_t latest_stretch_start; + uint8_t stretch_count; /* Between 0 and `BABY_KICKS_TIMEOUT`. */ + uint16_t movement_count; /* Between 0 and 9999. */ + baby_kicks_undo_buffer_t undo_buffer; +} baby_kicks_state_t; + +void baby_kicks_face_setup(uint8_t watch_face_index, void **context_ptr); +void baby_kicks_face_activate(void *context); +bool baby_kicks_face_loop(movement_event_t event, void *context); +void baby_kicks_face_resign(void *context); +movement_watch_face_advisory_t baby_kicks_face_advise(void *context); + +#define baby_kicks_face ((const watch_face_t) { \ + baby_kicks_face_setup, \ + baby_kicks_face_activate, \ + baby_kicks_face_loop, \ + baby_kicks_face_resign, \ + baby_kicks_face_advise, \ +}) From 3baff2f5a7989387e1f5bfe6be2e0a7e51c86dae Mon Sep 17 00:00:00 2001 From: Lorenzo Prosseda Date: Mon, 4 Aug 2025 11:32:32 +0200 Subject: [PATCH 21/92] Avoid confusion between T and E on classic display In place of T, display a + character in positions 4 and 6 --- watch-library/shared/watch/watch_common_display.c | 1 + 1 file changed, 1 insertion(+) diff --git a/watch-library/shared/watch/watch_common_display.c b/watch-library/shared/watch/watch_common_display.c index ed558a1b..86ab352d 100644 --- a/watch-library/shared/watch/watch_common_display.c +++ b/watch-library/shared/watch/watch_common_display.c @@ -57,6 +57,7 @@ void watch_display_character(uint8_t character, uint8_t position) { else if (character == 'c') character = 'C'; // C needs to be uppercase else if (character == 'J') character = 'j'; // same else if (character == 'v' || character == 'V' || character == 'U' || character == 'W' || character == 'w') character = 'u'; // bottom segment duplicated, so show in top half + else if (character == 't' || character == 'T') character = '+'; // avoid confusion with uppercase E } else { if (character == 'u') character = 'v'; // we can use the bottom segment; move to lower half else if (character == 'j') character = 'J'; // same but just display a normal J From aae792619179431bfeb516b3ae8817a0b8cf4563 Mon Sep 17 00:00:00 2001 From: Joey Castillo Date: Mon, 4 Aug 2025 07:16:37 -0400 Subject: [PATCH 22/92] remove outdated guidance from watch face template --- template/template.c | 5 ----- watch-faces/clock/ke_decimal_time_face.c | 5 ----- 2 files changed, 10 deletions(-) diff --git a/template/template.c b/template/template.c index 4f2e7e1b..cf51d778 100644 --- a/template/template.c +++ b/template/template.c @@ -81,11 +81,6 @@ bool <#watch_face_name#>_face_loop(movement_event_t event, void *context) { } // return true if the watch can enter standby mode. Generally speaking, you should always return true. - // Exceptions: - // * If you are displaying a color using the low-level watch_set_led_color function, you should return false. - // * If you are sounding the buzzer using the low-level watch_set_buzzer_on function, you should return false. - // Note that if you are driving the LED or buzzer using Movement functions like movement_illuminate_led or - // movement_play_alarm, you can still return true. This guidance only applies to the low-level watch_ functions. return true; } diff --git a/watch-faces/clock/ke_decimal_time_face.c b/watch-faces/clock/ke_decimal_time_face.c index a693aa5a..c2e9961b 100644 --- a/watch-faces/clock/ke_decimal_time_face.c +++ b/watch-faces/clock/ke_decimal_time_face.c @@ -130,11 +130,6 @@ bool ke_decimal_time_face_loop(movement_event_t event, void *context) { } // return true if the watch can enter standby mode. Generally speaking, you should always return true. - // Exceptions: - // * If you are displaying a color using the low-level watch_set_led_color function, you should return false. - // * If you are sounding the buzzer using the low-level watch_set_buzzer_on function, you should return false. - // Note that if you are driving the LED or buzzer using Movement functions like movement_illuminate_led or - // movement_play_alarm, you can still return true. This guidance only applies to the low-level watch_ functions. return true; } From 86fd87a3268cdc235a729411828be5b71992205a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20K=C3=B6finger?= <4404414+michaelk99@users.noreply.github.com> Date: Mon, 4 Aug 2025 23:38:38 +0200 Subject: [PATCH 23/92] watch-faces.mk: add totp_face.c, got lost while adding totp_lfs_face --- watch-faces.mk | 1 + 1 file changed, 1 insertion(+) diff --git a/watch-faces.mk b/watch-faces.mk index 906806d9..22e262cf 100644 --- a/watch-faces.mk +++ b/watch-faces.mk @@ -14,6 +14,7 @@ SRCS += \ ./watch-faces/complication/days_since_face.c \ ./watch-faces/complication/breathing_face.c \ ./watch-faces/complication/squash_face.c \ + ./watch-faces/complication/totp_face.c \ ./watch-faces/complication/totp_lfs_face.c \ ./watch-faces/complication/tally_face.c \ ./watch-faces/complication/wordle_face.c \ From d0ce60111eb74e0965b1ae3dbf6bee7308778205 Mon Sep 17 00:00:00 2001 From: Eli Dowling Date: Fri, 8 Aug 2025 22:23:33 +1000 Subject: [PATCH 24/92] 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 25/92] 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 26/92] 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 27/92] 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 28/92] 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 29/92] 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 30/92] 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 31/92] 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 32/92] 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 33/92] 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 34/92] 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 35/92] 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 47c62f66fdd1687bc71037c056246a4f45dc0f20 Mon Sep 17 00:00:00 2001 From: David Volovskiy Date: Sun, 17 Aug 2025 12:54:33 -0400 Subject: [PATCH 36/92] 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 37/92] 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 38/92] 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 39/92] 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 40/92] 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 41/92] 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 42/92] 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 95df9a66838ae2ae3dd6dc492a0bd3ffb05a18ad Mon Sep 17 00:00:00 2001 From: David Volovskiy Date: Tue, 19 Aug 2025 20:16:43 -0400 Subject: [PATCH 43/92] 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 44/92] 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 45/92] 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 46/92] 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 47/92] 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 48/92] 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 49/92] 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 50/92] 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 877d9721903916680d42e6dd21a0b4cd9534eec6 Mon Sep 17 00:00:00 2001 From: David Volovskiy Date: Sat, 23 Aug 2025 11:45:53 -0400 Subject: [PATCH 51/92] 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 5ecc3d6384b5143be7a19b920be0eabe719a076f Mon Sep 17 00:00:00 2001 From: Joey Castillo Date: Sun, 24 Aug 2025 08:28:50 -0400 Subject: [PATCH 52/92] increase delay before checking VBUS_DET --- movement.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/movement.c b/movement.c index 0046a489..ca0b0f36 100644 --- a/movement.c +++ b/movement.c @@ -610,7 +610,7 @@ void app_init(void) { // check if we are plugged into USB power. HAL_GPIO_VBUS_DET_in(); HAL_GPIO_VBUS_DET_pulldown(); - delay_ms(10); + delay_ms(100); if (HAL_GPIO_VBUS_DET_read()){ /// if so, enable USB functionality. _watch_enable_usb(); From 018c94f23d9407cc99bae030137bc0391aa11966 Mon Sep 17 00:00:00 2001 From: David Volovskiy Date: Sun, 24 Aug 2025 08:58:49 -0400 Subject: [PATCH 53/92] 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 54/92] 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 55/92] 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 56/92] 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 57/92] 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 58/92] 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 59/92] 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 60/92] 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 61/92] 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 62/92] 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 63/92] 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 64/92] 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 65/92] 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 838aa1e6f701e2d8b832f64c083261aae749a368 Mon Sep 17 00:00:00 2001 From: David Volovskiy Date: Tue, 2 Sep 2025 21:34:29 -0400 Subject: [PATCH 66/92] 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 67/92] 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 68/92] 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 69/92] 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 70/92] 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 71/92] 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 72/92] 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 73/92] 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 74/92] 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 75/92] 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 76/92] 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 77/92] 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 78/92] 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 79/92] 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 80/92] 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 81/92] 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 82/92] 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 83/92] 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 84/92] 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 85/92] 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 86/92] 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 6609b3f79a6a45ace613f9d803bf6b3d686a5f47 Mon Sep 17 00:00:00 2001 From: David Volovskiy Date: Sat, 15 Nov 2025 12:48:16 -0500 Subject: [PATCH 87/92] 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 437e513d1f416b2df2901e636263bd7d5488fec4 Mon Sep 17 00:00:00 2001 From: David Volovskiy Date: Sun, 16 Nov 2025 08:28:46 -0500 Subject: [PATCH 88/92] 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 89/92] 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 90/92] 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 91/92] 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 92/92] 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;