Skip to content

Astro pack: sun / moon / Maidenhead / lat-lon display modes#6

Draft
peterlewis wants to merge 6 commits into
mitxela:masterfrom
peterlewis:astro-pack
Draft

Astro pack: sun / moon / Maidenhead / lat-lon display modes#6
peterlewis wants to merge 6 commits into
mitxela:masterfrom
peterlewis:astro-pack

Conversation

@peterlewis

@peterlewis peterlewis commented Jun 30, 2026

Copy link
Copy Markdown

Adds five GPS-derived date-row modes — sun rise/set/noon, sun azimuth/elevation, moon phase, Maidenhead grid, and lat/lon — plus the page_ms dwell key for paged read-outs.
Stack: based on pcc-firmware-proposals (#5).
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.


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.

Draft / proposal. A self-contained, opt-in set of GPS-derived astronomy display modes for the Mk IV. Independent of the timekeeping-fix PR — branched from master, touches no timing code. All five modes — and the page_ms paging — have been built and confirmed working on real hardware; the maths is also verified natively (see Build & test). It stays a draft for your review, not for want of testing.

What this adds

Five new display modes, each disabled by default and enabled individually with a MODE_* key in config.txt, exactly like the existing modes. They follow the MODE_SATVIEW pattern: the read-out shows on the 10-character date row while the live clock keeps running on the time row, so you never lose the time to read the sky.

Mode Date row Example
MODE_SUN sunrise / sunset / solar noon (local), auto-paging (default 5.5 s, set by page_ms); RISE/SET/SOL labels are 4-char-padded so the time columns stay aligned RISE 04.43 · SET 21.21 · SOL 13.02
MODE_SUN_AZEL sun azimuth & elevation now AZ179EL62, AZ083EL-29
MODE_MOON phase index (0–7) + illuminated % MOON 4 100 (full), MOON 7 16
MODE_GRID Maidenhead grid locator IO91xl
MODE_LATLON latitude / longitude, auto-paging LAT 51.48 · LON 0.00

The sun page auto-pages (default 5.5 s, set by page_ms); the labels are padded to 4 characters so the
time digits stay column-aligned (note SET/SOL sit one column right of RISE):

RISE 04.43
SET  21.21
SOL  13.02

With no GPS fix the modes use fake_latitude/fake_longitude if set (handy indoors), otherwise show ----. The moon needs no fix at all.

Why these

They reuse data the firmware already has (position + disciplined UTC) for things the existing modes don't surface, and they suit the device's likely owner — a tinkerer / ham / observer: sun & moon position, sunrise/sunset, your grid square, your coordinates.

Design — keeping the maths out of the ISR

sendDate() runs inside the SysTick ISR (the 0.900 s repaint), and the L4 has only a single-precision FPU, so the double-precision astronomy would be slow soft-float in an interrupt. So the heavy maths runs in the main loopastro_update(), gated on the active mode in exactly the way measure_vbat() is gated for MODE_VBAT — and the small result struct is swapped into a cache under a brief __disable_irq() mask. The sendDate() cases only format cached scalars: no trig, no sun_times, nothing heavy in the ISR. Position is snapshotted once per update so az/el, grid and lat/lon are always mutually consistent.

Everything is parameterised by NUM_DISPLAY_MODES (the enum append grows config.modes_enabled[] automatically), so the modes join the button cycle and config parsing with no other changes.

The maths — Core/Src/astro.c (+ astro.h)

A self-contained C99 + <math.h> unit with no firmware dependencies:

  • sun_az_el() — apparent solar azimuth/elevation (NOAA/Meeus low-precision).
  • sun_times() — sunrise / sunset / solar noon (+ optional civil/nautical/golden), decimal UTC hours.
  • moon_phase() / moon_illuminated_fraction() / moon_phase_index().
  • equation_of_time().
  • maidenhead() — 6-char locator.

Because it has no firmware ties, it unit-tests on a host. test/test_astro.c checks it against 45 reference vectors (four locations spanning hemispheres/seasons incl. a polar-winter case, plus fixed Maidenhead known-answers like Trafalgar Sq → IO91wm, ARRL HQ → FN31pr):

$ cd mk4-time/test
$ cc -std=c99 -O2 -Wall -Wextra -I ../Core/Inc ../Core/Src/astro.c test_astro.c -lm -o test_astro && ./test_astro
...
45/45 passed, 0 failed

Agreement with the reference is ~1e-5 (well under the whole-degree / whole-minute the display shows). astro.c is added under Core/Src, which the .cproject compiles automatically; test/ is not a source path, so the test never enters the firmware build.

Accuracy & legibility (honest notes)

  • Low-precision algorithms: sun position good to a fraction of a degree, event times to a minute or two — appropriate for a 7-seg read-out.
  • 7-seg letters are approximate (S→5, I→1, O→0, Z→2), the same as the existing GPS/bat/ttff/rtc labels. Maidenhead's lowercase subsquare (ax) renders best-effort and is in-bounds for lut_7seg.
  • Moon phase is shown as the standard 8-step index because the names don't spell on 7-seg; the legend (0 new … 4 full … 7 waning crescent) is documented in config.txt.

Config

config.txt gains the five mode keys (disabled by default) with comments and the moon-index legend, plus page_ms — the dwell (ms) each sub-screen of the paged modes (SUN, LATLON) shows before flipping (default 5500, floored at 250):

MODE_SUN = disabled
MODE_SUN_AZEL = disabled
MODE_MOON = disabled
MODE_GRID = disabled
MODE_LATLON = disabled
page_ms = 5500
#fake_latitude  = 51.48
#fake_longitude = -0.01

Build & test

Build mk4-time in Release (Debug links a non-bootloader script, so a Debug fwt.bin won't flash via the bootloader) — no project-file changes are needed (astro.c auto-compiles). All five modes — and the page_ms sub-screen paging, its cadence tuned on the unit itself — have been built and confirmed working on real hardware (from a single location); cross-location and edge-case behaviour — hemispheres, the antimeridian, polar day/night — is covered by the 45 native reference vectors above. It stays a draft for your review, not for want of testing.

Review & verification

Before this PR the change was put through an adversarial review across three independent lenses, each trying to break it rather than confirm it:

  • Firmware integration / ARM build — enum + cycle wiring, config.modes_enabled[NUM_DISPLAY_MODES] sizing, buffer bounds, switch-case correctness, and that the new modes take the normal COUNT_NORMAL live-clock path.
  • Math & port fidelity — re-ran the native test (45/45) and checked edge cases: poles / polar day-night, the antimeridian, azimuth wrap, moon-phase index wrap, maidenhead clamping, fmod sign.
  • 7-seg display strings — every mode's output width across the full value range (≤10 chars, no overflow) and glyph legibility.

Verdict was ship-with-fixes; the findings were addressed in this branch:

  • ISR snapshot bug (fixed)MODE_LATLON formatted the live latitude/longitude inside sendDate() (which runs in the SysTick ISR) instead of the IRQ-mask-swapped astro cache, so the shown coordinates could disagree with the cached have_pos/grid in the same frame. Lat/lon are now snapshotted into the cache with everything else.
  • Negative zero (fixed) — on the prime meridian / equator %.2f printed LON-0.00; switched to integer-hundredths so it reads LON 0.00.
  • Glyph set (confirmed, no change) — the date board's lut_7seg renders the full printable-ASCII set, so the labels and lowercase Maidenhead subsquares display fine; maidenhead() is left faithful.
  • ISR safety (confirmed) — the double-precision trig runs once per second in the main loop (mirroring measure_vbat); only cached scalars are formatted in the ISR, so there's no soft-float in the interrupt.

Possible follow-ups

  • Render sun-event times on the big time row (needs the SHOW_OFFSET-style segment-buffer path) for nicer HH:MM:SS.
  • Civil/nautical twilight and moonrise/moonset (the maths is already present in astro.c).

Files

  • mk4-time/Core/Src/astro.c, mk4-time/Core/Inc/astro.h — new, self-contained maths.
  • mk4-time/test/test_astro.c — new, native test (not built into firmware).
  • mk4-time/Core/Src/main.c, Core/Inc/main.h — enum, cache, astro_update(), 5 sendDate cases, 5 config keys, main-loop hook.
  • qspi/config.txt — documented keys + legend.

🤖 Generated with Claude Code

@peterlewis peterlewis force-pushed the astro-pack branch 3 times, most recently from de7fb99 to ef309b5 Compare June 30, 2026 17:33
peterlewis added a commit to peterlewis/clock4 that referenced this pull request Jul 6, 2026
The exact residue between the pure union of the five draft PRs (mitxela#5 $PMTXTS,
mitxela#6 astro pack, mitxela#7 hardening, mitxela#8 sidereal/solar, mitxela#9 tempcomp + significance-fade
+ tc_seed — mitxela#5 and mitxela#6 arrive as ancestors of mitxela#9 and mitxela#8) and the rollup tree that
has been emulator-verified and hardware-run:

- astro.c/astro.h/test_astro.c: rollup astro refinements not yet folded back into
  the refreshed PR mitxela#6/mitxela#8 heads (candidates for a future PR update)
- version.c: rollup version string
- qspi/output/*.bin: rollup's built firmware images
- .settings IDE noise

After this commit the megapack tree is BYTE-IDENTICAL to the previously validated
rollup (feaf970) while the merge ancestry proves containment of all five PR heads.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
peterlewis and others added 6 commits July 12, 2026 11:45
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>
@peterlewis peterlewis force-pushed the astro-pack branch 2 times, most recently from 7cd0054 to 02c9823 Compare July 12, 2026 13:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant