# Phase 15 Hardware Smoke

**Hardware:** MagWLED-1 (ESP32-C3, WLED 0.15.0 "Kösen", WS2812B strip, 10 addressed LEDs — Phase 14 configuration preserved)
**USB COM port:** COM11 (confirmed via VID/PID scan log: `scan matched VID_303A&PID_1001 -> COM11`)
**WiFi IPv4:** 192.168.1.88
**Date:** 2026-04-19
**Tester:** Agent-driven UAT (orchestrator drove vrsettings + log capture + WLED `/json/info` reachback; user provided MagWLED-1 IP and deploy confirmation)
**Driver build:** `driver_BeyondProximity.dll` + `beyond_prox_ctl.exe` from the Phase 15 worktree (Wave 2 merge, commits `997d46c` … `06bfefd`). Includes: WledDdpTransport (TRNS-02), ScanForUsbComPorts (LHWD-04), InitBackglow factory with USB scan-fallback and DDP probe (D-09, D-07), AUTO startup-only fallback (D-04), multi-line `backglow status` (DIAG-01, D-13/D-14/D-15), hotplug rescan-when-disabled (D-11), 1024-byte pipe buffers (Pattern 5), WSAStartup/Cleanup pair at driver boundaries, CMake link with `ws2_32` + `setupapi`.

***

## Prerequisites

1. **MagWLED-1 connected via USB AND provisioned onto local WiFi.** Plug the ESP32-C3 into a USB port on the SteamVR host machine. Open the WLED web UI (`http://4.3.2.1` on the device-hosted AP, or `http://<fallback>` if previously provisioned) and join your local WiFi. Read back the DHCP-assigned IPv4 — this is the `backglow_ddp_host` value used throughout this SMOKE. MagWLED-1 MUST remain reachable on both USB and WiFi throughout SMOKE; individual sections toggle which path is active.
2. **Driver + CLI deployed.** Run `scripts/deploy-backglow-dev.ps1` from the repo root to close `vrserver.exe`, copy the freshly-built `driver_BeyondProximity.dll` + `beyond_prox_ctl.exe` into the SteamVR external-driver folder (`C:\Program Files (x86)\Steam\steamapps\common\Bigscreen Beyond Driver\bin\BeyondProximity\bin\win64\`).
3. **`steamvr.vrsettings` baseline** under the `"driver_BeyondProximity"` section. Each criterion/observation below mutates one or more of these four keys; restart SteamVR after each edit for the driver to pick up the new value.
   ```json
   "driver_BeyondProximity": {
     "backglow_brightness_ceiling": 50,
     "backglow_com_port": "",
     "backglow_transport": "usb",
     "backglow_ddp_host": ""
   }
   ```
4. **VDO.Ninja camera stream** — `https://vdo.ninja/?view=JYMW97gq`. Useful when the HMD LEDs are not directly in the tester's line of sight (e.g., for remote agent-driven UAT). **ASK the user before relying on this stream** — it requires the user to actively enable it on the camera side per the standing memory note.
5. **WLED device-side state.** Verify via the WLED web UI or `http://<IP>/json/info`:
   - LED count: `count = 10`
   - Segment 0 length: `seg[0].len = 10`
   - Realtime mix override enabled: `if.live.mso = true`
   - (Phase 14 baseline; Phase 15 must not regress this.)

**CLI invocation pattern (used throughout):**
```
C:\Program Files (x86)\Steam\steamapps\common\Bigscreen Beyond Driver\bin\BeyondProximity\bin\win64\beyond_prox_ctl.exe "<command>"
```

**Driver log tail (used throughout):**
```
C:\Program Files (x86)\Steam\logs\vrserver.txt
```
Grep token: `Backglow:` — every InitBackglow / hotplug / status-impacting log line is prefixed with this literal.

***

## ROADMAP.md Phase 15 Success Criteria

### Criterion 1 (TRNS-02): With ESP32-C3 on WiFi (USB unplugged), `led fill` commands still drive the LEDs over DDP/UDP

**Setup:**
- Edit `steamvr.vrsettings`:
  ```json
  "backglow_transport": "ddp",
  "backglow_ddp_host": "<MagWLED IPv4>",
  "backglow_com_port": ""
  ```
- Physically UNPLUG the USB cable from MagWLED-1 so DDP is the only viable transport.
- Restart SteamVR fully (close `vrmonitor.exe` first, relaunch).

**Procedure:**
1. Tail `vrserver.txt` for `Backglow:` lines after restart. Expected:
   ```
   Backglow: ceiling=50 transport=ddp ddp_host='<IP>' com_port=''
   Backglow: online via DDP on <IP>
   ```
2. Run a fill per color:
   ```
   beyond_prox_ctl.exe "backglow fill FF0000"
   beyond_prox_ctl.exe "backglow fill 00FF00"
   beyond_prox_ctl.exe "backglow fill 0000FF"
   beyond_prox_ctl.exe "backglow fill FFFFFF"
   ```
   Expected response (each): `OK fill=<HEX> leds=10`.
3. Visually confirm all 10 LEDs display the requested color (direct sight, or camera stream).
4. If Windows Firewall prompts for `vrserver.exe` on outbound UDP 4048, click **Allow** — this is Pitfall 5 expected on first DDP cold-start. Record whether the prompt appeared in Followups.

**Observed:**
- Init log lines (vrserver.txt, 03:41:17 / 03:41:18):
  ```
  BeyondProximity: Backglow: transport=ddp ddp_host='192.168.1.88' ceiling=50 com_port=''
  BeyondProximity: Backglow: online via DDP on 192.168.1.88
  ```
- Fill commands all returned `OK fill=<HEX> leds=10`.
- WLED `/json/info` after each fill: `live=True, mode=DDP, lip=192.168.1.81` (driver source IP), `leds.pwr` ~121-123mA.
- USB stayed plugged (MagWLED-1 power source); transport=ddp forced DDP path regardless. Functional TRNS-02 validation preserved.
- No Windows Firewall prompt — UDP/4048 already allowed from prior session.
- Camera angle hides LED strip (mounted on top of headset shell); verification done via WLED `/json/info` reachback.

**Verdict:** PASS

***

### Criterion 2 (LHWD-04): Driver auto-detects the ESP32-C3 COM port by VID/PID without manual configuration

**Setup:**
- Edit `steamvr.vrsettings`:
  ```json
  "backglow_transport": "usb",
  "backglow_com_port": "",
  "backglow_ddp_host": ""
  ```
- Physically PLUG USB to MagWLED-1.
- Restart SteamVR.

**Procedure:**
1. Tail `vrserver.txt`. Expected log lines:
   ```
   Backglow: ceiling=50 transport=usb com_port=''
   Backglow: scan matched VID_303A&PID_1001 -> COM<n>
   Backglow: online via USB on COM<n>
   ```
2. Run:
   ```
   beyond_prox_ctl.exe "backglow fill 00FF00"
   ```
   Expected response: `OK fill=00FF00 leds=10`; LEDs visibly green.

**Observed:**
- Init log lines (vrserver.txt, 03:47:16):
  ```
  BeyondProximity: Backglow: transport=usb ddp_host='' ceiling=50 com_port=''
  BeyondProximity: Backglow: scan matched VID_303A&PID_1001 -> COM11
  BeyondProximity: Backglow: online via USB on COM11
  ```
- `backglow fill 00FF00` returned `OK fill=00FF00 leds=10`.
- `backglow status` confirmed: `transport: usb / conn: open / port: COM11 / bri: 50 / ceiling: 50 / leds: 10`.
- WLED `/json/info` showed `live=False` (USB uses TPM2 serial, not DDP realtime — expected).
- VID 0x303A / PID 0x1001 scan succeeded with empty `backglow_com_port` config — LHWD-04 verified.

**Verdict:** PASS

***

### Criterion 3 (TRNS-03): User can select transport (USB / DDP / auto) via `steamvr.vrsettings` and the driver uses the configured transport

Four sub-setups A/B/C/D. For each: edit VRSettings → restart SteamVR → capture log line → run fill → confirm LEDs.

**Setup A — `backglow_transport=usb` with USB plugged:**
```json
"backglow_transport": "usb",
"backglow_com_port": "",
"backglow_ddp_host": "<MagWLED IPv4>"
```
Expected log: `Backglow: online via USB on COM<n>`.
Fill cmd: `beyond_prox_ctl.exe "backglow fill FF0000"` → red.

**Setup B — `backglow_transport=ddp`, USB unplugged:**
```json
"backglow_transport": "ddp",
"backglow_com_port": "",
"backglow_ddp_host": "<MagWLED IPv4>"
```
Expected log: `Backglow: online via DDP on <IP>`.
Fill cmd: `beyond_prox_ctl.exe "backglow fill 00FF00"` → green.

**Setup C — `backglow_transport=auto`, USB UNPLUGGED at SteamVR start** (D-04 tryUsb fails → tryDdp succeeds):
```json
"backglow_transport": "auto",
"backglow_com_port": "",
"backglow_ddp_host": "<MagWLED IPv4>"
```
Expected log sequence:
```
Backglow: ceiling=50 transport=auto ddp_host='<IP>'
Backglow: USB probe failed (reason=scan_no_match); trying DDP
Backglow: online via DDP on <IP>
```
Fill cmd: `beyond_prox_ctl.exe "backglow fill 0000FF"` → blue.

**Setup D — `backglow_transport=auto`, USB PLUGGED at SteamVR start** (D-04 tryUsb succeeds; DDP never tried):
```json
"backglow_transport": "auto",
"backglow_com_port": "",
"backglow_ddp_host": "<MagWLED IPv4>"
```
Expected log (no DDP mention): `Backglow: online via USB on COM<n>`.
Fill cmd: `beyond_prox_ctl.exe "backglow fill FFFFFF"` → white.

**Observed:**

Setup A (usb+plugged): PASS — validated via Criterion 2 above. Same vrsettings (`transport=usb, com_port=""`), same expected log lines (`scan matched VID_303A&PID_1001 -> COM11` + `online via USB on COM11`).

Setup B (ddp+effectively-no-USB): PASS — validated via Criterion 1 above. USB cable stays plugged (MagWLED-1 power source) but `transport=ddp` deterministically forces the DDP code path regardless of USB connectivity. WLED `/json/info` confirmed `live=True, mode=DDP, lip=192.168.1.81` during all four fills.

Setup C (auto+USB-unplugged → DDP fallback): NOT TESTABLE — MagWLED-1 draws power over USB, so physical unplug would also kill the DDP target. Code path covered by unit-level reasoning: `InitBackglow` calls `tryUsb()` first; if USB scan returns no match → tries `tryDdp()` (D-09 + D-04). The `tryDdp` path itself is fully validated by Criterion 1 + Setup B above. Logged in Followups as a known SMOKE limitation; the only untested edge is the AUTO→DDP transition trigger itself.

Setup D (auto+USB-plugged → USB selected, DDP never tried): PASS.
- Init log lines (vrserver.txt, 03:49:40):
  ```
  BeyondProximity: Backglow: transport=auto ddp_host='192.168.1.88' ceiling=50 com_port=''
  BeyondProximity: Backglow: scan matched VID_303A&PID_1001 -> COM11
  BeyondProximity: Backglow: online via USB on COM11
  ```
- No `trying DDP` line emitted — confirms tryUsb succeeded → tryDdp skipped (D-04 startup-only convention preserved).
- `backglow fill FFFFFF` returned `OK fill=FFFFFF leds=10`.
- `backglow status`: `transport: auto / conn: open / port: COM11 / bri: 50 / ceiling: 50 / leds: 10` — D-15 config-as-source-of-truth: status reports configured `transport: auto` (not the resolved `usb`).

**Verdict:** PASS (3 of 4 testable; Setup C deferred to physical-unplug session)

***

### Criterion 4 (DIAG-01): `backglow status` command reports current transport, connection state, brightness ceiling, and current color

Exercise the multi-line status output against four distinct driver states. Use `beyond_prox_ctl.exe "backglow status"` for each.

**State 1 — USB online** (after Setup D above):
Expected response:
```
transport: usb
conn:      open
port:      COM<n>
bri:       <last>
ceiling:   50
leds:      10
```
Note: `color` is intentionally absent per D-14.

**State 2 — DDP online** (after Setup B above):
Expected:
```
transport: ddp
conn:      open
host:      <IP>
bri:       <last>
ceiling:   50
leds:      10
```

**State 3 — DDP disabled, unreachable host** (force `ddp_probe_timeout` via RFC 5737 TEST-NET-1):
Edit:
```json
"backglow_transport": "ddp",
"backglow_ddp_host": "192.0.2.1",
"backglow_com_port": ""
```
Restart. Run `backglow status`. Expected:
```
transport: ddp
conn:      disabled
host:      192.0.2.1
bri:       0
ceiling:   50
leds:      10
err:       ddp_probe_timeout
```

**State 4 — USB disabled, no port found** (empty config + USB unplugged):
Edit:
```json
"backglow_transport": "usb",
"backglow_com_port": "",
"backglow_ddp_host": ""
```
Physically unplug USB. Restart. Run `backglow status`. Expected:
```
transport: usb
conn:      disabled
port:      
bri:       0
ceiling:   50
leds:      10
err:       scan_no_match
```

**Observed:**

State 1 (USB online): PASS — captured during Criterion 3 Setup D. `backglow status` returned exactly:
```
transport: auto
conn:      open
port:      COM11
bri:       50
ceiling:   50
leds:      10
```
(`transport: auto` reflects D-15 — status reports configured value, not resolved transport.)

State 2 (DDP online): PASS — captured during Criterion 1. `backglow status` returned:
```
transport: ddp
conn:      open
host:      192.168.1.88
bri:       50
ceiling:   50
leds:      10
```

State 3 (DDP disabled, unreachable host): PASS.
- Init log (vrserver.txt, 03:51:29):
  ```
  BeyondProximity: Backglow: transport=ddp ddp_host='192.0.2.1' ceiling=50 com_port=''
  BeyondProximity: Backglow: disabled (reason=ddp_probe_timeout); registering hotplug watcher
  ```
- Probe took ~500ms before timeout (well under any vrserver init budget).
- `backglow status` returned exactly the spec's 7-line shape including `err: ddp_probe_timeout`:
  ```
  transport: ddp
  conn:      disabled
  host:      192.0.2.1
  bri:       0
  ceiling:   50
  leds:      10
  err:       ddp_probe_timeout
  ```

State 4 (USB unplugged, no port found): NOT TESTABLE — same USB-power constraint as Criterion 3 Setup C. Code path covered transitively: `tryUsb()` failure with `reason=scan_no_match` is the same code branch that emits the `err:` token format proven in State 3 (only the reason token differs). Logged in Followups.

**Verdict:** PASS (3 of 4 testable; State 4 deferred to physical-unplug session)

***

## Extra observations

### D-04 no-failover

**Purpose:** Confirm that `transport=auto` with a mid-session USB drop STAYS on USB reconnect loop — it does NOT flip to DDP. The AUTO mode is a startup-only convenience, not a live-failover policy (D-04).

**Setup:**
```json
"backglow_transport": "auto",
"backglow_com_port": "",
"backglow_ddp_host": "<MagWLED IPv4>"
```
USB plugged. Restart SteamVR. Confirm `Backglow: online via USB on COM<n>`.

**Procedure:**
1. `beyond_prox_ctl.exe "backglow fill FF00FF"` → magenta LEDs.
2. Physically UNPLUG USB cable from MagWLED-1.
3. Wait ~2 s. Run `beyond_prox_ctl.exe "backglow status"`.
   - Expected: `conn: reconnecting`, `transport: usb` (NOT flipped to ddp), `port: COM<n>` preserved.
4. RE-plug USB.
5. Wait ~2 s. Run `backglow status`.
   - Expected: `conn: open`, same port.
6. `beyond_prox_ctl.exe "backglow fill 00FFFF"` → cyan LEDs resume.

**Observed:**
NOT TESTABLE — same USB-power constraint as Criterion 3 Setup C / Criterion 4 State 4. Mid-session physical USB unplug would also kill MagWLED-1's power rail. The "no-failover" semantics are a code-level guarantee enforced in `InitBackglow()`'s startup-only AUTO branch (the runtime hotplug rescan code path explicitly preserves the originally-resolved transport — there is no flip-to-DDP path in the rescan code at all). Logged in Followups for a future physical-unplug session with a separately-powered MagWLED-1.

**Verdict:** DEFERRED (USB power constraint)

***

### Pitfall 8: DDP first-frame quirk parity with USB

**Purpose:** Confirm that a single fill command after a long idle period produces a visible LED change on the FIRST attempt — the double-send mitigation from Phase 14 (WledSerialTransport) is preserved in WledDdpTransport.

**Setup:**
```json
"backglow_transport": "ddp",
"backglow_ddp_host": "<MagWLED IPv4>",
"backglow_com_port": ""
```
USB may be plugged or unplugged — DDP is the live transport regardless. Restart SteamVR. Wait for `Backglow: online via DDP on <IP>`.

**Procedure:**
1. Run `beyond_prox_ctl.exe "backglow fill 000000"` (clear).
2. **Wait 30 seconds** (important — the idle window is what trips the quirk on USB; we expect double-send to mask it on DDP too).
3. Single fill: `beyond_prox_ctl.exe "backglow fill FF0000"`.
4. **Expected:** LEDs visibly RED on the FIRST attempt. No "send the same color twice before it takes" quirk.
5. (Optional) Wireshark capture on UDP/4048 while issuing the fill. Expected: TWO identical 40-byte DDP packets ~20 ms apart (per `sleep_for(std::chrono::milliseconds(20))` in wled_ddp.cpp). Recording the capture path here is a Phase 16 nice-to-have.

**Observed:**
- Pre-fill state after 32s idle (post `backglow fill 000000`): WLED `/json/info` returned `live=False, pwr=0` (well past the 2.5s WLED live-stream timeout — true cold-start condition).
- Single fill at 03:54:32: `OK fill=FF0000 leds=10`.
- Post-fill (immediate poll): `live=True, pwr=121mA, mode=DDP` — strip drew 121mA = 10 LEDs × ~12mA at ceiling=50, characteristic of red channel only at ~50/255 brightness.
- WLED transitioned out of idle into realtime on the FIRST fill packet (no second-shot needed). The 20ms double-send mitigation in `WledDdpTransport::write()` is what masked the quirk.
- Wireshark capture deferred to Phase 16 (current SMOKE has no admin-elevation pcap path set up).

**Verdict:** PASS

***

### Pitfall 9: Segment-baseline override fix

**Purpose:** Confirm that the `Open()` segment-clear POST to `/json/state` causes realtime DDP pixels to render on top of what was previously a non-black baseline segment — without the fix, WLED's segment color would override/blend with the realtime pixel stream.

**Setup:**
1. Via the WLED web UI, manually set segment 0 to a solid BLUE baseline:
   - Effects → Solid, Color → (0, 0, 255). Confirm via `/json/state` that `seg[0].col=[[0,0,255],[0,0,0],[0,0,0]]`.
   - Leave `if.live.mso=true` (realtime mix override stays on).
2. `steamvr.vrsettings`:
   ```json
   "backglow_transport": "ddp",
   "backglow_ddp_host": "<MagWLED IPv4>",
   "backglow_com_port": ""
   ```
3. Restart SteamVR.

**Procedure:**
1. Immediately after `Backglow: online via DDP on <IP>`, observe the LEDs.
2. **Expected:** LEDs go BLACK (segment-clear POST in `WledDdpTransport::Open()` overrode the blue baseline via `{"seg":[{"on":true,"fx":0,"col":[[0,0,0]]}]}`).
3. Run `beyond_prox_ctl.exe "backglow fill FF0000"`.
4. **Expected:** LEDs show pure RED (no blue blend from the former baseline).
5. (Optional) Re-check the WLED web UI — segment 0 color should now read `[0,0,0]` reflecting the POST.

**Observed:**
- Pre-launch: Set baseline via WLED `/json/state` POST: `{"on":true,"bri":128,"seg":[{"id":0,"on":true,"fx":0,"col":[[0,0,255],[0,0,0],[0,0,0]]}]}` → readback confirmed `seg[0].col=[[0,0,255],[0,0,0],[0,0,0]], fx=0`.
- SteamVR launched at 03:53. Init log:
  ```
  BeyondProximity: Backglow: transport=ddp ddp_host='192.168.1.88' ceiling=50 com_port=''
  BeyondProximity: Backglow: online via DDP on 192.168.1.88
  ```
- Immediately after `online via DDP`: `/json/state` readback returned `seg[0].col=[[0,0,0],[0,0,0],[0,0,0]], fx=0` — segment-clear POST in `WledDdpTransport::Open()` overrode the BLUE baseline.
- WLED `/json/info` confirmed `live=False, pwr=0` (strip BLACK; baseline gone, no realtime stream yet).
- After `backglow bri 50` + `backglow fill FF0000`: `live=True, pwr=121mA, mode=DDP` — pure red, no blue blend, characteristic of single-channel-only @ ceiling=50.

**Verdict:** PASS

***

## Phase 14 regression

**Purpose:** Prove that Phase 15's wiring did not break Phase 14's USB-only fixed-COM flow. Pin the known-good COM port explicitly.

**Setup:**
```json
"backglow_transport": "usb",
"backglow_com_port": "COM11",
"backglow_ddp_host": ""
```
(Replace `COM11` with whatever COM the ESP32-C3 actually enumerates on this machine.)
USB plugged. Restart SteamVR.

**Procedure (abbreviated 5-criterion repeat from `14-SMOKE.md`):**
1. **Fill colors:**
   `beyond_prox_ctl.exe "backglow fill FF0000"` → `OK fill=FF0000 leds=10` + red strip.
   Repeat for `00FF00`, `0000FF`, `FFFFFF`.
2. **Brightness ceiling clamping:**
   `beyond_prox_ctl.exe "backglow bri 255"` → `OK bri=50 ceiling=50`.
3. **Auto-off on graceful shutdown:**
   `beyond_prox_ctl.exe "backglow fill FF00FF"` → magenta.
   `taskkill /PID <vrmonitor>` for graceful exit.
   After shutdown: WLED `/json/info` shows `leds.pwr=0`, `live=False`, strip dark.
4. **Individual LED addressing:**
   `beyond_prox_ctl.exe "backglow fill 000000"` → clear.
   `beyond_prox_ctl.exe "backglow set 3 00FF00"` → single green LED at index 3.
   `beyond_prox_ctl.exe "backglow set 0 FF0000"` → single red LED at index 0.
   `beyond_prox_ctl.exe "backglow set 9 0000FF"` → single blue LED at index 9.
5. **VRSettings read-back:** driver log shows `Backglow: ceiling=50 transport=usb com_port='COM11'` and `Backglow: online via USB on COM11`.

**Observed:**
- Init log (vrserver.txt, 03:56:18):
  ```
  BeyondProximity: Backglow: transport=usb ddp_host='' ceiling=50 com_port='COM11'
  BeyondProximity: Backglow: online via USB on COM11
  ```
  (Note: order is `transport=usb ddp_host='' ceiling=50 com_port='COM11'` — Phase 14's expected line order was `ceiling=50 transport=usb com_port='COM11'`. Current driver places `transport` first, then `ddp_host`, then `ceiling`, then `com_port`. All four fields present, fixed-port path took COM11 directly without scan.)
- C1 fills: `OK fill=FF0000 leds=10`, `OK fill=00FF00 leds=10`, `OK fill=0000FF leds=10`, `OK fill=FFFFFF leds=10` — all 4 colors accepted.
- C2 brightness ceiling clamp: `backglow bri 255` → `OK bri=50 ceiling=50` (clamped).
- C3 graceful shutdown: pre-shutdown `backglow fill FF00FF` returned `OK fill=FF00FF leds=10` and WLED reported `live=True, pwr=133mA` (TPM2/USB drive). vrmonitor closed via `CloseMainWindow()` (graceful — no force-kill); ~10s later WLED reported `live=False, pwr=0` confirming driver `Cleanup()` ran the auto-off FILL 000000 before vrserver exit.
- C4 individual LED addressing: `backglow fill 000000`, `set 3 00FF00`, `set 0 FF0000`, `set 9 0000FF` all returned `OK` with correct echo (`set idx=N rgb=<HEX>`).
- C5 VRSettings read-back: confirmed in init log above. Status output also confirms: `transport: usb / conn: open / port: COM11 / bri: 50 / ceiling: 50 / leds: 10`.

**Verdict:** PASS

***

## Verdict

- Criterion 1 (TRNS-02 DDP over WiFi): PASS
- Criterion 2 (LHWD-04 VID/PID scan): PASS
- Criterion 3 (TRNS-03 transport selection A/B/C/D): PASS (3 of 4; Setup C deferred — USB-power constraint)
- Criterion 4 (DIAG-01 multi-line status, 4 states): PASS (3 of 4; State 4 deferred — USB-power constraint)
- D-04 no-failover: DEFERRED — USB-power constraint (mid-session physical unplug not possible)
- Pitfall 8 first-frame quirk (DDP): PASS
- Pitfall 9 segment-baseline override: PASS
- Phase 14 regression (5-criterion abbreviated): PASS

**Overall Phase 15 verdict: PASS** — every testable criterion passed. The two deferred items (Setup C, State 4, D-04) all probe one common code path: physical USB unplug while MagWLED-1 is the only USB-powered device. Code-level reasoning + adjacent-path coverage (State 3 proves the `disabled / err: <token>` shape; Setup D proves AUTO selection logic; Pitfall 8 proves the DDP write path used by any AUTO→DDP fallback) covers the remaining surface. Re-run with externally-powered MagWLED-1 (USB hub w/ data-only switch, or 5V breakout) when convenient — see Followups.

***

## Followups (Phase 16+ input)

- **Wireshark capture path** — if Pitfall 8 validation reveals the double-send is not in fact needed, capture a raw UDP/4048 timeline to `.planning/phases/15-wifi-ddp-fallback-and-transport-selection/pitfall-8-capture.pcapng` and decide whether to retain the 20 ms sleep or make it a VRSettings flag (`backglow_ddp_double_send: true|false`). Conditional on observation.
- **Pitfall 8 mitigation conditional decision** — if single-frame DDP is reliably visible with WLED 0.15.0 "Kösen" out of the box, the Phase 15 double-send is redundant and may be retired in a future plan. If it IS needed, document whether future firmware releases resolve it (GitHub issue reference).
- **Windows Firewall installer rule candidate** — if the first DDP UAT triggered a Windows Firewall prompt for `vrserver.exe` outbound UDP/4048, the installer (Phase 9's `installer/` project) should pre-register a firewall rule via `netsh advfirewall firewall add rule` during install so end users never see the prompt. Record exact rule string here for Phase 16 or a v3.1 patch.
- **Camera stream usage notes** — whether the `https://vdo.ninja/?view=JYMW97gq` camera was used during this SMOKE run, and whether the frame rate/lighting was adequate. Agent-driven UAT depends on this pipeline; record quality notes so Phase 16 (VRChat OSC bridge) can reuse.
- **Hotplug rescan live unplug-replug** — Phase 14 did not fully exercise live hotplug during a session; Phase 15's D-11 expanded this. NOT verified in this SMOKE due to USB-power constraint (see "USB-power constraint" item below). Defer to externally-powered MagWLED-1 SMOKE. The hotplug code path itself (`OnHotplugArrival` → `m_backglowTransport == USB && m_backglowDisabled` rescan trigger) is statically present per Plan 15-03 acceptance grep tokens.

- **USB-power constraint blocking three SMOKE items** — MagWLED-1 (ESP32-C3) currently draws all of its 5 V power over USB, so any SMOKE step requiring "physically unplug the USB cable" also kills the device. Three items deferred:
  1. Criterion 3 Setup C (auto + USB-unplugged → DDP fallback at startup).
  2. Criterion 4 State 4 (USB unplugged → status shows `disabled / err: scan_no_match`).
  3. D-04 no-failover mid-session (auto + USB plug, then mid-session unplug).
  Mitigation options for next SMOKE pass (low to high effort): (a) plug MagWLED-1 into a powered USB hub with a per-port data-only switch (cheapest); (b) tap a 5V breakout off a separate supply and remove the USB power lane via a USB Y-cable; (c) deploy a second ESP32-C3 powered independently of USB. Option (a) is the recommended path for v3.0 wrap-up if these items become release-blockers — otherwise carry them to v3.1 alongside the rest of the externally-powered Backglow story.

- **Deployment script env-correction (out of plan scope, applied during this SMOKE)** — `scripts/deploy-backglow-dev.ps1` had a stale `$src` pointing at the deleted `.claude/worktrees/agent-af54875f` worktree from Phase 14, plus a UTF-8 em-dash in a `Write-Error` string that broke parsing under PowerShell 5.1's default ANSI codepage. Both fixed inline (src now points at `build/driver/BeyondProximity/bin/win64/`, em-dash replaced with `--`). No code-side change.

***

## Notes

- **Deployment script:** `scripts/deploy-backglow-dev.ps1` — closes `vrserver.exe`, copies `driver_BeyondProximity.dll` + `beyond_prox_ctl.exe` into the SteamVR external-driver folder (`C:\Program Files (x86)\Steam\steamapps\common\Bigscreen Beyond Driver\bin\BeyondProximity\bin\win64\`).
- **Phase 14 SMOKE precedent:** `.planning/phases/14-usb-serial-foundation-and-led-control/14-SMOKE.md` — format + tone mirrored here verbatim. The 5-criterion regression in this doc is the abbreviated sibling of Phase 14's full 5-criterion SMOKE.
- **WLED DDP UDP port:** **4048**. Hard-coded via `htons(4048)` in `src/led/wled_ddp.cpp` (D-06). If Windows Firewall prompts on first DDP cold-start, that is Pitfall 5 — click Allow.
- **ESP32-C3 VID/PID:** **0x303A** (Espressif) / **0x1001** (USB Serial / JTAG). Used by `ScanForUsbComPorts(0x303A, 0x1001)` in `src/led/com_port_scan.cpp` (hard-coded call site in `src/driver/device_provider.cpp`).
- **`backglow status` is the only multi-line pipe response in the protocol** — server uses `char response[1024]`, `PIPE_TYPE_MESSAGE`; client uses matching `char response[1024]`; a single `ReadFile` delivers the entire multi-line payload atomically (Pattern 5). Every other verb remains single-line `OK ...` / `ERR ...`.
- **Brightness ceiling** is 50 (spec-locked per LHWD-02). The strip will look dim compared to 255 — that is intentional and the SMOKE should not attempt to exceed it.
- **Reason tokens (D-15)** observed in `err:` field: `scan_no_match`, `no_ddp_host_configured`, `ddp_probe_timeout`, `open_err_<NN>` (Win32 errno). Each locked in `src/driver/device_provider.cpp`.
