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]].
|
||||
@@ -26,10 +26,12 @@ press → print → press again issued a second ticket immediately. That is not
|
||||
The guard lives on the entry relay's spec (`config.relays[]` — see [[entry-exit-points]]), because
|
||||
whether real one-car-one-ticket is *possible* depends on the hardware at that lane. Two modes:
|
||||
|
||||
### PRESENCE mode (preferred — when a vehicle loop is wired)
|
||||
`relays[].presenceInput` = the 1-based input terminal of an **induction loop / barrier presence
|
||||
signal** on the same [[dingtian-relay|controller]] (the Dingtian's inputs are decoupled from its
|
||||
relays, and loops are already in the [[bom]]). The rule makes one-car-one-ticket **physical**:
|
||||
### 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
|
||||
[[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**:
|
||||
|
||||
- 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**
|
||||
|
||||
@@ -46,11 +46,28 @@ see [[dingtian-vs-mqtt]].
|
||||
|
||||
## Driver & config API
|
||||
|
||||
The `dingtian` driver ([[device-registry]]) implements three capabilities:
|
||||
`AccessControlDevice` (relay pulse/latch over UDP), `InputDevice` (read inputs + poll-based
|
||||
press/release events ~50 ms), and `PreconditionDevice` (below). Config fields include a separate
|
||||
**`httpPort`** — the device's web/config API is on a configurable HTTP port (default **80**),
|
||||
distinct from the UDP control port 60001.
|
||||
The `dingtian` driver ([[device-registry]]) implements:
|
||||
`AccessControlDevice` (relay pulse/latch over UDP), `AuxOutputDevice` (latch a NON-barrier output —
|
||||
see below), `InputDevice` (read inputs + poll-based press/release events ~50 ms), and
|
||||
`PreconditionDevice` (below). Config fields include a separate **`httpPort`** — the device's
|
||||
web/config API is on a configurable HTTP port (default **80**), distinct from the UDP control port
|
||||
60001.
|
||||
|
||||
### 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.
|
||||
|
||||
### Per-input active level (`presenceActiveLow` / `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.)
|
||||
|
||||
### Precondition: input_link_relay must be OFF
|
||||
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
---
|
||||
type: entity
|
||||
tags: [parking, device, sensor, radar, entry, presence]
|
||||
sources: []
|
||||
updated: 2026-06-24
|
||||
status: settled
|
||||
---
|
||||
|
||||
# Hikvision Radar (vehicle-presence sensor)
|
||||
|
||||
A radar mounted at an entry barrier that **closes a dry-contact relay when it detects something in
|
||||
its vicinity** (a vehicle approaching the barrier). Wired to a **[[dingtian-relay|Dingtian]] input
|
||||
terminal**, it acts as the vehicle-**presence** signal for the entry flow — functionally the same
|
||||
role as an induction loop, just a different sensor.
|
||||
|
||||
## Where it sits in the model
|
||||
|
||||
The radar is a **child of the access controller config**, not a standalone device. On the entry
|
||||
relay's spec (`config.relays[]`):
|
||||
- `presenceInput` = the 1-based input terminal the radar's contact is wired to (e.g. **I2**).
|
||||
- `presenceKind: "radar"` = a label (vs. `"loop"`) for the UI + telemetry; the **gate behaviour is
|
||||
identical** either way.
|
||||
- `presenceActiveLow` = set when the radar idles HIGH and pulls LOW on detection (see below).
|
||||
|
||||
The booth's wiring (first install): **button on I1, radar on I2**, both on the same 4-input Dingtian.
|
||||
|
||||
## Its job: the one-car-one-ticket gate (advisory, never opens a barrier)
|
||||
|
||||
The radar feeds the **[[entry-double-press|one car = one ticket]]** gate exactly as a loop does: the
|
||||
entry button prints a ticket **only while the radar shows a vehicle present**, and **no second
|
||||
ticket** issues until the radar **clears** (the car drove in) and a new car re-occupies the zone.
|
||||
|
||||
> The radar is **advisory**. A detection NEVER opens a barrier on its own — it only *gates* the
|
||||
> button press. Entry still requires the physical press (and the capacity gate). This is the
|
||||
> [[threat-model]] rule: a sensor reading is never the sole reason a barrier opens. (Distinct from
|
||||
> the [[lane-presence-and-anpr-entry|ANPR bridge]], which admits *subscribers* through the gated
|
||||
> subscription flow — also never a transient open.)
|
||||
|
||||
## The active-level gotcha (why `presenceActiveLow` exists)
|
||||
|
||||
The Dingtian normalises **all** inputs against one board-wide resting level (`inputRestingHigh`).
|
||||
The booth's **button** (NO contact to GND) idles HIGH and pulls LOW on press. A **radar's dry
|
||||
contact may idle the opposite way** — and if it does, the controller would read "vehicle present"
|
||||
exactly when the zone is *clear*, inverting the gate (and the [[button-light-indicator|button
|
||||
lamp]]).
|
||||
|
||||
Fix: mark the radar's terminal **active-LOW** (`presenceActiveLow: true` on the relay spec). The
|
||||
driver then reads just that input inverted (active when LOW), leaving the button on the board
|
||||
default. Implemented as a per-input override in `access-dingtian.ts` (`inputActive()` +
|
||||
`inputActiveLow` set, derived from each relay's `presenceActiveLow`). Push-mode (the
|
||||
`/input/:n/:edge` HTTP path) relies instead on the device's own `ilu.active_level`; the override is
|
||||
the **poll-mode** equivalent.
|
||||
|
||||
## Also drives the button light
|
||||
|
||||
The same radar present/clear signal, combined with the camera's lane status, drives the entry
|
||||
button's 12 V lamp on a spare relay — see [[button-light-indicator]].
|
||||
|
||||
## Status
|
||||
|
||||
Modelled 2026-06-24 (button I1 + radar I2 on the first booth's Dingtian). Gate behaviour reuses the
|
||||
existing presence path; only the label + active-level override were added. Related:
|
||||
[[dingtian-relay]], [[entry-double-press]], [[lpr-camera]], [[entry-exit-points]].
|
||||
+4
-2
@@ -42,7 +42,8 @@ Counts: 4 sources · 19 entities · 45 concepts · 7 decision records.
|
||||
- [[lpr-camera]] — edge-AI plate recognition; host-side casual-identity source.
|
||||
- [[gee-qr-er80]] — QR access reader on hand; host-side serial → `read` bus (the QR-ticket scanner).
|
||||
- [[zkteco-controller]] — ❌ rejected/historical; aux-input path was a contender, not pursued.
|
||||
- [[dingtian-relay]] — ✅ CHOSEN access controller; decoupled inputs solve the button blocker (driver verified on hardware).
|
||||
- [[dingtian-relay]] — ✅ CHOSEN access controller; decoupled inputs solve the button blocker (driver verified on hardware); spare relays drive aux outputs (`setAux`).
|
||||
- [[hikvision-radar]] — vehicle-presence radar on a Dingtian input; the entry presence gate (per-input active-level caveat).
|
||||
- [[rongta-printer]] — ✅ CHOSEN 80mm thermal printer; ESC/POS over raw TCP 9100; driver written, one unit reachable at 10.0.10.6.
|
||||
- [[bom]] — reference bill of materials (barrier, loops, controller, readers, payment, host, network).
|
||||
|
||||
@@ -76,7 +77,8 @@ Counts: 4 sources · 19 entities · 45 concepts · 7 decision records.
|
||||
- [[challenge-response-auth]] — asymmetric nonce scheme for the ESP32 (auth + anti-replay).
|
||||
- [[entry-exit-readers]] — two populations, two integration paths; both can share a relay.
|
||||
- [[entry-exit-points]] — pool-of-spaces model (no lane); per-relay direction, reader→relay binding, camera snapshots.
|
||||
- [[entry-double-press]] — one car = one ticket: per-relay presence-loop gate (preferred) or cooldown fallback; suppressed press = telemetry.
|
||||
- [[entry-double-press]] — one car = one ticket: per-relay presence gate (loop OR radar) preferred, cooldown fallback; suppressed press = telemetry.
|
||||
- [[button-light-indicator]] — entry button lamp on a spare relay: radar × camera 3-state (blink/solid/off); aux-output; fails OFF.
|
||||
- [[uhppote-vs-esp32]] — comparison: detection vs. prevention.
|
||||
|
||||
## Concepts — business domain
|
||||
|
||||
+18
@@ -1552,3 +1552,21 @@ username chip links to it), `email` added to the session view + `SessionUser`. 7
|
||||
builds `.deb` + `.AppImage` on every push to dev/main and uploads them as UNSIGNED workflow artifacts
|
||||
(per-commit test build); the signed/versioned release stays on `release.yml` (tag `v*`). See
|
||||
[[desktop-shell-tauri]] "Desktop in CI".
|
||||
|
||||
## [2026-06-24] build | Radar presence input + button-light output on the Dingtian
|
||||
The first booth wired an **entry button on I1** and a **[[hikvision-radar|Hikvision radar]] on I2**
|
||||
(closes a dry contact on detection), plus the **button's 12 V lamp on a spare relay**. Modelled as
|
||||
children of the access controller config — no new device category. (1) The radar reuses the existing
|
||||
`relays[].presenceInput` one-car-one-ticket gate; added `presenceKind: loop|radar` (label) and
|
||||
`presenceActiveLow` (a radar may idle opposite the button — the Dingtian has ONE board-wide resting
|
||||
level, so a per-input override `inputActiveLow` inverts just that terminal; pure helper
|
||||
`inputActive()`). (2) New device-agnostic **`AuxOutputDevice.setAux(channel,on)`** capability (Dingtian
|
||||
latch) so business logic drives a NON-barrier lamp through the interface — barriers still only
|
||||
`pulseOpen` ([[barrier-not-a-door]] preserved). (3) New `ButtonLightController`
|
||||
(`apps/server/src/button-light.ts`): subscribes to the radar input edge + the camera
|
||||
[[lpr-camera|lane status]] and drives a **3-state lamp** — radar+car=SOLID, radar-only=BLINK (~1 Hz),
|
||||
else OFF; **fails OFF**; de-duped. (4) SetupWizard: presence kind + active-low + a button-light relay
|
||||
picker; i18n parity (sq+en). 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).
|
||||
A radar detection NEVER opens a barrier on its own — it only gates the button ([[threat-model]]). See
|
||||
[[hikvision-radar]], [[button-light-indicator]], [[entry-double-press]], [[dingtian-relay]].
|
||||
|
||||
Reference in New Issue
Block a user