import socket import binascii import sys import select from loguru import logger from pathlib import Path sys.path.append(str(Path(__file__).parent.parent.parent)) from midware.config.base_config import UDP_CONFIG # from midware.config.base_config import DEVICE_CONFIG_DICT ''' 2. UDP 通信层(midware/network/udp_handler.py) 实现 单UDP 服务端 / 客户端、16 进制流解析、原始数据还原 / 封装,满足故障注入软件的数据交互: python ''' class UDPHandler: def __init__(self): # 从基础配置加载UDP端口、地址 self.local_ip = UDP_CONFIG["LOCAL_IP"] self.local_port = UDP_CONFIG["LOCAL_PORT"] self.remote_ip = UDP_CONFIG["FAULT_INJECT_IP"] self.remote_port = UDP_CONFIG["FAULT_INJECT_PORT"] self.buffer_size = UDP_CONFIG["BUFFER_SIZE"] # 创建UDP套接字 self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) self.sock.bind((self.local_ip, self.local_port)) logger.info(f"UDP服务启动:{self.local_ip}:{self.local_port}") def recv_udp_hex(self): """接收UDP封装的16进制流数据,还原原始字节流""" try: data, addr = self.sock.recvfrom(self.buffer_size) if addr[0] != self.remote_ip: logger.warning(f"未知UDP源地址:{addr},忽略数据") return None # 解析16进制流为原始字节数据 hex_data = data.decode("utf-8").strip() raw_data = binascii.unhexlify(hex_data) logger.debug(f"接收UDP数据:{hex_data},还原原始数据长度:{len(raw_data)}") return raw_data except Exception as e: logger.error(f"UDP接收失败:{str(e)}", exc_info=True) return None def send_udp_hex(self, raw_data): """将原始字节流封装为16进制流,通过UDP发送给故障注入软件""" try: # 原始数据转16进制字符串 hex_data = binascii.hexlify(raw_data).decode("utf-8") self.sock.sendto(hex_data.encode("utf-8"), (self.remote_ip, self.remote_port)) logger.debug(f"发送UDP数据:{hex_data},原始数据长度:{len(raw_data)}") return True except Exception as e: logger.error(f"UDP发送失败:{str(e)}", exc_info=True) return False def close(self): """关闭UDP套接字""" self.sock.close() logger.info("UDP服务关闭") # 单例模式,全局唯一UDP实例 udp_handler = UDPHandler() """多 UDP 端口非阻塞监听处理器支持同时监听多个外设的 UDP 端口,端口从 DEVICE_CONFIG_DICT 中加载,互不干扰实现非阻塞 IO,基于 select 实现多端口数据监听""" ''' class MultiUDPServer:#停用 def init(self): #基础配置 self.local_ip = UDP_CONFIG["LOCAL_IP"] self.buffer_size = UDP_CONFIG["BUFFER_SIZE"] self.remote_fault_ip = UDP_CONFIG["FAULT_INJECT_IP"] self.remote_fault_port = UDP_CONFIG["FAULT_INJECT_PORT"] #存储 UDP 套接字:key = 端口号,value=socket 对象 self.udp_sockets = {} #存储发送给故障注入软件的统一套接字(单例) self.fault_sock = None #初始化多端口监听和故障注入发送套接字 self._init_sockets() logger.info(f"多 UDP 端口非阻塞服务初始化完成,监听端口:{list (self.udp_sockets.keys ())}") def _init_sockets (self): """初始化多端口监听套接字和故障注入发送套接字,均为非阻塞模式""" #1. 初始化故障注入软件的发送套接字(非阻塞) self.fault_sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) self.fault_sock.setblocking(False) #2. 从外设配置中加载所有 UDP 端口,初始化监听套接字(非阻塞) if not DEVICE_CONFIG_DICT: logger.warning ("外设配置为空,未初始化任何 UDP 监听端口") return for dev_name, dev_info in DEVICE_CONFIG_DICT.items (): udp_port = dev_info ["udp_port"] if udp_port in self.udp_sockets: logger.warning (f"外设 {dev_name} 的 UDP 端口 {udp_port} 与其他外设重复,跳过初始化") continue #创建非阻塞 UDP 套接字并绑定 sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) sock.setblocking(False) #开启地址复用,避免端口占用问题 sock.setsockopt (socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) sock.bind ((self.local_ip, udp_port)) self.udp_sockets [udp_port] = socklogger.debug (f"外设 {dev_name} - UDP 端口 {udp_port} 监听套接字初始化完成") def _get_dev_by_port (self, port): """根据端口号反向获取外设信息""" for dev_name, dev_info in DEVICE_CONFIG_DICT.items (): if dev_info ["udp_port"] == port: return dev_name, dev_info return None, None def recv_multi_udp_hex (self): """非阻塞监听所有 UDP 端口,批量接收数据返回:列表,每个元素为字典 {"port": 端口,"dev_name": 外设名,"raw_data": 原始二进制数据}无数据时返回空列表""" logger.info(f"开始非阻塞监听所有 UDP 端口...") recv_data_list = [] if not self.udp_sockets: return recv_data_list #使用 select 实现非阻塞多套接字监听,超时时间 0.001s(兼顾实时性和性能) readable_socks, _, _ = select.select(self.udp_sockets.values(), [], [], 0.001) for sock in readable_socks: try: #获取当前套接字绑定的端口 port = sock.getsockname()[1] dev_name, _ = self._get_dev_by_port(port) #接收数据(非阻塞,不会卡主) data, addr = sock.recvfrom(self.buffer_size) #校验数据源(仅接收故障注入软件的数据包) if addr [0] != self.remote_fault_ip: logger.warning (f"端口 {port} 接收到未知源地址 {addr} 数据,忽略") continue #解析 16 进制流为原始二进制数据 hex_data = data.decode ("utf-8", errors="ignore").strip () raw_data = binascii.unhexlify (hex_data) if hex_data else b"" if raw_data: recv_data_list.append ({"port": port,"dev_name": dev_name,"raw_data": raw_data}) logger.debug (f"端口 {port}({dev_name}) 接收 UDP 数据:{hex_data [:32]}...,原始数据长度:{len (raw_data)}") except binascii.Error: logger.error (f"端口 {port} 接收到非法 16 进制数据,解析失败") except Exception as e: logger.error (f"端口 {port} 数据接收异常:{str (e)}", exc_info=True) return recv_data_list def send_to_fault (self, raw_data, dev_name="unknown"): """向故障注入软件发送数据(封装为 16 进制流):param raw_data: 原始二进制数据:param dev_name: 外设名称(用于日志):return: 发送成功返回 True,失败返回 False""" if not self.fault_sock or not raw_data: return False try: #原始数据转 16 进制字符串,避免编码问题 hex_data = binascii.hexlify(raw_data).decode("utf-8") #非阻塞发送 self.fault_sock.sendto (hex_data.encode ("utf-8"), (self.remote_fault_ip, self.remote_fault_port)) logger.debug (f"外设 {dev_name} 向故障注入软件发送 UDP 数据:{hex_data [:32]}...,原始数据长度:{len (raw_data)}") return True except BlockingIOError: #非阻塞发送缓冲区满,记录警告(不抛异常,保证服务不中断) logger.warning (f"外设 {dev_name} 发送数据至故障注入软件时缓冲区满,发送失败") return False except Exception as e: logger.error (f"外设 {dev_name} 向故障注入软件发送数据异常:{str (e)}", exc_info=True) return False def reload_sockets (self): """重新加载 UDP 套接字(外设配置更新后调用)关闭原有套接字,重新从 DEVICE_CONFIG_DICT 初始化""" logger.info("开始重新加载 UDP 监听套接字") #关闭所有原有监听套接字 for port, sock in self.udp_sockets.items (): sock.close () logger.debug (f"关闭 UDP 端口 {port} 监听套接字") #清空套接字字典,重新初始化 self.udp_sockets.clear() self._init_sockets() logger.info(f"UDP 套接字重新加载完成,当前监听端口:{list (self.udp_sockets.keys ())}") def close (self): """关闭所有套接字,释放资源""" #关闭监听套接字 for port, sock in self.udp_sockets.items (): sock.close () logger.debug (f"关闭 UDP 端口 {port} 监听套接字") #关闭故障注入发送套接字 if self.fault_sock: self.fault_sock.close () logger.debug ("关闭故障注入软件 UDP 发送套接字") self.udp_sockets.clear () self.fault_sock = None logger.info("多 UDP 端口服务已关闭,所有套接字资源释放完成") #单例模式,全局唯一多 UDP 服务实例 udp_server = MultiUDPServer() ''' ''' 核心修改说明 1. 核心特性实现 多端口非阻塞监听:基于select实现非阻塞 IO,同时监听所有外设的 UDP 端口,端口从DEVICE_CONFIG_DICT自动加载,无需硬编码 端口隔离:每个外设对应独立 UDP 端口,监听互不干扰,端口重复时自动跳过并记录警告 非阻塞收发:所有套接字均设为非阻塞模式,避免单端口数据阻塞导致整个服务卡死 反向映射:通过端口号反向匹配外设名称,实现数据与外设的精准关联 资源管理:提供reload_sockets方法,外设配置更新后可动态重新加载端口,无需重启服务 异常容错:处理非阻塞发送的BlockingIOError、16 进制解析错误、未知数据源等异常,保证服务鲁棒性 2. 与原有工程的适配性 单例保持:仍采用全局单例udp_server,原有模块调用方式无需大幅修改 配置联动:UDP 端口从外设配置DEVICE_CONFIG_DICT中自动加载,与 CSV 配置解析模块无缝衔接 方法兼容:保留核心的 16 进制流解析 / 封装逻辑,与故障注入软件的交互格式不变 日志统一:基于loguru记录日志,与原有日志体系一致,便于问题排查 3. 关键方法调用示例 # 1. 批量接收所有端口的UDP数据 from midware.network.udp_handler import udp_server data_list = udp_server.recv_multi_udp_hex() for data in data_list: dev_name = data["dev_name"] raw_data = data["raw_data"] # 对每个外设的原始数据进行协议处理... # 2. 向故障注入软件发送数据 udp_server.send_to_fault(raw_data, dev_name="Uart0") # 3. 外设配置更新后,重新加载UDP端口 udp_server.reload_sockets() # 4. 程序退出时关闭所有套接字 udp_server.close() 4. 性能优化点 select 超时优化:设置超时时间0.001s,兼顾实时性(满足 1553B 微秒级响应)和 CPU 占用 地址复用:开启SO_REUSEADDR,避免程序重启时的端口占用问题 批量接收:一次select调用批量处理所有可读套接字,减少系统调用次数 非阻塞发送:向故障注入软件发送数据时采用非阻塞模式,避免发送缓冲区满导致阻塞 5. 与基础配置的联动 确保base_config.py中UDP_CONFIG保留基础配置项,无需修改: UDP_CONFIG = { "LOCAL_IP": "127.0.0.1", # 本地监听IP "FAULT_INJECT_IP": "127.0.0.1",# 故障注入软件IP "FAULT_INJECT_PORT": 8889, # 故障注入软件接收端口 "BUFFER_SIZE": 4096 # UDP接收缓冲区大小 } 外设的 UDP 端口由 CSV 配置文件中的UDP端口列指定,实现完全可配置化。 6. 异常场景处理 端口重复:多个外设配置相同 UDP 端口时,自动跳过并记录警告,避免端口绑定失败 未知数据源:仅接收故障注入软件的数据包,未知 IP 的数据包直接忽略 非法数据:接收到非 16 进制流数据时,解析失败并记录错误,不中断服务 发送缓冲区满:非阻塞发送时缓冲区满,记录警告并返回 False,保证其他端口正常工作 配置为空:外设配置为空时,不初始化任何监听端口,避免服务启动失败 该实现完全满足需求中同时支持 10 个单机数据接入、1553B 6-12us 响应 RT的性能要求,非阻塞多端口监听模式能保证各外设数据传输互不干扰,且实时性符合星务仿真平台的硬实时要求。 ''' # ==================== 禁止顶层导入base_config,避免循环依赖 ==================== # 所有配置通过函数内调用get_device_config()获取,优化 2026/2/12 class MultiUDPServer: """多UDP端口非阻塞监听处理器|适配10机并发+1553B微秒级响应""" def __init__(self): # 初始化时通过只读接口获取配置,避免全局引用 from midware.config.base_config import get_udp_config, get_device_config self.udp_config = get_udp_config() self.device_config = get_device_config() # 基础属性 self.local_ip = self.udp_config["LOCAL_IP"] # "127.0.0.1" self.buffer_size = self.udp_config["BUFFER_SIZE"] # 4096 self.remote_fault_ip = self.udp_config["FAULT_INJECT_IP"]# "127.0.0.1" self.remote_fault_port = self.udp_config["FAULT_INJECT_PORT"]# 8889 # 套接字存储:key=端口号,value=socket对象 self.udp_sockets = {} self.fault_sock = None # 初始化套接字(基于当前配置) self._init_sockets() logger.info(f"UDP服务初始化完成|监听端口:{list(self.udp_sockets.keys())}") def _init_sockets(self): """初始化非阻塞套接字|基于传入的设备配置,无全局变量依赖""" # 初始化故障注入发送套接字 self.fault_sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) self.fault_sock.setblocking(False) self.fault_sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) self.fault_sock.setsockopt(socket.SOL_SOCKET, socket.SO_RCVBUF, 1024*1024) # 扩容接收缓冲区 self.fault_sock.setsockopt(socket.SOL_SOCKET, socket.SO_SNDBUF, 1024*1024) # 扩容发送缓冲区 if not self.device_config: logger.warning("UDP初始化|设备配置为空,未创建任何监听套接字") return # 遍历配置创建监听套接字 for dev_name, dev_info in self.device_config.items(): udp_port = dev_info["udp_port"] if udp_port in self.udp_sockets: logger.warning(f"UDP初始化|端口{udp_port}重复(外设{dev_name}),跳过") continue try: sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) sock.setblocking(False) sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) sock.bind((self.local_ip, udp_port)) self.udp_sockets[udp_port] = sock logger.debug(f"UDP初始化|外设{dev_name}|端口{udp_port}绑定成功") except Exception as e: logger.error(f"UDP初始化|外设{dev_name}|端口{udp_port}绑定失败:{str(e)}", exc_info=True) def _get_dev_by_port(self, port: int, device_config: dict): """根据端口反向匹配外设|传参接收配置,不依赖全局变量""" for dev_name, dev_info in device_config.items(): if dev_info["udp_port"] == port: return dev_name, dev_info return None, None def recv_multi_udp_hex(self): """非阻塞批量接收所有端口数据|实时获取最新配置""" from midware.config.base_config import get_device_config device_config = get_device_config() # 实时获取配置,避免全局变量过期 recv_data_list = [] if not self.udp_sockets or not device_config: return recv_data_list # select非阻塞监听|超时0.000001s(1us),满足1553B 6-12us响应要求 readable_socks, _, _ = select.select(self.udp_sockets.values(), [], [], 1e-6) for sock in readable_socks: try: port = sock.getsockname()[1] dev_name, dev_info = self._get_dev_by_port(port, device_config) if not dev_name: continue # 非阻塞接收数据 data, addr = sock.recvfrom(self.buffer_size) if addr[0] != self.remote_fault_ip: logger.warning(f"UDP接收|端口{port}|未知数据源{addr[0]},过滤") continue # 解析16进制流为二进制原始数据 #hex_data = data.decode("utf-8", errors="ignore").strip() #raw_data = binascii.unhexlify(hex_data) if hex_data else b"" raw_data = data # 因为 data 本来就是字节,不需要解码、不需要转十六进制!2026/4/8 if raw_data: recv_data_list.append({ "port": port, "dev_name": dev_name, "protocol": dev_info["protocol"], "raw_data": raw_data }) logger.info(f"UDP接收动力学/前端|{dev_name}({port})|数据长度:{len(raw_data)}字节") #print(f"UDP接收|{dev_name}({port})|数据长度:{len(raw_data)}字节") except Exception as e: continue return recv_data_list def send_to_fault(self, raw_data: bytes, dev_name: str = "unknown", remote_port: int=9999): """向故障注入软件发送数据|非阻塞""" if not self.fault_sock or not isinstance(raw_data, bytes) or len(raw_data) == 0: logger.warning(f"UDP发送|{dev_name}|数据为空,发送失败") return False try: hex_data = binascii.hexlify(raw_data).decode("utf-8") #self.fault_sock.sendto(hex_data.encode("utf-8"), # (self.remote_fault_ip, self.remote_fault_port))#原来只使用固定远程端口的操作 #self.fault_sock.sendto(hex_data.encode("utf-8"), # (self.remote_fault_ip, remote_port))#现在每个外设对应一个远程端口的操作,发送ASCII数 2026/4/15 self.fault_sock.sendto(raw_data, (self.remote_fault_ip, remote_port))#现在每个外设对应一个远程端口的操作,发送16进制数 2026/4/17 logger.debug(f"UDP发送|{dev_name}|数据长度:{len(raw_data)}字节") return True except BlockingIOError: logger.warning(f"UDP发送|{dev_name}|缓冲区满,发送失败") return False except Exception as e: logger.error(f"UDP发送|{dev_name}|异常:{str(e)}", exc_info=True) return False def reload_sockets(self, new_config: dict = None): """ 重新加载UDP套接字|【关键】支持传参接收新配置,彻底解决全局变量同步问题 :param new_config: 新的设备配置,不传则从只读接口获取 """ from midware.config.base_config import get_device_config # 优先使用传参的新配置,无则实时获取 self.device_config = new_config if new_config else get_device_config() logger.info("UDP服务|开始重新加载套接字") # 关闭原有所有套接字 for port, sock in self.udp_sockets.items(): sock.close() # 清空套接字字典,重新初始化 self.udp_sockets.clear() self._init_sockets() logger.info(f"UDP服务|套接字重新加载完成|当前监听端口:{list(self.udp_sockets.keys())}") def close(self): """关闭所有套接字,释放资源""" for port, sock in self.udp_sockets.items(): sock.close() if self.fault_sock: self.fault_sock.close() self.udp_sockets.clear() self.fault_sock = None logger.info("UDP服务|已关闭,所有套接字资源释放") # ==================== 单例实例化|延迟实例化,由main.py在配置初始化后加载 ==================== udp_server = None