---
phase: 11-core-ipd-pipe-command
verified: 2026-03-26T06:23:14Z
status: human_needed
score: 6/7 must-haves verified
human_verification:
  - test: "End-to-end IPD pipeline — set, query, validation, config loading"
    expected: "ipd 63.5 -> OK ipd=63.5mm with visible eye separation change in HMD; ipd 10 -> ERR ipd out of range (48-75mm); ipd? -> OK ipd=63.5mm; status includes ipd= and lh_config= fields"
    why_human: "Requires SteamVR running with Beyond 2 HMD connected. SetDisplayEyeToHead effect is visual only — cannot verify programmatically. Config auto-discovery requires openvrpaths.vrpath pointing to lhr-* directory on test machine."
---

# Phase 11: Core IPD Pipe Command Verification Report

**Phase Goal:** Users can change IPD live via the named pipe interface and CLI, with the change taking effect immediately in SteamVR
**Verified:** 2026-03-26T06:23:14Z
**Status:** human_needed
**Re-verification:** No — initial verification

## Goal Achievement

### Observable Truths

| # | Truth | Status | Evidence |
|---|-------|--------|----------|
| 1 | Tracking serial is extracted from HID user flash tag 0x09 and available via HidDevice::GetTrackingSerial() | VERIFIED | `user_signature.h` has `TrackingSerial = 0x09` in SigTag enum and `char tracking_serial[32]` in CalibrationData; `user_signature.cpp` line 67-71 parses tag 0x09 with memcpy; `hid_device.h` declares `GetTrackingSerial()`; `hid_device.cpp` lines 90-94 implement it with mutex lock |
| 2 | Lighthouse config rotation matrices are loaded from lhr-\<serial\>/config.json instead of spawning lighthouse_console.exe | VERIFIED | `device_provider.cpp` `LoadLighthouseConfig()` reads openvrpaths.vrpath via ExtractVrPathValue, lowercases serial via ::tolower, builds path with `\\lighthouse\\` subdirectory; `ReadEyeToHeadFromConfigFile()` parses config.json via std::ifstream; grep for `lighthouse_console` returns no matches in src/ |
| 3 | Driver reads initial IPD from HMD container on startup and stores in m_fCurrentIpd | VERIFIED | `device_provider.cpp` lines 75-90 in Init() read `Prop_UserIpdMeters_Float` from HMD container and assign to `m_fCurrentIpd`; comment explicitly notes HARD-02 |
| 4 | ipd \<mm\> command sets IPD via SetDisplayEyeToHead + SetFloatProperty with 48-75mm validation | VERIFIED | `HandleIpdSet()` lines 648-708: validates mm < 48.0f or mm > 75.0f, calls `VRServerDriverHost()->SetDisplayEyeToHead()` and `VRProperties()->SetFloatProperty(...Prop_UserIpdMeters_Float...)`, updates `m_fCurrentIpd` |
| 5 | Config loading attempts eager load on startup with lazy fallback on first ipd command | VERIFIED | `device_provider.cpp` Init() lines 92-101 call `LoadLighthouseConfig()` after pipe creation; `HandleIpdSet()` lines 660-667 check `!m_bLhConfigLoaded` and call `LoadLighthouseConfig()` on first use |
| 6 | CLI tool accepts ipd \<mm\>, ipd?, and load_lh_config \<path\> commands and rejects all spike/legacy commands | VERIFIED | `src/ctl/main.cpp` lines 43-50 include `strncmp(command, "ipd ", 4)`, `strcmp(command, "ipd?")`, `strncmp(command, "load_lh_config ", 15)` in validCommand; grep for fallback1, ipd_test, eyetohead_check, slider_test, eyetohead_set returns no matches |
| 7 | IPD change takes effect immediately in SteamVR (visual confirmation in HMD) | ? NEEDS HUMAN | SetDisplayEyeToHead call is implemented and wired correctly. Actual effect on eye separation requires SteamVR running with connected HMD to confirm. This was the pending Task 2 (checkpoint:human-verify) in plan 02. |

**Score:** 6/7 truths verified (1 needs human)

### Required Artifacts

| Artifact | Expected | Status | Details |
|----------|----------|--------|---------|
| `src/hid/user_signature.h` | SigTag_TrackingSerial enum value and tracking_serial field in CalibrationData | VERIFIED | Lines 18 and 28: `TrackingSerial = 0x09` and `char tracking_serial[32] = {}` |
| `src/hid/user_signature.cpp` | Tag 0x09 parsing in ParseCalibration | VERIFIED | Lines 67-71: tag 0x09 branch with memcpy and null-termination |
| `src/hid/hid_device.h` | GetTrackingSerial() public method | VERIFIED | Line 48: `std::string GetTrackingSerial() const;` |
| `src/hid/hid_device.cpp` | GetTrackingSerial() implementation and tracking serial log in ReadCalibration | VERIFIED | Lines 90-94: mutex-protected implementation; lines 245-246: conditional DriverLog for tracking_serial |
| `src/driver/device_provider.h` | Production IPD methods replacing spike methods | VERIFIED | Lines 44-49: LoadLighthouseConfig, LoadLighthouseConfigFromPath, ReadEyeToHeadFromConfigFile, HandleIpdSet, HandleIpdQuery, HandleLoadLhConfig — no spike method names present |
| `src/driver/device_provider.cpp` | LoadLighthouseConfig, HandleIpdSet, config.json reading | VERIFIED | All three fully implemented; no lighthouse_console, no spike command handlers |
| `src/ctl/main.cpp` | Updated CLI with production IPD commands | VERIFIED | Lines 48-50: ipd/ipd?/load_lh_config in validCommand; PrintUsage documents all three |

### Key Link Verification

| From | To | Via | Status | Details |
|------|----|-----|--------|---------|
| `src/hid/user_signature.cpp` | CalibrationData.tracking_serial | tag == 0x09 extraction | WIRED | Line 67: `if (tag == 0x09 && length < sizeof(cal.tracking_serial))` then memcpy |
| `src/driver/device_provider.cpp` | HidDevice::GetCalibration().tracking_serial (via GetTrackingSerial) | config path construction with lowercased serial | WIRED | Lines 579-586: falls back to `m_pHidDevice->GetTrackingSerial()` if SteamVR property unavailable; line 622: `::tolower` transform applied |
| `src/driver/device_provider.cpp` | config.json file read | LoadLighthouseConfig using ExtractVrPathValue for config dir | WIRED | Line 613: `ExtractVrPathValue(vrpathContent, "config")` used to find configDir; line 625: builds path with `\\lighthouse\\` |
| `src/ctl/main.cpp` | named pipe | WriteFile sends ipd command, ReadFile receives OK/ERR response | WIRED | Line 85: WriteFile; line 95: ReadFile — confirmed in place |
| `src/driver/device_provider.cpp` HandleIpdSet | vr::VRServerDriverHost()->SetDisplayEyeToHead | Builds HmdMatrix34_t with cached rotation + IPD translation | WIRED | Lines 683-696: matrix construction and SetDisplayEyeToHead call |
| `src/driver/device_provider.cpp` HandleIpdSet | vr::VRProperties()->SetFloatProperty | Sets Prop_UserIpdMeters_Float | WIRED | Lines 699-703: SetFloatProperty call after SetDisplayEyeToHead |

### Data-Flow Trace (Level 4)

Not applicable. This phase implements pipe command handlers and an OpenVR driver — no UI components rendering dynamic data from a data store. The data flows are: HID flash -> CalibrationData -> tracking_serial -> config path -> rotation matrices -> SetDisplayEyeToHead. All hops verified via key link table above.

### Behavioral Spot-Checks

Step 7b: SKIPPED — requires SteamVR running and HMD connected. The driver DLL cannot be exercised without the SteamVR runtime. The CLI binary requires the pipe server to be active.

### Requirements Coverage

| Requirement | Source Plan | Description | Status | Evidence |
|-------------|-------------|-------------|--------|----------|
| IPD-01 | 11-01-PLAN.md | Driver accepts `ipd <mm>` command via named pipe and sets Prop_UserIpdMeters_Float on HMD container | SATISFIED | `HandleIpdSet()` in device_provider.cpp calls SetFloatProperty(Prop_UserIpdMeters_Float); `strncmp(cmd, "ipd ", 4)` dispatch in HandlePipeCommand |
| IPD-02 | 11-01-PLAN.md | Driver validates IPD value within Beyond physical range before applying | SATISFIED | `HandleIpdSet()` line 651: `if (mm < 48.0f \|\| mm > 75.0f)` returns ERR response before applying |
| IPD-03 | 11-02-PLAN.md | Driver responds with current IPD value via `ipd?` query command | SATISFIED | `HandleIpdQuery()` lines 710-719; `strcmp(cmd, "ipd?")` dispatch present |
| IPD-04 | 11-02-PLAN.md | `beyond_prox_ctl.exe` CLI supports `ipd <mm>` and `ipd?` commands | SATISFIED | main.cpp validCommand includes both; PrintUsage documents both |
| IPD-05 | 11-02-PLAN.md | Status command includes current IPD value in response | SATISFIED | device_provider.cpp status handler lines 266-287 include `ipd=%s lh_config=%s` in snprintf for both HID-present and no-HID branches |
| HARD-01 | 11-01-PLAN.md | IPD value validated and clamped to Beyond physical range on all input paths | SATISFIED (pipe path) / ? NEEDS HUMAN (slider path) | Pipe path validated: `HandleIpdSet()` rejects out-of-range. Slider path (Phase 12) not yet implemented — acceptable as this is Phase 11 scope |
| HARD-02 | 11-01-PLAN.md | Driver reads initial IPD from HMD container on startup and tracks current value | SATISFIED | Init() lines 75-90 read Prop_UserIpdMeters_Float and store to m_fCurrentIpd; RunFrame() lines 132-139 also updates m_fCurrentIpd on VREvent_IpdChanged |

**Note on REQUIREMENTS.md status:** The traceability table in REQUIREMENTS.md still marks IPD-01, IPD-02, HARD-01, HARD-02 as "Pending" with unchecked boxes. The implementations exist in the codebase and were committed (commits 78fa65b, f599cbb). REQUIREMENTS.md needs to be updated to reflect completion status.

**Note on orphaned requirements:** No requirements mapped to Phase 11 in REQUIREMENTS.md Traceability that are absent from plan frontmatter. All 7 phase requirements (IPD-01, IPD-02, IPD-03, IPD-04, IPD-05, HARD-01, HARD-02) are claimed across the two plans.

### Anti-Patterns Found

| File | Line | Pattern | Severity | Impact |
|------|------|---------|----------|--------|
| `src/ctl/main.cpp` | 93 | `char response[256]` — CLI read buffer is 256 bytes, driver sends responses from 512-byte buffer | Warning | Status response with full calibration data and IPD fields may exceed 256 bytes. Current status format is approximately 220-250 chars — marginally safe but brittle. Driver server buffer is 512; CLI receive buffer should match. |

No stub patterns found. No TODO/FIXME/placeholder comments found in modified files. No empty handler implementations. No spike command remnants.

### Human Verification Required

#### 1. End-to-End IPD Pipeline (SteamVR + HMD Required)

**Test:** Deploy build/Release/driver_bigscreenbeyond.dll to bigscreenbeyond driver directory, restart SteamVR with Beyond 2 connected, then run the full test sequence from 11-02-PLAN.md Task 2.

**Expected:**
- vrserver.txt shows `"IPD: Initial IPD from HMD container"` message on startup
- `beyond_prox_ctl.exe "ipd 63.5"` returns `OK ipd=63.5mm` and eye separation visibly changes in HMD
- `beyond_prox_ctl.exe "ipd 10"` returns `ERR ipd out of range (48-75mm)` without any change
- `beyond_prox_ctl.exe "ipd?"` returns `OK ipd=63.5mm` (last set value)
- `beyond_prox_ctl.exe "status"` output includes `ipd=63.5mm` and `lh_config=loaded` (or `not_loaded` if serial not matched)
- `beyond_prox_ctl.exe "fallback1 on"` returns CLI error `Error: Unknown command 'fallback1 on'`
- `beyond_prox_ctl.exe "proximity on"` still returns `OK proximity=true source=manual` (regression check)

**Why human:** SetDisplayEyeToHead effect on rendered eye separation is visual-only and requires a live SteamVR session with a connected HMD. The auto-discovery path (HID flash tag 0x09 -> openvrpaths.vrpath -> lhr-serial/config.json) requires the specific test machine's SteamVR config directory structure to verify lh_config=loaded.

### Gaps Summary

No blocking code gaps identified. All seven requirements have implementation evidence in the codebase. The one outstanding item is the human-verify checkpoint (Task 2 of Plan 02) that was explicitly deferred in the SUMMARY as "paused at checkpoint:human-verify."

Minor notes for follow-up (not blocking):
1. CLI response buffer at 256 bytes while driver sends from 512-byte buffer — consider increasing CLI buffer to 512 in a future pass to prevent truncation of long status responses.
2. REQUIREMENTS.md traceability table should be updated to mark IPD-01, IPD-02, HARD-01, HARD-02 as complete.

---

_Verified: 2026-03-26T06:23:14Z_
_Verifier: Claude (gsd-verifier)_
