# Phase 6: Standby/Wake Integration - Research

**Researched:** 2026-03-22
**Domain:** SteamVR OpenVR driver proximity integration (C++, Windows named pipes)
**Confidence:** HIGH

## Summary

Phase 6 wires the proximity algorithm's `person_detected` boolean (completed in Phase 5) to SteamVR's HMD proximity property so SteamVR correctly manages standby/wake. The implementation is intentionally minimal: read an atomic bool in `RunFrame()`, compare to cached state, call the existing `SetHmdProximity()` on change. Additionally, the existing `proximity on/off` pipe commands gain a manual override flag so they can lock proximity state for testing, and a new `proximity auto` command clears the override.

All building blocks already exist and are proven on hardware. `SetHmdProximity()` writes `Prop_ContainsProximitySensor_Bool` to the HMD property container with a state-change guard. `HidDevice::GetPersonDetected()` is an atomic bool safe to read from any thread. The pipe protocol is text-in/text-out with disconnect after each exchange. No new SteamVR API exploration is needed.

**Primary recommendation:** Wire `GetPersonDetected()` to `SetHmdProximity()` in `RunFrame()` with a manual override flag, add `proximity auto` pipe command, and verify end-to-end on hardware.

<user_constraints>
## User Constraints (from CONTEXT.md)

### Locked Decisions
- Simple RunFrame poll: read `GetPersonDetected()`, compare to `m_bProximity`, call `SetHmdProximity()` on change
- The existing `SetHmdProximity()` using `SetBoolProperty(Prop_ContainsProximitySensor_Bool)` on the HMD container IS the correct and proven mechanism
- No `/proximity` component needed -- driver has no registered tracked device (removed in Phase 3.1)
- State-change guard already exists in `SetHmdProximity()` -- only writes on transitions
- No driver-side debounce -- propagate state changes immediately in both directions
- The algorithm's hysteresis + 8-sample moving average is sufficient debounce
- Pipe `proximity on/off` commands set a manual override flag that disables algorithm-driven updates
- Override persists until explicitly cleared via new `proximity auto` command
- Status command shows source: `source=algorithm` or `source=manual`
- On driver init: start with `proximity=false` (person absent) -- match algorithm default
- On USB reconnect: reset to `proximity=false` (person absent) -- match algorithm reset behavior
- No special handling for buffer-fill window -- let the algorithm naturally transition

### Claude's Discretion
- Exact implementation of the manual override flag (bool + enum, etc.)
- Whether `proximity auto` logs to SteamVR driver log
- Status command formatting for the new source field
- Whether to add algorithm-driven state change logging

### Deferred Ideas (OUT OF SCOPE)
None -- discussion stayed within phase scope
</user_constraints>

<phase_requirements>
## Phase Requirements

| ID | Description | Research Support |
|----|-------------|-----------------|
| INTG-02 | Driver updates proximity state when person_detected changes (not every frame) | RunFrame polls `GetPersonDetected()`, compares to `m_bProximity`, calls `SetHmdProximity()` only on change. State-change guard in `SetHmdProximity()` provides double protection. |
| INTG-03 | SteamVR display turns off within configured timeout after removing headset | `SetBoolProperty(Prop_ContainsProximitySensor_Bool, false)` signals HMD idle to SteamVR. SteamVR's own Power Management timeout (configurable in SteamVR Settings) handles the actual standby transition. |
| INTG-04 | SteamVR display turns on when putting headset on | `SetBoolProperty(Prop_ContainsProximitySensor_Bool, true)` signals user interaction. SteamVR fires `VREvent_TrackedDeviceUserInteractionStarted` and transitions from Standby to UserInteraction. |
| INTG-05 | Games/applications can query proximity state via `GetTrackedDeviceActivityLevel()` and `Prop_ContainsProximitySensor_Bool` | `Prop_ContainsProximitySensor_Bool` (1025) is readable by any OpenVR client. `GetTrackedDeviceActivityLevel()` returns `EDeviceActivityLevel` which transitions based on proximity sensor state. |
</phase_requirements>

## Standard Stack

### Core
| Library | Version | Purpose | Why Standard |
|---------|---------|---------|--------------|
| OpenVR SDK | 2.5.1 (vendored) | SteamVR driver API | Already in project at `extern/openvr/` |
| HIDAPI | 0.14.0 (vendored) | HID device communication | Already in project at `extern/hidapi/` |

### Supporting
No new libraries needed. This phase uses only existing project code and OpenVR APIs.

**No installation needed.** All dependencies are already vendored.

## Architecture Patterns

### Integration Points (4 files to modify)

```
src/driver/
  device_provider.h     # Add manual override flag member
  device_provider.cpp   # Wire RunFrame, extend HandlePipeCommand
src/ctl/
  main.cpp              # Add "proximity auto" to known commands
scripts/
  verify_standby.ps1    # New verification script
```

### Pattern 1: RunFrame Algorithm Polling
**What:** Read atomic bool from algorithm, compare to cached state, update HMD property on change
**When to use:** Every `RunFrame()` call (SteamVR calls this at ~90Hz)
**Example:**
```cpp
// In DeviceProvider::RunFrame()
void DeviceProvider::RunFrame()
{
    PollPipe();

    // Phase 6: Wire algorithm output to SteamVR HMD proximity
    if (m_pHidDevice && !m_bManualOverride)
    {
        bool detected = m_pHidDevice->GetPersonDetected();
        SetHmdProximity(detected);  // state-change guard inside
    }
}
```
**Key insight:** `GetPersonDetected()` is `std::atomic<bool>` -- lock-free, safe to call every frame. `SetHmdProximity()` already has a state-change guard (`if (on == m_bProximity) return`), so calling it every frame with the same value is a no-op (two cheap comparisons, zero API calls).

### Pattern 2: Manual Override Flag
**What:** Boolean flag that disables algorithm-driven proximity updates
**When to use:** When developer uses `proximity on/off` pipe commands for testing
**Example:**
```cpp
// In device_provider.h
bool m_bManualOverride = false;

// In HandlePipeCommand for "proximity on"
m_bManualOverride = true;
SetHmdProximity(true);

// In HandlePipeCommand for "proximity auto"
m_bManualOverride = false;
DriverLog("Proximity: manual override cleared, algorithm driving\n");
```
**Key insight:** The override flag is only written from the main thread (pipe commands are processed in `RunFrame()` via `PollPipe()`), so no synchronization is needed.

### Pattern 3: Status Command Extension
**What:** Add `source=algorithm|manual` field to status response
**When to use:** Status pipe command
**Example:**
```cpp
// In status response formatting
snprintf(response, sizeof(response),
    "proximity=%s hid=%s source=%s prox_raw=%u ...",
    m_bProximity ? "true" : "false",
    hidStateStr,
    m_bManualOverride ? "manual" : "algorithm",
    ...);
```

### Anti-Patterns to Avoid
- **Calling SetBoolProperty every frame:** Even though the state-change guard prevents duplicate writes, relying on the guard as the primary mechanism is wasteful. Compare in `RunFrame()` first.
- **Adding driver-side debounce:** The algorithm already has hysteresis + 8-sample moving average. Adding more debounce would increase latency and create confusion about where delays come from.
- **Using `/proximity` component:** The driver has no registered tracked device (removed in Phase 3.1). The HMD property write mechanism is proven.
- **Trying to detect USB reconnect in RunFrame:** The HID reader thread handles reconnection. If HID disconnects, `GetPersonDetected()` retains its last value. The algorithm resets on reconnect (via `Reset(cal)`), which sets `m_personDetected = false`. RunFrame will naturally pick up the `false` state.

## Don't Hand-Roll

| Problem | Don't Build | Use Instead | Why |
|---------|-------------|-------------|-----|
| Standby/wake timing | Custom timers for standby delay | SteamVR Power Management settings | SteamVR manages its own standby timeout; driver just signals state |
| Debounce/hysteresis | Additional smoothing layer | Phase 5 algorithm (8-sample average + hysteresis) | Already tuned and tested on hardware |
| Thread synchronization for override flag | Mutex/atomic for m_bManualOverride | Plain bool | Both reads and writes happen on the main thread (RunFrame context) |

**Key insight:** The driver's sole job is to keep `Prop_ContainsProximitySensor_Bool` in sync with the algorithm's output. SteamVR decides what to do with that signal.

## Common Pitfalls

### Pitfall 1: Startup Proximity State
**What goes wrong:** Setting `proximity=true` on startup (current code does `SetHmdProximity(true)` in Init) means SteamVR thinks someone is wearing the headset immediately, even if nobody is.
**Why it happens:** Phase 3 set it to `true` on startup to ensure `Prop_ContainsProximitySensor_Bool` was registered. With the algorithm now driving state, this should change.
**How to avoid:** On startup, call `SetHmdProximity(false)` (or don't call it -- let the algorithm's first detection trigger it). The CONTEXT.md locks this: "start with `proximity=false`".
**Warning signs:** SteamVR shows "in use" when headset is sitting on a desk after driver restart.

### Pitfall 2: Algorithm Not Yet Producing Data
**What goes wrong:** After driver init, before the HID device connects and enough samples arrive, `GetPersonDetected()` returns `false` (default). This is actually correct behavior per CONTEXT.md ("let the algorithm naturally transition").
**Why it matters:** No special handling needed. The algorithm starts with `m_personDetected{false}`, which matches the desired startup state.
**How to avoid:** Don't add special "waiting for data" logic. The default `false` is the correct initial state.

### Pitfall 3: Override Not Cleared After Testing
**What goes wrong:** Developer runs `proximity on` for testing, forgets to run `proximity auto`, and the headset stays "in use" forever (or the reverse with `proximity off`).
**Why it happens:** Manual override persists by design.
**How to avoid:** Status command clearly shows `source=manual` so the developer knows override is active. Log a message when override is set.
**Warning signs:** Status shows `source=manual` when expected to be `source=algorithm`.

### Pitfall 4: INTG-02 Requirement Wording vs. Implementation
**What goes wrong:** INTG-02 says "updates `/proximity` component via `UpdateBooleanComponent`" but the actual mechanism is `SetBoolProperty` on the HMD container.
**Why it happens:** Requirements were written before Phase 3's spike proved the `/proximity` component doesn't work for sidecar drivers without a tracked device.
**How to avoid:** Treat the intent of INTG-02 (update proximity state only on change, not every frame) as the requirement. The mechanism is `SetBoolProperty`, not `UpdateBooleanComponent`.

## Code Examples

### RunFrame Wiring (the core change)
```cpp
// Source: CONTEXT.md + existing device_provider.cpp patterns
void DeviceProvider::RunFrame()
{
    PollPipe();

    if (m_pHidDevice && !m_bManualOverride)
    {
        bool detected = m_pHidDevice->GetPersonDetected();
        SetHmdProximity(detected);
    }
}
```

### HandlePipeCommand Extension
```cpp
// Source: existing HandlePipeCommand pattern in device_provider.cpp
if (strcmp(cmd, "proximity on") == 0)
{
    m_bManualOverride = true;
    SetHmdProximity(true);
    snprintf(response, sizeof(response), "OK proximity=true source=manual");
}
else if (strcmp(cmd, "proximity off") == 0)
{
    m_bManualOverride = true;
    SetHmdProximity(false);
    snprintf(response, sizeof(response), "OK proximity=false source=manual");
}
else if (strcmp(cmd, "proximity auto") == 0)
{
    m_bManualOverride = false;
    DriverLog("Proximity: manual override cleared, algorithm driving\n");
    snprintf(response, sizeof(response), "OK source=algorithm");
}
```

### Header Addition
```cpp
// In device_provider.h, private section
bool m_bManualOverride = false;
```

### Init Change (startup = false)
```cpp
// In DeviceProvider::Init(), change existing line from:
SetHmdProximity(true);
// to:
// Don't set proximity on startup -- let algorithm drive it.
// Proximity starts false (person absent) by default.
```

Note: The current `SetHmdProximity(true)` in Init was a Phase 3 artifact to ensure `Prop_ContainsProximitySensor_Bool` was set. With algorithm driving, the first `true` detection will set it. However, there's a subtlety: `m_bProximity` starts as `false`, so `SetHmdProximity(false)` on startup would be a no-op (the guard `if (on == m_bProximity) return` triggers). To ensure SteamVR knows the HMD has a proximity sensor, we should still write `false` explicitly on startup by bypassing the guard or initializing differently. The simplest approach: remove the startup call entirely. The property will be written on the first actual state change.

### CLI Extension
```cpp
// In src/ctl/main.cpp, add to known commands validation
strcmp(command, "proximity auto") != 0 &&
// and add to PrintUsage
fprintf(stderr, "  \"proximity auto\"  Clear manual override, let algorithm drive\n");
```

## State of the Art

| Old Approach | Current Approach | When Changed | Impact |
|--------------|------------------|--------------|--------|
| `/proximity` component on virtual tracker | `SetBoolProperty` on HMD container | Phase 3/3.1 | No tracked device needed; property write works directly |
| Manual `proximity on/off` only | Algorithm-driven + manual override | Phase 6 (this phase) | Automatic standby/wake based on sensor data |
| `SetHmdProximity(true)` on startup | `proximity=false` on startup | Phase 6 (this phase) | Correct initial state matches "nobody wearing headset" |

## Validation Architecture

### Test Framework
| Property | Value |
|----------|-------|
| Framework | PowerShell verification scripts (project pattern) |
| Config file | N/A -- scripts are self-contained |
| Quick run command | `powershell -ExecutionPolicy Bypass -File scripts/verify_standby.ps1` |
| Full suite command | `powershell -ExecutionPolicy Bypass -File scripts/verify_standby.ps1` |

### Phase Requirements to Test Map
| Req ID | Behavior | Test Type | Automated Command | File Exists? |
|--------|----------|-----------|-------------------|-------------|
| INTG-02 | Proximity updates only on state change (not every frame) | log-check + pipe | `verify_standby.ps1` checks log for state-change messages, not per-frame spam | No -- Wave 0 |
| INTG-03 | SteamVR enters standby when headset removed | manual + log-check | Partially automated: verify log shows proximity=false after headset removal. Standby timing is SteamVR-controlled. | No -- Wave 0 |
| INTG-04 | SteamVR wakes when headset put on | manual + log-check | Partially automated: verify log shows proximity=true after headset on. | No -- Wave 0 |
| INTG-05 | Apps can query via GetTrackedDeviceActivityLevel and Prop_ContainsProximitySensor_Bool | pipe + status | `beyond_prox_ctl status` shows proximity state and source field | No -- Wave 0 |

### Sampling Rate
- **Per task commit:** Build with cmake, deploy, restart SteamVR, run `beyond_prox_ctl status`
- **Per wave merge:** Full `verify_standby.ps1`
- **Phase gate:** Full verification script green + manual on-head/off-head test

### Wave 0 Gaps
- [ ] `scripts/verify_standby.ps1` -- Phase 6 verification script covering INTG-02 through INTG-05
- [ ] Manual test: put headset on head, verify SteamVR wakes; remove, verify standby (INTG-03, INTG-04)

## Open Questions

1. **Prop_ContainsProximitySensor_Bool semantics on startup**
   - What we know: Phase 3 set it to `true` on startup. CONTEXT.md says start with `false`.
   - What's unclear: Does SteamVR need the property written at least once (even to `false`) to recognize the HMD has a proximity sensor? Or does writing `true` later suffice?
   - Recommendation: Remove the startup `SetHmdProximity(true)` call. The first algorithm detection will write the property. If SteamVR needs an initial write, the algorithm will naturally produce one within seconds of HID connection. This is LOW risk -- can be tested immediately on hardware.

2. **`proximity on/off` response format change**
   - What we know: Currently responds with `OK proximity=true`. Adding `source=manual` changes the response format.
   - What's unclear: Does any tooling parse the exact response format?
   - Recommendation: Add `source=manual` to the response. The CLI tool only prints the response -- it doesn't parse it programmatically. LOW risk.

## Sources

### Primary (HIGH confidence)
- `extern/openvr/headers/openvr_driver.h` -- `Prop_ContainsProximitySensor_Bool` (1025), `EDeviceActivityLevel` enum, `VREvent_TrackedDeviceUserInteractionStarted/Ended`
- `src/driver/device_provider.cpp` -- Current `SetHmdProximity()`, `RunFrame()`, `HandlePipeCommand()` implementations
- `src/hid/proximity_algorithm.h` -- `GetPersonDetected()` atomic bool API
- `src/hid/hid_device.h` -- `GetPersonDetected()`, `GetAlgorithmDiag()` public API
- `.planning/phases/06-standby-wake-integration/06-CONTEXT.md` -- Locked decisions from user discussion

### Secondary (MEDIUM confidence)
- OpenVR header comments on `EDeviceActivityLevel` -- describe transition semantics (Standby -> UserInteraction on prox sensor activity)

### Tertiary (LOW confidence)
- None -- all findings verified against source code and OpenVR headers

## Metadata

**Confidence breakdown:**
- Standard stack: HIGH -- no new dependencies, all code exists
- Architecture: HIGH -- 4 lines of core logic in RunFrame, well-understood pipe protocol extension
- Pitfalls: HIGH -- startup state verified in code, override semantics discussed in CONTEXT.md

**Research date:** 2026-03-22
**Valid until:** 2026-04-22 (stable -- no external dependency changes expected)
