精简仓库结构并增强任务链路:归档双目可选后端、新增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:
keita
2026-10-21 14:48:41 +08:00
parent 53fdd3b13f
commit df91b1110e
159 changed files with 6121 additions and 1617 deletions
+110 -3
View File
@@ -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"]