Files
parking_solution/wiki/concepts/lane-presence-and-anpr-entry.md
T
julian 65328b8c11
CI / check (push) Failing after 15s
feat(anpr): subscriber-entry bridge + admin disable toggle
Wire the lane camera's vehicle event into the gated subscription flow: on a
vehicle/active push from an opt-in (config.anpr) camera, AnprBridge pulls a fresh
snapshot, runs ANPR, applies a stricter entry confidence floor, debounces, and —
matching the plate to a subscription BEFORE emitting — emits a kind:"plate" read.
The existing ReadDispatcher -> SubscriptionFlow then signs the entry/exit and opens
the barrier. A plate is never the sole authority: it routes through the same gate
(active/window/blocklist/car-count) as any credential. Fail-soft, fire-and-forget,
subscriber-only by construction. Field-verified end to end (plate AA504LX opened the
entry barrier and appended a signed vehicle_entry).

Add an admin master switch (site_config.anpr_entry_enabled, default ON) in Site
Settings that disables ONLY the barrier-driving bridge; advisory snapshot-ANPR and
lane busy/free are unaffected. Read live per event, so toggling takes effect with no
restart. Migration 0013 (additive ALTER ADD COLUMN, default 1).

- New: apps/server/src/anpr-entry.ts (AnprBridge) + tests (9)
- hikvision-alarm.ts hands vehicle detections to the bridge (fire-and-forget) + wiring tests (3)
- server.ts reorders the read flows above the hik-alarm registration
- snapshot.ts exports buildCamera for reuse
- env: VISION_ENTRY_MIN_CONFIDENCE (0.85), ANPR_DEBOUNCE_MS (12000)
- site route + SiteSettings checkbox + i18n (sq/en parity)
- wiki: lane-presence-and-anpr-entry / lpr-camera / index / log -> BUILT

Claude-Session: https://claude.ai/code/session_01Xcm6ikLgGoCxxHrxtjkk5V
2026-06-22 19:49:18 +02:00

128 lines
8.5 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: concept
tags: [parking, camera, anpr, subscription, lane, vision]
sources: []
updated: 2026-06-22
status: open
---
# Lane Presence & ANPR Subscriber Entry
Two related things a camera's vehicle detection feeds, worked out over a long field session
(2026-06-22, see [[lpr-camera]] for the camera-side saga + the corrected "it was the undrawn
detection area" conclusion):
1. **Lane busy/free** — BUILT. An advisory barrier light on the booth.
2. **ANPR subscriber entry** — BUILT (2026-06-22). A subscriber's plate, read from the lane camera,
drives their entry/exit through the EXISTING [[subscription]] flow. The "bridge" below.
The camera (Hik `DS-2CD1043G2-LIU`) only emits `VMD` events with `eventState=active` and a
`targetType` of `vehicle`/`human` — a coarse **presence** signal, never an identity. Everything
here is built on that, and on the camera's hard limits.
## What the camera actually gives us (measured)
- **Push only, no poll.** No ISAPI endpoint reports "is a car in the zone now"; the only live source
is the event push. We tried to force a steadier signal by flipping `notificationRecurrence`
`beginning → recurring` via ISAPI — the firmware **accepts the PUT but silently reverts** (locked to
`beginning` on this value line). So: notify-once-at-motion-start is all we get.
- **No leave signal.** The camera never sends an `inactive`/end event. Confirmed by config
(`notificationRecurrence: beginning`) AND a controlled in/out test.
- **Re-fire is MOVEMENT-driven, not steady.** Controlled test (call out enter/leave, correlate to the
event log): while a car MOVES, `active` repeats ~1–3 s apart; while it sits MOTIONLESS, gaps stretch
to ~15–25 s. Crucially the camera has **~no dwell lag** — it goes silent within ~1 s of the car
leaving (last event 16:15:17 vs car-left ~16:15:30).
## 1. Lane busy/free (BUILT)
`apps/server/src/lane-status.ts` (`LaneStatus`) + the hik-alarm handler + the booth WS. A `vehicle`
`active` event on a camera bound to entry/exit marks THAT lane busy and arms an auto-clear timer; the
booth shows two barrier lights beside the scan input (green=free, red=busy). **Advisory only — gates
nothing** (never blocks a ticket or opens a barrier; the standing rule).
- **"Free" is timeout-driven** (no leave signal). The TTL must exceed the still-car gap (~25 s) or a
parked car flickers free — so `LANE_BUSY_TTL_MS` default is **30 s** (started at a guessed 90 s,
briefly 5 s, then set to 30 s from the measured data). The camera's lack of dwell lag means 30 s
also clears promptly after departure.
- A `both`-direction camera marks both lanes. Pushed over the existing `/api/ws` (kind `lane-status`).
## 2. ANPR subscriber entry — THE BRIDGE (BUILT 2026-06-22)
> **"Bridge" = a HANDLER CLASS in `apps/server/src/anpr-entry.ts` (`AnprBridge`). NOT a new
> service / container / app.** It is in-process glue that calls things that ALREADY exist.
**As built:** `hikvision-alarm.ts`, on a `vehicle`/non-`inactive` push from an `anpr`-opted-in
camera, hands the deviceId to `AnprBridge.onVehicleDetected()` (fire-and-forget, never awaited on the
camera's 200). The bridge: debounce (camera-level, pre-snapshot) → `captureSnapshot` (fresh pull, via
the reused `snapshot.ts buildCamera`) → `vision.analyze` → entry confidence floor
(`VISION_ENTRY_MIN_CONFIDENCE`, 0.85) → normalize plate → **`subscriptionFlow.match()` (match BEFORE
emit)** → if a subscriber, `deviceEvents.emitRead({kind:"plate"})`; if not, record an advisory
`anpr-skip` device_event and stop. The existing `onRead → ReadDispatcher → SubscriptionFlow.run()`
then does the gated entry/exit + barrier open. Constructed in `server.ts` (the flows were reordered
above the hik-alarm registration so the bridge can take `subscriptionFlow`). Fail-soft throughout —
any snapshot/vision error degrades to the subscriber's card/QR, never throws into the push handler.
Two new env knobs: `VISION_ENTRY_MIN_CONFIDENCE` (0.85), `ANPR_DEBOUNCE_MS` (12_000). Covered by
`anpr-entry.test.ts` (7) + `hikvision-alarm.test.ts` wiring (3).
The goal (narrowed deliberately — see Rejected below): **a subscriber's plate, read by the lane
camera, admits them through the same gated flow a QR/card scan uses.** Scope was cut to subscribers
ONLY — no queue segmentation, no per-car tracking, no make/model, no ticket-button gating.
Almost everything already exists; the bridge is the one missing wire:
| Piece | Status |
| --- | --- |
| Camera vehicle event | ✅ [[lpr-camera]] (hik-alarm) |
| Pull a snapshot | ✅ `snapshot.ts` (`captureSnapshot`) |
| Read the plate | ✅ [[opencv-anpr-service]] `/analyze` (~50 ms on the DEV PC; appliance TBD) |
| Match a plate → subscriber | ✅ `subscription-flow.ts` `match()` + `subscription_plates` (`via:"plate"`) |
| Plate read → gated entry/exit | ✅ `read-dispatch.ts` + SubscriptionFlow (active/window/blocklist/car-count) |
| **Emit the plate onto the read bus** | ✅ `anpr-entry.ts` (`AnprBridge`) — on a vehicle push from an `anpr` camera it snapshots → analyzes → matches a subscriber → `emitRead({kind:"plate"})`. (Built 2026-06-22.) |
**The bridge logic:** on a camera `vehicle`/`active` event from an **opt-in** camera (`config.anpr`),
snapshot → `vision.analyze` → if a plate clears a **HIGH** confidence floor → **debounce** → emit
`DeviceReadEvent{kind:"plate", value, deviceId}`. The existing dispatcher + flow do the rest.
### Decisions settled with the user (2026-06-22)
- **Both directions.** Entry- and exit-bound cameras both work; the dispatcher infers the verb from
the camera's bound relay direction (an entry camera → entry, exit → exit). No per-read inference.
- **High confidence required.** A barrier-driving read needs a stricter bar than the advisory-record
floor — a NEW `VISION_ENTRY_MIN_CONFIDENCE` (≈0.85) distinct from `VISION_MIN_CONFIDENCE`. (The
subscriber still holds their card/QR, so a near-miss read just falls back to that.)
- **Opt-in** per camera (`config.anpr`), so a site that didn't ask for plate-entry is unaffected.
- **Debounce is REQUIRED — for correctness, NOT CPU.** The camera re-fires ~1 Hz while a car is
present; emitting a read every second would drive REPEAT entries (a fleet sub opens a 2nd
occurrence; a single-car sub spams "already inside") or, on an exit camera, repeat exits
(`sub.refused.noSession` after the first, and FIFO could phantom-close another occurrence). So the
same plate on the same camera within ~10–15 s = ONE credential presentation.
- NB: a direction-bound camera does NOT flip entry↔exit on repeat reads (the barrier's direction
fixes the verb), so the earlier "flip-flop" fear was wrong — but repeat-same-direction is still
bad. Debounce stands.
- **Threat-model rule preserved by construction.** A plate is trivially spoofable (print it on
paper); it must NEVER be the sole reason a barrier opens. Routing through the existing
SubscriptionFlow means the plate is just another credential through the same gate (active / window
/ blocklist / car-count) — not a bypass. See [[opencv-anpr-service]], [[append-only-event-chain]].
### To validate (booth-PC test day, ~2026-06-23)
The dev-PC ANPR is ~50 ms/frame, but that says little about the hardened **booth appliance** (likely a
low-power CPU, possibly 5–15× slower). Real-hardware latency is the open number. The user is bringing
the actual booth PC to test on.
## Rejected / out of scope (and why)
- **Vision monitors the RTSP livestream continuously** for presence — rejected: rebuilds (worse) what
the camera already does (presence), pegs the appliance CPU 24/7, and the leave-detection heuristic
is no cleaner than a 30 s timeout.
- **Vision polls every ~1 s to track THE car, segment a queue, count cars, read make/model** — a
genuinely harder need (bumper-to-bumper cars the camera can't tell apart). Set ASIDE, not dismissed:
it needs a general vehicle DETECTOR ([[opencv-anpr-service]] is plate-only today) + "same-car-vs-new"
logic + make/model (a third, weak, heavy model), and its viability is **gated on appliance
inference budget we cannot measure on the dev PC**. Revisit only if real traffic + hardware justify
it. The vision feasibility was checked: `fast_alpr` live + ready; plate-only; no vehicle/presence
detector exists yet.
- **Gate the entry ticket button on lane-busy** (don't print when no car) — deferred. The lane-busy
signal supports it, but it gates a PHYSICAL action so it must FAIL-OPEN (allow when presence is
unknown). Parked pending the user's real goal (anti-spam print vs one-ticket-per-car).