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