精简仓库结构并增强任务链路:归档双目可选后端、新增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。
This commit is contained in:
@@ -1,12 +1,14 @@
|
||||
"""ground_link_test.py —— 地面坐标注入通道单元测试。
|
||||
"""ground_link_test.py —— 地面站链路单元测试。
|
||||
|
||||
覆盖:parse_payload 解析(合法/缺字段/非法 JSON/非对象/类型错误)、
|
||||
is_valid 新鲜度判定(未注入/新鲜/超龄边界)。
|
||||
parse_message 双通道(坐标 + {"cmd"} 命令)、命令队列 drain、
|
||||
is_valid 新鲜度判定(未注入/新鲜/超龄边界)、run() 真 UDP 回环。
|
||||
"""
|
||||
|
||||
import pytest
|
||||
|
||||
from pod_bridge.ground_link import GroundInjectConfig, GroundLink, GroundTarget
|
||||
from pod_bridge.ground_link import (GroundCommand, GroundInjectConfig,
|
||||
GroundLink, GroundTarget)
|
||||
|
||||
|
||||
# --- parse_payload ----------------------------------------------------------
|
||||
@@ -92,3 +94,108 @@ def test_latest_returns_injected_target():
|
||||
_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"]
|
||||
|
||||
Reference in New Issue
Block a user