# Phase 7: Configuration - Research

**Researched:** 2026-03-22
**Domain:** SteamVR IVRSettings API, driver configuration patterns
**Confidence:** HIGH

## Summary

Phase 7 adds three developer-only tunables (`report_rate_ms`, `log_verbosity`, `moving_avg_length`) read from `steamvr.vrsettings` at driver startup. The scope is narrow and well-defined: read settings in `DeviceProvider::Init()`, pass values to existing subsystems, and add conditional verbose logging. No user-facing configuration, no runtime reload, no new dependencies.

The OpenVR `IVRSettings` API is straightforward -- `GetInt32(section, key)` returns the value directly, with `VRSettingsError_UnsetSettingHasNoDefault` when a key is missing. The simplehmd sample driver demonstrates the exact pattern needed. The main implementation work is (1) reading three int32 settings, (2) making `ProximityAlgorithm::kAverageLength` configurable via constructor or Reset parameter, and (3) adding log-verbosity-guarded DriverLog calls in the reader thread and RunFrame.

**Primary recommendation:** Read all three settings in `DeviceProvider::Init()` using `VRSettings()->GetInt32()` with error-checked fallbacks to hardcoded defaults. Pass `report_rate_ms` to `StartReading()`, `moving_avg_length` to `ProximityAlgorithm` (make it a constructor/Reset parameter), and store `log_verbosity` as a member for conditional logging.

<user_constraints>
## User Constraints (from CONTEXT.md)

### Locked Decisions
- **No user-facing configuration** -- user_trim is stored in headset flash, configurable via Beyond Utility. End users do not interact with steamvr.vrsettings.
- Three internal/dev tunables only: `report_rate_ms`, `log_verbosity`, `moving_avg_length`
- Threshold and hysteresis are NOT configurable -- they come from headset flash
- Section name: `driver_BeyondProximity` (standard SteamVR convention)
- Key naming: `snake_case`
- All settings have sensible defaults so the driver works with no vrsettings section
- No changes to Phase 4's calibration read from flash
- user_trim, threshold, hysteresis remain flash-only -- no priority/override logic
- Settings read once in `DeviceProvider::Init()` -- startup only, no runtime reload
- No pipe reload command, no live watch
- Two log levels: 0=normal (state transitions + errors), 1=verbose (raw samples, algorithm values per frame)
- Default log verbosity: 0

### Claude's Discretion
- Default value for `moving_avg_length` (currently 8 in ProximityAlgorithm)
- VRSettings error handling (missing section, missing keys -- fall back to defaults silently)
- How verbose mode actually logs (DriverLog format strings, frequency of raw sample logging)
- Whether `moving_avg_length` requires ProximityAlgorithm API changes or just parameterizing the existing constant

### Deferred Ideas (OUT OF SCOPE)
None -- discussion stayed within phase scope
</user_constraints>

<phase_requirements>
## Phase Requirements

| ID | Description | Research Support |
|----|-------------|-----------------|
| CONF-01 | Driver reads settings from steamvr.vrsettings on startup (report rate, threshold trim) | VRSettings API pattern verified in OpenVR SDK; section `driver_BeyondProximity`; `GetInt32()` with error fallback. Scope narrowed: "threshold trim" maps to `moving_avg_length` (not user_trim, which remains flash-only). |
| CONF-02 | Driver syncs user_trim from HMD user flash on startup, falling back to 0 if not available | Already implemented in Phase 4 (CalibrationData.user_trim read from flash, defaults to 0). This requirement is ALREADY SATISFIED by existing code -- Phase 7 just needs to verify it, not re-implement. |
| CONF-03 | User can adjust proximity sensitivity via user_trim offset in steamvr.vrsettings | Per CONTEXT.md decisions, user_trim is NOT in steamvr.vrsettings -- it is managed by Beyond Utility and stored in headset flash. This requirement is satisfied by the existing flash-based user_trim mechanism. Phase 7 scope excludes vrsettings-based user_trim. |
</phase_requirements>

## Standard Stack

### Core
| Library | Version | Purpose | Why Standard |
|---------|---------|---------|--------------|
| OpenVR SDK | 2.5.1 (vendored) | `IVRSettings` API for reading steamvr.vrsettings | Already in project; `VRSettings()` available after `VR_INIT_SERVER_DRIVER_CONTEXT` |

### Supporting
No new libraries needed. This phase uses only existing OpenVR APIs.

## Architecture Patterns

### Settings Read Pattern
**What:** Read all configuration in `DeviceProvider::Init()`, store in local variables, pass to subsystems before they start.
**When to use:** Always -- this is the standard SteamVR driver pattern.
**Example:**
```cpp
// Source: extern/openvr/samples/drivers/drivers/simplehmd/src/hmd_device_driver.cpp lines 12-47
static const char* kSettingsSection = "driver_BeyondProximity";

// In DeviceProvider::Init(), after VR_INIT_SERVER_DRIVER_CONTEXT:
vr::EVRSettingsError err;
int32_t reportRateMs = vr::VRSettings()->GetInt32(kSettingsSection, "report_rate_ms", &err);
if (err != vr::VRSettingsError_None)
    reportRateMs = 200;  // default

int32_t logVerbosity = vr::VRSettings()->GetInt32(kSettingsSection, "log_verbosity", &err);
if (err != vr::VRSettingsError_None)
    logVerbosity = 0;  // default: normal

int32_t movingAvgLength = vr::VRSettings()->GetInt32(kSettingsSection, "moving_avg_length", &err);
if (err != vr::VRSettingsError_None)
    movingAvgLength = 8;  // default: matches current kAverageLength
```

### ProximityAlgorithm Parameterization Pattern
**What:** Replace compile-time `kAverageLength` with a runtime parameter.
**When to use:** When `moving_avg_length` needs to be configurable.
**Example:**
```cpp
// Option A (recommended): Constructor parameter + dynamic buffer
class ProximityAlgorithm
{
public:
    explicit ProximityAlgorithm(int avgLength = 8);
    // ...
private:
    int m_averageLength;
    std::vector<uint16_t> m_buffer;  // replace std::array with vector
    // ...
};

// Option B: Reset() parameter (resets buffer anyway)
void ProximityAlgorithm::Reset(const CalibrationData& cal, int avgLength);
```

**Recommendation:** Option A (constructor parameter) is cleaner. The algorithm is created once in HidDevice and never changes average length at runtime. Use `std::vector<uint16_t>` instead of `std::array<uint16_t, kAverageLength>` since the size is now runtime. The performance difference is negligible (one allocation of 4-32 uint16_t values).

### Verbose Logging Pattern
**What:** Guard detailed logging behind a verbosity flag.
**When to use:** In the HID reader thread (raw samples) and RunFrame (algorithm output).
**Example:**
```cpp
// In HidDevice -- pass verbosity flag, store as member
// In ReaderThreadFunc, after ProcessSample:
if (m_logVerbose)
    DriverLog("HID: raw=%u cal_dist=%u\n", rawValue, calibratedValue);

// In DeviceProvider::RunFrame, after GetPersonDetected:
if (m_logVerbose && m_pHidDevice)
{
    auto diag = m_pHidDevice->GetAlgorithmDiag();
    DriverLog("Prox: avg=%u thresh=%u detected=%s samples=%u\n",
              diag.averaged_prox, diag.effective_threshold,
              diag.detected ? "true" : "false", diag.total_samples);
}
```

**Warning:** Verbose mode at 200ms report rate = 5 log lines/second from reader thread + RunFrame logs at ~90Hz. This is acceptable for dev tuning but would spam logs in production. Default=0 is critical.

### Anti-Patterns to Avoid
- **Reading settings in RunFrame:** VRSettings is for startup. Do not call GetInt32 every frame.
- **Caching EVRSettingsError globally:** Check error per-key, not once for the whole section. A missing section returns the same error as a missing key.
- **Clamping in the wrong place:** Clamp `moving_avg_length` to a sane range (e.g., 1-64) in Init, not in ProximityAlgorithm. The algorithm should trust its input.
- **Logging from reader thread at full rate without guard:** Without the verbosity check, every 200ms sample would produce a log line permanently.

## Don't Hand-Roll

| Problem | Don't Build | Use Instead | Why |
|---------|-------------|-------------|-----|
| Settings file parsing | JSON parser for steamvr.vrsettings | `vr::VRSettings()->GetInt32()` | SteamVR owns the file, has its own parser, handles file watching |
| Default value logic | Custom "settings with defaults" framework | Simple if-error-then-default after each GetInt32 call | Only 3 settings; a framework is overkill |
| Configuration validation | Elaborate validation system | Simple clamp in Init | Three integer ranges is trivial |

## Common Pitfalls

### Pitfall 1: VRSettingsError_UnsetSettingHasNoDefault is not an error
**What goes wrong:** Treating `UnsetSettingHasNoDefault` as a failure and logging warnings when the section/key is absent.
**Why it happens:** The error enum name sounds alarming, but it is the normal return when a user has not customized a setting.
**How to avoid:** Silently fall back to defaults on ANY non-None error. Only log the final resolved value at normal verbosity.
**Warning signs:** Log spam on every startup saying "Failed to read setting X."

### Pitfall 2: GetInt32 returns 0 when key is missing (if no default.vrsettings)
**What goes wrong:** If you ignore the error parameter, `GetInt32` returns 0 for missing keys. For `report_rate_ms`, 0 would mean "poll as fast as possible" -- overwhelming the HID device.
**Why it happens:** The OpenVR header comment says defaults come from `resources/settings/default.vrsettings`. Without that file, the API default is 0/false/"".
**How to avoid:** ALWAYS check the `EVRSettingsError` out-parameter and apply code-level defaults when error is non-None.
**Warning signs:** report_rate_ms=0 in logs, moving_avg_length=0 causing divide-by-zero.

### Pitfall 3: moving_avg_length = 0 or 1 edge cases
**What goes wrong:** Division by zero (avg = sum / length) or meaningless averaging.
**Why it happens:** User sets value to 0 or 1 in vrsettings.
**How to avoid:** Clamp to minimum of 1 (or 2) and maximum of 64 (or whatever is reasonable for the circular buffer). Log a warning if clamped.

### Pitfall 4: Verbose logging performance impact
**What goes wrong:** Verbose mode writes to DriverLog at reader-thread rate (5/sec) plus RunFrame rate (90/sec). Over long sessions, this generates massive log files.
**Why it happens:** DriverLog writes to disk synchronously.
**How to avoid:** This is acceptable for dev tuning sessions. Document that verbose mode is for debugging only, not continuous use. Default=0 protects normal users.

### Pitfall 5: std::vector reallocation in hot path
**What goes wrong:** If ProximityAlgorithm buffer is resized after construction, it could reallocate during ProcessSample on the reader thread.
**Why it happens:** Using push_back or resize in ProcessSample instead of pre-allocating.
**How to avoid:** Allocate the vector once in the constructor with the final size. ProcessSample only writes to existing indices.

## Code Examples

### Complete Settings Read in DeviceProvider::Init()
```cpp
// Source: OpenVR IVRSettings API (openvr_driver.h:2337-2356)
// Pattern: simplehmd sample (hmd_device_driver.cpp:12-47)

static const char* kSettingsSection = "driver_BeyondProximity";

// After VR_INIT_SERVER_DRIVER_CONTEXT(pDriverContext) and InitDriverLog():
vr::EVRSettingsError settingsErr;

// 1. Report rate (default: 200ms)
int32_t reportRateMs = vr::VRSettings()->GetInt32(kSettingsSection, "report_rate_ms", &settingsErr);
if (settingsErr != vr::VRSettingsError_None)
    reportRateMs = 200;
if (reportRateMs < 50) reportRateMs = 50;     // floor: don't overwhelm HID
if (reportRateMs > 5000) reportRateMs = 5000;  // ceiling: responsiveness

// 2. Log verbosity (default: 0)
int32_t logVerbosity = vr::VRSettings()->GetInt32(kSettingsSection, "log_verbosity", &settingsErr);
if (settingsErr != vr::VRSettingsError_None)
    logVerbosity = 0;
if (logVerbosity < 0) logVerbosity = 0;
if (logVerbosity > 1) logVerbosity = 1;

// 3. Moving average length (default: 8)
int32_t movingAvgLength = vr::VRSettings()->GetInt32(kSettingsSection, "moving_avg_length", &settingsErr);
if (settingsErr != vr::VRSettingsError_None)
    movingAvgLength = 8;
if (movingAvgLength < 1) movingAvgLength = 1;
if (movingAvgLength > 64) movingAvgLength = 64;

DriverLog("Config: report_rate_ms=%d, log_verbosity=%d, moving_avg_length=%d\n",
          reportRateMs, logVerbosity, movingAvgLength);

// Store verbosity for RunFrame logging
m_logVerbose = (logVerbosity > 0);

// Pass to subsystems
m_pHidDevice = std::make_unique<HidDevice>(movingAvgLength, m_logVerbose);
m_pHidDevice->StartReading(0x35BD, 0x0101, static_cast<uint16_t>(reportRateMs));
```

### ProximityAlgorithm with Runtime Buffer Size
```cpp
// proximity_algorithm.h changes:
class ProximityAlgorithm
{
public:
    explicit ProximityAlgorithm(int avgLength = 8);
    void Reset(const CalibrationData& cal);
    void ProcessSample(uint16_t hid_distance);
    // ... rest unchanged ...
private:
    int m_averageLength;                   // was: static constexpr kAverageLength
    std::vector<uint16_t> m_buffer;        // was: std::array<uint16_t, kAverageLength>
    int m_writeIndex = 0;
    // ... rest unchanged ...
};

// proximity_algorithm.cpp changes:
ProximityAlgorithm::ProximityAlgorithm(int avgLength)
    : m_averageLength(avgLength)
    , m_buffer(avgLength, 0)
{
}

void ProximityAlgorithm::Reset(const CalibrationData& cal)
{
    // ... existing logic, replace kAverageLength with m_averageLength ...
    std::fill(m_buffer.begin(), m_buffer.end(), 0);
    m_writeIndex = 0;
    // ...
}

void ProximityAlgorithm::ProcessSample(uint16_t hid_distance)
{
    m_buffer[m_writeIndex] = hid_distance;
    m_writeIndex = (m_writeIndex + 1) % m_averageLength;

    uint32_t sum = 0;
    for (int i = 0; i < m_averageLength; i++)
        sum += m_buffer[i];
    uint32_t averaged = sum / m_averageLength;
    // ... rest unchanged ...
}
```

### steamvr.vrsettings Example Entry
```json
{
    "driver_BeyondProximity": {
        "report_rate_ms": 200,
        "log_verbosity": 0,
        "moving_avg_length": 8
    }
}
```
Note: This section is OPTIONAL. The driver works correctly without it.

## State of the Art

| Old Approach | Current Approach | When Changed | Impact |
|--------------|------------------|--------------|--------|
| Hardcoded report rate 200ms | Configurable via vrsettings | Phase 7 | Dev team can tune without recompile |
| Compile-time kAverageLength=8 | Runtime moving_avg_length | Phase 7 | Dev team can tune responsiveness vs stability |
| No verbose logging option | log_verbosity=1 enables per-sample logging | Phase 7 | Dev team can diagnose sensor issues in field |

## Open Questions

1. **default.vrsettings file**
   - What we know: OpenVR docs say defaults should go in `resources/settings/default.vrsettings` in the driver directory. The simplehmd sample has one.
   - What's unclear: Whether this project needs one, given that the driver works with no section at all and code-level defaults are applied.
   - Recommendation: Create a `driver/BeyondProximity/resources/settings/default.vrsettings` with the three defaults. It costs nothing and follows the SDK convention. The CMake POST_BUILD copy_directory already handles copying `driver/BeyondProximity/` to the build output. However, since all defaults are code-enforced via error checking, this is a nice-to-have, not a requirement.

2. **HidDevice constructor signature change**
   - What we know: HidDevice currently has a default constructor. Adding `avgLength` and `logVerbose` parameters changes the signature.
   - What's unclear: Whether to pass these as constructor params or have separate setters.
   - Recommendation: Constructor params. The values are known at construction time and never change. This is simpler and makes the dependency explicit.

## Validation Architecture

### Test Framework
| Property | Value |
|----------|-------|
| Framework | Manual hardware validation (SteamVR driver, requires Beyond 2 HMD) |
| Config file | None -- hardware-dependent |
| Quick run command | `beyond_prox_ctl.exe status` (check config values in status output) |
| Full suite command | Manual: edit steamvr.vrsettings, restart SteamVR, verify via status pipe |

### Phase Requirements to Test Map
| Req ID | Behavior | Test Type | Automated Command | File Exists? |
|--------|----------|-----------|-------------------|-------------|
| CONF-01 | Settings read from vrsettings on startup | manual | `beyond_prox_ctl.exe status` -- verify report rate and avg length in output | N/A |
| CONF-02 | user_trim from flash, fallback to 0 | manual-only | `beyond_prox_ctl.exe status` -- verify trim=0 or trim=N | Already verified in Phase 4 |
| CONF-03 | user_trim adjustable via flash (Beyond Utility) | manual-only | Already working via existing flash mechanism | Already verified in Phase 4/5 |

### Sampling Rate
- **Per task commit:** Build succeeds: `"C:/Program Files (x86)/Microsoft Visual Studio/2022/BuildTools/Common7/IDE/CommonExtensions/Microsoft/CMake/CMake/bin/cmake.exe" --build build --config Release`
- **Per wave merge:** Build + deploy + `beyond_prox_ctl.exe status` shows configured values
- **Phase gate:** Full manual validation with modified vrsettings

### Wave 0 Gaps
- [ ] Extend `status` pipe command output to include `report_rate_ms`, `log_verbosity`, and `moving_avg_length` so settings can be verified without reading SteamVR logs
- [ ] Or: verify settings via SteamVR driver log output (search for "Config:" line)

## Sources

### Primary (HIGH confidence)
- `extern/openvr/headers/openvr_driver.h` lines 2323-2356 -- `EVRSettingsError` enum and `IVRSettings` interface with exact API signatures
- `extern/openvr/samples/drivers/drivers/simplehmd/src/hmd_device_driver.cpp` lines 12-47 -- Reference implementation of VRSettings usage in a driver
- `extern/openvr/samples/drivers/drivers/simplehmd/simplehmd/resources/settings/default.vrsettings` -- Default settings file format
- `src/driver/device_provider.cpp` -- Current Init() implementation with hardcoded values
- `src/hid/proximity_algorithm.h` line 33 -- `kAverageLength = 8` constant to parameterize
- `src/hid/hid_device.h` -- Current HidDevice interface including StartReading(vid, pid, rateMs)

### Secondary (MEDIUM confidence)
- OpenVR SDK header comments (line 2347-2348): "Users of the system need to provide a proper default in default.vrsettings in the resources/settings/ directory of either the runtime or the driver_xxx directory. Otherwise the default will be false, 0, 0.0 or ''"

## Metadata

**Confidence breakdown:**
- Standard stack: HIGH -- using existing vendored OpenVR SDK, API verified in headers
- Architecture: HIGH -- pattern copied directly from SDK sample drivers
- Pitfalls: HIGH -- verified against actual API behavior (error enum values, default returns)

**Research date:** 2026-03-22
**Valid until:** Indefinite -- OpenVR IVRSettings_003 is a stable API
