From 29acd183eac621d795d0bac9b6992f1f8bb9af54 Mon Sep 17 00:00:00 2001 From: Peter Lewis Date: Sun, 5 Jul 2026 12:07:00 +0100 Subject: [PATCH] Add sidereal (LST) and apparent-solar time-row modes MODE_LST and MODE_SUNDIAL turn the big HH:MM:SS digits into a live ticking Local Sidereal Time or apparent-solar ('sundial') clock. Both reuse the COUNT_COUNTDOWN takeover pattern: the heavy GMST/EoT double maths runs once per second in the main loop, staging pre-rendered digits that the SysTick handlers latch at the true GPS PPS boundary -- so accuracy and the P3..P0 sub-second digit-hiding are inherited from the disciplined civil second, and civil timekeeping (currentTime, PPS, the date row) is untouched. The display is quantised to civil second boundaries and reseeded every second, so sidereal's 1.00273791x rate makes the seconds double-step once every ~6 min -- the honest signature of a GPS-disciplined sidereal clock rather than a rate-warped one. A dedicated colon animation (alt_colon_mode, default alt_sawtooth, kept distinct from the civil colon) marks the mode so it can never be mistaken for civil time; sundial especially can sit within minutes of it. With no position the row shows dashes -- never GMST-as-LST. astro.c regains local_sidereal_time()/local_solar_time() with the GMST series factored into one helper (gmst_hours, +T^2 term); native suite 53/53 incl. the IAU J2000 anchor and Meeus example 12.b. All default-off: stock behaviour is unchanged. Co-Authored-By: Claude Opus 4.8 --- mk4-time/Core/Inc/astro.h | 3 + mk4-time/Core/Inc/main.h | 9 +- mk4-time/Core/Src/astro.c | 36 ++++- mk4-time/Core/Src/main.c | 302 +++++++++++++++++++++++++++++++++++-- mk4-time/test/test_astro.c | 19 +++ qspi/config.txt | 18 +++ 6 files changed, 370 insertions(+), 17 deletions(-) diff --git a/mk4-time/Core/Inc/astro.h b/mk4-time/Core/Inc/astro.h index c296f6c..26c5698 100644 --- a/mk4-time/Core/Inc/astro.h +++ b/mk4-time/Core/Inc/astro.h @@ -38,6 +38,9 @@ int moon_phase_index(double phase); /* 0..7, see ASTRO_MOON_NAMES /* (d) Equation of time in minutes (+ = apparent sun ahead of mean/clock sun). */ double equation_of_time(double unix_s); +double local_sidereal_time(double unix_s, double lon); /* LMST, hours [0,24), lon E+ */ +double local_solar_time(double unix_s, double lon); /* apparent solar, hours [0,24) */ + /* (e) 6-character Maidenhead locator for (lat, lon). out must hold >= 7 bytes. * Writes "----\0" if either coordinate is non-finite. */ void maidenhead(double lat, double lon, char out[7]); diff --git a/mk4-time/Core/Inc/main.h b/mk4-time/Core/Inc/main.h index 5cfa8ec..067f54a 100644 --- a/mk4-time/Core/Inc/main.h +++ b/mk4-time/Core/Inc/main.h @@ -171,13 +171,20 @@ enum { MODE_GRID, // Maidenhead grid locator MODE_LATLON, // latitude / longitude, auto-paged + // Alternate-timebase TIME-ROW modes: the big digits tick Local Sidereal Time or + // apparent solar ("sundial") time, reseeded from the GPS-disciplined second; the + // date row keeps the civil date and a dedicated colon animation marks the mode. + MODE_LST, + MODE_SUNDIAL, + NUM_DISPLAY_MODES }; enum { COUNT_NORMAL =0, COUNT_HIDDEN, - COUNT_DOWN + COUNT_DOWN, + COUNT_ALT // time row driven by the alternate timebase (MODE_LST / MODE_SUNDIAL) }; enum { diff --git a/mk4-time/Core/Src/astro.c b/mk4-time/Core/Src/astro.c index e34f680..3a64a83 100644 --- a/mk4-time/Core/Src/astro.c +++ b/mk4-time/Core/Src/astro.c @@ -22,6 +22,19 @@ const char *const ASTRO_MOON_NAMES[8] = { static double days_since_j2000(double unix_s) { return (unix_s - J2000_UNIX) / 86400.0; } +/* Greenwich Mean Sidereal Time in hours [0,24). Factored out so sun_az_el and + * local_sidereal_time share one series and can never drift apart. The quadratic + * term keeps truncation below ~1 ms for decades (the linear series alone drifts to + * ~10 ms by 2033). Note the input is GPS-derived UTC, not UT1: true-sky sidereal + * accuracy is floored by DUT1 (up to +/-0.9 s) by design. */ +static double gmst_hours(double n) { + double T = n / 36525.0; + double g = fmod(18.697374558 + 24.06570982441908 * n + 0.000026 * T * T, 24.0); + if (g < 0.0) g += 24.0; + if (g >= 24.0) g -= 24.0; + return g; +} + /* The shared low-precision solar block: from days-since-J2000 produce mean * longitude L, anomaly g, ecliptic longitude lambda, obliquity eps, and the * apparent right ascension alpha (deg) and declination delta (rad). */ @@ -46,7 +59,7 @@ void sun_az_el(double lat, double lon, double unix_s, double *az, double *el) { double alpha, delta; sun_ecliptic(n, NULL, NULL, NULL, NULL, &alpha, &delta); - double gmst = fmod(18.697374558 + 24.06570982441908 * n, 24.0); /* GMST in hours, used as-is */ + double gmst = gmst_hours(n); /* hours */ double lst = gmst * 15.0 + lon; /* degrees */ double ha = (lst - alpha) * DEG; /* radians */ @@ -67,6 +80,27 @@ double equation_of_time(double unix_s) { return 4.0 * diff; /* minutes */ } +/* Local Mean Sidereal Time, decimal hours [0,24). LMST = GMST + longitude/15 + * (east-positive). Anchors: GMST(J2000.0) = 18.697374558 h (IAU); Meeus ex. 12.b. */ +double local_sidereal_time(double unix_s, double lon) { + double lst = fmod(gmst_hours(days_since_j2000(unix_s)) + lon / 15.0, 24.0); + if (lst < 0.0) lst += 24.0; + if (lst >= 24.0) lst -= 24.0; /* fmod residue can round the wrap to exactly 24.0 */ + return lst; /* hours [0,24) */ +} + +/* Local apparent solar ("sundial") time, decimal hours [0,24). + * Apparent solar = mean solar (UTC shifted by longitude) + equation of time. + * At the sun's meridian transit this equals 12.0 exactly, consistent with + * sun_times()'s solar_noon = 12 - eot/60 - lon/15. */ +double local_solar_time(double unix_s, double lon) { + double utc_h = fmod(unix_s / 3600.0, 24.0); + double t = fmod(utc_h + lon / 15.0 + equation_of_time(unix_s) / 60.0, 24.0); + if (t < 0.0) t += 24.0; + if (t >= 24.0) t -= 24.0; + return t; /* hours [0,24) */ +} + int sun_times(double lat, double lon, double unix_s, double *sunrise, double *sunset, double *solar_noon, double *civil_dusk, double *nautical_dusk, double *golden_dusk) { diff --git a/mk4-time/Core/Src/main.c b/mk4-time/Core/Src/main.c index 7734603..42d3c38 100644 --- a/mk4-time/Core/Src/main.c +++ b/mk4-time/Core/Src/main.c @@ -188,6 +188,13 @@ _Bool resendDate = 0; uint32_t LPTIM1_high; uint8_t displayMode = 0, countMode = 0, colonMode = 0; +// Civil vs alternate-timebase colon animation: colonMode is the ACTIVE selection that +// loadColonAnimation() renders; the per-context choices live here and applyColonForMode() +// swaps between them. The sidereal default must stay visually distinct from civil so +// MODE_LST/MODE_SUNDIAL can never masquerade as civil time. +uint8_t colonModeCivil = 0; +uint8_t colonModeAlt = COLON_MODE_ALT_SAWTOOTH; +_Bool colonAltExplicit = 0; // user explicitly set alt_colon_mode uint8_t requestMode = 255; uint8_t nmea_cdc_level=0; int debug_rtc_val = 0; @@ -300,6 +307,8 @@ void sendDate( _Bool now ){ switch (displayMode) { default: + case MODE_LST: // alt-timebase modes keep the civil date on the date row — + case MODE_SUNDIAL: // the bottom row stays an unambiguous civil anchor case MODE_ISO8601_STD: uart2_tx_buffer[1] ='2'; uart2_tx_buffer[2] ='0'; @@ -655,6 +664,119 @@ void setNextCountdown(time_t nextTime){ // Store UTC on RTC // need to also write zone into backup registers // Only called at the start of a second, don't attempt to write subseconds. +// --- Alternate timebase (MODE_LST / MODE_SUNDIAL) ------------------------------------------ +// The TIME ROW ticks Local Sidereal Time or apparent solar ("sundial") time. Heavy double +// math runs in THREAD context once per second (alt_update), staging the reading for the +// coming civil boundary; the SysTick_Alt_* handlers latch it at the .900 prep mark. The +// display is quantized to civil second boundaries — value = floor(alt time at the boundary), +// reseeded every second — so GPS discipline and holdover honesty are inherited from +// currentTime for free. Sidereal runs 1.00273791x civil: the seconds display double-steps +// once every ~6 min 5 s. That skip is the authentic signature of a true sidereal clock. +static volatile struct { + uint8_t hh, mm, ss; + uint32_t for_time; // civil epoch this reading is the floor of; 0 = invalid +} alt_stage; +static uint8_t alt_hh, alt_mm, alt_ss; // ISR-owned: what the row currently shows +static volatile _Bool alt_have_pos = 0; +static volatile _Bool alt_seed_pending = 0; // mode entered: thread must seed the row +static volatile uint8_t alt_gen = 0; // bumped on mode entry; cancels in-flight staging + +// Overlay an alternate HH:MM:SS onto the next7seg staging buffer. The stock +// setNextTimestamp() has just run (keeping nextBcd / DST / date-row bookkeeping fresh); +// only the six time-row digit patterns are replaced. +#define alt_render_next7seg(hh, mm, ss) do { \ + next7seg.c = cLut[(ss) % 10]; \ + next7seg.b[0] = bCat0 | cLut[(hh) / 10] << 2; \ + next7seg.b[1] = bCat1 | cLut[(hh) % 10] << 2; \ + next7seg.b[2] = bCat2 | cLut[(mm) / 10] << 2; \ + next7seg.b[3] = bCat3 | cLut[(mm) % 10] << 2; \ + next7seg.b[4] = bCat4 | cLut[(ss) / 10] << 2; \ + } while (0) + +// The .900 prep for the alternate modes: stock next-second bookkeeping first, then latch +// the staged reading — or, if the main loop was starved past the boundary, advance the last +// shown reading by one second. LST's fallback runs SLOW (2.74 ms/s; the reseed snap is +// always forward), SUNDIAL's runs fast by at most ~0.35 ms/s at the EoT extremes — a +// visible backwards reseed would need ~48+ minutes of continuous main-loop starvation. +#define alt_prep_next() do { \ + currentTime++; \ + setNextTimestamp( currentTime ); \ + if (alt_stage.for_time == (uint32_t)currentTime) { \ + alt_hh = alt_stage.hh; alt_mm = alt_stage.mm; alt_ss = alt_stage.ss; \ + } else if (++alt_ss >= 60) { \ + alt_ss = 0; \ + if (++alt_mm >= 60) { alt_mm = 0; if (++alt_hh >= 24) alt_hh = 0; } \ + } \ + alt_render_next7seg(alt_hh, alt_mm, alt_ss); \ + sendDate(0); \ + } while (0) + +// Compute floor-HH:MM:SS of the alternate time at `when` (thread context only: doubles). +static _Bool alt_compute(uint32_t when, uint8_t *hh, uint8_t *mm, uint8_t *ss){ + float lat = latitude, lon = longitude; // one consistent snapshot (astro_update pattern) + if (!astro_pos_ok(lat, lon)) return 0; + double hours = (displayMode == MODE_LST) + ? local_sidereal_time((double)when, (double)lon) + : local_solar_time((double)when, (double)lon); + if (!(hours >= 0.0) || hours >= 24.0) hours = 0.0; // NaN / float-residue guard + int h2 = (int)hours; + double fm = (hours - h2) * 60.0; + int m2 = (int)fm; + int s2 = (int)((fm - m2) * 60.0); + if (h2 > 23) h2 = 23; + if (m2 > 59) m2 = 59; + if (s2 > 59) s2 = 59; + *hh = (uint8_t)h2; *mm = (uint8_t)m2; *ss = (uint8_t)s2; + return 1; +} + +// Main-loop staging (thread context — ALL the double math for these modes lives here). +// Two jobs: (a) SEED after mode entry or position go-live — render + latch the current +// reading immediately and install the live handlers, so a PPS latch can never show civil +// digits under the alternate colon; (b) STAGE the reading for the coming civil boundary. +// A generation counter cancels any in-flight computation when the mode flips mid-pass, so +// a stale timebase can never be stamped as valid. +void alt_update(void){ + if (displayMode != MODE_LST && displayMode != MODE_SUNDIAL) return; + + uint8_t gen = alt_gen; // snapshot: mode flips abort the publish below + + if (alt_seed_pending || !alt_have_pos) { + uint8_t hh, mm, ss; + if (!alt_compute((uint32_t)currentTime, &hh, &mm, &ss)) { + alt_have_pos = 0; // stay dashed; retried every pass + return; + } + __disable_irq(); + if (gen == alt_gen) { + alt_hh = hh; alt_mm = mm; alt_ss = ss; + alt_render_next7seg(alt_hh, alt_mm, alt_ss); // alt digits now staged: any latch is honest + latchSegments() // and shown immediately (countdown precedent) + alt_have_pos = 1; + alt_seed_pending = 0; + } + __enable_irq(); + if (gen == alt_gen) setPrecision(); // install Alt_Px/PPS now — don't wait for PendSV, + // or the NoUpdate .900 prep could stage civil digits + return; // stage the coming boundary on the next pass + } + + uint32_t target = (uint32_t)currentTime + 1; + if (alt_stage.for_time == target) return; + uint8_t hh, mm, ss; + if (!alt_compute(target, &hh, &mm, &ss)) { + alt_have_pos = 0; // position lost: setPrecision dashes it this second + alt_stage.for_time = 0; + return; + } + __disable_irq(); + if (gen == alt_gen) { // publish only if no mode flip happened mid-compute + alt_stage.hh = hh; alt_stage.mm = mm; alt_stage.ss = ss; + alt_stage.for_time = target; // IRQs masked: fields and stamp are one atomic unit + } + __enable_irq(); +} + void write_rtc(void){ RTC_DateTypeDef sdatestructure; @@ -821,6 +943,9 @@ void decodeRMC(void){ // Under normal conditions, we should only be parsing nmea at around .300 to .400 // USART1 preemption priority is currently 1, so we could be interrupted by systick here setNextTimestamp( currentTime ); + // In the alternate time-row modes the civil digits just staged must not reach the + // display: restore the alt overlay so the boundary latch stays honest. + if (countMode == COUNT_ALT) alt_render_next7seg(alt_hh, alt_mm, alt_ss); sendDate(0); } } @@ -1027,6 +1152,26 @@ float parseBrightness(char *v, _Bool invert){ #define set_mode_enabled(mode, value) \ if ((config.modes_enabled[mode] = truthy(value))) requestMode=mode; +static uint8_t parseColonName(const char *value){ + if (strcasecmp(value, "solid") == 0) return COLON_MODE_SOLID; + if (strcasecmp(value, "heartbeat") == 0) return COLON_MODE_HEARTBEAT; + if (strcasecmp(value, "sawtooth") == 0) return COLON_MODE_1PPS_SAWTOOTH; + if (strcasecmp(value, "alt_sawtooth") == 0) return COLON_MODE_ALT_SAWTOOTH; + if (strcasecmp(value, "toggle") == 0) return COLON_MODE_TOGGLE; + return COLON_MODE_SLOWFADE; +} + +// Select the colon animation for the current display mode (idempotent, thread context). +// Alternate-timebase modes get their own animation so they read as "not civil" at a glance. +void applyColonForMode(void){ + uint8_t want = (displayMode == MODE_LST || displayMode == MODE_SUNDIAL) + ? colonModeAlt : colonModeCivil; + if (want != colonMode) { + colonMode = want; + loadColonAnimation(); + } +} + void parseConfigString(char *key, char *value) { if (strcasecmp(key, "text") == 0) { @@ -1119,6 +1264,10 @@ void parseConfigString(char *key, char *value) { set_mode_enabled(MODE_GRID, value); } else if (strcasecmp(key, "MODE_LATLON") == 0) { set_mode_enabled(MODE_LATLON, value); + } else if (strcasecmp(key, "MODE_LST") == 0) { + set_mode_enabled(MODE_LST, value); + } else if (strcasecmp(key, "MODE_SUNDIAL") == 0) { + set_mode_enabled(MODE_SUNDIAL, value); } else if (strcasecmp(key, "Tolerance_time_1ms") == 0) { config.tolerance_1ms = atoi(value); } else if (strcasecmp(key, "Tolerance_time_10ms") == 0) { @@ -1131,17 +1280,12 @@ void parseConfigString(char *key, char *value) { config.fake_lat = atof(value); } else if (strcasecmp(key, "colon_mode") == 0) { - if (strcasecmp(value, "solid") == 0) { - colonMode = COLON_MODE_SOLID; - } else if (strcasecmp(value, "heartbeat") == 0) { - colonMode = COLON_MODE_HEARTBEAT; - } else if (strcasecmp(value, "sawtooth") == 0) { - colonMode = COLON_MODE_1PPS_SAWTOOTH; - } else if (strcasecmp(value, "alt_sawtooth") == 0) { - colonMode = COLON_MODE_ALT_SAWTOOTH; - } else if (strcasecmp(value, "toggle") == 0) { - colonMode = COLON_MODE_TOGGLE; - } else colonMode = COLON_MODE_SLOWFADE; + colonModeCivil = parseColonName(value); + + } else if (strcasecmp(key, "alt_colon_mode") == 0) { + + colonModeAlt = parseColonName(value); // shared by MODE_LST and MODE_SUNDIAL + colonAltExplicit = 1; } else if (strcasecmp(key, "nmea") == 0) { @@ -1171,7 +1315,13 @@ void parseConfigString(char *key, char *value) { } void postConfigCleanup(void){ - loadColonAnimation(); + // Keep the alternate-timebase colon distinct unless the user EXPLICITLY matched them. + if (!colonAltExplicit && colonModeAlt == colonModeCivil) { + colonModeAlt = (colonModeCivil != COLON_MODE_ALT_SAWTOOTH) ? COLON_MODE_ALT_SAWTOOTH + : COLON_MODE_TOGGLE; + } + colonMode = 0xFF; // force applyColonForMode to reload exactly once + applyColonForMode(); // check at least one mode is enabled uint8_t j = 0; @@ -1267,7 +1417,9 @@ void readConfigFile(void){ config.tolerance_100ms = 100000; config.zone_override = 0; config.brightness_override = -1.0; - colonMode = 0; + colonModeCivil = 0; + colonModeAlt = COLON_MODE_ALT_SAWTOOTH; + colonAltExplicit = 0; FIL file; @@ -1632,6 +1784,59 @@ void SysTick_CountDown_P0(void) } } +// Alternate-timebase handlers (MODE_LST / MODE_SUNDIAL): identical to the CountUp family — +// same cascade, same sub-second painting, same precision ladder — except the .900 prep +// overlays the staged alternate HH:MM:SS onto next7seg (see alt_prep_next). +void SysTick_Alt_P3(void) +{ + timetick() + + buffer_c[3].low=cLut[millisec]; + buffer_c[2].low=cLut[centisec]; + buffer_c[1].low=cLut[decisec]; + + HAL_IncTick(); + + if (decisec==9 && centisec==0 && millisec==0){ + alt_prep_next(); + } +} + +void SysTick_Alt_P2(void) { + timetick() + + buffer_c[2].low=cLut[centisec]; + buffer_c[1].low=cLut[decisec]; + + HAL_IncTick(); + + if (decisec==9 && centisec==0 && millisec==0){ + alt_prep_next(); + } +} + +void SysTick_Alt_P1(void) { + timetick() + + buffer_c[1].low=cLut[decisec]; + + HAL_IncTick(); + + if (decisec==9 && centisec==0 && millisec==0){ + alt_prep_next(); + } +} + +void SysTick_Alt_P0(void) { + timetick() + + HAL_IncTick(); + + if (decisec==9 && centisec==0 && millisec==0){ + alt_prep_next(); + } +} + void SysTick_Dummy(void){ HAL_IncTick(); } @@ -1798,6 +2003,51 @@ void setPrecision(void){ SetSysTick( &SysTick_CountUp_P0 ); } + } else if (countMode == COUNT_ALT) { + + if (!alt_have_pos) { + // No usable position (no fix, no fake_longitude): dashes, digits not ticking — + // never GMST-as-LST, never a guessed longitude. Re-evaluated every second; the + // row goes live the moment a position appears. resendDate keeps the civil date + // row refreshing (PendSV's resend check runs right after this) — without it the + // date would freeze across midnight while dashed. + resendDate = 1; + SetPPS( &PPS_NoUpdate ); + SetSysTick( &SysTick_CountUp_NoUpdate ); + buffer_b[0] = bCat0 | 0b01000000 << 2; + buffer_b[1] = bCat1 | 0b01000000 << 2; + buffer_b[2] = bCat2 | 0b01000000 << 2; + buffer_b[3] = bCat3 | 0b01000000 << 2; + buffer_b[4] = bCat4 | 0b01000000 << 2; + buffer_c[0].low = 0b01000000; + buffer_c[3].low = 0b01000000; + buffer_c[2].low = 0b01000000; + buffer_c[1].low = 0b01000000; + buffer_c[0].high= 0b11001110; + } else if (currentTime - last_pps_time < config.tolerance_1ms){ + SetPPS( &PPS ); + buffer_c[0].high= 0b11001110 | cSegDP; + SetSysTick( &SysTick_Alt_P3 ); + } else if (currentTime - last_pps_time < config.tolerance_10ms){ + SetPPS( &PPS ); + buffer_c[3].low = 0b01000000; + buffer_c[0].high= 0b11001110 | cSegDP; + SetSysTick( &SysTick_Alt_P2 ); + } else if (currentTime - rtc_last_calibration < config.tolerance_100ms){ + SetPPS( &PPS ); + buffer_c[3].low = 0b01000000; + buffer_c[2].low = 0b01000000; + buffer_c[0].high= 0b11001110 | cSegDP; + SetSysTick( &SysTick_Alt_P1 ); + } else { + SetPPS( &PPS ); + buffer_c[3].low = 0b01000000; + buffer_c[2].low = 0b01000000; + buffer_c[1].low = 0b01000000; + buffer_c[0].high= 0b11001110; + SetSysTick( &SysTick_Alt_P0 ); + } + } else if (displayMode == MODE_COUNTDOWN) { if (config.countdown_to >= currentTime) { @@ -1873,8 +2123,9 @@ void nextMode(_Bool reverse){ buffer_c[2].high &= ~cSegDP; buffer_c[3].high &= ~cSegDP; } - if ( displayMode == MODE_ISO_WEEK || justExited(MODE_COUNTDOWN)) { - // If we exit countdown mode at .9 seconds + if ( displayMode == MODE_ISO_WEEK || justExited(MODE_COUNTDOWN) + || justExited(MODE_LST) || justExited(MODE_SUNDIAL)) { + // If we exit countdown/alt mode at .9 seconds // it will show the wrong time for .1 seconds setNextTimestamp(currentTime); } @@ -1900,6 +2151,22 @@ void nextMode(_Bool reverse){ TIM2->CCR2 = 0; latchSegments(); + } else if (displayMode == MODE_LST || displayMode == MODE_SUNDIAL) { + + countMode = COUNT_ALT; + setNextTimestamp(currentTime); // stock civil bookkeeping (integer path; countdown-arm cost) + // NO double math here: nextMode can run in the USART2 button ISR (priority 0, which + // blocks SysTick and the PPS EXTI), and the LST/solar computation is ~100 µs of + // soft-double. Invalidate and let the main-loop alt_update() seed within one pass + // (<100 ms); until then setPrecision shows the dashed state. + alt_gen++; // cancels any in-flight staging for the previous timebase + alt_stage.for_time = 0; + alt_have_pos = 0; + alt_seed_pending = 1; + setPrecision(); + TIM2->CCR1 = 0; + TIM2->CCR2 = 0; + } else { if (countMode != COUNT_NORMAL) { @@ -1911,6 +2178,7 @@ void nextMode(_Bool reverse){ latchSegments(); } } + applyColonForMode(); // idempotent: alt colon on entry, civil colon on exit sendDate(1); } void button1pressed(void){ @@ -2251,6 +2519,10 @@ int main(void) || displayMode == MODE_GRID || displayMode == MODE_LATLON) astro_update(); + // MODE_LST / MODE_SUNDIAL: stage the next civil boundary's alternate reading + // (thread-context doubles; no-op in every other mode) + alt_update(); + /* USER CODE END WHILE */ diff --git a/mk4-time/test/test_astro.c b/mk4-time/test/test_astro.c index 4df94fc..13a1f41 100644 --- a/mk4-time/test/test_astro.c +++ b/mk4-time/test/test_astro.c @@ -99,6 +99,25 @@ int main(void) { maidenhead(-41.283, 174.745, g); chks("Wellington", g, "RE78ir"); maidenhead(1.0 / 0.0, 0.0, g); chks("non-finite", g, "----"); + printf("local_sidereal_time (h):\n"); + /* GMST at J2000.0 = 18.697374558 h (IAU); Meeus ex. 12.b, 1987-04-10 19:21:00 UT + * -> mean GMST 8h34m57.1s = 8.582525 h. LST = GMST + lon/15 checks shift + wrap. */ + chk("LST J2000 lon0", local_sidereal_time(946728000.0, 0.0), 18.697375, 0.0001); + chk("LST Meeus lon0", local_sidereal_time(545080860.0, 0.0), 8.582525, 0.0001); + chk("LST J2000 lon -75", local_sidereal_time(946728000.0, -75.0), 13.697375, 0.0001); + chk("LST J2000 lon +90wrap", local_sidereal_time(946728000.0, 90.0), 0.697375, 0.0001); + + printf("local_solar_time (h):\n"); + /* At meridian transit apparent solar time is 12:00 exactly. Build that instant from + * sun_times()'s solar_noon and confirm -- catches any longitude/EoT sign or wrap error. */ + for (int i = 0; i < 4; i++) { + double nn; + sun_times(V[i]->lat, V[i]->lon, V[i]->t, NULL, NULL, &nn, NULL, NULL, NULL); + double noon_unix = trunc(V[i]->t / 86400.0) * 86400.0 + nn * 3600.0; + char nm[40]; sprintf(nm, "solar@noon %s", V[i]->tag); + chk(nm, local_solar_time(noon_unix, V[i]->lon), 12.0, 0.02); + } + printf("\n%d/%d passed, %d failed\n", total - fails, total, fails); return fails ? 1 : 0; } diff --git a/qspi/config.txt b/qspi/config.txt index f68d10f..9bdc2db 100644 --- a/qspi/config.txt +++ b/qspi/config.txt @@ -124,6 +124,24 @@ MODE_GRID = disabled # Latitude / longitude in decimal degrees. Pages every 2 s: LAT -> LON. MODE_LATLON = disabled +# Local Sidereal Time as a LIVE TICKING CLOCK on the time row (big digits). The date row +# keeps the civil date, and the colons animate differently (alt_colon_mode below) so it +# can never be mistaken for civil time. Sidereal runs 1.00273791x faster than civil, so the +# seconds display double-steps about once every 6 minutes -- real sidereal behaviour, not a +# glitch. Needs a position (GPS fix or fake_longitude); shows dashes without one. +MODE_LST = disabled + +# Apparent solar ("sundial") time on the time row: what a sundial reads -- UTC shifted by +# your longitude plus the equation of time. Reads exactly 12:00:00 at local solar noon. +MODE_SUNDIAL = disabled + +# Colon animation while MODE_LST / MODE_SUNDIAL is shown, so the alternate timebase is +# unmistakable at a glance -- sundial especially, which can sit within minutes of civil. +# Same names as colon_mode; automatically kept different from your civil colon unless you +# explicitly set them equal here. One of: slowfade, heartbeat, sawtooth, alt_sawtooth, +# toggle, solid +#alt_colon_mode = alt_sawtooth + # Fixed position for the astro modes when there is no GPS fix (decimal degrees, N+/E+). # Note: setting these also pins the clock's position (GPS position updates are ignored). #fake_latitude = 51.48