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
9.5 KiB
type, tags, sources, updated
| type | tags | sources | updated | |||||
|---|---|---|---|---|---|---|---|---|
| entity |
|
|
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
curltest 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()→ validimage/jpeg, ~322 KB, correct JPEG magic. Digest handshake works throughHttpCamera. - 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 configuredhost) + 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
hikvisiondriver —alarmPushEnabled(bool),pushUser/pushPassword(optional Digest). The driver is nowpushesToBackend: true, so first-run setup offers the backend push IP. Point the camera's Alarm Server athttp://<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 extractseventType/target/plate/dateTime/channelID. The point of this first cut is to see exactly what a given camera sends (inspect viaGET /api/eventsor the server log) before wiring it to the read bus. - Not yet a barrier trigger. It records + breadcrumbs only; it does NOT emit a
DeviceReadEventor 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-deviceskipSourceIpCheckconfig 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 atype:"boolean"field fell through to a text input. Fixed (checkbox renderer); the server also coerces"true"/1/yes/ondefensively. - 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. httpBrokenlatches. Once the camera marks the host broken (from earlier failed deliveries), it staystrueacross reboots and won't retry. Clear it by re-PUTting the httpHost config (PUT /ISAPI/Event/notification/httpHosts/1with<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:
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).- SSH into the camera (BusyBox "psh" restricted shell; default creds work):
showStatus→ look forEventScribe: except(event subsystem in an exception state);dmesg→ UBIFS "recovery needed" on every volume (unclean flash) andfh rtc get time error ffffffff(dead RTC — a hardware fault; the clock reads 1970, which wedges time-keyed event logic).psshows thedavinciapp.df/printPartare NOT available; partition sizes via the UBIFS lines indmesg. netstaton the camera while triggering — watch for an OUTBOUND linecam: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,diskfullonEvent/triggers, dead RTC, and zero outbound to:3000across 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).