# Predecessor project: openvr-resilient — reference

Repo: `C:\Users\decid\Documents\projects\openvr-resilient` (read-only; working tree is on branch `libsurvive-sauna`, clean).
Investigated 2026-06-09 via `git show`/`git log` without touching the tree. Paths below are repo-relative; plain paths = working tree (= `libsurvive-sauna` branch); other branches cited as `branch:path`. All custom work is by Reavo End <decidel@gmail.com>, Mar 3 – Apr 13, 2025.

## TL;DR

- openvr-resilient is a **fork of ValveSoftware/openvr** (the public OpenVR SDK repo, base commit `ae46a8d` = SDK 2.5.1). Goal: keep getting head-orientation (raw IMU) data from a SteamVR HMD **after optical/lighthouse tracking is lost** — i.e., exactly the "Beyond as basestation-less spatial display" problem.
- Three approaches were attempted in sequence, none reached a working end-to-end product:
  1. **API-shim**: rebuild `openvr_api.dll` with a new `IVRRawIMUData` interface (fake data derived from pose velocity; dead end).
  2. **Sauna v1 SteamVR driver** (`drivers/sauna/`): wrap Valve's `driver_lighthouse.dll`, inject IMU fallback poses. Wrapping the lighthouse driver failed; pivoted mid-file to **direct Windows HID reads** of the headset's tracking SiP — this part actually produced real data.
  3. **libsurvive pivot** (`libsurvive-sauna` branch): vendored Collabora libsurvive (source submodule + prebuilt Windows binaries), then **wiped the Sauna v1 sources** to restart on a libsurvive foundation. Work stopped there (2025-04-13). The new `sauna` repo is the successor.
- **The single most valuable artifact** is captured runtime evidence (debug logs committed to git) proving the Beyond's onboard Watchman/Tundra SiP is readable as a plain USB HID device with **no driver, no SteamVR running**: VID `0x28DE` PID `0x2300`, interface MI_00 ("IMU") streaming 52-byte report-ID-`0x20` IMU packets via blocking `ReadFile`. Details in §6.

## 1. Repo anatomy / provenance

- Fork of github.com/ValveSoftware/openvr. Remotes: `origin` (the user's fork; master at SDK 2.5.1), `upstream` (Valve; master at SDK 2.15.6). `origin/tempBeta` / `upstream/tempBeta` = Valve's 2.7.1 beta, untouched.
- `master` = pristine SDK 2.5.1 (`ae46a8d`, 2024-03-26). No local changes on master.
- Everything custom lives in 22 local commits stacked on `ae46a8d`:
  - `resilient-imu` (local, **ahead 22** of `origin/resilient-imu`, which still points at `ae46a8d` — nothing custom was ever pushed): 20 shared commits + `7309691` (HID analysis toolset) + `d89c282` ("Attempts to improve stuff but very broken. Heh.", 2025-04-13, tip).
  - `libsurvive-sauna` (local-only, checked out): same 20 shared commits (through `8663934` "Fix build script") + `cf5f524` ("Add libsurvive") + `a58203e` ("Wipe Sauna early version files", tip, 2025-04-13).
- So the two branches diverge at `8663934`; `resilient-imu` kept hacking on the v1 driver + built HID tooling; `libsurvive-sauna` added libsurvive and deleted the v1 driver sources.
- `.roomodes` (12 lines, "Boomerang Mode" orchestrator) confirms the project was driven with Roo Code (AI agent). Much of the code/docs is AI-generated; treat doc claims skeptically (several are demonstrably aspirational — see §9).

## 2. Commit timeline (the 22 local commits)

| Date | Hash | Subject | Substance |
|---|---|---|---|
| 2025-03-03 | `e4bc79f`, `0de87d7` | mods plan | `OpenVR_Modifications.md` design doc |
| 2025-03-05 | `0f036fb`, `685913a`, `8e531b9`, `ccc5dac` | IVRRawIMUData prototype | edits to `headers/openvr.h`, `headers/openvr_driver.h`, `src/openvr_api_public.cpp` |
| 2025-03-05 | `b3c9599`…`4428570` (7 commits) | VS2022/MSBuild build fixes | `openvr_api.vcxproj`, `build.bat`, `src/vrcore/*_fixed.*`, committed `build/` dir + rebuilt `bin/win64/openvr_api.dll` |
| 2025-04-12 | `9148e45`, `b160f21` | **Initial implementation of Sauna** | `drivers/sauna/*` driver v1 |
| 2025-04-12 | `32e133f` | testing framework | `drivers/sauna/tests/*` |
| 2025-04-12 | `f7fd270`, `8ded47a`, `8663934` | docs, state note, build fix | `drivers/sauna/docs/*`; deprecation note added to `OpenVR_Modifications.md` |
| 2025-04-13 | `7309691` (resilient-imu only) | **HID analysis toolset** | `tools/*` |
| 2025-04-13 | `d89c282` (resilient-imu tip) | "very broken" | rewrote `lighthouse_driver_wrapper.cpp` to direct-HID approach; committed debug logs + built `driver_sauna.dll` |
| 2025-04-13 | `cf5f524` (libsurvive-sauna) | **Add libsurvive** | submodule + prebuilt release |
| 2025-04-13 | `a58203e` (libsurvive-sauna tip) | **Wipe Sauna early version files** | deleted the 7 v1 driver source files (1111 lines) |

## 3. Phase 1 — `openvr_api.dll` shim with `IVRRawIMUData` (dead end)

Design doc: `OpenVR_Modifications.md` (306 lines, working tree). Header note added by `8ded47a`: *"!!IMPORTANT: This early proposed solution is now deprecated. The development efforts of openvr-resilient have now been channeled into the Sauna driver at `drivers/sauna/`."* That note is the entire "statement of current project state".

What was actually built (cumulative diff `ae46a8d..8663934`):

- `headers/openvr.h` (+25): client-side `vr::IVRRawIMUData` interface — `GetRawGyroscopeData(float*x,y,z)` (rad/s), `GetRawAccelerometerData` (m/s²), `GetIMUSampleTimestamp(double*)`, `GetIMUSample(vr::ImuSample_t*)`; version string `"IVRRawIMUData_001"`.
- `headers/openvr_driver.h` (+59): a *different, conflicting* driver-side `IVRRawIMUData` (`GetIMUData`, `GetIMUSampleTime`) plus a concrete `CRawIMUData` latch class (latest-sample + has-new-data flag) defined directly in the header.
- `src/openvr_api_public.cpp` (+155): the real shim. A global `CRawIMUData g_rawIMUData`; `VR_GetGenericInterface("IVRRawIMUData_001")` returns it *before* touching vrclient. **Critical flaw:** the "IMU" sample is synthesized by `ExtractIMUDataFromPose()` which copies `TrackedDevicePose_t.vVelocity` → vAccel and `.vAngularVelocity` → vGyro — i.e., *pose-derived velocities mislabeled as raw IMU*, and it is only refreshed once, at the moment a client requests `IVRSystem` (inside `VR_GetGenericInterface`), never per-frame. It also sets `unOffScaleFlags` when |v|>10 m/s or |ω|>20 rad/s. So the interface never returned real, continuous IMU data. When the HMD is untracked, `bPoseIsValid` is false and even that path yields nothing.
- Also added `VR_INTERFACE`/`VR_CALLTYPE` decorations to `VR_GetInitToken`/`VR_InitInternal2`/`VR_ShutdownInternal`/`VR_GetGenericInterface`/`VR_IsInterfaceVersionValid` so the rebuilt DLL exports them (`bin/win64/openvr_api.exp/.lib` committed); replaced `bin/win64/openvr_api.dll` (Valve's 839 KB → own 133 KB build — note: this drops everything else Valve's shipping DLL contains).
- `ImuSample_t` (upstream, `headers/openvr_driver.h`): `{ double fSampleTime; HmdVector3d_t vAccel; HmdVector3d_t vGyro; uint32_t unOffScaleFlags; }` with `Imu_OffScaleFlags` bits AccelX..GyroZ (0x01..0x20).
- `OpenVR_Modifications.md` "Alternative approaches" §1 correctly identifies **`IVRIOBuffer`** as the sanctioned channel for raw IMU (SteamVR drivers publish IMU streams as IOBuffers; the doc guesses path `/dev/imu/hmd`, which is wrong — the lighthouse driver publishes per-device paths like `/devices/lighthouse/<serial>/imu`; that path guess was never tested in this repo). [External-knowledge note, marked speculation: reading the HMD imu IOBuffer while SteamVR runs is a known-working technique in other projects and may be the cheapest "IMU while tracked-or-not" channel for sauna when SteamVR is present.]

Verdict: architecture salvageable as an idea (client-visible IMU interface), implementation worthless.

## 4. Phase 2 — Sauna v1 SteamVR driver (`drivers/sauna/`)

Sources were deleted from the working tree by `a58203e`; read them at `cf5f524:drivers/sauna/...` (pre-wipe) or `resilient-imu:drivers/sauna/...` (latest, partially diverged). Docs/tests/manifest/build script survive in the working tree.

Architecture (per `drivers/sauna/README.md`, which matches code):

- `driver_sauna.cpp` — `SaunaDeviceProvider : IServerTrackedDeviceProvider`, exported via `HmdDriverFactory`. Owns a `LighthouseDriverWrapper` + `IMUDataProvider`. Its `RunFrame()` literally contains *"In a real implementation, we would collect IMU data here"* — v1 (pre-`d89c282`) had **no IMU source at all**.
- `imu_data_provider.{h,cpp}` — thread-safe per-device `std::queue<vr::ImuSample_t>` (cap 100), `RegisterDevice/AddIMUSample/GetLatestIMUSample/IsIMUDataAvailable`. Pure plumbing.
- `sauna_device_driver.{h,cpp}` — `SaunaDeviceDriver : ITrackedDeviceServerDriver + IVRIMUComponent`, a decorator around a real device driver. `GetComponent("IVRIMUComponent_001")` exposes the custom component; `GetPose()` passes through while tracking is good (caching last good pose) and, when `pose.result` ∈ {`TrackingResult_Fallback_RotationOnly`, `Calibrating_OutOfRange`, `Running_OutOfRange`}, replaces the pose via `IntegrateIMUData()`.
- `IntegrateIMUData()` fusion math (naive dead reckoning, no filter despite doc claims of "complementary filter"): gyro |ω|·dt → axis-angle → quaternion, right-multiplied onto `qRotation` and normalized; accel minus hardcoded world-gravity `(0, 9.81, 0)` (NOT rotated into world frame — acknowledged "simplified" in a comment) integrated to velocity with 0.95 decay per sample, then to position. Drift-prone and frame-incorrect; do not reuse as-is.
- `lighthouse_driver_wrapper.{h,cpp}` — see §5/§6; this file mutated the most.
- `driver.vrdrivermanifest`: `{ "alwaysActivate": true, "name": "sauna", "directory": "", "resourceOnly": false, "hmd_presence": ["*.*"] }`.
- `drivers/sauna/build.bat`: bare `cl` (VS2022/2019 vcvarsall x64) compiling the 4 .cpp into `bin/win64/driver_sauna.dll`, `/I..\..\headers`; copies manifest. Built DLL committed at `resilient-imu:drivers/sauna/bin/win64/driver_sauna.dll` (41 984 B).
- Install method (per README/user_guide): copy `bin\win64\*` into `<SteamVR>\drivers\sauna\` and restart SteamVR (i.e., a normal external driver; `vrpathreg` not used/mentioned).

### Lighthouse-wrapper saga (why it failed)

`cf5f524:drivers/sauna/lighthouse_driver_wrapper.cpp` (261 lines): `LoadLibrary` on `driver_lighthouse.dll` (paths tried: `C:\Program Files (x86)\Steam\steamapps\common\SteamVR\drivers\lighthouse\bin\win64\driver_lighthouse.dll`, Program Files variant, `%USERPROFILE%\AppData\Local\openvr\drivers\...`; Linux/macOS path lists exist but the rest is `#ifdef _WIN32` only), resolve `HmdDriverFactory`, create `IServerTrackedDeviceProvider`. Loading worked (after preloading/copying `openvr_api.dll` beside it — dependency resolution hack retained in the final version), and `CreateInterface` returned a provider, **but the provider could not be initialized outside vrserver** (no valid `IVRDriverContext`), so `RunFrame` on it was useless; logs show endless `m_pLighthouseProvider is null`.

`resilient-imu:drivers/sauna/lighthouse_driver_wrapper.cpp` (876 lines, `d89c282`) abandons that mid-`Initialize()` with the comment: *"Based on our analysis of lighthouse_console.exe, it's not using driver_lighthouse.dll at all! Instead, it's connecting directly to the lighthouse devices using HID interfaces."* It then enumerates HID devices (SetupAPI + `hid.dll`), keeps Valve VID `0x28DE` devices whose product string contains `Lighthouse`/`LHR-`/`IMU`/`Controller`/`Optical`, stores their paths, unloads the lighthouse DLL, and in `RunFrame()` opens each path with `CreateFileA` and reads reports (tries `HidD_GetInputReport` for report IDs {0,1,2} — always fails, error 87 — then falls back to blocking `ReadFile`, 64-byte buffer). Parsing: int16 LE accel at byte offsets 1/3/5, gyro at 7/9/11, uint32 LE timestamp at 13 (treated as µs), scales accel ×0.0024, gyro ×0.001; pushes into `IMUDataProvider` as device index 0. Hardcoded config duplicated in `lighthouse_device_configs.json` (root; three entries for `HID\VID_28DE&PID_2300&MI_00`, report IDs 1/0/2 — the report-ID part of the config is wrong, see §6). Per-call `fopen` of debug logs and device re-open every frame — performance-hostile scaffolding, not production code.

Status at tip: "very broken" by its own commit message; integration tests 2 passed / 2 failed (`drivers/sauna/tests/integration_test_results.txt`, 2025-04-13 04:07).

## 5. Phase 3 — HID analysis toolset (`resilient-imu:tools/`)

- `tools/hid_device_analyzer.{h,cpp}` (166+691 lines), `tools/main.cpp` (276), `tools/CMakeLists.txt`, `tools/build.bat`, prebuilt `tools/build/hid_analyzer.exe` (394 KB). Windows-only implementation (SetupAPI/`hid.dll`); Linux/macOS stubs empty.
- CLI: `--list`, `--valve` (filter VID 0x28DE), `--device <path>`, `--read N`, `--id <reportId>`, `--analyze`, `--print-descriptor`.
- `--print-descriptor` is mostly a stub (Windows can't dump raw descriptors; it prints `HIDP_CAPS`: usage page, input/output/feature report byte lengths, value caps counts — still useful).
- `--analyze` is a brute-force structure detector: groups reports by ID, finds bytes that vary across reports, flags runs of 2 (16-bit candidates) and 6 changing bytes (XYZ triplet candidates), prints LE int16 interpretations. `PrintReport` additionally always prints two fixed-offset guesses (1/3/5+7/9/11 and shifted by one). `tools/README.md.in` states the purpose: verify the wrapper's hardcoded assumptions (report ID 0x01, offsets 1–12).
- Generic and crude but functional; this is what produced the offset/scale guesses in §4.

## 6. THE HARD DATA — Beyond/Watchman USB HID facts (captured evidence)

Committed debug logs (gold; recorded 2025-04-12/13 on the user's machine, vrserver not required):
`resilient-imu:drivers/sauna/tests/lighthouse_init_debug.log` (device enumeration), `...lighthouse_runframe_debug.log` (2 950 lines incl. raw report hex), `...lighthouse_driver_debug.log`, `...lighthouse_provider_debug.log`, `...lighthouse_config_debug.log`, `...device_registration_debug.log`.

Devices found (init log):

| VID:PID | Interface | Product string | Notes |
|---|---|---|---|
| `28DE:2300` | MI_00 | **"IMU"** | streams IMU (below) |
| `28DE:2300` | MI_01 | **"Optical"** | streams lightcap/optical pulse data |
| `28DE:2300` | MI_02 | **"Controller"** | no data via blocking ReadFile in these runs |
| `28DE:2102` | (×2 instances) | "Valve VR Radio" | Watchman radio dongles (controller RF) |
| `35BD:0101` | MI_02 (HID) | (n/a) | **Bigscreen's own VID** — the Beyond's non-tracking interface(s); never probed by this project |

Attribution: `28DE:2300` is the standard Valve Watchman-firmware tracking device ID also used by Tundra SiP/Tracker hardware; given the simultaneous `35BD:0101` Bigscreen device and the project context, this is the **Beyond's onboard Tundra SiP** with high confidence (marked: inference, not proven in-repo — the repo itself never mentions Bigscreen/Beyond/Tundra anywhere except this captured path).

Observed report traffic (runframe log, via plain `ReadFile` on the HID interface, **SteamVR not involved**):

- **MI_00 "IMU": 52-byte reports, first byte `0x20`** (report ID 32). Example: `20 B3 00 50 0F 4F 03 F7 FF FA FF FD FF A0 B2 6F ...`
  - Decode (LE int16 from offset 1): accel = (179, 3920, 847), gyro = (−9, −6, −3) — device at rest, gravity ≈ +Y of sensor frame.
  - |accel| ≈ 4014 LSB ⇒ ≈ **4096 LSB/g (±8 g full scale)**; the repo's accel scale **0.0024 m/s²/LSB** (≈9.81/4096) is consistent with this data (verified arithmetic, this analysis). Gyro scale 0.001 rad/s/LSB was asserted, not verified (plausible for ±2000 °/s on int16; marked speculation).
  - 52 bytes matches the classic Valve Watchman `0x20` IMU report layout: `uint8 reportId; 3 × { int16 acc[3]; int16 gyro[3]; uint32 timecode; uint8 seq; }` (1+3×17=52) — i.e., **three IMU samples per report** [external knowledge / libsurvive `driver_vive.c`, marked speculation but strongly corroborated by the 52-byte length and offset arithmetic]. The repo only ever parsed sample 0 (offsets 1–16) and misread the per-sample timecode as µs.
- **MI_01 "Optical": 64-byte reports, first byte `0x28`** — high-entropy payload (lightcap data). The wrapper *also* parsed these as IMU: every "Parsed IMU data: Accel=[49.9, …]" line in the log (constant 49.9 m/s² X-accel = 0x51xx at offset 1–2 × 0.0024) is a **misparse of optical packets**, not real IMU. Don't trust those lines.
- `HidD_GetInputReport` always fails with Win32 error 87 on these interfaces (they are interrupt-IN streams, not polled input reports) — **use ReadFile/overlapped reads**.
- `lighthouse_device_configs.json`'s claim of "report IDs 0/1/2" is wrong; the device emits 0x20 (IMU) and 0x28 (optical).
- No IMU rate measurement exists in the repo (RunFrame-paced polling). [External knowledge: Watchman IMU effective rate ≈ 1 kHz, ~250–360 reports/s × 3 samples; marked speculation.]
- Nothing in the repo about: HID **feature** reports (none sent — note libsurvive sends a "magic" feature report to enable full streaming on some Watchman devices; the fact that IMU flowed here without any magic is itself a useful datapoint), the `35BD:0101` Bigscreen protocol (brightness/display control etc.), the linkbox, display modes, distortion, eye tracking, or DP/MIPI bridge.

## 7. Phase 4 — libsurvive pivot (`libsurvive-sauna` branch)

- `cf5f524` "Add libsurvive":
  - **Gitlink** (no `.gitmodules`! — registered as a raw submodule entry only) at `drivers/sauna/dependencies/libsurvive` → commit `32cf62c5` ("initialize variables before logging") of **https://github.com/collabora/libsurvive.git** (the maintained fork). The clone exists on disk in the working tree (full source: `src/`, `include/`, `redist/`, plugins).
  - `drivers/sauna/dependencies/libsurvive_release/` — committed **prebuilt Windows binaries**: `bin/libsurvive.dll` (1.3 MB), `survive-cli.exe`, `sensors-readout.exe`, `survive-buttons.exe`, `survive-solver.exe`, `api_example.exe`, `libusb-1.0.dll`, plugin DLLs (`driver_vive`, `poser_mpfit`, `poser_barycentric_svd`, `poser_kalman_only`, `disambiguator_statebased`, `driver_simulator`, `driver_playback`, `driver_global_scene_solver`, `driver_dummy`), .NET bindings (`libsurvive.net.dll`, `Demo.dll`), headers (`include/libsurvive/*` incl. `survive.h`, `poser.h`, `redist/*`, plus `cnkalman/`, `cnmatrix/`), `survive.lib`.
  - `bin/survive-websocketd.ps1`: `websocketd --passenv LOCALAPPDATA --port 8080 survive-cli --record-stdout --record-cal-imu --no-record-imu $args` — a one-liner exposing libsurvive's pose/IMU stream over WebSocket :8080 (record-format text on stdout). Indicates the intended integration shape: run libsurvive out-of-process, consume its stream.
- `a58203e` "Wipe Sauna early version files": deleted the 7 v1 sources (`driver_sauna.cpp`, `imu_data_provider.{h,cpp}`, `lighthouse_driver_wrapper.{h,cpp}` — at its 261-line pre-d89c282 state — and `sauna_device_driver.{h,cpp}`). Docs/tests/manifest/build.bat kept. **No new code was ever written after the wipe** — the libsurvive-based rewrite never started in this repo.
- Why libsurvive matters: it is the open-source lighthouse stack whose `driver_vive` plugin already implements exactly what Phase 3 was reinventing — Watchman USB HID enumeration (incl. `28DE:2300`), report decode (0x20 IMU, lightcap), bias/scale calibration download from the device, and IMU-only orientation filtering (`poser_kalman_only`, cnkalman) that works **without any basestations**.

## 8. Build system notes

- Root `build.bat` (2 lines): `MSBuild.exe openvr_api.vcxproj /p:Configuration=Release /p:Platform=x64`. `openvr_api.vcxproj` (root, 136 lines) added because CMake-generated VS solutions misbehaved; an entire stale CMake `build/` tree (CMake 3.30.5, VS2022 v23 toolchain) is committed.
- VS2022 compile fixes (`a9fe9a1`…`4428570`): `src/vrcore/pathtools_public_fixed.{h,cpp}` and `sharedlibtools_public_fixed.cpp` are patched copies; original `pathtools_public.h` gutted to a redirect (`780a920` "Redirect to fixed version"); `DYNAMIC_LIB_EXT` defined in `openvr_api_public.cpp` and literal-concat replaced with `std::string(...).append(...)`. `1dc31f5` notes are in commit history only.
- `lib/win64/openvr_api.lib` ballooned 5.5 KB → 8.7 MB (static import lib of their build).

## 9. Tests & docs inventory (with credibility caveats)

- `drivers/sauna/tests/`: `test_utils.h` (tiny TestSuite/TestResult framework), unit tests per component (`test_imu_data_provider.cpp`, `test_lighthouse_driver_wrapper.cpp`, `test_sauna_device_driver.cpp` + `run_unit_tests.*`), `integration_test.cpp` (615→interactive console harness; links wrapper classes directly rather than going through vrserver; prompts "Make sure SteamVR is installed but not running"), `performance_test.cpp` (648 lines: CPU/mem/latency/throughput measurement scaffolding), `validation_test.cpp` (556), `.bat` runners, committed `integration_tests.exe`. Final recorded result: 2/4 integration tests passing.
- `drivers/sauna/docs/`: `api_documentation.md` (578), `developer_guide.md` (262), `user_guide.md` (229), `testing_guide.md` (522). AI-generated; mostly accurate restatements of the code, but the client-side usage examples are **wrong/aspirational** — they call `vr::VRSystem()->GetComponentForPropertyContainer(...)` from an application to fetch `IVRIMUComponent`, which is not a thing client-side (components are driver-internal; a real client channel would need IOBuffer/debug-request/IPC). The README's "complementary filter" claim is also false (see §4).
- `docs/Driver_API_Documentation.md` is **upstream Valve's** driver docs (came with SDK 2.2.3 merge), not project-authored — but it is a good local reference for `IVRDisplayComponent`/`IVRVirtualDisplay`/`IVRDriverDirectModeComponent`, properties, poses, etc., which sauna will need.

## 10. What openvr-resilient never touched (gaps sauna must fill)

- Bigscreen-VID (`35BD:0101`) protocol: display control, brightness, fan, eye-tracking; never opened.
- Distortion correction / warp mesh, display modes, EDID, DP bridge (VXR7200), linkbox, MCU firmware, eye-tracking FPGA — zero coverage.
- Linux: all device I/O is `#ifdef _WIN32`; Linux branches are empty stubs.
- IMU calibration: raw LSB→SI scales were guessed; the Watchman on-device calibration (bias/scale via feature/config reports) was never read.
- Orientation filter: nothing beyond naive integration; no Mahony/Madgwick/Kalman, no gravity correction in the correct frame.
- Any client/compositor: no way to actually *display* anything on the HMD or feed poses to a renderer.

## 11. Relevance to sauna

**Load-bearing facts to build on**
1. The Beyond's tracking SiP is a standard Watchman USB HID device (`28DE:2300`) and its IMU stream is obtainable on Windows with userland `ReadFile` on the MI_00 "IMU" interface, with no SteamVR, no driver install, no firmware changes. This validates sauna's "no hardware changes, no basestations" premise at the transport level: a host-side daemon reading 0x20 reports gives gyro+accel for 3DOF.
- Report: 52 B, ID `0x20`, 3 samples × (int16 acc[3], int16 gyro[3], uint32 timecode, uint8 seq); ≈4096 LSB/g accel (±8 g). Parse all 3 samples and use timecodes, unlike the predecessor.
- Use overlapped/blocking interrupt reads; `HidD_GetInputReport` does not work (error 87).
2. libsurvive (Collabora fork, already vendored at `drivers/sauna/dependencies/`) is the shortest path to *correct* Watchman handling: device open (hidapi/libusb — also solves Linux), calibration readout, decoding, and IMU-only Kalman orientation (`poser_kalman_only`). The predecessor chose it for exactly this and stopped; sauna can resume from that decision. The websocketd one-liner sketches a workable process topology (tracker daemon → socket → driver/renderer).
3. If SteamVR remains in the stack, two integration shapes were explored: (a) shim/patched `openvr_api.dll` (fragile, breaks signing/updates, and the SDK repo can't rebuild Valve's real runtime DLL — avoid), (b) a proper external SteamVR driver in `<SteamVR>/drivers/<name>` with `driver.vrdrivermanifest` (`alwaysActivate`, `hmd_presence`) — the viable shape, but **don't wrap/load `driver_lighthouse.dll`**: it cannot be initialized outside vrserver (proven failure here), and two drivers claiming the same USB device will fight. For sauna's no-SteamVR mode this is moot; for a SteamVR-compat mode, implement an independent driver that owns the HID device itself (or feed poses via the driver from the sauna daemon).

**Reusable artifacts**
- The debug logs (§6) as protocol ground truth; `lighthouse_device_configs.json` offsets/scales (corrected per §6).
- `tools/hid_analyzer` as a quick probing utility (works today on Windows) — handy for reverse-engineering the `35BD:0101` interfaces next.
- Vendored libsurvive source+binaries; `docs/Driver_API_Documentation.md` (Valve driver docs); the vrdrivermanifest/build.bat as templates; the test-suite skeleton at most as a pattern.

**Redo from scratch**
- All fusion math (gravity-in-wrong-frame dead reckoning, 0.95 velocity decay hack).
- All device I/O architecture (per-frame `CreateFileA`/`fopen`, polling in `RunFrame`, single global "device 0") → dedicated reader thread(s), ring buffer, timestamps from device timecodes.
- The IVRRawIMUData/IVRIMUComponent API design (three inconsistent definitions across `headers/openvr.h`, `headers/openvr_driver.h`, `sauna_device_driver.h`; client docs reference an impossible access path). If an app-facing IMU API is needed, design it once, transport-first (shared memory/socket), not as an OpenVR header hack.
- Anything Linux: nothing exists.

**Strategic lesson** — three pivots in six weeks all stalled on the same misconception: trying to get IMU data *through* SteamVR machinery (modified API DLL, wrapped lighthouse driver) instead of *from the device*. The one afternoon spent reading the HID device directly produced the only real data in the repo. Sauna should start device-first (own the `28DE:2300` and `35BD:0101` interfaces), use libsurvive or a minimal Watchman decoder for the IMU path, and treat SteamVR/OpenVR integration as an optional presentation layer.
