""" 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 import fc_interfaces.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_gnss': [], '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( fc_interfaces.msg.GnssRaw, f'{base_topic}/position_gnss', lambda msg: self._on_message('position_gnss', 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_gnss': 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_gnss', '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_gnss': 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_gnss', '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()