first commit

This commit is contained in:
2026-06-16 15:40:19 +08:00
commit 8ba6324244
97 changed files with 6216 additions and 0 deletions

0
README.md Normal file
View File

0
__init__.py Normal file
View File

Binary file not shown.

View File

@@ -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
1 名称 协议类型 内存基地址 内存偏移地址 UDP端口 说明 发送缓存区大小 接收缓存区大小
2 Uart0 UART 0x20800000 0x0000 8880 光纤陀螺A 512 512
3 CAN1 CAN 0x20810000 0x0100 8881 姿态传感器 1024 1024
4 1553B0 1553B 0x20820000 0x0200 8882 测控模块 2048 2048

View File

@@ -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
1 名称 编号 协议类型 内存基地址 内存偏移地址 port remote_port 说明 发送缓存区大小 接收缓存区大小
2 光纤陀螺A 1 UART 0x20800000 0x0000 4000 11000 Uart0 512 512
3 反作用轮A 2 CAN 0x20810000 0x0100 4001 11011 CAN0 1024 1024
4 1553B0 3 1553B 0x20820000 0x0200 4002 11022 测控模块 2048 2048
5 电压采集 4 AD 0x20830000 0x0300 4003 11033 AD0 512 512
6 开关量输出 1 OC 0x20840000 0x0400 4004 11044 OC0 512 512
7 光纤陀螺B 1 UART 0x20850000 0X0001 4005 11055 UART1 512 512
8 星敏A 1 CAN 0x20860000 0x0101 4006 11066 CAN1 512 512
9 数传A 1 1553B 0x20870000 0x0201 4007 11077 1553B1 512 512
10 adc1 1 AD 0x21800000 0x0488 4018 11088 正电压采集热敏量采集 512 512
11 继电器输出 2 OC 0x20890000 0x0401 4009 11099 OC1 512 512
12 uart24 24 UART 0x20800c80 0x0000 3021 3071 地测串口 1024 2048

View File

@@ -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
1 名称 协议类型 内存基地址 内存偏移地址 port 说明 发送缓存区大小 接收缓存区大小
2 光纤陀螺A UART 0x20800000 0x0000 4000 Uart0 512 512
3 反作用轮A CAN 0x20810000 0x0100 4001 CAN0 1024 1024
4 1553B0 1553B 0x20820000 0x0200 4002 测控模块 2048 2048
5 电压采集 AD 0x20830000 0x0300 4003 AD0 512 512
6 开关量输出 OC 0x20840000 0x0400 4004 OC0 512 512
7 光纤陀螺B UART 0x20850000 0X0001 4005 UART1 512 512
8 星敏A CAN 0x20860000 0x0101 4006 CAN1 512 512
9 数传A 1553B 0x20870000 0x0201 4007 1553B1 512 512
10 adc1 AD 0x21800000 0x0488 4018 正电压采集热敏量采集 512 512
11 继电器输出 OC 0x20890000 0x0401 4009 OC1 512 512
12 uart24 UART 0x20800c80 0x0000 4030 地测串口 1024 2048

View File

@@ -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
1 名称 编号 协议类型 内存基地址 内存偏移地址 port remote_port tcp_local_port tcp_bus_port 说明 发送缓存区大小 接收缓存区大小
2 光纤陀螺A 1 UART 0x20800000 0x0000 4000 11000 Uart0 512 512
3 反作用轮A 2 CAN 0x20810000 0x0100 4001 11011 CAN0 1024 1024
4 1553B0 3 1553B 0x20820000 0x0200 4002 11022 测控模块 2048 2048
5 电压采集 4 AD 0x20830000 0x0300 4003 11033 AD0 512 512
6 开关量输出 1 OC 0x20840000 0x0400 4004 11044 OC0 512 512
7 光纤陀螺B 1 UART 0x20850000 0X0001 4005 11055 UART1 512 512
8 星敏A 3 CAN 0x20860000 0x0101 4006 11066 CAN1 512 512
9 数传A 1 1553B 0x20870000 0x0201 4007 11077 1553B1 512 512
10 adc1 1 AD 0x21800000 0x0488 4018 11088 正电压采集热敏量采集 512 512
11 继电器输出 2 OC 0x20890000 0x0401 4009 11099 OC1 512 512
12 uart24 24 UART 0x20800c80 0x0000 13045 13095 13024 12024 地测串口 1024 2048
13 uart26 26 UART 0x20C00000 0x0000 3145 3195 13026 12026 高速通信 2048 512
14 uart0 0 UART 0x20C00001 0x0001 3021 3071 陀螺A 512 512
15 uart1 1 UART 0x20C00002 0x0002 3022 3072 陀螺B 512 512
16 uart2 2 UART 0x20C00003 0x0003 3023 3073 陀螺C 512 512
17 uart3 3 UART 0x20C00004 0x0004 3024 3074 预留 512 512
18 uart18 18 UART 0x20C00005 0x0005 3039 3089 星敏C 512 512
19 uart19 18 UART 0x20C00006 0x0006 3040 3090 星敏B 512 512
20 uart25 25 UART 0x20C00007 0x0007 3046 3096 GNSS 512 512
21 can_a 1 CAN 0x20860000 0x0101 20031 10031 canA 512 512
22 can_b 2 CAN 0x20860000 0x0101 20032 10032 canB 512 512

View File

@@ -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
1 名称 编号 协议类型 内存基地址 内存偏移地址 port remote_port tcp_local_port tcp_bus_port 说明 发送缓存区大小 接收缓存区大小
2 光纤陀螺A 1 UART 0x20800000 0x0000 4000 11000 Uart0 512 512
3 反作用轮A 2 CAN 0x20810000 0x0100 4001 11011 CAN0 1024 1024
4 1553B0 3 1553B 0x20820000 0x0200 4002 11022 测控模块 2048 2048
5 电压采集 4 AD 0x20830000 0x0300 4003 11033 AD0 512 512
6 开关量输出 1 OC 0x20840000 0x0400 4004 11044 OC0 512 512
7 光纤陀螺B 1 UART 0x20850000 0X0001 4005 11055 UART1 512 512
8 星敏A 1 CAN 0x20860000 0x0101 4006 11066 CAN1 512 512
9 数传A 1 1553B 0x20870000 0x0201 4007 11077 1553B1 512 512
10 adc1 1 AD 0x21800000 0x0488 4018 11088 正电压采集热敏量采集 512 512
11 继电器输出 2 OC 0x20890000 0x0401 4009 11099 OC1 512 512
12 uart24 24 UART 0x20800c80 0x0000 13045 13095 13024 12024 地测串口 1024 2048
13 uart26 26 UART 0x20C00000 0x0000 3145 3195 13026 12026 高速通信 2048 512
14 uart0 0 UART 0x20C00001 0x0001 3021 3071 陀螺A 512 512
15 uart1 1 UART 0x20C00002 0x0002 3022 3072 陀螺B 512 512
16 uart2 2 UART 0x20C00003 0x0003 3023 3073 陀螺C 512 512
17 uart3 3 UART 0x20C00004 0x0004 3024 3074 预留 512 512
18 uart18 18 UART 0x20C00005 0x0005 3039 3089 星敏C 512 512
19 uart19 18 UART 0x20C00006 0x0006 3040 3090 星敏B 512 512
20 uart25 25 UART 0x20C00007 0x0007 3046 3096 GNSS 512 512
21 canA 1 CAN 0x20860000 0x0101 20031 10031 canA 512 512
22 canB 2 CAN 0x20860000 0x0101 20032 10032 canB 512 512

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

236
fault_inject_simulator.log Normal file
View File

@@ -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模拟器已关闭资源释放完成

191
main-20260227-01.py Normal file
View File

@@ -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≤1311+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)

199
main-20260228.py Normal file
View File

@@ -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)

171
main.py Normal file
View File

@@ -0,0 +1,171 @@
"""
anthor:Tan Mingyan
time2026/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("程序正常退出")

0
midware/bus/__init__.py Normal file
View File

View File

@@ -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()

View File

View File

Binary file not shown.

Binary file not shown.

View File

@@ -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()

View File

@@ -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
1 名称 协议类型 内存基地址 内存偏移地址 port 说明 发送缓存区大小 接收缓存区大小
2 光纤陀螺A UART 0x20800000 0x0000 4000 Uart0 512 512
3 反作用轮A CAN 0x20810000 0x0100 4001 CAN0 1024 1024
4 1553B0 1553B 0x20820000 0x0200 4002 测控模块 2048 2048
5 电压采集 AD 0x20830000 0x0300 4003 AD0 512 512
6 开关量输出 OC 0x20840000 0x0400 4004 OC0 512 512
7 光纤陀螺B UART 0x20850000 0X0001 4005 UART1 512 512
8 星敏A CAN 0x20860000 0x0101 4006 CAN1 512 512
9 数传A 1553B 0x20870000 0x0201 4007 1553B1 512 512
10 电流采集 AD 0x20880000 0x0301 4018 AD1 512 512
11 继电器输出 OC 0x20890000 0x0401 4009 OC1 512 512

View File

@@ -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):
"""处理十六进制字符串转整数(兼容带逗号 / 空格的格式,如 0x20800000、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)

View File

@@ -0,0 +1,137 @@
# -*- coding: utf-8 -*-
'''
Excel 配置解析excel_config.py基于openpyxl实现 Excel 文件解析提取外设名称、UDP 端口、协议类型、基地址等信息存入DEVICE_CONFIG_DICT。
'''
'''
### 核心功能说明
1. **格式兼容**支持解析Excel中**带逗号/空格的十六进制地址**(如`0x20800000`→`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`、`0x20800000`、`0x0000`
5. 端口/缓存大小填写**纯数字**。
### 简单测试
在`config_files/`目录下创建`device_config.xlsx`,填入如下测试数据,直接运行该文件即可看到解析结果:
| 名称 | 协议类型 | 内存基地址 | 内存偏移地址 | UDP端口 | 说明 | 发送缓存区大小 | 接收缓存区大小 |
|--------|----------|------------|--------------|---------|------------|----------------|----------------|
| Uart0 | UART | 0x20800000 | 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):
"""处理十六进制字符串转整数(兼容带逗号 / 空格的格式,如 0x20800000、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)

View File

@@ -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):
"""处理十六进制字符串转整数(兼容带逗号 / 空格的格式,如 0x20800000、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)

23
midware/core/__init__.py Normal file
View File

@@ -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"

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

View File

View File

View File

@@ -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__} 自动断开连接")

View File

View File

View File

View File

@@ -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},无法调整优先级")

View File

View File

@@ -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

View File

@@ -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("所有队列处理完成,线程已停止")
'''

View File

@@ -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设2000UART设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) # 默认优先级51最高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("所有队列处理完成,线程已停止")
'''

View File

@@ -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

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -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__} 自动断开连接")

View File

@@ -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

190
midware/drivers/bus_ad.py Normal file
View File

@@ -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发送数据帧ID0x{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

133
midware/drivers/bus_can.py Normal file
View File

@@ -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发送数据帧ID0x{frame_id:X}|扩展帧:{is_extended}"
f"数据:{data.hex()}|长度:{len(data)}字节"
)
# logger.info(
# #f"UART发送数据帧ID0x{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)<frame_len:
#补0
frame = frame.ljust(frame_len, b'\x00')
frame_list.append(frame)
sysbus_cmd = f'{dev_name} SendRxBufferDataString "0x{frame.hex()}"'
logger.info(f"CAN发送指令{sysbus_cmd}")
response = renode.send_sync(sysbus_cmd,timeout_ms=2)#2026/4/10
# response = renode.enqueue_cmd(sysbus_cmd)#2026/6/4
time.sleep(0.0001)
#sysbus_cmd = f'{dev_name} SendRxBufferDataString "0x{data.hex()}"'
#print(sysbus_cmd)
#logger.info(f"CAN发送指令{sysbus_cmd}")
#response = renode.send_sync(sysbus_cmd)#2026/4/10
# 实际逻辑调用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

View File

@@ -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发送数据帧ID0x{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

73
midware/drivers/bus_oc.py Normal file
View File

@@ -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发送数据帧ID0x{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

131
midware/drivers/bus_uart.py Normal file
View File

@@ -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发送数据帧ID0x{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

View File

@@ -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
)

View File

@@ -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 i<n:
buf.append(raw[i])
#当前字节FF下一个字节FF跳过下一个字节只保留一个
if raw[i] == 0xFF and i+1<n and raw[i+1]==0xFF:
i +=1
i +=1
return bytes(buf)
'''
# 去除转义FF的函数 2026/6/2
def unescape_ff(self, raw:bytes)->bytes:
out = bytearray()
idx=0
length = len(raw)
while idx<length:
cur = raw[idx]
out.append(cur)
if cur == 0xff and (idx + 1 < length) and raw[idx+1]==0xff:
idx+=1
idx+=1
return bytes(out)
def receive_loop(self):
from midware.core.bus_udp_decoupler import bus_recv_queues
from midware.config.base_config import DEVICE_CONFIG_DICT
while self.running:
try:
#data = self.tn.read_some()
data = self.socket.recv(8192)#4096修改为16384 2026/5/12 tmy 65536->8192 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)<self.MIN_LEN):
break
#从帧头向后查找结束符号
t_pos = self.tcp_buf.find(self.TAIL,2)
if t_pos == -1:
break
frame = self.tcp_buf[:t_pos+2]
#logger.info(f'去除转义FF帧前的有问题帧内容{frame.hex()}')
if(self.MIN_LEN<= len(frame) <= self.MAX_LEN):
#去除转义FF
#logger.info(f'去除转义FF帧内容')
real_data = self.unescape_ff(frame)
#logger.info(f'去除转义FF帧内容{real_data.hex()}')
#入队
try:
q = bus_recv_queues.get(protocol)
if q:
try:
q.put_nowait({
"dev_name": dev_name,
"raw_data": real_data,
"protocol": protocol,
})
logger.debug(f'接受TCP重定向数据存入bus_recv_queues内容{real_data.hex()}')
except Full as e:
print("队列已满",e)
except Exception as e:
print("其他异常",repr(e))
except:
pass
#切掉已经解析的帧
self.tcp_buf = self.tcp_buf[(t_pos+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
logger.info(f'接受TCP重定向数据存入bus_recv_queues内容{data.hex()}')
# 推入现有接收队列(完全兼容你原有架构)
q = bus_recv_queues.get(protocol)
if q:
try:
q.put_nowait({
"dev_name": dev_name,
"raw_data": data,
"protocol": protocol,
})
except:
pass
'''
except BlockingIOError:
time.sleep(0.001)
except Exception as e:
print(f"[TCP-UART] {self.dev_name} 接收异常: {e}")
break
self.running = False
def start(self):
if self.connect():
self.thread = threading.Thread(target=self.receive_loop, daemon=True)
self.thread.start()
tcp_clients[self.dev_name] = self
def stop(self):
self.running = False
if self.socket:
self.socket.close()
# ======================
# 批量启动所有UART TCP客户端
# ======================
def start_all_tcp_uart_clients():
from midware.config.base_config import DEVICE_CONFIG_DICT
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)
# 只启动端口 !=1 的UART设备
if tcp_local != 1 and tcp_bus != 1:
client = TCPUARTClient(
dev_name=dev_name,
local_port=tcp_local,
remote_port=tcp_bus
)
client.start()
time.sleep(0.1)

View File

Binary file not shown.

View File

@@ -0,0 +1,191 @@
# # -*- coding: utf-8 -*-
"""
Author Tan Mingyan
@Time : 2026/2/27
"""
"""
UDP数据协议分发处理器
功能解析udp_server.recv_multi_udp_hex()返回的数据按protocol字段分发到对应处理函数
支持协议UART、SYNC、CAN、1553B、AD、OC、网络
"""
import sys
import io
import os
# 设置标准输出和标准错误的编码为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 typing import List, Dict, Any
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))
import midware.config.base_config as base_config
#from midware.config.base_config import DEVICE_CONFIG_DICT, UDP_CONFIG
# ==================== 协议类型常量(统一管理,避免硬编码)====================
# 与你要求的协议类型严格对应
PROTOCOL_UART = "UART"
PROTOCOL_SYNC = "SYNC"
PROTOCOL_CAN = "CAN"
PROTOCOL_1553B = "1553B"
PROTOCOL_AD = "AD"
PROTOCOL_OC = "OC"
PROTOCOL_NETWORK = "网络"
# 支持的协议列表(用于合法性校验)
SUPPORTED_PROTOCOLS = [
PROTOCOL_UART,
PROTOCOL_SYNC,
PROTOCOL_CAN,
PROTOCOL_1553B,
PROTOCOL_AD,
PROTOCOL_OC,
PROTOCOL_NETWORK
]
# ==================== 各协议处理函数(预留接口,按需填充逻辑)====================
def handle_uart(data: Dict[str, Any]) -> 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()

View File

@@ -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.000001s1us满足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

View File

View File

View File

View File

View File

View File

@@ -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()

0
midware/ui/__init__.py Normal file
View File

Binary file not shown.

Binary file not shown.

94
midware/ui/main_ui.py Normal file
View File

@@ -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()

49
requirements.txt Normal file
View File

@@ -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

22
tests/test_udp_connect.py Normal file
View File

@@ -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']}(自测客户端监听)")

295
tests/test_udp_connect_2.py Normal file
View File

@@ -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()

View File

@@ -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()

410
tests/udp_auto_test.py Normal file
View File

@@ -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():
"""用例1UDP端口连接性检测"""
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):
"""用例41553B时延测试要求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):
"""用例510机并发接入测试要求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):
"""用例6CAN/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()

162
tests/udp_test_connect.py Normal file
View File

@@ -0,0 +1,162 @@
'''
功能对等客户端:模拟故障注入软件,发送/接收UDP数据
authorTan 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.12. 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)