完整 App、Brick、脚本和测试代码都可从左侧“配套源码”展开阅读。离线 .whl 文件显示为 无法预览的二进制文件,仍可下载原文件。

这个 App 用来学习 VENTUNO Q 板载 CAN 螺钉座:先认识 CAN ID 和 DATA,再通过 Python 发送、接收一帧数据,最后把自己的 CAN 报文规则写进 python/can_messages.py。

Brick 的导入名仍为 generic_can,App 名称仍为 Generic CAN0 Lab,便于沿用现有目录。 正式使用时直接创建 CanBus(),固定访问 can0。

1. 认识 VENTUNO Q CAN 螺钉座

螺钉座是开发板连接外部 CAN 设备的物理接口。在本项目使用的目标系统中,这条板载链路 被提供为 Linux SocketCAN 接口 can0:

VENTUNO Q CAN 螺钉座
        ↓
板载 CAN PHY / FDCAN1
        ↓
系统 CANnectivity / gs_usb
        ↓
Linux SocketCAN can0
        ↓
CanBus()

CAN PHY 负责总线电信号的收发,FDCAN1 是 CAN 控制器;系统链路让 Linux 程序能够通过 can0 访问它们。can0 是系统中的接口名,不是电机地址,也不是 CAN ID。

上述映射是使用本项目的前提,Brick 不负责识别物理设备。具体接线和电气要求以 VENTUNO Q 官方硬件手册为准。

2. 先认识 CAN ID 和 DATA

一帧 CAN 数据可以先看成两部分:

CAN ID = 0x123
DATA   = 01 02 03
名称 初学时需要理解的含义
CAN ID 报文标识符;具体含义由设备协议约定,并不天然等于设备地址
DATA 报文携带的字节;每个字节如何解释由发送端和接收端约定
Standard CAN 标准格式,使用 11 位 ID
Extended CAN 扩展格式,使用 29 位 ID
Classical CAN 经典 CAN,一帧最多携带 8 字节 DATA
CAN FD 可变数据速率 CAN,一帧最多携带 64 字节 DATA
BRS CAN FD 的位速率切换标志;启用后在数据阶段使用配置的数据位速率

标准/扩展格式决定 ID 有多少位,Classical CAN/CAN FD 决定 帧格式和数据容量, 两者是不同维度。SocketCAN 如何表示这些字段,可参考 Linux 内核 SocketCAN 文档。

本文所有示例 ID、状态位和数值换算都属于虚构教学规则,不对应任何真实电机或传感器。 连接真实设备后,需要按设备手册替换。

3. 先启动 App,再准备 can0

本文选择启动配套 App 来准备运行环境,这是教程的操作流程,不是说 can0 必须绑定这个 App。 如果已有其他合适的 Sketch 使板载 USB CAN 链路正常工作,使用系统 can0 不必额外启动 Generic CAN0 Lab。 具体条件和验证范围见第 3.2 节;配套 Python 示例仍需准备好它自己的依赖。

以下命令都在开发板的 Linux 终端执行,例如通过 SSH 登录后的终端,不是在电脑的 PowerShell 中执行。

3.1 用 Arduino App CLI 启动完整 App

将配套源码中的完整 app/ 目录放到开发板,并命名为:

/home/arduino/ArduinoApps/generic-can0-lab/

这个目录下应直接包含 app.yaml、python/、sketch/、bricks/ 和 scripts/, 不能只复制 Brick。首次启动需要准备 App 运行环境;随附的离线包只覆盖本 Brick 的 Python 依赖, 不代表新板子所需的系统组件、容器镜像和编译工具都已离线备齐。

使用普通的 arduino 用户,先查看版本和 App 状态:

cd /home/arduino/ArduinoApps/generic-can0-lab
arduino-app-cli version
arduino-app-cli app list

确认没有其他 App 正在运行,且此 App 尚未启动时,执行:

arduino-app-cli app start .

. 表示当前 App 根目录,也就是包含 app.yaml 的目录,不是 python/。 也可以用完整路径:arduino-app-cli app start /home/arduino/ArduinoApps/generic-can0-lab。 不要在这些 App CLI 命令前加 sudo。App 的目录约定和运行用户要求见 Arduino App CLI 官方使用说明。

对于本项目,启动完整 App 会处理 Sketch 的编译、上传及运行,并准备和启动 Linux 端的 python/main.py。它不是只执行一个 Python 文件。当前官方实现若发现已有 App 运行,会拒绝再次启动; 本 App 已运行时直接进入后面的检查步骤,其他 App 正在运行时,先确认可以停止它。 启动流程可查阅 官方 App 编排实现。

在 Arduino App Lab 中选择 Generic CAN0 Lab 并点击运行,也可以启动完整 App; 不必再同时执行 CLI 的 app start。

启动完成后查看状态和 Python 日志:

arduino-app-cli app list
arduino-app-cli app logs . --tail 100

想持续看新日志,执行 arduino-app-cli app logs . --follow。按 Ctrl+C 只退出日志查看, 不会停止 App。这里查看的是 App 的 Python 日志,不是后面 SSH 中手动运行的示例输出。 参数含义见 官方日志命令实现。

3.2 App 没启动,can0 到底能不能用

不必启动 generic-can0-lab 这个特定 App。关键是 MCU 是否已进入提供 USB CAN 功能的运行阶段, 以及 Linux 中的 can0 是否已正确配置并启用。“某个 App 没运行”和“MCU 没运行”不是一回事。

情况 板载 can0 能否使用
Generic CAN0 Lab 没启动,但其他 App 的 Sketch 已使板载 USB CAN 链路正常工作 可以;can0 不绑定 Generic CAN0 Lab
开机后所有 App 都没启动,且 MCU 停在 Wait for App(等待 App)阶段 按当前启动逻辑,不能正常使用这条板载 CAN 链路
MCU 配置为独立启动,且已正常提供 USB CAN 功能 可以不依赖 Linux App 启动;本教程尚未切换到这种模式验证
通过 App CLI 停止正在运行的 App 不能保证继续可用;停止操作可能复位 MCU,“全部停止后”的对照测试尚未完成

已确认的实机条件是:generic-can0-lab 为 uninitialized,zdt-motor-demo 为 running, 同时 can0 为 UP、500 kbit/s,收发计数持续增长。这证明了不必启动特定的 CAN 教学 App, 但不能证明“所有 App 都停止后仍然可用”,也不等于已完成本版 generic_can Brick 的硬件验收。

运行中的 zdt-motor-demo 的 .cache/sketch/build.options.json 记录了以下编译目标:

arduino:zephyr:ventunoq:wait_linux_boot=app

板上 Core 将 wait_linux_boot=app 定义为 Wait for App。据此结合 官方 loader 启动逻辑判断: 若开机后没有 App 放行 Sketch,MCU 会停在等待阶段,而不是自动进入 Sketch。 这是基于实际配置与源码的判断,尚未进行“冷启动后不启动任何 App”或“停止全部 App”的对照实测。 更换启动模式、Core 或固件后,需要重新确认;不要把此配置当成所有 VENTUNO Q 都不变的硬件限制。

仅关闭 App Lab 窗口或退出日志查看,不等于停止开发板上运行的 App;应通过 arduino-app-cli app list 确认状态。也不要只凭 Linux 开机、Python 进程运行或磁盘上存在 sketch.ino 判断 MCU 已运行。

这里要区分两件事:

  • MCU 系统固件在初始化时注册 CANnectivity,将 FDCAN 控制器接入 USB CAN 服务。 对应实现见 官方 loader/fixups.c。
  • Arduino Core 进入 Sketch 启动流程时,在用户的 setup() 之前调用 SerialUSB.begin(), 进而启用 USB 设备;Linux 才能枚举相应 USB 功能,并由 gs_usb 提供 SocketCAN 接口。 对应实现见 Core main.cpp 和 USB.cpp。

所以,本 App 保留最小 Sketch,让 MCU 正常进入并保持运行即可;CAN 报文由系统 CANnectivity 路径转发, 不需要自己在 loop() 中收发 CAN,也不要再用 CAN.begin() 初始化同一个 FDCAN1。 Bridge.begin() 初始化的是 RouterBridge,不是直接创建 Linux 的 can0; 本 Brick 的报文也不经过 Bridge.call()。

MCU 复位或刷写时,USB CAN 接口可能暂时消失。即使 App 显示“运行中”,也还要按下一节检查接口状态。

3.3 在宿主 Linux 配置并启用 can0

确认 MCU 已运行、板载 USB CAN 链路已就绪后,再执行:

ip -details -statistics link show can0

如果找不到 can0,先检查 App 的启动输出、Sketch 是否成功运行,以及 MCU 到 Linux 的 USB 链路; 此时重复执行 ip link set can0 up 不能创建缺失的设备。Brick 也不会创建接口、 设置位速率或执行 up/down。

以下是 500 kbit/s Classical CAN 的配置示例,只有设备要求这个位速率时才使用。 配置前先停下正在使用总线的收发脚本,down 会中断当前通信:

sudo ip link set can0 down
sudo ip link set can0 type can bitrate 500000
sudo ip link set can0 up
ip -details -statistics link show can0

UP 表示接口已启用;ERROR-ACTIVE 是 CAN 控制器的一种正常运行状态,不表示发生了 名为“ACTIVE”的错误。这些状态本身不能证明另一台设备已经收到或回复报文。

自动 Bus-Off 恢复需要驱动支持,不要把 restart-ms 100 当作必填参数;这里不添加它。 如果 up 报 Broken pipe,应检查 MCU/USB 链路和内核日志, 而不是继续运行收发脚本。只读诊断可用 sudo dmesg | tail -n 50。

设备重启、App 重启或 Sketch 重新上传后,都应重新检查 can0; 如果接口被重新创建,可能需要重新配置和启用,再重新打开收发程序。

3.4 区分 App 容器和宿主 Linux

本 App 的 python/main.py 默认只尝试监听,不会自动发送示例报文。暂时打不开接口时, 它会打印 can0 unavailable 并间隔约 10 秒重试;无需为了启用 can0 再去重启 App。

App CLI 管理的 Python 程序运行在容器中;SSH 终端操作的是开发板宿主 Linux。 宿主中存在 can0,不代表 App 容器中也能看到它,这取决于容器的网络配置。 如果宿主 can0 已正常启用,而 App 仍报告找不到接口,后面的教程示例可用 scripts/run_host_python.sh 在宿主运行。该脚本不会改变容器网络配置,也不代表 App 容器已成功访问 CAN。

4. 先用 Linux 工具观察 CAN

开发板安装 can-utils 后,可以直接监听:

candump can0

如果总线上收到示例数据,可能看到:

can0  123   [3]  01 02 03

这里 123 是十六进制 CAN ID,[3] 表示 3 个数据字节,后面是 DATA。 没有输出表示当前没有观察到报文,不一定是接口故障。按 Ctrl+C 结束监听。

candump 用于观察报文;cansend 会主动发送报文。工具说明见 can-utils 官方仓库。

5. 用 CanFrame 表示一帧数据

from generic_can import CanFrame

frame = CanFrame(
    arbitration_id=0x123,
    data=b"\x01\x02\x03",
)
Python 字段 对应的 CAN 内容
arbitration_id=0x123 CAN ID 为 0x123
data=b"\x01\x02\x03" DATA 为 01 02 03

b"\x01\x02\x03" 是三个字节,不是显示字符“01 02 03”。构造 CanFrame 只是在内存中 表示并检查数据,不会发送报文。

默认采用标准 ID、Classical CAN,且不启用 BRS。非法 ID、DATA 或帧标志会在构造时被拒绝。

6. 发送原始 CAN 帧

以下示例调用 send_frame() 后会主动向真实总线发送数据。运行前必须按所接设备的协议 选择安全的 ID 和 DATA;不要把这里的占位字节直接当成控制命令。

from generic_can import CanBus, CanFrame

with CanBus() as bus:
    frame = CanFrame(
        arbitration_id=0x123,
        data=b"\x01\x02\x03",
    )
    bus.send_frame(frame)

CanBus() 固定使用板载链路对应的 can0。with 会打开总线,并在退出代码块时释放 资源,包括代码块抛出异常的情况。发送调用成功表示本地发送没有报告错误,不代表设备 已经执行了某个动作。

配套 App 已有原始发帧脚本,同样需要先替换 ID 和 DATA:

bash scripts/run_host_python.sh scripts/send_test_frame.py \
  --id 0x123 \
  --data 01 02 03 \
  --confirm SEND_GENERIC_CAN_FRAME

7. 接收原始 CAN 帧

from generic_can import CanBus

with CanBus() as bus:
    frame = bus.receive_frame(timeout_s=1.0)
    if frame is None:
        print("1 秒内没有收到数据帧")
    else:
        print(hex(frame.arbitration_id))
        print(frame.data.hex(" "))
        print(frame.timestamp)

receive_frame() 从原始接收队列中取下一帧完整数据;超时返回 None。 timeout_s=0 表示立即检查队列,不等待新数据。

接收结果保留 CAN ID、DATA、时间戳和帧标志。接收函数不会替你解释字节含义。 SocketCAN 也可能向本地其他监听程序交付发送帧的回环副本,因此观察到一帧不等于收到 远端设备的业务应答。

持续观察可以使用配套的只读脚本:

bash scripts/run_host_python.sh scripts/can_monitor.py

在宿主 Linux 运行自己的 Python 示例

先完成第 3 节:启动完整 App、准备好依赖,并确认宿主 can0 已启用。 做这些宿主实验时保持 App 运行,不要为了运行单个示例先停止 App,以免 MCU 复位影响 USB CAN 链路。

上面的接收代码是完整示例。用编辑器在开发板 App 的 python/ 目录中新建 user_example.py, 粘贴该代码并保存。user_example.py 是你自己创建的文件,配套源码没有预置这个文件。 然后在 SSH 终端执行:

cd /home/arduino/ArduinoApps/generic-can0-lab
bash scripts/run_host_python.sh python/user_example.py

这条命令只运行指定的 Python 文件,不是 Arduino App 的标准启动命令。 配套的 run_host_python.sh 会找到此 App 的 .cache/.venv 中已安装的依赖, 将它和 bricks/、python/ 加入 Python 模块搜索路径,再用宿主的 python3 执行文件。 它不会安装依赖、编译或启动 Sketch,也不会配置 can0。

脚本没有启动虚拟环境中的 Python 解释器,而是复用其中的依赖文件。因此宿主 Python 版本和 CPU 架构必须与这些依赖匹配。当前随附的 wrapt 安装包用于 CPython 3.13 / aarch64; 其他环境需要匹配的安装包,不能把本方法理解为任意电脑都能运行。

例子的输出直接显示在当前 SSH 终端。没有报文时,它会打印“1 秒内没有收到数据帧”并正常退出; 持续监听脚本可按 Ctrl+C 结束,这不会停止 App CLI 管理的 App。

不想新建文件时,直接使用配套的监听脚本即可:

cd /home/arduino/ArduinoApps/generic-can0-lab
bash scripts/run_host_python.sh scripts/can_monitor.py

报文定义放在 python/can_messages.py,单独试验的业务代码放在自己的 Python 文件中。 App CLI 默认启动的入口仍是 python/main.py,不会因为多了一个 user_example.py 就自动执行它。

修改后重新运行与停止

只修改宿主示例时,结束该脚本后重新执行上面的命令即可,不必重启整个 App。 修改 App 入口、Brick、依赖或 Sketch 后,需要重新启动完整 App 才能应用相应修改:

cd /home/arduino/ArduinoApps/generic-can0-lab
arduino-app-cli app restart .
arduino-app-cli app logs . --tail 100
ip -details -statistics link show can0

重新启动前先结束宿主收发脚本;重新启动后按第 3 节检查接口,必要时重新配置。 全部用完后,先结束宿主脚本,再停止 App:

arduino-app-cli app stop .

官方 CLI 停止带 Sketch 的运行中 App 时会尝试复位 MCU;若采用“等待 App”启动模式, MCU 会等待下次 App 启动,其他启动模式则可能重新运行 Sketch。 因此,停止 App 后不能保证板载 can0 仍然可用。 详见 官方停止与重启流程。

若板上 CLI 版本的参数与本文不同,用 arduino-app-cli app --help 和对应子命令的 --help 查看本机支持的用法,例如 arduino-app-cli app logs --help;不要把项目自带脚本当成官方子命令。

运行时常见问题

现象 先检查什么
提示已有 App 运行 用 app list 确认名称;当前 App 已运行就不必重复启动,其他 App 要先确认可以停止
can0 不存在 完整 App 是否成功启动、MCU 是否进入 Sketch、USB CAN 链路是否出现
Network is down 在宿主检查 can0 是否为 UP,按设备要求配置后再运行脚本
App 报接口不存在,但 SSH 能看到 can0 区分容器与宿主;宿主示例使用配套 helper,容器访问问题仍需单独处理
App virtual environment not found 首次 App 启动是否已完成依赖安装;helper 不负责安装
找不到 python/user_example.py 是否已自行创建文件,以及当前是否位于 App 根目录
ModuleNotFoundError 或二进制依赖加载失败 App 依赖是否安装成功,宿主 Python 版本/架构是否匹配
监听一直没有数据 总线上是否有设备实际发帧;没有输出不等于接口故障

8. Standard 与 Extended:选择 ID 格式

标准格式使用 11 位 ID,范围为 0x000~0x7FF:

from generic_can import CanFrame

standard_frame = CanFrame(
    arbitration_id=0x123,
    data=b"\x01",
    is_extended=False,
)

扩展格式使用 29 位 ID,范围为 0x00000000~0x1FFFFFFF:

extended_frame = CanFrame(
    arbitration_id=0x123456,
    data=b"\xA5",
    is_extended=True,
)

是否使用扩展格式要按设备协议决定,不能只根据 ID 的数值大小猜测。

9. Classical CAN、CAN FD 与 BRS

默认的 is_fd=False 表示 Classical CAN,DATA 长度为 0~8 字节。

CAN FD 使用 is_fd=True。下面只构造一个包含 16 字节、启用 BRS 的示例帧:

fd_frame = CanFrame(
    arbitration_id=0x420,
    data=bytes(range(16)),
    is_fd=True,
    bitrate_switch=True,
)

bitrate_switch=True 只能配合 CAN FD。BRS 切换的是数据阶段的位速率,不改变 CAN ID 的标准/扩展格式。实际发送前,板载链路、总线设备以及位速率配置都必须支持该模式。

本 Brick V1 为避免 Backend 隐式 DLC 扩展,只接受能够直接对应 CAN FD DLC 的 DATA 长度:

0、1、2、3、4、5、6、7、8、12、16、20、24、32、48、64

DLC 是帧中的数据长度编码。上述检查是本 Brick 的输入约定:如果业务只有 9 个有意义字节, 应按设备协议规定的填充值和位置显式补齐到 12 字节。不能随意补零,也不依赖底层自动补齐。

is_fd=True 只设置帧标志,不会把系统 can0 自动切换到 FD 模式。本文没有 CAN FD 实机验证结果。

10. 用 MessageDefinition 自定义 CAN 报文

理解原始帧后,可以给重复使用的报文取一个名字。在 python/can_messages.py 中, MESSAGES 字典把名称映射到报文定义:

from generic_can import MessageDefinition

MESSAGES = {
    "fixed_command": MessageDefinition(
        arbitration_id=0x201,
        direction="tx",
        fixed_data=b"\x01\x01",
    ),
}

在自己的业务文件中使用:

from can_messages import MESSAGES
from generic_can import CanBus

with CanBus(messages=MESSAGES) as bus:
    bus.send("fixed_command")

这会发送 ID 为 0x201、DATA 为 01 01 的帧。名称只在 Python 程序内使用, 不会作为文本发到 CAN 总线上。MessageDefinition 的作用就是把设备手册中的 CAN ID 和 DATA 规则写成 Python。

11. encode:把数值转换成 DATA

假设教学报文规定:“数值乘以 10,存为两个字节的大端有符号整数”。完整示例中的 encode_value() 实现如下:

import math

def encode_value(value):
    """
    @description         : 把教学数值放大10倍并编码成2字节大端有符号整数
    @param value         : 需要编码的数值,单位由报文规则定义
    @return              : 两字节CAN DATA
    """
    if isinstance(value, bool) or not isinstance(value, (int, float)):
        raise ValueError("value must be a number")
    if not math.isfinite(float(value)):
        raise ValueError("value must be finite")

    scaled = int(round(float(value) * 10.0))
    if not -32768 <= scaled <= 32767:
        raise ValueError("value does not fit the signed 16-bit example field")
    return scaled.to_bytes(2, byteorder="big", signed=True)

在 MESSAGES 中加入定义:

MESSAGES["set_value"] = MessageDefinition(
    arbitration_id=0x202,
    direction="tx",
    encode=encode_value,
)

重新创建加载此定义的总线后,bus.send("set_value", value=120) 的转换过程是:

value = 120 → 乘以10 → 1200 → 0x04B0 → DATA = 04 B0

大端表示高位字节在前;signed=True 允许表示负数。比例、单位和字节顺序都由你 自己的报文规则决定,并不是 CAN 自动规定的。

12. decode:把 DATA 转换成数值

假设状态报文的第 0 字节存放状态位,第 1~2 字节存放前述放大 10 倍的数值:

def decode_status(data):
    """
    @description         : 把状态位和2字节大端数值解析成便于阅读的字典
    @param data          : 至少包含3字节的CAN DATA
    @return              : 包含ready、fault和value的字典
    """
    payload = bytes(data)
    if len(payload) < 3:
        raise ValueError("status requires at least 3 bytes")

    flags = payload[0]
    scaled_value = int.from_bytes(
        payload[1:3], byteorder="big", signed=True,
    )
    return {
        "ready": bool(flags & 0x01),
        "fault": bool(flags & 0x02),
        "value": scaled_value / 10.0,
    }


MESSAGES["status"] = MessageDefinition(
    arbitration_id=0x301,
    direction="rx",
    decode=decode_status,
)

03 04 AF 会被解析为:

{"ready": True, "fault": True, "value": 119.9}

原因是 03 的 bit0 和 bit1 都为 1,表示此教学规则中的就绪和故障标志; 04 AF 是整数 1199,除以 10 得到 119.9。

加载完整报文表后读取状态:

from can_messages import MESSAGES
from generic_can import CanBus, CANTimeoutError

with CanBus(messages=MESSAGES) as bus:
    try:
        print(bus.receive("status", timeout_s=1.0))
    except CANTimeoutError:
        print("1 秒内没有可读取的 status")

receive(name) 从该名称的 FIFO 队列中获取下一条待消费帧,并调用 decode。 FIFO 表示先进入队列的帧先被读取。未配置 decode 时,返回完整的 CanFrame。

它不会判断此帧是否由上一条 send() 引起:发送前已经收到的旧状态,也可能先被返回。 需要关联某次发送与其应答时,由具体设备协议层或业务代码根据序号等字段处理。 本 Brick 不提供通用 request()。

报文方向与错误处理

direction 允许的配置
tx 必须选择 fixed_data 或 encode,不能配置 decode
rx 可以配置 decode,不能配置发送数据来源
both 必须有一种发送数据来源,可按需配置 decode

fixed_data 与 encode 不能同时存在。命名接收匹配 (arbitration_id, is_extended, is_fd),因此不同 ID 格式不会混在一起。

异常 含义
CANConfigurationError ID、DATA、timeout 或报文定义不合法
CANBackendError 访问 can0 或底层收发失败
CANTimeoutError 等待命名报文超时
CANMessageError 名称、方向或用户编解码失败
CANUnsupportedFeatureError 使用了当前未支持的能力

以上异常都继承 CANError。原始接收超时返回 None,命名接收超时抛出 CANTimeoutError。timeout_s 只能是有限、非负的数字,不能是布尔值。

配套文件与使用范围

app/
├── app.yaml
├── README.md
├── python/
│   ├── main.py                 # 默认只监听
│   └── can_messages.py         # 编辑自己的报文规则
├── bricks/
│   └── generic_can/            # 完整Brick、API说明和离线依赖
├── sketch/                    # 最小RouterBridge Sketch
├── scripts/                   # 监听与手动收发脚本
└── tests/                     # 不依赖硬件的单元测试

V1 正式支持目标为 VENTUNO Q 板载 CAN 螺钉座及其 Linux can0。它提供原始帧收发、 标准/扩展格式、Classical CAN、CAN FD/BRS 字段和用户自定义编解码。不内置设备协议, 也不自动扫描接口、配置位速率或周期发送。

模块目录和第三方离线安装包继续独立保存在本 App 内,使用时不依赖其他 App 的虚拟环境、 vendor 或缓存。

开发者说明

以下内容供维护源码和测试使用,不是普通用户使用螺钉座 CAN 的前置知识。

内部结构与可靠性

CanBus
    ↓
SocketCANBackend
    ↓
python-can
    ↓
can0

CanFrame 表示数据,MessageDefinition 保存命名报文规则。python-can 仅由 backends/socketcan.py 导入。每个 CanBus 只有一个 Receiver Thread,将收到的帧 放入 raw queue 和匹配的 named queue,两种接收 API 不会分别争抢底层接收。

原始队列默认容纳 256 帧,每个命名队列默认容纳 64 帧。队列满时丢最旧帧、保留最新帧, describe() 可读取丢帧统计。close() 通过发送锁等待当前发送完成,再关闭底层资源。 被过滤的错误帧和远程帧不会无限延长正数 timeout。

backend 参数仅用于开发测试,允许注入 FakeBackend;正式路径仍是 CanBus() 内部创建 SocketCANBackend(device="can0")。 给 CanBus 注入设备名不匹配的 SocketCANBackend 会立即报错。 bus.interface 保留为只读诊断值 "can0"。

顶层稳定导出仍只有 CanBus、CanFrame、MessageDefinition 和上述六个异常类 (含基类 CANError)。没有接口选择器、Factory 或通用请求匹配器。

本 Brick 不承诺支持 can1、vcan0、USB-CAN 或其他 Linux 平台与物理接口。

无硬件测试

在完整 App 根目录运行:

bash tests/run.sh

本次在本地仓库实际执行的单元测试结果:

Ran 57 tests

OK

测试包括固定 can0、原始和命名收发、CAN FD 长度、单接收线程、有界队列、并发关闭、 严格超时、公开 API 和 App 启动只监听。FakeBackend 测试不需要开发板、真实 CAN 接口或 已安装的 python-can。

语法检查可在 App 根目录执行:

PYTHONPYCACHEPREFIX=/tmp/generic-can0-lab-pycache \
python3 -m compileall -q bricks/generic_can python tests scripts

附录:虚拟 CAN 回环

scripts/loopback_test.py 只供内部后端开发测试,固定使用已有的 vcan0,不经过 正式的 CanBus,也不自动创建接口。它直接验证两个内部后端之间的本机回环:

bash scripts/run_host_python.sh scripts/loopback_test.py \
  --confirm SEND_SOCKETCAN_LOOPBACK_FRAME

此测试不能证明 VENTUNO Q 螺钉座、外部接线或真实设备通信正常。

验证范围

本版代码已通过本地单元测试和 compileall,尚未完成它自身的真实 can0 收发、CAN FD 或设备通信验收。 第 3.2 节的实机观察只确认了:另一个 App 运行时,系统 can0 有收发活动, 不依赖 Generic CAN0 Lab 启动。它不是本版 Brick 的收发测试,也不是停止所有 App 后的对照测试。

这页先记到这里。问题拆开一点,答案就会近一点。