Lap detection & sessions¶
GT7 doesn't send "lap completed" messages — the datalogger has to cut clean laps out of a 60 Hz packet stream that also includes menus, replays, pauses, and restarts. This page explains exactly how.
What gets sampled¶
For every packet, a sample is recorded onto the in-progress lap only if all of these hold:
- the car is on track (flag bit 0) and the game is not paused (bit 1);
current_lap > 0— GT7 reports the out-lap in time trials as "lap 0", which is never recorded;- the race isn't already finished — after the checkered flag GT7 reports
current_lap = total_laps + 1for the cool-down lap, which is skipped.
Packets with the LOADING flag set are dropped entirely before any processing.
Time and distance are synthesized from the console's own packet counter, not from wall clocks or a fixed tick assumption:
- each sample covers
Δpacket_idframes (clamped to 1–60; a pid reset, non-monotonic value, or a gap over 1 s falls back to a single frame); t += Δframes × 1/60seconds,dist += speed_mps × Δframes × 1/60meters.
Dropped datagrams therefore widen the time/distance steps instead of silently
compressing the axes. The pid tracker also advances on paused/off-track packets, so
unpausing sees a ~1-frame gap — pauses still add no lap time or distance. Frames
lost in transit are counted and reported as frames_dropped in /api/status and
the Admin diagnostics.
Input metrics (full-throttle %, braking %, coasting %, tire spin, TCS/ASM activity)
are time-weighted using the t deltas, so a sample recorded after a gap counts
for the whole gap it covers.
When a lap completes¶
A lap-counter change is only accepted as a real completed lap when all four conditions hold:
- the previous lap number was
> 0(a real lap, not the out-lap); - the counter moved exactly +1 (monotonic step);
- the game reported a positive
last_lap_time_ms; - the lap collected at least 600 samples (~10 s of received packets at 60 Hz).
Condition 4 is the phantom-lap guard: in menus and replays GT7's lap counter
flickers through stale values and re-reports an old last_lap_time. Requiring 10
seconds of actual on-track samples filters all of that out.
The lap time itself is taken verbatim from GT7's last_lap_time_ms — it is never
computed from tick counts, so it matches the in-game timing exactly. (The one exception
is the manual Log lap now action, which saves a partial lap and derives its time from
the sample clock.)
Why state is committed before the database write
Packets keep arriving at 60 Hz while a lap is being written to SQLite. All processor state — lap counter, sample buffer, fuel and engine aggregates — is committed before the asynchronous save starts; otherwise the lap boundary would re-trigger on the next packet and duplicate the lap. There's a regression test for exactly this.
What's stored per lap¶
- The full 60 Hz sample series (~28 channels — see Derived channels), JSON-encoded.
- Per-lap aggregates: fuel used, full-throttle / full-brake / coasting / tire-spin percentages, max speed, min body height, TCS/ASM usage, engine health, time of day.
- Detected chassis events, computed once at save time.
- Gearing metadata (ratios, tune top speed, redline) captured from the boundary packet.
Session lifecycle¶
A session groups consecutive laps that belong together. A new session starts when:
- the car changes (
car_iddiffers from the session's car), or - the lap counter resets —
current_lapdrops below where it was (race restart, return to menu and back out).
When a new session starts, the previous one is closed first:
- a session that ended with zero laps is deleted — menu visits and quick restarts don't pile up as empty rows;
- a session with laps triggers the
session_summarywebhook notification (car, track, laps, best lap, fuel used).
On the first completed lap of a session, the lap's geometry is compared against the saved track signatures and the session is tagged automatically if it matches — see Track identification.
Best-lap tracking¶
The service tracks the session best and, separately, the best before the
just-completed lap (prev_best_ms). The live "Δ best" readout compares against
prev_best_ms, so when you set a new personal best you see the improvement
(e.g. −0.312) instead of a useless +0.000.
The personal_best webhook fires only when an existing session best is actually beaten —
never on the first lap of a session.
Partial-lap guard. A lap the logger only saw part of — a pit out-lap, or capture starting mid-lap — passes every structural check above (GT7 reports a time for it, and it lasts more than 10 s) while covering less of the track, with a distance axis anchored wherever recording began rather than at the line. Its reported time is shorter than a real lap's, so left alone it wins the session, becomes the live-delta reference, and turns every position-based comparison built on it into garbage.
_apply_span_guard judges each lap by its distance span against recent laps, so it
needs no knowledge of the track's true length. Two properties matter more than the
numbers:
- Every lap of the session is re-judged when a new one arrives. The yardstick
moves as laps accumulate, so a verdict fixed at lap time goes stale — a lap that
looked full next to one short lap is partial once three laps agree, and vice versa.
Stored rows are rewritten in both directions (
mark_session_laps_partial). - Dropping a lap promotes the fastest remaining full lap, rather than blanking the best until the next one arrives.
Calibrated against 850 recorded laps of real driving:
| Value | Why | |
|---|---|---|
| Yardstick | median span of the last 5 laps | 12 of those laps ran longer than their session median (one by 44 %, an off-track excursion), and against a max-based yardstick one such lap makes every normal lap after it look partial |
| Full-lap ratio | 97 % | 98 % of laps sit within 0.5 % of their session median; no legitimate lap fell below 94.7 %, while real partials measured 40-95 % |
| Ratio before 3 laps | 93 % | with two laps "this one is 6 % short" and "that one ran 6 % wide" are the same picture, so only a flagrant shortfall counts until a third lap settles it |
span_confirmed — several laps agreeing on the distance — is exported for consumers
that compare laps by position: the Race Engineer's coaching callouts stay silent
until it is true (see Race Engineer callouts).
What "invalid lap" means here¶
There is no track-limits detection (GT7 doesn't expose it). Laps are excluded only by the structural rules above: lap-0 out-laps, laps under 600 ticks, post-finish laps, and paused/off-track ticks. The Analysis view additionally hides laps that ended up with no samples.