Chronaxis用户手册English
本页目录

chronaxis.results.frame_patch

源码基线:8904c2f744951857659abd40aaa3535d57faf53c

1"""用已有DBC位域工具更新明确的连续信号,其他位沿用同一基础帧。23调用者传入本次已加载的cantools Message及本次已解码的物理值,不再次读取DBC4或解码原帧。整数按nearest_even-v1量化,浮点沿DBC编码后核实际表示误差。5码值或物理值越界立即拒绝。只更新选中位的掩码,计数、校验及未知位保持;6这些生成帧不承诺校验码有效或可用于车辆回放。7"""89from dataclasses import dataclass, replace10from fractions import Fraction11from math import isfinite1213from .budget import _reserve_owned_ram, _release_owned_ram, _cleanup_owner14from ..legacy.records import normalize_frame_id151617@dataclass(frozen=True)18class FrameRebuildRule:19    """明确报文身份、连续字段及禁止插值的字段;声明不能从名字猜业务含义。"""2021    channel: int22    frame_id: int23    continuous: tuple[str,...]24    is_extended: bool | None = None  # 省略时按实际帧与DBC确定,不等同标准帧。25    is_fd: bool | None = None  # 省略时保留输入帧的实际CAN/FD类型。26    protected: tuple[str,...] = ()27    same_time_policy: str = 'error'2829    def __post_init__(self):30        """核身份和字段分类。参数: 无。返回: None。异常: 非法/重复/保护字段冲突。31        """32        # ID只在规则建立时归一化,避免每个输出帧重新解释进制。33        object.__setattr__(self,'frame_id',normalize_frame_id(self.frame_id))34        if (type(self.channel) is not int or self.channel<035                or self.is_extended is False and self.frame_id>0x7ff36                or self.is_extended is not None and type(self.is_extended) is not bool37                or self.is_fd is not None and type(self.is_fd) is not bool38                or self.same_time_policy not in ('error','last_by_sequence')):39            raise ValueError('报文重建身份或同刻策略非法')40        for names in (self.continuous,self.protected):41            if type(names) is not tuple or len(names)>4096 or any(type(n) is not str or not n for n in names) or len(set(names))!=len(names):42                raise ValueError('报文重建字段需要唯一非空名称tuple')43        if not self.continuous or set(self.continuous)&set(self.protected):44            raise ValueError('必须选择连续量,且不能插值保护的计数或校验字段')4546    def matches(self,frame):47        """按实际帧身份匹配。参数: frame原帧。返回: bool。异常: 原模型访问错。48        """49        return (frame.channel==self.channel and frame.arbitration_id==self.frame_id50            and (self.is_extended is None or frame.is_extended_id==self.is_extended)51            and (self.is_fd is None or frame.is_fd==self.is_fd))525354class DBCFramePatch:55    """借用一条已加载DBC定义,拥有选中位格式与预算;只缓存当前复用布局。"""5657    def __init__(self,rule,message,budget):58        """核DBC身份与连续字段并预约格式空间。参数: rule/message/budget。返回: patcher。异常: 不匹配或预算错。59        """60        if type(rule) is not FrameRebuildRule:61            raise TypeError('需要FrameRebuildRule')62        self.rule=rule; self.message=message; self._budget=budget63        self._held_cost=self._fixed_cost=0; self._closed=False64        self.signals={}; self.formats=None; self.active=(); self.mask=065        self._identity=None66        try:67            _reserve_owned_ram(self,262144+len(message.signals)*16384,'_fixed_cost')68            if (message.frame_id!=rule.frame_id69                    or rule.is_extended is not None and message.is_extended_frame!=rule.is_extended70                    or rule.is_fd is True and not message.is_fd71                    or not 0<message.length<=64 or message.is_container):72                raise ValueError('重建规则与DBC报文身份/载荷不符或为容器报文')73            self.signals={signal.name:signal for signal in message.signals}74            for name in (*rule.continuous,*rule.protected):75                if name not in self.signals:76                    raise ValueError('DBC缺少重建声明字段: '+name)77            for name in rule.continuous:78                signal=self.signals[name]79                if signal.is_multiplexer or signal.choices or not signal.scale or signal.length==1:80                    raise ValueError('复用选择器、枚举/布尔或零倍率不能当连续量: '+name)81        except BaseException as error:82            _cleanup_owner(self,error,'frame_patch_cleanup_owner','DBC位域重建构造')83            raise8485    def accept_frame(self,frame):86        """检查实际基础帧与选定DBC,并识别自动规则后续出现的帧型冲突。8788        参数: frame为同次输入原帧。返回: None。异常: ValueError为类型不匹配或歧义。89        副作用: 只记录首次匹配的两个类型标志,后续同型帧不扩大状态。90        """91        if (not self.rule.matches(frame) or frame.is_extended_id!=self.message.is_extended_frame92                or frame.is_fd and not self.message.is_fd):93            raise ValueError('重建实际帧与已选DBC类型不符')94        identity=(frame.is_extended_id,frame.is_fd)95        if self._identity is not None and self._identity!=identity:96            flag='is_extended' if self._identity[0]!=identity[0] else 'is_fd'97            raise ValueError('重建规则匹配多种实际帧类型,请补充'+flag)98        self._identity=identity99100    def selected(self,layout):101        """只取当前复用分支适用的连续字段。参数: layout原生选择器码值tuple。返回: 名称tuple。异常: 布局非法。102        """103        selectors=dict(layout)104        if len(selectors)!=len(layout) or any(name not in self.signals or not self.signals[name].is_multiplexer105                or type(value) is not int for name,value in layout):106            raise ValueError('报文复用布局不合法')107        selected=[]108        for name in self.rule.continuous:109            signal=self.signals[name]; visited=set(); applicable=True110            # 沿真实父选择器判断,避免将另一分支同位置的位当作本连续量。111            while signal.multiplexer_signal is not None:112                parent=signal.multiplexer_signal113                if parent in visited or parent not in self.signals:114                    raise ValueError('DBC复用父链非法')115                visited.add(parent)116                if parent not in selectors or selectors[parent] not in signal.multiplexer_ids:117                    applicable=False; break118                signal=self.signals[parent]119            if applicable:120                selected.append(name)121        return tuple(selected)122123    def patch(self,base,values,layout,*,exact_values=None,trace=True):124        """仅按允许掩码更新载荷并记录量化误差。125126        参数: base同一基础LegacyRawFrame;values所需物理估计dict;layout实际复用布局;127            exact_values可选的有理数编码目标,用于避免中间float改变半码决策。128        返回: (新帧, 每字段目标/码值/实际值/误差tuple)。异常: 缺值、范围/格式/编码错误。129        """130        if self._closed or not self.rule.matches(base) or base.dlc!=self.message.length or len(base.data)!=self.message.length:131            raise ValueError('重建基础帧已关闭、身份或长度不符')132        self.accept_frame(base)133        selected=self.selected(layout)134        if not selected or set(values)!=set(selected):135            raise ValueError('当前复用布局缺少必须的连续值')136        if exact_values is not None and (set(exact_values)!=set(selected) or any(type(v) is not Fraction for v in exact_values.values())):137            raise ValueError('精确量化目标需要同组Fraction字段')138        from cantools.database.utils import create_encode_decode_formats,encode_data,decode_data139        if selected!=self.active:140            # 编解码格式来自现有库;仅所选信号的非padding位允许改变。141            self.formats=create_encode_decode_formats([self.signals[n] for n in selected],self.message.length)142            self.mask=((1<<(self.message.length*8))-1)^self.formats.padding_mask143            self.active=selected144        raw={}145        for name in selected:146            signal=self.signals[name]; value=values[name]147            if type(value) not in (int,float) or not isfinite(value):148                raise ValueError('重建物理值必须有限: '+name)149            physical=Fraction(str(value)) if exact_values is None else exact_values[name]150            scale=Fraction(str(signal.scale)); offset=Fraction(str(signal.offset))151            if ((signal.minimum is not None and physical<Fraction(str(signal.minimum)))152                    or (signal.maximum is not None and physical>Fraction(str(signal.maximum)))):153                raise ValueError('重建目标物理值越界: '+name)154            code=(physical-offset)/scale155            if signal.is_float:156                raw[name]=float(code)157            else:158                # Fraction的round精确实现最近整数/恰半取偶,负倍率和负值同样适用。159                integer=round(code)160                low=-(1<<(signal.length-1)) if signal.is_signed else 0161                high=(1<<(signal.length-int(signal.is_signed)))-1162                if not low<=integer<=high:163                    raise ValueError('重建码值超出位宽: '+name)164                raw[name]=integer165        fields=[self.signals[n] for n in selected]166        encoded=encode_data(raw,fields,self.formats,scaling=False)167        payload=((int.from_bytes(base.data,'big')&~self.mask)|(encoded&self.mask)).to_bytes(self.message.length,'big')168        decoded=decode_data(payload,self.message.length,fields,self.formats,False,True,False,False)169        facts=[]170        for name in selected:171            signal=self.signals[name]; actual=decoded[name]172            if (not isfinite(actual) or signal.minimum is not None and actual<signal.minimum173                    or signal.maximum is not None and actual>signal.maximum):174                raise ValueError('重建量化后物理值越界: '+name)175            if trace:176                target=Fraction(str(values[name])) if exact_values is None else exact_values[name]177                facts.append((name,float(target),raw[name],actual,float(Fraction(str(actual))-target)))178        return replace(base,data=payload),tuple(facts)179180    def close(self):181        """释放本格式缓存,不关闭借用DBC。参数: 无。返回: None。异常: 费用释放错可重试。182        """183        if self._closed:184            return185        self.signals.clear(); self.formats=None; self.message=None; self.active=()186        _release_owned_ram(self,self._fixed_cost,'_fixed_cost'); self._closed=True187
Chronaxis 用户手册 · 离线阅读