ClaudeでCANバスのログを解析 — candumpから異常フレームを探す
candumpで取ったCANログをpython-canで読み、cantoolsのDBCで解読して、周期の欠落・未知ID・範囲外の値を拾う手順です。Claudeには集計結果だけを渡します。
CANバスのログは、1台の車両や装置でも1分で数千〜数万フレームになります。candumpで取ったログを丸ごとClaudeに貼っても、16進数の羅列からは何も読み取れません。向いているのは、ログを読む解析スクリプトをClaudeに書かせ、手元で走らせて、IDごとの周期・欠落・範囲外の値といった集計結果だけを読ませるやり方です。
この記事では、Linuxのcandumpが出すログをpython-canで読み、DBCファイルで信号に解読して異常フレームを拾うまでを、再現できる合成ログで示します。実機が無くても、仮想CANインターフェース(vcan)で同じ流れを試せます。
解析の全体像 — ログは手元、Claudeには集計だけ
流れは4段です。
candumpでログを取る(または手元の既存ログを使う)- python-canの
LogReaderで1フレームずつ読み、IDごとに集計する - DBCがあれば
cantoolsで信号値に解読し、範囲外を拾う - スクリプトの出力だけをClaudeに渡し、原因の仮説を挙げさせる
Claudeに任せるのは2〜3のスクリプト作成と、4の読み解きです。ログそのものを会話に載せないので、車両や装置の通信内容が不要に外へ出ることも避けられます。同じ考え方の別分野の例として、PLCのログCSVを解析する手順があります。
candumpのログ形式を知っておく
candumpの画面表示とログファイルの形式は別物です。スクリプトで読ませるのは、ファイル向けの形式です。
| 指定 | 動き |
|---|---|
candump -l can0 | 動きログをファイルに保存する(自動で名前が付く) |
candump -f 名前 can0 | 動きファイル名を指定して保存する |
candump -L can0 | 動きログ形式で標準出力に出す |
candump -e can0 | 動きエラーフレームを人が読める形で表示する |
ログ形式の1行は、タイムスタンプ、インターフェース名、ID#データの並びです。python-canのドキュメントには(0.0) vcan0 001#8d00100100820100という例があります。IDが8桁のときは29bitの拡張ID、3桁のときは標準IDです。
エラーフレームだけを取りたい場合は、candumpのヘルプにある次の指定が使えます。データフレームを除き、エラーフレームだけを保存する書き方です。
candump -l any,0~0,#FFFFFFFFデータフレームも一緒に残すなら、0~0を0:0に変えます。異常の前後を見たいときは、後者で全部取る方が後から切り分けやすくなります。
vcanで再現できる環境を作る
実機のバスがなくても、Linuxの仮想CANドライバ(vcan)で試せます。python-canのSocketCANの節にある手順は次のとおりです。
sudo modprobe vcan
sudo ip link add dev vcan0 type vcan
sudo ip link set vcan0 up
cansend vcan0 123#DEADBEEFcansendはcan-utilsに含まれる送信ツールです。can-utilsには、ほかにもログを再生するcanplayer、ランダムなトラフィックを作るcangen、バス負荷を測るcanbusloadなどが入っています。SocketCANはLinuxのCANサブシステムなので、この節の手順はLinuxが前提です。次節以降のログ解析は、ファイルを読むだけなのでOSを問いません。
vcanはあくまで再現用の経路です。bus-off(エラーが多すぎてバスから切り離された状態)は、実機のcan0で起きる話として扱います。python-canのドキュメントでは、自動復帰にsudo ip link set canX type can restart-ms 100を使うか、アプリがエラーフレームを監視してip link set canX type can restartで復帰させる方法が示されています。復帰の操作自体もエラーフレームを1つ生みます。
解析対象の合成ログを作る
手順を確かめるため、60秒分の合成ログを作ります。実在する車両や装置のものではありません。
0x343(WheelSpeed): 100msごとの周期メッセージ0x1F4(Coolant): 1秒ごとの冷却水温- 異常は4種類を仕込む: 周期の欠落、DLCが短いフレーム、DBCにないID、値の範囲外
DBCは次の最小構成です。信号の[0|300]や[-40|120]が範囲の定義で、後でこれを使って範囲外を拾います。
BO_ 835 WheelSpeed: 8 ECU
SG_ FrontLeft : 0|16@1+ (0.1,0) [0|300] "km/h" Vector__XXX
SG_ FrontRight : 16|16@1+ (0.1,0) [0|300] "km/h" Vector__XXX
BO_ 500 Coolant: 2 ECU
SG_ CoolantTemp : 0|8@1+ (1,-40) [-40|120] "degC" Vector__XXXログの先頭は次のようになります。時刻はUNIX時間の秒です。
(1609779922.000000) vcan0 343#E503E50300000000
(1609779922.050000) vcan0 1F4#8200
(1609779922.100000) vcan0 343#EC03EC0300000000解析スクリプトを書かせる
Claudeへの依頼は、ログの先頭数行とDBCを渡し、出力の形を指定します。ログ全体は渡しません。
candump-demo.log(candump -L 形式)と demo.dbc を読む Python スクリプトを
scan_can.py に書いて。python-can と cantools を使うこと。
出すのは次の2つだけ。
1. IDごとの件数・周期の中央値・最大間隔・DLCの種類・DBCにあるか
2. 異常の一覧(エラーフレーム/デコード失敗/範囲外/周期の欠落/未知ID)
生のフレームは出力しない。周期の何倍で欠落とするかは引数にする。こうして書かれるスクリプトは、次の骨格になります。can.LogReaderは拡張子で読み取り方を選び、.logはcandump -L形式を読むCanutilsLogReaderになります。gzip圧縮したファイル(.log.gzなど)も、元の拡張子が対応していれば読めます。
import statistics
import sys
from collections import defaultdict
import can
import cantools
log_path, dbc_path = sys.argv[1], sys.argv[2]
gap_factor = float(sys.argv[3]) if len(sys.argv) > 3 else 3.0
db = cantools.database.load_file(dbc_path)
times = defaultdict(list)
dlcs = defaultdict(set)
issues = []
for msg in can.LogReader(log_path):
times[msg.arbitration_id].append(msg.timestamp)
dlcs[msg.arbitration_id].add(msg.dlc)
if msg.is_error_frame:
issues.append((msg.timestamp, "error_frame", f"id=0x{msg.arbitration_id:X}"))
continue
try:
m = db.get_message_by_frame_id(msg.arbitration_id)
except KeyError:
continue # DBCにないID。後で集計側で拾う
try:
decoded = m.decode(msg.data)
except Exception as e:
issues.append((msg.timestamp, "decode_error", f"{m.name} dlc={msg.dlc}: {e}"))
continue
for s in m.signals:
v = decoded[s.name]
if s.minimum is not None and s.maximum is not None \
and not (s.minimum <= v <= s.maximum):
issues.append((msg.timestamp, "out_of_range",
f"{m.name}.{s.name}={v} not in [{s.minimum},{s.maximum}]"))
# IDごとの周期・最大間隔・未知ID・欠落の判定と表示は省略省略した後半は、IDごとにタイムスタンプの差を取り、中央値を周期として、周期のgap_factor倍を超えた間隔をgapとして記録する処理です。DBCに存在しないIDも、ここでunknown_idとして記録します。
実行結果 — 4種類の異常がどう見えるか
python-can 4.6.1、cantools 45.1.0、Python 3.12.2で、合成ログに対して実行した出力です。
python3 scan_can.py candump-demo.log demo.dbcid count period_ms max_gap_s dlc in_dbc
0x1F4 60 1000.0 1.00 [2] True
0x343 577 100.0 2.40 [4, 8] True
0x7FF 3 10.0 0.01 [2] False
+ 20.00s decode_error WheelSpeed dlc=4: Wrong data size: 4 instead of 8 bytes
+ 20.10s decode_error WheelSpeed dlc=4: Wrong data size: 4 instead of 8 bytes
+ 20.20s decode_error WheelSpeed dlc=4: Wrong data size: 4 instead of 8 bytes
+ 29.90s gap 0x343 2.40s (period 100ms)
+ 42.00s unknown_id 0x7FF x3
+ 50.05s out_of_range Coolant.CoolantTemp=215 not in [-40,120]仕込んだ4種類が、すべて別の種別で並びました。表の列は次のように読みます。
出力の見方
period_ms と max_gap_s
周期メッセージなら、
period_msの何倍もmax_gap_sが大きいIDが欠落の候補です。0x343は100msに対して2.40秒でした。dlc の種類
同じIDでDLCが複数あれば、送信側の仕様変更か、フレームの破損を疑います。0x343は
[4, 8]と2種類です。in_dbc が False
DBCにないIDです。診断用のIDか、別のECUのメッセージか、DBCの更新漏れが考えられます。
欠落は2.40秒と出ましたが、仕込んだのは23フレーム分(2.3秒)です。最後に届いたフレームから次のフレームまでの時間なので、欠けた区間より1周期分長く出ます。時刻を根拠に報告させるときは、この差を頭に入れておきます。
結果をClaudeに読ませる
スクリプトの出力は十数行です。これをそのまま貼り、仮説を挙げさせます。
次は scan_can.py の出力です。生ログはありません。
異常ごとに、考えられる原因を2つまで、根拠になった行を引用して挙げてください。
ログに無い情報(配線、ECUの型番)は推測と明記すること。返ってきた仮説は、断定ではなく調べる順番の案として使います。たとえば「0x343のDLC 4は、送信側ECUの再起動直後の初期フレームではないか」という案が出たら、次の2つで確かめます。
- 該当時刻の前後だけをログから取り出す
- 同じ時刻の他のIDの動きと並べる
窓の切り出しは、awkで時刻を指定すれば済みます。欠落の前後を見る例です。
awk -F'[() ]' '$2>1609779951.85 && $2<1609779954.4' candump-demo.log(1609779951.900000) vcan0 343#E503E50300000000
(1609779952.050000) vcan0 1F4#8200
(1609779953.050000) vcan0 1F4#8200
(1609779954.050000) vcan0 1F4#8200
(1609779954.300000) vcan0 343#EB03EB03000000000x343だけが1609779951.9から1609779954.3まで止まり、1秒周期の0x1F4は動き続けています。バス全体が止まったのではなく、このECU(またはこのメッセージの送信処理)だけが止まった、という切り分けが数行でできます。
未知IDは、件数を数えれば足ります。grep -c ' 7FF#' candump-demo.logは3を返し、スクリプトのx3と一致しました。
毎回同じ判定をさせるためのCLAUDE.md
解析のたびに依頼文を書き直すと、判定基準がぶれます。決まりはCLAUDE.mdに置きます。
## CANログ解析の決まり
- captures/ のログの生フレームを出力しない。出すのは集計・時刻・ID・件数だけ
- 1回の出力は50行以内。超えるときは種別ごとの件数と先頭5件にする
- 周期の欠落は「中央値の3倍超」を既定とし、倍率は引数で変える
- 時刻はログ先頭からの経過秒で出し、UNIX時間も併記する
- 原因は仮説として書き、根拠にした出力行を必ず引用する
- スクリプトは analysis/ に保存し、毎回同じものを使い回すcantoolsのコマンドで済む場合と、スクリプトが要る場合
DBCがあるなら、スクリプトを書かずにcantoolsのコマンドで見られる範囲もあります。cantoolsのドキュメントには、candumpの出力をdecodeサブコマンドに流す例があります。
python3 -m cantools decode --single-line demo.dbc < candump-demo.log | head -3(1609779922.000000) vcan0 343#E503E50300000000 :: WheelSpeed(FrontLeft: 99.7 km/h, FrontRight: 99.7 km/h)
(1609779922.050000) vcan0 1F4#8200 :: Coolant(CoolantTemp: 90 degC)
(1609779922.100000) vcan0 343#EC03EC0300000000 :: WheelSpeed(FrontLeft: 100.4 km/h, FrontRight: 100.4 km/h)candump -L形式のログも、このdecodeに渡せました。信号の推移を目で追うだけなら、これで足ります。グラフが欲しければ、同じ形でplotサブコマンドに渡す例もドキュメントにあります。
| やりたいこと | 向く方法 |
|---|---|
| 信号値を目で追う | 向く方法cantools decode |
| 信号の推移を図にする | 向く方法cantools plot |
| 周期の欠落や未知IDを数える | 向く方法python-canのスクリプト |
| 範囲外の値を一覧にする | 向く方法cantoolsのAPIを使うスクリプト |
| 複数のログを毎日まとめて見る | 向く方法スクリプト+CLAUDE.mdの決まり |
decodeは信号を見せるコマンドで、異常を判定するものではありません。判定の基準(周期の何倍、どの範囲)を持たせたいときに、スクリプトが必要になります。
つまずきやすい点
落とし穴
時刻が相対か絶対かで、欠落の判定が崩れる
candumpの-tで時刻の出し方を変えられます(絶対・差分・ゼロ起点)。-t d(差分)や-t z(ゼロ起点)で出した表示をテキストに保存すると、絶対時刻を前提にしたスクリプトでは間隔が合わなくなります。差分時刻を足し合わせずに「前後の差」を取れば、欠落は見えません。ログは-lか-Lで取り、スクリプトの先頭でタイムスタンプが単調増加かを検査させておくと安全です。
- 拡張IDと標準IDの取り違え: DBCで拡張IDにしたメッセージが、ログでは3桁の標準IDで届くと一致しません。
get_message_by_frame_idでKeyErrorが連発するときは、ログのmsg.is_extended_idとDBCのis_extended_frameを並べて見直します。 - CAN FDの扱い: python-canの
SocketcanBusでFDフレームを受けるには、fd=Trueを指定します。この記事の例はクラシックCAN(最大8バイト)の合成ログです。 - ログの再生と時間:
LogReaderは遅延なしで一気に読みます。計測時の間隔を再現して仮想バスに流したいときは、can.MessageSyncを使います。既定では60秒を超える無通信区間を飛ばす(skip=60.0)ので、長い欠落の再現には値の調整が要ります。 - 受信側のフィルタ:
SocketcanBusにはcan_filtersとignore_rx_error_framesの引数があります。後者を有効にすると受信時のエラーフレームが捨てられ、エラーを調べる用途では見えなくなります。
まとめ
CANログの解析は、ログを会話に載せず、集計スクリプトの出力を読ませる形にすると回しやすくなります。欠落・DLC・未知ID・範囲外は、IDごとの周期とDBCの定義があれば機械的に拾えます。Claudeには、拾った行を根拠にした仮説の列挙を頼み、最後は該当時刻の前後をawkで切り出して人が確かめます。
通信ログを解析するという意味では、パケットキャプチャを扱うtsharkのpcapを解析する手順や、サーバーのログを時間帯で切り分けるNginxのアクセスログ解析も同じ型です。
参照した一次ソース
この記事の記述は、次の一次ソースと照合しています。更新時に参照先が変わった場合は一覧も更新します。