Engineering Guides · Module 12
Detecting GNSS degradation
A receiver that reports good accuracy can still be wrong by metres. Cross-check it against the motion you already know before you let it steer the estimate.
Assumptions: Horizontal position only; odometry (wheel, inertial or LiDAR-inertial) provides a short-term prediction with a known uncertainty. Thresholds are starting points to tune from recorded data. The monitor logic is executed by the site’s tests; ROS 2 commands are not.
1. Engineering problem
Near a building, trees or a bridge, GNSS positions degrade through multipath and fewer satellites. The danger is not only lost fixes: the receiver can keep reporting a small covariance while the position is several metres off. The estimator must decide, from data it can check, when to stop trusting GNSS.
2. When is this needed?
- Outdoor-to-indoor transitions (Module 12 flagship scenario).
- Operation in urban canyons, under canopy or near large structures.
- Any robot that fuses GNSS with odometry and must not jump.
3. Selection and design decisions
Signals that indicate degradation
| Signal | Where it comes from | Strength | Weakness |
|---|---|---|---|
| Fix status | NavSatFix.status.status: −1 no fix, 0 fix, 1 SBAS, 2 GBAS | Catches total loss | A “fix” can still be poor |
| Reported covariance | NavSatFix.position_covariance with position_covariance_type (0 unknown, 1 approximated, 2 diagonal known, 3 known) | Easy; many drivers fill it | Some receivers under-report; type 0 means it is not usable |
| Agreement with odometry (innovation) | Distance between the fix and the position predicted from odometry, divided by the combined sigma | Detects silent bias the receiver does not admit | Needs a trustworthy odometry prediction and a consistent frame |
| Satellite count / DOP / signal quality | Vendor messages or NMEA sentences (not part of NavSatFix) | Early warning | Vendor specific; check your receiver |
| Position jump rate | Change between consecutive fixes vs. the speed | Simple, no odometry needed | Slow bias growth is invisible |
4. Advantages and disadvantages
- Tight thresholds: reject early (safe) but may discard good GNSS near open buildings.
- Loose thresholds: keep GNSS longer (continuity of the global pose) but risk absorbing multipath bias.
- Hysteresis: avoids flapping at the cost of a few samples of delay in each direction.
5. Requirements and constraints
- The innovation test is only as good as the odometry uncertainty you give it: unrealistically small predicted sigma rejects good fixes.
- Frames must be consistent: GNSS position converted to the same local frame as the odometry (ENU or NED, same origin).
- GNSS is low rate (1–10 Hz typical, per receiver datasheet): the monitor sees few samples; the delay is in seconds, not milliseconds.
6. Architecture
GNSS receiver ──▶ NavSatFix ──┐
├──▶ GnssMonitor ──▶ state: TRUSTED / DEGRADED ──▶ estimator (weights GNSS, or rejects it)
odometry (LIO/wheel+IMU) ─────┘ (reported sigma + innovation vs prediction, with hysteresis)7. Implementation
class GnssMonitor:
"""Trust GNSS only while its reported accuracy AND its agreement with the odometry-predicted position stay good.
State changes need several consecutive samples (hysteresis) so one outlier does not flip the estimator."""
def __init__(self, sigma_ok=1.5, sigma_bad=3.0, gate_bad=3.0, gate_ok=2.0, bad_count=3, good_count=10):
self.sigma_ok, self.sigma_bad, self.gate_bad, self.gate_ok = sigma_ok, sigma_bad, gate_bad, gate_ok
self.bad_count, self.good_count = bad_count, good_count
self.state, self.bad, self.good = "TRUSTED", 0, 0
def update(self, sigma_h, innovation_m, predicted_sigma_m):
# innovation: horizontal distance between the GNSS fix and the position predicted from odometry
s = (sigma_h ** 2 + predicted_sigma_m ** 2) ** 0.5
z = innovation_m / s # how many sigma apart the two positions are
is_bad = sigma_h > self.sigma_bad or z > self.gate_bad
is_good = sigma_h < self.sigma_ok and z < self.gate_ok
self.bad = self.bad + 1 if is_bad else 0
self.good = self.good + 1 if is_good else 0
if self.state == "TRUSTED" and self.bad >= self.bad_count:
self.state = "DEGRADED"
elif self.state == "DEGRADED" and self.good >= self.good_count:
self.state = "TRUSTED"
return self.state
m = GnssMonitor()
# 20 healthy fixes, then 3 m of multipath bias while the receiver still claims 0.8 m accuracy
for k in range(20):
m.update(0.5, 0.3, 0.2)
switch = None
for k in range(20, 30):
if m.update(0.8, 3.0, 0.5) == "DEGRADED" and switch is None:
switch = k
print(switch, m.state)
# the receiver now admits 6 m of error; later it recovers
print(m.update(6.0, 4.0, 0.5))
for k in range(9):
m.update(0.5, 0.3, 0.3)
print(m.state)
print(m.update(0.5, 0.3, 0.3))Printed (checked by the site’s tests):
22 DEGRADED
DEGRADED
DEGRADED
TRUSTEDReading it: after 20 healthy fixes, a 3 m multipath bias with a receiver still claiming 0.8 m accuracy gives z = 3.0 / √(0.8² + 0.5²) = 3.18 sigma: three consecutive samples exceed the 3σ gate, so the monitor switches to DEGRADED at sample 22 even though the reported accuracy is excellent. Recovery needs ten consecutive good samples (the state is still DEGRADED after nine, TRUSTED after the tenth).
What to do with the state (design decision)
- Give the estimator a much larger GNSS measurement covariance (or none) while DEGRADED, rather than switching it off abruptly: see the Localization Handover Visualizer.
- Let the local odometry carry the pose; keep GNSS aligned in the background so it can return without a jump.
- Log the state, the reported sigma, the innovation and the position at each transition for later threshold tuning.
Not executed by the site’s automated tests (they need a ROS 2 installation).
ros2 topic echo /fix --once # NavSatFix: status, position_covariance, position_covariance_type
ros2 topic hz /fix # fix rate: very low or irregular rates are a symptom too
ros2 bag record -o gnss_test /fix /odometry/filteredTopic names depend on your driver; /fix is a common default, not a rule.
8. Real examples
- Walking a robot along a building wall: the receiver reports 0.8 m while the bias grows to 3 m. Only the innovation test catches it.
- Entering a car park: fix status goes to −1 (no fix) within seconds: the status check is enough.
- Under tree canopy: reported sigma rises slowly; the monitor flips once it crosses 3 m. Tune the thresholds on bags recorded under that canopy.
9. Failure scenarios
- Trusting the reported covariance alone.
- Hard switching with no hysteresis: GNSS on and off every few seconds near the threshold.
- A wrong frame or datum making every innovation large: the monitor declares GNSS degraded everywhere.
- Thresholds tuned in open sky and used in the city.
10. Troubleshooting
- Record
/fixand the filtered odometry on a route that crosses the problem area (ros2 bag record). - Plot reported sigma, innovation and the monitor state against distance.
- If the state is DEGRADED everywhere, check frames and the datum first.
- If real multipath is not caught, lower the gate or add a jump-rate check; if good fixes are rejected, raise the predicted sigma or the gate.
- Re-run on the recorded bag and compare transitions.
11. Validation
- On a recorded transition route, the state changes before the position error exceeds your allowed limit (use a surveyed or RTK reference).
- No state flapping over a clean 10-minute open-sky run.
- The published pose shows no jump at the transitions (see the handover guide when available).
- The monitor logic prints the values above (executed by the site tests).
12. Engineering checklist
13. Further references
sensor_msgs/NavSatFixdefinition (status and covariance fields): ros2/common_interfaces, jazzy branch. Status constants (−1 no fix, 0 fix, 1 SBAS, 2 GBAS; a fix is valid when status ≥ 0) and the covariance fields (ENU, with an unknown, approximated, diagonal-known or known type) were verified against this file on 2026-11-05.- Your receiver’s datasheet for update rate, accuracy specification (and the conditions it is stated under), DOP and satellite-count outputs.
Related on FixTheRobot
- GPS-Denied Localization SLAM · Lesson
- Mapping vs Localization Mode SLAM · Lesson