---
phase: 14-usb-serial-foundation-and-led-control
verified: 2026-04-19T12:00:00Z
status: passed
score: 15/15 must-haves verified
overrides_applied: 0
---

# Phase 14: USB Serial Foundation and LED Control — Verification Report

**Phase Goal:** Ship a USB-first backglow driver subsystem that accepts `backglow` pipe commands from a SteamVR driver and drives a MagWLED-1 ESP32-C3 strip via Win32 overlapped serial Adalight + JSON, with a brightness ceiling safety cap, graceful shutdown-off on driver unload, and degraded-mode hotplug re-init. All 5 ROADMAP-specified success criteria must be verifiable on real MagWLED-1 hardware.
**Verified:** 2026-04-19
**Status:** PASSED
**Re-verification:** No — initial verification

---

## Goal Achievement

### Observable Truths

Derived from the 5 ROADMAP.md success criteria plus the plan-level must-haves across Plans 14-01, 14-02, and 14-03.

| # | Truth | Status | Evidence |
|---|-------|--------|----------|
| 1 | User can run `backglow fill FF0000` and see all 10 LEDs turn red on physical hardware | VERIFIED | 14-SMOKE.md Criterion 1: `OK fill=FF0000 leds=10`, WLED `leds.pwr=303 mA`, `live=True`. Green and blue also PASS. |
| 2 | Brightness above ceiling (default 50/255) is silently clamped; LEDs never exceed the ceiling | VERIFIED | 14-SMOKE.md Criterion 2: `backglow bri 255` → `OK bri=50 ceiling=50`; physical strip brightness unchanged from ceiling-50 white fill. `ApplyCeilingInPlace` in `led_controller.cpp:166` + `HandleBackglowCommand` clamp in `device_provider.cpp:1460-1465`. |
| 3 | LEDs turn off automatically when SteamVR session ends or driver unloads, with no residual glow | VERIFIED | 14-SMOKE.md Criterion 3: graceful `taskkill /PID <vrmonitor>` → WLED `leds.pwr=0`, `live=False`. `ShutdownAllOff` called in `Cleanup()` at `device_provider.cpp:142`. |
| 4 | User can address individual LEDs by index; only the targeted LED changes | VERIFIED | 14-SMOKE.md Criterion 4: `backglow set 3 00FF00` → `OK set idx=3 rgb=00FF00`; only LED 3 lit green; idx=0 (red) and idx=9 (blue) also confirmed. `QueueLedSet` in `led_controller.cpp:210`. |
| 5 | Backglow settings (brightness ceiling, COM port override) are readable from steamvr.vrsettings | VERIFIED | 14-SMOKE.md Criterion 5: driver log shows `Backglow: ceiling=50 com_port='COM11'` + `Backglow: online on COM11`. VRSettings read in `InitBackglow` at `device_provider.cpp:1299-1310`. |
| 6 | `ILedTransport` interface exists with correct abstract surface; pluggable for Phase 15 DDP | VERIFIED | `src/led/led_transport.h` (44 lines): `SendRgbFrame`, `SendBrightness`, `SendPower`, `Open`, `Close`, `IsOpen`, `LastErrorCode`, `kBackglowMaxLeds=10`. |
| 7 | `WledSerialTransport` sends byte-for-byte-correct Adalight frames and JSON lines | VERIFIED | `src/led/wled_serial.cpp` (257 lines): header `'A' 'd' 'a'` + `hi ^ lo ^ 0x55` checksum at lines 222-225; JSON `{"bri":N}\n` at line 244; `{"on":true/false}\n` at line 254. Double-send patch (WLED 0.15 quirk) at lines 232-234. |
| 8 | `WledTpm2Transport` is a compiling stub satisfying `ILedTransport` (D-08a) | VERIFIED | `src/led/wled_tpm2.h` (21 lines): all methods return `false`/`0`; `src/led/wled_tpm2.cpp` (14 lines): compiles as minimal TU. |
| 9 | `LedController` owns a dedicated writer thread driven by cv with 33 ms timeout; callers never call serial I/O | VERIFIED | `led_controller.cpp:78`: `m_cv.wait_for(lk, std::chrono::milliseconds(33), ...)`. Queue methods stage under mutex and `notify_one` — no direct `WriteFile` anywhere in `QueueFrame`, `QueueLedSet`, etc. |
| 10 | Brightness ceiling applied in `LedController` BEFORE any transport write (defense-in-depth) | VERIFIED | `led_controller.cpp:166`: `ApplyCeilingInPlace(frame, kBackglowMaxLeds * 3, m_ceiling.load())` called before `m_transport->SendRgbFrame(frame, kBackglowMaxLeds)` on line 168. |
| 11 | `LedController::ShutdownAllOff()` sends synchronous all-black frame + `{"on":false}` before writer thread is joined | VERIFIED | `led_controller.cpp:261-282`: stops+joins writer, then calls `SendRgbFrame(black, ...)` and `SendPower(false)` on caller thread. Called from `Cleanup()` before `m_pLedController.reset()`. |
| 12 | `ENABLE_BACKGLOW` compile flag wraps all new backglow code; driver builds with and without it | VERIFIED | `CMakeLists.txt` lines 72-87: `option(ENABLE_BACKGLOW ... ON)` + `if(ENABLE_BACKGLOW)` block with all 7 `src/led/` files. All `device_provider.h/cpp` backglow code under `#ifdef ENABLE_BACKGLOW`. Both builds confirmed PASS in 14-02-SUMMARY and 14-03-SUMMARY. |
| 13 | COM port name validated against `^COM[0-9]{1,3}$` before `\\.\` concatenation (T-14.2-01) | VERIFIED | `wled_serial.cpp:42-51`: `IsValidComName` checks 4-6 char length, "COM" prefix, then digits. Used in `Open()` before `CreateFileA`. |
| 14 | Degraded-mode: missing/empty/invalid COM port logs INFO once, driver continues to load, backglow commands return `ERR backglow disabled (no port)` | VERIFIED | 14-SMOKE.md D-15: COM99 test — driver log shows `Backglow: disabled (port 'COM99' unavailable); registering hotplug watcher`; pipe returned `ERR backglow disabled (no port)`. HID/IPD paths unaffected. |
| 15 | `RegisterDeviceNotificationA` with `GUID_DEVINTERFACE_COMPORT` + `HWND_MESSAGE` + 500 ms debounce arms hotplug re-init | VERIFIED | `device_provider.cpp:1524`: `HWND_MESSAGE` window creation; line 1536: `RegisterDeviceNotificationA` with `GUID_DEVINTERFACE_COMPORT`; line 1570: `Sleep(500)` debounce in `OnHotplugArrival`. |

**Score:** 15/15 truths verified

---

## Required Artifacts

| Artifact | Expected | Status | Details |
|----------|----------|--------|---------|
| `src/led/led_transport.h` | ILedTransport abstract interface | VERIFIED | 44 lines; `SendRgbFrame`, `SendBrightness`, `SendPower`, `Open`, `Close`, `IsOpen`, `LastErrorCode`, `kBackglowMaxLeds=10` |
| `src/led/wled_serial.h` | WledSerialTransport class declaration | VERIFIED | 52 lines; inherits `ILedTransport`; `HANDLE m_hPort`, `m_bOpen`, `m_lastError`, `WriteAllWithTimeout` |
| `src/led/wled_serial.cpp` | Adalight encoder + JSON + Win32 overlapped serial | VERIFIED | 257 lines (min 200); `CBR_115200`, `DTR_CONTROL_DISABLE`, `WriteTotalTimeoutConstant=50`, `FILE_FLAG_OVERLAPPED`, double-send quirk patch |
| `src/led/wled_tpm2.h` | WledTpm2Transport stub | VERIFIED | 21 lines; all overrides return `false`/`0` |
| `src/led/wled_tpm2.cpp` | Stub translation unit | VERIFIED | 14 lines; includes header + comment |
| `src/led/led_controller.h` | LedController class declaration | VERIFIED | 113 lines; `QueueFrame`, `QueueLedSet`, `QueueBrightness`, `QueuePower`, `ShutdownAllOff`, `SetCeiling`, `GetConnectionState`, `m_cv`, `m_transport` |
| `src/led/led_controller.cpp` | Writer thread + ceiling + staged buffer + shutdown | VERIFIED | 282 lines (min 200); `wait_for(33ms)`, `ApplyCeilingInPlace` before `SendRgbFrame`, `ShutdownAllOff` |
| `CMakeLists.txt` | `option(ENABLE_BACKGLOW)` + conditional sources + `user32` | VERIFIED | Lines 72-87; all 7 src/led/ entries; `target_compile_definitions ENABLE_BACKGLOW`; `target_link_libraries user32`; no `setupapi` |
| `src/driver/device_provider.h` | `m_pLedController` + `HandleBackglowCommand` + hotplug members under `#ifdef ENABLE_BACKGLOW` | VERIFIED | `#ifdef ENABLE_BACKGLOW` block at lines 56-81; `m_pLedController`, `m_backglowCeiling`, `m_backglowPort`, hotplug thread/window/notify, `HandleBackglowCommand`; `LedController` forward-declared at line 10 |
| `src/driver/device_provider.cpp` | Init VRSettings read, LedController ownership, pipe routing, Cleanup, hotplug | VERIFIED | 375+ lines added; `InitBackglow` reads `backglow_brightness_ceiling` + `backglow_com_port`; `strncmp(cmd, "backglow ", 9)` branch; `ShutdownAllOff` in `Cleanup`; hotplug WndProc + message-only window |
| `src/ctl/main.cpp` | Extended command validation for `backglow ` prefix + usage text | VERIFIED | 5 validCommand ORs for backglow subcommands; 6 usage lines added; builds clean |
| `.planning/phases/14-usb-serial-foundation-and-led-control/14-SMOKE.md` | Repeatable hardware smoke for 5 ROADMAP criteria | VERIFIED | All 5 criteria + D-03 + D-15 PASS; `Overall Phase 14 verdict: PASS`; 128 lines |
| `.planning/phases/14-usb-serial-foundation-and-led-control/14.1-SPIKE-FINDINGS.md` | Spike verdict + empirical findings | VERIFIED | GO verdict; all 4 D-18 sections filled; carry-forward constraints documented |
| `src/spike/backglow_spike/harness.cpp` | Pure-function assertions for Adalight encoder, hex parser, brightness clamp | VERIFIED | 197 lines (min 80); `0x41`, `0x5C`, `FF8000` patterns present |
| `src/spike/backglow_spike/serial_spike.cpp` | Hardware-exercising CLI for D-18 steps 1-4 | VERIFIED | 433 lines (min 200); `CreateFileA`, `CBR_115200`, `DTR_CONTROL_DISABLE`, `FILE_FLAG_OVERLAPPED`, `WriteTotalTimeoutConstant`, `{"bri":50}` |

---

## Key Link Verification

| From | To | Via | Status | Details |
|------|----|-----|--------|---------|
| `led_controller.cpp` | `wled_serial.cpp` | `m_transport->SendRgbFrame` | WIRED | `led_controller.cpp:168`: `m_transport->SendRgbFrame(frame, kBackglowMaxLeds)` after ceiling; injected `unique_ptr<ILedTransport>` at construction |
| `led_controller.cpp` | `ApplyCeilingInPlace` | Writer thread applies ceiling before `SendRgbFrame` | WIRED | `led_controller.cpp:166-168`: `ApplyCeilingInPlace` then `SendRgbFrame` in sequence |
| `CMakeLists.txt` | `src/led/*.cpp` | `target_sources` guarded by `if(ENABLE_BACKGLOW)` | WIRED | All 7 `src/led/` files listed under `if(ENABLE_BACKGLOW)` block |
| `device_provider.cpp DeviceProvider::Init` | `led_controller.h` | `std::make_unique<LedController>(std::make_unique<WledSerialTransport>(), ceiling)` + `Start(port)` | WIRED | `device_provider.cpp:1317`: `m_pLedController = std::make_unique<LedController>(...)` |
| `device_provider.cpp HandlePipeCommand` | `HandleBackglowCommand` | `strncmp(cmd, "backglow ", 9) == 0` branch | WIRED | `device_provider.cpp:566-569`: `else if (strncmp(cmd, "backglow ", 9) == 0) { HandleBackglowCommand(cmd + 9, ...) }` |
| `device_provider.cpp Cleanup` | `led_controller.cpp ShutdownAllOff` | `m_pLedController->ShutdownAllOff()` before reset | WIRED | `device_provider.cpp:142`: `ShutdownAllOff()` called, then `m_pLedController.reset()` at line 143 |
| `src/ctl/main.cpp` | pipe `\\.\pipe\beyond_proximity_ctl` | `validCommand` ORs for backglow subcommands | WIRED | 5 backglow OR clauses in `validCommand`; usage text documents all forms |

---

## Data-Flow Trace (Level 4)

| Artifact | Data Variable | Source | Produces Real Data | Status |
|----------|---------------|--------|--------------------|--------|
| `LedController::WriterThreadFunc` | `frame[kBackglowMaxLeds * 3]` | `m_staged` buffer populated by `QueueFrame` / `QueueLedSet` (caller-supplied RGB) | Yes — pipe command supplies real hex color, copied to staged buffer, drained by writer | FLOWING |
| `WledSerialTransport::SendRgbFrame` | `buf[36]` Adalight frame | RGB from `LedController` post-ceiling | Yes — real bytes written to COM handle via overlapped `WriteFile` | FLOWING |
| `HandleBackglowCommand` fill response | `OK fill=RRGGBB leds=10` | `ParseHex6` on caller-supplied token from pipe command | Yes — hardware smoke confirmed response and physical LED illumination | FLOWING |
| `InitBackglow` ceiling/port | `m_backglowCeiling`, `m_backglowPort` | `vr::VRSettings()->GetInt32/GetString` from `steamvr.vrsettings` | Yes — smoke confirmed `ceiling=50 com_port='COM11'` in driver log | FLOWING |

---

## Behavioral Spot-Checks

| Behavior | Command | Result | Status |
|----------|---------|--------|--------|
| All 10 LEDs set red via pipe command | `beyond_prox_ctl.exe "backglow fill FF0000"` | `OK fill=FF0000 leds=10`, WLED `leds.pwr=303 mA` | PASS (hardware) |
| Brightness ceiling clamp | `beyond_prox_ctl.exe "backglow bri 255"` with ceiling=50 | `OK bri=50 ceiling=50` | PASS (hardware) |
| Auto-off on driver unload | graceful SteamVR exit after magenta fill | WLED `leds.pwr=0`, `live=False` | PASS (hardware) |
| Per-LED addressing | `beyond_prox_ctl.exe "backglow set 3 00FF00"` | `OK set idx=3 rgb=00FF00`, only LED 3 lit | PASS (hardware) |
| VRSettings read | driver restart with `backglow_com_port=COM11`, `backglow_brightness_ceiling=50` | log: `Backglow: ceiling=50 com_port='COM11'` | PASS (hardware) |
| Degraded mode ERR response | `beyond_prox_ctl.exe "backglow fill FF0000"` with COM99 | `ERR backglow disabled (no port)` | PASS (hardware) |
| Per-LED polymorphic fill | `beyond_prox_ctl.exe "backglow fill FF0000 00FF00 ... 004000"` | `OK fill=per-led leds=10`, rainbow confirmed | PASS (hardware) |
| CLI build produces executable | `cmake --build build --config Release --target beyond_prox_ctl` | DLL + CLI present in `build/driver/BeyondProximity/bin/win64/` | PASS |

---

## Requirements Coverage

| Requirement | Source Plan | Description | Status | Evidence |
|-------------|------------|-------------|--------|----------|
| LHWD-01 | 14-01, 14-02, 14-03 | Driver communicates with WLED ESP32-C3 over USB serial using Adalight binary protocol | SATISFIED | `WledSerialTransport::SendRgbFrame` encodes Adalight frames; hardware smoke PASS (`lm="USB Adalight/TPM2"` confirmed by WLED `/json/info`) |
| LHWD-02 | 14-02, 14-03 | Driver enforces configurable global brightness ceiling (default 50/255) before any LED output | SATISFIED | `ApplyCeilingInPlace` in writer thread before `SendRgbFrame`; ceiling clamped at `GetInt32`; smoke Criterion 2 PASS |
| LHWD-03 | 14-02, 14-03 | All LEDs turn off automatically when driver unloads or VR session ends | SATISFIED | `ShutdownAllOff` → all-black Adalight frame + `{"on":false}`; smoke Criterion 3 PASS (`leds.pwr=0` on exit) |
| TRNS-01 | 14-02 | LED communication uses abstract ILedTransport interface | SATISFIED | `src/led/led_transport.h`; `WledSerialTransport` and `WledTpm2Transport` both implement it; `LedController` owns via `unique_ptr<ILedTransport>` |
| LCTL-01 | 14-03 | User can set all LEDs to a single color via named pipe command | SATISFIED | `backglow fill <hex>` (1 or 10 args); smoke Criterion 1 PASS; note: command prefix is `backglow fill` not `led fill` — plan 14-03 explicitly documents `backglow fill FF0000` as "functional equivalent" to ROADMAP's `led fill 255 0 0` |
| LCTL-02 | 14-03 | User can set individual LEDs by index via named pipe command | SATISFIED | `backglow set <idx> <hex>`; smoke Criterion 4 PASS |
| LCTL-03 | 14-03 | User can set global brightness via named pipe command | SATISFIED | `backglow bri <0..255>`; smoke Criterion 2 clamp PASS |
| LCTL-04 | 14-03 | User can turn LEDs off via named pipe command | SATISFIED | `backglow off` → `QueuePower(false)` → `{"on":false}` sent; tested in smoke |
| LCTL-05 | 14-02 | Driver supports real-time color streaming at up to 30 fps target rate | SATISFIED | `LedController` writer thread with 33 ms cv timeout (~30 fps); WLED `realtimeTimeoutMs=2500 ms` provides ample headroom; spike findings confirmed 30 fps path unblocked |
| DIAG-02 | 14-03 | Backglow settings configurable via steamvr.vrsettings | SATISFIED | `backglow_brightness_ceiling` (int32, default 50, clamped [1..255]) and `backglow_com_port` (string) read at Init; smoke Criterion 5 PASS |

**Note on command naming:** REQUIREMENTS.md LCTL-01..04 use `led fill`/`led set`/`led bri`/`led off` syntax. The implementation uses `backglow fill`/`set`/`bri`/`off` per the Phase 14 design decisions (D-01..D-06 decision log, enforced by 14-03-PLAN `<interfaces>`). Plan 14-03 explicitly marks these as functionally equivalent (`"functional equivalent: backglow fill FF0000"`). The requirements intent — user-callable pipe commands that control LEDs — is fully satisfied.

**Orphaned requirement check:** LHWD-04 (VID/PID auto-detect) is assigned to Phase 15 in REQUIREMENTS.md and was NOT claimed by any Phase 14 plan. Not an orphan — correctly deferred.

---

## Anti-Patterns Found

| File | Line | Pattern | Severity | Impact |
|------|------|---------|----------|--------|
| `src/led/wled_tpm2.h` | 8-14 | All method bodies return `false`/`0` | Info | Intentional stub per D-08a; plan explicitly specifies this as the required behavior; not a gap |
| `src/driver/device_provider.cpp` | 120, 194 | "not yet" comments | Info | Pre-existing code in IPD/HID paths unrelated to Phase 14 backglow work; not introduced by this phase |

No blockers or warnings found. The `WledTpm2Transport` stub is the intended shape per D-08a and TRNS-01 pluggability proof — all stub returns are by design, not placeholder omissions.

---

## Human Verification Required

Hardware UAT was completed agent-driven during Plan 14-03 Task 3, documented in 14-SMOKE.md. All 7 items (5 ROADMAP criteria + D-03 + D-15) are recorded PASS with observed response strings and physical LED confirmation via VDO.Ninja camera stream and WLED `/json/info` telemetry. No additional human verification is required.

---

## Gaps Summary

No gaps. All 15 must-haves are verified. All 10 Phase 14 requirements (LHWD-01, LHWD-02, LHWD-03, TRNS-01, LCTL-01 through LCTL-05, DIAG-02) are satisfied by code in the codebase. Hardware UAT is complete and on record in 14-SMOKE.md. All 3 plans are committed (14 commits from `1788eec` through `09f8f0d`). The driver DLL, CLI, and all LED module files are present and substantive.

One documented carry-forward concern: the WLED first-frame quirk fix (double-send in `SendRgbFrame`) is a workaround rather than an upstream fix. This is logged in 14-SMOKE.md Followups and 14-03-SUMMARY as a Phase 15 action item — it does not affect Phase 14 goal achievement since the workaround is fully functional and hardware-verified.

---

_Verified: 2026-04-19_
_Verifier: Claude (gsd-verifier)_
