Repository navigation
Releases: avandeputte/SplitFlapGateway
Release list
v3.13.1 — 2026-09-25
A bug-fix release. If you use a Quiet Time schedule, take this one.
Fixed
- A Quiet Time schedule could reboot the gateway — and with Restore on Boot enabled, loop. The schedule was evaluated on the RTC task, which has a 2 KB stack that already peaks at about 1.4 KB just reading the clock. Asserting Quiet Time from there sends bus frames through a call chain that needs roughly 1.5 KB more: a stack overflow, a panic, a reboot. Reported as "quiet time does not work with restore on boot: it goes blank, after a minute it loads the backup, and loops" — exactly that sequence: power-up homing (blank), the boot restore, the schedule asserting quiet the moment the restore released the bus, the crash, and round again. The schedule now runs on the network task, which has the stack for it. Verified on hardware: asserting quiet from the schedule now costs 1.8 KB of that task's 6 KB and the gateway carries on.
- Restore on Boot now takes the bus before any task starts, so the schedule can no longer assert Quiet Time in the gap before the restore is armed.
Board: Waveshare ESP32-S3-RS485-CAN (16 MB flash).
SplitFlapGateway-3.13.1.bin— the application image. Upload it from the dashboard (Settings ▸ Open Firmware Updater), push it withespota, or flash it over USB at0x10000together withboot_app0.binat0xE000.boot_app0.bin— the boot selector for a USB flash of the app image.SplitFlapGateway-3.13.1.factory.bin— the complete image for a blank board, over USB at0x0(erases WiFi/MQTT settings).
If the web update fails, see the README's recovery without developer tools.
v3.13.0 — 2026-09-23
One setting, for the companion: the gateway now carries the default step pacing its wall needs.
Added
stepMs— a default step-pacing setting. Settings ▸ Display Layout gains Default step pacing (ms), stored on the gateway and reported asstepMsonGET /api/config(set it withPOST /api/config/settings {"stepMs":25}; 0–100, default 15). The companion (v2.10.22 and later) reads it as its defaultstep_ms— the per-module gap every app page send and Compose inherit — so the pacing a wall needs lives with the wall, not in each client. Larger walls need slower pacing: measured on 60 modules, 15 ms lost 27, 20 ms lost 9, 25 ms was stable. The gateway stores and reports the value; it does not pace its own sends by it.
Changed
- The per-request
step_mscap on/api/rs485/batchand/api/display/cellsis raised from 30 to 100 ms to match, so a stored default above 30 is honoured rather than silently clamped.
Board: Waveshare ESP32-S3-RS485-CAN (16 MB flash).
SplitFlapGateway-3.13.0.bin— the application image. Upload it from the dashboard (Settings ▸ Open Firmware Updater), push it withespota, or flash it over USB at0x10000together withboot_app0.binat0xE000.boot_app0.bin— the boot selector for a USB flash of the app image.SplitFlapGateway-3.13.0.factory.bin— the complete image for a blank board, over USB at0x0(erases WiFi/MQTT settings).
If the web update fails, see the README's recovery without developer tools.
v3.12.1 — 2026-09-14
A bug-fix release for anyone whose display layout is larger than about 50 cells.
Fixed
- Display layouts of more than ~50 cells broke the Display and Calibration tabs ("Could not load display state", "Error loading layout").
GET /api/display/statestreams its cell list in 256-byte batches, and every batch but the last went out without a string terminator, so stale bytes leaked into the JSON at each batch boundary. A small wall fits in one batch and never hit it; a 6 × 20 wall did on every poll. The batch is now terminated before each flush.
Everything else is as in v3.12.0.
Board: Waveshare ESP32-S3-RS485-CAN (16 MB flash).
SplitFlapGateway-3.12.1.bin— the application image. Upload it from the dashboard (Settings ▸ Open Firmware Updater), push it withespota, or flash it over USB at0x10000together withboot_app0.binat0xE000.boot_app0.bin— the boot selector for a USB flash of the app image.SplitFlapGateway-3.12.1.factory.bin— the complete image for a blank board, over USB at0x0(erases WiFi/MQTT settings).
If the web update fails, see the README's recovery without developer tools.
v3.12.0 — 2026-09-01
Everything since v3.8, summarised. The headline is Restore on Boot: keep a calibration backup on the gateway and have it replayed to every module on every power cycle, verified. Along the way it exposed — and this release fixes — the reason browser firmware uploads never completed on this board, a module registry that forgot quiet modules, and a read-back quirk that had been quietly corrupting backups.
Upgrading: flash
SplitFlapGateway-3.12.0.binfrom the dashboard's Open Firmware Updater — the browser upload works again as of this release (see Fixed) — or withpio run -e esp32s3_ota -t upload. If you use Restore on Boot, create a fresh backup after upgrading and upload that: backups made by earlier firmware can carry a truncated flap set for modules with big maps (3.12 skips those entries' flap sets rather than writing them, so an old file is safe, just less complete).
Added
- Restore on Boot (Settings tab) — upload a backup file (the one Backup Calibration produces), tick Restore calibration on every boot, set the post-boot delay (default 10 s), Save. On every boot the gateway writes each module by serial — offset, steps, flap set, then every map entry as its own short frame, paced — re-provisions it if the backup's ID differs, reads every module back and compares field by field (one repair round for anything that differs or does not answer), then homes the wall. A 15-module wall restores and verifies in about 33 s. Run Restore Now does it without a reboot; Cancel Restore stops after the current step.
- The bus is locked for the whole run. Every REST endpoint that would touch the RS-485 bus answers
503 {"error":"restore in progress"}, MQTT commands are dropped, and the gateway's own background traffic pauses. Read-only endpoints, settings, status, the maintenance switch and OTA keep working; the dashboard shows a red border and a progress banner. It always finishes — every wait is bounded and a silent module is counted, not waited for. - REST:
GET /api/restore,PUT/DELETE /api/restore/backup(validated before it is accepted, atomic, max 256 KB),POST /api/restore/run,POST /api/restore/cancel;restoreOnBoot/restoreDelayon/api/configandPOST /api/config/settings; arestoreobject onGET /api/status. - The module registry is permanent. A module is never removed for being quiet. One not heard from in 24 hours is re-queried (a few per minute) so its record stays current; only Identify All, De-provision or a corrupt record removes an entry. The module list and
GET /api/capabilitiesare complete from the first second after boot. pio run -e esp32s3_ota -t upload— a preconfigured espota environment inplatformio.ini.
Changed
- A restore never sends the flap map inside one frame. A module writes each entry to EEPROM as it parses it and cannot keep up with a long frame at line speed: on v31 firmware a 434-byte frame left 20 of 42 entries and the module was deaf for ~6 s. The boot restore,
POST /api/flap/restorebysnand the Settings tab's Restore from File all now send themXW(offset, steps, flap set — which clears the map), wait for that erase, then one pacedm<id>w<i>:<p>per entry — the calibration wizard's own frames, scheduled on the RS-485 task so the web server never blocks.restorebysnreturns at once with{entries, perEntry, id, settleMs}; waitsettleMsbefore re-provisioning or reading the module back. - An inconsistent flap-set tail is ignored. A v31
Areply that loses its tail in transit can parse as64:plus a few characters. That used to land in the registry, in capabilities and in backups — and restoring such a backup wrote the truncated set into the module. The parser now treats a tail whose character count does not match its flap count as "not reported", and no restore path writes or verifies one. Access-Control-Allow-Methodsnow also listsDELETE.
Fixed
- Browser OTA no longer dies near the end of a full-size image. The upload callback set the CORS header on every received chunk, and
WebServer::sendHeader()keeps each call as a heap-allocated node until the response goes out — ~1000 chunks ate ~80 KB of a ~100 KB heap, the connection reset at 1.2–1.3 MB with min-free heap under 1 KB, and nothing was flashed. (The browser's instant "100 %" is the file leaving the browser, not arriving.) The header is now set once, on the response; a 1.48 MB image uploads in ~10 s with 80 KB of heap to spare. The/otapage now waits for the gateway to come back and names the version it booted, and says plainly when nothing was flashed.
Board: Waveshare ESP32-S3-RS485-CAN (16 MB flash).
SplitFlapGateway-3.12.0.bin— the application image. Upload it from the dashboard (Settings ▸ Open Firmware Updater), push it withespota, or flash it over USB at0x10000together withboot_app0.binat0xE000(keeps your settings).boot_app0.bin— the boot selector, for a USB flash of the app image only; it makes the bootloader start the slot you just wrote.SplitFlapGateway-3.12.0.factory.bin— the complete image (bootloader + partition table + boot selector + app) for a blank board, flashed over USB at0x0. Erases WiFi/MQTT settings.
If the web update fails, the README's recovery without developer tools section walks through the browser-based USB flasher and the one-script Wi-Fi push.
v3.8.0 — 2026-07-14
Everything since v3.4, summarised. One client can now drive any wall: a new GET /api/capabilities tells it exactly what a wall can show, and a new POST /api/display/cells sets a whole row through the same contract the Matrix Portal Gateway answers — so the companion drives the physical wall and the LED-matrix emulation identically. Plus quiet-time blanking, a per-board MQTT identity, safer web OTA, and a security fix.
Upgrading from v3.4: if you use the Home Assistant integration the device id changes (see Fixed) — delete the old device. Modules re-report their flap set to the gateway over the first couple of minutes after boot (persistence format bumped); nothing you need to do.
Added
GET /api/capabilities— what characters the wall can show. Every module owns its reel, so the response reportsunion(any module),common(every module — these differ on a mixed wall), andsets: each distinct reel once, with the module ids that carry it ("0-44,50"). Colour flaps are listed by name; modules too old to report a reel areassumed, ones not yet known areunknown. The Matrix Portal Gateway answers this URL identically. The gateway learns each module's flap set from the v31+Areply and persists it (asking a 45-module wall costs ~90 s of bus time), filled by a background trickle and re-read only when anNchanges a reel.POST /api/display/cells— set a run of modules in one call:{ch | color | blank | skip}per cell, named colours,step_mscascade pacing. The same JSON contract the Matrix gateway answers. Lenient — a cell this wall cannot show is skipped, not a 400 that discards the row (the response reportssentvsskipped); only structural errors 400.
Changed
- The dashboard speaks 13 languages plus English, chosen from the browser, with a Settings override and
?lang=, all in one image. - Quiet Time now blanks the wall — entering quiet homes every reel (the Home All operation), leaving it restores what was showing (or the newest request made while quiet). Schedule, manual switch, REST, MQTT and Home Assistant alike.
- The Home Assistant re-skin is complete — every control clears WCAG AA in both themes, and the module cards, status tiles and bus monitor read the palette tokens too.
Fixed
- [Security] A remotely-reachable stack buffer overflow is closed. The by-serial flap-config path (
POST /api/flap/flapconfigwith ansn, and the MQTT flapconfig topic) built its RS-485 frame from an unvalidated serial into a fixed stack buffer, so a long serial overran it. The serial is now validated before use. - Every gateway now has its own MQTT identity. The client id and HA device id were derived from a MAC field identical on every ESP32, so two boards on one broker evicted each other in a loop. They now use the bytes that actually differ. The HA device id changes — delete the old device.
- Web OTA no longer reboots the board mid-flash. A low-heap watchdog fired during an upload (which drives heap low on purpose) and reset onto the old image; it now stands down for the duration of an upload. (Browser OTA on this 16 MB board remains heap-tight near the end of a large image; ArduinoOTA/espota is the reliable path.)
- Smaller fixes: the companion-URL save no longer contends with a firmware upload for the flash;
/api/capabilitiesreturns in ~0.03 s (buffered, scratch in PSRAM) and its module-id range list can no longer truncate or mis-order; and the double-quote flap is reachable by character ("maps to the reel'sq).
Board: Waveshare ESP32-S3-RS485-CAN (16 MB flash). Flash SplitFlapGateway-3.8.0.bin at offset 0x10000, or use the dashboard's OTA upload / espota.
v3.4 — 2026-07-12
Downloads
| File | Use it when |
|---|---|
firmware.bin |
Updating a board that already runs this firmware — upload it through the gateway's web updater (Settings ▸ Open Firmware Updater →, or http://<gateway-ip>/ota). This is the OTA app image. |
firmware.factory.bin |
First flash of a blank board, over USB — bundles bootloader + partition table + app. Write it at flash address 0x0 (e.g. with the ESP web flasher). |
Gives the dashboard the companion's look, folds backup & restore into the
Settings page, teaches the gateway and the companion to tell each other which tabs
they have, and fixes a group of real robustness bugs — most importantly a web
server that froze during a paced batch and a watchdog reboot on large walls.
Drop-in upgrade: no module, MQTT or wiring changes, and backups themselves are
untouched.
Changed
-
A new look — the Home Assistant design language. The dashboard is restyled to
match the companion (which ships the same look), so the two feel like one product.
It follows the browser/OS light or dark preference; there is no theme setting.
Purely cosmetic — no control moved or changed behaviour. -
Backup & restore moved to Settings. The Backup Calibration and Restore
Calibration cards now sit at the end of the Settings page; the Backup tab is
gone. Nothing about the backup format or the restore-by-serial behaviour changed —
only where the controls live. (The backup file is assembled in the browser from the
existing module/EEPROM endpoints; restore posts to/api/flap/restorebysn.) An old
#backupdeep link now lands on Settings rather than breaking. -
Batch pacing no longer blocks the web server.
POST /api/rs485/batchused to
produce itsstep_msstagger by sleeping between frames, on the web task — and
the HTTP server handles one connection at a time, so a paced batch froze the whole
web UI for the length of the cascade (up to the old 8 s cap) while further
connections piled up in the TCP accept queue holding window buffers. The handler now
stamps each frame with a due time, hands it to the RS-485 task (which already wakes
every 5 ms) and returns immediately. The cascade on the wall is unchanged.Two contract notes:
200now means accepted, not transmitted (sentcounts
frames accepted), and the 8 s total-pacing cap is replaced by a queue depth of 127
paced frames in flight. A frame goes out immediately instead of paced when
step_msis 0, when it is longer than 48 bytes, or when the queue is full — so a
cascade beyond 127 paced frames loses its stagger past that point. A whole-page
redraw is one frame per module, so that only bites above 127 modules. -
The companion URL is persisted on a debounce. Two companions pointed at one
gateway each re-register their own URL on their ~30 s heartbeat, so the stored URL
flipped back and forth — and saving on every change meant an NVS write every ~30 s,
forever (observed in the wild). The URL now applies to RAM instantly (the tab is
live at once) but only reaches flash once it has held still for two minutes. A
contested URL therefore never gets written at all, which is the right answer; a
companion re-registers within a heartbeat of any reboot, so nothing is lost.
New
-
Tab advertisement (
POST /api/companion). The companion may now sendtabs—
the deep links its own UI offers — and the response always carriesgwTabs, this
firmware's. Each side then renders the other's real tabs instead of a list hard-coded
on the far side, which is what made the Backup tab above a two-release problem.Both halves are optional and independent, so every old/new pairing works: an older
companion advertises nothing and the dashboard falls back to its built-in companion
tabs; an older gateway returns nogwTabsand the companion falls back to its own list
(which still includes Backup, because a pre-3.4 gateway really does have that tab).An advertised list is taken whole or not at all — any malformed entry, a bad id
(not[A-Za-z0-9_-]{1,24}), a label over 24 printable ASCII characters, more than 10
tabs, or a list over 384 bytes drops the list, and the peer then shows its built-in one.
The list is runtime-only (no flash writes: the companion re-sends it on every heartbeat)
and is cleared when the companion deregisters.
Fixes
-
Watchdog reboot on a large wall.
GET /api/flap/modulessent one chunk per
module. Each chunk could block on a slow client for up to the 3 s socket timeout, so
~41 modules × 3 s exceeded the 120 s web-stall threshold — the supervisor logged
STALL: Web=0and rebooted the gateway. It never fired below ~40 modules, which
is why it hid for so long. The response is now coalesced into ~1400-byte chunks, feeds
the watchdog on each flush, and aborts early if the client has gone away. -
A changed MQTT broker now takes effect.
POST /api/config/mqttsaved the new
host/port but never told PubSubClient about it, so the client kept dialling the
boot-time broker and failed forever withrc=-2until a reboot. It now re-points
the client and resets the failure counter. -
TCP connect timeouts were never actually set. Several call sites used
setTimeout(), which onNetworkClientsets the read timeout and leaves the
connect timeout at its 3 s default — the MQTT client and three web handlers all
meant the latter. They now callsetConnectionTimeout(). -
The dashboard re-downloaded its whole page on every navigation.
GET /now sends
anETagand answers a matchingIf-None-Matchwith304 Not Modified(~53 KB
saved per navigation); the favicon and logo are cached for a week. The dashboard also
gates its periodic polls so they can't stack up on the one-connection web server.
New
-
Tab advertisement (
POST /api/companion). The companion may now sendtabs—
the deep links its own UI offers — and the response always carriesgwTabs, this
firmware's. Each side then renders the other's real tabs instead of a list hard-coded
on the far side, which is what made the Backup tab above a two-release problem.Both halves are optional and independent, so every old/new pairing works: an older
companion advertises nothing and the dashboard falls back to its built-in companion
tabs; an older gateway returns nogwTabsand the companion falls back to its own list
(which still includes Backup, because a pre-3.4 gateway really does have that tab).An advertised list is taken whole or not at all — any malformed entry, a bad id
(not[A-Za-z0-9_-]{1,24}), a label over 24 printable ASCII characters, more than 10
tabs, or a list over 384 bytes drops the list, and the peer then shows its built-in one.
The list is runtime-only (no flash writes: the companion re-sends it on every heartbeat)
and is cleared when the companion deregisters.
v3.2 — 2026-07-10
v3.2 — 2026-07-10
Adds a Home All button to the web UI. Drop-in upgrade from v3.1 — no API,
MQTT, or module-behaviour changes; the only change is in the dashboard.
New
- Home All button. The Display tab (under the Live Display) and the
Calibration tab (in the module picker) each gained a Home All button that
homes every module at once — it broadcastsm*hvia the existing
POST /api/flap/homewith{"id":-1}, so there's no new endpoint.
v3.1 — 2026-07-09
v3.1 — 2026-07-09
Lets the Companion App
store its settings in the gateway's flash, so a companion container becomes
stateless — destroy it, start another on any host, and it restores its
configuration from the gateway. Drop-in upgrade from v3.0: every existing
endpoint, MQTT topic and module behaviour is unchanged, and the additions are
purely a new endpoint pair plus one new config field.
New
-
Companion settings blob store. The gateway now offers a small, dumb blob
store that the companion owns end to end:GET /api/companion/settings→ the storedgzip(minified JSON)body as
application/gzip, or404when nothing is stored yet.PUT /api/companion/settings→ stores the gzipped body verbatim; replies
{"ok":true,"bytes":N}.
The firmware never parses the payload — the companion owns the schema and
compresses/decompresses at its own end. Writes are atomic: the body streams
to a temp file on the FATFS partition and is renamed over the live copy only
once the last byte lands, so an interrupted upload cannot corrupt settings that
were already good. The blob (/compset.gz) survives OTA firmware updates, is
capped at 64 KB (real ones are 1–2 KB), and the companion debounces its writes,
so this costs the flash almost nothing.Errors are
400(empty or truncated body),413(too large),503
(filesystem not mounted) and507(write failed) — in every one of them the
previously stored blob is left untouched. -
Firmware version in
GET /api/config. The response now carries
"version"(e.g."3.1.0"). This is how the companion decides whether a
gateway is new enough to hold its settings; against a 3.0 gateway the field is
absent and the companion quietly falls back to storing them locally.
3.0
v3.0 — 2026-07-07
Adds batch RS-485 send and an automatic Quiet-Time schedule, plus a
cleaner dashboard and a more robust bus monitor. Drop-in upgrade from v2.1 — all
existing endpoints, MQTT topics, and module behaviour are unchanged; the
additions are purely new config + endpoints.
New
- Batch RS-485 send.
POST /api/rs485/batchaccepts many frames in one
request ({"frames":[…],"step_ms":15}), each normalized like/api/rs485/send,
with an optional device-sidestep_mspacing the cascade. A host can now draw
a whole animated page in a single HTTP call instead of one request per module.
Capped at 512 frames / 8 s of pacing; feeds the web watchdog during long
batches. - Quiet-Time schedule. Quiet Time can turn on/off automatically on a daily
schedule. Configure it under Settings → Quiet Time Schedule (enable, a
start/end time, and the days it applies). The schedule is evaluated once a
second against the RTC and toggles Quiet Time as local time crosses the window
(overnight windows supported). Transition-based, so a manual toggle within a
window is respected until the next boundary. Persisted in NVS.GET/POST /api/quiet/schedule→{enabled,start,end,days}(daysis a
bitmask, bit0=Sun … bit6=Sat).
Web UI
- Cleaner top bar. The maintenance checkbox and the IP address were removed
from the header, which now shows just the logo and version badge. Maintenance
mode is still signalled by the yellow border and banner — and that banner now
carries a one-click Turn Off Maintenance button. (The gateway's IP remains
on the Status page.)
Fixes
- Valid JSON on the bus-monitor MQTT topics. A frame containing a
"or\
could emit malformed JSON on the<prefix>/rxand<prefix>/txmonitor topics
(those characters weren't escaped). They are now escaped; the MQTT and
web-monitor encoders share one transcoder; and the MQTT buffer was enlarged so
a full frame of accented / multi-byte glyphs is never truncated mid-message.
Coming soon
- Companion app. A companion application — apps, playlists, and triggers — is
in the works and will integrate tightly with the gateway. More in a future
release.
Compatibility & upgrade notes
- No breaking changes: existing REST endpoints, MQTT topics, and backups from
earlier versions are unchanged. Upgrade the gateway over-the-air as usual
(Settings → firmware update), then confirm the version badge in the header
reads v3.0.
2.1 bugfixes
v2.1 — 2026-06-29
A bug-fix release that makes the color flaps work correctly end to end and the calibration tools honor a module's custom flap set. It is a drop-in upgrade from v2.0 — all endpoints, MQTT topics, and module behavior are unchanged.
Fixes
- Color flaps are no longer turned into letters. The seven color flaps are addressed by the lowercase letters
r o y g b p w(red, orange, yellow, green, blue, pink, white), but the gateway was normalising all lowercase ASCII to uppercase before sending — so a request for blue (b) went out as the letterB. Lowercase letters are still uppercased to match the default reel, except those seven colour codes, which now pass through verbatim. Ordinary text is unaffected (hello→HELLO); lowercase is now meaningful for colours (b→ blue flap). - Calibration Wizard and Character Map now reflect a module's custom flap set. Both were hardcoded to the default 64-flap reel, so a module on firmware v31+ with a custom character set or flap count was still calibrated against
A B C …. They now read the module's live character set and flap count (via the combinedAdump //api/flap/all) and use them throughout — the map labels, the Wizard's per-flap glyph and progress, the whole-board walk, and the default per-flap step positions (spaced by the module's actual flap count, not a fixed 64). - Live Display renders colour flaps. A flap currently showing a color now appears as a colour swatch on the Live Display wall, matching the Character Map, instead of printing the bare color letter.
- Lowercase is visible on the Display tab. The Send Text and Send Single Character inputs no longer force-uppercase what you type on screen, so you can enter and see the lowercase color codes you are sending.
- The web UI no longer serves a stale page after an update. The embedded page is now sent with
Cache-Control: no-cache, so a normal reload always loads the new UI after a firmware flash — previously the browser could keep serving the cached HTML/JS, making an update look like it had no effect.
Compatibility & upgrade notes
- No API, MQTT, or wiring changes; backups are unaffected. The custom-flap-set calibration display requires module firmware v31+ (older modules use the fixed 64-flap default reel, exactly as before).
- Upgrade the gateway over-the-air as usual (Settings → firmware update), then confirm the version badge in the header reads v2.1. Because of the caching fix, do one hard refresh (Cmd/Ctrl + Shift + R) after this upgrade; subsequent updates refresh on a normal reload.