Captures this session's back-and-forth as a new concept page
[[lane-presence-and-anpr-entry]] and cross-links it:
- BUILT: advisory lane busy/free booth lights (LaneStatus + WS), with the
measured camera limits behind the 30s timeout (no leave signal; movement-
driven re-fire; notificationRecurrence locked to "beginning" — ISAPI flip
silently reverts).
- PLANNED: the ANPR "bridge" — explicitly a small apps/server HANDLER (~40
lines), NOT a new service/container. On a camera vehicle event: snapshot ->
ANPR -> high-confidence match -> debounce -> emitRead{kind:"plate"}, then
the existing subscription match/dispatch/gate admits the subscriber. Both
directions, opt-in (config.anpr), plate never the sole authority.
- Records the decisions (high confidence floor, debounce-for-correctness)
and the REJECTED ideas (continuous livestream / per-car queue tracking /
make-model) with why, plus the open hardware question (booth-PC test).
Updates subscription.md (plate matching is built; the live source is this
bridge) and lpr-camera.md (the two consumers of the vehicle event).
Claude-Session: https://claude.ai/code/session_01Xcm6ikLgGoCxxHrxtjkk5V
12 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). Two consumers were since designed off this same vehicle event — see lane-presence-and-anpr-entry: (a) BUILT — advisory lane busy/free booth lights; (b) PLANNED — the ANPR "bridge" that snapshots → ANPR → emits akind:"plate"read for a SUBSCRIBER match through the existing gated flow (a smallapps/serverhandler, not a service). If the camera ever emits its own<plateNumber>we'd use it directly; thisDS-2CD1043G2does not, so the server pulls the frame and hands it to the opencv-anpr-service.
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). Always confirm the Arming Schedule covers the test time, too (a silent killer).
- ⭐ THE ROOT CAUSE (2026-06-22): no detection AREA drawn. This is what actually defeated us for most of a day. On the motion/smart-detection page there's a Draw Area step — if no region is drawn on the frame, the camera detects nothing, generates NO event, and therefore posts nothing anywhere (httpHost, FTP, alarm stream all stay silent because there's no event upstream). Enabling the detection + ticking Notify Surveillance Center is not enough — you must draw the region. Once an area was drawn, the very first vehicle produced a clean POST. Check this FIRST.
Confirmed real payload (DS-2CD1043G2-LIU, V5.8.10, 2026-06-22)
What this camera actually POSTs on a motion event with a target — captured end-to-end:
Content-Type: multipart/form-data; boundary=boundary, one XML part namedMoveDetection.xml(Content-Type: application/xml). A real frame/JPEG may be attached as a second part on other event types — our endpoint stores the readable head; splitting an image part tosnapshotsis a forward step (not needed for plain motion).- The XML is an
EventNotificationAlertwith the fields we care about:<eventType>VMD</eventType>(Video Motion Detection) +<eventState>active</eventState><targetType>vehicle</targetType>— the camera classifies vehicle vs human ON-DEVICE. (Field istargetType, NOTdetectionTarget.) This means simple presence + class comes for free, no vision model needed for that part.<targetInfo><targetRect>with normalizedX/Y/width/height(0–1) — the bounding box.<channelID>,<macAddress>(provenance),<dateTime>— but the dateTime is GARBAGE (2032-…) because this unit's RTC is dead (see below); we use our own server receive time, never the camera's. (No<plateNumber>— this is a motion event, not an ANPR camera.)
If a camera still won't push — diagnostics (read its OWN state)
Only after confirming the detection area is drawn + arming schedule covers now + Notify Surveillance Center is on. These read the camera directly (no cooperation from our server):
netstaton the camera (via SSH) while you trigger — watch for an OUTBOUND linecam:port → server:3000. It appearing = the camera fired and is delivering (then check our/api/devices/hikvision/alarms). None = no event was generated (almost always: no area drawn).GET /ISAPI/Event/notification/alertStream(Digest, needs a clean handshake) — the live event bus. NB: acurl --digesttap that fails the handshake returns empty and looks like "no events" — don't over-read silence here (this misled us); the netstat watch above is more reliable.- SSH
showStatus/dmesgexpose internal state. ⚠ Caveat learned the hard way: these surface scary-looking strings that are red herrings —EventScribe: except, adiskfullerror onEvent/triggers(on a camera with no disk), andfh rtc get time error/ a 1970 clock. On our unit ALL of these were present and the camera worked fine once an area was drawn. The dead RTC is real (hence the bogusdateTime) but harmless to event push. Do NOT conclude "dead camera / RMA" from these — they are not proof of a broken event engine.
Correction (2026-06-22): an earlier version of this page concluded this DS-2CD1043G2-LIU was a defective unit needing RMA, based on the silent alertStream +
diskfull/EventScribe:except+ dead RTC surviving a full factory reset. That was WRONG. The camera was healthy; the real cause was simply no detection area drawn, so no event was ever generated. Thediskfull/RTC findings were unrelated quirks (RTC genuinely dead, but it doesn't block event push). Lesson: don't escalate to "hardware fault" while a basic config precondition (the drawn region) is unmet — and treat vendor status-API error strings as unreliable. The pull + opencv-anpr-service path remains a valid fallback, but it was not needed here.