#!/usr/bin/env python3 from pymavlink import mavutil import socket import re import time import math import threading import json import os import datetime import queue import argparse from select import select class Drone: def __init__(self, instance_id, mavlink_port, udp_port): self.instance_id = instance_id self.mavlink_port = mavlink_port self.udp_port = udp_port self.running = True self.connection = None # 創建數據緩存 self.latest_position = None # 創建日誌佇列 self.log_queue = queue.Queue() # 創建日誌文件目錄 self.log_dir = "drone_logs" if not os.path.exists(self.log_dir): os.makedirs(self.log_dir) # 創建一個含時間戳的日誌文件名 timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S") self.log_file = os.path.join(self.log_dir, f"drone_{self.instance_id}_{timestamp}.log") # 通訊統計 self.packet_counter = 0 self.start_time = time.time() self.packets_per_second = 0 def connect(self): """僅建立MAVLink連接""" print(f"Drone {self.instance_id}: 建立MAVLink連接 (Port {self.mavlink_port})...") connection_string = f"udp:127.0.0.1:{self.mavlink_port}" self.connection = mavutil.mavlink_connection(connection_string) self.connection.wait_heartbeat() print(f"Drone {self.instance_id}: MAVLink連接已建立") def set_guided_mode(self): """切換到GUIDED模式""" self.connection.mav.set_mode_send( self.connection.target_system, mavutil.mavlink.MAV_MODE_FLAG_CUSTOM_MODE_ENABLED, 4 # GUIDED模式ID ) print(f"Drone {self.instance_id}: 已切換到GUIDED模式") def arm(self): """解鎖無人機""" self.connection.mav.command_long_send( self.connection.target_system, self.connection.target_component, mavutil.mavlink.MAV_CMD_COMPONENT_ARM_DISARM, 0, 1, 0, 0, 0, 0, 0, 0 # 1表示解鎖 ) print(f"Drone {self.instance_id}: 無人機已解鎖") def takeoff(self, altitude=3.0): """起飛到指定高度""" self.connection.mav.command_long_send( self.connection.target_system, self.connection.target_component, mavutil.mavlink.MAV_CMD_NAV_TAKEOFF, 0, 0, 0, 0, 0, 0, 0, altitude ) print(f"Drone {self.instance_id}: 起飛命令已發送,目標高度{altitude}公尺") def mavlink_listener(self): """事件驅動方式監聽MAVLink訊息""" # 獲取底層socket mavlink_socket = self.connection.port.fileno() while self.running: # 使用select等待數據,無須sleep readable, _, _ = select([mavlink_socket], [], [], 0.001) if readable: msg = self.connection.recv_msg() if msg and msg.get_type() == 'LOCAL_POSITION_NED': self.latest_position = msg def log_worker(self): """事件驅動方式處理日誌寫入""" while self.running: if not self.log_queue.empty(): try: # 批量處理日誌 batch = [] while not self.log_queue.empty() and len(batch) < 100: try: entry = self.log_queue.get_nowait() batch.append(entry) except queue.Empty: break # 寫入日誌 if batch: with open(self.log_file, 'a', encoding='utf-8') as f: for entry in batch: f.write(json.dumps(entry, ensure_ascii=False) + "\n") except Exception as e: print(f"Drone {self.instance_id}: 日誌寫入錯誤: {e}") # 每秒計算一次統計數據 now = time.time() if now - self.start_time >= 1.0: self.packets_per_second = self.packet_counter print(f"Drone {self.instance_id} - 每秒封包數: {self.packets_per_second}") self.packet_counter = 0 self.start_time = now # 使用select等待短暫時間而非sleep select([], [], [], 0.01) # 10ms的微小等待 def log_packet(self, packet_data, packet_num): """將數據加入日誌佇列""" try: timestamp = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S.%f")[:-3] log_entry = { "timestamp": timestamp, "packet_number": packet_num, "data": packet_data } self.log_queue.put(log_entry) except Exception as e: print(f"Drone {self.instance_id}: 佇列添加錯誤: {e}") def run(self): """使用事件驅動方式處理UDP通訊""" # 啟動背景線程 mavlink_thread = threading.Thread(target=self.mavlink_listener) mavlink_thread.daemon = True mavlink_thread.start() log_thread = threading.Thread(target=self.log_worker) log_thread.daemon = True log_thread.start() # 設置UDP服務器 server_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_RCVBUF, 262144) # 增大接收緩衝區 server_socket.bind(('0.0.0.0', self.udp_port)) server_socket.setblocking(0) # 非阻塞模式 print(f"Drone {self.instance_id}: UDP服務器已啟動,監聽端口{self.udp_port}") print(f"Drone {self.instance_id}: 日誌將保存至 {self.log_file}") # 主事件循環 while self.running: # 使用select監聽UDP readable, _, _ = select([server_socket], [], [], 0.001) if readable: try: data, _ = server_socket.recvfrom(4096) decoded_data = data.decode('utf-8') # 增加計數器 self.packet_counter += 1 total_packets = self.packet_counter try: # 解析JSON json_data = json.loads(decoded_data) # 寫入日誌 self.log_packet(json_data, total_packets) # 提取速度數據 vel_match = re.search(r'"next_velocity":\[([-\d\.\,]+)\]', decoded_data) if vel_match: velocity_str = vel_match.group(1) velocity = [float(x) for x in velocity_str.split(',')] vx, vy, vz = velocity # 設置type_mask - 僅使用速度控制 type_mask = ( mavutil.mavlink.POSITION_TARGET_TYPEMASK_X_IGNORE | mavutil.mavlink.POSITION_TARGET_TYPEMASK_Y_IGNORE | mavutil.mavlink.POSITION_TARGET_TYPEMASK_Z_IGNORE | mavutil.mavlink.POSITION_TARGET_TYPEMASK_AX_IGNORE | mavutil.mavlink.POSITION_TARGET_TYPEMASK_AY_IGNORE | mavutil.mavlink.POSITION_TARGET_TYPEMASK_AZ_IGNORE | mavutil.mavlink.POSITION_TARGET_TYPEMASK_YAW_IGNORE | mavutil.mavlink.POSITION_TARGET_TYPEMASK_YAW_RATE_IGNORE ) # 直接發送速度指令,不需等待位置數據 self.connection.mav.set_position_target_local_ned_send( 0, self.connection.target_system, self.connection.target_component, mavutil.mavlink.MAV_FRAME_LOCAL_NED, type_mask, 0, 0, 0, # 位置 vx, vy, -vz, # 速度 0, 0, 0, # 加速度 0, 0 # 航向 ) print(f"Drone {self.instance_id}: 發送速度指令: vx={vx}, vy={vy}, vz={-vz}") except json.JSONDecodeError as je: print(f"Drone {self.instance_id}: JSON解析錯誤: {je}") self.log_packet({"raw_data": decoded_data}, total_packets) except Exception as e: print(f"Drone {self.instance_id}: 處理錯誤: {e}") # 清理資源 server_socket.close() print(f"Drone {self.instance_id}: 線程結束") def main(): # 解析命令行參數 parser = argparse.ArgumentParser(description='多無人機控制程序') parser.add_argument('--drones', type=str, default='1,2,3', help='要控制的無人機ID,用逗號分隔 (例如: 1,2,3)') args = parser.parse_args() # 所有可用的無人機配置 all_drone_configs = [ (1, 14550, 5003), (2, 14560, 5013), (3, 14570, 5023) ] # 解析要使用的無人機ID drone_ids = [int(id_str) for id_str in args.drones.split(',')] drone_configs = [config for config in all_drone_configs if config[0] in drone_ids] drones = [] threads = [] try: # 創建所有無人機實例 for instance_id, mavlink_port, udp_port in drone_configs: drone = Drone(instance_id, mavlink_port, udp_port) drones.append(drone) # 步驟1: 所有無人機連接 for drone in drones: drone.connect() # 適當延遲確保連接穩定 time.sleep(1) # 步驟2: 所有無人機同時切換到GUIDED模式 for drone in drones: drone.set_guided_mode() time.sleep(1) # 步驟3: 所有無人機同時解鎖 for drone in drones: drone.arm() time.sleep(1) # 步驟4: 所有無人機同時起飛 for drone in drones: drone.takeoff() # 啟動監聽線程 for drone in drones: thread = threading.Thread(target=drone.run) thread.daemon = True threads.append(thread) thread.start() print(f"已啟動 {len(drones)} 台無人機 (ID: {args.drones}),按Ctrl+C結束程序") # 主線程事件循環 while True: select([], [], [], 0.1) # 使用select代替time.sleep except KeyboardInterrupt: print("正在停止所有無人機...") for drone in drones: drone.running = False for thread in threads: thread.join(timeout=2) print("程序已結束") if __name__ == "__main__": main()