Skip to content

Fix code-review findings C1–C3, H1, H2: line init, midnight rollover, detection window, input validation, buffer wraparound#41

Merged
TheAngryRaven merged 1 commit into
BETAfrom
claude/code-review-fixes-w94j17
Jun 11, 2026
Merged

Fix code-review findings C1–C3, H1, H2: line init, midnight rollover, detection window, input validation, buffer wraparound#41
TheAngryRaven merged 1 commit into
BETAfrom
claude/code-review-fixes-w94j17

Conversation

@TheAngryRaven

Copy link
Copy Markdown
Owner

Addresses all five findings from the professional code review (scored 4.5/10). Each fix lands with regression tests; the C3 and H2 tests were verified to fail against the pre-fix code. All 89 host tests green, and the pinned NMEA replay goldens are unchanged (no timing drift on real-track data).

C1 — Start/finish line never initialized

  • All 12 line-coordinate members are now zero-initialized in the header.
  • New startFinishLineConfigured flag (mirroring the sector flags) gates checkStartFinish() in loop() — calling loop() before setStartFinishLine() is now a safe no-op instead of UB on indeterminate doubles.
  • All three line setters reject degenerate (A == B, e.g. the 0.00 placeholders shipped in basic_oled_example) or non-finite lines, with a debug warning.
  • New public getter: isStartFinishLineConfigured().

C2 — Midnight rollover / backwards GPS time

  • New shared timeSinceMidnightDelta() helper normalizes every duration across the 86,399,999 → 0 UTC midnight wrap. Applied at all subtraction sites in DovesLapTimer (lap, all three sectors, getCurrentLapTime, getPaceDifference) and WaypointLapTimer.
  • The crossing interpolator computes its fix-pair delta in double, wrap-normalizes it, and clamps it to CROSSING_MAX_FIX_GAP_MS (10 s) before any conversion back to unsigned long — the old path underflowed unsigned subtraction and pushed an out-of-range double through double→unsigned conversion ([conv.fpint] UB).
  • Crossing validity is now an explicit bool through interpolateCrossingPoint() / _detectLineCrossing() instead of the time != 0 sentinel, so a legitimate crossing at exactly 00:00:00.000 is no longer discarded.
  • Documented limitation (CLAUDE.md): a small backwards time step that isn't a midnight wrap is indistinguishable from one; the interpolator clamp catches it inside a crossing zone, lap-level deltas do not.

C3 — Course detection false-matched longer layouts

  • _checkWaypointProximity() now re-anchors _waypointOdometer after a completed proximity pass with no match, so distanceSinceWaypoint can't accumulate across laps and falsely match a 2×-length layout on the next pass. Same root-cause class as the v4.1.0 rejection-cooldown fix (CLAUDE.md issue little review with opus #13); regression test mirrors that one.

H1 — GPS input validation

  • New GeoMath.h helpers geoIsFinite() / geoCoordinatesValid() (no isnan/isinf dependency — portable across Arduino cores and host).
  • DovesLapTimer::loop(), WaypointLapTimer::loop(), and CourseDetector::update() reject NaN/Inf, |lat|>90, |lng|>180, and exact (0,0) fixes; non-finite altitude/speed are sanitized rather than dropping the fix.
  • Teleport rejection: a single-fix jump > GPS_MAX_PLAUSIBLE_JUMP_METERS (500 m) is dropped; after GPS_JUMP_REACCEPT_COUNT (3) consecutive far fixes the new position is accepted as a genuine relocation and re-seeded without crediting the gap to the odometer.

H2 — Crossing-buffer wraparound

  • interpolateCrossingPoint() unwinds the ring chronologically (oldest → newest) before scanning for the straddling pair. Once the ring wrapped (kart parked in the zone: standing start, red flag, stop-and-go), physical index order ≠ chronological order and the seam pair (newest adjacent to oldest) masqueraded as the crossing — fabricating a crossing from points minutes apart or invalidating the real one. Verified failing pre-fix: the park-cross-park scenario dropped the crossing entirely.

Tests

  • test_midnight_rollover.cpp (7): lap/sector/current-lap/crossing-zone timing across the wrap, WaypointLapTimer across the wrap, delta-helper units.
  • test_input_validation.cpp (11): adversarial fixes, teleport/relocation, unconfigured + degenerate line safety, detector/WLT guards.
  • test_buffer_wraparound.cpp (3): park-cross-park, standing start on the grid, normal lap after a wrapped start.
  • test_course_detector.cpp (+2): no-match re-anchor + post-re-anchor genuine match.

CHANGELOG, README, CLAUDE.md, keywords.txt, and test/README.md updated accordingly.

https://claude.ai/code/session_01XNmVH6ySJ6Dz2QKDjB69mE


Generated by Claude Code

…n window, input validation, buffer wraparound

Addresses the C1/C2/C3/H1/H2 findings from the professional code review:

- C1: zero-initialize all crossing-line members, gate checkStartFinish()
  on a new startFinishLineConfigured flag (exposed via
  isStartFinishLineConfigured()), and reject degenerate/non-finite lines
  in all three line setters. loop() before setStartFinishLine() was
  reading indeterminate doubles (UB, phantom crossings).

- C2: normalize every duration across the UTC midnight wrap via a shared
  timeSinceMidnightDelta() helper (DovesLapTimer + WaypointLapTimer).
  The crossing interpolator now computes its fix-pair delta in double,
  wrap-normalized and clamped to CROSSING_MAX_FIX_GAP_MS (10s), before
  converting back to unsigned long - the old path underflowed unsigned
  subtraction and pushed an out-of-range double through double->unsigned
  conversion (UB). Crossing validity is now a bool return instead of the
  time != 0 sentinel, so a legitimate crossing at exactly 00:00:00.000
  is no longer discarded.

- C3: CourseDetector re-anchors the lap-distance window after every
  completed proximity pass, match or no match. Previously a no-match lap
  left distanceSinceWaypoint accumulating, falsely matching a 2x-length
  layout on the next pass (same root cause as the v4.1.0 rejection-
  cooldown fix, CLAUDE.md issue 13).

- H1: validate GPS input in DovesLapTimer::loop(), WaypointLapTimer::loop()
  and CourseDetector::update(): reject NaN/Inf/out-of-range/(0,0) fixes,
  sanitize non-finite alt/speed, drop single-fix teleports >500m, and
  re-accept sustained relocations after 3 consecutive far fixes without
  crediting the gap to the odometer. A single NaN or (0,0) fix previously
  poisoned odometers for the whole session and hung course detection.

- H2: unwind the crossing ring buffer chronologically before scanning for
  the straddling pair. Once the ring wrapped (kart parked in the zone -
  standing start, red flag), the seam pair (newest entry adjacent to
  oldest) masqueraded as the crossing, fabricating or invalidating laps.

Adds 23 new host unit tests in three new suites (test_midnight_rollover,
test_input_validation, test_buffer_wraparound) plus two CourseDetector
re-anchor regression tests; the C3 and H2 tests were verified to fail
against the pre-fix code. All 89 tests green, NMEA replay goldens
unchanged.

https://claude.ai/code/session_01XNmVH6ySJ6Dz2QKDjB69mE
@github-actions

Copy link
Copy Markdown

GCC Code Coverage Report

📂 Overall coverage

Metric Coverage
Lines 🔴 833/1242 (67.1%)
Functions 🔴 127/195 (65.1%)
Branches 🔴 410/776 (52.8%)

📄 File coverage

File Lines Functions Branches
src/CourseDetector.cpp 🟢 94/100 (94.0%) 🟡 17/19 (89.5%) 🟡 39/46 (84.8%)
src/CourseManager.cpp 🔴 0/142 (0.0%) 🔴 0/22 (0.0%) 🔴 0/78 (0.0%)
src/CourseManager.h 🔴 0/24 (0.0%) 🔴 0/8 (0.0%) 🔴 0/16 (0.0%)
src/DovesLapTimer.cpp 🟡 426/545 (78.2%) 🟡 43/57 (75.4%) 🔴 268/440 (60.9%)
src/DovesLapTimer.h 🔴 123/187 (65.8%) 🟡 41/48 (85.4%) 🔴 38/88 (43.2%)
src/GeoMath.h 🟢 21/21 (100.0%) 🟢 4/4 (100.0%) 🟡 21/24 (87.5%)
src/WaypointLapTimer.cpp 🔴 148/199 (74.4%) 🔴 15/29 (51.7%) 🔴 37/68 (54.4%)
src/WaypointLapTimer.h 🟡 21/24 (87.5%) 🟡 7/8 (87.5%) 🔴 7/16 (43.8%)

@TheAngryRaven TheAngryRaven merged commit 7910b00 into BETA Jun 11, 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