You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
AirTrapMine/src/GUI/command_sender.py

165 lines
5.7 KiB
Python

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

#!/usr/bin/env python3
"""
指令發送模組
負責將目標位置轉成具體的通訊指令發送到飛控
目前使用 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
class CommandSender(ABC):
"""指令發送抽象介面"""
@abstractmethod
def send_position_global(self, drone_id, sysid, lat, lon, alt):
"""
發送全球座標位置指令
Args:
drone_id: GUI 用的 drone 識別字串 (如 's0_11')
sysid: 目標無人機的 MAVLink system ID
lat: 緯度 (度)
lon: 經度 (度)
alt: 高度 (公尺)
"""
pass
@abstractmethod
def close(self):
"""關閉連線"""
pass
class MavlinkSender(CommandSender):
"""
MAVLink 直接發送 (驗證階段用)
透過 SET_POSITION_TARGET_GLOBAL_INT 發送目標位置
使用方式:
sender = MavlinkSender("udpout:127.0.0.1:14550")
sender.send_position_global(sysid=1, lat=24.123, lon=120.567, alt=10.0)
"""
# type_mask: 只使用位置 (lat, lon, alt)
# 忽略速度 (bit 3,4,5)、加速度 (bit 6,7,8)、yaw (bit 10)、yaw_rate (bit 11)
TYPE_MASK_POSITION_ONLY = (
0b0000_1101_1111_1000 # = 0x0DF8
)
def __init__(self, connection_string="udpout:127.0.0.1:14550"):
"""
Args:
connection_string: MAVLink 連線字串
SITL 範例: "udpout:127.0.0.1:14550"
"""
self.connection_string = connection_string
self.mav = mavutil.mavlink_connection(connection_string)
print(f"MavlinkSender 已建立連線: {connection_string}")
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
1, # target_component (autopilot)
mavutil.mavlink.MAV_FRAME_GLOBAL_RELATIVE_ALT_INT,
self.TYPE_MASK_POSITION_ONLY,
int(lat * 1e7), # lat_int
int(lon * 1e7), # lon_int
float(alt), # alt
0, 0, 0, # vx, vy, vz (忽略)
0, 0, 0, # afx, afy, afz (忽略)
0, 0 # yaw, yaw_rate (忽略)
)
def close(self):
"""關閉 MAVLink 連線"""
if self.mav:
self.mav.close()
self.mav = None
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 已關閉")