1、okr
- O:做一个不用 CAN 硬件的虚拟车速表 demo
- KR1:python-can 虚拟环境单机跑
- KR2:发送端每 200ms 稳定发车速报文
- KR3::接收端实时显示车速,有终端界
- KR4:改发送值,界面 1 秒内同步变
2、准备方案分两种
方案A:因为我们没车也没CAN卡,所以只能用虚拟方案
环境
- python-can 的 VirtualBus(纯软件,无需硬件)
- 界面:简单 GUI(tkinter/PyQt)或终端打印
步骤,见上述 kr
方案B:真实的实车测试场景
环境
- 硬件:CAN 接口卡(PCAN/Kvaser)+ 连接车辆 CAN 总线
- 软件:python-can 库 + 设备驱动
- 协议:获取车速信号的 CAN ID、字节位置、比例因子(DBC 或协议文档)
步骤
- 配置 CAN 通道和波特率,建立连接
- 编码 车速值(60 km/h 按协议换算成原始值)
- 发送 CAN 报文
- 验证 在仪表或诊断工具上查看车速变化
3、核心思路
用 python-can 的 udp_multicast 虚拟总线 在同一台机器上模拟 CAN 通信,发送方编码车速报文,接收方解析并可视化显示。
┌─────────────┐ UDP Multicast ┌─────────────┐
│ 发送方脚本 │ ═════════════════► │ 接收方脚本 │
│ (模拟ECU) │ 虚拟CAN总线(channel) │ (仪表/DBS) │
│ 车速=60km/h │ │ 解析+展示 │
└─────────────┘ └─────────────┘
3.1 python-can
python-can 就是 Python 的"CAN 总线翻译官"
- CAN 总线本身是一套硬件通信协议(像说德语),你的 Python 代码说英语,python-can 就是中间的翻译,让你用简单的 Python 代码就能收发 CAN 报文,不用管底层硬件细节。
- 它封装了各种 CAN 接口卡(PCAN、Kvaser、Vector)的驱动,也提供虚拟总线(virtual/udp_multicast),没有硬件也能开发和测试。
安装
bash
pip install python-can
验证
python
import can
print(can.__version__) # 输出版本即安装成功
3.2 udp_multicast
虚拟总线像微信群,在群里发了公告,线上的人都知道。比如发了车速加到了60km/h,那仪表盘、发动机、ABS都知道了。
注意不是广播,而是多播,只有特定频段的可以收到消息,换言之,只有进了群的才能收到。
(备注:文件传输协议UDP和TCP是两大寡头,UDP类似广播,发了不管你收不收得到,但速度快;TCP类似打电话,一对一的,按顺序通信比较稳)
4、开始编码
4.1 发送方 - sender_only.py
python
import can
import time
import sys
print("=== 启动调试发送端 ===")
try:
# 1. 尝试连接虚拟总线
print("[1] 正在连接虚拟总线 'virtual_bus' ...")
# bus = can.interface.Bus(interface='virtual', channel='virtual_bus', bitrate=500000)
# 修改后 👇👇
bus = can.interface.Bus(
interface='udp_multicast',
channel='239.255.0.1', # 必须和接收端的 IP 一模一样!
bitrate=500000
)
# 1.1 bitrate在虚拟总线里是干嘛的?
# bitrate = 500000 # 500 kbps,真实CAN总线速度
# 在 udp_multicast里实际不起作用(UDP没有固定波特率),但保留是为了:
# - 接口统一(和真实CAN卡代码兼容)
# - 某些虚拟接口会模拟时序
print("[OK] 连接成功!")
# 2. 准备数据
# 2.1 帧id是什么?
# arbitration_id------帧id,就像门牌号,两个作用:
# a、识别报文类型:告诉接收者这数据是什么类型的给谁的,0x1A0 表示"这是车速报文",0x1B0可能是转速报文
# b、决定优先级:ID越小,总线竞争时优先级越高(0x100比0x500优先发送)
# 2.2 data是什么?
# data = [60, 0, 0, 0, 0, 0, 0, 0] # 8字节,每字节0-255
# CAN报文固定最多8字节。这里的写法是直接把60塞进第一个字节。
# 但注意:实际车上通常按DBC规则编码,比如:车速 = 第一个字节 × 0.5km/h
# 那么60km/h = 120 = 0x78
# data = [0x78, 0, 0, 0, 0, 0, 0, 0]
msg = can.Message(arbitration_id=0x1A0, data=[60, 0, 0, 0, 0, 0, 0, 0])
print(f"[2] 准备发送报文 ID: 0x{msg.arbitration_id:X}, 数据:{msg.data}")
# 3. 开始循环发送
count = 0
print("[3] 进入发送循环 (按 Ctrl+C 停止)...")
while True:
bus.send(msg)
count += 1
# 打印进度,让你知道它活着
if count % 5 == 0:
print(f" -> 已发送 {count} 帧,车速 60 km/h")
time.sleep(0.2) # 200ms 周期
except Exception as e: # ← 捕获所有异常(Exception是基类)
print(f"\n[ERROR] 发生严重错误,程序退出:{e}")
print(f"错误类型:{type(e).__name__}")
import traceback
traceback.print_exc() # 打印详细堆栈信息
finally:
# 不管成功还是失败,最后都会执行(清理工作)
print("\n[END] 发送端已关闭")
# locals()返回当前作用域的所有变量字典:
# 'bus' in locals() # 检查变量bus是否已定义(避免未赋值就shutdown报错)
if 'bus' in locals():
bus.shutdown()
4.2 接收方 - dashboard.py
python
import can
import time
import os
import sys
# 配置:必须和发送端一致
BUS_CONFIG = {
'interface': 'udp_multicast',
'channel': '239.255.0.1',
'bitrate': 500000
}
CAN_ID_SPEED = 0x1A0
def clear_screen():
"""清屏命令,Mac/Linux 用 clear,Windows 用 cls"""
os.system('clear' if os.name != 'nt' else 'cls')
def main():
print("🚀 正在连接 CAN 总线...")
try:
bus = can.interface.Bus(**BUS_CONFIG)
bus.set_filters([{"can_id": CAN_ID_SPEED, "can_mask": 0x7FF}])
print("✅ 连接成功!监听中...\n")
time.sleep(1)
except Exception as e:
print(f"❌ 连接失败:{e}")
return
current_speed = "--"
frame_count = 0
# 隐藏光标,让刷新更流畅
sys.stdout.write("\033[?25l")
try:
while True:
msg = bus.recv(timeout=0.1) # 稍微等一点点数据
if msg and msg.arbitration_id == CAN_ID_SPEED:
current_speed = msg.data[0]
frame_count += 1
# --- 绘制终端仪表盘 ---
clear_screen()
print("=" * 40)
print(" 🚗 虚拟车速仪表盘 (Terminal) 🚗")
print("=" * 40)
print()
# 根据速度改变颜色逻辑 (如果终端支持 ANSI 颜色)
speed_str = str(current_speed)
color_code = "\033[92m" # 绿色
if current_speed > 80:
color_code = "\033[91m" # 红色
elif current_speed > 40:
color_code = "\033[93m" # 黄色
reset_code = "\033[0m"
# 打印大字
print(f" 当前车速")
print(f" {color_code}┌─────────────┐{reset_code}")
print(f" {color_code}│ │{reset_code}")
print(f" {color_code}│ {speed_str:>3} km/h │{reset_code}")
print(f" {color_code}│ │{reset_code}")
print(f" {color_code}└─────────────┘{reset_code}")
print()
print(f" 📡 已接收帧数:{frame_count}")
print(f" 🆔 报文 ID: 0x{msg.arbitration_id:X}")
print("=" * 40)
print("按 Ctrl+C 退出")
else:
# 如果没收到新数据,至少显示初始状态,证明程序活着
if frame_count == 0:
clear_screen()
print("⏳ 等待第一帧数据... (请确保发送端已运行)")
print(f" 通道:{BUS_CONFIG['channel']}")
except KeyboardInterrupt:
print("\n👋 退出程序")
finally:
# 恢复光标
sys.stdout.write("\033[?25h")
bus.shutdown()
if __name__ == "__main__":
main()
5、运行
打开两个终端,左边先启动接收方,右边再启动发送方
bash
python dashboard.py
bash
python sender_only.py
6、效果
如下:右边发送数据,左边接收到数据并显示
7、练一练
之前速度是写死的,掌握上述内容后,可以试着把速度改"活"
写了两种"活"的模式,你可以切换试试:
|---------|--------------------------|--------------|
| 模式 | 代码 | 效果 |
| 正弦波(默认) | 60 + 60*sin(count*0.1) | 平滑加速减速,像真实驾驶 |
| 线性突变 | (count * 5)% 240 | 测试边界值、回绕、跳变 |
把之前sender_only.py的while里的代码替换即可,代码如下:
python
while True:
# ========== 动态计算车速 ==========
# 用正弦波生成 0~120 之间的车速
# count * 0.1 控制变化速度,数字越大变化越快
speed = int(60 + 60 * math.sin(count * 0.1))
# 也可以改成线性突变模式(取消下面注释):
# speed = (count * 5) % 240 # 0,5,10...235,0,5... 测试突变和回绕
# 组装报文:车速放入第1字节
msg = can.Message(
arbitration_id=0x1A0,
data=[speed, 0, 0, 0, 0, 0, 0, 0]
)
# 发送
bus.send(msg)
count += 1
# 打印状态(每5帧打印一次,避免刷屏)
if count % 5 == 0:
print(f" -> 帧[{count:4d}] 车速: {speed:3d} km/h")
time.sleep(0.2) # 200ms周期