Skip to content

Matrix Gateway

Alex Van de Putte edited this page Jul 23, 2026 · 28 revisions

Matrix Gateway

A split-flap display with no split flaps.

This is the SplitFlap Gateway firmware ported to the Waveshare ESP32-S3-RGB-Matrix driver board driving a HUB75 RGB LED matrix. The RS-485 transceiver is gone; in its place is a software bus and a wall of virtual split-flap modules, each one emulating a real module's wire protocol byte-for-byte and rendering itself as a flapping character cell on the panel.

Repo: github.com/avandeputte/MatrixPortalGateway — versions up to v1.25.0 ran on the Adafruit MatrixPortal S3; that final MatrixPortal build lives on the matrixportal branch. The move to the Waveshare board (v2.0.0) is a hardware port only: the API, capabilities and companion behaviour are unchanged. v3.0.0 replaced the HTTP server (ESP-IDF esp_http_server, spoken natively) and added GET /api/events — a Server-Sent Events stream that pushes the display state the instant the wall changes (the events capability token; the dashboard's live preview rides it). One breaking change came with it: POST /api/ota/upload and POST /api/fs/upload?name=<file> take the raw file as the request body (no multipart) — curl --data-binary @file instead of curl -F. v3.0.1 rounds it out: the dashboard preview renders the actual framebuffer while canvas/effects own the panel (and tells colour flaps from lowercase letters), companion-advertised tabs appear automatically, ops throughput went from ~8 fps to ~40 fps, and ops/ticker text is full UTF-8 → CP1252. v3.1.0 adds two panel-efficiency features the companion uses automatically: delta framesPUT /api/canvas/rects redraws only the rectangles that changed since the last frame instead of the whole panel — and a named sprite-atlas library (PUT /api/canvas/atlas/<name>, …/save), so a sheet of tiles uploads once and is reused (and optionally persisted across reboots) rather than re-sent every draw. Both cut WiFi traffic on the shared bus. v3.2.0 adds a persistent draw channel (PUT /api/canvas/stream — TLV records over one connection, no per-frame round trip, ~28 fps rect-delta animation), pushes the status JSON over the SSE stream so a connected dashboard needs no pollers, draws glyphs through the row blitter, and serves the dashboard pre-gzipped (54 KB → 17 KB on the wire). v3.3.0 rebuilds the Arduino core with WiFi/lwIP buffers in PSRAM — network traffic no longer competes with the firmware for internal RAM, quadrupling the worst-case heap margin under load (certified: 2 h adversarial soak, 0 reboots, 0 errors, 65 KB floor on the 256×64 board). v3.0 also removed MQTT and Home Assistant support from this gateway: the companion is pure REST and the dashboard is SSE-pushed, so the broker connection was pure weight on a RAM-tight board. (The physical SplitFlap Gateway keeps its full MQTT/HA surface — this applies to the Matrix Gateway only.)

Everything above the bus is the unmodified gateway: the same web UI, REST API and OTA (MQTT/Home Assistant support was removed in v3.0 — see the note above). The Companion drives the split-flap wall without any changes — and, because it asks the gateway what it is, also unlocks the extras a drawn wall adds: mixed-case text, pictographs, named colours, and the canvas it can draw on.

Why use it

  • Try the whole stack with no mechanical build — web UI, companion, the lot, on one driver board plus a panel. No modules, no RS-485, no steppers, no calibration.
  • An LED-matrix display in its own right — full colour, real accented characters, lowercase, the colour flaps, and an animated flip effect with a physical-looking seam.
  • A faithful test target — because it emulates at the protocol level, it exercises the same code paths a real gateway does.

What you need

Part Notes
Waveshare ESP32-S3-RGB-Matrix driver board The controller — an ESP32-S3 with 32 MB octal flash and 16 MB octal PSRAM, USB-C, and a battery-backed PCF85063 RTC (SH1.0 cell socket). It plugs straight onto the panel's HUB75 header.
A HUB75 LED panel Default layout is a 15 × 3 wall on a 128 × 32 chain (two 64×32 panels in series, or one native 128×32). Bigger panels give roomier cells.
A 5 V power supply Sized for your panel — check the panel specs.

64-row panels (128×64) use the HUB75 E address line, which this board wires directly on its HUB75 header — so a 64-row panel just works, with no jumpers or board changes. See the repo's README for the panel/size table.

Panel wiring varies one other way: some panels are BGR rather than RGB. Nothing can detect that — it shows up as every colour being wrong in a telltale pattern (red draws blue, yellow draws cyan, while green and white look fine). That's a setting, not a hardware fix: Settings → LED Panel → BGR.

On a large panel (say 256×64) the framebuffer can outgrow the ESP32-S3's internal SRAM at a high bit depth. Rather than refuse and run headless, the driver auto-reduces the bit depth to the deepest that fits — 256×64 lands on 3 bitplanes — and reports the real value as panel.depth in GET /api/status. See Canvas API → Panel bit depth.

Flash & set up

pio run -t upload        # build + flash over USB-C
pio device monitor       # 115200 baud, native USB CDC

Later updates can also go over the air, from the web updater at /ota. There is no UF2 bootloader on this board — no drag-and-drop flashing, no double-tap reset. If the board ever won't program, hold BOOT while plugging in (or while tapping RESET) and flash with esptool / pio run -t upload.

WiFi works like the real SplitFlap Gateway — with one difference: its hostname is configurable, and the fallback AP is named after it.

  1. On first boot it raises an access point called splitflap-gw-<6 hex digits> (derived from the board's MAC — e.g. splitflap-gw-a3f19c), password 12345678.
  2. Join it, open http://192.168.4.1, and save your SSID + password in Settings → WiFi.
  3. It joins your LAN and the AP shuts off. Reach it at http://.local — and you can change the hostname to something friendlier in Settings.

The same mDNS caveats apply: if .local doesn't resolve on your network, find the board's IP in your router's DHCP client list (look for the hostname above) and use that instead — see Finding it on your network. From there it's the same web UI as the gateway — same tabs, same settings.

One nicety a physical gateway doesn't have: the board's battery-backed RTC (fit a cell in its SH1.0 socket) means the clock is valid seconds after power-on, before any network is up — NTP then disciplines it on every sync. Without a cell, the gateway simply waits for NTP as before.

What's emulated (and what isn't)

  • Emulated: the display commands the gateway sends — show a character (-), show a flap by index (+) and home (h), addressed by module id or broadcast. That is the whole contract now: frames flow one way, the modules act on them, and nothing replies. (Earlier firmware also modelled the bus's v/A identity and batch queries and fabricated a stable serial number per module. With no physical bus to interrogate — every answer was a compile-time constant — that machinery was removed: a drawn module is just an id and its current flap, and GET /api/flap/modules rows are simply {id, flapIndex, flapChar}.)

  • The reel is bigger: nothing is drawn from a physical reel, so nothing is rationed. A virtual module carries 237 flaps where a real one carries 64:

    flaps what they are
    0–155 every Windows-1252 glyph — letters, digits, punctuation, accents
    156–162 the seven colour flaps — red, orange, yellow, green, blue, violet, white
    163–222 the 60 lowercase letters, accents included
    223–236 14 pictographs — ♥ ♦ ♣ ♠ ☀ ☺ ♪ ● ■ ⌂ and the four arrows
  • Not emulated: the mechanism. There's no stepper, Hall sensor or EEPROM, so calibration, diagnostics and provisioning are no-ops (the modules are "born" provisioned). You won't use the Calibration or Provision tabs here.

  • The flip is a rendering effect, not a physics simulation — you can cap or disable it.

Reaching the extra flaps

The last two rows of that table are the interesting ones, and they need a word of explanation, because for a long time they were on the reel but unreachable.

The gateway's original display protocol carries one byte per character, and it has a collision it can never fix: the byte for lowercase r already means RED. That is why m38-r sets the red flap, and it is why, on that path, lowercase has to fold to uppercase and a heart — which has no Windows-1252 byte at all — cannot be addressed by character in any way. This is not a bug to be tidied up; it is the price of a protocol that spends seven of its letters on colours.

Firmware 1.6 answers it by adding a second, index-addressed endpoint:

POST /api/display/cells
{ "start": 0, "step_ms": 15,
  "cells": [ {"ch": "H"}, {"ch": "i"}, {"color": "red"}, {"skip": true} ] }

A cell names a character, a colour by name, a blank, or skip (leave that module alone). Because a colour is now a name rather than a borrowed letter, i can simply be the letter i — and flap 223 can be a heart.

The plain character protocol works byte for byte, and a physical wall is unaffected.

You do not have to do any of this yourself. The Companion asks the gateway what it is (GET /api/config), and if it finds a Matrix Gateway on firmware 1.6 or newer it uses the index API automatically — so apps render in mixed case, colours stay colours, and pictographs appear. Point it at a physical wall and it falls back to the character protocol and folds everything to capitals. Same app, same companion, whichever wall you own.

The panel as a raw canvas

Everything above treats the panel as a wall of flaps. But it is also, physically, a grid of RGB LEDs — and the Matrix Gateway can hand that grid over as a plain framebuffer, bypassing the split-flap emulation entirely, so you can draw anything on it. A real split-flap gateway has no framebuffer at all, so this is the one place a Matrix wall does something the hardware it emulates simply cannot.

There is a lot here — full frames (rgb888, rgb565 or compressed QOI), partial-rectangle updates, an on-device animation loop that plays from PSRAM after the client disconnects, a scrolling ticker, JSON draw ops, six on-device effects (plasma, fire, matrix, fliporama, clock, life) with hue/density parameters, and a framebuffer readback you can poll for a live preview. A client discovers the whole surface from GET /api/capabilities — the canvas, effects and effectParams keys, which a physical gateway never carries.

On-device effects and animations run on the panel itself with nothing on the network — pushing frames over HTTP sustains ~40 fps since fw 3.0.1 (near 8 fps before), while a panel effect is limited only by the display. While a canvas is active the reel renderer stands down; a split-flap command or Quiet Time reclaims the panel, and nothing canvas is persisted, so a reboot returns to the flaps.

→ Full reference: the Canvas API page. You rarely drive any of this by hand — the Companion exposes it through canvas apps: Effects, Image, Animation, Ticker, Lumina Clock, Weather Sky, Overview, Date Card, Stock Graph, Weather Panel, Scoreboard, HA Dashboard and Aquarium — plus dual-surface views of nearly every app: each draws a rich view on the panel and runs on flaps, with channel/quiz apps rendering as text + a themed icon — and any you write yourself.

How it fits the rest of the wiki

The Matrix Gateway is a drop-in replacement for the modules and the gateway. Wherever the rest of this wiki says "the gateway," it applies here too — including registering the Companion. (Exception: the Home Assistant page is physical-gateway-only since Matrix Gateway v3.0 removed MQTT/HA support.) Skip the Hardware and Module Firmware pages entirely.


Next: Companion →

Clone this wiki locally