FixTheRobot Explore Robotics / Systems Engineering / Engineering Guides / Module 12

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.

  • Advanced
  • Module 12: Outdoor-to-Indoor Localization & GNSS-Denied Navigation
  • Written for ROS 2 Jazzy where ROS 2 is used
  • Sources checked 2026-11-04

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

GNSS degradation signals
SignalWhere it comes fromStrengthWeakness
Fix statusNavSatFix.status.status: −1 no fix, 0 fix, 1 SBAS, 2 GBASCatches total lossA “fix” can still be poor
Reported covarianceNavSatFix.position_covariance with position_covariance_type (0 unknown, 1 approximated, 2 diagonal known, 3 known)Easy; many drivers fill itSome 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 sigmaDetects silent bias the receiver does not admitNeeds a trustworthy odometry prediction and a consistent frame
Satellite count / DOP / signal qualityVendor messages or NMEA sentences (not part of NavSatFix)Early warningVendor specific; check your receiver
Position jump rateChange between consecutive fixes vs. the speedSimple, no odometry neededSlow 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
TRUSTED

Reading 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/filtered

Topic names depend on your driver; /fix is a common default, not a rule.

8. Real examples

  1. 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.
  2. Entering a car park: fix status goes to −1 (no fix) within seconds: the status check is enough.
  3. 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

  1. Record /fix and the filtered odometry on a route that crosses the problem area (ros2 bag record).
  2. Plot reported sigma, innovation and the monitor state against distance.
  3. If the state is DEGRADED everywhere, check frames and the datum first.
  4. 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.
  5. 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/NavSatFix definition (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