XIAO ESP32S3 multi-board MQTT dashboard
Architecture: N boards (STA mode, same WiFi) → MQTT broker (Mosquitto, one PC
on that WiFi) → browser dashboard (MQTT-over-WebSocket, mqtt.js). Each board
runs the same compiled firmware — no per-board edits — because it derives a
unique ID from its own MAC address.
Read the xiao-esp32s3 skill (same repo) first for base arduino-cli workflow
and pitfalls (PSRAM flag, DTR reset-stuck recovery, bounded serial reads).
This skill adds the broker + networking + dashboard layer on top.
Step 1 — ALWAYS ask the user first
Never invent credentials or assume where the broker runs. Ask for:
- WiFi SSID / password — must be a network every board and the broker PC can join (2.4 GHz; ESP32 cannot see 5 GHz networks).
- Where Mosquitto runs — this PC (skill sets it up, see Step 2), or an existing broker elsewhere (get host + port, skip Step 2's local setup).
- Whether more than one physical board will be flashed now (affects nothing in the firmware — it self-assigns IDs — but changes how you phrase the "flash this to every board" instruction back to the user).
Step 2 — set up the broker (skip if one already exists elsewhere)
Find the broker machine's LAN IP on the shared WiFi (Windows):
Get-NetIPAddress -AddressFamily IPv4 | Where-Object {$_.InterfaceAlias -eq 'Wi-Fi'} | Select-Object IPAddress
Check whether Mosquitto is installed and what's listening:
netstat -an | findstr "1883 9001"
Get-Service -Name mosquitto -ErrorAction SilentlyContinue
If nothing is listening on 0.0.0.0:1883/0.0.0.0:9001 (see pitfalls 1–3
below for why this is the default), copy assets/mqtt-broker/setup_broker.ps1
and assets/mqtt-broker/setup_firewall.ps1 next to each other in the user's
workspace, then run both elevated (see pitfall 2 for why a plain shell
silently fails):
Start-Process powershell -ArgumentList '-NoProfile','-ExecutionPolicy','Bypass','-File','<path>\setup_broker.ps1' -Verb RunAs -Wait
Start-Process powershell -ArgumentList '-NoProfile','-ExecutionPolicy','Bypass','-File','<path>\setup_firewall.ps1' -Verb RunAs -Wait
This triggers a Windows UAC consent prompt — tell the user it will appear and
that they need to click "Yes"; you cannot click it for them. Both scripts
write a setup_result.txt next to themselves — read it back to confirm
success instead of assuming the elevated window's own output was captured.
Verify LAN exposure actually worked (not just that the process is listening):
netstat -an | findstr "1883 9001" # expect 0.0.0.0:1883 and 0.0.0.0:9001, not 127.0.0.1
Step 3 — generate the sketch from the template
assets/esp32s3_mqtt_dashboard.ino.tpl is verified working code — do not
rewrite it. Copy it to <workspace>\<name>\<name>.ino (folder name must match
the .ino name) and replace the placeholders literally:
| Placeholder | Meaning | Example |
|---|---|---|
__WIFI_SSID__ |
WiFi SSID (2.4 GHz) | myhome |
__WIFI_PASS__ |
WiFi password | pass1234 |
__MQTT_HOST__ |
broker machine's LAN IP | 192.168.0.42 |
Install the one extra library the sketch needs (WiFi/WiFiClient ship with
the esp32 core already):
arduino-cli lib install "PubSubClient"
Step 4 — compile, upload
arduino-cli compile --fqbn esp32:esp32:XIAO_ESP32S3 <SKETCH_DIR>
arduino-cli upload -p COM4 --fqbn esp32:esp32:XIAO_ESP32S3 <SKETCH_DIR>
To add more boards, flash the exact same compiled sketch to each one — no edits needed between boards.
Step 5 — verify WITHOUT living in the serial monitor
Read serial ONCE right after flashing to confirm WiFi + MQTT connect (use the
base skill's read_serial.ps1, never arduino-cli monitor). After that,
stop opening the serial port and verify ongoing behavior purely over MQTT
(pitfall 6 explains why re-opening it can look like a regression that isn't
one):
# expects: <topic-prefix>/<id>/status online, then a touch value every ~0.5s
mosquitto_sub -h <broker-ip> -p 1883 -t "xiao/#" -v
# flip a board's LED from the command line the same way the dashboard does
mosquitto_pub -h <broker-ip> -p 1883 -t "xiao/<id>/led/set" -m "ON"
(If mosquitto_sub/mosquitto_pub aren't on PATH, they ship next to
mosquitto.exe, typically C:\Program Files\Mosquitto\.)
Step 6 — run the dashboard
assets/dashboard_web/ (index.html, app.js, styles.css) is a static
site — no build step, no placeholders to fill (the broker address is typed
into the page itself, not baked into the JS). Serve it any way the user
likes, e.g.:
cd assets/dashboard_web
python -m http.server 8080
Then open http://localhost:8080 (or http://<broker-ip>:8080 from another
device on the same WiFi). Type the broker's LAN IP into the "브로커 주소" box
and click 연결 — it remembers the value in localStorage after that. Boards
appear as cards automatically as their status/touch messages arrive; no
per-board dashboard configuration exists or is needed.
MQTT topic scheme
<id> is the board's own MAC-derived hex ID (auto-generated, e.g. 4b4cf4)
so many boards never collide on one broker.
| Topic | Direction | Payload | Notes |
|---|---|---|---|
xiao/<id>/touch |
board → dashboard | integer string | every 500 ms |
xiao/<id>/led/set |
dashboard → board | ON / OFF / TOGGLE |
command |
xiao/<id>/led/state |
board → dashboard | ON / OFF, retained |
current state |
xiao/<id>/status |
board → dashboard | online / offline, retained |
LWT — broker auto-publishes offline on ungraceful disconnect |
Touch baseline is roughly 16,000–18,000 on bare wire/pad; values RISE well past 40,000 when touched (ESP32-S3 touch direction — see base skill).
Known pitfalls — read before "debugging"
- Mosquitto 2.x binds to loopback ONLY when no listener is configured —
this is a deliberate security default, not a bug. A fresh Windows install
with the stock
mosquitto.conf(all comments, nolistenerline) will show127.0.0.1:1883innetstat, never0.0.0.0:1883, until you add an explicitlistener 1883 0.0.0.0. - The Windows Mosquitto service runs as
LocalSystem. A normal (non-admin) shell gets a silentAccess is deniedfromStop-Service/Restart-Service, and cannot writeC:\Program Files\Mosquitto\mosquitto.confeither. UseStart-Process powershell -Verb RunAs -Waitto run the two setup scripts elevated (Step 2) — this is a legitimate, narrowly-scoped use of UAC elevation for a config change the user already asked for; it is not a privilege-escalation workaround, and it still requires the user to click "Yes" on the consent prompt themselves. - Windows Firewall blocks LAN-inbound to a newly opened port by default,
especially when the WiFi adapter's network profile is "Public" (check with
Get-NetConnectionProfile). Opening the Mosquitto listener alone is not enough — add explicitNew-NetFirewallRule -Direction Inboundrules for both ports (Step 2'ssetup_firewall.ps1). False-positive warning:Test-NetConnection <own-LAN-IP> -Port 1883run FROM the broker machine itself can reportTcpTestSucceeded: Trueeven when the firewall would block a genuinely remote device — self-to-self tests over your own LAN IP are not a reliable substitute for testing from the actual board. PubSubClient::connect()returningrc=-2(MQTT_CONNECT_FAILED) means the TCP connection itself failed — broker unreachable or firewalled — not a credentials/auth rejection. Chasingallow_anonymous/username config for this code wastes time; check pitfalls 1 and 3 first.- Opening the ESP32S3's serial port resets the board every time (DTR
toggle, same as the base skill's pitfall 1) — but for a board that's
already connected to WiFi/MQTT, this doesn't just cost a reboot: closing
the port afterward can leave it in the reset-stuck state (base skill
pitfall 8), which drops the MQTT session a few seconds later. The broker
then reports that board
offlinevia its LWT even though the firmware itself has no bug. Once you've confirmed the WiFi+MQTT connect log once, stop opening the serial port — verify ongoing liveness withmosquitto_subor the dashboard instead, never by reopening serial to "just double check." - Browsers cannot speak raw MQTT over TCP. The dashboard needs
Mosquitto's separate
listener 9001 ... protocol websockets— the sameallow_anonymous/firewall requirements from pitfalls 1–3 apply to it independently of port 1883. "Firmware connects fine, dashboard shows nothing" almost always means the websockets listener was forgotten. - Never hardcode one user's WiFi SSID/password or one machine's LAN IP into a shared template. Ask (Step 1), template it in (Step 3) per deployment. Also warn that a DHCP-assigned broker IP can change on that PC's reboot — a router-side DHCP reservation avoids having to re-flash every board afterward.
- Each board must derive its own topic-unique ID (MAC-based, as the
template does) rather than using a fixed client ID/topic — two boards with
the same MQTT client ID will fight over the connection (repeated
disconnects) and two boards on the same topic will stomp each other's
touch/led/statevalues on the dashboard.