システム開発部のTです。
今回は車ネタってことで、ELM327を使ってAndroid版シフトインジケーターアプリを作ってみたので、そのことについて話します。

さて、前回はPCとELM327を接続してコマンドを実際に送信し、車速等を受信できたことを確認したかと思います。今回は実際にAndroidアプリを作りながら実装していきます。

開発環境

Androidアプリの開発となりますので、以下の準備をします。

  • AndroidStudio3.5
    とりあえず正規版最新をダウンロード
    開発言語はKotlinとなります
  • OBD2マルチメーター(M-OBD-V01)
    だいたい2000円くらいで購入可能
    MAXWINのELM327デバイス
    Bluetooth接続タイプ
  • 2003年以降のOBD2コネクタのある車
    なるべく最新の車が望ましいです
  • Android5.0以降の端末
    V5.0以前のバージョンでも開発可能だが、一旦縛る

ライブラリ選定

本件にてメインで利用するライブラリです。
個人的には以下を選定しましたが、個々使いやすいのをご使用いただければと思います。

メインでは上記2つを選択しましたが、実際にアプリを作成するときはDaggerや、RxJava、AndroidXなども入れてます。

要件定義の再確認

前回の要件定義を再確認します。

  • 車速、エンジン回転数の表示
    ELM327と接続し、車速、エンジン回転数を取得し、画面に表示する機能の実装
  • シフトインジケーター機能の実装
    シフトインジケーターの表示については、車速、エンジン回転数、設定画面で入力した車両情報から割り出し、その結果を画面に表示する
  • シフトタイミングランプの実装
    設定したエンジン回転数以上になったときに、画面効果と音でユーザーに通知する機能
  • OS側であらかじめペアリング済みのELM327と接続するための設定画面の実装
  • 車の情報(タイヤの外径、ギアレシオ、エンジン回転数警告)を登録するための画面の実装

上記の要件的には、以下の画面数になりますね。
Navigation Graphをそのまま表示・・・。

上記より、大きく以下の画面を用意しております。

  • タイトル画面
    トップ画面
  • メイン画面(mainFragment)
    シフトインジケーターを表示
  • 設定画面(settingFragment)
    車両情報の登録
  • ELM327との接続画面(elm327Fragment)
    利用するELM327を選択&接続確認
  • ライセンス画面
    ライブラリ等を一覧表示

全ての実装を紹介するとキリ無いので、本件では上記より以下の画面での実装の話をやっていきたいと思います。

  • タイトル画面
  • ELM327接続画面
  • メイン画面

タイトル画面

ただのタイトル画面ですが、このタイミングでRun Permissionの確認しましょう。実際は接続する直前でもいいかな?とも思いますが、記事を書くうえではキリがいいので・・・。

とはいえで、処理書くまえに、AndroidManifestには以下を追記しておきましょう。

    <uses-permission android:name="android.permission.BLUETOOTH" />
    <uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
    <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />

Run Permissionの対象は「ACCESS_COARSE_LOCATION」になります。
Android6.0から必要になったパーミッションですね。
個人的には、PermissionsDispatcherライブラリを利用して、Activityに実装しておりますが、Bluetooth接続する前であれば、タイミングはどこでもいいかと思います。PermissionsDispatcherの実装方法については、割愛させてください。公式サイトにも記載されているので、それに習って実装しましょう。

一応、NeedsPermission定義のメソッド内で、Bluetoothの有効化を実装してもいいかと思います。

    @NeedsPermission(Manifest.permission.ACCESS_COARSE_LOCATION)
    fun checkBluetooth() {
        val rxBluetooth = RxBluetooth(this)
        if (!rxBluetooth.isBluetoothEnabled) {
            rxBluetooth.enable()
        }
    }

ELM327接続画面

さて、ここから実装の話になりますが、まずはこの画面の要件を箇条書きしますね。

  • ペアリング済みのデバイスを一覧表示
    無条件に一覧表示するのではなく、シリアル通信可能なデバイスのみを表示するようにします。
  • 一覧から任意のデバイスを選択し、接続確認を実行
    そのデバイスがELM327であるかどうかをチェックする処理をします。
  • ELM327デバイスであれば、MACアドレスをストレージに保持
    チェック後、ELM327であった場合は、メイン画面で再度接続が必要であるため、あらかじめMACアドレスをストレージ等に保持しておきます。
  • ELM327有無はATZコマンドでチェック
    ATコマンドの一種で、ATZコマンドがございます。これを実行することでELM327であるかの確認が可能です。

画面イメージは以下を想定。

こんなところでしょうか。
早速、実装の説明に進みたいと思います。

ペアリング済みのデバイスを一覧表示

ペアリング済みのデバイス一覧を取得するのは特に難しくありません。
以下のコードで取得可能です。
(本件ではRxBluetoothライブラリを利用したコーディングで実装致します)

val rxBluetooth = RxBluetooth(this)
val deviceList: List<BluetoothDevice> = rxBluetooth.bondedDevices

上記で簡単に取得できるのですが、このままだとELM327のようなシリアル通信不可能なデバイスも一緒に表示されることになってしまうので、以下のようにデバイスのUUID(本件ではSPP(Serial Port Profile)のみ対象)を参照し、SPP以外はフィルタしましょう。

val SPP_UUID = "00001101-0000-1000-8000-00805f9b34fb"
val rxBluetooth = RxBluetooth(this)
val deviceList: List<BluetoothDevice> = rxBluetooth.bondedDevices
       .filter { device ->
           // シリアル通信をサポートしているデバイスのみ抽出する
           device.uuids?.find { it.uuid.toString() == SPP_UUID } != null
       }

これでデバイス一覧に表示する内容が絞られたかと思います。
今度は一覧に表示されたものから、ELM327を選択することになりますが、選択されたものが必ずしもELM327であるかは分からないので、ここでATコマンドを利用したチェック処理を実装していきたいと思います。

デバイスチェック処理

このチェック処理は以下の流れで実装致します。

  1. 選択したデバイスに接続
  2. 接続が確立したら、ATZ(設定の初期化)コマンドを送信
    本件では、初期化というよりはデバイス名確認のために実行する。
  3. コマンドのレスポンスをチェックし、「ELM327」の文字列が存在していること
  4. ELM327であった場合、そのデバイスのMACアドレスを保持しておく

上記の感じでしょうか。
ここで初めてコマンドをデバイスに送信する処理が出てきましたね。
ELM327に接続~コマンド送信~切断までを説明します。

まずはデバイスに接続するところから。

    fun connectDevice(position: Int): LiveData<BluetoothSocket?> {
        // 一覧に表示されたデバイスから1つを選択
        val device = deviceList[position]
        // BluetoothSocket用のLiveData
        val socket = MutableLiveData<BluetoothSocket?>()
        // 上記にて選択したdeviceと接続する
        disposable.add(
            rxBluetooth.observeConnectDevice(device, UUID.fromString(SPP_UUID))
                .retry(3)
                .subscribeOn(Schedulers.io())
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe({
                    // 接続成功時、Socketを取得するので、
                    // LiveData経由で本メソッドの呼び出し元に通知する
                    socket.value = it
                }, {
                    // エラーの場合、socketにNULLを通知
                    socket.value = null
                })
        )
        return socket
    }

上記のような感じにしてみました。
rxBluetooth.observeConnectDeviceの引数に選択済みのデバイス、SPPのUUIDを設定して接続していきます。接続不良も考慮し、リトライ3回まで実行するようにしました。
rxBluetooth.observeConnectDevice自体、内部がRxJavaになっているので、動作的には非同期になっています。そのため、接続成功時に、BluetoothSocketを取得できた場合、本メソッドの呼び出し元に通知するようにLiveDataでリターンするようにしました。

さて、Socketを取得したところで、続けてコマンドの送受信の実装に入ります。
以下、まとめて送受信部を書いてしまいますね。

    fun checkElm327Command(socket: BluetoothSocket): LiveData<Boolean> {
        // 送受信部は非同期なので、ここでもレスポンスはLiveData任せ
        val response = MutableLiveData<Boolean>()
        // デバイス接続時に取得したSocketを引数に、BTコネクションを取得する
        val bluetoothConnection = BluetoothConnection(socket)
        // コマンドのレスポンス取得処理を実装
        disposable.add(
            //OBD2のレスポンス終端値">"を設定
            bluetoothConnection.observeStringStream('>'.toInt()) 
                .subscribeOn(Schedulers.io())
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe(
                    { res ->
                       // レスポンスに「ELM327」をチェック、Booleanを返却
                        response.value = (res.indexOf("ELM327") != -1)
                    },{
                       // 例外時はfalseを返却
                        response.value = false
                    }
                )
        )
        // コマンド送信処理を実装
        // `ATZ`コマンドを送信する際、`\r`を付与しておくこと
        bluetoothConnection.send("atz\r".toByteArray())

        return response
    }

要所にコメント入れましたが、送受信は非同期で動くので、LiveDataを利用しています。observeStringStreamで受信待ちし、sendの引数にコマンドatzを設定し、送信します。コマンド後ろの\rは必要みたいです。

その後、observeStringStreamsubscribeonNextに以下のレスポンスが流れてくるはずです。

[\r][\r]ELM327 v1.3[\r][\r]OK>

上記のELM327の文字列をチェックし、存在していればELM327デバイスということになります。なお、余談ですがELM327のクローン品が横行しているようで、最悪ATコマンドすら受け付けない品もあるとのことです。上記の内容が返却されれば、一応は大丈夫かと思います。

文字列のチェックに関しては、内部にELM327が存在しているかを確認するだけなので、res.indexOf("ELM327") != -1で比較しています。この結果を呼び出し元に通知し、trueだったら以降の処理で利用するので、ELM327デバイスのMACアドレスを端末のストレージに保持しておきましょう。

また、チェック処理終了後、コネクションをつかみっぱなしにせず、終了処理も忘れないように、以下を実行しておきましょう。

bluetoothConnection.closeConnection()

以上で、ELM327接続処理の実装内容になります。

メイン画面

と行きたかったのですが、記事が長くなってきたので、
本日はここまでとします。


次回
「ELM327でシフトインジケータアプリを作成・実装編②」につづく

前回
「ELM327でシフトインジケータアプリを作成・接続確認編」はこちら



ギャップロを運営しているアップフロンティア株式会社では、一緒に働いてくれる仲間を随時、募集しています。 興味がある!一緒に働いてみたい!という方は下記よりご応募お待ちしております。
採用情報をみる