The existing suite passed both before and after the fusion fixes, so it
never covered either failure. Two tests close that gap; both were checked
against the pre-fix sources and fail there.
calibration_offset_test reproduces the bench part's +28 mg zero-g offset
and requires startup to complete, while still refusing a sustained 3 m/s2
acceleration. It also asserts the blind spot rather than leaving it
implicit: horizontal acceleration adds to gravity in quadrature, so
1.5 m/s2 moves the magnitude by only 0.11 m/s2 and is not separable from
offset by any accelerometer gate. Separating it needs GNSS velocity.
long_outage_trajectory_test drives pedestrian, gentle-car (0-100 km/h in
30 s), slow-city and isolated-mount profiles across 1 and 5 minute
outages, using the production rest supervisor rather than a hand-set
flag. Only the isolated-mount slow-city case reproduces the field bug:
GNSS motion evidence compares displacement against covariance, so 4 m/s
under 6 m sigma scores a displacement NIS of 0.22 against a 9.21 gate and
reads as stationary, whereas highway speed scores 42.9 and is correctly
called moving. Pre-fix that case produces 608 zero-velocity updates while
driving, matching the 609 in the board logs.
Drift is judged against the filter's own covariance rather than a fixed
distance. Five minutes unaided legitimately reaches kilometres on this
hardware; what must not happen is the error outrunning the covariance,
which is divergence rather than honest coasting.
tdk_noise_test characterises the sensor at rest through the production
driver, so ODR, full scale, UI filter and LSB scaling match the flight
build, and reports the norms fusion_motion_update() actually gates on.
It also carries an INT1-to-PLIC continuity test that drives the pin to a
static level and reads the PLIC pending bit directly, separating a broken
line from a source that never fires.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Tuning the velocity covariance floors and the zero-velocity veto
threshold needs the receiver's velocity noise, which cannot be recovered
from the existing records: the raw observation and the filter state were
logged in different lines at different rates, so they could not be
differenced.
Hold the converted ENU Doppler velocity from the last fix and publish it
alongside the filter state in one line at one instant, with their
difference (the velocity innovation) and the at-rest flag. With the
platform still, the raw reading is the noise itself.
An IMU does not measure speed; the eskf_* fields are the integrated
result after GNSS correction, and are the only inertial speed available.
First capture, stationary: per-axis sigma 0.12 E, 0.25 N, 0.59 U m/s.
The vertical channel carries most of it, which the previous scalar speed
field could not show.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Halting the core with a debugger, or resetting it while a transaction is
in flight, leaves the sensor's SPI shift register part-way through a
word. Every later access is misaligned, WHO_AM_I reads back 0x00 and
initialisation fails, which is the `sensor=none who=0x00` case in the
field logs.
Toggle chip select around idle bytes to return the device to command
state, then soft reset so the register file starts from defaults
regardless of what the previous image was doing.
This covers the sensor-side wedge. It does not recover a wedged fabric
SPI controller, which a CPU-only debugger reset does not touch and which
was observed to need a board power cycle.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A cruising vehicle produces almost no specific force and almost no
angular rate, so an inertial rest supervisor declares rest while moving.
The engine trusted that flag, and three circular dependencies then locked
the filter out of GNSS entirely:
- GNSS motion evidence was derived only from accepted fixes, so it
froze exactly when a rejection storm made it the one signal able to
veto a false zero-velocity update.
- The zero-velocity update pinned filter velocity near zero, which kept
eskf_speed_mps() below the rest-detector speed limit and latched the
mistake, and clamped the position covariance to metres while the true
error grew to hundreds.
- Position recovery, the intended escape, was gated on being at rest --
unreachable during the moving divergence that needs it.
Field logs show the result: 91 m from GNSS with a 5 m sigma, every fix
failing the NIS gate, gps_age_ms climbing past 40 s and recoveries=0.
Motion evidence now comes from every fix on its own displacement
baseline; accepted Doppler speed vetoes the zero-velocity update and is
taken from the measurement rather than the update outcome, since a filter
that has drifted far enough to reject it is the case that matters; and
that veto latches until a later fix shows low speed, because an unaided
platform gives no evidence that it has stopped.
Startup calibration is fixed alongside it. One tolerance served both as
the sensor zero-g offset allowance and as the motion gate, so the bench
part's in-spec +28 mg offset consumed 93% of it and left no headroom:
calibration_samples thrashed 0,3,0,4,2,1 and never reached the minimum,
so the engine never left CALIBRATING and no fused location was published
at all. The offset allowance is now applied to the calibration window
mean, where a fixed bias actually shows up, leaving the full tolerance to
reject sustained acceleration.
Rest-detector thresholds move into the per-sensor quality profile and are
sized from a stationary bench capture rather than datasheet noise
densities, which describe the bare part and not this board or bandwidth.
Verified on hardware: calibration completes in ~2 s, 1980 fixes accepted
with 1 rejection, sub-metre sigma, gps_age_ms mean 223 ms.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Wait for every navigation-ready tracking channel in a GPS epoch, remove unlocked channels without a timeout, and keep late joins for the next epoch. Reset PLIC ownership during warm initialization and extend Linux synthetic, integrity, and fake-MMIO regression coverage.