Skip to content

feat(ekf2): GNSS altitude drift correction detection - #27155

Closed
haumarco wants to merge 23 commits into
mainfrom
ekf_mv_gps_alt_drift_corr_home
Closed

haumarco wants to merge 23 commits into
mainfrom
ekf_mv_gps_alt_drift_corr_home

Conversation

@haumarco

@haumarco haumarco commented Apr 21, 2026 •

Copy link
Copy Markdown
Contributor

EKF2 GNSS altitude drift detector with covariance decorrelation and control-side velocity-only fallback

Solved Problem

With EKF2_HGT_REF=GNSS, slow GPS altitude drift correction pulls the fused vertical state along with it. The vehicle silently climbs or descends to track it, even when baro and vertical-velocity integration both say the altitude hasn't changed.
Prior mitigation in HomePosition only ran in a short post-takeoff window and only nudged home.alt.

Solution

Move the detector into EKF2 and signal downstream consumers via a new vehicle_local_position flag. While the flag is set, the EKF decorrelates the vertical position state from the rest of the state vector so baro corrections can pull pos(z) without leaking into vel(z) or accel-bias, and the altitude flight task drops position control on z.

Detector (EKF2::GpsAltDriftDetector): 20 s sliding window at ~1 Hz of

  • d1 = ekf_amsl - baro_lpf
  • d2 = ekf_amsl - vel_integral

Trigger when |Δd1| and |Δd2| both exceed a threshold AND |Δ(d1 - d2)| is smaller than either: baro and vel-integral agree, the fused altitude is the outlier. One-cycle hit_pending hysteresis filters single-sample outliers. Publishes GpsAltitudeDriftCorrection.

Detector reference is the fused AMSL (getLatLonAlt().altitude()), not raw gps.altitude_msl_m, so the detector sees the altitude the system is actually acting on.

EKF response (only runs in_air && height_sensor_ref == GNSS):

  • Publishes vehicle_local_position.altitude_good_for_local_control reflecting the detector flag.
  • While the flag is false, Ekf::decorrelateAltPos() zeros the off-diagonals on the z-position state every GPS sample (P.uncorrelateCovariance<1>(State::pos.idx + 2)). This breaks the covariance coupling between z-pos and z-vel / accel-bias so the GPS-driven pos correction doesn't poison neighbouring states; baro fusion then pulls pos cleanly back toward truth.

Control response: FlightTaskManualAltitude reads the flag and, while it's false, sets _position_setpoint(2) = _dist_to_ground_lock = NAN so the controller falls back to vertical velocity control only (introduced together with this change in the simplification of the unlock logic).

Expected outcome by mode:

  • Local-position (POSCTL): the drift should not be visible on the airframe. The covariance decorrelation lets baro hold the EKF's vertical state at truth, and the velocity-only controller fallback prevents the controller from chasing any residual local-z error during the transient.
  • Global mission with an AMSL waypoint: the drift is expected to remain visible in the airframe trajectory. The drone is explicitly asked to fly to a specific AMSL number, and that number is computed against the (still-drift-correcting) GPS reference. The EKF state is healthy via baro, but the navigation target is what the user requested in absolute terms.

Alternatives

Earlier iteration absorbed the detected offset into the local origin altitude. Each detection event then produced an instantaneous step in lpos.ref_alt and lpos.z, which the position controller saw as a position error and tried to chase, producing visible altitude jumps in flight. Dropped in favour of the covariance-decorrelation approach above, which keeps the published frame continuous and lets baro do the rebalancing.

Changelog Entry

Feature: EKF2 detects slow GNSS altitude correction drift against baro + vertical velocity integration. While drift is flagged, EKF decorrelates the vertical position covariance and altitude flight tasks fall back to vertical velocity control only.
New uORB topic: GpsAltitudeDriftCorrection
New field: vehicle_local_position.altitude_good_for_local_control

Test coverage

Tested on hardware with artificial GNSS altitude drift injected (applied to vehicle_gps_position).

global mission: estimate and controller follows GNSS even during drift-correction
Screenshot from 2026-04-29 14-54-55

position-mode: estimate follows drift correction while controller only follows velocity estimate to stay "static". The initial 1m drift is the threshold window until the drift gets detected:
Screenshot from 2026-04-29 14-54-13

local mission: in controller we cant detect what mission type it originally was. Therefore we cant ignore the position setpoint which leads to the "wave" behavior. I still think this is much better compared to the drift we had previously.
image

@github-actions

github-actions Bot commented Apr 21, 2026 •

Copy link
Copy Markdown
Contributor

🔎 FLASH Analysis

px4_fmu-v5x [Total VM Diff: 880 byte (0.04 %)]
    FILE SIZE        VM SIZE    
 --------------  -------------- 
  +0.0%    +888  +0.0%    +888    .text
    [NEW]    +372  [NEW]    +372    GnssAltitudeDriftDetector::analyze()
     +42%    +264   +42%    +264    EKF2::UpdateGpsSample()
    [NEW]    +240  [NEW]    +240    GnssAltitudeDriftDetector::update()
    +1.5%     +92  +1.5%     +92    EKF2::EKF2()
   -99.8%     +68 -99.8%     +68    [92 Others]
    +0.2%     +40  +0.2%     +40    uORB::compressed_fields
    +0.9%     +36  +0.9%     +36    Commander::handle_command()
    +2.1%     +32  +2.1%     +32    EKF2::~EKF2()
    +0.0%     +28  +0.0%     +28    [section .text]
    +2.0%     +24  +2.0%     +24    Commander::dataLinkCheck()
     +36%     +20   +36%     +20    FlightTaskManualAltitude::update()
    +0.9%     +20  +0.9%     +20    MissionBlock::is_mission_item_reached_or_completed()
    +0.5%     +20  +0.5%     +20    Navigator::run()
    +0.6%     +16  +0.6%     +16    EKF2::PublishAidSourceStatus()
    +1.1%     +16  +1.1%     +16    FixedWingModeManager::control_auto_takeoff()
    [NEW]     +16  [NEW]     +16    __orb_gnss_altitude_drift
    +1.5%     +16  +1.5%     +16    ucdr_serialize_vehicle_local_position()
    -2.1%     -12  -2.1%     -12    Commander::control_status_leds()
    -3.3%     -12  -3.3%     -12    Commander::handleAutoDisarm()
   -13.7%     -52 -13.7%     -52    HomePosition::HomePosition()
   -40.5%    -356 -40.5%    -356    HomePosition::update()
  +0.0%    +822  [ = ]       0    .debug_abbrev
  +0.0%     +64  [ = ]       0    .debug_aranges
  +0.0%    +156  [ = ]       0    .debug_frame
  +0.1% +18.6Ki  [ = ]       0    .debug_info
  +0.1% +2.73Ki  [ = ]       0    .debug_line
    [NEW]      +2  [ = ]       0    [Unmapped]
    +0.1% +2.73Ki  [ = ]       0    [section .debug_line]
  +0.1% +2.05Ki  [ = ]       0    .debug_loclists
  +0.0%    +159  [ = ]       0    .debug_rnglists
  +0.1% +3.95Ki  [ = ]       0    .debug_str
  -0.8%      -2  [ = ]       0    .shstrtab
  +0.0%    +134  [ = ]       0    .strtab
    [NEW]     +11  [ = ]       0    CSWTCH.823
    [DEL]     -11  [ = ]       0    CSWTCH.824
    [NEW]     +41  [ = ]       0    GnssAltitudeDriftDetector::analyze()
    [NEW]     +43  [ = ]       0    GnssAltitudeDriftDetector::update()
    +0.1%     +24  [ = ]       0    [section .strtab]
   -31.4%     -16  [ = ]       0    __nxsched_remove_readytorun_veneer
     +47%     +16  [ = ]       0    __nxsched_resume_scheduler_veneer
    [NEW]     +26  [ = ]       0    __orb_gnss_altitude_drift
  +0.0%    +160  [ = ]       0    .symtab
    [NEW]     +32  [ = ]       0    CSWTCH.823
    [DEL]     -32  [ = ]       0    CSWTCH.824
   -25.0%     -16  [ = ]       0    Commander::control_status_leds()
   -33.3%     -16  [ = ]       0    Commander::handleModeIntentionAndFailsafe()
    +100%     +16  [ = ]       0    Commander::modeManagementUpdate()
    +100%     +16  [ = ]       0    Commander::updateControlMode()
   -25.0%     -16  [ = ]       0    Commander::updateParamsImpl()
    +100%     +16  [ = ]       0    EKF2::AdvertiseTopics()
    +200%     +32  [ = ]       0    EKF2::PublishInnovationTestRatios()
   -50.0%     -16  [ = ]       0    EKF2Selector::PublishVehicleGlobalPosition()
     +50%     +16  [ = ]       0    EKF2Selector::PublishVehicleOdometry()
   -25.0%     -16  [ = ]       0    Ekf::isYawFailure()
   -25.0%     -16  [ = ]       0    FlightTaskManualAltitude::_updateConstraintsFromEstimator()
    +200%     +32  [ = ]       0    FlightTaskManualAltitude::update()
    [NEW]     +48  [ = ]       0    GnssAltitudeDriftDetector::analyze()
    [NEW]     +64  [ = ]       0    GnssAltitudeDriftDetector::update()
    +100%     +16  [ = ]       0    HealthAndArmingCheckBase::updateParams()
    +100%     +16  [ = ]       0    Loiter::reposition()
   -33.3%     -16  [ = ]       0    Loiter::set_loiter_position()
   -50.0%     -32  [ = ]       0    McAutotuneAttitudeControl::custom_command()
   -99.7%     +32  [ = ]       0    [20 Others]
  -8.4%    -880  [ = ]       0    [Unmapped]
  -0.1%      -8  -0.1%      -8    .ramfunc
     +14%      +1   +14%      +1    __stm32_phyread_veneer
   -12.5%      -1 -12.5%      -1    __pthread_mutex_unlock_veneer
    -1.2%      -4  -1.2%      -4    Ekf::measurementUpdate()
   -16.7%      -4 -16.7%      -4    get_orb_meta()
  +0.1% +28.8Ki  +0.0%    +880    TOTAL

px4_fmu-v6x [Total VM Diff: 872 byte (0.04 %)]
    FILE SIZE        VM SIZE    
 --------------  -------------- 
  +0.0%    +872  +0.0%    +872    .text
    [NEW]    +372  [NEW]    +372    GnssAltitudeDriftDetector::analyze()
     +42%    +264   +42%    +264    EKF2::UpdateGpsSample()
    [NEW]    +240  [NEW]    +240    GnssAltitudeDriftDetector::update()
    +1.5%     +92  +1.5%     +92    EKF2::EKF2()
   -99.9%     +60 -99.9%     +60    [94 Others]
    +0.2%     +40  +0.2%     +40    uORB::compressed_fields
    +0.9%     +36  +0.9%     +36    Commander::handle_command()
    +2.1%     +32  +2.1%     +32    EKF2::~EKF2()
    +2.0%     +24  +2.0%     +24    Commander::dataLinkCheck()
     +36%     +20   +36%     +20    FlightTaskManualAltitude::update()
    +0.9%     +20  +0.9%     +20    MissionBlock::is_mission_item_reached_or_completed()
    +0.5%     +20  +0.5%     +20    Navigator::run()
    +0.0%     +20  +0.0%     +20    [section .text]
    +0.6%     +16  +0.6%     +16    EKF2::PublishAidSourceStatus()
    +1.1%     +16  +1.1%     +16    FixedWingModeManager::control_auto_takeoff()
    [NEW]     +16  [NEW]     +16    __orb_gnss_altitude_drift
    +1.5%     +16  +1.5%     +16    ucdr_serialize_vehicle_local_position()
    -2.1%     -12  -2.1%     -12    Commander::control_status_leds()
    -3.3%     -12  -3.3%     -12    Commander::handleAutoDisarm()
   -13.7%     -52 -13.7%     -52    HomePosition::HomePosition()
   -40.5%    -356 -40.5%    -356    HomePosition::update()
  +0.0%    +822  [ = ]       0    .debug_abbrev
  +0.0%     +64  [ = ]       0    .debug_aranges
  +0.0%    +156  [ = ]       0    .debug_frame
  +0.1% +18.3Ki  [ = ]       0    .debug_info
  +0.1% +2.71Ki  [ = ]       0    .debug_line
     +50%      +1  [ = ]       0    [Unmapped]
    +0.1% +2.71Ki  [ = ]       0    [section .debug_line]
  +0.1% +2.23Ki  [ = ]       0    .debug_loclists
  +0.0%    +159  [ = ]       0    .debug_rnglists
  +0.1% +3.95Ki  [ = ]       0    .debug_str
  +0.9%      +2  [ = ]       0    .shstrtab
  +0.0%    +134  [ = ]       0    .strtab
    [NEW]     +11  [ = ]       0    CSWTCH.823
    [DEL]     -11  [ = ]       0    CSWTCH.824
    [NEW]     +41  [ = ]       0    GnssAltitudeDriftDetector::analyze()
    [NEW]     +43  [ = ]       0    GnssAltitudeDriftDetector::update()
    +0.1%     +24  [ = ]       0    [section .strtab]
    [NEW]     +26  [ = ]       0    __orb_gnss_altitude_drift
  +0.0%    +160  [ = ]       0    .symtab
    [NEW]     +32  [ = ]       0    CSWTCH.823
    [DEL]     -32  [ = ]       0    CSWTCH.824
   -25.0%     -16  [ = ]       0    Commander::control_status_leds()
   -33.3%     -16  [ = ]       0    Commander::handleModeIntentionAndFailsafe()
    +100%     +16  [ = ]       0    Commander::modeManagementUpdate()
    +100%     +16  [ = ]       0    Commander::updateControlMode()
   -25.0%     -16  [ = ]       0    Commander::updateParamsImpl()
    +100%     +16  [ = ]       0    EKF2::AdvertiseTopics()
    +200%     +32  [ = ]       0    EKF2::PublishInnovationTestRatios()
   -50.0%     -16  [ = ]       0    EKF2Selector::PublishVehicleGlobalPosition()
     +50%     +16  [ = ]       0    EKF2Selector::PublishVehicleOdometry()
   -25.0%     -16  [ = ]       0    Ekf::isYawFailure()
   -25.0%     -16  [ = ]       0    FlightTaskManualAltitude::_updateConstraintsFromEstimator()
    +200%     +32  [ = ]       0    FlightTaskManualAltitude::update()
    [NEW]     +48  [ = ]       0    GnssAltitudeDriftDetector::analyze()
    [NEW]     +64  [ = ]       0    GnssAltitudeDriftDetector::update()
    +100%     +16  [ = ]       0    HealthAndArmingCheckBase::updateParams()
    +100%     +16  [ = ]       0    Loiter::reposition()
   -33.3%     -16  [ = ]       0    Loiter::set_loiter_position()
   -50.0%     -32  [ = ]       0    McAutotuneAttitudeControl::custom_command()
   -99.7%     +32  [ = ]       0    [14 Others]
 -12.8%    -872  [ = ]       0    [Unmapped]
  +0.1% +28.7Ki  +0.0%    +872    TOTAL

Updated: 2026-05-28T13:09:40

@dakejahl dakejahl left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Excuse the Claude comment. I'm throwing it at everything these days, it's pretty good..

Comment thread src/modules/ekf2/EKF2.cpp Resolved

@dakejahl dakejahl left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you motivate why the drift detection is an external sibling of _baro_b_est rather than an extension of it?

If there's a reason the existing bias estimator + its isOffsetDetected() check isn't usable here, that'd be worth a comment in the code; if there isn't, I'd prefer the detection live inside _baro_b_est with an exposed accessor, and have EKF2::UpdateGpsSample call shiftAltOrigin when the bias estimator's status crosses a threshold. See inline comments for specifics.

Posted by Claude on behalf of @dakejahl

Comment thread src/modules/ekf2/EKF2.hpp Resolved
Comment thread src/modules/ekf2/EKF2.hpp Resolved
@github-actions github-actions Bot added kind:feature Request or change that adds new functionality. risk:safety-critical May affect arming, failsafe, control, navigation, or flight safety. scope:estimation EKF, local position, attitude, wind, bias, or aiding logic. scope:control Rate, attitude, position, allocation, or actuator control. scope:commander Arming, modes, failsafe, health checks, or vehicle state. scope:uorb uORB messages, generated interfaces, or message translation. labels Apr 29, 2026
@haumarco
haumarco force-pushed the ekf_mv_gps_alt_drift_corr_home branch from adedee3 to 22a358d Compare April 30, 2026 07:39
@haumarco
haumarco requested a review from MaEtUgR April 30, 2026 08:08
@haumarco
haumarco marked this pull request as ready for review April 30, 2026 08:08
@haumarco
haumarco force-pushed the ekf_mv_gps_alt_drift_corr_home branch from 7ad23d8 to 4054bd6 Compare May 4, 2026 13:34
@github-actions github-actions Bot added the scope:middleware DDS, ROS 2, Cyphal/UAVCAN, zenoh, or bridge layers. label May 4, 2026
@haumarco
haumarco force-pushed the ekf_mv_gps_alt_drift_corr_home branch from 4054bd6 to 29c3d3f Compare May 4, 2026 14:04
Comment thread msg/versioned/VehicleLocalPosition.msg Outdated
float32 delta_heading # Heading delta caused by latest heading reset [rad]
uint8 heading_reset_counter # Index of latest heading reset
bool heading_good_for_control
bool altitude_good_for_local_control

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add a comment please. Something like.

Suggested change
bool altitude_good_for_local_control
bool altitude_good_for_local_control # Set true if altitude can be used for local control

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That would be another absolutely useless comment.
bool - Set true if
altitude_ - altitude
good_for_ - can be used for
local_control - local control
pure redundancy without a single piece of information added about why this could be useful.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fair call @MaEtUgR.

So the rule is "fields should be named as far as possible to be self-documenting, and comments are only needed in order to capture non-obvious behaviour"?

I don't mind redundant comments because they show that there is nothing special - while the absense of a comment may well mean the developer didn't think documentation was important.

@dakejahl dakejahl left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not convinced this is the right approach. We're ignoring distance sensor and any other altitude sources. Baro is already very problematic due to propwash and ground effect.

@haumarco

Copy link
Copy Markdown
Contributor Author

@dakejahl for any vehicle operating at higher altitudes, or even moving slightly in the horizontal direction, using a distance sensor would be fairly ineffective. Since this is a voting system, disturbed barometer measurements (e.g. due to prop wash or ground effect) would very likely be rejected, because the GPS velocity integral and GPS altitude generally agree with each other.
Which additional altitude sources would you want to include? Also, what alternative approach would you prefer for addressing this issue (just at a high level discussion)? What specifically don't you like about the approach?

@github-actions github-actions Bot added the vehicle:copter Copter-specific behavior. label May 12, 2026

@MaEtUgR MaEtUgR left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tried to rename and restrcuture a bit to hopefully make the EKF2 less cluttered and interface easy to understand but I still have open questions.

  • Didn't yet grasp the detection logic yet. Maybe we can add a simple test to make it more clear.
  • Can't we go without an additional uORB message just for the drift? Maybe the better answer is no.
  • The drift message I think should contain a cumulative value such that it stays accurate even if a message is missed by the consumer.

Comment thread src/modules/commander/HomePosition.cpp Outdated
Comment on lines +361 to +362
home.alt += gnss_altitude_drift.altitude_offset;
home.z -= gnss_altitude_drift.altitude_offset;

@MaEtUgR MaEtUgR May 12, 2026 •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm concerned with this difference only approach. I obviously misunderstood the meaning of the field commenting accumulated altitude offset from drifting to a more accurate solution. What if the consumer misses some gnss_altitude_drift message? Then it cannot track the drift anyemore.

Can't we publish the accumulated drift and make it easy to keep track of? 🤔

Comment thread msg/GnssAltitudeDrift.msg
@@ -0,0 +1,2 @@
uint64 timestamp # [us] time since system start

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great start. Short description and optional long description?

float _gps_eph{0.f};
float _gps_epv{0.f};
uORB::Subscription _vehicle_gps_position_sub{ORB_ID(vehicle_gps_position)};
uORB::Subscription _gnss_altitude_drift_sub{ORB_ID(gnss_altitude_drift)};

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To support multi-ekf, should this be a multi subscription with estimator_selector_status logic like src/modules/sensors/vehicle_acceleration/VehicleAcceleration.cpp:134?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point, I haven't checked that. Does it still work with the single vehicle_gps_position subscription because that's consolidated at the publication side and break with gnss_altitude_drift because there's one instance per EKF? If so then that would need to be addressed, if not it would be a perpendicular issue and I'd suggest addressing multi-EKF separately. 🤔

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes I think you're right.

I'm guessing that with multi EKF, it would be difficult to break because all EKFs only differ by IMU and/or mag, not by baro or GPS, meaning that all instances of gnss_altitude_drift should be about the same other than timing differences.

@MaEtUgR MaEtUgR left a comment •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I checked again, from my side it's good to go now. The multi-EKF concern needs to be checked though to not break functionality.

I'm not sure why it exceeds kakute flash? I double-checked and my last commit making the defines consistent doesn't change the binary for the kakute board and if it would could only make it smaller.
EDIT: CI rightfully builds against main and the flash addition was merged there...

Comment thread src/modules/ekf2/EKF2.cpp
gnss_altitude_drift_s gnss_altitude_drift{};
gnss_altitude_drift.timestamp = hrt_absolute_time();
gnss_altitude_drift.altitude_offset = _gnss_altitude_drift_detector.accumulatedAltitudeOffset();
_gnss_altitude_drift_pub.publish(gnss_altitude_drift);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this worth logging?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

will add it

@harrisonmg

harrisonmg commented May 28, 2026 •

Copy link
Copy Markdown
Contributor

While testing I found that, when altitude_good_for_lock becomes true again and vehicle_local_position_setpoint.z gets set, there is a lurch caused by the setpoint smoothing from the stale setpoint. It seems to be fixed by keeping the smoothing updated with the current position:

--- a/src/modules/flight_mode_manager/tasks/ManualAltitudeSmoothVel/FlightTaskManualAltitudeSmoothVel.cpp
+++ b/src/modules/flight_mode_manager/tasks/ManualAltitudeSmoothVel/FlightTaskManualAltitudeSmoothVel.cpp
@@ -103,6 +103,12 @@ void FlightTaskManualAltitudeSmoothVel::_updateTrajConstraints()

 void FlightTaskManualAltitudeSmoothVel::_setOutputState()
 {
+       // Reset position setpoint to current position while altitude is unlocked
+       // to prevent a jump in setpoint due to smoothing when it re-locks
+       if (!_altitude_good_for_lock && PX4_ISFINITE(_position(2))) {
+               _smoothing.setCurrentPosition(_position(2));
+       }
+
        _jerk_setpoint(2) = _smoothing.getCurrentJerk();
        _acceleration_setpoint(2) = _smoothing.getCurrentAcceleration();
        _velocity_setpoint(2) = _smoothing.getCurrentVelocity();

I've been testing by running gps reset cold before takeoff with pretty lax gps checks to induce some drift as GPS alt converges. The drift detection gate allows a pretty significant amount of drift before it kicks in.

I'll try and collect some real world examples of drift for tuning.

@harrisonmg

Copy link
Copy Markdown
Contributor

I think it's worth adding in a max time that we will wait for stability before locking altitude again, maybe in the flight task. That seems preferable to permanently losing altitude lock in the case of some failure.

@harrisonmg

harrisonmg commented Jun 1, 2026 •

Copy link
Copy Markdown
Contributor

After more testing and tuning, I've found that it's difficult to detect drift fast enough to avoid unintended movement without causing false positives or rejecting altitude lock for long periods of time. I have some examples with 2 minute of slow GPS altitude drift, and some where it drifts over 3 meters in 10 seconds.

I'm going to try making the following changes:

  • use leaky integrators for velocity and baro to help reduce artifacting (e.g. baro craziness during takeoff) while capturing long-period trends
  • use a threshold on estimator_aid_src_gnss_hgt.test_ratio_filtered to capture rapid shifts in GPS alt
  • if either of these checks fail, immediately stop fusing GPS alt and fallback to barometer instead of unlocking z-position control
  • when drift is no longer detected, restarting GPS alt fusion will reset the altitude via Ekf::resetAltitudeTo; I think this should result in flight changes only when the setpoint is absolute (e.g. an AMSL mission waypoint)

It seems to me that resetting the GPS altitude this way is more "correct" than trying to control the drone in a way where the drift won't matter. False positives also feel much less risky when it's just falling back to baro.

@dakejahl

dakejahl commented Jun 1, 2026

Copy link
Copy Markdown
Contributor

use leaky integrators for velocity and baro to help reduce artifacting (e.g. baro craziness during takeoff) while capturing long-period trends

We need proper ground-effect detection and fusion gating (or temporary variance inflation).

There's also the thrust affect on baro pressure which pulls the Z estimate during throttle transients. See #26924

I think it will be hard for this baro-gnss-drift-detection to work reliably without considering the effects of thrust and ground-effect. My 2 cents.

@harrisonmg

Copy link
Copy Markdown
Contributor

@dakejahl I do see those effects on baro, which is why I'm hesitant about home adjustment based on baro. My current iteration primarily uses the GPS velocity as a means of rejecting slow drift and just requires that the baro agrees in direction and be closer to the estimated drift than it is to the ekf (like the OG PR).

@dakejahl

dakejahl commented Jun 2, 2026

Copy link
Copy Markdown
Contributor

I'd try to systematically induce the position jump (toggle on SBAS after takeoff/hover?) and then post-analyze the log. You can check and see if GNSS vel is reliably independant fom position and correlated with baro-derivative and accel-integral as a means of identifying "gps position drift windows". Maybe that's what you're already doing?

@github-actions

Copy link
Copy Markdown
Contributor

This pull request has been automatically closed due to 120 days of inactivity. If you would like to continue, please feel free to reopen it or submit a new PR.

@github-actions github-actions Bot closed this Sep 30, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

kind:feature Request or change that adds new functionality. risk:safety-critical May affect arming, failsafe, control, navigation, or flight safety. scope:commander Arming, modes, failsafe, health checks, or vehicle state. scope:control Rate, attitude, position, allocation, or actuator control. scope:estimation EKF, local position, attitude, wind, bias, or aiding logic. scope:middleware DDS, ROS 2, Cyphal/UAVCAN, zenoh, or bridge layers. scope:uorb uORB messages, generated interfaces, or message translation. stale vehicle:copter Copter-specific behavior.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants