1. 为什么需要 CANviz
刚接上设备时,用 candump 抓到一条报文 123#112233603B000000,知道第 4、5 个字节是转速,手算一次并不难。同一个 ID 如果每秒发几十次,想看转速在哪一刻掉下去、掉了多久,就得把这些字节按时间排好再看。接下来如果要盯住几个 ID、看信号随工况怎么变,浏览器里的表格和曲线更顺手。下面在虚拟 CAN 上走一遍这个过程,把 60 3B 解成 1900 rpm,再录一段转速变化。
调试时可以两个窗口并开。网页看趋势,终端留逐帧记录。比如转速曲线多了一个尖峰,翻回同一时刻的原始字节,就能判断是设备真的报了那个数,还是解码时遇到了状态值。
2. CANviz 能做什么
CANviz 是开源的 Web CAN 分析工具。它按 ID 汇总收到的帧,可以过滤报文、查看载荷和计数。导入 DBC 后,同一条报文能展开成带单位的信号,也能添加到实时曲线。工具还提供日志录制、原始帧回放和 J1939 查看功能。下文使用 CANviz 0.3.1 。 项目主页 · 下载页
Linux 上的数据先到 SocketCAN 接口,再由 CANviz 接收并推送给浏览器。物理 CAN 接口和 vcan0 都可以做输入,浏览器只需能访问 CANviz 服务。

3. 在 Linux 上安装 CANviz
准备支持 SocketCAN 的 Linux 和 Python 3.10 或更高版本 。做下面的模拟实验还需要内核支持 vcan,并安装 can-utils。先用 python3 -V 看版本。Ubuntu 或 Debian 上如果缺少 venv 组件或 CAN 工具,可以用 sudo apt install python3-venv can-utils 安装对应软件包。
临时调试用 venv 很方便。Python 依赖集中在本次目录,收工时可以一起清理。下面用 /userdata/testapp/canviz-test 作为示例 BASE。普通账号没有写权限时,把第一行换成自己的可写目录,例如 BASE="$HOME/canviz-test"。
创建环境
在同一个 Bash 终端执行,记下打印出来的 RUN。每次安装会得到一个新目录,日志和临时文件也放在里面。
bash
BASE="/userdata/testapp/canviz-test"
mkdir -p -- "$BASE/runs" || exit 1
BASE=$(realpath -e -- "$BASE") || exit 1
RUN=$(mktemp -d -- "$BASE/runs/canviz.XXXXXXXX") || exit 1
printf 'RUN=%s\n' "$RUN"
mkdir -- "$RUN/tmp" "$RUN/replay-uploads" "$RUN/logs" || exit 1
python3 -m venv "$RUN/.venv" || exit 1
. "$RUN/.venv/bin/activate" || exit 1
TMPDIR="$RUN/tmp" "$RUN/.venv/bin/python" -m pip install \
--no-cache-dir 'canviz==0.3.1' || exit 1
"$RUN/.venv/bin/python" -m pip check
"$RUN/.venv/bin/python" -m pip show canviz
pip check 应显示依赖没有冲突,pip show 中的版本为 0.3.1。安装用了 --no-cache-dir,这次下载的包不会留在共享 pip 缓存里。
准备接口并打开网页
先用虚拟接口跑一遍。下面只在缺少 vcan0 时创建它,并把本轮目录名写进接口 alias,方便结束时认领。已有的 vcan0 要确认是 UP 的虚拟接口,且当前可供本次实验使用。如果提示不支持 vcan,可尝试 sudo modprobe vcan,再检查内核是否提供该模块。
bash
if ! ip link show dev vcan0 >/dev/null 2>&1; then
sudo ip link add dev vcan0 type vcan &&
sudo ip link set dev vcan0 alias "${RUN##*/}" &&
sudo ip link set dev vcan0 up || exit 1
fi
[[ -n $(ip -o -d link show dev vcan0 up type vcan) ]] || exit 1
在该终端前台启动 CANviz。服务工作目录设为 RUN,录制文件会写到它的 logs/。
bash
cd "$RUN" || exit 1
TMPDIR="$RUN/tmp" canviz_UPLOAD_DIR="$RUN/replay-uploads" \
"$RUN/.venv/bin/canviz" serve \
--interface socketcan --channel vcan0 \
--host 127.0.0.1 --port 8080 --headless
同机打开 http://127.0.0.1:8080/。--headless 只跳过自动打开浏览器。等启动终端显示 Auto-connected 后,在第二个终端运行 curl -fsS http://127.0.0.1:8080/status,确认 connected 为 true,channel 为 vcan0。
在远程 Linux 上运行时,保持服务监听回环地址,从电脑走已授权的 SSH 转发。把命令中的用户和主机名换成实际值,连接后打开 http://127.0.0.1:18080/。
bash
ssh -N -L 127.0.0.1:18080:127.0.0.1:8080 user@host
这套 venv 安装方法也适合具备相应 Python 环境、CAN 接口和驱动的 Linux 工业设备,例如映翰通 EC312。现场采集时,将启动命令中的通道换成设备上已配置好只监听的 SocketCAN 接口。
4. 用完怎么卸载
做完后面的实验再回到这里。先停掉网页中的发送定时器、录制和回放,下载要留存的日志。回到启动终端按 Ctrl+C ,远程使用时再关闭本轮 SSH 转发。用 ss -ltnp 'sport = :8080' 确认本轮服务已退出。
在原终端沿用 BASE 和 RUN。下面先核对 RUN 的实际路径、目录名和所有者,再交互删除本轮 venv 与临时文件。确认这些目录没有挂入共享存储后执行。
bash
[[ -n "${BASE:-}" && -n "${RUN:-}" ]] || exit 1
if [[ ${VIRTUAL_ENV:-} == "$RUN/.venv" ]]; then
deactivate
fi
cd "$BASE" || exit 1
run_path=$(realpath -e -- "$RUN") || exit 1
[[ "$run_path" == "$RUN" && "${run_path%/*}" == "$BASE/runs" &&
"${run_path##*/}" =~ ^canviz\.[[:alnum:]]{8}$ &&
-d "$run_path" && -O "$run_path" ]] || exit 1
rm -rI --one-file-system -- "$run_path"/{.venv,tmp,replay-uploads} || exit 1
du -sh -- "$run_path"
日志仍在 $RUN/logs/,可归档后按需删除。要收回虚拟接口,用 ip -d link show dev vcan0 确认它的类型是 vcan,alias 等于本轮的 ${RUN##*/},并确认其他程序已经不用它,再执行 sudo ip link delete dev vcan0。如果这次复用的是已有接口,跳过删除。
5. 用 vcan0 看报文、过滤和发送
在设备上的另一个终端发模拟帧。先确认网页连的是 SocketCAN (Linux) 的 vcan0,菜单里的 virtual (testing) 是进程内总线,外部 cansend 的帧进不到那里。以下发帧、定时发送和回放都使用虚拟接口。
可以先在一个终端运行 candump -L -n 3 vcan0,再从另一个终端发送。
bash
[[ -n $(ip -o -d link show dev vcan0 up type vcan) ]] || exit 1
cansend vcan0 123#1122334455667788
cansend vcan0 18F004E5#FFFF91603BFFFFFF
cansend vcan0 123#AA22334455667788
网页里应该有 0x123 和 0x18F004E5 两行。前者是 11 位标准帧,后者是 29 位扩展帧。两条样例的 DLC 都是 8,表示有 8 个数据字节。最后一帧到达后,0x123 的首字节会变成 AA,Count 增加。要找前一帧,就看刚才的 candump 输出。
打开 Filters → ID Filter ,填 0x100-0x1FF。表格会留下 0x123,扩展帧被筛掉。点 Clear filters 即可恢复。
想从网页发一帧,在 Send Frame 里填 ID 0x321、DLC 8、数据 01 23 45 67 89 AB CD EF,保持 29-bit 未勾选。用 candump -L -n 1 vcan0,321:7FF 等待,然后点击上箭头 Send once ,终端应收到 321#0123456789ABCDEF。同一面板也能设 Interval 做周期发送,用完点 Stop timer。
录制时点 Record → ● Record ,再发一条帧。点 ■ Stop 后可以下载 CSV 或 ASC。录制好的文件也会留在服务端。想重新看一轮报文,可在 Connection 区点 Clear 清空表格和曲线缓冲。
6. 用 DBC 把 60 3B 解成 1900 rpm
DBC 说明某个 ID 的数据区里,哪些位对应什么信号。这个例子给 0x123 定义一个转速信号,从第 4 字节的最低位开始读取 16 位。将下方保存成 engine_demo.dbc。
dbc
VERSION "CANviz teaching fixture"
NS_ :
BS_:
BU_: Simulator Viewer
BO_ 291 DemoEngine: 8 Simulator
SG_ EngineSpeed : 24|16@1+ (0.125,0) [0|8191.875] "rpm" Viewer
BO_ 后的十进制 291 对应 0x123。24|16@1+ 表示从 bit 24 取 16 位,@1 是 Intel 小端,+ 表示无符号。括号里的 0.125 和 0 分别是 Factor 和 Offset 。第 4、5 字节为 60 3B 时,小端原值是 0x3B60 = 15200,乘 0.125 再加 0,得到 1900 rpm。

展开 DBC / Signals ,点击 Drop .dbc file or click to browse 选中 DBC,开启 Show decoded signals。导入后再发一帧,让它走一次接收和解码。
bash
[[ -n $(ip -o -d link show dev vcan0 up type vcan) ]] || exit 1
cansend vcan0 123#112233603B000000
展开 0x123 这行,DemoEngine.EngineSpeed 应显示 1900.000 rpm。如果字节看起来对,但数值差了很多,可以先拿 60 3B 手算一遍,检查有没有把高低字节读反。
7. 画实时曲线,再用日志重建
收到转速后,打开 Plot → + add signal ,选 DemoEngine.EngineSpeed,窗口设为 30s 。点 Record → ● Record,切回 Plot。另开设备终端运行下面的循环,五个载荷依次对应 800、1200、1900、1500 和 1000 rpm。
bash
[[ -n $(ip -o -d link show dev vcan0 up type vcan) ]] || exit 1
for data in 0019 8025 603B E02E 401F; do
for ((i=0; i<8; i++)); do
cansend vcan0 "123#112233${data}000000" || exit 1
sleep 0.2
done
done
曲线应该先升到 1900 rpm,再降到 1000 rpm。用鼠标拖选可以放大一段,双击回到实时滚动。点 Record → ■ Stop,下载 CSV 或 ASC。在第二个设备终端做日志操作时,先把安装终端打印的 RUN 路径复制过来。
bash
read -r -p '粘贴 RUN 的绝对路径: ' RUN
如果只想看当时的原始帧,打开 Replay 并上传刚才的 CSV。0.3.1 的内置回放直接把帧送到网页,DBC 解码流程不会重新执行。此时能看到报文,Plot 仍可能是空的。
想重建转速曲线,停止内置 Replay,保留 DBC,把 CSV 转成 canplayer 能读的格式,再送回 vcan0。下面的代码按文件名选取最近一次录制。转换适用于本例的经典 CAN 数据帧,并保留时间戳和扩展帧标志。
bash
logs=("$RUN"/logs/canviz_*.csv)
CSV=${logs[-1]}
[[ -f "$CSV" ]] || { printf '%s\n' '先完成一轮录制' >&2; exit 1; }
REPLAY_LOG=$(mktemp -- "$RUN/tmp/replay.XXXXXXXX.log") || exit 1
python3 - "$CSV" "$REPLAY_LOG" <<'PY' || exit 1
import csv
import math
import sys
frames = []
with open(sys.argv[1], newline="", encoding="utf-8-sig") as src:
for row in csv.DictReader(src):
stamp = float(row["timestamp"])
can_id = int(row["id"], 16)
data = bytes.fromhex(row["data"])
flag = row["is_extended_id"]
if not math.isfinite(stamp) or flag not in ("0", "1"):
raise SystemExit("CSV 时间戳或扩展帧标志无效")
width, limit = (8, 0x1FFFFFFF) if flag == "1" else (3, 0x7FF)
dlc = int(row["dlc"])
if not 0 <= can_id <= limit or len(data) != dlc or dlc > 8:
raise SystemExit("需要经典 CAN 数据帧")
token = f"{can_id:0{width}X}#{data.hex().upper()}"
frames.append(f"({stamp:.6f}) vcan0 {token}\n")
if not frames:
raise SystemExit("CSV 中没有数据帧")
with open(sys.argv[2], "w", encoding="ascii") as dst:
dst.writelines(frames)
PY
[[ -n $(ip -o -d link show dev vcan0 up type vcan) ]] || exit 1
canplayer -I "$REPLAY_LOG" vcan0=vcan0
新帧经 SocketCAN 到达,CANviz 会重新匹配 DBC。若刚清空过视图,收到第一帧后在 Plot 重新添加转速信号。这里复建的是按原间隔到达的信号变化,曲线横轴使用这次接收的时间。手里已有 candump -L 保存的同类日志时,可以直接用 canplayer,省去 CSV 转换。

8. J1939 与 FMS 数据怎么读
先从 29 位 ID 读出 PGN
看到 18F004E5#FFFF91603BFFFFFF 时,先拆 ID。优先级是 6,源地址 SA 是 0xE5,PGN 是 0xF004(61444),即 EEC1。PGN 标识参数组,SPN 标识组内的具体参数。EEC1 里的 SPN 190 是发动机转速,SPN 513 是实际扭矩百分比。

拆 PGN 时先看 PF 。PF 小于 0xF0 时是 PDU1,PS 是目的地址,算 PGN 时清零低字节。比如 0x18EA25E5 的目的地址是 0x25,PGN 是 0xEA00。PF 大于等于 0xF0 时是 PDU2,PS 参与 PGN 计算,EEC1 就属于这一类。
python
for can_id in (0x18EA25E5, 0x18F004E5):
pf = (can_id >> 16) & 0xFF
ps = (can_id >> 8) & 0xFF
pgn = (can_id >> 8) & 0x3FFFF
if pf < 0xF0:
pgn &= 0x3FF00
print(f"PGN=0x{pgn:X}, PS=0x{ps:02X}, SA=0x{can_id & 0xFF:02X}")
在网页右侧打开 J1939 → Enable Decoder,可以查看收到的 PGN、节点和近期记录。扩展帧也可能承载其他协议,分析实车数据时结合接口说明和报文内容来判断。
用模拟 FMS V05 帧看转速、车速和油量
商用车的 CAN 型 FMS 接口通常用来读取一部分 J1939 数据,例如转速、车速和累计里程。手头有车厂给的 DBC 时,可以用它把载荷换算成工程值。下面是一份 FMS V05 教学 DBC,按完整 CAN ID 匹配。保存为 fms_demo.dbc,在网页里卸载前一个 DBC,再导入这份。
dbc
VERSION "CANviz teaching fixture"
NS_ :
BS_:
BU_: Simulator Viewer
BO_ 2565866725 FMS_F004: 8 Simulator
SG_ EngineSpeed : 24|16@1+ (0.125,0) [0|8031.875] "rpm" Viewer
SG_ ActualTorque : 16|8@1+ (1,-125) [-125|125] "%" Viewer
BO_ 2566844901 FMS_FEF1: 8 Simulator
SG_ WheelSpeed : 8|16@1+ (0.00390625,0) [0|250.99609375] "km/h" Viewer
BO_ 2566847717 FMS_FEFC: 8 Simulator
SG_ FuelLevel1 : 8|8@1+ (0.4,0) [0|100] "%" Viewer
BO_ 2566841829 FMS_FEE5: 8 Simulator
SG_ EngineHours : 0|32@1+ (0.05,0) [0|210554060.75] "h" Viewer
BO_ 2566832613 FMS_FEC1: 8 Simulator
SG_ HighResDistance : 0|32@1+ (0.005,0) [0|21055406.075] "km" Viewer
BO_ 2566842853 FMS_FEE9: 8 Simulator
SG_ TotalFuelUsed : 32|32@1+ (0.5,0) [0|2105540607.5] "L" Viewer
扩展帧在 DBC 的 BO_ ID 中置 bit 31,例如 0x18F004E5 写成 0x98F004E5,十进制为 2565866725。总线上仍发原始 ID。换一台车时,优先级或源地址变了,PGN 可能相同,DBC 匹配的完整 ID 则要重新核对。
保持 CANviz 连接 vcan0,清空旧帧后发下面六条。字节位置从 1 数,下面的数值字段按无符号小端读取。
bash
[[ -n $(ip -o -d link show dev vcan0 up type vcan) ]] || exit 1
cansend vcan0 18F004E5#FFFF91603BFFFFFF
cansend vcan0 18FEF1E5#FF004BFFFFFFFFFF
cansend vcan0 18FEFCE5#FF7DFFFFFFFFFFFF
cansend vcan0 18FEE5E5#204E0000FFFFFFFF
cansend vcan0 18FEC1E5#00127A00FFFFFFFF
cansend vcan0 18FEE9E5#FFFFFFFF204E0000
| PGN | SPN 与字节 | 解码结果 |
|---|---|---|
F004 |
190,字节 4--5 | 15200 × 0.125 = 1900 rpm |
F004 |
513,字节 3 | 145 − 125 = 20% 扭矩 |
FEF1 |
84,字节 2--3 | 19200 ÷ 256 = 75 km/h |
FEFC |
96,字节 2 | 125 × 0.4 = 50% 油量 |
FEE5 |
247,字节 1--4 | 20000 × 0.05 = 1000 h |
FEC1 |
917,字节 1--4 | 8000000 × 0.005 = 40000 km |
FEE9 |
250,字节 5--8 | 20000 × 0.5 = 10000 L |
看转速时要识别状态值。例如 SPN 190 的 00 FE 对应原值 0xFE00,表示错误。FF FF 表示不可用。通用 DBC 解码可能照常换算,把它们画成 8128 和 8191.875 rpm。遇到类似尖峰,先看原始字节,统计时把这些状态点筛掉。接车时以车辆网关实际提供的数据和字段定义为准。
从 BAM 里拼出 VIN
VIN 对应 PGN 0xFEEC 和 SPN 237。这里用虚构标识 ZZZTEST0123456789*,末尾 * 是分隔符,总长 18 字节。经典 CAN 的一帧装不下,所以用 BAM 广播。TP.CM 先宣布目标 PGN、总长度和包数,再由三条 TP.DT 按序号送出数据。
bash
[[ -n $(ip -o -d link show dev vcan0 up type vcan) ]] || exit 1
cansend vcan0 1CECFFE5#20120003FFECFE00 && sleep 0.1 &&
cansend vcan0 1CEBFFE5#015A5A5A54455354 && sleep 0.1 &&
cansend vcan0 1CEBFFE5#0230313233343536 && sleep 0.1 &&
cansend vcan0 1CEBFFE5#033738392AFFFFFF
首帧的数据是 20 12 00 03 FF EC FE 00。20 表示 BAM,12 00 是小端长度 18,03 是三包,最后三个字节 EC FE 00 给出目标 PGN。三条 DT 的首字节是 1、2、3,拼接其余字节后只取前 18 个,去掉填充的 FF,再按 ASCII 读取。

打开 J1939 → BAM Log 查看重组结果。需要完整数据时,在设备终端运行 curl -fsS http://127.0.0.1:8080/j1939/status,看 recent_bam 里的 PGN 65260、长度 18 和十六进制载荷。遇到缺包时,先在 candump 里按源地址、时间和包序号对齐同一次广播。
DM1 先看原帧,再核对故障码
DM1 的 PGN 是 0xFECA,用于报告活动故障。前两字节 04 FF 是灯状态和闪烁状态,中间 64 00 01 03 是一条 DTC,最后两字节是填充。
bash
[[ -n $(ip -o -d link show dev vcan0 up type vcan) ]] || exit 1
cansend vcan0 18FECAE5#04FF64000103FFFF
对转换方法 CM=0,可用这几行从 DTC 提取 SPN、故障模式 FMI 和出现次数 OC。
python
a, b, c, d = bytes.fromhex("64 00 01 03")
cm = d >> 7
spn = a | (b << 8) | ((c >> 5) << 16)
fmi = c & 0x1F
oc = d & 0x7F
print(spn, fmi, oc, cm) # 100 1 3 0
0.3.1 的 DM1 面板对这条样例会解错。遇到该版本的结果,保留原帧并按字节独立核对 DTC。若原帧的 CM 为 1,查 ECU 的旧转换方法。现场判断还要核对报文来自哪个 ECU,以及厂商对 SPN 和 FMI 的说明。
9. 日常调试还能怎么用
- 盯住一个 ID。 先用过滤缩小范围,在操作设备前后分别看 Data 和 Count。网页上看到某个字节在变时,把原始帧也留一份,后面可以逐条比较。
- 把两个信号放到同一窗口。 比如把转速和扭矩一起画出来,再记下加负载的时刻。对比哪个信号先变、哪个恢复得慢,能更快找到需要回看的一段数据。
- 从日志里算周期。 用
candump -L的采集时间戳筛出同一 ID 和来源的帧,计算相邻间隔。连续少了几帧、突然扎堆或周期拉长,都比只看网页上的 Last Seen 更容易定位。
10. 三个实战应用
看一次转速升降
用第 7 节的五档数据练一遍。先记住 1900 rpm 对应的原始字节,打开 Plot 再录制,等转速降到 1000 rpm 后停止。回放时核对峰值和先后顺序。排查现场掉速也可以这么做,把异常时间记下来,再查当时的转速和相关工况信号。
从 FMS 接口读取车辆数据
接入车厂允许使用的 FMS 口后,先抓几帧确认源地址和 PGN,再导入车辆对应的 DBC。起步时可以同时看 EEC1 转速和车速,核对车辆从静止到移动时两个信号的变化。油量、小时数和里程适合抽样核对。碰到读数异常,先分清总线上没收到对应 PGN,还是字段本身给出了错误或不可用状态。
记录工业设备的一次通信异常
以 EC312 等具备 SocketCAN 的 Linux 边缘设备为例,让 CANviz 连接到已配置好只监听的接口,通过 SSH 转发在电脑上查看。平时用 ID 过滤盯住相关报文,故障发生前后保存一段日志,并记下时间和操作。把日志带回分析机后再回放和检查信号。示例里的发帧和注入回放都留在独立的 vcan0 上。
11. 常见问题与总结
- 安装完启动失败。 确认调用的是
$RUN/.venv/bin/canviz,用pip check检查依赖,再看启动终端是否提示端口占用。 - 网页打不开。 在运行服务的 Linux 上先请求
/status。远程设备能响应而电脑打不开时,检查 SSH 转发的端口和访问地址。 - 网页没有帧。 先确认
candump能收到,接着检查 SocketCAN 通道和过滤条件。 - DBC 解码不对。 按原始帧核对完整 ID、帧格式、载荷长度和字节序。刚导入 DBC 时再收一条新帧。
- 曲线空白。 展开帧确认已经有解码值,再检查 Plot 选中的信号、时间窗口和滚动状态。
- Replay 有帧但没有信号。 内置回放用于查看原始帧。要重画曲线,按第 7 节回放到
vcan0。
调试收工前,把原始日志、用过的 DBC 和异常时间放在一起,再清理这轮 venv。下次遇到同样的问题,可以直接从那几秒的报文重建曲线,核对当时到底收到了什么。