Files
parking_solution/wiki/entities/lpr-camera.md
T
julian 6133923094 feat(camera): Hikvision Alarm Server event-push ingress (discovery-first)
Newer Hik firmware can PUSH events to us: Event -> Smart/VCA with
"Detection Target: Human/Vehicle" + Notify Surveillance Center + Alarm
Settings -> Alarm Server makes the camera HTTP-POST an
EventNotificationAlert on each detection.

- New POST /api/devices/hikvision/:deviceId/event (routes/hikvision-alarm.ts):
  same machine-push pattern as the Dingtian Input Link — source-IP guarded
  + optional HTTP Digest, not behind the SPA cookie/CSRF.
- Discovery-first / permissive: a wildcard content-type parser accepts ANY
  body as raw bytes (event XML, multipart+JPEG, or JSON — Hik varies by
  firmware), records it verbatim as a kind:"alarm" device_event, and
  best-effort extracts eventType/target/plate/dateTime/channelID for the
  summary + a loud log line. The point is to SEE exactly what a camera
  sends before wiring it further.
- hikvision driver gains alarmPushEnabled + pushUser/pushPassword config and
  pushesToBackend:true (setup offers the backend push IP).
- NOT yet a barrier trigger / DeviceReadEvent — records only. A plate read
  is advisory, never the sole reason a barrier opens; the read-bus/ANPR
  wiring is a deliberate next step once the real payload is known.

Tests: hikvision-alarm.test.ts (6: vehicle XML summary, ANPR plate, raw
JSON, wrong-IP 404, disabled 404, unknown-device 404). server 109/109;
build+lint 14/14. Wiki: lpr-camera.md + log.

Claude-Session: https://claude.ai/code/session_01Xcm6ikLgGoCxxHrxtjkk5V
2026-06-22 10:02:35 +02:00

89 lines
5.8 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
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/<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|vision service]] for ANPR.