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.
First boot and Wi-Fi
Today- Power the board. The panel announces its setup hotspot:
JOIN ME → DEVMATRIX-XXXX(the Xs are from your board's serial). - On your phone, join that
DEVMATRIX-XXXXWi-Fi network. A captive portal opens by itself. If it doesn't, browse tohttp://192.168.4.1. - 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.
- 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.)
- 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.
POST /api/v1/wifi/reset).Claim the device and pair more browsers
TodayThe phone that ran setup is already paired. For every other browser:
- Browse to
http://dmx-xxxx.local/and tap Pair. - The panel shows a 6-digit code — a white row, then blue.
- 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 identity → VERIFY NOW.
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.The Console, page by page
TodayServed 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
curlcommand. 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.
Push things from your own code
TodayGet $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).
The three kinds of apps
The app model (ADR-0026, owner: docs/FIRMWARE.md):
| Kind | Runs on | You need | Status |
|---|---|---|---|
| Declarative app — layout + data bindings; the device fetches its own data | the DK-01 | nothing else | Today (messages, flights list, custom layout) · more at M4 |
| Host app — a program pushing content over the LAN API | a machine you keep on | Pi / NAS / HA box / mini PC | Today (chapter 8) |
| Scripted app — sandboxed code in an on-device VM | the DK-01 | — | Deferred (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.
Flights Overhead — host app
TodayShows 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+.
- Console → Apps → Flights list → type your receiver's
aircraft.jsonURL. 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 likepiawareorraspberrypi) 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. - Set interval, rows, format, and List or Radar. Save — the config lives on the device, and any host machine obeys it.
- 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 - 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.
More examples and script details: examples/README.md.
Update firmware over the air
Today- Build the new
.bin(firmware/dk01/README.md → Build it) — or take a published release artifact once releases begin (Ahead · M0, signed). - Console → Deploy → OTA upload → choose the
.bin→ UPLOAD & REBOOT. It writes to the inactive slot and reboots into it; the Dashboard shows the new version and slot.
Recovery, resets, and re-flash to default
TodayThe never-brick ladder, mildest first:
| Action | How | What it wipes |
|---|---|---|
| Reboot | Dashboard → REBOOT, or POST /api/v1/reboot | nothing |
| Change Wi-Fi | Security → CHANGE WI-FI… (wifi/reset) | Wi-Fi credentials only — token and config survive |
| Rotate the token | Security → ROTATE LAN TOKEN | every paired browser/script credential |
| Factory reset | Security → FACTORY RESET (factory/reset) | everything in NVS: Wi-Fi, token, timezone, MQTT credentials, flights config |
| Factory reset over USB | esptool NVS erase — first section below; needs no token and no Console | same as factory reset |
| USB recovery | Double-press the board's reset button — it mounts as a USB drive; drag a UF2 firmware file on | nothing 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.
- Install the flasher once:
python3 -m pip install esptool. - Connect USB-C. The board appears as
/dev/cu.usbmodem*on macOS,/dev/ttyACM0on Linux. - Erase exactly the settings region (offsets from the flash map):
python3 -m esptool --port /dev/cu.usbmodem* erase-region 0x9000 0x5000 - 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.localonly 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.
Home Assistant and MQTT
TodayMQTT is optional and the broker is yours. I never operate one, and leaving the host empty keeps the device's MQTT client completely off.
- Run a broker you control, such as Home Assistant's Mosquitto add-on or Mosquitto on a Pi.
- Create one broker user for this device and scope its ACL to the
device's
devmatrix/<serial>/#tree, Home Assistant discovery writes, and thehomeassistant/statusbirth topic. - 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.
- 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.
- 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.
- Prove it from any terminal — optional, but satisfying. With the
mosquitto-clientstools 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 serialDMX-4E71-0952— yours is on the Console's Devices page:
Then push text through the broker. Every request is enveloped JSON — a UUID, amosquitto_sub -h <broker> -u <user> -P '<password>' -v \ -t 'devmatrix/DMX-4E71-0952/#'Z-suffixed UTC timestamp, and a short expiry so a stale queued command can never replay (contracts/mqtt.md owns the envelope):
The panel shows the text, and the device answers onmosquitto_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\"}}"devmatrix/DMX-4E71-0952/response/<that id>. The other implemented request verbs today aredisplay.brightness,display.clear, andapp.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).
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.
Troubleshooting
Today| Symptom | Fix |
|---|---|
| Captive portal never opened | Browse to http://192.168.4.1 while on the DEVMATRIX-XXXX network |
dmx-xxxx.local not found | Your 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 unauthorized | Stale token — re-pair (chapter 4) or re-copy from the Dev console view |
| Panel resets at high brightness | Under-powered supply. The 150 cap exists for this; the Dashboard's reset-reason tile confirms a brown-out |
| Clock is wrong | Settings → timezone; the clock needs one internet moment for SNTP after boot |
| Weather / Flights list / any fetching app shows only the clock | The 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 nothing | The host script isn't running — chapter 8; check systemctl status dmx-flights |
| MQTT stays disabled | Turn on ENABLE MQTT and enter a host; an empty host deliberately keeps MQTT off |
| MQTT shows error | Check 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 device | Confirm MQTT says connected and Home Assistant publishes online to homeassistant/status; then check the discovery-write ACL in the contract |
| Upload port busy while flashing | Close any serial monitor — the port is exclusive-open |
| Panel dark, no hotspot, but the USB port shows up | Probably 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 reset | The 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 unreachable | Chapter 10 → Factory reset over USB — no token needed, then set up again |
| Nothing works at all | USB recovery (chapter 10), then set up again — setup data is five minutes to recreate |
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).