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

clientplugin

package module
v0.1.0 Latest Latest
Warning

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

Go to latest
Published: Sep 19, 2026 License: Apache-2.0 Imports: 17 Imported by: 0

README

clientplugin

The contract between the Pacenote client — the telemetry app a driver runs beside the simulator — and the plugins compiled into it. A Go module of interfaces and types, nothing else: what a plugin can be, what the app lends it, the sample a simulator reader produces, the events the app raises, the registry a plugin registers into, and the manifest that says what a plugin is.

Go has no run-time plugins on Windows, so a client plugin is a Go module whose package registers itself when imported. The team's server builds the client: it generates a main that imports the app and every plugin the operator chose, and compiles it into one exe.

Two kinds of plugin

A source reads one simulator and produces a Sample at the simulator's own rate. One source runs at a time — the one whose simulator is running — so a client may hold several.

Running is the whole of that choice, so it answers for the simulator and not for the plugin: a source that says yes when its game is not up is driven in front of the one the driver started.

package iracing

func init() { clientplugin.RegisterSource(&source{}) }

type source struct{ /* … */ }

func (*source) Name() string                                     { return "iracing" }
func (*source) Running() bool                                    { /* the shared memory exists */ }
func (s *source) Open(ctx context.Context) error                 { /* attach */ }
func (s *source) Read(ctx context.Context) (clientplugin.Sample, error) { /* the next reading */ }
func (s *source) Close() error                                   { /* detach */ }

A companion is the client half of a server plugin. The app starts it when GET /me lists a running server plugin of the same name, delivers the events it asked for, and stops it when the plugin disappears. Through its Host it can reach exactly its own plugin's routes with the driver's token, play audio, speak words, show a page, and log. Nothing else.

package engineer

func init() { clientplugin.RegisterCompanion(&companion{}) }

func (*companion) Name() string { return "engineer" }
func (*companion) Wants() []clientplugin.EventKind {
	return []clientplugin.EventKind{clientplugin.KindLapLastCorner, clientplugin.KindCornerApproaching}
}
func (c *companion) Notify(ctx context.Context, e clientplugin.Event) error {
	switch e := e.(type) {
	case *clientplugin.LapLastCorner:
		// post the lap to /laps through c.host.Do, keep the lines that come back
	case *clientplugin.CornerApproaching:
		// c.host.Play the line for e.Turn
	}
	return nil
}

What the host lends a companion

A companion is given a Host and that is its whole reach into the app. There is no way from here to the driver's disk, to another plugin, or to any server but its own plugin's routes with the driver's token.

Do(ctx, method, path, body, header) a request to this plugin's own routes. The path is relative to the mount — /laps, /cues?stint=… — and a path that would leave it is refused before anything is sent
Play(ctx, audio, contentType) a clip, through whichever running companion makes sound. The latest wins: a line for the corner behind the car is worth nothing
Say(ctx, text) words that came without audio, the same way
Audible() whether anything handed to Play would be heard: a player is running and its driver has the sound on. Ask before making a server plugin buy audio
Page(html) the companion's page in the window. Plain HTML, no scripts, replaced whole
Status(text) one short line beside the companion's name
Log() the app's log, with the plugin's name on it and the token kept out of it
Driver() who is signed in: slug and display name
Server() the address the app is paired to, for showing a person where something of theirs lives. Requests do not need it
Setting(key), SetSetting(key, value) what this plugin keeps between runs, its own, stored by the client and never read by it

A companion that makes sound implements Player — Play and Say — and the app routes every other companion's audio to it. If it can be muted, it implements Muted as well, so that Audible tells the truth and a server plugin is not asked for audio nobody will hear.

Start from an example

Two plugins live in examples/, each a complete module-shaped package with its manifest and its tests, meant to be copied into a repository of your own:

What it does Copy it for
examples/source-demo drives a made-up 3 km lap at 20 Hz, for a machine with no simulator a simulator reader: replace Running, Open, Read, Close with the simulator's memory or socket
examples/companion-echo posts each lap's time to its server plugin's /laps, shows the answers on its page, says each corner's number the client half of a server plugin: keep the shape, change what is posted and what is played

A plugin repository holds one package, client-plugin.json at its root, a go.mod requiring this module and, if the plugin is a companion, nothing about the server but the routes it posts to. The team's server adds it to a build by module address and version.

Testing your plugin

clientplugintest is the app for your tests. NewHost is a Host whose plugin routes are an http.Handler you write; it records everything the companion played, said, showed and requested. Deliver gives a companion events the way the app does, only the kinds it asked for. Drain opens a source, reads a number of samples and closes it.

h := clientplugintest.NewHost(t, fakeRoutes)
c := mine.New()
require.NoError(t, c.Start(ctx, h))
require.NoError(t, clientplugintest.Deliver(ctx, c, &clientplugin.LapCompleted{Lap: 1, LapMs: 98765}))
require.Equal(t, "/laps", h.Requests()[0].Path)

Controls on a page, and settings

A companion's page is plain HTML. A form or an input in it with a data-action attribute is a control: the window sends the action's name and the values to the companion's Act when the form is submitted or the input changes, and the companion redraws its page. What a companion wants to keep between runs — a volume, a language — it keeps with Host.SetSetting; the client stores it beside its own configuration, by plugin, and never reads it. That is how a plugin owns its own settings and the client has none.

func (c *voice) page() string {
	return fmt.Sprintf(`<form data-action="volume"><label>Volume</label>
	  <input type="range" name="level" min="0" max="100" value="%s"></form>`, c.host.Setting("volume"))
}

func (c *voice) Act(_ context.Context, action string, values map[string]string) error {
	if action == "volume" {
		if err := c.host.SetSetting("volume", values["level"]); err != nil {
			return err
		}
		c.host.Page(c.page())
	}
	return nil
}

The manifest

Every plugin module carries client-plugin.json at its root. The server reads it, from the module as the proxy serves it, to draw the build page; a module without it is refused as not a client plugin.

{"name": "engineer", "kind": "companion", "title": "Race engineer",
 "description": "Posts each lap to engineer and plays its lines.",
 "interface_version": 1, "server_plugin": "engineer"}

A source's name is the simulator; a companion's is its server plugin, and server_plugin repeats it. interface_version is the InterfaceVersion the plugin was written against; the app refuses a plugin built against another at start.

The events

In the order a stint produces them: stint.started, sampled, corner.passed, lap.last-corner, lap.completed, corner.approaching, stint.finished, server.changed. Each has a type; a companion switches on the type. lap.last-corner is raised when every corner of the lap is measured and the straight to the line remains — the moment a lap should be posted, so the straight pays for the answer. A Corner is what the client measured on this lap and nothing compared: comparison is the server plugin's job.

Dependencies

The standard library and github.com/pacenote-sim/protocol. Nothing else, enforced by lint.

Testing it

make runs what CI runs: format, build, vet, lint, tests with the race detector, coverage over 90 %, benchmarks once, go mod tidy a no-op, and govulncheck. TESTING.md has what the suite asserts, what the contract costs on the hot path, and how to test a plugin of your own.

Licence

Apache-2.0 — see LICENSE. Like the server-side plugin contract, so that a plugin author may license their plugin however they like. Note that a plugin compiled into the client, which is GPL-3, has to be GPL-compatible for that build to be distributed.

Documentation

Overview

Package clientplugin is the contract between the Pacenote client — the telemetry app a driver runs beside the simulator — and the plugins compiled into it.

The client is one executable that a team's server builds: the operator picks the plugins, the server generates a main that imports them all and compiles it. Go has no run-time plugins on Windows, so a client plugin is a Go module whose package registers itself when imported:

func init() { clientplugin.RegisterSource(&iracing{}) }

and carries a client-plugin.json at its root saying what it is. This module holds what both sides agree on and nothing else: the two things a plugin can be, what the app lends a plugin, the sample a simulator reader produces, the events the app raises, the registry, and the manifest.

Two kinds of plugin

A Source reads one simulator and produces a Sample at the simulator's own rate. One source runs at a time: the one whose simulator is running.

A Companion is the client half of a server plugin. It subscribes to the app's events and talks to its own server plugin's routes under /plugin/<name>/, with the driver's token, through the Host the app lends it. It cannot reach anything else. It is started when the server lists its plugin and stopped when the plugin disappears.

Versions

InterfaceVersion is what a plugin was compiled against and what the app checks at start. It moves only when a type or method here changes shape.

Dependencies

The standard library and the wire protocol, so that a plugin author depends on nothing they did not already need.

Index

Constants

View Source
const InterfaceVersion = 1

InterfaceVersion is the shape of this contract. A plugin's manifest says which version it was written against; the app refuses a plugin built against another at start, with a sentence, rather than misreading it later.

It moves when a type or method a plugin implements or calls changes shape. Adding an event kind or an optional field does not move it: a plugin that does not know a kind never asks for it, and a zero field means "not measured".

View Source
const ManifestName = "client-plugin.json"

ManifestName is the file at the root of a plugin's module that says what it is. The server reads it, from the module as the proxy serves it, to draw the build page; the app never does, because by the time the app runs the plugin is compiled in and speaks for itself.

View Source
const MaxManifestBytes = 64 << 10

MaxManifestBytes is the largest manifest that is read.

Variables

View Source
var Default = NewRegistry()

Default is the registry plugins register into and the app reads.

View Source
var ErrNoManifest = errors.New("no " + ManifestName + ": not a client plugin")

ErrNoManifest reports that a directory holds no client-plugin.json: it is not a client plugin.

Functions

func RegisterCompanion

func RegisterCompanion(c Companion)

RegisterCompanion adds a companion to Default. Call it from init().

func RegisterSource

func RegisterSource(s Source)

RegisterSource adds a source to Default. Call it from init().

Types

type Actor

type Actor interface {
	Act(ctx context.Context, action string, values map[string]string) error
}

Actor is what a companion implements when its page has controls. A page is plain HTML; a form or an input in it with a data-action attribute is wired by the window to this: submitting the form, or changing the input, calls Act with the action's name and the values. The companion answers by redrawing its page. That is how a plugin owns its own settings — a volume slider on the voice companion's page — without the client knowing what a volume is.

<form data-action="volume"><input type="range" name="level" value="80"></form>
<select data-action="voice" name="mode">…</select>

type Companion

type Companion interface {
	// Name is the server plugin this is the client half of, exactly as GET /me
	// lists it: "engineer", "visual-telemetry".
	Name() string
	// Wants is the events to deliver. A kind not listed is never delivered;
	// a kind this contract version does not know is ignored.
	Wants() []EventKind
	// Start is called with the Host the companion keeps for its lifetime.
	// Returning an error leaves the companion stopped, and the app shows why.
	Start(ctx context.Context, h Host) error
	// Notify is one event. It should return quickly; anything that waits on
	// the network belongs in a goroutine the companion runs under the ctx
	// it was started with, so that Stop can end it. An error is logged by the
	// app with the companion's name and changes nothing else: the next event
	// is still delivered.
	Notify(ctx context.Context, e Event) error
	// Stop is called once after the last Notify. Everything the companion
	// started must have ended when it returns.
	Stop() error
}

Companion is the client half of a server plugin. It exists so that a server plugin can have a part running beside the simulator — measuring, posting, playing what comes back — without the client knowing what any particular plugin does.

The app starts a companion when GET /me lists a running server plugin with the companion's Companion.Name, and stops it when the plugin disappears from that list or the app closes. Between those, the app delivers the events the companion asked for, one at a time and in order, from a goroutine that belongs to the companion: a slow Notify delays this companion's next event and nobody else's.

type Corner

type Corner struct {
	Turn    int `json:"turn"`
	ApexPct int `json:"apex_pct"`
	// ApexKmh is the speed at the apex; MinKmh the lowest in the corner, which
	// is the apex speed unless the driver slowed past the apex; ExitKmh the
	// speed where the throttle was fully back on.
	ApexKmh int `json:"apex_kmh,omitempty"`
	MinKmh  int `json:"min_kmh,omitempty"`
	ExitKmh int `json:"exit_kmh,omitempty"`
	// BrakeAtPct is where braking began, in thousandths of the lap.
	BrakeAtPct int `json:"brake_at_pct,omitempty"`
	// PeakBrakePct is the hardest the pedal was pressed for this corner;
	// TurnInBrakePct the pressure at turn-in; BrakeAtApex the pressure still
	// on at the apex.
	PeakBrakePct   int `json:"peak_brake_pct,omitempty"`
	TurnInBrakePct int `json:"turn_in_brake_pct,omitempty"`
	BrakeAtApex    int `json:"brake_at_apex,omitempty"`
	// ThrottleLag is the distance from the apex to the throttle coming back, in
	// thousandths of the lap.
	ThrottleLag int `json:"throttle_lag,omitempty"`
	// GearAtApex is the gear at the apex.
	GearAtApex int `json:"gear_at_apex,omitempty"`
	// Pattern is the protocol's name for the shape of the mistake, when the
	// detector named one.
	Pattern wire.CornerPattern `json:"pattern,omitempty"`
}

Corner is one corner of a lap as the client measured it, with nothing compared against anything. Turn is from 1 in the order driven, counting the corners the detector found. Positions are in thousandths of the lap, speeds in whole km/h, pressures in percent. A zero is "not measured".

func (Corner) Wire

func (c Corner) Wire() wire.Corner

Wire is this corner as the server's lap upload carries it. The server's shape has room for a comparison; the client makes none, so those fields are zero, which the protocol defines as "compared against nothing".

type CornerApproaching

type CornerApproaching struct {
	At    time.Time
	Stint Stint
	Lap   int
	// Turn and ApexPct name the corner, as the last lap's measurement numbered it.
	Turn    int
	ApexPct int
	// MetresToApex is the distance to it, or zero when the track length is unknown.
	MetresToApex int
}

CornerApproaching is KindCornerApproaching.

func (*CornerApproaching) Kind

func (*CornerApproaching) Kind() EventKind

Kind implements Event.

func (*CornerApproaching) When

func (e *CornerApproaching) When() time.Time

When implements Event.

type CornerPassed

type CornerPassed struct {
	At     time.Time
	Stint  Stint
	Lap    int
	Corner Corner
}

CornerPassed is KindCornerPassed.

func (*CornerPassed) Kind

func (*CornerPassed) Kind() EventKind

Kind implements Event.

func (*CornerPassed) When

func (e *CornerPassed) When() time.Time

When implements Event.

type Event

type Event interface {
	// Kind names the event.
	Kind() EventKind
	// When is when it happened, by the simulator's clock where there is one.
	When() time.Time
}

Event is what the app delivers to a companion. Every kind has its own type below; a companion switches on the type, not the kind:

switch e := e.(type) {
case *clientplugin.LapLastCorner: …
}

type EventKind

type EventKind string

EventKind names an event the app raises. A companion lists the kinds it wants; the app delivers those and no others.

const (
	// KindStintStarted: the driver is on track in a car for the first time
	// this sitting. Carries a [StintStarted].
	KindStintStarted EventKind = "stint.started"
	// KindSampled: one reading of the simulator, at its rate. Carries a
	// [Sampled]. Ask for it only to do something on every sample.
	KindSampled EventKind = "sampled"
	// KindCornerPassed: the detector has closed a corner and measured it.
	// Carries a [CornerPassed].
	KindCornerPassed EventKind = "corner.passed"
	// KindLapLastCorner: the last corner of the lap is behind the car and every
	// corner of the lap is measured; the straight to the line remains. Carries
	// a [LapLastCorner]. This is when engineer wants the lap.
	KindLapLastCorner EventKind = "lap.last-corner"
	// KindLapCompleted: the line. Carries a [LapCompleted] with the trace.
	KindLapCompleted EventKind = "lap.completed"
	// KindCornerApproaching: a corner's apex is a set distance ahead. Carries a
	// [CornerApproaching]. This is when a line for the corner is played.
	KindCornerApproaching EventKind = "corner.approaching"
	// KindStintFinished: the sitting is over. Carries a [StintFinished].
	KindStintFinished EventKind = "stint.finished"
	// KindServerChanged: GET /me was read again. Carries a [ServerChanged].
	// Companions are started and stopped by the app on this; one that wants
	// it too can read the driver's entitlements from it.
	KindServerChanged EventKind = "server.changed"
)

The v1 events, in the order a stint produces them.

func Kinds

func Kinds() []EventKind

Kinds is every event kind this version knows, in the order a stint produces them.

func (EventKind) Known

func (k EventKind) Known() bool

Known reports whether k is a kind this version knows.

type Host

type Host interface {
	// Do sends a request to the companion's own server plugin. path is relative
	// to the plugin's mount — "/laps", "/cues?stint=…" — and the app supplies
	// the server, the mount and the driver's token. A path that would leave the
	// mount is an error before anything is sent. The caller closes the body.
	Do(ctx context.Context, method, path string, body io.Reader, header http.Header) (*http.Response, error)
	// Play plays audio through the running companion that is a [Player]:
	// the voice companion. contentType is audio/mpeg or audio/wav. The latest
	// call wins: a clip still playing when the next arrives is cut, because a
	// line for the corner behind the car is worth nothing. Without a Player
	// running, nothing is heard and the app logs the loss.
	Play(ctx context.Context, audio []byte, contentType string) error
	// Say speaks words that came without audio, through the same Player.
	Say(ctx context.Context, text string) error
	// Audible reports that something handed to Play would actually be heard:
	// a Player is running and its driver has sound turned on. It is false with
	// no Player at all, and false while one is muted.
	//
	// It is for deciding what to ask for, not what to send. Audio is bought by
	// the character at somebody's vendor, and a companion whose server half
	// would buy some should say first that nobody is listening.
	Audible() bool
	// Page shows HTML in the app's window, under a tab with the companion's
	// title. The last call replaces the page. Scripts are not run.
	Page(html string)
	// Status is one short line beside the companion's name in the app's
	// window: "3 lines ready", "waiting for the coach". The last call replaces
	// it; an empty string clears it.
	Status(text string)
	// Log is the app's log with the companion's name attached. The driver's
	// token never appears in it, whatever a companion logs.
	Log() *slog.Logger
	// Driver is who is signed in, as GET /me said: slug and display name.
	Driver() (slug, name string)
	// Server is the address the app is paired to, "https://pacenote.example".
	// It is for showing a person where something of theirs lives on the
	// server; requests go through Do, which needs no address.
	Server() string
	// Setting is a value this companion kept with SetSetting, or "" when it
	// kept none. Settings are the companion's own, by key, and survive a
	// restart: the app keeps them beside its own configuration.
	Setting(key string) string
	// SetSetting keeps a value. An empty value forgets the key.
	SetSetting(key, value string) error
}

Host is what the app lends a companion, and all of it. A companion that needs something not here needs a change to this contract, not a way round it.

type Kind

type Kind string

Kind is what a plugin is.

const (
	KindSource    Kind = "source"
	KindCompanion Kind = "companion"
)

The two kinds.

type LapCompleted

type LapCompleted struct {
	At    time.Time
	Stint Stint
	Lap   int
	// LapMs is the lap time. LapKind is the protocol's: clean, or not.
	LapMs   int
	LapKind wire.Kind
	// StartedAt is when the lap began.
	StartedAt time.Time
	// Trace is the lap at the source's full rate, in wire scaling, from the
	// line to the line. A companion posting it resamples to its own limit.
	Trace []wire.TracePoint
	// SectorMs is each sector's time, in the order of Stint.Sectors, or nil.
	SectorMs []int
	Corners  []Corner
}

LapCompleted is KindLapCompleted.

func (*LapCompleted) Kind

func (*LapCompleted) Kind() EventKind

Kind implements Event.

func (*LapCompleted) When

func (e *LapCompleted) When() time.Time

When implements Event.

type LapLastCorner

type LapLastCorner struct {
	At    time.Time
	Stint Stint
	Lap   int
	// ElapsedMs is the lap time so far.
	ElapsedMs int
	Corners   []Corner
}

LapLastCorner is KindLapLastCorner: every corner of the lap, measured, with the line still ahead.

func (*LapLastCorner) Kind

func (*LapLastCorner) Kind() EventKind

Kind implements Event.

func (*LapLastCorner) When

func (e *LapLastCorner) When() time.Time

When implements Event.

type Manifest

type Manifest struct {
	// Name is the plugin's name: for a source, the simulator ("iracing"); for
	// a companion, the server plugin it belongs to ("engineer"). It is what
	// Source.Name or Companion.Name returns.
	Name string `json:"name"`
	// Kind is source or companion.
	Kind Kind `json:"kind"`
	// Title is what a person sees on the build page.
	Title string `json:"title"`
	// Description is a sentence for the build page. Optional.
	Description string `json:"description,omitempty"`
	// InterfaceVersion is the [InterfaceVersion] this plugin was written against.
	InterfaceVersion int `json:"interface_version"`
	// ServerPlugin is the server plugin a companion belongs to. Required for a
	// companion, where it equals Name; not allowed on a source.
	ServerPlugin string `json:"server_plugin,omitempty"`
}

Manifest is client-plugin.json.

{"name": "engineer", "kind": "companion", "title": "Race engineer",
 "description": "Posts each lap to engineer and plays its lines.",
 "interface_version": 1, "server_plugin": "engineer"}

func LoadManifest

func LoadManifest(dir string) (Manifest, error)

LoadManifest reads and validates the manifest in dir.

func LoadManifestFS

func LoadManifestFS(fsys fs.FS, dir string) (Manifest, error)

LoadManifestFS reads and validates the manifest in dir of fsys — a module the server unpacked, a test fixture.

func (Manifest) Validate

func (m Manifest) Validate() error

Validate checks that the manifest describes a plugin this contract can hold. Every error is a sentence the build page can show.

type Muted

type Muted interface {
	// Audible is false while the driver has the sound turned off, or down to
	// the words alone.
	Audible() bool
}

Muted is a Player that knows whether anything would be heard. A Player that does not implement it is taken to be audible whenever it is running, which is what a player with no volume of its own is.

type Player

type Player interface {
	// Play plays audio through the driver's speakers at the driver's volume.
	// contentType is audio/mpeg or audio/wav. The latest call wins.
	Play(ctx context.Context, audio []byte, contentType string) error
	// Say speaks words that came without audio, however it can.
	Say(ctx context.Context, text string) error
}

Player is what a companion implements when it is the one that makes sound: the voice companion. The app routes every companion's Host.Play and Host.Say to the running companion that implements it. Without one, nothing is heard and the log says so: sound is a plugin, like everything else that talks to the world.

type Registry

type Registry struct {
	// contains filtered or unexported fields
}

Registry holds what was compiled in. Plugins register into Default from their init(); the app reads it once at start. A Registry of one's own is for tests.

func NewRegistry

func NewRegistry() *Registry

NewRegistry is an empty registry.

func (*Registry) AddCompanion

func (r *Registry) AddCompanion(c Companion)

AddCompanion adds a companion. Two companions for one server plugin is refused the same way.

func (*Registry) AddSource

func (r *Registry) AddSource(s Source)

AddSource adds a source. Two sources with one name is a build that cannot be right — two plugins both claiming "iracing" — so it panics at start, where the operator building the client sees it, rather than picking one.

func (*Registry) Companion

func (r *Registry) Companion(name string) (Companion, bool)

Companion finds a companion by its server plugin's name.

func (*Registry) Companions

func (r *Registry) Companions() []Companion

Companions is every registered companion, by name.

func (*Registry) Source

func (r *Registry) Source(name string) (Source, bool)

Source finds a source by simulator name.

func (*Registry) Sources

func (r *Registry) Sources() []Source

Sources is every registered source, by name.

type Sample

type Sample struct {
	// At is when the simulator produced this reading.
	At time.Time

	// Session is the kind of session the simulator is in.
	Session wire.SessionType
	// SessionName is the simulator's own name for it ("PRACTICE", "RACE").
	SessionName string
	// Track is the circuit's display name; TrackID the simulator's stable
	// identifier for it ("spa gp"). TrackLengthM is metres round it, or zero.
	Track        string
	TrackID      string
	TrackLengthM int
	// Car is the car's display name, CarClass its class ("gt3").
	Car      string
	CarClass string
	// Sectors are the sector starts as fractions of the lap, ascending, the
	// first 0. Empty when the simulator publishes none.
	Sectors []float64
	// SetupOpen reports that the car's setup can be changed in this session.
	SetupOpen bool

	// OnTrack is false in the garage or spectating; OnPitRoad true on the pit lane.
	OnTrack   bool
	OnPitRoad bool
	// Lap is the simulator's lap counter. LapDistPct is the position round the
	// lap as a fraction, 0…1.
	Lap        int
	LapDistPct float64
	// SpeedKmh is the speed. Throttle and Brake are pedal positions, 0…1.
	SpeedKmh float64
	Throttle float64
	Brake    float64
	// Gear: -1 reverse, 0 neutral, 1 and up. RPM as the simulator gives it.
	Gear int
	RPM  float64
	// SteerDeg is the steering angle in degrees, left negative.
	SteerDeg float64
	// LatG and LongG are accelerations in g.
	LatG  float64
	LongG float64
	// Lat and Lon are the car's position in degrees, both zero when the
	// simulator publishes none.
	Lat float64
	Lon float64

	// LastLapMs and BestLapMs are the simulator's own lap times, or zero.
	LastLapMs int
	BestLapMs int
	// Incidents is the simulator's incident count, where it has one.
	Incidents int
	// Position is the car's place in the session, from 1, or zero.
	Position int
	// FuelL is fuel on board in litres; FuelPerHourL the burn rate.
	FuelL        float64
	FuelPerHourL float64
	// Weather and Tyres are what the simulator publishes of each.
	Weather Weather
	Tyres   Tyres

	// Flag is the session flag. LapsTotal is the race length in laps, or zero.
	Flag      wire.Flag
	LapsTotal int
	// Field is every car in the session, the driver's own included. Empty
	// when the simulator publishes none, or on the samples between the ones a
	// source chooses to fill it on.
	Field []wire.FieldCar
}

Sample is one reading of the simulator, in the units a person uses: km/h, degrees, litres, fractions of a lap. A Source fills what its simulator publishes and leaves the rest zero; the app and the companions never see the simulator's own units or layout.

Everything that describes the session rather than the instant — track, car, sectors, session type — is repeated on every sample. It costs nothing, and it means a source has no second thing to publish and the app has no state to keep in step with it.

func (Sample) LivePoint

func (s Sample) LivePoint(offsetMs int) wire.LivePoint

LivePoint is this sample as POST /live carries it: the trace point and the three things that only make sense live.

func (Sample) TracePoint

func (s Sample) TracePoint(offsetMs int) wire.TracePoint

TracePoint is this sample as the wire carries it: whole km/h, pedals in percent, position in thousandths of the lap, accelerations in hundredths of a g, coordinates in hundred-thousandths of a degree. offsetMs is the time since the lap began.

type Sampled

type Sampled struct {
	Stint  Stint
	Sample Sample
	// Lap is the lap counter the app keeps, which is the one every other event
	// uses; the sample's own is the simulator's.
	Lap int
	// OffsetMs is the time since this lap began.
	OffsetMs int
}

Sampled is KindSampled.

func (*Sampled) Kind

func (*Sampled) Kind() EventKind

Kind implements Event.

func (*Sampled) When

func (e *Sampled) When() time.Time

When implements Event.

type ServerChanged

type ServerChanged struct {
	At time.Time
	Me wire.Me
}

ServerChanged is KindServerChanged.

func (*ServerChanged) Kind

func (*ServerChanged) Kind() EventKind

Kind implements Event.

func (*ServerChanged) When

func (e *ServerChanged) When() time.Time

When implements Event.

type Source

type Source interface {
	// Name is the simulator, lower-case and stable: "iracing", "assetto-corsa".
	// It is what the stint's Sim field says and what a recording is filed under.
	Name() string
	// Running reports whether the simulator is up and publishing. It is called
	// often, from one goroutine, and must be cheap: a check that a shared-memory
	// name exists, not an attach.
	Running() bool
	// Open attaches to the simulator. It is called once before the first Read
	// and only after Running said yes.
	Open(ctx context.Context) error
	// Read blocks until the simulator publishes the next reading, or ctx ends,
	// and returns it. An error ends the session: the app closes the source and
	// goes back to polling. A simulator that has gone away is an error, not a
	// zero sample.
	Read(ctx context.Context) (Sample, error)
	// Close detaches. It is called once after the last Read, whatever Read
	// returned.
	Close() error
}

Source reads one simulator. The app polls Source.Running on every registered source while nothing is being read, opens the first that says yes, reads it until Source.Read fails, closes it, and polls again.

A source is one simulator and one way of reading it. It converts what the simulator publishes into a Sample and does nothing else: no lap detection, no filtering, no rate limiting. The app does those, the same way for every simulator.

type Stint

type Stint struct {
	// ID is the stint's id, the one the server knows it by.
	ID string
	// Sim, Track, TrackID, Car, CarClass and Session are as the source reported them.
	Sim      string
	Track    string
	TrackID  string
	Car      string
	CarClass string
	Session  wire.SessionType
	// TrackLengthM is metres round the circuit, or zero when unknown.
	TrackLengthM int
	// Sectors are the sector starts as fractions of the lap.
	Sectors []float64
	// SetupOpen reports that the car's setup can be changed in this session.
	SetupOpen bool
	// StartedAt is when the stint began.
	StartedAt time.Time
}

Stint describes the sitting an event belongs to. It is repeated on every event that concerns a stint, so a companion never has to remember it.

type StintFinished

type StintFinished struct {
	At    time.Time
	Stint Stint
	// Laps is how many laps were completed.
	Laps int
}

StintFinished is KindStintFinished.

func (*StintFinished) Kind

func (*StintFinished) Kind() EventKind

Kind implements Event.

func (*StintFinished) When

func (e *StintFinished) When() time.Time

When implements Event.

type StintStarted

type StintStarted struct {
	At    time.Time
	Stint Stint
}

StintStarted is KindStintStarted.

func (*StintStarted) Kind

func (*StintStarted) Kind() EventKind

Kind implements Event.

func (*StintStarted) When

func (e *StintStarted) When() time.Time

When implements Event.

type Tyres

type Tyres struct {
	LF, RF, LR, RR float64
}

Tyres is the temperature of each tyre in °C: left front, right front, left rear, right rear.

type Weather

type Weather struct {
	// Skies: 0 clear, 1 partly cloudy, 2 mostly cloudy, 3 overcast.
	Skies int
	// Wetness: 0 dry, rising with the water on the track, in the simulator's steps.
	Wetness int
	// WindKmh, Humidity (percent), TrackTempC, AirTempC.
	WindKmh    float64
	Humidity   float64
	TrackTempC float64
	AirTempC   float64
}

Weather is what the simulator says about the sky and the surface.

Directories

Path Synopsis
Package clientplugintest is what a plugin author tests against: a Host that records what a companion did, a way to deliver events the way the app does, and a way to drain a source.
Package clientplugintest is what a plugin author tests against: a Host that records what a companion did, a way to deliver events the way the app does, and a way to drain a source.
examples
companion-echo
Package echo is a companion to start one from.
Package echo is a companion to start one from.
source-demo
Package demo is a source to start a simulator reader from.
Package demo is a source to start a simulator reader from.

Jump to

Keyboard shortcuts

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