#!/usr/bin/env python3 """ 任務執行模組 管理多架無人機的 GUIDED 模式飛行控制迴圈 設計: - 每架無人機持有一個航點序列,逐點推進 - 各自到達就各自切換到下一個航點 - 用 QTimer 驅動,在 Qt 主線程執行 - 暫停 = 停止發送 setpoint,飛控自動懸停 - 相容舊的 2 階段任務與新的多航點任務 (Grid Sweep) """ import math from enum import Enum from PyQt6.QtCore import QObject, QTimer, pyqtSignal class MissionState(Enum): """整體任務狀態""" IDLE = "idle" RUNNING = "running" PAUSED = "paused" class DroneTask: """單架無人機的任務資料""" __slots__ = ('drone_id', 'sysid', 'waypoints', 'wp_index', 'done') def __init__(self, drone_id, sysid, waypoints): """ Args: drone_id: GUI 用的 ID (如 's0_11') sysid: MAVLink system ID (如 11) waypoints: 航點序列 [(lat, lon, alt), ...] """ self.drone_id = drone_id self.sysid = sysid self.waypoints = waypoints self.wp_index = 0 self.done = len(waypoints) == 0 @property def current_target(self): if self.done or self.wp_index >= len(self.waypoints): return None return self.waypoints[self.wp_index] @property def total_waypoints(self): return len(self.waypoints) class MissionExecutor(QObject): """ 任務執行器 planned_waypoints 格式: { 'drone_ids': ['s0_1', 's0_2', ...], 'waypoints': [ [(lat,lon,alt), ...], # drone 0 [(lat,lon,alt), ...], # drone 1 ] } """ # 信號 drone_waypoint_reached = pyqtSignal(str, int, int) # (drone_id, wp_index, total) mission_completed = pyqtSignal() def __init__(self, sender, drone_gps, arrival_radius=2.0, send_rate_hz=2.0): super().__init__() self.sender = sender self.drone_gps = drone_gps self.arrival_radius = arrival_radius self.state = MissionState.IDLE self.tasks = {} self._interval_ms = int(1000 / send_rate_hz) self._timer = QTimer() self._timer.timeout.connect(self._tick) # ------------------------------------------------------------------ 公開方法 def start(self, planned_waypoints): """啟動任務""" if self.state == MissionState.RUNNING: print("任務已在執行中") return self.tasks.clear() drone_ids = planned_waypoints['drone_ids'] waypoints_list = planned_waypoints['waypoints'] for i, drone_id in enumerate(drone_ids): sysid = int(drone_id.split('_')[1]) self.tasks[drone_id] = DroneTask(drone_id, sysid, waypoints_list[i]) self.state = MissionState.RUNNING self._timer.start(self._interval_ms) total_wps = sum(t.total_waypoints for t in self.tasks.values()) print(f"任務啟動: {len(self.tasks)} 架無人機, " f"共 {total_wps} 個航點, " f"到達半徑={self.arrival_radius}m, " f"發送週期={self._interval_ms}ms") def pause(self): """暫停任務""" if self.state == MissionState.RUNNING: self._timer.stop() self.state = MissionState.PAUSED print("任務暫停") def resume(self): """恢復任務""" if self.state == MissionState.PAUSED: self._timer.start(self._interval_ms) self.state = MissionState.RUNNING print("任務恢復") def stop(self): """停止任務""" self._timer.stop() self.tasks.clear() self.state = MissionState.IDLE print("任務停止") # ------------------------------------------------------------------ 控制迴圈 def _tick(self): """控制迴圈""" all_done = True for task in self.tasks.values(): if task.done: continue all_done = False target = task.current_target if target is None: continue # 讀取當前 GPS gps = self.drone_gps.get(task.drone_id) if gps is None: continue tgt_lat, tgt_lon, tgt_alt = target distance = _haversine(gps['lat'], gps['lon'], tgt_lat, tgt_lon) # 到達判定 if distance < self.arrival_radius: task.wp_index += 1 if task.wp_index >= task.total_waypoints: task.done = True self.drone_waypoint_reached.emit( task.drone_id, task.wp_index, task.total_waypoints ) print(f" {task.drone_id} → DONE " f"({task.total_waypoints}/{task.total_waypoints})") continue else: self.drone_waypoint_reached.emit( task.drone_id, task.wp_index, task.total_waypoints ) print(f" {task.drone_id} → WP {task.wp_index}/{task.total_waypoints} " f"(距離: {distance:.1f}m)") # 更新目標 tgt_lat, tgt_lon, tgt_alt = task.current_target # 發送 setpoint self.sender.send_position_global( task.sysid, tgt_lat, tgt_lon, tgt_alt ) # 全部完成檢查 if all_done and self.tasks: self._timer.stop() self.state = MissionState.IDLE self.mission_completed.emit() print("===== 任務全部完成 =====") # ------------------------------------------------------------------ 工具函式 def _haversine(lat1, lon1, lat2, lon2): """計算兩個 GPS 座標間的水平距離 (公尺)""" R = 6371000.0 lat1_r = math.radians(lat1) lat2_r = math.radians(lat2) dlat = math.radians(lat2 - lat1) dlon = math.radians(lon2 - lon1) a = (math.sin(dlat / 2) ** 2 + math.cos(lat1_r) * math.cos(lat2_r) * math.sin(dlon / 2) ** 2) return R * 2 * math.atan2(math.sqrt(a), math.sqrt(1 - a))