Help us shape the future of Go! Take the 2026 Go Developer Survey

wire

package
v0.2.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 16, 2026 License: GPL-3.0 Imports: 3 Imported by: 2

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.

  1. 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.

  2. 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.

  3. 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

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 (
	// CodeUnauthorized (401): the token is not valid. Discard it and return to
	// 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

func (c Code) HTTPStatus() int

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.

func (Code) Known

func (c Code) Known() bool

Known reports whether c is one of the v1 codes. A server must only send known codes; a client must treat an unknown one as the HTTP status alone rather than as an error in itself.

func (Code) Retryable

func (c Code) Retryable() bool

Retryable reports whether a client may retry the request that produced this code. Only 429 and 5xx are retryable; every other 4xx is a permanent answer and retrying it just burns the driver's connection (API-V1.md, "Rate limits and retries").

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"`
	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.

func (Discovery) Has

func (d Discovery) Has(f Feature) bool

Has reports whether the server declares the feature. It is the server half of the two-sided check; the driver half is Me.Has.

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.

func (*Error) Error

func (e *Error) Error() string

Error implements the error interface. The rendering is for a log, never for the driver: the driver sees Message.

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

type FieldResult struct {
	OK    bool `json:"ok"`
	Armed bool `json:"armed"`
}

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 Flag

type Flag string

Flag is the session flag a FieldReport carries.

const (
	FlagGreen     Flag = "green"
	FlagYellow    Flag = "yellow"
	FlagRed       Flag = "red"
	FlagWhite     Flag = "white"
	FlagCheckered Flag = "checkered"
	FlagBlack     Flag = "black"
)

The v1 session flags.

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.

const (
	KindClean   Kind = "clean"   // a full lap from the line, on track throughout
	KindIn      Kind = "in"      // entered the pits
	KindOut     Kind = "out"     // started on pit road
	KindInvalid Kind = "invalid" // driven, but not to be counted
)

The v1 lap kinds.

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

type LapBatchResult struct {
	Accepted  int `json:"accepted"`
	BestLapMs int `json:"best_lap_ms"`
}

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

func (m Me) Has(f Feature) bool

Has reports whether this driver is entitled to the feature. A capability needs both this and Discovery.Has.

func (Me) Plugin added in v0.2.0

func (m Me) Plugin(name string) (PluginInfo, bool)

Plugin finds a running plugin by name. A plugin that is not running is not there, and a client turns its button off.

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.

const (
	ScopeSelf  Scope = "self"  // this driver's own best
	ScopeCar   Scope = "car"   // the best anyone has driven in this car
	ScopeClass Scope = "class" // the best anyone has driven in this class
)

The v1 reference-lap scopes, narrowest first.

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

type StintResult struct {
	StintID   string  `json:"stint_id"`
	SessionID *string `json:"session_id"`
}

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 Team

type Team struct {
	ID   string `json:"id"`
	Name string `json:"name"`
}

Team is the driver's team, or nil when they drive alone.

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.

type TyreTemps

type TyreTemps struct {
	LF float64 `json:"lf"`
	RF float64 `json:"rf"`
	LR float64 `json:"lr"`
	RR float64 `json:"rr"`
}

TyreTemps is the four corners in °C, left and right, front and rear.

type Wheel

type Wheel string

Wheel names a corner of the car. The four spellings are frozen wire literals.

const (
	WheelLF Wheel = "lf"
	WheelRF Wheel = "rf"
	WheelLR Wheel = "lr"
	WheelRR Wheel = "rr"
)

The four wheels.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL