Add Host Lighting add-on: live RGB LED control from host software over a vendor HID interface - #1691
Add Host Lighting add-on: live RGB LED control from host software over a vendor HID interface#1691djGLiTCH wants to merge 10 commits into
Conversation
New AddonOptions entry (field 31) with an enable flag and an XInput lighting mode (OFF/ON/AUTO). Disabled by default like all add-ons; defaults overridable per board via HOST_LIGHTING_ENABLED and HOST_LIGHTING_XINPUT board config defines.
Vendor HID protocol (fixed 64-byte reports) letting host software drive the RGB LEDs live: staged frame edits published atomically to the render loop by COMMIT, whole-frame or overlay takeover, and an automatic return to on-board animations on RELEASE or keepalive timeout. GET_CAPS serves the board's LED map, identity, runtime state, animations and per-light positions from the live configuration, so hosts need no per-board data. Frames cross to the render core through a sequence-locked double buffer; colour conversion is cached so steady-state render ticks reduce to a copy. Config writes use the deferred GPStorageSaveEvent path. The interface is not yet referenced by any USB descriptor; this commit is inert until the transport commits.
Second configuration-descriptor variants append the lighting HID interface next to the gamepad interface; the variant is selected at runtime and the stock descriptors are untouched, so the add-on disabled means byte-identical descriptors to stock firmware. usbdriver routes HID class callbacks for the lighting instance to the protocol module.
A composite descriptor variant carries the XUSB gamepad interface plus the lighting interface, bound to the OS Xbox 360 driver on Windows via MS OS 1.0 compatible-ID descriptors and on Linux via the kernel xpad vendor match. In AUTO mode the board boots with its stock console-identical identity and only re-enumerates as the composite when the host has sent no console authentication within the detection window - consoles never see anything but stock behaviour. ON forces the composite (PC-only); OFF keeps XInput fully stock.
Enable switch and XInput lighting mode select, following the existing add-on section pattern; the two settings are exposed through the get/setAddonsOptions API.
Feature overview, per-mode behaviour, discovery, the full protocol reference, a typical host flow, and measured performance notes.
Capability page 5 serves a light table: one fixed-stride record per light naming the control that owns it, the GPIO and action behind that control, player index, case group and grid position. Page 2 carries one range per button ID and so cannot describe a control that owns several lights, which boards do when two physical buttons share an action. Page 1 gains a feature bitmask, the LED framework, the animation namespace and the render rate, each reserving zero for "not reported", so a host reads what a board offers instead of inferring it. Button IDs 18-19 and 30-41 name A3, A4 and E1-E12 in the light table; SET_BUTTONS still stages only the IDs v1.0 could. Both per-record flags are positive assertions, so a record asserting nothing reads as the weaker case: had the second bit meant "synthesised", an all-zero record would have passed as a genuine per-light table, itself a legal state and so indistinguishable from one never filled in. The map fingerprint now covers the whole pin map rather than per-control lookups, so a profile switch that remaps pins invalidates a cached map even though no stored LED index moved. Staged writes are bounds-checked against the addressable space, and a case range whose configured index falls outside it reports as absent rather than publishing a range no pixel occupies. The takeover is released on USB unmount and suspend instead of waiting out the keepalive. Page 2 [6] reports the extent of the mapped range rather than the sum of the ranges above it, which is the number a host needs to size a frame; SET_MODE's timeout gains a 10 s ceiling. Both are documented in the compatibility section, alongside a correction: SET_BUTTONS colours every light of a control only on the LED-refactor pipeline, which the docs had stated unconditionally. Command IDs and payload layouts are unchanged and the caps format byte stays at 2, so v1.0 hosts are unaffected.
SET_LIGHT stages single lights by their page 5 ordinal, so a host can colour one light of a control that owns several without deriving raw LED indexes from the table first. Entries are batched like SET_BUTTONS and the reply counts applied and skipped the same way; an ordinal at or past the reported total is skipped, exactly as a control without a light is. SET_BUTTONS entries naming the extended IDs - A3, A4 and E1-E12 - now apply on boards that have such lights, through the same action lookup the canonical IDs use, and count as skipped where there is none. They were previously report-only, which made them the one part of the light table a host could see but not act on semantically. Both are additive: a new command in a reserved staging slot, and a skip that becomes an apply where a light exists, which is the same observable change as a user wiring a new light. Hosts detect the version by PING minor >= 2; nothing existing moved and the caps format byte stays at 2.
SET_LIGHT replies now carry a per-entry outcome mask in bytes [5..6]: bit n set means entry n applied, the popcount equals the applied count, and bits at or above the entry count stay zero. A skipped entry names the stale ordinal directly, so a host re-walks page 5 when its cached table has actually moved rather than on every skip. Earlier firmware zero-filled these bytes, so a host gates on the minor version before reading them. SET_LIGHT_RGBW stages up to twelve five-byte entries per report, the RGBW pairing SET_RANGE already had; boards whose chain has no white channel ignore the fifth byte, exactly as SET_RANGE_RGBW does, and the reply carries the same counts and mask. Both staging commands share one handler, so their ordinal handling cannot drift apart. The render conversion honours a host-supplied white byte on chains that have the channel. RGB::value()'s achromatic shortcut never reads W, which would have rendered the textbook subtractive white (0,0,0,W) black, so host pixels carrying W now compose their channel word directly with the same per-channel arithmetic. Hosts that send no white byte - every host predating these commands - render exactly as before, achromatic colours mapped to the white emitter. All additive: the mask occupies bytes v1.2 zero-filled, the new command sits in a reserved staging slot, and hosts detect the version by PING minor >= 3; nothing existing moved and the caps format byte stays at 2.
|
HLP v1.1, v1.2, and v1.3 are now on the branch, as one commit each. All are additive: no command ID or existing payload moved, the capability format byte stays at 2, and a v1.0 host keeps working against a v1.3 board unchanged. HLP Version History
Test builds for every protocol version are published on my fork, each with Classic and Refactor images for all supported boards plus the matching host tools: HLP v1.0, HLP v1.1, HLP v1.2, and HLP v1.3 (v1.3 is the current one to test against). Gate on v1.1: v1.2: v1.3: Validation:
|
Summary
Host Lighting is an optional add-on exposing a second vendor HID interface
(usage page 0xFF47, fixed 64-byte reports) that lets host software drive
the board's RGB LEDs while the controller interface operates normally.
Hosts can address individual buttons, the case strip and player LEDs, in
whole-frame or overlay mode; the board's own animations resume when the
host releases control, disconnects, or goes quiet past a keepalive
timeout.
The board serves its own LED map, colour order, identity, runtime state
and animations over the same interface, read from the live configuration -
no per-board data exists on the host side, and user remaps are picked up
automatically. Discovery is by the vendor usage page, which is stable
across input modes and VID/PID overrides.
Disabled by default like all add-ons; while disabled, every mode presents
byte-identical descriptors to stock firmware.
Trying it
CI artifacts from this PR contain a UF2 per board config. Flash one,
enable the add-on under Configuration -> Add-Ons -> Host Lighting, and the
lighting interface enumerates alongside the controller.
Prebuilt UF2s for every board, from this PR's exact commit, are also
published here if that is easier: https://github.com/djGLiTCH/GP2040-CE/releases/tag/HLP_v1.0
The same release carries a set built on top of the LED refactor (#1514)
for anyone testing that combination, since CI does not build those.
Python reference clients are attached below (discovery, capability decode,
streaming, per-button tests); hlp-ping.py runs a conformance pass
against a connected board. The attachment is self-contained for quick
testing against this PR; OpenStickCommunity/gp2040ce-binary-tools#12
proposes packaged, cross-platform versions of the everyday tools as
their permanent home.
HLP Python Reference Clients v1.0: hlp-tools_HLP-v1.0.zip
HLP Python Reference Clients v1.1: hlp-tools_HLP-v1.1.zip
HLP Python Reference Clients v1.2: hlp-tools_HLP-v1.2.zip
HLP Python Reference Clients v1.3: hlp-tools_HLP-v1.3.zip
Design notes
function ranges with reserved slots; from 1.0, IDs and payload layouts
are frozen and minor versions are additive-only. Full reference in
docs/host-lighting.md (in this PR).
runtime, and CFG_TUD_HID is already 2. Console modes (PS3/PS4/PS5,
Switch, Xbox) never expose the interface.
boots with the stock console-identical identity and re-enumerates as
the composite only if the host sends no console authentication within
~4 s; the composite binds the OS Xbox 360 driver (MS OS 1.0 compatible
ID on Windows, xpad vendor match on Linux). Verified against a real
Xbox 360 console (see Validation). ON forces the composite; OFF keeps
XInput stock.
to the render core through a sequence-locked double buffer, so
multi-report frames cannot tear.
nothing writes flash from USB context. SET_INPUT_MODE validates against
an explicit mode list; unknown modes are rejected rather than rebooted
into.
LED refactor (#1514)
Ported experimentally onto #1514's head: 13 of the 15 modified files and
all new files applied verbatim; the adaptations needed were the
render-hook line and the function reading the light layout for
capabilities. The full test suite passed on that pipeline at the same
throughput. On the refactor, the positions capability page carries real
per-light coordinates (empty on the classic pipeline), and controls with
multiple wired LEDs light all of them. I can rebase this PR onto the
refactor once it is accepted into main, or hold this PR until then.
Validation
Hardware: Haute42 COSMOX (16 LEDs) and COSMOX M-Ultra (46 LEDs including
a 30-LED case strip), both render pipelines, plus a real Xbox 360 console.
commands rejected, capabilities matching each board's wiring (case
strip included; per-light positions on the refactor pipeline).
staging reports plus COMMIT per frame); overlay compositing over live
animations; timeout fallback; input-mode round-trips.
interface on the bus, auto-detect never fires.
inputs behave as stock.
factory IDs and were driven simultaneously without cross-talk.
identical loop-counter probe: ~0.2 us added to the input sampling
interval at 60 fps and ~0.3 us at 100 fps (~0.03 % of the 1 ms USB
polling budget); no configuration measured slower than stock; the cost
was unchanged on the board needing twice the USB traffic per frame;
enabled-but-idle is not measurable. Rendering stays capped by the
existing 100 Hz LED tick.
Discord ahead of this PR.
Scope
split out if that makes review easier.