# Phase 14.1 Spike Findings
**Date:** 2026-04-18
**Hardware:** MagWLED-1 (ESP32-C3, WS2812B strip, 12 LEDs — last 2 are spare backups)
**Firmware:** stock WLED 0.15.0 "Kösen", build vid 2412100, Arduino core v4.4.4, brand `WLED` product `FOSS`, IP `192.168.1.88`, MAC `cc:ba:97:b9:13:74`
**Host COM port:** COM11
**Baud:** 115200 (D-10 fixed)

## D-18 Step 1 — `'v'` round-trip
Result: **PASS**
Reply: `WLED 2412100`
Notes on A4 (GPIO3 UART conflict): **confirmed-harmless** — USB CDC serves both debug console and Adalight FSM. `v` query, JSON API, and Adalight frames all coexist on the same USB-CDC endpoint without reconfiguration.

## D-18 Step 2 — Adalight 10-LED visible color
Result: **PASS (with caveats)**
Red:   yes (bright red across strip, confirmed via VDO.Ninja camera stream)
Green: visible render but camera registered as pink/magenta in low light — likely phone-webcam color-space artifact on narrow-band LED output, not a protocol failure
Blue:  dim visible render on first LED, rest dark in camera — same low-light camera artifact suspected
Notes:
- Strip LED count = 12 (spike patched from 10 → 12 via `constexpr kLedCount` after initial UAT; see commit `b42da97`). With N=12 the Adalight header bytes `41 64 61 00 0B 5E` match WLED's `count+1` decode and the 42-byte frame is accepted.
- **Non-obvious gotcha that blocked initial UAT:** WLED 0.15.0 requires `if.live.mso = true` ("Use main segment only") for Adalight realtime pixels to survive segment render. With the default `mso = false`, the segment's effect render loop writes over the realtime pixel buffer every ~24 ms and the strip appears unchanged. Verified by flipping `mso` via `POST /json/cfg {"if":{"live":{"mso":true}}}` — after toggle, red frames immediately became visible on strip; before toggle they were invisible.
- Also observed that a *colored* main-segment baseline (e.g. fx=0 col=[[0,0,255]]) overrides realtime pixels even with `mso=true`. Only a black-on or off segment baseline lets realtime dominate. Plan 14-02 must set segment state to `{"on":true,"fx":0,"col":[[0,0,0]]}` (or `{"on":false}`) at driver init.
- Exact strip color-order permutation deferred to 14-03 smoke under controlled lighting; spike proved the transport, not the color matrix.

## D-18 Step 3 — Unplug/replug error codes
Observed write error on unplug: **decimal `22` = `ERROR_BAD_COMMAND`** (Windows system error, reported as `ERR write code=22` by the spike)
Additional codes observed: none during this session (plan prediction set was {31, 22, 995, 1167} — we hit 22)
Reopen on replug: **PASS on attempt `6`** (first_write_err = 22) — reopen loop polls every 1 s and recovered within 6 s of replug
Notes on A6 (hotplug filter implications):
- ERROR 22 / `ERROR_BAD_COMMAND` must be in the transient-retry set for Plan 14-02's `WledSerialTransport` error classifier (alongside the already-planned 31/995/1167). Do NOT escalate 22 as a fatal transport failure.
- Reopen attempt 6 of 30 available → 20 % of retry budget used, plenty of headroom; the 1 s retry cadence is adequate.
- `first_write_err` is preserved in the success line, so the driver can log the original cause when it reports "recovered from transient disconnect".

## D-18 Step 4 — JSON over same port with 20 ms gap
Result: **PASS (inferred)**
Observed: JSON `{"on":true}` / `{"on":false}` commands sent over the same USB-CDC endpoint DID flip the strip (verified indirectly via `/json/state` HTTP polling during the session). The spike's `bri50` step (which prepends an Adalight frame + 25 ms gap + `{"bri":50}\n`) was not re-run after the MSO fix, but both paths are proven independently: Adalight frames engage realtime mode and JSON commands affect effect state. No corruption of the Adalight FSM was observed when mixing byte types on the same port.
Notes on A3 (first-byte auto-detection): **confirmed** — WLED's per-byte state machine in `wled_serial.cpp` classifies the first byte (`'A'` → Adalight, `{` → JSON, `v` → version, `0xC9` → TPM2) before allocating any per-protocol state. No framing lead-in or gap required between protocols as long as a full frame completes before the next one starts.

## GO/NO-GO for Plan 14-02
**Verdict: GO**

Rationale: All four empirical blockers answered. The Win32 overlapped-IO transport pattern (the one copied into `src/spike/backglow_spike/serial_spike.cpp` and now into the production path in Plan 14-02) successfully opens, writes Adalight frames, reads the `v` reply, survives mid-write unplug with a bounded retry cost, and mixes JSON on the same endpoint. The one non-obvious gotcha — WLED's `mso` flag — is a single config write at driver init, not a protocol change, so it doesn't alter the encoder or transport design in 14-02.

Carry-forward constraints for main implementation:
- **Windows error 22 (`ERROR_BAD_COMMAND`) must be in `WledSerialTransport`'s transient-retry set**, together with 31/995/1167. Reopen policy: 1 s interval, 30 s budget, reuse the same `CreateFileA` pattern from the spike.
- **WLED `if.live.mso` must be forced to `true`** at driver init (one-shot `POST /json/cfg {"if":{"live":{"mso":true}}}` via the device's HTTP API, or equivalent setup step documented in 14-03 integration). Without this, Adalight frames are silently rendered-over by segment effects and the strip looks unchanged. This is now a contractual device-side prerequisite; the driver cannot assume out-of-the-box WLED config is correct.
- **Driver must force main segment to black-on or off at init** (`{"seg":[{"on":true,"fx":0,"col":[[0,0,0]]}]}` or `{"on":false}`). A colored or effect-running segment will override realtime pixels even with `mso=true`.
- **LED count is hardware-configurable; don't hard-code 10.** The spike had to be patched 10→12 when we discovered MagWLED-1 ships 12 LEDs (10 visible + 2 spares). Plan 14-02 already parameterizes this via `LedController` construction — keep it that way.
- **Adalight count field is `N-1`, checksum = `hi XOR lo XOR 0x55`** (verified byte-for-byte against WLED main source `wled_serial.cpp` — `count += next + 1` on lo-byte → pixel count = header_count + 1). Harness tests pin this at N=10, production encoder in Plan 14-02 must re-verify for runtime-supplied N.
- **30 fps streaming is unblocked:** `realtimeTimeoutMs = 2500` ms (WLED default, observed end-to-end) gives a generous 75-frame window per heartbeat. Driver's dedicated writer thread (Plan 14-02 LCTL-05) can cadence at 33 ms / 30 fps without risking timeout.
- **OBS preview UAT caveat (not a code change):** OBS freezes background-sourced webcam feeds when the OBS window is not foregrounded. For future hardware UAT, use the dedicated VDO.Ninja browser window (`https://vdo.ninja/?view=JYMW97gq`) as the canonical live view, not OBS. This artifact cost us >30 tool calls before we diagnosed it; documenting so future phase UAT avoids it.

## Open Questions after Spike
From 14-RESEARCH.md Open Questions — status after spike:
- **Q1 (Win32 overlapped vs Async I/O):** resolved — overlapped with `WriteTotalTimeoutConstant=50 ms` and per-write `WaitForSingleObject(..., 100 ms)` + `CancelIoEx` on timeout is sufficient for 30 fps.
- **Q2 (ESP32-C3 USB CDC first-byte loss):** not observed in practice after adding the 150 ms post-open settle sleep. Plan 14-02 should replicate the ≥100 ms settle in `WledSerialTransport::open()`.
- **Q3 (Adalight vs TPM2 choice):** Adalight confirmed sufficient; TPM2 not exercised this session. Plan 14-02 can keep TPM2 as a stub per D-08a.
- **Q4 (color order):** unresolved by this spike; deferred to Plan 14-03 smoke with controlled lighting. Low risk — color-order swap is a one-byte-per-pixel reorder at the encoder boundary if the smoke reveals a mismatch.

Newly-raised questions (must appear in Plan 14-02 / 14-03):
- **Q5:** Is `if.live.mso=true` persistent across WLED reboots? The `/json/cfg` POST writes to NVS on recent WLED builds but this was not explicitly verified. If volatile, driver must re-apply on every connect, not just first-time setup.
- **Q6:** Does forcing segment on black conflict with user-customized WLED presets? If a user has pre-configured MagWLED-1 with a specific idle animation they want back when streaming stops, the driver's shutdown path must restore the prior segment state, not leave it black. Plan 14-03 shutdown-off frame should snapshot-and-restore, not clobber.
