chronaxis.execution.text_can_reader
源码基线:8904c2f744951857659abd40aaa3535d57faf53c
1"""把ASC、BLF、原始CSV的同次解码接到公共信号批次和真实观测协议。23适配器声明字段和时基,不读文件;每个TextCANObservationRead拥有独立解析器、4有限原帧块和预算。每次交付同时段各别名批,总样本数不超过batch_rows;同刻别名未全部5交付前不越过其watermark。完整EOF并关闭解析器后才开放观测证明和可选原帧仓。6原帧仓仅显式导出时创建,共用已解码值,不另扫描原文件。7"""89from pathlib import Path10from sys import getsizeof11from uuid import uuid41213from ..core.validation import require_identifier14from ..sources.text_can_spec import CANTextSpec15from ..sources.text_can_decode import CANTextDecodeRead,pack_text_records16from ..sources.descriptions import describe_fields,source_timebase_identity17from ..sources.models import ReadRequest,SignalBatch,SourceDescription,SourceCapabilities,freeze_metadata18from ..sources.protocol import BatchValidator,SourceClosedError19from ..results.budget import _reserve_owned_ram,_release_owned_ram,_cleanup_owner20from ..results.errors import ResultClosedError21from .can_reader import CANObservation22from .frame_capture import TextFrameStore23from .history import _metadata_bytes,_pending_bytes242526def _record_metadata_bytes(record):27 """按已检查记录的固定结构计算与通用估算器相同的费用。2829 参数: record为当前解码器交付的冻结DecodedCANRecord,键和值均已检查。30 返回: values、重建值/布局、路径及原时间文本的基础字节费用,尚未乘32。31 异常: 传播内存分配错误;不接受任意外部对象树。32 每个引用位置仍计费,仅省去通用元数据的递归类型分流;不降低预算金额。33 """34 # 外层五元组逐项收32字节;每个键值对的两个引用同样各收32字节。35 total = getsizeof((None,) * 5) + 5 * 3236 for pairs in (record.values, record.frame_values, record.frame_layout):37 total += getsizeof(pairs)38 for pair in pairs:39 total += 32 + getsizeof(pair) + 2 * 3240 total += getsizeof(pair[0]) + 64 + getsizeof(pair[1]) + 6441 total += getsizeof(str(record.raw.path)) + 6442 total += getsizeof(record.raw.raw_time) + 6443 return total444546class BudgetedTextCANSourceAdapter:47 """拥有静态描述和活read集合,借用导出DBC上下文;关闭不会释放借用上下文。"""4849 def __init__(self,budget,spec,session_id,signals,*,backend='numpy',work_dir=None,retain_raw_frames=False,database_context=None,message_consumer=None,cycle_fields=(), metadata_fields=(),message_keys=(),integrity=True,trace=True,selected_ids=None,time_frame_ids=None,process_count=1,emit_frame_ids=None,blf_time_scope='source',legacy_blf_decode=False,serial_blf_policy=None,post_decode_frame_ids=None):50 """构造声明,不读取输入。参数: 预算/spec/会话/定义、后端、可选暂存目录及DBC上下文;message_keys为无值公式的完整触发定位;message_consumer同步接收必要消息,保留需自付预算;cycle_fields显式请求初始化DBC周期;metadata_fields仅确认驱动候选,两者不增加值解码。返回: adapter。异常: 配置/预算错。5152 time_frame_ids将所需通道/ID的单调性检查与全来源范围统计分开,不改变原帧时间。53 selected_ids是纯消息计算需要的有限ID集合;仅在不要求全来源时间或原帧导出时54 下推给原始CSV读取器。此时观测范围是保留记录的范围,不能冒称全部来源范围。55 legacy_blf_decode仅由纯can_blf转接设置;serial_blf_policy仅串行使用,保原异常及all/requested范围。56 post_decode_frame_ids为串行all完成解码后交付的ID集合,None全部交付;不下推并行。57 这些是内部转接参数,不增加用户配置项。58 """59 if type(spec) is not CANTextSpec or backend not in ('reference','numpy') or type(retain_raw_frames) is not bool:60 raise ValueError('文本CAN来源配置或后端非法')61 from ..sources.can_updates import require_message_keys62 require_message_keys(message_keys, channel=spec.channel)63 if message_keys and message_consumer is None:64 raise ValueError('消息触发定位必须有实际消费者')65 if message_consumer is not None and not callable(message_consumer):66 raise TypeError('message_consumer需要同步可调用对象')67 if retain_raw_frames and work_dir is None:68 raise ValueError('显式原帧保存需要新工作目录')69 from ..legacy.dbc_cycles import require_cycle_fields70 require_cycle_fields(cycle_fields,spec.channel)71 require_cycle_fields(metadata_fields,spec.channel)72 if retain_raw_frames and time_frame_ids is not None:73 raise ValueError('原帧导出需要完整时间顺序检查')74 self._process_count = process_count75 self._emit_frame_ids = emit_frame_ids76 self._blf_time_scope=blf_time_scope77 self._legacy_blf_decode=legacy_blf_decode78 self._serial_blf_policy=serial_blf_policy79 self._post_decode_frame_ids=post_decode_frame_ids80 self._metadata_fields = metadata_fields81 self._selected_ids = selected_ids82 self._time_frame_ids = time_frame_ids83 self._cycle_fields=cycle_fields84 self._integrity=integrity85 self._trace=trace86 require_identifier(session_id,'text_can.session_id')87 self.budget=self._budget=budget88 self.spec=spec89 self.session_id=session_id90 self.signals=signals91 self.backend=backend92 self.work_dir=None if work_dir is None else Path(work_dir)93 self._retain_raw_frames=retain_raw_frames94 self.database_context=database_context95 self._update_consumer=message_consumer96 self._update_projection=None97 self._held_cost=self._fixed_cost=098 self._closed=False99 self._requests=set()100 self._pending=None101 try:102 cost=131072+_metadata_bytes((signals,selected_ids,time_frame_ids))*32+sum(1024+len(str(i.path).encode('utf-8'))*32 for i in spec.inputs)103 if message_keys:104 cost += 8192 + 2048 * len(message_keys)105 if cycle_fields or metadata_fields:106 cost += 16384 + _metadata_bytes((cycle_fields,metadata_fields))*32107 _reserve_owned_ram(self,cost,'_fixed_cost')108 if message_consumer is not None:109 from ..sources.can_updates import CANUpdateProjection110 from ..sources.can_spec import can_field_from_key111 self._update_projection=CANUpdateProjection(spec.name,session_id,tuple(can_field_from_key(s.field) for s in signals),message_keys=message_keys,clock_fields=metadata_fields)112 locator={'paths':tuple(str(i.path) for i in spec.inputs),'dbc_path':str(spec.dbc_path),113 'channel':spec.channel,'profile':'can-text-v1','offset_ns':0,'time_unit':'ns',114 'source_timebase_sha256':source_timebase_identity(spec.timebase,0)}115 self._description=SourceDescription(spec.name,describe_fields(spec.name,signals,locator),116 spec.timebase.timebase_id,None,None,SourceCapabilities(True,True,True,False,117 ('ASC/CSV按文本转换时间并保原行;混合BLF沿用库浮点秒转纳秒并以记录序号定位;CSV按ID筛选时仅观测保留记录',),118 (('所需通道/ID按声明顺序时间非递减;其他帧仅参与来源范围统计;DBC缺单位仍未核实'119 if time_frame_ids is not None else '输入必须按声明顺序时间非递减;DBC缺单位仍未核实'),)),legacy_profile='can-text-v1')120 except BaseException as error:121 _cleanup_owner(self,error,'text_can_source_cleanup_owner','文本CAN来源构造')122 raise123124 @property125 def retain_raw_frames(self):126 """返回本来源冻结的原帧保存选择。参数: 无。返回: bool。异常: 无。127 """128 return self._retain_raw_frames129130 def _ensure_open(self):131 """拒绝使用已关闭适配器。参数: 无。返回: None。异常: SourceClosedError。132 """133 if self._closed:134 raise SourceClosedError('文本CAN来源已关闭')135136 def describe(self):137 """返回静态字段及时间声明。参数: 无。返回: SourceDescription。异常: 已关闭。138 """139 self._ensure_open()140 return self._description141142 def iter_batches_with_observation(self,request):143 """创建具有同次观测的独立read。参数: request。返回: read。异常: 合同/预算错及可恢复owner。144 """145 self._ensure_open()146 if self._pending is not None:147 raise ResultClosedError('文本CAN仍有未返回read待清理')148 try:149 self._pending=TextCANObservationRead(self,request)150 read=self._pending151 self._requests.add(read)152 self._pending=None153 return read154 except BaseException as error:155 self._pending=self._pending or getattr(error,'text_can_observation_cleanup_owner',None)156 _cleanup_owner(self._pending,error,'text_can_observation_cleanup_owner','文本CAN读取登记') if self._pending is not None else None157 if self._pending is not None and self._pending.closed:158 self._pending=None159 if self._pending is not None:160 error.text_can_source_cleanup_owner=self161 raise162163 def iter_batches(self,request):164 """沿同一批次合同创建read,不另读输入。参数: request。返回: read。异常: 原工厂错。165 """166 return self.iter_batches_with_observation(request)167168 def close(self):169 """关闭所有本来源活read,再释放描述费用。参数: 无。返回: None。异常: 关闭/费用错可重试。170 """171 if self._closed:172 return173 if self._pending is not None:174 self._pending.close()175 self._pending=None176 while self._requests:177 next(iter(self._requests)).close()178 self._description=None179 self._update_projection=self._update_consumer=None180 _release_owned_ram(self,self._fixed_cost,'_fixed_cost')181 self._closed=True182183184class TextCANObservationRead:185 """拥有一个有限原帧块和一个归一化批,完整EOF前不开放完成事实。"""186187 def __init__(self,owner,request):188 """验证原会话/投影并构造未开始的解码器。参数: adapter/request。返回: read。异常: 合同/预算错。189 """190 owner._ensure_open()191 if type(request) is not ReadRequest or request.session_id!=owner.session_id:192 raise ValueError('文本CAN读取需要原会话ReadRequest')193 definitions={s.name:s for s in owner.signals}194 if not set(request.signal_names)<=set(definitions):195 raise ValueError('文本CAN请求包含未声明信号')196 self._owner=owner197 self._request=request198 self._budget=owner.budget199 self.identity=uuid4().hex200 self._definitions=tuple(definitions[n] for n in request.signal_names)201 self._held_cost=self._fixed_cost=self._records_cost=self._batch_cost=self._pending_cost=self._validation_cost=self._runtime_cost=self._update_cost=0202 self._decoder=self._raw_store=self._current=self._validator=self._runtime_metadata=self._observation=None203 self._binding_projection=None204 self._records=[]205 self._column=0206 self._raw_eof=self._eof_sent=False207 self._closed=self._exhausted=self._failed=False208 try:209 _reserve_owned_ram(self,131072+_metadata_bytes((request,owner.signals))*32,'_fixed_cost')210 self._validator=BatchValidator(owner.describe(),request)211 try:212 factory = CANTextDecodeRead213 parallel_options = dict(emit_frame_ids=owner._emit_frame_ids,blf_time_scope=owner._blf_time_scope,serial_blf_policy=owner._serial_blf_policy,post_decode_frame_ids=owner._post_decode_frame_ids)214 if owner._process_count > 1 or owner.spec.file_merge == 'time_order':215 from .parallel_can_decode import ParallelCANDecodeRead216 factory = ParallelCANDecodeRead217 parallel_options = dict(process_count=owner._process_count,work_dir=owner.work_dir,218 emit_frame_ids=owner._emit_frame_ids,blf_time_scope=owner._blf_time_scope)219 self._decoder=factory(owner.spec,self._budget,220 owner.signals if owner._update_consumer is not None else self._definitions,221 **parallel_options,222 database_context=owner.database_context, integrity=owner._integrity, trace=owner._trace,retain_source_time=owner.retain_raw_frames,223 legacy_blf_decode=owner._legacy_blf_decode,224 classify_unselected=owner._update_consumer is None,225 **({'cycle_fields':owner._cycle_fields} if owner._cycle_fields else {}),226 **({'metadata_fields':owner._metadata_fields} if owner._metadata_fields else {}),227 **({'selected_ids':owner._selected_ids} if owner._selected_ids is not None else {}),228 **({'time_frame_ids':owner._time_frame_ids} if owner._time_frame_ids is not None else {}))229 except BaseException as error:230 self._decoder=getattr(error,'text_can_cleanup_owner',None)231 raise232 if owner.retain_raw_frames:233 try:234 self._raw_store=TextFrameStore(owner.work_dir/('frames-'+self.identity),self._budget,235 owner.spec.name,owner.session_id,self.identity,database_context=owner.database_context,channel=owner.spec.channel,integrity=owner._integrity,trace=owner._trace)236 except BaseException as error:237 self._raw_store=getattr(error,'text_frame_store_cleanup_owner',None)238 raise239 except BaseException as error:240 _cleanup_owner(self,error,'text_can_observation_cleanup_owner','文本CAN观测构造')241 raise242243 @property244 def closed(self):245 """返回真实关闭状态。参数: 无。返回: bool。异常: 无。246 """247 return self._closed248249 @property250 def failed(self):251 """返回当前读取是否失败。参数: 无。返回: bool。异常: 无。252 """253 return self._failed254255 @property256 def exhausted(self):257 """返回完整EOF及解析器关闭事实。参数: 无。返回: bool。异常: 无。258 """259 return self._exhausted260261 @property262 def observation(self):263 """仅交付本read已完成的真实观测。参数: 无。返回: CANObservation。异常: 未完成/失败。264 """265 if not self._exhausted or self._failed or self._observation is None:266 raise ResultClosedError('文本CAN观测尚未完成')267 return self._observation268269 @property270 def observed_bounds(self):271 """返回同次解析已经看到的首末时间,不额外扫描或冒充EOF。272273 参数: 无。返回: (first,last),空输入为None。异常: 失败/关闭状态拒绝借读。274 """275 if self._closed or self._failed:276 raise ResultClosedError('文本CAN观测已关闭或失败')277 if self._observation is not None:278 return self._observation.first_ns,self._observation.last_ns279 return self._decoder.first_ns,self._decoder.last_ns280281 @property282 def runtime_metadata(self):283 """交付同次DBC身份与解码次数。参数: 无。返回: 冻结元数据。异常: 未完成。284 """285 self.observation286 return self._runtime_metadata287288 def prepare_metadata(self):289 """在读取前取得同次DBC周期,不冒充最终解码计数。290291 参数: 无。返回: 冻结DBC事实。异常: 关闭/失败或启动原错;清理由本read负责。292 """293 if self._closed or self._failed or self._decoder is None:294 raise ResultClosedError('文本CAN读取已关闭或失败')295 try:296 facts = self._decoder.prepare_metadata()297 if self._binding_projection is None and self._owner._update_projection is not None:298 self._binding_projection = self._owner._update_projection.for_read(facts['fields'])299 return facts300 except BaseException as error:301 self._failed = True302 _cleanup_owner(self, error, 'text_can_observation_cleanup_owner', '文本CAN初始化')303 raise304305 def __iter__(self):306 """返回独立游标。参数: 无。返回: self。异常: 无。307 """308 return self309310 def _accept(self,batch):311 """按既有批合同核同刻组,保留所需pending费用。参数: batch。返回: 原批。异常: 校验/预算错。312 """313 _reserve_owned_ram(self,4096+2*_pending_bytes(self._validator)+1024*batch.sample_count,'_validation_cost')314 self._validator.accept(batch)315 retained=_pending_bytes(self._validator)316 _release_owned_ram(self,self._pending_cost,'_pending_cost')317 self._validation_cost-=retained318 self._pending_cost+=retained319 _release_owned_ram(self,self._validation_cost,'_validation_cost')320 self._current=batch321 return batch322323 def _fill(self):324 """按全部别名的样本容量取有限原记录,所有值只解码一次。参数: 无。返回: None。异常: 原读/预算/暂存错。325 """326 self._records.clear()327 self._column=0328 if self._binding_projection is None:329 self.prepare_metadata()330 _release_owned_ram(self,self._records_cost,'_records_cost')331 raw_rows=min(256,max(1,self._request.batch_rows//max(1,len(self._definitions))))332 for _ in range(raw_rows):333 try:334 record=next(self._decoder)335 except StopIteration:336 self._raw_eof=True337 break338 cost=16384+_record_metadata_bytes(record)*32339 _reserve_owned_ram(self,cost,'_records_cost')340 self._records.append(record)341 if self._raw_store is not None:342 self._raw_store.append(record)343 if self._owner._update_consumer is not None:344 from .message_delivery import deliver_message345 deliver_message(self,self._binding_projection,self._owner._update_consumer,346 record.raw.frame,record.sequence,dict(record.values),record.raw.time_ns,canonical_keys=True)347348 def __next__(self):349 """交付全局时间非递减的有界批,最终EOF后保完成事实。参数: 无。返回: SignalBatch。异常: EOF/原读/预算/合同错。350 """351 if self._closed or self._exhausted:352 raise StopIteration353 if self._failed:354 raise ResultClosedError('失败的文本CAN读取只能清理')355 try:356 self._owner._ensure_open()357 self._current=None358 _release_owned_ram(self,self._batch_cost,'_batch_cost')359 if self._eof_sent:360 self._finish()361 raise StopIteration362 if not self._records or self._column>=max(1,len(self._definitions)):363 self._fill()364 if not self._records and self._raw_eof:365 self._eof_sent=True366 return self._accept(SignalBatch(self._owner.spec.name,self._request.session_id,{},367 self._decoder.last_ns,True,self._decoder.last_ns))368 column_count=min(self._request.batch_rows,max(1,len(self._definitions)-self._column))369 _reserve_owned_ram(self,65536+(2*self._records_cost+16384)*column_count,'_batch_cost')370 columns={}371 if self._definitions:372 for definition in self._definitions[self._column:self._column+column_count]:373 columns[definition.name]=pack_text_records(definition,tuple(self._records),self._request.session_id,self._owner.backend)374 self._column+=column_count375 # watermark含端点;尚待交付的别名可能从块首开始,因此只能推进到首点前1ns。376 progress=self._records[-1].raw.time_ns if self._column>=max(1,len(self._definitions)) else self._records[0].raw.time_ns377 watermark=None if progress is None or progress==-(1<<63) else progress-1378 return self._accept(SignalBatch(self._owner.spec.name,self._request.session_id,columns,watermark,False,None))379 except StopIteration:380 raise381 except BaseException as error:382 self._failed=True383 _cleanup_owner(self,error,'text_can_observation_cleanup_owner','文本CAN观测读取')384 raise385386 def _finish(self):387 """封口可选原帧仓,解析器真正关闭后提交完整事实。参数: 无。返回: None。异常: 封口/关闭/预算错。388 """389 if not self._decoder.exhausted:390 raise ValueError('文本原始解析器没有真正到达EOF')391 raw=self._decoder392 fact=CANObservation(self._owner.spec.name,self._request.session_id,self._request,self.identity,393 raw.first_ns,raw.last_ns,raw.count,True,394 self._owner._blf_time_scope if self._owner._blf_time_scope!='source' else395 'selected' if self._owner._selected_ids is not None and self._owner.spec.source_type=='raw_can_csv' else 'source',396 raw.retained_count,getattr(raw,'full_check_count',0))397 metadata=dict(raw.metadata,decode_calls=raw.decode_calls,record_count=raw.count,source_kind='can_blf' if self._owner._serial_blf_policy is not None else self._owner.spec.source_type)398 _reserve_owned_ram(self,65536+_metadata_bytes(metadata)*32,'_runtime_cost')399 self._runtime_metadata=freeze_metadata(metadata)400 if self._raw_store is not None:401 self._raw_store.seal()402 self._decoder.close()403 self._decoder=None404 self._observation=fact405 self._exhausted=True406407 def take_raw_store(self,receiver,slot):408 """移交本read的同次封口原帧仓。参数: receiver空槽及slot名。返回: None。异常: 未完成/已有责任。409 """410 self.observation411 if self._raw_store is None or self._raw_store.state!='sealed' or getattr(receiver,slot,None) is not None:412 raise ValueError('文本原帧仓不能移交')413 setattr(receiver,slot,self._raw_store)414 self._raw_store=None415416 def close(self):417 """关闭实际解析器/原帧仓并释放自身费用,不释放共享DBC。参数: 无。返回: None。异常: 原清理错可恢复。418 """419 if self._closed:420 return421 self._current=None422 self._records.clear()423 self._validator=None424 if self._decoder is not None:425 self._decoder.close()426 self._decoder=None427 if self._raw_store is not None:428 self._raw_store.close()429 self._raw_store=None430 self._runtime_metadata=self._observation=self._binding_projection=None431 for slot in ('_update_cost','_batch_cost','_records_cost','_pending_cost','_validation_cost','_runtime_cost','_fixed_cost'):432 _release_owned_ram(self,getattr(self,slot),slot)433 self._owner._requests.discard(self)434 self._closed=True435