From: joeycastillo Date: Mon, 30 Sep 2024 03:13:59 +0000 (-0400) Subject: bring sunrise/sunset face into new movement X-Git-Url: https://git.earman.xyz/?a=commitdiff_plain;h=5b12491ebe49f1a5b82c6ee2cc6f58a9a0ac8ff6;p=sensor-watch.git bring sunrise/sunset face into new movement --- diff --git a/Makefile b/Makefile index 3e18df6..6a76e69 100644 --- a/Makefile +++ b/Makefile @@ -35,6 +35,7 @@ INCLUDES += \ -I./utz \ -I./filesystem \ -I./shell \ + -I./movement/lib/sunriset \ -I./watch-library/shared/watch \ -I./watch-library/hardware/watch \ -I./watch-faces/clock \ @@ -50,6 +51,7 @@ SRCS += \ ./utz/zones.c \ ./shell/shell.c \ ./shell/shell_cmd_list.c \ + ./movement/lib/sunriset/sunriset.c \ ./watch-library/shared/watch/watch_common_buzzer.c \ ./watch-library/shared/watch/watch_common_display.c \ ./watch-library/shared/watch/watch_utility.c \ diff --git a/movement/watch_faces/complication/sunrise_sunset_face.c b/movement/watch_faces/complication/sunrise_sunset_face.c deleted file mode 100644 index 87d8226..0000000 --- a/movement/watch_faces/complication/sunrise_sunset_face.c +++ /dev/null @@ -1,432 +0,0 @@ -/* - * MIT License - * - * 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. - * - * Sunrise/sunset calculations are public domain code by Paul Schlyter, December 1992 - * - */ - -#include -#include -#include -#include "sunrise_sunset_face.h" -#include "watch.h" -#include "watch_utility.h" -#include "sunriset.h" - -#if __EMSCRIPTEN__ -#include -#endif - -static const uint8_t _location_count = sizeof(longLatPresets) / sizeof(long_lat_presets_t); - -static void _sunrise_sunset_set_expiration(sunrise_sunset_state_t *state, watch_date_time next_rise_set) { - uint32_t timestamp = watch_utility_date_time_to_unix_time(next_rise_set, 0); - state->rise_set_expires = watch_utility_date_time_from_unix_time(timestamp + 60, 0); -} - -static void _sunrise_sunset_face_update(sunrise_sunset_state_t *state) { - char buf[14]; - double rise, set, minutes, seconds; - bool show_next_match = false; - movement_location_t movement_location; - if (state->longLatToUse == 0 || _location_count <= 1) - movement_location = (movement_location_t) watch_get_backup_data(1); - else{ - movement_location.bit.latitude = longLatPresets[state->longLatToUse].latitude; - movement_location.bit.longitude = longLatPresets[state->longLatToUse].longitude; - } - - if (movement_location.reg == 0) { - watch_display_string("RI no Loc", 0); - return; - } - - watch_date_time date_time = watch_rtc_get_date_time(); // the current local date / time - watch_date_time utc_now = watch_utility_date_time_convert_zone(date_time, movement_get_current_timezone_offset(), 0); // the current date / time in UTC - watch_date_time scratch_time; // scratchpad, contains different values at different times - scratch_time.reg = utc_now.reg; - - // Weird quirky unsigned things were happening when I tried to cast these directly to doubles below. - // it looks redundant, but extracting them to local int16's seemed to fix it. - int16_t lat_centi = (int16_t)movement_location.bit.latitude; - int16_t lon_centi = (int16_t)movement_location.bit.longitude; - - double lat = (double)lat_centi / 100.0; - double lon = (double)lon_centi / 100.0; - - // sunriset returns the rise/set times as signed decimal hours in UTC. - // this can mean hours below 0 or above 31, which won't fit into a watch_date_time struct. - // to deal with this, we set aside the offset in hours, and add it back before converting it to a watch_date_time. - double hours_from_utc = ((double)movement_get_current_timezone_offset()) / 3600.0; - - // we loop twice because if it's after sunset today, we need to recalculate to display values for tomorrow. - for(int i = 0; i < 2; i++) { - uint8_t result = sun_rise_set(scratch_time.unit.year + WATCH_RTC_REFERENCE_YEAR, scratch_time.unit.month, scratch_time.unit.day, lon, lat, &rise, &set); - - if (result != 0) { - watch_clear_colon(); - watch_clear_indicator(WATCH_INDICATOR_PM); - watch_clear_indicator(WATCH_INDICATOR_24H); - sprintf(buf, "%s%2d none ", (result == 1) ? "SE" : "rI", scratch_time.unit.day); - watch_display_string(buf, 0); - return; - } - - watch_set_colon(); - if (movement_clock_mode_24h()) watch_set_indicator(WATCH_INDICATOR_24H); - - rise += hours_from_utc; - set += hours_from_utc; - - minutes = 60.0 * fmod(rise, 1); - seconds = 60.0 * fmod(minutes, 1); - scratch_time.unit.hour = floor(rise); - if (seconds < 30) scratch_time.unit.minute = floor(minutes); - else scratch_time.unit.minute = ceil(minutes); - - if (scratch_time.unit.minute == 60) { - scratch_time.unit.minute = 0; - scratch_time.unit.hour = (scratch_time.unit.hour + 1) % 24; - } - - if (date_time.reg < scratch_time.reg) _sunrise_sunset_set_expiration(state, scratch_time); - - if (date_time.reg < scratch_time.reg || show_next_match) { - if (state->rise_index == 0 || show_next_match) { - if (!movement_clock_mode_24h()) { - if (watch_utility_convert_to_12_hour(&scratch_time)) watch_set_indicator(WATCH_INDICATOR_PM); - else watch_clear_indicator(WATCH_INDICATOR_PM); - } - sprintf(buf, "rI%2d%2d%02d%s", scratch_time.unit.day, scratch_time.unit.hour, scratch_time.unit.minute,longLatPresets[state->longLatToUse].name); - watch_display_string(buf, 0); - return; - } else { - show_next_match = true; - } - } - - minutes = 60.0 * fmod(set, 1); - seconds = 60.0 * fmod(minutes, 1); - scratch_time.unit.hour = floor(set); - if (seconds < 30) scratch_time.unit.minute = floor(minutes); - else scratch_time.unit.minute = ceil(minutes); - - if (scratch_time.unit.minute == 60) { - scratch_time.unit.minute = 0; - scratch_time.unit.hour = (scratch_time.unit.hour + 1) % 24; - } - - if (date_time.reg < scratch_time.reg) _sunrise_sunset_set_expiration(state, scratch_time); - - if (date_time.reg < scratch_time.reg || show_next_match) { - if (state->rise_index == 0 || show_next_match) { - if (!movement_clock_mode_24h()) { - if (watch_utility_convert_to_12_hour(&scratch_time)) watch_set_indicator(WATCH_INDICATOR_PM); - else watch_clear_indicator(WATCH_INDICATOR_PM); - } - sprintf(buf, "SE%2d%2d%02d%s", scratch_time.unit.day, scratch_time.unit.hour, scratch_time.unit.minute, longLatPresets[state->longLatToUse].name); - watch_display_string(buf, 0); - return; - } else { - show_next_match = true; - } - } - - // it's after sunset. we need to display sunrise/sunset for tomorrow. - uint32_t timestamp = watch_utility_date_time_to_unix_time(utc_now, 0); - timestamp += 86400; - scratch_time = watch_utility_date_time_from_unix_time(timestamp, 0); - } -} - -static int16_t _sunrise_sunset_face_latlon_from_struct(sunrise_sunset_lat_lon_settings_t val) { - int16_t retval = (val.sign ? -1 : 1) * - ( - val.hundreds * 10000 + - val.tens * 1000 + - val.ones * 100 + - val.tenths * 10 + - val.hundredths - ); - return retval; -} - -static sunrise_sunset_lat_lon_settings_t _sunrise_sunset_face_struct_from_latlon(int16_t val) { - sunrise_sunset_lat_lon_settings_t retval; - - retval.sign = val < 0; - val = abs(val); - retval.hundredths = val % 10; - val /= 10; - retval.tenths = val % 10; - val /= 10; - retval.ones = val % 10; - val /= 10; - retval.tens = val % 10; - val /= 10; - retval.hundreds = val % 10; - - return retval; -} - -static void _sunrise_sunset_face_update_location_register(sunrise_sunset_state_t *state) { - if (state->location_changed) { - movement_location_t movement_location; - int16_t lat = _sunrise_sunset_face_latlon_from_struct(state->working_latitude); - int16_t lon = _sunrise_sunset_face_latlon_from_struct(state->working_longitude); - movement_location.bit.latitude = lat; - movement_location.bit.longitude = lon; - watch_store_backup_data(movement_location.reg, 1); - state->location_changed = false; - } -} - -static void _sunrise_sunset_face_update_settings_display(movement_event_t event, sunrise_sunset_state_t *state) { - char buf[12]; - - switch (state->page) { - case 0: - return; - case 1: - sprintf(buf, "LA %c %04d", state->working_latitude.sign ? '-' : '+', abs(_sunrise_sunset_face_latlon_from_struct(state->working_latitude))); - break; - case 2: - sprintf(buf, "LO %c%05d", state->working_longitude.sign ? '-' : '+', abs(_sunrise_sunset_face_latlon_from_struct(state->working_longitude))); - break; - } - if (event.subsecond % 2) { - buf[state->active_digit + 4] = ' '; - } - watch_display_string(buf, 0); -} - -static void _sunrise_sunset_face_advance_digit(sunrise_sunset_state_t *state) { - state->location_changed = true; - switch (state->page) { - case 1: // latitude - switch (state->active_digit) { - case 0: - state->working_latitude.sign++; - break; - case 1: - // we skip this digit - break; - case 2: - state->working_latitude.tens = (state->working_latitude.tens + 1) % 10; - if (abs(_sunrise_sunset_face_latlon_from_struct(state->working_latitude)) > 9000) { - // prevent latitude from going over ±90. - // TODO: perform these checks when advancing the digit? - state->working_latitude.ones = 0; - state->working_latitude.tenths = 0; - state->working_latitude.hundredths = 0; - } - break; - case 3: - state->working_latitude.ones = (state->working_latitude.ones + 1) % 10; - if (abs(_sunrise_sunset_face_latlon_from_struct(state->working_latitude)) > 9000) state->working_latitude.ones = 0; - break; - case 4: - state->working_latitude.tenths = (state->working_latitude.tenths + 1) % 10; - if (abs(_sunrise_sunset_face_latlon_from_struct(state->working_latitude)) > 9000) state->working_latitude.tenths = 0; - break; - case 5: - state->working_latitude.hundredths = (state->working_latitude.hundredths + 1) % 10; - if (abs(_sunrise_sunset_face_latlon_from_struct(state->working_latitude)) > 9000) state->working_latitude.hundredths = 0; - break; - } - break; - case 2: // longitude - switch (state->active_digit) { - case 0: - state->working_longitude.sign++; - break; - case 1: - state->working_longitude.hundreds = (state->working_longitude.hundreds + 1) % 10; - if (abs(_sunrise_sunset_face_latlon_from_struct(state->working_longitude)) > 18000) { - // prevent longitude from going over ±180 - state->working_longitude.tens = 8; - state->working_longitude.ones = 0; - state->working_longitude.tenths = 0; - state->working_longitude.hundredths = 0; - } - break; - case 2: - state->working_longitude.tens = (state->working_longitude.tens + 1) % 10; - if (abs(_sunrise_sunset_face_latlon_from_struct(state->working_longitude)) > 18000) state->working_longitude.tens = 0; - break; - case 3: - state->working_longitude.ones = (state->working_longitude.ones + 1) % 10; - if (abs(_sunrise_sunset_face_latlon_from_struct(state->working_longitude)) > 18000) state->working_longitude.ones = 0; - break; - case 4: - state->working_longitude.tenths = (state->working_longitude.tenths + 1) % 10; - if (abs(_sunrise_sunset_face_latlon_from_struct(state->working_longitude)) > 18000) state->working_longitude.tenths = 0; - break; - case 5: - state->working_longitude.hundredths = (state->working_longitude.hundredths + 1) % 10; - if (abs(_sunrise_sunset_face_latlon_from_struct(state->working_longitude)) > 18000) state->working_longitude.hundredths = 0; - break; - } - break; - } -} - -void sunrise_sunset_face_setup(uint8_t watch_face_index, void ** context_ptr) { - (void) watch_face_index; - if (*context_ptr == NULL) { - *context_ptr = malloc(sizeof(sunrise_sunset_state_t)); - memset(*context_ptr, 0, sizeof(sunrise_sunset_state_t)); - } -} - -void sunrise_sunset_face_activate(void *context) { - if (watch_sleep_animation_is_running()) watch_stop_sleep_animation(); - -#if __EMSCRIPTEN__ - int16_t browser_lat = EM_ASM_INT({ - return lat; - }); - int16_t browser_lon = EM_ASM_INT({ - return lon; - }); - if ((watch_get_backup_data(1) == 0) && (browser_lat || browser_lon)) { - movement_location_t browser_loc; - browser_loc.bit.latitude = browser_lat; - browser_loc.bit.longitude = browser_lon; - watch_store_backup_data(browser_loc.reg, 1); - } -#endif - - - sunrise_sunset_state_t *state = (sunrise_sunset_state_t *)context; - movement_location_t movement_location = (movement_location_t) watch_get_backup_data(1); - state->working_latitude = _sunrise_sunset_face_struct_from_latlon(movement_location.bit.latitude); - state->working_longitude = _sunrise_sunset_face_struct_from_latlon(movement_location.bit.longitude); -} - -bool sunrise_sunset_face_loop(movement_event_t event, void *context) { - sunrise_sunset_state_t *state = (sunrise_sunset_state_t *)context; - - switch (event.event_type) { - case EVENT_ACTIVATE: - _sunrise_sunset_face_update(state); - break; - case EVENT_LOW_ENERGY_UPDATE: - case EVENT_TICK: - if (state->page == 0) { - // if entering low energy mode, start tick animation - if (event.event_type == EVENT_LOW_ENERGY_UPDATE && !watch_sleep_animation_is_running()) watch_start_sleep_animation(1000); - // check if we need to update the display - watch_date_time date_time = watch_rtc_get_date_time(); - if (date_time.reg >= state->rise_set_expires.reg) { - // and on the off chance that this happened before EVENT_TIMEOUT snapped us back to rise/set 0, go back now - state->rise_index = 0; - _sunrise_sunset_face_update(state); - } - } else { - _sunrise_sunset_face_update_settings_display(event, state); - } - break; - case EVENT_LIGHT_BUTTON_DOWN: - if (state->page) { - state->active_digit++; - if (state->page == 1 && state->active_digit == 1) state->active_digit++; // max latitude is +- 90, no hundreds place - if (state->active_digit > 5) { - state->active_digit = 0; - state->page = (state->page + 1) % 3; - _sunrise_sunset_face_update_location_register(state); - } - _sunrise_sunset_face_update_settings_display(event, context); - } else if (_location_count <= 1) { - movement_illuminate_led(); - } - if (state->page == 0) { - movement_request_tick_frequency(1); - _sunrise_sunset_face_update(state); - } - break; - case EVENT_LIGHT_LONG_PRESS: - if (_location_count <= 1) break; - else if (!state->page) movement_illuminate_led(); - break; - case EVENT_LIGHT_BUTTON_UP: - if (state->page == 0 && _location_count > 1) { - state->longLatToUse = (state->longLatToUse + 1) % _location_count; - _sunrise_sunset_face_update(state); - } - break; - case EVENT_ALARM_BUTTON_UP: - if (state->page) { - _sunrise_sunset_face_advance_digit(state); - _sunrise_sunset_face_update_settings_display(event, context); - } else { - state->rise_index = (state->rise_index + 1) % 2; - _sunrise_sunset_face_update(state); - } - break; - case EVENT_ALARM_LONG_PRESS: - if (state->page == 0) { - if (state->longLatToUse != 0) { - state->longLatToUse = 0; - _sunrise_sunset_face_update(state); - break; - } - state->page++; - state->active_digit = 0; - watch_clear_display(); - movement_request_tick_frequency(4); - _sunrise_sunset_face_update_settings_display(event, context); - } - else { - state->active_digit = 0; - state->page = 0; - _sunrise_sunset_face_update_location_register(state); - _sunrise_sunset_face_update(state); - } - break; - case EVENT_TIMEOUT: - if (watch_get_backup_data(1) == 0) { - // if no location set, return home - movement_move_to_face(0); - } else if (state->page || state->rise_index) { - // otherwise on timeout, exit settings mode and return to the next sunrise or sunset - state->page = 0; - state->rise_index = 0; - movement_request_tick_frequency(1); - _sunrise_sunset_face_update(state); - } - break; - default: - return movement_default_loop_handler(event); - } - - return true; -} - -void sunrise_sunset_face_resign(void *context) { - sunrise_sunset_state_t *state = (sunrise_sunset_state_t *)context; - state->page = 0; - state->active_digit = 0; - state->rise_index = 0; - _sunrise_sunset_face_update_location_register(state); -} diff --git a/movement/watch_faces/complication/sunrise_sunset_face.h b/movement/watch_faces/complication/sunrise_sunset_face.h deleted file mode 100644 index 133eed0..0000000 --- a/movement/watch_faces/complication/sunrise_sunset_face.h +++ /dev/null @@ -1,88 +0,0 @@ -/* - * MIT License - * - * 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 SUNRISE_SUNSET_FACE_H_ -#define SUNRISE_SUNSET_FACE_H_ - -/* - * SUNRISE & SUNSET FACE - * - * The Sunrise/Sunset face is designed to display the next sunrise or sunset - * for a given location. It also functions as an interface for setting the - * location register, which other watch faces can use for various purposes. - * - * Refer to the wiki for usage instructions: - * https://www.sensorwatch.net/docs/watchfaces/complication/#sunrisesunset - */ - -#include "movement.h" - -typedef struct { - uint8_t sign: 1; // 0-1 - uint8_t hundreds: 1; // 0-1, ignored for latitude - uint8_t tens: 4; // 0-9 (must wrap at 10) - uint8_t ones: 4; // 0-9 (must wrap at 10) - uint8_t tenths: 4; // 0-9 (must wrap at 10) - uint8_t hundredths: 4; // 0-9 (must wrap at 10) -} sunrise_sunset_lat_lon_settings_t; - -typedef struct { - uint8_t page; - uint8_t rise_index; - uint8_t active_digit; - bool location_changed; - watch_date_time rise_set_expires; - sunrise_sunset_lat_lon_settings_t working_latitude; - sunrise_sunset_lat_lon_settings_t working_longitude; - uint8_t longLatToUse; -} sunrise_sunset_state_t; - -void sunrise_sunset_face_setup(uint8_t watch_face_index, void ** context_ptr); -void sunrise_sunset_face_activate(void *context); -bool sunrise_sunset_face_loop(movement_event_t event, void *context); -void sunrise_sunset_face_resign(void *context); - -#define sunrise_sunset_face ((const watch_face_t){ \ - sunrise_sunset_face_setup, \ - sunrise_sunset_face_activate, \ - sunrise_sunset_face_loop, \ - sunrise_sunset_face_resign, \ - NULL, \ -}) - -typedef struct { - char name[2]; - int16_t latitude; - int16_t longitude; -} long_lat_presets_t; - -static const long_lat_presets_t longLatPresets[] = -{ - { .name = " "}, // Default, the long and lat get replaced by what's set in the watch -// { .name = "Ny", .latitude = 4072, .longitude = -7401 }, // New York City, NY -// { .name = "LA", .latitude = 3405, .longitude = -11824 }, // Los Angeles, CA -// { .name = "dE", .latitude = 4221, .longitude = -8305 }, // Detroit, MI -}; - -#endif // SUNRISE_SUNSET_FACE_H_ diff --git a/movement_config.h b/movement_config.h index a6a9610..c54149e 100644 --- a/movement_config.h +++ b/movement_config.h @@ -29,7 +29,7 @@ const watch_face_t watch_faces[] = { clock_face, - world_clock_face, + sunrise_sunset_face, beats_face, advanced_alarm_face, countdown_face, diff --git a/movement_faces.h b/movement_faces.h index a33645a..09d682f 100644 --- a/movement_faces.h +++ b/movement_faces.h @@ -30,6 +30,7 @@ #include "advanced_alarm_face.h" #include "countdown_face.h" #include "fast_stopwatch_face.h" +#include "sunrise_sunset_face.h" #include "set_time_face.h" #include "preferences_face.h" // New includes go above this line. diff --git a/watch-faces.mk b/watch-faces.mk index 7d562fa..417e30d 100644 --- a/watch-faces.mk +++ b/watch-faces.mk @@ -5,5 +5,6 @@ SRCS += \ ./watch-faces/complication/advanced_alarm_face.c \ ./watch-faces/complication/countdown_face.c \ ./watch-faces/complication/fast_stopwatch_face.c \ + ./watch-faces/complication/sunrise_sunset_face.c \ ./watch-faces/settings/set_time_face.c \ ./watch-faces/settings/preferences_face.c \ diff --git a/watch-faces/complication/sunrise_sunset_face.c b/watch-faces/complication/sunrise_sunset_face.c new file mode 100644 index 0000000..95cddfb --- /dev/null +++ b/watch-faces/complication/sunrise_sunset_face.c @@ -0,0 +1,433 @@ +/* + * MIT License + * + * 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. + * + * Sunrise/sunset calculations are public domain code by Paul Schlyter, December 1992 + * + */ + +#include +#include +#include +#include "sunrise_sunset_face.h" +#include "watch.h" +#include "watch_utility.h" +#include "sunriset.h" + +#if __EMSCRIPTEN__ +#include +#endif + +static const uint8_t _location_count = sizeof(longLatPresets) / sizeof(long_lat_presets_t); + +static void _sunrise_sunset_set_expiration(sunrise_sunset_state_t *state, watch_date_time next_rise_set) { + uint32_t timestamp = watch_utility_date_time_to_unix_time(next_rise_set, 0); + state->rise_set_expires = watch_utility_date_time_from_unix_time(timestamp + 60, 0); +} + +static void _sunrise_sunset_face_update(sunrise_sunset_state_t *state) { + char buf[14]; + double rise, set, minutes, seconds; + bool show_next_match = false; + movement_location_t movement_location; + if (state->longLatToUse == 0 || _location_count <= 1) + movement_location = (movement_location_t) watch_get_backup_data(1); + else{ + movement_location.bit.latitude = longLatPresets[state->longLatToUse].latitude; + movement_location.bit.longitude = longLatPresets[state->longLatToUse].longitude; + } + + if (movement_location.reg == 0) { + watch_display_text_with_fallback(WATCH_POSITION_TOP, "Sunri", "rI"); + watch_display_text_with_fallback(WATCH_POSITION_BOTTOM, "No LOC", "No Loc"); + return; + } + + watch_date_time date_time = watch_rtc_get_date_time(); // the current local date / time + watch_date_time utc_now = watch_utility_date_time_convert_zone(date_time, movement_get_current_timezone_offset(), 0); // the current date / time in UTC + watch_date_time scratch_time; // scratchpad, contains different values at different times + scratch_time.reg = utc_now.reg; + + // Weird quirky unsigned things were happening when I tried to cast these directly to doubles below. + // it looks redundant, but extracting them to local int16's seemed to fix it. + int16_t lat_centi = (int16_t)movement_location.bit.latitude; + int16_t lon_centi = (int16_t)movement_location.bit.longitude; + + double lat = (double)lat_centi / 100.0; + double lon = (double)lon_centi / 100.0; + + // sunriset returns the rise/set times as signed decimal hours in UTC. + // this can mean hours below 0 or above 31, which won't fit into a watch_date_time struct. + // to deal with this, we set aside the offset in hours, and add it back before converting it to a watch_date_time. + double hours_from_utc = ((double)movement_get_current_timezone_offset()) / 3600.0; + + // we loop twice because if it's after sunset today, we need to recalculate to display values for tomorrow. + for(int i = 0; i < 2; i++) { + uint8_t result = sun_rise_set(scratch_time.unit.year + WATCH_RTC_REFERENCE_YEAR, scratch_time.unit.month, scratch_time.unit.day, lon, lat, &rise, &set); + + if (result != 0) { + watch_clear_colon(); + watch_clear_indicator(WATCH_INDICATOR_PM); + watch_clear_indicator(WATCH_INDICATOR_24H); + sprintf(buf, "%s%2d none ", (result == 1) ? "SE" : "rI", scratch_time.unit.day); + watch_display_text(WATCH_POSITION_FULL, buf); + return; + } + + watch_set_colon(); + if (movement_clock_mode_24h()) watch_set_indicator(WATCH_INDICATOR_24H); + + rise += hours_from_utc; + set += hours_from_utc; + + minutes = 60.0 * fmod(rise, 1); + seconds = 60.0 * fmod(minutes, 1); + scratch_time.unit.hour = floor(rise); + if (seconds < 30) scratch_time.unit.minute = floor(minutes); + else scratch_time.unit.minute = ceil(minutes); + + if (scratch_time.unit.minute == 60) { + scratch_time.unit.minute = 0; + scratch_time.unit.hour = (scratch_time.unit.hour + 1) % 24; + } + + if (date_time.reg < scratch_time.reg) _sunrise_sunset_set_expiration(state, scratch_time); + + if (date_time.reg < scratch_time.reg || show_next_match) { + if (state->rise_index == 0 || show_next_match) { + if (!movement_clock_mode_24h()) { + if (watch_utility_convert_to_12_hour(&scratch_time)) watch_set_indicator(WATCH_INDICATOR_PM); + else watch_clear_indicator(WATCH_INDICATOR_PM); + } + sprintf(buf, "rI%2d%2d%02d%s", scratch_time.unit.day, scratch_time.unit.hour, scratch_time.unit.minute,longLatPresets[state->longLatToUse].name); + watch_display_text(WATCH_POSITION_FULL, buf); + return; + } else { + show_next_match = true; + } + } + + minutes = 60.0 * fmod(set, 1); + seconds = 60.0 * fmod(minutes, 1); + scratch_time.unit.hour = floor(set); + if (seconds < 30) scratch_time.unit.minute = floor(minutes); + else scratch_time.unit.minute = ceil(minutes); + + if (scratch_time.unit.minute == 60) { + scratch_time.unit.minute = 0; + scratch_time.unit.hour = (scratch_time.unit.hour + 1) % 24; + } + + if (date_time.reg < scratch_time.reg) _sunrise_sunset_set_expiration(state, scratch_time); + + if (date_time.reg < scratch_time.reg || show_next_match) { + if (state->rise_index == 0 || show_next_match) { + if (!movement_clock_mode_24h()) { + if (watch_utility_convert_to_12_hour(&scratch_time)) watch_set_indicator(WATCH_INDICATOR_PM); + else watch_clear_indicator(WATCH_INDICATOR_PM); + } + sprintf(buf, "SE%2d%2d%02d%s", scratch_time.unit.day, scratch_time.unit.hour, scratch_time.unit.minute, longLatPresets[state->longLatToUse].name); + watch_display_text(WATCH_POSITION_FULL, buf); + return; + } else { + show_next_match = true; + } + } + + // it's after sunset. we need to display sunrise/sunset for tomorrow. + uint32_t timestamp = watch_utility_date_time_to_unix_time(utc_now, 0); + timestamp += 86400; + scratch_time = watch_utility_date_time_from_unix_time(timestamp, 0); + } +} + +static int16_t _sunrise_sunset_face_latlon_from_struct(sunrise_sunset_lat_lon_settings_t val) { + int16_t retval = (val.sign ? -1 : 1) * + ( + val.hundreds * 10000 + + val.tens * 1000 + + val.ones * 100 + + val.tenths * 10 + + val.hundredths + ); + return retval; +} + +static sunrise_sunset_lat_lon_settings_t _sunrise_sunset_face_struct_from_latlon(int16_t val) { + sunrise_sunset_lat_lon_settings_t retval; + + retval.sign = val < 0; + val = abs(val); + retval.hundredths = val % 10; + val /= 10; + retval.tenths = val % 10; + val /= 10; + retval.ones = val % 10; + val /= 10; + retval.tens = val % 10; + val /= 10; + retval.hundreds = val % 10; + + return retval; +} + +static void _sunrise_sunset_face_update_location_register(sunrise_sunset_state_t *state) { + if (state->location_changed) { + movement_location_t movement_location; + int16_t lat = _sunrise_sunset_face_latlon_from_struct(state->working_latitude); + int16_t lon = _sunrise_sunset_face_latlon_from_struct(state->working_longitude); + movement_location.bit.latitude = lat; + movement_location.bit.longitude = lon; + watch_store_backup_data(movement_location.reg, 1); + state->location_changed = false; + } +} + +static void _sunrise_sunset_face_update_settings_display(movement_event_t event, sunrise_sunset_state_t *state) { + char buf[12]; + + switch (state->page) { + case 0: + return; + case 1: + sprintf(buf, "LA %c %04d", state->working_latitude.sign ? '-' : '+', abs(_sunrise_sunset_face_latlon_from_struct(state->working_latitude))); + break; + case 2: + sprintf(buf, "LO %c%05d", state->working_longitude.sign ? '-' : '+', abs(_sunrise_sunset_face_latlon_from_struct(state->working_longitude))); + break; + } + if (event.subsecond % 2) { + buf[state->active_digit + 4] = ' '; + } + watch_display_text(WATCH_POSITION_FULL, buf); +} + +static void _sunrise_sunset_face_advance_digit(sunrise_sunset_state_t *state) { + state->location_changed = true; + switch (state->page) { + case 1: // latitude + switch (state->active_digit) { + case 0: + state->working_latitude.sign++; + break; + case 1: + // we skip this digit + break; + case 2: + state->working_latitude.tens = (state->working_latitude.tens + 1) % 10; + if (abs(_sunrise_sunset_face_latlon_from_struct(state->working_latitude)) > 9000) { + // prevent latitude from going over ±90. + // TODO: perform these checks when advancing the digit? + state->working_latitude.ones = 0; + state->working_latitude.tenths = 0; + state->working_latitude.hundredths = 0; + } + break; + case 3: + state->working_latitude.ones = (state->working_latitude.ones + 1) % 10; + if (abs(_sunrise_sunset_face_latlon_from_struct(state->working_latitude)) > 9000) state->working_latitude.ones = 0; + break; + case 4: + state->working_latitude.tenths = (state->working_latitude.tenths + 1) % 10; + if (abs(_sunrise_sunset_face_latlon_from_struct(state->working_latitude)) > 9000) state->working_latitude.tenths = 0; + break; + case 5: + state->working_latitude.hundredths = (state->working_latitude.hundredths + 1) % 10; + if (abs(_sunrise_sunset_face_latlon_from_struct(state->working_latitude)) > 9000) state->working_latitude.hundredths = 0; + break; + } + break; + case 2: // longitude + switch (state->active_digit) { + case 0: + state->working_longitude.sign++; + break; + case 1: + state->working_longitude.hundreds = (state->working_longitude.hundreds + 1) % 10; + if (abs(_sunrise_sunset_face_latlon_from_struct(state->working_longitude)) > 18000) { + // prevent longitude from going over ±180 + state->working_longitude.tens = 8; + state->working_longitude.ones = 0; + state->working_longitude.tenths = 0; + state->working_longitude.hundredths = 0; + } + break; + case 2: + state->working_longitude.tens = (state->working_longitude.tens + 1) % 10; + if (abs(_sunrise_sunset_face_latlon_from_struct(state->working_longitude)) > 18000) state->working_longitude.tens = 0; + break; + case 3: + state->working_longitude.ones = (state->working_longitude.ones + 1) % 10; + if (abs(_sunrise_sunset_face_latlon_from_struct(state->working_longitude)) > 18000) state->working_longitude.ones = 0; + break; + case 4: + state->working_longitude.tenths = (state->working_longitude.tenths + 1) % 10; + if (abs(_sunrise_sunset_face_latlon_from_struct(state->working_longitude)) > 18000) state->working_longitude.tenths = 0; + break; + case 5: + state->working_longitude.hundredths = (state->working_longitude.hundredths + 1) % 10; + if (abs(_sunrise_sunset_face_latlon_from_struct(state->working_longitude)) > 18000) state->working_longitude.hundredths = 0; + break; + } + break; + } +} + +void sunrise_sunset_face_setup(uint8_t watch_face_index, void ** context_ptr) { + (void) watch_face_index; + if (*context_ptr == NULL) { + *context_ptr = malloc(sizeof(sunrise_sunset_state_t)); + memset(*context_ptr, 0, sizeof(sunrise_sunset_state_t)); + } +} + +void sunrise_sunset_face_activate(void *context) { + if (watch_sleep_animation_is_running()) watch_stop_sleep_animation(); + +#if __EMSCRIPTEN__ + int16_t browser_lat = EM_ASM_INT({ + return lat; + }); + int16_t browser_lon = EM_ASM_INT({ + return lon; + }); + if ((watch_get_backup_data(1) == 0) && (browser_lat || browser_lon)) { + movement_location_t browser_loc; + browser_loc.bit.latitude = browser_lat; + browser_loc.bit.longitude = browser_lon; + watch_store_backup_data(browser_loc.reg, 1); + } +#endif + + + sunrise_sunset_state_t *state = (sunrise_sunset_state_t *)context; + movement_location_t movement_location = (movement_location_t) watch_get_backup_data(1); + state->working_latitude = _sunrise_sunset_face_struct_from_latlon(movement_location.bit.latitude); + state->working_longitude = _sunrise_sunset_face_struct_from_latlon(movement_location.bit.longitude); +} + +bool sunrise_sunset_face_loop(movement_event_t event, void *context) { + sunrise_sunset_state_t *state = (sunrise_sunset_state_t *)context; + + switch (event.event_type) { + case EVENT_ACTIVATE: + _sunrise_sunset_face_update(state); + break; + case EVENT_LOW_ENERGY_UPDATE: + case EVENT_TICK: + if (state->page == 0) { + // if entering low energy mode, start tick animation + if (event.event_type == EVENT_LOW_ENERGY_UPDATE && !watch_sleep_animation_is_running()) watch_start_sleep_animation(1000); + // check if we need to update the display + watch_date_time date_time = watch_rtc_get_date_time(); + if (date_time.reg >= state->rise_set_expires.reg) { + // and on the off chance that this happened before EVENT_TIMEOUT snapped us back to rise/set 0, go back now + state->rise_index = 0; + _sunrise_sunset_face_update(state); + } + } else { + _sunrise_sunset_face_update_settings_display(event, state); + } + break; + case EVENT_LIGHT_BUTTON_DOWN: + if (state->page) { + state->active_digit++; + if (state->page == 1 && state->active_digit == 1) state->active_digit++; // max latitude is +- 90, no hundreds place + if (state->active_digit > 5) { + state->active_digit = 0; + state->page = (state->page + 1) % 3; + _sunrise_sunset_face_update_location_register(state); + } + _sunrise_sunset_face_update_settings_display(event, context); + } else if (_location_count <= 1) { + movement_illuminate_led(); + } + if (state->page == 0) { + movement_request_tick_frequency(1); + _sunrise_sunset_face_update(state); + } + break; + case EVENT_LIGHT_LONG_PRESS: + if (_location_count <= 1) break; + else if (!state->page) movement_illuminate_led(); + break; + case EVENT_LIGHT_BUTTON_UP: + if (state->page == 0 && _location_count > 1) { + state->longLatToUse = (state->longLatToUse + 1) % _location_count; + _sunrise_sunset_face_update(state); + } + break; + case EVENT_ALARM_BUTTON_UP: + if (state->page) { + _sunrise_sunset_face_advance_digit(state); + _sunrise_sunset_face_update_settings_display(event, context); + } else { + state->rise_index = (state->rise_index + 1) % 2; + _sunrise_sunset_face_update(state); + } + break; + case EVENT_ALARM_LONG_PRESS: + if (state->page == 0) { + if (state->longLatToUse != 0) { + state->longLatToUse = 0; + _sunrise_sunset_face_update(state); + break; + } + state->page++; + state->active_digit = 0; + watch_clear_display(); + movement_request_tick_frequency(4); + _sunrise_sunset_face_update_settings_display(event, context); + } + else { + state->active_digit = 0; + state->page = 0; + _sunrise_sunset_face_update_location_register(state); + _sunrise_sunset_face_update(state); + } + break; + case EVENT_TIMEOUT: + if (watch_get_backup_data(1) == 0) { + // if no location set, return home + movement_move_to_face(0); + } else if (state->page || state->rise_index) { + // otherwise on timeout, exit settings mode and return to the next sunrise or sunset + state->page = 0; + state->rise_index = 0; + movement_request_tick_frequency(1); + _sunrise_sunset_face_update(state); + } + break; + default: + return movement_default_loop_handler(event); + } + + return true; +} + +void sunrise_sunset_face_resign(void *context) { + sunrise_sunset_state_t *state = (sunrise_sunset_state_t *)context; + state->page = 0; + state->active_digit = 0; + state->rise_index = 0; + _sunrise_sunset_face_update_location_register(state); +} diff --git a/watch-faces/complication/sunrise_sunset_face.h b/watch-faces/complication/sunrise_sunset_face.h new file mode 100644 index 0000000..133eed0 --- /dev/null +++ b/watch-faces/complication/sunrise_sunset_face.h @@ -0,0 +1,88 @@ +/* + * MIT License + * + * 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 SUNRISE_SUNSET_FACE_H_ +#define SUNRISE_SUNSET_FACE_H_ + +/* + * SUNRISE & SUNSET FACE + * + * The Sunrise/Sunset face is designed to display the next sunrise or sunset + * for a given location. It also functions as an interface for setting the + * location register, which other watch faces can use for various purposes. + * + * Refer to the wiki for usage instructions: + * https://www.sensorwatch.net/docs/watchfaces/complication/#sunrisesunset + */ + +#include "movement.h" + +typedef struct { + uint8_t sign: 1; // 0-1 + uint8_t hundreds: 1; // 0-1, ignored for latitude + uint8_t tens: 4; // 0-9 (must wrap at 10) + uint8_t ones: 4; // 0-9 (must wrap at 10) + uint8_t tenths: 4; // 0-9 (must wrap at 10) + uint8_t hundredths: 4; // 0-9 (must wrap at 10) +} sunrise_sunset_lat_lon_settings_t; + +typedef struct { + uint8_t page; + uint8_t rise_index; + uint8_t active_digit; + bool location_changed; + watch_date_time rise_set_expires; + sunrise_sunset_lat_lon_settings_t working_latitude; + sunrise_sunset_lat_lon_settings_t working_longitude; + uint8_t longLatToUse; +} sunrise_sunset_state_t; + +void sunrise_sunset_face_setup(uint8_t watch_face_index, void ** context_ptr); +void sunrise_sunset_face_activate(void *context); +bool sunrise_sunset_face_loop(movement_event_t event, void *context); +void sunrise_sunset_face_resign(void *context); + +#define sunrise_sunset_face ((const watch_face_t){ \ + sunrise_sunset_face_setup, \ + sunrise_sunset_face_activate, \ + sunrise_sunset_face_loop, \ + sunrise_sunset_face_resign, \ + NULL, \ +}) + +typedef struct { + char name[2]; + int16_t latitude; + int16_t longitude; +} long_lat_presets_t; + +static const long_lat_presets_t longLatPresets[] = +{ + { .name = " "}, // Default, the long and lat get replaced by what's set in the watch +// { .name = "Ny", .latitude = 4072, .longitude = -7401 }, // New York City, NY +// { .name = "LA", .latitude = 3405, .longitude = -11824 }, // Los Angeles, CA +// { .name = "dE", .latitude = 4221, .longitude = -8305 }, // Detroit, MI +}; + +#endif // SUNRISE_SUNSET_FACE_H_