Files
parking_solution/wiki/entities/lpr-camera.md
T
julian 547061edf9 docs(camera): Hik event-push gotchas + dead-camera diagnostic method
Captures the hard-won findings from the field session: the WSL source-IP
rewrite + skipSourceIpCheck fix, the boolean-as-string setup bug, the
unreliable "Test" button, the latching httpBroken flag, and Notify-
Surveillance-Center vs HTTP-Alarm-Server.

Adds a "diagnose a non-pushing camera from its OWN state" runbook
(alertStream heartbeat silence, SSH showStatus EventScribe:except, dmesg
RTC/UBIFS, netstat outbound watch) and documents the verified-dead
DS-2CD1043G2-LIU unit (defective event engine, survives factory reset ->
RMA), with the pull+vision fallback.

Claude-Session: https://claude.ai/code/session_01Xcm6ikLgGoCxxHrxtjkk5V
2026-06-22 12:50:44 +02:00

9.5 KiB
Raw Blame History

type, tags, sources, updated
type tags sources updated
entity
parking
hardware
readers
offline-first
parking-system-architecture
2026-06-15

LPR Camera

License-plate-recognition camera. For casual/transient vehicles, the plate acts as ticket + an independent record. (See parking-system-architecture §8, §9.)

Superseded direction (2026-06-15): recognition now runs host-side on snapshots from ordinary Hikvision/Dahua cameras via the opencv-anpr-service, not on a dedicated edge-AI LPR camera — see vision-service. The edge-AI camera below is kept as the original assumption / a fallback option, but is no longer the planned path. The host-side service also does vehicle verification (anti-plate-spoofing), which an edge-LPR camera does not.

  • Edge AI (original assumption): recognition runs on-device, so it keeps working with no internet — fits offline-first.
  • It's a host-side identity source: only the host sees the read; the host decides and commands the relay open (the uhppote-controller is demoted to a commanded relay for that lane). See entry-exit-readers.
  • Being host-in-the-loop is good for fraud detection — you get two independent records (the host's signed append-only-event-chain entry + the controller's remote-open event) that should reconcile one-to-one; any mismatch is an anomaly.
  • Mounting: within ~15° of vehicle travel at a controlled chokepoint for best reads.

Snapshot driver (entry/exit fraud-control record)

Separate from edge-AI LPR: the camera driver (packages/devices/src/drivers/camera.ts) does snapshot-on-event — the host pulls a still over HTTP when an entry/exit fires and stores it, referenced from the signed append-only-event-chain entry as an independent record. The camera pulls, it does not push — so it is NOT pushesToBackend and the setup wizard correctly hides the "Backend push IP" field for it (gated on the driver's pushesToBackend flag; only dingtian-relay sets it).

  • Hikvision uses ISAPI: GET /ISAPI/Streaming/channels/<id>/picture (101 = ch1 main stream) with HTTP Digest auth. The "Enable Hikvision-CGI" toggle (Network → Advanced → Integration Protocol) is a different legacy CGI surface — not needed for ISAPI.
  • Dahua uses CGI: GET /cgi-bin/snapshot.cgi?channel=<n> (0-based channel; the wizard's 1-based channel is decremented).

Driver / storage boundary: the driver FETCHES the image bytes (client-side HTTP Digest in drivers/http-digest.ts) and returns them on Snapshot.bytes; storage is the caller's job (the future entry/exit flow stores the bytes + mints a durable imageRef). This keeps the device adapter free of any filesystem/blob-store dependency. healthCheck() is honest — it actually pulls a frame (exercising reachability + auth + path/channel in one shot), not a fake ready/stub.

Verified on hardware (2026-06-15)

A Hikvision unit ("Camera 20", MAC 94:e1:ac:…, Hikvision OUI) at 10.0.10.121, creds admin / admin123 (Digest), TCP 80:

  • Initial curl test confirmed the ISAPI path returns a 2688×1520 JPEG (~306 KB).
  • The real driver (no longer a stub) was then run end to end against it: healthCheck() → ready (pulled a frame), captureSnapshot() → valid image/jpeg, ~322 KB, correct JPEG magic. Digest handshake works through HttpCamera.
  • Reaching it from the WSL dev box required forcing the source address (config.localAddress, threaded into the driver) — see wsl-dev-networking (multi-subnet source-selection trap).

Camera PUSH — "Alarm Server" event notifications (2026-06-22)

Separate from the pull snapshot path above: newer Hikvision firmware can push an event to us. Under Event → Smart/VCA (e.g. line crossing / intrusion / "Vehicle Detection") the unit exposes Detection Target: Human / Vehicle — selecting Vehicle + Notify Surveillance Center, then Alarm Settings → Alarm Server, makes the camera HTTP-POST an EventNotificationAlert to a URL we host on each detection. Same machine-call shape as the dingtian-relay Input Link push — no polling.

  • Ingress: POST /api/devices/hikvision/:deviceId/event (apps/server/src/routes/hikvision-alarm.ts). Source-IP guarded (must come from the device's configured host) + optional HTTP Digest (some firmware can't authenticate the Alarm Server call → source-IP only). NOT behind the SPA cookie/CSRF (it's a device call), exactly like the Dingtian push.
  • Config: added to the hikvision driver — alarmPushEnabled (bool), pushUser/pushPassword (optional Digest). The driver is now pushesToBackend: true, so first-run setup offers the backend push IP. Point the camera's Alarm Server at http://<backend-ip>:<port>/api/devices/hikvision/<deviceId>/event.
  • Discovery-first: the endpoint is permissive — accepts ANY content-type as raw bytes (event XML, multipart-with-JPEG, or JSON; Hik's format varies by model/firmware), records the verbatim body as a kind:"alarm" device_event, and best-effort extracts eventType / target / plate / dateTime / channelID. The point of this first cut is to see exactly what a given camera sends (inspect via GET /api/events or the server log) before wiring it to the read bus.
  • Not yet a barrier trigger. It records + breadcrumbs only; it does NOT emit a DeviceReadEvent or open anything. A plate read is advisory, never the sole reason a barrier opens (append-only-event-chain, opencv-anpr-service) — the entry/exit wiring is a deliberate next step once the real payload is known. If the camera emits its own plate (<plateNumber>), we can use it as an advisory read directly; otherwise the server hands the attached/pulled frame to the opencv-anpr-service for ANPR.

Gotchas learned the hard way (2026-06-22 field session)

Several traps surfaced trying to get a real camera to push. In order of how long each cost:

  • WSL rewrites the inbound source IP. On the dev host (WSL mirrored mode), an inbound LAN packet arrives at our server with its source rewritten to the host's own IP (10.0.10.203), not the camera's. The source-IP guard then rejects every push as a mismatch. Fix: a per-device skipSourceIpCheck config flag (a Setup checkbox) that bypasses the IP guard — the signed ledger + optional Digest remain the real guards. Leave OFF on a normal LAN.
  • The setup checkbox saved booleans as the STRING "true". The generic config-field form had no boolean renderer, so a type:"boolean" field fell through to a text input. Fixed (checkbox renderer); the server also coerces "true"/1/yes/on defensively.
  • The camera's "Test" button proves almost nothing. It does a TCP/connectivity probe and reports "service available" on ANY HTTP reply (even our 404) — it does not POST a real event to your URL. Only a real detection (or the ISAPI httpHosts/<id>/test) actually exercises the path.
  • httpBroken latches. Once the camera marks the host broken (from earlier failed deliveries), it stays true across reboots and won't retry. Clear it by re-PUTting the httpHost config (PUT /ISAPI/Event/notification/httpHosts/1 with <httpBroken>false</httpBroken>).
  • "Notify Surveillance Center" ≠ the HTTP Alarm Server on some firmware (separate upload channels); and basic Motion Detection may not POST to the httpHost while smart/VCA events do. Always confirm the Arming Schedule covers the test time, too (a silent killer).

Diagnosing a non-pushing camera — read its OWN state, don't guess

Status APIs lie (a diskfull error on a camera with no disk is a misleading label). The decisive checks, in order — they need no cooperation from our server:

  1. GET /ISAPI/Event/notification/alertStream (Digest) — the camera's live event bus. A HEALTHY camera streams periodic multipart heartbeat frames even when idle, and an event line when one fires. Total silence (no heartbeat) for 20s = the event subsystem is producing nothing — the fault is internal, upstream of any linkage (HTTP host, FTP, alarm stream all die together).
  2. SSH into the camera (BusyBox "psh" restricted shell; default creds work): showStatus → look for EventScribe: except (event subsystem in an exception state); dmesg → UBIFS "recovery needed" on every volume (unclean flash) and fh rtc get time error ffffffff (dead RTC — a hardware fault; the clock reads 1970, which wedges time-keyed event logic). ps shows the davinci app. df/printPart are NOT available; partition sizes via the UBIFS lines in dmesg.
  3. netstat on the camera while triggering — watch for an OUTBOUND line cam:port → server:3000. None appearing = the camera never even attempts the POST.

Verified-dead unit (2026-06-22): a DS-2CD1043G2-LIU (V5.8.10, 8 months old) failed all three: silent alertStream, EventScribe: except, diskfull on Event/triggers, dead RTC, and zero outbound to :3000 across a 3-minute netstat watch — through reboot, basic reset, AND full factory reset. Conclusion: defective event engine (RMA), not our integration. Our endpoint was verified correct throughout (it accepts + records a POST the instant one actually arrives). When a camera's event push is dead, fall back to the pull + opencv-anpr-service path, which works with the same camera (it still serves snapshots fine).