Setup·
From an unboxed UNO Q to a board that serves q.cagatay.my. Commands marked verified were run on the real board.
1. First boot·
Plug the board directly into the Mac with a USB-C data cable. It enumerates as an ADB device.
The arduino password is expired on first boot, so set one now. Until then sudo and SSH are disabled. If the first plug fails to enumerate (descriptor timeout in the kernel log), unplug and plug again.
2. Wi-Fi·
adb -s 2431388868 shell nmcli dev wifi connect "<ssid>" password "<psk>"
adb -s 2431388868 shell nmcli con mod "<ssid>" connection.autoconnect yes
Verified: the board joins and gets 192.168.1.210.
3. SSH from the Mac·
Verified: key auth works, ssh q resolves.
4. Fleet daemon·
Verified on the board: distro Node 20.19.2 and npm 9.2 (about 300 MB, the leanest option), a pinned tiny-tech in ~/.tiny/daemon, and q-tiny.service enabled and active as user arduino. Verified from the Mac: use_device invoke round-trips to uno-q.
Run on the board, as arduino, after /etc/q/env exists (root, 0600, model credentials):
scripts/board/install-tiny.sh # idempotent: nodejs/npm, ~/.tiny/daemon, stub, systemd unit
systemctl is-active q-tiny
Enrollment happens once and needs the owner's ~/.tiny/credentials.json on the board (0600); the exact one-liner is in the script header. That session expires after about 52 days (around 2026-10-31); log in again or copy it again then.
Fleet device, not a LAN mesh peer
tiny-tech's zenoh transport ships no linux-arm64 prebuilt. scripts/board/dialtone-stub.cjs stands in for the native binding so the daemon starts: presence, heartbeat and use_device tasks work, LAN mesh discovery does not. Remove the stub the day a linux-arm64 build is published.
The owner token for the dashboard lives at ~/.q/token (48 hex characters, mode 0600). It is read on the board only, never copied into a file that leaves it.
5. Dashboard and tunnel·
Verified live on 2026-09-09 (read over ssh q and from the public edge): q-dash.service and q-tunnel.service are both active, /api/health answers on the board and at https://q.cagatay.my, and the gate holds at the edge: /api/state, /api/telemetry and / all return 401 without a key, /api/auth/me returns {"via": null, "owner": "cagataycali"}.
q-dash.service, as installed on the board:
| key | value |
|---|---|
User |
arduino |
After |
network-online.target arduino-router.service |
EnvironmentFile |
/etc/q/env (root, 0600: model credentials) |
Environment |
Q_HOME=/home/arduino/.q, Q_OWNER=cagataycali, Q_TICK_S=2, Q_ROUTER_SOCKET=/var/run/arduino-router.sock |
ExecStart |
/opt/q/bin/python -m uvicorn dashboard.server:app --host 127.0.0.1 --port 8095 --no-access-log |
MemoryMax |
700M |
q-tunnel runs cloudflared with TUNNEL_GRACE_PERIOD=5s and TimeoutStopSec=15. The default grace period is 30 s and the dashboard's SSE streams never finish, so every tunnel restart (self-heal, unit change, reboot) used to mean about 35 s of Cloudflare 530s; measured on 2026-09-09 a restart is now 6.5 s of non-200 from the Mac (stop 21:56:38, four edge connections registered again by 21:56:44). The flag belongs to the tunnel command, not to run; the environment variable avoids the position question, and a wrong position crash-loops cloudflared with flag provided but not defined, which is how NIGHT caused a 2.5 min public outage that night before correcting it.
q-heal.timer runs systemd/q-heal.service as root once a minute (python -m q.ext.heal --act): it reads the watcher's verdict from GET /api/ext/heal and may run exactly two bounded actions, systemctl restart q-tunnel when the public URL failed three spaced probes while the internet is up, and systemctl restart arduino-router when the MCU stopped answering, each with a 600 s cooldown and a record in ~/.q/heal.jsonl. q-dash itself runs with NoNewPrivileges=true and never restarts anything. With q-dash down the timer skips and exits 0. Measured cost: about 3 s CPU per run on the A53s (the import path pulls in FastAPI), ~29 MB peak.
The Python environment lives in /opt/q and Q_FAKE is unset. Since c9587b4 on main, /api/health.source reports what the adapters actually deliver; with the q package deployed (BOARD e463df1) it reads board, as does /api/state.source.
Deploying a new main to the board is one command from the Mac:
scripts/board/deploy.sh # rsync ~/q-the-brain → q:~/q-the-brain, /opt/q venv, pip -e .[agent], units, restart q-dash
scripts/board/deploy.sh ~/other-checkout --no-restart
It needs the ssh q alias and the board's sudo password, read from Q_SUDO_PW or a local file outside the repo. The password is never committed and never appears in these docs. q-tunnel, q-tiny and q-heal.timer are restarted only when their unit file changed.
Static assets follow the deploy
The dashboard's signed-in page needs dashboard/static/assets/ and index.html on the board. A board checkout older than the frontend commit serves the login gate correctly but an empty page after sign-in (/assets/app.js 404). This happened once on 2026-09-09 and was fixed by redeploying main with deploy.sh (assets 200 afterwards). Always deploy from main, never from a lane worktree.
Readings are real, the matrix is not yet
With BOARD e463df1 deployed, /api/state and /api/telemetry report source: "board": thermal zones, load, memory, disk, Wi-Fi, unit states and mcu.link come from the board. mcu.sketch is none and led.applied is false with method q/matrix not available until the q sketch is flashed to the STM32. The RGB user LED works today.
q-tunnel.service runs cloudflared 2026.9.0 with /etc/cloudflared/q.yml (ingress q.cagatay.my → http://127.0.0.1:8095, credentials root 0600).
Verify·
tools/verify-live.sh on the Mac is the read-only end-to-end check: units on the board, board-local /api/health, https health at the edge, the gate on /api/state and /, and the board in use_device list. Exit 0 means all green.
On the board, q doctor runs the same checks with more detail (20 today: units incl. the q-heal timer, Wi-Fi power save, tunnel, MCU link, temperatures, disk, journal size, unit failures this boot). q doctor --history shows ~/.q/doctor.jsonl: one line per verdict change or new boot, so a regression between two reboots has a timestamp.
Reboot test·
scripts/board/reboot-test.sh reboots the board from the Mac and measures how it comes back: seconds until SSH, /api/health, all units active, https at the edge and the fleet daemon, then the doctor verdict, q-tunnel restarts in this boot and the last doctor-history lines. It needs the arduino sudo password once, through the Q_SUDO_PW variable (the same one deploy.sh uses); the script does not read or store it itself. --dry-run measures the same gates without rebooting.
Measured 2026-09-09 21:20Z (NIGHT), after the unit changes (StartLimitIntervalSec=0, Restart=always, q-tunnel waits for nss-lookup.target and a DNS answer before starting): SSH back 40 s, /api/health 42 s, all units 42 s, https at the edge 42 s, fleet daemon 44 s, systemd-analyze 37.9 s to userspace done, q-tunnel registered both connections at 21:20:39Z with 0 restarts, doctor 18/18 with no failed units in the new boot. The earlier boot had 3 q-tunnel start failures from DNS not being ready yet; that is what this run cleared.
Wi-Fi latency
wlan0 (ath10k, WCN3990) ships with 802.11 power save on, which showed up as RTT jitter from the Mac: 20 pings avg 152 ms, max 629 ms, stddev 147 (2026-09-09 21:39Z, signal -47 dBm). NIGHT turned it off on the connection profile, so it survives reboots: sudo nmcli connection modify Verizon_SG4VBJ 802-11-wireless.powersave 2 && sudo nmcli connection up Verizon_SG4VBJ (3 to 5 s blip, same IP). After: avg 55 to 80 ms, max about 210 ms over three runs of 20 to 30 pings; iw dev wlan0 get power_save reports off (iw 6.9 installed from Debian for this). q doctor has a "wifi power save off" check so the setting cannot silently come back. The remaining spikes were then measured from the board itself: to the router, p50 2.8 ms but p90 about 204 ms, bimodal (either ~3 ms or ~205 ms, which is exactly two 102.4 ms beacon intervals), the same under a 20 pps flood, with iw station dump showing 0 tx retries, 4 failed of 11,659, signal -43 dBm, DTIM 3. So it is not the Mac, not load, not retries, and not the client power-save flag (off): frames are being held for a beacon somewhere between the ath10k firmware and the AP (5 GHz channel 104, 80 MHz). No client-side knob left that we know of; the AP is the owner's. Practical effect on the dashboard: an occasional 200 ms on one request, nothing more.
6. Back on the hub·
Move the board to the Arduino 8-in-1 hub for HDMI, Ethernet and the USB stick. The board becomes the USB host, so the Mac no longer sees it over USB. Use ssh q.