docs(wiki): booth bring-up fixes — relay password, secret re-merge, lamp concurrency

- dingtian-relay: the "offline despite ping" gotcha (relay_pw in every binary frame,
  missing form field → Test connection sent 0 → timeout) + the identity-gated secret
  re-merge that stops a redirected probe exfiltrating the password.
- button-light-indicator: serialized desired-state worker (UDP is unordered → the lamp
  stuck on/off) and hot-reload of the lamp config (no restart).
- log entry for the three fixes (commits 420542c / fd15988 / 830993b).

Claude-Session: https://claude.ai/code/session_01Xcm6ikLgGoCxxHrxtjkk5V
This commit is contained in:
2026-06-24 19:11:49 +02:00
parent 830993bcb8
commit 5a5fedf4f4
3 changed files with 57 additions and 4 deletions
+17 -4
View File
@@ -51,12 +51,25 @@ a **device-agnostic aux-output** capability.
- **Fails OFF.** On host loss, shutdown, or a `setAux` error the lamp defaults OFF — a dead lamp is - **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 "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). 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 - **Serialized sends (must — UDP is unordered).** The first cut fired fire-and-forget `setAux` every
the controller over UDP. 500 ms; over **unordered UDP** the on/off packets reordered/overlapped and the relay **latched on
whichever packet the device processed last** — the lamp got stuck on/off at random (observed on
hardware). Fix: a **desired-state + serialized worker** (`#pump`). The blink timer only flips a
`desiredOn` flag; the worker guarantees **one in-flight send per lamp** and, on completion,
re-converges to the latest desired state. So the **final state is always authoritative** and a
lost/stale packet self-corrects. This also de-dupes (it skips a send when `confirmedOn === desiredOn`),
so the input stream never spams the controller.
- **Hot-reloads the config (no restart).** The lamp map is reconciled against the live device config
at start AND before each event (mirroring [[device-status-monitoring|DeviceMonitor]], which re-reads
the device set each tick) — adding/updating/dropping lamps. So a button light added or re-pointed in
the setup UI takes effect on the **next radar edge**, not after a server restart. (The first cut
loaded the map once at boot, so a just-saved lamp silently did nothing until restart.)
## Status ## Status
Built 2026-06-24 for the first booth (button I1, radar I2, lamp on a spare relay). Covered by Built 2026-06-24 for the first booth (button I1, radar I2, lamp on a spare relay); the serialized-send
`apps/server/src/button-light.test.ts` (the truth table + blink toggling + fail-OFF + de-dupe). + hot-reload fixes landed the same day after the lamp stuck on/off on hardware. Covered by
`apps/server/src/button-light.test.ts` (the truth table, blink toggling asserted on the device's
*confirmed* state, fail-OFF, de-dupe, and a lamp-added-after-start reconcile case).
Related: [[hikvision-radar]], [[entry-double-press]], [[lpr-camera]], [[dingtian-relay]], Related: [[hikvision-radar]], [[entry-double-press]], [[lpr-camera]], [[dingtian-relay]],
[[barrier-not-a-door]]. [[barrier-not-a-door]].
+22
View File
@@ -157,6 +157,28 @@ On assign the driver runs `harden()` (the [[device-registry|HardenableDevice]] c
> drop connections (ECONNRESET), locking out the API the driver depends on — recoverable only by > drop connections (ECONNRESET), locking out the API the driver depends on — recoverable only by
> factory reset. `harden()` deliberately never touches it. > factory reset. `harden()` deliberately never touches it.
## `relayPassword` field + the "offline despite ping" gotcha (2026-06-24)
`relay_pw` is in **every** binary frame — control AND the status read `healthCheck()` uses. With a
wrong/missing value the device **silently drops the packet** (no NAK), so the probe **times out →
the controller shows "offline" even though it pings** (ping is ICMP and never touches the binary
protocol). This bit a real bring-up: the driver read `config.relayPassword` but there was **no form
field** for it, so Test connection sent `0` → timeout → "offline", while `relay_pw` was actually a
non-zero value the harden flow had set. Diagnostic: a raw UDP status frame
(`FF AA <s> 00 <pwLo> <pwHi>`) replies *only* with the right password — `pw=N` → `ffaa…`, `pw=0` →
timeout — and binding the WSL socket to the device-facing NIC (`localAddress`) also broke the reply
(leave it unbound on WSL). Fix: a **"Relay control password"** config field (a **secret**; blank =
keep the stored value).
> 🔒 **Secret re-merge is identity-gated (don't let a redirected probe exfiltrate it).** Because
> `relayPassword`/`pushPassword` are redacted from the client ([[first-run-setup]]), the edit form
> can't resend them, so `/api/setup/test` re-merges the stored secret by device **id** — but ONLY
> when the submitted config addresses the **same device**: matching `driverId` and every
> connection-identity field it sets (`host`/`port`/`binaryPort`/`httpPort`/`serial`). A redirected
> host/port or mismatched driver returns NO secret, so an authenticated admin can't point a test at
> an attacker host and have the password sent there (the booth operator is the [[threat-model]]
> adversary). Save already merged from the stored row; this closes the same gap on test.
## Status — VERIFIED on hardware (DT-R004, sw V3.1.5461A, 10.0.10.172) ## Status — VERIFIED on hardware (DT-R004, sw V3.1.5461A, 10.0.10.172)
- ✅ status read (`0000:1111:4`), relay pulse, input press/release events (active-LOW, idle HIGH). - ✅ status read (`0000:1111:4`), relay pulse, input press/release events (active-LOW, idle HIGH).
+18
View File
@@ -1570,3 +1570,21 @@ picker; i18n parity (sq+en). Tests: `button-light.test.ts` (truth table + blink
`access-dingtian.test.ts` (active-level inversion). Workspace build+lint+test green (158 server tests). `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 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]]. [[hikvision-radar]], [[button-light-indicator]], [[entry-double-press]], [[dingtian-relay]].
## [2026-06-24] fix | Booth bring-up fixes — relay password, form split, lamp concurrency
Three fixes from wiring the radar/lamp on the first booth (committed 420542c, fd15988, 830993b on
top of the 2915d14 feature). (1) **"Offline despite ping"** — the Dingtian's `relay_pw` is in every
binary frame incl. the status read, but had NO form field, so Test connection sent 0 → device
silently drops the packet → "offline" (ping is ICMP, unrelated). Added a **"Relay control password"**
secret field; because the secret is redacted, the test endpoint re-merges it by device id but ONLY
when host/port/driver match the stored row (a redirected probe can't exfiltrate it — `setup-secrets.test.ts`).
(2) **Form split** — the controller editor now has separate **Outputs** (relays + pulse-open + lamp)
and **Inputs** (button + presence/radar terminals, "For relay N") sections; UI-only, storage
unchanged. `pulse open (ms)` clarified as a relay/output setting, not an input. (3) **Lamp stuck
on/off** — the blink fired fire-and-forget `setAux` over UNORDERED UDP; concurrent on/off packets
reordered and the relay latched on the last-processed one. Replaced with a serialized desired-state
worker (one in-flight send/lamp, re-converges to the latest state → final state authoritative). Also
**hot-reload**: the lamp map now reconciles against live config each event, so a button light added
in the UI works without a server restart. Workspace build+lint+test green (163 server tests). See
[[dingtian-relay]] ("offline despite ping" + secret re-merge), [[button-light-indicator]] (serialized
sends + hot-reload).