Declare WLED firmware compatibility and pin it with fixtures (#62) #116
No reviewers
Labels
No labels
area/ai
area/backend
area/frontend
area/infra
area/scheduler
area/wled
good-first-issue
priority/high
priority/low
priority/medium
type/bug
type/chore
type/ci-cd
type/docs
type/feature
type/qa
v1.0.0
v1.1.0
v1.2.0
v2.0.0
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
rbrooks/Iris-WLED!116
Loading…
Reference in a new issue
No description provided.
Delete branch "feat/62-wled-firmware-compat"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Closes #62.
WLED's JSON API drifts across releases and Iris assumed one shape of it. This declares the supported range, records real API responses to test against, and makes untested firmware fail legibly.
I probed the live roofline controller read-only before writing anything, which turned up three real bugs — so this is more than the docs-and-fixtures job the issue reads as.
The dev controller runs WLED 16.0.0
Not 0.16.0. WLED dropped the leading zero for the 2026-05-03 release, so
16.0.0follows0.15.x. Versions are parsed into component tuples and compared as such: reading the second component as "the version" — the natural habit from the entire 0.x era — yields0on 16.0.0 and would reject the newest firmware as ancient.Bugs found and fixed
segment_countwas always 1.get_inforeadinfo.leds.segs, a field no WLED firmware has ever sent — absent from both the API reference and a real device. It silently took the default on every controller. The count comes fromleds.seglc, the per-segment capabilities array.The old test passed only because it invented the same field the code invented. Worth noting the fixture design here: the captured 16.0.0 device genuinely has one segment, so it cannot distinguish the fix from the bug. The derived fixtures carry two segments deliberately, and I verified that by reintroducing the bug —
test_segment_count_comes_from_seglc[0.14.0]and[0.15.0]fail,[16.0.0]passes.GET /api/v1/wled/effectsdid not exist. The frontend has called it since the effect picker was built (frontend/src/api/index.ts:144).useWledEffectsis written to fail silently, so a missing endpoint was indistinguishable from an unreachable controller — the picker always fell back to its built-in 113-effect list against a controller reporting 220. Now implemented as a proxy of/json/eff; it has to be proxied because the controller is usually on a LAN the browser cannot reach and serves no CORS headers.Effect ids drift, and two entries were already wrong. Ids are positional and get reassigned:
Neighbours unchanged either side, so it is a repurposed slot rather than an offset. A test now checks the whole offline list against the captured 16.0.0 table.
Graceful failure, without a hard gate
Firmware below 0.14.0 — or a host that is not a WLED device — now produces one clear warning at startup,
firmware_supported: falseplus an explanation onGET /wled/status, and that explanation appended to any failing push.It does not refuse to drive the controller, which is a deliberate departure from the issue's fourth bullet. The fields Iris writes have been stable far longer than the supported range, so a 0.13 device will very likely work fine; a hard gate would strand a working setup over a problem that may not exist. The bullet's emphasis is "clear error, not a stack trace", and that is what this delivers. Happy to tighten it to a refusal if you'd rather.
Fixtures
backend/tests/fixtures/wled/, one directory per version, with provenance documented in its README and reflected in the tests:16.0.0/— captured from the roofline controller, verbatim apart from redacted device identifiers (mac,ip,deviceId,bootloaderSHA256,wifi.bssid), none of which Iris reads.0.15.0/,0.14.0/— derived from WLED's published API reference and labelled as such. No hardware for those was available, so they carry only documented fields; inventing anuptimewould add fabricated detail without adding coverage.The interesting test is the drift tripwire: Iris only ever writes state, and WLED ignores keys it does not recognise, so a removed field would not raise — the lights would just quietly do the wrong thing. Every key Iris writes is asserted present in each firmware's recorded segment. There is also a check that the preset payload matches a preset the device actually saved.
Split out
#115 — preset sync targets
GET/POST /json/presets, which no firmware implements; a real device answers501 {"error":4}. Live sync has never worked. The rework needs a different mechanism (/presets.jsonread,/uploadwrite) and hardware to verify, so it is its own issue.This PR only makes the failure loud.
sync_presetsswallowed the read error and built its payload on{}, dropping every preset outside the managed range — exactly the ones it documents itself as preserving. Nothing was lost because the write fails too, but that stops being true the moment #115 fixes the write.Also
Fixed the README's Status section, which still said "Approaching the v1.0.0 release", and gitignored
frontend/*.tsbuildinfo(a baretsc -bwrites them, and they turned up staged after running the frontend checks).Verification
ruff check,ruff format --check,mypy appall cleannode:22:tsc -b,eslint,vitest(76 tests),npm run buildall cleanuvx pre-commit run --all-filespassessegsfix verified by reintroducing the bug and confirming the tests catch itNothing in this PR drove the lights — every probe was a GET.
🤖 Generated with Claude Code