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 (commits420542c/fd15988/830993b). Claude-Session: https://claude.ai/code/session_01Xcm6ikLgGoCxxHrxtjkk5V
This commit is contained in:
@@ -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
|
||||
"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.
|
||||
- **Serialized sends (must — UDP is unordered).** The first cut fired fire-and-forget `setAux` every
|
||||
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
|
||||
|
||||
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).
|
||||
Built 2026-06-24 for the first booth (button I1, radar I2, lamp on a spare relay); the serialized-send
|
||||
+ 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]],
|
||||
[[barrier-not-a-door]].
|
||||
|
||||
@@ -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
|
||||
> 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 read (`0000:1111:4`), relay pulse, input press/release events (active-LOW, idle HIGH).
|
||||
|
||||
+18
@@ -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).
|
||||
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]].
|
||||
|
||||
## [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).
|
||||
|
||||
Reference in New Issue
Block a user