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

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)