feat(mission): virtual-leader path following + 固定領隊 + barrier
- mission_planner: Leader-Follower 改用 arc-length 參數化(virtual leader),_build_center_path 以同心圓弧連接直線段,銳角退化成 hover 單點並在該點放 rendezvous barrier,避免暴衝與編隊交叉 - mission_planner: rank 改以輸入順序為準,不再對投影排序,避免浮點噪音 導致 run-to-run leader 漂移 - mission_group / gui: 新增 leader_drone_id 與「領隊」下拉選單,handle_ route_confirmed 把指定領隊重排到 rank 0,整個縱隊順序固定如軍隊行進 - mission_executor: rendezvous barrier 釋放邏輯、fallback LOITER、send retry;加 barrier_timeout 保護 - command_sender / communication / navigation: 配合 setpoint 送達與 Ros2 bridge 更新 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
+89
-14
@@ -3,10 +3,16 @@
|
||||
指令發送模組
|
||||
負責將目標位置轉成具體的通訊指令發送到飛控
|
||||
|
||||
現階段: MavlinkSender (直接 pymavlink 發送)
|
||||
未來: 替換為 ROS2Sender (發到 ROS2 topic,由 fc_network_adapter 轉發)
|
||||
目前使用 Ros2CommandSender,經 fc_network_adapter 的
|
||||
/fc_network/vehicle/pos_global_int service 發送。
|
||||
|
||||
MavlinkSender 保留作為直連 SITL 的 fallback 工具,但已不是預設路徑。
|
||||
"""
|
||||
import asyncio
|
||||
import traceback
|
||||
from abc import ABC, abstractmethod
|
||||
|
||||
from PyQt6.QtCore import QObject, pyqtSignal
|
||||
from pymavlink import mavutil
|
||||
|
||||
|
||||
@@ -14,11 +20,12 @@ class CommandSender(ABC):
|
||||
"""指令發送抽象介面"""
|
||||
|
||||
@abstractmethod
|
||||
def send_position_global(self, sysid, lat, lon, alt):
|
||||
def send_position_global(self, drone_id, sysid, lat, lon, alt):
|
||||
"""
|
||||
發送全球座標位置指令
|
||||
|
||||
Args:
|
||||
drone_id: GUI 用的 drone 識別字串 (如 's0_11')
|
||||
sysid: 目標無人機的 MAVLink system ID
|
||||
lat: 緯度 (度)
|
||||
lon: 經度 (度)
|
||||
@@ -58,16 +65,8 @@ class MavlinkSender(CommandSender):
|
||||
self.mav = mavutil.mavlink_connection(connection_string)
|
||||
print(f"MavlinkSender 已建立連線: {connection_string}")
|
||||
|
||||
def send_position_global(self, sysid, lat, lon, alt):
|
||||
"""
|
||||
發送 SET_POSITION_TARGET_GLOBAL_INT
|
||||
|
||||
注意:
|
||||
- coordinate_frame 使用 MAV_FRAME_GLOBAL_RELATIVE_ALT_INT
|
||||
高度是相對起飛點的高度 (公尺)
|
||||
- 如果 FormationPlanner 產出的 alt 是 AMSL 絕對高度,
|
||||
需要在外部先減去起飛點高度再傳入
|
||||
"""
|
||||
def send_position_global(self, drone_id, sysid, lat, lon, alt):
|
||||
"""發送 SET_POSITION_TARGET_GLOBAL_INT。drone_id 未使用,保留為介面相容。"""
|
||||
self.mav.mav.set_position_target_global_int_send(
|
||||
0, # time_boot_ms (不使用)
|
||||
sysid, # target_system
|
||||
@@ -87,4 +86,80 @@ class MavlinkSender(CommandSender):
|
||||
if self.mav:
|
||||
self.mav.close()
|
||||
self.mav = None
|
||||
print("MavlinkSender 已關閉")
|
||||
print("MavlinkSender 已關閉")
|
||||
|
||||
|
||||
class Ros2CommandSender(QObject):
|
||||
"""
|
||||
透過 fc_network_apps.PositionTargetGlobalIntClient 送 goto 指令。
|
||||
|
||||
設計重點:
|
||||
- send_position_global 是同步介面,立刻回傳。結果透過 send_result signal 回報
|
||||
- 每台 drone 維護自己的 asyncio.Task pipeline,新的目標會 cancel 掉舊的 in-flight
|
||||
- client 由 DroneMonitor.get_or_create_position_client 管理,per-drone 獨立節點
|
||||
"""
|
||||
|
||||
# (drone_id, sysid, success, message)
|
||||
send_result = pyqtSignal(str, int, bool, str)
|
||||
|
||||
DEFAULT_TIMEOUT_SEC = 2.0
|
||||
|
||||
def __init__(self, monitor, timeout_sec: float = DEFAULT_TIMEOUT_SEC):
|
||||
super().__init__()
|
||||
self._monitor = monitor
|
||||
self._timeout_sec = timeout_sec
|
||||
self._pending = {} # drone_id -> asyncio.Task
|
||||
|
||||
def send_position_global(self, drone_id, sysid, lat, lon, alt):
|
||||
"""同步介面。立刻回,結果透過 send_result signal 回報。"""
|
||||
# 取消該機上一個未完成的 task,避免舊指令覆蓋新指令
|
||||
old = self._pending.get(drone_id)
|
||||
if old is not None and not old.done():
|
||||
old.cancel()
|
||||
|
||||
try:
|
||||
loop = asyncio.get_event_loop()
|
||||
except RuntimeError:
|
||||
loop = asyncio.new_event_loop()
|
||||
asyncio.set_event_loop(loop)
|
||||
|
||||
task = loop.create_task(
|
||||
self._do_send(drone_id, int(sysid), float(lat), float(lon), float(alt))
|
||||
)
|
||||
self._pending[drone_id] = task
|
||||
|
||||
async def _do_send(self, drone_id, sysid, lat, lon, alt):
|
||||
try:
|
||||
client = self._monitor.get_or_create_position_client(drone_id)
|
||||
if client is None:
|
||||
self.send_result.emit(
|
||||
drone_id, sysid, False,
|
||||
"PositionTargetGlobalIntClient 無法建立"
|
||||
)
|
||||
return
|
||||
|
||||
result = await client.goto_position_only_async(
|
||||
target_sysid=sysid,
|
||||
target_compid=0,
|
||||
latitude_deg=lat,
|
||||
longitude_deg=lon,
|
||||
alt=alt,
|
||||
timeout_sec=self._timeout_sec,
|
||||
)
|
||||
self.send_result.emit(
|
||||
drone_id, sysid, bool(result.success), str(result.message or "")
|
||||
)
|
||||
except asyncio.CancelledError:
|
||||
# 被新的目標取代,正常狀況,不用回報
|
||||
pass
|
||||
except Exception as e:
|
||||
traceback.print_exc()
|
||||
self.send_result.emit(drone_id, sysid, False, f"exception: {e}")
|
||||
|
||||
def close(self):
|
||||
"""取消所有未完成的 task。PositionTargetGlobalIntClient 由 DroneMonitor 負責 destroy。"""
|
||||
for task in self._pending.values():
|
||||
if not task.done():
|
||||
task.cancel()
|
||||
self._pending.clear()
|
||||
print("Ros2CommandSender 已關閉")
|
||||
Reference in New Issue
Block a user