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:
@@ -56,18 +56,31 @@ web/config API is on a configurable HTTP port (default **80**), distinct from th
|
||||
### Spare relays + aux outputs (`setAux`)
|
||||
|
||||
A 4-input board typically has spare relays once the entry/exit barriers are wired. These drive
|
||||
**non-barrier indicators** — e.g. the entry button's 12 V lamp (see [[button-light-indicator]]).
|
||||
Business logic drives them through the device-agnostic `AuxOutputDevice.setAux(channel, on)` (a
|
||||
latch), **never** the barrier `pulseOpen`. The [[barrier-not-a-door]] rule doesn't apply to an aux
|
||||
output (it never gates a vehicle), so holding/blinking it is fine.
|
||||
**non-barrier indicators** — e.g. the entry button's 12 V lamp. Every relay is a `config.relays[]`
|
||||
row carrying the **event** it reacts to (`direction`): the barrier events (`entry`/`exit`/`both`)
|
||||
pulse, while a **`radarAlert`** row is an [[button-light-indicator|alert relay]] (blink + camera-lock).
|
||||
Business logic drives alert relays through the device-agnostic `AuxOutputDevice.setAux(channel, on)`
|
||||
(a latch), **never** the barrier `pulseOpen`; every barrier resolver skips `radarAlert` rows. The
|
||||
[[barrier-not-a-door]] rule doesn't apply to an aux output (it never gates a vehicle), so
|
||||
holding/blinking it is fine.
|
||||
|
||||
### Per-input active level (`presenceActiveLow` / `inputActiveLow`)
|
||||
### Inputs are a first-class list (`config.inputs[]`)
|
||||
|
||||
Input wiring lives in `config.inputs[] = [{ input, role, relay?, kind?, activeLow?, cooldownSec? }]`
|
||||
— the twin of `relays[]`. `role` ∈ `button` | `presence` | `alertTrigger`; a button/presence row
|
||||
names the `relay` it serves; presence rows carry `kind` (loop/radar) + `activeLow`. Adding an exit
|
||||
radar is adding a `presence` row. (Pre-2026-06-28 configs wired this on the relay itself —
|
||||
`relays[].button/presenceInput/...`; `inputsOf()` synthesizes inputs[] from those for back-compat,
|
||||
so old configs keep working until re-saved.)
|
||||
|
||||
### Per-input active level (`inputs[].activeLow` / `inputActiveLow`)
|
||||
|
||||
Inputs are normalised against ONE board-wide resting level (`inputRestingHigh`). When a sensor (e.g.
|
||||
a [[hikvision-radar|radar]]) idles **opposite** the button, list its terminal as active-LOW —
|
||||
sourced from each relay's `presenceActiveLow`, merged into the driver's `inputActiveLow` set — so
|
||||
that one input is read inverted while the button keeps the board default. (`inputActive()` is the
|
||||
pure helper; push-mode uses the device's own `ilu.active_level` instead.)
|
||||
a [[hikvision-radar|radar]]) idles **opposite** the button, mark its terminal active-LOW — sourced
|
||||
from `inputs[].activeLow` (and the legacy `relays[].presenceActiveLow`, plus an explicit top-level
|
||||
`inputActiveLow[]` escape hatch), all merged by `activeLowFrom()` into the driver's `inputActiveLow`
|
||||
set — so that one input is read inverted while the button keeps the board default. (`inputActive()`
|
||||
is the pure helper; push-mode uses the device's own `ilu.active_level` instead.)
|
||||
|
||||
### Precondition: input_link_relay must be OFF
|
||||
|
||||
|
||||
Reference in New Issue
Block a user