WebSocket¶
Eine dauerhafte Verbindung von deinem Programm zum Timer: Der Timer schickt jedes Event live, und du kannst über dieselbe Verbindung Befehle schicken. Gut für alles, was mitlaufen soll: Overlay, Anzeige, Bot, Auswertung am Platz.
ws://<pi>:5800/api/v1/ws?key=lt_…
Der Schlüssel geht als ?key= (Browser können beim WebSocket keine
Header setzen) oder als Authorization-/X-API-Key-Header. Ohne gültigen Schlüssel lehnt der
Server den Verbindungsaufbau mit 401 ab.
Ablauf¶
sequenceDiagram
participant P as Dein Programm
participant T as Laptimer
P->>T: verbinden (/api/v1/ws?key=…)
T-->>P: hello (Zustand, Befehle)
T-->>P: event session_started
P->>T: {"id": 1, "cmd": "channels.list"}
T-->>P: reply id 1
T-->>P: event pilot_pass
T-->>P: event pilot_pass
- Gleich nach dem Verbinden kommt
hellomit dem ganzen Zustand. Ab diesem Moment verpasst du nichts: der Server abonniert die Events, bevor er den Zustand liest. - Danach kommt jedes Event, in der Reihenfolge, in der es entsteht.
- Dazwischen beantwortet er deine Befehle mit
reply, einen nach dem anderen.
Alle Nachrichten sind JSON-Text; welche es ist, sagt type.
Nachrichten vom Timer¶
| type | wann | Inhalt |
|---|---|---|
hello |
einmal, gleich nach dem Verbinden | api (Version der API, jetzt 1), version (des Laptimers), state (wie der Befehl state, siehe Datenformate), commands (alle Befehlsnamen) |
event |
jedes Ereignis | event, seq, time_us, data, siehe Events |
reply |
Antwort auf einen deiner Befehle | id, ok, dann data oder status + error |
lagged |
dein Programm hat zu langsam gelesen | missed: so viele Events sind verloren |
{"type": "hello", "api": 1, "version": "0.2.0", "state": {…}, "commands": ["ping", "state", …]}
Befehle schicken¶
← {"id": 1, "cmd": "session.start", "args": {"race": true}}
→ {"type": "reply", "id": 1, "ok": true, "data": {…}}
← {"id": 2, "cmd": "session.start"}
→ {"type": "reply", "id": 2, "ok": false, "status": 400, "error": "Es läuft schon eine Session – erst stoppen."}
idist frei wählbar (Zahl oder Text) und kommt in der Antwort zurück; so ordnest du Antworten zu.argsdarf fehlen.statusbei Fehlern wie per HTTP: 400 abgelehnt, 404 unbekannter Befehl. Eine Nachricht, die gar kein Befehl ist, bekommt 400 mitid: null.- Während ein Befehl läuft, warten die Events dieser Verbindung. Löst er selbst Events aus
(z. B.
session.start→session_started), kommt also erst dasreply, dann die Events. Andere Verbindungen bekommen die Events unabhängig davon.
Verbindung halten¶
- Abbruch: Der Timer merkt sich keine Clients. Nach einem Abbruch (WLAN weg, Neustart,
Update) einfach neu verbinden; das neue
hellobringt den aktuellen Stand, Events von dazwischen kommen nicht nach. Ein Neustart des Timers setztseqauf 1 zurück. - Lücken: Steigt
sequm mehr als 1, oder kommtlagged, fehlen Events. Dann den Zustand neu holen (Befehlstate, bei einer Sessionsessions.get). - Ping/Pong beantwortet der Server von selbst; er schickt selbst keine Pings.
- Wer Events nachgeliefert haben will (Server, der den Timer nicht immer erreicht), nimmt die Server-Verbindung.
Beispiele¶
Python (Paket websockets): alle Events mitlesen, ein Rennen starten, am Ende bestätigen
import asyncio, json, websockets
async def main():
async with websockets.connect("ws://pi:5800/api/v1/ws?key=lt_…") as ws:
hello = json.loads(await ws.recv())
print("verbunden, Session:", hello["state"]["session"])
await ws.send(json.dumps({"id": 1, "cmd": "session.start", "args": {"race": True, "laps": 3}}))
async for text in ws:
msg = json.loads(text)
if msg["type"] == "event" and msg["event"] == "pilot_pass" and msg["data"]["kind"] == "lap":
d = msg["data"]
print(f'{d["pilot"]}: Runde {d["lap"]} in {d["lap_time_s"]:.2f} s')
elif msg["type"] == "event" and msg["event"] == "session_stopped" and msg["data"]["needs_confirmation"]:
await ws.send(json.dumps({"id": 2, "cmd": "session.confirm"}))
asyncio.run(main())
JavaScript (Browser oder Node ≥ 22):
const ws = new WebSocket('ws://pi:5800/api/v1/ws?key=lt_…')
ws.onmessage = (e) => {
const m = JSON.parse(e.data)
if (m.type === 'event') console.log(m.event, m.data)
}
ws.onopen = () => ws.send(JSON.stringify({ id: 1, cmd: 'channels.set', args: { index: 0, pilot: 'NITRO' } }))
Neuverbinden, Zuordnen der Antworten und Lücken nehmen dir die Clients ab.