.pr_agent_auto_best_practices - iNavFlight/inav GitHub Wiki

Pattern 1: Derive MAVLink SITL test executable paths from the project version or build output instead of hardcoding a versioned filename in test launchers and configurations.

Example code before:

simulator: build/inav_1.2.0_SITL

Example code after:

# Generate the test configuration using the configured project version.
simulator: build/inav_${PROJECT_VERSION}_SITL
Relevant past accepted suggestions:
Suggestion 1: [correctness] MAVLink SITL tests cannot start
MAVLink SITL tests cannot start The versioned SITL artifact changes from `inav_9.1.1_SITL` to `inav_10.0.0_SITL`, while the MAVLink mission, multi-port, and sanity test configurations retain the old path. After a clean 10.0.0 build, the mission runner exits on its missing-file check and the Python tests attempt to launch a nonexistent executable.

Issue description

The version bump changes the generated SITL executable from inav_9.1.1_SITL to inav_10.0.0_SITL, but checked-in MAVLink test launchers still use the old path, so those tests fail after a clean build.

Fix Focus Areas

  • src/test/mavlink/missions/run_mavlink_mission_tester.sh[10-10]
  • src/test/mavlink/ports/test_config_multiport4_sweep_rc460800_tele115200.yaml[2-2]
  • src/test/mavlink/sanity/mavlink_sitl_sanity.yaml[2-2]

Recommended Fix

Replace the hardcoded inav_9.1.1_SITL references with a version-independent path or update them to the generated inav_10.0.0_SITL artifact. Prefer deriving the path from the configured project version so future version bumps do not leave these test entry points stale, then run the affected MAVLink SITL tests.


Suggestion 2: [correctness] Three tests cannot start the simulator
Three tests cannot start the simulator `project(INAV VERSION 9.1.1)` changes `FIRMWARE_VERSION`, which `cmake/sitl.cmake` embeds in the executable name, while three runnable test configurations still request `inav_9.1.0_SITL`. After a clean build, the mission launcher fails its file check and the sanity and multi-port runners pass a nonexistent path to their process launchers.

Issue description

The project version bump renames the generated simulator executable, but operational test launchers and configurations still point at the previous versioned filename.

Fix Focus Areas

  • CMakeLists.txt[56-76]
  • cmake/sitl.cmake[124-124]
  • src/test/mavlink/missions/run_mavlink_mission_tester.sh[10-10]
  • src/test/mavlink/missions/run_mavlink_mission_tester.sh[146-162]
  • src/test/mavlink/sanity/mavlink_sitl_sanity.yaml[2-2]
  • src/test/mavlink/ports/test_config_multiport4_sweep_rc460800_tele115200.yaml[2-2]

Recommended Fix

Update all operational simulator paths to the 9.1.1 executable and preferably derive or discover the filename from the CMake project version so future version bumps do not break these test runners.


Pattern 2: Set SRXL2 link timeouts and telemetry freshness windows relative to the selected telemetry request interval, with allowance for response latency or missed requests. Do not expire readings on unrelated busy-loop callbacks.

Example code before:

linkTimeoutMs = 500;
telemetryFreshMs = 1000;

Example code after:

linkTimeoutMs = requestIntervalMs * 3 + responseMarginMs;
telemetryFreshMs = requestIntervalMs * 2 + responseMarginMs;
Relevant past accepted suggestions:
Suggestion 1: [correctness] Slow telemetry drops the ESC link
Slow telemetry drops the ESC link `srxl2MotorSetTelemetryRate()` maps the 0.2 Hz setting to one request every 25 control frames, exactly 500 ms, while `SRXL2_RUNNING` declares the link dead after 500 ms without receiving a frame. Before the first reply to that request can be drained, the same processing pass meets the `>= 500` timeout, returns the port to low baud, and leaves the powered ESC behind at the negotiated baud.

Issue description

The 0.2 Hz telemetry mode schedules its next possible reply no earlier than the link timeout, so the driver disconnects before receiving it.

Fix Focus Areas

  • src/main/io/motor_srxl2.c[184-188]
  • src/main/io/motor_srxl2.c[811-824]
  • src/main/io/motor_srxl2.c[1011-1029]

Recommended Fix

Base the link timeout on the selected request interval with allowance for multiple unanswered requests, or use a separate sufficiently frequent link-health exchange independent of the telemetry delivery rate.


Suggestion 2: [correctness] Slow telemetry vanishes between samples
Slow telemetry vanishes between samples `srxl2MotorGetTelemetry()` rejects every reading after a fixed one-second age, and `escSensorUpdate()` then increments its age on every busy-loop callback rather than on expected telemetry opportunities. At the offered 0.5 Hz rate, readings disappear for roughly half of each interval, while consumers using voltage, current, temperature, and combined ESC data repeatedly see no valid sensor.

Issue description

The fixed one-second freshness window is shorter than supported telemetry intervals and invalidation is advanced at busy-loop frequency.

Fix Focus Areas

  • src/main/io/motor_srxl2.c[187-188]
  • src/main/io/motor_srxl2.c[1094-1103]
  • src/main/sensors/esc_sensor.c[273-303]

Recommended Fix

Derive freshness from the configured delivery interval with jitter and missed-response allowance, and update dataAge only when an expected sample opportunity is missed rather than on every realtime callback.


Pattern 3: Clear remembered fixed-wing launch acceleration when detection is aborted or its recency window expires; accept it only when its computed age is nonnegative and within that window.

Example code before:

if (cmpTimeUs(now, accelerationTimeUs) < launchWindowUs)
    accelerationRecent = true;

Example code after:

if (throttleLow)
    accelerationTimeUs = 0;
if (accelerationTimeUs) {
    int32_t age = cmpTimeUs(now, accelerationTimeUs);
    if (age < 0 || age > launchWindowUs)
        accelerationTimeUs = 0;
}
accelerationRecent = accelerationTimeUs != 0;
Relevant past accepted suggestions:
Suggestion 1: [correctness] Old jolts become valid after 36 minutes
Old jolts become valid after 36 minutes `wasForwardAccelerationHigh` treats every negative result from `cmpTimeUs` as less than the one-second limit, while `forwardAccelHighTimeUs` is retained after its intended window expires. Once a launch wait continues roughly 35.8 minutes beyond an earlier acceleration event, signed-delta overflow makes that stale event qualify again and GPS speed can complete detection.

Issue description

The remembered acceleration timestamp is never expired, and the signed 32-bit comparison becomes negative after about 35 minutes, causing an old event to satisfy the one-second recency check.

Fix Focus Areas

  • src/main/navigation/navigation_fw_launch.c[426-435]
  • src/main/common/time.h[27-30]
  • src/main/common/time.h[63-64]

Recommended Fix

Explicitly clear the remembered timestamp once its one-second window expires and require the computed delta to be nonnegative before accepting it as recent. Keep expiry evaluation running on every detection cycle so the timestamp cannot survive until signed-delta overflow.


Suggestion 2: [correctness] An aborted attempt can still launch
An aborted attempt can still launch `fwLaunchState_FW_LAUNCH_STATE_WAIT_DETECTION` records `forwardAccelHighTimeUs` but does not clear it when low throttle returns the controller to `WAIT_THROTTLE`. When the pilot raises throttle again within one second, the previous attempt's acceleration can combine with qualifying GPS speed and enter detection without a new throw.

Issue description

A throttle-low transition aborts detection and returns the controller to WAIT_THROTTLE, but the newly added acceleration timestamp survives that transition and remains usable by the next attempt.

Fix Focus Areas

  • src/main/navigation/navigation_fw_launch.c[416-431]

Recommended Fix

Clear forwardAccelHighTimeUs when low throttle exits WAIT_DETECTION, ensuring every newly armed detection attempt requires its own acceleration event.


Pattern 4: Use voltage-specific battery thresholds for voltage warnings and voltage-display blinking, rather than the aggregate battery state, which may represent remaining capacity. Guard voltage-only APIs on builds without voltage sensing.

Example code before:

if (getBatteryState() >= BATTERY_WARNING)
    blinkVoltage();

Example code after:

#ifdef USE_ADC
if (checkBatteryVoltageState() >= BATTERY_WARNING)
    blinkVoltage();
#endif
Relevant past accepted suggestions:
Suggestion 1: [correctness] Capacity triggers voltage warning
Capacity triggers voltage warning When capacity thresholds are enabled, `getBatteryState()` returns capacity-derived state, so the multifunction voltage warning duplicates the capacity warning and can report `VBATT LOW/LAND` despite healthy voltage. It can also omit a genuine low-voltage warning while remaining capacity is healthy.

Issue description

Preserve voltage-specific warning semantics while avoiding references to checkBatteryVoltageState() on builds without USE_ADC. The aggregate getBatteryState() must not drive the explicitly voltage-based warning because it becomes capacity-derived when capacity thresholds are active.

Issue Context

batteryUpdate() selects checkBatteryCapacityState() when batteryUseCapacityThresholds is true. The following capacity-warning block already reports that aggregate state separately, so using it for the voltage block produces incorrect and duplicate warnings.

Fix Focus Areas

  • src/main/fc/multifunction.c[268-280]
  • src/main/sensors/battery.c[397-435]
  • src/main/sensors/battery.c[498-510]

Suggestion 2: [correctness] Capacity controls voltage blinking
Capacity controls voltage blinking When capacity thresholds are enabled, `getBatteryState()` makes the displayed voltage blink according to remaining capacity rather than the voltage thresholds. Consequently, healthy voltage can blink on low capacity while genuinely low voltage may not blink when capacity remains healthy.

Issue description

Keep voltage-display blinking based on voltage thresholds while preventing non-ADC builds from referencing an unavailable symbol. Do not substitute the aggregate battery state because it can represent remaining capacity.

Issue Context

checkBatteryVoltageState() evaluates batteryWarningVoltage and batteryCriticalVoltage, but getBatteryState() returns the shared state that batteryUpdate() derives from capacity whenever capacity thresholds are enabled. Use compile-time guarding or an always-defined voltage-state API with an appropriate non-ADC fallback.

Fix Focus Areas

  • src/main/io/osd.c[1588-1607]
  • src/main/sensors/battery.c[397-435]
  • src/main/sensors/battery.c[498-510]
  • src/main/sensors/battery.h[76-80]

Pattern 5: When fixed-wing waypoint turn mode is DIRECT, disable both fly-by turn anticipation and coordinated-turn feed-forward so the mode retains its documented legacy heading-PID behavior.

Example code before:

armFlyByTurn(nextTurnAngle);
rollAdjustment += getFwTurnFeedForward(headingError);

Example code after:

if (turnMode != NAV_FW_WP_TURN_DIRECT) {
    armFlyByTurn(nextTurnAngle);
    rollAdjustment += getFwTurnFeedForward(headingError);
}
Relevant past accepted suggestions:
Suggestion 1: [correctness] DIRECT mode cuts corners
DIRECT mode cuts corners In DIRECT mode, the code can still arm the FLY_BY anticipation path and `isWaypointReached()` will advance the mission as soon as the anticipated turn-start is reached, causing corner-cut/early waypoint advance even though DIRECT is supposed to be legacy behavior.

Issue description

DIRECT (legacy) waypoint turning is not respected: FLY_BY turn anticipation can still arm in DIRECT, and the mission can advance early (waypoint considered reached at turn start).

Issue Context

  • calculateAndSetActiveWaypoint() currently populates activeWaypoint.nextTurnAngle for all modes except COORD_FLYOVER.
  • calculateVirtualPositionTarget_FW() uses nextTurnAngle to arm wpTurnSmoothingActive without checking that the configured turn mode is COORD_FLYBY (or landing-approach forced FLY_BY).
  • isWaypointReached() returns true immediately when wpTurnSmoothingActive is set, which advances the mission early.

Fix Focus Areas

  • src/main/navigation/navigation.c[4300-4313]
  • src/main/navigation/navigation_fixedwing.c[1123-1144]
  • src/main/navigation/navigation.c[3103-3115]

Suggested change

  • Only compute activeWaypoint.nextTurnAngle for modes that actually need anticipation (COORD_FLYBY, COORD_FLYINTO, and landing approach override).
  • Additionally (defense-in-depth), gate the FLY_BY corner-cut arming (wpTurnSmoothingActive) so it only runs when wp_turn_mode == NAV_FW_WP_TURN_COORD_FLY_BY (or landing approach state forces FLY_BY).
  • Ensure DIRECT mode never sets wpTurnSmoothingActive and therefore never triggers early isWaypointReached().

Suggestion 2: [correctness] DIRECT mode gets turn feedforward
DIRECT mode gets turn feedforward Even when `nav_fw_wp_turn_mode` is DIRECT, the roll controller still adds coordinated-turn feed-forward (`getFwTurnFeedForward()`), so DIRECT cannot reproduce legacy heading-PID behavior.

Issue description

DIRECT mode is documented as legacy heading-PID behavior, but the new coordinated-turn feed-forward is applied unconditionally whenever WP tracking is active.

Issue Context

  • updatePositionHeadingController_FW() always executes rollAdjustment += getFwTurnFeedForward(navHeadingError); when not in an arc.
  • getFwTurnFeedForward() does not check navConfig()->fw.wp_turn_mode; it only checks WP tracking/heading error/speed.

Fix Focus Areas

  • src/main/navigation/navigation_fixedwing.c[1341-1347]
  • src/main/navigation/navigation_fixedwing.c[551-565]

Suggested change

  • Gate feed-forward by turn mode, e.g. return 0 (or skip calling) when navConfig()->fw.wp_turn_mode == NAV_FW_WP_TURN_DIRECT.
  • If you still want FF in DIRECT for some reason, update docs/setting semantics accordingly; otherwise DIRECT should be a true opt-out.

[Auto-generated best practices - 2026-10-03]

⚠️ **GitHub.com Fallback** ⚠️