sensor-watch/movement/movement.c
Matheus Afonso Martins Moreira c9cbb82163 Revert merge of PR #437 - debouncing logic
This reverts commit a715265af6, reversing
changes made to 9c093f9540.

Insidious issues were found in the course of long term testing
by the community, and further reviews of the code were not enough
to pinpoint the issue and fix it. So for now the appropriate action
is to revert these changes while development continues, and possibly
merge them back in once they have been stabilized.

Tested-on-hardware-by: David Volovskiy <devolov@gmail.com>
Tested-on-hardware-by: CarpeNoctem <cryptomax@pm.me>
Tested-on-hardware-by: Krzysztof Gałka <@kshysztof@Discord>
2024-09-15 21:00:22 -03:00

845 lines
33 KiB
C

/*
* 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"
#include "watch_utility.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
// Default to having DST get set
#ifndef MOVEMENT_DEFAULT_DST_ACTIVE
#define MOVEMENT_DEFAULT_DST_ACTIVE true
#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;
#define NUM_TIME_ZONES 41
const int16_t movement_timezone_offsets[NUM_TIME_ZONES] = {
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)
};
/* These are approximate equivalent DST timezones for each
* timezone in the offset table. Unlike the full tzinfo file,
* the time-offsets used above are incomplete, so there are
* cases below where an approximate DST timezone is proposed
* for a timezone where no one observes DST, and cases
* where we can't propose an equivaent DST timezone since
* there isn't an appropriate one in the offset table.
*
* However, this should be good enough for anyone living in
* a DST-observing region to manually toggle DST without
* having to separately change the hour and timezone info
* in the time set face.
*/
const int16_t movement_timezone_dst_offsets[NUM_TIME_ZONES] = {
60, // 0 UTC + 1 = CET
120, // 1 CET + 1 = SAST
189, // 2 SAST + 1 = AST
240, // 3 AST + 1 = GST
270, // 4 IST + 1 = AT
300, // 5 GST + 1 = PST
330, // 6 AT + 1 = IST
360, // 7 PST + 1 = KT
390, // 8 IST + 1 = MT
345, // 9 Nepal has no equivalent DST timezone, but they don't observe DST anyway
420, // 10 KT + 1 = TST
390, // 11 Myanmar has no equivalent DST timezone, but they don't observe DST anyway
480, // 12 TST + 1 = CST
540, // 13 CST + 1 = JST
525, // 14 ACWST has no equivalent DST timezone, but they don't observe DST anyway
600, // 15 JST + 1 = AEST
630, // 16 ACST + 1 = LHST
660, // 17 AEST + 1 = SIT
630, // 18 LHST has no equivalent DST timezone, but they don't observe DST anyway
720, // 19 SIT + 1 = NZST
780, // 20 NZST + 1 = TT
825, // 21 CST + 1 = CDT
840, // 22 TT + 1 = LIT
825, // 23 CDT is already a daylight timezone
840, // 24 LIT has no equivalent DST timezone, but they don't observe DST anyway
-660, // 25 BIT + 1 = NT
-600, // 26 NT + 1 = HAST
-540, // 27 HAST + 1 = AST
-570, // 28 MIT has no equivalent DST timezone, but they don't observe DST anyway
-480, // 29 AST + 1 = PST
-420, // 30 PST + 1 = MST
-360, // 31 MST + 1 = CST
-300, // 32 CST + 1 = EST
-240, // 33 EST + 1 = AST
-210, // 34 VST + 1 = NST
-180, // 35 AST + 1 = BT
-150, // 36 NST + 1 = NDT
-120, // 37 BT + 1 = 39
-150, // 38 NDT is already a daylight timezone
-60, // 39 FNT + 1 = AST
0 // 40 AST + 1 = UTC
};
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 bool _check_and_act_on_daylight_savings(void) {
if (!movement_state.settings.bit.dst_active) return false;
watch_date_time date_time = watch_rtc_get_date_time();
// No need for all of the unix time calculations for times not at the beginning or end of the hour
if (date_time.unit.minute > 1 && date_time.unit.minute < 59) return false;
uint8_t dst_result = get_dst_status(date_time);
bool dst_skip_rolling_back = get_dst_skip_rolling_back();
if (dst_skip_rolling_back && (dst_result == DST_ENDED)) {
clear_dst_skip_rolling_back();
}
else if (dst_result == DST_ENDING && !dst_skip_rolling_back) {
date_time.unit.hour = (date_time.unit.hour + 24 - 1) % 24;
watch_rtc_set_date_time(date_time);
set_dst_skip_rolling_back();
return true;
}
else if (dst_result == DST_STARTING) {
date_time.unit.hour = (date_time.unit.hour + 1) % 24;
watch_rtc_set_date_time(date_time);
return true;
}
return false;
}
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]);
}
}
_check_and_act_on_daylight_savings();
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++;
}
int16_t get_timezone_offset(uint8_t timezone_idx, watch_date_time date_time) {
if (movement_state.settings.bit.dst_active && dst_occurring(date_time))
return movement_timezone_dst_offsets[timezone_idx];
return movement_timezone_offsets[timezone_idx];
}
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.settings.bit.dst_active = MOVEMENT_DEFAULT_DST_ACTIVE;
#ifdef MAKEFILE_TIMEZONE
timezone_offsets = dst_occurring(watch_rtc_get_date_time()) ? movement_timezone_dst_offsets : movement_timezone_offsets;
for (int i = 0; i < NUM_TIME_ZONES; i++) {
if (timezone_offsets[i] == MAKEFILE_TIMEZONE) {
movement_state.settings.bit.time_zone = i;
break;
}
}
#else
movement_state.settings.bit.time_zone = 35; // Atlantic Time as default
#endif
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__
const int16_t* timezone_offsets;
int32_t time_zone_offset = EM_ASM_INT({
return -new Date().getTimezoneOffset();
});
timezone_offsets = dst_occurring(watch_rtc_get_date_time()) ? movement_timezone_dst_offsets : movement_timezone_offsets;
for (int i = 0; i < NUM_TIME_ZONES; i++) {
if (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++;
}
}