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:
@@ -27,11 +27,16 @@ The guard lives on the entry relay's spec (`config.relays[]` — see [[entry-exi
|
||||
whether real one-car-one-ticket is *possible* depends on the hardware at that lane. Two modes:
|
||||
|
||||
### PRESENCE mode (preferred — when a vehicle-presence sensor is wired)
|
||||
`relays[].presenceInput` = the 1-based input terminal of a **vehicle-presence sensor** on the same
|
||||
Inputs are a first-class `config.inputs[]` list (the twin of `relays[]`): each row is a terminal +
|
||||
a **role** (`button` / `presence` / `alertTrigger`) + the `relay` it serves. A **presence** row =
|
||||
the 1-based input terminal of a **vehicle-presence sensor** serving an entry/both relay on the same
|
||||
[[dingtian-relay|controller]] (the Dingtian's inputs are decoupled from its relays). The sensor may
|
||||
be an **induction loop** OR a **[[hikvision-radar|radar]]** (`relays[].presenceKind: "loop"|"radar"`
|
||||
— a label; the gate behaviour is identical). A radar wired to idle opposite the button needs
|
||||
`presenceActiveLow: true` so its edge reads correctly. The rule makes one-car-one-ticket **physical**:
|
||||
be an **induction loop** OR a **[[hikvision-radar|radar]]** (`inputs[].kind: "loop"|"radar"` — a
|
||||
label; the gate behaviour is identical). A radar wired to idle opposite the button needs
|
||||
`inputs[].activeLow: true` so its edge reads correctly. **Multiple radars (entry + exit) are just
|
||||
multiple presence rows** — adding an exit radar is adding a row. (Pre-2026-06-28 configs wired this
|
||||
on `relays[].presenceInput/presenceKind/presenceActiveLow`; the resolvers still read those as
|
||||
back-compat, synthesizing inputs[] from them.) The rule makes one-car-one-ticket **physical**:
|
||||
|
||||
- A press prints **only while a car is present** on the loop.
|
||||
- After a ticket prints, the relay is **disarmed** — no second ticket — **until the loop CLEARS**
|
||||
@@ -68,11 +73,14 @@ in telemetry if ever needed.
|
||||
host (single-writer); it is derived from live input edges, never the source of truth. A restart
|
||||
starts armed (the first press after a restart works), which is the safe default.
|
||||
|
||||
## As-built (2026-06-19)
|
||||
## As-built (2026-06-19; inputs[] 2026-06-28)
|
||||
|
||||
- `RelaySpec` gains `presenceInput?` + `entryCooldownSec?` (`device-resolve.ts`); `relayForButton`
|
||||
carries them onto the `ResolvedRelay`, and a new `relayForPresence()` resolves a loop-input edge to
|
||||
the entry relay it gates.
|
||||
- Inputs live in `config.inputs[] = [{ input, role, relay?, kind?, activeLow?, cooldownSec? }]`
|
||||
(`device-resolve.ts`). `inputsOf(row)` returns them, **or synthesizes** the list from the legacy
|
||||
`relays[].button/presenceInput/...` fields when a controller predates inputs[] (one back-compat
|
||||
shim; the UI no longer writes the legacy fields). `relayForButton`/`relayForPresence` resolve
|
||||
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).
|
||||
|
||||
Reference in New Issue
Block a user