diff --git a/wiki/concepts/lane-presence-and-anpr-entry.md b/wiki/concepts/lane-presence-and-anpr-entry.md new file mode 100644 index 0000000..71f110a --- /dev/null +++ b/wiki/concepts/lane-presence-and-anpr-entry.md @@ -0,0 +1,114 @@ +--- +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** — PLANNED. 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 (PLANNED, not built) + +> **"Bridge" = a HANDLER FUNCTION in `apps/server` (≈40 lines, e.g. `anpr-entry.ts`). NOT a new +> service / container / app.** It is in-process glue that calls things that ALREADY exist. + +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** | ❌ **the bridge** — today the snapshot ANPR only RECORDS the plate as advisory telemetry; it does NOT `emitRead`. hik-alarm.ts literally says "NOT a DeviceReadEvent yet". | + +**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). diff --git a/wiki/entities/lpr-camera.md b/wiki/entities/lpr-camera.md index 02ffaf7..f9807ca 100644 --- a/wiki/entities/lpr-camera.md +++ b/wiki/entities/lpr-camera.md @@ -82,10 +82,12 @@ Center**, then **Alarm Settings → Alarm Server**, makes the camera **HTTP-POST 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. + ([[append-only-event-chain]], [[opencv-anpr-service]]). Two consumers were since designed off this + same vehicle event — see **[[lane-presence-and-anpr-entry]]**: (a) BUILT — advisory lane busy/free + booth lights; (b) PLANNED — the ANPR "bridge" that snapshots → ANPR → emits a `kind:"plate"` read + for a SUBSCRIBER match through the existing gated flow (a small `apps/server` handler, not a + service). If the camera ever emits its own `` we'd use it directly; this `DS-2CD1043G2` + does not, so the server pulls the frame and hands it to the [[opencv-anpr-service|vision service]]. ### Gotchas learned the hard way (2026-06-22 field session) diff --git a/wiki/entities/subscription.md b/wiki/entities/subscription.md index bfaed13..24c2c26 100644 --- a/wiki/entities/subscription.md +++ b/wiki/entities/subscription.md @@ -258,9 +258,13 @@ LPR/ANPR plate identity** (the plate binding below): number. The GEE readers are combo QR + RFID (ID/IC/NFC), so the same device captures both. A Wiegand-out reader keeps a future autonomous path open ([[entry-exit-readers]]); the [[dingtian-relay]] has no onboard card list. -- **Plate (LPR/ANPR) — NOT YET IMPLEMENTED.** When plate-bound (below), a matching plate read is an - accepted identity too. The vision/ANPR service that produces plate reads is future work - ([[opencv-anpr-service]] / [[lpr-camera]]); until it exists, plate binding has no live source. +- **Plate (LPR/ANPR) — matching is BUILT, the live SOURCE is the one missing wire.** When plate-bound + (below), a matching plate read is an accepted identity — and `subscription-flow.ts` `match()` + + `read-dispatch.ts` already handle a `via:"plate"` read end to end (gate + entry/exit). What's + missing is the thing that EMITS a plate read from the lane camera: the **ANPR "bridge"** (a small + handler in `apps/server`, not a new service) that snapshots on a camera vehicle event, runs + [[opencv-anpr-service|ANPR]], and on a high-confidence match emits the plate onto the read bus. PLANNED, + scoped to subscribers only. See [[lane-presence-and-anpr-entry]] for the full design + decisions. Both feed the host as a reader event whose `source` is `wiegand` / `qr` (the `IdentitySource` already in the model) and whose value is the credential id. diff --git a/wiki/index.md b/wiki/index.md index 5970c2b..b248430 100644 --- a/wiki/index.md +++ b/wiki/index.md @@ -99,6 +99,7 @@ Counts: 4 sources · 19 entities · 45 concepts · 7 decision records. - [[soft-delete]] — BUILT: accidental admin deletes of master data (users/roles/subs/plans/tariffs) are soft (deleted_at) + recoverable from a recycle bin; auto-purge after N days; signed ledger out of scope. - [[subscription]] — recurring plan (e.g. 10,000 ALL/month); RF/QR or plate identity, car-count + max-concurrent, host-in-loop; short-circuits payment. (Renamed from "permit"; time-of-day windows noted, deferred.) - [[opencv-anpr-service]] — host-side vision microservice: ANPR (plate identity) + vehicle verification (anti-plate-spoofing witness); fast-alpr (MIT, YOLOv9+CCT/ONNX) the evaluated recognizer baseline. +- [[lane-presence-and-anpr-entry]] — camera vehicle detection → (BUILT) advisory lane busy/free booth lights + (PLANNED) the ANPR "bridge": a subscriber's plate read at the lane admits them via the existing gated subscription flow. Measured camera limits; rejected the queue-tracking/livestream ideas. - [[blocklist]] — barred plates/cards refused at entry (never at exit); signed, attributed. ## Concepts — frontend / operator UI diff --git a/wiki/log.md b/wiki/log.md index 691ca9a..8d50f25 100644 --- a/wiki/log.md +++ b/wiki/log.md @@ -1412,3 +1412,22 @@ is unmet; vendor status-API error strings are unreliable. Confirmed real payload targetType=vehicle (vehicle/human classified ON-DEVICE), targetRect bounding box. The push endpoint + all-methods + skipSourceIpCheck + rejection-recording are all validated against the real device now. See [[lpr-camera]] (corrected). + +## [2026-06-22] design+build | Lane presence (BUILT) + ANPR subscriber-entry "bridge" (PLANNED) +Off the now-working Hik vehicle event: BUILT advisory lane busy/free booth barrier lights +(LaneStatus + WS; timeout-driven "free" since the camera sends no leave signal — TTL settled at 30s +after a controlled in/out test showed movement-driven ~1-3s re-fire but ~15-25s gaps for a still +car, and ~no dwell lag on leave). Measured the camera's hard limits: no current-state poll exists, +and flipping notificationRecurrence beginning->recurring via ISAPI is silently reverted (firmware +locked). Then narrowed the bigger ambition to a clean, high-value scope: ANPR for SUBSCRIBERS ONLY — +a plate read at the lane admits a subscriber through the EXISTING gated subscription flow. Found the +whole subscription side already supports via:"plate" (match + dispatch + gate); the one missing piece +is a small `apps/server` HANDLER ("the bridge", ~40 lines, NOT a new service/container) that on a +camera vehicle event snapshots -> ANPR -> on a HIGH-confidence match (new VISION_ENTRY_MIN_CONFIDENCE) +-> debounces (required for ledger correctness, not CPU: ~1Hz re-fire would drive repeat entries) -> +emitRead{kind:"plate"}. Both directions, opt-in per camera (config.anpr), plate never the sole +authority (routes through the gate). REJECTED: continuous livestream presence + per-car queue +tracking/make-model (needs a vehicle detector the plate-only vision lacks + appliance compute we can't +measure on the dev PC). Vision checked: fast_alpr live, ~50ms/frame on DEV PC (appliance TBD — +booth-PC test ~2026-06-23). New page [[lane-presence-and-anpr-entry]]; updated [[lpr-camera]], +[[subscription]], index.