fix(entry): enforce the camera press-gate + duplicate-ticket defenses
Field report (park-buzi): a BLINKING entry button still printed — the lamp encoded blink-vs-solid (radar-only vs radar+camera) but #suppressReason only checked the radar, so a radar false-positive (rain, pedestrian) minted a real signed ticket. Three layered fixes: 1. CAMERA gate on the physical press: with an entry camera configured, a press is live only in the lamp's SOLID state (LaneStatus.entry busy, mirrored into EntryFlow via onLaneStatus). Suppress-only — the camera stays advisory (never opens, never traps). Camera-less sites keep the radar-only gate; a faulty camera is dropped via the existing bypassPresenceCamera admin toggle. 2. Cooldown as a REAL backstop behind presence: the presence branch returned early, so entryCooldownSec was dead wherever a loop was wired. Now it bounds the stationary-car double-ticket (a motion radar drops a motionless car → spurious loop-clear re-arms one-car-one-ticket → same car reprints). 3. Post-hoc duplicate-plate anomaly (entry-side twin of plateSwapSuspected): when entry ANPR recognizes a plate already OPEN under another session entered within ENTRY_DUP_PLATE_WINDOW_MIN (default 15 min), sign ONE entry.duplicatePlate anomaly naming both tickets for the operator to void. ANPR stays non-blocking (rides the post-open snapshot as before). REJECTED: camera-vetoed re-arm (defer re-arm until the lane flips free). The camera has no leave events — "free" is a ~30s silence timeout that never lapses inside a queue, so every queued car after the first would be suppressed until an operator intervened. Blocking legit entry at peak beats nothing; the proper preventive fix is a pass-through sensor (passedInput) — recorded as open in wiki/concepts/entry-double-press.md. Also: setup.relayTest reason was missing from both web catalogs (parity is only enforced sq<->en, so the build passed) — added. Tests: entry-press-gate.test.ts (blink suppresses / solid prints / camera-less unaffected / bypass honored / cooldown catches the dropout re-press / residual risk documented / still-present re-press stays suppressed) + entry-duplicate-plate.test.ts (flags open dup, ignores closed/stale/self/other plates). Suite 258 green. Claude-Session: https://claude.ai/code/session_01Xcm6ikLgGoCxxHrxtjkk5V
This commit is contained in:
@@ -2,7 +2,7 @@
|
||||
type: concept
|
||||
tags: [parking, entry, anti-fraud, safety, devices]
|
||||
sources: []
|
||||
updated: 2026-06-19
|
||||
updated: 2026-07-04
|
||||
status: open
|
||||
---
|
||||
|
||||
@@ -53,7 +53,42 @@ car to pull through, short enough not to block the next legitimate car). It is a
|
||||
mitigation, not a guarantee** — a determined abuser can wait it out. Use it only where presence
|
||||
feedback isn't available; prefer wiring a loop.
|
||||
|
||||
The two can coexist (presence first, cooldown as a backstop), but presence is authoritative when set.
|
||||
The two **coexist** (2026-07-04): presence is checked first, and the cooldown now genuinely runs as
|
||||
a **backstop behind it** (before that date the presence branch returned early and the cooldown never
|
||||
ran when a loop was wired). The backstop exists because of the **stationary-car radar dropout**: a
|
||||
motion (doppler) radar loses a car that stops moving — the flow sees a spurious loop-clear, re-arms
|
||||
one-car-one-ticket, and the *same* car's next press mints a second ticket. A configured cooldown
|
||||
bounds how fast that can happen. Keep it short (~10–15 s): in presence mode it only fires on a
|
||||
press the loop already approved, which includes the *next legitimate car in a queue*.
|
||||
|
||||
### CAMERA gate (2026-07-04 — when an entry camera is configured)
|
||||
The [[button-light-indicator]] lamp always encoded the intended UX — **blink** = radar-only
|
||||
(something in the zone, no confirmed car), **solid** = radar + camera agree — but the press handler
|
||||
only ever checked the radar, so a blinking button still printed (radar false-positives: rain, a
|
||||
pedestrian, reflections). Now the press gate enforces the lamp's rule: with an entry camera
|
||||
configured, a press is live **only while the entry lane's camera confirms a vehicle**
|
||||
([[lane-presence-and-anpr-entry|LaneStatus]] busy — the SOLID state). This keeps the camera
|
||||
**advisory** in the safety sense: it only ever *suppresses a ticket*, never opens a barrier and
|
||||
never traps a car. A camera-less site keeps the radar-only gate; a faulty camera is dropped via the
|
||||
admin [[entry-presence-bypass]] (`bypassPresenceCamera`), same as the operator-issued path.
|
||||
|
||||
### REJECTED: camera-vetoed re-arm (the queue trap)
|
||||
The obvious fix for the stationary-car dropout — *don't re-arm on loop-clear while the camera still
|
||||
sees a car; re-arm when the lane flips free* — was designed and **rejected** (2026-07-04). The
|
||||
entry camera sends no leave events; "free" is a ~30 s detection-silence timeout. In any queue the
|
||||
next car occupies the zone before that timeout can lapse, so the lane never flips free between two
|
||||
legitimate cars — every queued car after the first would be suppressed until an operator intervened.
|
||||
Blocking legitimate entry at peak load is strictly worse than the occasional duplicate ticket. The
|
||||
duplicate is handled **post-hoc** instead:
|
||||
|
||||
### Duplicate-plate reconciliation (post-hoc, entry-side twin of [[plate-reconciliation]])
|
||||
ANPR already rides the entry snapshot (never blocking — the barrier is open before recognition
|
||||
starts). When the recognized plate is **already OPEN under another session entered within the last
|
||||
~15 min** (`ENTRY_DUP_PLATE_WINDOW_MIN`), the flow signs ONE `entry.duplicatePlate` **anomaly**
|
||||
pointing at both tickets; the operator voids the duplicate. Window short on purpose — a closed
|
||||
session or an old read is a legit re-visit, not a double press. The proper *preventive* fix is a
|
||||
**pass-through sensor** (closing loop / photocell past the barrier) as an unambiguous "a car went
|
||||
through" signal — noted below as open.
|
||||
|
||||
## A suppressed press is a NO-OP, not an anomaly
|
||||
|
||||
@@ -82,17 +117,27 @@ in telemetry if ever needed.
|
||||
through `inputsOf`, carry `presenceInput`/`entryCooldownSec` onto the `ResolvedRelay`, and only ever
|
||||
gate entry/both relays. An exit radar = a `presence` row on the exit relay.
|
||||
- `EntryFlow` (`entry-flow.ts`) keeps a `#guard` map keyed `controllerId:relay`: `#onPresenceEdge`
|
||||
tracks the loop, `#suppressReason` decides presence/cooldown, `#recordSuppressedPress` writes the
|
||||
telemetry. The guard disarms + stamps the cooldown on **print success** (not on open).
|
||||
tracks the loop, `#suppressReason` decides camera → presence → cooldown (in that order),
|
||||
`#recordSuppressedPress` writes the telemetry. The guard disarms + stamps the cooldown on
|
||||
**print success** (not on open). The camera state is a live `LaneStatus` mirror
|
||||
(`onLaneStatus`, wired in `server.ts`); "an entry camera is configured" is read per press so
|
||||
adding/removing one needs no restart. The duplicate-plate check is
|
||||
`flagDuplicateEntryPlate` in `snapshot.ts`, called from the ANPR ride-along when the entry
|
||||
flow passes its `EventLog`.
|
||||
- [[first-run-setup|SetupWizard]] relay editor: entry/both relays expose a **Presence loop
|
||||
(terminal)** field and, when no loop is set, a **Cooldown after ticket (s)** field (localized
|
||||
sq+en — see [[i18n]]).
|
||||
|
||||
## Open
|
||||
|
||||
- **No automated test yet** (the standing harness gap) — verify on hardware: with a loop, a held
|
||||
button issues one ticket; after the car clears the loop a new car gets a fresh one. Without a loop,
|
||||
a cooldown blocks the repeat and the suppressed press lands in telemetry.
|
||||
- ~~No automated test yet~~ **Covered 2026-07-04**: `entry-press-gate.test.ts` pins the camera
|
||||
gate (blink suppresses / solid prints / camera-less unaffected / bypass honored), the cooldown
|
||||
backstop behind presence, and one-car-one-ticket; `entry-duplicate-plate.test.ts` pins the
|
||||
post-hoc plate anomaly. Hardware verification on park-buzi still worthwhile.
|
||||
- **Pass-through sensor** (closing loop / photocell past the barrier, a `passedInput` role): the
|
||||
only *unambiguous* "the car went through" signal. Would let re-arm key on actual passage instead
|
||||
of loop-clear, killing the stationary-car dropout without the queue trap. Procurement + wiring
|
||||
question for the lanes.
|
||||
- **Exit side:** the symmetric concern (re-reading a ticket at exit) is already handled differently —
|
||||
exit validates against an open session, so a second read finds the session closed (no double-exit).
|
||||
No presence gate needed there today.
|
||||
|
||||
+16
@@ -2207,3 +2207,19 @@ all interfaces; plus correctness/hygiene (cwd-relative `.env`, `/health` always-
|
||||
swallowing failures, unbounded `min_confidence`). Reassurance recorded: a forged image can't open a
|
||||
barrier (server re-gates at 0.85 + debounce), content-type isn't trusted, non-root, `.env` not baked
|
||||
into the image. Nothing fixed yet — this is the backlog to work from.
|
||||
|
||||
## [2026-07-04] update | Entry press gate: camera enforced, cooldown backstop, duplicate-plate anomaly
|
||||
|
||||
Field report from park-buzi: a BLINKING entry button (radar-only, no camera confirmation) still
|
||||
printed a ticket — the [[button-light-indicator]] encoded blink-vs-solid but `#suppressReason`
|
||||
only ever checked the radar. Fixed in [[entry-double-press]]: (1) CAMERA gate on the physical
|
||||
press — live only in the lamp's SOLID state when an entry camera is configured; honors
|
||||
[[entry-presence-bypass]]; suppress-only, so the camera stays advisory; (2) cooldown now a REAL
|
||||
backstop behind presence (the old code returned early, so `entryCooldownSec` was dead in presence
|
||||
mode) — bounds the stationary-car motion-radar-dropout double-ticket; (3) post-hoc
|
||||
`entry.duplicatePlate` signed anomaly when the recognized entry plate is already open under a
|
||||
recent session (entry-side twin of [[plate-reconciliation]]; ANPR stays non-blocking). REJECTED
|
||||
along the way: camera-vetoed re-arm (defer re-arm until the lane flips free) — the camera's ~30 s
|
||||
silence-timeout "free" never fires inside a queue, so it would suppress every queued car after the
|
||||
first. Proper preventive fix noted open: a pass-through sensor (`passedInput`). 13 new tests
|
||||
(`entry-press-gate.test.ts`, `entry-duplicate-plate.test.ts`); suite 258 green.
|
||||
|
||||
Reference in New Issue
Block a user