fpv-laptimer-client (Python)
Python-Client für die API des FPV-Laptimers
(/api/v1, ausführlich in der Doku). Befehle laufen über HTTP und brauchen nur die
Standardbibliothek; für Events (WebSocket) braucht es zusätzlich das Paket websockets.
Python ab 3.10.
Installieren
pip install fpv-laptimer-client # nur Befehle (HTTP)
pip install "fpv-laptimer-client[events]" # dazu Events über den WebSocket
Aus dem Repo statt von PyPI: pip install "./clients/python[events]".
Oder ohne Installation: laptimer_client/__init__.py als laptimer_client.py neben das
eigene Script kopieren. Befehle funktionieren dann sofort (auch auf dem Pi in Hook-Scripts);
für Events zusätzlich pip install websockets (ab Version 13).
Einen API-Schlüssel legst du auf der Seite API & Hooks → Schlüssel an.
Kurz
from laptimer_client import Laptimer, LaptimerError
lt = Laptimer("http://pi:5800", key="lt_…")
lt.assign_channels(["NITRO", "ACE", "BOB"], create=True) # Kanal 0, 1, 2
lt.start_session(race=True, laps=3, countdown_s=5)
…
lt.confirm() # Ergebnis bestätigen
- Jede Methode gibt das JSON des Servers als
dict/listzurück (Felder wie in Datenformate). - Argumente mit
Nonewerden nicht mitgeschickt, d. h. „bleibt, wie es ist“ bzw. „Standard“. Ein ausdrücklichesnull(z. B. „kein Pilot auf diesem Kanal“) istNOBODY:lt.set_channel(2, NOBODY). - Fehler werfen
LaptimerError:e.messageist die (deutsche) Meldung des Servers zum direkten Anzeigen,e.statusder HTTP-Status (400 falsche Argumente, 401 Schlüssel falsch, 404 unbekannter Befehl; über den WebSocket dieselben Zahlen;None, wenn der Server nicht erreichbar ist). Laptimer()ohne Angaben nimmtLAPTIMER_API_URL(Standardhttp://localhost:5800) undLAPTIMER_API_KEYaus der Umgebung. Die Adresse darf auchpi:5800oder…/api/v1sein.
Beispiele
Rennen mit 3 Runden starten, jede Runde ausgeben, Ergebnis bestätigen
Mit Handler-Funktionen, ohne asyncio (lt.on(...) und lt.run_forever()):
from laptimer_client import Laptimer
lt = Laptimer("http://pi:5800", key="lt_…")
@lt.on("connected") # nach dem Verbinden: erst dann starten, damit nichts fehlt
def start(ev):
if ev.data["session"] is None:
lt.start_session(race=True, laps=3)
@lt.on("pilot_pass")
def lap(ev):
d = ev.data
if d["kind"] == "lap":
print(f'{d["pilot"]}: Runde {d["lap"]} in {d["lap_time_s"]:.2f} s, Platz {d["position"]}')
@lt.on("session_stopped")
def stopped(ev):
print("Sieger:", ev.data["winner"])
if ev.data["needs_confirmation"]:
lt.confirm(ev.data["session_id"])
lt.stop() # run_forever() beenden
lt.run_forever() # bis lt.stop() oder Strg+C; verbindet sich neu, wenn nötig
"*" als Event-Name bekommt jedes Event. Handler dürfen auch async def sein. Eine
Ausnahme im Handler wird ausgegeben, die Schleife läuft weiter.
Dasselbe mit asyncio
import asyncio
from laptimer_client import Laptimer
lt = Laptimer("http://pi:5800", key="lt_…")
async def main():
async for ev in lt.events("pilot_pass", "session_stopped"):
if ev.event == "pilot_pass" and ev.data["kind"] == "lap":
print(ev.data["pilot"], ev.data["lap"], ev.data["lap_time_s"])
elif ev.event == "session_stopped":
break
asyncio.run(main())
lt.events(*namen) liefert die Events (ohne Namen: alle) und verbindet sich nach einem
Abbruch selbst neu (reconnect=False beendet die Schleife stattdessen).
Befehle über dieselbe WebSocket-Verbindung (ohne Neuverbinden):
async with lt.connect() as conn:
print(conn.initial_state["session"]) # Zustand beim Verbinden (aus „hello“)
await conn.start_session(race=True, laps=3) # alle Methoden wie oben, mit await
ev = await conn.wait_for("session_stopped", timeout=600)
await conn.confirm()
async for ev in conn: # alle weiteren Events
...
Ein Event ist ein Event
| Feld | Inhalt |
|---|---|
event |
Name, z. B. pilot_pass (Liste in Events) |
seq |
laufende Nummer ab Serverstart (Lücke = Events verpasst) |
time_us |
wann, µs seit 1970 |
data |
die Felder des Events als dict |
Zwei Events kommen vom Client selbst (seq = 0): connected nach jedem (Neu-)Verbinden,
data = der Zustand (wie state()), und lagged, wenn der Client zu langsam war
(data["missed"] Events fehlen; Zustand neu holen).
Hook-Script
Hooks (Seite API & Hooks → Hooks) starten bei einem Event ein Script; das Event steht in
LAPTIMER_JSON. Event.from_env() liest es:
#!/usr/bin/env python3
from pathlib import Path
from laptimer_client import Event, Laptimer
ev = Event.from_env()
if ev.event == "session_stopped" and ev.data["race"] and ev.data["reason"] == "finished":
lt = Laptimer("http://localhost:5800", key=Path("api_key.txt").read_text().strip())
lt.confirm(ev.data["session_id"]) # alle im Ziel: Ergebnis von selbst bestätigen
Ins Script-Ordner (Standard server/data/hooks/) gehören dann das Script (chmod +x),
laptimer_client.py und api_key.txt. Hooks laufen im Script-Ordner.
Fertige Beispiele in examples/:
race.py: Piloten setzen, Rennen starten, Countdown und Runden ausgeben, am Ende bestätigen oder verwerfen (python examples/race.py --url http://pi:5800 --key lt_… --laps 3 NITRO ACE).hook_autoconfirm.py: Hook, der fertige Rennen bestätigt und den Sieger in eine Datei schreibt.
Methoden
Alle gibt es auf Laptimer (HTTP, blockierend) und auf Connection (WebSocket, mit
await). Kanäle sind Indizes ab 0, Zeiten µs seit 1970.
| Methode | Befehl | Ergebnis |
|---|---|---|
ping() |
ping |
{"pong", "now_us"} |
state() |
state |
laufende Session, unconfirmed, Kanäle, Piloten, Boards |
stats() |
stats |
Bestzeiten, Rennen, Siege pro Pilot |
channels() |
channels.list |
Liste der Kanäle |
set_channel(index, pilot=None, *, create, label, enabled, frequency) |
channels.set |
der Kanal. pilot: Callsign, Pilot-ID oder NOBODY; frequency: "R3" oder MHz |
assign_channels(pilots, create=False) |
channels.assign |
alle Kanäle. pilots: pro Kanal Callsign, ID oder None |
pilots() |
pilots.list |
[{"id", "name", "full_name"}] |
pilot(id) |
pilots.get |
Pilot |
create_pilot(name, full_name="") |
pilots.create |
Pilot |
update_pilot(id, name=None, full_name=None) |
pilots.update |
Pilot |
delete_pilot(id) |
pilots.delete |
{} |
start_session(race=False, *, laps, seconds, countdown_s, start_mode) |
session.start |
Session |
stop_session() |
session.stop |
{"session_id"} |
cancel_session() |
session.cancel |
stoppt und löscht |
confirm(id=None) |
session.confirm |
{"session_id"}; ohne id das wartende Rennen |
discard(id=None) |
session.discard |
löscht die beendete Session |
sessions(limit=20) |
sessions.list |
neueste Sessions |
session(id) |
sessions.get |
Session mit passes |
add_pass(channel, time_us=None, session_id=None) |
passes.add |
{"pass_id", "session_id", "time_us"} |
delete_pass(id) |
passes.delete |
erkannte werden nur markiert |
restore_pass(id) |
passes.restore |
|
command(name, **args) |
jeder | z. B. lt.command("session.start", race=True) |
Nur auf Laptimer: events(*namen, reconnect=True, retry_s=2.0), connect(),
on(event, handler), run_forever(), stop(), ws_url().
Nur auf Connection: hello, initial_state (Zustand beim Verbinden; await conn.state() holt ihn neu), commands,
next_event(timeout=None), wait_for(*namen, timeout=None), close();
async for ev in conn endet, wenn die Verbindung zu ist.
Tests
Gegen einen laufenden Server, nicht den, mit dem ihr gerade fliegt (die Tests starten und löschen
Sessions und legen Piloten an). Ohne LAPTIMER_API_KEY legen sie selbst einen Schlüssel an.
cd clients/python
python3 -m venv /tmp/lt-venv && /tmp/lt-venv/bin/pip install websockets
LAPTIMER_API_URL=http://localhost:5802 /tmp/lt-venv/bin/python -m unittest discover -s tests -v
Ohne websockets laufen nur die HTTP-Tests, die Event-Tests werden übersprungen.
Veröffentlichen (PyPI)
Die Version kommt aus dem Git-Tag und ist unabhängig von der Server-Version. Im Repo steht in
laptimer_client/__init__.py nur der Platzhalter __version__ = "0.0.0.dev0"; der Workflow
schreibt vor dem Bauen die echte Version hinein, pyproject.toml liest sie von dort. Ablauf:
- Tag
py-v<version>pushen, z. B.git tag py-v0.2.0 && git push origin py-v0.2.0(Version nach PEP 440 in Normalform, also0.2.0rc1, nicht0.2.0-rc1). - Der Workflow
.github/workflows/pypi.ymlsetzt die Version, baut sdist und Wheel, prüft sie (twine check, Import aus dem Wheel) und lädt sie per Trusted Publishing (ohne Token) auf PyPI.
Von Hand starten (Actions → PyPI → Run workflow) lädt auf TestPyPI statt PyPI, zum
Ausprobieren, als <letzter py-v-Tag>.dev<Laufnummer> (z. B. 0.2.0.dev14), damit jeder Lauf
eine neue Version hat: pip install --pre -i https://test.pypi.org/simple/ fpv-laptimer-client.
Einmal einrichten: auf pypi.org bzw. test.pypi.org unter Publishing → Add a new pending
publisher das Projekt fpv-laptimer-client, Owner bli-blu-bla, Repo fpv-laptimer, Workflow
pypi.yml, Environment pypi bzw. testpypi eintragen; im GitHub-Repo unter Settings →
Environments die beiden Environments anlegen (bei pypi am besten mit Freigabe durch dich).
Lokal bauen und prüfen:
cd clients/python
python3 -m venv /tmp/lt-build && /tmp/lt-build/bin/pip install build twine
/tmp/lt-build/bin/python -m build # → dist/*.tar.gz, dist/*.whl
/tmp/lt-build/bin/twine check --strict dist/*
LICENSE ist ein Symlink auf die Lizenz im Repo; im Paket landet sie als Datei.
Metadata
Release files for fpv-laptimer-client 0.0.11
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| fpv_laptimer_client-0.0.11.tar.gz | 28.6 kB | With provenance | Details for fpv_laptimer_client-0.0.11.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| fpv_laptimer_client-0.0.11-py3-none-any.whl | Python 3 | none | any | Details for fpv_laptimer_client-0.0.11-py3-none-any.whl |
Total release size: 54.5 kB
Release files / fpv_laptimer_client-0.0.11.tar.gz
| Download URL | fpv_laptimer_client-0.0.11.tar.gz |
|---|---|
| Size | 28.6 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
dd940e7f186c81200bb4f1a6a0a87d00856bf3766738bf84ccba493c1346227f
|
|
BLAKE2b-256 checksum How to use checksums |
16c2af6266074b48d52ed6f9de1524699ec678f63e71b3ebc2bbdf811bd9fca7
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 29, 2026.
Transparency logRelease files / fpv_laptimer_client-0.0.11-py3-none-any.whl
| Download URL | fpv_laptimer_client-0.0.11-py3-none-any.whl |
|---|---|
| Size | 25.9 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
baf39d62483f6b0da88eb70fa92e3e1749bd28a2cd3691eff63b6223c87ce8b4
|
|
BLAKE2b-256 checksum How to use checksums |
4c4005495302a21b810f1ddb18b540da4e04561e371ff8730b0df736ca02486b
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 29, 2026.
Transparency logSecurity for fpv-laptimer-client 0.0.11
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
Full provenance
All 2 files in this release have been uploaded with provenance metadata.
Attestation for fpv_laptimer_client-0.0.11.tar.gz
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 29, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.