Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions docs/adr/2026-07-29-license-provenance-discipline.md
Original file line number Diff line number Diff line change
Expand Up @@ -131,3 +131,33 @@ section. Review of any PR touching `lp-xt-*` should check the headers.
- ~~Formalize the CLA / DCO-with-grant mechanism when the first outside
contribution to relicensing-sensitive code is proposed.~~ Done ahead of that
trigger — see `2026-07-31-contributor-license-agreement.md`.

## Addendum (2026-08-01): WLED's license changed, and is not GPL

Several planning docs referring to WLED's ESP32 RMT driver as off-limits
(the classic-ESP32 bring-up roadmap's M5 brief, `lp-fw/lp-ws281x`'s
provenance note) describe it loosely as "WLED's GPL shim." That is imprecise
and, for older code, wrong. Following a community tip, verified from WLED's
own repo history during roadmap M5's `2026-08-01-1459-rmt-priority-hli` plan:

- WLED was **MIT-licensed from 2016-12-28 to 2024-10-15**, then relicensed to
**EUPL** ("Re-license the WLED project from MIT to EUPL (#4194)").
Relicensing is not retroactive — revisions before the switch remain MIT.
- The consult-prohibition in this ADR and `AGENTS.md` (no copying,
transliterating, or line-by-line adapting from copyleft sources) applies
to WLED's **post-switch EUPL code** the same as it would to GPL: EUPL is
copyleft, and neither this ADR's decision nor `AGENTS.md`'s enforcement
distinguishes "which copyleft license" — the rule is "no copyleft source,
full stop," so the practical prohibition is unchanged by this correction.
- What changes: a **pre-2024-10-15 MIT revision** of WLED's own code (not
NeoPixelBus, which it may have adapted — NeoPixelBus was historically
LGPL and needs its own check) is permissively licensed and, per this ADR's
rule 2, portable with attribution and a provenance header, unlike GPL/EUPL
source. This does not by itself authorize using it — it opens a path that
did not previously exist for one specific piece of code.
- The concrete instance: the classic ESP32's level-4/5 high-priority
interrupt vector, parked NO-GO at M5's G1 gate
(`2026-08-01-1459-rmt-priority-hli/notes.md`, "G1 COMPLETE"). Its
documented reopen ladder now runs a step-zero, per-file provenance check
against WLED's MIT-era history before falling back to pure clean-room from
Espressif's Apache-licensed `hli_vector.S`.
1 change: 1 addition & 0 deletions docs/debt/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ stay in place when retired; the log is the history).

| Entry | Status | Since | Area | Cost in one line |
| --- | --- | --- | --- | --- |
| [c6-scan-truncation-accepted](c6-scan-truncation-accepted.md) | carried | 2026-08-01 | fw-esp32c6 ws281x default config | 2ch default truncates ~28% of frames during WiFi scans (editing-time only); reopens on OPC/E1.31 streaming |
| [brightness-applied-before-gamma](brightness-applied-before-gamma.md) | carried | 2026-08-01 | lpc-engine fixture node (value pipeline) | dim gamma-on fixtures collapse to ~1 wire code (30× resolution loss at brightness 38); projects work around it by shipping gamma off |
| [lps-probe-perf-test-load-sensitive](lps-probe-perf-test-load-sensitive.md) | carried | 2026-08-01 | lps-probe/tests | spurious full-gate reds whenever a dev server or sibling session runs; ~20% wall-clock headroom |
| [bundled-firmware-chip-unplumbed](bundled-firmware-chip-unplumbed.md) | carried | 2026-07-27 | studio-web roster cards | the "firmware update available" chip is implemented, tested and story-visible, but `bundled_fw` is never supplied in production — the feature never fires |
Expand Down
40 changes: 40 additions & 0 deletions docs/debt/c6-scan-truncation-accepted.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
---
status: carried
since: 2026-08-01
logged: 2026-08-01
area: fw-esp32c6 WS281x output (2-channel default config)
related:
- docs/adr/2026-07-29-license-provenance-discipline.md
- lp-fw/fw-esp32c6/README.md (WS281x section)
---
# The C6's 2-channel default truncates ~28 % of frames during WiFi scans

**Shape** — the shipped C6 default gives each of its two RMT TX channels one
48-word block (24-word ping-pong halves, 30 µs refill deadlines). Measured
2026-08-01 on the desk jig: under a continuous active-scan loop the
first-started channel truncates **28.0 %** of frames (guard-word trips;
bounded staleness, never corruption), the second 1.3 %. The alternative
config (`ws281x_2blocks`: one channel, 60 µs deadlines) measures 0.49 % under
the same load. Idle and ESP-NOW — the C6's actual shipping radio — measure
clean in both configs (worst: 1 trip in ~2,800 frames). The mechanism is
interrupt-to-service latency during the radio's masked windows, measured at
`Priority::max()`; RISC-V has no higher level, so no software priority fix
exists. Margin (bigger halves) is the only working software lever.

**Why it is acceptable now** — decided at the RMT-priority plan's G1
(2026-08-01): scan-class WiFi load is an editing-time concern, not a
deployed-install concern, and the second output is worth more than scan
robustness. The truncation is visible shimmer during scans, bounded to one
frame period per trip.

**Exit criteria** — reopens if OPC or E1.31 streaming (or any sustained
STA/UDP usage — the never-measured S4 scenario) enters the C6's product
path: measure S4 first, then revisit the default via the recorded matrix
(plan dir `2026-08-01-1459-rmt-priority-hli`, p4 file + logs). A future
I2S/DMA backend (no refill deadline at all) would retire this entirely.

**Log**

- 2026-08-01 — measured (P4 matrix), accepted at G1 with the reopen trigger
named by Yona: "wifi load isn't a big concern for actual used installs...
unless we start streaming opc or e131."
41 changes: 33 additions & 8 deletions lp-fw/fw-esp32c6/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,24 +31,49 @@ belongs in `fw-host`. Browser Studio simulation belongs in `fw-browser`.
`src/output/rmt/` implements `lp_ws281x::RmtHw` for this chip and registers one
driver at boot; the sequencing lives in `lp-fw/lp-ws281x` and is shared with
`fw-esp32s3` and `fw-esp32v3`. The chip has **two RMT TX channels** (48-word
memory blocks), and both are usable in the shipped configuration:
memory blocks):

| Build | Blocks/channel | Usable channels | Window | Refill half |
| --- | --- | --- | --- | --- |
| default | 1 | 2 (`/rmt/ws281x0`, `/rmt/ws281x1`) | 48 words | 24 words (~30 µs) |
| default (**shipped**) | 1 | 2 (`/rmt/ws281x0`, `/rmt/ws281x1`) | 48 words | 24 words (~30 µs) |
| `--features ws281x_2blocks` | 2 | 1 (slot 1 absorbed) | 96 words | 48 words (~60 µs) |

Which one ships is a measurement question (roadmap M5's stress matrix), so it is
a cargo feature, never an env var — and cargo tracks feature-driven `cfg`
unreliably here, so `touch src/main.rs` before rebuilding after flipping it.
`--features ws281x_telemetry` adds a periodic `[WS281X]` counters line per
channel, in the same field order the classic firmware prints.
**2ch × 24-word halves ships as the default**, decided at roadmap M5's G1 gate
(`2026-08-01-1459-rmt-priority-hli`, phase P4) from measurement, not
arithmetic: under a WiFi scan, the default's first channel truncates 28.0 %
of frames (1,380/4,932) vs the `ws281x_2blocks` single-channel config's
0.49 % (25/5,071) — margin drops truncation 57×, but does not eliminate it,
and the C6 sits at `Priority::max()` already (RISC-V has no headroom above
maskable levels to raise into). Yona's call: "wifi load isn't a big concern
for actual used installs. that's mostly an editing concern, unless we start
streaming opc or e131" — so `ws281x_2blocks` is the documented fallback for
scan-heavy conditions (e.g. mid-edit with the client connected over WiFi),
not the shipped default. **Reopen trigger**: OPC/E1.31-over-WiFi streaming
lands (that is also the unmeasured S4 scenario — sustained UDP while
associated — so S4 measurement precedes any config revisit).

Which one ships is therefore a cargo feature, never an env var — and cargo
tracks feature-driven `cfg` unreliably here, so `touch src/main.rs` before
rebuilding after flipping it. `--features ws281x_telemetry` adds a periodic
`[WS281X]` counters line per channel, in the same field order the classic
firmware prints, including the entry-delay fields (see `lp-fw/lp-ws281x`'s
README for what they measure — they are what attributed the scan truncation
above to interrupt-to-service latency rather than refill work).

`--features stress_s2` and `--features stress_s3` add embassy tasks that
replace the ESP-NOW radio driver registration with a continuous load
generator — S2 repeats active WiFi scans to completion, S3 sends bursts of
ESP-NOW broadcasts — for reproducing the M5 stress matrix on the desk. Both
imply `radio`, are off by default, and cost the shipping image nothing
(byte-identical size-check with either enabled). They are a measurement tool,
not a product feature.

Which pins the two channels drive is authored, not fixed: an `Output` node names
a board label (`ws281x:rmt:D10`) and the driver binds that GPIO when the project
opens the endpoint. The desk jig used for M5 wires three strips — D10/GPIO18,
D9/GPIO20, and D8/GPIO19. Only the first two can be RMT channels; D8 is spare
for a future SPI-class output, since the chip has no third TX channel.
for a future SPI-class output — **not an RMT resource**, since the chip has
no third TX channel.

## Common Commands

Expand Down
8 changes: 8 additions & 0 deletions lp-fw/lp-ws281x/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,14 @@ higher-numbered ones includes every earlier channel's refill — which is the co
`on_interrupt`'s index-order service actually imposes, now measured rather than
inferred.

This is the instrument that settled the C6's WiFi-scan truncation: roadmap
M5's stress matrix (`2026-08-01-1459-rmt-priority-hli`, phase P4) found
refill lag flat with or without radio load, while the entry-delay histogram's
delayed-entry population grew two orders of magnitude under scan — the
truncation is interrupt-to-service latency, not refill work, which is also
why raising software priority alone (already at `Priority::max()`) had no
headroom left to give.

`record_lag` deliberately keeps the running maximum with a load/compare/store
rather than `fetch_max`, and the interrupt handler is the only writer, so
nothing is lost. This began as a workaround — `AtomicI32::fetch_max` would not
Expand Down
Loading