diff options
-rw-r--r-- | movement/make/Makefile | 1 | ||||
-rw-r--r-- | movement/movement_faces.h | 1 | ||||
-rw-r--r-- | movement/watch_faces/complication/interval_face.c | 677 | ||||
-rw-r--r-- | movement/watch_faces/complication/interval_face.h | 81 | ||||
-rw-r--r-- | movement/watch_faces/settings/set_time_face.c | 103 | ||||
-rw-r--r-- | watch-library/hardware/watch/watch_buzzer.c | 122 | ||||
-rw-r--r-- | watch-library/shared/watch/watch_buzzer.h | 23 | ||||
-rw-r--r-- | watch-library/simulator/watch/watch_buzzer.c | 76 |
8 files changed, 1050 insertions, 34 deletions
diff --git a/movement/make/Makefile b/movement/make/Makefile index 48d985f4..1ecb7c28 100644 --- a/movement/make/Makefile +++ b/movement/make/Makefile @@ -76,6 +76,7 @@ SRCS += \ ../watch_faces/demo/frequency_correction_face.c \ ../watch_faces/complication/alarm_face.c \ ../watch_faces/complication/ratemeter_face.c \ + ../watch_faces/complication/interval_face.c \ ../watch_faces/complication/rpn_calculator_alt_face.c \ ../watch_faces/complication/stock_stopwatch_face.c \ ../watch_faces/complication/tachymeter_face.c \ diff --git a/movement/movement_faces.h b/movement/movement_faces.h index 8ca3930f..c251d68b 100644 --- a/movement/movement_faces.h +++ b/movement/movement_faces.h @@ -70,6 +70,7 @@ #include "tempchart_face.h" #include "tally_face.h" #include "tarot_face.h" +#include "interval_face.h" // New includes go above this line. #endif // MOVEMENT_FACES_H_ diff --git a/movement/watch_faces/complication/interval_face.c b/movement/watch_faces/complication/interval_face.c new file mode 100644 index 00000000..0c35cdfc --- /dev/null +++ b/movement/watch_faces/complication/interval_face.c @@ -0,0 +1,677 @@ +/* + * 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 <stdlib.h> +#include <string.h> + +#include "interval_face.h" +#include "watch.h" +#include "watch_utility.h" +#include "watch_private_display.h" +#include "watch_buzzer.h" + +/* + 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 ;-) + +*/ + +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}; + +interval_setting_idx_t _setting_idx; +int8_t _ticks; +bool _erase_timer_flag; +uint32_t _target_ts; +uint32_t _now_ts; +uint32_t _paused_ts; +uint8_t _timer_work_round; +uint8_t _timer_full_round; +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 now = watch_rtc_get_date_time(); + return watch_utility_date_time_to_unix_time(now, 0); +} + +static inline void _button_beep(movement_settings_t *settings) { + // play a beep as confirmation for a button press (if applicable) + if (settings->bit.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 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 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(movement_settings_t *settings, uint8_t watch_face_index, void **context_ptr) { + (void) settings; + + 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(movement_settings_t *settings, void *context) { + (void) settings; + 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(movement_settings_t *settings, void *context) { + (void) settings; + 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, movement_settings_t *settings, 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_MODE_BUTTON_UP: + movement_move_to_next_face(); + break; + case EVENT_TIMEOUT: + if (state->face_state != interval_state_running) movement_move_to_face(0); + break; + default: + break; + } + return true; +}
\ No newline at end of file diff --git a/movement/watch_faces/complication/interval_face.h b/movement/watch_faces/complication/interval_face.h new file mode 100644 index 00000000..fa0a4280 --- /dev/null +++ b/movement/watch_faces/complication/interval_face.h @@ -0,0 +1,81 @@ +/* + * 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_ + +#include "movement.h" + +/* +A face for customizable interval timers +*/ + +#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(movement_settings_t *settings, uint8_t watch_face_index, void ** context_ptr); +void interval_face_activate(movement_settings_t *settings, void *context); +bool interval_face_loop(movement_event_t event, movement_settings_t *settings, void *context); +void interval_face_resign(movement_settings_t *settings, 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/watch_faces/settings/set_time_face.c b/movement/watch_faces/settings/set_time_face.c index 1605f119..f6ac4935 100644 --- a/movement/watch_faces/settings/set_time_face.c +++ b/movement/watch_faces/settings/set_time_face.c @@ -29,6 +29,55 @@ #define SET_TIME_FACE_NUM_SETTINGS (7) const char set_time_face_titles[SET_TIME_FACE_NUM_SETTINGS][3] = {"HR", "M1", "SE", "YR", "MO", "DA", "ZO"}; +static bool _quick_ticks_running; + +static void _handle_alarm_button(movement_settings_t *settings, watch_date_time date_time, uint8_t current_page) { + // handles short or long pressing of the alarm button + const uint8_t days_in_month[12] = {31, 28, 31, 30, 31, 30, 30, 31, 30, 31, 30, 31}; + + switch (current_page) { + case 0: // hour + date_time.unit.hour = (date_time.unit.hour + 1) % 24; + break; + case 1: // minute + date_time.unit.minute = (date_time.unit.minute + 1) % 60; + break; + case 2: // second + date_time.unit.second = 0; + break; + case 3: // year + // only allow 2021-2030. fix this sometime next decade + date_time.unit.year = ((date_time.unit.year % 10) + 1); + break; + case 4: // month + date_time.unit.month = (date_time.unit.month % 12) + 1; + break; + case 5: { // day + uint32_t tmp_day = date_time.unit.day; // use a temporary variable to avoid messing up the months + tmp_day = tmp_day + 1; + // handle February 29th on a leap year + if (((tmp_day > days_in_month[date_time.unit.month - 1]) && (date_time.unit.month != 2 || (date_time.unit.year % 4) != 0)) + || (date_time.unit.month == 2 && (date_time.unit.year % 4) == 0 && tmp_day > 29)) { + tmp_day = 1; + } + date_time.unit.day = tmp_day; + break; + } + case 6: // time zone + settings->bit.time_zone++; + if (settings->bit.time_zone > 40) settings->bit.time_zone = 0; + break; + } + watch_rtc_set_date_time(date_time); +} + +static void _abort_quick_ticks() { + if (_quick_ticks_running) { + _quick_ticks_running = false; + movement_request_tick_frequency(4); + } +} + void set_time_face_setup(movement_settings_t *settings, uint8_t watch_face_index, void ** context_ptr) { (void) settings; (void) watch_face_index; @@ -39,15 +88,31 @@ void set_time_face_activate(movement_settings_t *settings, void *context) { (void) settings; *((uint8_t *)context) = 0; movement_request_tick_frequency(4); + _quick_ticks_running = false; } bool set_time_face_loop(movement_event_t event, movement_settings_t *settings, void *context) { uint8_t current_page = *((uint8_t *)context); - const uint8_t days_in_month[12] = {31, 28, 31, 30, 31, 30, 30, 31, 30, 31, 30, 31}; watch_date_time date_time = watch_rtc_get_date_time(); switch (event.event_type) { + case EVENT_TICK: + if (_quick_ticks_running) { + if (watch_get_pin_level(BTN_ALARM)) _handle_alarm_button(settings, date_time, current_page); + else _abort_quick_ticks(); + } + break; + case EVENT_ALARM_LONG_PRESS: + if (current_page != 2) { + _quick_ticks_running = true; + movement_request_tick_frequency(8); + } + break; + case EVENT_ALARM_LONG_UP: + _abort_quick_ticks(); + break; case EVENT_MODE_BUTTON_UP: + _abort_quick_ticks(); movement_move_to_next_face(); return false; case EVENT_LIGHT_BUTTON_UP: @@ -55,39 +120,11 @@ bool set_time_face_loop(movement_event_t event, movement_settings_t *settings, v *((uint8_t *)context) = current_page; break; case EVENT_ALARM_BUTTON_UP: - switch (current_page) { - case 0: // hour - date_time.unit.hour = (date_time.unit.hour + 1) % 24; - break; - case 1: // minute - date_time.unit.minute = (date_time.unit.minute + 1) % 60; - break; - case 2: // second - date_time.unit.second = 0; - break; - case 3: // year - // only allow 2021-2050. fix this if we make it that far. - date_time.unit.year = ((date_time.unit.year % 30) + 1); - break; - case 4: // month - date_time.unit.month = (date_time.unit.month % 12) + 1; - break; - case 5: // day - date_time.unit.day = date_time.unit.day + 1; - // can't set to the 29th on a leap year. if it's february 29, set to 11:59 on the 28th. - // and it should roll over. - if (date_time.unit.day > days_in_month[date_time.unit.month - 1]) { - date_time.unit.day = 1; - } - break; - case 6: // time zone - settings->bit.time_zone++; - if (settings->bit.time_zone > 40) settings->bit.time_zone = 0; - break; - } - watch_rtc_set_date_time(date_time); + _abort_quick_ticks(); + _handle_alarm_button(settings, date_time, current_page); break; case EVENT_TIMEOUT: + _abort_quick_ticks(); movement_move_to_face(0); break; default: @@ -121,7 +158,7 @@ bool set_time_face_loop(movement_event_t event, movement_settings_t *settings, v } // blink up the parameter we're setting - if (event.subsecond % 2) { + if (event.subsecond % 2 && !_quick_ticks_running) { switch (current_page) { case 0: case 3: diff --git a/watch-library/hardware/watch/watch_buzzer.c b/watch-library/hardware/watch/watch_buzzer.c index 07303482..b999397a 100644 --- a/watch-library/hardware/watch/watch_buzzer.c +++ b/watch-library/hardware/watch/watch_buzzer.c @@ -23,13 +23,133 @@ */ #include "watch_buzzer.h" +#include "../../../watch-library/hardware/include/saml22j18a.h" +#include "../../../watch-library/hardware/include/component/tc.h" +#include "../../../watch-library/hardware/hri/hri_tc_l22.h" - inline void watch_enable_buzzer(void) { +void cb_watch_buzzer_seq(void); + +static uint16_t _seq_position; +static int8_t _tone_ticks, _repeat_counter; +static bool _callback_running = false; +static int8_t *_sequence; +static void (*_cb_finished)(void); + +static void _tcc_write_RUNSTDBY(bool value) { + // enables or disables RUNSTDBY of the tcc + hri_tcc_clear_CTRLA_ENABLE_bit(TCC0); + hri_tcc_write_CTRLA_RUNSTDBY_bit(TCC0, value); + hri_tcc_set_CTRLA_ENABLE_bit(TCC0); + hri_tcc_wait_for_sync(TCC0, TCC_SYNCBUSY_ENABLE); +} + +static inline void _tc3_start() { + // start the TC3 timer + hri_tc_set_CTRLA_ENABLE_bit(TC3); + _callback_running = true; +} + +static inline void _tc3_stop() { + // stop the TC3 timer + hri_tc_clear_CTRLA_ENABLE_bit(TC3); + hri_tc_wait_for_sync(TC3, TC_SYNCBUSY_ENABLE); + _callback_running = false; +} + +static void _tc3_initialize() { + // setup and initialize TC3 for a 64 Hz interrupt + hri_mclk_set_APBCMASK_TC3_bit(MCLK); + hri_gclk_write_PCHCTRL_reg(GCLK, TC3_GCLK_ID, GCLK_PCHCTRL_GEN_GCLK3 | GCLK_PCHCTRL_CHEN); + _tc3_stop(); + hri_tc_write_CTRLA_reg(TC3, TC_CTRLA_SWRST); + hri_tc_wait_for_sync(TC3, TC_SYNCBUSY_SWRST); + hri_tc_write_CTRLA_reg(TC3, TC_CTRLA_PRESCALER_DIV64 | + TC_CTRLA_MODE_COUNT8 | + TC_CTRLA_RUNSTDBY); + hri_tccount8_write_PER_reg(TC3, 7); // 32 Khz divided by 64 divided by 8 equals 64 Hz + hri_tc_set_INTEN_OVF_bit(TC3); + NVIC_ClearPendingIRQ(TC3_IRQn); + NVIC_EnableIRQ (TC3_IRQn); +} + +void watch_buzzer_play_sequence(int8_t *note_sequence, void (*callback_on_end)(void)) { + if (_callback_running) _tc3_stop(); + watch_set_buzzer_off(); + _sequence = note_sequence; + _cb_finished = callback_on_end; + _seq_position = 0; + _tone_ticks = 0; + _repeat_counter = -1; + // prepare buzzer + watch_enable_buzzer(); + // setup TC3 timer + _tc3_initialize(); + // TCC should run in standby mode + _tcc_write_RUNSTDBY(true); + // start the timer (for the 64 hz callback) + _tc3_start(); +} + +void cb_watch_buzzer_seq(void) { + // callback for reading the note sequence + if (_tone_ticks == 0) { + if (_sequence[_seq_position] < 0 && _sequence[_seq_position + 1]) { + // repeat indicator found + if (_repeat_counter == -1) { + // first encounter: load repeat counter + _repeat_counter = _sequence[_seq_position + 1]; + } else _repeat_counter--; + if (_repeat_counter > 0) + // rewind + if (_seq_position > _sequence[_seq_position] * -2) + _seq_position += _sequence[_seq_position] * 2; + else + _seq_position = 0; + else { + // continue + _seq_position += 2; + _repeat_counter = -1; + } + } + if (_sequence[_seq_position] && _sequence[_seq_position + 1]) { + // read note + BuzzerNote note = _sequence[_seq_position]; + if (note != BUZZER_NOTE_REST) { + watch_set_buzzer_period(NotePeriods[note]); + watch_set_buzzer_on(); + } else watch_set_buzzer_off(); + // set duration ticks and move to next tone + _tone_ticks = _sequence[_seq_position + 1]; + _seq_position += 2; + } else { + // end the sequence + watch_buzzer_abort_sequence(); + if (_cb_finished) _cb_finished(); + } + } else _tone_ticks--; +} + +void watch_buzzer_abort_sequence(void) { + // ends/aborts the sequence + if (_callback_running) _tc3_stop(); + watch_set_buzzer_off(); + // disable standby mode for TCC + _tcc_write_RUNSTDBY(false); +} + +void TC3_Handler(void) { + // interrupt handler vor TC3 (globally!) + cb_watch_buzzer_seq(); + TC3->COUNT8.INTFLAG.reg |= TC_INTFLAG_OVF; +} + +inline void watch_enable_buzzer(void) { if (!hri_tcc_get_CTRLA_reg(TCC0, TCC_CTRLA_ENABLE)) { _watch_enable_tcc(); } gpio_set_pin_direction(BUZZER, GPIO_DIRECTION_OUT); } + inline void watch_set_buzzer_period(uint32_t period) { hri_tcc_write_PERBUF_reg(TCC0, period); hri_tcc_write_CCBUF_reg(TCC0, WATCH_BUZZER_TCC_CHANNEL, period / 2); diff --git a/watch-library/shared/watch/watch_buzzer.h b/watch-library/shared/watch/watch_buzzer.h index 1b5d197c..7ba9a52e 100644 --- a/watch-library/shared/watch/watch_buzzer.h +++ b/watch-library/shared/watch/watch_buzzer.h @@ -160,5 +160,28 @@ void watch_buzzer_play_note(BuzzerNote note, uint16_t duration_ms); /// @brief An array of periods for all the notes on a piano, corresponding to the names in BuzzerNote. extern const uint16_t NotePeriods[108]; +/** @brief Plays the given sequence of notes in a non-blocking way. + * @param note_sequence A pointer to the sequence of buzzer note & duration tuples, ending with a zero. A simple + * RLE logic is implemented: a negative number instead of a buzzer note means that the sequence + * is rewound by the given number of notes. The byte following a negative number determines the number + * of loops. I.e. if you want to repeat the last three notes of the sequence one time, you should provide + * the tuple -3, 1. The repeated notes must not contain any other repeat markers, or you will end up with + * an eternal loop. + * @param callback_on_end A pointer to a callback function to be invoked when the sequence has finished playing. + * @note This function plays the sequence asynchronously, so the UI will not be blocked. + * Hint: It is not possible to play the lowest note BUZZER_NOTE_A1 (55.00 Hz). The note is represented by a + * zero byte, which is used here as the end-of-sequence marker. But hey, a frequency that low cannot be + * played properly by the watch's buzzer, anyway. + */ +void watch_buzzer_play_sequence(int8_t *note_sequence, void (*callback_on_end)(void)); + +/** @brief Aborts a playing sequence. + */ +void watch_buzzer_abort_sequence(void); + +#ifndef __EMSCRIPTEN__ +void TC3_Handler(void); +#endif + /// @} #endif diff --git a/watch-library/simulator/watch/watch_buzzer.c b/watch-library/simulator/watch/watch_buzzer.c index 1c95a96d..68d9a139 100644 --- a/watch-library/simulator/watch/watch_buzzer.c +++ b/watch-library/simulator/watch/watch_buzzer.c @@ -26,10 +26,86 @@ #include "watch_main_loop.h" #include <emscripten.h> +#include <emscripten/html5.h> static bool buzzer_enabled = false; static uint32_t buzzer_period; +void cb_watch_buzzer_seq(void *userData); + +static uint16_t _seq_position; +static int8_t _tone_ticks, _repeat_counter; +static long _em_interval_id = 0; +static int8_t *_sequence; +static void (*_cb_finished)(void); + +static inline void _em_interval_stop() { + emscripten_clear_interval(_em_interval_id); + _em_interval_id = 0; +} + +void watch_buzzer_play_sequence(int8_t *note_sequence, void (*callback_on_end)(void)) { + if (_em_interval_id) _em_interval_stop(); + watch_set_buzzer_off(); + _sequence = note_sequence; + _cb_finished = callback_on_end; + _seq_position = 0; + _tone_ticks = 0; + _repeat_counter = -1; + // prepare buzzer + watch_enable_buzzer(); + // initiate 64 hz callback + _em_interval_id = emscripten_set_interval(cb_watch_buzzer_seq, (double)(1000/64), (void *)NULL); +} + +void cb_watch_buzzer_seq(void *userData) { + // callback for reading the note sequence + (void) userData; + if (_tone_ticks == 0) { + if (_sequence[_seq_position] < 0 && _sequence[_seq_position + 1]) { + // repeat indicator found + if (_repeat_counter == -1) { + // first encounter: load repeat counter + _repeat_counter = _sequence[_seq_position + 1]; + } else _repeat_counter--; + if (_repeat_counter > 0) + // rewind + if (_seq_position > _sequence[_seq_position] * -2) + _seq_position += _sequence[_seq_position] * 2; + else + _seq_position = 0; + else { + // continue + _seq_position += 2; + _repeat_counter = -1; + } + } + if (_sequence[_seq_position] && _sequence[_seq_position + 1]) { + // read note + BuzzerNote note = _sequence[_seq_position]; + if (note == BUZZER_NOTE_REST) { + watch_set_buzzer_off(); + } else { + watch_set_buzzer_period(NotePeriods[note]); + watch_set_buzzer_on(); + } + // set duration ticks and move to next tone + _tone_ticks = _sequence[_seq_position + 1]; + _seq_position += 2; + } else { + // end the sequence + watch_buzzer_abort_sequence(); + if (_cb_finished) _cb_finished(); + } + } else _tone_ticks--; +} + +void watch_buzzer_abort_sequence(void) { + // ends/aborts the sequence + if (_em_interval_id) _em_interval_stop(); + watch_set_buzzer_off(); +} + void watch_enable_buzzer(void) { buzzer_enabled = true; buzzer_period = NotePeriods[BUZZER_NOTE_A4]; |