Files
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

205 lines
7.7 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""log_test.py —— 任务日志模块单元测试。
覆盖:open 建目录/写表头、事件 JSONL、遥测 CSV、telemetry_every_n 节流、
disabled 时零副作用、close 收尾、简洁任务简报(task_brief.log)。
"""
import csv
import json
import pytest
from task_runner.log import TaskLogConfig, TaskLogger, TelemetryRecord
def _cfg(tmp_path, **kw):
c = TaskLogConfig(dir=str(tmp_path), **kw)
return c
def test_open_creates_dir_and_csv_header(tmp_path):
logger = TaskLogger(_cfg(tmp_path))
logger.open()
logger.close()
assert (tmp_path / "task_events.jsonl").exists()
assert (tmp_path / "task_telemetry.csv").exists()
with open(tmp_path / "task_telemetry.csv", encoding="utf-8") as f:
header = next(csv.reader(f))
assert header[0] == "t_s"
assert "state" in header
assert "distance_to_target_m" in header
def test_log_event_writes_jsonl(tmp_path):
logger = TaskLogger(_cfg(tmp_path))
logger.open()
logger.log_event(1000, "state", from_state="wait",
to_state="arm_takeoff", reason="收到地面坐标")
logger.log_event(2000, "emergency", reason="RC 接管")
logger.close()
with open(tmp_path / "task_events.jsonl", encoding="utf-8") as f:
lines = [json.loads(x) for x in f if x.strip()]
assert len(lines) == 2
assert lines[0]["type"] == "state"
assert lines[0]["from_state"] == "wait"
assert lines[0]["to_state"] == "arm_takeoff"
assert lines[0]["ts_ms"] == 1000
assert lines[0]["t_s"] == 0.0 # 首个事件 t_s = 0
assert lines[1]["t_s"] == 1.0 # 第二个事件 t_s = 1.0s
def test_log_telemetry_writes_csv_rows(tmp_path):
logger = TaskLogger(_cfg(tmp_path))
logger.open()
rec = TelemetryRecord(state="goto", pos_valid=True, latitude_deg=47.39,
distance_to_target_m=33.5, mode="VELOCITY_HEADING",
yaw_rate_dps=-30.0, vx_body_mps=1.7)
logger.log_telemetry(1000, rec)
logger.log_telemetry(1100, rec)
logger.close()
with open(tmp_path / "task_telemetry.csv", encoding="utf-8") as f:
rows = list(csv.DictReader(f))
assert len(rows) == 2
assert rows[0]["state"] == "goto"
assert rows[0]["pos_valid"] == "True"
assert rows[0]["distance_to_target_m"] == "33.5"
assert rows[0]["t_s"] == "0.0"
assert rows[1]["t_s"] == "0.1"
def test_telemetry_throttled_by_every_n(tmp_path):
logger = TaskLogger(_cfg(tmp_path, telemetry_every_n=5))
logger.open()
rec = TelemetryRecord(state="goto")
for i in range(12):
logger.log_telemetry(1000 + i * 100, rec)
logger.close()
with open(tmp_path / "task_telemetry.csv", encoding="utf-8") as f:
rows = list(csv.DictReader(f))
assert len(rows) == 2 # 第 5、10 次各记一次
def test_disabled_logs_nothing(tmp_path):
logger = TaskLogger(_cfg(tmp_path, enabled=False))
logger.open()
logger.log_event(1000, "state", from_state="wait", to_state="arm_takeoff")
logger.log_telemetry(1000, TelemetryRecord(state="goto"))
logger.close()
# 未 open(disabled)→ 不建目录、不写文件
assert not (tmp_path / "task_events.jsonl").exists()
assert not (tmp_path / "task_telemetry.csv").exists()
def test_log_before_open_is_noop(tmp_path):
logger = TaskLogger(_cfg(tmp_path))
logger.log_event(1000, "state", from_state="wait", to_state="arm_takeoff")
logger.log_telemetry(1000, TelemetryRecord(state="goto"))
logger.close()
assert not (tmp_path / "task_events.jsonl").exists()
def test_append_keeps_existing_rows(tmp_path):
"""重复 open(追加模式)不清空既有内容,表头不重复写。"""
logger = TaskLogger(_cfg(tmp_path))
logger.open()
logger.log_telemetry(1000, TelemetryRecord(state="wait"))
logger.close()
logger2 = TaskLogger(_cfg(tmp_path))
logger2.open()
logger2.log_telemetry(2000, TelemetryRecord(state="goto"))
logger2.close()
with open(tmp_path / "task_telemetry.csv", encoding="utf-8") as f:
rows = list(csv.DictReader(f))
assert len(rows) == 2
assert rows[0]["state"] == "wait"
assert rows[1]["state"] == "goto"
# --- 简洁任务简报(task_brief.log)-------------------------------------------
def test_brief_log_lines_and_summary(tmp_path):
logger = TaskLogger(_cfg(tmp_path))
logger.open()
logger.log_event(1000, "state", from_state="wait", to_state="wait",
reason="任务启动")
logger.log_event(2000, "state", from_state="wait", to_state="arm_takeoff",
reason="收到地面坐标")
logger.log_event(3000, "command", name="rtl")
logger.log_event(4000, "fc_status", severity="WARNING", text="Low battery\t")
logger.log_event(5000, "takeoff", altitude_m=5.0)
logger.log_event(6000, "rtl")
logger.log_telemetry(1000, TelemetryRecord(
state="wait", relative_altitude_m=0.0, distance_to_target_m=33.5))
logger.log_telemetry(1100, TelemetryRecord(
state="arm_takeoff", relative_altitude_m=5.2, distance_to_target_m=20.0))
logger.log_telemetry(1200, TelemetryRecord(
state="arm_takeoff", relative_altitude_m=5.0,
distance_to_target_m=float("inf")))
logger.close()
text = (tmp_path / "task_brief.log").read_text(encoding="utf-8")
assert "任务开始" in text
assert "[00:00.0] 任务启动(当前: 待命)" in text
assert "[00:01.0] 阶段: 待命 → 起飞(收到地面坐标)" in text
assert "[00:02.0] 地面站命令: rtl" in text
assert "[00:03.0] 飞控 WARNING: Low battery" in text # 尾随 tab 已 strip
assert "[00:04.0] 起飞至 5.0 m" in text
assert "[00:05.0] 开始返航" in text
assert "本次运行汇总" in text
assert "时长 0.2s" in text # 最后一行遥测的 t_s
assert "最高 5.2m" in text
assert "最近距目标 20.0m" in text # inf 不计入
assert "事件 6 条" in text
assert "状态链 待命→起飞" in text
def test_brief_required_no_file_when_disabled_or_closed(tmp_path):
logger = TaskLogger(_cfg(tmp_path, enabled=False))
logger.open()
logger.log_event(1000, "rtl")
logger.close()
assert not (tmp_path / "task_brief.log").exists()
# --- 遥测落盘节流(2026-10-15:主循环 20Hz 不得每行同步 flush)----------------
def test_telemetry_buffered_until_flush_interval(tmp_path):
"""telemetry_flush_ms 内只写不 flush;close 兜底落盘,一行不丢。"""
logger = TaskLogger(_cfg(tmp_path, telemetry_flush_ms=1000))
logger.open()
rec = TelemetryRecord(state="track")
logger.log_telemetry(1000, rec) # 首行:落盘一次并打戳
for i in range(1, 19): # 其后 19 行都在 1s 内 → 不再 flush
logger.log_telemetry(1000 + i * 50, rec)
logger.close()
with open(tmp_path / "task_telemetry.csv", encoding="utf-8") as f:
rows = list(csv.DictReader(f))
assert len(rows) == 19
def test_telemetry_flush_zero_flushes_every_row(tmp_path):
"""telemetry_flush_ms=0 保留旧的「每行立即 flush」行为(兼容/调试)。"""
logger = TaskLogger(_cfg(tmp_path, telemetry_flush_ms=0))
logger.open()
logger.log_telemetry(1000, TelemetryRecord(state="goto"))
assert logger._last_tel_flush_ms == 1000
logger.close()
def test_events_still_flushed_immediately(tmp_path):
"""事件(阶段切换)仍即时落盘:地面站页面实时性不受遥测节流影响。"""
logger = TaskLogger(_cfg(tmp_path, telemetry_flush_ms=10_000))
logger.open()
logger.log_event(1000, "state", from_state="goto", to_state="search")
with open(tmp_path / "task_events.jsonl", encoding="utf-8") as f:
lines = [json.loads(x) for x in f if x.strip()]
assert len(lines) == 1
logger.close()