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.

354 lines
11 KiB
Python

"""ROS2 client for fc_network MavPositionTargetGlobalInt service (SET_POSITION_TARGET_GLOBAL_INT / Offboard)."""
from __future__ import annotations
import math
from dataclasses import dataclass
import rclpy
from rclpy.node import Node
from fc_interfaces.srv import MavPositionTargetGlobalInt
DEFAULT_SERVICE_NAME = "/fc_network/vehicle/pos_global_int"
DEFAULT_TIMEOUT_SEC = 2.0
# MAV_FRAME
MAV_FRAME_GLOBAL_RELATIVE_ALT_INT = 6 # 使用 WGS84 GNSS 座標系 高度為相對於 Home 點高度
# POSITION_TARGET_TYPEMASK
# 1 1 Ignore position x
# 2 2 Ignore position y
# 3 4 Ignore position z
# 4 8 Ignore velocity x
# 5 16 Ignore velocity y
# 6 32 Ignore velocity z
# 7 64 Ignore acceleration x
# 8 128 Ignore acceleration y
# 9 256 Ignore acceleration z
# 10 512 Use force instead of acceleration
# 11 1024 Ignore yaw
# 12 2048 Ignore yaw rate
# 13+ Not Used
# 6543210987654321
POSITION_ONLY = 0b0000110111111000 # 忽略速度、加速度、偏航
# 回傳 echo 與送出值比對時浮點欄位允許的絕對誤差MAVLink float 轉傳常有小差)
_FLOAT_ECHO_ABS_TOL = 1e-5
def _float_echo_equal(a: float, b: float) -> bool:
return math.isclose(float(a), float(b), rel_tol=0.0, abs_tol=_FLOAT_ECHO_ABS_TOL)
def _echo_position_target_matches(
*,
type_mask: int,
lat_int: int,
lon_int: int,
alt: float,
vx: float,
vy: float,
vz: float,
afx: float,
afy: float,
afz: float,
yaw: float,
yaw_rate: float,
time_boot_ms: int,
resp,
) -> tuple[bool, str]:
"""
比對本次送出的軌跡欄位與 server 填回的 r_* 是否一致
若不符回傳 (False, 簡短說明)相符則 (True, "")
"""
_tm_sent = int(type_mask) & 0xFFF
_tm_recv = int(resp.r_type_mask) & 0xFFF
if _tm_recv != _tm_sent:
return False, f"type_mask echo: sent {_tm_sent:012b} != ack {_tm_recv:012b}"
coordinate_tolerance = 5 * 100
if abs(resp.r_lat_int - lat_int) >= coordinate_tolerance:
return False, f"lat_int echo: sent {lat_int} != ack {resp.r_lat_int}"
if abs(resp.r_lon_int - lon_int) >= coordinate_tolerance:
return False, f"lon_int echo: sent {lon_int} != ack {resp.r_lon_int}"
# TODO 我給飛控相對高度 它回給我絕對高度 看來這個比較方法 還有很多改良點 但是之後吧
# if not _float_echo_equal(resp.r_alt, alt):
# return False, f"alt echo: sent {alt} != ack {resp.r_alt}"
if not _float_echo_equal(resp.r_vx, vx):
return False, f"vx echo: sent {vx} != ack {resp.r_vx}"
if not _float_echo_equal(resp.r_vy, vy):
return False, f"vy echo: sent {vy} != ack {resp.r_vy}"
if not _float_echo_equal(resp.r_vz, vz):
return False, f"vz echo: sent {vz} != ack {resp.r_vz}"
if not _float_echo_equal(resp.r_afx, afx):
return False, f"afx echo: sent {afx} != ack {resp.r_afx}"
if not _float_echo_equal(resp.r_afy, afy):
return False, f"afy echo: sent {afy} != ack {resp.r_afy}"
if not _float_echo_equal(resp.r_afz, afz):
return False, f"afz echo: sent {afz} != ack {resp.r_afz}"
if not _float_echo_equal(resp.r_yaw, yaw):
return False, f"yaw echo: sent {yaw} != ack {resp.r_yaw}"
if not _float_echo_equal(resp.r_yaw_rate, yaw_rate):
return False, f"yaw_rate echo: sent {yaw_rate} != ack {resp.r_yaw_rate}"
# if int(resp.r_time_boot_ms) != int(time_boot_ms):
# return False, f"time_boot_ms echo: sent {time_boot_ms} != r {resp.r_time_boot_ms}"
return True, ""
@dataclass
class PositionTargetGlobalIntResult:
"""對應 MavPositionTargetGlobalInt.srv 的 Responsesuccess 依「送出與 r_* echo 是否一致」。"""
success: bool
message: str
ack_result: int
r_time_boot_ms: int = 0
r_type_mask: int = 0
r_lat_int: int = 0
r_lon_int: int = 0
r_alt: float = 0.0
r_vx: float = 0.0
r_vy: float = 0.0
r_vz: float = 0.0
r_afx: float = 0.0
r_afy: float = 0.0
r_afz: float = 0.0
r_yaw: float = 0.0
r_yaw_rate: float = 0.0
class PositionTargetGlobalIntClient(Node):
"""
ROS2 client pos_global_int service 發送 SET_POSITION_TARGET_GLOBAL_INT (msg 86)
並等待 POSITION_TARGET_GLOBAL_INT (msg 87) 回應 server 端處理
"""
def __init__(self, service_name: str = DEFAULT_SERVICE_NAME) -> None:
if not rclpy.ok():
rclpy.init(args=None)
super().__init__("fc_position_target_global_int_client")
self._client = self.create_client(MavPositionTargetGlobalInt, service_name)
def wait_for_service(self, timeout_sec: float = 3.0) -> bool:
return self._client.wait_for_service(timeout_sec=timeout_sec)
def _send_position_target_global_int(
self,
*,
target_sysid: int,
target_compid: int,
time_boot_ms: int,
coordinate_frame: int,
type_mask: int,
lat_int: int,
lon_int: int,
alt: float,
vx: float,
vy: float,
vz: float,
afx: float,
afy: float,
afz: float,
yaw: float,
yaw_rate: float,
timeout_sec: float,
) -> PositionTargetGlobalIntResult:
req = MavPositionTargetGlobalInt.Request()
req.target_sysid = target_sysid
req.target_compid = target_compid
req.time_boot_ms = time_boot_ms
req.coordinate_frame = coordinate_frame
req.type_mask = type_mask
req.lat_int = lat_int
req.lon_int = lon_int
req.alt = float(alt)
req.vx = float(vx)
req.vy = float(vy)
req.vz = float(vz)
req.afx = float(afx)
req.afy = float(afy)
req.afz = float(afz)
req.yaw = float(yaw)
req.yaw_rate = float(yaw_rate)
req.timeout_sec = float(timeout_sec)
future = self._client.call_async(req)
rclpy.spin_until_future_complete(self, future, timeout_sec=float(timeout_sec) + 1.0)
if not future.done() or future.result() is None:
return PositionTargetGlobalIntResult(
success=False,
message="Service timeout.",
ack_result=-1,
)
resp = future.result()
if resp.ack_result != 0:
return PositionTargetGlobalIntResult(
success=False,
message=resp.message,
ack_result=resp.ack_result,
)
echo_ok, echo_detail = _echo_position_target_matches(
type_mask=type_mask,
lat_int=lat_int,
lon_int=lon_int,
alt=alt,
vx=vx,
vy=vy,
vz=vz,
afx=afx,
afy=afy,
afz=afz,
yaw=yaw,
yaw_rate=yaw_rate,
time_boot_ms=time_boot_ms,
resp=resp,
)
# out_message = resp.message
# if not echo_ok:
# out_message = echo_detail if not out_message else f"{echo_detail} | server: {out_message}"
return PositionTargetGlobalIntResult(
success=echo_ok,
message=echo_detail,
ack_result=resp.ack_result,
r_time_boot_ms=int(resp.r_time_boot_ms),
r_type_mask=int(resp.r_type_mask),
r_lat_int=int(resp.r_lat_int),
r_lon_int=int(resp.r_lon_int),
r_alt=float(resp.r_alt),
r_vx=float(resp.r_vx),
r_vy=float(resp.r_vy),
r_vz=float(resp.r_vz),
r_afx=float(resp.r_afx),
r_afy=float(resp.r_afy),
r_afz=float(resp.r_afz),
r_yaw=float(resp.r_yaw),
r_yaw_rate=float(resp.r_yaw_rate),
)
# def set_position_target_global_int(
# self,
# *,
# target_sysid: int,
# target_compid: int = 0,
# time_boot_ms: int = 0,
# coordinate_frame: int = 6,
# type_mask: int = 0,
# lat_int: int = 0,
# lon_int: int = 0,
# alt: float = 0.0,
# vx: float = 0.0,
# vy: float = 0.0,
# vz: float = 0.0,
# afx: float = 0.0,
# afy: float = 0.0,
# afz: float = 0.0,
# yaw: float = 0.0,
# yaw_rate: float = 0.0,
# timeout_sec: float = DEFAULT_TIMEOUT_SEC,
# ) -> PositionTargetGlobalIntResult:
# """
# 送出 SET_POSITION_TARGET_GLOBAL_INTcoordinate_frame 預設 6 (MAV_FRAME_GLOBAL_INT)
# 可依飛控/任務覆寫。
# """
# return self._send_position_target_global_int(
# target_sysid=target_sysid,
# target_compid=target_compid,
# time_boot_ms=time_boot_ms,
# coordinate_frame=coordinate_frame,
# type_mask=type_mask,
# lat_int=lat_int,
# lon_int=lon_int,
# alt=alt,
# vx=vx,
# vy=vy,
# vz=vz,
# afx=afx,
# afy=afy,
# afz=afz,
# yaw=yaw,
# yaw_rate=yaw_rate,
# timeout_sec=timeout_sec,
# )
def goto_position_only(
self,
*,
target_sysid: int,
target_compid: int = 0,
latitude_deg: float,
longitude_deg: float,
alt: float = 0.0,
timeout_sec: float,
):
lat_int = int(round(latitude_deg * 1e7))
lon_int = int(round(longitude_deg * 1e7))
return self._send_position_target_global_int(
target_sysid=target_sysid,
target_compid=target_compid,
time_boot_ms= 0,
coordinate_frame=MAV_FRAME_GLOBAL_RELATIVE_ALT_INT,
type_mask=POSITION_ONLY,
lat_int=lat_int,
lon_int=lon_int,
alt=alt,
vx=0,
vy=0,
vz=0,
afx=0,
afy=0,
afz=0,
yaw=0,
yaw_rate=0,
timeout_sec=timeout_sec,
)
# def set_position_target_global_int_deg(
# self,
# *,
# target_sysid: int,
# latitude_deg: float,
# longitude_deg: float,
# target_compid: int = 0,
# time_boot_ms: int = 0,
# coordinate_frame: int = MAV_FRAME_GLOBAL_RELATIVE_ALT_INT,
# type_mask: int = 0,
# alt: float = 0.0,
# vx: float = 0.0,
# vy: float = 0.0,
# vz: float = 0.0,
# afx: float = 0.0,
# afy: float = 0.0,
# afz: float = 0.0,
# yaw: float = 0.0,
# yaw_rate: float = 0.0,
# timeout_sec: float = DEFAULT_TIMEOUT_SEC,
# ) -> PositionTargetGlobalIntResult:
# """以度為單位的緯經,轉成 MAVLink 的 lat_int / lon_int (1e7 * deg)。"""
# lat_int = int(round(latitude_deg * 1e7))
# lon_int = int(round(longitude_deg * 1e7))
# return self.set_position_target_global_int(
# target_sysid=target_sysid,
# target_compid=target_compid,
# time_boot_ms=time_boot_ms,
# coordinate_frame=coordinate_frame,
# type_mask=type_mask,
# lat_int=lat_int,
# lon_int=lon_int,
# alt=alt,
# vx=vx,
# vy=vy,
# vz=vz,
# afx=afx,
# afy=afy,
# afz=afz,
# yaw=yaw,
# yaw_rate=yaw_rate,
# timeout_sec=timeout_sec,
# )