Files
parking_solution/wiki/concepts/lane-presence-and-anpr-entry.md
T
julian 25a72ff20a
Build desktop / desktop (push) Successful in 4m32s
Build & push images / images (push) Successful in 2m43s
CI / check (push) Successful in 37s
feat(anpr): per-camera auto-open toggle (anprAutoTrigger) for shared lanes
A shared entry/exit lane has both an entry and an exit camera on ONE lane: a
subscriber driving IN is admitted by the entry cam, but the exit cam sees the same
car leaving its frame and phantom-EXITs the occurrence just opened (its back plate).

Separate RECOGNITION from AUTO-OPEN per camera:
- config.anpr (unchanged) = run snapshots through the recognizer, record the plate
  (evidence), BOTH directions — stays on.
- config.anprAutoTrigger (new, absent ⇒ on when anpr is on) = may THIS camera
  auto-open the barrier. Set false on the shared-lane exit cam: it still recognises
  plates but never auto-triggers. The bridge gates on it (anpr-entry.ts), before the
  poll loop.

UI: a "Auto open/close on subscriber plate" checkbox under ANPR in the camera setup
(shown when anpr is on); persisted true/false so a park can explicitly disable it.
i18n sq+en (also corrected the now-stale anprHint "never opens a barrier" wording —
it does, via the bridge). +1 server test (anprAutoTrigger=false → no snapshot, no
read); 172 green. Documented the two toggle levels (site-wide + per-camera) in
lane-presence-and-anpr-entry.

Claude-Session: https://claude.ai/code/session_01Xcm6ikLgGoCxxHrxtjkk5V
2026-06-27 23:52:16 +02:00

12 KiB
Raw Blame History

type, tags, sources, updated, status
type tags sources updated status
concept
parking
camera
anpr
subscription
lane
vision
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):

  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 + hikvision-alarm.test.ts wiring.

Toggles — two levels (the auto-open is optional). ANPR auto entry/exit can be turned off without losing plate recognition:

  • Site-wide: site_config.anprEntryEnabled (a Site Settings switch) gates the WHOLE bridge (both directions); off ⇒ no camera auto-opens, recognition/lane-status unaffected.
  • Per-camera (2026-06-27): config.anprAutoTrigger (absent ⇒ on when anpr is on). This separates recognition (config.anpr — snapshots run through the recognizer, plate recorded, BOTH directions) from auto-open (anprAutoTrigger — may this camera fire the barrier). The case it solves: a shared entry/exit lane where ONE physical lane has both an entry and an exit camera. A subscriber driving IN is admitted by the entry cam — but the exit cam sees the SAME car leaving its frame and would phantom-exit the occurrence just opened (its back plate). Set the exit cam's anprAutoTrigger = false: it still recognises plates for the record, but never auto-opens. (The Setup checkbox "Auto open/close on subscriber plate" appears under ANPR.)

POLL-until-confident (2026-06-27). The single-shot capture above was upgraded to a poll loop. The camera fires its vehicle alarm the INSTANT motion starts — the car is still APPROACHING, so the first frame's plate is small/blurry/half-in-frame and ANPR returns a low-confidence misread (observed live on the exit lane: '111'@0.20, 'AE18671'@0.19 …, while the manual test-anpr on the SAME stopped car read AA890XX@1.00). The car then STOPS at the barrier waiting for it to open — the stationary, well-framed moment the test reads at ~100%. So the bridge now pulls a FRESH frame every ANPR_POLL_MS (1000) and re-runs ANPR until one clears the floor, or ANPR_POLL_WINDOW_MS (8000) elapses (car drove off / non-subscriber → give up cleanly). One loop per camera (#polling set) so the ~1Hz alarm re-fires JOIN it, not spawn N; each tick is a fresh camera.captureSnapshot (NOT captureSnapshotShared, whose TTL would re-serve the same bad frame). VERIFIED on hardware: 7 garbage approach frames → AA890XX@0.999 at the barrier → signed vehicle_exit. This is what made subscriber auto-exit actually work on the DS-2CD1047G3H-LIU (whose alarm fires on approach, not at the readable moment — see lpr-camera "auto-enter but don't auto-exit"). Still advisory + fail-soft; a barrier never opens on a low-confidence read.

Two concurrency guards on the loop (the edges a single push-triggered loop creates):

  1. Credential-mid-poll abort. If the subscriber scans their card/QR at the reader DURING the loop, they've already transacted — the bridge watches their openOccurrenceCount (baselined once a frame reads the bound plate) and aborts without emitting if it moves, so it never double-acts (which would exit the NEXT open occurrence — bad for a fleet sub).
  2. SLIDING window for a different car arriving mid-poll. A loop started by a far/early car must not (a) give up before the REAL car settles, nor (b) swallow the real car's pushes. So a push that joins a running loop extends the deadline (lastPush + ANPR_POLL_WINDOW_MS), capped at start + ANPR_POLL_MAX_MS (30s) so a continuously-busy lane can't slide forever. Because each tick pulls a FRESH frame, the loop naturally tracks whoever is at the barrier now, not the car that started it. (Knobs: ANPR_POLL_MS, ANPR_POLL_WINDOW_MS, ANPR_POLL_MAX_MS.)

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).