Scrolling Meshtastic Messages on an 8x32 WS2812B LED Panel: Custom Firmware, MQTT, and the Decryption Part Nobody Explains
A custom ESP32 build that subscribes to Meshtastic's encrypted MQTT topic, decrypts packets on the fly, and scrolls messages across a WS2812B 8x32 LED panel. Full build guide: parts, wiring, PlatformIO flashing from scratch, and MQTT setup.
There's a Meshtastic node on my desk. Messages bounce around the mesh all day and I have no idea until I check the app, which I often forget to do. A notification on my phone helps approximately never because the phone is always in a different room.
So I built a ticker. An ESP32, a WS2812B 8x32 LED panel, and a chunk of custom C++ firmware that pulls live messages off the NEPAMesh MQTT broker, decrypts them, and scrolls them across the display as green text. It also learns everyone's node names automatically as they check in, so you see callsigns instead of hex IDs.
Total hardware cost is around $20. The firmware is already written. Getting it running takes maybe an hour the first time, mostly waiting for PlatformIO to download things.
This covers everything: what to buy, how to wire it, how to get the firmware on the chip from scratch, and what to do when something inevitably looks wrong.
What It Actually Does
Quick version: the ESP32 connects to your WiFi, subscribes to the Meshtastic MQTT topic on mqtt.nepamesh.com, and decrypts incoming packets using the same key every Meshtastic device uses by default. When a text message comes through, it queues it and scrolls it across the panel as NodeName: message text. If it hasn't seen a node before, it uses the short hex ID until a NodeInfo packet comes through and tells it the callsign.
The display itself is a 32x8 grid of WS2812B LEDs — the same addressable RGB LEDs in LED strips and gaming keyboards. Each pixel is individually controllable, full color. Default is green. You can change it to whatever you want through the web interface without touching the code.
On boot it runs a red-blue-green snake across all 256 LEDs so you can verify the wiring works before you start wondering why nothing is showing up. Then it connects and waits. When messages arrive, they scroll. If no messages are coming in, the display stays dark.
Meshtastic encrypts every packet using AES-128-CTR. The default channel (LongFast) uses a public key — it's not secret, it's just how the protocol works. This firmware has that key baked in and decrypts packets automatically. If your mesh uses a custom channel key, you replace the 16 bytes in
MESH_KEY[] in main.cpp with your own. If you're on the default channel, you don't have to do anything.What You Need
- ESP32 development board — any standard 30 or 38-pin ESP32 DevKit works. An ESP32-C6 DevKitC-1 also works and is what the
esp32c6build target in the firmware targets. Either one is fine. ~$7–12. - WS2812B 8x32 LED matrix — the SVFISHKK panels from Amazon (B0CY2R8FSL) work well. They come in a two-pack. You only need one. ~$16 for the pair.
- 5V power supply, 3A or more — this is important. A full panel of WS2812B LEDs pulls real current. Do not power the panel off the ESP32's 5V pin. Your USB port will not thank you and neither will the LEDs. A phone charger brick rated at 3A is fine.
- Three wires — short ones, for data, GND, and 5V.
- One 300–500 Ω resistor — goes in series on the data line. Smooths out signal ringing, protects the first LED in the chain. Costs almost nothing.
- One 100 µF electrolytic capacitor — across the panel's 5V and GND pads. Prevents the power surge on plug-in from killing LEDs. Also costs almost nothing.
- USB cable — for the initial flash. After that, updates go over WiFi.
Wiring
Three connections between the ESP32 and the panel, plus a shared ground between the two power supplies.
| From | To |
|---|---|
GPIO 5 | Panel DIN (via 300–500 Ω resistor) |
GND | Panel GND |
| External 5V supply | Panel 5V |
| External supply GND | ESP32 GND — they must share ground |
The resistor goes on the data line between GPIO 5 and the panel's DIN pin. The capacitor goes directly across the panel's 5V and GND pads, as close to the panel as you can get it. Both of these are optional in the sense that it'll probably work without them, but they're cheap enough that skipping them is just asking for flicker and dead LEDs down the line.
The ESP32 and the LED panel need to share a common ground even though they're powered separately. If you don't connect ESP32 GND to the external supply's GND, the data signal has nothing to reference against and the panel will either not respond at all or do deeply confusing things. Connect the grounds.
Flashing the Firmware
This firmware is built with PlatformIO, which is an extension for VS Code. If you've never used either, here's the whole path from nothing to a working build.
Install VS Code and PlatformIO
VS Code is a free code editor from Microsoft. Download and install it from code.visualstudio.com. Once it's running, open the Extensions panel (Ctrl+Shift+X on Windows/Linux, Cmd+Shift+X on Mac), search for PlatformIO IDE, and install it. Let it finish — it downloads a fair amount on first run.
PlatformIO is what actually compiles the firmware and handles the upload. VS Code is just the wrapper it lives in.
Linux: USB Permissions (Don't Skip This)
On Linux, regular users can't talk to USB serial devices by default. If you skip this, you'll get a permission denied error when you try to flash and spend 20 minutes confused about why.
Run these commands and then log out and back in. Not just close the terminal — actually log out.
# Add yourself to the dialout group
sudo usermod -a -G dialout $USER
# Install PlatformIO's udev rules
curl -fsSL https://raw.githubusercontent.com/platformio/platformio-core/develop/platformio/assets/system/99-platformio-udev.rules \
| sudo tee /etc/udev/rules.d/99-platformio-udev.rules
sudo udevadm control --reload-rules
sudo udevadm trigger
The group change only takes effect after you log out and back in. This is the number one reason setups appear broken when they aren't.
The ESP32-C6 DevKitC-1 uses a built-in USB controller instead of a CH340 or CP2102. On Linux it shows up as
/dev/ttyACM0 instead of /dev/ttyUSB0. PlatformIO handles this automatically. If you're using esptool directly, use ttyACM0.Windows: CH340 Driver
Classic ESP32 boards (not C6) use a CH340 USB-to-serial chip. Windows sometimes includes a driver for it, sometimes doesn't. If the board shows up in Device Manager with a yellow warning icon, download the CH340 driver, install it, unplug and replug. After that it should appear as a COM port and PlatformIO will find it.
Configure Before Flashing
Open the project folder in VS Code. The only thing you have to set before the first flash is your WiFi credentials. Open src/config.h and fill in your network name and password:
#define WIFI_SSID "your-network-name"
#define WIFI_PASS "your-wifi-password"
Everything else — MQTT host, port, credentials, display orientation, scroll speed, text color — can be changed through the web interface after the device is running. You only need to touch config.h again if you change WiFi networks.
Build and Upload
With the project open in VS Code, select your target board from the PlatformIO toolbar at the bottom of the screen. Hit the arrow (Upload) button or press Ctrl+Alt+U. PlatformIO will compile and flash. When you see Hard resetting... in the terminal output, it worked.
From the command line, if you prefer:
# Standard ESP32
pio run -e esp32dev -t upload
# ESP32-C6
pio run -e esp32c6 -t upload
Some boards need help. Hold BOOT, press and release EN (reset), release BOOT, then start the upload. This manually puts the chip into download mode. Once the first flash succeeds, subsequent uploads usually work without this.
First Boot — Captive Portal
If the device can't connect to your WiFi within 15 seconds, it gives up and creates its own open access point called sign. Connect to it from your phone or laptop — you should get an automatic "sign in to network" prompt that opens the settings page. If the prompt doesn't appear, open a browser and go to http://192.168.4.1. Enter your WiFi credentials, hit Save & Restart, and it'll reboot and join your network.
The display scrolls Connect to: sign when it's in this mode so you know what's happening.
What You Should See
After a successful flash and WiFi connection, open the serial monitor at 115200 baud or check the log page in the web interface. You should see:
WiFi YourNetwork ... 192.168.x.x
Web: http://192.168.x.x
MQTT OK
SUB msh/nepa/2/e/LongFast/# -> OK
When a mesh message comes through:
[NODE] !A1B2C3D4 -> W3XYZ
[MSG] W3XYZ: Hello from the mesh
That node entry means it learned the callsign from a NodeInfo packet. Until that happens, messages show up as !XXXX — the short version of the hex node ID. Give it a few minutes of traffic and it'll fill in the names.
Pointing Your Node at NEPAMesh MQTT
The display subscribes to MQTT and shows whatever arrives. But messages only arrive if your Meshtastic node is publishing them to the same broker. This is the other half of the setup.
Open the Meshtastic app, connect to your node, and go to Radio Configuration > MQTT (Android) or Settings > Module Configuration > MQTT (iOS).
The credentials (server address, username, password) are in the NEPAMesh MQTT setup guide at nepamesh.com. The rest of the settings are below.
| Setting | Value |
|---|---|
| Enabled | ON |
| Encryption Enabled | ON |
| TLS Enabled | OFF |
| Root Topic | msh/nepa |
| Proxy to Client Enabled | ON (phone) / OFF (node has WiFi) |
| Map Reporting Enabled | ON |
| Map Publish Interval | 3600 |
| Approximate Location | 14 |
The default is
msh/US. NEPAMesh uses msh/nepa. Case-sensitive. No trailing slash. Get this wrong and your node publishes to the public server instead of ours.Next, your primary channel needs to have Uplink enabled. In the app, go to your primary channel (LongFast) settings:
| Setting | Value |
|---|---|
| Uplink Enabled | ON |
| Downlink Enabled | OFF |
| Position Enabled | ON |
| Precise Location | OFF |
Downlink takes everything from the MQTT broker and rebroadcasts it over radio. That floods the mesh with internet traffic. Don't turn it on.
Last one, and it's the easiest to miss: Radio Configuration > LoRa, make sure OK to MQTT is ON. This is the master permission switch. Without it, the node ignores all the MQTT settings you just configured. I found this one after 20 minutes of staring at logs. Now you don't have to.
The Web Interface
Once the device is on your network, open http://[device-ip] in any browser. Three pages:
- Settings (/) — WiFi, MQTT host/port/credentials/topic, display orientation, brightness, scroll speed, repeat count, and text color (color picker). Also has a Send Message box at the top — type anything and it scrolls immediately, bypassing the MQTT queue. Useful for testing whether your wiring is right before you bother with network config. Hit Save & Restart to apply changes.
- Log (/log) — live debug output, same as serial, polling every second in the browser. Good for confirming MQTT is connected and watching packets come in without plugging anything in.
- OTA (/ota) — upload a new .bin firmware file from your browser. Build in PlatformIO, browse to
.pio/build/esp32c6/firmware.bin, upload. No USB cable required for updates after the initial flash.
All settings survive power cycles. They're stored in the ESP32's NVS (non-volatile storage), separate from the firmware, so a firmware update won't wipe your configuration.
When Things Don't Work
Display is blank after boot test
- Shared ground — this is almost always the problem. ESP32 GND and the external supply GND must be connected.
- VCC on the panel needs 5V from the external supply, not from the ESP32.
- Check that
GPIO 5is on the DIN side of the resistor, not the panel side.
Text is mirrored or upside-down
Open the web interface and go to Settings > Display. Toggle Connector Side (Right/Left) and Flip Y (Normal/Flipped) until it looks right. No recompile needed.
| Symptom | Fix |
|---|---|
| Text scrolls backwards / mirrored | Change Connector Side |
| Text is upside-down | Enable Flip Y |
| Both | Change both |
Messages show as ?????
Your mesh is using a custom channel key instead of the default LongFast key. You need to replace the 16 bytes in MESH_KEY[] at the top of main.cpp with your channel's expanded AES key and reflash.
Nodes show as !XXXX instead of callsigns
Node names are learned passively from NodeInfo packets in the traffic stream. Ask someone to trigger a NodeInfo broadcast from their app (Admin > Send NodeInfo), or wait — most nodes broadcast automatically every 15–60 minutes.
MQTT not connecting
- Broker address, username, and password — set these in the web UI under Settings > MQTT. See the NEPAMesh MQTT setup guide for the correct values.
- The firmware generates a unique client ID from the ESP32's chip ID, so you won't collide with other devices on the broker.
- Weak WiFi signal causes more disconnects than anything else. Check signal strength first.
Panel flickers or random pixels fire
- Shared ground between ESP32 and the panel's power supply.
- Resistor on DIN (300–500 Ω).
- Capacitor across panel 5V/GND (100 µF).
- Also: running at full brightness on USB power will overdraw. The firmware defaults to brightness 40 out of 255 specifically to stay within USB limits. If you're running higher brightness, use an external supply.
Linux: permission denied when flashing
You either skipped the usermod step, forgot to log out and back in, or the udev rules didn't apply. Run through the Linux setup steps again. Closing the terminal isn't the same as logging out.
Full source: github.com/nepamesh/led-mqtt-meshtastic-8x32
Part of the NEPAMesh project. Meshtastic is a registered trademark of Meshtastic LLC. No affiliation. No warranty. Build stuff.