Skip to content

compass: mag_learn fits the offsets against the attitude while the GSF heading is usable, and learns mag_yaw_trim - #12160

Open
MrScothh wants to merge 12 commits into
iNavFlight:maintenance-10.xfrom
MrScothh:feature/mag-learn-gsf
Open

MrScothh wants to merge 12 commits into
iNavFlight:maintenance-10.xfrom
MrScothh:feature/mag-learn-gsf

Conversation

@MrScothh

@MrScothh MrScothh commented Oct 18, 2026 •

Copy link
Copy Markdown
Contributor

Builds on #12159 (GSF heading) and #12142 (mag_learn): the change of its own is the last commit.

#12142 fits the compass offsets to a sphere: it needs about 270 degrees of turning, can't tell a compass turned about the vertical, and refuses large steps (over 30 % of the field) because a sphere fit can be wrong in ways it can't see. With the heading of #12159 the readings can also be fitted against the attitude, which sees those.

The attitude fit:

  • With the GSF heading and the AHRS tilt, each reading is m = R^T B + o: B the earth field in INAV's north/west/up frame, o the offsets in body axes at the stored scale, linear in six unknowns. Only weighted running means of R, m, R m and |m|^2 are kept (17 floats, no samples in RAM); eliminating B leaves a 3x3 system, with a small ridge that keeps o at the stored offsets along directions a flight never swept (the vertical, in flat turns).
  • Samples only while the GSF heading is usable with sigma under 10 degrees, each weighed by how sure it is, (3 degrees / sigma)^2 up to 1, at most 200 of weight per heading sector: the first samples after the heading turns usable, its least sure, count little.
  • It needs a weight of 100 over at least 6 heading sectors, so a half turn is enough: with more sectors than unknowns a heading error that differs between the legs leaves a residual. Then an RMS residual under 12 % of the field (otherwise MISFIT: wrong orientation, scale or soft iron); the field's direction within 15 degrees of the declination the AHRS applies (otherwise DECLINATION_OFF: a compass turned about the vertical, which the residual doesn't show in flat turns); |B| within 30 % of 1024.
  • Not on tailsitters, whose body axes turn at each transition.

How it decides, with the sphere fit of #12142 as the other candidate, compared in body axes:

  • a large step (over 30 %) is saved only when both fits agree on X and Y within 10 % of the field;
  • a small step takes the attitude fit's X and Y;
  • the vertical moves at most 10 % of the field a flight on every path, since it is where the GPS delay leaves its mark;
  • the attitude fit alone (a half turn, not enough coverage for the sphere fit) moves every axis at most 10 % a flight and refuses a step over 30 %, so a systematic heading error can only move the offsets a little a flight;
  • fits that disagree set FITS_DISAGREE and save nothing; the sphere fit alone works as in compass: learn the offsets in flight (mag_learn) #12142;
  • a change under 1 % of the gain on every axis sets CONFIRMED and writes nothing, so a good calibration is not rewritten flight after flight.

mag_yaw_trim (new, -300..300 decidegrees, default 0) turns the compass reading about the airplane's vertical after align_mag, positive clockwise: the alignment between a compass and the board can't be measured by hand better than a few degrees. With mag_learn ON the attitude fit refines it: the angle between the fitted field and magnetic north, at most 5 degrees a flight, nothing under 1 degree, never past the setting's range; over 15 degrees, or with no room left in the range, it is reported as DECLINATION_OFF instead of trimmed. A pending trim is enough for a save. A compass calibration, or an orientation the calibration detects, sets it back to 0; after changing align_mag by hand it should be set to 0 too.

Blackbox: magBiasField (the fitted field), magBiasYawTrim, the mag_yaw_trim header line, and the flags ATTITUDE_FIT (2), MISFIT (3), DECLINATION_OFF (12), FITS_DISAGREE (13), CONFIRMED (14) in magBiasFlags; magBiasSpread is the misfit of the fit that decided, 0 when none did. MSP2_INAV_MAG_LEARN gets the field and the trim appended (20 bytes), so a configurator that reads the old 12 bytes still works; msp_messages.json 2.2.0 and the README regenerated from it. The version in msp_messages.json conflicts with #12156 and #12141, which raise it too: whichever is merged second takes the next one.

Size against #12159:

Target Flash RAM
TBS_LUCID_H7_WING +3284 B +128 B
KAKUTEF7 / KAKUTEF7HDV +3060 / +3044 B +136 B, ITCM unchanged
SPEEDYBEEF745AIO +3220 B +128 B
MATEKF765 +3224 B +144 B
MATEKF405 +3264 B (80.34 %) +120 B
NEUTRONRCF435WING +3056 B +116 B (89.53 %)
MATEKF722 unchanged: no USE_MAG_LEARN on 512 KB, and the trim lives with it

compassConfig_t gets magYawTrim (int16), inside the same #ifdef USE_MAG_LEARN as magLearn, with a zero default: on targets with USE_MAG_LEARN it sits at offset 26, the end of #12142's record (26 bytes on the PG reference target), never in its padding, so the PG version stays and the reference size is 28; on the others nothing changes.

Tested:

  • 23 new unit tests next to compass: learn the offsets in flight (mag_learn) #12142's 13, among them: a large offset fixed on X and Y in one flight and on Z 10 % a flight, a half turn moving the offsets at most 10 % a flight and closing them in two, a big step needing both fits, a heading error of ±12 degrees between two legs kept within 10 %, early unsure samples 8 degrees off barely counting, a tilted compass making the fits disagree, a good calibration confirmed and not rewritten, the declination learned, offsets back in sensor axes, a turned or flipped compass, a wrong declination setting, the trim learned over three flights, at most 5 degrees a flight, untouched by the sphere fit, and a trim at the end of its range reported and not saved. Taking out the 10 % cap, the weights or the range limit each makes one of them fail.
  • SITL, offsets wrong by (500, -400, 300) on a field of 1024: both fits agree and save (3, -9, 13); right offsets: CONFIRMED, nothing written; trim wrong by 8 degrees: -3 after one flight, -1.3 after two (the SITL's fake GPS has an error of its own of 1-3 degrees). These SITL runs were before the 10 % caps, the weights and the half-turn rule.
  • HITL with X-Plane 11 on a TBS Lucid H7, offsets (300, -250, 200): saved (12, 21, 93), then (0, 12, 57) and (5, 10, 46) on the next flights; with the compass turned 30 degrees, DECLINATION_OFF and the sphere fit decided. Also before the caps, the weights and the half-turn rule.

Configurator: iNavFlight/inav-configurator#2845. Blackbox Explorer: iNavFlight/blackbox-log-viewer#128.

The header declared motor[0..7], while the I and P frames write
getMotorCount() values, up to MAX_SUPPORTED_MOTORS (12). With 9 to 12 motors
and motor logging on, every frame carried more values than the header
described, and no frame of the log decoded.

The header now declares motor[8..11] behind four more conditions. The
condition cache was a uint64_t already full on dual-gyro targets, so it
becomes an array of 32-bit words sized for every condition. The MSP enum
reference gets the new values (that enum's section only).
With mag_learn ON, the compass keeps one raw reading per field direction
during each flight (12 headings x 6 equal-area elevation bands), refits the
offsets with the sphere fit the ground calibration already uses and saves
magzero at disarm when the flight turned through at least 270 degrees and
the fit agrees with the stored calibration: radius spread within 5%, radius
within 30% of what maggain implies, no axis moving more than 30% of the
radius. maggain is kept.

The fit runs on samples scaled by maggain, so axes of different sensitivity
weigh alike, and a ridge toward the stored offset keeps a direction the
flight never swept (the vertical in level flight, the normal of a circle
flown at one bank) where it was.

A calibration started while armed, or mag_learn switched off, drops what the
flight had collected. The save waits out the 5 s in which INAV accepts an
emergency rearm after a disarm, so a rearm finds the same calibration and no
flash write, and then for the aircraft to be still; if it still moves a minute
after the disarm, nothing is saved. EMERGENCY_INFLIGHT_REARM_TIME_WINDOW_MS
moves to fc_core.h for that.

The estimate is refitted about once a second in flight and logged in the
blackbox slow frame (magBias, magBiasFlags, magBiasSectors, magBiasSpread;
header mag_learn and mag_zero) under its own field group, MAG_LEARN (on by
default in a new configuration, logged only while mag_learn is ON), and
MSP2_INAV_MAG_LEARN reports it for the Configurator. Built on iNavFlight#12127 for the wider condition cache and on
the slow-frame conditions.

USE_MAG_LEARN on USE_MAG targets with more than 512 KB of flash. The setting
is appended to compassConfig_t with a zero default, so the PG version stays:
on ARM (short enums) the struct had no tail padding and grows from 24 to 26
bytes; on SITL the new byte lands in former tail padding, which the reset
template writes as zero.
A Gaussian sum of five 3-state EKFs (velocity north and east, yaw), each
started from a different yaw, as in Paul Riseborough's EKF-GSF yaw
estimator. Each model is handed the horizontal acceleration and the yaw
rate instead of running its own attitude filter, so roll and pitch can
come from INAV's AHRS. Core only, with unit tests on simulated flights.
Every AHRS update adds the horizontal acceleration and the yaw rate, both
from rMat's roll and pitch only, so the compass never reaches the
estimate. A 50 Hz task steps the filter on 20 ms windows held back by the
GPS delay and fuses each GPS velocity at the time it was taken, with a 3D
fix, 6 satellites and 5 m accuracy, and not while the aircraft rotates
faster than 90 deg/s. It learns from INAV's first flight detection to its
landing detection, since isFlightDetected alone drops whenever the
throttle is under cruise or hover. The estimate counts as usable once it
has seen 25 m/s of velocity change and its sigma has stayed under 15 deg
for 3 s. Blackbox group GSF: yaw, sigma, velocity change seen, filtered
NIS of the leading model, flags. Targets with more than 512 KB of flash.
…sf_yaw)

The GPS delay starts at one receiver interval plus 20 ms (120 ms at 10 Hz,
220 ms at 5 Hz, ArduPilot's u-blox lags) and is then measured in flight:
for each delay from 0 to 300 ms, how well the 400 ms velocity change of
the IMU matches the GPS one after the best rotation, the rotation standing
for the unknown heading. A measurement is kept once its least-squares
sigma is under 25 ms. Manoeuvres count per second and only past 1 m/s,
so vibration and tilt errors do not add up in straight flight. A filtered
NIS over 6 flags the estimate inconsistent and not usable; 10 s of it
start it over and give the compass its trust back.

The yaw rate fed to the models is the rate of the nose's heading (the
Euler yaw rate from roll and pitch), not the rate about the vertical: a
roll about the nose at pitch leaves the heading alone.

With ahrs_gsf_yaw on (off by default), a usable estimate replaces the GPS
course as the AHRS's GPS heading, the compass is left out after it has
stayed 25 deg or 3 sigma off it for 3 s (back after 10 s within half
that, and whenever disarmed), and a heading 45 deg off with no compass
to hold it is reset to it, at most twice a flight; the navigation
heading target moves with the reset so the aircraft does not turn.
The compass still reaches the estimate through the tilt, since the GPS
inertia compensation uses the full orientation. Timers are 64-bit, so a
flight past 35 min keeps its state. The IMU accumulation runs only while
the task does. A tailsitter transition restarts the estimator.

Blackbox: gsfGpsDelay, gsfMagErr, more flags, header ahrs_gsf_yaw; the
log is where the estimator is read, there is no MSP message. imuConfig_t
grows by the appended zero-default field without a version bump (pgLoad
copies the stored bytes over the defaults): the PG size database says
18 bytes at version 2.
…mag_yaw_trim

With the GSF heading known each sample is the earth field turned into the body plus the offset,
linear in both, and only weighted running means are kept. Samples count while the heading is
usable, weighed by how sure it is; the fit needs a half turn, more heading sectors than unknowns,
so a heading error that differs between the legs leaves a residual. Its residual tells a compass
that does not follow the attitude (orientation, scale, soft iron), the fitted field's direction
one turned about the vertical. No attitude fit on tailsitters, whose body axes turn at each
transition.

The sphere fit still decides as before. The attitude fit confirms a step beyond the 30% limit
when the two agree on the horizontal offsets, refines a smaller one, and alone moves every axis
at most 10% of the field a flight; the vertical never moves more than that. Fits that disagree
are a tilt the alignment does not know, and nothing is saved. A change under 1% is not written.

mag_yaw_trim turns the reading about the airplane's vertical after the alignment, which can only
be set to what was measured by hand. While the attitude fit decides, the angle between the field
it found and magnetic north becomes a change to the trim: up to 5 degrees a flight, nothing under
1, within the setting's range; beyond 15 degrees, or with no room left, the compass is reported
as turned instead. It lives with magLearn under USE_MAG_LEARN, appended with a zero default; a
compass calibration or a detected orientation sets it back to 0.

The fitted field and the trim go to the blackbox (magBiasField, magBiasYawTrim, the mag_yaw_trim
header line) and are appended to MSP2_INAV_MAG_LEARN (msp_messages.json 2.2.0, README regenerated).
@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

Learn compass offsets and yaw trim using a GPS-derived heading

✨ Enhancement 🐞 Bug fix 🧪 Tests 📝 Documentation 🕐 40+ Minutes

Grey Divider

AI Description

• Add GPS/IMU-derived GSF yaw for compass-independent heading checks and optional AHRS correction.
• Combine attitude and sphere fits to safely learn compass offsets and yaw trim in flight.
• Expose diagnostics through Blackbox and MSP, fix 9–12 motor log headers, and add tests.
Diagram

graph TD
  GPS["GPS velocity"] --> GSF["GSF estimator"] --> AHRS["AHRS heading"] --> FIT["Compass learning"] --> CFG[("Saved calibration")]
  IMU["IMU motion"] --> GSF
  IMU --> FIT
  MAG["Compass readings"] --> FIT
  GSF --> FIT
  FIT --> LOG["MSP and Blackbox"]
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Retain sphere-only learning
  • ➕ Less estimation code and lower resource use
  • ➕ Does not depend on GPS heading quality
  • ➖ Needs broader turning coverage
  • ➖ Cannot reliably identify yaw misalignment or validate large offset steps
2. Port an existing GSF implementation
  • ➕ Potentially reuses a field-proven estimator
  • ➖ Requires adapting another project's attitude, timing, GPS, and memory assumptions
  • ➖ May complicate target-size constraints and integration

Recommendation: Keep the native, bounded-memory GSF and use the attitude fit alongside—not instead of—the established sphere fit. Their agreement and conservative per-flight limits provide independent checks; review should focus on estimator confidence, coordinate-frame consistency, and save behavior.

Files changed (32) +3398 / -181

Enhancement (13) +1948 / -71
blackbox.cLog estimator and calibration diagnostics +163/-49

Log estimator and calibration diagnostics

• Adds conditional GSF main fields and compass-learning slow fields, including fitted field and trim. Declares motor fields through motor 11 and expands the condition cache so their headers match logged frames.

src/main/blackbox/blackbox.c

blackbox.hDeclare new Blackbox feature groups +2/-0

Declare new Blackbox feature groups

• Adds MAG_LEARN and GSF bits to the logging include mask.

src/main/blackbox/blackbox.h

fc_msp.cReturn compass-learning status over MSP +17/-0

Return compass-learning status over MSP

• Implements the MAG_LEARN response with flags, coverage, spread, offset deltas, fitted earth field, and pending yaw trim.

src/main/fc/fc_msp.c

fc_tasks.cSchedule GSF yaw estimation +13/-0

Schedule GSF yaw estimation

• Enables a 50 Hz GSF task when GPS and accelerometer inputs are available.

src/main/fc/fc_tasks.c

gsf_yaw.cImplement GPS/IMU yaw estimation +878/-0

Implement GPS/IMU yaw estimation

• Adds a five-model Gaussian-sum yaw filter, GPS-delay estimation, usability checks, and compass disagreement tracking. Supplies optional AHRS heading input and bounded reset requests.

src/main/flight/gsf_yaw.c

gsf_yaw.hDefine GSF estimator interfaces and status +115/-0

Define GSF estimator interfaces and status

• Declares filter and delay-estimator state, confidence flags, and firmware integration functions.

src/main/flight/gsf_yaw.h

imu.cFuse usable GSF heading into AHRS +103/-3

Fuse usable GSF heading into AHRS

• Optionally replaces GPS-course heading input with GSF yaw, suppresses a persistently disagreeing compass, and handles bounded heading resets. Exposes the applied declination and compass-derived heading for calibration and comparison.

src/main/flight/imu.c

imu.hExpose AHRS GSF setting and heading helpers +8/-0

Expose AHRS GSF setting and heading helpers

• Adds the optional GSF setting and declarations for magnetic declination and compass-heading queries.

src/main/flight/imu.h

msp_protocol_v2_inav.hAssign compass-learning MSP command +2/-0

Assign compass-learning MSP command

• Defines MSP2_INAV_MAG_LEARN as command 0x2235.

src/main/msp/msp_protocol_v2_inav.h

navigation_pos_estimator.cForward GPS updates to GSF +5/-0

Forward GPS updates to GSF

• Notifies the yaw estimator when new GPS data arrives so it can time and fuse velocity samples.

src/main/navigation/navigation_pos_estimator.c

compass.cIntegrate flight learning and yaw trim +167/-19

Integrate flight learning and yaw trim

• Applies yaw trim after compass alignment and collects samples when calibrated and flying. Evaluates fits during flight, waits past the emergency rearm window after disarm, and saves validated offsets and trim; calibration resets trim.

src/main/sensors/compass.c

compass_learn.cFit compass offsets to sphere and attitude +375/-0

Fit compass offsets to sphere and attitude

• Implements binned sphere fitting and weighted running-statistics attitude fitting. Compares candidates, limits unsafe steps and trim changes, and reports save or rejection status.

src/main/sensors/compass_learn.c

compass_learn.hDefine compass-learning limits and diagnostics +100/-0

Define compass-learning limits and diagnostics

• Declares sample coverage, confidence and step thresholds, result flags, status fields, and fit interfaces.

src/main/sensors/compass_learn.h

Bug fix (1) +7 / -0
blackbox_fielddefs.hExtend conditional log fields +7/-0

Extend conditional log fields

• Adds motor-count conditions for motors 9–12 and conditions for GSF and compass-learning diagnostics.

src/main/blackbox/blackbox_fielddefs.h

Refactor (2) +2 / -1
fc_core.cShare emergency rearm timing constant +0/-1

Share emergency rearm timing constant

• Removes the local definition now published by fc_core.h for compass-learning save timing.

src/main/fc/fc_core.c

fc_core.hPublish emergency rearm window +2/-0

Publish emergency rearm window

• Makes the five-second emergency rearm window available to calibration save logic.

src/main/fc/fc_core.h

Documentation (7) +317 / -109
Blackbox.mdDocument compass-learning log group +1/-0

Document compass-learning log group

• Adds the MAG_LEARN Blackbox include flag and its slow-frame behavior.

docs/Blackbox.md

Sensors.mdExplain in-flight compass offset learning +10/-0

Explain in-flight compass offset learning

• Documents sphere-fit coverage and validation, flight-only sampling, delayed saving, and calibration diagnostics.

docs/Sensors.md

Settings.mdDocument GSF and compass settings +30/-0

Document GSF and compass settings

• Adds generated entries for ahrs_gsf_yaw, mag_learn, and mag_yaw_trim, including defaults and limits.

docs/Settings.md

README.mdDocument MAG_LEARN MSP response +19/-0

Document MAG_LEARN MSP response

• Adds the generated command reference, including the extended 20-byte reply, flags, and compatibility notes.

docs/development/msp/README.md

inav_enums.jsonUpdate exported logging and estimator enums +95/-54

Update exported logging and estimator enums

• Adds GSF and compass-learning flags and Blackbox feature bits; renumbers and extends log-field conditions for 12 motors.

docs/development/msp/inav_enums.json

inav_enums_ref.mdRegenerate enum reference +102/-53

Regenerate enum reference

• Reflects the new estimator flags, logging groups, and motor field conditions in the generated tables.

docs/development/msp/inav_enums_ref.md

msp_messages.jsonSpecify the compass-learning MSP message +60/-2

Specify the compass-learning MSP message

• Defines MSP2_INAV_MAG_LEARN and its 20-byte response, and raises the specification version to 2.2.0.

docs/development/msp/msp_messages.json

Other (9) +1124 / -0
CMakeLists.txtBuild GSF and compass-learning modules +4/-0

Build GSF and compass-learning modules

• Adds the new estimator and calibration source and header files to the main source list.

src/main/CMakeLists.txt

cli.cExpose logging groups in CLI +2/-0

Expose logging groups in CLI

• Adds MAG_LEARN and GSF names to Blackbox include-flag settings.

src/main/fc/cli.c

settings.yamlConfigure GSF heading and compass learning +19/-0

Configure GSF heading and compass learning

• Defines default-off ahrs_gsf_yaw and mag_learn settings plus the bounded mag_yaw_trim setting.

src/main/fc/settings.yaml

scheduler.hDeclare GSF scheduler task +3/-0

Declare GSF scheduler task

• Adds the conditionally compiled TASK_GSF_YAW identifier.

src/main/scheduler/scheduler.h

compass.hPersist compass learning and yaw trim settings +4/-0

Persist compass learning and yaw trim settings

• Adds magLearn and magYawTrim to compass configuration on supported targets.

src/main/sensors/compass.h

common_post.hGate estimators by target flash size +10/-0

Gate estimators by target flash size

• Enables compass learning and GSF on applicable targets with more than 512 KB of flash.

src/main/target/common_post.h

CMakeLists.txtRegister estimator unit tests +6/-0

Register estimator unit tests

• Builds the GSF and compass-learning test suites with their required sources and feature definitions.

src/test/unit/CMakeLists.txt

compass_learn_unittest.ccExercise compass-fit safety and trim behavior +774/-0

Exercise compass-fit safety and trim behavior

• Tests coverage, scale and interference rejection, fit agreement, heading uncertainty, bounded corrections, orientation and declination errors, and multi-flight trim learning.

src/test/unit/compass_learn_unittest.cc

gsf_yaw_unittest.ccTest GSF convergence and GPS delay +302/-0

Test GSF convergence and GPS delay

• Simulates turns, noisy velocity, heading wraparound, insufficient excitation, GPS delay, and body-rate conversion.

src/test/unit/gsf_yaw_unittest.cc

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

qodo-free-for-open-source-projects Bot commented Oct 18, 2026 •

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (0) 📘 Rule violations (0) 📎 Requirement gaps (0) 🎨 UX issues (0) 🔗 Cross-repo conflicts (0) 📜 Skill insights (0)

Grey Divider


Action required

1. GPS loss leaves the compass disabled ✓ Resolved
Description
taskGsfYaw has no timeout for valid GPS velocity updates, and gsfYawGetAhrsInput sets
ignoreMag from the latched disagreement state even when the heading estimate is no longer usable.
If GPS updates stop after the compass has been marked disagreeing, an airborne airplane can continue
excluding its compass after the estimate loses its heading reference.
Code

src/main/flight/gsf_yaw.c[R870-874]

+    if (enabled && (gsfStatus.flags & GSF_YAW_USABLE)) {
+        in->weight = constrainf(scaleRangef(gsfStatus.yawSigma, GSF_AHRS_FULL_SIGMA, GSF_USABLE_SIGMA, 1.0f, 0.0f), 0.0f, 1.0f);
+    }
+    in->ignoreMag = enabled && gsfMagDisagrees;
+    in->resetYaw = gsfResetRequested;
Evidence
Invalid GPS input returns without invalidating GSF state. Usability has no GPS-age condition, while
the AHRS input excludes the compass based on the disagreement latch independently of usability.

src/main/flight/gsf_yaw.c[572-598]
src/main/flight/gsf_yaw.c[764-774]
src/main/flight/gsf_yaw.c[829-875]
src/main/flight/imu.c[843-861]

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 GSF estimate can remain usable without recent valid GPS velocity, and a latched disagreement can keep the AHRS from using the compass even after the estimate becomes unusable.
## Fix Focus Areas
- src/main/flight/gsf_yaw.c[572-598]
- src/main/flight/gsf_yaw.c[829-875]
## Recommended Fix
Track the time of the last accepted GPS velocity and make recent valid input a requirement for a usable GSF heading. Stop excluding the compass when that requirement fails; ensure a stale disagreement cannot remain active indefinitely.

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


2. GPS spikes bypass outlier clipping ✓ Resolved
Description
correctModel uses a positive off-diagonal term when calculating the inverse-covariance innovation
ratio, although the inverse requires a negative term. With correlated north/east errors, some GPS
spikes fall below the clipping threshold and receive an unscaled state correction, while some
ordinary updates are clipped.
Code

src/main/flight/gsf_yaw.c[R159-160]

+    const float iNE = m->S[0][1] * detS;
+    const float testRatio = m->innov[0] * (m->innov[0] * iNN + m->innov[1] * iNE) + m->innov[1] * (m->innov[0] * iNE + m->innov[1] * iEE);
Evidence
For an innovation covariance [[2, 0.5], [0.5, 2]] and innovation (5, -5), the correct squared ratio
is 33.33, but the implemented positive cross term gives 20, below the clipping threshold of 25. The
resulting scale is applied directly to the Kalman correction; the Gaussian-density calculation
elsewhere uses the negative inverse off-diagonal sign.

src/main/flight/gsf_yaw.c[146-161]
src/main/flight/gsf_yaw.c[217-220]
src/main/flight/gsf_yaw.c[227-239]

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 GPS outlier test uses the wrong sign for the off-diagonal term of the inverse innovation covariance, changing which velocity updates are clipped.
## Fix Focus Areas
- src/main/flight/gsf_yaw.c[152-161]
- src/test/unit/gsf_yaw_unittest.cc[1-302]
## Recommended Fix
Negate the off-diagonal inverse term in `correctModel`. Add a test with nonzero north/east covariance and opposite-sign innovations that verifies a greater-than-five-sigma update is clipped.

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



Informational

3. Learning status shows an old offset step after the fits drop out ✓ Resolved
Description
magLearnEvaluate clears field, spread and yawTrimDelta but leaves magLearnStatus.delta
unchanged when neither fit is available. If an earlier attitude fit produced a delta and later fails
before the sphere fit has enough coverage, Blackbox magBias[] and the MSP2_INAV_MAG_LEARN delta
continue reporting that unsupported step alongside failure flags and zero spread, although the stale
value is not saved.
Code

src/main/sensors/compass_learn.c[R352-355]

+    } else {
+        magLearnStatus.flags = flags;
+        return;
+    }
Evidence
magLearnEvaluate resets field at line 300, yawTrimDelta at line 310 and spread at line 313,
but writes delta only at lines 358-364, after the no-fit return at lines 352-355. magLearnReset
clears the status only at arming (lines 51-57), so an earlier delta persists within the flight and
is published unconditionally by slow-frame logging and the MSP reply. The rebuilt flags remove
SAVE_DUE, making this a misleading reported status rather than an unintended configuration write.

src/main/sensors/compass_learn.c[296-373]
src/main/sensors/compass_learn.c[51-57]
src/main/sensors/compass_learn.c[299-313]
src/main/sensors/compass_learn.c[348-364]
src/main/fc/fc_msp.c[1766-1778]
src/main/blackbox/blackbox.c[1672-1678]

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

## Issue description
`magLearnEvaluate` returns when neither fit is usable without clearing `magLearnStatus.delta`, leaving Blackbox and MSP to report an earlier offset step even though the current evaluation has no fit.
## Fix Focus Areas
- src/main/sensors/compass_learn.c[299-313]
- src/main/sensors/compass_learn.c[352-355]
## Recommended Fix
Clear `magLearnStatus.delta` at the start of each evaluation, next to the `field` reset, using `memset(magLearnStatus.delta, 0, sizeof(magLearnStatus.delta));`, or clear it in the no-fit branch before returning. Preserve the delta reported after a successful save. Test an attitude-only estimate followed by an evaluation in which that fit fails before sphere coverage is sufficient.

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


4. Compass learning docs describe only the old sphere fit ✓ Resolved
Description
The new docs/Sensors.md section lists the magBiasFlags bits as 0, 1, 4-9 and 11 and describes
saving only after a 270-degree turn with the sphere-fit checks. It leaves out the attitude fit,
which saves after a half turn with a 10% cap, bits 2, 3, 12, 13 and 14, magBiasField,
magBiasYawTrim and the mag_yaw_trim header line. Pilots who read blackbox logs or MSP output
from this page will misread saves that the attitude fit decided, and flags such as CONFIRMED or
FITS_DISAGREE.
Code

docs/Sensors.md[26]

+With the `MAG_LEARN` field group selected (`blackbox MAG_LEARN`; on by default in a new configuration, while one kept from an earlier version needs it set, or the box in the Configurator's Blackbox tab), the blackbox logs the estimate, refitted about once a second, in the slow frame: `magBias[0..2]` (the change a save would write, in raw counts), `magBiasFlags` (bit 0 collecting, 1 paused, 4 save due, 5 too few sectors, 6 too few headings, 7 not a sphere, 8 off the stored scale, 9 step too big, 11 disarmed while flying), `magBiasSectors` and `magBiasSpread` (0.1% of the radius). The header records `mag_learn` and the `mag_zero` the flight started from. With the default `blackbox_arm_control` the log closes at disarm, before the save, and the next log's `mag_zero` shows what was written. `MSP2_INAV_MAG_LEARN` reports the same state, with bit 10 when the last disarm saved.
Evidence
compass_learn.h defines MAG_LEARN_ATTITUDE_FIT (bit 2), MISFIT (3), DECLINATION_OFF (12),
FITS_DISAGREE (13) and CONFIRMED (14), and the blackbox writes magBiasField and magBiasYawTrim.
docs/Sensors.md line 26 lists only the older bits and fields, and lines 15-20 give only the
sphere-fit save conditions.

src/main/sensors/compass_learn.h[56-72]
docs/Sensors.md[12-27]
src/main/blackbox/blackbox.c[556-567]

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 `docs/Sensors.md` section on compass offset learning still covers only the sphere fit and an older set of flags.
## Fix Focus Areas
- docs/Sensors.md[12-27]
## Recommended Fix
Add a paragraph on the attitude fit: it runs while the GSF heading is usable, needs a half turn, caps each step at 10% per flight, and saves a large step only when both fits agree. Document `mag_yaw_trim` learning. Extend the `magBiasFlags` list with bits 2, 3, 12, 13 and 14, and mention `magBiasField`, `magBiasYawTrim` and the `mag_yaw_trim` header line.

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


Grey Divider

Tip of the day
💡 Did you know, you can import rules and skills only from the repositories your team follows

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

Comment thread src/main/flight/gsf_yaw.c
Comment thread src/main/flight/gsf_yaw.c Outdated
Comment thread src/main/sensors/compass_learn.c
Comment thread docs/Sensors.md Outdated
@github-actions

github-actions Bot commented Oct 18, 2026 •

Copy link
Copy Markdown

RAM / Flash usage vs. base commit 13ebed9 — commit 5becae3

Target Flash Δ RAM Δ
MATEKF405 ⚠️ +14512 B (+2.00%) CCM: +2368 B (+7.27%)
RAM: +268 B (+0.26%)
MATEKF722 +120 B (+0.02%) ITCM_RAM: ±0 B (±0.00%)
RAM: +8 B (+0.01%)
TCM: ±0 B (±0.00%)
MATEKF765 ⚠️ +14684 B (+1.94%) DTCM_RAM: +2376 B (+8.30%)
SRAM1: +280 B (+0.22%)
MATEKH743 ⚠️ +14720 B (+1.84%) D2_RAM: ±0 B (±0.00%)
DTCM_RAM: +2336 B (+18.04%)
ITCM_RAM: +32 B (+0.21%)
RAM: +232 B (+0.16%)

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

@github-actions

github-actions Bot commented Oct 18, 2026 •

Copy link
Copy Markdown

Test firmware build ready — commit 5becae3

Download firmware for PR #12160

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.

With correlated north and east errors the 5 sigma clip judged spikes along the correlation as ordinary and clipped
ordinary updates across it. A unit test fuses a 6.7 sigma innovation of opposite signs.
… the compass

- Usable needs a GPS velocity in the last second: without one the heading is the gyro's alone.
- The compass is left out only while the estimate is usable, so a lost GPS gives it back at once.
- The pending GPS sample is fused with the delay it was stamped and stepped to with; a new delay applies from
  the next one, and a sample the filter is already well past (the delay grew) is dropped.
- gsfYawInit() zeroes the state at boot from imuInit(): AT32's startup clears .bss but not .fastram_bss.
The delta of an earlier attitude fit stayed in magBias[] and MSP2_INAV_MAG_LEARN next to the failure flags.
@MrScothh

Copy link
Copy Markdown
Contributor Author

Expected: #12159's GSF estimator (see there) plus the in-flight compass learning, about 3.3-3.4 KB of flash more than #12159 on F405, F765 and H743, and 64 to 136 B of RAM; MATEKF722 builds neither.

@MrScothh

Copy link
Copy Markdown
Contributor Author

Most of the deltas are #12159 (the GSF heading, +8.2 KB on MATEKF405) and #12142 (mag_learn), which this PR builds on. Its own share against #12159 is +3.0 to +3.3 KB of flash and +120 to +144 B of RAM, MATEKF405 at 80.34 % (table in the description). MATEKF722 builds neither the GSF heading nor this fit (512 KB of flash).

@MrScothh

Copy link
Copy Markdown
Contributor Author

Flight data from the four flights listed on #12142 and #12159 (fixed wing, mag_learn = ON, ahrs_gsf_yaw = ON).
This flew as the local branch this PR was made from, without dc034d1 and the two GSF fixes of 2026-10-18.

The attitude fit ran in every flight once the GSF heading was usable. On this compass, whose field followed the motor
current, it gave different answers from flight to flight:

flight ATTITUDE_FIT MISFIT FITS_DISAGREE yaw trim it was learning
1 0 s 336 s 12 s 0
2 239 s 0 s 0 s 0 to +5.0 deg
3 9 s 413 s 18 s 0 to +5.0 deg
4 159 s 6 s 1 s -1.4 to +4.1 deg

With MISFIT (the samples did not follow the attitude) it fell back to the sphere fit, which stopped at NOT_SPHERE. Where
the attitude fit held, NOT_SPHERE still applied, so nothing was saved and mag_yaw_trim stayed at 0 in the settings.
No DECLINATION_OFF and no CONFIRMED in any flight.

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.

1 participant