commit 8ba6324244e94853c5c36cbbc140e95f586bf775 Author: tanmingyan <2591656752@qq.com> Date: Tue Jun 16 15:40:19 2026 +0800 first commit diff --git a/README.md b/README.md new file mode 100644 index 0000000..e69de29 diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/config_files/chip_config.xlsx b/config_files/chip_config.xlsx new file mode 100644 index 0000000..dbd573c Binary files /dev/null and b/config_files/chip_config.xlsx differ diff --git a/config_files/device_config - 20260210.csv b/config_files/device_config - 20260210.csv new file mode 100644 index 0000000..bbfa831 --- /dev/null +++ b/config_files/device_config - 20260210.csv @@ -0,0 +1,4 @@ +名称,协议类型,内存基地址,内存偏移地址,UDP端口,说明,发送缓存区大小,接收缓存区大小 +Uart0,UART,0x20800000,0x0000,8880,光纤陀螺A,512,512 +CAN1,CAN,0x20810000,0x0100,8881,姿态传感器,1024,1024 +1553B0,1553B,0x20820000,0x0200,8882,测控模块,2048,2048 diff --git a/config_files/device_config - 20260420.csv b/config_files/device_config - 20260420.csv new file mode 100644 index 0000000..286a470 --- /dev/null +++ b/config_files/device_config - 20260420.csv @@ -0,0 +1,12 @@ +名称,编号,协议类型,内存基地址,内存偏移地址,port,remote_port,说明,发送缓存区大小,接收缓存区大小 +光纤陀螺A,1,UART,0x20800000,0x0000,4000,11000,Uart0,512,512 +反作用轮A,2,CAN,0x20810000,0x0100,4001,11011,CAN0,1024,1024 +1553B0,3,1553B,0x20820000,0x0200,4002,11022,测控模块,2048,2048 +电压采集,4,AD,0x20830000,0x0300,4003,11033,AD0,512,512 +开关量输出,1,OC,0x20840000,0x0400,4004,11044,OC0,512,512 +光纤陀螺B,1,UART,0x20850000,0X0001,4005,11055,UART1,512,512 +星敏A,1,CAN,0x20860000,0x0101,4006,11066,CAN1,512,512 +数传A,1,1553B,0x20870000,0x0201,4007,11077,1553B1,512,512 +adc1,1,AD,0x21800000,0x0488,4018,11088,正电压采集热敏量采集,512,512 +继电器输出,2,OC,0x20890000,0x0401,4009,11099,OC1,512,512 +uart24,24,UART,0x20800c80,0x0000,3021,3071,地测串口,1024,2048 diff --git a/config_files/device_config-20260413.csv b/config_files/device_config-20260413.csv new file mode 100644 index 0000000..fafef64 --- /dev/null +++ b/config_files/device_config-20260413.csv @@ -0,0 +1,12 @@ +名称,协议类型,内存基地址,内存偏移地址,port,说明,发送缓存区大小,接收缓存区大小 +光纤陀螺A,UART,0x20800000,0x0000,4000,Uart0,512,512 +反作用轮A,CAN,0x20810000,0x0100,4001,CAN0,1024,1024 +1553B0,1553B,0x20820000,0x0200,4002,测控模块,2048,2048 +电压采集,AD,0x20830000,0x0300,4003,AD0,512,512 +开关量输出,OC,0x20840000,0x0400,4004,OC0,512,512 +光纤陀螺B,UART,0x20850000,0X0001,4005,UART1,512,512 +星敏A,CAN,0x20860000,0x0101,4006,CAN1,512,512 +数传A,1553B,0x20870000,0x0201,4007,1553B1,512,512 +adc1,AD,0x21800000,0x0488,4018,正电压采集热敏量采集,512,512 +继电器输出,OC,0x20890000,0x0401,4009,OC1,512,512 +uart24,UART,0x20800c80,0x0000,4030,地测串口,1024,2048 \ No newline at end of file diff --git a/config_files/device_config.csv b/config_files/device_config.csv new file mode 100644 index 0000000..61bee7e --- /dev/null +++ b/config_files/device_config.csv @@ -0,0 +1,22 @@ +名称,编号,协议类型,内存基地址,内存偏移地址,port,remote_port,tcp_local_port,tcp_bus_port,说明,发送缓存区大小,接收缓存区大小 +光纤陀螺A,1,UART,0x20800000,0x0000,4000,11000,,,Uart0,512,512 +反作用轮A,2,CAN,0x20810000,0x0100,4001,11011,,,CAN0,1024,1024 +1553B0,3,1553B,0x20820000,0x0200,4002,11022,,,测控模块,2048,2048 +电压采集,4,AD,0x20830000,0x0300,4003,11033,,,AD0,512,512 +开关量输出,1,OC,0x20840000,0x0400,4004,11044,,,OC0,512,512 +光纤陀螺B,1,UART,0x20850000,0X0001,4005,11055,,,UART1,512,512 +星敏A,3,CAN,0x20860000,0x0101,4006,11066,,,CAN1,512,512 +数传A,1,1553B,0x20870000,0x0201,4007,11077,,,1553B1,512,512 +adc1,1,AD,0x21800000,0x0488,4018,11088,,,正电压采集热敏量采集,512,512 +继电器输出,2,OC,0x20890000,0x0401,4009,11099,,,OC1,512,512 +uart24,24,UART,0x20800c80,0x0000,13045,13095,13024,12024,地测串口,1024,2048 +uart26,26,UART,0x20C00000,0x0000,3145,3195,13026,12026,高速通信,2048,512 +uart0,0,UART,0x20C00001,0x0001,3021,3071,,,陀螺A,512,512 +uart1,1,UART,0x20C00002,0x0002,3022,3072,,,陀螺B,512,512 +uart2,2,UART,0x20C00003,0x0003,3023,3073,,,陀螺C,512,512 +uart3,3,UART,0x20C00004,0x0004,3024,3074,,,预留,512,512 +uart18,18,UART,0x20C00005,0x0005,3039,3089,,,星敏C,512,512 +uart19,18,UART,0x20C00006,0x0006,3040,3090,,,星敏B,512,512 +uart25,25,UART,0x20C00007,0x0007,3046,3096,,,GNSS,512,512 +can_a,1,CAN,0x20860000,0x0101,20031,10031,,,canA,512,512 +can_b,2,CAN,0x20860000,0x0101,20032,10032,,,canB,512,512 diff --git a/config_files/device_config_29260515.csv b/config_files/device_config_29260515.csv new file mode 100644 index 0000000..567c41b --- /dev/null +++ b/config_files/device_config_29260515.csv @@ -0,0 +1,22 @@ +,,Э,ڴַ,ڴƫƵַ,port,remote_port,tcp_local_port,tcp_bus_port,˵,ͻС,ջС +A,1,UART,0x20800000,0x0000,4000,11000,,,Uart0,512,512 +A,2,CAN,0x20810000,0x0100,4001,11011,,,CAN0,1024,1024 +1553B0,3,1553B,0x20820000,0x0200,4002,11022,,,ģ,2048,2048 +ѹɼ,4,AD,0x20830000,0x0300,4003,11033,,,AD0,512,512 +,1,OC,0x20840000,0x0400,4004,11044,,,OC0,512,512 +B,1,UART,0x20850000,0X0001,4005,11055,,,UART1,512,512 +A,1,CAN,0x20860000,0x0101,4006,11066,,,CAN1,512,512 +A,1,1553B,0x20870000,0x0201,4007,11077,,,1553B1,512,512 +adc1,1,AD,0x21800000,0x0488,4018,11088,,,ѹɼɼ,512,512 +̵,2,OC,0x20890000,0x0401,4009,11099,,,OC1,512,512 +uart24,24,UART,0x20800c80,0x0000,13045,13095,13024,12024,ز⴮,1024,2048 +uart26,26,UART,0x20C00000,0x0000,3145,3195,13026,12026,ͨ,2048,512 +uart0,0,UART,0x20C00001,0x0001,3021,3071,,,A,512,512 +uart1,1,UART,0x20C00002,0x0002,3022,3072,,,B,512,512 +uart2,2,UART,0x20C00003,0x0003,3023,3073,,,C,512,512 +uart3,3,UART,0x20C00004,0x0004,3024,3074,,,Ԥ,512,512 +uart18,18,UART,0x20C00005,0x0005,3039,3089,,,C,512,512 +uart19,18,UART,0x20C00006,0x0006,3040,3090,,,B,512,512 +uart25,25,UART,0x20C00007,0x0007,3046,3096,,,GNSS,512,512 +canA,1,CAN,0x20860000,0x0101,20031,10031,,,canA,512,512 +canB,2,CAN,0x20860000,0x0101,20032,10032,,,canB,512,512 diff --git a/config_files/device_config_old.xlsx b/config_files/device_config_old.xlsx new file mode 100644 index 0000000..0f65319 Binary files /dev/null and b/config_files/device_config_old.xlsx differ diff --git a/dependencys/colorama-0.4.6-py2.py3-none-any.whl b/dependencys/colorama-0.4.6-py2.py3-none-any.whl new file mode 100644 index 0000000..f666ce9 Binary files /dev/null and b/dependencys/colorama-0.4.6-py2.py3-none-any.whl differ diff --git a/dependencys/et_xmlfile-2.0.0-py3-none-any.whl b/dependencys/et_xmlfile-2.0.0-py3-none-any.whl new file mode 100644 index 0000000..52a7eec Binary files /dev/null and b/dependencys/et_xmlfile-2.0.0-py3-none-any.whl differ diff --git a/dependencys/loguru-0.7.3-py3-none-any.whl b/dependencys/loguru-0.7.3-py3-none-any.whl new file mode 100644 index 0000000..58bf021 Binary files /dev/null and b/dependencys/loguru-0.7.3-py3-none-any.whl differ diff --git a/dependencys/numpy-2.4.4-cp311-cp311-win_amd64.whl b/dependencys/numpy-2.4.4-cp311-cp311-win_amd64.whl new file mode 100644 index 0000000..05539ab Binary files /dev/null and b/dependencys/numpy-2.4.4-cp311-cp311-win_amd64.whl differ diff --git a/dependencys/openpyxl-3.1.5-py2.py3-none-any.whl b/dependencys/openpyxl-3.1.5-py2.py3-none-any.whl new file mode 100644 index 0000000..e1dccd4 Binary files /dev/null and b/dependencys/openpyxl-3.1.5-py2.py3-none-any.whl differ diff --git a/dependencys/pandas-3.0.2-cp311-cp311-win_amd64.whl b/dependencys/pandas-3.0.2-cp311-cp311-win_amd64.whl new file mode 100644 index 0000000..4086a98 Binary files /dev/null and b/dependencys/pandas-3.0.2-cp311-cp311-win_amd64.whl differ diff --git a/dependencys/psutil-7.2.2-cp37-abi3-win_amd64.whl b/dependencys/psutil-7.2.2-cp37-abi3-win_amd64.whl new file mode 100644 index 0000000..1cf36c3 Binary files /dev/null and b/dependencys/psutil-7.2.2-cp37-abi3-win_amd64.whl differ diff --git a/dependencys/python_dateutil-2.9.0.post0-py2.py3-none-any.whl b/dependencys/python_dateutil-2.9.0.post0-py2.py3-none-any.whl new file mode 100644 index 0000000..b9a14e1 Binary files /dev/null and b/dependencys/python_dateutil-2.9.0.post0-py2.py3-none-any.whl differ diff --git a/dependencys/six-1.17.0-py2.py3-none-any.whl b/dependencys/six-1.17.0-py2.py3-none-any.whl new file mode 100644 index 0000000..c506fd0 Binary files /dev/null and b/dependencys/six-1.17.0-py2.py3-none-any.whl differ diff --git a/dependencys/tzdata-2026.1-py2.py3-none-any.whl b/dependencys/tzdata-2026.1-py2.py3-none-any.whl new file mode 100644 index 0000000..0d334d6 Binary files /dev/null and b/dependencys/tzdata-2026.1-py2.py3-none-any.whl differ diff --git a/dependencys/win32_setctime-1.2.0-py3-none-any.whl b/dependencys/win32_setctime-1.2.0-py3-none-any.whl new file mode 100644 index 0000000..7e21ea6 Binary files /dev/null and b/dependencys/win32_setctime-1.2.0-py3-none-any.whl differ diff --git a/fault_inject_simulator.log b/fault_inject_simulator.log new file mode 100644 index 0000000..a37b6c1 --- /dev/null +++ b/fault_inject_simulator.log @@ -0,0 +1,236 @@ +2026-02-26 22:55:07 | INFO | 故障注入平台UDP模拟器初始化完成 +2026-02-26 22:55:07 | INFO | 开始单端口循环发送 | 外设:Uart0 | 端口:8880 | 发送次数:500 +2026-02-26 22:55:08 | INFO | 单端口发送完成 | 外设:Uart0 | 端口:8880 | 发送统计:{'success': 500, 'failed': 0} +2026-02-26 22:55:08 | INFO | ================================================== 发送统计汇总 ================================================== +2026-02-26 22:55:08 | INFO | 全局统计 | 总发送:500 | 成功:500 | 失败:0 +2026-02-26 22:55:08 | INFO | 端口统计: +2026-02-26 22:55:08 | INFO | 外设:Uart0 | 端口: 8880 | 发送: 500 | 成功: 500 | 失败: 0 | 成功率:100.00% +2026-02-26 22:55:08 | INFO | 外设:CAN1 | 端口: 8881 | 发送: 0 | 成功: 0 | 失败: 0 | 成功率:0.00% +2026-02-26 22:55:08 | INFO | 外设:1553B0 | 端口: 8882 | 发送: 0 | 成功: 0 | 失败: 0 | 成功率:0.00% +2026-02-26 22:55:08 | INFO | 外设:AD0 | 端口: 8883 | 发送: 0 | 成功: 0 | 失败: 0 | 成功率:0.00% +2026-02-26 22:55:08 | INFO | 外设:OC0 | 端口: 8884 | 发送: 0 | 成功: 0 | 失败: 0 | 成功率:0.00% +2026-02-26 22:55:08 | INFO | 外设:Uart1 | 端口: 8885 | 发送: 0 | 成功: 0 | 失败: 0 | 成功率:0.00% +2026-02-26 22:55:08 | INFO | 外设:CAN2 | 端口: 8886 | 发送: 0 | 成功: 0 | 失败: 0 | 成功率:0.00% +2026-02-26 22:55:08 | INFO | 外设:1553B1 | 端口: 8887 | 发送: 0 | 成功: 0 | 失败: 0 | 成功率:0.00% +2026-02-26 22:55:08 | INFO | 外设:AD1 | 端口: 8888 | 发送: 0 | 成功: 0 | 失败: 0 | 成功率:0.00% +2026-02-26 22:55:08 | INFO | 外设:OC1 | 端口: 8889 | 发送: 0 | 成功: 0 | 失败: 0 | 成功率:0.00% +2026-02-26 22:55:08 | INFO | ==================================================================================================== +2026-02-26 22:55:08 | INFO | 故障注入平台UDP模拟器已关闭,资源释放完成 +2026-02-26 22:56:44 | INFO | 故障注入平台UDP模拟器初始化完成 +2026-02-26 22:56:44 | INFO | 开始单端口循环发送 | 外设:Uart0 | 端口:4000 | 发送次数:500 +2026-02-26 22:56:45 | INFO | 单端口发送完成 | 外设:Uart0 | 端口:4000 | 发送统计:{'success': 500, 'failed': 0} +2026-02-26 22:56:45 | INFO | ================================================== 发送统计汇总 ================================================== +2026-02-26 22:56:45 | INFO | 全局统计 | 总发送:500 | 成功:500 | 失败:0 +2026-02-26 22:56:45 | INFO | 端口统计: +2026-02-26 22:56:45 | INFO | 外设:Uart0 | 端口: 4000 | 发送: 500 | 成功: 500 | 失败: 0 | 成功率:100.00% +2026-02-26 22:56:45 | INFO | 外设:CAN1 | 端口: 8881 | 发送: 0 | 成功: 0 | 失败: 0 | 成功率:0.00% +2026-02-26 22:56:45 | INFO | 外设:1553B0 | 端口: 8882 | 发送: 0 | 成功: 0 | 失败: 0 | 成功率:0.00% +2026-02-26 22:56:45 | INFO | 外设:AD0 | 端口: 8883 | 发送: 0 | 成功: 0 | 失败: 0 | 成功率:0.00% +2026-02-26 22:56:45 | INFO | 外设:OC0 | 端口: 8884 | 发送: 0 | 成功: 0 | 失败: 0 | 成功率:0.00% +2026-02-26 22:56:45 | INFO | 外设:Uart1 | 端口: 8885 | 发送: 0 | 成功: 0 | 失败: 0 | 成功率:0.00% +2026-02-26 22:56:45 | INFO | 外设:CAN2 | 端口: 8886 | 发送: 0 | 成功: 0 | 失败: 0 | 成功率:0.00% +2026-02-26 22:56:45 | INFO | 外设:1553B1 | 端口: 8887 | 发送: 0 | 成功: 0 | 失败: 0 | 成功率:0.00% +2026-02-26 22:56:45 | INFO | 外设:AD1 | 端口: 8888 | 发送: 0 | 成功: 0 | 失败: 0 | 成功率:0.00% +2026-02-26 22:56:45 | INFO | 外设:OC1 | 端口: 8889 | 发送: 0 | 成功: 0 | 失败: 0 | 成功率:0.00% +2026-02-26 22:56:45 | INFO | ==================================================================================================== +2026-02-26 22:56:45 | INFO | 故障注入平台UDP模拟器已关闭,资源释放完成 +2026-02-26 23:01:41 | INFO | 故障注入平台UDP模拟器初始化完成 +2026-02-26 23:01:41 | INFO | 开始单端口循环发送 | 外设:Uart0 | 端口:4000 | 发送次数:500 +2026-02-26 23:02:31 | INFO | 单端口发送完成 | 外设:Uart0 | 端口:4000 | 发送统计:{'success': 500, 'failed': 0} +2026-02-26 23:02:31 | INFO | ================================================== 发送统计汇总 ================================================== +2026-02-26 23:02:31 | INFO | 全局统计 | 总发送:500 | 成功:500 | 失败:0 +2026-02-26 23:02:31 | INFO | 端口统计: +2026-02-26 23:02:31 | INFO | 外设:Uart0 | 端口: 4000 | 发送: 500 | 成功: 500 | 失败: 0 | 成功率:100.00% +2026-02-26 23:02:31 | INFO | ==================================================================================================== +2026-02-26 23:02:31 | INFO | 故障注入平台UDP模拟器已关闭,资源释放完成 +2026-02-26 23:05:01 | INFO | 故障注入平台UDP模拟器初始化完成 +2026-02-26 23:05:01 | INFO | 开始单端口循环发送 | 外设:Uart0 | 端口:4000 | 发送次数:500 +2026-02-26 23:05:52 | INFO | 单端口发送完成 | 外设:Uart0 | 端口:4000 | 发送统计:{'success': 500, 'failed': 0} +2026-02-26 23:05:52 | INFO | ================================================== 发送统计汇总 ================================================== +2026-02-26 23:05:52 | INFO | 全局统计 | 总发送:500 | 成功:500 | 失败:0 +2026-02-26 23:05:52 | INFO | 端口统计: +2026-02-26 23:05:52 | INFO | 外设:Uart0 | 端口: 4000 | 发送: 500 | 成功: 500 | 失败: 0 | 成功率:100.00% +2026-02-26 23:05:52 | INFO | ==================================================================================================== +2026-02-26 23:05:52 | INFO | 故障注入平台UDP模拟器已关闭,资源释放完成 +2026-02-26 23:13:20 | INFO | 故障注入平台UDP模拟器初始化完成 +2026-02-26 23:13:20 | INFO | 开始单端口循环发送 | 外设:Uart0 | 端口:4000 | 发送次数:500 +2026-02-26 23:14:10 | INFO | 单端口发送完成 | 外设:Uart0 | 端口:4000 | 发送统计:{'success': 500, 'failed': 0} +2026-02-26 23:14:10 | INFO | ================================================== 发送统计汇总 ================================================== +2026-02-26 23:14:10 | INFO | 全局统计 | 总发送:500 | 成功:500 | 失败:0 +2026-02-26 23:14:10 | INFO | 端口统计: +2026-02-26 23:14:10 | INFO | 外设:Uart0 | 端口: 4000 | 发送: 500 | 成功: 500 | 失败: 0 | 成功率:100.00% +2026-02-26 23:14:10 | INFO | ==================================================================================================== +2026-02-26 23:14:10 | INFO | 故障注入平台UDP模拟器已关闭,资源释放完成 +2026-02-26 23:27:54 | INFO | 故障注入平台UDP模拟器初始化完成 +2026-02-26 23:27:54 | INFO | 开始单端口循环发送 | 外设:Uart0 | 端口:4000 | 发送次数:500 +2026-02-26 23:28:44 | INFO | 单端口发送完成 | 外设:Uart0 | 端口:4000 | 发送统计:{'success': 500, 'failed': 0} +2026-02-26 23:28:44 | INFO | ================================================== 发送统计汇总 ================================================== +2026-02-26 23:28:44 | INFO | 全局统计 | 总发送:500 | 成功:500 | 失败:0 +2026-02-26 23:28:44 | INFO | 端口统计: +2026-02-26 23:28:44 | INFO | 外设:Uart0 | 端口: 4000 | 发送: 500 | 成功: 500 | 失败: 0 | 成功率:100.00% +2026-02-26 23:28:44 | INFO | ==================================================================================================== +2026-02-26 23:28:44 | INFO | 故障注入平台UDP模拟器已关闭,资源释放完成 +2026-02-27 00:17:43 | INFO | 故障注入平台UDP模拟器初始化完成 +2026-02-27 00:17:43 | INFO | 开始单端口循环发送 | 外设:Uart0 | 端口:4000 | 发送次数:500 +2026-02-27 00:18:33 | INFO | 单端口发送完成 | 外设:Uart0 | 端口:4000 | 发送统计:{'success': 500, 'failed': 0} +2026-02-27 00:18:33 | INFO | ================================================== 发送统计汇总 ================================================== +2026-02-27 00:18:33 | INFO | 全局统计 | 总发送:500 | 成功:500 | 失败:0 +2026-02-27 00:18:33 | INFO | 端口统计: +2026-02-27 00:18:33 | INFO | 外设:Uart0 | 端口: 4000 | 发送: 500 | 成功: 500 | 失败: 0 | 成功率:100.00% +2026-02-27 00:18:33 | INFO | ==================================================================================================== +2026-02-27 00:18:33 | INFO | 故障注入平台UDP模拟器已关闭,资源释放完成 +2026-02-27 00:28:00 | INFO | 故障注入平台UDP模拟器初始化完成 +2026-02-27 00:28:00 | INFO | 开始单端口循环发送 | 外设:Uart0 | 端口:4000 | 发送次数:500 +2026-02-27 00:28:50 | INFO | 单端口发送完成 | 外设:Uart0 | 端口:4000 | 发送统计:{'success': 500, 'failed': 0} +2026-02-27 00:28:50 | INFO | ================================================== 发送统计汇总 ================================================== +2026-02-27 00:28:50 | INFO | 全局统计 | 总发送:500 | 成功:500 | 失败:0 +2026-02-27 00:28:50 | INFO | 端口统计: +2026-02-27 00:28:50 | INFO | 外设:Uart0 | 端口: 4000 | 发送: 500 | 成功: 500 | 失败: 0 | 成功率:100.00% +2026-02-27 00:28:50 | INFO | 外设:CAN0 | 端口: 4001 | 发送: 0 | 成功: 0 | 失败: 0 | 成功率:0.00% +2026-02-27 00:28:50 | INFO | ==================================================================================================== +2026-02-27 00:28:50 | INFO | 故障注入平台UDP模拟器已关闭,资源释放完成 +2026-02-27 00:29:57 | INFO | 故障注入平台UDP模拟器初始化完成 +2026-02-27 00:29:57 | INFO | 开始多端口并发发送 | 外设列表:['Uart0', 'CAN1', '1553B0', 'AD0', 'OC0'] | 每端口发送次数:20 +2026-02-27 00:29:57 | WARNING | 外设CAN1不存在,跳过 +2026-02-27 00:29:57 | WARNING | 外设1553B0不存在,跳过 +2026-02-27 00:29:57 | WARNING | 外设AD0不存在,跳过 +2026-02-27 00:29:57 | WARNING | 外设OC0不存在,跳过 +2026-02-27 00:29:59 | INFO | 多端口并发发送完成 | 全局统计:总发送20 | 成功20 | 失败0 +2026-02-27 00:29:59 | INFO | ================================================== 发送统计汇总 ================================================== +2026-02-27 00:29:59 | INFO | 全局统计 | 总发送:20 | 成功:20 | 失败:0 +2026-02-27 00:29:59 | INFO | 端口统计: +2026-02-27 00:29:59 | INFO | 外设:Uart0 | 端口: 4000 | 发送: 20 | 成功: 20 | 失败: 0 | 成功率:100.00% +2026-02-27 00:29:59 | INFO | 外设:CAN0 | 端口: 4001 | 发送: 0 | 成功: 0 | 失败: 0 | 成功率:0.00% +2026-02-27 00:29:59 | INFO | ==================================================================================================== +2026-02-27 00:29:59 | INFO | 故障注入平台UDP模拟器已关闭,资源释放完成 +2026-02-27 00:30:16 | INFO | 故障注入平台UDP模拟器初始化完成 +2026-02-27 00:30:16 | INFO | 开始多端口并发发送 | 外设列表:['Uart0', 'CAN1', '1553B0', 'AD0', 'OC0'] | 每端口发送次数:20 +2026-02-27 00:30:16 | WARNING | 外设CAN1不存在,跳过 +2026-02-27 00:30:16 | WARNING | 外设1553B0不存在,跳过 +2026-02-27 00:30:16 | WARNING | 外设AD0不存在,跳过 +2026-02-27 00:30:16 | WARNING | 外设OC0不存在,跳过 +2026-02-27 00:30:18 | INFO | 多端口并发发送完成 | 全局统计:总发送20 | 成功20 | 失败0 +2026-02-27 00:30:18 | INFO | ================================================== 发送统计汇总 ================================================== +2026-02-27 00:30:18 | INFO | 全局统计 | 总发送:20 | 成功:20 | 失败:0 +2026-02-27 00:30:18 | INFO | 端口统计: +2026-02-27 00:30:18 | INFO | 外设:Uart0 | 端口: 4000 | 发送: 20 | 成功: 20 | 失败: 0 | 成功率:100.00% +2026-02-27 00:30:18 | INFO | 外设:CAN0 | 端口: 4001 | 发送: 0 | 成功: 0 | 失败: 0 | 成功率:0.00% +2026-02-27 00:30:18 | INFO | ==================================================================================================== +2026-02-27 00:30:18 | INFO | 故障注入平台UDP模拟器已关闭,资源释放完成 +2026-02-27 00:32:04 | INFO | 故障注入平台UDP模拟器初始化完成 +2026-02-27 00:32:04 | INFO | 开始多端口并发发送 | 外设列表:['Uart0', 'CAN0', '1553B0', 'AD0', 'OC0'] | 每端口发送次数:20 +2026-02-27 00:32:04 | WARNING | 外设1553B0不存在,跳过 +2026-02-27 00:32:04 | WARNING | 外设AD0不存在,跳过 +2026-02-27 00:32:04 | WARNING | 外设OC0不存在,跳过 +2026-02-27 00:32:06 | INFO | 多端口并发发送完成 | 全局统计:总发送40 | 成功40 | 失败0 +2026-02-27 00:32:06 | INFO | ================================================== 发送统计汇总 ================================================== +2026-02-27 00:32:06 | INFO | 全局统计 | 总发送:40 | 成功:40 | 失败:0 +2026-02-27 00:32:06 | INFO | 端口统计: +2026-02-27 00:32:06 | INFO | 外设:Uart0 | 端口: 4000 | 发送: 20 | 成功: 20 | 失败: 0 | 成功率:100.00% +2026-02-27 00:32:06 | INFO | 外设:CAN0 | 端口: 4001 | 发送: 20 | 成功: 20 | 失败: 0 | 成功率:100.00% +2026-02-27 00:32:06 | INFO | ==================================================================================================== +2026-02-27 00:32:06 | INFO | 故障注入平台UDP模拟器已关闭,资源释放完成 +2026-02-27 00:43:47 | INFO | 故障注入平台UDP模拟器初始化完成 +2026-02-27 00:43:47 | INFO | 开始多端口并发发送 | 外设列表:['Uart0', 'CAN0', '1553B0', 'AD0', 'OC0'] | 每端口发送次数:20 +2026-02-27 00:43:47 | WARNING | 外设1553B0不存在,跳过 +2026-02-27 00:43:47 | WARNING | 外设AD0不存在,跳过 +2026-02-27 00:43:47 | WARNING | 外设OC0不存在,跳过 +2026-02-27 00:43:49 | INFO | 多端口并发发送完成 | 全局统计:总发送40 | 成功40 | 失败0 +2026-02-27 00:43:49 | INFO | ================================================== 发送统计汇总 ================================================== +2026-02-27 00:43:49 | INFO | 全局统计 | 总发送:40 | 成功:40 | 失败:0 +2026-02-27 00:43:49 | INFO | 端口统计: +2026-02-27 00:43:49 | INFO | 外设:Uart0 | 端口: 4000 | 发送: 20 | 成功: 20 | 失败: 0 | 成功率:100.00% +2026-02-27 00:43:49 | INFO | 外设:CAN0 | 端口: 4001 | 发送: 20 | 成功: 20 | 失败: 0 | 成功率:100.00% +2026-02-27 00:43:49 | INFO | ==================================================================================================== +2026-02-27 00:43:49 | INFO | 故障注入平台UDP模拟器已关闭,资源释放完成 +2026-02-27 00:47:32 | INFO | 故障注入平台UDP模拟器初始化完成 +2026-02-27 00:47:32 | INFO | 开始多端口并发发送 | 外设列表:['Uart0', 'CAN0', '1553B0', 'AD0', 'OC0'] | 每端口发送次数:20 +2026-02-27 00:47:32 | WARNING | 外设1553B0不存在,跳过 +2026-02-27 00:47:32 | WARNING | 外设AD0不存在,跳过 +2026-02-27 00:47:32 | WARNING | 外设OC0不存在,跳过 +2026-02-27 00:47:34 | INFO | 多端口并发发送完成 | 全局统计:总发送40 | 成功40 | 失败0 +2026-02-27 00:47:34 | INFO | ================================================== 发送统计汇总 ================================================== +2026-02-27 00:47:34 | INFO | 全局统计 | 总发送:40 | 成功:40 | 失败:0 +2026-02-27 00:47:34 | INFO | 端口统计: +2026-02-27 00:47:34 | INFO | 外设:Uart0 | 端口: 4000 | 发送: 20 | 成功: 20 | 失败: 0 | 成功率:100.00% +2026-02-27 00:47:34 | INFO | 外设:CAN0 | 端口: 4001 | 发送: 20 | 成功: 20 | 失败: 0 | 成功率:100.00% +2026-02-27 00:47:34 | INFO | ==================================================================================================== +2026-02-27 00:47:34 | INFO | 故障注入平台UDP模拟器已关闭,资源释放完成 +2026-02-27 00:49:20 | INFO | 故障注入平台UDP模拟器初始化完成 +2026-02-27 00:49:20 | INFO | 开始多端口并发发送 | 外设列表:['Uart0', 'CAN0', '1553B0', 'AD0', 'OC0'] | 每端口发送次数:20 +2026-02-27 00:49:20 | WARNING | 外设1553B0不存在,跳过 +2026-02-27 00:49:20 | WARNING | 外设AD0不存在,跳过 +2026-02-27 00:49:20 | WARNING | 外设OC0不存在,跳过 +2026-02-27 00:49:22 | INFO | 多端口并发发送完成 | 全局统计:总发送40 | 成功40 | 失败0 +2026-02-27 00:49:22 | INFO | ================================================== 发送统计汇总 ================================================== +2026-02-27 00:49:22 | INFO | 全局统计 | 总发送:40 | 成功:40 | 失败:0 +2026-02-27 00:49:22 | INFO | 端口统计: +2026-02-27 00:49:22 | INFO | 外设:Uart0 | 端口: 4000 | 发送: 20 | 成功: 20 | 失败: 0 | 成功率:100.00% +2026-02-27 00:49:22 | INFO | 外设:CAN0 | 端口: 4001 | 发送: 20 | 成功: 20 | 失败: 0 | 成功率:100.00% +2026-02-27 00:49:22 | INFO | ==================================================================================================== +2026-02-27 00:49:22 | INFO | 故障注入平台UDP模拟器已关闭,资源释放完成 +2026-02-27 09:52:45 | INFO | 故障注入平台UDP模拟器初始化完成 +2026-02-27 09:52:45 | INFO | 开始多端口并发发送 | 外设列表:['Uart0', 'CAN0', '1553B0', 'AD0', 'OC0'] | 每端口发送次数:20 +2026-02-27 09:52:45 | WARNING | 外设1553B0不存在,跳过 +2026-02-27 09:52:45 | WARNING | 外设AD0不存在,跳过 +2026-02-27 09:52:45 | WARNING | 外设OC0不存在,跳过 +2026-02-27 09:52:47 | INFO | 多端口并发发送完成 | 全局统计:总发送40 | 成功40 | 失败0 +2026-02-27 09:52:47 | INFO | ================================================== 发送统计汇总 ================================================== +2026-02-27 09:52:47 | INFO | 全局统计 | 总发送:40 | 成功:40 | 失败:0 +2026-02-27 09:52:47 | INFO | 端口统计: +2026-02-27 09:52:47 | INFO | 外设:Uart0 | 端口: 4000 | 发送: 20 | 成功: 20 | 失败: 0 | 成功率:100.00% +2026-02-27 09:52:47 | INFO | 外设:CAN0 | 端口: 4001 | 发送: 20 | 成功: 20 | 失败: 0 | 成功率:100.00% +2026-02-27 09:52:47 | INFO | ==================================================================================================== +2026-02-27 09:52:47 | INFO | 故障注入平台UDP模拟器已关闭,资源释放完成 +2026-02-27 09:53:07 | INFO | 故障注入平台UDP模拟器初始化完成 +2026-02-27 09:53:07 | INFO | 开始多端口并发发送 | 外设列表:['Uart0', 'CAN0', '1553B0', 'AD0', 'OC0'] | 每端口发送次数:20 +2026-02-27 09:53:07 | WARNING | 外设1553B0不存在,跳过 +2026-02-27 09:53:07 | WARNING | 外设AD0不存在,跳过 +2026-02-27 09:53:07 | WARNING | 外设OC0不存在,跳过 +2026-02-27 09:53:09 | INFO | 多端口并发发送完成 | 全局统计:总发送40 | 成功40 | 失败0 +2026-02-27 09:53:09 | INFO | ================================================== 发送统计汇总 ================================================== +2026-02-27 09:53:09 | INFO | 全局统计 | 总发送:40 | 成功:40 | 失败:0 +2026-02-27 09:53:09 | INFO | 端口统计: +2026-02-27 09:53:09 | INFO | 外设:Uart0 | 端口: 4000 | 发送: 20 | 成功: 20 | 失败: 0 | 成功率:100.00% +2026-02-27 09:53:09 | INFO | 外设:CAN0 | 端口: 4001 | 发送: 20 | 成功: 20 | 失败: 0 | 成功率:100.00% +2026-02-27 09:53:09 | INFO | ==================================================================================================== +2026-02-27 09:53:09 | INFO | 故障注入平台UDP模拟器已关闭,资源释放完成 +2026-02-27 14:25:34 | INFO | 故障注入平台UDP模拟器初始化完成 +2026-02-27 14:25:34 | INFO | 开始多端口并发发送 | 外设列表:['Uart0', 'CAN0', '1553B0', 'AD0', 'OC0'] | 每端口发送次数:20 +2026-02-27 14:25:34 | WARNING | 外设1553B0不存在,跳过 +2026-02-27 14:25:34 | WARNING | 外设AD0不存在,跳过 +2026-02-27 14:25:34 | WARNING | 外设OC0不存在,跳过 +2026-02-27 14:25:36 | INFO | 多端口并发发送完成 | 全局统计:总发送40 | 成功40 | 失败0 +2026-02-27 14:25:36 | INFO | ================================================== 发送统计汇总 ================================================== +2026-02-27 14:25:36 | INFO | 全局统计 | 总发送:40 | 成功:40 | 失败:0 +2026-02-27 14:25:36 | INFO | 端口统计: +2026-02-27 14:25:36 | INFO | 外设:Uart0 | 端口: 4000 | 发送: 20 | 成功: 20 | 失败: 0 | 成功率:100.00% +2026-02-27 14:25:36 | INFO | 外设:CAN0 | 端口: 4001 | 发送: 20 | 成功: 20 | 失败: 0 | 成功率:100.00% +2026-02-27 14:25:36 | INFO | ==================================================================================================== +2026-02-27 14:25:36 | INFO | 故障注入平台UDP模拟器已关闭,资源释放完成 +2026-02-27 22:07:12 | INFO | 故障注入平台UDP模拟器初始化完成 +2026-02-27 22:07:12 | INFO | 开始多端口并发发送 | 外设列表:['Uart0', 'CAN0', '1553B0', 'AD0', 'OC0'] | 每端口发送次数:20 +2026-02-27 22:07:12 | WARNING | 外设1553B0不存在,跳过 +2026-02-27 22:07:12 | WARNING | 外设AD0不存在,跳过 +2026-02-27 22:07:12 | WARNING | 外设OC0不存在,跳过 +2026-02-27 22:07:14 | INFO | 多端口并发发送完成 | 全局统计:总发送40 | 成功40 | 失败0 +2026-02-27 22:07:14 | INFO | ================================================== 发送统计汇总 ================================================== +2026-02-27 22:07:14 | INFO | 全局统计 | 总发送:40 | 成功:40 | 失败:0 +2026-02-27 22:07:14 | INFO | 端口统计: +2026-02-27 22:07:14 | INFO | 外设:Uart0 | 端口: 4000 | 发送: 20 | 成功: 20 | 失败: 0 | 成功率:100.00% +2026-02-27 22:07:14 | INFO | 外设:CAN0 | 端口: 4001 | 发送: 20 | 成功: 20 | 失败: 0 | 成功率:100.00% +2026-02-27 22:07:14 | INFO | ==================================================================================================== +2026-02-27 22:07:14 | INFO | 故障注入平台UDP模拟器已关闭,资源释放完成 +2026-02-27 22:10:52 | INFO | 故障注入平台UDP模拟器初始化完成 +2026-02-27 22:10:52 | INFO | 开始多端口并发发送 | 外设列表:['Uart0', 'CAN0', '1553B0', 'AD0', 'OC0'] | 每端口发送次数:20 +2026-02-27 22:10:52 | WARNING | 外设1553B0不存在,跳过 +2026-02-27 22:10:52 | WARNING | 外设AD0不存在,跳过 +2026-02-27 22:10:52 | WARNING | 外设OC0不存在,跳过 +2026-02-27 22:10:54 | INFO | 多端口并发发送完成 | 全局统计:总发送40 | 成功40 | 失败0 +2026-02-27 22:10:54 | INFO | ================================================== 发送统计汇总 ================================================== +2026-02-27 22:10:54 | INFO | 全局统计 | 总发送:40 | 成功:40 | 失败:0 +2026-02-27 22:10:54 | INFO | 端口统计: +2026-02-27 22:10:54 | INFO | 外设:Uart0 | 端口: 4000 | 发送: 20 | 成功: 20 | 失败: 0 | 成功率:100.00% +2026-02-27 22:10:54 | INFO | 外设:CAN0 | 端口: 4001 | 发送: 20 | 成功: 20 | 失败: 0 | 成功率:100.00% +2026-02-27 22:10:54 | INFO | ==================================================================================================== +2026-02-27 22:10:54 | INFO | 故障注入平台UDP模拟器已关闭,资源释放完成 diff --git a/main-20260227-01.py b/main-20260227-01.py new file mode 100644 index 0000000..ccd1b6e --- /dev/null +++ b/main-20260227-01.py @@ -0,0 +1,191 @@ +# -*- coding: utf-8 -*- +import sys +import os +import io +import time + +# 设置标准输出和标准错误的编码为UTF-8 +sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8') +sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8') + +from loguru import logger +from midware.ui.main_ui import start_main_ui +from midware.config.base_config import init_base_config +#from midware.core.data_forward import init_forward_service +from midware.config.excel_config import load_csv_config +from midware.core.error_check import check_checksum, verify_frame_integrity +from midware.network.udp_handler import udp_server +from midware.network.protocol_dispatcher import dispatch_udp_data + +import midware.config.base_config as base_config + +# from midware.config.base_config import init_base_config +# from midware.config.base_config import DEVICE_CONFIG_DICT +# 先导入配置核心模块 +from midware.config.base_config import init_base_config, update_device_config, get_device_config +# 后导入UDP模块(此时udp_server还是None) +from midware.network.udp_handler import MultiUDPServer, udp_server + +# 添加工程根目录到环境变量 +sys.path.append(os.path.dirname(os.path.abspath(__file__))) +''' +1. 程序入口(main.py) +负责初始化日志、加载配置、启动 UI 和核心服务,实现程序一键启动 +''' +# 初始化日志 +def init_logger(): + logger.add( + "logs/run_{time:YYYY-MM-DD}.log", + rotation="daily", + level="INFO", + format="{time} | {level} | {module}:{line} | {message}" + ) + logger.add( + "logs/error_{time:YYYY-MM-DD}.log", + rotation="daily", + level="ERROR", + format="{time} | {level} | {module}:{line} | {message}" + ) + return logger + +if __name__ == "__main__": + # 初始化日志 + log = init_logger() + log.info("虚拟仿真平台中间层软件启动") + logger.info("========== 虚拟仿真平台中间层软件 启动 ==========") + ##试一下2026/2/27 + csv_path = "config_files/device_config.csv" + + DEVICE_CONFIG_DICT = load_csv_config(csv_path) + + #try: + # 初始化基础配置(外设、UDP、总线) + # 步骤1:初始化外设配置(核心!先初始化配置,再实例化其他模块) + init_base_config() + + device_config = get_device_config() + if not device_config: + logger.error("配置加载为空,工程无法启动,退出!") + exit(1) + + # 初始化数据转发核心服务 + #init_forward_service() + + if not base_config.DEVICE_CONFIG_DICT: + logger.error("外设配置加载失败,工程启动终止") + else: + logger.info("外设配置加载成功") + print("程序运行到这里了") + + # 步骤2:实例化UDP单例服务(此时配置已初始化,能读取到有效数据) + global udp_server + udp_server = MultiUDPServer() + logger.info(f"工程启动成功|外设数量:{len(device_config)}|UDP监听端口:{list(udp_server.udp_sockets.keys())}") + + # 初始化UDP服务器 + #udp_server.__init__() # 确保先初始化 + + ''' + # # 初始化UDP服务器 + # udp_server.init() # 确保先初始化 + # 启动主UI + start_main_ui() + log.info("软件运行中...") + except Exception as e: + log.error(f"程序启动失败:{str(e)}", exc_info=True) + sys.exit(1) + ''' + + # 步骤3:工程主循环(阻塞运行,处理UDP数据) + try: + while True: + # 非阻塞接收所有UDP端口数据 + recv_data = udp_server.recv_multi_udp_hex() + if recv_data: + # 后续对接协议解析层/总线适配层的逻辑写在这里 + for data in recv_data: + port = data["port"] + dev_name = data["dev_name"] + protocol = data["protocol"] + raw_data = data["raw_data"] + if len(raw_data) > 0: + #打印原始数据(测试用) + print(f"接收到 {dev_name} 的原始数据:{raw_data}, 协议类型:{protocol}") + + # 根据协议类型进行数据解析和转发 + print("后续根据设备的协议类型进行数据解析和转发...") + + #判断协议类型,采用不同解析和转发方法 + if protocol == "UART": + pass + + # 示例:直接回传数据到故障注入软件(测试用) + # 2. 分发处理数据(核心调用) + dispatch_udp_data(recv_data) + + # 示例:直接回传数据到故障注入软件(测试用) + udp_server.send_to_fault(raw_data, dev_name) + # 主循环间隔1us,不影响实时性 + time.sleep(1e-6) + except KeyboardInterrupt: + logger.info("用户终止工程,开始释放资源...") + finally: + # 退出时关闭UDP服务 + udp_server.close() + logger.info("========== 虚拟仿真平台中间层软件 退出 ==========") + + +# if name == "main": + #test_config = load_excel_config("../../config_files/device_config.xlsx") + #test_config = load_excel_config("config_files/device_config.xlsx") + + # test_config = load_csv_config("config_files/device_config.csv") ##暂时注释这行 + # print(test_config) + ''' + #测试校验和代码(单独运行该文件时执行) + + #测试 UART 校验:原始数据 + b'x9c\x47'(crc16_modbus 校验位) + uart_data = b"test_uart_data" + b'\x9c\x47' + print (f"UART 校验结果:{check_checksum (uart_data, 'UART', 'Uart0')}") + #测试 CAN 校验:原始数据(10 字节)+b'\x1a\x2b'(crc16_ccitt 校验位),总长度 12≤13(11+2) + can_data = b"test_can_123" + b'\x1a\x2b' + print (f"CAN 校验结果:{check_checksum (can_data, 'CAN', 'CAN1')}") + print (f"CAN 帧完整性结果:{verify_frame_integrity (can_data, 'CAN', 'CAN1')}") + #测试 AD 校验:原始数据 + b'\x5f'(累加和校验位) + ad_data = b"test_ad" + b'\x5f' + print (f"AD 校验结果:{check_checksum (ad_data, 'AD', 'AD0')}") + #测试失败场景:校验位错误 + wrong_checksum_data = b"test_uart_data" + b'\x00\x00' + print (f"UART 校验失败场景结果:{check_checksum (wrong_checksum_data, 'UART', 'Uart0')}") + ''' + + try: + # 初始化基础配置 + init_base_config() + + # 初始化UDP服务器 + udp_server.init() # 确保先初始化 + + # 测试数据接收 + data_list = udp_server.recv_multi_udp_hex() + for data in data_list: + dev_name = data["dev_name"] + raw_data = data["raw_data"] + # 对每个外设的原始数据进行协议处理... + + # 2. 向故障注入软件发送数据 + if len(raw_data) > 0: + udp_server.send_to_fault(raw_data, dev_name="Uart0") + + # 3. 外设配置更新后,重新加载UDP端口 + udp_server.reload_sockets() + + # 4. 程序退出时关闭所有套接字 + udp_server.close() + + except Exception as e: + log.error(f"程序启动失败:{str(e)}", exc_info=True) + sys.exit(1) + + + \ No newline at end of file diff --git a/main-20260228.py b/main-20260228.py new file mode 100644 index 0000000..8f00346 --- /dev/null +++ b/main-20260228.py @@ -0,0 +1,199 @@ +# -*- coding: utf-8 -*- +import sys +import os +import io +import time +from pathlib import Path + +# ========== 修复点1:安全重定向标准流(避免直接覆盖导致关闭) ========== +def safe_redirect_stdio(): + """安全重定向stdout/stderr为UTF-8,兼容流未初始化/已关闭场景""" + try: + # 仅在流可用时重定向,使用原始buffer避免递归关闭 + if sys.stdout and not sys.stdout.closed: + sys.stdout = io.TextIOWrapper( + sys.stdout.buffer, + encoding='utf-8', + line_buffering=True, # 行缓冲避免数据滞留 + write_through=True # 立即写入避免句柄阻塞 + ) + if sys.stderr and not sys.stderr.closed: + sys.stderr = io.TextIOWrapper( + sys.stderr.buffer, + encoding='utf-8', + line_buffering=True, + write_through=True + ) + except Exception as e: + # 重定向失败时使用原始stdout输出,避免崩溃 + print(f"标准流重定向警告: {e}", file=sys.__stdout__) + +# 先执行安全重定向(放在导入前,避免导入时输出乱码) +safe_redirect_stdio() + +# ========== 修复点2:规范导入(避免重复/顺序错误) ========== +from loguru import logger +from midware.ui.main_ui import start_main_ui +from midware.config.base_config import ( + init_base_config, + update_device_config, + get_device_config +) +from midware.config.excel_config import load_csv_config +from midware.core.error_check import check_checksum, verify_frame_integrity +from midware.network.udp_handler import MultiUDPServer +from midware.network.protocol_dispatcher import dispatch_udp_data +import midware.config.base_config as base_config + +# 添加工程根目录到环境变量(确保配置文件路径正确) +PROJECT_ROOT = os.path.dirname(os.path.abspath(__file__)) +sys.path.append(PROJECT_ROOT) +# 修正配置文件路径为绝对路径(避免相对路径导致文件找不到) +CSV_CONFIG_PATH = os.path.join(PROJECT_ROOT, "config_files/device_config.csv") + +# ========== 修复点3:初始化日志(避免日志写入触发流错误) ========== +def init_logger(): + """初始化日志,使用原始stdout避免重定向流关闭问题""" + # 清空默认日志配置 + logger.remove() + # 确保日志目录存在 + Path("logs").mkdir(exist_ok=True) + + # 运行日志 + logger.add( + "logs/run_{time:YYYY-MM-DD}.log", + rotation="daily", + level="INFO", + format="{time} | {level} | {module}:{line} | {message}", + enqueue=True, # 异步写入避免句柄阻塞 + catch=True # 捕获日志写入异常 + ) + # 错误日志 + logger.add( + "logs/error_{time:YYYY-MM-DD}.log", + rotation="daily", + level="ERROR", + format="{time} | {level} | {module}:{line} | {message}", + enqueue=True, + catch=True + ) + # 控制台输出(关键:使用原始stdout,避免重定向流关闭报错) + logger.add( + sys.__stdout__, + level="INFO", + format="{time} | {level} | {module}:{line} | {message}", + enqueue=True, + catch=True + ) + return logger + +# ========== 主程序逻辑(核心修复:结构清晰+资源安全释放) ========== +if __name__ == "__main__": + # 初始化日志(最先执行,避免后续报错无日志) + log = init_logger() + log.info("虚拟仿真平台中间层软件启动") + logger.info("========== 虚拟仿真平台中间层软件 启动 ==========") + + # ========== 修复点4:安全加载CSV配置(核心解决load_csv_config报错) ========== + ''' + try: + # 加载CSV配置(使用绝对路径,避免文件找不到导致流异常) + DEVICE_CONFIG_DICT = load_csv_config(CSV_CONFIG_PATH) + if not DEVICE_CONFIG_DICT: + logger.error(f"CSV配置文件加载为空!路径:{CSV_CONFIG_PATH}") + sys.exit(1) + logger.info(f"CSV配置加载成功|设备数量:{len(DEVICE_CONFIG_DICT)}") + except Exception as e: + logger.error(f"加载CSV配置失败:{str(e)}", exc_info=True) + sys.exit(1) + ''' + # 初始化基础配置 + try: + init_base_config() + device_config = get_device_config() + if not device_config: + logger.error("基础配置加载为空,工程无法启动!") + sys.exit(1) + logger.info("基础配置初始化成功") + except Exception as e: + logger.error(f"基础配置初始化失败:{str(e)}", exc_info=True) + sys.exit(1) + + # 检查外设配置 + if not base_config.DEVICE_CONFIG_DICT: + logger.error("外设配置加载失败,工程启动终止") + sys.exit(1) + else: + logger.info("外设配置加载成功") + # 改用logger输出,避免print触发流错误 + logger.info("程序运行到这里了") + + # 实例化UDP服务(避免global关键字,减少生命周期问题) + udp_server = None + try: + udp_server = MultiUDPServer() + logger.info( + f"工程启动成功|外设数量:{len(device_config)}|" + f"UDP监听端口:{list(udp_server.udp_sockets.keys()) if hasattr(udp_server, 'udp_sockets') else []}" + ) + except Exception as e: + logger.error(f"UDP服务初始化失败:{str(e)}", exc_info=True) + sys.exit(1) + + # 主循环:处理UDP数据 + try: + # 启动主UI(如需启用,取消注释) + # start_main_ui() + + logger.info("进入UDP数据处理主循环...") + while True: + if udp_server is None: + break + + # 非阻塞接收UDP数据 + recv_data = udp_server.recv_multi_udp_hex() + if recv_data: + for data in recv_data: + # 安全取值,避免KeyError + port = data.get("port", "") + dev_name = data.get("dev_name", "") + protocol = data.get("protocol", "") + raw_data = data.get("raw_data", b"") + + if len(raw_data) > 0: + # 改用logger输出,避免print操作关闭的流 + logger.info(f"接收到 {dev_name} 的原始数据:{raw_data}, 协议类型:{protocol}") + logger.info("后续根据设备的协议类型进行数据解析和转发...") + + # 协议类型判断 + if protocol == "UART": + pass + + # 分发处理数据(修正:传入单条数据,而非整个列表) + # dispatch_udp_data([data]) + + # 回传数据到故障注入软件 + udp_server.send_to_fault(raw_data, dev_name) + # 微休眠,降低CPU占用 + time.sleep(1e-6) + + except KeyboardInterrupt: + logger.info("用户终止工程,开始释放资源...") + except Exception as e: + logger.error(f"主循环异常:{str(e)}", exc_info=True) + finally: + # ========== 修复点5:安全释放资源(避免重复关闭/空对象调用) ========== + try: + # 关闭UDP服务(仅当对象存在且未关闭时) + if udp_server and hasattr(udp_server, "close"): + udp_server.close() + logger.info("UDP服务已关闭") + except Exception as e: + logger.warning(f"UDP服务关闭警告:{str(e)}") + + # 刷新并关闭日志(避免日志句柄泄漏) + logger.info("========== 虚拟仿真平台中间层软件 退出 ==========") + logger.remove() # 清理所有日志句柄 + + # 安全退出 + sys.exit(0) \ No newline at end of file diff --git a/main.py b/main.py new file mode 100644 index 0000000..16e3488 --- /dev/null +++ b/main.py @@ -0,0 +1,171 @@ +""" +anthor:Tan Mingyan +time:2026/3/6 +功能:主函数,实现多线程,UDP收发和总线收发 +""" + +import sys +import os +import io +import time +from pathlib import Path + +# main.py 导入解耦模块 +from midware.core import start_all_threads, stop_all_threads +from midware.network.udp_handler import MultiUDPServer +from loguru import logger +from midware.config.base_config import ( + init_base_config, + update_device_config, + get_device_config +) +import midware.config.base_config as base_config +from midware.drivers import DRIVER_MAPPING #2026/3/9 + +# 添加工程根目录到环境变量(确保配置文件路径正确) +PROJECT_ROOT = os.path.dirname(os.path.abspath(__file__)) +sys.path.append(PROJECT_ROOT) +# 修正配置文件路径为绝对路径(避免相对路径导致文件找不到) +CSV_CONFIG_PATH = os.path.join(PROJECT_ROOT, "config_files/device_config.csv") + +# 驱动配置(可从yaml文件加载)波特率暂时无效 +DRIVER_CONFIGS = { + "1553B": {"board_id": 0, "bc_address": 0x01}, + "CAN": {"channel": 0, "baudrate": 500000}, + "UART": {"port": "COM3", "baudrate": 9600}, + "AD": {"sampling_rate": 1000}, + "OC": {"output_voltage": 5.0}, + "网络": {"ip": "192.168.1.100", "port": 8080} +} + + +# ========== 修复点3:初始化日志(避免日志写入触发流错误) ========== +def init_logger(): + """初始化日志,使用原始stdout避免重定向流关闭问题""" + # 清空默认日志配置 + logger.remove() + # 确保日志目录存在 + Path("logs").mkdir(exist_ok=True) + + # 运行日志 + logger.add( + "logs/run_{time:YYYY-MM-DD}.log", + rotation="daily", + level="INFO", + format="{time} | {level} | {module}:{line} | {message}", + enqueue=True, # 异步写入避免句柄阻塞 + catch=True # 捕获日志写入异常 + ) + # 错误日志 + logger.add( + "logs/error_{time:YYYY-MM-DD}.log", + rotation="daily", + level="ERROR", + format="{time} | {level} | {module}:{line} | {message}", + enqueue=True, + catch=True + ) + # 控制台输出(关键:使用原始stdout,避免重定向流关闭报错) + logger.add( + sys.__stdout__, + level="INFO", + format="{time} | {level} | {module}:{line} | {message}", + enqueue=True, + catch=True + ) + return logger + +# 启动线程(无需写冗长路径) +#threads = start_all_threads(udp_server) +# main.py 核心修改 +if __name__ == "__main__": + # 初始化日志、加载配置、实例化UDP服务(原有逻辑)... + # 初始化日志(最先执行,避免后续报错无日志) + log = init_logger() + log.info("虚拟仿真平台中间层软件启动") + logger.info("========== 虚拟仿真平台中间层软件 启动 ==========") + +# 初始化基础配置 + try: + init_base_config() + device_config = get_device_config() + if not device_config: + logger.error("基础配置加载为空,工程无法启动!") + sys.exit(1) + logger.info("基础配置初始化成功") + except Exception as e: + logger.error(f"基础配置初始化失败:{str(e)}", exc_info=True) + sys.exit(1) + + # 检查外设配置 + if not base_config.DEVICE_CONFIG_DICT: + logger.error("外设配置加载失败,工程启动终止") + sys.exit(1) + else: + logger.info("外设配置加载成功") + # 改用logger输出,避免print触发流错误 + logger.info("程序运行到这里了") + + # 实例化UDP服务(避免global关键字,减少生命周期问题) + udp_server = None + try: + udp_server = MultiUDPServer() + logger.info( + f"工程启动成功|外设数量:{len(device_config)}|" + f"UDP监听端口:{list(udp_server.udp_sockets.keys()) if hasattr(udp_server, 'udp_sockets') else []}" + ) + except Exception as e: + logger.error(f"UDP服务初始化失败:{str(e)}", exc_info=True) + sys.exit(1) + + # 启动所有解耦线程(替代原主循环) + #threads = start_all_threads(udp_server) + threads = start_all_threads( + udp_server=udp_server, + bus_driver_configs=DRIVER_CONFIGS # 2026/3/9 新增驱动配置参数 + ) + # main.py 主线程 + ''' + while True: + # 打印各队列长度 + logger.info("队列状态:") + for proto, q in bus_recv_queues.items(): + logger.info(f" {proto} 接收队列:{q.qsize()}/{q.maxsize}") + for proto, q in bus_send_queues.items(): + logger.info(f" {proto} 发送队列:{q.qsize()}/{q.maxsize}") + time.sleep(1) # 每秒监控一次 + ''' + # 主线程:仅负责等待退出信号,不处理业务 + + # main.py + from midware.drivers.renode_agent import renode, renode_async_data_dispatcher + + # 只加这一行 + if not renode.start(): + exit() + time.sleep(5) + renode.start_async_listener(renode_async_data_dispatcher) + time.sleep(10) + # 加载完DEVICE_CONFIG_DICT之后加: + + from midware.drivers.renode_agent import start_renode_uart_redirect + from midware.drivers.tcp_uart_agent import start_all_tcp_uart_clients + + # 启动Renode的UART TCP重定向 + start_renode_uart_redirect() + time.sleep(1) + + # 启动所有TCP客户端接收 + start_all_tcp_uart_clients() + + try: + while True: + time.sleep(1) # 主线程休眠,避免CPU占用 + except KeyboardInterrupt: + logger.info("用户终止程序,开始优雅退出...") + stop_all_threads() # 等待队列处理完成 + udp_server.close() # 关闭UDP服务 + udp_server.socket.close() # 强制关闭系统套接字 + logger.info("程序正常退出") + + \ No newline at end of file diff --git a/midware/bus/__init__.py b/midware/bus/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/midware/bus/cache_handler.py b/midware/bus/cache_handler.py new file mode 100644 index 0000000..31f7e95 --- /dev/null +++ b/midware/bus/cache_handler.py @@ -0,0 +1,55 @@ +''' +5. 缓存交互模块(midware/bus/cache_handler.py) +实现与 SJA1000/61580 共享缓存的交互,提供WriteDoubleWord/ReadDoubleWord核心操作,对接虚拟芯片: +''' +from loguru import logger + +class CacheHandler: + """缓存交互层,实现sysbus WriteDoubleWord/ReadDoubleWord操作""" + def __init__(self): + # 模拟共享缓存(字典实现,key=内存地址,value=32位无符号整数) + self.shared_cache = {} + logger.info("共享缓存初始化完成(模拟SJA1000/61580)") + + def write_double_word(self, mem_addr, data): + """ + 写入双字(32位)数据到共享缓存 + :param mem_addr: 内存地址(int) + :param data: 16/32位数据(int),自动转为32位无符号整数 + """ + if not isinstance(mem_addr, int) or not isinstance(data, int): + logger.error("写入缓存参数错误,地址/数据必须为整数") + return False + # 转为32位无符号整数 + + ##给RENODE的写入的语句未定 + self.shared_cache[mem_addr] = data & 0xFFFFFFFF + logger.debug(f"写入缓存:地址{hex(mem_addr)},数据{hex(self.shared_cache[mem_addr])}") + return True + + def read_double_word(self, mem_addr): + """ + 从共享缓存读取双字(32位)数据 + :param mem_addr: 内存地址(int) + :return: 32位无符号整数,地址不存在返回0 + """ + if not isinstance(mem_addr, int): + logger.error("读取缓存参数错误,地址必须为整数") + return 0 + data = self.shared_cache.get(mem_addr, 0) + logger.debug(f"读取缓存:地址{hex(mem_addr)},数据{hex(data)}") + return data + + def clear_cache(self, mem_addr=None): + """清空指定地址/全部缓存""" + if mem_addr: + if mem_addr in self.shared_cache: + del self.shared_cache[mem_addr] + logger.debug(f"清空缓存地址:{hex(mem_addr)}") + else: + self.shared_cache.clear() + logger.info("清空全部共享缓存") + return True + +# 单例模式,全局唯一缓存实例 +cache_handler = CacheHandler() \ No newline at end of file diff --git a/midware/bus/chip_register.py b/midware/bus/chip_register.py new file mode 100644 index 0000000..e69de29 diff --git a/midware/config/__init__.py b/midware/config/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/midware/config/__pycache__/__init__.cpython-311.pyc b/midware/config/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000..820a668 Binary files /dev/null and b/midware/config/__pycache__/__init__.cpython-311.pyc differ diff --git a/midware/config/__pycache__/base_config.cpython-311.pyc b/midware/config/__pycache__/base_config.cpython-311.pyc new file mode 100644 index 0000000..240d216 Binary files /dev/null and b/midware/config/__pycache__/base_config.cpython-311.pyc differ diff --git a/midware/config/__pycache__/excel_config.cpython-311.pyc b/midware/config/__pycache__/excel_config.cpython-311.pyc new file mode 100644 index 0000000..756e1ab Binary files /dev/null and b/midware/config/__pycache__/excel_config.cpython-311.pyc differ diff --git a/midware/config/base_config.py b/midware/config/base_config.py new file mode 100644 index 0000000..e31aa64 --- /dev/null +++ b/midware/config/base_config.py @@ -0,0 +1,179 @@ +import os +from loguru import logger +import threading +from midware.config.excel_config import load_csv_config + +''' +3. 基础配置模块(midware/config/base_config.py) +定义全局配置常量,初始化外设、UDP、总线基础参数,预留配置动态加载接口: +''' +# 基础配置 - UDP(故障注入软件交互) +UDP_CONFIG = { + "LOCAL_IP": "10.20.48.138", # 本地UDP地址,用于回环测试,自测时候是"127.0.0.1",科学所是"192.168.0.150"","10.20.48.138" + "LOCAL_PORT": 8888, # 本地UDP端口 + "FAULT_INJECT_IP": "10.20.48.129",# 故障注入软件IP,用于回环测试,自测时候是"127.0.0.1",科学所是"192.168.0.130"","10.20.48.129" + "FAULT_INJECT_PORT": 18889, # 故障注入软件端口 + "BUFFER_SIZE": 4096 # UDP缓冲区大小 +} + +# 基础配置 - 外设总线(支持UART/CAN/1553B/AD/OC) +BUS_CONFIG = { + "SUPPORT_BUS": ["UART", "CAN", "1553B", "AD", "OC"], + "MAX_DEVICE_NUM": 10, # 最大支持10个单机 + "WRITE_BYTE_UART": 2, # UART每次写入2字节 + "WRITE_BYTE_CAN": 2, # CAN每次写入2字节 + "READ_BYTE_CAN": 1, # CAN每次读取1字节 + "WRITE_BYTE_1553B": 2, # 1553B每次写入2字节 + "WRITE_BYTE_AD": 2, # AD每次写入2字节 + "READ_BYTE_OC": 1 # OC每次读取1字节 +} + +# 基础配置 - UART芯片寄存器(初始写死,后续由chip_config.ini加载) +UART_CHIP_CONFIG = { + "TBR": 0x00, # 发送FIFO + "FIFO_STATUS": 0x04, # FIFO状态寄存器 + "FRAME_COUNT": 0x08, # 帧计数 + "FIFO_REMAIN": 0x0C, # FIFO剩余字节数 + "CLOCK_CONFIG": 0x10, # 时钟配置 + "SCRAMBLE": 0x14, # 加解扰使能 + "RESET": 0x7C # 复位 +} + +# 全局外设配置字典,存储已启用的外设信息 +# ==================== 全局配置(唯一数据源,仅在此文件定义)==================== +# 初始化为空字典,由init_base_config()初始化 +DEVICE_CONFIG_DICT = {} #{1: {}, 2: {}, 3: {}, 4: {}, 5: {}, 6: {}, 7: {}, 8: {}, 9: {}, 10: {}} + +# 配置读写锁(保证多线程安全,防止读写冲突) +CONFIG_LOCK = threading.RLock() + +# ==================== 日志初始化(工程全局)==================== +def init_logger(): + os.makedirs("logs", exist_ok=True) + # 运行日志+错误日志分离 + logger.add("logs/run_{time:YYYYMMDDHHmmss}.log", level="INFO", + format="{time:YYYY-MM-DD HH:mm:ss} | {level} | {message}") + logger.add("logs/error_{time:YYYYMMDDHHmmss}.log", level="ERROR", + format="{time:YYYY-MM-DD HH:mm:ss} | {level} | {file}:{line} | {message}") +# 工程启动时自动初始化日志 +init_logger() + +''' +# 加载xlsx文件 +def init_base_config(): + """初始化基础配置,加载默认外设配置""" + try: + # 加载默认Excel配置(后续实现) + from midware.config.excel_config import load_excel_config + default_excel = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), "config_files/device_config.xlsx") + if os.path.exists(default_excel): + DEVICE_CONFIG_DICT = load_excel_config(default_excel) + logger.info(f"加载默认Excel配置,共{len(DEVICE_CONFIG_DICT)}个外设") + else: + logger.warning(f"默认配置文件不存在:{default_excel},使用空配置") + logger.info("基础配置初始化完成") + except Exception as e: + logger.error(f"基础配置初始化失败:{str(e)}", exc_info=True) + raise e +''' + +''' +# midware/config/base_config.py 中init_base_config函数 +def init_base_config(): + """初始化基础配置,加载默认外设配置""" + global DEVICE_CONFIG_DICT + try: + # 替换为CSV加载方法 + #from midware.config.excel_config import load_csv_config + default_csv = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), "config_files/device_config.csv") + if os.path.exists(default_csv): + #global DEVICE_CONFIG_DICT + DEVICE_CONFIG_DICT = load_csv_config(default_csv) + logger.info(f"加载默认CSV配置,共{len(DEVICE_CONFIG_DICT)}个外设") + else: + logger.warning(f"默认配置文件不存在:{default_csv},使用空配置") + DEVICE_CONFIG_DICT = {} + logger.info("基础配置初始化完成") + except Exception as e: + logger.error(f"基础配置初始化失败:{str(e)}", exc_info=True) + DEVICE_CONFIG_DICT = {} + #raise e #注释掉这行,避免程序崩溃 + +def update_device_config(new_config: dict): + """ + 对外提供配置更新方法(如UI勾选外设、导入新配置时调用) + :param new_config: 新的外设配置字典,格式与DEVICE_CONFIG_DICT一致 + """ + global DEVICE_CONFIG_DICT + if isinstance(new_config, dict): + DEVICE_CONFIG_DICT = new_config + logger.info(f"外设配置已更新,当前共{len(DEVICE_CONFIG_DICT)}个外设") + # 配置更新后,自动触发UDP套接字重新加载(关键:保证UDP端口与配置同步) + from midware.network.udp_handler import udp_server + udp_server.reload_sockets() + else: + logger.error("配置更新失败:新配置非字典类型") +# 工程启动时自动初始化日志 +#init_logger() +''' + +# ==================== 配置核心方法(对外暴露,唯一入口)==================== +def init_base_config(csv_path: str = None): + """ + 初始化外设配置:工程启动时**唯一调用**,加载CSV配置到DEVICE_CONFIG_DICT + :param csv_path: 配置文件路径,默认取工程根目录config_files/device_config.csv + """ + global DEVICE_CONFIG_DICT # 声明修改全局变量 + if csv_path is None: + # 自动拼接工程根目录的配置文件路径 + csv_path = os.path.join( + os.path.dirname(os.path.dirname(os.path.dirname(__file__))), + "config_files/device_config.csv" + ) + # 加锁保证初始化原子性,防止多线程同时修改 + with CONFIG_LOCK: + try: + if os.path.exists(csv_path): + # 加载CSV配置并覆盖全局变量 + DEVICE_CONFIG_DICT = load_csv_config(csv_path) + logger.info(f"配置初始化成功|加载{len(DEVICE_CONFIG_DICT)}个外设|配置文件:{csv_path}") + else: + DEVICE_CONFIG_DICT = {} + logger.error(f"配置初始化失败|配置文件不存在:{csv_path}") + except Exception as e: + DEVICE_CONFIG_DICT = {} + logger.error(f"配置初始化异常|{str(e)}", exc_info=True) + return DEVICE_CONFIG_DICT + +def update_device_config(new_config: dict): + """ + 唯一配置更新入口:修改配置后自动同步UDP端口 + 【关键】延迟导入udp_server,避免模块顶层循环导入 + """ + global DEVICE_CONFIG_DICT + with CONFIG_LOCK: + if not isinstance(new_config, dict): + logger.error("配置更新失败|新配置非字典类型") + return False + # 覆盖全局配置 + DEVICE_CONFIG_DICT = new_config.copy() # 深拷贝,防止外部修改源字典 + logger.info(f"配置更新成功|当前外设数量:{len(DEVICE_CONFIG_DICT)}") + # 延迟导入:仅在函数内导入,避免模块加载时循环依赖 + try: + from midware.network.udp_handler import udp_server + udp_server.reload_sockets(new_config) # 传参同步配置,不依赖全局引用 + except ImportError as e: + logger.warning(f"UDP配置同步失败|未找到UDP模块:{str(e)}") + return True + +def get_device_config(): + """ + 全局配置**只读接口**:所有其他模块必须通过此方法获取配置 + 返回副本,防止外部修改原全局变量导致数据混乱 + """ + with CONFIG_LOCK: + return DEVICE_CONFIG_DICT.copy() # 返回深拷贝,彻底隔离原变量 + +def get_udp_config(): + """UDP基础配置只读接口""" + return UDP_CONFIG.copy() \ No newline at end of file diff --git a/midware/config/device_config.csv b/midware/config/device_config.csv new file mode 100644 index 0000000..6579e8a --- /dev/null +++ b/midware/config/device_config.csv @@ -0,0 +1,11 @@ +名称,协议类型,内存基地址,内存偏移地址,port,说明,发送缓存区大小,接收缓存区大小 +光纤陀螺A,UART,0x20800000,0x0000,4000,Uart0,512,512 +反作用轮A,CAN,0x20810000,0x0100,4001,CAN0,1024,1024 +1553B0,1553B,0x20820000,0x0200,4002,测控模块,2048,2048 +电压采集,AD,0x20830000,0x0300,4003,AD0,512,512 +开关量输出,OC,0x20840000,0x0400,4004,OC0,512,512 +光纤陀螺B,UART,0x20850000,0X0001,4005,UART1,512,512 +星敏A,CAN,0x20860000,0x0101,4006,CAN1,512,512 +数传A,1553B,0x20870000,0x0201,4007,1553B1,512,512 +电流采集,AD,0x20880000,0x0301,4018,AD1,512,512 +继电器输出,OC,0x20890000,0x0401,4009,OC1,512,512 diff --git a/midware/config/excel_config-20260226-02.py b/midware/config/excel_config-20260226-02.py new file mode 100644 index 0000000..10983d3 --- /dev/null +++ b/midware/config/excel_config-20260226-02.py @@ -0,0 +1,135 @@ +# -*- coding: utf-8 -*- +''' +Excel 配置解析(excel_config.py):基于openpyxl实现 Excel 文件解析,提取外设名称、UDP 端口、协议类型、基地址等信息,存入DEVICE_CONFIG_DICT。 +''' +import openpyxl +from loguru import logger +import re +import pandas as pd +import os # 新增:用于删除临时文件 +import sys +import io +# 设置标准输出和标准错误的编码为UTF-8 +sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8') +sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8') + +# 添加工程根目录到环境变量 +# sys.path.append(os.path.dirname(os.path.abspath(__file__))) + +def hex_to_int (hex_str): + """处理十六进制字符串转整数(兼容带逗号 / 空格的格式,如 0x2080,0000、0x0000 1234) + :param hex_str: 十六进制字符串 + :return: 转换后的整数,失败返回 0 + """ + if not isinstance (hex_str, str): + return 0 # 非字符串直接返回0 + # 移除逗号、空格等非十六进制字符 + clean_hex = re.sub (r'[,\s]', '', hex_str.strip ()) + try: + return int (clean_hex, 16) if clean_hex else 0 + except (ValueError, TypeError): + logger.warning (f"十六进制转换失败:{hex_str},默认返回 0") + return 0 + +def load_csv_config (file_path): + """加载 Excel/CSV 外设配置文件,解析核心配置项 + :param file_path: Excel/CSV文件路径(xlsx/xls/csv) + :return: 外设配置字典,key = 外设名称,value = 配置项字典 + """ + device_config = {} + wb = None # 初始化工作簿对象 + temp_excel = "file.xlsx" # CSV转换的临时Excel文件 + try: + # 处理CSV文件:转换为Excel后解析 + if file_path.endswith('.csv'): + df = pd.read_csv(file_path) + df.to_excel(temp_excel, index=False) + file_path = temp_excel # 切换为临时Excel文件路径 + + # 打开工作簿(兼容直接传入Excel文件的情况) + wb = openpyxl.load_workbook(file_path, data_only=True) + ws = wb.active + logger.info(f"成功打开配置文件:{file_path},工作表:{ws.title}") + + # 获取表头行,匹配核心配置列(兼容列名大小写 / 空格) + header = [cell.value.strip() if cell.value else "" for cell in ws[1]] + col_mapping = { + "名称": header.index([h for h in header if h.lower() == "名称"][0]) if [h for h in header if h.lower() == "名称"] else -1, + "协议类型": header.index([h for h in header if h.lower() in ["协议", "协议类型"]][0]) if [h for h in header if h.lower() in ["协议", "协议类型"]] else -1, + "内存基地址": header.index([h for h in header if h.lower() in ["内存基地址", "基地址"]][0]) if [h for h in header if h.lower() in ["内存基地址", "基地址"]] else -1, + "内存偏移地址": header.index([h for h in header if h.lower() in ["内存偏移地址", "偏移地址"]][0]) if [h for h in header if h.lower() in ["内存偏移地址", "偏移地址"]] else -1, + "UDP端口": header.index([h for h in header if h.lower() in ["UDP端口", "端口地址", "udp 地址", "UDP port","port"]][0]) if [h for h in header if h.lower() in ["UDP端口", "端口地址", "udp 地址", "UDP port","port"]] else -1, + "说明": header.index([h for h in header if h.lower() == "说明"][0]) if [h for h in header if h.lower() == "说明"] else -1, + "发送缓存区大小": header.index([h for h in header if h.lower() in ["发送缓存区大小", "发送缓存"]][0]) if [h for h in header if h.lower() in ["发送缓存区大小", "发送缓存"]] else -1, + "接收缓存区大小": header.index([h for h in header if h.lower() in ["接收缓存区大小", "接收缓存"]][0]) if [h for h in header if h.lower() in ["接收缓存区大小", "接收缓存"]] else -1, + } + + # 校验核心列(名称、协议、基地址必须存在) + required_cols = ["名称", "协议类型", "内存基地址"] + for col in required_cols: + if col_mapping[col] == -1: + logger.error(f"配置文件缺少核心列:{col},请检查表头") + return device_config + + # 遍历数据行(从第 2 行开始) + for row_num, row in enumerate(ws.iter_rows(min_row=2, values_only=True), start=2): + if not row[col_mapping["名称"]]: # 外设名称为空则跳过 + logger.warning(f"第 {row_num} 行:外设名称为空,跳过该行") + continue + + dev_name = row[col_mapping["名称"]].strip() + # 解析单行配置,缺省项赋默认值 + dev_info = { + "protocol": row[col_mapping["协议类型"]].strip().upper() if row[col_mapping["协议类型"]] else "", + "base_addr": hex_to_int(row[col_mapping["内存基地址"]]), + "offset_addr": hex_to_int(row[col_mapping["内存偏移地址"]]), + "udp_port": int(row[col_mapping["UDP端口"]]) if (row[col_mapping["UDP端口"]] and str(row[col_mapping["UDP端口"]]).isdigit()) else 8888, + "desc": row[col_mapping["说明"]].strip() if row[col_mapping["说明"]] else "无", + "send_cache": int(row[col_mapping["发送缓存区大小"]]) if (row[col_mapping["发送缓存区大小"]] and str(row[col_mapping["发送缓存区大小"]]).isdigit()) else 512, + "recv_cache": int(row[col_mapping["接收缓存区大小"]]) if (row[col_mapping["接收缓存区大小"]] and str(row[col_mapping["接收缓存区大小"]]).isdigit()) else 512, + "enable": True # 默认启用该外设 + } + + # 校验协议类型合法性 + if dev_info["protocol"] not in ["UART", "CAN", "1553B", "AD", "OC"]: + logger.warning(f"第 {row_num} 行 [{dev_name}]:协议类型 {dev_info['protocol']} 不合法,仅支持 UART/CAN/1553B/AD/OC,跳过该行") + continue + + device_config[dev_name] = dev_info + logger.debug( + f"解析外设配置:{dev_name} | {dev_info['protocol']} | 基地址 {hex(dev_info['base_addr'])} | " + f"端口号 {dev_info['udp_port']} | 发送缓存 {dev_info['send_cache']} | 接收缓存 {dev_info['recv_cache']} | 说明 {dev_info['desc']}" + ) + + logger.info(f"配置文件解析完成,共加载 {len(device_config)} 个有效外设配置") + print(f"配置文件解析完成,共加载 {len(device_config)} 个有效外设配置") + + except FileNotFoundError: + logger.error(f"配置文件不存在:{file_path}") + print(f"配置文件不存在:{file_path}") + except Exception as e: + logger.error(f"配置文件解析失败:{str(e)}", exc_info=True) + print(f"配置文件解析失败:{str(e)}") + finally: + # 确保工作簿最终关闭(无论是否异常) + if wb: + try: + wb.close() + logger.debug("Excel工作簿已正常关闭") + except Exception as e: + logger.warning(f"关闭Excel工作簿失败:{str(e)}") + # 删除CSV转换的临时Excel文件 + if os.path.exists(temp_excel): + try: + os.remove(temp_excel) + logger.debug("临时Excel文件已删除") + except Exception as e: + logger.warning(f"删除临时Excel文件失败:{str(e)}") + + return device_config + +# 测试代码(单独运行该文件时执行) +if __name__ == "__main__": + # test_config = load_csv_config("../../config_files/device_config.csv") # 替换为实际文件路径 + test_config = load_csv_config("config_files/device_config.csv") # 替换为实际文件路径,难道这的根目录 + print(test_config) \ No newline at end of file diff --git a/midware/config/excel_config-20260226.py b/midware/config/excel_config-20260226.py new file mode 100644 index 0000000..4d8642a --- /dev/null +++ b/midware/config/excel_config-20260226.py @@ -0,0 +1,137 @@ +# -*- coding: utf-8 -*- +''' +Excel 配置解析(excel_config.py):基于openpyxl实现 Excel 文件解析,提取外设名称、UDP 端口、协议类型、基地址等信息,存入DEVICE_CONFIG_DICT。 +''' +''' +### 核心功能说明 +1. **格式兼容**:支持解析Excel中**带逗号/空格的十六进制地址**(如`0x2080,0000`→`0x20800000`),自动清洗非十六进制字符。 +2. **列名容错**:兼容表头列名的**大小写、别名**(如“基地址”/“内存基地址”、“发送缓存”/“发送缓存区大小”)。 +3. **缺省值赋值**:未配置的项赋予合理默认值(如UDP端口默认8888、缓存大小默认512)。 +4. **合法性校验**: + - 必须包含**名称、协议类型、内存基地址**核心列,否则解析失败; + - 协议类型仅支持`UART/CAN/1553B/AD/OC`,非法协议直接跳过; + - 外设名称为空的行直接跳过。 +5. **数据类型转换**:自动将十六进制地址转整数、端口/缓存大小转整数,转换失败友好提示并赋默认值。 +6. **日志记录**:全程打印解析日志(成功/失败/警告),便于问题排查。 + +### 配套Excel配置文件规范 +1. 保存为**xlsx格式**(兼容openpyxl解析,xls格式需额外安装xlrd); +2. 表头至少包含:**名称、协议类型、内存基地址**,其他列可选; +3. 协议类型填写:`UART/CAN/1553B/AD/OC`(大小写均可); +4. 地址填写:十六进制格式(如`0x20800000`、`0x2080,0000`、`0x0000`); +5. 端口/缓存大小填写**纯数字**。 + +### 简单测试 +在`config_files/`目录下创建`device_config.xlsx`,填入如下测试数据,直接运行该文件即可看到解析结果: + +| 名称 | 协议类型 | 内存基地址 | 内存偏移地址 | UDP端口 | 说明 | 发送缓存区大小 | 接收缓存区大小 | +|--------|----------|------------|--------------|---------|------------|----------------|----------------| +| Uart0 | UART | 0x2080,0000 | 0x0000 | 8880 | 光纤陀螺A | 512 | 512 | +| CAN1 | CAN | 0x20810000 | 0x0100 | 8881 | 姿态传感器 | 1024 | 1024 | +| 1553B0 | 1553B | 0x20820000 | 0x0200 | 8882 | 测控模块 | 2048 | 2048 | +''' + +import openpyxl +from loguru import logger +import re +import pandas as pd + +import sys +import io +# 设置标准输出和标准错误的编码为UTF-8 +sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8') +sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8') + +def hex_to_int (hex_str): + """处理十六进制字符串转整数(兼容带逗号 / 空格的格式,如 0x2080,0000、0x0000 1234):param hex_str: 十六进制字符串:return: 转换后的整数,失败返回 0""" + if not isinstance (hex_str, str): + return 0 #移除逗号、空格等非十六进制字符 + clean_hex = re.sub (r'[,\s]', '', hex_str.strip ()) + try: + return int (clean_hex, 16) if clean_hex else 0 + except (ValueError, TypeError): + logger.warning (f"十六进制转换失败:{hex_str},默认返回 0") + return 0 +''' + """加载 Excel 外设配置文件,解析核心配置项 + :param file_path: Excel ,csv文件路径(xlsx/xls/csv) + :return: 外设配置字典,key = 外设名称,value = 配置项字典 + 错误处理: 配置文件不存在,配置文件解析失败 + """ +''' +def load_csv_config (file_path): + + device_config = {} + try: + #打开 Excel 文件,只读取第一个工作表 + # 先用 pandas 读取 CSV + df = pd.read_csv(file_path) + # 保存为 Excel + df.to_excel('file.xlsx', index=False) + # 然后使用 openpyxl 读取 + wb = openpyxl.load_workbook('file.xlsx', data_only=True) + + #wb = openpyxl.load_workbook(file_path, data_only=True) + ws = wb.active + logger.info(f"成功打开 Excel 配置文件:{file_path},工作表:{ws.title}") + #获取表头行,匹配核心配置列(兼容列名大小写 / 空格) + + header = [cell.value.strip () if cell.value else "" for cell in ws [1]] + col_mapping = {"名称": header.index ([h for h in header if h.lower () == "名称"][0]) if [h for h in header if h.lower () == "名称"] else -1, + "协议类型": header.index ([h for h in header if h.lower () in ["协议", "协议类型"]][0]) if [h for h in header if h.lower () in ["协议", "协议类型"]] else -1, + "内存基地址": header.index ([h for h in header if h.lower () in ["内存基地址", "基地址"]][0]) if [h for h in header if h.lower () in ["内存基地址", "基地址"]] else -1, + "内存偏移地址": header.index ([h for h in header if h.lower () in ["内存偏移地址", "偏移地址"]][0]) if [h for h in header if h.lower () in ["内存偏移地址", "偏移地址"]] else -1, + "UDP端口": header.index ([h for h in header if h.lower () in ["UDP端口", "端口地址", "udp 地址", "UDP port","port"]][0]) if [h for h in header if h.lower () in ["UDP端口", "端口地址", "udp 地址", "UDP port","port"]] else -1, + "说明": header.index ([h for h in header if h.lower () == "说明"][0]) if [h for h in header if h.lower () == "说明"] else -1, + "发送缓存区大小": header.index ([h for h in header if h.lower () in ["发送缓存区大小", "发送缓存"]][0]) if [h for h in header if h.lower () in ["发送缓存区大小", "发送缓存"]] else -1, + "接收缓存区大小": header.index ([h for h in header if h.lower () in ["接收缓存区大小", "接收缓存"]][0]) if [h for h in header if h.lower () in ["接收缓存区大小", "接收缓存"]] else -1, + } + # 校验核心列(名称、协议、基地址必须存在) + required_cols = ["名称", "协议类型", "内存基地址"] + for col in required_cols: + if col_mapping [col] == -1: + logger.error (f"Excel 配置文件缺少核心列:{col},请检查表头") + # wb.close () + return device_config + #遍历数据行(从第 2 行开始) + + for row_num, row in enumerate (ws.iter_rows (min_row=2, values_only=True), start=2): + if not row [col_mapping ["名称"]]: # 外设名称为空则跳过 + logger.warning (f"第 {row_num} 行:外设名称为空,跳过该行") + continue + dev_name = row [col_mapping ["名称"]].strip () + #解析单行配置,缺省项赋默认值 + dev_info = {"protocol": row [col_mapping ["协议类型"]].strip ().upper () if row [col_mapping ["协议类型"]] else "", + "base_addr": hex_to_int (row [col_mapping ["内存基地址"]]), + "offset_addr": hex_to_int (row [col_mapping ["内存偏移地址"]]), + "udp_port": int (row [col_mapping ["UDP端口"]]) if (row [col_mapping ["UDP端口"]] and str (row [col_mapping ["UDP端口"]]).isdigit ()) else 8888, + "desc": row [col_mapping ["说明"]].strip () if row [col_mapping ["说明"]] else "无", + "send_cache": int (row [col_mapping ["发送缓存区大小"]]) if (row [col_mapping ["发送缓存区大小"]] and str (row [col_mapping ["发送缓存区大小"]]).isdigit ()) else 512, + "recv_cache": int (row [col_mapping ["接收缓存区大小"]]) if (row [col_mapping ["接收缓存区大小"]] and str (row [col_mapping ["接收缓存区大小"]]).isdigit ()) else 512, + "enable": True # 默认启用该外设 + } + #校验协议类型合法性 + if dev_info ["protocol"] not in ["UART", "CAN", "1553B", "AD", "OC"]: + logger.warning (f"第 {row_num} 行 [{dev_name}]:协议类型 {dev_info ['protocol']} 不合法,仅支持 UART/CAN/1553B/AD/OC,跳过该行") + continue + device_config [dev_name] = dev_info + logger.debug (f"解析外设配置:{dev_name} | {dev_info ['protocol']} | 基地址 {hex (dev_info ['base_addr'])} | 端口号 {dev_info ['udp_port']} | 发送缓存 {dev_info ['send_cache']} | 接收缓存 {dev_info ['recv_cache']} | 说明 {dev_info ['desc']}") + wb.close() + logger.info(f"Excel 配置文件解析完成,共加载 {len (device_config)} 个有效外设配置") + print(f"Excel 配置文件解析完成,共加载 {len (device_config)} 个有效外设配置") + except FileNotFoundError: + logger.error (f"Excel 配置文件不存在:{file_path}") + print(f"Excel 配置文件不存在:{file_path}") + except Exception as e: + logger.error (f"Excel 配置文件解析失败:{str (e)}", exc_info=True) + print(f"Excel 配置文件解析失败:{str (e)}", exc_info=True) + return device_config +#测试代码(单独运行该文件时执行) +# if name == "main": +# test_config = load_excel_config("../../config_files/device_config.xlsx") +# print(test_config) + +# 测试代码(单独运行该文件时执行) +if __name__ == "__main__": + test_config = load_csv_config("../../config_files/device_config.csv") # 替换为实际文件路径 + print(test_config) \ No newline at end of file diff --git a/midware/config/excel_config.py b/midware/config/excel_config.py new file mode 100644 index 0000000..a001e26 --- /dev/null +++ b/midware/config/excel_config.py @@ -0,0 +1,155 @@ +# -*- coding: utf-8 -*- +''' +Excel 配置解析(excel_config.py):基于openpyxl实现 Excel 文件解析,提取外设名称、UDP 端口、协议类型、基地址等信息,存入DEVICE_CONFIG_DICT。 +''' +import openpyxl +from loguru import logger +import re +import pandas as pd +import os +import sys +import io + +# 设置标准输出和标准错误的编码为UTF-8 +# sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8') +# sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8') + +def hex_to_int (hex_str): + """处理十六进制字符串转整数(兼容带逗号 / 空格的格式,如 0x2080,0000、0x0000 1234) + :param hex_str: 十六进制字符串 + :return: 转换后的整数,失败返回 0 + """ + if not isinstance (hex_str, str): + return 0 # 非字符串直接返回0 + # 移除逗号、空格等非十六进制字符 + clean_hex = re.sub (r'[,\s]', '', hex_str.strip ()) + try: + return int (clean_hex, 16) if clean_hex else 0 + except (ValueError, TypeError): + logger.warning (f"十六进制转换失败:{hex_str},默认返回 0") + return 0 + +def load_csv_config (file_path): + """加载 Excel/CSV 外设配置文件,解析核心配置项 + :param file_path: Excel/CSV文件路径(xlsx/xls/csv) + :return: 外设配置字典,key = 外设名称,value = 配置项字典 + """ + device_config = {} + wb = None + temp_excel = "file.xlsx" + is_temp_file = False # 标记是否生成了临时文件 + + try: + # 处理CSV文件:转换为Excel后解析 + if file_path.endswith('.csv'): + df = pd.read_csv(file_path) + df.to_excel(temp_excel, index=False) + file_path = temp_excel + is_temp_file = True # 标记临时文件已生成 + + # 打开工作簿(兼容直接传入Excel文件的情况) + wb = openpyxl.load_workbook(file_path, data_only=True) + ws = wb.active + logger.info(f"成功打开配置文件:{file_path},工作表:{ws.title}") + + # 获取表头行,匹配核心配置列(兼容列名大小写 / 空格) + header = [cell.value.strip() if cell.value else "" for cell in ws[1]] + col_mapping = { + "名称": header.index([h for h in header if h.lower() == "名称"][0]) if [h for h in header if h.lower() == "名称"] else -1, + "编号": header.index([h for h in header if h.lower() == "编号"][0]) if [h for h in header if h.lower() == "编号"] else -1, + "协议类型": header.index([h for h in header if h.lower() in ["协议", "协议类型"]][0]) if [h for h in header if h.lower() in ["协议", "协议类型"]] else -1, + "内存基地址": header.index([h for h in header if h.lower() in ["内存基地址", "基地址"]][0]) if [h for h in header if h.lower() in ["内存基地址", "基地址"]] else -1, + "内存偏移地址": header.index([h for h in header if h.lower() in ["内存偏移地址", "偏移地址"]][0]) if [h for h in header if h.lower() in ["内存偏移地址", "偏移地址"]] else -1, + "UDP端口": header.index([h for h in header if h.lower() in ["UDP端口", "端口地址", "udp 地址", "UDP port","port"]][0]) if [h for h in header if h.lower() in ["UDP端口", "端口地址", "udp 地址", "UDP port","port"]] else -1, + "UDP远程端口": header.index([h for h in header if h.lower() in ["UDP远程端口", "远程端口地址", "远程udp 地址", "remote_port","remote port"]][0]) if [h for h in header if h.lower() in ["UDP端口", "端口地址", "udp 地址", "remote_port","remote port"]] else -1, + "TCP端口": header.index([h for h in header if h.lower() in ["TCP端口", "端口地址", "TCP 地址", "tcp_local_port"]][0]) if [h for h in header if h.lower() in ["TCP端口", "tcp_local_port"]] else -1, + "TCP总线端口": header.index([h for h in header if h.lower() in ["TCP远程端口", "远程端口地址", "远程tcp 地址", "tcp_bus_port"]][0]) if [h for h in header if h.lower() in ["TCP端口", "tcp_bus_port"]] else -1, + "说明": header.index([h for h in header if h.lower() == "说明"][0]) if [h for h in header if h.lower() == "说明"] else -1, + "发送缓存区大小": header.index([h for h in header if h.lower() in ["发送缓存区大小", "发送缓存"]][0]) if [h for h in header if h.lower() in ["发送缓存区大小", "发送缓存"]] else -1, + "接收缓存区大小": header.index([h for h in header if h.lower() in ["接收缓存区大小", "接收缓存"]][0]) if [h for h in header if h.lower() in ["接收缓存区大小", "接收缓存"]] else -1, + } + + # 校验核心列(名称、协议、基地址必须存在) + required_cols = ["名称", "协议类型", "内存基地址"] + missing_cols = [col for col in required_cols if col_mapping[col] == -1] + if missing_cols: + logger.error(f"配置文件缺少核心列:{','.join(missing_cols)},请检查表头") + return device_config + + # 关键优化:将迭代器转为列表,提前加载所有行数据(避免后续关闭wb后访问) + rows_data = list(ws.iter_rows(min_row=2, values_only=True)) + + # 遍历数据行(从第 2 行开始) + for row_num, row in enumerate(rows_data, start=2): + if not row[col_mapping["名称"]]: # 外设名称为空则跳过 + logger.warning(f"第 {row_num} 行:外设名称为空,跳过该行") + continue + + dev_name = row[col_mapping["名称"]].strip() + print(f'原始值:{row[col_mapping["编号"]]}') + print(f'类型:{type(row[col_mapping["编号"]])}') + print(str(row[col_mapping["编号"]])) + str_value = str(row[col_mapping["编号"]]) + print(f'isdigit?:{str_value.isdigit()}') + # 解析单行配置,缺省项赋默认值 + dev_info = { + "protocol": row[col_mapping["协议类型"]].strip().upper() if row[col_mapping["协议类型"]] else "", + "number": int(row[col_mapping["编号"]]) if (row[col_mapping["编号"]] is not None and str(row[col_mapping["编号"]]).isdigit()) else 99,#2026/4/15 + "base_addr": hex_to_int(row[col_mapping["内存基地址"]]), + "offset_addr": hex_to_int(row[col_mapping["内存偏移地址"]]), + "udp_port": int(row[col_mapping["UDP端口"]]) if (row[col_mapping["UDP端口"]] and str(row[col_mapping["UDP端口"]]).isdigit()) else 8888, + "remote_port": int(row[col_mapping["UDP远程端口"]]) if (row[col_mapping["UDP远程端口"]] and str(row[col_mapping["UDP远程端口"]]).isdigit()) else 9999, + "tcp_local_port": int(row[col_mapping["TCP端口"]]) if (row[col_mapping["TCP端口"]] and str(row[col_mapping["TCP端口"]]).isdigit()) else 1, + "tcp_bus_port": int(row[col_mapping["TCP总线端口"]]) if (row[col_mapping["TCP总线端口"]] and str(row[col_mapping["TCP总线端口"]]).isdigit()) else 1, + "desc": row[col_mapping["说明"]].strip() if row[col_mapping["说明"]] else "无", + "send_cache": int(row[col_mapping["发送缓存区大小"]]) if (row[col_mapping["发送缓存区大小"]] and str(row[col_mapping["发送缓存区大小"]]).isdigit()) else 512, + "recv_cache": int(row[col_mapping["接收缓存区大小"]]) if (row[col_mapping["接收缓存区大小"]] and str(row[col_mapping["接收缓存区大小"]]).isdigit()) else 512, + "enable": True # 默认启用该外设 + } + + # 校验协议类型合法性 + if dev_info["protocol"] not in ["UART", "CAN", "1553B", "AD", "OC"]: + logger.warning(f"第 {row_num} 行 [{dev_name}]:协议类型 {dev_info['protocol']} 不合法,仅支持 UART/CAN/1553B/AD/OC,跳过该行") + continue + + device_config[dev_name] = dev_info + logger.debug( + f"解析外设配置:{dev_name} | {dev_info['protocol']} | 基地址 {hex(dev_info['base_addr'])} | " + f"端口号 {dev_info['udp_port']} | 发送缓存 {dev_info['send_cache']} | 接收缓存 {dev_info['recv_cache']} | 说明 {dev_info['desc']}" + ) + + logger.info(f"配置文件解析完成,共加载 {len(device_config)} 个有效外设配置") + print(f"配置文件解析完成,共加载 {len(device_config)} 个有效外设配置") + + except FileNotFoundError: + logger.error(f"配置文件不存在:{file_path}") + print(f"配置文件不存在:{file_path}") + except Exception as e: + logger.error(f"配置文件解析失败:{str(e)}", exc_info=True) + print(f"配置文件解析失败:{str(e)}") + finally: + # 第一步:先关闭工作簿(确保所有数据已读取完成) + if wb: + try: + wb.close() + logger.debug("Excel工作簿已正常关闭") + except Exception as e: + logger.warning(f"关闭Excel工作簿失败:{str(e)}") + + # 第二步:删除临时文件(仅当生成了临时文件时执行) + if is_temp_file and os.path.exists(temp_excel): + try: + # 延迟删除(避免文件句柄未释放) + import time + time.sleep(0.1) + os.remove(temp_excel) + logger.debug("临时Excel文件已删除") + except Exception as e: + logger.warning(f"删除临时Excel文件失败:{str(e)}") + + return device_config + +# 测试代码(单独运行该文件时执行) +if __name__ == "__main__": + test_config = load_csv_config("config_files/device_config.csv") + print(test_config) \ No newline at end of file diff --git a/midware/core/__init__.py b/midware/core/__init__.py new file mode 100644 index 0000000..ea96fbc --- /dev/null +++ b/midware/core/__init__.py @@ -0,0 +1,23 @@ +# midware/core/__init__.py +""" +核心业务层:总线-UDP解耦、总线驱动、数据处理 +""" +# from .bus_udp_decoupler import ( +# start_all_threads, +# stop_all_threads, +# protocol_dispatch_queue, +# bus_queues, +# udp_send_queue +# ) + +from .bus_udp_decoupler import ( + start_all_threads, + stop_all_threads, + bus_recv_queues, + bus_send_queues, + udp_send_queue, + udp_recv_dispatch_queue +) +# 暴露版本/作者等元信息(可选) +__version__ = "1.0.0" +__author__ = "xxx" diff --git a/midware/core/__pycache__/__init__.cpython-311.pyc b/midware/core/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000..9133808 Binary files /dev/null and b/midware/core/__pycache__/__init__.cpython-311.pyc differ diff --git a/midware/core/__pycache__/bus_udp_decoupler.cpython-311.pyc b/midware/core/__pycache__/bus_udp_decoupler.cpython-311.pyc new file mode 100644 index 0000000..f9c438d Binary files /dev/null and b/midware/core/__pycache__/bus_udp_decoupler.cpython-311.pyc differ diff --git a/midware/core/__pycache__/bus_udp_decoupler_20260306.cpython-311.pyc b/midware/core/__pycache__/bus_udp_decoupler_20260306.cpython-311.pyc new file mode 100644 index 0000000..2f23cd4 Binary files /dev/null and b/midware/core/__pycache__/bus_udp_decoupler_20260306.cpython-311.pyc differ diff --git a/midware/core/__pycache__/data_forward.cpython-311.pyc b/midware/core/__pycache__/data_forward.cpython-311.pyc new file mode 100644 index 0000000..4234555 Binary files /dev/null and b/midware/core/__pycache__/data_forward.cpython-311.pyc differ diff --git a/midware/core/__pycache__/error_check.cpython-311.pyc b/midware/core/__pycache__/error_check.cpython-311.pyc new file mode 100644 index 0000000..cae4fd3 Binary files /dev/null and b/midware/core/__pycache__/error_check.cpython-311.pyc differ diff --git a/midware/core/bus_drivers/1553b_driver.py b/midware/core/bus_drivers/1553b_driver.py new file mode 100644 index 0000000..e69de29 diff --git a/midware/core/bus_drivers/__init__.py b/midware/core/bus_drivers/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/midware/core/bus_drivers/ad_driver.py b/midware/core/bus_drivers/ad_driver.py new file mode 100644 index 0000000..e69de29 diff --git a/midware/core/bus_drivers/base_driver.py b/midware/core/bus_drivers/base_driver.py new file mode 100644 index 0000000..c096bed --- /dev/null +++ b/midware/core/bus_drivers/base_driver.py @@ -0,0 +1,78 @@ +# midware/drivers/base_driver.py + +""" +Tan mingyan +2026/3/9 + 驱动基类(base_driver.py)—— 定义标准接口(核心) +所有总线驱动继承该基类,保证接口统一,解耦 bus_udp_decoupler.py 与具体驱动实现: +""" +from abc import ABC, abstractmethod +from typing import Optional, Dict, Any +from loguru import logger + +class BaseBusDriver(ABC): + """ + 总线驱动基类:定义所有总线必须实现的标准接口 + 遵循「开闭原则」:新增总线只需继承该类实现接口,无需修改现有代码 + """ + def __init__(self, config: Optional[Dict[str, Any]] = None): + """ + 初始化驱动 + :param config: 驱动配置(如波特率、端口、IP等) + """ + self.config = config or {} + self.is_connected = False # 驱动连接状态 + self._init_config() # 初始化配置 + + def _init_config(self): + """初始化默认配置(子类可重写)""" + logger.info(f"初始化 {self.__class__.__name__} 默认配置") + + @abstractmethod + def connect(self) -> bool: + """ + 连接总线(如打开串口、建立CAN通道、连接1553B板卡) + :return: 连接成功返回True,失败返回False + """ + pass + + @abstractmethod + def disconnect(self) -> bool: + """ + 断开总线连接 + :return: 断开成功返回True,失败返回False + """ + pass + + @abstractmethod + def send(self, data: bytes, **kwargs) -> bool: + """ + 发送数据到总线 + :param data: 待发送的原始字节数据 + :param kwargs: 扩展参数(如优先级、地址、帧ID等) + :return: 发送成功返回True,失败返回False + """ + pass + + @abstractmethod + def recv(self, timeout: float = 0.1, **kwargs) -> Optional[bytes]: + """ + 从总线接收数据 + :param timeout: 接收超时时间(秒) + :param kwargs: 扩展参数(如过滤条件、地址等) + :return: 接收到的字节数据,超时/失败返回None + """ + pass + + def check_status(self) -> bool: + """ + 检查驱动状态(默认实现,子类可重写) + :return: 驱动正常返回True,异常返回False + """ + return self.is_connected + + def __del__(self): + """析构函数:自动断开连接""" + if self.is_connected: + self.disconnect() + logger.info(f"{self.__class__.__name__} 自动断开连接") \ No newline at end of file diff --git a/midware/core/bus_drivers/can_driver.py b/midware/core/bus_drivers/can_driver.py new file mode 100644 index 0000000..e69de29 diff --git a/midware/core/bus_drivers/oc_driver.py b/midware/core/bus_drivers/oc_driver.py new file mode 100644 index 0000000..e69de29 diff --git a/midware/core/bus_drivers/uart_driver.py b/midware/core/bus_drivers/uart_driver.py new file mode 100644 index 0000000..e69de29 diff --git a/midware/core/bus_udp_decoupler.py b/midware/core/bus_udp_decoupler.py new file mode 100644 index 0000000..954ed9c --- /dev/null +++ b/midware/core/bus_udp_decoupler.py @@ -0,0 +1,637 @@ +# -*- coding: utf-8 -*- + +""" +Tan Mintgyan +2026/3/9 +总线-UDP双向通信解耦模块(新增带协议优先级:1553B>CAN>UART>AD>OC>网络) +""" +import threading +import queue +import time +from loguru import logger +from typing import Dict, Any, Optional + +from midware.config.base_config import DEVICE_CONFIG_DICT +from midware.drivers import create_bus_driver +import midware.config.base_config as base_config + +from ..drivers.renode_agent import renode + + +# ========== 1. 核心:定义协议优先级映射(数值越小优先级越高) ========== +PROTOCOL_PRIORITY = { + "1553B": 1, # 最高优先级 + "CAN": 2, + "UART": 3, + "AD": 4, + "OC": 5, + "网络": 6 # 最低优先级 +} +MAX_QUEUE_SIZE = 1000 # 队列最大长度(可按协议单独调整) + +# ========== 2. 收发队列分离(发送队列统一用PriorityQueue) ========== +# 总线接收队列(总线→中间件:上行数据,无需优先级) +bus_recv_queues = { + "1553B": queue.Queue(maxsize=MAX_QUEUE_SIZE), + "CAN": queue.Queue(maxsize=MAX_QUEUE_SIZE), + "UART": queue.Queue(maxsize=MAX_QUEUE_SIZE), + "AD": queue.Queue(maxsize=MAX_QUEUE_SIZE), + "OC": queue.Queue(maxsize=MAX_QUEUE_SIZE), + "网络": queue.Queue(maxsize=MAX_QUEUE_SIZE) +} + +# 总线发送队列(中间件→总线:下行数据,带优先级) +# 统一使用PriorityQueue,按PROTOCOL_PRIORITY的数值排序 +bus_send_queues = { + "1553B": queue.PriorityQueue(maxsize=MAX_QUEUE_SIZE), + "CAN": queue.PriorityQueue(maxsize=MAX_QUEUE_SIZE), + "UART": queue.PriorityQueue(maxsize=MAX_QUEUE_SIZE), + "AD": queue.PriorityQueue(maxsize=MAX_QUEUE_SIZE), + "OC": queue.PriorityQueue(maxsize=MAX_QUEUE_SIZE), + "网络": queue.PriorityQueue(maxsize=MAX_QUEUE_SIZE) +} + +# UDP发送队列(中间件→UDP:上行数据) +udp_send_queue = queue.Queue(maxsize=MAX_QUEUE_SIZE) +# UDP接收分发队列(UDP→中间件:下行数据) +udp_recv_dispatch_queue = queue.Queue(maxsize=MAX_QUEUE_SIZE) + +# ========== 3. UDP接收线程:自动映射协议优先级 ========== +def udp_recv_thread(udp_server): + """UDP接收线程:接收下行指令,自动分配优先级后放入总线发送队列""" + logger.info("UDP接收线程(下行指令)启动") + + ##测试用 + try: + logger.debug("测试队列入队") + print("测试队列入队") + protocol= 'AD' + dev_name = 'adc1',#2026/4/9 + #dev_name = '正电压采集热敏量采集', + raw_data = b'\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f'#56*2=112字节 + priority = 3 + bus_send_queues[protocol].put_nowait(( + priority, # 按数值从小到大优先 + { + "dev_name": dev_name, + "raw_data": raw_data, + "protocol": protocol, + "priority": priority, + "recv_time": time.time() # 记录接收时间 + } + )) + logger.debug( + f"下行指令入队|{protocol}(优先级{priority})|{dev_name}|" + f"队列长度:{bus_send_queues[protocol].qsize()}" + ) + except queue.Full: + logger.warning( + f"{protocol}(优先级{priority})发送队列已满,丢弃 {dev_name} 下行指令" + ) + + while True: + try: + recv_data = udp_server.recv_multi_udp_hex() + if recv_data: + for data in recv_data: + protocol = data.get("protocol", "").strip().upper() + dev_name = data.get("dev_name", "") + raw_data = data.get("raw_data", b"") + recv_time = time.time() + # 核心:自动获取协议优先级(未知协议设为最低优先级7) + priority = PROTOCOL_PRIORITY.get(protocol, 7) + + if protocol not in bus_send_queues: + logger.warning(f"未知协议 {protocol},丢弃下行指令:{dev_name}(优先级:{priority})") + continue + + # 放入总线发送队列(PriorityQueue格式:(优先级数值, 数据)) + try: + bus_send_queues[protocol].put_nowait(( + priority, # 按数值从小到大优先 + #recv_time,#新增,用来打破优先级相同的平局 + { + "dev_name": dev_name, + "raw_data": raw_data, + "protocol": protocol, + "priority": priority, + "recv_time": recv_time # 记录接收时间 + } + )) + logger.debug( + f"下行指令入队|{protocol}(优先级{priority})|{dev_name}|" + f"队列长度:{bus_send_queues[protocol].qsize()}" + ) + except queue.Full: + logger.warning( + f"{protocol}(优先级{priority})发送队列已满,丢弃 {dev_name} 下行指令" + ) + time.sleep(1e-6) + except Exception as e: + logger.error(f"UDP接收线程异常:{str(e)}", exc_info=True) + time.sleep(0.1) + +# ========== 4. 总线发送线程:按优先级消费数据 ========== +''' +def bus_send_thread(protocol: str, bus_driver: Optional[Any] = None):#2026/3/9停用 + """总线发送线程:优先消费高优先级数据(数值越小越先处理)""" + logger.info(f"{protocol} 总线发送线程启动(优先级:{PROTOCOL_PRIORITY.get(protocol, 7)})") + while True: + try: + # PriorityQueue自动按优先级取值(数值小的先出) + priority, send_data = bus_send_queues[protocol].get(timeout=1) + dev_name = send_data["dev_name"] + raw_data = send_data["raw_data"] + recv_time = send_data["recv_time"] + + # 超时检查(可选:超过5秒未发送则丢弃) + if time.time() - recv_time > 5: + logger.warning( + f"{protocol}(优先级{priority})|{dev_name} 指令超时(5秒),丢弃" + ) + bus_send_queues[protocol].task_done() + continue + + # 核心:总线发送逻辑(替换为真实驱动调用) + logger.info( + f"总线发送|{protocol}(优先级{priority})|{dev_name}|" + f"数据:{raw_data.hex()}|队列剩余:{bus_send_queues[protocol].qsize()}" + ) + # if bus_driver: + # bus_driver.send(raw_data) # 调用总线驱动发送 + + # 标记任务完成 + bus_send_queues[protocol].task_done() + except queue.Empty: + continue + except Exception as e: + logger.error(f"{protocol} 发送线程异常:{str(e)}", exc_info=True) + time.sleep(0.1) +''' + +# ========== 修正:总线发送线程(参数改为接收驱动配置) ========== +def bus_send_thread(protocol: str, bus_driver_config: Optional[Dict[str, Any]] = None): + """ + 总线发送线程:通过统一驱动接口发送数据 + :param protocol: 协议名称 + :param bus_driver_config: 驱动配置字典 + """ + logger.info(f"{protocol} 总线发送线程启动(优先级:{PROTOCOL_PRIORITY.get(protocol, 7)})") + + # 创建驱动实例 + try: + driver = create_bus_driver(protocol, config=bus_driver_config) + # 连接总线 + if not driver.connect(): + logger.error(f"{protocol} 驱动连接失败,线程退出") + return + except Exception as e: + logger.error(f"{protocol} 驱动创建失败:{str(e)},线程退出") + return + + while True: + try: + priority, send_data = bus_send_queues[protocol].get(timeout=1) + dev_name = send_data["dev_name"] + raw_data = send_data["raw_data"] + recv_time = send_data["recv_time"] + + '''# 超时检查 + if time.time() - recv_time > 5: + logger.warning(f"{protocol}(优先级{priority})|{dev_name} 指令超时,丢弃") + bus_send_queues[protocol].task_done() + continue + ''' + # 调用驱动发送(统一接口) + send_success = driver.send( + data=raw_data, + priority=priority, # 传递优先级参数 + dev_name=dev_name # 传递设备名 + ) + + if send_success: + logger.info( + f"{protocol}(优先级{priority})|{dev_name} 发送成功|" + f"数据:{raw_data.hex()}|队列剩余:{bus_send_queues[protocol].qsize()}" + ) + else: + logger.error(f"{protocol}(优先级{priority})|{dev_name} 发送失败") + + bus_send_queues[protocol].task_done() + except queue.Empty: + continue + except Exception as e: + logger.error(f"{protocol} 发送线程异常:{str(e)}", exc_info=True) + time.sleep(0.1) + # 线程退出时断开驱动(析构函数也会自动处理) + driver.disconnect() + +# ========== 修正:start_all_threads 函数(适配 bus_driver_configs 参数) ========== +def start_all_threads(udp_server, bus_driver_configs: Optional[Dict[str, Any]] = None): + """ + 启动所有线程 + :param udp_server: UDP服务实例 + :param bus_driver_configs: 驱动配置字典,格式:{"1553B": {...}, "CAN": {...}, ...} + """ + threads = [] + bus_driver_configs = bus_driver_configs or {} # 无配置时设为空字典 + + # 按优先级从高到低排序协议 + sorted_protos = sorted(PROTOCOL_PRIORITY.keys(), key=lambda x: PROTOCOL_PRIORITY[x]) + + # 1. UDP接收线程 + udp_recv_t = threading.Thread(target=udp_recv_thread, args=(udp_server,), daemon=True) + threads.append(udp_recv_t) + + # 2. 总线发送线程(按优先级排序启动,传递对应驱动配置) + for protocol in sorted_protos: + # 获取当前协议的驱动配置 + driver_config = bus_driver_configs.get(protocol, {}) + send_t = threading.Thread( + target=bus_send_thread, + args=(protocol, driver_config), # 传递协议名+驱动配置 + daemon=True + ) + threads.append(send_t) + ''' + # 3. 总线接收线程(按优先级排序启动,传递对应驱动配置) + for protocol in sorted_protos: + driver_config = bus_driver_configs.get(protocol, {}) + recv_t = threading.Thread( + target=bus_recv_thread, + args=(protocol, driver_config), # 传递协议名+驱动配置 + daemon=True + ) + threads.append(recv_t) + ''' + + # 4. 总线接收分发线程 + recv_dispatch_t = threading.Thread(target=bus_recv_dispatch_thread, daemon=True) + threads.append(recv_dispatch_t) + + # 5. UDP发送线程 + udp_send_t = threading.Thread(target=udp_send_thread, args=(udp_server,), daemon=True) + threads.append(udp_send_t) + + #6. 总线轮询线程 + for protocol in sorted_protos: + driver_config = bus_driver_configs.get(protocol, {}) + daq_t = threading.Thread( + target=bus_daq_thread, + args=(protocol, driver_config), # 传递协议名+驱动配置 + daemon=True + ) + threads.append(daq_t) + + # 启动所有线程 + for t in threads: + t.start() + logger.info(f"线程 {t.name} 启动成功") + return threads + +# ========== 其他函数(stop_all_threads、udp_recv_thread等)保持不变 ========== + + +# ========== 5. 总线接收线程(无优先级,保持原有逻辑) ========== +# ========== 修正:总线接收线程(参数改为接收驱动配置) ========== +def bus_recv_thread(protocol: str, bus_driver_config: Optional[Dict[str, Any]] = None): + """总线接收线程:通过统一驱动接口接收数据""" + logger.info(f"{protocol} 总线接收线程启动") + + # 创建驱动实例 + try: + driver = create_bus_driver(protocol, config=bus_driver_config) + if not driver.connect(): + logger.error(f"{protocol} 驱动连接失败,线程退出") + return + except Exception as e: + logger.error(f"{protocol} 驱动创建失败:{str(e)},线程退出") + return + + time.sleep(0.001)#5->0.5 + while True: + try: + ''' + ##循环发送读取请求 + # if(protocol=="UART"): + # sysbus_cmd = f'uart24 GetTXFIFODataString' + # print(sysbus_cmd) + # response = renode.send_sync(sysbus_cmd)#2026/4/10 + time.sleep(0.001) + + ##循环发送读取请求 + if(protocol == "UART"):#发现protocol == "CAN"时候循环更快 + sysbus_cmd = f'uart0 GetTXFIFODataString'#uart 0 + logger.info(f'uart0轮询请求:{sysbus_cmd}')#print(sysbus_cmd) + response = renode.send_sync(sysbus_cmd,timeout_ms=10)#2026/4/10 + time.sleep(0.001) + + if(protocol == "CAN"): + sysbus_cmd = f'can_a GetTxBufferDataString'#CAN_A + #sysbus_cmd = f'uart0 GetTXFIFODataString'#uart 0 + logger.info(f'can_a轮询请求:{sysbus_cmd}')#print(sysbus_cmd) + response = renode.send_sync(sysbus_cmd,timeout_ms=10)#2026/4/10 + time.sleep(0.25) + ''' + # 调用驱动接收(统一接口) + # raw_data = driver.recv(timeout=0.001) # 短超时,非阻塞 + ## 以下代码没有作用,因为recv接口没有实现 + ''' + if raw_data and len(raw_data) > 0: + dev_name = f"{protocol}_DEV" + try: + bus_recv_queues[protocol].put_nowait({ + "dev_name": dev_name, + "raw_data": raw_data, + "protocol": protocol, + "recv_time": time.time() + }) + logger.debug(f"{protocol} 接收:{dev_name} 上行数据,队列长度:{bus_recv_queues[protocol].qsize()}") + except queue.Full: + logger.warning(f"{protocol} 接收队列已满,丢弃上行数据") + time.sleep(1e-6) + ''' + except Exception as e: + logger.error(f"{protocol} 接收线程异常:{str(e)}", exc_info=True) + time.sleep(0.1) + driver.disconnect() +''' +def bus_recv_thread(protocol: str, bus_driver: Optional[Any] = None):#2026/3/9 停用 + """总线接收线程:接收上行数据,放入接收队列""" + logger.info(f"{protocol} 总线接收线程启动") + while True: + try: + # 模拟总线接收(替换为真实驱动的recv接口) + # raw_data = bus_driver.recv() + raw_data = b"" # 占位 + + if raw_data: + dev_name = f"{protocol}_DEV" + try: + bus_recv_queues[protocol].put_nowait({ + "dev_name": dev_name, + "raw_data": raw_data, + "protocol": protocol, + "recv_time": time.time() + }) + logger.debug( + f"总线接收|{protocol}|{dev_name}|" + f"队列长度:{bus_recv_queues[protocol].qsize()}" + ) + except queue.Full: + logger.warning(f"{protocol} 接收队列已满,丢弃上行数据") + time.sleep(1e-6) + except Exception as e: + logger.error(f"{protocol} 接收线程异常:{str(e)}", exc_info=True) + time.sleep(0.1) +''' + +# ========== 5.5 总线轮询线程(无优先级,保持原有逻辑) ========== +def bus_daq_thread(protocol: str, bus_driver_config: Optional[Dict[str, Any]] = None): + """总线接收线程:通过统一驱动接口接收数据""" + logger.info(f"{protocol} 总线接收线程启动") + + # 创建驱动实例 + try: + driver = create_bus_driver(protocol, config=bus_driver_config) + if not driver.connect(): + logger.error(f"{protocol} 驱动连接失败,线程退出") + return + except Exception as e: + logger.error(f"{protocol} 驱动创建失败:{str(e)},线程退出") + return + + time.sleep(0.001)#5->0.5 + while True: + try: + ##循环发送读取请求 + # if(protocol=="UART"): + # sysbus_cmd = f'uart24 GetTXFIFODataString' + # print(sysbus_cmd) + # response = renode.send_sync(sysbus_cmd)#2026/4/10 + time.sleep(0.001) + + ##循环发送读取请求 + if(protocol == "CAN"):#发现protocol == "CAN"时候循环更快 + sysbus_cmd = f'uart0 GetTXFIFODataString'#uart 0 + logger.info(f'uart0轮询请求:{sysbus_cmd}')#print(sysbus_cmd) + response = renode.send_sync(sysbus_cmd,timeout_ms=5)#2026/4/10 + # response = renode.enqueue_cmd(sysbus_cmd)#2026/4/10 + time.sleep(0.001) + + sysbus_cmd = f'uart18 GetTXFIFODataString'#uart 0 + logger.info(f'uart18 轮询请求:{sysbus_cmd}')#print(sysbus_cmd) + response = renode.send_sync(sysbus_cmd,timeout_ms=5)#2026/4/10 + # response = renode.enqueue_cmd(sysbus_cmd)#2026/4/10 + time.sleep(0.001) + + + if(protocol == "CAN"): + sysbus_cmd = f'can_a GetTxBufferDataString'#CAN_A + #sysbus_cmd = f'uart0 GetTXFIFODataString'#uart 0 + logger.info(f'can_a轮询请求:{sysbus_cmd}')#print(sysbus_cmd) + + response = renode.send_sync(sysbus_cmd,timeout_ms=5)#2026/4/10 + #response = renode.enqueue_cmd(sysbus_cmd)#2026/4/10 + time.sleep(0.25) + + # 调用驱动接收(统一接口) + raw_data = driver.recv(timeout=0.001) # 短超时,非阻塞 + + except Exception as e: + logger.error(f"{protocol} 接收线程异常:{str(e)}", exc_info=True) + time.sleep(0.1) + + driver.disconnect() + +import time +''' +# ========== 5.5 总线轮询线程(无优先级,保持原有逻辑) ========== +def bus_daq_thread(protocol: str, bus_driver_config: Optional[Dict[str, Any]] = None): + logger.info(f"{protocol}总线轮询线程启动") + # 创建驱动实例 + try: + driver = create_bus_driver(protocol, config=bus_driver_config) + if not driver.connect(): + logger.error(f"{protocol} 驱动连接失败,线程退出") + return + except Exception as e: + logger.error(f"{protocol} 驱动创建失败:{str(e)},线程退出") + return + time.sleep(5) + # ==================原来的while循环删除==============# + poll_period = 0.25 + next_run_time = time.perf_counter() + while True: + try: + if protocol == "UART": + sysbus_cmd = f'uart0 GetTXFIFODataString'#uart 0 + logger.info(f'uart0轮询请求:{sysbus_cmd}')#print(sysbus_cmd) + response = renode.send_sync(sysbus_cmd,timeout_ms=1)#2026/4/10 + elif protocol =="CAN": + sysbus_cmd = f'can_a GetTxBufferDataString'#CAN_A + logger.info(f'can_a轮询请求:{sysbus_cmd}')#print(sysbus_cmd) + response = renode.send_sync(sysbus_cmd,timeout_ms=1)#2026/4/10 + next_run_time = poll_period + next_run_time + sleep_time = next_run_time- time.perf_counter() + if sleep_time >0: + time.sleep(sleep_time) + #如果小于0,代表超时, + except Exception as e: + logger.error(f"{protocol} 接收线程异常:{str(e)}", exc_info=True) + time.sleep(0.01) +''' + + + +# ========== 6. 总线接收分发线程(无优先级,保持原有逻辑) ========== +def bus_recv_dispatch_thread(): + """汇总总线接收数据,转发到UDP发送队列""" + logger.info("总线接收分发线程启动") + while True: + try: + for protocol, q in bus_recv_queues.items(): + try: + recv_data = q.get_nowait() + try: + udp_send_queue.put_nowait(recv_data) + logger.debug( + f"上行数据转发到udp_send_queue|{protocol}|{recv_data['dev_name']}|{recv_data['raw_data'].hex()}" + f"UDP队列长度:{udp_send_queue.qsize()}" + ) + except queue.Full: + logger.warning(f"UDP发送队列已满,丢弃 {protocol} 上行数据") + q.task_done() + except queue.Empty: + continue + time.sleep(1e-6) + except Exception as e: + logger.error(f"总线接收分发线程异常:{str(e)}", exc_info=True) + time.sleep(0.1) + + +''' +# ========== 7. UDP发送线程(无优先级,保持原有逻辑) ========== +def udp_send_thread(udp_server): #2026/4/15停用,因为本函数智能输出一个UDP端口 + """UDP发送线程:发送上行数据到故障注入平台""" + logger.info("UDP发送线程(上行数据)启动") + while True: + try: + send_data = udp_send_queue.get(timeout=1) + dev_name = send_data["dev_name"] + raw_data = send_data["raw_data"] + + udp_server.send_to_fault(raw_data, dev_name) + logger.debug( + f"UDP发送|{dev_name}|数据:{raw_data.hex()}|" + f"队列剩余:{udp_send_queue.qsize()}" + ) + udp_send_queue.task_done() + except queue.Empty: + continue + except Exception as e: + logger.error(f"UDP发送线程异常:{str(e)}", exc_info=True) + time.sleep(0.1) +''' +# ========== 7. UDP发送线程(已修改:按设备send_port动态发送) ==========2026/4/15 +def udp_send_thread(udp_server): + """UDP发送线程:按设备配置的send_port发送到故障注入平台""" + logger.info("UDP发送线程(上行数据)启动") + + while True: + try: + send_data = udp_send_queue.get(timeout=1) + dev_name = send_data["dev_name"] + raw_data = send_data["raw_data"] + protocol = send_data["protocol"] + + # ====================== + # 🔥 关键:从设备配置获取远端端口 send_port + # ====================== + send_port = 9999 # 默认端口 + if dev_name in base_config.DEVICE_CONFIG_DICT: ##引用全局变量时候有问题,这里需要修改 + dev_cfg = base_config.DEVICE_CONFIG_DICT[dev_name] + send_port = dev_cfg.get("remote_port", 9999) # 读取CSV里的 remote_port + + # ====================== + # 🔥 发送到指定远端端口(修改这里) + # ====================== + udp_server.send_to_fault(raw_data, dev_name, send_port) + + logger.info( + f"通过UDP向动力学或前端发送|{dev_name}|port:{send_port}|数据:{raw_data.hex()}" #防止刷屏,先注释 + ) + udp_send_queue.task_done() + + except queue.Empty: + continue + except Exception as e: + logger.error(f"UDP发送线程异常:{str(e)}", exc_info=True) + time.sleep(0.1) +''' +# ========== 8. 线程管理(启动/停止) ========== +def start_all_threads(udp_server, bus_drivers: Dict[str, Any] = None):#2026/3/9停用 + """启动所有线程(按优先级顺序启动,非必须,仅为日志清晰)""" + threads = [] + bus_drivers = bus_drivers or {} + + # 按优先级从高到低启动总线发送线程(日志更清晰) + sorted_protos = sorted(PROTOCOL_PRIORITY.keys(), key=lambda x: PROTOCOL_PRIORITY[x]) + + # 1. UDP接收线程 + udp_recv_t = threading.Thread(target=udp_recv_thread, args=(udp_server,), daemon=True) + threads.append(udp_recv_t) + + # 2. 总线发送线程(按优先级排序启动) + for protocol in sorted_protos: + driver = bus_drivers.get(protocol) + send_t = threading.Thread(target=bus_send_thread, args=(protocol, driver), daemon=True) + threads.append(send_t) + + # 3. 总线接收线程(按优先级排序启动) + for protocol in sorted_protos: + driver = bus_drivers.get(protocol) + recv_t = threading.Thread(target=bus_recv_thread, args=(protocol, driver), daemon=True) + threads.append(recv_t) + + # 4. 总线接收分发线程 + recv_dispatch_t = threading.Thread(target=bus_recv_dispatch_thread, daemon=True) + threads.append(recv_dispatch_t) + + # 5. UDP发送线程 + udp_send_t = threading.Thread(target=udp_send_thread, args=(udp_server,), daemon=True) + threads.append(udp_send_t) + + # 启动所有线程 + for t in threads: + t.start() + logger.info(f"线程 {t.name} 启动成功") + return threads +''' +def stop_all_threads(): + """优雅停止所有线程""" + logger.info("开始停止所有线程,等待队列处理完成...") + # 等待发送队列(按优先级从高到低) + sorted_protos = sorted(PROTOCOL_PRIORITY.keys(), key=lambda x: PROTOCOL_PRIORITY[x]) + for protocol in sorted_protos: + bus_send_queues[protocol].join() + # 等待接收队列 + for protocol in sorted_protos: + bus_recv_queues[protocol].join() + # 等待UDP发送队列 + udp_send_queue.join() + logger.info("所有队列处理完成,线程已停止") + +# ========== 扩展函数:动态调整优先级(可选) ========== +def update_protocol_priority(protocol: str, new_priority: int): + """ + 动态调整协议优先级(运行时生效) + :param protocol: 协议名称(如"1553B") + :param new_priority: 新优先级数值(越小越高) + """ + if protocol in PROTOCOL_PRIORITY: + old_priority = PROTOCOL_PRIORITY[protocol] + PROTOCOL_PRIORITY[protocol] = new_priority + logger.info(f"协议 {protocol} 优先级调整:{old_priority} → {new_priority}") + else: + logger.warning(f"未知协议 {protocol},无法调整优先级") \ No newline at end of file diff --git a/midware/core/data_forward.py b/midware/core/data_forward.py new file mode 100644 index 0000000..e69de29 diff --git a/midware/core/error_check.py b/midware/core/error_check.py new file mode 100644 index 0000000..8a5ece0 --- /dev/null +++ b/midware/core/error_check.py @@ -0,0 +1,97 @@ +import crcmod +from loguru import logger +import os + +#初始化校验和算法,适配不同总线协议(通用 CRC16 为主,兼容自定义校验) +#预定义各总线校验算法,可根据需求扩展 +#CRC16_UART = crcmod.predefined.Crc('crc16_modbus') +CRC16_UART = crcmod.predefined.Crc('crc16') +#CRC16_CAN = crcmod.predefined.Crc('crc16/ccitt') +CRC16_CAN = crcmod.predefined.Crc('crc16') +#CRC16_1553B = crcmod.predefined.Crc('crc16_x25') +CRC16_1553B = crcmod.predefined.Crc('crc16') + + +#确保日志目录存在 +if not os.path.exists("logs"):os.makedirs("logs") +def _calculate_checksum (data: bytes, protocol: str) -> bytes: + """私有方法:根据协议类型计算校验和 + :param data: 原始二进制数据 + :param protocol: 总线协议(UART/CAN/1553B/AD/OC) + :return: 校验和字节流(2 字节,大端) + """ + if not isinstance (data, bytes) or len (data) == 0: + return b'\x00\x00' + crc = None + if protocol == "UART": + crc = CRC16_UART + elif protocol == "CAN": + crc = CRC16_CAN + elif protocol == "1553B": + crc = CRC16_1553B + elif protocol in ["AD", "OC"]: + #AD/OC 采用简单累加和校验(低字节),适配星务平台常用规则 + sum_val = sum(byte for byte in data) & 0xFF + return bytes([sum_val]) + else: + #未知协议默认使用 CRC16_MODBUS + crc = CRC16_UART + #重置 CRC 计算器并计算 + #crc.reset() + crc.update(data) + + #返回 2 字节大端校验和 + return crc.crcValue.to_bytes(2, byteorder='big') + +def check_checksum (data: bytes, protocol: str, device_name: str) -> bool: + """核心校验和检测逻辑:分离数据与校验位,对比计算值与传入值规则:数据末尾携带校验位,不同协议校验位长度不同(UART/CAN/1553B 为 2 字节,AD/OC 为 1 字节) + :param data: 包含校验位的原始二进制数据 + :param protocol: 总线协议(UART/CAN/1553B/AD/OC) + :param device_name: 外设名称(用于日志定位) + :return: 校验通过返回 True,失败返回 False 并记录错误日志 + """ + #基础参数校验 + if not isinstance (data, bytes) or len (data) == 0: + logger.error (f"【{device_name}-{protocol}】校验和检测失败:输入数据为空或非二进制格式") + return False + if protocol not in ["UART", "CAN", "1553B", "AD", "OC"]: + logger.error (f"【{device_name}-{protocol}】校验和检测失败:不支持的协议类型") + return False + #定义各协议校验位长度 + checksum_len = 2 if protocol in ["UART", "CAN", "1553B"] else 1 + #数据长度需大于校验位长度,否则无效 + if len (data) <= checksum_len: + logger.error (f"【{device_name}-{protocol}】校验和检测失败:数据长度 {len (data)} 字节,小于校验位长度 {checksum_len} 字节") + return False + #分离原始数据和携带的校验位 + raw_data = data[:-checksum_len] + received_checksum = data[-checksum_len:] + #计算原始数据的校验和 + calculated_checksum = _calculate_checksum(raw_data, protocol) + #对比校验和 + if received_checksum == calculated_checksum: + logger.debug (f"【{device_name}-{protocol}】校验和检测通过:接收校验位 {received_checksum.hex ()},计算校验位 {calculated_checksum.hex ()}") + return True + else: + #校验失败,记录详细错误日志(含设备、协议、数据、校验位对比) + error_msg = (f"【{device_name}-{protocol}】校验和检测失败 |"f"原始数据长度:{len (raw_data)} 字节 |"f"接收校验位:{received_checksum.hex ()} |"f"计算校验位:{calculated_checksum.hex ()} |"f"原始数据(前 16 字节):{raw_data [:16].hex () if len (raw_data)>=16 else raw_data.hex ()}") + logger.error (error_msg) + return False + +def verify_frame_integrity (data: bytes, protocol: str, device_name: str) -> bool: + """扩展方法:帧完整性检测(可选),结合校验和 + 协议帧长度规则用于 CAN(单帧≤11 字节)、1553B 等有帧长限制的协议 + :param data: 包含校验位的原始二进制数据 + :return: 完整性通过返回 True,否则 False""" + #先执行校验和检测 + if not check_checksum(data, protocol, device_name): + return False + #各协议帧长度规则校验 + frame_rules = {"CAN": lambda d: len (d) - 2 <= 11, # CAN 单帧≤11 字节(扣除 2 字节校验位)"1553B": lambda d: len (d) % 2 == 0, # 1553B 数据为 2 字节整数倍(军用总线规范)"UART": lambda d: True, # UART 不定长,无帧长限制"AD": lambda d: True, # AD 无帧长限制"OC": lambda d: True # OC 无帧长限制 + } + if protocol in frame_rules: + if not frame_rulesprotocol: + error_msg = f"【{device_name}-{protocol}】帧完整性检测失败:数据长度不符合协议规范,数据总长度 {len (data)} 字节" + logger.error (error_msg) + return False + logger.debug (f"【{device_name}-{protocol}】帧完整性检测通过") + return True diff --git a/midware/core/history/__pycache__/bus_udp_decoupler_20260306.cpython-311.pyc b/midware/core/history/__pycache__/bus_udp_decoupler_20260306.cpython-311.pyc new file mode 100644 index 0000000..3d0fb41 Binary files /dev/null and b/midware/core/history/__pycache__/bus_udp_decoupler_20260306.cpython-311.pyc differ diff --git a/midware/core/history/bus_udp_decoupler_20260306.py b/midware/core/history/bus_udp_decoupler_20260306.py new file mode 100644 index 0000000..e6abe8e --- /dev/null +++ b/midware/core/history/bus_udp_decoupler_20260306.py @@ -0,0 +1,203 @@ + +# -*- coding: utf-8 -*- +""" +Created on Tan Mingyan 2026/3/6 + +四、关键设计亮点(解耦 + 队列的核心价值) +彻底解耦: +UDP 收发 ≠ 总线处理:UDP 线程只做「数据搬运」,总线线程只做「业务处理」,修改总线逻辑无需改动 UDP 代码; +异常隔离:某一线程(如 CAN 处理)崩溃,不影响 UDP 收发和其他总线线程运行。 +队列缓冲区的核心作用: +削峰填谷:UDP 突发大量数据时,队列暂存,总线线程按能力消费,避免数据丢失; +异步通信:线程间通过队列通信,无直接调用,消除阻塞依赖; +可监控:通过 qsize() 监控队列长度,及时发现「生产 > 消费」的瓶颈(如队列持续满则需优化总线处理速度)。 +工业级容错: +队列满时丢弃数据 + 告警,避免内存溢出; +线程异常时休眠重试,避免 CPU 100%; +守护线程(daemon=True):主线程退出时自动终止子线程,避免僵尸线程; +优雅退出:join() 等待队列处理完成,避免强制退出导致数据丢失。 +五、扩展建议(根据业务需求调整) +队列持久化:若需断电不丢数据,可将队列数据写入本地文件 / Redis; +优先级队列:对关键设备(如 1553B)使用 queue.PriorityQueue,优先处理高优先级数据; +流量控制:监控队列长度,超过阈值时暂停 UDP 接收(或降低接收频率); +线程池优化:若总线协议过多,改用 concurrent.futures.ThreadPoolExecutor 管理线程,避免线程数量过多。 +""" +""" +总线-UDP解耦核心模块:队列+多线程 +""" +import threading +import queue +import time +from loguru import logger +from typing import Dict, Any, List + +# ========== 1. 全局队列定义(按协议分类) ========== +# 队列最大长度:避免内存溢出,根据业务调整(如1000) +MAX_QUEUE_SIZE = 1000 + +# 协议分发队列(UDP接收→协议分流) +protocol_dispatch_queue = queue.Queue(maxsize=MAX_QUEUE_SIZE) + +# 各总线处理队列 +bus_queues = { + "UART": queue.Queue(maxsize=MAX_QUEUE_SIZE), + "CAN": queue.Queue(maxsize=MAX_QUEUE_SIZE), + "1553B": queue.Queue(maxsize=MAX_QUEUE_SIZE), + "AD": queue.Queue(maxsize=MAX_QUEUE_SIZE), + "OC": queue.Queue(maxsize=MAX_QUEUE_SIZE), + "网络": queue.Queue(maxsize=MAX_QUEUE_SIZE) +} + +# UDP发送队列(总线回传→UDP发送) +udp_send_queue = queue.Queue(maxsize=MAX_QUEUE_SIZE) + +# ========== 2. UDP接收线程(仅负责收数据,不处理) ========== +def udp_receive_thread(udp_server): + """UDP接收线程:独立运行,仅收数据放入分发队列""" + logger.info("UDP接收线程启动") + while True: + try: + recv_data = udp_server.recv_multi_udp_hex() + if recv_data: + for data in recv_data: + # 非阻塞放入队列,避免线程阻塞 + try: + protocol_dispatch_queue.put_nowait(data) + logger.debug(f"UDP接收:{data['dev_name']} 数据放入分发队列,队列当前长度:{protocol_dispatch_queue.qsize()}") + except queue.Full: + logger.warning(f"协议分发队列已满,丢弃 {data['dev_name']} 数据") + time.sleep(1e-6) + except Exception as e: + logger.error(f"UDP接收线程异常:{str(e)}", exc_info=True) + time.sleep(0.1) # 异常时休眠,避免CPU飙高 + +# ========== 3. 协议分发线程(仅负责分流,不处理业务) ========== +def protocol_dispatch_thread(): + """协议分发线程:从分发队列取数据,按protocol分流到对应总线队列""" + logger.info("协议分发线程启动") + while True: + try: + # 阻塞取数据(队列为空时等待,无CPU消耗) + data = protocol_dispatch_queue.get(timeout=1) + protocol = data.get("protocol", "").strip().upper() + + # 分流到对应总线队列 + if protocol in bus_queues: + try: + bus_queues[protocol].put_nowait(data) + logger.debug(f"分发:{data['dev_name']} 数据到 {protocol} 队列,队列长度:{bus_queues[protocol].qsize()}") + except queue.Full: + logger.warning(f"{protocol} 队列已满,丢弃 {data['dev_name']} 数据") + else: + logger.warning(f"未知协议 {protocol},丢弃 {data['dev_name']} 数据") + + # 标记任务完成(队列join时需要) + protocol_dispatch_queue.task_done() + except queue.Empty: + continue # 队列为空,继续循环 + except Exception as e: + logger.error(f"协议分发线程异常:{str(e)}", exc_info=True) + time.sleep(0.1) + +# ========== 4. 总线处理线程(以UART为例,其他协议同理) ========== +def bus_process_thread(protocol: str, udp_server): + """通用总线处理线程:从对应队列取数据,处理后放入UDP发送队列""" + logger.info(f"{protocol} 总线处理线程启动") + while True: + try: + # 阻塞取数据 + data = bus_queues[protocol].get(timeout=1) + dev_name = data["dev_name"] + raw_data = data["raw_data"] + + # ========== 核心:总线业务处理(解耦后仅在此处修改) ========== + logger.info(f"{protocol} 处理:{dev_name} 原始数据:{raw_data.hex()}") + # 1. 总线数据解析(如UART波特率解析、CAN帧解析等) + processed_data = raw_data # 示例:实际需替换为业务逻辑 + # 2. 总线数据发送(如写入UART串口、发送CAN帧等) + # bus_driver.send(protocol, processed_data) # 总线驱动调用 + + # ========== 处理完成后,回传数据到UDP发送队列 ========== + try: + udp_send_queue.put_nowait({ + "dev_name": dev_name, + "data": processed_data, + "protocol": protocol + }) + logger.debug(f"{protocol} 处理完成,{dev_name} 回传数据放入UDP发送队列") + except queue.Full: + logger.warning(f"UDP发送队列已满,丢弃 {dev_name} 回传数据") + + # 标记任务完成 + bus_queues[protocol].task_done() + except queue.Empty: + continue + except Exception as e: + logger.error(f"{protocol} 总线线程异常:{str(e)}", exc_info=True) + time.sleep(0.1) + +# ========== 5. UDP发送线程(仅负责发数据,不处理) ========== +def udp_send_thread(udp_server): + """UDP发送线程:从发送队列取数据,发送到故障注入平台""" + logger.info("UDP发送线程启动") + while True: + try: + # 阻塞取数据 + send_data = udp_send_queue.get(timeout=1) + dev_name = send_data["dev_name"] + data = send_data["data"] + + # 调用UDP服务发送 + udp_server.send_to_fault(data, dev_name) + logger.debug(f"UDP发送:{dev_name} 数据到故障注入平台,队列剩余:{udp_send_queue.qsize()}") + + # 标记任务完成 + udp_send_queue.task_done() + except queue.Empty: + continue + except Exception as e: + logger.error(f"UDP发送线程异常:{str(e)}", exc_info=True) + time.sleep(0.1) + +# ========== 6. 线程启动/停止管理(集成到main.py) ========== +''' +def start_all_threads(udp_server): + """启动所有解耦线程""" + threads = [] + + # 1. UDP接收线程 + udp_recv_t = threading.Thread(target=udp_receive_thread, args=(udp_server,), daemon=True) + threads.append(udp_recv_t) + + # 2. 协议分发线程 + dispatch_t = threading.Thread(target=protocol_dispatch_thread, daemon=True) + threads.append(dispatch_t) + + # 3. 各总线处理线程 + for protocol in bus_queues.keys(): + bus_t = threading.Thread(target=bus_process_thread, args=(protocol, udp_server), daemon=True) + threads.append(bus_t) + + # 4. UDP发送线程 + udp_send_t = threading.Thread(target=udp_send_thread, args=(udp_server,), daemon=True) + threads.append(udp_send_t) + + # 启动所有线程 + for t in threads: + t.start() + logger.info(f"线程 {t.name} 启动成功") + + return threads + +def stop_all_threads(): + """优雅停止所有线程(等待队列处理完成)""" + logger.info("开始停止所有线程,等待队列处理完成...") + + # 等待队列所有任务完成 + protocol_dispatch_queue.join() + for q in bus_queues.values(): + q.join() + udp_send_queue.join() + + logger.info("所有队列处理完成,线程已停止") +''' \ No newline at end of file diff --git a/midware/core/history/bus_udp_decoupler_20260307.py b/midware/core/history/bus_udp_decoupler_20260307.py new file mode 100644 index 0000000..493f477 --- /dev/null +++ b/midware/core/history/bus_udp_decoupler_20260307.py @@ -0,0 +1,234 @@ +# -*- coding: utf-8 -*- +""" +Created on Tan Mingyan 2026/3/7 +""" +""" +总线-UDP双向通信解耦模块(加入收发缓冲区分离) +""" +import threading +import queue +import time +from loguru import logger +from typing import Dict, Any, Optional + +# ========== 1. 收发队列分离:为每个协议定义独立的收/发队列 ========== +MAX_QUEUE_SIZE = 1000 # 可按协议单独调整(如1553B设2000,UART设500) + +# 总线接收队列(总线→中间件:上行数据) +bus_recv_queues = { + "UART": queue.Queue(maxsize=MAX_QUEUE_SIZE), + "CAN": queue.Queue(maxsize=MAX_QUEUE_SIZE), + "1553B": queue.Queue(maxsize=2000), # 1553B数据量大,单独调整 + "AD": queue.Queue(maxsize=MAX_QUEUE_SIZE), + "OC": queue.Queue(maxsize=MAX_QUEUE_SIZE), + "网络": queue.Queue(maxsize=MAX_QUEUE_SIZE) +} + +# 总线发送队列(中间件→总线:下行数据) +bus_send_queues = { + "UART": queue.PriorityQueue(maxsize=MAX_QUEUE_SIZE), # 发送队列支持优先级 + "CAN": queue.PriorityQueue(maxsize=MAX_QUEUE_SIZE), + "1553B": queue.PriorityQueue(maxsize=2000), + "AD": queue.PriorityQueue(maxsize=MAX_QUEUE_SIZE), + "OC": queue.PriorityQueue(maxsize=MAX_QUEUE_SIZE), + "网络": queue.PriorityQueue(maxsize=MAX_QUEUE_SIZE) +} + +# UDP发送队列(中间件→UDP:上行数据,总线接收后转发) +udp_send_queue = queue.Queue(maxsize=MAX_QUEUE_SIZE) +# UDP接收队列(UDP→中间件:下行数据,转发到总线发送队列) +udp_recv_dispatch_queue = queue.Queue(maxsize=MAX_QUEUE_SIZE) + +# ========== 2. UDP接收线程:仅处理下行指令(转发到总线发送队列) ========== +def udp_recv_thread(udp_server): + """UDP接收线程:接收故障注入平台的下行指令,分发到总线发送队列""" + logger.info("UDP接收线程(下行指令)启动") + while True: + try: + recv_data = udp_server.recv_multi_udp_hex() + if recv_data: + for data in recv_data: + protocol = data.get("protocol", "").strip().upper() + dev_name = data.get("dev_name", "") + raw_data = data.get("raw_data", b"") + priority = data.get("priority", 5) # 默认优先级5(1最高,10最低) + + if protocol not in bus_send_queues: + logger.warning(f"未知协议 {protocol},丢弃下行指令:{dev_name}") + continue + + # 放入总线发送队列(带优先级) + try: + # PriorityQueue格式:(优先级, 数据) + bus_send_queues[protocol].put_nowait((priority, { + "dev_name": dev_name, + "raw_data": raw_data, + "protocol": protocol + })) + logger.debug(f"下行指令:{dev_name} ({protocol}) 放入发送队列,优先级:{priority}") + except queue.Full: + logger.warning(f"{protocol} 发送队列已满,丢弃 {dev_name} 下行指令") + time.sleep(1e-6) + except Exception as e: + logger.error(f"UDP接收线程异常:{str(e)}", exc_info=True) + time.sleep(0.1) + +# ========== 3. 总线发送线程:处理下行指令(中间件→总线) ========== +def bus_send_thread(protocol: str, bus_driver: Optional[Any] = None): + """总线发送线程:从发送队列取下行指令,发送到总线""" + logger.info(f"{protocol} 总线发送线程启动") + while True: + try: + # PriorityQueue阻塞取数据(优先级高的先取) + priority, send_data = bus_send_queues[protocol].get(timeout=1) + dev_name = send_data["dev_name"] + raw_data = send_data["raw_data"] + + # 总线发送逻辑(适配双向通信:调用总线驱动发送指令) + logger.info(f"{protocol} 发送:{dev_name} | 优先级:{priority} | 数据:{raw_data.hex()}") + # 实际业务:调用总线驱动发送(如CAN发送帧、UART写串口) + # if bus_driver: + # bus_driver.send(raw_data) + + # 标记任务完成 + bus_send_queues[protocol].task_done() + + # 在bus_send_thread中添加超时检查 + if time.time() - send_data.get("send_time", 0) > 5: # 5秒超时 + logger.error(f"{protocol} 发送超时:{dev_name}") + bus_send_queues[protocol].task_done() + continue + except queue.Empty: + continue + except Exception as e: + logger.error(f"{protocol} 发送线程异常:{str(e)}", exc_info=True) + time.sleep(0.1) + +# ========== 4. 总线接收线程:处理上行数据(总线→中间件) ========== +def bus_recv_thread(protocol: str, bus_driver: Optional[Any] = None): + """总线接收线程:从总线接收上行数据,放入接收队列""" + logger.info(f"{protocol} 总线接收线程启动") + while True: + try: + # 模拟总线接收(实际需替换为总线驱动的接收接口) + # raw_data = bus_driver.recv() # 从总线读取上行数据 + # 此处为示例,实际需对接真实总线驱动 + raw_data = b"" # 占位:替换为总线接收逻辑 + + if raw_data: + dev_name = f"{protocol}_DEV" # 实际从总线数据解析设备名 + # 放入总线接收队列 + try: + bus_recv_queues[protocol].put_nowait({ + "dev_name": dev_name, + "raw_data": raw_data, + "protocol": protocol, + "recv_time": time.time() # 记录接收时间,便于时序分析 + }) + logger.debug(f"{protocol} 接收:{dev_name} 上行数据,队列长度:{bus_recv_queues[protocol].qsize()}") + except queue.Full: + logger.warning(f"{protocol} 接收队列已满,丢弃上行数据") + time.sleep(1e-6) # 匹配总线接收速率 + except Exception as e: + logger.error(f"{protocol} 接收线程异常:{str(e)}", exc_info=True) + time.sleep(0.1) + +# ========== 5. 总线接收分发线程:汇总上行数据→UDP发送 ========== +def bus_recv_dispatch_thread(): + """总线接收分发线程:汇总各协议接收队列数据,转发到UDP发送队列""" + logger.info("总线接收分发线程启动") + while True: + try: + # 遍历所有总线接收队列,非阻塞取数据(避免单队列阻塞) + for protocol, q in bus_recv_queues.items(): + try: + recv_data = q.get_nowait() + # 转发到UDP发送队列(发送到故障注入平台) + try: + udp_send_queue.put_nowait(recv_data) + logger.debug(f"{protocol} 上行数据:{recv_data['dev_name']} 转发到UDP发送队列") + except queue.Full: + logger.warning(f"UDP发送队列已满,丢弃 {protocol} 上行数据") + q.task_done() + except queue.Empty: + continue + time.sleep(1e-6) + except Exception as e: + logger.error(f"总线接收分发线程异常:{str(e)}", exc_info=True) + time.sleep(0.1) + +# ========== 6. UDP发送线程:处理上行数据(中间件→UDP) ========== +def udp_send_thread(udp_server): + """UDP发送线程:从UDP发送队列取上行数据,发送到故障注入平台""" + logger.info("UDP发送线程(上行数据)启动") + while True: + try: + send_data = udp_send_queue.get(timeout=1) + dev_name = send_data["dev_name"] + raw_data = send_data["raw_data"] + + # 调用UDP服务发送到故障注入平台 + udp_server.send_to_fault(raw_data, dev_name) + logger.debug(f"UDP发送:{dev_name} 上行数据,队列剩余:{udp_send_queue.qsize()}") + udp_send_queue.task_done() + except queue.Empty: + continue + except Exception as e: + logger.error(f"UDP发送线程异常:{str(e)}", exc_info=True) + time.sleep(0.1) + +''' +# ========== 7. 线程管理:启动/停止所有收发线程 ========== +def start_all_threads(udp_server, bus_drivers: Dict[str, Any] = None): + """ + 启动所有双向通信线程 + :param udp_server: UDP服务实例 + :param bus_drivers: 总线驱动字典,格式:{"UART": uart_driver, "CAN": can_driver, ...} + """ + threads = [] + bus_drivers = bus_drivers or {} # 无驱动时传空字典 + + # 1. UDP接收线程(下行指令) + udp_recv_t = threading.Thread(target=udp_recv_thread, args=(udp_server,), daemon=True) + threads.append(udp_recv_t) + + # 2. 总线发送线程(每个协议一个) + for protocol in bus_send_queues.keys(): + driver = bus_drivers.get(protocol) + send_t = threading.Thread(target=bus_send_thread, args=(protocol, driver), daemon=True) + threads.append(send_t) + + # 3. 总线接收线程(每个协议一个) + for protocol in bus_recv_queues.keys(): + driver = bus_drivers.get(protocol) + recv_t = threading.Thread(target=bus_recv_thread, args=(protocol, driver), daemon=True) + threads.append(recv_t) + + # 4. 总线接收分发线程 + recv_dispatch_t = threading.Thread(target=bus_recv_dispatch_thread, daemon=True) + threads.append(recv_dispatch_t) + + # 5. UDP发送线程(上行数据) + udp_send_t = threading.Thread(target=udp_send_thread, args=(udp_server,), daemon=True) + threads.append(udp_send_t) + + # 启动所有线程 + for t in threads: + t.start() + logger.info(f"线程 {t.name} 启动成功") + return threads + +def stop_all_threads(): + """优雅停止所有线程,等待队列处理完成""" + logger.info("开始停止所有线程,等待队列处理完成...") + # 等待发送队列处理完成 + for q in bus_send_queues.values(): + q.join() + # 等待接收队列处理完成 + for q in bus_recv_queues.values(): + q.join() + # 等待UDP发送队列处理完成 + udp_send_queue.join() + logger.info("所有队列处理完成,线程已停止") + + ''' \ No newline at end of file diff --git a/midware/drivers/__init__.py b/midware/drivers/__init__.py new file mode 100644 index 0000000..371350d --- /dev/null +++ b/midware/drivers/__init__.py @@ -0,0 +1,37 @@ +# midware/drivers/__init__.py +""" +总线驱动模块:统一管理所有总线驱动的导入和初始化 +""" +from .base_driver import BaseBusDriver +from .bus_1553b import Bus1553BDriver +from .bus_can import BusCANDriver +from .bus_uart import BusUARTDriver +from .bus_ad import BusADDriver +from .bus_oc import BusOCDriver +from .bus_network import BusNetworkDriver +from loguru import logger + +# 驱动映射表:协议名称 → 驱动类(与bus_udp_decoupler.py中的协议名对齐) +DRIVER_MAPPING = { + "1553B": Bus1553BDriver, + "CAN": BusCANDriver, + "UART": BusUARTDriver, + "AD": BusADDriver, + "OC": BusOCDriver, + "网络": BusNetworkDriver +} + +def create_bus_driver(protocol: str, config: dict = None) -> BaseBusDriver: + """ + 工厂函数:根据协议名称创建驱动实例 + :param protocol: 协议名称(如"1553B") + :param config: 驱动配置 + :return: 驱动实例 + """ + if protocol not in DRIVER_MAPPING: + raise ValueError(f"未知协议 {protocol},无对应驱动") + + driver_class = DRIVER_MAPPING[protocol] + driver = driver_class(config=config) + logger.info(f"创建 {protocol} 驱动实例成功:{driver.__class__.__name__}") + return driver \ No newline at end of file diff --git a/midware/drivers/__pycache__/__init__.cpython-311.pyc b/midware/drivers/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000..7cef826 Binary files /dev/null and b/midware/drivers/__pycache__/__init__.cpython-311.pyc differ diff --git a/midware/drivers/__pycache__/base_driver.cpython-311.pyc b/midware/drivers/__pycache__/base_driver.cpython-311.pyc new file mode 100644 index 0000000..3bf40b0 Binary files /dev/null and b/midware/drivers/__pycache__/base_driver.cpython-311.pyc differ diff --git a/midware/drivers/__pycache__/bus_1553b.cpython-311.pyc b/midware/drivers/__pycache__/bus_1553b.cpython-311.pyc new file mode 100644 index 0000000..1d03c45 Binary files /dev/null and b/midware/drivers/__pycache__/bus_1553b.cpython-311.pyc differ diff --git a/midware/drivers/__pycache__/bus_ad.cpython-311.pyc b/midware/drivers/__pycache__/bus_ad.cpython-311.pyc new file mode 100644 index 0000000..d86f07f Binary files /dev/null and b/midware/drivers/__pycache__/bus_ad.cpython-311.pyc differ diff --git a/midware/drivers/__pycache__/bus_can.cpython-311.pyc b/midware/drivers/__pycache__/bus_can.cpython-311.pyc new file mode 100644 index 0000000..412791e Binary files /dev/null and b/midware/drivers/__pycache__/bus_can.cpython-311.pyc differ diff --git a/midware/drivers/__pycache__/bus_network.cpython-311.pyc b/midware/drivers/__pycache__/bus_network.cpython-311.pyc new file mode 100644 index 0000000..3b23878 Binary files /dev/null and b/midware/drivers/__pycache__/bus_network.cpython-311.pyc differ diff --git a/midware/drivers/__pycache__/bus_oc.cpython-311.pyc b/midware/drivers/__pycache__/bus_oc.cpython-311.pyc new file mode 100644 index 0000000..a4d208c Binary files /dev/null and b/midware/drivers/__pycache__/bus_oc.cpython-311.pyc differ diff --git a/midware/drivers/__pycache__/bus_uart.cpython-311.pyc b/midware/drivers/__pycache__/bus_uart.cpython-311.pyc new file mode 100644 index 0000000..5e37b2d Binary files /dev/null and b/midware/drivers/__pycache__/bus_uart.cpython-311.pyc differ diff --git a/midware/drivers/__pycache__/renode_agent.cpython-311.pyc b/midware/drivers/__pycache__/renode_agent.cpython-311.pyc new file mode 100644 index 0000000..eb821d5 Binary files /dev/null and b/midware/drivers/__pycache__/renode_agent.cpython-311.pyc differ diff --git a/midware/drivers/__pycache__/tcp_uart_agent.cpython-311.pyc b/midware/drivers/__pycache__/tcp_uart_agent.cpython-311.pyc new file mode 100644 index 0000000..79b392c Binary files /dev/null and b/midware/drivers/__pycache__/tcp_uart_agent.cpython-311.pyc differ diff --git a/midware/drivers/base_driver.py b/midware/drivers/base_driver.py new file mode 100644 index 0000000..c096bed --- /dev/null +++ b/midware/drivers/base_driver.py @@ -0,0 +1,78 @@ +# midware/drivers/base_driver.py + +""" +Tan mingyan +2026/3/9 + 驱动基类(base_driver.py)—— 定义标准接口(核心) +所有总线驱动继承该基类,保证接口统一,解耦 bus_udp_decoupler.py 与具体驱动实现: +""" +from abc import ABC, abstractmethod +from typing import Optional, Dict, Any +from loguru import logger + +class BaseBusDriver(ABC): + """ + 总线驱动基类:定义所有总线必须实现的标准接口 + 遵循「开闭原则」:新增总线只需继承该类实现接口,无需修改现有代码 + """ + def __init__(self, config: Optional[Dict[str, Any]] = None): + """ + 初始化驱动 + :param config: 驱动配置(如波特率、端口、IP等) + """ + self.config = config or {} + self.is_connected = False # 驱动连接状态 + self._init_config() # 初始化配置 + + def _init_config(self): + """初始化默认配置(子类可重写)""" + logger.info(f"初始化 {self.__class__.__name__} 默认配置") + + @abstractmethod + def connect(self) -> bool: + """ + 连接总线(如打开串口、建立CAN通道、连接1553B板卡) + :return: 连接成功返回True,失败返回False + """ + pass + + @abstractmethod + def disconnect(self) -> bool: + """ + 断开总线连接 + :return: 断开成功返回True,失败返回False + """ + pass + + @abstractmethod + def send(self, data: bytes, **kwargs) -> bool: + """ + 发送数据到总线 + :param data: 待发送的原始字节数据 + :param kwargs: 扩展参数(如优先级、地址、帧ID等) + :return: 发送成功返回True,失败返回False + """ + pass + + @abstractmethod + def recv(self, timeout: float = 0.1, **kwargs) -> Optional[bytes]: + """ + 从总线接收数据 + :param timeout: 接收超时时间(秒) + :param kwargs: 扩展参数(如过滤条件、地址等) + :return: 接收到的字节数据,超时/失败返回None + """ + pass + + def check_status(self) -> bool: + """ + 检查驱动状态(默认实现,子类可重写) + :return: 驱动正常返回True,异常返回False + """ + return self.is_connected + + def __del__(self): + """析构函数:自动断开连接""" + if self.is_connected: + self.disconnect() + logger.info(f"{self.__class__.__name__} 自动断开连接") \ No newline at end of file diff --git a/midware/drivers/bus_1553b.py b/midware/drivers/bus_1553b.py new file mode 100644 index 0000000..7d7f72a --- /dev/null +++ b/midware/drivers/bus_1553b.py @@ -0,0 +1,100 @@ +""" +Tan mingyan +2026/3/9 + +2. 具体驱动实现(以 1553B 为例,bus_1553b.py) +其他总线(CAN/UART/AD 等)按相同逻辑实现,仅需重写基类接口: +""" +# midware/drivers/bus_1553b.py +from .base_driver import BaseBusDriver +from typing import Optional, Dict, Any +from loguru import logger +import time + +class Bus1553BDriver(BaseBusDriver): + """1553B总线驱动实现(适配实际1553B板卡/仿真器)""" + def _init_config(self): + """初始化1553B默认配置""" + # 默认配置(可从yaml文件加载) + self.config.setdefault("board_id", 0) # 板卡ID + self.config.setdefault("bc_address", 0x01) # BC地址 + self.config.setdefault("rt_address", 0x02) # RT地址 + self.config.setdefault("baudrate", 1000000) # 波特率1Mbps + logger.info(f"1553B驱动配置初始化完成:{self.config}") + + def connect(self) -> bool: + """连接1553B板卡/仿真器""" + try: + # 实际逻辑:调用1553B板卡SDK的连接接口 + # 示例:self.board = SDK_1553B.connect(board_id=self.config["board_id"]) + time.sleep(0.1) # 模拟连接耗时 + self.is_connected = True + logger.info("1553B总线连接成功") + return True + except Exception as e: + logger.error(f"1553B总线连接失败:{str(e)}") + self.is_connected = False + return False + + def disconnect(self) -> bool: + """断开1553B连接""" + try: + # 实际逻辑:调用SDK的断开接口 + # self.board.disconnect() + self.is_connected = False + logger.info("1553B总线断开成功") + return True + except Exception as e: + logger.error(f"1553B总线断开失败:{str(e)}") + return False + + def send(self, data: bytes, **kwargs) -> bool: + """发送1553B数据帧""" + if not self.is_connected: + logger.error("1553B驱动未连接,发送失败") + return False + + try: + # 扩展参数:优先级、子地址等 + priority = kwargs.get("priority", 1) # 1553B最高优先级 + sub_address = kwargs.get("sub_address", 0x00) + + # 实际逻辑:调用SDK发送1553B帧 + logger.info( + f"1553B发送数据|优先级:{priority}|子地址:{sub_address}|" + f"数据:{data.hex()}|长度:{len(data)}字节" + ) + # self.board.send_frame( + # bc_addr=self.config["bc_address"], + # rt_addr=self.config["rt_address"], + # sub_addr=sub_address, + # data=data, + # priority=priority + # ) + return True + except Exception as e: + logger.error(f"1553B数据发送失败:{str(e)}") + return False + + def recv(self, timeout: float = 0.1, **kwargs) -> Optional[bytes]: + """接收1553B数据帧""" + if not self.is_connected: + logger.error("1553B驱动未连接,接收失败") + return None + + try: + # 实际逻辑:调用SDK接收1553B帧 + # frame = self.board.recv_frame(timeout=timeout) + # if frame: + # return frame.data + # return None + + # 模拟接收(替换为真实逻辑) + time.sleep(0.001) # 模拟接收耗时 + # mock_data = b"\x01\x02\x03\x04\x05" # 模拟1553B数据 + # logger.debug(f"1553B接收数据:{mock_data.hex()}") + #return mock_data + return None + except Exception as e: + logger.error(f"1553B数据接收失败:{str(e)}") + return None \ No newline at end of file diff --git a/midware/drivers/bus_ad.py b/midware/drivers/bus_ad.py new file mode 100644 index 0000000..d3f87e2 --- /dev/null +++ b/midware/drivers/bus_ad.py @@ -0,0 +1,190 @@ +""" +Tan mingyan +2026/3/9 + +AD 驱动示例(bus_ad.py)—— 补充参考 +""" +# midware/drivers/bus_can.py +from .base_driver import BaseBusDriver +from typing import Optional, Dict, Any +from loguru import logger +import time +# 导入全局配置字典(需确保main.py已提前加载CSV配置) +from midware.config.base_config import DEVICE_CONFIG_DICT, get_device_config +import midware.config.base_config as base_config + +from .base_driver import BaseBusDriver +from .renode_agent import renode +from midware.config.base_config import DEVICE_CONFIG_DICT + +class BusADDriver(BaseBusDriver): + """uart总线驱动实现(适配)""" + def _init_config(self): + self.config.setdefault("channel", 0) # CAN通道 + self.config.setdefault("baudrate", 500000) # 500kbps + self.config.setdefault("frame_format", "standard") # 标准帧/扩展帧 + logger.info(f"UART驱动配置初始化完成:{self.config}") + + def connect(self) -> bool: + try: + # 实际逻辑:打开CAN通道 + time.sleep(0.05) + self.is_connected = True + logger.info("CAN总线连接成功") + return True + except Exception as e: + logger.error(f"CAN总线连接失败:{str(e)}") + return False + + def disconnect(self) -> bool: + try: + self.is_connected = False + logger.info("CAN总线断开成功") + return True + except Exception as e: + logger.error(f"CAN总线断开失败:{str(e)}") + return False + + def send(self, data: bytes, **kwargs) -> bool: + if not self.is_connected: + logger.error("CAN驱动未连接,发送失败") + return False + ''' + try: + frame_id = kwargs.get("frame_id", 0x123) # CAN帧ID + is_extended = kwargs.get("is_extended", False) # 是否扩展帧 + logger.info( + f"CAN发送数据|帧ID:0x{frame_id:X}|扩展帧:{is_extended}|" + f"数据:{data.hex()}|长度:{len(data)}字节" + ) + # 实际逻辑:调用AD驱动发送接口 + # 获取优先级,设备参数,地址等参数 + + return True + except Exception as e: + logger.error(f"CAN数据发送失败:{str(e)}") + return False + ''' + # ========== 核心步骤1:从kwargs提取设备名称 ========== + #dev_name = kwargs.get("dev_name")[0] + dev_name = kwargs.get("dev_name")#2026/4/10 + if not dev_name: + logger.error("AD发送失败:kwargs中未传入dev_name(设备名称)") + return False + + # ========== 核心步骤2:从全局配置字典查找设备参数 ========== + # 检查全局配置是否加载 + if not base_config.DEVICE_CONFIG_DICT: + logger.error("AD发送失败:全局配置DEVICE_CONFIG_DICT未加载") + return False + + # 查找当前设备的配置 + + #device_config = base_config.DEVICE_CONFIG_DICT.get(dev_name) + device_config = get_device_config().get(dev_name) + if not device_config: + logger.error(f"AD发送失败:设备 {dev_name} 未在DEVICE_CONFIG_DICT中配置") + return False + + # 提取关键配置项(兼容配置项缺失的情况) + try: + # 内存基地址(转为16进制整数,如"0x80000000" → 0x80000000) + # mem_base_addr = int(str(device_config.get("base_addr", "0x0")), 16)#mem_base_addr = int(device_config.get("base_addr", "0x0"), 16)#内存基地址 + mem_base_addr = int(device_config.get("base_addr", 0)) + # 内存偏移地址 + #mem_offset = int(str(device_config.get("offset_addr", "0x0")), 16)#内存偏移地址 + mem_offset = int(device_config.get("offset_addr", 0)) + # 发送缓存区大小(字节) + send_buffer_size = int(str(device_config.get("send_cache", 1024)))#发送缓存区大小 + # 接收缓存区大小(字节) + recv_buffer_size = int(device_config.get("recv_cache", 1024))#接收缓存区大小 + except (ValueError, TypeError) as e: + logger.error(f"AD发送失败:{dev_name} 配置参数解析错误 → {str(e)}") + return False + + # ========== 核心步骤3:使用配置项处理发送逻辑 ========== + # 1. 校验数据长度不超过发送缓存区大小 + if len(data) > send_buffer_size: + logger.error( + f"AD发送失败:{dev_name} 数据长度 {len(data)} 超过发送缓存区大小 {send_buffer_size}" + ) + return False + + # 2. 模拟AD指令发送(结合内存地址) + logger.info( + f"AD发送指令|设备:{dev_name}|" + f"内存基地址:0x{mem_base_addr:X}|偏移地址:0x{mem_offset:X}|" + f"发送缓存区:{send_buffer_size}B|接收缓存区:{recv_buffer_size}B|" + f"数据:{data.hex()}|长度:{len(data)}字节" + ) + + # 实际业务逻辑: + # - 将数据写入指定内存地址(mem_base_addr + mem_offset) + # - 配置接收缓存区大小 + # - 调用AD采集卡SDK发送指令 + # ad_card.write_mem(mem_base_addr + mem_offset, data, send_buffer_size) + + #AD0-AD6的寄存器写入策略 + if(dev_name == "正电压采集热敏量采集"): + for i in range(4): + logger.info(f"AD发送指令:{i}") + for j in range(14): + logger.info(f"AD发送指令:{j}") + target_addr = mem_base_addr + mem_offset + i * 32 + j*8 + canshu = data[2*(i*14+j)]*256 + data[2*(i*14+j)+1]# 2字节 + sysbus_cmd = f"sysbus WriteDoubleWord {target_addr:#010x} {canshu:#006x}" + print(sysbus_cmd) + pass + + ## 外设名称+函数名+字符串参数 adc1 LoadRegistersFromHexString "55AA55AA55AA55AA55AA55AA55AA55AA55AA55AA55AA55AA55AA55AA55AA55AA55AA55AA55AA55AA55AA55AA55AA55AA55AA55AA55AA55AA55AA55AA55AA55AA55AA55AA55AA55AA55AA55AA55AA55AA55AA55AA55AA55AA55AA55AA55AA55AA55AA55AA55AA55AA55AA55AA55AA55AA55AA55AA55AA55AA55AA55AA55AA55AA"' + ad_cmd = f'{dev_name} LoadRegistersFromHexString "{data.hex()}"' + print(ad_cmd) + logger.info(f"AD发送指令:{ad_cmd}") + response = renode.send_sync(ad_cmd,10)#2026/4/10 + ad_cmd2 = f'{dev_name} ReadAllChannelsToString02' + logger.info(f"AD发送指令:{ad_cmd2}") + response = renode.send_sync(ad_cmd2,10)#2026/4/10 + + return True + + def recv(self, timeout: float = 0.1, **kwargs) -> Optional[bytes]: + if not self.is_connected: + logger.error("ADC驱动未连接,接收失败") + return None + + try: + # 模拟接收 + #遍历所有AD外设,循环发送读取指令:例如 adc1 ReadAllChannelsToString02 + #接收renode回复 + time.sleep(0.51) + ''' + mock_data = b"\x11\x22\x33\x44" + logger.debug(f"AD接收数据:{mock_data.hex()}") + + #2026/4/10 + dev_name = kwargs.get("dev_name") + if not dev_name: + logger.error("AD接收失败:kwargs中未传入dev_name(设备名称)") + return None + + cfg = DEVICE_CONFIG_DICT[dev_name] + base = int(cfg["内存基地址"], 16) + offset = int(cfg["内存偏移地址"], 16) + addr = base + offset + + # ====================== + # 调用 Renode 读取数据 + # ====================== + cmd = f"read 0x{addr:X}" + hex_str = renode.send_sync(cmd) + #return bytes.fromhex(hex_str.strip()) + logger.info(f"AD接收数据:{mock_data.hex()}") + return mock_data + ''' + return None + except Exception as e: + logger.error(f"AD数据接收失败:{str(e)}") + return None + + + \ No newline at end of file diff --git a/midware/drivers/bus_can.py b/midware/drivers/bus_can.py new file mode 100644 index 0000000..4521952 --- /dev/null +++ b/midware/drivers/bus_can.py @@ -0,0 +1,133 @@ +""" +Tan mingyan +2026/3/9 + +CAN 驱动示例(bus_can.py)—— 补充参考 +""" +# midware/drivers/bus_can.py +from .base_driver import BaseBusDriver +from typing import Optional, Dict, Any +from loguru import logger +import time + +from midware.config.base_config import get_device_config +from .base_driver import BaseBusDriver +from .renode_agent import renode +from midware.config.base_config import DEVICE_CONFIG_DICT + +class BusCANDriver(BaseBusDriver): + """CAN总线驱动实现(适配CANoe/CANalyzer/USB-CAN)""" + def _init_config(self): + self.config.setdefault("channel", 0) # CAN通道 + self.config.setdefault("baudrate", 500000) # 500kbps + self.config.setdefault("frame_format", "standard") # 标准帧/扩展帧 + logger.info(f"CAN驱动配置初始化完成:{self.config}") + + def connect(self) -> bool: + try: + # 实际逻辑:打开CAN通道 + time.sleep(0.05) + self.is_connected = True + logger.info("CAN总线连接成功") + return True + except Exception as e: + logger.error(f"CAN总线连接失败:{str(e)}") + return False + + def disconnect(self) -> bool: + try: + self.is_connected = False + logger.info("CAN总线断开成功") + return True + except Exception as e: + logger.error(f"CAN总线断开失败:{str(e)}") + return False + + def send(self, data: bytes, **kwargs) -> bool: + if not self.is_connected: + logger.error("CAN驱动未连接,发送失败") + return False + # ========== 核心步骤1:从kwargs提取设备名称 ========== + + dev_name = kwargs.get("dev_name")#2026/4/10 + if not dev_name: + logger.error("CAN发送失败:kwargs中未传入dev_name(设备名称)") + return False + + device_config = get_device_config().get(dev_name) + if not device_config: + logger.error(f"CAN发送失败:设备 {dev_name} 未在DEVICE_CONFIG_DICT中配置") + return False + + try: + frame_id = kwargs.get("frame_id", 0x123) # CAN帧ID + is_extended = kwargs.get("is_extended", False) # 是否扩展帧 + logger.info( + f"CAN发送数据|帧ID:0x{frame_id:X}|扩展帧:{is_extended}|" + f"数据:{data.hex()}|长度:{len(data)}字节" + ) + # logger.info( + # #f"UART发送数据|帧ID:0x{frame_id:X}|扩展帧:{is_extended}|" + # f"CAN发送数据:{data.hex()}|长度:{len(data)}字节" + # ) + ## 外设名称+函数名+字符串参数 uart24 WriteRXFIFODataString "0xEB9000C571000827C00000BC1003190000106C02AC020101FF5716"' + + ## 收到的CAN帧可能是多帧连在一起,需要拆分单帧保证虚拟平台能读取。 + frame_len = 11 + frame_list = [] + total =len(data) + #步长11字节截取完整帧,不满足的丢弃(尾帧长度可以不满11,不丢弃) + end = 0 + for start in range(0,total, frame_len): + ''' + if end>total: + break + end = start + frame_len + frame_list.append(data[start:end]) + sysbus_cmd = f'{dev_name} SendRxBufferDataString "0x{data[start:end].hex()}"' + #print(sysbus_cmd) + logger.info(f"CAN发送指令:{sysbus_cmd}") + response = renode.send_sync(sysbus_cmd)#2026/4/10 + time.sleep(0.0001) + ''' + #计算当前帧结束的位置 + end = min(start + frame_len, total) + frame = data[start:end] + #尾帧长度不足11时,补0到11字节 + if len(frame) Optional[bytes]: + if not self.is_connected: + logger.error("CAN驱动未连接,接收失败") + return None + + try: + # 模拟接收 + time.sleep(0.001) + # mock_data = b"\x11\x22\x33\x44" + # logger.debug(f"CAN接收数据:{mock_data.hex()}") + # return mock_data + return None + except Exception as e: + logger.error(f"CAN数据接收失败:{str(e)}") + return None \ No newline at end of file diff --git a/midware/drivers/bus_network.py b/midware/drivers/bus_network.py new file mode 100644 index 0000000..c408369 --- /dev/null +++ b/midware/drivers/bus_network.py @@ -0,0 +1,73 @@ +""" +Tan mingyan +2026/3/9 + +network 驱动示例(bus_network .py)—— 补充参考 +""" +# midware/drivers/bus_can.py +from .base_driver import BaseBusDriver +from typing import Optional, Dict, Any +from loguru import logger +import time + +class BusNetworkDriver(BaseBusDriver): + """network 总线驱动实现(适配)""" + def _init_config(self): + self.config.setdefault("channel", 0) # CAN通道 + self.config.setdefault("baudrate", 500000) # 500kbps + self.config.setdefault("frame_format", "standard") # 标准帧/扩展帧 + logger.info(f"UART驱动配置初始化完成:{self.config}") + + def connect(self) -> bool: + try: + # 实际逻辑:打开CAN通道 + time.sleep(0.05) + self.is_connected = True + logger.info("CAN总线连接成功") + return True + except Exception as e: + logger.error(f"CAN总线连接失败:{str(e)}") + return False + + def disconnect(self) -> bool: + try: + self.is_connected = False + logger.info("CAN总线断开成功") + return True + except Exception as e: + logger.error(f"CAN总线断开失败:{str(e)}") + return False + + def send(self, data: bytes, **kwargs) -> bool: + if not self.is_connected: + logger.error("CAN驱动未连接,发送失败") + return False + + try: + frame_id = kwargs.get("frame_id", 0x123) # CAN帧ID + is_extended = kwargs.get("is_extended", False) # 是否扩展帧 + logger.info( + f"CAN发送数据|帧ID:0x{frame_id:X}|扩展帧:{is_extended}|" + f"数据:{data.hex()}|长度:{len(data)}字节" + ) + # 实际逻辑:调用CAN驱动发送接口 + return True + except Exception as e: + logger.error(f"CAN数据发送失败:{str(e)}") + return False + + def recv(self, timeout: float = 0.1, **kwargs) -> Optional[bytes]: + if not self.is_connected: + logger.error("CAN驱动未连接,接收失败") + return None + + try: + # 模拟接收 + time.sleep(0.001) + # mock_data = b"\x11\x22\x33\x44" + # logger.debug(f"CAN接收数据:{mock_data.hex()}") + # return mock_data + return None + except Exception as e: + logger.error(f"CAN数据接收失败:{str(e)}") + return None \ No newline at end of file diff --git a/midware/drivers/bus_oc.py b/midware/drivers/bus_oc.py new file mode 100644 index 0000000..b96a387 --- /dev/null +++ b/midware/drivers/bus_oc.py @@ -0,0 +1,73 @@ +""" +Tan mingyan +2026/3/9 + +oc 驱动示例(bus_oc.py)—— 补充参考 +""" +# midware/drivers/bus_can.py +from .base_driver import BaseBusDriver +from typing import Optional, Dict, Any +from loguru import logger +import time + +class BusOCDriver(BaseBusDriver): + """uart总线驱动实现(适配)""" + def _init_config(self): + self.config.setdefault("channel", 0) # CAN通道 + self.config.setdefault("baudrate", 500000) # 500kbps + self.config.setdefault("frame_format", "standard") # 标准帧/扩展帧 + logger.info(f"UART驱动配置初始化完成:{self.config}") + + def connect(self) -> bool: + try: + # 实际逻辑:打开CAN通道 + time.sleep(0.05) + self.is_connected = True + logger.info("CAN总线连接成功") + return True + except Exception as e: + logger.error(f"CAN总线连接失败:{str(e)}") + return False + + def disconnect(self) -> bool: + try: + self.is_connected = False + logger.info("CAN总线断开成功") + return True + except Exception as e: + logger.error(f"CAN总线断开失败:{str(e)}") + return False + + def send(self, data: bytes, **kwargs) -> bool: + if not self.is_connected: + logger.error("CAN驱动未连接,发送失败") + return False + + try: + frame_id = kwargs.get("frame_id", 0x123) # CAN帧ID + is_extended = kwargs.get("is_extended", False) # 是否扩展帧 + logger.info( + f"CAN发送数据|帧ID:0x{frame_id:X}|扩展帧:{is_extended}|" + f"数据:{data.hex()}|长度:{len(data)}字节" + ) + # 实际逻辑:调用CAN驱动发送接口 + return True + except Exception as e: + logger.error(f"CAN数据发送失败:{str(e)}") + return False + + def recv(self, timeout: float = 0.1, **kwargs) -> Optional[bytes]: + if not self.is_connected: + logger.error("CAN驱动未连接,接收失败") + return None + + try: + # 模拟接收 + time.sleep(0.001) + # mock_data = b"\x11\x22\x33\x44" + # logger.debug(f"CAN接收数据:{mock_data.hex()}") + # return mock_data + return None + except Exception as e: + logger.error(f"CAN数据接收失败:{str(e)}") + return None \ No newline at end of file diff --git a/midware/drivers/bus_uart.py b/midware/drivers/bus_uart.py new file mode 100644 index 0000000..0edb1cc --- /dev/null +++ b/midware/drivers/bus_uart.py @@ -0,0 +1,131 @@ +""" +Tan mingyan +2026/3/9 + +uart 驱动示例(bus_can.py)—— 补充参考 +""" +# midware/drivers/bus_can.py +from midware.config.base_config import get_device_config +from .base_driver import BaseBusDriver +from typing import Optional, Dict, Any +from loguru import logger +import time + +from .base_driver import BaseBusDriver +from .renode_agent import renode +from midware.config.base_config import DEVICE_CONFIG_DICT + +class BusUARTDriver(BaseBusDriver): + """uart总线驱动实现(适配)""" + def _init_config(self): + self.config.setdefault("channel", 0) # CAN通道 + self.config.setdefault("baudrate", 500000) # 500kbps + self.config.setdefault("frame_format", "standard") # 标准帧/扩展帧 + logger.info(f"UART驱动配置初始化完成:{self.config}") + + def connect(self) -> bool: + try: + # 实际逻辑:打开CAN通道 + time.sleep(0.05) + self.is_connected = True + logger.info("CAN总线连接成功") + return True + except Exception as e: + logger.error(f"CAN总线连接失败:{str(e)}") + return False + + def disconnect(self) -> bool: + try: + self.is_connected = False + logger.info("CAN总线断开成功") + return True + except Exception as e: + logger.error(f"CAN总线断开失败:{str(e)}") + return False + + def send(self, data: bytes, **kwargs) -> bool: + if not self.is_connected: + logger.error("UART驱动未连接,发送失败") + return False + # 查找当前设备的配置 + + # ========== 核心步骤1:从kwargs提取设备名称 ========== + + dev_name = kwargs.get("dev_name")#2026/4/10 + if not dev_name: + logger.error("UART发送失败:kwargs中未传入dev_name(设备名称)") + return False + + device_config = get_device_config().get(dev_name) + if not device_config: + logger.error(f"UART发送失败:设备 {dev_name} 未在DEVICE_CONFIG_DICT中配置") + return False + + try: + #frame_id = kwargs.get("frame_id", 0x123) # CAN帧ID + #is_extended = kwargs.get("is_extended", False) # 是否扩展帧 + logger.info( + #f"UART发送数据|帧ID:0x{frame_id:X}|扩展帧:{is_extended}|" + f"UART发送数据:{data.hex()}|长度:{len(data)}字节" + ) + ## 外设名称+函数名+字符串参数 uart24 WriteRXFIFODataString "0xEB9000C571000827C00000BC1003190000106C02AC020101FF5716"' + + sysbus_cmd = f'{dev_name} WriteRXFIFODataString "0x{data.hex()}"' + + #sysbus_cmd = f'can_a GetTxBufferDataString' + print(sysbus_cmd) + logger.info(f"UART发送指令:{sysbus_cmd}") + response = renode.send_sync(sysbus_cmd,timeout_ms=20)#2026/4/10 + #response = renode.enqueue_cmd(sysbus_cmd)#2026/6/4 + # sysbus_cmd = f'uart24 GetTXFIFODataString' + # print(sysbus_cmd) + # response = renode.send_sync(sysbus_cmd)#2026/4/10 + # 实际逻辑:调用UART驱动发送接口 + return True + except Exception as e: + logger.error(f"UART数据发送失败:{str(e)}") + return False + + def recv(self, timeout: float = 0.1, **kwargs) -> Optional[bytes]: + if not self.is_connected: + logger.error("UART驱动未连接,接收失败") + return None + + try: + + #遍历所有UART外设,循环发送读取指令:例如 adc1 ReadAllChannelsToString02 + + # 模拟接收 + time.sleep(0.91) + # mock_data = b"\x11\x22\x33\x44" + # logger.debug(f"CAN接收数据:{mock_data.hex()}") + #logger.info(f"CAN接收数据:") + # return mock_data + ''' + mock_data = b"\x11\x22\x33\x44" + logger.debug(f"AD接收数据:{mock_data.hex()}") + + #2026/4/10 + dev_name = kwargs.get("dev_name") + if not dev_name: + logger.error("AD接收失败:kwargs中未传入dev_name(设备名称)") + return None + + cfg = DEVICE_CONFIG_DICT[dev_name] + base = int(cfg["内存基地址"], 16) + offset = int(cfg["内存偏移地址"], 16) + addr = base + offset + + # ====================== + # 调用 Renode 读取数据 + # ====================== + cmd = f"read 0x{addr:X}" + hex_str = renode.send_sync(cmd) + #return bytes.fromhex(hex_str.strip()) + logger.info(f"AD接收数据:{mock_data.hex()}") + #return mock_data + ''' + return None + except Exception as e: + logger.error(f"UART数据接收失败:{str(e)}") + return None \ No newline at end of file diff --git a/midware/drivers/renode_agent.py b/midware/drivers/renode_agent.py new file mode 100644 index 0000000..3bfb468 --- /dev/null +++ b/midware/drivers/renode_agent.py @@ -0,0 +1,528 @@ +""" +2026/4/10 +TMY +在connect_renode_19.py的基础上,添加单例模式,并添加总线类型映射 +""" +# midware/drivers/renode_agent.py +import threading +import binascii +import subprocess +import time +import threading +import re +import statistics +from typing import Optional, Callable, List, Dict +from loguru import logger + +# ====================== +# 这里放你原来 connect_renode_19.py 的全部代码 +# 只加 2 个东西:单例 + 总线类型映射 +# ====================== +# from . import DRIVER_MAPPING +# from midware.config.base_config import DEVICE_CONFIG_DICT + +BUS_TYPE_MAP = { + 0x11: "UART", + 0x22: "CAN", + 0x33: "1553B", + 0x44: "NET", + 0x55: "AD", + 0x66: "OC", +} +# 合法的总线类型第一个字节(字符串形式,方便判断) +LEGAL_BUS_PREFIX = {"11", "22", "33", "44", "55", "66"} + +class RenodeAgent: + _instance = None + _lock = threading.Lock() + + def __new__(cls, *args, **kwargs): + with cls._lock: + if cls._instance is None: + cls._instance = super().__new__(cls) + return cls._instance + + def __init__(self, + renode_path="renode", + script_path=None, + max_queue=50, # 指令队列最大长度(限流) + reconnect_retries=5, # 自动重连次数 + socket_timeout=3): + if hasattr(self, "inited"): + return + self.inited = True + + # ========= 你的原有代码 ========= + # self.process = ... + # self.async_queue = ... + # self.start_renode() + # self.start_async_listener() + # 基础配置 + self.renode_path = renode_path + self.script_path = script_path + self.process: Optional[subprocess.Popen] = None + self.lock = threading.Lock() + + # 输出与异步 + self.output_buffer: List[str] = [] + self.async_running = False + self.async_callback: Optional[Callable[[str], None]] = None + + # 限流队列 + self.max_queue = max_queue + self.cmd_queue: List[str] = [] + self.busy = False + + # 自动重连 + self.reconnect_retries = reconnect_retries + self.connected = False + + # 时延统计 + self.latency_stats: Dict[str, List[float]] = { + "send": [], "process": [], "total": [] + } + + # ====================== 超强过滤规则 ====================== + self.filter_re = re.compile( + r"\(.*?\)" # 过滤 (machine) (TestPlatform) 等 + r"|\d{2}:\d{2}:\d{2}\.\d+" # 过滤时间 20:15:26.1234 + r"|renode>|\$|->" # 过滤提示符 + r"|\x1b\[[0-9;]*m" # 过滤颜色码 + r"|^\s*$" # 过滤空行 + ) + + self.last_send_t =0.0 + self.send_min_gap = 0.01 #控制下发间隔 + + ''' + # 你原来的函数,完全不变 + def send_sync(self, cmd: str, timeout=1.0): + # 你的原有逻辑 + pass + + def enqueue_cmd(self, cmd: str): + # 你的原有逻辑 + pass + + def start_async_listener(self, callback): + # 你的原有异步监听 + pass + ''' + def start(self) -> bool: + try: + args = [ + self.renode_path, + "--console", + #"--disable-ansi-colors", + "-e", "set echo off; set line-editing off; set raw on" + ] + if self.script_path: + args.extend(["-e", f"include @{self.script_path}"]) + #args.extend(["-e", f"{self.script_path}"]) + + self.process = subprocess.Popen( + args, + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + bufsize=0, + universal_newlines=True, + creationflags=subprocess.CREATE_NEW_CONSOLE #LWB + ) + + self.async_running = True + self.connected = True + threading.Thread(target=self._read_thread, daemon=True).start() + threading.Thread(target=self._queue_worker, daemon=True).start() + time.sleep(1) + self.clear_buffers() + print("✅ 仿真平台 启动成功") + + sysbus_cmd = "logLevel 3" #3级-只显示error + response = renode.send_sync(sysbus_cmd) + + return True + + except Exception as e: + print(f"❌ 启动失败: {e}") + self.connected = False + return self._try_reconnect() + + # ------------------------------------------------------------------------- + # 后台读取线程(永远实时读取,永不堆积) + # ------------------------------------------------------------------------- + def _read_thread(self): + while self.async_running and self.process.poll() is None: + try: + line = self.process.stdout.readline() + if not line: + time.sleep(0.001) + continue + + cleaned = self.filter_line(line) + if not cleaned: + continue + + with self.lock: + self.output_buffer.append(cleaned) + + if self.async_callback: + self.async_callback(cleaned) + + except Exception: + break + + self.connected = False + print("⚠️ Renode 已断开") + self._try_reconnect() + + # ------------------------------------------------------------------------- + # 过滤垃圾输出 + # ------------------------------------------------------------------------- + def filter_line(self, line: str) -> Optional[str]: + res = self.filter_re.sub("", line).strip() + return res if res else None + + + # ------------------------------------------------------------------------- + # 同步发送(带时延测量 + 限流), + # ------------------------------------------------------------------------- + def send_sync( + self, + cmd: str, + timeout_ms: int = 20 + ) -> tuple[Optional[str], float, float, float]: + if not self.connected or not self.process: + return None, 0, 0, 0 + + # 限流:队列满则等待 + while len(self.cmd_queue) >= self.max_queue: + time.sleep(0.001) + + # self.clear_buffers() #同步发送不再清空全局输出缓冲区,缓冲区只有一部读取线程消费 2026/6/3 + t0 = time.perf_counter() + + # 发送 + try: + self.process.stdin.write(f"{cmd}\n") + self.process.stdin.flush() + except: + return None, 0, 0, 0 + + t1 = time.perf_counter() + + # 等待结果 + timeout = time.time() + timeout_ms / 1000 + while time.time() < timeout: + with self.lock: + if self.output_buffer: + res = "\n".join(self.output_buffer) + self.output_buffer.clear() + t2 = time.perf_counter() + + send_lat = (t1 - t0) * 1000 + proc_lat = (t2 - t1) * 1000 + total_lat = (t2 - t0) * 1000 + + self.latency_stats["send"].append(send_lat) + self.latency_stats["process"].append(proc_lat) + self.latency_stats["total"].append(total_lat) + + return res, send_lat, proc_lat, total_lat + time.sleep(0.001) + + return None, 0, 0, 0 + + ''' + # ------------------------------------------------------------------------- + # 同步发送(有时延测量 + 限流,不等待回复), + # 2026/6/4 + # ------------------------------------------------------------------------- + def send_sync( + self, + cmd: str, + timeout_ms: int = 20 + ) -> tuple[Optional[str], float, float, float]: + if not self.connected or not self.process: + return None,0,0,0 + #队列限流等待 + while len(self.cmd_queue) >=self.max_queue: + time.sleep(0.001) + t0 = time.perf_counter() + + try: + self.process.stdin.write(f"{cmd}\n") + self.process.stdin.flush() + + except: + return None,0,0,0 + + t1 = time.perf_counter() + + #【关键改动:不在阻塞轮询收应答,直接退出;所有应答给dispacher消费】 + send_lat = (t1 - t0)*1000 + + return None,send_lat, 0 , send_lat + ''' + # ------------------------------------------------------------------------- + # 异步监听 + # ------------------------------------------------------------------------- + def start_async_listener(self, callback: Callable[[str], None]): + self.async_callback = callback + + def clear_buffers(self): + with self.lock: + self.output_buffer.clear() + + # ------------------------------------------------------------------------- + # 指令队列限流(后台异步发送) + # ------------------------------------------------------------------------- + def enqueue_cmd(self, cmd: str): + if len(self.cmd_queue) < self.max_queue: + self.cmd_queue.append(cmd) + ''' + def _queue_worker(self): #老版本,停用 2026、6、4 + while self.async_running: + if not self.connected or self.busy or not self.cmd_queue: + time.sleep(0.001) + continue + + self.busy = True + cmd = self.cmd_queue.pop(0) + self.send_sync(cmd, timeout_ms=1) #150->1 + self.busy = False + ''' + ''' + def _queue_worker(self): #新版本,2026、6、4 + while self.async_running: + if not self.connected or not self.cmd_queue: + time.sleep(0.001) + continue + + cmd = self.cmd_queue.pop(0) + self.send_sync(cmd) #150->1 + logger.info(f" _queue_worker向管道发送数据:{cmd}") + ''' + def _queue_worker(self): #新版本,2026、6、4 + while self.async_running: + now = time.time() + #时间没有到,让出CPU + if now - self.last_send_t < self.send_min_gap: + time.sleep(0.0005) + continue + if not self.connected or not self.cmd_queue: + time.sleep(0.001) + continue + + cmd = self.cmd_queue.pop(0) + self.send_sync(cmd) #150->1 + logger.info(f" _queue_worker向管道发送数据:{cmd}") + # ------------------------------------------------------------------------- + # 自动重连 + # ------------------------------------------------------------------------- + def _try_reconnect(self) -> bool: + print(f"🔌 尝试自动重连 ({self.reconnect_retries} 次)") + for i in range(self.reconnect_retries): + self.stop() + time.sleep(1) + if self.start(): + print(f"✅ 第 {i+1} 次重连成功") + return True + time.sleep(1) + print("❌ 重连全部失败") + return False + + # ------------------------------------------------------------------------- + # 时延统计 + # ------------------------------------------------------------------------- + def get_latency_report(self) -> Dict[str, float]: + def stats(arr): + if not arr: return 0,0,0 + return round(statistics.mean(arr),2), round(max(arr),2), round(min(arr),2) + return { + "send_avg,max,min": stats(self.latency_stats["send"]), + "process_avg,max,min": stats(self.latency_stats["process"]), + "total_avg,max,min": stats(self.latency_stats["total"]), + } + + def clear_latency(self): + for k in self.latency_stats: + self.latency_stats[k].clear() + + # ------------------------------------------------------------------------- + # 停止 + # ------------------------------------------------------------------------- + def stop(self): + self.async_running = False + self.connected = False + try: + if self.process: + self.process.stdin.write("quit\n") + self.process.stdin.flush() + time.sleep(0.2) + self.process.terminate() + except: + pass + self.process = None + print("🔌 Renode 已停止") +# midware/drivers/renode_agent.py + + +# 第三步:异步数据分发(完全不影响你现有架构) +#在 renode_agent.py 里加全局分发回调,直接把数据投递到你现有的驱动 recv +# def renode_async_data_dispatcher(line: str): +# try: +# # 1. 转成字节 + +# byte_data = binascii.unhexlify(line.strip()) +# print(byte_data) + +# # 2. 解析头部 3 字节 +# if len(byte_data) < 3: +# return +# bus_type_id = byte_data[0] +# machine_type = byte_data[1] +# machine_id = byte_data[2] +# payload = byte_data[3:] + +# # 3. 映射协议类型 +# protocol = BUS_TYPE_MAP.get(bus_type_id) +# if not protocol: +# return + +# # 4. 找到对应设备(根据单机编号) +# dev_name = None +# for name, cfg in DEVICE_CONFIG_DICT.items(): +# if cfg.get("协议类型") == protocol and cfg.get("单机编号") == machine_id: +# dev_name = name +# break +# if not dev_name: +# return + +# # 5. 把数据 **直接放入你现有的总线接收队列** +# from midware.core.bus_udp_decoupler import bus_recv_queues +# q = bus_recv_queues[protocol] +# q.put({ +# "dev_name": dev_name, +# "raw_data": payload, +# "protocol": protocol, +# }) + +# except Exception: +# pass + + +def renode_async_data_dispatcher(line: str): + try: + # ----------------------- + # 🔥 延迟导入(放在函数内部,不会循环引用!) + # ----------------------- + from midware.core.bus_udp_decoupler import bus_recv_queues + from midware.config.base_config import DEVICE_CONFIG_DICT + + # print(f"异步监听到的数据{line}") + logger.info( + f"异步监听到的数据{line}" + ) + # ----------------------- + # 0.1. 基础过滤 + # ----------------------- + line = line.strip() + if len(line) < 6: # 至少3字节 = 6个十六进制字符 + return + + # ----------------------- + # 0.2. 【关键】判断前2个字符是否是合法总线类型 + # ----------------------- + first_byte_str = line[:2] + if first_byte_str not in LEGAL_BUS_PREFIX: + # 不是我们要的总线数据 → 直接丢弃 + return + + # ----------------------- + # 0.3. 判断整个字符串是否是合法十六进制(安全转换) + # ----------------------- + def is_hex(s): + try: + int(s, 16) + return True + except: + return False + + if not is_hex(line): + return + + # 1. 把 Renode 返回的字符串转成字节,首先判断首字符是否为 + byte_data = bytes.fromhex(line.strip()) + + if len(byte_data) < 3: + return + + # 2. 解析 3 字节头部 + bus_type_id = byte_data[0] + machine_type = byte_data[1] + machine_id = byte_data[2] + payload = byte_data[3:] + + # 3. 映射协议名 + protocol = BUS_TYPE_MAP.get(bus_type_id) + if not protocol: + return + + # 4. 根据 协议类型 + 单机编号 → 找到设备名 + dev_name = None + for name, cfg in DEVICE_CONFIG_DICT.items(): + if (cfg.get("protocol") == protocol and + cfg.get("number") == machine_id): + dev_name = name + break + + if not dev_name: + return + + # 5. 放入你现有的接收队列(完全兼容你原来架构) + q = bus_recv_queues[protocol] + try: + q.put_nowait({ + "dev_name": dev_name, + "raw_data": payload, + "protocol": protocol, + }) + except: + pass + + except Exception: + import traceback + traceback.print_exc() + + +# 在 renode_agent.py 末尾加这个函数 +def start_renode_uart_redirect(): + from midware.config.base_config import DEVICE_CONFIG_DICT + renode = RenodeAgent() # 单例 + + for dev_name, cfg in DEVICE_CONFIG_DICT.items(): + tcp_local = cfg.get("tcp_local_port", 1) + tcp_bus = cfg.get("tcp_bus_port", 1) + + if tcp_local != 1 and tcp_bus != 1: + term_name = f"term_{dev_name}" + cmd1 = f'emulation CreateServerSocketTerminal {tcp_bus} "{term_name}"'#临时注释2026/6/2 + cmd2 = f'connector Connect sysbus.{dev_name} {term_name}' + + print(f"[Renode] 配置UART重定向: {cmd1}") + renode.send_sync(cmd1, timeout_ms=2) + time.sleep(0.1) + + print(f"[Renode] 连接外设: {cmd2}") + renode.send_sync(cmd2, timeout_ms=2) + time.sleep(0.2) + print("[Renode] UART重定向配置完成!") + +# 全局单例,全局唯一 +renode = RenodeAgent( + renode_path = r"E:\simulation\1_simulation\卫星仿真平台_后端.exe", #r"E:\codes\1_simulation\卫星仿真平台_后端.exe", #r"C:\Users\PingCe\Desktop\1_simulation\卫星仿真平台_后端.exe", # Windows可写绝对路径如 C:/Renode/renode.exe + script_path = r'E:\simulation\1_simulation\generated.resc',#r'E:\codes\1_simulation\generated_771_ADC1.resc', #r'include @C:\Users\PingCe\Desktop\1_simulation\generated_771_ADC1.resc', + max_queue=25, # 最多同时排队5条指令,防止压爆 + reconnect_retries=5 + ) \ No newline at end of file diff --git a/midware/drivers/tcp_uart_agent.py b/midware/drivers/tcp_uart_agent.py new file mode 100644 index 0000000..1641fcb --- /dev/null +++ b/midware/drivers/tcp_uart_agent.py @@ -0,0 +1,226 @@ +from queue import Full +import socket +import threading +import time +from typing import Dict, Optional +from loguru import logger + +import telnetlib #2026/6/2 + +# 全局:TCP客户端管理(每个UART一个实例) +tcp_clients: Dict[str, "TCPUARTClient"] = {} +# HEAD = b"\xeb\x90" #地测帧头 +# TAIL = b"\x57\x16" #地测帧尾 +# MIN_FRAME = 23 ##地测长度最小值 +# MAX_FRAME = 1000 ##地测帧长度最大值 + +class TCPUARTClient: + def __init__(self, dev_name: str, local_port: int, remote_port: int, remote_ip="127.0.0.1"): + self.dev_name = dev_name + self.local_port = local_port + self.remote_port = remote_port + self.remote_ip = remote_ip + self.socket = None + self.running = False + self.thread = None + + self.HEAD = b"\xeb\x90" #地测帧头 + self.TAIL = b"\x57\x16" #地测帧尾 + self.MIN_LEN = 23 ##地测长度最小值 + self.MAX_LEN = 1000 ##地测帧长度最大值 + self.tcp_buf = b'' + + #self.tn = None + + + def connect(self): + try: + + self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + # 关键点:禁止Nagle算法,模仿网络调试助手,强制使用纯raw socket连接,但是导致TCP收不到数据 2026/6/2 + self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) #停用 + #self.socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) #telnet + self.socket.bind(("127.0.0.1", self.local_port)) + self.socket.connect((self.remote_ip, self.remote_port)) + self.socket.setblocking(False) + + self.running = True + print(f"[TCP-UART] {self.dev_name} 连接成功: local={self.local_port} remote={self.remote_port}") + + #telnet + #self.tn = telnetlib.Telnet("127.0.0.1",{self.remote_port},timeout=5) + + + return True + except Exception as e: + print(f"[TCP-UART] {self.dev_name} 连接失败: {e}") + return False + ''' + # 去除转义FF的函数 2026/6/2,可能有安全问题 + def unescape_ff(raw:bytes)->bytes: + buf = bytearray() + i = 0 + n = len(raw) + while ibytes: + out = bytearray() + idx=0 + length = len(raw) + while idx8192 2026/5/15 + if not data: + time.sleep(0.001) + continue + self.tcp_buf += data #新数据追加到缓存尾部2026/6/2 + + dev_name = None + protocol = None + for name, cfg in DEVICE_CONFIG_DICT.items(): + if cfg.get("tcp_bus_port") == self.remote_port: + dev_name = name + protocol = cfg.get("protocol") + break + + if not dev_name or not protocol: + continue + + #循环拆包,大多数情况是只跑0~1次循环 2026/6/2 + while True: + if len(self.tcp_buf)>self.MAX_LEN * 2: + self.tcp_buf = self.tcp_buf[-1000:] + h_pos = self.tcp_buf.find(self.HEAD) + if h_pos ==-1: + break + #截断帧头前面垃圾 + if h_pos>0: + self.tcp_buf = self.tcp_buf[h_pos:] + #缓存不足最小帧,直接退出 + if(len(self.tcp_buf) None: + """处理UART协议数据""" + logger.info(f"[UART处理] 外设:{data.get('dev_name', '未知')} | 端口:{data.get('port', '未知')} | 数据长度:{len(data.get('raw_data', b''))}字节") + # TODO: 补充UART数据解析、转发、故障注入等业务逻辑 + + pass + +def handle_sync(data: Dict[str, Any]) -> None: + """处理SYNC(同步)协议数据""" + logger.info(f"[SYNC处理] 外设:{data.get('dev_name', '未知')} | 端口:{data.get('port', '未知')} | 数据长度:{len(data.get('raw_data', b''))}字节") + # TODO: 补充SYNC同步数据解析、时序控制等业务逻辑 + pass + +def handle_can(data: Dict[str, Any]) -> None: + """处理CAN协议数据""" + logger.info(f"[CAN处理] 外设:{data.get('dev_name', '未知')} | 端口:{data.get('port', '未知')} | 数据长度:{len(data.get('raw_data', b''))}字节") + # TODO: 补充CAN帧解析、总线转发等业务逻辑 + pass + +def handle_1553b(data: Dict[str, Any]) -> None: + """处理1553B协议数据""" + logger.info(f"[1553B处理] 外设:{data.get('dev_name', '未知')} | 端口:{data.get('port', '未知')} | 数据长度:{len(data.get('raw_data', b''))}字节") + # TODO: 补充1553B总线数据解析、RT/BC交互等业务逻辑 + pass + +def handle_ad(data: Dict[str, Any]) -> None: + """处理AD(模拟量输入)协议数据""" + logger.info(f"[AD处理] 外设:{data.get('dev_name', '未知')} | 端口:{data.get('port', '未知')} | 数据长度:{len(data.get('raw_data', b''))}字节") + # TODO: 补充AD采样值解析、校准、越限告警等业务逻辑 + pass + +def handle_oc(data: Dict[str, Any]) -> None: + """处理OC(数字量输出)协议数据""" + logger.info(f"[OC处理] 外设:{data.get('dev_name', '未知')} | 端口:{data.get('port', '未知')} | 数据长度:{len(data.get('raw_data', b''))}字节") + # TODO: 补充OC通道控制、状态反馈、故障模拟等业务逻辑 + pass + +def handle_network(data: Dict[str, Any]) -> None: + """处理网络协议数据""" + logger.info(f"[网络处理] 外设:{data.get('dev_name', '未知')} | 端口:{data.get('port', '未知')} | 数据长度:{len(data.get('raw_data', b''))}字节") + # TODO: 补充网络包转发、IP/TCP解析、网络故障注入等业务逻辑 + pass + +def handle_unknown_protocol(data: Dict[str, Any]) -> None: + """处理未知协议数据(容错兜底)""" + unknown_protocol = data.get('protocol', '未知') + logger.warning( + f"[未知协议] 外设:{data.get('dev_name', '未知')} | 端口:{data.get('port', '未知')} | " + f"协议类型:{unknown_protocol} | 支持的协议列表:{SUPPORTED_PROTOCOLS}" + ) + +# ==================== 核心分发函数(对外暴露的唯一入口)==================== +def dispatch_udp_data(recv_data: List[Dict[str, Any]]) -> None: + """ + 核心分发函数:遍历recv_data,按protocol分发到对应处理函数 + :param recv_data: udp_server.recv_multi_udp_hex()返回的原始数据列表,每条数据格式: + {"port": 端口号, "dev_name": 外设名, "protocol": 协议类型, "raw_data": 原始二进制数据} + """ + # 第一步:校验输入数据类型 + if not isinstance(recv_data, list): + logger.error("分发失败:输入的recv_data不是列表类型") + return + + # 第二步:空数据直接返回(避免无意义日志) + if not recv_data: + return + + # 第三步:构建协议-处理函数映射表(核心:新增/修改协议只需改此表) + protocol_handler_map = { + PROTOCOL_UART: handle_uart, + PROTOCOL_SYNC: handle_sync, + PROTOCOL_CAN: handle_can, + PROTOCOL_1553B: handle_1553b, + PROTOCOL_AD: handle_ad, + PROTOCOL_OC: handle_oc, + PROTOCOL_NETWORK: handle_network + } + + # 第四步:遍历每条数据,按协议分发处理 + for single_data in recv_data: + try: + # 容错:确保protocol字段存在且为字符串 + protocol = single_data.get("protocol", "").strip().upper() + port = single_data["port"] + dev_name = single_data["dev_name"] + # protocol = data["protocol"] + raw_data = single_data["raw_data"] + + #根据dev_name,获取内存基地址,偏移地址,发送缓存区大小,接收缓存区大小 + #dev_info = DEVICE_CONFIG_DICT.get(dev_name, {}) + if dev_name not in base_config.DEVICE_CONFIG_DICT: + logger.error(f"UART外设不存在:{dev_name}") + return False + + # 获取外设基地址、偏移地址 + base_addr = base_config.DEVICE_CONFIG_DICT[device_name]["base_addr"] + offset_addr = base_config.DEVICE_CONFIG_DICT[device_name]["offset_addr"] + print(f"sysbus WriteDoubleWord {base_addr + offset_addr} {raw_data}") + + # 兼容协议名大小写(如"can"→"CAN"、"sync"→"SYNC") + if protocol in protocol_handler_map: + # 调用对应处理函数 + protocol_handler_map[protocol](single_data) + else: + # 未知协议调用兜底函数 + handle_unknown_protocol(single_data) + + except Exception as e: + # 单条数据处理异常不影响整体,记录错误日志 + logger.error( + f"[数据处理异常] 外设:{single_data.get('dev_name', '未知')} | 错误信息:{str(e)}", + exc_info=True + ) + +# ==================== 测试用例(验证分发逻辑是否正常)==================== +def test_dispatcher(): + """模拟udp_server.recv_multi_udp_hex()返回的数据,测试分发逻辑""" + # 模拟接收数据 + mock_recv_data = [ + {"port": 8880, "dev_name": "Uart0", "protocol": "uart", "raw_data": b"UART_TEST_DATA"}, + {"port": 8881, "dev_name": "Sync0", "protocol": "SYNC", "raw_data": b"SYNC_TEST_DATA"}, + {"port": 8882, "dev_name": "Can1", "protocol": "Can", "raw_data": b"CAN_TEST_DATA"}, + {"port": 8883, "dev_name": "1553B0", "protocol": "1553B", "raw_data": b"1553B_TEST_DATA"}, + {"port": 8884, "dev_name": "AD0", "protocol": "ad", "raw_data": b"AD_TEST_DATA"}, + {"port": 8885, "dev_name": "OC0", "protocol": "OC", "raw_data": b"OC_TEST_DATA"}, + {"port": 8886, "dev_name": "Net0", "protocol": "网络", "raw_data": b"NETWORK_TEST_DATA"}, + {"port": 8887, "dev_name": "Unknown0", "protocol": "SPI", "raw_data": b"UNKNOWN_TEST_DATA"}, + ] + # 执行分发 + logger.info("开始测试协议分发逻辑...") + dispatch_udp_data(mock_recv_data) + logger.info("协议分发测试完成") + +if __name__ == "__main__": + # 运行测试用例,验证分发逻辑 + test_dispatcher() \ No newline at end of file diff --git a/midware/network/udp_handler.py b/midware/network/udp_handler.py new file mode 100644 index 0000000..f951059 --- /dev/null +++ b/midware/network/udp_handler.py @@ -0,0 +1,394 @@ +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 \ No newline at end of file diff --git a/midware/protocol/__init__.py b/midware/protocol/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/midware/protocol/ad_protocol.py b/midware/protocol/ad_protocol.py new file mode 100644 index 0000000..e69de29 diff --git a/midware/protocol/b1553b_protocol.py b/midware/protocol/b1553b_protocol.py new file mode 100644 index 0000000..e69de29 diff --git a/midware/protocol/can_protocol.py b/midware/protocol/can_protocol.py new file mode 100644 index 0000000..e69de29 diff --git a/midware/protocol/oc_protocol.py b/midware/protocol/oc_protocol.py new file mode 100644 index 0000000..e69de29 diff --git a/midware/protocol/uart_protocol.py b/midware/protocol/uart_protocol.py new file mode 100644 index 0000000..bd91dec --- /dev/null +++ b/midware/protocol/uart_protocol.py @@ -0,0 +1,69 @@ +''' +4. UART 协议模块(midware/protocol/uart_protocol.py) +实现 UART 协议核心逻辑:寄存器配置、FIFO 缓冲区写入 / 读取、校验和检测、日志记录,严格遵循需求中的字节操作规则: +''' +import struct +from loguru import logger +from midware.config.base_config import BUS_CONFIG, UART_CHIP_CONFIG, DEVICE_CONFIG_DICT +from midware.bus.cache_handler import CacheHandler +from midware.core.error_check import check_checksum + +class UARTProtocol: + def __init__(self): + self.cache_handler = CacheHandler() # 缓存交互实例 + self.write_byte = BUS_CONFIG["WRITE_BYTE_UART"] # 每次写入2字节 + self.read_byte = BUS_CONFIG["WRITE_BYTE_UART"] # 每次读取2字节 + + def write_uart_fifo(self, device_name, raw_data): + """将原始数据写入虚拟芯片FIFO缓冲区,按UART协议每次2字节""" + if device_name not in DEVICE_CONFIG_DICT: + logger.error(f"UART外设不存在:{device_name}") + return False + # 获取外设基地址、偏移地址 + base_addr = DEVICE_CONFIG_DICT[device_name]["base_addr"] + offset_addr = DEVICE_CONFIG_DICT[device_name]["offset_addr"] + # 校验和检测 + if not check_checksum(raw_data, "UART", device_name): + return False + # 不定长数据分块,每次2字节写入 + data_len = len(raw_data) + for i in range(0, data_len, self.write_byte): + # 截取2字节,不足补0 + chunk = raw_data[i:i+self.write_byte] + if len(chunk) < self.write_byte: + chunk += b'\x00' * (self.write_byte - len(chunk)) + # 计算内存地址:基地址+偏移地址 + mem_addr = base_addr + offset_addr + UART_CHIP_CONFIG["TBR"] + # 写入共享缓存(sysbus WriteDoubleWord) + self.cache_handler.write_double_word(mem_addr, struct.unpack(">H", chunk)[0]) + logger.debug(f"UART外设{device_name}写入FIFO完成,数据长度:{data_len}") + return True + + def read_uart_fifo(self, device_name): + """从虚拟芯片FIFO缓冲区读取数据,按UART协议每次2字节,组成完整帧""" + if device_name not in DEVICE_CONFIG_DICT: + logger.error(f"UART外设不存在:{device_name}") + return b"" + # 获取外设基地址、偏移地址 + base_addr = DEVICE_CONFIG_DICT[device_name]["base_addr"] + offset_addr = DEVICE_CONFIG_DICT[device_name]["offset_addr"] + # 读取FIFO剩余字节数 + remain_addr = base_addr + offset_addr + UART_CHIP_CONFIG["FIFO_REMAIN"] + remain_bytes = self.cache_handler.read_double_word(remain_addr) + if remain_bytes == 0: + logger.debug(f"UART外设{device_name}FIFO缓冲区无数据") + return b"" + # 分块读取,每次2字节 + raw_data = b"" + for _ in range(0, remain_bytes, self.read_byte): + mem_addr = base_addr + offset_addr + UART_CHIP_CONFIG["TBR"] + # 从共享缓存读取(sysbus ReadDoubleWord) + data = self.cache_handler.read_double_word(mem_addr) + raw_data += struct.pack(">H", data) + # 去除补0的空字节,得到完整帧 + raw_data = raw_data.rstrip(b'\x00') + logger.debug(f"UART外设{device_name}读取FIFO完成,数据长度:{len(raw_data)}") + return raw_data + +# 全局UART协议实例 +uart_protocol = UARTProtocol() \ No newline at end of file diff --git a/midware/ui/__init__.py b/midware/ui/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/midware/ui/__pycache__/__init__.cpython-311.pyc b/midware/ui/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000..5f62ef3 Binary files /dev/null and b/midware/ui/__pycache__/__init__.cpython-311.pyc differ diff --git a/midware/ui/__pycache__/main_ui.cpython-311.pyc b/midware/ui/__pycache__/main_ui.cpython-311.pyc new file mode 100644 index 0000000..490a155 Binary files /dev/null and b/midware/ui/__pycache__/main_ui.cpython-311.pyc differ diff --git a/midware/ui/main_ui.py b/midware/ui/main_ui.py new file mode 100644 index 0000000..26b2269 --- /dev/null +++ b/midware/ui/main_ui.py @@ -0,0 +1,94 @@ +'''6. 简易 UI 模块(midware/ui/main_ui.py) +基于 tkinter 实现基础主界面,包含配置文件导入、外设配置表、数据传输状态、原始数据查看,满足用户界面需求: +''' + +import tkinter as tk +from tkinter import ttk, filedialog, messagebox +import os +from loguru import logger +from midware.config.excel_config import load_csv_config +from midware.config.base_config import DEVICE_CONFIG_DICT, BUS_CONFIG + +def load_config_file(): + """导入Excel配置文件""" + file_path = filedialog.askopenfilename( + title="选择外设配置文件", + filetypes=[("Excel文件", "*.xlsx;*.xls"), ("所有文件", "*.*")] + ) + if not file_path: + return + try: + global DEVICE_CONFIG_DICT + DEVICE_CONFIG_DICT = load_csv_config(file_path) + # 刷新外设配置表 + refresh_device_table() + messagebox.showinfo("成功", f"导入配置文件成功,共{len(DEVICE_CONFIG_DICT)}个外设") + logger.info(f"手动导入配置文件:{os.path.basename(file_path)}") + except Exception as e: + messagebox.showerror("失败", f"导入配置文件失败:{str(e)}") + logger.error(f"导入配置文件失败:{str(e)}", exc_info=True) + +def refresh_device_table(): + """刷新外设配置表""" + # 清空原有数据 + for item in device_tree.get_children(): + device_tree.delete(item) + # 插入新数据 + for dev_name, dev_info in DEVICE_CONFIG_DICT.items(): + device_tree.insert( + "", tk.END, + values=( + dev_name, + dev_info.get("protocol", ""), + hex(dev_info.get("base_addr", 0)), + hex(dev_info.get("offset_addr", 0)), + dev_info.get("udp_ip", ""), + dev_info.get("udp_port", ""), + dev_info.get("desc", ""), + dev_info.get("send_cache", ""), + dev_info.get("recv_cache", "") + ) + ) + +def show_raw_data(): + """查看原始数据(预留接口,后续实现时间排序展示)""" + messagebox.showinfo("提示", "原始数据查看功能开发中,将按时间排序展示所有单机数据") + +def start_main_ui(): + """启动主界面""" + root = tk.Tk() + root.title("虚拟仿真平台中间层软件") + root.geometry("1200x600") + root.resizable(True, True) + + # 顶部按钮栏 + top_frame = ttk.Frame(root, padding="10") + top_frame.pack(fill=tk.X) + ttk.Button(top_frame, text="导入Excel配置", command=load_config_file).grid(row=0, column=0, padx=5) + ttk.Button(top_frame, text="查看原始数据", command=show_raw_data).grid(row=0, column=1, padx=5) + + # 外设配置表 + global device_tree + columns = ("名称", "协议", "基地址", "偏移地址", "UDPIP", "UDPPort", "说明", "发送缓存", "接收缓存") + device_tree = ttk.Treeview(root, columns=columns, show="headings", height=20) + # 设置列标题 + for col in columns: + device_tree.heading(col, text=col) + device_tree.column(col, width=120, anchor=tk.CENTER) + device_tree.pack(fill=tk.BOTH, expand=True, padding="10") + + # 数据传输状态栏 + status_frame = ttk.Frame(root, padding="10") + status_frame.pack(fill=tk.X) + ttk.Label(status_frame, text="数据传输状态:").grid(row=0, column=0) + status_var = tk.StringVar(value="运行中(已连接UDP/缓存)") + ttk.Label(status_frame, textvariable=status_var, foreground="green").grid(row=0, column=1) + + # 初始化配置表 + refresh_device_table() + + # 主循环 + root.mainloop() + +if __name__ == "__main__": + start_main_ui() \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..8633fa3 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,49 @@ +altgraph==0.17.4 +click==8.1.7 +colorama==0.4.6 +crcmod==1.7 +et_xmlfile==2.0.0 +Jinja2==3.1.4 +loguru==0.7.3 +lxml==5.3.1 +MarkupSafe==2.1.5 +numpy==2.2.3 +openpyxl==3.1.5 +packaging==24.0 +pandas==2.2.3 +pefile==2023.2.7 +prettytable==3.10.0 +pyecharts==2.0.5 +pyinstaller==6.11.1 +pyinstaller-hooks-contrib==2024.10 +PyQt5==5.15.9 +pyqt5-plugins==5.15.9.2.3 +PyQt5-Qt5==5.15.2 +PyQt5-sip==12.13.0 +pyqt5-tools==5.15.9.3.3 +PyQtWebEngine==5.15.6 +PyQtWebEngine-Qt5==5.15.2 +pyserial==3.5 +PySide6==6.8.2.1 +PySide6_Addons==6.8.2.1 +PySide6_Essentials==6.8.2.1 +python-dateutil==2.9.0.post0 +python-docx==1.1.2 +python-dotenv==1.0.1 +pytz==2025.1 +pywin32==308 +pywin32-ctypes==0.2.3 +qt5-applications==5.15.2.2.3 +qt5-tools==5.15.2.1.3 +QtAwesome==1.3.1 +QtPy==2.4.1 +shiboken6==6.8.2.1 +simplejson==3.19.2 +six==1.17.0 +typing_extensions==4.12.2 +tzdata==2025.1 +vboxapi==1.0 +wcwidth==0.2.13 +win32_setctime==1.2.0 +xlrd==2.0.1 +xlwt==1.3.0 diff --git a/tests/test_udp_connect.py b/tests/test_udp_connect.py new file mode 100644 index 0000000..eb77c68 --- /dev/null +++ b/tests/test_udp_connect.py @@ -0,0 +1,22 @@ +import socket +import psutil +from midware.config.base_config import DEVICE_CONFIG_DICT, UDP_CONFIG + +def check_port_listen(ip, port): + """检测指定IP:端口是否被监听""" + for conn in psutil.net_connections(kind='inet'): + if conn.laddr.ip == ip and conn.laddr.port == port and conn.status == 'LISTEN': + return True + return False + +if __name__ == "__main__": + local_ip = UDP_CONFIG["LOCAL_IP"] + # 检测所有外设UDP端口 + print("===== UDP端口监听状态检测 =====") + for dev_name, dev_info in DEVICE_CONFIG_DICT.items(): + port = dev_info["udp_port"] + is_listen = check_port_listen(local_ip, port) + status = "✅ 正常监听" if is_listen else "❌ 未监听" + print(f"{dev_name} | {local_ip}:{port} | {status}") + # 检测故障注入软件对接端口(非监听,为发送套接字) + print(f"\n故障注入软件接收端口:{local_ip}:{UDP_CONFIG['FAULT_INJECT_PORT']}(自测客户端监听)") \ No newline at end of file diff --git a/tests/test_udp_connect_2.py b/tests/test_udp_connect_2.py new file mode 100644 index 0000000..5dd97dc --- /dev/null +++ b/tests/test_udp_connect_2.py @@ -0,0 +1,295 @@ +# 注释掉/删除Linux的shebang行,替换为Windows兼容的写法(可选) +# #!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +故障注入平台UDP模拟客户端 +功能:向中间件多UDP端口发送16进制流数据,模拟故障注入平台的发包行为 +作者:工程测试专用 +日期:2026-02-26 +""" +""" +该程序专门模拟故障注入平台的 UDP 数据发送行为,向中间件的多 UDP 端口发送符合协议格式的 16 进制流数据,可验证中间件udp_handler.py的多端口非阻塞接收、数据解析、端口隔离等核心功能,支持单端口定向发送、多端口并发发送、自定义数据格式,全程适配工程的 16 进制流数据规范。 +一、测试程序核心功能 +模拟故障注入平台向中间件指定 UDP 端口发送 16 进制流数据; +支持单端口循环发送、多端口并发发送(验证端口隔离性); +可自定义测试数据(原始二进制 / 16 进制字符串),自动适配工程数据格式; +实时打印发送日志,包含目标端口、数据长度、发送状态; +支持按协议类型(UART/CAN/1553B/AD/OC)批量发送对应外设数据。 +""" +""" +使用步骤 +步骤 1:配置适配 +修改DEVICE_UDP_PORTS字典,确保与中间件device_config.csv中的外设名称 + UDP 端口完全一致; +确认MIDDLEWARE_IP为中间件运行的 IP(本地测试填127.0.0.1,跨机器测试填中间件所在机器 IP); +调整TEST_CONFIG中的参数: +send_interval:发送间隔(秒),默认 1ms,模拟实时数据; +concurrent_threads:并发线程数,默认 5,可根据机器性能调整; +send_times_per_port:每个端口发送次数,默认 1000 次。 +步骤 2:运行准备 +先启动中间件主程序main.py,确保: +配置初始化成功(日志显示 “加载 X 个外设”); +UDP 服务初始化完成(日志显示 “监听端口:[8880,8881,...]”); +安装依赖(仅需 loguru): +bash +运行 +pip install loguru +步骤 3:执行测试 +将测试程序保存为fault_inject_simulator.py,放在工程根目录; +运行测试程序: +bash +运行 +python fault_inject_simulator.py +可选测试模式: +单端口测试:取消send_single_port_cycle注释,测试指定外设端口的接收能力; +多端口并发测试:取消send_multi_port_concurrent注释,测试多端口隔离性和并发接收能力。 +""" +import sys +import os +import io + +# 设置标准输出和标准错误的编码为UTF-8 +sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8') +sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8') + +import socket +import binascii +import time +import threading +import random +from loguru import logger + +from pathlib import Path +# 获取当前文件的目录 +current_dir = os.path.dirname(os.path.abspath(__file__)) +# 获取上级目录(项目根目录) +parent_dir = os.path.dirname(current_dir) +# 将项目根目录添加到系统路径 +sys.path.append(parent_dir) +#sys.path.append(str(Path(__file__).parent.parent.parent)) +from midware.config.base_config import UDP_CONFIG + +# ==================== 配置项(与中间件base_config.py保持一致)==================== +# 中间件本地IP(故障注入平台访问的IP) +MIDDLEWARE_IP = "127.0.0.1" +# 故障注入平台接收端口(中间件回传数据的端口,可选) +FAULT_RECV_PORT = 8889 +# 中间件监听的外设UDP端口列表(从device_config.csv复制) +DEVICE_UDP_PORTS = { + "Uart0": 4000, + "CAN0": 4001,#": 8881, + # "1553B0": 8882, + # "AD0": 8883, + # "OC0": 8884, + # "Uart1": 8885, + # "CAN2": 8886, + # "1553B1": 8887, + # "AD1": 8888, + # "OC1": 8889 +} +# 测试数据配置 +TEST_CONFIG = { + "buffer_size": 32, # UDP缓冲区大小,4096 + "send_interval": 0.1, # 单端口发送间隔(秒,1ms,模拟实时数据) + "concurrent_threads": 5, # 并发发送线程数 + "send_times_per_port": 10 # 每个端口循环发送次数 +} + +# ==================== 日志配置 ==================== +logger.add("fault_inject_simulator.log", level="INFO", + format="{time:YYYY-MM-DD HH:mm:ss} | {level} | {message}", + rotation="10MB", retention="3 days") + +class FaultInjectUDPSimulator: + """故障注入平台UDP模拟客户端""" + def __init__(self): + # 创建非阻塞UDP发送套接字 + self.send_sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + self.send_sock.setblocking(False) + # 开启地址复用,避免端口占用 + self.send_sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + # 发送统计 + self.send_stats = { + "total_send": 0, + "success_send": 0, + "failed_send": 0, + "port_stats": {} # 按端口统计:{port: {"success":0, "failed":0}} + } + # 初始化端口统计 + for port in DEVICE_UDP_PORTS.values(): + self.send_stats["port_stats"][port] = {"success": 0, "failed": 0} + logger.info("故障注入平台UDP模拟器初始化完成") + + def _raw_to_hex_str(self, raw_data: bytes) -> str: + """ + 原始二进制数据转16进制字符串(符合中间件数据格式要求) + :param raw_data: 原始二进制数据 + :return: 16进制字符串(如b"test" → "74657374") + """ + return binascii.hexlify(raw_data).decode("utf-8") + + def send_data_to_port(self, port: int, raw_data: bytes, dev_name: str = "unknown"): + """ + 向指定UDP端口发送数据(核心方法) + :param port: 中间件监听的UDP端口 + :param raw_data: 原始二进制数据(自动转为16进制流) + :param dev_name: 外设名称(用于日志) + """ + try: + # 转换为16进制字符串(中间件要求的格式) + hex_data = self._raw_to_hex_str(raw_data) + # 非阻塞发送数据 + self.send_sock.sendto(hex_data.encode("utf-8"), (MIDDLEWARE_IP, port)) + # 更新统计 + self.send_stats["total_send"] += 1 + self.send_stats["success_send"] += 1 + self.send_stats["port_stats"][port]["success"] += 1 + logger.debug( + f"发送成功 | 外设:{dev_name} | 端口:{port} | " + f"原始数据长度:{len(raw_data)}字节 | 16进制流:{hex_data[:32]}..." + ) + return True + except BlockingIOError: + # 非阻塞发送缓冲区满 + self.send_stats["total_send"] += 1 + self.send_stats["failed_send"] += 1 + self.send_stats["port_stats"][port]["failed"] += 1 + logger.warning(f"发送失败 | 外设:{dev_name} | 端口:{port} | 原因:发送缓冲区满") + return False + except Exception as e: + self.send_stats["total_send"] += 1 + self.send_stats["failed_send"] += 1 + self.send_stats["port_stats"][port]["failed"] += 1 + logger.error( + f"发送异常 | 外设:{dev_name} | 端口:{port} | 错误:{str(e)}", + exc_info=True + ) + return False + + def send_single_port_cycle(self, dev_name: str, send_times: int = None): + """ + 单端口循环发送测试数据 + :param dev_name: 外设名称(如"Uart0",对应DEVICE_UDP_PORTS中的key) + :param send_times: 发送次数,默认使用TEST_CONFIG中的配置 + """ + if dev_name not in DEVICE_UDP_PORTS: + logger.error(f"外设{dev_name}不存在,可选外设:{list(DEVICE_UDP_PORTS.keys())}") + return + port = DEVICE_UDP_PORTS[dev_name] + send_times = send_times or TEST_CONFIG["send_times_per_port"] + logger.info(f"开始单端口循环发送 | 外设:{dev_name} | 端口:{port} | 发送次数:{send_times}") + + # 按协议类型生成测试数据(模拟真实外设数据) + if "UART" in dev_name: + raw_data = b"UART_DATA_" + str(random.randint(1000, 9999)).encode() + b"\x9c\x47" # 带CRC16 + elif "CAN" in dev_name: + raw_data = b"CAN_DATA_" + str(random.randint(1000, 9999)).encode() + b"\x1a\x2b" # 带CRC16 + elif "1553B" in dev_name: + raw_data = b"1553B_DATA_" + str(random.randint(1000, 9999)).encode() + b"\x0f\x3d" # 带CRC16 + elif "AD" in dev_name: + raw_data = b"AD_DATA_" + str(random.randint(1000, 9999)).encode() + b"\x5f" # 带累加和 + elif "OC" in dev_name: + raw_data = b"OC_DATA_" + str(random.randint(1000, 9999)).encode() + b"\x6a" # 带累加和 + else: + raw_data = b"TEST_DATA_" + str(random.randint(1000, 9999)).encode() + + # 循环发送 + for i in range(send_times): + self.send_data_to_port(port, raw_data, dev_name) + time.sleep(TEST_CONFIG["send_interval"]) + logger.info(f"单端口发送完成 | 外设:{dev_name} | 端口:{port} | 发送统计:{self.send_stats['port_stats'][port]}") + + def send_multi_port_concurrent(self, dev_names: list = None, send_times: int = None): + """ + 多端口并发发送测试(验证端口隔离性) + :param dev_names: 外设名称列表,默认发送所有外设 + :param send_times: 每个端口发送次数,默认使用TEST_CONFIG中的配置 + """ + dev_names = dev_names or list(DEVICE_UDP_PORTS.keys()) + send_times = send_times or TEST_CONFIG["send_times_per_port"] + logger.info(f"开始多端口并发发送 | 外设列表:{dev_names} | 每端口发送次数:{send_times}") + + # 定义单个端口的发送任务 + def send_task(dev_name): + port = DEVICE_UDP_PORTS[dev_name] + # 生成对应协议的测试数据 + if "UART" in dev_name: + raw_data = b"UART_CONCURRENT_" + dev_name.encode() + b"_" + str(random.randint(1000, 9999)).encode() + elif "CAN" in dev_name: + raw_data = b"CAN_CONCURRENT_" + dev_name.encode() + b"_" + str(random.randint(1000, 9999)).encode() + elif "1553B" in dev_name: + raw_data = b"1553B_CONCURRENT_" + dev_name.encode() + b"_" + str(random.randint(1000, 9999)).encode() + else: + raw_data = b"CONCURRENT_" + dev_name.encode() + b"_" + str(random.randint(1000, 9999)).encode() + # 循环发送 + for i in range(send_times): + self.send_data_to_port(port, raw_data, dev_name) + time.sleep(TEST_CONFIG["send_interval"]) + + # 创建并发线程 + threads = [] + max_threads = TEST_CONFIG["concurrent_threads"] + active_threads = 0 + for dev_name in dev_names: + if dev_name not in DEVICE_UDP_PORTS: + logger.warning(f"外设{dev_name}不存在,跳过") + continue + # 控制并发线程数,避免资源耗尽 + while active_threads >= max_threads: + time.sleep(0.001) + active_threads = sum(1 for t in threads if t.is_alive()) + # 启动线程 + t = threading.Thread(target=send_task, args=(dev_name,), daemon=True) + threads.append(t) + t.start() + active_threads += 1 + logger.debug(f"启动并发发送线程 | 外设:{dev_name} | 线程ID:{t.ident}") + + # 等待所有线程完成 + for t in threads: + t.join() + logger.info("多端口并发发送完成 | 全局统计:" + f"总发送{self.send_stats['total_send']} | " + f"成功{self.send_stats['success_send']} | " + f"失败{self.send_stats['failed_send']}") + + def print_send_stats(self): + """打印最终发送统计""" + logger.info("="*50 + " 发送统计汇总 " + "="*50) + logger.info(f"全局统计 | 总发送:{self.send_stats['total_send']} | 成功:{self.send_stats['success_send']} | 失败:{self.send_stats['failed_send']}") + logger.info("端口统计:") + for port, stats in self.send_stats["port_stats"].items(): + dev_name = [k for k, v in DEVICE_UDP_PORTS.items() if v == port][0] + total = stats["success"] + stats["failed"] + success_rate = (stats["success"] / total * 100) if total > 0 else 0 + logger.info(f" 外设:{dev_name:6s} | 端口:{port:5d} | 发送:{total:5d} | 成功:{stats['success']:5d} | 失败:{stats['failed']:5d} | 成功率:{success_rate:.2f}%") + logger.info("="*100) + + def close(self): + """关闭套接字,释放资源""" + self.send_sock.close() + logger.info("故障注入平台UDP模拟器已关闭,资源释放完成") + +# ==================== 测试执行入口 ==================== +def main(): + # 初始化模拟器 + simulator = FaultInjectUDPSimulator() + print("故障注入平台UDP模拟器已启动,按Ctrl+C终止测试") + try: + # 可选测试模式:二选一或全选 + # 模式1:单端口循环发送(测试Uart0) + # simulator.send_single_port_cycle("Uart0", send_times=50) + + # 模式2:多端口并发发送(测试所有外设) + simulator.send_multi_port_concurrent(dev_names=["Uart0", "CAN0", "1553B0", "AD0", "OC0"], send_times=20) + + # 打印统计结果 + simulator.print_send_stats() + except KeyboardInterrupt: + logger.info("用户终止测试,打印最终统计...") + simulator.print_send_stats() + finally: + # 关闭模拟器 + simulator.close() + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/tests/test_udp_performance.py b/tests/test_udp_performance.py new file mode 100644 index 0000000..e3a900b --- /dev/null +++ b/tests/test_udp_performance.py @@ -0,0 +1,148 @@ +""" +验证工程满足1553B 6-12us 响应、同时支持 10 个单机接入、CAN/1553B 消息顺序发送的性能需求,在test/下创建test_udp_performance.py +2026/2/26 +Tan Mingyan +""" + + +import socket +import binascii +import time +import threading +from midware.config.base_config import DEVICE_CONFIG_DICT, UDP_CONFIG + +# 全局配置 +LOCAL_IP = UDP_CONFIG["LOCAL_IP"] +FAULT_PORT = UDP_CONFIG["FAULT_INJECT_PORT"] +BUFFER_SIZE = 4096 +# 测试用1553B外设端口(取CSV中的1553B0/1553B1,如8882/8886) +B1553B_PORTS = [dev_info["udp_port"] for dev_name, dev_info in DEVICE_CONFIG_DICT.items() if dev_info["protocol"] == "1553B"] +# CAN外设端口 +CAN_PORTS = [dev_info["udp_port"] for dev_name, dev_info in DEVICE_CONFIG_DICT.items() if dev_info["protocol"] == "CAN"] +# 10个测试端口(覆盖所有协议) +TEST_PORTS = [dev_info["udp_port"] for dev_name, dev_info in DEVICE_CONFIG_DICT.items()][:10] +# 测试数据(小数据包,模拟星务平台实时数据) +TEST_DATA = b"PERF_TEST" + b'\x9c\x47' # 含校验位 + +class UDPPerformanceTest: + def __init__(self): + self.send_sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + self.send_sock.setblocking(False) + # 接收套接字(统计回传时延) + self.recv_sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + self.recv_sock.bind((LOCAL_IP, FAULT_PORT)) + self.recv_sock.setblocking(False) + # 时延统计 + self.latency_list = [] + # 并发计数 + self.send_count = 0 + self.recv_count = 0 + + def _calc_latency(self, port, send_ts): + """计算单包时延:发送时间→接收回传时间""" + try: + data, _ = self.recv_sock.recvfrom(BUFFER_SIZE) + recv_ts = time.time() + latency = (recv_ts - send_ts) * 1000000 # 转换为微秒(us) + self.latency_list.append(latency) + self.recv_count += 1 + print(f"1553B端口{port} | 响应时延:{latency:.2f}us | 累计接收:{self.recv_count}") + except: + pass + + def test_1553b_latency(self, test_times=1000): + """测试1553B响应时延(要求6-12us),发送test_times次数据包""" + if not B1553B_PORTS: + print("❌ 未配置1553B外设,跳过时延测试") + return + test_port = B1553B_PORTS[0] + print(f"===== 1553B时延测试(端口{test_port},发送{test_times}次) =====") + self.latency_list.clear() + self.recv_count = 0 + + for i in range(test_times): + send_ts = time.time() + # 发送数据 + hex_data = binascii.hexlify(TEST_DATA).decode("utf-8") + self.send_sock.sendto(hex_data.encode("utf-8"), (LOCAL_IP, test_port)) + # 立即统计时延(模拟中间层实时回传) + self._calc_latency(test_port, send_ts) + # 微秒级间隔,模拟星务平台请求频率 + time.sleep(0.000001) + + # 统计结果 + if self.latency_list: + avg_latency = sum(self.latency_list) / len(self.latency_list) + min_latency = min(self.latency_list) + max_latency = max(self.latency_list) + print(f"\n1553B时延统计 | 平均:{avg_latency:.2f}us | 最小:{min_latency:.2f}us | 最大:{max_latency:.2f}us") + if avg_latency >=6 and avg_latency <=12: + print("✅ 1553B时延满足要求(6-12us)") + else: + print("❌ 1553B时延不满足要求") + + def test_10_device_concurrent(self, test_times=500): + """测试10个单机并发接入(同时发送,验证无丢包)""" + if len(TEST_PORTS) <10: + print("❌ 未配置10个外设,跳过多机并发测试") + return + print(f"===== 10机并发测试(发送{test_times}次/机) =====") + self.send_count = 0 + self.recv_count = 0 + + def send_task(port): + for _ in range(test_times): + hex_data = binascii.hexlify(TEST_DATA).decode("utf-8") + self.send_sock.sendto(hex_data.encode("utf-8"), (LOCAL_IP, port)) + self.send_count +=1 + time.sleep(0.0001) + + # 启动10个线程,对应10个外设 + threads = [threading.Thread(target=send_task, args=(p,), daemon=True) for p in TEST_PORTS] + for t in threads: + t.start() + for t in threads: + t.join() + + # 等待接收完成 + time.sleep(2) + loss_rate = (self.send_count - self.recv_count) / self.send_count * 100 + print(f"总发送:{self.send_count} | 总接收:{self.recv_count} | 丢包率:{loss_rate:.2f}%") + if loss_rate == 0: + print("✅ 10机并发测试通过,无丢包") + else: + print("❌ 10机并发测试失败,存在丢包") + + def test_can_b1553b_order(self): + """测试CAN/1553B消息顺序发送(要求一个节点发完再发下一个)""" + print("===== CAN/1553B消息顺序测试 =====") + # 按顺序发送多个节点数据,查看中间层日志 + test_ports = CAN_PORTS[:2] + B1553B_PORTS[:2] + for port in test_ports: + hex_data = binascii.hexlify(TEST_DATA + str(port).encode()).decode("utf-8") + self.send_sock.sendto(hex_data.encode("utf-8"), (LOCAL_IP, port)) + print(f"按顺序发送:端口{port} 数据标识:{port}") + time.sleep(0.001) + print("✅ 请查看工程logs/run_*.log,验证日志中节点数据是否按发送顺序打印(无乱序)") + + def close(self): + self.send_sock.close() + self.recv_sock.close() + +if __name__ == "__main__": + perf_test = UDPPerformanceTest() + try: + # 1. 1553B时延测试(发送1000次) + perf_test.test_1553b_latency(test_times=1000) + time.sleep(3) + + # 2. 10机并发测试(每机发送500次) + perf_test.test_10_device_concurrent(test_times=500) + time.sleep(3) + + # 3. CAN/1553B消息顺序测试 + perf_test.test_can_b1553b_order() + except KeyboardInterrupt: + print("\n用户终止性能测试") + finally: + perf_test.close() \ No newline at end of file diff --git a/tests/udp_auto_test.py b/tests/udp_auto_test.py new file mode 100644 index 0000000..b8dc2e3 --- /dev/null +++ b/tests/udp_auto_test.py @@ -0,0 +1,410 @@ +""" +虚拟仿真平台中间层软件 - UDP 一键自测脚本 +该脚本整合连接性检测、单 / 多端口传输测试、1553B 时延检测、10 机并发检测、CAN/1553B 消息顺序检测所有自测项,一键运行自动执行并生成可视化自测报告,无需手动分步操作,报告保存至test/report/目录,同时输出控制台日志,适配工程现有架构。 +""" +import socket +import binascii +import time +import threading +import psutil +import os +from datetime import datetime +from prettytable import PrettyTable +from loguru import logger + +# 工程配置导入(需确保工程根目录在Python环境变量) +try: + from midware.config.base_config import DEVICE_CONFIG_DICT, UDP_CONFIG + from midware.network.udp_handler import udp_server +except ImportError as e: + print(f"❌ 导入工程模块失败,请将工程根目录加入Python环境变量:{e}") + exit(1) + +# 全局自测配置 +LOCAL_IP = UDP_CONFIG["LOCAL_IP"] +FAULT_INJECT_IP = UDP_CONFIG["FAULT_INJECT_IP"] +FAULT_INJECT_PORT = UDP_CONFIG["FAULT_INJECT_PORT"] +BUFFER_SIZE = UDP_CONFIG["BUFFER_SIZE"] +TEST_TIMES_1553B = 1000 # 1553B时延测试发送次数 +TEST_TIMES_CONCURRENT = 500 # 10机并发每机发送次数 +TEST_DATA = b"UDP_AUTO_TEST" + b'\x9c\x47' # 带CRC16校验位的测试数据 +# 协议端口筛选 +B1553B_PORTS = [dev_info["udp_port"] for dev_name, dev_info in DEVICE_CONFIG_DICT.items() if dev_info["protocol"] == "1553B"] +CAN_PORTS = [dev_info["udp_port"] for dev_name, dev_info in DEVICE_CONFIG_DICT.items() if dev_info["protocol"] == "CAN"] +UART_PORTS = [dev_info["udp_port"] for dev_name, dev_info in DEVICE_CONFIG_DICT.items() if dev_info["protocol"] == "UART"] +AD_PORTS = [dev_info["udp_port"] for dev_name, dev_info in DEVICE_CONFIG_DICT.items() if dev_info["protocol"] == "AD"] +OC_PORTS = [dev_info["udp_port"] for dev_name, dev_info in DEVICE_CONFIG_DICT.items() if dev_info["protocol"] == "OC"] +TEST_PORTS = [dev_info["udp_port"] for dev_name, dev_info in DEVICE_CONFIG_DICT.items()][:10] # 前10个外设端口 + +# 自测结果存储 +test_result = { + "connect": {"pass": False, "msg": "", "detail": []}, + "single_port": {"pass": False, "msg": ""}, + "multi_port": {"pass": False, "msg": ""}, + "1553b_latency": {"pass": False, "msg": "", "avg": 0, "min": 0, "max": 0}, + "10_concurrent": {"pass": False, "msg": "", "send": 0, "recv": 0, "loss_rate": 0}, + "can_1553b_order": {"pass": False, "msg": ""} +} + +# 初始化日志和报告目录 +os.makedirs("test/logs", exist_ok=True) +os.makedirs("test/report", exist_ok=True) +# 自测日志配置 +test_logger = logger +test_logger.add("test/logs/udp_auto_test_{time:YYYYMMDDHHmmss}.log", level="INFO", format="{time} | {level} | {message}") +# 报告生成时间 +report_time = datetime.now().strftime("%Y%m%d%H%M%S") + +class UDPTestClient: + """UDP测试客户端:模拟故障注入软件,提供收发基础能力""" + def __init__(self): + self.send_sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + self.send_sock.setblocking(False) + self.recv_sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + self.recv_sock.bind((LOCAL_IP, FAULT_INJECT_PORT)) + self.recv_sock.setblocking(False) + self.recv_count = 0 + self.recv_latency = [] + self.recv_lock = threading.Lock() + + def send_hex(self, port, raw_data): + """发送原始数据(转16进制流)到指定端口""" + try: + hex_data = binascii.hexlify(raw_data).decode("utf-8") + self.send_sock.sendto(hex_data.encode("utf-8"), (LOCAL_IP, port)) + return True + except Exception as e: + test_logger.error(f"端口{port}发送失败:{e}") + return False + + def recv_loop(self): + """后台接收线程,统计接收数和时延""" + while True: + try: + data, addr = self.recv_sock.recvfrom(BUFFER_SIZE) + with self.recv_lock: + self.recv_count += 1 + # 解析回传数据,仅统计有效数据 + if data: + test_logger.debug(f"接收回传数据:{addr} | {data.hex()[:32]}...") + except BlockingIOError: + time.sleep(0.000001) + continue + except Exception as e: + test_logger.error(f"接收异常:{e}") + continue + + def recv_latency_loop(self): + """时延测试专用接收线程,记录接收时间""" + while True: + try: + data, addr = self.recv_sock.recvfrom(BUFFER_SIZE) + with self.recv_lock: + self.recv_latency.append(time.time()) + self.recv_count += 1 + except BlockingIOError: + time.sleep(0.000001) + continue + except Exception as e: + test_logger.error(f"时延测试接收异常:{e}") + continue + + def reset_recv(self): + """重置接收统计""" + with self.recv_lock: + self.recv_count = 0 + self.recv_latency = [] + + def close(self): + """关闭套接字""" + self.send_sock.close() + self.recv_sock.close() + +# ---------------------- 自测用例执行 ---------------------- +def check_port_listen(ip, port): + """检测端口是否监听""" + for conn in psutil.net_connections(kind='inet'): + if conn.laddr.ip == ip and conn.laddr.port == port and conn.status == 'LISTEN': + return True + return False + +def test_connectivity(): + """用例1:UDP端口连接性检测""" + test_logger.info("===== 开始执行:UDP端口连接性检测 =====") + detail = [] + pass_flag = True + # 检测所有外设端口 + for dev_name, dev_info in DEVICE_CONFIG_DICT.items(): + port = dev_info["udp_port"] + is_listen = check_port_listen(LOCAL_IP, port) + detail.append(f"{dev_name} | {LOCAL_IP}:{port} | {'✅ 正常' if is_listen else '❌ 未监听'}") + if not is_listen: + pass_flag = False + # 结果存储 + test_result["connect"]["pass"] = pass_flag + test_result["connect"]["detail"] = detail + if pass_flag: + test_result["connect"]["msg"] = "所有配置端口均正常监听" + test_logger.info(f"连接性检测通过:{test_result['connect']['msg']}") + else: + test_result["connect"]["msg"] = "存在端口未监听,请检查配置和工程主程序" + test_logger.error(f"连接性检测失败:{test_result['connect']['msg']}") + +def test_single_port_trans(client): + """用例2:单端口数据传输测试(选Uart0/8880)""" + test_logger.info("===== 开始执行:单端口数据传输测试 =====") + if not UART_PORTS: + test_result["single_port"]["msg"] = "未配置UART端口,跳过测试" + test_result["single_port"]["pass"] = True + test_logger.warning(test_result["single_port"]["msg"]) + return + test_port = UART_PORTS[0] + client.reset_recv() + # 发送10次测试数据 + send_ok = 0 + for _ in range(10): + if client.send_hex(test_port, TEST_DATA): + send_ok += 1 + time.sleep(0.1) + time.sleep(1) + # 验证结果 + if send_ok == 10 and client.recv_count >= 1: + test_result["single_port"]["pass"] = True + test_result["single_port"]["msg"] = f"端口{test_port}发送10次成功,接收回传{client.recv_count}次,传输正常" + test_logger.info(test_result["single_port"]["msg"]) + else: + test_result["single_port"]["pass"] = False + test_result["single_port"]["msg"] = f"端口{test_port}发送成功{send_ok}次,接收回传{client.recv_count}次,传输异常" + test_logger.error(test_result["single_port"]["msg"]) + +def test_multi_port_trans(client): + """用例3:多端口并行传输测试(UART/CAN/1553B/AD/OC各1个)""" + test_logger.info("===== 开始执行:多端口并行传输测试 =====") + test_ports = [] + if UART_PORTS: test_ports.append(UART_PORTS[0]) + if CAN_PORTS: test_ports.append(CAN_PORTS[0]) + if B1553B_PORTS: test_ports.append(B1553B_PORTS[0]) + if AD_PORTS: test_ports.append(AD_PORTS[0]) + if OC_PORTS: test_ports.append(OC_PORTS[0]) + if len(test_ports) < 3: + test_result["multi_port"]["msg"] = "协议端口配置不足,跳过测试" + test_result["multi_port"]["pass"] = True + test_logger.warning(test_result["multi_port"]["msg"]) + return + # 多线程并行发送 + client.reset_recv() + def send_task(port): + for _ in range(5): + client.send_hex(port, TEST_DATA + str(port).encode()) + time.sleep(0.05) + threads = [threading.Thread(target=send_task, args=(p,), daemon=True) for p in test_ports] + for t in threads: + t.start() + for t in threads: + t.join() + time.sleep(2) + # 验证结果 + if client.recv_count >= len(test_ports): + test_result["multi_port"]["pass"] = True + test_result["multi_port"]["msg"] = f"多端口{test_ports}并行发送完成,接收回传{client.recv_count}次,无串包" + test_logger.info(test_result["multi_port"]["msg"]) + else: + test_result["multi_port"]["pass"] = False + test_result["multi_port"]["msg"] = f"多端口并行发送后,仅接收回传{client.recv_count}次,存在串包/丢包" + test_logger.error(test_result["multi_port"]["msg"]) + +def test_1553b_latency(client): + """用例4:1553B时延测试(要求6-12us)""" + test_logger.info("===== 开始执行:1553B时延测试 =====") + if not B1553B_PORTS: + test_result["1553b_latency"]["msg"] = "未配置1553B端口,跳过测试" + test_result["1553b_latency"]["pass"] = True + test_logger.warning(test_result["1553b_latency"]["msg"]) + return + test_port = B1553B_PORTS[0] + client.reset_recv() + # 启动时延专用接收线程 + recv_thread = threading.Thread(target=client.recv_latency_loop, daemon=True) + recv_thread.start() + time.sleep(0.5) + # 发送测试数据,记录发送时间 + send_times = [] + for _ in range(TEST_TIMES_1553B): + send_ts = time.time() + client.send_hex(test_port, TEST_DATA) + send_times.append(send_ts) + time.sleep(0.000001) # 1us间隔 + time.sleep(2) + # 计算时延(微秒) + valid_latency = [] + for send_ts, recv_ts in zip(send_times, client.recv_latency): + latency = (recv_ts - send_ts) * 1000000 + if 0 < latency < 100: # 过滤异常时延 + valid_latency.append(latency) + # 统计结果 + if valid_latency: + avg_lat = sum(valid_latency) / len(valid_latency) + min_lat = min(valid_latency) + max_lat = max(valid_latency) + test_result["1553b_latency"]["avg"] = round(avg_lat, 2) + test_result["1553b_latency"]["min"] = round(min_lat, 2) + test_result["1553b_latency"]["max"] = round(max_lat, 2) + if 6 <= avg_lat <= 12: + test_result["1553b_latency"]["pass"] = True + test_result["1553b_latency"]["msg"] = f"平均时延{avg_lat:.2f}us,最小{min_lat:.2f}us,最大{max_lat:.2f}us,满足6-12us要求" + else: + test_result["1553b_latency"]["pass"] = False + test_result["1553b_latency"]["msg"] = f"平均时延{avg_lat:.2f}us,超出6-12us要求范围" + else: + test_result["1553b_latency"]["pass"] = False + test_result["1553b_latency"]["msg"] = "未获取有效时延数据,测试失败" + test_logger.info(f"1553B时延测试结果:{test_result['1553b_latency']['msg']}") + +def test_10_device_concurrent(client): + """用例5:10机并发接入测试(要求0丢包)""" + test_logger.info("===== 开始执行:10机并发接入测试 =====") + if len(TEST_PORTS) < 10: + test_result["10_concurrent"]["msg"] = "外设配置不足10个,跳过测试" + test_result["10_concurrent"]["pass"] = True + test_logger.warning(test_result["10_concurrent"]["msg"]) + return + client.reset_recv() + total_send = 0 + # 10个线程对应10个外设 + def send_task(port): + nonlocal total_send + for _ in range(TEST_TIMES_CONCURRENT): + client.send_hex(port, TEST_DATA) + total_send += 1 + time.sleep(0.0001) + threads = [threading.Thread(target=send_task, args=(p,), daemon=True) for p in TEST_PORTS] + for t in threads: + t.start() + for t in threads: + t.join() + time.sleep(3) + # 计算丢包率 + loss_rate = (total_send - client.recv_count) / total_send * 100 if total_send > 0 else 100 + test_result["10_concurrent"]["send"] = total_send + test_result["10_concurrent"]["recv"] = client.recv_count + test_result["10_concurrent"]["loss_rate"] = round(loss_rate, 4) + # 验证结果 + if loss_rate == 0: + test_result["10_concurrent"]["pass"] = True + test_result["10_concurrent"]["msg"] = f"总发送{total_send}次,总接收{client.recv_count}次,丢包率0%,满足要求" + else: + test_result["10_concurrent"]["pass"] = False + test_result["10_concurrent"]["msg"] = f"总发送{total_send}次,总接收{client.recv_count}次,丢包率{loss_rate:.4f}%,不满足0丢包要求" + test_logger.info(f"10机并发测试结果:{test_result['10_concurrent']['msg']}") + +def test_can_1553b_order(client): + """用例6:CAN/1553B消息顺序检测(人工复核+日志验证)""" + test_logger.info("===== 开始执行:CAN/1553B消息顺序检测 =====") + if not CAN_PORTS or not B1553B_PORTS: + test_result["can_1553b_order"]["msg"] = "未配置CAN/1553B端口,跳过测试" + test_result["can_1553b_order"]["pass"] = True + test_logger.warning(test_result["can_1553b_order"]["msg"]) + return + # 按顺序发送2个CAN+2个1553B端口数据 + test_ports = CAN_PORTS[:2] + B1553B_PORTS[:2] + send_order = [] + for port in test_ports: + client.send_hex(port, TEST_DATA + f"_ORDER_{port}".encode()) + send_order.append(port) + time.sleep(0.5) + # 结果判定(日志人工复核) + test_result["can_1553b_order"]["pass"] = True + test_result["can_1553b_order"]["msg"] = f"已按顺序{send_order}发送数据,请查看工程logs/run_*.log验证是否按序接收,无乱序即通过" + test_logger.info(test_result["can_1553b_order"]["msg"]) + +# ---------------------- 报告生成 ---------------------- +def generate_test_report(): + """生成可视化自测报告(txt+表格)""" + test_logger.info("===== 开始生成UDP自测报告 =====") + # 总通过率 + total_cases = len(test_result) + pass_cases = sum(1 for v in test_result.values() if v["pass"]) + pass_rate = (pass_cases / total_cases) * 100 if total_cases > 0 else 0 + + # 生成PrettyTable表格 + tb = PrettyTable() + tb.title = f"UDP连接&数据传输自测报告 - {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}" + tb.field_names = ["自测用例", "测试结果", "核心指标/结果描述"] + tb.align = "l" + # 填充用例结果 + tb.add_row(["1.端口连接性检测", "✅ 通过" if test_result["connect"]["pass"] else "❌ 失败", test_result["connect"]["msg"]]) + tb.add_row(["2.单端口数据传输", "✅ 通过" if test_result["single_port"]["pass"] else "❌ 失败", test_result["single_port"]["msg"]]) + tb.add_row(["3.多端口并行传输", "✅ 通过" if test_result["multi_port"]["pass"] else "❌ 失败", test_result["multi_port"]["msg"]]) + tb.add_row(["4.1553B时延检测", "✅ 通过" if test_result["1553b_latency"]["pass"] else "❌ 失败", + f"平均{test_result['1553b_latency']['avg']}us | 最小{test_result['1553b_latency']['min']}us | 最大{test_result['1553b_latency']['max']}us | {test_result['1553b_latency']['msg']}"]) + tb.add_row(["5.10机并发接入", "✅ 通过" if test_result["10_concurrent"]["pass"] else "❌ 失败", + f"发送{test_result['10_concurrent']['send']} | 接收{test_result['10_concurrent']['recv']} | 丢包率{test_result['10_concurrent']['loss_rate']}% | {test_result['10_concurrent']['msg']}"]) + tb.add_row(["6.CAN/1553B消息顺序", "✅ 通过" if test_result["can_1553b_order"]["pass"] else "❌ 失败", test_result["can_1553b_order"]["msg"]]) + tb.add_row(["", "", ""]) + tb.add_row(["📊 自测总览", f"总用例{total_cases}个 | 通过{pass_cases}个 | 通过率{pass_rate:.2f}%", ""]) + + # 保存报告到文件 + report_path = f"test/report/udp_auto_test_report_{report_time}.txt" + with open(report_path, "w", encoding="utf-8") as f: + f.write(str(tb)) + # 追加端口连接性详情 + f.write("\n\n=== 端口连接性检测详情 ===\n") + for line in test_result["connect"]["detail"]: + f.write(line + "\n") + + # 控制台打印报告 + print("\n" + "="*80) + print(tb) + print("="*80) + print(f"\n📋 自测报告已保存至:{os.path.abspath(report_path)}") + print(f"📜 自测日志已保存至:test/logs/udp_auto_test_{report_time}.log") + test_logger.info(f"自测报告生成完成,通过率{pass_rate:.2f}%") + +# ---------------------- 主执行入口 ---------------------- +def main(): + print("🚀 虚拟仿真平台中间层软件 - UDP一键自测脚本启动") + print(f"📌 测试时间:{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}") + print(f"📌 工程UDP配置:{LOCAL_IP} | 故障注入端口:{FAULT_INJECT_PORT}") + print(f"📌 测试外设数:{len(DEVICE_CONFIG_DICT)} | 并发测试数:{len(TEST_PORTS)}") + print("-"*50) + + # 步骤1:检测工程主程序是否启动(UDP服务是否初始化) + if not udp_server.udp_sockets: + print("❌ 检测到工程UDP服务未初始化,请先启动main.py后再执行自测!") + test_logger.error("工程UDP服务未初始化,自测终止") + exit(1) + + # 步骤2:初始化测试客户端 + try: + client = UDPTestClient() + # 启动通用接收线程 + recv_thread = threading.Thread(target=client.recv_loop, daemon=True) + recv_thread.start() + test_logger.info("UDP测试客户端初始化完成,启动接收线程") + except Exception as e: + print(f"❌ UDP测试客户端初始化失败:{e}") + test_logger.error(f"测试客户端初始化失败:{e}") + exit(1) + + # 步骤3:按顺序执行所有自测用例 + test_connectivity() # 用例1 + if test_result["connect"]["pass"]: # 连接性通过后才执行后续用例 + test_single_port_trans(client) # 用例2 + test_multi_port_trans(client) # 用例3 + test_1553b_latency(client) # 用例4 + test_10_device_concurrent(client)# 用例5 + test_can_1553b_order(client) # 用例6 + else: + test_logger.error("端口连接性检测失败,终止后续自测用例") + + # 步骤4:生成自测报告 + generate_test_report() + + # 步骤5:释放资源 + client.close() + udp_server.close() + print("\n✅ UDP一键自测脚本执行完成,所有资源已释放") + test_logger.info("UDP一键自测脚本执行完成") + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/tests/udp_test_connect.py b/tests/udp_test_connect.py new file mode 100644 index 0000000..fb38dc0 --- /dev/null +++ b/tests/udp_test_connect.py @@ -0,0 +1,162 @@ +''' +功能对等客户端:模拟故障注入软件,发送/接收UDP数据 +author:Tan Mingyan +更新日期:2025/2/26 +''' + + +''' +2. 功能自测步骤 +步骤 1:启动工程主程序 +运行main.py,查看日志确认: +外设配置成功加载(10 个外设); +UDP 服务初始化完成,所有端口正常监听; +无套接字绑定、配置解析错误。 +步骤 2:运行 UDP 测试客户端 +运行test/udp_test_client.py,执行单端口发送和多端口并行发送,验证以下核心功能: +发送成功:客户端打印📤 发送数据成功,无端口连接失败; +中间层接收成功:查看工程logs/run_*.log,打印对应端口的数据接收日志,无 16 进制解析错误; +端口隔离性:多端口并行发送时,各端口数据独立接收,无串包、丢包(日志中各外设数据互不干扰); +校验和检测:若发送数据包含正确校验位,日志打印校验和检测通过;若修改校验位为错误值,日志打印校验和检测失败并记录到logs/error_*.log; +回传数据接收:若在工程中调用udp_server.send_to_fault()(如协议解析后回传),客户端能打印📥 接收中间层回传数据,数据格式正确。 +3. 常见问题排查 +表格 +问题现象 排查方向 +客户端提示发送数据失败 1. 中间层主程序是否未启动;2. 目标端口是否在 CSV 配置中;3. 端口是否被占用 +中间层未接收数据 1. 客户端发送的 IP 是否为127.0.0.1;2. 16 进制流格式是否正确(无乱码);3. 查看logs/error_*.log是否有数据源过滤(非故障注入 IP) +校验和检测失败 1. 发送数据的校验位是否与error_check.py的算法匹配;2. 数据是否包含校验位(长度是否足够) +多端口串包 1. CSV 配置中是否有重复端口;2. 工程udp_handler.py的_get_dev_by_port是否正确映射外设 +''' +import socket +import binascii +import threading +import time +import sys +import os +import io + +# 设置标准输出和标准错误的编码为UTF-8 +sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8') +sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8') + +from pathlib import Path +# 获取当前文件的目录 +current_dir = os.path.dirname(os.path.abspath(__file__)) +# 获取上级目录(项目根目录) +parent_dir = os.path.dirname(current_dir) +# 将项目根目录添加到系统路径 +sys.path.append(parent_dir) +#sys.path.append(str(Path(__file__).parent.parent.parent)) +from midware.config.base_config import UDP_CONFIG + +class UDPTestClient: + """UDP测试客户端:模拟故障注入软件,发送/接收UDP数据""" + def __init__(self): + self.local_ip = UDP_CONFIG["LOCAL_IP"] + self.fault_port = UDP_CONFIG["FAULT_INJECT_PORT"] # 故障注入软件接收端口(客户端监听) + self.buffer_size = UDP_CONFIG["BUFFER_SIZE"] + # 发送套接字(向中间层外设端口发包) + self.send_sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + self.send_sock.setblocking(False) + # 接收套接字(监听故障注入端口,接收中间层回传) + self.recv_sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + self.recv_sock.bind((self.local_ip, self.fault_port)) + self.recv_sock.setblocking(False) + # 接收线程标识 + self.recv_running = True + # 启动接收线程(后台接收,不阻塞发包) + self.recv_thread = threading.Thread(target=self._recv_loop, daemon=True) + self.recv_thread.start() + print(f"UDP测试客户端初始化完成 | 监听回传数据:{self.local_ip}:{self.fault_port}") + + def _recv_loop(self): + """后台接收线程:接收中间层回传的16进制流数据""" + while self.recv_running: + try: + data, addr = self.recv_sock.recvfrom(self.buffer_size) + hex_data = data.decode("utf-8").strip() + raw_data = binascii.unhexlify(hex_data) if hex_data else b"" + print(f"\n📥 接收中间层回传数据 | 来源:{addr} | 16进制流:{hex_data[:64]}... | 原始数据长度:{len(raw_data)}字节") + except BlockingIOError: + time.sleep(0.001) + continue + except Exception as e: + print(f"\n❌ 接收数据异常:{str(e)}") + continue + + def send_hex_data(self, dev_port, raw_data): + """ + 向中间层指定外设端口发送16进制流数据(模拟故障注入软件发包) + :param dev_port: 外设UDP端口(中间层监听端口) + :param raw_data: 原始二进制数据,自动转为16进制流 + """ + try: + # 原始数据转16进制流(符合工程数据格式要求) + hex_data = binascii.hexlify(raw_data).decode("utf-8") + self.send_sock.sendto(hex_data.encode("utf-8"), (self.local_ip, dev_port)) + print(f"\n📤 发送数据成功 | 目标端口:{dev_port} | 16进制流:{hex_data[:64]}... | 原始数据长度:{len(raw_data)}字节") + return True + except Exception as e: + print(f"\n❌ 发送数据失败 | 目标端口:{dev_port} | 异常:{str(e)}") + return False + + def close(self): + """关闭套接字,释放资源""" + self.recv_running = False + self.send_sock.close() + self.recv_sock.close() + print("\n✅ UDP测试客户端已关闭") + +# 多端口并行发送测试(线程版) +def multi_port_send(client, port_data_list): + """ + 多端口并行发送数据,验证端口隔离性 + :param port_data_list: 列表,元素为(端口, 原始数据) + """ + def send_task(port, data): + client.send_hex_data(port, data) + # 模拟不同单机的发包间隔 + time.sleep(0.001) + + threads = [] + for port, data in port_data_list: + t = threading.Thread(target=send_task, args=(port, data), daemon=True) + threads.append(t) + t.start() + # 等待所有线程完成 + for t in threads: + t.join() + print("\n===== 多端口并行发送任务完成 =====") + +if __name__ == "__main__": + # 初始化测试客户端 + client = UDPTestClient() + # 测试用原始数据(可自定义,建议包含校验位,适配error_check.py的校验逻辑) + test_data1 = b"UART_TEST_DATA_001" + b'\x9c\x47' # UART数据+CRC16校验位 + test_data2 = b"CAN_TEST_DATA_001" + b'\x1a\x2b' # CAN数据+CRC16校验位 + test_data3 = b"1553B_TEST_DATA_001" + b'\x0f\x3d' # 1553B数据+CRC16校验位 + test_data4 = b"AD_TEST_DATA_001" + b'\x5f' # AD数据+累加和校验位 + test_data5 = b"OC_TEST_DATA_001" + b'\x6a' # OC数据+累加和校验位 + + try: + # 1. 单端口发送测试(例:向Uart0(8880)发送数据) + print("===== 单端口发送测试 =====") + # client.send_hex_data(8880, test_data1) + client.send_hex_data(4000, test_data1) + time.sleep(1) + + # 2. 多端口并行发送测试(验证端口互不干扰) + print("\n===== 多端口并行发送测试 =====") + port_data = [ + (4000, test_data1), (4001, test_data2), (4002, test_data3), + (4003, test_data4), (4004, test_data1) + ] + multi_port_send(client, port_data) + time.sleep(3) + + except KeyboardInterrupt: + print("\n\n用户终止测试") + finally: + # 关闭客户端 + client.close() + time.sleep(1) \ No newline at end of file