Skip to content

Position estimator sensor correction refactor#11322

Open
breadoven wants to merge 6 commits intoiNavFlight:maintenance-9.xfrom
breadoven:abo_pos_est_sensor_corr_refactor
Open

Position estimator sensor correction refactor#11322
breadoven wants to merge 6 commits intoiNavFlight:maintenance-9.xfrom
breadoven:abo_pos_est_sensor_corr_refactor

Conversation

@breadoven
Copy link
Collaborator

PR changes the way sensor corrections are applied in position estimator. Currently corrections are applied every loop which doesn't make sense given sensors update at a much slower rate than loop rate. This leads to corrections repeatedly being applied that are correcting back to an out of date sensor position which isn't be beneficial and wastes processor time.

This PR changes the corrections so they are only applied once immediately after a sensor update occurs. It affects the GPS, Baro and Flow sensors that are used for position estimator correction input. In between sensor updates the estimation will rely on the accelerometer based input only.

Only other related changes are:

  1. Position estimation EPH/V degradation between sensor updates changed to linear behaviour which times out in a maximum of 10s if no sensor EPH/V correction updates occur.
  2. Velocity decay on invalid estimation changed from a correction to a direct reduction in posEstimator.est.vel (fits better with the new corrections logic and does the same thing ultimately)

Tested OK on HITL so far which only covers the GPS and Baro. The Flow sensor changes need further testing.

@qodo-code-review
Copy link
Contributor

Review Summary by Qodo

Refactor sensor corrections to apply only on sensor updates

✨ Enhancement 🐞 Bug fix

Grey Divider

Walkthroughs

Description
• Apply sensor corrections only once per sensor update, not every loop iteration
• Change EPH/EPV degradation from exponential to linear with 10s timeout
• Replace velocity decay correction with direct velocity reduction
• Track sensor update time deltas in GPS, Baro, and Flow structures
• Constrain corrections based on actual sensor update intervals
Diagram
flowchart LR
  A["Sensor Update<br/>GPS/Baro/Flow"] -->|Store updateDt| B["Sensor Data<br/>Structure"]
  B -->|Check updateDt| C{"Sensor<br/>Updated?"}
  C -->|Yes| D["Calculate<br/>Corrections"]
  C -->|No| E["Skip<br/>Corrections"]
  D --> F["Apply Once<br/>Per Update"]
  E --> G["Degrade EPH/EPV<br/>Linearly"]
  F --> H["Reset updateDt<br/>to Zero"]
  G --> I["Estimate Position"]
Loading

Grey Divider

File Changes

1. src/main/navigation/navigation_pos_estimator.c ✨ Enhancement +120/-85

Implement sensor-update-triggered corrections with linear EPH/EPV degradation

• Store GPS and Baro update time deltas (updateDt) when sensors provide new data
• Wrap sensor correction calculations in updateDt checks to apply corrections only once per sensor
 update
• Change EPH/EPV degradation from exponential (1.0f + ctx.dt) to linear (100.0f * ctx.dt) with
 10s timeout
• Replace velocity decay correction logic with direct velocity reduction in posEstimator.est.vel
• Modify correction application to use sensor-specific updateDt instead of loop ctx.dt
• Reset all sensor updateDt values to zero after corrections are applied
• Adjust correction limit calculation to use maximum sensor update interval

src/main/navigation/navigation_pos_estimator.c


2. src/main/navigation/navigation_pos_estimator_flow.c ✨ Enhancement +17/-8

Add flow sensor update-triggered correction logic

• Add check for optical flow updateDt to skip corrections if no new flow data available
• Store flow update time delta in posEstimator.flow.updateDt and reset opflow.updateDt
• Use flow-specific dt variable instead of loop ctx.dt for correction calculations
• Set ctx->applyCorrections flag when flow corrections are applied

src/main/navigation/navigation_pos_estimator_flow.c


3. src/main/navigation/navigation_pos_estimator_private.h ✨ Enhancement +5/-0

Add sensor update tracking fields to data structures

• Add updateDt field to navPositionEstimatorGPS_t structure
• Add updateDt field to navPositionEstimatorBARO_t structure
• Add updateDt field to navPositionEstimatorFLOW_t structure
• Add applyCorrections boolean flag to estimationContext_t structure
• Add INAV_EST_CORR_LIMIT_VALUE constant definition (appears twice, likely a merge artifact)

src/main/navigation/navigation_pos_estimator_private.h


View more (2)
4. src/main/sensors/opflow.c ✨ Enhancement +1/-0

Track optical flow sensor update time delta

• Calculate and store optical flow update time delta (updateDt) when new flow data is available
• Update opflow.updateDt before updating lastValidUpdate timestamp

src/main/sensors/opflow.c


5. src/main/sensors/opflow.h ✨ Enhancement +1/-0

Add optical flow update time delta field

• Add updateDt field to opflow_s structure to track time since last valid optical flow update

src/main/sensors/opflow.h


Grey Divider

Qodo Logo

@qodo-code-review
Copy link
Contributor

qodo-code-review bot commented Feb 11, 2026

Code Review by Qodo

🐞 Bugs (3) 📘 Rule violations (2) 📎 Requirement gaps (0)

Grey Divider


Action required

✅ 1. baroAltRate divides by zero 📘 Rule violation ⛯ Reliability
Description
posEstimator.baro.baroAltRate is computed by dividing by baroDtSec without guarding against
0/non-finite values, which can introduce inf/NaN into estimation state. This can destabilize
correction logic and violates the requirement to handle boundary/invalid inputs deterministically.
Code

src/main/navigation/navigation_pos_estimator.c[R303-310]

+            const float baroDtSec = US2S(baroDtUs);
+            posEstimator.baro.updateDt = baroDtSec;
+
+            posEstimator.baro.alt = pt1FilterApply3(&posEstimator.baro.avgFilter, posEstimator.baro.alt, baroDtSec);

           // baro altitude rate
           static float baroAltPrevious = 0;
-            posEstimator.baro.baroAltRate = (posEstimator.baro.alt - baroAltPrevious) / US2S(baroDtUs);
+            posEstimator.baro.baroAltRate = (posEstimator.baro.alt - baroAltPrevious) / baroDtSec;
Evidence
The compliance checklist requires explicit handling of boundary values and invalid numeric inputs.
The updated code computes baroAltRate using baroDtSec with no check for <= 0 or isfinite(),
so a zero/invalid delta time leads to undefined/unstable estimator values.

Rule 3: Generic: Robust Error Handling and Edge Case Management
src/main/navigation/navigation_pos_estimator.c[303-310]
Best Practice: Learned patterns

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`baroAltRate` is calculated with a division by `baroDtSec` without guarding against `0`/non-finite values.
## Issue Context
`baroDtSec` originates from time deltas and can be zero (same timestamp/re-entrancy) or otherwise invalid; dividing by it can inject `inf/NaN` into the estimator.
## Fix Focus Areas
- src/main/navigation/navigation_pos_estimator.c[303-310]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


✅ 2. Duplicate macro define 🐞 Bug ✓ Correctness
Description
INAV_EST_CORR_LIMIT_VALUE is defined twice in the same header, causing a macro redefinition warning
and potentially a build failure when warnings are treated as errors.
Code

src/main/navigation/navigation_pos_estimator_private.h[R36-38]

+#define INAV_EST_CORR_LIMIT_VALUE           4000.0f

#define INAV_EST_CORR_LIMIT_VALUE           4000.0f   // Sanity constraint limit for pos/vel estimate correction value (max 40m correction per s)
Evidence
The header contains two consecutive definitions of the same macro. The build system can enable
-Werror (warnings as errors), which would turn the redefinition warning into a hard failure.

src/main/navigation/navigation_pos_estimator_private.h[35-39]
cmake/stm32.cmake[250-253]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`INAV_EST_CORR_LIMIT_VALUE` is defined twice in `navigation_pos_estimator_private.h`, which can break builds when warnings are treated as errors.
### Issue Context
The PR adds a second `#define INAV_EST_CORR_LIMIT_VALUE 4000.0f` without removing the existing one.
### Fix Focus Areas
- src/main/navigation/navigation_pos_estimator_private.h[35-39]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. applyCorrections uninitialized 🐞 Bug ⛯ Reliability
Description
updateEstimatedTopic() reads ctx.applyCorrections without initializing it, leading to undefined
behavior (randomly applying corrections and resetting sensor updateDt flags).
Code

src/main/navigation/navigation_pos_estimator.c[R794-796]

+    // Only apply corrections if new sensor update available
+    if (ctx.applyCorrections) {
+        ctx.applyCorrections = false;
Evidence
estimationContext_t gains a new bool field applyCorrections, but updateEstimatedTopic() never
initializes it before the if (ctx.applyCorrections) check. Because ctx is stack-allocated,
applyCorrections can take arbitrary values, which can spuriously enter the correction-application
block and clear sensor updateDt values.

src/main/navigation/navigation_pos_estimator_private.h[180-188]
src/main/navigation/navigation_pos_estimator.c[743-769]
src/main/navigation/navigation_pos_estimator.c[794-832]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`ctx.applyCorrections` is read without initialization, causing undefined behavior.
### Issue Context
`estimationContext_t` is stack allocated in `updateEstimatedTopic()`; vectors are zeroed but the new boolean flag is not.
### Fix Focus Areas
- src/main/navigation/navigation_pos_estimator.c[743-769]
- src/main/navigation/navigation_pos_estimator.c[794-832]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

4. updateDt uses 0 sentinel 📘 Rule violation ✓ Correctness
Description
Multiple sensors use updateDt == 0.0f as an implicit "no new data" sentinel without explicit
range/validity checks, which can silently skip corrections or accept unrealistic deltas. This
violates the requirement for deterministic sentinel states and validation of external numeric inputs
before use.
Code

src/main/navigation/navigation_pos_estimator_flow.c[R76-82]

+    if (!opflow.updateDt) {
+        return true;
+    }
+    posEstimator.flow.updateDt = opflow.updateDt;
+    opflow.updateDt = 0.0f;
+    const float dt = posEstimator.flow.updateDt;
+
Evidence
The checklist requires explicit sentinel values and validation of external numeric inputs
(ranges/NaN) before applying them. The flow path treats 0.0f as "no update" and forwards
opflow.updateDt into estimator logic without checking isfinite() and expected bounds, making
behavior nondeterministic for invalid/huge/NaN deltas.

src/main/navigation/navigation_pos_estimator_flow.c[76-82]
Best Practice: Learned patterns

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The code uses `updateDt == 0.0f` as an implicit &amp;quot;no new update&amp;quot; sentinel and does not validate `updateDt` for NaN/inf/out-of-range values before using it.
## Issue Context
`updateDt` comes from sensor timing and can be uninitialized/huge/non-finite; using `0` as a sentinel can also mask legitimate boundary conditions and silently skip corrections.
## Fix Focus Areas
- src/main/navigation/navigation_pos_estimator_flow.c[76-82]
- src/main/sensors/opflow.c[171-175]
- src/main/navigation/navigation_pos_estimator.c[677-679]
- src/main/navigation/navigation_pos_estimator.c[828-831]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


5. Opflow first dt huge 🐞 Bug ⛯ Reliability
Description
On the first optical-flow update, opflow.updateDt is computed from lastValidUpdate=0, producing an
abnormally large dt that can cause a one-time large correction and overly permissive correction
limits.
Code

src/main/sensors/opflow.c[R174-175]

+        opflow.updateDt = US2S(currentTimeUs - opflow.lastValidUpdate);
       opflow.lastValidUpdate = currentTimeUs;
Evidence
opflow is a global (zero-initialized) struct and lastValidUpdate is not set during init. The first
call computes dt as time since boot rather than time since the previous valid update.

src/main/sensors/opflow.c[66-68]
src/main/sensors/opflow.c[166-176]
src/main/navigation/navigation_pos_estimator_flow.c[76-82]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The first optical-flow dt is computed from a zero timestamp, producing a large dt.
### Issue Context
`opflow.lastValidUpdate` starts at 0; `opflow.updateDt` is used directly by the estimator to scale corrections.
### Fix Focus Areas
- src/main/sensors/opflow.c[166-176]
- src/main/sensors/opflow.c[139-153]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

ⓘ The new review experience is currently in Beta. Learn more

Grey Divider

Qodo Logo

Comment on lines 36 to 38
#define INAV_EST_CORR_LIMIT_VALUE 4000.0f

#define INAV_EST_CORR_LIMIT_VALUE 4000.0f // Sanity constraint limit for pos/vel estimate correction value (max 40m correction per s)
Copy link
Contributor

Choose a reason for hiding this comment

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

Action required

2. Duplicate macro define 🐞 Bug ✓ Correctness

INAV_EST_CORR_LIMIT_VALUE is defined twice in the same header, causing a macro redefinition warning
and potentially a build failure when warnings are treated as errors.
Agent Prompt
### Issue description
`INAV_EST_CORR_LIMIT_VALUE` is defined twice in `navigation_pos_estimator_private.h`, which can break builds when warnings are treated as errors.

### Issue Context
The PR adds a second `#define INAV_EST_CORR_LIMIT_VALUE 4000.0f` without removing the existing one.

### Fix Focus Areas
- src/main/navigation/navigation_pos_estimator_private.h[35-39]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

@Jetrell
Copy link

Jetrell commented Feb 12, 2026

@breadoven Out of interest before testing. How many of these changes cross over with changes you made in the other one merged two days back.. I asked because I wasn't sure how that last one sat with respect to the master. And if the action builds for these commits will also include the merge of the last one ?

@breadoven
Copy link
Collaborator Author

@breadoven Out of interest before testing. How many of these changes cross over with changes you made in the other one merged two days back.. I asked because I wasn't sure how that last one sat with respect to the master. And if the action builds for these commits will also include the merge of the last one ?

The Action build for this PR includes the changes from the other PR. They're both against the maintenance-9.x branch. That's the reason I merged the other one so it was included in this PR otherwise things get a bit messy given they interact with each other. I'm not sure if this PR will be in 9.1 though, needs some decent testing given the extent of the changes.

@breadoven
Copy link
Collaborator Author

breadoven commented Feb 12, 2026

I did a quick test of this just carrying an armed quad with a GPS lock. It seems to work as expected except for odd behaviour of the GPS updates. The debugs I was using are in the estimationCalculateCorrection_XY_GPS routine and only update when EST_GPS_XY_VALID is true. The debugs should update at GPS update rate of 10Hz which they did except every 5s they didn't update for 0.5s. This can only mean that the GPS data is not being updated correctly in Position estimator every 5s which ties in with the 5s glitch that's been mentioned before. The GPS itself however continues to update in the BB log during the 0.5s period so it seems to be a glitch in position estimator rather than a lack of data from the GPS itself. Needs further checking.

Edit: Checking the last HITL log the 5s / 0.5s glitch is absent.

@Jetrell
Copy link

Jetrell commented Feb 13, 2026

Tested it with another 3" quad today that always has rather average satellite acquisition performance... When I took-off the HDOP was 1.8.
This Poshold test was done in strong winds.. And it perform rather well.

When I first enabled Poshold it drifted a little on the XYZ, by about 0.5m from what the log showed and from what I remember LOS.. But then it settled down on the XY within a few seconds. And still continued to drift up and down by about 1m. Which I didn't think was too bad considering the strong wind blowing over the foam covered baro.. But as soon as I switched out of Poshold back to Angle for a few seconds and then back to Poshold, It sat very firmly positioned on all axis's for the rest of the flight.

I only had a quick look at what you changed. And one thing than now noticeably works, is the altitude throttle stick adjustment in Poshold.
I could move the stick upwards slowly, and the copter would start ramping up its ascending velocity.
Then I could do the same with the down stick. And the copter would start descending faster in a controlled manor.. It felt very smooth and linear overall.

Looking at the log. The navVels were always quick to track the targets. And the target positions were also made quickly in response.

This quad was never anything special as far as navigation performance. So it's refreshing to see it work this way.
But all my copters are mechanically clean, with low IMU vibrations. So the next step may be it give it bent props.. But I want to do this in a more open area.. In case it goes a bit wild.. But this might not be for a little while.

@breadoven
Copy link
Collaborator Author

breadoven commented Feb 13, 2026

More debugging and it appears the 5s glitch is caused by Position Estimator onNewGPSData not being updated for 0.5s every 5s. So the problem is with gps.c gpsProcessNewSolutionData. Any idea why @sensei-hacker ? Almost looks like the GPS is reinitialising every 4.5s.

Edit:
More debugging confirms that gpsProcessNewDriverData is being run but not gpsProcessNewSolutionData during the 0.5s period so the problem must be ingps_ublox.c gpsProtocolStateThread.

@breadoven
Copy link
Collaborator Author

Finally the 5s glitch has been tracked down. It's caused by GPS_CAPA_INTERVAL in the ublox gpsProtocolStateThread. GPS_CAPA_INTERVAL = 5s and GPS_CFG_CMD_TIMEOUT_MS = 0.5s.

        if (gpsState.gpsConfig->autoConfig) {
            if ((millis() - gpsState.lastCapaPoolMs) > GPS_CAPA_INTERVAL) {
                gpsState.lastCapaPoolMs = millis();

                if (gpsState.hwVersion == UBX_HW_VERSION_UNKNOWN)
                {
                    pollVersion();
                    ptWaitTimeout((_ack_state == UBX_ACK_GOT_ACK || _ack_state == UBX_ACK_GOT_NAK), GPS_CFG_CMD_TIMEOUT_MS);
                }

                pollGnssCapabilities();
                ptWaitTimeout((_ack_state == UBX_ACK_GOT_ACK || _ack_state == UBX_ACK_GOT_NAK), GPS_CFG_CMD_TIMEOUT_MS);
            }
        }

It's not immediately obvious what this is doing but I assume it's behaving as intended ... or maybe not ?

@sensei-hacker
Copy link
Member

sensei-hacker commented Feb 13, 2026

Nice work, Breadoven!

I believe it's checking which constellations are currently enabled, etc. With a 500ms timeout. It is intended to still process the position data in the loop -- if the queries are answered in a timely fashion.
BUT it looks for an ACK to that message. UBX-MON-GNSS doesn't ACK, it just answers with the data. So no ack arrives and we have the 500ms glitch you found.

I will make a PR fixing it in a moment.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants