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.pyWithout 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) |
2.4 Pilot, failsafes and link (test_pilot_takes_control_from_another_station, test_low_battery_failsafe_reason, test_ground_station_link_loss_failsafe_reason, test_vehicle_silence_is_stale_and_blocks_commands)Link to the section 2.4 Pilot, failsafes and link (test_pilot_takes_control_from_another_station, test_low_battery_failsafe_reason, test_ground_station_link_loss_failsafe_reason, test_vehicle_silence_is_stale_and_blocks_commands)
| 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 andMAV_STATE_EMERGENCY. STATUSTEXTof CRITICAL severity becomes a CRITICAL health fault for 30 s, which makes the gateway reject movement commands withSAFETY. 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_CONTROLon 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.