mirror of
https://github.com/PX4/PX4-Autopilot.git
synced 2026-07-24 15:27:41 +08:00
* fix(mavlink): reject unsupported params in commands and missions PX4 accepted any non-NaN value in params it does not use for a given MAV_CMD, silently storing or ignoring them. This made it impossible for a GCS to detect that a mission item or command was malformed, and risked unexpected behaviour if the param meaning is assigned in a future update. Add mavlink_command_params.h: a header-only sorted lookup table mapping each supported MAV_CMD to uint8 bitmasks (one for mission items, one for commands) indicating which of params 1-4 are valid. check_params() does a binary search and returns the 1-based index of the first offending param, or 0 if all unsupported params are unset. A param is considered unset when it is NaN (the MAVLink standard) or 0.0 (the conventional GCS default for unused float fields). Any other value in an unsupported slot is rejected: - Mission uploads (MISSION_ITEM / MISSION_ITEM_INT): NACK with MAV_MISSION_INVALID_PARAMn for the offending param index. - Commands (COMMAND_LONG / COMMAND_INT): ACK with MAV_RESULT_DENIED. Validation is placed at the MAVLink ingress boundary (parse_mavlink_mission_item and handle_message_command_both) before any downstream processing, keeping Commander and Navigator unmodified. The table covers all 45 MAV_CMDs handled by PX4's mission and command parsers. The static_assert enforces sort order at compile time. Fixes #27483 Signed-off-by: Himaghna <pen314paper@gmail.com> * fix(mavlink): split mission/command p5-7 masks, fix int_mode branch, add vehicle overrides Signed-off-by: Himaghna <pen314paper@gmail.com> * fix(mavlink): fix 142-char lines and Python CI type/style errors Signed-off-by: Himaghna <pen314paper@gmail.com> * fix(mavlink): pack p5-7 bits into mission/command byte, fix astyle/type-limits/mypy * fix(mavlink): range-for over VehicleParamOverrides, fix mypy ignore code * fix(mavlink): wire check_params_for_vehicle, add int variant, drop non-vehicle check_params Signed-off-by: Himaghna <pen314paper@gmail.com> * fix(mavlink): suppress clang unused-function on check_params_*_for_vehicle Signed-off-by: Himaghna <pen314paper@gmail.com> * fix(mavlink): rename local vs to avoid shadow in handle_message_command_both Signed-off-by: Himaghna <pen314paper@gmail.com> * fix(mavlink): cache vehicle_type_bitmask as member, update on vehicle_status change Signed-off-by: Himaghna <pen314paper@gmail.com> * fix(mavlink): use named VEHICLE_TYPE_* consts, fix masks, drop WARN spam, fix int_mode cast Signed-off-by: Himaghna <pen314paper@gmail.com> * fix(mavlink): normalize INT32_MAX to 0 for MAV_FRAME_MISSION p5/p6 in int_mode For MAV_FRAME_MISSION items, x/y are generic p5/p6 params (not lat/lon), so GCS tools send 0 for unused params — not INT32_MAX. Using check_params_int_for_vehicle here caused int_param_is_unset(0)=false, which rejected valid DO_LAND_START and similar items with x=y=0. Fix by normalizing INT32_MAX to 0.0f before calling check_params_for_vehicle, so both the MISSION_ITEM_INT sentinel and the conventional float zero are treated as unset. This restores acceptance of zero-param items like DO_LAND_START while still correctly rejecting items that carry non-zero, non-sentinel values in unsupported param slots. Signed-off-by: Himaghna <pen314paper@gmail.com> * fix(mavlink): address review feedback on VTOL masks, NaN sentinel, dead enum, and RTL test frame * fix(mavlink): move param validation test out of mavsdk_tests, rename header to .hpp * fix(mavlink): dedupe param-scan loop shared by check_params_for_vehicle variants --------- Signed-off-by: Himaghna <pen314paper@gmail.com>
427 lines
13 KiB
Python
427 lines
13 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
MAVLink parameter-validation regression test.
|
|
|
|
Verifies that PX4 rejects mission items and commands carrying non-zero
|
|
values in parameter slots that the MAVLink spec marks as unsupported for
|
|
that MAV_CMD, and accepts identical items/commands with those slots
|
|
zeroed or NaN.
|
|
|
|
Usage:
|
|
python3 test_mavlink_param_validation.py [--url udp://:14540] [--timeout 5]
|
|
|
|
Requirements:
|
|
pip install pymavlink
|
|
"""
|
|
|
|
import argparse
|
|
import sys
|
|
import time
|
|
from typing import Any, Optional
|
|
|
|
try:
|
|
from pymavlink import mavutil # type: ignore[import-not-found]
|
|
except ImportError:
|
|
print("ERROR: pymavlink not installed. Run: pip install pymavlink")
|
|
sys.exit(1)
|
|
|
|
# MAVLink enum constants
|
|
|
|
MAV_MISSION_ACCEPTED = 0
|
|
MAV_MISSION_INVALID_PARAM1 = 6
|
|
MAV_MISSION_INVALID_PARAM2 = 7
|
|
MAV_MISSION_INVALID_PARAM3 = 8
|
|
MAV_MISSION_INVALID_PARAM4 = 9
|
|
MAV_MISSION_INVALID_PARAM5 = 10
|
|
MAV_MISSION_INVALID_PARAM6 = 11
|
|
MAV_MISSION_INVALID_PARAM7 = 12
|
|
|
|
MAV_RESULT_ACCEPTED = 0
|
|
MAV_RESULT_DENIED = 2
|
|
|
|
MAV_FRAME_GLOBAL_INT = 5
|
|
MAV_FRAME_GLOBAL_RELATIVE_ALT = 3
|
|
MAV_FRAME_MISSION = 2
|
|
|
|
# MAV_CMD values used in tests
|
|
CMD_NAV_WAYPOINT = 16
|
|
CMD_NAV_RTL = 20
|
|
CMD_NAV_TAKEOFF = 22
|
|
CMD_NAV_VTOL_TAKEOFF = 84
|
|
CMD_NAV_VTOL_LAND = 85
|
|
CMD_NAV_DELAY = 93
|
|
CMD_COMPONENT_ARM_DISARM = 400
|
|
|
|
NAN = float("nan")
|
|
INT32_MAX = 2_147_483_647
|
|
|
|
# Helpers
|
|
|
|
PASS = "\033[32mPASS\033[0m"
|
|
FAIL = "\033[31mFAIL\033[0m"
|
|
_results: list[bool] = []
|
|
|
|
|
|
def _check(label: str, got: Any, expected: Any) -> bool:
|
|
ok = bool(got == expected)
|
|
_results.append(ok)
|
|
status = PASS if ok else FAIL
|
|
exp_name = (
|
|
_mission_result_name(expected)
|
|
if isinstance(expected, int) else expected
|
|
)
|
|
got_name = (
|
|
_mission_result_name(got)
|
|
if isinstance(got, int) else got
|
|
)
|
|
print(f" [{status}] {label}")
|
|
if not ok:
|
|
print(
|
|
f" got={got_name} ({got}),"
|
|
f" expected={exp_name} ({expected})"
|
|
)
|
|
return ok
|
|
|
|
|
|
def _mission_result_name(v: int) -> str:
|
|
names = {
|
|
0: "ACCEPTED", 6: "INVALID_PARAM1", 7: "INVALID_PARAM2",
|
|
8: "INVALID_PARAM3", 9: "INVALID_PARAM4", 10: "INVALID_PARAM5",
|
|
11: "INVALID_PARAM6", 12: "INVALID_PARAM7",
|
|
}
|
|
return names.get(v, str(v))
|
|
|
|
|
|
def connect(url: str) -> Any:
|
|
mav = mavutil.mavlink_connection(url)
|
|
print(f"Waiting for heartbeat on {url} ...")
|
|
mav.wait_heartbeat(timeout=15)
|
|
print(
|
|
f"Connected: sysid={mav.target_system}"
|
|
f" compid={mav.target_component}\n"
|
|
)
|
|
return mav
|
|
|
|
|
|
def _upload_mission(
|
|
mav: Any, items: list[dict[str, Any]], timeout: float
|
|
) -> Optional[int]:
|
|
"""
|
|
Run the MISSION_ITEM_INT upload protocol and return the
|
|
MAV_MISSION_RESULT from the final MISSION_ACK, or None on timeout.
|
|
"""
|
|
mav.mav.mission_count_send(
|
|
mav.target_system, mav.target_component,
|
|
len(items), 0, # mission_type=0 (main)
|
|
)
|
|
|
|
deadline = time.monotonic() + timeout * (len(items) + 2)
|
|
|
|
while time.monotonic() < deadline:
|
|
msg = mav.recv_match(
|
|
type=["MISSION_REQUEST_INT", "MISSION_REQUEST", "MISSION_ACK"],
|
|
blocking=True,
|
|
timeout=timeout,
|
|
)
|
|
if msg is None:
|
|
return None
|
|
|
|
t = msg.get_type()
|
|
|
|
if t in ("MISSION_REQUEST_INT", "MISSION_REQUEST"):
|
|
item = items[msg.seq]
|
|
mav.mav.mission_item_int_send(
|
|
item["sys"], item["comp"],
|
|
item["seq"], item["frame"], item["cmd"],
|
|
item["current"], item["autocontinue"],
|
|
item["p1"], item["p2"], item["p3"], item["p4"],
|
|
item["x"], item["y"], item["z"],
|
|
0, # mission_type
|
|
)
|
|
|
|
elif t == "MISSION_ACK":
|
|
return int(msg.type)
|
|
|
|
return None
|
|
|
|
|
|
def _item(
|
|
seq: int, cmd: int, frame: int,
|
|
p1: float = NAN, p2: float = NAN,
|
|
p3: float = NAN, p4: float = NAN,
|
|
x: int = INT32_MAX, y: int = INT32_MAX, z: float = 0.0,
|
|
current: int = 0,
|
|
) -> dict[str, Any]:
|
|
return dict(
|
|
sys=1, comp=1, seq=seq, cmd=cmd, frame=frame,
|
|
current=current, autocontinue=1,
|
|
p1=p1, p2=p2, p3=p3, p4=p4,
|
|
x=x, y=y, z=z,
|
|
)
|
|
|
|
|
|
def _send_command(
|
|
mav: Any, cmd: int, timeout: float,
|
|
p1: float = 0.0, p2: float = 0.0,
|
|
p3: float = 0.0, p4: float = 0.0,
|
|
p5: float = 0.0, p6: float = 0.0, p7: float = 0.0,
|
|
) -> Optional[int]:
|
|
"""Send COMMAND_LONG and return the MAV_RESULT from the ACK, or None."""
|
|
mav.mav.command_long_send(
|
|
mav.target_system, mav.target_component,
|
|
cmd, 0,
|
|
p1, p2, p3, p4, p5, p6, p7,
|
|
)
|
|
deadline = time.monotonic() + timeout
|
|
while time.monotonic() < deadline:
|
|
msg = mav.recv_match(
|
|
type="COMMAND_ACK", blocking=True, timeout=timeout,
|
|
)
|
|
if msg and msg.command == cmd:
|
|
return int(msg.result)
|
|
return None
|
|
|
|
|
|
# Test cases
|
|
|
|
|
|
def run_mission_tests(mav: Any, timeout: float) -> None:
|
|
print("=== Mission upload tests ===")
|
|
|
|
# Coordinates for a valid waypoint (47.397 N 8.545 E 50 m)
|
|
LAT = 473_977_420 # 1e-7 deg
|
|
LON = 85_456_060 # 1e-7 deg
|
|
ALT = 50.0
|
|
|
|
# 1. Valid NAV_WAYPOINT
|
|
# mask 0x0B: p1 (hold), p2 (accept_radius), p4 (yaw); p3 unsupported.
|
|
result = _upload_mission(mav, [
|
|
_item(0, CMD_NAV_WAYPOINT, MAV_FRAME_GLOBAL_INT,
|
|
p1=0.0, p2=2.0, p3=NAN, p4=NAN,
|
|
x=LAT, y=LON, z=ALT, current=1),
|
|
], timeout)
|
|
_check("Valid NAV_WAYPOINT -> ACCEPTED", result, MAV_MISSION_ACCEPTED)
|
|
|
|
# 2. NAV_WAYPOINT with unsupported param3 set
|
|
result = _upload_mission(mav, [
|
|
_item(0, CMD_NAV_WAYPOINT, MAV_FRAME_GLOBAL_INT,
|
|
p1=0.0, p2=2.0, p3=1.0, p4=NAN,
|
|
x=LAT, y=LON, z=ALT, current=1),
|
|
], timeout)
|
|
_check(
|
|
"NAV_WAYPOINT unsupported param3 -> INVALID_PARAM3",
|
|
result, MAV_MISSION_INVALID_PARAM3,
|
|
)
|
|
|
|
# 3. NAV_RTL with param1 set (mask 0x00, no params). RTL is only a valid
|
|
# mission item under MAV_FRAME_MISSION (no coordinates) in PX4; sending
|
|
# it as MAV_FRAME_GLOBAL_INT hits the frame switch's default case
|
|
# (MAV_MISSION_UNSUPPORTED) once params pass, regardless of param values.
|
|
result = _upload_mission(mav, [
|
|
_item(0, CMD_NAV_RTL, MAV_FRAME_MISSION,
|
|
p1=1.0, p2=NAN, p3=NAN, p4=NAN, current=1),
|
|
], timeout)
|
|
_check(
|
|
"NAV_RTL unsupported param1 -> INVALID_PARAM1",
|
|
result, MAV_MISSION_INVALID_PARAM1,
|
|
)
|
|
|
|
# 4. NAV_RTL with all params NaN (should pass). mask 0x00 means p5-7
|
|
# (x/y/z) are unsupported too, so they must stay at the unset sentinel.
|
|
result = _upload_mission(mav, [
|
|
_item(0, CMD_NAV_RTL, MAV_FRAME_MISSION,
|
|
p1=NAN, p2=NAN, p3=NAN, p4=NAN, current=1),
|
|
], timeout)
|
|
_check(
|
|
"NAV_RTL all params NaN -> ACCEPTED",
|
|
result, MAV_MISSION_ACCEPTED,
|
|
)
|
|
|
|
# 5. NAV_DELAY with unsupported param5 (MAV_FRAME_MISSION, x=param5)
|
|
# mask 0x0F; mission567=0x00 -> p5/p6/p7 not supported
|
|
result = _upload_mission(mav, [
|
|
_item(0, CMD_NAV_DELAY, MAV_FRAME_MISSION,
|
|
p1=10.0, p2=NAN, p3=NAN, p4=NAN,
|
|
x=42, y=0, z=0.0),
|
|
], timeout)
|
|
_check(
|
|
"NAV_DELAY unsupported param5 (x=42) -> INVALID_PARAM5",
|
|
result, MAV_MISSION_INVALID_PARAM5,
|
|
)
|
|
|
|
# 6. NAV_DELAY valid (p1 set, p5/p6/p7 zero)
|
|
result = _upload_mission(mav, [
|
|
_item(0, CMD_NAV_DELAY, MAV_FRAME_MISSION,
|
|
p1=10.0, p2=NAN, p3=NAN, p4=NAN,
|
|
x=0, y=0, z=0.0),
|
|
], timeout)
|
|
_check(
|
|
"NAV_DELAY valid (p1=10, p5/p6/p7=0) -> ACCEPTED",
|
|
result, MAV_MISSION_ACCEPTED,
|
|
)
|
|
|
|
# 7. NAV_TAKEOFF with unsupported param2 set (mask 0x08: only p4)
|
|
result = _upload_mission(mav, [
|
|
_item(0, CMD_NAV_TAKEOFF, MAV_FRAME_GLOBAL_INT,
|
|
p1=NAN, p2=5.0, p3=NAN, p4=NAN,
|
|
x=LAT, y=LON, z=ALT, current=1),
|
|
], timeout)
|
|
_check(
|
|
"NAV_TAKEOFF unsupported param2 -> INVALID_PARAM2",
|
|
result, MAV_MISSION_INVALID_PARAM2,
|
|
)
|
|
|
|
# 13. NAV_VTOL_LAND mission with unsupported param1 set (mission mask
|
|
# 0x78: only p4/yaw; mission_block.cpp never reads p1-p3 for VTOL_LAND).
|
|
result = _upload_mission(mav, [
|
|
_item(0, CMD_NAV_VTOL_LAND, MAV_FRAME_GLOBAL_INT,
|
|
p1=1.0, p2=NAN, p3=NAN, p4=NAN,
|
|
x=LAT, y=LON, z=ALT, current=1),
|
|
], timeout)
|
|
_check(
|
|
"NAV_VTOL_LAND mission unsupported param1 -> INVALID_PARAM1",
|
|
result, MAV_MISSION_INVALID_PARAM1,
|
|
)
|
|
|
|
# 14. NAV_VTOL_LAND mission valid (only p4/yaw set)
|
|
result = _upload_mission(mav, [
|
|
_item(0, CMD_NAV_VTOL_LAND, MAV_FRAME_GLOBAL_INT,
|
|
p1=NAN, p2=NAN, p3=NAN, p4=90.0,
|
|
x=LAT, y=LON, z=ALT, current=1),
|
|
], timeout)
|
|
_check(
|
|
"NAV_VTOL_LAND mission valid (p4=yaw only) -> ACCEPTED",
|
|
result, MAV_MISSION_ACCEPTED,
|
|
)
|
|
|
|
# 15. NAV_VTOL_TAKEOFF mission with unsupported param3 set (mission mask
|
|
# 0x78: only p4/yaw; mission_block.cpp never reads p3/approach_hdg).
|
|
result = _upload_mission(mav, [
|
|
_item(0, CMD_NAV_VTOL_TAKEOFF, MAV_FRAME_GLOBAL_INT,
|
|
p1=NAN, p2=NAN, p3=45.0, p4=NAN,
|
|
x=LAT, y=LON, z=ALT, current=1),
|
|
], timeout)
|
|
_check(
|
|
"NAV_VTOL_TAKEOFF mission unsupported param3 -> INVALID_PARAM3",
|
|
result, MAV_MISSION_INVALID_PARAM3,
|
|
)
|
|
|
|
# 16. NAV_VTOL_TAKEOFF mission valid (only p4/yaw set)
|
|
result = _upload_mission(mav, [
|
|
_item(0, CMD_NAV_VTOL_TAKEOFF, MAV_FRAME_GLOBAL_INT,
|
|
p1=NAN, p2=NAN, p3=NAN, p4=90.0,
|
|
x=LAT, y=LON, z=ALT, current=1),
|
|
], timeout)
|
|
_check(
|
|
"NAV_VTOL_TAKEOFF mission valid (p4=yaw only) -> ACCEPTED",
|
|
result, MAV_MISSION_ACCEPTED,
|
|
)
|
|
|
|
|
|
def run_command_tests(mav: Any, timeout: float) -> None:
|
|
print("\n=== Command (COMMAND_LONG) tests ===")
|
|
|
|
# 8. Valid COMPONENT_ARM_DISARM (p1=0 disarm, p2=0 no-force)
|
|
result = _send_command(
|
|
mav, CMD_COMPONENT_ARM_DISARM, timeout, p1=0.0, p2=0.0,
|
|
)
|
|
_check(
|
|
"COMPONENT_ARM_DISARM valid params -> not DENIED",
|
|
result != MAV_RESULT_DENIED, True,
|
|
)
|
|
|
|
# 9. COMPONENT_ARM_DISARM with unsupported param3 set (mask 0x03)
|
|
result = _send_command(
|
|
mav, CMD_COMPONENT_ARM_DISARM, timeout, p1=0.0, p2=0.0, p3=1.0,
|
|
)
|
|
_check(
|
|
"COMPONENT_ARM_DISARM unsupported param3 -> DENIED",
|
|
result, MAV_RESULT_DENIED,
|
|
)
|
|
|
|
# 10. NAV_RTL command with param1 set (mask 0x00)
|
|
result = _send_command(mav, CMD_NAV_RTL, timeout, p1=1.0)
|
|
_check(
|
|
"NAV_RTL command unsupported param1 -> DENIED",
|
|
result, MAV_RESULT_DENIED,
|
|
)
|
|
|
|
# 11. NAV_RTL command all params zero -> not DENIED
|
|
result = _send_command(mav, CMD_NAV_RTL, timeout)
|
|
_check(
|
|
"NAV_RTL command all params zero -> not DENIED",
|
|
result != MAV_RESULT_DENIED, True,
|
|
)
|
|
|
|
# 12. NAV_WAYPOINT command unsupported param3 (mask 0x0B)
|
|
result = _send_command(
|
|
mav, CMD_NAV_WAYPOINT, timeout, p1=0.0, p2=2.0, p3=1.0, p4=0.0,
|
|
)
|
|
_check(
|
|
"NAV_WAYPOINT command unsupported param3 -> DENIED",
|
|
result, MAV_RESULT_DENIED,
|
|
)
|
|
|
|
# 17. NAV_VTOL_LAND command with p1 (land options) set -> supported
|
|
# on the command path (mask 0x7F), unlike the mission path.
|
|
result = _send_command(
|
|
mav, CMD_NAV_VTOL_LAND, timeout, p1=1.0,
|
|
)
|
|
_check(
|
|
"NAV_VTOL_LAND command p1 (options) -> not DENIED",
|
|
result != MAV_RESULT_DENIED, True,
|
|
)
|
|
|
|
# 18. NAV_VTOL_TAKEOFF command with p1 set -> unsupported even on the
|
|
# command path (mask 0x7C: only p3/p4 + p5-7).
|
|
result = _send_command(
|
|
mav, CMD_NAV_VTOL_TAKEOFF, timeout, p1=1.0,
|
|
)
|
|
_check(
|
|
"NAV_VTOL_TAKEOFF command unsupported param1 -> DENIED",
|
|
result, MAV_RESULT_DENIED,
|
|
)
|
|
|
|
# 19. NAV_VTOL_TAKEOFF command with p3 (approach heading) set ->
|
|
# supported on the command path (mask 0x7C).
|
|
result = _send_command(
|
|
mav, CMD_NAV_VTOL_TAKEOFF, timeout, p3=45.0,
|
|
)
|
|
_check(
|
|
"NAV_VTOL_TAKEOFF command p3 (approach_hdg) -> not DENIED",
|
|
result != MAV_RESULT_DENIED, True,
|
|
)
|
|
|
|
|
|
# Entry point
|
|
|
|
|
|
def main() -> int:
|
|
parser = argparse.ArgumentParser(
|
|
description=__doc__,
|
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
)
|
|
parser.add_argument(
|
|
"--url", default="udp://:14540",
|
|
help="MAVLink connection URL (default: udp://:14540)",
|
|
)
|
|
parser.add_argument(
|
|
"--timeout", type=float, default=5.0,
|
|
help="Per-message receive timeout in seconds (default: 5)",
|
|
)
|
|
args = parser.parse_args()
|
|
|
|
mav = connect(args.url)
|
|
|
|
run_mission_tests(mav, args.timeout)
|
|
run_command_tests(mav, args.timeout)
|
|
|
|
passed = sum(_results)
|
|
total = len(_results)
|
|
print(f"\nResult: {passed}/{total} passed")
|
|
return 0 if passed == total else 1
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|