#!/usr/bin/env python """ 測試腳本,用於測試 mavlinkObject.py 中的 mavlink_object 和 async_io_manager 類別 """ import os import sys sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) import unittest import time import threading import socket import asyncio # 導入要測試的模組 from ..fc_network_adapter.mavlinkObject import ( mavlink_object, async_io_manager, MavlinkObjectState, stream_bridge_ring, return_packet_ring ) # 預先定義好的真實 MAVLink heartbeat 封包 (MAVLink 1.0 格式) # Format: STX(0xFE) + LEN + SEQ + SYS + COMP + MSG_ID + PAYLOAD(9 bytes for heartbeat) + CRC(2 bytes) HEARTBEAT_PACKET_1 = bytes([ 0xFE, # STX (MAVLink 1.0) 0x09, # payload length (9 bytes) 0x00, # sequence 0x01, # system ID = 1 0x01, # component ID = 1 0x00, # message ID (HEARTBEAT = 0) # Payload (9 bytes): custom_mode(4), type(1), autopilot(1), base_mode(1), system_status(1), mavlink_version(1) 0x00, 0x00, 0x00, 0x00, # custom_mode = 0 0x02, # type = MAV_TYPE_QUADROTOR (2) 0x03, # autopilot = MAV_AUTOPILOT_ARDUPILOTMEGA (3) 0x40, # base_mode = MAV_MODE_FLAG_CUSTOM_MODE_ENABLED (64) 0x03, # system_status = MAV_STATE_STANDBY (3) 0x03, # mavlink_version = 3 0x62, 0x8E # CRC (simplified placeholder) ]) HEARTBEAT_PACKET_2 = bytes([ 0xFE, # STX 0x09, # payload length 0x01, # sequence (增加) 0x01, # system ID = 1 0x01, # component ID = 1 0x00, # message ID (HEARTBEAT = 0) 0x00, 0x00, 0x00, 0x00, 0x02, 0x03, 0x41, 0x03, 0x03, 0x33, 0xEC ]) HEARTBEAT_PACKET_3 = bytes([ 0xFE, # STX 0x09, # payload length 0x02, # sequence 0x02, # system ID = 2 0x01, # component ID = 1 0x00, # message ID (HEARTBEAT = 0) 0x00, 0x00, 0x00, 0x00, 0x02, 0x03, 0x42, 0x03, 0x03, 0x37, 0x44 ]) class MockMavlinkSocket: """模擬 Mavlink Socket 的類別,用於測試 使用真實的 MAVLink 封包,而不是模擬的訊息對象 """ def __init__(self, test_packets=None): """ Args: test_packets: list of bytes,每個元素都是完整的 MAVLink 封包 """ self.closed = False self.test_packets = test_packets or [] self.packet_index = 0 self.written_data = [] # 使用 pymavlink 來解析封包 from pymavlink import mavutil self.mav_parser = mavutil.mavlink.MAVLink(self) def recv_msg(self): """返回解析後的 MAVLink 訊息對象""" if not self.test_packets or self.packet_index >= len(self.test_packets): return None packet = self.test_packets[self.packet_index] self.packet_index += 1 # 使用 pymavlink 解析封包 try: for byte in packet: msg = self.mav_parser.parse_char(bytes([byte])) if msg: return msg except Exception as e: print(f"Error parsing packet: {e}") return None return None def write(self, data): """寫入數據(用於檢查轉發)""" self.written_data.append(data) def close(self): """關閉 socket""" self.closed = True class TestMavlinkObject(unittest.TestCase): """測試 mavlink_object 類別的獨立功能""" def setUp(self): """在每個測試方法執行前準備環境""" # 清空全局變數 mavlink_object.mavlinkObjects = {} mavlink_object.socket_num = 0 # 清空 ring buffer stream_bridge_ring.clear() return_packet_ring.clear() # 創建模擬的 socket,使用真實封包 self.mock_socket = MockMavlinkSocket([HEARTBEAT_PACKET_1]) # 創建測試對象 self.mavlink_obj = mavlink_object(self.mock_socket) def test_initialization(self): """測試 mavlink_object 初始化是否正確""" self.assertEqual(self.mavlink_obj.socket_id, 0) self.assertEqual(self.mavlink_obj.state, MavlinkObjectState.INIT) self.assertEqual(len(self.mavlink_obj.target_sockets), 0) self.assertEqual(self.mavlink_obj.bridge_msg_types, [0]) self.assertEqual(self.mavlink_obj.return_msg_types, []) def test_add_remove_target_socket(self): """測試添加和移除目標端口功能""" # 添加目標端口 self.assertTrue(self.mavlink_obj.add_target_socket(1)) self.assertEqual(len(self.mavlink_obj.target_sockets), 1) self.assertEqual(self.mavlink_obj.target_sockets[0], 1) self.assertTrue(self.mavlink_obj.add_target_socket(2)) self.assertEqual(len(self.mavlink_obj.target_sockets), 2) self.assertIn(2, self.mavlink_obj.target_sockets) # 嘗試添加已存在的端口 self.assertFalse(self.mavlink_obj.add_target_socket(1)) self.assertEqual(len(self.mavlink_obj.target_sockets), 2) # 嘗試添加自己的端口 self.assertFalse(self.mavlink_obj.add_target_socket(0)) self.assertEqual(len(self.mavlink_obj.target_sockets), 2) # 移除端口 self.assertTrue(self.mavlink_obj.remove_target_socket(2)) self.assertEqual(len(self.mavlink_obj.target_sockets), 1) # 嘗試移除不存在的端口 self.assertFalse(self.mavlink_obj.remove_target_socket(2)) def test_set_message_types(self): """測試設置訊息類型功能""" # 設置橋接器訊息類型 self.assertTrue(self.mavlink_obj.set_bridge_message_types([0, 30])) self.assertEqual(self.mavlink_obj.bridge_msg_types, [0, 30]) # 設置回傳處理器訊息類型 self.assertTrue(self.mavlink_obj.set_return_message_types([32])) self.assertEqual(self.mavlink_obj.return_msg_types, [32]) # 測試無效的訊息類型 self.assertFalse(self.mavlink_obj.set_bridge_message_types("invalid")) self.assertFalse(self.mavlink_obj.set_return_message_types([0, "invalid"])) def test_send_message_validation(self): """測試 send_message 的數據驗證功能(不需要啟動 manager)""" # 測試非運行狀態下發送消息 self.assertFalse(self.mavlink_obj.send_message(HEARTBEAT_PACKET_1)) # 測試無效的數據類型 self.mavlink_obj.state = MavlinkObjectState.RUNNING # 臨時設置狀態 self.assertFalse(self.mavlink_obj.send_message("invalid")) self.assertFalse(self.mavlink_obj.send_message(123)) # 測試太短的封包 self.assertFalse(self.mavlink_obj.send_message(bytes([0xFE, 0x00]))) # 測試無效的起始標記 invalid_packet = bytes([0xFF, 0x09, 0x00, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00]) self.assertFalse(self.mavlink_obj.send_message(invalid_packet)) # 測試有效的封包可以加入佇列 self.assertTrue(self.mavlink_obj.send_message(HEARTBEAT_PACKET_1)) self.assertEqual(len(self.mavlink_obj.outgoing_msgs), 1) self.mavlink_obj.state = MavlinkObjectState.INIT # 恢復狀態 class TestAsyncIOManager(unittest.TestCase): """測試 async_io_manager 類別的獨立功能""" def setUp(self): """在每個測試方法執行前準備環境""" # 清空全局變數 mavlink_object.mavlinkObjects = {} mavlink_object.socket_num = 0 # 清空 ring buffer stream_bridge_ring.clear() return_packet_ring.clear() # 創建 async_io_manager 實例 self.manager = async_io_manager() # 創建模擬 mavlink 對象,使用真實封包 self.mock_socket1 = MockMavlinkSocket([HEARTBEAT_PACKET_1, HEARTBEAT_PACKET_2]) self.mock_socket2 = MockMavlinkSocket([HEARTBEAT_PACKET_3]) self.mavlink_obj1 = mavlink_object(self.mock_socket1) self.mavlink_obj2 = mavlink_object(self.mock_socket2) def tearDown(self): """在每個測試方法執行後清理環境""" if self.manager.running: self.manager.shutdown() def test_singleton_pattern(self): """測試 async_io_manager 的單例模式""" manager1 = async_io_manager() manager2 = async_io_manager() self.assertIs(manager1, manager2) def test_start_stop(self): """測試 async_io_manager 的啟動和停止功能""" # 啟動管理器 self.manager.start() self.assertTrue(self.manager.running) self.assertIsNotNone(self.manager.thread) # 再次啟動應該沒有效果 old_thread = self.manager.thread self.manager.start() self.assertIs(self.manager.thread, old_thread) # 停止管理器 self.manager.shutdown() self.assertFalse(self.manager.running) # 最多等待 5 秒讓線程結束 start_time = time.time() while self.manager.thread.is_alive() and time.time() - start_time < 5: time.sleep(0.1) self.assertFalse(self.manager.thread.is_alive()) def test_add_remove_objects(self): """測試添加和移除 mavlink_object""" # 啟動管理器 self.manager.start() time.sleep(0.5) # 等待事件循環啟動 # 添加對象 self.assertTrue(self.manager.add_mavlink_object(self.mavlink_obj1)) self.assertEqual(len(self.manager.managed_objects), 1) self.assertEqual(self.mavlink_obj1.state, MavlinkObjectState.RUNNING) # 添加另一個對象 self.assertTrue(self.manager.add_mavlink_object(self.mavlink_obj2)) self.assertEqual(len(self.manager.managed_objects), 2) # 檢查受管理對象列表 managed_objects = self.manager.get_managed_objects() self.assertEqual(len(managed_objects), 2) self.assertIn(0, managed_objects) self.assertIn(1, managed_objects) # 移除對象 self.assertTrue(self.manager.remove_mavlink_object(0)) self.assertEqual(len(self.manager.managed_objects), 1) # 嘗試移除不存在的對象 self.assertFalse(self.manager.remove_mavlink_object(999)) # 停止管理器 self.manager.shutdown() class TestIntegration(unittest.TestCase): """整合測試,測試多個 mavlink_object 之間的互動與資料流""" def setUp(self): """在每個測試方法執行前準備環境""" # 清空全局變數 mavlink_object.mavlinkObjects = {} mavlink_object.socket_num = 0 # 清空 ring buffer stream_bridge_ring.clear() return_packet_ring.clear() # 創建 async_io_manager 實例 self.manager = async_io_manager() def tearDown(self): """在每個測試方法執行後清理環境""" if self.manager.running: self.manager.shutdown() def test_send_message_with_manager(self): """測試透過 async_io_manager 發送訊息的完整流程""" # 創建一個新的 mavlink_object 實例 mock_socket = MockMavlinkSocket() mavlink_obj = mavlink_object(mock_socket) # 測試初始狀態 self.assertEqual(len(mock_socket.written_data), 0) # 測試非運行狀態下發送消息 self.assertFalse(mavlink_obj.send_message(HEARTBEAT_PACKET_1)) self.assertEqual(len(mock_socket.written_data), 0) # 啟動 manager self.manager.start() time.sleep(0.5) # 等待事件循環啟動 # 添加對象到 manager self.manager.add_mavlink_object(mavlink_obj) time.sleep(0.1) # 等待對象啟動 # 確認對象狀態 self.assertEqual(mavlink_obj.state, MavlinkObjectState.RUNNING) # 測試發送消息 self.assertTrue(mavlink_obj.send_message(HEARTBEAT_PACKET_1)) time.sleep(0.2) # 等待消息處理 # 確認消息已發送 self.assertEqual(len(mock_socket.written_data), 1) self.assertEqual(mock_socket.written_data[0], HEARTBEAT_PACKET_1) # 測試連續發送多條消息 self.assertTrue(mavlink_obj.send_message(HEARTBEAT_PACKET_2)) time.sleep(0.2) # 等待消息處理 # 確認兩條消息都已發送 self.assertEqual(len(mock_socket.written_data), 2) self.assertEqual(mock_socket.written_data[1], HEARTBEAT_PACKET_2) # 停止 manager self.manager.shutdown() time.sleep(0.5) # 等待 manager 停止 # 測試對象已關閉後發送消息 self.assertFalse(mavlink_obj.send_message(HEARTBEAT_PACKET_1)) self.assertEqual(len(mock_socket.written_data), 2) # 消息數量未增加 def test_data_processing_and_forwarding(self): """測試數據處理與轉發流程""" # 創建用於轉發的 mavlink_objects mock_socket1 = MockMavlinkSocket([HEARTBEAT_PACKET_1, HEARTBEAT_PACKET_2,]) mock_socket3 = MockMavlinkSocket() mavlink_obj1 = mavlink_object(mock_socket1) mavlink_obj3 = mavlink_object(mock_socket3) # 設置訊息類型 mavlink_obj1.set_bridge_message_types([0]) # 只處理 HEARTBEAT # 設置轉發: obj1 -> obj3 mavlink_obj1.add_target_socket(mavlink_obj3.socket_id) # socket1 轉發到 socket3 (socket_id=1) # 啟動管理器並添加對象 self.manager.start() time.sleep(0.5) # 等待事件循環啟動 """ 這邊出現很奇怪的狀況 應該說 設計時沒有考量 但是實測會發現 mavlink_obj3 是接收端 必需要被優先加入 manager 才能正確接收來自 mavlink_obj1 的轉發封包 若先把 mavlink_ojb1 加入 manger 則可能會導致前面幾個封包丟失 """ self.manager.add_mavlink_object(mavlink_obj3) self.manager.add_mavlink_object(mavlink_obj1) # 等待處理完成 time.sleep(0.5) # 檢查 Ring buffer 是否有正確的數據 self.assertGreaterEqual(stream_bridge_ring.size(), 2) # 至少 2 個 HEARTBEAT # 檢查是否正確轉發 self.assertGreaterEqual(len(mock_socket3.written_data), 2) # 至少 2 個 HEARTBEAT # 停止管理器 self.manager.shutdown() def test_bidirectional_forwarding(self): """測試雙向轉發""" # 清空全局變數和 ring buffer mavlink_object.mavlinkObjects = {} mavlink_object.socket_num = 0 stream_bridge_ring.clear() return_packet_ring.clear() # 創建三個 mavlink 對象,模擬三個通道 socket1 = MockMavlinkSocket() socket2 = MockMavlinkSocket() socket3 = MockMavlinkSocket() obj1 = mavlink_object(socket1) obj2 = mavlink_object(socket2) obj3 = mavlink_object(socket3) # 設置雙向轉發 # obj1 <-> obj2 <-> obj3 obj1.add_target_socket(1) # obj1 -> obj2 obj2.add_target_socket(0) # obj2 -> obj1 obj2.add_target_socket(2) # obj2 -> obj3 obj3.add_target_socket(1) # obj3 -> obj2 # 啟動 async_io_manager self.manager.start() time.sleep(0.5) # 等待事件循環啟動 # 添加所有 mavlink_object self.manager.add_mavlink_object(obj1) self.manager.add_mavlink_object(obj2) self.manager.add_mavlink_object(obj3) # 對三個對象添加數據 socket1.test_packets.append(HEARTBEAT_PACKET_1) socket2.test_packets.append(HEARTBEAT_PACKET_2) socket3.test_packets.append(HEARTBEAT_PACKET_3) # 等待處理所有訊息 time.sleep(1.0) # 檢查轉發結果 # socket1 應該收到 socket2 的訊息 self.assertGreaterEqual(len(socket1.written_data), 1) # socket2 應該收到 socket1 和 socket3 的訊息 self.assertGreaterEqual(len(socket2.written_data), 2) # socket3 應該收到 socket2 的訊息 self.assertGreaterEqual(len(socket3.written_data), 1) # 檢查 ring buffer 的數據 # 所有對象都啟用了橋接器,且預設的 bridge_msg_types = [0] self.assertGreaterEqual(stream_bridge_ring.size(), 3) # 至少 3 個 HEARTBEAT # 停止管理器 self.manager.shutdown() if __name__ == "__main__": # 可以指定要運行的測試 # unittest.main(defaultTest="TestMavlinkObject.test_send_message_validation") # unittest.main(defaultTest="TestAsyncIOManager.test_add_remove_objects") unittest.main(defaultTest="TestIntegration.test_bidirectional_forwarding") unittest.main()