import type { Db } from "@parking/db"; import type { FastifyBaseLogger } from "fastify"; import { deviceEvents, type DeviceInputEvent, type LanePresenceEvent } from "./device-events.js"; import { presenceLaneOf } from "./device-resolve.js"; // Per-lane RADAR presence for the booth's barrier lights. A vehicle-presence INPUT // (loop/radar) shorted at an entry/exit barrier means "something is in the lane vicinity" // BEFORE the camera confirms a vehicle. This is the SAME signal that makes the physical // button lamp (relay 3) blink — see button-light.ts (#onInput) — so the on-screen light // and the lamp stay in lockstep: both react to a presence edge resolved the SAME way // (relayForPresence, on an entry/both relay). ADVISORY ONLY: it gates nothing. // // A radar serving an entry (or "both") barrier marks the ENTRY lane present; an exit radar // marks EXIT. The lane is resolved via `presenceLaneOf` (direction-agnostic — unlike the // entry-gated `relayForPresence` the one-car-one-ticket gate uses), so both lanes blink. export class LanePresence { readonly #db: Db; readonly #logger: FastifyBaseLogger; /** Active presence terminals per lane, keyed `${deviceId}:${input}` (several radars may * serve one lane). A lane is "present" while its set is non-empty. */ readonly #entry = new Set(); readonly #exit = new Set(); #unsub: (() => void) | null = null; constructor(db: Db, logger: FastifyBaseLogger) { this.#db = db; this.#logger = logger; } /** Subscribe to presence input edges. */ start(): void { this.#unsub = deviceEvents.onInput((e) => this.#onInput(e)); } /** Current snapshot (for the WS hello). */ snapshot(): LanePresenceEvent { return { entry: this.#entry.size > 0, exit: this.#exit.size > 0 }; } #onInput(e: DeviceInputEvent): void { const lane = presenceLaneOf(this.#db, e.deviceId, e.input); if (!lane) return; // not a presence terminal on a barrier relay const key = `${e.deviceId}:${e.input}`; const set = lane === "entry" ? this.#entry : this.#exit; const before = set.size > 0; if (e.edge === "on") set.add(key); else set.delete(key); const after = set.size > 0; if (before !== after) { this.#logger.info(`lane-presence: ${lane} -> ${after ? "present" : "clear"}`); deviceEvents.emitLanePresence(this.snapshot()); } } /** Unsubscribe on shutdown. */ stop(): void { this.#unsub?.(); this.#unsub = null; } }