- 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
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
Fix two bugs found running the real assign flow: the saved web password
didn't match the device (login stayed admin/admin), and the UDP2 warning
never reached the admin.
Web password:
- Split the conflated field into webPassword (the DESIRED login; blank ->
auto-generate) and webPasswordCurrent (the device's EXISTING password used
as the old cred, default admin). Before, an admin typing a desired password
made harden send it as the old cred -> rotation failed -> but the DB still
saved the typed value, so it claimed a password the device never accepted.
- harden() now rotates current -> desired, VERIFIES by re-authenticating with
the new password, and only returns secrets.webPassword on success (else a
warning, nothing saved). Stores webPasswordCurrent for future re-runs.
- assign strips the typed webPassword/webPasswordCurrent and persists only the
verified secret -- the DB never claims an unapplied password.
Warnings to the UI:
- assignDevice returns warnings[]; SetupWizard shows them in an amber
"saved, but action needed" banner per category. This is how the admin learns
the firmware wouldn't disable UDP2 (finish in the device web UI).
Verified on hardware: after harden the device rejects admin/admin and accepts
the chosen password; the UDP2 warning surfaces.
The string protocol (UDP 60001) has no password field but can fire relays
("11" = relay 1 on), bypassing relay_pw entirely. Proven on hardware: an
unauthenticated packet opened a relay. harden() had left it enabled "for
status reads".
- #status() now reads via the authenticated binary command (relay cmd 0x00)
instead of the string protocol, so the string protocol is no longer needed.
- harden() disables the string protocol (udp2.p=255). BEST-EFFORT: firmware
V3.6J's config API silently refuses to disable udp2 (the device web UI can),
so it's not part of the blocking verify -- harden() re-checks and returns a
warning instead of throwing. After a web-UI disable, the attack is dead and
binary control/status still work (verified on hardware).
- HardenResult gains an optional `warnings[]`; the assign route surfaces them
to the admin and logs them.
- Corrected the false comment claiming relay_pw stops an attacker (it is
defence-in-depth on plaintext UDP, not a boundary).
- Thread localAddress through the driver's UDP/HTTP calls so a multi-homed
host sources device traffic from the device-facing NIC.
- Device web login (webUser/webPassword) is no longer redacted from setup
state -- it's an operational credential for the admin-only device area;
pushPassword/relayPassword stay machine-only.
Wiki: document the vuln + fix, the firmware caveat, and the out-of-band
actuation gap (the log captures host actions only; reconciliation vs. an
independent witness is the real control and is not yet built).
harden() now rotates the device's default admin/admin web-UI login via
GET /userset.cgi?<old>&<old>&<new>&<new>& (best-effort: a failure logs
and doesn't fail the assign). The new password is stored back in config
(webUser/webPassword) so a re-run can rotate again, and is stripped from
the assign response like the push secret.
Documented the load-bearing caveat: this device's CGI API is fully
UNAUTHENTICATED — config read/write, relay fire, and userset.cgi itself
all return 200 with no credentials (verified on hardware). admin/admin
gates only the browser UI, and there's no inbound-auth setting (only
session_en, which bricks the read API). So the rotation is defence-in-
depth for the UI, NOT a boundary; the signed event log remains the real
anti-fraud guarantee. Verified rotation end-to-end on 10.0.10.5
(success &0&, wrong-old-pw &2&); device left at admin/admin.
Lock down the relay device for the flat (no-VLAN) network.
Relay control:
- pulseOpen/setRelay now use the Dingtian BINARY protocol (:60000) with a
relay password — the only relay option with auth (string :60001 has none, and
is kept only for the read-only status query). Frame verified on hardware.
HardenableDevice capability (driver harden()):
- set a random relay_pw (1-9999); disable unused channels (rs485/can/tcp x2/mqtt
-> p:255), keeping UDP1 binary (control) + UDP2 string (status).
- write-verified (device reboots on apply).
Assign/Save flow now does: fix preconditions -> harden -> set up input push;
the relay password is stored in lane_devices so the runtime device can command
the relay.
DELIBERATELY NOT touching the device's HTTP CGI session check (session_en):
enabling it on this firmware breaks the config-READ API (ECONNRESET) and locked
the backend out — required a factory reset to recover. The open CGI API is
accepted as flat-network reality; the signed event log is the real guarantee.
Verified end to end on hardware: assign hardens + configures the device, config
API stays reachable, pulseOpen with the stored password fires the relay, without
it is rejected. wiki: device-input-flow + dingtian-relay updated.
The device pushes button events to the backend via its Input Link URL feature;
the backend decides. No polling — the chosen entry architecture.
packages/devices:
- dingtian driver: configureInputPush() writes the device's input_link_url
config (per-input server/port/path, en=1, active-LOW, plain HTTP) so each
input HTTP-GETs the backend on press/release. Extracted #readConfig/#writeConfig
(with the required command:setconfig injection + post-write reset tolerance).
apps/server:
- routes/devices.ts: public GET/POST
/api/devices/dingtian/:deviceId/input/:n/{on,off} — translates a device push
into an internal device event. Not behind cookie/CSRF (machine call from the
device); trust comes from the signed event log, not this request.
- device-events.ts: internal EventEmitter bus so the entry flow subscribes to
input events without coupling to HTTP. Wired into the server.
Verified on hardware: configured the device, then real presses on all 4 inputs
pushed to the backend (input N on+off, source = device IP). No polling.
wiki: device-input-flow concept (path + trust model for the flat/no-VLAN
network); dingtian-relay updated; index + log.
The Dingtian board's inputs are independent of its relays (configurable), so a
button on an input can report to the host WITHOUT auto-firing a relay — solving
the access-controller-button-flow blocker the UHPPOTE/ZKTeco couldn't.
packages/devices:
- access-dingtian.ts: `dingtian` access driver implementing AccessControlDevice
(relay pulse/latch via UDP string protocol :60001), InputDevice (read inputs +
poll-based press/release events, active-LOW), and the new PreconditionDevice.
- PreconditionDevice capability on the interface: a device can report config it
requires for parking and optionally fix it. Dingtian checks input_link_relay
via the HTTP config API and can disable it.
- httpPort config field — the web/config API port is separate from UDP control
(this unit uses 8080, not the default 80).
- Register dingtian; export driver objects from the package.
Verified on real hardware (DT-R004 @ 10.0.10.172): status read, relay pulse,
input events; disabled input_link_relay via the driver, then confirmed pressing
inputs fires NO relay (0000) — host-in-the-loop entry works.
Config-write gotcha recorded: config_set.cgi requires "command":"setconfig"
injected after "status" (GET omits it) or the POST silently no-ops.
apps/server/scripts/dingtian-test.mjs: status / watch / pulse hardware test.
wiki: dingtian-relay verified; button-flow marked RESOLVED; index + log.
- dingtian-relay: relay+input controller (4ch on hand). Inputs are decoupled
from relays (configurable via input_link_relay) — solves the
access-controller-button-flow blocker the UHPPOTE couldn't. Full protocol from
the SDK (UDP string control :60001, `00` status parse, input_link_url push,
multicast discovery). Driver + hardware test still to build.
- dingtian-vs-mqtt: use direct HTTP/UDP now; MQTT skipped (broker = extra infra
+ failure mode + overkill at one-host/few-devices scale) but kept for later
multi-lane scale.
- autonomous-direction: record the roadmap to fully unmanned (no booth) and how
it reshapes the threat model (operator-fraud -> unattended-machine threats),
makes host-in-the-loop entry mandatory, and raises fail-state stakes.
- threat-model: note the unmanned shift. index + log.
gitignore the vendor SDK (dingtian/, 71MB of binaries/examples) — reference
only, protocol captured in the wiki.