first build of Second Movement with two watch faces

This commit is contained in:
joeycastillo
2024-09-18 18:22:33 -04:00
parent 24598ec280
commit dccad01e8f
11 changed files with 165 additions and 149 deletions

View File

@@ -1,733 +0,0 @@
/*
* MIT License
*
* Copyright (c) 2022 Joey Castillo
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
#define MOVEMENT_LONG_PRESS_TICKS 64
#include <stdio.h>
#include <string.h>
#include <limits.h>
#include <string.h>
#include <stdlib.h>
#include <stdio.h>
#include "watch.h"
#include "filesystem.h"
#include "movement.h"
#include "shell.h"
#ifndef MOVEMENT_FIRMWARE
#include "movement_config.h"
#elif MOVEMENT_FIRMWARE == MOVEMENT_FIRMWARE_STANDARD
#include "movement_config.h"
#elif MOVEMENT_FIRMWARE == MOVEMENT_FIRMWARE_BACKER
#include "alt_fw/backer.h"
#elif MOVEMENT_FIRMWARE == MOVEMENT_FIRMWARE_ALT_TIME
#include "alt_fw/alt_time.h"
#elif MOVEMENT_FIRMWARE == MOVEMENT_FIRMWARE_FOCUS
#include "alt_fw/focus.h"
#elif MOVEMENT_FIRMWARE == MOVEMENT_FIRMWARE_THE_BACKPACKER
#include "alt_fw/the_backpacker.h"
#elif MOVEMENT_FIRMWARE == MOVEMENT_FIRMWARE_THE_ATHLETE
#include "alt_fw/the_athlete.h"
#elif MOVEMENT_FIRMWARE == MOVEMENT_FIRMWARE_THE_STARGAZER
#include "alt_fw/the_stargazer.h"
#elif MOVEMENT_FIRMWARE == MOVEMENT_FIRMWARE_DEEP_SPACE_NOW
#include "alt_fw/deep_space_now.h"
#endif
#include "movement_custom_signal_tunes.h"
// Default to no secondary face behaviour.
#ifndef MOVEMENT_SECONDARY_FACE_INDEX
#define MOVEMENT_SECONDARY_FACE_INDEX 0
#endif
// Set default LED colors if not set
#ifndef MOVEMENT_DEFAULT_RED_COLOR
#define MOVEMENT_DEFAULT_RED_COLOR 0x0
#endif
#ifndef MOVEMENT_DEFAULT_GREEN_COLOR
#define MOVEMENT_DEFAULT_GREEN_COLOR 0xF
#endif
// Default to 12h mode
#ifndef MOVEMENT_DEFAULT_24H_MODE
#define MOVEMENT_DEFAULT_24H_MODE false
#endif
// Default to mode button sounding on press
#ifndef MOVEMENT_DEFAULT_BUTTON_SOUND
#define MOVEMENT_DEFAULT_BUTTON_SOUND true
#endif
// Default to switch back to main watch face after 60 seconds
#ifndef MOVEMENT_DEFAULT_TIMEOUT_INTERVAL
#define MOVEMENT_DEFAULT_TIMEOUT_INTERVAL 0
#endif
// Default to switch to low energy mode after 2 hours
#ifndef MOVEMENT_DEFAULT_LOW_ENERGY_INTERVAL
#define MOVEMENT_DEFAULT_LOW_ENERGY_INTERVAL 2
#endif
// Default to 1 second led duration
#ifndef MOVEMENT_DEFAULT_LED_DURATION
#define MOVEMENT_DEFAULT_LED_DURATION 1
#endif
#if __EMSCRIPTEN__
#include <emscripten.h>
#endif
movement_state_t movement_state;
void * watch_face_contexts[MOVEMENT_NUM_FACES];
watch_date_time scheduled_tasks[MOVEMENT_NUM_FACES];
const int32_t movement_le_inactivity_deadlines[8] = {INT_MAX, 600, 3600, 7200, 21600, 43200, 86400, 604800};
const int16_t movement_timeout_inactivity_deadlines[4] = {60, 120, 300, 1800};
movement_event_t event;
const int16_t movement_timezone_offsets[] = {
0, // 0 : 0:00:00 (UTC)
60, // 1 : 1:00:00 (Central European Time)
120, // 2 : 2:00:00 (South African Standard Time)
180, // 3 : 3:00:00 (Arabia Standard Time)
210, // 4 : 3:30:00 (Iran Standard Time)
240, // 5 : 4:00:00 (Georgia Standard Time)
270, // 6 : 4:30:00 (Afghanistan Time)
300, // 7 : 5:00:00 (Pakistan Standard Time)
330, // 8 : 5:30:00 (Indian Standard Time)
345, // 9 : 5:45:00 (Nepal Time)
360, // 10 : 6:00:00 (Kyrgyzstan time)
390, // 11 : 6:30:00 (Myanmar Time)
420, // 12 : 7:00:00 (Thailand Standard Time)
480, // 13 : 8:00:00 (China Standard Time, Australian Western Standard Time)
525, // 14 : 8:45:00 (Australian Central Western Standard Time)
540, // 15 : 9:00:00 (Japan Standard Time, Korea Standard Time)
570, // 16 : 9:30:00 (Australian Central Standard Time)
600, // 17 : 10:00:00 (Australian Eastern Standard Time)
630, // 18 : 10:30:00 (Lord Howe Standard Time)
660, // 19 : 11:00:00 (Solomon Islands Time)
720, // 20 : 12:00:00 (New Zealand Standard Time)
765, // 21 : 12:45:00 (Chatham Standard Time)
780, // 22 : 13:00:00 (Tonga Time)
825, // 23 : 13:45:00 (Chatham Daylight Time)
840, // 24 : 14:00:00 (Line Islands Time)
-720, // 25 : -12:00:00 (Baker Island Time)
-660, // 26 : -11:00:00 (Niue Time)
-600, // 27 : -10:00:00 (Hawaii-Aleutian Standard Time)
-570, // 28 : -9:30:00 (Marquesas Islands Time)
-540, // 29 : -9:00:00 (Alaska Standard Time)
-480, // 30 : -8:00:00 (Pacific Standard Time)
-420, // 31 : -7:00:00 (Mountain Standard Time)
-360, // 32 : -6:00:00 (Central Standard Time)
-300, // 33 : -5:00:00 (Eastern Standard Time)
-270, // 34 : -4:30:00 (Venezuelan Standard Time)
-240, // 35 : -4:00:00 (Atlantic Standard Time)
-210, // 36 : -3:30:00 (Newfoundland Standard Time)
-180, // 37 : -3:00:00 (Brasilia Time)
-150, // 38 : -2:30:00 (Newfoundland Daylight Time)
-120, // 39 : -2:00:00 (Fernando de Noronha Time)
-60, // 40 : -1:00:00 (Azores Standard Time)
};
const char movement_valid_position_0_chars[] = " AaBbCcDdEeFGgHhIiJKLMNnOoPQrSTtUuWXYZ-='+\\/0123456789";
const char movement_valid_position_1_chars[] = " ABCDEFHlJLNORTtUX-='01378";
void cb_mode_btn_interrupt(void);
void cb_light_btn_interrupt(void);
void cb_alarm_btn_interrupt(void);
void cb_alarm_btn_extwake(void);
void cb_alarm_fired(void);
void cb_fast_tick(void);
void cb_tick(void);
static inline void _movement_reset_inactivity_countdown(void) {
movement_state.le_mode_ticks = movement_le_inactivity_deadlines[movement_state.settings.bit.le_interval];
movement_state.timeout_ticks = movement_timeout_inactivity_deadlines[movement_state.settings.bit.to_interval];
}
static inline void _movement_enable_fast_tick_if_needed(void) {
if (!movement_state.fast_tick_enabled) {
movement_state.fast_ticks = 0;
watch_rtc_register_periodic_callback(cb_fast_tick, 128);
movement_state.fast_tick_enabled = true;
}
}
static inline void _movement_disable_fast_tick_if_possible(void) {
if ((movement_state.light_ticks == -1) &&
(movement_state.alarm_ticks == -1) &&
((movement_state.light_down_timestamp + movement_state.mode_down_timestamp + movement_state.alarm_down_timestamp) == 0)) {
movement_state.fast_tick_enabled = false;
watch_rtc_disable_periodic_callback(128);
}
}
static void _movement_handle_background_tasks(void) {
for(uint8_t i = 0; i < MOVEMENT_NUM_FACES; i++) {
// For each face, if the watch face wants a background task...
if (watch_faces[i].wants_background_task != NULL && watch_faces[i].wants_background_task(&movement_state.settings, watch_face_contexts[i])) {
// ...we give it one. pretty straightforward!
movement_event_t background_event = { EVENT_BACKGROUND_TASK, 0 };
watch_faces[i].loop(background_event, &movement_state.settings, watch_face_contexts[i]);
}
}
movement_state.needs_background_tasks_handled = false;
}
static void _movement_handle_scheduled_tasks(void) {
watch_date_time date_time = watch_rtc_get_date_time();
uint8_t num_active_tasks = 0;
for(uint8_t i = 0; i < MOVEMENT_NUM_FACES; i++) {
if (scheduled_tasks[i].reg) {
if (scheduled_tasks[i].reg <= date_time.reg) {
scheduled_tasks[i].reg = 0;
movement_event_t background_event = { EVENT_BACKGROUND_TASK, 0 };
watch_faces[i].loop(background_event, &movement_state.settings, watch_face_contexts[i]);
// check if loop scheduled a new task
if (scheduled_tasks[i].reg) {
num_active_tasks++;
}
} else {
num_active_tasks++;
}
}
}
if (num_active_tasks == 0) {
movement_state.has_scheduled_background_task = false;
} else {
_movement_reset_inactivity_countdown();
}
}
void movement_request_tick_frequency(uint8_t freq) {
// Movement uses the 128 Hz tick internally
if (freq == 128) return;
// Movement requires at least a 1 Hz tick.
// If we are asked for an invalid frequency, default back to 1 Hz.
if (freq == 0 || __builtin_popcount(freq) != 1) freq = 1;
// disable all callbacks except the 128 Hz one
watch_rtc_disable_matching_periodic_callbacks(0xFE);
movement_state.subsecond = 0;
movement_state.tick_frequency = freq;
watch_rtc_register_periodic_callback(cb_tick, freq);
}
void movement_illuminate_led(void) {
if (movement_state.settings.bit.led_duration != 0b111) {
watch_set_led_color(movement_state.settings.bit.led_red_color ? (0xF | movement_state.settings.bit.led_red_color << 4) : 0,
movement_state.settings.bit.led_green_color ? (0xF | movement_state.settings.bit.led_green_color << 4) : 0);
if (movement_state.settings.bit.led_duration == 0) {
movement_state.light_ticks = 1;
} else {
movement_state.light_ticks = (movement_state.settings.bit.led_duration * 2 - 1) * 128;
}
_movement_enable_fast_tick_if_needed();
}
}
static void _movement_led_off(void) {
watch_set_led_off();
movement_state.light_ticks = -1;
_movement_disable_fast_tick_if_possible();
}
bool movement_default_loop_handler(movement_event_t event, movement_settings_t *settings) {
(void)settings;
switch (event.event_type) {
case EVENT_MODE_BUTTON_UP:
movement_move_to_next_face();
break;
case EVENT_LIGHT_BUTTON_DOWN:
movement_illuminate_led();
break;
case EVENT_LIGHT_BUTTON_UP:
if (movement_state.settings.bit.led_duration == 0) {
_movement_led_off();
}
break;
case EVENT_MODE_LONG_PRESS:
if (MOVEMENT_SECONDARY_FACE_INDEX && movement_state.current_face_idx == 0) {
movement_move_to_face(MOVEMENT_SECONDARY_FACE_INDEX);
} else {
movement_move_to_face(0);
}
break;
default:
break;
}
return true;
}
void movement_move_to_face(uint8_t watch_face_index) {
movement_state.watch_face_changed = true;
movement_state.next_face_idx = watch_face_index;
}
void movement_move_to_next_face(void) {
uint16_t face_max;
if (MOVEMENT_SECONDARY_FACE_INDEX) {
face_max = (movement_state.current_face_idx < (int16_t)MOVEMENT_SECONDARY_FACE_INDEX) ? MOVEMENT_SECONDARY_FACE_INDEX : MOVEMENT_NUM_FACES;
} else {
face_max = MOVEMENT_NUM_FACES;
}
movement_move_to_face((movement_state.current_face_idx + 1) % face_max);
}
void movement_schedule_background_task(watch_date_time date_time) {
movement_schedule_background_task_for_face(movement_state.current_face_idx, date_time);
}
void movement_cancel_background_task(void) {
movement_cancel_background_task_for_face(movement_state.current_face_idx);
}
void movement_schedule_background_task_for_face(uint8_t watch_face_index, watch_date_time date_time) {
watch_date_time now = watch_rtc_get_date_time();
if (date_time.reg > now.reg) {
movement_state.has_scheduled_background_task = true;
scheduled_tasks[watch_face_index].reg = date_time.reg;
}
}
void movement_cancel_background_task_for_face(uint8_t watch_face_index) {
scheduled_tasks[watch_face_index].reg = 0;
bool other_tasks_scheduled = false;
for(uint8_t i = 0; i < MOVEMENT_NUM_FACES; i++) {
if (scheduled_tasks[i].reg != 0) {
other_tasks_scheduled = true;
break;
}
}
movement_state.has_scheduled_background_task = other_tasks_scheduled;
}
void movement_request_wake() {
movement_state.needs_wake = true;
_movement_reset_inactivity_countdown();
}
static void end_buzzing() {
movement_state.is_buzzing = false;
}
static void end_buzzing_and_disable_buzzer(void) {
end_buzzing();
watch_disable_buzzer();
}
static void set_initial_clock_mode(void) {
#ifdef CLOCK_FACE_24H_ONLY
movement_state.settings.bit.clock_mode_24h = true;
#else
movement_state.settings.bit.clock_mode_24h = MOVEMENT_DEFAULT_24H_MODE;
#endif
}
void movement_play_signal(void) {
void *maybe_disable_buzzer = end_buzzing_and_disable_buzzer;
if (watch_is_buzzer_or_led_enabled()) {
maybe_disable_buzzer = end_buzzing;
} else {
watch_enable_buzzer();
}
movement_state.is_buzzing = true;
watch_buzzer_play_sequence(signal_tune, maybe_disable_buzzer);
if (movement_state.le_mode_ticks == -1) {
// the watch is asleep. wake it up for "1" round through the main loop.
// the sleep_mode_app_loop will notice the is_buzzing and note that it
// only woke up to beep and then it will spinlock until the callback
// turns off the is_buzzing flag.
movement_state.needs_wake = true;
movement_state.le_mode_ticks = 1;
}
}
void movement_play_alarm(void) {
movement_play_alarm_beeps(5, BUZZER_NOTE_C8);
}
void movement_play_alarm_beeps(uint8_t rounds, BuzzerNote alarm_note) {
if (rounds == 0) rounds = 1;
if (rounds > 20) rounds = 20;
movement_request_wake();
movement_state.alarm_note = alarm_note;
// our tone is 0.375 seconds of beep and 0.625 of silence, repeated as given.
movement_state.alarm_ticks = 128 * rounds - 75;
_movement_enable_fast_tick_if_needed();
}
uint8_t movement_claim_backup_register(void) {
if (movement_state.next_available_backup_register >= 8) return 0;
return movement_state.next_available_backup_register++;
}
void app_init(void) {
#if defined(NO_FREQCORR)
watch_rtc_freqcorr_write(0, 0);
#elif defined(WATCH_IS_BLUE_BOARD)
watch_rtc_freqcorr_write(11, 0);
#else
watch_rtc_freqcorr_write(22, 0);
#endif
memset(&movement_state, 0, sizeof(movement_state));
set_initial_clock_mode();
movement_state.settings.bit.led_red_color = MOVEMENT_DEFAULT_RED_COLOR;
movement_state.settings.bit.led_green_color = MOVEMENT_DEFAULT_GREEN_COLOR;
movement_state.settings.bit.button_should_sound = MOVEMENT_DEFAULT_BUTTON_SOUND;
movement_state.settings.bit.to_interval = MOVEMENT_DEFAULT_TIMEOUT_INTERVAL;
movement_state.settings.bit.le_interval = MOVEMENT_DEFAULT_LOW_ENERGY_INTERVAL;
movement_state.settings.bit.led_duration = MOVEMENT_DEFAULT_LED_DURATION;
movement_state.light_ticks = -1;
movement_state.alarm_ticks = -1;
movement_state.next_available_backup_register = 4;
_movement_reset_inactivity_countdown();
filesystem_init();
#if __EMSCRIPTEN__
int32_t time_zone_offset = EM_ASM_INT({
return -new Date().getTimezoneOffset();
});
for (int i = 0, count = sizeof(movement_timezone_offsets) / sizeof(movement_timezone_offsets[0]); i < count; i++) {
if (movement_timezone_offsets[i] == time_zone_offset) {
movement_state.settings.bit.time_zone = i;
break;
}
}
#endif
}
void app_wake_from_backup(void) {
movement_state.settings.reg = watch_get_backup_data(0);
}
void app_setup(void) {
watch_store_backup_data(movement_state.settings.reg, 0);
static bool is_first_launch = true;
if (is_first_launch) {
#ifdef MOVEMENT_CUSTOM_BOOT_COMMANDS
MOVEMENT_CUSTOM_BOOT_COMMANDS()
#endif
for(uint8_t i = 0; i < MOVEMENT_NUM_FACES; i++) {
watch_face_contexts[i] = NULL;
scheduled_tasks[i].reg = 0;
is_first_launch = false;
}
// set up the 1 minute alarm (for background tasks and low power updates)
watch_date_time alarm_time;
alarm_time.reg = 0;
alarm_time.unit.second = 59; // after a match, the alarm fires at the next rising edge of CLK_RTC_CNT, so 59 seconds lets us update at :00
watch_rtc_register_alarm_callback(cb_alarm_fired, alarm_time, ALARM_MATCH_SS);
}
if (movement_state.le_mode_ticks != -1) {
watch_disable_extwake_interrupt(BTN_ALARM);
watch_enable_external_interrupts();
watch_register_interrupt_callback(BTN_MODE, cb_mode_btn_interrupt, INTERRUPT_TRIGGER_BOTH);
watch_register_interrupt_callback(BTN_LIGHT, cb_light_btn_interrupt, INTERRUPT_TRIGGER_BOTH);
watch_register_interrupt_callback(BTN_ALARM, cb_alarm_btn_interrupt, INTERRUPT_TRIGGER_BOTH);
watch_enable_buzzer();
watch_enable_leds();
watch_enable_display();
movement_request_tick_frequency(1);
for(uint8_t i = 0; i < MOVEMENT_NUM_FACES; i++) {
watch_faces[i].setup(&movement_state.settings, i, &watch_face_contexts[i]);
}
watch_faces[movement_state.current_face_idx].activate(&movement_state.settings, watch_face_contexts[movement_state.current_face_idx]);
event.subsecond = 0;
event.event_type = EVENT_ACTIVATE;
}
}
void app_prepare_for_standby(void) {
}
void app_wake_from_standby(void) {
}
static void _sleep_mode_app_loop(void) {
movement_state.needs_wake = false;
// as long as le_mode_ticks is -1 (i.e. we are in low energy mode), we wake up here, update the screen, and go right back to sleep.
while (movement_state.le_mode_ticks == -1) {
// we also have to handle background tasks here in the mini-runloop
if (movement_state.needs_background_tasks_handled) _movement_handle_background_tasks();
event.event_type = EVENT_LOW_ENERGY_UPDATE;
watch_faces[movement_state.current_face_idx].loop(event, &movement_state.settings, watch_face_contexts[movement_state.current_face_idx]);
// if we need to wake immediately, do it!
if (movement_state.needs_wake) return;
// otherwise enter sleep mode, and when the extwake handler is called, it will reset le_mode_ticks and force us out at the next loop.
else watch_enter_sleep_mode();
}
}
bool app_loop(void) {
const watch_face_t *wf = &watch_faces[movement_state.current_face_idx];
bool woke_up_for_buzzer = false;
if (movement_state.watch_face_changed) {
if (movement_state.settings.bit.button_should_sound) {
// low note for nonzero case, high note for return to watch_face 0
watch_buzzer_play_note(movement_state.next_face_idx ? BUZZER_NOTE_C7 : BUZZER_NOTE_C8, 50);
}
wf->resign(&movement_state.settings, watch_face_contexts[movement_state.current_face_idx]);
movement_state.current_face_idx = movement_state.next_face_idx;
// we have just updated the face idx, so we must recache the watch face pointer.
wf = &watch_faces[movement_state.current_face_idx];
watch_clear_display();
movement_request_tick_frequency(1);
wf->activate(&movement_state.settings, watch_face_contexts[movement_state.current_face_idx]);
event.subsecond = 0;
event.event_type = EVENT_ACTIVATE;
movement_state.watch_face_changed = false;
}
// if the LED should be off, turn it off
if (movement_state.light_ticks == 0) {
// unless the user is holding down the LIGHT button, in which case, give them more time.
if (watch_get_pin_level(BTN_LIGHT)) {
movement_state.light_ticks = 1;
} else {
_movement_led_off();
}
}
// handle background tasks, if the alarm handler told us we need to
if (movement_state.needs_background_tasks_handled) _movement_handle_background_tasks();
// if we have a scheduled background task, handle that here:
if (event.event_type == EVENT_TICK && movement_state.has_scheduled_background_task) _movement_handle_scheduled_tasks();
// if we have timed out of our low energy mode countdown, enter low energy mode.
if (movement_state.le_mode_ticks == 0) {
movement_state.le_mode_ticks = -1;
watch_register_extwake_callback(BTN_ALARM, cb_alarm_btn_extwake, true);
event.event_type = EVENT_NONE;
event.subsecond = 0;
// _sleep_mode_app_loop takes over at this point and loops until le_mode_ticks is reset by the extwake handler,
// or wake is requested using the movement_request_wake function.
_sleep_mode_app_loop();
// as soon as _sleep_mode_app_loop returns, we prepare to reactivate
// ourselves, but first, we check to see if we woke up for the buzzer:
if (movement_state.is_buzzing) {
woke_up_for_buzzer = true;
}
event.event_type = EVENT_ACTIVATE;
// this is a hack tho: waking from sleep mode, app_setup does get called, but it happens before we have reset our ticks.
// need to figure out if there's a better heuristic for determining how we woke up.
app_setup();
}
// default to being allowed to sleep by the face.
bool can_sleep = true;
if (event.event_type) {
event.subsecond = movement_state.subsecond;
// the first trip through the loop overrides the can_sleep state
can_sleep = wf->loop(event, &movement_state.settings, watch_face_contexts[movement_state.current_face_idx]);
// Keep light on if user is still interacting with the watch.
if (movement_state.light_ticks > 0) {
switch (event.event_type) {
case EVENT_LIGHT_BUTTON_DOWN:
case EVENT_MODE_BUTTON_DOWN:
case EVENT_ALARM_BUTTON_DOWN:
movement_illuminate_led();
}
}
event.event_type = EVENT_NONE;
}
// if we have timed out of our timeout countdown, give the app a hint that they can resign.
if (movement_state.timeout_ticks == 0) {
movement_state.timeout_ticks = -1;
if (movement_state.settings.bit.to_always == false) {
// if "timeout always" is false, give the current watch face a chance to exit gracefully...
event.event_type = EVENT_TIMEOUT;
}
event.subsecond = movement_state.subsecond;
// if we run through the loop again to time out, we need to reconsider whether or not we can sleep.
// if the first trip said true, but this trip said false, we need the false to override, thus
// we will be using boolean AND:
//
// first trip | can sleep | cannot sleep | can sleep | cannot sleep
// second trip | can sleep | cannot sleep | cannot sleep | can sleep
// && | can sleep | cannot sleep | cannot sleep | cannot sleep
bool can_sleep2 = wf->loop(event, &movement_state.settings, watch_face_contexts[movement_state.current_face_idx]);
can_sleep = can_sleep && can_sleep2;
event.event_type = EVENT_NONE;
if (movement_state.settings.bit.to_always && movement_state.current_face_idx != 0) {
// ...but if the user has "timeout always" set, give it the boot.
movement_move_to_face(0);
}
}
// Now that we've handled all display update tasks, handle the alarm.
if (movement_state.alarm_ticks >= 0) {
uint8_t buzzer_phase = (movement_state.alarm_ticks + 80) % 128;
if(buzzer_phase == 127) {
// failsafe: buzzer could have been disabled in the meantime
if (!watch_is_buzzer_or_led_enabled()) watch_enable_buzzer();
// play 4 beeps plus pause
for(uint8_t i = 0; i < 4; i++) {
// TODO: This method of playing the buzzer blocks the UI while it's beeping.
// It might be better to time it with the fast tick.
watch_buzzer_play_note(movement_state.alarm_note, (i != 3) ? 50 : 75);
if (i != 3) watch_buzzer_play_note(BUZZER_NOTE_REST, 50);
}
}
if (movement_state.alarm_ticks == 0) {
movement_state.alarm_ticks = -1;
_movement_disable_fast_tick_if_possible();
}
}
// if we are plugged into USB, handle the serial shell
if (watch_is_usb_enabled()) {
shell_task();
}
event.subsecond = 0;
// if the watch face changed, we can't sleep because we need to update the display.
if (movement_state.watch_face_changed) can_sleep = false;
// if we woke up for the buzzer, stay awake until it's finished.
if (woke_up_for_buzzer) {
while(watch_is_buzzer_or_led_enabled());
}
// if the LED is on, we need to stay awake to keep the TCC running.
if (movement_state.light_ticks != -1) can_sleep = false;
return can_sleep;
}
static movement_event_type_t _figure_out_button_event(bool pin_level, movement_event_type_t button_down_event_type, uint16_t *down_timestamp) {
// force alarm off if the user pressed a button.
if (movement_state.alarm_ticks) movement_state.alarm_ticks = 0;
if (pin_level) {
// handle rising edge
_movement_enable_fast_tick_if_needed();
*down_timestamp = movement_state.fast_ticks + 1;
return button_down_event_type;
} else {
// this line is hack but it handles the situation where the light button was held for more than 20 seconds.
// fast tick is disabled by then, and the LED would get stuck on since there's no one left decrementing light_ticks.
if (movement_state.light_ticks == 1) movement_state.light_ticks = 0;
// now that that's out of the way, handle falling edge
uint16_t diff = movement_state.fast_ticks - *down_timestamp;
*down_timestamp = 0;
_movement_disable_fast_tick_if_possible();
// any press over a half second is considered a long press. Fire the long-up event
if (diff > MOVEMENT_LONG_PRESS_TICKS) return button_down_event_type + 3;
else return button_down_event_type + 1;
}
}
void cb_light_btn_interrupt(void) {
bool pin_level = watch_get_pin_level(BTN_LIGHT);
_movement_reset_inactivity_countdown();
event.event_type = _figure_out_button_event(pin_level, EVENT_LIGHT_BUTTON_DOWN, &movement_state.light_down_timestamp);
}
void cb_mode_btn_interrupt(void) {
bool pin_level = watch_get_pin_level(BTN_MODE);
_movement_reset_inactivity_countdown();
event.event_type = _figure_out_button_event(pin_level, EVENT_MODE_BUTTON_DOWN, &movement_state.mode_down_timestamp);
}
void cb_alarm_btn_interrupt(void) {
bool pin_level = watch_get_pin_level(BTN_ALARM);
_movement_reset_inactivity_countdown();
event.event_type = _figure_out_button_event(pin_level, EVENT_ALARM_BUTTON_DOWN, &movement_state.alarm_down_timestamp);
}
void cb_alarm_btn_extwake(void) {
// wake up!
_movement_reset_inactivity_countdown();
}
void cb_alarm_fired(void) {
movement_state.needs_background_tasks_handled = true;
}
void cb_fast_tick(void) {
movement_state.fast_ticks++;
if (movement_state.light_ticks > 0) movement_state.light_ticks--;
if (movement_state.alarm_ticks > 0) movement_state.alarm_ticks--;
// check timestamps and auto-fire the long-press events
// Notice: is it possible that two or more buttons have an identical timestamp? In this case
// only one of these buttons would receive the long press event. Don't bother for now...
if (movement_state.light_down_timestamp > 0)
if (movement_state.fast_ticks - movement_state.light_down_timestamp == MOVEMENT_LONG_PRESS_TICKS + 1)
event.event_type = EVENT_LIGHT_LONG_PRESS;
if (movement_state.mode_down_timestamp > 0)
if (movement_state.fast_ticks - movement_state.mode_down_timestamp == MOVEMENT_LONG_PRESS_TICKS + 1)
event.event_type = EVENT_MODE_LONG_PRESS;
if (movement_state.alarm_down_timestamp > 0)
if (movement_state.fast_ticks - movement_state.alarm_down_timestamp == MOVEMENT_LONG_PRESS_TICKS + 1)
event.event_type = EVENT_ALARM_LONG_PRESS;
// this is just a fail-safe; fast tick should be disabled as soon as the button is up, the LED times out, and/or the alarm finishes.
// but if for whatever reason it isn't, this forces the fast tick off after 20 seconds.
if (movement_state.fast_ticks >= 128 * 20) {
watch_rtc_disable_periodic_callback(128);
movement_state.fast_tick_enabled = false;
}
}
void cb_tick(void) {
event.event_type = EVENT_TICK;
watch_date_time date_time = watch_rtc_get_date_time();
if (date_time.unit.second != movement_state.last_second) {
// TODO: can we consolidate these two ticks?
if (movement_state.settings.bit.le_interval && movement_state.le_mode_ticks > 0) movement_state.le_mode_ticks--;
if (movement_state.timeout_ticks > 0) movement_state.timeout_ticks--;
movement_state.last_second = date_time.unit.second;
movement_state.subsecond = 0;
} else {
movement_state.subsecond++;
}
}

View File

@@ -1,316 +0,0 @@
/*
* MIT License
*
* Copyright (c) 2022 Joey Castillo
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
#ifndef MOVEMENT_H_
#define MOVEMENT_H_
#include <stdio.h>
#include <stdbool.h>
#include "watch.h"
// Movement Preferences
// These four 32-bit structs store information about the wearer and their preferences. Tentatively, the plan is
// for Movement to use four 32-bit registers for these preferences and to store them in the RTC's backup registers
// 0-3, leaving registers 4-7 available for third party watch faces to use as they see fit.
// * The movement_settings_t struct is provided to all watch faces in the callback functions, and is stored in the
// RTC's first backup register (BKUP[0]).
// * The movement_location_t and movement_birthdate_t types are defined here, and are tentatively meant to be
// stored in BKUP[1] and BKUP[2], respectively.
// * The movement_reserved_t type is here as a placeholder, because I sense there's some other generally useful
// stuff we'll want to make available to all watch faces and stash in the BKUP[3] register.
// This allows these preferences to be stored before entering BACKUP mode and and restored after waking from reset.
// movement_settings_t contains global settings that cover watch behavior, including preferences around clock and unit
// display, time zones, buzzer behavior, LED color and low energy mode timeouts. These settings are stored in BKUP[0].
// If your watch face changes one of these settings, you should store your changes in either your loop or resign
// function by calling `watch_store_backup_data(settings->reg, 0)`. Otherwise it may not persist after a reset event.
typedef union {
struct {
bool button_should_sound : 1; // if true, pressing a button emits a sound.
uint8_t to_interval : 2; // an inactivity interval for asking the active face to resign.
bool to_always : 1; // if true, always time out from the active face to face 0. otherwise only faces that time out will resign (the default).
uint8_t le_interval : 3; // 0 to disable low energy mode, or an inactivity interval for going into low energy mode.
uint8_t led_duration : 3; // how many seconds to shine the LED for (x2), 0 to shine only while the button is depressed, or all bits set to disable the LED altogether.
uint8_t led_red_color : 4; // for general purpose illumination, the red LED value (0-15)
uint8_t led_green_color : 4; // for general purpose illumination, the green LED value (0-15)
uint8_t time_zone : 6; // an integer representing an index in the time zone table.
// while Movement itself doesn't implement a clock or display units, it may make sense to include some
// global settings for watch faces to check. The 12/24 hour preference could inform a clock or a
// time-oriented complication like a sunrise/sunset timer, and a simple locale preference could tell an
// altimeter to display feet or meters as easily as it tells a thermometer to display degrees in F or C.
bool clock_mode_24h : 1; // indicates whether clock should use 12 or 24 hour mode.
bool clock_24h_leading_zero : 1; // indicates whether clock should leading zero to indicate 24 hour mode.
bool use_imperial_units : 1; // indicates whether to use metric units (the default) or imperial.
bool alarm_enabled : 1; // indicates whether there is at least one alarm enabled.
uint8_t reserved : 5; // room for more preferences if needed.
} bit;
uint32_t reg;
} movement_settings_t;
// movement_location_t is for storing the wearer's location. This will be useful for astronomical calculations such as
// sunrise and sunset, or predictions of visible satellite passes.
// If you create a UI for this register or need to access it, look for it in the RTC's BKUP[1] register.
typedef union {
struct {
int16_t latitude : 16; // signed latutide in hundredths of a degree
int16_t longitude : 16; // signed longitude in hundredths of a degree
} bit;
uint32_t reg;
} movement_location_t;
// movement_birthdate_t is for storing the user's birth date. This will be useful for calculating the user's age — or
// hey, playing happy birthday at midnight? Fields for birth time (with hour and minute resolution) are also available,
// partly because they fit so nicely, but also because they can be useful for certain astrological calculations.
// If you create a UI for birth date or need to access it, look for it in the RTC's BKUP[2] register.
typedef union {
struct {
uint16_t year : 12; // good through the year 4095
uint8_t month : 4;
uint8_t day : 5;
uint8_t hour : 5;
uint8_t minute : 6;
} bit;
uint32_t reg;
} movement_birthdate_t;
// movement_reserved_t is a placeholder for future use of the BKUP[3] register.
typedef union {
struct {
uint32_t reserved : 32;
} bit;
uint32_t reg;
} movement_reserved_t;
typedef enum {
EVENT_NONE = 0, // There is no event to report.
EVENT_ACTIVATE, // Your watch face is entering the foreground.
EVENT_TICK, // Most common event type. Your watch face is being called from the tick callback.
EVENT_LOW_ENERGY_UPDATE, // If the watch is in low energy mode and you are in the foreground, you will get a chance to update the display once per minute.
EVENT_BACKGROUND_TASK, // Your watch face is being invoked to perform a background task. Don't update the display here; you may not be in the foreground.
EVENT_TIMEOUT, // Your watch face has been inactive for a while. You may want to resign, depending on your watch face's intended use case.
EVENT_LIGHT_BUTTON_DOWN, // The light button has been pressed, but not yet released.
EVENT_LIGHT_BUTTON_UP, // The light button was pressed for less than half a second, and released.
EVENT_LIGHT_LONG_PRESS, // The light button was held for over half a second, but not yet released.
EVENT_LIGHT_LONG_UP, // The light button was held for over half a second, and released.
EVENT_MODE_BUTTON_DOWN, // The mode button has been pressed, but not yet released.
EVENT_MODE_BUTTON_UP, // The mode button was pressed for less than half a second, and released.
EVENT_MODE_LONG_PRESS, // The mode button was held for over half a second, but not yet released.
EVENT_MODE_LONG_UP, // The mode button was held for over half a second, and released. NOTE: your watch face will resign immediately after receiving this event.
EVENT_ALARM_BUTTON_DOWN, // The alarm button has been pressed, but not yet released.
EVENT_ALARM_BUTTON_UP, // The alarm button was pressed for less than half a second, and released.
EVENT_ALARM_LONG_PRESS, // The alarm button was held for over half a second, but not yet released.
EVENT_ALARM_LONG_UP, // The alarm button was held for over half a second, and released.
} movement_event_type_t;
typedef struct {
uint8_t event_type;
uint8_t subsecond;
} movement_event_t;
extern const int16_t movement_timezone_offsets[];
extern const char movement_valid_position_0_chars[];
extern const char movement_valid_position_1_chars[];
/** @brief Perform setup for your watch face.
* @details It's tempting to say this is 'one-time' setup, but technically this function is called more than
* once. When the watch first boots, this function is called with a NULL context_ptr, indicating
* that it is the first run. At this time you should set context_ptr to something non-NULL if you
* need to keep track of any state in your watch face. If your watch face requires any other setup,
* like configuring a pin mode or a peripheral, you may want to do that here too.
* This function will be called again after waking from sleep mode, since sleep mode disables all
* of the device's pins and peripherals.
* @param settings A pointer to the global Movement settings. You can use this to inform how you present your
* display to the user (i.e. taking into account whether they have silenced the buttons, or if
* they prefer 12 or 24-hour mode). You can also change these settings if you like.
* @param watch_face_index The index of this watch face in the global array of watch faces; 0 is the first face,
* 1 is the second, etc. You may stash this value in your context if you wish to reference
* it later; your watch face's index is set at launch and will not change.
* @param context_ptr A pointer to a pointer; at first invocation, this value will be NULL, and you can set it
* to any value you like. Subsequent invocations will pass in whatever value you previously
* set. You may want to check if this is NULL and if so, allocate some space to store any
* data required for your watch face.
*
*/
typedef void (*watch_face_setup)(movement_settings_t *settings, uint8_t watch_face_index, void ** context_ptr);
/** @brief Prepare to go on-screen.
* @details This function is called just before your watch enters the foreground. If your watch face has any
* segments or text that is always displayed, you may want to set that here. In addition, if your
* watch face depends on data from a peripheral (like an I2C sensor), you will likely want to enable
* that peripheral here. In addition, if your watch face requires an update frequncy other than 1 Hz,
* you may want to request that here using the movement_request_tick_frequency function.
* @param settings A pointer to the global Movement settings. @see watch_face_setup.
* @param context A pointer to your watch face's context. @see watch_face_setup.
*
*/
typedef void (*watch_face_activate)(movement_settings_t *settings, void *context);
/** @brief Handle events and update the display.
* @details This function is called in response to an event. You should set up a switch statement that handles,
* at the very least, the EVENT_TICK and EVENT_MODE_BUTTON_UP event types. The tick event happens once
* per second (or more frequently if you asked for a faster tick with movement_request_tick_frequency).
* The mode button up event occurs when the user presses the MODE button. **Your loop function SHOULD
* call the movement_move_to_next_face function in response to this event.** If you have a good reason
* to override this behavior (e.g. your user interface requires all three buttons), your watch face MUST
* call the movement_move_to_next_face function in response to the EVENT_MODE_LONG_PRESS event. If you
* fail to do this, the user will become stuck on your watch face.
* @param event A struct containing information about the event, including its type. @see movement_event_type_t
* for a list of all possible event types.
* @param settings A pointer to the global Movement settings. @see watch_face_setup.
* @param context A pointer to your application's context. @see watch_face_setup.
* @return true if your watch face is prepared for the system to enter STANDBY mode; false to keep the system awake.
* You should almost always return true.
* Note that this return value has no effect if your loop function has called movement_move_to_next_face
* or movement_move_to_face; in that case, your watch face will resign immediately, and the next watch
* face will make the decision on entering standby mode.
* @note There are two event types that require some extra thought:
The EVENT_LOW_ENERGY_UPDATE event type is a special case. If you are in the foreground when the watch
goes into low energy mode, you will receive this tick once a minute (at the top of the minute) so that
you can update the screen. Great! But! When you receive this event, all pins and peripherals other than
the RTC will have been disabled to save energy. If your display is clock or calendar oriented, this is
fine. But if your display requires polling an I2C sensor or reading a value with the ADC, you won't be
able to do this. You should either display the name of the watch face in response to the low power tick,
or ensure that you resign before low power mode triggers, (e.g. by calling movement_move_to_face(0)).
**Your watch face MUST NOT wake up peripherals in response to a low power tick.** The purpose of this
mode is to consume as little energy as possible during the (potentially long) intervals when it's
unlikely the user is wearing or looking at the watch.
EVENT_BACKGROUND_TASK is also a special case. @see watch_face_wants_background_task for details.
*/
typedef bool (*watch_face_loop)(movement_event_t event, movement_settings_t *settings, void *context);
/** @brief Prepare to go off-screen.
* @details This function is called before your watch face enters the background. If you requested a tick
* frequency other than the standard 1 Hz, **you must call movement_request_tick_frequency(1) here**
* to reset to 1 Hz. You should also disable any peripherals you enabled when you entered the foreground.
* @param settings A pointer to the global Movement settings. @see watch_face_setup.
* @param context A pointer to your application's context. @see watch_face_setup.
*/
typedef void (*watch_face_resign)(movement_settings_t *settings, void *context);
/** @brief OPTIONAL. Request an opportunity to run a background task.
* @details Most apps will not need this function, but if you provide it, Movement will call it once per minute in
* both active and low power modes, regardless of whether your app is in the foreground. You can check the
* current time to determine whether you require a background task. If you return true here, Movement will
* immediately call your loop function with an EVENT_BACKGROUND_TASK event. Note that it will not call your
* activate or deactivate functions, since you are not going on screen.
*
* Examples of background tasks:
* - Wake and play a sound when an alarm or timer has been triggered.
* - Check the state of an RTC interrupt pin or the timestamp of an RTC interrupt event.
* - Log a data point from a sensor, and then return to sleep mode.
*
* Guidelines for background tasks:
* - Assume all peripherals and pins other than the RTC will be disabled when you get an EVENT_BACKGROUND_TASK.
* - Even if your background task involves only the RTC peripheral, try to request background tasks sparingly.
* - If your background task involves an external pin or peripheral, request background tasks no more than once per hour.
* - If you need to enable a pin or a peripheral to perform your task, return it to its original state afterwards.
*
* @param settings A pointer to the global Movement settings. @see watch_face_setup.
* @param context A pointer to your application's context. @see watch_face_setup.
* @return true to request a background task; false otherwise.
*/
typedef bool (*watch_face_wants_background_task)(movement_settings_t *settings, void *context);
typedef struct {
watch_face_setup setup;
watch_face_activate activate;
watch_face_loop loop;
watch_face_resign resign;
watch_face_wants_background_task wants_background_task;
} watch_face_t;
typedef struct {
// properties stored in BACKUP register
movement_settings_t settings;
// transient properties
int16_t current_face_idx;
int16_t next_face_idx;
bool watch_face_changed;
bool fast_tick_enabled;
int16_t fast_ticks;
// LED stuff
int16_t light_ticks;
// alarm stuff
int16_t alarm_ticks;
bool is_buzzing;
BuzzerNote alarm_note;
// button tracking for long press
uint16_t light_down_timestamp;
uint16_t mode_down_timestamp;
uint16_t alarm_down_timestamp;
// background task handling
bool needs_background_tasks_handled;
bool has_scheduled_background_task;
bool needs_wake;
// low energy mode countdown
int32_t le_mode_ticks;
// app resignation countdown (TODO: consolidate with LE countdown?)
int16_t timeout_ticks;
// stuff for subsecond tracking
uint8_t tick_frequency;
uint8_t last_second;
uint8_t subsecond;
// backup register stuff
uint8_t next_available_backup_register;
} movement_state_t;
void movement_move_to_face(uint8_t watch_face_index);
void movement_move_to_next_face(void);
bool movement_default_loop_handler(movement_event_t event, movement_settings_t *settings);
void movement_illuminate_led(void);
void movement_request_tick_frequency(uint8_t freq);
// note: watch faces can only schedule a background task when in the foreground, since
// movement will associate the scheduled task with the currently active face.
void movement_schedule_background_task(watch_date_time date_time);
// note: watch faces can only cancel their background task when in the foreground, since
// movement will associate the scheduled task with the currently active face.
void movement_cancel_background_task(void);
// these functions should work around the limitation of the above functions, which will be deprecated.
void movement_schedule_background_task_for_face(uint8_t watch_face_index, watch_date_time date_time);
void movement_cancel_background_task_for_face(uint8_t watch_face_index);
void movement_request_wake(void);
void movement_play_signal(void);
void movement_play_alarm(void);
void movement_play_alarm_beeps(uint8_t rounds, BuzzerNote alarm_note);
uint8_t movement_claim_backup_register(void);
#endif // MOVEMENT_H_

View File

@@ -1,177 +0,0 @@
/*
* MIT License
*
* Copyright (c) 2022 Joey Castillo
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
#include <stdlib.h>
#include "simple_clock_face.h"
#include "watch.h"
#include "watch_utility.h"
#include "watch_private_display.h"
static void _update_alarm_indicator(bool settings_alarm_enabled, simple_clock_state_t *state) {
state->alarm_enabled = settings_alarm_enabled;
if (state->alarm_enabled) watch_set_indicator(WATCH_INDICATOR_SIGNAL);
else watch_clear_indicator(WATCH_INDICATOR_SIGNAL);
}
void simple_clock_face_setup(movement_settings_t *settings, uint8_t watch_face_index, void ** context_ptr) {
(void) settings;
(void) watch_face_index;
if (*context_ptr == NULL) {
*context_ptr = malloc(sizeof(simple_clock_state_t));
simple_clock_state_t *state = (simple_clock_state_t *)*context_ptr;
state->signal_enabled = false;
state->watch_face_index = watch_face_index;
}
}
void simple_clock_face_activate(movement_settings_t *settings, void *context) {
simple_clock_state_t *state = (simple_clock_state_t *)context;
if (watch_tick_animation_is_running()) watch_stop_tick_animation();
#ifdef CLOCK_FACE_24H_ONLY
watch_set_indicator(WATCH_INDICATOR_24H);
#else
if (settings->bit.clock_mode_24h) watch_set_indicator(WATCH_INDICATOR_24H);
#endif
// handle chime indicator
if (state->signal_enabled) watch_set_indicator(WATCH_INDICATOR_BELL);
else watch_clear_indicator(WATCH_INDICATOR_BELL);
// show alarm indicator if there is an active alarm
_update_alarm_indicator(settings->bit.alarm_enabled, state);
watch_set_colon();
// this ensures that none of the timestamp fields will match, so we can re-render them all.
state->previous_date_time = 0xFFFFFFFF;
}
bool simple_clock_face_loop(movement_event_t event, movement_settings_t *settings, void *context) {
simple_clock_state_t *state = (simple_clock_state_t *)context;
char buf[11];
uint8_t pos;
watch_date_time date_time;
uint32_t previous_date_time;
switch (event.event_type) {
case EVENT_ACTIVATE:
case EVENT_TICK:
case EVENT_LOW_ENERGY_UPDATE:
date_time = watch_rtc_get_date_time();
previous_date_time = state->previous_date_time;
state->previous_date_time = date_time.reg;
// check the battery voltage once a day...
if (date_time.unit.day != state->last_battery_check) {
state->last_battery_check = date_time.unit.day;
watch_enable_adc();
uint16_t voltage = watch_get_vcc_voltage();
watch_disable_adc();
// 2.2 volts will happen when the battery has maybe 5-10% remaining?
// we can refine this later.
state->battery_low = (voltage < 2200);
}
// ...and set the LAP indicator if low.
if (state->battery_low) watch_set_indicator(WATCH_INDICATOR_LAP);
bool set_leading_zero = false;
if ((date_time.reg >> 6) == (previous_date_time >> 6) && event.event_type != EVENT_LOW_ENERGY_UPDATE) {
// everything before seconds is the same, don't waste cycles setting those segments.
watch_display_character_lp_seconds('0' + date_time.unit.second / 10, 8);
watch_display_character_lp_seconds('0' + date_time.unit.second % 10, 9);
break;
} else if ((date_time.reg >> 12) == (previous_date_time >> 12) && event.event_type != EVENT_LOW_ENERGY_UPDATE) {
// everything before minutes is the same.
pos = 6;
sprintf(buf, "%02d%02d", date_time.unit.minute, date_time.unit.second);
} else {
// other stuff changed; let's do it all.
#ifndef CLOCK_FACE_24H_ONLY
if (!settings->bit.clock_mode_24h) {
// if we are in 12 hour mode, do some cleanup.
if (date_time.unit.hour < 12) {
watch_clear_indicator(WATCH_INDICATOR_PM);
} else {
watch_set_indicator(WATCH_INDICATOR_PM);
}
date_time.unit.hour %= 12;
if (date_time.unit.hour == 0) date_time.unit.hour = 12;
}
#endif
if (settings->bit.clock_mode_24h && settings->bit.clock_24h_leading_zero && date_time.unit.hour < 10) {
set_leading_zero = true;
}
pos = 0;
if (event.event_type == EVENT_LOW_ENERGY_UPDATE) {
if (!watch_tick_animation_is_running()) watch_start_tick_animation(500);
sprintf(buf, "%s%2d%2d%02d ", watch_utility_get_weekday(date_time), date_time.unit.day, date_time.unit.hour, date_time.unit.minute);
} else {
sprintf(buf, "%s%2d%2d%02d%02d", watch_utility_get_weekday(date_time), date_time.unit.day, date_time.unit.hour, date_time.unit.minute, date_time.unit.second);
}
}
watch_display_string(buf, pos);
if (set_leading_zero)
watch_display_string("0", 4);
// handle alarm indicator
if (state->alarm_enabled != settings->bit.alarm_enabled) _update_alarm_indicator(settings->bit.alarm_enabled, state);
break;
case EVENT_ALARM_LONG_PRESS:
state->signal_enabled = !state->signal_enabled;
if (state->signal_enabled) watch_set_indicator(WATCH_INDICATOR_BELL);
else watch_clear_indicator(WATCH_INDICATOR_BELL);
break;
case EVENT_BACKGROUND_TASK:
// uncomment this line to snap back to the clock face when the hour signal sounds:
// movement_move_to_face(state->watch_face_index);
movement_play_signal();
break;
default:
return movement_default_loop_handler(event, settings);
}
return true;
}
void simple_clock_face_resign(movement_settings_t *settings, void *context) {
(void) settings;
(void) context;
}
bool simple_clock_face_wants_background_task(movement_settings_t *settings, void *context) {
(void) settings;
simple_clock_state_t *state = (simple_clock_state_t *)context;
if (!state->signal_enabled) return false;
watch_date_time date_time = watch_rtc_get_date_time();
return date_time.unit.minute == 0;
}

View File

@@ -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_

View File

@@ -1,188 +0,0 @@
/*
* MIT License
*
* Copyright (c) 2022 Joey Castillo
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
#include <stdlib.h>
#include "set_time_face.h"
#include "watch.h"
#include "watch_utility.h"
#define SET_TIME_FACE_NUM_SETTINGS (7)
const char set_time_face_titles[SET_TIME_FACE_NUM_SETTINGS][3] = {"HR", "M1", "SE", "YR", "MO", "DA", "ZO"};
static bool _quick_ticks_running;
static void _handle_alarm_button(movement_settings_t *settings, watch_date_time date_time, uint8_t current_page) {
// handles short or long pressing of the alarm button
switch (current_page) {
case 0: // hour
date_time.unit.hour = (date_time.unit.hour + 1) % 24;
break;
case 1: // minute
date_time.unit.minute = (date_time.unit.minute + 1) % 60;
break;
case 2: // second
date_time.unit.second = 0;
break;
case 3: // year
date_time.unit.year = ((date_time.unit.year % 60) + 1);
break;
case 4: // month
date_time.unit.month = (date_time.unit.month % 12) + 1;
break;
case 5: { // day
date_time.unit.day = date_time.unit.day + 1;
break;
}
case 6: // time zone
settings->bit.time_zone++;
if (settings->bit.time_zone > 40) settings->bit.time_zone = 0;
break;
}
if (date_time.unit.day > days_in_month(date_time.unit.month, date_time.unit.year + WATCH_RTC_REFERENCE_YEAR))
date_time.unit.day = 1;
watch_rtc_set_date_time(date_time);
}
static void _abort_quick_ticks() {
if (_quick_ticks_running) {
_quick_ticks_running = false;
movement_request_tick_frequency(4);
}
}
void set_time_face_setup(movement_settings_t *settings, uint8_t watch_face_index, void ** context_ptr) {
(void) settings;
(void) watch_face_index;
if (*context_ptr == NULL) *context_ptr = malloc(sizeof(uint8_t));
}
void set_time_face_activate(movement_settings_t *settings, void *context) {
(void) settings;
*((uint8_t *)context) = 0;
movement_request_tick_frequency(4);
_quick_ticks_running = false;
}
bool set_time_face_loop(movement_event_t event, movement_settings_t *settings, void *context) {
uint8_t current_page = *((uint8_t *)context);
watch_date_time date_time = watch_rtc_get_date_time();
switch (event.event_type) {
case EVENT_TICK:
if (_quick_ticks_running) {
if (watch_get_pin_level(BTN_ALARM)) _handle_alarm_button(settings, date_time, current_page);
else _abort_quick_ticks();
}
break;
case EVENT_ALARM_LONG_PRESS:
if (current_page != 2) {
_quick_ticks_running = true;
movement_request_tick_frequency(8);
}
break;
case EVENT_ALARM_LONG_UP:
_abort_quick_ticks();
break;
case EVENT_MODE_BUTTON_UP:
_abort_quick_ticks();
movement_move_to_next_face();
return false;
case EVENT_LIGHT_BUTTON_DOWN:
current_page = (current_page + 1) % SET_TIME_FACE_NUM_SETTINGS;
*((uint8_t *)context) = current_page;
break;
case EVENT_ALARM_BUTTON_UP:
_abort_quick_ticks();
_handle_alarm_button(settings, date_time, current_page);
break;
case EVENT_TIMEOUT:
_abort_quick_ticks();
movement_move_to_face(0);
break;
default:
return movement_default_loop_handler(event, settings);
}
char buf[11];
bool set_leading_zero = false;
if (current_page < 3) {
watch_set_colon();
if (settings->bit.clock_mode_24h) {
if (!settings->bit.clock_24h_leading_zero)
watch_set_indicator(WATCH_INDICATOR_24H);
else if (date_time.unit.hour < 10)
set_leading_zero = true;
sprintf(buf, "%s %2d%02d%02d", set_time_face_titles[current_page], date_time.unit.hour, date_time.unit.minute, date_time.unit.second);
} else {
sprintf(buf, "%s %2d%02d%02d", set_time_face_titles[current_page], (date_time.unit.hour % 12) ? (date_time.unit.hour % 12) : 12, date_time.unit.minute, date_time.unit.second);
if (date_time.unit.hour < 12) watch_clear_indicator(WATCH_INDICATOR_PM);
else watch_set_indicator(WATCH_INDICATOR_PM);
}
} else if (current_page < 6) {
watch_clear_colon();
watch_clear_indicator(WATCH_INDICATOR_24H);
watch_clear_indicator(WATCH_INDICATOR_PM);
sprintf(buf, "%s %2d%02d%02d", set_time_face_titles[current_page], date_time.unit.year + 20, date_time.unit.month, date_time.unit.day);
} else {
if (event.subsecond % 2) {
watch_clear_colon();
sprintf(buf, "%s ", set_time_face_titles[current_page]);
} else {
watch_set_colon();
sprintf(buf, "%s %3d%02d ", set_time_face_titles[current_page], (int8_t) (movement_timezone_offsets[settings->bit.time_zone] / 60), (int8_t) (movement_timezone_offsets[settings->bit.time_zone] % 60) * (movement_timezone_offsets[settings->bit.time_zone] < 0 ? -1 : 1));
}
}
// blink up the parameter we're setting
if (event.subsecond % 2 && !_quick_ticks_running) {
switch (current_page) {
case 0:
case 3:
buf[4] = buf[5] = ' ';
break;
case 1:
case 4:
buf[6] = buf[7] = ' ';
break;
case 2:
case 5:
buf[8] = buf[9] = ' ';
break;
}
}
watch_display_string(buf, 0);
if (set_leading_zero)
watch_display_string("0", 4);
return true;
}
void set_time_face_resign(movement_settings_t *settings, void *context) {
(void) settings;
(void) context;
watch_set_led_off();
watch_store_backup_data(settings->reg, 0);
}

View File

@@ -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_