Skip to content

Changelog

A consolidated timeline of operator- and contributor-visible changes across the four RaceLink repositories. Format inspired by Keep a Changelog — each release is grouped into Added / Changed / Fixed / Removed / Breaking sections.

Source of truth. Each repository maintains its own GitHub releases page (see Useful queries below); this changelog is a curated cross-repo summary for operators and contributors. Engineering detail (commit SHAs, test counts, internal symbol renames, stage-by-stage commit breakdowns) lives in the maintainer-internal engineering ledger and never reaches this file.

2026-08-01 — Configure at flash time, and update without losing it

Added

  • The LED output can be set while flashing. Data pin, chip type and pixel count per output, prefilled from what the selected build actually compiles. Previously a strip that differed from the profile meant a rebuild or a trip through the WLED UI on every device. Requires RaceLink_WLED v0.1.10 or newer; the step does not appear for older versions, because their firmware cannot read the seed.
  • The radio channel is visible and changeable on the device. WLED's usermod settings show the channel the node is really on — as a region and a channel number, not six raw values — and changing it moves the node. A node stranded on a channel no gateway uses can now be reached over its own Wi-Fi AP and put back, instead of being re-flashed. The info panel gained a line with region, channel and frequency.
  • A firmware-only update mode in the flasher. Writes the application partition alone and keeps the radio channel, LED output, Wi-Fi, presets and master binding. The same thing an over-the-air update does, over the cable — for a device with no Wi-Fi in reach, or one whose firmware no longer boots far enough to accept one.

Fixed

  • The RL_RF block in the usermod settings showed the compile-time defaults, not the device's state. It read 867.7 MHz on every node regardless of what the radio was doing, and nothing read it back on save. Anyone using it to check a configured channel was reading a constant.

Changed

  • Each release now publishes the channel table it understands, so the flasher offers exactly the channels the selected version knows. A channel added later is not offered for an older release, whose firmware would store it, fail to recognise it, and leave the node on a frequency nothing else uses.

2026-07-31 — Smaller, readable releases

Changed

  • Two files per build environment instead of five. Gateway v0.1.7 and RaceLink_WLED v0.1.9 publish <env>-<version>-ota.bin (the application, for OTA and the host's firmware dialog) and <env>-<version>-usbflash.bin (the merged factory image, written at 0x0). A six-target release went from 32 assets to 13.
  • Shorter names. The product, the TYPEn device type and the upstream WLED release left the filename — all three are in …-assets.json. The longest asset name went from 100 characters to under 60. The build environment is still spelled out in full, because it is the identifier the release index and the web flasher's device catalog already share.
  • The two files name what you do with them. -ota and -usbflash replace -app and -factory-usb-serial-only. Forcing a factory image through an OTA path is the one mistake that bricks a device, and the new pair is harder to confuse at a glance.

Removed

  • …-bootloader.bin, …-partitions.bin and …-boot_app0.bin are no longer published separately. They are still merged into the factory image; writing -usbflash.bin at 0x0 reaches the same state in one command, on every SoC, with no per-SoC bootloader offset to look up. Their offsets, sizes and digests are recorded as the parts list in …-assets.json. For a build of your own they are still where PlatformIO puts them, in .pio/build/<env>/.
  • …-sha256.txt. It restated the per-file digests that …-assets.json already carries.

Breaking

  • Anything that downloaded a release asset by filename needs updating; the names changed. Anything that reads …-assets.json does not — including the web flasher, which needed no change at all. Releases up to gateway v0.1.6 and RaceLink_WLED v0.1.8 keep the old layout and stay flashable.

2026-07-30 — Flashable releases and a web flasher

Added

  • Factory images in every release. Gateway v0.1.6 and RaceLink_WLED v0.1.8 publish, per build environment, the application image, the bootloader, the partition table, the OTA selector, and all four merged into one …-factory-usb-serial-only.bin written at offset 0x0. A blank chip can now be commissioned from a release without a local toolchain — previously that needed a full PlatformIO build, and for nodes the only alternative was a detour through stock WLED and its Ignore firmware validation checkbox.
  • A machine-readable release index. Each release carries a …-assets.json naming every file's kind, flash offset, size and SHA-256, so consumers never have to parse filenames.
  • The web flasher at https://fw.racelink.dev/. Commissions a gateway, node or startblock from a Chromium-based desktop browser over USB. It verifies the download against the digest the release published, refuses an image built for a different chip, records each device's MAC and can export the session as CSV.
  • The radio channel can be set while flashing. The web flasher writes the chosen channel into the image, so a device starts on it rather than on the European default. This matters most for nodes: a node has no serial command interface, so its frequency otherwise arrives over LoRa — from a gateway that must already be on the node's frequency to reach it at all.

Changed

  • Release asset names now state what they are. Gateway assets carry the device type (…-TYPE1-app.bin); node assets carry the device type and the upstream WLED release they wrap (…-TYPE12-wled_v16.0.0-app.bin), so both versions are readable from the filename.
  • The gateway's platform version and partition table are pinned. Releases now publish images that name concrete flash offsets, so neither may drift between two releases. The layout itself is unchanged.

Fixed

  • Node asset names were malformed. Up to v0.1.7 the staging code folded the whole WLED release name into the file extension, producing names such as RaceLink_WLED-0.1.7-RaceLink_Node_v1_c3_ct62-v16.0.0.0.0_RaceLink_Node_V1_TYPE_10.bin.
  • Flashing firmware from a release described artifacts that did not exist. It listed idealised asset names and an upstream WLED version (v0.15.3) that no release had used. Both are corrected, and the guide now covers the factory images and the web flasher.
  • The guide said the radio band could not be changed at runtime and told operators outside EU868 to build firmware from source. The firmware accepts 863–928 MHz at runtime, which covers US915; what is fixed is the band the hardware was built for.

Unreleased — Ethernet networks: device firmware + full opcode parity (Draft)

Draft — unreleased, on-device bring-up in progress

The core flows — discovery, status, preset and group assignment — are verified on a real W5500 node; broader on-device validation (full control / config / sync / stream, the DHCP lease, broadcast RX, reset timing) is ongoing. Fold this block into a dated release once that bring-up is signed off. See Ethernet networks.

Added

  • RaceLink_Ethernet device firmware. An Ethernet build of the RaceLink_WLED usermod (-D RACELINK_ETH) for an ESP32-S3 + Wiznet W5500 module: a self-contained W5500 SPI/UDP driver, non-blocking DHCP with a static-IP fallback, an Ethernet web-UI panel (link / IP), and the RaceLink_Node_v5_s3_eth build target (device type 13). The core flows are verified on real W5500 hardware; broader on-device validation is ongoing.
  • Full opcode parity over Ethernet. The host's EthernetTransport now sends the complete M2N set — control, config / get-config, sync, offset, indicate, set-group and streaming — in addition to discovery / status / preset. The firmware's transport-agnostic dispatch handles every opcode regardless of medium. The stdlib mock node (scripts/mock_ethernet_node.py) answers the full set too, so the host's loopback end-to-end tests cover every opcode without hardware.
  • Host-driven autosync for Ethernet networks. An Ethernet network has no gateway to emit the periodic OPC_SYNC that keeps node timebases aligned, so the host drives it — one timebase-only OPC_SYNC broadcast per attached Ethernet network every 30 s (configurable via rl_eth_autosync_interval_s). RF networks are left to their gateways; the loop is a no-op on RF-only deployments.
  • Runtime-configurable W5500 SPI pins. The Ethernet build's W5500 pins (SCLK / MOSI / MISO / CS / RST / INT) are now editable in WLED's Config → Usermod Settings → RaceLink UI, exactly like the LoRa radio pins on RF builds. The -D RACELINK_ETH_* build flags become per-target defaults; a saved change reboots to re-init Ethernet on the new pins. Pins are reserved through WLED's PinManager, so a conflict with an LED-bus pin fails loudly at init.

Changed

  • DHCP lease renewal is non-blocking. The W5500 driver tracks the lease (option 51) and rebinds at ~85 % of its lifetime via the existing non-blocking state machine; a failed renewal keeps the current lease IP instead of dropping to the static fallback (which is reserved for initial-acquisition failure). The Ethernet link state is now read live in the info panel instead of being polled every 500 ms.
  • The WLED Ethernet info panel shows the live link / IP and UDP port; the redundant read-only W5500 pin-map row was dropped (the pins now live in the usermod settings).

Fixed

  • Ethernet link status showed "no link" alongside a valid DHCP IP. The link flag was read once right after reset, before auto-negotiation finished. It now refreshes from the PHY register so the info line reflects the real link state.

Wire protocol

Unchanged. The Ethernet firmware reuses racelink_proto.h byte-for-byte, so there is no Ethernet-specific protocol version. On RACELINK_ETH builds the LoRa-only OPC_RF_CONFIG / OPC_GET_RF_CONFIG now reject with ACK_BAD_TYPE instead of persisting a meaningless PHY config and rebooting (the host's EthernetTransport never sends them).

2026-07-29 — Initial-flashing guide for the release binaries

The docs covered building firmware from source and updating it over the air, but never the step in between: getting a published .bin onto a factory-fresh dongle or node. Both firmware repositories publish pre-built binaries, so that gap is now closed.

Added

  • Flashing firmware from a release — new User-Guide page covering the whole commissioning path on Windows and Linux: the per-repository release-asset tables (which <env> belongs to which board, DEV_TYPE, WLED_RELEASE_NAME), checksum verification, USB-bridge drivers and COM-port / dialout setup, esptool installation, download-mode entry, the application-only flash at 0x10000, the four-file full flash for a blank chip, merge-bin for a single factory image, the browser flashers (esptool-js, ESP Launchpad DIY), the stock-WLED bootstrap path for blank nodes, first-boot checks, erase / recovery, and a symptom-driven troubleshooting list.
  • Per-asset flash-layout table. For each of the eight published assets: SoC, bootloader offset, app offset, app-partition size and which partition table the build pins. The app offset is 0x10000 for every asset, so the normal flash is a single decision-free command; the bootloader offset is the only per-SoC value (0x0 on ESP32-S3 / C3, 0x1000 on ESP32-S2) and the full-flash route ships one ready-made command block per SoC instead of leaving the derivation to the reader.
  • Glossary entries for application image, factory image and download mode.
  • Troubleshooting index gained a Flashing / commissioning section — see troubleshooting.md.

Changed

  • The gateway and WLED operator-setup pages and the Firmware Update (OTA) page now point at the flashing guide for the first flash instead of at the build-from-source READMEs. The WLED README leads with "you may not need to build at all".
  • Regional note sharpened. The published binaries are EU-868 builds; there is no runtime way to change the radio band, so non-EU deployments must build from source. Called out in RaceLink_Gateway/operator-setup.md §"Radio defaults" and in the flashing guide.

Documented (no code change)

  • The release .bin files are application images, not factory images. Both workflows publish PlatformIO's firmware.bin; the second-stage bootloader and partition table are not part of a release. Consequence: an application-only write at 0x10000 works on a board that already carries a compatible bootloader (retail dev board, or anything that previously ran an Arduino / WLED build), but a bare module on a custom PCB needs the full four-file flash. Both routes are spelled out in the new guide.
  • All six node profiles pin the 4 MB WLED partition table (board_build.partitions = ${esp32.default_partitions}WLED_ESP32_4MB_1MB_FS.csv, app0 1536 KiB), including the ESP32-S3 ones — which thereby override WLED's own 8 MB S3 default. The gateway pins nothing and lands on Arduino's default_8MB.csv (app0 3264 KiB) via the esp32-s3-devkitc-1 board definition.
  • --flash-mode / --flash-freq / --flash-size are never needed for flashing a release asset — all three default to keep, so the compiled bootloader's own header values survive.
  • Converting a stock-WLED node to RaceLink firmware over the air requires WLED's Ignore firmware validation checkbox, because the release-name gate always reports a mismatch. Gate detail: reference/wled-ota-gates.md.

2026-06-22 — Gateway handling: restart-free detection + assign flow

Added

  • Scan Gateways. A header button that re-enumerates the USB ports on demand and attaches any gateway on a free port — so plugging in a new or swapped gateway no longer needs a host restart.
  • Unexpected-gateway bar + Gateway Assign dialog. An unbound gateway now raises a non-blocking amber bar instead of auto-popping a modal. Its Assign… button opens a dialog with one row per unexpected gateway and, per row, the choice to create a new network, replace the gateway of an "orphaned" network (the network whose bound gateway went missing — pre-selected for a one-click hardware-swap), or bind to any other RF network. Multiple gateways are assigned in one pass, so re-mapping a whole multi-gateway setup onto new hardware is a single flow. See Detecting and assigning gateways.

Changed

  • Bind wizard auto-opens for conflict only. Unexpected (unbound) gateways are routed to the new assign flow; the header ⚠ Pair… button and the SetupChangeAssistant's unknown-gateway action follow the same split.

Fixed

  • RF gateway can no longer be bound to an Ethernet network. The rebind path now rejects a cross-kind bind on the server, and the network pickers only list RF networks — an Ethernet network runs over the host NIC and has no gateway to take over.

2026-06-03 — Startblock group streaming + multi-gateway discovery/IDENTIFY routing + Heltec Wireless Paper

Three threads landed together: the starting-block scene action now streams slot data efficiently to a whole group, multi-gateway hosts route discovery and auto-restore to the gateway that can actually reach a device, and a new e-paper node board joins the lineup.

Added

  • Heltec Wireless Paper node boardRaceLink_Node_v6_s3_heltec_wpaper, device type 51 (NODE_WLED_STARTBLOCK_HWP). An ESP32-S3 + SX1262 LoRa board with a built-in 2.13″ 250×122 e-paper panel, built as a starting-block node — it shares the v3 startblock's pilot-slot / e-paper feature set (the RACELINK_STARTBLOCK feature now covers device types 50 and 51). The GxEPD2 panel class is compile-time selectable (RACELINK_EPAPER_PANEL_E0213A367 / _FC1 / default GDEY037T03) because Heltec ships this board with two different panels; a Vext (active-low) e-ink power rail is enabled before the display inits.
  • RaceLink_Node_v5_s3_eth (the Ethernet node) and the new Heltec profile are both registered as shipping build profiles. See WLED build profiles.

Changed

  • Startblock Control streams only the slots a group actually covers. The scene Startblock Control action used to push all eight pilot slots to a whole group regardless of how many startblock devices were present or which slots they showed. It now reads each device's number of slots / first slot config, computes the covered slot set, and broadcasts only those slots — one packet per covered slot, reaching every device that shows it. A group with no startblock device streams nothing instead of fanning out eight frames.
  • Stream ACKs are only awaited from the devices that should answer. A broadcast OPC_STREAM to a group now scopes its expected-ACK set to the online startblock devices that cover the slot; a plain WLED node sharing the group, an offline member, or a device that doesn't cover the slot is never awaited and can't burn the retry budget. Previously a single offline member could turn a ~1 s startblock update into ~30 s of dead retransmits. When no expected device is online the broadcast still fires once.
  • A new or emptied group stays network-agnostic until its first member. Groups no longer carry a network until a device joins them — the first member sets the group's network (RF or Ethernet). This is what lets a discovered Ethernet device be dropped into a fresh group (see Ethernet networks).
  • Scene editor group picker shows network badges. The Select target groups dialog now carries the same RF / Ethernet network badge the devices page and sidebar show; static and unbound groups stay badge-less. See Multi-Network §Network badges.

Fixed

  • Multi-gateway: a device is bound to the gateway that hears it. On a host with two or more gateways an RF-discovered device never had a network_id stamped, so every unicast to it fell back to the first gateway — a node that IDENTIFYed on gateway B had its auto-restore SET_GROUP sent on gateway A, never heard it, and so was never assigned a group or learned its master. The IDENTIFY handler now binds an unbound device to the network of the gateway that just heard it (it never overrides an existing binding) and reconciles that device's group to the same network, so group-targeted scene sends route correctly too.
  • Discovery reaches every gateway. Discover now broadcasts OPC_DEVICES across every attached transport, not just the first, so devices on a second gateway show up. Freshly discovered devices land in the group the operator created for them rather than whatever was selected in the sidebar.
  • A child action's "broadcast" honours its container's scope. A broadcast child inside an offset_group container used to fan out to every network the scene touched; it now resolves to the parent container's networks only. A top-level broadcast keeps its fleet-wide meaning.

Wire protocol

PROTO_VER_MAJOR/MINOR unchanged. NODE_WLED_STARTBLOCK_HWP (device type 51) is an additive enum value; the startblock config properties (0x8C slot count / 0x8D first slot) now apply to device types 50 and 51.


2026-05-27 — Network-move end-to-end + parallel multi-gateway status

The per-group network move shipped its UI in the previous release, but several layers had to line up before a move actually relocated a node. This release closes those gaps so moving a group to another network works without any manual master-clear or Channel-Scan, and makes the multi-gateway status poll fast.

Fixed

  • Moving a group to another network now completes end-to-end. From the operator's side: after the move the node briefly reboots (~5 s offline), comes up on the target network's radio settings, and re-binds to the target gateway on its own — no manual re-pair. Under the hood three layers were fixed: the gateway firmware now forwards the RF-config opcodes it previously dropped silently; the WLED node clears its master persistence on an RF change so it accepts the new gateway; and the host marks the device offline during the reboot and routes the follow-up group assignment through the target gateway instead of the old one. See WLED master pairing.
  • Node config / stream / RF read-back reach the right gateway. On a multi-gateway setup, per-device actions like WLAN AP open (Node Config), preset streaming, and the RF read-back used by the Setup-Change-Assistant were always sent through the first gateway and timed out for any device on a second gateway. They now route to the gateway that owns the device's network.
  • A moved node is no longer shown online by mistake. A status reply arriving via a gateway that doesn't own the device's network no longer flips the device to "online" — the operator sees the true state during a migration instead of a misleading green dot.
  • Scene network scope is honored on a draft Run. Running an unsaved scene from the editor now respects the scene's network scope. Previously the draft Run fell back to auto-mode and a broadcast action could reach every network even when the scope pinned one.

Changed

  • Get Status (all) polls every gateway in parallel. Each gateway is queried on its own with an independent reply collector, so the wall-clock cost is bounded by the slowest single gateway's reply window rather than the sum across gateways. The result is identical, just faster, and a device on a second gateway no longer needs a per-device retry pass.
  • Pair Assistant uses the region + channel picker. The "migrate to new settings" (B) and "align gateway to devices" (C) flows now pick a Region and Channel from the shipped channel table — the same picker the Network Manager uses — instead of raw frequency / spreading factor / bandwidth number fields. See Region & Channels.
  • Scene editor footer is pinned. The cost-estimator Total badge and the Delete / Duplicate / Save / Run buttons stay fixed at the bottom of the editor, so they're reachable without scrolling past a long action list. The Pair Assistant dialog is also wider so the region + channel dropdowns fit without a horizontal scrollbar.

Wire protocol

Unchanged. OPC_RF_CONFIG carries the same 12-byte body; its node-side behaviour was extended to also clear master persistence before the reboot (see Wire protocol — RF configuration and WLED master pairing).


2026-05-27 — BroadcastTarget + scene-derived sync scope

Changed

  • SYNC fires only on involved networks. A scene whose non-sync actions resolve to a strict subset of networks (e.g. target group on network A only) now scopes its sync action to that subset. Uninvolved networks see no SYNC traffic and cannot accidentally fire pre-loaded arm_on_sync effects.
  • Multi-gateway SYNC is fast again. Two-gateway setups were measured ~104 ms OPC_SYNC USB round-trip before this release; the threaded fan-out brings it back to ~52 ms, bounded by the slowest gateway's air time rather than the sum. Three or more gateways scale the same way.
  • Sync-only scenes fall back to all-attached. Scenes containing only sync / delay actions can't derive a scope from action targets — they keep the conservative pre-refactor "fire on every attached gateway" behaviour. A deprecation warning is logged so the fallback can be tightened to an error in a future release.

Added — operator-pinned scene.network_scope (follow-up iteration)

  • Scope chip in the scene editor header. Click to open an Auto / Explicit picker with a checkbox list of networks. Auto-mode chip shows the live server-resolved preview ("Auto · TrackA + TrackB"); Explicit pins a specific set.
  • Per-action target filter cascade. When the scope is Explicit, the action's group / device dropdown filters to in-scope networks only. Out-of-scope choices disappear from the editor.
  • Out-of-scope warning chip. Switching to a smaller Explicit scope marks any now-out-of-scope action with an "out of scope" warning + red border on the target picker. Saving is rejected with HTTP 400.
  • Sidebar scope badge. Scenes with Explicit scope show a small "N nets" badge next to the label; an amber dot appears when one of the scope's networks no longer exists.
  • Fan-out pill in the editor. When the scene's broadcasts reach 2+ gateways, a green "Fan-out: 2 gateways" pill surfaces above the action list. The cost-estimator's airtime figure stays single-network (parallel airtime; wall-clock is bounded by the slowest radio, not summed).

Fixed

  • Broadcast PRESET / CONTROL / OFFSET fan-out on multi- gateway setups — previously only SYNC honoured scene scope, so broadcast ops with target_group == 255 failed routing at N≥2 gateways.

Wire protocol

Unchanged.


2026-05-27 — Per-group network migration + unified Manage-groups dialog

A group ended up on the wrong network and the operator had no UI path to fix it — the bulk-regroup endpoint actively refused cross-network moves, so the only recovery was NVS-reset + complete re-discovery. This release adds a group-granular migration path and consolidates the WebUI surface into a single dialog.

Added

  • Manage groups dialog (sidebar ↕ button, formerly "Reorder groups") bundles drag-reorder with multi-group network migration. Pick rows via checkboxes, choose a Target network, click Move N selected. Reorder + move are independent Apply buttons — operator can do either, both, or several moves in a row without closing the dialog.
  • Offline-mode dialog flow: default Move uses block — refuses if any device is offline and reveals Skip offline / Force offline buttons. Skip flips metadata only for offline devices (Channel Scan recovers physically); Force attempts the wire push anyway.

Changed

  • Network badge moved out of the DeviceTable column (now removed) into the sidebar group rows and the header band above the device table for the currently-selected group. Read-only in both places.

Removed

  • ResortGroupsDialog.vue + NetworkMoveDialog.vue — replaced by ManageGroupsDialog.vue.

Wire protocol

Unchanged.


2026-05-22 — Multi-network reconnect hardening + UX polish

Stage 5 (2026-05-21) shipped the end-to-end multi-USB-gateway plan. Six bench-test rounds against two physical gateways on a Pi exposed follow-up issues. This release covers the surgical fixes that closed each round.

Changed

  • Central master pill is gone. The header's master bar now carries one pill per attached gateway instead of a single pill driven by the primary slot. Each pill colour-codes the combined Bind + RF state (TX = blue, IDLE = green, RX_WINDOW = warm yellow, ERROR = red, conflict = amber border, unbound = red border, pending = grey). Hover for full details.
  • Pair Assistant is no longer auto-open. Replaces the prior "popup on every USB flicker" model. Reachable via the new reconnect banner, the ⚠ Pair… header button (visible while any gateway needs attention), or the host-settings menu.

Added

  • Per-network reconnect banner. When a gateway disappears mid-session only that one transport drops out — sibling gateways stay fully online. A red banner lists every missing network's gateway with a live 5 s countdown, per-row Cancel, global "Cancel all", and an "Open Pair Assistant" button. The countdown ticks locally and resyncs on every SSE update.
  • Hot-reconnect for known gateways. The host polls every 5 s while any persisted gateway_mac is missing from the transport list. Replug → next poll attaches it → pill comes back. No host restart needed.
  • "Re-discover now" button in Pair Assistant — operator-driven trigger that runs the soft rediscover immediately + clears any per-MAC cancels.

Removed

  • GatewayRfConfigDialog. The 📡 header button is gone — the NetworkManagerDialog channel-edit flow with the Migrate prompt covers the operator-visible RF-config use case.

Fixed

  • Bind-wizard "all dashes" mismatch. When GET_RF_CONFIG returned no readback, the wizard used to pop with every "Gateway reports" cell blank. Bind eval now parks the record at PENDING (grey pill) instead of CONFLICT, no spurious wizard.
  • Pair Assistant title changed from "Pair Assistant (Single Gateway)" to "Pair Assistant" — the Single-Gateway constraint is documented in the dialog description, not the title.
  • Secondary transport silent on RX, IDENTIFY_REPLY, and disconnect detection (listener was not installed).
  • Single-transport disconnect leaving a dead transport in the controller's transport list with controller.ready = False.
  • Cascade where one disconnect caused the sibling to flap-cycle every 5 s (the soft-rediscover IDENTIFY probe was landing on the live sibling's USB-CDC stream).

Wire protocol

PROTO_VER_MAJOR/MINOR unchanged (host + UI only).


2026-05-21 — Multi-USB-gateway support

The multi-USB-gateway plan landed end-to-end: one host can now drive several attached gateways, each carrying its own LoRa channel and its own subset of networks/devices. Single-gateway operators see no behaviour change.

Added

  • RL_Network domain model — the operator-visible bundle of a name, a gateway_mac binding, and an rf_config. Groups and devices belong to exactly one network at a time. The v1→v2 persistence migration synthesises a default network on first boot so single-gateway deployments inherit the data model transparently.
  • Region + channel tables. EU868 and US915 each ship five named channel slots (≥500 kHz separation between every same-SyncWord pair). The Network Manager dialog binds against the table; the host resolves the seven wire-format P_RfConfig fields at apply-time. See reference/channels.md.
  • Gateway-bind state machine. Per attached gateway: pending / bound / conflict / unbound. The GatewayBindWizard auto-opens for conflict / unbound and renders the per-field diff with resolve options (accept_gateway / accept_host / create_network / rebind).
  • RF migration engine. Four-phase pipeline (pre-check → device push → gateway switch → verification). Triggered by the bind wizard's accept_host flow or by POST /api/networks/{id}/migrate. Stranded devices land in Channel Scan recovery.
  • Channel Scan service. Walks a region's channel table on one gateway (volatile-switch → settle → broadcast OPC_DEVICES → dwell → partition into known/unknown). try/finally restores the pre-scan config even on mid-channel failure.
  • Cross-network fan-outOPC_SYNC and broadcast PRESET / CONTROL / OFFSET route via per-network helpers (transport_for_group / transport_for_device).
  • Network Manager + Setup-Change Assistant + scene picker. A two-pane CRUD UI plus a session-once auto-open assistant that diffs against the persisted setup and offers one-click hand-offs to the right wizard.

Changed

  • Network-boundary enforcement. Bulk regroups that span networks now return HTTP 400 with a structured detail payload before the TaskManager job runs. New groups inherit the default network's id.

Wire protocol

PROTO_VER_MAJOR/MINOR unchanged. New opcodes are additive: OPC_RF_CONFIG / OPC_GET_RF_CONFIG / GW_CMD_SET_RF_CONFIG / GW_CMD_GET_RF_CONFIG / EV_RF_CHANGED (0xF6). Pre-Stage-1 firmware silently ignores them per the protocol's forward-compat rule. No persistence-format breakage from the v1→v2 migration engine.

Docs touched (new)


2026-05-20 — Host WebUI: brand visual identity

End-to-end visual sweep that ports the racelink.dev brand language into the Host WebUI without restructuring components or operator flows.

Added

  • RaceLink wordmark in the header (pink-glowing dot + "RACE" in off-white + "LINK" with a pink→cyan gradient text-clip).
  • Self-hosted fonts. Chakra Petch (display) + Sora (body) as GDPR-friendly WOFF2 — no third-party font CDN calls.
  • Brand background. Two radial glows (pink top-left, cyan top-right, both ~12 % opacity) layered over a faint 64 px speed-grid that fades out toward the bottom.
  • Button variant taxonomybrand (Save / Create / Apply / Confirm), run (Run / Start / Re-sync / Send / Start update), destructive (Delete + destructive confirm CTAs). Restyled destructive to fit the outline language.

Changed

  • --color-accent tuned blue → neon cyan → medium teal so links, checkbox/radio fills, and focus rings stop screaming.
  • --color-card moved to a semi-transparent dark surface so panels read as glass plates over the body atmosphere.
  • Native form controls (<input type="checkbox|radio|range">, <progress>, <meter>, <select>) declare accent-color: var(--color-accent) — UA defaults (Edge cyan, Firefox blue) no longer leak through.

Fixed

  • Favicon was 404-ing because no <link rel="icon"> was declared. Now wired through Vite's public/ directory so it resolves under any URL prefix.

Docs touched

  • New contributor playbook: RaceLink_Host/docs/webui-styling-tips.md.
  • Updated RaceLink_Host/ui-conventions.md §"Button visual variants" cross-references the verb vocabulary with the new variant names.

Non-changes

No protocol, API, or operator-flow changes. Same buttons in the same places fire the same packets. No new dependencies. No light-mode support (the reference site has none).


2026-05-19 — WLED: Headless reliability + SYNC precision

Field-testing pass after the 2026-05-18 Headless landing surfaced four issues; all four fixed plus a structural cleanup. No wire- protocol or operator-action changes.

Fixed

  • First slave silently dropped on Headless-master reboot. With two persisted slaves, only the second received its OPC_SET_GROUP. Retry-on-busy + 500 ms inter-send grace delay ensure the slave is never silently skipped. A 40-slave sweep now takes ~20 s, reliable.
  • Master timebase drift on offset scenes. Slaves stayed in sync; the master drifted because it never re-anchored its own strip.timebase after setActivePhaseOffsetMs(). The master now re-asserts the invariant after every SYNC.
  • SYNC precision ±240 ms (Headless-master) vs ±15 ms (regular Gateway). The LBT branch hard-overwrote jitterMaxMs to a 50..300 ms random delay, so the ts24 body field was sampled at caller's millis() but transmitted up to 300 ms later. Headless SYNC now bypasses LBT.
  • Pairing-TX indicator firing for routine SYNC keepalive. Renamed IND_TX_BLIPIND_PAIRING_TX (catalog ID 5 unchanged — wire-stable). Only SET_GROUP sends arm the indicator; routine traffic produces no flash.

Added

  • Auto-scene-rebroadcast after pairing. When a slave joins (proactive boot-burst OR individual reactive pairing) the master broadcasts the current scene once, 1 s after the last successful SET_GROUP in the burst. Freshly bound slaves snap to the master's visual state instead of staying on their boot colour. Debounced (1 s window) so a 10-slave burst collapses to one packet. No-op when the master has no current scene yet — operator picks a scene first.

Master-side ACK telemetry (diagnostic only)

Slaves' ACKs to OPC_SET_GROUP are now logged on the master (RX ACK from XXXXXX echoOpcode=0x03 status=0) and counted in rxAccepted. No visual indicator — the pairing-TX flash already covers the per-send case.

Wire protocol

PROTO_VER_MAJOR/MINOR unchanged. Firmware-internal behaviour fixes + a renamed (but wire-stable) indicator ID.

The Gateway + Host copies of racelink_transport_core.h need the same scheduleSend() body sync for the SYNC-precision fix to benefit cross-repo; Gateway's rl_queueTxNoCad() can simplify to a one-liner once done. WLED-side changes work standalone.


2026-05-19 — Host: faster OTA error path + live error detail + Total-Time badge

Field feedback on the firmware-update workflow surfaced three operator pain-points after a device fails. All fixed in one pass. No wire-protocol or operator-action changes.

Fixed

  • Failure path ~11 s → ~3 s. AP-Enable per-device ACK wait switched from a single 8 s attempt to 1.5 s × 2. Healthy devices ACK in <1 s; the retry recovers a single dropped frame without paying the 8 s penalty.
  • AP-Close skipped on the clean-success path. A successful firmware POST triggers a WLED reboot that drops the AP automatically — sending an explicit AP-Close into the reboot window timed out 3 s per device for nothing. AP-Close now fires only when AP-Enable ACKed and a later step failed (wrong OTA password, bad firmware binary, HTTP 401 / 500 / timeout, …), so the device's still-broadcasting AP doesn't leak its credentials.
  • Live error detail in the per-device row. The OTA workflow publishes a parallel MAC → message map every time it emits a stage event. FwProgressPanel.vue reads it directly and shows the message inline on the red row (e.g. Timeout waiting for CONFIG ACK from <MAC> (AP-enable)).
  • RuntimeError: Python class-name prefix dropped from all operator-visible error strings.
  • Live ETA timer no longer starts at "0:07" on hosts without NTP sync — the timer now anchors on a server-supplied elapsed_s field instead of Date.now()/1000 - started_ts.

Added

  • Total time: M:SS badge in the OTA summary panel.

2026-05-18 — WLED: persistent boot colour + Headless slave registry + pairing-TX indicator

Three coordinated WLED-firmware changes that close long-standing gaps in node identity persistence and Headless-Mode reboot behaviour, plus a visible "I'm transmitting" cue for the Headless Master.

Added

  • Persistent per-device boot colour. The boot-time R/G/B pick is now rolled once on the very first boot and immediately persisted to cfg.json. Every subsequent boot reuses the stored value — a device always lights up in the same colour until the operator changes it. The physical-button click cycle doubles as a boot-colour editor: 10 s after the last click, the currently- displayed colour is written back.
  • Headless Master persistent slave registry. Up to 40 (addr3, groupId) records persisted in cfg.json. Survives reboot and battery swap. After auto-resume probe the master sweeps its registry and sends one OPC_SET_GROUP per known slave so slaves that did not power-cycle alongside the master regain their pairing without having to re-emit IDENTIFY_REPLY. A full 40-slave sweep reads as a continuous green-cyan flash on the master.
  • IND_PAIRING_TX indicator. Local-only (never wire- triggered). Fires on SET_GROUP sends; routine traffic (scene broadcast, brightness, 30 s SYNC keepalive, IDENTIFY probes) produces no flash. 200 ms throttle prevents back-to-back sends from extending the indicator deadline into a sustained overlay; a 40-slave re-bind sweep reads as one continuous flash rather than a flicker storm.

Changed

  • Group-id layout. HEADLESS_FIRST_GROUP_ID was 1; the master conceptually owned no group. Now: master = group 1 (set on entry, cleared on exit), first slave = group 2. Group 0 = unconfigured pool, group 255 = broadcast pseudo-group.

Fixed

  • Flash-wear from pairing-burst saves. Used to fire one cfg.json save per slave (40 slaves → ~80 saves / 30 s). A 5 s debounce now collapses a 40-slave burst into a single save.

Wire protocol

PROTO_VER_MAJOR/MINOR unchanged. IND_PAIRING_TX is append-only and never travels on the wire; older firmware silently drops unknown indicator types. The new Headless Slaves cfg.json key is read-as-empty by pre-change firmware, so a mixed-firmware operator path is benign.

Non-changes

No operator action required for existing fleets. Coordinated flash not required — slaves are agnostic to the master-side changes; OPC_SET_GROUP semantics on the wire are unchanged. The first slave paired under the new firmware on an upgraded master will be assigned to Group 2 (was Group 1); pre-existing pairings continue to work via the idempotent Case A path.


2026-05-17 — Indicator catalog + overlay-rendering refactor + host UI polish

A multi-thread session that landed the operator-facing Click-to- Locate feature, then iteratively cleaned up rendering side effects.

Added

  • Click-to-Locate. Clicking a device name in DeviceTable.vue (when not in rename-edit mode) and the per-row "Locate" button in BatteryDevicesDialog.vue both POST /api/devices/indicate to fire a magenta strobe on the targeted node for 5 s. UI verb is "Locate". Toast wording: "Locating …" / "Locate failed …".
  • IND_IDENTIFY (catalog row, magenta 0xFF00CC, operator-locate trigger).

Changed

  • Indicator catalog standardisation. STROBE-only (BREATH retired for indicators — too subtle in race environments). Urgency-coded speed: 235 = slow / positive event, 245 = medium / informational / operator action, 250 = fast / error.
  • Indicator rendering — frame-buffer overlay. The previous setMode(STROBE) + snapshot/restore approach is gone; indicators now render via Usermod::handleOverlayDraw() after every segment effect has been blended into the strip frame-buffer. The underlying effect is never interrupted, so SEGENV.step / aux0 advance during the overlay and the device returns to fleet phase the instant the overlay clears.

Fixed

  • MasterBar pill staying IDLE while the banner said "Gateway link lost." The pill now shows ERROR (red) whenever gateway.gateway.ready === false, even if no firmware-side STATE_REPORT reached the host.
  • err: detail field reliably cleared on the false→true ready transition.
  • Per-device OTA estimate bumped 21 s → 30 s to match field observations.
  • Post-first-device countdown counting up instead of down. Observed average is now frozen at each deviceIndex advance, so remaining = avg × total - elapsed counts down monotonically at 1 s/s.

2026-05-17 — Wire: rename OPC_SCENEOPC_HEADLESS

Changed

  • Identifier-only rename. The wire byte value (0x0B) and body layout are unchanged — mixed binaries built before and after this rename interoperate byte-for-byte.
  • struct P_Scenestruct P_Headless in lockstep.
  • RaceLinkHeadless::buildScenePacket()RaceLinkHeadless::buildHeadlessPacket(). Same signature, same emitted bytes.

Motivation

Today's host-side RaceLink Scenes travel as OPC_CONTROL on the wire. A future refactor may give them a dedicated opcode named OPC_SCENE — exactly the name yesterday's Headless-Mode trigger had grabbed. Renaming today removes the collision before either side has shipped to operators.

Coordinated update required across the three repos

The Host's gen_racelink_proto_py.py re-run bubbles the new constant into racelink_proto_auto.py automatically. Gateway- and Host-side passthrough is pending follow-up.


2026-05-16 — WLED: Headless Mode + central Indicator system

Two coordinated feature waves on the WLED node.

Added — Headless Mode

  • Five-click on the boot/user button promotes the device to Headless Master after a 1.5-second IDENTIFY_REPLY probe. Any incoming M2N traffic during the probe (typically OPC_SET_GROUP) refuses the promotion (red strobe indicator).
  • Persisted across reboots. A power-cycled headless master re-runs the probe at boot, so a real Gateway that came back up while the device was off correctly overrides the headless mode.
  • Master role on the device — automatic group assignment (counter persisted in cfg.json, wraps at 254); 30-second OPC_SYNC autosync keepalive; long-press drives a non-linear S-curve brightness fade locally, final value broadcast once on release.
  • Gateway always wins (runtime override). Any M2N packet from a non-self sender during active headless triggers immediate step-down. Removes the previously possible "two simultaneous masters on the channel" race window.

Added — Indicators

  • New central status-notification mechanism. OPC_INDICATE carries {type: u8, durationSec: u8}; receivers look the type up in a shared catalog (racelink_indicators.h), overlay the indicator for the requested duration, restore the underlying segment at expiry.
  • Initial catalog: IND_PAIR_CONFIRMED, IND_PROBE_REJECTED, IND_HEADLESS_ENTER, IND_HEADLESS_EXIT. All animated, no pure R/G/B/W — project rule.
  • Duration 0 = cancel. A Host can send OPC_INDICATE (any_type, 0) to clear an active indicator without showing a new one.

Changed

  • Pair-confirmation visual. The previous indefinite white breath after OPC_SET_GROUP is replaced by a 5-second hot-pink breath (IND_PAIR_CONFIRMED). Operators using the visual cue to confirm pairing will need to glance within ~5 s.
  • Probe-reject visual. The previous 1 Hz red/black blink mechanism replaced by an indicator overlay.

Wire protocol

PROTO_VER_MAJOR/MINOR unchanged. Both new opcodes are additive (OPC_HEADLESS = 0x0B, OPC_INDICATE = 0x0C) — older firmware silently drops the packets via the RULES-table length check.

Non-changes

No operator-visible behaviour change for Gateway+Host fleets. A device that has never been promoted to headless operates exactly as before.


2026-05-15 — Host: firmware-update workflow stabilised + live ETA

Multi-device OTA used to fail intermittently on the 2nd or 3rd device of a fleet. A 10-device fleet now finishes in ~3:30 min with zero retries or recoveries needed.

Fixed

  • Per-device sync sequence rewritten. Post-upload host disconnect with nmcli -w 0, wait for IDENTIFY_REPLY after the reboot, wait for the standard _restore_known_device_group worker to push SET_GROUP and ACK, only then send AP-Close. AP-Close is now ACK-blocked instead of fire-and-forget.
  • BSSID selection cascade drops stale scan-cache entries so NetworkManager can't pick a previously-flashed node's dead AP.

Added

  • Pre-commit ETA next to Start button (~21 s × <target count>).
  • Live elapsed · ~remaining left timer in the progress panel that self-refines once one device completes.
  • Single-line summary in the status pill (fwupdate done · (211.4s) · 10/10 ok) instead of the full per-device JSON dump.

2026-05-15 — Host: modal-locked long-running dialogs + cooperative task cancel

Long-running operations that previously left the operator without a status view if the dialog was dismissed mid-flight now lock their dialog and expose an explicit Cancel button.

Added

  • POST /api/task/cancel — single generic cancel endpoint.
  • Cancel button in the Firmware Update dialog. Cancel waits for the current device's flash + verify + reconnect to finish before breaking out of the per-device loop, so no node is left in a half-flashed bootloader state. Worst-case operator wait after clicking Cancel is ~60-90 s (one full per-device round- trip).
  • lockClose prop on dialogs — interactOutside and escapeKeyDown are blocked; the corner X button is hidden. The only way out is an explicit in-dialog action.
  • Browser navigation guard during long-running tasks (back / forward, refresh, tab close, in-app routes) prompts via the native confirm.
  • Three-phase Firmware Update state machine (configprogresssummary). Summary phase renders successful / failed / skipped device lists, host-Wi-Fi-restore status and workflow-level errors.

Changed

  • Same lockdown pattern applied to WledPresetsDialog (download path) and a lighter variant in DiscoverDialog.

Non-changes

The Wi-Fi-restore in the finally block runs regardless, so cancel cannot strand the host on a device AP.

Wire protocol

Unchanged.


2026-05-14 — Host: unified RX reply matcher + single-packet OPC_STREAM

Fixed

  • Startblock Control on a device with empty slots silently rejected. OPC_STREAM now accepts single-packet streams (totalPackets >= 1) on both sender and receiver. Previously anything shorter than two 8-byte chunks was rejected, which is exactly the region the startblock per-slot payload falls into when a slot has no pilot assigned. Saves ~30-50 ms of time-on- air per empty slot at SF7 / BW250.
  • Noisy try_match MISS opc=ACK ... pending_keys=0 debug log on every legitimate stream / discovery / status ACK. The replacement NO_MATCH line only fires when a matcher actually had the right bucket key but its full filter rejected the event — a genuinely diagnostic signal.

Changed

  • Reply-matching primitive unified. Host-internal cleanup merges the previous unicast and broadcast matchers into one data structure (PendingMatcher). One wait loop, one primitive (GatewayService.send_and_match). New developer-guide page: Reply Matching (PendingMatcher).

Wire protocol

PROTO_VER_MAJOR/MINOR unchanged. The OPC_STREAM ctrl-byte encoding is identical; only the accept/reject predicate on both endpoints widened.

Coordinated flash required

OPC_STREAM single-packet support depends on matching firmware on both sides. Roll Gateway and all WLED-startblock nodes together. Hosts on either matcher generation continue to interoperate with both firmwares (the host change is purely internal refactoring).


A two-day investigation into V3 (ESP32-S2) ↔ V4 (ESP32-S3) phase- sync drift on sharp-edged effects (Strobe), plus a related "weak Breathe" symptom on internally-triggered pair-confirmation effects.

Several speculative code-level patches were tried; none demonstrably solved the V3↔V4 drift. The weak-Breathe symptom was identified as operator-side state divergence (segment geometry on devices was historically normalized by an auto-applied Boot Preset; removing the preset exposed the underlying divergence) — a firmware fix is not needed.

The full retrospective with per-change rollback pointers lives in the maintainer-internal session archive at _private/sessions/dev-session-2026-05-sync-investigation.md (gitignored, local-only — relocated from the published docs in Gruppe 6).


Changed

  • Async ePaper rendering. The GxEPD2 e-paper driver is now driven from a dedicated FreeRTOS worker task. Both epaperInit() (boot screen, ~1 s) and every refresh (~1 s on the GDEY037T03 panel) used to block WLED's main loop, freezing LED effects, the web UI and LoRa servicing for the duration. The async refactor pushes all GxEPD2 calls off the main loop.
  • Runtime-configurable radio + ePaper pins. Configurable via Config → Usermod Settings → RaceLink in the WLED Web UI. The previous -D RACELINK_PIN_* and -D RACELINK_EPAPER_* build flags become defaults per build profile rather than hard-coded values, so first-boot behavior on every shipping target is unchanged. A saved pin change triggers an automatic reboot to re-init SPI on the new pins. Pins are now allocated through WLED's PinManager — a conflict with an LED-bus pin fails loudly at radioInit() time instead of silently breaking SPI.

Non-changes

  • Radio chip family stays compile-time. SX1262 vs LLCC68 (and any future chip choice) is still selected at build time via -D RACELINK_SX1262 / -D RACELINK_LLCC68. Rationale: the underlying RadioLib chip-family APIs (SX126x vs SX127x) are not interchangeable at the abstract base.

Breaking — for external embedders only

epaperInit() C++ signature changed from no-argument to seven pin arguments. Only one in-tree caller exists; no external callers. Operators flashing the new firmware on top of an existing install keep their cfg.json — pin values fall back to the build-time defaults when a pins / epaper_pins block is absent.

Docs touched (new)


2026-05-04 — Preset terminology cleanup

Breaking. Disambiguates the long-standing "WLED Control" vs RL-preset confusion in operator-saved data and the SSE topic vocabulary. Wire protocol unchanged.

Breaking

  • Saved scenes containing {"kind": "wled_control"} actions fail to load. Operators must re-save those scenes — the WebUI scene editor now offers Apply RL Effect (with the 14-field parameter form) in their place.
  • Saved Specials configs referencing the wled_control function key fail to load. Operators must re-configure the affected device's Specials → WLED → RaceLink Preset entry.
  • Third-party SSE consumers subscribed to the presets topic must switch to rl_presets (RL preset CRUD) and/or wled_presets (WLED preset upload/select). The legacy union topic is gone.
  • Plugins / scripts calling Controller.sendWledControl or ControlService.send_wled_control must update their call sites (renames listed in the maintainer-internal engineering ledger). RotorHazard plugin shipping with this release is updated in lockstep.

Removed

  • state_scope.PRESETS token (and SSE topic presets). Callers must use state_scope.RL_PRESETS / state_scope.WLED_PRESETS.

Changed

  • wled_control Specials function renamed to rl_preset. Operator-facing label "WLED Control" → "RaceLink Preset".
  • wled_control scene action kind renamed to rl_effect.
  • Classical wled_preset (WLED's per-device numeric preset slot, OPC_PRESET) and host-side rl_preset (lookup of a named RL preset, emits OPC_CONTROL) are unchanged.

Wire protocol

OPC_PRESET, OPC_CONTROL, and packet identifiers are unchanged. RaceLink_Gateway and RaceLink_WLED firmwares interoperate byte-for-byte with both pre- and post-rename hosts.


2026-05-04 — Sidebar group rows: live counts + flash

Added

  • M / N per row — devices currently online out of total devices in the group — with a hover tooltip explaining what "online" means (replied to the last status query or sent an unsolicited IDENTIFY_REPLY recently). Falls back to the server-side device_count when the device list hasn't loaded yet on first render.
  • Group rows now flash the same way the device-table rows do when any of their devices receives data. First-render-doesn't- flash semantics so a fresh page load doesn't strobe the sidebar.

Wire protocol

Unchanged (UI-only change).


2026-05-03 — WebUI: Chrome SSE slot-pool stall fix

Fixed

  • 20–50 s UI freeze in Chrome after ~5 quick switches between /racelink/ and /racelink/scenes via in-page links. The freeze also affected any parallel RotorHazard tab on the same origin. F5 reload was always fine; Firefox was never affected. The fix is three layers (explicit pagehide close on the client, shorter SSE idle-ping cadence on the server, Connection: close on the SSE response) — full technical write-up in reference/sse-channels.md §"Connection lifecycle and Chrome HTTP/1.1 slot pool".

Wire protocol

Unchanged.


2026-05-03 — Groups target picker: search dialog

Changed

  • Groups target picker in the scene editor's unified target picker replaces the inline checkbox grid with a compact summary chip + a modal selection dialog. The summary shows the selected groups in small text together with the total group count and total device count across the selection, so the operator can scan an action without opening the picker. Edit groups… opens a dialog with a search field (filters by name or id), a scrollable result list, and three batch buttons (Select all hits, Deselect all hits, Invert hits). Designed for fleets with many groups.

Wire protocol

Unchanged (UI-only change). No on-disk migration — scene format unchanged.


2026-05-02 — Estimator ↔ runner structural sync

Fixed

  • Cost-badge under-report on sparse-subset offset_group containers. A stray .controller indirection in the API's group-id resolution silently returned [], closing the optimizer's Strategy-C gate. Pre-fix reported 8 packets / 121 B; post-fix correctly reports 5 packets matching the wire.
  • SYNC body sizing. The estimator was sizing OPC_SYNC with flags=0 (4-byte legacy form); the runner has always sent trigger_armed=True (5-byte form). The planner now sizes with SYNC_FLAG_TRIGGER_ARMED so the cost badge matches the wire.

Changed

  • Single source of truth for dispatch planning. A new pure module is now consulted by both the cost estimator and the scene runner — per-kind logic that used to live in two parallel implementations is now in one place. A parity test suite runs every action shape through (planner, estimator, runner-with- recording-stubs) and asserts identical packet counts.

Wire protocol

Unchanged.


2026-05-01 — Broadcast / target-picker unification

Changed

  • Unified target shape across every scene action: {kind: "broadcast"} | {kind: "groups", value: [...]} | {kind: "device", value: "<MAC>"}. The scene editor exposes the unified three-radio picker (Broadcast / Groups / Device) everywhere, replacing the previous mix of "Group/Device" radios
  • "All groups" checkbox + multi-select + "Scope (broadcast)" radio. Pre-unification shapes (scope, singular group, standalone groups field on offset_group) are migrated on read; save-time canonicalisation collapses "every known group selected" → broadcast so future-added groups are also hit.

Added

  • Broadcast option on top-level effect actions (was not available before).
  • New Broadcast Ruleset reference page (full per-opcode rules across Host / Gateway / WLED).
  • New Roadmap page recording two future-feature commitments (capability-agnostic broadcast addressing, group- agnostic re-identification).

Wire protocol

PROTO_VER_MAJOR/MINOR unchanged; host + UI change only. Old persisted scenes load as-is and are rewritten on next save.


2026-04-30 — Documentation consolidation

Added

  • RaceLink_Docs repository — the consolidated public documentation collection.

Non-changes

No code or wire-protocol changes.


Unreleased / in progress

  • (placeholder)

Template for new entries

## YYYY-MM-DD — <release name or component>

### Added
* <operator-facing or contributor-facing addition>

### Changed
* <behaviour or naming change>

### Fixed
* <bug fix>

### Removed
* <surface that was deleted>

### Breaking
* <on-disk shape change, public-API rename, …>

### Wire protocol
<unchanged / additive / `PROTO_VER_MAJOR/MINOR` bump>

Engineering trail (commit SHAs, test counts, internal renames, stage-by-stage breakdown) goes into the maintainer-internal engineering ledger, not here.


Useful queries

GitHub releases per repository:

  • https://github.com/PSi86/RaceLink_Host/releases
  • https://github.com/PSi86/RaceLink_Gateway/releases
  • https://github.com/PSi86/RaceLink_WLED/releases
  • https://github.com/PSi86/RaceLink_RH-plugin/releases

The wire-protocol version pair lives in racelink_proto.h:

#define PROTO_VER_MAJOR 2
#define PROTO_VER_MINOR 0

A drift in any of the three byte-identical copies fails tests/test_proto_header_drift.py.