MAVLink Adapter Validation Against PX4 and ArduPilot SITLLink to the section MAVLink Adapter Validation Against PX4 and ArduPilot SITL

On this page

Status: Test report, 2026-10-15. The reference gateway's MAVLink adapter (gateway/univip_gateway/adapters/mavlink.py) had only been tested against a fake PX4-like vehicle. This report records a run against real autopilot software in the loop: what was exercised, what failed, what was fixed, how PX4 and ArduPilot differ, and what remains unverified. Every result below was observed in this run; nothing is inferred from documentation alone unless it says so.

Follow-up validation — 2026-10-16Link to the section Follow-up validation — 2026-10-16

GitHub Actions run 38794922963, at source commit e1ff3d8, passed 10 PX4 cases and 10 ArduCopter cases on Ubuntu. Both jobs used the pinned releases below. The original six cases per autopilot passed again, alongside these additions:

Additional check PX4 ArduCopter
Controller geofence return; new client movement refused with SAFETY while failsafe owns control PASS PASS
Gateway restart rediscovers the same running autopilot PASS PASS
Vehicle lease expiry during goto stops motion PASS PASS
GNSS visibility and recovery PASS: suppress/recover GPS messages at the gateway boundary PASS: disable/re-enable simulated GPS; takeoff fails and the aircraft stays on the ground

PX4 SIH did not acknowledge GPS failure injection. Its message-staleness case leaves controller GPS healthy and does not validate a GPS failsafe. The geofence run exposed a command-admission gap: both gateways now refuse new movement during a reported failsafe return. Parameter writes require read-back acknowledgment. See software validation for protocol-emulator, network-fault and recorded browser coverage.

The original report sections below retain their dated observations; the follow-up above extends their coverage.

1. What was runLink to the section 1. What was run

PX4 ArduPilot
Release v1.17.0 Copter-4.7.2 (FIRMWARE_VERSION 4,7,2, official)
Commit d6f12ad1c4f70ad3230afd7d86e971421e02fef4 fb773567896014da007bca58358704d4a0d8516f
Target px4_sitl_default, airframe 10040 sihsim_quadx (simulation in hardware: physics inside the PX4 process, no Gazebo) sitl board, arducopter with the built-in + multicopter model, Tools/autotest/default_params/copter.parm
Payload gimbal module, MNT_MODE_IN=4 (MAVLink gimbal v2), MNT_MODE_OUT=0 (AUX); no MAVLink camera servo mount MNT1_TYPE=1 (pitch −90…30°, yaw ±180°), servo camera CAM1_TYPE=1
Other parameters NAV_DLL_ACT=2 (return on GCS link loss), RTL_CONE_ANG=0 (return altitude always applied) FS_GCS_ENABLE=1, FS_GCS_TIMEOUT=5, BATT_FS_LOW_ACT=2 (RTL)
Gateway connection udpin:127.0.0.1:14550 tcp:127.0.0.1:5760 (+10 per instance)
Second ground station (pilot) udpout:127.0.0.1:14580 (onboard link) tcp:127.0.0.1:5762 (SERIAL1)

Host: Ubuntu 24.04, 4 cores shared with other jobs, GCC 13.3, CMake 3.28, Ninja 1.11, Python 3.12 for the autopilots' build tooling (separate venv, empy==3.3.4), Python 3.13 and pymavlink 2.4.50 for the gateway. Gateway base commit 1733d29.

Every test runs the gateway with one mavlink device against a fresh SITL and the development endpoint (univip_gateway/endpoint.py), which validates every message it receives against the schemas and the ordering rules, and drives the device over REST as a VMS would (leases, commands, telemetry, events). Telemetry is published at 5 Hz.

ReproducingLink to the section Reproducing

tools/sitl/build_px4.sh          # clones into ~/sitl (SITL_DIR); about 6 min here with JOBS=3
tools/sitl/build_ardupilot.sh    # about 8 min here with JOBS=2
cd gateway
UNIVIP_SITL_PX4=~/sitl/PX4-Autopilot/build/px4_sitl_default/bin/px4 \
UNIVIP_SITL_ARDUPILOT=~/sitl/ardupilot/build/sitl/bin/arducopter \
pytest -v -s tests/test_sitl.py

Without the two variables tests/test_sitl.py skips, so the normal suite is unaffected. The sitl GitHub Actions workflow (workflow_dispatch only) builds either autopilot and runs the same tests; the follow-up above records its execution on GitHub's runners. The build scripts fetch only the submodules the SITL builds need (PX4 needs modules/mavlink recursively for pymavlink; ArduPilot SITL needs modules/lwip).

2. ResultsLink to the section 2. Results

PASS = observed working end to end through the gateway (after the fixes in §3 where marked). NOT TESTED = not exercised against SITL; the reason is given. Test names refer to gateway/tests/test_sitl.py.

2.1 Discovery and telemetry (test_discovery_and_telemetry_at_rest)Link to the section 2.1 Discovery and telemetry (test_discovery_and_telemetry_at_rest)

Check PX4 ArduPilot
Locks onto the autopilot; manufacturer, model (Quadrotor), firmware (1.17.0 / 4.7.2), serial from AUTOPILOT_VERSION PASS PASS
Gimbal discovered after connection, capabilities re-announced, profiles S, C, N claimed PASS PASS
Gimbal limits in degrees PASS after fix 1 (±45° pitch, ±180° yaw) PASS (−90…30° from MNT1_PITCH_MIN/MAX)
Camera discovered, only real functions advertised NOT TESTED (no MAVLink camera in PX4 SITL) PASS after fix 2 (snapshot only)
Capability document valid against the schema PASS PASS
Position at home; altitude_msl_m = home + altitude_rel_m; ellipsoid height PASS (SIH reports alt_ellipsoid = alt) PASS (home 584 m at CMAC)
Heading in [0, 360), level attitude at rest PASS PASS
Battery %, voltage; no negative current PASS after fix 3 (PX4 sends −100 cA for "unknown") PASS
GNSS fix, satellites, HDOP; position.source PASS (3D, 10) PASS (SITL reports RTK fixed, 10)
AGL from a downward rangefinder PASS after fix 4 (SIH distance sensor, was 0.5 Hz) NOT TESTED (no rangefinder configured)
First sample carries battery data PASS after fix 5 PASS after fix 5
Every message valid at the endpoint, no sequence gaps PASS PASS

2.2 Vehicle commands and state (test_flight_commands_end_to_end)Link to the section 2.2 Vehicle commands and state (test_flight_commands_end_to_end)

Check PX4 ArduPilot
takeoff 15 m: IDLE → PREDEPLOY → DEPLOYING → HOLDING, operation.started PASS (NAV_TAKEOFF at home AMSL + 15) PASS (GUIDED, arm, NAV_TAKEOFF)
goto with altitude_rel_m PASS after fix 6 PASS
goto with altitude_msl_m and heading_deg PASS (yaw sent in radians) PASS
goto with altitude_agl_m Rejected INVALID_PARAMETERS by design (fix 6) NOT TESTED (needs terrain data; SITL has none offline)
move_relative NED (20 m south) PASS PASS
move_relative BODY (15 m forward, 5 m up) PASS PASS
set_heading 270° PASS after fix 7 (completion waited for the turn) PASS
hold stops a goto under way, then holds altitude PASS after fix 8 PASS after fix 9 (BRAKE)
cancel stops a goto under way PASS after fix 8 PASS after fix 9
return_home 35 m, land_on_arrival: false: returning_home (reason CLIENT), climbs to ≥ 33 m, hovers over home, command COMPLETED PASS after fix 10 PASS after fixes 10, 11
RTL parameters set, then restored once the vehicle leaves RTL PASS (RTL_RETURN_ALT, RTL_LAND_DELAY) PASS after fix 11 (RTL_ALT_M, RTL_ALT_FINAL_M)
land at the current position: RECOVERING → RECOVERED → IDLE, operation.ended PASS PASS
return_home with landing: RETURNING → RECOVERING → RECOVERED → IDLE, lands within 5 m of home PASS PASS
land at a given position NOT TESTED NOT TESTED (rejected by design: ArduPilot lands where it is)

2.3 Gimbal and camera (test_flight_commands_end_to_end)Link to the section 2.3 Gimbal and camera (test_flight_commands_end_to_end)

Check PX4 ArduPilot
set_orientation EARTH −30°/90° PASS after fix 12 (primary control) PASS after fix 13 (attitude stream)
set_orientation BODY −20°/−30°, reported in EARTH (heading added) PASS PASS
set_orientation with one axis omitted keeps the other PASS PASS after fix 14
move_continuous 10°/s for 1.5 s, then stop PASS after fix 15 PASS after fix 15
recenter PASS PASS
snapshot NOT TESTED (no camera) PASS (acknowledged; no image file checked)
zoom.*, recording.* NOT TESTED (no camera) NOT TESTED (servo camera has neither)
Check PX4 ArduPilot
Second ground station switches to a manual mode in flight (POSCTL / LOITER, sticks centred): pilot_override, control source PILOT, event ACTIVE, vehicle.* rejected PILOT_OVERRIDE; hand-back to Hold / BRAKE clears it PASS PASS
Low-battery failsafe during a client goto: returning_home with reason LOW_BATTERY, the goto FAILED/SAFETY, control source FAILSAFE PASS after fix 16 (reason from the battery level: drained to 6 %) PASS after fix 16 (reason from STATUSTEXT: the level was still above 80 %)
Ground-station link loss (gateway heartbeats stop, telemetry still flows): failsafe return and its reason Return PASS; reason OTHER: no STATUSTEXT of WARNING or worse reached the gateway (F12) PASS: reason LINK_LOST from "GCS Failsafe"
Vehicle silent (SITL process stopped): vehicle.telemetry.stale, link.state: LOST without position, vehicle.hold rejected LINK_LOST; gateway shuts down cleanly PASS PASS after fix 19
A new adapter connecting to an already running SITL (exploration runs, not in the test module) Works with udpin:…:14550; with udpout the second client got nothing (F20) Works (TCP)
MAV_STATE_EMERGENCY NOT TESTED (not reachable in SITL without a crash; covered by a hand-fed test) NOT TESTED
RADIO_STATUS, link degraded, multiple batteries, geofence return, GNSS loss NOT TESTED NOT TESTED

3. Bugs found and fixedLink to the section 3. Bugs found and fixed

All fixes are in gateway/univip_gateway/adapters/mavlink.py unless stated. Each has a regression test in the normal (fast) suite, either against the fake vehicle, which now reproduces the PX4 behaviour concerned, or as a hand-fed message sequence replaying what SITL sent (Feed in tests/test_mavlink_adapter.py).

# Bug (autopilot where seen) Cause Fix Regression test
1 Gimbal limits advertised as ±5156.6° pitch, ±20626.5° yaw (PX4) PX4 1.17 AUX-output gimbals put MNT_RANGE_* in degrees into the radian fields of GIMBAL_MANAGER_INFORMATION, as ±range, while moving within ±range/2 Values beyond 2π are taken as degrees; PX4's are halved test_px4_aux_gimbal_limits_in_degrees_and_current_marker
2 zoom_ratio 5.5 and recording.active reported for a camera with neither (ArduPilot) CAMERA_SETTINGS/CAMERA_CAPTURE_STATUS were used without checking CAMERA_INFORMATION.flags Gated on HAS_BASIC_ZOOM / CAPTURE_VIDEO test_camera_without_zoom_or_video_reports_neither
3 current_a: −1.0 (PX4) PX4 scales its internal "invalid" (−1 A) to −100 cA −100 cA from PX4 is treated as unknown test_px4_aux_gimbal_limits_in_degrees_and_current_marker
4 altitude_agl_m flickered in and out (PX4) PX4 streams DISTANCE_SENSOR at 0.5 Hz, older than the 2 s freshness window DISTANCE_SENSOR requested at 2 Hz (SITL only)
5 First telemetry samples published battery_pct: 0 (both) The first observation could precede SYS_STATUS, and the gateway fills a missing power block with 0 % First observation held back up to 3 s for battery data test_first_observation_waits_briefly_for_power
6 Relative-altitude goto flew to that altitude AMSL: the vehicle descended to the ground and landed (PX4) PX4 ignores the COMMAND_INT frame of DO_REPOSITION; z is always AMSL Relative targets converted with the home altitude and sent in MAV_FRAME_GLOBAL_INT; AGL rejected test_goto_reaches_target (the fake now ignores the frame too)
7 set_heading COMPLETED while still turning at 17°/s (PX4) A vehicle turning in place already reads HOLDING; completion only checked heading ±5° (gateway/univip_gateway/commands.py) Completion also needs yaw_rate_degps ≤ 5 test_set_heading_completes_once_the_turn_has_settled
8 hold and cancel (and the stop on lease loss) did not stop a goto (PX4) PX4 flies DO_REPOSITION in Hold, so commanding Hold again changes nothing PX4 hold is its pause: all-NaN DO_REPOSITION test_hold_stops_a_goto_in_progress
9 After hold the copter descended from 20 m and landed (ArduPilot) LOITER follows the throttle stick; with no RC (or the stick low) it descends Copter holds in BRAKE test_ardupilot_copter_hold_is_brake
10 Return without landing never completed and read RETURNING for ever (both) Both autopilots hover over home in RTL mode (PX4 at RTL_DESCEND_ALT with RTL_LAND_DELAY < 0, ArduPilot at RTL_ALT_FINAL); the fake switched to Hold instead RTL mode, within 5 m of home and at rest (horizontal and vertical) for 10 s reads HOLDING; parameters are restored only after RTL ends (restoring RTL_LAND_DELAY during the hover would make PX4 land) test_return_home_reports_client_and_applies_rtl_altitude, test_holding_needs_vertical_rest_and_rtl_hover_holds
11 return_home with altitude_rel_m or land_on_arrival rejected INVALID_PARAMETERS (ArduPilot 4.7) Copter 4.7 renamed RTL_ALT/RTL_ALT_FINAL (cm) to RTL_ALT_M/RTL_ALT_FINAL_M (m) Names and units chosen from the firmware version, with a fallback; only Copter claims a settable return altitude test_ardupilot_copter_rtl_parameters_by_firmware (4.7, 4.6, unknown)
12 Every gimbal command DENIED → SAFETY (PX4) PX4's gimbal manager obeys only its primary controller DO_GIMBAL_MANAGER_CONFIGURE first; BUSY if another system holds control test_gimbal_orientation_frames, test_gimbal_held_by_another_system_is_busy
13 Gimbal orientation never reported (ArduPilot) ArduPilot does not stream GIMBAL_DEVICE_ATTITUDE_STATUS unless asked Attitude (5 Hz) and manager status (1 Hz) requested from the gimbal manager (SITL only)
14 Pitch-only set_orientation acknowledged but ignored (ArduPilot) ArduPilot applies an angle pair only when both are finite; a NaN yaw only sets the yaw lock Omitted axis sent at its current angle test_gimbal_orientation_frames
15 move_continuous did nothing (PX4) PX4 has no GIMBAL_MANAGER_SET_PITCHYAW handler; ArduPilot needs both rates finite Rates sent with DO_GIMBAL_MANAGER_PITCHYAW (acknowledged); the fake dropped its handler test_gimbal_orientation_frames
16 Every failsafe return read EMERGENCY: no RETURNING, no returning_home event, client commands failed "vehicle emergency" (both) Both report MAV_STATE_CRITICAL while any failsafe is active; A.2.2 maps CRITICAL to EMERGENCY Only MAV_STATE_EMERGENCY and FLIGHT_TERMINATION mean emergency test_failsafe_critical_state_is_not_an_emergency
17 Client's own take-off failed PILOT_OVERRIDE (fake; flaky test_mavlink_vehicle_through_gateway) The override test used the mode from the last heartbeat (POSCTL on the ground) while a newer EXTENDED_SYS_STATE already said airborne Override is a switch into a manual mode not commanded by the gateway, or lift-off in one, judged on heartbeats only test_pilot_override_only_on_a_heartbeat_after_liftoff
18 HOLDING while descending at 1.2 m/s in Hold (PX4, during bug 6) The HOLDING rule only looked at ground speed Vertical speed must also be < 0.5 m/s test_holding_needs_vertical_rest_and_rtl_hover_holds
19 Gateway shutdown raised AttributeError after the vehicle went away, and the reader logged the refused reconnect twice a second (ArduPilot over TCP) pymavlink's failed TCP reconnect leaves no socket, and its close() then fails close() failures ignored on stop; one warning per outage test_stop_survives_a_connection_that_lost_its_socket

Across a commanded take-off the SITLs reported: PX4 LOITER (Hold, on the ground) → TAKEOFF with landed_state TAKEOFF → LOITER; ArduCopter STABILIZE → GUIDED → armed, landed_state TAKEOFF → GUIDED. Neither idled in a manual mode, so bug 17's race (a vehicle armed in POSCTL on the ground, as the fake does, and as a PX4 with a pilot's controller would) did not occur in SITL. Fix 17 was verified with the fake and the hand-fed sequence; the real pilot override was verified against both SITLs (§2.4).

4. Behaviour that differs between PX4 and ArduPilotLink to the section 4. Behaviour that differs between PX4 and ArduPilot

Topic PX4 1.17 ArduCopter 4.7.2
GCS link UDP; answers only the first peer of a link for the process lifetime TCP (or serial); a new connection is accepted after the old one closes
Mode on the ground at start Hold (AUTO_LOITER; SIH, no RC) STABILIZE
goto DO_REPOSITION, flown in Hold mode, z always AMSL, yaw in radians GUIDED + SET_POSITION_TARGET_GLOBAL_INT, frame honoured
Hold All-NaN DO_REPOSITION (pause); selecting Hold during a reposition does nothing BRAKE; LOITER is pilot-throttle
set_heading DO_REPOSITION at the current position with a yaw GUIDED + CONDITION_YAW
Return without landing Descends to RTL_DESCEND_ALT over home and hovers, in RTL Hovers at RTL_ALT_FINAL_M over home, in RTL
Return altitude near home Did not climb to RTL_RETURN_ALT 7 m from home (attributed to the RTL_CONE_ANG cone, disabled in the tests) Climbed to RTL_ALT_M from 30 m out
RTL parameters RTL_RETURN_ALT (m), RTL_LAND_DELAY (s, < 0 = hover) RTL_ALT_M, RTL_ALT_FINAL_M (m) from 4.7; RTL_ALT, RTL_ALT_FINAL (cm) before (from source, 4.6.3)
Failsafe system_status CRITICAL for any active failsafe CRITICAL for any active failsafe
Failsafe start Holds for 5 s ("Failsafe activated: entering Hold for 5 seconds"), then acts Acts at once
Failsafe STATUSTEXT seen (severity) Low battery: "Low battery", "Critical battery", "Failsafe activated: entering Hold for 5 seconds", "Failsafe activated" (all CRITICAL). GCS link loss: none of WARNING or worse. The cause is in the events protocol (EVENT, decoded with PX4's metadata) Low battery: "Battery 1 is low 12.60V used 160 mAh", "Battery Failsafe". GCS link loss: "GCS Failsafe" (all WARNING)
Pre-arm and arming refusals CRITICAL STATUSTEXT ("Arming denied: …", from the source, mavlink_log_critical; not observed in this run) CRITICAL STATUSTEXT ("PreArm: Need Position Estimate", "Arm: Need Position Estimate", observed)
Pilot's controller disappears Manual-control-loss failsafe (Hold for 5 s, then its action), observed Not exercised; without RC input the throttle reads low (bug 9)
Gimbal manager Primary control required; no GIMBAL_MANAGER_SET_PITCHYAW; AUX limits in degrees; attitude streamed at 1 Hz unasked Commands accepted without taking control; both angles (or both rates) required; limits in radians; attitude only when requested
Gimbal attitude flags ROLL_LOCK, PITCH_LOCK, YAW_LOCK only (no frame flags) Always YAW_IN_VEHICLE_FRAME, plus YAW_LOCK when locked
Continuous gimbal rate Stopped about 2 s after the command in one exploration run (the tests' moves last 1.5 s) Continued until the stop command
Battery (SIH / SITL) SIH drains to SIM_BAT_MIN_PCT (50 %) in about 30 s of flight and resets on disarm; current unknown (−100 cA) 12.6 V with consumed capacity; percentage falls slowly
Rangefinder SIH has one (DISTANCE_SENSOR, 0.5 Hz by default) None by default

5. What remains unverifiedLink to the section 5. What remains unverified

  • Items marked NOT TESTED in the original results remain unverified unless covered by the follow-up: camera zoom and recording, AGL targets, landing at a given position, RADIO_STATUS, multiple batteries, airborne GNSS failsafes and MAV_STATE_EMERGENCY.
  • STATUSTEXT of CRITICAL severity becomes a CRITICAL health fault for 30 s, which makes the gateway reject movement commands with SAFETY. ArduPilot sends pre-arm and arming refusals at that severity (observed; PX4 does too according to its source), and PX4 its battery warnings (observed), so one refused arming would block client commands for 30 s (F21). The texts were observed; the resulting rejection was not isolated in a test, and the behaviour is unchanged.
  • Repeatability under load: on a host shared with other builds, one PX4 SIH run lost its GNSS and magnetometer health during take-off and landed by itself (the link-loss test failed at take-off); the rerun passed. The ArduPilot low-battery test once saw no failsafe within 120 s, apparently because the second ground station's TCP connection was closed right after the parameter write; it now stays open, and the test has passed since.
  • Other airframes and vehicle types: fixed wing, VTOL, ArduPlane, Rover, Sub; PX4 with a MAVLink gimbal device (MNT_MODE_OUT=2) or a MAVLink camera.
  • PX4 with an RC transmitter, and an RC pilot override on either autopilot. The override was produced by a second ground station (MANUAL_CONTROL on PX4, RC override on ArduPilot).
  • ArduCopter before 4.7 (the cm parameter names are taken from the 4.6.3 source and covered by a stub test only), and PX4 releases other than 1.17.
  • Real radios, real link loss and real time sync: SITL runs on loopback with no latency or loss.

6. Findings for specification 0.2 (Annex A.2)Link to the section 6. Findings for specification 0.2 (Annex A.2)

Numbered after F1–F9 in implementation-findings.md, in its format.

# Severity Finding Reference implementation Proposal
F10 Gap A.2.2 maps system_status CRITICAL to EMERGENCY, but MAVLink defines CRITICAL as "failsafe, can still navigate", and PX4 and ArduPilot report it for every active failsafe, including a low-battery return. A gateway following the annex reports an emergency instead of RETURNING and hides the return reason. EMERGENCY only for MAV_STATE_EMERGENCY and FLIGHT_TERMINATION; the vehicle state follows the mode. Map CRITICAL to "failsafe active" (state from the mode) and keep EMERGENCY for EMERGENCY/FLIGHT_TERMINATION.
F11 Gap A.2.3 maps goto REL to MAV_FRAME_GLOBAL_RELATIVE_ALT_INT and AGL to MAV_FRAME_GLOBAL_TERRAIN_ALT_INT for DO_REPOSITION, but PX4 (1.17) ignores the frame and reads z as AMSL; a relative target then flies into the ground. PX4: REL converted with the home altitude and sent AMSL; AGL rejected. ArduPilot: frame honoured. State that DO_REPOSITION altitude is AMSL for PX4; drop the frame column for PX4 or require gateways to convert.
F12 Gap Beyond F5 (no failsafe-reason message), PX4 1.17 announces the failsafe cause only through the events protocol (EVENT, decoded with PX4's component metadata). Its STATUSTEXT says "Failsafe activated" for a battery failsafe and nothing at WARNING or worse for a GCS link loss, so a gateway reading STATUSTEXT cannot tell a link-loss return from others on PX4. ArduPilot names the cause ("Battery Failsafe", "GCS Failsafe"). Low battery from the battery level or text; link loss reads OTHER on PX4, LINK_LOST on ArduPilot. Name the events protocol as the PX4 source for reason; keep OTHER legitimate.
F13 Ambiguity A.2.3 maps vehicle.hold to DO_SET_MODE Hold (PX4) / LOITER (ArduPilot). On PX4 a goto already flies in Hold, so the command stops nothing; ArduCopter LOITER follows the throttle stick and descends without RC. PX4: all-NaN DO_REPOSITION; ArduCopter: BRAKE. Map hold to PX4's pause (DO_REPOSITION, lat/lon/alt NaN) and ArduCopter BRAKE; require hold to keep altitude without pilot input.
F14 Gap A.2.2 has no row for a vehicle that has finished a return without landing: PX4 and ArduPilot hover over home still in RTL mode, which A.2.2 reads as RETURNING indefinitely. HOLDING when in RTL, within 5 m of home and at rest for 10 s (longer than ArduPilot's 5 s RTL_LOIT_TIME). Add the row; define when a return_home with land_on_arrival: false is complete.
F15 Ambiguity The HOLDING rule (ground speed < 0.5 m/s for > 2 s) ignores vertical motion and yaw: a vehicle climbing at 2 m/s or turning in place at 45°/s in a hold mode reads HOLDING, and set_heading completion based on it fires mid-turn. Vertical speed also < 0.5 m/s; set_heading completion also needs yaw rate ≤ 5°/s. Include vertical speed in the rule; define set_heading completion with a settled yaw rate.
F16 Gap A.2.3 maps camera.gimbal.move_continuous to GIMBAL_MANAGER_SET_PITCHYAW, which PX4 (1.17) does not implement, and does not say that gimbal protocol v2 requires taking the manager's primary control (DO_GIMBAL_MANAGER_CONFIGURE), without which PX4 denies every command. ArduPilot also ignores an angle pair with one NaN member. Rates via DO_GIMBAL_MANAGER_PITCHYAW; control taken first, BUSY when another system holds it; omitted axes sent at their current angle. Map continuous moves to DO_GIMBAL_MANAGER_PITCHYAW rates; require control acquisition and map a foreign controller to BUSY (this also settles the ⚠ on primary_control_sysid).
F17 Ambiguity GIMBAL_MANAGER_INFORMATION limits are radians, but PX4 AUX-output gimbals send degrees (as ±range while moving within ±range/2); the annex gives no unit guidance. Values beyond 2π read as degrees, halved for PX4. Note the deviation; advise plausibility checks.
F18 Ambiguity A.2.3 names RTL_ALT for the return altitude; ArduCopter 4.7 renamed it RTL_ALT_M (metres) and RTL_ALT_FINAL to RTL_ALT_FINAL_M. Parameter names are firmware-specific and change. Names and units by firmware version, with fallback; only Copter claims a settable return altitude. List parameters per autopilot and version, or reference the autopilots' parameter documentation instead of naming them.
F19 Gap Telemetry requires power, but a MAVLink vehicle's first messages after connection may not include battery data; the gateway's placeholder (battery_pct: 0) is a fabricated value that can trigger battery warnings. The adapter withholds the first observation up to 3 s for battery data. Make battery_pct optional or allow an explicit unknown.
F20 Editorial Annex A.2 gives no connection guidance, and PX4 answers only the first peer on each MAVLink UDP link for its lifetime: a gateway on an ephemeral port (udpout) is not answered after it restarts. Documented; udpin on PX4's remote port 14550. Add a deployment note: bind a fixed local port.
F21 Ambiguity A.2.1 maps STATUSTEXT of severity WARNING or worse to health faults, but ArduPilot sends routine refusals at CRITICAL ("PreArm: …", "Arm: Need Position Estimate", observed; PX4's "Arming denied: …" too, from its source), and §11.2 rejects commands while a critical fault is reported. One refused arming then blocks client commands. Texts kept as transient faults for 30 s, mapped by severity (unchanged). Map STATUSTEXT to events (vehicle.health.fault with a short life), not to the fault set used by the §11.2 safety check; or exclude pre-arm texts.

7. Test inventoryLink to the section 7. Test inventory

  • gateway/tests/test_sitl.py (opt-in): 20 cases, 10 per autopilot; the GPS case differs as described in the follow-up.
  • gateway/tests/test_mavlink_adapter.py: 11 new tests (14 cases; fake vehicle and hand-fed sequences); existing ones extended.
  • gateway/tests/test_gateway_mavlink.py: test_set_heading_completes_once_the_turn_has_settled.
  • tools/sitl/build_px4.sh, tools/sitl/build_ardupilot.sh, .github/workflows/sitl.yml.

Generated from docs/sitl-validation.md.