You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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:
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
};
On new best lap: auto-promote currentLapVector → referenceLapVector.
On every subsequent GPS fix: find the nearest reference point and compute:
rawDelta = currentLapTime - referenceTimeAtNearestPoint
Apply an Exponential Moving Average filter to smooth GPS noise:
filteredDelta = alpha * filteredDelta + (1 - alpha) * rawDelta
(default alpha = 0.3f; user-configurable)
Apply a ±120s sanity guard — reject physically impossible values without
corrupting the EMA state (no update on reject, not zeroing).
Throttle updates to 25 Hz (every 40ms) to match typical GPS output rate
and avoid spinning on high-update microcontrollers.
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;
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.
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
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.
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.
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:
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
};
On new best lap: auto-promote
currentLapVector→referenceLapVector.On every subsequent GPS fix: find the nearest reference point and compute:
rawDelta = currentLapTime - referenceTimeAtNearestPoint
Apply an Exponential Moving Average filter to smooth GPS noise:
filteredDelta = alpha * filteredDelta + (1 - alpha) * rawDelta
(default alpha = 0.3f; user-configurable)
Apply a ±120s sanity guard — reject physically impossible values without
corrupting the EMA state (no update on reject, not zeroing).
Throttle updates to 25 Hz (every 40ms) to match typical GPS output rate
and avoid spinning on high-update microcontrollers.
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
lastBestIdxfrom 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:
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:
known position/time offset → verify getDelta() converges to expected value
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
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.
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.
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.