实现预定流程全链路: 任务状态机 + 编排器 + 地面站 + 真机/SITL入口
- utils/task_runner: 6态状态机(WAIT→ARM_TAKEOFF→GOTO(L2)→TRACK(L1)→APPROACH→RTL) + 编排器(快照采集/状态动作) + 双层日志(事件JSONL/遥测CSV) + 吊舱控制指令(0x07/0x0F/0x0E/0x08) - pod_bridge: ground_link 地面坐标UDP注入(超龄判无效,无激光替代表4); set_heading_servo_enabled() 运行时L1/L2切换(切换即reset清积分防输出跳变); pod_locked 提为公共门控; config 新增 ground_inject 段 - virtual_pod 模拟吊舱(无电机语义) + verify_px4 增量④仅飞控验证 + pod_sitl_test L1 SITL全流程(飞行编排不塞进 run_bridge, 保持纯offboard闭环入口) - 入口: task_runner_sitl(VirtualPod+虚拟目标, SITL端到端跑通约1m24s) / task_runner_real(真串口+UDP注入+吊舱控制) - 地面站 ground_station: 坐标注入 + 状态可视化(Flask) - S1标定回填(2026-10-09): beta_sign=+1 / alpha_sign=-1, 加标定回归测试守护 - 新增测试: task_runner x4 + ground_link/virtual_pod*/bridge/px4_gateway 增量; 全量 483 passed - 文档: 预定流程(需求) + 预定流程-操作指南(操作/待办) + L1方案补S1结论与实现状态
This commit is contained in:
@@ -0,0 +1,123 @@
|
||||
"""log_test.py —— 任务日志模块单元测试。
|
||||
|
||||
覆盖:open 建目录/写表头、事件 JSONL、遥测 CSV、telemetry_every_n 节流、
|
||||
disabled 时零副作用、close 收尾。
|
||||
"""
|
||||
|
||||
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"
|
||||
Reference in New Issue
Block a user