基于ZMQ Router重构状态机相关代码以及readme和.sh文件的修改

This commit is contained in:
2026-06-12 18:01:26 +08:00
parent 4a5c81a9c3
commit 4dc5b48415
4 changed files with 1462 additions and 227 deletions
+664 -158
View File
@@ -1,208 +1,714 @@
"""
mission_manager.py — 基于 ZeroMQ RouterProxy 的航点巡航状态机
【重构背景】
系统底层架构已升级为 ZMQ Router 代理通信协议。本模块彻底摒弃
旧版 DroneController 直连方式,全面迁移至 RouterProxy。
【核心改动】
1. self.drone → self.proxy(注入的 RouterProxy 实例)
2. 所有状态读取:proxy.get_latest_state()["drone"][...]
3. 所有指令发送:proxy.send_command() / send_waypoint() / send_setpoint()
4. 新增 STATE 新鲜度校验(协议 §3.8.1),过期数据保守处理
5. 新增 Failsafe 模式断流保护,退出 OFFBOARD 即熔断
6. VISUAL_TRACKING 状态下完全让渡控制权,不发送任何 SETPOINT
【位置估计】
Router 协议未暴露局部 NED 坐标,本模块使用速度积分
(vx × dt 累加)推算相对起飞点的水平位置,用于航点到
达判定。垂直轴直接使用 state["drone"]["alt_rel"]。
【控制权协调】
MissionManager 通过自有 proxy 发送指令。连续 20Hz SETPOINT
流由 UAVControlLoop 的独立线程负责。run_mission.py 负责协调:
在 VISUAL_TRACKING 外停止 UAVControlLoop 线程,避免两套
SETPOINT 流同时激活产生冲突。
"""
import time
import math
import logging
from enum import Enum
logger = logging.getLogger(__name__)
class MissionState(Enum):
INIT = 0 # 初始化/等待连接
ARMING = 1 # 解锁中
TAKEOFF = 2 # 自动起飞中
NAVIGATING = 3 # 航点巡航导航
HOLD_TASK = 4 # 到达航点后的原地搜索/悬停
VISUAL_TRACKING = 5 # 🎯 视觉追踪中(由视觉主循环接管控制权)
RETURNING = 6 # 返航中
LANDING = 7 # 自动降落中
FINISHED = 8 # 任务结束/已上锁
HOLD_FINAL = 9 # 结束巡点后原地悬停(当 return_to_home 为 False 时使用)
"""无人机任务状态枚举"""
INIT = 0 # 初始化 / 等待连接
ARMING = 1 # 解锁中
TAKEOFF = 2 # 自动起飞中
NAVIGATING = 3 # 航点巡航导航
HOLD_TASK = 4 # 到达航点后的原地搜索 / 悬停
VISUAL_TRACKING = 5 # 🎯 视觉追踪中(控制权让渡给 UAVControlLoop)
RETURNING = 6 # 返航中
LANDING = 7 # 自动降落中
FINISHED = 8 # 任务结束 / 已上锁
HOLD_FINAL = 9 # 结束巡点后原地悬停(return_to_home=False)
class FailsafeTriggered(Exception):
"""自定义安全异常:当安全员人工介入切出自控模式时抛出,用于暴力熔断主循环"""
"""
自定义安全异常:当安全员人工介入切出自控模式时抛出,
用于暴力熔断主循环。
"""
pass
# ================================================================
# 内部工具
# ================================================================
def _dist_3d(x1, y1, z1, x2, y2, z2):
"""三维欧氏距离"""
return math.sqrt((x1 - x2) ** 2 + (y1 - y2) ** 2 + (z1 - z2) ** 2)
# ================================================================
# 航点巡航管理器
# ================================================================
class MissionManager:
def __init__(self, drone, waypoints, target_altitude=1.5, arrival_radius=0.3, hold_duration=5.0,
return_to_home=True):
"""
🛸 ZMQ 航点巡航状态机管理器
职责:
- 封装完整的无人机任务生命周期(初始化 → 解锁 → 起飞 →
航点巡航 → 视觉追踪 → 返航 → 降落)
- 所有底层通信通过注入的 RouterProxy 完成
- 在 VISUAL_TRACKING 状态下让渡控制权给 UAVControlLoop
- Failsafe 熔断保护
使用示例:
proxy = RouterProxy()
proxy.start()
manager = MissionManager(proxy, waypoints=[...], target_altitude=1.5)
while True:
manager.update(target_detected=some_bool)
time.sleep(0.05)
"""
# ---- Failsafe:自主飞行的合法模式白名单 ----
AUTONOMOUS_MODES = frozenset({
"OFFBOARD", "GUIDED", "AUTO", "HOLD", "LAND", "RTL",
})
# ---- 位置估计:速度积分有效时间窗口 ----
MAX_POSITION_DT = 1.0
# ---- 目标丢失超时(秒) ----
DEFAULT_TARGET_LOST_TIMEOUT = 3.0
# ---- 各阶段超时 ----
TAKEOFF_TIMEOUT = 15.0
OFFBOARD_TIMEOUT = 5.0
ARM_TIMEOUT = 5.0
def __init__(self, proxy, waypoints, target_altitude=1.5,
arrival_radius=0.3, hold_duration=5.0,
return_to_home=True, target_lost_timeout=None):
"""
:param drone: 实例化的 DroneController 底层驱动对象
:param waypoints: 航点列表 [{'x': 1.0, 'y': 2.0, 'z': -1.5, 'yaw': 0}, ... ]
:param target_altitude: 默认起飞高度(米,NED中向上为负值)
:param arrival_radius: 航点到达判定半径(米)
:param hold_duration: 抵达巡点后,没有发现目标时的最大搜寻/悬停时间(秒)
:param return_to_home: 航点巡完后是否返航,False则在最后一个点原地悬停
Args:
proxy: RouterProxy 实例(调用方负责 start/close)
waypoints: 航点列表 [{'x':1,'y':2,'z':-1.5,'yaw':0}, ...]
target_altitude: 默认起飞高度(米,正值=向上)
arrival_radius: 航点到达判定半径(米)
hold_duration: 巡点无目标的最大悬停搜索时间(秒)
return_to_home: 航点巡完后是否返航,False 则终点悬停
target_lost_timeout: VISUAL_TRACKING 目标丢失超时(秒)
"""
self.drone = drone
self.waypoints = waypoints
self.target_altitude = target_altitude if target_altitude < 0 else -target_altitude
# ---- 依赖注入 ----
self.proxy = proxy
# ---- 任务参数 ----
# 航点列表(外部队列已经使用 NED 坐标系,Z 负值向上)
self.waypoints = list(waypoints) if waypoints else []
self.target_altitude = -abs(target_altitude) # NED Z(负值向上)
self.arrival_radius = arrival_radius
self.hold_duration = hold_duration
self.return_to_home = return_to_home
self.target_lost_timeout = (
target_lost_timeout if target_lost_timeout is not None
else self.DEFAULT_TARGET_LOST_TIMEOUT
)
# ---- 状态机 ----
self.state = MissionState.INIT
self.current_wp_index = 0
# 计时器定义
# ---- 当前目标航点(共享给外部主循环 / UAVControlLoop) ----
self.current_target = {
"x": 0.0, "y": 0.0,
"z": self.target_altitude, "yaw": 0.0,
}
# ================================================================
# 本地位置估计(速度积分法)
# ================================================================
self._est_x = 0.0 # NED 北向(相对起飞点)
self._est_y = 0.0 # NED 东向
self._pos_time = 0.0 # 上次位置更新时间戳
self._home_set = False # 首次 STATE 标记
# ---- 计时器 ----
self.hold_start_time = 0.0
self.tracking_start_time = 0.0
self._target_lost_start = None
self._state_start_time = time.time()
# 起始点(Home)坐标缓存
self.home_x = 0.0
self.home_y = 0.0
self.home_z = 0.0
# ================================================================
# 追踪完成信号(外部设置)
# ================================================================
# UAVControlLoop 或 run_mission.py 在判定视觉追踪完成时置 True
self.tracking_completed = False
# 追踪最大时长兜底
self.tracking_max_duration = 30.0
def _is_tracking_task_complete(self):
"""
🛠️ 预留接口:判断视觉追踪任务是否完成
你可以根据需求修改内部逻辑。例如:
- 当前:假设持续追踪 5 秒视为完成任务。
- 后续扩展:可以引入与目标的物理距离、传感器状态等。
"""
tracking_duration = time.time() - self.tracking_start_time
target_hold_time = 5.0 # 默认设定5秒
logger.info("MissionManager 初始化: %d 个航点, 目标高度=%.1fm, "
"到达半径=%.1fm, 搜寻时长=%.1fs, 返航=%s",
len(self.waypoints), -self.target_altitude,
self.arrival_radius, self.hold_duration,
self.return_to_home)
if tracking_duration >= target_hold_time:
return True
return False
# ================================================================
# 主入口
# ================================================================
def update(self, target_detected=False):
"""
状态机高频核心业务逻辑,由外部主循环驱动
:param target_detected: 当前帧是否发现了视觉追踪目标
状态机高频核心业务逻辑,由外部主循环驱动(建议 ≥20Hz)
Args:
target_detected: 当前帧视觉检测是否有目标
Raises:
FailsafeTriggered: 检测到非法模式切换时抛出
"""
# 0. 越权检查(最强人肉兜底):只要切出自主控制模式,立刻暴力熔断
if self.state not in [MissionState.INIT, MissionState.FINISHED]:
if self.drone.current_mode not in ['GUIDED', 'OFFBOARD']:
print(f"\n⚠️ [Failsafe] 检测到飞控模式被切为: {self.drone.current_mode}! 外部人工强行接管,熔断任务!")
self.state = MissionState.FINISHED
raise FailsafeTriggered("安全员强行接管")
# ---------------------------------------------------------------
# 步骤 0:获取最新 STATE + 新鲜度校验 + Failsafe
# ---------------------------------------------------------------
state = self.proxy.get_latest_state()
# 1. INIT: 等待飞控连接并获取当前位置作为 Home 点
if self.state == MissionState.INIT:
if self.drone.is_connected:
print("[INIT] 飞控已连上。获取当前坐标为起始点...")
self.home_x, self.home_y, self.home_z = self.drone.local_position
print(f"[INIT] 起始点坐标: ({self.home_x:.2f}, {self.home_y:.2f}, {self.home_z:.2f})")
print("[INIT] 正在下发 GUIDED/OFFBOARD 模式并尝试解锁...")
# ---- 新鲜度校验(协议 §3.8.1) ----
if self.state not in (MissionState.INIT, MissionState.FINISHED):
if state is not None:
if not self.proxy.is_state_fresh(state):
age_us, _ = self.proxy.get_state_freshness(state)
age_s = (age_us or 0) / 1_000_000
logger.warning("⚠️ STATE 严重过期 (age=%.2fs),"
"本轮保守跳过", age_s)
return
else:
logger.warning("⚠️ 尚未收到 STATE,跳过本轮处理")
return
target_mode = "OFFBOARD" if "OFFBOARD" in self.drone.master.mode_mapping() else "GUIDED"
self.drone.set_mode(target_mode)
self.drone.arm()
self.state = MissionState.ARMING
# ---- Failsafe 越权检查 ----
if self.state not in (MissionState.INIT, MissionState.FINISHED):
if state is not None:
mode = state["drone"].get("mode", "")
if mode not in self.AUTONOMOUS_MODES:
logger.warning(
"\n⚠️ [Failsafe] 飞控模式被切为: %s!"
"人工接管,熔断任务!", mode
)
self.state = MissionState.FINISHED
raise FailsafeTriggered(
f"安全员接管,当前模式: {mode}"
)
# 2. ARMING: 监测解锁状态
elif self.state == MissionState.ARMING:
if self.drone.is_armed and self.drone.current_mode in ['GUIDED', 'OFFBOARD']:
print("\n[解锁成功] 正在下发自动起飞指令...")
# 下发位置到期望高度
self.drone.send_target_position(self.home_x, self.home_y, self.target_altitude, 0)
self.state = MissionState.TAKEOFF
# ---- 无 STATE 时仅 INIT 可容忍 ----
if state is None:
if self.state == MissionState.INIT:
logger.info("[INIT] 等待 Router 状态推送...")
return
# 3. TAKEOFF: 自动起飞爬升判定
elif self.state == MissionState.TAKEOFF:
current_z = self.drone.local_position[2]
# 持续发送起飞点位置,激活控制链路
self.drone.send_target_position(self.home_x, self.home_y, self.target_altitude, 0)
# ---------------------------------------------------------------
# 步骤 1:更新本地位置估计
# ---------------------------------------------------------------
self._update_local_position(state)
# NED 坐标系下 Z 为负数,判断是否接近目标高度
if abs(current_z - self.target_altitude) < self.arrival_radius:
print("\n[起飞完成] 达到预定高度。开始前往第 1 个巡航航点...")
# ---------------------------------------------------------------
# 步骤 2:状态机分支
# ---------------------------------------------------------------
try:
if self.state == MissionState.INIT:
self._handle_init(state)
elif self.state == MissionState.ARMING:
self._handle_arming(state)
elif self.state == MissionState.TAKEOFF:
self._handle_takeoff(state)
elif self.state == MissionState.NAVIGATING:
self._handle_navigating(state)
elif self.state == MissionState.HOLD_TASK:
self._handle_hold_task(state, target_detected)
elif self.state == MissionState.VISUAL_TRACKING:
self._handle_visual_tracking(state, target_detected)
elif self.state == MissionState.RETURNING:
self._handle_returning(state)
elif self.state == MissionState.LANDING:
self._handle_landing(state)
elif self.state == MissionState.HOLD_FINAL:
self._handle_hold_final(state)
except FailsafeTriggered:
raise
except Exception as e:
logger.error("状态机异常 (state=%s): %s", self.state.name, e,
exc_info=True)
# ================================================================
# 位置估计
# ================================================================
def _update_local_position(self, state):
"""通过速度积分更新本地 NED 位置估计"""
try:
drone = state["drone"]
now = time.time()
if not self._home_set:
self._est_x = 0.0
self._est_y = 0.0
self._pos_time = now
self._home_set = True
logger.info("[位置] 初始位置记录完成")
dt = now - self._pos_time
if 0 < dt < self.MAX_POSITION_DT:
vx = drone.get("vx", 0.0)
vy = drone.get("vy", 0.0)
self._est_x += vx * dt
self._est_y += vy * dt
# dt ≥ MAX_POSITION_DT 时跳过积分防跳变
self._pos_time = now
except (KeyError, TypeError) as e:
logger.warning("[位置] 更新异常: %s", e)
def _get_current_z(self, state):
"""从 STATE 获取当前 NED Z(负值向上)"""
try:
return -state["drone"].get("alt_rel", 0.0)
except (KeyError, TypeError):
return self.target_altitude
# ================================================================
# 状态处理器
# ================================================================
# ---- 初始化 ----
def _handle_init(self, state):
"""
[INIT] 等待飞控连接,判断是否需要执行完整起飞序列
三种分支:
A) 已解锁 + OFFBOARD → 直接跳 NAVIGATING
B) 未解锁 → 发 ARM 指令
C) 已解锁但非 OFFBOARD → 进入 ARMING 等待后续 TAKEOFF 序列
"""
try:
drone = state["drone"]
armed = drone.get("armed", False)
mode = drone.get("mode", "")
alt_rel = drone.get("alt_rel", 0.0)
logger.info("[INIT] mode=%s armed=%s alt_rel=%.1fm",
mode, armed, alt_rel)
# A) 已在 OFFBOARD + 已解锁 → 直接巡航
if armed and mode == "OFFBOARD":
logger.info("[INIT] 已在 OFFBOARD,直接进入航点巡航")
self.state = MissionState.NAVIGATING
self.current_wp_index = 0
# 4. NAVIGATING: 航点导航巡航
elif self.state == MissionState.NAVIGATING:
if not self.waypoints or self.current_wp_index >= len(self.waypoints):
# 航点全部飞完
if self.return_to_home:
print("\n[巡航完成] 所有航点已巡视完毕。正在返回起始点上方...")
self.state = MissionState.RETURNING
else:
print("\n[巡航完成] 所有航点已巡视完毕。根据配置切换至末端就地悬停...")
self.state = MissionState.HOLD_FINAL
self._set_current_target_from_wp(0)
return
# 获取当前目标航点
wp = self.waypoints[self.current_wp_index]
self.drone.send_target_position(wp['x'], wp['y'], wp['z'], wp['yaw'])
# 计算水平及高度距离
dx = wp['x'] - self.drone.local_position[0]
dy = wp['y'] - self.drone.local_position[1]
dz = wp['z'] - self.drone.local_position[2]
dist_xyz = math.sqrt(dx ** 2 + dy ** 2 + dz ** 2)
print(
f"\n\r[巡航中] 前往航点 [{self.current_wp_index + 1}/{len(self.waypoints)}] | 距离目标: {dist_xyz:.2f}m",
end="", flush=True)
if dist_xyz < self.arrival_radius:
print(f"\n[到达航点 {self.current_wp_index + 1}] 进入原地悬停与视觉检索阶段...")
self.state = MissionState.HOLD_TASK
self.hold_start_time = time.time()
# 5. HOLD_TASK: 抵达巡点后的搜索悬停阶段
elif self.state == MissionState.HOLD_TASK:
# 优先判定视觉:如果在巡点悬停搜寻期间,视觉发现了目标
if target_detected:
print("\n🎯 [目标锁定] 视觉检测到目标!立刻中断悬停,切入 [视觉追踪] 状态!")
self.state = MissionState.VISUAL_TRACKING
self.tracking_start_time = time.time()
# B) 未解锁
if not armed:
logger.info("[INIT] 执行解锁...")
ok, ack = self.proxy.send_command("ARM")
if ok:
self.state = MissionState.ARMING
self._state_start_time = time.time()
else:
logger.error("[INIT] ARM 失败: %s", ack)
return
# 如果没有发现目标,则保持航点位置,并检查悬停是否超时
wp = self.waypoints[self.current_wp_index]
self.drone.send_target_position(wp['x'], wp['y'], wp['z'], wp['yaw'])
# C) 已解锁但非 OFFBOARD
if armed and mode != "OFFBOARD":
logger.info("[INIT] 已解锁但 mode=%s,进入起飞序列", mode)
self.state = MissionState.ARMING
self._state_start_time = time.time()
elapsed_time = time.time() - self.hold_start_time
print(f"\r[检索中] 保持航点中... 剩余搜寻时间: {max(0.0, self.hold_duration - elapsed_time):.1f}s", end="",
flush=True)
except (KeyError, TypeError) as e:
logger.warning("[INIT] 解析异常: %s", e)
if elapsed_time >= self.hold_duration:
print(f"\n[未发现目标] 搜寻超时。放弃当前点,前往下一个航点...")
self.current_wp_index += 1
self.state = MissionState.NAVIGATING
# ---- 解锁 ----
# 6. VISUAL_TRACKING: 🎯 视觉闭环追踪状态(核心新增)
elif self.state == MissionState.VISUAL_TRACKING:
# 在此状态下,高层状态机【不主动发送】绝对位置坐标,把控制权完全让渡给主循环的 PID 速度控制
# 我们只需要在这里进行任务结束判定
print(f"\r[追踪中] 正在执行AI视觉闭环精准追踪... 计时: {time.time() - self.tracking_start_time:.1f}s",
end="", flush=True)
def _handle_arming(self, state):
"""
[ARMING] 等待解锁确认,成功后进入 TAKEOFF
"""
try:
armed = state["drone"].get("armed", False)
if self._is_tracking_task_complete():
print("\n✅ [任务完成] 当前目标追踪任务圆满完成!重置追踪器,继续执行航点巡航...")
self.current_wp_index += 1 # 切换到下一个航点
self.state = MissionState.NAVIGATING
# 7. RETURNING: 自动返航至起始点上方
elif self.state == MissionState.RETURNING:
self.drone.send_target_position(self.home_x, self.home_y, self.target_altitude, 0)
dist_to_home = math.sqrt((self.home_x - self.drone.local_position[0]) ** 2 +
(self.home_y - self.drone.local_position[1]) ** 2)
print(f"\r[返航中] 正在飞回起始点位置 | 剩余水平距离: {dist_to_home:.2f}m", end="", flush=True)
if dist_to_home < self.arrival_radius:
print("\n[到达] 已安全返回起始点上方。正在下发自动降落指令 (LAND)...")
if self.drone.set_mode('LAND'):
self.state = MissionState.LANDING
if not armed:
elapsed = time.time() - self._state_start_time
if elapsed > self.ARM_TIMEOUT:
logger.warning("[ARMING] 超时 (%.1fs),重试 ARM", elapsed)
self.proxy.send_command("ARM")
self._state_start_time = time.time()
else:
print("⚠️ 降落模式切换失败,正在重新下发...")
logger.info("[ARMING] 等待解锁... (%.1fs)", elapsed)
return
# 8. LANDING: 自动降落中
elif self.state == MissionState.LANDING:
print(f"\r[降落中] 当前高度: {self.drone.local_position[2]:+.2f}m | 解锁状态: {self.drone.is_armed}",
end="", flush=True)
if not self.drone.is_armed:
print("\n[完结] 无人机已安全着陆并自动上锁。整个任务流结束。")
# ---- 解锁成功 → 起飞 ----
takeoff_alt = -self.target_altitude # 正值(协议 altitude 为正)
logger.info("\n[解锁成功] 发送起飞指令 (alt=%.1fm)", takeoff_alt)
ok, ack = self.proxy.send_waypoint(
action="TAKEOFF", alt=takeoff_alt,
alt_frame="RELATIVE", speed=2.0,
)
if ok:
self.state = MissionState.TAKEOFF
self._state_start_time = time.time()
else:
logger.warning("[TAKEOFF] WAYPOINT 失败: %s,尝试 SETPOINT 爬升", ack)
ok2, _ = self.proxy.send_setpoint(
x=0, y=0, z=self.target_altitude, yaw=0.0,
control_mode="POSITION",
)
if ok2:
self.state = MissionState.TAKEOFF
self._state_start_time = time.time()
else:
logger.error("[TAKEOFF] 降级爬升也失败")
except (KeyError, TypeError) as e:
logger.warning("[ARMING] 异常: %s", e)
# ---- 起飞 ----
def _handle_takeoff(self, state):
"""
[TAKEOFF] 持续发送 POSITION SETPOINT 爬升,到达后进入 OFFBOARD
流程:
① 每帧发送 POSITION 悬停/爬升 SETPOINT(预热 + 保持流)
② 高度到达目标 90% → 发送 OFFBOARD 指令
③ 模式切换确认 → 跳转到 NAVIGATING
"""
try:
drone = state["drone"]
alt_rel = drone.get("alt_rel", 0.0)
mode = drone.get("mode", "")
target_up = -self.target_altitude # 正值
# ---- ① 持续发送 POSITION SETPOINT(预热 OFFBOARD 流) ----
# 通过 MissionManager 自有 proxy 发送,此阶段 UAVControlLoop
# 的 20Hz 线程会被 run_mission.py 关闭,不会冲突。
# NED 坐标系:x=0,y=0,z=target_altitude 表示飞到正上方目标高度
self.proxy.send_setpoint(
x=0, y=0, z=self.target_altitude, yaw=0.0,
control_mode="POSITION",
)
# ---- ② 到达目标高度 → 进入 OFFBOARD ----
reached_alt = alt_rel >= target_up * 0.9
if reached_alt and mode != "OFFBOARD":
logger.info("[TAKEOFF] 高度 %.1f/%.1f,切换 OFFBOARD...",
alt_rel, target_up)
# 先预热悬停(协议 §3.3.1 ③→④)
self.proxy.send_setpoint(
x=0, y=0, z=-alt_rel, yaw=0.0,
control_mode="POSITION",
)
time.sleep(0.2)
ok, ack = self.proxy.send_command("OFFBOARD")
if ok:
# 等待模式切换
for _ in range(int(self.OFFBOARD_TIMEOUT / 0.1)):
st = self.proxy.get_latest_state()
if st and st["drone"]["mode"] == "OFFBOARD":
logger.info("[TAKEOFF] 已进入 OFFBOARD")
break
time.sleep(0.1)
else:
logger.warning("[TAKEOFF] OFFBOARD 切换超时")
else:
logger.error("[TAKEOFF] OFFBOARD 指令失败: %s", ack)
# ---- ③ 高度到位 + OFFBOARD 模式 → 跳转巡航 ----
if reached_alt and mode == "OFFBOARD":
logger.info("\n[起飞完成] 高度 %.1fm,进入航点巡航", alt_rel)
self.state = MissionState.NAVIGATING
self.current_wp_index = 0
self._set_current_target_from_wp(0)
return
# ---- 爬升超时保护 ----
elapsed = time.time() - self._state_start_time
if elapsed > self.TAKEOFF_TIMEOUT:
logger.warning("[TAKEOFF] 超时 (%.1fs),强行进入巡航", elapsed)
self.state = MissionState.NAVIGATING
self.current_wp_index = 0
self._set_current_target_from_wp(0)
return
logger.info("[起飞中] alt=%.1f/%.1f mode=%s 耗时=%.1fs",
alt_rel, target_up, mode, elapsed)
except (KeyError, TypeError) as e:
logger.warning("[TAKEOFF] 异常: %s", e)
# ---- 航点导航 ----
def _handle_navigating(self, state):
"""
[NAVIGATING] 每帧发送 POSITION SETPOINT 前往当前航点
到达判定后推进索引;所有航点飞完后根据 return_to_home
决定返航或悬停。
"""
if not self.waypoints or self.current_wp_index >= len(self.waypoints):
self._finish_navigation()
return
wp = self.waypoints[self.current_wp_index]
wp_x = wp["x"]
wp_y = wp["y"]
wp_z = wp.get("z", self.target_altitude)
wp_yaw = wp.get("yaw", 0.0)
# ---- 发送 POSITION SETPOINT(通过自有 proxy) ----
# 此 SETPOINT 让 PX4 飞向目标航点
self.proxy.send_setpoint(
x=wp_x, y=wp_y, z=wp_z, yaw=wp_yaw,
control_mode="POSITION",
)
# ---- 共享目标 ----
self.current_target = {"x": wp_x, "y": wp_y, "z": wp_z, "yaw": wp_yaw}
# ---- 到达判定 ----
current_z = self._get_current_z(state)
dist = _dist_3d(self._est_x, self._est_y, current_z,
wp_x, wp_y, wp_z)
if dist < self.arrival_radius:
logger.info("\n✅ [到达航点 %d/%d] dist=%.2fm",
self.current_wp_index + 1, len(self.waypoints), dist)
self.state = MissionState.HOLD_TASK
self.hold_start_time = time.time()
else:
logger.info("[巡航中] 航点 [%d/%d] 距离=%.2fm",
self.current_wp_index + 1, len(self.waypoints), dist)
def _finish_navigation(self):
"""所有航点飞完后的跳转"""
if self.return_to_home:
logger.info("\n[巡航完成] 返回起始点...")
self.state = MissionState.RETURNING
self.current_target = {"x": 0, "y": 0,
"z": self.target_altitude, "yaw": 0}
else:
logger.info("\n[巡航完成] 末端悬停...")
self.state = MissionState.HOLD_FINAL
self._state_start_time = time.time()
# ---- 悬停搜索 ----
def _handle_hold_task(self, state, target_detected):
"""
[HOLD_TASK] 抵达航点后的悬停搜索
策略:
- 检测到目标 → VISUAL_TRACKING
- 超时无目标 → 下一个航点
- 持续发送 POSITION SETPOINT 悬停
"""
# ---- 发现目标 ----
if target_detected:
logger.info("\n🎯 [目标锁定] 切入视觉追踪状态!")
self.state = MissionState.VISUAL_TRACKING
self.tracking_start_time = time.time()
self._target_lost_start = None
self.tracking_completed = False
return
# ---- 保持当前位置悬停 ----
wp = self.waypoints[self.current_wp_index]
self.proxy.send_setpoint(
x=wp["x"], y=wp["y"],
z=wp.get("z", self.target_altitude),
yaw=wp.get("yaw", 0),
control_mode="POSITION",
)
self.current_target = {
"x": wp["x"], "y": wp["y"],
"z": wp.get("z", self.target_altitude),
"yaw": wp.get("yaw", 0),
}
# ---- 超时判定 ----
elapsed = time.time() - self.hold_start_time
if elapsed >= self.hold_duration:
logger.info("\n[未发现目标] 搜寻超时 (%.1fs),前往下一航点",
self.hold_duration)
self.current_wp_index += 1
self.state = MissionState.NAVIGATING
else:
logger.info("[检索中] 航点 %d 剩余 %.1fs",
self.current_wp_index + 1,
self.hold_duration - elapsed)
# ---- 视觉追踪 ----
def _handle_visual_tracking(self, state, target_detected):
"""
[VISUAL_TRACKING] 🎯 完全让渡控制权给 UAVControlLoop
【核心设计】
MissionManager 在此状态下 **不发送任何 SETPOINT**。
UAVControlLoop (20Hz 独立线程) 通过其自有 proxy 发送
VELOCITY SETPOINT 进行视觉闭环追踪。
本模块仅负责追踪退出条件的判定和状态切换。
【退出条件】
1. self.tracking_completed == True(外部模块设置)
2. 目标丢失超过 target_lost_timeout 秒
3. 追踪总时长超过 tracking_max_duration(兜底)
"""
# ---- 不发送任何 SETPOINT — 控制权已让渡 ----
tracking_elapsed = time.time() - self.tracking_start_time
# ---- 兜底超时 ----
if tracking_elapsed > self.tracking_max_duration:
logger.info("\n⏰ [追踪超时] %.1fs ≥ %.1fs",
tracking_elapsed, self.tracking_max_duration)
self._exit_tracking()
return
# ---- 外部完成信号 ----
if self.tracking_completed:
logger.info("\n✅ [追踪完成] 外部模块报告完成,返回巡航")
self.tracking_completed = False
self._exit_tracking()
return
# ---- 目标丢失超时 ----
if not target_detected:
if self._target_lost_start is None:
self._target_lost_start = time.time()
else:
lost_elapsed = time.time() - self._target_lost_start
if lost_elapsed >= self.target_lost_timeout:
logger.info("\n⏰ [丢失超时] %.1fs,放弃追踪",
lost_elapsed)
self._exit_tracking()
return
logger.info("[追踪中] 丢失 %.1fs/%.1fs",
lost_elapsed, self.target_lost_timeout)
else:
self._target_lost_start = None
logger.info("[追踪中] 进行中... %.1fs", tracking_elapsed)
def _exit_tracking(self):
"""退出 VISUAL_TRACKING 的统一出口"""
self.current_wp_index += 1
self.state = MissionState.NAVIGATING
self._target_lost_start = None
logger.info("[状态切换] VISUAL_TRACKING → NAVIGATING,"
"前往航点 %d", self.current_wp_index + 1)
# ---- 返航 ----
def _handle_returning(self, state):
"""
[RETURNING] 发送 POSITION SETPOINT 到起始点 (0,0,z)
"""
self.proxy.send_setpoint(
x=0, y=0, z=self.target_altitude, yaw=0,
control_mode="POSITION",
)
self.current_target = {"x": 0, "y": 0,
"z": self.target_altitude, "yaw": 0}
current_z = self._get_current_z(state)
dist = _dist_3d(self._est_x, self._est_y, current_z,
0.0, 0.0, self.target_altitude)
logger.info("[返航中] 距离=%.2fm", dist)
if dist < self.arrival_radius:
logger.info("\n[到达] 已返回起始点,发送 LAND 指令...")
ok, ack = self.proxy.send_command("LAND")
if ok:
self.state = MissionState.LANDING
else:
logger.warning("[返航] LAND 失败: %s", ack)
# ---- 降落 ----
def _handle_landing(self, state):
"""
[LANDING] 监控 disarm 状态判断着陆
"""
try:
armed = state["drone"].get("armed", False)
alt_rel = state["drone"].get("alt_rel", 0.0)
logger.info("[降落中] 高度=%.1fm 已解锁=%s",
alt_rel, not armed)
if not armed:
logger.info("\n[完结] 已着陆上锁,任务结束")
self.state = MissionState.FINISHED
except (KeyError, TypeError) as e:
logger.warning("[LANDING] 异常: %s", e)
# 9. HOLD_FINAL: 末端就地悬停
elif self.state == MissionState.HOLD_FINAL:
final_wp = self.waypoints[-1] if self.waypoints else {'x': self.home_x, 'y': self.home_y,
'z': self.target_altitude, 'yaw': 0}
self.drone.send_target_position(final_wp['x'], final_wp['y'], final_wp['z'], final_wp['yaw'])
print(
f"\r[挂起] 无人机保持在最终航点悬停。坐标:({final_wp['x']}, {final_wp['y']}, {final_wp['z']}) | 等待人工接管中...",
end="", flush=True)
# ---- 末端悬停 ----
def _handle_hold_final(self, state):
"""
[HOLD_FINAL] 末端悬停,保持最终航点位置
"""
if self.waypoints:
final_wp = self.waypoints[-1]
else:
final_wp = {"x": 0, "y": 0,
"z": self.target_altitude, "yaw": 0}
self.proxy.send_setpoint(
x=final_wp["x"], y=final_wp["y"],
z=final_wp.get("z", self.target_altitude),
yaw=final_wp.get("yaw", 0),
control_mode="POSITION",
)
self.current_target = {
"x": final_wp["x"], "y": final_wp["y"],
"z": final_wp.get("z", self.target_altitude),
"yaw": final_wp.get("yaw", 0),
}
logger.info("[挂起] 保持 (%.1f, %.1f, %.1f) 等待接管",
final_wp["x"], final_wp["y"],
final_wp.get("z", self.target_altitude))
# ================================================================
# 辅助
# ================================================================
def _set_current_target_from_wp(self, index):
"""从航点列表更新 current_target"""
if self.waypoints and index < len(self.waypoints):
wp = self.waypoints[index]
self.current_target = {
"x": wp["x"], "y": wp["y"],
"z": wp.get("z", self.target_altitude),
"yaw": wp.get("yaw", 0),
}