Skip to content

Limit fixed-wing climb pitch when thrust cannot hold the climb - #12194

Open
sensei-hacker wants to merge 1 commit into
iNavFlight:maintenance-10.xfrom
sensei-hacker:fix-fw-underthrust-pitch-demand
Open

sensei-hacker wants to merge 1 commit into
iNavFlight:maintenance-10.xfrom
sensei-hacker:fix-fw-underthrust-pitch-demand

Conversation

@sensei-hacker

Copy link
Copy Markdown
Member

Problem

updateAltitudeVelocityAndPitchController_FW has no airspeed or thrust input. When the motor cannot deliver the demanded climb (sagging battery, thrust-limited airframe) it keeps the pitch demand pinned at nav_fw_climb_angle while the aircraft sinks and slows toward a stall. Reproduced in JSBSim SITL in ALTHOLD, ALTHOLD + climb, RTH and WP, on a C172 and a T-6 (airspeed down to 51.7 kt against a stall of about 48 kt).

Change

Extends updateFwEnergyBankGuard() to also lower the climb-pitch ceiling (climbPitchCut, fed into the altitude PID limit so anti-windup applies) when:

  • speed is below 0.88 of a slow peak-following reference while nose-up demand is present and the climb deficit is latched (airspeed). Without a pitot (ground speed) the climb deficit is required too, so wind does not trigger it; or
  • speed is below 0.93 of the reference with a sustained pinned pitch demand and a latched deficit; or
  • speed is falling quickly, also below 0.97 of the reference, with a sustained pinned demand and a latched deficit.

The ceiling is held until the deficit clears, then restored at 2 deg/s. No stall speed and no new settings. The cut clears on an altitude-controller reset and on a guard resync.

Testing (JSBSim SITL, C172 and T-6, 4 modes, climb angle 20 and 35)

Pass criteria per run: min IAS >= 1.2 x Vs1, and at most 3.0 s with the motor at the cap, pitch demand above +1 deg and IAS more than 5 kt below its pre-cut value.

  • Thrust cut to 10-20%: baseline 0 of 12 pass (36-49 s in the pinned state); this change 9 of 12 pass.
  • Not met: three T-6 cases still exceed 3.0 s: t6 rth at 20 and 35 deg (4.2 s each) and t6 althold at 35 deg (6.3 s). In those the cut engages a few seconds late because 5 kt is only about 3% of T-6 cruise speed.
  • 12 of 12 negative controls (full throttle, no cut) pass, and none of them start cutting.

Known limits

  • The cut can ratchet down to a 0 deg climb ceiling (stall-safe). It is held until the deficit clears.
  • A stale high speed reference after a dive may over-trigger the large-loss branch (the reference falls with a 60 s time constant).
  • The bank guard's nearPitchLimit still compares against the full climb angle; follow-up.

Cost

Text +736 B (F722SE) to +944 B (F405SE) against baseline, with about 28 B of bss; roughly +0.2% of the image.

The fixed-wing altitude controller has no airspeed input, so when the
motor cannot deliver the demanded climb (low battery, thrust-limited
airframe) it holds the full nav_fw_climb_angle while the aircraft sinks
and slows toward a stall.

Extend the energy bank guard to also lower the climb-pitch ceiling when
speed falls against a slow peak-following reference while nose-up demand
is sustained and the climb deficit is latched, or when speed has dropped
well below the reference. The ceiling is held until the deficit clears,
then restored gradually. No stall speed and no new settings are needed.
Without a pitot the large-loss trigger also requires the latched deficit,
so wind-driven ground speed changes do not cut the ceiling.
@qodo-code-review

Copy link
Copy Markdown
Contributor

ⓘ Qodo reviews are paused because the subscription is no longer active. Ask your workspace admin to reactivate the subscription to resume reviews. Manage billing

@qodo-free-for-open-source-projects

Copy link
Copy Markdown

PR Summary by Qodo

Limit fixed-wing climb pitch during under-thrust speed loss

🐞 Bug fix 🕐 20-40 Minutes

Grey Divider

AI Description

• Reduce the climb-pitch ceiling when sustained climb demand coincides with speed loss and unmet
 climb.
Diagram

graph TD
  A["Speed input"] --> C["Energy guard"] --> D{"Under-thrust?"} -->|Yes| E["Pitch cut"] --> G["Altitude PID"] --> H["Pitch command"]
  B["Climb deficit"] --> C
  D -->|No| F["Hold or recover"] --> G
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Coordinated energy controller
  • ➕ Could coordinate throttle and pitch directly around speed and altitude objectives.
  • ➖ Requires a substantially broader controller redesign, tuning, and flight validation.
2. Configured minimum airspeed
  • ➕ Provides an explicit, aircraft-specific threshold for reducing climb pitch.
  • ➖ Requires configuration and reliable airspeed sensing; does not cover aircraft without a pitot.

Recommendation: The existing guard is a proportionate integration point: it already tracks unmet climb, and limiting the altitude PID applies anti-windup without new settings. Treat the three T-6 SITL runs that missed the stated timing criterion and the potentially stale speed reference after a dive as remaining validation risks.

Files changed (1) +103 / -1

Bug fix (1) +103 / -1
navigation_fixedwing.cAdd an under-thrust climb-pitch ceiling to fixed-wing navigation +103/-1

Add an under-thrust climb-pitch ceiling to fixed-wing navigation

• The energy guard now tracks pitot airspeed or fallback ground speed against a peak-following reference, with hysteresis and filtered deceleration. It reduces the altitude PID's climb-pitch limit when speed-loss and demand conditions indicate under-thrust, holds the cut during an unmet climb, and restores it gradually afterward. Altitude-controller reset or guard resync clears the cut; a lateral reset preserves it.

src/main/navigation/navigation_fixedwing.c

@qodo-free-for-open-source-projects

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (4) 📘 Rule violations (0) 📜 Skill insights (0)

Grey Divider


Action required

1. Auto-speed climbs never receive a pitch cut 🐞 Bug ≡ Correctness
Description
updateFwEnergyBankGuard() contains all updates to climbPitchCut, but its caller skips it
whenever isFixedwingAutoSpeedActive() is true. In an altitude-controlled waypoint climb using auto
speed, falling speed and an unmet climb demand therefore cannot lower the pitch ceiling, and an
existing cut cannot recover until auto speed stops.
Code

src/main/navigation/navigation_fixedwing.c[R707-708]

+    const bool underThrust = veryLowSpeedTrigger ||
+        (pitchPinned && (fwGuard.speedLow || (fwGuard.speedFalling && speed < NAV_FW_GUARD_DECEL_SPEED_FRAC * fwGuard.speedRef)) && fwGuard.deficitLatched && fwGuard.pinnedSec >= NAV_FW_GUARD_PINNED_MIN_S);
Evidence
The auto-speed branch bypasses the only guard call, while the new cut and its recovery are both
implemented inside that guard. Waypoint navigation can request auto speed while retaining altitude
control.

src/main/navigation/navigation_fixedwing.c[1825-1870]
src/main/navigation/navigation_fixedwing.c[700-716]
src/main/navigation/navigation_fixedwing.c[2025-2055]

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 new pitch-cut logic is bypassed whenever auto speed owns throttle, including altitude-controlled waypoint climbs.
## Fix Focus Areas
- src/main/navigation/navigation_fixedwing.c[1825-1870]
- src/main/navigation/navigation_fixedwing.c[633-716]
## Recommended Fix
Run the speed, climb-deficit, and pitch-cut updates on both throttle paths. Keep any bank-guard logic that requires the normal auto-throttle demand conditional on that demand being available.

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


2. Lost GPS speed can cut climb pitch 🐞 Bug ☼ Reliability
Description
updateFwEnergyBankGuard() uses posControl.actualState.velXY as ground speed without checking
whether the horizontal velocity estimate is usable. Without a valid pitot, loss of horizontal
estimation publishes zero speed; a latched climb deficit and nose-up demand then turn that
unavailable measurement into a pitch cut while altitude control can still run.
Code

src/main/navigation/navigation_fixedwing.c[R589-590]

+    float speed = posControl.actualState.velXY;     // cm/s
+    bool speedIsAirspeed = false;
Evidence
The estimator explicitly publishes zero XY velocity on horizontal-estimate failure, and navigation
records both that zero magnitude and an invalid velocity status. The new guard reads the magnitude
without the status and can pass its very-low-speed and deficit conditions.

src/main/navigation/navigation_pos_estimator.c[875-885]
src/main/navigation/navigation.c[4293-4314]
src/main/navigation/navigation_fixedwing.c[589-595]
src/main/navigation/navigation_fixedwing.c[685-716]

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

## Issue description
An invalid horizontal velocity estimate is published as zero and is treated as evidence of lost flight speed when no valid airspeed is available.
## Fix Focus Areas
- src/main/navigation/navigation_fixedwing.c[589-619]
- src/main/navigation/navigation_fixedwing.c[671-716]
## Recommended Fix
Gate ground-speed tracking and pitch-cut triggering on a usable horizontal velocity estimate. When valid speed returns, resynchronize the speed reference, previous sample, and deceleration state rather than comparing it against invalid samples.

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


3. A cut can persist after climb is cancelled 🐞 Bug ≡ Correctness
Description
climbPitchCut recovers only when deficitLatched clears, although the pitch trigger can remain
active through climbPitchCut > 0 without checking whether a climb is still commanded. After
switching to level flight or descent, an aircraft descending faster than its new target can retain
the deficit latch and remain unable to demand its full positive pitch to arrest that descent.
Code

src/main/navigation/navigation_fixedwing.c[R712-714]

+    } else if (!fwGuard.deficitLatched && (currentTimeUs - fwGuard.lastPitchTriggerUs) > ((timeUs_t)NAV_FW_GUARD_RECOVER_HOLDOFF_MS * 1000)) {
+        // Hold the ceiling until the climb demand is being met again, otherwise pitch returns to the limit and speed bleeds off again
+        fwGuard.climbPitchCut -= DEGREES_TO_DECIDEGREES(NAV_FW_GUARD_PITCH_RECOVER_DPS) * dtSec;
Evidence
The deficit latch compares target and actual vertical rates without climb gating; only the older
bank trigger requires climbCommanded. The new pitch trigger and recovery omit that gate, and the
reduced ceiling is applied to the altitude PID.

src/main/navigation/navigation_fixedwing.c[633-653]
src/main/navigation/navigation_fixedwing.c[700-716]
src/main/navigation/navigation_fixedwing.c[303-304]

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 pitch ceiling remains reduced while a vertical-rate deficit persists, even after the commanded climb ends.
## Fix Focus Areas
- src/main/navigation/navigation_fixedwing.c[633-653]
- src/main/navigation/navigation_fixedwing.c[700-716]
## Recommended Fix
Separate an unmet commanded climb from a generic target-minus-actual vertical-rate deficit. Stop extending the cut when climb is no longer requested, and allow the ceiling to recover for level or descent demands rather than waiting for the climb-deficit latch alone.

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



Remediation recommended

4. Pitot fallback can cause a false pitch cut 🐞 Bug ☼ Reliability
Description
speedIsAirspeed treats hardware pitot and virtual airspeed as the same source, so their transition
does not reset speedRef or prevSpeed. When pitot validation fails but virtual airspeed remains
available, a lower virtual reading is interpreted as speed loss against the hardware-derived
reference and can reduce climb pitch without a measured deceleration.
Code

src/main/navigation/navigation_fixedwing.c[R671-675]

+    if (speedIsAirspeed != fwGuard.speedIsAirspeed) {
+        // Airspeed and ground speed are not comparable: restart the reference on the new source
+        fwGuard.speedIsAirspeed = speedIsAirspeed;
+        fwGuard.speedRef = speed;
+        fwGuard.prevSpeed = speed;
Evidence
The pitot API switches its returned estimate to virtual airspeed after hardware failure, while
validity can remain true. The guard therefore sees the changed value but not a changed boolean
source, and processes the step as a speed change.

src/main/sensors/pitotmeter.c[397-413]
src/main/sensors/pitotmeter.c[468-527]
src/main/navigation/navigation_fixedwing.c[589-595]
src/main/navigation/navigation_fixedwing.c[671-708]

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

## Issue description
Hardware and virtual airspeed can replace one another while the new source flag remains unchanged, leaving discontinuous speed samples in the pitch guard.
## Fix Focus Areas
- src/main/navigation/navigation_fixedwing.c[589-595]
- src/main/navigation/navigation_fixedwing.c[671-681]
- src/main/sensors/pitotmeter.c[397-413]
## Recommended Fix
Track the actual speed producer as well as airspeed-versus-ground-speed selection. Reset the speed reference, prior sample, deceleration filter, and speed-loss latches whenever the hardware-to-virtual producer changes.

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


Grey Divider

Context sources
Review mode: Auto: ⚖️ Balanced: Downgraded extended -> standard: change is below the extended eligibility bar (hunks 8/18, lines 104/200; both must reach the floor). Router rationale: Substantial flight-control logic adds many interacting stateful branches and safety-critical edge cases.

Grey Divider

Tip of the day
💡 Did you know, you can add REVIEW.md to your repo root and Qodo follows it on every PR

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

Comment on lines +707 to +708
const bool underThrust = veryLowSpeedTrigger ||
(pitchPinned && (fwGuard.speedLow || (fwGuard.speedFalling && speed < NAV_FW_GUARD_DECEL_SPEED_FRAC * fwGuard.speedRef)) && fwGuard.deficitLatched && fwGuard.pinnedSec >= NAV_FW_GUARD_PINNED_MIN_S);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Action required

1. Auto-speed climbs never receive a pitch cut 🐞 Bug ≡ Correctness

updateFwEnergyBankGuard() contains all updates to climbPitchCut, but its caller skips it
whenever isFixedwingAutoSpeedActive() is true. In an altitude-controlled waypoint climb using auto
speed, falling speed and an unmet climb demand therefore cannot lower the pitch ceiling, and an
existing cut cannot recover until auto speed stops.
Agent Prompt
## Issue description
The new pitch-cut logic is bypassed whenever auto speed owns throttle, including altitude-controlled waypoint climbs.
## Fix Focus Areas
- src/main/navigation/navigation_fixedwing.c[1825-1870]
- src/main/navigation/navigation_fixedwing.c[633-716]
## Recommended Fix
Run the speed, climb-deficit, and pitch-cut updates on both throttle paths. Keep any bank-guard logic that requires the normal auto-throttle demand conditional on that demand being available.

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

Comment on lines +589 to +590
float speed = posControl.actualState.velXY; // cm/s
bool speedIsAirspeed = false;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Action required

2. Lost gps speed can cut climb pitch 🐞 Bug ☼ Reliability

updateFwEnergyBankGuard() uses posControl.actualState.velXY as ground speed without checking
whether the horizontal velocity estimate is usable. Without a valid pitot, loss of horizontal
estimation publishes zero speed; a latched climb deficit and nose-up demand then turn that
unavailable measurement into a pitch cut while altitude control can still run.
Agent Prompt
## Issue description
An invalid horizontal velocity estimate is published as zero and is treated as evidence of lost flight speed when no valid airspeed is available.
## Fix Focus Areas
- src/main/navigation/navigation_fixedwing.c[589-619]
- src/main/navigation/navigation_fixedwing.c[671-716]
## Recommended Fix
Gate ground-speed tracking and pitch-cut triggering on a usable horizontal velocity estimate. When valid speed returns, resynchronize the speed reference, previous sample, and deceleration state rather than comparing it against invalid samples.

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

Comment on lines +712 to +714
} else if (!fwGuard.deficitLatched && (currentTimeUs - fwGuard.lastPitchTriggerUs) > ((timeUs_t)NAV_FW_GUARD_RECOVER_HOLDOFF_MS * 1000)) {
// Hold the ceiling until the climb demand is being met again, otherwise pitch returns to the limit and speed bleeds off again
fwGuard.climbPitchCut -= DEGREES_TO_DECIDEGREES(NAV_FW_GUARD_PITCH_RECOVER_DPS) * dtSec;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Action required

3. A cut can persist after climb is cancelled 🐞 Bug ≡ Correctness

climbPitchCut recovers only when deficitLatched clears, although the pitch trigger can remain
active through climbPitchCut > 0 without checking whether a climb is still commanded. After
switching to level flight or descent, an aircraft descending faster than its new target can retain
the deficit latch and remain unable to demand its full positive pitch to arrest that descent.
Agent Prompt
## Issue description
The pitch ceiling remains reduced while a vertical-rate deficit persists, even after the commanded climb ends.
## Fix Focus Areas
- src/main/navigation/navigation_fixedwing.c[633-653]
- src/main/navigation/navigation_fixedwing.c[700-716]
## Recommended Fix
Separate an unmet commanded climb from a generic target-minus-actual vertical-rate deficit. Stop extending the cut when climb is no longer requested, and allow the ceiling to recover for level or descent demands rather than waiting for the climb-deficit latch alone.

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

Comment on lines +671 to +675
if (speedIsAirspeed != fwGuard.speedIsAirspeed) {
// Airspeed and ground speed are not comparable: restart the reference on the new source
fwGuard.speedIsAirspeed = speedIsAirspeed;
fwGuard.speedRef = speed;
fwGuard.prevSpeed = speed;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Remediation recommended

4. Pitot fallback can cause a false pitch cut 🐞 Bug ☼ Reliability

speedIsAirspeed treats hardware pitot and virtual airspeed as the same source, so their transition
does not reset speedRef or prevSpeed. When pitot validation fails but virtual airspeed remains
available, a lower virtual reading is interpreted as speed loss against the hardware-derived
reference and can reduce climb pitch without a measured deceleration.
Agent Prompt
## Issue description
Hardware and virtual airspeed can replace one another while the new source flag remains unchanged, leaving discontinuous speed samples in the pitch guard.
## Fix Focus Areas
- src/main/navigation/navigation_fixedwing.c[589-595]
- src/main/navigation/navigation_fixedwing.c[671-681]
- src/main/sensors/pitotmeter.c[397-413]
## Recommended Fix
Track the actual speed producer as well as airspeed-versus-ground-speed selection. Reset the speed reference, prior sample, deceleration filter, and speed-loss latches whenever the hardware-to-virtual producer changes.

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

@github-actions

Copy link
Copy Markdown

RAM / Flash usage vs. base commit 7c2ac35 — commit f9882f0

Target Flash Δ RAM Δ
MATEKF405 +976 B (+0.13%) CCM: ±0 B (±0.00%)
RAM: +48 B (+0.05%)
MATEKF722 +736 B (+0.15%) ITCM_RAM: ±0 B (±0.00%)
RAM: +48 B (+0.06%)
TCM: ±0 B (±0.00%)
MATEKF765 +1040 B (+0.14%) DTCM_RAM: ±0 B (±0.00%)
SRAM1: +48 B (+0.04%)
MATEKH743 +1040 B (+0.13%) D2_RAM: ±0 B (±0.00%)
DTCM_RAM: ±0 B (±0.00%)
ITCM_RAM: ±0 B (±0.00%)
RAM: +64 B (+0.04%)

See RAM/flash optimization guide for techniques to reduce usage.

@github-actions

Copy link
Copy Markdown

Test firmware build ready — commit f9882f0

Download firmware for PR #12194

251 targets built. Find your board's .hex file by name on that page (e.g. MATEKF405SE.hex). Files are individually downloadable — no GitHub login required.

Development build for testing only. Use Full Chip Erase when flashing.

#define NAV_FW_GUARD_REDUCE_RATE_DPS 20.0f // bank-limit reduce rate
#define NAV_FW_GUARD_RECOVER_RATE_DPS 5.0f // bank-limit recover rate
#define NAV_FW_GUARD_RECOVER_HOLDOFF_MS 1000 // healthy time before recovery starts
#define NAV_FW_GUARD_PITCH_REDUCE_DPS 20.0f // climb-pitch ceiling reduce rate when thrust is insufficient

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Does it make sense to include these constant defines when they're only used once in the code ? If used more than once then it's a necessity otherwise it just makes the code more difficult to read having to find the defined value somewhere else. Why not just have the actual value where it's used and add a comment if need be to clarify what it is ?

Just a general quibble ... nothing more.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Agreed, thank you.
It would be better for them to be local const, which will be optimized away by the compiler, but descriptive for those reading the code.

Or have it as a literal with comments as needed.

I don't want to read too much into what you did NOT say.
Since it seems you have at least begun reading the code, do you have feedback, good or bad, on the physics etc?

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.

2 participants