Skip to content

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.

Grace_Koo

Published October 23, 2026

Original author: Scorch1262Original source (new tab)

How Does an RP2350 Run an MQTT Broker and Its Dashboard on a W5500 With No PC Behind It?

Components

Hardware components

WIZnet parts

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

ItemBehaviour
ProtocolMQTT 3.1.1 and 3.1. MQTT 5 gets "unacceptable protocol version"
QoS0/1/2 accepted on publish, delivered at 0 or 1, subscriptions granted up to 1
Topics+ and # wildcards
Retained48 messages, up to 512 bytes each. An empty payload clears one
Last Willsent when a client drops without DISCONNECT or misses its keepalive
Same client IDthe older connection is closed
Loginoptional single username and password

The status LED

ColourMeaning
whitebooting
red, blinkingthe W5500 is not answering
blue, blinkingno Ethernet link
bluelink up, waiting for DHCP
greenbroker running
tealbroker running on the fallback address
amber flashan 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.

SocketUse
0DHCP client, UDP, kept open for lease renewal
1–2web dashboard
3–7MQTT, 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:

ItemValue
BusSPI0 at 33 MHz
PinsMISO GP16, CS GP17, SCK GP18, MOSI GP19, RST GP20, INT GP21
Buffers2 KB RX and 2 KB TX on every socket
Chip checkVERSIONR must read 0x04
Retransmission200 ms, 6 retries
MAC0x02 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.

TaskJob
net_tasklink check every 250 ms, DHCP with lease renewal, fallback address
broker_pollserve the five MQTT sockets
http_pollserve the two web sockets
cli_taskemergency commands over USB serial
led_taskstatus 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 8080

test_broker.py runs 45 functional tests with paho-mqtt and requests.

AreaWhat is checked
Routingwildcard matching, QoS per subscription
Sizes5 KB delivered intact, 20 KB logged as discarded with the connection kept
Retained / Willdelivery to late subscribers, clearing by empty payload, Last Will on drop
Dashboardhex preview for binary, incremental log, limits under flood
Robustnesssustained load, duplicate client ID, garbage bytes, MQTT 5 attempt
Settingsport 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

ChangeWhat restarts
MQTT port, loginbroker only
Network, web portthe 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

Documents

Comments

Similar projects you might like

Comments