chronaxis.core.message_binding
源码基线:8904c2f744951857659abd40aaa3535d57faf53c
1"""为当前消息表达式声明参数,并把独立消息仓事实关联为运行绑定。23ExpressionParameter将显式单位的配置数值规范成精确FLOAT64。MessageDeclarations4引用当前DeclarationBindings中选定来源的原SignalDefinition,不复制或虚构信号、5普通历史或TTL;当前policy只使用本条消息原字典,不继承上一条值。6MessageSourceEvidence只检查显式来源标签、全仓摘要、条数和首末的结构;7MessageBindings将其与完整声明身份关联。真正封存文件的EOF/SHA/条数验证由8执行层bind_message_store负责,单独构造证据不证明来源文件已经读取。910本模块只保存冻结配置与有限事实,不读文件、不执行表达式、不加载NumPy或11CAN,也不证明仓来自同一次CAN捕获或DBC单位已核实。这些关联留给实际生产端。12身份使用新版本前缀与固定字段的长度前缀增量SHA,不改变普通绑定的身份协议。13模型及调用者保留的配置由调用者拥有;执行工厂另付其新运行元数据费用。14"""1516# 参数、证据及绑定均不可变;数值配置不从对象隐式推断。17from dataclasses import dataclass, field18from hashlib import sha25619from math import isfinite2021from .binding import SignalDefinition22from .declarations import DeclarationBindings23from .unit_conversion import binding_version, require_config_version24from .unit_registry import REGISTRY_VERSION, resolve_unit25from .types import SignalDType26from .validation import require_identifier, require_text27from ..timebase.conversion import require_int64_ns282930def _hash_parts(parts: tuple[str, ...]) -> str:31 """按固定模型字段顺序增量编码文本身份,失败不保活新增整段编码副本。3233 参数: parts为本模块已检查的原生文本tuple,首项含独立版本前缀。34 返回: 小写SHA-256文本;每段UTF-8字节有长度前缀,分隔符不能造成碰撞。35 异常: 原Unicode、内存或取消错误;不调用外部repr或保存全量canonical文本。36 """37 raw = None38 try:39 digest = sha256()40 for part in parts:41 # 每次只保留一段编码;不同文本和字段边界均进入新身份。42 if type(part) is not str:43 raise TypeError('消息身份只编码已检查的原生文本')44 raw = part.encode('utf-8')45 digest.update(str(len(raw)).encode('ascii'))46 digest.update(b':')47 digest.update(raw)48 raw = None49 return digest.hexdigest()50 finally:51 # 执行工厂可能在此异常后抵费,TB不得留下本层新编码或parts容器。52 raw = parts = None535455@dataclass(frozen=True)56class ExpressionParameter:57 """保存一个有明确单位、不可被对方重赋单位的FLOAT64配置参数。5859 只接受原生有限int/float;int须能精确转为有限float,不能静默舍入。60 该参数不是信号或裸数字literal,不产生来源依赖和时间保持状态。61 """6263 name: str # 表达式使用的安全简单名称。64 value: int | float # 构造后规范为有限原生float。65 unit: str # 明确单位标签;不按对方信号自动改写。66 config_version: int = field(default=4, kw_only=True) # 参数与绑定同版本,默认使用当前规则。67 unit_status: str = field(init=False) # 只说明标签是否属于已有单位表。6869 def __post_init__(self) -> None:70 """验证参数名称、精确数值及单位,不执行来源或表达式。7172 参数: 无,读取name/value/unit。73 返回: None,value规范成FLOAT64可精确表示的原生float。74 异常: TypeError为非原生数值;ValueError为非有限、有损整数或声明非法。75 """76 # 独立参数也携带规则版本,不由绑定事后重解释。77 require_config_version(self.config_version)78 if type(self.name) is not str or type(self.unit) is not str:79 raise TypeError('表达式参数名称和单位需要原生str')80 require_identifier(self.name, 'parameter.name')81 require_text(self.unit, 'parameter.unit')82 if type(self.value) not in (int, float):83 raise TypeError('表达式参数只接受原生int/float,不能为bool')84 # 在配置边界证明精确性,不把参数有损转换拖到每条消息求值中。85 try:86 number = float(self.value)87 except OverflowError as exc:88 raise ValueError('表达式参数不能表示为有限FLOAT64') from exc89 if not isfinite(number) or type(self.value) is int and int(number) != self.value:90 raise ValueError('表达式参数必须能精确表示为有限FLOAT64')91 object.__setattr__(self, 'value', number)92 known = resolve_unit(self.unit) is not None93 object.__setattr__(self, 'unit_status', 'verified' if known else 'unverified')9495 @property96 def dtype(self) -> SignalDType:97 """读取固定数值类型;参数: 无;返回: FLOAT64;异常: 无。"""98 return SignalDType.FLOAT6499100101@dataclass(frozen=True)102class MessageDeclarations:103 """为一个选定来源建立独立的当前消息名称空间,不制造运行证明。104105 signals引用原声明中的同一SignalDefinition对象。原定义的转换声明保留,106 其TTL不在本模型执行;依赖仅能来自selected source。parameters及局部107 derived_names不能覆盖原计划任何signal/derived,也不能互相覆盖。108 """109110 declarations: DeclarationBindings # 完整原配置声明及身份,非实际文件证明。111 source: str # 只允许这个原来源的信号参与当前消息表达式。112 parameters: tuple[ExpressionParameter, ...] = () # 有明确单位的配置参数。113 derived_names: tuple[str, ...] = () # 当前指标显式登记的局部派生名。114 message_policy: str = 'current_message-v1' # 只看当前字典,不沿用缓存或TTL。115 config_version: int | None = field(default=None, kw_only=True) # 继承原声明,参数必须同版。116 identity: str = field(init=False) # 完整原配置与消息配置的独立声明SHA。117118 def __post_init__(self) -> None:119 """核对来源、完整名称空间与policy,生成静态消息声明身份。120121 参数: 无,读取冻结字段,不读取文件或DBC。122 返回: None;保原定义对象,不将声明身份当真实仓EOF证明。123 异常: TypeError为模型/容器错误;ValueError为来源、名称或policy冲突。124 """125 if not isinstance(self.declarations, DeclarationBindings):126 raise TypeError('消息声明需要DeclarationBindings')127 if type(self.source) is not str:128 raise TypeError('消息来源需要原生str')129 require_identifier(self.source, 'message.source')130 if self.source not in {item.name for item in self.declarations.sources}:131 raise ValueError('消息来源不在原声明中')132 if (type(self.parameters) is not tuple or type(self.derived_names) is not tuple133 or any(type(item) is not ExpressionParameter for item in self.parameters)):134 raise TypeError('消息参数与局部派生名称需要typed tuple')135 # 原声明即使无信号也有明确版本,参数不能引入另一套单位规则。136 version = self.declarations.config_version137 if self.config_version is not None and require_config_version(self.config_version) != version:138 raise ValueError('消息声明config_version与原声明不一致')139 binding_version(self.parameters, version)140 object.__setattr__(self, 'config_version', version)141 if type(self.message_policy) is not str or self.message_policy != 'current_message-v1':142 raise ValueError('消息policy仅支持current_message-v1')143 # 全原计划名称都受保护,不能利用selected source隐藏其他来源的名称冲突。144 names = {item.name for item in self.declarations.signals} | set(self.declarations.derived_names)145 for parameter in self.parameters:146 if parameter.name in names:147 raise ValueError('消息参数与原声明或其他参数名称冲突:' + parameter.name)148 names.add(parameter.name)149 for name in self.derived_names:150 if type(name) is not str:151 raise TypeError('消息局部派生名称需要原生str')152 require_identifier(name, 'message.derived_name')153 if name in names:154 raise ValueError('消息局部派生名称冲突:' + name)155 names.add(name)156 parts = None157 try:158 # 数量字段固定参数与派生分段;完整base身份含未选来源,不能遗漏配置变化。159 parts = ('message-declaration-v1', self.declarations.identity, self.source,160 self.message_policy, str(len(self.parameters)))161 parts += tuple(value for item in self.parameters162 for value in (item.name, item.value.hex(), item.unit, item.unit_status))163 parts += (str(len(self.derived_names)), *self.derived_names)164 # 空信号/仅参数绑定也必须携带当前规则版本。165 parts += (str(self.config_version), REGISTRY_VERSION)166 object.__setattr__(self, 'identity', _hash_parts(parts))167 finally:168 # 只清本层新身份容器,原声明/参数仍由调用者持有。169 parts = None170171 @property172 def extensions(self):173 """读取宿主注册。参数: 无。返回: 冻结注册或None。异常: 无。"""174 return self.declarations.extensions175176 @property177 def lookups(self):178 """读取普通声明中的标定表。参数: 无。返回: 冻结tuple。异常: 无。"""179 return self.declarations.lookups180181 @property182 def signals(self) -> tuple[SignalDefinition, ...]:183 """选择同一原定义对象;参数: 无;返回: tuple;异常: 无主动异常。"""184 return tuple(item for item in self.declarations.signals if item.source == self.source)185186 def resolve(self, name: str) -> SignalDefinition:187 """只定位本来源原信号,参数名称另由编译器处理。188189 参数: name为安全简单信号别名。返回: 原SignalDefinition对象。190 异常: ValueError为未知、其他来源或非法名称;不从点号猜测原字段。191 """192 require_identifier(name, 'message.signal_alias')193 for definition in self.signals:194 if definition.name == name:195 return definition196 raise ValueError('未知或不属消息来源的信号别名:' + name)197198 def require_synchronized(self, names: tuple[str, ...]) -> None:199 """检查实际依赖均在本来源,不借其他来源的同步声明引入缓存值。200201 参数: names为展开后的原信号依赖tuple,空依赖合法。202 返回: None;单源当前字典不需要合并其他来源时钟。203 异常: TypeError为容器错误;ValueError为非本来源或未知依赖。204 """205 if type(names) is not tuple:206 raise TypeError('消息依赖names需要tuple')207 for name in names:208 self.resolve(name)209210211@dataclass(frozen=True)212class MessageSourceEvidence:213 """保存独立解码消息仓的有限事实,仅校结构而不声明真实回读已发生。214215 decoder_profile/read_identity是仓内显式标签,不认证旧CAN来源。原序SHA216 包含记录和值/映射时间;首末可相同,空仓必须两端None且条数为0。217 """218219 source: str # 本消息来源。220 session_id: str # 原声明会话。221 decoder_profile: str # 显式解码来源标签,不冒充计算policy。222 read_identity: str # 仓的显式读取标识,CAN同次关联由生产端另核。223 store_sha256: str # 已封口全消息行流摘要。224 record_count: int # 全decoded条数,不是普通历史样本数。225 first_time_ns: int | None # 全仓首条映射坐标,空仓为None。226 last_time_ns: int | None # 全仓最后坐标,不补采样周期。227228 def __post_init__(self) -> None:229 """验证有限证据一致性,单独构造不能替代实际EOF校验。230231 参数: 无,读取冻结字段。返回: None,已知时间规范成原生int。232 异常: TypeError/ValueError为类型、SHA、条数或首末矛盾;时间超界原样传播。233 """234 if any(type(value) is not str for value in (self.source, self.session_id,235 self.decoder_profile, self.read_identity)):236 raise TypeError('消息证据来源、会话及读取标签需要原生str')237 require_identifier(self.source, 'message_evidence.source')238 require_identifier(self.session_id, 'message_evidence.session_id')239 require_text(self.decoder_profile, 'message_evidence.decoder_profile')240 require_text(self.read_identity, 'message_evidence.read_identity')241 if type(self.store_sha256) is not str:242 raise TypeError('消息仓摘要需要原生str')243 if (len(self.store_sha256) != 64244 or any(char not in '0123456789abcdef' for char in self.store_sha256)):245 raise ValueError('消息仓摘要需要64位小写十六进制SHA-256')246 if type(self.record_count) is not int or self.record_count < 0:247 raise ValueError('消息条数需要非负原生int')248 if self.record_count == 0:249 if self.first_time_ns is not None or self.last_time_ns is not None:250 raise ValueError('空消息仓首末必须为None')251 return252 if self.first_time_ns is None or self.last_time_ns is None:253 raise ValueError('非空消息仓必须有真实首末时间')254 # 共享int64单端点规则允许真实零长观测,不计算两端差值造成溢出。255 first = require_int64_ns(self.first_time_ns, 'message_evidence.first_time_ns')256 last = require_int64_ns(self.last_time_ns, 'message_evidence.last_time_ns')257 if last < first:258 raise ValueError('消息仓终点早于起点')259 object.__setattr__(self, 'first_time_ns', first)260 object.__setattr__(self, 'last_time_ns', last)261262263@dataclass(frozen=True)264class MessageBindings:265 """关联消息声明与实际仓证据,保持独立于普通DecodedBindings的运行身份。266267 证据实际性由bind_message_store完整回读负责;本模型只校结构/归属。268 不证明普通历史、TTL、DBC字段单位或同次CAN捕获,也不保存逐条记录。269 """270271 declarations: MessageDeclarations # 单源消息配置及完整原声明身份。272 evidence: MessageSourceEvidence # 执行工厂核实后的仓事实,非手造即证明。273 config_version: int | None = field(default=None, kw_only=True) # 继承消息声明,不允许运行时切换。274 identity: str = field(init=False) # 独立message-binding-v1身份。275276 def __post_init__(self) -> None:277 """核对声明与证据来源/会话,增量生成有限运行身份。278279 参数: 无,读取declarations/evidence。返回: None,不再次打开消息仓。280 异常: TypeError为模型错误;ValueError为来源/会话不符;编码或取消原错传播。281 """282 if not isinstance(self.declarations, MessageDeclarations) or not isinstance(self.evidence, MessageSourceEvidence):283 raise TypeError('消息运行绑定需要MessageDeclarations和MessageSourceEvidence')284 # 运行绑定不重设消息参数和信号的已冻结版本。285 version = self.declarations.config_version286 if self.config_version is not None and require_config_version(self.config_version) != version:287 raise ValueError('消息运行绑定config_version与声明不一致')288 object.__setattr__(self, 'config_version', version)289 if (self.evidence.source != self.declarations.source290 or self.evidence.session_id != self.declarations.declarations.session_id):291 raise ValueError('消息仓证据的来源或会话与当前声明不符')292 parts = None293 try:294 # 每一证据字段都进入独立身份,不把read标签或同名配置单独当输入证明。295 evidence = self.evidence296 parts = ('message-binding-v1', self.declarations.identity, evidence.source,297 evidence.session_id, evidence.decoder_profile, evidence.read_identity,298 evidence.store_sha256, str(evidence.record_count),299 'none' if evidence.first_time_ns is None else str(evidence.first_time_ns),300 'none' if evidence.last_time_ns is None else str(evidence.last_time_ns))301 object.__setattr__(self, 'identity', _hash_parts(parts))302 finally:303 # 原错误可被调用者保留,执行层抵费后不由本TB留下新身份容器。304 parts = None305306 @property307 def extensions(self):308 """读取宿主注册。参数: 无。返回: 冻结注册或None。异常: 无。"""309 return self.declarations.extensions310311 @property312 def lookups(self):313 """读取消息声明中的标定表。参数: 无。返回: 冻结tuple。异常: 无。"""314 return self.declarations.lookups315316 @property317 def signals(self) -> tuple[SignalDefinition, ...]:318 """读取同一原信号定义;参数: 无;返回: tuple;异常: 无主动异常。"""319 return self.declarations.signals320321 @property322 def parameters(self) -> tuple[ExpressionParameter, ...]:323 """读取冻结配置参数;参数: 无;返回: tuple;异常: 无。"""324 return self.declarations.parameters325326 @property327 def derived_names(self) -> tuple[str, ...]:328 """读取显式局部派生名;参数: 无;返回: tuple;异常: 无。"""329 return self.declarations.derived_names330331 def resolve(self, name: str) -> SignalDefinition:332 """按单来源原定义定位信号,不从证据标签生成字段。333334 参数: name为安全别名。返回: 原SignalDefinition。335 异常: ValueError为非法、未知或其他来源名称。336 """337 return self.declarations.resolve(name)338339 def require_synchronized(self, names: tuple[str, ...]) -> None:340 """执行前复核全部依赖属于本来源,不执行普通TTL或时钟合并。341342 参数: names为原信号依赖tuple。返回: None。343 异常: TypeError/ValueError为容器或非本来源依赖。344 """345 self.declarations.require_synchronized(names)346