"""MIL SDK 核心数据模型""" import os import json import logging from dataclasses import dataclass, field, fields from typing import Any logger = logging.getLogger(__name__) @dataclass class Config(): """MIL SDK 全局配置 承载测试工程相关的全局开关与路径信息,是 UI 层与 core 层之间的 配置传输载体。序列化使用 JSON 文件持久化。 Attributes: path: 配置文件路径(JSON 文件) DataPath: 仿真数据目录 FilePath: 当前打开的 Excel 文件路径 AddTimeEn: 用例步骤时间是否按累加方式记录 GeratePath: 是否为生成的用例另存新文件 CurrProject: 当前工程名称 ItemConfigs: 各模块/用例项的细粒度配置 """ path:str DataPath:str = "" FilePath:str = "" AddTimeEn:bool = True GeratePath:bool = True CurrProject:str = "" # dict 是可变类型,必须用 default_factory 显式实例化,避免多个 Config 共享同一对象 ItemConfigs: dict = field(default_factory=dict) def check_path(self): return os.path.exists(self.path) def to_dict(self): """将 Config 实例序列化为 dict,便于写入 JSON 文件。""" return { "DataPath":self.DataPath, "FilePath":self.FilePath, "AddTimeEn":self.AddTimeEn, "GeratePath":self.GeratePath, "CurrProject":self.CurrProject, "ItemConfigs":self.ItemConfigs, } def load_config(self) -> "Config": """从 self.path 指定的 JSON 文件读取配置并填充到当前实例的各个字段。 解析规则: - JSON 中存在的字段会被回写到 Config 的对应字段; - JSON 中缺失的字段保持当前 Config 实例的默认值; - JSON 中多余字段被忽略(向前兼容:增加字段不会破坏老配置)。 Returns: self:填充后的 Config 实例,便于链式调用。 Raises: FileNotFoundError: 配置文件不存在。 json.JSONDecodeError: 文件内容不是合法 JSON。 """ try: # 以 UTF-8 读取 JSON,避免 Windows 默认编码带来的乱码问题 with open(self.path, 'r', encoding='utf-8') as f: raw = json.load(f) except FileNotFoundError: # 显式记录并重抛,遵循"错误必须显式处理,禁止静默失败" logger.error(f"配置文件{self.path}未找到") raise if not isinstance(raw, dict): # 文件不合法(非 dict 根节点)→ 用一个空 dict 填充,保持实例可用 raw = {} # 用反射取出本类已声明的字段名白名单,避免 JSON 脏字段污染 allowed = {f.name for f in fields(self.__class__)} for key, value in raw.items(): if key in allowed: setattr(self, key, value) return self def save_config(self): """将当前配置以 JSON 格式写入 self.path 指定的文件。 Raises: Exception: 写入失败时记录日志并原样抛出异常。 """ try: with open(self.path,'w',encoding='utf-8') as f: json.dump(self.to_dict(), f, indent=4, ensure_ascii=False) except Exception as e: logger.error(f"配置文件{self.path}写入失败: {e}") raise @dataclass class DataLog: """仿真数据日志记录 Attributes: time: 时间戳(秒) value: 信号值(字符串形式,写入 Excel 时按内容推断 int/str 类型) """ time: float = 0.0 value: str = "" @dataclass class SignalData: """信号数据封装 描述 Excel 中某一列信号的完整信息: - 所在列号(column) - 列上方的属性行(attributes,例如 Signal Name / BlockPath 等) - 时间-值采样序列(datalog) Attributes: column: 信号在 Excel 工作表中的列索引(1-based) attributes: 列上方各属性行(行号 -> 文本)的字典映射 datalog: 时间戳-数值采样点列表 """ column: int = 0 attributes: dict[str, str] = field(default_factory=dict) datalog: list[DataLog] = field(default_factory=list) def to_dict(self) -> dict[str, Any]: """将 SignalData 转为 dict,便于跨层传输与持久化。 Returns: 包含 column、attributes、datalog 字段的字典。 """ return { "column": self.column, "attributes": self.attributes, "datalog": self.datalog } @dataclass class ExcelDataResult: """Excel 数据读取结果封装 提供对 Excel 数据的类型安全访问,隐藏内部实现细节 Attributes: sheet_name: 工作表名称 source_row: Source: Input 所在行号 signals: 信号名称到信号数据的映射 """ sheet_name: str = "Scenario1" source_row: int = 0 signals: dict[str, SignalData] = field(default_factory=dict) def get_signal(self, name: str) -> SignalData | None: """获取指定信号的数据 Args: name: 信号名称 Returns: 信号数据对象,如果不存在返回 None """ return self.signals.get(name) def get_signal_names(self) -> list[str]: """获取所有信号名称 Returns: 信号名称列表 """ return list(self.signals.keys())