背景
结合智能驾驶,can通信,线控调试。会想到一些业务需求:can数据采集记录,can数据离线可视化分析,can数据切割,can和udp数据格式互转,车辆控制调试界面,车辆线控性能分析,dbc和xlsx互转,dbc生成c代码等等(更多需求可提到评论区里)。
需求分析
can数据采集记录
windows中canoe中Measurement Setup的Logging模块可实现asc和blf格式的can数据记录。linux中使用can卡(kvaser,pcan或者nvidia orin的can口),实现一样的功能。
需求实现
1. can卡驱动安装
kvaser驱动下载 Downloads - Kvaser Drivers, Documentation, Software, more...
pcan驱动下载 Linux PCAN Driver: Overview
解压安装包之后看里面的README文档进行安装。要区分是否为socketcan方式安装
nvidia orin上是socketcan, socketcan启动方式是
# 虚拟can通道
sudo modprobe vcan
sudo ip link add dev can0 type vcan
sudo ip link set up can0
# ip a可以查看是否生效
# 打开真实can通道
# 普通can例子
sudo ip link set can0 up type can bitrate 500000
# canfd例子
sudo ip link set can0 up type can bitrate 500000 sample-point 0.75 dbitrate 2000000 dsample-point 0.8 fd on
# 用can-utils验证
sudo apt-get install can-utils
#接受can信息
candump can0
#发送can信息
cansend can0 123#DEADBEEF
2. python-can实现数据记录基本功能
py库安装 pip3 install python-can
import can
# 初始化can通道,要注意can通道名称,波特率,是否为canfd
bus_pcan = can.Bus(channel="PCAN_USBBUS1", interface='pcan', bitrate=500000)
# bus_socketcan = can.interface.Bus(channel='can0', bustype='socketcan')
# can.Logger是python-can的内置方法,通过识别文件名后缀来记录成对应的文件格式
asc_filepath = "./xxx.asc"
asc_logger = can.Logger(path)
# buses是个list,需要监听哪路,就把哪路append进来
buses = [bus_pcan]
# listeners是个list,用来存放回调函数,这些函数的输入参数是can message
# def print_msg(msg: can.Message):
# print(msg)
# listeners = [asc_logger, print_msg]
listeners = [asc_logger]
# can.Notifier是让buses内的can通道,在收到can msg后就会调用listeners里的函数
notifier = can.Notifier(buses, listeners)
# notifier是异步运行的,所以需要阻塞主线程,要不然程序会不等待就退出
while True:
cmd = input()
if cmd == "quit"
notifier.stop()
bus_pcan.shutdown()
break
3. 数据记录方式自定义
真实的业务场景需要考虑更多,下面摘取一些python片段来简述某些业务场景的实现:
1. 记录数据前需要检测can线是否已连接,
车辆行驶中的抖动容易让DB9接头松脱,此时数据记录设备检测到的负载率就很低。
如果是不同波特率的两路can线掉落了,线上也没有标签,人为再接回去就有可能插反了。如果插反了,总线就会有很多错误帧出现。
如果两路波特率是一样的,那可以通过线上特有的can报文ID来区分。
import threading
#检查can连接状态是定时检测,需要跑一个多线程来异步进行
check_bus_status_thread = threading.Thread(target=self.check_bus_status, args=())
check_bus_stats_run = True
def check_bus_stats():
while check_bus_stats_run:
...
2. 每次启动记录都能产生新的文件夹,
import os
def mkdir_log_folder(folder):
is_exists = os.path.exists(folder)
if not is_exists:
os.makedirs(folder)
3. log文件名能反映当时的时间戳,
import os
import time
def get_time_stamp(ct):
local_time = time.localtime(ct)
date_head = time.strftime("%Y_%m_%d_%H_%M_%S", local_time)
date_secs = (ct - int(ct)) * 1000
time_stamp = "%s_%03d" % (date_head, date_secs)
return time_stamp
log_folder = "./"
file_name = os.path.join(log_folder, ('log_' + get_time_stamp(time.time()) + ".asc"))
4. 记录的文件大小不能太大,记录的文件时长是指定周期的,
import can
import os
import psutil
import time
from apscheduler.schedulers.background import BackgroundScheduler
# 这是后台运行的一个线程,也是需要阻塞主线程来保证它不退出
def logger_a_b():
sched = BackgroundScheduler()
@sched.scheduled_job('interval', id='can_logger_a_b', seconds=120)
def change_logger():
log_run_folder = "./"
path = os.path.join(log_run_folder, ('log_' + get_time_stamp(time.time()) + ".asc"))
if asc_logger_a in notifier.listeners:
asc_logger_b = can.Logger(path)
notifier.add_listener(asc_logger_b)
notifier.remove_listener(asc_logger_a)
time.sleep(2)
asc_logger_a.stop()
elif asc_logger_b in notifier.listeners:
asc_logger_a = can.Logger(path)
notifier.add_listener(asc_logger_A)
notifier.remove_listener(asc_logger_B)
time.sleep(2)
asc_logger_b.stop()
if psutil.disk_usage(log_run_folder).percent > 90:
print("disk_usage more than 90%, please stop logger")
return sched
logger_a_b_thread = logger_a_b
5. 实时截取车辆出现问题时前后几分钟的那一段数据并另存为,
import can
import time
issue_asc_path = "./xxx.asc"
issue_point_time = time.time()
issue_asc = can.ASCWriter(issue_asc_path)
for asc_file in asc_files:
with can.ASCWriter(asc_file) as file:
for msg in file:
# 取issue_point_time的前30s和后10s内的数据
if msg.timestamp >= issue_point_time - 30:
issue_asc.on_message_received(msg)
if msg.timestamp >= issue_point_time + 10:
break
6. 退出记录时python脚本能保存最后一份文件并关闭can通道等等。
import signal
def on_kill(signum, frame):
# 关闭回调函数集
notifier.stop()
# 关闭上面提到的按时长滚动记录后台线程
logger_a_b_thread.shutdown()
# 关闭所有can通道
for bus in bues:
bus.shutdown()
print("can logger shutdown")
# 程序被ctrl C或正常退出时会执行on_kill函数
signal.signal(signal.SIGTERM, on_kill)
signal.signal(signal.SIGINT, on_kill)