Skip to content

On-device two-button menu with persistent settings (final: stack = validated rollup)#13

Draft
peterlewis wants to merge 85 commits into
mitxela:masterfrom
peterlewis:menu
Draft

On-device two-button menu with persistent settings (final: stack = validated rollup)#13
peterlewis wants to merge 85 commits into
mitxela:masterfrom
peterlewis:menu

Conversation

@peterlewis

@peterlewis peterlewis commented Jul 12, 2026

Copy link
Copy Markdown

The capstone: a sectioned on-device menu (CAL / ASTRO / DISP / DIAG / SYS) driven by the two date-
board buttons through a rolling-chord grammar, live-preview editors, and settings that persist
across power loss — in the top two internal-flash pages on 1 MB parts, or a host-visible
SETTINGS.BIN on the QSPI volume on 256 KB parts (which have no spare internal flash). Includes the
companion date-board firmware (chord protocol), per-build version tags, and the documentation for
the whole series.

Stack: based on star-transit (#12) — the FINAL PR. Merging #5 → … → this PR in order is a chain of
fast-forwards whose result is byte-identical to the
rollup branch
(the tree this series was
built, emulator-tested, and hardware-validated on; the last commit here is an explicit integration-
delta aligning the union to that validated tree).

Try it now (ready to flash):
fwt.bin (time board) ·
fwd.bin (date board — needed
for the menu buttons; everything else works on a stock date board). Copy to the CLOCK drive and
power-cycle. On 256 KB clocks also create SETTINGS.BIN for persistence (16 KiB of 0xFF, first
file on a fresh volume — see docs).
Docs: this PR ships docs/firmware-additions.md — a complete write-up of every feature in the
series (modes, config keys, serial sentences field-by-field, the menu, flash-drive file formats), written
to slot into the project's documentation page.

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.

Retargeting offer: the nine PRs form a linear stack against master. If you'd prefer to take the
series onto an integration branch (e.g. pcc-series), create it and say the word — I'll retarget all
nine in one pass; each merge then becomes a fast-forward and the per-PR diffs collapse to just their
own commits as the stack lands.

Highlights

  • Rolling chord grammar: hold-both cycles self-labelled stages; release fires the shown one. Commit
    (APPLY) and discard (CANCEL) are separated by a buffer stage; toggles save on exit (DONE);
    a deep hold bails to the clock from anywhere; REBOOT arms only on the second cycle so the entry
    gesture can't reset the clock.
  • Editors: live preview on the real digits (including the colon animations), hold-acceleration with
    grid snap, blink-at-rest (the digital-watch "you are setting this" idiom), a T-3 s warning flicker
    before the 15 s idle-abandon, and a last-enabled-mode guard (LASt).
  • Persistence: 64-byte CRC-guarded append-log records, A/B page rotation, config.txt precedence via
    an mtime stamp (editing config.txt on the drive reclaims any key it defines). On 256 KB silicon the
    store rides inside SETTINGS.BIN — resolved through the read-only FAT layer at boot, rewritten
    raw in place (never touching FAT metadata), host-clobber-safe (mapping re-verified before every
    write; content sentinel; graceful RAM-only fallback if the file is absent or fragmented).
  • Diagnostics from the front panel: live PPS LOCK/HOLD n and star-catalogue count rows;
    menu_dump serial verdict; factory reset with a real confirm (DELETE ALL) and completion tell.

Known issues

  • Rare one-frame date-row corruption (e.g. 026-07-12 for a single second, self-healing at the
    next 1 Hz repaint). Root cause is in the stock date-board UART configuration: reception is POLLED
    from the main loop with overrun detection disabled (USART_CR3_OVRDIS), so if the loop is delayed
    more than ~2 byte-times (~174 µs at 115200) while a frame streams in, one byte is silently
    dropped. The features in this series add date-row traffic and display-scan ISR load, which raises
    the (still small) odds of hitting the window. It cannot corrupt state or stick — command bytes
    have bit 7 set, so the parser resyncs on every frame and the next repaint heals the row. Candidate
    fixes (deliberately not bundled here): trim the scan-ISR worst-case path, or enable ORE handling
    with a careful drain path; a drop-counter diagnostic would quantify the real-world rate first.

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 and others added 23 commits July 12, 2026 14:14
- 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>
peterlewis and others added 29 commits July 12, 2026 14:24
§3c — the value scrubber. A run of same-direction STEP taps within 350 ms grows the
increment: fine for the first couple (precise nudge), then doubling every 2 taps,
capped at range/16, so page_ms (250..60000) and matrix (1000..100000) sweep in a few
seconds. Accelerated steps SNAP to the coarse grid so you can't sail past a round
target; a pause (>350 ms) or a direction reversal drops back to 1x. Run-state is
main-loop-only statics, reset on enter-edit and on exit/idle — never ISR-touched.

§3b — the ring never hides a value. STEP items get a compact unit form (page_ms "5.5S",
matrix "20KHZ") + label-trim; ENUM/TOGGLE keep the LABEL recognisable (you scroll by
label) and truncate the value, or fall back to label-only when there's no room.

Verified: menu_accel_check.mjs (single tap = one step; held run hits the floor in <=12
taps not ~21; grid-snap; reversal + pause both reset to fine; ring shows "5.5S"). All
emu suites green; ARM 81%.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Exposes the multiplex refresh rate (setDisplayFreq) as an editable menu
item under SYS, 8000..100000 Hz step 1000. Live-applies while editing via
the clamping setter (never ARR-direct from the menu path, so a stray value
can't stall the display), reverts on CANCEL, persists on SAVE.

Persistence: ovr.matrix_freq (u32) stored at record bytes 33..36 (was
reserved padding) — EE_SCHEMA stays 1, old records round-trip with
matrix_freq=0 which the clamping setter treats as a no-op. config.txt
MATRIX_FREQUENCY still wins under the existing mtime-precedence merge.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…TC VIEW

Adds a DIAG toggle (KID_TEMPCOMP) that arms the self-learning oscillator
compensator as a unit: tc_learn (sample ppm-vs-die-temp while GPS-locked)
AND tc_apply (steer SysTick from the model in holdover). Previously the
menu only exposed MODE_TEMPCOMP, which is just the diagnostic *view* — so
the mode row is relabelled TC VIEW to end the ambiguity (enabling the
compensator vs. looking at it).

config.txt keeps the finer tc_learn / tc_apply keys for asymmetric setups;
either one marks KID_TEMPCOMP defined so the mtime-precedence merge treats
config as authoritative. Persisted at record byte 37 (was reserved),
EE_SCHEMA stays 1 — old records read tc=0 (disarmed), matching prior
default behaviour.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
DISP gains one BALANCE toggle (KID_BALANCE) that arms BOTH brightness-
uniformity systems at their baked AUTO curves — per-segment duty
equalisation (seg_balance) and rail-tied colon dimming (colon_balance) —
the 'brightness uniformity' control the menu was missing. config.txt keeps
the finer seg_balance / colon_balance keys (incl. manual strengths) for
calibration; either marks KID_BALANCE defined so config precedence holds.

Persisted at record byte 38 (was reserved), EE_SCHEMA stays 1.

(Split out of 0f0634c — this is its BALANCE menu-row half; the 2-anchor
colon-curve engine half lives on stack/segment-balance.)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Editing ALTCOLON picks the colon animation the sidereal/solar faces use,
but you edit it from a civil face — so applyColonForMode() kept rendering
the civil colon and the choice was invisible. Add a colon_preview override:
while a colon-animation item is being edited, the value under the cursor is
forced onto the real colons regardless of display context, tracks live as
you step, and is cleared + restored to the context colon on SAVE / CANCEL /
idle-exit (menu_to_L0 clears it too, so an idle timeout can't leave the
preview stuck). COLON editing already previewed via the civil context; this
makes ALTCOLON behave identically.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Renames the alt-colon control from ALTCOLON to COLONALT (reads as a COLON
variant, groups with COLON in DISP). The stable KID_COLON_ALT id (3) is
unchanged, so persisted overrides survive.

(Split out of 633cc06 — this is its menu-label half; the config-key rename
alt_colon_mode -> colon_alt_mode half lives on stack/sidereal-modes.)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The bootloader reflashes only when the image CRC differs from the loaded
one. Two builds of the same version on the same day could be byte-identical
(the pre-build only did 'rm -f version.o', which is skipped on direct-target
/ incremental builds, so __DATE__/__TIME__ went stale) — so the second flash
was silently treated as 'already loaded', and the reported version still
read a bare 0.0.5.

gen-build-id.sh (new pre-build hook, wired from the .cproject prebuildStep
and the generated Release makefile) regenerates Core/Src/build_id.h with a
fresh epoch-seconds BUILD_ID and removes version.o every build, so version.c
recompiles with a fresh id + timestamp. The version string becomes
'Version 0.0.5+<epoch>' — every image is byte-unique (proven: identical
source, two builds -> different CRC -> reflashes), and the detected string
distinguishes them. Kept at 0.0.5 per request; build_id.h is a gitignored
generated artifact, guarded by __has_include so bare checkouts + the
emulator build (never compiles this TU) fall back to 'dev'.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
All low/medium (no brick risk found — the EE record layout, schema-1
round-trip, CRC coverage and precedence merge all held up):

1. (medium) idle-timeout mid-edit now abandons like CANCEL. menu_to_L0 (the
   only path that reaches an L3 editor via the 15 s idle check) neither
   restored the pre-edit value nor ran the KID_MATRIX_FREQ setDisplayFreq()
   flush that commit/cancel do — so a scrubbed MATRIX rate could leave the
   date board multiplexing at a stale frequency, and any abandoned edit kept
   its live-preview value until reboot. Now the idle check calls
   menu_cancel_edit() before menu_to_L0 when in L3_EDIT.

2. (low) BALANCE toggle no longer collapses a config manual strength. ON now
   enables AUTO only where a sub-system is OFF (both s_bal and the boot
   OVR_S), so seg_balance=150 / colon_balance=128 from config.txt survive a
   BALANCE=on override instead of being silently overwritten with AUTO(1).

3. (low) matrix_freq_hz is seeded from the real TIM1->ARR right after
   MX_TIM1_Init, so g_matrix can't report a rate the hardware isn't running
   (and a no-edit SAVE can't persist a wrong baseline) if config.txt omits
   MATRIX_FREQUENCY.

Tests: menu_accel_check gains an idle-abandon-reverts case; menu_bal_check
gains manual-strength-preserved-on-boot + off-forces-both cases. All 11 emu
suites + ARM green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…lashes

The date board (mk4-date, STM32L010) had the same staleness as the time
board: its pre-build only did 'rm -f version.o', so on incremental/direct
builds __DATE__/__TIME__ went stale and a rebuilt fwd.bin was byte-identical
-> the time board's CRC compare treated it as 'already loaded' and skipped
the reflash. (This is why the date board didn't flash even though the time
board did — its image hadn't changed.)

Same fix as mk4-time: gen-build-id.sh pre-build hook (wired from both
.cproject prebuildSteps + the Release makefile) regenerates a gitignored
Core/Src/build_id.h with a fresh epoch and removes version.o every build.
Version string is now 'Version 0.0.2+<epoch>'. Proven: fwd.bin rebuilt ->
new CRC (e6b48928) != the loaded image -> the date-board updater reflashes.
__has_include fallback to 'dev' for bare checkouts.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…anner

On real hardware, entering a section showed the section name as a one-shot
breadcrumb (e.g. ENTER ASTRO -> 'ASTRO'), which looked like nothing had
happened ('just in astro again'). Worse, an EDIT chord under the banner ran
menu_enter_edit() on the section's first item WITHOUT ever showing it — so
you'd edit SUN blind and toggling it on enabled SUN unexpectedly.

Removed the banner mechanism entirely (menu_banner + its render/dispatch/
fire_stage branches): ENTER now lands directly on the section's first item,
so you always see what you're on ('SUN off') and EDIT edits the visible
item. Fewer taps, no blind edit, no ambiguous 'same name' screen. Chosen
over keeping the banner + a reveal-before-edit step (user call).

Tests updated: menu_check asserts ENTER lands on BRIGHT (no dismiss tap);
the goto helpers in accel/tc/bal/colon_preview drop their banner-dismiss
tap. All 11 emu suites + ARM green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
MATRIX rendered its value as '20KHZ', but K and Z have no clean 7-segment
glyph so on hardware it read as garbled 'ghz'. Show it in kHz as a bare
number instead: the L2 item reads 'MATRIX 20' (the full label now fits) and
the L3 editor reads '20', stepping by 1 kHz. The stored value stays in Hz
(1000 Hz = 1 kHz step), so setDisplayFreq / config MATRIX_FREQUENCY /
persistence are unchanged — display-only. menu_accel_check guards it.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… fire on release)

The chord was far too unforgiving: a lone button emitted its single press
at btn_debounce = 2 ticks (~40ms at TIM21's 50Hz), so unless both buttons
landed within ~40ms the first registered as a stray single (mode cycle /
scroll) instead of starting the chord.

Defer the single-press decision to chord_grace = 6 ticks (~120ms): if the
other button overlaps within that window (b*_chorded) the pair becomes a
chord and no single is sent. Raising the threshold can't drop quick taps,
because a lone tap released before the window now fires on RELEASE instead
of being lost. Auto-repeat (btn_delay/btn_repeat) and the both-tapped-too-
brief-emits-nothing behaviour are preserved. Not emulated (mk4-date isn't
in the WASM twin) — ARM-built, verify on hardware.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Simple on/off items (modes + BALANCE/TEMPCOMP/PPS/SIG-FADE) now flip AND
record in a single EDIT press at L2 — no editor, no SAVE step. This fixes
the surprise where enabling a mode applied live but silently reverted on
reboot because it was never committed. Values (BRIGHT/PAGE MS/MATRIX) and
enums (COLON/NMEA) keep the editor + SAVE/CANCEL. The last enabled display
mode is still refused (LASt guard).

Because a toggle now has no editor, its state must be readable at L2, so
toggle rows show a compact ON/OF and trim the label if tight (8-char labels
lose their last char, e.g. TEMPCOMP -> 'TEMPCOM ON') rather than dropping
the state entirely as before. Enums still keep the full label. Tests updated
for the one-press flow + the visible-state render.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The two were conflated: the L0 chord's 'RESET' stage actually did NVIC reset
(a reboot), and the real factory reset (menu_reset: wipe the on-device store
-> config.txt defaults) was serial-only.

Now:
 - L0 deep-hold chord relabelled RESET -> REBOOT (unchanged action = reboot).
 - New SYS > RESET item (MIT_ACTION, a new item type): EDIT opens a 'SURE?'
   confirm, SAVE fires the factory reset (menu_reset_pending), CANCEL aborts.
   It is deliberately NOT a one-press toggle. Fires menu_reset_step (erase the
   store + re-read config.txt, no reboot). MIT_ACTION renders label-only at L2
   and 'SURE?' at L3; menu_edit_step no-ops it; menu_commit_edit runs its set
   hook instead of recording.

New menu_reset_check.mjs covers the confirm gate + CANCEL-safe + SAVE-wipes.
All 12 emu suites + ARM green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replace the one-press toggle (EDIT flips in place) with the enter->change->
exit model the user asked for: EDIT opens the item's editor (shows the
current ON/OFF, does NOT flip), a single tap toggles it live, and exiting
saves — the L3 chord for a toggle commits on EITHER release (labelled DONE
instead of SAVE/CANCEL) so there's no separate SAVE step and no
save-vs-cancel decision. Idle still abandons (reverts). Values (BRIGHT/PAGE
MS/MATRIX) + enums (COLON/NMEA) keep SAVE/CANCEL; the SYS>RESET action keeps
SAVE=confirm/CANCEL=abort. L2 still shows the live state (LABEL ON/OF) so you
can glance without entering. Tests reworked for the editor flow.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…or RAM-only?

Settings not surviving reboot points at the override store being RAM-only, not
a commit-timing bug. The store lives in the two flash pages above the app-CRC
region (top-2*PAGE). On RG silicon (1 MB) there is room and ee_avail=1 (persists);
on RC silicon (256 KB) the flash ends exactly at the app boundary 0x08040000, so
ee_avail=0 and every setting is lost on power-off. tempcomp (ee2) shares the same
gate, one page lower, so it is RAM-only too on RC (its real persistence is the
config.txt seed). The emulator always has ee_avail=1, which masked this in tests.

"menu_dump = on" emits one human-readable line:
  # menu store: avail=<0|1> flash=<kb>KB page_a=<hex> gen=<n> ovr=<0|1> -- <verdict>
so the hardware tells us which case it is instead of guessing at the die. Mirrors
the star_dump/tc_dump CDC/BUSY-retry contract; forward-declared, defined below the
ee_* globals.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…gn C)

The on-device settings store (menu overrides ee_* + learned tempcomp model
ee2_*) lived in the top two internal-flash pages. On RC silicon (256 KB) flash
ends exactly at the app-CRC boundary, so ee_avail=0 and EVERY setting was
RAM-only — confirmed on hardware via menu_dump (avail=0 flash=256KB). Fix:
relocate the store into /SETTINGS.BIN on the QSPI FAT16 volume, chosen from a
3-design adversarial review as the only option that changes neither the MSC
geometry nor the bootloader:

- 16 KiB contiguous host-visible file = 4 QSPI sectors: menu A/B + tempcomp
  A/B. Boot resolves file->physical via the read-only FATFS fastseek CLMT
  (single-fragment REQUIRED; fragmented/missing -> RAM-only fallback, warned).
  Record format/CRC16/generation/A-B ping-pong unchanged; slots per page
  32->64 (4 KB sectors). RG silicon keeps the internal-flash store untouched
  (EE_BK_INTERNAL); the QSPI path engages only where internal has no room.
- Firmware writes the file RAW through qspi_drv (new QSPI_Program, 8-byte
  doublewords so CRC still lands last) — never FAT metadata, size, or dirent.
- Safety set from the design review: (1) settings_map_gen bumped by every
  STORAGE_Write_FS; commits re-resolve the mapping when stale, so a host that
  moves/rewrites the file can never cause a blind write (worst-case target was
  fwt.bin/the FAT); (2) content sentinel before erase/program — active page
  must read our magic or 0xFF, else re-init INSIDE the file; (3) fatfs_busy
  across the whole commit (eject-reset atomicity); (4) NOR skip-to-blank
  cursor + first-use erase of non-blank pages (a 0x00-provisioned file would
  otherwise AND every record into permanent CRC failure); (5) commits defer
  while host MSC I/O is in flight; (6) menu_dump now reports backing
  (INT/QSPI/NONE) + the RAM-only reason. Known cost: a rollover erase makes
  the USB drive NOT_READY for <=800 ms once per 64 commits.
- flash.sh provisions SETTINGS.BIN FIRST (contiguity) filled 0xFF; qspi.md
  documents the file contract.
- Emu now models the real medium: NOR AND-program + 4 KB erase + attach/
  fragment/host-write hooks. The old memcpy shim with ee_avail hard-coded 1
  is gone — it let every persistence test pass while hardware lost settings.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ements

From the 24-finding adversarial review. Bugs: (1) releasing an L0 chord on an
unlabeled stage stranded the DATE row on "----" (now every unhandled stage
restores/repaints); (2) a toggle SAVED when released on a stage labeled
"----"/CANCEL (now shallow stages = DONE save, deep stage = CANCEL revert);
(3) CANCEL/idle out of the alt-colon editor leaked colonAltExplicit=1 via the
restore write, defeating the auto-distinguish on the next config load
(snapshot + restore); (4) disabling FW CRC while viewing the CRC-on-date mode
stranded the display on a disabled mode (mirror menuSetMode\x27s advance guard);
(5) a no-op editor visit burned a flash record (skip when value unchanged);
(6) a staggered both-press leaked a nextMode before SETUP fired (snapshot the
mode on an L0 single tap; SETUP within 2.5 s undoes it — the ~120 ms merge
window itself is untouched).

Grammar/labels (7-seg glyph reality: S=5, O=0, I=1, Z=2, no ? glyph):
APPLY/----/CANCEL replaces SAVE/CANCEL adjacency (overshooting a commit can no
longer silently discard the edit); deep hold = CLOCK bail-out from L1/L2;
REBOOT arms only on the SECOND stage cycle (~4.3 s) so an exploratory over-
hold of the menu-entry gesture cannot reset the clock, and the label only
shows once armed; "SURE?" (rendered 5URE-) -> "DELETE ALL"; toggle state "OF"
(0F, reads as the word of) -> full "OFF" both layers; PAGE MS -> PAGE with
seconds in BOTH layers and no unit-S (5.5S read as 5.55); SOLID -> FULL
(50L1D); COLONALT -> ACOLON so the live value fits at L2; PPS OUT -> PPS MSG
(it gates the $PMTXTS sentence, not a pulse output); TC VIEW -> TC DATA.

Tests: stage remap applied across the suites + new regressions for every bug
(L0 unlabeled-stage release, REBOOT two-cycle arming, mode-leak undo, toggle
deep-CANCEL, no-op-visit no-record).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…t DONE flash

New read-only MIT_INFO row type (no editor, never persisted): DIAG "PPS
LOCK"/"HOLD <s>"/"PPS ----" answers "am I disciplined right now?"; ASTRO
"STARS <n>" / "STARS b<n>" (baked fallback tagged) answers "did my catalogue
load?" — the tell that would have caught a failed card a session early. The
chord offers no EDIT on INFO rows. SYS>RESET now flashes "DONE" on completion
(a destructive action no longer returns to an identical screen).

(Split out of f7ba9c4 'opportunity bundle' — menu hunks only; ADEV hunks
live on stack/adev-display, star hunks on stack/star-transit.)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…nt, mechanism A)

The signed-off §7 spec, mechanism A (time-board only — works unchanged on a
stock date board; the per-digit brightness overlay B stays a documented future
layer gated on the next date-board protocol change):

- EDITOR BLINK: a resting L3 editor value blinks at 1 Hz — the digital-watch
  "you are setting this" idiom. It stays SOLID while actively scrubbing (any
  event within 600 ms) so the number never flickers under your thumb, starts
  on the visible phase, and ACTION confirms stay steady. Chord stage labels
  always win.
- IDLE WARNING: the whole row flickers once (300 ms) at T-3 s before the 15 s
  idle-abandon reverts an open edit — "act or lose it". Any press resets both
  behaviours\x27 clocks.
- VBAT -> DIAG: coin-cell health joins the menu as a normal persisted mode
  toggle (was config-only).

menu_check regressions: solid-while-fresh, blank/visible blink phases,
solid-while-scrubbing, T-3 s flicker + restore, abandon still fires, VBAT row.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ig cleanup)

Factory reset now clears EVERYTHING learned: SYS>RESET (and menu_reset) also
fires tc_forget + tc_reset, wiping the retained tempcomp model and the live
learned state; the config.txt re-read then re-seeds from the file's tc_* block
— the clock returns to exactly what the file says, compensation included.
tc_forget_step gains the same stale-mapping (settings_mapping_ok) + eject
(fatfs_busy) guards the other QSPI erasers use, and the internal-flash
unlock is now backing-gated like everywhere else. The ASTRO info row reads
plain "STARS <n>" (0 = nothing loaded) now that provenance is single-source.

Cleanup (docs review findings): golden config.txt's stale "#alt_colon_mode"
comment corrected to the parser's colon_alt_mode spelling; a worked refined-
model example block (real learned values: t0 40, die coverage 26..36) added.

(Split out of dc83c30 — menu/factory-reset/config half; the star-core
no-fallback half lives on stack/star-transit.)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Comprehensive documentation for every feature in PRs mitxela#5-mitxela#10 and the three
follow-ons (ADEV, star transits, the menu), written to slot into the
project's documentation page: all new display modes with exact row renders,
every config.txt key (menu-settable ones marked, precedence explained), every
serial command and emitted sentence field-by-field ($PMTXTS/$PMTXTC/$PMADEV/
$PMHDEV/$PMSTAR), a full user guide for the on-device menu, the SETTINGS.BIN
and STARS.BIN file contracts, and the 0.0.5/0.0.2 version train with the
per-build-tag rationale. Describes the ROLLUP build (the tested union this
tree IS), with a real unit's learned tempcomp model as the worked example.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…rsist

The DIAG toggle armed tc_learn+tc_apply but left tc_persist config/serial-only
— a split that made sense before the settings store existed on 256K silicon
and doesn't now: enabling the compensator from the front panel obviously
means wanting its learned model to survive power-off. Same bundling pattern
as BALANCE (seg+colon). A stored TEMPCOMP override now arms all three on boot,
so existing menu-armed units gain persistence with no user action; a config.txt
that explicitly defines tc_persist claims the bundle key and wins, exactly
like tc_learn/tc_apply. Docs updated.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…om the tree

Fine-tooth-comb chain-audit fixes (all text, no behaviour): docs — TEMPCOMP
menu line now says the learn+apply+persist trio (matching s_tc and its own
config section); star catalogue consistently 93 / HYG v4.1; the three astro
render examples now show the firmware\x27s exact padding ("SET  21.14",
"MOON 4  99", the LON sign-slot note). config.txt — the MODE_STAR and ADEV
example strings now match the real renders ("SIRI  0 45"; no unit-s).
generate-stars.py — CLOCK-drive terminology, v4.1, real star count. main.c —
the last stale "card or baked default" comment corrected. And
mk4-time/.settings/language.settings.xml reverted to upstream\x27s blob: a
machine-specific IDE environment hash has no business in a PR chain.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The exact residue between the pure union of the nine stacked PR tiers and
the emulator-verified + hardware-run rollup tree @ 8a36c89. After the
round-5 re-homes (MODE_TEMPCOMP cadence/enum -> tempcomp tier, segbal fade
mirror -> segment-balance tier, golden-config mode docs -> their feature
tiers, subsolar helper -> sidereal tier) what remains is comment wording,
comment placement and whitespace, plus one default-off config key line:

- evil-merge comment wording from the original merge commits (10c8bf6 /
  15567d1), not cherry-pickable as tier commits: tc_seed trigger comments
  (x3 sites), page_ms comment wording (x4 sites), postConfigCleanup /
  applyColonForMode 'sidereal colon' wording, delayedPostConfigCleanup and
  astro repaint notes, a comment-block move around write_rtc, and two
  whitespace-only line tweaks.
- 16ece45/8515aa7 astro comment refinements (gmst_hours shared-series
  comment, local_solar_time wrap note, LST/solar test-anchor comments +
  one blank line) that postdate the relocated subsolar commit's content.
- config.txt: the significance_fade ladder paragraph incl. the
  'significance_fade = off' key line (default-off; absence is
  behaviourally identical), the mode_tempcomp wording, one trailing-space
  line, one blank line.

No functional code differs: all firmware logic, data tables, binaries and
build inputs are byte-identical before this commit except the lines above.

After this commit the stack tip is BYTE-IDENTICAL to rollup @ 8a36c89 while
the linear ancestry contains exactly the nine tiers' assigned commits.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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