Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bbd120d25a | ||
|
|
e0165c9aab |
@@ -68,6 +68,7 @@ N. logs 是執行時期的記錄檔
|
||||
. ./install/local_setup.bash
|
||||
# 範例
|
||||
python -m fc_network_adapter.fc_network_adapter.mainOrchestrator
|
||||
python -m fc_network_adapter.tests.test_ringBuffer
|
||||
python -m fc_network_adapter.tests.demo_integration
|
||||
|
||||
```
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@ from pymavlink import mavutil
|
||||
from . import mavlinkObject as mo
|
||||
from . import serialManager as sm
|
||||
from . import mavlinkVehicleView as mvv
|
||||
from . import mavlinkROS2Nodes as mros
|
||||
|
||||
from .utils import RingBuffer, setup_logger
|
||||
from .utils import acquireSerial, acquirePort
|
||||
@@ -37,6 +38,7 @@ class PanelState:
|
||||
self.mavlink_bridge_state = "Stopped"
|
||||
self.object_manager_state = "Stopped"
|
||||
self.serial_manager_state = "Stopped"
|
||||
self.ros2_manager_state = "Stopped"
|
||||
self.socket_object_list = [] # 已有的 mavlink object
|
||||
self.linked_serial_dict = {} # 已連線的 serial 端口 serial id num : serial_port string
|
||||
self.panel_info_msg_list = [] # 顯示在面板上的資訊訊息
|
||||
@@ -240,12 +242,13 @@ class ControlPanel:
|
||||
stdscr.border()
|
||||
|
||||
# 更新模組狀態顯示
|
||||
stdscr.addstr(0, 10, " MavLink MiddleWare ", curses.A_BOLD)
|
||||
stdscr.addstr(1, 2, f" Panel Status : {state.panel_status}")
|
||||
stdscr.addstr(2, 2, f"Object Manager State : {state.object_manager_state}")
|
||||
stdscr.addstr(3, 2, f"Mavlink Bridge State : {state.mavlink_bridge_state}")
|
||||
stdscr.addstr(4, 2, f"Socket Object number : {len(state.socket_object_list)}")
|
||||
stdscr.addstr(0, 10, " MavLink MiddleWare ", curses.A_BOLD)
|
||||
stdscr.addstr(1, 2, f" Panel Status : {state.panel_status}")
|
||||
stdscr.addstr(2, 2, f"Object Manager State : {state.object_manager_state}")
|
||||
stdscr.addstr(3, 2, f"Mavlink Bridge State : {state.mavlink_bridge_state}")
|
||||
stdscr.addstr(4, 2, f"Socket Object number : {len(state.socket_object_list)}")
|
||||
stdscr.addstr(2, 36, f"Serial Manager State : {state.serial_manager_state}")
|
||||
stdscr.addstr(3, 36, f"ROS2 Manager State : {state.ros2_manager_state}")
|
||||
|
||||
# 顯示當前選單項目
|
||||
start_line = 6
|
||||
@@ -1133,6 +1136,20 @@ class Orchestrator:
|
||||
# === 3) serial_manager 部分的準備 ===
|
||||
self.plumber = sm.serial_manager()
|
||||
|
||||
# === 4) ros 部分的準備 ===
|
||||
self.fc_ros_manager = mros.ros2_manager
|
||||
if not self.fc_ros_manager.initialized:
|
||||
self.fc_ros_manager.initialize()
|
||||
self.fc_ros_manager.status_publisher.rate_controller.topic_intervals = {
|
||||
'position': 1.0,
|
||||
'attitude': 0.0,
|
||||
'velocity': 0.0,
|
||||
'battery': 1.0,
|
||||
'vfr_hud': 1.0,
|
||||
'mode': 0.0,
|
||||
'summary': 1.0,
|
||||
}
|
||||
|
||||
def engageWholeSystem(self):
|
||||
"""啟動整個系統"""
|
||||
# === 1) 面板部分的啟動 ===
|
||||
@@ -1146,6 +1163,9 @@ class Orchestrator:
|
||||
# === 3) serial_manager 部分的啟動 ===
|
||||
self.plumber.start()
|
||||
|
||||
# === 4) ros 部分的啟動 ===
|
||||
self.fc_ros_manager.start()
|
||||
|
||||
def mainLoop(self):
|
||||
logger.info("Main orchestrator started <-")
|
||||
try:
|
||||
@@ -1174,6 +1194,11 @@ class Orchestrator:
|
||||
linked_serial_dict = self.plumber.get_serial_link()
|
||||
self.panelState.linked_serial_dict = linked_serial_dict
|
||||
|
||||
if self.fc_ros_manager.running:
|
||||
self.panelState.ros2_manager_state = 'Running'
|
||||
else:
|
||||
self.panelState.ros2_manager_state = 'Stopped'
|
||||
|
||||
# B. 更新載具列表(從 vehicle_registry 獲取)
|
||||
self._update_vehicles_list()
|
||||
|
||||
@@ -1284,10 +1309,17 @@ class Orchestrator:
|
||||
self.plumber.shutdown()
|
||||
self.plumber.thread.join(timeout=2)
|
||||
|
||||
if self.fc_ros_manager.spin_thread.is_alive():
|
||||
if self.fc_ros_manager.running:
|
||||
self.fc_ros_manager.stop()
|
||||
self.fc_ros_manager.spin_thread.join(timeout=2)
|
||||
|
||||
# 關閉面板執行緒
|
||||
if self.panel_thread.is_alive():
|
||||
self.panel_thread.join(timeout=2)
|
||||
|
||||
time.sleep(0.5) # 等待各模組穩定關閉
|
||||
|
||||
logger.info("Main orchestrator END!")
|
||||
|
||||
# =============== 面板動作 - Mavlink Object ===============
|
||||
@@ -1560,6 +1592,6 @@ if __name__ == "__main__":
|
||||
2. 修正 udp port 在移除後仍被記錄為佔用的問題
|
||||
3. 因應 mvalinkObject.py 中 mavlinkObjects 修正變數存取方式
|
||||
4. 註解掉無效代碼 action == "UPDATE_VEHICLES_LIST" 區塊
|
||||
|
||||
5. 系統納入 mavlink ROS2 Manager 模組
|
||||
|
||||
'''
|
||||
|
||||
@@ -0,0 +1,614 @@
|
||||
"""
|
||||
MAVLink ROS2 Nodes
|
||||
包含兩個獨立的 ROS2 Node:
|
||||
1. VehicleStatusPublisher - 發布載具狀態到 ROS2 topics
|
||||
2. MavlinkCommandService - 提供 MAVLink 指令 service 介面
|
||||
|
||||
從 vehicle_registry 讀取狀態數據,頻率控制,模組化設計
|
||||
"""
|
||||
|
||||
import os
|
||||
import time
|
||||
import math
|
||||
import threading
|
||||
from typing import Dict, Optional
|
||||
|
||||
# ROS2 imports
|
||||
import rclpy
|
||||
from rclpy.node import Node
|
||||
from rclpy.executors import MultiThreadedExecutor
|
||||
|
||||
# ROS2 Message imports
|
||||
import std_msgs.msg
|
||||
import sensor_msgs.msg
|
||||
import geometry_msgs.msg
|
||||
import mavros_msgs.msg
|
||||
|
||||
# 自定義 imports
|
||||
from . import mavlinkVehicleView as mvv
|
||||
from .utils import setup_logger
|
||||
|
||||
logger = setup_logger(os.path.basename(__file__))
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# 頻率控制器
|
||||
# ============================================================================
|
||||
|
||||
class PublishRateController:
|
||||
"""發布頻率控制器 - 按時間間隔控制發布頻率"""
|
||||
|
||||
def __init__(self):
|
||||
# 各 topic 的發布間隔(秒)
|
||||
self.topic_intervals = {
|
||||
'position': 0.5, # GPS位置
|
||||
'attitude': 0.5, # 姿態
|
||||
'velocity': 0.5, # 速度
|
||||
'battery': 1.0, # 電池
|
||||
'vfr_hud': 0.5, # VFR HUD
|
||||
'mode': 1.0, # 飛行模式
|
||||
'summary': 1.0, # 載具摘要
|
||||
}
|
||||
# 記錄每個 topic 的最後發布時間 {(sysid, topic): timestamp}
|
||||
self.last_publish_time: Dict[tuple, float] = {}
|
||||
|
||||
def should_publish(self, sysid: int, topic: str) -> bool:
|
||||
"""
|
||||
檢查是否應該發布此 topic
|
||||
|
||||
Args:
|
||||
sysid: 系統ID
|
||||
topic: topic 名稱
|
||||
|
||||
Returns:
|
||||
bool: True 表示應該發布
|
||||
"""
|
||||
key = (sysid, topic)
|
||||
now = time.time()
|
||||
|
||||
# 當間隔設定為0或負數時 關閉該 topic 的發布
|
||||
interval = self.topic_intervals.get(topic, 0)
|
||||
if interval <= 0:
|
||||
return False
|
||||
|
||||
# 首次發布
|
||||
if key not in self.last_publish_time:
|
||||
self.last_publish_time[key] = now
|
||||
return True
|
||||
|
||||
# 檢查時間間隔
|
||||
if now - self.last_publish_time[key] >= interval:
|
||||
self.last_publish_time[key] = now
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def reset(self):
|
||||
"""重置所有計時器"""
|
||||
self.last_publish_time.clear()
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# VehicleStatusPublisher Node
|
||||
# ============================================================================
|
||||
|
||||
class VehicleStatusPublisher(Node):
|
||||
"""
|
||||
載具狀態發布者 - 從 vehicle_registry 讀取數據並發布到 ROS2 topics
|
||||
|
||||
職責:
|
||||
- 定期從 vehicle_registry 讀取載具狀態
|
||||
- 頻率控制(位置/姿態 2Hz,電池/摘要 1Hz)
|
||||
- 發布標準 ROS2 消息類型
|
||||
- 檢測訂閱者,按需發布
|
||||
"""
|
||||
topicString_prefix = f'/fc_network/vehicle'
|
||||
|
||||
def __init__(self):
|
||||
super().__init__('vehicle_status_publisher')
|
||||
|
||||
# 頻率控制器
|
||||
self.rate_controller = PublishRateController()
|
||||
|
||||
# fc_publishers 字典 {(sysid, topic_name): publisher}
|
||||
self.fc_publishers: Dict[tuple, any] = {}
|
||||
|
||||
# 定時器:以較高頻率檢查 vehicle_registry 並發布
|
||||
# 10Hz 檢查頻率,但通過 rate_controller 控制實際發布頻率
|
||||
self.timer_period = 0.1 # 100ms
|
||||
self.timer = self.create_timer(self.timer_period, self.timer_callback)
|
||||
|
||||
# 狀態標誌
|
||||
self.running = True
|
||||
|
||||
# logger.info("VehicleStatusPublisher initialized")
|
||||
|
||||
def timer_callback(self):
|
||||
"""定時器回調 - 檢查所有載具並發布狀態"""
|
||||
if not self.running:
|
||||
return
|
||||
|
||||
# 從 vehicle_registry 獲取所有載具
|
||||
all_vehicles = mvv.vehicle_registry.get_all()
|
||||
|
||||
for sysid, vehicle in all_vehicles.items():
|
||||
self._publish_vehicle_status(vehicle)
|
||||
|
||||
def _publish_vehicle_status(self, vehicle: mvv.VehicleView):
|
||||
"""
|
||||
發布單個載具的所有狀態
|
||||
|
||||
Args:
|
||||
vehicle: VehicleView 實例
|
||||
"""
|
||||
sysid = vehicle.sysid
|
||||
|
||||
# 假設只有一個 autopilot component (component_id=1)
|
||||
component = vehicle.get_component(1)
|
||||
if not component:
|
||||
return
|
||||
|
||||
status = component.status
|
||||
|
||||
# 發布各種狀態(通過頻率控制器判斷是否發布)
|
||||
self._publish_position(sysid, status)
|
||||
self._publish_attitude(sysid, status)
|
||||
self._publish_velocity(sysid, status)
|
||||
self._publish_battery(sysid, status)
|
||||
self._publish_vfr_hud(sysid, status)
|
||||
self._publish_mode(sysid, status)
|
||||
self._publish_summary(vehicle)
|
||||
|
||||
def _get_or_create_publisher(self, sysid: int, topic: str, msg_type, qos: int = 1):
|
||||
"""
|
||||
獲取或創建 publisher
|
||||
|
||||
Args:
|
||||
sysid: 系統ID
|
||||
topic: topic 名稱
|
||||
msg_type: ROS2 消息類型
|
||||
qos: QoS 設置
|
||||
|
||||
Returns:
|
||||
publisher 對象
|
||||
"""
|
||||
key = (sysid, topic)
|
||||
if key not in self.fc_publishers:
|
||||
topic_name = f'{self.topicString_prefix}/sys{sysid}/{topic}'
|
||||
publisher = self.create_publisher(msg_type, topic_name, qos)
|
||||
self.fc_publishers[key] = publisher
|
||||
logger.info(f"Created publisher: {topic_name}")
|
||||
return self.fc_publishers[key]
|
||||
|
||||
def _publish_position(self, sysid: int, status: mvv.ComponentStatus):
|
||||
"""發布 GPS 位置"""
|
||||
if not self.rate_controller.should_publish(sysid, 'position'):
|
||||
return
|
||||
|
||||
pos = status.position
|
||||
if pos.latitude is None or pos.longitude is None:
|
||||
return
|
||||
|
||||
publisher = self._get_or_create_publisher(sysid, 'position', sensor_msgs.msg.NavSatFix)
|
||||
|
||||
# 檢查是否有訂閱者
|
||||
if publisher.get_subscription_count() == 0:
|
||||
return
|
||||
|
||||
msg = sensor_msgs.msg.NavSatFix()
|
||||
msg.latitude = pos.latitude
|
||||
msg.longitude = pos.longitude
|
||||
msg.altitude = pos.altitude if pos.altitude is not None else 0.0
|
||||
|
||||
# GPS 狀態資訊
|
||||
gps = status.gps
|
||||
if gps.fix_type is not None:
|
||||
msg.status.status = gps.fix_type - 1 # MAVLink fix_type 轉 NavSatStatus
|
||||
|
||||
publisher.publish(msg)
|
||||
|
||||
def _publish_attitude(self, sysid: int, status: mvv.ComponentStatus):
|
||||
"""發布姿態(IMU)"""
|
||||
if not self.rate_controller.should_publish(sysid, 'attitude'):
|
||||
return
|
||||
|
||||
att = status.attitude
|
||||
if att.roll is None:
|
||||
return
|
||||
|
||||
publisher = self._get_or_create_publisher(sysid, 'attitude', sensor_msgs.msg.Imu)
|
||||
|
||||
if publisher.get_subscription_count() == 0:
|
||||
return
|
||||
|
||||
msg = sensor_msgs.msg.Imu()
|
||||
|
||||
# 歐拉角轉四元數
|
||||
qx, qy, qz, qw = self._euler_to_quaternion(
|
||||
att.roll, att.pitch, att.yaw
|
||||
)
|
||||
msg.orientation.x = qx
|
||||
msg.orientation.y = qy
|
||||
msg.orientation.z = qz
|
||||
msg.orientation.w = qw
|
||||
|
||||
# 角速度
|
||||
if att.rollspeed is not None:
|
||||
msg.angular_velocity.x = att.rollspeed
|
||||
msg.angular_velocity.y = att.pitchspeed
|
||||
msg.angular_velocity.z = att.yawspeed
|
||||
|
||||
publisher.publish(msg)
|
||||
|
||||
def _publish_velocity(self, sysid: int, status: mvv.ComponentStatus):
|
||||
"""發布速度"""
|
||||
if not self.rate_controller.should_publish(sysid, 'velocity'):
|
||||
return
|
||||
|
||||
vfr = status.vfr
|
||||
if vfr.groundspeed is None:
|
||||
return
|
||||
|
||||
publisher = self._get_or_create_publisher(sysid, 'velocity', geometry_msgs.msg.TwistStamped)
|
||||
|
||||
if publisher.get_subscription_count() == 0:
|
||||
return
|
||||
|
||||
msg = geometry_msgs.msg.TwistStamped()
|
||||
msg.header.stamp = self.get_clock().now().to_msg()
|
||||
|
||||
# 使用 VFR HUD 的地速和航向計算速度分量
|
||||
if vfr.heading is not None:
|
||||
heading_rad = math.radians(vfr.heading)
|
||||
msg.twist.linear.x = vfr.groundspeed * math.cos(heading_rad)
|
||||
msg.twist.linear.y = vfr.groundspeed * math.sin(heading_rad)
|
||||
|
||||
# 爬升率作為 z 軸速度
|
||||
if vfr.climb is not None:
|
||||
msg.twist.linear.z = vfr.climb
|
||||
|
||||
publisher.publish(msg)
|
||||
|
||||
def _publish_battery(self, sysid: int, status: mvv.ComponentStatus):
|
||||
"""發布電池狀態"""
|
||||
if not self.rate_controller.should_publish(sysid, 'battery'):
|
||||
return
|
||||
|
||||
bat = status.battery
|
||||
if bat.voltage is None:
|
||||
return
|
||||
|
||||
publisher = self._get_or_create_publisher(sysid, 'battery', sensor_msgs.msg.BatteryState)
|
||||
|
||||
if publisher.get_subscription_count() == 0:
|
||||
return
|
||||
|
||||
msg = sensor_msgs.msg.BatteryState()
|
||||
msg.voltage = bat.voltage
|
||||
|
||||
if bat.current is not None:
|
||||
msg.current = bat.current
|
||||
|
||||
if bat.remaining is not None:
|
||||
msg.percentage = bat.remaining / 100.0
|
||||
|
||||
if bat.temperature is not None:
|
||||
msg.temperature = bat.temperature
|
||||
|
||||
publisher.publish(msg)
|
||||
|
||||
def _publish_vfr_hud(self, sysid: int, status: mvv.ComponentStatus):
|
||||
"""發布 VFR HUD"""
|
||||
if not self.rate_controller.should_publish(sysid, 'vfr_hud'):
|
||||
return
|
||||
|
||||
vfr = status.vfr
|
||||
if vfr.airspeed is None:
|
||||
return
|
||||
|
||||
publisher = self._get_or_create_publisher(sysid, 'vfr_hud', mavros_msgs.msg.VfrHud)
|
||||
|
||||
if publisher.get_subscription_count() == 0:
|
||||
return
|
||||
|
||||
msg = mavros_msgs.msg.VfrHud()
|
||||
msg.airspeed = vfr.airspeed if vfr.airspeed is not None else 0.0
|
||||
msg.groundspeed = vfr.groundspeed if vfr.groundspeed is not None else 0.0
|
||||
msg.heading = vfr.heading if vfr.heading is not None else 0
|
||||
msg.throttle = float(vfr.throttle) if vfr.throttle is not None else 0.0
|
||||
msg.climb = vfr.climb if vfr.climb is not None else 0.0
|
||||
|
||||
# altitude 需要從 position 獲取
|
||||
if status.position.altitude is not None:
|
||||
msg.altitude = status.position.altitude
|
||||
|
||||
publisher.publish(msg)
|
||||
|
||||
def _publish_mode(self, sysid: int, status: mvv.ComponentStatus):
|
||||
"""發布飛行模式"""
|
||||
if not self.rate_controller.should_publish(sysid, 'mode'):
|
||||
return
|
||||
|
||||
mode = status.mode
|
||||
if mode.mode_name is None:
|
||||
return
|
||||
|
||||
publisher = self._get_or_create_publisher(sysid, 'mode', std_msgs.msg.String)
|
||||
|
||||
if publisher.get_subscription_count() == 0:
|
||||
return
|
||||
|
||||
msg = std_msgs.msg.String()
|
||||
msg.data = mode.mode_name
|
||||
publisher.publish(msg)
|
||||
|
||||
def _publish_summary(self, vehicle: mvv.VehicleView):
|
||||
"""
|
||||
發布載具摘要資訊(自定義格式,使用 String 暫時代替)
|
||||
TODO: 未來可以定義專門的 VehicleSummary.msg
|
||||
"""
|
||||
sysid = vehicle.sysid
|
||||
|
||||
if not self.rate_controller.should_publish(sysid, 'summary'):
|
||||
return
|
||||
|
||||
publisher = self._get_or_create_publisher(sysid, 'summary', std_msgs.msg.String)
|
||||
|
||||
if publisher.get_subscription_count() == 0:
|
||||
return
|
||||
|
||||
# 獲取 autopilot component
|
||||
component = vehicle.get_component(1)
|
||||
if not component:
|
||||
return
|
||||
|
||||
status = component.status
|
||||
|
||||
# 構建摘要資訊(JSON 格式字串)
|
||||
import json
|
||||
summary = {
|
||||
'sysid': sysid,
|
||||
'vehicle_type': vehicle.vehicle_type if vehicle.vehicle_type else 0,
|
||||
'autopilot': component.mav_autopilot if component.mav_autopilot else 0,
|
||||
'socket_id': vehicle.custom_meta.get('socket_id', -1), # 重要!
|
||||
'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_name': status.mode.mode_name if status.mode.mode_name else "UNKNOWN",
|
||||
'latitude': status.position.latitude if status.position.latitude else 0.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,
|
||||
'last_update': component.packet_stats.last_msg_time if component.packet_stats.last_msg_time else 0.0,
|
||||
}
|
||||
|
||||
msg = std_msgs.msg.String()
|
||||
msg.data = json.dumps(summary)
|
||||
publisher.publish(msg)
|
||||
|
||||
@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):
|
||||
"""停止發布"""
|
||||
self.running = False
|
||||
# logger.info("VehicleStatusPublisher stopped")
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# MavlinkCommandService Node
|
||||
# ============================================================================
|
||||
|
||||
class MavlinkCommandService(Node):
|
||||
"""
|
||||
MAVLink 指令服務節點 - 提供 ROS2 service 介面來發送 MAVLink 指令
|
||||
|
||||
職責:
|
||||
- 作為 service server,等待 client 請求
|
||||
- 接收請求,組裝 MAVLink 封包
|
||||
- 調用 mavlinkObject 發送封包
|
||||
- 處理 ACK 等待和超時(未來實現)
|
||||
|
||||
TODO: 稍後實現
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__('mavlink_command_service')
|
||||
|
||||
logger.info("MavlinkCommandService initialized (not implemented yet)")
|
||||
|
||||
def stop(self):
|
||||
"""停止服務"""
|
||||
# logger.info("MavlinkCommandService stopped")
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# ROS2 節點管理器
|
||||
# ============================================================================
|
||||
|
||||
class fc_ros_manager:
|
||||
"""
|
||||
MAVLink ROS2 節點管理器
|
||||
|
||||
管理兩個獨立的 ROS2 Node:
|
||||
- VehicleStatusPublisher
|
||||
- MavlinkCommandService
|
||||
|
||||
提供統一的啟動/停止介面給 mainOrchestrator
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.initialized = False
|
||||
self.running = False
|
||||
|
||||
# 两个 node 实例
|
||||
self.status_publisher: Optional[VehicleStatusPublisher] = None
|
||||
self.command_service: Optional[MavlinkCommandService] = None
|
||||
|
||||
# Executor & Thread
|
||||
self.spin_thread: Optional[threading.Thread] = None
|
||||
self.executor: Optional[MultiThreadedExecutor] = None
|
||||
|
||||
def initialize(self):
|
||||
"""初始化 ROS2 环境和 nodes"""
|
||||
if self.initialized:
|
||||
logger.warning("fc_ros_manager already initialized")
|
||||
return False
|
||||
|
||||
try:
|
||||
# 初始化 ROS2
|
||||
rclpy.init()
|
||||
|
||||
# 創建節點 node
|
||||
self.status_publisher = VehicleStatusPublisher()
|
||||
self.command_service = MavlinkCommandService()
|
||||
|
||||
# 創建執行者 MultiThreadedExecutor
|
||||
self.executor = MultiThreadedExecutor()
|
||||
self.executor.add_node(self.status_publisher)
|
||||
self.executor.add_node(self.command_service)
|
||||
|
||||
self.initialized = True
|
||||
# logger.info("fc_ros_manager initialized")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to initialize fc_ros_manager: {e}")
|
||||
return False
|
||||
|
||||
def start(self):
|
||||
"""啟動 ROS2 nodes (在獨立執行緒中運行 executor) """
|
||||
if not self.initialized:
|
||||
logger.error("fc_ros_manager initialize failed or not called")
|
||||
return False
|
||||
|
||||
if self.running:
|
||||
logger.warning("fc_ros_manager already running")
|
||||
return False
|
||||
|
||||
try:
|
||||
self.running = True
|
||||
|
||||
self.spin_thread = threading.Thread(
|
||||
target=self._spin_executor,
|
||||
daemon=True,
|
||||
name="ROS2ExecutorThread"
|
||||
)
|
||||
self.spin_thread.start()
|
||||
|
||||
logger.info("fc_ros_manager started <-")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to start fc_ros_manager: {e}")
|
||||
self.running = False
|
||||
return False
|
||||
|
||||
def _spin_executor(self):
|
||||
"""在 thread 中運行的 executor"""
|
||||
try:
|
||||
# logger.info("ROS2 executor spinning...")
|
||||
while self.running:
|
||||
self.executor.spin_once(timeout_sec=0.1)
|
||||
except Exception as e:
|
||||
logger.error(f"fc_ros_manager error in spinning executor: {e}")
|
||||
|
||||
def stop(self):
|
||||
"""停止 ROS2 nodes"""
|
||||
if not self.running:
|
||||
logger.warning("fc_ros_manager not running")
|
||||
return False
|
||||
|
||||
try:
|
||||
# 標記停止
|
||||
self.running = False
|
||||
|
||||
# 停止各個 node
|
||||
if self.status_publisher:
|
||||
self.status_publisher.stop()
|
||||
if self.command_service:
|
||||
self.command_service.stop()
|
||||
|
||||
# 等待 spin 執行緒結束
|
||||
if self.spin_thread and self.spin_thread.is_alive():
|
||||
self.spin_thread.join(timeout=2.0)
|
||||
|
||||
logger.info("fc_ros_manager thread END!")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error stopping fc_ros_manager: {e}")
|
||||
return False
|
||||
|
||||
def shutdown(self):
|
||||
"""完全關閉並清理資源"""
|
||||
if self.running:
|
||||
self.stop()
|
||||
|
||||
if self.initialized:
|
||||
try:
|
||||
# 銷毀 nodes
|
||||
if self.status_publisher:
|
||||
self.status_publisher.destroy_node()
|
||||
if self.command_service:
|
||||
self.command_service.destroy_node()
|
||||
|
||||
# 關閉 ROS2
|
||||
if rclpy.ok():
|
||||
rclpy.shutdown()
|
||||
|
||||
self.initialized = False
|
||||
logger.info("fc_ros_manager Node END!")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error during shutdown: {e}")
|
||||
|
||||
def get_status(self) -> dict:
|
||||
return {
|
||||
'initialized': self.initialized,
|
||||
'running': self.running,
|
||||
'status_publisher_active': self.status_publisher is not None and self.status_publisher.running,
|
||||
'command_service_active': self.command_service is not None,
|
||||
}
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# 全域實例
|
||||
# ============================================================================
|
||||
|
||||
# 全域管理器實例(供 mainOrchestrator 使用)
|
||||
ros2_manager = fc_ros_manager()
|
||||
|
||||
|
||||
'''
|
||||
================= 改版記錄 ============================
|
||||
|
||||
2026.01.20
|
||||
1. 重構自 mavlinkPublish.py (該檔案將被棄用)
|
||||
2. 提供 fc_ros_manager 統一管理介面
|
||||
3. 實現 VehicleStatusPublisher - 從 vehicle_registry 讀取並發布狀態
|
||||
4. 添加頻率控制器 控制各 topic 發布頻率 以及是否發布
|
||||
5. 預留 MavlinkCommandService 結構(稍後實現)
|
||||
|
||||
'''
|
||||
@@ -0,0 +1,507 @@
|
||||
"""
|
||||
VehicleStatusPublisher 測試程式
|
||||
|
||||
測試從 vehicle_registry 讀取資料並發布到 ROS2 topics
|
||||
"""
|
||||
|
||||
import time
|
||||
import json
|
||||
import threading
|
||||
|
||||
# ROS2 imports
|
||||
import rclpy
|
||||
from rclpy.node import Node
|
||||
|
||||
# 標準 ROS2 消息類型
|
||||
import std_msgs.msg
|
||||
import sensor_msgs.msg
|
||||
import geometry_msgs.msg
|
||||
import mavros_msgs.msg
|
||||
|
||||
# 專案 imports
|
||||
from ..fc_network_adapter.mavlinkROS2Nodes import (
|
||||
VehicleStatusPublisher,
|
||||
fc_ros_manager,
|
||||
ros2_manager
|
||||
)
|
||||
from ..fc_network_adapter.mavlinkVehicleView import (
|
||||
vehicle_registry,
|
||||
ConnectionType,
|
||||
ComponentType,
|
||||
)
|
||||
|
||||
|
||||
class TestSubscriber(Node):
|
||||
"""測試用的訂閱者節點 - 接收並記錄收到的消息"""
|
||||
|
||||
def __init__(self, sysid: int = 1):
|
||||
super().__init__(f'test_subscriber_sys{sysid}')
|
||||
|
||||
self.sysid = sysid
|
||||
self.received_messages = {
|
||||
'position': [],
|
||||
'attitude': [],
|
||||
'velocity': [],
|
||||
'battery': [],
|
||||
'vfr_hud': [],
|
||||
'mode': [],
|
||||
'summary': [],
|
||||
}
|
||||
|
||||
# 建立所有訂閱者
|
||||
self._create_subscriptions()
|
||||
|
||||
print(f"[TestSubscriber] 已建立,監聽 sys{sysid} 的所有 topics")
|
||||
|
||||
def _create_subscriptions(self):
|
||||
"""建立所有 topic 的訂閱者"""
|
||||
|
||||
base_topic = f'{VehicleStatusPublisher.topicString_prefix}/sys{self.sysid}'
|
||||
|
||||
# Position
|
||||
self.create_subscription(
|
||||
sensor_msgs.msg.NavSatFix,
|
||||
f'{base_topic}/position',
|
||||
lambda msg: self._on_message('position', msg),
|
||||
10
|
||||
)
|
||||
|
||||
# Attitude
|
||||
self.create_subscription(
|
||||
sensor_msgs.msg.Imu,
|
||||
f'{base_topic}/attitude',
|
||||
lambda msg: self._on_message('attitude', msg),
|
||||
10
|
||||
)
|
||||
|
||||
# Velocity
|
||||
self.create_subscription(
|
||||
geometry_msgs.msg.TwistStamped,
|
||||
f'{base_topic}/velocity',
|
||||
lambda msg: self._on_message('velocity', msg),
|
||||
10
|
||||
)
|
||||
|
||||
# Battery
|
||||
self.create_subscription(
|
||||
sensor_msgs.msg.BatteryState,
|
||||
f'{base_topic}/battery',
|
||||
lambda msg: self._on_message('battery', msg),
|
||||
10
|
||||
)
|
||||
|
||||
# VFR HUD
|
||||
self.create_subscription(
|
||||
mavros_msgs.msg.VfrHud,
|
||||
f'{base_topic}/vfr_hud',
|
||||
lambda msg: self._on_message('vfr_hud', msg),
|
||||
10
|
||||
)
|
||||
|
||||
# Mode
|
||||
self.create_subscription(
|
||||
std_msgs.msg.String,
|
||||
f'{base_topic}/mode',
|
||||
lambda msg: self._on_message('mode', msg),
|
||||
10
|
||||
)
|
||||
|
||||
# Summary
|
||||
self.create_subscription(
|
||||
std_msgs.msg.String,
|
||||
f'{base_topic}/summary',
|
||||
lambda msg: self._on_message('summary', msg),
|
||||
10
|
||||
)
|
||||
|
||||
def _on_message(self, topic_name: str, msg):
|
||||
"""通用消息接收回調"""
|
||||
self.received_messages[topic_name].append(msg)
|
||||
print(f"[TestSubscriber] 收到 {topic_name}: {self._format_msg(topic_name, msg)}")
|
||||
|
||||
def _format_msg(self, topic_name: str, msg) -> str:
|
||||
"""格式化消息以便顯示"""
|
||||
if topic_name == 'position':
|
||||
return f"lat={msg.latitude:.6f}, lon={msg.longitude:.6f}, alt={msg.altitude:.2f}m"
|
||||
elif topic_name == 'attitude':
|
||||
return f"quat=({msg.orientation.x:.3f}, {msg.orientation.y:.3f}, {msg.orientation.z:.3f}, {msg.orientation.w:.3f})"
|
||||
elif topic_name == 'velocity':
|
||||
return f"linear=({msg.twist.linear.x:.2f}, {msg.twist.linear.y:.2f}, {msg.twist.linear.z:.2f})"
|
||||
elif topic_name == 'battery':
|
||||
return f"voltage={msg.voltage:.2f}V, percent={msg.percentage*100:.1f}%"
|
||||
elif topic_name == 'vfr_hud':
|
||||
return f"airspeed={msg.airspeed:.2f}, groundspeed={msg.groundspeed:.2f}, heading={msg.heading}"
|
||||
elif topic_name == 'mode':
|
||||
return f"mode={msg.data}"
|
||||
elif topic_name == 'summary':
|
||||
try:
|
||||
data = json.loads(msg.data)
|
||||
return f"sysid={data['sysid']}, socket_id={data['socket_id']}, mode={data['mode_name']}"
|
||||
except:
|
||||
return msg.data
|
||||
return str(msg)
|
||||
|
||||
def get_message_count(self, topic_name: str) -> int:
|
||||
"""獲取收到的消息數量"""
|
||||
return len(self.received_messages[topic_name])
|
||||
|
||||
def clear_messages(self):
|
||||
"""清空已收到的消息"""
|
||||
for key in self.received_messages:
|
||||
self.received_messages[key].clear()
|
||||
|
||||
|
||||
def setup_test_vehicle(sysid: int = 1, socket_id: int = 10):
|
||||
"""
|
||||
建立測試用的載具數據
|
||||
|
||||
Args:
|
||||
sysid: 系統 ID
|
||||
socket_id: Socket ID
|
||||
"""
|
||||
print(f"\n=== 建立測試載具 (sysid={sysid}, socket_id={socket_id}) ===")
|
||||
|
||||
# 註冊載具
|
||||
vehicle = vehicle_registry.register(sysid)
|
||||
vehicle.kind = "Copter"
|
||||
vehicle.vehicle_type = 2 # MAV_TYPE_QUADROTOR
|
||||
vehicle.connected_via = ConnectionType.UDP
|
||||
vehicle.custom_meta['socket_id'] = socket_id
|
||||
|
||||
# 新增 autopilot 組件 (component_id=1)
|
||||
autopilot = vehicle.add_component(1, ComponentType.AUTOPILOT)
|
||||
autopilot.mav_type = 2 # MAV_TYPE_QUADROTOR
|
||||
autopilot.mav_autopilot = 3 # MAV_AUTOPILOT_ARDUPILOTMEGA
|
||||
|
||||
# 填充狀態資料
|
||||
status = autopilot.status
|
||||
|
||||
# 位置
|
||||
status.position.latitude = 25.0330
|
||||
status.position.longitude = 121.5654
|
||||
status.position.altitude = 100.5
|
||||
status.position.relative_altitude = 50.0
|
||||
status.position.timestamp = time.time()
|
||||
|
||||
# 姿態
|
||||
status.attitude.roll = 0.1
|
||||
status.attitude.pitch = -0.05
|
||||
status.attitude.yaw = 1.57
|
||||
status.attitude.rollspeed = 0.01
|
||||
status.attitude.pitchspeed = 0.02
|
||||
status.attitude.yawspeed = 0.03
|
||||
status.attitude.timestamp = time.time()
|
||||
|
||||
# 飛行模式
|
||||
status.mode.base_mode = 89
|
||||
status.mode.custom_mode = 4
|
||||
status.mode.mode_name = "GUIDED"
|
||||
status.mode.timestamp = time.time()
|
||||
|
||||
# 電池
|
||||
status.battery.voltage = 12.6
|
||||
status.battery.current = 15.3
|
||||
status.battery.remaining = 75
|
||||
status.battery.temperature = 35.2
|
||||
status.battery.timestamp = time.time()
|
||||
|
||||
# GPS
|
||||
status.gps.fix_type = 3 # 3D fix
|
||||
status.gps.satellites_visible = 12
|
||||
status.gps.eph = 100
|
||||
status.gps.epv = 150
|
||||
status.gps.timestamp = time.time()
|
||||
|
||||
# VFR
|
||||
status.vfr.airspeed = 5.5
|
||||
status.vfr.groundspeed = 6.0
|
||||
status.vfr.heading = 90
|
||||
status.vfr.throttle = 65
|
||||
status.vfr.climb = 1.2
|
||||
status.vfr.timestamp = time.time()
|
||||
|
||||
# 系統狀態
|
||||
status.armed = True
|
||||
status.system_status = 4 # MAV_STATE_ACTIVE
|
||||
|
||||
# 更新封包統計
|
||||
autopilot.update_packet_stats(seq=10, msg_type=33, timestamp=time.time())
|
||||
|
||||
print(f"✓ 載具 {sysid} 已建立並填充測試數據")
|
||||
return vehicle
|
||||
|
||||
|
||||
def test_basic_publishing():
|
||||
"""測試基本的發布功能"""
|
||||
print("\n" + "="*60)
|
||||
print("測試 1: 基本發布功能")
|
||||
print("="*60)
|
||||
|
||||
# 清空 registry
|
||||
vehicle_registry.clear()
|
||||
|
||||
# 建立測試載具
|
||||
vehicle = setup_test_vehicle(sysid=1, socket_id=10)
|
||||
|
||||
# 初始化 ROS2 管理器
|
||||
if not ros2_manager.initialized:
|
||||
ros2_manager.initialize()
|
||||
|
||||
# 建立測試訂閱者
|
||||
test_sub = TestSubscriber(sysid=1)
|
||||
|
||||
# 啟動 publisher
|
||||
ros2_manager.start()
|
||||
|
||||
print("\n--- 開始發布,等待 5 秒 ---")
|
||||
|
||||
# 運行 5 秒,持續 spin
|
||||
start_time = time.time()
|
||||
while time.time() - start_time < 5.0:
|
||||
rclpy.spin_once(test_sub, timeout_sec=0.1)
|
||||
time.sleep(0.1)
|
||||
|
||||
# 檢查收到的消息
|
||||
print("\n--- 消息統計 ---")
|
||||
total_messages = 0
|
||||
for topic in ['position', 'attitude', 'velocity', 'battery', 'vfr_hud', 'mode', 'summary']:
|
||||
count = test_sub.get_message_count(topic)
|
||||
total_messages += count
|
||||
print(f" {topic:15s}: {count:3d} 條消息")
|
||||
|
||||
print(f"\n總計收到: {total_messages} 條消息")
|
||||
|
||||
# 驗證
|
||||
if total_messages > 0:
|
||||
print("\n✓ 測試通過:成功接收到消息")
|
||||
else:
|
||||
print("\n✗ 測試失敗:沒有接收到任何消息")
|
||||
|
||||
# 停止
|
||||
ros2_manager.stop()
|
||||
test_sub.destroy_node()
|
||||
|
||||
|
||||
def test_frequency_control():
|
||||
"""測試頻率控制功能"""
|
||||
print("\n" + "="*60)
|
||||
print("測試 2: 頻率控制")
|
||||
print("="*60)
|
||||
|
||||
# 清空 registry
|
||||
vehicle_registry.clear()
|
||||
|
||||
# 建立測試載具
|
||||
vehicle = setup_test_vehicle(sysid=1, socket_id=10)
|
||||
|
||||
# 初始化(如果還沒初始化)
|
||||
if not ros2_manager.initialized:
|
||||
ros2_manager.initialize()
|
||||
|
||||
# 建立測試訂閱者
|
||||
test_sub = TestSubscriber(sysid=1)
|
||||
|
||||
# 修改頻率設定
|
||||
publisher_node = ros2_manager.status_publisher
|
||||
publisher_node.rate_controller.topic_intervals = {
|
||||
'position': 1.5,
|
||||
'attitude': 1.0,
|
||||
'velocity': 1.0,
|
||||
'battery': 1.0,
|
||||
'vfr_hud': 0.5,
|
||||
'mode': 0.0,
|
||||
'summary': 0.0,
|
||||
}
|
||||
|
||||
# 啟動 publisher
|
||||
ros2_manager.start()
|
||||
|
||||
print("\n--- 測試頻率控制,運行 3 秒 ---")
|
||||
|
||||
# 運行 3 秒
|
||||
start_time = time.time()
|
||||
while time.time() - start_time < 3.0:
|
||||
rclpy.spin_once(test_sub, timeout_sec=0.1)
|
||||
time.sleep(0.1)
|
||||
|
||||
# 檢查頻率
|
||||
print("\n--- 頻率分析 ---")
|
||||
print("預期:position 約 2 條 (0.67Hz),attitude/battery/velocity 約 3 條 (1Hz),vfr_hud 約 6 條 (2Hz) mode/summary 不發布")
|
||||
|
||||
print("2Hz Topics (預期 ~6 條):")
|
||||
for topic in ['position', 'attitude', 'velocity', 'vfr_hud']:
|
||||
count = test_sub.get_message_count(topic)
|
||||
print(f" {topic:15s}: {count:3d} 條")
|
||||
for topic in ['battery', 'mode', 'summary']:
|
||||
count = test_sub.get_message_count(topic)
|
||||
print(f" {topic:15s}: {count:3d} 條")
|
||||
|
||||
print("\n✓ 頻率控制測試完成")
|
||||
|
||||
# 停止
|
||||
ros2_manager.stop()
|
||||
test_sub.destroy_node()
|
||||
|
||||
|
||||
def test_multi_vehicle():
|
||||
"""測試多載具發布"""
|
||||
print("\n" + "="*60)
|
||||
print("測試 3: 多載具發布")
|
||||
print("="*60)
|
||||
|
||||
# 清空 registry
|
||||
vehicle_registry.clear()
|
||||
|
||||
# 建立 3 個測試載具
|
||||
v1 = setup_test_vehicle(sysid=1, socket_id=10)
|
||||
v2 = setup_test_vehicle(sysid=2, socket_id=11)
|
||||
v3 = setup_test_vehicle(sysid=3, socket_id=12)
|
||||
|
||||
# 修改各載具的位置以便區分
|
||||
v2.components[1].status.position.latitude = 26.0
|
||||
v3.components[1].status.position.latitude = 27.0
|
||||
|
||||
# 初始化
|
||||
if not ros2_manager.initialized:
|
||||
ros2_manager.initialize()
|
||||
|
||||
# 建立 3 個測試訂閱者
|
||||
test_sub1 = TestSubscriber(sysid=1)
|
||||
test_sub2 = TestSubscriber(sysid=2)
|
||||
test_sub3 = TestSubscriber(sysid=3)
|
||||
|
||||
# 啟動 publisher
|
||||
ros2_manager.start()
|
||||
|
||||
print("\n--- 測試多載具,運行 3 秒 ---")
|
||||
|
||||
# 運行 3 秒
|
||||
start_time = time.time()
|
||||
while time.time() - start_time < 3.0:
|
||||
rclpy.spin_once(test_sub1, timeout_sec=0.05)
|
||||
rclpy.spin_once(test_sub2, timeout_sec=0.05)
|
||||
rclpy.spin_once(test_sub3, timeout_sec=0.05)
|
||||
time.sleep(0.1)
|
||||
|
||||
# 檢查每個載具的消息
|
||||
print("\n--- 各載具消息統計 ---")
|
||||
for sysid, test_sub in [(1, test_sub1), (2, test_sub2), (3, test_sub3)]:
|
||||
total = sum(test_sub.get_message_count(t) for t in test_sub.received_messages.keys())
|
||||
print(f"載具 {sysid}: {total:3d} 條消息")
|
||||
|
||||
# 檢查 summary 中的 socket_id
|
||||
if test_sub.get_message_count('summary') > 0:
|
||||
last_summary = test_sub.received_messages['summary'][-1]
|
||||
data = json.loads(last_summary.data)
|
||||
print(f" └─ socket_id={data['socket_id']}, lat={data['latitude']:.1f}")
|
||||
|
||||
print("\n✓ 多載具測試完成")
|
||||
|
||||
# 停止
|
||||
ros2_manager.stop()
|
||||
test_sub1.destroy_node()
|
||||
test_sub2.destroy_node()
|
||||
test_sub3.destroy_node()
|
||||
|
||||
|
||||
def test_dynamic_vehicle():
|
||||
"""測試動態新增/移除載具"""
|
||||
print("\n" + "="*60)
|
||||
print("測試 4: 動態載具管理")
|
||||
print("="*60)
|
||||
|
||||
# 清空 registry
|
||||
vehicle_registry.clear()
|
||||
|
||||
# 初始化
|
||||
if not ros2_manager.initialized:
|
||||
ros2_manager.initialize()
|
||||
|
||||
# 建立測試訂閱者
|
||||
test_sub = TestSubscriber(sysid=1)
|
||||
|
||||
# 啟動 publisher
|
||||
ros2_manager.start()
|
||||
|
||||
print("\n--- 階段 1: 無載具,運行 1 秒 ---")
|
||||
start_time = time.time()
|
||||
while time.time() - start_time < 1.0:
|
||||
rclpy.spin_once(test_sub, timeout_sec=0.1)
|
||||
time.sleep(0.1)
|
||||
|
||||
count_before = sum(test_sub.get_message_count(t) for t in test_sub.received_messages.keys())
|
||||
print(f"收到消息: {count_before} 條")
|
||||
|
||||
# 新增載具
|
||||
print("\n--- 階段 2: 新增載具,運行 2 秒 ---")
|
||||
vehicle = setup_test_vehicle(sysid=1, socket_id=10)
|
||||
|
||||
start_time = time.time()
|
||||
while time.time() - start_time < 2.0:
|
||||
rclpy.spin_once(test_sub, timeout_sec=0.1)
|
||||
time.sleep(0.1)
|
||||
|
||||
count_after = sum(test_sub.get_message_count(t) for t in test_sub.received_messages.keys())
|
||||
print(f"收到消息: {count_after - count_before} 條")
|
||||
|
||||
# 移除載具
|
||||
print("\n--- 階段 3: 移除載具,運行 1 秒 ---")
|
||||
vehicle_registry.unregister(1)
|
||||
|
||||
start_time = time.time()
|
||||
while time.time() - start_time < 1.0:
|
||||
rclpy.spin_once(test_sub, timeout_sec=0.1)
|
||||
time.sleep(0.1)
|
||||
|
||||
count_final = sum(test_sub.get_message_count(t) for t in test_sub.received_messages.keys())
|
||||
print(f"收到消息: {count_final - count_after} 條(應該為 0)")
|
||||
|
||||
if count_final - count_after == 0:
|
||||
print("\n✓ 動態載具管理測試通過")
|
||||
else:
|
||||
print("\n✗ 移除載具後仍收到消息")
|
||||
|
||||
# 停止
|
||||
ros2_manager.stop()
|
||||
test_sub.destroy_node()
|
||||
|
||||
|
||||
def main():
|
||||
"""主測試函數"""
|
||||
print("\n" + "="*60)
|
||||
print("VehicleStatusPublisher 測試程式")
|
||||
print("="*60)
|
||||
|
||||
try:
|
||||
# 執行各項測試
|
||||
test_basic_publishing()
|
||||
# time.sleep(1)
|
||||
|
||||
# test_frequency_control()
|
||||
# time.sleep(1)
|
||||
|
||||
# test_multi_vehicle()
|
||||
# time.sleep(1)
|
||||
|
||||
# test_dynamic_vehicle()
|
||||
|
||||
print("\n" + "="*60)
|
||||
print("所有測試完成!")
|
||||
print("="*60)
|
||||
|
||||
except KeyboardInterrupt:
|
||||
print("\n\n測試被中斷")
|
||||
except Exception as e:
|
||||
print(f"\n\n測試出錯: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
finally:
|
||||
# 清理
|
||||
if ros2_manager.initialized:
|
||||
ros2_manager.shutdown()
|
||||
vehicle_registry.clear()
|
||||
print("\n清理完成")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
Reference in New Issue
Block a user