/* * 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 #include #include #include #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 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; volatile bool has_pending_accelerometer; // 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, 3, BUZZER_NOTE_REST, 4, BUZZER_NOTE_C8, 3, BUZZER_NOTE_REST, 4, BUZZER_NOTE_C8, 3, BUZZER_NOTE_REST, 4, BUZZER_NOTE_C8, 5, BUZZER_NOTE_REST, 38, -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 uint32_t _movement_get_accelerometer_events() { uint32_t accelerometer_events = 0; uint8_t int_src = lis2dw_get_interrupt_source(); if (int_src & LIS2DW_REG_ALL_INT_SRC_DOUBLE_TAP) { accelerometer_events |= 1 << EVENT_DOUBLE_TAP; printf("Double tap!\r\n"); } if (int_src & LIS2DW_REG_ALL_INT_SRC_SINGLE_TAP) { accelerometer_events |= 1 << EVENT_SINGLE_TAP; printf("Single tap!\r\n"); } return accelerometer_events; } 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.has_pending_accelerometer = 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(); } } if (movement_volatile_state.has_pending_accelerometer) { movement_volatile_state.has_pending_accelerometer = false; pending_events |= _movement_get_accelerometer_events(); } // 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(bool pin_level, movement_button_t* button) { if (!button->is_down) { return EVENT_NONE; } if (pin_level) { return button->down_event + 2; // event_longpress } else { // hypotetical corner case: if the timeout fired but the pin level is actually up, we may have missed/rejected the up event, so fire it here #if MOVEMENT_DEBOUNCE_TICKS // we're in a corner case, we don't know when the up actually happened. button->up_timestamp = button->down_timestamp; #endif button->is_down = false; return button->down_event + 1; // event_up } } void cb_light_btn_timeout_interrupt(void) { bool pin_level = HAL_GPIO_BTN_LIGHT_read(); movement_button_t* button = &movement_volatile_state.light_button; movement_volatile_state.pending_events |= 1 << _process_button_longpress_timeout(pin_level, button); } void cb_mode_btn_timeout_interrupt(void) { bool pin_level = HAL_GPIO_BTN_MODE_read(); movement_button_t* button = &movement_volatile_state.mode_button; movement_volatile_state.pending_events |= 1 << _process_button_longpress_timeout(pin_level, button); } void cb_alarm_btn_timeout_interrupt(void) { bool pin_level = HAL_GPIO_BTN_ALARM_read(); movement_button_t* button = &movement_volatile_state.alarm_button; movement_volatile_state.pending_events |= 1 << _process_button_longpress_timeout(pin_level, 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) { movement_volatile_state.has_pending_accelerometer = true; } void cb_accelerometer_wake(void) { movement_volatile_state.pending_events |= 1 << EVENT_ACCELEROMETER_WAKE; // also: wake up! _movement_reset_inactivity_countdown(); }