advanced
Durchsetzung
Echtzeit
Tempolimit

Echtzeit-Geschwindigkeitsdurchsetzung

Der OEM-spezifische IoT-Befehlsweg, sha256-Idempotenz, der <5min Stale-GPS-Skip und was passiert, wenn eine Stadt-Policy ueber einer bereits unterwegs befindlichen Flotte aktiv wird.

Levy Fleets TeamMay 18, 202614 min read

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:

FunktionAufruferWas sie tut
fanOutEnforcementForRule(ruleId, reason)mds-policy-activate-Cron + Zone-Crossing-EngineFragt 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:

OEMBefehlFormat
OKAI•••••sign in•••••sign in -- ECU-Geschwindigkeitskappe
Segway•••••sign inIotrip-Parameter S4 ueber HBCS-Protokoll -- setzt max Throttle
Omni 4G (Levy Max, Acton, Feishen)•••••sign inSCOS-Protokoll S4-Parameter -- Hoechstgeschwindigkeit in km/h
Queclink•••••sign in•••••sign in -- Tempoalarm + ECU-Kappe
ZIMOMQTT •••••sign inJSON-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:

  1. Wir wissen nicht wirklich, wo das Fahrzeug ist, also wissen wir nicht, ob die Regel gilt.
  2. 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.
  3. Wenn das Fahrzeug erwacht und ein frisches GPS sendet, prueft die bestehende Zone-Crossing-Engine policy_geofences und 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:

FeldHinweise
rule_idFK zu mds_policy_rules
vehicle_uuidDas betroffene Fahrzeug
actionspeed_limit, lock, unlock_on_exit
command_sent_atUTC-Ts bei Versand
command_ack_atUTC-Ts, wenn OEM bestaetigte (null bei keinem Ack)
command_responseOEM-spezifisches Response-JSON
errorstale_gps, offline, oem_rejected usw., oder null
idempotency_keysha256-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 mit error = 'no_iot_device'. Meist ein Fahrzeug ohne IMEI-Verknuepfung importiert.
  • policy_geofences.geometry ist derzeit NULL -- das In-JS-pointInPolygon ist der Fallback. Wechsel zu ST_GeomFromGeoJSON beim 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?