# Phase 2: HID Access Validation - Research

**Researched:** 2026-03-21
**Domain:** Windows HID device access via HIDAPI from SteamVR driver context
**Confidence:** HIGH

## Summary

Phase 2 is a feasibility gate: prove that the sidecar driver can call `hid_open()` on the Beyond 2 proprietary HID interface (VID 0x35BD, PID 0x0101) from within vrserver.exe while the Bigscreen companion app (BeyondHID.exe) is also accessing the same device. No HID reading or proximity logic is needed -- just a successful open and close.

HIDAPI is the correct library choice. Its Windows backend (`windows/hid.c`) opens HID devices via `CreateFileW` with `FILE_SHARE_READ | FILE_SHARE_WRITE`, meaning shared-mode access is the default behavior. Multiple processes can hold handles to the same HID device simultaneously. The Python tools in `code_samples/` already use the `hidapi` Python binding against this exact VID/PID, confirming hardware compatibility.

One critical correction surfaced during research: the `hmd_presence` field in `driver.vrdrivermanifest` uses the format `"VID.PID"` (period-separated, single string), not two separate array elements. The correct entry is `["35BD.0101"]`, not `["35BD", "0101"]` as stated in CONTEXT.md.

**Primary recommendation:** Vendor HIDAPI 0.14.0 source in `extern/hidapi/`, build as static library via CMake `add_subdirectory`, wrap in a minimal C++ RAII class (`HidDevice`) in `src/hid/`, call `hid_open()` during `DeviceProvider::Init()`, and update the driver manifest `hmd_presence` to `["35BD.0101"]`.

<user_constraints>
## User Constraints (from CONTEXT.md)

### Locked Decisions
- Use HIDAPI (C library) -- matches the Python tools' hidapi binding already proven with Beyond 2
- Vendor HIDAPI source in `extern/hidapi/`, built via CMake -- same pattern as `extern/openvr/`
- Zero external installs needed; self-contained build
- Identify the proprietary HID interface by VID/PID match only (VID 0x35BD, PID 0x0101) -- no usage page filtering needed
- Update driver manifest: add `hmd_presence` so SteamVR auto-activates when Beyond 2 is connected (FEAS-04)
- Keep `alwaysActivate: true` alongside hmd_presence for Phase 2 as belt-and-suspenders; clean up later
- If shared-mode open fails (unexpected exclusive lock): disable proximity features, log warning, keep driver running -- matches RBST-01 graceful degradation pattern
- No retry loop for Phase 2; revisit if contention actually occurs in practice
- Automated PowerShell script (extends existing verify_driver.ps1 pattern) for verification

### Claude's Discretion
- Whether to wrap HIDAPI in a C++ RAII class or use the C API directly -- choose what fits the existing codebase style best
- Where in the driver lifecycle to call hid_open() (Init vs RunFrame vs background thread)
- HID error logging format and verbosity

### Deferred Ideas (OUT OF SCOPE)
None -- discussion stayed within phase scope
</user_constraints>

<phase_requirements>
## Phase Requirements

| ID | Description | Research Support |
|----|-------------|-----------------|
| FEAS-02 | Driver can open Beyond 2 proprietary HID device (VID 0x35BD, PID 0x0101) from within vrserver.exe without contention with built-in driver or Bigscreen client | HIDAPI shared-mode access confirmed via `FILE_SHARE_READ\|FILE_SHARE_WRITE` in Windows backend; `hid_open(0x35BD, 0x0101, NULL)` is the exact call; Python samples prove VID/PID hardware compatibility |
| FEAS-04 | Driver manifest `hmd_presence` can target the proprietary HID interface (separate from lighthouse HID device on the Beyond USB hub chain) | OpenVR docs confirm format is `"VID.PID"` -- correct value is `["35BD.0101"]`; with `alwaysActivate: true` the driver loads regardless, but hmd_presence enables `VR_IsHmdPresent()` detection |
| HID-01 | Driver opens Beyond 2 proprietary HID device using HIDAPI in shared mode | HIDAPI Windows backend always uses shared mode; no configuration needed; `hid_open()` returns `hid_device*` on success, `NULL` on failure |
</phase_requirements>

## Standard Stack

### Core
| Library | Version | Purpose | Why Standard |
|---------|---------|---------|--------------|
| HIDAPI | 0.14.0 | Cross-platform HID device access | Proven with Beyond 2 via Python binding; C library with CMake build; Windows backend uses native HID API with shared-mode access; actively maintained (libusb/hidapi) |

### Supporting
| Library | Version | Purpose | When to Use |
|---------|---------|---------|-------------|
| OpenVR SDK | 2.5.1 (vendored) | SteamVR driver interface | Already vendored in extern/openvr; provides IServerTrackedDeviceProvider, DriverLog |

### Alternatives Considered
| Instead of | Could Use | Tradeoff |
|------------|-----------|----------|
| HIDAPI 0.14.0 | HIDAPI 0.15.0 | 0.15.0 adds `hid_send_output_report()` and `hid_read_error()` not needed for Phase 2; 0.14.0 is more battle-tested and sufficient for open/close validation |
| HIDAPI | Raw Win32 HID API (SetupDi + CreateFile) | Works but requires ~200 lines of boilerplate for enumeration, path lookup, and handle management that HIDAPI encapsulates in one call |

**Why 0.14.0 specifically:**
- Released 2023-05-28, well-tested across ecosystems (SDL embeds it, Fedora packages it)
- CMake minimum version: 3.1.3 (project already requires 3.20, no conflict)
- 0.15.0 (2025-12-09) adds features not needed until later phases
- Git tag: `hidapi-0.14.0` on `libusb/hidapi` repository

**Installation (vendoring):**
```bash
git subtree add --prefix extern/hidapi https://github.com/libusb/hidapi.git hidapi-0.14.0 --squash
```

Or download and extract the release tarball into `extern/hidapi/`.

## Architecture Patterns

### Recommended Project Structure
```
src/
  driver/          # Existing: DeviceProvider, ProximityDevice, DriverLog
  hid/             # New: HID device wrapper (currently empty .gitkeep)
    hid_device.h   # RAII wrapper class declaration
    hid_device.cpp # Implementation
extern/
  openvr/          # Existing: vendored OpenVR SDK
  hidapi/          # New: vendored HIDAPI 0.14.0 source tree
driver/
  beyond_proximity/
    driver.vrdrivermanifest  # Updated: hmd_presence added
```

### Pattern 1: RAII HID Device Wrapper
**What:** A minimal C++ class wrapping `hid_device*` with constructor/destructor lifecycle management
**When to use:** Matches the existing codebase style (C++17, unique_ptr patterns in DeviceProvider, RAII throughout)
**Recommendation (Claude's Discretion):** Use RAII wrapper. The codebase already uses `std::unique_ptr<ProximityDevice>` and explicit ctor/dtor patterns. A thin RAII wrapper prevents resource leaks and fits naturally.

```cpp
// src/hid/hid_device.h
#pragma once
#include <hidapi.h>
#include <cstdint>

class HidDevice
{
public:
    HidDevice();
    ~HidDevice();

    // Non-copyable, non-movable (device handle is not transferable)
    HidDevice(const HidDevice&) = delete;
    HidDevice& operator=(const HidDevice&) = delete;

    // Returns true if device was opened successfully
    bool Open(uint16_t vid, uint16_t pid);

    // Close the device (also called by destructor)
    void Close();

    // Query state
    bool IsOpen() const;

private:
    hid_device* m_pDevice;
};
```

```cpp
// src/hid/hid_device.cpp
#include "hid_device.h"
#include "../driver/driverlog.h"

HidDevice::HidDevice()
    : m_pDevice(nullptr)
{
}

HidDevice::~HidDevice()
{
    Close();
}

bool HidDevice::Open(uint16_t vid, uint16_t pid)
{
    if (m_pDevice)
    {
        DriverLog("HID device already open, closing first\n");
        Close();
    }

    m_pDevice = hid_open(vid, pid, nullptr);
    if (!m_pDevice)
    {
        DriverLog("HID: Failed to open device %04X:%04X - %ls\n",
                  vid, pid, hid_error(nullptr));
        return false;
    }

    DriverLog("HID: Successfully opened device %04X:%04X\n", vid, pid);
    return true;
}

void HidDevice::Close()
{
    if (m_pDevice)
    {
        hid_close(m_pDevice);
        m_pDevice = nullptr;
        DriverLog("HID: Device closed\n");
    }
}

bool HidDevice::IsOpen() const
{
    return m_pDevice != nullptr;
}
```

### Pattern 2: HID Open During DeviceProvider::Init()
**What:** Call `hid_init()` and `hid_open()` synchronously during driver initialization
**When to use:** Phase 2 only needs to prove the open works; no background thread or deferred open needed
**Recommendation (Claude's Discretion):** Call in `DeviceProvider::Init()`. This is the simplest approach for a feasibility gate. The device is physically connected before SteamVR starts. If the open fails, log a warning and continue (graceful degradation per RBST-01 pattern).

```cpp
// In DeviceProvider::Init() -- after existing initialization
vr::EVRInitError DeviceProvider::Init(vr::IVRDriverContext* pDriverContext)
{
    VR_INIT_SERVER_DRIVER_CONTEXT(pDriverContext);
    InitDriverLog(vr::VRDriverLog());
    DriverLog("Beyond Proximity driver initializing\n");

    // Initialize HIDAPI library
    if (hid_init() != 0)
    {
        DriverLog("HID: hid_init() failed\n");
        // Continue without HID -- graceful degradation
    }
    else
    {
        // Attempt to open Beyond 2 proprietary HID device
        m_pHidDevice = std::make_unique<HidDevice>();
        if (!m_pHidDevice->Open(0x35BD, 0x0101))
        {
            DriverLog("HID: Beyond 2 device not available - proximity disabled\n");
            m_pHidDevice.reset();
        }
    }

    // Existing device registration (unchanged)
    m_pDevice = std::make_unique<ProximityDevice>();
    vr::VRServerDriverHost()->TrackedDeviceAdded(
        m_pDevice->GetSerialNumber(),
        vr::TrackedDeviceClass_GenericTracker,
        m_pDevice.get());

    DriverLog("Beyond Proximity driver initialized successfully\n");
    return vr::VRInitError_None;
}
```

### Pattern 3: Driver Manifest hmd_presence
**What:** Add VID.PID to manifest so SteamVR can detect Beyond 2 hardware without loading the driver
**Format (CRITICAL):** Each entry is `"VID.PID"` with a period separator, as a single string in the array

```json
{
    "alwaysActivate": true,
    "name": "beyond_proximity",
    "directory": "",
    "resourceOnly": false,
    "hmd_presence": ["35BD.0101"]
}
```

### Anti-Patterns to Avoid
- **Separate VID and PID as array elements:** `["35BD", "0101"]` is WRONG. The correct format is `["35BD.0101"]` (one string with period separator). Confirmed via OpenVR official docs and sample manifests (simplehmd uses `["*.*"]`).
- **Exclusive HID access:** Do not attempt to open HID devices exclusively. HIDAPI defaults to shared mode, and the companion app needs concurrent access. Never pass `0` as share mode to CreateFile.
- **HID open on background thread in Phase 2:** Unnecessary complexity for a feasibility gate. Call synchronously in Init().
- **Failing driver Init on HID open failure:** Return `VRInitError_None` regardless. The driver must load even if HID is unavailable (graceful degradation).

## Don't Hand-Roll

| Problem | Don't Build | Use Instead | Why |
|---------|-------------|-------------|-----|
| HID device enumeration and open | Raw SetupDi + CreateFile + HidP_ calls | `hid_open(vid, pid, NULL)` | HIDAPI handles device path discovery, handle creation, and share mode in one call; proven with Beyond 2 |
| HIDAPI build system | Custom Makefile or manual source compilation | `add_subdirectory(extern/hidapi)` + `hidapi::winapi` | HIDAPI's CMake build handles Windows system lib linking (setupapi, hid.dll) automatically |
| VID/PID matching logic | Manual USB enumeration with WMI or devcon | HIDAPI `hid_open(0x35BD, 0x0101, NULL)` | Single function call matches by VID/PID, opens first matching device |

**Key insight:** The entire Phase 2 implementation is effectively three function calls (`hid_init`, `hid_open`, `hid_close`) plus build system integration and manifest update. HIDAPI eliminates all the Win32 HID boilerplate.

## Common Pitfalls

### Pitfall 1: Wrong hmd_presence format
**What goes wrong:** SteamVR ignores the hmd_presence field or fails to match the Beyond 2
**Why it happens:** The format is not obvious; two separate strings `["35BD", "0101"]` seems logical but is wrong
**How to avoid:** Use `["35BD.0101"]` -- one string, period-separated VID.PID
**Warning signs:** `VR_IsHmdPresent()` returns false despite Beyond 2 being connected; no error logged because SteamVR silently ignores malformed entries

### Pitfall 2: Forgetting hid_init() before hid_open()
**What goes wrong:** `hid_open()` may return NULL or crash on some platforms
**Why it happens:** HIDAPI requires explicit initialization; the Windows backend needs to set up internal state
**How to avoid:** Always call `hid_init()` once during driver Init, and `hid_exit()` during Cleanup
**Warning signs:** NULL return from hid_open with no meaningful error from hid_error

### Pitfall 3: Not calling hid_exit() during Cleanup
**What goes wrong:** Memory leak and potential resource leak on driver unload
**Why it happens:** Easy to forget the symmetric teardown
**How to avoid:** Call `hid_exit()` in `DeviceProvider::Cleanup()` after closing all devices
**Warning signs:** Memory leak detectors flag HIDAPI internals

### Pitfall 4: HIDAPI as shared library leaking into driver package
**What goes wrong:** Extra DLL in driver bin/ directory; potential DLL loading issues
**Why it happens:** HIDAPI defaults to `BUILD_SHARED_LIBS=TRUE`
**How to avoid:** Set `BUILD_SHARED_LIBS` to `FALSE` before `add_subdirectory(extern/hidapi)` to build static
**Warning signs:** `hidapi.dll` appears in build output alongside `driver_beyond_proximity.dll`

### Pitfall 5: Blocking vrserver.exe main thread with slow HID operations
**What goes wrong:** SteamVR frame hitches or watchdog timeout
**Why it happens:** hid_open() on Windows does USB enumeration which can take milliseconds
**How to avoid:** For Phase 2, a single hid_open() during Init() is acceptable (one-time cost). For later phases with hid_read(), use a dedicated thread.
**Warning signs:** SteamVR watchdog warnings in vrserver.txt

### Pitfall 6: Thread safety violations with HIDAPI globals
**What goes wrong:** Corruption of internal HIDAPI state, crashes
**Why it happens:** `hid_init`, `hid_exit`, `hid_open`, `hid_close` are NOT thread-safe and must be serialized
**How to avoid:** Call all lifecycle functions from the main vrserver thread (RunFrame/Init/Cleanup). This is natural for Phase 2 since everything happens in Init/Cleanup.
**Warning signs:** Intermittent crashes during startup or shutdown

### Pitfall 7: HIDAPI CMake target name confusion
**What goes wrong:** Linker errors or wrong backend selected
**Why it happens:** HIDAPI provides multiple targets: `hidapi::hidapi` (auto-select), `hidapi::winapi` (Windows-specific), `hidapi::libusb` (Linux)
**How to avoid:** Use `hidapi::hidapi` for cross-platform or `hidapi::winapi` for explicit Windows. Both work; `hidapi::hidapi` resolves to `hidapi::winapi` on Windows automatically.
**Warning signs:** CMake configuration errors about missing targets

## Code Examples

### CMakeLists.txt Integration
```cmake
# Add HIDAPI as static library (must be before add_subdirectory)
set(BUILD_SHARED_LIBS FALSE CACHE BOOL "" FORCE)
add_subdirectory(extern/hidapi)

# In target_link_libraries for the driver:
target_link_libraries(${DRIVER_NAME} PRIVATE
    ${OPENVR_LIB}
    hidapi::hidapi
)
```

### DeviceProvider Lifecycle with HID
```cpp
// DeviceProvider::Init()
hid_init();
m_pHidDevice = std::make_unique<HidDevice>();
if (!m_pHidDevice->Open(0x35BD, 0x0101))
{
    DriverLog("HID: Beyond 2 not available - proximity disabled\n");
    m_pHidDevice.reset();
}

// DeviceProvider::Cleanup()
m_pHidDevice.reset();  // Close HID device first
hid_exit();             // Then finalize HIDAPI
CleanupDriverLog();
```

### Graceful Degradation Pattern
```cpp
// The driver MUST NOT fail Init if HID is unavailable.
// Log a warning and continue -- tracking/display/audio are unaffected.
if (!m_pHidDevice || !m_pHidDevice->IsOpen())
{
    DriverLog("WARNING: HID device unavailable. "
              "Proximity detection will be disabled.\n");
    // Driver continues normally -- ProximityDevice still registers
    // but will never get proximity data
}
```

### Updated driver.vrdrivermanifest
```json
{
    "alwaysActivate": true,
    "name": "beyond_proximity",
    "directory": "",
    "resourceOnly": false,
    "hmd_presence": ["35BD.0101"]
}
```

### PowerShell Verification (extend verify_driver.ps1)
```powershell
# --- HID Access Verification Checks ---

# Check: Beyond 2 HID device visible to system
$hidDevices = Get-PnpDevice -Class HIDClass -Status OK -ErrorAction SilentlyContinue |
    Where-Object { $_.HardwareID -match "VID_35BD&PID_0101" }
Test-Check "Beyond 2 HID device present" ($null -ne $hidDevices -and $hidDevices.Count -gt 0) ""

# Check: BeyondHID.exe companion app running (contention test precondition)
$beyondHid = Get-Process -Name "BeyondHID" -ErrorAction SilentlyContinue
Test-Check "BeyondHID.exe is running" ($null -ne $beyondHid) "Required for contention test"

# Check: Driver log shows successful HID open
$hidOpenSuccess = $logContent -match "HID: Successfully opened device 35BD:0101"
Test-Check "Driver opened HID device" $hidOpenSuccess ""

# Check: No HID contention errors
$hidErrors = $logLines | Where-Object {
    $_ -match "HID:" -and $_ -match "(fail|error|unable|contention)"
}
$noHidErrors = ($null -eq $hidErrors -or $hidErrors.Count -eq 0)
Test-Check "No HID contention errors" $noHidErrors $(
    if (-not $noHidErrors) { "HID errors:`n$($hidErrors -join "`n")" } else { "" }
)
```

## State of the Art

| Old Approach | Current Approach | When Changed | Impact |
|--------------|------------------|--------------|--------|
| signal11/hidapi (original, unmaintained) | libusb/hidapi (actively maintained fork) | ~2020 | All new development should use libusb/hidapi; signal11 repo is archived |
| HIDAPI Autotools build | HIDAPI CMake build | 0.14.0 (2023-05) | Autotools deprecated; CMake is the recommended build system |
| hid_open with no thread safety docs | Documented threading model | 0.15.0 wiki | Thread safety rules now documented; lifecycle functions must be serialized |

**Deprecated/outdated:**
- signal11/hidapi GitHub repository: archived, no updates since 2016. Use libusb/hidapi.
- HIDAPI Autotools build: deprecated as of 0.14.0; use CMake.
- hid_open_path with manually enumerated paths: unnecessary when VID/PID is known; use hid_open(vid, pid, NULL).

## Open Questions

1. **Does hmd_presence actually resolve to the proprietary HID interface specifically?**
   - What we know: VID 0x35BD PID 0x0101 is the Beyond 2's proprietary HID interface. The Beyond 2 USB hub also exposes lighthouse tracking HID interfaces with different VID/PID (Valve's VID).
   - What's unclear: Whether SteamVR's hmd_presence check sees the proprietary interface at the USB level, or only enumerates certain interface classes.
   - Recommendation: This is exactly what Phase 2 validates. If hmd_presence does not detect the device, `alwaysActivate: true` ensures the driver still loads. Log whether VR_IsHmdPresent would return true.

2. **Will hid_open timing be acceptable during vrserver Init?**
   - What we know: hid_open does USB enumeration on Windows which typically takes 1-10ms. DeviceProvider::Init() is called once during SteamVR startup.
   - What's unclear: Whether vrserver imposes a strict timeout on Init() calls.
   - Recommendation: Proceed with synchronous open in Init(). If timing proves to be an issue (unlikely for a one-time call), defer to first RunFrame() call in a later iteration.

## Validation Architecture

### Test Framework
| Property | Value |
|----------|-------|
| Framework | PowerShell verification script (manual trigger) |
| Config file | `scripts/verify_driver.ps1` (existing from Phase 1) |
| Quick run command | `powershell -ExecutionPolicy Bypass -File scripts/verify_driver.ps1` |
| Full suite command | `powershell -ExecutionPolicy Bypass -File scripts/verify_hid.ps1` |

### Phase Requirements to Test Map
| Req ID | Behavior | Test Type | Automated Command | File Exists? |
|--------|----------|-----------|-------------------|-------------|
| FEAS-02 | hid_open returns non-null for VID 35BD PID 0101 from within vrserver.exe | integration (log parse) | `powershell -ExecutionPolicy Bypass -File scripts/verify_hid.ps1` | Wave 0 |
| FEAS-04 | hmd_presence in manifest matches Beyond 2 USB device | integration (log parse + manifest check) | `powershell -ExecutionPolicy Bypass -File scripts/verify_hid.ps1` | Wave 0 |
| HID-01 | Driver opens HID in shared mode while BeyondHID.exe runs | integration (log parse + process check) | `powershell -ExecutionPolicy Bypass -File scripts/verify_hid.ps1` | Wave 0 |

### Sampling Rate
- **Per task commit:** Build succeeds (`cmake --build build --config Release`)
- **Per wave merge:** Full verification script with SteamVR running
- **Phase gate:** All HID verification checks pass with Beyond 2 connected and BeyondHID.exe running

### Wave 0 Gaps
- [ ] `scripts/verify_hid.ps1` -- HID-specific verification script (extends verify_driver.ps1 pattern)
- [ ] `extern/hidapi/` -- HIDAPI 0.14.0 source tree must be vendored
- [ ] CMakeLists.txt updated to include HIDAPI subdirectory and link target

## Sources

### Primary (HIGH confidence)
- [libusb/hidapi GitHub](https://github.com/libusb/hidapi) -- repository overview, releases, CMake build docs
- [HIDAPI BUILD.cmake.md](https://github.com/libusb/hidapi/blob/master/BUILD.cmake.md) -- CMake subdirectory integration pattern, available targets
- [HIDAPI windows/hid.c](https://github.com/libusb/hidapi/blob/master/windows/hid.c) -- Windows backend source confirming FILE_SHARE_READ|FILE_SHARE_WRITE in open_device
- [HIDAPI API docs](https://libusb.info/hidapi/group__API.html) -- function signatures for hid_init, hid_open, hid_close, hid_exit
- [HIDAPI Multi-threading Notes](https://github.com/libusb/hidapi/wiki/Multi%E2%80%90threading-Notes) -- thread safety rules
- [OpenVR Driver API Documentation](https://github.com/ValveSoftware/openvr/blob/master/docs/Driver_API_Documentation.md) -- hmd_presence format: "VID.PID" with period separator
- OpenVR sample manifests (vendored in extern/openvr/samples/) -- confirmed "VID.PID" format via simplehmd's `["*.*"]`

### Secondary (MEDIUM confidence)
- [OpenVR issue #570](https://github.com/ValveSoftware/openvr/issues/570) -- community discussion on hmd_presence vs EDID VID/PID
- [HIDAPI issue #302](https://github.com/signal11/hidapi/issues/302) -- confirms shared mode is intentional, no exclusive access option

### Tertiary (LOW confidence)
- None -- all critical claims verified against primary sources

## Metadata

**Confidence breakdown:**
- Standard stack: HIGH -- HIDAPI is a well-documented, actively maintained library; version, CMake targets, and API verified against official docs
- Architecture: HIGH -- existing codebase patterns (vendored deps, RAII, DriverLog) directly inform the approach; HIDAPI CMake integration is straightforward
- Pitfalls: HIGH -- hmd_presence format verified against OpenVR docs and sample manifests; HIDAPI shared-mode confirmed in source; threading rules documented in wiki
- hmd_presence format correction: HIGH -- verified against official OpenVR docs, sample manifests in vendored SDK, and community discussions

**Research date:** 2026-03-21
**Valid until:** 2026-04-21 (stable domain -- HIDAPI and OpenVR driver API change slowly)
