# 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 \
./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
+++ /dev/null
-#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
--- /dev/null
+/*
+ * 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++;
+ }
+}
--- /dev/null
+/*
+ * 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);
+++ /dev/null
-/*
- * 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++;
- }
-}
+++ /dev/null
-/*
- * 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_
+++ /dev/null
-/*
- * 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;
-}
+++ /dev/null
-/*
- * 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_
+++ /dev/null
-/*
- * 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);
-}
+++ /dev/null
-/*
- * 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_
--- /dev/null
+/*
+ * 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_
--- /dev/null
+/*
+ * 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.
--- /dev/null
+SRCS += \
+ ./watch-faces/clock/simple_clock_face.c \
+ ./watch-faces/settings/set_time_face.c \
--- /dev/null
+/*
+ * 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;
+}
--- /dev/null
+/*
+ * 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_
--- /dev/null
+/*
+ * 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);
+}
--- /dev/null
+/*
+ * 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_