返回教程正文

配套源码

bus.py

app/bricks/generic_can/bus.py
generic_can Brick:在 VENTUNO Q 上自定义 CAN 报文app/bricks/generic_can/bus.py
Python596 行
# SPDX-License-Identifier: MIT
"""Thread-safe public CanBus with one receiver and bounded dispatch queues."""

from collections.abc import Mapping
import queue
import threading
import time

from .backends import CanBackend, SocketCANBackend
from .compat import brick
from .config import (
    validate_device,
    validate_queue_size,
    validate_timeout,
)
from .definition import MessageDefinition
from .errors import (
    CANBackendError,
    CANConfigurationError,
    CANError,
    CANMessageError,
    CANTimeoutError,
)
from .frame import CanFrame


_NO_FRAME = object()


@brick
class CanBus:
    """Own one backend, one receiver thread, and independent receive queues."""

    def __init__(
        self,
        interface="can0",
        messages=None,
        backend=None,
        raw_queue_size=256,
        message_queue_size=64,
        receiver_poll_s=0.05,
    ):
        """
        @description         : 创建总线对象、命名报文路由和有界接收队列
        @param self          : 当前CanBus对象
        @param interface     : Linux SocketCAN接口名称,默认can0
        @param messages      : 名称到MessageDefinition的映射
        @param backend       : 可选的自定义CanBackend,测试时可传FakeBackend
        @param raw_queue_size: 原始报文队列最大帧数
        @param message_queue_size: 每个命名报文队列最大帧数
        @param receiver_poll_s: 接收线程单次等待秒数
        @return              : 无
        """
        self.interface = validate_device(interface)
        self.raw_queue_size = validate_queue_size(
            raw_queue_size,
            "raw_queue_size",
        )
        self.message_queue_size = validate_queue_size(
            message_queue_size,
            "message_queue_size",
        )
        self.receiver_poll_s = validate_timeout(receiver_poll_s)
        if self.receiver_poll_s <= 0.0:
            raise CANConfigurationError(
                "receiver_poll_s must be greater than zero"
            )

        if backend is None:
            backend = SocketCANBackend(device=self.interface)
        if not isinstance(backend, CanBackend):
            raise CANConfigurationError(
                "backend must implement the CanBackend contract"
            )
        if (
            isinstance(backend, SocketCANBackend)
            and backend.device != self.interface
        ):
            raise CANConfigurationError(
                "SocketCAN backend device does not match CanBus interface"
            )
        self._backend = backend
        self._messages = self._validate_messages(messages)

        self._raw_frames = queue.Queue(maxsize=self.raw_queue_size)
        self._message_queues = {
            name: queue.Queue(maxsize=self.message_queue_size)
            for name, definition in self._messages.items()
            if definition.allows_rx
        }
        self._routes = {}
        for name, definition in self._messages.items():
            if definition.allows_rx:
                self._routes.setdefault(definition.match_key(), []).append(name)

        self._lifecycle_lock = threading.RLock()
        self._send_lock = threading.Lock()
        self._stop_event = threading.Event()
        self._receiver_thread = None
        self._receiver_error = None
        self._is_open = False

        self.dropped_raw_frames = 0
        self.dropped_message_frames = 0
        self.dropped_message_frames_by_name = {
            name: 0 for name in self._message_queues
        }
        self.received_frames = 0
        self.sent_frames = 0

    @staticmethod
    def _message_name(name):
        """
        @description         : 校验并规范化命名报文名称
        @param name          : 用户提供的报文名称
        @return              : 去除首尾空白后的名称
        """
        if not isinstance(name, str):
            raise CANConfigurationError("message name must be a string")
        normalized = name.strip()
        if not normalized:
            raise CANConfigurationError("message name must not be empty")
        return normalized

    @classmethod
    def _validate_messages(cls, messages):
        """
        @description         : 校验整张命名报文定义表
        @param cls           : CanBus类
        @param messages      : 名称到MessageDefinition的映射或None
        @return              : 规范化后的普通字典
        """
        if messages is None:
            return {}
        if not isinstance(messages, Mapping):
            raise CANConfigurationError(
                "messages must be a mapping of names to MessageDefinition"
            )

        normalized = {}
        for raw_name, definition in messages.items():
            name = cls._message_name(raw_name)
            if name in normalized:
                raise CANConfigurationError(
                    f"duplicate normalized message name '{name}'"
                )
            if not isinstance(definition, MessageDefinition):
                raise CANConfigurationError(
                    f"message '{name}' must be a MessageDefinition"
                )
            normalized[name] = definition
        return normalized

    @property
    def is_open(self):
        """
        @description         : 判断当前CanBus是否已经打开后端
        @param self          : 当前CanBus对象
        @return              : 已打开时返回True,否则返回False
        """
        return self._is_open

    def _clear_queue(self, target_queue):
        """
        @description         : 非阻塞清空一个内部接收队列
        @param self          : 当前CanBus对象
        @param target_queue  : 需要清空的queue.Queue
        @return              : 无
        """
        while True:
            try:
                target_queue.get_nowait()
            except queue.Empty:
                return

    def _clear_receive_queues(self):
        """
        @description         : 打开总线前清空原始队列和所有命名队列
        @param self          : 当前CanBus对象
        @return              : 无
        """
        self._clear_queue(self._raw_frames)
        for target_queue in self._message_queues.values():
            self._clear_queue(target_queue)

    @staticmethod
    def _put_latest(target_queue, frame):
        """
        @description         : 将新帧放入有界队列,满时丢弃最旧帧
        @param target_queue  : 接收新帧的queue.Queue
        @param frame         : 需要入队的CanFrame
        @return              : 本次是否丢弃过一帧旧数据
        """
        try:
            target_queue.put_nowait(frame)
            return False
        except queue.Full:
            pass

        dropped = False
        try:
            target_queue.get_nowait()
            dropped = True
        except queue.Empty:
            pass

        try:
            target_queue.put_nowait(frame)
        except queue.Full:
            # A competing consumer/producer changed the queue between calls.
            # The single receiver is the only producer in V1, so this branch
            # is defensive and never blocks the receiver.
            return dropped
        return dropped

    def _run_receiver(self):
        """
        @description         : 在唯一接收线程内读取后端并分发到各个队列
        @param self          : 当前CanBus对象
        @return              : 无;关闭或发生后端错误时退出
        """
        while not self._stop_event.is_set():
            try:
                frame = self._backend.receive(self.receiver_poll_s)
            except CANError as error:
                self._receiver_error = error
                self._stop_event.set()
                return
            except Exception as error:
                self._receiver_error = CANBackendError(
                    f"receiver thread failed: {error}"
                )
                self._stop_event.set()
                return

            if frame is None:
                continue
            if not isinstance(frame, CanFrame):
                self._receiver_error = CANBackendError(
                    "backend receive returned a non-CanFrame value"
                )
                self._stop_event.set()
                return

            self.received_frames += 1
            if self._put_latest(self._raw_frames, frame):
                self.dropped_raw_frames += 1

            for name in self._routes.get(
                (frame.arbitration_id, frame.is_extended, frame.is_fd),
                (),
            ):
                if self._put_latest(self._message_queues[name], frame):
                    self.dropped_message_frames += 1
                    self.dropped_message_frames_by_name[name] += 1

    def _raise_receiver_error(self):
        """
        @description         : 在调用线程中重新抛出接收线程保存的错误
        @param self          : 当前CanBus对象
        @return              : 无;存在错误时抛出统一CAN异常
        """
        if self._receiver_error is not None:
            error = self._receiver_error
            if isinstance(error, CANError):
                raise error
            raise CANBackendError(str(error)) from error

    def _ensure_open(self):
        """
        @description         : 确认总线已打开
        @param self          : 当前CanBus对象
        @return              : 无;未打开时抛出CANBackendError
        """
        if not self._is_open:
            raise CANBackendError("CAN bus is not open")

    def open(self):
        """
        @description         : 打开后端并启动唯一的接收线程
        @param self          : 当前CanBus对象
        @return              : 当前CanBus,便于with语句和链式使用
        """
        with self._lifecycle_lock:
            if self._is_open:
                return self

            self._clear_receive_queues()
            self._receiver_error = None
            self._stop_event.clear()
            try:
                self._backend.open()
                self._is_open = True
                self._receiver_thread = threading.Thread(
                    target=self._run_receiver,
                    name=f"generic-can-rx-{self.interface}",
                    daemon=True,
                )
                self._receiver_thread.start()
            except CANError:
                self._is_open = False
                self._receiver_thread = None
                try:
                    self._backend.close()
                except Exception:
                    pass
                raise
            except Exception as error:
                self._is_open = False
                self._receiver_thread = None
                try:
                    self._backend.close()
                except Exception:
                    pass
                raise CANBackendError(
                    f"failed to open CAN bus '{self.interface}': {error}"
                ) from error
            return self

    def close(self):
        """
        @description         : 停止接收线程并关闭后端,重复调用安全
        @param self          : 当前CanBus对象
        @return              : 无
        """
        with self._lifecycle_lock:
            if not self._is_open:
                return

            self._is_open = False
            self._stop_event.set()
            receiver_thread = self._receiver_thread
            self._receiver_thread = None

            if receiver_thread is not None:
                receiver_thread.join(
                    timeout=max(1.0, self.receiver_poll_s * 4.0)
                )

            close_error = None
            with self._send_lock:
                try:
                    self._backend.close()
                except CANError as error:
                    close_error = error
                except Exception as error:
                    close_error = CANBackendError(
                        f"failed to close CAN bus '{self.interface}': {error}"
                    )

            if receiver_thread is not None and receiver_thread.is_alive():
                receiver_thread.join(timeout=0.25)
                if receiver_thread.is_alive() and close_error is None:
                    close_error = CANBackendError(
                        "receiver thread did not stop"
                    )

            if close_error is not None:
                raise close_error

    def __enter__(self):
        """
        @description         : 进入with代码块时打开总线
        @param self          : 当前CanBus对象
        @return              : 已打开的当前CanBus
        """
        return self.open()

    def __exit__(self, exc_type, exc_value, traceback):
        """
        @description         : 离开with代码块时关闭总线且不吞掉原异常
        @param self          : 当前CanBus对象
        @param exc_type      : with代码块异常类型或None
        @param exc_value     : with代码块异常对象或None
        @param traceback     : with代码块异常调用栈或None
        @return              : False,表示原异常需要继续传播
        """
        self.close()
        return False

    def send_frame(self, frame):
        """
        @description         : 发送一帧已经构造并校验过的CanFrame
        @param self          : 当前CanBus对象
        @param frame         : 需要发送的CanFrame
        @return              : 成功发送的同一个CanFrame
        """
        if not isinstance(frame, CanFrame):
            raise CANConfigurationError(
                "send_frame requires a CanFrame object"
            )
        self._ensure_open()
        self._raise_receiver_error()

        with self._send_lock:
            self._ensure_open()
            try:
                self._backend.send(frame)
            except CANError:
                raise
            except Exception as error:
                raise CANBackendError(
                    f"backend failed to send frame: {error}"
                ) from error
            self.sent_frames += 1
        return frame

    def _queue_receive(self, target_queue, timeout_s):
        """
        @description         : 从指定内部队列等待一帧并同步检查接收线程错误
        @param self          : 当前CanBus对象
        @param target_queue  : 需要读取的queue.Queue
        @param timeout_s     : 最长等待秒数
        @return              : CanFrame或内部无帧标记
        """
        timeout = validate_timeout(timeout_s)
        self._ensure_open()

        if timeout == 0.0:
            try:
                return target_queue.get_nowait()
            except queue.Empty:
                self._raise_receiver_error()
                return _NO_FRAME

        deadline = time.monotonic() + timeout
        while True:
            self._ensure_open()
            self._raise_receiver_error()
            remaining = deadline - time.monotonic()
            if remaining <= 0.0:
                return _NO_FRAME
            try:
                return target_queue.get(
                    timeout=min(remaining, self.receiver_poll_s)
                )
            except queue.Empty:
                continue

    def receive_frame(self, timeout_s=1.0):
        """
        @description         : 从原始队列读取下一帧CAN数据
        @param self          : 当前CanBus对象
        @param timeout_s     : 最长等待秒数,零表示立即返回
        @return              : 收到时返回CanFrame,超时返回None
        """
        frame = self._queue_receive(self._raw_frames, timeout_s)
        return None if frame is _NO_FRAME else frame

    def _definition(self, name):
        """
        @description         : 按名称查找一条已经校验的报文定义
        @param self          : 当前CanBus对象
        @param name          : 报文名称
        @return              : 规范化名称和MessageDefinition组成的元组
        """
        normalized = self._message_name(name)
        try:
            return normalized, self._messages[normalized]
        except KeyError as error:
            raise CANMessageError(
                f"unknown message '{normalized}'"
            ) from error

    def send(self, name, **values):
        """
        @description         : 使用固定DATA或encode函数构造并发送命名报文
        @param self          : 当前CanBus对象
        @param name          : 需要发送的命名报文名称
        @param values        : 传给该报文encode函数的关键字参数
        @return              : 成功发送的CanFrame
        """
        normalized_name, definition = self._definition(name)
        if not definition.allows_tx:
            raise CANMessageError(
                f"message '{normalized_name}' does not allow tx"
            )

        try:
            if definition.fixed_data is not None:
                if values:
                    raise CANMessageError(
                        f"fixed message '{normalized_name}' takes no values"
                    )
                data = definition.fixed_data
            else:
                data = definition.encode(**values)

            frame = CanFrame(
                arbitration_id=definition.arbitration_id,
                data=data,
                is_extended=definition.is_extended,
                is_fd=definition.is_fd,
                bitrate_switch=definition.bitrate_switch,
            )
        except CANMessageError:
            raise
        except Exception as error:
            raise CANMessageError(
                f"failed to encode message '{normalized_name}': {error}"
            ) from error

        return self.send_frame(frame)

    def receive(self, name, timeout_s=1.0):
        """
        @description         : 等待命名报文并按定义解码
        @param self          : 当前CanBus对象
        @param name          : 需要接收的命名报文名称
        @param timeout_s     : 最长等待秒数
        @return              : 有decode时返回工程值,否则返回CanFrame
        """
        normalized_name, definition = self._definition(name)
        if not definition.allows_rx:
            raise CANMessageError(
                f"message '{normalized_name}' does not allow rx"
            )

        frame = self._queue_receive(
            self._message_queues[normalized_name],
            timeout_s,
        )
        if frame is _NO_FRAME:
            raise CANTimeoutError(
                f"timed out waiting for message '{normalized_name}'"
            )
        if definition.decode is None:
            return frame

        try:
            return definition.decode(frame.data)
        except Exception as error:
            raise CANMessageError(
                f"failed to decode message '{normalized_name}': {error}"
            ) from error

    def describe(self):
        """
        @description         : 导出可JSON序列化的配置、状态和队列统计
        @param self          : 当前CanBus对象
        @return              : 诊断信息字典
        """
        messages = {}
        for name, definition in self._messages.items():
            messages[name] = {
                "arbitration_id": definition.arbitration_id,
                "arbitration_id_hex": f"0x{definition.arbitration_id:X}",
                "direction": definition.direction,
                "is_extended": definition.is_extended,
                "is_fd": definition.is_fd,
                "bitrate_switch": definition.bitrate_switch,
                "payload_source": (
                    "fixed_data"
                    if definition.fixed_data is not None
                    else (
                        "encode" if definition.encode is not None else None
                    )
                ),
                "has_decode": definition.decode is not None,
            }

        receiver_thread = self._receiver_thread
        return {
            "interface": self.interface,
            "is_open": self._is_open,
            "receiver_alive": bool(
                receiver_thread is not None
                and receiver_thread.is_alive()
            ),
            "receiver_error": (
                None
                if self._receiver_error is None
                else str(self._receiver_error)
            ),
            "raw_queue_size": self.raw_queue_size,
            "message_queue_size": self.message_queue_size,
            "raw_queue_depth": self._raw_frames.qsize(),
            "message_queue_depths": {
                name: target_queue.qsize()
                for name, target_queue in self._message_queues.items()
            },
            "sent_frames": self.sent_frames,
            "received_frames": self.received_frames,
            "dropped_raw_frames": self.dropped_raw_frames,
            "dropped_message_frames": self.dropped_message_frames,
            "dropped_message_frames_by_name": dict(
                self.dropped_message_frames_by_name
            ),
            "ignored_error_frames": int(
                getattr(self._backend, "ignored_error_frames", 0)
            ),
            "ignored_remote_frames": int(
                getattr(self._backend, "ignored_remote_frames", 0)
            ),
            "messages": messages,
        }