chronaxis.sources.text_can_decode
源码基线:8904c2f744951857659abd40aaa3535d57faf53c
1"""按需解码新ASC/CSV原帧,并接到既有数值归一化核心。23CANTextDecodeRead消费显式CANTextSpec与本次需要的SignalDefinition,逐个交付4DecodedCANRecord;未知ID、未选中和非数据帧仍保原记录,仅所需帧调用cantools。5DBC沿用decode_choices=False/allow_truncated=True参数,但新入口先严格核已选帧长度;6已定义报文解码失败直接定位,不冒充未知ID。pack_text_records输出既有SignalSeries。7完整读取核对全部原帧时间;按需ASC仅核所需通道/ID的连续顺序,并同次统计来源范围。8文件不重读;DBC加载和临时对象先付预算。9解析文件逐个关闭,失败/取消保原错误及必要恢复owner;纯BLF串行复用显式解码政策。10"""1112from dataclasses import dataclass13from decimal import Decimal14from hashlib import file_digest15from pathlib import Path1617from ..dbc_loading import load_dbc_file1819from ..core.binding import SignalDefinition20from ..core.validation import require_identifier21from ..execution.history import _metadata_bytes22from ..legacy.records import field_to_dict23from ..legacy.decode_policy import check_decode_length, decode_serial_frame24from ..legacy.worker import _field_metadata25from ..results.budget import ResourceBudget, _reserve_owned_ram, _release_owned_ram, _cleanup_owner26from ..results.errors import ResultClosedError27from .asc import ASCSpec, ASCRead28from .blf import BLFSpec, BLFRead, _UntimedBLFRecord29from .raw_csv import CANCSVRead30from .raw_can import RawCANRecord, CANTextError31from .can_spec import can_field_from_key32from .models import freeze_metadata33from .packing import pack_decoded34from .text_can_ref import CANTextSourceRef35from .text_can_spec import CANTextSpec363738@dataclass(frozen=True)39class DecodedCANRecord:40 """保留一条原帧及本次所需原信号值;非decoded记录的values必须为空。"""4142 source: str # 当前来源名称。43 input_index: int # 原输入列表位置。44 sequence: int # 全部文件连续的原帧序号,不仅是参与计算的序号。45 raw: RawCANRecord | _UntimedBLFRecord # 保留原帧;无时间需求的BLF记录明确不提供时间。46 values: tuple[tuple[str, int | float | bool | None], ...] # 规范CAN字段键到DBC物理值。47 status: str # decoded/unknown/unselected/non_data;不把错误记成unknown。48 frame_values: tuple[tuple[str,int | float | bool | None],...] = () # 显式重建所需原物理字段。49 frame_layout: tuple[tuple[str,int],...] = () # 当次复用选择器的原始码值。5051 def __post_init__(self):52 """核记录冻结结构。参数: 无。返回: None。异常: 来源、位置、状态或值结构非法。53 """54 self._validate(None)5556 @classmethod57 def _from_bound_values(cls, source, input_index, sequence, raw, values, status,58 frame_values, frame_layout, known_fields):59 """用本次绑定已经解析的字段名建立内部记录,其余检查与公开构造相同。6061 参数: 前八项与记录字段相同;known_fields为当前读取器从已验证绑定取得的字段键集合。62 返回: 新的冻结DecodedCANRecord,不持有额外的绑定副本。63 异常: 字段不在当前绑定中、值类型或记录结构不合法时拒绝。64 外部构造仍调用完整字段解析;此入口只供同次解码结果复用绑定检查。65 """66 record = object.__new__(cls)67 # 仅安装当前固定模型的八项字段,不按输入提供的属性名恢复对象。68 for name, value in zip(('source', 'input_index', 'sequence', 'raw', 'values',69 'status', 'frame_values', 'frame_layout'),70 (source, input_index, sequence, raw, values, status,71 frame_values, frame_layout)):72 object.__setattr__(record, name, value)73 record._validate(known_fields)74 return record7576 def _validate(self, known_fields):77 """检查固定记录,内部已绑定键只核成员关系。7879 参数: known_fields为本次已解析字段集合,None表示公开入口需要完整解析。80 返回: None。异常: 非法结构、未知字段、重复键或无值状态携带数据。81 """82 # 该模型不开放任意对象值,也不在外部构造时接受重复字段覆盖。83 require_identifier(self.source,'decoded_can.source')84 if (type(self.raw) not in (RawCANRecord,_UntimedBLFRecord) or type(self.input_index) is not int or self.input_index < 085 or type(self.sequence) is not int or self.sequence < 0 or type(self.values) is not tuple86 or self.status not in ('decoded','unknown','unselected','non_data')):87 raise ValueError('解码CAN记录结构或状态非法')88 keys = set()89 for pair in self.values:90 if type(pair) is not tuple or len(pair) != 2 or type(pair[0]) is not str or type(pair[1]) not in (int,float,bool,type(None)):91 raise TypeError('解码值需要规范字段键及原生数值/None')92 if known_fields is None:93 can_field_from_key(pair[0])94 elif pair[0] not in known_fields:95 raise ValueError('解码CAN字段不属于当前绑定')96 if pair[0] in keys:97 raise ValueError('解码CAN字段重复')98 keys.add(pair[0])99 if self.status != 'decoded' and self.values:100 raise ValueError('未解码原帧不能携带信号值')101 for pairs,kinds in ((self.frame_values,(int,float,bool,type(None))),(self.frame_layout,(int,))):102 if type(pairs) is not tuple or len(pairs)>4096 or any(type(p) is not tuple or len(p)!=2 or type(p[0]) is not str103 or not p[0] or type(p[1]) not in kinds for p in pairs) or len({p[0] for p in pairs})!=len(pairs):104 raise ValueError('重建物理值或布局结构非法')105 if self.status!='decoded' and (self.frame_values or self.frame_layout):106 raise ValueError('未解码原帧不能伪造重建值')107108109class CANTextDecodeRead:110 """拥有单次DBC和逐文件解析器;EOF后保有限元数据到显式close。"""111112 def __init__(self, spec, budget, signals, *, check_cancel=None, database_context=None, classify_unselected=True, cycle_fields=(), metadata_fields=(), integrity=True, trace=True, retain_source_time=True, selected_ids=None, time_frame_ids=None, emit_frame_ids=None, blf_time_scope='source',legacy_blf_decode=False,serial_blf_policy=None,post_decode_frame_ids=None):113 """只核声明并预约固定费用。参数: spec来源、budget账本、signals所需定义tuple、check_cancel取消回调;classify_unselected保旧逐帧分类,消息消费者无需区分无关ID是否在DBC中;cycle_fields只请求初始化周期,metadata_fields只请求候选,两者不加入解码投影。114115 time_frame_ids仅在无需完整顺序/原帧导出时传入:选中通道/ID之间必须有序,116 其他帧仍用于同次来源最早/最晚时间统计,不交付给计算或作为单调性前点。117 selected_ids仅供不需要全来源观测或原帧导出的纯消息计算使用:原始CSV先按ID跳过118 无关行;None保留全部行。count保原始行计数,retained_count只数实际交付的记录。119 legacy_blf_decode默认False;纯can_blf转接保留legacy长度政策和DecodeError缺值行为。120 serial_blf_policy仅纯BLF串行设置requested/all:唯一ID沿原数据库入口的普通异常121 缺值处理,多定义ID按实际帧型直接解码;all还核完整值。并行和文本政策保持独立。122 post_decode_frame_ids仅串行all且无原帧导出时传入有限ID集合:每帧完成解码和123 完整值检查后再跳过无消费者的帧。None交付全部帧;计数只数实际交付。124 纯BLF串行沿用按ID检查时间和交付,通道由后续绑定筛选;其他来源按通道及ID。125126 返回: 未开始读取的owner。异常: 声明/预算错误;持续清理失败附text_can_cleanup_owner。127 """128 # 构造不访问DBC/输入;只接受显式同来源、同通道且唯一的别名定义。129 if (type(spec) is not CANTextSpec or not isinstance(budget,ResourceBudget)130 or type(signals) is not tuple or len(signals) > 4096131 or any(type(item) is not SignalDefinition or item.source != spec.name for item in signals)132 or check_cancel is not None and not callable(check_cancel)):133 raise TypeError('文本CAN读取需要静态声明、预算及所需信号定义')134 if type(classify_unselected) is not bool:135 raise TypeError('classify_unselected需要bool')136 if type(legacy_blf_decode) is not bool or (legacy_blf_decode and137 (spec.decode_policy!='legacy' or any(type(item) is not BLFSpec for item in spec.inputs))):138 raise ValueError('legacy_blf_decode仅用于明确的纯BLF legacy转接')139 self._legacy_blf_decode=legacy_blf_decode140 if serial_blf_policy not in (None,'requested','all') or (serial_blf_policy is not None and any(type(item) is not BLFSpec for item in spec.inputs)):141 raise ValueError('纯BLF串行政策需要requested/all及全部BLF输入')142 self._serial_blf_policy=serial_blf_policy143 if post_decode_frame_ids is not None and serial_blf_policy!='all':144 raise ValueError('解码后投影仅用于纯BLF串行all政策')145 self._post_decode_frame_ids=None if post_decode_frame_ids is None else frozenset(post_decode_frame_ids)146 self._duplicate_ids=frozenset()147 if len({item.name for item in signals}) != len(signals):148 raise ValueError('文本CAN别名不能重复')149 from ..legacy.dbc_cycles import require_cycle_fields150 require_cycle_fields(cycle_fields,spec.channel)151 require_cycle_fields(metadata_fields,spec.channel)152 self._metadata_fields = metadata_fields153 self._selected_ids = selected_ids154 # 并行传输只发送实际消费者需要的帧;时间检查和来源范围仍在筛选前完成。155 self._emit_frame_ids = None if emit_frame_ids is None else frozenset(emit_frame_ids)156 self._blf_time_scope=blf_time_scope157 self.first_order_time = None158 if time_frame_ids is not None and (type(time_frame_ids) is not tuple or len(time_frame_ids)>4096159 or any(type(value) is not int or not 0 <= value <= 0x1fffffff for value in time_frame_ids)):160 raise ValueError('参与时间检查的CAN ID需要有限合法整数tuple')161 if time_frame_ids is not None and spec.input_check == 'full':162 raise ValueError('完整输入检查不能限制时间顺序检查范围')163 self._time_frame_ids = None if time_frame_ids is None else frozenset(time_frame_ids)164 self._last_selected_time = None165 parsed = tuple((item,can_field_from_key(item.field)) for item in signals)166 if any(field.channel != spec.channel for _,field in parsed):167 raise ValueError('文本CAN字段通道与来源声明不一致')168 # 所有资源槽先安装,任何后续预算/读取错误均可由同一个owner关闭。169 self.spec,self._budget,self.signals,self._fields = spec,budget,signals,parsed170 self._cycle_fields = cycle_fields171 if type(integrity) is not bool: raise TypeError("integrity需要bool")172 self._integrity=integrity173 self._trace=trace174 self._retain_source_time=retain_source_time175 self._check_cancel = check_cancel176 self._classify_unselected = classify_unselected177 self._binding_resolver = None178 self._database_context=database_context179 self._extra_fields=() if database_context is None else database_context.extra_fields180 self._frame_values=self._frame_layout=()181 self._known_field_keys = None182 self._held_cost = self._fixed_cost = self._dbc_cost = self._metadata_cost = 0183 self._db = self._raw = self._metadata = None184 self._input_index = 0185 self._completed_count = 0186 self.count = self.decode_calls = self.retained_count = 0187 self.full_check_count = 0188 self.first_ns = self.last_ns = None189 self._closed = self._exhausted = self._failed = self._closing = False190 try:191 # 固定费用包含所需字段、路径和DBC元数据;不保存随原帧数增长的列表。192 cost = 131072 + _metadata_bytes((signals,self._extra_fields,selected_ids,time_frame_ids))*16193 if cycle_fields or metadata_fields:194 cost += 16384 + _metadata_bytes((cycle_fields,metadata_fields))*32195 if database_context is not None:196 database_context.require_open()197 if database_context.path!=spec.dbc_path or database_context._budget is not budget:198 raise ValueError('共用DBC上下文必须匹配当前文件和共享预算')199 cost += sum(1024+len(str(item.path).encode('utf-8'))*16 for item in spec.inputs)200 _reserve_owned_ram(self,cost,'_fixed_cost')201 except BaseException as error:202 _cleanup_owner(self,error,'text_can_cleanup_owner','文本CAN构造')203 raise204205 @property206 def closed(self):207 """返回真实资源关闭事实。参数: 无。返回: bool。异常: 无。208 """209 return self._closed210211 @property212 def exhausted(self):213 """区别真实全文件EOF与提前close。参数: 无。返回: bool。异常: 无。214 """215 return self._exhausted216217 @property218 def metadata(self):219 """取得本次实际DBC事实。参数: 无。返回: 冻结Mapping。异常: 未启动或已关闭时ResultClosedError。220 """221 if self._metadata is None:222 raise ResultClosedError('文本CAN尚无可用的同次DBC元数据')223 return self._metadata224225 def prepare_metadata(self):226 """只初始化一次DBC,供计算在首帧前取得周期。227228 参数: 无。返回: 同次冻结元数据。异常: 失败/关闭拒绝,启动错误交实际owner清理。229 不打开原始文件,不改变EOF状态。230 """231 if self._closed or self._closing or self._failed:232 raise ResultClosedError('文本CAN读取已关闭或失败')233 try:234 if self._metadata is None:235 self._start()236 return self.metadata237 except BaseException as error:238 self._failed = True239 _cleanup_owner(self, error, 'text_can_cleanup_owner', '文本CAN初始化')240 raise241242 def _start(self):243 """一次加载并核所需定位/单位。参数: 无。返回: None。异常: 原DBC/I/O/预算或单位不符。244 """245 # 只有首次next进入此处;为库的DBC对象图和哈希缓冲预约保守费用,不声称RSS硬上限。246 from ..can_dependencies import import_can_dependency247 cantools = import_can_dependency('cantools')248 if self._database_context is None:249 size = self.spec.dbc_path.stat().st_size250 _reserve_owned_ram(self,1048576+size*64,'_dbc_cost')251 before_stat=self.spec.dbc_path.stat()252 before=None253 if self._integrity:254 with self.spec.dbc_path.open('rb') as stream:255 before = file_digest(stream,'sha256').hexdigest()256 self._db = load_dbc_file(str(self.spec.dbc_path), cantools)257 else:258 # 新文本读取和新输出共用同一已付费DBC;借用结束不能替父owner释放费用。259 self._database_context.require_open()260 self._db=self._database_context.database261 before=self._database_context.sha256262 fields = tuple(dict.fromkeys((*[field for _,field in self._fields],*self._extra_fields)))263 # 仅缓存已冻结声明的规范键;范围固定,避免每条消息重建相同JSON字段键。264 self._field_keys={field:field.key for field in fields}265 # 与字段字典共用已预付的声明空间;只保存引用,不复制每帧字段或扩大声明范围。266 self._known_field_keys = frozenset(self._field_keys.values())267 from ..legacy.binding import FrameBindingResolver268 self._binding_resolver = FrameBindingResolver(self._db, fields)269 if self._serial_blf_policy is not None:270 from collections import Counter271 self._duplicate_ids=frozenset(key for key,count in Counter(message.frame_id for message in self._db.messages).items() if count>1)272 if self._cycle_fields or self._metadata_fields:273 fields=tuple(dict.fromkeys((*fields,*self._cycle_fields,*self._metadata_fields)))274 facts=_field_metadata(self._db,fields,cycle_fields=self._cycle_fields)275 else:276 facts = _field_metadata(self._db,fields)277 # 已知非空DBC单位与input_unit逐字核对;缺单位不伪装成已认证单位。278 for definition,field in self._fields:279 unit = next(item['unit'] for item in facts if item['locator']==field_to_dict(field))280 if unit not in (None,'') and unit != definition.input_unit:281 raise ValueError('DBC非空单位与input_unit不同: '+definition.name)282 if self._database_context is None:283 after=None284 if self._integrity:285 with self.spec.dbc_path.open('rb') as stream:286 after = file_digest(stream,'sha256').hexdigest()287 after_stat=self.spec.dbc_path.stat()288 changed=(before_stat.st_size,before_stat.st_mtime_ns,before_stat.st_ino)!=(after_stat.st_size,after_stat.st_mtime_ns,after_stat.st_ino)289 if changed or before != after:290 raise ValueError('加载期间DBC内容发生变化')291 # EOF后仍保留单位/定位等事实;与DBC对象图分开付费,不能随DBC释放而漏账。292 _reserve_owned_ram(self,4096+_metadata_bytes(freeze_metadata(facts))*16293 +len(str(self.spec.dbc_path).encode('utf-8'))*16,'_metadata_cost')294 self._metadata = freeze_metadata({'profile':'can-text-v1','dbc_path':str(self.spec.dbc_path),295 'dbc_sha256':before,'fields':facts,'decode_choices':False,'allow_truncated':True,296 'selected_length_check':'legacy' if self._legacy_blf_decode else 'partial' if self.spec.decode_policy=='partial' else 'exact',297 'decode_scope':self._serial_blf_policy or 'requested','cantools_version':cantools.__version__,298 'record_check_scope':'selected_ids' if ((self._selected_ids is not None and self.spec.source_type=='raw_can_csv') or (self._serial_blf_policy is not None and self._emit_frame_ids is not None and self.spec.input_check=='auto')) else 'source',299 'time_order_check_scope':'none' if self._blf_time_scope=='none' else 'selected_ids' if self._time_frame_ids is not None else 'source'})300301 def _decode(self, raw):302 """给本条原帧分类并按需解码。参数: raw为本次实际RawCANRecord。返回: (values,status)。303304 异常: 所选政策的长度检查或非DBC解码错误。文本legacy遇DecodeError停止;305 strict/partial及明确的纯BLF legacy转接将其记为本条无值,缺失信号不沿用前值。306 """307 frame = raw.frame308 self._frame_values=self._frame_layout=()309 # 普通文本政策不从远程/错误帧生成信号;纯BLF legacy沿旧政策尝试解码。310 if (frame.is_remote_frame or frame.is_error_frame) and not self._legacy_blf_decode:311 return (),'non_data'312 selected = self._binding_resolver.fields_for(frame)313 decode_all = self._serial_blf_policy == 'all'314 if not selected and not decode_all:315 if not self._classify_unselected:316 return (),'unselected'317 return (), 'unselected' if any(message.frame_id == frame.arbitration_id for message in self._db.messages) else 'unknown'318 message = self._binding_resolver.message_for(frame, selected) if selected else None319 if message is not None:320 try:321 check_decode_length(frame,message.length,322 'strict' if self.spec.decode_policy == 'legacy' and not self._legacy_blf_decode else self.spec.decode_policy)323 except ValueError as error:324 raise CANTextError(raw.path,raw.line_number,str(error)) from error325 from cantools.database.errors import DecodeError326 self.decode_calls += 1327 if self._serial_blf_policy is not None:328 try:329 values=decode_serial_frame(self._db,frame,message,self._duplicate_ids)330 except (ValueError,KeyError) as error:331 raise CANTextError(raw.path,raw.line_number,'已定义DBC报文解码失败: '+str(error)) from error332 else:333 try:334 values = message.decode(frame.data, decode_choices=False, allow_truncated=True)335 except DecodeError as error:336 if self.spec.decode_policy == 'legacy' and not self._legacy_blf_decode:337 raise CANTextError(raw.path,raw.line_number,'已定义DBC报文解码失败: '+str(error)) from error338 values = {}339 except (ValueError,KeyError) as error:340 raise CANTextError(raw.path,raw.line_number,'已定义DBC报文解码失败: '+str(error)) from error341 if decode_all:342 # 显式完整诊断保留旧全部值与64KiB限制;正常requested不构造这份副本。343 from ..legacy.records import LegacyDecodedFrame344 LegacyDecodedFrame(self._completed_count+raw.sequence,frame,values)345 if message is not None and self._database_context is not None and any(rule.matches(frame) for rule in self._database_context.rules):346 from ..legacy.binding import field_matches347 names={field.signal for field in self._extra_fields if field_matches(field, frame)}348 self._frame_values=tuple((name,value) for name,value in values.items() if name in names)349 self._frame_layout=tuple((signal.name,int(signal.conversion.numeric_scaled_to_raw(values[signal.name])))350 for signal in message.signals if signal.is_multiplexer and signal.name in values)351 # 同一原字段可有多个分析别名;只交付一次物理值,额外转换由pack_decoded按别名执行。352 return tuple((self._field_keys[field],values.get(field.signal)) for field in dict.fromkeys(selected)),'decoded'353354 def __iter__(self):355 """迭代同次读取。参数: 无。返回: self。异常: 无。356 """357 return self358359 def _time_selected(self, frame):360 """判断此帧是否属于所需通道与ID;不解码、不改时间。361362 参数: frame为已解析原帧。返回: bool;None选择全部帧。异常: 无。363 纯BLF串行按ID沿用旧时间范围,其他来源同时匹配通道;同ID帧型交绑定解析器核对。364 """365 return self._time_frame_ids is None or ((self._serial_blf_policy is not None366 or frame.channel == self.spec.channel) and frame.arbitration_id in self._time_frame_ids)367368 def __next__(self):369 """逐条交付原帧和所需解码值。参数: 无。返回: DecodedCANRecord。异常: EOF/定位数据错/原I/O、预算、取消。370 """371 if self._closed or self._exhausted:372 raise StopIteration373 if self._closing or self._failed:374 raise ResultClosedError('文本CAN读取已失败或正在关闭')375 try:376 if self._check_cancel is not None:377 self._check_cancel()378 if self._database_context is not None:379 self._database_context.require_open()380 if self._db is None:381 self._start()382 while self._input_index < len(self.spec.inputs):383 if self._raw is None:384 # 实际parser owner在创建成功后安装;失败的未返回owner也必须接管清理。385 item = self.spec.inputs[self._input_index]386 factory = ASCRead if type(item) is ASCSpec else BLFRead if type(item) is BLFSpec else CANCSVRead387 try:388 self._raw = factory(item,self._budget,check_cancel=self._check_cancel,389 retain_original=self._trace,retain_source_time=self._retain_source_time or self._trace,390 **({'flag_resolver':self._binding_resolver.missing_flags,391 'selected_ids':self._selected_ids} if factory is CANCSVRead else392 {'time_selector':self._time_selected if self._time_frame_ids is not None else None,393 'previous_exact_time':self._last_selected_time} if factory is ASCRead else394 {'emit_frame_ids':self._emit_frame_ids,'channel':None if self._serial_blf_policy is not None else self.spec.channel,395 'binary_time':self._serial_blf_policy is not None,396 'previous_order_ns':int(self._last_selected_time.scaleb(9)) if self._serial_blf_policy is not None and self._last_selected_time is not None else None,397 'projection_only':self.spec.input_check=='auto',398 'time_scope':self._blf_time_scope,399 'time_check_all':self._time_frame_ids is None} if self._emit_frame_ids is not None else {'binary_time':True} if factory is BLFRead and self._serial_blf_policy is not None else {}))400 except BaseException as error:401 self._raw = (getattr(error,'asc_read_cleanup_owner',None)402 or getattr(error,'raw_csv_cleanup_owner',None) or getattr(error,'blf_read_cleanup_owner',None))403 raise404 try:405 raw = next(self._raw)406 except StopIteration:407 # 单文件真实EOF才换下一文件;不重排输入、不从头回退另一个parser。408 self.count = self._completed_count + self._raw.count409 self._completed_count = self.count410 # full及all是主动全检;这里只累计auto实际发生的非原生回退。411 if isinstance(self._raw, BLFRead) and self.spec.input_check == 'auto' and self._serial_blf_policy != 'all':412 self.full_check_count += self._raw.full_check_count413 self._blf_observation()414 if isinstance(self._raw, ASCRead):415 # full模式的最后元数据时刻也延续;按需模式只保所需帧,空文件不清零。416 self._last_selected_time = self._raw.last_exact_time417 first=self._raw.first_exact_time418 if first is not None and (self.first_order_time is None or first<self.first_order_time):419 self.first_order_time=first420 self._raw.close()421 self._raw = None422 self._input_index += 1423 continue424 # 来源范围与时序输入不是一回事:范围取同次读到的极值,不要求无关帧有序。425 if raw.time_ns is not None:426 self.first_ns = raw.time_ns if self.first_ns is None else min(self.first_ns,raw.time_ns)427 self.last_ns = raw.time_ns if self.last_ns is None else max(self.last_ns,raw.time_ns)428 self.count = self._completed_count + self._raw.count429 self._blf_observation()430 if not self._time_selected(raw.frame):431 continue432 # ASC只借当前比较点,避免跨文件舍入为同一ns后漏掉精确回退。433 # 其他格式沿用其原有整数ns精度;不在每条记录中保存双份时间。434 current_time = (self._raw.last_exact_time if isinstance(self._raw, ASCRead)435 else None if raw.time_ns is None else Decimal(f'{raw.time_ns}e-9'))436 if current_time is not None and self._last_selected_time is not None and current_time < self._last_selected_time:437 raise CANTextError(raw.path,raw.line_number,'连续文件时间回退,需显式对齐起点')438 self._last_selected_time = current_time439 if self.first_order_time is None:440 self.first_order_time = current_time441 if self._emit_frame_ids is not None and ((self._serial_blf_policy is None442 and raw.frame.channel != self.spec.channel) or raw.frame.arbitration_id not in self._emit_frame_ids):443 continue444 values,status = self._decode(raw)445 if self._post_decode_frame_ids is not None and raw.frame.arbitration_id not in self._post_decode_frame_ids:446 continue447 result = DecodedCANRecord._from_bound_values(448 self.spec.name, self._input_index, self._completed_count + raw.sequence,449 raw, values, status, self._frame_values, self._frame_layout,450 self._known_field_keys)451 # 来源范围已在筛选前统计;原序号和count包含跳过行,retained_count仅数交付。452 self.retained_count += 1453 return result454 # EOF后释放大DBC图,有限事实保留到显式close,供父层先接收并付费。455 self._db = self._binding_resolver = None456 _release_owned_ram(self,self._dbc_cost,'_dbc_cost')457 self._exhausted = True458 raise StopIteration459 except StopIteration:460 raise461 except BaseException as error:462 self._failed = True463 _cleanup_owner(self,error,'text_can_cleanup_owner','文本CAN读取')464 raise465466 def _blf_observation(self):467 """接收BLF投影前已核的范围和顺序事实。参数: 无。返回: None。异常: 无。468469 文件尾部只有无关帧时也更新来源结束;这些事实不能当成已消费水位。470 """471 if isinstance(self._raw,BLFRead) and self._raw.first_ns is not None:472 self.first_ns=self._raw.first_ns if self.first_ns is None else min(self.first_ns,self._raw.first_ns)473 self.last_ns=self._raw.last_ns if self.last_ns is None else max(self.last_ns,self._raw.last_ns)474 if self._time_frame_ids is None:475 if self.first_order_time is None and self._raw.first_order_ns is not None:476 self.first_order_time=Decimal(f'{self._raw.first_order_ns}e-9')477 if self._raw.last_order_ns is not None:478 current=Decimal(f'{self._raw.last_order_ns}e-9')479 # EOF或当前记录之后的全来源时间,不在当前记录检查之前前移比较点。480 if self._raw.closed:481 self._last_selected_time=current482 def close(self):483 """关闭自有解析器及DBC状态,可重试。参数: 无。返回: None。异常: 原关闭/预算错误。484 """485 if self._closed:486 return487 self._closing = True488 # 子文件真实关闭后才撤责任;持续失败仍由本owner保留引用。489 if self._raw is not None:490 self._raw.close()491 self._raw = None492 self._db = self._metadata = self._binding_resolver = None493 self._field_keys = self._known_field_keys = None494 self._time_frame_ids = None495 self._last_selected_time = None496 _release_owned_ram(self,self._dbc_cost,'_dbc_cost')497 _release_owned_ram(self,self._metadata_cost,'_metadata_cost')498 _release_owned_ram(self,self._fixed_cost,'_fixed_cost')499 self._closed = True500501 def __enter__(self):502 """建立显式资源上下文。参数: 无。返回: self。异常: 无。503 """504 return self505506 def __exit__(self, exception_type, error, traceback):507 """保原业务错误并收尾。参数: exception_type/error/traceback异常上下文。返回: False。异常: 正常退出时的关闭错误。508 """509 if error is None:510 self.close()511 else:512 _cleanup_owner(self,error,'text_can_cleanup_owner','文本CAN上下文')513 return False514515516def pack_text_records(definition, records, session_id, backend):517 """把有限已解码原帧交给现有数值核心,不再次解码或重复DBC缩放。518519 参数: definition为SignalDefinition;records为有限DecodedCANRecord tuple;session_id会话;backend为reference/numpy。520 返回: SignalSeries,保精确纳秒、同刻原序号和CANTextSourceRef;调用方负责批次数组的长期预算。521 异常: 来源/声明/记录类型不符,或既有pack_decoded类型、数值转换错误。522 """523 if type(definition) is not SignalDefinition or type(records) is not tuple or any(type(item) is not DecodedCANRecord for item in records):524 raise TypeError('文本CAN归一化需要定义及有限解码记录tuple')525 require_identifier(session_id,'text_can.session_id')526 samples,refs = [],[]527 for record in records:528 if record.source != definition.source:529 raise ValueError('文本CAN记录与信号来源不一致')530 # 未选中帧无该字段,不制造更新;匹配报文实际缺值则None更新,覆盖旧值。531 values = dict(record.values)532 if definition.field not in values:533 continue534 raw = record.raw535 samples.append((raw.time_ns,record.sequence,values[definition.field]))536 refs.append(CANTextSourceRef(session_id,record.source,definition.field,str(raw.path),537 raw.parser_version,raw.line_number,record.input_index,raw.sequence,record.sequence,538 raw.raw_time,raw.time_quantized))539 return pack_decoded(definition,tuple(samples),tuple(refs),backend)540