Skip to content

Code-review fixes for #60: USB MSC hardening + low-severity cleanups#69

Merged
TheAngryRaven merged 8 commits into
BETAfrom
claude/pr-60-review-fixes
Jun 18, 2026
Merged

Code-review fixes for #60: USB MSC hardening + low-severity cleanups#69
TheAngryRaven merged 8 commits into
BETAfrom
claude/pr-60-review-fixes

Conversation

@TheAngryRaven

Copy link
Copy Markdown
Owner

Multi-agent code review of #60 surfaced two CRITICAL structural defects in the newly-added USB Mass Storage feature plus a handful of lower-severity issues. This branch fixes them, one commit per fix for auditability. All 121 host tests pass and the test build is clean.

Fixes (severity order)

Severity Fix
🔴 CRITICAL Park the main loop while USB MSC is active. While the drive was mounted, loop() kept running GPS_LOOP(), trackDetectionLoop(), checkAutoIdle(), etc., so the firmware and the host PC could both drive the FAT — corruption was prevented only as a side effect of the SD-access mutex denying those acquires, not by design. loop() now early-returns on usbMscActive (mirroring the bleActive branch), servicing only the Exit button and the status page.
🔴 CRITICAL Exit USB mode when the cable is unplugged. The only exit was the on-device Exit button; pulling the cable left the device stuck usbMscActive, holding SD_ACCESS_USB_MSC and the EMI-unsafe 8 MHz SD clock forever — a subsequent drive would then be silently denied logging. The parked branch now watches VBUS and reboots out of USB mode on disconnect.
🟠 HIGH Check usb_msc.begin() result + lengthen re-enumeration delay. begin()'s bool was ignored, so a failed MSC registration still flipped usbMscActive and re-enumerated, stranding the user on a "drive active" screen with no drive. Now bails cleanly (restore clock, release lock, return false). Detach delay raised 10 ms → 50 ms (10 ms is below the USB spec's recommended settle window).
🟡 MEDIUM Validate sector alignment + retry in the MSC read/write callbacks. A non-multiple bufsize silently dropped a partial sector via integer division. Now reject zero/unaligned sizes and retry the SD op up to 3× (matching the EMI-hardening retry pattern used elsewhere).
🟡 MEDIUM Flush card + drop media-ready before the exit reboot. USB_MSC_DISABLE() went straight to reset, relying on the host having ejected first; an un-ejected exit could lose a buffered write / leave the FAT inconsistent. Now setUnitReady(false) + syncDevice() before resetting.
🟡 MEDIUM Reject over-length BLE filename commands. The file-request callback truncated commands to 64 bytes and then validated/path-built the mangled name. Raw-data paths (track upload, OTA chunks) already return earlier, so a len >= 65 command at dispatch is malformed — now rejected with ERROR.
🟢 LOW Pin kSpace minutes>0 seconds zero-padding with a column-stability test + CHANGELOG note (cosmetic 1: 5.0071:05.007 change from the lap_format refactor).

CHANGELOG.md and CLAUDE.md updated alongside the behavior changes.

Lows deliberately left as-is

  • forceReleaseSDAccess() policy bypass — dead code (zero callers); left the exported API untouched.
  • Tach overflow transient-discard — an accuracy coast (Kalman + 2 s sanity bound absorb it), not corruption; the suggested tightening touches the timing-critical ISR SPSC logic and isn't worth the risk.
  • bleSendFileList lock-hold / TRACK_PARSE-preemptible invariant — accepted by review (recovered by the 1 Hz log-open retry / correct under current synchronous usage).
  • USB revision string "1.0" — can't use FIRMWARE_VERSION ("2.2.3" is 5 chars; the MSC revision field caps at 4), so the current value is correct.

🤖 Generated with Claude Code


Generated by Claude Code

claude added 8 commits June 18, 2026 16:40
While the USB mass-storage drive is mounted the host PC owns the SD card,
but the main loop kept running GPS_LOOP(), trackDetectionLoop(),
checkAutoIdle() etc., so the firmware and host could both drive the FAT.
Corruption was prevented only as a side effect of the SD-access policy
denying those acquires, not by design.

Add a usbMscActive early-return branch in loop() mirroring the existing
bleActive branch: skip all GPS/tach/lap/SD processing, service only the
Exit button (Select) and the status-page refresh, then return. The host
now owns the card uncontested.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gjbi1LXFAFetL5GPohypVu
The only exit from mass-storage mode was the on-device Exit button. Pulling
the USB cable — the natural way to finish a drag-and-drop session — left the
device stuck with usbMscActive true, holding SD_ACCESS_USB_MSC and the
EMI-unsafe 8 MHz SD clock forever. A subsequent driving session would then
be silently denied SD logging with no indication.

The parked USB loop branch now checks isUsbConnected() (VBUS via
NRF_POWER->USBREGSTATUS) each iteration and calls USB_MSC_DISABLE() (reboot)
when the cable is removed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gjbi1LXFAFetL5GPohypVu
USB_MSC_ENABLE() ignored the bool returned by usb_msc.begin(); on a failed
MSC interface registration it still set usbMscActive and re-enumerated,
leaving the user on a "drive active" screen with no drive. Now bail cleanly
on failure: drop media-ready, restore the normal SD clock, release the SD
lock, and return false (the UI already shows the SD-busy warning on false).

Also raise the detach()->attach() re-enumeration delay from 10 ms to 50 ms;
10 ms is below the USB spec's recommended detach settle window and some
hosts would not re-mount the drive reliably.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gjbi1LXFAFetL5GPohypVu
msc_read_cb/msc_write_cb computed the sector count with bufsize / 512 and
trusted the comment that bufsize is always a multiple of 512. A malformed
host transfer with a non-multiple size would silently read/write fewer
sectors than the host believed succeeded. Reject non-aligned/zero sizes
with -1, and retry the SD op up to 3x to match the EMI-hardening retry
pattern used elsewhere instead of aborting the whole transfer on one glitch.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gjbi1LXFAFetL5GPohypVu
USB_MSC_DISABLE() went straight to delay(50) + NVIC_SystemReset(), relying
entirely on the host having ejected first. If the user hit the on-device
Exit (or pulled the cable) without safely ejecting, any buffered write was
lost and the FAT could be left inconsistent across the hard reset. Now drop
setUnitReady(false) and SD.card()->syncDevice() before resetting.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gjbi1LXFAFetL5GPohypVu
The file-request callback copies at most 64 bytes into buffer[65], so a
command longer than that was silently truncated and then run through
filename validation and path building as a mangled-but-plausible name. The
raw-data paths (track upload, OTA image chunks) already return earlier using
data/len directly, so by the command-dispatch point anything len >= 65 is a
malformed command, never a valid (FAT-short) filename command. Reject with
ERROR rather than acting on the truncated string.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gjbi1LXFAFetL5GPohypVu
The lap_format refactor changed the big-font live pages from space-padded
to zero-padded seconds once a lap passes one minute (1:05.007 vs the old
1: 5.007). Same field width, so the column stays stable, but the change was
undocumented. Add an explicit column-stability test across the 1-minute
boundary and call out the cosmetic change in the CHANGELOG.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gjbi1LXFAFetL5GPohypVu
Document the new usbMscActive loop-parking branch, VBUS-unplug exit,
begin()-failure bailout, longer re-enumeration delay, and the
flush/media-ready drop before the exit reboot.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gjbi1LXFAFetL5GPohypVu
@github-actions

Copy link
Copy Markdown

Coverage — host-testable units

📂 Overall coverage

Metric Coverage
Lines 🟢 259/260 (99.6%)
Functions 🟢 26/26 (100.0%)
Branches 🟡 237/271 (87.5%)

📄 File coverage

File Lines Functions Branches
BirdsEye/crc32.cpp 🟢 30/30 (100.0%) 🟢 4/4 (100.0%) 🟢 24/24 (100.0%)
BirdsEye/dovex_header.cpp 🟢 100/101 (99.0%) 🟢 6/6 (100.0%) 🔴 61/92 (66.3%)
BirdsEye/filename_validator.cpp 🟢 14/14 (100.0%) 🟢 1/1 (100.0%) 🟢 30/30 (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/lap_format.cpp 🟢 18/18 (100.0%) 🟢 1/1 (100.0%) 🟢 9/9 (100.0%)
BirdsEye/sd_access_policy.cpp 🟢 5/5 (100.0%) 🟢 2/2 (100.0%) 🟢 10/10 (100.0%)
BirdsEye/tach_filter.cpp 🟢 15/15 (100.0%) 🟢 3/3 (100.0%) 🟡 7/8 (87.5%)

@TheAngryRaven TheAngryRaven merged commit 07dabca into BETA Jun 18, 2026
6 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