DEVMATRIX DK-01 Owner's Manual Setup guide FW v0.12.5

DK-01 Owner's Manual

The complete instruction manual for using a DK-01: setup, everyday control, apps, updates, and recovery. Product specs live in their owner files (linked throughout); this manual owns the owner's step-by-step instructions.

Today  Current firmware v0.12.5 behavior. This slice is build-verified; final on-panel and live-receiver acceptance remains a hardware step. Ahead · gate X  Specified and coming; the gate names are ROADMAP.md's. Nothing labeled Ahead is a promise the current firmware keeps.
Ahead · R0 Sold units arrive already flashed and provisioned — this manual starts where a sold unit does: first boot. Getting firmware onto a bare board yourself (forks and DIY builds) is chapters 1–2 of the full manual on GitHub, with exact build commands in firmware/dk01/README.md.
3

First boot and Wi-Fi

Today
  1. Power the board. The panel announces its setup hotspot: JOIN ME → DEVMATRIX-XXXX (the Xs are from your board's serial).
  2. On your phone, join that DEVMATRIX-XXXX Wi-Fi network. A captive portal opens by itself. If it doesn't, browse to http://192.168.4.1.
  3. The portal scans and lists your networks live. Pick yours, type the password, and watch it join in real time — no blind reboot-and-hope.
  4. When the join succeeds, the phone that ran setup is signed in to the Console automatically. (If nothing taps Finish, the device closes its setup hotspot by itself 90 seconds after the join and boots onto your Wi-Fi.)
  5. After the restart the panel itself walks you to the last step: WIFI CONNECTED · LAST STEP: OPEN DMX-XXXX.LOCAL IN YOUR BROWSER. That card stays up until the Console reaches the device for the first time — opening the Console on any browser, or pairing (chapter 4) — then retires forever and the panel shows its clock. A factory reset re-arms it.
Wrong password? The portal tells you and lets you retry — the device doesn't reboot into limbo. Changed routers later? Console → Security → CHANGE WI-FI… reopens this flow (or POST /api/v1/wifi/reset).
Ahead · gate M0 USB Improv setup (join Wi-Fi over the cable, no hotspot step) — specified in docs/PORTAL.md; docs/MODES.md owns the gate.
4

Claim the device and pair more browsers

Today

The phone that ran setup is already paired. For every other browser:

  1. Browse to http://dmx-xxxx.local/ and tap Pair.
  2. The panel shows a 6-digit code — a white row, then blue.
  3. Type the code into that browser. It now holds the LAN token — the bearer credential every API call uses (docs/GLOSSARY.md).

Codes expire after 5 minutes, die after 5 wrong tries, and asking again never extends an active code's life. Reading the panel is the proof of possession: nothing to write down, and a lost browser never means factory reset — just pair again. To revoke every existing session at once: Security → ROTATE LAN TOKEN.

Pairing also pins the device's identity key: the box signs every later challenge with an Ed25519 key minted on its first boot, and the Console verifies the signature against the key it pinned here — so an mDNS spoofer squatting dmx-xxxx.local cannot impersonate your panel (ADR-0031; docs/SECURITY.md → Discovery & local transport). Check or re-run the proof any time: Security → Device identityVERIFY NOW.

Ahead · hosted cutover Prefer starting from the hosted Console? (docs/OPERATIONS.md owns the recipe; the domain is not live yet — until then the panel's own address above is the Console, and nothing is missing from it.) Once devmatrix.flighttrackerled.com is live: open it, follow the welcome screen, and enter the panel's address — Chrome, Edge, and Firefox ask once for local-network permission and then talk straight to the panel over your LAN. Safari doesn't allow that yet; use the panel's own address there. Either way the panel stays 100 % local — the hosted page is a static file, and nothing routes through a server of mine.
Ahead · M1 The full claim ceremony — session code on the panel plus a 2-second physical button hold, per docs/SECURITY.md → Ceremonies.
5

The Console, page by page

Today

Served by the device itself at http://dmx-xxxx.local/ — no internet needed. Today's Console has eight views, converged with docs/PORTAL.md from one codebase per ADR-0027. The hosted copy adds a welcome screen that walks a new owner from unboxing to a connected, identity-verified panel (or into a clearly-labeled interactive demo) — Ahead · hosted cutover, chapter 4:

  • Dashboard — live status tiles: firmware version and slot, display refresh (Hz), free heap, uptime, Wi-Fi signal, IP address, current scene, and last reset reason. A brown-out reset shows a visible alert. Use SEND TO PANEL for quick text, the live BRIGHTNESS control for the USB-safe 10–150 range, and IDENTIFY or REBOOT for quick device actions. The 64×32 paint canvas is now here too: choose a color, draw, then use PUSH FRAME or turn on LIVE STROKES.
  • Devices — shows the device that served this page with its name, serial, address, firmware, and online state. Pair another browser walks through reading the panel's 6-digit code; the new browser keeps its LAN token locally and retries the interrupted request.
  • Apps — opens with On the panel now: what the panel is showing this second, what's up next, and the full rotation order — every SAVE or SHOW NOW is visible here immediately, and action results pop up as a toast. The Messages card leads with your first app — 30 seconds: type words, press PUT IT ON THE PANEL, and they're saved, enabled, and showing; below the divider it edits and rotates up to eight offline phrases. Flights list enables the small on-device list and configures its local receiver URL, fetch interval, rows, and speed/altitude value; the Radar choice and copied command still need the host app in chapter 8. Custom layout leads with its starter template — ADD LIVE WEATHER installs a National Weather Service layout in one click (ADR-0015's no-key provider; US METAR stations) and fills the editor so you can see how it's built — then validates and saves any 2 KB JSON layout with literal rows or RFC 6901 bindings. Each on-device card can be enabled, assigned a scene interval, saved to NVS, or shown immediately. The Pixlet bridge card covers 1,000+ community-built Tidbyt-ecosystem apps via the owner-hosted bridge (chapter 8's host tier; installer in examples/). Ahead · gate M4 The Community Registry adds more reviewed apps, permission sheets, and one-click installation.
  • Deploy — shows the running version and slot. Choose a .bin, then UPLOAD & REBOOT to send it to the inactive OTA slot and watch progress while the device returns; chapter 9 covers the full process. Ahead · gate M0 Signed OTA verification and automatic rollback after a failed boot. USB recovery is available today.
  • Dev console — select any documented LAN API route, inspect its method and path, edit the JSON body where applicable, and build a ready-to-run curl command. COPY WITH MY TOKEN includes this browser's LAN token; health and claim routes remain open.
  • Security — the Device identity card shows the pinned Ed25519 key fingerprint and VERIFY NOW re-runs the signed-nonce proof (chapter 4). ROTATE LAN TOKEN logs out every other client, CHANGE WI-FI… removes only Wi-Fi credentials and reboots to setup, and FACTORY RESET wipes device settings after you type the exact confirmation. Ahead · gate M1 Optional account passkeys, hardware-key enrollment, and the timestamped exportable audit log.
  • Settings — choose a common Clock timezone preset or enter a custom POSIX STRING, then SAVE TIMEZONE. The same view shows the hostname, IP address, and current Console target, with FORGET / SWITCH DEVICE… to clear this browser's stored address, token, and pinned key. Its MQTT broker card holds the optional broker host/port, username, write-only password, TLS and enable toggles, plus a live connection status chip.
  • Guide — this manual's working summary, inside the Console: the five-minute setup path, what every page does, the Local/Cloud split, and first-line troubleshooting. Served by the panel itself, so the instructions survive an internet outage.
6

Push things from your own code

Today

Get $TOKEN from the Console's Dev console view (COPY WITH MY TOKEN). Full route list: firmware/dk01/README.md.

Text (up to 300 s on screen):

curl -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
     -d '{"text":"SHIP IT","duration_s":30}' \
     http://dmx-xxxx.local/api/v1/display/text

Multi-line boards: embed \n in text and the panel switches to a tiny 3×5 font — up to 5 rows of 16 characters, perfect for tabular boards like the flights list.

Full frames: POST /api/v1/display/frame takes one 64×32 frame as 4096 bytes of RGB565 (little-endian), base64-encoded in {"b64":…}. Host apps can add "lease_ms":3000; each new frame renews the lease, and the panel returns to its own rotation if the host disappears. Frames ride REST/WebSocket only, never MQTT (ADR-0029) — push them as fast as ~15 fps on your LAN.

Also useful: display/brightness (10–150), display/clear, identify (flashes the panel so you can find it), health (open, no token — good for monitoring).

7

The three kinds of apps

The app model (ADR-0026, owner: docs/FIRMWARE.md):

KindRuns onYou needStatus
Declarative app — layout + data bindings; the device fetches its own data the DK-01nothing else Today (messages, flights list, custom layout) · more at M4
Host app — a program pushing content over the LAN API a machine you keep onPi / NAS / HA box / mini PC Today (chapter 8)
Scripted app — sandboxed code in an on-device VM the DK-01Deferred (ADR-0026)

Messages, Flights list, and Custom layout need no other computer. Richer host apps such as the animated Flights Overhead radar still need a computer that stays on; M4 adds more reviewed declarative apps.

8

Flights Overhead — host app

Today

Shows aircraft your own ADS-B receiver hears — a live list, or an animated radar with altitude-colored aircraft, comet trails, runways, and green landing strobes. Local receiver only, by design (docs/VISION.md — never a feed of mine). The radar view pushes raw frames, so it is frame-layer and same-LAN in Local Mode; Cloud Mode's paid relay (Ahead · gate C1) is the only remote path (ADR-0029).

You need: a receiver on your LAN speaking the open dump1090/readsb aircraft.json format (a PiAware Pi qualifies), plus any always-on machine with Node 18+.

  1. Console → AppsFlights list → type your receiver's aircraft.json URL. The panel never scans your network (ADR-0032) — it only talks to addresses you give it. Don't know the URL? COPY FINDER PROMPT on that card puts a step-by-step request on your clipboard for Claude, ChatGPT, or any assistant: it walks through your router's connected-devices list (names like piaware or raspberrypi) and browser checks of the standard paths (http://IP:8080/data/aircraft.json, /skyaware/data/aircraft.json, /tar1090/data/aircraft.json) — no scanning tools anywhere.
  2. Set interval, rows, format, and List or Radar. Save — the config lives on the device, and any host machine obeys it.
  3. The Flights list card prints your exact run command under OPTIONAL RADAR HOST COMMAND. It looks like:
    DMX_URL=http://dmx-xxxx.local DMX_TOKEN=<your LAN token> \
      node examples/flights-overhead.mjs
  4. Flip List/Radar from the Console while it runs — no restart needed.

Keep it running when you close your laptop

The script must live on a machine that stays on, and the checkout must remain at the same path. Run the installer from the repository root; it prompts for the device URL and hides the token while you type it:

# macOS
node examples/install-flights.mjs

# Linux / Raspberry Pi (system files require root)
sudo "$(command -v node)" examples/install-flights.mjs

It installs and starts a launchd agent on macOS or a systemd service on Linux. The environment file is mode 0600; on macOS the credential-bearing plist is also mode 0600, and service output goes to ~/Library/Logs/devmatrix/. Optional overrides are --receiver-url, --airport, --view-mi, and --fps.

Preview every file and command without changing the machine, inspect the service, or remove it with:

node examples/install-flights.mjs --dry-run
node examples/install-flights.mjs --status
node examples/install-flights.mjs --uninstall       # keep flights.env
node examples/install-flights.mjs --uninstall --purge

On Linux, use the same sudo "$(command -v node)" ... prefix for install and uninstall. Re-running the installer replaces and restarts the existing service cleanly.

What it writes on Linux (systemd)

The installer writes credentials to /etc/devmatrix/flights.env and the following auditable unit to /etc/systemd/system/dmx-flights.service. The two ExecStart paths are resolved absolute paths on the machine running the installer.

[Unit]
Description=Devmatrix Flights Overhead
After=network-online.target

[Service]
EnvironmentFile=/etc/devmatrix/flights.env
ExecStart="/absolute/path/to/node" "/absolute/path/to/examples/flights-overhead.mjs"
Restart=always
RestartSec=5

[Install]
WantedBy=multi-user.target

It then runs systemctl daemon-reload and systemctl enable --now dmx-flights.service.

A thousand community apps — the Pixlet bridge

The owner-hosted Pixlet bridge runs the open-source, Tronbyt-maintained Pixlet engine and community catalog on your always-on machine, then pushes the rendered 64×32 frames straight to the DK-01. I render, proxy, and store nothing for this feature (ADR-0030).

You need: Node 20+ on the always-on machine, plus your own API keys for any apps that call outside services. One command fetches the rest — the Tronbyt Pixlet engine (sha256-pinned download), the community apps catalog, the bridge's one pinned GIF-decoder dependency — writes a starter config, and checks it can reach your panel (the Console's Pixlet card copies this with your panel's address filled in):

git clone https://github.com/JFForsythe/devmatrix
node devmatrix/examples/setup-pixlet.mjs --device http://dmx-xxxx.local

Prefer a browser to a JSON file? Pixlet Easy Mode wraps the whole loop in a local page — search the catalog, fill an app's settings in a form, preview the exact 64×32 result, pair with the panel by claim code, test-push one animation cycle, and build the rotation:

node devmatrix/examples/pixlet-manager/manager.mjs

It binds to 127.0.0.1 only, edits the same ~/tronbyt/bridge.config.json the bridge reads, and keeps the LAN token in a mode-0600 secret file next to the config — never in the JSON. Install the rotation as a background service with the installer below, exactly as before.

Prefer assembling the pieces by hand? The step-by-step path is in examples/pixlet-bridge/README.md.

Edit ~/tronbyt/bridge.config.json (written by the setup command), or craft your own anywhere and set BRIDGE_CONFIG to its absolute path:

{
  "device": {
    "url": "http://dmx-xxxx.local",
    "tokenEnv": "DMX_TOKEN"
  },
  "pixlet": "auto",
  "appsDir": "/absolute/path/to/tronbyt-apps",
  "rotation": [
    {
      "app": "apps/weather/weather.star",
      "duration_s": 15,
      "render_interval_s": 30,
      "config": { "location": "Chicago" }
    }
  ]
}

pixlet may instead be the binary's absolute path. An app may be a path relative to appsDir or a catalog name. Keep the LAN token out of JSON: the bridge reads the environment variable named by tokenEnv.

From the repository root, check the complete setup, push one app for one animation cycle, then install the rotation as a background service:

export BRIDGE_CONFIG="$HOME/tronbyt/bridge.config.json"   # or your own
DMX_TOKEN='<LAN token>' node examples/pixlet-bridge/bridge.mjs --check
DMX_TOKEN='<LAN token>' node examples/pixlet-bridge/bridge.mjs --once dvdlogo
node examples/install-pixlet-bridge.mjs --config "$BRIDGE_CONFIG"

The installer securely prompts for the token, records the absolute BRIDGE_CONFIG path and DMX_TOKEN in a mode-0600 environment file, and installs dmx-pixlet.service on Linux or com.devmatrix.pixlet on macOS. Preview it, inspect it, or remove it with the same lifecycle as the Flights installer above:

node examples/install-pixlet-bridge.mjs --dry-run --token 'test-only'
node examples/install-pixlet-bridge.mjs --status
node examples/install-pixlet-bridge.mjs --uninstall
node examples/install-pixlet-bridge.mjs --uninstall --purge

Use the same sudo "$(command -v node)" ... prefix on Linux.

Honest limits: raw frames are LAN-only (ADR-0029), so the bridge and DK-01 must share a LAN in Local Mode. These apps are community-maintained; quality, data sources, key requirements, and continued maintenance vary. Every render runs on the owner's hardware — I operate no Pixlet service.

More examples and script details: examples/README.md.

9

Update firmware over the air

Today
  1. Build the new .bin (firmware/dk01/README.md → Build it) — or take a published release artifact once releases begin (Ahead · M0, signed).
  2. Console → DeployOTA upload → choose the .binUPLOAD & REBOOT. It writes to the inactive slot and reboots into it; the Dashboard shows the new version and slot.
Honest limits, today: images are length/magic-checked but not yet signature-verified, and rollback on a failed boot is not automatic — both are M0 acceptance work. Until then, chapter 10 is the safety net. (firmware/dk01/README.md owns these caveats.)
10

Recovery, resets, and re-flash to default

Today

The never-brick ladder, mildest first:

ActionHowWhat it wipes
RebootDashboard → REBOOT, or POST /api/v1/rebootnothing
Change Wi-FiSecurity → CHANGE WI-FI… (wifi/reset)Wi-Fi credentials only — token and config survive
Rotate the tokenSecurity → ROTATE LAN TOKENevery paired browser/script credential
Factory resetSecurity → FACTORY RESET (factory/reset)everything in NVS: Wi-Fi, token, timezone, MQTT credentials, flights config
Factory reset over USBesptool NVS erase — first section below; needs no token and no Consolesame as factory reset
USB recoveryDouble-press the board's reset button — it mounts as a USB drive; drag a UF2 firmware file onnothing by itself — reflashes firmware

The TinyUF2 factory partition survives every OTA, so USB recovery is always there even if both app slots are bad. Physical access is the recovery tool — by design (docs/SECURITY.md). To turn a compiled .bin into the UF2 file the drive wants, follow firmware/dk01/README.md → "USB recovery (make a UF2)". The one thing that removes TinyUF2 is a deliberate full-chip erase — Back to default below covers when that is worth it and how everything comes back.

Factory reset over USB — no Console, no token

Lost the LAN token, wrong Wi-Fi saved, picked up a used board, or the Console is simply unreachable? You never need the token to start over. Every setting the device holds lives in one small flash region — NVS (docs/FIRMWARE.md → Hardware budget owns the flash map): Wi-Fi credentials, LAN token, identity key, timezone, MQTT settings, app config. Blank NVS is the out-of-box state, and the firmware in both app slots stays untouched.

  1. Install the flasher once: python3 -m pip install esptool.
  2. Connect USB-C. The board appears as /dev/cu.usbmodem* on macOS, /dev/ttyACM0 on Linux.
  3. Erase exactly the settings region (offsets from the flash map):
    python3 -m esptool --port /dev/cu.usbmodem* erase-region 0x9000 0x5000
  4. Tap the reset button. The panel comes back factory-fresh — JOIN ME → DEVMATRIX-XXXX — and chapter 3 takes it from there.

The identity key is minted fresh on the next boot, so browsers that paired before will show the identity warning — that is chapter 13's "key mismatch" row behaving exactly as designed: FORGET / SWITCH DEVICE…, then pair again.

Back to default — the complete re-flash

"Make it exactly like a fresh one" is two independent resets — pick the ones you actually need:

  • Settings to default: the USB factory reset above (or Security → FACTORY RESET while the Console still works).
  • Firmware to a known version: the cable flash from firmware/dk01/README.md, or the UF2 drag-and-drop from the ladder above — either writes the build you chose over whatever was running.

Doing both, in either order, is a complete return to stock. There is usually no reason to erase the whole chip — but for the true zero-mile state, or a flash you no longer trust:

python3 -m esptool --port /dev/cu.usbmodem* erase-flash

then run the cable flash from firmware/dk01/README.md. One upload restores everything the erase removed — bootloader, partition table, app, and the TinyUF2 factory partition (docs/FIRMWARE.md → Hardware budget). Know what you are choosing: between the erase and a completed upload the board has no firmware and no UF2 drive, and only the cable path brings it back. Never-brick still holds — the serial flasher the cable talks to lives in the chip's ROM, not in flash — but don't full-erase unless the build toolchain from that README is already set up.

Tips from the bench

Hard-won on real boards; each of these looked like a dead unit until it wasn't.

  • Dark panel, no hotspot, but the USB port shows up — the board is probably stranded in the ROM download mode, not broken. An interrupted flash, or any serial tool that toggles the port's DTR/RTS lines while opening it, can park the board there: dark on the LAN, alive on USB. Any esptool command that ends with a hard reset frees it:
    python3 -m esptool --port /dev/cu.usbmodem* --after hard-reset chip-id
  • Serial monitors lie on this board. The S3's USB serial port re-enumerates on every reset, so a monitor you hold open goes silently dead instead of erroring — and you miss the boot lines you were waiting for. Reopen the monitor after each reset, and close it completely before flashing (the port is exclusive-open).
  • A "dead" board is often your own Wi-Fi. Laptops and phones roam between networks, and guest networks isolate clients from each other. Before touching the hardware, check which network your computer is on — dmx-xxxx.local only resolves from the network the panel joined.
  • Never hardcode the port. macOS re-enumerates /dev/cu.usbmodem* constantly — glob it every time.
  • Experiments are cheap. After every OTA the previous firmware is still in the other app slot (the Dashboard shows which slot is live), and USB recovery is the floor under everything — update boldly.
11

Home Assistant and MQTT

Today

MQTT is optional and the broker is yours. I never operate one, and leaving the host empty keeps the device's MQTT client completely off.

  1. Run a broker you control, such as Home Assistant's Mosquitto add-on or Mosquitto on a Pi.
  2. Create one broker user for this device and scope its ACL to the device's devmatrix/<serial>/# tree, Home Assistant discovery writes, and the homeassistant/status birth topic.
  3. Console → Settings → MQTT broker. Enter the broker hostname or IP, port (1883 by default), username, and password; choose TLS if needed, turn on ENABLE MQTT, then SAVE MQTT.
  4. Watch the card's status move through CONNECTING to CONNECTED. The password is write-only: a blank password field leaves the saved value unchanged, and entering a value replaces it.
  5. With Home Assistant's MQTT integration and discovery enabled, the device publishes retained light-brightness, text, and notify configs with its availability topic. Home Assistant can control them with zero YAML.
  6. Prove it from any terminal — optional, but satisfying. With the mosquitto-clients tools and the broker user from step 2, watch the device's own topic tree; the retained availability, display, and health state appear immediately. The examples use the canonical serial DMX-4E71-0952 — yours is on the Console's Devices page:
    mosquitto_sub -h <broker> -u <user> -P '<password>' -v \
      -t 'devmatrix/DMX-4E71-0952/#'
    Then push text through the broker. Every request is enveloped JSON — a UUID, a Z-suffixed UTC timestamp, and a short expiry so a stale queued command can never replay (contracts/mqtt.md owns the envelope):
    mosquitto_pub -h <broker> -u <user> -P '<password>' \
      -t 'devmatrix/DMX-4E71-0952/request/display.text' \
      -m "{\"v\":1,\"id\":\"$(uuidgen)\",\"ts\":\"$(date -u +%Y-%m-%dT%H:%M:%SZ)\",\"expiry\":30,\"payload\":{\"text\":\"VIA MQTT\"}}"
    The panel shows the text, and the device answers on devmatrix/DMX-4E71-0952/response/<that id>. The other implemented request verbs today are display.brightness, display.clear, and app.show.

TLS is encrypted but not yet CA-verified in this pre-P2 firmware; use a trusted LAN or VPN path. The exact topics, envelope, QoS/retain rules, per-device Mosquitto ACL, and the broker WebSocket listener needed by a browser MQTT workbench are in contracts/mqtt.md (DRAFT until the P2 freeze).

12

Remote control

Today & Ahead
  • Your own infrastructure, free: put the LAN behind your own VPN/Tailscale (docs/MODES.md → owner-hosted remote), or point the device and a remote host app at a broker they can both reach. Today's MQTT semantic commands cover text, brightness, clear, and app selection; raw frames never ride a broker (ADR-0029).
  • Cloud Mode — offered only if demand requires it: a paid relay, fleet view, alerts — optional, never required, and the box never depends on it. docs/MODES.md is the line.
13

Troubleshooting

Today
SymptomFix
Captive portal never openedBrowse to http://192.168.4.1 while on the DEVMATRIX-XXXX network
dmx-xxxx.local not foundYour network blocks mDNS — use the IP the panel showed at setup; both work
Hosted Console can't reach the panel(Ahead · hosted cutover — the hosted domain isn't live yet; chapter 4.) Same Wi-Fi? Allow the browser's local-network permission when asked (Chrome/Edge/Firefox). Safari can't do this — open the panel's own address instead. Firmware older than 0.9.0 also can't answer the hosted origin; update from the panel's own Deploy page first
Identity warning (key mismatch)A reflash or factory reset legitimately changes the device key — Settings → FORGET / SWITCH DEVICE…, then reconnect and re-pair. If you didn't reflash, stop and check what's answering on that address
401 unauthorizedStale token — re-pair (chapter 4) or re-copy from the Dev console view
Panel resets at high brightnessUnder-powered supply. The 150 cap exists for this; the Dashboard's reset-reason tile confirms a brown-out
Clock is wrongSettings → timezone; the clock needs one internet moment for SNTP after boot
Weather / Flights list / any fetching app shows only the clockThe app has no usable data and is telling you why: check GET /api/v1/apps/diag (or run node examples/dmx-top.mjs) — too-big means firmware older than 0.10.0 (its 4 KB fetch cap was smaller than real feeds; update from Deploy), no-url means set the receiver URL, http-…/connect-failed means the source is unreachable, no-aircraft/bind-miss means the feed answered but held nothing to render
Apps → Flights list saves but the radar shows nothingThe host script isn't running — chapter 8; check systemctl status dmx-flights
MQTT stays disabledTurn on ENABLE MQTT and enter a host; an empty host deliberately keeps MQTT off
MQTT shows errorCheck the broker address, port, per-device username/password and ACL from chapter 11; pre-P2 TLS also requires a trusted network path
Home Assistant did not discover the deviceConfirm MQTT says connected and Home Assistant publishes online to homeassistant/status; then check the discovery-write ACL in the contract
Upload port busy while flashingClose any serial monitor — the port is exclusive-open
Panel dark, no hotspot, but the USB port shows upProbably stranded in ROM download mode, not dead — chapter 10 → Tips from the bench frees it with one esptool command
Serial monitor went silent after a resetThe S3's USB serial re-enumerates on every reset — reopen the monitor afterward; close it before flashing
Lost the token and the panel or its Wi-Fi is unreachableChapter 10 → Factory reset over USB — no token needed, then set up again
Nothing works at allUSB recovery (chapter 10), then set up again — setup data is five minutes to recreate
14

Rules this manual follows

Facts belong to their owner docs — this manual only walks you through them. If this file ever contradicts docs/PORTAL.md, docs/MODES.md, docs/SECURITY.md, docs/FIRMWARE.md, or an ADR, the owner doc wins and this manual gets fixed in the same change (CLAUDE.md).