* Datenmodell + Alembic-Initialmigration (Plan §4), Seed Saison 2026 * Wertungs-Engine (Plan §3.3), regelbasiert je Kategorie/Saison - Abnahmetest gegen die Ergebnisliste 2026-05-31 * Meisterschaftsberechnung mit Streichresultaten + Tie-Break (PLATZHALTER-Punktetabelle, O-1 offen) * Ingest-API (idempotent, dry_run) inkl. Excel-Leser und Fahrer-Matching/Merge * Zeitmessungs-Endpoint (Batch, idempotent, unzugeordnet/unplausibel) + SSE-Anzeige - Mess-Pi-Client bewusst NICHT enthalten (docs/zeitmessung-endpoint.md) * Frontend: öffentliche Seiten + internes Backend (Jinja2/HTMX, kein Build) * Auslieferung: Docker (multi-arch) + docker-compose sowie Debian-Paket/APT-Repo * 57 Tests grün gegen echte PostgreSQL-Testdatenbank Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
113 lines
4.6 KiB
Markdown
113 lines
4.6 KiB
Markdown
# Endpoint für die Zeitmessung (Lichtschranke)
|
|
|
|
> Der **Mess-Pi-Client selbst ist nicht Teil dieses Projekts** — dieses Dokument
|
|
> beschreibt nur den **Endpoint**, den ein beliebiger Client (der zweite Raspberry
|
|
> Pi an der Lichtschranke) bespielt. Fachlicher Hintergrund: `plan.md` §3.6 / §6.2.
|
|
|
|
## Authentifizierung
|
|
|
|
Alle Geräte-Endpunkte erwarten einen **Geräte-Token** (nicht den Auswerter-Login):
|
|
|
|
```
|
|
Authorization: Bearer <geraete-token>
|
|
```
|
|
|
|
Token anlegen (interne Admin-Session):
|
|
|
|
```
|
|
POST /api/v1/admin/geraete
|
|
{ "bezeichnung": "lichtschranke-ziel-01", "typ": "lichtschranke" }
|
|
→ 201 { "id": 1, "bezeichnung": "...", "token": "oamc_dev_…" } # Token nur hier einmalig
|
|
```
|
|
|
|
Token neu ausgeben: `POST /api/v1/admin/geraete/{id}/token` ·
|
|
Gerät sperren: `POST /api/v1/admin/geraete/{id}/sperren`
|
|
|
|
## Ablauf auf dem Client (empfohlen)
|
|
|
|
1. `POST /api/v1/geraete/heartbeat` alle ~15 s → Statusanzeige + `aktives_turnier_id`
|
|
2. `GET /api/v1/geraete/konfiguration` → Entprellzeit, Messpunkte, Plausibilitätsgrenzen
|
|
3. `GET /api/v1/turniere/aktiv/startliste` (ETag-fähig) → Startreihenfolge für die Anzeige
|
|
4. Pro Durchfahrt eine Messung lokal puffern, dann als **Batch** senden:
|
|
`POST /api/v1/zeitmessungen`
|
|
5. `GET /api/v1/turniere/aktiv/anzeige` (SSE) → Live-Stream für die Anzeige am Parcours
|
|
|
|
## `POST /api/v1/zeitmessungen` — Batch, idempotent
|
|
|
|
```jsonc
|
|
{
|
|
"geraet": "lichtschranke-ziel-01", // informativ
|
|
"messungen": [
|
|
{
|
|
"id": "9f2c1e7a-4d38-4b6e-9a11-5c0b2d7e8f30", // UUID VOM GERÄT → Idempotenz
|
|
"turnier_id": 42,
|
|
"startnummer_gemeldet": 32, // Hinweis, kein Fremdschlüssel
|
|
"messpunkt": "durchgang-1",
|
|
"dauer_sekunden": 159.16, // vom Gerät berechnet (monotone Zeit)
|
|
"gemessen_am": "2026-05-31T10:14:02.482+02:00",
|
|
"monotonic_ns": 884213771004,
|
|
"roh": { "trigger_start_ns": 884054611004, "trigger_ziel_ns": 884213771004 }
|
|
}
|
|
]
|
|
}
|
|
```
|
|
|
|
### Antwort — jede Messung wird **einzeln** quittiert
|
|
|
|
```jsonc
|
|
{
|
|
"quittungen": [
|
|
{ "id": "9f2c1e7a-…", "status": "angenommen", "start_id": 512, "hinweis": null }
|
|
],
|
|
"angenommen": 1, "duplikate": 0, "unzugeordnet": 0, "unplausibel": 0
|
|
}
|
|
```
|
|
|
|
| `status` | Bedeutung | Client-Reaktion |
|
|
|----------------|-----------|-----------------|
|
|
| `angenommen` | gespeichert und einem Start zugeordnet | aus lokaler Queue löschen |
|
|
| `duplikat` | UUID war schon da (Nachlieferung nach Timeout) | aus lokaler Queue löschen |
|
|
| `unzugeordnet` | gespeichert, aber `startnummer_gemeldet` nicht auflösbar → manuelle Zuordnung im Frontend | aus lokaler Queue löschen |
|
|
| `unplausibel` | gespeichert, Dauer außerhalb der Grenzen → im Frontend markiert | aus lokaler Queue löschen |
|
|
| `fehler` | z. B. `turnier_id` unbekannt | **in der Queue behalten**, später erneut |
|
|
|
|
**Grundregeln** (Server-seitig umgesetzt):
|
|
|
|
* **Idempotent über `id`** — ein doppelt gesendeter Batch erzeugt keine Dubletten.
|
|
Ein abgebrochener Request darf gefahrlos wiederholt werden.
|
|
* Die **Dauer kommt fertig vom Gerät** — der Server rechnet **keine** Differenz aus
|
|
zwei Wanduhr-Zeitstempeln (NTP-Sprung auf einem Pi ohne RTC).
|
|
* Eine Messung **ohne** zuordenbare Startnummer wird **nie verworfen**, sondern als
|
|
`unzugeordnet` gespeichert und im internen Frontend zur Zuordnung angeboten.
|
|
* Plausibilitätsverstöße werden **markiert**, nicht abgelehnt. Grenzen:
|
|
`OAMC_MESS_MIN_SEKUNDEN` / `OAMC_MESS_MAX_SEKUNDEN` bzw. je Gerät in
|
|
`geraet.konfiguration`.
|
|
* `gemessen_am` weit in der Vergangenheit → die Messung wird als *nachgeliefert*
|
|
markiert und trotzdem angenommen.
|
|
|
|
## Fehlmessung nachträglich verwerfen
|
|
|
|
```
|
|
POST /api/v1/zeitmessungen/{id}/verwerfen
|
|
{ "grund": "Zuschauer hat die Schranke ausgelöst" }
|
|
```
|
|
|
|
Die Messung bleibt mit Grund gespeichert (Status `verworfen`), zählt aber nicht in
|
|
die `summe_zeit`.
|
|
|
|
## Was der Server aus den Messungen macht
|
|
|
|
`ergebnis.summe_zeit` wird bei der Auswertung als **Summe aller gültigen,
|
|
zugeordneten Einzelmessungen** eines Starts gebildet
|
|
(`oamc.wertung.service.summe_zeit_fuer_start`). Aus Einzelmessungen lässt sich die
|
|
Summe jederzeit bilden — der umgekehrte Weg existiert nicht, deshalb speichert der
|
|
Server jede Einzelmessung.
|
|
|
|
## Offene Punkte (aus `plan.md` §10), die den Endpoint noch betreffen
|
|
|
|
* **O-13** Was misst die Lichtschranke genau (eine Gesamtdurchfahrt, mehrere
|
|
Durchgänge, einzelne Abschnitte)? → bestimmt `messpunkt` und die Summenbildung.
|
|
* **O-14** Woher kennt der Client die Startnummer? → bis geklärt: `startnummer_gemeldet`
|
|
als Hinweis, Server löst gegen die Startliste auf.
|
|
* **O-16** Geforderte Messgenauigkeit / eine oder zwei Lichtschranken.
|