Skip to content

MAVLink: allow mission upload and clear in flight when the mission is not being executed - #11851

Open
Raffi1202 wants to merge 5 commits into
iNavFlight:maintenance-10.xfrom
Raffi1202:mavlink-inflight-mission-upload-10x
Open

Raffi1202 wants to merge 5 commits into
iNavFlight:maintenance-10.xfrom
Raffi1202:mavlink-inflight-mission-upload-10x

Conversation

@Raffi1202

@Raffi1202 Raffi1202 commented Sep 2, 2026 •

Copy link
Copy Markdown
Contributor

Problem

A ground station connected over MAVLink cannot change the mission once the aircraft is armed: MISSION_COUNT and MISSION_CLEAR_ALL are answered with MAV_MISSION_DENIED, and any MISSION_ITEM / MISSION_ITEM_INT that is not a guided waypoint with MAV_MISSION_ERROR, even when no waypoint mission is running. The same upload over MSP has been accepted since #10273 (INAV 8.0) as long as WP mode is not active. No issue is filed for this; nothing crashes or misbehaves, the two transports simply apply different rules.

Cause

src/main/mavlink/mavlink_mission.c:986 (MISSION_COUNT), :953 (MISSION_CLEAR_ALL), :1051-1058 and :1254-1261 (MISSION_ITEM / _INT) on maintenance-10.x reject on ARMING_FLAG(ARMED) alone, while the MSP path in src/main/navigation/navigation.c:5468 only checks !FLIGHT_MODE(NAV_WP_MODE). Lifting the gate alone would not work: commit and clear (mavlink_mission.c:430, :197) always call mavlinkPersistMission(), and saveNonVolatileWaypointList() (navigation.c:5668) returns false while armed. Two related spots: navigation.c:5492 clamps activeWaypointIndex with > although the index is 0-based, and resetWaypointList() (navigation.c:5501) keeps the on-the-fly planner's write index and status when the list is replaced (Qodo finding on this PR).

Change

A mavlinkMissionEditBlocked() gate replaces the four ARMED checks: edits are refused only while armed and WP mode is active, the mission's own RTH leg is running, or the on-the-fly mission planner is active; if the refused sender owns a receiving transfer, it is aborted with MAV_MISSION_DENIED. Guided fly-to-here / altitude-target items are recognised by current == 2 || 3 on the item instead of by transfer state. While armed, commit and clear apply to RAM only and skip persistence, and armed commits apply the arm-time JUMP rules (no JUMP as first item, no self/adjacent target, sane repeat count, geo-referenced target) in mavlinkResolveUploadedMissionJumps(). setWaypoint() clamps with >=, resetWaypointList() resets wpPlannerActiveWPIndex and wpMissionPlannerStatus, and isWpMissionPlannerActive() is exported.

Test

Not run on hardware and no SITL session. Cause verified by reading the lines above on maintenance-10.x. Unit tests in src/test/unit/mavlink_unittest.cc: MissionCountWhileArmedIsRejected becomes MissionCountWhileArmedStartsTransfer; new tests cover rejection during WP mode and during the mission RTH leg, RAM-only commit and clear while armed (saveNonVolatileWaypointList not called), and the clear rejection during WP mode. Fork CI for the head commit, test job plus all targets and the four SITL builds green: https://github.com/Raffi1202/inav/actions/runs/34374150507. Upstream CI ran for the first commit only: https://github.com/iNavFlight/inav/actions/runs/33616820819; the runs for the two later commits are held at "action required".

Flash / RAM

Not measured yet. The upstream firmware CI has not been released for this PR, so no size report exists.

Docs

docs/Mavlink.md: the capability summary, the MISSION_ITEM and MISSION_CLEAR_ALL entries and the mission-storage paragraph described the old blanket armed rejection and unconditional persistence. They now state when an armed edit is refused, that a clear while armed leaves stored waypoints alone, and what RAM-only means for the pilot (survives disarming, lost on reboot, an armed upload collapses a loaded multi-mission set).

… not being executed

Aligns the MAVLink mission path with the MSP policy introduced in iNavFlight#10273,
where setWaypoint() accepts mission uploads while armed as long as the WP
mission is not actively being flown. The MAVLink handlers denied every
mission transfer and clear outright while armed.

Changes:
- New mavlinkMissionEditBlocked() gate used by MISSION_COUNT,
  MISSION_CLEAR_ALL, MISSION_ITEM and MISSION_ITEM_INT: edits are refused
  while armed AND (WP mode active OR the mission's own RTH leg is running
  OR the on-the-fly mission planner is active). The RTH-leg term protects
  the land/loiter decision at home, which reads the live list; the planner
  term prevents two writers on the same list. The ARMED term keeps the
  disarmed path provably unchanged.
- When the refused sender owns the receiving transfer, the transfer is
  aborted via mavlinkAbortMissionUpload(MAV_MISSION_DENIED) so the retry
  engine stops soliciting items from a partner that was just denied.
- Guided fly-to-here / altitude-target items are now dispatched on the
  item itself (NAV_WAYPOINT with current == 2 or 3) instead of on transfer
  state, so a guided click can never be absorbed into a running upload.
- mavlinkCommitMissionUpload() / mavlinkClearPersistedMission(): while
  armed the commit or clear applies to RAM only and skips persistence -
  saveNonVolatileWaypointList() refuses to run while armed, and a flash
  write mid-flight would stall the main loop. The uploaded mission
  survives disarm but not a reboot; persisting after landing remains the
  GCS's responsibility. An in-flight upload also collapses a loaded
  multi-mission set to the uploaded mission for the rest of the session.
- mavlinkResolveUploadedMissionJumps(): armed commits now enforce the same
  JUMP rules as the arm-time validation they bypass (no JUMP as first
  item, no self/adjacent targets, sane repeat count, geo-referenced
  target). Ground uploads are left to the arm-time check.
- navigation.c: setWaypoint()'s post-upload clamp of activeWaypointIndex
  uses >= instead of > (the index is 0-based, so index == waypointCount is
  already out of range); new public isWpMissionPlannerActive() accessor.
- Unit tests: the old MissionCountWhileArmedIsRejected asserts the new
  policy as MissionCountWhileArmedStartsTransfer; new tests cover the
  WP-mode and mission-RTH rejections, RAM-only armed commit and clear
  (persist not called), and the preserved clear rejection during WP mode.

The staged upload buffer with atomic commit and snapshot rollback means an
in-flight upload never exposes a partially written list to the navigation
state machine within a main-loop tick. Note the deliberate MSP-parity
semantics carried over from iNavFlight#10273: with nav_wp_mission_restart = RESUME,
replacing the mission mid-flight keeps the waypoint index when the new
mission is at least as long as the index.
@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

Allow safe MAVLink mission edits while armed

✨ Enhancement 🐞 Bug fix 🧪 Tests 🕐 40+ Minutes

Grey Divider

AI Description

• Allows MAVLink mission edits while armed unless the mission is actively executing.
• Keeps in-flight edits RAM-only and validates JUMP safety before atomic commit.
• Corrects waypoint bounds and tests allowed and blocked in-flight states.
Diagram

graph TD
    GCS["Ground station"] --> MAV["Mission handlers"] --> Gate{"Edit blocked?"} -->|Yes| Deny["Deny or abort"]
    Gate -->|No| Edit["Commit or clear"] --> RAM["RAM mission"] -->|Disarmed only| Store["Mission storage"]
    Nav["Navigation state"] --> Gate
Loading
High-Level Assessment

The gated, staged-edit approach is appropriate because it preserves atomic mission replacement while matching existing MSP behavior. Retaining the blanket armed prohibition would prevent the intended telemetry workflow, while persisting in flight would introduce flash-write latency and conflict with existing storage safeguards.

Files changed (4) +255 / -35

Enhancement (2) +97 / -12
mavlink_mission.cPermit guarded, RAM-only MAVLink mission edits in flight +96/-12

Permit guarded, RAM-only MAVLink mission edits in flight

• Replaces blanket armed-state rejection with a gate covering active WP mode, mission RTH, and on-the-fly planner activity. Armed uploads and clears remain RAM-only, unsafe JUMP commands receive arm-time-equivalent validation, denied owned transfers are aborted, and guided items are identified independently of transfer state.

src/main/mavlink/mavlink_mission.c

navigation.hDeclare the mission planner activity accessor +1/-0

Declare the mission planner activity accessor

• Publishes the navigation API used by the MAVLink mission-edit safety gate to detect planner activity.

src/main/navigation/navigation.h

Bug fix (1) +8 / -2
navigation.cExpose planner activity and correct active waypoint bounds +8/-2

Expose planner activity and correct active waypoint bounds

• Adds an accessor for on-the-fly mission planner activity so MAVLink can avoid concurrent mission edits. Corrects the active waypoint clamp because an index equal to the 0-based waypoint count is already invalid.

src/main/navigation/navigation.c

Tests (1) +150 / -21
mavlink_unittest.ccCover allowed and blocked in-flight mission edits +150/-21

Cover allowed and blocked in-flight mission edits

• Updates the armed upload expectation and adds tests for RAM-only upload and clear behavior. Covers rejection during active WP mode and mission RTH, with navigation-state stubs supporting the new gate.

src/test/unit/mavlink_unittest.cc

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

qodo-free-for-open-source-projects Bot commented Sep 2, 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. Stale planner corrupts missions ✓ Resolved 🐞 Bug ≡ Correctness
Description
The new gate permits armed MAVLink upload or clear after the planner is disabled, but those
operations do not reset wpPlannerActiveWPIndex, which planner deactivation also preserves.
Reactivating the planner then writes at the stale index and derives the mission count from it,
resurrecting cleared waypoints or overwriting/truncating the newly uploaded mission.
Code

src/main/mavlink/mavlink_mission.c[R101-102]

+    return ARMING_FLAG(ARMED) &&
+        (FLIGHT_MODE(NAV_WP_MODE) || isWaypointMissionRTHActive() || isWpMissionPlannerActive());
Evidence
The new predicate allows editing as soon as wpMissionPlannerActive becomes false. Planner shutdown
only clears that flag and leaves wpPlannerActiveWPIndex intact; both MAVLink edit paths call
resetWaypointList(), which also omits the index. On reactivation, the planner skips its list reset
when the stale index is nonzero, writes at that index, increments it, and assigns the resulting
value as the new waypoint count.

src/main/mavlink/mavlink_mission.c[99-103]
src/main/mavlink/mavlink_mission.c[203-219]
src/main/mavlink/mavlink_mission.c[446-477]
src/main/navigation/navigation.c[5502-5514]
src/main/navigation/navigation.c[6440-6480]

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

## Issue description
Armed MAVLink mission upload and clear are now allowed after the on-the-fly planner is disabled, but the planner's progress index remains stale. Reset planner progress whenever an external operation replaces or clears the waypoint list so later planner activation starts from a consistent list.
## Issue Context
Planner deactivation does not clear `wpPlannerActiveWPIndex`. MAVLink clear and commit reset the mission metadata without resetting that index, while `missionPlannerSetWaypoint()` uses any nonzero index directly and then sets the mission count from it.
## Fix Focus Areas
- src/main/mavlink/mavlink_mission.c[203-219]
- src/main/mavlink/mavlink_mission.c[446-477]
- src/main/navigation/navigation.c[5502-5514]
- src/main/navigation/navigation.c[6440-6480]

ⓘ 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 start a comment with 'qodo' or '@qodo' to chat about any finding

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

Comment thread src/main/mavlink/mavlink_mission.c
@github-actions

github-actions Bot commented Sep 4, 2026 •

Copy link
Copy Markdown

RAM / Flash usage vs. base commit 2e8857e — commit 5ebd67e

Target Flash Δ RAM Δ
MATEKF405 -696 B (-0.10%) -252 B (-0.17%)
MATEKF722 +2536 B (+0.54%) +56 B (+0.04%)
MATEKF765 -2172 B (-0.29%) -232 B (-0.14%)
MATEKH743 +84 B (+0.01%) +988 B (+0.59%)

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

@github-actions

github-actions Bot commented Sep 4, 2026 •

Copy link
Copy Markdown

Test firmware build ready — commit 5ebd67e

Download firmware for PR #11851

249 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.

@sensei-hacker sensei-hacker added this to the 10.0 milestone Sep 5, 2026
@sensei-hacker

Copy link
Copy Markdown
Member

@raphaelhunziker1202-stack what do you think of that Qodo comment? Is that real?

@Raffi1202

Copy link
Copy Markdown
Contributor Author

@sensei-hacker I checked it against the actual navigation code, and I don't think that Qodo finding is real.

Both paths it calls out already go through resetWaypointList():

  • mavlinkClearPersistedMission() calls resetWaypointList() before clearing the MAVLink mission state.
  • mavlinkCommitMissionUpload() calls resetWaypointList() before installing the staged mission.

resetWaypointList() resets posControl.wpPlannerActiveWPIndex = 0 and also clears posControl.flags.wpMissionPlannerActive.

So after either an external clear or a replacement upload, re-enabling the on-the-fly planner cannot resume from the previous planner index; it starts from index 0.

The failure sequence Qodo describes would require wpPlannerActiveWPIndex to survive the clear/commit, but it does not. I therefore believe this is a false positive rather than a bug in the PR.

I can add a focused regression test for this interaction if you think it would be useful as documentation/guard coverage, but I don't think a production-code change is needed for this finding.

An upload or clear (MSP, MAVLink, EEPROM load) goes through
resetWaypointList(), but the on-the-fly planner kept its write index and
status across it. Re-enabling the planner afterwards wrote at the stale
index inside the new mission and derived the waypoint count from it, or
refused new waypoints if the old status was still FULL.
@Raffi1202

Raffi1202 commented Sep 9, 2026 •

Copy link
Copy Markdown
Contributor Author

Addressed the review finding on the mission planner index in ea0577c: resetWaypointList() now resets wpPlannerActiveWPIndex and wpMissionPlannerStatus, so an upload or clear that replaces the list (MAVLink, MSP or EEPROM load) leaves the on-the-fly planner in a consistent state when it is re-enabled. Placing it there rather than in the MAVLink handlers covers the MSP in-flight path from #10273 as well.

The upstream workflow for this commit is waiting for approval; the same commit built green on my fork: https://github.com/Raffi1202/inav/actions/runs/34327738409 (all targets, SITL, unit tests).

@Raffi1202

Copy link
Copy Markdown
Contributor Author

@sensei-hacker I have to correct my reply from 6 September — it was wrong.

At 1209520, resetWaypointList() did not touch wpPlannerActiveWPIndex, wpMissionPlannerStatus or flags.wpMissionPlannerActive at all; I misread it. So the Qodo finding was real, not a false positive: after an upload or clear, re-enabling the on-the-fly planner would have resumed at its old index inside the new mission.

Fixed in ea0577cbb: resetWaypointList() now resets wpPlannerActiveWPIndex = 0 and wpMissionPlannerStatus = WP_PLAN_WAIT. flags.wpMissionPlannerActive is deliberately left alone — updateWpMissionPlanner() sets it right before the first planner waypoint is stored, so clearing it there would undo that.

@sensei-hacker

Copy link
Copy Markdown
Member

@sensei-hacker I have to correct my reply from 6 September — it was wrong.

At 1209520, resetWaypointList() did not touch wpPlannerActiveWPIndex, wpMissionPlannerStatus or flags.wpMissionPlannerActive at all; I misread it. So the Qodo finding was real, not a false positive: after an upload or clear, re-enabling the on-the-fly planner would have resumed at its old index inside the new mission.

Fixed in ea0577cbb: resetWaypointList() now resets wpPlannerActiveWPIndex = 0 and wpMissionPlannerStatus = WP_PLAN_WAIT. flags.wpMissionPlannerActive is deliberately left alone — updateWpMissionPlanner() sets it right before the first planner waypoint is stored, so clearing it there would undo that.

Thanks!

Raphael Hunziker added 2 commits September 13, 2026 20:59
Those two files belong to iNavFlight#11885, which replaces check-pg-versions.sh
with a Python checker. Carrying a second, older edit of the same file
here only produces a conflict once either lands, and it is unrelated to
the MAVLink mission change.
Four statements no longer held: the blanket armed rejection, the clear
that always removed the saved mission, and the claim that every completed
upload reaches nonvolatile storage. Adds what RAM-only means for the
pilot - the mission survives disarming but not a reboot - and that an
upload while armed collapses a loaded multi-mission set.
@sensei-hacker sensei-hacker modified the milestones: 10.0, 10.1 Sep 21, 2026
@b14ckyy

b14ckyy commented Sep 23, 2026

Copy link
Copy Markdown
Collaborator

Thanks for catching the off-by-one in setWaypoint(); that one came from my #10273.

A few thoughts from my side, mostly suggestions to consider rather than requirements — happy to hear what you and @sensei-hacker think:

Shared edit gate for MSP and MAVLink
Your gate also blocks edits during the mission RTH leg and while the planner is active, which I think is the right set. MSP (setWaypoint()) currently only checks NAV_WP_MODE, so the two paths now follow slightly different rules, and your reasoning about the land/loiter decision at home would apply to MSP as well. Would it make sense to move the check into navigation.c (something like isWaypointMissionEditAllowed()) and use it from both sides? That would keep them consistent going forward.

JUMP validation
Good point that an in-flight upload bypasses the arm-time checks — that's true for MSP too. Instead of mirroring the rules in mavlinkResolveUploadedMissionJumps(), one option would be to factor them out of navigationIsBlockingArming() into a common helper that both upload paths can use while armed. Not a blocker, but it would avoid the two copies drifting apart.

Smaller things

  • The deny/abort block appears four times; a small helper could shorten mavlink_mission.c a bit.
  • Guided items are now recognised by current == 2/3. That matches the QGC/ArduPilot convention, but do you know whether any GCS we commonly see sends guided waypoints with other current values? Those would no longer be treated as guided.

Testing
Since this changes the active mission in flight, a SITL run with QGC and/or Mission Planner would give a lot of confidence: upload while armed outside WP mode, rejection during WP mode and mission RTH, and guided "fly to here" before, during and after an upload.

Thanks again!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants