From 1b18b6a077a3c7c537df4fbd0e3f38e161f699dd Mon Sep 17 00:00:00 2001 From: James Haggerty Date: Tue, 2 Jan 2024 20:33:24 +1100 Subject: [PATCH 1/2] Easter egg for simple clock face Hold down the ALARM button to show SENSOR -> HACKED, and display inverts. This appears to have some weird bug where it will occasionally end up just displaying hacked; seems to do with being in sleep mode, and presumably getting somehow stuck in an active state. --- .../watch_faces/clock/simple_clock_face.c | 164 ++++++++++++++++++ 1 file changed, 164 insertions(+) diff --git a/movement/watch_faces/clock/simple_clock_face.c b/movement/watch_faces/clock/simple_clock_face.c index 35984472a..ad8c55896 100644 --- a/movement/watch_faces/clock/simple_clock_face.c +++ b/movement/watch_faces/clock/simple_clock_face.c @@ -28,6 +28,18 @@ #include "watch_utility.h" #include "watch_private_display.h" +// Defining the EASTER_EGG means that very long presses on 'ALARM' on this face +// show these words on the screen, and once the end is reached the watch display +// inverts. If EASTER_EGG is not defined, all of this code path is not included. +// If there's more than one string in the easter egg array, after completion +// the display will 'flip' all the characters. To undo this, start the easter +// egg again (but don't let it complete). +#define EASTER_EGG {" SENSOR ", " HACKED"} + +#ifdef EASTER_EGG +static bool easter_egg_loop(movement_event_type_t event_type, uint8_t watch_face_index); +#endif + static void _update_alarm_indicator(bool settings_alarm_enabled, simple_clock_state_t *state) { state->alarm_enabled = settings_alarm_enabled; if (state->alarm_enabled) watch_set_indicator(WATCH_INDICATOR_SIGNAL); @@ -71,6 +83,17 @@ bool simple_clock_face_loop(movement_event_t event, movement_settings_t *setting char buf[11]; uint8_t pos; + #ifdef EASTER_EGG + // If easter egg mode is enabled, then first see if the easter + // egg wants to do anything before our normal processing. + if (easter_egg_loop(event.event_type, state->watch_face_index)) { + // It's returned true. This means it's currently doing something weird + // on the screen, so (a) we shouldn't pass to the normal logic and (b) the watch + // shouldn't enter standby. + return false; + } + #endif + watch_date_time date_time; uint32_t previous_date_time; switch (event.event_type) { @@ -159,3 +182,144 @@ bool simple_clock_face_wants_background_task(movement_settings_t *settings, void return date_time.unit.minute == 0; } + +#ifdef EASTER_EGG + +#define DISPLAY_WIDTH 10 + +static bool easter_egg_display_string_morph(char *old_string_arg, char *new_string_arg) { + static char *old_string = NULL; + static char *new_string = NULL; + static uint8_t old_segdata[DISPLAY_WIDTH]; + static uint8_t new_segdata[DISPLAY_WIDTH]; + static size_t position = 0; + size_t i; + + if (old_string_arg == NULL && new_string_arg == NULL) { + old_string = new_string = NULL; + return false; + } + + if (old_string != old_string_arg) { + position = 0; + old_string = old_string_arg; + for (i = 0; old_string[i] && i < DISPLAY_WIDTH; ++i) { + old_segdata[i] = watch_convert_char_to_segdata(old_string[i], i); + } + for (; i < DISPLAY_WIDTH; ++i) { + old_segdata[i] = watch_convert_char_to_segdata(' ', i); + } + } + if (new_string != new_string_arg) { + position = 0; + new_string = new_string_arg; + for (i = 0; new_string[i] && i < DISPLAY_WIDTH; ++i) { + new_segdata[i] = watch_convert_char_to_segdata(new_string[i], i); + } + for (; i < DISPLAY_WIDTH; ++i) { + new_segdata[i] = watch_convert_char_to_segdata(' ', i); + } + } + + // Make old_segdata approach new_segdata by the first bit we find. + for (; position < DISPLAY_WIDTH; ++position) { + if (old_segdata[position] == new_segdata[position]) { + continue; + } + + for (int bit_pos = 0; bit_pos < 8; ++bit_pos) { + int bit = 1 << bit_pos; + + if ((old_segdata[position] & bit) != (new_segdata[position] & bit)) { + old_segdata[position] ^= bit; + watch_display_segment(bit_pos, position, old_segdata[position] & bit); + // We only do one segment change. Call again to keep morphing. + return true; + } + } + } + + // This cleans up the extra tweaks that wouldn't have been done in the + // segment map. + watch_display_string(new_string, 0); + return false; +} + +static char *egg_states[] = EASTER_EGG; +static const size_t egg_states_length = sizeof(egg_states) / sizeof(egg_states[0]); +static const int32_t easter_egg_delay = 3; + +static bool easter_egg_loop(movement_event_type_t event_type, uint8_t watch_face_index) { + static int32_t egg_state_index = -1; + static uint32_t alarm_button_down_time = 0; + static bool morphing = false; + + if (alarm_button_down_time == 0 && event_type != EVENT_ALARM_BUTTON_DOWN) { + // Nothing to do here folks. Avoid any unnecessary processing. + return false; + } + + watch_date_time date_time = watch_rtc_get_date_time(); + int32_t time_since_alarm_button = date_time.reg - alarm_button_down_time; + + switch (event_type) { + case EVENT_ACTIVATE: + case EVENT_LOW_ENERGY_UPDATE: + case EVENT_ALARM_BUTTON_UP: + case EVENT_ALARM_LONG_UP: + // Either a button press ended, or we've somehow still got a button down + // time after more interesting things have happened. Clean stuff up. + if (egg_state_index != -1) { + movement_move_to_face(watch_face_index); + easter_egg_display_string_morph(NULL, NULL); + egg_state_index = -1; + morphing = false; + movement_request_tick_frequency(1); + } + alarm_button_down_time = 0; + break; + case EVENT_ALARM_BUTTON_DOWN: + alarm_button_down_time = date_time.reg; + break; + case EVENT_TICK: + if (egg_state_index == -1) { + if (time_since_alarm_button > easter_egg_delay) { + egg_state_index = 0; + watch_display_invert(false); + watch_clear_colon(); + watch_clear_all_indicators(); + watch_display_string(egg_states[egg_state_index], 0); + } + } else if (egg_state_index >= 0) { + if (morphing) { + morphing = easter_egg_display_string_morph(egg_states[egg_state_index - 1], egg_states[egg_state_index]); + if (!morphing) { // Finished changing to new string. + movement_request_tick_frequency(1); + + if (egg_state_index + 1 >= (int32_t)egg_states_length) { + // We've finished our animation after the final morph. Flip! + watch_display_invert(true); + // And display so people are immediately confused. + watch_display_string(egg_states[egg_state_index], 0); + } + } + } + + if (egg_state_index + 1 < (int32_t)egg_states_length && time_since_alarm_button > easter_egg_delay * (egg_state_index + 2)) { + // In case we didn't finish the morph, set it here. + if (morphing) { + watch_display_string(egg_states[egg_state_index], 0); + } + egg_state_index += 1; + movement_request_tick_frequency(16); + morphing = true; + } + } + break; + default: + break; + } + + return egg_state_index != -1; +} +#endif // EASTER_EGG From f38ac8a00f348f47e6fba0e6e88068a255c87676 Mon Sep 17 00:00:00 2001 From: James Haggerty Date: Sun, 21 Jul 2024 08:47:21 +1000 Subject: [PATCH 2/2] Handle missing button interrupt. --- movement/watch_faces/clock/simple_clock_face.c | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/movement/watch_faces/clock/simple_clock_face.c b/movement/watch_faces/clock/simple_clock_face.c index ad8c55896..af3958ec9 100644 --- a/movement/watch_faces/clock/simple_clock_face.c +++ b/movement/watch_faces/clock/simple_clock_face.c @@ -262,6 +262,14 @@ static bool easter_egg_loop(movement_event_type_t event_type, uint8_t watch_face watch_date_time date_time = watch_rtc_get_date_time(); int32_t time_since_alarm_button = date_time.reg - alarm_button_down_time; + // Sometimes, events are missed. This is particularly bad in this face + // since the alarm release flips us back to the normal face. Since we should + // never get a 'tick' if the alarm button is already up, detect this + // and terminate the fake face. + if (event_type == EVENT_TICK && !watch_get_pin_level(BTN_ALARM)) { + event_type = EVENT_ALARM_BUTTON_UP; + } + switch (event_type) { case EVENT_ACTIVATE: case EVENT_LOW_ENERGY_UPDATE: