# Phase 3: Proximity Component Spike - Research

**Researched:** 2026-03-21
**Domain:** OpenVR driver IVRDriverInput /proximity component, cross-device standby/wake, named pipe IPC
**Confidence:** MEDIUM (primary approach is well-documented; cross-device behavior on GenericTracker is uncharted)

## Summary

The OpenVR SDK has a well-documented mechanism for proximity sensors: drivers call `IVRDriverInput::CreateBooleanComponent` with the path `/proximity` and update it via `UpdateBooleanComponent`. The official Device Sleep States wiki confirms that creating this component automatically sets `Prop_ContainsProximitySensor_Bool`, and that "a device will never go to sleep while this component is set to true." SteamVR uses the proximity state to drive activity level transitions (`k_EDeviceActivityLevel_UserInteraction` when true, timeout to `Idle` then `Standby` when false).

The critical unknown -- and the reason this phase is a spike -- is whether a `/proximity` component on a `TrackedDeviceClass_GenericTracker` (our sidecar) can influence the HMD's standby/wake cycle. All documented examples show `/proximity` on HMD-class devices. The OpenVR SDK API allows any driver to read/write property containers for any device index (including the HMD at index 0 via `TrackedDeviceToPropertyContainer(k_unTrackedDeviceIndex_Hmd)`), which opens fallback paths if the primary approach fails.

**Primary recommendation:** Implement `/proximity` on the sidecar GenericTracker first (simplest, least invasive). If SteamVR does not propagate standby/wake to the HMD, execute the structured fallback checklist: (1) set `Prop_ContainsProximitySensor_Bool` on the HMD's property container from the sidecar, (2) try `VendorSpecificEvent` to simulate proximity, (3) try writing the HMD's activity level via property manipulation. Document each result.

<user_constraints>
## User Constraints (from CONTEXT.md)

### Locked Decisions
- **Toggle mechanism:** Named pipe debug control channel. Driver listens on named pipe for commands from external CLI tool (`beyond_prox_ctl`). Supports `proximity on`, `proximity off`, `status`. Persists as developer tool through later phases.
- **Fallback strategy:** Primary: `/proximity` on sidecar GenericTracker. If fails: structured checklist of 3-4 alternative SteamVR API approaches before declaring failure. Ultimate fallback (override driver / firmware change) deferred to after spike results.
- **Verification approach:** Layered -- scripted checks (PowerShell queries `GetTrackedDeviceActivityLevel()`, parses vrserver log) then manual UAT (SteamVR dashboard does NOT reset location while proximity=true and headset motionless). Beyond 2 display dark/light is firmware-controlled, NOT affected by this project.

### Claude's Discretion
- Whether to set `Prop_ContainsProximitySensor_Bool` on the sidecar device, the HMD, or both
- Named pipe path and protocol format (text-based commands are fine)
- Exact sequence of alternative API approaches in the structured checklist
- Component path naming (e.g., `/proximity` vs `/input/proximity/click`)

### Deferred Ideas (OUT OF SCOPE)
None -- discussion stayed within phase scope
</user_constraints>

<phase_requirements>
## Phase Requirements

| ID | Description | Research Support |
|----|-------------|-----------------|
| FEAS-03 | Sidecar driver's `/proximity` component triggers SteamVR standby/wake for the lighthouse-owned HMD device | Core spike objective. Device Sleep States wiki documents `/proximity` mechanism. Cross-device behavior is the unknown being validated. Fallback checklist covers alternatives if primary fails. |
| INTG-01 | Driver creates `/proximity` boolean component via `IVRDriverInput::CreateBooleanComponent` | Fully documented in OpenVR SDK. `CreateBooleanComponent(container, "/proximity", &handle)` pattern confirmed in wiki and header. Auto-sets `Prop_ContainsProximitySensor_Bool`. |
</phase_requirements>

## Standard Stack

### Core
| Library | Version | Purpose | Why Standard |
|---------|---------|---------|--------------|
| OpenVR SDK | 2.5.1 (vendored) | `IVRDriverInput::CreateBooleanComponent` / `UpdateBooleanComponent` | Already vendored in extern/openvr; provides the `/proximity` component API |
| Win32 Named Pipes | Windows SDK | Debug IPC channel (driver <-> CLI tool) | Native Windows IPC, zero dependencies, text-based protocol trivial to implement |

### Supporting
| Library | Version | Purpose | When to Use |
|---------|---------|---------|-------------|
| CMake | 3.20+ | Build system, add `beyond_prox_ctl` CLI target | Already the project build system |

### Alternatives Considered
| Instead of | Could Use | Tradeoff |
|------------|-----------|----------|
| Named pipes | TCP socket | Named pipes are simpler for local IPC, no firewall concerns, natural for Windows driver context |
| Named pipes | Shared memory | Shared memory needs synchronization primitives; pipe is simpler for command/response |
| Named pipes | File-based polling | Too slow, unreliable, no bidirectional communication |

## Architecture Patterns

### Recommended Project Structure
```
src/
  driver/
    proximity_device.h     # Add VRInputComponentHandle_t, proximity state, pipe handle
    proximity_device.cpp   # Add CreateBooleanComponent in Activate(), UpdateBooleanComponent method
    device_provider.h      # Add pipe server handle
    device_provider.cpp    # Add pipe creation in Init(), poll in RunFrame()
  ctl/
    main.cpp               # beyond_prox_ctl CLI tool (named pipe client)
scripts/
  verify_proximity.ps1     # Phase 3 verification (activity level checks, log parsing)
```

### Pattern 1: /proximity Boolean Component Creation
**What:** Create the proximity sensor component in `Activate()` using the special `/proximity` path
**When to use:** During device activation (called once by vrserver)
**Example:**
```cpp
// Source: OpenVR Device Sleep States wiki + openvr_driver.h line 3710
vr::EVRInitError ProximityDevice::Activate(uint32_t unObjectId)
{
    // ... existing property setup ...

    // Create /proximity boolean component
    // NOTE: The path "/proximity" is special -- it auto-sets
    // Prop_ContainsProximitySensor_Bool on this device
    vr::EVRInputError err = vr::VRDriverInput()->CreateBooleanComponent(
        props, "/proximity", &m_proximityHandle);
    if (err != vr::VRInputError_None)
    {
        DriverLog("Failed to create /proximity component: %d\n", err);
    }

    // Optionally also set the property explicitly for clarity
    vr::VRProperties()->SetBoolProperty(props,
        vr::Prop_ContainsProximitySensor_Bool, true);

    return vr::VRInitError_None;
}
```

### Pattern 2: Updating Proximity State
**What:** Call `UpdateBooleanComponent` to change the proximity value
**When to use:** Whenever proximity state changes (in this phase: when pipe command received)
**Example:**
```cpp
// Source: openvr_driver.h line 3713, tutorial controller_device.cpp line 38
void ProximityDevice::SetProximity(bool detected)
{
    if (m_proximityHandle != vr::k_ulInvalidInputComponentHandle)
    {
        vr::VRDriverInput()->UpdateBooleanComponent(
            m_proximityHandle, detected, 0.0);
        DriverLog("Proximity updated: %s\n", detected ? "true" : "false");
    }
}
```

### Pattern 3: Non-blocking Named Pipe Server in RunFrame
**What:** Create a named pipe server, poll for connections and commands in RunFrame()
**When to use:** Driver RunFrame() is called by vrserver on every frame (~11ms at 90Hz)
**Example:**
```cpp
// Source: Win32 Named Pipe API (PIPE_NOWAIT for non-blocking)
// In Init():
m_hPipe = CreateNamedPipeA(
    "\\\\.\\pipe\\beyond_proximity_ctl",
    PIPE_ACCESS_DUPLEX,
    PIPE_TYPE_MESSAGE | PIPE_READMODE_MESSAGE | PIPE_NOWAIT,
    1,             // max instances
    256, 256,      // buffer sizes
    0,             // default timeout
    nullptr);      // default security

// In RunFrame() -- poll for connection + read command:
if (m_hPipe != INVALID_HANDLE_VALUE)
{
    if (!m_bClientConnected)
    {
        // PIPE_NOWAIT: ConnectNamedPipe returns immediately
        ConnectNamedPipe(m_hPipe, nullptr);
        DWORD err = GetLastError();
        if (err == ERROR_PIPE_CONNECTED || err == ERROR_NO_DATA)
            m_bClientConnected = true;
    }
    if (m_bClientConnected)
    {
        char buf[256];
        DWORD bytesRead = 0;
        if (ReadFile(m_hPipe, buf, sizeof(buf)-1, &bytesRead, nullptr) && bytesRead > 0)
        {
            buf[bytesRead] = '\0';
            // Parse command and respond
            HandlePipeCommand(buf, bytesRead);
        }
        else if (GetLastError() == ERROR_BROKEN_PIPE)
        {
            DisconnectNamedPipe(m_hPipe);
            m_bClientConnected = false;
        }
    }
}
```

### Pattern 4: CLI Pipe Client (beyond_prox_ctl)
**What:** Simple command-line tool that connects to the named pipe and sends commands
**When to use:** Developer testing, built from same CMake project
**Example:**
```cpp
// Source: Win32 Named Pipe Client API
int main(int argc, char* argv[])
{
    if (argc < 2) { /* usage */ return 1; }

    HANDLE hPipe = CreateFileA(
        "\\\\.\\pipe\\beyond_proximity_ctl",
        GENERIC_READ | GENERIC_WRITE,
        0, nullptr, OPEN_EXISTING, 0, nullptr);

    if (hPipe == INVALID_HANDLE_VALUE)
    {
        fprintf(stderr, "Cannot connect to driver pipe (is driver loaded?)\n");
        return 1;
    }

    // Send command
    DWORD written;
    WriteFile(hPipe, argv[1], (DWORD)strlen(argv[1]), &written, nullptr);

    // Read response
    char response[256];
    DWORD bytesRead;
    if (ReadFile(hPipe, response, sizeof(response)-1, &bytesRead, nullptr))
    {
        response[bytesRead] = '\0';
        printf("%s\n", response);
    }

    CloseHandle(hPipe);
    return 0;
}
```

### Anti-Patterns to Avoid
- **Blocking pipe operations in RunFrame():** RunFrame() must return quickly (~11ms budget). Use `PIPE_NOWAIT` mode, never blocking `ConnectNamedPipe` or `ReadFile`.
- **Calling UpdateBooleanComponent every frame:** Only call when state actually changes. SteamVR uses state transitions, not polling.
- **Using `/input/proximity/click` path:** The documented special path is `/proximity` (no `/input/` prefix). The `/input/` prefix is for controller inputs. Proximity is device-level, not input-level.

## Don't Hand-Roll

| Problem | Don't Build | Use Instead | Why |
|---------|-------------|-------------|-----|
| IPC protocol | Custom binary protocol | Simple text commands over named pipe | Text is debuggable, human-readable; protocol is trivial (3 commands) |
| Pipe abstraction | Complex async pipe library | Direct Win32 CreateNamedPipe + PIPE_NOWAIT | Only one pipe, one client, simple text -- no abstraction needed |
| Activity level polling | Custom vrserver log parser in C++ | PowerShell verification script | Verification is separate from driver code; PS1 scripts are established pattern |

## Common Pitfalls

### Pitfall 1: /proximity on GenericTracker May Not Affect HMD Standby
**What goes wrong:** SteamVR may scope proximity sensor behavior to the device that owns the component. A GenericTracker going "in use" may not propagate to the HMD's standby/wake cycle.
**Why it happens:** The Device Sleep States documentation says "a device will never go to sleep while this component is set to true" -- this likely refers to the OWNING device, not cross-device.
**How to avoid:** This IS the spike's purpose. Accept this as the expected potential failure. Have the fallback checklist ready.
**Warning signs:** After toggling proximity=true, `GetTrackedDeviceActivityLevel(0)` for the HMD still shows `Standby` or `Idle`, while the tracker shows `UserInteraction`.

### Pitfall 2: Component Path Must Be Exactly "/proximity"
**What goes wrong:** Using `/input/proximity/click` or `/input/proximity` or other variations -- the component gets created but SteamVR does not treat it as a proximity sensor.
**Why it happens:** The special behavior (auto-setting `Prop_ContainsProximitySensor_Bool`, preventing sleep) is triggered by the exact path string `/proximity`. This is a magic string, not a pattern.
**How to avoid:** Use exactly `"/proximity"` as the component name.
**Warning signs:** `Prop_ContainsProximitySensor_Bool` is not automatically set after component creation.

### Pitfall 3: Blocking Named Pipe in RunFrame Freezes SteamVR
**What goes wrong:** Using default blocking mode for pipe operations causes RunFrame() to hang waiting for a client connection or data.
**Why it happens:** `ConnectNamedPipe` blocks by default until a client connects. `ReadFile` blocks until data is available.
**How to avoid:** Use `PIPE_NOWAIT` flag in `CreateNamedPipeA` call. Check return values and `GetLastError()` for `ERROR_NO_DATA`.
**Warning signs:** SteamVR hangs, controllers stop tracking, compositor stalls.

### Pitfall 4: Pipe Disconnect Not Handled
**What goes wrong:** Client disconnects, subsequent `ReadFile` returns `ERROR_BROKEN_PIPE`, but driver never calls `DisconnectNamedPipe` so no new client can connect.
**Why it happens:** Named pipe instances require `DisconnectNamedPipe` + new `ConnectNamedPipe` cycle after client disconnects.
**How to avoid:** On `ERROR_BROKEN_PIPE`, call `DisconnectNamedPipe(m_hPipe)` and reset connection state flag.
**Warning signs:** First `beyond_prox_ctl` invocation works, second one fails with "pipe busy" or "cannot connect".

### Pitfall 5: ShouldBlockStandbyMode Returns True Accidentally
**What goes wrong:** If `ShouldBlockStandbyMode()` returns true, SteamVR never enters standby regardless of proximity state, making the test meaningless.
**Why it happens:** Copy-paste error or defensive coding.
**How to avoid:** Verify `ShouldBlockStandbyMode()` returns false (already correct in current code).
**Warning signs:** SteamVR never enters standby even with proximity=false and headset motionless.

### Pitfall 6: UpdateBooleanComponent With Wrong Time Offset
**What goes wrong:** Passing a positive time offset (future) causes SteamVR to treat the update as predicted/deferred, potentially not applying it immediately.
**Why it happens:** The `fTimeOffset` parameter is relative to now. Negative = past (include transmission latency), positive = future.
**How to avoid:** Use `0.0` for immediate updates (no hardware latency in a software toggle).
**Warning signs:** Proximity state appears to lag or not take effect.

## Code Examples

### Fallback 1: Set Prop_ContainsProximitySensor_Bool on HMD Property Container
```cpp
// Source: openvr_driver.h lines 253, 408, 3226
// If /proximity on GenericTracker doesn't affect HMD standby,
// try setting the proximity sensor property directly on the HMD's container.
// k_unTrackedDeviceIndex_Hmd = 0 (always the HMD)
vr::PropertyContainerHandle_t hmdProps =
    vr::VRProperties()->TrackedDeviceToPropertyContainer(
        vr::k_unTrackedDeviceIndex_Hmd);

vr::ETrackedPropertyError propErr =
    vr::VRProperties()->SetBoolProperty(hmdProps,
        vr::Prop_ContainsProximitySensor_Bool, true);

DriverLog("Set Prop_ContainsProximitySensor on HMD: %s\n",
    vr::VRProperties()->GetPropErrorNameFromEnum(propErr));
```

### Fallback 2: Create /proximity Component on HMD Container
```cpp
// Source: openvr_driver.h line 3710
// MORE AGGRESSIVE: Create the /proximity boolean component
// using the HMD's property container handle rather than the tracker's.
// This may fail (permission error) since we don't own the HMD device.
vr::PropertyContainerHandle_t hmdProps =
    vr::VRProperties()->TrackedDeviceToPropertyContainer(
        vr::k_unTrackedDeviceIndex_Hmd);

vr::VRInputComponentHandle_t hmdProxHandle;
vr::EVRInputError err = vr::VRDriverInput()->CreateBooleanComponent(
    hmdProps, "/proximity", &hmdProxHandle);

DriverLog("Create /proximity on HMD container: %d\n", err);
// If err == VRInputError_None, update this handle instead of tracker's
```

### Fallback 3: VendorSpecificEvent Approach
```cpp
// Source: openvr_driver.h line 3782
// Send vendor-specific event to simulate proximity sensor press/release
// on the HMD device. Note: may generate errors per issue #1106.
vr::VREvent_Data_t eventData = {};
eventData.controller.button = vr::k_EButton_ProximitySensor; // button 31

// Simulate proximity ON = button press
vr::VRServerDriverHost()->VendorSpecificEvent(
    vr::k_unTrackedDeviceIndex_Hmd,
    vr::VREvent_ButtonPress,
    eventData, 0.0);
```

### Verification: Query Activity Level from PowerShell
```powershell
# The scripted verification uses the OpenVR client API.
# A minimal C# or C++ OpenVR client can call GetTrackedDeviceActivityLevel().
# Alternatively, parse vrserver.txt for activity transitions:

# Look for standby/wake events in vrserver log
$log = Get-Content "$env:LOCALAPPDATA\Steam\logs\vrserver.txt" -Raw
$standbyEvents = Select-String -InputObject $log -Pattern "EnterStandby|LeaveStandby|UserInteraction" -AllMatches
foreach ($match in $standbyEvents.Matches) {
    Write-Host $match.Value
}
```

## State of the Art

| Old Approach | Current Approach | When Changed | Impact |
|--------------|------------------|--------------|--------|
| `k_EButton_ProximitySensor` (button 31) | `/proximity` boolean component via IVRDriverInput | OpenVR input system rewrite (~2018-2019) | New input system uses components, not raw button events. Legacy button approach may still generate events but is deprecated for new drivers. |
| VendorSpecificEvent for proximity | IVRDriverInput::CreateBooleanComponent + UpdateBooleanComponent | Same timeframe | VendorSpecificEvent with reserved event range generates errors per issue #1106 |

**Deprecated/outdated:**
- `k_EButton_ProximitySensor` (button 31): Legacy input system enum. May still be internally used by SteamVR for activity detection, but new drivers should use the `/proximity` component path.
- `VendorSpecificEvent` for proximity: Does not work reliably with the new input system.

## Open Questions

1. **Does `/proximity` on a GenericTracker affect HMD standby/wake?**
   - What we know: The Device Sleep States wiki says "a device will never go to sleep while this component is set to true." The wiki does not specify device-class restrictions. All examples are HMD-class.
   - What's unclear: Whether SteamVR scopes proximity sleep-prevention to only the owning device, or whether it globally affects the standby system (particularly the HMD).
   - Recommendation: This IS the spike. Try it, document the observed behavior. If it fails, execute fallback checklist.

2. **Can a sidecar driver write properties on another driver's device container?**
   - What we know: `TrackedDeviceToPropertyContainer(k_unTrackedDeviceIndex_Hmd)` is a public API. The hidden area helpers in the SDK use it. The API does not document per-driver ownership restrictions.
   - What's unclear: Whether vrserver enforces write permissions per-driver on property containers. A cross-driver write might succeed or return `TrackedProp_PermissionDenied`.
   - Recommendation: Try it as fallback 1. Log the error code. If it works, it's the cleanest fallback.

3. **Can CreateBooleanComponent be called on a property container we don't own?**
   - What we know: `CreateBooleanComponent` takes a `PropertyContainerHandle_t`. Nothing in the API docs restricts it to the calling driver's devices.
   - What's unclear: Runtime permission checks. The HMD's lighthouse driver already owns the HMD device -- adding a component from another driver may be rejected.
   - Recommendation: Try it as fallback 2. This is more aggressive than fallback 1.

4. **Exact vrserver log format for proximity/standby transitions**
   - What we know: Events like `VREvent_TrackedDeviceUserInteractionStarted` (103), `VREvent_EnterStandbyMode` (106), `VREvent_LeaveStandbyMode` (107) exist.
   - What's unclear: Exact log line format in vrserver.txt for these events.
   - Recommendation: After first build, examine vrserver.txt with Beyond 2 connected to identify log patterns before writing verification script.

## Validation Architecture

### Test Framework
| Property | Value |
|----------|-------|
| Framework | PowerShell verification scripts + manual UAT |
| Config file | None (scripts are standalone) |
| Quick run command | `powershell -File scripts/verify_proximity.ps1` |
| Full suite command | `powershell -File scripts/verify_proximity.ps1` |

### Phase Requirements to Test Map
| Req ID | Behavior | Test Type | Automated Command | File Exists? |
|--------|----------|-----------|-------------------|-------------|
| FEAS-03 | Sidecar /proximity triggers HMD standby/wake | integration + manual | `powershell -File scripts/verify_proximity.ps1` then manual UAT (dashboard reset check) | No -- Wave 0 |
| INTG-01 | Driver creates /proximity boolean component | integration | `powershell -File scripts/verify_proximity.ps1` (check vrserver log for component creation) | No -- Wave 0 |

### Sampling Rate
- **Per task commit:** Build succeeds (`cmake --build build --config Release`)
- **Per wave merge:** `powershell -File scripts/verify_proximity.ps1`
- **Phase gate:** Full verification green + manual UAT confirmation before `/gsd:verify-work`

### Wave 0 Gaps
- [ ] `scripts/verify_proximity.ps1` -- covers FEAS-03, INTG-01 (log parsing, activity level checks)
- [ ] `beyond_prox_ctl` CLI tool -- required for toggling proximity during verification
- [ ] Manual UAT procedure documented in verification script output

## Sources

### Primary (HIGH confidence)
- [OpenVR Device Sleep States Wiki](https://github.com/ValveSoftware/openvr/wiki/Device-sleep-states) - `/proximity` component auto-sets `Prop_ContainsProximitySensor_Bool`, prevents device sleep while true
- [IVRDriverInput Overview Wiki](https://github.com/ValveSoftware/openvr/wiki/IVRDriverInput-Overview) - `CreateBooleanComponent` and `UpdateBooleanComponent` API signatures and semantics
- `extern/openvr/headers/openvr_driver.h` (vendored v2.5.1) - `IVRDriverInput` class (line 3705-3732), `Prop_ContainsProximitySensor_Bool` (line 408), `EDeviceActivityLevel` enum (line 1029-1037), `k_EButton_ProximitySensor` (line 1052), `IVRServerDriverHost` (line 3766-3815), `IVRProperties` (line 3211-3227)
- `extern/openvr/samples/drivers/drivers/tutorial/src/controller_device.cpp` - `CreateBooleanComponent` + `UpdateBooleanComponent` usage pattern (lines 16-43)

### Secondary (MEDIUM confidence)
- [OpenVR Issue #1631](https://github.com/ValveSoftware/openvr/issues/1631) - Proximity sensor behavior in custom drivers, display blanking vs standby
- [OpenVR Issue #1096](https://github.com/ValveSoftware/openvr/issues/1096) - `/user/head/proximity` input path, proximity via generic_hmd bindings
- [OpenVR Issue #1106](https://github.com/ValveSoftware/openvr/issues/1106) - VendorSpecificEvent unreliable with new input system
- [Steam Community: Vive Proximity Sensor](https://steamcommunity.com/app/358720/discussions/0/405691491125141530/) - `VREvent_TrackedDeviceUserInteractionStarted/Ended` events, `GetTrackedDeviceActivityLevel()` polling, 10-second timeout
- [OpenVR Driver API Documentation](https://github.com/ValveSoftware/openvr/blob/master/docs/Driver_API_Documentation.md) - Device classes, icon state properties
- [Win32 Named Pipe API](https://learn.microsoft.com/en-us/windows/win32/api/namedpipeapi/nf-namedpipeapi-connectnamedpipe) - `PIPE_NOWAIT` for non-blocking, `DisconnectNamedPipe` for reset

### Tertiary (LOW confidence)
- Cross-device property writing (no official documentation found confirming or denying permission model -- needs empirical validation in spike)
- Cross-device `CreateBooleanComponent` (theoretical -- API accepts any container handle but runtime behavior unknown)

## Metadata

**Confidence breakdown:**
- Standard stack: HIGH - OpenVR SDK vendored, Win32 pipes are native
- Architecture: HIGH - Patterns follow established OpenVR driver samples and existing project conventions
- /proximity component creation: HIGH - Documented in Device Sleep States wiki, confirmed in SDK header
- Cross-device standby/wake behavior: LOW - This is explicitly uncharted territory; the spike exists to test this
- Fallback approaches: MEDIUM - API surface supports them but runtime permission model is undocumented
- Named pipe IPC: HIGH - Standard Win32 pattern, well-documented, no complexity
- Pitfalls: HIGH - Derived from documented API semantics and real OpenVR issues

**Research date:** 2026-03-21
**Valid until:** Stable -- OpenVR driver API evolves slowly; findings valid for months
