# Phase 15: WiFi/DDP Fallback and Transport Selection - Context

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

<domain>
## Phase Boundary

Finish the backglow transport story. Add a second `ILedTransport` implementation (`WledDdpTransport`) that speaks WLED DDP over UDP, let the user pick `usb | ddp | auto` via VRSettings, auto-detect the ESP32-C3 COM port by VID/PID, and add the previously deferred `backglow status` command.

Phase 15 covers: `LHWD-04` (VID/PID auto-detect + disconnect/reconnect), `TRNS-02` (DDP transport to WLED on port 4048), `TRNS-03` (transport selection via VRSettings), `DIAG-01` (`backglow status` output).

Phase 15 does **NOT** cover: VRChat OSC bridge daemon (Phase 16), avatar prefab / reference world (Phase 17), mDNS WLED discovery, runtime transport hot-swap, TPM2 functional implementation.

</domain>

<decisions>
## Implementation Decisions

### Transport Selection
- **D-01:** VRSettings `backglow.transport` ∈ `usb | ddp | auto`. Explicit values are strict (chosen transport fails → degraded-disabled). `auto` tries USB first; on USB `Open()` failure, falls through to DDP. Default value TBD by planner (likely `usb` to preserve Phase 14 behavior).
- **D-02:** Transport is chosen at driver init only. Restart required to change. No runtime pipe command, no VRSettings hot-reload. Matches Phase 14 D-14 (`backglow.com_port`) behavior.
- **D-03:** When the chosen transport cannot be opened, enter the same degraded-disabled state defined by Phase 14 D-15: log INFO once, mark backglow disabled, pipe `backglow.*` commands reply `ERR backglow disabled (<reason>)`. Driver loads normally — backglow failure must never block proximity/IPD.
- **D-04:** `auto` fallback fires once at startup only. Mid-session USB drop stays on USB reconnect loop (Phase 14 pattern) — does not migrate to DDP. Intent: "user unplugged USB and moved to WiFi before session" scenario, not live failover.

### DDP Target Configuration
- **D-05:** VRSettings `backglow.ddp_host` = IPv4 string (e.g. `192.168.1.42`). No DNS resolution, no mDNS discovery. Keeps Phase 14 "explicit user config" philosophy.
- **D-06:** DDP port fixed at **4048** (WLED default, per `TRNS-02`). No VRSettings knob.
- **D-07:** "Connected" for DDP = `Open()` succeeds in reaching host: bind UDP socket + one-shot HTTP `GET /json/info` to the target within bounded timeout (planner picks timeout; ~500 ms suggested). Response received = connected. No periodic heartbeat — after `Open()`, DDP frames are fire-and-forget UDP.
- **D-08:** The same `GET /json/info` at `Open()` also yields the WLED-reported strip length (LED count). `LedController` surface remains locked at `kBackglowMaxLeds=10` (Phase 14). DDP encoder uses the returned count for wire framing. If returned count ≠ 10, log WARN and clamp encoding to 10.

### VID/PID Auto-Detect (USB transport)
- **D-09:** COM port resolution when transport resolves to USB:
  1. If `backglow.com_port` is set and non-empty → try it first (Phase 14 behavior preserved).
  2. If that `Open()` fails **or** the key is blank/missing → fall through to `SetupDiGetClassDevs(GUID_DEVINTERFACE_COMPORT)` scan, matching VID `0x303A` / PID `0x1001`.
  3. Scan yields zero matches → degraded-disabled (D-03).
- **D-10:** Scan multi-match policy: lowest COM number wins. Log WARN listing all matched ports and the hint that `backglow.com_port` can pin a specific one.
- **D-11:** Scan fires at driver init. `DBT_DEVICEARRIVAL` hotplug notification (already registered in Phase 14) re-runs the scan **if and only if** backglow is currently disabled. Already-connected state is not disturbed.
- **D-12:** TPM2 stub (Phase 14 D-08a) stays as a class skeleton in Phase 15. No expansion, no removal.

### `backglow status` (DIAG-01)
- **D-13:** Output format = **multi-line human-readable**, aligned columns. Breaks the Phase 14 single-line `OK ...` convention for this command only. CLI (`beyond_prox_ctl.exe`) reads the pipe response until EOF / bounded timeout rather than one line.
- **D-14:** Fields included: `transport`, `conn`, `port` (when transport=usb) **or** `host` (when transport=ddp), `bri`, `ceiling`, `leds`. **Not included:** `color` (no LedController tracking of last-sent color in Phase 15).
- **D-15:** Error context: when `conn` is `disabled` or `reconnecting`, append an `err:` line with a short reason token (e.g. `err: ERROR_FILE_NOT_FOUND`, `err: no_com_port_configured`, `err: ddp_probe_timeout`, `err: scan_no_match`). Omitted on healthy connections.

### File Layout (delta on Phase 14)
- **D-16:** New files in `src/led/`:
  - `wled_ddp.h/.cpp` — `WledDdpTransport : public ILedTransport`. Implements DDP protocol header + payload chunking per WLED DDP docs. Uses Winsock UDP socket + WinHTTP (or WinINet) for the one-shot `/json/info` probe at `Open()`.
- **D-17:** New helper in `src/led/` or `src/driver/`:
  - `com_port_scan.h/.cpp` (or inline in `wled_serial.cpp`) — SetupDi-based VID/PID enumeration. `setupapi.lib` now linked (already anticipated in Phase 14 integration points).
- **D-18:** `DeviceProvider` changes:
  - Read `backglow.transport` + `backglow.ddp_host` at init alongside existing `backglow.com_port` + `backglow.brightness_ceiling`.
  - Factory logic to build correct `ILedTransport` concrete type (with USB scan fallback per D-09, `auto` fallback per D-04).
  - `HandleBackglowCommand()` gains `status` subcommand producing multi-line output per D-13/D-14/D-15.
- **D-19:** `beyond_prox_ctl.exe` gains `backglow status` validation and adjusts pipe read loop to handle multi-line response.

### Claude's Discretion
- Exact default value of `backglow.transport` (likely `usb` to preserve Phase 14 behavior; planner decides).
- DDP probe timeout (500 ms suggested, planner tunes).
- Order and exact formatting of `status` fields (only the field *set* and format *family* are locked).
- Whether `/json/info` probe uses WinHTTP, WinINet, or raw socket + minimal HTTP/1.1 write.
- DDP payload chunking strategy (WLED DDP allows data fragmentation; 10 LEDs × 3 bytes fits one packet easily).
- Whether `com_port_scan` is a standalone helper file or inlined into `wled_serial.cpp`.
- UAT scaffolding for WiFi-mode testing (SMOKE doc analogous to 14-SMOKE.md).
- VRSettings key naming: `backglow.transport` vs `backglow.transport_mode` vs similar.

### Folded Todos
None.

</decisions>

<canonical_refs>
## Canonical References

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

### Project / Roadmap / Requirements
- `.planning/PROJECT.md` — driver architecture, key decisions log
- `.planning/REQUIREMENTS.md` — `LHWD-04`, `TRNS-02`, `TRNS-03`, `DIAG-01` mapped to Phase 15
- `.planning/ROADMAP.md` §"Phase 15: WiFi/DDP Fallback and Transport Selection" — goal + success criteria
- `.planning/STATE.md` — current progress, outstanding concerns

### Prior Phase Context (Phase 14 — USB Serial Foundation)
- `.planning/phases/14-usb-serial-foundation-and-led-control/14-CONTEXT.md` — all Phase 14 decisions; **D-01..D-23** remain in force for Phase 15 (command surface, hex format, brightness ceiling, writer thread, etc.)
- `.planning/phases/14-usb-serial-foundation-and-led-control/14.1-SPIKE-FINDINGS.md` — validated Adalight + serial behavior on MagWLED-1
- `.planning/phases/14-usb-serial-foundation-and-led-control/14-SMOKE.md` — USB hardware UAT format; Phase 15 should produce an analogous `15-SMOKE.md` covering WiFi, VID/PID scan, `status` output, and transport selection
- `.planning/phases/14-usb-serial-foundation-and-led-control/14-RESEARCH.md` — Adalight framing, COMMTIMEOUTS, reconnect pattern
- `.planning/phases/14-usb-serial-foundation-and-led-control/14-VERIFICATION.md` — completion evidence

### v3.0 Milestone Research
- `.planning/research/SUMMARY.md` — recommended stack + top risks
- `.planning/research/ARCHITECTURE.md` §1–§7 — LED subsystem architecture, threading, build order, anti-patterns
- `.planning/research/STACK.md` — WLED DDP protocol notes, JSON API format
- `.planning/research/PITFALLS.md` — reconnect handling, WiFi stability caveats, brightness/thermal safety

### Existing Code (Phase 14 artefacts to extend)
- `src/led/led_transport.h` — `ILedTransport` interface (stable; second impl plugs in here)
- `src/led/led_controller.h/.cpp` — writer thread, brightness ceiling, state machine (unchanged; only owns transport)
- `src/led/wled_serial.h/.cpp` — USB transport reference implementation
- `src/led/wled_tpm2.h/.cpp` — TPM2 stub (untouched in Phase 15, D-12)
- `src/driver/device_provider.cpp` — VRSettings reads, `HandleBackglowCommand()`, hotplug `RegisterDeviceNotification` (already wired)
- `src/ctl/main.cpp` — pipe client CLI pattern (`backglow status` extension lands here)

### External Specs
- WLED DDP interface: https://kno.wled.ge/interfaces/ddp/
- WLED JSON API (`/json/info` for probe + strip length): https://kno.wled.ge/interfaces/json-api/
- DDP protocol spec (3waylabs): http://www.3waylabs.com/ddp/
- WLED UDP realtime overview: https://kno.wled.ge/interfaces/udp-realtime/
- Windows `SetupDiGetClassDevs` + `GUID_DEVINTERFACE_COMPORT`: https://learn.microsoft.com/en-us/windows/win32/api/setupapi/nf-setupapi-setupdigetclassdevsa
- Espressif USB CDC VID/PID reference (ESP32-C3 0x303A/0x1001): https://docs.espressif.com/projects/esp-idf/en/stable/esp32c3/api-guides/usb-serial-jtag-console.html
- `RegisterDeviceNotification` + `DBT_DEVICEARRIVAL`: https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-registerdevicenotificationa

</canonical_refs>

<code_context>
## Existing Code Insights

### Reusable Assets
- `ILedTransport` (Phase 14, `src/led/led_transport.h`): stable interface. `WledDdpTransport` implements the same 5 methods (`Open`, `Close`, `IsOpen`, `SendRgbFrame`, `SendBrightness`, `SendPower`, `LastErrorCode`) — no interface changes required.
- `LedController` (`src/led/led_controller.h/.cpp`): transport-agnostic. Accepts `std::unique_ptr<ILedTransport>` at construction. No changes needed for DDP — writer thread, brightness ceiling, shutdown-off, state machine all transport-independent.
- `WledSerialTransport::WriteAllWithTimeout` + `Close()` error-invalidation pattern: DDP transport mirrors the "send-fail → invalidate handle → next writer iteration reopens" contract (interface-level LedController semantic, carries forward).
- `DeviceProvider::HandleBackglowCommand()` (Phase 14): prefix-routing slot for `status` subcommand.
- `DBT_DEVICEARRIVAL` notification handler (Phase 14 D-15): already registered; re-scan hook lands in this handler (D-11).

### Established Patterns
- **Pattern 1 (atomic state + background thread):** LedController writer thread already uses this; WledDdpTransport inherits the pattern for free — `Send*` methods called only from the writer thread.
- **Pattern 2 (pipe command prefix routing):** `backglow status` slots in alongside `fill | set | bri | off | on`.
- **Pattern 3 (compile-time feature flag):** `ENABLE_BACKGLOW` wraps all new Phase 15 code (mirrors Phase 14 D-22).
- **Pattern 4 (reconnect in background thread):** Unchanged for USB. For DDP, Phase 15 treats `Open()` as one-shot at startup (D-07) — no mid-session reconnect loop.
- **Pattern 5 (VRSettings-driven config):** New keys `backglow.transport`, `backglow.ddp_host` read alongside existing `backglow.com_port`, `backglow.brightness_ceiling`.

### Integration Points
- `DeviceProvider::ProvideDriver` / init path: gains transport-factory branch before constructing `LedController`.
- `CMakeLists.txt`: link `ws2_32.lib` (Winsock UDP) and `winhttp.lib` (or `wininet.lib`) for DDP transport's HTTP probe. `setupapi.lib` (anticipated in Phase 14 §Integration Points) now actually linked for COM scan.
- `beyond_prox_ctl.exe` pipe read loop: extend to consume multi-line response for `status` (other commands stay single-line OK/ERR).

</code_context>

<specifics>
## Specific Ideas

### `status` sample output (user-selected preview, with D-14 tweak dropping `color`)
```
transport: usb
conn:      open
port:      COM5
bri:       50
ceiling:   50
leds:      10
```

For DDP transport the `port:` line becomes `host:`:
```
transport: ddp
conn:      open
host:      192.168.1.42
bri:       50
ceiling:   50
leds:      10
```

Disabled/reconnecting example:
```
transport: usb
conn:      disabled
err:       no_com_port_configured
ceiling:   50
```

### Manual-override-with-scan-fallback (D-09)
User's explicit design intent (Q1 of Area 3): manual `backglow.com_port` wins first, BUT if that `Open()` fails, fall through to VID/PID scan. Preserves Phase 14 pinned-config workflow while recovering automatically when Windows reassigns COM numbers. Planner should make sure the failure path is `Open() failed` — not "port string looked wrong" — and that the fall-through is logged clearly.

### `auto` is a migration convenience, not live failover (D-04)
"I moved my headset off my desk and the ESP32 is on WiFi now" startup scenario. Not "USB cable just popped out, keep the party going over WiFi". The latter is mid-session complexity with edge cases (half-written frames, dual-transport state) that the user explicitly rejected in discussion.

### Camera stream for agent-in-the-loop UAT (carried from Phase 14)
Stream URL: https://vdo.ninja/?view=JYMW97gq — user can enable a live view of the LEDs during WiFi-mode smoke tests. Ask before assuming it's on. Especially useful for validating that DDP + USB produce visually identical output when the same `fill` is issued on each transport.

</specifics>

<deferred>
## Deferred Ideas

- **Runtime transport switching** via VRSettings hot-reload or `backglow transport <usb|ddp>` pipe command — rejected in D-02. Revisit if UX feedback demands it post-ship.
- **Mid-session USB→DDP failover** on cable drop — rejected in D-04. Too much state-machine complexity for a marginal scenario.
- **mDNS / Bonjour WLED discovery** — rejected in D-05 in favor of explicit IP. Candidate for a future "zero-config" ergonomics phase.
- **DDP heartbeat / periodic reachability probe** — rejected in D-07. Only revisit if stability issues surface (user is using a flaky AP).
- **`color` field in `status`** — dropped in D-14. Would require LedController to track last-sent frame; deferred until a real consumer needs it.
- **TPM2 functional implementation** (carrying Phase 14 D-08a) — still gated on Adalight reliability issues that have not materialized.
- **Per-transport brightness ceiling** — current design shares ceiling across transports; sufficient for v3.0 hardware.
- **Configurable DDP port** — rejected in D-06; fixed 4048.
- **VRChat OSC bridge daemon** → 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` — matched on keyword "driver" but scope is proximity/input-component exploration unrelated to LED transport. Stays in v2.0 follow-up backlog.
- `2026-03-23-milestone-2-0-live-ipd-change-and-steamvr-slider.md` — already shipped in v2.0; stale pending file. Cleanup owned separately, not Phase 15.

</deferred>

---

*Phase: 15-wifi-ddp-fallback-and-transport-selection*
*Context gathered: 2026-04-19*
