From 5a5fedf4f44375c9bc5d83a6df3aa3679205a05a Mon Sep 17 00:00:00 2001 From: Julian Cuni Date: Wed, 24 Jun 2026 19:11:49 +0200 Subject: [PATCH] =?UTF-8?q?docs(wiki):=20booth=20bring-up=20fixes=20?= =?UTF-8?q?=E2=80=94=20relay=20password,=20secret=20re-merge,=20lamp=20con?= =?UTF-8?q?currency?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- wiki/concepts/button-light-indicator.md | 21 +++++++++++++++++---- wiki/entities/dingtian-relay.md | 22 ++++++++++++++++++++++ wiki/log.md | 18 ++++++++++++++++++ 3 files changed, 57 insertions(+), 4 deletions(-) diff --git a/wiki/concepts/button-light-indicator.md b/wiki/concepts/button-light-indicator.md index bcffb54..90eeaed 100644 --- a/wiki/concepts/button-light-indicator.md +++ b/wiki/concepts/button-light-indicator.md @@ -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]]. diff --git a/wiki/entities/dingtian-relay.md b/wiki/entities/dingtian-relay.md index b5e3783..05bd920 100644 --- a/wiki/entities/dingtian-relay.md +++ b/wiki/entities/dingtian-relay.md @@ -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 00 `) 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). diff --git a/wiki/log.md b/wiki/log.md index 1a1e076..491b691 100644 --- a/wiki/log.md +++ b/wiki/log.md @@ -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).