]> git.earman.xyz Git - sensor-watch.git/commitdiff
Make fast_stopwatch power efficiend by using the new counter32
authorAlessandro Genova <ales.genova@gmail.com>
Sun, 27 Jul 2025 02:31:53 +0000 (22:31 -0400)
committerAlessandro Genova <ales.genova@gmail.com>
Sat, 3 Jan 2026 01:07:56 +0000 (20:07 -0500)
watch-faces/complication/fast_stopwatch_face.c
watch-faces/complication/fast_stopwatch_face.h

index ce998102ad89782cbc7dc143c3727cace76e3192..a4af13f3d4700d6cb3fb76779e9f7aaacf7bc75a 100644 (file)
@@ -2,6 +2,7 @@
  * MIT License
  *
  * Copyright (c) 2022 Andreas Nebinger
+ * 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
        turns on on each button press or it doesn't.
 */
 
-#if __EMSCRIPTEN__
-#include <emscripten.h>
-#include <emscripten/html5.h>
-#else
-#include "tc.h"
-#endif
-
-// distant future for background task: January 1, 2083
-static const watch_date_time_t distant_future = {
-    .unit = {0, 0, 0, 1, 1, 63}
-};
-
-static uint32_t _ticks;
-static uint32_t _lap_ticks;
-static uint8_t _blink_ticks;
-static uint32_t _old_seconds;
-static uint8_t _old_minutes;
-static uint8_t _hours;
-static bool _colon;
-static bool _is_running;
-
-#if __EMSCRIPTEN__
-
-static long _em_interval_id = 0;
-
-void em_cb_handler(void *userData) {
-    // interrupt handler for emscripten 128 Hz callbacks
-    (void) userData;
-    _ticks++;
-}
-
-static void _cb_initialize() { }
-
-static inline void _cb_stop() {
-    emscripten_clear_interval(_em_interval_id);
-    _em_interval_id = 0;
-    _is_running = false;
-}
-
-static inline void _cb_start() {
-    // initiate 128 hz callback
-    _em_interval_id = emscripten_set_interval(em_cb_handler, (double)(1000/128), (void *)NULL);
-}
-
-#else
-
-static inline void _cb_start() {
-    // start the TC1 timer
-    tc_enable(1);
-    _is_running = true;
-}
-
-static inline void _cb_stop() {
-    // stop the TC1 timer
-    tc_disable(1);
-    _is_running = false;
-}
-
-static void _cb_initialize() {
-    tc_init(1, GENERIC_CLOCK_3, TC_PRESCALER_DIV4);
-    tc_set_counter_mode(1, TC_COUNTER_MODE_8BIT);
-    tc_set_run_in_standby(1, true);
-    _cb_stop();
-    tc_count8_set_period(1, 1); // 1024 Hz divided by 4 divided by 2 results in a 128 Hz interrupt
-    /// FIXME: #SecondMovement, we need a gossamer wrapper for interrupts.
-    TC1->COUNT8.INTENSET.bit.OVF = 1;
-    NVIC_ClearPendingIRQ(TC1_IRQn);
-    NVIC_EnableIRQ (TC1_IRQn);
-}
-
-void irq_handler_tc1(void);
-void irq_handler_tc1(void) {
-    // interrupt handler for TC1 (globally!)
-    _ticks++;
-    TC1->COUNT8.INTFLAG.reg |= TC_INTFLAG_OVF;
-}
-
-#endif
+// Loosely implement the watch as a state machine
+typedef enum {
+    SW_STATUS_IDLE = 0,
+    SW_STATUS_RUNNING,
+    SW_STATUS_RUNNING_LAPPING,
+    SW_STATUS_STOPPED,
+    SW_STATUS_STOPPED_LAPPING
+} stopwatch_status_t;
 
 static inline void _button_beep() {
     // play a beep as confirmation for a button press (if applicable)
     if (movement_button_should_sound()) watch_buzzer_play_note_with_volume(BUZZER_NOTE_C7, 50, movement_button_volume());
 }
 
+// How quickly should the elapsing time be displayed?
+// This is just for looks, timekeeping is always accurate to 128Hz
+static const uint8_t DISPLAY_RUNNING_RATE = 32;
+
+static const uint32_t TWENTY_FOUR_HOURS = 24 * 60 * 60 * 128;
+
 /// @brief Display minutes, seconds and fractions derived from 128 Hz tick counter
 ///        on the lcd.
 /// @param ticks
-static void _display_ticks(uint32_t ticks) {
+static void _display_elapsed(uint32_t ticks) {
+    ticks = ticks % TWENTY_FOUR_HOURS;
     char buf[14];
     uint8_t sec_100 = (ticks & 0x7F) * 100 / 128;
     uint32_t seconds = ticks >> 7;
     uint32_t minutes = seconds / 60;
-    if (_hours) {
-        sprintf(buf, "%2u", _hours);
+    uint32_t hours = minutes / 60;
+    if (hours) {
+        sprintf(buf, "%2u", hours);
         watch_display_text(WATCH_POSITION_TOP_RIGHT, buf);
     } else {
         watch_display_text(WATCH_POSITION_TOP_RIGHT, "  ");
     }
 
-    sprintf(buf, "%02lu%02lu%02u", minutes, (seconds % 60), sec_100);
+    sprintf(buf, "%02lu%02lu%02u", (minutes % 60), (seconds % 60), sec_100);
     watch_display_text(WATCH_POSITION_BOTTOM, buf);
 }
 
-/// @brief Displays the current stopwatch time on the LCD (more optimized than _display_ticks())
-static void _draw() {
-    if (_lap_ticks == 0) {
-        char buf[14];
-        uint8_t sec_100 = (_ticks & 0x7F) * 100 / 128;
-        if (_is_running) {
-            uint32_t seconds = _ticks >> 7;
-            if (seconds != _old_seconds) {
-                // seconds have changed
-                _old_seconds = seconds;
-                uint8_t minutes = seconds / 60;
-                seconds %= 60;
-                if (minutes != _old_minutes) {
-                    // minutes have changed, draw everything
-                    _old_minutes = minutes;
-                    minutes %= 60;
-                    if (_hours) {
-                        // with hour indicator
-                        sprintf(buf, "%2u", _hours);
-                        watch_display_text(WATCH_POSITION_TOP_RIGHT, buf);
-                    } else {
-                        // no hour indicator
-                        watch_display_text(WATCH_POSITION_TOP_RIGHT, "  ");
-                    }
-                    sprintf(buf, "%02u%02lu%02u", minutes, seconds, sec_100);
-                    watch_display_text(WATCH_POSITION_BOTTOM, buf);
-                } else {
-                    // just draw seconds
-                    sprintf(buf, "%02lu", seconds);
-                    // note that we're drawing the seconds in the "minutes" position, since this
-                    // watch face uses the "seconds" position for hundredths of seconds
-                    watch_display_text(WATCH_POSITION_MINUTES, buf);
-                    watch_display_character_lp_seconds('0' + sec_100 / 10, 8);
-                    watch_display_character_lp_seconds('0' + sec_100 % 10, 9);
-                }
+static void _draw_indicators(fast_stopwatch_state_t *state, movement_event_t event, uint32_t elapsed) {
+    uint8_t subsecond;
+    bool tock;
+
+    switch (state->status) {
+        case SW_STATUS_RUNNING:
+            subsecond = elapsed & 127;
+            tock = subsecond >= 64;
+
+            watch_clear_indicator(WATCH_INDICATOR_LAP);
+            if (tock) {
+                watch_clear_colon();
             } else {
-                // only draw 100ths of seconds
-                watch_display_character_lp_seconds('0' + sec_100 / 10, 8);
-                watch_display_character_lp_seconds('0' + sec_100 % 10, 9);
+                watch_set_colon();
             }
-        } else {
-            _display_ticks(_ticks);
-        }
+
+            return;
+
+        case SW_STATUS_RUNNING_LAPPING:
+            tock = event.subsecond > 0;
+
+            if (tock) {
+                watch_clear_indicator(WATCH_INDICATOR_LAP);
+                watch_clear_colon();
+            } else {
+                watch_set_indicator(WATCH_INDICATOR_LAP);
+                watch_set_colon();
+            }
+
+            return;
+
+        case SW_STATUS_STOPPED_LAPPING:
+            watch_set_indicator(WATCH_INDICATOR_LAP);
+            watch_set_colon();
+
+            return;
+
+        case SW_STATUS_STOPPED:
+        case SW_STATUS_IDLE:
+        default:
+            watch_clear_indicator(WATCH_INDICATOR_LAP);
+            watch_set_colon();
+            return;
     }
-    if (_is_running) {
-        // blink the colon every half second
-        uint8_t blink_ticks = ((_ticks >> 6) & 1);
-        if (blink_ticks != _blink_ticks) {
-            _blink_ticks = blink_ticks;
-            _colon = !_colon;
-            if (_colon) watch_set_colon();
-            else watch_clear_colon();
-        }
+}
+
+static uint8_t get_refresh_rate(fast_stopwatch_state_t *state) {
+    switch (state->status) {
+        case SW_STATUS_RUNNING:
+            return DISPLAY_RUNNING_RATE;
+        case SW_STATUS_RUNNING_LAPPING:
+            return 2;
+        case SW_STATUS_STOPPED:
+        case SW_STATUS_IDLE:
+        default:
+            return 1;
     }
 }
 
-static inline void _update_lap_indicator() {
-    if (_lap_ticks) watch_set_indicator(WATCH_INDICATOR_LAP);
-    else  watch_clear_indicator(WATCH_INDICATOR_LAP);
+static void state_transition(fast_stopwatch_state_t *state, rtc_counter_t counter, movement_event_type_t event_type) {
+    switch (state->status) {
+        case SW_STATUS_IDLE:
+            switch (event_type) {
+                case EVENT_ALARM_BUTTON_DOWN:
+                    state->status = SW_STATUS_RUNNING;
+                    state->start_counter = counter;
+                    movement_request_tick_frequency(DISPLAY_RUNNING_RATE);
+                    return;
+                default:
+                    return;
+            }
+
+        case SW_STATUS_RUNNING:
+            switch (event_type) {
+                case EVENT_ALARM_BUTTON_DOWN:
+                    state->status = SW_STATUS_STOPPED;
+                    state->stop_counter = counter;
+                    movement_request_tick_frequency(1);
+                    return;
+                case EVENT_LIGHT_BUTTON_DOWN:
+                    state->status = SW_STATUS_RUNNING_LAPPING;
+                    state->lap_counter = counter;
+                    movement_request_tick_frequency(2);
+                    return;
+                default:
+                    return;
+            }
+
+        case SW_STATUS_RUNNING_LAPPING:
+            switch (event_type) {
+                case EVENT_ALARM_BUTTON_DOWN:
+                    state->status = SW_STATUS_STOPPED_LAPPING;
+                    state->stop_counter = counter;
+                    movement_request_tick_frequency(1);
+                    return;
+                case EVENT_LIGHT_BUTTON_DOWN:
+                    state->status = SW_STATUS_RUNNING;
+                    state->lap_counter = counter;
+                    movement_request_tick_frequency(DISPLAY_RUNNING_RATE);
+                    return;
+                default:
+                    return;
+            }
+
+        case SW_STATUS_STOPPED_LAPPING:
+            switch (event_type) {
+                case EVENT_ALARM_BUTTON_DOWN:
+                    state->status = SW_STATUS_RUNNING_LAPPING;
+                    state->start_counter = counter - state->stop_counter + state->start_counter;
+                    state->lap_counter = counter - state->stop_counter + state->lap_counter;
+                    movement_request_tick_frequency(2);
+                    return;
+                case EVENT_LIGHT_BUTTON_DOWN:
+                    state->status = SW_STATUS_STOPPED;
+                    return;
+                default:
+                    return;
+            }
+
+        case SW_STATUS_STOPPED:
+            switch (event_type) {
+                case EVENT_ALARM_BUTTON_DOWN:
+                    state->status = SW_STATUS_RUNNING;
+                    state->start_counter = counter - state->stop_counter + state->start_counter;
+                    movement_request_tick_frequency(DISPLAY_RUNNING_RATE);
+                    return;
+                case EVENT_LIGHT_BUTTON_DOWN:
+                    state->status = SW_STATUS_IDLE;
+                    return;
+                default:
+                    return;
+            }
+
+        default:
+            return;
+    }
 }
 
-static inline void _set_colon() {
-    watch_set_colon();
-    _colon = true;
+static uint32_t elapsed_time(fast_stopwatch_state_t *state, rtc_counter_t counter) {
+    switch (state->status) {
+        case SW_STATUS_IDLE:
+            return 0;
+
+        case SW_STATUS_RUNNING:
+            return counter - state->start_counter;
+
+        case SW_STATUS_RUNNING_LAPPING:
+        case SW_STATUS_STOPPED_LAPPING:
+            return state->lap_counter - state->start_counter;
+
+        case SW_STATUS_STOPPED:
+            return state->stop_counter - state->start_counter;
+
+        default:
+            return 0;
+    }
 }
 
 void fast_stopwatch_face_setup(uint8_t watch_face_index, void ** context_ptr) {
@@ -215,114 +246,49 @@ void fast_stopwatch_face_setup(uint8_t watch_face_index, void ** context_ptr) {
         *context_ptr = malloc(sizeof(fast_stopwatch_state_t));
         memset(*context_ptr, 0, sizeof(fast_stopwatch_state_t));
         fast_stopwatch_state_t *state = (fast_stopwatch_state_t *)*context_ptr;
-        _ticks = _lap_ticks = _blink_ticks = _old_minutes = _old_seconds = _hours = 0;
-    _is_running = _colon = false;
-        state->light_on_button = true;
-    }
-    if (!_is_running) {
-        // prepare the 128 Hz callback source
-        _cb_initialize();
+        state->start_counter = 0;
+        state->stop_counter = 0;
+        state->lap_counter = 0;
+        state->status = SW_STATUS_IDLE;
     }
 }
 
 void fast_stopwatch_face_activate(void *context) {
-    (void) context;
-    if (_is_running) {
-        // The background task will keep the watch from entering low energy mode while the stopwatch is on screen.
-        movement_schedule_background_task(distant_future);
-    }
+    fast_stopwatch_state_t *state = (fast_stopwatch_state_t *) context;
+    movement_request_tick_frequency(get_refresh_rate(state));
 }
 
 bool fast_stopwatch_face_loop(movement_event_t event, void *context) {
     fast_stopwatch_state_t *state = (fast_stopwatch_state_t *)context;
 
-    // handle overflow of fast ticks
-    while (_ticks >= (128 * 60 * 60)) {
-        _ticks -= (128 * 60 * 60);
-        _hours++;
-        if (_hours >= 24) _hours -= 24;
-        // initiate a re-draw
-        _old_minutes = 59;
-    }
+    rtc_counter_t counter = watch_rtc_get_counter();
+
+    state_transition(state, counter, event.event_type);
+    rtc_counter_t elapsed = elapsed_time(state, counter);
 
     switch (event.event_type) {
         case EVENT_ACTIVATE:
-            _set_colon();
             watch_display_text_with_fallback(WATCH_POSITION_TOP_LEFT, "STW", "ST");
-            _update_lap_indicator();
-            if (_is_running) movement_request_tick_frequency(16);
-            _display_ticks(_lap_ticks ? _lap_ticks : _ticks);
-            break;
-        case EVENT_TICK:
-            _draw();
-            break;
-        case EVENT_LIGHT_LONG_PRESS:
-            // kind od hidden feature: long press toggles light on or off
-            state->light_on_button = !state->light_on_button;
-            if (state->light_on_button) movement_illuminate_led();
-            else watch_set_led_off();
+            _draw_indicators(state, event, elapsed);
+            _display_elapsed(elapsed);
             break;
         case EVENT_ALARM_BUTTON_DOWN:
-            _is_running = !_is_running;
-            if (_is_running) {
-                // start or continue stopwatch
-                movement_request_tick_frequency(16);
-                // register 128 hz callback for time measuring
-                _cb_start();
-                // schedule the keepalive task when running
-                movement_schedule_background_task(distant_future);
-            } else {
-                // stop the stopwatch
-                _cb_stop();
-                movement_request_tick_frequency(1);
-                _set_colon();
-                // cancel the keepalive task
-                movement_cancel_background_task();
-            }
-            _draw();
-            _button_beep();
-            break;
         case EVENT_LIGHT_BUTTON_DOWN:
-            if (state->light_on_button) movement_illuminate_led();
-            if (_is_running) {
-                if (_lap_ticks) {
-                    // clear lap and continue running
-                    _lap_ticks = 0;
-                    movement_request_tick_frequency(16);
-                } else {
-                    // set lap ticks and stop updating the display
-                    _lap_ticks = _ticks;
-                    movement_request_tick_frequency(2);
-                    _set_colon();
-                }
-            } else {
-                if (_lap_ticks) {
-                    // clear lap and show running stopwatch
-                    _lap_ticks = 0;
-                } else if (_ticks) {
-                    // reset stopwatch
-                    _ticks = _lap_ticks = _blink_ticks = _old_minutes = _old_seconds = _hours = 0;
-                    _button_beep();
-                }
-            }
-            _display_ticks(_ticks);
-            _update_lap_indicator();
-            break;
-        case EVENT_TIMEOUT:
-            if (!_is_running) movement_move_to_face(0);
-            break;
-        case EVENT_LOW_ENERGY_UPDATE:
-            _draw();
+            _button_beep();
+            // Fall into the case below;
+        case EVENT_TICK:
+            _draw_indicators(state, event, elapsed);
+            _display_elapsed(elapsed);
             break;
         default:
             movement_default_loop_handler(event);
             break;
     }
+
     return true;
 }
 
 void fast_stopwatch_face_resign(void *context) {
     (void) context;
-    // cancel the keepalive task
-    movement_cancel_background_task();
+    movement_request_tick_frequency(1);
 }
index eac566517ad5144fa85184bde655024bf02c5f6d..d99f60853bd43155f2170f4b644075c275914ce7 100644 (file)
 #include "movement.h"
 
 typedef struct {
-    bool light_on_button;   // determines whether the light button actually triggers the led
+    rtc_counter_t start_counter; // rtc counter when the stopwatch was started
+    rtc_counter_t lap_counter;   // rtc counter when the stopwatch was lapped
+    rtc_counter_t stop_counter;  // rtc counter when the stopwatch was stopped
+    uint8_t status;              // the status the stopwatch is in (idle, running, stopped)
 } fast_stopwatch_state_t;
 
 void fast_stopwatch_face_setup(uint8_t watch_face_index, void ** context_ptr);
@@ -63,12 +66,6 @@ void fast_stopwatch_face_activate(void *context);
 bool fast_stopwatch_face_loop(movement_event_t event, void *context);
 void fast_stopwatch_face_resign(void *context);
 
-#if __EMSCRIPTEN__
-void em_cb_handler(void *userData);
-#else
-void TC2_Handler(void);
-#endif
-
 #define fast_stopwatch_face ((const watch_face_t){ \
     fast_stopwatch_face_setup, \
     fast_stopwatch_face_activate, \