Skip to content

fix: don't let auto-race hijack the menu the instant you land on it - #131

Merged
TheAngryRaven merged 1 commit into
BETAfrom
claude/auto-race-menu-grace
Aug 5, 2026
Merged

fix: don't let auto-race hijack the menu the instant you land on it#131
TheAngryRaven merged 1 commit into
BETAfrom
claude/auto-race-menu-grace

Conversation

@TheAngryRaven

Copy link
Copy Markdown
Owner

Summary

Found on the bench: exiting a page while riding dropped straight into race mode.

autoRaceModeCheck() guarded only on "are we on the main menu", with no notion of when we got there. displayLoop() switches the page at the end of one loop iteration and autoRaceModeCheck() runs at the top of the next — so above the 10 mph / 500 RPM trigger, a deliberate "leave this page" became "start racing" about four milliseconds later. The menu is never drawn; from the driver's seat the device just does something on its own.

This has always been true of every page. It surfaced now because the on-device course creator (#128) is the first screen you use out on the course with the vehicle possibly rolling — and its save path lands on the menu too, so finishing a walked course could immediately start a session. The fix is general and independent of that work, which is why it's split out here.

Auto-race now requires the menu to have been settled for AUTO_RACE_MENU_GRACE_MS (3 s), anchored on the newest of:

  • the menu-arrival stamp (mainMenuEnteredAtMs, set in switchToDisplayPage()), and
  • the three button lastPressed values.

Using the button stamps as well as arrival means actively navigating the menu at speed defers it too — the same "a human is driving the UI, not the vehicle" signal. Those stamps persist across iterations (the menu-idle block already relies on this), so the guard doesn't care where in loop() it runs.

The normal auto-race path is untouched: a device parked on the menu has been quiet for minutes before anyone drives off. The cost is up to three seconds of a session that begins by leaving a menu, and only when the user was pressing buttons moments earlier.

Type of change

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

How it was verified

  • Host unit tests pass — 297 cases / 4269 assertions
  • Simulator: all 6 ctest targets pass
  • The regression test was proven to bite. The golden walk now rolls at 15 mph before exiting the Bluetooth page and asserts it lands on the main menu. With the guard removed the fixture fails exactly as reported, and the firmware logs the culprit:
    CourseManager created (Lap Anything)
    FIXTURE main_menu_exit_while_moving: expected page -1, got 5
    
    (page 5 is GPS_SPEED — race mode). Verified by removing the guard, running, and restoring it.
  • 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 a bug found on hardware

Checklist

  • CHANGELOG.md updated under [Unreleased]
  • CLAUDE.md updated — auto-race description + the new constant
  • New testable logic has a matching test in tests/ — see the note below
  • Branch is focused — one commit

Notes for reviewers

The regression test deliberately uses the Bluetooth page, not the course creator. I originally wrote it inside the creator walk on #128, but exercising it through a page that already exists on BETA is the better test: it proves the bug is general to every page exit, not something the creator introduced. That's also what makes this PR standalone.

switchToDisplayPage() is a safe place to stamp arrival — I checked every other currentPage = assignment in the tree and they are all race-page rotation clamps (GPS_SPEED, TACHOMETER, LOGGING_STOP - 1, …) or PAGE_BOOT. None targets PAGE_MAIN_MENU, so there is no path to the menu that skips the stamp.

No new pure unit. The guard is a single wrap-safe elapsed comparison over live button/GPS state; extracting it would be more indirection than logic. The behaviour is covered by the sim fixture instead, which exercises the real loop() ordering that caused the bug — the part a unit test of the comparison would have missed entirely.

Three seconds matches the other deliberate-hold windows in the codebase (status-page auto-close, format confirm), rather than being a fresh magic number.

Related issues

Split out of #128 (on-device course creator), where the bug was found. #128 carries the same commit for now; once this merges I'll rebase it out so its diff stays single-concern.


Generated by Claude Code

Reported from the bench: exiting the course creator dropped straight into
race mode. The reporter's own guess was right — they were on a bike,
above the 10 mph auto-race trigger, when they hit Cancel.

autoRaceModeCheck() guarded only on "are we on the main menu", with no
notion of WHEN we got there. displayLoop() switches the page at the end
of one loop iteration and autoRaceModeCheck() runs at the top of the
next, so a deliberate exit became "start racing" about four milliseconds
later. The menu is never drawn; the device appears to act on its own.

This was always true for every page, but the creator is what made it
reachable: it is the one screen you use out on the course, on a vehicle
that may well be rolling, and the save path lands on the menu too — so
finishing a walked course could immediately start a session.

Auto-race now requires the menu to have been settled for
AUTO_RACE_MENU_GRACE_MS, anchored on the newest of the menu-arrival stamp
and the three button lastPressed values. Using the button stamps as well
as arrival means actively navigating the menu at speed defers it too,
which is the same "a human is driving the UI, not the vehicle" signal.
They persist across iterations (the menu-idle block relies on this
already), so the guard doesn't care where in loop() it runs.

The normal auto-race path is untouched: a device parked on the menu has
been quiet for minutes before anyone drives off. The cost is up to three
seconds of a session that starts by leaving a menu — and only when the
user was pressing buttons moments earlier.

switchToDisplayPage() is a safe place to stamp arrival: the direct
`currentPage =` assignments elsewhere are all race-page rotation clamps,
never the menu.

The golden walk now exits the creator at 15 mph, which reproduces the
bug exactly — with the guard removed the fixture fails "expected page
-1, got 5" as the firmware logs "Auto-entering race mode". Verified by
removing it. It coasts back to 0 mph afterwards, since gpsData holds its
last value between PVTs and a latched 15 mph would trip auto-race once
the window expired.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ESkRRtF4vRrANPL6huSgmD
@TheAngryRaven
TheAngryRaven force-pushed the claude/auto-race-menu-grace branch from d78f8f3 to a3282af Compare August 5, 2026 00:24
@github-actions

github-actions Bot commented Aug 5, 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 dd277e2 into BETA Aug 5, 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