fix(anpr): sliding poll window so a car arriving mid-loop isn't lost

A loop started by a far/early car would (a) give up before the REAL car settled at
the barrier, and (b) swallow the real car's pushes (the #polling guard dropped them).
So a confident-but-wrong far-car plate could win, or the intended car get debounced
out after the loop ended — wrong car acted on, right car blocked.

Fix: a push that JOINS a running loop now EXTENDS the deadline (lastPush +
ANPR_POLL_WINDOW_MS) instead of being dropped, capped at start + ANPR_POLL_MAX_MS
(30s) so a continuously-busy lane can't slide forever. Each tick still pulls a FRESH
frame, so the loop tracks whoever is at the barrier NOW, not the car that started it.
Per-camera sliding deadline in #pollDeadline (cleared with #polling in finally).

+1 test (push mid-poll keeps the loop alive past the initial deadline); 171 server
tests green. New knob ANPR_POLL_MAX_MS documented in the komodo env reference + the
two concurrency guards written up in lane-presence-and-anpr-entry.

Claude-Session: https://claude.ai/code/session_01Xcm6ikLgGoCxxHrxtjkk5V
This commit is contained in:
2026-06-27 23:30:41 +02:00
parent a888125eca
commit c2a861208f
4 changed files with 92 additions and 11 deletions
@@ -79,6 +79,19 @@ Two new env knobs: `VISION_ENTRY_MIN_CONFIDENCE` (0.85), `ANPR_DEBOUNCE_MS` (12_
> 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