From 705e4be13a83a884dcc9e23d2c29fc362d514aa4 Mon Sep 17 00:00:00 2001 From: Konrad Rieck Date: Mon, 27 Jul 2026 21:44:55 +0200 Subject: [PATCH] Hydration watch face This watch face monitors daily hydration by tracking and logging water intake. In tracking mode: Display current water intake and percentage - Alarm button: +100ml (or configured glass size) - Light button: -100ml (or configured glass size) - Alarm long press: Display deviation from estimate - Light long press: Switch to settings mode - Alarm really long press: Switch to log mode In settings mode: Configure glass size, daily goal, wake time, sleep time, alert interval - Light button: Switch to next setting - Alarm button: Advance current setting - Alarm long press: Reset to default value - Mode button: Switch to tracking mode In log mode: Display log entries of water intake, date and deviation from goal - Alarm button: Switch to next log entry - Light button: Cycle through intake, date and deviation - Mode button: Switch to tracking mode Background tasks: - Automatic reset at wake time - Alert if intake below estimate at sleep time - Alert at interval if intake below estimate, waking the watch if asleep --- movement_faces.h | 1 + watch-faces.mk | 1 + watch-faces/complication/hydration_face.c | 719 ++++++++++++++++++++++ watch-faces/complication/hydration_face.h | 126 ++++ 4 files changed, 847 insertions(+) create mode 100644 watch-faces/complication/hydration_face.c create mode 100644 watch-faces/complication/hydration_face.h diff --git a/movement_faces.h b/movement_faces.h index 3c2f5b71..fd082b50 100644 --- a/movement_faces.h +++ b/movement_faces.h @@ -83,4 +83,5 @@ #include "tomato_face.h" #include "solar_time_face.h" #include "tide_face.h" +#include "hydration_face.h" // New includes go above this line. diff --git a/watch-faces.mk b/watch-faces.mk index c9e4a4f1..62f148f7 100644 --- a/watch-faces.mk +++ b/watch-faces.mk @@ -42,6 +42,7 @@ SRCS += \ ./watch-faces/clock/ke_decimal_time_face.c \ ./watch-faces/complication/baby_kicks_face.c \ ./watch-faces/complication/counter_face.c \ + ./watch-faces/complication/hydration_face.c \ ./watch-faces/complication/pulsometer_face.c \ ./watch-faces/complication/interval_face.c \ ./watch-faces/complication/timer_face.c \ diff --git a/watch-faces/complication/hydration_face.c b/watch-faces/complication/hydration_face.c new file mode 100644 index 00000000..89d87934 --- /dev/null +++ b/watch-faces/complication/hydration_face.c @@ -0,0 +1,719 @@ +/* + * MIT License + * + * Copyright (c) 2025 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. + */ + +#include +#include +#include "hydration_face.h" +#include "watch_slcd.h" +#include "watch_utility.h" + +/* Display frequency */ +#define DISPLAY_FREQUENCY 1 + +/* Maximum possible water intake (9.9l) */ +#define MAX_WATER_INTAKE 9900 + +/* Settings */ +#define NUM_SETTINGS 5 + +/* Default values */ +#define DEFAULT_WATER_GLASS_ML 100 +#define DEFAULT_WATER_GOAL_ML 1600 +#define DEFAULT_WAKE_HOUR 7 +#define DEFAULT_SLEEP_HOUR 22 +#define DEFAULT_ALERT_INTERVAL 2 + +static void _display_water_ml(uint16_t water_ml) +{ + char buf[10], *unit = "ml"; + if (watch_get_lcd_type() != WATCH_LCD_TYPE_CUSTOM) { + unit = "nl"; /* nl looks more like ml on the original LCD */ + } + + snprintf(buf, sizeof(buf), "%4d%s", water_ml, unit); + watch_display_text_with_fallback(WATCH_POSITION_BOTTOM, buf, buf); +} + +static void _settings_title_display(hydration_state_t *state, char *buf1, char *buf2) +{ + char buf[10]; + + watch_display_text_with_fallback(WATCH_POSITION_TOP, buf1, buf2); + if (watch_get_lcd_type() != WATCH_LCD_TYPE_CUSTOM) { + snprintf(buf, sizeof(buf), "%2d", state->settings_page + 1); + watch_display_text_with_fallback(WATCH_POSITION_TOP_RIGHT, buf, buf); + } + + if (state->alert_enabled) { + watch_set_indicator(WATCH_INDICATOR_SIGNAL); + } else { + watch_clear_indicator(WATCH_INDICATOR_SIGNAL); + } +} + +static bool _settings_blink(uint8_t subsecond) +{ + if (subsecond % 2 == 0) { + watch_display_text_with_fallback(WATCH_POSITION_BOTTOM, " ", " "); + return true; + } + return false; +} + +static void _settings_water_glass_display(void *context, uint8_t subsecond) +{ + hydration_state_t *state = (hydration_state_t *) context; + + _settings_title_display(state, "GLASS", "GL"); + if (_settings_blink(subsecond)) + return; + + /* First setting so clear colon and indicators */ + watch_clear_colon(); + watch_clear_indicator(WATCH_INDICATOR_24H); + watch_clear_indicator(WATCH_INDICATOR_PM); + + _display_water_ml(state->water_glass); +} + +static void _settings_water_glass_advance(void *context) +{ + hydration_state_t *state = (hydration_state_t *) context; + + state->water_glass += 100; + if (state->water_glass > 1000) { + state->water_glass = 100; + } +} + +static void _settings_water_goal_display(void *context, uint8_t subsecond) +{ + hydration_state_t *state = (hydration_state_t *) context; + + _settings_title_display(state, "GOAL ", "GO"); + if (_settings_blink(subsecond)) + return; + + _display_water_ml(state->water_goal); +} + +static void _settings_water_goal_advance(void *context) +{ + hydration_state_t *state = (hydration_state_t *) context; + + state->water_goal += state->water_glass; + if (state->water_goal > 3000) { + state->water_goal = 100; + } +} + +static void _settings_wake_time_display(void *context, uint8_t subsecond) +{ + char buf[10]; + hydration_state_t *state = (hydration_state_t *) context; + + _settings_title_display(state, "WAKE ", "WK"); + if (_settings_blink(subsecond)) + return; + + /* Start of time settings, set colon and check 24h indicator */ + watch_set_colon(); + + uint8_t hour = state->wake_hour; + if (movement_clock_mode_24h()) { + watch_set_indicator(WATCH_INDICATOR_24H); + } else { + if (state->wake_hour > 12) { + watch_set_indicator(WATCH_INDICATOR_PM); + hour = hour % 12; + } else { + watch_clear_indicator(WATCH_INDICATOR_PM); + } + } + + snprintf(buf, sizeof(buf), "%02d00", hour); + watch_display_text_with_fallback(WATCH_POSITION_BOTTOM, buf, buf); +} + +static void _settings_wake_time_advance(void *context) +{ + hydration_state_t *state = (hydration_state_t *) context; + + state->wake_hour += 1; + if (state->wake_hour >= 24) { + state->wake_hour = 0; + } +} + +static void _settings_sleep_time_display(void *context, uint8_t subsecond) +{ + char buf[10]; + hydration_state_t *state = (hydration_state_t *) context; + + _settings_title_display(state, "SLEEP", "SL"); + if (_settings_blink(subsecond)) + return; + + uint8_t hour = state->sleep_hour; + if (movement_clock_mode_24h()) { + watch_set_indicator(WATCH_INDICATOR_24H); + } else { + if (state->sleep_hour >= 12) { + watch_set_indicator(WATCH_INDICATOR_PM); + hour = hour % 12; + } else { + watch_clear_indicator(WATCH_INDICATOR_PM); + } + } + + snprintf(buf, sizeof(buf), "%02d00", hour); + watch_display_text_with_fallback(WATCH_POSITION_BOTTOM, buf, buf); +} + +static void _settings_sleep_time_advance(void *context) +{ + hydration_state_t *state = (hydration_state_t *) context; + + state->sleep_hour += 1; + if (state->sleep_hour >= 24) { + state->sleep_hour = 0; + } +} + +static void _settings_alert_interval_display(void *context, uint8_t subsecond) +{ + char buf[10]; + hydration_state_t *state = (hydration_state_t *) context; + + _settings_title_display(state, "INTER", "IN"); + if (_settings_blink(subsecond)) + return; + + /* No time setting so clear colon and indicators */ + watch_clear_colon(); + watch_clear_indicator(WATCH_INDICATOR_24H); + watch_clear_indicator(WATCH_INDICATOR_PM); + + if (state->alert_interval == 0) { + snprintf(buf, sizeof(buf), " off "); + } else { + snprintf(buf, sizeof(buf), " %2dh ", state->alert_interval); + } + + watch_display_text_with_fallback(WATCH_POSITION_BOTTOM, buf, buf); +} + +static void _settings_alert_interval_advance(void *context) +{ + hydration_state_t *state = (hydration_state_t *) context; + + state->alert_interval += 1; + if (state->alert_interval > 6) { + state->alert_interval = 0; + } +} + +static void _hydration_start_sleep_animation(void) +{ + if (!watch_sleep_animation_is_running()) { + watch_start_sleep_animation(500); + watch_start_indicator_blink_if_possible(WATCH_INDICATOR_COLON, 500); + } +} + +static void _hydration_stop_sleep_animation(void) +{ + if (watch_sleep_animation_is_running()) { + watch_stop_sleep_animation(); + watch_stop_blink(); + } +} + +/* Play beep sound */ +static inline void _beep() +{ + if (!movement_button_should_sound()) + return; + watch_buzzer_play_note(BUZZER_NOTE_C7, 50); +} + +/* Calculate expected intake */ +static uint16_t _get_expected_intake(hydration_state_t *state, uint8_t hours_since_wake) +{ + uint8_t day_hours = (state->sleep_hour + 24 - state->wake_hour) % 24; + uint16_t expected_intake = (state->water_goal * hours_since_wake) / day_hours; + return expected_intake; +} + +static void _log_intake(hydration_state_t *state, uint32_t ts) +{ + if (state->water_intake == 0) + return; + + state->log_head = (state->log_head + HYDRATION_LOG_ENTRIES - 1) % HYDRATION_LOG_ENTRIES; + hydration_log_t *log = &state->log[state->log_head]; + + /* Log entry (intake / 100) and date (ts / 86400) */ + log->water_intake = state->water_intake / 100; + log->date = ts / 86400; +} + +static void _tracking_display(hydration_state_t *state) +{ + char buf[4]; + + watch_display_text_with_fallback(WATCH_POSITION_TOP_LEFT, "HYDRA", "Hy"); + + if (state->alert_enabled) { + watch_set_indicator(WATCH_INDICATOR_SIGNAL); + } else { + watch_clear_indicator(WATCH_INDICATOR_SIGNAL); + } + + if (!state->display_deviation) { + /* Display current intake in bottom */ + _display_water_ml(state->water_intake); + + /* Display percentage in top right */ + uint8_t percent = (state->water_intake * 10) / state->water_goal; + snprintf(buf, sizeof(buf), "%2d", percent); + watch_display_text_with_fallback(WATCH_POSITION_TOP_RIGHT, buf, buf); + } else { + watch_date_time_t now = movement_get_local_date_time(); + uint16_t hours_since_wake = (now.unit.hour + 24 - state->wake_hour) % 24; + uint16_t expected_intake = _get_expected_intake(state, hours_since_wake); + int16_t deviation = state->water_intake - expected_intake; + + /* Display absolute deviation from expected intake */ + _display_water_ml(abs(deviation)); + + /* Display sign in top right */ + snprintf(buf, sizeof(buf), "%s", deviation >= 0 ? " +" : " -"); + watch_display_text_with_fallback(WATCH_POSITION_TOP_RIGHT, buf, buf); + } +} + +#define _log_next_entry(state) _log_find_entry(state, +1) +#define _log_prev_entry(state) _log_find_entry(state, -1) + +static uint8_t _log_find_entry(hydration_state_t *state, int direction) +{ + uint8_t start = state->log_index; + uint8_t step = (direction > 0) ? 1 : (HYDRATION_LOG_ENTRIES - 1); + uint8_t idx = (start + step) % HYDRATION_LOG_ENTRIES; + + while (idx != start) { + if (state->log[idx].date != 0) + return idx; + idx = (idx + step) % HYDRATION_LOG_ENTRIES; + } + // If no other non-empty entries found, just return current + return start; +} + +static void _log_display(hydration_state_t *state) +{ + char buf[10]; + int16_t deviation; + + watch_display_text_with_fallback(WATCH_POSITION_TOP_LEFT, "LOG", "LG"); + + hydration_log_t *log = &state->log[state->log_index]; + if (log->date == 0) { + watch_display_text_with_fallback(WATCH_POSITION_BOTTOM, "no dat", "no dat"); + return; + } + + /* Calculate position in ring buffer (1 = most recent) */ + uint8_t distance = (state->log_index - state->log_head + HYDRATION_LOG_ENTRIES) % HYDRATION_LOG_ENTRIES; + uint8_t index = distance + 1; + + switch (state->log_type) { + case HYDRATION_LOG_INTAKE: + snprintf(buf, sizeof(buf), "%2d", index); + watch_display_text_with_fallback(WATCH_POSITION_TOP_RIGHT, buf, buf); + _display_water_ml(log->water_intake * 100); + break; + case HYDRATION_LOG_DEVIATION: + deviation = log->water_intake * 100 - state->water_goal; + snprintf(buf, sizeof(buf), "%s", deviation >= 0 ? " +" : " -"); + watch_display_text_with_fallback(WATCH_POSITION_TOP_RIGHT, buf, buf); + _display_water_ml(abs(deviation)); + break; + case HYDRATION_LOG_DATE: + snprintf(buf, sizeof(buf), "%2d", index); + watch_display_text_with_fallback(WATCH_POSITION_TOP_RIGHT, buf, buf); + watch_date_time_t now = watch_utility_date_time_from_unix_time(log->date * 86400, 0); + snprintf(buf, sizeof(buf), "%02d%02d%02d", now.unit.day, now.unit.month, now.unit.year + 20); + watch_display_text_with_fallback(WATCH_POSITION_BOTTOM, buf, buf); + break; + } +} + +static void _switch_to_tracking(hydration_state_t *state) +{ + movement_request_tick_frequency(DISPLAY_FREQUENCY); + state->page = PAGE_HYDRATION_TRACKING; + + /* No colon or indicators in tracking mode */ + watch_clear_colon(); + watch_clear_indicator(WATCH_INDICATOR_24H); + watch_clear_indicator(WATCH_INDICATOR_PM); + + /* Display tracking data */ + _tracking_display(state); +} + +static void _switch_to_settings(hydration_state_t *state) +{ + movement_request_tick_frequency(4); + state->page = PAGE_HYDRATION_SETTINGS; + state->settings_page = HYDRATION_SETTING_WATER_GLASS; + + /* Display current settings */ + state->settings[state->settings_page].display(state, 0); +} + +static void _switch_to_log(hydration_state_t *state) +{ + movement_request_tick_frequency(DISPLAY_FREQUENCY); + state->page = PAGE_HYDRATION_LOG; + + /* Set index to entry before tail */ + state->log_index = state->log_head; + state->log_type = HYDRATION_LOG_INTAKE; + + /* Display log data */ + _log_display(state); +} + +static void _check_hydration_alert(hydration_state_t *state) +{ + watch_date_time_t now = movement_get_local_date_time(); + + /* Check if time is between sleep and wake */ + uint8_t hour = now.unit.hour; + if (state->sleep_hour == state->wake_hour) { + /* Disable alerts completely */ + return; + } else if (state->sleep_hour < state->wake_hour) { + /* Sleep time not over midnight */ + if (hour > state->sleep_hour && hour <= state->wake_hour) { + return; + } + } else { + /* Sleep time crosses midnight */ + if (hour > state->sleep_hour || hour <= state->wake_hour) { + return; + } + } + + /* Check for alert at sleep time */ + bool due = (hour == state->sleep_hour); + + /* Check for alert at interval */ + uint8_t hours_since_wake = (hour + 24 - state->wake_hour) % 24; + due |= (state->alert_interval > 0 && hours_since_wake % state->alert_interval == 0); + + if (!due) + return; + + uint16_t expected_intake = _get_expected_intake(state, hours_since_wake); + if (state->water_intake < expected_intake) { + /* This runs from the background task, regardless of which face is + * currently on screen, so request the wake here rather than relying + * on this face's own EVENT_LOW_ENERGY_UPDATE handler to catch it. */ + movement_request_wake(); + movement_move_to_face(state->face_index); + movement_play_alarm(); + } +} + +static bool _tracking_loop(movement_event_t event, void *context) +{ + hydration_state_t *state = (hydration_state_t *) context; + + switch (event.event_type) { + case EVENT_ACTIVATE: + watch_clear_colon(); + _tracking_display(state); + break; + case EVENT_TICK: + if (state->display_deviation > 0) { + state->display_deviation--; + } + _tracking_display(state); + break; + case EVENT_ALARM_BUTTON_UP: + /* Prevent overflow of water intake */ + if (state->water_intake + state->water_glass > MAX_WATER_INTAKE) { + state->water_intake = MAX_WATER_INTAKE; + } else { + state->water_intake += state->water_glass; + } + _tracking_display(state); + _beep(); + break; + case EVENT_LIGHT_BUTTON_UP: + /* Prevent underflow of water intake */ + if (state->water_intake - state->water_glass < 0) { + state->water_intake = 0; + } else { + state->water_intake -= state->water_glass; + } + _tracking_display(state); + _beep(); + break; + case EVENT_LIGHT_BUTTON_DOWN: + /* Do nothing. No light */ + break; + case EVENT_LIGHT_LONG_PRESS: + _switch_to_settings(state); + _beep(); + break; + case EVENT_ALARM_LONG_PRESS: + /* Display intake deviation for 2 seconds */ + state->display_deviation = 2; + _tracking_display(state); + break; + case EVENT_ALARM_REALLY_LONG_PRESS: + state->display_deviation = 0; + _switch_to_log(state); + _beep(); + break; + case EVENT_LOW_ENERGY_UPDATE: + _hydration_start_sleep_animation(); + break; + case EVENT_BACKGROUND_TASK: + _check_hydration_alert(state); + break; + case EVENT_TIMEOUT: + movement_move_to_face(0); + break; + default: + movement_default_loop_handler(event); + break; + } + + return true; +} + +static bool _log_loop(movement_event_t event, void *context) +{ + hydration_state_t *state = (hydration_state_t *) context; + + switch (event.event_type) { + case EVENT_ACTIVATE: + case EVENT_TICK: + _log_display(state); + break; + case EVENT_ALARM_BUTTON_UP: + state->log_index = _log_next_entry(state); + _log_display(state); + break; + case EVENT_LIGHT_BUTTON_UP: + state->log_type = (state->log_type + 1) % HYDRATION_LOG_TYPES; + _log_display(state); + _beep(); + break; + case EVENT_LIGHT_BUTTON_DOWN: + /* Do nothing. No light */ + break; + case EVENT_MODE_BUTTON_UP: + _switch_to_tracking(state); + _beep(); + break; + case EVENT_BACKGROUND_TASK: + _check_hydration_alert(state); + break; + case EVENT_TIMEOUT: + movement_move_to_face(0); + break; + default: + movement_default_loop_handler(event); + break; + } + return true; +} + +static bool _settings_loop(movement_event_t event, void *context) +{ + hydration_state_t *state = (hydration_state_t *) context; + + switch (event.event_type) { + case EVENT_ACTIVATE: + case EVENT_TICK: + state->settings[state->settings_page].display(context, event.subsecond); + break; + case EVENT_LIGHT_BUTTON_UP: + state->settings_page = (state->settings_page + 1) % NUM_SETTINGS; + state->settings[state->settings_page].display(context, event.subsecond); + break; + case EVENT_MODE_BUTTON_UP: + _switch_to_tracking(state); + _beep(); + break; + case EVENT_LIGHT_BUTTON_DOWN: + /* Do nothing. No light */ + break; + case EVENT_ALARM_BUTTON_UP: + /* Advance current settings */ + state->settings[state->settings_page].advance(context); + state->settings[state->settings_page].display(context, event.subsecond); + break; + case EVENT_ALARM_LONG_PRESS: + /* Reset to default value */ + switch (state->settings_page) { + case HYDRATION_SETTING_WATER_GLASS: + state->water_glass = DEFAULT_WATER_GLASS_ML; + break; + case HYDRATION_SETTING_WATER_GOAL: + state->water_goal = DEFAULT_WATER_GOAL_ML; + break; + case HYDRATION_SETTING_WAKE_TIME: + state->wake_hour = DEFAULT_WAKE_HOUR; + break; + case HYDRATION_SETTING_SLEEP_TIME: + state->sleep_hour = DEFAULT_SLEEP_HOUR; + break; + case HYDRATION_SETTING_ALERT_INTERVAL: + state->alert_interval = DEFAULT_ALERT_INTERVAL; + break; + } + state->settings[state->settings_page].display(context, event.subsecond); + break; + case EVENT_LIGHT_LONG_PRESS: + state->alert_enabled = !state->alert_enabled; + state->settings[state->settings_page].display(context, event.subsecond); + _beep(); + break; + case EVENT_BACKGROUND_TASK: + _check_hydration_alert(state); + break; + case EVENT_TIMEOUT: + movement_move_to_face(0); + break; + default: + movement_default_loop_handler(event); + break; + } + return true; +} + +void hydration_face_setup(uint8_t watch_face_index, void **context_ptr) +{ + (void) watch_face_index; + if (*context_ptr != NULL) + return; /* Skip setup if context available */ + + *context_ptr = malloc(sizeof(hydration_state_t)); + memset(*context_ptr, 0, sizeof(hydration_state_t)); + + hydration_state_t *state = (hydration_state_t *) * context_ptr; + + /* Default setup */ + state->water_glass = DEFAULT_WATER_GLASS_ML; + state->water_goal = DEFAULT_WATER_GOAL_ML; + state->wake_hour = DEFAULT_WAKE_HOUR; + state->sleep_hour = DEFAULT_SLEEP_HOUR; + state->alert_interval = DEFAULT_ALERT_INTERVAL; + + /* Initialize settings */ + uint8_t settings_page = 0; + state->settings = malloc(NUM_SETTINGS * sizeof(hydration_settings_t)); + state->settings[settings_page].display = _settings_water_glass_display; + state->settings[settings_page].advance = _settings_water_glass_advance; + settings_page++; + state->settings[settings_page].display = _settings_water_goal_display; + state->settings[settings_page].advance = _settings_water_goal_advance; + settings_page++; + state->settings[settings_page].display = _settings_wake_time_display; + state->settings[settings_page].advance = _settings_wake_time_advance; + settings_page++; + state->settings[settings_page].display = _settings_sleep_time_display; + state->settings[settings_page].advance = _settings_sleep_time_advance; + settings_page++; + state->settings[settings_page].display = _settings_alert_interval_display; + state->settings[settings_page].advance = _settings_alert_interval_advance; + settings_page++; + + /* Store face index for background tasks */ + state->face_index = watch_face_index; +} + +void hydration_face_activate(void *context) +{ + hydration_state_t *state = (hydration_state_t *) context; + + _hydration_stop_sleep_animation(); + + /* Switch to tracking page. */ + _switch_to_tracking(state); +} + +bool hydration_face_loop(movement_event_t event, void *context) +{ + hydration_state_t *state = (hydration_state_t *) context; + + switch (state->page) { + default: + case PAGE_HYDRATION_TRACKING: + return _tracking_loop(event, context); + case PAGE_HYDRATION_SETTINGS: + return _settings_loop(event, context); + case PAGE_HYDRATION_LOG: + return _log_loop(event, context); + } +} + +void hydration_face_resign(void *context) +{ + (void) context; +} + +movement_watch_face_advisory_t hydration_face_advise(void *context) +{ + hydration_state_t *state = (hydration_state_t *) context; + movement_watch_face_advisory_t retval = { 0 }; + + watch_date_time_t now = movement_get_local_date_time(); + + /* Daily reset at wake time and log entry */ + if (now.unit.hour == state->wake_hour && now.unit.minute == 0) { + uint32_t ts = watch_utility_date_time_to_unix_time(now, 0); + /* Correct date if midnight between sleep and wake time */ + if (state->wake_hour < state->sleep_hour) + ts -= 24 * 60 * 60; + _log_intake(state, ts); + /* Reset water intake */ + state->water_intake = 0; + } + + /* Check for alert at every hour */ + if (state->alert_enabled && now.unit.minute == 0) { + retval.wants_background_task = true; + } + + return retval; +} diff --git a/watch-faces/complication/hydration_face.h b/watch-faces/complication/hydration_face.h new file mode 100644 index 00000000..80dd7b74 --- /dev/null +++ b/watch-faces/complication/hydration_face.h @@ -0,0 +1,126 @@ +/* + * MIT License + * + * Copyright (c) 2025 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. + */ + +/* + * This watch face monitors daily hydration by tracking and logging water intake. + * + * In tracking mode: Display current water intake and percentage + * - Alarm button: +100ml (or configured glass size) + * - Light button: -100ml (or configured glass size) + * - Alarm long press: Display deviation from estimate + * - Light long press: Switch to settings mode + * - Alarm really long press: Switch to log mode + * + * In settings mode: Configure glass size, daily goal, wake time, sleep time, alert interval + * - Light button: Switch to next setting + * - Alarm button: Advance current setting + * - Alarm long press: Reset to default value + * - Mode button: Switch to tracking mode + + * In log mode: Display log entries of water intake, date and deviation from goal + * - Alarm button: Switch to next log entry + * - Light button: Cycle through intake, date and deviation + * - Mode button: Switch to tracking mode + * + * Background tasks: + * - Automatic reset at wake time + * - Alert if intake below estimate at sleep time + * - Alert at interval if intake below estimate + * + */ + +#include "movement.h" + +/* Number of log entries */ +#define HYDRATION_LOG_ENTRIES 30 + +/* Number of log types */ +#define HYDRATION_LOG_TYPES 3 + +typedef enum { + PAGE_HYDRATION_TRACKING, + PAGE_HYDRATION_SETTINGS, + PAGE_HYDRATION_LOG +} hydration_page_t; + +typedef enum { + HYDRATION_SETTING_WATER_GLASS, + HYDRATION_SETTING_WATER_GOAL, + HYDRATION_SETTING_WAKE_TIME, + HYDRATION_SETTING_SLEEP_TIME, + HYDRATION_SETTING_ALERT_INTERVAL, +} hydration_setting_t; + +typedef enum { + HYDRATION_LOG_INTAKE, + HYDRATION_LOG_DATE, + HYDRATION_LOG_DEVIATION, +} hydration_log_type_t; + +typedef struct { + void (*display)(void *, uint8_t); + void (*advance)(void *); +} hydration_settings_t; + +typedef struct { + uint8_t water_intake; /* Logged intake (intake / 100) */ + uint16_t date; /* Logged date (ts / 86400) */ +} hydration_log_t; + +typedef struct { + uint16_t water_intake; /* Current water intake in ml */ + uint16_t water_glass; /* Glass size in ml (default 100ml) */ + uint16_t water_goal; /* Target daily water intake (default 1600ml) */ + uint8_t wake_hour; /* Hour for wake time (default 7) */ + uint8_t sleep_hour; /* Hour for sleep time (default 22) */ + uint8_t alert_interval; /* Alert interval in hours (default 2) */ + uint8_t face_index; /* Face index */ + uint8_t display_deviation; /* Display deviation from estimate */ + bool alert_enabled; /* Alert enabled */ + hydration_page_t page; /* Current page */ + + /* Log */ + hydration_log_t log[HYDRATION_LOG_ENTRIES]; /* Log entries */ + hydration_log_type_t log_type; /* Log type */ + uint8_t log_index; /* Log index */ + uint8_t log_head; /* Log head */ + + /* Settings */ + hydration_setting_t settings_page; /* Current settings page */ + hydration_settings_t *settings; /* Settings configuration */ +} hydration_state_t; + +void hydration_face_setup(uint8_t watch_face_index, void **context_ptr); +void hydration_face_activate(void *context); +bool hydration_face_loop(movement_event_t event, void *context); +void hydration_face_resign(void *context); +movement_watch_face_advisory_t hydration_face_advise(void *context); + +#define hydration_face ((const watch_face_t){ \ + hydration_face_setup, \ + hydration_face_activate, \ + hydration_face_loop, \ + hydration_face_resign, \ + hydration_face_advise, \ +})