chronaxis.sources.can_spec
源码基线:8904c2f744951857659abd40aaa3535d57faf53c
1"""声明有序BLF来源,并保存解码样本的完整CAN定位。23公共Runner将LegacyCANSpec接到共用BLF读取器,低层旧适配器仍可消费此声明。4LegacyCANSpec接收首个Path、可选连续paths、一个DBC Path、明确通道、TimebaseSpec和原浮点秒5表示,输出冻结声明;构造只查类型、后缀和纳秒偏移,不open/stat/resolve,不加载6旧模块、CAN库或DBC,也不猜时区、单位、方向、实际观测范围和解码质量。7CANSourceRef在通用会话/来源/field上保留原路径、profile、通道、信号、实际帧型、8yield序号、原浮点秒及时间量化标志。field是ID/信号及可选帧类型的规范JSON,9不能把原名称当DSL变量、隐藏改名、再次施加DBC缩放或声称原物理行号。10仅依赖通用模型和基于标准库的CAN读取记录协议;无文件、子进程、reader或预算生命周期。11"""1213# 冻结声明和显式路径不访问文件;原浮点秒只检查有限性,不恢复丢失精度。14from dataclasses import dataclass15from math import isfinite16from pathlib import Path17from ..core.models import SourceRef, TimebaseSpec18from ..core.validation import require_identifier, require_text19from ..legacy.records import (LegacyCANField, MAX_WIRE_BYTES, PROFILE_ID, decode_packet, field_from_dict)20from ..timebase.conversion import require_int64_ns212223def _nonnegative_integer(value, name):24 """检查原生非负通道/序号,不接受bool或外部整数子类替代。2526 参数: value为候选值;name为字段诊断名。返回: None,不重编号或转换类型。27 异常: TypeError为非原生int,ValueError为负数;无I/O。28 """29 if type(value) is not int:30 raise TypeError(name + '需要原生int')31 if value < 0:32 raise ValueError(name + '需要非负整数')333435def can_field_from_key(text):36 """从严格规范JSON恢复完整原定位,复用CAN定位模型,不解析DBC。3738 参数: text为LegacyCANField.key生成的原生str,UTF-8包不超过65536字节。39 返回: LegacyCANField,保信号名大小写、点号及可选帧类型,不读取DBC。40 异常: TypeError/ValueError/UnicodeError为类型、JSON、重复键、字段、数值或非规范键;41 不接受仅语义相同但空白、键序或转义不同的文本,不eval或反序列化任意对象。42 """43 if type(text) is not str:44 raise TypeError('CAN field需要规范JSON str')45 # 先以字符数拒绝明显超长输入,再核UTF-8字节、JSON树和唯一定位字段。46 if not text or len(text) > MAX_WIRE_BYTES:47 raise ValueError('CAN field为空或超过wire上限')48 field = field_from_dict(decode_packet(text.encode('utf-8')))49 if text != field.key:50 raise ValueError('CAN field不是规范JSON定位键')51 return field525354@dataclass(frozen=True)55class LegacyCANSpec:56 """冻结有序同格式BLF、单通道和单DBC声明,不认证输入文件或实际旧链能力。"""5758 name: str # 当前计划来源的安全简单名称,不是DBC消息名。59 path: Path # 首个BLF声明;不扫描目录,连续文件由paths显式提供。60 dbc_path: Path # 同次worker加载的唯一DBC声明,不在此读取。61 channel: int # 明确单通道,非负原生int,不猜缺失通道。62 timebase: TimebaseSpec # 完整时基声明,来源名称相同不自动证明同步。63 unit: str = 's' # 原函数返回浮点秒,首版只接受s,不按文件名推断。64 offset_ns: int = 0 # 明确int64固定纳秒偏移,不更改原timestamp。65 legacy_profile: str = PROFILE_ID # 唯一冻结sclicer-functions-v1,不自动回退profile。66 decode_scope: str = 'requested' # 默认仅处理本次实际需要的报文/字段;all显式保留完整诊断。67 input_check: str = 'auto' # auto按实际消费者检查;full显式检查全部原帧和时间。6869 paths: tuple[Path, ...] = () # 显式连续BLF;空tuple沿用path单文件,首项必须等于path。70 decode_policy: str = 'legacy' # 新版本默认strict;旧调用保原宽松解码。71 file_merge: str = 'concatenate' # concatenate按文件顺序;time_order按原时间稳定归并。7273 @property74 def input_paths(self):75 """取得原序文件。参数: 无。返回: tuple。异常: 无;不扫描目录。"""76 return self.paths or (self.path,)7778 def __post_init__(self):79 """检查静态来源约束,路径和时基对象均原样保留。8081 参数: 无,读取本冻结声明字段。返回: None。82 异常: TypeError为路径/通道/时基/单位/profile/偏移/解码范围类型错误;ValueError为名称、83 后缀、通道、单位、profile或解码/检查范围不符;OverflowError为offset_ns超出int64。84 副作用: 无open/stat/resolve/DBC/依赖加载,不核实存在性或实际BLF内容。85 """86 if type(self.file_merge) is not str or self.file_merge not in ('concatenate', 'time_order'):87 raise ValueError('file_merge需要concatenate或time_order')88 require_identifier(self.name, 'can.name')89 # 后缀只限定本次支持范围;DBC保Path,不额外猜格式、改路径或检查内容。90 if not isinstance(self.path, Path) or not isinstance(self.dbc_path, Path):91 raise TypeError('CAN path/dbc_path需要Path')92 if self.path.suffix.lower() != '.blf':93 raise ValueError('首版CAN仅声明单BLF来源')94 from ..legacy.decode_policy import validate_decode_policy95 validate_decode_policy(self.decode_policy)96 if (type(self.paths) is not tuple or len(self.paths) > 409697 or any(not isinstance(p, Path) or p.suffix.lower() != '.blf' for p in self.paths)98 or self.paths and self.paths[0] != self.path):99 raise ValueError('连续BLF需要原序Path tuple且首项等于path')100 _nonnegative_integer(self.channel, 'can.channel')101 if not isinstance(self.timebase, TimebaseSpec):102 raise TypeError('CAN timebase需要TimebaseSpec')103 if type(self.unit) is not str or type(self.legacy_profile) is not str:104 raise TypeError('CAN unit/legacy_profile需要原生str')105 if self.unit != 's' or self.legacy_profile != PROFILE_ID:106 raise ValueError('CAN仅支持原浮点秒与sclicer-functions-v1')107 # 通用时间校验允许其他Integral;此静态契约额外要求原生int而非隐式归一化。108 if type(self.offset_ns) is not int:109 raise TypeError('CAN offset_ns需要原生int')110 require_int64_ns(self.offset_ns, 'can.offset_ns')111 # 本模型只检查选项值;requested与整个计划的兼容性由静态规划统一检查。112 if type(self.decode_scope) is not str:113 raise TypeError('CAN decode_scope需要原生str')114 if self.decode_scope not in ('all', 'requested'):115 raise ValueError('CAN decode_scope需要all或requested')116 # 检查策略与解码范围分开;完整计划负责确定实际时间消费者。117 if type(self.input_check) is not str:118 raise TypeError('CAN input_check需要原生str')119 if self.input_check not in ('auto', 'full'):120 raise ValueError('CAN input_check需要auto或full')121122123@dataclass(frozen=True)124class CANSourceRef(SourceRef):125 """保留一次原yield对应样本的CAN身份,不替代实际绑定或质量检查。126127 继承session_id/source_name/field;实际定位须满足field声明的可选类型约束。128 yield_sequence从0开始且保原来源顺序,同刻消息不会被去重;不是物理行号。129 原timestamp和量化标志同时保留,不用新纳秒坐标覆盖原浮点诊断。130 """131132 path: str # 原单BLF路径文本,是否真实打开由实际read证明。133 profile: str # 实际固定旧函数链标识。134 channel: int # 明确来源通道。135 signal: str # 原DBC信号名,不裁剪、替换点号或改大小写。136 frame_id: int # 原报文ID,与field一致。137 is_extended: bool # 原定位标准/扩展帧标志。138 is_fd: bool # 原定位经典CAN/FD标志。139 yield_sequence: int # 0起原函数yield序号,不是legacy_sum的历史求值序号。140 timestamp: float # 有限原浮点秒,可保负数和-0.0;精度已损失时不能恢复。141 time_quantized: bool # 显式时间转换是否按纳秒量化,不等于浮点精度完好。142143 def __post_init__(self):144 """校验通用身份与完整原定位相等,不访问路径或重新解码。145146 参数: 无。返回: None,全部原声明保持冻结。147 异常: TypeError/ValueError/UnicodeError为身份、profile、JSON定位、序号、原时间148 或量化标志非法,或field与附加定位冲突;没有失效补零或隐式别名。149 """150 super().__post_init__()151 require_text(self.path, 'can_ref.path')152 if type(self.profile) is not str or self.profile != PROFILE_ID:153 raise ValueError('CANSourceRef仅支持sclicer-functions-v1')154 # field保留用户的可选选择器,附加帧类型记录实际帧;无需逐条重新解析JSON。155 _nonnegative_integer(self.channel, 'can_ref.channel')156 if type(self.frame_id) is not int or type(self.signal) is not str:157 raise TypeError('CANSourceRef实际ID需要int、信号名需要str')158 if type(self.is_extended) is not bool or type(self.is_fd) is not bool:159 raise TypeError('CANSourceRef必须保存实际帧类型bool')160 actual = LegacyCANField(self.channel, self.signal, self.frame_id, self.is_extended, self.is_fd).key161 # 规范键按名称排序,这两个字段都在signal之前;只删除完整键值及逗号。162 # 信号文本中的双引号会被JSON转义,因此不能误删文本中的相似片段。163 extended = '"is_extended":' + str(self.is_extended).lower() + ','164 fd = '"is_fd":' + str(self.is_fd).lower() + ','165 if type(self.field) is not str or self.field not in (166 actual, actual.replace(extended, ''), actual.replace(fd, ''),167 actual.replace(extended, '').replace(fd, '')):168 raise ValueError('CANSourceRef实际帧与绑定定位不一致')169 _nonnegative_integer(self.yield_sequence, 'can_ref.yield_sequence')170 if type(self.timestamp) is not float:171 raise TypeError('CANSourceRef timestamp需要原生float秒')172 if not isfinite(self.timestamp):173 raise ValueError('CANSourceRef timestamp必须有限')174 if type(self.time_quantized) is not bool:175 raise TypeError('CANSourceRef time_quantized需要bool')176177 @property178 def evaluation_index(self):179 """提供本次原yield位置的只读别名,明确不声称legacy_sum求值流已存在。180181 参数: 无。返回: 与yield_sequence完全相同的int。异常: 无;不产生额外求值或计数。182 """183 return self.yield_sequence184