Skip to content

Sensor-fusion polish: calibration badge, 3D replay, richer CSV, chart-label fixes - #25

Merged
lkasso merged 14 commits into
mainfrom
fusion-polish
Jul 28, 2026
Merged

Sensor-fusion polish: calibration badge, 3D replay, richer CSV, chart-label fixes#25
lkasso merged 14 commits into
mainfrom
fusion-polish

Conversation

@lkasso

@lkasso lkasso commented Jul 22, 2026

Copy link
Copy Markdown
Member

No description provided.

lkasso and others added 14 commits July 22, 2026 10:15
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>
@lkasso
lkasso merged commit fdb95de into main Jul 28, 2026
6 checks passed
@lkasso
lkasso deleted the fusion-polish branch July 28, 2026 03:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant