conftest新增fake_mavsdk fixture打桩mavsdk.offboard设定点类型, 两组网关测试autouse启用。修复树莓派原生库符号缺失时纯mock测试误失败 (该问题在树莓派实测暴露: 348条全绿不依赖mavsdk原生库)
183 lines
5.8 KiB
Python
183 lines
5.8 KiB
Python
"""mavsdk_gateway_test.py —— MavsdkCommandGateway 单元测试(mock offboard)。
|
||
|
||
移植自 reference-c++/visual_track_control/tests/mavlink_command_gateway_test.cpp。
|
||
C++ 版校验 MAVLink ID 84 线格式字节流;Python 版采用 MAVSDK 语义映射,
|
||
改为以 FakeOffboard 捕获设定点对象,校验三分支映射(物理追踪思路 §2.2、
|
||
A2 掩码三分支的语义等价):
|
||
|
||
角速率非零 → 机体速度设定点 + 非零 yawspeed;
|
||
航向角非零(角速率为零)→ NED 速度设定点 + 绝对 yaw;
|
||
两者均零 → 机体速度设定点 + 零 yawspeed(保持航向,A2 回归语义);
|
||
刹车 → 全零机体速度设定点。
|
||
传输失败(异常)→ 返回 False 且不向外抛。
|
||
"""
|
||
|
||
import asyncio
|
||
|
||
import pytest
|
||
|
||
from visual_track_control.mavsdk_gateway import MavsdkCommandGateway
|
||
from visual_track_control.types import ControlOutput
|
||
|
||
|
||
@pytest.fixture(autouse=True)
|
||
def _stub_mavsdk(fake_mavsdk):
|
||
"""本模块全部用例与真实 libcmavsdk.so 脱钩(树莓派原生库符号缺失实测)。"""
|
||
return fake_mavsdk
|
||
|
||
|
||
class FakeOffboard:
|
||
"""捕获式 offboard mock:记录每次调用的设定点,可注入失败。"""
|
||
|
||
def __init__(self):
|
||
self.body_setpoints = []
|
||
self.ned_setpoints = []
|
||
self.attempts = 0
|
||
self.start_count = 0
|
||
self.stop_count = 0
|
||
self.succeed = True
|
||
|
||
async def set_velocity_body(self, setpoint):
|
||
self.attempts += 1 # 与 C++ CaptureTransport 一致:失败也计入发送次数
|
||
if not self.succeed:
|
||
raise RuntimeError("mock transport failure")
|
||
self.body_setpoints.append(setpoint)
|
||
|
||
async def set_velocity_ned(self, setpoint):
|
||
self.attempts += 1
|
||
if not self.succeed:
|
||
raise RuntimeError("mock transport failure")
|
||
self.ned_setpoints.append(setpoint)
|
||
|
||
async def start(self):
|
||
if not self.succeed:
|
||
raise RuntimeError("mock start failure")
|
||
self.start_count += 1
|
||
|
||
async def stop(self):
|
||
self.stop_count += 1
|
||
|
||
@property
|
||
def send_count(self):
|
||
return self.attempts
|
||
|
||
|
||
class FakeDrone:
|
||
def __init__(self):
|
||
self.offboard = FakeOffboard()
|
||
|
||
|
||
def run(coro):
|
||
return asyncio.run(coro)
|
||
|
||
|
||
def make_gateway():
|
||
drone = FakeDrone()
|
||
return MavsdkCommandGateway(drone), drone.offboard
|
||
|
||
|
||
def test_start_sends_initial_setpoint_then_starts():
|
||
gateway, offboard = make_gateway()
|
||
assert run(gateway.start())
|
||
assert offboard.start_count == 1
|
||
# PX4 要求先流式发送首条设定点(全零)再请求启动
|
||
assert len(offboard.body_setpoints) == 1
|
||
sp = offboard.body_setpoints[0]
|
||
assert sp.forward_m_s == 0.0
|
||
assert sp.right_m_s == 0.0
|
||
assert sp.down_m_s == 0.0
|
||
assert sp.yawspeed_deg_s == 0.0
|
||
|
||
|
||
def test_start_failure_returns_false():
|
||
gateway, offboard = make_gateway()
|
||
offboard.succeed = False
|
||
assert not run(gateway.start())
|
||
|
||
|
||
def _make_velocity_heading_output():
|
||
from visual_track_control.types import ControlMode
|
||
out = ControlOutput()
|
||
out.mode = ControlMode.VELOCITY_HEADING
|
||
return out
|
||
|
||
|
||
def test_yaw_rate_branch_uses_body_frame_with_yawspeed():
|
||
from visual_track_control.types import ControlMode
|
||
gateway, offboard = make_gateway()
|
||
out = _make_velocity_heading_output()
|
||
out.vx_body_mps = 1.5
|
||
out.vz_mps = 0.25
|
||
out.yaw_rate_dps = 5.0
|
||
assert run(gateway.send_velocity_heading(out, 12345))
|
||
assert offboard.send_count == 1
|
||
assert len(offboard.body_setpoints) == 1
|
||
sp = offboard.body_setpoints[0]
|
||
assert sp.forward_m_s == pytest.approx(1.5)
|
||
assert sp.right_m_s == 0.0
|
||
assert sp.down_m_s == pytest.approx(0.25)
|
||
assert sp.yawspeed_deg_s == pytest.approx(5.0)
|
||
assert offboard.ned_setpoints == []
|
||
|
||
|
||
def test_yaw_only_branch_uses_ned_frame_with_absolute_yaw():
|
||
# 航向角非零且角速率为零 → 航向掩码语义:NED 速度 + 绝对航向
|
||
gateway, offboard = make_gateway()
|
||
out = _make_velocity_heading_output()
|
||
out.vx_mps = 2.0
|
||
out.vy_mps = -1.0
|
||
out.vz_mps = 0.5
|
||
out.yaw_deg = 10.0
|
||
out.yaw_rate_dps = 0.0
|
||
assert run(gateway.send_velocity_heading(out, 1))
|
||
assert offboard.send_count == 1
|
||
assert len(offboard.ned_setpoints) == 1
|
||
sp = offboard.ned_setpoints[0]
|
||
assert sp.north_m_s == pytest.approx(2.0)
|
||
assert sp.east_m_s == pytest.approx(-1.0)
|
||
assert sp.down_m_s == pytest.approx(0.5)
|
||
assert sp.yaw_deg == pytest.approx(10.0)
|
||
assert offboard.body_setpoints == []
|
||
|
||
|
||
def test_both_zero_keeps_heading_via_body_zero_yawspeed():
|
||
# A2 回归:两者均零时机体速度设定点配零角速率 = 保持当前航向,
|
||
# 不得误发绝对航向 0(正北)。
|
||
gateway, offboard = make_gateway()
|
||
out = _make_velocity_heading_output()
|
||
out.vx_body_mps = 1.0
|
||
out.yaw_rate_dps = 0.0
|
||
out.yaw_deg = 0.0
|
||
assert run(gateway.send_velocity_heading(out, 1))
|
||
assert len(offboard.body_setpoints) == 1
|
||
sp = offboard.body_setpoints[0]
|
||
assert sp.forward_m_s == pytest.approx(1.0)
|
||
assert sp.yawspeed_deg_s == 0.0
|
||
assert offboard.ned_setpoints == [] # 不得走绝对航向支路
|
||
|
||
|
||
def test_brake_hover_sends_zero_velocity_body():
|
||
gateway, offboard = make_gateway()
|
||
assert run(gateway.send_brake_hover(777))
|
||
assert offboard.send_count == 1
|
||
sp = offboard.body_setpoints[0]
|
||
assert sp.forward_m_s == 0.0
|
||
assert sp.right_m_s == 0.0
|
||
assert sp.down_m_s == 0.0
|
||
assert sp.yawspeed_deg_s == 0.0
|
||
|
||
|
||
def test_transport_failure_propagates_as_false():
|
||
gateway, offboard = make_gateway()
|
||
offboard.succeed = False
|
||
out = _make_velocity_heading_output()
|
||
assert not run(gateway.send_velocity_heading(out, 1))
|
||
assert not run(gateway.send_brake_hover(1))
|
||
assert offboard.send_count == 2
|
||
|
||
|
||
def test_stop_calls_offboard_stop():
|
||
gateway, offboard = make_gateway()
|
||
run(gateway.stop())
|
||
assert offboard.stop_count == 1
|