Documentation
¶
Overview ¶
Package wire holds the version 1 data types of the sim telemetry API, exactly as docs/API-V1.md defines them.
It is the shared vocabulary of two programs built independently in separate repositories: the capture client that produces the data and the server that stores it. Nothing here does any work — there is no HTTP, no database and no business rule in this package, only the shape of the bytes that cross between them. That is deliberate: a type whose JSON tags are the contract must be readable in one sitting.
Three rules hold for every type below.
The JSON tag is the contract, not the Go field name. Renaming a Go field is free; changing a tag is a wire-format change and breaks every deployed client. TestJSONShape pins the serialised shape of every type in this package against testdata/shapes.json so a tag cannot drift silently.
Times are RFC 3339 with an offset, and durations are integer milliseconds. time.Time marshals to RFC 3339 by itself; an optional time is a *time.Time so that "absent" and "the zero instant" stay distinct.
Ids the client generates are UUIDv7 strings, so they sort by creation time. This package does not generate or validate them: it carries them.
The integer scaling of TracePoint is shared with the client's own telemetry.TracePoint byte for byte. See that type's documentation for the units, and package github.com/pacenote-sim/protocol/trace for the binary codec that puts a slice of them on the wire.
Index ¶
- type CarSetup
- type CarState
- type Code
- type Conditions
- type Corner
- type CornerPattern
- type Discovery
- type Driver
- type Error
- type ErrorEnvelope
- type Feature
- type FieldCar
- type FieldReport
- type FieldResult
- type Flag
- type Kind
- type Lap
- type LapBatch
- type LapBatchResult
- type Limits
- type LivePoint
- type LiveSample
- type Me
- type OK
- type PairPoll
- type PairPollRequest
- type PairStart
- type PairStatus
- type PluginInfo
- type PluginRoute
- type ReferenceLap
- type Scope
- type SessionType
- type SetupTyre
- type SetupValue
- type Stint
- type StintResult
- type Summary
- type Team
- type TracePoint
- type TyreTemps
- type Wheel
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
This section is empty.
Types ¶
type CarSetup ¶
type CarSetup struct {
// UpdateCount is the simulator's own revision counter for the sheet, when
// it publishes one. Two stints of the same sitting with different counts
// were driven on different setups.
UpdateCount int `json:"update_count,omitempty"`
// Tyres is one entry per wheel the simulator published, in the order LF,
// RF, LR, RR. A car whose tyres are not published at all has none.
Tyres []SetupTyre `json:"tyres,omitempty"`
// RearWing is the rear wing setting, for the cars that have one. It is
// lifted out of Values and does not appear there as well.
RearWing *SetupValue `json:"rear_wing,omitempty"`
// Values is every other setting of the sheet — aero, chassis, brakes,
// drivetrain, fuel — in the order the simulator published them.
Values []SetupValue `json:"values,omitempty"`
}
CarSetup is the car's setup for a stint.
type CarState ¶
type CarState struct {
FuelUsedL float64 `json:"fuel_used_l"`
FuelLevelL float64 `json:"fuel_level_l"`
TyreTempC TyreTemps `json:"tyre_temp_c"`
}
CarState is what the car had left at the end of a Summary: litres used, litres remaining, and the four tyre temperatures in °C.
type Code ¶
type Code string
Code is the stable, machine-readable identifier a client branches on. It is snake_case, it never changes once published, and a client that does not recognise one must fall back to the HTTP status alone — which is what makes adding a code a backwards-compatible change.
const ( // the pairing screen. CodeUnauthorized Code = "unauthorized" // CodeForbidden (403): the feature is not available to this account. // Disable the button and tell the driver once. CodeForbidden Code = "forbidden" // CodeNotFound (404): no such resource. CodeNotFound Code = "not_found" // CodeConflict (409): an idempotency key was reused with a different body. // That is a bug in the caller: log it, never retry it. CodeConflict Code = "conflict" // CodeInvalid (422): the body failed validation. Log Detail, drop the // payload, do not retry. CodeInvalid Code = "invalid" // CodeRateLimited (429): back off for RetryAfterS and then resume. It // applies to the whole client, not to one endpoint. CodeRateLimited Code = "rate_limited" // CodeServerError (500): retry with backoff up to the client's cap. CodeServerError Code = "server_error" // CodeClientTooOld (426): the client is older than Discovery.MinClient. // Stop uploading and tell the driver to update. CodeClientTooOld Code = "client_too_old" )
The v1 codes, with the client behaviour each one demands (API-V1.md, "Errors"). Nothing outside this list may appear in a v1 response.
func Codes ¶
func Codes() []Code
Codes returns the v1 codes in the order API-V1.md tabulates them. The returned slice is freshly allocated, so a caller may keep or sort it.
func (Code) HTTPStatus ¶
HTTPStatus is the status the spec's error table pairs with the code, or zero for an unknown code. It exists so that the two sides cannot drift: the server chooses its status from here and the client's expectations come from the same table.
type Conditions ¶
type Conditions struct {
Skies int `json:"skies"`
Wetness int `json:"wetness"`
WindKmh float64 `json:"wind_kmh"`
Humidity float64 `json:"humidity"`
TrackTempC float64 `json:"track_temp_c"`
AirTempC float64 `json:"air_temp_c"`
}
Conditions are the session-mean weather values of a Summary.
Skies simulator enum, 0 clear … 3 overcast Wetness simulator enum, 0 unknown … 7 very wet WindKmh km/h Humidity percent, 0…100 TrackTempC °C AirTempC °C
type Corner ¶
type Corner struct {
// Turn is the corner's number on this lap, from 1, in the order driven.
Turn int `json:"turn"`
// ApexPct is where the apex is, in ‰ of the lap (0…1000).
ApexPct int `json:"apex_pct"`
// ApexKmh is the speed at the apex and RefApexKmh the speed the reference
// lap carried at the same point. A RefApexKmh of zero means the client
// compared against nothing there.
ApexKmh int `json:"apex_kmh"`
RefApexKmh int `json:"ref_apex_kmh,omitempty"`
// DeficitKmh is how much apex speed this corner lost against the
// reference, in whole km/h. It is positive: a client sends the corners
// that cost time and not the ones that gained it.
//
// It is a speed and not a time, because a speed is what is measured. A
// per-corner time loss would be an integration over a piece of track that
// the two laps did not necessarily cover at the same points, and calling
// the result milliseconds would dress an estimate up as a measurement.
DeficitKmh int `json:"deficit_kmh"`
// BrakeAtApex is the brake still applied at the apex, in percent. A high
// value is the driver trail-braking past the apex.
BrakeAtApex int `json:"brake_at_apex,omitempty"`
// ThrottleLag is the distance between the apex and the throttle pickup, in
// ‰ of the lap. A high value is the car not rotated and the driver waiting.
ThrottleLag int `json:"throttle_lag,omitempty"`
// Pattern is the shape of the mistake, when the two channels above name
// one. Empty is normal and means nothing conclusive.
Pattern CornerPattern `json:"pattern,omitempty"`
}
Corner is one corner of a completed lap and what it cost.
Turn is the detector's own numbering and not the circuit's. The corners of a lap are numbered from 1 in the order they are driven, counting only the ones the detector found: a turn taken flat, one whose speed drop is below the detector's threshold and one whose apex is below walking pace are not in the list, and every later corner shifts down by one when they are dropped. It is what the client's own spoken cues have always said out loud ("Turn 4"), so it is what a written cue must say too, and a consumer that maps it onto a circuit's published turn table is inventing a number this server cannot check.
Every speed is whole km/h, because the trace channel it is read from is. The two lap positions are per mille of the lap, the same scaling TracePoint uses for DistPct, so a consumer never has to reconcile two units for one axis.
type CornerPattern ¶
type CornerPattern string
CornerPattern names the shape of a mistake, so that a consumer can branch on it and a prompt can be written against a fixed vocabulary rather than against free text.
The vocabulary is deliberately short: it holds the patterns a detector can measure from the throttle and brake channels of one corner, and nothing a detector would have to guess at. An empty pattern is the normal case — a corner that was slower with nothing conclusive about why — and a consumer must treat it as an answer rather than as missing data.
const ( // PatternEarlyApex is a corner still being slowed at its apex that the // driver then has to wait to get back on the power in: the car was turned // in too early and the apex arrived before the corner did. PatternEarlyApex CornerPattern = "early_apex" // PatternLateBraking is a corner whose apex still carries brake pressure, // with the throttle picked up normally afterwards. PatternLateBraking CornerPattern = "late_braking" // PatternSlowExit is a corner the driver is off the brakes in but waits a // long way past the apex before picking the throttle up. PatternSlowExit CornerPattern = "slow_exit" )
The v1 corner patterns.
type Discovery ¶
type Discovery struct {
API string `json:"api"`
Name string `json:"name"`
ShortName string `json:"short_name"`
Logo string `json:"logo"`
Accent string `json:"accent"`
Features []Feature `json:"features"`
Limits Limits `json:"limits"`
MinClient string `json:"min_client"`
PairURI string `json:"pair_uri"`
PrivacyURI string `json:"privacy_uri"`
}
Discovery is the one unauthenticated document that everything else follows from, served at GET /.well-known/sim-telemetry.json and cacheable for five minutes. A client resolves a typed host name to it over HTTPS, falling back to plain HTTP only for localhost, and never hardcodes anything it can read here.
API is the base path for every other endpoint, conventionally "/api/v1". Name, ShortName, Logo and Accent are the white-label branding the client wears once paired; Accent is a CSS hex colour. MinClient is the lowest client version this server accepts, compared against the X-Client-Version header and answered with CodeClientTooOld. PairURI and PrivacyURI are shown on the pairing screen before the driver approves.
type Driver ¶
type Driver struct {
ID string `json:"id"`
Name string `json:"name"`
Slug string `json:"slug"`
Class string `json:"class"`
Avatar string `json:"avatar,omitempty"`
}
Driver is the identity behind a token, as the server knows it. Slug is the stable URL-safe name, Class is the driver's licence or category ("gt3"), and Avatar is an absolute URL or empty.
type Error ¶
type Error struct {
Code Code `json:"code"`
Message string `json:"message"`
Detail map[string]any `json:"detail,omitempty"`
RetryAfterS int `json:"retry_after_s,omitempty"`
}
Error is the machine-readable-first payload inside an ErrorEnvelope.
Code is what the client branches on. Message is shown to the driver exactly as written, so a server must make it a complete sentence with no jargon, no identifier and no stack detail. Detail is free-form context for the client's log and is never shown. RetryAfterS is meaningful for CodeRateLimited and is zero elsewhere.
It implements error, so a client can return one straight up its own stack and recover it again with errors.As.
type ErrorEnvelope ¶
type ErrorEnvelope struct {
Error *Error `json:"error"`
}
ErrorEnvelope is the body of every non-2xx response. The envelope exists so a success body can never be mistaken for a failure body by shape alone:
{ "error": { "code": "conflict", "message": "...", "detail": {...},
"retry_after_s": 0 } }
func NewError ¶
func NewError(code Code, message string) *ErrorEnvelope
NewError builds the envelope for one code and message. It is the only constructor either side needs, and it keeps the envelope from being built by hand differently in twenty places.
type Feature ¶
type Feature string
Feature names one optional capability. A capability is present only when it is named in both Discovery.Features and Me.Features: the first says the server has it, the second says this driver is entitled to it. Anything not named is absent, and absent means a disabled button, never an error dialog.
const ( FeatureTelemetry Feature = "telemetry" // stints, laps, traces, summaries — the core FeatureReference Feature = "reference" // GET /reference, the lap a driver's own is compared against FeatureLive Feature = "live" // POST /live, the fire-and-forget sample stream FeatureField Feature = "field" // POST /field, relaying the whole field FeatureCompetition Feature = "competition" // championships, qualifying sessions, results )
The v1 features. A client that meets a feature it does not know ignores it.
type FieldCar ¶
type FieldCar struct {
Num string `json:"num"`
Driver string `json:"driver"`
Class string `json:"class"`
Lap int `json:"lap"`
DistPct float64 `json:"dist"`
LastMs int `json:"last_ms"`
BestMs int `json:"best_ms"`
GapMs int `json:"gap_ms"`
Pit bool `json:"pit"`
Inc int `json:"inc"`
IsPlayer bool `json:"is_player"`
}
FieldCar is one car of the session field. The tags match the capture client's own telemetry.FieldCar byte for byte.
Num car number as shown, a string because "07" is not 7
Class car class ("gt3")
Lap the car's lap counter
Dist lap distance as a fraction, 0…1 — note that this is not the ‰ of
a TracePoint: the field relay is not a trace
LastMs last lap time, ms
BestMs best lap time, ms
GapMs gap to the leader, ms
Pit true while the car is on pit road
Inc incident count
IsPlayer true for the car of the client sending the report
type FieldReport ¶
type FieldReport struct {
StintID string `json:"stint_id"`
SessionType SessionType `json:"session_type"`
Flag Flag `json:"flag"`
LapsTotal int `json:"laps_total"`
Cars []FieldCar `json:"cars"`
}
FieldReport is the body of POST /field, the whole-field relay of the FeatureField feature.
Only one client per session needs to send this and the server decides which: see FieldResult.Armed. LapsTotal is zero in a timed session. Sent no more often than Limits.FieldIntervalMs.
type FieldResult ¶
FieldResult is the answer to POST /field. A client that receives Armed false is not the authoritative relay for this session and stops sending until its next stint; it is not an error and the driver is never told.
type Kind ¶
type Kind string
Kind classifies a completed lap. The four values are frozen wire literals: the capture client emits exactly these bytes and the server stores them.
type Lap ¶
type Lap struct {
Number int `json:"number"`
LapMs int `json:"lap_ms"`
Kind Kind `json:"kind"`
StartedAt time.Time `json:"started_at"`
Trace []TracePoint `json:"trace"`
Corners []Corner `json:"corners,omitempty"`
}
Lap is one completed lap with its trace. Number is the simulator's lap counter, LapMs the lap time in milliseconds, StartedAt when the lap began.
Laps are append-only and idempotent on (stint_id, number): the same number with identical content is accepted and counted once, and with different content it is CodeConflict. A trace longer than Limits.TracePoints is rejected.
Corners is the client's own corner analysis of this lap, worst first, or empty. It is derived from the trace and a reference the client chose, so it is not recomputable from the trace alone and is therefore carried rather than inferred. See Corner.
type LapBatch ¶
type LapBatch struct {
Laps []Lap `json:"laps"`
}
LapBatch is the body of POST /stints/{stint_id}/laps: at most Limits.LapsPerRequest laps, and at most Limits.MaxBodyBytes of them.
type LapBatchResult ¶
LapBatchResult is the answer to POST /stints/{stint_id}/laps. Accepted counts the laps stored by this call — a repeat of a lap already held counts once, not twice. BestLapMs is the best over the whole stint, not over this batch.
type Limits ¶
type Limits struct {
TracePoints int `json:"trace_points"`
LapsPerRequest int `json:"laps_per_request"`
LiveIntervalMs int `json:"live_interval_ms"`
FieldIntervalMs int `json:"field_interval_ms"`
SummaryIntervalMs int `json:"summary_interval_ms"`
MaxBodyBytes int `json:"max_body_bytes"`
}
Limits replaces every hardcoded interval in the client. The capture period stays local — the client samples the simulator as fast as it likes — but how often anything is *sent* comes from here, and the server's own rate limiter is built from the same constants, so the two agree by construction.
TracePoints the longest trace the server accepts, in samples; a
longer one is rejected outright (300 is typical)
LapsPerRequest the most laps in one POST /stints/{id}/laps
LiveIntervalMs minimum gap between POST /live calls
FieldIntervalMs minimum gap between POST /field calls
SummaryIntervalMs minimum gap between PUT /stints/{id}/summary calls
MaxBodyBytes the largest request body the server will read
type LivePoint ¶
type LivePoint struct {
TracePoint
Lap int `json:"lap"`
OnTrack bool `json:"on_track"`
OnPitRoad bool `json:"on_pit_road"`
}
LivePoint is one live sample: the twelve trace keys of a TracePoint plus the three things that only make sense live. The trace keys are promoted, so a LivePoint marshals as a flat object with fifteen keys and not as a nested one.
Lap is the simulator's lap counter. OnTrack is false while the driver is in the garage or spectating; OnPitRoad is true on the pit lane.
type LiveSample ¶
type LiveSample struct {
StintID string `json:"stint_id"`
At time.Time `json:"at"`
Sample LivePoint `json:"sample"`
CurLap []TracePoint `json:"cur_lap"`
}
LiveSample is the body of POST /live, the fire-and-forget sample stream of the FeatureLive feature.
It is never retried and never queued: a stale live sample is worthless, so a failure is dropped silently rather than kept. CurLap is the lap so far, which lets a viewer that has just connected draw the trace behind the car instead of waiting for the next lap. Sent no more often than Limits.LiveIntervalMs.
type Me ¶
type Me struct {
Driver Driver `json:"driver"`
Team *Team `json:"team"`
Features []Feature `json:"features"`
Plugins []PluginInfo `json:"plugins"`
}
Me is the answer to GET /me: who this token belongs to and what it may do.
Features here is the intersection of the server's features with this driver's entitlements, so a free account and a paid one get different buttons on the same server. Once authenticated the client uses this list and not Discovery.Features.
Plugins is what is installed and running on this server that a client could talk to, by name and route. It is here and not in Discovery because an installation's plugins are the driver's business once paired and nobody else's, and a public, proxy-cached document is the wrong place for an inventory.
func (Me) Has ¶
Has reports whether this driver is entitled to the feature. A capability needs both this and Discovery.Has.
type OK ¶
type OK struct {
OK bool `json:"ok"`
}
OK is the answer to a call that has nothing to report but success, such as PUT /stints/{stint_id}/summary.
type PairPoll ¶
type PairPoll struct {
Status PairStatus `json:"status"`
Token string `json:"token,omitempty"`
Driver *Driver `json:"driver,omitempty"`
}
PairPoll is the answer to POST /pair/poll. Token and Driver are present only when Status is StatusApproved; the token is a bearer credential that the client stores and never logs.
type PairPollRequest ¶
type PairPollRequest struct {
DeviceCode string `json:"device_code"`
}
PairPollRequest is the body of POST /pair/poll: the device code and nothing else. The call is unauthenticated — a pairing is how a client gets its first token, so it cannot present one.
type PairStart ¶
type PairStart struct {
DeviceCode string `json:"device_code"`
UserCode string `json:"user_code"`
VerificationURI string `json:"verification_uri"`
IntervalS int `json:"interval_s"`
ExpiresInS int `json:"expires_in_s"`
}
PairStart is the answer to POST /pair/start, an unauthenticated standard device-code grant. The call has no request body.
DeviceCode is the secret the client polls with and never shows anyone. UserCode is the short human code, "H4T-9KQ", that the driver types into VerificationURI in a browser. IntervalS is a floor, not a suggestion: a client must not poll faster. ExpiresInS is how long the code stays good.
type PairStatus ¶
type PairStatus string
PairStatus is the state of a device-code pairing as POST /pair/poll reports it. Only StatusApproved carries a token.
const ( StatusPending PairStatus = "pending" // keep polling, no faster than IntervalS StatusApproved PairStatus = "approved" // Token and Driver are set; stop polling StatusDenied PairStatus = "denied" // the driver refused; stop polling StatusExpired PairStatus = "expired" // the device code timed out; start again )
The v1 pairing states.
type PluginInfo ¶ added in v0.2.0
type PluginInfo struct {
// Name is the plugin's own name, and its mount: every route below is under
// /plugin/<name>/.
Name string `json:"name"`
// Title is what the plugin calls itself for a person, or Name when it
// declared none.
Title string `json:"title"`
// Version is the plugin's own version, spelled as its author spells it. A
// client that knows a particular plugin checks it here rather than
// discovering a mismatch one request at a time.
Version string `json:"version"`
// Routes are the addresses a client could reach. Routes only an operator
// may open are not listed.
Routes []PluginRoute `json:"routes"`
}
PluginInfo is one running plugin, as GET /me describes it.
type PluginRoute ¶ added in v0.2.0
type PluginRoute struct {
// Path is the address under /plugin/<name>. "/" is the plugin's front page.
Path string `json:"path"`
// Access is who may reach it: "public" for anyone, "driver" for a signed-in
// driver, "custom" for a plugin that checks its callers itself.
Access string `json:"access"`
}
PluginRoute is one address a plugin serves, relative to its mount.
type ReferenceLap ¶
type ReferenceLap struct {
Scope Scope `json:"scope"`
LapMs int `json:"lap_ms"`
DriverName string `json:"driver_name,omitempty"`
Trace []TracePoint `json:"trace"`
}
ReferenceLap is the answer to GET /reference: the lap the coach's training mode compares the driver against, or CodeNotFound when the server has none.
Scope is the scope the server actually used, which may be narrower than the one asked for. DriverName is a display name and may be empty when the lap is the driver's own.
type Scope ¶
type Scope string
Scope says how wide a net a reference lap was drawn from. It is a preference on the request and a statement of fact on the response: a server returns the narrowest scope it actually has and names it, so a server with no class data can still answer with the driver's own best rather than a 404.
type SessionType ¶
type SessionType string
SessionType is what the driver was doing, and it is the same vocabulary the simulator uses.
const ( SessionPractice SessionType = "practice" SessionQualifying SessionType = "qualifying" SessionRace SessionType = "race" SessionTesting SessionType = "testing" )
The v1 session types.
type SetupTyre ¶
type SetupTyre struct {
// Wheel is which corner of the car this is.
Wheel Wheel `json:"wheel"`
// ColdKpa is the starting pressure the setup sheet holds and HotKpa the
// pressure the tyre was last read at, both in kPa. The difference is how
// far the cold setting has to move.
ColdKpa float64 `json:"cold_kpa,omitempty"`
HotKpa float64 `json:"hot_kpa,omitempty"`
// The last tread temperatures across the tyre, in °C, inner to outer.
TempInnerC float64 `json:"temp_inner_c,omitempty"`
TempMiddleC float64 `json:"temp_middle_c,omitempty"`
TempOuterC float64 `json:"temp_outer_c,omitempty"`
// The tread remaining across the tyre, in percent, inner to outer.
TreadInnerPct float64 `json:"tread_inner_pct,omitempty"`
TreadMiddlePct float64 `json:"tread_middle_pct,omitempty"`
TreadOuterPct float64 `json:"tread_outer_pct,omitempty"`
}
SetupTyre is one wheel of a CarSetup: what it was set to and what it came back at.
The three temperatures and the three tread depths are ordered inner, middle, outer as the car stands, whichever order the simulator published them in. A simulator that reports a left tyre outside-in and a right tyre inside-out is normalised here, once, so that "the inners were hotter than the outers" is the same comparison on both sides of the car.
A field left at zero was not published. Nothing here is negative and a real tyre is never at zero kPa, zero degrees or zero tread.
type SetupValue ¶
type SetupValue struct {
// Group is the path the simulator published this under, slash-separated
// ("Chassis/LeftFront"). It is empty for a value published at the top of
// the block.
Group string `json:"group,omitempty"`
// Name is the key, spelled the way the simulator spells it, so that a
// driver can find the control on the setup screen.
Name string `json:"name"`
// Text is the value as published.
Text string `json:"text"`
// Number is the leading number of Text, and Unit what followed it.
Number float64 `json:"number,omitempty"`
Unit string `json:"unit,omitempty"`
}
SetupValue is one named setting of a setup sheet.
Text is what the simulator printed and is authoritative: "−2.5 deg", "Soft", "9 hole", "0 clicks". Number and Unit are that text read as a measurement where it is one — −2.5 and "deg" — and are the leading number and the rest of the line, nothing cleverer. A setting whose text carries no number has Number zero and Unit empty, which is indistinguishable from a setting whose value really is a bare zero; Text is what separates the two, which is why it is always present.
type Stint ¶
type Stint struct {
Sim string `json:"sim"`
Track string `json:"track"`
TrackID string `json:"track_id"`
Car string `json:"car"`
CarClass string `json:"car_class"`
SessionType SessionType `json:"session_type"`
StartedAt time.Time `json:"started_at"`
Sectors []float64 `json:"sectors"`
Setup *CarSetup `json:"setup,omitempty"`
}
Stint is the body of PUT /stints/{stint_id}: one continuous capture, one car, one track, one session type, one sitting. The client generates the id, which is a UUIDv7 in the path, so it can create a stint offline and upload it hours later and a retry is always safe.
The call is idempotent: repeating it updates the mutable fields and returns the same stint. TrackID is the simulator's own stable identifier ("barcelona gp") and Track is the display name. Sectors are the sector start positions as fractions of the lap, 0…1, ascending, the first conventionally 0.
Setup is the car's setup for this sitting, or nil. It is on the stint and not on the lap because it is one per sitting, and it is optional because a simulator that publishes no setup, and a series that locks it away, are normal rather than broken. See CarSetup.
type StintResult ¶
StintResult is the answer to PUT /stints/{stint_id}. SessionID is the server's own competition session if it chose to attach one; the client only ever echoes it back in the user interface and never sends it anywhere.
type Summary ¶
type Summary struct {
Laps int `json:"laps"`
Incidents int `json:"incidents"`
BestLapMs int `json:"best_lap_ms"`
AvgLapMs int `json:"avg_lap_ms"`
ConsistencyPct int `json:"consistency_pct"`
TopSpeedKmh int `json:"top_speed_kmh"`
BestTrace []TracePoint `json:"best_trace"`
Conditions Conditions `json:"conditions"`
CarState CarState `json:"car_state"`
FinishedAt *time.Time `json:"finished_at,omitempty"`
}
Summary is the body of PUT /stints/{stint_id}/summary, sent periodically during a stint and once more when it ends. It is a whole-document replace, so a retry can never merge two halves of two different states, and a present FinishedAt means this one is final.
ConsistencyPct is 0…100. TopSpeedKmh is km/h. BestTrace is the best clean lap's trace and obeys Limits.TracePoints like any other.
type TracePoint ¶
type TracePoint struct {
OffsetMs int `json:"t"`
SpeedKmh int `json:"v"`
Throttle int `json:"thr"`
Brake int `json:"brk"`
Gear int `json:"g"`
RPM int `json:"r"`
Steer int `json:"st"`
DistPct int `json:"p"`
LatG int `json:"lg"`
LongG int `json:"og"`
La int `json:"la"`
Lo int `json:"lo"`
}
TracePoint is one sample of a lap trace: every channel as a scaled integer so that a 300-point lap is a few kilobytes of JSON rather than tens, and a few hundred bytes once package trace has compressed it.
The JSON tags and the scaling are frozen. They are the "Trace format" table of API-V1.md:
key field meaning unit range t OffsetMs offset from lap start ms ≥ 0 p DistPct lap distance ‰ of lap 0…1000 v SpeedKmh speed km/h 0…700 thr Throttle throttle % 0…100 brk Brake brake % 0…100 g Gear gear −1 reverse, 0 neutral −1…10 r RPM engine speed rpm ≥ 0 st Steer steering, + is left degrees −1000…1000 lg LatG lateral acceleration g × 100 −1000…1000 og LongG longitudinal acceleration g × 100 −1000…1000 la La latitude degrees × 10⁵ −9000000…9000000 lo Lo longitude degrees × 10⁵ −18000000…18000000
Every key is present on every sample; a producer clamps, it never wraps, and it never emits a non-finite value. Within one trace t is strictly increasing. la and lo are optional as a pair: both zero means the sample carries no GPS fix, which is why they are not pointers — zero is the sentinel the spec chose.
The two open bounds of the spec's table, t and r, and the unstated bounds of la and lo, are closed by the codec at trace.MaxOffsetMs, trace.MaxRPM and the latitude and longitude of a real planet. They are wide enough that no honest sample reaches them and narrow enough that the delta between two of them cannot overflow.
The field order below is the capture client's, not the spec table's, and that is deliberate: encoding/json writes object keys in struct-declaration order, so declaring the fields as telemetry.TracePoint declares them makes a marshalled point byte-identical on both sides of the wire rather than merely equivalent. The tags are what the contract pins; the order is what makes the golden vectors reusable by the client unchanged.
func (TracePoint) HasGPS ¶
func (p TracePoint) HasGPS() bool
HasGPS reports whether the sample carries a GPS fix. La and Lo are optional as a pair and both zero means absent, so a trace recorded without GPS is a valid trace and not an error.