685 lines
29 KiB
Python
685 lines
29 KiB
Python
"""
|
||
run_mission.py — 基于 ZMQ Router 协议的主任务入口
|
||
|
||
【架构概览】
|
||
本脚本是整个无人机任务系统的总装粘合剂。采用领域驱动模块化设计,
|
||
将各子系统按职责独立实例化,在主循环中进行控制权协调。
|
||
|
||
┌─ MissionOrchestrator ──────────┐
|
||
│ RouterProxy (shared) │ 单一共享 ZMQ 代理
|
||
└──────────┬─────────────────────┘
|
||
│ 注入
|
||
┌─────┴──────┐
|
||
▼ ▼
|
||
┌─ MissionManager ┌─ UAVControlLoop ──┐
|
||
│ 状态机 + 航点巡航 │ 20Hz VELOCITY │
|
||
│ POSITION SETPOINT │ 闭环追踪 │
|
||
│ (非追踪期间) │ (VISUAL_TRACKING) │
|
||
└───────────────────┴────────────────────┘
|
||
┌─ 感知模块 ─────────┐
|
||
│ VideoStreaming │ 摄像头/RTSP 采集
|
||
│ YOLO26UAVInfer │ 昇腾 NPU 推理
|
||
└───────────────────┘
|
||
|
||
【控制权协调规则】
|
||
同一时刻只有一个人持有「SETPOINT 发令权」:
|
||
- VISUAL_TRACKING 之外 → MissionManager 发 POSITION SETPOINT
|
||
- VISUAL_TRACKING 之中 → UAVControlLoop 线程发 VELOCITY SETPOINT
|
||
- 切换由主循环检测 MissionManager.state 变化时触发
|
||
|
||
【安全设计】
|
||
- try / except FailsafeTriggered / finally 全面资源释放
|
||
- MissionManager 和 UAVControlLoop 共享同一 RouterProxy,
|
||
通过控制权协调避免 REQ socket 并发冲突
|
||
- 控制线程启停由状态机状态变化驱动,逻辑可预测
|
||
|
||
【可扩展的图像输入接口】
|
||
如需替换视频源(如模拟器/录播文件),替换 VideoStreaming 实例
|
||
即可,保持 read_frame() → (ret, frame) 的协议不变。
|
||
"""
|
||
|
||
import json
|
||
import logging
|
||
import os
|
||
import sys
|
||
import threading
|
||
import time
|
||
import socket
|
||
import cv2
|
||
|
||
from drone_controller.mission_manager import (
|
||
MissionManager,
|
||
MissionState,
|
||
FailsafeTriggered,
|
||
)
|
||
# ---- ZMQ Router 代理与状态机 ----
|
||
from drone_controller.router_proxy import RouterProxy
|
||
# ---- 视觉闭环控制(复用 infer_camera_modular 现有实现) ----
|
||
# 注意:此处不修改 UAVControlLoop 任何内部逻辑。
|
||
# run_mission.py 在外部协调其控制线程的启停。
|
||
from infer_camera_modular import UAVControlLoop, VideoStreaming, YOLO26UAVInfer
|
||
# 注:coord.latlon_to_ned 不再需要 — 航点直接存储原始经纬度
|
||
# ---- 飞行日志 ----
|
||
from utils.flight_logger import FlightLogger
|
||
# ---- 光流模块 ----
|
||
from drone_controller.optical_flow_controller import OpticalFlowObstacleDetector
|
||
|
||
logger = logging.getLogger("RunMission")
|
||
|
||
|
||
class MissionOrchestrator:
|
||
"""
|
||
🎯 全任务编排器
|
||
|
||
职责:
|
||
1. 创建并初始化所有子系统
|
||
2. 在主循环中协调 MissionManager 状态机与 UAVControlLoop 的控制权
|
||
3. 处理 Failsafe 熔断、键盘中断等异常退出
|
||
4. 提供标准视频源接口(可替换)
|
||
"""
|
||
|
||
def __init__(self,waypoints,config_path="./config.json"):
|
||
self.logger = logging.getLogger("Orchestrator")
|
||
self.logger.info("正在加载配置: %s", config_path)
|
||
|
||
with open(config_path, "r", encoding="utf-8") as f:
|
||
cfg = json.load(f)
|
||
self.cfg = cfg
|
||
|
||
# ---- 感知子系统 ----
|
||
self.streamer = VideoStreaming(cfg["video"], cfg["network"])
|
||
self.detector = YOLO26UAVInfer(cfg["model"])
|
||
|
||
# ---- 航线定义(经纬度坐标系,Z 向上为负) ----
|
||
self.waypoints = waypoints
|
||
|
||
# ================================================================
|
||
# csv飞行日志参数
|
||
# ================================================================
|
||
log_cfg = cfg.get("log", {})
|
||
self.flight_logger = FlightLogger(log_cfg)
|
||
self._latest_detections = None
|
||
|
||
# ================================================================
|
||
# 🚁 MissionManager(状态机 + 航点巡航)
|
||
# ================================================================
|
||
# 与 UAVControlLoop 共享同一个 RouterProxy(见下方)。
|
||
fc = cfg["flight_control"]
|
||
self.proxy = RouterProxy(
|
||
req_endpoint=fc.get("req_endpoint", "tcp://127.0.0.1:5555"),
|
||
sub_endpoint=fc.get("sub_endpoint", "tcp://127.0.0.1:5556"),
|
||
)
|
||
|
||
self.proxy.set_waypoints_callback(self._on_qgc_waypoints)
|
||
|
||
missions = cfg["missions"]
|
||
landing_cfg = missions.get("landing", {})
|
||
self.mission = MissionManager(
|
||
proxy=self.proxy,
|
||
waypoints=self.waypoints,
|
||
target_altitude=missions.get("target_altitude", 3.0),
|
||
arrival_radius=missions.get("arrival_radius", 0.3),
|
||
hold_duration=missions.get("hold_duration", 5.0),
|
||
return_to_home=missions.get("return_to_home", True),
|
||
max_speed=missions.get("max_speed", 1.2),
|
||
pre_land_hover_duration=landing_cfg.get("pre_hover_duration", 3.0),
|
||
landing_descent_speed=landing_cfg.get("descent_speed", 0.3),
|
||
)
|
||
|
||
# ================================================================
|
||
# 💡 OpticalFlowObstacleDetector (光流计算)
|
||
# ================================================================
|
||
flow_cfg = cfg.get("optical_flow", {})
|
||
self.optical_flow = OpticalFlowObstacleDetector(
|
||
grid_step=flow_cfg.get("grid_step", 25),
|
||
reset_threshold=flow_cfg.get("reset_threshold", 30.0)
|
||
)
|
||
|
||
# ================================================================
|
||
# 🛸 UAVControlLoop(20Hz VELOCITY 闭环追踪)
|
||
# ================================================================
|
||
# 注入与 MissionManager 相同的 RouterProxy 实例,避免两个独立
|
||
# SUB 线程和 REQ socket 竞争同一 Router。
|
||
self.controller = UAVControlLoop(
|
||
cfg["flight_control"], cfg["pid_y"], cfg["pid_z"],
|
||
proxy=self.proxy,
|
||
)
|
||
self._control_active = False # 控制线程运行标志
|
||
|
||
# ---- 主循环参数 ----
|
||
self.running = False
|
||
self.loop_interval = 0.05 # 20Hz
|
||
# ---- TCP 命令通道(急停 / 后续扩展) ----
|
||
self.tcp_port = cfg.get("command_channel", {}).get("tcp_port", 9999)
|
||
self.tcp_server_socket = None
|
||
self.tcp_server_running = False
|
||
self.tcp_server_thread = None
|
||
self.tcp_clients = [] # 保存已连接的客户端 socket(用于广播)
|
||
|
||
self.logger.info("MissionOrchestrator 初始化完成")
|
||
|
||
# ================================================================
|
||
# 📥 QGC 航点回调(由 RouterProxy 的 SUB 线程触发)
|
||
# ================================================================
|
||
|
||
def _on_qgc_waypoints(self, data: dict):
|
||
"""
|
||
收到 QGC 下发的航点列表 → 直接存储原始经纬度 → 注入 MissionManager
|
||
|
||
【协议变更 v2】
|
||
航点格式已从 NED (x, y, z) 切换为经纬度 (lat, lon, alt)。
|
||
send_setpoint 的 x=纬度, y=经度, z=相对高度(正值向上)。
|
||
因此不再调用 latlon_to_ned 转换,直接存储原始数据。
|
||
"""
|
||
try:
|
||
raw_wps = data.get("waypoints", [])
|
||
if not raw_wps:
|
||
self.logger.warning("QGC 航点列表为空,请检查是否提交或路由问题")
|
||
return
|
||
|
||
self.logger.info("📥 收到 %d 个航点,直接存储经纬度", len(raw_wps))
|
||
|
||
# ---- ① 获取 STATE 用于后续校验(不再需要参考原点) ----
|
||
state = self.proxy.get_latest_state()
|
||
if state is None:
|
||
self.logger.warning("无法获取 STATE,航点仍将加载但 HOME 校验跳过")
|
||
return
|
||
|
||
# ---- ② 逐点存储(原始经纬度,不做 NED 转换) ----
|
||
converted = []
|
||
for idx, wp in enumerate(raw_wps):
|
||
lat = wp.get("lat", 0.0)
|
||
lon = wp.get("lon", 0.0)
|
||
alt = wp.get("z", 0.0) # QGC 中 z 为相对高度(正值向上)
|
||
cmd = wp.get("command", 16) # 16=WAYPOINT, 22=TAKEOFF, 21=LAND
|
||
yaw = wp.get("param4", 0.0) # param4 通常存偏航角
|
||
hold_time = wp.get("param1", 8.0) # 悬停时间(如有)
|
||
|
||
# 根据 MAV_CMD 类型构建内部航点
|
||
if cmd == 22: # TAKEOFF
|
||
converted.append({
|
||
"lat": lat,
|
||
"lon": lon,
|
||
"alt": alt,
|
||
"yaw": yaw,
|
||
"command": "TAKEOFF",
|
||
"hold_duration": hold_time,
|
||
})
|
||
self.logger.info("航点 %d [TAKEOFF]: lat=%.6f lon=%.6f alt=%.1fm",
|
||
idx, lat, lon, alt)
|
||
|
||
elif cmd == 21: # LAND
|
||
converted.append({
|
||
"lat": lat,
|
||
"lon": lon,
|
||
"alt": alt,
|
||
"yaw": yaw,
|
||
"command": "LAND",
|
||
})
|
||
self.logger.info("航点 %d [LAND]: lat=%.6f lon=%.6f", idx, lat, lon)
|
||
|
||
else: # 普通航点 (command=16 或未识别)
|
||
converted.append({
|
||
"lat": lat,
|
||
"lon": lon,
|
||
"alt": alt,
|
||
"yaw": yaw,
|
||
"command": "WAYPOINT",
|
||
"hold_duration": hold_time,
|
||
})
|
||
self.logger.info("航点 %d [WAYPOINT]: lat=%.6f lon=%.6f alt=%.1f",
|
||
idx, lat, lon, alt)
|
||
|
||
self.mission.set_waypoints(converted)
|
||
|
||
# 如果当前处于等待航点状态,状态机会在下一帧自动响应
|
||
self.logger.info("✅ 航点加载完成,共 %d 个航点(原始经纬度格式)", len(converted))
|
||
|
||
except Exception as e:
|
||
self.logger.error("航点处理异常: %s", e)
|
||
|
||
# ================================================================
|
||
# 🖥️ TCP Server
|
||
# ================================================================
|
||
# run_mission.py — MissionOrchestrator 类中新增
|
||
|
||
def _start_tcp_command_server(self):
|
||
"""启动 TCP 命令服务器(独立线程)"""
|
||
if self.tcp_server_thread and self.tcp_server_thread.is_alive():
|
||
return
|
||
|
||
self.tcp_server_running = True
|
||
self.tcp_server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||
self.tcp_server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||
self.tcp_server_socket.bind(("0.0.0.0", self.tcp_port))
|
||
self.tcp_server_socket.listen(5)
|
||
self.tcp_server_socket.settimeout(1.0) # 用于检查 self.tcp_server_running
|
||
|
||
self.tcp_server_thread = threading.Thread(
|
||
target=self._tcp_command_server_worker,
|
||
daemon=True,
|
||
name="tcp-command-server"
|
||
)
|
||
self.tcp_server_thread.start()
|
||
self.logger.info(f"🔌 TCP 命令通道已启动: port={self.tcp_port} (指令: STOP)")
|
||
|
||
def _tcp_command_server_worker(self):
|
||
"""TCP 服务器主循环:接受客户端,分发处理"""
|
||
while self.tcp_server_running:
|
||
try:
|
||
client_sock, addr = self.tcp_server_socket.accept()
|
||
self.logger.info(f"📡 地面站已连接: {addr[0]}:{addr[1]}")
|
||
# 为每个客户端创建独立处理线程(支持多地面站同时连接)
|
||
client_thread = threading.Thread(
|
||
target=self._handle_tcp_client,
|
||
args=(client_sock, addr),
|
||
daemon=True
|
||
)
|
||
client_thread.start()
|
||
except socket.timeout:
|
||
continue
|
||
except Exception as e:
|
||
if self.tcp_server_running:
|
||
self.logger.error(f"TCP Server 异常: {e}")
|
||
break
|
||
|
||
# 清理
|
||
if self.tcp_server_socket:
|
||
try:
|
||
self.tcp_server_socket.close()
|
||
except Exception:
|
||
pass
|
||
self.tcp_server_socket = None
|
||
self.logger.info("🔌 TCP 命令通道已关闭")
|
||
|
||
def _handle_tcp_client(self, client_sock, addr):
|
||
"""处理单个 TCP 客户端连接(长连接)"""
|
||
client_sock.settimeout(1.0) # 读超时,便于检测连接断开
|
||
try:
|
||
while self.tcp_server_running:
|
||
try:
|
||
data = client_sock.recv(1024).decode().strip()
|
||
if not data:
|
||
# 客户端主动断开
|
||
self.logger.info(f"📡 地面站断开: {addr[0]}:{addr[1]}")
|
||
break
|
||
|
||
msg = data.upper()
|
||
self.logger.info(f"📨 收到指令: {msg} (来自 {addr[0]}:{addr[1]})")
|
||
|
||
if msg == "STOP":
|
||
self.logger.warning(f"🚨 触发紧急降落 (指令来自 {addr[0]}:{addr[1]})")
|
||
self.mission.emergency_stop()
|
||
self.running = False
|
||
client_sock.send(b"ACK: EMERGENCY_STOP_TRIGGERED\n")
|
||
# 触发后不立即断开,让客户端收到 ACK
|
||
# 但主循环即将退出,服务器也会关闭
|
||
|
||
elif msg == "PING":
|
||
# 心跳 / 保活响应
|
||
client_sock.send(b"PONG\n")
|
||
|
||
elif msg == "STATUS":
|
||
# 返回当前状态(便于调试)
|
||
status = f"STATE:{self.mission.state.name}, WP:{self.mission.current_wp_index}/{len(self.mission.waypoints)}\n"
|
||
client_sock.send(status.encode())
|
||
|
||
else:
|
||
client_sock.send(f"UNKNOWN_CMD: {msg}\n".encode())
|
||
|
||
except socket.timeout:
|
||
# 读超时,继续循环检查 self.tcp_server_running
|
||
continue
|
||
except BrokenPipeError:
|
||
self.logger.warning(f"📡 地面站连接异常断开: {addr[0]}:{addr[1]}")
|
||
break
|
||
except Exception as e:
|
||
self.logger.error(f"TCP 客户端处理异常: {e}")
|
||
break
|
||
finally:
|
||
try:
|
||
client_sock.close()
|
||
except Exception:
|
||
pass
|
||
|
||
# ================================================================
|
||
# 🔌 启动
|
||
# ================================================================
|
||
|
||
def start(self):
|
||
"""启动所有子系统"""
|
||
self.logger.info("正在启动所有子系统...")
|
||
|
||
# 1. 启动共享 ZMQ 代理(MissionManager 与 UAVControlLoop 共用)
|
||
self.proxy.start()
|
||
self.logger.info("✔ 共享 RouterProxy 已启动(MissionManager + UAVControlLoop 共用)")
|
||
|
||
# 3. 等待收到第一条 Router STATE
|
||
self._wait_for_initial_state()
|
||
|
||
# ---- 启动 TCP 命令通道 ----
|
||
cmd_cfg = self.cfg.get("command_channel", {})
|
||
if cmd_cfg.get("enabled", True):
|
||
self._start_tcp_command_server()
|
||
|
||
# ---- 飞行日志由 FlightLogger 在构造时自动启动 ----
|
||
|
||
self.running = True
|
||
self.logger.info("🚀 所有子系统就绪,主循环启动")
|
||
|
||
def _wait_for_initial_state(self, timeout=5.0):
|
||
"""等待 Router 推送第一条 STATE"""
|
||
deadline = time.time() + timeout
|
||
while time.time() < deadline:
|
||
state = self.proxy.get_latest_state()
|
||
if state is not None:
|
||
drone = state.get("drone", {})
|
||
self.logger.info(
|
||
"✔ 收到 Router 状态: mode=%s armed=%s alt=%.1fm",
|
||
drone.get("mode"), drone.get("armed"),
|
||
drone.get("alt_rel", 0),
|
||
)
|
||
return True
|
||
time.sleep(0.1)
|
||
self.logger.warning("⚠ 未收到 Router 状态 (%ds 超时),继续启动...", timeout)
|
||
return False
|
||
|
||
# ================================================================
|
||
# 🔁 主循环
|
||
# ================================================================
|
||
|
||
def run(self):
|
||
"""主业务循环(20Hz)"""
|
||
self.logger.info("主循环开始 (20Hz)")
|
||
|
||
prev_state = MissionState.INIT
|
||
loop_count = 0
|
||
|
||
try:
|
||
while self.running:
|
||
loop_start = time.time()
|
||
|
||
# ---- ① 读取视频帧 ----
|
||
ret, frame = self.streamer.read_frame()
|
||
if not ret:
|
||
# 无新帧时短暂等待,不浪费 CPU
|
||
time.sleep(0.005)
|
||
continue
|
||
|
||
loop_count += 1
|
||
orig_shape = frame.shape[:2]
|
||
|
||
# ---- ② NPU 推理 + 后处理 ----
|
||
input_tensor, ratio, dwdh = self.detector.preprocess(frame)
|
||
outputs = self.detector.session.infer([input_tensor])
|
||
detections = self.detector.postprocess(
|
||
outputs, ratio, dwdh, orig_shape,
|
||
)
|
||
self._latest_detections = detections
|
||
target_detected = len(detections) > 0
|
||
|
||
# ---- ③ 控制权协调 ----
|
||
# 根据当前状态机 state 决定 UAVControlLoop 控制线程启停
|
||
current_state = self.mission.state
|
||
self._coordinate_control(current_state, prev_state,
|
||
detections, orig_shape)
|
||
prev_state = current_state
|
||
|
||
# ---- 光流避障决策 ----
|
||
if self.mission.state == MissionState.VISUAL_TRACKING:
|
||
_, _ = self.optical_flow.process_frame(frame)
|
||
yaw_setpoint, speed_scale = self.optical_flow.get_zone_decision()
|
||
self.controller.set_obstacle_decision(yaw_setpoint, speed_scale)
|
||
else:
|
||
# 非追踪状态清除避障
|
||
self.controller.set_obstacle_decision(0.0, 1.0)
|
||
|
||
# ---- ④ 状态机更新 ----
|
||
self.mission.update(target_detected=target_detected)
|
||
|
||
# ---- ⑤ HUD 渲染 + 图传 ----
|
||
frame = self._render_hud(frame, loop_count, detections)
|
||
self.streamer.push_to_stream(frame)
|
||
|
||
# ---- 采集飞行日志 ----
|
||
if self.flight_logger.enabled and self.flight_logger.should_collect():
|
||
self.flight_logger.collect(
|
||
state=self.proxy.get_latest_state(),
|
||
mission_state_name=self.mission.state.name,
|
||
wp_index=self.mission.current_wp_index,
|
||
detect_count=len(self._latest_detections) if self._latest_detections is not None else 0,
|
||
tracking_active=1 if self._control_active else 0,
|
||
)
|
||
|
||
# ---- 频率控制 ----
|
||
elapsed = time.time() - loop_start
|
||
sleep_time = self.loop_interval - elapsed
|
||
if sleep_time > 0:
|
||
time.sleep(sleep_time)
|
||
|
||
except FailsafeTriggered as e:
|
||
# Failsafe 熔断:安全员切出手动模式 → 程序安全退出
|
||
self.logger.warning("🚨 [Failsafe] %s", e)
|
||
except KeyboardInterrupt:
|
||
self.logger.info("⌨ 用户键盘中断,正在安全退出...")
|
||
except Exception as e:
|
||
self.logger.exception("💥 主循环异常: %s", e)
|
||
finally:
|
||
self.shutdown()
|
||
|
||
# ================================================================
|
||
# 控制权协调
|
||
# ================================================================
|
||
|
||
def _coordinate_control(self, current_state, prev_state,
|
||
detections, orig_shape):
|
||
"""
|
||
控制权协调:基于状态机状态启停 UAVControlLoop 线程
|
||
|
||
┌──────────────┬─────────────────────┬──────────────────────┐
|
||
│ 状态机状态 │ MissionManager │ UAVControlLoop │
|
||
├──────────────┼─────────────────────┼──────────────────────┤
|
||
│ 非 TRACKING │ proxy → POSITION │ 控制线程已停止 │
|
||
│ │ SETPOINT │ │
|
||
├──────────────┼─────────────────────┼──────────────────────┤
|
||
│ TRACKING │ 不发 SETPOINT │ 20Hz VELOCITY 线程 │
|
||
│ │ (控制权已让渡) │ TargetTracker + PID │
|
||
└──────────────┴─────────────────────┴──────────────────────┘
|
||
"""
|
||
is_tracking = current_state == MissionState.VISUAL_TRACKING
|
||
was_tracking = prev_state == MissionState.VISUAL_TRACKING
|
||
|
||
if is_tracking:
|
||
# ---- 进入 VISUAL_TRACKING → 启动/保持控制线程 ----
|
||
if not self._control_active:
|
||
self._start_control_thread()
|
||
|
||
# 不断喂入检测数据(TargetTracker 在主线程更新)
|
||
self.controller.update_detections(detections, orig_shape)
|
||
|
||
else:
|
||
# ---- 非 VISUAL_TRACKING → 停止控制线程 ----
|
||
if self._control_active:
|
||
self._stop_control_thread()
|
||
|
||
# 非追踪期间也喂入空检测,保持数据结构新鲜
|
||
self.controller.update_detections([], orig_shape)
|
||
|
||
def _start_control_thread(self):
|
||
"""启动 UAVControlLoop 的 20Hz VELOCITY 控制线程"""
|
||
self.logger.info("▶ [控制权] 启动 UAVControlLoop VELOCITY 线程")
|
||
|
||
# 记录进入追踪时的飞机状态
|
||
state = self.proxy.get_latest_state()
|
||
if state:
|
||
d = state.get("drone", {})
|
||
self.logger.info("追踪初始状态: mode=%s alt=%.1fm armed=%s",
|
||
d.get("mode"), d.get("alt_rel", 0),
|
||
d.get("armed"))
|
||
|
||
# 直接复用 UAVControlLoop 已有的内部 _control_loop 方法
|
||
# (不修改任何内部逻辑,仅从外部启动其线程)
|
||
self.controller.running = True
|
||
self.controller.control_thread = threading.Thread(
|
||
target=self.controller._control_loop,
|
||
daemon=True,
|
||
name="uav-vel-control",
|
||
)
|
||
self.controller.control_thread.start()
|
||
self._control_active = True
|
||
|
||
def _stop_control_thread(self):
|
||
"""安全停止 UAVControlLoop 的控制线程"""
|
||
if not self._control_active:
|
||
return
|
||
|
||
self.logger.info("⏹ [控制权] 停止 UAVControlLoop 线程")
|
||
self.controller.running = False
|
||
|
||
if self.controller.control_thread:
|
||
self.controller.control_thread.join(timeout=2.0)
|
||
self.controller.control_thread = None
|
||
|
||
# 复位 PID 积分项和跟踪器,避免下次启动时积分暴冲
|
||
self.controller.pid_y.reset()
|
||
self.controller.pid_z.reset()
|
||
self.controller.tracker.reset()
|
||
self._control_active = False
|
||
self.logger.info("✔ 控制线程已停止,PID/跟踪器已重置")
|
||
|
||
# ================================================================
|
||
# 渲染
|
||
# ================================================================
|
||
|
||
def _render_hud(self, frame, loop_count, detections):
|
||
"""
|
||
在视频帧上叠加 HUD 信息
|
||
|
||
包含:状态机阶段、航点索引、控制权归属、检测结果
|
||
"""
|
||
# ---- 检测框 ----
|
||
frame = self.detector.draw_boxes(frame, detections)
|
||
|
||
# ---- 状态面板 ----
|
||
state_name = self.mission.state.name
|
||
wp_text = f"WP: {self.mission.current_wp_index + 1}/{len(self.mission.waypoints)}"
|
||
ctrl_text = "CTRL:VELOCITY" if self._control_active else "CTRL:POSITION"
|
||
|
||
cv2.putText(frame, f"State: {state_name} | {wp_text}",
|
||
(20, 40), cv2.FONT_HERSHEY_COMPLEX, 0.6,
|
||
(0, 255, 255), 2)
|
||
cv2.putText(frame, ctrl_text,
|
||
(20, 65), cv2.FONT_HERSHEY_SIMPLEX, 0.5,
|
||
(0, 255, 0) if self._control_active else (255, 255, 0), 2)
|
||
|
||
# ---- 每 100 帧打印详细状态日志 ----
|
||
if loop_count % 100 == 0:
|
||
state = self.proxy.get_latest_state()
|
||
if state:
|
||
d = state.get("drone", {})
|
||
self.logger.info(
|
||
"📡 mode=%s armed=%s alt=%.1fm spd=%.1f bat=%.0f%% "
|
||
"%s %s detect=%d",
|
||
d.get("mode"), d.get("armed"),
|
||
d.get("alt_rel", 0), d.get("ground_speed", 0),
|
||
d.get("battery", 0),
|
||
state_name, ctrl_text,
|
||
len(detections),
|
||
)
|
||
|
||
return frame
|
||
|
||
# ================================================================
|
||
# 🧹 资源释放
|
||
# ================================================================
|
||
|
||
def shutdown(self):
|
||
"""
|
||
安全关闭所有子系统
|
||
|
||
释放顺序(逆初始化顺序):
|
||
1. 停止控制线程
|
||
2. 关闭 MissionManager 的 ZMQ 代理
|
||
3. 关闭 UAVControlLoop 的 ZMQ 代理
|
||
4. 释放视频流资源
|
||
"""
|
||
self.logger.info("====== 安全关闭所有子系统 ======")
|
||
self.running = False
|
||
|
||
# 1. 停止控制线程
|
||
self._stop_control_thread()
|
||
|
||
# ---- 停止飞行日志线程 ----
|
||
self.flight_logger.stop()
|
||
|
||
# 2. 关闭共享 ZMQ 代理(MissionManager + UAVControlLoop 共用)
|
||
# 注意:UAVControlLoop 不拥有 proxy 生命周期(_owns_proxy=False),
|
||
# 仅在此处统一关闭一次。
|
||
try:
|
||
self.proxy.close()
|
||
self.logger.info("✔ 共享 RouterProxy 已关闭")
|
||
except Exception as e:
|
||
self.logger.warning("proxy close 异常: %s", e)
|
||
|
||
# 3. 释放视频资源
|
||
try:
|
||
self.streamer.release()
|
||
self.logger.info("✔ VideoStreamer 已释放")
|
||
except Exception as e:
|
||
self.logger.warning("streamer release 异常: %s", e)
|
||
|
||
# ---- 停止 TCP 命令通道 ----
|
||
self.tcp_server_running = False
|
||
if self.tcp_server_socket:
|
||
try:
|
||
self.tcp_server_socket.close()
|
||
except Exception:
|
||
pass
|
||
self.tcp_server_socket = None
|
||
if self.tcp_server_thread and self.tcp_server_thread.is_alive():
|
||
self.tcp_server_thread.join(timeout=2.0)
|
||
self.logger.info("✔ TCP 命令通道已关闭")
|
||
|
||
self.logger.info("🏁 所有资源已安全释放,程序退出")
|
||
|
||
|
||
# ================================================================
|
||
# 入口
|
||
# ================================================================
|
||
|
||
def main():
|
||
"""主入口:日志配置 → 编排器创建 → 启动 → 运行"""
|
||
# 创建日志目录
|
||
os.makedirs("./log", exist_ok=True)
|
||
|
||
# 生成带时间戳的日志文件名(精确到秒)
|
||
log_timestamp = time.strftime("%Y%m%d_%H%M%S")
|
||
log_file_path = f"./log/mission_run_{log_timestamp}.log"
|
||
|
||
logging.basicConfig(
|
||
level=logging.INFO,
|
||
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
|
||
handlers=[
|
||
logging.StreamHandler(), # 终端输出(保留)
|
||
logging.FileHandler(log_file_path, mode="w"), # 每次新建,覆盖写入
|
||
],
|
||
)
|
||
|
||
# 允许命令行指定配置文件路径
|
||
config_file = sys.argv[1] if len(sys.argv) > 1 else "./config.json"
|
||
# 也可以传入固定点
|
||
# waypoints = [
|
||
# {'x': 1.0, 'y': 0.0, 'z': -1.5, 'yaw': 0},
|
||
# {'x': 1.0, 'y': 1.0, 'z': -1.5, 'yaw': 90},
|
||
# {'x': 0.0, 'y': 0.0, 'z': -1.2, 'yaw': 0},
|
||
# ]
|
||
# orchestrator = MissionOrchestrator(config_path=config_file,waypoints=waypoints)
|
||
|
||
orchestrator = MissionOrchestrator(config_path=config_file,waypoints=[])
|
||
orchestrator.start()
|
||
orchestrator.run()
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main()
|