Files
code-test-drone/task_runner_real.py
T
keita ded17d7995 实现预定流程全链路: 任务状态机 + 编排器 + 地面站 + 真机/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结论与实现状态
2026-10-13 15:50:32 +08:00

166 lines
6.4 KiB
Python
Raw 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.
#!/usr/bin/env python3
"""task_runner_real.py —— 真机任务编排入口(真实吊舱串口 + 地面站 UDP 注入 + PX4)。
与 task_runner_sitl.py(仿真)的差异:
* 吊舱 = PodSerial(真实串口表2-2 遥测),非 VirtualPod;
* 地面坐标 = GroundLink.run()(UDP 接收地面站 JSON),非 inject() 直接注入;
* 吊舱目标识别/锁定 = PodController(GOTO 进入发 0x07 识别 + 0x0F 自动锁定,
RTL 前发 0x0E 停止 + 0x08 识别关);
* PX4 连接 = cfg.px4.connection_string(真机串口/遥测),非 SITL 14540。
前置(真机联调增量:纯软单测 → 串口回环 → 仅吊舱 → 仅飞控 → 地面联调):
1. 填 utils/config/pod_bridge.json 的 pod_serial.port / px4.connection_string;
2. 吊舱上电、串口连通(run_bridge.py --dry-run 已验证遥测);
3. 地面站往 ground_inject.port(默认 14555)发 JSON 坐标
{"lat":..,"lon":..,"alt":..}(每 0.5~1s 一次)。
用法:
python task_runner_real.py
python task_runner_real.py --config utils/config/pod_bridge.json --target-type 1
"""
import argparse
import asyncio
import logging
import sys
import time
from datetime import datetime
from pathlib import Path
_ROOT = Path(__file__).resolve().parent
_UTILS = _ROOT / "utils"
for _p in (_UTILS, _ROOT):
if str(_p) not in sys.path:
sys.path.insert(0, str(_p))
from mavsdk import System
from pod_bridge.bridge import PodBridge
from pod_bridge.config import load_from_json
from pod_bridge.ground_link import GroundLink
from pod_bridge.pod_serial import PodSerial
from pod_bridge.px4_gateway import Px4Gateway
from task_runner.log import TaskLogConfig, TaskLogger
from task_runner.pod_control import PodController
from task_runner.runner import TaskRunner
from task_runner.state_machine import TaskStateConfig, TaskStateMachine
from pod_sitl_test import now_ms, print_flight_mode, set_telemetry_rates, wait_until_ready
logger = logging.getLogger("task_real")
CONFIG_PATH = _UTILS / "config" / "pod_bridge.json"
TASK_TIMEOUT_S = 300.0 # 全流程总超时(真机放宽)
def build_sm_config() -> TaskStateConfig:
"""真机状态机参数。⚠ 全部为占位值,须台架标定后回填,标定前不得实飞。"""
return TaskStateConfig(
takeoff_altitude_m=5.0,
switch_radius_m=30.0, # GOTO→TRACK 切换半径(待标定)
goto_timeout_s=180.0,
track_timeout_s=180.0,
center_hold_s=3.0,
center_threshold_deg=3.0,
approach_hover_s=5.0,
loop_frequency_hz=20.0,
)
async def run(args) -> int:
cfg = load_from_json(args.config)
if not cfg.pod_serial.port:
print("! 未配置吊舱串口:在 pod_bridge.json 填 pod_serial.port,或用 --port 覆盖")
return 2
if args.port:
cfg.pod_serial.port = args.port
cfg.ground_inject.enabled = True # 地面注入必须开(真机无激光,表4 永不出)
sm_cfg = build_sm_config()
drone = System()
await drone.connect(system_address=args.conn or cfg.px4.connection_string)
try:
await wait_until_ready(drone)
except RuntimeError as e:
print(f"! {e}")
return 1
await set_telemetry_rates(drone)
# 装配:真吊舱串口 + 地面站 UDP 注入 + 吊舱识别/锁定控制
px4 = Px4Gateway(drone)
ground = GroundLink(cfg.ground_inject, now_ms_fn=now_ms)
pod = PodSerial(cfg.pod_serial.port, cfg.pod_serial.baudrate)
bridge = PodBridge(cfg, px4, pod, now_ms_fn=now_ms,
ground_target_fn=lambda: ground.latest())
sm = TaskStateMachine(sm_cfg)
pod_control = PodController(bridge, target_type=args.target_type)
log_dir = args.log_dir or f"logs/real_{datetime.now().strftime('%Y%m%d_%H%M%S')}"
task_log = TaskLogger(TaskLogConfig(dir=log_dir), now_ms_fn=now_ms)
runner = TaskRunner(cfg, sm_cfg, px4, bridge, sm, ground, drone,
now_ms_fn=now_ms, logger=task_log, pod_control=pod_control)
stop = asyncio.Event()
ground_task = asyncio.ensure_future(ground.run(stop)) # UDP 接收地面站坐标
runner_task = asyncio.ensure_future(runner.run(stop))
print(f"-- 启动真机任务全流程(吊舱 {cfg.pod_serial.port},"
f"地面站 UDP {cfg.ground_inject.port},目标类型 {args.target_type})")
print(f" 等待地面站注入坐标(每 0.5~1s 一次 JSON)… 日志 → {log_dir}")
started = time.monotonic()
last_state = None
try:
while not runner.rtl_done:
await asyncio.sleep(1.0)
if runner.state is not last_state:
last_state = runner.state
print(f"[任务] 进入状态: {runner.state.value}")
if runner_task.done():
exc = runner_task.exception()
if exc is not None:
print(f"! runner 异常退出: {type(exc).__name__}: {exc}")
break
if time.monotonic() - started > TASK_TIMEOUT_S:
print("! 全流程超时,强制返航")
runner.request_rtl()
if runner.rtl_done:
print("-- 任务完成(RTL 落地)")
except asyncio.CancelledError:
raise
finally:
stop.set()
ground_task.cancel()
runner_task.cancel()
try:
await runner_task
except asyncio.CancelledError:
pass
try:
await ground_task
except asyncio.CancelledError:
pass
await print_flight_mode(drone)
print("\n===== 真机任务全流程完成 =====")
return 0
def main() -> int:
ap = argparse.ArgumentParser(description="真机任务状态机全流程")
ap.add_argument("--config", default=str(CONFIG_PATH), help="桥接配置 JSON 路径")
ap.add_argument("--conn", default=None, help="MAVSDK 连接串(覆盖 px4.connection_string)")
ap.add_argument("--port", help="覆盖吊舱串口设备号(如 /dev/ttyAMA0)")
ap.add_argument("--target-type", type=int, default=1,
help="吊舱目标类型:0=人/车,1=无人机")
ap.add_argument("--log-dir", default=None, help="日志输出目录(默认按时间戳)")
args = ap.parse_args()
logging.basicConfig(level=logging.INFO,
format="%(asctime)s.%(msecs)03d %(levelname)s %(message)s",
datefmt="%H:%M:%S")
return asyncio.run(run(args))
if __name__ == "__main__":
sys.exit(main())