/* * MIT License * * Copyright (c) 2023-2024 Konrad Rieck * * 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. */ /* * # Deadline Face * * This is a watch face for tracking deadlines. It draws inspiration from * other watch faces of the project but focuses on keeping track of * deadlines. You can enter and monitor up to four different deadlines by * providing their respective date and time. The face has two modes: * *running mode* and *settings mode*. * * ## Running Mode * * When the watch face is activated, it defaults to running mode. The top * right corner shows the current deadline number, and the main display * presents the time left until the deadline. The format of the display * varies depending on the remaining time. * * - When less than a day is left, the display shows the remaining hours, * minutes, and seconds in the form `HH:MM:SS`. * * - When less than a month is left, the display shows the remaining days * and hours in the form `DD:HH` with the unit `dy` for days. * * - When less than a year is left, the display shows the remaining months * and days in the form `MM:DD` with the unit `mo` for months. * * - When more than a year is left, the years and months are displayed in * the form `YY:MM` with the unit `yr` for years. * * - When a deadline has passed in the last 24 hours, the display shows * `over` to indicate that the deadline has just recently been reached. * * - When no deadline is set for a particular slot, or if a deadline has * already passed by more than 24 hours, `--:--` is displayed. * * The user can navigate in running mode using the following buttons: * * - The *alarm button* moves the next deadline. There are currently four * slots available for deadlines. When the last slot has been reached, * pressing the button moves to the first slot. * * - A *long press* on the *alarm button* activates settings mode and * enables configuring the currently selected deadline. * * - A *long press* on the *light button* activates a deadline alarm. The * bell icon is displayed, and the alarm will ring upon reaching any of * the deadlines set. It is important to note that the watch will not * enter low-energy sleep mode while the alarm is enabled. * * * ## Settings Mode * * In settings mode, the currently selected slot for a deadline can be * configured by providing the date and the time. Like running mode, the * top right corner of the display indicates the current deadline number. * The main display shows the date and, on the next page, the time to be * configured. * * The user can use the following buttons in settings mode. * * - The *light button* navigates through the different date and time * settings, going from year, month, day, hour, to minute. The selected * position is blinking. * * - A *long press* on the light button resets the date and time to the next * day at midnight. This is the default deadline. * * - The *alarm button* increments the currently selected position. A *long * press* on the *alarm button* changes the value faster. * * - The *mode button* exists setting mode and returns to *running mode*. * Here the selected deadline slot can be changed. * */ #include #include #include "deadline_face.h" #include "watch.h" #include "watch_utility.h" #define SETTINGS_NUM (5) const char settings_titles[SETTINGS_NUM][3] = { "YR", "MO", "DA", "HR", "M1" }; /* Local functions */ static void _running_init(movement_settings_t *settings, deadline_state_t *state); static bool _running_loop(movement_event_t event, movement_settings_t *settings, void *context); static void _running_display(movement_event_t event, movement_settings_t *settings, deadline_state_t *state); static void _setting_init(movement_settings_t *settings, deadline_state_t *state); static bool _setting_loop(movement_event_t event, movement_settings_t *settings, void *context); static void _setting_display(movement_event_t event, movement_settings_t *settings, deadline_state_t *state, watch_date_time date); /* Utility functions */ static void _background_alarm_play(movement_settings_t *settings, deadline_state_t *state); static void _background_alarm_schedule(movement_settings_t *settings, deadline_state_t *state); static void _background_alarm_cancel(movement_settings_t *settings, deadline_state_t *state); static void _increment_date(movement_settings_t *settings, deadline_state_t *state, watch_date_time date_time); static inline int32_t _get_tz_offset(movement_settings_t *settings); static inline void _change_tick_freq(uint8_t freq, deadline_state_t *state); static inline bool _is_leap(int16_t y); static inline int _days_in_month(int16_t mpnth, int16_t y); static inline unsigned int _mod(int a, int b); static inline void _beep_button(movement_settings_t *settings); static inline void _beep_enable(movement_settings_t *settings); static inline void _beep_disable(movement_settings_t *settings); static inline void _reset_deadline(movement_settings_t *settings, deadline_state_t *state); /* Check for leap year */ static inline bool _is_leap(int16_t y) { y += 1900; return !(y % 4) && ((y % 100) || !(y % 400)); } /* Modulo function */ static inline unsigned int _mod(int a, int b) { int r = a % b; return r < 0 ? r + b : r; } /* Return days in month */ static inline int _days_in_month(int16_t month, int16_t year) { uint8_t days[] = { 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 }; month = _mod(month - 1, 12); if (month == 1 && _is_leap(year)) { return days[month] + 1; } else { return days[month]; } } /* Return time zone offset */ static inline int32_t _get_tz_offset(movement_settings_t *settings) { return movement_timezone_offsets[settings->bit.time_zone] * 60; } /* Beep for a button press*/ static inline void _beep_button(movement_settings_t *settings) { // Play a beep as confirmation for a button press (if applicable) if (!settings->bit.button_should_sound) return; watch_buzzer_play_note(BUZZER_NOTE_C7, 50); } /* Beep for entering settings */ static inline void _beep_enable(movement_settings_t *settings) { if (!settings->bit.button_should_sound) return; watch_buzzer_play_note(BUZZER_NOTE_G7, 50); watch_buzzer_play_note(BUZZER_NOTE_REST, 75); watch_buzzer_play_note(BUZZER_NOTE_C8, 75); } /* Beep for leaving settings */ static inline void _beep_disable(movement_settings_t *settings) { if (!settings->bit.button_should_sound) return; watch_buzzer_play_note(BUZZER_NOTE_C8, 50); watch_buzzer_play_note(BUZZER_NOTE_REST, 75); watch_buzzer_play_note(BUZZER_NOTE_G7, 75); } /* Change tick frequency */ static inline void _change_tick_freq(uint8_t freq, deadline_state_t *state) { if (state->tick_freq != freq) { movement_request_tick_frequency(freq); state->tick_freq = freq; } } /* Determine index of closest deadline */ static uint8_t _closest_deadline(movement_settings_t *settings, deadline_state_t *state) { watch_date_time now = watch_rtc_get_date_time(); uint32_t now_ts = watch_utility_date_time_to_unix_time(now, _get_tz_offset(settings)); uint32_t min_ts = UINT32_MAX; uint8_t min_index = 0; for (uint8_t i = 0; i < DEADLINE_FACE_DATES; i++) { if (state->deadlines[i] < now_ts || state->deadlines[i] > min_ts) continue; min_ts = state->deadlines[i]; min_index = i; } return min_index; } /* Play background alarm */ static void _background_alarm_play(movement_settings_t *settings, deadline_state_t *state) { (void) settings; /* Use the default alarm from movement and move to foreground */ if (state->alarm_enabled) { movement_play_alarm(); movement_move_to_face(state->face_idx); } } /* Schedule background alarm */ static void _background_alarm_schedule(movement_settings_t *settings, deadline_state_t *state) { /* We simply re-use the scheduling in the background task */ deadline_face_wants_background_task(settings, state); } /* Cancel background alarm */ static void _background_alarm_cancel(movement_settings_t *settings, deadline_state_t *state) { (void) settings; movement_cancel_background_task_for_face(state->face_idx); } /* Reset deadline to tomorrow */ static inline void _reset_deadline(movement_settings_t *settings, deadline_state_t *state) { /* Get current time and reset hours/minutes/seconds */ watch_date_time date_time = watch_rtc_get_date_time(); date_time.unit.second = 0; date_time.unit.minute = 0; date_time.unit.hour = 0; /* Add 24 hours to obtain first second of tomorrow */ uint32_t ts = watch_utility_date_time_to_unix_time(date_time, _get_tz_offset(settings)); ts += 24 * 60 * 60; state->deadlines[state->current_index] = ts; } /* Increment date in settings mode. Function taken from `set_time_face.c` */ static void _increment_date(movement_settings_t *settings, deadline_state_t *state, watch_date_time date_time) { const uint8_t days_in_month[12] = { 31, 28, 31, 30, 31, 30, 30, 31, 30, 31, 30, 31 }; switch (state->current_page) { case 0: /* Only 10 years covered. Fix this sometime next decade */ date_time.unit.year = ((date_time.unit.year % 10) + 1); break; case 1: date_time.unit.month = (date_time.unit.month % 12) + 1; break; case 2: date_time.unit.day = date_time.unit.day + 1; /* Check for leap years */ int8_t days = days_in_month[date_time.unit.month - 1]; if (date_time.unit.month == 2 && _is_leap(date_time.unit.year)) days++; if (date_time.unit.day > days) date_time.unit.day = 1; break; case 3: date_time.unit.hour = (date_time.unit.hour + 1) % 24; break; case 4: date_time.unit.minute = (date_time.unit.minute + 1) % 60; break; } uint32_t ts = watch_utility_date_time_to_unix_time(date_time, _get_tz_offset(settings)); state->deadlines[state->current_index] = ts; } /* Update display in running mode */ static void _running_display(movement_event_t event, movement_settings_t *settings, deadline_state_t *state) { (void) event; (void) settings; /* Seconds, minutes, hours, days, months, years */ int16_t unit[] = { 0, 0, 0, 0, 0, 0 }; uint8_t i, range[] = { 60, 60, 24, 30, 12, 0 }; char buf[16]; /* Display indicators */ if (state->alarm_enabled) watch_set_indicator(WATCH_INDICATOR_BELL); else watch_clear_indicator(WATCH_INDICATOR_BELL); watch_date_time now = watch_rtc_get_date_time(); uint32_t now_ts = watch_utility_date_time_to_unix_time(now, _get_tz_offset(settings)); /* Deadline expired */ if (state->deadlines[state->current_index] < now_ts) { if (state->deadlines[state->current_index] + 24 * 60 * 60 > now_ts) sprintf(buf, "DL%2dOVER ", state->current_index + 1); else sprintf(buf, "DL%2d---- ", state->current_index + 1); //watch_clear_indicator(WATCH_INDICATOR_BELL); watch_display_string(buf, 0); return; } /* Get date time structs */ watch_date_time deadline = watch_utility_date_time_from_unix_time(state->deadlines[state->current_index], _get_tz_offset(settings) ); /* Calculate naive difference of dates */ unit[0] = deadline.unit.second - now.unit.second; unit[1] = deadline.unit.minute - now.unit.minute; unit[2] = deadline.unit.hour - now.unit.hour; unit[3] = deadline.unit.day - now.unit.day; unit[4] = deadline.unit.month - now.unit.month; unit[5] = deadline.unit.year - now.unit.year; /* Correct errors of naive difference */ for (i = 0; i < 6; i++) { if (unit[i] < 0) { /* Correct remaining units */ if (i == 3) unit[i] += _days_in_month(deadline.unit.month - 1, deadline.unit.year); else unit[i] += range[i]; /* Carry over change to next unit if non-zero */ if (i < 5 && unit[i + 1] != 0) unit[i + 1] -= 1; } } /* Set range */ i = state->current_index + 1; if (unit[5] > 0) { /* years:months */ sprintf(buf, "DL%2d%02d%02dYR", i, unit[5] % 100, unit[4] % 12); } else if (unit[4] > 0) { /* months:days */ sprintf(buf, "DL%2d%02d%02dMO", i, (unit[5] * 12 + unit[4]) % 100, unit[3] % 32); } else if (unit[3] > 0) { /* days:hours */ sprintf(buf, "DL%2d%02d%02ddY", i, unit[3] % 32, unit[2] % 24); } else { /* hours:minutes:seconds */ sprintf(buf, "DL%2d%02d%02d%02d", i, unit[2] % 24, unit[1] % 60, unit[0] % 60); } watch_display_string(buf, 0); } /* Init running mode */ static void _running_init(movement_settings_t *settings, deadline_state_t *state) { (void) settings; (void) state; watch_clear_indicator(WATCH_INDICATOR_24H); watch_clear_indicator(WATCH_INDICATOR_PM); watch_set_colon(); /* Ensure 1Hz updates only */ _change_tick_freq(1, state); } /* Loop of running mode */ static bool _running_loop(movement_event_t event, movement_settings_t *settings, void *context) { deadline_state_t *state = (deadline_state_t *) context; if (event.event_type != EVENT_BACKGROUND_TASK) _running_display(event, settings, state); switch (event.event_type) { case EVENT_ALARM_BUTTON_UP: _beep_button(settings); state->current_index = (state->current_index + 1) % DEADLINE_FACE_DATES; _running_display(event, settings, state); break; case EVENT_ALARM_LONG_PRESS: _beep_enable(settings); _setting_init(settings, state); state->mode = DEADLINE_FACE_SETTING; break; case EVENT_MODE_BUTTON_UP: movement_move_to_next_face(); return false; case EVENT_LIGHT_BUTTON_DOWN: break; case EVENT_LIGHT_LONG_PRESS: _beep_button(settings); state->alarm_enabled = !state->alarm_enabled; if (state->alarm_enabled) { _background_alarm_schedule(settings, context); } else { _background_alarm_cancel(settings, context); } _running_display(event, settings, state); break; case EVENT_TIMEOUT: movement_move_to_face(0); break; case EVENT_BACKGROUND_TASK: _background_alarm_play(settings, state); break; case EVENT_LOW_ENERGY_UPDATE: break; default: return movement_default_loop_handler(event, settings); } return true; } /* Update display in settings mode */ static void _setting_display(movement_event_t event, movement_settings_t *settings, deadline_state_t *state, watch_date_time date_time) { char buf[11]; int i = state->current_index + 1; if (state->current_page > 2) { watch_set_colon(); if (settings->bit.clock_mode_24h) { watch_set_indicator(WATCH_INDICATOR_24H); sprintf(buf, "%s%2d%2d%02d ", settings_titles[state->current_page], i, date_time.unit.hour, date_time.unit.minute); } else { sprintf(buf, "%s%2d%2d%02d ", settings_titles[state->current_page], i, (date_time.unit.hour % 12) ? (date_time.unit.hour % 12) : 12, date_time.unit.minute); if (date_time.unit.hour < 12) watch_clear_indicator(WATCH_INDICATOR_PM); else watch_set_indicator(WATCH_INDICATOR_PM); } } else { watch_clear_colon(); watch_clear_indicator(WATCH_INDICATOR_24H); watch_clear_indicator(WATCH_INDICATOR_PM); sprintf(buf, "%s%2d%2d%02d%02d", settings_titles[state->current_page], i, date_time.unit.year + 20, date_time.unit.month, date_time.unit.day); } /* Blink up the parameter we are setting */ if (event.subsecond % 2) { switch (state->current_page) { case 0: case 3: buf[4] = buf[5] = ' '; break; case 1: case 4: buf[6] = buf[7] = ' '; break; case 2: buf[8] = buf[9] = ' '; break; } } watch_display_string(buf, 0); } /* Init setting mode */ static void _setting_init(movement_settings_t *settings, deadline_state_t *state) { state->current_page = 0; /* Init fresh deadline to next day */ if (state->deadlines[state->current_index] == 0) { _reset_deadline(settings, state); } /* Ensure 1Hz updates only */ _change_tick_freq(1, state); } /* Loop of setting mode */ static bool _setting_loop(movement_event_t event, movement_settings_t *settings, void *context) { deadline_state_t *state = (deadline_state_t *) context; watch_date_time date_time; date_time = watch_utility_date_time_from_unix_time(state->deadlines[state->current_index], _get_tz_offset(settings)); if (event.event_type != EVENT_BACKGROUND_TASK) _setting_display(event, settings, state, date_time); switch (event.event_type) { case EVENT_TICK: if (state->tick_freq == 8) { if (watch_get_pin_level(BTN_ALARM)) { _increment_date(settings, state, date_time); _setting_display(event, settings, state, date_time); } else { _change_tick_freq(4, state); } } break; case EVENT_ALARM_LONG_PRESS: _change_tick_freq(8, state); break; case EVENT_ALARM_LONG_UP: _change_tick_freq(4, state); break; case EVENT_LIGHT_LONG_PRESS: _beep_button(settings); _reset_deadline(settings, state); break; case EVENT_LIGHT_BUTTON_DOWN: break; case EVENT_LIGHT_BUTTON_UP: state->current_page = (state->current_page + 1) % SETTINGS_NUM; _setting_display(event, settings, state, date_time); break; case EVENT_ALARM_BUTTON_UP: _change_tick_freq(4, state); _increment_date(settings, state, date_time); _setting_display(event, settings, state, date_time); break; case EVENT_TIMEOUT: _beep_button(settings); _background_alarm_schedule(settings, context); _change_tick_freq(1, state); state->mode = DEADLINE_FACE_RUNNING; movement_move_to_face(0); break; case EVENT_MODE_BUTTON_UP: _beep_disable(settings); _background_alarm_schedule(settings, context); _running_init(settings, state); _running_display(event, settings, state); state->mode = DEADLINE_FACE_RUNNING; break; case EVENT_BACKGROUND_TASK: _background_alarm_play(settings, state); break; default: return movement_default_loop_handler(event, settings); } return true; } /* Setup face */ void deadline_face_setup(movement_settings_t *settings, uint8_t watch_face_index, void **context_ptr) { (void) settings; (void) watch_face_index; if (*context_ptr != NULL) return; /* Skip setup if context available */ /* Allocate state */ *context_ptr = malloc(sizeof(deadline_state_t)); memset(*context_ptr, 0, sizeof(deadline_state_t)); /* Store face index for background tasks */ deadline_state_t *state = (deadline_state_t *) *context_ptr; state->face_idx = watch_face_index; } /* Activate face */ void deadline_face_activate(movement_settings_t *settings, void *context) { (void) settings; deadline_state_t *state = (deadline_state_t *) context; /* Set display options */ _running_init(settings, state); state->mode = DEADLINE_FACE_RUNNING; state->current_index = _closest_deadline(settings, state); } /* Loop face */ bool deadline_face_loop(movement_event_t event, movement_settings_t *settings, void *context) { (void) settings; deadline_state_t *state = (deadline_state_t *) context; switch (state->mode) { case DEADLINE_FACE_SETTING: _setting_loop(event, settings, context); break; default: case DEADLINE_FACE_RUNNING: _running_loop(event, settings, context); break; } return true; } /* Resign face */ void deadline_face_resign(movement_settings_t *settings, void *context) { (void) settings; (void) context; } /* Want background task */ bool deadline_face_wants_background_task(movement_settings_t *settings, void *context) { deadline_state_t *state = (deadline_state_t *) context; if (!state->alarm_enabled) return false; /* Determine closest deadline */ watch_date_time now = watch_rtc_get_date_time(); uint32_t now_ts = watch_utility_date_time_to_unix_time(now, _get_tz_offset(settings)); uint32_t next_ts = state->deadlines[_closest_deadline(settings, state)]; /* No active deadline */ if (next_ts < now_ts) return false; /* No deadline within next 60 seconds */ if (next_ts >= now_ts + 60) return false; /* Deadline within next minute. Let's set up an alarm */ watch_date_time next = watch_utility_date_time_from_unix_time(next_ts, _get_tz_offset(settings)); movement_request_wake(); movement_schedule_background_task_for_face(state->face_idx, next); return false; }