Bright-star meridian-transit predictor (MODE_STAR + $PMSTAR + STARS.BIN)#12
Draft
peterlewis wants to merge 41 commits into
Draft
Bright-star meridian-transit predictor (MODE_STAR + $PMSTAR + STARS.BIN)#12peterlewis wants to merge 41 commits into
peterlewis wants to merge 41 commits into
Conversation
With `pps = on` in config.txt, emit one proprietary NMEA sentence per PPS edge over the existing CDC stream, carrying the sub-second phase captured at the edge plus calibration / holdover / die-temperature telemetry. Time-critical fields are snapshotted in the PPS ISR; formatting and CDC submission happen in the main loop, serialised against the NMEA passthrough. With no enumerated host the record is dropped before any formatting. mk4-time/Core/Src/main.c: capturePPS(), emitPPSTimestamp(), measure_temp(), a `pps` config key and one main-loop hook. qspi/config.txt documents the key. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ver USB Experimental attack on the ~6ms host-driven USB read jitter that swamps the clock's true ~180us precision — without a hardware PPS wire. The OTG_FS core rules out mitxela's last-microsecond FIFO injection, so instead we let the host anchor each PPS to a USB Start-Of-Frame, whose own arrival time the host can read in hardware (macOS IOKit GetBusFrameNumberWithTime). - Enable DWT->CYCCNT (free-running 12.5ns core-cycle counter) as the timebase both the PPS edge and each SOF are latched against. Unaffected by tempcomp SysTick steering (counts raw core clock), so it's a stable monotonic ruler. - Enable the USB SOF interrupt; PCD_SOFCallback latches (DWT, 11-bit frame from DSTS[13:8]) every 1ms, as its first act for minimal latency. - capturePPS() latches DWT at the edge; emitPPSTimestamp() appends the tail ,dwt_pps,sof_frame,dwt_sof to $PMTXTS (read under __disable_irq, no torn read). - Bump NMEA_BUF_SIZE 90->128 for the longer sentence. Host places the edge at hostTime(sof_frame) + (dwt_pps-dwt_sof)/f_dwt, immune to delivery lateness; dwt_pps deltas self-calibrate f_dwt (no core-clock assumption). Backward-compatible: lenient $PMTXTS parsers read the first 9 fields. Bench-only; +1kHz SOF ISR (~0.05% CPU) is the cost to watch. Builds clean (0 errors). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adversarial audit of the SOF-correlation change surfaced two real issues: - Stale first anchor: pps_sof_dwt/frame init to 0, so the first PPS emitted after enumeration (or with pps just toggled on, or on the emulator which has no USB SOF) carried a (0,0) anchor → a ~53s/−729ms host error on that record. Now a pps_sof_valid flag gates the tail: emit the plain 9-field sentence until a real SOF has latched. Backward-compatible; also makes the emulator correct. - SOF work ran unconditionally. Gate the latch on pps_ts_enabled (the SOF IRQ still fires — cheap, below the priority-0 display DMA — but does nothing when the feature is off); clear valid when off so a re-enable can't reuse a stale anchor. config.txt documents that pps=on enables the SOF tail. Builds clean (0 errors). Refuted findings (torn read, 16-bit atomicity, buffer truncation, display preemption, scope) left as-is. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Five GPS-derived astronomy read-outs, each opt-in via a MODE_* config key and shown on the 10-char date row while the live clock keeps running on the time row (SATVIEW-style, COUNT_NORMAL): MODE_SUN sunrise / sunset / solar noon (local), auto-paged MODE_SUN_AZEL sun azimuth & elevation, now MODE_MOON moon phase index (0-7) + illuminated % MODE_GRID Maidenhead grid locator MODE_LATLON latitude / longitude, auto-paged The astronomy maths is a self-contained Core/Src/astro.c (+ astro.h): low- precision NOAA/Meeus sun alt-az and event times, moon phase, equation of time, and Maidenhead. It has no firmware dependencies, so it unit-tests natively (test/test_astro.c, 45 reference vectors) and was validated to ~1e-5 before touching the firmware. The L4 has only a single-precision FPU, so the double maths would be slow soft- float. It is therefore computed in the main loop (astro_update(), gated on the active mode exactly as measure_vbat() is for MODE_VBAT) and swapped into a cache under a brief __disable_irq() mask; the sendDate() cases that run inside the SysTick ISR only format the cached scalars -- no trig in the interrupt. Modes are disabled by default; with no GPS fix they use fake_latitude/longitude if set, else show "----". config.txt documents the keys and the moon-phase index legend. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
"LAT 51.48" vs "LAT-51.48" let the minus swallow the separator. Add a sign
slot after the label space — "LAT 51.48" / "LAT -51.48" — so the digits
start in the same column either way, matching the RISE/SET layout. A 3-digit
longitude can't fit both the separator and the slot in 10 characters, so the
separator alone is dropped there ("LON-179.99").
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The paged modes (SUN, LATLON) previously flipped sub-screen every 2 s hard-coded. Make the dwell a config key (page_ms, ms; unset -> 5500, floored at 250 so a tiny value can't flood the date-board UART), driven off uwTick, and repaint the moment a page flips rather than waiting for the 1 Hz date-row refresh (guarded by decisec!=9 to avoid racing the SysTick sendDate). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- loadRules: reject untrusted rowLength/numEntries from TZRULES.BIN (RAM overflow) - MODE_TEXT: clamp snprintf untruncated-return index (uart2_tx_buffer OOB) - mk4-date latchDisplay: bound dp_pos per orientation (buffer_a/b OOB) - readConfigFile: don't treat a zero FAT timestamp as a cache hit (first-boot hang) - config-over-USB: defer postConfigCleanup out of the USB ISR (sendDate/nextMode races) - firmwareCheckOnEject: fatfs_busy guard for FATFS reentrancy on USB eject Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Two further pre-existing bugs from the same read-through. - mk4-time NMEA-over-CDC: CDC_Copy_Transmit() / CDC_Transmit_FS() dereference hUsbDeviceFS.pClassDataCDC unconditionally, but it is NULL until a USB host enumerates. The default nmea_cdc_level = NMEA_ALL forwards every GPS sentence to CDC, so on charger-only power the first forwarded sentence faults in ISR context. NULL guard on both, plus a length clamp before the memcpy. - mk4-date CMD_LOAD_TEXT: 'if (text_idx > MAX_TEXT_LEN) return;' passes at text_idx == MAX_TEXT_LEN and writes text[32], one past the 32-byte array. '>' -> '>='. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
MODE_LST and MODE_SOLAR turn the big HH:MM:SS digits into a live ticking Local Sidereal Time or apparent-solar clock. Both reuse the MODE_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. 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; apparent solar 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 unchanged. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
sun_subsolar(t, &lat, &lon) returns the point where the sun is at the zenith: latitude = solar declination, longitude where the local hour angle is zero (lon = alpha - gmst*15). It reuses the same gmst_hours series as sun_az_el and local_sidereal_time, so the three can never drift apart. Library + tests only — no display mode consumes it yet (a companion app plots it). Tests: the sun must sit at elevation 90 deg over its own subsolar point at every existing test instant (self-consistent but not circular — az/el and subsolar take different paths to the sky), plus declination anchors at the solstices and the absolute GMST anchors (IAU J2000, Meeus ex. 12.b) recorded in comments. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Builds on the $PMTXTS telemetry: while GPS-locked the clock learns each oscillator's frequency error vs die temperature (binned, per-oscillator), then during a GPS-loss holdover it steers the SysTick timebase from the HSE model to cancel temperature-driven drift, and optionally trims RTC->CALR from the LSE model so the battery RTC hands over better time across a power loss. - tc_learn / tc_apply / tc_rtc config keys (all default off = stock behaviour) - steering is a Bresenham SysTick->LOAD stretch inside the tick ISR (integer only; all model maths is main-loop); origin-free HSE model so only the temperature DIFFERENCE since GPS loss is applied, and the per-edge PPS phase snap makes re-lock instantly neutral - 'tc_dump = on' over serial prints the learned coefficients as paste-ready config lines (+ a $PMTXTC sentence); pasting them back freezes the model - MODE_TEMPCOMP diagnostic display: die temp / model offset / sample count - ticks-per-ppm derived from the runtime SysTick period (no core-clock assumption) Hardware-validated on an Mk IV: the LSE model learned 18.68 ppm vs 18.9 ppm measured directly (~1%). Survived an adversarial review pass (ISR-safety, integer math, sign conventions, re-lock neutrality, off-means-identical). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Two coupled additions on top of the self-learning tempcomp, both opt-in and byte-identical to stock behaviour when off. significance_fade: during GPS-loss holdover, retire each sub-second digit by its remaining SIGNIFICANCE rather than by the fixed dash timeouts. A live 3-sigma time-interval-error bound U(tau) = k*sigma*tau (sigma = RSS of the calibration, the MEASURED temp-model residual, and aging terms) drives setPrecision's P3/P2/P1/P0 ladder: a digit dashes the moment U passes its place value — the same dash glyph as the Tolerance_time_* ladder, but driven by measured significance instead of fixed timeouts. digit_bright[] carries the per-digit intensity for a future smooth-dimming display change (and for emulators). tc_fit now also reports the weighted-RMS fit residual, mean-centred for HSE whose model origin is arbitrary. tc_seed (warm start): reload a previously-learned model -- the last tc_dump, persisted in config.txt by the host -- as an EVOLVING prior rather than a freeze. The clock is temperature-compensated from the first second and keeps refining: tc_fit_one returns the achieved model order and tc_fit preserves the seed until real data supports a fit at least as rich, then hands over. New keys tc_seed / tc_seed_lo / tc_seed_hi (documented in qspi/config.txt); tc_dump emits the coverage so a paste round-trips. Over serial the "tc_seed = on" line is the trigger -- it arms tc_seed_pending (a single-word ISR write) and tc_housekeeping applies the seed from the MAIN LOOP, serialized with tc_fit/tc_governor, after the whole paste has parsed. The learned state keeps its main-loop-only contract; the USB ISR never touches the model. Survived two adversarial review passes (the feature, then this port), with fixes re-verified in the emulator against this exact tree: boot-time tc_nom_load capture (ticks-per-ppm is never 0), origin-invariant HSE residual, main-loop-serialized serial seeding with a whole-paste trigger, tc_reset clears the held prior, significance_fade drives the dash ladder (no regression vs stock when enabled), and measure_temp's cadence gate includes the fade so die_temp_c is always real. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…, not FADE_MAX
On a warm boot with significance_fade on, digit_bright[] defaulted to
{FADE_MAX,...} ("certainly significant"), so setPrecision() at boot (and each
PendSV second) showed the ms digit ticking — a ~1 s flash of numbers — until the
first per-second computeHoldoverFade() saw the huge holdover age (last_pps_time=0,
no PPS yet) and dashed them. Default to 0 (not significant): the sub-second digits
start dashed and only light once computeHoldoverFade proves significance after
lock (the same PendSV setPrecision path applies it, symmetric). Pre-existing PR mitxela#9
issue, not the SOF change. Builds clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ing arrives with seg_balance) PR mitxela#9 computes each sub-second digit's live significance (digit_bright, 0..FADE_MAX, from the 3-sigma holdover uncertainty) but the display driver could only show-or-dash. PR mitxela#10 built the missing per-digit dimmer. This connects them: - segbal_poll scales the ds/cs/ms columns' mirror duty by their digit_bright, and the seconds column's decimal point by digit_bright[3] (it dies with the 0.1 s digit). While any digit is mid-fade the mirror runs even with seg_balance off (identity duty), so the fade works standalone; with balance on the two multiply. - setPrecision's significance_fade branch now retires digits to BLANK instead of a dash glyph — the digit fades to black, as the feature's own comment always intended. - Stock behaviour (significance_fade off) unchanged: the Tolerance dash ladder remains. Verified in the WASM build of this source: partial fade tracked exactly (holdover age 11 s -> ms digit at 14/16 significance -> lit in exactly 14 of 16 cycles, DP tracking), zero-significance digits blank in the masters, relock restores full digits with auto balance resumed. ARM Release 0 errors. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The MODE_TEMPCOMP date-row pages honour the configurable page_ms dwell instead of a hardcoded 5.5 s, and the mode's enum entry moves to its final position (after the astro read-outs, before the alternate-timebase modes) so later tiers append ordinals without reshuffling. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…efault off) The display is voltage-driven (buffer chips + 10R per segment; the DAC sets the rail), so a digit's lit segments share the drop across its common return path — FEWER segments leaves more voltage per LED, and '-', '1', '7' glow visibly brighter than '8', worst at low brightness. There is no per-digit analog knob, but the scan buffers are sized [80] with the DMA circular: replay the 5-step scan as 16 cycles of 5 slots. Slots 0..4 remain the live masters every existing writer already targets (206 write sites, none changed). segbal_poll() — main loop, at most 1 kHz — mirrors them into slots 5..79 with each digit lit in s of its 16 cycles, s = 16 - strength*(16-2N)/100: at full strength s = 2N and every lit segment averages the same duty (1/N * 2N/16 = 1/8), uniform across the display; dense digits are untouched, sparse ones dim to match. The cycle mask (k*s)%16<s spreads the lit cycles evenly (worst refresh ~7.8 kHz) and always lights cycle 0 — the master — so masters count toward the duty exactly. GPIOB mirror slots keep the bCat column-select bits in every cycle; buffer_c[].high is GPIOC's one-cold column select + enables (NOT segment data — an earlier draft duty-cycled it and adversarial review caught the mis-addressing) and is carried verbatim except cSegDP, the digit's decimal point, which is counted in the digit's N and rides its duty. Config: seg_balance = on (=100) or 0..100 percent; default 0 = stock scan, stock timing. display_scan_len shadows the DMA count so the poll steers 5<->80 across standby (guarded: never restarts DMA in MODE_STANDBY) and the date-board chainloader's 40-slot takeover. Verified: WASM build of this source — exact duty counts at strength 100/50, GPIOC addressing byte verbatim in all 16 cycles, DP synced to its digit, live enable/disable/ re-enable clean (20/20 checks); ARM Release build 0 errors; independent line-by-line review verdict SOUND. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…hout the diode knee Hardware A/B at low brightness showed strength 100 (s = 2N, the linear map) still leaving sparse digits visibly brighter. That matches the drive physics: near the LED forward-voltage knee, segment current is exponential in per-segment voltage, so a dense digit's shared-path droop collapses its current by MORE than the linear 1/N model — the true bloom ratio can exceed the 8x a linear duty map can counter. segbal_duty(): 0..100 keeps the exact linear blend (100 -> s = 2N, unchanged behaviour); 101..300 becomes s = 16*(N/8)^(strength/100) — continuous at 100 (gamma 1), floor 1 for lit digits. At 200 a 1-segment '-' runs 1/16 duty against the 8-segment '8.''s 16/16: a 16x range on a knee-shaped curve. seg_balance widens to uint16 (atomic on M4), parse clamps to 300. Tune live over serial against the real display. Verified: WASM slot checks green at 100/50/200/300 incl. the 999->300 clamp (segbal_check); ARM Release 0 errors. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… anchors Hardware A/B confirmed the bloom is rail-dependent: one fixed strength over-dims sparse digits at full brightness while under-correcting at 5%. The firmware already knows the rail (dac_target, 4095 = dimmest), so interpolate the effective strength between two live-tunable anchors: seg_balance (dim end) and seg_balance_bright (full brightness). Unset (default) keeps constant strength — prior behaviour unchanged. -1 unsets. Tune each anchor at its own operating point over serial; the clock then compensates correctly across the whole auto-brightness range. Verified: WASM slot checks green at both rail ends (dac 4095 -> dim anchor's curve, dac 0 -> bright anchor's curve) + unset fallback; ARM Release 0 errors. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…r depth
The date row blooms identically to the time row (same shared-rail electronics), so
equalising one row alone made the display look worse, not better. The date board is a
UART slave with no config store, so the time board forwards the compensation:
- mk4-time: segbal_forward() sends CMD_SET_SEG_BALANCE (0x94) + 9 duty bytes (the 0..16
scale for digits with 0..8 lit segments) whenever the effective strength changes — from
the main loop, only when the UART is idle and outside the date board's latch window
(RX disabled there), with a ~2 s re-assert so a lost frame or a date-board reboot
self-heals. One config key now drives both rows.
- mk4-date (-> v0.0.3): staged atomic table commit in the RX parser (an interrupted frame
leaves the previous table intact); duty recompute at every buffer-write site; the TIM2
scan ISR applies a D-cycle duty mask with per-port segment masks (port A: segments
bits 4..10 + DP bit 14, mask 0x47F0; port B: segments bits 4..10 + DP bit 0, mask
0x07F1 — cathode selects verbatim in every cycle, verified against all ten cathode
words). Identity table (boot default) is bit-identical to stock.
- BOTH boards: adaptive dither depth. display_frequency (user config, 1..100 kHz) retunes
the scan timers, and the previous fixed 16-cycle dither would strobe at slow scans:
depth now picks the deepest of {16,8,4} keeping the dither >= ~200 Hz, off below that.
The forwarded table stays depth-agnostic; each board rescales to its own depth.
Verified: both ARM Release builds 0 errors; WASM slot checks all green (time board);
adversarial review (bit-layout / protocol+concurrency / ISR-timing lenses + adversarial
verification) confirmed zero defects, including the ISR cycle budget at the fastest
commandable scan.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ry brightness Hardware calibration (eyeball-matched digits at four rail levels, with the anchor confound guarded out) measured the required strength as a VALLEY, not a monotone: dac 0 614 2048 4095 strength 50 35 30 100 Strong at the dim end (LED knee: current exponential in voltage), moderate again at full rail (maximum-current IR drop in the shared return), mild between. The curve is baked as a piecewise-linear table and interpolated by the live rail every refill. Interface simplified to what a user should ever see: seg_balance = on -> AUTO, the measured curve (the intended setting) seg_balance = 0/off -> stock scan, stock timing (default) seg_balance = 2..300 -> fixed manual strength, kept for tuning experiments seg_balance_bright is retired (the two-anchor scheme is superseded by the table). The forwarded duty table keeps the date board in step automatically — auto included. Verified: WASM slot checks green at all four breakpoints plus interpolated points (dac 1331 -> 33, C truncation semantics mirrored in the test); ARM Release 0 errors. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…e's to bump Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…olons with the rail) seg_balance AUTO: full eyeballed rail sweep (2026-07-11) landed the even point on K = 10*9^(dac/4096) — the LED knee. Replaced the mis-calibrated 4-point valley LUT with a 9-point sample on power-of-two dac breakpoints; hits the measured anchors 10/30/90 exactly. segbal_check.mjs ALL PASS, GPIOC addressing verbatim. colon_balance (NEW): colons are TIM2 PWM, not scanned, not coupled to dac_target — held fixed brightness while the digits dimmed. New key ties the colon PWM to the rail: 0/off, on/1 (AUTO rail curve), 2..256 (fixed manual scale for eyeball calibration). Folded into loadColonAnimation, re-applied by colon_balance_poll() from the main loop. Adversarial review (workflow): only real finding was an ISR race — loadColonAnimation() also runs in the USART2 button ISR (nextMode), so the main-loop reload masks JUST USART2 (deliberately not PPS/EXTI9_5, to protect timing); cosmetic + self-healing, no fault possible. Also from review: standby guard (match segbal_poll) + colonForce so an explicit colon_balance set lands even inside the AUTO hysteresis (smooth sweeps). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… not 4096 The 12-bit DAC full-scale code is 4095 (dac_target maxes there), matching the original firmware — so anchor the exponential rail curves there instead of 4096. Top LUT breakpoint 4096 -> 4095 for both SEGBAL_AUTO_DAC and COLON_AUTO_DAC; formula K = 10*9^(dac/4095). The 9 anchor values round identically at either divisor, so the strength/scale LUTs are unchanged — this only removes the one-code overshoot at the dim end and squares the maths with the hardware. Also fixed the stale parseBrightness "0 to 4096" comment (code checks <=4095). Refreshed fwt/fwd images. No behaviour change beyond the dimmest code; no version bump. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Bake the measured VTT9812FH (R11=470K) brightness curve as the compiled-in brightnessCurve[] default, so the clock needs no config.txt BS lines (a BS line still overrides its stop). config.txt marks the BS lines optional. (Split out of the tempcomp auto-persist commit 3218270 — this is its brightness-curve-bake half; the persist half lives on stack/tempcomp.) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Colon AUTO curve reworked from the 9-point starter LUT to the straight line a full production-Mk IV eyeball sweep (2026-07-11) actually landed: scale = clamp( 256*(4095-dac) / (4095-colon_full_at), colon_floor, 256 ) Two per-hardware anchors — colon_full_at (dac at which colons hit full scale, default 2048) and colon_floor (scale at the dimmest rail, default 20) — are baked in and overridable from config.txt, exactly like the BS brightness curve. Passes through the measured points (dac 3276->102, 2867->153); ~ (4095-dac)/8 at the defaults. (Split out of 0f0634c — this is its colon-curve engine half; the single BALANCE menu toggle half lives on stack/menu.) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The per-digit dimmer significance_fade anticipated: while any sub-second digit is mid-fade, run the duty mirror even with seg_balance off (identity duty) so digit_bright (0..FADE_MAX, recomputed each second from the live U(tau)) scales each digit's cycles — the real display renders the partial fade the emulator always could. The decimal point rides its digit's fade. Also document the display-balance controls (seg_balance / colon_balance) in the golden config. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The honest oscillator-stability signal is the FREE-RUNNING HSE phase from DWT->CYCCNT, not the SysTick residual (capturePPS re-pins SysTick every second -> infinite-gain reset -> the residual's ADEV rolls off at tau>=2s and measures the loop, not the crystal). Per locked second: phase += DWT delta - 80e6 (frac-freq error), integrated to cumulative phase (int32 ticks, 12.5ns) in a 16 KB ring placed in the previously-unused RAM2 bank (new .ram2 NOLOAD section, off the app-CRC/reflash region). Overlapping ADEV (IEEE-1139) computed on demand: int64 second-difference kernel, one float sqrt per octave. Gap-restart on missed PPS (needs contiguous 1s samples). adev_reset() clears the un-zeroed RAM2 at boot. Core verified in the emulator (adev_check.mjs): white FM -> tau^-1/2, random-walk FM -> tau^+1/2, and the MCU kernel matches a double-precision reference to 5e-8 across all tau. Display (MODE_ADEV paging) + $PMADEV serial dump are the next increment. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Display: sigma_y(tau) paged one octave per page_ms dwell (tau=2^page s) on the date row while the time row keeps live GPS time — the satview/tempcomp pattern. Compact scientific that always fits 10 chars: "1s 3.2e-11" / "64s 3e-11" / "1024s3e-11" (mantissa digit dropped as the tau label grows). "Adev ----" until the ring holds 3 contiguous 1 s samples. Main loop recomputes the octave cache + repaints on each page flip (thread context, sub-ms), 0xFFFFFFFF sentinel forces an immediate paint on entry. MODE_ADEV config key gates it like any mode. Serial: "adev_dump = on" (serial-only, origin-guarded) emits ONE checksummed $PMADEV,<valid>,<noct>,<sigma_0..noct-1>*CC sentence — sigma_k at tau=2^k s, %.2e, fresh cache so it works in any display mode. Same NMEA framing/CDC submit/BUSY-retry path as $PMTXTS and tc_dump. Verified in the emulator: "Adev ----" fallback, a well-formed <=10-char octave row, and the real adev_dump_step() sentence — framing + XOR checksum valid, 9 octaves, every dumped sigma matches the on-MCU cache it read. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…erance, maturity gate, render
From the 27-finding adversarial review of MODE_ADEV. (1) The int32 phase
accumulator hit signed-overflow UB after ~311 days of continuous lock at 1 ppm
— accumulate unsigned (defined wrap) and compute the second difference back in
unsigned, exact modulo 2^32. (2) A real PPS gap now zeroes adev_noct IN THE ISR
so the display cannot keep paging the stale pre-gap curve until the next page
flip, and the render shows "Adev HOLd" when no fresh sample has arrived for
2.5 s (holdover previously replayed a frozen curve as if live). (3) ONE missed
PPS second no longer wipes the whole record (a single dropout used to discard
up to 68 min and make tau=1024 practically unreachable) — the phase midpoint is
interpolated and the chain continues; >=2 missing seconds still restart. (4)
Publication maturity gate: an octave used to appear at valid=2m+1, i.e. ONE
second-difference rendered at full authority — adev_reduce now requires
valid >= 4m (>= 2m overlaps); the raw estimator keeps the mathematical gate for
tests/oracle. (5) The tau unit "s" rendered as the digit 5 ("1024s3e-11" read
as 10245...) — dropped, with a guaranteed separator ("1024 3e-11"). (6) The
page flip recomputed all 11 octaves (~4-6 ms); it now computes noct
arithmetically and only the displayed octave (~0.2-0.4 ms); the $PMADEV dump
still full-reduces.
Tests: capture-path regressions through the REAL adev_push_dwt (2^32 DWT-wrap
exactness, missed-second continuation, gap-zeroes-noct) + maturity-gate
assertions + render format.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
$PMADEV is now self-describing BEFORE any consumer ships: leading
epoch (unix s) + tau0 fields, so tau_k = tau0*2^k needs no out-of-band spec
and stale sentences are detectable. NEW $PMHDEV ("hdev_dump = on"): overlapping
HADAMARD deviation from the same phase ring (no extra RAM) — the third-
difference kernel annihilates linear frequency drift, so long-tau octaves
report the oscillator rather than the temperature ramp. Dump line buffers
widened (the enriched sentences outgrew NMEA_BUF_SIZE).
(Split out of f7ba9c4 'opportunity bundle' — ADEV hunks only; star hunks
live on stack/star-transit, menu hunks on stack/menu.)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…STAR) A star culminates on the local meridian when LST == its right ascension. From the GPS fix + local_sidereal_time, star_update() computes for each of 30 catalogue stars the seconds until its next transit and the altitude it reaches (90 - |lat - dec|), precesses the J2000 positions to date (first-order IAU), drops stars that never clear the horizon, and caches the soonest 8. MODE_STAR pages them as "<name> <h:mm>" countdowns on the date row (time row keeps live GPS); "STAr ----" with no fix. Recomputed once a second in the main loop (double trig, thread context), countdown ticks off the cached transit epoch at 1 Hz. MODE_STAR config key gates it. "star_dump = on" (serial-only) emits one $PMSTAR,<n>,<name>,<sec>,<alt>,..*CC sentence (same CDC/checksum path as $PMADEV/$PMTXTS) for the app. Verified in the emulator: an independent double-precision recomputation reading the firmware's OWN catalogue + LST matches $PMSTAR (names, order, seconds<=2, altitude<=1deg), the list is sorted, altitudes are real culminations, the row renders <=10 chars, and the no-fix fallback holds. CATALOGUE NOTE: the 30 J2000 coordinates are hand-entered and want a pass against SIMBAD/Hipparcos before upstreaming; the transit maths is verified independent of the values. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The date-row countdown was <name> H:MM — the H-MM dash read ambiguously (is 0-01 an hour or a minute?). Show bare space-separated values instead: under an hour it's minutes seconds (<name> 1 35), an hour or more switches to hours-'h'-minutes (<name> 2h15) so a distant transit can't masquerade as 2 min 15 s and the field still fits the 10-char row. serial sentence unchanged. NOTE / cascade: MODE_STAR is the star-transit PR (onto the mitxela#8 base); this display tweak should be back-ported there when that PR is refreshed. Applied on rollup so it's flashable now. FORMAT is my read of 'm and s rather than dashes, just the values' — confirm the exact unit choice. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Mirrors generate-tzrules.py. Reads HYG database v4 (CC0), keeps naked-eye
stars people recognise (default mag<=2.5, 92 after dropping secondary
multiple-star components that duplicate a primary's transit), magnitude-
sorted for a firmware early-stop at star_max_mag. Emits stars.bin: 16B
header (magic MST1 / count / recordLength=10 / mag_scale) + 10B packed
records {ra u16, dec i16, mag i16, nm char[4]}. Names are 4-char uppercase
proper-name abbreviations (Bayer greek+con fallback), unique, with a
legibility report (every letter renders on the mk4-date font; I/O/S/Z read
as 1/0/5/2, K/M/Q/V/W/X are rough — flagged, not excluded). hyg cache
gitignored. NOT yet wired into firmware.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…d set) The 30 hand-entered stars become the FALLBACK; the live catalogue is now loaded from /STARS.BIN on the card (generate-stars.py, HYG v4, ~90 famous named stars). loadStars() is hardened like loadRules (PR#7): validate magic 'MST1' + fixed 10-byte record length, clamp count to STAR_MAX(128) BEFORE writing, byte-check every f_read, sanity-check RA/Dec; ANY failure (no card, bad header, torn read) falls back to the baked bright set so the feature always works. star_update() reworked to iterate the runtime star_buf/star_count and pick the soonest-8 via a bounded insertion sort into a STAR_SHOW-sized array — killing the per-STAR_N stack VLAs (dth/altd/vis) that would overflow the stack at catalogue scale. New config knob 'star_max_mag' trims the (mag- sorted) file at load with an early-stop. Names widen to 4 chars in the /date-row render. star_max_mag applied at boot in loadStars. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
No memory-safety defects were found (star_buf bounds, the insertion sort and the fallback all held up). Three low-severity robustness fixes for untrusted /STARS.BIN + config.txt: - magcut: saturate star_max_mag*100 into int16 instead of an out-of-range float->int16 cast (UB). A huge star_max_mag now means 'load all' rather than wrapping negative and silently collapsing to the baked fallback. - unsorted files: filter per-record (continue) instead of break-on-first- over-cut, so a non-mag-sorted file isn't truncated (loop still bounded by EOF + STAR_MAX). - header: validate mag_scale == 100 so a file written to a different scale is rejected (fallback) rather than mis-filtered. The 'live star_max_mag change needs reboot' note is left as documented boot-time behaviour. star_check gains a saturation regression test. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… $PMSTAR, hardening
From the 26-finding adversarial review. Astronomy: the first-order precession
formula carried a tan(dec) term that diverges near the pole — Polaris\x27s
transit countdown was ~5 min wrong by 2026 and growing. Replaced with the
rigorous IAU-1976 zeta/z/theta rotation (singularity-free at every dec), plus
linear PROPER MOTION (previously neglected: alpha Cen, mu_alpha* -3678 mas/yr,
drifted ~14 s of transit time by 2028 — larger than both the 1.32 s RA storage
quantum and the 0.9 s DUT1 floor). Apparent places are cached and refreshed at
LOW cadence (daily; precession moves ~0.14 arcsec/day), so the per-second
star_update gets CHEAPER (consumes the cache, no per-star trig).
STARS.BIN v2: recordLength 14 adds pmra/pmdec i16 (mas/yr, HYG); legacy
10-byte files still load (PM=0). Generator + self-verify updated; regenerated
card catalogue = 92 stars / 1304 B.
Hardening: (1) $PMSTAR emitted %.3s of 4-char names — 13 three-char prefixes
collide across the card (POLL/POLA -> POL): now the full 4 chars + a
provenance field ($PMSTAR,<n>,<C|B>,...) so a failed card can no longer
masquerade as success; (2) fallback fires ONLY when no usable file exists — a
valid catalogue filtered empty by star_max_mag stays honestly empty; (3)
star_max_mag rejects NaN/inf (atof("nan") reached the float->int16 UB the
clamp claimed to guard); (4) name bytes are sanitized on load (control/high-
bit bytes from a hostile file went raw to the date-board UART).
Tests: oracle upgraded to an INDEPENDENT rigorous-precession+PM implementation
(the old one reused the firmware\x27s own constants — a transcribed-wrong model
passed silently), plus ABSOLUTE astropy FK5-of-date anchors for Polaris /
Sirius / Rigil Kentaurus computed from the exact quantized record values;
provenance + 4-char-name assertions.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…talogue The transit moment gets its payoff: when the soonest star culminates, "NAME NOW" latches for 8 s (the 1 Hz re-sort used to roll it off the list the second it happened). Culmination side (due South/North — the sign fabs() threw away) rides in both star_cache and $PMSTAR (per-entry 4th field). Horizon gate widened to the refraction band (alt > -0.57 deg: ~34 arcmin lifts a grazer into view; displayed alt clamps at 0). Catalogue curated per review: Mirach MIRA->MRCH (impersonated the more-famous Mira), Markab takes MARK (obscure Markeb had stolen it; Markeb->MARB), Megrez admitted above the mag cut to complete the Big Dipper (92->93 stars). Anchor names untouched. (Split out of f7ba9c4 'opportunity bundle' — star hunks only; ADEV hunks live on stack/adev-display, menu hunks on stack/menu.) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Owner call: the transit mode must not work without the catalogue file. The
~30-star baked fallback is GONE — no /STARS.BIN (or an invalid one) leaves the
catalogue empty and the mode honestly shows "STAr ----" instead of quietly
substituting lookalike data. With a single source the $PMSTAR provenance field
(C|B) is meaningless and is dropped ($PMSTAR,<n>,{name,sec,alt,dir}...).
Frees ~1 KB.
(Split out of dc83c30 — star-core half; the factory-reset-forgets-tempcomp +
golden-config cleanup half lives on stack/menu.)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
(star_max_mag is a parser-only key with no config.txt block anywhere in the rollup tree — nothing to document beyond the MODE_STAR paragraph.) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Predicts the next meridian transits of the bright stars:
MODE_STARpages the soonest eight with alive countdown; a
STARS.BINcatalogue on the CLOCK drive (93 famous stars, HYG v4.1, CC0; the mode requires the file — no baked fallback);$PMSTARfor hosts. Rigorous IAU-1976 precession + proper motion, oracle- and ephemeris-anchored.Stack: based on
adev-display(#11).Try it now: fwt.bin · fwd.bin — the union of this PR stack, byte-identical to the merged result.
Docs:
docs/firmware-additions.md, shipped in the final (menu) PR — see Star transits and STARS.BIN.Note
You can try this PR without hardware: the PCC web app runs the real clock4 firmware compiled to WebAssembly, and its clock is built from a rollup of the whole PR series together, so this PR's behaviour is live there alongside the others. The app's DEVICE → UPDATES panel shows the exact commit it was built from. Fair warning: the app around the firmware is beta and still has bugs — the firmware behaviour is the real thing, the app chrome less so.
What it does
(no near-pole divergence — a first-order formula puts Polaris ~5 min wrong by 2026) plus linear
proper motion (alpha Cen alone drifts ~14 s of transit time by 2028 without it), refreshed daily.
NAME NOWlatches for 8 s at culmination; each entry carriesits culmination side (due S/N) and altitude; the horizon gate admits the atmospheric-refraction
band so a grazer the atmosphere lifts into view isn't dropped.
STARS 0= no valid file,and the mode shows
STAr ----— there is deliberately no baked substitute, so a failed file cannever masquerade as success). Hostile-file hardening on every field;
star_max_magtrims thecatalogue.