fix(fusion): break the zero-velocity/innovation-gate deadlock

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>
This commit is contained in:
onur cayan
2026-08-07 22:05:40 +03:00
committed by onur
parent b4920d31e3
commit 8349f32df1
4 changed files with 391 additions and 56 deletions
@@ -30,6 +30,7 @@ typedef struct {
double calibration_duration_s;
unsigned int calibration_min_samples;
double calibration_accel_norm_tolerance_mps2;
double calibration_accel_offset_allowance_mps2;
double calibration_gyro_limit_rps;
double calibration_accel_std_limit_mps2;
double calibration_gyro_std_limit_rps;
@@ -44,6 +45,7 @@ typedef struct {
double position_recovery_timeout_s;
double position_recovery_max_speed_mps;
double position_recovery_sigma_multiplier;
double gnss_velocity_motion_timeout_s;
unsigned int position_recovery_min_samples;
} fusion_engine_config_t;
@@ -54,16 +56,33 @@ typedef struct {
fusion_gnss_motion_evidence_t gnss_evidence;
double last_accepted_position_m[3];
double last_accepted_position_covariance_m2[9];
/*
* Displacement baseline for GNSS motion evidence. Held separately from
* the accepted-fix baseline because the evidence must keep updating while
* the innovation gate is rejecting fixes -- that is precisely when it is
* the only signal able to release the zero-velocity update.
*/
double last_evidence_position_m[3];
double last_evidence_position_covariance_m2[9];
double last_gnss_speed_mps;
double zero_gyro_integral_rad[3];
double zero_gyro_duration_s;
uint64_t filter_timestamp_ticks;
uint64_t last_zero_update_ticks;
uint64_t last_accepted_position_ticks;
uint64_t last_evidence_position_ticks;
uint64_t last_gnss_velocity_ticks;
uint64_t last_position_fuse_ticks;
unsigned int gnss_moving_count;
unsigned int gnss_still_count;
bool have_held_imu;
bool have_accepted_position;
bool have_evidence_position;
bool have_gnss_velocity;
/* Latched once GNSS Doppler has shown the platform moving, and cleared
* only by a later fix showing it slow. A timeout cannot clear it: an
* unaided platform gives no evidence that it has stopped. */
bool gnss_confirmed_moving;
bool vehicle_at_rest;
} fusion_runtime_t;
@@ -96,6 +115,11 @@ typedef struct {
double gyro_mean[3];
double accel_m2[3];
double gyro_m2[3];
/* Running statistics of the accelerometer magnitude. Comparing a sample
* against the window's own mean cancels any fixed sensor offset, which
* leaves the whole norm tolerance available to detect real acceleration. */
double norm_mean;
double norm_m2;
unsigned int sample_count;
bool have_start;
} fusion_calibration_accumulator_t;