v0.3.6
Four GNSS defects, all of them found by running FusionCore on real hardware rather than by the test suite. Three were reported or triggered by issue #73.
The common thread: a quality gate expressed as an absolute distance in metres, calibrated against clean or simulated GPS, silently discarding good measurements on a real receiver. A rejected fix costs accuracy but surfaces nowhere except a throttled log line, so the symptom looks like "the filter is bad" rather than "the gate is wrong".
The DOP gate compared metres against thresholds named as DOP
sensor_msgs/NavSatFix carries no DOP fields, so the node derives fix quality from position_covariance as sqrt(variance), which is metres. That was compared against gnss.max_hdop (4.0) and gnss.max_vdop (6.0), parameters every ROS user reads as the dimensionless geometry factor. What the defaults actually meant was "reject any fix worse than 4 m horizontal or 6 m vertical".
Measured on 500 fixes from a u-blox NEO-M9N outdoor run: 3.6 to 6.0 m horizontal, 14.4 to 24.0 m vertical. All 500 were rejected at shipped defaults, and the filter dead-reckoned the whole run.
GnssFix now carries explicit sigma_xy / sigma_z in metres, gated by the new gnss.max_sigma_xy (25.0) and gnss.max_sigma_z (50.0). max_hdop / max_vdop keep their original meaning and apply only when a fix carries no covariance, which in practice means gps_msgs/GPSFix reporting receiver-native DOP. Accepted fixes are fused exactly as before: replaying the same bag gives a bit-identical trajectory.
The jump gate rejected ordinary GPS noise
gnss.max_speed bounded a fix at max_speed * gap + margin, entirely absolute metres. At 1 Hz with max_speed: 2.0 and the default 5 m margin that is a 7 m bound, and a receiver whose own sigma is ~6 m trips it constantly. Measured: 157 of 500 good fixes rejected, loop closure 2.62 m to 7.27 m, on a rover whose real top speed is 0.6 m/s, so no honest motion was ever involved.
The bound now adds gnss.max_speed_sigma_k (default 5.0) multiples of the receiver's reported sigma. With the gate enabled at 2.0 the same bag gives 0 of 500 rejected and a 2.62 m closure, identical to disabling it, while a 700 m spike is still rejected.
The noise term scales with the receiver's sigma and deliberately not with the filter's covariance. Chi-squared is already the covariance-scaled test, and this gate exists to catch what a coast-inflated chi-squared admits, so scaling it by P would reopen the hole it was built to plug. A test pins that property.
GPS track heading overrode better heading sources
Track heading derives yaw from the GPS displacement bearing, which is course over ground. On a curved path that differs from body heading by a real bias, not just noise, so fusing it pulls the estimate wrong regardless of its covariance.
It ran even when a stronger absolute source was already active (dual antenna, magnetometer, or 9-axis IMU orientation), all of which the heading-source ladder already ranks above it. Separately, gps_track_heading_min_speed and gps_track_heading_max_yaw_rate were documented as guarding this fusion but only gated the heading_validated_ flag, so a slow turning robot fused its turn radius as a heading.
Reported by a user whose Nav2 path was straight without GPS and a zig-zag with it, on a robot with a stable magnetometer. Both guards now apply to the fusion. Robots with no absolute heading source are unaffected, pinned by a regression test.
GNSS rejections were reported as NOT_PROCESSED
The enum-to-string mapper feeding /fusion/debug/gnss_status had no case for IMPLAUSIBLE_JUMP, so it fell through to NOT_PROCESSED, the value meaning "update_gnss was never called". Fixes disappeared with no stated cause. The mapper is now exhaustive and covers the two new sigma reasons.
Also
- Corrected the GPSFix driver list:
nmea_navsat_driverpublishesNavSatFix, notgps_msgs/GPSFix(verified against the driver source, reported on #73) UKF::last_position_correction()exposes how far the last update moved position, for per-sensor attribution- The
Wm[0] = -99sigma-weight conditioning hazard at 23 states is documented inukf.cpp
126 tests, 0 failures.
A note on the benchmarks
tools/benchmark_baseline.json is marked SUPERSEDED in this release. Every entry in it was measured with the jump gate in its old form, so those numbers are not a valid reference for current main. The published NCLT figures remain stale pending a controlled full-suite re-run, and nothing in this release updates them.