Sensor-fusion polish: calibration badge, 3D replay, richer CSV, chart-label fixes - #25
Merged
Conversation
Users who think in Euler angles had to do quaternion math in a spreadsheet. The exporter now appends host-computed Tait-Bryan angles (heading 0-360, pitch +/-90, roll +/-180) after the raw w/x/y/z columns, in both logged-session and live-buffer CSVs. Derivation happens at export via a new MWDataConvertible hook so stored samples stay raw. No derived yaw column on purpose: the firmware's yaw channel is gyro-INTEGRATED (unbounded, drifts) and cannot be reconstructed from a single orientation quaternion - it would just duplicate heading. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Bosch fusion outputs (especially heading) are unreliable until the algorithm calibrates against real motion, and the app never said so - users had no way to know their orientation data was garbage. While any fusion channel streams, a badge now shows the chip's own 0-3 accuracy per sensor (A/G/M chips colored by level) with a rotate-to-calibrate hint until everything reads high. The read ([0x19, 0x8B]) is only valid while fusion is RUNNING, which is exactly when the badge shows: polling starts with the streams and ends with them (pause included). Any poll failure just stops the updates - a courtesy readout must never tear down the streams. Gated on sensor- fusion revision >= 1 (C++ CALIBRATION_REVISION). The demo transport answers all-high, so the badge is simulator-testable. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Two fixes for saved sessions: 1. The detail chart styled everything with generic x/y/z/w labels, which actively MISLABELED fusion sessions - a quaternion's first channel is w (rendered as "x"), and Euler channels are heading/pitch/ roll/yaw. Styles now resolve from the persisted kind (fusion types) or the capture-time label (units, colors, and the captured +/-range for everything else), falling back to generic only for legacy records. 2. Quaternion sessions gain a 3D replay: the same board view the live stream uses, driven by a playback clock instead of BLE, with play/ pause, a scrubber, and a 1x/2x/4x speed toggle. Timing comes from the board's own tick clock (normalized 0-based; clamped monotonic so a stray out-of-order tick can't break the scrub binary search). QuaternionRealityView is a pure function of "latest sample", so replay is just choosing which sample is latest - and it picks up the real MetaMotion.usdz automatically once that ships. Also deletes SessionHistoryViewModel, dead since the history screen went direct-to-store. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
55%/45% of the container height crowded out the chart and replay controls; 42%/35% (min 240pt) leaves the board plenty readable while keeping the rest of the screen above the fold. Applies to both the live view and session replay, which share this component. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Field test: all-green is nearly unreachable at a desk and doesn't stick - the fusion algorithm continuously re-evaluates accuracy and demotes the magnetometer the moment it sees field distortion, which indoors is constant (laptop, monitor, chargers, metal). Nagging users toward permanent green was asking the impossible. Bosch's own guidance is that MEDIUM (2 of 3) is good enough to record, so the badge now has three states: Calibrating (any sensor below medium), Ready To Record (all at least medium - the blue state), and Fully Calibrated (all high). While calibrating, each sensor below the bar gets ITS OWN instruction - the three sensors calibrate with different motions (accel: rest on each face; gyro: hold still; mag: figure-8 away from electronics), so one generic "rotate the board" line couldn't get anyone to green. The ready state explains that a dipping M rating indoors is normal. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Built from the owner's CAD STEP exports (100-050 bottom shell + 100-060 top-cover assembly with the button; 100-051 is the same top without the button and was skipped). Pipeline: OpenCascade (cadquery) tessellation at 0.05 mm -> SceneKit assembly with PBR materials (top shell brighter/smoother than bottom so the seam reads) -> USDZ. Both parts share one CAD frame so they align natively, and the case already sits long-axis-Y / face-toward-+Z - the exact orientation QuaternionRealityView expects, so no axis correction needed. The loader finds it by name and the synchronized project group bundles it - zero code change; the procedural board remains as the fallback. CAD meshes carry no textures, so the shells are tinted white in-material like the physical case. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The top-cover STEP is an assembly of three solids (front cover, button, LED light pipe) that the first conversion flattened into one white mesh. Exploding the compound lets each part carry its own material: grey button, glossier lens-like LED window, white shells - matching the physical device. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Hardware test: the model's rest pose was right but rotations came out about the wrong axes - physical yaw rolled the model, etc. The fusion quaternion lives in Bosch's Z-up world; RealityKit is Y-up with Z toward the camera. Applying the raw quaternion mixed the two frames. Fix is a change of basis: conjugate by the fixed -90-about-X rotation (q' = M q M^-1) that sends Earth-up to screen-up. Conjugation - not a pre-multiply - is the load-bearing choice: identity conjugates to identity, which is exactly the observed symptom pattern (rest pose correct, axes switched). Also guards against zero/malformed samples that would produce NaN orientations. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Hardware truth session: with the -90-about-X remap, yaw (spinning the
board flat on the desk) tipped the model away, pitch turned it left-
right, and roll flipped it. Three axis-isolated motions pin down the
frame exactly: the board's vertical lives in the quaternion's X
component, the lateral (pitch) axis in Z, the longitudinal (roll)
axis in Y. The wire parse is NOT the culprit - our decoder matches
the C++ SDK's memcpy into {w,x,y,z} - it is the frame convention
itself, against what the docs imply. The radio wins again.
Two consumers relabeled to that frame:
- QuaternionRealityView: sensorToScene becomes the cyclic X->Y, Y->Z,
Z->X map (120 degrees about the (1,1,1) diagonal), so yaw spins the
model about the screen's vertical, pitch tips it toward the viewer,
and roll steers in-plane.
- Quaternion.derivedEulerAngles: heading now derives about quat X
(the true vertical), pitch about Z, roll about Y. The previous
formulas produced a "heading" about a horizontal axis. Tests
updated to the empirical frame, including the pitch-pole clamp.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Hardware round 2: every motion landed on the right screen axis, but yaw and pitch ran backwards while roll was correct. Mirroring exactly those two images (X to -Y, Z to -X, Y to Z unchanged) is still a proper rotation - sign flips come in pairs - namely the 120-degree turn about the (1,-1,-1) diagonal instead of (1,1,1). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Hardware round 3 was the decisive one: the same physical yaw excited DIFFERENT quaternion components than in earlier sessions, which no fixed remap constant can explain - the fusion quaternion's absolute frame is not stable session-to-session (magnetic heading reference, world-frame initialization). Fitting constants to it was a losing game. The view now renders motion RELATIVE to a reference sample (tare): delta = ref_inverse * q is the board's rotation since the reference, expressed in the reference pose's body axes - magnetic north, the session's world frame, and the convention ambiguity all cancel. The reference defaults to the first sample, so the board always starts face-on, and a Zero button re-anchors on demand. The body-to-scene map is the IDENTITY, and that is load-bearing, not a shrug: body-axis motions right-multiply the tared delta, which the display composes in the model's local frame - so the model rotates about its own case axes exactly as the physical board does, for any tare pose. That invariant is what the fixed-frame attempts could never provide. If hardware still shows all-mirrored directions, the convention is world-to-body and the one-line fix is inverting delta. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Hardware round 4 on the tared view: rotation about the face normal tracked flawlessly, pitch and roll ran backwards. Negating the X and Y rotation axes while keeping Z is diag(-1,-1,1) - a proper rotation, namely 180 degrees about Z. Physically: the IMU's X/Y axes run opposite the case axes the model is authored in (chip mounted rotated 180 about the face normal inside the case). One constant, and the tare invariant guarantees it cannot vary by session or pose. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Hardware round 5, with the motion finally described precisely: front- back rocking renders as left-right rocking and vice versa, while face-normal rotation is flawless. That is an X<->Y axis INTERCHANGE, not the direction mirror the earlier "flipped/reversed" reports were read as - an interchange no 180-degree map can produce. The chip is mounted rotated 90 degrees about the face normal, its X along the case's length. bodyToScene becomes the 90-about-Z rotation carrying chip axes onto case axes. The sign of the 90 is the one remaining free parameter (no PCB axis diagram exists to derive it); if both rocking motions land on the right axes but run inverted, flip iz. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Validated with paired captures on hardware: streamed the firmware's Euler channel and the quaternion channel at the same two static poses (~34 deg tilt and ~9 deg tilt) and compared. The firmware convention in classic-ZYX terms: heading is the classic yaw about z (this part was standard all along), but the firmware's "pitch" is the NEGATED classic roll about x, and its "roll" the NEGATED classic pitch about y. With that relabel, all three derived angles agree with the firmware to under 1 degree at both poses - and heading matched across two separate sessions, so absolute derived angles are meaningful. Also collapses IEEE -0.0 in the derived degrees so identity rows print "0.0000" rather than "-0.0000". Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
No description provided.