Files
julian 1efa77bf56 devices: pool-of-spaces model — drop lane, per-relay direction
A parking lot is one pool of spaces with a flexible set of entry/exit
points — no "lane". Direction is a property of each RELAY inside an access
controller; readers/cameras bind to a controller relay and inherit it.

Schema:
- drop `lane` from ledger_events, device_events, sessions
- rename lane_devices -> devices (no lane/direction columns)
- access config.relays=[{relay,direction,button?}]; reader/camera
  config.controllerId+relay binding
- fresh 0000_baseline migration (history reset; dev data was throwaway)

Signed ledger:
- remove `lane` from canonicalize(); bump signer keyId sw-hmac-v1 -> v2
  (v1 events won't verify under v2 — intentional, gated per-event by keyId)

Server:
- new device-resolve.ts (replaces lane-map.ts): relayForButton,
  relayForDevice, firstRelayByDirection, devicesByDirection
- entry-flow: button terminal -> its relay; exit/permit: reader's bound
  relay; dispatcher resolves the bound relay + inherited direction
- camera snapshots fire by direction site-wide, async, never block open
- DeviceConfig widened to nested JSON for relays[]

Web:
- wizard: no lane selector; add controllers (relay map + entry-button
  terminal) first, then bind readers/cameras/printers to a controller relay

Wiki: new entry-exit-points.md (replaces lane-direction); reworked
entry-exit-readers, parking-session, first-run-setup, device-registry,
append-only-event-chain, device-events; removed stale lane/LaneMap mentions.
2026-06-16 20:29:38 +02:00

5.8 KiB
Raw Permalink Blame History

type, tags, sources, updated
type tags sources updated
concept
parking
architecture
devices
entry-flow
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/<deviceId>/input/<N>/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/<N>/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.

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 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):

  • 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.