# Feature Research: Backglow LED Control

**Domain:** VR facial-interface LED illumination with VRChat world/avatar control
**Researched:** 2026-04-05
**Confidence:** MEDIUM (avatar OSC path is well-documented; world-to-hardware path has confirmed gaps)

## VRChat-to-Hardware Control: Feasibility Matrix

Before the feature landscape, this matrix answers the critical question: how can VRChat worlds/avatars drive external hardware?

| Control Path | Feasibility | Confidence | Notes |
|---|---|---|---|
| **Avatar OSC parameters -> local app -> LEDs** | CONFIRMED WORKS | HIGH | Proven pattern (Patstrap, HapticPatPat, GiggleTech). Avatar contact receivers set animator params, VRChat outputs param changes via OSC on localhost:9001. Local bridge app reads OSC, drives hardware. |
| **World Udon -> OSC** | NOT AVAILABLE | HIGH | Udon has no OSC send/receive API. Community feature request open since 2022, not shipped. Worlds cannot send OSC. |
| **World Udon -> HTTP/WebSocket** | NOT AVAILABLE | HIGH | Udon has no HTTP request or WebSocket API. Only hacky workarounds exist (log-file parsing + MIDI injection). Not viable for production. |
| **World Udon -> MIDI** | AVAILABLE BUT LIMITED | MEDIUM | Udon supports MIDI output. Could theoretically drive a local MIDI listener that bridges to LEDs. Very constrained data bandwidth. |
| **World -> synced avatar param -> OSC** | POSSIBLE WORKAROUND | MEDIUM | World sets a synced avatar parameter via animator/contacts. Parameter change propagates to OSC output. Indirect, requires avatar setup. |
| **OpenVR input components (app writes to driver)** | NOT POSSIBLE | HIGH | IVRDriverInput components are driver-to-app only. Apps cannot write values to input components. Only haptic output components allow app-to-driver flow, limited to vibration events (frequency, amplitude, duration). |
| **OpenVR haptic component abuse** | TECHNICALLY POSSIBLE | LOW | Could create haptic component, app sends haptic events, driver reinterprets frequency/amplitude as LED data. Extremely hacky, unreliable, not recommended. |
| **Named pipe / CLI from local app** | CONFIRMED WORKS | HIGH | Existing bey-closer pattern. Any local process can send pipe commands. The question is what triggers the local process. |
| **Standalone local app (non-VRChat)** | CONFIRMED WORKS | HIGH | Desktop app with color picker / presets / reactive modes. No VRChat dependency. |

### Confirmed Architecture: Avatar OSC Bridge Pattern

The proven pattern used by all VRChat hardware projects (haptics, shockers, LEDs):

```
VRChat Avatar                    Local PC                      Hardware
+-------------------+    OSC    +------------------+   USB/   +----------+
| Contact Receivers |---------->| Bridge App       |-- WiFi ->| ESP32    |
| set animator      | localhost | Listens 9001     |   DDP/   | WLED     |
| parameters        | port 9001| Maps params to   |   JSON   | WS2812B  |
| (Float 0.0-1.0)  |          | LED commands     |          |          |
+-------------------+    OSC    +------------------+          +----------+
                    <---------- |                  |
                     port 9000  | Can also SET     |
                     (optional) | avatar params    |
                                +------------------+
```

**How world control works despite no Udon OSC:**
1. World creator places trigger volumes / interactables in world
2. These interact with the player's avatar contact receivers
3. Contact receivers set avatar animator parameters (Float 0.0-1.0)
4. VRChat automatically outputs changed params via OSC to localhost:9001
5. Bridge app reads OSC, translates to LED commands, sends to hardware

This is how "VRChat worlds that control pianos" work -- the world interacts with avatar contacts, which trigger OSC output. The world never sends OSC directly.

## Feature Landscape

### Table Stakes (Users Expect These)

| Feature | Why Expected | Complexity | Notes |
|---------|--------------|------------|-------|
| Static color setting via CLI | Basic functionality test, manual control | LOW | Named pipe command `backglow rgb 255 0 128`. Extends existing pipe server. Trivial. |
| Global brightness control | Safety (eye proximity), usability | LOW | Master brightness ceiling (0-255). WLED supports master `bri` in JSON API. Must be clamped in driver, not just WLED-side. |
| Per-LED RGB addressability | 10 LEDs around face need independent colors for directional effects | MEDIUM | WLED JSON API `seg.i` property supports per-index color. 10 LEDs is well within limits. |
| USB-first communication | Prototype uses USB-connected ESP32-C3. WiFi adds latency/config burden | MEDIUM | WLED serial interface supports JSON API over serial at 115200 baud. For 10 LEDs this is abundant. GPIO3 must not be allocated to LED output for serial to work -- verify MagWLED-1 pin assignments. |
| Off/black state on exit | LEDs must turn off when driver unloads or VR session ends | LOW | Send `{"on":false}` on Cleanup(). Critical for not leaving face LEDs glowing after VR. |
| Brightness safety ceiling | LEDs are millimeters from eyes. Must enforce max brightness | LOW | Driver-side clamp before any command reaches WLED. Non-negotiable safety feature. |

### Differentiators (Competitive Advantage)

| Feature | Value Proposition | Complexity | Notes |
|---------|-------------------|------------|-------|
| VRChat avatar OSC bridge | World/avatar-reactive facial glow -- no other headset does this | HIGH | Requires: avatar parameter design, OSC listener in driver or companion app, parameter-to-LED mapping logic. This IS the product. |
| World-driven color zones | World places colored lights/triggers, face illuminates to match | MEDIUM | Depends on avatar OSC bridge. World creator places contact senders; avatar has receivers mapped to backglow params (R, G, B floats or zone params). |
| DDP/UDP fallback for WiFi | Low-latency wireless control when USB not connected | MEDIUM | DDP on port 4048. Single packet carries 480 RGB pixels (10 LEDs = trivial). Good for untethered/wireless VR future. |
| Preset/effect system | Named presets (campfire, underwater, alert) via CLI | LOW | Map preset names to WLED JSON payloads. Store as config. Nice for demos. |
| Real-time color streaming | Smooth animated transitions, not just static colors | MEDIUM | Requires frame-rate loop sending DDP/JSON updates. 10 LEDs at 30fps is ~900 bytes/sec over serial -- trivial bandwidth. |
| Avatar-based self-control | User controls own backglow via avatar menu toggles | MEDIUM | Avatar menu toggles -> animator params -> OSC -> bridge. User doesn't need external UI. |

### Anti-Features (Commonly Requested, Often Problematic)

| Feature | Why Requested | Why Problematic | Alternative |
|---------|---------------|-----------------|-------------|
| Screen color sampling (ambilight) | "Match headset display colors to face LEDs" | Requires screen capture from VR compositor, massive complexity, privacy concerns, high latency. VR Ambilight project exists for Valve Index but requires Prismatik + Arduino + custom scripts. | World/avatar-driven colors are more intentional and lower latency. |
| OpenVR input component for app control | "Let any SteamVR app control LEDs" | IVRDriverInput is read-only from app side. No writable channel exists except haptics (unsuitable). Would require SteamVR API changes Valve won't make. | Named pipe API for any local app. OSC bridge for VRChat specifically. |
| WiFi-only control (no USB) | "Wireless is cleaner" | WiFi adds network config complexity, latency, discovery problems. ESP32-C3 USB is already connected for power. | USB-first with WiFi/DDP as optional fallback. |
| Custom WLED firmware | "Add special backglow protocol to firmware" | Breaks upgradability, adds maintenance burden, requires ESP32 toolchain. | Use stock WLED APIs (JSON, DDP, serial). Protocol-independent core. |
| Per-LED brightness control | "Individual LED brightness, not just color" | WLED JSON API individual LED control (`seg.i`) does not support per-pixel brightness -- documented limitation. Brightness is per-segment or global only. Individual LEDs set via `seg.i` have known dimness bugs. | Encode brightness in RGB values (darker color = dimmer). Use global brightness for overall intensity. |
| Direct Udon world networking | "World sends HTTP to local bridge" | Udon has no HTTP/WebSocket API. Only hacks (log parsing + MIDI). Fragile, version-dependent, bad UX. | Avatar contact receiver pattern is the sanctioned path. |

## Feature Dependencies

```
[Brightness Safety Ceiling]
    +--required-by--> [ALL LED control features]

[USB Serial Communication]
    +--required-by--> [Static Color Setting]
    +--required-by--> [Per-LED RGB Control]
    +--required-by--> [Real-time Streaming]

[Named Pipe Extension]
    +--required-by--> [CLI Color Commands]
    +--required-by--> [Preset System]
    +--required-by--> [OSC Bridge Integration]

[OSC Listener (avatar params)]
    +--required-by--> [VRChat Avatar Bridge]
    +--required-by--> [World-driven Color Zones]
    +--required-by--> [Avatar Self-control]

[VRChat Avatar Bridge]
    +--requires--> [OSC Listener]
    +--requires--> [USB Serial Communication]
    +--requires--> [Parameter-to-LED Mapping]

[DDP/WiFi Fallback]
    +--enhances--> [USB Serial Communication]
    +--independent-of--> [OSC Bridge]

[Static Color Setting] --precedes--> [Real-time Streaming]
[Real-time Streaming] --precedes--> [VRChat Avatar Bridge]
```

### Dependency Notes

- **Brightness Safety Ceiling required by all**: Every path that sets LED values must clamp through the safety ceiling. Build first.
- **USB Serial Communication is foundation**: All prototype hardware control flows through serial. Must be solid before any features on top.
- **OSC Listener is the VRChat gateway**: Without it, no VRChat integration. But it's a separate concern from LED control -- can be built independently.
- **Avatar bridge requires avatar design**: Not just code -- requires Unity avatar prefab with contact receivers mapped to OSC parameters. Documentation/template needed for world creators.

## MVP Definition

### Launch With (v3.0 Core)

- [ ] USB serial communication to WLED (JSON API over serial) -- foundation
- [ ] Brightness safety ceiling (driver-side max clamp) -- safety
- [ ] Static color via named pipe (`backglow rgb R G B`, `backglow off`) -- basic control
- [ ] Per-LED color via named pipe (`backglow led <index> R G B`) -- addressability
- [ ] Global brightness via named pipe (`backglow brightness <0-255>`) -- usability
- [ ] Auto-off on driver cleanup -- safety
- [ ] WiFi/DDP fallback path -- for wireless VR scenarios

### Add After Validation (v3.x)

- [ ] OSC listener for VRChat avatar parameters -- VRChat bridge (trigger: core LED control working)
- [ ] Parameter-to-LED mapping config -- how avatar params map to which LEDs/colors
- [ ] Reference avatar prefab with backglow contact receivers -- for world creators to target
- [ ] Preset/effect system -- named configurations
- [ ] Real-time color streaming at target framerate -- smooth transitions

### Future Consideration (v4+)

- [ ] World creator SDK/documentation -- enabling ecosystem
- [ ] Multi-headset sync (multiple users with backglow in same world)
- [ ] Alternative hardware backends (non-WLED, custom protocols)
- [ ] Screen color sampling (ambilight mode) -- if demand exists

## Feature Prioritization Matrix

| Feature | User Value | Implementation Cost | Priority |
|---------|------------|---------------------|----------|
| USB serial to WLED | HIGH | MEDIUM | P1 |
| Brightness safety ceiling | HIGH (safety) | LOW | P1 |
| Static color CLI | HIGH | LOW | P1 |
| Per-LED CLI control | HIGH | LOW | P1 |
| Auto-off on exit | HIGH (safety) | LOW | P1 |
| Global brightness CLI | MEDIUM | LOW | P1 |
| DDP/WiFi fallback | MEDIUM | MEDIUM | P1 |
| OSC avatar bridge | HIGH | HIGH | P2 |
| Param-to-LED mapping | HIGH | MEDIUM | P2 |
| Reference avatar prefab | HIGH | MEDIUM | P2 |
| Preset system | MEDIUM | LOW | P2 |
| Real-time streaming | MEDIUM | MEDIUM | P2 |
| World creator docs | MEDIUM | LOW | P3 |

## Prior Art Analysis

| Project | What It Does | Relevance |
|---------|-------------|-----------|
| **VR Ambilight** (Arduino blog, 2023) | WS2812B strips inside Valve Index, samples screen edge colors via Prismatik, Arduino Nano control | Closest prior art for face-mounted LEDs in VR. Different goal (peripheral immersion vs transscleral effect). Proves WS2812B-in-headset is viable. |
| **Patstrap / HapticPatPat** | ESP32 + motors for VRChat haptic feedback via avatar OSC | Proves avatar OSC -> local bridge -> ESP32 hardware pattern. Exact architecture we need. |
| **GiggleTech OSC Router** | OSC router for VRChat hardware control (LED + haptics) | OSC routing middleware. May be usable as-is or as reference. |
| **OpenShock / ShockOSC** | VRChat avatar -> shock collar control via OSC | Another avatar OSC -> hardware bridge. Shows the pattern scales to various devices. |
| **VR Stage Lighting** | DMX-style stage lighting in VRChat worlds | In-world virtual lighting, not physical hardware. But shows world lighting design patterns. |

## WLED Control Interface Summary

For the prototype MagWLED-1 (ESP32-C3 + WLED), these are the viable control paths:

| Interface | Transport | Latency | Per-LED | Best For |
|-----------|-----------|---------|---------|----------|
| JSON API over Serial | USB 115200 baud | ~10ms | Yes (`seg.i`) | Primary USB control. 10 LEDs trivially fits bandwidth. |
| JSON API over HTTP | WiFi TCP | ~50-100ms | Yes (`seg.i`) | Fallback, debugging, web UI |
| DDP over UDP | WiFi UDP | ~5ms | Yes (raw RGB) | Low-latency WiFi streaming |
| Adalight over Serial | USB 115200 baud | ~5ms | Yes (raw RGB) | Alternative to JSON for streaming |

**Recommendation:** JSON API over serial for command-based control (set color, set brightness). DDP over UDP for low-latency WiFi streaming fallback. Both support per-LED control for 10 LEDs.

**Key WLED JSON API commands for backglow:**
```json
{"on":true,"bri":128}                          // master brightness
{"seg":{"i":[0,"FF0000",1,"00FF00"]}}          // per-LED by index
{"seg":{"i":["FF0000","00FF00","0000FF"]}}     // sequential LEDs
{"on":false}                                    // off
{"v":true}                                      // query state
```

**Master brightness vs segment brightness:**
- `bri` at root level: master brightness (0-255), affects all output
- `bri` in segment: per-segment brightness (0-255), multiplicative with master
- Individual LED colors set via `seg.i` do NOT have per-pixel brightness -- use darker RGB values instead

**Serial constraint:** GPIO3 must not be allocated to LED output, or serial RX is unavailable. Verify MagWLED-1 pin assignments. Serial baud rate 115200 is sufficient for ~50-100 LEDs at reasonable refresh rates; 10 LEDs is trivial.

**WLED individual LED known issue:** Setting individual LEDs via `seg.i` can appear dimmer than expected (GitHub issue #2549). Turning on LEDs from off state and setting individual colors in the same request does NOT work -- must set brightness/on state first.

## Sources

- [VRChat OSC Avatar Parameters](https://docs.vrchat.com/docs/osc-avatar-parameters) -- HIGH confidence, official docs
- [VRChat OSC Overview](https://docs.vrchat.com/docs/osc-overview) -- HIGH confidence, official docs
- [VRChat Contacts](https://creators.vrchat.com/common-components/contacts/) -- HIGH confidence, official creation docs
- [VRChat Udon OSC feature request](https://feedback.vrchat.com/udon/p/osc-for-worlds-udon) -- confirms Udon OSC not available
- [VRChat Udon Web API request](https://feedback.vrchat.com/udon/p/vrchat-udon-web-api) -- confirms no HTTP in Udon
- [WLED JSON API](https://kno.wled.ge/interfaces/json-api/) -- HIGH confidence, official WLED docs
- [WLED Serial Interface](https://kno.wled.ge/interfaces/serial/) -- HIGH confidence, official WLED docs
- [WLED DDP Protocol](https://kno.wled.ge/interfaces/ddp/) -- HIGH confidence, official WLED docs
- [WLED individual LED brightness issue #2549](https://github.com/Aircoookie/WLED/issues/2549) -- known limitation
- [OpenVR IVRDriverInput](https://github.com/ValveSoftware/openvr/wiki/IVRDriverInput-Overview) -- HIGH confidence, Valve wiki
- [OpenVR Driver API Docs](https://github.com/ValveSoftware/openvr/blob/master/docs/Driver_API_Documentation.md) -- HIGH confidence
- [Patstrap project](https://github.com/danielfvm/Patstrap) -- avatar OSC -> ESP32 reference implementation
- [HapticPatPat](https://github.com/kikookraft/HapticPatPat) -- avatar OSC -> ESP32 reference implementation
- [GiggleTech OSC Router](https://github.com/giggletech/GiggleTech_OSC_Router) -- OSC routing reference
- [OpenShock VRChat Setup](https://wiki.openshock.org/guides/shockosc/avatar-setup-vrc) -- avatar OSC hardware bridge
- [VR Ambilight](https://blog.arduino.cc/2023/07/10/add-peripheral-lighting-to-improve-vr-immersion/) -- MEDIUM confidence, prior art for face LEDs
- [wled-json-api-over-serial](https://github.com/miwied/wled-json-api-over-serial) -- reference for serial JSON control
- [DDP Protocol Spec](http://www.3waylabs.com/ddp/) -- official protocol spec

---
*Feature research for: Backglow LED Control (bey-closer v3.0)*
*Researched: 2026-04-05*
