--- type: concept tags: [parking, architecture, devices, entry-flow] sources: [] updated: 2026-06-15 --- # Device Input Flow (button → backend → relay) How a physical button press drives the entry lane. The **backend is the source of truth**: the device only *reports* the press; the host decides and commands the relay. This is the host-in-the- loop flow the [[dingtian-relay]] makes possible (and the [[uhppote-controller]] could not). ## The path (no polling) ``` car arrives → driver presses button (input I_N, dry contact to GND) → device HTTP-pushes GET …/api/devices/dingtian//input//on → backend: emit internal device event (device-events bus) → backend entry flow: create + sign an entry event, print the ticket → backend: pulseOpen(N) over UDP → barrier opens → (on release) device pushes …/input//off ``` - **Push, not poll.** The device's `input_link_url` feature is configured (by the driver's `configureInputPush()`) to call the backend on each input edge — see [[dingtian-relay]]. The driver's poll path remains only as a dev/fallback aid. - **Per-input path** carries the input number in the URL (`…/input/3/on`), so routing needs no body parsing. Both edges (`on`/`off`) are sent. - **Internal event bus** (`device-events.ts`, a Node `EventEmitter`) decouples the HTTP/transport layer from business logic — drivers/pushes emit; the entry flow subscribes. Keeps the app [[device-adapter-pattern|device-agnostic]]. ## Trust model (important — flat network, no VLAN) The site is a **flat network with no VLAN** ([[network-isolation]] is not yet enforceable here), so we do **not** trust the device or the network. Both directions now have defence-in-depth, but neither is the real boundary: - **Relay control (host → device)** — UDP, now via the Dingtian **binary protocol on :60000 with a `relay_pw`** (the only authenticated relay option; the string protocol has none). Set on the device + stored in `devices` by the harden step (below). - **Input push (device → host)** — guarded by **HTTP Digest auth** + a **source-IP allowlist**. - **The real guarantee is the signed log:** every barrier open is a host decision, recorded as a signed event BEFORE the relay fires ([[append-only-event-chain]]). An out-of-band open (which a flat network allows) has **no matching signed event → a detectable anomaly**. Device/network auth is just speed bumps; both are plaintext over a sniffable network. - This sharpens under the [[autonomous-direction|unmanned]] roadmap: with no operator, tamper detection via the signed log matters more than perimeter auth. ## Device hardening (on assign) The assign/Save step configures the device end-to-end (admin never touches the device web UI): fix preconditions (disable `input_link_relay`) → **harden** → set up input push. The `harden` capability ([[device-registry|HardenableDevice]]): - **Sets a random `relay_pw`** (1–9999) so binary relay commands need it; stores it in `devices` so the backend can keep commanding the relay. - **Disables unused protocol channels** (rs485, can, tcp×2, mqtt → `p:255`), keeping only UDP1 binary (relay control) + UDP2 string (status read) — fewer open doors. > **⚠️ Lesson (the hard way):** do **NOT** enable the device's HTTP CGI session check > (`session_en`). On this firmware (DT-R004) it makes the config-**read** API drop connections > (`ECONNRESET`), locking the backend out of the very API it depends on — it required a **factory > reset** to recover. The harden step deliberately leaves `session_en` off. The CGI config API > being open is accepted as part of the flat-network reality (the signed log is the guarantee); > the proper fix is network isolation, not this fragile device feature. ## Push authentication — Digest (decided by hardware testing) The secret must not be in the URL (sniffable, logged) and the password must not cross the wire in the clear. We **empirically tested the device** to pick the strongest achievable option: | Option | Device result | | --- | --- | | HTTPS (self-signed) | ❌ device won't push to a self-signed cert | | **Digest auth** (`auth=2`) | ✅ **works** — full 401-nonce challenge/response | | Basic auth | ✅ works (but password base64 on the wire) | | URL token | rejected by design (visible in URL/logs) | → **HTTP Digest** (MD5, qop=auth). The password is never sent (only a nonce-keyed hash); nonces are **single-use** (replay resistance). Per-device credentials (`pushUser`/`pushPassword`) are generated by the backend on **device assign**, written to the device's `input_link_url` config, and stored in `devices` — the admin never types a URL or secret. HTTPS would be stronger but the device can't do it here; Digest + the signed log is the practical answer on a flat network. See `apps/server/src/digest-auth.ts`. ## Dingtian config-write gotchas (cost a lot of debugging) Writing the device's config API (`/api/v2/config_set.cgi`) has two non-obvious traps — both now handled in the driver: 1. **Content-Length is mandatory.** The device's embedded HTTP server does **not** accept chunked request bodies. Node uses chunked encoding when `Content-Length` is absent, so the device silently ignores the body and returns `{"status":0}` anyway — the write looks successful but nothing changes. Always set `Content-Length`. 2. **The `pass` field caps at 31 chars** (longer is silently truncated → Digest mismatch). The generated push password is 24 hex chars (96 bits). 3. (Also: the device reboots on apply, so the driver writes then **polls until the change is verified**, retrying — back-to-back writes onto a rebooting device are lost.) ## Status Input push **verified on hardware** with Digest auth (all 4 inputs, real presses authenticated, no failures). The entry flow itself (signed event + ticket print + `pulseOpen`) is the next build — see [[dingtian-relay]].