ELM327でシフトインジケータアプリを作成・実装編②からのつづきです。

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

さて、前回の実装編②からの続きで、今度は受信処理になります。
今回で実装編完結です。

ELM327コマンド受信処理

実装編①でもATZコマンドの受信処理がございましたが、基本的には違いはございません。今回は、車速とエンジン回転数を取得するための処理を実装します。
全体的なコードは以下となります。

// UIスレッドに処理を渡すために定義
private val uiHandler = Handler(Looper.getMainLooper())
// UI側に値を通知するためのLiveData
private val meterLiveData = MutableLiveData<MeterContainer>()

private fun receiveResponse(connection: BluetoothConnection) {
    // レスポンス最後尾文字を示す「>」を設定しておく
    connection.observeStringStream('>'.toInt())
        .map { st ->
            // (1)余分なエスケープシーケンスを取り除き、
            //       半角スペースで分割後、16進数文字を数値化
            st.replace("r", "")
              .split(" ")
              .filter { it.isNotEmpty() }
              .map { it.toInt(16) }
        }
        .subscribeOn(Schedulers.io())
        .subscribe(
            { intArray ->
                when {
                    // (2)レスポンスがエンジン回転数だった場合
                    intArray.size >= 4 && intArray[1]==0x0C -> 
                      revCount = (intArray[2] * 256 + intArray[3]) / 4
                    // (3)レスポンスが車速だった場合
                    intArray.size >= 3 && intArray[1]==0x0D -> 
                      speedCount = intArray[2]
                    else -> 
                      Log.i("Command response error!")
                }
                // (4)ギアポジションを取得
                val gear = getGearRatio()
                // (5)UIスレッドに上記の値を渡す
                uiHandler.post {
                    // 上記で取得した値をLiveData経由で画面に表示する
                    meterLiveData.value = gear
                }            
            },{ e ->
                // 任意のエラー処理を実装
            }
        )
}

上記でのポイントは(1)~(5)になります。
順に説明します。

(1)レスポンス文字列の最適化

重要なのはレスポンスの文字列がどういう形で来ているか?
イメージとしては、以下の内容となります。
(※ zzにはコマンドID、xxには結果の数値が入ります)

01zzr41 zz xx xxrr

rは改行を表していて、実際の文字列としては見えない制御文字になります。
でもって、mapでやっていることは、

st.replace("r", "")
	.split(" ")
	.filter { it.isNotEmpty() }
	.map { it.toInt(16) }

何をやっているかというと、

  1. 01zzr41 zz xx xxrrからrを取り除く
    つまり、
    before: 01zzr41 zz xx xxrr
    after: 01zz41 zz xx xx
    としている。
  2. 半角スペースでレスポンス文字列分割
    before: 01zz41 zz xx xx
    after: 01zz41zzxxxx
    と分割され、文字列配列化される。
  3. 配列化された文字列に、空文字、NULL値を排除
    基本的にあり得ないとは思うが、一応判定としてフィルタ入れました。
    なので、01zz41、zz、xx、xxがそのまま以降に流れます。
  4. 16進数文字列としてIntに変換
    01zz41zzxxxxのそれぞれの16進数文字列に対して数値に変換。

ということをやっています。
最終的に数値配列になったデータをsubscribeに送って、(2)以降の処理につなげています。

(2)エンジン回転数の取得

(1)から流れてきたデータを解析し、それがエンジン回転数だった場合の処理になります。
例えば、以下のようなレスポンスデータが流れてきたとします。

010C41、0C、32、43

上記のデータの意味ですが、

  • 010C41
    リクエスト時に投げたコマンドそのものに、レスポンスを表す0x40にサービス番号を示す0x01を合算した値0x41を指しています。
    本件では、特に利用しないので無視しています。
  • 0C
    リクエストのコマンドIDになります。
    0Cはエンジン回転数を取得するコマンドIDを指します。
    本件では、何のコマンドのレスポンスなのか?という判定に利用します。
  • 上記以降の値
    本件では3243を指しています。
    これこそ本件で取得したいメインのレスポンス値になります。
    実際のエンジン回転数になります。

という意味がございます。
上記にて、エンジン回転数を求めますが、「上記以降の値」が示している3243だけではなんのこっちゃ?ってなるかと思います。

なので、以下のWikiを参照してみてください。
https://en.wikipedia.org/wiki/OBD-II_PIDs#Service_01

すると、以下に計算式が載っています。

つまり、32に対して256倍し、それに対して43を足した数を4で割った値がエンジン回転数ということです。
実際の計算式では、
(0x32 * 256 + 0x43)/ 4 = 3216
ということで、実際の回転数は3216となります。
コードで示すと、

// (2)レスポンスがエンジン回転数だった場合
intArray.size >= 4 && intArray[1]==0x0C -> 
	revCount = (intArray[2] * 256 + intArray[3]) / 4

上記では、intArrayの配列数をチェックし、配列数が4つであること、
かつ、intArray[1]がコマンドIDになるので、0x0Cのエンジン回転数のコマンドであるかをチェックしています。
intArray[2][3]がそれぞれ、0x320x43が入っているので、
revCount = (intArray[2] * 256 + intArray[3]) / 4
の計算式でエンジン回転数が取得可能ということになります。

(3)車速の取得

基本的にはエンジン回転数を取得するときと同じです。
レスポンスは以下のように来るかと思います。

010D41、0D、32

上記の0DというのがコマンドIDになります。
車速を示すIDですね。
その次にある32が速度を指しています。
この速度、回転数のときと違い、10進数にするだけで速度が分かります。
0x32になるので、実際は時速50kmってことになります。
コードで示した場合、

// (3)レスポンスが車速だった場合
intArray.size >= 3 && intArray[1]==0x0D -> 
	speedCount = intArray[2]

上記だけです。
さて、ここまででELM327からの必須項目は取得できたかと思います。
受信時は、コマンドIDを確認し、何が受信されたかをチェック。
その後、各コマンドIDにそった値が返却されるので、intArray[2]以降の値をチェックしていきましょう。

車速とエンジン回転数が取得できたところで、いよいよギアポジションの取得にいきます。

(4)ギアポジションを取得

以下、実際にギアポジションを取得するための処理になります。

private fun getGearRatio(): String {
    // (タイヤ外径*回転数*60)/ 速度=現在のギアポジションに紐づくギア比
    val gearRatio = (tireOutSize * revCount.toFloat() * 60f) / speedCount.toFloat()
    // 以下にてギアポジションを取得する
    val gearPosition = gearRatioList
        .map { gr ->
            // 走行中のギア比を、リスト化した比較対象のギア比で除算
            Pair(gr.first, gearRatio / gr.second)
        }
        .map {
            // 登録済みのギア比との差を取得
            Pair(gr.first, abs(1f - it.second))
        }
        .minBy {
            // ギア比との差が最小のデータを返却する
            it.second
        }
        ?.let { it.first }?: kotlin.run { 0 }

    // 速度が0だった場合、取得したギアポジションではなく、Neutralの"N"を返却する
    return if (speedCount > 0) gearPosition.toString() else "N"
}

最初の計算式で走行中のギア比を求めていますが、以下の方程式を見ていただければ意味が通じるかと思います。つまり、ここでいうギア比については、
ギアレシオ×ファイナル比×1000000の値となることが分かっていただけると思います。

でもって、比較するためのギア比については、「ギア比の取得」でやっていますよね?
なので以降の処理で、走行中のギア比と、リスト化した比較対象のギア比を除算し、除算結果が限りなく数値の1に近いものが紐づくギアポジションになります。minByでは、その限りなく走行中のギア比に近いギアポジションを返すようにしています。(ちょっとややこしいかな・・・)

最後に、走行時の速度を確認し、車が止まっている状態の場合は、Neutralを示すNを返すようにしています。それ以外は、ギアポジションを返すようにします。

ここで注意になりますが、走行中のギア比と、比較するためのギア比が一致することはほぼ無いと思っていいです。確かに計算上は一致するはずですが、タイヤの外径というのはタイヤを酷使していれば減っていくため、数値どおりにいかないのです。なので、計算上ではギア比にもっとも近いものをギアポジションとしています。

以上で、本機能を満たすための値は全て取得できたかと思います。
最後に、その値をUIスレッドに渡して画面表示すればOKかと。

(5)UIスレッドに上記の値を渡す

よく見てもらえばわかりますが、subscribe内はUIスレッドではございません。
そもそも、オペレータに以下が付与されていないので、UIスレッドにはなっていません。

.observeOn(AndroidSchedulers.mainThread())

なので、わざわざUIスレッドで実行してもらうためのHandlerを定義しております。

// (5)UIスレッドに上記の値を渡す
uiHandler.post {
	// 上記で取得した値をLiveData経由で画面に表示する
	meterLiveData.value = gear
}

「何故だ!!?」とギレン総帥のように叫びたくなるとは思うのですが、BluetoothConnectionobserveStringStreamを追っていただけると分かります。
上記のメソッドの返却値はFlowableになっており、onBackpressureBufferでスレッド間のデータの流れを制御していますが、observeOnを定義することで、スレッド間制御が入ってしまい、制御するためのバッファ溢れがおき、結果としてsubscribeに流れてこなくなってしまうのです。

なので、それを回避すべく、observeOnを定義せず、そのままsubscribeに流すようにしました。これ以降はHandler経由でUIスレッドに処理を委譲できます。

私自身、動作検証中に画面表示している値の更新がしばらくしてピタっと止まってしまったことから、調査して判明した事象です。解決策は他にもあるとは思うのですが、本件ではHandlerでUIスレッドに渡すようにしました。
取得したギアポジションをLiveDataで渡して終了。

以上が、ELM327からの受信処理になります。

まとめ

いかがだったでしょうか。
今回実装編①〜③と、膨大な内容となりましたが、これでシフトインジケーターとしての最低限の機能の実装は可能になるかと思います。

おさらいになりますが、最後本件で記載した内容を箇条書きします。

  • 接続確認編
    実装前に、ターミナルソフトでELM327と会話する方法を記載しています。
  • 実装編①
    ここでは、BluetoothでELM327に接続する処理を記載しました。
    接続自体は難しくはありませんが、ELM327デバイスであるかの簡単なチェック処理を記載しました。
  • 実装編②
    ELM327に対して、車速とエンジン回転数を取得するためのコマンド送信処理を記載しました。今回のように、数種類のコマンドを送信するうえで気をつけるべき内容を記載しました。
  • 実装編③
    ELM327からのコマンド受信処理についてまとめてみました。
    車速と、エンジン回転数のレスポンスを受けてから、その実数値にパースする処理の内容、シフトポジションを求めるための計算などを記載しています。

ELM327を利用しての車との対話はなかなか機会を得ることが無いとは思いますが、この記事を機会に皆様にも車との関わりをもつのもよろしいかと思います。

今後も、ELM327に対する記事を投稿できればと思いますので、その際見ていただければと思います。

以上、ありがとうございました。



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