AndroidアプリでのOBD2を使った車載情報の取得方法
はじめに
こんにちは。ほりおーです。ナビゲーション向けの測位技術の研究開発を担当しています。
今回は当社のカーナビアプリ『カーナビタイム』でリリースされました、OBD2接続によるトンネル内の位置精度向上機能に関する簡単な解説と、AndroidでのOBD2の情報取得の実装についてご紹介できればと思います。
▼2023/04/20配信のプレスリリース
OBD2とは?
はじめにOBD2について簡単にご紹介します。
OBD2とは「On Board Diagnostics Ver.2」の略称で車載故障診断装置とも呼ばれます。
主に車検などで利用され、エンジン回転数など目には見えない車両情報を数値で出力して確認することができる機器です。
2009年以降、日本の車両にはOBD2搭載が義務付けられており、現在はほとんどの車両で利用可能です。
こちらの情報はOBD2アダプターと呼ばれる出力機器を使い、BluetoothやWi-Fiで接続することでスマートフォンでも車両情報を取得することが可能です。
なぜOBD2を使うと位置精度が上がる?
『カーナビタイム』はスマートフォン向けアプリのため、ユーザーの位置情報には主にスマートフォンのGPSセンサーを利用しています。
GPSセンサーの位置情報を利用する際の課題の一つとして、GPS衛星から電波が受信が出来ない場合に位置の精度が悪くなるという問題があります。
特にトンネル内では電波が全く受信できないため、ユーザーの位置を特定することが非常に難しいです。
GPS情報のみでトンネル内のユーザーの位置を推定する場合は、トンネル進入時の速度と走行時間から位置を算出する必要があります。
しかし、実際の走行ではトンネルの形状や渋滞によってトンネル内で加減速を行うため、進入時の情報だけでは推測は容易ではなく、殆どの場合で実際と異なる位置を表示してしまいます。
この問題を解決するために、今回当社ではOBD2から取得できる車両の速度情報を利用しています。
OBD2から取得できる情報は接続されていれば常に取得可能なため道路形状などの影響を受けません。
更に、車両から直接取得した情報であるため、GPSで算出している速度情報よりもより高精度な情報を利用可能です。
この速度情報からトンネル内の位置を割り出すことで、加減速や停止などの情報も加味した位置を算出でき、実際の走行位置と遜色のない位置情報を提供することが出来ます。
OBD2を使った車載器情報の取得手順
ここからは実際にAndroidアプリでOBD2の情報を取得する方法をご紹介したいと思います。
① OBD2アダプターとBluetooth接続する
はじめにOBD2アダプターとAndroid端末をBluetooth接続します。
BLEでの接続の実装方法は公式ドキュメントで紹介されているため、こちらを参考にしてください。
機器との接続が出来たら、BluetoothGattCallback#onServicesDiscoveredの通知でOBD2情報のリクエストと結果の取得に必要な書き込み用のCharacteristicと通知用のCharacteristicを取得します。
各種UUIDはOBD2アダプター毎に異なるため、ドキュメントやBluetooth情報を取得できる外部アプリなどを用いてUUIDを確認してください。
override fun onServicesDiscovered(gatt: BluetoothGatt?, status: int) {
// OBD2サービス取得
val obd2Service = gatt.services.find { it.uuid == "接続したいOBD2のサービスUUID" }
// キャラクターのリスト取得
val characteristics = obd2Service.characteristics
// 書き込みキャラクターを取得 (リクエスト時に使用する)
val writeCharacteristic = characteristics.find { it.uuid == "接続したいOBD2の書き込み用キャラクターUUID" }
// 通知用のキャラクターを取得
val notifyCharacteristic = characteristics.find { it.uuid == "接続したいOBD2の通知用キャラクターUUID" }
// 通知用のキャラクターを設定. OBD2情報のリクエストを受け取れるようになる
gatt?.setCharacteristicNotification(notifyCharacteristic, true)
}
② 車両情報をリクエストする
OBD2の情報はサービスID(以下SID)とパラメータID(以下PID)と呼ばれる二種のIDの組み合わせで情報をリクエストします。
取得できる情報のSIDとPIDの組み合わせ表は下記に詳細に記載されています。
リクエストするときのフォーマットは"SID PID\r"となり、例えば速度情報を取得する際は"01 0D\r"になります。
この情報を①で取得したBluetoothの書き込み用Characteristicにバイト配列として設定し、BluetoothGattの書き込み処理を呼ぶことでリクエストができます。
// 速度を取得するコマンドのバイト配列
val speedCommand = "01 0D\r".toByteArray()
// 書き込み用のキャラクターに設定
writeCharacteristic.setValue(speedCommand)
// リクエストの実行
bluetoothGatt.writeCharacteristic(writeCharacteristic)
③ 結果通知から値を取得する
リクエストが成功するとバイト配列が返却されます。
そのまま利用すると、終端文字や処理中のログ文字列などのノイズが入るためそれらを除外し、文字列に変換すると「01 0D 41 0D 3C」のような16進数の値となります。
それぞれの値は下記のようになります。
ここの情報を元に16進数から10進数への変換や、単位、情報ごとの計算式を元に値を計算します。単位や情報ごとの計算式に関しては②で紹介したページに同様に記載されています。
例えば、速度(SID=01、PID=0D)の場合は単位はkm/h、計算式はAとなり、エンジン回転数(SID=01、PID=0C)の場合は単位はrpm、計算式は(256*A + B) / 4となります。
計算式内で表されているA、B…といった値は、上記画像の値A以降を順番に当てはめたものとなります。このアルファベットの種類が多いほどレスポンスに入ってくる情報の数も増えていきます。(エンジン回転数であれば「01 0C 141 0C 56 1E 」となり、計算式内のAが"56"、Bが"1E"となる)
「01 0D 41 0D 3C」の場合は速度のリクエストのため、値は3Cとなり10進数に変換すると60km/hという速度情報が取得できたということになります。
// レスポンス結果の生値
private var resultRawData: String = ""
override fun onCharacteristicWrite(gatt: BluetoothGatt?, characteristic: BluetoothGattCharacteristic?, status: Int) {
// 終端までレスポンスが取得できているかをチェック
if (!fillBuffer(characteristic.value)) {
return
}
// レスポンス結果に含まれていた際に除外する文字列パターン
val REMOVE_PATTERN = Regex("(\\s)|(BUS INIT)|(NO DATA)|(SEARCHING)|(STOPPED)|(\\.)|(:)|(>)|(\r)|(INIT)|(BUS ERROR)|(UNABLE TO CONNECT)|(ELM327.*)|(OK)|(ATZ)")
// レスポンス結果内のノイズを削除
resultRawData = resultRawData.replace(REMOVE_PATTERN, "")
// 16進数のデータを10進数のintのリストに変換
val resultBuffer = resultRawData.chunked(2).map { it.toInt(16) }
// 変数A
val valueA = resultBuffer[5]
// 変数B (B以降は計算式に含まれていなければ結果に入ってこないので配列外アクセスにならないように注意)
val valueB = resultBuffer[6]
...
// 速度の計算式は A なのでそのまま利用可能
val speed = valueA
}
// 終端までの結果かどうかを判定
private fun fillBuffer(value: byteArray) : boolean {
val returnedBytes = try {
// 結果の生データ配列を文字列化
String(value, StandardCharsets.UTF_8)
} catch (UnsupportedEncodingException e) {
""
}
resultRawData = resultRawData + returnedBytes
val end = resultRawData.length
if (end < 3) {
return false
}
val endText = resultRawData.substring(end - 3, end)
// ユニコードの"\r\r>"または"\r\n>"が出てきたらストリーム終端.
// 終端文字は機器によって異なる
return endText == "\r\r>" || endText == "\r\n>"
}
以上がBluetoothを用いたOBD2情報取得の流れです。
位置情報の更新と合わせて都度取得したい場合は、②、③の処理をタイマー処理を使って定期的に実行する必要があります。
終わりに
当社では今回の機能のようにスマートフォン端末以外からの情報を利用した位置精度向上の開発も日々行っております。
安心安全なナビゲーションの大本である正確な位置情報をユーザーに提供できるようにこれからも邁進していきます。
最後に補足ですが、今回の『カーナビタイム』でのOBD2接続に関して下記の制約があるため、利用の際はご注意ください。
Bluetoothバージョン4.0以降が対象
Wi-Fi接続には非対応