How Does an RP2350 Run an MQTT Broker and Its Dashboard on a W5500 With No PC Behind It?
An RP2350-MINI(ETH) runs an MQTT broker and its live dashboard alone, splitting the W5500's eight hardware sockets into DHCP, web and five MQTT clients.
0
Project description
📚 Context: Scorch1262/RP2350-MQTT-Dashboard-Broker-Mini-MK1 — firmware that turns a Seengreat RP2350-MINI(ETH) into the LAN's MQTT broker and serves its own live dashboard, with every TCP connection carried by the W5500's hardware sockets.
01 — What the repository builds
The board is the broker. Plug in a cable, it takes an address over DHCP and registers with the router as MQTTMON-xxxxxx (the last three bytes of its MAC). MQTT clients connect to port 1883, and a browser pointed at http://<board-ip>/ gets the dashboard.
The dashboard is the firmware port of the author's Python/Flask project MQTT-Monitor-Dashboard-MK1, keeping its look and controls.
- Key figures: broker address, uptime, clients (x/5), topics, messages per 5 s
- A live node graph: the broker in the middle, clients and topics around it, and links that pulse on every packet
- Client and topic tables, and a chronological packet log
- A settings page saved to flash: DHCP or static address, web and MQTT ports, anonymous access, username and password
What the broker supports
| Item | Behaviour |
|---|---|
| Protocol | MQTT 3.1.1 and 3.1. MQTT 5 gets "unacceptable protocol version" |
| QoS | 0/1/2 accepted on publish, delivered at 0 or 1, subscriptions granted up to 1 |
| Topics | + and # wildcards |
| Retained | 48 messages, up to 512 bytes each. An empty payload clears one |
| Last Will | sent when a client drops without DISCONNECT or misses its keepalive |
| Same client ID | the older connection is closed |
| Login | optional single username and password |
The status LED
| Colour | Meaning |
|---|---|
| white | booting |
| red, blinking | the W5500 is not answering |
| blue, blinking | no Ethernet link |
| blue | link up, waiting for DHCP |
| green | broker running |
| teal | broker running on the fallback address |
| amber flash | an MQTT message was handled |
02 — Eight hardware sockets, divided up front
The W5500 has exactly eight hardware sockets. config.h hands them out before anything runs.
| Socket | Use |
|---|---|
| 0 | DHCP client, UDP, kept open for lease renewal |
| 1–2 | web dashboard |
| 3–7 | MQTT, one client per socket |
All five MQTT sockets are opened on the same port, 1883, and put into LISTEN. Each incoming connection lands on one of them. Once all five are connected there is no listening socket left, and per the README the W5500 itself turns the sixth client away with "connection refused" until a slot frees up. The RP2350 never sees that attempt.
When a socket drops back to CLOSED, the poll loop calls socket() and listen() on it again, so the slot rejoins the pool on its own.
The rest of the chip setup in main.c:
| Item | Value |
|---|---|
| Bus | SPI0 at 33 MHz |
| Pins | MISO GP16, CS GP17, SCK GP18, MOSI GP19, RST GP20, INT GP21 |
| Buffers | 2 KB RX and 2 KB TX on every socket |
| Chip check | VERSIONR must read 0x04 |
| Retransmission | 200 ms, 6 retries |
| MAC | 0x02 locally administered, rest from the RP2350 unique ID |
The retry count is cut so a peer that vanished is given up on sooner. The MAC comes from the RP2350 because the W5500 does not ship with one. If the version check fails, the LED blinks red and the init is retried every three seconds; on success the watchdog reboots the board cleanly.
The ioLibrary is v3.2.0, vendored under lib/ioLibrary, with one change: the DHCP hostname prefix is MQTTMON-.
03 — A broker that never waits
One core, no RTOS. The main loop runs five tasks cooperatively under an 8-second watchdog.
| Task | Job |
|---|---|
| net_task | link check every 250 ms, DHCP with lease renewal, fallback address |
| broker_poll | serve the five MQTT sockets |
| http_poll | serve the two web sockets |
| cli_task | emergency commands over USB serial |
| led_task | status LED |
For that to hold, nothing may block. Every TCP socket is opened with SF_IO_NONBLOCK | SF_TCP_NODELAY, and the header comment of net_io_w5500.c states the reason: a hanging client must never stall the main loop.
Sending
nio_send_all() never asks the chip for more than it can take.
uint16_t fsr = getSn_TX_FSR(sn);
uint16_t chunk = (len > fsr) ? fsr : (uint16_t)len;
int32_t r = SOCK_BUSY;
if (chunk) r = send(sn, (uint8_t *)buf, chunk);
if (r > 0) { buf += r; len -= (uint32_t)r; deadline = now_ms() + timeout_ms; continue; }
if (r == SOCK_BUSY) { if (now_ms() > deadline) return false; nio_idle(); continue; }It writes only as much as the free TX space allows, feeds the watchdog while the socket is busy, and restarts the deadline every time bytes move. The deadline is 400 ms for MQTT and 2 s for HTTP. A subscriber that stops reading costs the loop at most 400 ms per message before the broker moves on.
Receiving
Each client has an 8 KB receive buffer. A packet larger than that does not cost the connection.
- The fixed header and topic are read from what has arrived.
- For QoS 1 or 2 the PUBACK or PUBREC is still sent.
- The rest of the payload is drained from the socket in 512-byte pieces and thrown away.
- The log records it as "discarded: N bytes – larger than buffer (8192 bytes)".
The web side
Two sockets, Connection: close. A response is closed once getSn_TX_FSR equals the socket's full TX size, meaning everything sent has been acknowledged, or after 2 s. The dashboard polls /api/state?since=<seq> and gets only the log entries it has not seen, so the board does not resend the whole log on every refresh.
04 — v1.0.1: time that ran backwards
v1.0.0 occasionally disconnected clients for "keepalive expired" right after they connected.
The cause was ordering. slot_poll() read now once at the top. client_read(), called next, stamped last_rx with a fresh now_ms(). The keepalive check then computed now - last_rx in uint64_t. With last_rx a millisecond newer than now, the subtraction wrapped to an enormous number and the client was dropped as silent. The same pattern cut web responses short.
The fix is one helper in net_io.h.
static inline uint64_t ms_since(uint64_t t)
{
uint64_t n = now_ms();
return n > t ? n - t : 0;
}It reads the clock fresh and returns 0 when the stamp is newer. Every elapsed-time comparison in the broker and the web server now goes through it. A regression test came with it: eight seconds of continuous publishing to five topics while polling /api/state, failing on any disconnect event.
05 — The same firmware on a PC
The broker, the monitor and the web server touch the network only through src/net_io.h. On the board, net_io_w5500.c implements it with the ioLibrary. For tests, test/net_io_posix.c implements the same functions on POSIX sockets, emulating the W5500's eight-socket model and its CLOSED / LISTEN / ESTABLISHED / CLOSE_WAIT states.
That lets the unchanged firmware logic run on a PC under AddressSanitizer and UBSan, with real clients.
./test/build_host.sh
python3 test/test_broker.py
./test/build/host_sim 1883 8080test_broker.py runs 45 functional tests with paho-mqtt and requests.
| Area | What is checked |
|---|---|
| Routing | wildcard matching, QoS per subscription |
| Sizes | 5 KB delivered intact, 20 KB logged as discarded with the connection kept |
| Retained / Will | delivery to late subscribers, clearing by empty payload, Last Will on drop |
| Dashboard | hex preview for binary, incremental log, limits under flood |
| Robustness | sustained load, duplicate client ID, garbage bytes, MQTT 5 attempt |
| Settings | port clash, bad IP, broken JSON, login on and off |
GitHub Actions runs the host build and these tests on every push, then builds the UF2 with Pico SDK 2.1.1. A v* tag attaches the UF2 to a release.
06 — Settings and recovery
| Change | What restarts |
|---|---|
| MQTT port, login | broker only |
| Network, web port | the whole board |
Settings live in the last flash sector. DHCP gives up after 30 s with a link present and falls back to 192.168.1.200/24; the LED turns teal so the fallback is visible. A lease that cannot be renewed keeps the current address and keeps retrying. If DHCP hands out a new address, the broker and web server close and reopen their sockets.
If a wrong static address makes the board unreachable, a serial terminal on USB takes commands: status, dhcp, static, webport, mqttport, anon on|off, factory, reboot. On connect it prints the current address by itself.
07 — Where it stands
- There is no licence file. Reusing the code needs a word with the author.
- The README and comments are in German.
- Five MQTT clients at once. That is the socket budget; giving the web side one socket instead of two is a change in
config.h. - MQTT 3.1 and 3.1.1 only. Outbound delivery is at most QoS 1.
- No persistent sessions. Subscriptions live as long as the TCP connection.
- No TLS, and one username and password pair.
- The board has no clock. Timestamps are converted to local time in the browser.
- The test suite runs on the PC emulation of the socket model; chip-level behaviour such as the sixth-client refusal sits outside it.
It takes a fixed budget of eight sockets, spends it deliberately, and builds a broker around it that never blocks. For a small LAN of a few devices, the broker and its view of the traffic fit on one board with nothing behind it.
08 — Similar Projects on WIZnet Makers
- ESP32 MQTT Broker is the same idea on the other kind of stack. It runs on lwIP sockets with PSRAM and takes up to 100 clients, and the W5500 bridges a wired LAN to the ESP32's Wi-Fi subnet. Here the W5500 carries TCP itself, and the client count is set by its eight sockets.
- RP2350-MINI(ETH) is the board this firmware targets: an RP2350A and a W5500 on one small module.
- OGMS — Open Greenhouse Management System (RP2350B) also pairs an RP2350, a W5500, MQTT and a web UI, but drives the chip through lwIP in MACRAW. Read side by side, the two show both ways the same chip is used on an RP2350.
Q&A
Q. In what mode is the W5500 used?
Hardware TCP/IP. The firmware uses the ioLibrary socket API directly with no lwIP: socket 0 is UDP for DHCP, sockets 1–7 are TCP servers for the web and MQTT.
Q. Why five clients?
Eight sockets, one for DHCP, two for the web, five for MQTT. A sixth client is refused by the chip until a slot frees. The split is a few defines in config.h.
Q. What happens to a message larger than 8 KB?
The broker reads its topic, acknowledges it at QoS 1 or 2, drains and drops the payload, and logs it as discarded. The client stays connected.
Q. Can it be tried without the board?
Yes. test/build_host.sh builds a host simulator from the same sources, and host_sim 1883 8080 serves the broker and dashboard on a PC.
Q. Can it be reused?
There is no licence file. The structure and settings can be read and applied; reusing the code needs the author's confirmation.
Original Link: https://github.com/Scorch1262/RP2350-MQTT-Dashboard-Broker-Mini-MK1
Socket split: src/config.h · W5500 socket layer: src/net_io_w5500.c · Broker: src/mqtt_broker.c · PC emulation: test/net_io_posix.c · Tests: test/test_broker.py
