使用した OBDⅡアダプタ については、前回の記事を参照してください。
このアダプタは、BLE接続ができるので、ESP32-C3と接続してみました。
1. ESP32-C3 と接続
- 使用した言語
MicroPython
v1.28.0 on 2026-04-06; ESP32C3 module with ESP32C3
- 使用したライブラリ
aioble
This library provides an object-oriented, asyncio-based wrapper for MicroPython's bluetooth API.
- コード
次のコードで接続できますimport asyncio import bluetooth import aioble import utime # --- 設定項目 --- TARGET_BLE_NAME = "OBD" # 「OBD」が含まれれば接続 # BLE UUID設定 UART_SERVICE_UUID = bluetooth.UUID(0xFFF0) TX_CHAR_UUID = bluetooth.UUID(0xFFF1) # Notify(データ受信) RX_CHAR_UUID = bluetooth.UUID(0xFFF2) # Write(コマンド送信) # --- タイムスタンプ生成関数 --- def get_ts(): """起動からの経過時間を [秒.ミリ秒] 形式の文字列で返す""" ms = utime.ticks_ms() return f"[{ms // 1000:4d}.{ms % 1000:03d}]" async def main(): print(f"{get_ts()} 【OBD2 BLEアダプター】'{TARGET_BLE_NAME}'を検索中...") device = None # 10秒間のスキャンを最大10回試行 for attempt in range(1, 11): print(f"{get_ts()} スキャン試行 {attempt}/10 回目 (10秒間)...") async with aioble.scan(duration_ms=10000, interval_us=30000, window_us=30000, active=True) as scanner: async for result in scanner: # デバイス名を取得 raw_name = result.name() if raw_name is None: continue # ヌル文字(\x00)や改行、前後のスペースをクリーンアップ clean_name = raw_name.replace("\x00", "").strip() # MACアドレス(ログ用) addr_str = ":".join(f"{b:02x}" for b in result.device.addr) # デバッグ用:発見したすべてのBLEデバイスをコンソールに薄く出す #print(f" [スキャン中] 発見: '{clean_name}' [{addr_str}]") # "obd" が含まれているか判定 if TARGET_BLE_NAME.lower() in clean_name.lower(): device = result.device print(f"{get_ts()} 発見しました! Target: {clean_name} [{addr_str}]") break if device: break if not device: return print(f"{get_ts()} エラー: 「{TARGET_BLE_NAME}」を含むBLEデバイスが見つかりません。") print(f"{get_ts()} 接続中...") async with await device.connect(timeout_ms=5000, min_conn_interval_us=7500, max_conn_interval_us=15000) as connection: print(f"{get_ts()} 接続成功。通信の安定化を待っています...") await asyncio.sleep_ms(500) # 電波を安定させるためのウェイト print(f"{get_ts()} [最適化] サービス(UUID: FFF0)のみをピンポイント検索中...") # 明示的に上位スコープのUUIDを使用して検索 srv_uuid = bluetooth.UUID(0xFFF0) uart_service = await connection.service(srv_uuid) if uart_service is None: return print(f"{get_ts()} エラー: サービス FFF0 が見つかりませんでした。") print(f"{get_ts()} キャラクタリスティック(FFF1/FFF2)を特定中...") tx_char = await uart_service.characteristic(bluetooth.UUID(0xFFF1)) rx_char = await uart_service.characteristic(bluetooth.UUID(0xFFF2)) # 最終チェック if tx_char is None or rx_char is None: return print(f"{get_ts()} エラー: 通信キャラクタリスティックの特定に失敗しました。") print(f"{get_ts()} 受信(Notify)のsubscribeを開始します...") await tx_char.subscribe(notify=True) print(f"{get_ts()} subscribe成功。通信リンク確立") # プログラムの実行 asyncio.run(main())
- 実行ログ
MPY: soft reboot [ 5.121] 【OBD2 BLEアダプター】'OBD'を検索中... [ 5.122] スキャン試行 1/10 回目 (10秒間)... [ 5.636] 発見しました! Target: OBDII [77:70:07:31:9d:dc] [ 5.640] 接続中... [ 6.223] 接続成功。通信の安定化を待っています... [ 6.732] [最適化] サービス(UUID: FFF0)のみをピンポイント検索中... [ 7.072] キャラクタリスティック(FFF1/FFF2)を特定中... [ 7.193] 受信(Notify)のsubscribeを開始します... [ 7.253] subscribe成功。通信リンク確立 >>>
続けて、ATZなどの初期化コマンドを送信すればよいです...
2. 自由にATコマンドを打つ
OBDⅡアダプタに接続できたので、自由にATコマンドを打ってみて、どんな応答が返るのか見てみたいと思い、次のコードを作成しました。
ESP32-C3はシングルコアですが、asyncio を使ってマルチスレッドで処理することで、ATコマンドを手打ちできるようにしています。
途中、応答の一部が受信できない aioble のバグに遭遇しましたが、AI に助けてもらい完成したコードです。
- ATコマンドを自由に打てるコード
import asyncio import bluetooth import aioble import utime import sys import select from collections import deque # ============================================================ # 設定項目 # ============================================================ TARGET_BLE_NAME = "OBD" UART_SERVICE_UUID = bluetooth.UUID(0xFFF0) TX_CHAR_UUID = bluetooth.UUID(0xFFF1) # Notify(データ受信) RX_CHAR_UUID = bluetooth.UUID(0xFFF2) # Write(コマンド送信) # ============================================================ # グローバル状態 # ============================================================ START_TICK = utime.ticks_ms() _rx_buffer = "" # 受信データの蓄積バッファ(取りこぼし防止のため常時保持) _cmd_queue = asyncio.ThreadSafeFlag() if False else None # 未使用(将来拡張用に残した) def get_ts(): diff = utime.ticks_diff(utime.ticks_ms(), START_TICK) sec = diff // 1000 ms = diff % 1000 return f"[{sec:4d}.{ms:03d}]" # ============================================================ # 受信常駐タスク:BLE Notifyを監視し続け、'>' が来るたびに1応答として出力 # ============================================================ async def notify_listener(tx_char): global _rx_buffer print(f"{get_ts()} [System] 受信常駐タスク起動(キュー直接ドレインモード)") pkt_count = 0 while True: try: # 最初の1件だけ notified() で待つ(キューが空の時のみ安全に動作する) first = await tx_char.notified() pkt_count += 1 #print(f"{get_ts()} [PKT#{pkt_count}] len={len(first)} raw={repr(first)}") _rx_buffer += first.decode('utf-8', 'ignore') # 後続パケットがキューに溜まるのを少し待つ await asyncio.sleep_ms(80) # aiobleバグ回避(issue#934):notified()を再度呼ばず、キューを直接空になるまで吸い出す q = tx_char._notify_queue while q: data = q.popleft() pkt_count += 1 #print(f"{get_ts()} [PKT#{pkt_count}] len={len(data)} raw={repr(data)}") _rx_buffer += data.decode('utf-8', 'ignore') # 次回のnotified()呼び出しのため、イベントフラグを明示的にクリア tx_char._notify_event.clear() # バッファ内に完成した応答('>'まで)があれば切り出して表示 while ">" in _rx_buffer: idx = _rx_buffer.find(">") raw = _rx_buffer[:idx + 1] _rx_buffer = _rx_buffer[idx + 1:] pretty = raw.replace("\r\r", "\n").replace("\r", "\n").strip() print(f"{get_ts()} [応答RAW] {repr(raw)}") if pretty: #print(f"{get_ts()} [応答] {pretty}") pass except Exception as e: print(f"{get_ts()} [System] 受信タスクでエラー: {e}") await asyncio.sleep_ms(100) # ============================================================ # 送信常駐タスク:_next_cmd にセットされたコマンドを検知して送信 # ============================================================ _next_cmd = None async def command_sender(rx_char): global _next_cmd print(f"{get_ts()} [System] 送信常駐タスク起動") while True: if _next_cmd is not None: cmd = _next_cmd _next_cmd = None print(f"{get_ts()} >> [TX] {cmd}") try: await rx_char.write((cmd + "\r").encode()) except Exception as e: print(f"{get_ts()} [System] 送信エラー: {e}") await asyncio.sleep_ms(50) # ============================================================ # REPL入力監視タスク:標準入力を非ブロッキングで監視し、_next_cmdにセット # ============================================================ async def repl_command_listener(): global _next_cmd print(f"{get_ts()} [System] REPL入力監視タスク起動。ATコマンドを入力してEnterを押してください。") poller = select.poll() poller.register(sys.stdin, select.POLLIN) while True: # 0ms待機(ノンブロッキング)で入力があるかだけチェック events = poller.poll(0) if events: line = sys.stdin.readline().strip() if line: _next_cmd = line await asyncio.sleep_ms(50) # ============================================================ # 初期化コマンド送信(起動時の自動シーケンス用) # 受信は notify_listener が並行して処理しているので、ここでは送信して少し待つだけ # ============================================================ async def send_init_cmd(rx_char, cmd_str, wait_ms=700): print(f"{get_ts()} [自動初期化] 送信: {cmd_str}") await rx_char.write((cmd_str + "\r").encode()) await asyncio.sleep_ms(wait_ms) # ============================================================ # BLEスキャン&接続 # ============================================================ async def scan_and_connect(): print(f"{get_ts()} 【OBD2 BLEアダプター】'{TARGET_BLE_NAME}' を検索中...") device = None for attempt in range(1, 11): print(f"{get_ts()} スキャン試行 {attempt}/10 回目...") async with aioble.scan(duration_ms=10000, interval_us=30000, window_us=30000, active=True) as scanner: async for result in scanner: raw_name = result.name() if raw_name is None: continue clean_name = raw_name.replace("\x00", "").strip() addr_str = ":".join(f"{b:02x}" for b in result.device.addr) #print(f" [スキャン中] 発見: '{clean_name}' [{addr_str}]") if TARGET_BLE_NAME.lower() in clean_name.lower(): device = result.device print(f"{get_ts()} ★発見しました! Target: {clean_name} [{addr_str}]") break if device: break if not device: print(f"{get_ts()} エラー: 「{TARGET_BLE_NAME}」を含むBLEデバイスが見つかりません。") return None print(f"{get_ts()} 接続中...") connection = await device.connect(timeout_ms=5000, min_conn_interval_us=7500, # 7.5ms(BLE規格上の最短値) max_conn_interval_us=15000, # 15ms ) print(f"{get_ts()} 接続成功。安定化を待っています...") await asyncio.sleep_ms(500) # 接続直後にMTU拡張を試みる(対応していれば大きめのMTUを要求) try: mtu = await connection.exchange_mtu(247) print(f"{get_ts()} MTU交渉成功: {mtu}") except Exception as e: print(f"{get_ts()} MTU交渉失敗またはメソッド無し: {e}") return connection # ============================================================ # メイン処理 # ============================================================ async def main(): connection = await scan_and_connect() if connection is None: return uart_service = await connection.service(UART_SERVICE_UUID) if uart_service is None: print(f"{get_ts()} エラー: サービス FFF0 が見つかりませんでした。") return tx_char = await uart_service.characteristic(TX_CHAR_UUID) rx_char = await uart_service.characteristic(RX_CHAR_UUID) if tx_char is None or rx_char is None: print(f"{get_ts()} エラー: 通信キャラクタリスティックの特定に失敗しました。") return print(f"{get_ts()} 受信(Notify)の購読を開始します...") await tx_char.subscribe(notify=True) tx_char._notify_queue = deque((), 20) print(f"{get_ts()} subscribe成功。通知キューを深さ20に拡張しました。") await asyncio.sleep_ms(200) # --- 受信・送信・REPL監視の3タスクを先に起動 --- # ここから先は、届いたデータはnotify_listenerが拾ってくれる listener_task = asyncio.create_task(notify_listener(tx_char)) sender_task = asyncio.create_task(command_sender(rx_char)) repl_task = asyncio.create_task(repl_command_listener()) # --- 自動初期化シーケンス(必要に応じてコメントアウト可) --- print(f"\n{get_ts()} --- ELM327自動初期化シーケンス開始 ---") await send_init_cmd(rx_char, "ATZ", wait_ms=1500) await send_init_cmd(rx_char, "ATE1", wait_ms=700) await send_init_cmd(rx_char, "ATL0", wait_ms=700) await send_init_cmd(rx_char, "ATS1", wait_ms=700) await send_init_cmd(rx_char, "ATSP6", wait_ms=700) print(f"\n{get_ts()} [準備完了] REPLからATコマンドを自由に入力してください。") print(f"{get_ts()} (プログラムを止めずに動き続けます。Ctrl+Cで終了)") # --- 3つの常駐タスクを永続的に並行実行 --- await asyncio.gather(listener_task, sender_task, repl_task) # ============================================================ # 実行 # ============================================================ try: asyncio.run(main()) except KeyboardInterrupt: print(f"\n{get_ts()} ユーザーにより中断されました。") finally: asyncio.new_event_loop() # 次回実行時のためにイベントループをリセット
- 実行ログ
前回同様、実車に接続していないため、車速や回転数は取得できていません。
[ 0.001] 【OBD2 BLEアダプター】'OBD' を検索中... [ 0.002] スキャン試行 1/10 回目... [ 0.331] ★発見しました! Target: OBDII [77:70:07:31:9d:dc] [ 0.333] 接続中... [ 1.419] 接続成功。安定化を待っています... [ 1.959] MTU交渉成功: 247 [ 2.439] 受信(Notify)の購読を開始します... [ 2.499] subscribe成功。通知キューを深さ20に拡張しました。 [ 2.708] --- ELM327自動初期化シーケンス開始 --- [ 2.710] [自動初期化] 送信: ATZ [ 2.713] [System] 受信常駐タスク起動(キュー直接ドレインモード) [ 2.719] [System] 送信常駐タスク起動 [ 2.721] [System] REPL入力監視タスク起動。ATコマンドを入力してEnterを押してください。 [ 3.629] [応答RAW] 'ATZ\rELM327 v1.5\r\r>' [ 4.268] [自動初期化] 送信: ATE1 [ 4.409] [応答RAW] 'ATE1\rOK\r\r>' [ 5.028] [自動初期化] 送信: ATL0 [ 5.189] [応答RAW] 'ATL0\rOK\r\r>' [ 5.808] [自動初期化] 送信: ATS1 [ 5.969] [応答RAW] 'ATS1\rOK\r\r>' [ 6.588] [自動初期化] 送信: ATSP6 [ 6.749] [応答RAW] 'ATSP6\rOK\r\r>' [ 7.378] [準備完了] REPLからATコマンドを自由に入力してください。 [ 7.388] (プログラムを止めずに動き続けます。Ctrl+Cで終了) ATRV [ 56.418] >> [TX] ATRV [ 56.699] [応答RAW] 'ATRV\r12.1V\r\r>' 010C [ 64.168] >> [TX] 010C [ 65.039] [応答RAW] '010C\rNO DATA\r\r>' 010D [ 82.288] >> [TX] 010D [ 83.149] [応答RAW] '010D\rNO DATA\r\r>'
次回は、実車に接続して確認してみます。
次回は、実車に接続して確認してみます。
実車に接続して実行しましたが、車速や回転数は取得できませんでした。
接続したクルマは この OBDⅡアダプタ と相性が悪いようです。
このOBDⅡアダプタメーカーのサイトで紹介されていたアプリを使ってみましたが、やはり、車速や回転数は取得できませんでした。
と いうことで、この先に予定していた 後続記事は中止します。
技適もありませんので、これ以降使うこともないでしょう。
簡単に基板が取り出せたので、写真を載せておきます。
- 中央 YMB02 のすぐ下の16pのICは
YM1130:ELM327のコマンドエミュレーションとBluetooth通信機能 - その右横のクリスタル:40MHz
- 上部 YMIOT 横の8pのICは表面に印字無し:SN65HVD230等の互換品のCANトランシーバーICと推測
以上