Files
parking_solution/wiki/entities/dingtian-relay.md
T
julian 2a86e578a8 Dingtian harden(): rotate the admin/admin web login (cosmetic)
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.
2026-06-14 19:00:42 +02:00

121 lines
7.6 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
type: entity
tags: [parking, hardware, access-control, relay]
sources: []
updated: 2026-06-14
---
# Dingtian Relay Controller
A network **relay + input** board (the unit on hand is the **4-channel** variant: 4 relays + 4
inputs). Chosen to drive the entry/exit lane because — unlike the [[uhppote-controller]] — its
**inputs are independent of its relays**, which solves the [[access-controller-button-flow]]
blocker (a button on an input does not auto-open a relay; the host decides).
SDK: `dingtian/4ch/sdk_v2_0_0/` (programming manual, examples). MIT-compatible use; no vendor
runtime needed.
## ⚠️ The one gotcha: `input_link_relay`
By **default the device links each input to auto-fire its matching relay** (`input_link_relay: 1`,
`on_action_on: [[0],[1],…]` in the config) — i.e. the *same* auto-open problem as the UHPPOTE.
The difference: **it is configurable.** Set `input_link_relay: 0` (or clear the action mappings)
so an input only *reports* and the host commands the relay. **This config step is mandatory** for
the ticket-first entry flow. See [[autonomous-direction]].
## Protocol (Dingtian string — what we use)
Transport options: UDP/TCP string, UDP binary, HTTP CGI, Modbus, MQTT. We use **HTTP + UDP** —
see [[dingtian-vs-mqtt]].
- **Relay control — UDP *binary*, port 60000 (authenticated):** the driver's `pulseOpen` sends a
binary "write relay with jogging" frame carrying the `relay_pw` (the only relay option with a
password). Frame (verified on hardware):
`FF AA <session> 03 <pwLo> <pwHi> <relayByte> <jogLo> <jogHi>` — relayByte bit0=on, bits1-7=
channel-1; jog is 100 ms units, LSB-first; password 16-bit LSB-first (0 = none). The relay jogs
ON then auto-releases, so we never time a close ([[barrier-not-a-door]]). *(The simpler string
protocol — `1`+ch on, `2`+ch off, `11*` jog — works too but has no auth; we use it only for the
read-only status query.)*
- **Status / inputs — send `00`** → `「relays」:「inputs」:「count」`, e.g. **`0000:1111:4`** (4ch:
relays off, inputs high). `0` = OFF/Low, `1` = ON/High. Poll-based.
- **Input push — `input_link_url`:** device **HTTP POSTs to a host URL on input change** — the
push path for button events without a broker.
- **Discovery:** UDP multicast `224.0.2.11:60000`, send `\x05\xAA` (devices reply). Defaults:
IP `192.168.1.100`, UDP `60000` (binary) / `60001` (string).
- Binary protocol (port 60000) adds optional **password** + multicast; bitmask relay/input maps.
## 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.
### Precondition: input_link_relay must be OFF
The driver reads the device's JSON config (`GET /api/v2/config.cgi`) and **checks
`input_link_relay`**; if enabled it reports a fixable issue, and `fixPreconditions()` writes the
correction (`POST /api/v2/config_set.cgi`) — setting the flag to 0 and clearing `on_action_on`,
preserving everything else (network, etc.). This is the generic [[device-registry|precondition]]
capability: the app doesn't own full device config (that's the vendor web UI), only the few
settings our flow depends on.
> **Write gotcha (cost real debugging):** the GET config payload **omits** a `"command"` field, but
> the set endpoint **requires `"command":"setconfig"`** injected right after `"status"`. Without it
> the POST returns/looks like success but silently does nothing (and the device may reset). With it,
> POST returns `{"status":0}` and the change sticks. JSON node order must be preserved.
## Input push (no polling) — the chosen architecture
The device **pushes** button events to the backend; the backend decides. **No polling.** The
driver's `configureInputPush()` writes the device's `input_link_url` config to point each input at
the backend: input N HTTP-GETs `…/api/devices/dingtian/<deviceId>/input/<N>/on` (and `/off`) on
press/release. The backend ([[fastify]] route `routes/devices.ts`) translates each push into an
internal device event ([[device-input-flow]]); the entry flow then prints a ticket and commands
the relay via UDP. See [[device-input-flow]] for the full path + trust model.
> The input-poll path in the driver (`onInput`) remains as a dev/fallback aid, but **push is the
> real path** — lower latency, and it can be authenticated (the device supports Basic/Digest +
> HTTPS on the push), unlike the open UDP control direction.
## Hardening (`harden()`) — and why HTTP auth is not a boundary here
On assign the driver runs `harden()` (the [[device-registry|HardenableDevice]] capability):
1. **`relay_pw`** — set a random relay password so binary relay commands (UDP 60000) need it.
2. **Disable unused channels** — set `p:255` on rs485/can/tcp×2/mqtt; keep only UDP1 binary
(relay control) + UDP2 string (status read).
3. **Rotate the `admin`/`admin` web login** — `GET /userset.cgi?<old_u>&<old_p>&<new_u>&<new_p>&`
(response `&0&…&` = success, verified on hardware). The new password is stored back in
config (`webUser`/`webPassword`) so a re-run can rotate again (the device checks the *old*
creds). This step is **best-effort** — a failure logs and does not fail the assign.
> ⚠️ **The device CGI API is UNAUTHENTICATED.** Verified on hardware: `GET /api/v2/config.cgi`,
> `/`, and even `/userset.cgi` all return **200 with no credentials**. The `admin`/`admin` login
> gates only the interactive **browser UI** — the CGI control plane (read/write full config, fire
> relays, change the password) bypasses it entirely. The `http` config block has **no** setting to
> require Basic/Digest on inbound requests; the only inbound gate is `session_en`, which **bricks
> the config-read API on this firmware** (the factory-reset incident — *do not enable it*). So
> **rotating the login is cosmetic** (stops a casual browser reaching settings); it is **not** a
> boundary. On this flat, no-VLAN network the device control plane is effectively open — the
> **signed event log is the real anti-fraud guarantee**. See [[device-input-flow]].
> ⚠️ **`session_en` must stay OFF.** Enabling the HTTP CGI session check makes the config-read API
> drop connections (ECONNRESET), locking out the API the driver depends on — recoverable only by
> factory reset. `harden()` deliberately never touches it.
## 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).
- ✅ **`input_link_relay` disabled via the driver** → pressing an input reports the event and
**fires NO relay** (`0000` after presses). The [[access-controller-button-flow]] blocker is
**solved**.
- ✅ **Input HTTP-push end to end** — configured the device via `configureInputPush()`, then real
button presses (all 4 inputs) **pushed to the backend** (`/input/N/on` + `/off` per press,
source = the device IP). No polling. Host-in-the-loop entry (`button → backend → ticket →
backend opens relay`) is real.
- ✅ **Web-login rotation** — `userset.cgi` rotates `admin`/`admin` (response `&0&/&`; wrong old
password → `&2&/&`). Confirmed the device validates the old creds. **Also confirmed the CGI API
needs NO auth** (config dump + `userset.cgi` return 200 unauthenticated) → rotation is cosmetic.
- ⬜ Next: wire the actual entry flow (input event → signed event + print ticket → `pulseOpen`).