摘要:最近用到python-can,发现网上参考资料实在不多,也缺乏一定的系统性,特此进行一番整理。——by catmemo
注:
这段时间以来,主要基于python-can这个库,开发了一个CAN节点模拟仿真工具,有需要的可以去下载来用,当然也欢迎二次开发。
(当然也包括CAN收发、诊断、图形化显示等功能,详细内容可以在抖音搜索:47:/ 复制打开抖音,看看【一夜春风的作品】CAN 仿真神器——QX工具链之UltraSim… ЭmAELbZr5bntmw8ππ )。
基于python-can 4.0.0 重新进行整理。在翻译的基础上,会加入一点自己的理解,参考来源:https://python-can.readthedocs.io/en/4.0.0/api.html
1、简述
python-can 是一个为 Python 提供的控制器局域网(CAN)支持库,通过统一的抽象接口兼容不同硬件设备,并包含一系列工具用于在 CAN 总线上收发消息。
python-can可以在Python运行的任何地方运行; 从具有商用CAN的高功率计算机到USB设备,再到运行Linux的低功率设备(例如BeagleBone或RaspberryPi)。
典型应用场景:
- 数据监控与分析:通过 OBD-II 端口记录车辆 CAN 总线数据,用于故障诊断或性能分析,如商用车队管理。
- 硬件测试:对汽车、摩托车等设备的 CAN 模块进行自动化测试,例如验证电子控制单元(ECU)的通信协议兼容性。
- 快速原型开发:
- 硬件在环(HIL):在算法开发阶段直接与真实 CAN 总线交互,加速迭代。
- ECU仿真(虚拟节点模拟):创建虚拟 CAN 设备模拟总线行为,测试其他节点的响应逻辑。
连接到CAN总线,创建和发送消息的简单实例:
#!/usr/bin/env python
# coding: utf-8
"""
This example shows how sending a single message works.
"""
from __future__ import print_function
import can
def send_one():
# this uses the default configuration (for example from the config file)
# see https://python-can.readthedocs.io/en/stable/configuration.html
bus = can.interface.Bus()
# Using specific buses works similar:
# bus = can.interface.Bus(bustype='socketcan', channel='vcan0', bitrate=250000)
# bus = can.interface.Bus(bustype='pcan', channel='PCAN_USBBUS1', bitrate=250000)
# bus = can.interface.Bus(bustype='ixxat', channel=0, bitrate=250000)
# bus = can.interface.Bus(bustype='vector', app_name='CANalyzer', channel=0, bitrate=250000)
# ...
msg = can.Message(arbitration_id=0xc0ffee, data=[0, 25, 0, 1, 3, 1, 4, 1], is_extended_id=True)
try:
bus.send(msg)
print("Message sent on {}".format(bus.channel_info))
except can.CanError:
print("Message NOT sent")
if __name__ == '__main__':
send_one()
例子中app_name 参数是一个容易被忽略但可能导致隐蔽问题的关键配置项。一个典型的故障案例是:使用 channel = 1 可以正常发送 CAN 消息,但切换到 channel = 3 时(硬件连接也使用channel 3)消息却无法发出。经过排查,根源正是 app_name 的设置问题。
原因分析:
如果不显式指定 app_name,其默认值似乎是 “CANoe”,即 app_name =“CANoe”。
当 app_name = “CANoe” 时,channel 参数指向的是 CANoe 软件环境内部定义的通道(可能是虚拟通道或仿真通道)。如果 CANoe 工程中未定义 CAN3 通道,或者该通道是虚拟通道而非指向实际硬件的物理通道,那么发送操作自然会失败。
因此,在调用相关函数时,务必显式设置 app_name 参数。这是最推荐的做法。如果不喜欢命名,也可以直接 app_name = “”,明确告知python-can忽略 CANoe 内部通道配置,直接使用底层硬件驱动映射的物理通道号。
2、安装
可以使用pip命令进行安装:
$ pip install python-can
由于您很可能与某些硬件连接,因此可能还必须安装对应的平台依赖项。
许多接口可以像安装python依赖项一样,通过如下命令进行安装:
$ pip install python-can[serial]
2.1 GNU/Linux 依赖项
理论上来说,现代Linux内核(2.6.25或更新版本)有一个socketcan的实现。如果用Python3.3或更高版本调用,此版本的python can将直接使用socketcan,否则将通过ctypes的类型来判断。
2.2 Windows 依赖项
2.2.1 Kvaser
使用Kvaser CANLib SDK作为后端安装python-can:
安装Kvaser最新的Windows CANLib驱动程序。
测试Kvaser自己的工具是否正常工作,以确保驱动程序安装正确,以及硬件是否能正常工作。
2.2.2 PCAN
为您的接口下载并安装最新的驱动:
Windows (also supported on Cygwin)
Linux(also works without, see also Linux installation)
macOS
请注意,PCANBasic API时间戳从系统启动开始计算秒数。要将这些时间转换为纪元时间,请使用uptime库。否则,时间将以系统启动后的秒数返回。要安装uptime库,请运行pip install python can[pcan]。
如果安装了Python for Windows Extensions库,此库将可以很好的被用于获得新消息的通知,而不是不得不使用的CPU密集型轮询。
2.2.3 IXXAT
使用IXXAT VCI V3 或者 V4 SDK 作为后端安装python-can:
(1)安装IXXAT’s latest Windows VCI V3 SDK or VCI V4 SDK (Win10) drivers
(2)测试IXXAT自己的工具(例如MiniMon)是否正常工作,以确保驱动程序安装正确,以及硬件是否能正常工作。
2.2.4 NI-CAN
下载并安装National Instruments
当前该驱动在Windows平台只支持32位python。
2.2.5 neoVI
查看neoVI
2.2.6 Vector
使用XL Driver Library作为后端安装python-can:
(1)为Vector硬件接口安装最新的驱动程序。
(2)安装XL Driver Library或复制vxlapi.dll和/或vxlapi64.dll到您的工作目录中。
(3)使用Vector Hardware Configuration为您的应用程序分配通道。
2.2.7 CANtact
Linux、Windows和macOS均支持CANtact。要使用CANtact driver作为后端安装python-can,请执行以下操作:
(1)python3 -m pip install "python-can[cantact]"
(2)如果已经安装了python-can,则可以单独安装CANtact后端:
python3 -m pip install cantact
(3)更多相关的文档,请查看cantact.io
2.2.8 CanViewer
当前python-can已经支持通过运行python -m can.viewer 命令显示一个简单的CAN查看器终端应用程序。(类似于CANalyzer的trace)
在Windows上,需要运行如下命令安装库依赖:
python -m pip install "python-can[viewer]"
2.2.9 Installing python-can in development mode
此软件包的“开发版本”安装允许您在本地进行更改,或从Git存储库中获取更新并使用它们,而无需重新安装。下载或克隆源存储库,然后:
python setup.py develop
3、配置
通常,此库与特定的CAN接口一起使用,可以在代码中指定,也可以从配置文件或环境变量中读取。
3.1、代码中指定
can对象暴露了一个rc字典,该字典可用于在从can.interfaces导入之前设置接口和通道。
import can
can.rc['interface'] = 'socketcan'
can.rc['channel'] = 'vcan0'
can.rc['bitrate'] = 500000
from can.interface import Bus
bus = Bus()
您还可以为每个Bus实例指定接口和通道:
import can
bus = can.interface.Bus(bustype='socketcan', channel='vcan0', bitrate=500000)
3.2、配置文件
在Linux系统上,将在以下路径中搜索配置文件:
- 〜/ can.conf
- /etc/can.conf
- $ HOME / .can
- $ HOME / .canrc
在Windows系统上,将在以下路径中搜索配置文件:
- %USERPROFILE%/can.conf
- can.ini(当前工作目录)
- %APPDATA%/can.ini
配置文件设置默认接口和通道:
[default]
interface = <the name of the interface to use>
channel = <the channel to use by default>
bitrate = <the bitrate in bits/s to use by default>
该配置还可以包含其他部分(或上下文):
[default]
interface = <the name of the interface to use>
channel = <the channel to use by default>
bitrate = <the bitrate in bits/s to use by default>
[HS]
# All the values from the 'default' section are inherited
channel = <the channel to use>
bitrate = <the bitrate in bits/s to use. i.e. 500000>
[MS]
# All the values from the 'default' section are inherited
channel = <the channel to use>
bitrate = <the bitrate in bits/s to use. i.e. 125000>
from can.interfaces.interface import Bus
hs_bus = Bus(context='HS')
ms_bus = Bus(context='MS')
3.3、环境变量
可以从以下环境变量中获取配置:
- CAN_INTERFACE
- CAN_CHANNEL
- CAN_BITRATE
3.4、接口名称
| Name | Documentation |
|---|---|
| “socketcan” | SocketCAN |
| “kvaser” | Kvaser’s CANLIB |
| “serial” | CAN over Serial |
| “slcan” | CAN over Serial / SLCAN |
| “ixxat” | IXXAT Virtual CAN Interface |
| “pcan” | PCAN Basic API |
| “usb2can” | USB2CAN Interface |
| “nican” | NI-CAN |
| “iscan” | isCAN |
| “neovi” | NEOVI Interface |
| “vector” | Vector |
| “virtual” | Virtual |
| “canalystii” | CANalyst-II |
| “systec” | SYSTEC interface |
4、库API
主要对象是BusABC和Message。
- Bus
- Thread safe bus
- Message
- Listeners
- Asyncio support
- Broadcast Manager
- Bit Timing Configuration
- Internal API
Utilities
can.detect_available_configs(interfaces=None)
检测接口当前可以连接的所有配置/通道。
这可能相当耗时。
并非每个平台上的每个接口都可以实现自动配置检测。在这种情况下,此方法不会引发错误,而是返回该接口的空列表。
- Parameters
interfaces (Union[None, str, Iterable[str]])
- None:在所有已知接口中搜索
- string:在字符串名称的接口中搜索
- 在可迭代的接口名称中搜索
- Return type
list[dict] - Returns
一个可迭代的dicts,每个dict都适合在can.BusABC的构造函数中使用。
Notifier
Notifier对象是消息总线(bus)的一个消息分发器。它通过创建一个线程来从总线读取消息,然后将这些消息分发给监听器(listeners)。
类结构:
classcan.Notifier(bus, listeners, timeout=1.0, loop=None)
功能:管理can.Message实例向侦听器的分发。
特性:支持多个总线和多个监听器。
需要注意的是,记得在所有消息接收完毕后调用 stop(),因为许多监听器会执行刷新(flush)操作来确保数据被持久化保存。
在python-can中,监听器是处理接收到的CAN消息的对象。它们可以被添加到总线实例中,用于实时处理或记录消息。
Flush操作是将暂存在内存缓冲区中的数据强制写入到它最终应该去的地方(比如一个文件、数据库,或者只是确保操作系统发送出去)。这样可以防止程序意外终止时,内存中未来得及保存的数据丢失。
而调用 stop() 方法会通知所有这些监听器执行最终的清理工作,其中就包括flush操作。这对于像 Logger、Printer 这类需要确保所有数据都已被写入的监听器来说至关重要。
构造函数参数:
- bus (Union[BusABC, List[BusABC]]) – 可以是一个总线(BusABC)对象,也可以是总线对象的列表,用于监听消息。
- listeners (Iterable[Union[Listener, Callable[[Message], None]]]) – 一个可迭代对象,包括Listener或可以处理Message的可调用对象。
- timeout (float) – 可选参数,指定等待消息的最长秒数。
- loop (Optional[AbstractEventLoop]) –一个可选的事件循环,用于安排监听器的执行。
方法:
1. add_bus(bus)
功能:添加一个新的总线用于通知。
参数:bus (BusABC) ,一个CAN总线实例(BusABC)。
2. add_listener(listener)
功能:将新的监听器添加到通知列表中。如果已经存在,该监听器将在每次消息到达时被调用两次。
参数:listener (Listener) ,要被通知的Listener对象。
3. remove_listener(listener)
功能:从通知列表中移除一个监听器。如果指定的监听器不在存储的监听器中,会抛出异常。
参数:listener (Listener) , 要移除的Listener对象。
异常:如果监听器从未被添加抛出 ValueError 异常。
4. stop(timeout=5)
功能:停止在新消息到达时通知监听器,并调用每个监听器的stop方法。
参数:timeout (float) ,等待接收线程结束的最长时间,应该长于实例化时设置的timeout。
Errors
python-can 库的异常类继承自 Python 标准库的 Exception,专门用于处理 CAN 总线相关场景。
Exception (Python standard library)
+-- ...
+-- CanError (python-can)
+-- CanInterfaceNotImplementedError
+-- CanInitializationError
+-- CanOperationError
+-- CanTimeoutError
- CanError(基础类):
exceptioncan.exceptions.CanError(message='', error_code=None)- 这是所有CAN相关异常的基类。
- 可以附带一个可选的错误代码,以帮助确定故障原因。例如,CanOperationError可以接受一个消息和错误代码来显示错误信息。
>>> # With an error code (it also works with a specific error):
>>> error = CanOperationError(message="Failed to do the thing", error_code=42)
>>> str(error)
'Failed to do the thing [Error Code 42]'
>>>
>>> # Missing the error code:
>>> plain_error = CanError(message="Something went wrong ...")
>>> str(plain_error)
'Something went wrong ...'
-
CanInitializationError:
exceptioncan.exceptions.CanInitializationError(message='', error_code=None)- 这个异常在初始化CAN总线时发生错误时触发。
- 如果因为缺少驱动程序或平台不被支持导致初始化失败,则会抛出CanInterfaceNotImplementedError。
- 而如果初始化失败是因为参数值超出范围,则会抛出ValueError。
-
CanInterfaceNotImplementedError:
exceptioncan.exceptions.CanInterfaceNotImplementedError(message='', error_code=None)- 表示在当前平台上不支持某个接口。
- 可能场景包括接口名称不存在、操作系统或解释器不支持该接口、或驱动程序不存在或版本不正确。
-
CanOperationError:
exceptioncan.exceptions.CanOperationError(message='', error_code=None)- 表示在操作过程中出现的错误。
- 例如,调用库函数返回值不符合预期、接收到无效消息、驱动程序拒绝发送消息等错误情形。
-
CanTimeoutError:
exceptioncan.exceptions.CanTimeoutError(message='', error_code=None)- 与超时相关的操作错误。
- 场景包括未能在超时时间内发送消息或未能在给定时间内读取消息。
-
error_check 函数:
can.exceptions.error_check(error_message=None, exception_type=<class 'can.exceptions.CanOperationError'>)- 该函数用于捕获任何异常,并将其转换为新的异常类型,同时保留堆栈跟踪。
这些异常类提供了一种精细化的错误处理机制,使得开发者可以根据具体的错误情形采取相应的响应措施。可以在写代码时对可能出现的不同类型的异常进行捕获和处理,从而增强系统的鲁棒性和稳定性。
4.1、Bus
正如其名,BusABC 类提供了一个 CAN 总线的抽象。该总线为物理或虚拟 CAN 总线提供了一个封装。也就是说,BusABC是所有具体接口的抽象基类,它提供了一个标准化的API供CAN总线操作使用。我们可以将此类用作上下文管理器,或通过迭代器来接收消息。
例如,Bus 类会创建 BusABC 的特定接口实例:
vector_bus = can.Bus(interface='vector', ...)
该总线随后能够处理特定于接口的软件/硬件交互,并实现 BusABC API。
还提供了一个线程安全的总线包装器,可以在多个线程中同时使用。详情请参阅Thread safe bus。
4.1.1 Autoconfig Bus
class can.Bus(channel, can_filters=None, **kwargs)
- 该类是用于配置加载的 CAN 总线封装器,支持自动从默认位置读取配置文件并实例化指定接口的 CAN 总线。
- 通过 channel 和 **kwargs 传递设备相关参数。channel 即实际使用的CAN通道,例如 “can0”(SocketCAN)或 PCAN_USBBUS1(PCAN)。**kwargs 例如,bitrate=500000 设置比特率。
- 通过 can_filters 参数设置硬件或软件过滤器,优化总线负载。可选序列(CanFilter 或 CanFilterExtended)
- 异常处理:
- ValueError:参数超出有效范围时触发,如无效的通道名或比特率。
- CanInterfaceNotImplementedError :指定的设备驱动无法访问(如未安装或系统不支持)。
- CanInitializationError :总线初始化失败,可能因硬件故障或配置冲突。
4.1.2 API
class can.BusABC(channel, can_filters=None, **kwargs)
作为所有具体接口基础的 CAN 总线抽象基类,为具体的接口实现提供了标准化的操作API。
此类可用作已接收消息的迭代器,并可用作上下文管理器,在使用完毕后自动关闭总线。
参数上面已经介绍过了,不再赘述。(channel/can_filters/**kwargs)
主要讲一下提供了哪些方法:
- _iter_():支持迭代接收消息
>>> for msg in bus:... print(msg)
- RECV_LOGGING_LEVEL= 9:设置接收消息的日志级别(默认为 9)。
- channel_info= ‘unknown’:描述通道信息。
- propertyfilters: Optional[Sequence[Union[can.typechecking.CanFilter, can.typechecking.CanFilterExtended]]]:过滤器属性,修改总线的消息过滤器,调用set_filters()方法实现。
- flush_tx_buffer():清除所有在输出缓冲队列中的消息。
- recv(timeout=None):阻塞等待从总线接收消息,超时时会返回None。
- abstract send(msg, timeout=None):定义了发送消息至CAN总线的接口。用于发送单条 CAN 消息,支持超时控制。
- send_periodic(msgs, period, duration=None, store_task=True):
- 开始以指定周期发送消息,通过 period 控制间隔,duration 控制总时长。可通过任务的stop方法或者其他条件终止发送。
- 需要注意的是,消息停止发送的实际持续时间可能与用户指定的持续时间不完全相同。一般来说,消息将按照指定的频率发送,至少持续到用户设定的持续时间(duration)结束。
- 对于那些运行时间非常长且包含许多短期任务的Bus实例,使用默认API(即store_task=True)可能不太合适。因为即便任务已停止,它们仍与Bus实例关联,从而占用内存。这意味着在这种情况下,可能需要采取措施手动管理这些任务以释放内存。
- set_filters(filters=None):应用消息过滤。若未传入过滤器,则重置为无过滤。过滤器通过 can_id 和 can_mask 匹配消息,支持标准帧和扩展帧。
- shutdown():对接口执行必要的清理操作以释放资源,关闭总线。
- propertystate: can.bus.BusState:获取当前硬件的状态。
- stop_all_periodic_tasks(remove_tasks=True):停止所有使用send_periodic开始的周期性任务。
4.1.3 Transmitting
我们可以调用send()方法并传过去一个Message类型的参数将message发送到CAN总线上,如下所示:
with can.Bus() as bus:
msg = can.Message(
arbitration_id=0xC0FFEE,
data=[0, 25, 0, 1, 3, 1, 4, 1],
is_extended_id=True
)
try:
bus.send(msg)
print(f"Message sent on {bus.channel_info}")
except can.CanError:
print("Message NOT sent")
周期型信号的发送则通过boradcast manager控制。
4.1.4 Receiving
通过调用recv()方法或者直接在bus上进行迭代可以读取总线上的messages:
with can.Bus() as bus:
for msg in bus:
print(msg.data)
此外,我们也可以通过Listener api来接收和处理来自Notifier的消息。
4.1.5 Filtering
可以为每一个总线设置消息过滤。
如果接口支持,这种过滤是在硬件或内核层进行的,而不是在Python层面进行的。这提高了效率,因为它减少了Python上不必要的消息处理。
所有符合至少一个过滤器的消息都会被返回。这意味着,只要消息匹配任何一个过滤条件,就会被接受和处理。
下面是定义两个过滤器的示例:
第一个过滤器允许11位ID为0x451的消息通过。
第二个过滤器允许29位ID为0xA0000的消息通过。
filters = [
{"can_id": 0x451, "can_mask": 0x7FF, "extended": False},
{"can_id": 0xA0000, "can_mask": 0x1FFFFFFF, "extended": True},
]
bus = can.interface.Bus(channel="can0", bustype="socketcan", can_filters=filters)
我们进一步解释一下,过滤器的核心机制基于CAN标识符(ID)和掩码(mask)的匹配规则,每个过滤器由三个关键参数定义:
- can_id:目标消息ID值
- can_mask:二进制掩码,决定ID的哪些位需要匹配
- extended:标识符类型(标准11位或扩展29位)
硬件层会将接收到的消息ID与过滤条件按位比较,仅当 (received_id & mask) == (can_id & mask) 成立时,消息才会被传递到应用层。
-
第一个过滤器:匹配标准ID消息
- can_id=0x451(二进制 0100 0101 0001)
- can_mask=0x7FF(二进制 111 1111 1111,全匹配11位)
- 即,仅接收精确等于 0x451 的标准ID消息才通过
-
第二个过滤器:匹配扩展ID消息
- can_id=0xA0000(二进制 1010 0000 0000 0000 0000)
- can_mask=0x1FFFFFFF(二进制 0001 1111 1111 1111 1111 1111 1111 1111,全匹配29位)
- 即,仅接收精确等于 0xA0000 的扩展ID消息才通过
我们解释下掩码的作用:
掩码控制ID的匹配粒度:
- 若掩码某位为 1,则对应ID位必须严格匹配,即接收的ID必须与预设ID的位一致;
- 若掩码某位为 0,则对应ID位被忽略,即接收的ID可以不与预设ID的位一致;
- 例如,can_id = 0x100(二进制:0001 0000 0000),can_mask = 0x700(二进制:0111 0000 0000)时,此例中第9-11位掩码为0111,预设ID对应位为0001,因此:
- 第8位:必须为1(因预设ID此位为1)
- 第9位:必须为0
- 第10位:必须为0
- 掩码为0的位(其余位):这些位的值可任意,不影响过滤结果。例如第0-7位可接受0或1。
- 以下ID均能通过过滤:
- 0001 0000 0000(0x100)
- 0001 0000 0001(0x101)
- 0001 0000 1000(0x108)
4.2 Thread safe bus
这个版本的 BusABC 类通过独立锁机制确保多线程环境下的安全使用。send 和 recv 方法分别使用不同的锁进行同步,避免因共享锁导致的性能瓶颈。冲突调用会通过阻塞等待总线可用来解决,从而保证操作的原子性。
与普通 BusABC 使用方法完全一致。例如:
classcan.ThreadSafeBus(*args, **kwargs)
# 'socketcan' is only an example interface, it works with all the others too
my_bus = can.ThreadSafeBus(interface='socketcan', channel='vcan0')
my_bus.send(...)
my_bus.recv(...)
适用于需要多线程共享总线资源的 CAN 通信场景,如高频数据采集或实时控制系统中。
4.3 Message
Message 对象用于表示 CAN 消息,用于发送、接收以及在不同日志格式之间进行转换等用途。
消息可以使用扩展标识符,可以是远程帧或错误帧,包含数据,并且可能与通道相关联。
消息始终通过标识进行比较,而绝不会通过值进行比较,因为那样可能会导致意外的行为。
消息不支持“动态”属性,即除文档中列出的属性之外的任何属性,因为它使用了 _slots_ 。
要创建一个消息对象,只需向构造函数提供以下任意属性以及作为关键字参数的其他参数即可。
参数
check(布尔型) - 该类的构造函数默认情况下不会严格检查输入。因此,调用者必须防止创建无效的消息,或者将此参数设置为 True,以便在输入无效时引发错误。可能存在的问题包括 dlc 字段与数据长度不匹配,或者创建一个同时将 is_remote_frame 和 is_error_frame 设置为 True 的消息。
Raises
ValueError - 当且仅当check设置为 True 并且一个或多个参数无效时上报。
可以实例化一个定义了数据的消息,并为诸如仲裁 ID、标志和时间戳等所有属性提供可选参数,例如:
from can import Message
test = Message(data=[1, 2, 3, 4, 5])
test.data
bytearray(b’\x01\x02\x03\x04\x05’)
test.dlc
5
print(test)
Timestamp: 0.000000 ID: 00000000 010 DLC: 5 01 02 03 04 05
CAN消息中的arbitration_id字段长度可以是11位(标准寻址,CAN 2.0A)或29位(扩展寻址,CAN 2.0B), python-can通过is_extended_id属性公开了这种差异。
实际上,Message是一个比较好理解的概念。我们可以把 python-can 库想象成一个专业的CAN总线信使,而CAN总线是信使要去的各个办事处之间的专用通信网络。那么,Message 类就是这个信使手里拿着的“标准信封”。所有要通过CAN总线发送的信息,都必须按照这个标准信封的格式来填写。同样,所有从CAN总线上收到的信息,也会被自动整理成这种标准信封的格式,方便你读取。
以下是Message的属性:
- timestamp(float):时间戳,消息被发送或接收时的时间点,用于分析时序和延迟。
- arbitration_id(int):消息的ID,决定了消息的优先级和它的“身份”。ID值越小,优先级越高。其他设备可以根据这个ID来判断是否要接收和处理这个消息。它可以是 11位 (标准帧) 或 29位 (扩展帧) 的。
print(Message(is_extended_id=False, arbitration_id=100))
Timestamp: 0.000000 ID: 0064 S DLC: 0
- data(bytearray):要发送的真正数据。它必须是 字节形式 的,通常用一个 bytes 或 bytearray 对象来表示,比如 b’\x01\x02\x03’。或者用一个列表,如 [0, 1, 2, 3, 4, 5, 6, 7]。CAN消息的数据长度最多只能是 8个字节,所以你的内容不能太长。
example_data = bytearray([1, 2, 3])
print(Message(data=example_data))
Timestamp: 0.000000 ID: 00000000 X DLC: 3 01 02 03
m1 = Message(data=[0x64, 0x65, 0x61, 0x64, 0x62, 0x65, 0x65, 0x66])
print(m1.data)
bytearray(b’deadbeef’)
m2 = Message(data=b’deadbeef’)
m2.data
bytearray(b’deadbeef’)
- dlc(int):数据长度码,这个值直接告诉你 data 字段里有多少个字节的数据。通常你不需要手动设置它,当你给 data 赋值时,dlc 会自动计算出来。比如 data 有5个字节,dlc 就是5。
m = Message(data=[1, 2, 3])
m.dlc
3
需要注意的是,DLC值不一定定义消息中的数据字节数。
它的用途取决于帧类型——对于数据帧,它表示消息中包含的数据量,而在远程帧中,它表示请求的数据量。
可能很多同学对于dlc和data length的区别不甚了了,我们通过下图来浅谈一下其中的区别:

DLC位于CAN帧的控制段,固定为 4位,可表示的范围是0到15。在经典CAN协议中,实际数据长度限制在0到8字节。CAN FD兼容CAN的数据格式,同时最大还能支持:12、16、 20、 24、 32、 48和64byte。
CAN FD DLC也是4位,表示帧中数据字节的数量。为了维持4位DLC,CAN FD使用从9到15的其余7个值来表示所使用的数据字节数(12、16、20、24、32、48、64)。
| DLC | Data Length CAN FD |
|---|---|
| 9 | 12 |
| 10 | 16 |
| 11 | 20 |
| 12 | 24 |
| 13 | 32 |
| 14 | 48 |
| 15 | 64 |
也就是说,对于CAN标准帧来说,DLC和Data Length的值相同;
对于CAN-FD拓展帧时,DLC值表示范围0-15,DataLength表示的范围0-64,且当DLC<=8时,DLC和Data Length的值相同,当DLC>8时,需要满足上述的映射表。
DATA LENGTH 指数据域实际有效载荷的字节数,由发送节点根据应用需求决定。例如:DLC=5时,实际数据长度可能是2字节。
举个例子:23 01 00 22 15 AA AA AA
DLC = 8,DATA LENGTH = 5
- channel(str or int or None):通道,如果你的CAN卡有多个通道,这个属性告诉你消息来自或要去哪个通道(如 ‘can0‘, ‘can1‘)。
- is_extended_id(bool):是否是扩展ID,这是一个True/False的选项。打勾(True)就表示你上面填的 arbitration_id 是29位的扩展格式。不勾(False)就是11位的标准格式。
print(Message(is_extended_id=False))
Timestamp: 0.000000 ID: 0000 S DLC: 0
print(Message(is_extended_id=True))
Timestamp: 0.000000 ID: 00000000 X DLC: 0
- is_error_frame(bool):是否是错误帧,标识这个消息是不是一个错误帧(用于报告总线错误)。
print(Message(is_error_frame=True))
Timestamp: 0.000000 ID: 00000000 X E DLC: 0
- is_remote_frame(bool):是否是远程帧,通常我们发送的消息是“数据帧”,里面包含了具体数据。而远程帧是一种特殊的消息,它不携带数据,只发一个ID出去,目的是请求拥有这个ID的设备发送对应的数据回来。如果你要发远程帧,就把这个设为 True,并且 data 通常为空。
print(Message(is_remote_frame=True))
Timestamp: 0.000000 ID: 00000000 X R DLC: 0
-
is_fd(bool):是否是CAN FD,标识这个消息是不是用了更先进的CAN FD协议(速度更快,数据量更大)。
-
is_rx(bool):指示该消息是发送(Tx)帧还是接收(Rx)帧
-
bitrate_switch(bool):是否切换比特率。这个参数只对于CAN FD有用,如果值为True,则表明在数据传输时使用了更高的比特率。如下图所示,我们可以为CAN FD设置两个不同的波特率,一个用于正常通信,一个用于数据传输,比如升级。

-
error_state_indicator(bool):如果这是一个CAN FD消息,这表明一个错误的活动状态。
-
_str_():CAN消息的字符串表示形式:
from can import Message
test = Message()
print(test)
Timestamp: 0.000000 ID: 00000000 X DLC: 0
test2 = Message(data=[1, 2, 3, 4, 5])
print(test2)
Timestamp: 0.000000 ID: 00000000 X DLC: 5 01 02 03 04 05
打印消息中的字段如下(按顺序排列):
timestamp,
arbitration ID,
flags,
dlc,
data。
flags字段有以下几种可能:
- 如果设置了is_extended_id属性,则为X,否则为S;
- E如果设置了is_error_frame属性,
- R,如果设置了is_remote_frame属性。
根据仲裁ID的长度(11位或29位),仲裁ID字段表示为4位或8位十六进制数。
数据字段中的每个字节(如果存在)都表示为两位十六进制数。
4.4 Listeners
4.4.1 Listener
Listener类被设计为一个“抽象”基类,这意味着它本身不能被直接实例化。它的主要作用是定义一组接口或方法规范,要求所有子类必须实现这些方法。这种设计模式在OOP(面向对象编程)中常见,用于强制子类遵守特定规则,确保代码的一致性和可扩展性。
在消息总线系统中,Listener作为基类,允许任何自定义对象(如日志处理器、数据解析器)通过继承来注册并响应消息事件,从而解耦消息生产者和消费者。
Listener对象可以通过两种模式响应消息通知,这提供了灵活性以适应不同场景:
- 默认方式:直接调用Listener对象
- 当新消息到达时,系统可以直接调用Listener实例并传入消息作为参数。
- 例如,在代码中可能表现为listener(msg)。这种方式简单直接,适用于轻量级处理,如下代码所示:
listener = SomeListener()
msg = my_bus.recv()
# now either call
listener(msg)
# Important to ensure all outputs are flushed
listener.stop()
- 调用on_message_received方法
- 系统也可以显式调用Listener的on_message_received方法来传递消息。
- 例如,listener.on_message_received(msg)。这种方式更结构化,便于在复杂逻辑中集成,如添加预处理或错误处理。如下代码所示:
listener.on_message_received(msg)
# Important to ensure all outputs are flushed
listener.stop()
Listener对象必须注册到一个或多个Notifier对象上,以确保在接收到新消息时通知它们。Notifier充当中介者(Mediator Pattern),负责监听消息总线(如CAN总线)的新消息事件,并在消息到达时自动通知所有已注册的Listener。
这种设计实现了观察者模式(Observer Pattern),Notifier作为主题(Subject),Listener作为观察者(Observer),确保了消息传递的实时性和松耦合。Notifier可以管理多个Listener,支持动态添加或移除 Listener。
如果Listener的子类没有覆盖on_message_received方法,当总线接收到消息时,系统会抛出NotImplementedError异常。通过抛出异常,系统避免了未定义行为(如空方法调用),确保所有Listeners都能正确响应消息。
classcan.Listener(*args, **kwargs)
- on_error(exc)
- 当接收线程中发生未处理的异常时,此方法被调用。它用于错误处理,确保线程安全地停止,并防止整个程序崩溃。
- 参数:exc(类型为Exception),表示导致线程停止的异常对象。例如,这可能是网络错误、解析错误或资源耗尽异常。
- 返回值:无(None)。方法应专注于错误恢复,如日志记录、通知用户或清理临时状态。
- abstract on_message_received(msg)
- 当接收到一条新消息时,此方法被调用。它是 Listener 的核心,用于处理消息内容(如过滤、转换或存储)。
- 参数:msg(类型为Message),表示从CAN总线传递的消息对象。Message通常包含ID、数据、时间戳等属性。
- 返回值:无(None)。方法应实现具体业务逻辑,如将消息写入数据库、触发事件或进行实时分析。
- stop()
- 当需要停止 Listener 时调用此方法。它执行最终任务,如确保数据持久化(将缓冲数据写入磁盘)和清理资源(如关闭文件句柄或释放网络连接)。
python-can 库提供了一些预置的 Listener,简化了常见任务。比如,CSVWriter通过on_message_received方法将消息写入文件(CSV格式)。又如,Logger(日志记录)、Printer(控制台输出)和SqliteWriter(数据库存储)等。这些监听器在stop方法中确保文件或连接正确关闭。
如果我们打算实现自定义的 Listener,则同样需要创建子类并覆盖上述所列的抽象方法,例如:
class CustomListener:
def on_message_received(self, msg):
# 自定义处理逻辑,如分析消息数据
pass
def on_error(self, exc):
# 错误处理,如发送警报
pass
def stop(self):
# 清理资源
pass
需要注意的有两点:
-
在写入和读取一个消息(例如数据或信息)时,由于某些文件格式可能不支持消息中的所有属性(如元数据、格式细节等),因此重新读取的消息可能不会与原始消息完全一致。这可能会导致信息丢失或变化,特别是在使用不兼容的文件格式时。
-
python-can 允许添加新的文件格式(如.asc格式)来读写日志文件,这通过插件机制实现。外部开发包(如Python包)可以定义自己的阅读器(用于读取文件)和写入器(用于写入文件),并通过入口点(can.io.message_reader 或 can.io.message_writer entry point)注册到系统中。这使软件更灵活,用户或开发者可以轻松扩展支持的文件格式,而不需要修改核心代码。入口点的格式是reader_name=module:classname:
- reader_name:一个标识符,通常以点开头(如.asc),表示支持的文件扩展名。
- module:classname:指定实现类的路径,例如my_package.io.asc:ASCReader,其中my_package.io.asc是模块名,ASCReader是类名。
- classname必须是can.io.generic.BaseIOHandler的具体实现类(即继承自该基类并实现其方法)。
如下代码展示了如何注册一个新的阅读器:
entry_points={
'can.io.message_reader': [
'.asc = my_package.io.asc:ASCReader'
]
},
接下来我们对 python-can 库提供了一些预置的 Listener 进行详细的介绍。
399





