|
|
|
@ -45,7 +45,7 @@ from . import mavlinkObject as mo
|
|
|
|
from .utils import setup_logger
|
|
|
|
from .utils import setup_logger
|
|
|
|
|
|
|
|
|
|
|
|
logger = setup_logger(os.path.basename(__file__))
|
|
|
|
logger = setup_logger(os.path.basename(__file__))
|
|
|
|
MODULE_VER = "2.10"
|
|
|
|
MODULE_VER = "2.50"
|
|
|
|
|
|
|
|
|
|
|
|
FC_ROS_DOMAIN_ID = "0"
|
|
|
|
FC_ROS_DOMAIN_ID = "0"
|
|
|
|
NODE_KEYS = ("status_publisher", "command_service", "rtcm_relay")
|
|
|
|
NODE_KEYS = ("status_publisher", "command_service", "rtcm_relay")
|
|
|
|
@ -68,14 +68,16 @@ class PublishRateController:
|
|
|
|
# 注意 這邊是定義區 不要把參數寫在這裡 所以預設全部關閉
|
|
|
|
# 注意 這邊是定義區 不要把參數寫在這裡 所以預設全部關閉
|
|
|
|
# 以這個專案 請看 mainOrchestrator.py 的 Orchestrator 初始化階段
|
|
|
|
# 以這個專案 請看 mainOrchestrator.py 的 Orchestrator 初始化階段
|
|
|
|
self.topic_intervals = {
|
|
|
|
self.topic_intervals = {
|
|
|
|
'position_gnss': 0.0, # GNSS位置
|
|
|
|
'summary': 0.0, # 載具摘要 (sysid 飛行模式 解鎖上鎖 gps狀態)
|
|
|
|
'position_ned': 0.0, # LOCAL_POSITION_NED (位置+速度)
|
|
|
|
'position_gnss': 0.0, # GNSS位置 (海拔高度)
|
|
|
|
|
|
|
|
'position_ned': 0.0, # LOCAL_POSITION_NED (位置+速度+相對高度)
|
|
|
|
'attitude': 0.0, # 姿態 (pitch yaw row 與其加速狀態)
|
|
|
|
'attitude': 0.0, # 姿態 (pitch yaw row 與其加速狀態)
|
|
|
|
'velocity': 0.0, # 速度 (已經包含在 vfr_hud 未來移除)
|
|
|
|
|
|
|
|
'battery': 0.0, # 電池
|
|
|
|
'battery': 0.0, # 電池
|
|
|
|
'vfr_hud': 0.0, # VFR HUD (地速 空速 絕對高度 爬升率 航向 油門)
|
|
|
|
'vfr_hud': 0.0, # VFR HUD (地速 空速 絕對高度 爬升率 航向 油門)
|
|
|
|
|
|
|
|
'sys_diags': 0.0, # SYS_STATUS 系統診斷
|
|
|
|
|
|
|
|
'status_text': 0.0, # STATUSTEXT 飛控文字(佇列驅動,>0 僅作啟用旗標)
|
|
|
|
'mode': 0.0, # 飛行模式 (已經在 summary 裡 未來移除)
|
|
|
|
'mode': 0.0, # 飛行模式 (已經在 summary 裡 未來移除)
|
|
|
|
'summary': 0.0, # 載具摘要 (sysid 飛行模式 解鎖上鎖 gps狀態)
|
|
|
|
'velocity': 0.0, # 速度 (已經包含在 vfr_hud 未來移除)
|
|
|
|
# 在這裡新增更多 topics...
|
|
|
|
# 在這裡新增更多 topics...
|
|
|
|
}
|
|
|
|
}
|
|
|
|
# 記錄每個 topic 的最後發布時間 {(sysid, topic): timestamp}
|
|
|
|
# 記錄每個 topic 的最後發布時間 {(sysid, topic): timestamp}
|
|
|
|
@ -112,6 +114,10 @@ class PublishRateController:
|
|
|
|
|
|
|
|
|
|
|
|
return False
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def is_topic_enabled(self, topic: str) -> bool:
|
|
|
|
|
|
|
|
"""檢查 topic 是否啟用(interval > 0)"""
|
|
|
|
|
|
|
|
return self.topic_intervals.get(topic, 0) > 0
|
|
|
|
|
|
|
|
|
|
|
|
def reset(self):
|
|
|
|
def reset(self):
|
|
|
|
"""重置所有計時器"""
|
|
|
|
"""重置所有計時器"""
|
|
|
|
self.last_publish_time.clear()
|
|
|
|
self.last_publish_time.clear()
|
|
|
|
@ -122,7 +128,7 @@ class VehicleStatusPublisher(Node):
|
|
|
|
|
|
|
|
|
|
|
|
職責:
|
|
|
|
職責:
|
|
|
|
- 定期從 vehicle_registry 讀取載具狀態
|
|
|
|
- 定期從 vehicle_registry 讀取載具狀態
|
|
|
|
- 頻率控制(位置/姿態 2Hz,電池/摘要 1Hz)
|
|
|
|
- 頻率控制 (位置/姿態 2Hz, 電池/摘要 1Hz)
|
|
|
|
- 發布標準 ROS2 消息類型
|
|
|
|
- 發布標準 ROS2 消息類型
|
|
|
|
- 檢測訂閱者,按需發布
|
|
|
|
- 檢測訂閱者,按需發布
|
|
|
|
"""
|
|
|
|
"""
|
|
|
|
@ -181,11 +187,13 @@ class VehicleStatusPublisher(Node):
|
|
|
|
self._publish_position_gnss(sysid, status)
|
|
|
|
self._publish_position_gnss(sysid, status)
|
|
|
|
self._publish_position_ned(sysid, status)
|
|
|
|
self._publish_position_ned(sysid, status)
|
|
|
|
self._publish_attitude(sysid, status)
|
|
|
|
self._publish_attitude(sysid, status)
|
|
|
|
self._publish_velocity(sysid, status)
|
|
|
|
|
|
|
|
self._publish_battery(sysid, status)
|
|
|
|
self._publish_battery(sysid, status)
|
|
|
|
self._publish_vfr_hud(sysid, status)
|
|
|
|
self._publish_vfr_hud(sysid, status)
|
|
|
|
self._publish_mode(sysid, status)
|
|
|
|
|
|
|
|
self._publish_summary(vehicle)
|
|
|
|
self._publish_summary(vehicle)
|
|
|
|
|
|
|
|
self._publish_system_diagnostics(sysid, status)
|
|
|
|
|
|
|
|
self._publish_status_text(sysid, status)
|
|
|
|
|
|
|
|
self._publish_velocity(sysid, status)
|
|
|
|
|
|
|
|
self._publish_mode(sysid, status)
|
|
|
|
# 在這裡新增更多 publish 方法調用...
|
|
|
|
# 在這裡新增更多 publish 方法調用...
|
|
|
|
|
|
|
|
|
|
|
|
def _get_or_create_publisher(self, sysid: int, topic: str, msg_type, qos: int = 1):
|
|
|
|
def _get_or_create_publisher(self, sysid: int, topic: str, msg_type, qos: int = 1):
|
|
|
|
@ -439,11 +447,7 @@ class VehicleStatusPublisher(Node):
|
|
|
|
'armed': status.armed if status.armed is not None else False,
|
|
|
|
'armed': status.armed if status.armed is not None else False,
|
|
|
|
# 'mode_custom': status.mode.custom_mode if status.mode.custom_mode else 0,
|
|
|
|
# 'mode_custom': status.mode.custom_mode if status.mode.custom_mode else 0,
|
|
|
|
'mode_name': status.mode.mode_name if status.mode.mode_name else "UNKNOWN",
|
|
|
|
'mode_name': status.mode.mode_name if status.mode.mode_name else "UNKNOWN",
|
|
|
|
# 'latitude': status.position.latitude if status.position.latitude else 0.0,
|
|
|
|
'mav_status': status.system_status if status.system_status is not None else 0,
|
|
|
|
# 'longitude': status.position.longitude if status.position.longitude else 0.0,
|
|
|
|
|
|
|
|
# 'altitude': status.position.altitude if status.position.altitude else 0.0,
|
|
|
|
|
|
|
|
# 'battery_percent': status.battery.remaining if status.battery.remaining else 0,
|
|
|
|
|
|
|
|
# 'gps_fix': status.gps.fix_type if status.gps.fix_type else 0,
|
|
|
|
|
|
|
|
'connection_type': vehicle.connected_via.value,
|
|
|
|
'connection_type': vehicle.connected_via.value,
|
|
|
|
'last_update': component.packet_stats.last_msg_time if component.packet_stats.last_msg_time else 0.0,
|
|
|
|
'last_update': component.packet_stats.last_msg_time if component.packet_stats.last_msg_time else 0.0,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
@ -452,6 +456,62 @@ class VehicleStatusPublisher(Node):
|
|
|
|
msg.data = json.dumps(summary)
|
|
|
|
msg.data = json.dumps(summary)
|
|
|
|
publisher.publish(msg)
|
|
|
|
publisher.publish(msg)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _publish_system_diagnostics(self, sysid: int, status: mvv.ComponentStatus):
|
|
|
|
|
|
|
|
"""發布 SYS_STATUS 系統診斷資訊"""
|
|
|
|
|
|
|
|
if not self.rate_controller.should_publish(sysid, 'sys_diags'):
|
|
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
diag = status.sys_diag
|
|
|
|
|
|
|
|
if diag.sensors_install_mask is None:
|
|
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
publisher = self._get_or_create_publisher(
|
|
|
|
|
|
|
|
sysid, 'sys_diags', fcmsg.SystemDiagnosticsRaw
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if publisher.get_subscription_count() == 0:
|
|
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
msg = fcmsg.SystemDiagnosticsRaw()
|
|
|
|
|
|
|
|
msg.stamp = self.get_clock().now().to_msg()
|
|
|
|
|
|
|
|
msg.sensors_install_mask = int(diag.sensors_install_mask)
|
|
|
|
|
|
|
|
msg.sensors_enabled_mask = int(diag.sensors_enabled_mask or 0)
|
|
|
|
|
|
|
|
msg.sensors_health_mask = int(diag.sensors_health_mask or 0)
|
|
|
|
|
|
|
|
msg.mcu_load = int(diag.mcuLoad or 0)
|
|
|
|
|
|
|
|
msg.bus_error_rate = int(diag.busErrorRate or 0)
|
|
|
|
|
|
|
|
msg.bus_error_count = int(diag.busErrorCount or 0)
|
|
|
|
|
|
|
|
msg.errors_count1 = int(diag.errors_count1 or 0)
|
|
|
|
|
|
|
|
msg.errors_count2 = int(diag.errors_count2 or 0)
|
|
|
|
|
|
|
|
msg.errors_count3 = int(diag.errors_count3 or 0)
|
|
|
|
|
|
|
|
msg.errors_count4 = int(diag.errors_count4 or 0)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
publisher.publish(msg)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _publish_status_text(self, sysid: int, status: mvv.ComponentStatus):
|
|
|
|
|
|
|
|
"""發布 STATUSTEXT 飛控文字 (佇列 drain, 無訂閱者直接丟棄) """
|
|
|
|
|
|
|
|
# 是否啟用
|
|
|
|
|
|
|
|
if not self.rate_controller.is_topic_enabled('status_text'):
|
|
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
# 是否有資料
|
|
|
|
|
|
|
|
queue = status.status_text_queue
|
|
|
|
|
|
|
|
if not queue:
|
|
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
publisher = self._get_or_create_publisher(sysid, 'status_text', std_msgs.msg.String)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# 是否有監聽者
|
|
|
|
|
|
|
|
if publisher.get_subscription_count() == 0:
|
|
|
|
|
|
|
|
queue.clear()
|
|
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
while queue:
|
|
|
|
|
|
|
|
entry = queue.popleft()
|
|
|
|
|
|
|
|
msg = std_msgs.msg.String()
|
|
|
|
|
|
|
|
ts = entry.timestamp if entry.timestamp is not None else 0.0
|
|
|
|
|
|
|
|
sev = entry.severity if entry.severity is not None else -1
|
|
|
|
|
|
|
|
msg.data = f'[{ts:.3f}] [{sev}] {entry.text}'
|
|
|
|
|
|
|
|
publisher.publish(msg)
|
|
|
|
|
|
|
|
|
|
|
|
# ═══════════════════════════════════════════════════════════════
|
|
|
|
# ═══════════════════════════════════════════════════════════════
|
|
|
|
# 【新增 Topic 位置 3/4】
|
|
|
|
# 【新增 Topic 位置 3/4】
|
|
|
|
# 若要新增 topic,請在此處實作對應的發布方法
|
|
|
|
# 若要新增 topic,請在此處實作對應的發布方法
|
|
|
|
@ -470,29 +530,6 @@ class VehicleStatusPublisher(Node):
|
|
|
|
# # ... 實作發布邏輯
|
|
|
|
# # ... 實作發布邏輯
|
|
|
|
# ═══════════════════════════════════════════════════════════════
|
|
|
|
# ═══════════════════════════════════════════════════════════════
|
|
|
|
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
|
|
|
|
def _euler_to_quaternion(roll, pitch, yaw):
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
歐拉角轉四元數
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
|
|
|
roll: 橫滾角 (弧度)
|
|
|
|
|
|
|
|
pitch: 俯仰角 (弧度)
|
|
|
|
|
|
|
|
yaw: 偏航角 (弧度)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
|
|
|
tuple: (qx, qy, qz, qw)
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
qx = math.sin(roll/2) * math.cos(pitch/2) * math.cos(yaw/2) - \
|
|
|
|
|
|
|
|
math.cos(roll/2) * math.sin(pitch/2) * math.sin(yaw/2)
|
|
|
|
|
|
|
|
qy = math.cos(roll/2) * math.sin(pitch/2) * math.cos(yaw/2) + \
|
|
|
|
|
|
|
|
math.sin(roll/2) * math.cos(pitch/2) * math.sin(yaw/2)
|
|
|
|
|
|
|
|
qz = math.cos(roll/2) * math.cos(pitch/2) * math.sin(yaw/2) - \
|
|
|
|
|
|
|
|
math.sin(roll/2) * math.sin(pitch/2) * math.cos(yaw/2)
|
|
|
|
|
|
|
|
qw = math.cos(roll/2) * math.cos(pitch/2) * math.cos(yaw/2) + \
|
|
|
|
|
|
|
|
math.sin(roll/2) * math.sin(pitch/2) * math.sin(yaw/2)
|
|
|
|
|
|
|
|
return (qx, qy, qz, qw)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def stop(self):
|
|
|
|
def stop(self):
|
|
|
|
"""停止發布"""
|
|
|
|
"""停止發布"""
|
|
|
|
self.running = False
|
|
|
|
self.running = False
|
|
|
|
@ -531,6 +568,14 @@ class MavlinkCommandService(Node):
|
|
|
|
每次接到一個 service 請求 要整個系統丟某種指令給載具時
|
|
|
|
每次接到一個 service 請求 要整個系統丟某種指令給載具時
|
|
|
|
會做兩件事 1."丟出mavlink封包" 2."創造一個臨時信箱 Pending"
|
|
|
|
會做兩件事 1."丟出mavlink封包" 2."創造一個臨時信箱 Pending"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
然後透過每次 manager spin
|
|
|
|
|
|
|
|
會去呼叫 return_router() 方法
|
|
|
|
|
|
|
|
這個方法會監聽 return_packet_ring 跟臨時信箱的 Pending 做配對
|
|
|
|
|
|
|
|
配對到的解開 Pending
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
解開後 相對應的 handle_XXX 就會開始做事
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
"""
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
serviceString_prefix = '/fc_network/vehicle'
|
|
|
|
serviceString_prefix = '/fc_network/vehicle'
|
|
|
|
@ -1153,6 +1198,10 @@ class fc_ros_manager:
|
|
|
|
- RtcmRelay
|
|
|
|
- RtcmRelay
|
|
|
|
|
|
|
|
|
|
|
|
提供統一的啟動/停止介面給 mainOrchestrator
|
|
|
|
提供統一的啟動/停止介面給 mainOrchestrator
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
另外 這邊用到 MultiThreadedExecutor 會開出額外的 thread 的特性
|
|
|
|
|
|
|
|
使得就算 executor 在跑一些需要等待的方法
|
|
|
|
|
|
|
|
常態的 spin_once 也不會被 block (spin_thread 是另一個支線)
|
|
|
|
"""
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
def __init__(self):
|
|
|
|
def __init__(self):
|
|
|
|
@ -1432,6 +1481,10 @@ ros2_manager = fc_ros_manager()
|
|
|
|
2. schedule_restart_node / _restart_node : 手動重啟單一 node (spin thread 內執行)
|
|
|
|
2. schedule_restart_node / _restart_node : 手動重啟單一 node (spin thread 內執行)
|
|
|
|
3. orchestrator cmd: ("RESTART_ROS_NODE", node_key), node_key 見 NODE_KEYS
|
|
|
|
3. orchestrator cmd: ("RESTART_ROS_NODE", node_key), node_key 見 NODE_KEYS
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
2026.06.10
|
|
|
|
|
|
|
|
1. 增加了 _publish_system_diagnostics 與 _publish_status_text 功能
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
TODO
|
|
|
|
TODO
|
|
|
|
1. service 部分會需要跟 mavlinkobject 大量互動 也許需要考慮對方的生命週期
|
|
|
|
1. service 部分會需要跟 mavlinkobject 大量互動 也許需要考慮對方的生命週期
|
|
|
|
|
|
|
|
|
|
|
|
|