advanced
enforcement
real-time
speed-limit

Real-Time Speed Enforcement

The per-OEM IoT command path, sha256 idempotency, the <5min stale-GPS skip, and what happens when a city policy activates over a fleet already on the road.

Levy Fleets TeamMay 18, 202614 min read

Real-Time Speed Enforcement

When a city Policy rule activates, Levy fans out per-OEM IoT commands to every vehicle currently inside the rule's geometry. This is the moment that proves to the city that you're not just reporting compliance — you're enforcing it. The target latency is <10s p95 from rule activation to first command sent.

Target latencies

OKAI Cat-M roundtrip is ~3-5s. Omni 4G is ~5-8s. Segway BLE-relay can spike to 30s+ on cold vehicles — for Segway fleets we document the gap to cities rather than promise sub-10s. Queclink and ZIMO sit between OKAI and Omni.

The enforcement service

src/lib/compliance/enforcement.ts exposes two entry points:

FunctionCallerWhat it does
fanOutEnforcementForRule(ruleId, reason)mds-policy-activate cron + zone-crossing engineQueries vehicles in the rule's geometry, skips stale-GPS, dispatches per-OEM speed-limit command, logs to policy_enforcement_events.
setSpeedLimit(vehicleUuid, kph, reason)Dashboard "manual override" actionSame per-OEM dispatch path but for a single vehicle, called from operator UI.

Both go through the same src/lib/iot/dispatch.ts module that handles the OEM-specific command formatting.

Per-OEM command catalogue

For each vehicle in the affected geometry, the service looks up iot_devices.iot_type and routes to the correct command:

OEMCommandFormat
OKAI•••••sign in•••••sign in — ECU speed cap
Segway•••••sign inIotrip parameter S4 over HBCS protocol — sets max throttle
Omni 4G (Levy Max, Acton, Feishen)•••••sign inSCOS protocol S4 parameter — max speed in km/h
Queclink•••••sign in•••••sign in — speed alarm + ECU cap
ZIMOMQTT •••••sign inJSON payload •••••sign in on the device's command topic

For no_ride rules, the same dispatcher emits the OEM's lock command instead (OKAI •••••sign in, Segway •••••sign in, Omni •••••sign in, etc.). For parking rules, no IoT command is sent — parking enforcement happens at ride-end, not on the vehicle.

Idempotency

Every command is tagged with a sha256 idempotency key derived from:

sha256(rule_id + vehicle_uuid + action + rule_value + activation_timestamp)

The key is written into policy_enforcement_events as a unique column. A re-run (e.g., the activation cron fires twice during a deployment) tries to insert the same key, hits the unique constraint, and silently skips. The IoT layer is never asked to send a duplicate command.

This is the same pattern we use for Sentry-noise filtering and OKAI firmware OTA. It means the enforcement loop can be safely retried.

The stale-GPS skip

When a rule activates, the service queries vehicles by their last known GPS sample:

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))

If a vehicle's last GPS is older than 5 minutes, it is skipped and a row is logged with error = 'stale_gps'. The reasoning:

  1. We don't actually know where the vehicle is, so we don't know whether the rule applies.
  2. Cat-M wakeups can be slow on shelved vehicles — a command sent to a sleeping radio queues up and arrives much later, sometimes after the vehicle has moved out of the zone.
  3. When the vehicle wakes up and sends a fresh GPS, the existing zone-crossing engine will check policy_geofences and dispatch the command then — no command is lost.

The 5-minute threshold is a constant in enforcement.ts. We chose it to match the existing telemetry freshness window used elsewhere in the codebase (the auto-lock cron, the SCOR battery-trust check).

Sequence on rule activation

mds-policy-activate cron
   |
   v
flip mds_policies.status from 'pending' -> 'active'
   |
   v
for each rule in policy:
   |
   v
fanOutEnforcementForRule(rule.id, "policy_activated")
   |
   +---> query vehicles in geometry, fresh GPS only
   |
   +---> for each vehicle:
   |        lookup iot_devices.iot_type
   |        compute sha256 idempotency key
   |        INSERT INTO policy_enforcement_events (idempotency unique)
   |        dispatch per-OEM command via iot-proxy
   |        await ack (best-effort, 5s timeout)
   |        UPDATE policy_enforcement_events SET command_ack_at = ...
   |
   v
log run summary to Sentry (success count, skip count, error count)

Sequence on vehicle entering an existing zone

The activation cron is the "rule changed" path. The other path is "vehicle moved into a zone that was already active". That's handled by the existing zone-crossing engine (src/lib/zones/enforcement.ts), which now checks policy_geofences in addition to operator zones:

GPS telemetry update arrives during active ride
   |
   v
stackForPoint(point)  // PostGIS RPC + policy_geofences scan
   |
   v
resolveStack(stack)  // priority ladder
   |
   v
active.speed_limit_kph differs from current_speed_limit_zone_id?
   |
   v
yes -> dispatch per-OEM speed-limit command (deduped against current state)

The same idempotency key shape applies, but with rule.activation_timestamp replaced by the GPS sample's timestamp. This means a vehicle that ping-pongs across a zone boundary doesn't generate duplicate commands every GPS update.

What riders see

In-app, riders get a slow-zone notification:

Title: "City slow zone"
Body:  "You are now in a Boulder city slow-zone. Top speed limited to 8 km/h."

For no-ride rules:

Title: "City no-ride zone"
Body:  "Ride controls paused while you are in this area. Move to an approved area to continue."

These messages are differentiated from operator-zone notifications by the "City" prefix, so riders know the operator isn't the one putting the rule in place. The notification copy is configurable in Settings -> Notifications -> Compliance.

What the dashboard shows

The Enforcement Events view at /dashboard/compliance/{jurisdiction-id} (Enforcement tab) is the canonical record. Each row carries:

FieldNotes
rule_idFK to mds_policy_rules
vehicle_uuidThe affected vehicle
actionspeed_limit, lock, unlock_on_exit
command_sent_atUTC ts at dispatch
command_ack_atUTC ts when the OEM acked (null if no ack yet)
command_responseOEM-specific response JSON
errorstale_gps, offline, oem_rejected, etc., or null
idempotency_keysha256 hex

Filterable by jurisdiction, rule, vehicle, and date range. Useful when a city asks "how do I know you enforced the slow zone at 3 PM on Tuesday?".

Limits and known gaps

  • Segway BLE-relay latency: vehicles that haven't been near a phone in a while can take >30s to wake up. For Segway-heavy fleets, document this with the city in writing rather than over-promising.
  • Vehicles in non_operational state: skipped automatically. We don't try to push commands to vehicles you've taken offline.
  • Vehicles with no iot_devices row: skipped with error = 'no_iot_device'. Most often this is a vehicle imported without IMEI linkage.
  • policy_geofences.geometry is currently NULL — the in-JS pointInPolygon is the fallback. Moving to ST_GeomFromGeoJSON at ingest is on the roadmap; it'll cut the in-JS scan time for fleets with thousands of policy geofences.

Manual override from the dashboard

If you ever need to override a policy enforcement decision for a single vehicle (e.g., to demo the system to a city contact, or to recover a vehicle from a no-ride zone for a technician), use Vehicle detail -> Compliance actions -> Set speed limit. This calls setSpeedLimit(vehicleUuid, kph, reason) directly. The action is logged to policy_enforcement_events with action = 'manual_override' and reason carrying the dashboard user's note.

Manual overrides expire on the next zone-crossing event. They're a tactical tool, not a way to permanently exempt a vehicle from a policy.

What's next