Skip to content

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.

Terminal window
cd riser-auth
scripts/phase1-up.sh # step-ca + EMQX + broker certs
scripts/phase2-up.sh # Authentik
docker compose up -d provisioning-svc

Commission (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, or offline_provision.py for deferred/no-connectivity) drives this and lands crt.pem + key.pem on 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:

Terminal window
API=https://backend-production-1145.up.railway.app/api/v1
TOK=$(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"}'

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

Terminal window
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.py

Controller (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.


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