feat(booth): blink the Entry/Exit lights on radar presence (mirror relay 3)
Build desktop / desktop (push) Successful in 4m14s
Build & push images / images (push) Successful in 2m42s
CI / check (push) Successful in 37s

The on-screen Hyrje/Dalje barrier lights were 2-state (green=free / red=busy)
off the camera lane-status only — they couldn't show the radar-only "detected,
not yet confirmed" state that makes the physical button lamp (relay 3) blink.
Now they mirror the lamp's 3-state rule per lane:
  radar present + camera not busy → BLINK green↔red (~1 Hz)
  camera busy                     → SOLID red
  otherwise                       → SOLID green

End-to-end:
- LanePresence (lane-presence.ts): subscribes to deviceEvents.onInput, resolves
  each presence edge to its lane via the new direction-agnostic presenceLaneOf()
  (device-resolve.ts) — entry AND exit, unlike the entry-gated relayForPresence
  the one-car-one-ticket gate uses — and emits a lane-presence {entry,exit} bus
  event on change. Wired in server.ts (start + onClose).
- WS forwards it (hello snapshot + push) into live-store.radar.
- BarrierLight (BoothScreen.tsx) is now 3-state; blinks via the .lane-blink
  keyframe (index.css), which holds solid-red under prefers-reduced-motion.

Same input + same rule as the lamp, so the screen and the post never disagree.

A new test (lane-presence.test.ts) caught a real bug: the first cut reused
relayForPresence, so the EXIT lane never resolved (it's entry-gated) and never
blinked — presenceLaneOf fixes it. Covers entry/exit independence, de-dupe
across several radars on one lane, and ignoring non-presence inputs.

Full workspace build/lint/test green (185 server tests). Updated the
button-light-indicator wiki page ("On-screen twin").

Claude-Session: https://claude.ai/code/session_01Xcm6ikLgGoCxxHrxtjkk5V
This commit is contained in:
2026-06-28 11:48:12 +02:00
parent 4418594af0
commit 38481f105f
12 changed files with 378 additions and 24 deletions
+20
View File
@@ -86,6 +86,16 @@ export interface LaneStatusEvent {
readonly exit: boolean; // true = busy (a vehicle is at the exit vicinity)
}
/** Per-lane RADAR presence — a vehicle-presence INPUT (loop/radar) is shorted at the
* entry/exit barrier, i.e. "something is in the lane vicinity" BEFORE the camera has
* confirmed a vehicle. Same signal that makes the physical button lamp (relay 3) blink:
* radar-present + camera-not-busy. Drives the booth's barrier light blink. Advisory only —
* it gates nothing. See wiki/concepts/button-light-indicator.md. */
export interface LanePresenceEvent {
readonly entry: boolean; // true = a presence input on an entry barrier is active
readonly exit: boolean; // true = a presence input on an exit barrier is active
}
class DeviceEventBus extends EventEmitter {
emitInput(event: DeviceInputEvent): void {
this.emit("input", event);
@@ -148,6 +158,16 @@ class DeviceEventBus extends EventEmitter {
this.on("lane-status", cb);
return () => this.off("lane-status", cb);
}
/** Emitted whenever a lane's RADAR presence CHANGES (a presence input shorted/cleared
* at an entry/exit barrier). Drives the booth barrier light's blink. Advisory only. */
emitLanePresence(event: LanePresenceEvent): void {
this.emit("lane-presence", event);
}
onLanePresence(cb: (event: LanePresenceEvent) => void): () => void {
this.on("lane-presence", cb);
return () => this.off("lane-presence", cb);
}
}
/** Process-wide device event bus. */