Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Keep GitHub Actions dependencies monitored. All workflow actions are
# pinned to commit SHAs (supply-chain hardening); Dependabot proposes PRs
# that bump those SHAs when new releases land, so pins don't rot.
version: 2
updates:
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
groups:
actions:
patterns:
- "*"
6 changes: 3 additions & 3 deletions .github/workflows/arduino-lint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,10 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4

- name: Arduino Lint
uses: arduino/arduino-lint-action@v2
uses: arduino/arduino-lint-action@4de5fc895deecbda2f6b1cc05de7a8fdd5c4c51f # v2.0.0
with:
project-type: library
compliance: specification
Expand All @@ -26,7 +26,7 @@ jobs:

- name: Upload report
if: always()
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: arduino-lint-report
path: arduino-lint-report.json
Expand Down
6 changes: 3 additions & 3 deletions .github/workflows/compile-examples.yml
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ jobs:

steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4

# Seeed nRF52 BSP shells out to adafruit-nrfutil during link to package
# the .hex into a .zip, even when we're only compiling — so install it
Expand All @@ -71,7 +71,7 @@ jobs:
run: pip install --user adafruit-nrfutil

- name: Compile sketches
uses: arduino/compile-sketches@v1
uses: arduino/compile-sketches@8ac27e99289705c4abec832089575d687b859227 # v1.1.2
with:
fqbn: ${{ matrix.board.fqbn }}
platforms: ${{ matrix.board.platforms }}
Expand All @@ -88,7 +88,7 @@ jobs:

- name: Upload size deltas report
if: always()
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: size-deltas-${{ matrix.board.alias }}
path: sketches-reports
Expand Down
14 changes: 7 additions & 7 deletions .github/workflows/coverage.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ jobs:
name: Line coverage (gcovr)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4

- name: Install g++ + gcovr
run: |
Expand All @@ -27,7 +27,7 @@ jobs:

- name: Run coverage with gate
# Fails the job if line coverage drops below COVERAGE_GATE (see
# test/Makefile). Gate starts at 1%bump it as coverage grows.
# test/Makefile, currently 80%)keep ratcheting it up with new tests.
working-directory: test
run: make coverage

Expand All @@ -39,7 +39,7 @@ jobs:

- name: Post coverage summary comment
if: github.event_name == 'pull_request'
uses: actions/github-script@v7
uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7
with:
script: |
const fs = require('fs');
Expand Down Expand Up @@ -69,19 +69,19 @@ jobs:

# ---- Badge publish (master only) ----
- name: Generate badge JSON
if: github.event_name == 'push' || github.event_name == 'workflow_dispatch'
if: github.ref == 'refs/heads/master' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch')
working-directory: test
run: make coverage-badge

- name: Stage badge for publish
if: github.event_name == 'push' || github.event_name == 'workflow_dispatch'
if: github.ref == 'refs/heads/master' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch')
run: |
mkdir -p badge-publish
cp test/coverage-badge.json badge-publish/

- name: Publish badge to the `badges` branch
if: github.event_name == 'push' || github.event_name == 'workflow_dispatch'
uses: peaceiris/actions-gh-pages@v3
if: github.ref == 'refs/heads/master' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch')
uses: peaceiris/actions-gh-pages@84c30a85c19949d7eee79c4ff27748b70285e453 # v4.0.0
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
publish_branch: badges
Expand Down
6 changes: 3 additions & 3 deletions .github/workflows/docs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ jobs:
name: Doxygen build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4

- name: Install Doxygen + Graphviz
run: |
Expand All @@ -32,8 +32,8 @@ jobs:
run: doxygen Doxyfile

- name: Deploy to gh-pages
if: github.event_name == 'push' || github.event_name == 'workflow_dispatch'
uses: peaceiris/actions-gh-pages@v3
if: github.ref == 'refs/heads/master' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch')
uses: peaceiris/actions-gh-pages@84c30a85c19949d7eee79c4ff27748b70285e453 # v4.0.0
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
publish_dir: ./docs-build/html
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/unit-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ jobs:
name: Host-native unit tests (g++)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4

- name: Show compiler version
run: g++ --version
Expand Down
127 changes: 127 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,20 +7,147 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]

### Fixed
- **Low-rate GPS silently killed all lap counting** — crossing validation
required the straddle pair's summed distance to the line to be under
`crossingThresholdMeters` in absolute meters, conflating zone size with
sample density: at 1 Hz / 70 km/h fixes are ~19 m apart, every genuine
crossing failed the check, and the lap counter never moved (the failure
went only to debug serial). Validation now scales with the pair's own
spacing (`CROSSING_PAIR_SPACING_FACTOR`), the zone-exiting fix is included
in the buffer so a straddle pair exists at low rates at all, a geometric
backstop requires the interpolated point to land on the line *segment*
(not its infinite extension), and rejected crossings are surfaced through
the new `getRejectedCrossingCount()` getter.
- **Lap-Anything fallback was unreachable from the most likely failure
mode** — the fallback only fired after candidate *rejections*, but a
wrong/missing course config never produces candidates, leaving detection
(and `getActiveTimer()`) hung forever while the WaypointLapTimer silently
timed laps unseen. `CourseDetector` now counts completed no-match passes
(`getNoMatchCount()`), and `CourseManager` falls back after
`COURSE_DETECT_MAX_NO_MATCH_PASSES` of them — plus a distance failsafe
(`COURSE_DETECT_FALLBACK_DISTANCE_FACTOR` × longest course, floored at
`COURSE_DETECT_FALLBACK_MIN_METERS`) for the case where the waypoint is
never revisited at all.
- **Lap-Anything mode left all 8 course timers running forever** —
`_activateLapAnything()` now deactivates every course entry, so the
full crossing pipelines stop being fed on every fix once the fallback is
active (previously `pruneInactiveCourses()` couldn't help: it
early-returns precisely in that no-active-course state).
- **README "Direction Detection" described the algorithm removed in
2026-05-20** ("first sector line crossed determines direction"); rewritten
to match the actual both-sectors temporal-order resolution. Two unit lies
fixed with it: `getPaceDifference()` returns **ms per meter** (README said
"seconds", the header said "milliseconds"), and `pointLineSegmentDistance()`
returns **meters** (header claimed degrees). A doc-claim spot check was
added to the release checklist in `CONTRIBUTING.md`.
- **Test Makefile tracked no header dependencies** — `make run` gave false
greens on binaries stale against `../src/*.h`, `replay_runner.h`, or the
NMEA fixtures (`touch ../src/DovesLapTimer.h && make` → "Nothing to be
done"). All headers are now prerequisites of every test binary.
- **`M_PI`/unit-conversion literal drift** — speed/distance conversions used
three different hand-typed literals across five files (`1.852`,
`0.621371`, `1.60934` — the latter two disagree at the 7th digit). All
conversions now use shared `constexpr` constants in `GeoMath.h`
(`GEOMATH_KNOTS_TO_KMH`, `GEOMATH_KMH_TO_MPH`, `GEOMATH_MPH_TO_KMH`,
`GEOMATH_METERS_TO_FEET`), derived from the exact definitions of the
nautical and statute mile.
- **UTC midnight rollover corrupted every time computation** — the GPS time
base (ms since midnight) wraps 86,399,999 → 0 at UTC midnight, and every
duration was a naked unsigned subtraction. A lap (or sector, or current-lap
display) straddling midnight produced a ~4.29-billion-ms time that stuck as
`bestLapTime`; a crossing zone straddling midnight pushed an out-of-range
double through a double→unsigned conversion (undefined behavior). All
duration sites in `DovesLapTimer` and `WaypointLapTimer` now normalize via
a shared `timeSinceMidnightDelta()` helper, the crossing interpolator
computes its delta in `double` with wrap normalization and a 10s coherence
clamp (`CROSSING_MAX_FIX_GAP_MS`), and a legitimate crossing at exactly
00:00:00.000 is no longer discarded by the old `time != 0` validity sentinel.
- **Uninitialized start/finish line read by `loop()`** — the four
`startFinishPoint*` doubles had no initializers and no configured guard, so
calling `loop()` before `setStartFinishLine()` fed indeterminate memory into
`haversine()` (undefined behavior, potential phantom crossings). All line
members are now zero-initialized and `loop()` skips start/finish detection
until a valid line is set. Line setters also reject degenerate (zero-length,
e.g. the example sketch's `0.00` placeholders) or non-finite lines.
- **Crossing-buffer wraparound corrupted the interpolator** — a kart parked
inside the crossing zone (standing start on the grid, red flag, stop-and-go)
wraps the 100-entry ring in ~4-5.5s at 18-25Hz, after which physical index
order no longer matched chronological order: the seam pair (newest entry
adjacent to oldest) could masquerade as the line crossing, fabricating a
crossing from points minutes apart or invalidating the real one. The
interpolator now unwinds the ring chronologically before scanning.
- **Course detection could falsely match a longer layout** — a completed
proximity pass that matched no course left the lap-distance window
accumulating, so a driver lapping a course missing from config would match
a 2x-length layout with cumulative distance on the next pass. The window
now re-anchors after every completed pass, match or not (same root cause
as the v4.1.0 rejection-cooldown fix).
- **`M_PI` portability** — `GeoMath.h` now provides a fallback `#define M_PI`
when the platform's `<math.h>` doesn't expose it. `M_PI` is a POSIX/BSD
extension, not standard C/C++; glibc happens to expose it by default but
stricter standard libraries (under `-std=c++NN` / `__STRICT_ANSI__`) hide
it, breaking the host test build with `'M_PI' was not declared in this scope`.

### Changed
- **AVR (Mega/Uno) support is now documented as degraded** — on classic AVR
`double` is a 32-bit float, which quantizes GPS coordinates to ~0.2–0.75 m
and degrades odometer/interpolation accuracy; lap counting still works. A
compile-time `#warning` now fires on 4-byte-double targets and the README
hardware section spells out the limitation.
- **Memory documentation corrected** — `pruneInactiveCourses()` saves CPU,
not RAM: the 8-slot course-timer array (~29 KB with 64-bit doubles) is
statically allocated regardless of course count and never released. The
README and CLAUDE.md no longer claim it "drops to ~5 KB", and now state
plainly that `CourseManager` does not fit on AVR Mega.
- **Coverage gate raised from 1% to 80%** (measured line coverage is 84.5%
after the new suites) so deleting behavioral tests actually fails CI.
- **CI supply-chain hardening** — every workflow action is pinned to a
verified commit SHA (and `peaceiris/actions-gh-pages` bumped v3 → v4),
`.github/dependabot.yml` keeps the pins monitored weekly, and the
gh-pages/badge deploy steps now require `github.ref == refs/heads/master`
so a `workflow_dispatch` from a feature branch can no longer overwrite
production docs or the coverage badge.

### Added
- **`test_course_manager.cpp` and `test_waypoint_lap_timer.cpp`** — the two
largest v4.0 modules previously had zero direct tests. Now covered:
detection accept via `raceStarted` validation, reject → fallback (issue
#13 exercised through `CourseManager` where it manifested), no-match →
fallback, distance failsafe, Lap-Anything timer deactivation, prune
behavior, waypoint lap accounting, and reset semantics. Plus
`test_low_rate_gps.cpp` for 1 Hz / 5 Hz crossing detection and the
rejected-crossing counter. Line coverage: ~51% → 84.5%.
- **`getRejectedCrossingCount()`** on `DovesLapTimer`,
**`getNoMatchCount()`** on `CourseDetector`, and
**`isCourseTimerActive(index)`** on `CourseManager`.

- **GPS input validation** — `DovesLapTimer::loop()`, `WaypointLapTimer::loop()`
and `CourseDetector::update()` now reject NaN/Inf, out-of-range, and exact
(0,0) "null island" fixes (routine parser output during fix loss) that
previously poisoned odometers permanently and silently hung course
detection. Non-finite speed/altitude values are sanitized rather than the
whole fix being dropped. Single-fix teleports beyond
`GPS_MAX_PLAUSIBLE_JUMP_METERS` (500m) are 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.
- **`isStartFinishLineConfigured()`** getter on `DovesLapTimer`.
- 23 new host unit tests across three new suites (midnight rollover,
adversarial GPS input, crossing-buffer wraparound) plus course-detection
window re-anchor regression tests.
- Project governance docs: `CONTRIBUTING.md` (dev setup, the 3-layer testing
philosophy, PR workflow, release process), `SECURITY.md`, GitHub issue
forms (bug report + feature request) and a pull-request template.
- **`getCurrentSpeedKmh()` and `getCurrentSpeedMph()`** getters to
`DovesLapTimer.h`, `DovesLapTimer.cpp`, `WaypointLapTimer.h` and `WaypointLapTimer.cpp`

### Removed
- **`WaypointLapTimer`'s 50-entry proximity buffer** (and the
`ProximityBufferEntry` struct + `WAYPOINT_LAP_BUFFER_SIZE` constant) —
1.6 KB of SRAM per instance that was written on every in-proximity fix
and never read; the closest-approach lap split only ever used three
scalars. `WaypointLapTimer` shrinks from ~1.8 KB to ~150 B.

## [4.1.0] – 2026-05-21

A quality-and-confidence release. No breaking API changes; everything from
Expand Down
Loading
Loading