# Phase 16 Hardware Smoke

**Hardware:** MagWLED-1 (ESP32-C3, WLED 0.15.0 "Kösen", WS2812B strip, 10 addressed LEDs — Phase 14/15 configuration preserved)
**USB COM port:** COM11 (confirmed via VID/PID scan log: `scan matched VID_303A&PID_1001 -> COM11`)
**WiFi IPv4:** _TBD (placeholder — fill in at UAT time if DDP transport selected)_
**Date:** _TBD_
**Tester:** _TBD_
**Driver build:** `driver_BeyondProximity.dll` + `beyond_prox_ctl.exe` + NEW `beyond_backglow_ctl.exe` from the Phase 16 worktree. Includes the full Wave 0..3 stack: CMake target scaffolding (Wave 0), daemon core (osc_server / param_map / pipe_client / silence_fade / startup_anim — Wave 1), driver lifecycle hookup (Job Object + CreateProcess + watchdog respawn backoff — Wave 2), and OSCQuery/mDNS discovery (Wave 3). All Phase 14/15 regressions preserved (USB scan-fallback, DDP fallback, multi-line `backglow status`, degraded-disabled hotplug rescan, 1024-byte pipe buffers).

***

## Prerequisites

1. **MagWLED-1 connected via USB (USB transport) OR provisioned on local WiFi (DDP transport).** Phase 14/15 baseline; Phase 16 does not regress the USB/DDP split. The backglow transport path is independent of the daemon — the daemon only cares that the pipe is up and `backglow fill`/`backglow bri` verbs round-trip to LEDs.
2. **Driver + CLI + daemon deployed.** Run `scripts/deploy-backglow-dev.ps1` from the repo root. The script closes `vrserver.exe`, copies `driver_BeyondProximity.dll` + `beyond_prox_ctl.exe` + NEW `beyond_backglow_ctl.exe` into the SteamVR external-driver folder (`C:\Program Files (x86)\Steam\steamapps\common\Bigscreen Beyond Driver\bin\BeyondProximity\bin\win64\`), then prints a `Get-Item` table showing all three artifacts landed.
3. **`steamvr.vrsettings` baseline** under the `"driver_BeyondProximity"` section. D-01 explicitly declines a new backglow-enable toggle; Phase 16 reuses the Phase 14/15 keys unchanged:
   ```json
   "driver_BeyondProximity": {
     "backglow_brightness_ceiling": 50,
     "backglow_com_port": "",
     "backglow_transport": "usb",
     "backglow_ddp_host": ""
   }
   ```
4. **Python 3 + `python-osc` installed** for `tests/osc_fixture/send_backglow.py`. Install via:
   ```
   py -m pip install python-osc
   ```
   before running any fixture row below. `send_backglow.py --help` must exit 0 before rows VRCH-02a..d are attempted.
5. **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. Most rows below are camera-visual and depend on the stream being on.

**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 / daemon-spawn / hotplug / respawn-backoff line is prefixed with this literal.

**Fixture invocation (used by VRCH-02 rows):**
```
py tests/osc_fixture/send_backglow.py --led 0 --r 1.0 --g 0.0 --b 0.0
py tests/osc_fixture/send_backglow.py --sweep
py tests/osc_fixture/send_backglow.py --silence 5
py tests/osc_fixture/send_backglow.py --bri-dedup
```
Default target is `127.0.0.1:9001` (D-19 fallback port). If OSCQuery bound an ephemeral port instead (D-18), pass `--port <N>` — the daemon logs its bound port on startup; scrape it from `vrserver.txt` or the daemon's own stderr.

***

## Row VRCH-01a — Daemon spawns when driver loads

**Steps:**
1. Ensure `steamvr.vrsettings` has valid backglow config (USB mode with MagWLED-1 connected, per Prerequisite 3).
2. Launch SteamVR: `"C:/Program Files (x86)/Steam/steamapps/common/SteamVR/bin/win64/vrstartup.exe"`.
3. Tail `vrserver.txt`: `Get-Content "C:/Program Files (x86)/Steam/logs/vrserver.txt" -Tail 200 -Wait | Select-String Backglow`.
4. Verify log contains `Backglow: daemon spawned (pid=N)` within ~2 s of `Backglow: online via USB on COM<n>`.
5. Run `Get-Process beyond_backglow_ctl` — expect exactly one row returned.

**Pass:** Log line present; process listed.
**Fail evidence:** Paste log excerpt + `Get-Process` output.

***

## Row VRCH-01b — Daemon dies within 2s when vrserver.exe force-killed (Job Object kill-on-close)

**Steps:**
1. Confirm VRCH-01a PASSED (daemon currently running).
2. Note daemon PID from `Get-Process beyond_backglow_ctl | Select-Object Id`.
3. Force-kill vrserver: `Stop-Process -Name vrserver -Force`.
4. Wait 2 seconds.
5. Run `Get-Process beyond_backglow_ctl -ErrorAction SilentlyContinue`.

**Pass:** Command returns nothing (daemon process terminated by Job Object on handle close — D-03 kill-on-close).
**Fail evidence:** Daemon PID still alive after 2 s → paste `Get-Process` output + `vrserver.txt` tail for the last 50 lines.

***

## Row VRCH-01c — Daemon does NOT spawn when backglow is in degraded-disabled state

**Steps:**
1. Edit `steamvr.vrsettings`: set `"backglow_transport": "usb"`, `"backglow_com_port": ""`, `"backglow_ddp_host": ""`.
2. Physically UNPLUG MagWLED-1 USB cable (so VID/PID scan finds no match) — OR — set `"backglow_transport": "ddp"` with a known-unreachable host (RFC 5737 TEST-NET-1: `"backglow_ddp_host": "192.0.2.1"`) to force the degraded-disabled state without unplugging.
3. Launch SteamVR.
4. Tail `vrserver.txt`. Expected: `Backglow: disabled (reason=scan_no_match)` OR `Backglow: disabled (reason=ddp_probe_timeout)`.
5. Search same log for `Backglow: daemon spawned` — must NOT appear.
6. Run `Get-Process beyond_backglow_ctl -ErrorAction SilentlyContinue`.

**Pass:** Log shows `disabled (reason=…)`, NO `daemon spawned` line, `Get-Process` returns nothing. (D-02: degraded state gates daemon spawn.)
**Fail evidence:** `daemon spawned` line present → paste log excerpt + process list.

***

## Row VRCH-01d — Respawn backoff 1s/2s/4s with cap at 3

**Steps:**
1. Confirm VRCH-01a PASSED (daemon running on successful init).
2. Tail `vrserver.txt` filtered to `Backglow:` lines with timestamps.
3. Force-kill daemon once: `Stop-Process -Name beyond_backglow_ctl -Force`.
4. Wait 2 s. Watch log for `Backglow: daemon respawn attempt 1 (delay=1s)` followed by `Backglow: daemon spawned (pid=N)` ~1 s later.
5. Immediately kill again: `Stop-Process -Name beyond_backglow_ctl -Force`.
6. Watch for `attempt 2 (delay=2s)` + respawn.
7. Immediately kill a third time. Watch for `attempt 3 (delay=4s)` + respawn.
8. Immediately kill a fourth time. Watch for `Backglow: daemon respawn cap reached; giving up for session` ERR line. No further respawn.

**Pass:** Delays between `kill` and `daemon spawned` lines are approximately 1s / 2s / 4s; fourth kill produces the cap-reached ERR and no further respawn.
**Fail evidence:** Delays wrong, or respawn continues past 3 attempts → paste 3 full backoff cycles + timestamps from `vrserver.txt`.

***

## Row VRCH-01e — Startup animation visible on first spawn (camera stream, 4s white fade)

**Steps:**
1. Ask user to enable `https://vdo.ninja/?view=JYMW97gq` camera stream pointed at the HMD.
2. Fully close SteamVR (`vrmonitor.exe` → File → Exit) so next launch is cold.
3. Launch SteamVR.
4. Watch camera stream at the moment `vrserver.txt` shows `Backglow: daemon spawned (pid=N)` followed by `Backglow: startup animation begin`.
5. Expected visual: all 10 LEDs fade from black → white (@ ~50% ceiling) over ~1 s, hold briefly, fade back to black over ~3 s. Total ~4 s.
6. After fade completes, log shows `Backglow: startup animation complete`.

**Pass:** Camera captures the fade-up/fade-down; log confirms begin + complete lines.
**Fail evidence:** No fade visible or fade truncated (wrong duration) → paste daemon stderr + camera screenshot.

***

## Row VRCH-01f — Daemon listens on UDP (OSCQuery or fallback 9001)

**Steps (primary path — OSCQuery):**
1. After SteamVR launch, check daemon log for `OSCQuery primary path: UDP=N HTTP=M`.
2. Confirm the ephemeral ports are listening: `netstat -ano | findstr "127.0.0.1"` — expect two rows at 127.0.0.1:N (UDP) and 127.0.0.1:M (TCP).
3. mDNS discovery (optional): from another shell, run `dns-sd -B _oscjson._tcp` (if dns-sd installed) or use `Bonjour Browser` — expect "Beyond Backglow" to appear.
4. HTTP probe: `curl http://127.0.0.1:M/` returns JSON containing `"FULL_PATH":"/"` + `/avatar`.
5. HTTP probe: `curl "http://127.0.0.1:M/?HOST_INFO"` returns JSON with `OSC_PORT` = N + `OSC_TRANSPORT:"UDP"`.

**Steps (fallback path — 9001 legacy, triggered by argv or OSCQuery setup failure):**
1. Check daemon log for `fallback path: listening on 127.0.0.1:9001 (D-19 legacy)`.
2. Confirm: `netstat -ano | findstr :9001 | findstr LISTENING`.

**Pass:** Either primary-path OSCQuery is active (preferred) OR fallback 9001 is active. Both are acceptable; OSCQuery is the designed primary.
**Fail evidence:** Paste daemon-log excerpt + netstat output.

***

## Row VRCH-02a — Float param → LED color update within 1 frame

**Steps:**
1. Confirm VRCH-01a + VRCH-01f PASSED (daemon alive + UDP port known).
2. Ask user to enable camera stream (or view HMD directly).
3. Send first-LED red via fixture:
   ```
   py tests/osc_fixture/send_backglow.py --led 0 --r 1.0 --g 0.0 --b 0.0
   ```
   (Add `--port <N>` if OSCQuery bound an ephemeral port.)
4. Fixture prints `sent LED0 R=1.0 G=0.0 B=0.0` and exits.
5. Within ~50 ms (1 frame @ 90 Hz tick rate = ~11 ms; camera captures the next frame), observe LED 0 turn RED. Other 9 LEDs remain OFF.
6. `vrserver.txt` shows one `backglow fill` emission in the daemon tick following the OSC receipt (echo via pipe server).

**Pass:** Camera sees LED 0 red within ~1 frame; LED 1..9 unchanged; pipe-server log confirms the corresponding `backglow fill` command.
**Fail evidence:** LED 0 doesn't turn red, or wrong LED lights up, or visible delay > 1 s → paste fixture stdout + camera screenshot + `vrserver.txt` tail.

***

## Row VRCH-02b — All 31 parameters route correctly (sweep)

**Steps:**
1. Confirm VRCH-01a PASSED.
2. Run sweep fixture:
   ```
   py tests/osc_fixture/send_backglow.py --sweep
   ```
3. Fixture sends `BackglowR0..9`, `BackglowG0..9`, `BackglowB0..9`, `BackglowBri` all at 0.5 (10 ms between sends → ~310 ms total), prints `sweep complete: 31 params -> 0.5`, exits.
4. Watch camera: all 10 LEDs should settle at 50% brightness × 50% per channel (dim mid-grey at the ceiling=50/255 cap → visually ~12% of max per channel).
5. Inspect daemon log (stderr or file sink — implementation's choice): count dispatches. Must show 31 distinct `OSC: /avatar/parameters/Backglow*` received lines during the sweep window.
6. Pipe server in `vrserver.txt` shows one `backglow fill` per daemon tick for the duration of the sweep + one final `backglow bri 127` (round(0.5 * 255) = 127 or 128 depending on rounding mode; either is acceptable).

**Pass:** All 31 params land; LEDs visibly mid-grey; daemon log shows 31 dispatches.
**Fail evidence:** Any param dropped (count < 31) or LED colors wrong → paste daemon log counter + camera screenshot.

***

## Row VRCH-02c — Silence fade after 3s no OSC

**Steps:**
1. Confirm VRCH-02a PASSED so LEDs are currently lit (e.g., LED 0 red from VRCH-02a, or re-fire `--led 0 --r 1.0` to make LEDs visible).
2. Run silence fixture (sends one frame then sleeps, does nothing for `<seconds>` = plenty beyond the 3 s silence threshold):
   ```
   py tests/osc_fixture/send_backglow.py --silence 5
   ```
3. Watch camera from the moment the fixture prints `sent initial frame, sleeping 5s (silence test)...`.
4. Expected timeline:
   - 0 s: LED 0 lit red (from initial frame).
   - ~3 s after last OSC: daemon log shows `Backglow: silence detected; fade begin`.
   - 3.0 s → 3.5 s: linear ramp `backglow bri 50 → 0` (visible on camera as brightness fade over ~500 ms).
   - ~3.5 s: daemon emits `backglow off`; LEDs go fully dark.
   - 5 s: fixture wakes + prints `silence complete — LEDs should have faded off by now` + exits. LEDs STAY off because no new OSC arrived.

**Pass:** Camera sees linear ~500 ms fade starting ~3 s after last OSC; final `backglow off` in pipe log; LEDs remain dark after fixture exits.
**Fail evidence:** LEDs stay lit after 5 s silence, or fade too fast / too slow → paste camera timeline + daemon log.

***

## Row VRCH-02d — Bri pipe command emitted only on integer change

**Steps:**
1. Confirm VRCH-01a PASSED.
2. Clear daemon log (or note current tail cursor in `vrserver.txt`).
3. Run dedup fixture (sends `BackglowBri=0.5` ten times in 50 ms):
   ```
   py tests/osc_fixture/send_backglow.py --bri-dedup
   ```
4. Fixture prints `bri-dedup complete: sent 10 x BackglowBri=0.5` and exits.
5. `round(0.5 * 255) = 127` (or 128 depending on rounding — daemon picks a convention in Wave 1).
6. Inspect `vrserver.txt` pipe-server log after the fixture window: must contain **exactly ONE** `backglow bri 127` (or `bri 128`) line for this 10-shot burst. Any subsequent ticks that see the same float value → NO pipe write (D-13 integer-change dedup).

**Pass:** Exactly one `backglow bri <N>` command emitted for 10 identical float sends.
**Fail evidence:** 2+ duplicate bri commands in the pipe log → paste log excerpt + daemon stderr dispatch counter.

***

## Verdict

- VRCH-01a (Daemon spawn): _TBD_
- VRCH-01b (Job Object kill-on-close within 2s): _TBD_
- VRCH-01c (No spawn in degraded state): _TBD_
- VRCH-01d (Respawn backoff 1/2/4s, cap 3): _TBD_
- VRCH-01e (Startup animation visible): _TBD_
- VRCH-01f (UDP listener present on OSCQuery or 9001): _TBD_
- VRCH-02a (Float param → LED within 1 frame): _TBD_
- VRCH-02b (31-param sweep): _TBD_
- VRCH-02c (Silence fade over ~500 ms after 3 s): _TBD_
- VRCH-02d (Bri dedup on integer change): _TBD_

**Overall Phase 16 verdict: _TBD_**

***

## Notes

- **Deployment script:** `scripts/deploy-backglow-dev.ps1` — closes `vrserver.exe`, copies three artifacts (driver DLL + `beyond_prox_ctl.exe` + `beyond_backglow_ctl.exe`) into the SteamVR external-driver folder (`C:\Program Files (x86)\Steam\steamapps\common\Bigscreen Beyond Driver\bin\BeyondProximity\bin\win64\`).
- **Phase 14/15 SMOKE precedents:** `.planning/phases/14-usb-serial-foundation-and-led-control/14-SMOKE.md` and `.planning/phases/15-wifi-ddp-fallback-and-transport-selection/15-SMOKE.md` — format + tone mirrored here verbatim.
- **VRChat end-to-end:** simulated via `tests/osc_fixture/send_backglow.py` (python-osc). A live VRChat session is an *optional* final-mile confidence check, not a pass/fail gate. The fixture exercises every OSC path the real VRChat client uses.
- **Daemon log sink:** Wave 1 picks stderr vs file sink. If file-sink (`%LOCALAPPDATA%\Beyond Backglow\daemon.log`), all log-inspection rows above read from that path instead of `vrserver.txt`.
- **90 Hz tick rate (D-12, D-26):** `LedController::m_maxFps` retuned from 30 → 90 in Wave 1. Writer thread cv-wait reduces to ~11 ms. Camera stream at 30 fps will undersample the 90 Hz tick — visible fade-ramps and step-changes are still unambiguous at 30 fps capture.
- **Brightness ceiling** stays at 50 (spec-locked per LHWD-02). LEDs will look dim compared to 255 — that is intentional.
- **OSCQuery port ephemeral binding (D-18):** `netstat` checks in VRCH-01f + VRCH-02a must resolve the actual port at test time. Daemon logs its bound port; don't hardcode 9001 unless the fallback path was taken.
