From: Lorenzo Prosseda Date: Sun, 3 Aug 2025 17:22:18 +0000 (+0000) Subject: Port the interval_face complication to Second Movement (#66) X-Git-Url: https://git.earman.xyz/?a=commitdiff_plain;h=4eee544762c661d6b43aed34b07cead6e1010dc6;p=sensor-watch.git Port the interval_face complication to Second Movement (#66) * Port the interval_face complication to Second Movement - Compile inside Second Movement - Support custom display * Refactor display buffer name, enlarge index buffer Also removed now unused _blink_idx array * Fix Clear setting not showing, and its formatting * Rename work interval label to make it unique * Skip empty interval timers while cycling through --- diff --git a/legacy/watch_faces/complication/interval_face.c b/legacy/watch_faces/complication/interval_face.c deleted file mode 100644 index c354e72..0000000 --- a/legacy/watch_faces/complication/interval_face.c +++ /dev/null @@ -1,622 +0,0 @@ -/* - * MIT License - * - * Copyright (c) 2022 Andreas Nebinger - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - */ - -#include -#include - -#include "interval_face.h" -#include "watch.h" -#include "watch_utility.h" -#include "watch_private_display.h" -#include "watch_buzzer.h" - -typedef enum { - interval_setting_0_timer_idx, - interval_setting_1_clear_yn, - interval_setting_2_warmup_minutes, - interval_setting_3_warmup_seconds, - interval_setting_4_work_minutes, - interval_setting_5_work_seconds, - interval_setting_6_work_rounds, - interval_setting_7_break_minutes, - interval_setting_8_break_seconds, - interval_setting_9_full_rounds, - interval_setting_10_cooldown_minutes, - interval_setting_11_cooldown_seconds, - interval_setting_max -} interval_setting_idx_t; - -#define INTERVAL_FACE_STATE_DEFAULT "IT" // Interval Timer -#define INTERVAL_FACE_STATE_WARMUP "PR" // PRepare / warm up -#define INTERVAL_FACE_STATE_WORK "WO" // WOrk -#define INTERVAL_FACE_STATE_BREAK "BR" // BReak -#define INTERVAL_FACE_STATE_COOLDOWN "CD" // CoolDown - -// Define some default timer settings. Each timer is described in an array like this: -// 1. warm-up seconds, -// 2. work time (seconds/minutes) -// 3. break time (seconds/minutes) -// 4. full rounds (0 = no limit) -// 5. cooldown seconds -// Work time and break time: positive number = seconds, negative number = minutes -static const int8_t _default_timers[6][5] = {{0, 40, 20, 0, 0}, - {0, 45, 15, 0, 0}, - {10, 20, 10, 8, 10}, - {0, 35, 0, 0, 0}, - {0, -25, -5, 0, 0}, - {0, -20, -5, 0, 0}}; - -static const uint8_t _intro_segdata[4][2] = {{1, 8}, {0, 8}, {0, 7}, {1, 7}}; -static const uint8_t _blink_idx[] = {3, 9, 4, 6, 4, 6, 8, 4, 6, 8, 4, 6}; -static const uint8_t _setting_page_idx[] = {1, 0, 1, 1, 2, 2, 2, 3, 3, 3, 4, 4}; -static const int8_t _sound_seq_warmup[] = {BUZZER_NOTE_F6, 8, BUZZER_NOTE_REST, 1, -2, 3, 0}; -static const int8_t _sound_seq_work[] = {BUZZER_NOTE_F6, 8, BUZZER_NOTE_REST, 1, -2, 2, BUZZER_NOTE_C7, 24, 0}; -static const int8_t _sound_seq_break[] = {BUZZER_NOTE_B6, 15, BUZZER_NOTE_REST, 1, -2, 1, BUZZER_NOTE_B6, 16, 0}; -static const int8_t _sound_seq_cooldown[] = {BUZZER_NOTE_C7, 15, BUZZER_NOTE_REST, 1, -2, 1, BUZZER_NOTE_C7, 24, 0}; -static const int8_t _sound_seq_finish[] = {BUZZER_NOTE_C7, 6, BUZZER_NOTE_E7, 6, BUZZER_NOTE_G7, 6, BUZZER_NOTE_C8, 18, 0}; - -static interval_setting_idx_t _setting_idx; -static int8_t _ticks; -static bool _erase_timer_flag; -static uint32_t _target_ts; -static uint32_t _now_ts; -static uint32_t _paused_ts; -static uint8_t _timer_work_round; -static uint8_t _timer_full_round; -static uint8_t _timer_run_state; - -static inline void _inc_uint8(uint8_t *value, uint8_t step, uint8_t max) { - *value += step; - if (*value >= max) *value = 0; -} - -static uint32_t _get_now_ts() { - // returns the current date time as unix timestamp - watch_date_time_t now = watch_rtc_get_date_time(); - return watch_utility_date_time_to_unix_time(now, 0); -} - -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(BUZZER_NOTE_C7, 50); -} - -static void _timer_write_info(interval_face_state_t *state, char *buf, uint8_t timer_page) { - // fill display string with requested timer information - switch (timer_page) { - case 0: - // clear timer? - sprintf(buf, "%2s %1dCLEARn", INTERVAL_FACE_STATE_DEFAULT, state->timer_idx + 1); - if (_erase_timer_flag) buf[9] = 'y'; - watch_clear_colon(); - break; - case 1: - // warmup time info - sprintf(buf, "%2s %1d%02d%02d ", INTERVAL_FACE_STATE_WARMUP, state->timer_idx + 1, - state->timer[state->timer_idx].warmup_minutes, - state->timer[state->timer_idx].warmup_seconds); - break; - case 2: - // work interval info - sprintf(buf, "%2s %1d%02d%02d%2d", INTERVAL_FACE_STATE_WORK, state->timer_idx + 1, - state->timer[state->timer_idx].work_minutes, - state->timer[state->timer_idx].work_seconds, - state->timer[state->timer_idx].work_rounds); - break; - case 3: - // break interval info - sprintf(buf, "%2s %1d%02d%02d%2d", INTERVAL_FACE_STATE_BREAK, state->timer_idx + 1, - state->timer[state->timer_idx].break_minutes, - state->timer[state->timer_idx].break_seconds, - state->timer[state->timer_idx].full_rounds); - if (!state->timer[state->timer_idx].full_rounds) buf[9] = '-'; - break; - case 4: - // cooldown time info - sprintf(buf, "%2s %1d%02d%02d ", INTERVAL_FACE_STATE_COOLDOWN ,state->timer_idx + 1, - state->timer[state->timer_idx].cooldown_minutes, - state->timer[state->timer_idx].cooldown_seconds); - break; - default: - break; - } -} - -static void _face_draw(interval_face_state_t *state, uint8_t subsecond) { - // draws current face state - if (!state->is_active) return; - char buf[14]; - buf[0] = 0; - uint8_t tmp; - if (state->face_state == interval_state_waiting && _ticks >= 0) { - // play info slideshow for current timer - int8_t ticks = _ticks % 12; - if (ticks == 0) { - if ((state->timer[state->timer_idx].warmup_minutes + state->timer[state->timer_idx].warmup_seconds) == 0) { - // skip warmup info if there is none for this timer - ticks = 3; - _ticks += 3; - } - } - tmp = ticks / 3 + 1; - _timer_write_info(state, buf, tmp); - // don't show '1 round' when displaying workout time to avoid detail overload - if (tmp == 2 && state->timer[state->timer_idx].work_rounds == 1) buf[9] = ' '; - // blink colon - if (subsecond % 2 == 0 && _ticks < 24) watch_clear_colon(); - else watch_set_colon(); - } else if (state->face_state == interval_state_setting) { - if (_setting_idx == interval_setting_0_timer_idx) { - if ((state->timer[state->timer_idx].warmup_minutes + state->timer[state->timer_idx].warmup_seconds) == 0) - tmp = 1; - else - tmp = 2; - } else { - tmp = _setting_page_idx[_setting_idx]; - } - _timer_write_info(state, buf, tmp); - // blink at cursor position - if (subsecond % 2 && _ticks != -2) { - buf[_blink_idx[_setting_idx]] = ' '; - if (_blink_idx[_setting_idx] % 2 == 0) buf[_blink_idx[_setting_idx] + 1] = ' '; - } - // show lap indicator only when rounds are set - if (_setting_idx == interval_setting_6_work_rounds || _setting_idx == interval_setting_9_full_rounds) - watch_set_indicator(WATCH_INDICATOR_LAP); - else - watch_clear_indicator(WATCH_INDICATOR_LAP); - } else if (state->face_state == interval_state_running || state->face_state == interval_state_pausing) { - tmp = _timer_full_round; - switch (_timer_run_state) { - case 0: - sprintf(buf, INTERVAL_FACE_STATE_WARMUP); - break; - case 1: - sprintf(buf, INTERVAL_FACE_STATE_WORK); - if (state->timer[state->timer_idx].work_rounds > 1) tmp = _timer_work_round; - break; - case 2: - sprintf(buf, INTERVAL_FACE_STATE_BREAK); - break; - case 3: - sprintf(buf, INTERVAL_FACE_STATE_COOLDOWN); - break; - default: - break; - } - div_t delta; - - if (state->face_state == interval_state_pausing) { - // pausing - delta = div(_target_ts - _paused_ts, 60); - // blink the bell icon - if (_now_ts % 2) watch_set_indicator(WATCH_INDICATOR_BELL); - else watch_clear_indicator(WATCH_INDICATOR_BELL); - } else - // running - delta = div(_target_ts - _now_ts, 60); - sprintf(&buf[2], " %1d%02d%02d%2d", state->timer_idx + 1, delta.quot, delta.rem, tmp + 1); - } - // write out to lcd - if (buf[0]) { - watch_display_character(buf[0], 0); - watch_display_character(buf[1], 1); - // set the bar for the i-like symbol on position 2 - watch_set_pixel(2, 9); - // display the rest of the string - watch_display_string(&buf[3], 3); - } -} - -static void _initiate_setting(interval_face_state_t *state, uint8_t subsecond) { - state->face_state = interval_state_setting; - _setting_idx = interval_setting_0_timer_idx; - _ticks = 0; - _erase_timer_flag = false; - watch_set_colon(); - movement_request_tick_frequency(4); - _face_draw(state, subsecond); -} - -static void _resume_setting(interval_face_state_t *state, uint8_t subsecond) { - state->face_state = interval_state_waiting; - _ticks = 0; - _face_draw(state, subsecond); - movement_request_tick_frequency(2); - watch_clear_indicator(WATCH_INDICATOR_LAP); -} - -static void _abort_quick_ticks() { - if (_ticks == -2) { - _ticks = -1; - movement_request_tick_frequency(4); - } -} - -static void _handle_alarm_button(interval_face_state_t *state) { - // handles the alarm button press and alters the corresponding timer settings - switch (_setting_idx) { - case interval_setting_0_timer_idx: - _inc_uint8(&state->timer_idx, 1, INTERVAL_TIMERS); - _erase_timer_flag = false; - break; - case interval_setting_1_clear_yn: - _erase_timer_flag ^= 1; - break; - case interval_setting_2_warmup_minutes: - _inc_uint8(&state->timer[state->timer_idx].warmup_minutes, 1, 60); - break; - case interval_setting_3_warmup_seconds: - _inc_uint8(&state->timer[state->timer_idx].warmup_seconds, 5, 60); - break; - case interval_setting_4_work_minutes: - _inc_uint8(&state->timer[state->timer_idx].work_minutes, 1, 60); - if (state->timer[state->timer_idx].work_rounds == 0) state->timer[state->timer_idx].work_rounds = 1; - break; - case interval_setting_5_work_seconds: - _inc_uint8(&state->timer[state->timer_idx].work_seconds, 5, 60); - if (state->timer[state->timer_idx].work_rounds == 0) state->timer[state->timer_idx].work_rounds = 1; - break; - case interval_setting_6_work_rounds: - _inc_uint8(&state->timer[state->timer_idx].work_rounds, 1, 100); - break; - case interval_setting_7_break_minutes: - _inc_uint8(&state->timer[state->timer_idx].break_minutes, 1, 60); - break; - case interval_setting_8_break_seconds: - _inc_uint8(&state->timer[state->timer_idx].break_seconds, 5, 60); - break; - case interval_setting_9_full_rounds: - _inc_uint8(&state->timer[state->timer_idx].full_rounds, 1, 100); - break; - case interval_setting_10_cooldown_minutes: - _inc_uint8(&state->timer[state->timer_idx].cooldown_minutes, 1, 60); - break; - case interval_setting_11_cooldown_seconds: - _inc_uint8(&state->timer[state->timer_idx].cooldown_seconds, 5, 60); - break; - default: - break; - } -} - -static void _set_next_timestamp(interval_face_state_t *state) { - // set next timestamp for the running timer, set background task and pay sound sequence - uint16_t delta = 0; - int8_t *sound_seq; - interval_timer_setting_t timer = state->timer[state->timer_idx]; - switch (_timer_run_state) { - case 0: - delta = timer.warmup_minutes * 60 + timer.warmup_seconds; - sound_seq = (int8_t *)_sound_seq_warmup; - break; - case 1: - delta = timer.work_minutes * 60 + timer.work_seconds; - sound_seq = (int8_t *)_sound_seq_work; - break; - case 2: - delta = timer.break_minutes * 60 + timer.break_seconds; - sound_seq = (int8_t *)_sound_seq_break; - break; - case 3: - delta = timer.cooldown_minutes * 60 + timer.cooldown_seconds; - sound_seq = (int8_t *)_sound_seq_cooldown; - break; - default: - sound_seq = NULL; - break; - } - // failsafe - if (delta <= 0) delta = 1; - _target_ts += delta; - // schedule next background task - watch_date_time_t target_dt = watch_utility_date_time_from_unix_time(_target_ts, 0); - movement_schedule_background_task_for_face(state->face_idx, target_dt); - // play sound - watch_buzzer_play_sequence(sound_seq, NULL); -} - -static inline bool _is_timer_empty(interval_timer_setting_t *timer) { - // checks if a timer is empty - return (timer->warmup_minutes + timer->warmup_seconds - + timer->work_minutes + timer->work_seconds - + timer->break_minutes + timer->break_seconds - + timer->cooldown_minutes + timer->cooldown_seconds == 0); -} - -static void _init_timer_info(interval_face_state_t *state) { - state->face_state = interval_state_waiting; - _ticks = 0; - if (state->is_active) movement_request_tick_frequency(2); -} - -static void _abort_running_timer() { - _timer_work_round = _timer_full_round = 0; - _timer_run_state = 0; - movement_cancel_background_task(); - watch_clear_indicator(WATCH_INDICATOR_BELL); - watch_buzzer_play_note(BUZZER_NOTE_C8, 100); -} - -static void _resume_paused_timer(interval_face_state_t *state) { - // resume paused timer - _now_ts = _get_now_ts(); - _target_ts += _now_ts - _paused_ts; - watch_date_time_t target_dt = watch_utility_date_time_from_unix_time(_target_ts, 0); - movement_schedule_background_task_for_face(state->face_idx, target_dt); - state->face_state = interval_state_running; - watch_set_indicator(WATCH_INDICATOR_BELL); -} - -void interval_face_setup(uint8_t watch_face_index, void **context_ptr) { - - if (*context_ptr == NULL) { - *context_ptr = malloc(sizeof(interval_face_state_t)); - interval_face_state_t *state = (interval_face_state_t *)*context_ptr; - memset(*context_ptr, 0, sizeof(interval_face_state_t)); - state->face_idx = watch_face_index; - // somehow the memset above doesn't to the trick. So set the state explicitly - state->face_state = interval_state_waiting; - for (uint8_t i = 0; i < INTERVAL_TIMERS; i++) state->timer[i].work_rounds = 1; - // set up default timers - for (uint8_t i = 0; i < 6; i++) { - state->timer[i].warmup_seconds = _default_timers[i][0]; - if (_default_timers[i][1] < 0) state->timer[i].work_minutes = -_default_timers[i][1]; - else state->timer[i].work_seconds = _default_timers[i][1]; - state->timer[i].work_rounds = 1; - if (_default_timers[i][2] < 0) state->timer[i].break_minutes = -_default_timers[i][2]; - else state->timer[i].break_seconds = _default_timers[i][2]; - state->timer[i].full_rounds = _default_timers[i][3]; - state->timer[i].cooldown_seconds = _default_timers[i][4]; - } - } -} - -void interval_face_activate(void *context) { - interval_face_state_t *state = (interval_face_state_t *)context; - _erase_timer_flag = false; - state->is_active = true; - if (state->face_state <= interval_state_waiting) { - // initiate the intro loop - state->face_state = interval_state_intro; - _ticks = 0; - movement_request_tick_frequency(8); - } else watch_set_colon(); -} - -void interval_face_resign(void *context) { - interval_face_state_t *state = (interval_face_state_t *)context; - if (state->face_state <= interval_state_setting) state->face_state = interval_state_waiting; - watch_set_led_off(); - movement_request_tick_frequency(1); - state->is_active = false; -} - -bool interval_face_loop(movement_event_t event, void *context) { - interval_face_state_t *state = (interval_face_state_t *)context; - interval_timer_setting_t *timer = &state->timer[state->timer_idx]; - - switch (event.event_type) { - case EVENT_TICK: - if (state->face_state == interval_state_intro) { - // play intro animation so the wearer knows the face - if (_ticks == 4) { - // transition to default view of current interval slot - watch_set_colon(); - _init_timer_info(state); - _face_draw(state, event.subsecond); - break; - } - watch_set_pixel(_intro_segdata[_ticks][0], _intro_segdata[_ticks][1]); - _ticks++; - } else if (state->face_state == interval_state_waiting && _ticks >= 0) { - // play information slideshow for current interval timer - _ticks++; - if ((_ticks % 12 == 9) && (timer->cooldown_minutes + timer->cooldown_seconds == 0)) _ticks += 3; - if (_ticks > 24) _ticks = -1; - else _face_draw(state, event.subsecond); - } else if (state->face_state == interval_state_setting) { - if (_ticks == -2) { - // fast counting - _handle_alarm_button(state); - } - _face_draw(state, event.subsecond); - } else if (state->face_state == interval_state_running || state->face_state == interval_state_pausing) { - _now_ts = _get_now_ts(); - _face_draw(state, event.subsecond); - } - break; - case EVENT_ACTIVATE: - watch_display_string(INTERVAL_FACE_STATE_DEFAULT, 0); - if (state->face_state) _face_draw(state, event.subsecond); - break; - case EVENT_LIGHT_BUTTON_UP: - if (state->face_state == interval_state_setting) { - if (_setting_idx == interval_setting_0_timer_idx) { - // skip clear page if timer is empty - if (_is_timer_empty(timer)) _setting_idx = interval_setting_1_clear_yn; - } else if (_setting_idx == interval_setting_1_clear_yn) { - watch_set_colon(); - if (_erase_timer_flag) { - // clear the current timer - memset((void *)timer, 0, sizeof(interval_timer_setting_t)); - // play a short beep as confirmation - watch_buzzer_play_note(BUZZER_NOTE_C8, 70); - } - } else if (_setting_idx == interval_setting_9_full_rounds && !timer->full_rounds) { - // skip cooldown if full rounds are not limited - _setting_idx = interval_setting_11_cooldown_seconds; - } - _setting_idx += 1; - if (_setting_idx == interval_setting_max) { - // we have done a full settings circle: resume setting - _resume_setting(state, event.subsecond); - } else - _face_draw(state, event.subsecond); - } else { - movement_illuminate_led(); - } - break; - case EVENT_LIGHT_LONG_PRESS: - _button_beep(settings); - if (state->face_state == interval_state_setting) { - _resume_setting(state, event.subsecond); - } else { - if (state->face_state >= interval_state_running ) _abort_running_timer(); - _initiate_setting(state, event.subsecond); - } - break; - case EVENT_ALARM_BUTTON_UP: - switch (state->face_state) { - case interval_state_waiting: - // cycle through timers - _inc_uint8(&state->timer_idx, 1, INTERVAL_TIMERS); - _ticks = 0; - _face_draw(state, event.subsecond); - break; - case interval_state_setting: - // alter timer settings - _abort_quick_ticks(); - _handle_alarm_button(state); - break; - case interval_state_running: - // pause timer - _button_beep(settings); - _paused_ts = _get_now_ts(); - state->face_state = interval_state_pausing; - movement_cancel_background_task(); - _face_draw(state, event.subsecond); - break; - case interval_state_pausing: - // resume paused timer - _button_beep(settings); - _resume_paused_timer(state); - _face_draw(state, event.subsecond); - break; - default: - break; - } - break; - case EVENT_ALARM_LONG_PRESS: - if (state->face_state == interval_state_setting && _setting_idx != interval_setting_1_clear_yn) { - // initiate quick counting - _ticks = -2; - movement_request_tick_frequency(8); - break; - } else if (state->face_state <= interval_state_waiting) { - if (_is_timer_empty(timer)) { - // jump back to timer #1 - _button_beep(settings); - state->timer_idx = 0; - _init_timer_info(state); - } else { - // set initial state and start timer - _timer_work_round = _timer_full_round = 0; - if (timer->warmup_minutes + timer->warmup_seconds) _timer_run_state = 0; - else if (timer->work_minutes + timer->work_seconds) _timer_run_state = 1; - else if (timer->break_minutes + timer->break_seconds) _timer_run_state = 2; - else if (timer->cooldown_minutes + timer->cooldown_seconds) _timer_run_state = 3; - movement_request_tick_frequency(1); - _now_ts = _get_now_ts(); - _target_ts = _now_ts; - _set_next_timestamp(state); - state->face_state = interval_state_running; - watch_set_indicator(WATCH_INDICATOR_BELL); - watch_set_colon(); - } - } else if (state->face_state == interval_state_running) { - // stop the timer - _abort_running_timer(); - _init_timer_info(state); - } else if (state->face_state == interval_state_pausing) { - // resume paused timer - _button_beep(settings); - _resume_paused_timer(state); - } - _face_draw(state, event.subsecond); - break; - case EVENT_ALARM_LONG_UP: - _abort_quick_ticks(); - break; - case EVENT_BACKGROUND_TASK: - // find the next timestamp or end the timer - if (_timer_run_state == 0) { - // warmup finished - if (timer->work_minutes + timer->work_seconds) _timer_run_state = 1; - else if (timer->break_minutes + timer->break_seconds) _timer_run_state = 2; - else if (timer->cooldown_minutes + timer->cooldown_seconds) _timer_run_state = 3; - else _timer_run_state = 4; - } else if (_timer_run_state == 1) { - // work finished - _timer_work_round++; - if (_timer_work_round == timer->work_rounds) { - _timer_work_round = 0; - if (timer->break_minutes + timer->break_seconds && (timer->full_rounds == 0 - || (timer->full_rounds && _timer_full_round + 1 < timer->full_rounds))) _timer_run_state = 2; - else { - _timer_full_round++; - if (timer->full_rounds && _timer_full_round == timer->full_rounds) { - if (timer->cooldown_minutes + timer->cooldown_seconds) _timer_run_state = 3; - else _timer_run_state = 4; - } else _timer_run_state = 1; - } - } - } else if (_timer_run_state == 2) { - // break finished - _timer_full_round++; - _timer_work_round = 0; - if (timer->full_rounds && _timer_full_round == timer->full_rounds) { - if (timer->cooldown_minutes + timer->cooldown_seconds) _timer_run_state = 3; - else _timer_run_state = 4; - _timer_full_round--; - } else { - if (timer->work_minutes + timer->work_seconds) _timer_run_state = 1; - } - } else if (_timer_run_state == 3) - // cooldown finished - _timer_run_state = 4; - // set next timestamp or play final sound sequence - if (_timer_run_state < 4) { - // transition to next timer phase - _set_next_timestamp(state); - } else { - // timer has finished - state->face_state = interval_state_waiting; - _init_timer_info(state); - _face_draw(state, event.subsecond); - watch_buzzer_play_sequence((int8_t *)_sound_seq_finish, NULL); - } - break; - case EVENT_TIMEOUT: - if (state->face_state != interval_state_running) movement_move_to_face(0); - break; - case EVENT_LIGHT_BUTTON_DOWN: - // don't light up every time light is hit - break; - default: - movement_default_loop_handler(event); - break; - } - return true; -} diff --git a/legacy/watch_faces/complication/interval_face.h b/legacy/watch_faces/complication/interval_face.h deleted file mode 100644 index def944b..0000000 --- a/legacy/watch_faces/complication/interval_face.h +++ /dev/null @@ -1,127 +0,0 @@ -/* - * MIT License - * - * Copyright (c) 2022 Andreas Nebinger - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - */ - -#ifndef INTERVAL_FACE_H_ -#define INTERVAL_FACE_H_ - -/* - * INTERVAL TIMER face - * - * This face brings 9 customizable interval timers to the sensor watch, - * to be used as hiit training device and/or for time management techniques. - * - * - There are 9 interval timer slots, you can cycle through these with the - * alarm button (short press). For each timer slot, a short "slideshow" - * displaying the relevant details (like length of each phase - see below) - * is shown. - * - * - To start an interval timer, press and hold the alarm button. - * - * - To pause a running timer, press the alarm button (short press). - * - * - To completely abort a running timer, press and hold the alarm button. - * - * - Press and hold the light button to enter settings mode for each interval - * timer slot. - * - * - Each interval timer has 1 to 4 phases of customizable length like so: - * (1) prepare/warum up --> (2) work --> (3) break --> (4) cool down. - * When setting up or running a timer, each of these phases is displayed by - * the letters "PR" (prepare), "WO" (work), "BR" (break), "CD" (cool down). - * - * - Each of these phases is optional, you can set the corresponding - * minutes and seconds to zero. But at least one phase needs to be set, if - * you want to use the timer. - * - * - You can define the number of rounds either only for the work - * phase and/or for the combination of work + break phase. Let's say you - * want an interval timer that counts 3 rounds of 30 seconds work, - * followed by 20 seconds rest: - * work 30s --> work 30s --> work 30s --> break 20s - * You can do this by setting 30s for the "WO"rk phase and setting a 3 - * in the lower right hand corner of the work page. The "LAP" indicator - * lights up at this position, to explain that we are setting laps here. - * After that, set the "BR"eak phase to 20s and leave the rest as it is. - * - * - If you want to set up a certain number of "full rounds", consisting - * of work phase(s) plus breaks, you can do so at the "BR"eak page. The - * number in the lower right hand corner determines the number of full - * rounds to be counted. A "-" means, that there is no limit and the - * timer keeps alternating between work and break phases. - * - * - This watch face comes with several pre-defined interval timers, - * suitable for hiit training (timer slots 1 to 4) as well as doing - * work according to the pomodoro principle (timer slots 5 to 6). - * Feel free to adjust the timer slots to your own needs (or completely - * wipe them ;-) - */ - -#include "movement.h" - -#define INTERVAL_TIMERS 9 // no of available customizable timers (be aware: only 4 bits reserved for this value in struct below) - -typedef struct { - uint8_t warmup_minutes; - uint8_t warmup_seconds; - uint8_t work_minutes; - uint8_t work_seconds; - uint8_t break_minutes; - uint8_t break_seconds; - uint8_t cooldown_minutes; - uint8_t cooldown_seconds; - uint8_t work_rounds; - uint8_t full_rounds; -} interval_timer_setting_t; - -typedef enum { - interval_state_intro, - interval_state_waiting, - interval_state_setting, - interval_state_running, - interval_state_pausing -} interval_timer_state_t; - -typedef struct { - bool is_active; - uint8_t face_idx; - uint8_t timer_idx; - uint8_t timer_running_idx; - interval_timer_state_t face_state; - interval_timer_setting_t timer[INTERVAL_TIMERS]; -} interval_face_state_t; - -void interval_face_setup(uint8_t watch_face_index, void ** context_ptr); -void interval_face_activate(void *context); -bool interval_face_loop(movement_event_t event, void *context); -void interval_face_resign(void *context); - -#define interval_face ((const watch_face_t) { \ - interval_face_setup, \ - interval_face_activate, \ - interval_face_loop, \ - interval_face_resign, \ - NULL \ -}) - -#endif // INTERVAL_FACE_H_ diff --git a/movement_faces.h b/movement_faces.h index 542a066..1e14d23 100644 --- a/movement_faces.h +++ b/movement_faces.h @@ -63,6 +63,7 @@ #include "tally_face.h" #include "probability_face.h" #include "ke_decimal_time_face.h" +#include "interval_face.h" #include "timer_face.h" #include "simple_coin_flip_face.h" #include "lis2dw_monitor_face.h" diff --git a/watch-faces.mk b/watch-faces.mk index bf3c54d..2b728e0 100644 --- a/watch-faces.mk +++ b/watch-faces.mk @@ -38,6 +38,7 @@ SRCS += \ ./watch-faces/complication/kitchen_conversions_face.c \ ./watch-faces/complication/periodic_table_face.c \ ./watch-faces/clock/ke_decimal_time_face.c \ + ./watch-faces/complication/interval_face.c \ ./watch-faces/complication/timer_face.c \ ./watch-faces/complication/simple_coin_flip_face.c \ ./watch-faces/sensor/lis2dw_monitor_face.c \ diff --git a/watch-faces/complication/interval_face.c b/watch-faces/complication/interval_face.c new file mode 100644 index 0000000..1559356 --- /dev/null +++ b/watch-faces/complication/interval_face.c @@ -0,0 +1,681 @@ +/* + * MIT License + * + * Copyright (c) 2022 Andreas Nebinger + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +#include +#include + +#include "interval_face.h" +#include "watch.h" +#include "watch_utility.h" + +typedef enum { + interval_setting_0_timer_idx, + interval_setting_1_clear_yn, + interval_setting_2_warmup_minutes, + interval_setting_3_warmup_seconds, + interval_setting_4_work_minutes, + interval_setting_5_work_seconds, + interval_setting_6_work_rounds, + interval_setting_7_break_minutes, + interval_setting_8_break_seconds, + interval_setting_9_full_rounds, + interval_setting_10_cooldown_minutes, + interval_setting_11_cooldown_seconds, + interval_setting_max +} interval_setting_idx_t; + +#define INTERVAL_FACE_STATE_DEFAULT "IT" // Interval Timer +#define INTERVAL_FACE_STATE_DEFAULT_CD "INT" +#define INTERVAL_FACE_STATE_WARMUP "PR" // PRepare / warm up +#define INTERVAL_FACE_STATE_WARMUP_CD "PRE" +#define INTERVAL_FACE_STATE_WORK "WO" // WOrk +#define INTERVAL_FACE_STATE_WORK_CD "WOR" +#define INTERVAL_FACE_STATE_BREAK "BR" // BReak +#define INTERVAL_FACE_STATE_BREAK_CD "BRK" +#define INTERVAL_FACE_STATE_COOLDOWN "CD" // CoolDown +#define INTERVAL_FACE_STATE_COOLDOWN_CD "CLD" + +// Define some default timer settings. Each timer is described in an array like this: +// 1. warm-up seconds, +// 2. work time (seconds/minutes) +// 3. break time (seconds/minutes) +// 4. full rounds (0 = no limit) +// 5. cooldown seconds +// Work time and break time: positive number = seconds, negative number = minutes +static const int8_t _default_timers[6][5] = {{0, 40, 20, 0, 0}, + {0, 45, 15, 0, 0}, + {10, 20, 10, 8, 10}, + {0, 35, 0, 0, 0}, + {0, -25, -5, 0, 0}, + {0, -20, -5, 0, 0}}; + +static const uint8_t _intro_segdata[4][2] = {{1, 8}, {0, 8}, {0, 7}, {1, 7}}; +static const uint8_t _intro_segdata_cd[4][2] = {{1, 8}, {1, 9}, {0, 9}, {0, 8}}; +static const uint8_t _setting_page_idx[] = {1, 0, 1, 1, 2, 2, 2, 3, 3, 3, 4, 4}; +static const int8_t _sound_seq_warmup[] = {BUZZER_NOTE_F6, 8, BUZZER_NOTE_REST, 1, -2, 3, 0}; +static const int8_t _sound_seq_work[] = {BUZZER_NOTE_F6, 8, BUZZER_NOTE_REST, 1, -2, 2, BUZZER_NOTE_C7, 24, 0}; +static const int8_t _sound_seq_break[] = {BUZZER_NOTE_B6, 15, BUZZER_NOTE_REST, 1, -2, 1, BUZZER_NOTE_B6, 16, 0}; +static const int8_t _sound_seq_cooldown[] = {BUZZER_NOTE_C7, 15, BUZZER_NOTE_REST, 1, -2, 1, BUZZER_NOTE_C7, 24, 0}; +static const int8_t _sound_seq_finish[] = {BUZZER_NOTE_C7, 6, BUZZER_NOTE_E7, 6, BUZZER_NOTE_G7, 6, BUZZER_NOTE_C8, 18, 0}; + +static interval_setting_idx_t _setting_idx; +static int8_t _ticks; +static bool _erase_timer_flag; +static uint32_t _target_ts; +static uint32_t _now_ts; +static uint32_t _paused_ts; +static uint8_t _timer_work_round; +static uint8_t _timer_full_round; +static uint8_t _timer_run_state; + +static inline void _inc_uint8(uint8_t *value, uint8_t step, uint8_t max) { + *value += step; + if (*value >= max) *value = 0; +} + +static uint32_t _get_now_ts() { + // returns the current date time as unix timestamp + watch_date_time_t now = watch_rtc_get_date_time(); + return watch_utility_date_time_to_unix_time(now, 0); +} + +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(BUZZER_NOTE_C7, 50); +} + +static void _timer_write_info(interval_face_state_t *state, char* bottom_row, char state_str[2][4], char* index_str, uint8_t timer_page) { + // fill display string with requested timer information + switch (timer_page) { + case 0: + // clear timer? + sprintf(bottom_row, "CLEARn"); + sprintf(state_str[0], "%2s", INTERVAL_FACE_STATE_DEFAULT); + sprintf(state_str[1], "%3s", INTERVAL_FACE_STATE_DEFAULT_CD); + sprintf(index_str, " %1d", state->timer_idx + 1); + if (_erase_timer_flag) bottom_row[5] = 'y'; + watch_clear_colon(); + break; + case 1: + // warmup time info + sprintf(bottom_row, "%02d%02d ", + state->timer[state->timer_idx].warmup_minutes, + state->timer[state->timer_idx].warmup_seconds); + sprintf(state_str[0], "%2s", INTERVAL_FACE_STATE_WARMUP); + sprintf(state_str[1], "%3s", INTERVAL_FACE_STATE_WARMUP_CD); + sprintf(index_str, " %1d", state->timer_idx + 1); + break; + case 2: + // work interval info + sprintf(bottom_row, "%02d%02d%2d", + state->timer[state->timer_idx].work_minutes, + state->timer[state->timer_idx].work_seconds, + state->timer[state->timer_idx].work_rounds); + sprintf(state_str[0], "%2s", INTERVAL_FACE_STATE_WORK); + sprintf(state_str[1], "%3s", INTERVAL_FACE_STATE_WORK_CD); + sprintf(index_str, " %1d", state->timer_idx + 1); + break; + case 3: + // break interval info + sprintf(bottom_row, "%02d%02d%2d", + state->timer[state->timer_idx].break_minutes, + state->timer[state->timer_idx].break_seconds, + state->timer[state->timer_idx].full_rounds); + if (!state->timer[state->timer_idx].full_rounds) bottom_row[5] = '-'; + sprintf(state_str[0], "%2s", INTERVAL_FACE_STATE_BREAK); + sprintf(state_str[1], "%3s", INTERVAL_FACE_STATE_BREAK_CD); + sprintf(index_str, " %1d", state->timer_idx + 1); + break; + case 4: + // cooldown time info + sprintf(bottom_row, "%02d%02d ", + state->timer[state->timer_idx].cooldown_minutes, + state->timer[state->timer_idx].cooldown_seconds); + sprintf(state_str[0], "%2s", INTERVAL_FACE_STATE_COOLDOWN); + sprintf(state_str[1], "%3s", INTERVAL_FACE_STATE_COOLDOWN_CD); + sprintf(index_str, " %1d", state->timer_idx + 1); + break; + default: + break; + } +} + +static void _face_draw(interval_face_state_t *state, uint8_t subsecond) { + // draws current face state + if (!state->is_active) return; + char bottom_row[10]; + char int_state_str[2][4]; + char int_index_str[5]; + bottom_row[0] = 0; + uint8_t tmp; + if (state->face_state == interval_state_waiting && _ticks >= 0) { + // play info slideshow for current timer + int8_t ticks = _ticks % 12; + if (ticks == 0) { + if ((state->timer[state->timer_idx].warmup_minutes + state->timer[state->timer_idx].warmup_seconds) == 0) { + // skip warmup info if there is none for this timer + ticks = 3; + _ticks += 3; + } + } + tmp = ticks / 3 + 1; + _timer_write_info(state, bottom_row, int_state_str, int_index_str, tmp); + // don't show '1 round' when displaying workout time to avoid detail overload + if (tmp == 2 && state->timer[state->timer_idx].work_rounds == 1) bottom_row[5] = ' '; + // blink colon + if (subsecond % 2 == 0 && _ticks < 24) watch_clear_colon(); + else watch_set_colon(); + } else if (state->face_state == interval_state_setting) { + if (_setting_idx == interval_setting_0_timer_idx) { + if ((state->timer[state->timer_idx].warmup_minutes + state->timer[state->timer_idx].warmup_seconds) == 0) + tmp = 1; + else + tmp = 2; + } else { + tmp = _setting_page_idx[_setting_idx]; + } + _timer_write_info(state, bottom_row, int_state_str, int_index_str, tmp); + // blink at cursor position + if (subsecond % 2 && _ticks != -2) { + switch (_setting_idx) { + case interval_setting_0_timer_idx: + int_index_str[0] = int_index_str[1] = ' '; + break; + case interval_setting_1_clear_yn: + bottom_row[5] = ' '; + break; + case interval_setting_2_warmup_minutes: + case interval_setting_4_work_minutes: + case interval_setting_7_break_minutes: + case interval_setting_10_cooldown_minutes: + bottom_row[0] = bottom_row[1] = ' '; + break; + case interval_setting_3_warmup_seconds: + case interval_setting_5_work_seconds: + case interval_setting_8_break_seconds: + case interval_setting_11_cooldown_seconds: + bottom_row[2] = bottom_row[3] = ' '; + break; + case interval_setting_6_work_rounds: + case interval_setting_9_full_rounds: + bottom_row[4] = bottom_row[5] = ' '; + break; + default: + break; + } + } + // show lap indicator only when rounds are set + if (_setting_idx == interval_setting_6_work_rounds || _setting_idx == interval_setting_9_full_rounds) + watch_set_indicator(WATCH_INDICATOR_LAP); + else + watch_clear_indicator(WATCH_INDICATOR_LAP); + } else if (state->face_state == interval_state_running || state->face_state == interval_state_pausing) { + tmp = _timer_full_round; + switch (_timer_run_state) { + case 0: + sprintf(int_state_str[0], "%2s", INTERVAL_FACE_STATE_WARMUP); + sprintf(int_state_str[1], "%3s", INTERVAL_FACE_STATE_WARMUP_CD); + break; + case 1: + sprintf(int_state_str[0], "%2s", INTERVAL_FACE_STATE_WORK); + sprintf(int_state_str[1], "%3s", INTERVAL_FACE_STATE_WORK_CD); + if (state->timer[state->timer_idx].work_rounds > 1) tmp = _timer_work_round; + break; + case 2: + sprintf(int_state_str[0], "%2s", INTERVAL_FACE_STATE_BREAK); + sprintf(int_state_str[1], "%3s", INTERVAL_FACE_STATE_BREAK_CD); + break; + case 3: + sprintf(int_state_str[0], "%2s", INTERVAL_FACE_STATE_COOLDOWN); + sprintf(int_state_str[1], "%3s", INTERVAL_FACE_STATE_COOLDOWN_CD); + break; + default: + break; + } + div_t delta; + + if (state->face_state == interval_state_pausing) { + // pausing + delta = div(_target_ts - _paused_ts, 60); + // blink the bell icon + if (_now_ts % 2) watch_set_indicator(WATCH_INDICATOR_BELL); + else watch_clear_indicator(WATCH_INDICATOR_BELL); + } else + // running + delta = div(_target_ts - _now_ts, 60); + sprintf(bottom_row, "%02d%02d%2d", delta.quot, delta.rem, tmp + 1); + sprintf(int_index_str, " %1d", state->timer_idx + 1); + } + // write out to lcd + if (int_state_str[0][0]) { + watch_display_text_with_fallback(WATCH_POSITION_TOP_LEFT, int_state_str[1], int_state_str[0]); + watch_display_text_with_fallback(WATCH_POSITION_TOP_RIGHT, int_index_str, int_index_str); + // set the bar for the i-like symbol on position 2 + if (watch_get_lcd_type() == WATCH_LCD_TYPE_CLASSIC) { + watch_set_pixel(2, 9); + } else { + watch_set_pixel(2, 10); + } + // display the rest of the string + watch_display_text_with_fallback(WATCH_POSITION_BOTTOM, bottom_row, bottom_row); + } +} + +static void _initiate_setting(interval_face_state_t *state, uint8_t subsecond) { + state->face_state = interval_state_setting; + _setting_idx = interval_setting_0_timer_idx; + _ticks = 0; + _erase_timer_flag = false; + watch_set_colon(); + movement_request_tick_frequency(4); + _face_draw(state, subsecond); +} + +static void _resume_setting(interval_face_state_t *state, uint8_t subsecond) { + state->face_state = interval_state_waiting; + _ticks = 0; + _face_draw(state, subsecond); + movement_request_tick_frequency(2); + watch_clear_indicator(WATCH_INDICATOR_LAP); +} + +static void _abort_quick_ticks() { + if (_ticks == -2) { + _ticks = -1; + movement_request_tick_frequency(4); + } +} + +static void _handle_alarm_button(interval_face_state_t *state) { + // handles the alarm button press and alters the corresponding timer settings + switch (_setting_idx) { + case interval_setting_0_timer_idx: + _inc_uint8(&state->timer_idx, 1, INTERVAL_TIMERS); + _erase_timer_flag = false; + break; + case interval_setting_1_clear_yn: + _erase_timer_flag ^= 1; + break; + case interval_setting_2_warmup_minutes: + _inc_uint8(&state->timer[state->timer_idx].warmup_minutes, 1, 60); + break; + case interval_setting_3_warmup_seconds: + _inc_uint8(&state->timer[state->timer_idx].warmup_seconds, 5, 60); + break; + case interval_setting_4_work_minutes: + _inc_uint8(&state->timer[state->timer_idx].work_minutes, 1, 60); + if (state->timer[state->timer_idx].work_rounds == 0) state->timer[state->timer_idx].work_rounds = 1; + break; + case interval_setting_5_work_seconds: + _inc_uint8(&state->timer[state->timer_idx].work_seconds, 5, 60); + if (state->timer[state->timer_idx].work_rounds == 0) state->timer[state->timer_idx].work_rounds = 1; + break; + case interval_setting_6_work_rounds: + _inc_uint8(&state->timer[state->timer_idx].work_rounds, 1, 100); + break; + case interval_setting_7_break_minutes: + _inc_uint8(&state->timer[state->timer_idx].break_minutes, 1, 60); + break; + case interval_setting_8_break_seconds: + _inc_uint8(&state->timer[state->timer_idx].break_seconds, 5, 60); + break; + case interval_setting_9_full_rounds: + _inc_uint8(&state->timer[state->timer_idx].full_rounds, 1, 100); + break; + case interval_setting_10_cooldown_minutes: + _inc_uint8(&state->timer[state->timer_idx].cooldown_minutes, 1, 60); + break; + case interval_setting_11_cooldown_seconds: + _inc_uint8(&state->timer[state->timer_idx].cooldown_seconds, 5, 60); + break; + default: + break; + } +} + +static void _set_next_timestamp(interval_face_state_t *state) { + // set next timestamp for the running timer, set background task and pay sound sequence + uint16_t delta = 0; + int8_t *sound_seq; + interval_timer_setting_t timer = state->timer[state->timer_idx]; + switch (_timer_run_state) { + case 0: + delta = timer.warmup_minutes * 60 + timer.warmup_seconds; + sound_seq = (int8_t *)_sound_seq_warmup; + break; + case 1: + delta = timer.work_minutes * 60 + timer.work_seconds; + sound_seq = (int8_t *)_sound_seq_work; + break; + case 2: + delta = timer.break_minutes * 60 + timer.break_seconds; + sound_seq = (int8_t *)_sound_seq_break; + break; + case 3: + delta = timer.cooldown_minutes * 60 + timer.cooldown_seconds; + sound_seq = (int8_t *)_sound_seq_cooldown; + break; + default: + sound_seq = NULL; + break; + } + // failsafe + if (delta <= 0) delta = 1; + _target_ts += delta; + // schedule next background task + watch_date_time_t target_dt = watch_utility_date_time_from_unix_time(_target_ts, 0); + movement_schedule_background_task_for_face(state->face_idx, target_dt); + // play sound + watch_buzzer_play_sequence(sound_seq, NULL); +} + +static inline bool _is_timer_empty(interval_timer_setting_t *timer) { + // checks if a timer is empty + return (timer->warmup_minutes + timer->warmup_seconds + + timer->work_minutes + timer->work_seconds + + timer->break_minutes + timer->break_seconds + + timer->cooldown_minutes + timer->cooldown_seconds == 0); +} + +static void _init_timer_info(interval_face_state_t *state) { + state->face_state = interval_state_waiting; + _ticks = 0; + if (state->is_active) movement_request_tick_frequency(2); +} + +static void _abort_running_timer() { + _timer_work_round = _timer_full_round = 0; + _timer_run_state = 0; + movement_cancel_background_task(); + watch_clear_indicator(WATCH_INDICATOR_BELL); + watch_buzzer_play_note(BUZZER_NOTE_C8, 100); +} + +static void _resume_paused_timer(interval_face_state_t *state) { + // resume paused timer + _now_ts = _get_now_ts(); + _target_ts += _now_ts - _paused_ts; + watch_date_time_t target_dt = watch_utility_date_time_from_unix_time(_target_ts, 0); + movement_schedule_background_task_for_face(state->face_idx, target_dt); + state->face_state = interval_state_running; + watch_set_indicator(WATCH_INDICATOR_BELL); +} + +void interval_face_setup(uint8_t watch_face_index, void **context_ptr) { + + if (*context_ptr == NULL) { + *context_ptr = malloc(sizeof(interval_face_state_t)); + interval_face_state_t *state = (interval_face_state_t *)*context_ptr; + memset(*context_ptr, 0, sizeof(interval_face_state_t)); + state->face_idx = watch_face_index; + // somehow the memset above doesn't to the trick. So set the state explicitly + state->face_state = interval_state_waiting; + for (uint8_t i = 0; i < INTERVAL_TIMERS; i++) state->timer[i].work_rounds = 1; + // set up default timers + for (uint8_t i = 0; i < 6; i++) { + state->timer[i].warmup_seconds = _default_timers[i][0]; + if (_default_timers[i][1] < 0) state->timer[i].work_minutes = -_default_timers[i][1]; + else state->timer[i].work_seconds = _default_timers[i][1]; + state->timer[i].work_rounds = 1; + if (_default_timers[i][2] < 0) state->timer[i].break_minutes = -_default_timers[i][2]; + else state->timer[i].break_seconds = _default_timers[i][2]; + state->timer[i].full_rounds = _default_timers[i][3]; + state->timer[i].cooldown_seconds = _default_timers[i][4]; + } + } +} + +void interval_face_activate(void *context) { + interval_face_state_t *state = (interval_face_state_t *)context; + _erase_timer_flag = false; + state->is_active = true; + if (state->face_state <= interval_state_waiting) { + // initiate the intro loop + state->face_state = interval_state_intro; + _ticks = 0; + movement_request_tick_frequency(8); + } else watch_set_colon(); +} + +void interval_face_resign(void *context) { + interval_face_state_t *state = (interval_face_state_t *)context; + if (state->face_state <= interval_state_setting) state->face_state = interval_state_waiting; + watch_set_led_off(); + movement_request_tick_frequency(1); + state->is_active = false; +} + +bool interval_face_loop(movement_event_t event, void *context) { + interval_face_state_t *state = (interval_face_state_t *)context; + interval_timer_setting_t *timer = &state->timer[state->timer_idx]; + + switch (event.event_type) { + case EVENT_TICK: + if (state->face_state == interval_state_intro) { + // play intro animation so the wearer knows the face + if (_ticks == 4) { + // transition to default view of current interval slot + watch_set_colon(); + _init_timer_info(state); + _face_draw(state, event.subsecond); + break; + } + if (watch_get_lcd_type() == WATCH_LCD_TYPE_CLASSIC) { + watch_set_pixel(_intro_segdata[_ticks][0], _intro_segdata[_ticks][1]); + } else { + watch_set_pixel(_intro_segdata_cd[_ticks][0], _intro_segdata_cd[_ticks][1]); + } + _ticks++; + } else if (state->face_state == interval_state_waiting && _ticks >= 0) { + // play information slideshow for current interval timer + _ticks++; + if ((_ticks % 12 == 9) && (timer->cooldown_minutes + timer->cooldown_seconds == 0)) _ticks += 3; + if (_ticks > 24) _ticks = -1; + else _face_draw(state, event.subsecond); + } else if (state->face_state == interval_state_setting) { + if (_ticks == -2) { + // fast counting + _handle_alarm_button(state); + } + _face_draw(state, event.subsecond); + } else if (state->face_state == interval_state_running || state->face_state == interval_state_pausing) { + _now_ts = _get_now_ts(); + _face_draw(state, event.subsecond); + } + break; + case EVENT_ACTIVATE: + watch_display_text_with_fallback(WATCH_POSITION_TOP_LEFT, INTERVAL_FACE_STATE_DEFAULT_CD, INTERVAL_FACE_STATE_DEFAULT); + if (state->face_state) _face_draw(state, event.subsecond); + break; + case EVENT_LIGHT_BUTTON_UP: + if (state->face_state == interval_state_setting) { + if (_setting_idx == interval_setting_0_timer_idx) { + // skip clear page if timer is empty + if (_is_timer_empty(timer)) _setting_idx = interval_setting_1_clear_yn; + } else if (_setting_idx == interval_setting_1_clear_yn) { + watch_set_colon(); + if (_erase_timer_flag) { + // clear the current timer + memset((void *)timer, 0, sizeof(interval_timer_setting_t)); + // play a short beep as confirmation + watch_buzzer_play_note(BUZZER_NOTE_C8, 70); + } + } else if (_setting_idx == interval_setting_9_full_rounds && !timer->full_rounds) { + // skip cooldown if full rounds are not limited + _setting_idx = interval_setting_11_cooldown_seconds; + } + _setting_idx += 1; + if (_setting_idx == interval_setting_max) { + // we have done a full settings circle: resume setting + _resume_setting(state, event.subsecond); + } else + _face_draw(state, event.subsecond); + } else { + movement_illuminate_led(); + } + break; + case EVENT_LIGHT_LONG_PRESS: + _button_beep(); + if (state->face_state == interval_state_setting) { + _resume_setting(state, event.subsecond); + } else { + if (state->face_state >= interval_state_running ) _abort_running_timer(); + _initiate_setting(state, event.subsecond); + } + break; + case EVENT_ALARM_BUTTON_UP: + switch (state->face_state) { + case interval_state_waiting: + // cycle through timers, skipping empty ones + do { + _inc_uint8(&state->timer_idx, 1, INTERVAL_TIMERS); + } while (_is_timer_empty(&state->timer[state->timer_idx]) && state->timer_idx != 0); + _ticks = 0; + _face_draw(state, event.subsecond); + break; + case interval_state_setting: + // alter timer settings + _abort_quick_ticks(); + _handle_alarm_button(state); + break; + case interval_state_running: + // pause timer + _button_beep(); + _paused_ts = _get_now_ts(); + state->face_state = interval_state_pausing; + movement_cancel_background_task(); + _face_draw(state, event.subsecond); + break; + case interval_state_pausing: + // resume paused timer + _button_beep(); + _resume_paused_timer(state); + _face_draw(state, event.subsecond); + break; + default: + break; + } + break; + case EVENT_ALARM_LONG_PRESS: + if (state->face_state == interval_state_setting && _setting_idx != interval_setting_1_clear_yn) { + // initiate quick counting + _ticks = -2; + movement_request_tick_frequency(8); + break; + } else if (state->face_state <= interval_state_waiting) { + if (_is_timer_empty(timer)) { + // jump back to timer #1 + _button_beep(); + state->timer_idx = 0; + _init_timer_info(state); + } else { + // set initial state and start timer + _timer_work_round = _timer_full_round = 0; + if (timer->warmup_minutes + timer->warmup_seconds) _timer_run_state = 0; + else if (timer->work_minutes + timer->work_seconds) _timer_run_state = 1; + else if (timer->break_minutes + timer->break_seconds) _timer_run_state = 2; + else if (timer->cooldown_minutes + timer->cooldown_seconds) _timer_run_state = 3; + movement_request_tick_frequency(1); + _now_ts = _get_now_ts(); + _target_ts = _now_ts; + _set_next_timestamp(state); + state->face_state = interval_state_running; + watch_set_indicator(WATCH_INDICATOR_BELL); + watch_set_colon(); + } + } else if (state->face_state == interval_state_running) { + // stop the timer + _abort_running_timer(); + _init_timer_info(state); + } else if (state->face_state == interval_state_pausing) { + // resume paused timer + _button_beep(); + _resume_paused_timer(state); + } + _face_draw(state, event.subsecond); + break; + case EVENT_ALARM_LONG_UP: + _abort_quick_ticks(); + break; + case EVENT_BACKGROUND_TASK: + // find the next timestamp or end the timer + if (_timer_run_state == 0) { + // warmup finished + if (timer->work_minutes + timer->work_seconds) _timer_run_state = 1; + else if (timer->break_minutes + timer->break_seconds) _timer_run_state = 2; + else if (timer->cooldown_minutes + timer->cooldown_seconds) _timer_run_state = 3; + else _timer_run_state = 4; + } else if (_timer_run_state == 1) { + // work finished + _timer_work_round++; + if (_timer_work_round == timer->work_rounds) { + _timer_work_round = 0; + if (timer->break_minutes + timer->break_seconds && (timer->full_rounds == 0 + || (timer->full_rounds && _timer_full_round + 1 < timer->full_rounds))) _timer_run_state = 2; + else { + _timer_full_round++; + if (timer->full_rounds && _timer_full_round == timer->full_rounds) { + if (timer->cooldown_minutes + timer->cooldown_seconds) _timer_run_state = 3; + else _timer_run_state = 4; + } else _timer_run_state = 1; + } + } + } else if (_timer_run_state == 2) { + // break finished + _timer_full_round++; + _timer_work_round = 0; + if (timer->full_rounds && _timer_full_round == timer->full_rounds) { + if (timer->cooldown_minutes + timer->cooldown_seconds) _timer_run_state = 3; + else _timer_run_state = 4; + _timer_full_round--; + } else { + if (timer->work_minutes + timer->work_seconds) _timer_run_state = 1; + } + } else if (_timer_run_state == 3) + // cooldown finished + _timer_run_state = 4; + // set next timestamp or play final sound sequence + if (_timer_run_state < 4) { + // transition to next timer phase + _set_next_timestamp(state); + } else { + // timer has finished + state->face_state = interval_state_waiting; + _init_timer_info(state); + _face_draw(state, event.subsecond); + watch_buzzer_play_sequence((int8_t *)_sound_seq_finish, NULL); + } + break; + case EVENT_TIMEOUT: + if (state->face_state != interval_state_running) movement_move_to_face(0); + break; + case EVENT_LIGHT_BUTTON_DOWN: + // don't light up every time light is hit + break; + default: + movement_default_loop_handler(event); + break; + } + return true; +} diff --git a/watch-faces/complication/interval_face.h b/watch-faces/complication/interval_face.h new file mode 100644 index 0000000..def944b --- /dev/null +++ b/watch-faces/complication/interval_face.h @@ -0,0 +1,127 @@ +/* + * MIT License + * + * Copyright (c) 2022 Andreas Nebinger + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +#ifndef INTERVAL_FACE_H_ +#define INTERVAL_FACE_H_ + +/* + * INTERVAL TIMER face + * + * This face brings 9 customizable interval timers to the sensor watch, + * to be used as hiit training device and/or for time management techniques. + * + * - There are 9 interval timer slots, you can cycle through these with the + * alarm button (short press). For each timer slot, a short "slideshow" + * displaying the relevant details (like length of each phase - see below) + * is shown. + * + * - To start an interval timer, press and hold the alarm button. + * + * - To pause a running timer, press the alarm button (short press). + * + * - To completely abort a running timer, press and hold the alarm button. + * + * - Press and hold the light button to enter settings mode for each interval + * timer slot. + * + * - Each interval timer has 1 to 4 phases of customizable length like so: + * (1) prepare/warum up --> (2) work --> (3) break --> (4) cool down. + * When setting up or running a timer, each of these phases is displayed by + * the letters "PR" (prepare), "WO" (work), "BR" (break), "CD" (cool down). + * + * - Each of these phases is optional, you can set the corresponding + * minutes and seconds to zero. But at least one phase needs to be set, if + * you want to use the timer. + * + * - You can define the number of rounds either only for the work + * phase and/or for the combination of work + break phase. Let's say you + * want an interval timer that counts 3 rounds of 30 seconds work, + * followed by 20 seconds rest: + * work 30s --> work 30s --> work 30s --> break 20s + * You can do this by setting 30s for the "WO"rk phase and setting a 3 + * in the lower right hand corner of the work page. The "LAP" indicator + * lights up at this position, to explain that we are setting laps here. + * After that, set the "BR"eak phase to 20s and leave the rest as it is. + * + * - If you want to set up a certain number of "full rounds", consisting + * of work phase(s) plus breaks, you can do so at the "BR"eak page. The + * number in the lower right hand corner determines the number of full + * rounds to be counted. A "-" means, that there is no limit and the + * timer keeps alternating between work and break phases. + * + * - This watch face comes with several pre-defined interval timers, + * suitable for hiit training (timer slots 1 to 4) as well as doing + * work according to the pomodoro principle (timer slots 5 to 6). + * Feel free to adjust the timer slots to your own needs (or completely + * wipe them ;-) + */ + +#include "movement.h" + +#define INTERVAL_TIMERS 9 // no of available customizable timers (be aware: only 4 bits reserved for this value in struct below) + +typedef struct { + uint8_t warmup_minutes; + uint8_t warmup_seconds; + uint8_t work_minutes; + uint8_t work_seconds; + uint8_t break_minutes; + uint8_t break_seconds; + uint8_t cooldown_minutes; + uint8_t cooldown_seconds; + uint8_t work_rounds; + uint8_t full_rounds; +} interval_timer_setting_t; + +typedef enum { + interval_state_intro, + interval_state_waiting, + interval_state_setting, + interval_state_running, + interval_state_pausing +} interval_timer_state_t; + +typedef struct { + bool is_active; + uint8_t face_idx; + uint8_t timer_idx; + uint8_t timer_running_idx; + interval_timer_state_t face_state; + interval_timer_setting_t timer[INTERVAL_TIMERS]; +} interval_face_state_t; + +void interval_face_setup(uint8_t watch_face_index, void ** context_ptr); +void interval_face_activate(void *context); +bool interval_face_loop(movement_event_t event, void *context); +void interval_face_resign(void *context); + +#define interval_face ((const watch_face_t) { \ + interval_face_setup, \ + interval_face_activate, \ + interval_face_loop, \ + interval_face_resign, \ + NULL \ +}) + +#endif // INTERVAL_FACE_H_