-
Notifications
You must be signed in to change notification settings - Fork 1
Gateway API
This is the integrator's reference for driving a gateway programmatically — both the
SplitFlap Gateway (driving a real RS-485 wall) and the
Matrix Gateway (the same firmware family drawing virtual
modules on an LED panel). They speak the same API by design; where they differ, this page
says so. (Both products run on ESP32-class boards — the Matrix Gateway is an ESP32-S3 —
so this page never says "ESP32" to tell them apart: the distinction is the wall each
drives, and the labels are SplitFlap Gateway and Matrix Gateway.) Every display
endpoint here ultimately wraps the raw bus frames documented in
Bus Protocol — POST /api/flap/char with {"id":5,"char":"A"} becomes
m5-A\n on the wire.
(The Companion exposes a separate, Vestaboard-compatible surface of its own — that's Vestaboard API, not this page.)
-
Base URL:
http://<gateway-ip>— orhttp://192.168.4.1while on the setup AP. The SplitFlap Gateway advertises itself over mDNS ashttp://splitflap-gw.local; the Matrix Portal's hostname is configurable and defaults tosplitflap-gw-<6 hex digits>. mDNS is best-effort — see finding it on your network. -
JSON in, JSON out. All
POSTendpoints accept and returnapplication/json(the two binary exceptions — OTA upload and the companion settings blob — are called out below). -
CORS is open. Every endpoint sends
Access-Control-Allow-Origin: *, so a page served from anywhere on your LAN can call the API straight from the browser. - No authentication. Anything that can reach the gateway's IP can drive the wall — the API treats your LAN as the trust boundary. The only credential anywhere is the optional OTA password.
-
Which wall am I talking to?
GET /api/configreportsversion— the API level, not the build: the Matrix Gateway deliberately answers3.1.0there (clients gate features on it) and puts its real build infwVersion, next toproduct.GET /api/capabilitiesalso reportsproduct,fwandapi. Version pairings live on Compatibility.
| Method | Path | Body | Reply | Gateway |
|---|---|---|---|---|
GET |
/api/rs485/messages |
— | Frames buffered since the last call (up to 64); each call drains the buffer | SplitFlap Gateway only |
GET |
/api/log |
— | Commands acted on (REST + MQTT) since the last call, up to 64 — same drain-cursor idea, but there is no wire to trace on a drawn wall | Matrix Gateway only |
POST |
/api/rs485/send |
{"data":"m5-A\n"} — optional "raw":true
|
{ok,bytes} |
both |
POST |
/api/rs485/batch |
{"frames":["m00-A\n","m01-B\n",…],"step_ms":15} |
{ok,sent} |
both |
Framing is normalized for you. Unless "raw":true, the gateway strips/re-adds the
trailing newline and trims junk past a complete known command — m5-A and m5-A\n behave
identically. raw is the debugging escape hatch that sends bytes verbatim. Frame syntax
lives on Bus Protocol.
Batch is how pages get drawn. One POST /api/rs485/batch carries a whole wall — up to
512 frames — in a single HTTP call; it's what the Companion uses for every page,
with no broker in the display path. step_ms (0–30) paces the cascade device-side so the
wall animates instead of snapping over at once. Since gateway 3.4 paced frames are queued
on the bus task and the call returns immediately: 200 means accepted, not yet on the
wire, and the pacing queue holds 127 frames in flight (a cascade longer than that loses
its stagger past that point; the frames still all arrive).
| Method | Path | Body | Reply | Gateway |
|---|---|---|---|---|
GET |
/api/flap/modules |
— | Array of module objects (id, serial, firmware, current character, last-seen) | both |
POST |
/api/flap/char |
{"id":5,"char":"A"} — id:-1 broadcasts |
{ok} |
both |
POST |
/api/flap/index |
{"id":5,"index":1} |
{ok} |
both — 0–63 on a real 64-flap reel, 0–236 on the Matrix Gateway's 237-flap reel |
POST |
/api/flap/text |
{"text":"HELLO","start":0} |
{ok,chars} |
both |
POST |
/api/flap/home |
{"id":5} — id:-1 homes all |
{ok} |
both |
char and text take UTF-8 and transcode to the single Windows-1252 byte the bus uses, so
€ and accented letters work. ASCII letters are uppercased — except the seven lowercase
colour codes r o y g b p w, which address the colour flaps and go through as-is. Characters
with no Windows-1252 representation are rejected.
The SplitFlap Gateway also carries a whole hardware-maintenance tier under /api/flap/* —
version/EEPROM queries, calibration, provisioning, self-diagnostics, per-module flap-set
config, restore-by-serial. The Matrix Gateway omits all of it by design: its modules are
drawn, so there is nothing to calibrate or provision. That tier is documented in the
SplitFlapGateway README and openapi.yaml (see the footer).
| Method | Path | Body | Reply | Gateway |
|---|---|---|---|---|
GET |
/api/display/state |
— |
{rows,cols,cells:[…]} — cell index = row·cols + col = module id; null = no module there |
both |
POST |
/api/display/cells |
{"start":0,"step_ms":15,"cells":[{"ch":"H"},{"color":"red"},{"blank":true},{"skip":true}]} |
RS-485: {ok,cells,sent,skipped} · Matrix Gateway: {ok,cells,sent}
|
both — gateway 3.8, Matrix 1.6 |
/api/display/state drives the Live Display. On the SplitFlap Gateway a cell is the tracked
character ("?" when unknown, e.g. after a home or an index set); on the Matrix Gateway it is
read straight from the module and reported as a code point, so lowercase, accents and a
♥ read back as themselves (a colour flap reports as its protocol letter).
/api/display/cells is the index-addressed display API — the same JSON contract on both
gateways, so one client can drive either wall through one endpoint. Each cell is exactly one
of ch (a character), color (a named flag: red orange yellow green blue purple white), blank (home the module) or skip (leave it alone); step_ms (0–30) paces the
cascade without ever blocking the web server. It exists because the one-byte character
protocol cannot say everything: the byte for lowercase r already means red, and a heart
has no byte at all — see Reaching the extra
flaps.
One deliberate difference:
| SplitFlap Gateway | Matrix Gateway | |
|---|---|---|
| A cell the wall can't show |
Lenient — skipped, reported in skipped (real reels differ per module; only structural errors 400) |
Strict — the whole request is a 400 ("no flap for U+1F600"), resolved before anything is sent |
| Sent on the (emulated) bus as |
m<id>-<char> — each module maps the byte against its own reel |
m<id>+<n> — one shared reel, so an index always names the same flap |
A client that needs certainty consults /api/capabilities first.
| Method | Path | Body | Reply | Gateway |
|---|---|---|---|---|
GET |
/api/status |
— | Uptime, IP, MQTT state, RTC time, NTP, heap, maintenance and quiet flags | both |
GET |
/api/config |
— | Current configuration, passwords excluded; includes version (the API level) |
both |
POST |
/api/config/wifi |
{"ssid":"…","pass":"…"} |
{ok} — reconnects |
both |
POST |
/api/config/mqtt |
{"host":"…","port":1883,"user":"…","pass":"…","prefix":"splitflap"} |
{ok} — reconnects |
both |
POST |
/api/config/settings |
Send only what you're changing: posixTZ, ntpServer, serialDebug, haEnabled, otaPassword — the Matrix Gateway adds grid, panel and flip-effect fields |
{ok} |
both |
POST |
/api/config/rs485 |
Bus parameters (baud, data bits, parity, stop bits) | {ok} |
SplitFlap Gateway only — the Matrix Gateway removed it (there is no RS-485 bus) |
GET |
/api/capabilities |
— | What the wall can show — see below | both, identical shape |
GET / POST
|
/api/maintenance |
{"on":true} |
{ok,on} |
both |
GET / POST
|
/api/quiet |
{"on":true} |
{ok,on} |
both |
GET / POST
|
/api/quiet/schedule |
{"enabled":true,"start":"22:00","end":"07:00","days":127} |
{enabled,start,end,days} |
both |
POST |
/api/mqtt/test |
{"host","port","user","pass"} — all optional, defaults to saved config |
Broker reachability + credentials, without touching the live connection | both |
Maintenance mode makes the gateway ignore externally-originated MQTT commands (the web UI
keeps working) so calibration work isn't disturbed. Quiet time blanks the wall — every
reel homes to its blank flap — and restores it when turned off; the schedule's days is a
bitmask, bit 0 = Sunday. Both always reset to off on reboot.
| Method | Path | Body | Reply | Gateway |
|---|---|---|---|---|
GET |
/ota |
— | Browser firmware-upload page | both |
POST |
/api/ota/upload |
Multipart upload of firmware.bin — the plain app image, never the factory image |
Flashes and reboots | both |
GET |
/lang/{code} |
— | The dashboard's translation dictionary for one language (keys are the English strings); 404 for a code the firmware doesn't ship |
both |
/lang/{code} is served with Content-Encoding: gzip — a transfer encoding the client
inflates transparently. Note the contrast with the companion settings blob below, where the
gzip bytes are the payload.
GET /api/capabilities answers "what characters can this display show?" in one call, made
once when a client connects. Both gateways answer the same URL with the same shape by
design — a client never has to know which kind of wall it is talking to. (It is the "what
can you show" companion to /api/display/cells' "show this".)
The subtlety it exists for: on a real wall the answer is not one set. Every module owns its reel, and since module firmware v31 each can be told a different one — so the response reports set arithmetic, not a single alphabet:
| Field | The question it answers |
|---|---|
charset.union |
Can this wall show a Z anywhere? — every character some module can show |
charset.common |
Can I lay this text across arbitrary cells? — every character every module can show |
charset.uniform |
Does every module carry the same reel? (always true on the Matrix Gateway — one drawn reel) |
charset.assumed |
Module ids too old (pre-v31) to report a reel — theirs is assumed to be the firmware default, and the guess is visible rather than folded in silently |
charset.unknown |
Module ids whose reel is genuinely not known yet — excluded from both sets |
sets |
Each distinct reel once: flaps, source (reported/assumed/builtin), the reel chars verbatim in flap-index order, and the ids that carry it as a compressed list ("0-44,50") |
colors |
The colour flaps the wall actually has, by name (["red","orange",…]) |
maxFlaps |
Flaps a module can carry — 64 real, 237 drawn |
features |
e.g. ["colors","index","batch","quiet","maintenance","ha","ota","flapconfig"]
|
motion |
How the wall moves (Gateway 3.10+, Matrix Gateway 1.12+): {"kind": "drawn" | "mechanical", "settleMs": …}. drawn — a cell is a repaint, interruptible, nothing queues, so sub-second updates are honest; mechanical — motion must physically complete, and settleMs (~4000: a full revolution) is a real constraint. On the drawn wall settleMs is the worst-case flip animation (flapMs × flapMax, live-configurable) — cosmetic pacing, advisory. Stated directly so a client never infers the wall's nature from which endpoints exist. |
Plus product, fw, api, grid (rows × cols) and modules.
union and common genuinely differ: if module 1 carries A-Z and module 2 carries 0-9,
the union is A-Z0-9 but the common set is empty — the wall cannot show HI42 wherever
it likes, and only common says so. Reporting each distinct reel once (rather than one entry
per module) is what keeps the response small: a uniform wall is a few hundred bytes however
large it is.
Two translations are applied to the character sets, matching exactly what the gateway
does when it resolves a frame: the seven colour flaps r o y g b p w are kept out of the
sets and reported by name under colors, and q — which the classic reel borrows for the
double-quote flap — is reported as ". A client that read those as letters would believe a
classic reel can show a lowercase w.
Two endpoint pairs make the Companion and the gateway feel like one product — and let a containerized companion run diskless.
| Method | Body / reply | Behavior |
|---|---|---|
POST |
{"url":"http://192.168.1.60:8000","status":"Running: Weather","tabs":[{"id":"apps","label":"Apps"},…]} |
Register and heartbeat. The URL is persisted (debounced, so heartbeats don't wear the flash); status and tabs are runtime-only and clear on reboot. An empty url deregisters. Either field may be sent alone |
GET |
{url,status,tabs,gwTabs} |
The registered companion's URL, its last reported status, and both sides' tab lists |
The tabs advertisement (gateway 3.4) is how the two navs stay in sync without matched
releases: the companion sends tabs — the deep links its UI actually has — and the reply
always carries gwTabs, the gateway's own. Each side then links exactly what the other
really offers. Both halves are optional and independent, so any old/new pairing works: a peer
that says nothing simply gets the built-in list.
The gateway is deliberately a dumb blob store for the companion's settings, playlists and
triggers. The payload is gzip(minified JSON) whose schema belongs entirely to the
companion — the firmware stores the bytes verbatim, hands them back byte-for-byte, and never
parses them. A companion container becomes effectively stateless: destroy it, start
another on a different host, and it restores its configuration from the gateway on boot.
| Method | Behavior |
|---|---|
GET |
Returns the stored blob as application/gzip, or 404 when nothing is stored yet |
PUT |
Stores the gzipped request body atomically; replies 200 {"ok":true,"bytes":N}
|
- Atomic writes. The body streams to a temp file, renamed over the live one only once the last byte lands — a crash or dropped connection mid-upload can never corrupt settings that were already good.
-
No
Content-Encoding: gzip. The gzip bytes are the payload, not a transfer encoding of it — declaring the encoding would make HTTP clients silently decompress the body. The companion decompresses it itself. -
Bounded. Blobs over 64 KB are rejected with
413; real ones are 1–2 KB, and the companion debounces its writes so a burst of edits becomes one write. -
Durable. The blob lives on the FATFS partition (
/compset.gz), which a firmware update doesn't touch — it survives OTA. -
Errors:
400empty or truncated body ·413too large ·503filesystem not mounted ·507write failed.
On the companion side this is COMPANION_SETTINGS_STORE: mirror (default — local file
primary, mirrored here), local, or gateway (diskless). The companion gates the feature on
GET /api/config reporting version >= 3.1 and quietly falls back to local storage against
an older gateway.
Optional. Default topic prefix splitflap (configurable in Settings → MQTT), default
port 1883. MQTT never carries display frames for the companion — that path is always
REST — but it is the transport for Home Assistant and for external automations.
| Topic | Payload | When |
|---|---|---|
splitflap/rx |
{"ts":…,"wt":"…","command":"m5-A"} |
Frame received from the bus |
splitflap/tx |
{"ts":…,"wt":"…","command":"m5-A"} |
Frame transmitted to the bus |
splitflap/status |
Heartbeat with the full diagnostic set — uptime, frame counters, heap, RSSI, module count, IP, version, maintenance/quiet flags | Once per minute |
splitflap/flap/adv |
"AABBCCDD…" |
Unprovisioned module advertisement |
splitflap/flap/ack |
{"id":5,"sn":"…"} |
Provisioning acknowledgement |
splitflap/flap/version |
{"id":5,"ver":"12","reportedId":5,"sn":"…"} |
Version response |
splitflap/flap/calibrated |
{"id":5,"stepsPerRev":4096} |
Calibration result |
splitflap/flap/dump |
{"id":5,"dump":"…"} |
EEPROM dump response |
splitflap/availability |
online / offline
|
Retained; offline via MQTT Last Will |
splitflap/display/state |
HELLO WORLD |
Best-known display contents (? = unknown), on change |
splitflap/maintenance/state |
ON / OFF
|
Maintenance mode |
splitflap/quiet/state |
ON / OFF
|
Quiet time |
The rx/tx/status/flap/* topics publish whenever MQTT is connected; availability,
display/state, maintenance/state and quiet/state (plus the discovery configs) publish
only when Home Assistant integration is enabled on the Settings tab.
| Topic | Payload | Action |
|---|---|---|
splitflap/send |
m9h\n or {"data":"m9h\n"} (optional "raw":true) |
Send a raw frame — normalized like /api/rs485/send
|
splitflap/flap/set |
{"id":5,"char":"A"} |
Show character |
splitflap/flap/home |
{"id":5} |
Home module |
splitflap/flap/provision |
{"sn":"AABBCC…","id":5} |
Provision module |
splitflap/flap/flapconfig |
{"id":5,"flapCount":40,"charSet":" ABC…€é"} |
Configure a module's flap set (fw v31+) — SplitFlap Gateway only; the Matrix Gateway dropped this topic (its reel isn't configurable) |
splitflap/display/set |
HELLO |
Show a string from module 0 (the Home Assistant text entity) |
splitflap/maintenance/set |
ON/OFF/true/1
|
Set maintenance mode (reachable even while it's on) |
splitflap/quiet/set |
ON/OFF/true/1
|
Set quiet time |
The Matrix Gateway carries this MQTT surface over from the gateway untouched (minus
flap/flapconfig); topics tied to physical-bus events — provisioning adverts, calibration
results — simply never have anything to report there, since its modules are born provisioned.
With MQTT configured, both gateways can also announce themselves to Home Assistant via retained MQTT discovery (opt-in on the Settings tab) — a device with a Display text entity, Maintenance and Quiet switches, and diagnostic sensors. That, and what the companion adds on top, is covered on Home Assistant.
The two repos are the source of truth; each ships a machine-readable OpenAPI 3.1 spec you can
import straight into Postman (Import → drop the file, then set a baseUrl collection
variable to your gateway's IP), Swagger Editor (editor.swagger.io
for interactive try-it-out docs), or Insomnia:
| Gateway | README (API sections) | OpenAPI spec |
|---|---|---|
| Split-Flap Gateway (RS-485) | README.md | openapi.yaml |
| Matrix Gateway | README.md | openapi.yaml |
The specs can lag the READMEs in places — the Matrix Gateway spec, for instance, still caps
/api/flap/index at 63 from before the 237-flap reel. Where the two disagree, the README
is current.
See also: SplitFlap Gateway · Matrix-Portal-Gateway · Bus-Protocol · Vestaboard-API · Home-Assistant · Compatibility
Start
Build it
- Hardware
- Module Firmware
- Provisioning
- Calibration
- Flaps & Character Sets
- SplitFlap Gateway
- Matrix Gateway
- LCD Gateway
Drive it
- Companion
- Built-in Apps
- Standalone & Docker
- Multiple Displays
- Home Assistant
- Vestaboard API
- MCP Server
- Using splitflap-os
Extend it
Reference
Hardware © Adam G Makes