From 975df0211e2fba8edd21b4bc672a0594192e4891 Mon Sep 17 00:00:00 2001 From: Ray Morris Date: Thu, 17 Sep 2026 10:54:15 -0500 Subject: [PATCH 01/10] docs: document configurator macOS signing and WASM SITL/PWA release flow Adds the macOS code-signing/notarization sequencing (release.yml tag-push trigger + nightly dry-run before tagging), the WASM SITL + PWA build steps for 10.x+, and updates the order of operations/checklist so the configurator is tag-first while firmware stays verify-before-tag. --- docs/development/release-create.md | 80 +++++++++++++++++++++--------- 1 file changed, 57 insertions(+), 23 deletions(-) diff --git a/docs/development/release-create.md b/docs/development/release-create.md index a75f8f6848a..6362d828eeb 100644 --- a/docs/development/release-create.md +++ b/docs/development/release-create.md @@ -14,15 +14,16 @@ Order of operations: 3. **Push to release branch to trigger nightly build** (merge the workflow PR, or push trivial commit) 4. Wait for nightly build to complete, verify ALL jobs passed 5. **Download firmware artifacts from inav-nightly** (includes SITL binaries needed for configurator) -6. Update SITL binaries in configurator repo, wait for CI, merge -7. Download configurator artifacts after SITL update merged -8. Verify all artifacts (automated checks) -9. **Manual testing on Linux and Windows** (required before tagging) -10. **Only then** create tags pointing to the verified commits +6. Update SITL binaries in configurator repo (and WASM SITL + PWA support for 10.x+), wait for CI, merge — this triggers the configurator nightly +7. **Dry-run the configurator signing via the nightly (no tag):** verify its macOS artifact is signed + notarized (`codesign --verify`, `xcrun stapler validate`) +8. Push the configurator version tag (triggers `release.yml`, signed + notarized macOS) — only after step 7's nightly dry-run passes +9. Download the signed configurator artifacts and verify (code signature, SITL, PWA/WASM) +10. **Manual testing on Linux and Windows** +11. **Only then** create the GitHub releases referencing the verified commits/tags If CI fails or any verification fails, fix the issue first. Do not tag broken commits. -**Why this matters:** If you tag first and then discover the build is broken, you have a tag pointing to a broken commit. By verifying artifacts first, you only tag commits that are proven to work. +**Why this matters:** If you tag first and then discover the build is broken, you have a tag pointing to a broken commit. By verifying artifacts first, you only tag commits that are proven to work. The configurator is the one partial exception: its signed macOS build requires a tag push, so its "verify first" step is the **nightly dry-run** (step 7) — confirm the nightly macOS artifact is signed before you push the tag. ## CRITICAL: CI Runs on PR Creation, Not Merge @@ -118,6 +119,7 @@ The Configurator firmware flasher uses a case-sensitive regex to parse firmware - [ ] No critical open issues blocking release - [ ] Version numbers updated in both repositories - [ ] SITL binaries updated in configurator +- [ ] WASM SITL built and added to configurator `js/web/WASM/` (10.x+) - [ ] **PG struct validation passed** (see [PG Validation](#pg-parameter-group-validation)) ### Documentation @@ -142,35 +144,41 @@ The Configurator firmware flasher uses a case-sensitive regex to parse firmware ├── Download firmware hex files from CI ├── Download SITL binaries from same CI run ├── Build Linux x64 SITL locally if needed (for glibc ≤2.35 compatibility) - └── This provides SITL binaries needed for configurator + ├── Build the WASM SITL firmware (10.x+) + └── This provides SITL binaries + WASM SITL needed for configurator -3. Update SITL in configurator - ├── Create PR with SITL binaries from step 2 +3. Update SITL (and WASM SITL) in configurator + ├── Create PR with SITL binaries from step 2 (and WASM artifacts + SITL-Webassembly.js import fix for 10.x+) ├── Wait for configurator CI to pass └── Merge SITL update PR -4. Download and verify configurator artifacts - ├── Download from CI run after SITL PR merged - ├── Verify macOS DMGs (no cross-platform contamination) +4. Build the PWA, dry-run signing, then push the tag and download configurator artifacts + ├── Build the PWA (yarn web:build) after the WASM SITL is in place (10.x+) + ├── Dry-run the signing via the nightly (no tag): codesign --verify + stapler validate on the nightly macOS artifact + ├── Push the version tag (v*.*.*) to trigger release.yml AFTER the nightly dry-run passes (signed + notarized macOS) + ├── Download from that release.yml run + ├── Verify macOS DMGs are signed and have no cross-platform contamination ├── Verify Windows SITL (cygwin1.dll present) ├── Verify Linux SITL (glibc <= 2.35 for Ubuntu 22.04 compatibility) └── Automated SITL verification (glibc check, binary runs) -5. Manual testing (REQUIRED before creating tags) +5. Manual testing ├── Test configurator + SITL on Linux ├── Test configurator + SITL on Windows ├── Test configurator + SITL on macOS (if available) └── Verify basic functionality works on each platform + (The configurator tag is already pushed by now — that's what produced the signed build. + The firmware tag is still verify-before-tag.) 6. Generate changelog ├── List PRs since last tag ├── Categorize changes └── Format release notes -7. Create tags and draft releases (ONLY after manual testing passed) - ├── Create tag + draft release for firmware (targeting verified commit) - ├── Create tag + draft release for configurator (targeting verified commit) - ├── Upload verified artifacts +7. Create draft releases (ONLY after manual testing passed) + ├── Create tag + draft release for firmware (targeting verified commit) — verify-before-tag + ├── Create the configurator release referencing the already-pushed tag + ├── Upload verified artifacts (including the PWA output for 10.x+) └── Add release notes 8. Review and publish @@ -288,9 +296,34 @@ If glibc > 2.35, the binary will fail on Ubuntu 22.04 with: The `extraResource` config in `forge.config.js` copies `resources/public/sitl` to `resources/sitl` in packaged builds. +## macOS Code Signing + PWA (10.x+) + +### macOS Signing Sequencing + +The release-ready macOS build is **code-signed and notarized** by `.github/workflows/release.yml`, which triggers on a **tag push** (`v*.*.*` or `*.*.*`). PR CI never signs; the nightly build signs+notarizes whenever the full secret set is present. Three rules: + +1. **Verify signing via the nightly before you tag.** A pushed tag is effectively immutable, so don't tag blind. After the SITL PR merges, the nightly runs automatically — check its macOS artifact with `codesign --verify` and `xcrun stapler validate`. Only push the tag once both pass (if either fails, the six signing secrets are missing and `release.yml` would fail anyway). +2. **Push the tag only AFTER the SITL is in place** — the native SITL binaries (and, for 10.x+, the WASM SITL) must already be committed and merged into the configurator repo. Otherwise the signed `.app`/`.dmg` ships stale SITL. +3. **Never modify the signed macOS artifacts after that run** — no re-zipping, re-bundling, or re-signing. If a fix is needed, commit it and push a new tag. + +The code enforces this: SITL pruning runs in the `afterCopyExtraResources` hook (before signing); files must not be deleted from the bundle after signing or notarization fails. See `inav-configurator/CLAUDE.md` ("macOS Code Signing & Notarization"). + +**If the nightly doesn't fire:** prefer adding `workflow_dispatch:` to `release.yml` and running `gh workflow run release.yml --ref ` (same fail-closed test, no tag). Last resort is a throwaway test tag (`10.0.0-sign-test`, which matches `*.*.*`), verified then deleted before pushing the real `10.0.0-RC1` tag. + +### WASM SITL + PWA Build + +For 10.x+, the browser-based PWA Configurator build bundles an in-browser WASM build of SITL, in addition to the native per-platform SITL binaries: + +1. Build the WASM SITL firmware from `feature/wasm-sitl-firmware` (`cmake .. -DTOOLCHAIN=wasm; make SITL`). +2. Rename the output to `inav__WASM.js`/`.wasm` and copy it into `inav-configurator/js/web/WASM/`, updating the hardcoded import in `js/web/SITL-Webassembly.js`. +3. Build the PWA: `yarn web:build` → `dist-web/`. +4. Upload the PWA output as an additional configurator asset (confirm the packaging format with maintainers). + +Both the WASM SITL and the native SITL must be in place before the release CI runs. + ## Tagging and Publishing -**IMPORTANT:** Tags should only be created AFTER testing artifacts and confirming the release is ready to publish. +**IMPORTANT:** Tags should only be created AFTER testing artifacts and confirming the release is ready to publish — **except the configurator**, which is tag-first: its signed macOS build is produced by pushing the tag (which triggers `release.yml`). Before pushing that tag, dry-run the signing via the nightly (no tag) and confirm its macOS artifact is signed + notarized. Firmware stays verify-before-tag. ### Check Latest Tags @@ -306,21 +339,22 @@ git fetch --tags git tag --sort=-v:refname | head -10 ``` -### Create and Push Tags (Final Step Before Publishing) +### Create and Push Tags -Only create tags after artifacts are tested and draft release is reviewed: +Firmware: verify first, then tag. Configurator: dry-run signing via the nightly first, then tag (to trigger the signed build), then create the release. ```bash -# Firmware +# Firmware (verify artifacts first) cd inav git pull git tag -a -m "INAV " git push origin -# Configurator +# Configurator — dry-run signing via the nightly FIRST (codesign --verify + stapler validate), +# then push the tag (this push triggers release.yml → signed + notarized macOS) cd inav-configurator git pull -git tag -a -m "INAV Configurator " +git tag -a -m "INAV Configurator " # or v git push origin ``` From a2464619fa2063ff301dd03698901e6bf0a0e672 Mon Sep 17 00:00:00 2001 From: Ray Morris Date: Sun, 20 Sep 2026 10:04:13 -0500 Subject: [PATCH 02/10] Update PR branch-suggestion workflow for the 10.x/11.x cycle maintenance-9.x -> maintenance-10.x (now the compatible/current choice) maintenance-10.x -> maintenance-11.x (now the breaking/next-major choice) maintenance-11.x was just created off maintenance-10.x's tip, per the release process's "create the next major's branch at first RC" rule. --- .github/workflows/pr-branch-suggestion.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/pr-branch-suggestion.yml b/.github/workflows/pr-branch-suggestion.yml index 110deb98338..06c65bfd2f8 100644 --- a/.github/workflows/pr-branch-suggestion.yml +++ b/.github/workflows/pr-branch-suggestion.yml @@ -20,9 +20,9 @@ jobs: You've targeted the \`master\` branch with this PR. Please consider if a version branch might be more appropriate: - - **\`maintenance-9.x\`** - If your change is backward-compatible and won't create compatibility issues between INAV firmware and Configurator 9.x versions. This will allow your PR to be included in the next 9.x release. + - **\`maintenance-10.x\`** - If your change is backward-compatible and won't create compatibility issues between INAV firmware and Configurator 10.x versions. This will allow your PR to be included in the next 10.x release. - - **\`maintenance-10.x\`** - If your change introduces compatibility requirements between firmware and configurator that would break 9.x compatibility. This is for PRs which will be included in INAV 10.x + - **\`maintenance-11.x\`** - If your change introduces compatibility requirements between firmware and configurator that would break 10.x compatibility. This is for PRs which will be included in INAV 11.x If \`master\` is the correct target for this change, no action is needed. From f043c0ba57f97c6e59185a9cafa2977d02be6cfe Mon Sep 17 00:00:00 2001 From: Ray Morris Date: Sun, 20 Sep 2026 10:11:59 -0500 Subject: [PATCH 03/10] Clarify the master-target note in the branch-suggestion comment Merging to master means the change never ends up in any release, rather than implying that's an acceptable default. --- .github/workflows/pr-branch-suggestion.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pr-branch-suggestion.yml b/.github/workflows/pr-branch-suggestion.yml index 06c65bfd2f8..ffa6e52850e 100644 --- a/.github/workflows/pr-branch-suggestion.yml +++ b/.github/workflows/pr-branch-suggestion.yml @@ -24,7 +24,7 @@ jobs: - **\`maintenance-11.x\`** - If your change introduces compatibility requirements between firmware and configurator that would break 10.x compatibility. This is for PRs which will be included in INAV 11.x - If \`master\` is the correct target for this change, no action is needed. + Merging to \`master\` would mean your change never ends up in any release. --- *This is an automated suggestion to help route contributions to the appropriate branch.*`; From 863c2fa8405e220d28cde2047bbf33ea732e41f8 Mon Sep 17 00:00:00 2001 From: Ray Morris Date: Sun, 20 Sep 2026 14:28:21 -0500 Subject: [PATCH 04/10] docs: update pr-branch-suggestion README entry for 10.x/11.x cycle --- .github/workflows/README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/README.md b/.github/workflows/README.md index fbd80adeda8..e315c472ff1 100644 --- a/.github/workflows/README.md +++ b/.github/workflows/README.md @@ -120,7 +120,8 @@ as `pr-test-builds.yml`** (secrets available even for fork PRs). #### `pr-branch-suggestion.yml` - Branch Targeting Suggestion **Triggers:** PRs targeting master branch -**Purpose:** Suggests using maintenance-9.x or maintenance-10.x instead +**Purpose:** Suggests using maintenance-10.x (backward-compatible) or +maintenance-11.x (breaking compatibility) instead #### `non-code-change.yaml` - Non-Code Change Detection **Triggers:** Pull requests From f3c75c6ee9eb688367f155e8e9a8556f44e34324 Mon Sep 17 00:00:00 2001 From: Ray Morris Date: Sun, 20 Sep 2026 22:26:03 -0500 Subject: [PATCH 05/10] add link to https://inavflight.github.io --- readme.md | 31 ++++--------------------------- 1 file changed, 4 insertions(+), 27 deletions(-) diff --git a/readme.md b/readme.md index b8dbadf3909..ee65067f36f 100644 --- a/readme.md +++ b/readme.md @@ -1,5 +1,8 @@ # INAV - navigation capable flight controller +# INAV Documentation +> For documentation on using INAV, see our new docs site at [inavflight.github.io](https://inavflight.github.io/) + # F411 PSA > INAV no longer accepts targets based on STM32 F411 MCU. @@ -12,11 +15,7 @@ # M7, M6 and older UBLOX GPS units PSA -> INAV 8.0 will mark those GPS as deprecated and INAV 9.0.0 will require UBLOX units with Protocol version 15.00 or newer. This means that you need a GPS unit based on UBLOX M8 or newer. - -> If you want to check the protocol version of your unit, it is displayed in INAV's 7.0.0+ status cli command. -> INAV 8.0.0 will warn you if your GPS is too old. -> ```GPS: HW Version: Unknown Proto: 0.00 Baud: 115200 (UBLOX Proto >= 15.0 required)``` +> INAV 8.0 marked those GPS as deprecated and INAV 9.0.0 and higher require UBLOX units with Protocol version 15.00 or newer. This means that you need a GPS unit based on UBLOX M8 or newer. > M8, M9 and M10 GPS are the most common units in use today, are readly available and have similar capabilities. @@ -24,28 +23,6 @@ ![INAV](http://static.rcgroups.net/forums/attachments/6/1/0/3/7/6/a9088858-102-inav.png) -# PosHold, Navigation and RTH without compass PSA - -Attention all drone pilots and enthusiasts, - -Are you ready to take your flights to new heights with INAV 7.1? We've got some important information to share with you. - -INAV 7.1 brings an exciting update to navigation capabilities. Now, you can soar through the skies, navigate waypoints, and even return to home without relying on a compass. Yes, you heard that right! But before you launch into the air, there's something crucial to consider. - -While INAV 7.1 may not require a compass for basic navigation functions, we strongly advise you to install one for optimal flight performance. Here's why: - -🛰️ Better Flight Precision: A compass provides essential data for accurate navigation, ensuring smoother and more precise flight paths. - -🌐 Enhanced Reliability: With a compass onboard, your drone can maintain stability even in challenging environments, low speeds and strong wind. - -🚀 Minimize Risks: Although INAV 7.1 can get you where you need to go without a compass, flying without one may result in a bumpier ride and increased risk of drift or inaccurate positioning. - -Remember, safety and efficiency are paramount when operating drones. By installing a compass, you're not just enhancing your flight experience, but also prioritizing safety for yourself and those around you. - -So, before you take off on your next adventure, make sure to equip your drone with a compass. It's the smart choice for smoother flights and better navigation. - -Fly safe, fly smart with INAV 7.1 and a compass by your side! - # INAV Community * [INAV Discord Server](https://discord.gg/peg2hhbYwN) From 50b16374d80c658469fc8ddcda600101657956f7 Mon Sep 17 00:00:00 2001 From: Ray Morris Date: Thu, 24 Sep 2026 13:07:46 -0500 Subject: [PATCH 06/10] Disable LSM6DSL/DSO/DS3 on-chip gyro high-pass filter The legacy LSM6DXX config path enabled the gyro's on-chip HPF at its slowest (16mHz) cutoff. That filter has a ~10s RC time constant, so its output keeps settling for ~20-30s after every power-on - well past INAV's ~2s startup gyro calibration window. The result is a real step in the raw gyro reading after calibration has already locked in a zero, which the AHRS reads as several seconds of attitude drift after boot. INAV already zeroes gyro bias in software, making the on-chip HPF redundant, and no other accgyro driver in the tree enables one. --- src/main/drivers/accgyro/accgyro_lsm6dxx.c | 8 +- src/test/unit/CMakeLists.txt | 4 + src/test/unit/accgyro_lsm6dxx_unittest.cc | 189 +++++++++++++++++++++ 3 files changed, 200 insertions(+), 1 deletion(-) create mode 100644 src/test/unit/accgyro_lsm6dxx_unittest.cc diff --git a/src/main/drivers/accgyro/accgyro_lsm6dxx.c b/src/main/drivers/accgyro/accgyro_lsm6dxx.c index d00df7442d7..6e0c7528383 100644 --- a/src/main/drivers/accgyro/accgyro_lsm6dxx.c +++ b/src/main/drivers/accgyro/accgyro_lsm6dxx.c @@ -217,7 +217,13 @@ static void lsm6dxxConfig(gyroDev_t *gyro) lsm6dxxWriteRegisterBits(dev, LSM6DXX_REG_CTRL6_C, (lsm6dID == LSM6DSO_CHIP_ID? LSM6DXX_MASK_CTRL6_C:LSM6DSL_MASK_CTRL6_C), (LSM6DXX_VAL_CTRL6_C_XL_HM_MODE | getLsmDlpfBandwidth(gyro)), 1); // Configure control register 7 - lsm6dxxWriteRegisterBits(dev, LSM6DXX_REG_CTRL7_G, LSM6DXX_MASK_CTRL7_G, (LSM6DXX_VAL_CTRL7_G_HP_EN_G | LSM6DXX_VAL_CTRL7_G_HPM_G_16), 1); + // Leave the gyro's on-chip high-pass filter disabled: at its slowest (16mHz) cutoff it + // has a ~10s RC time constant, so the chip's own output keeps settling for ~20-30s after + // every power-on/reset - well past INAV's ~2s startup gyro calibration window. That + // produces a real post-calibration step in the raw gyro reading as the on-chip filter + // catches up, which the AHRS then reads as attitude drift. INAV already removes gyro bias + // in software (see sensors/gyro.c calibration), making the on-chip HPF redundant here. + lsm6dxxWriteRegisterBits(dev, LSM6DXX_REG_CTRL7_G, LSM6DXX_MASK_CTRL7_G, 0, 1); // Configure control register 9 // disable I3C interface diff --git a/src/test/unit/CMakeLists.txt b/src/test/unit/CMakeLists.txt index 7ac93335d98..061efc2606d 100644 --- a/src/test/unit/CMakeLists.txt +++ b/src/test/unit/CMakeLists.txt @@ -4,6 +4,10 @@ set(MAIN_DIR "${CMAKE_CURRENT_SOURCE_DIR}/../../../src/main") # Keep these alphabetically sorted by test name +set_property(SOURCE accgyro_lsm6dxx_unittest.cc PROPERTY depends + "drivers/accgyro/accgyro_lsm6dxx.c") +set_property(SOURCE accgyro_lsm6dxx_unittest.cc PROPERTY definitions USE_IMU_LSM6DXX) + set_property(SOURCE alignsensor_unittest.cc PROPERTY depends "common/maths.c" "sensors/boardalignment.c") diff --git a/src/test/unit/accgyro_lsm6dxx_unittest.cc b/src/test/unit/accgyro_lsm6dxx_unittest.cc new file mode 100644 index 00000000000..377aadfced6 --- /dev/null +++ b/src/test/unit/accgyro_lsm6dxx_unittest.cc @@ -0,0 +1,189 @@ +/* + * This file is part of INAV. + * + * INAV is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * INAV is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with INAV. If not, see . + */ + +// Regression test for the LSM6DSL/LSM6DSO/LSM6DS3 attitude-drift bug reported on Discord +// (custom STM32F405 + LSM6DSL board, INAV 9.1.0): roll/pitch drifted for ~10-20s after +// boot because lsm6dxxConfig() enabled the chip's on-chip gyro high-pass filter at its +// slowest (16mHz) cutoff, which has a ~10s RC time constant and keeps settling the raw +// gyro output for ~20-30s after every power-on/reset - well past INAV's ~2s startup +// gyro calibration window. This test drives the real, unmodified production entry point +// (lsm6dGyroDetect() -> gyro->initFn() -> lsm6dxxConfig()) against a fake register-map +// "chip" and asserts the byte actually written to CTRL7_G does not enable the on-chip HPF. + +#include +#include + +extern "C" { +#include "platform.h" + +#include "common/utils.h" +#include "drivers/time.h" +#include "drivers/resource.h" +#include "drivers/bus.h" +#include "drivers/accgyro/accgyro.h" +#include "drivers/accgyro/accgyro_mpu.h" +#include "drivers/accgyro/accgyro_lsm6dxx.h" +} + +#include "gtest/gtest.h" + +extern "C" { + +bool lsm6dGyroDetect(gyroDev_t *gyro); + +// --- Fake "chip": a plain byte-addressed register map, backing busRead()/busWrite() --- +static uint8_t fakeRegs[256]; +static busDevice_t fakeBusDevice; +static uint32_t fakeScratchpad[(BUS_SCRATCHPAD_MEMORY_SIZE + 3) / 4]; + +busDevice_t * busDeviceInit(busType_e bus, devHardwareType_e hw, uint8_t tag, resourceOwner_e owner) +{ + UNUSED(bus); UNUSED(hw); UNUSED(tag); UNUSED(owner); + return &fakeBusDevice; +} + +busDevice_t * busDeviceOpen(busType_e bus, devHardwareType_e hw, uint8_t tag) +{ + UNUSED(bus); UNUSED(hw); UNUSED(tag); + return &fakeBusDevice; +} + +void busDeviceDeInit(busDevice_t *dev) +{ + UNUSED(dev); +} + +void * busDeviceGetScratchpadMemory(const busDevice_t *dev) +{ + UNUSED(dev); + return fakeScratchpad; +} + +void busSetSpeed(const busDevice_t *dev, busSpeed_e speed) +{ + UNUSED(dev); UNUSED(speed); +} + +bool busWrite(const busDevice_t *dev, uint8_t reg, uint8_t data) +{ + UNUSED(dev); + fakeRegs[reg] = data; + return true; +} + +bool busRead(const busDevice_t *dev, uint8_t reg, uint8_t *data) +{ + UNUSED(dev); + *data = fakeRegs[reg]; + return true; +} + +bool busWriteBuf(const busDevice_t *dev, uint8_t reg, const uint8_t *data, uint8_t length) +{ + UNUSED(dev); + for (uint8_t i = 0; i < length; i++) { + fakeRegs[reg + i] = data[i]; + } + return true; +} + +bool busReadBuf(const busDevice_t *dev, uint8_t reg, uint8_t *data, uint8_t length) +{ + UNUSED(dev); + for (uint8_t i = 0; i < length; i++) { + data[i] = fakeRegs[reg + i]; + } + return true; +} + +void delay(timeMs_t ms) +{ + UNUSED(ms); +} + +void delayMicroseconds(timeUs_t us) +{ + UNUSED(us); +} + +// Real mpuChooseGyroConfig() only needs to pick a table row for boards sharing the MPU +// family; it's irrelevant to the CTRL7_G behavior under test, so return a fixed, valid +// config regardless of input. +const gyroFilterAndRateConfig_t * mpuChooseGyroConfig(uint8_t desiredLpf, uint16_t desiredRateHz) +{ + UNUSED(desiredLpf); UNUSED(desiredRateHz); + static const gyroFilterAndRateConfig_t fakeConfig = { 0, 1000, { 0, 0 } }; + return &fakeConfig; +} + +bool gyroCheckDataReady(gyroDev_t *gyro) +{ + UNUSED(gyro); + return true; +} + +} // extern "C" + +class LSM6DxxConfigTest : public ::testing::Test { +protected: + void SetUp() override + { + memset(fakeRegs, 0, sizeof(fakeRegs)); + memset(&fakeBusDevice, 0, sizeof(fakeBusDevice)); + memset(&gyro, 0, sizeof(gyro)); + gyro.lpf = GYRO_HARDWARE_LPF_NORMAL; + gyro.requestedSampleIntervalUs = 1000; + } + + gyroDev_t gyro; +}; + +// Drives the real production path for the LSM6DSL chip (the exact chip reported in the +// bug: attitude drift over ~10-20s caused by raw gyro X stepping after calibration closed). +TEST_F(LSM6DxxConfigTest, LSM6DSLDoesNotEnableOnChipGyroHighPassFilter) +{ + fakeRegs[LSM6DXX_REG_WHO_AM_I] = 0x6A; // LSM6DSL_CHIP_ID + + ASSERT_TRUE(lsm6dGyroDetect(&gyro)); + ASSERT_NE(gyro.initFn, nullptr); + + gyro.initFn(&gyro); // -> lsm6dxxSpiGyroInit() -> lsm6dxxConfig() + + const uint8_t ctrl7g = fakeRegs[LSM6DXX_REG_CTRL7_G]; + EXPECT_EQ(ctrl7g & LSM6DXX_MASK_CTRL7_G, 0) + << "CTRL7_G HPF bits were written as 0x" << std::hex << (int)(ctrl7g & LSM6DXX_MASK_CTRL7_G) + << " - the on-chip gyro high-pass filter must stay disabled. Its slowest cutoff has a " + << "~10s RC time constant, so re-enabling it reproduces the reported ~10-20s attitude " + << "drift as the chip's own filter output keeps settling well past INAV's startup " + << "gyro calibration window."; + EXPECT_EQ(ctrl7g & LSM6DXX_VAL_CTRL7_G_HP_EN_G, 0) << "on-chip gyro HPF enable bit must not be set"; +} + +// Same assertion for LSM6DSO, which shares lsm6dxxConfig()'s legacy (non-"Gen V") path. +TEST_F(LSM6DxxConfigTest, LSM6DSODoesNotEnableOnChipGyroHighPassFilter) +{ + fakeRegs[LSM6DXX_REG_WHO_AM_I] = 0x6C; // LSM6DSO_CHIP_ID + + ASSERT_TRUE(lsm6dGyroDetect(&gyro)); + ASSERT_NE(gyro.initFn, nullptr); + + gyro.initFn(&gyro); + + const uint8_t ctrl7g = fakeRegs[LSM6DXX_REG_CTRL7_G]; + EXPECT_EQ(ctrl7g & LSM6DXX_MASK_CTRL7_G, 0); + EXPECT_EQ(ctrl7g & LSM6DXX_VAL_CTRL7_G_HP_EN_G, 0); +} From 3e689b514eec05ef73ec30c0d312ae6c5f9863f8 Mon Sep 17 00:00:00 2001 From: Ray Morris Date: Thu, 24 Sep 2026 14:36:57 -0500 Subject: [PATCH 07/10] Shorten CTRL7_G comment --- src/main/drivers/accgyro/accgyro_lsm6dxx.c | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src/main/drivers/accgyro/accgyro_lsm6dxx.c b/src/main/drivers/accgyro/accgyro_lsm6dxx.c index 6e0c7528383..c7c6e2f83c0 100644 --- a/src/main/drivers/accgyro/accgyro_lsm6dxx.c +++ b/src/main/drivers/accgyro/accgyro_lsm6dxx.c @@ -217,12 +217,7 @@ static void lsm6dxxConfig(gyroDev_t *gyro) lsm6dxxWriteRegisterBits(dev, LSM6DXX_REG_CTRL6_C, (lsm6dID == LSM6DSO_CHIP_ID? LSM6DXX_MASK_CTRL6_C:LSM6DSL_MASK_CTRL6_C), (LSM6DXX_VAL_CTRL6_C_XL_HM_MODE | getLsmDlpfBandwidth(gyro)), 1); // Configure control register 7 - // Leave the gyro's on-chip high-pass filter disabled: at its slowest (16mHz) cutoff it - // has a ~10s RC time constant, so the chip's own output keeps settling for ~20-30s after - // every power-on/reset - well past INAV's ~2s startup gyro calibration window. That - // produces a real post-calibration step in the raw gyro reading as the on-chip filter - // catches up, which the AHRS then reads as attitude drift. INAV already removes gyro bias - // in software (see sensors/gyro.c calibration), making the on-chip HPF redundant here. + // On-chip HPF disabled: slow settling causes drift lsm6dxxWriteRegisterBits(dev, LSM6DXX_REG_CTRL7_G, LSM6DXX_MASK_CTRL7_G, 0, 1); // Configure control register 9 From e08ddd6b7f7ce76c678945dc9e6bc2a6d178c17b Mon Sep 17 00:00:00 2001 From: Ray Morris Date: Thu, 24 Sep 2026 14:38:14 -0500 Subject: [PATCH 08/10] Reword CTRL7_G comment --- src/main/drivers/accgyro/accgyro_lsm6dxx.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/drivers/accgyro/accgyro_lsm6dxx.c b/src/main/drivers/accgyro/accgyro_lsm6dxx.c index c7c6e2f83c0..18ff17969d9 100644 --- a/src/main/drivers/accgyro/accgyro_lsm6dxx.c +++ b/src/main/drivers/accgyro/accgyro_lsm6dxx.c @@ -217,7 +217,7 @@ static void lsm6dxxConfig(gyroDev_t *gyro) lsm6dxxWriteRegisterBits(dev, LSM6DXX_REG_CTRL6_C, (lsm6dID == LSM6DSO_CHIP_ID? LSM6DXX_MASK_CTRL6_C:LSM6DSL_MASK_CTRL6_C), (LSM6DXX_VAL_CTRL6_C_XL_HM_MODE | getLsmDlpfBandwidth(gyro)), 1); // Configure control register 7 - // On-chip HPF disabled: slow settling causes drift + // Disable on-chip filter to avoid distorting runtime calibration lsm6dxxWriteRegisterBits(dev, LSM6DXX_REG_CTRL7_G, LSM6DXX_MASK_CTRL7_G, 0, 1); // Configure control register 9 From 99e3918277838176d17004d8b9d8ad9b015d8a5a Mon Sep 17 00:00:00 2001 From: b14ckyy <33039058+b14ckyy@users.noreply.github.com> Date: Sun, 27 Sep 2026 00:51:58 +0200 Subject: [PATCH 09/10] GPS: keep the heartbeat across the driver solution copy gpsProcessNewDriverData() copies gpsSolDRV over gpsSol including flags.gpsHeartbeat, which is only ever toggled on gpsSol afterwards. With UBLOX and HITL the copy and the toggle run back-to-back, so the flag has been a constant since 8.0.0 and the IMU, which waits for a change, never refreshed its GPS-based inertial compensation: VELNED was dead on all platforms, and the fixed-wing TURNRATE GPS speed froze once it was gated on the same flag in 9.1.0. Unit test links the real gps.c and checks that the flag toggles once per solution and survives a driver copy on its own. Fixes #12029 Co-Authored-By: Claude Fable 5.1 (cherry picked from commit 2d5fd063db4b4edcbfefd92f54ff9178eec71c3e) --- src/main/io/gps.c | 3 + src/test/unit/CMakeLists.txt | 1 + src/test/unit/gps_heartbeat_unittest.cc | 151 ++++++++++++++++++++++++ 3 files changed, 155 insertions(+) create mode 100644 src/test/unit/gps_heartbeat_unittest.cc diff --git a/src/main/io/gps.c b/src/main/io/gps.c index 41901e1e209..337daf3e3e7 100755 --- a/src/main/io/gps.c +++ b/src/main/io/gps.c @@ -343,7 +343,10 @@ void updateEstimatedGPSFix(void) void gpsProcessNewDriverData(void) { + // gpsHeartbeat is toggled on gpsSol only, so the driver copy must not reset it + const bool gpsHeartbeat = gpsSol.flags.gpsHeartbeat; gpsSol = gpsSolDRV; + gpsSol.flags.gpsHeartbeat = gpsHeartbeat; #ifdef USE_GPS_FIX_ESTIMATION processDisableGPSFix(); diff --git a/src/test/unit/CMakeLists.txt b/src/test/unit/CMakeLists.txt index 061efc2606d..56f2e993447 100644 --- a/src/test/unit/CMakeLists.txt +++ b/src/test/unit/CMakeLists.txt @@ -47,6 +47,7 @@ set_property(SOURCE gps_ublox_unittest.cc PROPERTY definitions GPS_UBLOX_UNIT_TE set_property(SOURCE gps_null_port_unittest.cc PROPERTY depends "io/gps.c") set_property(SOURCE gps_null_port_unittest.cc PROPERTY definitions GPS_NULL_PORT_UNIT_TEST USE_GPS_PROTO_UBLOX) +set_property(SOURCE gps_heartbeat_unittest.cc PROPERTY depends "io/gps.c") set_property(SOURCE gimbal_serial_unittest.cc PROPERTY depends "io/gimbal_serial.c" "drivers/gimbal_common.c" "common/maths.c" "drivers/headtracker_common.c") set_property(SOURCE gimbal_serial_unittest.cc PROPERTY definitions USE_SERIAL_GIMBAL GIMBAL_UNIT_TEST USE_HEADTRACKER) diff --git a/src/test/unit/gps_heartbeat_unittest.cc b/src/test/unit/gps_heartbeat_unittest.cc new file mode 100644 index 00000000000..75505ec1ff0 --- /dev/null +++ b/src/test/unit/gps_heartbeat_unittest.cc @@ -0,0 +1,151 @@ +/* + * This file is part of INAV. + * + * INAV is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * INAV is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with INAV. If not, see . + */ + +#include "gtest/gtest.h" +#include "unittest_macros.h" + +#include +#include +#include + +extern "C" { + +#include "common/time.h" + +#include "config/parameter_group.h" +#include "config/parameter_group_ids.h" + +#include "drivers/serial.h" + +#include "fc/config.h" +#include "fc/runtime_config.h" + +#include "io/gps.h" +#include "io/gps_private.h" + +extern gpsReceiverData_t gpsState; +extern gpsSolutionData_t gpsSolDRV; +extern gpsSolutionData_t gpsSol; +extern gpsConfig_t gpsConfig_System; + +const uint32_t baudRates[] = { 0, 1200, 2400, 4800, 9600, 19200, 38400, + 57600, 115200, 230400, 250000, 460800, 921600, + 1000000, 1500000, 2000000, 2470000 }; + +serialConfig_t serialConfig_System; + +uint32_t millis(void) { return 5000; } +uint32_t micros(void) { return 5000000; } + +static uint32_t fakeFeatureMask = 0; + +bool feature(uint32_t mask) { return (fakeFeatureMask & mask) != 0; } +void featureSet(uint32_t mask) { fakeFeatureMask |= mask; } +void featureClear(uint32_t mask) { fakeFeatureMask &= ~mask; } + +static uint32_t fakeSensorMask = 0; + +bool sensors(uint32_t mask) { return (fakeSensorMask & mask) != 0; } +void sensorsSet(uint32_t mask) { fakeSensorMask |= mask; } +void sensorsClear(uint32_t mask) { fakeSensorMask &= ~mask; } + +uint32_t serialRxBytesWaiting(const serialPort_t *instance) { UNUSED(instance); return 0; } +uint32_t serialTxBytesFree(const serialPort_t *instance) { UNUSED(instance); return 256; } +bool isSerialTransmitBufferEmpty(const serialPort_t *instance) { UNUSED(instance); return true; } +uint8_t serialRead(serialPort_t *instance) { UNUSED(instance); return 0; } +void serialWrite(serialPort_t *instance, uint8_t ch) { UNUSED(instance); UNUSED(ch); } +void serialWriteBuf(serialPort_t *instance, const uint8_t *data, int count) { UNUSED(instance); UNUSED(data); UNUSED(count); } +void serialPrint(serialPort_t *instance, const char *str) { UNUSED(instance); UNUSED(str); } +void serialSetMode(serialPort_t *instance, portMode_t mode) { UNUSED(instance); UNUSED(mode); } +void serialSetBaudRate(serialPort_t *instance, uint32_t baudRate) { UNUSED(instance); UNUSED(baudRate); } + +serialPort_t *openSerialPort(serialPortIdentifier_e identifier, serialPortFunction_e function, + serialReceiveCallbackPtr rxCallback, void *rxCallbackData, + uint32_t baudRate, portMode_t mode, portOptions_t options) +{ + UNUSED(identifier); UNUSED(function); UNUSED(rxCallback); + UNUSED(rxCallbackData); UNUSED(baudRate); UNUSED(mode); UNUSED(options); + return NULL; +} + +void closeSerialPort(serialPort_t *serialPort) { UNUSED(serialPort); } +serialPortConfig_t *findSerialPortConfig(serialPortFunction_e function) { UNUSED(function); return NULL; } +void waitForSerialPortToFinishTransmitting(serialPort_t *serialPort) { UNUSED(serialPort); } + +uint32_t stateFlags = 0; +uint32_t armingFlags = 0; + +bool isMPUSoftReset(void) { return false; } + +void LED0_ON(void) {} +void LED0_OFF(void) {} +void LED1_ON(void) {} +void LED1_OFF(void) {} +void LED1_TOGGLE(void) {} + +void onNewGPSData(void) {} + +bool rtcHasTime(void) { return false; } +bool rtcSetDateTime(dateTime_t *dt) { UNUSED(dt); return true; } + +bool baroIsHealthy(void) { return false; } +bool pitotIsHealthy(void) { return false; } + +void gpsRestartUBLOX(void) {} +void gpsHandleUBLOX(void) {} + +} // extern "C" + +class GpsHeartbeatTest : public ::testing::Test { +protected: + void SetUp() override { + memset(&gpsSol, 0, sizeof(gpsSol)); + memset(&gpsSolDRV, 0, sizeof(gpsSolDRV)); + memset(&gpsConfig_System, 0, sizeof(gpsConfig_System)); + gpsConfig_System.provider = GPS_UBLOX; + gpsState.gpsConfig = &gpsConfig_System; + gpsState.baseTimeoutMs = 1000; + stateFlags = 0; + armingFlags = 0; + } + + void TearDown() override { + fakeFeatureMask = 0; + fakeSensorMask = 0; + } +}; + +TEST_F(GpsHeartbeatTest, HeartbeatTogglesOncePerSolution) +{ + bool prev = gpsSol.flags.gpsHeartbeat; + for (int i = 0; i < 4; i++) { + gpsProcessNewDriverData(); + gpsProcessNewSolutionData(false); + EXPECT_NE(prev, gpsSol.flags.gpsHeartbeat); + prev = gpsSol.flags.gpsHeartbeat; + } +} + +// MSP/CRSF/DroneCAN copy in another task than the toggle +TEST_F(GpsHeartbeatTest, DriverCopyKeepsHeartbeat) +{ + gpsProcessNewDriverData(); + gpsProcessNewSolutionData(false); + const bool before = gpsSol.flags.gpsHeartbeat; + gpsProcessNewDriverData(); + EXPECT_EQ(before, gpsSol.flags.gpsHeartbeat); +} From 1bf1f57eaffc6a2da03d5b050c5f31c8b6a25843 Mon Sep 17 00:00:00 2001 From: b14ckyy <33039058+b14ckyy@users.noreply.github.com> Date: Sun, 27 Sep 2026 00:51:58 +0200 Subject: [PATCH 10/10] IMU: filter the TURNRATE GPS speed every loop again Since the GPS speed is only recomputed on a new GPS solution, the PT1 filter was also only applied then, but with the IMU loop dT. That stretched its 53 ms time constant to seconds and turned the longitudinal term into one spike per GPS frame. Only the square root stays gated. Co-Authored-By: Claude Fable 5.1 (cherry picked from commit ec0694758a3785befe76b44a453d7963f7346744) --- src/main/flight/imu.c | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/main/flight/imu.c b/src/main/flight/imu.c index e5d2e3be75f..e56eaec5bc5 100644 --- a/src/main/flight/imu.c +++ b/src/main/flight/imu.c @@ -742,13 +742,13 @@ static void imuCalculateTurnRateacceleration(fpVector3_t *vEstcentrifugalAccelBF // second choice is gps static bool lastGPSHeartbeat; static pt1Filter_t GPS3DspeedFilter; - static float GPS3DspeedFiltered = 0.0f; + static float GPS3Dspeed = 0.0f; if (gpsSol.flags.gpsHeartbeat != lastGPSHeartbeat) { lastGPSHeartbeat = gpsSol.flags.gpsHeartbeat; - float GPS3Dspeed = calc_length_pythagorean_3D(gpsSol.velNED[X], gpsSol.velNED[Y], gpsSol.velNED[Z]); - GPS3DspeedFiltered = pt1FilterApply4(&GPS3DspeedFilter, GPS3Dspeed, IMU_ROTATION_LPF, dT); + GPS3Dspeed = calc_length_pythagorean_3D(gpsSol.velNED[X], gpsSol.velNED[Y], gpsSol.velNED[Z]); } - currentspeed = GPS3DspeedFiltered; + // Filter every loop: dT is the IMU loop time, not the GPS interval + currentspeed = pt1FilterApply4(&GPS3DspeedFilter, GPS3Dspeed, IMU_ROTATION_LPF, dT); *acc_ignore_slope_multipiler = 4.0f; } else