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
8.5 KiB
type, tags, sources, updated, status
| type | tags | sources | updated | status | ||||||
|---|---|---|---|---|---|---|---|---|---|---|
| concept |
|
2026-06-22 | 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):
- Lane busy/free — BUILT. An advisory barrier light on the booth.
- 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
notificationRecurrencebeginning → recurringvia ISAPI — the firmware accepts the PUT but silently reverts (locked tobeginningon 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,
activerepeats ~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_MSdefault 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(kindlane-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 fromVISION_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.noSessionafter 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_alprlive + 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).