Skip to content

Releases: avandeputte/SplitFlapGateway

v3.13.1 — 2026-09-25

Choose a tag to compare

@avandeputte avandeputte released this 26 Sep 03:08

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 with espota, or flash it over USB at 0x10000 together with boot_app0.bin at 0xE000.
  • 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 at 0x0 (erases WiFi/MQTT settings).

If the web update fails, see the README's recovery without developer tools.

v3.13.0 — 2026-09-23

Choose a tag to compare

@avandeputte avandeputte released this 23 Sep 23:47

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 as stepMs on GET /api/config (set it with POST /api/config/settings {"stepMs":25}; 0–100, default 15). The companion (v2.10.22 and later) reads it as its default step_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_ms cap on /api/rs485/batch and /api/display/cells is 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 with espota, or flash it over USB at 0x10000 together with boot_app0.bin at 0xE000.
  • 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 at 0x0 (erases WiFi/MQTT settings).

If the web update fails, see the README's recovery without developer tools.

v3.12.1 — 2026-09-14

Choose a tag to compare

@avandeputte avandeputte released this 14 Sep 16:06

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/state streams 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 with espota, or flash it over USB at 0x10000 together with boot_app0.bin at 0xE000.
  • 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 at 0x0 (erases WiFi/MQTT settings).

If the web update fails, see the README's recovery without developer tools.

v3.12.0 — 2026-09-01

Choose a tag to compare

@avandeputte avandeputte released this 01 Sep 22:26

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.bin from the dashboard's Open Firmware Updater — the browser upload works again as of this release (see Fixed) — or with pio 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 / restoreDelay on /api/config and POST /api/config/settings; a restore object on GET /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/capabilities are complete from the first second after boot.
  • pio run -e esp32s3_ota -t upload — a preconfigured espota environment in platformio.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/restorebysn and the Settings tab's Restore from File all now send the mXW (offset, steps, flap set — which clears the map), wait for that erase, then one paced m<id>w<i>:<p> per entry — the calibration wizard's own frames, scheduled on the RS-485 task so the web server never blocks. restorebysn returns at once with {entries, perEntry, id, settleMs}; wait settleMs before re-provisioning or reading the module back.
  • An inconsistent flap-set tail is ignored. A v31 A reply that loses its tail in transit can parse as 64: 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-Methods now also lists DELETE.

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 /ota page 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 with espota, or flash it over USB at 0x10000 together with boot_app0.bin at 0xE000 (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 at 0x0. 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

Choose a tag to compare

@avandeputte avandeputte released this 14 Jul 21:53

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 reports union (any module), common (every module — these differ on a mixed wall), and sets: 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 are assumed, ones not yet known are unknown. The Matrix Portal Gateway answers this URL identically. The gateway learns each module's flap set from the v31+ A reply and persists it (asking a 45-module wall costs ~90 s of bus time), filled by a background trickle and re-read only when an N changes a reel.
  • POST /api/display/cells — set a run of modules in one call: {ch | color | blank | skip} per cell, named colours, step_ms cascade 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 reports sent vs skipped); 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/flapconfig with an sn, 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/capabilities returns 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's q).

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

Choose a tag to compare

@avandeputte avandeputte released this 12 Jul 15:08

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
    #backup deep link now lands on Settings rather than breaking.

  • Batch pacing no longer blocks the web server. POST /api/rs485/batch used to
    produce its step_ms stagger 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: 200 now means accepted, not transmitted (sent counts
    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_ms is 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 send tabs —
    the deep links its own UI offers — and the response always carries gwTabs, 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 no gwTabs and 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/modules sent 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=0 and 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/mqtt saved the new
    host/port but never told PubSubClient about it, so the client kept dialling the
    boot-time broker and failed forever with rc=-2 until 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 on NetworkClient sets 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 call setConnectionTimeout().

  • The dashboard re-downloaded its whole page on every navigation. GET / now sends
    an ETag and answers a matching If-None-Match with 304 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 send tabs —
    the deep links its own UI offers — and the response always carries gwTabs, 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 no gwTabs and 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

Choose a tag to compare

@avandeputte avandeputte released this 10 Jul 19:15
c9b652f

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 broadcasts m*h via the existing
    POST /api/flap/home with {"id":-1}, so there's no new endpoint.

v3.1 — 2026-07-09

Choose a tag to compare

@avandeputte avandeputte released this 09 Jul 23:45
8815cb0

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 stored gzip(minified JSON) body as
      application/gzip, or 404 when 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) and 507 (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

3.0

Choose a tag to compare

@avandeputte avandeputte released this 09 Jul 01:46
4c2a4b2

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/batch accepts many frames in one
    request ({"frames":[…],"step_ms":15}), each normalized like /api/rs485/send,
    with an optional device-side step_ms pacing 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} (days is 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>/rx and <prefix>/tx monitor 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

Choose a tag to compare

@avandeputte avandeputte released this 30 Jun 02:42
10464c4

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 letter B. 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 combined A dump / /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.