MAX7219 LED dot matrix display driven by an ESP32 or ESP8266 MCU.
The latest firmware version can be found in Releases
The latest stable version is v4.1.1, fixing stale text in Manual/MQTT zones and clearing MQTT postfixes in profiles, and adding an original Robot sprite with automatic migration from MARIO. See the release notes for the changes. Back up your settings before updating.
- Home Assistant client [display sensor values]
- OpenWeatherMap client [display: temperature (C/F), humidity, pressure, wind speed, weather icon]
- MQTT client [each display zone support own topic]
- Display controll through MQTT
- Wall NTP clock [NTP sync clock]
- Manual input
- Stock Ticker Mode: Display real-time stock prices on any zone. Supports multiple symbols, customizable display format, prefix/suffix, and price change arrows (↑↓)
- Countdown timer
- Progress Bar Overlay: 1-pixel bottom-row bar on any zone — supports HA sensors, MQTT topics, and time-based modes
- Dynamic mode adds 16 animations and two HA/MQTT value rules, with complete progress-bar settings stored per profile and zone.
- Full controll through web UI
- Display snapshot on Home shows lit and unlit pixels for the active zone layout. A snapshot loads once when opening the page; click Refresh for a new image. Display power and 180° rotation are reflected in the snapshot.
- Home Assistant MQTT discovery [When MQTT settings specified, device will be automatically send discovery message to HA]
- Control display as light in Home Assistant [MQTT setup required]
- Initial setup through wifi AP and web UI
- Support 4 independent display zones
- Rotate display 180° in Service Settings → System settings rotates the entire display, including zones, animations and progress bars. Click Apply to save. This is one global setting shared by all profiles and retained after reboot.
- Support up to 12 display segments at the same time [configured in web UI]
- Multiple fonts, including PixelFonts 3D
- Plus symbols font
- Display profiles and scheduling: 4 profiles on ESP8266 or 8 on ESP32, including the default profile; scheduled overrides and Apply Now.
Check what your display is showing directly on Home. The snapshot shows lit and unlit pixels for the configured module count, including clocks, text, icons and progress bars. It loads once when the page opens; click Refresh whenever you need a fresh image. The status line also shows the active profile number and name captured with that image.
Choose red, yellow-green, neon blue or white LEDs beside the preview. The 180° button rotates only the preview, independently of the hardware rotation setting, so a physically inverted display can be viewed upright. Both choices are saved in your browser for that device address. Changing the appearance redraws the existing snapshot without requesting another frame or changing device settings.
The NTP section in Service Settings shows the device's current date and time, UTC offset, sync status and last successful sync since restart. It uses the device's applied time zone, even if your browser uses a different one.
Click Sync now to request a sync and see its progress and result. If a sync fails, the previous successful sync time stays visible and you can retry. Apply changes to the NTP settings before using Sync now.
Open Profiles to configure a default baseline and additional profiles. Profiles can override power, brightness, zone layout, display modes, fonts, scrolling, countdown, stock ticker and progress bars. No change preserves the active value. Saving a profile stores it; Apply Now saves and activates it. Changing the zone layout can restart the device.
Enable scheduling and select hours, minutes, days of the week and days of the month. The scheduler uses the configured NTP time and time-zone offset and waits for a successful sync. To restore the baseline after another profile has run, apply or schedule the default profile explicitly.
The ON AIR badge follows the last applied profile across reboots, including when scheduling is disabled. Disabling scheduling leaves that profile active.
Progress bars support Regular numeric/time mode and Dynamic animation mode. Dynamic mode uses up to two HA/MQTT value rules with independent animations and speeds. Leaving both values unset runs Rule 1 continuously; configured rules hide the bar when neither value matches.
- Dot matrix display MAX7219, something like this: https://aliexpress.com/item/32618155357.html
- ESP32 or WeMos D1 Mini (ESP8266), something like this: https://aliexpress.com/item/32651747570.html
- Optional: DS18B20 sensor for local temperature readings. The display works without it.
- 3D Printed Case: Download from Printables
Select PixelFonts 3D (Latin) in a zone's Font field under Profiles, then
use Apply Now. This is pixelfont_3d_5x5_thin from
mcer12/PixelFonts, converted for MD_MAX72XX.
The outline and shadow fit the display's full 8-pixel height. ASCII letters,
digits, punctuation and the euro sign use the original 3D glyphs; Cyrillic and
accented letters are not included. Unsupported characters display ?.
Temperature degrees and stock arrows retain their existing plain glyphs, and
blinking clock separators preserve their width. Characters are wider than the
compact fonts, so longer messages may scroll. On a 32-column zone, set character
spacing to 0 to fit a 24-hour HH:MM clock.
On a real display:
The font is MIT-licensed; attribution is in lib/wledFont/PixelFonts-LICENSE.txt.
The generated header links the exact upstream BDF revision. To regenerate it,
download that BDF and run python3 tools/convert_pixelfont.py /path/to/font.bdf.
This font allows you to display text alongside icons within a single message.
This font allows accessing special icons using the ~ prefix followed by a specific character.
Mapped by ~ + Character.
| Code | Index | Symbol | Code | Index | Symbol |
|---|---|---|---|---|---|
~! |
129 | Battery Empty | ~G |
167 | Key |
~" |
130 | Battery 25% | ~H |
168 | Male |
~# |
131 | Battery 50% | ~I |
169 | Alarm |
~$ |
132 | Battery 75% | ~J |
170 | Clock |
~% |
133 | Battery Full | ~K |
171 | Garbage |
~0 |
144 | WLED Logo | ~M |
173 | Moon |
~1 |
145 | X Symbol | ~N |
174 | Message |
~2 |
146 | Stop | ~O |
175 | Reminder |
~3 |
147 | Rain | ~S |
179 | Sun |
~4 |
148 | IP Icon | ~T |
180 | Thunderstorm |
~8 |
152 | Full Block | ~c |
195 | Scattered Clouds |
~A |
161 | Calendar | ~f |
198 | Mist / Fog |
~B |
162 | Windows Logo | ~m |
205 | Few Clouds (Night) |
~C |
163 | Broken Clouds | ~r |
210 | Rain |
~D |
164 | Door | ~s |
211 | Few Clouds (Sun) |
~E |
165 | Female | ~P |
224 | P Icon |
~F |
166 | Snow | ~@ |
230 | @ Symbol |
This font maps standard ASCII characters directly to icons.
| Input | Index | Symbol | Input | Index | Symbol |
|---|---|---|---|---|---|
0 |
48 | WLED Logo | K |
75 | Garbage |
1 |
49 | X Symbol | M |
77 | Moon |
2 |
50 | Stop | N |
78 | Message |
3 |
51 | Rain | O |
79 | Reminder |
4 |
52 | IP Icon | P |
80 | P Icon |
8 |
56 | Full Block | R |
82 | Shower Rain |
A |
65 | Calendar | S |
83 | Sun |
B |
66 | Windows Logo | T |
84 | Thunderstorm |
C |
67 | Broken Clouds | c |
99 | Scattered Clouds |
D |
68 | Door | f |
102 | Mist / Fog |
E |
69 | Female | m |
109 | Few Clouds (Night) |
F |
70 | Snow | r |
114 | Rain |
G |
71 | Key | s |
115 | Few Clouds (Sun) |
H |
72 | Male | DEL |
127 | Hollow Up Arrow |
I |
73 | Alarm | ° |
176 | Degree Symbol |
J |
74 | Clock |
// Display pinout
#define DATA_PIN 23 // ESP32 GPIO23
#define CS_PIN 5 // ESP32 GPIO5
#define CLK_PIN 18 // ESP32 GPIO18
// Optional DS18B20 temperature sensor
const int oneWireBus = 4; // ESP32 GPIO04
// Display pinout
#define DATA_PIN D7 // WeMos D1 mini or ESP8266 -> GPIO13
#define CS_PIN D6 // WeMos D1 mini or ESP8266 -> GPIO12
#define CLK_PIN D5 // WeMos D1 mini or ESP8266 -> GPIO14
// Optional DS18B20 temperature sensor
const int oneWireBus = D4; // WeMos D1 mini or ESP8266 -> GPIO02
The DS18B20 connection shown below is optional. Connect the sensor only if you want local temperature readings.
- Device will boot in wifi access point mode and show "AP Mode" on the display.
- AP wifi name: wledPixel-[short MAC address]
- AP wifi password: 12345678
- Connect to this AP and configure your wifi settings (connect to your wifi network)
- When the device connects to the WIFI network, the device's IP address will be displayed on the zone0
- Open the browser and go to the device IP address
- PlatformIO (VSCode extension or CLI)
- Git
-
Clone the repository:
git clone https://github.com/widapro/wledPixel.git cd wledPixel -
Build for ESP32:
pio run -e esp32
-
Build for ESP8266 (D1 Mini):
pio run -e d1_mini
-
Upload firmware:
# For ESP32 pio run -e esp32 -t upload # For ESP8266 pio run -e d1_mini -t upload
-
Monitor serial output:
pio device monitor -b 115200
If the device gets stuck at boot, erase the flash memory:
pio run -e esp32 -t erase
pio run -e esp32 -t upload/api/temperature - get measured temperature from connected ds18b20 sensor
/api/display/frame - get a one-shot pixel snapshot of the configured display (GET)
/api/ntp-status - get device time, UTC offset and NTP synchronization status (GET)
/api/ntp-sync - queue an NTP synchronization attempt (POST)
The display snapshot contains modules, rows (8), columns, power,
capturedAt (device uptime in milliseconds), profileIndex (zero-based),
profileName and data (one byte per column, left to right, bit 0 at the top).
Only modules in the active zone layout are
included; spare driver capacity and layouts in other saved profiles do not
increase its width. A powered-off display returns unlit pixels. The snapshot
represents firmware output; it cannot detect disconnected or faulty LED hardware.
Topic Structure: [devicePrefix]/[zone]/[command]
- devicePrefix:
wledPixel-{shortMAC}(shown aswledPixel-XXXXin the examples below) - zone:
zoneNwhere N is 0, 1, 2, or 3.
Custom zone topics and progress-bar source IDs in profiles support up to 127 UTF-8 bytes. Longer values are rejected when saving. If an older firmware already truncated a topic, enter its full value again and save the profile.
| Topic Suffix | Description | Payload / Values |
|---|---|---|
/text |
Zone text | Text to display (active when workmode=mqttClient) |
/scrolleffectIn |
Scroll effect IN | Effect name (e.g., PA_PRINT) |
/scrolleffectOut |
Scroll effect OUT | Effect name |
/scrollspeed |
Scroll speed | 0 - 100 |
/scrollpause |
Scroll pause | Seconds |
/scrollalign |
Scroll alignment | PA_LEFT, PA_CENTER, PA_RIGHT |
/charspacing |
Character spacing | Pixels |
/workmode |
Zone work mode | mqttClient, manualInput, wallClock, countdown, wopr, etc. |
/scrollInfinite |
Infinite scroll | ON/OFF, true/false, 1/0 |
| Topic Suffix | Description | Values |
|---|---|---|
/intensity |
Brightness | 0 - 15 |
/power |
Power control | ON / OFF |
- Topic:
.../zoneN/countdown - Payload: String with date/time.
Supported Formats:
| Format | Example | Description |
|---|---|---|
YYYY-MM-DDTHH:MM:SS |
2026-12-31T23:59:59 |
Full ISO format |
YYYY-MM-DDTHH:MM |
2026-12-31T23:59 |
ISO without seconds |
YYYY-MM-DD |
2026-12-31 |
Date only (Time: 00:00:00) |
HH:MM:SS |
23:59:59 |
Time only (Date: Today) |
- Topic:
.../zoneN/countdownFormat - Payload: Format token string.
Options:
- Basic:
SS(seconds),MM(minutes),HH(hours),DD(days) - Combined:
HHMM,HHMMSS,DDHH,DDHHMM,DDHHMMSS - Calendar:
YY(years),YYMM,YYMMDDHHMM,YYMMDDHHMMSS - Automatic:
auto(hides zero leading units)
where devicePrefix = wledPixel-XXXX zoneN = zone number (e.g. Zone0, Zone1, Zone2, Zone3)
workMode supported values:
mqttClient- MQTT clientmanualInput- Manual static text inputed in UIwallClock- NTP sync clockowmWeather- Open Weather Map clienthaClient- Home Assistant clientwopr- WarGames effectcountdown- Countdown timer
Both MQTT Client and Manual Input modes share the same display logic:
- The message appears using the selected IN effect.
- If Infinite Scroll is DISABLED:
- If OUT effect is
NO_EFFECT: The message stays static on the display until a new message arrives. - If OUT effect is any other effect: The animation plays once (IN → Scroll pause → OUT) and then stops. The message does not repeat.
- If OUT effect is
- If Infinite Scroll is ENABLED:
- If OUT effect is
NO_EFFECT: The message stays static on the display until a new message arrives. - If OUT effect is any other effect: The message loops continuously (IN → Scroll pause → OUT → repeat).
- If OUT effect is
- It automatically scrolls to show the full text.
- If Infinite Scroll is enabled: It scrolls in a loop.
- If Infinite Scroll is disabled: It scrolls once and stops.
- If a service message doesn't fit, it scrolls once and then remains static.
- Duplicate status messages are ignored to prevent flickering.
- Can be completely disabled via the "Disable service messages" setting.
Wall clock mode support next following display options:
HH:MM- 24-hour Hours : Minutes [21:43]hh:mm- 12-hour Hours : Minutes without am/pm [09:43]hh:mm am/pm- 12-hour Hours : Minutes with am/pm [09:43 pm]HH:MM:SS- Hours : Minutes : Seconds [21:43:54]HH- Hours [21]MM- Minutes [43]dd.mm.yyyy- Day.Month.Year [21.06.2022]dd.mm- Day.Month [21.06]dd.mm aa- Day.Month weekday name (e.g., Sun) [21.06 Tue] *in Cyrillic font weekday name will be in Cyrillicaa- Weekday name (e.g. Sun) *in Cyrillic font weekday name will be in Cyrillic
- If text fits in the zone:
- Displays statically.
- Dots blink (unless disabled).
- If text does NOT fit in the zone:
- Text scrolls completely until it disappears.
- Screen remains blank for the duration of Scroll Pause.
- Text starts scrolling again.
- Dots do NOT blink to prevent interrupting the scroll cycle.
Note: The Scroll Effect Out setting in the Web UI is disabled (greyed out) for Wall Clock mode because the loop animation doesn't use an exit effect.
A 1-pixel horizontal progress bar rendered on the bottom row (row 7) of any zone. It works as a transparent overlay — the bar coexists with clocks, text, weather, and other modes running in the same zone. Each zone can have its own independent progress bar configured in the "Progress Bar" section of the Settings page.
| Source | Description |
|---|---|
| Home Assistant | Any HA sensor entity (e.g., sensor.battery_level). Polled at the HA update interval. |
| MQTT | Any MQTT topic. Device subscribes automatically. |
| Time — Seconds | Fills over 60 seconds, resets each minute. |
| Time — Minutes | Fills over 60 minutes, resets each hour. |
| Time — Hours | Fills over 24 hours, resets each day. |
- Min / Max values: Define the scale for HA and MQTT sources (e.g., 0–100 for battery percentage). Time mode auto-calculates its scale.
- Conditional display: Optionally show the bar only when a condition is met — a specific HA sensor or MQTT topic value matches an expected string (exact match).
- WOPR exclusion: If a zone is running in WOPR mode, the progress bar is automatically disabled.
| Use Case | Data Source | Min | Max | Notes |
|---|---|---|---|---|
| Battery level | HA sensor / MQTT | 0 | 100 | Show charge state alongside clock |
| Seconds ticker | Time — Seconds | — | — | Visual second hand on a clock zone |
| Hour progress | Time — Hours | — | — | See how much of the day has passed |
| Download progress | MQTT | 0 | 100 | Publish progress % from a script |
| Tank / reservoir level | HA sensor | 0 | 500 | Scale to your sensor's range |
Default effects:
PA_RANDOMPA_PRINTPA_SCAN_HORIZPA_SCROLL_LEFTPA_WIPEPA_SCAN_VERTXPA_SCROLL_UP_LEFTPA_SCROLL_UPPA_FADEPA_OPENING_CURSORPA_GROW_UPPA_SCROLL_UP_RIGHTPA_BLINDSPA_CLOSINGPA_GROW_DOWNPA_SCAN_VERTPA_SCROLL_DOWN_LEFTPA_WIPE_CURSORPA_SCAN_HORIZXPA_DISSOLVEPA_MESHPA_OPENINGPA_CLOSING_CURSORPA_SCROLL_DOWN_RIGHTPA_SCROLL_RIGHTPA_SLICEPA_SCROLL_DOWNPA_SPRITE(Generic)
PACMANWAVEROLLLINESARROWSAILBOATSTEAMBOATHEARTINVADERROCKETFBALLCHEVRONWALKERROBOT— original 8×8 robot with blinking eyesGHOST(New!)DINO(New!)
Existing MARIO settings and profiles automatically use ROBOT, including when
restoring older backups. The legacy MARIO identifier is also accepted through
MQTT and the API.
Watch the effects demo on YouTube
Send sensor value each time when a value has been changed
---
- alias: Outside temp change - send new temp to wled panel mqtt
initial_state: 'on'
trigger:
platform: state
entity_id: sensor.example_temperature
action:
- service: mqtt.publish
data:
topic: wledPixel-XXXX/zone0/text
payload_template: "{{ states('sensor.example_temperature') }} C"
Replace wledPixel-XXXX and sensor.example_temperature with your own device prefix and sensor entity.
See v4.1.1 release notes for Manual/MQTT zone fixes, MQTT postfix clearing and the Robot sprite,
v4.1 release notes for display snapshots, NTP controls and active-profile fixes,
v4.0.1 release notes for the MQTT fix and upgrade instructions,
and v4.0 release notes for the full feature overview and compatibility details,
and local test documentation for the test and HIL workflows.
Local tests: ./tools/test_local.sh all.
