From 8a60653a57f324d3d4902a7778e64e0e42976ad5 Mon Sep 17 00:00:00 2001 From: Pete Gordon Date: Sun, 24 May 2026 09:59:30 -0400 Subject: [PATCH] feat(steam-controller): Phase 3a IMU quaternion to visualizer + UI fixes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds quaternion-orientation plumbing for the Steam Controller and the supporting infrastructure (Test Report multi-interface fan-out, runtime axis-transform switcher). Pitch + roll track physical motion correctly in the on-screen model when using the field-tested 'yzp-xy-alt' transform. Yaw signal is captured but ~10× weaker than pitch/roll (typical for IMUs without a magnetometer), and visually not convincing — left as a known limitation for future iteration. Driver (packages/core/src/drivers/steam-controller-driver.js): - Parses 4× int16 LE at bytes 31-38 as quaternion components, returns parsed.orientation = {x, y, z, w}. The first component (bytes 31-32) is actually the high half of the device's uint32 timestamp on this firmware — SteamlessController's docs claim the quaternion lives at data[31-38] but variance analysis of a real Test Report capture shows bytes 29-32 are a ~1MHz uint32 timestamp. Three.js's normalize flattens the timestamp byte's effect enough that pitch + roll visualize correctly. Better encoding TBD — possibly Euler angles in radians, didn't lead to working motion when tried. - emitsRawGyro = false (already) flag tells app.js to skip the rate-based calibration UX since the driver emits orientation, not rates. App (apps/overlay/src/js/app.js): - Quaternion fast-path: when parsed.orientation is present, writes directly into gyroFusion.orientation (bypasses SensorFusion). All downstream consumers (overlay body rotation, gimbal widget, R/P/Y readout, gyro HUD) pick it up unchanged. - Reference-quaternion capture: first valid parsed.orientation becomes the rest reference; every subsequent frame is emitted as delta = current * ref⁻¹. L3+R3 (existing calibrate combo) recaptures the reference. Hooks into existing showCalibHint UX so user sees feedback when they recenter. - Runtime axis-transform switcher: window.setSteamQuatTransform(mode) cycles between 10+ candidate IMU-body→visualizer-world basis transforms. Default 'yzp-xy-alt' = (z, x, y), found via empirical iteration. Other transforms in the registry for future tuning. Test Report wizard (apps/overlay/src/js/test-report.js): - Multi-interface input fan-out: previously the wizard listened on one Puck HID handle (whichever the picker returned) — that's the wrong interface for the Puck, so every step recorded 0 reports. Now mirrors the gyro pipeline's fan-out: enumerates approved siblings with same vid:pid, opens + attaches the recorder to each. Single-interface pads (DualSense filtered by usagePage) are no-op. UI: - Axis readout (P/R/Y degrees) moved from top-right to bottom-center. Was overlapping the Puck status banner. - HUD Position setting renamed → "Roll HUD Position" since it only controls the gyro/roll arc widget, not the axis readout. Verified via Test Report variance analysis on real hardware: - Pitch movement primarily varies bytes 35-36 (353× baseline stddev) - Roll movement primarily varies bytes 33-34 (467× baseline) - Yaw movement varies all components weakly (max 59× baseline, but absolute stddev 10× smaller than pitch/roll) Phase 3a is sized as "best-with-current-understanding" — the right IMU encoding remains unclear and the test infrastructure to iterate further is in place for a future contributor. --- apps/overlay/src/index.html | 13 ++- apps/overlay/src/js/app.js | 98 ++++++++++++++++++- apps/overlay/src/js/test-report.js | 36 ++++++- .../src/drivers/steam-controller-driver.js | 48 ++++++--- 4 files changed, 172 insertions(+), 23 deletions(-) diff --git a/apps/overlay/src/index.html b/apps/overlay/src/index.html index 04558c8..43b92d8 100644 --- a/apps/overlay/src/index.html +++ b/apps/overlay/src/index.html @@ -354,17 +354,20 @@ #axis-readout { position: fixed; - top: 40px; right: 42px; + bottom: 12px; + left: 50%; + transform: translateX(-50%); display: none; gap: 14px; font-family: monospace; - font-size: 30px; + font-size: 24px; font-weight: 600; z-index: 110; - background: rgba(0,0,0,0.35); - padding: 4px 12px; + background: rgba(0,0,0,0.45); + padding: 4px 14px; border-radius: 14px; pointer-events: none; + white-space: nowrap; } body.show-axis-readout #axis-readout { display: flex; } #axis-readout .ax-pitch { color: #44dd66; } @@ -784,7 +787,7 @@

Controller Overlay

- +