ESP32-S3 Matrix Peckworks Lab

Peckworks Lab · open-source ESP32-S3 firmware · HTTP-native

An 8×8 LED panel you talk to, over WiFi, over HTTP, or just from your phone.

// 64 RGB LEDs on a ~$15 board that runs the whole show itself: no app, no cloud, no computer in the loop.

Flash one binary onto a Waveshare ESP32-S3-Matrix and it becomes a self-contained little display: it opens its own WiFi setup hotspot, serves its own control panel from the device, and exposes an HTTP API that anything can drive: fire, rain, fireworks, weather, a clock, scrolling text, or your own 8×8 pixels. It boots back into whatever it was last showing. And because it's all just HTTP, it doubles as an ambient status light for Claude.

64
WS2812B RGB LEDs · 8×8
ESP32-S3
+ QMI8658C 6-axis IMU
0
cloud · app · computer deps
1 API
every capability is HTTP
PART 01

What it is: a display that's genuinely yours

The Waveshare ESP32-S3-Matrix: an 8×8 grid of WS2812B RGB LEDs on a small USB-C ESP32-S3 board

The whole idea in one line

A tiny standalone panel that runs the show by itself, and that any program can light up.

Most LED-matrix projects are tethered: a sketch on your laptop pushes pixels over USB, and the moment you unplug, it's dark. This firmware flips that. Everything the panel needs lives on the board: the animations, the web UI, the WiFi onboarding, the persistence. Power it over USB-C and it boots straight into whatever it was last showing. Open its address on your phone and you've got a full control panel. Point any HTTP client at it and you're driving pixels from any language.

Plug-in & standalone
Boots into its last animation, brightness, and settings, all persisted to flash. No computer in the loop.
Self-onboarding WiFi
First boot opens a ESP32-Matrix-Setup hotspot; join it, pick your network, done. Then it's at esp32matrix.local.
Its own control panel
A full web UI served from the device: animations, brightness, weather, clock, a pixel painter, a calibration lab.
A real animation library
Fire, matrix rain, fireworks, dance floor, comet, Liquid, Liquid 2, snow, starfield, each tunable live.
Useful, not just pretty
Live weather by ZIP, an NTP clock + countdown timers, scrolling text, and a vibration-reactive VU meter.
An open API
Every capability is an HTTP endpoint: send a built-in effect, push your own frames, set brightness, read sensors.
The shape of it: think of a smart bulb that happens to be an 8×8 canvas: it has an IP address, a web page, and an API, and it keeps glowing whether or not anything is talking to it. Except this one is fully open, runs offline, and you flash it yourself.

See them move: real frames, captured from the panel

A few of the good ones, running.

These aren't mockups or re-implementations: each card replays an actual frame sequence pulled straight off the board over /api/display/framebuffer, bloom-rendered the way the LEDs read in person. Every effect is tunable live from its own control page.

// 9 of ~20 built-in animations · captured at the board's raw pre-brightness colors · fire · matrix_rain · fireworks · dancefloor · rainbow · frostbite · claudesweep · clock · spiral

PART 02

The onboard web UI: no install, served from the device

What you get the moment it's on WiFi

Open esp32matrix.local on any phone and the board hands you a control panel.

The UI ships inside the firmware (LittleFS) and is served with no cache, so there's nothing to install and nothing to keep in sync. These are live screenshots of the real board:

PART 03

The HTTP API: the contract anything can drive

One surface, every capability

If it can make an HTTP request, it can drive the panel.

There's no SDK to adopt and no protocol to learn. The board's entire feature set is a small set of JSON endpoints under http://esp32matrix.local. This is also the documented contract that external clients (including the Claude integration) depend on, so it stays stable.

Launch a built-in animation. The transient flag keeps it out of boot-resume:

curl: fire at half intensity
# roar some fire, classic theme
curl -X POST http://esp32matrix.local/api/display/animation \
  -H "Content-Type: application/json" \
  -d '{"type":"fire","transient":true,"intensity":6,"theme":"classic"}'

Push your own animation, up to 24 frames, each 64 RRGGBB pixels in row-major order:

curl: a custom 2-frame blink
# loop:0 = forever
curl -X POST http://esp32matrix.local/api/display/frames \
  -d '{"frames":["…384 hex chars…","…"],"frame_ms":150,"loop":0}'

The rest of the surface

MethodEndpointWhat it does
GET/api/statusFirmware/web version + live heap telemetry; use it for reachability checks.
POST/api/brightness{ level: 0-255 }. Global brightness (persisted to flash).
POST/api/display/textScroll a message, with color / gradient options.
POST/api/display/matrixSet a single static 8×8 frame.
GET/api/display/framebufferRead the live 8×8 back as 64 RRGGBB. Mirror it anywhere.
GET/POST/api/presenceA semantic status message (intent + headline + data) any renderer can show.
GET/POST/api/settingsNVS-backed config: idle screensaver, default brightness, timezone…
GET/POST/api/calibrationRead / overwrite the LED correction profile, live-reloaded, no reflash.

Full reference: docs/API.md. The board persists its last animation, brightness, and settings, so it powers back up into whatever it was doing.

PART 04

How it actually renders: from a pixel to a lit LED

The part most 8×8 projects get subtly wrong

The grid is plain row-major, not serpentine.

Plenty of LED matrices zig-zag their wiring, so even rows run left-to-right and odd rows run right-to-left. This panel does not. The mapping is the boring, predictable one: XY(x, y) = y * 8 + x, origin top-left, x increasing right, y increasing down. Out-of-bounds returns -1. Getting this assumption right is the difference between a clean image and a mirrored mess on every other row.

index = y · 8 + x · row-major, origin top-left x y 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 · 17 · 18 · 19 · 20 · 21 · 22 · 23 24 · 25 · 26 · 27 · 28 · 29 · 30 · 31 32 · 33 · 34 · 35 · 36 · 37 · 38 · 39 40 · 41 · 42 · 43 · 44 · 45 · 46 · 47 48 · 49 · 50 · 51 · 52 · 53 · 54 · 55 56 57 58 59 60 61 62 63 (0,0) → 0 (4,1) → 1·8+4 = 12 (7,7) → 7·8+7 = 63 every row just continues counting, no zig-zag.

The coordinate system. Always draw through setPixel(x, y, CRGB). It's bounds-checked, so an off-grid write is a safe no-op instead of memory you didn't mean to touch.

One more twist: the color order is RGB, not GRB

WS2812B strips almost always expect green-first (GRB). This panel is wired so FastLED's COLOR_ORDER is plain RGB, meaning CRGB(r, g, b) maps straight through. It's a small thing that costs an afternoon if you assume the default.

Hardware factValue
LEDs64 × WS2812B, data on pin 14
COLOR_ORDERRGB ⚠ not the usual GRB
XY(x,y)y · 8 + x. Row-major, origin top-left, OOB returns -1
IMUQMI8658C 6-axis · I²C SDA 11 / SCL 12
Flash / PSRAM4 MB flash + 2 MB PSRAM · web UI in LittleFS

Calibration without a reflash: the matrixShow() chokepoint

Real LEDs lie. The panel's green runs hot and dim content gets crushed. Rather than bake corrections into every animation, the firmware applies one global per-pixel transform at the single point every frame passes through on its way to the hardware: matrixShow(). It saves the frame, corrects in place, pushes it, then restores the originals, so animation code keeps working on clean leds[] values and never knows calibration happened.

animation leds[ ] matrixShow( ) 1 · save a copy 2 · correct in place 3 · controller.show() 4 · restore originals leds[] is left exactly as the animation wrote it 8×8 panel corrected profile loaded from calibration.json · live-reload over /api/calibration · no reflash · an uncorrected frame fails SAFE (shows raw)

One transform, one place. Calibration is a save → correct-in-place → restore wrapper around the hardware push, so it's always-on, never double-applied, and tunable live from the calibration lab without a firmware rebuild.

PART 05

Drive it with Claude: the panel as an expression channel

What an open HTTP API unlocks

Because the whole board is just an API, it becomes an ambient status light for AI coding sessions.

A separate, public companion project (Claude Expression Studio) turns the panel into Claude's expression channel: a spinner while it works, a checkmark when it's done, a glyph when it's waiting on you. It does this purely by POSTing to the same endpoints documented above. No shared code, no firmware coupling. Just the contract.

PART 06

Publish its own telemetry: the board as a sensor node

The same board, pushing data out

Part 05 was software driving the panel. This is the panel driving a pipeline.

Flip one switch on the settings page and the board publishes its own sensor readings to any MQTT broker on your LAN: chip temperature and the IMU's accelerometer, retained, every few seconds, no PC in the loop. It's off by default; with no broker configured, the firmware behaves exactly as if the feature didn't exist. Setup is all in the web UI: broker address (a numeric LAN IP), port, publish frequency.

TopicPayload · all retained, QoS 0
plantfloor/matrix/temperature{"celsius":<n>,"ts":"<ISO8601 UTC>"}
plantfloor/matrix/accelerometer{"ax":<n>,"ay":<n>,"az":<n>,"ts":"<ISO8601 UTC>"}
plantfloor/status/matrix{"online":true|false} · birth on connect, last will on a drop

This isn't hypothetical: the publisher feeds a real, working industrial-style pipeline. A companion project (peckworks-plantfloor) runs a Mosquitto broker, a C#/.NET historian that stores every reading, and an OPC UA server that re-exposes the live values to industrial clients, all fed by this firmware publishing for itself. Its site tells the consuming side of the same story, including the dead-man's-switch demo: unplug the board, and an Online node flips to false in a real OPC UA client with nobody polling anything.

PART 07

Flash one yourself: a ~$15 board and one binary

From zero to a glowing panel

Three steps, no toolchain required.

  1. Get the board.

    The Waveshare ESP32-S3-Matrix, an 8×8 WS2812B grid on a USB-C ESP32-S3. On Amazon ↗

  2. Flash one binary.

    One-click flash in Chrome/Edge, or grab the offline installer (merged .bin + flash.bat/flash.sh). It's a single merged image (app + web UI), so there's nothing to assemble.

  3. Join WiFi.

    Connect to the ESP32-Matrix-Setup hotspot, pick your network, then open esp32matrix.local. From here you're back at Part 02, the onboard control panel.

Want to build from source instead? The firmware is plain Arduino (.ino). See the repo's README for the Arduino IDE setup and library list.