前回、ESP32-C3 + CANトランシーバ で OBD2コネクタから情報取得し、BLEで無線化しました。
一旦完成として、取得した情報の表示側を工夫(単なる数値や文字での表示を、レベルメーター表示や、円形メーター表示などに変更)すればよいということになりますが、現在は、取得する情報(PID)の主体は、ESP32-C3側にあります。
これを、表示側に主体を持たせることにして、ESP32-C3側は、表示側から指示されたPIDをクルマから取得して返す ゲートウェイ、という役割に変更しようと思います。
ゲートウェイ は、シングルフレーム/マルチフレームを制御した上で、物理的に1つのデータに結合して、(デコード処理は行わずに)表示側に渡します(BLEで可変長データを送信します)。
こうすることで、表示側で、取得したい情報(PID)を要求し、それをどのように表示するのか(デコード処理を行い、文字表示やグラフ化など)、UIやビジュアライゼーションの幅も広がって使い易くなると思います。
1. ゲートウェイ化
WriteキャラクタリスティックでPIDリストを受信する処理が増えますが、デコード処理が無くなります。前回対応した OTA 処理はそのまま適用します。
ESP32-C3はシングルコアですが、PIDリスト受信→PIDリクエスト送信処理、CAN受信処理→PID返送送信は、マルチタスクにて完全に非同期で処理します。
クルマ(ECU)へは同時に一つのPIDしか要求できません。要求した応答を受け取って 完結すると、次のPIDを要求することができます。この制御のために FreeRTOS のキューを利用します。
// ESP32-C3 Supermini and SN65HVD230 CAN receiver
// Wifi AP & ElegantOTA
// SSID: ESP32C3_OTA_AP
// http://192.168.4.1/update
#include <WiFi.h>
#include <WiFiClient.h>
#include <WebServer.h>
#include <ElegantOTA.h>
#undef LED_BUILTIN
#define LED_BUILTIN 8 //actve low
#define LED_ON LOW
#define LED_OFF HIGH
const char *ssid = "ESP32C3_OTA_AP";
const char *password = "876543210";
uint32_t last_ota_time = 0;
// モード切替用のピン(スイッチを押すとGNDに落ちる)
const int MODE_SWITCH_PIN = 10;
// 動作モードを保持するフラグ
bool isOtaMode = false;
// 一括出力用のグローバル固定長バッファ
char outputBuffer[384];
WebServer server(80);
// 定数定義・構造体・グローバル宣言
#include <Arduino.h>
#include <BLEDevice.h>
#include <BLEServer.h>
#include <BLEUtils.h>
#include <BLE2902.h>
#include "driver/twai.h"
// ESP32-C3のGPIOピン設定
#define CAN_TX_PIN GPIO_NUM_2
#define CAN_RX_PIN GPIO_NUM_3
// 通信定数
const uint16_t CAN_REQ_ID = 0x7DF;
const uint16_t CAN_RES_ID = 0x7E8;
const uint32_t TIMEOUT_MS = 1000;
const uint8_t MAX_QUEUE_SIZE = 30;
// BLE UUID定義 (一般的なNordic UART Service準拠)
#define SERVICE_UUID "6E400001-B5A3-F393-E0A9-E50E24DCCA9E"
#define CHARACTERISTIC_UUID_RX "6E400002-B5A3-F393-E0A9-E50E24DCCA9E"
#define CHARACTERISTIC_UUID_TX "6E400003-B5A3-F393-E0A9-E50E24DCCA9E"
// ゲートウェイの内部ステータス定義
#define GT_STATUS_SUCCESS 0x00
#define GT_STATUS_TIMEOUT 0x01
#define GT_STATUS_REJECTED 0x02
#define GT_STATUS_SEQ_ERROR 0x03 // SN(シーケンス番号)不一致による中断
// リクエスト構造体(キュー転送用)
struct CanRequest {
uint8_t mode;
uint8_t pid;
};
// FreeRTOS ハンドル・BLE管理
QueueHandle_t rxtxQueue = NULL;
BLECharacteristic* pTxCharacteristic = NULL;
bool deviceConnected = false;
// ISO-TP 受信バッファ
uint8_t isoTpRxBuffer[256];
uint16_t isoTpRxLen = 0;
uint16_t isoTpExpectedLen = 0;
uint8_t isoTpNextSn = 1;
// プロトタイプ宣言
void send_ble_response(uint8_t status, uint8_t mode, uint8_t pid, uint8_t* payload, uint16_t len);
// CAN (TWAI) 初期化
void initCAN() {
twai_general_config_t g_config = TWAI_GENERAL_CONFIG_DEFAULT(CAN_TX_PIN, CAN_RX_PIN, TWAI_MODE_NORMAL);
twai_timing_config_t t_config = TWAI_TIMING_CONFIG_500KBITS(); // BMW 500kbps適合
twai_filter_config_t f_config = TWAI_FILTER_CONFIG_ACCEPT_ALL();
if (twai_driver_install(&g_config, &t_config, &f_config) == ESP_OK) {
twai_start();
}
}
// ISO-TP シングルフレームでの要求送信関数
void send_can_isotp_req(uint8_t mode, uint8_t pid) {
twai_message_t tx_msg = {}; // all 0
tx_msg.identifier = CAN_REQ_ID;
tx_msg.extd = 0;
tx_msg.rtr = 0;
tx_msg.data_length_code = 8;
// ISO-TP Single Frame
tx_msg.data[0] = 0x02;
tx_msg.data[1] = mode;
tx_msg.data[2] = pid;
twai_transmit(&tx_msg, pdMS_TO_TICKS(10));
}
// ISO-TP フロー制御フレーム (Flow Control) 送信関数
void send_can_flow_control(uint32_t responding_ecu_id) {
twai_message_t tx_msg = {}; // all 0
tx_msg.identifier = responding_ecu_id;
tx_msg.extd = 0;
tx_msg.rtr = 0;
tx_msg.data_length_code = 8;
tx_msg.data[0] = 0x30; // 30: Continue Sending
tx_msg.data[1] = 0x00; // Block Size = 0
tx_msg.data[2] = 0x00; // STmin = 0
twai_transmit(&tx_msg, pdMS_TO_TICKS(10));
}
// ISO-TP 応答受信・結合メインロジック
uint8_t wait_and_assemble_isotp(uint8_t target_mode, uint8_t target_pid, uint8_t* out_buffer, uint16_t* out_len, uint32_t timeout_ms) {
uint32_t start_time = millis();
isoTpRxLen = 0;
isoTpExpectedLen = 0;
isoTpNextSn = 1;
uint8_t expected_res_mode = target_mode + 0x40;
while ((millis() - start_time) < timeout_ms) {
twai_message_t rx_msg;
if (twai_receive(&rx_msg, pdMS_TO_TICKS(10)) != ESP_OK) {
continue;
}
if (rx_msg.identifier != CAN_RES_ID) {
continue;
}
uint8_t frame_type = (rx_msg.data[0] >> 4) & 0x0F;
// 1) Single Frame
if (frame_type == 0x00) {
uint8_t sf_len = rx_msg.data[0] & 0x0F;
if (sf_len >= 2 && rx_msg.data[1] == expected_res_mode && rx_msg.data[2] == target_pid) {
*out_len = sf_len - 2;
if (*out_len > 0) {
memcpy(out_buffer, &rx_msg.data[3], *out_len);
}
return GT_STATUS_SUCCESS;
}
if (rx_msg.data[1] == 0x7F && rx_msg.data[2] == target_mode) {
return GT_STATUS_REJECTED;
}
}
// 2) First Frame
else if (frame_type == 0x01) {
if (rx_msg.data[2] == expected_res_mode && rx_msg.data[3] == target_pid) {
uint16_t declared_len = (((uint16_t)(rx_msg.data[0] & 0x0F)) << 8) | rx_msg.data[1];
if (declared_len <= 6) continue; // 異常なFFは無視
isoTpExpectedLen = declared_len;
uint8_t ff_pure_len = 4;
memcpy(&isoTpRxBuffer[0], &rx_msg.data[4], ff_pure_len);
isoTpRxLen = ff_pure_len;
isoTpNextSn = 1;
uint32_t responding_ecu_id = rx_msg.identifier - 8;
send_can_flow_control(responding_ecu_id);
}
}
// 3) Consecutive Frame
else if (frame_type == 0x02) {
uint8_t sn = rx_msg.data[0] & 0x0F;
if (isoTpExpectedLen == 0) continue;
if (sn == isoTpNextSn) {
uint8_t available = (rx_msg.data_length_code > 0) ? rx_msg.data_length_code - 1 : 0;
uint16_t rem_len = (isoTpExpectedLen - 2) - isoTpRxLen;
uint8_t cf_pure_len = (rem_len < available) ? rem_len : available;
if (isoTpRxLen + cf_pure_len <= sizeof(isoTpRxBuffer)) {
memcpy(&isoTpRxBuffer[isoTpRxLen], &rx_msg.data[1], cf_pure_len);
isoTpRxLen += cf_pure_len;
}
isoTpNextSn = (isoTpNextSn + 1) & 0x0F;
if (isoTpRxLen >= (isoTpExpectedLen - 2)) {
*out_len = isoTpRxLen;
memcpy(out_buffer, isoTpRxBuffer, isoTpRxLen);
return GT_STATUS_SUCCESS;
}
} else {
// SN不一致:フレーム順序異常またはロスト
return GT_STATUS_SEQ_ERROR;
}
}
}
return GT_STATUS_TIMEOUT;
}
// BLE 接続状態管理コールバック
class ServerCallbacks : public BLEServerCallbacks {
void onConnect(BLEServer* pServer) override {
deviceConnected = true;
}
void onDisconnect(BLEServer* pServer) override {
deviceConnected = false;
xQueueReset(rxtxQueue);
// 切断時にアドバタイズを自動再開
BLEDevice::startAdvertising();
}
};
// リモートからのリクエスト受信(RX)コールバック
class CharacteristicCallbacks : public BLECharacteristicCallbacks {
void onWrite(BLECharacteristic* pCharacteristic) override {
// データ取得処理
uint8_t* data = pCharacteristic->getData();
size_t len = pCharacteristic->getLength();
if (len < 3) return;
uint8_t mode = data[0];
uint8_t pid_count = data[1];
if (len < (size_t)(2 + pid_count)) return;
for (uint8_t i = 0; i < pid_count; i++) {
CanRequest req;
req.mode = mode;
req.pid = data[2 + i];
if (uxQueueSpacesAvailable(rxtxQueue) > 0) {
xQueueSend(rxtxQueue, &req, 0);
}
}
}
};
// BLEの初期化と設定
void initBLE() {
BLEDevice::init("AsyncCAN-C3"); // デバイス名設定
BLEDevice::setMTU(247); // MTUサイズ拡張
BLEServer* pServer = BLEDevice::createServer();
pServer->setCallbacks(new ServerCallbacks());
BLEService* pService = pServer->createService(SERVICE_UUID);
// TX Characteristic (Notify用)
pTxCharacteristic = pService->createCharacteristic(
CHARACTERISTIC_UUID_TX,
BLECharacteristic::PROPERTY_NOTIFY
);
// RX Characteristic (Write用)
BLECharacteristic* pRxCharacteristic = pService->createCharacteristic(
CHARACTERISTIC_UUID_RX,
BLECharacteristic::PROPERTY_WRITE | BLECharacteristic::PROPERTY_WRITE_NR
);
pRxCharacteristic->setCallbacks(new CharacteristicCallbacks());
pService->start();
// アドバタイズ開始
BLEAdvertising* pAdvertising = BLEDevice::getAdvertising();
pAdvertising->addServiceUUID(SERVICE_UUID);
pAdvertising->setScanResponse(true);
pAdvertising->setMinPreferred(0x06); // iOS/macOSとの親和性を上げる?
pAdvertising->setMinPreferred(0x12);
BLEDevice::startAdvertising();
}
// リモートへのバイナリレスポンス返送関数
void send_ble_response(uint8_t status, uint8_t mode, uint8_t pid, uint8_t* payload, uint16_t len) {
if (!deviceConnected || pTxCharacteristic == NULL) return;
static uint8_t tx_buf[261];
tx_buf[0] = status;
tx_buf[1] = mode;
tx_buf[2] = pid;
tx_buf[3] = (uint8_t)((len >> 8) & 0xFF);
tx_buf[4] = (uint8_t)(len & 0xFF);
uint16_t total_tx_len = 5;
if (status == GT_STATUS_SUCCESS && len > 0 && payload != NULL) {
memcpy(&tx_buf[5], payload, len);
total_tx_len += len;
}
pTxCharacteristic->setValue(tx_buf, total_tx_len);
pTxCharacteristic->notify();
}
// CANゲートウェイ同期・調和処理タスク
void canGatewayTask(void* pvParameters) {
CanRequest current_req;
static uint8_t assembled_payload[256];
while (1) {
if (xQueueReceive(rxtxQueue, ¤t_req, portMAX_DELAY) == pdTRUE) {
send_can_isotp_req(current_req.mode, current_req.pid);
uint16_t payload_len = 0;
uint8_t result_status = wait_and_assemble_isotp(current_req.mode, current_req.pid, assembled_payload, &payload_len, TIMEOUT_MS);
if (result_status == GT_STATUS_SUCCESS) {
send_ble_response(GT_STATUS_SUCCESS, current_req.mode, current_req.pid, assembled_payload, payload_len);
} else {
send_ble_response(result_status, current_req.mode, current_req.pid, NULL, 0);
}
vTaskDelay(pdMS_TO_TICKS(25));
}
}
}
// メインエントリー(setup / loop)
void setupTWAIandBLE() {
rxtxQueue = xQueueCreate(MAX_QUEUE_SIZE, sizeof(CanRequest));
if (rxtxQueue != NULL) {
initCAN();
initBLE();
xTaskCreate(canGatewayTask, "CAN_Gateway_Task", 4096, NULL, 3, NULL);
}
}
void loopTWAIandBLE() {
vTaskDelay(pdMS_TO_TICKS(1000));
}
void logWithTimestamp(const char* message, bool addNewLine = true) {
unsigned long currentMillis = millis();
unsigned long total_seconds = currentMillis / 1000;
unsigned long hours = total_seconds / 3600;
unsigned long minutes = (total_seconds % 3600) / 60;
unsigned long seconds = total_seconds % 60;
unsigned long ms = currentMillis % 1000;
// タイムスタンプバッファ
char timestamp[32];
snprintf(timestamp, sizeof(timestamp), "[%lu:%02lu:%02lu.%03lu] ", hours, minutes, seconds, ms);
// シリアルへの出力
Serial.print(timestamp);
if (addNewLine) {
Serial.println(message);
} else {
Serial.print(message);
}
}
void setup() {
Serial.begin(115200);
delay(1000); // シリアル安定用
pinMode(LED_BUILTIN, OUTPUT);
digitalWrite(LED_BUILTIN, LED_OFF);
// スイッチピンを内部プルアップで初期化
pinMode(MODE_SWITCH_PIN, INPUT_PULLUP);
delay(100); // チャタリング・電位安定待ち
// 起動時のピンの状態をチェック (LOW = スイッチが押されている)
if (digitalRead(MODE_SWITCH_PIN) == LOW) {
isOtaMode = true;
digitalWrite(LED_BUILTIN, LED_ON);
while (digitalRead(MODE_SWITCH_PIN) == LOW) delay(10);
}
// ----------------------------------------------------
// モード分岐
// ----------------------------------------------------
if (isOtaMode) {
// 【OTAモードの処理】
logWithTimestamp("--- OTAモード: Wi-Fi & OTA を起動します ---");
WiFi.mode(WIFI_AP);
WiFi.softAP(ssid, password);
{
outputBuffer[0] = '\0';
sprintf(outputBuffer, "AP Wi-Fi Started. SSID: %s", ssid);
logWithTimestamp(outputBuffer);
}
{
// APモード時のESP32のIPアドレスを取得(デフォルトは通常 192.168.4.1)
IPAddress IP = WiFi.softAPIP();
outputBuffer[0] = '\0';
sprintf(outputBuffer, "WebサーバのURL: http://%s/update", IP.toString().c_str());
logWithTimestamp(outputBuffer);
}
// ルートページ(アクセス確認用)
server.on("/", []() {
server.send(200, "text/plain", "ESP32 OTA Server is Running!");
});
// ElegantOTAの設定
ElegantOTA.begin(&server);
server.begin();
Serial.println("HTTP server started");
} else {
// 【通常モードの処理】
logWithTimestamp("--- 通常モード: TWAI & BLE を起動します ---");
// Wi-Fiの電波を完全にオフにしてメモリと電力を節約
WiFi.mode(WIFI_OFF);
// TWAIとBLEの初期化を実行
setupTWAIandBLE();
}
}
void loop() {
if (isOtaMode) {
// OTAモード時はOTAの監視のみを行う(TWAI/BLEは動かない)
server.handleClient();
// ElegantOTAの内部ループ処理
ElegantOTA.loop();
} else {
// 通常モード時は既存のTWAI/BLE処理のみを行う(Wi-Fi/OTAは動かない)
loopTWAIandBLE();
}
}
前回と同様、Arduino-IDEメニュー「ツール」→「Partition Scheme」で"Minimal SPIFFS (1.9MB APP with OTA/128KB SPIFFS)"を選択します。
Arduino-IDEメニュー「スケッチ」→「コンパイル済みバイナリをエクスポート」にて、バイナリファイルを出力します。
http://192.168.4.1/update (ElegantOTA) の Select Fileで can_v3ino.binを指定して ESP32-C3へ書き込みます。
2. リクエスト側(表示側)テストアプリ
ゲートウェイ化した ESP32-C3プログラム をテストするために、次のPythonアプリを用意しました。
- REPLにて、任意のタイミングで、任意のPIDを(複数)リクエストできる
- レスポンスは非同期に受信し、(デコードは無し)RAWデータとして表示
(レスポンスはリクエストと対応付けしない)
import asyncio
import sys
from datetime import datetime
from bleak import BleakScanner, BleakClient
# =================================================================
# ゲートウェイの接続・UUID定義 (ESP32側の設定と一致)
# =================================================================
DEVICE_NAME = "AsyncCAN-C3"
SERVICE_UUID = "6E400001-B5A3-F393-E0A9-E50E24DCCA9E"
CHARACTERISTIC_UUID_RX = "6E400002-B5A3-F393-E0A9-E50E24DCCA9E" # PC -> ESP32[送信]
CHARACTERISTIC_UUID_TX = "6E400003-B5A3-F393-E0A9-E50E24DCCA9E" # ESP32 -> PC[受信]
STATUS_MAP = {
0x00: "SUCCESS", # (正常完了)
0x01: "TIMEOUT", # (車両ECU無応答/NA)
0x02: "REJECTED", # (車両ECUからの拒否応答: NRC 0x7F)
0x03: "SEQ_ERROR" # (ISO-TPフレーム順序異常/ロスト)
}
def ts_print(msg: str):
now = datetime.now()
timestamp = now.strftime("[%H:%M:%S") + f".{now.microsecond // 1000:03d}]"
print(f"{timestamp} {msg}")
# BLE Notify ストリームクラス
class BleNotifyReceiver:
def __init__(self):
self.rx_buffer = bytearray()
self.expected_len = 0
self.header_parsed = False
def handle_notification(self, sender: int, data: bytearray):
self.rx_buffer.extend(data)
# 溜まっている限り、切り出せるフレームを連続して処理する
# (1回の通知で複数フレーム分届く/複数通知にまたがる、どちらにも対応)
while True:
if not self.header_parsed:
if len(self.rx_buffer) < 5:
return
payload_len = (self.rx_buffer[3] << 8) | self.rx_buffer[4]
self.expected_len = 5 + payload_len
self.header_parsed = True
if len(self.rx_buffer) < self.expected_len:
return
self._parse_and_dump(self.rx_buffer[:self.expected_len])
self.rx_buffer = self.rx_buffer[self.expected_len:]
self.header_parsed = False
self.expected_len = 0
def _parse_and_dump(self, complete_packet: bytearray):
status = complete_packet[0]
mode = complete_packet[1]
pid = complete_packet[2]
payload_len = (complete_packet[3] << 8) | complete_packet[4]
payload = complete_packet[5:]
status_str = STATUS_MAP.get(status, f"UNKNOWN ERROR (0x{status:02X})")
if status == 0x00 and payload_len > 0:
hex_str = payload.hex().upper()
# 4桁(2バイト)ごとに区切ってスペースを挿入
grouped = " ".join(hex_str[i:i+4] for i in range(0, len(hex_str), 4))
payload_str = f"[ {grouped} ]"
else:
payload_str = "none"
ts_print(
f"[Response] Mode 0x{mode:02X} / PID 0x{pid:02X} | "
f"Status: 0x{status:02X} [{status_str}] | "
f"Len: {payload_len} | "
f"Payload: {payload_str}"
)
# 要求パケット生成
def build_request_packet(mode: int, pids: list) -> bytes:
packet = bytearray()
packet.append(mode)
packet.append(len(pids))
for pid in pids:
packet.append(pid)
return bytes(packet)
# ユーザ入力解析
def parse_user_input(line: str):
"""
入力例: "01 0C 0D 04 83 88"
先頭がMode、以降が任意個数のPID。
戻り値: (mode, [pid, ...]) または None(不正入力/コマンド時)
"""
tokens = line.strip().split()
if not tokens:
return None
try:
values = [int(t, 16) for t in tokens]
except ValueError:
ts_print("[!] 16進数として解釈できません。例: 01 0C 0D 83 88")
return None
if len(values) < 2:
ts_print("[!] Modeと最低1個のPIDを指定してください。例: 01 0C")
return None
mode, pids = values[0], values[1:]
if not (0x00 <= mode <= 0xFF):
ts_print("[!] Modeの値が不正です。")
return None
if len(pids) > 253:
ts_print("[!] 一度に指定できるPIDは253個まで")
return None
return mode, pids
# メイン処理(REPL)
async def main():
ts_print(f"[*] BLEデバイス '{DEVICE_NAME}' を検索中...")
device = await BleakScanner.find_device_by_name(DEVICE_NAME)
if not device:
ts_print(f"[-] デバイス '{DEVICE_NAME}' が見つかりません。ゲートウェイ(ESP32-C3)の電源を確認してください。")
sys.exit(1)
ts_print(f"[+] デバイス発見! アドレス: {device.address}")
ts_print(f"[*] 接続中...")
receiver = BleNotifyReceiver()
async with BleakClient(device) as client:
ts_print(f"[+] 接続成功!")
ts_print(f"[*] 現在のMTUサイズ: {client.mtu_size} バイト")
ts_print(f"[*] 受信通知 (Notify) を有効化しています...")
await client.start_notify(CHARACTERISTIC_UUID_TX, receiver.handle_notification)
print("-" * 75)
ts_print("[*] REPLモードです。'Mode PID [PID...]' の形式で入力してください(16進数)。")
ts_print(" 例: 01 0C 0D 04 83 88")
ts_print(" 'q' または 'quit' で終了します。応答は届き次第、非同期に表示されます。")
print("-" * 75)
try:
while True:
# イベントループ(=BLE通知の受信処理)を止めない
line = await asyncio.to_thread(input, ">> ")
if line.strip().lower() in ("q", "quit", "exit"):
break
parsed = parse_user_input(line)
if parsed is None:
continue
mode, pids = parsed
request_data = build_request_packet(mode, pids)
pid_list_str = ", ".join(f"0x{p:02X}" for p in pids)
ts_print(f"[Request] Mode 0x{mode:02X} / PIDs: [{pid_list_str}]")
# Write without response
await client.write_gatt_char(CHARACTERISTIC_UUID_RX, request_data, response=False)
except (asyncio.CancelledError, KeyboardInterrupt):
pass
finally:
try:
ts_print("[*] 終了処理中...通知を停止して安全に切断します...")
await client.stop_notify(CHARACTERISTIC_UUID_TX)
except Exception:
pass
if __name__ == "__main__":
try:
asyncio.run(main())
except KeyboardInterrupt:
print("")
ts_print("[+] ユーザーによる Ctrl+C を検知しました。テストスクリプトを安全に終了しました。")
3. 実車テスト
# エンジン始動して実行
% python3 gateway_test.py
[18:17:35.670] [*] BLEデバイス 'AsyncCAN-C3' を検索中...
[18:17:35.949] [+] デバイス発見! アドレス: 1B5B8159-3FA3-40FF-199F-5434B62B243E
[18:17:35.949] [*] 接続中...
[18:17:36.363] [+] 接続成功!
[18:17:36.364] [*] 現在のMTUサイズ: 247 バイト
[18:17:36.364] [*] 受信通知 (Notify) を有効化しています...
---------------------------------------------------------------------------
[18:17:36.393] [*] REPLモードです。'Mode PID [PID...]' の形式で入力してください(16進数)。
[18:17:36.393] 例: 01 0C 0D 04 83 88
[18:17:36.393] 'q' または 'quit' で終了します。応答は届き次第、非同期に表示されます。
---------------------------------------------------------------------------
>> 01 01 04 05 0C 0D 0F 10 11 1C 1F 21 30 31 33 41 42 45 46 49 4A 69 6D 70 78 7A 83 88 8B 8C
[18:18:23.623] [Request] Mode 0x01 / PIDs: [0x01, 0x04, 0x05, 0x0C, 0x0D, 0x0F, 0x10, 0x11, 0x1C, 0x1F, 0x21, 0x30, 0x31, 0x33, 0x41, 0x42, 0x45, 0x46, 0x49, 0x4A, 0x69, 0x6D, 0x70, 0x78, 0x7A, 0x83, 0x88, 0x8B, 0x8C]
[18:18:23.651] [Response] Mode 0x01 / PID 0x01 | Status: 0x00 [SUCCESS] | Len: 4 | Payload: [ 000F EA00 ]
[18:18:23.681] [Response] Mode 0x01 / PID 0x04 | Status: 0x00 [SUCCESS] | Len: 1 | Payload: [ 3C ]
[18:18:23.711] [Response] Mode 0x01 / PID 0x05 | Status: 0x00 [SUCCESS] | Len: 1 | Payload: [ 4D ]
[18:18:23.740] [Response] Mode 0x01 / PID 0x0C | Status: 0x00 [SUCCESS] | Len: 2 | Payload: [ 0C2E ]
[18:18:23.771] [Response] Mode 0x01 / PID 0x0D | Status: 0x00 [SUCCESS] | Len: 1 | Payload: [ 00 ]
[18:18:23.801] [Response] Mode 0x01 / PID 0x0F | Status: 0x00 [SUCCESS] | Len: 1 | Payload: [ 49 ]
[18:18:23.831] [Response] Mode 0x01 / PID 0x10 | Status: 0x00 [SUCCESS] | Len: 2 | Payload: [ 0556 ]
[18:18:23.860] [Response] Mode 0x01 / PID 0x11 | Status: 0x00 [SUCCESS] | Len: 1 | Payload: [ 1B ]
[18:18:23.890] [Response] Mode 0x01 / PID 0x1C | Status: 0x00 [SUCCESS] | Len: 1 | Payload: [ 06 ]
[18:18:23.921] [Response] Mode 0x01 / PID 0x1F | Status: 0x00 [SUCCESS] | Len: 2 | Payload: [ 0000 ]
[18:18:23.950] [Response] Mode 0x01 / PID 0x21 | Status: 0x00 [SUCCESS] | Len: 2 | Payload: [ 0000 ]
[18:18:23.980] [Response] Mode 0x01 / PID 0x30 | Status: 0x00 [SUCCESS] | Len: 1 | Payload: [ AF ]
[18:18:24.011] [Response] Mode 0x01 / PID 0x31 | Status: 0x00 [SUCCESS] | Len: 2 | Payload: [ 054B ]
[18:18:24.041] [Response] Mode 0x01 / PID 0x33 | Status: 0x00 [SUCCESS] | Len: 1 | Payload: [ 64 ]
[18:18:24.071] [Response] Mode 0x01 / PID 0x41 | Status: 0x00 [SUCCESS] | Len: 4 | Payload: [ 001F EAEA ]
[18:18:24.101] [Response] Mode 0x01 / PID 0x42 | Status: 0x00 [SUCCESS] | Len: 2 | Payload: [ 2D00 ]
[18:18:24.130] [Response] Mode 0x01 / PID 0x45 | Status: 0x00 [SUCCESS] | Len: 1 | Payload: [ 00 ]
[18:18:24.161] [Response] Mode 0x01 / PID 0x46 | Status: 0x00 [SUCCESS] | Len: 1 | Payload: [ 47 ]
[18:18:24.190] [Response] Mode 0x01 / PID 0x49 | Status: 0x00 [SUCCESS] | Len: 1 | Payload: [ 24 ]
[18:18:24.221] [Response] Mode 0x01 / PID 0x4A | Status: 0x00 [SUCCESS] | Len: 1 | Payload: [ 25 ]
[18:18:24.251] [Response] Mode 0x01 / PID 0x69 | Status: 0x00 [SUCCESS] | Len: 7 | Payload: [ 1B00 0000 0000 00 ]
[18:18:24.280] [Response] Mode 0x01 / PID 0x6D | Status: 0x00 [SUCCESS] | Len: 11 | Payload: [ 0200 000D 1600 0000 0000 00 ]
[18:18:24.311] [Response] Mode 0x01 / PID 0x70 | Status: 0x00 [SUCCESS] | Len: 10 | Payload: [ 0200 000C 8000 0000 0000 ]
[18:18:24.341] [Response] Mode 0x01 / PID 0x78 | Status: 0x00 [SUCCESS] | Len: 9 | Payload: [ 0703 7D03 5002 E000 00 ]
[18:18:24.371] [Response] Mode 0x01 / PID 0x7A | Status: 0x00 [SUCCESS] | Len: 7 | Payload: [ 0100 4600 0000 00 ]
[18:18:24.416] [Response] Mode 0x01 / PID 0x83 | Status: 0x00 [SUCCESS] | Len: 9 | Payload: [ 33FF FFFF FF00 0000 00 ]
[18:18:24.460] [Response] Mode 0x01 / PID 0x88 | Status: 0x00 [SUCCESS] | Len: 13 | Payload: [ 0000 0000 0009 7700 0000 0000 00 ]
[18:18:24.491] [Response] Mode 0x01 / PID 0x8B | Status: 0x00 [SUCCESS] | Len: 7 | Payload: [ 1F00 6900 0000 00 ]
[18:18:24.521] [Response] Mode 0x01 / PID 0x8C | Status: 0x00 [SUCCESS] | Len: 17 | Payload: [ 3000 0000 0000 0000 0020 0A20 0A00 0000 00 ]
>> q
[18:18:52.483] [*] 終了処理中...通知を停止して安全に切断します...
4. 取得したPIDについて(考察)
初回記事の『サポートPIDを調査』で得た、次のPIDの情報がすべて取得できることを確認しました。
PID=0x69 以降のマルチフレームで返るような情報については、以前リンクしたWikipediaでは 詳細が不明でした。が、次のOBD-II J1979 PID Listingを見つけました。
| $\small\textsf{ PID }$ | 意味 | 生データ | 計算式 | 結果 |
|---|---|---|---|---|
| 0x01 | Monitor status | 00 0F EA 00 | MIL=off, DTC数=0 | MIL=0 DTC=0 Raw=000FEA00 |
| 0x04 | Load | 3C | 60/255*100 | 23.5% |
| 0x05 | 冷却水温 | 4D | 77-40 | 37℃ |
| 0x0C | RPM | 0C 2E | 3118/4 | 779.5rpm |
| 0x0D | 車速 | 00 | — | 0km/h |
| 0x0F | 吸気温 | 49 | 73-40 | 33℃ |
| 0x10 | MAF | 05 56 | 1366/100 | 13.66g/s |
| 0x11 | スロットル | 1B | 27/255*100 | 10.6% |
| 0x1C | OBD規格 | 06 | — | EOBD(Euro OBD) |
| 0x1F | 始動後経過時間 | 00 00 | — | 0秒 |
| 0x21 | MIL点灯距離 | 00 00 | — | 0km |
| 0x30 | ウォームアップ回数 | AF | — | 175回 |
| 0x31 | DTC消去後距離 | 05 4B | — | 1355km |
| 0x33 | 大気圧 | 64 | — | 1000hPa |
| 0x41 | Monitor(今サイクル) | 00 1F EA EA | — | MIL=0 DTC=0 Raw=001FEAEA |
| 0x42 | 制御電圧 | 2D 00 | 11520/1000 | 11.52V |
| 0x45 | 相対スロットル | 00 | — | 0.0% |
| 0x46 | 外気温 | 47 | 71-40 | 31℃ |
| 0x49 | アクセルペダルD | 24 | 36/255*100 | 14.1% |
| 0x4A | アクセルペダルE | 25 | 37/255*100 | 14.5% |
| 0x69 | EGR | 1B 00 00 00 ... | Cmd/Actual A,B全て0% | CmdA:0.0% ActA:0.0% CmdB:0.0% ActB:0.0% |
| 0x6D | コモンレール圧 | 02 00 00 0D ... | (3328+22)*10 | RailA:33.5MPa |
| 0x70 | ブースト圧 | 02 00 00 0C ... | 3200/32 | SensA:100.0kPa |
| 0x78 | EGT Bank1 | 07 03 7D 03 ... | S1/S2/S3 | S1:49.3℃ S2:44.8℃ S3:33.6℃ |
| 0x7A | DPF差圧 | 01 00 46 00 ... | 70/100 | Delta:0.70kPa |
| 0x83 | NOxセンサー | 33 FF FF FF ... | B1S1/B1S2=0xFFFF | B1S1:N/A B1S2:N/A |
| 0x88 | SCR誘導システム | 00 00 00 00 ... | (下記参照) | 現在の状況:すべて正常 |
| 0x8B | DPF状態 | 1F 00 69 00 ... | Trigger=105/255*100 | Regen=0 Type=Passive Trigger:41.2% |
| 0x8C | O2ワイドレンジ | 30 00 00 00 ... | Lambda B1S1/S2 | Lambda0:1.0006 Lambda1:1.0006 |
PID=0x88について、Google AIに分析してもらいました。
『
13バイトのRAWデータ(00 00 00 00 00 09 77 00 00 00 00 00 00)を SAE J1979(ISO 15031-5)規格の PID 0x88 (SCR Inducement System) に基づいてデコードします。
結論から申し上げますと、このデータは「現在SCRシステムは正常(誘導システムは非作動)であるが、過去の特定の走行区間で、NOx排出量超過および尿素水(AdBlue)の消費異常が発生し、その状態で『2,423 km』走行した履歴がある」ことを示しています。
詳細な解析結果
RAWデータのバイト分解
- 13バイトのデータを先頭から順に A から M の変数に割り当てます。
- Byte A (現在の状態): 00
- Byte B (履歴状態): 00
- Byte C (履歴状態): 00
- Byte D (距離データ上位): 00
- Byte E (距離データ下位): 00
- Byte F (距離データ上位): 09
- Byte G (距離データ下位): 77
- Byte H〜M (予備/その他履歴): 00 00 00 00 00 00
- 現在の状態 (Byte A) = 00
- 結果:すべてクリア(正常)
- 解説:Bit 7(ACTIVEフラグ)を含めすべて 0 です。現在、エンジン始動制限や警告などのインデュースメント規制は一切発動していません。
- 過去の履歴状態 (Byte B, C) = 00, 00
- 結果:直近(0〜20,000km区間など)の履歴はなし
- 解説:比較的最近の走行ブロック履歴においては、誘導システムを作動させるような異常は記録されていません。
- 誘導システム作動時の走行距離 (Byte F, G) = 09 77
- 結果:2,423 km
- 解説:J1979規格におけるこのセクションの距離データは、2バイト(16ビット)を結合したバイナリ値に 1 count = 1 km のスケーリングを適用します。0x0977 を10進数に変換:((9 \times 256) + 119 =) 2,423つまり、過去の累積(あるいは特定の走行ブロック)において、SCRの警告・規制状態のまま「2,423 km」走行した履歴がECU内部に刻まれています。
- その他の履歴バイト (Byte H〜M) = 全て 00
- 過去の古いブロック(30,000km〜40,000km等)や、その他の特定のインデュースメントトリガー履歴(触媒の紛失、断線、不正なDEF品質など)はすべて 0(記録なし)となっています。
💡 診断のポイント現在はアクティブなエラーがなくシステムも正常(Byte A = 00)ですが、過去に長距離(2,423km分)にわたり排ガス異常状態のまま走行した「消えない過去ログ」が残っている状態です。このログをスキャンツールで強制消去(Service $04)しようとしても、J1979法規の規定により「SCR誘導履歴はコードクリアコマンドでは消去できない(do not reset upon code clearing)」仕様になっています。
『診断』の意味合いが強いですね。もし、走行中に警告やエラーが起きた場合は、車内のインフォメーションシステムで通知されるものと思います。
次回は、これらの情報をもとにデコードした情報を表示します。
(4) へ続く

