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 diff --git a/.github/workflows/pr-branch-suggestion.yml b/.github/workflows/pr-branch-suggestion.yml index 110deb98338..ffa6e52850e 100644 --- a/.github/workflows/pr-branch-suggestion.yml +++ b/.github/workflows/pr-branch-suggestion.yml @@ -20,11 +20,11 @@ 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. + 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.*`; 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 ``` 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) diff --git a/src/main/drivers/accgyro/accgyro_lsm6dxx.c b/src/main/drivers/accgyro/accgyro_lsm6dxx.c index d00df7442d7..18ff17969d9 100644 --- a/src/main/drivers/accgyro/accgyro_lsm6dxx.c +++ b/src/main/drivers/accgyro/accgyro_lsm6dxx.c @@ -217,7 +217,8 @@ 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); + // 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 // disable I3C interface 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 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 7ac93335d98..56f2e993447 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") @@ -43,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/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); +} 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); +}