chronaxis.legacy.updates
源码基线:8904c2f744951857659abd40aaa3535d57faf53c
1"""表示旧路径一次实际求值所见的更新,并校验单来源的求值顺序。23输入是调用方已经取得的时间、原帧身份、SourceRef、原求值序号和完整值字典;4输出是冻结的LegacyDecodedUpdate及只保存最后事实的LegacyUpdateOrder。5无需时间的固定周期消息累计显式使用LegacyUntimedUpdate,其time_ns只能6为None;同一顺序器不能混用有时间和无时间更新,不补造0时间。7模型保留原字段全文、None和非有限浮点,不推断类型、单位、缩放、TTL、缓存8或有效性。QualityFlag.NONE只表示没有记录质量原因,不证明值的新鲜度。910本模块只依赖标准库和共享核心模型,不读取日志、不调用旧解码器,也不提供11旧求值点的采集接口。调用方构造这些对象不能证明真实旧生产者已经暴露求值12过程;真实求值证据仍须由后续来源契约提供。值字典由调用方提供,本层复制13并冻结它,不宣称该复制已有执行预算或IPC大小界限。14"""1516# 只复用类型、时间及标量校验;原字段不经过表达式别名或CAN线协议转换。17from collections.abc import Mapping18from dataclasses import dataclass19from types import MappingProxyType2021from ..core.models import SourceRef22from ..core.types import QualityFlag23from ..core.validation import require_identifier, require_scalar24from ..timebase.conversion import require_int64_ns252627def _require_nonnegative_int(value: int, field: str) -> int:28 """校验原生非负整数身份,不把布尔或数值子类转成合法序号。2930 参数:31 value: 原生int,表示通道、帧ID、来源序号或求值序号。32 field: 错误消息使用的字段名称。33 返回:34 int: 输入的原整数;不重编号,也不设置额外的序号上限。35 异常:36 TypeError: 输入不是精确的原生int,包括bool和整数子类。37 ValueError: 输入为负数。38 """39 # 身份整数的协议比共享时间的Integral协议严格,不能隐式接受数组标量。40 if type(value) is not int:41 raise TypeError(f"{field}必须为原生非布尔整数")42 if value < 0:43 raise ValueError(f"{field}必须非负")44 return value454647@dataclass(frozen=True)48class LegacyDecodedUpdate:49 """冻结一次求值的原值快照,区分原帧序号与求值序号。5051 time_ns按共享int64纳秒协议规范化;sequence定位原帧,evaluation_index52 定位求值点,同一帧可以有多次求值。四个原帧标志必须显式传入,不能由ID53 或载荷猜测。source_ref保留原对象并核对source,不补造旧缓存或求值证据。54 values只接收精确dict或MappingProxyType,复制后冻结;键与原生不可变55 标量保持原样,空字典合法,非有限浮点不会被替换或自动附加质量原因。56 """5758 time_ns: int # 调用方已取得的时间;仅按共享时间协议规范为Python整数。59 source: str # 与SourceRef.source_name一致的来源标识。60 channel: int # 原生非负通道编号,不从其他帧信息推断。61 frame_id: int # 标准帧或扩展帧范围内的原CAN ID。62 is_extended: bool # 原扩展帧标志,决定ID的合法上限。63 is_fd: bool # 原CAN FD标志,不由数据长度推断。64 is_remote: bool # 原远程帧标志,必须由调用方明确提供。65 is_error: bool # 原错误帧标志,不由质量位推断。66 source_ref: SourceRef # 原来源引用;允许已有SourceRef子类。67 sequence: int # 原帧序号;不要求连续,也不等同于求值序号。68 evaluation_index: int # 旧求值顺序;跨帧及同帧均独立递增。69 values: Mapping[str, int | float | bool | str | None] # 原字段和值的独立冻结副本。70 quality: QualityFlag = QualityFlag.NONE # 只保留明确记录的原因位。7172 def __post_init__(self) -> None:73 """校验显式身份并建立不受调用方字典后续修改影响的快照。7475 参数:76 无额外参数;读取构造时传入的全部字段。77 返回:78 None: 成功后time_ns已规范,values为新MappingProxyType。79 异常:80 TypeError: 身份、标志、来源引用、质量位、字典或标量类型不符。81 ValueError: 来源不匹配、负身份、ID越界或值字典含空键。82 TimeOverflowError: time_ns超出共享int64纳秒范围。83 MemoryError: 复制原值快照所需的分配失败;不返回部分模型。84 """85 # 时间沿已有协议允许Integral并精确规范;其余身份须为原生整数。86 time_ns = require_int64_ns(self.time_ns)87 # 无时间子类复用同一身份/数值校验,原入口仍在此严格要求真实时间。88 self._freeze_fields()89 object.__setattr__(self, "time_ns", time_ns)9091 def _freeze_fields(self) -> None:92 """校验并冻结有时间与无时间更新共有的身份、质量和原值。9394 参数:95 无额外参数;读取当前模型中除time_ns外的全部字段。96 返回:97 None: values被独立复制并冻结,其他身份及质量保持原值。98 异常:99 TypeError: 身份、标志、来源引用、质量或原值类型不合法。100 ValueError: 来源不匹配、负身份、ID越界或值字典含空键。101 MemoryError: 复制失败,调用方不能得到部分构造的模型。102 副作用:103 仅在当前待构造模型上安装冻结字典,不访问或伪造时间。104 """105 # 两种更新的区别仅为时间需求,不放宽原帧身份或原值契约。106 require_identifier(self.source, "legacy_update.source")107 _require_nonnegative_int(self.channel, "channel")108 _require_nonnegative_int(self.frame_id, "frame_id")109 _require_nonnegative_int(self.sequence, "sequence")110 _require_nonnegative_int(self.evaluation_index, "evaluation_index")111112 # 四个标志各自来自原帧,不用Python真值转换补齐缺失或整数标志。113 for field in ("is_extended", "is_fd", "is_remote", "is_error"):114 if type(getattr(self, field)) is not bool:115 raise TypeError(f"{field}必须为原生bool")116 maximum_id = 0x1FFFFFFF if self.is_extended else 0x7FF117 if self.frame_id > maximum_id:118 raise ValueError("frame_id超出对应标准帧或扩展帧范围")119120 # 引用只核对已有类型和来源身份,不把手工引用当作真实旧求值的证明。121 if not isinstance(self.source_ref, SourceRef):122 raise TypeError("source_ref必须为SourceRef")123 if self.source_ref.source_name != self.source:124 raise ValueError("source_ref.source_name必须与source一致")125 if not isinstance(self.quality, QualityFlag):126 raise TypeError("quality必须为QualityFlag")127128 # 边遍历边校验并复制,避免先校验后再遍历时混入未经检查的新值。129 if type(self.values) not in (dict, MappingProxyType):130 raise TypeError("values仅接受精确dict或MappingProxyType")131 copied = {}132 for key, value in self.values.items():133 if type(key) is not str:134 raise TypeError("values的键必须为原生str")135 if not key:136 raise ValueError("values的键不能为空")137 require_scalar(value)138 copied[key] = value139140 # 值均为不可变原生标量,单层独立复制已足够;不深拷贝或改写数值。141 frozen_values = MappingProxyType(copied)142 object.__setattr__(self, "values", frozen_values)143144145@dataclass(frozen=True)146class LegacyUntimedUpdate(LegacyDecodedUpdate):147 """保存无需时间的固定周期消息求值,复用原更新的完整身份和原值合同。148149 time_ns只能显式为None,表示未采集时间;不代表0时刻或无观测数据。150 sequence与evaluation_index保留实际原序及求值序,其他字段与父类一致。151 本模型仍是LegacyDecodedUpdate,可交由相同逐消息数值核求值;调用方152 必须确保没有时间消费者,并且不从本模型构造带时间的贡献或观测范围。153 """154155 time_ns: None # 显式未采集;必填,禁止借默认0或其他占位满足时间模型。156157 def __post_init__(self) -> None:158 """拒绝任何实数或占位时间,仅执行父类的共有字段校验。159160 参数:161 无额外参数;读取构造字段,time_ns必须为None。162 返回:163 None: 原值冻结,身份和质量检查与LegacyDecodedUpdate完全相同。164 异常:165 TypeError: time_ns不是None,或共有字段类型不合法。166 ValueError或MemoryError: 共有身份越界、来源冲突或复制失败。167 副作用:168 仅冻结当前模型,不构造虚假时间或修改调用方值字典。169 """170 # 调用方不能把带真实时间的输入误报为未采集时间。171 if self.time_ns is not None:172 raise TypeError("LegacyUntimedUpdate.time_ns必须为None")173 self._freeze_fields()174175176class LegacyUpdateOrder:177 """校验同一会话和来源的逐次求值,不累计原值或维护旧信号缓存。178179 session_id和source固定;last_time_ns、last_sequence、180 last_evaluation_index初始为None,count初始为0。时间和原帧序号不回退,181 求值序号严格递增且允许间隙。首个更新固定是否带时间,之后禁止混用;182 无时间模式的last_time_ns始终为None,仅按原序和求值序前进。183 同一原帧序号还须保留完全相同的时间、184 通道、ID和四个标志;值、质量及section引用可不同。最后事实保存在一个185 当前状态tuple中,全部检查和新状态分配完成后才一次替换;拒绝不改状态。186 """187188 def __init__(self, session_id: str, source: str) -> None:189 """建立一个来源的空顺序状态,不接收或消费任何更新。190191 参数:192 session_id: 沿共享模型规则校验的会话标识。193 source: 沿共享模型规则校验的来源标识。194 返回:195 None: 创建只读身份及初始空状态。196 异常:197 TypeError: 会话或来源不是文本。198 ValueError: 会话或来源不是合法的共享简单标识符。199 """200 # 身份只在构造时安装,公开属性不提供可绕过来源检查的赋值入口。201 self._session_id = require_identifier(session_id, "session_id")202 self._source = require_identifier(source, "source")203 self._state = (None, None, None, 0, None)204205 @property206 def session_id(self) -> str:207 """返回固定会话标识;参数: 无;返回: 原str;异常: 无。"""208 return self._session_id209210 @property211 def source(self) -> str:212 """返回固定来源标识;参数: 无;返回: 原str;异常: 无。"""213 return self._source214215 @property216 def last_time_ns(self) -> int | None:217 """读取最后接受时间;参数: 无;返回: int或初始None;异常: 无。"""218 return self._state[0]219220 @property221 def last_sequence(self) -> int | None:222 """读取最后原帧序号;参数: 无;返回: int或初始None;异常: 无。"""223 return self._state[1]224225 @property226 def last_evaluation_index(self) -> int | None:227 """读取最后求值序号;参数: 无;返回: int或初始None;异常: 无。"""228 return self._state[2]229230 @property231 def count(self) -> int:232 """读取已接受次数;参数: 无;返回: 非负int;异常: 无。"""233 return self._state[3]234235 def accept(self, update: LegacyDecodedUpdate) -> None:236 """接受一个完整更新,仅在全部身份及顺序检查成功后提交最后事实。237238 参数:239 update: 已构造的LegacyDecodedUpdate或LegacyUntimedUpdate;240 同帧可携带不同值快照,同一顺序器禁止混用时间模式。241 返回:242 None: 接受成功,count加一,最后事实切换到本次更新。243 异常:244 TypeError: update不是LegacyDecodedUpdate。245 ValueError: 会话、来源不符,时间模式混用,时间或原帧序号回退,求值序号不递增,246 或同一原帧序号的时间、通道、ID、四个标志出现冲突。247 MemoryError: 新状态分配失败;旧状态仍保持不变。248 """249 # 只接受已验证的更新模型;序号关系不从其他帧对象或原值中猜测。250 if not isinstance(update, LegacyDecodedUpdate):251 raise TypeError("update必须为LegacyDecodedUpdate")252 if update.source != self._source or update.source_ref.session_id != self._session_id:253 raise ValueError("update必须属于本会话和来源")254255 # 当前帧身份不含值、质量或section引用,允许同帧多次真实求值。256 frame = (257 update.time_ns, update.channel, update.frame_id,258 update.is_extended, update.is_fd, update.is_remote, update.is_error,259 )260 last_time, last_sequence, last_evaluation, count, last_frame = self._state261 # count区分空状态和已接受无时间更新;保持原五元状态便于原子复制。262 if count and (last_time is None) != (update.time_ns is None):263 raise ValueError("同一顺序器不能混用有时间和无时间更新")264 if last_time is not None and update.time_ns < last_time:265 raise ValueError("time_ns不能回退")266 if last_sequence is not None and update.sequence < last_sequence:267 raise ValueError("sequence不能回退")268 if last_evaluation is not None and update.evaluation_index <= last_evaluation:269 raise ValueError("evaluation_index必须严格递增")270 if update.sequence == last_sequence and frame != last_frame:271 raise ValueError("同一sequence的原帧身份必须一致")272273 # 先完成计数和tuple分配,再以单一写入提交;拒绝路径不接触已有状态。274 next_state = (275 update.time_ns, update.sequence, update.evaluation_index, count + 1, frame,276 )277 self._state = next_state278