Skip to content

FEATURE: GPS-based real-time lap delta (gap to reference lap) #29

Description

@FabioRoss

What problem are you trying to solve?

When karting or circuit racing, the most useful number on a live display is not
your current lap time — it's whether you are gaining or losing time compared to
your personal best. "Am I +0.4s slower through the first sector?" tells you
exactly where you're losing it.

DovesLapTimer already tracks the best lap time. The missing piece is comparing
WHERE you are on track right now (GPS position + elapsed time) against WHERE
you were at the same track position during the best lap, and giving the user
a signed delta in seconds: positive = currently slower, negative = currently faster.

Without this, users have to maintain a separate GPS trace buffer and correlation
logic in their sketch, duplicating the lap vector infrastructure the library
already needs internally.

Proposed solution

Add a GPS-position-based lap delta calculation as a first-class feature inside
DovesLapTimer. The library already records lat/lng every fix — we need to:

  1. Store each GPS fix during the current lap as a LapPoint:

    struct LapPoint {
    unsigned long time; // ms from lap start
    double posX; // latitude
    double posY; // longitude
    int32_t speed; // reserved for future use
    };

  2. On new best lap: auto-promote currentLapVectorreferenceLapVector.

  3. On every subsequent GPS fix: find the nearest reference point and compute:
    rawDelta = currentLapTime - referenceTimeAtNearestPoint

  4. Apply an Exponential Moving Average filter to smooth GPS noise:
    filteredDelta = alpha * filteredDelta + (1 - alpha) * rawDelta
    (default alpha = 0.3f; user-configurable)

  5. Apply a ±120s sanity guard — reject physically impossible values without
    corrupting the EMA state (no update on reject, not zeroing).

  6. Throttle updates to 25 Hz (every 40ms) to match typical GPS output rate
    and avoid spinning on high-update microcontrollers.

  7. Expose public API:
    float getDelta() const; // filtered delta, seconds
    void setFilterAlpha(float alpha); // EMA smoothing factor
    float getFilterAlpha() const;
    void setUseNewDeltaCalc(bool use); // algorithm selector
    bool getUseNewDeltaCalc() const;
    const std::vector& getReferenceLapVector() const; // for SD save/load
    void setReferenceLapVector(const std::vector&); // for SD restore

TWO SEARCH ALGORITHMS (user-selectable):

Algorithm A — Full scan (simple, good for static reference):
For each fix: scan ALL reference points, return nearest by haversine.
O(n) per update. Fine for short tracks or infrequent GPS rates.

Algorithm B — Windowed search (default recommended):
Maintain lastBestIdx from previous match. Search only a bounded window:
- LOOK_BACK = 125 frames (~5s at 25 Hz) — handles driver gaining time fast
- LOOK_FORWARD = 375 frames (~15s at 25 Hz) — handles driver losing time badly
O(1) amortized. Eliminates hairpin jitter where the full-scan jumps to the
"wrong" nearest point on the opposite side of a u-turn.

Internal state reset on lap start:
currentDelta = 0; filteredDelta = 0; lastBestIdx = 0;

===============================================================================
REAL-WORLD VALIDATION

This exact implementation has been running in a real karting datalogger (RaceBoard)
for multiple race sessions at Italian circuits. Development went through three
significant evolution commits:

  • Initial implementation: identified hairpin jitter with full-scan algorithm
  • Windowed search fix: bounded ±5s/±15s window eliminated jitter at hairpins
  • Sanity guard fix: original ±5s clamp caused delta to freeze during aggressive
    sector improvements >5s; replaced with ±120s guard that preserves EMA state

The filtered delta has been validated as smooth and physically meaningful on
track. Race displays update correctly through sector changes, hairpins, and
chicanes without jumps.

===============================================================================
CONTRIBUTION READINESS

Code exists and is working. For a PR:

  • Strip SD logger and circuit-database dependencies (those are RaceBoard-specific)
  • Update loop() call sites to upstream's current signature
  • Add regression test: synthetic reference lap + synthetic current lap with
    known position/time offset → verify getDelta() converges to expected value
  • Add unit test: EMA filter state NOT corrupted when rawDelta > ±120s
  • Add unit test: windowed algorithm correctly handles driver gaining/losing time
  • Doxygen comments on all new public methods
  • Consider: should reference lap auto-save/load be part of this PR or a
    follow-on? (See also Issue [NEEDS MORE DISCUSSION] FEATURE: Session-persistent lap state injection (setLapsData) #34 Session Persistence)

Memory note: std::vector on a 125-point lap at 25 Hz costs ~
125 * sizeof(LapPoint) = ~2.5 KB RAM per vector (two vectors = ~5 KB).
This is acceptable on ESP32/nRF52 targets; may need opt-out on AVR Mega.

Alternatives considered

  1. Pace difference (getPaceDifference) — already exists in the library but
    uses distance/time pace ratio, not position correlation. This doesn't
    tell you WHERE on track you are gaining/losing; it's a trailing average.

  2. User manages their own GPS trace vector externally — current workaround.
    Forces every user who wants delta to reinvent the same infrastructure.
    The library already has the GPS fixes; duplicating in user code is wasteful.

  3. Sector-based delta only (see companion issue FEATURE: Per-sector delta timing (vs best and optimal) with live lap progress delta #31) — sector deltas tell you
    per-split performance but only update at line crossings, not continuously.
    GPS delta updates every fix, giving live feedback through corners.

Metadata

Metadata

Assignees

Labels

enhancementNew feature or request

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions