見出し画像

【CAN】J1939 まずは費用ゼロ・Windows パソコン1台とPython(ハードウェア不要)でJ1939を体験してみよう


はじめに

「J1939を勉強したいけど、専用のハードウェアを買わないといけないの?」
トラックや建設機械の通信プロトコルであるSAE J1939に興味を持ったとき、多くの人がこの壁にぶつかります。実際、CAN(Controller Area Network)バスと通信するためには、PCANやKvaserといったUSBアダプタが必要で、安くても1万円以上はかかります。

しかし実は、Windowsパソコン1台とPythonさえあれば、費用ゼロでJ1939の基本をリアルに体験できます。
この記事では、ハードウェアを一切使わずにJ1939の送受信・解析をシミュレーションする方法を、仕組みの解説から実際のコードまで丁寧に紹介します。

まずは体験してみましょう。


1.そもそもJ1939とは何か

まずは前提知識を整理しましょう。
CAN(Controller Area Network) は、もともと自動車の車内ネットワーク向けに開発された通信規格です。エンジン制御ユニット(ECU)、ブレーキ、メーター、エアコンなど、車内の多数のコンピュータが互いにデータをやり取りするための「共通言語」です。

SAE J1939 は、そのCANをトラック・バス・建設機械・農業機械などの大型車両向けに拡張した上位プロトコルです。アメリカ自動車技術会(SAE)が策定したもので、以下のような特徴があります。

  • 29ビット拡張IDを使用(標準CANの11ビットより情報量が多い)

  • PGN(Parameter Group Number):メッセージの種類を定義する識別子

  • SPN(Suspect Parameter Number):PGN内の各パラメータ(回転数・速度など)

  • SA(Source Address):送信元ECUのアドレス(0x00〜0xFE)

たとえばエンジン回転数のメッセージは「PGN=0xF004(EEC1)」で定義されており、受信側はこのPGNを見ることでどんなデータが入っているかを判断します。

J1939はトラックの故障診断、テレマティクス、車両管理システムなどに幅広く使われており、組み込みエンジニア・自動車エンジニアにとって必須の知識となっています。

詳しくはコチラ👇️


2.なぜパソコン1台で体験できるのか

ここが本記事の核心です。
通常のCAN通信では、物理的なCANバス(ケーブル)と、パソコンに接続するUSBアダプタが必要です。しかし今回使うpython-canというライブラリには、仮想CANバスの機能が内蔵されています。

【通常の実機構成】

ECU-A ──→ 物理CANケーブル ──→ USBアダプタ ──→ PC
                                (1〜3万円)
【今回の仮想構成(PC1台・費用ゼロ)】

sender.py ──→ 仮想CANバス ──→ receiver.py
             (PC内部で完結)

interface='virtual'というオプションを指定するだけで、パソコンのメモリ上に仮想的なCANバスが生成されます。 この仮想バスは複数のPythonスクリプトから同時に接続でき、あたかも実際のCANバスがあるかのように送受信ができます。

ターミナルを2つ開いて、片方で送信スクリプト・もう片方で受信スクリプトを起動するだけです。物理的な配線も、高価なアダプタも不要です。

この仕組みのおかげで、J1939のメッセージ構造・PGNの解析・データデコードといったプロトコルの本質的な部分を、環境構築の手間なく学べます。


3.Python-canとは

python-can は、CANバス通信をPythonで扱うためのオープンソースライブラリです。
最大の特徴はマルチハードウェア対応です。PCANでもKvaserでもVectorでも、同じPythonコードでほぼそのまま動きます。ハードウェアを買い替えてもコードを書き直す必要がありません。

あなたのPythonコード
       ↓
   python-can(橋渡し役)
       ↓
PCAN / Kvaser / Vector / 仮想バス など
       ↓
   CANバス(実機 or シミュレーション)

郵便配達に例えると、python-canは「配達システム」、CANフレームは「荷物」、J1939プロトコルは「手紙の書き方ルール」、PCANやKvaserは「配達トラックの種類」に相当します。配達システム(python-can)がトラックの種類を吸収してくれるので、利用者はトラックの違いを気にせずに荷物(データ)を届けられます。

さらに j1939ライブラリ を組み合わせることで、PGN・SPN・アドレスクレーミングといったJ1939固有の処理も扱えます。


4.実際にやってみよう

ステップ1:必要なライブラリをインストール

コマンドプロンプトまたはPowerShellを開いて以下を実行します。

pip install python-can j1939

ステップ2:送信スクリプト(sender.py

以下のコードをsender.pyとして保存します。

import can
import time

# 仮想CANバスを作成(ハードウェア不要)
bus = can.Bus(interface='virtual', channel='j1939_test', receive_own_messages=False)

print("送信開始... Ctrl+Cで停止")

try:
    while True:
        # EEC1(Electronic Engine Controller 1)
        # PGN: 0xF004 → 拡張ID: 0x18F00400
        # SA=0x00(エンジンECU)、DA=0xFF(ブロードキャスト)
        msg = can.Message(
            arbitration_id=0x18F00400,
            data=[
                0xFF,        # Byte1: トルクモード
                0xFF,        # Byte2: ドライバ要求トルク
                0x7D,        # Byte3: 実際のトルク(125=0%)
                0xE8, 0x03,  # Byte4-5: エンジン回転数(1000rpm)
                0xFF,        # Byte6: SA
                0xFF,        # Byte7
                0xFF,        # Byte8
            ],
            is_extended_id=True
        )
        bus.send(msg)
        print(f"送信: ID=0x{msg.arbitration_id:08X}  Data={msg.data.hex(' ')}")
        time.sleep(0.1)

except KeyboardInterrupt:
    print("停止")
    bus.shutdown()

コードのポイント解説:

  • interface='virtual':ここが仮想バスの指定。実機を使う場合は'pcan'や'kvaser'に変えるだけ

  • channel='j1939_test':仮想バスの名前。受信側と同じ名前にする必要がある

  • arbitration_id=0x18F00400:J1939の29ビット拡張IDで、PGN・SA・優先度が詰め込まれている

  • is_extended_id=True:J1939は必ず29ビット拡張IDを使うのでTrueにする

  • J1939のマルチバイトデータは、下位バイトから順に並べるリトルエンディアン形式

ステップ3:受信・解析スクリプト(receiver.py

以下のコードをreceiver.pyとして保存します。

import can

# 同じ仮想バスに接続
bus = can.Bus(interface='virtual', channel='j1939_test', receive_own_messages=False)

def parse_j1939(msg):
    """拡張IDからJ1939フィールドを解析する"""
    eid = msg.arbitration_id
    sa  = eid & 0xFF                    # 下位8ビット:Source Address
    pf  = (eid >> 16) & 0xFF           # PDU Format

    if pf >= 240:
        pgn = (eid >> 8) & 0x3FFFF    # PF≥240:ブロードキャスト
        da  = 0xFF
    else:
        pgn = (eid >> 16) & 0x3FF00   # PF<240:アドレス指定
        da  = (eid >> 8) & 0xFF

    return sa, da, pgn

print("受信待機中... Ctrl+Cで停止")

try:
    while True:
        msg = bus.recv(timeout=5.0)
        if msg is None:
            print("タイムアウト(メッセージなし)")
            continue

        sa, da, pgn = parse_j1939(msg)

        print(f"\n--- 受信 ---")
        print(f"  PGN : 0x{pgn:05X}")
        print(f"  SA  : 0x{sa:02X}")
        print(f"  DA  : 0x{da:02X}")
        print(f"  Data: {msg.data.hex(' ')}")

        # EEC1(PGN=0xF004)の簡易デコード
        if pgn == 0xF004:
            rpm = (msg.data[4] << 8 | msg.data[3]) * 0.125
            torque = msg.data[2] - 125
            print(f"  ▶ エンジン回転数: {rpm:.1f} rpm")
            print(f"  ▶ 実トルク      : {torque} %")

except KeyboardInterrupt:
    print("停止")
    bus.shutdown()

コードのポイント解説

  • parse_j1939():29ビットのIDからPGN・SA・DAを切り出す関数。J1939解析の要

  • pf >= 240の分岐:J1939のPDU1形式(アドレス指定)とPDU2形式(ブロードキャスト)の判定

  • EEC1のデコード:rpm = (Byte5 << 8 | Byte4) × 0.125という変換式はJ1939仕様書で定義されている


ステップ4:実行する

ターミナルを2つ開いて、それぞれ別のスクリプトを実行します。

# ターミナル1(先に起動)
python receiver.py

# ターミナル2
python sender.py

受信側のターミナルに以下のような出力が流れ始めれば成功です。

受信待機中... Ctrl+Cで停止

--- 受信 ---
  PGN : 0xF004
  SA  : 0x00
  DA  : 0xFF
  Data: ff ff 7d e8 03 ff ff ff
  ▶ エンジン回転数: 1000.0 rpm
  ▶ 実トルク      : 0 %

5.発展:複数PGNを送ってみる

EEC1(エンジン回転数)だけでなく、実際のトラックで使われる代表的なPGNをいくつか試してみましょう。それぞれのPGNには固有のバイト構造とデコード式があり、これを実際に手を動かして確認することがJ1939理解の近道です。

PGN 0xFEF1:CCVS(車速・クルーズ制御)

CCVS(Cruise Control/Vehicle Speed)は車速やクルーズコントロールの状態を含む、非常によく使われるPGNです。

# CCVS 送信例:車速 80.0 km/h
# 車速のSPN: Byte2〜3(リトルエンディアン)
# デコード式: 値 × 0.00390625 = km/h
# 80.0 km/h → 80 / 0.00390625 = 20480 = 0x5000

msg_ccvs = can.Message(
    arbitration_id=0x18FEF100,   # PGN=0xFEF1, SA=0x00
    data=[
        0xFF,        # Byte1: パーキングブレーキ・クルーズ制御スイッチ類
        0x00, 0x50,  # Byte2-3: 車速 80.0 km/h(リトルエンディアン)
        0xFF,        # Byte4: クルーズ制御設定速度
        0xFF,        # Byte5: PTO状態
        0xFF,        # Byte6: クルーズ制御状態
        0xFF,        # Byte7
        0xFF,        # Byte8
    ],
    is_extended_id=True
)

受信側のデコード:

if pgn == 0xFEF1:
    speed_raw = (msg.data[2] << 8 | msg.data[1])
    speed_kmh = speed_raw * 0.00390625
    print(f"  ▶ 車速: {speed_kmh:.2f} km/h")

    # Byte1のビット0-1:パーキングブレーキ状態
    parking_brake = (msg.data[0] >> 2) & 0x03
    brake_status = {0: 'OFF', 1: 'ON', 2: '不明', 3: 'エラー'}
    print(f"  ▶ パーキングブレーキ: {brake_status.get(parking_brake, '不明')}")

PGN 0xFEE9:EFL/P1(燃料消費量)

EFL/P1(Electronic Engine Controller Fuel/Load Information 1)は燃料消費の累積データを持つPGNです。テレマティクスや燃費管理システムで頻繁に使われます。

# EFL/P1 送信例:総燃料消費量 500,000L相当
# 総燃料消費SPN: Byte5〜8(リトルエンディアン)
# デコード式: 値 × 0.5 = L(ライフタイム累積)
# 500,000L → 500000 / 0.5 = 1,000,000 = 0x000F4240

msg_fuel = can.Message(
    arbitration_id=0x18FEE900,   # PGN=0xFEE9, SA=0x00
    data=[
        0xFF, 0xFF,              # Byte1-2: 瞬間燃料消費率(未使用)
        0xFF, 0xFF,              # Byte3-4: 平均燃料節約率(未使用)
        0x40, 0x42, 0x0F, 0x00, # Byte5-8: 累積燃料消費量 500,000L
    ],
    is_extended_id=True
)

受信側のデコード:

if pgn == 0xFEE9:
    # Byte5〜8:4バイトリトルエンディアン
    fuel_raw = (msg.data[7] << 24 | msg.data[6] << 16 |
                msg.data[5] << 8  | msg.data[4])
    fuel_L = fuel_raw * 0.5
    print(f"  ▶ 累積燃料消費量: {fuel_L:,.1f} L")

PGN 0xFEFC:VD(車両走行距離)

VD(Vehicle Distance)は総走行距離と旅行距離を持つPGNです。車両管理・メンテナンス管理で中核となるデータです。

# VD 送信例:総走行距離 250,000 km
# デコード式: 値 × 0.125 = km
# 250,000 km → 250000 / 0.125 = 2,000,000 = 0x001E8480

msg_vd = can.Message(
    arbitration_id=0x18FEFC00,   # PGN=0xFEFC, SA=0x00
    data=[
        0xFF, 0xFF, 0xFF, 0xFF,  # Byte1-4: 旅行距離(今回はN/A)
        0x80, 0x84, 0x1E, 0x00,  # Byte5-8: 総走行距離 250,000 km
    ],
    is_extended_id=True
)

受信側のデコード:

if pgn == 0xFEFC:
    dist_raw = (msg.data[7] << 24 | msg.data[6] << 16 |
                msg.data[5] << 8  | msg.data[4])
    dist_km = dist_raw * 0.125
    print(f"  ▶ 総走行距離: {dist_km:,.1f} km")

PGN 0xFEEB:EEC3(エンジン要求トルク)

EEC3(Electronic Engine Controller 3)はドライバや外部システムからエンジンへのトルク要求を伝えるPGNです。EEC1(実際のトルク)と組み合わせて学ぶと、制御の流れが理解しやすくなります。

# EEC3 送信例:要求トルク +20%
# デコード式: 値 - 125 = %(-125%〜+125%)
# +20% → 20 + 125 = 145 = 0x91

msg_eec3 = can.Message(
    arbitration_id=0x18FEEB00,   # PGN=0xFEEB, SA=0x00
    data=[
        0x91,        # Byte1: 要求トルク +20%
        0xFF,        # Byte2: 要求速度制限
        0xFF, 0xFF,  # Byte3-4: 要求速度
        0xFF,        # Byte5: エンジン電気的負荷
        0xFF,        # Byte6: エンジン冷却ファン速度
        0xFF,        # Byte7
        0xFF,        # Byte8
    ],
    is_extended_id=True
)

受信側のデコード:

if pgn == 0xFEEB:
    req_torque = msg.data[0] - 125
    print(f"  ▶ 要求トルク: {req_torque:+d} %")

複数PGNをまとめて送信するスクリプト

上記のPGNをすべてまとめ、実際のトラックのような複数データを同時に流す送信スクリプトです。

import can
import time

bus = can.Bus(interface='virtual', channel='j1939_test', receive_own_messages=False)

# 各PGNのメッセージ定義
messages = {
    'EEC1(エンジン回転数)': can.Message(
        arbitration_id=0x18F00400,
        data=[0xFF, 0xFF, 0x7D, 0xE8, 0x03, 0xFF, 0xFF, 0xFF],
        is_extended_id=True
    ),
    'CCVS(車速80km/h)': can.Message(
        arbitration_id=0x18FEF100,
        data=[0xFF, 0x00, 0x50, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF],
        is_extended_id=True
    ),
    'EFL/P1(燃料消費)': can.Message(
        arbitration_id=0x18FEE900,
        data=[0xFF, 0xFF, 0xFF, 0xFF, 0x40, 0x42, 0x0F, 0x00],
        is_extended_id=True
    ),
    'VD(走行距離)': can.Message(
        arbitration_id=0x18FEFC00,
        data=[0xFF, 0xFF, 0xFF, 0xFF, 0x80, 0x84, 0x1E, 0x00],
        is_extended_id=True
    ),
    'EEC3(要求トルク)': can.Message(
        arbitration_id=0x18FEEB00,
        data=[0x91, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF],
        is_extended_id=True
    ),
}

print("複数PGN送信開始... Ctrl+Cで停止\n")

try:
    while True:
        for name, msg in messages.items():
            bus.send(msg)
            print(f"送信 [{name}] ID=0x{msg.arbitration_id:08X}")
            time.sleep(0.05)
        print("---")
        time.sleep(1.0)  # 1秒ごとに全PGNを送信

except KeyboardInterrupt:
    print("停止")
    bus.shutdown()

受信側(receiver.py)も、すべてのPGNに対応したデコード処理を追加すると、次のような出力が得られます。

--- 受信 ---
  PGN : 0xF004  ▶ エンジン回転数: 1000.0 rpm / 実トルク: 0 %
--- 受信 ---
  PGN : 0xFEF1  ▶ 車速: 80.00 km/h / パーキングブレーキ: 不明
--- 受信 ---
  PGN : 0xFEE9  ▶ 累積燃料消費量: 500,000.0 L
--- 受信 ---
  PGN : 0xFEFC  ▶ 総走行距離: 250,000.0 km
--- 受信 ---
  PGN : 0xFEEB  ▶ 要求トルク: +20 %

代表的なPGN早見表

PGN   / 名称 / 主なSPN / デコード式
0xF004 / EEC1 / エンジン回転数 / Byte4-5 × 0.125 = rpm
0xFEF1 / CCVS / 車速 / Byte2-3 × 0.00390625 = km/h
0xFEFC / VD / 総走行距離 / Byte5-8 × 0.125 = km
0xFEE9 / EFL/P1 / 累積燃料消費 / Byte5-8 × 0.5 = L
0xFEEB / EEC3 / 要求トルク / Byte1 - 125 = %
0xFEEE / ET1 / エンジン冷却水温 / Byte1 - 40 = ℃
0xFEF2 / LFE / 瞬間燃費 / Byte1-2 × 0.05 = L/h
0xFDA5 / AIR1 / 吸気圧力 / Byte3 × 2 = kPa

6.実機への移行は簡単

仮想バスで学んだ後、実際のトラックや機器に接続したくなったときの移行はとても簡単です。コードの変更はたった1行です。

# 仮想バス(学習時)
bus = can.Bus(interface='virtual', channel='j1939_test')

# ↓ 実機接続(PCANアダプタを使う場合)
bus = can.Bus(interface='pcan', channel='PCAN_USBBUS1', bitrate=250000)

ハードウェアを購入したタイミングで1行書き換えるだけで、これまで書いたコードがそのまま動きます。python-canのマルチハードウェア対応の恩恵です。

まとめ

本記事で紹介した方法を振り返ります。

必要なもの:

  • WindowsパソコンとPython(無料)

  • pip install python-can j1939(無料)

  • ターミナル2つ

できること:

  • J1939の29ビット拡張IDの構造を理解する

  • PGN・SA・DAの解析を実際に動かして体感する

  • エンジン回転数・車速などのSPNデコードを実装する

J1939は仕様書を読むだけではなかなか理解が深まりません。実際にコードを動かしてメッセージを送受信し、IDをビット演算で分解する体験が、理解を一気に加速させます。

まずはこの記事のコードをそのままコピーして動かしてみてください。「あ、こういう構造なんだ」という感覚が、きっと得られるはずです。
この記事で使ったコードはすべてパソコン1台・費用ゼロで動作します。実機環境への移行も1行の変更で対応できます。


次のステップはコチラ👇️

標準PGNで実装してみる👇️


いいなと思ったら応援しよう!