@ -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. 1 0"
MODULE_VER = " 2. 5 0"
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位置 (海拔高度)
' 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 僅作啟用旗標)
' mode ' : 0.0 , # 飛行模式 (已經在 summary 裡 未來移除)
' 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 讀取載具狀態
- 頻率控制 ( 位置 / 姿態 2 Hz , 電池 / 摘要 1 Hz )
- 頻率控制 ( 位置 / 姿態 2 Hz , 電池 / 摘要 1 Hz )
- 發布標準 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 大量互動 也許需要考慮對方的生命週期