refactor(setup): unify controller I/O — event-driven relays[] + generic inputs[]
The controller new/edit modal hardcoded both its outputs and its inputs, so an
operator could neither add a generic event-driven relay nor a free-standing input
(e.g. a second radar at the exit). This unifies both into symmetric, first-class
lists. Behaviour for existing booths is unchanged (back-compat, no DB migration).
Outputs — one event→action relays[] list:
- A relay is "when EVENT X happens, do its action": entry/exit/both pulse a
barrier; a new `radarAlert` event drives a non-barrier alert lamp (blink while
its trigger input is active, SOLID once the camera confirms a car).
- Dropped the separate config.buttonLight block — the lamp is just a relays[] row
with direction:"radarAlert" (triggerInput + blink cadence). `alertRelaysOf()`
replaces `buttonLightOf()`; ButtonLightController keeps its proven 3-state
machine (serialized UDP, fail-OFF, hot-reload), now keyed per controllerId:relay
so several alert lamps on one controller run independently. Every barrier
resolver skips radarAlert rows (no auto-open; barrier-not-a-door intact).
Inputs — one first-class config.inputs[] list (the twin of relays[]):
- Each row is { input, role, relay?, kind?, activeLow?, cooldownSec? } with a
"+ Add input" button. role ∈ button | presence | alertTrigger; button/presence
name the relay they serve. An exit radar is just another presence row.
- Keystone `inputsOf(row)`: returns config.inputs[] or SYNTHESIZES it from the
legacy relays[].button/presenceInput/... fields, so relayForButton /
relayForPresence resolve identically from either shape — zero-downtime, no
migration. entry-flow.ts is unchanged (resolves through the same functions).
- Fixed a latent bug this exposed: the alert lamp's camera lock was hardcoded to
the ENTRY camera. Added relays[].lockLane ("entry"|"exit", default entry); the
lamp now locks on its own lane's camera, so an exit radar's lamp tracks the exit
camera. button-light tracks both #entryBusy/#exitBusy.
- Driver: extracted activeLowFrom(config) — merges inputs[] activeLow, legacy
relays[].presenceActiveLow, and the inputActiveLow escape hatch.
UI: the relay dropdown gained a "Radar alert" option (reveals trigger/lock/blink
inputs); InputEditor is rewritten to a generic list (role select folds loop/radar);
i18n sq+en kept at type-parity.
Tests: new device-resolve.test.ts (inputs[] resolution + legacy fallback identical
+ exit-radar resolves to the exit relay); button-light gains a two-independent-
alert-relays case and an exit-lamp lockLane case; access-dingtian gains
activeLowFrom cases. Full workspace build/lint/test green (i18n parity included).
Wiki + memory updated (button-light-indicator, entry-double-press, dingtian-relay).
Claude-Session: https://claude.ai/code/session_01Xcm6ikLgGoCxxHrxtjkk5V
This commit is contained in:
+40
-21
@@ -275,30 +275,49 @@ export type DeviceConfig = Record<string, ConfigValue>;
|
||||
/** Direction a barrier/relay (or a device bound to it) serves. */
|
||||
export type Direction = "entry" | "exit" | "both";
|
||||
|
||||
/** One relay on an access controller: which barrier it opens, in which direction,
|
||||
* and (optionally) the input terminal its entry button is wired to. */
|
||||
export interface RelaySpec {
|
||||
relay: number;
|
||||
direction: Direction;
|
||||
/** Input terminal of the entry button that fires this relay (transient entry). */
|
||||
button?: number;
|
||||
/** Anti-double-press (one car = one ticket). PRESENCE: input terminal of a vehicle
|
||||
* loop/barrier-feedback signal; a press prints only with a car present + re-arms when
|
||||
* it clears. COOLDOWN (fallback, no feedback): suppress repeat presses for N seconds. */
|
||||
presenceInput?: number;
|
||||
/** Sensor on the presence input: induction LOOP or a RADAR (label only). */
|
||||
presenceKind?: "loop" | "radar";
|
||||
/** The presence terminal is active-LOW (idles HIGH) — e.g. a radar wired opposite
|
||||
* the button. Maps to the driver's per-input active-level override. */
|
||||
presenceActiveLow?: boolean;
|
||||
entryCooldownSec?: number;
|
||||
/** The EVENT a relay reacts to. entry/exit/both → pulse a barrier; `radarAlert` → drive a
|
||||
* non-barrier alert lamp (blink while its trigger input is active, SOLID once the camera
|
||||
* confirms a car). The action is implied by the event. */
|
||||
export type RelayEvent = Direction | "radarAlert";
|
||||
|
||||
/** What a controller input terminal means: a transient-entry `button`, a one-car-one-ticket
|
||||
* `presence` sensor (loop/radar), or an `alertTrigger` for a radarAlert lamp. */
|
||||
export type InputRole = "button" | "presence" | "alertTrigger";
|
||||
|
||||
/** One INPUT terminal the host reads (the twin of RelaySpec). An exit radar is just another
|
||||
* `presence` row serving the exit relay. */
|
||||
export interface InputSpec {
|
||||
input: number;
|
||||
role: InputRole;
|
||||
/** The barrier relay this input serves (required for button/presence; optional for
|
||||
* alertTrigger). */
|
||||
relay?: number;
|
||||
/** presence only — induction LOOP or RADAR (label only). */
|
||||
kind?: "loop" | "radar";
|
||||
/** This terminal idles HIGH / is active-LOW (e.g. a radar wired opposite the button). */
|
||||
activeLow?: boolean;
|
||||
/** button only — presence-less fallback cooldown (seconds). */
|
||||
cooldownSec?: number;
|
||||
}
|
||||
|
||||
/** A non-barrier indicator lamp wired to a spare relay (e.g. the entry button light),
|
||||
* driven by the radar input vs. the camera lane status. */
|
||||
export interface ButtonLightSpec {
|
||||
/** 1-based spare relay the lamp is on. */
|
||||
/** One relay on an access controller: the event it reacts to. Input wiring lives in
|
||||
* `config.inputs[]`; the legacy per-relay button/presence fields are still read for
|
||||
* back-compat but no longer written. */
|
||||
export interface RelaySpec {
|
||||
relay: number;
|
||||
/** The event this relay reacts to (UI label: "Event"). */
|
||||
direction: RelayEvent;
|
||||
// ── legacy input fields (read-only back-compat; superseded by config.inputs[]) ──
|
||||
button?: number;
|
||||
presenceInput?: number;
|
||||
presenceKind?: "loop" | "radar";
|
||||
presenceActiveLow?: boolean;
|
||||
entryCooldownSec?: number;
|
||||
// ── radarAlert-only ──
|
||||
/** Input terminal whose active edge starts the blink (the radar). */
|
||||
triggerInput?: number;
|
||||
/** Which lane's camera locks this lamp SOLID (default entry). An exit radar locks on exit. */
|
||||
lockLane?: "entry" | "exit";
|
||||
/** Blink cadence (ms on / ms off) for the radar-only state. Default 500/500. */
|
||||
blinkOnMs?: number;
|
||||
blinkOffMs?: number;
|
||||
|
||||
Reference in New Issue
Block a user