--- type: entity tags: [parking, hardware, readers, offline-first] sources: [parking-system-architecture] updated: 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//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=` (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://:/api/devices/hikvision//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 (``), we can use it as an advisory read directly; otherwise the server hands the attached/pulled frame to the [[opencv-anpr-service|vision 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//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 `false`). - **"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|vision]]** path, which > works with the same camera (it still serves snapshots fine).