fix: don't let auto-race hijack the menu the instant you land on it - #131
Merged
Conversation
11 tasks
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
force-pushed
the
claude/auto-race-menu-grace
branch
from
August 5, 2026 00:24
d78f8f3 to
a3282af
Compare
Coverage — host-testable units📂 Overall coverage
📄 File coverage
|
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.
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 andautoRaceModeCheck()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:mainMenuEnteredAtMs, set inswitchToDisplayPage()), andlastPressedvalues.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
How it was verified
GPS_SPEED— race mode). Verified by removing the guard, running, and restoring it.clang-tidyclean — not run locally (CI covers it)arduino-clihereChecklist
CHANGELOG.mdupdated under[Unreleased]CLAUDE.mdupdated — auto-race description + the new constanttests/— see the note belowNotes 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
BETAis 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 othercurrentPage =assignment in the tree and they are all race-page rotation clamps (GPS_SPEED,TACHOMETER,LOGGING_STOP - 1, …) orPAGE_BOOT. None targetsPAGE_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