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
720 lines
23 KiB
C
720 lines
23 KiB
C
/*
|
|
* 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 <stdlib.h>
|
|
#include <string.h>
|
|
#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;
|
|
}
|