Echtzeit-Geschwindigkeitsdurchsetzung
Wenn eine Stadt-Policy-Regel aktiv wird, sendet Levy OEM-spezifische IoT-Befehle an alle Fahrzeuge, die sich derzeit in der Geometrie der Regel befinden. Das ist der Moment, in dem Sie der Stadt beweisen, dass Sie Compliance nicht nur berichten -- sondern durchsetzen. Die Latenzvorgabe ist <10s p95 von Regelaktivierung bis zum ersten gesendeten Befehl.
Ziellatenzen
OKAI-Cat-M-Roundtrip betraegt ~3-5s. Omni 4G ~5-8s. Segway BLE-Relay kann bei kalten Fahrzeugen auf 30s+ steigen -- fuer Segway-Flotten dokumentieren wir die Luecke zu Staedten, statt unter 10s zu versprechen. Queclink und ZIMO liegen zwischen OKAI und Omni.
Der Enforcement-Service
src/lib/compliance/enforcement.ts exponiert zwei Einstiegspunkte:
| Funktion | Aufrufer | Was sie tut |
|---|---|---|
fanOutEnforcementForRule(ruleId, reason) | mds-policy-activate-Cron + Zone-Crossing-Engine | Fragt Fahrzeuge in der Regelgeometrie ab, ueberspringt Stale-GPS, sendet OEM-spezifischen Geschwindigkeitsbefehl, loggt in policy_enforcement_events. |
setSpeedLimit(vehicleUuid, kph, reason) | Dashboard-"manueller Override" | Gleicher OEM-Versandweg, aber fuer ein einzelnes Fahrzeug, aufgerufen aus der Betreiber-UI. |
Beide gehen durch dasselbe src/lib/iot/dispatch.ts-Modul, das die OEM-spezifische Befehlsformatierung uebernimmt.
OEM-Befehlskatalog
Fuer jedes Fahrzeug in der betroffenen Geometrie schlaegt der Service iot_devices.iot_type nach und routet zum richtigen Befehl:
| OEM | Befehl | Format |
|---|---|---|
| OKAI | •••••sign in | •••••sign in -- ECU-Geschwindigkeitskappe |
| Segway | •••••sign in | Iotrip-Parameter S4 ueber HBCS-Protokoll -- setzt max Throttle |
| Omni 4G (Levy Max, Acton, Feishen) | •••••sign in | SCOS-Protokoll S4-Parameter -- Hoechstgeschwindigkeit in km/h |
| Queclink | •••••sign in | •••••sign in -- Tempoalarm + ECU-Kappe |
| ZIMO | MQTT •••••sign in | JSON-Payload •••••sign in auf dem Befehls-Topic des Geraets |
Fuer no_ride-Regeln emittiert derselbe Versender stattdessen den OEM-Sperrbefehl (OKAI •••••sign in, Segway •••••sign in, Omni •••••sign in usw.). Fuer parking-Regeln wird kein IoT-Befehl gesendet -- die Park-Durchsetzung passiert am Fahrtende, nicht am Fahrzeug.
Idempotenz
Jeder Befehl wird mit einem sha256-Idempotenzschluessel markiert, abgeleitet aus:
sha256(rule_id + vehicle_uuid + action + rule_value + activation_timestamp)
Der Schluessel wird in policy_enforcement_events als eindeutige Spalte geschrieben. Eine Wiederholung (z.B. der Aktivierungs-Cron feuert zweimal waehrend eines Deployments) versucht denselben Schluessel einzufuegen, trifft auf den Unique-Constraint und ueberspringt still. Die IoT-Schicht wird nie aufgefordert, einen doppelten Befehl zu senden.
Das ist dasselbe Muster, das wir fuer Sentry-Noise-Filterung und OKAI-Firmware-OTA verwenden. Es bedeutet, dass die Durchsetzungsschleife sicher wiederholbar ist.
Der Stale-GPS-Skip
Bei Regelaktivierung fragt der Service Fahrzeuge nach ihrer letzten bekannten GPS-Probe ab:
SELECT v.* FROM vehicles v
JOIN vehicle_events e ON e.vehicle_uuid = v.id
WHERE e.event_type = 'gps'
AND e.created_at > now() - interval '5 minutes'
AND ST_Contains(<rule.geometry>, ST_MakePoint(e.lng, e.lat))
Wenn das letzte GPS eines Fahrzeugs aelter als 5 Minuten ist, wird es uebersprungen und eine Zeile wird mit error = 'stale_gps' geloggt. Die Begruendung:
- Wir wissen nicht wirklich, wo das Fahrzeug ist, also wissen wir nicht, ob die Regel gilt.
- Cat-M-Aufwachvorgaenge koennen bei abgestellten Fahrzeugen langsam sein -- ein an ein schlafendes Funkmodul gesendeter Befehl wartet in der Warteschlange und kommt viel spaeter an, manchmal nachdem das Fahrzeug die Zone verlassen hat.
- Wenn das Fahrzeug erwacht und ein frisches GPS sendet, prueft die bestehende Zone-Crossing-Engine
policy_geofencesund sendet den Befehl dann -- kein Befehl geht verloren.
Der 5-Minuten-Schwellwert ist eine Konstante in enforcement.ts. Wir haben ihn passend zum bestehenden Telemetrie-Frischefenster gewaehlt, das anderswo im Code verwendet wird (Auto-Lock-Cron, SCOR-Batterie-Vertrauenscheck).
Sequenz bei Regelaktivierung
mds-policy-activate Cron
|
v
mds_policies.status von 'pending' -> 'active' flippen
|
v
fuer jede Regel in der Policy:
|
v
fanOutEnforcementForRule(rule.id, "policy_activated")
|
+---> Fahrzeuge in Geometrie abfragen, nur frisches GPS
|
+---> fuer jedes Fahrzeug:
| iot_devices.iot_type nachschlagen
| sha256-Idempotenzschluessel berechnen
| INSERT INTO policy_enforcement_events (Idempotenz Unique)
| OEM-spezifischen Befehl via iot-proxy senden
| auf Ack warten (Best-Effort, 5s Timeout)
| UPDATE policy_enforcement_events SET command_ack_at = ...
|
v
Laufzusammenfassung in Sentry loggen (Erfolg, Skip, Fehler)
Sequenz bei Fahrzeugeintritt in eine bestehende Zone
Der Aktivierungs-Cron ist der "Regel geaendert"-Weg. Der andere Weg ist "Fahrzeug bewegt sich in eine bereits aktive Zone". Das wird von der bestehenden Zone-Crossing-Engine (src/lib/zones/enforcement.ts) gehandhabt, die nun zusaetzlich zu Betreiberzonen auch policy_geofences prueft:
GPS-Telemetrie-Aktualisierung kommt waehrend aktiver Fahrt an
|
v
stackForPoint(point) // PostGIS-RPC + policy_geofences-Scan
|
v
resolveStack(stack) // Prioritaetsleiter
|
v
active.speed_limit_kph weicht von current_speed_limit_zone_id ab?
|
v
ja -> OEM-spezifischen Geschwindigkeitsbefehl senden (gegen aktuellen Zustand dedupliziert)
Dieselbe Idempotenzschluessel-Form gilt, aber mit rule.activation_timestamp durch den GPS-Probe-Zeitstempel ersetzt. Das bedeutet, dass ein Fahrzeug, das an einer Zonengrenze hin und her pingpongt, nicht bei jeder GPS-Aktualisierung doppelte Befehle erzeugt.
Was Fahrer sehen
In-App erhalten Fahrer eine Slow-Zonen-Benachrichtigung:
Titel: "Stadt-Slow-Zone"
Text: "Sie befinden sich in einer Boulder-Stadt-Slow-Zone. Hoechstgeschwindigkeit auf 8 km/h begrenzt."
Fuer No-Ride-Regeln:
Titel: "Stadt-No-Ride-Zone"
Text: "Fahrsteuerung pausiert, solange Sie in diesem Bereich sind. Bewegen Sie sich in einen zugelassenen Bereich, um fortzufahren."
Diese Nachrichten unterscheiden sich von Betreiberzonen-Benachrichtigungen durch das "Stadt"-Praefix, sodass Fahrer wissen, dass nicht der Betreiber die Regel aufstellt. Der Benachrichtigungstext ist unter Einstellungen -> Benachrichtigungen -> Compliance konfigurierbar.
Was das Dashboard zeigt
Die Durchsetzungsereignisse-Ansicht unter /dashboard/compliance/{jurisdiction-id} (Enforcement-Tab) ist der kanonische Datensatz. Jede Zeile traegt:
| Feld | Hinweise |
|---|---|
rule_id | FK zu mds_policy_rules |
vehicle_uuid | Das betroffene Fahrzeug |
action | speed_limit, lock, unlock_on_exit |
command_sent_at | UTC-Ts bei Versand |
command_ack_at | UTC-Ts, wenn OEM bestaetigte (null bei keinem Ack) |
command_response | OEM-spezifisches Response-JSON |
error | stale_gps, offline, oem_rejected usw., oder null |
idempotency_key | sha256-Hex |
Filterbar nach Zustaendigkeit, Regel, Fahrzeug und Datumsbereich. Nuetzlich, wenn eine Stadt fragt "wie weiss ich, dass Sie die Slow-Zone Dienstag 15 Uhr durchgesetzt haben?".
Grenzen und bekannte Luecken
- Segway-BLE-Relay-Latenz: Fahrzeuge, die laenger nicht in Telefonnaehe waren, koennen >30s zum Aufwachen brauchen. Bei Segway-lastigen Flotten dies schriftlich mit der Stadt dokumentieren, statt zu uebersprechen.
- Fahrzeuge im
non_operational-Status: automatisch uebersprungen. Wir versuchen keine Befehle an offline gestellte Fahrzeuge zu pushen. - Fahrzeuge ohne
iot_devices-Zeile: uebersprungen miterror = 'no_iot_device'. Meist ein Fahrzeug ohne IMEI-Verknuepfung importiert. policy_geofences.geometryist derzeit NULL -- das In-JS-pointInPolygonist der Fallback. Wechsel zuST_GeomFromGeoJSONbeim Aufnehmen steht auf der Roadmap; es wird die In-JS-Scan-Zeit fuer Flotten mit Tausenden Policy-Geofences senken.
Manueller Override aus dem Dashboard
Wenn Sie jemals eine Policy-Durchsetzungs-Entscheidung fuer ein einzelnes Fahrzeug ueberschreiben muessen (z.B. zur Demo des Systems an einen Stadtkontakt oder zur Bergung eines Fahrzeugs aus einer No-Ride-Zone fuer einen Techniker), nutzen Sie Fahrzeugdetail -> Compliance-Aktionen -> Tempolimit setzen. Das ruft setSpeedLimit(vehicleUuid, kph, reason) direkt auf. Die Aktion wird in policy_enforcement_events mit action = 'manual_override' und reason mit dem Dashboard-Benutzer-Hinweis geloggt.
Manuelle Overrides verfallen beim naechsten Zonenwechselereignis. Sie sind ein taktisches Werkzeug, kein Weg, ein Fahrzeug dauerhaft von einer Policy auszunehmen.
Was kommt als naechstes?
- Policy-Aufnahme von Staedten -- woher die Regeln kommen.
- Prioritaet gestapelter Geofences -- wie die aktive Regel gewaehlt wird.
- Fehlerbehebung -- wenn Durchsetzung nicht feuert.