mirror of
https://github.com/joeycastillo/second-movement.git
synced 2026-02-04 18:05:35 +00:00
1491 lines
55 KiB
C
1491 lines
55 KiB
C
/*
|
|
* MIT License
|
|
*
|
|
* Copyright (c) 2022 Joey Castillo
|
|
* Copyright (c) 2025 Alessandro Genova
|
|
*
|
|
* 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 <stdlib.h>
|
|
#include "app.h"
|
|
#include "watch.h"
|
|
#include "watch_utility.h"
|
|
#include "usb.h"
|
|
#include "watch_private.h"
|
|
#include "movement.h"
|
|
#include "filesystem.h"
|
|
#include "shell.h"
|
|
#include "utz.h"
|
|
#include "zones.h"
|
|
#include "tc.h"
|
|
#include "evsys.h"
|
|
#include "delay.h"
|
|
#include "thermistor_driver.h"
|
|
|
|
#include "movement_config.h"
|
|
|
|
#include "movement_custom_signal_tunes.h"
|
|
|
|
#if __EMSCRIPTEN__
|
|
#include <emscripten.h>
|
|
void _wake_up_simulator(void);
|
|
#else
|
|
#include "watch_usb_cdc.h"
|
|
#endif
|
|
|
|
volatile movement_state_t movement_state;
|
|
void * watch_face_contexts[MOVEMENT_NUM_FACES];
|
|
watch_date_time_t 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};
|
|
|
|
const uint32_t _movement_mode_button_events_mask = 0b1111 << EVENT_MODE_BUTTON_DOWN;
|
|
const uint32_t _movement_light_button_events_mask = 0b1111 << EVENT_LIGHT_BUTTON_DOWN;
|
|
const uint32_t _movement_alarm_button_events_mask = 0b1111 << EVENT_ALARM_BUTTON_DOWN;
|
|
const uint32_t _movement_button_events_mask = _movement_mode_button_events_mask | _movement_light_button_events_mask | _movement_alarm_button_events_mask;
|
|
|
|
typedef struct {
|
|
movement_event_type_t down_event;
|
|
watch_cb_t cb_longpress;
|
|
movement_timeout_index_t timeout_index;
|
|
volatile bool is_down;
|
|
volatile rtc_counter_t down_timestamp;
|
|
#if MOVEMENT_DEBOUNCE_TICKS
|
|
volatile rtc_counter_t up_timestamp;
|
|
#endif
|
|
} movement_button_t;
|
|
|
|
/* Pieces of state that can be modified by the various interrupt callbacks.
|
|
The interrupt writes state changes here, and it will be acted upon on the next app_loop invokation.
|
|
*/
|
|
typedef struct {
|
|
volatile uint32_t pending_events;
|
|
volatile bool turn_led_off;
|
|
volatile bool has_pending_sequence;
|
|
volatile bool enter_sleep_mode;
|
|
volatile bool exit_sleep_mode;
|
|
volatile bool is_sleeping;
|
|
volatile uint8_t subsecond;
|
|
volatile rtc_counter_t minute_counter;
|
|
volatile bool minute_alarm_fired;
|
|
volatile bool is_buzzing;
|
|
volatile uint8_t pending_sequence_priority;
|
|
volatile bool schedule_next_comp;
|
|
|
|
// button tracking for long press
|
|
movement_button_t mode_button;
|
|
movement_button_t light_button;
|
|
movement_button_t alarm_button;
|
|
|
|
// button events that will not be passed to the current face loop, but will instead passed directly to the default loop handler.
|
|
volatile uint32_t passthrough_events;
|
|
} movement_volatile_state_t;
|
|
|
|
movement_volatile_state_t movement_volatile_state;
|
|
|
|
// The last sequence that we have been asked to play while the watch was in deep sleep
|
|
static int8_t *_pending_sequence;
|
|
|
|
// The note sequence of the default alarm
|
|
int8_t alarm_tune[] = {
|
|
BUZZER_NOTE_C8, 4,
|
|
BUZZER_NOTE_REST, 7,
|
|
BUZZER_NOTE_C8, 4,
|
|
BUZZER_NOTE_REST, 7,
|
|
BUZZER_NOTE_C8, 4,
|
|
BUZZER_NOTE_REST, 7,
|
|
BUZZER_NOTE_C8, 4,
|
|
BUZZER_NOTE_REST, 27,
|
|
-8, 9,
|
|
0
|
|
};
|
|
|
|
int8_t _movement_dst_offset_cache[NUM_ZONE_NAMES] = {0};
|
|
#define TIMEZONE_DOES_NOT_OBSERVE (-127)
|
|
|
|
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_minute_alarm_fired(void);
|
|
void cb_tick(void);
|
|
void cb_mode_btn_timeout_interrupt(void);
|
|
void cb_light_btn_timeout_interrupt(void);
|
|
void cb_alarm_btn_timeout_interrupt(void);
|
|
void cb_led_timeout_interrupt(void);
|
|
void cb_resign_timeout_interrupt(void);
|
|
void cb_sleep_timeout_interrupt(void);
|
|
void cb_buzzer_start(void);
|
|
void cb_buzzer_stop(void);
|
|
|
|
void cb_accelerometer_event(void);
|
|
void cb_accelerometer_wake(void);
|
|
|
|
#if __EMSCRIPTEN__
|
|
void yield(void) {
|
|
}
|
|
#else
|
|
void yield(void) {
|
|
tud_task();
|
|
cdc_task();
|
|
}
|
|
#endif
|
|
|
|
static udatetime_t _movement_convert_date_time_to_udate(watch_date_time_t date_time) {
|
|
return (udatetime_t) {
|
|
.date.dayofmonth = date_time.unit.day,
|
|
.date.dayofweek = dayofweek(UYEAR_FROM_YEAR(date_time.unit.year + WATCH_RTC_REFERENCE_YEAR), date_time.unit.month, date_time.unit.day),
|
|
.date.month = date_time.unit.month,
|
|
.date.year = UYEAR_FROM_YEAR(date_time.unit.year + WATCH_RTC_REFERENCE_YEAR),
|
|
.time.hour = date_time.unit.hour,
|
|
.time.minute = date_time.unit.minute,
|
|
.time.second = date_time.unit.second
|
|
};
|
|
}
|
|
|
|
static watch_buzzer_volume_t _movement_get_buzzer_volume(movement_buzzer_priority_t priority) {
|
|
switch (priority) {
|
|
case BUZZER_PRIORITY_BUTTON:
|
|
return movement_button_volume();
|
|
case BUZZER_PRIORITY_SIGNAL:
|
|
return movement_signal_volume();
|
|
case BUZZER_PRIORITY_ALARM:
|
|
return movement_alarm_volume();
|
|
default:
|
|
return WATCH_BUZZER_VOLUME_LOUD;
|
|
}
|
|
}
|
|
|
|
static void _movement_set_top_of_minute_alarm() {
|
|
uint32_t counter = watch_rtc_get_counter();
|
|
uint32_t next_minute_counter;
|
|
watch_date_time_t date_time = watch_rtc_get_date_time();
|
|
uint32_t freq = watch_rtc_get_frequency();
|
|
uint32_t half_freq = freq >> 1;
|
|
uint32_t subsecond_mask = freq - 1;
|
|
uint32_t ticks_per_minute = watch_rtc_get_ticks_per_minute();
|
|
|
|
// get the counter at the last second tick
|
|
next_minute_counter = counter & (~subsecond_mask);
|
|
// add/subtract half second shift to sync up second tick with the 1Hz interrupt
|
|
next_minute_counter += (counter & subsecond_mask) >= half_freq ? half_freq : -half_freq;
|
|
// counter at the next top of the minute
|
|
next_minute_counter += (60 - date_time.unit.second) * freq;
|
|
|
|
// Since the minute alarm is very important, double/triple check to make sure that it will fire.
|
|
// These are theoretical corner cases that probably can't even happen, but since we do a subtraction
|
|
// above I wanna be certain that we don't schedule the next alarm at a counter value just before the
|
|
// current counter, which would result in the alarm firing after more than one year.
|
|
// This should be robust to the counter overflow, and we should ever iterate once at most.
|
|
if (next_minute_counter == counter) {
|
|
next_minute_counter += ticks_per_minute;
|
|
}
|
|
|
|
while ((next_minute_counter - counter) > ticks_per_minute) {
|
|
next_minute_counter += ticks_per_minute;
|
|
}
|
|
|
|
movement_volatile_state.minute_counter = next_minute_counter;
|
|
|
|
watch_rtc_register_comp_callback_no_schedule(cb_minute_alarm_fired, next_minute_counter, MINUTE_TIMEOUT);
|
|
movement_volatile_state.schedule_next_comp = true;
|
|
}
|
|
|
|
static bool _movement_update_dst_offset_cache(void) {
|
|
uzone_t local_zone;
|
|
udatetime_t udate_time;
|
|
bool dst_changed = false;
|
|
watch_date_time_t system_date_time = watch_rtc_get_date_time();
|
|
|
|
for (uint8_t i = 0; i < NUM_ZONE_NAMES; i++) {
|
|
unpack_zone(&zone_defns[i], "", &local_zone);
|
|
watch_date_time_t date_time = watch_utility_date_time_convert_zone(system_date_time, 0, local_zone.offset.hours * 3600 + local_zone.offset.minutes * 60);
|
|
|
|
if (!!local_zone.rules_len) {
|
|
// if local zone has DST rules, we need to see if DST applies.
|
|
udate_time = _movement_convert_date_time_to_udate(date_time);
|
|
uoffset_t offset;
|
|
get_current_offset(&local_zone, &udate_time, &offset);
|
|
int8_t new_offset = (offset.hours * 60 + offset.minutes) / 15;
|
|
if (_movement_dst_offset_cache[i] != new_offset) {
|
|
_movement_dst_offset_cache[i] = new_offset;
|
|
dst_changed = true;
|
|
}
|
|
} else {
|
|
// otherwise set the cache to a constant value that indicates no DST check needs to be performed.
|
|
_movement_dst_offset_cache[i] = TIMEZONE_DOES_NOT_OBSERVE;
|
|
}
|
|
}
|
|
|
|
return dst_changed;
|
|
}
|
|
|
|
static inline void _movement_reset_inactivity_countdown(void) {
|
|
rtc_counter_t counter = watch_rtc_get_counter();
|
|
uint32_t freq = watch_rtc_get_frequency();
|
|
|
|
watch_rtc_register_comp_callback_no_schedule(
|
|
cb_resign_timeout_interrupt,
|
|
counter + movement_timeout_inactivity_deadlines[movement_state.settings.bit.to_interval] * freq,
|
|
RESIGN_TIMEOUT
|
|
);
|
|
|
|
movement_volatile_state.enter_sleep_mode = false;
|
|
|
|
watch_rtc_register_comp_callback_no_schedule(
|
|
cb_sleep_timeout_interrupt,
|
|
counter + movement_le_inactivity_deadlines[movement_state.settings.bit.le_interval] * freq,
|
|
SLEEP_TIMEOUT
|
|
);
|
|
|
|
movement_volatile_state.schedule_next_comp = true;
|
|
}
|
|
|
|
static inline void _movement_disable_inactivity_countdown(void) {
|
|
watch_rtc_disable_comp_callback_no_schedule(RESIGN_TIMEOUT);
|
|
watch_rtc_disable_comp_callback_no_schedule(SLEEP_TIMEOUT);
|
|
movement_volatile_state.schedule_next_comp = true;
|
|
}
|
|
|
|
static void _movement_renew_top_of_minute_alarm(void) {
|
|
// Renew the alarm for a minute from the previous one (ensures no drift)
|
|
movement_volatile_state.minute_counter += watch_rtc_get_ticks_per_minute();
|
|
watch_rtc_register_comp_callback_no_schedule(cb_minute_alarm_fired, movement_volatile_state.minute_counter, MINUTE_TIMEOUT);
|
|
movement_volatile_state.schedule_next_comp = true;
|
|
}
|
|
|
|
static void _movement_handle_button_presses(uint32_t pending_events) {
|
|
bool any_up = false;
|
|
bool any_down = false;
|
|
|
|
movement_button_t* buttons[3] = {
|
|
&movement_volatile_state.mode_button,
|
|
&movement_volatile_state.light_button,
|
|
&movement_volatile_state.alarm_button
|
|
};
|
|
|
|
uint32_t button_events_masks[3] = {
|
|
_movement_mode_button_events_mask,
|
|
_movement_light_button_events_mask,
|
|
_movement_alarm_button_events_mask,
|
|
};
|
|
|
|
for (uint8_t i = 0; i < 3; i++) {
|
|
movement_button_t* button = buttons[i];
|
|
|
|
// If a button down occurred
|
|
if (pending_events & (1 << button->down_event)) {
|
|
watch_rtc_register_comp_callback_no_schedule(button->cb_longpress, button->down_timestamp + MOVEMENT_LONG_PRESS_TICKS, button->timeout_index);
|
|
any_down = true;
|
|
// this button's events will start getting passed to the face
|
|
movement_volatile_state.passthrough_events &= ~button_events_masks[i];
|
|
}
|
|
|
|
// If a button up or button long up occurred
|
|
if (pending_events & (
|
|
(1 << (button->down_event + 1)) |
|
|
(1 << (button->down_event + 3))
|
|
)) {
|
|
// We cancel the timeout if it hasn't fired yet
|
|
watch_rtc_disable_comp_callback_no_schedule(button->timeout_index);
|
|
any_up = true;
|
|
}
|
|
}
|
|
|
|
if (any_down) {
|
|
// force alarm off if the user pressed a button.
|
|
watch_buzzer_abort_sequence();
|
|
|
|
// Delay auto light off if the user is still interacting with the watch.
|
|
if (movement_state.light_on) {
|
|
movement_illuminate_led();
|
|
}
|
|
}
|
|
|
|
if (any_down || any_up) {
|
|
_movement_reset_inactivity_countdown();
|
|
movement_volatile_state.schedule_next_comp = true;
|
|
}
|
|
}
|
|
|
|
static void _movement_handle_top_of_minute(void) {
|
|
watch_date_time_t date_time = watch_rtc_get_date_time();
|
|
|
|
// update the DST offset cache every 30 minutes, since someplace in the world could change.
|
|
if (date_time.unit.minute % 30 == 0) {
|
|
_movement_update_dst_offset_cache();
|
|
}
|
|
|
|
for(uint8_t i = 0; i < MOVEMENT_NUM_FACES; i++) {
|
|
// For each face that offers an advisory...
|
|
if (watch_faces[i].advise != NULL) {
|
|
// ...we ask for one.
|
|
movement_watch_face_advisory_t advisory = watch_faces[i].advise(watch_face_contexts[i]);
|
|
|
|
// If it wants a background task...
|
|
if (advisory.wants_background_task) {
|
|
// we give it one. pretty straightforward!
|
|
movement_event_t background_event = { EVENT_BACKGROUND_TASK, 0 };
|
|
watch_faces[i].loop(background_event, watch_face_contexts[i]);
|
|
}
|
|
|
|
// TODO: handle other advisory types
|
|
}
|
|
}
|
|
}
|
|
|
|
static void _movement_handle_scheduled_tasks(void) {
|
|
watch_date_time_t 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, 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 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 periodic callbacks
|
|
watch_rtc_disable_matching_periodic_callbacks(0xFF);
|
|
|
|
// this left-justifies the period in a 32-bit integer.
|
|
uint32_t tmp = (freq & 0xFF) << 24;
|
|
// now we can count the leading zeroes to get the value we need.
|
|
// 0x01 (1 Hz) will have 7 leading zeros for PER7. 0x80 (128 Hz) will have no leading zeroes for PER0.
|
|
uint8_t per_n = __builtin_clz(tmp);
|
|
|
|
movement_state.tick_frequency = freq;
|
|
movement_state.tick_pern = per_n;
|
|
|
|
watch_rtc_register_periodic_callback(cb_tick, freq);
|
|
}
|
|
|
|
void movement_illuminate_led(void) {
|
|
if (movement_state.settings.bit.led_duration != 0b111) {
|
|
movement_state.light_on = true;
|
|
watch_set_led_color_rgb(movement_state.settings.bit.led_red_color | movement_state.settings.bit.led_red_color << 4,
|
|
movement_state.settings.bit.led_green_color | movement_state.settings.bit.led_green_color << 4,
|
|
movement_state.settings.bit.led_blue_color | movement_state.settings.bit.led_blue_color << 4);
|
|
if (movement_state.settings.bit.led_duration == 0) {
|
|
// Do nothing it'll be turned off on button release
|
|
} else {
|
|
// Set a timeout to turn off the light
|
|
rtc_counter_t counter = watch_rtc_get_counter();
|
|
uint32_t freq = watch_rtc_get_frequency();
|
|
watch_rtc_register_comp_callback_no_schedule(
|
|
cb_led_timeout_interrupt,
|
|
counter + (movement_state.settings.bit.led_duration * 2 - 1) * freq,
|
|
LED_TIMEOUT
|
|
);
|
|
movement_volatile_state.schedule_next_comp = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
void movement_force_led_on(uint8_t red, uint8_t green, uint8_t blue) {
|
|
// this is hacky, we need a way for watch faces to set an arbitrary color and prevent Movement from turning it right back off.
|
|
movement_state.light_on = true;
|
|
watch_set_led_color_rgb(red, green, blue);
|
|
// The led will stay on until movement_force_led_off is called, so disable the led timeout in case we were in the middle of it.
|
|
watch_rtc_disable_comp_callback_no_schedule(LED_TIMEOUT);
|
|
movement_volatile_state.schedule_next_comp = true;
|
|
}
|
|
|
|
void movement_force_led_off(void) {
|
|
movement_state.light_on = false;
|
|
// The led timeout probably already triggered, but still disable just in case we are switching off the light by other means
|
|
watch_rtc_disable_comp_callback_no_schedule(LED_TIMEOUT);
|
|
movement_volatile_state.schedule_next_comp = true;
|
|
watch_set_led_off();
|
|
}
|
|
|
|
bool movement_default_loop_handler(movement_event_t event) {
|
|
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:
|
|
case EVENT_LIGHT_LONG_UP:
|
|
if (movement_state.settings.bit.led_duration == 0) {
|
|
movement_force_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_t 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_t date_time) {
|
|
watch_date_time_t 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_sleep(void) {
|
|
movement_volatile_state.enter_sleep_mode = true;
|
|
}
|
|
|
|
void movement_request_wake() {
|
|
movement_volatile_state.exit_sleep_mode = true;
|
|
_movement_reset_inactivity_countdown();
|
|
}
|
|
|
|
void cb_buzzer_start(void) {
|
|
movement_volatile_state.is_buzzing = true;
|
|
}
|
|
|
|
void cb_buzzer_stop(void) {
|
|
movement_volatile_state.is_buzzing = false;
|
|
movement_volatile_state.pending_sequence_priority = 0;
|
|
}
|
|
|
|
void movement_play_note(watch_buzzer_note_t note, uint16_t duration_ms) {
|
|
static int8_t single_note_sequence[3];
|
|
|
|
single_note_sequence[0] = note;
|
|
// 64 ticks per second for the tc0
|
|
// Each tick is approximately 15ms
|
|
uint16_t duration = duration_ms / 15;
|
|
if (duration > 127) duration = 127;
|
|
single_note_sequence[1] = (int8_t)duration;
|
|
single_note_sequence[2] = 0;
|
|
|
|
movement_play_sequence(single_note_sequence, BUZZER_PRIORITY_BUTTON);
|
|
}
|
|
|
|
void movement_play_signal(void) {
|
|
movement_play_sequence(signal_tune, BUZZER_PRIORITY_SIGNAL);
|
|
}
|
|
|
|
void movement_play_alarm(void) {
|
|
movement_play_sequence(alarm_tune, BUZZER_PRIORITY_ALARM);
|
|
}
|
|
|
|
void movement_play_alarm_beeps(uint8_t rounds, watch_buzzer_note_t alarm_note) {
|
|
// Ugly but necessary to avoid breaking backward compatibility with some faces.
|
|
// Create an alarm tune on the fly with the specified note and repetition.
|
|
static int8_t custom_alarm_tune[19];
|
|
|
|
if (rounds == 0) rounds = 1;
|
|
if (rounds > 20) rounds = 20;
|
|
|
|
for (uint8_t i = 0; i < 9; i++) {
|
|
uint8_t note_idx = i * 2;
|
|
uint8_t duration_idx = note_idx + 1;
|
|
|
|
int8_t note = alarm_tune[note_idx];
|
|
int8_t duration = alarm_tune[duration_idx];
|
|
|
|
if (note == BUZZER_NOTE_C8) {
|
|
note = alarm_note;
|
|
} else if (note < 0) {
|
|
duration = rounds;
|
|
}
|
|
|
|
custom_alarm_tune[note_idx] = note;
|
|
custom_alarm_tune[duration_idx] = duration;
|
|
}
|
|
|
|
custom_alarm_tune[18] = 0;
|
|
|
|
movement_play_sequence(custom_alarm_tune, BUZZER_PRIORITY_ALARM);
|
|
}
|
|
|
|
void movement_play_sequence(int8_t *note_sequence, movement_buzzer_priority_t priority) {
|
|
// Priority is used to ensure that lower priority sequences don't cancel higher priority ones
|
|
// Priotity order: alarm(2) > signal(1) > note(0)
|
|
if (priority < movement_volatile_state.pending_sequence_priority) {
|
|
return;
|
|
}
|
|
|
|
movement_volatile_state.pending_sequence_priority = priority;
|
|
|
|
// The tcc is off during sleep, we can't play immediately.
|
|
// Ask to wake up the watch.
|
|
if (movement_volatile_state.is_sleeping) {
|
|
_pending_sequence = note_sequence;
|
|
movement_volatile_state.has_pending_sequence = true;
|
|
movement_volatile_state.exit_sleep_mode = true;
|
|
} else {
|
|
watch_buzzer_play_sequence_with_volume(note_sequence, NULL, _movement_get_buzzer_volume(priority));
|
|
}
|
|
}
|
|
|
|
uint8_t movement_claim_backup_register(void) {
|
|
// We use backup register 7 in watch_rtc to keep track of the reference time
|
|
if (movement_state.next_available_backup_register >= 7) return 0;
|
|
return movement_state.next_available_backup_register++;
|
|
}
|
|
|
|
int32_t movement_get_current_timezone_offset_for_zone(uint8_t zone_index) {
|
|
int8_t cached_dst_offset = _movement_dst_offset_cache[zone_index];
|
|
|
|
if (cached_dst_offset == TIMEZONE_DOES_NOT_OBSERVE) {
|
|
// if time zone doesn't observe DST, we can just return the standard time offset from the zone definition.
|
|
return (int32_t)zone_defns[zone_index].offset_inc_minutes * OFFSET_INCREMENT * 60;
|
|
} else {
|
|
// otherwise, we've precalculated the offset for this zone and can return it.
|
|
return (int32_t)cached_dst_offset * OFFSET_INCREMENT * 60;
|
|
}
|
|
}
|
|
|
|
int32_t movement_get_current_timezone_offset(void) {
|
|
return movement_get_current_timezone_offset_for_zone(movement_state.settings.bit.time_zone);
|
|
}
|
|
|
|
int32_t movement_get_timezone_index(void) {
|
|
return movement_state.settings.bit.time_zone;
|
|
}
|
|
|
|
void movement_set_timezone_index(uint8_t value) {
|
|
movement_state.settings.bit.time_zone = value;
|
|
}
|
|
|
|
watch_date_time_t movement_get_utc_date_time(void) {
|
|
return watch_rtc_get_date_time();
|
|
}
|
|
|
|
watch_date_time_t movement_get_date_time_in_zone(uint8_t zone_index) {
|
|
int32_t offset = movement_get_current_timezone_offset_for_zone(zone_index);
|
|
unix_timestamp_t timestamp = watch_rtc_get_unix_time();
|
|
return watch_utility_date_time_from_unix_time(timestamp, offset);
|
|
}
|
|
|
|
watch_date_time_t movement_get_local_date_time(void) {
|
|
static struct {
|
|
unix_timestamp_t timestamp;
|
|
rtc_date_time_t datetime;
|
|
} cached_date_time = {.datetime.reg=0, .timestamp=0};
|
|
|
|
unix_timestamp_t timestamp = watch_rtc_get_unix_time();
|
|
|
|
if (timestamp != cached_date_time.timestamp) {
|
|
cached_date_time.timestamp = timestamp;
|
|
cached_date_time.datetime = watch_utility_date_time_from_unix_time(timestamp, movement_get_current_timezone_offset());
|
|
}
|
|
|
|
return cached_date_time.datetime;
|
|
}
|
|
|
|
uint32_t movement_get_utc_timestamp(void) {
|
|
return watch_rtc_get_unix_time();
|
|
}
|
|
|
|
void movement_set_utc_date_time(watch_date_time_t date_time) {
|
|
movement_set_utc_timestamp(watch_utility_date_time_to_unix_time(date_time, 0));
|
|
}
|
|
|
|
void movement_set_local_date_time(watch_date_time_t date_time) {
|
|
int32_t current_offset = movement_get_current_timezone_offset();
|
|
movement_set_utc_timestamp(watch_utility_date_time_to_unix_time(date_time, current_offset));
|
|
}
|
|
|
|
void movement_set_utc_timestamp(uint32_t timestamp) {
|
|
watch_rtc_set_unix_time(timestamp);
|
|
|
|
// If the time was changed, the top of the minute alarm needs to be reset accordingly
|
|
_movement_set_top_of_minute_alarm();
|
|
|
|
// this may seem wasteful, but if the user's local time is in a zone that observes DST,
|
|
// they may have just crossed a DST boundary, which means the next call to this function
|
|
// could require a different offset to force local time back to UTC. Quelle horreur!
|
|
_movement_update_dst_offset_cache();
|
|
}
|
|
|
|
|
|
bool movement_button_should_sound(void) {
|
|
return movement_state.settings.bit.button_should_sound;
|
|
}
|
|
|
|
void movement_set_button_should_sound(bool value) {
|
|
movement_state.settings.bit.button_should_sound = value;
|
|
}
|
|
|
|
watch_buzzer_volume_t movement_button_volume(void) {
|
|
return movement_state.settings.bit.button_volume;
|
|
}
|
|
|
|
void movement_set_button_volume(watch_buzzer_volume_t value) {
|
|
movement_state.settings.bit.button_volume = value;
|
|
}
|
|
|
|
watch_buzzer_volume_t movement_signal_volume(void) {
|
|
return movement_state.signal_volume;
|
|
}
|
|
void movement_set_signal_volume(watch_buzzer_volume_t value) {
|
|
movement_state.signal_volume = value;
|
|
}
|
|
|
|
watch_buzzer_volume_t movement_alarm_volume(void) {
|
|
return movement_state.alarm_volume;
|
|
}
|
|
|
|
void movement_set_alarm_volume(watch_buzzer_volume_t value) {
|
|
movement_state.alarm_volume = value;
|
|
}
|
|
|
|
movement_clock_mode_t movement_clock_mode_24h(void) {
|
|
return movement_state.settings.bit.clock_mode_24h ? MOVEMENT_CLOCK_MODE_24H : MOVEMENT_CLOCK_MODE_12H;
|
|
}
|
|
|
|
void movement_set_clock_mode_24h(movement_clock_mode_t value) {
|
|
movement_state.settings.bit.clock_mode_24h = (value == MOVEMENT_CLOCK_MODE_24H);
|
|
}
|
|
|
|
bool movement_use_imperial_units(void) {
|
|
return movement_state.settings.bit.use_imperial_units;
|
|
}
|
|
|
|
void movement_set_use_imperial_units(bool value) {
|
|
movement_state.settings.bit.use_imperial_units = value;
|
|
}
|
|
|
|
uint8_t movement_get_fast_tick_timeout(void) {
|
|
return movement_state.settings.bit.to_interval;
|
|
}
|
|
|
|
void movement_set_fast_tick_timeout(uint8_t value) {
|
|
movement_state.settings.bit.to_interval = value;
|
|
}
|
|
|
|
uint8_t movement_get_low_energy_timeout(void) {
|
|
return movement_state.settings.bit.le_interval;
|
|
}
|
|
|
|
void movement_set_low_energy_timeout(uint8_t value) {
|
|
movement_state.settings.bit.le_interval = value;
|
|
}
|
|
|
|
movement_color_t movement_backlight_color(void) {
|
|
return (movement_color_t) {
|
|
.red = movement_state.settings.bit.led_red_color,
|
|
.green = movement_state.settings.bit.led_green_color,
|
|
.blue = movement_state.settings.bit.led_blue_color
|
|
};
|
|
}
|
|
|
|
void movement_set_backlight_color(movement_color_t color) {
|
|
movement_state.settings.bit.led_red_color = color.red;
|
|
movement_state.settings.bit.led_green_color = color.green;
|
|
movement_state.settings.bit.led_blue_color = color.blue;
|
|
}
|
|
|
|
uint8_t movement_get_backlight_dwell(void) {
|
|
return movement_state.settings.bit.led_duration;
|
|
}
|
|
|
|
void movement_set_backlight_dwell(uint8_t value) {
|
|
movement_state.settings.bit.led_duration = value;
|
|
}
|
|
|
|
void movement_store_settings(void) {
|
|
movement_settings_t old_settings;
|
|
filesystem_read_file("settings.u32", (char *)&old_settings, sizeof(movement_settings_t));
|
|
if (movement_state.settings.reg != old_settings.reg) {
|
|
filesystem_write_file("settings.u32", (char *)&movement_state.settings, sizeof(movement_settings_t));
|
|
}
|
|
}
|
|
|
|
bool movement_alarm_enabled(void) {
|
|
return movement_state.alarm_enabled;
|
|
}
|
|
|
|
void movement_set_alarm_enabled(bool value) {
|
|
movement_state.alarm_enabled = value;
|
|
}
|
|
|
|
bool movement_enable_tap_detection_if_available(void) {
|
|
if (movement_state.has_lis2dw) {
|
|
// configure tap duration threshold and enable Z axis
|
|
lis2dw_configure_tap_threshold(0, 0, 12, LIS2DW_REG_TAP_THS_Z_Z_AXIS_ENABLE);
|
|
lis2dw_configure_tap_duration(2, 2, 2);
|
|
|
|
// ramp data rate up to 400 Hz and high performance mode
|
|
lis2dw_set_low_noise_mode(true);
|
|
lis2dw_set_data_rate(LIS2DW_DATA_RATE_HP_400_HZ);
|
|
lis2dw_set_mode(LIS2DW_MODE_LOW_POWER);
|
|
lis2dw_enable_double_tap();
|
|
|
|
// Settling time (1 sample duration, i.e. 1/400Hz)
|
|
delay_ms(3);
|
|
|
|
// enable tap detection on INT1/A3.
|
|
lis2dw_configure_int1(LIS2DW_CTRL4_INT1_SINGLE_TAP | LIS2DW_CTRL4_INT1_DOUBLE_TAP);
|
|
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
bool movement_disable_tap_detection_if_available(void) {
|
|
if (movement_state.has_lis2dw) {
|
|
// Ramp data rate back down to the usual lowest rate to save power.
|
|
lis2dw_set_low_noise_mode(false);
|
|
lis2dw_set_data_rate(movement_state.accelerometer_background_rate);
|
|
lis2dw_set_mode(LIS2DW_MODE_LOW_POWER);
|
|
lis2dw_disable_double_tap();
|
|
// ...disable Z axis (not sure if this is needed, does this save power?)...
|
|
lis2dw_configure_tap_threshold(0, 0, 0, 0);
|
|
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
lis2dw_data_rate_t movement_get_accelerometer_background_rate(void) {
|
|
if (movement_state.has_lis2dw) return movement_state.accelerometer_background_rate;
|
|
else return LIS2DW_DATA_RATE_POWERDOWN;
|
|
}
|
|
|
|
bool movement_set_accelerometer_background_rate(lis2dw_data_rate_t new_rate) {
|
|
if (movement_state.has_lis2dw) {
|
|
if (movement_state.accelerometer_background_rate != new_rate) {
|
|
lis2dw_set_data_rate(new_rate);
|
|
movement_state.accelerometer_background_rate = new_rate;
|
|
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
uint8_t movement_get_accelerometer_motion_threshold(void) {
|
|
if (movement_state.has_lis2dw) return movement_state.accelerometer_motion_threshold;
|
|
else return 0;
|
|
}
|
|
|
|
bool movement_set_accelerometer_motion_threshold(uint8_t new_threshold) {
|
|
if (movement_state.has_lis2dw) {
|
|
if (movement_state.accelerometer_motion_threshold != new_threshold) {
|
|
lis2dw_configure_wakeup_threshold(new_threshold);
|
|
movement_state.accelerometer_motion_threshold = new_threshold;
|
|
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
float movement_get_temperature(void) {
|
|
float temperature_c = (float)0xFFFFFFFF;
|
|
#if __EMSCRIPTEN__
|
|
temperature_c = EM_ASM_DOUBLE({
|
|
return temp_c || 25.0;
|
|
});
|
|
#else
|
|
|
|
if (movement_state.has_thermistor) {
|
|
thermistor_driver_enable();
|
|
temperature_c = thermistor_driver_get_temperature();
|
|
thermistor_driver_disable();
|
|
} else if (movement_state.has_lis2dw) {
|
|
int16_t val = lis2dw_get_temperature();
|
|
val = val >> 4;
|
|
temperature_c = 25 + (float)val / 16.0;
|
|
}
|
|
#endif
|
|
|
|
return temperature_c;
|
|
}
|
|
|
|
void app_init(void) {
|
|
_watch_init();
|
|
|
|
filesystem_init();
|
|
|
|
// check if we are plugged into USB power.
|
|
HAL_GPIO_VBUS_DET_in();
|
|
HAL_GPIO_VBUS_DET_pulldown();
|
|
delay_ms(100);
|
|
if (HAL_GPIO_VBUS_DET_read()){
|
|
/// if so, enable USB functionality.
|
|
_watch_enable_usb();
|
|
}
|
|
HAL_GPIO_VBUS_DET_off();
|
|
|
|
memset((void *)&movement_state, 0, sizeof(movement_state));
|
|
|
|
movement_volatile_state.pending_events = 0;
|
|
movement_volatile_state.turn_led_off = false;
|
|
|
|
movement_volatile_state.minute_alarm_fired = false;
|
|
movement_volatile_state.minute_counter = 0;
|
|
|
|
movement_volatile_state.enter_sleep_mode = false;
|
|
movement_volatile_state.exit_sleep_mode = false;
|
|
movement_volatile_state.has_pending_sequence = false;
|
|
movement_volatile_state.is_sleeping = false;
|
|
|
|
movement_volatile_state.is_buzzing = false;
|
|
movement_volatile_state.pending_sequence_priority = 0;
|
|
|
|
movement_volatile_state.mode_button.down_event = EVENT_MODE_BUTTON_DOWN;
|
|
movement_volatile_state.mode_button.is_down = false;
|
|
movement_volatile_state.mode_button.down_timestamp = 0;
|
|
movement_volatile_state.mode_button.timeout_index = MODE_BUTTON_TIMEOUT;
|
|
movement_volatile_state.mode_button.cb_longpress = cb_mode_btn_timeout_interrupt;
|
|
|
|
movement_volatile_state.light_button.down_event = EVENT_LIGHT_BUTTON_DOWN;
|
|
movement_volatile_state.light_button.is_down = false;
|
|
movement_volatile_state.light_button.down_timestamp = 0;
|
|
movement_volatile_state.light_button.timeout_index = LIGHT_BUTTON_TIMEOUT;
|
|
movement_volatile_state.light_button.cb_longpress = cb_light_btn_timeout_interrupt;
|
|
|
|
movement_volatile_state.alarm_button.down_event = EVENT_ALARM_BUTTON_DOWN;
|
|
movement_volatile_state.alarm_button.is_down = false;
|
|
movement_volatile_state.alarm_button.down_timestamp = 0;
|
|
movement_volatile_state.alarm_button.timeout_index = ALARM_BUTTON_TIMEOUT;
|
|
movement_volatile_state.alarm_button.cb_longpress = cb_alarm_btn_timeout_interrupt;
|
|
|
|
movement_state.has_thermistor = thermistor_driver_init();
|
|
|
|
bool settings_file_exists = filesystem_file_exists("settings.u32");
|
|
movement_settings_t maybe_settings;
|
|
if (settings_file_exists && maybe_settings.bit.version == 0) {
|
|
filesystem_read_file("settings.u32", (char *) &maybe_settings, sizeof(movement_settings_t));
|
|
}
|
|
|
|
if (settings_file_exists && maybe_settings.bit.version == 0) {
|
|
// If settings file exists and has a valid version, restore it!
|
|
movement_state.settings.reg = maybe_settings.reg;
|
|
} else {
|
|
// Otherwise set default values.
|
|
movement_state.settings.bit.version = 0;
|
|
movement_state.settings.bit.clock_mode_24h = MOVEMENT_DEFAULT_24H_MODE;
|
|
movement_state.settings.bit.time_zone = UTZ_UTC;
|
|
movement_state.settings.bit.led_red_color = MOVEMENT_DEFAULT_RED_COLOR;
|
|
movement_state.settings.bit.led_green_color = MOVEMENT_DEFAULT_GREEN_COLOR;
|
|
#if defined(WATCH_BLUE_TCC_CHANNEL) && !defined(WATCH_GREEN_TCC_CHANNEL)
|
|
// If there is a blue LED but no green LED, this is a blue Special Edition board.
|
|
// In the past, the "green color" showed up as the blue color on the blue board.
|
|
if (MOVEMENT_DEFAULT_RED_COLOR == 0 && MOVEMENT_DEFAULT_BLUE_COLOR == 0) {
|
|
// If the red color is 0 and the blue color is 0, we'll fall back to the old
|
|
// behavior, since otherwise there would be no default LED color.
|
|
movement_state.settings.bit.led_blue_color = MOVEMENT_DEFAULT_GREEN_COLOR;
|
|
} else {
|
|
// however if either the red or blue color is nonzero, we'll assume the user
|
|
// has used the new defaults and knows what color they want. this could be red
|
|
// if blue is 0, or a custom color if both are nonzero.
|
|
movement_state.settings.bit.led_blue_color = MOVEMENT_DEFAULT_BLUE_COLOR;
|
|
}
|
|
#else
|
|
movement_state.settings.bit.led_blue_color = MOVEMENT_DEFAULT_BLUE_COLOR;
|
|
#endif
|
|
movement_state.settings.bit.button_should_sound = MOVEMENT_DEFAULT_BUTTON_SOUND;
|
|
movement_state.settings.bit.button_volume = MOVEMENT_DEFAULT_BUTTON_VOLUME;
|
|
movement_state.settings.bit.to_interval = MOVEMENT_DEFAULT_TIMEOUT_INTERVAL;
|
|
#ifdef MOVEMENT_LOW_ENERGY_MODE_FORBIDDEN
|
|
movement_state.settings.bit.le_interval = 0;
|
|
#else
|
|
movement_state.settings.bit.le_interval = MOVEMENT_DEFAULT_LOW_ENERGY_INTERVAL;
|
|
#endif
|
|
movement_state.settings.bit.led_duration = MOVEMENT_DEFAULT_LED_DURATION;
|
|
|
|
movement_store_settings();
|
|
}
|
|
|
|
watch_date_time_t date_time = watch_rtc_get_date_time();
|
|
if (date_time.reg == 0) {
|
|
date_time = watch_get_init_date_time();
|
|
// but convert from local time to UTC
|
|
date_time = watch_utility_date_time_convert_zone(date_time, movement_get_current_timezone_offset(), 0);
|
|
watch_rtc_set_date_time(date_time);
|
|
}
|
|
|
|
// register callbacks to be notified when buzzer starts/stops playing.
|
|
// this is so movement can be notified even when triggered by a face bypassing movement
|
|
watch_buzzer_register_global_callbacks(cb_buzzer_start, cb_buzzer_stop);
|
|
|
|
// populate the DST offset cache
|
|
_movement_update_dst_offset_cache();
|
|
|
|
if (movement_state.accelerometer_motion_threshold == 0) movement_state.accelerometer_motion_threshold = 32;
|
|
|
|
movement_state.signal_volume = MOVEMENT_DEFAULT_SIGNAL_VOLUME;
|
|
movement_state.alarm_volume = MOVEMENT_DEFAULT_ALARM_VOLUME;
|
|
movement_state.light_on = false;
|
|
movement_state.next_available_backup_register = 2;
|
|
_movement_reset_inactivity_countdown();
|
|
|
|
// set up the 1 minute alarm (for background tasks and low power updates)
|
|
_movement_set_top_of_minute_alarm();
|
|
}
|
|
|
|
void app_wake_from_backup(void) {
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
#if __EMSCRIPTEN__
|
|
int32_t time_zone_offset = EM_ASM_INT({
|
|
return -new Date().getTimezoneOffset();
|
|
});
|
|
for (int i = 0; i < NUM_ZONE_NAMES; i++) {
|
|
if (movement_get_current_timezone_offset_for_zone(i) == time_zone_offset * 60) {
|
|
movement_state.settings.bit.time_zone = i;
|
|
break;
|
|
}
|
|
}
|
|
#endif
|
|
}
|
|
|
|
// LCD autodetect uses the buttons as a a failsafe, so we should run it before we enable the button interrupts
|
|
watch_enable_display();
|
|
|
|
if (!movement_volatile_state.is_sleeping) {
|
|
watch_disable_extwake_interrupt(HAL_GPIO_BTN_ALARM_pin());
|
|
|
|
watch_enable_external_interrupts();
|
|
watch_register_interrupt_callback(HAL_GPIO_BTN_MODE_pin(), cb_mode_btn_interrupt, INTERRUPT_TRIGGER_BOTH);
|
|
watch_register_interrupt_callback(HAL_GPIO_BTN_LIGHT_pin(), cb_light_btn_interrupt, INTERRUPT_TRIGGER_BOTH);
|
|
watch_register_interrupt_callback(HAL_GPIO_BTN_ALARM_pin(), cb_alarm_btn_interrupt, INTERRUPT_TRIGGER_BOTH);
|
|
|
|
#ifdef I2C_SERCOM
|
|
static bool lis2dw_checked = false;
|
|
if (!lis2dw_checked) {
|
|
watch_enable_i2c();
|
|
if (lis2dw_begin()) {
|
|
movement_state.has_lis2dw = true;
|
|
} else {
|
|
movement_state.has_lis2dw = false;
|
|
watch_disable_i2c();
|
|
}
|
|
lis2dw_checked = true;
|
|
} else if (movement_state.has_lis2dw) {
|
|
watch_enable_i2c();
|
|
lis2dw_begin();
|
|
}
|
|
|
|
if (movement_state.has_lis2dw) {
|
|
lis2dw_set_mode(LIS2DW_MODE_LOW_POWER); // select low power (not high performance) mode
|
|
lis2dw_set_low_power_mode(LIS2DW_LP_MODE_1); // lowest power mode, 12-bit
|
|
lis2dw_set_low_noise_mode(false); // low noise mode raises power consumption slightly; we don't need it
|
|
lis2dw_enable_stationary_motion_detection(); // stationary/motion detection mode keeps the data rate at 1.6 Hz even in sleep
|
|
lis2dw_set_range(LIS2DW_RANGE_2_G); // Application note AN5038 recommends 2g range
|
|
lis2dw_enable_sleep(); // allow acceleromter to sleep and wake on activity
|
|
lis2dw_configure_wakeup_threshold(movement_state.accelerometer_motion_threshold); // g threshold to wake up: (THS * FS / 64) where FS is "full scale" of ±2g.
|
|
lis2dw_configure_6d_threshold(3); // 0-3 is 80, 70, 60, or 50 degrees. 50 is least precise, hopefully most sensitive?
|
|
|
|
// set up interrupts:
|
|
// INT1 is wired to pin A3. We'll configure the accelerometer to output an interrupt on INT1 when it detects an orientation change.
|
|
/// TODO: We had routed this interrupt to TC2 to count orientation changes, but TC2 consumed too much power.
|
|
/// Orientation changes helped with sleep tracking; would love to bring this back if we can find a low power solution.
|
|
/// For now, commenting these lines out; check commit 27f0c629d865f4bc56bc6e678da1eb8f4b919093 for power-hungry but working code.
|
|
// lis2dw_configure_int1(LIS2DW_CTRL4_INT1_6D);
|
|
// HAL_GPIO_A3_in();
|
|
|
|
// next: INT2 is wired to pin A4. We'll configure the accelerometer to output the sleep state on INT2.
|
|
// a falling edge on INT2 indicates the accelerometer has woken up.
|
|
lis2dw_configure_int2(LIS2DW_CTRL5_INT2_SLEEP_STATE | LIS2DW_CTRL5_INT2_SLEEP_CHG);
|
|
HAL_GPIO_A4_in();
|
|
|
|
// Wake on motion seemed like a good idea when the threshold was lower, but the UX makes less sense now.
|
|
// Still if you want to wake on motion, you can do it by uncommenting this line:
|
|
// watch_register_extwake_callback(HAL_GPIO_A4_pin(), cb_accelerometer_wake, false);
|
|
|
|
// later on, we are going to use INT1 for tap detection. We'll set up that interrupt here,
|
|
// but it will only fire once tap recognition is enabled.
|
|
watch_register_interrupt_callback(HAL_GPIO_A3_pin(), cb_accelerometer_event, INTERRUPT_TRIGGER_RISING);
|
|
|
|
// Enable the interrupts...
|
|
lis2dw_enable_interrupts();
|
|
|
|
// At first boot, this next line sets the accelerometer's sampling rate to 0, which is LIS2DW_DATA_RATE_POWERDOWN.
|
|
// This means the interrupts we just configured won't fire.
|
|
// Tap detection will ramp up sesing and make use of the A3 interrupt.
|
|
// If a watch face wants to check in on the A4 interrupt pin for motion status, it can call
|
|
// movement_set_accelerometer_background_rate with another rate like LIS2DW_DATA_RATE_LOWEST or LIS2DW_DATA_RATE_25_HZ.
|
|
lis2dw_set_data_rate(movement_state.accelerometer_background_rate);
|
|
}
|
|
#endif
|
|
|
|
movement_request_tick_frequency(1);
|
|
|
|
for(uint8_t i = 0; i < MOVEMENT_NUM_FACES; i++) {
|
|
watch_faces[i].setup(i, &watch_face_contexts[i]);
|
|
}
|
|
|
|
watch_faces[movement_state.current_face_idx].activate(watch_face_contexts[movement_state.current_face_idx]);
|
|
movement_volatile_state.pending_events |= 1 << EVENT_ACTIVATE;
|
|
}
|
|
}
|
|
|
|
#ifndef MOVEMENT_LOW_ENERGY_MODE_FORBIDDEN
|
|
|
|
static void _sleep_mode_app_loop(void) {
|
|
// as long as we are in low energy mode, we wake up here, update the screen, and go right back to sleep.
|
|
while (movement_volatile_state.is_sleeping) {
|
|
// if we need to wake immediately, do it!
|
|
if (movement_volatile_state.exit_sleep_mode) {
|
|
movement_volatile_state.exit_sleep_mode = false;
|
|
movement_volatile_state.is_sleeping = false;
|
|
|
|
return;
|
|
}
|
|
|
|
// we also have to handle top-of-the-minute tasks here in the mini-runloop
|
|
if (movement_volatile_state.minute_alarm_fired) {
|
|
movement_volatile_state.minute_alarm_fired = false;
|
|
_movement_renew_top_of_minute_alarm();
|
|
_movement_handle_top_of_minute();
|
|
}
|
|
|
|
movement_event_t event;
|
|
event.event_type = EVENT_LOW_ENERGY_UPDATE;
|
|
event.subsecond = 0;
|
|
watch_faces[movement_state.current_face_idx].loop(event, watch_face_contexts[movement_state.current_face_idx]);
|
|
|
|
// If any of the previous loops requested to wake up, do it!
|
|
if (movement_volatile_state.exit_sleep_mode) {
|
|
movement_volatile_state.exit_sleep_mode = false;
|
|
movement_volatile_state.is_sleeping = false;
|
|
|
|
return;
|
|
}
|
|
|
|
// If we have made changes to any of the RTC comp timers, schedule the next one in the queue
|
|
if (movement_volatile_state.schedule_next_comp) {
|
|
movement_volatile_state.schedule_next_comp = false;
|
|
watch_rtc_schedule_next_comp();
|
|
}
|
|
|
|
// otherwise enter sleep mode, until either the top of the minute interrupt or extwake wakes us up.
|
|
watch_enter_sleep_mode();
|
|
}
|
|
}
|
|
|
|
#endif
|
|
|
|
static bool _switch_face(void) {
|
|
const watch_face_t *wf = &watch_faces[movement_state.current_face_idx];
|
|
|
|
wf->resign(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);
|
|
|
|
if (movement_state.settings.bit.button_should_sound) {
|
|
// low note for nonzero case, high note for return to watch_face 0
|
|
movement_play_note(movement_state.next_face_idx ? BUZZER_NOTE_C7 : BUZZER_NOTE_C8, 50);
|
|
}
|
|
|
|
wf->activate(watch_face_contexts[movement_state.current_face_idx]);
|
|
|
|
movement_event_t event;
|
|
event.subsecond = 0;
|
|
event.event_type = EVENT_ACTIVATE;
|
|
movement_state.watch_face_changed = false;
|
|
bool can_sleep = wf->loop(event, watch_face_contexts[movement_state.current_face_idx]);
|
|
|
|
// Button events that follow a down event that happened on the previous face should not be forwarded to the new face
|
|
movement_volatile_state.passthrough_events = _movement_button_events_mask;
|
|
|
|
return can_sleep;
|
|
}
|
|
|
|
bool app_loop(void) {
|
|
const watch_face_t *wf = &watch_faces[movement_state.current_face_idx];
|
|
|
|
// default to being allowed to sleep by the face.
|
|
bool can_sleep = true;
|
|
|
|
// Any events that have been added by the various interrupts in between app_loop invokations
|
|
uint32_t pending_events = movement_volatile_state.pending_events;
|
|
movement_volatile_state.pending_events = 0;
|
|
|
|
movement_event_t event;
|
|
event.event_type = EVENT_NONE;
|
|
// Subsecond is determined by the TICK event, if concurrent events have happened,
|
|
// they will all have the same subsecond as they should to keep backward compatibility.
|
|
event.subsecond = movement_volatile_state.subsecond;
|
|
|
|
// if the LED should be off, turn it off
|
|
if (movement_volatile_state.turn_led_off) {
|
|
// unless the user is holding down the LIGHT button, in which case, give them more time.
|
|
if (movement_volatile_state.light_button.is_down) {
|
|
} else {
|
|
movement_volatile_state.turn_led_off = false;
|
|
movement_force_led_off();
|
|
}
|
|
}
|
|
|
|
// handle any button up/down events that occurred, e.g. schedule longpress timeouts, reset inactivity, etc.
|
|
_movement_handle_button_presses(pending_events);
|
|
|
|
// if we have a scheduled background task, handle that here:
|
|
if (
|
|
(pending_events & (1 << EVENT_TICK))
|
|
&& event.subsecond == 0
|
|
&& movement_state.has_scheduled_background_task
|
|
) {
|
|
_movement_handle_scheduled_tasks();
|
|
}
|
|
|
|
// Pop the EVENT_TIMEOUT out of the pending_events so it can be handled separately
|
|
bool resign_timeout = (pending_events & (1 << EVENT_TIMEOUT)) != 0;
|
|
if (resign_timeout) {
|
|
pending_events &= ~(1 << EVENT_TIMEOUT);
|
|
}
|
|
|
|
// Consume all the pending events
|
|
uint32_t passthrough_pending_events = pending_events & movement_volatile_state.passthrough_events;
|
|
pending_events = pending_events & ~movement_volatile_state.passthrough_events;
|
|
|
|
movement_event_type_t event_type = 0;
|
|
while (passthrough_pending_events) {
|
|
uint8_t next_event = __builtin_ctz(passthrough_pending_events);
|
|
event.event_type = event_type + next_event;
|
|
can_sleep = movement_default_loop_handler(event) && can_sleep;
|
|
passthrough_pending_events = passthrough_pending_events >> (next_event + 1);
|
|
event_type = event_type + next_event + 1;
|
|
}
|
|
|
|
event_type = 0;
|
|
while (pending_events) {
|
|
uint8_t next_event = __builtin_ctz(pending_events);
|
|
event.event_type = event_type + next_event;
|
|
can_sleep = wf->loop(event, watch_face_contexts[movement_state.current_face_idx]) && can_sleep;
|
|
pending_events = pending_events >> (next_event + 1);
|
|
event_type = event_type + next_event + 1;
|
|
}
|
|
|
|
// handle top-of-minute tasks, if the alarm handler told us we need to
|
|
if (movement_volatile_state.minute_alarm_fired) {
|
|
movement_volatile_state.minute_alarm_fired = false;
|
|
_movement_renew_top_of_minute_alarm();
|
|
_movement_handle_top_of_minute();
|
|
}
|
|
|
|
// Now handle the EVENT_TIMEOUT
|
|
if (resign_timeout && movement_state.current_face_idx != 0) {
|
|
event.event_type = EVENT_TIMEOUT;
|
|
can_sleep = wf->loop(event, watch_face_contexts[movement_state.current_face_idx]) && can_sleep;
|
|
}
|
|
|
|
// The watch_face_changed flag might be set again by the face loop, so check it again
|
|
if (movement_state.watch_face_changed) {
|
|
can_sleep = _switch_face() && can_sleep;
|
|
}
|
|
|
|
#ifndef MOVEMENT_LOW_ENERGY_MODE_FORBIDDEN
|
|
// if we have timed out of our low energy mode countdown, enter low energy mode.
|
|
if (movement_volatile_state.enter_sleep_mode && !movement_volatile_state.is_buzzing) {
|
|
movement_volatile_state.enter_sleep_mode = false;
|
|
movement_volatile_state.is_sleeping = true;
|
|
|
|
// No need to fire resign and sleep interrupts while in sleep mode
|
|
_movement_disable_inactivity_countdown();
|
|
|
|
watch_register_extwake_callback(HAL_GPIO_BTN_ALARM_pin(), cb_alarm_btn_extwake, true);
|
|
|
|
// _sleep_mode_app_loop takes over at this point and loops until exit_sleep_mode is set 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
|
|
|
|
// // 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();
|
|
|
|
// If we woke up to play a note sequence, actually play the note sequence we were asked to play while in deep sleep.
|
|
if (movement_volatile_state.has_pending_sequence) {
|
|
movement_volatile_state.has_pending_sequence = false;
|
|
watch_buzzer_play_sequence_with_volume(_pending_sequence, movement_request_sleep, _movement_get_buzzer_volume(movement_volatile_state.pending_sequence_priority));
|
|
// When this sequence is done playing, movement_request_sleep is invoked and the watch will go,
|
|
// back to sleep (unless the user interacts with it in the meantime)
|
|
_pending_sequence = NULL;
|
|
}
|
|
|
|
// don't let the watch sleep when exiting deep sleep mode,
|
|
// so that app_loop will run again and process the events that may have fired.
|
|
can_sleep = false;
|
|
}
|
|
#endif
|
|
|
|
// If we have made changes to any of the RTC comp timers, schedule the next one in the queue
|
|
if (movement_volatile_state.schedule_next_comp) {
|
|
movement_volatile_state.schedule_next_comp = false;
|
|
watch_rtc_schedule_next_comp();
|
|
}
|
|
|
|
#if __EMSCRIPTEN__
|
|
shell_task();
|
|
#else
|
|
// if we are plugged into USB, handle the serial shell
|
|
if (usb_is_enabled()) {
|
|
shell_task();
|
|
}
|
|
#endif
|
|
|
|
// if we are plugged into USB, we can't sleep because we need to keep the serial shell running.
|
|
if (usb_is_enabled()) {
|
|
yield();
|
|
can_sleep = false;
|
|
}
|
|
|
|
return can_sleep;
|
|
}
|
|
|
|
static movement_event_type_t _process_button_event(bool pin_level, movement_button_t* button) {
|
|
movement_event_type_t event_type = EVENT_NONE;
|
|
|
|
// This shouldn't happen normally
|
|
if (pin_level == button->is_down) {
|
|
return event_type;
|
|
}
|
|
|
|
uint32_t counter = watch_rtc_get_counter();
|
|
|
|
#if MOVEMENT_DEBOUNCE_TICKS
|
|
if (
|
|
(counter - button->up_timestamp) <= MOVEMENT_DEBOUNCE_TICKS &&
|
|
(counter - button->down_timestamp) <= MOVEMENT_DEBOUNCE_TICKS
|
|
) {
|
|
return event_type;
|
|
}
|
|
#endif
|
|
|
|
button->is_down = pin_level;
|
|
|
|
if (pin_level) {
|
|
button->down_timestamp = counter;
|
|
event_type = button->down_event;
|
|
} else {
|
|
#if MOVEMENT_DEBOUNCE_TICKS
|
|
button->up_timestamp = counter;
|
|
#endif
|
|
if ((counter - button->down_timestamp) >= MOVEMENT_LONG_PRESS_TICKS) {
|
|
event_type = button->down_event + 3;
|
|
} else {
|
|
event_type = button->down_event + 1;
|
|
}
|
|
}
|
|
|
|
return event_type;
|
|
}
|
|
|
|
void cb_light_btn_interrupt(void) {
|
|
bool pin_level = HAL_GPIO_BTN_LIGHT_read();
|
|
|
|
movement_volatile_state.pending_events |= 1 << _process_button_event(pin_level, &movement_volatile_state.light_button);
|
|
}
|
|
|
|
void cb_mode_btn_interrupt(void) {
|
|
bool pin_level = HAL_GPIO_BTN_MODE_read();
|
|
|
|
movement_volatile_state.pending_events |= 1 << _process_button_event(pin_level, &movement_volatile_state.mode_button);
|
|
}
|
|
|
|
void cb_alarm_btn_interrupt(void) {
|
|
bool pin_level = HAL_GPIO_BTN_ALARM_read();
|
|
|
|
movement_volatile_state.pending_events |= 1 << _process_button_event(pin_level, &movement_volatile_state.alarm_button);
|
|
}
|
|
|
|
static movement_event_type_t _process_button_longpress_timeout(movement_button_t* button) {
|
|
// Looks like all these checks are not needed for the longpress detection to work reliably.
|
|
// Keep the code around for now in case problems arise long-term.
|
|
|
|
// if (!button->is_down) {
|
|
// return EVENT_NONE;
|
|
// }
|
|
|
|
// movement_event_type_t up_event = button->down_event + 1;
|
|
|
|
// if (movement_volatile_state.pending_events & 1 << up_event) {
|
|
// return EVENT_NONE;
|
|
// }
|
|
|
|
// uint32_t counter = watch_rtc_get_counter();
|
|
// if ((counter - button->down_timestamp) < MOVEMENT_LONG_PRESS_TICKS) {
|
|
// return EVENT_NONE;
|
|
// }
|
|
|
|
movement_event_type_t longpress_event = button->down_event + 2;
|
|
|
|
return longpress_event;
|
|
}
|
|
|
|
void cb_light_btn_timeout_interrupt(void) {
|
|
movement_button_t* button = &movement_volatile_state.light_button;
|
|
|
|
movement_volatile_state.pending_events |= 1 << _process_button_longpress_timeout(button);
|
|
}
|
|
|
|
void cb_mode_btn_timeout_interrupt(void) {
|
|
movement_button_t* button = &movement_volatile_state.mode_button;
|
|
|
|
movement_volatile_state.pending_events |= 1 << _process_button_longpress_timeout(button);
|
|
}
|
|
|
|
void cb_alarm_btn_timeout_interrupt(void) {
|
|
movement_button_t* button = &movement_volatile_state.alarm_button;
|
|
|
|
movement_volatile_state.pending_events |= 1 << _process_button_longpress_timeout(button);
|
|
}
|
|
|
|
void cb_led_timeout_interrupt(void) {
|
|
movement_volatile_state.turn_led_off = true;
|
|
}
|
|
|
|
void cb_resign_timeout_interrupt(void) {
|
|
movement_volatile_state.pending_events |= 1 << EVENT_TIMEOUT;
|
|
}
|
|
|
|
void cb_sleep_timeout_interrupt(void) {
|
|
movement_request_sleep();
|
|
}
|
|
|
|
void cb_alarm_btn_extwake(void) {
|
|
// wake up!
|
|
movement_request_wake();
|
|
}
|
|
|
|
void cb_minute_alarm_fired(void) {
|
|
movement_volatile_state.minute_alarm_fired = true;
|
|
|
|
#if __EMSCRIPTEN__
|
|
_wake_up_simulator();
|
|
#endif
|
|
}
|
|
|
|
void cb_tick(void) {
|
|
rtc_counter_t counter = watch_rtc_get_counter();
|
|
uint32_t freq = watch_rtc_get_frequency();
|
|
uint32_t half_freq = freq >> 1;
|
|
uint32_t subsecond_mask = freq - 1;
|
|
movement_volatile_state.pending_events |= 1 << EVENT_TICK;
|
|
movement_volatile_state.subsecond = ((counter + half_freq) & subsecond_mask) >> movement_state.tick_pern;
|
|
}
|
|
|
|
void cb_accelerometer_event(void) {
|
|
uint8_t int_src = lis2dw_get_interrupt_source();
|
|
|
|
if (int_src & LIS2DW_REG_ALL_INT_SRC_DOUBLE_TAP) {
|
|
movement_volatile_state.pending_events |= 1 << EVENT_DOUBLE_TAP;
|
|
printf("Double tap!\n");
|
|
}
|
|
if (int_src & LIS2DW_REG_ALL_INT_SRC_SINGLE_TAP) {
|
|
movement_volatile_state.pending_events |= 1 << EVENT_SINGLE_TAP;
|
|
printf("Single tap!\n");
|
|
}
|
|
}
|
|
|
|
void cb_accelerometer_wake(void) {
|
|
movement_volatile_state.pending_events |= 1 << EVENT_ACCELEROMETER_WAKE;
|
|
// also: wake up!
|
|
_movement_reset_inactivity_countdown();
|
|
}
|