Lap Tracking
The lap thread counts laps by watching the car cross a virtual finish line. You set the line once (at the start/finish, while driving through it), and from then on every crossing in the same direction increments the lap count. The logic lives in lap_app.c.
Feature branch: not fully wired yet
This subsystem lives on feature/lap-tracking. The detection algorithm below is implemented, but three pieces it depends on are not yet defined in the tree:
- The tunables
LAP_MIN_DISTANCE_M,LAP_MIN_TIME_MS,LAP_LINE_HALF_WIDTH_M,LAP_THREAD_STACK_SIZE,LAP_THREAD_PRIORITY(belong insys_conf.h). - The CAN emitters
send_lap_completed_can_frame()/send_lap_reset_can_frame(). - The telemetry lap-domain setters
telemetry_lap_count_set()/telemetry_lap_write_begin/_end().
Define these before expecting the node to build and publish a lap count.
How the finish line is defined
Lap tracking is driven entirely by GPS. When a set request comes in (lap_app_request_set()), the thread captures the current fix and freezes a finish line:
- Center = the car's current lat/lon.
- Normal = the car's current heading (yaw). The finish line is the segment perpendicular to the direction of travel, so you "break the tape" as you drive straight through it.
The normal is stored as an East/North unit vector (sin(yaw), cos(yaw)).
You need a fix to set the line
If there's no GPS fix at set-time the request is ignored: there's no position to anchor the line to.
How a crossing is detected
On every GPS update (lap_app_notify_gps_update()), the thread takes a snapshot and, for the current position, computes two quantities relative to the finish line:
- Signed distance along the normal: how far in front of (+) or behind (−) the line you are.
- Lateral distance along the line: how far sideways from the line's center you are.
Positions are projected to local metres using an equirectangular approximation around the line center (dx = Δlon·cos(lat)·R·π/180, dy = Δlat·R·π/180).
A lap is counted when all of these hold:
- Sign flip: the signed distance went from negative on the previous fix to ≥ 0 now (you crossed the line in the correct direction).
- Within the line width: the lateral distance is ≤
LAP_LINE_HALF_WIDTH_M(you crossed the actual line segment, not its infinite extension somewhere across the paddock). - Minimum distance: you've travelled at least
LAP_MIN_DISTANCE_Msince the last lap. - Minimum time: at least
LAP_MIN_TIME_MShas elapsed since the last lap.
Conditions 3 and 4 are debounces: they stop a car that's crawling back and forth over the line (in the pits, or under yellow) from racking up phantom laps.
The geometry the test is built on: the crossing happens when the signed distance flips sign while staying within the line's half-width.
signed distance: ── prev(−) ───┼─── now(+) ──► crossing candidate
finish line: │
◄──────┼──────► lateral must be ≤ half widthDistance accumulation
Between fixes the thread adds the great-circle (haversine) distance travelled to distance_since_last_lap_m. That running total is what condition 3 checks, and it resets to zero on every counted lap.
Set / reset / count
| Trigger | Effect |
|---|---|
lap_app_request_set() | Freeze a new finish line at the current position, reset count to 0, emit a lap-completed frame with count 0 |
lap_app_request_reset() | Clear the finish line and count, emit a reset frame |
| finish-line crossing | Increment count, reset per-lap distance/time, publish count, emit a lap-completed frame |
The request flags are set from elsewhere (e.g. dashboard buttons) and consumed by the lap thread, which is woken through its own semaphore, so requests and GPS updates funnel through the same event loop.
Geometry notes
- Distances use
EARTH_RADIUS_M = 6,371,000 m. - The haversine result is clamped (
a ≤ 1.0) to stay numerically safe near zero. - The equirectangular projection is only used locally around the finish line, where its small-angle error is negligible: much cheaper than a full haversine on every fix for the crossing test.
