]> git.earman.xyz Git - sensor-watch.git/commitdiff
first build of Second Movement with two watch faces
authorjoeycastillo <joeycastillo@utexas.edu>
Wed, 18 Sep 2024 22:22:33 +0000 (18:22 -0400)
committerjoeycastillo <joeycastillo@utexas.edu>
Wed, 18 Sep 2024 22:22:33 +0000 (18:22 -0400)
17 files changed:
Makefile
app.c [deleted file]
movement.c [new file with mode: 0644]
movement.h [new file with mode: 0644]
movement/movement.c [deleted file]
movement/movement.h [deleted file]
movement/watch_faces/clock/simple_clock_face.c [deleted file]
movement/watch_faces/clock/simple_clock_face.h [deleted file]
movement/watch_faces/settings/set_time_face.c [deleted file]
movement/watch_faces/settings/set_time_face.h [deleted file]
movement_config.h [new file with mode: 0644]
movement_faces.h [new file with mode: 0644]
watch-faces.mk [new file with mode: 0644]
watch-faces/clock/simple_clock_face.c [new file with mode: 0644]
watch-faces/clock/simple_clock_face.h [new file with mode: 0644]
watch-faces/settings/set_time_face.c [new file with mode: 0644]
watch-faces/settings/set_time_face.h [new file with mode: 0644]

index e1c98e91ee4c5fc1117cdf502ce92ce33e157184..0210e959e9b7d5a2d8f51d13b61f8f91d3d67920 100644 (file)
--- a/Makefile
+++ b/Makefile
@@ -27,14 +27,18 @@ endif
 
 # Add your include directories here.
 INCLUDES += \
+  -I./ \
   -I./tinyusb/src \
   -I./watch-library/shared/watch \
   -I./watch-library/hardware/watch \
+  -I./watch-faces/clock \
+  -I./watch-faces/settings \
 
 # Add your source files here.
 SRCS += \
   ./watch-library/shared/watch/watch_common_buzzer.c \
   ./watch-library/shared/watch/watch_common_display.c \
+  ./watch-library/shared/watch/watch_utility.c \
   ./watch-library/hardware/watch/watch.c \
   ./watch-library/hardware/watch/watch_adc.c \
   ./watch-library/hardware/watch/watch_deepsleep.c \
@@ -47,7 +51,11 @@ SRCS += \
   ./watch-library/hardware/watch/watch_tcc.c \
   ./watch-library/hardware/watch/watch_usb_descriptors.c \
   ./watch-library/hardware/watch/watch_usb_cdc.c \
-  ./app.c \
+
+include watch-faces.mk
+
+SRCS += \
+  ./movement.c \
 
 # Finally, leave this line at the bottom of the file.
 include $(GOSSAMER_PATH)/rules.mk
diff --git a/app.c b/app.c
deleted file mode 100644 (file)
index 29a77d9..0000000
--- a/app.c
+++ /dev/null
@@ -1,102 +0,0 @@
-#include "app.h"
-#include "watch.h"
-#include "watch_private.h"
-#include "delay.h"
-#include "usb.h"
-#include "tusb.h"
-#include "watch_usb_cdc.h"
-#include "tcc.h"
-
-uint8_t ticks = 0;
-bool beep = false;
-
-void cb_tick(void);
-void cb_mode_btn_interrupt(void);
-void cb_light_btn_interrupt(void);
-void cb_alarm_btn_extwake(void);
-
-void yield(void) {
-    tud_task();
-    cdc_task();
-}
-
-void app_init(void) {
-    // initialize the watch hardware.
-    _watch_init();
-
-    // check if we are plugged into USB power.
-    HAL_GPIO_VBUS_DET_in();
-    HAL_GPIO_VBUS_DET_pulldown();
-    if (HAL_GPIO_VBUS_DET_read()){
-        /// if so, enable USB functionality.
-        _watch_enable_usb();
-    }
-    HAL_GPIO_VBUS_DET_off();
-}
-
-void app_setup(void) {
-    watch_enable_leds();
-    watch_enable_external_interrupts();
-
-    HAL_GPIO_BTN_LIGHT_in();
-    HAL_GPIO_BTN_LIGHT_pulldown();
-    HAL_GPIO_BTN_LIGHT_pmuxen(HAL_GPIO_PMUX_EIC);
-    HAL_GPIO_BTN_MODE_in();
-    HAL_GPIO_BTN_MODE_pulldown();
-    HAL_GPIO_BTN_MODE_pmuxen(HAL_GPIO_PMUX_EIC);
-
-    // Simple test sketch exercises RTC and EIC, plus LEDs, screen and buzzer.
-    // Periodic callback increments the tick counter.
-    watch_rtc_register_periodic_callback(cb_tick, 1);
-    // Light button turns on the LED.
-    watch_register_interrupt_callback(HAL_GPIO_BTN_LIGHT_pin(), cb_light_btn_interrupt, INTERRUPT_TRIGGER_RISING);
-    // Mode button beeps the piezo.
-    watch_register_interrupt_callback(HAL_GPIO_BTN_MODE_pin(), cb_mode_btn_interrupt, INTERRUPT_TRIGGER_RISING);
-    // Alarm buttton resets the tick counter.
-    watch_register_extwake_callback(HAL_GPIO_BTN_ALARM_pin(), cb_alarm_btn_extwake, true);
-
-    watch_enable_display();
-    watch_display_top_left("WA");
-    watch_display_top_right(" 0");
-    watch_display_main_line(" test ");
-}
-
-bool app_loop(void) {
-    if (usb_is_enabled()) {
-        yield();
-    }
-
-    if (beep) {
-        watch_buzzer_play_note(BUZZER_NOTE_C8, 100);
-        beep = false;
-    }
-
-    char buf[4];
-    sprintf(buf, "%2d", ticks);
-    printf("ticks: %d\n", ticks);
-    watch_display_top_right(buf);
-
-    if (ticks >= 30) {
-        watch_enter_sleep_mode();
-    }
-
-    return !usb_is_enabled();
-}
-
-void cb_tick(void) {
-    ticks = ticks + 1;
-    watch_set_led_off();
-}
-
-void cb_light_btn_interrupt(void) {
-    watch_set_led_green();
-}
-
-void cb_mode_btn_interrupt(void) {
-    beep = true;
-}
-
-void cb_alarm_btn_extwake(void) {
-    watch_set_led_red();
-    ticks = 0;
-}
\ No newline at end of file
diff --git a/movement.c b/movement.c
new file mode 100644 (file)
index 0000000..36f34a5
--- /dev/null
@@ -0,0 +1,722 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2022 Joey Castillo
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+#define MOVEMENT_LONG_PRESS_TICKS 64
+
+#include <stdio.h>
+#include <string.h>
+#include <limits.h>
+#include <string.h>
+#include <stdlib.h>
+#include <stdio.h>
+#include "app.h"
+#include "watch.h"
+#include "usb.h"
+#include "movement.h"
+
+/// FIXMME: #SecondMovement needs to bring back the following includes (and remove the default signal_tune)
+// #include "filesystem.h"
+// #include "shell.h"
+// #include "movement_custom_signal_tunes.h"
+int8_t signal_tune[] = {
+    BUZZER_NOTE_C8, 5,
+    BUZZER_NOTE_REST, 6,
+    BUZZER_NOTE_C8, 5,
+    0
+};
+
+
+#include "movement_config.h"
+
+
+// Default to no secondary face behaviour.
+#ifndef MOVEMENT_SECONDARY_FACE_INDEX
+#define MOVEMENT_SECONDARY_FACE_INDEX 0
+#endif
+
+// Set default LED colors if not set
+#ifndef MOVEMENT_DEFAULT_RED_COLOR
+#define MOVEMENT_DEFAULT_RED_COLOR 0x0
+#endif
+#ifndef MOVEMENT_DEFAULT_GREEN_COLOR
+#define MOVEMENT_DEFAULT_GREEN_COLOR 0xF
+#endif
+
+// Default to 12h mode
+#ifndef MOVEMENT_DEFAULT_24H_MODE
+#define MOVEMENT_DEFAULT_24H_MODE false
+#endif
+
+// Default to mode button sounding on press
+#ifndef MOVEMENT_DEFAULT_BUTTON_SOUND
+#define MOVEMENT_DEFAULT_BUTTON_SOUND true
+#endif
+
+// Default to switch back to main watch face after 60 seconds
+#ifndef MOVEMENT_DEFAULT_TIMEOUT_INTERVAL
+#define MOVEMENT_DEFAULT_TIMEOUT_INTERVAL 0
+#endif
+
+// Default to switch to low energy mode after 2 hours
+#ifndef MOVEMENT_DEFAULT_LOW_ENERGY_INTERVAL
+#define MOVEMENT_DEFAULT_LOW_ENERGY_INTERVAL 2
+#endif
+
+// Default to 1 second led duration
+#ifndef MOVEMENT_DEFAULT_LED_DURATION
+#define MOVEMENT_DEFAULT_LED_DURATION 1
+#endif
+
+#if __EMSCRIPTEN__
+#include <emscripten.h>
+#endif
+
+movement_state_t movement_state;
+void * watch_face_contexts[MOVEMENT_NUM_FACES];
+watch_date_time scheduled_tasks[MOVEMENT_NUM_FACES];
+const int32_t movement_le_inactivity_deadlines[8] = {INT_MAX, 600, 3600, 7200, 21600, 43200, 86400, 604800};
+const int16_t movement_timeout_inactivity_deadlines[4] = {60, 120, 300, 1800};
+movement_event_t event;
+
+const int16_t movement_timezone_offsets[] = {
+    0,      //  0 :   0:00:00 (UTC)
+    60,     //  1 :   1:00:00 (Central European Time)
+    120,    //  2 :   2:00:00 (South African Standard Time)
+    180,    //  3 :   3:00:00 (Arabia Standard Time)
+    210,    //  4 :   3:30:00 (Iran Standard Time)
+    240,    //  5 :   4:00:00 (Georgia Standard Time)
+    270,    //  6 :   4:30:00 (Afghanistan Time)
+    300,    //  7 :   5:00:00 (Pakistan Standard Time)
+    330,    //  8 :   5:30:00 (Indian Standard Time)
+    345,    //  9 :   5:45:00 (Nepal Time)
+    360,    // 10 :   6:00:00 (Kyrgyzstan time)
+    390,    // 11 :   6:30:00 (Myanmar Time)
+    420,    // 12 :   7:00:00 (Thailand Standard Time)
+    480,    // 13 :   8:00:00 (China Standard Time, Australian Western Standard Time)
+    525,    // 14 :   8:45:00 (Australian Central Western Standard Time)
+    540,    // 15 :   9:00:00 (Japan Standard Time, Korea Standard Time)
+    570,    // 16 :   9:30:00 (Australian Central Standard Time)
+    600,    // 17 :  10:00:00 (Australian Eastern Standard Time)
+    630,    // 18 :  10:30:00 (Lord Howe Standard Time)
+    660,    // 19 :  11:00:00 (Solomon Islands Time)
+    720,    // 20 :  12:00:00 (New Zealand Standard Time)
+    765,    // 21 :  12:45:00 (Chatham Standard Time)
+    780,    // 22 :  13:00:00 (Tonga Time)
+    825,    // 23 :  13:45:00 (Chatham Daylight Time)
+    840,    // 24 :  14:00:00 (Line Islands Time)
+    -720,   // 25 : -12:00:00 (Baker Island Time)
+    -660,   // 26 : -11:00:00 (Niue Time)
+    -600,   // 27 : -10:00:00 (Hawaii-Aleutian Standard Time)
+    -570,   // 28 :  -9:30:00 (Marquesas Islands Time)
+    -540,   // 29 :  -9:00:00 (Alaska Standard Time)
+    -480,   // 30 :  -8:00:00 (Pacific Standard Time)
+    -420,   // 31 :  -7:00:00 (Mountain Standard Time)
+    -360,   // 32 :  -6:00:00 (Central Standard Time)
+    -300,   // 33 :  -5:00:00 (Eastern Standard Time)
+    -270,   // 34 :  -4:30:00 (Venezuelan Standard Time)
+    -240,   // 35 :  -4:00:00 (Atlantic Standard Time)
+    -210,   // 36 :  -3:30:00 (Newfoundland Standard Time)
+    -180,   // 37 :  -3:00:00 (Brasilia Time)
+    -150,   // 38 :  -2:30:00 (Newfoundland Daylight Time)
+    -120,   // 39 :  -2:00:00 (Fernando de Noronha Time)
+    -60,    // 40 :  -1:00:00 (Azores Standard Time)
+};
+
+const char movement_valid_position_0_chars[] = " AaBbCcDdEeFGgHhIiJKLMNnOoPQrSTtUuWXYZ-='+\\/0123456789";
+const char movement_valid_position_1_chars[] = " ABCDEFHlJLNORTtUX-='01378";
+
+void cb_mode_btn_interrupt(void);
+void cb_light_btn_interrupt(void);
+void cb_alarm_btn_interrupt(void);
+void cb_alarm_btn_extwake(void);
+void cb_alarm_fired(void);
+void cb_fast_tick(void);
+void cb_tick(void);
+
+static inline void _movement_reset_inactivity_countdown(void) {
+    movement_state.le_mode_ticks = movement_le_inactivity_deadlines[movement_state.settings.bit.le_interval];
+    movement_state.timeout_ticks = movement_timeout_inactivity_deadlines[movement_state.settings.bit.to_interval];
+}
+
+static inline void _movement_enable_fast_tick_if_needed(void) {
+    if (!movement_state.fast_tick_enabled) {
+        movement_state.fast_ticks = 0;
+        watch_rtc_register_periodic_callback(cb_fast_tick, 128);
+        movement_state.fast_tick_enabled = true;
+    }
+}
+
+static inline void _movement_disable_fast_tick_if_possible(void) {
+    if ((movement_state.light_ticks == -1) &&
+        (movement_state.alarm_ticks == -1) &&
+        ((movement_state.light_down_timestamp + movement_state.mode_down_timestamp + movement_state.alarm_down_timestamp) == 0)) {
+        movement_state.fast_tick_enabled = false;
+        watch_rtc_disable_periodic_callback(128);
+    }
+}
+
+static void _movement_handle_background_tasks(void) {
+    for(uint8_t i = 0; i < MOVEMENT_NUM_FACES; i++) {
+        // For each face, if the watch face wants a background task...
+        if (watch_faces[i].wants_background_task != NULL && watch_faces[i].wants_background_task(&movement_state.settings, watch_face_contexts[i])) {
+            // ...we give it one. pretty straightforward!
+            movement_event_t background_event = { EVENT_BACKGROUND_TASK, 0 };
+            watch_faces[i].loop(background_event, &movement_state.settings, watch_face_contexts[i]);
+        }
+    }
+    movement_state.needs_background_tasks_handled = false;
+}
+
+static void _movement_handle_scheduled_tasks(void) {
+    watch_date_time date_time = watch_rtc_get_date_time();
+    uint8_t num_active_tasks = 0;
+
+    for(uint8_t i = 0; i < MOVEMENT_NUM_FACES; i++) {
+        if (scheduled_tasks[i].reg) {
+            if (scheduled_tasks[i].reg <= date_time.reg) {
+                scheduled_tasks[i].reg = 0;
+                movement_event_t background_event = { EVENT_BACKGROUND_TASK, 0 };
+                watch_faces[i].loop(background_event, &movement_state.settings, watch_face_contexts[i]);
+                // check if loop scheduled a new task
+                if (scheduled_tasks[i].reg) {
+                    num_active_tasks++;
+                }
+            } else {
+                num_active_tasks++;
+            }
+        }
+    }
+
+    if (num_active_tasks == 0) {
+        movement_state.has_scheduled_background_task = false;
+    } else {
+        _movement_reset_inactivity_countdown();
+    }
+}
+
+void movement_request_tick_frequency(uint8_t freq) {
+    // Movement uses the 128 Hz tick internally
+    if (freq == 128) return;
+
+    // Movement requires at least a 1 Hz tick.
+    // If we are asked for an invalid frequency, default back to 1 Hz.
+    if (freq == 0 || __builtin_popcount(freq) != 1) freq = 1;
+
+    // disable all callbacks except the 128 Hz one
+    watch_rtc_disable_matching_periodic_callbacks(0xFE);
+
+    movement_state.subsecond = 0;
+    movement_state.tick_frequency = freq;
+    watch_rtc_register_periodic_callback(cb_tick, freq);
+}
+
+void movement_illuminate_led(void) {
+    if (movement_state.settings.bit.led_duration != 0b111) {
+        watch_set_led_color(movement_state.settings.bit.led_red_color ? (0xF | movement_state.settings.bit.led_red_color << 4) : 0,
+                            movement_state.settings.bit.led_green_color ? (0xF | movement_state.settings.bit.led_green_color << 4) : 0);
+        if (movement_state.settings.bit.led_duration == 0) {
+            movement_state.light_ticks = 1;
+        } else {
+            movement_state.light_ticks = (movement_state.settings.bit.led_duration * 2 - 1) * 128;
+        }
+        _movement_enable_fast_tick_if_needed();
+    }
+}
+
+static void _movement_led_off(void) {
+    watch_set_led_off();
+    movement_state.light_ticks = -1;
+    _movement_disable_fast_tick_if_possible();
+}
+
+bool movement_default_loop_handler(movement_event_t event, movement_settings_t *settings) {
+    (void)settings;
+
+    switch (event.event_type) {
+        case EVENT_MODE_BUTTON_UP:
+            movement_move_to_next_face();
+            break;
+        case EVENT_LIGHT_BUTTON_DOWN:
+            movement_illuminate_led();
+            break;
+        case EVENT_LIGHT_BUTTON_UP:
+            if (movement_state.settings.bit.led_duration == 0) {
+                _movement_led_off();
+            }
+            break;
+        case EVENT_MODE_LONG_PRESS:
+            if (MOVEMENT_SECONDARY_FACE_INDEX && movement_state.current_face_idx == 0) {
+                movement_move_to_face(MOVEMENT_SECONDARY_FACE_INDEX);
+            } else {
+                movement_move_to_face(0);
+            }
+            break;
+        default:
+            break;
+    }
+
+    return true;
+}
+
+void movement_move_to_face(uint8_t watch_face_index) {
+    movement_state.watch_face_changed = true;
+    movement_state.next_face_idx = watch_face_index;
+}
+
+void movement_move_to_next_face(void) {
+    uint16_t face_max;
+    if (MOVEMENT_SECONDARY_FACE_INDEX) {
+        face_max = (movement_state.current_face_idx < (int16_t)MOVEMENT_SECONDARY_FACE_INDEX) ? MOVEMENT_SECONDARY_FACE_INDEX : MOVEMENT_NUM_FACES;
+    } else {
+        face_max = MOVEMENT_NUM_FACES;
+    }
+    movement_move_to_face((movement_state.current_face_idx + 1) % face_max);
+}
+
+void movement_schedule_background_task(watch_date_time date_time) {
+    movement_schedule_background_task_for_face(movement_state.current_face_idx, date_time);
+}
+
+void movement_cancel_background_task(void) {
+    movement_cancel_background_task_for_face(movement_state.current_face_idx);
+}
+
+void movement_schedule_background_task_for_face(uint8_t watch_face_index, watch_date_time date_time) {
+    watch_date_time now = watch_rtc_get_date_time();
+    if (date_time.reg > now.reg) {
+        movement_state.has_scheduled_background_task = true;
+        scheduled_tasks[watch_face_index].reg = date_time.reg;
+    }
+}
+
+void movement_cancel_background_task_for_face(uint8_t watch_face_index) {
+    scheduled_tasks[watch_face_index].reg = 0;
+    bool other_tasks_scheduled = false;
+    for(uint8_t i = 0; i < MOVEMENT_NUM_FACES; i++) {
+        if (scheduled_tasks[i].reg != 0) {
+            other_tasks_scheduled = true;
+            break;
+        }
+    }
+    movement_state.has_scheduled_background_task = other_tasks_scheduled;
+}
+
+void movement_request_wake() {
+    movement_state.needs_wake = true;
+    _movement_reset_inactivity_countdown();
+}
+
+static void end_buzzing() {
+    movement_state.is_buzzing = false;
+}
+
+static void end_buzzing_and_disable_buzzer(void) {
+    end_buzzing();
+    watch_disable_buzzer();
+}
+
+static void set_initial_clock_mode(void) {
+#ifdef CLOCK_FACE_24H_ONLY
+    movement_state.settings.bit.clock_mode_24h = true;
+#else
+    movement_state.settings.bit.clock_mode_24h = MOVEMENT_DEFAULT_24H_MODE;
+#endif
+}
+
+void movement_play_signal(void) {
+    void *maybe_disable_buzzer = end_buzzing_and_disable_buzzer;
+    if (watch_is_buzzer_or_led_enabled()) {
+        maybe_disable_buzzer = end_buzzing;
+    } else {
+        watch_enable_buzzer();
+    }
+    movement_state.is_buzzing = true;
+    watch_buzzer_play_sequence(signal_tune, maybe_disable_buzzer);
+    if (movement_state.le_mode_ticks == -1) {
+        // the watch is asleep. wake it up for "1" round through the main loop.
+        // the sleep_mode_app_loop will notice the is_buzzing and note that it
+        // only woke up to beep and then it will spinlock until the callback
+        // turns off the is_buzzing flag.
+        movement_state.needs_wake = true;
+        movement_state.le_mode_ticks = 1;
+    }
+}
+
+void movement_play_alarm(void) {
+    movement_play_alarm_beeps(5, BUZZER_NOTE_C8);
+}
+
+void movement_play_alarm_beeps(uint8_t rounds, BuzzerNote alarm_note) {
+    if (rounds == 0) rounds = 1;
+    if (rounds > 20) rounds = 20;
+    movement_request_wake();
+    movement_state.alarm_note = alarm_note;
+    // our tone is 0.375 seconds of beep and 0.625 of silence, repeated as given.
+    movement_state.alarm_ticks = 128 * rounds - 75;
+    _movement_enable_fast_tick_if_needed();
+}
+
+uint8_t movement_claim_backup_register(void) {
+    if (movement_state.next_available_backup_register >= 8) return 0;
+    return movement_state.next_available_backup_register++;
+}
+
+void app_init(void) {
+#if defined(NO_FREQCORR)
+    watch_rtc_freqcorr_write(0, 0);
+#elif defined(WATCH_IS_BLUE_BOARD)
+    watch_rtc_freqcorr_write(11, 0);
+#else
+    watch_rtc_freqcorr_write(22, 0);
+#endif
+
+    memset(&movement_state, 0, sizeof(movement_state));
+    set_initial_clock_mode();
+    movement_state.settings.bit.led_red_color = MOVEMENT_DEFAULT_RED_COLOR;
+    movement_state.settings.bit.led_green_color = MOVEMENT_DEFAULT_GREEN_COLOR;
+    movement_state.settings.bit.button_should_sound = MOVEMENT_DEFAULT_BUTTON_SOUND;
+    movement_state.settings.bit.to_interval = MOVEMENT_DEFAULT_TIMEOUT_INTERVAL;
+    movement_state.settings.bit.le_interval = MOVEMENT_DEFAULT_LOW_ENERGY_INTERVAL;
+    movement_state.settings.bit.led_duration = MOVEMENT_DEFAULT_LED_DURATION;
+
+    movement_state.light_ticks = -1;
+    movement_state.alarm_ticks = -1;
+    movement_state.next_available_backup_register = 4;
+    _movement_reset_inactivity_countdown();
+
+/// FIXME: #SecondMovement needs filesystem support
+    // filesystem_init();
+
+#if __EMSCRIPTEN__
+    int32_t time_zone_offset = EM_ASM_INT({
+        return -new Date().getTimezoneOffset();
+    });
+    for (int i = 0, count = sizeof(movement_timezone_offsets) / sizeof(movement_timezone_offsets[0]); i < count; i++) {
+        if (movement_timezone_offsets[i] == time_zone_offset) {
+            movement_state.settings.bit.time_zone = i;
+            break;
+        }
+    }
+#endif
+}
+
+void app_wake_from_backup(void) {
+    movement_state.settings.reg = watch_get_backup_data(0);
+}
+
+void app_setup(void) {
+    watch_store_backup_data(movement_state.settings.reg, 0);
+
+    static bool is_first_launch = true;
+
+    if (is_first_launch) {
+        #ifdef MOVEMENT_CUSTOM_BOOT_COMMANDS
+        MOVEMENT_CUSTOM_BOOT_COMMANDS()
+        #endif
+
+        for(uint8_t i = 0; i < MOVEMENT_NUM_FACES; i++) {
+            watch_face_contexts[i] = NULL;
+            scheduled_tasks[i].reg = 0;
+            is_first_launch = false;
+        }
+
+        // set up the 1 minute alarm (for background tasks and low power updates)
+        watch_date_time alarm_time;
+        alarm_time.reg = 0;
+        alarm_time.unit.second = 59; // after a match, the alarm fires at the next rising edge of CLK_RTC_CNT, so 59 seconds lets us update at :00
+        watch_rtc_register_alarm_callback(cb_alarm_fired, alarm_time, ALARM_MATCH_SS);
+    }
+    if (movement_state.le_mode_ticks != -1) {
+        watch_disable_extwake_interrupt(HAL_GPIO_BTN_ALARM_pin());
+
+        watch_enable_external_interrupts();
+        watch_register_interrupt_callback(HAL_GPIO_BTN_MODE_pin(), cb_mode_btn_interrupt, INTERRUPT_TRIGGER_BOTH);
+        watch_register_interrupt_callback(HAL_GPIO_BTN_LIGHT_pin(), cb_light_btn_interrupt, INTERRUPT_TRIGGER_BOTH);
+        watch_register_interrupt_callback(HAL_GPIO_BTN_ALARM_pin(), cb_alarm_btn_interrupt, INTERRUPT_TRIGGER_BOTH);
+
+        watch_enable_buzzer();
+        watch_enable_leds();
+        watch_enable_display();
+
+        movement_request_tick_frequency(1);
+
+        for(uint8_t i = 0; i < MOVEMENT_NUM_FACES; i++) {
+            watch_faces[i].setup(&movement_state.settings, i, &watch_face_contexts[i]);
+        }
+
+        watch_faces[movement_state.current_face_idx].activate(&movement_state.settings, watch_face_contexts[movement_state.current_face_idx]);
+        event.subsecond = 0;
+        event.event_type = EVENT_ACTIVATE;
+    }
+}
+
+static void _sleep_mode_app_loop(void) {
+    movement_state.needs_wake = false;
+    // as long as le_mode_ticks is -1 (i.e. we are in low energy mode), we wake up here, update the screen, and go right back to sleep.
+    while (movement_state.le_mode_ticks == -1) {
+        // we also have to handle background tasks here in the mini-runloop
+        if (movement_state.needs_background_tasks_handled) _movement_handle_background_tasks();
+
+        event.event_type = EVENT_LOW_ENERGY_UPDATE;
+        watch_faces[movement_state.current_face_idx].loop(event, &movement_state.settings, watch_face_contexts[movement_state.current_face_idx]);
+
+        // if we need to wake immediately, do it!
+        if (movement_state.needs_wake) return;
+        // otherwise enter sleep mode, and when the extwake handler is called, it will reset le_mode_ticks and force us out at the next loop.
+        else watch_enter_sleep_mode();
+    }
+}
+
+bool app_loop(void) {
+    const watch_face_t *wf = &watch_faces[movement_state.current_face_idx];
+    bool woke_up_for_buzzer = false;
+    if (movement_state.watch_face_changed) {
+        if (movement_state.settings.bit.button_should_sound) {
+            // low note for nonzero case, high note for return to watch_face 0
+            watch_buzzer_play_note(movement_state.next_face_idx ? BUZZER_NOTE_C7 : BUZZER_NOTE_C8, 50);
+        }
+        wf->resign(&movement_state.settings, watch_face_contexts[movement_state.current_face_idx]);
+        movement_state.current_face_idx = movement_state.next_face_idx;
+        // we have just updated the face idx, so we must recache the watch face pointer.
+        wf = &watch_faces[movement_state.current_face_idx];
+        watch_clear_display();
+        movement_request_tick_frequency(1);
+        wf->activate(&movement_state.settings, watch_face_contexts[movement_state.current_face_idx]);
+        event.subsecond = 0;
+        event.event_type = EVENT_ACTIVATE;
+        movement_state.watch_face_changed = false;
+    }
+
+    // if the LED should be off, turn it off
+    if (movement_state.light_ticks == 0) {
+        // unless the user is holding down the LIGHT button, in which case, give them more time.
+        if (HAL_GPIO_BTN_LIGHT_read()) {
+            movement_state.light_ticks = 1;
+        } else {
+            _movement_led_off();
+        }
+    }
+
+    // handle background tasks, if the alarm handler told us we need to
+    if (movement_state.needs_background_tasks_handled) _movement_handle_background_tasks();
+
+    // if we have a scheduled background task, handle that here:
+    if (event.event_type == EVENT_TICK && movement_state.has_scheduled_background_task) _movement_handle_scheduled_tasks();
+
+    // if we have timed out of our low energy mode countdown, enter low energy mode.
+    if (movement_state.le_mode_ticks == 0) {
+        movement_state.le_mode_ticks = -1;
+        watch_register_extwake_callback(HAL_GPIO_BTN_ALARM_pin(), cb_alarm_btn_extwake, true);
+        event.event_type = EVENT_NONE;
+        event.subsecond = 0;
+
+        // _sleep_mode_app_loop takes over at this point and loops until le_mode_ticks is reset by the extwake handler,
+        // or wake is requested using the movement_request_wake function.
+        _sleep_mode_app_loop();
+        // as soon as _sleep_mode_app_loop returns, we prepare to reactivate
+        // ourselves, but first, we check to see if we woke up for the buzzer:
+        if (movement_state.is_buzzing) {
+            woke_up_for_buzzer = true;
+        }
+        event.event_type = EVENT_ACTIVATE;
+        // this is a hack tho: waking from sleep mode, app_setup does get called, but it happens before we have reset our ticks.
+        // need to figure out if there's a better heuristic for determining how we woke up.
+        app_setup();
+    }
+
+    // default to being allowed to sleep by the face.
+    bool can_sleep = true;
+
+    if (event.event_type) {
+        event.subsecond = movement_state.subsecond;
+        // the first trip through the loop overrides the can_sleep state
+        can_sleep = wf->loop(event, &movement_state.settings, watch_face_contexts[movement_state.current_face_idx]);
+
+        // Keep light on if user is still interacting with the watch.
+        if (movement_state.light_ticks > 0) {
+            switch (event.event_type) {
+                case EVENT_LIGHT_BUTTON_DOWN:
+                case EVENT_MODE_BUTTON_DOWN:
+                case EVENT_ALARM_BUTTON_DOWN:
+                    movement_illuminate_led();
+            }
+        }
+
+        event.event_type = EVENT_NONE;
+    }
+
+    // if we have timed out of our timeout countdown, give the app a hint that they can resign.
+    if (movement_state.timeout_ticks == 0) {
+        movement_state.timeout_ticks = -1;
+        if (movement_state.settings.bit.to_always == false) {
+            // if "timeout always" is false, give the current watch face a chance to exit gracefully...
+            event.event_type = EVENT_TIMEOUT;
+        }
+        event.subsecond = movement_state.subsecond;
+        // if we run through the loop again to time out, we need to reconsider whether or not we can sleep.
+        // if the first trip said true, but this trip said false, we need the false to override, thus
+        // we will be using boolean AND:
+        //
+        // first trip  | can sleep | cannot sleep | can sleep    | cannot sleep
+        // second trip | can sleep | cannot sleep | cannot sleep | can sleep
+        //          && | can sleep | cannot sleep | cannot sleep | cannot sleep
+        bool can_sleep2 = wf->loop(event, &movement_state.settings, watch_face_contexts[movement_state.current_face_idx]);
+        can_sleep = can_sleep && can_sleep2;
+        event.event_type = EVENT_NONE;
+        if (movement_state.settings.bit.to_always && movement_state.current_face_idx != 0) {
+            // ...but if the user has "timeout always" set, give it the boot.
+            movement_move_to_face(0);
+        }
+    }
+
+    // Now that we've handled all display update tasks, handle the alarm.
+    if (movement_state.alarm_ticks >= 0) {
+        uint8_t buzzer_phase = (movement_state.alarm_ticks + 80) % 128;
+        if(buzzer_phase == 127) {
+            // failsafe: buzzer could have been disabled in the meantime
+            if (!watch_is_buzzer_or_led_enabled()) watch_enable_buzzer();
+            // play 4 beeps plus pause
+            for(uint8_t i = 0; i < 4; i++) {
+                // TODO: This method of playing the buzzer blocks the UI while it's beeping.
+                // It might be better to time it with the fast tick.
+                watch_buzzer_play_note(movement_state.alarm_note, (i != 3) ? 50 : 75);
+                if (i != 3) watch_buzzer_play_note(BUZZER_NOTE_REST, 50);
+            }
+        }
+        if (movement_state.alarm_ticks == 0) {
+            movement_state.alarm_ticks = -1;
+            _movement_disable_fast_tick_if_possible();
+        }
+    }
+
+    // if we are plugged into USB, handle the serial shell
+    if (usb_is_enabled()) {
+        /// FIXME: #SecondMovement needs to bring back the shell
+        // shell_task();
+    }
+
+    event.subsecond = 0;
+
+    // if the watch face changed, we can't sleep because we need to update the display.
+    if (movement_state.watch_face_changed) can_sleep = false;
+
+    // if we woke up for the buzzer, stay awake until it's finished.
+    if (woke_up_for_buzzer) {
+        while(watch_is_buzzer_or_led_enabled());
+    }
+
+    // if the LED is on, we need to stay awake to keep the TCC running.
+    if (movement_state.light_ticks != -1) can_sleep = false;
+
+    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) {
+    // force alarm off if the user pressed a button.
+    if (movement_state.alarm_ticks) movement_state.alarm_ticks = 0;
+
+    if (pin_level) {
+        // handle rising edge
+        _movement_enable_fast_tick_if_needed();
+        *down_timestamp = movement_state.fast_ticks + 1;
+        return button_down_event_type;
+    } else {
+        // this line is hack but it handles the situation where the light button was held for more than 20 seconds.
+        // fast tick is disabled by then, and the LED would get stuck on since there's no one left decrementing light_ticks.
+        if (movement_state.light_ticks == 1) movement_state.light_ticks = 0;
+        // now that that's out of the way, handle falling edge
+        uint16_t diff = movement_state.fast_ticks - *down_timestamp;
+        *down_timestamp = 0;
+        _movement_disable_fast_tick_if_possible();
+        // any press over a half second is considered a long press. Fire the long-up event
+        if (diff > MOVEMENT_LONG_PRESS_TICKS) return button_down_event_type + 3;
+        else return button_down_event_type + 1;
+    }
+}
+
+void cb_light_btn_interrupt(void) {
+    bool pin_level = HAL_GPIO_BTN_LIGHT_read();
+    _movement_reset_inactivity_countdown();
+    event.event_type = _figure_out_button_event(pin_level, EVENT_LIGHT_BUTTON_DOWN, &movement_state.light_down_timestamp);
+}
+
+void cb_mode_btn_interrupt(void) {
+    bool pin_level = HAL_GPIO_BTN_MODE_read();
+    _movement_reset_inactivity_countdown();
+    event.event_type = _figure_out_button_event(pin_level, EVENT_MODE_BUTTON_DOWN, &movement_state.mode_down_timestamp);
+}
+
+void cb_alarm_btn_interrupt(void) {
+    bool pin_level = HAL_GPIO_BTN_ALARM_read();
+    _movement_reset_inactivity_countdown();
+    event.event_type = _figure_out_button_event(pin_level, EVENT_ALARM_BUTTON_DOWN, &movement_state.alarm_down_timestamp);
+}
+
+void cb_alarm_btn_extwake(void) {
+    // wake up!
+    _movement_reset_inactivity_countdown();
+}
+
+void cb_alarm_fired(void) {
+    movement_state.needs_background_tasks_handled = true;
+}
+
+void cb_fast_tick(void) {
+    movement_state.fast_ticks++;
+    if (movement_state.light_ticks > 0) movement_state.light_ticks--;
+    if (movement_state.alarm_ticks > 0) movement_state.alarm_ticks--;
+    // check timestamps and auto-fire the long-press events
+    // Notice: is it possible that two or more buttons have an identical timestamp? In this case
+    // only one of these buttons would receive the long press event. Don't bother for now...
+    if (movement_state.light_down_timestamp > 0)
+        if (movement_state.fast_ticks - movement_state.light_down_timestamp == MOVEMENT_LONG_PRESS_TICKS + 1)
+            event.event_type = EVENT_LIGHT_LONG_PRESS;
+    if (movement_state.mode_down_timestamp > 0)
+        if (movement_state.fast_ticks - movement_state.mode_down_timestamp == MOVEMENT_LONG_PRESS_TICKS + 1)
+            event.event_type = EVENT_MODE_LONG_PRESS;
+    if (movement_state.alarm_down_timestamp > 0)
+        if (movement_state.fast_ticks - movement_state.alarm_down_timestamp == MOVEMENT_LONG_PRESS_TICKS + 1)
+            event.event_type = EVENT_ALARM_LONG_PRESS;
+    // this is just a fail-safe; fast tick should be disabled as soon as the button is up, the LED times out, and/or the alarm finishes.
+    // but if for whatever reason it isn't, this forces the fast tick off after 20 seconds.
+    if (movement_state.fast_ticks >= 128 * 20) {
+        watch_rtc_disable_periodic_callback(128);
+        movement_state.fast_tick_enabled = false;
+    }
+}
+
+void cb_tick(void) {
+    event.event_type = EVENT_TICK;
+    watch_date_time date_time = watch_rtc_get_date_time();
+    if (date_time.unit.second != movement_state.last_second) {
+        // TODO: can we consolidate these two ticks?
+        if (movement_state.settings.bit.le_interval && movement_state.le_mode_ticks > 0) movement_state.le_mode_ticks--;
+        if (movement_state.timeout_ticks > 0) movement_state.timeout_ticks--;
+
+        movement_state.last_second = date_time.unit.second;
+        movement_state.subsecond = 0;
+    } else {
+        movement_state.subsecond++;
+    }
+}
diff --git a/movement.h b/movement.h
new file mode 100644 (file)
index 0000000..10ef44d
--- /dev/null
@@ -0,0 +1,314 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2022 Joey Castillo
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+#pragma once
+
+#include <stdio.h>
+#include <stdbool.h>
+#include "watch.h"
+
+// Movement Preferences
+// These four 32-bit structs store information about the wearer and their preferences. Tentatively, the plan is
+// for Movement to use four 32-bit registers for these preferences and to store them in the RTC's backup registers
+// 0-3, leaving registers 4-7 available for third party watch faces to use as they see fit.
+// * The movement_settings_t struct is provided to all watch faces in the callback functions, and is stored in the
+//   RTC's first backup register (BKUP[0]).
+// * The movement_location_t and movement_birthdate_t types are defined here, and are tentatively meant to be
+//   stored in BKUP[1] and BKUP[2], respectively.
+// * The movement_reserved_t type is here as a placeholder, because I sense there's some other generally useful
+//   stuff we'll want to make available to all watch faces and stash in the BKUP[3] register.
+// This allows these preferences to be stored before entering BACKUP mode and and restored after waking from reset.
+
+// movement_settings_t contains global settings that cover watch behavior, including preferences around clock and unit
+// display, time zones, buzzer behavior, LED color and low energy mode timeouts. These settings are stored in BKUP[0].
+// If your watch face changes one of these settings, you should store your changes in either your loop or resign
+// function by calling `watch_store_backup_data(settings->reg, 0)`. Otherwise it may not persist after a reset event.
+typedef union {
+    struct {
+        bool button_should_sound : 1;       // if true, pressing a button emits a sound.
+        uint8_t to_interval : 2;            // an inactivity interval for asking the active face to resign.
+        bool to_always : 1;                 // if true, always time out from the active face to face 0. otherwise only faces that time out will resign (the default).
+        uint8_t le_interval : 3;            // 0 to disable low energy mode, or an inactivity interval for going into low energy mode.
+        uint8_t led_duration : 3;           // how many seconds to shine the LED for (x2), 0 to shine only while the button is depressed, or all bits set to disable the LED altogether.
+        uint8_t led_red_color : 4;          // for general purpose illumination, the red LED value (0-15)
+        uint8_t led_green_color : 4;        // for general purpose illumination, the green LED value (0-15)
+        uint8_t time_zone : 6;              // an integer representing an index in the time zone table.
+
+        // while Movement itself doesn't implement a clock or display units, it may make sense to include some
+        // global settings for watch faces to check. The 12/24 hour preference could inform a clock or a
+        // time-oriented complication like a sunrise/sunset timer, and a simple locale preference could tell an
+        // altimeter to display feet or meters as easily as it tells a thermometer to display degrees in F or C.
+        bool clock_mode_24h : 1;            // indicates whether clock should use 12 or 24 hour mode.
+        bool clock_24h_leading_zero : 1;    // indicates whether clock should leading zero to indicate 24 hour mode.
+        bool use_imperial_units : 1;        // indicates whether to use metric units (the default) or imperial.
+        bool alarm_enabled : 1;             // indicates whether there is at least one alarm enabled.
+        uint8_t reserved : 5;               // room for more preferences if needed.
+    } bit;
+    uint32_t reg;
+} movement_settings_t;
+
+// movement_location_t is for storing the wearer's location. This will be useful for astronomical calculations such as
+// sunrise and sunset, or predictions of visible satellite passes.
+// If you create a UI for this register or need to access it, look for it in the RTC's BKUP[1] register.
+typedef union {
+    struct {
+        int16_t latitude : 16;   // signed latutide in hundredths of a degree
+        int16_t longitude : 16;  // signed longitude in hundredths of a degree
+    } bit;
+    uint32_t reg;
+} movement_location_t;
+
+// movement_birthdate_t is for storing the user's birth date. This will be useful for calculating the user's age â€” or
+// hey, playing happy birthday at midnight? Fields for birth time (with hour and minute resolution) are also available,
+// partly because they fit so nicely, but also because they can be useful for certain astrological calculations.
+// If you create a UI for birth date or need to access it, look for it in the RTC's BKUP[2] register.
+typedef union {
+    struct {
+        uint16_t year : 12;  // good through the year 4095
+        uint8_t month : 4;
+        uint8_t day : 5;
+        uint8_t hour : 5;
+        uint8_t minute : 6;
+    } bit;
+    uint32_t reg;
+} movement_birthdate_t;
+
+// movement_reserved_t is a placeholder for future use of the BKUP[3] register.
+typedef union {
+    struct {
+        uint32_t reserved : 32;
+    } bit;
+    uint32_t reg;
+} movement_reserved_t;
+
+typedef enum {
+    EVENT_NONE = 0,             // There is no event to report.
+    EVENT_ACTIVATE,             // Your watch face is entering the foreground.
+    EVENT_TICK,                 // Most common event type. Your watch face is being called from the tick callback.
+    EVENT_LOW_ENERGY_UPDATE,    // If the watch is in low energy mode and you are in the foreground, you will get a chance to update the display once per minute.
+    EVENT_BACKGROUND_TASK,      // Your watch face is being invoked to perform a background task. Don't update the display here; you may not be in the foreground.
+    EVENT_TIMEOUT,              // Your watch face has been inactive for a while. You may want to resign, depending on your watch face's intended use case.
+    EVENT_LIGHT_BUTTON_DOWN,    // The light button has been pressed, but not yet released.
+    EVENT_LIGHT_BUTTON_UP,      // The light button was pressed for less than half a second, and released.
+    EVENT_LIGHT_LONG_PRESS,     // The light button was held for over half a second, but not yet released.
+    EVENT_LIGHT_LONG_UP,        // The light button was held for over half a second, and released.
+    EVENT_MODE_BUTTON_DOWN,     // The mode button has been pressed, but not yet released.
+    EVENT_MODE_BUTTON_UP,       // The mode button was pressed for less than half a second, and released.
+    EVENT_MODE_LONG_PRESS,      // The mode button was held for over half a second, but not yet released.
+    EVENT_MODE_LONG_UP,         // The mode button was held for over half a second, and released. NOTE: your watch face will resign immediately after receiving this event.
+    EVENT_ALARM_BUTTON_DOWN,    // The alarm button has been pressed, but not yet released.
+    EVENT_ALARM_BUTTON_UP,      // The alarm button was pressed for less than half a second, and released.
+    EVENT_ALARM_LONG_PRESS,     // The alarm button was held for over half a second, but not yet released.
+    EVENT_ALARM_LONG_UP,        // The alarm button was held for over half a second, and released.
+} movement_event_type_t;
+
+typedef struct {
+    uint8_t event_type;
+    uint8_t subsecond;
+} movement_event_t;
+
+extern const int16_t movement_timezone_offsets[];
+extern const char movement_valid_position_0_chars[];
+extern const char movement_valid_position_1_chars[];
+
+/** @brief Perform setup for your watch face.
+  * @details It's tempting to say this is 'one-time' setup, but technically this function is called more than
+  *          once. When the watch first boots, this function is called with a NULL context_ptr, indicating
+  *          that it is the first run. At this time you should set context_ptr to something non-NULL if you
+  *          need to keep track of any state in your watch face. If your watch face requires any other setup,
+  *          like configuring a pin mode or a peripheral, you may want to do that here too.
+  *          This function will be called again after waking from sleep mode, since sleep mode disables all
+  *          of the device's pins and peripherals.
+  * @param settings A pointer to the global Movement settings. You can use this to inform how you present your
+  *                 display to the user (i.e. taking into account whether they have silenced the buttons, or if
+  *                 they prefer 12 or 24-hour mode). You can also change these settings if you like.
+  * @param watch_face_index The index of this watch face in the global array of watch faces; 0 is the first face,
+  *                         1 is the second, etc. You may stash this value in your context if you wish to reference
+  *                         it later; your watch face's index is set at launch and will not change.
+  * @param context_ptr A pointer to a pointer; at first invocation, this value will be NULL, and you can set it
+  *                    to any value you like. Subsequent invocations will pass in whatever value you previously
+  *                    set. You may want to check if this is NULL and if so, allocate some space to store any
+  *                    data required for your watch face.
+  *
+  */
+typedef void (*watch_face_setup)(movement_settings_t *settings, uint8_t watch_face_index, void ** context_ptr);
+
+/** @brief Prepare to go on-screen.
+  * @details This function is called just before your watch enters the foreground. If your watch face has any
+  *          segments or text that is always displayed, you may want to set that here. In addition, if your
+  *          watch face depends on data from a peripheral (like an I2C sensor), you will likely want to enable
+  *          that peripheral here. In addition, if your watch face requires an update frequncy other than 1 Hz,
+  *          you may want to request that here using the movement_request_tick_frequency function.
+  * @param settings A pointer to the global Movement settings. @see watch_face_setup.
+  * @param context A pointer to your watch face's context. @see watch_face_setup.
+  *
+  */
+typedef void (*watch_face_activate)(movement_settings_t *settings, void *context);
+
+/** @brief Handle events and update the display.
+  * @details This function is called in response to an event. You should set up a switch statement that handles,
+  *          at the very least, the EVENT_TICK and EVENT_MODE_BUTTON_UP event types. The tick event happens once
+  *          per second (or more frequently if you asked for a faster tick with movement_request_tick_frequency).
+  *          The mode button up event occurs when the user presses the MODE button. **Your loop function SHOULD
+  *          call the movement_move_to_next_face function in response to this event.** If you have a good reason
+  *          to override this behavior (e.g. your user interface requires all three buttons), your watch face MUST
+  *          call the movement_move_to_next_face function in response to the EVENT_MODE_LONG_PRESS event. If you
+  *          fail to do this, the user will become stuck on your watch face.
+  * @param event A struct containing information about the event, including its type. @see movement_event_type_t
+  *              for a list of all possible event types.
+  * @param settings A pointer to the global Movement settings. @see watch_face_setup.
+  * @param context A pointer to your application's context. @see watch_face_setup.
+  * @return true if your watch face is prepared for the system to enter STANDBY mode; false to keep the system awake.
+  *         You should almost always return true.
+  *         Note that this return value has no effect if your loop function has called movement_move_to_next_face
+  *         or movement_move_to_face; in that case, your watch face will resign immediately, and the next watch
+  *         face will make the decision on entering standby mode.
+  * @note There are two event types that require some extra thought:
+          The EVENT_LOW_ENERGY_UPDATE event type is a special case. If you are in the foreground when the watch
+          goes into low energy mode, you will receive this tick once a minute (at the top of the minute) so that
+          you can update the screen. Great! But! When you receive this event, all pins and peripherals other than
+          the RTC will have been disabled to save energy. If your display is clock or calendar oriented, this is
+          fine. But if your display requires polling an I2C sensor or reading a value with the ADC, you won't be
+          able to do this. You should either display the name of the watch face in response to the low power tick,
+          or ensure that you resign before low power mode triggers, (e.g. by calling movement_move_to_face(0)).
+          **Your watch face MUST NOT wake up peripherals in response to a low power tick.** The purpose of this
+          mode is to consume as little energy as possible during the (potentially long) intervals when it's
+          unlikely the user is wearing or looking at the watch.
+          EVENT_BACKGROUND_TASK is also a special case. @see watch_face_wants_background_task for details.
+  */
+typedef bool (*watch_face_loop)(movement_event_t event, movement_settings_t *settings, void *context);
+
+/** @brief Prepare to go off-screen.
+  * @details This function is called before your watch face enters the background. If you requested a tick
+  *          frequency other than the standard 1 Hz, **you must call movement_request_tick_frequency(1) here**
+  *          to reset to 1 Hz. You should also disable any peripherals you enabled when you entered the foreground.
+  * @param settings A pointer to the global Movement settings. @see watch_face_setup.
+  * @param context A pointer to your application's context. @see watch_face_setup.
+  */
+typedef void (*watch_face_resign)(movement_settings_t *settings, void *context);
+
+/** @brief OPTIONAL. Request an opportunity to run a background task.
+  * @details Most apps will not need this function, but if you provide it, Movement will call it once per minute in
+  *          both active and low power modes, regardless of whether your app is in the foreground. You can check the
+  *          current time to determine whether you require a background task. If you return true here, Movement will
+  *          immediately call your loop function with an EVENT_BACKGROUND_TASK event. Note that it will not call your
+  *          activate or deactivate functions, since you are not going on screen.
+  *
+  *          Examples of background tasks:
+  *           - Wake and play a sound when an alarm or timer has been triggered.
+  *           - Check the state of an RTC interrupt pin or the timestamp of an RTC interrupt event.
+  *           - Log a data point from a sensor, and then return to sleep mode.
+  *
+  *          Guidelines for background tasks:
+  *           - Assume all peripherals and pins other than the RTC will be disabled when you get an EVENT_BACKGROUND_TASK.
+  *           - Even if your background task involves only the RTC peripheral, try to request background tasks sparingly.
+  *           - If your background task involves an external pin or peripheral, request background tasks no more than once per hour.
+  *           - If you need to enable a pin or a peripheral to perform your task, return it to its original state afterwards.
+  *
+  * @param settings A pointer to the global Movement settings. @see watch_face_setup.
+  * @param context A pointer to your application's context. @see watch_face_setup.
+  * @return true to request a background task; false otherwise.
+  */
+typedef bool (*watch_face_wants_background_task)(movement_settings_t *settings, void *context);
+
+typedef struct {
+    watch_face_setup setup;
+    watch_face_activate activate;
+    watch_face_loop loop;
+    watch_face_resign resign;
+    watch_face_wants_background_task wants_background_task;
+} watch_face_t;
+
+typedef struct {
+    // properties stored in BACKUP register
+    movement_settings_t settings;
+
+    // transient properties
+    int16_t current_face_idx;
+    int16_t next_face_idx;
+    bool watch_face_changed;
+    bool fast_tick_enabled;
+    int16_t fast_ticks;
+
+    // LED stuff
+    int16_t light_ticks;
+
+    // alarm stuff
+    int16_t alarm_ticks;
+    bool is_buzzing;
+    BuzzerNote alarm_note;
+
+    // button tracking for long press
+    uint16_t light_down_timestamp;
+    uint16_t mode_down_timestamp;
+    uint16_t alarm_down_timestamp;
+
+    // background task handling
+    bool needs_background_tasks_handled;
+    bool has_scheduled_background_task;
+    bool needs_wake;
+
+    // low energy mode countdown
+    int32_t le_mode_ticks;
+
+    // app resignation countdown (TODO: consolidate with LE countdown?)
+    int16_t timeout_ticks;
+
+    // stuff for subsecond tracking
+    uint8_t tick_frequency;
+    uint8_t last_second;
+    uint8_t subsecond;
+
+    // backup register stuff
+    uint8_t next_available_backup_register;
+} movement_state_t;
+
+void movement_move_to_face(uint8_t watch_face_index);
+void movement_move_to_next_face(void);
+
+bool movement_default_loop_handler(movement_event_t event, movement_settings_t *settings);
+
+void movement_illuminate_led(void);
+
+void movement_request_tick_frequency(uint8_t freq);
+
+// note: watch faces can only schedule a background task when in the foreground, since
+// movement will associate the scheduled task with the currently active face.
+void movement_schedule_background_task(watch_date_time date_time);
+
+// note: watch faces can only cancel their background task when in the foreground, since
+// movement will associate the scheduled task with the currently active face.
+void movement_cancel_background_task(void);
+
+// these functions should work around the limitation of the above functions, which will be deprecated.
+void movement_schedule_background_task_for_face(uint8_t watch_face_index, watch_date_time date_time);
+void movement_cancel_background_task_for_face(uint8_t watch_face_index);
+
+void movement_request_wake(void);
+
+void movement_play_signal(void);
+void movement_play_alarm(void);
+void movement_play_alarm_beeps(uint8_t rounds, BuzzerNote alarm_note);
+
+uint8_t movement_claim_backup_register(void);
diff --git a/movement/movement.c b/movement/movement.c
deleted file mode 100644 (file)
index fe6f9d1..0000000
+++ /dev/null
@@ -1,733 +0,0 @@
-/*
- * MIT License
- *
- * Copyright (c) 2022 Joey Castillo
- *
- * Permission is hereby granted, free of charge, to any person obtaining a copy
- * of this software and associated documentation files (the "Software"), to deal
- * in the Software without restriction, including without limitation the rights
- * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- * copies of the Software, and to permit persons to whom the Software is
- * furnished to do so, subject to the following conditions:
- *
- * The above copyright notice and this permission notice shall be included in all
- * copies or substantial portions of the Software.
- *
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- * SOFTWARE.
- */
-
-#define MOVEMENT_LONG_PRESS_TICKS 64
-
-#include <stdio.h>
-#include <string.h>
-#include <limits.h>
-#include <string.h>
-#include <stdlib.h>
-#include <stdio.h>
-#include "watch.h"
-#include "filesystem.h"
-#include "movement.h"
-#include "shell.h"
-
-#ifndef MOVEMENT_FIRMWARE
-#include "movement_config.h"
-#elif MOVEMENT_FIRMWARE == MOVEMENT_FIRMWARE_STANDARD
-#include "movement_config.h"
-#elif MOVEMENT_FIRMWARE == MOVEMENT_FIRMWARE_BACKER
-#include "alt_fw/backer.h"
-#elif MOVEMENT_FIRMWARE == MOVEMENT_FIRMWARE_ALT_TIME
-#include "alt_fw/alt_time.h"
-#elif MOVEMENT_FIRMWARE == MOVEMENT_FIRMWARE_FOCUS
-#include "alt_fw/focus.h"
-#elif MOVEMENT_FIRMWARE == MOVEMENT_FIRMWARE_THE_BACKPACKER
-#include "alt_fw/the_backpacker.h"
-#elif MOVEMENT_FIRMWARE == MOVEMENT_FIRMWARE_THE_ATHLETE
-#include "alt_fw/the_athlete.h"
-#elif MOVEMENT_FIRMWARE == MOVEMENT_FIRMWARE_THE_STARGAZER
-#include "alt_fw/the_stargazer.h"
-#elif MOVEMENT_FIRMWARE == MOVEMENT_FIRMWARE_DEEP_SPACE_NOW
-#include "alt_fw/deep_space_now.h"
-#endif
-
-#include "movement_custom_signal_tunes.h"
-
-// Default to no secondary face behaviour.
-#ifndef MOVEMENT_SECONDARY_FACE_INDEX
-#define MOVEMENT_SECONDARY_FACE_INDEX 0
-#endif
-
-// Set default LED colors if not set
-#ifndef MOVEMENT_DEFAULT_RED_COLOR
-#define MOVEMENT_DEFAULT_RED_COLOR 0x0
-#endif
-#ifndef MOVEMENT_DEFAULT_GREEN_COLOR
-#define MOVEMENT_DEFAULT_GREEN_COLOR 0xF
-#endif
-
-// Default to 12h mode
-#ifndef MOVEMENT_DEFAULT_24H_MODE
-#define MOVEMENT_DEFAULT_24H_MODE false
-#endif
-
-// Default to mode button sounding on press
-#ifndef MOVEMENT_DEFAULT_BUTTON_SOUND
-#define MOVEMENT_DEFAULT_BUTTON_SOUND true
-#endif
-
-// Default to switch back to main watch face after 60 seconds
-#ifndef MOVEMENT_DEFAULT_TIMEOUT_INTERVAL
-#define MOVEMENT_DEFAULT_TIMEOUT_INTERVAL 0
-#endif
-
-// Default to switch to low energy mode after 2 hours
-#ifndef MOVEMENT_DEFAULT_LOW_ENERGY_INTERVAL
-#define MOVEMENT_DEFAULT_LOW_ENERGY_INTERVAL 2
-#endif
-
-// Default to 1 second led duration
-#ifndef MOVEMENT_DEFAULT_LED_DURATION
-#define MOVEMENT_DEFAULT_LED_DURATION 1
-#endif
-
-#if __EMSCRIPTEN__
-#include <emscripten.h>
-#endif
-
-movement_state_t movement_state;
-void * watch_face_contexts[MOVEMENT_NUM_FACES];
-watch_date_time scheduled_tasks[MOVEMENT_NUM_FACES];
-const int32_t movement_le_inactivity_deadlines[8] = {INT_MAX, 600, 3600, 7200, 21600, 43200, 86400, 604800};
-const int16_t movement_timeout_inactivity_deadlines[4] = {60, 120, 300, 1800};
-movement_event_t event;
-
-const int16_t movement_timezone_offsets[] = {
-    0,      //  0 :   0:00:00 (UTC)
-    60,     //  1 :   1:00:00 (Central European Time)
-    120,    //  2 :   2:00:00 (South African Standard Time)
-    180,    //  3 :   3:00:00 (Arabia Standard Time)
-    210,    //  4 :   3:30:00 (Iran Standard Time)
-    240,    //  5 :   4:00:00 (Georgia Standard Time)
-    270,    //  6 :   4:30:00 (Afghanistan Time)
-    300,    //  7 :   5:00:00 (Pakistan Standard Time)
-    330,    //  8 :   5:30:00 (Indian Standard Time)
-    345,    //  9 :   5:45:00 (Nepal Time)
-    360,    // 10 :   6:00:00 (Kyrgyzstan time)
-    390,    // 11 :   6:30:00 (Myanmar Time)
-    420,    // 12 :   7:00:00 (Thailand Standard Time)
-    480,    // 13 :   8:00:00 (China Standard Time, Australian Western Standard Time)
-    525,    // 14 :   8:45:00 (Australian Central Western Standard Time)
-    540,    // 15 :   9:00:00 (Japan Standard Time, Korea Standard Time)
-    570,    // 16 :   9:30:00 (Australian Central Standard Time)
-    600,    // 17 :  10:00:00 (Australian Eastern Standard Time)
-    630,    // 18 :  10:30:00 (Lord Howe Standard Time)
-    660,    // 19 :  11:00:00 (Solomon Islands Time)
-    720,    // 20 :  12:00:00 (New Zealand Standard Time)
-    765,    // 21 :  12:45:00 (Chatham Standard Time)
-    780,    // 22 :  13:00:00 (Tonga Time)
-    825,    // 23 :  13:45:00 (Chatham Daylight Time)
-    840,    // 24 :  14:00:00 (Line Islands Time)
-    -720,   // 25 : -12:00:00 (Baker Island Time)
-    -660,   // 26 : -11:00:00 (Niue Time)
-    -600,   // 27 : -10:00:00 (Hawaii-Aleutian Standard Time)
-    -570,   // 28 :  -9:30:00 (Marquesas Islands Time)
-    -540,   // 29 :  -9:00:00 (Alaska Standard Time)
-    -480,   // 30 :  -8:00:00 (Pacific Standard Time)
-    -420,   // 31 :  -7:00:00 (Mountain Standard Time)
-    -360,   // 32 :  -6:00:00 (Central Standard Time)
-    -300,   // 33 :  -5:00:00 (Eastern Standard Time)
-    -270,   // 34 :  -4:30:00 (Venezuelan Standard Time)
-    -240,   // 35 :  -4:00:00 (Atlantic Standard Time)
-    -210,   // 36 :  -3:30:00 (Newfoundland Standard Time)
-    -180,   // 37 :  -3:00:00 (Brasilia Time)
-    -150,   // 38 :  -2:30:00 (Newfoundland Daylight Time)
-    -120,   // 39 :  -2:00:00 (Fernando de Noronha Time)
-    -60,    // 40 :  -1:00:00 (Azores Standard Time)
-};
-
-const char movement_valid_position_0_chars[] = " AaBbCcDdEeFGgHhIiJKLMNnOoPQrSTtUuWXYZ-='+\\/0123456789";
-const char movement_valid_position_1_chars[] = " ABCDEFHlJLNORTtUX-='01378";
-
-void cb_mode_btn_interrupt(void);
-void cb_light_btn_interrupt(void);
-void cb_alarm_btn_interrupt(void);
-void cb_alarm_btn_extwake(void);
-void cb_alarm_fired(void);
-void cb_fast_tick(void);
-void cb_tick(void);
-
-static inline void _movement_reset_inactivity_countdown(void) {
-    movement_state.le_mode_ticks = movement_le_inactivity_deadlines[movement_state.settings.bit.le_interval];
-    movement_state.timeout_ticks = movement_timeout_inactivity_deadlines[movement_state.settings.bit.to_interval];
-}
-
-static inline void _movement_enable_fast_tick_if_needed(void) {
-    if (!movement_state.fast_tick_enabled) {
-        movement_state.fast_ticks = 0;
-        watch_rtc_register_periodic_callback(cb_fast_tick, 128);
-        movement_state.fast_tick_enabled = true;
-    }
-}
-
-static inline void _movement_disable_fast_tick_if_possible(void) {
-    if ((movement_state.light_ticks == -1) &&
-        (movement_state.alarm_ticks == -1) &&
-        ((movement_state.light_down_timestamp + movement_state.mode_down_timestamp + movement_state.alarm_down_timestamp) == 0)) {
-        movement_state.fast_tick_enabled = false;
-        watch_rtc_disable_periodic_callback(128);
-    }
-}
-
-static void _movement_handle_background_tasks(void) {
-    for(uint8_t i = 0; i < MOVEMENT_NUM_FACES; i++) {
-        // For each face, if the watch face wants a background task...
-        if (watch_faces[i].wants_background_task != NULL && watch_faces[i].wants_background_task(&movement_state.settings, watch_face_contexts[i])) {
-            // ...we give it one. pretty straightforward!
-            movement_event_t background_event = { EVENT_BACKGROUND_TASK, 0 };
-            watch_faces[i].loop(background_event, &movement_state.settings, watch_face_contexts[i]);
-        }
-    }
-    movement_state.needs_background_tasks_handled = false;
-}
-
-static void _movement_handle_scheduled_tasks(void) {
-    watch_date_time date_time = watch_rtc_get_date_time();
-    uint8_t num_active_tasks = 0;
-
-    for(uint8_t i = 0; i < MOVEMENT_NUM_FACES; i++) {
-        if (scheduled_tasks[i].reg) {
-            if (scheduled_tasks[i].reg <= date_time.reg) {
-                scheduled_tasks[i].reg = 0;
-                movement_event_t background_event = { EVENT_BACKGROUND_TASK, 0 };
-                watch_faces[i].loop(background_event, &movement_state.settings, watch_face_contexts[i]);
-                // check if loop scheduled a new task
-                if (scheduled_tasks[i].reg) {
-                    num_active_tasks++;
-                }
-            } else {
-                num_active_tasks++;
-            }
-        }
-    }
-
-    if (num_active_tasks == 0) {
-        movement_state.has_scheduled_background_task = false;
-    } else {
-        _movement_reset_inactivity_countdown();
-    }
-}
-
-void movement_request_tick_frequency(uint8_t freq) {
-    // Movement uses the 128 Hz tick internally
-    if (freq == 128) return;
-
-    // Movement requires at least a 1 Hz tick.
-    // If we are asked for an invalid frequency, default back to 1 Hz.
-    if (freq == 0 || __builtin_popcount(freq) != 1) freq = 1;
-
-    // disable all callbacks except the 128 Hz one
-    watch_rtc_disable_matching_periodic_callbacks(0xFE);
-
-    movement_state.subsecond = 0;
-    movement_state.tick_frequency = freq;
-    watch_rtc_register_periodic_callback(cb_tick, freq);
-}
-
-void movement_illuminate_led(void) {
-    if (movement_state.settings.bit.led_duration != 0b111) {
-        watch_set_led_color(movement_state.settings.bit.led_red_color ? (0xF | movement_state.settings.bit.led_red_color << 4) : 0,
-                            movement_state.settings.bit.led_green_color ? (0xF | movement_state.settings.bit.led_green_color << 4) : 0);
-        if (movement_state.settings.bit.led_duration == 0) {
-            movement_state.light_ticks = 1;
-        } else {
-            movement_state.light_ticks = (movement_state.settings.bit.led_duration * 2 - 1) * 128;
-        }
-        _movement_enable_fast_tick_if_needed();
-    }
-}
-
-static void _movement_led_off(void) {
-    watch_set_led_off();
-    movement_state.light_ticks = -1;
-    _movement_disable_fast_tick_if_possible();
-}
-
-bool movement_default_loop_handler(movement_event_t event, movement_settings_t *settings) {
-    (void)settings;
-
-    switch (event.event_type) {
-        case EVENT_MODE_BUTTON_UP:
-            movement_move_to_next_face();
-            break;
-        case EVENT_LIGHT_BUTTON_DOWN:
-            movement_illuminate_led();
-            break;
-        case EVENT_LIGHT_BUTTON_UP:
-            if (movement_state.settings.bit.led_duration == 0) {
-                _movement_led_off();
-            }
-            break;
-        case EVENT_MODE_LONG_PRESS:
-            if (MOVEMENT_SECONDARY_FACE_INDEX && movement_state.current_face_idx == 0) {
-                movement_move_to_face(MOVEMENT_SECONDARY_FACE_INDEX);
-            } else {
-                movement_move_to_face(0);
-            }
-            break;
-        default:
-            break;
-    }
-
-    return true;
-}
-
-void movement_move_to_face(uint8_t watch_face_index) {
-    movement_state.watch_face_changed = true;
-    movement_state.next_face_idx = watch_face_index;
-}
-
-void movement_move_to_next_face(void) {
-    uint16_t face_max;
-    if (MOVEMENT_SECONDARY_FACE_INDEX) {
-        face_max = (movement_state.current_face_idx < (int16_t)MOVEMENT_SECONDARY_FACE_INDEX) ? MOVEMENT_SECONDARY_FACE_INDEX : MOVEMENT_NUM_FACES;
-    } else {
-        face_max = MOVEMENT_NUM_FACES;
-    }
-    movement_move_to_face((movement_state.current_face_idx + 1) % face_max);
-}
-
-void movement_schedule_background_task(watch_date_time date_time) {
-    movement_schedule_background_task_for_face(movement_state.current_face_idx, date_time);
-}
-
-void movement_cancel_background_task(void) {
-    movement_cancel_background_task_for_face(movement_state.current_face_idx);
-}
-
-void movement_schedule_background_task_for_face(uint8_t watch_face_index, watch_date_time date_time) {
-    watch_date_time now = watch_rtc_get_date_time();
-    if (date_time.reg > now.reg) {
-        movement_state.has_scheduled_background_task = true;
-        scheduled_tasks[watch_face_index].reg = date_time.reg;
-    }
-}
-
-void movement_cancel_background_task_for_face(uint8_t watch_face_index) {
-    scheduled_tasks[watch_face_index].reg = 0;
-    bool other_tasks_scheduled = false;
-    for(uint8_t i = 0; i < MOVEMENT_NUM_FACES; i++) {
-        if (scheduled_tasks[i].reg != 0) {
-            other_tasks_scheduled = true;
-            break;
-        }
-    }
-    movement_state.has_scheduled_background_task = other_tasks_scheduled;
-}
-
-void movement_request_wake() {
-    movement_state.needs_wake = true;
-    _movement_reset_inactivity_countdown();
-}
-
-static void end_buzzing() {
-    movement_state.is_buzzing = false;
-}
-
-static void end_buzzing_and_disable_buzzer(void) {
-    end_buzzing();
-    watch_disable_buzzer();
-}
-
-static void set_initial_clock_mode(void) {
-#ifdef CLOCK_FACE_24H_ONLY
-    movement_state.settings.bit.clock_mode_24h = true;
-#else
-    movement_state.settings.bit.clock_mode_24h = MOVEMENT_DEFAULT_24H_MODE;
-#endif
-}
-
-void movement_play_signal(void) {
-    void *maybe_disable_buzzer = end_buzzing_and_disable_buzzer;
-    if (watch_is_buzzer_or_led_enabled()) {
-        maybe_disable_buzzer = end_buzzing;
-    } else {
-        watch_enable_buzzer();
-    }
-    movement_state.is_buzzing = true;
-    watch_buzzer_play_sequence(signal_tune, maybe_disable_buzzer);
-    if (movement_state.le_mode_ticks == -1) {
-        // the watch is asleep. wake it up for "1" round through the main loop.
-        // the sleep_mode_app_loop will notice the is_buzzing and note that it
-        // only woke up to beep and then it will spinlock until the callback
-        // turns off the is_buzzing flag.
-        movement_state.needs_wake = true;
-        movement_state.le_mode_ticks = 1;
-    }
-}
-
-void movement_play_alarm(void) {
-    movement_play_alarm_beeps(5, BUZZER_NOTE_C8);
-}
-
-void movement_play_alarm_beeps(uint8_t rounds, BuzzerNote alarm_note) {
-    if (rounds == 0) rounds = 1;
-    if (rounds > 20) rounds = 20;
-    movement_request_wake();
-    movement_state.alarm_note = alarm_note;
-    // our tone is 0.375 seconds of beep and 0.625 of silence, repeated as given.
-    movement_state.alarm_ticks = 128 * rounds - 75;
-    _movement_enable_fast_tick_if_needed();
-}
-
-uint8_t movement_claim_backup_register(void) {
-    if (movement_state.next_available_backup_register >= 8) return 0;
-    return movement_state.next_available_backup_register++;
-}
-
-void app_init(void) {
-#if defined(NO_FREQCORR)
-    watch_rtc_freqcorr_write(0, 0);
-#elif defined(WATCH_IS_BLUE_BOARD)
-    watch_rtc_freqcorr_write(11, 0);
-#else
-    watch_rtc_freqcorr_write(22, 0);
-#endif
-
-    memset(&movement_state, 0, sizeof(movement_state));
-    set_initial_clock_mode();
-    movement_state.settings.bit.led_red_color = MOVEMENT_DEFAULT_RED_COLOR;
-    movement_state.settings.bit.led_green_color = MOVEMENT_DEFAULT_GREEN_COLOR;
-    movement_state.settings.bit.button_should_sound = MOVEMENT_DEFAULT_BUTTON_SOUND;
-    movement_state.settings.bit.to_interval = MOVEMENT_DEFAULT_TIMEOUT_INTERVAL;
-    movement_state.settings.bit.le_interval = MOVEMENT_DEFAULT_LOW_ENERGY_INTERVAL;
-    movement_state.settings.bit.led_duration = MOVEMENT_DEFAULT_LED_DURATION;
-
-    movement_state.light_ticks = -1;
-    movement_state.alarm_ticks = -1;
-    movement_state.next_available_backup_register = 4;
-    _movement_reset_inactivity_countdown();
-
-    filesystem_init();
-
-#if __EMSCRIPTEN__
-    int32_t time_zone_offset = EM_ASM_INT({
-        return -new Date().getTimezoneOffset();
-    });
-    for (int i = 0, count = sizeof(movement_timezone_offsets) / sizeof(movement_timezone_offsets[0]); i < count; i++) {
-        if (movement_timezone_offsets[i] == time_zone_offset) {
-            movement_state.settings.bit.time_zone = i;
-            break;
-        }
-    }
-#endif
-}
-
-void app_wake_from_backup(void) {
-    movement_state.settings.reg = watch_get_backup_data(0);
-}
-
-void app_setup(void) {
-    watch_store_backup_data(movement_state.settings.reg, 0);
-
-    static bool is_first_launch = true;
-
-    if (is_first_launch) {
-        #ifdef MOVEMENT_CUSTOM_BOOT_COMMANDS
-        MOVEMENT_CUSTOM_BOOT_COMMANDS()
-        #endif
-
-        for(uint8_t i = 0; i < MOVEMENT_NUM_FACES; i++) {
-            watch_face_contexts[i] = NULL;
-            scheduled_tasks[i].reg = 0;
-            is_first_launch = false;
-        }
-
-        // set up the 1 minute alarm (for background tasks and low power updates)
-        watch_date_time alarm_time;
-        alarm_time.reg = 0;
-        alarm_time.unit.second = 59; // after a match, the alarm fires at the next rising edge of CLK_RTC_CNT, so 59 seconds lets us update at :00
-        watch_rtc_register_alarm_callback(cb_alarm_fired, alarm_time, ALARM_MATCH_SS);
-    }
-    if (movement_state.le_mode_ticks != -1) {
-        watch_disable_extwake_interrupt(BTN_ALARM);
-
-        watch_enable_external_interrupts();
-        watch_register_interrupt_callback(BTN_MODE, cb_mode_btn_interrupt, INTERRUPT_TRIGGER_BOTH);
-        watch_register_interrupt_callback(BTN_LIGHT, cb_light_btn_interrupt, INTERRUPT_TRIGGER_BOTH);
-        watch_register_interrupt_callback(BTN_ALARM, cb_alarm_btn_interrupt, INTERRUPT_TRIGGER_BOTH);
-
-        watch_enable_buzzer();
-        watch_enable_leds();
-        watch_enable_display();
-
-        movement_request_tick_frequency(1);
-
-        for(uint8_t i = 0; i < MOVEMENT_NUM_FACES; i++) {
-            watch_faces[i].setup(&movement_state.settings, i, &watch_face_contexts[i]);
-        }
-
-        watch_faces[movement_state.current_face_idx].activate(&movement_state.settings, watch_face_contexts[movement_state.current_face_idx]);
-        event.subsecond = 0;
-        event.event_type = EVENT_ACTIVATE;
-    }
-}
-
-void app_prepare_for_standby(void) {
-}
-
-void app_wake_from_standby(void) {
-}
-
-static void _sleep_mode_app_loop(void) {
-    movement_state.needs_wake = false;
-    // as long as le_mode_ticks is -1 (i.e. we are in low energy mode), we wake up here, update the screen, and go right back to sleep.
-    while (movement_state.le_mode_ticks == -1) {
-        // we also have to handle background tasks here in the mini-runloop
-        if (movement_state.needs_background_tasks_handled) _movement_handle_background_tasks();
-
-        event.event_type = EVENT_LOW_ENERGY_UPDATE;
-        watch_faces[movement_state.current_face_idx].loop(event, &movement_state.settings, watch_face_contexts[movement_state.current_face_idx]);
-
-        // if we need to wake immediately, do it!
-        if (movement_state.needs_wake) return;
-        // otherwise enter sleep mode, and when the extwake handler is called, it will reset le_mode_ticks and force us out at the next loop.
-        else watch_enter_sleep_mode();
-    }
-}
-
-bool app_loop(void) {
-    const watch_face_t *wf = &watch_faces[movement_state.current_face_idx];
-    bool woke_up_for_buzzer = false;
-    if (movement_state.watch_face_changed) {
-        if (movement_state.settings.bit.button_should_sound) {
-            // low note for nonzero case, high note for return to watch_face 0
-            watch_buzzer_play_note(movement_state.next_face_idx ? BUZZER_NOTE_C7 : BUZZER_NOTE_C8, 50);
-        }
-        wf->resign(&movement_state.settings, watch_face_contexts[movement_state.current_face_idx]);
-        movement_state.current_face_idx = movement_state.next_face_idx;
-        // we have just updated the face idx, so we must recache the watch face pointer.
-        wf = &watch_faces[movement_state.current_face_idx];
-        watch_clear_display();
-        movement_request_tick_frequency(1);
-        wf->activate(&movement_state.settings, watch_face_contexts[movement_state.current_face_idx]);
-        event.subsecond = 0;
-        event.event_type = EVENT_ACTIVATE;
-        movement_state.watch_face_changed = false;
-    }
-
-    // if the LED should be off, turn it off
-    if (movement_state.light_ticks == 0) {
-        // unless the user is holding down the LIGHT button, in which case, give them more time.
-        if (watch_get_pin_level(BTN_LIGHT)) {
-            movement_state.light_ticks = 1;
-        } else {
-            _movement_led_off();
-        }
-    }
-
-    // handle background tasks, if the alarm handler told us we need to
-    if (movement_state.needs_background_tasks_handled) _movement_handle_background_tasks();
-
-    // if we have a scheduled background task, handle that here:
-    if (event.event_type == EVENT_TICK && movement_state.has_scheduled_background_task) _movement_handle_scheduled_tasks();
-
-    // if we have timed out of our low energy mode countdown, enter low energy mode.
-    if (movement_state.le_mode_ticks == 0) {
-        movement_state.le_mode_ticks = -1;
-        watch_register_extwake_callback(BTN_ALARM, cb_alarm_btn_extwake, true);
-        event.event_type = EVENT_NONE;
-        event.subsecond = 0;
-
-        // _sleep_mode_app_loop takes over at this point and loops until le_mode_ticks is reset by the extwake handler,
-        // or wake is requested using the movement_request_wake function.
-        _sleep_mode_app_loop();
-        // as soon as _sleep_mode_app_loop returns, we prepare to reactivate
-        // ourselves, but first, we check to see if we woke up for the buzzer:
-        if (movement_state.is_buzzing) {
-            woke_up_for_buzzer = true;
-        }
-        event.event_type = EVENT_ACTIVATE;
-        // this is a hack tho: waking from sleep mode, app_setup does get called, but it happens before we have reset our ticks.
-        // need to figure out if there's a better heuristic for determining how we woke up.
-        app_setup();
-    }
-
-    // default to being allowed to sleep by the face.
-    bool can_sleep = true;
-
-    if (event.event_type) {
-        event.subsecond = movement_state.subsecond;
-        // the first trip through the loop overrides the can_sleep state
-        can_sleep = wf->loop(event, &movement_state.settings, watch_face_contexts[movement_state.current_face_idx]);
-
-        // Keep light on if user is still interacting with the watch.
-        if (movement_state.light_ticks > 0) {
-            switch (event.event_type) {
-                case EVENT_LIGHT_BUTTON_DOWN:
-                case EVENT_MODE_BUTTON_DOWN:
-                case EVENT_ALARM_BUTTON_DOWN:
-                    movement_illuminate_led();
-            }
-        }
-
-        event.event_type = EVENT_NONE;
-    }
-
-    // if we have timed out of our timeout countdown, give the app a hint that they can resign.
-    if (movement_state.timeout_ticks == 0) {
-        movement_state.timeout_ticks = -1;
-        if (movement_state.settings.bit.to_always == false) {
-            // if "timeout always" is false, give the current watch face a chance to exit gracefully...
-            event.event_type = EVENT_TIMEOUT;
-        }
-        event.subsecond = movement_state.subsecond;
-        // if we run through the loop again to time out, we need to reconsider whether or not we can sleep.
-        // if the first trip said true, but this trip said false, we need the false to override, thus
-        // we will be using boolean AND:
-        //
-        // first trip  | can sleep | cannot sleep | can sleep    | cannot sleep
-        // second trip | can sleep | cannot sleep | cannot sleep | can sleep
-        //          && | can sleep | cannot sleep | cannot sleep | cannot sleep
-        bool can_sleep2 = wf->loop(event, &movement_state.settings, watch_face_contexts[movement_state.current_face_idx]);
-        can_sleep = can_sleep && can_sleep2;
-        event.event_type = EVENT_NONE;
-        if (movement_state.settings.bit.to_always && movement_state.current_face_idx != 0) {
-            // ...but if the user has "timeout always" set, give it the boot.
-            movement_move_to_face(0);
-        }
-    }
-
-    // Now that we've handled all display update tasks, handle the alarm.
-    if (movement_state.alarm_ticks >= 0) {
-        uint8_t buzzer_phase = (movement_state.alarm_ticks + 80) % 128;
-        if(buzzer_phase == 127) {
-            // failsafe: buzzer could have been disabled in the meantime
-            if (!watch_is_buzzer_or_led_enabled()) watch_enable_buzzer();
-            // play 4 beeps plus pause
-            for(uint8_t i = 0; i < 4; i++) {
-                // TODO: This method of playing the buzzer blocks the UI while it's beeping.
-                // It might be better to time it with the fast tick.
-                watch_buzzer_play_note(movement_state.alarm_note, (i != 3) ? 50 : 75);
-                if (i != 3) watch_buzzer_play_note(BUZZER_NOTE_REST, 50);
-            }
-        }
-        if (movement_state.alarm_ticks == 0) {
-            movement_state.alarm_ticks = -1;
-            _movement_disable_fast_tick_if_possible();
-        }
-    }
-
-    // if we are plugged into USB, handle the serial shell
-    if (watch_is_usb_enabled()) {
-        shell_task();
-    }
-
-    event.subsecond = 0;
-
-    // if the watch face changed, we can't sleep because we need to update the display.
-    if (movement_state.watch_face_changed) can_sleep = false;
-
-    // if we woke up for the buzzer, stay awake until it's finished.
-    if (woke_up_for_buzzer) {
-        while(watch_is_buzzer_or_led_enabled());
-    }
-
-    // if the LED is on, we need to stay awake to keep the TCC running.
-    if (movement_state.light_ticks != -1) can_sleep = false;
-
-    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) {
-    // force alarm off if the user pressed a button.
-    if (movement_state.alarm_ticks) movement_state.alarm_ticks = 0;
-
-    if (pin_level) {
-        // handle rising edge
-        _movement_enable_fast_tick_if_needed();
-        *down_timestamp = movement_state.fast_ticks + 1;
-        return button_down_event_type;
-    } else {
-        // this line is hack but it handles the situation where the light button was held for more than 20 seconds.
-        // fast tick is disabled by then, and the LED would get stuck on since there's no one left decrementing light_ticks.
-        if (movement_state.light_ticks == 1) movement_state.light_ticks = 0;
-        // now that that's out of the way, handle falling edge
-        uint16_t diff = movement_state.fast_ticks - *down_timestamp;
-        *down_timestamp = 0;
-        _movement_disable_fast_tick_if_possible();
-        // any press over a half second is considered a long press. Fire the long-up event
-        if (diff > MOVEMENT_LONG_PRESS_TICKS) return button_down_event_type + 3;
-        else return button_down_event_type + 1;
-    }
-}
-
-void cb_light_btn_interrupt(void) {
-    bool pin_level = watch_get_pin_level(BTN_LIGHT);
-    _movement_reset_inactivity_countdown();
-    event.event_type = _figure_out_button_event(pin_level, EVENT_LIGHT_BUTTON_DOWN, &movement_state.light_down_timestamp);
-}
-
-void cb_mode_btn_interrupt(void) {
-    bool pin_level = watch_get_pin_level(BTN_MODE);
-    _movement_reset_inactivity_countdown();
-    event.event_type = _figure_out_button_event(pin_level, EVENT_MODE_BUTTON_DOWN, &movement_state.mode_down_timestamp);
-}
-
-void cb_alarm_btn_interrupt(void) {
-    bool pin_level = watch_get_pin_level(BTN_ALARM);
-    _movement_reset_inactivity_countdown();
-    event.event_type = _figure_out_button_event(pin_level, EVENT_ALARM_BUTTON_DOWN, &movement_state.alarm_down_timestamp);
-}
-
-void cb_alarm_btn_extwake(void) {
-    // wake up!
-    _movement_reset_inactivity_countdown();
-}
-
-void cb_alarm_fired(void) {
-    movement_state.needs_background_tasks_handled = true;
-}
-
-void cb_fast_tick(void) {
-    movement_state.fast_ticks++;
-    if (movement_state.light_ticks > 0) movement_state.light_ticks--;
-    if (movement_state.alarm_ticks > 0) movement_state.alarm_ticks--;
-    // check timestamps and auto-fire the long-press events
-    // Notice: is it possible that two or more buttons have an identical timestamp? In this case
-    // only one of these buttons would receive the long press event. Don't bother for now...
-    if (movement_state.light_down_timestamp > 0)
-        if (movement_state.fast_ticks - movement_state.light_down_timestamp == MOVEMENT_LONG_PRESS_TICKS + 1)
-            event.event_type = EVENT_LIGHT_LONG_PRESS;
-    if (movement_state.mode_down_timestamp > 0)
-        if (movement_state.fast_ticks - movement_state.mode_down_timestamp == MOVEMENT_LONG_PRESS_TICKS + 1)
-            event.event_type = EVENT_MODE_LONG_PRESS;
-    if (movement_state.alarm_down_timestamp > 0)
-        if (movement_state.fast_ticks - movement_state.alarm_down_timestamp == MOVEMENT_LONG_PRESS_TICKS + 1)
-            event.event_type = EVENT_ALARM_LONG_PRESS;
-    // this is just a fail-safe; fast tick should be disabled as soon as the button is up, the LED times out, and/or the alarm finishes.
-    // but if for whatever reason it isn't, this forces the fast tick off after 20 seconds.
-    if (movement_state.fast_ticks >= 128 * 20) {
-        watch_rtc_disable_periodic_callback(128);
-        movement_state.fast_tick_enabled = false;
-    }
-}
-
-void cb_tick(void) {
-    event.event_type = EVENT_TICK;
-    watch_date_time date_time = watch_rtc_get_date_time();
-    if (date_time.unit.second != movement_state.last_second) {
-        // TODO: can we consolidate these two ticks?
-        if (movement_state.settings.bit.le_interval && movement_state.le_mode_ticks > 0) movement_state.le_mode_ticks--;
-        if (movement_state.timeout_ticks > 0) movement_state.timeout_ticks--;
-
-        movement_state.last_second = date_time.unit.second;
-        movement_state.subsecond = 0;
-    } else {
-        movement_state.subsecond++;
-    }
-}
diff --git a/movement/movement.h b/movement/movement.h
deleted file mode 100644 (file)
index 6747ff6..0000000
+++ /dev/null
@@ -1,316 +0,0 @@
-/*
- * MIT License
- *
- * Copyright (c) 2022 Joey Castillo
- *
- * Permission is hereby granted, free of charge, to any person obtaining a copy
- * of this software and associated documentation files (the "Software"), to deal
- * in the Software without restriction, including without limitation the rights
- * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- * copies of the Software, and to permit persons to whom the Software is
- * furnished to do so, subject to the following conditions:
- *
- * The above copyright notice and this permission notice shall be included in all
- * copies or substantial portions of the Software.
- *
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- * SOFTWARE.
- */
-
-#ifndef MOVEMENT_H_
-#define MOVEMENT_H_
-#include <stdio.h>
-#include <stdbool.h>
-#include "watch.h"
-
-// Movement Preferences
-// These four 32-bit structs store information about the wearer and their preferences. Tentatively, the plan is
-// for Movement to use four 32-bit registers for these preferences and to store them in the RTC's backup registers
-// 0-3, leaving registers 4-7 available for third party watch faces to use as they see fit.
-// * The movement_settings_t struct is provided to all watch faces in the callback functions, and is stored in the
-//   RTC's first backup register (BKUP[0]).
-// * The movement_location_t and movement_birthdate_t types are defined here, and are tentatively meant to be
-//   stored in BKUP[1] and BKUP[2], respectively.
-// * The movement_reserved_t type is here as a placeholder, because I sense there's some other generally useful
-//   stuff we'll want to make available to all watch faces and stash in the BKUP[3] register.
-// This allows these preferences to be stored before entering BACKUP mode and and restored after waking from reset.
-
-// movement_settings_t contains global settings that cover watch behavior, including preferences around clock and unit
-// display, time zones, buzzer behavior, LED color and low energy mode timeouts. These settings are stored in BKUP[0].
-// If your watch face changes one of these settings, you should store your changes in either your loop or resign
-// function by calling `watch_store_backup_data(settings->reg, 0)`. Otherwise it may not persist after a reset event.
-typedef union {
-    struct {
-        bool button_should_sound : 1;       // if true, pressing a button emits a sound.
-        uint8_t to_interval : 2;            // an inactivity interval for asking the active face to resign.
-        bool to_always : 1;                 // if true, always time out from the active face to face 0. otherwise only faces that time out will resign (the default).
-        uint8_t le_interval : 3;            // 0 to disable low energy mode, or an inactivity interval for going into low energy mode.
-        uint8_t led_duration : 3;           // how many seconds to shine the LED for (x2), 0 to shine only while the button is depressed, or all bits set to disable the LED altogether.
-        uint8_t led_red_color : 4;          // for general purpose illumination, the red LED value (0-15)
-        uint8_t led_green_color : 4;        // for general purpose illumination, the green LED value (0-15)
-        uint8_t time_zone : 6;              // an integer representing an index in the time zone table.
-
-        // while Movement itself doesn't implement a clock or display units, it may make sense to include some
-        // global settings for watch faces to check. The 12/24 hour preference could inform a clock or a
-        // time-oriented complication like a sunrise/sunset timer, and a simple locale preference could tell an
-        // altimeter to display feet or meters as easily as it tells a thermometer to display degrees in F or C.
-        bool clock_mode_24h : 1;            // indicates whether clock should use 12 or 24 hour mode.
-        bool clock_24h_leading_zero : 1;    // indicates whether clock should leading zero to indicate 24 hour mode.
-        bool use_imperial_units : 1;        // indicates whether to use metric units (the default) or imperial.
-        bool alarm_enabled : 1;             // indicates whether there is at least one alarm enabled.
-        uint8_t reserved : 5;               // room for more preferences if needed.
-    } bit;
-    uint32_t reg;
-} movement_settings_t;
-
-// movement_location_t is for storing the wearer's location. This will be useful for astronomical calculations such as
-// sunrise and sunset, or predictions of visible satellite passes.
-// If you create a UI for this register or need to access it, look for it in the RTC's BKUP[1] register.
-typedef union {
-    struct {
-        int16_t latitude : 16;   // signed latutide in hundredths of a degree
-        int16_t longitude : 16;  // signed longitude in hundredths of a degree
-    } bit;
-    uint32_t reg;
-} movement_location_t;
-
-// movement_birthdate_t is for storing the user's birth date. This will be useful for calculating the user's age â€” or
-// hey, playing happy birthday at midnight? Fields for birth time (with hour and minute resolution) are also available,
-// partly because they fit so nicely, but also because they can be useful for certain astrological calculations.
-// If you create a UI for birth date or need to access it, look for it in the RTC's BKUP[2] register.
-typedef union {
-    struct {
-        uint16_t year : 12;  // good through the year 4095
-        uint8_t month : 4;
-        uint8_t day : 5;
-        uint8_t hour : 5;
-        uint8_t minute : 6;
-    } bit;
-    uint32_t reg;
-} movement_birthdate_t;
-
-// movement_reserved_t is a placeholder for future use of the BKUP[3] register.
-typedef union {
-    struct {
-        uint32_t reserved : 32;
-    } bit;
-    uint32_t reg;
-} movement_reserved_t;
-
-typedef enum {
-    EVENT_NONE = 0,             // There is no event to report.
-    EVENT_ACTIVATE,             // Your watch face is entering the foreground.
-    EVENT_TICK,                 // Most common event type. Your watch face is being called from the tick callback.
-    EVENT_LOW_ENERGY_UPDATE,    // If the watch is in low energy mode and you are in the foreground, you will get a chance to update the display once per minute.
-    EVENT_BACKGROUND_TASK,      // Your watch face is being invoked to perform a background task. Don't update the display here; you may not be in the foreground.
-    EVENT_TIMEOUT,              // Your watch face has been inactive for a while. You may want to resign, depending on your watch face's intended use case.
-    EVENT_LIGHT_BUTTON_DOWN,    // The light button has been pressed, but not yet released.
-    EVENT_LIGHT_BUTTON_UP,      // The light button was pressed for less than half a second, and released.
-    EVENT_LIGHT_LONG_PRESS,     // The light button was held for over half a second, but not yet released.
-    EVENT_LIGHT_LONG_UP,        // The light button was held for over half a second, and released.
-    EVENT_MODE_BUTTON_DOWN,     // The mode button has been pressed, but not yet released.
-    EVENT_MODE_BUTTON_UP,       // The mode button was pressed for less than half a second, and released.
-    EVENT_MODE_LONG_PRESS,      // The mode button was held for over half a second, but not yet released.
-    EVENT_MODE_LONG_UP,         // The mode button was held for over half a second, and released. NOTE: your watch face will resign immediately after receiving this event.
-    EVENT_ALARM_BUTTON_DOWN,    // The alarm button has been pressed, but not yet released.
-    EVENT_ALARM_BUTTON_UP,      // The alarm button was pressed for less than half a second, and released.
-    EVENT_ALARM_LONG_PRESS,     // The alarm button was held for over half a second, but not yet released.
-    EVENT_ALARM_LONG_UP,        // The alarm button was held for over half a second, and released.
-} movement_event_type_t;
-
-typedef struct {
-    uint8_t event_type;
-    uint8_t subsecond;
-} movement_event_t;
-
-extern const int16_t movement_timezone_offsets[];
-extern const char movement_valid_position_0_chars[];
-extern const char movement_valid_position_1_chars[];
-
-/** @brief Perform setup for your watch face.
-  * @details It's tempting to say this is 'one-time' setup, but technically this function is called more than
-  *          once. When the watch first boots, this function is called with a NULL context_ptr, indicating
-  *          that it is the first run. At this time you should set context_ptr to something non-NULL if you
-  *          need to keep track of any state in your watch face. If your watch face requires any other setup,
-  *          like configuring a pin mode or a peripheral, you may want to do that here too.
-  *          This function will be called again after waking from sleep mode, since sleep mode disables all
-  *          of the device's pins and peripherals.
-  * @param settings A pointer to the global Movement settings. You can use this to inform how you present your
-  *                 display to the user (i.e. taking into account whether they have silenced the buttons, or if
-  *                 they prefer 12 or 24-hour mode). You can also change these settings if you like.
-  * @param watch_face_index The index of this watch face in the global array of watch faces; 0 is the first face,
-  *                         1 is the second, etc. You may stash this value in your context if you wish to reference
-  *                         it later; your watch face's index is set at launch and will not change.
-  * @param context_ptr A pointer to a pointer; at first invocation, this value will be NULL, and you can set it
-  *                    to any value you like. Subsequent invocations will pass in whatever value you previously
-  *                    set. You may want to check if this is NULL and if so, allocate some space to store any
-  *                    data required for your watch face.
-  *
-  */
-typedef void (*watch_face_setup)(movement_settings_t *settings, uint8_t watch_face_index, void ** context_ptr);
-
-/** @brief Prepare to go on-screen.
-  * @details This function is called just before your watch enters the foreground. If your watch face has any
-  *          segments or text that is always displayed, you may want to set that here. In addition, if your
-  *          watch face depends on data from a peripheral (like an I2C sensor), you will likely want to enable
-  *          that peripheral here. In addition, if your watch face requires an update frequncy other than 1 Hz,
-  *          you may want to request that here using the movement_request_tick_frequency function.
-  * @param settings A pointer to the global Movement settings. @see watch_face_setup.
-  * @param context A pointer to your watch face's context. @see watch_face_setup.
-  *
-  */
-typedef void (*watch_face_activate)(movement_settings_t *settings, void *context);
-
-/** @brief Handle events and update the display.
-  * @details This function is called in response to an event. You should set up a switch statement that handles,
-  *          at the very least, the EVENT_TICK and EVENT_MODE_BUTTON_UP event types. The tick event happens once
-  *          per second (or more frequently if you asked for a faster tick with movement_request_tick_frequency).
-  *          The mode button up event occurs when the user presses the MODE button. **Your loop function SHOULD
-  *          call the movement_move_to_next_face function in response to this event.** If you have a good reason
-  *          to override this behavior (e.g. your user interface requires all three buttons), your watch face MUST
-  *          call the movement_move_to_next_face function in response to the EVENT_MODE_LONG_PRESS event. If you
-  *          fail to do this, the user will become stuck on your watch face.
-  * @param event A struct containing information about the event, including its type. @see movement_event_type_t
-  *              for a list of all possible event types.
-  * @param settings A pointer to the global Movement settings. @see watch_face_setup.
-  * @param context A pointer to your application's context. @see watch_face_setup.
-  * @return true if your watch face is prepared for the system to enter STANDBY mode; false to keep the system awake.
-  *         You should almost always return true.
-  *         Note that this return value has no effect if your loop function has called movement_move_to_next_face
-  *         or movement_move_to_face; in that case, your watch face will resign immediately, and the next watch
-  *         face will make the decision on entering standby mode.
-  * @note There are two event types that require some extra thought:
-          The EVENT_LOW_ENERGY_UPDATE event type is a special case. If you are in the foreground when the watch
-          goes into low energy mode, you will receive this tick once a minute (at the top of the minute) so that
-          you can update the screen. Great! But! When you receive this event, all pins and peripherals other than
-          the RTC will have been disabled to save energy. If your display is clock or calendar oriented, this is
-          fine. But if your display requires polling an I2C sensor or reading a value with the ADC, you won't be
-          able to do this. You should either display the name of the watch face in response to the low power tick,
-          or ensure that you resign before low power mode triggers, (e.g. by calling movement_move_to_face(0)).
-          **Your watch face MUST NOT wake up peripherals in response to a low power tick.** The purpose of this
-          mode is to consume as little energy as possible during the (potentially long) intervals when it's
-          unlikely the user is wearing or looking at the watch.
-          EVENT_BACKGROUND_TASK is also a special case. @see watch_face_wants_background_task for details.
-  */
-typedef bool (*watch_face_loop)(movement_event_t event, movement_settings_t *settings, void *context);
-
-/** @brief Prepare to go off-screen.
-  * @details This function is called before your watch face enters the background. If you requested a tick
-  *          frequency other than the standard 1 Hz, **you must call movement_request_tick_frequency(1) here**
-  *          to reset to 1 Hz. You should also disable any peripherals you enabled when you entered the foreground.
-  * @param settings A pointer to the global Movement settings. @see watch_face_setup.
-  * @param context A pointer to your application's context. @see watch_face_setup.
-  */
-typedef void (*watch_face_resign)(movement_settings_t *settings, void *context);
-
-/** @brief OPTIONAL. Request an opportunity to run a background task.
-  * @details Most apps will not need this function, but if you provide it, Movement will call it once per minute in
-  *          both active and low power modes, regardless of whether your app is in the foreground. You can check the
-  *          current time to determine whether you require a background task. If you return true here, Movement will
-  *          immediately call your loop function with an EVENT_BACKGROUND_TASK event. Note that it will not call your
-  *          activate or deactivate functions, since you are not going on screen.
-  *
-  *          Examples of background tasks:
-  *           - Wake and play a sound when an alarm or timer has been triggered.
-  *           - Check the state of an RTC interrupt pin or the timestamp of an RTC interrupt event.
-  *           - Log a data point from a sensor, and then return to sleep mode.
-  *
-  *          Guidelines for background tasks:
-  *           - Assume all peripherals and pins other than the RTC will be disabled when you get an EVENT_BACKGROUND_TASK.
-  *           - Even if your background task involves only the RTC peripheral, try to request background tasks sparingly.
-  *           - If your background task involves an external pin or peripheral, request background tasks no more than once per hour.
-  *           - If you need to enable a pin or a peripheral to perform your task, return it to its original state afterwards.
-  *
-  * @param settings A pointer to the global Movement settings. @see watch_face_setup.
-  * @param context A pointer to your application's context. @see watch_face_setup.
-  * @return true to request a background task; false otherwise.
-  */
-typedef bool (*watch_face_wants_background_task)(movement_settings_t *settings, void *context);
-
-typedef struct {
-    watch_face_setup setup;
-    watch_face_activate activate;
-    watch_face_loop loop;
-    watch_face_resign resign;
-    watch_face_wants_background_task wants_background_task;
-} watch_face_t;
-
-typedef struct {
-    // properties stored in BACKUP register
-    movement_settings_t settings;
-
-    // transient properties
-    int16_t current_face_idx;
-    int16_t next_face_idx;
-    bool watch_face_changed;
-    bool fast_tick_enabled;
-    int16_t fast_ticks;
-
-    // LED stuff
-    int16_t light_ticks;
-
-    // alarm stuff
-    int16_t alarm_ticks;
-    bool is_buzzing;
-    BuzzerNote alarm_note;
-
-    // button tracking for long press
-    uint16_t light_down_timestamp;
-    uint16_t mode_down_timestamp;
-    uint16_t alarm_down_timestamp;
-
-    // background task handling
-    bool needs_background_tasks_handled;
-    bool has_scheduled_background_task;
-    bool needs_wake;
-
-    // low energy mode countdown
-    int32_t le_mode_ticks;
-
-    // app resignation countdown (TODO: consolidate with LE countdown?)
-    int16_t timeout_ticks;
-
-    // stuff for subsecond tracking
-    uint8_t tick_frequency;
-    uint8_t last_second;
-    uint8_t subsecond;
-
-    // backup register stuff
-    uint8_t next_available_backup_register;
-} movement_state_t;
-
-void movement_move_to_face(uint8_t watch_face_index);
-void movement_move_to_next_face(void);
-
-bool movement_default_loop_handler(movement_event_t event, movement_settings_t *settings);
-
-void movement_illuminate_led(void);
-
-void movement_request_tick_frequency(uint8_t freq);
-
-// note: watch faces can only schedule a background task when in the foreground, since
-// movement will associate the scheduled task with the currently active face.
-void movement_schedule_background_task(watch_date_time date_time);
-
-// note: watch faces can only cancel their background task when in the foreground, since
-// movement will associate the scheduled task with the currently active face.
-void movement_cancel_background_task(void);
-
-// these functions should work around the limitation of the above functions, which will be deprecated.
-void movement_schedule_background_task_for_face(uint8_t watch_face_index, watch_date_time date_time);
-void movement_cancel_background_task_for_face(uint8_t watch_face_index);
-
-void movement_request_wake(void);
-
-void movement_play_signal(void);
-void movement_play_alarm(void);
-void movement_play_alarm_beeps(uint8_t rounds, BuzzerNote alarm_note);
-
-uint8_t movement_claim_backup_register(void);
-
-#endif // MOVEMENT_H_
diff --git a/movement/watch_faces/clock/simple_clock_face.c b/movement/watch_faces/clock/simple_clock_face.c
deleted file mode 100644 (file)
index 68e1ccd..0000000
+++ /dev/null
@@ -1,177 +0,0 @@
-/*
- * MIT License
- *
- * Copyright (c) 2022 Joey Castillo
- *
- * Permission is hereby granted, free of charge, to any person obtaining a copy
- * of this software and associated documentation files (the "Software"), to deal
- * in the Software without restriction, including without limitation the rights
- * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- * copies of the Software, and to permit persons to whom the Software is
- * furnished to do so, subject to the following conditions:
- *
- * The above copyright notice and this permission notice shall be included in all
- * copies or substantial portions of the Software.
- *
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- * SOFTWARE.
- */
-
-#include <stdlib.h>
-#include "simple_clock_face.h"
-#include "watch.h"
-#include "watch_utility.h"
-#include "watch_private_display.h"
-
-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);
-    else watch_clear_indicator(WATCH_INDICATOR_SIGNAL);
-}
-
-void simple_clock_face_setup(movement_settings_t *settings, uint8_t watch_face_index, void ** context_ptr) {
-    (void) settings;
-    (void) watch_face_index;
-
-    if (*context_ptr == NULL) {
-        *context_ptr = malloc(sizeof(simple_clock_state_t));
-        simple_clock_state_t *state = (simple_clock_state_t *)*context_ptr;
-        state->signal_enabled = false;
-        state->watch_face_index = watch_face_index;
-    }
-}
-
-void simple_clock_face_activate(movement_settings_t *settings, void *context) {
-    simple_clock_state_t *state = (simple_clock_state_t *)context;
-
-    if (watch_tick_animation_is_running()) watch_stop_tick_animation();
-
-#ifdef CLOCK_FACE_24H_ONLY
-    watch_set_indicator(WATCH_INDICATOR_24H);
-#else
-    if (settings->bit.clock_mode_24h) watch_set_indicator(WATCH_INDICATOR_24H);
-#endif
-
-    // handle chime indicator
-    if (state->signal_enabled) watch_set_indicator(WATCH_INDICATOR_BELL);
-    else watch_clear_indicator(WATCH_INDICATOR_BELL);
-
-    // show alarm indicator if there is an active alarm
-    _update_alarm_indicator(settings->bit.alarm_enabled, state);
-
-    watch_set_colon();
-
-    // this ensures that none of the timestamp fields will match, so we can re-render them all.
-    state->previous_date_time = 0xFFFFFFFF;
-}
-
-bool simple_clock_face_loop(movement_event_t event, movement_settings_t *settings, void *context) {
-    simple_clock_state_t *state = (simple_clock_state_t *)context;
-    char buf[11];
-    uint8_t pos;
-
-    watch_date_time date_time;
-    uint32_t previous_date_time;
-    switch (event.event_type) {
-        case EVENT_ACTIVATE:
-        case EVENT_TICK:
-        case EVENT_LOW_ENERGY_UPDATE:
-            date_time = watch_rtc_get_date_time();
-            previous_date_time = state->previous_date_time;
-            state->previous_date_time = date_time.reg;
-
-            // check the battery voltage once a day...
-            if (date_time.unit.day != state->last_battery_check) {
-                state->last_battery_check = date_time.unit.day;
-                watch_enable_adc();
-                uint16_t voltage = watch_get_vcc_voltage();
-                watch_disable_adc();
-                // 2.2 volts will happen when the battery has maybe 5-10% remaining?
-                // we can refine this later.
-                state->battery_low = (voltage < 2200);
-            }
-
-            // ...and set the LAP indicator if low.
-            if (state->battery_low) watch_set_indicator(WATCH_INDICATOR_LAP);
-
-            bool set_leading_zero = false;
-            if ((date_time.reg >> 6) == (previous_date_time >> 6) && event.event_type != EVENT_LOW_ENERGY_UPDATE) {
-                // everything before seconds is the same, don't waste cycles setting those segments.
-                watch_display_character_lp_seconds('0' + date_time.unit.second / 10, 8);
-                watch_display_character_lp_seconds('0' + date_time.unit.second % 10, 9);
-                break;
-            } else if ((date_time.reg >> 12) == (previous_date_time >> 12) && event.event_type != EVENT_LOW_ENERGY_UPDATE) {
-                // everything before minutes is the same.
-                pos = 6;
-                sprintf(buf, "%02d%02d", date_time.unit.minute, date_time.unit.second);
-            } else {
-                // other stuff changed; let's do it all.
-#ifndef CLOCK_FACE_24H_ONLY
-                if (!settings->bit.clock_mode_24h) {
-                    // if we are in 12 hour mode, do some cleanup.
-                    if (date_time.unit.hour < 12) {
-                        watch_clear_indicator(WATCH_INDICATOR_PM);
-                    } else {
-                        watch_set_indicator(WATCH_INDICATOR_PM);
-                    }
-                    date_time.unit.hour %= 12;
-                    if (date_time.unit.hour == 0) date_time.unit.hour = 12;
-                }
-#endif
-
-                if (settings->bit.clock_mode_24h && settings->bit.clock_24h_leading_zero && date_time.unit.hour < 10) {
-                    set_leading_zero = true;
-                }
-
-                pos = 0;
-                if (event.event_type == EVENT_LOW_ENERGY_UPDATE) {
-                    if (!watch_tick_animation_is_running()) watch_start_tick_animation(500);
-                    sprintf(buf, "%s%2d%2d%02d  ", watch_utility_get_weekday(date_time), date_time.unit.day, date_time.unit.hour, date_time.unit.minute);
-                } else {
-                    sprintf(buf, "%s%2d%2d%02d%02d", watch_utility_get_weekday(date_time), date_time.unit.day, date_time.unit.hour, date_time.unit.minute, date_time.unit.second);
-                }
-            }
-            watch_display_string(buf, pos);
-
-            if (set_leading_zero)
-                watch_display_string("0", 4);
-
-            // handle alarm indicator
-            if (state->alarm_enabled != settings->bit.alarm_enabled) _update_alarm_indicator(settings->bit.alarm_enabled, state);
-            break;
-        case EVENT_ALARM_LONG_PRESS:
-            state->signal_enabled = !state->signal_enabled;
-            if (state->signal_enabled) watch_set_indicator(WATCH_INDICATOR_BELL);
-            else watch_clear_indicator(WATCH_INDICATOR_BELL);
-            break;
-        case EVENT_BACKGROUND_TASK:
-            // uncomment this line to snap back to the clock face when the hour signal sounds:
-            // movement_move_to_face(state->watch_face_index);
-            movement_play_signal();
-            break;
-        default:
-            return movement_default_loop_handler(event, settings);
-    }
-
-    return true;
-}
-
-void simple_clock_face_resign(movement_settings_t *settings, void *context) {
-    (void) settings;
-    (void) context;
-}
-
-bool simple_clock_face_wants_background_task(movement_settings_t *settings, void *context) {
-    (void) settings;
-    simple_clock_state_t *state = (simple_clock_state_t *)context;
-    if (!state->signal_enabled) return false;
-
-    watch_date_time date_time = watch_rtc_get_date_time();
-
-    return date_time.unit.minute == 0;
-}
diff --git a/movement/watch_faces/clock/simple_clock_face.h b/movement/watch_faces/clock/simple_clock_face.h
deleted file mode 100644 (file)
index e74a6e8..0000000
+++ /dev/null
@@ -1,62 +0,0 @@
-/*
- * MIT License
- *
- * Copyright (c) 2022 Joey Castillo
- *
- * Permission is hereby granted, free of charge, to any person obtaining a copy
- * of this software and associated documentation files (the "Software"), to deal
- * in the Software without restriction, including without limitation the rights
- * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- * copies of the Software, and to permit persons to whom the Software is
- * furnished to do so, subject to the following conditions:
- *
- * The above copyright notice and this permission notice shall be included in all
- * copies or substantial portions of the Software.
- *
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- * SOFTWARE.
- */
-
-#ifndef SIMPLE_CLOCK_FACE_H_
-#define SIMPLE_CLOCK_FACE_H_
-
-/*
- * SIMPLE CLOCK FACE
- *
- * Displays the current time, matching the original operation of the watch.
- * This is the default display mode in most watch configurations.
- *
- * Long-press ALARM to toggle the hourly chime.
- */
-
-#include "movement.h"
-
-typedef struct {
-    uint32_t previous_date_time;
-    uint8_t last_battery_check;
-    uint8_t watch_face_index;
-    bool signal_enabled;
-    bool battery_low;
-    bool alarm_enabled;
-} simple_clock_state_t;
-
-void simple_clock_face_setup(movement_settings_t *settings, uint8_t watch_face_index, void ** context_ptr);
-void simple_clock_face_activate(movement_settings_t *settings, void *context);
-bool simple_clock_face_loop(movement_event_t event, movement_settings_t *settings, void *context);
-void simple_clock_face_resign(movement_settings_t *settings, void *context);
-bool simple_clock_face_wants_background_task(movement_settings_t *settings, void *context);
-
-#define simple_clock_face ((const watch_face_t){ \
-    simple_clock_face_setup, \
-    simple_clock_face_activate, \
-    simple_clock_face_loop, \
-    simple_clock_face_resign, \
-    simple_clock_face_wants_background_task, \
-})
-
-#endif // SIMPLE_CLOCK_FACE_H_
diff --git a/movement/watch_faces/settings/set_time_face.c b/movement/watch_faces/settings/set_time_face.c
deleted file mode 100644 (file)
index 605849f..0000000
+++ /dev/null
@@ -1,188 +0,0 @@
-/*
- * MIT License
- *
- * Copyright (c) 2022 Joey Castillo
- *
- * Permission is hereby granted, free of charge, to any person obtaining a copy
- * of this software and associated documentation files (the "Software"), to deal
- * in the Software without restriction, including without limitation the rights
- * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- * copies of the Software, and to permit persons to whom the Software is
- * furnished to do so, subject to the following conditions:
- *
- * The above copyright notice and this permission notice shall be included in all
- * copies or substantial portions of the Software.
- *
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- * SOFTWARE.
- */
-
-#include <stdlib.h>
-#include "set_time_face.h"
-#include "watch.h"
-#include "watch_utility.h"
-
-#define SET_TIME_FACE_NUM_SETTINGS (7)
-const char set_time_face_titles[SET_TIME_FACE_NUM_SETTINGS][3] = {"HR", "M1", "SE", "YR", "MO", "DA", "ZO"};
-
-static bool _quick_ticks_running;
-
-static void _handle_alarm_button(movement_settings_t *settings, watch_date_time date_time, uint8_t current_page) {
-    // handles short or long pressing of the alarm button
-
-    switch (current_page) {
-        case 0: // hour
-            date_time.unit.hour = (date_time.unit.hour + 1) % 24;
-            break;
-        case 1: // minute
-            date_time.unit.minute = (date_time.unit.minute + 1) % 60;
-            break;
-        case 2: // second
-            date_time.unit.second = 0;
-            break;
-        case 3: // year
-            date_time.unit.year = ((date_time.unit.year % 60) + 1);
-            break;
-        case 4: // month
-            date_time.unit.month = (date_time.unit.month % 12) + 1;
-            break;
-        case 5: { // day
-            date_time.unit.day = date_time.unit.day + 1;
-            break;
-        }
-        case 6: // time zone
-            settings->bit.time_zone++;
-            if (settings->bit.time_zone > 40) settings->bit.time_zone = 0;
-            break;
-    }
-    if (date_time.unit.day > days_in_month(date_time.unit.month, date_time.unit.year + WATCH_RTC_REFERENCE_YEAR))
-        date_time.unit.day = 1;
-    watch_rtc_set_date_time(date_time);
-}
-
-static void _abort_quick_ticks() {
-    if (_quick_ticks_running) {
-        _quick_ticks_running = false;
-        movement_request_tick_frequency(4);
-    }
-}
-
-void set_time_face_setup(movement_settings_t *settings, uint8_t watch_face_index, void ** context_ptr) {
-    (void) settings;
-    (void) watch_face_index;
-    if (*context_ptr == NULL) *context_ptr = malloc(sizeof(uint8_t));
-}
-
-void set_time_face_activate(movement_settings_t *settings, void *context) {
-    (void) settings;
-    *((uint8_t *)context) = 0;
-    movement_request_tick_frequency(4);
-    _quick_ticks_running = false;
-}
-
-bool set_time_face_loop(movement_event_t event, movement_settings_t *settings, void *context) {
-    uint8_t current_page = *((uint8_t *)context);
-    watch_date_time date_time = watch_rtc_get_date_time();
-
-    switch (event.event_type) {
-        case EVENT_TICK:
-            if (_quick_ticks_running) {
-                if (watch_get_pin_level(BTN_ALARM)) _handle_alarm_button(settings, date_time, current_page);
-                else _abort_quick_ticks();
-            }
-            break;
-        case EVENT_ALARM_LONG_PRESS:
-            if (current_page != 2) {
-                _quick_ticks_running = true;
-                movement_request_tick_frequency(8);
-            }
-            break;
-        case EVENT_ALARM_LONG_UP:
-            _abort_quick_ticks();
-            break;
-        case EVENT_MODE_BUTTON_UP:
-            _abort_quick_ticks();
-            movement_move_to_next_face();
-            return false;
-        case EVENT_LIGHT_BUTTON_DOWN:
-            current_page = (current_page + 1) % SET_TIME_FACE_NUM_SETTINGS;
-            *((uint8_t *)context) = current_page;
-            break;
-        case EVENT_ALARM_BUTTON_UP:
-            _abort_quick_ticks();
-            _handle_alarm_button(settings, date_time, current_page);
-            break;
-        case EVENT_TIMEOUT:
-            _abort_quick_ticks();
-            movement_move_to_face(0);
-            break;
-        default:
-            return movement_default_loop_handler(event, settings);
-    }
-
-    char buf[11];
-    bool set_leading_zero = false;
-    if (current_page < 3) {
-        watch_set_colon();
-        if (settings->bit.clock_mode_24h) {
-            if (!settings->bit.clock_24h_leading_zero)
-                watch_set_indicator(WATCH_INDICATOR_24H);
-            else if (date_time.unit.hour < 10)
-                set_leading_zero = true;
-            sprintf(buf, "%s  %2d%02d%02d", set_time_face_titles[current_page], date_time.unit.hour, date_time.unit.minute, date_time.unit.second);
-        } else {
-            sprintf(buf, "%s  %2d%02d%02d", set_time_face_titles[current_page], (date_time.unit.hour % 12) ? (date_time.unit.hour % 12) : 12, date_time.unit.minute, date_time.unit.second);
-            if (date_time.unit.hour < 12) watch_clear_indicator(WATCH_INDICATOR_PM);
-            else watch_set_indicator(WATCH_INDICATOR_PM);
-        }
-    } else if (current_page < 6) {
-        watch_clear_colon();
-        watch_clear_indicator(WATCH_INDICATOR_24H);
-        watch_clear_indicator(WATCH_INDICATOR_PM);
-        sprintf(buf, "%s  %2d%02d%02d", set_time_face_titles[current_page], date_time.unit.year + 20, date_time.unit.month, date_time.unit.day);
-    } else {
-        if (event.subsecond % 2) {
-            watch_clear_colon();
-            sprintf(buf, "%s        ", set_time_face_titles[current_page]);
-        } else {
-            watch_set_colon();
-            sprintf(buf, "%s %3d%02d  ", set_time_face_titles[current_page], (int8_t) (movement_timezone_offsets[settings->bit.time_zone] / 60), (int8_t) (movement_timezone_offsets[settings->bit.time_zone] % 60) * (movement_timezone_offsets[settings->bit.time_zone] < 0 ? -1 : 1));
-        }
-    }
-
-    // blink up the parameter we're setting
-    if (event.subsecond % 2 && !_quick_ticks_running) {
-        switch (current_page) {
-            case 0:
-            case 3:
-                buf[4] = buf[5] = ' ';
-                break;
-            case 1:
-            case 4:
-                buf[6] = buf[7] = ' ';
-                break;
-            case 2:
-            case 5:
-                buf[8] = buf[9] = ' ';
-                break;
-        }
-    }
-
-    watch_display_string(buf, 0);
-    if (set_leading_zero)
-        watch_display_string("0", 4);
-
-    return true;
-}
-
-void set_time_face_resign(movement_settings_t *settings, void *context) {
-    (void) settings;
-    (void) context;
-    watch_set_led_off();
-    watch_store_backup_data(settings->reg, 0);
-}
diff --git a/movement/watch_faces/settings/set_time_face.h b/movement/watch_faces/settings/set_time_face.h
deleted file mode 100644 (file)
index f66dc9e..0000000
+++ /dev/null
@@ -1,60 +0,0 @@
-/*
- * MIT License
- *
- * Copyright (c) 2022 Joey Castillo
- *
- * Permission is hereby granted, free of charge, to any person obtaining a copy
- * of this software and associated documentation files (the "Software"), to deal
- * in the Software without restriction, including without limitation the rights
- * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- * copies of the Software, and to permit persons to whom the Software is
- * furnished to do so, subject to the following conditions:
- *
- * The above copyright notice and this permission notice shall be included in all
- * copies or substantial portions of the Software.
- *
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- * SOFTWARE.
- */
-
-#ifndef SET_TIME_FACE_H_
-#define SET_TIME_FACE_H_
-
-/*
- * SET TIME face
- *
- * The default method for adjusting Sensor Watch time.
- *
- * The Time Set watch face allows you to set the time on Sensor Watch. Use
- * the LIGHT button to advance through the field you are setting, and the
- * ALARM button to change the value in that field. The fields are, in order:
- * Hour, Minute, Second, Year, Month, Day and Time Zone.
- *
- * For features like World Clock and Sunrise/Sunset to work correctly, you
- * must set the time to your local time, and the time zone to your local time
- * zone. This allows Sensor Watch to correctly offset the time. This also
- * means that when daylight savings time starts or ends, you must update
- * both the time and the time zone on this screen.
- */
-
-#include "movement.h"
-
-void set_time_face_setup(movement_settings_t *settings, uint8_t watch_face_index, void ** context_ptr);
-void set_time_face_activate(movement_settings_t *settings, void *context);
-bool set_time_face_loop(movement_event_t event, movement_settings_t *settings, void *context);
-void set_time_face_resign(movement_settings_t *settings, void *context);
-
-#define set_time_face ((const watch_face_t){ \
-    set_time_face_setup, \
-    set_time_face_activate, \
-    set_time_face_loop, \
-    set_time_face_resign, \
-    NULL, \
-})
-
-#endif // SET_TIME_FACE_H_
diff --git a/movement_config.h b/movement_config.h
new file mode 100644 (file)
index 0000000..9412690
--- /dev/null
@@ -0,0 +1,91 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2022 Joey Castillo
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+#ifndef MOVEMENT_CONFIG_H_
+#define MOVEMENT_CONFIG_H_
+
+#include "movement_faces.h"
+
+const watch_face_t watch_faces[] = {
+    simple_clock_face,
+    set_time_face,
+};
+
+#define MOVEMENT_NUM_FACES (sizeof(watch_faces) / sizeof(watch_face_t))
+
+/* Determines what face to go to from the first face on long press of the Mode button.
+ * Also excludes these faces from the normal rotation.
+ * In the default firmware, this lets you access temperature and battery voltage with a long press of Mode.
+ * Some folks also like to use this to hide the preferences and time set faces from the normal rotation.
+ * If you don't want any faces to be excluded, set this to 0 and a long Mode press will have no effect.
+ */
+#define MOVEMENT_SECONDARY_FACE_INDEX (MOVEMENT_NUM_FACES - 2) // or (0)
+
+/* Custom hourly chime tune. Check movement_custom_signal_tunes.h for options. */
+#define SIGNAL_TUNE_DEFAULT
+
+/* Determines the intensity of the led colors
+ * Set a hex value 0-15 with 0x0 being off and 0xF being max intensity
+ */
+#define MOVEMENT_DEFAULT_GREEN_COLOR 0xF
+#define MOVEMENT_DEFAULT_RED_COLOR 0x0
+
+/* Set to true for 24h mode or false for 12h mode */
+#define MOVEMENT_DEFAULT_24H_MODE false
+
+/* Enable or disable the sound on mode button press */
+#define MOVEMENT_DEFAULT_BUTTON_SOUND true
+
+/* Set the timeout before switching back to the main watch face
+ * Valid values are:
+ * 0: 60 seconds
+ * 1: 2 minutes
+ * 2: 5 minutes
+ * 3: 30 minutes
+ */
+#define MOVEMENT_DEFAULT_TIMEOUT_INTERVAL 0
+
+/* Set the timeout before switching to low energy mode
+ * Valid values are:
+ * 0: Never
+ * 1: 1 hour
+ * 2: 2 hours
+ * 3: 6 hours
+ * 4: 12 hours
+ * 5: 1 day
+ * 6: 2 days
+ * 7: 7 days
+ */
+#define MOVEMENT_DEFAULT_LOW_ENERGY_INTERVAL 1
+
+/* Set the led duration
+ * Valid values are:
+ * 0: No LED
+ * 1: 1 second
+ * 2: 3 seconds
+ * 3: 5 seconds
+ */
+#define MOVEMENT_DEFAULT_LED_DURATION 1
+
+#endif // MOVEMENT_CONFIG_H_
diff --git a/movement_faces.h b/movement_faces.h
new file mode 100644 (file)
index 0000000..9e3f7cf
--- /dev/null
@@ -0,0 +1,29 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2022 Joey Castillo
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+#pragma once
+
+#include "simple_clock_face.h"
+#include "set_time_face.h"
+// New includes go above this line.
diff --git a/watch-faces.mk b/watch-faces.mk
new file mode 100644 (file)
index 0000000..14e4319
--- /dev/null
@@ -0,0 +1,3 @@
+SRCS += \
+  ./watch-faces/clock/simple_clock_face.c \
+  ./watch-faces/settings/set_time_face.c \
diff --git a/watch-faces/clock/simple_clock_face.c b/watch-faces/clock/simple_clock_face.c
new file mode 100644 (file)
index 0000000..bcf3d8e
--- /dev/null
@@ -0,0 +1,177 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2022 Joey Castillo
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+#include <stdlib.h>
+#include "simple_clock_face.h"
+#include "watch.h"
+#include "watch_utility.h"
+#include "watch_common_display.h"
+
+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);
+    else watch_clear_indicator(WATCH_INDICATOR_SIGNAL);
+}
+
+void simple_clock_face_setup(movement_settings_t *settings, uint8_t watch_face_index, void ** context_ptr) {
+    (void) settings;
+    (void) watch_face_index;
+
+    if (*context_ptr == NULL) {
+        *context_ptr = malloc(sizeof(simple_clock_state_t));
+        simple_clock_state_t *state = (simple_clock_state_t *)*context_ptr;
+        state->signal_enabled = false;
+        state->watch_face_index = watch_face_index;
+    }
+}
+
+void simple_clock_face_activate(movement_settings_t *settings, void *context) {
+    simple_clock_state_t *state = (simple_clock_state_t *)context;
+
+    if (watch_tick_animation_is_running()) watch_stop_tick_animation();
+
+#ifdef CLOCK_FACE_24H_ONLY
+    watch_set_indicator(WATCH_INDICATOR_24H);
+#else
+    if (settings->bit.clock_mode_24h) watch_set_indicator(WATCH_INDICATOR_24H);
+#endif
+
+    // handle chime indicator
+    if (state->signal_enabled) watch_set_indicator(WATCH_INDICATOR_BELL);
+    else watch_clear_indicator(WATCH_INDICATOR_BELL);
+
+    // show alarm indicator if there is an active alarm
+    _update_alarm_indicator(settings->bit.alarm_enabled, state);
+
+    watch_set_colon();
+
+    // this ensures that none of the timestamp fields will match, so we can re-render them all.
+    state->previous_date_time = 0xFFFFFFFF;
+}
+
+bool simple_clock_face_loop(movement_event_t event, movement_settings_t *settings, void *context) {
+    simple_clock_state_t *state = (simple_clock_state_t *)context;
+    char buf[11];
+    uint8_t pos;
+
+    watch_date_time date_time;
+    uint32_t previous_date_time;
+    switch (event.event_type) {
+        case EVENT_ACTIVATE:
+        case EVENT_TICK:
+        case EVENT_LOW_ENERGY_UPDATE:
+            date_time = watch_rtc_get_date_time();
+            previous_date_time = state->previous_date_time;
+            state->previous_date_time = date_time.reg;
+
+            // check the battery voltage once a day...
+            if (date_time.unit.day != state->last_battery_check) {
+                state->last_battery_check = date_time.unit.day;
+                watch_enable_adc();
+                uint16_t voltage = watch_get_vcc_voltage();
+                watch_disable_adc();
+                // 2.2 volts will happen when the battery has maybe 5-10% remaining?
+                // we can refine this later.
+                state->battery_low = (voltage < 2200);
+            }
+
+            // ...and set the LAP indicator if low.
+            if (state->battery_low) watch_set_indicator(WATCH_INDICATOR_LAP);
+
+            bool set_leading_zero = false;
+            if ((date_time.reg >> 6) == (previous_date_time >> 6) && event.event_type != EVENT_LOW_ENERGY_UPDATE) {
+                // everything before seconds is the same, don't waste cycles setting those segments.
+                watch_display_character_lp_seconds('0' + date_time.unit.second / 10, 8);
+                watch_display_character_lp_seconds('0' + date_time.unit.second % 10, 9);
+                break;
+            } else if ((date_time.reg >> 12) == (previous_date_time >> 12) && event.event_type != EVENT_LOW_ENERGY_UPDATE) {
+                // everything before minutes is the same.
+                pos = 6;
+                sprintf(buf, "%02d%02d", date_time.unit.minute, date_time.unit.second);
+            } else {
+                // other stuff changed; let's do it all.
+#ifndef CLOCK_FACE_24H_ONLY
+                if (!settings->bit.clock_mode_24h) {
+                    // if we are in 12 hour mode, do some cleanup.
+                    if (date_time.unit.hour < 12) {
+                        watch_clear_indicator(WATCH_INDICATOR_PM);
+                    } else {
+                        watch_set_indicator(WATCH_INDICATOR_PM);
+                    }
+                    date_time.unit.hour %= 12;
+                    if (date_time.unit.hour == 0) date_time.unit.hour = 12;
+                }
+#endif
+
+                if (settings->bit.clock_mode_24h && settings->bit.clock_24h_leading_zero && date_time.unit.hour < 10) {
+                    set_leading_zero = true;
+                }
+
+                pos = 0;
+                if (event.event_type == EVENT_LOW_ENERGY_UPDATE) {
+                    if (!watch_tick_animation_is_running()) watch_start_tick_animation(500);
+                    sprintf(buf, "%s%2d%2d%02d  ", watch_utility_get_weekday(date_time), date_time.unit.day, date_time.unit.hour, date_time.unit.minute);
+                } else {
+                    sprintf(buf, "%s%2d%2d%02d%02d", watch_utility_get_weekday(date_time), date_time.unit.day, date_time.unit.hour, date_time.unit.minute, date_time.unit.second);
+                }
+            }
+            watch_display_string(buf, pos);
+
+            if (set_leading_zero)
+                watch_display_string("0", 4);
+
+            // handle alarm indicator
+            if (state->alarm_enabled != settings->bit.alarm_enabled) _update_alarm_indicator(settings->bit.alarm_enabled, state);
+            break;
+        case EVENT_ALARM_LONG_PRESS:
+            state->signal_enabled = !state->signal_enabled;
+            if (state->signal_enabled) watch_set_indicator(WATCH_INDICATOR_BELL);
+            else watch_clear_indicator(WATCH_INDICATOR_BELL);
+            break;
+        case EVENT_BACKGROUND_TASK:
+            // uncomment this line to snap back to the clock face when the hour signal sounds:
+            // movement_move_to_face(state->watch_face_index);
+            movement_play_signal();
+            break;
+        default:
+            return movement_default_loop_handler(event, settings);
+    }
+
+    return true;
+}
+
+void simple_clock_face_resign(movement_settings_t *settings, void *context) {
+    (void) settings;
+    (void) context;
+}
+
+bool simple_clock_face_wants_background_task(movement_settings_t *settings, void *context) {
+    (void) settings;
+    simple_clock_state_t *state = (simple_clock_state_t *)context;
+    if (!state->signal_enabled) return false;
+
+    watch_date_time date_time = watch_rtc_get_date_time();
+
+    return date_time.unit.minute == 0;
+}
diff --git a/watch-faces/clock/simple_clock_face.h b/watch-faces/clock/simple_clock_face.h
new file mode 100644 (file)
index 0000000..e74a6e8
--- /dev/null
@@ -0,0 +1,62 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2022 Joey Castillo
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+#ifndef SIMPLE_CLOCK_FACE_H_
+#define SIMPLE_CLOCK_FACE_H_
+
+/*
+ * SIMPLE CLOCK FACE
+ *
+ * Displays the current time, matching the original operation of the watch.
+ * This is the default display mode in most watch configurations.
+ *
+ * Long-press ALARM to toggle the hourly chime.
+ */
+
+#include "movement.h"
+
+typedef struct {
+    uint32_t previous_date_time;
+    uint8_t last_battery_check;
+    uint8_t watch_face_index;
+    bool signal_enabled;
+    bool battery_low;
+    bool alarm_enabled;
+} simple_clock_state_t;
+
+void simple_clock_face_setup(movement_settings_t *settings, uint8_t watch_face_index, void ** context_ptr);
+void simple_clock_face_activate(movement_settings_t *settings, void *context);
+bool simple_clock_face_loop(movement_event_t event, movement_settings_t *settings, void *context);
+void simple_clock_face_resign(movement_settings_t *settings, void *context);
+bool simple_clock_face_wants_background_task(movement_settings_t *settings, void *context);
+
+#define simple_clock_face ((const watch_face_t){ \
+    simple_clock_face_setup, \
+    simple_clock_face_activate, \
+    simple_clock_face_loop, \
+    simple_clock_face_resign, \
+    simple_clock_face_wants_background_task, \
+})
+
+#endif // SIMPLE_CLOCK_FACE_H_
diff --git a/watch-faces/settings/set_time_face.c b/watch-faces/settings/set_time_face.c
new file mode 100644 (file)
index 0000000..09bda39
--- /dev/null
@@ -0,0 +1,188 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2022 Joey Castillo
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+#include <stdlib.h>
+#include "set_time_face.h"
+#include "watch.h"
+#include "watch_utility.h"
+
+#define SET_TIME_FACE_NUM_SETTINGS (7)
+const char set_time_face_titles[SET_TIME_FACE_NUM_SETTINGS][3] = {"HR", "M1", "SE", "YR", "MO", "DA", "ZO"};
+
+static bool _quick_ticks_running;
+
+static void _handle_alarm_button(movement_settings_t *settings, watch_date_time date_time, uint8_t current_page) {
+    // handles short or long pressing of the alarm button
+
+    switch (current_page) {
+        case 0: // hour
+            date_time.unit.hour = (date_time.unit.hour + 1) % 24;
+            break;
+        case 1: // minute
+            date_time.unit.minute = (date_time.unit.minute + 1) % 60;
+            break;
+        case 2: // second
+            date_time.unit.second = 0;
+            break;
+        case 3: // year
+            date_time.unit.year = ((date_time.unit.year % 60) + 1);
+            break;
+        case 4: // month
+            date_time.unit.month = (date_time.unit.month % 12) + 1;
+            break;
+        case 5: { // day
+            date_time.unit.day = date_time.unit.day + 1;
+            break;
+        }
+        case 6: // time zone
+            settings->bit.time_zone++;
+            if (settings->bit.time_zone > 40) settings->bit.time_zone = 0;
+            break;
+    }
+    if (date_time.unit.day > days_in_month(date_time.unit.month, date_time.unit.year + WATCH_RTC_REFERENCE_YEAR))
+        date_time.unit.day = 1;
+    watch_rtc_set_date_time(date_time);
+}
+
+static void _abort_quick_ticks() {
+    if (_quick_ticks_running) {
+        _quick_ticks_running = false;
+        movement_request_tick_frequency(4);
+    }
+}
+
+void set_time_face_setup(movement_settings_t *settings, uint8_t watch_face_index, void ** context_ptr) {
+    (void) settings;
+    (void) watch_face_index;
+    if (*context_ptr == NULL) *context_ptr = malloc(sizeof(uint8_t));
+}
+
+void set_time_face_activate(movement_settings_t *settings, void *context) {
+    (void) settings;
+    *((uint8_t *)context) = 0;
+    movement_request_tick_frequency(4);
+    _quick_ticks_running = false;
+}
+
+bool set_time_face_loop(movement_event_t event, movement_settings_t *settings, void *context) {
+    uint8_t current_page = *((uint8_t *)context);
+    watch_date_time date_time = watch_rtc_get_date_time();
+
+    switch (event.event_type) {
+        case EVENT_TICK:
+            if (_quick_ticks_running) {
+                if (HAL_GPIO_BTN_ALARM_read()) _handle_alarm_button(settings, date_time, current_page);
+                else _abort_quick_ticks();
+            }
+            break;
+        case EVENT_ALARM_LONG_PRESS:
+            if (current_page != 2) {
+                _quick_ticks_running = true;
+                movement_request_tick_frequency(8);
+            }
+            break;
+        case EVENT_ALARM_LONG_UP:
+            _abort_quick_ticks();
+            break;
+        case EVENT_MODE_BUTTON_UP:
+            _abort_quick_ticks();
+            movement_move_to_next_face();
+            return false;
+        case EVENT_LIGHT_BUTTON_DOWN:
+            current_page = (current_page + 1) % SET_TIME_FACE_NUM_SETTINGS;
+            *((uint8_t *)context) = current_page;
+            break;
+        case EVENT_ALARM_BUTTON_UP:
+            _abort_quick_ticks();
+            _handle_alarm_button(settings, date_time, current_page);
+            break;
+        case EVENT_TIMEOUT:
+            _abort_quick_ticks();
+            movement_move_to_face(0);
+            break;
+        default:
+            return movement_default_loop_handler(event, settings);
+    }
+
+    char buf[11];
+    bool set_leading_zero = false;
+    if (current_page < 3) {
+        watch_set_colon();
+        if (settings->bit.clock_mode_24h) {
+            if (!settings->bit.clock_24h_leading_zero)
+                watch_set_indicator(WATCH_INDICATOR_24H);
+            else if (date_time.unit.hour < 10)
+                set_leading_zero = true;
+            sprintf(buf, "%s  %2d%02d%02d", set_time_face_titles[current_page], date_time.unit.hour, date_time.unit.minute, date_time.unit.second);
+        } else {
+            sprintf(buf, "%s  %2d%02d%02d", set_time_face_titles[current_page], (date_time.unit.hour % 12) ? (date_time.unit.hour % 12) : 12, date_time.unit.minute, date_time.unit.second);
+            if (date_time.unit.hour < 12) watch_clear_indicator(WATCH_INDICATOR_PM);
+            else watch_set_indicator(WATCH_INDICATOR_PM);
+        }
+    } else if (current_page < 6) {
+        watch_clear_colon();
+        watch_clear_indicator(WATCH_INDICATOR_24H);
+        watch_clear_indicator(WATCH_INDICATOR_PM);
+        sprintf(buf, "%s  %2d%02d%02d", set_time_face_titles[current_page], date_time.unit.year + 20, date_time.unit.month, date_time.unit.day);
+    } else {
+        if (event.subsecond % 2) {
+            watch_clear_colon();
+            sprintf(buf, "%s        ", set_time_face_titles[current_page]);
+        } else {
+            watch_set_colon();
+            sprintf(buf, "%s %3d%02d  ", set_time_face_titles[current_page], (int8_t) (movement_timezone_offsets[settings->bit.time_zone] / 60), (int8_t) (movement_timezone_offsets[settings->bit.time_zone] % 60) * (movement_timezone_offsets[settings->bit.time_zone] < 0 ? -1 : 1));
+        }
+    }
+
+    // blink up the parameter we're setting
+    if (event.subsecond % 2 && !_quick_ticks_running) {
+        switch (current_page) {
+            case 0:
+            case 3:
+                buf[4] = buf[5] = ' ';
+                break;
+            case 1:
+            case 4:
+                buf[6] = buf[7] = ' ';
+                break;
+            case 2:
+            case 5:
+                buf[8] = buf[9] = ' ';
+                break;
+        }
+    }
+
+    watch_display_string(buf, 0);
+    if (set_leading_zero)
+        watch_display_string("0", 4);
+
+    return true;
+}
+
+void set_time_face_resign(movement_settings_t *settings, void *context) {
+    (void) settings;
+    (void) context;
+    watch_set_led_off();
+    watch_store_backup_data(settings->reg, 0);
+}
diff --git a/watch-faces/settings/set_time_face.h b/watch-faces/settings/set_time_face.h
new file mode 100644 (file)
index 0000000..f66dc9e
--- /dev/null
@@ -0,0 +1,60 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2022 Joey Castillo
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+#ifndef SET_TIME_FACE_H_
+#define SET_TIME_FACE_H_
+
+/*
+ * SET TIME face
+ *
+ * The default method for adjusting Sensor Watch time.
+ *
+ * The Time Set watch face allows you to set the time on Sensor Watch. Use
+ * the LIGHT button to advance through the field you are setting, and the
+ * ALARM button to change the value in that field. The fields are, in order:
+ * Hour, Minute, Second, Year, Month, Day and Time Zone.
+ *
+ * For features like World Clock and Sunrise/Sunset to work correctly, you
+ * must set the time to your local time, and the time zone to your local time
+ * zone. This allows Sensor Watch to correctly offset the time. This also
+ * means that when daylight savings time starts or ends, you must update
+ * both the time and the time zone on this screen.
+ */
+
+#include "movement.h"
+
+void set_time_face_setup(movement_settings_t *settings, uint8_t watch_face_index, void ** context_ptr);
+void set_time_face_activate(movement_settings_t *settings, void *context);
+bool set_time_face_loop(movement_event_t event, movement_settings_t *settings, void *context);
+void set_time_face_resign(movement_settings_t *settings, void *context);
+
+#define set_time_face ((const watch_face_t){ \
+    set_time_face_setup, \
+    set_time_face_activate, \
+    set_time_face_loop, \
+    set_time_face_resign, \
+    NULL, \
+})
+
+#endif // SET_TIME_FACE_H_