Python实现车载CAN通信(1):CAN报文封装

目录

1. 第三方库:can

1.1 can库安装

1.2 Message类

2. 自定义CANMessage类

3 总结


        在纷繁复杂的车载网络协议中(如用于车身控制的LIN、用于多媒体传输的MOST、用于高速自动驾驶的以太网),CAN总线始终扮演着最基础、最核心的角色。在车载通信测试中,CAN通信测试尤为重要。

        CAN报文通常包含标识符(ID)、数据长度(DLC)、数据字段(Data)、收发方向(RX/TX),报文类型(CAN经典/CANFD)等核心属性。本文提供了两种CAN报文类的实现方法,用于封装CAN报文的相关操作。

1. 第三方库:can

1.1 can库安装

pip install can

1.2 Message类

        安装完can库之后,可以在can/message.py中找到Message类的定义,部分代码如下:

# from can/message.py
class Message:  # pylint: disable=too-many-instance-attributes; OK for a dataclass
    """
    The :class:`~can.Message` object is used to represent CAN messages for
    sending, receiving and other purposes like converting between different
    logging formats.

    Messages can use extended identifiers, be remote or error frames, contain
    data and may be associated to a channel.

    Messages are always compared by identity and never by value, because that
    may introduce unexpected behaviour. See also :meth:`~can.Message.equals`.

    :func:`~copy.copy`/:func:`~copy.deepcopy` is supported as well.

    Messages do not support "dynamic" attributes, meaning any others than the
    documented ones, since it uses :obj:`~object.__slots__`.
    """

    __slots__ = (
        "timestamp",
        "arbitration_id",
        "is_extended_id",
        "is_remote_frame",
        "is_error_frame",
        "channel",
        "dlc",
        "data",
        "is_fd",
        "is_rx",
        "bitrate_switch",
        "error_state_indicator",
        "__weakref__",  # support weak references to messages
    )

    def __init__(  # pylint: disable=too-many-locals, too-many-arguments
        self,
        timestamp: float = 0.0,
        arbitration_id: int = 0,
        is_extended_id: bool = True,
        is_remote_frame: bool = False,
        is_error_frame: bool = False,
        channel: Optional[typechecking.Channel] = None,
        dlc: Optional[int] = None,
        data: Optional[typechecking.CanData] = None,
        is_fd: bool = False,
        is_rx: bool = True,
        bitrate_switch: bool = False,
        error_state_indicator: bool = False,
        check: bool = False,
    ):
        """
        To create a message object, simply provide any of the below attributes
        together with additional parameters as keyword arguments to the constructor.

        :param check: By default, the constructor of this class does not strictly check the input.
                      Thus, the caller must prevent the creation of invalid messages or
                      set this parameter to `True`, to raise an Error on invalid inputs.
                      Possible problems include the `dlc` field not matching the length of `data`
                      or creating a message with both `is_remote_frame` and `is_error_frame` set
                      to `True`.

        :raises ValueError:
            If and only if `check` is set to `True` and one or more arguments were invalid
        """
        self.timestamp = timestamp
        self.arbitration_id = arbitration_id
        self.is_extended_id = is_extended_id
        self.is_remote_frame = is_remote_frame
        self.is_error_frame = is_error_frame
        self.channel = channel
        self.is_fd = is_fd
        self.is_rx = is_rx
        self.bitrate_switch = bitrate_switch
        self.error_state_indicator = error_state_indicator

        if data is None or is_remote_frame:
            self.data = bytearray()
        elif isinstance(data, bytearray):
            self.data = data
        else:
            try:
                self.data = bytearray(data)
            except TypeError as error:
                err = f"Couldn't create message from {data} ({type(data)})"
                raise TypeError(err) from error

        if dlc is None:
            self.dlc = len(self.data)
        else:
            self.dlc = dlc

        if check:
            self._check()

类的成员属性具体含义可参考下个章节——《自定义CANMessage类》

2. 自定义CANMessage类

        参考can库中的Message类,对其进行了简化设计,自定义了CANMessage类。

代码如下:

import time
from typing import Union, Optional
import dataclasses


__all__ = ["CANMessage"]


class ProtocolError(ValueError):
    """CAN 协议规范违反异常"""

    def __init__(self, message="", *, code: int = 0xEF):
        super().__init__(f"[CAN_ERROR:{code:#x}] {message}")
        self.error_code = code


@dataclasses.dataclass(frozen=False, slots=True)
class CANMessage:
    """符合 ISO 11898 标准的 CAN Message

    实现 CAN 2.0B 和 CAN FD 规范的核心数据结构,支持:
    - 自动协议合规性检查
    - 数据格式智能转换
    - 动态字段校正
    - 协议参数组(PGN)解析

    Attributes:
        arbitration_id (int): 11/29位报文标识符,范围:
            - 标准帧:[0x000, 0x7FF]
            - 扩展帧:[0x00000000, 0x1FFFFFFF]
        data (Optional[Union[bytes, bytearray, List[int]]]): 有效载荷数据,规则:
            - 远程帧:必须为空
            - 错误帧:≤8字节
            - CAN FD:最长64字节
        channel (int): 物理通道编号,取值范围 [0, 63],对应Vector Hardware Config中的配置
        period (int): 周期发送间隔(毫秒 ms),0表示单次传输
        timestamp (float): 纳秒级时间戳(单调时钟)
        is_extended_id (bool): 扩展帧标识
        is_remote_frame (bool): 远程传输请求
        is_error_frame (bool): 错误帧标识
        is_fd (bool): CAN FD格式标识
        is_rx (bool): 接收方向标识
        bitrate_switch (bool): CAN FD比特率切换(仅发送有效)
        error_state_indicator (bool): CAN FD错误状态指示

    Raises:
        ProtocolError: 违反CAN协议规范时抛出,包含错误代码

    Examples:
    >>> msg = CANMessage(0x123, data=[0xFF] * 8)
    """

    arbitration_id: int
    data: Optional[Union[bytes, bytearray, list[int]]] = None
    channel: int = dataclasses.field(
        default=0,
        metadata={"min": 0, "max": 63,
                  "description": "物理通道编号 (0=默认通道)"}
    )
    period: int = dataclasses.field(
        default=0,
        metadata={"min": 0,
                  "description": "周期发送间隔 (0=单次传输), uint: ms"}
    )
    timestamp: float = dataclasses.field(
        default_factory=time.monotonic,
        repr=False,
        metadata={"unit": "seconds",
                  "precision": "nanoseconds"}
    )
    is_extended_id: bool = dataclasses.field(
        default=False,
    )
    is_remote_frame: bool = dataclasses.field(
        default=False,
    )
    is_error_frame: bool = dataclasses.field(
        default=False,
    )
    is_fd: bool = dataclasses.field(
        default=False,
        metadata={"version": "ISO 11898-7"}
    )
    is_rx: bool = dataclasses.field(
        default=True,
        metadata={"description": "True=接收帧, False=发送帧"}
    )
    bitrate_switch: bool = dataclasses.field(
        default=False,
        repr=False,
        metadata={"condition": "is_fd=True"}
    )
    error_state_indicator: bool = dataclasses.field(
        default=False,
        repr=False,
        metadata={"condition": "is_fd=True"}
    )

    def __post_init__(self):
        """后初始化处理流程"""
        self._convert_data_format()
        self._validate_protocol_compliance()
        self._auto_correct_fields()

    def _convert_data_format(self):
        """统一数据格式为 bytes"""
        if self.data is None:
            self.data = bytes()
            return

        if isinstance(self.data, (list, bytearray)):
            try:
                self.data = bytes(self.data)
            except ValueError as e:
                raise ProtocolError(f"Invalid data bytes: {str(e)}") from None
        elif not isinstance(self.data, bytes):
            raise TypeError("Data payload must be bytes/bytearray/list[int]")

    def _validate_protocol_compliance(self):
        """协议合规性验证"""
        self._validate_id_range()
        self._validate_frame_type_conflict()
        self._validate_fd_features()
        self._validate_remote_frame()
        self._validate_error_frame()
        # self._validate_bitrate_switch()

    def _validate_id_range(self):
        """ID 范围校验"""
        max_id = 0x1FFFFFFF if self.is_extended_id else 0x7FF
        if not 0 <= self.arbitration_id <= max_id:
            raise ProtocolError(f"ID out of range ({max_id:#x})")

    def _validate_frame_type_conflict(self):
        """帧类型冲突检查"""
        if sum([self.is_remote_frame, self.is_error_frame, self.bitrate_switch]) > 1:
            raise ProtocolError("Frame type flags conflict")

    def _validate_fd_features(self):
        """CAN FD 特性验证"""
        if not self.is_fd and (self.bitrate_switch or self.error_state_indicator):
            raise ProtocolError("CAN FD features require is_fd=True")

    def _validate_remote_frame(self):
        """远程帧规范检查"""
        if self.is_remote_frame:
            if self.is_fd:
                raise ProtocolError("CAN FD prohibits remote frames")
            if len(self.data) > 0:
                raise ProtocolError("Remote frame must have empty payload")

    def _validate_error_frame(self):
        """错误帧规范检查"""
        if self.is_error_frame and len(self.data) > 8:
            raise ProtocolError("Error frame payload exceeds 8 bytes")

    def _validate_bitrate_switch(self):
        """比特率切换条件检查"""
        if self.bitrate_switch and len(self.data) <= 8:
            raise ProtocolError("BRS requires payload >8 bytes")

    def _auto_correct_fields(self):
        """字段自动校正"""
        if self.is_error_frame and not self.is_rx:
            self.is_rx = True  # 错误帧只能为接收帧

    @property
    def dlc(self) -> int:
        """获取数据长度码"""
        length = len(self.data)
        return length if length <= 8 else 8 + (length - 8) // 4

    @property
    def is_standard_frame(self) -> bool:
        """是否为标准帧"""
        return not any([self.is_remote_frame, self.is_error_frame, self.is_fd])

    def to_pgn(self) -> int:
        """解析参数组编号 (PGN)"""
        if self.is_extended_id:
            return (self.arbitration_id >> 8) & 0x1FF00
        return 0

3 总结

        本文提供了两种CAN Message类的设计方法,其实内容上大同小异,通过对CAN报文进行类的封装设计,后续CAN通信的自动化测试中,可以更方便地处理CAN通信测试中的报文操作,提高测试效率和代码可维护性。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

python自动化码农

感谢打赏,您的鼓励是我创作动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值