Enhance mavlink handling and add status text support

- Added handling for SYS_STATUS and STATUSTEXT messages in mainOrchestrator.py and mavlinkObject.py.
- Introduced a new StatusTextEntry data class in mavlinkVehicleView.py to manage status text messages.
- Updated topic management in mavlinkROS2Nodes.py to include new status text functionality.
Chiyu Chen 2 weeks ago
parent 0a84bb68fe
commit 324fced754

@ -83,17 +83,16 @@ python gui.py
建立、維護與飛控韌體的連接 建立、維護與飛控韌體的連接
構築 mavlink 封包 構築 mavlink 封包
處理無線模組的通訊格式 (XBee) 處理無線模組的通訊格式 (XBee)
--同時處理與 Gazebo 的 ardupilot_plugin 溝通的 FDM/JSON 訊息 (移除)-- 2. fc_interfaces (必要)
2. fc_interfaces (重要) 自定義的 ROS2 介面檔 沒有這個核心會運作不了
自定義的 ROS2 介面檔 沒啥好說的 沒有這個核心會運作不了
3. fc_network_module (重要) 3. fc_network_module (重要)
非核心 但是支援載具的重要附屬功能 非核心 但是支援載具的重要附屬功能
需要該功能時 作為一個 ros2 節點打開 需要該功能時 作為一個 ros2 節點打開
例如 : ntrip rtk 訊號轉接 例如 : ntrip rtk 訊號轉接
4. fc_network_apps 4. fc_network_apps
與 fc_network_adapter 銜接做高階功能包裝的應用小程式 是 fc_network_adapter 高階包裝API
利於開發GUI或其他應用 使用者的外層包裝 利於開發GUI或其他應用 使用者的外層包裝
這裡的定位是 "核心功能的高階包裝" 可以完全不去用 可以不使用 或者當作一個範例程式來看
5. someotherpkg 5. someotherpkg
如何使用 fc_network_apps 的範例檔案 如何使用 fc_network_apps 的範例檔案
6. GUI 6. GUI

@ -1106,7 +1106,7 @@ class ControlPanel:
0: "HB", 1: "S_STAT", 2: "S_TIME", 24: "GPS_RAW", 27: "RAW_IMU", 0: "HB", 1: "S_STAT", 2: "S_TIME", 24: "GPS_RAW", 27: "RAW_IMU",
30: "ATT", 32: "LOC_POS", 33: "GLB_POS", 62: "NAV_CTL", 30: "ATT", 32: "LOC_POS", 33: "GLB_POS", 62: "NAV_CTL",
74: "VFR_HUD", 147: "BATT_ST", 136: "TERRAIN", 241: "VIBRAT", 74: "VFR_HUD", 147: "BATT_ST", 136: "TERRAIN", 241: "VIBRAT",
125: "POW_STA", 125: "POW_STA", 253: "STAT_TXT",
} }
# ardupilot mega # ardupilot mega
@ -1198,7 +1198,8 @@ class Orchestrator:
'vfr_hud': 1.0, 'vfr_hud': 1.0,
'mode': 0.0, 'mode': 0.0,
'summary': 1.0, 'summary': 1.0,
'system_diagnostics': 1.0, 'sys_diags': 1.0,
'status_text': 1.0,
} }
def engageWholeSystem(self): def engageWholeSystem(self):

@ -51,7 +51,7 @@ from .mavlinkVehicleView import (
VehicleView, VehicleView,
VehicleComponent, VehicleComponent,
ComponentType, ComponentType,
ConnectionType StatusTextEntry,
) )
from .utils import RingBuffer, setup_logger from .utils import RingBuffer, setup_logger
@ -108,14 +108,15 @@ class mavlink_bridge:
def _init_message_handlers(self): def _init_message_handlers(self):
"""初始化訊息處理器映射表,提高處理效率""" """初始化訊息處理器映射表,提高處理效率"""
self.message_handlers = { self.message_handlers = {
0: self._handle_heartbeat, # HEARTBEAT 0: self._handle_heartbeat, # HEARTBEAT
1: self._handle_vehicle_sys_status, # SYS_STATUS 1: self._handle_vehicle_sys_status, # SYS_STATUS
24: self._handle_gps_raw_int, # GPS_RAW_INT 24: self._handle_gps_raw_int, # GPS_RAW_INT
30: self._handle_attitude, # ATTITUDE 30: self._handle_attitude, # ATTITUDE
32: self._handle_local_position, # LOCAL_POSITION_NED 32: self._handle_local_position, # LOCAL_POSITION_NED
33: self._handle_global_position, # GLOBAL_POSITION_INT 33: self._handle_global_position, # GLOBAL_POSITION_INT
74: self._handle_vfr_hud, # VFR_HUD 74: self._handle_vfr_hud, # VFR_HUD
147: self._handle_battery_status, # BATTERY_STATUS 147: self._handle_battery_status, # BATTERY_STATUS
253: self._handle_status_text, # STATUSTEXT
} }
def start(self): def start(self):
@ -247,6 +248,15 @@ class mavlink_bridge:
diag.errors_count4 = msg.errors_count4 diag.errors_count4 = msg.errors_count4
diag.timestamp = timestamp diag.timestamp = timestamp
def _handle_status_text(self, vehicle, component, msg, timestamp):
"""處理 STATUSTEXT 訊息 (msg_id: 253)"""
text = msg.text.rstrip('\x00') if msg.text else ''
if not text:
return
component.status.status_text_queue.append(
StatusTextEntry(text=text, severity=msg.severity, timestamp=timestamp)
)
def _handle_global_position(self, vehicle, component, msg, timestamp): def _handle_global_position(self, vehicle, component, msg, timestamp):
"""處理 GLOBAL_POSITION_INT 訊息 (msg_id: 33)""" """處理 GLOBAL_POSITION_INT 訊息 (msg_id: 33)"""
component.status.position.latitude = msg.lat / 1e7 # 轉換為度 component.status.position.latitude = msg.lat / 1e7 # 轉換為度
@ -411,8 +421,8 @@ class mavlink_object:
# 記錄訊息過濾類型 (可選) # 記錄訊息過濾類型 (可選)
# 0 HEARTBEAT, 1 SYS_STATUS, 24 GPS_RAW_INT, 30 ATTITUDE, # 0 HEARTBEAT, 1 SYS_STATUS, 24 GPS_RAW_INT, 30 ATTITUDE,
# 32 LOCAL_POSITION_NED, 33 GLOBAL_POSITION_INT, 74 VFR_HUD, 147 BATTERY_STATUS # 32 LOCAL_POSITION_NED, 33 GLOBAL_POSITION_INT, 74 VFR_HUD, 147 BATTERY_STATUS, 253 STATUSTEXT
self.bridge_msg_types = set([0, 1, 24, 30, 32, 33, 74, 147]) self.bridge_msg_types = set([0, 1, 24, 30, 32, 33, 74, 147, 253])
self.return_msg_types = set([]) self.return_msg_types = set([])
# 轉發到別的 mavlink object 作為目標端口 的列表 # 轉發到別的 mavlink object 作為目標端口 的列表
@ -849,5 +859,8 @@ if __name__ == '__main__':
1. async_io_manager.managed_objects mavlink_object.mavlinkObjects 功能重複整合 保留 mavlink_object.mavlinkObjects 1. async_io_manager.managed_objects mavlink_object.mavlinkObjects 功能重複整合 保留 mavlink_object.mavlinkObjects
2. async_io_manager _stop_event 無效變數移除 2. async_io_manager _stop_event 無效變數移除
2026 06 10
1. 增加 SYS_STATUS STATUSTEXT 訊息的處理機制
''' '''

@ -68,15 +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位置 (海拔高度)
'attitude': 0.0, # 姿態 (pitch yaw row 與其加速狀態) 'position_ned': 0.0, # LOCAL_POSITION_NED (位置+速度+相對高度)
'velocity': 0.0, # 速度 (已經包含在 vfr_hud 未來移除) 'attitude': 0.0, # 姿態 (pitch yaw row 與其加速狀態)
'battery': 0.0, # 電池 'battery': 0.0, # 電池
'vfr_hud': 0.0, # VFR HUD (地速 空速 絕對高度 爬升率 航向 油門) 'vfr_hud': 0.0, # VFR HUD (地速 空速 絕對高度 爬升率 航向 油門)
'mode': 0.0, # 飛行模式 (已經在 summary 裡 未來移除) 'sys_diags': 0.0, # SYS_STATUS 系統診斷
'summary': 0.0, # 載具摘要 (sysid 飛行模式 解鎖上鎖 gps狀態) 'status_text': 0.0, # STATUSTEXT 飛控文字(佇列驅動,>0 僅作啟用旗標)
'system_diagnostics': 0.0, # SYS_STATUS 系統診斷 'mode': 0.0, # 飛行模式 (已經在 summary 裡 未來移除)
'velocity': 0.0, # 速度 (已經包含在 vfr_hud 未來移除)
# 在這裡新增更多 topics... # 在這裡新增更多 topics...
} }
# 記錄每個 topic 的最後發布時間 {(sysid, topic): timestamp} # 記錄每個 topic 的最後發布時間 {(sysid, topic): timestamp}
@ -113,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()
@ -123,7 +128,7 @@ class VehicleStatusPublisher(Node):
職責: 職責:
- 定期從 vehicle_registry 讀取載具狀態 - 定期從 vehicle_registry 讀取載具狀態
- 頻率控制位置/姿態 2Hz電池/摘要 1Hz - 頻率控制 (位置/姿態 2Hz, 電池/摘要 1Hz)
- 發布標準 ROS2 消息類型 - 發布標準 ROS2 消息類型
- 檢測訂閱者按需發布 - 檢測訂閱者按需發布
""" """
@ -182,12 +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_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):
@ -452,7 +458,7 @@ class VehicleStatusPublisher(Node):
def _publish_system_diagnostics(self, sysid: int, status: mvv.ComponentStatus): def _publish_system_diagnostics(self, sysid: int, status: mvv.ComponentStatus):
"""發布 SYS_STATUS 系統診斷資訊""" """發布 SYS_STATUS 系統診斷資訊"""
if not self.rate_controller.should_publish(sysid, 'system_diagnostics'): if not self.rate_controller.should_publish(sysid, 'sys_diags'):
return return
diag = status.sys_diag diag = status.sys_diag
@ -460,7 +466,7 @@ class VehicleStatusPublisher(Node):
return return
publisher = self._get_or_create_publisher( publisher = self._get_or_create_publisher(
sysid, 'system_diagnostics', fcmsg.SystemDiagnosticsRaw sysid, 'sys_diags', fcmsg.SystemDiagnosticsRaw
) )
if publisher.get_subscription_count() == 0: if publisher.get_subscription_count() == 0:
@ -481,6 +487,31 @@ class VehicleStatusPublisher(Node):
publisher.publish(msg) 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請在此處實作對應的發布方法
@ -499,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
@ -560,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'
@ -1182,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):
@ -1461,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 大量互動 也許需要考慮對方的生命週期

@ -5,6 +5,7 @@ VehicleView - Pure State Container
""" """
import os import os
from collections import deque
from typing import Dict, Optional, Any, Tuple from typing import Dict, Optional, Any, Tuple
from dataclasses import dataclass, field from dataclasses import dataclass, field
from enum import Enum from enum import Enum
@ -117,6 +118,14 @@ class VFR:
timestamp: Optional[float] = None # 時間戳記 timestamp: Optional[float] = None # 時間戳記
@dataclass
class StatusTextEntry:
"""飛控狀態文字來源MAVLink STATUSTEXT"""
text: str
severity: Optional[int] = None
timestamp: Optional[float] = None
@dataclass @dataclass
class SystemDiagnostics: class SystemDiagnostics:
"""系統診斷資訊來源MAVLink SYS_STATUS不含電池欄位""" """系統診斷資訊來源MAVLink SYS_STATUS不含電池欄位"""
@ -144,6 +153,7 @@ class ComponentStatus:
gps: GPS = field(default_factory=GPS) gps: GPS = field(default_factory=GPS)
vfr: VFR = field(default_factory=VFR) vfr: VFR = field(default_factory=VFR)
sys_diag: SystemDiagnostics = field(default_factory=SystemDiagnostics) sys_diag: SystemDiagnostics = field(default_factory=SystemDiagnostics)
status_text_queue: deque = field(default_factory=lambda: deque(maxlen=64))
# 系統狀態 # 系統狀態
system_status: Optional[int] = None # MAV_STATE system_status: Optional[int] = None # MAV_STATE

Loading…
Cancel
Save