0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

前回、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 のキューを利用します。

can_v3.ino
//  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, &current_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();
  }
}

partition.png

前回と同様、Arduino-IDEメニュー「ツール」→「Partition Scheme」で"Minimal SPIFFS (1.9MB APP with OTA/128KB SPIFFS)"を選択します。

Arduino-IDEメニュー「スケッチ」→「コンパイル済みバイナリをエクスポート」にて、バイナリファイルを出力します。

http://192.168.4.1/update (ElegantOTA) の Select Filecan_v3ino.binを指定して ESP32-C3へ書き込みます。

2. リクエスト側(表示側)テストアプリ

ゲートウェイ化した ESP32-C3プログラム をテストするために、次のPythonアプリを用意しました。

  • REPLにて、任意のタイミングで、任意のPIDを(複数)リクエストできる
  • レスポンスは非同期に受信し、(デコードは無し)RAWデータとして表示
    (レスポンスはリクエストと対応付けしない)
gateway_test.py
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_result.png

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
  1. 現在の状態 (Byte A) = 00
    • 結果:すべてクリア(正常)
    • 解説:Bit 7(ACTIVEフラグ)を含めすべて 0 です。現在、エンジン始動制限や警告などのインデュースメント規制は一切発動していません。
  2. 過去の履歴状態 (Byte B, C) = 00, 00
    • 結果:直近(0〜20,000km区間など)の履歴はなし
    • 解説:比較的最近の走行ブロック履歴においては、誘導システムを作動させるような異常は記録されていません。
  3. 誘導システム作動時の走行距離 (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内部に刻まれています。
  4. その他の履歴バイト (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) へ続く

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?