From 55534a9a8f14249d91a7c57799a2a2ed21057b29 Mon Sep 17 00:00:00 2001 From: Konrad Rieck Date: Thu, 2 Oct 2025 21:58:16 +0200 Subject: [PATCH] Port of world_clock2 to second movement. Improvements: - The name of the time zone is displayed for a brief movement when cycling through the selected time zones. - The face now comes with pre-selected zones to show-case its functionality: Seattle, New York, UTC, Shanghai, and Tokyo. --- movement_faces.h | 1 + watch-faces.mk | 1 + watch-faces/clock/world_clock2_face.c | 438 ++++++++++++++++++++++++++ watch-faces/clock/world_clock2_face.h | 134 ++++++++ 4 files changed, 574 insertions(+) create mode 100644 watch-faces/clock/world_clock2_face.c create mode 100644 watch-faces/clock/world_clock2_face.h diff --git a/movement_faces.h b/movement_faces.h index fc43716d9..42c6fb4cb 100644 --- a/movement_faces.h +++ b/movement_faces.h @@ -73,4 +73,5 @@ #include "wareki_face.h" #include "deadline_face.h" #include "wordle_face.h" +#include "world_clock2_face.h" // New includes go above this line. diff --git a/watch-faces.mk b/watch-faces.mk index 22e262cff..d2ba9e0a5 100644 --- a/watch-faces.mk +++ b/watch-faces.mk @@ -48,4 +48,5 @@ SRCS += \ ./watch-faces/sensor/lis2dw_monitor_face.c \ ./watch-faces/complication/wareki_face.c \ ./watch-faces/complication/deadline_face.c \ + ./watch-faces/clock/world_clock2_face.c \ # New watch faces go above this line. diff --git a/watch-faces/clock/world_clock2_face.c b/watch-faces/clock/world_clock2_face.c new file mode 100644 index 000000000..301a5c502 --- /dev/null +++ b/watch-faces/clock/world_clock2_face.c @@ -0,0 +1,438 @@ +/* + * MIT License + * + * Copyright (c) 2023-2025 Konrad Rieck + * Copyright (c) 2022 Joey Castillo + * + * 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 "world_clock2_face.h" +#include "watch_utility.h" +#include "watch_common_display.h" +#include "watch.h" +#include "zones.h" + +static bool refresh_face; + +/* Beep types */ +typedef enum { + BEEP_BUTTON, + BEEP_ENABLE, + BEEP_DISABLE +} beep_type_t; + +/* Simple macros for navigation */ +#define FORWARD +1 +#define BACKWARD -1 + +/* Constants */ +#define UTC_ZONE_INDEX 15 +#define NAME_DISPLAY_TIME 2 + +/* Pre-selected zones: Seattle, New York, UTC, Shanghai, Tokyo */ +#define SELECTED_ZONES {3, 8, 15, 32, 36, -1} + +/* Modulo function */ +static inline unsigned int mod(int a, int b) +{ + int r = a % b; + return r < 0 ? r + b : r; +} + +/* Convert watch date time to udatetime (Copied from movement.c) */ +static udatetime_t _movement_convert_date_time_to_udate(watch_date_time_t dt) +{ + /* *INDENT-OFF* */ + return (udatetime_t) { + .date.dayofmonth = dt.unit.day, + .date.dayofweek = dayofweek(UYEAR_FROM_YEAR(dt.unit.year + WATCH_RTC_REFERENCE_YEAR), dt.unit.month, dt.unit.day), + .date.month = dt.unit.month,.date.year = UYEAR_FROM_YEAR(dt.unit.year + WATCH_RTC_REFERENCE_YEAR), + .time.hour = dt.unit.hour, + .time.minute = dt.unit.minute, + .time.second = dt.unit.second + }; + /* *INDENT-ON* */ +} + +/* Find the next selected time zone */ +static inline uint8_t _next_selected_zone(world_clock2_state_t *state, int direction) +{ + uint8_t i = state->current_zone; + while (true) { + i = mod(i + direction, NUM_ZONE_NAMES); + /* Return next selected zone */ + if (state->zones[i].selected) { + return i; + } + /* Could not find a selected zone. Return UTC */ + if (i == state->current_zone) { + return UTC_ZONE_INDEX; + } + } +} + +/* Play beep sound based on type */ +static inline void _beep(beep_type_t beep_type) +{ + if (!movement_button_should_sound()) + return; + + switch (beep_type) { + case BEEP_BUTTON: + watch_buzzer_play_note(BUZZER_NOTE_C7, 50); + break; + + case BEEP_ENABLE: + watch_buzzer_play_note(BUZZER_NOTE_G7, 50); + watch_buzzer_play_note(BUZZER_NOTE_REST, 75); + watch_buzzer_play_note(BUZZER_NOTE_C8, 75); + break; + + case BEEP_DISABLE: + watch_buzzer_play_note(BUZZER_NOTE_C8, 50); + watch_buzzer_play_note(BUZZER_NOTE_REST, 75); + watch_buzzer_play_note(BUZZER_NOTE_G7, 75); + break; + } +} + +/* Display zone abbreviation on top */ +static void _display_zone_abbr(world_clock2_state_t *state, const char *abbr) +{ + char buf[11]; + + if (watch_get_lcd_type() == WATCH_LCD_TYPE_CUSTOM) { + /* Long abbreviation on custom LCD */ + sprintf(buf, "%-5s", abbr); + watch_display_text_with_fallback(WATCH_POSITION_TOP, buf, buf); + } else { + /* Short abbreviation with zone number on classic LCD */ + sprintf(buf, "%.2s%2d", abbr, state->current_zone); + watch_display_text(WATCH_POSITION_TOP_LEFT, buf); + watch_display_text(WATCH_POSITION_TOP_RIGHT, buf + 2); + } +} + +/* Get abbreviation for current zone */ +static void _get_zone_info(world_clock2_state_t *state, char *abbr, uoffset_t *offset) +{ + uzone_t zone_info; + udatetime_t dt; + char ds; + + watch_date_time_t utc_time = watch_rtc_get_date_time(); + unpack_zone(&zone_defns[state->current_zone], "", &zone_info); + dt = _movement_convert_date_time_to_udate(utc_time); + ds = get_current_offset(&zone_info, &dt, offset); + sprintf(abbr, zone_info.abrev_formatter, ds); +} + +/* Efficient time display taken from world_clock_face.c */ +static void _efficient_time_display(movement_event_t event, watch_date_time_t date_time, + uint32_t previous_date_time, char *buf) +{ + if ((date_time.reg >> 6) == (previous_date_time >> 6) + && event.event_type != EVENT_LOW_ENERGY_UPDATE) { + // everything before seconds is the same, don't waste cycles setting those segments. + watch_display_character_lp_seconds('0' + date_time.unit.second / 10, 8); + watch_display_character_lp_seconds('0' + date_time.unit.second % 10, 9); + } else if ((date_time.reg >> 12) == (previous_date_time >> 12) + && event.event_type != EVENT_LOW_ENERGY_UPDATE) { + // everything before minutes is the same. + sprintf(buf, "%02d%02d", date_time.unit.minute, date_time.unit.second); + watch_display_text(WATCH_POSITION_MINUTES, buf); + watch_display_text(WATCH_POSITION_SECONDS, buf + 2); + } else { + // other stuff changed; let's do it all. + if (!movement_clock_mode_24h()) { + // if we are in 12 hour mode, do some cleanup. + if (date_time.unit.hour < 12) { + watch_clear_indicator(WATCH_INDICATOR_PM); + } else { + watch_set_indicator(WATCH_INDICATOR_PM); + } + date_time.unit.hour %= 12; + if (date_time.unit.hour == 0) + date_time.unit.hour = 12; + } + + /* Display colon and 24h indicator */ + watch_set_colon(); + if (movement_clock_mode_24h()) + watch_set_indicator(WATCH_INDICATOR_24H); + + /* Display day and time */ + sprintf(buf, "%02d%02d%02d", date_time.unit.hour, date_time.unit.minute, date_time.unit.second); + watch_display_text(WATCH_POSITION_HOURS, buf + 0); + watch_display_text(WATCH_POSITION_MINUTES, buf + 2); + + if (event.event_type == EVENT_LOW_ENERGY_UPDATE) { + if (!watch_sleep_animation_is_running()) { + watch_display_text(WATCH_POSITION_SECONDS, " "); + watch_start_sleep_animation(500); + watch_start_indicator_blink_if_possible(WATCH_INDICATOR_COLON, 500); + } + } else { + watch_display_text(WATCH_POSITION_SECONDS, buf + 4); + } + } +} + +static void _clock_display(movement_event_t event, world_clock2_state_t *state) +{ + char buf[11], zone_abbr[MAX_ZONE_NAME_LEN + 1], *zone_name; + watch_date_time_t date_time, utc_time; + uint32_t previous_date_time; + int32_t offset; + uoffset_t zone_offset; + + /* Update indicators and reset previous date time */ + if (refresh_face) { + watch_clear_indicator(WATCH_INDICATOR_SIGNAL); + state->previous_date_time = 0xFFFFFFFF; + refresh_face = false; + } + + /* Determine current time at time zone and store date/time */ + utc_time = watch_rtc_get_date_time(); + offset = movement_get_current_timezone_offset_for_zone(state->current_zone); + date_time = watch_utility_date_time_convert_zone(utc_time, 0, offset); + previous_date_time = state->previous_date_time; + state->previous_date_time = date_time.reg; + + if (state->show_zone_name > 0) { + /* Check for first call to display zone name */ + if (state->show_zone_name == NAME_DISPLAY_TIME) { + watch_clear_colon(); + watch_clear_indicator(WATCH_INDICATOR_24H); + watch_clear_indicator(WATCH_INDICATOR_PM); + zone_name = watch_utility_time_zone_name_at_index(state->current_zone); + watch_display_text_with_fallback(WATCH_POSITION_BOTTOM, zone_name, zone_name); + } + state->show_zone_name--; + /* Check for last call to display zone name */ + if (state->show_zone_name == 0) { + refresh_face = true; + } + } else { + _efficient_time_display(event, date_time, previous_date_time, buf); + } + + _get_zone_info(state, zone_abbr, &zone_offset); + _display_zone_abbr(state, zone_abbr); +} + +static void _settings_display(movement_event_t event, world_clock2_state_t *state) +{ + char buf[11], zone_abbr[MAX_ZONE_NAME_LEN + 1], *zone_name; + uoffset_t zone_offset; + + /* Update indicator and colon on refresh */ + if (refresh_face) { + watch_clear_colon(); + watch_clear_indicator(WATCH_INDICATOR_24H); + watch_clear_indicator(WATCH_INDICATOR_PM); + refresh_face = false; + } + + /* Mark selected zone */ + if (state->zones[state->current_zone].selected) + watch_set_indicator(WATCH_INDICATOR_SIGNAL); + else + watch_clear_indicator(WATCH_INDICATOR_SIGNAL); + + /* Display zone abbreviation on top */ + _get_zone_info(state, zone_abbr, &zone_offset); + /* Blink up abbreviation */ + if (event.subsecond % 2) { + sprintf(zone_abbr, "%5s", " "); + } + _display_zone_abbr(state, zone_abbr); + + /* Display zone name or offset on bottom */ + if (state->show_zone_name > 0) { + zone_name = watch_utility_time_zone_name_at_index(state->current_zone); + watch_display_text_with_fallback(WATCH_POSITION_BOTTOM, zone_name, zone_name); + } else { + sprintf(buf, " %3d%02d", zone_offset.hours, zone_offset.minutes); + watch_display_text_with_fallback(WATCH_POSITION_BOTTOM, buf, buf); + } +} + +static bool _clock_loop(movement_event_t event, world_clock2_state_t *state) +{ + switch (event.event_type) { + case EVENT_ACTIVATE: + case EVENT_TICK: + case EVENT_LOW_ENERGY_UPDATE: + _clock_display(event, state); + break; + case EVENT_ALARM_BUTTON_UP: + refresh_face = true; + state->current_zone = _next_selected_zone(state, FORWARD); + state->show_zone_name = NAME_DISPLAY_TIME; + _clock_display(event, state); + break; + case EVENT_LIGHT_BUTTON_DOWN: + /* Do nothing. No light. */ + break; + case EVENT_LIGHT_BUTTON_UP: + refresh_face = true; + state->current_zone = _next_selected_zone(state, BACKWARD); + state->show_zone_name = NAME_DISPLAY_TIME; + _clock_display(event, state); + break; + case EVENT_LIGHT_LONG_PRESS: + /* Switch to settings mode */ + state->current_mode = WORLD_CLOCK2_MODE_SETTINGS; + state->show_zone_name = true; + refresh_face = true; + movement_request_tick_frequency(4); + _settings_display(event, state); + _beep(BEEP_BUTTON); + break; + case EVENT_MODE_BUTTON_UP: + /* Reset frequency and move to next face */ + movement_request_tick_frequency(1); + movement_move_to_next_face(); + break; + default: + return movement_default_loop_handler(event); + } + + return true; +} + +static bool _settings_loop(movement_event_t event, world_clock2_state_t *state) +{ + uint8_t zone; + + switch (event.event_type) { + case EVENT_ACTIVATE: + case EVENT_TICK: + case EVENT_LOW_ENERGY_UPDATE: + _settings_display(event, state); + break; + case EVENT_ALARM_BUTTON_UP: + state->current_zone = mod(state->current_zone + FORWARD, NUM_ZONE_NAMES); + _settings_display(event, state); + break; + case EVENT_LIGHT_BUTTON_UP: + state->current_zone = mod(state->current_zone + BACKWARD, NUM_ZONE_NAMES); + _settings_display(event, state); + break; + case EVENT_LIGHT_BUTTON_DOWN: + /* Do nothing. No light. */ + break; + case EVENT_ALARM_LONG_PRESS: + /* Toggle selection of current zone */ + zone = state->current_zone; + state->zones[zone].selected = !state->zones[zone].selected; + _settings_display(event, state); + if (state->zones[zone].selected) { + _beep(BEEP_ENABLE); + } else { + _beep(BEEP_DISABLE); + } + break; + case EVENT_LIGHT_LONG_PRESS: + state->show_zone_name = !state->show_zone_name; + _settings_display(event, state); + break; + case EVENT_MODE_BUTTON_UP: + /* Find next selected zone */ + if (!state->zones[state->current_zone].selected) + state->current_zone = _next_selected_zone(state, FORWARD); + + /* Switch to display mode */ + state->current_mode = WORLD_CLOCK2_MODE_CLOCK; + state->show_zone_name = NAME_DISPLAY_TIME; + refresh_face = true; + movement_request_tick_frequency(1); + _clock_display(event, state); + _beep(BEEP_BUTTON); + break; + default: + return movement_default_loop_handler(event); + } + + return true; +} + +void world_clock2_face_setup(uint8_t watch_face_index, void **context_ptr) +{ + (void) watch_face_index; + int8_t selected_zones[] = SELECTED_ZONES, *selected_zones_ptr; + + if (*context_ptr == NULL) { + *context_ptr = malloc(sizeof(world_clock2_state_t)); + memset(*context_ptr, 0, sizeof(world_clock2_state_t)); + + /* Start in settings mode */ + world_clock2_state_t *state = (world_clock2_state_t *) * context_ptr; + state->current_mode = WORLD_CLOCK2_MODE_CLOCK; + state->current_zone = UTC_ZONE_INDEX; + state->show_zone_name = NAME_DISPLAY_TIME; + + selected_zones_ptr = selected_zones; + while (*selected_zones_ptr != -1) { + state->zones[*selected_zones_ptr].selected = true; + selected_zones_ptr++; + } + } +} + +void world_clock2_face_activate(void *context) +{ + world_clock2_state_t *state = (world_clock2_state_t *) context; + + switch (state->current_mode) { + case WORLD_CLOCK2_MODE_CLOCK: + /* Normal tick frequency */ + movement_request_tick_frequency(1); + break; + case WORLD_CLOCK2_MODE_SETTINGS: + /* Faster frequency for blinking effect */ + movement_request_tick_frequency(4); + break; + } + + /* Set initial state */ + refresh_face = true; +} + +bool world_clock2_face_loop(movement_event_t event, void *context) +{ + world_clock2_state_t *state = (world_clock2_state_t *) context; + switch (state->current_mode) { + case WORLD_CLOCK2_MODE_CLOCK: + return _clock_loop(event, state); + case WORLD_CLOCK2_MODE_SETTINGS: + return _settings_loop(event, state); + } + return false; +} + +void world_clock2_face_resign(void *context) +{ + (void) context; +} diff --git a/watch-faces/clock/world_clock2_face.h b/watch-faces/clock/world_clock2_face.h new file mode 100644 index 000000000..eedff8085 --- /dev/null +++ b/watch-faces/clock/world_clock2_face.h @@ -0,0 +1,134 @@ +/* + * MIT License + * + * Copyright (c) 2023 Konrad Rieck + * Copyright (c) 2022 Joey Castillo + * + * 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 WORLD_CLOCK2_FACE_H_ +#define WORLD_CLOCK2_FACE_H_ + +/* + * WORLD CLOCK 2 + * + * This is an alternative world clock face that allows the user to cycle + * through a list of selected time zones. It extends the original + * implementation by Joey Castillo. The face has two modes: clock mode + * and settings mode. + * + * Clock mode + * + * When the clock face is activated for the first time, it enters clock mode. + * The face displays the time of one of multiple selected time zones. It + * includes the following components: + * + * - The top of the face displays the first letters of the time zone + * abbreviation, such as "PS" for Pacific Standard Time on the classic + * display and "PST" on the custom display. + * + * - On the classic display, the upper-right corner additionally shows the + * index number of the time zone. This helps avoid confusion when multiple + * time zones have the same two-letter abbreviation. + * + * - The bottom of the face shows the name of the current zone time, such as + * "Tokyo" or "Berlin" for about a second when activating the face or cycling + * between time zones. + * + * - After displaying the zone name, the face displays the time of the time + * zone. There is no timeout, allowing users to keep the chosen time zone + * displayed for as long as they wish. + * + * The user can navigate through the selected time zones using the following + * buttons: + * + * - The ALARM button moves to the next selected time zone, while the LIGHT + * button moves to the previous zone. + * + * - A long press on the LIGHT button enters settings mode and enables the + * user to re-configure the selected time zones. + * + * Settings mode + * + * In settings mode, the user can select the time zones they want to display + * and cycle through. The face shows a summary of the current time zone: + * + * - The top of the face displays the first letters of the time zone + * abbreviation, such as "PS" for Pacific Standard Time on the classic + * display and "PST" on the custom display. The letters blink. + * + * - On the classic display, the upper-right corner additionally shows the + * index number of the time zone. This helps avoid confusion when multiple + * time zones have the same two-letter abbreviation. + * + * - The bottom display shows either the name of the time zone or its + * offset from UTC. For example, it either shows "Tokyo" or "9:00" + * for Japanese Standard Time. + * + * The user can navigate through the time zones and select them using the + * following buttons: + * + * - The ALARM button moves forward to the next time zone, while the LIGHT + * button moves backward to the previous zone. + * + * - A long press on the ALARM button (de)selects the current time zone, and + * the signal indicator (dis)appears at the top left. + * + * - A long press on the LIGHT button toggles the display of the time zone + * name or offset in the bottom display. + * + * - A press on the MODE button exits settings mode and returns to the + * clock mode. + */ + +#include "movement.h" +#include "zones.h" + +typedef enum { + WORLD_CLOCK2_MODE_CLOCK, + WORLD_CLOCK2_MODE_SETTINGS +} world_clock2_mode_t; + +typedef struct { + bool selected; +} world_clock2_zone_t; + +typedef struct { + world_clock2_zone_t zones[NUM_ZONE_NAMES]; + uint8_t current_zone; + world_clock2_mode_t current_mode; + uint32_t previous_date_time; + uint8_t show_zone_name; +} world_clock2_state_t; + +void world_clock2_face_setup(uint8_t watch_face_index, void **context_ptr); +void world_clock2_face_activate(void *context); +bool world_clock2_face_loop(movement_event_t event, void *context); +void world_clock2_face_resign(void *context); + +#define world_clock2_face ((const watch_face_t){ \ + world_clock2_face_setup, \ + world_clock2_face_activate, \ + world_clock2_face_loop, \ + world_clock2_face_resign, \ + NULL, \ +}) + +#endif /* WORLD_CLOCK2_FACE_H_ */