# Phase 11: Core IPD Pipe Command - Context

**Gathered:** 2026-03-25
**Status:** Ready for planning

<domain>
## Phase Boundary

Users can change IPD live via the named pipe interface (`ipd <mm>`) and CLI (`beyond_prox_ctl.exe`), with the change taking effect immediately in SteamVR via SetDisplayEyeToHead. Includes query (`ipd?`), status integration, IPD validation (48-75mm), startup IPD tracking, and rotation matrix caching from lighthouse config files. SteamVR slider UI is Phase 12.

</domain>

<decisions>
## Implementation Decisions

### Spike-to-production cleanup
- Remove ALL spike commands: `ipd_test`, `eyetohead_check`, `slider_test`, `eyetohead_set`
- Remove legacy `fallback1 on/off` commands
- Production command: `ipd <mm>` (set) and `ipd?` (query) — internally uses the eyetohead_set approach (SetDisplayEyeToHead + SetFloatProperty)
- Keep CLI tool name as `beyond_prox_ctl.exe` — established name, no rename
- Production pipe commands after cleanup: `proximity on/off/auto`, `status`, `ipd <mm>`, `ipd?`, `load_lh_config <path>`

### Startup IPD behavior
- On Init: read Prop_UserIpdMeters_Float from HMD container, store in m_fCurrentIpd for status/query
- Do NOT call SetDisplayEyeToHead on startup — lighthouse driver already sets correct transforms at boot
- Default m_fCurrentIpd = 0.0f (unknown) if property not set. `ipd?` returns `OK ipd=unknown` in that case

### Config reading strategy
- Eager rotation cache on startup: read `lhr-<serial>/config.json` from SteamVR config directory
- Discover config dir via `openvrpaths.vrpath` → `config` key
- Match `lhr-*` folder name to the HMD's lighthouse serial read from HID user flash (same data source as proximity calibration)
- Discover lighthouse serial extraction approach from `code_samples/` (testgui, beyond_firmware)
- NO fallback to lighthouse_console.exe — if config.json not found, log error, IPD commands return ERR
- Fallback plan B: `load_lh_config <path>` pipe command — external program can provide explicit config file path
- Rotation cache is static for the session (no invalidation) — lens alignment doesn't change during a session

### Command response format
- `ipd 63.5` success: `OK ipd=63.5mm` — simple, no Euler angles in response (angles logged to driver log only)
- `ipd 63.5` error: `ERR ipd out of range (48-75mm)` or `ERR lh_config not loaded`
- `ipd?` success: `OK ipd=63.5mm` (or `OK ipd=unknown` if not yet set)
- `status` command: append `ipd=63.5mm lh_config=loaded` (or `ipd=unknown lh_config=error`) to existing status key=value string
- `load_lh_config <path>` success: `OK lh_config=loaded` — failure: `ERR failed to read config: <reason>`

### Claude's Discretion
- Exact lighthouse serial extraction from user flash (research code_samples for approach)
- How to structure the config.json parsing (reuse/refactor existing ParseEyeToHead3x3 or write new)
- Internal code organization (new methods, member variables)
- Error message wording details
- Log verbosity for rotation matrix diagnostics

</decisions>

<canonical_refs>
## Canonical References

**Downstream agents MUST read these before planning or implementing.**

### OpenVR API
- `extern/openvr/headers/openvr_driver.h` — SetDisplayEyeToHead signature (IVRServerDriverHost_006), HmdMatrix34_t type, Prop_UserIpdMeters_Float=2003, VREvent_IpdChanged=105

### Existing driver code
- `src/driver/device_provider.cpp` — Current pipe command handler, eyetohead_set implementation (becomes basis for production ipd command), ReadEyeToHeadRotation (to be refactored from lighthouse_console to config.json), SetHmdIpd, HandleEyeToHeadCheck
- `src/driver/device_provider.h` — Class structure, IPD spike state, cached rotation members
- `src/ctl/main.cpp` — CLI tool, command validation, pipe communication pattern

### Config reading (lighthouse serial + lhr-* config)
- `code_samples/` (testgui, beyond_firmware) — Source for discovering how lighthouse serial is extracted from HID user flash
- `code_samples/vertical_alignment_proximity/alignment.py` — VAP alignment code: Euler XYZ convention, HORIZONTAL_CANT=6.17 deg, eye_to_head rotation matrix structure
- `code_samples/vertical_alignment_proximity/dist/main/_internal/steamvr/steamvr.py` — set_ipd_default_mm, set_eye_to_head config structure

### HID user flash (proximity calibration + lighthouse serial)
- `src/hid/user_signature.h` / `src/hid/user_signature.cpp` — Existing user flash parsing (proximity calibration). Lighthouse serial extraction to be added based on code_samples research
- `src/hid/hid_device.h` / `src/hid/hid_device.cpp` — HID reader thread, TLV+CRC8 parser, calibration data access

### Tundra SiP (research target)
- https://tundra-labs.com/products/tl448k6d-vr-system-in-package-for-steamvr-tracking — Beyond 2 uses Tundra SiP for SteamVR tracking. Research whether the SiP/its driver has IPD persistence features that could avoid lighthouse_console for config writes (relevant to Phase 13 persistence, may also inform Phase 11 config reading)

### Requirements
- `.planning/REQUIREMENTS.md` — IPD-01 through IPD-05, HARD-01, HARD-02

### Phase 10/10.1 findings
- `.planning/phases/10-feasibility-spike/10-FINDINGS.md` — FEAS-01/02/03 results, root cause of why SetDisplayEyeToHead is needed
- `.planning/phases/10.1-setdisplayeyetohead-spike/10.1-CONTEXT.md` — Spike decisions and code context

</canonical_refs>

<code_context>
## Existing Code Insights

### Reusable Assets
- `DeviceProvider::HandleEyeToHeadSet()` (device_provider.cpp:740-808) — Core SetDisplayEyeToHead + matrix construction logic. Becomes the production `ipd` command implementation
- `DeviceProvider::ReadEyeToHeadRotation()` (device_provider.cpp:531-738) — Config reading via lighthouse_console. Must be refactored to read from lhr-*/config.json instead
- `ParseEyeToHead3x3()` (device_provider.cpp:444-491) — JSON 3x3 matrix parser. Reusable for config.json parsing
- `ExtractVrPathValue()` (device_provider.cpp:495-529) — openvrpaths.vrpath parser. Reusable for config dir discovery
- `DeviceProvider::HandlePipeCommand()` (device_provider.cpp:199-343) — Pipe command dispatcher. Add `ipd`, `ipd?`, `load_lh_config` and remove spike/legacy commands
- `UserSignature` (user_signature.h/cpp) — HID user flash parser. Extend to extract lighthouse serial

### Established Patterns
- HMD container access: `VRProperties()->TrackedDeviceToPropertyContainer(k_unTrackedDeviceIndex_Hmd)`
- Pipe command format: `"command arg"` in, `"OK key=value"` / `"ERR message"` out
- State-change guard pattern in SetHmdProximity (only write on change)
- Cached rotation matrices: `m_bEyeToHeadCached`, `m_cachedLeftRot[3][3]`, `m_cachedRightRot[3][3]`

### Integration Points
- `DeviceProvider::Init()` — Read initial IPD from HMD container + eager rotation cache from config.json
- `DeviceProvider::RunFrame()` — VREvent_IpdChanged tracking (already implemented)
- `DeviceProvider::HandlePipeCommand()` — New command routing
- `HidDevice` — Expose lighthouse serial from user flash data

</code_context>

<specifics>
## Specific Ideas

- The `eyetohead_set` spike code is the proven, working implementation — production `ipd` command is a refactored version of it, not a rewrite
- Lighthouse serial from user flash is the same data source as proximity calibration — both come from HID user signature data
- The `load_lh_config <path>` command is a deliberate plan B — if auto-discovery fails, an external program (e.g., Beyond Utility) can provide the config path
- Tundra SiP documentation should be researched during the research phase — may reveal IPD persistence capabilities relevant to Phase 13 and potentially simpler config reading for Phase 11

</specifics>

<deferred>
## Deferred Ideas

- Tundra SiP IPD persistence investigation — potentially relevant to Phase 13 (slider persistence) if the SiP driver handles config writes when IPD properties change on the HMD container
- SteamVR IPD slider UI — Phase 12

</deferred>

---

*Phase: 11-core-ipd-pipe-command*
*Context gathered: 2026-03-25*
