Bring devices online
How a controller goes from a bare box to a live, controllable device on the operator dashboard. Same flow for the test bench today and the ModBerry later — only the serial owner and the cert-issue trigger differ.
Godview merge (2026-07-09): the standalone god-view service is gone. Points, provisioning, ingest, and commands all live in the main backend; the web surface is the operator app (
/fleet,/devices/{id},/devices/{id}/hmi), one JWT login.
A device needs three registrations. They are separate on purpose (separate services, separate auth):
| # | Registration | What it grants | Where |
|---|---|---|---|
| 1 | Broker identity — mTLS cert (CN = device id) + EMQX ACL | connect + publish riser/v1/{t}/{s}/{d}/telemetry,/ack, subscribe .../cmd |
riser-auth/ provisioning-svc /csr |
| 2 | Backend — device row + point catalog + API key | readings land in telemetry + point_current; UI shows the device; the bridge can POST as it |
POST /api/v1/buildings/{id}/provision-bench (or the /onboard “Register bench” card) |
| 3 | Runtime — bench bridge on the controller + backend bridge on the cloud | live values flow; commands reach the boiler | Pi/ModBerry + a backend-bridge service |
Device id / cert CN format: {tenant}-{site}-blr-{nn} (e.g. neos-franklin-blr-01).
The CN is the device id — EMQX authenticates it and the ACL pins each cert to its
own topic base, so the topic can’t be spoofed.
1. Broker identity — sign the device cert (full CSR flow)
Section titled “1. Broker identity — sign the device cert (full CSR flow)”Requires the riser-auth stack up (step-ca + EMQX + Authentik + provisioning-svc).
Docker-gated — run on a Docker host or the Pi, not the current Mac.
cd riser-authscripts/phase1-up.sh # step-ca + EMQX + broker certsscripts/phase2-up.sh # Authentikdocker compose up -d provisioning-svcCommission (admin seeds the factory record, tech enrolls, device makes its own key + CSR, service signs and writes the EMQX ACL — key never leaves the device):
- Automated end-to-end proof:
python scripts/commission_gate.py. - Real device:
/factory(admin) →/enroll(tech + PSK) → device/csr. The device agent (device-agent/agent.py, oroffline_provision.pyfor deferred/no-connectivity) drives this and landscrt.pem+key.pemon the box.
Manual bench shortcut (skips PSK, dev only): scripts/issue-device-cert.sh <device-id>
then scripts/authz-device.sh <device-id>. The remote-test broker’s static rules live
in edge/railway-emqx/acl.conf (includes the riser/v1/... tree).
2. Backend — register the device + its points + API key
Section titled “2. Backend — register the device + its points + API key”Creates/relinks the device row, installs the point catalog (hydronic/steam), stores the MQTT tenant/site for the command publisher, and issues the device API key once (shown once — it feeds the bridge keymap). Idempotent; re-running upgrades an existing device when the catalog grows and never re-issues the key.
Web (normal path): operator app → Onboard → “Register a bench” card (operator+).
CLI fallback:
API=https://backend-production-1145.up.railway.app/api/v1TOK=$(curl -s $API/auth/login -H 'content-type: application/json' \ -d '{"email":"you@org","password":"…"}' | jq -r .access_token)curl -X POST $API/buildings/<building_id>/provision-bench \ -H "authorization: Bearer $TOK" -H 'content-type: application/json' \ -d '{"device_id":"neos-franklin-blr-01","profile":"hydronic"}'3. Runtime — start the bridges
Section titled “3. Runtime — start the bridges”Cloud (once, serves every device): the backend bridge subscribes the broker and
POSTs into the backend as each device (BRIDGE_KEYMAP = {device_id: api_key}):
BRIDGE_BACKEND_URL=https://backend-… BRIDGE_KEYMAP=keymap.json \MQTT_HOST=<broker> MQTT_PORT=8883 MQTT_CA=… MQTT_CERT=… MQTT_KEY=… \python backend/scripts/run_bridge.pyController (bench today): benchd owns the Mega tty and keeps local interlocks
(safety rule 5). bench/workbench/riser_bench_bridge.py (systemd user unit
riser-bench-bridge.service) tails benchd’s SSE stream and speaks the broker:
telemetry: benchd /stream (°C) -> riser/v1/{t}/{s}/{d}/telemetry (rich, °F) -> riser/{t}/{s}/{d}/telemetry (legacy flat — iOS)commands: riser/v1/{...}/cmd {command_id, point_key, value} -> benchd POST /cmd -> riser/v1/{...}/ack {command_id, value}Config: ~/RISER/certs/riser-bridge.env on the Pi (see
bench/workbench/systemd/riser-bridge.env.example). Deploy/restart via
bench/workbench/run.sh; one-time install via systemd/install.sh.
Controller (ModBerry later): the edge runner is the agent — edge/tools/run.py
publishes the same rich v1 frames straight off the serial line.
Data flow (once all three are done)
Section titled “Data flow (once all three are done)”telemetry: controller --riser/v1/{t}/{s}/{d}/telemetry--> broker --> run_bridge.py --> POST /telemetry (+ /telemetry/points) --> Postgres --> /fleet + /devices/{id} + /devices/{id}/hmi (5s poll)command: operator UI --> POST /devices/{id}/commands (clamped vs point catalog) --> broker riser/v1/{...}/cmd --> riser_bench_bridge --> benchd /cmd --> riser/v1/{...}/ack --> run_bridge --> command status "applied"identity: /factory -> /enroll -> /csr --> step-ca cert (CN=device id) + EMQX ACLGated / not runnable from the dev Mac
Section titled “Gated / not runnable from the dev Mac”- Step 1 needs Docker (step-ca/EMQX/Authentik) — run on a Docker host + the Pi.
- One EMQX broker serves the whole
riser/*+riser/v1/*tree; device cert CN = device id everywhere (bench bridge, backend bridge keymap, provision endpoint).