feat(devices): radar presence input + button-light output on the controller
Model the entry button (I1) and a Hikvision radar (I2) as named children of the access controller, and drive the button's 12V lamp on a spare relay. - Radar = the existing relays[].presenceInput one-car-one-ticket gate, now labelled presenceKind: loop|radar. A radar may idle opposite the button, so add a per-input active-level override: relays[].presenceActiveLow -> driver inputActiveLow set, inverting just that terminal (pure helper inputActive()). The Dingtian has one board-wide resting level otherwise. - AuxOutputDevice.setAux(channel,on) capability on the device interface (Dingtian latch) so business logic drives a NON-barrier lamp through the interface. Barriers still only pulseOpen — barrier-not-a-door preserved. - ButtonLightController: subscribes to the radar input edge + the camera lane status and drives a 3-state lamp — radar+car=solid, radar-only=blink (~1Hz), else off. Fails OFF on host loss/error; de-duped. A radar detection never opens a barrier on its own (advisory; threat model). - SetupWizard: presence kind + active-low + a button-light relay picker; sq+en i18n. Tests: button-light.test.ts (truth table + blink + fail-OFF + de-dupe), access-dingtian.test.ts (active-level inversion). Workspace build+lint+test green (158 server tests). Wiki: hikvision-radar, button-light-indicator + updates. Claude-Session: https://claude.ai/code/session_01Xcm6ikLgGoCxxHrxtjkk5V
This commit is contained in:
@@ -0,0 +1,62 @@
|
||||
---
|
||||
type: concept
|
||||
tags: [parking, device, indicator, radar, camera, aux-output, barrier-not-a-door]
|
||||
sources: []
|
||||
updated: 2026-06-24
|
||||
status: settled
|
||||
---
|
||||
|
||||
# Button-light indicator (radar × camera disagreement lamp)
|
||||
|
||||
The entry button has a **12 V light**. It is driven by the host on a **spare relay** of the
|
||||
[[dingtian-relay|Dingtian]] controller as a 3-state indicator that combines the **[[hikvision-radar|
|
||||
radar]]** input with the **camera "car in zone"** signal:
|
||||
|
||||
| Radar input | Camera (lane entry busy) | Button light |
|
||||
| --- | --- | --- |
|
||||
| detecting | **free** — no car confirmed | **BLINK** (~1 Hz) |
|
||||
| detecting | **busy** — camera confirms a car | **SOLID on** |
|
||||
| clear | — | **OFF** |
|
||||
|
||||
It is a **disagreement indicator**: the radar sees *something* but the camera hasn't confirmed a
|
||||
real vehicle → blink (attention / "pull forward"); both agree → solid; nothing there → off.
|
||||
|
||||
## Signals
|
||||
|
||||
- **Radar** = the presence input edge on the entry relay (`relays[].presenceInput`, the same edge
|
||||
the [[entry-double-press|one-car-one-ticket]] gate observes — so the lamp and the gate always
|
||||
agree on "a car is here").
|
||||
- **Camera "car in zone"** = the existing **[[lpr-camera|lane status]]** (`LaneStatusEvent` entry
|
||||
busy/free, from camera vehicle detection). Already advisory; already drives the booth's barrier
|
||||
lights. No new camera plumbing.
|
||||
|
||||
## Config
|
||||
|
||||
A controller-level `config.buttonLight = { relay, blinkOnMs?, blinkOffMs? }` (the operator picks a
|
||||
**spare** relay — not a barrier relay; the setup UI warns if it overlaps one). Blink defaults to
|
||||
500 ms / 500 ms.
|
||||
|
||||
## Implementation
|
||||
|
||||
`apps/server/src/button-light.ts` — `ButtonLightController` subscribes to `deviceEvents.onInput`
|
||||
(radar) + `onLaneStatus` (camera), computes the target state per controller, and drives the lamp via
|
||||
a **device-agnostic aux-output** capability.
|
||||
|
||||
- **Aux-output capability.** `AuxOutputDevice { setAux(channel, on) }` on the device interface (the
|
||||
Dingtian driver implements it as a latch). Business logic drives the lamp through this — **never**
|
||||
the driver's barrier methods.
|
||||
- **Barrier-not-a-door is preserved.** The lamp is **not a barrier**, so holding / blinking it on a
|
||||
timer is fine — the [[barrier-not-a-door]] rule forbids timing a *barrier* closed, and barriers
|
||||
still only ever `pulseOpen`. The lamp uses the separate `setAux` latch.
|
||||
- **Fails OFF.** On host loss, shutdown, or a `setAux` error the lamp defaults OFF — a dead lamp is
|
||||
"no hint", never a misleading solid "go". SOLID is only ever held while busy + present is actively
|
||||
true (never latched on through a crash path).
|
||||
- **De-duped.** Only writes when the effective output changes, so the 50 ms input poll doesn't spam
|
||||
the controller over UDP.
|
||||
|
||||
## Status
|
||||
|
||||
Built 2026-06-24 for the first booth (button I1, radar I2, lamp on a spare relay). Covered by
|
||||
`apps/server/src/button-light.test.ts` (the truth table + blink toggling + fail-OFF + de-dupe).
|
||||
Related: [[hikvision-radar]], [[entry-double-press]], [[lpr-camera]], [[dingtian-relay]],
|
||||
[[barrier-not-a-door]].
|
||||
Reference in New Issue
Block a user