Chronaxis用户手册English
本页目录

chronaxis.legacy.worker

源码基线:8904c2f744951857659abd40aaa3535d57faf53c

1"""在独立spawn进程按单条PULL许可调用共用BLF读取与串行解码政策。23有效INIT后导入依赖并加载一次DBC;惰性读取器在首次PULL才打开输入,READY不取帧。4每次许可按原序读取和解码一次,STATUS/ACK/STOP不授予读取许可。只使用限长JSON5bytes协议,异常发送有限事实,实际子退出和父侧预算清理由协议控制器确认。"""67# spawn加载时不触及CAN库或输入;读取依赖只在收到有效INIT后加载。8from importlib import metadata9import os10from pathlib import Path11import platform12from ..dbc_loading import load_dbc_file13from .records import (LegacyCANRequest, LegacyDecodedFrame, LegacyRawFrame, MAX_WIRE_BYTES,14    PROFILE_ID, decode_packet, decoded_to_dict, encode_packet, field_from_dict, field_to_dict)1516def _send(connection, packet):17    """通过bytes接口发送一个有界JSON包,不调用pickle通道。1819    参数: connection为本子进程Connection;packet为协议dict。返回: None。20    异常: 原wire类型/限额或管道错误;不重试导致重复ITEM。21    """22    connection.send_bytes(encode_packet(packet))232425def _receive(connection):26    """只接收一个不超过单包上限的bytes消息。2728    参数: connection为本子进程Connection。返回: 已校验dict。29    异常: EOFError/OSError/协议错误传播,不把断管当正常来源EOF。30    """31    return decode_packet(connection.recv_bytes(MAX_WIRE_BYTES))323334def _exact(packet, names):35    """限制每种控制包的精确字段,不让多余许可或版本字段静默生效。3637    参数: packet为已解析dict;names为允许键容器。返回: None。38    异常: ValueError为字段缺失或多余。39    """40    if set(packet) != set(names):41        raise ValueError('旧CAN控制包字段缺失或多余')424344def _request(packet):45    """解析唯一INIT,只接受单个实际BLF和原DBC路径,不读取其内容摘要。4647    参数: packet为INIT dict。返回: LegacyCANRequest。48    异常: 类型/形状/版本/路径错误;不打开BLF或加载DBC。49    """50    _exact(packet, ('op', 'version', 'path', 'dbc_path', 'fields'))51    if packet['op'] != 'init' or type(packet['version']) is not int or packet['version'] != 1:52        raise ValueError('旧CAN需要init/version1')53    if type(packet['path']) is not str or type(packet['dbc_path']) is not str or type(packet['fields']) is not list:54        raise TypeError('旧CAN INIT需要文本路径与fields数组')55    request = LegacyCANRequest(Path(packet['path']), Path(packet['dbc_path']),56        tuple(field_from_dict(value) for value in packet['fields']))57    # 只做stat和扩展名边界;不额外读取原CAN或预扫消息,不暗换另一种profile。58    if request.input_path.suffix.lower() != '.blf' or not request.input_path.is_file():59        raise ValueError('首版旧CAN仅支持单个现有BLF文件')60    if not request.dbc_path.is_file():61        raise FileNotFoundError(request.dbc_path)62    return request636465def _load_can_runtime():66    """在有效INIT后导入真实CAN依赖,模块导入本身只使用标准库。6768    参数: 无。返回: python-can与cantools模块。异常: 保留缺失依赖的可执行提示。69    不读取来源、DBC或原项目文件,不安装同名vendor模块。70    """71    from ..can_dependencies import import_can_dependency72    return import_can_dependency('can'), import_can_dependency('cantools')737475def _iter_blf(path):76    """按许可惰性创建共用BLF读取器,原消息顺序交付且可靠关闭本次文件。7778    参数: 已检查的BLF路径。返回: can.Message生成器。异常: 真实读取/关闭错误。79    创建生成器不打开文件;第一次next才调用BLFRead,STOP或EOF关闭同一owner。80    """81    from ..sources.blf import BLFRead, BLFSpec82    from ..results.budget import ResourceBudget, _cleanup_owner83    reader = primary = None84    try:85        reader = BLFRead(BLFSpec(Path(path)), ResourceBudget(), frame_only=True)86        yield from reader87    except BaseException as error:88        primary = error89        if reader is None:90            pending=getattr(error,'blf_read_cleanup_owner',None)91            if pending is not None:92                _cleanup_owner(pending,error,'blf_read_cleanup_owner','协议BLF构造')93        raise94    finally:95        if reader is not None and not reader.closed:96            if primary is None or isinstance(primary,GeneratorExit):97                reader.close()98            else:99                _cleanup_owner(reader,primary,'blf_read_cleanup_owner','协议BLF读取器')100101102def _field_metadata(db, fields, *, cycle_fields=()):103    """只读同次db公开messages/signals核定位唯一,并返回原单位。104105    参数: db为同次加载的唯一DBC数据库;fields为原LegacyCANField tuple;106        cycle_fields仅指定需要周期的字段,须在fields中。默认不访问cycle_time。107    返回: 按请求顺序的字段事实;周期请求的字段另含正cycle_ns。108        仅DBC为FD而定位为经典CAN时增加固定兼容提示。109        channel保声明,不冒称DBC已测通道;不把DBC声明写回原帧。110    异常: ValueError为ID/消息/帧型/信号歧义或不符,TypeError为单位不受支持。111    """112    if cycle_fields:113        from .dbc_cycles import require_cycle_fields, cycle_time_ns114        require_cycle_fields(cycle_fields)115        if not set(cycle_fields).issubset(fields):116            raise ValueError('周期字段须包含在本次DBC定位中')117    from .binding import dbc_candidates118    periods = {} if cycle_fields else None119    result = []120    for field in fields:121        # 多定义允许由实际原帧决定;初始化所需的单位不能随候选改变。122        candidates = dbc_candidates(db, field.frame_id, (field.signal,), field.is_extended, field.is_fd)123        units = {signal.unit for message in candidates for signal in message.signals if signal.name == field.signal}124        if len(units) != 1:125            raise ValueError('候选DBC信号单位不同,请补充 is_extended 或 is_fd: ' + field.signal)126        unit = next(iter(units))127        compatible = field.is_fd is False and all(message.is_fd is True for message in candidates)128        if unit is not None and type(unit) is not str:129            raise TypeError('原DBC公开unit需要文本或None')130        item = {'locator': field_to_dict(field), 'unit': unit}131        if field.is_extended is None or field.is_fd is None:132            # 父读取器据此跳过同ID但不含所需信号的帧,不伪造一次失效更新。133            item['dbc_frames'] = [list(pair) for pair in dict.fromkeys(134                (message.is_extended_frame, message.is_fd) for message in candidates)]135        if compatible:136            item['frame_format_compatibility'] = {'code': 'dbc_fd_classic_selector',137                'dbc_is_fd': True, 'requested_is_fd': False}138        if field in cycle_fields:139            # 同一物理报文多个别名只取一次周期,后续记录不再访问DBC周期。140            values = set()141            for message in candidates:142                # 多信号来自同一消息对象时只读一次周期,同ID不同定义不能相互覆盖。143                identity = id(message)144                if identity not in periods:145                    periods[identity] = cycle_time_ns(message.cycle_time)146                values.add(periods[identity])147            if len(values) != 1:148                raise ValueError('候选DBC周期不同,请补充帧类型、period 或 valid_for: ' + field.signal)149            item['cycle_ns'] = next(iter(values))150        result.append(item)151    return result152153154def _frame(msg):155    """保存当前消息的公开写出字段,不重建CAN消息或执行解码。156157    参数: msg为共用BLF读取器返回的can.Message。返回: LegacyRawFrame。158    异常: 新边界字段校验失败;data仅无损复制为bytes,不改timestamp/DLC/标志。159    """160    if len(msg.data) > 64:161        raise ValueError('原报文data超过64字节,不能先复制再拒绝')162    return LegacyRawFrame(timestamp=msg.timestamp, arbitration_id=msg.arbitration_id,163        is_extended_id=msg.is_extended_id, is_remote_frame=msg.is_remote_frame,164        is_error_frame=msg.is_error_frame, channel=msg.channel, dlc=msg.dlc, data=bytes(msg.data),165        is_fd=msg.is_fd, is_rx=msg.is_rx, bitrate_switch=msg.bitrate_switch,166        error_state_indicator=msg.error_state_indicator)167168169def _argument_text(value, limit):170    """只摘要原生异常参数,不执行任意对象str或先展开完整巨型消息。171172    参数: value为一个原args值;limit为最大字符数。返回: 有限str,短原str保持内容。173    异常: 无主动异常;未知对象仅标明类型,不调用其自定义字符串方法。174    """175    if type(value) is str:176        return value[:limit]177    if value is None or type(value) in (bool, float):178        return repr(value)[:limit]179    if type(value) is int:180        return str(value)[:limit] if value.bit_length() <= 128 else '<超过128位的整数参数>'181    return '<参数类型:' + type(value).__name__[:64] + '>'182183184def _error_packet(exc, phase, request_id):185    """保存有限原错误事实,不把远端异常对象或任意类传给父进程。186187    参数: exc为本child原错;phase为有限阶段文本;request_id为当前许可或None。188    返回: 有限ERROR dict;message是至多4个args摘要,traceback仅至多8个栈地点,189        均不是完整str(exc)或完整traceback,截断有明确note,短单str参数保持原文本。190    异常: 标准库字段访问/编码错误传播;调用方仍须确定退出,不伪报正常EOF。191    """192    # 不全量format_exception/str(exc),仅读有限原参数及栈地点,绝不访问locals/gi_frame。193    arguments = exc.args if type(exc.args) is tuple else ()194    message = ', '.join(_argument_text(value, 1024) for value in arguments[:4])195    if not arguments:196        message = '远端异常无可安全摘要的原生args'197    places, current = [], exc.__traceback__198    for _ in range(8):199        if current is None:200            break201        code = current.tb_frame.f_code202        places.append(code.co_filename[:256] + ':' + str(current.tb_lineno) + ' ' + code.co_name[:128])203        current = current.tb_next204    trace = '\n'.join(places)205    original_notes = getattr(exc, '__notes__', ())206    notes = [note[:256] if type(note) is str else '<非文本note>' for note in207        (original_notes[:8] if type(original_notes) in (tuple, list) else ())]208    notes.append('远端message仅原args有限摘要;traceback仅有限栈地点,非完整str/trace。')209    limit = 4096210    while True:211        packet = {'op': 'error', 'request_id': request_id, 'phase': phase,212            'error': {'module': type(exc).__module__[:128], 'type': type(exc).__name__[:128],213                'message': message[:limit], 'traceback': trace[:limit],214                'notes': [note[:min(limit, 256)] for note in notes] + ['远端诊断有界;可能截断。']}}215        try:216            encode_packet(packet)217            return packet218        except ValueError:219            # 每轮减半,固定包头的诊断不受输入长度放大;最短仍明确失败类型。220            if limit <= 64:221                return {'op': 'error', 'request_id': request_id, 'phase': phase,222                    'error': {'module': type(exc).__module__[:32], 'type': type(exc).__name__[:32],223                        'message': '远端错误诊断超过wire上限', 'traceback': '', 'notes': ['诊断已截断。']}}224            limit //= 2225226227def _report_error(connection, exc, phase, request_id):228    """尽力发送原错误事实,断管不替代child原错或阻止退出。229230    参数: connection为本连接;exc/phase/request_id为原失败事实。返回: None。231    异常: 不传播附加报告错误;原失败仍由worker最终非零退出表达。232    """233    try:234        _send(connection, _error_packet(exc, phase, request_id))235    except BaseException as diagnostic:236        exc.add_note('旧CAN远端错误报告附加失败: ' + type(diagnostic).__name__)237238239def worker_main(connection):240    """作为spawn目标按INIT/PULL协议调用共用BLF读取与串行兼容解码,等待许可时不预取。241242    参数: connection为父传来的独立duplex Connection。返回: 正常STOP或真实EOF时None。243    异常: 原错误尽力发ERROR后以SystemExit(1)退出;连接/生成器附加关闭错误也非零。244    副作用: 本child内一次DBC加载、一次iter_frames、每许可一次next/parse;不额外扫描。245    """246    iterator, failure, request_id = None, None, None247    phase = 'init'248    pulls_received = frames_returned = parse_calls = 0249    try:250        request = _request(_receive(connection))251        phase = 'dependencies'252        can, cantools = _load_can_runtime()253        phase = 'dbc'254        # 共用安全DBC加载函数执行一次加载,不做第二次解析。255        db = load_dbc_file(str(request.dbc_path), cantools)256        phase = 'metadata'257        from .binding import FrameBindingResolver258        from .decode_policy import decode_serial_frame as _parse_selected259        from collections import Counter260        resolver = FrameBindingResolver(db, request.fields)261        duplicate_ids = {frame_id for frame_id, count in Counter(message.frame_id for message in db.messages).items()262                         if count > 1}263        phase = 'metadata'264        fields = _field_metadata(db, request.fields)265        iterator = _iter_blf(str(request.input_path))266        versions = {'python': platform.python_version(), 'python-can': metadata.version('python-can'),267            'cantools': metadata.version('cantools'), 'natsort': metadata.version('natsort')}268        _send(connection, {'op': 'ready', 'version': 1, 'profile': PROFILE_ID, 'worker_pid': os.getpid(),269            'fields': fields, 'dependency_versions': versions})270        # 只在收到下一条PULL时取原下一帧;STATUS/ACK/STOP均不授予此许可。271        while True:272            phase, request_id = 'control', None273            packet = _receive(connection)274            operation = packet.get('op')275            if operation == 'stop':276                _exact(packet, ('op',))277                break278            if operation == 'status':279                _exact(packet, ('op',))280                _send(connection, {'op': 'status', 'pulls_received': pulls_received,281                    'frames_returned': frames_returned, 'parse_calls': parse_calls})282                continue283            if operation == 'ack':284                expected = ('op', 'request_id') if 'request_id' in packet else ('op',)285                _exact(packet, expected)286                if 'request_id' in packet and (frames_returned == 0 or type(packet['request_id']) is not int287                        or packet['request_id'] != frames_returned - 1):288                    raise ValueError('ACK只允许确认最近返回ITEM')289                _send(connection, packet)290                continue291            if operation != 'pull':292                raise ValueError('未知旧CAN控制操作')293            _exact(packet, ('op', 'request_id'))294            candidate_id = packet['request_id']295            request_id = candidate_id if type(candidate_id) is int and candidate_id >= 0 else None296            if request_id is None or request_id != frames_returned:297                raise ValueError('PULL许可必须从0严格连续')298            pulls_received += 1299            phase = 'next'300            try:301                msg = next(iterator)302            except StopIteration:303                # EOF需要实际多一次许可,候选发出后仍须父确认本child退出,不能靠断管猜EOF。304                _send(connection, {'op': 'eof', 'request_id': request_id, 'frame_count': frames_returned})305                break306            phase = 'parse'307            parse_calls += 1308            fields_for_frame = resolver.fields_for(msg)309            message = resolver.message_for(msg, fields_for_frame)310            values = _parse_selected(db, msg, message, duplicate_ids)311            phase = 'item'312            record = LegacyDecodedFrame(frames_returned, _frame(msg), values)313            _send(connection, {'op': 'item', 'request_id': request_id, 'record': decoded_to_dict(record)})314            frames_returned += 1315            # 释放本层当前wire/记录引用,不给下一帧任何许可;旧generator内部状态不读取。316            msg = values = record = None317    except BaseException as exc:318        failure = exc319        _report_error(connection, exc, phase, request_id)320    finally:321        # 生成器退出时关闭共用BLF owner;父进程仍须确认实际子退出。322        if iterator is not None:323            try:324                iterator.close()325            except BaseException as cleanup:326                if failure is None:327                    failure = cleanup328                    _report_error(connection, cleanup, 'generator_close', request_id)329                else:330                    failure.add_note('旧生成器关闭附加失败: ' + type(cleanup).__name__)331        try:332            connection.close()333        except BaseException as cleanup:334            if failure is None:335                failure = cleanup336            else:337                failure.add_note('旧CAN子连接关闭附加失败: ' + type(cleanup).__name__)338    if failure is not None:339        raise SystemExit(1)340
Chronaxis 用户手册 · 离线阅读