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