Skip to content

fix: say which GPS milestone is outstanding on the status page - #129

Merged
TheAngryRaven merged 1 commit into
BETAfrom
claude/gps-status-time-sync
Aug 4, 2026
Merged

fix: say which GPS milestone is outstanding on the status page#129
TheAngryRaven merged 1 commit into
BETAfrom
claude/gps-status-time-sync

Conversation

@TheAngryRaven

Copy link
Copy Markdown
Owner

Summary

Found while bench-testing: a logger showing Sats:10/12 HDOP:1.5 and FIX (time sync) looked like it had failed to get a lock. It hadn't — it had a perfectly good position fix and was waiting on the clock.

FIX (time sync) parses as a kind of fix — a time-only, position-less one, which is a real and bad u-blox state — when it meant the opposite. Worse, HDOP only renders at all when gpsData.fix is true (display_pages.ino:59), so the screen was already carrying the proof and nobody could see it. The natural response to "it's not locking" is a power cycle, which is the single most harmful thing to do here: it restarts the ~12.5-minute UTC decode being waited on.

The line now reads FIX ok UTC.., and line 3 names the milestone that's actually outstanding rather than leaving the user with nothing to wait for:

State Line 3
No validDate/validTime yet UTC: no date/time
Date+time in, fullyResolved pending UTC: resolving <=12m
Locked Mode:GPS-only (as before)

That middle one is the slow, normal case. timeValid requires validDate && validTime && fullyResolved (gps_functions.ino:210), and fullyResolved needs the UTC/leap-second parameters decoded out of the GPS navigation message — subframe 4 page 18, which repeats every ~12.5 minutes. A clean 3D fix in under a minute followed by several more minutes of waiting is expected, not a fault, and weak signal makes it worse: tracking a satellite well enough to range off it is a far lower bar than decoding its data bits cleanly. Cold starts are the common case here too, since the SAM-M10Q has no flash and sleep is System OFF (gps_functions.ino:284-288).

The <=12m is a bound, not an estimate — most of the time it resolves much faster. Showing a number at all is what stops the power-cycling.

Nothing was lost. The constellation readout keeps that line once the clock is locked, so the diagnostic only occupies space while there's something to diagnose — and Mode:GPS-only was a hardcoded literal that couldn't be wrong, so trading it for live state while waiting is a strict upgrade.

Type of change

  • Bug fix (no user-visible behavior change beyond the fix)
  • New feature / behavior
  • Refactor / Tests only / CI / docs
  • Breaking change

Display-only. No timing, logging, or protocol behaviour changes — timeValid is still exactly validDate && validTime && fullyResolved, now derived from the two halves rather than computed separately so they can't drift.

How it was verified

  • Host unit tests pass — 302 cases / 4277 assertions, 5 new for timeSyncState
  • Simulator: all 6 ctest targets pass, including both lap oracles
  • Golden fixture updated and the rendered PNG eyeballed for the 21-char panel width
  • clang-tidy clean — not run locally (CI covers it)
  • Compiles for the XIAO nRF52840 Sense — not verified locally, no arduino-cli here
  • Tested on real hardware — not yet; this is the fix for the confusion that prompted it, so it wants a cold start under open sky to see UTC: resolving <=12m in the wild

Checklist

  • CHANGELOG.md updated under [Unreleased]
  • CLAUDE.md updated — file map entry + the ~12.5 min constant
  • New testable logic has a matching test in tests/
  • Branch is focused — one commit, display + the state it needs

Notes for reviewers

Split out of #128 (on-device course creator) so it can merge on its own — it's independent of that work and useful immediately, and #128 is still unverified on hardware. #128 currently also carries this commit; once this merges I'll rebase it out so its diff is single-concern again.

The classification is a pure gps_status_page::timeSyncState() rather than branching in the renderer. One of its tests asserts kLocked agrees with timeValid across all four input combinations — a page claiming a lock while log-file creation still waits would be the same confusion wearing a different hat.

gpsData gains timeDateValid + timeResolved, with timeValid now derived from them. Two extra bools; no behaviour change for any existing reader.

One gap I'll name rather than paper over: there's no golden fixture for the resolving state specifically — the one that actually caused this. The sim's SimPvt.fix = 1 sets every validity bit at once, so producing "fix but not fully resolved" would mean extending the sim's PVT injection contract, which is documented as the WASM API v1. Not worth that surface for a three-arm switch whose classifier is exhaustively unit-tested, but nobody should assume that pixel path is locked.

Related issues

Follows the diagnosis in the bench session that produced #128.


Generated by Claude Code

"FIX (time sync)" parses as a KIND of fix — a time-only, position-less
one — when it meant the opposite: the position fix is good and the clock
isn't ready yet. So a healthy device looked broken. That cost a bench
session today, and the instinctive response to it, a power cycle, is
actively harmful: it restarts the ~12.5-minute UTC decode being waited
on.

The line now reads "FIX ok  UTC..", and line 3 names the outstanding
milestone instead of leaving the user with nothing to wait for:
"UTC: no date/time", then "UTC: resolving <=12m" once date and time are
valid but fullyResolved is not. That second state is the slow, normal
one — the leap-second parameters live in nav-message subframe 4 page 18,
which repeats every ~12.5 minutes, so a clean 3D fix minutes ahead of
timeValid is expected rather than a fault. The bound is worst case, not
an estimate; seeing a number at all is what stops the power-cycling.

Nothing is lost: the constellation readout keeps that line once the
clock is locked, so the diagnostic only takes the space while there is
something to diagnose.

gpsData gains the two halves of timeValid so the page can tell them
apart, and the three-way classification is a pure timeSyncState() in
gps_status_page rather than branching in the renderer. One of its tests
asserts kLocked agrees with timeValid across all four input
combinations — a page claiming a lock while logging still waits would be
the same confusion wearing a different hat.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ESkRRtF4vRrANPL6huSgmD
@github-actions

github-actions Bot commented Aug 4, 2026

Copy link
Copy Markdown

Coverage — host-testable units

📂 Overall coverage

Metric Coverage
Lines 🟢 1166/1188 (98.1%)
Functions 🟢 123/123 (100.0%)
Branches 🟡 862/966 (89.2%)

📄 File coverage

File Lines Functions Branches
BirdsEye/camera_fsm.cpp 🟢 222/230 (96.5%) 🟢 20/20 (100.0%) 🟡 126/144 (87.5%)
BirdsEye/course_creator.cpp 🟢 209/217 (96.3%) 🟢 21/21 (100.0%) 🟡 114/131 (87.0%)
BirdsEye/crc32.cpp 🟢 30/30 (100.0%) 🟢 4/4 (100.0%) 🟢 24/24 (100.0%)
BirdsEye/crossing_pattern.cpp 🟢 15/15 (100.0%) 🟢 1/1 (100.0%) 🟢 12/12 (100.0%)
BirdsEye/dovex_header.cpp 🟢 106/107 (99.1%) 🟢 7/7 (100.0%) 🔴 62/88 (70.5%)
BirdsEye/filename_validator.cpp 🟢 14/14 (100.0%) 🟢 1/1 (100.0%) 🟢 30/30 (100.0%)
BirdsEye/gps_stats.cpp 🟢 25/25 (100.0%) 🟢 3/3 (100.0%) 🟢 8/8 (100.0%)
BirdsEye/gps_status_page.cpp 🟢 29/29 (100.0%) 🟢 4/4 (100.0%) 🟢 28/28 (100.0%)
BirdsEye/gps_time.cpp 🟢 45/45 (100.0%) 🟢 6/6 (100.0%) 🟢 30/32 (93.8%)
BirdsEye/gps_validation.cpp 🟢 24/24 (100.0%) 🟢 2/2 (100.0%) 🟢 66/66 (100.0%)
BirdsEye/haversine.cpp 🟢 8/8 (100.0%) 🟢 1/1 (100.0%) ⚫ 0/0 (0.0%)
BirdsEye/insta360_protocol.cpp 🟢 140/140 (100.0%) 🟢 16/16 (100.0%) 🟡 86/98 (87.8%)
BirdsEye/lap_format.cpp 🟢 18/18 (100.0%) 🟢 1/1 (100.0%) 🟢 9/9 (100.0%)
BirdsEye/sat_bars.cpp 🟢 33/33 (100.0%) 🟢 2/2 (100.0%) 🟢 51/54 (94.4%)
BirdsEye/sd_access_policy.cpp 🟢 9/9 (100.0%) 🟢 3/3 (100.0%) 🟢 18/18 (100.0%)
BirdsEye/sd_format_page.cpp 🟢 25/25 (100.0%) 🟢 3/3 (100.0%) 🟢 25/26 (96.2%)
BirdsEye/sensoregg_protocol.cpp 🟢 44/45 (97.8%) 🟢 7/7 (100.0%) 🟢 33/34 (97.1%)
BirdsEye/sprint_select.cpp 🟢 25/25 (100.0%) 🟢 4/4 (100.0%) 🟢 46/48 (95.8%)
BirdsEye/tach_filter.cpp 🟢 15/15 (100.0%) 🟢 3/3 (100.0%) 🟡 7/8 (87.5%)
BirdsEye/track_json.cpp 🟢 116/120 (96.7%) 🟢 12/12 (100.0%) 🟡 67/88 (76.1%)
BirdsEye/wake_cause.cpp 🟢 14/14 (100.0%) 🟢 2/2 (100.0%) 🟢 20/20 (100.0%)

@TheAngryRaven
TheAngryRaven merged commit 037a692 into BETA Aug 4, 2026
8 checks passed
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.

2 participants