Chronaxis用户手册English
本页目录

chronaxis.execution.can_reader

源码基线:8904c2f744951857659abd40aaa3535d57faf53c

1"""把同次旧CAN逐帧输出接成预算信号批,保留原始帧与真实观测证明。23adapter构造/describe只检查声明并创建新工作目录,不读BLF/DBC、不启动进程。4独立CANObservationRead首次next才启动CAN读取进程,先复制同次READY并核实际非空DBC单位,5再逐原yield读取;显式all每帧调用一次原parse,仅retain_raw_frames时将全部原帧写入6RawFrameStore。所有原帧计读取数,时间按source/selected/none分别采集,7本层收到的signals是运行实际需要的定义;requested只解码这些字段和消息指标所需ID,8只传输所需值;auto跳过无用途字段与时间,full/source保原完整检查。显式all保留原全解码检查。9当前原帧的匹配别名按全部列样本总和batch_rows拆批;其他报文10不更新此信号,匹配定位但原dict缺key则None→MISSING,不延用旧值或猜DECODE_ERROR。1112时间仅原浮点秒convert_time(...,'s')加一次offset。当前ns-1才作为非EOF watermark,13所以同刻后继不会被提前承诺;末次真实EOF交付独立空批,下一次StopIteration前须14确认CAN读取进程退出、raw writer封口、validator结束和本次输入关闭成功,才开放CANObservation。15runtime_metadata保原READY冻结键形状,None/空unit原样表示未核实,不能假verified。16take_raw_store只在完成后一次移交原store;未移交则本read.close负责,不关已移交store。17显式capture_message_updates=True时,同一record在raw追加后另存完整decoded仓,18不在别名拆批处重复捕获。消息仓保同次READY.profile/read_identity和原量化事实,19真EOF才封口;take_message_store同样先安装接收父,再撤本子责任。默认关闭时20不创建消息目录或额外预约,原PULL/parse、字段投影与批次协议保持。2122来源/请求/同次元数据、当前原记录、装箱与validator动态去重状态分别先付费。23未返回read及CAN读取进程/raw构造责任均真实安装后再推进,持续清理失败保最高恢复owner;24close可重试且不抵销借款。完成read脱离adapter活登记,事实可活到自身close。25全部只用新分析层和已有pack_decoded两后端,不修改旧链、重复读DBC或声明RSS上限。26batch_read默认自动选择:requested由can_batch_reader接有限批,all保原逐条读取;27显式bool仍覆盖自动选择。未保存raw由公开元数据None/None/0明确表达。28直接累计且没有普通样本请求时,另核两段真实输入字节的SHA256:全部原时间、29所需记录的长度与JSON;selected/none的全时间SHA为None。只借用已严格解包的原包,不重编码或保存副本;真实EOF及30进程关闭后才开放指纹。它不是普通信号样本摘要,read关闭后由接收方自付保留费用。31"""3233# 数值归一化复用既有pack;外层只处理实际来源身份、原行进度和资源责任。34from dataclasses import dataclass35import hashlib36from pathlib import Path37from uuid import uuid438from ..core.binding import SignalDefinition39from ..core.validation import require_identifier, require_text40from ..legacy.process_read import LegacyCANProcessRead41from ..legacy.raw_store import RawFrameStore42from ..legacy.message_store import DecodedMessageStore, MAX_DECODED_MESSAGE_ROW_BYTES43from ..legacy.records import LegacyCANRequest, field_to_dict44from ..legacy.batch_wire import HEADER45from ..legacy.batch_worker import REQUESTED_VALUES_POLICY46from ..results.budget import ResourceBudget47from ..results.errors import ResultClosedError48from ..results.spool import _cleanup_owner, _reserve_owned_ram, _release_owned_ram49from ..sources.can_spec import LegacyCANSpec, CANSourceRef, can_field_from_key50from ..sources.descriptions import describe_fields, source_timebase_identity51from ..sources.models import (ReadRequest, SignalBatch, SourceDescription, SourceCapabilities, freeze_metadata)52from ..sources.packing import pack_decoded53from ..sources.protocol import BatchValidator, SourceClosedError, SourceReadError54from ..timebase.conversion import checked_add_ns, convert_time, require_int64_ns55from .history import _metadata_bytes, _pending_bytes565758@dataclass(frozen=True)59class CANObservation:60    """保存本read实际全部yield的完成事实,不接受请求范围作为原行首末。"""6162    source: str  # 当前来源名。63    session_id: str  # 原会话。64    request: ReadRequest  # 创建本read的原对象,不按值重建。65    read_identity: str  # 每个read独立UUID。66    first_ns: int | None  # 全部原yield经一次映射的首端,全空为None。67    last_ns: int | None  # 全部原yield经一次映射的末端,全空为None。68    row_count: int  # 包含未知ID/空投影的原yield数,不是样本数。69    source_eof: bool  # 仅真实完整完成为True。70    time_scope: str = 'source'  # source为全来源;selected仅目标;none未采集。71    retained_record_count: int = 0  # 本次实际保留记录数,不等于贡献数。72    full_check_count: int = 0  # 按需策略下非原生输入的完整检查回退数。7374    def __post_init__(self):75        """核完成事实结构。参数: 无。返回: None。异常: 类型/会话/端点或EOF矛盾。76        """77        require_identifier(self.source, 'can_observation.source')78        require_identifier(self.session_id, 'can_observation.session_id')79        require_text(self.read_identity, 'can_observation.read_identity')80        if (not isinstance(self.request, ReadRequest) or self.request.session_id != self.session_id81                or type(self.row_count) is not int or self.row_count < 0 or self.source_eof is not True):82            raise ValueError('CAN观测需原请求、非负原行数及完整EOF')83        if self.time_scope not in ('none', 'selected', 'source'):84            raise ValueError('CAN观测时间范围非法')85        if (type(self.retained_record_count) is not int or not 0 <= self.retained_record_count <= self.row_count86                or type(self.full_check_count) is not int or not 0 <= self.full_check_count <= self.row_count):87            raise ValueError('CAN观测保留/完整回退计数非法')88        if self.time_scope == 'none':89            if self.first_ns is not None or self.last_ns is not None:90                raise ValueError('未采集时间不能声明首末')91            return92        if self.row_count == 0 or self.time_scope == 'selected' and not self.retained_record_count:93            if self.first_ns is not None or self.last_ns is not None:94                raise ValueError('空CAN输入没有可猜测首末')95        else:96            require_int64_ns(self.first_ns)97            require_int64_ns(self.last_ns)98            if self.first_ns > self.last_ns:99                raise ValueError('CAN观测时间不能回退')100101102class BudgetedLegacyCANSourceAdapter:103    """拥有静态来源描述和活read登记,完成read与已移交store由新父负责。"""104105    def __init__(self, budget, spec, session_id, signals, *, backend='numpy', work_dir,106                 capture_message_updates=False, batch_read=None, retain_raw_frames=True,107                 input_check='full', time_scope='source',108                 message_frame_ids=None,extra_fields=(), message_consumer=None, cycle_fields=(), metadata_fields=(), message_keys=(),109                 input_fingerprints=True, history_integrity=True, frame_observer=None):110        """先付声明费用,建立显式新工作根但不访问原输入。111112        参数: budget为共享ResourceBudget;spec为LegacyCANSpec;session_id为会话;113            signals为SignalDefinition tuple;backend为numpy/reference;work_dir必须全新;114            capture_message_updates为严格bool,默认False,仅显式开启才存同次decoded仓;115            batch_read默认None自动选择:requested为True,all为False;显式bool保持选择,116            requested不能显式False;retain_raw_frames决定是否保存原帧;117            message_frame_ids为需要完整逐消息记录的ID,None保留所有实际返回记录。118            extra_fields只为显式报文重建传递连续量/复用选择器,不生成普通信号别名。119            message_keys为没有数值依赖也须触发的完整报文定位;120            message_consumer可选同步接收共同消息模型;只传signals所需值,保留对象须由消费者另付费用。121            cycle_fields独立请求初始化周期;metadata_fields仅请求字段候选,两者不添加解码值或普通样本。122            frame_observer仅显式导出时接收原帧身份,不保存额外载荷或扫描文件。123            input_fingerprints仅控制可选输入摘要,默认保持旧调用;必要值检查始终执行。124            input_check/time_scope为计划固定的检查和时间范围;低层默认full/source保旧入口。125            spec.decode_scope=requested使用批协议,将实际需要的字段及指标ID固定为解码集合;126            其他帧仍读取并检查原字段/时间,但不再生成或检查其解码值。127        返回: 静态adapter。异常: 原配置/目录/预算/取消;持续清理失败附can_source_cleanup_owner。128        """129        if batch_read is None and type(spec) is LegacyCANSpec:130            batch_read = (spec.decode_scope == 'requested' or len(spec.input_paths) > 1131                          or spec.decode_policy != 'legacy' or bool(cycle_fields) or bool(metadata_fields))132        if (not isinstance(budget, ResourceBudget) or type(spec) is not LegacyCANSpec133                or type(signals) is not tuple or any(type(item) is not SignalDefinition for item in signals)134                or backend not in ('numpy', 'reference') or not isinstance(work_dir, (str, Path))135                or any(type(value) is not bool for value in (capture_message_updates, batch_read, retain_raw_frames, input_fingerprints, history_integrity))136                or message_frame_ids is not None and (type(message_frame_ids) is not tuple137                    or any(type(value) is not int or not 0 <= value <= 0x1fffffff for value in message_frame_ids))):138            raise TypeError('CAN来源需预算、静态spec、定义tuple、后端及新工作目录')139        from ..sources.can_updates import require_message_keys140        require_message_keys(message_keys, channel=spec.channel)141        if message_keys and message_consumer is None:142            raise ValueError('消息触发定位必须有实际消费者')143        if message_keys:144            message_frame_ids = tuple(sorted(set(message_frame_ids or ()) | {key[1] for key in message_keys}))145        if message_consumer is not None and not callable(message_consumer):146            raise TypeError('message_consumer需要同步可调用对象')147        if not batch_read and (len(spec.input_paths) > 1 or spec.decode_policy != 'legacy' or cycle_fields or metadata_fields):148            raise ValueError('连续文件和新解码政策需要批次读取')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._cycle_fields = cycle_fields154        self._input_fingerprints_requested = input_fingerprints155        self._history_integrity=history_integrity156        require_identifier(session_id, 'can.session_id')157        from ..legacy.records import LegacyCANField158        if type(extra_fields) is not tuple or len(extra_fields)>4096 or any(type(f) is not LegacyCANField or f.channel!=spec.channel for f in extra_fields):159            raise ValueError('重建字段需要同通道有限LegacyCANField tuple')160        if any(item.source != spec.name for item in signals) or len({item.name for item in signals}) != len(signals):161            raise ValueError('CAN信号来源须匹配且别名唯一')162        self.budget = self._budget = budget163        self.spec, self.session_id, self.signals, self.backend = spec, session_id, signals, backend164        self._capture_message_updates = capture_message_updates165        if input_check not in ('auto', 'full') or time_scope not in ('none', 'selected', 'source'):166            raise ValueError('CAN检查范围或时间范围非法')167        if (input_check == 'full' or spec.decode_scope == 'all' or retain_raw_frames or capture_message_updates) and time_scope != 'source':168            raise ValueError('完整诊断/原帧/消息历史必须保全来源时间')169        self.input_check, self.time_scope = input_check, time_scope170        self._batch_read, self._retain_raw_frames = batch_read, retain_raw_frames171        self._message_frame_ids = message_frame_ids172        # 策略与固定集合只在创建时决定;all不构造集合或增加声明费用。173        self._decode_scope, self._decode_frame_ids = spec.decode_scope, ()174        # 当前消息消费者只接收共同update,不借用read内部的临时原帧槽。175        self._update_consumer = message_consumer176        if frame_observer is not None and not callable(frame_observer):177            raise TypeError('frame_observer需要可调用对象')178        self._frame_observer = frame_observer179        self._update_projection = None180        self.work_dir = self._description = self._definitions = self._fields = None181        self._requests = self._uninstalled_request = None182        self._fixed_cost = self._held_cost = 0183        self.closed = self._closing = False184        try:185            # 仅计静态有限树;解析field JSON/复制描述/创建登记前已覆盖所需空间。186            cost = 65536 + 32 * _metadata_bytes((spec.name, str(spec.path) if len(spec.input_paths)==1 else tuple(str(p) for p in spec.input_paths), str(spec.dbc_path),187                spec.channel, spec.timebase, spec.unit, spec.offset_ns, session_id, signals, str(work_dir)))188            if cycle_fields or metadata_fields:189                cost += 16384 + _metadata_bytes((cycle_fields,metadata_fields))*32190            _reserve_owned_ram(self, cost, '_fixed_cost')191            if extra_fields:192                _reserve_owned_ram(self,65536+64*_metadata_bytes(extra_fields),'_fixed_cost')193            if self._decode_scope == 'requested':194                if not batch_read:195                    raise ValueError('requested来源需要批次读取,batch_read不能为False')196                # 每输入ID覆盖tuple/set/排序及十进制诊断文本;每别名另覆盖scope定位副本。197                # 先付保守上界再解析和创建集合,不按去重后的较小数额追补。198                _reserve_owned_ram(self, 8192 + 256 * (len(signals) + len(message_frame_ids or ()))199                    + 512 * len(signals), '_fixed_cost')200            if message_keys:201                _reserve_owned_ram(self, 8192 + 2048 * len(message_keys), '_fixed_cost')202            parsed = tuple((item, can_field_from_key(item.field)) for item in signals)203            if any(field.channel != spec.channel for _, field in parsed):204                raise ValueError('CAN字段通道必须等于单声明通道')205            self._definitions = parsed206            if message_consumer is not None:207                from ..sources.can_updates import CANUpdateProjection208                self._update_projection = CANUpdateProjection(spec.name, session_id, tuple(field for _, field in parsed), message_keys=message_keys,clock_fields=metadata_fields)209            # 同一定位多别名只在声明元数据去重;默认全解码保持,requested固定集合210            # 由下一分支建立。这里不建立DBC缓存,也不共享目标帧的解码调用。211            self._fields = tuple(dict.fromkeys((*[field for _,field in parsed],*extra_fields)))212            if self._decode_scope == 'requested':213                self._decode_frame_ids = tuple(sorted({field.frame_id for field in self._fields}214                    | set(message_frame_ids or ())))215                if len(self._decode_frame_ids) > 4096:216                    raise ValueError('requested最多声明4096个不同报文ID')217            self._requests = set()218            path = Path(work_dir).resolve()219            path.mkdir(parents=True, exist_ok=False)220            self.work_dir = path221            locator = {'path': str(spec.path), 'dbc_path': str(spec.dbc_path), 'channel': spec.channel,222                'legacy_profile': spec.legacy_profile, 'time_unit': spec.unit, 'offset_ns': spec.offset_ns,223                'source_timebase_sha256': source_timebase_identity(spec.timebase, spec.offset_ns)}224            if len(spec.input_paths) > 1:225                locator.pop('path')226                locator['paths'] = tuple(str(p) for p in spec.input_paths)227            if spec.decode_policy != 'legacy':228                locator['decode_policy'] = spec.decode_policy229            diagnostics = (('原yield序号/完整原帧/原时间/同次DBC元数据' if retain_raw_frames else230                '全部原yield观测/所需原序号与原时间/同次DBC元数据;未保存原帧副本'),)231            if input_check == 'auto' and time_scope != 'source':232                diagnostics = (('原yield计数/所需原序号与值;未采集时间' if time_scope == 'none' else233                    '原yield计数/所需原序号、值与时间;未采集非目标帧时间') + '/同次DBC元数据;未保存原帧副本',)234            if self._decode_scope == 'requested':235                locator['decode_scope'] = 'requested'236                # 即使没有signal字段,来源级描述也必须绑定完整固定集合而非PULL子投影。237                diagnostics += ('decode_scope=requested;decode_frame_ids='238                    + ','.join(str(value) for value in self._decode_frame_ids),)239            if input_check == 'auto':240                locator.update(input_check=input_check, time_scope=time_scope, check_policy_version='demand-check-v1')241                diagnostics += ('按需检查:time_scope=' + time_scope + ';原生非目标帧仅保留本次要求的ID/时间处理;非原生输入完整检查回退',)242            self._description = SourceDescription(spec.name, describe_fields(spec.name, signals, locator),243                spec.timebase.timebase_id, None, None, SourceCapabilities(True, True, True, False,244                    diagnostics,245                    (('同格式原序BLF/单声明通道;原空dict不能区分解码失败原因' if len(spec.input_paths)>1246                      else '仅单BLF/单声明通道;原空dict不能区分解码失败原因'),247                     '缺DBC单位保持未核实;原始片段仅BLF且由run阶段写出;旧进程RSS未测')),248                legacy_profile=spec.legacy_profile, metadata_status='declared')249        except BaseException as exc:250            _cleanup_owner(self, exc, 'can_source_cleanup_owner', 'CAN来源构造')251            raise252253    @property254    def capture_message_updates(self):255        """读取原配置的消息捕获开关,不启动来源或开放完成证明。256257        参数: 无。返回: 原生bool,关闭adapter后仍可核原开关。258        异常: 无;只读属性不允许调用方改写活read的捕获来源。259        """260        return self._capture_message_updates261262    @property263    def batch_read(self):264        """读取本adapter冻结的批协议选择。参数: 无。返回: bool。异常: 无。"""265        return self._batch_read266267    @property268    def retain_raw_frames(self):269        """读取原始帧保存选择。参数: 无。返回: bool。异常: 无,不随关闭改变。"""270        return self._retain_raw_frames271272    @property273    def message_frame_ids(self):274        """读取实际消息保留ID。参数: 无。返回: tuple或None。异常: 无。"""275        return self._message_frame_ids276277    @property278    def decode_scope(self):279        """读取创建时的解码方式。参数: 无。返回: all/requested。异常: 无。"""280        return self._decode_scope281282    @property283    def decode_frame_ids(self):284        """借用已付费的固定解码集合。参数: 无。返回: tuple,close后为空。异常: 无。"""285        return self._decode_frame_ids286287    def _ensure_open(self):288        """拒绝关闭中/已关闭。参数: 无。返回: None。异常: SourceClosedError。289        """290        if self.closed or self._closing:291            raise SourceClosedError('CAN来源已关闭或正在清理')292293    def describe(self):294        """返回已付费静态描述,不读BLF/DBC或spawn。参数: 无。返回: SourceDescription。295        异常: SourceClosedError为关闭中/已关闭。296        """297        self._ensure_open()298        return self._description299300    def iter_batches(self, request):301        """创建独立观测read,首次next才实际输入。参数: request为原ReadRequest。302        返回: CANObservationRead。异常: 原配置/预约/登记错误,持续失败保最高真实owner。303        """304        self._ensure_open()305        if self._uninstalled_request is not None:306            raise SourceClosedError('CAN来源有未返回read待清理')307        try:308            if self.batch_read:309                from .can_batch_reader import CANBatchObservationRead310                read = CANBatchObservationRead(self, request)311            else:312                read = CANObservationRead(self, request)313            self._requests.add(read)314            self._uninstalled_request = None315            return read316        except BaseException as exc:317            read = self._uninstalled_request318            if read is not None:319                read._failed = True320                _cleanup_owner(read, exc, 'can_observation_read_cleanup_owner', 'CAN请求登记')321                if not read.closed:322                    exc.can_source_cleanup_owner = self323            raise324325    def iter_batches_with_observation(self, request):326        """使用同一独立read合同。参数: request为原请求。返回: CANObservationRead。327        异常: 与iter_batches相同;不复用adapter全局最后事实或request值map。328        """329        return self.iter_batches(request)330331    def close(self):332        """关闭本来源活read及费用,不关闭已完成脱登记read或其新父raw store。333334        参数: 无。返回: None,可重试。异常: 首个实际关闭/抵销错误,最高can_source_cleanup_owner。335        """336        if self.closed:337            return338        self._closing = True339        failure = None340        for read in (() if self._uninstalled_request is None else (self._uninstalled_request,)) + tuple(self._requests or ()):341            try:342                read.close()343            except BaseException as exc:344                if failure is None:345                    failure = exc346                else:347                    failure.add_note('CAN其他read关闭附加失败: ' + type(exc).__name__)348        if self._uninstalled_request is None and not self._requests:349            self._description = self._definitions = self._fields = None350            self._decode_frame_ids = ()351            self._update_projection = self._update_consumer = None352            self._frame_observer = None353            self.spec, self.signals = None, ()354            try:355                _release_owned_ram(self, self._fixed_cost, '_fixed_cost')356            except BaseException as exc:357                if failure is None:358                    failure = exc359            if self._held_cost == 0:360                self.closed = True361        if failure is not None:362            failure.can_source_cleanup_owner = self363            raise failure364365366class CANObservationRead:367    """拥有同次CAN读取进程进程、原帧/可选消息仓及当前帧;事实活到自身close,移交仓归新父。"""368369    def __init__(self, owner, request):370        """先安装未返回槽并预付固定树,不实际打开来源。371372        参数: owner为预算CAN来源,request为同会话原ReadRequest。返回: 独立read。373        异常: 原请求/预算/取消;构造未返回资源由本read最高恢复owner负责。374        """375        if not isinstance(owner, BudgetedLegacyCANSourceAdapter) or not isinstance(request, ReadRequest):376            raise TypeError('CAN read需要实际来源及ReadRequest')377        owner._ensure_open()378        if request.session_id != owner.session_id or any(name not in {item.name for item in owner.signals} for name in request.signal_names):379            raise ValueError('CAN请求会话或投影不匹配')380        self._owner, self._budget, self._request, self._spec = owner, owner.budget, request, owner.spec381        self._process = self._raw_store = self._message_store = self._record = self._current = self._validator = None382        self._capture_message_updates = owner.capture_message_updates383        self._message_store_taken = False384        self._selected = self._runtime_metadata = self._observation = self._identity = self._work_path = None385        self._binding_projection = None386        self._sample_pack_costs = ()387        self._fixed_cost = self._metadata_cost = self._record_cost = self._pack_cost = 0388        self._pending_cost = self._validation_cost = self._held_cost = self._update_cost = 0389        self._first_ns = self._last_ns = self._conversion = self._mapped = None390        self._row_count = self._alias_index = 0391        self._retained_record_count = self._full_check_count = 0392        self._started = self._eof_delivered = self._finished = self._taken = False393        self._closed = self._closing = self._failed = False394        # 只有requested直接累计且无普通样本时需要输入字节证明;all保原费用和扫描证据。395        self._input_fingerprints_enabled = (owner._input_fingerprints_requested and owner.decode_scope == 'requested'396            and owner._update_consumer is not None and not request.signal_names)397        self._input_timestamp_sha = self._input_records_sha = self._input_fingerprints = None398        self._input_frame_count = self._input_retained_count = 0399        owner._uninstalled_request = self400        try:401            cost = 65536 + 16 * _metadata_bytes((request, owner._description, owner._definitions))402            # 默认费用保持;开启时先覆盖本层新子路径及固定责任槽,仓本身另按原公式付款。403            if self._capture_message_updates:404                cost += 4096 + 32 * len(str(owner.work_dir))405            if self._input_fingerprints_enabled:406                cost += 8192  # 两个SHA状态、计数、memoryview与最终64字符摘要独立先付费。407            _reserve_owned_ram(self, cost, '_fixed_cost')408            if self._input_fingerprints_enabled:409                if not owner.batch_read:410                    raise ValueError('CAN真实输入指纹需要批次读取')411                self._input_timestamp_sha = hashlib.sha256()412                self._input_records_sha = hashlib.sha256()413            self._identity = uuid4().hex414            self._work_path = owner.work_dir / self._identity415            self._selected = tuple(pair for pair in owner._definitions if pair[0].name in request.signal_names)416            self._validator = BatchValidator(owner._description, request)417        except BaseException as exc:418            self._failed = True419            _cleanup_owner(self, exc, 'can_observation_read_cleanup_owner', 'CAN read构造')420            raise421422    @property423    def capture_message_updates(self):424        """读取创建本read时的原捕获开关,不因adapter关闭而失去来源。425426        参数: 无。返回: 原生bool快照。异常: 无;该标志本身不证明已经完成输入。427        """428        return self._capture_message_updates429430    @property431    def identity(self):432        """读取本次独立UUID。参数: 无。返回: str。异常: 无,不按request值复用。"""433        return self._identity434435    @property436    def request(self):437        """读取创建时原请求。参数: 无。返回: 原ReadRequest。异常: 关闭时ResultClosedError。"""438        self._require_open()439        return self._request440441    @property442    def closed(self):443        """读取真实关闭事实。参数: 无。返回: bool。异常: 无。"""444        return self._closed445446    @property447    def failed(self):448        """读取失败标记。参数: 无。返回: bool。异常: 无,不代表已清理。"""449        return self._failed450451    @property452    def finished(self):453        """读取真实完整StopIteration事实。参数: 无。返回: bool。异常: 无。"""454        return self._finished455456    @property457    def exhausted(self):458        """提供完成别名。参数: 无。返回: bool。异常: 无,EOF批交付仍为False。"""459        return self._finished460461    def _require_open(self):462        """拒绝已关闭/失败/半关闭。参数: 无。返回: None。异常: ResultClosedError。"""463        if self._closed or self._closing or self._failed:464            raise ResultClosedError('CAN read已关闭、失败或正在清理')465466    @property467    def observation(self):468        """仅完整完成后借读事实。参数: 无。返回: CANObservation。异常: ResultClosedError。"""469        self._require_open()470        if not self._finished:471            raise ResultClosedError('CAN read尚未真正完整耗尽')472        return self._observation473474    @property475    def observed_bounds(self):476        """读取已经看到的首末时间供在线规则定界,不表示输入已完整结束。477478        参数: 无。返回: 两个必要整数或None;none时间范围不补造坐标。异常: 读取器已关闭/失败。479        """480        self._require_open()481        return self._first_ns,self._last_ns482483    @property484    def input_fingerprints(self):485        """借读实际输入的两段字节指纹,不把它解释成普通信号样本摘要。486487        参数: 无。返回: (时间SHA256,记录SHA256,全原帧数,保留记录数)的tuple。488        异常: ResultClosedError为未启用、未完成、失败或已关闭;不返回半成品hash。489        指纹只活到本read关闭,接收方要继续保留须在自己的预算下复制/持有。490        """491        self._require_open()492        if not self._finished or self._input_fingerprints is None:493            raise ResultClosedError('CAN真实输入指纹尚未完成或没有启用')494        return self._input_fingerprints495496    def _observe_validated_packet(self, process, payload, batch):497        """在严格批解包后借用原字节,按原顺序增量计算时间和所需记录指纹。498499        参数: process必须是本read的实际子进程read;payload/batch为其刚核完的借用包。500        返回: None。异常: SourceReadError为归属/原序不符;原hash/取消错误保持且标记失败。501        时间区是头后每原帧8字节,记录区保每条长度及原JSON;不包含随分批改变的包头。502        任一更新中断后本read永久失败,不能重试同一批或发布部分指纹。503        """504        view = time_view = record_view = None505        try:506            self._require_open()507            if (not self._input_fingerprints_enabled or self._input_timestamp_sha is None508                    or self._input_records_sha is None or self._input_fingerprints is not None509                    or process is not self._process or payload is not process._pending_reply510                    or batch.start_sequence != self._input_frame_count):511                raise SourceReadError('CAN输入指纹须来自同次已核批且按全原帧序号连续')512            view = memoryview(payload)513            from ..legacy.batch_wire import record_payload_offset, FILE_HEADER514            end = record_payload_offset(batch) + (FILE_HEADER.size if len(self._spec.input_paths)>1 else 0)515            record_view = view[end:]516            if self._owner.time_scope == 'source':517                # 全来源模式才生成全部原时间SHA;新头长度由记录区与帧数确定。518                time_view = view[end - 8 * batch.frame_count:end]519                self._input_timestamp_sha.update(time_view)520            self._input_records_sha.update(record_view)521            self._input_frame_count += batch.frame_count522            self._input_retained_count += len(batch.records)523        except BaseException as exc:524            self._failed = True525            exc.can_observation_read_cleanup_owner = self526            raise527        finally:528            # 即使调用方保存错误栈,也不由本层的视图继续保活整包;不复制任何原数据。529            for borrowed in (time_view, record_view, view):530                if borrowed is not None:531                    borrowed.release()532            process = payload = batch = borrowed = view = time_view = record_view = None533534    @property535    def runtime_metadata(self):536        """借读同次READY原冻结映射,None/空unit仍未核实。537538        参数: 无。返回: 保operation/version/profile等原键的映射。异常: 未READY/关闭ResultClosedError。539        """540        self._require_open()541        if self._runtime_metadata is None:542            raise ResultClosedError('CAN read尚未取得同次READY')543        return self._runtime_metadata544545    def prepare_metadata(self):546        """初始化同次DBC并返回周期/单位事实,不读取首帧。547548        参数: 无。返回: 同一冻结READY。异常: 原启动错误,沿本read清理链处理。549        重复调用复用同次初始化;不把初始化成功当作输入EOF。550        """551        self._require_open()552        try:553            if self._runtime_metadata is None:554                self._start()555            return self._runtime_metadata556        except BaseException as exc:557            self._fail(exc)558            raise559560    def _adopt(self, exc):561        """接管未返回的实际CAN读取进程/raw/消息仓子owner。参数: exc为原错误。返回: None。异常: 无预期异常。"""562        if self._process is None:563            self._process = getattr(exc, 'can_process_read_cleanup_owner', None)564        if self._raw_store is None and not self._taken:565            self._raw_store = getattr(exc, 'raw_frame_store_cleanup_owner', None)566        # 消息仓构造可能已经取得真实FD/费用却未返回;只接尚未移交的本层子责任。567        if self._capture_message_updates and self._message_store is None and not self._message_store_taken:568            self._message_store = getattr(exc, 'decoded_message_store_cleanup_owner', None)569570    def _fail(self, exc):571        """保原错/取消并关闭同一最高read。参数: exc。返回: None。异常: 清理副错只note。"""572        self._failed = True573        self._adopt(exc)574        exc.can_observation_read_cleanup_owner = self575        _cleanup_owner(self, exc, 'can_observation_read_cleanup_owner', 'CAN观测read')576577    def _start(self):578        """原start仅INIT,复制同次READY并核全部声明单位后才允许第一PULL。579580        参数: 无。返回: None。异常: 原CAN读取进程/单位/预算/原始暂存错误;上层保真实子槽。581        """582        request = LegacyCANRequest(self._spec.path, self._spec.dbc_path, self._owner._fields)583        if self._owner.batch_read:584            from ..legacy.batch_read import LegacyCANBatchRead585            ids = None if (self._owner.retain_raw_frames or self._capture_message_updates586                and self._owner.message_frame_ids is None) else tuple(sorted(587                {field.frame_id for _, field in self._owner._definitions}588                | set(self._owner.message_frame_ids or ())))589            # 传输预付不能吃光下一步装箱/验证/磁盘编码需要的空间;这里只留余量,590            # 费用仍由实际消费owner在使用前逐笔入账,_record_cost已在PULL前支付。591            # 同一次read的定位/路径/定义不可变;算一次每别名装箱费,后续样本592            # 仍逐笔预付同一金额,不为相同的元数据反复遍历整个定义树。593            self._sample_pack_costs = tuple(32768 + 32 * _metadata_bytes((definition, field,594                max((str(p) for p in self._spec.input_paths),key=_metadata_bytes), self._spec.legacy_profile, self._request.session_id,595                self._spec.name)) for definition, field in self._selected)596            per_sample = 2 * max(self._sample_pack_costs, default=32768)597            scratch = (65536 + 64 * MAX_DECODED_MESSAGE_ROW_BYTES598                if self._capture_message_updates or self._owner.retain_raw_frames else 0)599            sample_bound = self._request.batch_rows if self._selected else 0600            headroom = 131072 + sample_bound * per_sample + scratch601            # 当前批交给分析层前,只在上述消费空间之外预付并授权下一批。602            # 当前处理失败/取消可能多读至多一批;预算不足时批read自动同步回退。603            source_options = {'cycle_fields':self._owner._cycle_fields} if self._owner._cycle_fields else {}604            if self._owner._metadata_fields:605                source_options['metadata_fields'] = self._owner._metadata_fields606            if len(self._spec.input_paths) > 1:607                source_options['input_paths'] = self._spec.input_paths608            if self._spec.decode_policy != 'legacy':609                source_options['decode_policy'] = self._spec.decode_policy610            if self._owner.decode_scope == 'requested':611                self._process = LegacyCANBatchRead(request, self._budget, frame_ids=ids,612                    consumer_headroom_bytes=headroom, prefetch=True, decode_scope='requested',613                    input_check=self._owner.input_check, time_scope=self._owner.time_scope,614                    decode_frame_ids=self._owner.decode_frame_ids, **source_options)615            else:616                # 旧调用连新增默认关键字也不传,保原构造与故障注入入口。617                self._process = LegacyCANBatchRead(request, self._budget, frame_ids=ids,618                    consumer_headroom_bytes=headroom, prefetch=True, **source_options)619        else:620            self._process = LegacyCANProcessRead(request, self._budget)621        if self._input_fingerprints_enabled:622            self._process._validated_packet_consumer = self._observe_validated_packet623        self._process.start()624        ready = self._process.metadata625        _reserve_owned_ram(self, 4096 + 16 * _metadata_bytes(ready), '_metadata_cost')626        self._runtime_metadata = freeze_metadata(ready)627        from ..sources.can_updates import CANUpdateProjection628        projection = self._owner._update_projection or CANUpdateProjection(self._spec.name,629            self._request.session_id, tuple(field for _, field in self._owner._definitions))630        self._binding_projection = projection.for_read(ready['fields'])631        if len(self._spec.input_paths)>1 or self._spec.decode_policy != 'legacy' or self._owner._cycle_fields or self._owner._metadata_fields:632            if (ready.get('version') != 5633                    or ready.get('input_paths') != tuple(str(p) for p in self._spec.input_paths)634                    or ready.get('decode_policy') != self._spec.decode_policy):635                raise SourceReadError('同次CAN READY与连续文件/解码政策不符')636            ready = dict(ready,version=ready['base_version'])637        # 同次READY先与实际adapter交叉核;版本/集合不能从缺项猜成默认成功。638        if self._owner.decode_scope == 'requested':639            fixed = ready.get('decode_frame_ids')640            expected_version = 4 if self._owner.input_check == 'auto' else 3641            if (type(ready.get('version')) is not int or ready['version'] != expected_version642                    or type(ready.get('decode_scope')) is not str or ready['decode_scope'] != 'requested'643                    or type(fixed) is not tuple or any(type(value) is not int for value in fixed)644                    or fixed != self._owner.decode_frame_ids645                    or type(ready.get('decoded_values_policy')) is not str646                    or ready['decoded_values_policy'] != REQUESTED_VALUES_POLICY):647                raise SourceReadError('同次CAN READY与来源固定requested策略不符')648            if expected_version == 4 and (ready.get('input_check') != self._owner.input_check or ready.get('time_scope') != self._owner.time_scope):649                raise SourceReadError('同次CAN READY检查/时间范围与来源不同')650        elif (type(ready.get('version')) is not int or ready['version'] != 1651                or 'decode_scope' in ready or 'decode_frame_ids' in ready652                or 'decoded_values_policy' in ready):653            raise SourceReadError('全解码来源必须保原v1 READY且不带按需策略')654        for definition, field in self._owner._definitions:655            unit = next(item['unit'] for item in ready['fields'] if item['locator'] == field_to_dict(field))656            if unit not in (None, '') and unit != definition.input_unit:657                raise SourceReadError('同次DBC非空单位不等于声明input_unit: ' + definition.name)658        if self._owner.retain_raw_frames:659            self._raw_store = RawFrameStore(self._budget, self._work_path, self._spec.name, integrity=self._owner._history_integrity)660        # 原raw目录保持不变;可选子目录只在READY/单位核对完成后、首PULL之前创建。661        if self._capture_message_updates:662            self._message_store = DecodedMessageStore(self._budget, self._work_path / 'messages',663                self._spec.name, self._request.session_id, decoder_profile=self._runtime_metadata['profile'],664                read_identity=self._identity, sparse_sequences=self._owner.batch_read, integrity=self._owner._history_integrity)665        self._started = True666667    def _accept(self, batch):668        """先预约validator候选及同刻去重集合,再原子接收并留下实际保留费用。669670        参数: batch为有限SignalBatch。返回: 原batch。异常: 原协议/预算/取消,由上层poison。671        """672        _reserve_owned_ram(self, 4096 + 2 * _pending_bytes(self._validator)673            + 1024 * batch.sample_count, '_validation_cost')674        self._validator.accept(batch)675        retained = _pending_bytes(self._validator)676        _release_owned_ram(self, self._pending_cost, '_pending_cost')677        self._validation_cost -= retained678        self._pending_cost += retained679        _release_owned_ram(self, self._validation_cost, '_validation_cost')680        self._current = batch681        return batch682683    def _release_current(self):684        """下一步前停止持有原batch再归还其装箱空间。参数: 无。返回: None。异常: 原抵销/取消。"""685        self._current = None686        _release_owned_ram(self, self._pack_cost, '_pack_cost')687688    def _new_record(self):689        """原帧进入前先付本层记录/引用空间;全部yield先暂存再作分析层列选择。690691        参数: 无。返回: True有新原帧,False为CAN读取进程真实EOF。异常: 原输入/时间/暂存错误。692        """693        self._record = None694        _release_owned_ram(self, self._record_cost, '_record_cost')695        _reserve_owned_ram(self, 4259840, '_record_cost')696        record = conversion = None697        try:698            try:699                record = next(self._process)700            except StopIteration:701                _release_owned_ram(self, self._record_cost, '_record_cost')702                if not self._process.finished:703                    raise SourceReadError('CAN进程没有完整EOF')704                if self._raw_store is not None:705                    self._raw_store.seal()706                # 两仓仅在同一次真实CAN读取进程 EOF后封口;任一失败都不能开放finished或移交。707                if self._capture_message_updates:708                    self._message_store.seal()709                return False710            conversion = convert_time(record.frame.timestamp, 's')711            mapped = checked_add_ns(conversion.time_ns, self._spec.offset_ns)712            if self._last_ns is not None and mapped < self._last_ns:713                raise SourceReadError('CAN原yield时间回退,禁止重排')714            if self._raw_store is not None:715                self._raw_store.append(record, mapped)716            # 同一个原decode对象恰追加一次,位于列选择之前,不因空投影或拆别名改变次数。717            if self._capture_message_updates:718                self._message_store.append(record, mapped, time_quantized=conversion.quantized)719            self._binding_projection.fields_for(record.frame)720            if self._owner._frame_observer is not None:721                self._owner._frame_observer(record.frame)722            if self._first_ns is None:723                self._first_ns = mapped724            self._last_ns, self._row_count = mapped, self._row_count + 1725            self._record, self._conversion, self._mapped, self._alias_index = record, conversion, mapped, 0726            self._deliver_message(record, mapped)727            return True728        finally:729            # 原异常可被保留;只断本函数临时大record/转换,不清原输入或其他模块TB。730            record = conversion = None731732    def _deliver_message(self, record, time_ns):733        """仅有明确消费者时构造共同消息,默认读取不增加逐帧对象。734735        参数: record为同次原解码帧;time_ns为已检查的时间或未采集None。736        返回: None。异常: 投影、预算或消费者错误由read统一标记失败并清理。737        """738        if self._owner._update_consumer is not None:739            from .message_delivery import deliver_message740            deliver_message(self, self._binding_projection, self._owner._update_consumer,741                            record.frame, record.sequence, record.values, time_ns)742743    def _batch(self):744        """为当前原帧最多batch_rows个匹配别名装箱,缺key产生明确MISSING更新。745746        参数: 无。返回: 已校验SignalBatch。异常: 原refs/归一化/预约/协议错误。747        """748        _reserve_owned_ram(self, 65536, '_pack_cost')749        columns = {}750        while self._alias_index < len(self._selected):751            definition, field = self._selected[self._alias_index]752            self._alias_index += 1753            if field not in self._binding_projection.fields_for(self._record.frame):754                continue755            # 完整定位/定义及单样本数组均先付;不按原数值相同而合并独立别名。756            _reserve_owned_ram(self, 16384 + 32 * _metadata_bytes((definition, field, str(self._spec.path),757                self._spec.legacy_profile, self._request.session_id, self._spec.name)), '_pack_cost')758            ref = CANSourceRef(self._request.session_id, self._spec.name, definition.field,759                str(self._spec.path), self._spec.legacy_profile, field.channel, field.signal,760                field.frame_id, self._record.frame.is_extended_id, self._record.frame.is_fd, self._record.sequence,761                self._record.frame.timestamp, self._conversion.quantized)762            value = self._record.values.get(field.signal)763            columns[definition.name] = pack_decoded(definition,764                ((self._mapped, self._record.sequence, value),), (ref,), self._owner.backend)765            if len(columns) == self._request.batch_rows:766                break767        watermark = None if self._mapped == -(1 << 63) else self._mapped - 1768        return self._accept(SignalBatch(self._spec.name, self._request.session_id, columns,769            watermark, False, None))770771    def __iter__(self):772        """保持本次原序位置。参数: 无。返回: self。异常: 无,不启动读取。"""773        return self774775    def __next__(self):776        """逐次交付当前原帧分片或独立空EOF批,真正下一次耗尽才开放完成事实。777778        参数: 无。返回: SignalBatch。异常: 真StopIteration/原来源/预算/取消,最高read可恢复。779        """780        if self._finished:781            raise StopIteration782        self._require_open()783        try:784            self._release_current()785            if not self._started:786                self._start()787            if self._eof_delivered:788                self._finish()789            elif self._record is not None and self._alias_index < len(self._selected):790                return self._batch()791            elif self._new_record():792                return self._batch()793            else:794                _reserve_owned_ram(self, 65536, '_pack_cost')795                batch = SignalBatch(self._spec.name, self._request.session_id, {}, self._last_ns,796                    True, self._last_ns)797                self._accept(batch)798                self._eof_delivered = True799                return batch800        except BaseException as exc:801            self._fail(exc)802            raise803        raise StopIteration804805    def _close_child(self, slot):806        """真实子owner完成关闭后才清槽。参数: slot为实际子槽。返回: None。异常: 原close错误。"""807        child = getattr(self, slot)808        if child is not None:809            try:810                child.close()811            finally:812                if child.closed:813                    setattr(self, slot, None)814815    def _finish(self):816        """确认本read全部完成条件后开放事实,并解除adapter活登记。817818        参数: 无。返回: None。异常: 原关闭/协议/登记错误,不能伪造完成。819        """820        if (not self._validator.ended or not self._process.finished821                or self._owner.retain_raw_frames and (self._raw_store is None822                    or self._raw_store.state != 'sealed' or self._raw_store.closed)):823            raise SourceReadError('CAN输入尚未满足真实完成条件')824        if self._capture_message_updates:825            # 完成事实必须同时属于本read的原消息仓,不能只凭原帧仓已封口推断。826            store = self._message_store827            if (store is None or store.closed or store.state != 'sealed'828                    or (store.count != self._row_count if not store.sparse_sequences else829                        store.count > self._row_count or store._last_sequence >= self._row_count)830                    or store.source_name != self._spec.name or store.session_id != self._request.session_id831                    or store.read_identity != self._identity832                    or store.decoder_profile != self._runtime_metadata['profile']):833                raise SourceReadError('同次CAN消息仓尚未满足原对象及完整封口事实')834        self._close_child('_process')835        self._record = None836        _release_owned_ram(self, self._record_cost, '_record_cost')837        if self._input_fingerprints_enabled:838            if (self._input_frame_count != self._row_count839                    or self._input_timestamp_sha is None or self._input_records_sha is None):840                raise SourceReadError('CAN真实输入指纹的原帧数与完成观测不符')841            self._input_fingerprints = (self._input_timestamp_sha.hexdigest() if self._owner.time_scope == 'source' else None,842                self._input_records_sha.hexdigest(), self._input_frame_count, self._input_retained_count)843            self._input_timestamp_sha = self._input_records_sha = None844        self._observation = CANObservation(self._spec.name, self._request.session_id, self._request,845            self._identity, self._first_ns, self._last_ns, self._row_count, True,846            self._owner.time_scope, self._retained_record_count, self._full_check_count)847        self._owner._requests.discard(self)848        self._finished = True849850    def take_raw_store(self, receiver, slot='_raw_store'):851        """仅完成后一次移交真实sealed store,本read后续不再关闭新父资源。852853        参数: receiver为同一_budget的可信内部owner;slot为其已初始化为None的实际接收槽。854        返回: 原RawFrameStore,先装父槽再撤本子槽。异常: 状态/预算/槽错误或原安装取消;855            安装后取消仍由接收父实际槽可达,不在返回边界丢失真实费用。856        """857        self._require_open()858        if not self._finished or self._taken or self._raw_store is None:859            raise ResultClosedError('原始store未完成或已移交')860        result = self._raw_store861        if (getattr(receiver, '_budget', None) is not self._budget or type(slot) is not str862                or not slot.startswith('_') or getattr(receiver, slot) is not None):863            raise ValueError('原始store移交须同账本且接收实际槽为空')864        try:865            # 父赋值先发生;即使setter在真实安装后取消,也至少有最高父实际责任。866            setattr(receiver, slot, result)867        except BaseException as exc:868            exc.can_observation_read_cleanup_owner = self869            raise870        finally:871            if getattr(receiver, slot) is result:872                self._raw_store, self._taken = None, True873        return result874875    def take_message_store(self, receiver, slot='_message_store'):876        """完成后一次移交同次消息仓,父实际安装成功才解除本read责任。877878        参数: receiver为同一_budget的可信owner;slot为其已存在且为空的实际槽。879        返回: 原DecodedMessageStore对象,不重建文件或复制decoded记录。880        异常: ResultClosedError为未开启、未完成或已移交;ValueError为预算/槽不符;881            父安装前后取消保原异常和can_observation_read_cleanup_owner。882        """883        self._require_open()884        if (not self._finished or not self._capture_message_updates or self._message_store_taken885                or self._message_store is None):886            raise ResultClosedError('消息store未开启、未完成或已移交')887        store = self._message_store888        if (getattr(receiver, '_budget', None) is not self._budget or type(slot) is not str889                or not slot.startswith('_') or not hasattr(receiver, slot) or getattr(receiver, slot) is not None):890            raise ValueError('消息store移交须同账本且接收实际槽为空')891        try:892            # setter可能在实际安装后取消;finally按真实父槽判断,不能先清子再return。893            setattr(receiver, slot, store)894        except BaseException as exc:895            exc.can_observation_read_cleanup_owner = self896            raise897        finally:898            if getattr(receiver, slot, None) is store:899                self._message_store, self._message_store_taken = None, True900        return store901902    def close(self):903        """只关闭仍自有的输入/raw/消息store和所有实际费用,可持续重试。904905        参数: 无。返回: None。异常: 首个子关闭/费用取消;can_observation_read_cleanup_owner保本对象。906        """907        if self._closed:908            return909        self._closing = True910        failure = None911        for slot in ('_process', '_raw_store', '_message_store'):912            try:913                self._close_child(slot)914            except BaseException as exc:915                if failure is None:916                    failure = exc917                else:918                    failure.add_note('CAN其他子资源关闭附加失败: ' + type(exc).__name__)919        if self._process is None and self._raw_store is None and self._message_store is None:920            self._record = self._current = self._runtime_metadata = self._observation = None921            self._validator = self._selected = None922            self._binding_projection = None923            self._sample_pack_costs = ()924            self._input_timestamp_sha = self._input_records_sha = self._input_fingerprints = None925            for slot in ('_update_cost', '_pack_cost', '_record_cost', '_validation_cost', '_pending_cost', '_metadata_cost', '_fixed_cost'):926                try:927                    _release_owned_ram(self, getattr(self, slot), slot)928                except BaseException as exc:929                    if failure is None:930                        failure = exc931                    else:932                        failure.add_note('CAN费用抵销附加失败: ' + type(exc).__name__)933            if self._held_cost == 0:934                try:935                    self._owner._requests.discard(self)936                    if self._owner._uninstalled_request is self:937                        self._owner._uninstalled_request = None938                    self._closed = True939                except BaseException as exc:940                    if failure is None:941                        failure = exc942                    else:943                        failure.add_note('CAN解除read登记附加失败: ' + type(exc).__name__)944        if failure is not None:945            failure.can_observation_read_cleanup_owner = self946            raise failure947
Chronaxis 用户手册 · 离线阅读