# Phase 14: USB Serial Foundation and LED Control - Context

**Gathered:** 2026-04-18
**Status:** Ready for planning

<domain>
## Phase Boundary

Deliver safe, CLI-controllable WS2812B LEDs over USB serial to the WLED ESP32-C3 prototype hardware (MagWLED-1). Per-LED RGB addressability for 10 LEDs, with a non-negotiable software brightness ceiling and a dedicated writer thread isolated from RunFrame.

Phase 14 covers: `LHWD-01` (USB serial via Adalight), `LHWD-02` (brightness ceiling), `LHWD-03` (auto-off on unload), `TRNS-01` (`ILedTransport` interface), `LCTL-01..05` (pipe commands + 30fps streaming), `DIAG-02` (vrsettings configurable).

Phase 14 does NOT cover: `LHWD-04` (VID/PID auto-detect — Phase 15), `DIAG-01` (`status` command — Phase 15), `TRNS-02/03` (DDP fallback + transport selection — Phase 15), VRChat OSC bridge (Phase 16), avatar prefab (Phase 17).

</domain>

<decisions>
## Implementation Decisions

### Pipe Command Surface
- **D-01:** Command prefix is `backglow` (matches `beyond_backglow_ctl.exe` and VRSettings naming). Replaces the `led` prefix used in some research docs.
- **D-02:** Color values are **hex strings** on the pipe wire (e.g. `FF8000`). Decimal RGB triplets rejected.
- **D-03:** `backglow fill <hex>...` is **polymorphic**:
  - 1 hex argument → all 10 LEDs set to that color (uniform fill)
  - N hex arguments → per-LED colors, one hex per LED, must equal the LED count (10 in v3.0)
  - Any other count → `ERR fill expects 1 or 10 hex values`
- **D-04:** `backglow set <index> <hex>` sets a single LED by zero-based index (`set 3 00FF00` lights LED 3 green).
- **D-05:** `backglow bri <0-255>` sets master brightness (see D-09 for clamping).
- **D-06:** `backglow off` powers all LEDs off (sends WLED `{"on":false}` JSON over the same serial port).
- **D-07:** No `backglow status` in Phase 14 — DIAG-01 is mapped to Phase 15.

### Serial Transport
- **D-08:** **Adalight** binary protocol for per-LED color streaming, behind the `ILedTransport` interface. Format: `41 64 61 <count_hi> <count_lo> <checksum> RGB×N` (36 bytes for 10 LEDs).
- **D-08a:** Leave a TPM2 implementation as a **stub** in `src/led/` so transport can be swapped if Adalight reveals reliability issues during integration. Stub does not need to be functional in Phase 14 — just a class skeleton against `ILedTransport`.
- **D-09:** **JSON over the same serial port** for non-streaming control: `{"bri":N}` for brightness, `{"on":bool}` for power. WLED auto-detects JSON vs binary by first byte. Inter-command gap: 20ms minimum.
- **D-10:** Baud rate **fixed at 115200** (no VRSettings knob). ESP32-C3 USB CDC ignores baud anyway.
- **D-11:** Writer thread driven by **condition variable + 33ms timeout**. RunFrame/pipe handler signals on update for low-latency dispatch; thread falls back to periodic tick if no signal arrives. Minimal CPU when idle.

### Safety + Configuration
- **D-12:** Brightness ceiling is **VRSettings-only**: `backglow.brightness_ceiling` (default `50`, range 1–255). Read at driver init and on VRSettings change. **No pipe command** to change the ceiling — protects against runaway VRChat OSC in later phases.
- **D-13:** `backglow bri <N>` clamps to ceiling and is sent as WLED master brightness via JSON `{"bri":clamped}`. WLED's master then multiplies all RGB.
- **D-14:** **COM port is VRSettings-required**: `backglow.com_port` (string, e.g. `COM5`). No scan, no auto-pick. (VID/PID auto-detect lands in Phase 15.)
- **D-15:** **Degraded startup**: if the COM port is absent/unconfigured/open-fails, log INFO once, mark backglow subsystem `disabled`, and register for hotplug notifications via `RegisterDeviceNotification` so a later plug attempts re-init. Pipe `backglow.*` commands respond `ERR backglow disabled (no port)` while disabled. Driver itself loads normally — backglow failure must never block proximity/IPD work.
- **D-16:** LEDs default **OFF** at driver startup (no auto-enable). All LEDs turn off automatically on driver `Cleanup()` (LHWD-03).

### Spike Plan Structure
- **D-17:** **Plan 14.1 is a hardware-validation spike** before main implementation. Mirrors the v2.0 Phase 10/10.1 spike pattern. Throwaway code in `src/spike/` + findings doc.
- **D-18:** Spike validates four specific things:
  1. **`'v'` round-trip** — open COM port, send `0x76`, parse WLED version reply (proves device identity + framing).
  2. **Adalight 10-LED visible color change** — send all-red, all-green, all-blue frames, watch LEDs (proves end-to-end + catches MagWLED-1 GPIO3/UART conflict immediately).
  3. **Unplug/replug recovery** — observe error codes from active write during disconnect, confirm reconnect path (validates research on `ERROR_OPERATION_ABORTED` etc.).
  4. **JSON `{"bri":50}` over same port** — confirms WLED's first-byte protocol detection works as documented (validates D-09).
- **D-19:** Spike artifacts: code committed under `src/spike/backglow_spike/` (followed by removal in main impl plan), findings written to `14.1-SPIKE-FINDINGS.md`. Main impl rebuilds clean against `ILedTransport`.

### File Layout (locked from research)
- **D-20:** New module: `src/led/`
  - `led_transport.h` — `ILedTransport` interface
  - `wled_serial.h/.cpp` — Adalight + JSON-over-serial implementation
  - `wled_tpm2.h/.cpp` — stub class skeleton (D-08a)
  - `led_controller.h/.cpp` — writer thread, ceiling enforcement, double-buffered state
- **D-21:** `DeviceProvider` modified: own `LedController`, route `backglow ` pipe commands via new `HandleBackglowCommand()`. Pattern matches existing `proximity`/`ipd` prefix routing.
- **D-22:** Compile-time flag `ENABLE_BACKGLOW` wraps all new code (mirrors `ENABLE_IPD_PERSIST` pattern). Default ON for Phase 14 builds.
- **D-23:** `beyond_prox_ctl.exe` extended with `backglow ` command validation. No new CLI executable in Phase 14.

### Claude's Discretion
- Exact thread-shutdown ordering on `Cleanup()` (off frame → join writer thread, vs. join → final off via main thread).
- Specific error code → log message mapping for serial errors.
- Whether the spike uses `FILE_FLAG_OVERLAPPED` from the start or starts synchronous and adds overlapping in main impl.
- Internal LED count constant (`MAX_LEDS = 10`) location — controller header vs. shared config header.
- Pipe response strings (e.g. `OK fill=FF8000 leds=10` exact wording).
- VRSettings section/key namespace formatting (`driver_BeyondProximity` section, key naming).

### Folded Todos
None. The two pending todos (Explore /input/system/click probing; Milestone 2.0 IPD slider) are unrelated to backglow scope.

</decisions>

<canonical_refs>
## Canonical References

**Downstream agents MUST read these before planning or implementing.**

### Project / Roadmap
- `.planning/PROJECT.md` — overall driver architecture, key decisions log
- `.planning/REQUIREMENTS.md` — full v3.0 requirement list with phase mapping (LHWD-01..03, TRNS-01, LCTL-01..05, DIAG-02 in Phase 14)
- `.planning/ROADMAP.md` §"Phase 14: USB Serial Foundation and LED Control" — phase goal, dependencies, success criteria
- `.planning/STATE.md` — carried concerns: MagWLED-1 GPIO3 conflict + ESP32-C3 USB CDC startup framing

### Research (v3.0 milestone)
- `.planning/research/SUMMARY.md` — executive summary, recommended stack, top risks
- `.planning/research/STACK.md` — Adalight protocol byte layout, baud commands, WLED config requirements, oscpp library
- `.planning/research/ARCHITECTURE.md` §1–§7 — full LED subsystem architecture, threading model, file layout, build order, anti-patterns
- `.planning/research/PITFALLS.md` §1–§7 — blocking serial in RunFrame, COM port enumeration fragility, JSON unreliability, brightness/thermal safety, ESP32-C3 disconnect handling, WLED coupling. Note: PITFALLS recommends TPM2 over Adalight — we chose Adalight per D-08 with TPM2 stub fallback per D-08a.

### Existing Codebase Patterns to Mirror
- `src/hid/hid_device.h/.cpp` — background reader thread + atomic state + reconnect loop (the canonical pattern for `LedController` writer thread + `WledSerialTransport` reconnect)
- `src/driver/device_provider.cpp` lines 278 (`RunFrame`), 410 (`HandlePipeCommand`) — extension points for backglow integration
- `src/ctl/main.cpp` — pattern for `beyond_prox_ctl.exe` CLI command validation extension

### External Specs
- WLED Serial Interface: https://kno.wled.ge/interfaces/serial/
- WLED JSON API: https://kno.wled.ge/interfaces/json-api/
- Adalight protocol header layout: https://www.partsnotincluded.com/visualizing-adalight-header-information/
- WLED `wled_serial.cpp` source: https://github.com/Aircoookie/WLED/blob/main/wled00/wled_serial.cpp
- ESP32-C3 USB Serial/JTAG console: https://docs.espressif.com/projects/esp-idf/en/stable/esp32c3/api-guides/usb-serial-jtag-console.html
- MagWLED-1 product page: https://magwled.com/pages/about-magwled-1
- COM port `\\\\.\\COMx` prefix requirement: https://support.microsoft.com/en-us/topic/howto-specify-serial-ports-larger-than-com9-db9078a5-b7b6-bf00-240f-f749ebfd913e

</canonical_refs>

<code_context>
## Existing Code Insights

### Reusable Assets
- `HidDevice` (`src/hid/hid_device.cpp`, 307 LOC): canonical implementation of the background-thread + atomic-state + reconnect pattern. `LedController` writer thread and `WledSerialTransport` reconnect loop should mirror its state machine (Disconnected → Connecting → Connected → Error → Disconnected) and atomic connection-state field.
- `DeviceProvider::HandlePipeCommand` (line 410): existing prefix-routing pattern (`proximity`/`ipd`/`status`/`load_lh_config`). New `backglow ` branch slots in identically.
- `DeviceProvider::RunFrame` (line 278): pipe polling loop. No new polling needed for backglow — pipe handler signals writer thread directly.
- `beyond_prox_ctl.exe` (`src/ctl/main.cpp`, 109 LOC): pattern for adding `backglow` command validation alongside existing `proximity`/`ipd`/`status`.
- `ENABLE_IPD_PERSIST` compile flag pattern: direct model for `ENABLE_BACKGLOW`.

### Established Patterns
- **Pattern 1 (atomic state + background thread):** RunFrame reads atomics; background thread does blocking I/O. No locks on hot path. Used by `HidDevice`.
- **Pattern 2 (pipe command prefix routing):** `strncmp` on first token routes to handler. Used for `proximity`, `ipd`.
- **Pattern 3 (compile-time feature flag):** `#ifdef ENABLE_xxx` wraps optional subsystems. Used by IPD persistence.
- **Pattern 4 (reconnect in background thread):** Disconnect detection + periodic re-enumeration + state machine, never propagating errors to RunFrame. Used by HID reader.

### Integration Points
- `DeviceProvider`: gains `m_pLedController` member, `HandleBackglowCommand()`, calls `m_pLedController->ShutdownAllOff()` in `Cleanup()`.
- `CMakeLists.txt`: add `src/led/` sources to driver target; conditionally compile when `ENABLE_BACKGLOW` is ON; link `setupapi.lib` (for Phase 15 hotplug; Phase 14 only needs `RegisterDeviceNotification` which is in `user32.lib`).
- VRSettings keys live under existing `driver_BeyondProximity` section.

### USB Coexistence
- HMD = HID device (VID `0x35BD` / PID `0x0101`) via HIDAPI. ESP32-C3 = USB CDC (VID `0x303A` / PID `0x1001`) via Win32 serial API. Different device classes, different physical hardware. No contention with `BeyondHID.exe` companion app or the proximity reader thread.

</code_context>

<specifics>
## Specific Ideas

### Camera stream available for agent-in-the-loop UAT
The user can enable a live camera stream during this milestone so agents can visually verify physical LED behavior:

**Stream URL (view-only):** https://vdo.ninja/?view=JYMW97gq

**When to use:** Plan 14.1 spike step "Adalight 10-LED visible color change" is the prime case — the agent can ask the user to enable the stream, then watch the LEDs change color directly during the test. Subsequent UAT for `backglow fill`/`set` pipe commands also benefits.

**How to use:** Ask the user to enable the stream before running a hardware-visible test. Don't assume it's always on. Fetch the stream when needed.

### Polymorphic `fill` (D-03) — design intent
The user explicitly asked for `fill` to handle both uniform (`fill FF8000`) and full-frame (`fill <hex0> <hex1> ... <hex9>`) in a single command. Reasoning: VRChat OSC bridge in Phase 16 will likely push full per-LED frames, and a single command name keeps the pipe surface compact. The parser must validate count (1 or LED_COUNT) and respond `ERR fill expects 1 or 10 hex values` on anything else.

### Spike-first cadence
The user accepted Plan 14.1 as a separate spike plan that will produce throwaway code + findings doc, mirroring v2.0 Phase 10/10.1. This cadence has already shipped successfully — repeat it.

</specifics>

<deferred>
## Deferred Ideas

- **`backglow status` pipe command** → Phase 15 (DIAG-01).
- **VID/PID auto-detect for COM port** → Phase 15 (LHWD-04).
- **WiFi/DDP fallback transport (`WledDdpTransport`)** → Phase 15 (TRNS-02, TRNS-03). The `ILedTransport` interface in Phase 14 must be designed so a second concrete implementation can plug in without refactoring.
- **TPM2 functional implementation** → only if Adalight reveals reliability issues during integration (D-08a stub deferred indefinitely otherwise).
- **Configurable baud rate** → out of scope; no current need (D-10).
- **Per-LED brightness control** → marked out-of-scope at REQUIREMENTS level (WLED `seg.i` GitHub #2549).
- **Idle-timeout / watchdog auto-off from MCU side** → out of scope for Phase 14; driver `Cleanup()` is the only off trigger.
- **VRChat OSC bridge daemon (`beyond_backglow_ctl.exe`)** → Phase 16 (VRCH-01, VRCH-02).
- **Avatar prefab + reference world** → Phase 17 (VRCH-03, VRCH-04).

### Reviewed Todos (not folded)
- `2026-03-26-explore-input-system-click-handle-probing-on-hmd.md` — unrelated to backglow scope; remains in v2.0 follow-up backlog.
- `2026-03-23-milestone-2-0-live-ipd-change-and-steamvr-slider.md` — already shipped in v2.0; pending todo file is stale and should be cleaned up separately.

</deferred>

---

*Phase: 14-usb-serial-foundation-and-led-control*
*Context gathered: 2026-04-18*
