diff --git a/movement/lib/moonrise/moonrise.c b/movement/lib/moonrise/moonrise.c new file mode 100644 index 00000000..379d63ad --- /dev/null +++ b/movement/lib/moonrise/moonrise.c @@ -0,0 +1,304 @@ +// Compute times of moonrise and moonset at a specified latitude and longitude. +// +// This software minimizes computational work by performing the full calculation +// of the lunar position three times, at the beginning, middle, and end of the +// period of interest. Three point interpolation is used to predict the +// position for each hour, and the arithmetic mean is used to predict the +// half-hour positions. +// +// The full computational burden is negligible on modern computers, but the +// algorithm is effective and still useful for small embedded systems. +// +// This software was originally adapted to javascript by Stephen R. Schmitt +// from a BASIC program from the 'Astronomical Computing' column of Sky & +// Telescope, April 1989, page 78. +// +// Subsequently adapted from Stephen R. Schmitt's javascript to c++ for the +// Arduino by Cyrus Rahman. +// +// Subsequently adapted from Cyrus Rahman's Arduino C++ to C for the Sensor +// Watch by hueso, this work is subject to Stephen Schmitt's copyright: +// +// Copyright 2007 Stephen R. Schmitt +// Subsequent work Copyright 2020 Cyrus Rahman +// You may use or modify this source code in any way you find useful, provided +// that you agree that the author(s) have no warranty, obligations or liability. +// You must determine the suitability of this source code for your use. +// +// Redistributions of this source code must retain this copyright notice. + +#include "moonrise.h" +#include + +#define K1 15 * (M_PI / 180) * 1.0027379 + +// Determine the nearest moon rise or set event previous, and the nearest +// moon rise or set event subsequent, to the specified time in seconds since the +// Unix epoch (January 1, 1970) and at the specified latitude and longitude in +// degrees. +// +// We look for events from MR_WINDOW/2 hours in the past to MR_WINDOW/2 hours +// in the future. +MoonRise MoonRise_calculate(double latitude, double longitude, uint32_t t) { + MoonRise self = {}; + skyCoordinates moonPosition[3]; + double offsetDays; + + self.queryTime = t; + offsetDays = julianDate(t) - 2451545L; // Days since Jan 1, 2000, 1200UTC. + // Begin testing (MR_WINDOW / 2) hours before requested time. + // offsetDays -= (double)MR_WINDOW / (2 * 24) ; + + // Calculate coordinates at start, middle, and end of search period. + for (int i = 0; i < 3; i++) { + moonPosition[i] = moon(offsetDays + i * (double)MR_WINDOW / (2 * 24)); + } + + // If the RA wraps around during this period, unwrap it to keep the + // sequence smooth for interpolation. + if (moonPosition[1].RA <= moonPosition[0].RA) + moonPosition[1].RA += 2 * M_PI; + if (moonPosition[2].RA <= moonPosition[1].RA) + moonPosition[2].RA += 2 * M_PI; + + // Initialize interpolation array. + skyCoordinates mpWindow[3]; + mpWindow[0].RA = moonPosition[0].RA; + mpWindow[0].declination = moonPosition[0].declination; + mpWindow[0].distance = moonPosition[0].distance; + + for (int k = 0; k < MR_WINDOW; k++) { // Check each interval of search period + float ph = (float)(k + 1) / MR_WINDOW; + + mpWindow[2].RA = interpolate(moonPosition[0].RA, moonPosition[1].RA, + moonPosition[2].RA, ph); + mpWindow[2].declination = + interpolate(moonPosition[0].declination, moonPosition[1].declination, + moonPosition[2].declination, ph); + mpWindow[2].distance = moonPosition[2].distance; + + // Look for moonrise/set events during this interval. + { + double ha[3], VHz[3]; + double lSideTime; + + // Get (local_sidereal_time - MR_WINDOW / 2) hours in radians. + lSideTime = localSiderealTime(offsetDays, longitude) * 2 * M_PI / 360; + + // Calculate Hour Angle. + ha[0] = lSideTime - mpWindow[0].RA + k * K1; + ha[2] = lSideTime - mpWindow[2].RA + k * K1 + K1; + + // Hour Angle and declination at half hour. + ha[1] = (ha[2] + ha[0]) / 2; + mpWindow[1].declination = + (mpWindow[2].declination + mpWindow[0].declination) / 2; + + double s = sin(M_PI / 180 * latitude); + double c = cos(M_PI / 180 * latitude); + + // refraction + semidiameter at horizon + distance correction + double z = cos(M_PI / 180 * (90.567 - 41.685 / mpWindow[0].distance)); + + VHz[0] = s * sin(mpWindow[0].declination) + + c * cos(mpWindow[0].declination) * cos(ha[0]) - z; + VHz[2] = s * sin(mpWindow[2].declination) + + c * cos(mpWindow[2].declination) * cos(ha[2]) - z; + + if (signbit(VHz[0]) == signbit(VHz[2])) + goto noevent; // No event this hour. + + VHz[1] = s * sin(mpWindow[1].declination) + + c * cos(mpWindow[1].declination) * cos(ha[1]) - z; + + double a, b, d, e, time; + a = 2 * VHz[2] - 4 * VHz[1] + 2 * VHz[0]; + b = 4 * VHz[1] - 3 * VHz[0] - VHz[2]; + d = b * b - 4 * a * VHz[0]; + + if (d < 0) + goto noevent; // No event this hour. + + d = sqrt(d); + e = (-b + d) / (2 * a); + if ((e < 0) || (e > 1)) + e = (-b - d) / (2 * a); + time = k + e + 1 / 120; // Time since k=0 of event (in hours). + + // The time we started searching + the time from the start of the search + // to the event is the time of the event. + uint32_t eventTime; + eventTime = self.queryTime + (time) * 60 * 60; + + double hz, nz, dz, az; + hz = ha[0] + e * (ha[2] - ha[0]); // Azimuth of the moon at the event. + nz = -cos(mpWindow[1].declination) * sin(hz); + dz = c * sin(mpWindow[1].declination) - + s * cos(mpWindow[1].declination) * cos(hz); + az = atan2(nz, dz) / (M_PI / 180); + if (az < 0) + az += 360; + + // If there is no previously recorded event of this type, save this event. + // + // If this event is previous to queryTime, and is the nearest event to + // queryTime of events of its type previous to queryType, save this event, + // replacing the previously recorded event of its type. Events subsequent + // to queryTime are treated similarly, although since events are tested in + // chronological order no replacements will occur as successive events + // will be further from queryTime. + // + // If this event is subsequent to queryTime and there is an event of its + // type previous to queryTime, then there is an event of the other type + // between the two events of this event's type. If the event of the other + // type is previous to queryTime, then it is the nearest event to + // queryTime that is previous to queryTime. In this case save the current + // event, replacing the previously recorded event of its type. Otherwise + // discard the current event. + // + if ((VHz[0] < 0) && (VHz[2] > 0)) { + if (!self.hasRise || + ((self.riseTime < self.queryTime) == (eventTime < self.queryTime) && + (self.riseTime - self.queryTime) > (eventTime - self.queryTime)) || + ((self.riseTime < self.queryTime) != (eventTime < self.queryTime) && + (self.hasSet && (self.riseTime < self.queryTime) == + (self.setTime < self.queryTime)))) { + self.riseTime = eventTime; + self.riseAz = az; + self.hasRise = true; + } + } + if ((VHz[0] > 0) && (VHz[2] < 0)) { + if (!self.hasSet || + ((self.setTime < self.queryTime) == (eventTime < self.queryTime) && + (self.setTime - self.queryTime) > (eventTime - self.queryTime)) || + ((self.setTime < self.queryTime) != (eventTime < self.queryTime) && + (self.hasRise && (self.setTime < self.queryTime) == + (self.riseTime < self.queryTime)))) { + self.setTime = eventTime; + self.setAz = az; + self.hasSet = true; + } + } + + noevent: + // There are obscure cases in the polar regions that require extra logic. + if (!self.hasRise && !self.hasSet) + self.isVisible = !signbit(VHz[2]); + else if (self.hasRise && !self.hasSet) + self.isVisible = (self.queryTime > self.riseTime); + else if (!self.hasRise && self.hasSet) + self.isVisible = (self.queryTime < self.setTime); + else + self.isVisible = + ((self.riseTime < self.setTime && self.riseTime < self.queryTime && + self.setTime > self.queryTime) || + (self.riseTime > self.setTime && (self.riseTime < self.queryTime || + self.setTime > self.queryTime))); + } + + if (self.hasSet && self.hasRise) + break; + + mpWindow[0] = mpWindow[2]; // Advance to next interval. + } + + return self; +} + +// Moon position using fundamental arguments +// (Van Flandern & Pulkkinen, 1979) +skyCoordinates moon(double dayOffset) { + double l = 0.606434 + 0.03660110129 * dayOffset; + double m = 0.374897 + 0.03629164709 * dayOffset; + double f = 0.259091 + 0.03674819520 * dayOffset; + double d = 0.827362 + 0.03386319198 * dayOffset; + double n = 0.347343 - 0.00014709391 * dayOffset; + double g = 0.993126 + 0.00273777850 * dayOffset; + + l = 2 * M_PI * (l - floor(l)); + m = 2 * M_PI * (m - floor(m)); + f = 2 * M_PI * (f - floor(f)); + d = 2 * M_PI * (d - floor(d)); + n = 2 * M_PI * (n - floor(n)); + g = 2 * M_PI * (g - floor(g)); + + double v, u, w; + v = 0.39558 * sin(f + n) + + 0.08200 * sin(f) + + 0.03257 * sin(m - f - n) + + 0.01092 * sin(m + f + n) + + 0.00666 * sin(m - f) + - 0.00644 * sin(m + f - 2*d + n) + - 0.00331 * sin(f - 2*d + n) + - 0.00304 * sin(f - 2*d) + - 0.00240 * sin(m - f - 2*d - n) + + 0.00226 * sin(m + f) + - 0.00108 * sin(m + f - 2*d) + - 0.00079 * sin(f - n) + + 0.00078 * sin(f + 2*d + n); + + u = 1 + - 0.10828 * cos(m) + - 0.01880 * cos(m - 2*d) + - 0.01479 * cos(2*d) + + 0.00181 * cos(2*m - 2*d) + - 0.00147 * cos(2*m) + - 0.00105 * cos(2*d - g) + - 0.00075 * cos(m - 2*d + g); + + w = 0.10478 * sin(m) + - 0.04105 * sin(2*f + 2*n) + - 0.02130 * sin(m - 2*d) + - 0.01779 * sin(2*f + n) + + 0.01774 * sin(n) + + 0.00987 * sin(2*d) + - 0.00338 * sin(m - 2*f - 2*n) + - 0.00309 * sin(g) + - 0.00190 * sin(2*f) + - 0.00144 * sin(m + n) + - 0.00144 * sin(m - 2*f - n) + - 0.00113 * sin(m + 2*f + 2*n) + - 0.00094 * sin(m - 2*d + g) + - 0.00092 * sin(2*m - 2*d); + + double s; + skyCoordinates sc; + s = w / sqrt(u - v*v); + sc.RA = l + atan(s / sqrt(1 - s*s)); // Right ascension + + s = v / sqrt(u); + sc.declination = atan(s / sqrt(1 - s*s)); // Declination + sc.distance = 60.40974 * sqrt(u); // Distance + return(sc); +} + +// 3-point interpolation +double interpolate(double f0, double f1, double f2, double p) { + double a = f1 - f0; + double b = f2 - f1 - a; + return(f0 + p * (2*a + b * (2*p - 1))); +} + +// Determine Julian date from Unix time. +// Provides marginally accurate results with Arduino 4-byte double. +double julianDate(uint32_t t) { + return (t / 86400.0L + 2440587.5); +} + +// Local Sidereal Time +// Provides local sidereal time in degrees, requires longitude in degrees +// and time in fractional Julian days since Jan 1, 2000, 1200UTC (e.g. the +// Julian date - 2451545). +// cf. USNO Astronomical Almanac and +// https://astronomy.stackexchange.com/questions/24859/local-sidereal-time +double localSiderealTime(double offsetDays, double longitude) { + double lSideTime = (15.0L * (6.697374558L + 0.06570982441908L * offsetDays + + remainder(offsetDays, 1) * 24 + 12 + + 0.000026 * (offsetDays / 36525) * (offsetDays / 36525)) + + longitude) / 360; + lSideTime -= floor(lSideTime); + lSideTime *= 360; // Convert to degrees. + return(lSideTime); +} + diff --git a/movement/lib/moonrise/moonrise.h b/movement/lib/moonrise/moonrise.h new file mode 100644 index 00000000..70612cdc --- /dev/null +++ b/movement/lib/moonrise/moonrise.h @@ -0,0 +1,43 @@ +#ifndef MoonRise_h +#define MoonRise_h + +#include +#include + +// Size of event search window in hours. +// Events further away from the search time than MR_WINDOW/2 will not be +// found. At higher latitudes the moon rise/set intervals become larger, so if +// you want to find the nearest events this will need to increase. Larger +// windows will increase interpolation error. Useful values are probably from +// 12 - 48 but will depend upon your application. + +#define MR_WINDOW 48 // Even integer + +typedef struct { + double RA; // Right ascension + double declination; // Declination + double distance; // Distance +} skyCoordinates; + +typedef struct { + uint32_t queryTime; + uint32_t riseTime; + uint32_t setTime; + float riseAz; + float setAz; + bool hasRise; + bool hasSet; + bool isVisible; +} MoonRise; + +MoonRise MoonRise_calculate(double latitude, double longitude, uint32_t t); + +// private: +void testMoonRiseSet(MoonRise *self, int i, double offsetDays, double latitude, + double longitude, skyCoordinates *mp); +skyCoordinates moon(double dayOffset); +double interpolate(double f0, double f1, double f2, double p); +double julianDate(uint32_t t); +double localSiderealTime(double offsetDays, double longitude); + +#endif diff --git a/movement/make/Makefile b/movement/make/Makefile index 2116ebab..b8727356 100644 --- a/movement/make/Makefile +++ b/movement/make/Makefile @@ -25,6 +25,7 @@ INCLUDES += \ -I../lib/astrolib/ \ -I../lib/morsecalc/ \ -I../lib/smallchesslib/ \ + -I../lib/moonrise/ \ # If you add any other source files you wish to compile, add them after ../app.c # Note that you will need to add a backslash at the end of any line you wish to continue, i.e. @@ -40,6 +41,7 @@ SRCS += \ ../lib/TOTP/TOTP.c \ ../lib/base32/base32.c \ ../lib/sunriset/sunriset.c \ + ../lib/moonrise/moonrise.c \ ../lib/vsop87/vsop87a_milli.c \ ../lib/astrolib/astrolib.c \ ../lib/morsecalc/calc.c \ @@ -149,6 +151,7 @@ SRCS += \ ../watch_faces/sensor/accel_interrupt_count_face.c \ ../watch_faces/complication/metronome_face.c \ ../watch_faces/complication/smallchess_face.c \ + ../watch_faces/complication/moonrise_face.c \ # New watch faces go above this line. # Leave this line at the bottom of the file; it has all the targets for making your project. diff --git a/movement/movement_faces.h b/movement/movement_faces.h index 630f264a..572e91ee 100644 --- a/movement/movement_faces.h +++ b/movement/movement_faces.h @@ -123,6 +123,7 @@ #include "accel_interrupt_count_face.h" #include "metronome_face.h" #include "smallchess_face.h" +#include "moonrise_face.h" // New includes go above this line. #endif // MOVEMENT_FACES_H_ diff --git a/movement/watch_faces/complication/moonrise_face.c b/movement/watch_faces/complication/moonrise_face.c new file mode 100644 index 00000000..20c84bd5 --- /dev/null +++ b/movement/watch_faces/complication/moonrise_face.c @@ -0,0 +1,384 @@ +/* + * MIT License + * + * Copyright (c) 2025 hueso + * + * 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 +#include "moonrise_face.h" +#include "sunrise_sunset_face.h" +#include "watch.h" +#include "watch_utility.h" +#include "moonrise.h" + +#if __EMSCRIPTEN__ +#include +#endif + +static const uint8_t _location_count = sizeof(longLatPresets) / sizeof(long_lat_presets_t); + +static void _moonrise_face_update(movement_settings_t *settings, moonrise_state_t *state) { + char buf[11]; + 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_clear_colon(); + watch_display_string("Mz no Loc", 0); + return; + } + + watch_date_time date_time = watch_rtc_get_date_time(); // the current local date / time + watch_date_time scratch_time; // scratchpad, contains different values at different times + scratch_time.reg = date_time.reg; + + double lat = (double)movement_location.bit.latitude / 100.0; + double lon = (double)movement_location.bit.longitude / 100.0; + + uint32_t t = watch_utility_date_time_to_unix_time(date_time, movement_timezone_offsets[settings->bit.time_zone] * 60); + MoonRise mr = MoonRise_calculate(lat, lon, t); + + if(mr.isVisible) + watch_set_indicator(WATCH_INDICATOR_LAP); + else + watch_clear_indicator(WATCH_INDICATOR_LAP); + + if ( (state->rise_index == 0 && !mr.hasRise) || + (state->rise_index == 1 && !mr.hasSet) ) { + watch_clear_colon(); + watch_clear_indicator(WATCH_INDICATOR_PM); + watch_clear_indicator(WATCH_INDICATOR_24H); + snprintf(buf, sizeof(buf), "%s%2d none ", state->rise_index ? "M_" : "M~", scratch_time.unit.day); + watch_display_string(buf, 0); + return; + } + watch_set_colon(); + if (settings->bit.clock_mode_24h && !settings->bit.clock_24h_leading_zero) + watch_set_indicator(WATCH_INDICATOR_24H); + + if(state->rise_index == 0) + scratch_time = watch_utility_date_time_from_unix_time(mr.riseTime, movement_timezone_offsets[settings->bit.time_zone] * 60); + else + scratch_time = watch_utility_date_time_from_unix_time(mr.setTime, movement_timezone_offsets[settings->bit.time_zone] * 60); + + state->rise_set_expires.reg = scratch_time.reg; + + bool set_leading_zero = false; + if (!settings->bit.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); + else if (settings->bit.clock_24h_leading_zero && scratch_time.unit.hour < 10) { + set_leading_zero = true; + } + snprintf(buf, sizeof(buf), "%s%2d%2d%02d%2s", state->rise_index ? "M_" : "M~", scratch_time.unit.day, scratch_time.unit.hour, scratch_time.unit.minute,longLatPresets[state->longLatToUse].name); + watch_display_string(buf, 0); + + if (set_leading_zero) + watch_display_string("0", 4); + return; + +} + +static int16_t _moonrise_face_latlon_from_struct(moonrise_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 moonrise_lat_lon_settings_t _moonrise_face_struct_from_latlon(int16_t val) { + moonrise_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 _moonrise_face_update_location_register(moonrise_state_t *state) { + if (state->location_changed) { + movement_location_t movement_location; + int16_t lat = _moonrise_face_latlon_from_struct(state->working_latitude); + int16_t lon = _moonrise_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 _moonrise_face_update_settings_display(movement_event_t event, moonrise_state_t *state) { + char buf[12]; + + switch (state->page) { + case 0: + return; + case 1: + snprintf(buf, sizeof(buf), "LA %c %04d", state->working_latitude.sign ? '-' : '+', abs(_moonrise_face_latlon_from_struct(state->working_latitude))); + break; + case 2: + snprintf(buf, sizeof(buf), "LO %c%05d", state->working_longitude.sign ? '-' : '+', abs(_moonrise_face_latlon_from_struct(state->working_longitude))); + break; + } + if (event.subsecond % 2) { + buf[state->active_digit + 4] = ' '; + } + watch_display_string(buf, 0); +} + +static void _moonrise_face_advance_digit(moonrise_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(_moonrise_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(_moonrise_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(_moonrise_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(_moonrise_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(_moonrise_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(_moonrise_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(_moonrise_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(_moonrise_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(_moonrise_face_latlon_from_struct(state->working_longitude)) > 18000) state->working_longitude.hundredths = 0; + break; + } + break; + } +} + +void moonrise_face_setup(movement_settings_t *settings, uint8_t watch_face_index, void ** context_ptr) { + (void) settings; + (void) watch_face_index; + if (*context_ptr == NULL) { + *context_ptr = malloc(sizeof(moonrise_state_t)); + memset(*context_ptr, 0, sizeof(moonrise_state_t)); + } +} + +void moonrise_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 + + moonrise_state_t *state = (moonrise_state_t *)context; + movement_location_t movement_location = (movement_location_t) watch_get_backup_data(1); + state->working_latitude = _moonrise_face_struct_from_latlon(movement_location.bit.latitude); + state->working_longitude = _moonrise_face_struct_from_latlon(movement_location.bit.longitude); +} + +bool moonrise_face_loop(movement_event_t event, movement_settings_t *settings, void *context) { + moonrise_state_t *state = (moonrise_state_t *)context; + + switch (event.event_type) { + case EVENT_ACTIVATE: + _moonrise_face_update(settings, 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_tick_animation_is_running()) watch_start_tick_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; + _moonrise_face_update(settings, state); + } + } else { + _moonrise_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; + _moonrise_face_update_location_register(state); + } + _moonrise_face_update_settings_display(event, context); + } else if (_location_count <= 1) { + movement_illuminate_led(); + } + if (state->page == 0) { + movement_request_tick_frequency(1); + _moonrise_face_update(settings, 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; + _moonrise_face_update(settings, state); + } + break; + case EVENT_ALARM_BUTTON_UP: + if (state->page) { + _moonrise_face_advance_digit(state); + _moonrise_face_update_settings_display(event, context); + } else { + state->rise_index = (state->rise_index + 1) % 2; + _moonrise_face_update(settings, state); + } + break; + case EVENT_ALARM_LONG_PRESS: + if (state->page == 0) { + if (state->longLatToUse != 0) { + state->longLatToUse = 0; + _moonrise_face_update(settings, state); + break; + } + state->page++; + state->active_digit = 0; + watch_clear_display(); + movement_request_tick_frequency(4); + _moonrise_face_update_settings_display(event, context); + } + else { + state->active_digit = 0; + state->page = 0; + _moonrise_face_update_location_register(state); + _moonrise_face_update(settings, 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); + _moonrise_face_update(settings, state); + } + break; + default: + return movement_default_loop_handler(event, settings); + } + + return true; +} + +void moonrise_face_resign(movement_settings_t *settings, void *context) { + (void) settings; + moonrise_state_t *state = (moonrise_state_t *)context; + state->page = 0; + state->active_digit = 0; + state->rise_index = 0; + _moonrise_face_update_location_register(state); +} diff --git a/movement/watch_faces/complication/moonrise_face.h b/movement/watch_faces/complication/moonrise_face.h new file mode 100644 index 00000000..6fe09f3d --- /dev/null +++ b/movement/watch_faces/complication/moonrise_face.h @@ -0,0 +1,90 @@ +/* + * 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 MOONRISE_FACE_H_ +#define MOONRISE_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) +} moonrise_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; + moonrise_lat_lon_settings_t working_latitude; + moonrise_lat_lon_settings_t working_longitude; + uint8_t longLatToUse; +} moonrise_state_t; + +void moonrise_face_setup(movement_settings_t *settings, uint8_t watch_face_index, void ** context_ptr); +void moonrise_face_activate(movement_settings_t *settings, void *context); +bool moonrise_face_loop(movement_event_t event, movement_settings_t *settings, void *context); +void moonrise_face_resign(movement_settings_t *settings, void *context); + +#define moonrise_face ((const watch_face_t){ \ + moonrise_face_setup, \ + moonrise_face_activate, \ + moonrise_face_loop, \ + moonrise_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 // MOONRISE_FACE_H_