Files
code-test-drone/utils/tests/pod_bridge/ground_link_test.py
T
keita df91b1110e 精简仓库结构并增强任务链路:归档双目可选后端、新增LOS判据与自定义返航
归档 legacy/:stereo_yolo/stereo_ranger/target_tracker 三包 + 175 条测试 + demo/配置/权重/三份规格;运行链路不再依赖,默认 pytest 446 条,归档测试按需运行。

目标有效性第二判据改为 LOS 运动学一致性(pod_bridge/los_consistency.py),删除 require_vision;pod_locked() 叠加该判据,状态机随之后退重搜。

自定义返航(task_runner/return_home.py):保持转场高度回起飞点 → 恢复初始航向 → 垂直降落,不再爬 RTL_RETURN_ALT;未起飞/无起飞点/超时/异常仍退回 PX4 原生 RTL。

任务链安全兜底:主循环异常→紧急返航、stop 幂等、landing_stop 保留遥测、in_air 流复用+超时、解锁/起飞与吊舱识别仅执行一次、rtl_timeout 上限。

入口共享件拆到 utils/sitl_support.py;pod_sitl_test.py→l1_sitl_test.py;删除旧架构 test.py;相机脚本移入 tools/。

新增 .gitignore 并剔除历史误跟踪的 59 个 .pyc;requirements 按实际依赖重写;文档全面对齐,新增 legacy/README.md 与 tools/README.md。
2026-10-21 14:48:41 +08:00

202 lines
6.5 KiB
Python

"""ground_link_test.py —— 地面站链路单元测试。
覆盖:parse_payload 解析(合法/缺字段/非法 JSON/非对象/类型错误)、
parse_message 双通道(坐标 + {"cmd"} 命令)、命令队列 drain、
is_valid 新鲜度判定(未注入/新鲜/超龄边界)、run() 真 UDP 回环。
"""
import pytest
from pod_bridge.ground_link import (GroundCommand, GroundInjectConfig,
GroundLink, GroundTarget)
# --- parse_payload ----------------------------------------------------------
def test_parse_valid_payload():
t = GroundLink.parse_payload(
b'{"lat": 39.9042, "lon": 116.4074, "alt": 50.0}')
assert t is not None
assert t.latitude_deg == pytest.approx(39.9042)
assert t.longitude_deg == pytest.approx(116.4074)
assert t.abs_alt_m == pytest.approx(50.0)
assert t.time_ms == 0 # 打戳由接收方(run)负责
def test_parse_default_alt_is_zero():
t = GroundLink.parse_payload(b'{"lat": 1.0, "lon": 2.0}')
assert t is not None
assert t.abs_alt_m == 0.0
def test_parse_missing_lat_returns_none():
assert GroundLink.parse_payload(b'{"lon": 2.0}') is None
def test_parse_missing_lon_returns_none():
assert GroundLink.parse_payload(b'{"lat": 1.0}') is None
def test_parse_invalid_json_returns_none():
assert GroundLink.parse_payload(b'not json') is None
def test_parse_non_object_returns_none():
assert GroundLink.parse_payload(b'[1, 2, 3]') is None
def test_parse_string_coords_returns_none():
assert GroundLink.parse_payload(b'{"lat": "x", "lon": 2.0}') is None
def test_parse_bool_coords_returns_none():
# True 是 bool 不是数值 → 拒绝
assert GroundLink.parse_payload(b'{"lat": true, "lon": 2.0}') is None
def test_parse_ignores_ts_field():
"""ts 字段仅诊断,不参与解析(新鲜度用本地接收时刻)。"""
t = GroundLink.parse_payload(b'{"lat": 1.0, "lon": 2.0, "ts": 999}')
assert t is not None
assert t.latitude_deg == pytest.approx(1.0)
# --- is_valid ---------------------------------------------------------------
def _link(timeout_ms=5000):
return GroundLink(GroundInjectConfig(timeout_ms=timeout_ms),
now_ms_fn=lambda: 0)
def _inject(link, time_ms):
link._target = GroundTarget(latitude_deg=1.0, longitude_deg=2.0,
time_ms=time_ms)
def test_is_valid_false_when_never_injected():
assert _link().is_valid(1000) is False
def test_is_valid_true_when_fresh():
link = _link(timeout_ms=5000)
_inject(link, 1000)
assert link.is_valid(1000) is True
assert link.is_valid(4000) is True
def test_is_valid_timeout_boundary():
link = _link(timeout_ms=5000)
_inject(link, 1000)
assert link.is_valid(6000) is True # 恰好 5000ms,未超龄
assert link.is_valid(6001) is False # 5001ms,超龄
def test_latest_returns_injected_target():
link = _link()
_inject(link, 1000)
assert link.latest().latitude_deg == pytest.approx(1.0)
assert link.latest().time_ms == 1000
# --- parse_message(坐标 + 命令双通道)---------------------------------------
def test_parse_message_command():
m = GroundLink.parse_message(b'{"cmd": "rtl"}')
assert isinstance(m, GroundCommand)
assert m.name == "rtl"
def test_parse_message_command_normalized():
m = GroundLink.parse_message(b'{"cmd": " RTL "}')
assert isinstance(m, GroundCommand)
assert m.name == "rtl"
def test_parse_message_cmd_takes_precedence_over_coords():
m = GroundLink.parse_message(b'{"cmd": "emergency", "lat": 1.0, "lon": 2.0}')
assert isinstance(m, GroundCommand)
assert m.name == "emergency"
def test_parse_message_coords_still_work():
m = GroundLink.parse_message(b'{"lat": 1.0, "lon": 2.0}')
assert isinstance(m, GroundTarget)
assert m.latitude_deg == pytest.approx(1.0)
def test_parse_message_empty_cmd_falls_back_to_coords():
m = GroundLink.parse_message(b'{"cmd": "", "lat": 1.0, "lon": 2.0}')
assert isinstance(m, GroundTarget)
def test_parse_message_non_string_cmd_falls_back_to_coords():
m = GroundLink.parse_message(b'{"cmd": 5, "lat": 1.0, "lon": 2.0}')
assert isinstance(m, GroundTarget)
def test_parse_message_invalid_returns_none():
assert GroundLink.parse_message(b"not json") is None
# --- drain_commands ---------------------------------------------------------
def test_drain_commands_empty():
assert _link().drain_commands() == []
def test_drain_commands_fifo_and_clear():
link = _link()
link._commands.append("rtl")
link._commands.append("pod_center")
assert link.drain_commands() == ["rtl", "pod_center"]
assert link.drain_commands() == [] # 取后清空
def test_commands_queue_drops_oldest_when_full():
link = _link()
for i in range(20):
link._commands.append(f"c{i}")
cmds = link.drain_commands()
assert len(cmds) == 16 # maxlen=16
assert cmds[0] == "c4" and cmds[-1] == "c19"
# --- run() 真 UDP 回环(坐标 + 命令)-----------------------------------------
def test_run_receives_coords_and_commands():
import asyncio
import socket as socket_mod
probe = socket_mod.socket(socket_mod.AF_INET, socket_mod.SOCK_DGRAM)
probe.bind(("127.0.0.1", 0))
port = probe.getsockname()[1]
probe.close()
link = GroundLink(GroundInjectConfig(host="127.0.0.1", port=port),
now_ms_fn=lambda: 1234)
addr = ("127.0.0.1", port)
async def scenario():
stop = asyncio.Event()
task = asyncio.ensure_future(link.run(stop))
udp = socket_mod.socket(socket_mod.AF_INET, socket_mod.SOCK_DGRAM)
try:
for _ in range(200): # 重发防 bind 竞态
udp.sendto(b'{"lat": 1.5, "lon": 2.5}', addr)
await asyncio.sleep(0.01)
if link.latest().time_ms == 1234:
break
udp.sendto(b'{"cmd": "rtl"}', addr)
for _ in range(100):
if link._commands:
break
await asyncio.sleep(0.01)
finally:
udp.close()
stop.set()
task.cancel() # 收包循环阻塞在 sock_recvfrom,须 cancel 唤醒(同 SITL 入口)
try:
await task
except asyncio.CancelledError:
pass
asyncio.run(scenario())
assert link.latest().latitude_deg == pytest.approx(1.5)
assert link.latest().time_ms == 1234
assert link.drain_commands() == ["rtl"]