# Pitfalls Research

**Domain:** Backglow LED control for SteamVR driver (VRChat world-driven, WLED/ESP32-C3 prototype hardware)
**Researched:** 2026-04-05
**Confidence:** HIGH (verified against official WLED docs, ESP-IDF docs, VRChat creator docs, OpenVR patterns from existing codebase)

## Critical Pitfalls

### Pitfall 1: Blocking Serial Writes in RunFrame Kill VR

**What goes wrong:**
Serial `WriteFile` to a COM port blocks when the ESP32-C3 USB buffer is full or the device disconnects mid-write. Since `RunFrame()` executes on vrserver's main thread, a blocking write stalls **all** SteamVR driver processing -- tracking, input, display timing. Even a 50ms stall causes visible judder; a hang kills the VR session.

**Why it happens:**
The ESP32-C3's USB Serial/JTAG controller has a small internal buffer. When full, the ESP32 pauses for 50ms waiting for the host to drain it. If the device enters sleep or USB disconnects, `WriteFile` can block indefinitely without overlapped I/O. Developers often test with fast USB and small payloads, never hitting the buffer-full case until real use.

**How to avoid:**
- Never call `WriteFile` (or any serial I/O) from `RunFrame()`. Use a dedicated writer thread with a lock-free command queue (single-producer/single-consumer ring buffer).
- Open the COM port with `FILE_FLAG_OVERLAPPED` and use `WaitForSingleObject` with timeouts (never INFINITE).
- Set `SetCommTimeouts` with `WriteTotalTimeoutConstant` = 50ms max.
- The writer thread should be fire-and-forget: if a write times out, drop the frame, log it, and move on. LED color is not safety-critical.

**Warning signs:**
- Any `CreateFile` of `\\\\.\\COMx` without `FILE_FLAG_OVERLAPPED` in code that runs on the RunFrame thread.
- `WriteFile` calls without corresponding timeout configuration.
- VR judder that correlates with LED activity.

**Phase to address:**
Phase 1 (USB serial spike) -- must validate non-blocking I/O pattern before building on it.

---

### Pitfall 2: COM Port Enumeration and Device Identity Fragility

**What goes wrong:**
COM port numbers are assigned dynamically by Windows and change when the ESP32 is plugged into a different USB port, after Windows Update, or if another serial device is installed. Hardcoding `COM3` or even persisting a COM number in config breaks when the user replugs their headset. The driver either connects to the wrong device or fails to find the ESP32 at all.

**Why it happens:**
Windows assigns COM port numbers sequentially and caches them per USB port path. ESP32-C3 native USB CDC devices all share the same generic VID/PID (303A:1001) unless custom descriptors are flashed. There is no reliable way to distinguish one ESP32-C3 from another without a unique serial number in the USB descriptor, which stock WLED does not set.

**How to avoid:**
- Enumerate COM ports using `SetupDiGetClassDevs` with GUID_DEVINTERFACE_COMPORT. Match by VID/PID (Espressif 303A:1001 for native CDC, or the specific USB-UART bridge chip VID/PID like CP2102/CH340).
- If multiple matches exist, try each one: send WLED version query (`v` byte = 0x76) and verify the response contains WLED version string.
- Store the USB instance path (not COM number) in VRSettings as a fallback hint, but always re-enumerate on startup.
- For COM ports above COM9, use the `\\\\.\\COM10` prefix format (this is a known Windows API requirement).

**Warning signs:**
- Config file or VRSettings containing a raw COM number like `"port": "COM3"`.
- Device discovery that works on the developer's machine but fails for testers.
- No retry/re-enumeration logic after initial connection.

**Phase to address:**
Phase 1 (USB serial spike) -- device discovery is foundational.

---

### Pitfall 3: WLED JSON Over Serial Drops Commands Silently

**What goes wrong:**
WLED's JSON API over serial is unreliable at the default 115200 baud rate. Not all JSON objects arrive at the MCU -- the ESP32 may parse a truncated JSON payload and either ignore it or apply a partial state. There is no acknowledgment protocol: the sender has no way to know the command was received, let alone applied. At higher baud rates, reliability improves but is still not guaranteed for large payloads.

**Why it happens:**
The ESP32-C3's USB Serial/JTAG controller has a small internal buffer (undocumented exact size, estimated 64-256 bytes based on USB CDC spec). JSON payloads for per-LED color (`{"seg":[{"i":[[r,g,b],[r,g,b],...]}]}`) can easily exceed buffer capacity for 10 LEDs. The WLED serial parser processes incoming bytes at the main loop rate (~42 FPS internal), and if bytes arrive faster than parsing, they're dropped.

**How to avoid:**
- Use **TPM2 binary protocol** instead of JSON for per-LED color data. TPM2 uses 2-4x less bandwidth than JSON, has a defined packet structure with header/length, and WLED handles it reliably at 115200 baud for 10 LEDs (30 bytes of color data + 5 bytes header = 35 bytes, well under any buffer limit).
- Reserve JSON for infrequent config commands (brightness ceiling, effect mode) sent with inter-command delays.
- If JSON is required, send small payloads (one property at a time) with a minimum 20ms gap between commands.
- At 115200 baud, 10 LEDs via TPM2 = 35 bytes = ~3ms transfer time. Plenty of headroom for 40+ FPS updates.

**Warning signs:**
- LEDs sometimes not responding to color changes, especially when multiple properties change simultaneously.
- Using JSON API for realtime color streaming.
- No inter-command delay between serial JSON writes.

**Phase to address:**
Phase 1 (USB serial spike) -- protocol choice is a foundational decision.

---

### Pitfall 4: VRChat-to-LED Pipeline Has No Direct Path

**What goes wrong:**
Developers assume VRChat world Udon scripts can directly communicate with external hardware or local applications. They cannot. VRChat's Udon sandbox has no filesystem access, no socket API, no HTTP client, and no OSC send capability from worlds. The only outbound data paths from VRChat are: (1) OSC output of **avatar parameters** (not world state), and (2) synced player parameters visible to other clients. There is no world-to-localhost communication channel.

**Why it happens:**
VRChat's security sandbox prevents worlds from executing arbitrary code or network calls on the user's machine. OSC is avatar-scoped, not world-scoped. Developers familiar with Unity networking assume Udon has similar capabilities.

**How to avoid:**
The VRChat-to-LED pipeline requires an **indirect path**:
1. **World drives avatar parameters** via Udon (e.g., world script sets synced float parameters on the local player's avatar using `SetAnimatorFloat` or parameter drivers).
2. **VRChat outputs those avatar parameters via OSC** to localhost (port 9000 by default).
3. **A bridge process** (separate from the SteamVR driver) listens for OSC messages and translates them to LED commands.
4. **Bridge sends commands** to the SteamVR driver via named pipe, or directly to the WLED device via serial/UDP.

Alternative: Avatar-based control where the user's avatar has dedicated parameters that a companion app (like VRCOSC) maps to LED commands. This is simpler but requires per-avatar setup.

Key constraint: avatar parameters are limited to 256 bits synced (but up to 8192 unsynced). For 10 LEDs x RGB = 30 bytes = 240 bits, this barely fits synced. Use unsynced parameters to avoid the sync budget.

**Warning signs:**
- Architecture diagrams showing a direct arrow from "VRChat World" to "LED Controller."
- Attempting to use Udon's networking features for local hardware control.
- No bridge/middleware component in the design.

**Phase to address:**
Early architecture phase -- this shapes the entire control pipeline. Must be resolved before implementing any VRChat integration.

---

### Pitfall 5: WS2812B Thermal and Power Risk in Facial Interface

**What goes wrong:**
10x WS2812B LEDs at full brightness (white) draw 60mA each = 600mA total at 5V = 3W of heat dissipated directly against the user's face. The facial interface foam is an insulator, trapping heat. Users report discomfort within minutes. At sustained full brightness, the foam/silicone can degrade. Additionally, 600mA may exceed some USB port current limits (500mA for USB 2.0).

**Why it happens:**
WS2812B LEDs are 60mA per LED at max brightness (20mA per color channel x 3). Full white = all channels max. Backglow use cases rarely need full brightness -- transscleral illumination is a subtle ambient effect, not a spotlight. But without a firmware or software brightness ceiling, a buggy world script or direct API call can set all LEDs to (255, 255, 255).

**How to avoid:**
- **Hardware brightness ceiling**: Set WLED's brightness limit to 40-60 (out of 255) as a firmware-level cap. At brightness 50, power drops to ~0.6W for 10 LEDs -- warm but safe.
- **Software brightness ceiling**: In the driver, clamp all incoming brightness/color values before forwarding to WLED. This is the defense-in-depth layer.
- **Named pipe brightness cap command**: Let users set their own max via CLI, stored in VRSettings, enforced by the driver regardless of what VRChat sends.
- **Default-off**: LEDs should be off at startup. Only turn on when explicitly commanded. Never auto-enable.
- **USB power budget**: At brightness 50, total draw is ~120mA (LEDs) + ~50mA (ESP32) = ~170mA. Well within USB 2.0 limits.

**Warning signs:**
- No brightness ceiling in the design.
- Testing only with dim colors, never full white sustained.
- No user-configurable max brightness.
- Drawing power from a USB 2.0 port without budgeting.

**Phase to address:**
Phase 1 (spike) for hardware validation, enforced in every subsequent phase. Brightness ceiling must be one of the first things implemented.

---

### Pitfall 6: ESP32-C3 USB Disconnect Crashes the Driver

**What goes wrong:**
When the ESP32-C3 disconnects (user unplugs USB, ESP32 resets, enters sleep, or USB bus reset from the host), pending serial operations fail. If the driver doesn't handle `ERROR_GEN_FAILURE`, `ERROR_DEVICE_NOT_CONNECTED`, or `ERROR_OPERATION_ABORTED` from `WriteFile`/`ReadFile`, it either crashes vrserver.exe or enters a tight retry loop that burns CPU.

The ESP32-C3's native USB CDC is particularly prone to this: deep-sleep causes full USB disconnect, and light-sleep makes the controller "unresponsive" which the host may interpret as a device error. WLED's normal operation shouldn't trigger sleep, but a firmware crash or OTA update will.

**Why it happens:**
Serial port handles become invalid on device disconnect, but Windows doesn't automatically signal all pending I/O. Overlapped I/O may complete with errors that aren't checked. The existing codebase's HID reconnect pattern (from `HidDevice`) handles HID disconnects gracefully, but serial port error codes differ from HID error codes.

**How to avoid:**
- Model serial device management after the existing `HidDevice` pattern: background thread with reconnect loop, clean handle teardown, and state machine (Disconnected -> Connecting -> Connected -> Error -> Disconnected).
- Handle these specific error codes from serial operations: `ERROR_GEN_FAILURE` (31), `ERROR_DEVICE_NOT_CONNECTED` (1167), `ERROR_OPERATION_ABORTED` (995), `ERROR_BAD_COMMAND` (22).
- On any serial error, close the handle, wait 1-2 seconds, re-enumerate COM ports, and attempt reconnection.
- Never let a serial error propagate to `RunFrame()` or any vrserver thread. The serial thread catches all errors internally.
- Log disconnects at INFO level, not ERROR, since USB disconnects are expected during normal use (user unplugging headset).

**Warning signs:**
- Serial error handling that only checks for `ERROR_SUCCESS` vs "everything else."
- No reconnect logic -- assumes the device is always present.
- Serial code that doesn't reference the HidDevice reconnect pattern.

**Phase to address:**
Phase 1 (USB serial spike) -- reconnect resilience is part of the foundational serial layer.

---

### Pitfall 7: Coupling to WLED Makes Hardware Swaps Painful

**What goes wrong:**
Building the driver's LED control logic directly against WLED's JSON API or Adalight/TPM2 protocol means every function call, every data format, every baud rate assumption is WLED-specific. When the prototype evolves (custom firmware, different LED controller, direct SPI, different MCU), the entire LED control stack needs rewriting.

**Why it happens:**
WLED is convenient for prototyping: it handles LED timing, provides WiFi fallback, has a web UI for debugging. Developers build "just the prototype" but the prototype's protocol assumptions leak into every layer.

**How to avoid:**
- Define an internal **LED command interface** (abstract C++ class or simple function table): `SetPixels(rgb_array, count)`, `SetBrightness(uint8_t)`, `GetStatus()`. This is the "core" layer.
- Implement `WledSerialBackend` as one concrete backend behind this interface. It handles TPM2 framing, baud rate, COM port management.
- The driver, named pipe, and VRChat bridge all talk to the abstract interface, never to WLED directly.
- When hardware changes, only the backend implementation changes. The interface, pipe commands, and bridge protocol stay the same.
- PROJECT.md already calls for "core/prototype architecture separation" -- enforce this from day one.

**Warning signs:**
- `#include "wled.h"` or WLED-specific constants (baud rates, JSON keys) in driver code outside the backend module.
- Named pipe commands that expose WLED-specific concepts (segment IDs, effect numbers).
- No abstract interface between driver logic and hardware communication.

**Phase to address:**
Architecture phase -- interface definition must precede any WLED-specific implementation.

---

## Technical Debt Patterns

| Shortcut | Immediate Benefit | Long-term Cost | When Acceptable |
|----------|-------------------|----------------|-----------------|
| Hardcoded COM port | Fast prototype testing | Breaks on every other machine | Never in shipped code; spike only |
| JSON API for all serial commands | Easy to debug (human-readable) | Unreliable at speed, high bandwidth | Config-only commands (not per-frame color) |
| No brightness ceiling | Simplifies early testing | Thermal risk, user safety issue | Never -- implement ceiling in first working build |
| Monolithic RunFrame with serial calls | Fewer files, less threading complexity | VR judder, potential vrserver crash | Never -- threading from day one |
| WLED API in driver core | Faster initial implementation | Rewrite when hardware changes | Spike only; refactor before main implementation |
| Synchronous OSC listener in bridge | Simple bridge implementation | Missed messages under load | Acceptable for MVP if update rate is low (<10 Hz) |
| Single-process bridge (no separate .exe) | Less to deploy | Serial contention if driver and bridge both open COM port | Never -- either bridge talks to driver via pipe, or bridge owns serial exclusively |

## Integration Gotchas

| Integration | Common Mistake | Correct Approach |
|-------------|----------------|------------------|
| WLED Serial (TPM2) | Sending packets faster than ESP32 can process (~42 FPS internal limit) | Rate-limit sender to 30 FPS max; WLED's internal rendering caps at ~42 FPS |
| WLED Serial (JSON) | Sending large JSON payloads without inter-command delay | Send one property per command, 20ms+ gap between commands |
| WLED Baud Rate | Changing baud rate with byte command and expecting it to persist | Persistent baud rate requires Config > Sync Interfaces setting; byte command (0xB0-0xB7) resets on reboot |
| WLED Baud Rate (Improv) | Changing baud from 115200 breaks Improv device detection | Keep 115200 as startup baud; switch to higher speed after detection if needed |
| VRChat OSC | Assuming worlds can send OSC directly | Only avatar parameter changes emit OSC; worlds must drive avatar params via parameter drivers |
| VRChat OSC | Listening on wrong port or not enabling OSC in VRChat settings | OSC must be enabled in VRChat Action Menu; default ports 9000 (receive from VRC) / 9001 (send to VRC) |
| VRChat Avatar Params | Using synced parameters for LED data, burning sync budget | Use unsynced parameters for LED control (no 256-bit limit, up to 8192 total params) |
| VRChat Networking | Assuming Udon can make HTTP/socket calls | Udon has no outbound network API; only VRChat's own sync and OSC output exist |
| Windows COM Port | Opening COM10+ as "COM10" instead of `\\\\.\\COM10` | Always use `\\\\.\\COMx` prefix format for all port numbers |
| ESP32-C3 CDC | Assuming device stays enumerated during firmware OTA or crash | WLED OTA causes USB disconnect; driver must handle graceful reconnect |
| Named Pipe Extension | Adding LED commands that conflict with existing proximity/IPD commands | Namespace LED commands (e.g., `backglow set ...`, `backglow status`) to avoid collision |
| Serial Port Locking | Opening COM port from both driver and a debug tool simultaneously | COM ports are exclusive-access; only one process can open at a time. Driver must close port for user to debug via WLED web/serial |

## Performance Traps

| Trap | Symptoms | Prevention | When It Breaks |
|------|----------|------------|----------------|
| Serial write in RunFrame | VR judder correlating with LED activity | Dedicated writer thread with command queue | Immediately on first buffer-full event |
| Polling COM port existence every RunFrame | CPU spike from SetupDi enumeration | Enumerate once on init + on disconnect; use RegisterDeviceNotification for hotplug | With multiple serial devices installed |
| Unbounded OSC listener queue | Memory growth, increasing latency over time | Ring buffer with fixed capacity; drop oldest on overflow | After sustained VRChat session (>30 min) |
| JSON parsing on serial thread | Delays between LED frames | Parse JSON rarely (status queries only); use binary protocol for color data | When adding status readback features |
| Full COM port scan on every reconnect attempt | 2-3 second freeze during enumeration of COM1-COM256 | Cache last-known port, try it first; only full scan on cache miss | When user has many virtual COM ports (Bluetooth, etc.) |
| Sending LED updates when color hasn't changed | Wasted serial bandwidth, unnecessary ESP32 processing | Dirty-flag: only send when color state actually changes | Not catastrophic but wasteful; matters at higher LED counts |

## Safety Considerations

| Concern | Risk | Prevention |
|---------|------|------------|
| No brightness ceiling | 3W heat against face at full white, discomfort | WLED firmware limit + software clamp + user-configurable max |
| LED failure mode (stuck on last color) | If ESP32 crashes, WS2812B LEDs retain last color indefinitely | WLED has configurable auto-off timeout. Driver should send "all off" on Cleanup(). Consider watchdog: if no command in 5s, ESP32 dims to zero |
| USB power draw spike | 600mA at full white may exceed USB 2.0 500mA limit | Brightness ceiling of ~80/255 keeps total under 500mA including ESP32 |
| Eye exposure from light leaking past foam | Bright LEDs in peripheral vision cause discomfort | Brightness ceiling of 50/255 makes this ambient glow, not a light source. Default to OFF |
| ESP32 thermal in enclosed space | ESP32-C3 in facial interface adds heat | ESP32-C3 at idle/light load is ~50mW. Internal temp sensor readable for monitoring |

## UX Pitfalls

| Pitfall | User Impact | Better Approach |
|---------|-------------|-----------------|
| LEDs on by default at startup | Unexpected light in face when putting on headset | Default OFF; only activate when VRChat world or user explicitly enables |
| No indication of connection status | User can't tell if LED controller is connected | `backglow status` pipe command returns device state, port, firmware version |
| Brightness ceiling too low by default | Effect invisible; user thinks it's broken | Default ceiling at 50/255 (~20%) which is visible but comfortable. Allow user override |
| Color format confusion | RGB vs GRB vs HSV -- user sets color and gets wrong result | Driver accepts standard RGB everywhere; GRB conversion handled internally in WLED backend only |
| No graceful degradation when ESP32 absent | Error spam in logs, potential crash | If no LED device found, log once and disable backglow silently. Re-check on hotplug |
| Complex setup for VRChat control | User needs avatar params + OSC + bridge -- too many steps | Provide clear setup guide; consider bundling bridge with installer |

## "Looks Done But Isn't" Checklist

- [ ] **Serial connection:** Often missing reconnect-on-disconnect -- verify device unplug/replug recovers automatically within 5 seconds
- [ ] **Brightness ceiling:** Often only in WLED firmware, not enforced by driver -- verify driver clamps values before sending
- [ ] **VRChat pipeline:** Often missing the bridge component -- verify full chain from Udon parameter change to LED color change end-to-end
- [ ] **Multi-device:** Often assumes single serial device -- verify behavior when multiple ESP32s are connected (should pick correct one or let user choose)
- [ ] **Startup race:** Often assumes LED controller is ready before driver Init -- verify graceful startup when ESP32 powers on after vrserver
- [ ] **OTA survival:** Often missing -- verify WLED firmware update (USB disconnect + reconnect, possibly new COM port) doesn't permanently break the driver
- [ ] **Thread safety:** Often missing -- verify that named pipe commands and RunFrame don't race on shared LED state
- [ ] **Clean shutdown:** Often missing -- verify LEDs turn off when SteamVR exits (Cleanup called), not left in last color state
- [ ] **WiFi fallback:** If WiFi/DDP fallback is implemented, verify it works when USB is also connected (WLED handles both simultaneously but priority matters)
- [ ] **Latency budget:** Often unmeasured -- verify end-to-end latency from OSC receive to LED change is under 100ms

## Recovery Strategies

| Pitfall | Recovery Cost | Recovery Steps |
|---------|---------------|----------------|
| Blocking serial in RunFrame | MEDIUM | Refactor to writer thread; requires adding thread + queue + synchronization |
| Hardcoded COM port | LOW | Replace with enumeration; 1-2 hours of work |
| No brightness ceiling | LOW | Add clamp in one location; deploy firmware config change |
| WLED coupling in driver core | HIGH | Extract interface, move WLED specifics behind it; touches every call site |
| JSON for realtime colors | MEDIUM | Switch to TPM2; requires binary framing code but protocol is simple |
| Missing VRChat bridge | HIGH | Architectural addition; requires new process, OSC listener, protocol design |
| ESP32 disconnect crash | MEDIUM | Add error handling to all serial calls; model after existing HidDevice pattern |
| No clean shutdown | LOW | Add "all off" command in DeviceProvider::Cleanup(); single line of code |

## Pitfall-to-Phase Mapping

| Pitfall | Prevention Phase | Verification |
|---------|------------------|--------------|
| Blocking serial in RunFrame | Phase 1 (USB serial spike) | Confirm serial I/O on dedicated thread; RunFrame only reads shared state |
| COM port enumeration fragility | Phase 1 (USB serial spike) | Unplug ESP32, replug to different port, verify auto-reconnect |
| WLED JSON unreliability | Phase 1 (USB serial spike) | Send 1000 TPM2 frames at 30 FPS, verify zero drops on 10 LEDs |
| VRChat pipeline indirection | Architecture/design phase | Document full data path; identify bridge component; prototype OSC receive |
| Thermal/brightness safety | Phase 1 (spike) | Measure temperature at facial interface with LEDs at ceiling brightness for 30 min |
| ESP32-C3 USB disconnect | Phase 1 (USB serial spike) | Unplug ESP32 during active streaming; verify no crash, clean reconnect within 5s |
| WLED coupling | Architecture phase | Define abstract LED interface before implementing WLED backend |
| WiFi unreliability | Design decision phase | Document USB-first rationale; WiFi/DDP as optional fallback only |
| Latency budget | Architecture phase | Measure end-to-end: OSC -> pipe -> serial -> LED change. Target <100ms |
| Thread safety (pipe + RunFrame + serial) | Implementation phase | Code review for shared state; use atomic/mutex where RunFrame reads LED state |
| Clean shutdown | Implementation phase | Kill SteamVR; verify LEDs turn off within 1 second |

## Sources

- [WLED Serial Interface Documentation](https://kno.wled.ge/interfaces/serial/) -- protocol details, baud rates, Adalight/TPM2 support, JSON reliability (HIGH confidence)
- [WLED UDP Realtime Documentation](https://kno.wled.ge/interfaces/udp-realtime/) -- DDP/UDP protocol, timeout behavior (HIGH confidence)
- [WLED JSON API over serial issues (GitHub #2557)](https://github.com/Aircoookie/WLED/issues/2557) -- JSON command drop reports (MEDIUM confidence)
- [WLED Serial Protocol Limitations (Discourse)](https://wled.discourse.group/t/serial-protocol-limitations/11728) -- baud rate and bandwidth analysis (MEDIUM confidence)
- [ESP-IDF USB Serial/JTAG Console (ESP32-C3)](https://docs.espressif.com/projects/esp-idf/en/stable/esp32c3/api-guides/usb-serial-jtag-console.html) -- buffer behavior, sleep mode disconnects (HIGH confidence)
- [ESP32-C3 Serial Connection Setup](https://docs.espressif.com/projects/esp-idf/en/stable/esp32c3/get-started/establish-serial-connection.html) -- VID/PID, driver requirements (HIGH confidence)
- [VRChat Networking Specs](https://creators.vrchat.com/worlds/udon/networking/network-details/) -- Udon rate limits, 11KB/s bandwidth, serialization constraints (HIGH confidence)
- [VRChat OSC Avatar Parameters](https://docs.vrchat.com/docs/osc-avatar-parameters) -- OSC scope, parameter types, 256-bit sync limit (HIGH confidence)
- [VRChat OSC Overview](https://docs.vrchat.com/docs/osc-overview) -- port configuration, enable requirements (HIGH confidence)
- [WS2812B Power Consumption (PJRC)](https://www.pjrc.com/how-much-current-do-ws2812-neopixel-leds-really-use/) -- actual 60mA/LED measurement at full white (HIGH confidence)
- [USB 3.0 RF Interference on 2.4GHz (USB.org whitepaper)](https://www.usb.org/sites/default/files/327216.pdf) -- 20dB noise floor increase in 2.4GHz band (HIGH confidence)
- [Windows COM Ports above COM9 (Microsoft Support)](https://support.microsoft.com/en-us/topic/howto-specify-serial-ports-larger-than-com9-db9078a5-b7b6-bf00-240f-f749ebfd913e) -- `\\\\.\\COMx` prefix requirement (HIGH confidence)
- Existing codebase: `HidDevice` reconnect pattern, `DeviceProvider::RunFrame` architecture, named pipe server pattern (HIGH confidence -- local code)

---
*Pitfalls research for: Backglow LED control in SteamVR driver*
*Researched: 2026-04-05*
