diff options
author | randogoth <kflux@posteo.de> | 2023-04-16 15:13:21 +0100 |
---|---|---|
committer | GitHub <noreply@github.com> | 2023-04-16 10:13:21 -0400 |
commit | 2b876236112b8f7f85d4ff0675f8fac8ddc190b9 (patch) | |
tree | b669b29f1cf7e0fabad5a10c5b293f18a4dcaff9 /movement | |
parent | 462f24b31321107834d00016d0b07aa044be5d7e (diff) | |
download | Sensor-Watch-2b876236112b8f7f85d4ff0675f8fac8ddc190b9.tar.gz Sensor-Watch-2b876236112b8f7f85d4ff0675f8fac8ddc190b9.tar.bz2 Sensor-Watch-2b876236112b8f7f85d4ff0675f8fac8ddc190b9.zip |
Planetary Hours Faces (#234)
* initial setup & test
* simplified solar calulcation function
* Initial Release
* initial setup
* UTC offset fixes
* finished and cleaned up
* renamed faces
* optimizations & astro symbols
* wrapping ruler
* cleanup
* optimizations
* merged latest place_face
* documentation
* fixed faster seconds
* removed place_face from branch
* reset config
---------
Co-authored-by: NN Solex <info@sublunar.space>
Co-authored-by: joeycastillo <joeycastillo@utexas.edu>
Diffstat (limited to 'movement')
-rw-r--r-- | movement/make/Makefile | 2 | ||||
-rw-r--r-- | movement/movement_faces.h | 2 | ||||
-rw-r--r-- | movement/watch_faces/complication/planetary_hours_face.c | 401 | ||||
-rw-r--r-- | movement/watch_faces/complication/planetary_hours_face.h | 107 | ||||
-rw-r--r-- | movement/watch_faces/complication/planetary_time_face.c | 337 | ||||
-rw-r--r-- | movement/watch_faces/complication/planetary_time_face.h | 108 |
6 files changed, 957 insertions, 0 deletions
diff --git a/movement/make/Makefile b/movement/make/Makefile index 4614cd27..50ba05cf 100644 --- a/movement/make/Makefile +++ b/movement/make/Makefile @@ -104,6 +104,8 @@ SRCS += \ ../watch_faces/sensor/lightmeter_face.c \ ../watch_faces/complication/discgolf_face.c \ ../watch_faces/complication/habit_face.c \ + ../watch_faces/complication/planetary_time_face.c \ + ../watch_faces/complication/planetary_hours_face.c \ ../watch_faces/complication/breathing_face.c \ ../watch_faces/clock/repetition_minute_face.c \ ../watch_faces/complication/timer_face.c \ diff --git a/movement/movement_faces.h b/movement/movement_faces.h index afa8f14a..7ff1d1ee 100644 --- a/movement/movement_faces.h +++ b/movement/movement_faces.h @@ -79,6 +79,8 @@ #include "lightmeter_face.h" #include "discgolf_face.h" #include "habit_face.h" +#include "planetary_time_face.h" +#include "planetary_hours_face.h" #include "breathing_face.h" #include "repetition_minute_face.h" #include "timer_face.h" diff --git a/movement/watch_faces/complication/planetary_hours_face.c b/movement/watch_faces/complication/planetary_hours_face.c new file mode 100644 index 00000000..f5f36f19 --- /dev/null +++ b/movement/watch_faces/complication/planetary_hours_face.c @@ -0,0 +1,401 @@ +/* + * MIT License + * + * Copyright (c) 2023 Tobias Raayoni Last / @randogoth + * + * 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 <math.h> +#include "sunriset.h" +#include "watch.h" +#include "watch_utility.h" +#include "planetary_hours_face.h" + +#if __EMSCRIPTEN__ +#include <emscripten.h> +#endif + +// STATIC FUNCTIONS AND CONSTANTS ///////////////////////////////////////////// + +/** @brief Planetary rulers in the Chaldean order from slowest to fastest + * @details Planetary rulers in the Chaldean order from slowest to fastest: + * Jupiter, Mars, Sun, Venus, Mercury, Moon + */ +static const char planets[7][3] = {"Sa", "Ju", "Ma", "So", "Ve", "Me", "Lu"}; // Latin +static const char planetes[7][3] = {"Ch", "Ze", "Ar", "He", "Af", "Hr", "Se"}; // Greek + +/** @brief Ruler of each weekday for easy lookup + */ +static const uint8_t plindex[7] = {3, 6, 2, 5, 1, 4, 0}; // day ruler index + +/** @brief Astrological symbol for each planet + */ +static void _planetary_icon(uint8_t planet) { + + watch_clear_pixel(0, 13); + watch_clear_pixel(0, 14); + watch_clear_pixel(1, 13); + watch_clear_pixel(1, 14); + watch_clear_pixel(1, 15); + watch_clear_pixel(2, 13); + watch_clear_pixel(2, 14); + watch_clear_pixel(2, 15); + + switch (planet) { + case 0: // Saturn + watch_set_pixel(0, 14); + watch_set_pixel(2, 14); + watch_set_pixel(1, 15); + watch_set_pixel(2, 13); + break; + case 1: // Jupiter + watch_set_pixel(0, 14); + watch_set_pixel(1, 15); + watch_set_pixel(1, 14); + break; + case 2: // Mars + watch_set_pixel(2, 14); + watch_set_pixel(2, 15); + watch_set_pixel(1, 15); + watch_set_pixel(2, 13); + watch_set_pixel(1, 13);\ + break; + case 3: // Sol + watch_set_pixel(0, 14); + watch_set_pixel(2, 14); + watch_set_pixel(1, 13); + watch_set_pixel(2, 13); + watch_set_pixel(0, 13); + watch_set_pixel(2, 15); + break; + case 4: // Venus + watch_set_pixel(0, 14); + watch_set_pixel(0, 13); + watch_set_pixel(1, 13); + watch_set_pixel(1, 15); + watch_set_pixel(1, 14); + break; + case 5: // Mercury + watch_set_pixel(0, 14); + watch_set_pixel(1, 13); + watch_set_pixel(1, 14); + watch_set_pixel(1, 15); + watch_set_pixel(2, 15); + break; + case 6: // Luna + watch_set_pixel(2, 14); + watch_set_pixel(2, 15); + watch_set_pixel(2, 13); + break; + } +} + +/** @details A solar phase can be a day phase between sunrise and sunset or an alternating night phase. + * This function calculates the start and end of the current phase based on a given geographic location. + * It also calculates the start of the next following phase. + */ +static void _planetary_solar_phases(movement_settings_t *settings, planetary_hours_state_t *state) { + uint8_t phase, h; + double sunrise, sunset; + double hour_duration, next_hour_duration; + uint32_t now_epoch; + uint32_t sunrise_epoch_today, sunset_epoch_today, midnight_epoch_today; + uint32_t sunset_epoch_yesterday, midnight_epoch_yesterday; + uint32_t sunrise_epoch_tomorrow, sunset_epoch_tomorrow, midnight_epoch_tomorrow; + movement_location_t movement_location = (movement_location_t) watch_get_backup_data(1); + + // check if we have a location. If not, display error + if (movement_location.reg == 0) { + watch_display_string(" no Loc", 0); + state->no_location = true; + return; + } + + // location detected + state->no_location = false; + + 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_timezone_offsets[settings->bit.time_zone] * 60, 0); // the current date / time in UTC + watch_date_time scratch_time; // scratchpad, contains different values at different times + watch_date_time midnight; + scratch_time.reg = midnight.reg = utc_now.reg; + midnight.unit.hour = midnight.unit.minute = midnight.unit.second = 0; // start of the day at midnight + + // get location coordinate + 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; + + // save UTC offset + state->utc_offset = ((double)movement_timezone_offsets[settings->bit.time_zone]) / 60.0; + + // calculate sunrise and sunset of current day in decimal hours after midnight + sun_rise_set(scratch_time.unit.year + WATCH_RTC_REFERENCE_YEAR, scratch_time.unit.month, scratch_time.unit.day, lon, lat, &sunrise, &sunset); + + // calculate sunrise and sunset UNIX timestamps + midnight_epoch_today = watch_utility_date_time_to_unix_time(midnight, 0); + sunrise_epoch_today = midnight_epoch_today + sunrise * 3600; + sunset_epoch_today = midnight_epoch_today + sunset * 3600; + + // go back to yesterday and calculate sunset + midnight_epoch_yesterday = midnight_epoch_today - 86400; + scratch_time = watch_utility_date_time_from_unix_time(midnight_epoch_yesterday, 0); + sun_rise_set(scratch_time.unit.year + WATCH_RTC_REFERENCE_YEAR, scratch_time.unit.month, scratch_time.unit.day, lon, lat, &sunrise, &sunset); + sunset_epoch_yesterday = midnight_epoch_yesterday + sunset * 3600; + + // go to tomorrow and calculate sunrise and sunset + midnight_epoch_tomorrow = midnight_epoch_today + 86400; + scratch_time = watch_utility_date_time_from_unix_time(midnight_epoch_tomorrow, 0); + sun_rise_set(scratch_time.unit.year + WATCH_RTC_REFERENCE_YEAR, scratch_time.unit.month, scratch_time.unit.day, lon, lat, &sunrise, &sunset); + sunrise_epoch_tomorrow = midnight_epoch_tomorrow + sunrise * 3600; + sunset_epoch_tomorrow = midnight_epoch_tomorrow + sunset * 3600; + + // get UNIX epoch time + now_epoch = watch_utility_date_time_to_unix_time(utc_now, 0); + + // by default we assume it is daytime (phase 1) between sunrise and sunset + phase = 1; + state->phase_start = sunrise_epoch_today; + state->phase_end = sunset_epoch_today; + state->phase_next = sunrise_epoch_tomorrow; + state->start_at_night = false; + + // night time calculations + if ( now_epoch < sunrise_epoch_today && now_epoch < sunset_epoch_today ) phase = 0; // morning before dawn + if ( now_epoch > sunrise_epoch_today && now_epoch >= sunset_epoch_today ) phase = 2; // evening after dusk + + // phase 0: we are before sunrise + if ( phase == 0) { + state->phase_start = sunset_epoch_yesterday; + state->phase_end = sunrise_epoch_today; + state->phase_next = sunset_epoch_today; + state->start_at_night = true; + } + + // phase 2: we are after sunset + if ( phase == 2) { + state->phase_start = sunset_epoch_today; + state->phase_end = sunrise_epoch_tomorrow; + state->phase_next = sunset_epoch_tomorrow; + state->start_at_night = true; + } + + // calculate the duration of a planetary hour during this and the next solar phase + hour_duration = ( state->phase_end - state->phase_start ) / 12.0; + next_hour_duration = ( state->phase_next - state->phase_end ) / 12.0; + + // populate list of 24 planetary hour start points in UNIX timestamp format + // starting from the beginning of the current phase + for ( h = 0; h < 24; h++ ) { + if ( h < 12 ) state->planetary_hours[h] = state->phase_start + h * hour_duration; // current phase + else state->planetary_hours[h] = state->phase_end + ( h - 12 ) * next_hour_duration; // next phase + } + + // initialize + state->hour = 0; + state->ruler = 0; + state->skip_to_current = true; + +} + +/** @details A planetary hour is one of exactly twelve hours of a solar phase. Its length varies. + * This function calculates the current planetary hour and divides it up into relative minutes and seconds. + * It also calculates the current planetary ruler of the hour and of the day. + */ +static void _planetary_hours(movement_settings_t *settings, planetary_hours_state_t *state) { + char buf[14]; + char ruler[3]; + uint8_t weekday, planet, planetary_hour; + uint32_t current_hour_epoch; + watch_date_time scratch_time; + + // check if we have a location. If not, display error + if ( state->no_location ) { + watch_display_string(" no Loc", 0); + return; + } + + // get current time + 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_timezone_offsets[settings->bit.time_zone] * 60, 0); // the current date / time in UTC + current_hour_epoch = watch_utility_date_time_to_unix_time(utc_now, 0); + + // set the current planetary hour as default screen + if ( state->skip_to_current ) { + state->hour = ( current_hour_epoch - state->phase_start ) / (( state->phase_end - state->phase_start ) / 12.0); + state->skip_to_current = false; + } + + + // when current phase ends calculate the next phase + if ( watch_utility_date_time_to_unix_time(utc_now, 0) >= state->phase_end ) { + _planetary_solar_phases(settings, state); + return; + } + + if (settings->bit.clock_mode_24h) watch_set_indicator(WATCH_INDICATOR_24H); + + // roll over hour iterator + if ( state->hour < 0 ) state->hour = 23; + if ( state->hour > 23 ) state->hour = 0; + if ( state->ruler < 0 ) state->hour = 2; + if ( state->ruler > 2 ) state->hour = 0; + + // clear indicators + watch_clear_indicator(WATCH_INDICATOR_BELL); + watch_clear_indicator(WATCH_INDICATOR_LAP); + + // display bell indicator when displaying the current planetary hour + if ( state->hour < 24 ) + if ( current_hour_epoch >= state->planetary_hours[state->hour] && current_hour_epoch < state->planetary_hours[state->hour + 1]) { + watch_set_indicator(WATCH_INDICATOR_BELL); + } + + // display LAP indicator when the hours of the next phase belong to the next day + if ( state->start_at_night == true && state->hour > 11 ) + watch_set_indicator(WATCH_INDICATOR_LAP); + + // determine weekday from start of current phase + scratch_time = watch_utility_date_time_from_unix_time(state->phase_start, 0); + scratch_time = watch_utility_date_time_convert_zone(scratch_time, 0, state->utc_offset * 3600); + weekday = watch_utility_get_iso8601_weekday_number(scratch_time.unit.year, scratch_time.unit.month, scratch_time.unit.day) - 1; + + // which planetary hour are we in? + planetary_hour = state->hour % 12; + + // accomodate night hour count + if ( state->hour < 12 ) { + if ( state->start_at_night ) { + planetary_hour += 12; + } + } else { + if ( state->start_at_night ) { + weekday = ( weekday + 1 ) % 7; + } else { + planetary_hour += 12; + } + } + + // make datetime object for selected planetary hour + scratch_time = watch_utility_date_time_from_unix_time(state->planetary_hours[state->hour], 0); + scratch_time = watch_utility_date_time_convert_zone(scratch_time, 0, state->utc_offset * 3600); + + // round minutes + if (scratch_time.unit.second < 30 && scratch_time.unit.minute > 0 ) scratch_time.unit.minute--; + else if ( scratch_time.unit.minute < 59 ) scratch_time.unit.minute++; + + // if we are in 12 hour mode, do some cleanup + if (!settings->bit.clock_mode_24h) { + if (scratch_time.unit.hour < 12) { + watch_clear_indicator(WATCH_INDICATOR_PM); + } else { + watch_set_indicator(WATCH_INDICATOR_PM); + } + scratch_time.unit.hour %= 12; + if (scratch_time.unit.hour == 0) scratch_time.unit.hour = 12; + } + + // planetary ruler of the hour + planet = ( plindex[weekday] + planetary_hour ) % 7; + + // latin or greek ruler names or astrological symbol + if ( state->ruler == 0 ) strncpy(ruler, planets[planet], 3); + if ( state->ruler == 1 ) strncpy(ruler, planetes[planet], 3); + if ( state->ruler == 2 ) strncpy(ruler, " ", 3); + + // display planetary time with ruler of the hour or ruler of the day + sprintf(buf, "%s%2d%2d%02d ", ruler, (planetary_hour % 24) + 1, scratch_time.unit.hour, scratch_time.unit.minute); + + watch_set_colon(); + watch_display_string(buf, 0); + + if ( state->ruler == 2 ) _planetary_icon(planet); +} + +// PUBLIC WATCH FACE FUNCTIONS //////////////////////////////////////////////// + +void planetary_hours_face_setup(movement_settings_t *settings, uint8_t watch_face_index, void ** context_ptr) { + (void) settings; + if (*context_ptr == NULL) { + *context_ptr = malloc(sizeof(planetary_hours_state_t)); + memset(*context_ptr, 0, sizeof(planetary_hours_state_t)); + } +} + +void planetary_hours_face_activate(movement_settings_t *settings, void *context) { + (void) settings; + if (watch_tick_animation_is_running()) watch_stop_tick_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 + + planetary_hours_state_t *state = (planetary_hours_state_t *)context; + _planetary_solar_phases(settings, state); + +} + +bool planetary_hours_face_loop(movement_event_t event, movement_settings_t *settings, void *context) { + planetary_hours_state_t *state = (planetary_hours_state_t *)context; + + switch (event.event_type) { + case EVENT_ACTIVATE: + // Show your initial UI here. + watch_clear_indicator(WATCH_INDICATOR_PM); + watch_clear_indicator(WATCH_INDICATOR_24H); + _planetary_hours(settings, state); + break; + case EVENT_LIGHT_BUTTON_UP: + state->ruler = (state->ruler + 1) % 3; + _planetary_hours(settings, state); + break; + case EVENT_LIGHT_LONG_PRESS: + state->skip_to_current = true; + _planetary_hours(settings, state); + break; + case EVENT_ALARM_BUTTON_UP: + state->hour++; + _planetary_hours(settings, state); + break; + case EVENT_ALARM_LONG_PRESS: + state->hour--; + _planetary_hours(settings, state); + break; + default: + return movement_default_loop_handler(event, settings); + } + return true; +} + +void planetary_hours_face_resign(movement_settings_t *settings, void *context) { + (void) settings; + (void) context; +} + diff --git a/movement/watch_faces/complication/planetary_hours_face.h b/movement/watch_faces/complication/planetary_hours_face.h new file mode 100644 index 00000000..53237df2 --- /dev/null +++ b/movement/watch_faces/complication/planetary_hours_face.h @@ -0,0 +1,107 @@ +/* + * MIT License + * + * Copyright (c) 2023 Tobias Raayoni Last / @randogoth + * Copyright (c) 2022 Joey Castillo (sunrise_sunset_face) + * + * 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 planetary_hours_face_H_ +#define planetary_hours_face_H_ + +#include "movement.h" +#include "sunrise_sunset_face.h" + +/* + * BACKGROUND + + * Both the 24 hour day and the order of our weekdays have quite esoteric roots. + * The ancient Egyptians divided the day up into 12 hours of sunlight and 12 hours + * of night time. Obviously the length of these hours varied throughout the year. + * + * The Greeks assigned each hour a ruler of the planetary gods in the ancient + * "Chaldean" order from slowest (Chronos for Saturn) to fastest (Selene for Moon). + * Because 24 hours cannot be equally divided by seven, the planetary rulers carried + * over to the first hour of the next day, effectively ruling over the entire day + * and lending the whole day their name. The seven day week was born. + * + * PLANETARY HOUR CHART COMPLICATION + * + * This complication watch face displays the start time of the current planetary hour + * according to the given location and day of the year. The number of the current + * planetary hour (1 - 24) is indicated at the top right. + * + * Short pressing the ALARM button flips through the start times of the following + * planetary hours, long pressing it flips backwards in time. A long press of the + * LIGHT button immediately switches back to the start time of the current hour. + * The Bell indicator always marks the current planetary hour in the list. + * The LAP indicator shows up when the hours of the next phase are part of the + * upcoming day instead of the current one. This happens when the watch face is + * launched after sunset. + * + * The planetary ruler of the current hour and day is displayed at the top in + * Latin or Greek shorthand notation: + * + * Saturn (SA) / Chronos (CH) / ♄ + * Jupiter (JU) / Zeus (ZE) / ♃ + * Mars (MA) / Ares (AR) / ♂ + * Sol (SO) / Helios (HE) / ☉ + * Venus (VE) / Aphrodite (AF) / ♀ + * Mercury (ME) / Hermes (HR) / ☿ + * Luna (LU) / Selene (SE) / ☾ + * + * A short press of the LIGHT button toggles between Latin and Greek ruler shorthand + * notation. + * + * (IMPORTANT: Make sure the watch's time, timezone and location are set correctly for this + * watch face to work properly!) + */ + +typedef struct { + // Anything you need to keep track of, put it here! + uint32_t planetary_hours[24]; + uint32_t phase_start; + uint32_t phase_end; + uint32_t phase_next; + bool next; + double utc_offset; + bool no_location; + int8_t hour; + int8_t ruler; + bool start_at_night; + bool skip_to_current; + sunrise_sunset_state_t sunstate; +} planetary_hours_state_t; + +void planetary_hours_face_setup(movement_settings_t *settings, uint8_t watch_face_index, void ** context_ptr); +void planetary_hours_face_activate(movement_settings_t *settings, void *context); +bool planetary_hours_face_loop(movement_event_t event, movement_settings_t *settings, void *context); +void planetary_hours_face_resign(movement_settings_t *settings, void *context); + +#define planetary_hours_face ((const watch_face_t){ \ + planetary_hours_face_setup, \ + planetary_hours_face_activate, \ + planetary_hours_face_loop, \ + planetary_hours_face_resign, \ + NULL, \ +}) + +#endif // planetary_hours_face_H_ + diff --git a/movement/watch_faces/complication/planetary_time_face.c b/movement/watch_faces/complication/planetary_time_face.c new file mode 100644 index 00000000..3d4ce34e --- /dev/null +++ b/movement/watch_faces/complication/planetary_time_face.c @@ -0,0 +1,337 @@ +/* + * MIT License + * + * Copyright (c) 2023 Tobias Raayoni Last / @randogoth + * + * 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 <math.h> +#include "sunriset.h" +#include "watch.h" +#include "watch_utility.h" +#include "planetary_time_face.h" + +#if __EMSCRIPTEN__ +#include <emscripten.h> +#endif + +// STATIC FUNCTIONS AND CONSTANTS ///////////////////////////////////////////// + +/** @brief Planetary rulers in the Chaldean order from slowest to fastest + * @details Planetary rulers in the Chaldean order from slowest to fastest: + * Jupiter, Mars, Sun, Venus, Mercury, Moon + */ +static const char planets[7][3] = {"Sa", "Ju", "Ma", "So", "Ve", "Me", "Lu"}; // Latin +static const char planetes[7][3] = {"Ch", "Ze", "Ar", "He", "Af", "Hr", "Se"}; // Greek + +/** @brief Ruler of each weekday for easy lookup + */ +static const uint8_t plindex[7] = {3, 6, 2, 5, 1, 4, 0}; // day ruler index + +/** @brief Astrological symbol for each planet + */ +static void _planetary_icon(uint8_t planet) { + + watch_clear_pixel(0, 13); + watch_clear_pixel(0, 14); + watch_clear_pixel(1, 13); + watch_clear_pixel(1, 14); + watch_clear_pixel(1, 15); + watch_clear_pixel(2, 13); + watch_clear_pixel(2, 14); + watch_clear_pixel(2, 15); + + switch (planet) { + case 0: // Saturn + watch_set_pixel(0, 14); + watch_set_pixel(2, 14); + watch_set_pixel(1, 15); + watch_set_pixel(2, 13); + break; + case 1: // Jupiter + watch_set_pixel(0, 14); + watch_set_pixel(1, 15); + watch_set_pixel(1, 14); + break; + case 2: // Mars + watch_set_pixel(2, 14); + watch_set_pixel(2, 15); + watch_set_pixel(1, 15); + watch_set_pixel(2, 13); + watch_set_pixel(1, 13);\ + break; + case 3: // Sol + watch_set_pixel(0, 14); + watch_set_pixel(2, 14); + watch_set_pixel(1, 13); + watch_set_pixel(2, 13); + watch_set_pixel(0, 13); + watch_set_pixel(2, 15); + break; + case 4: // Venus + watch_set_pixel(0, 14); + watch_set_pixel(0, 13); + watch_set_pixel(1, 13); + watch_set_pixel(1, 15); + watch_set_pixel(1, 14); + break; + case 5: // Mercury + watch_set_pixel(0, 14); + watch_set_pixel(1, 13); + watch_set_pixel(1, 14); + watch_set_pixel(1, 15); + watch_set_pixel(2, 15); + break; + case 6: // Luna + watch_set_pixel(2, 14); + watch_set_pixel(2, 15); + watch_set_pixel(2, 13); + break; + } +} + +/** @details solar phase can be a day phase between sunrise and sunset or an alternating night phase. + * This function calculates the start and end of the current phase based on a given geographic location. + */ +static void _planetary_solar_phase(movement_settings_t *settings, planetary_time_state_t *state) { + uint8_t phase; + double sunrise, sunset; + uint32_t now_epoch, sunrise_epoch, sunset_epoch, midnight_epoch; + movement_location_t movement_location = (movement_location_t) watch_get_backup_data(1); + + // check if we have a location. If not, display error + if (movement_location.reg == 0) { + watch_display_string(" no Loc", 0); + state->no_location = true; + return; + } + + // location detected + state->no_location = false; + + 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_timezone_offsets[settings->bit.time_zone] * 60, 0); // the current date / time in UTC + watch_date_time scratch_time; // scratchpad, contains different values at different times + watch_date_time midnight; + scratch_time.reg = midnight.reg = utc_now.reg; + midnight.unit.hour = midnight.unit.minute = midnight.unit.second = 0; // start of the day at midnight + + // get location coordinate + 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; + + // save UTC offset + state->utc_offset = ((double)movement_timezone_offsets[settings->bit.time_zone]) / 60.0; + + // get UNIX epoch time + now_epoch = watch_utility_date_time_to_unix_time(utc_now, 0); + midnight_epoch = watch_utility_date_time_to_unix_time(midnight, 0); + + // calculate sunrise and sunset of current day in decimal hours after midnight + sun_rise_set(scratch_time.unit.year + WATCH_RTC_REFERENCE_YEAR, scratch_time.unit.month, scratch_time.unit.day, lon, lat, &sunrise, &sunset); + + // calculate sunrise and sunset UNIX timestamps + sunrise_epoch = midnight_epoch + sunrise * 3600; + sunset_epoch = midnight_epoch + sunset * 3600; + + // by default we assume it is daytime (phase 1) between sunrise and sunset + phase = 1; + state->night = false; + state->phase_start = sunrise_epoch; + state->phase_end = sunset_epoch; + + // night time calculations + if ( now_epoch < sunrise_epoch && now_epoch < sunset_epoch ) phase = 0; // morning before dawn + if ( now_epoch > sunrise_epoch && now_epoch >= sunset_epoch ) phase = 2; // evening after dusk + + // phase 0: we are before sunrise + if ( phase == 0) { + // go back to yesterday and calculate sunset + midnight_epoch -= 86400; + scratch_time = watch_utility_date_time_from_unix_time(midnight_epoch, 0); + sun_rise_set(scratch_time.unit.year + WATCH_RTC_REFERENCE_YEAR, scratch_time.unit.month, scratch_time.unit.day, lon, lat, &sunrise, &sunset); + sunset_epoch = midnight_epoch + sunset * 3600; + // we are still in yesterday's night hours + state->night = true; + state->phase_start = sunset_epoch; + state->phase_end = sunrise_epoch; + } + + // phase 2: we are after sunset + if ( phase == 2) { + // skip to tomorrow and calculate sunrise + midnight_epoch += 86400; + scratch_time = watch_utility_date_time_from_unix_time(midnight_epoch, 0); + sun_rise_set(scratch_time.unit.year + WATCH_RTC_REFERENCE_YEAR, scratch_time.unit.month, scratch_time.unit.day, lon, lat, &sunrise, &sunset); + sunrise_epoch = midnight_epoch + sunrise * 3600; + // we are still in yesterday's night hours + state->night = true; + state->phase_start = sunset_epoch; + state->phase_end = sunrise_epoch; + } + + // calculate the duration of a planetary second during this solar phase + // and convert to Hertz so we can call a faster tick rate + state->freq = (1 / ((double)( state->phase_end - state->phase_start ) / 43200)); +} + +/** @details A planetary hour is one of exactly twelve hours of a solar phase. Its length varies. + * This function calculates the current planetary hour and divides it up into relative minutes and seconds. + * It also calculates the current planetary ruler of the hour and of the day. + */ +static void _planetary_time(movement_event_t event, movement_settings_t *settings, planetary_time_state_t *state) { + char buf[14]; + char ruler[3]; + double night_hour_count = 0.0; + uint8_t weekday, planet, planetary_hour; + double hour_duration, current_hour, current_minute, current_second, second_duration; + + watch_set_colon(); + + // get current time and convert to UTC + state->scratch = watch_utility_date_time_convert_zone(watch_rtc_get_date_time(), movement_timezone_offsets[settings->bit.time_zone] * 60, 0); + + // when current phase ends calculate the next phase + if ( watch_utility_date_time_to_unix_time(state->scratch, 0) >= state->phase_end ) { + _planetary_solar_phase(settings, state); + return; + } + + if (settings->bit.clock_mode_24h) watch_set_indicator(WATCH_INDICATOR_24H); + + // PM for night hours, otherwise the night hours are counted from 13 + if ( state->night ) { + if (settings->bit.clock_mode_24h) night_hour_count = 12; + else watch_set_indicator(WATCH_INDICATOR_PM); + } + + // calculate the duration of a planetary hour during this solar phase + hour_duration = (( state->phase_end - state->phase_start)) / 12.0; + + // which planetary hour are we in? + + // RTC only provides full second precision, so we have to manually add subseconds with each tick + current_hour = ((( watch_utility_date_time_to_unix_time(state->scratch, 0) ) + event.subsecond * 0.11111111) - state->phase_start ) / hour_duration; + planetary_hour = floor(current_hour) + ( state->night ? 12 : 0 ); + current_hour += night_hour_count; //adjust for 24hr display + current_minute = modf(current_hour, ¤t_hour) * 60.0; + current_second = modf(current_minute, ¤t_minute) * 60.0; + + // the day changes after sunrise, so if we are at night it is yesterday's planetary day + // hence we take the datetime object of when the last solar phase started as the current day + // and then fill in the hours and minutes + state->scratch = watch_utility_date_time_from_unix_time(state->phase_start, 0); + state->scratch.unit.hour = floor(current_hour); + state->scratch.unit.minute = floor(current_minute); + state->scratch.unit.second = (uint8_t)floor(current_second) % 60; + + // what weekday is it (0 - 6) + weekday = watch_utility_get_iso8601_weekday_number(state->scratch.unit.year, state->scratch.unit.month, state->scratch.unit.day) - 1; + + // planetary ruler of the hour or the day + if ( state->day_ruler ) planet = plindex[weekday]; + else planet = ( plindex[weekday] + planetary_hour ) % 7; + + // latin or greek ruler names or astrological symbol + if ( state->ruler == 0 ) strncpy(ruler, planets[planet], 3); + if ( state->ruler == 1 ) strncpy(ruler, planetes[planet], 3); + if ( state->ruler == 2 ) strncpy(ruler, " ", 3); + + // display planetary time with ruler of the hour or ruler of the day + if ( state->day_ruler ) sprintf(buf, "%s d%2d%02d%02d", ruler, state->scratch.unit.hour, state->scratch.unit.minute, state->scratch.unit.second); + else sprintf(buf, "%s h%2d%02d%02d", ruler, state->scratch.unit.hour, state->scratch.unit.minute, state->scratch.unit.second); + + watch_display_string(buf, 0); + + if ( state->ruler == 2 ) _planetary_icon(planet); + +} + +// PUBLIC WATCH FACE FUNCTIONS //////////////////////////////////////////////// + +void planetary_time_face_setup(movement_settings_t *settings, uint8_t watch_face_index, void ** context_ptr) { + (void) settings; + if (*context_ptr == NULL) { + *context_ptr = malloc(sizeof(planetary_time_state_t)); + memset(*context_ptr, 0, sizeof(planetary_time_state_t)); + } +} + +void planetary_time_face_activate(movement_settings_t *settings, void *context) { + (void) settings; + if (watch_tick_animation_is_running()) watch_stop_tick_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 + + planetary_time_state_t *state = (planetary_time_state_t *)context; + + // calculate phase + _planetary_solar_phase(settings, state); +} + +bool planetary_time_face_loop(movement_event_t event, movement_settings_t *settings, void *context) { + planetary_time_state_t *state = (planetary_time_state_t *)context; + + switch (event.event_type) { + case EVENT_ACTIVATE: + _planetary_time(event, settings, state); + if ( state->freq > 1 ) + // for hours with shorter seconds let's increase the tick to not skip seconds in the display + movement_request_tick_frequency( 8 ); + break; + case EVENT_TICK: + _planetary_time(event, settings, state); + break; + case EVENT_LIGHT_BUTTON_UP: + state->ruler = (state->ruler + 1) % 3; + break; + case EVENT_ALARM_BUTTON_UP: + // Just in case you have need for another button. + state->day_ruler = !state->day_ruler; + break; + case EVENT_LOW_ENERGY_UPDATE: + watch_start_tick_animation(500); + break; + default: + return movement_default_loop_handler(event, settings); + } + + return true; +} + +void planetary_time_face_resign(movement_settings_t *settings, void *context) { + (void) settings; + (void) context; + movement_request_tick_frequency( 1 ); +} + diff --git a/movement/watch_faces/complication/planetary_time_face.h b/movement/watch_faces/complication/planetary_time_face.h new file mode 100644 index 00000000..0ecc11af --- /dev/null +++ b/movement/watch_faces/complication/planetary_time_face.h @@ -0,0 +1,108 @@ +/* + * MIT License + * + * Copyright (c) 2023 Tobias Raayoni Last / @randogoth + * Copyright (c) 2022 Joey Castillo (sunrise_sunset_face) + * + * 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 planetary_time_face_H_ +#define planetary_time_face_H_ + +#include "movement.h" +#include "sunrise_sunset_face.h" + +/* + * BACKGROUND + + * Both the 24 hour day and the order of our weekdays have quite esoteric roots. + * The ancient Egyptians divided the day up into 12 hours of sunlight and 12 hours + * of night time. Obviously the length of these hours varied throughout the year. + * + * The Greeks assigned each hour a ruler of the planetary gods in the ancient + * "Chaldean" order from slowest (Chronos for Saturn) to fastest (Selene for Moon). + * Because 24 hours cannot be equally divided by seven, the planetary rulers carried + * over to the first hour of the next day, effectively ruling over the entire day + * and lending the whole day their name. The seven day week was born. + * + * PLANETARY TIME COMPLICATION + * + * The hour digits of this complication watch-face display the current planetary hour + * according to the given location and day of the year (First hour from 12am to 1am, + * the second hour from 1am to 2am, and so forth). + * + * Like with normal clocks the minutes and seconds help dividing the hour into smaller + * units. On this watch-face, all units naturally vary in length because the planetary + * hours are not fixed by duration but by the moments of sunrise and sunset which + * obviously vary throughout the year, especially in higher latitudes. + * + * On this watch-face the hours indicated as 12am to 12pm (00:00 - 12:00) are used for + * the planetary daytime hours between sunrise and sunset and hours indicated as 12pm + * to 12am (12:00 - 00:00) are used for the planetary night hours after sunset and before + * sunrise. + * + * The planetary ruler of the current hour and day is displayed at the top in Latin or + * Greek shorthand notation: + * + * Saturn (SA) / Chronos (CH) / ♄ + * Jupiter (JU) / Zeus (ZE) / ♃ + * Mars (MA) / Ares (AR) / ♂ + * Sol (SO) / Helios (HE) / ☉ + * Venus (VE) / Aphrodite (AF) / ♀ + * Mercury (ME) / Hermes (HR) / ☿ + * Luna (LU) / Selene (SE) / ☾ + * + * The ALARM button toggles between displaying the ruler of the hour and the ruler of the day + * + * The LIGHT button toggles between Latin and Greek ruler shorthand notation + * + * (IMPORTANT: Make sure the watch's time, timezone and location are set correctly for this + * watch face to work properly!) + */ + +typedef struct { + // Anything you need to keep track of, put it here! + uint32_t phase_start; + uint32_t phase_end; + bool night; + double utc_offset; + double freq; + uint8_t ruler; + bool day_ruler; + bool no_location; + sunrise_sunset_state_t sunstate; + watch_date_time scratch; +} planetary_time_state_t; + +void planetary_time_face_setup(movement_settings_t *settings, uint8_t watch_face_index, void ** context_ptr); +void planetary_time_face_activate(movement_settings_t *settings, void *context); +bool planetary_time_face_loop(movement_event_t event, movement_settings_t *settings, void *context); +void planetary_time_face_resign(movement_settings_t *settings, void *context); + +#define planetary_time_face ((const watch_face_t){ \ + planetary_time_face_setup, \ + planetary_time_face_activate, \ + planetary_time_face_loop, \ + planetary_time_face_resign, \ + NULL, \ +}) + +#endif // planetary_time_face_H_ + |