AndroidスマホとOBD2アダプターを使って車の内部情報を取得するのに「obd2-lib」を活用


# はじめに

bluetooth対応のOBD2アダプターを車のOBD(故障診断)コネクターと接続し、車の内部情報を取得するAndroidスマホアプリを作りたいと思います。類似のアプリは多数ありますが、自分なりに確認したり点や、改造したいところがあったので、自作を考えました。

OBD2アダプターの多くは「ELM327」というチップを使っているようで、車の内部情報を取得するにはOBD2アダプターに、[「ELM327 datasheet」](https://www.elmelectronics.com/wp-content/uploads/2016/07/ELM327DSF.pdf)にあるような手順でATコマンドを送信し、プロトコルの設定や車側の対応の有無のチェックし、対応している情報をリクエスト、データ受信、物理的な値に変換すればよさそうです。

OBD2に関するライブラリーも多くあるようですが、google検索でたまたま見つけた[「obd2-lib」](https://android.googlesource.com/platform/packages/services/Car/+/refs/heads/master/obd2-lib/)（アンドロイド オープンソースプロジェクト？）で公開されているソースコードを活用することで、主な機能を自作せず、車の内部情報を取得できました。

# 検証環境

この記事の内容は、以下の環境で検証しました。
* Windows 10 Home
* Android Studio Chipmunk | 2021.2.1 Patch 1
* targetSdk 32
* 実機確認 Sony SOG04 Android 12,API 31

# obd2-libの内容

obd2-libは以下の図のような構成になっています。
![obd2-lib.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2335076/9c5ec9f7-2634-0635-e47c-465c0bbe73cb.png)

## commands ディレクトリ

`commands`ディレクトリ内のファイルは、OBD2ツールで取得可能な自動車の内部データの定義(パラメータID、データサイズ、変換式等）の情報が、取得可能なデータごとに記載されています。例えば、パラメータID(以下、"PID"と表記) 05 の Engine coolant temperature(エンジン冷却水温) は`EngineCoolantTemperature.java`に入っています。

```Java:EngineCoolantTemperature.java
package com.android.car.obd2.commands;

import com.android.car.obd2.IntegerArrayStream;
import com.android.car.obd2.Obd2Command;
import java.util.Optional;

public class EngineCoolantTemperature implements Obd2Command.OutputSemanticHandler<Integer> {
    @Override
    public int getPid() {
        return 0x05;
    }

    @Override
    public Optional<Integer> consume(IntegerArrayStream data) {
        return data.hasAtLeast(
                1,
                theData -> Optional.of(theData.consume() - 40),
                theData -> Optional.<Integer>empty());
    }
}
```
[「OBD-II PIDs」](https://en.wikipedia.org/wiki/OBD-II_PIDs)によると、

|PIDs(hex)|Data bytes|Formula|
|---|---|---|
|05|1|$A-40$|

とあるので、`data.hasAtLeast(Data bytes, Formula)`の、`theData.consume() - 40`がFormula(変換式)に相当する部分ですね。この`IntegerArrayStream`は１～４文字の文字列をint型配列として受け取り、値があれば`consume()`するたびに一文字ずつ取り出し、となっているのでこのような形で実現しているようです。

他にも、取りたいデータがサンプルコードの中になければ[OBD-II PIDs](https://en.wikipedia.org/wiki/OBD-II_PIDs)の定義に従って用意すればいいようです。ただし、例えばAmbientAirTemperature(＝外気温)はパラメータIDが16進で 46、変換式は $A-40$ なのに、A*100/256になっていましたので、`EngineCoolantTemperature.java`と同じにした方がいいと思います。他のPIDも使用する前に合っているかどうか、確認してからにした方がよさそうです。


```Java:AmbientAirTemperature.java
package com.android.car.obd2.commands;

import com.android.car.obd2.IntegerArrayStream;
import com.android.car.obd2.Obd2Command;
import java.util.Optional;

public class AmbientAirTemperature implements Obd2Command.OutputSemanticHandler<Float> {
    @Override
    public int getPid() {
        return 0x46;
    }

    @Override
    public Optional<Float> consume(IntegerArrayStream data) {
        return data.hasAtLeast(
                1,
                theData -> Optional.of(theData.consume() - 40),
            //  theData -> Optional.of(theData.consume() * (100.0f / 255.0f)), <- 変更前
                theData -> Optional.<Float>empty());
    }
}
```

## IntegerArrayStream.java
`IntegerArrayStream.java`は`package`を今回のアプリに合わせて変更する以外は、そのまま使えます。

|型|メソッド|概要|
|---|---|---|
|int|consume()|int型配列からひとつづつ値を取り出す|
|int|residualLength()|配列(データ数)の残りの数を返す|
|boolean|isEmpty()|残りの配列(データ数)がゼロかどうか|
|boolean|hasAtLeast(int n)|残りの配列(データ数)がnより多いかどうか|
|\<T>|hasAtLeast(int n, Function\<IntegerArrayStream, T> ifTrue, Function\<IntegerArrayStream, T> ifFalse)|配列の残りがnの場合はifTrue 関数を、違う場合はifFalse関数を適用|
|boolean|expect(int... values)|いくつかの文字コードが決められた順番で格納されているかどうか|

`hasAtLeast(int n, Function<IntegerArrayStream, T> ifTrue, Function<IntegerArrayStream, T> ifFalse)`は
```Java
    public <T> T hasAtLeast(
            int n,
            Function<IntegerArrayStream, T> ifTrue,
            Function<IntegerArrayStream, T> ifFalse) {
        if (hasAtLeast(n)) {
            return ifTrue.apply(this);
        } else {
            if (ifFalse != null) {
                return ifFalse.apply(this);
            } else {
                return null;
            }
        }
    }
```
というロジックになっています。

## Obd2Command.java
`Obd2Command.java`もロジック部分はそのまま持ってきました。
ここには`commands`ディレクトリの情報を実際にどう処理するか記述されています。戻り値がint型(Integer型)の情報は、`SUPPORTED_INTEGER_COMMANDS`というマップに、float型(Float型)の情報は`SUPPORTED_FLOAT_COMMANDS`というマップに、PIDをキーに格納されます(`addSupportedIntegerCommands`、`addSupportedFloatCommands`)。取得した車両情報を追加したい場合は、該当するPID、変換式を記述したクラスを`commands`に追加し、ここに追記することで追加可能ですし、不要になればここで削除すればよさそうです。

さらに、`getSupportedIntegerCommands`、`getSupportedFloatCommands`メソッドでこれらのマップのキーセットが取得できます。また、`getIntegerCommand(int pid)`、`getFloatCommand(int pid)`でPIDを指定して各クラスを選択することも出来ます。

```Java:Obd2Command.java
...
    private static final HashMap<Integer, OutputSemanticHandler<Integer>>
            SUPPORTED_INTEGER_COMMANDS = new HashMap<>();
    private static final HashMap<Integer, OutputSemanticHandler<Float>> SUPPORTED_FLOAT_COMMANDS =
            new HashMap<>();

    private static void addSupportedIntegerCommands(
            OutputSemanticHandler<Integer>... integerOutputSemanticHandlers) {
        for (OutputSemanticHandler<Integer> integerOutputSemanticHandler :
                integerOutputSemanticHandlers) {
            SUPPORTED_INTEGER_COMMANDS.put(
                    integerOutputSemanticHandler.getPid(), integerOutputSemanticHandler);
        }
    }

    private static void addSupportedFloatCommands(
            OutputSemanticHandler<Float>... floatOutputSemanticHandlers) {
        for (OutputSemanticHandler<Float> floatOutputSemanticHandler :
                floatOutputSemanticHandlers) {
            SUPPORTED_FLOAT_COMMANDS.put(
                    floatOutputSemanticHandler.getPid(), floatOutputSemanticHandler);
        }
    }

    public static Set<Integer> getSupportedIntegerCommands() {
        return SUPPORTED_INTEGER_COMMANDS.keySet();
    }

    public static Set<Integer> getSupportedFloatCommands() {
        return SUPPORTED_FLOAT_COMMANDS.keySet();
    }

    public static OutputSemanticHandler<Integer> getIntegerCommand(int pid) {
        return SUPPORTED_INTEGER_COMMANDS.get(pid);
    }

    public static OutputSemanticHandler<Float> getFloatCommand(int pid) {
        return SUPPORTED_FLOAT_COMMANDS.get(pid);
    }

    static {
        addSupportedFloatCommands(
                new AmbientAirTemperature(),
                new CalculatedEngineLoad(),
                new FuelTankLevel(),
                new Bank2ShortTermFuelTrimCommand(),
                new Bank2LongTermFuelTrimCommand(),
                new Bank1LongTermFuelTrimCommand(),
                new Bank1ShortTermFuelTrimCommand(),
                new ThrottlePosition());
        addSupportedIntegerCommands(
                new EngineOilTemperature(),
                new EngineCoolantTemperature(),
                new FuelGaugePressure(),
                new FuelSystemStatus(),
                new RPM(),
                new EngineRuntime(),
                new Speed());
    }

    ...
```
次に、`Obd2Command`は`int型`の`mode`と`OutputSemanticHandler<ValueType>`が引数になっていて、`mode`は`LIVE_FRAME`か`FREEZE_FRAME`ですが、今回は`LIVE_FRAME`を使います。`OutputSemanticHandler`は`getPid()`と`consume`というインターフェースを持つクラスで、前述の`commands`ディレクトリーにある、それぞれのPIDごとのクラスがこのインターフェースを実装(implements)しているので、あとでPIDごとに引き当ててListにして使います。

```Java:Obd2Command.java

    ...

    protected final int mMode;
    protected final OutputSemanticHandler<ValueType> mSemanticHandler;

    Obd2Command(int mode, OutputSemanticHandler<ValueType> semanticHandler) {
        mMode = mode;
        mSemanticHandler = Objects.requireNonNull(semanticHandler);
    }

    public abstract Optional<ValueType> run(Obd2Connection connection) throws Exception;

    public int getPid() {
        return mSemanticHandler.getPid();
    }

    public static final <T> LiveFrameCommand<T> getLiveFrameCommand(OutputSemanticHandler handler) {
        return new LiveFrameCommand<>(handler);
    }

    ...

```
その後に、`Obd2Command`クラスを継承する`LiveFrameCommand`、`FreezeFrameCommand`が定義されています。今回は、走行中のデータを取りたいので、`LiveFrameCommand`を使います。FreezeFrameデータは、故障判定時に原因調査が出来ると思われるデータを保存する(Freeze)時に有効ですが、今回は使いません。

`run`メソッドはまず、`String`型の文字列`command`を作っていますが、これがELM327を通して車両のデータを要求するコマンドになっています。[「ELM327 datasheet」](https://www.elmelectronics.com/wp-content/uploads/2016/07/ELM327DSF.pdf)に詳細はありますが、例えばPID 05(Hex)の今の値を要求するコマンドは、`01 05`です。
```java
    String command = String.format("%02X%02X", mMode, mSemanticHandler.getPid() 
```
の部分で文字列として作成していて、コンストラクタで`super(LIVE_FRAME, semanticHandler)`が呼ばれて`LIVE_FRAME = 1`なので`mMode = 1`となり、該当する`getPid()`メソッドで今回の例だと`05`となって`01 05`が実現されています。`01`と`05`の間のスペース有無はどちらでもいいようです。ここで、`01`は"mode 01"現在の値を取得(show current data)という意味で、`01 05`はPID 05の現在の値を要求するという意味になり、`01`の要求の回答は先頭が`41`になって返送されます。

`41`は`LiveFrameCommand`の直後で
```java
    private static final int RESPONSE_MARKER = 0x41;
```
と定義されていて、mode 01 の応答(01 + 40 = 41)を意味するそうです。

PID 05の回答は`41 05 7B`になり、最後の`7B`が取得できたデータ(現在の値)になります。なので、`IntegerArrayStream stream`の`stream.expect`で RESPONSE_MARKER(= 41) と、該当するPID(今回は05)が続けて返って来たと判定できれば、`mSemanticHandler.consume(stream)`でその次に送られてきた値を取り出し、それぞれのPIDの変換式にしたがって変換した値が取得できる、という仕組みになっています。

```Java:Obd2Command.java
    ...

    /**
     * An OBD2 command that returns live frame data.
     *
     * @param <ValueType> The Java type that represents the command's result type.
     */
    public static class LiveFrameCommand<ValueType> extends Obd2Command<ValueType> {
        private static final int RESPONSE_MARKER = 0x41;

        LiveFrameCommand(OutputSemanticHandler<ValueType> semanticHandler) {
            super(LIVE_FRAME, semanticHandler);
        }

        public Optional<ValueType> run(Obd2Connection connection)
                throws IOException, InterruptedException {
            String command = String.format("%02X%02X", mMode, mSemanticHandler.getPid());
            int[] data = connection.run(command);
            IntegerArrayStream stream = new IntegerArrayStream(data);
            if (stream.expect(RESPONSE_MARKER, mSemanticHandler.getPid())) {
                return mSemanticHandler.consume(stream);
            }
            return Optional.empty();
        }
        ...
```

## Obd2Connection.java
### UnderlyingTransport インターフェース
ELM327の設定や、コマンドの送受信、車両からの応答の解析を実際に処理する部分は、`Obd2Connection.java`にまとめられています。最初に、`InputStream`や`OutputStream`のインターフェースの定義があります。これを実際にBluetooth接続するクラス(後述の`BluetoothConnection`クラス)と関連付けて使います。また、オリジナルには再接続の設定もありますが、接続が切れた時の再接続はどう処理するのが妥当なのか、今は考えが及んでいませんので、実体の部分も合わせてコメントアウトしておきます。

### ELM327設定用コマンド
UnderlyingTransport インターフェースのあとに
```java:Obd2Connection.java
    private static final String[] initCommands =
            new String[] {"ATD", "ATZ", "AT E0", "AT L0", "AT S0", "AT H0", "AT SP 0"};

```
があります。この、ATDから始まる文字は、ELM327の設定をするためのコマンド(ATコマンド)で、[「ELM327 datasheet」](https://www.elmelectronics.com/wp-content/uploads/2016/07/ELM327DSF.pdf)から抜粋すると以下のようになります。

|コマンド|機能|
|---|---|
|ATD|初期化(set all to Defaults)|
|ATZ|全リセット(reset all)|
|AT E0|エコーオフ(Echo off)。 E1ならエコーオン、要求コマンドが応答に先立ちエコーとして送信される|
|AT L0|改行コードオフ(Linefeeds off)。 L1なら改行コードオン、送信文字列に改行コードが追加される|
|AT S0|スペース挿入オフ(printing of Spaces off)。 S1ならECUの応答にスペースが挿入される|
|AT H0|ヘッダーオフ(Headers off)。 H1なら車両からのヘッダー情報が追加されるようですが、通常はオフでいいようです|
|AT SP 0|プロトコル0(Automatic)を選択(set protocol to 0)。 プロトコルは0～9,A,B,Cから選択|

最後のプロトコルは、車によっては自動(Automatic)では応答が来ない場合もあるので、上手く情報が出てこない場合はこれを変更して試すことになると思います。そして、コンストラクタが呼び出されたときに、`runInitCommands`メソッドが呼ばれて、このコマンドが順番に送信されます。

### コマンドの送信と応答の受信処理

`run`メソッドと`runImpl`で、`Obd2Command.java`の`LiveFrameCommand`でPIDごとに作ったコマンドを送信し、応答を処理する機能が実装されています。`LiveFrameCommand.run(Obd2Connection connection)`で、`connection.run(command)`とすると、この部分が実行される構成になっています。なかなか複雑ですね．．．

`run`メソッドから`runImpl`にコマンドを渡し、さらに`OutputStream`に書き込んだところで`InputStream`の`read`メソッドで応答を待ちます。この`OutputStream`は`mConnection.getOutputStream()`なので、その先は`BluetoothConnection`クラスの`getOutputStream()`で、さらには`BluetoothSocket`の`mSocket.getOutputStream()`ですね。これも先が長い．．．

読み出した文字がプロンプト(`>`)であれば読み込みを終了、`\r`、`\n`、`' '`、`'\t'`、`.`は無視してStringBuilderに詰め込んで応答を返します。出来た文字列をさらに加工し、スペースを除いたり、今回は該当ないですが、OBD2のプロトコル的には複数のPIDをまとめて要求でき、その際は応答もまとまって返ってくるので、それを分解(`unpackLongFrame`)したりし、残った文字をさらに調べます。

ELM327を通じて返ってくる車の応答には、「SEARCHING」、「ERROR」、「BUS INIT」、「BUSINIT」、「BUS ERROR」、「BUSERROR」、「STOPPED」、「?」、「NODATA」、「UNABLETOCONNECT」、「CANERROR」が返ってくることがあります。「UNABLETOCONNECT」、「CANERROR」の場合は、IOExceptionを出すようになっていますが、それ以外は基本は無視ですね。上記のATコマンドがうまく成功した場合は「OK」が返ってきますが、処理としては特に何もしません。残った文字列を、さらに2桁ごとの16進文字として10進にし、int型配列に`toHexValues`メソッドで入れて呼び出し元に返します。

### 各PIDの対応(サポート)の有無調査

ATコマンドでELM327の設定が出来、クルマからの応答があることが確認できると、次は、欲しいPIDコードの情報がクルマ側で対応(サポート)されているかを調べます。車によって対応しているPIDが結構違うので、実際のデータを要求する前に、そのPIDの情報が返ってくる(サポートされている)のか、調べる必要があります。

コマンド`01 00`を送ると、PID 01から20までのコードが対応されているか、ビットごとに対応されてる場合は1が、されていない場合は0でセットされます。

[「OBD-II PIDs」](https://en.wikipedia.org/wiki/OBD-II_PIDs)の例で説明すると、`01 00`の応答が`BE1FA813`の場合は、2進にすると
	1011 1110 0001 1111 1010 1000 0001 0011
となるので、PID 01, 03, 04, 05, 06, 07, 0C, 0D, 0E, 0F, 10, 11, 13, 15, 1C, 1F と 20 がサポートされています、となります(最上位ビットからPID 01、02、... 最後が20という割付になっています)。

PID 20 がサポートされている、ということは、PIDs supported [21 - 40]ということで、PID 21から40も存在する、という意味になります。これは次に`01 20`を送ると応答が返ってくることを意味していて、この場合の最下位ビットはPID 40で、これが1なら、さらにPIDs supported [41 - 60]ということで、[「OBD-II PIDs」](https://en.wikipedia.org/wiki/OBD-II_PIDs)では PID C0まで、192個のPIDが定義されています。

オリジナルの[「obd2-lib」](https://android.googlesource.com/platform/packages/services/Car/+/refs/heads/master/obd2-lib/)の`getSupportedPIDs`メソッドでは、サポート有無を調べるコマンドを、`"0100", "0120", "0140", "0160"`と用意し、順番にforループで送信、それぞれのビットを調べ、サポートあるPID コードをマップ(リスト化)して返しています。が、試したところ例えばPID 20が対応していない車両に`01 20`を送信しても応答が返ってこず、処理が止まってしまいました。そこで、今回のコマンドに対する応答の最下位ビットが1なら次のコマンドも送信、0ならbreakという処理を追加しています。

```java:Obd2Connection.java
    ...

    public Set<Integer> getSupportedPIDs() throws IOException, InterruptedException {
        Set<Integer> result = new HashSet<>();
        String[] pids = new String[] {"0100", "0120", "0140", "0160"};
        int basePid = 1;
        for (String pid : pids) {
            int[] responseData = run(pid);
            if (responseData.length >= 6) {
                byte byte0 = (byte) (responseData[2] & 0xFF);
                byte byte1 = (byte) (responseData[3] & 0xFF);
                byte byte2 = (byte) (responseData[4] & 0xFF);
                byte byte3 = (byte) (responseData[5] & 0xFF);
                if (DBG) {
                    Log.i(TAG, String.format("supported PID at base %d payload %02X%02X%02X%02X",
                        basePid, byte0, byte1, byte2, byte3));
                }
                FourByteBitSet fourByteBitSet = new FourByteBitSet(byte0, byte1, byte2, byte3);
                for (int byteIndex = 0; byteIndex < 4; ++byteIndex) {
                    for (int bitIndex = 7; bitIndex >= 0; --bitIndex) {
                        if (fourByteBitSet.getBit(byteIndex, bitIndex)) {
                            int command = basePid + 8 * byteIndex + 7 - bitIndex;
                            if (DBG) {
                                Log.i(TAG, "command " + command + " found supported");
                            }
                            result.add(command);
                        }
                    }
                }
            }
            basePid += 0x20;
        //  最下位ビットが1なら継続(次のコマンドも送信)、0ならbreak
            if(result.contains(basePid - 1))
                Log.d(TAG,String.format("next Support id %02X supported",basePid - 1));
            else
                break;
        }
        // 追加部分終わり 
        return result;
    }

    ...
```

## Obd2LiveFrameGenerator.java

`Obd2LiveFrameGenerator`は、`Obd2Connection`を引数として呼び出され、コンストラクタでは`Obd2Connection`の`getSupportedPIDs`でサポートしているPIDをList化し、`Obd2Command`で設定されているPIDを突き合わせ、Java Stream API を使ってサポートのあるPIDを`mIntegerCommands`、`mFloatCommands`というListにまとめています。

その後に、`generate`メソッドで`mIntegerCommands`、`mFloatCommands`をそれぞれPIDの数だけ呼び出してコマンドを送信、受信した結果を返します。このメソッドはJsonWriterで返すことになっているので、PIDと値を対にしてJSON形式で戻すようになっています。オリジナルでは`JsonWriter`を引数にしていますが、`JsonWriter`の使い方がいまいち分からなかったので引数なしで、`JsonObject`にPIDと値を対にして入れて返すことにしました。

```Java:Obd2LiveFrameGenerator.java
/*
 * Copyright (C) 2017 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.example.myapplication;

import android.os.SystemClock;
import android.util.Log;

import com.example.myapplication.Obd2Command.LiveFrameCommand;

import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.Set;

public class Obd2LiveFrameGenerator {
    public static final String FRAME_TYPE_LIVE = "live";
    public static final String TAG = Obd2LiveFrameGenerator.class.getSimpleName();

    private final Obd2Connection mConnection;
    private final List<LiveFrameCommand<Integer>> mIntegerCommands = new ArrayList<>();
    private final List<LiveFrameCommand<Float>> mFloatCommands = new ArrayList<>();

    public Obd2LiveFrameGenerator(Obd2Connection connection)
            throws IOException, InterruptedException {
        mConnection = connection;
        Set<Integer> connectionPids = connection.getSupportedPIDs();
        Set<Integer> apiIntegerPids = Obd2Command.getSupportedIntegerCommands();
        Set<Integer> apiFloatPids = Obd2Command.getSupportedFloatCommands();
        apiIntegerPids
                .stream()
                .filter(connectionPids::contains)
                .forEach(
                        (Integer pid) ->
                                mIntegerCommands.add(
                                        Obd2Command.getLiveFrameCommand(
                                                Obd2Command.getIntegerCommand(pid))));
        apiFloatPids
                .stream()
                .filter(connectionPids::contains)
                .forEach(
                        (Integer pid) ->
                                mFloatCommands.add(
                                        Obd2Command.getLiveFrameCommand(
                                                Obd2Command.getFloatCommand(pid))));
        Log.i(
                TAG,
                String.format(
                        "connectionPids = %s\napiIntegerPids=%s\napiFloatPids = %s\n"
                                + "mIntegerCommands = %s\nmFloatCommands = %s\n",
                        connectionPids,
                        apiIntegerPids,
                        apiFloatPids,
                        mIntegerCommands,
                        mFloatCommands));
    }

//    public JsonWriter generate(JsonWriter jsonWriter) throws IOException {    オリジナルの戻り値の型はJsonWriter
    public JSONObject generate() throws IOException {
        return generate(SystemClock.elapsedRealtimeNanos());
    }

//    以下取得したデータは JsonWriter ではなく、JSONObject を使って処理、に変更
    public JSONObject generate(long timestamp) throws IOException {
        JSONObject jsonObject = new JSONObject();
        JSONArray datasArray = new JSONArray();
        try {
            jsonObject.put("type", FRAME_TYPE_LIVE);
            jsonObject.put("timestamp",timestamp);
        } catch (JSONException e) {
            e.printStackTrace();
        }
        for (LiveFrameCommand<Integer> command : mIntegerCommands) {
            try {
                Optional<Integer> result = command.run(mConnection);
                JSONObject intValues = new JSONObject();
                if (result.isPresent()) {
                    intValues.put("id",command.getPid());
                    intValues.put("value",result.get());
                    datasArray.put(intValues);
                }
            } catch (IOException | InterruptedException | JSONException e) {
                Log.w(
                        TAG,
                        String.format(
                                "unable to retrieve OBD2 pid %d due to exception: %s",
                                command.getPid(), e));
                // skip this entry
            }
        }

        for (LiveFrameCommand<Float> command : mFloatCommands) {
            try {
                Optional<Float> result = command.run(mConnection);
                JSONObject floatValues = new JSONObject();
                if (result.isPresent()) {
                    floatValues.put("id",command.getPid());
                    floatValues.put("value",result.get());
                    datasArray.put(floatValues);
                }
            } catch (IOException | InterruptedException | JSONException e) {
                Log.w(
                        TAG,
                        String.format(
                                "unable to retrieve OBD2 pid %d due to exception: %s",
                                command.getPid(), e));
                // skip this entry
            }
        }
        try {
            jsonObject.put("data",datasArray);
        } catch (JSONException e) {
            e.printStackTrace();
        }

        return jsonObject;
    }
}
```
# bluetooth接続機能との統合
## BluetoothConnection.java

OBD2アダプターをbluetoothデバイスとしてシリアルポートプロファイルで接続する部分です。`BluetoothDevice`の`mDevice`を接続したいOBD2アダプターのMAC アドレスで作成し(`BluetoothAdapter.getDefaultAdapter().getRemoteDevice(String address)`)、`BluetoothSocket`の`mSocket`を、`mDevice.createRfcommSocketToServiceRecord(SERIAL_PORT_PROFILE)`メソッドで関連付け、`mSocket.connect()`で接続する、という流れですが、一般的にはこの`connect()`が時間がかかり、その間、反応しないアプリになると良くないとされているので、接続する部分は別のスレッドになるようにしています。と言っても欲しい機能は、前回作ったアプリ([【Android】 PCとBluetooth接続し、PCに文字列を送信する)](https://qiita.com/naoto_deuxchats/items/e6a9af6c7a9ee40f0fb9)と同じなので、前述の`Obd2Connection`の`UnderlyingTransport` インターフェースをimplementsして、`Obd2Connection`から送受信等出来るようにしている所だけが変更点です。

https://qiita.com/naoto_deuxchats/items/e6a9af6c7a9ee40f0fb9


```java:BluetoothConnection.java


public class BluetoothConnection implements Obd2Connection.UnderlyingTransport {
//public class BluetoothConnection {

    //    以下、前回のBluetoothConnection.javaと同じ
    ...

}
```

## Bluetooth接続処理
Bluetooth接続はUIスレッドのSTART ボタンが押されたときに、`Service`を`ForegroundService`でスタートさせ、`BluetoothConnection`の`connect()`を呼ぶことで接続し、接続に成功すると`onBluetoothConnected()`メソッドが呼ばれます。Bluetooth接続後の処理もUIスレッドとは別のスレッドで実行します。`onBluetoothConnected()`が呼ばれたら`ConnectedThread`を開始する(`start()`を呼ぶ)ようにしておきます。

前回は`OutputStream`を`BluetoothConnection`の`getOutputStream`と関連付けて文字列を`write`することで送信しましたが、今回は、`Obd2Connection`のインスタンスを生成するときに、`BluetoothConnection`を引数にし、`Obd2Connection`経由で送信します。`Obd2Connection`のコンストラクタが呼び出されると、ELM327を設定(`runInitCommands()`)する処理が実行されます。

さらに、`Obd2LiveFrameGenerator`を`Obd2Connection`を引数に生成し、`Obd2LiveFrameGenerator`の`generate()`を呼び出すことで、`JSONObject`として応答を取得します。取得したデータはファイル名を`Test.csv`として保存しておきます。Context#getFilesDir()を使いましたので、`data/data/[package_name]/files/Test.csv`として保存されます。一行目はデータのラベル名として、timestamp(時刻)、取得できたPID、2行目以降は実際のデータで時系列で保存することにしました。

一通りのデータが取得出来たら(`generate()`が終了したら)、繰り返す、としていますので、データ取得周期は車からの応答速度に依存します。まずはこの仕様で試してみます。

また、UIスレッドのSTOPボタンを押すと、`onActive`が`false`になり、`while`文を抜けるようにしています。

```java:MyService.java
package com.example.myapplication;

import android.app.Notification;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.os.IBinder;
import android.util.Log;

import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;

import java.io.File;
import java.io.FileWriter;
import java.io.IOException;

public class MyService extends Service implements BluetoothConnection.connectionInterface {

    private BluetoothConnection mBluetoothConnection;

    ConnectedThread thread;

    public IBinder onBind(Intent intent) {
        return null;
    }

    public int onStartCommand(Intent intent, int flags, int startId) {
        Log.d("MyService", "onStartCommand()");
        Log.d("MyService", "Thread name = " + Thread.currentThread().getName());

        int requestCode = intent.getIntExtra("REQUEST_CODE",0);
        Context context = getApplicationContext();
        String channelId = "default";
        String title = context.getString(R.string.app_name);

        if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {

            PendingIntent pendingIntent =
                    PendingIntent.getActivity(context, requestCode, intent, PendingIntent.FLAG_IMMUTABLE);

            NotificationManager notificationManager =
                    (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);

            // Notification Channel 設定
            NotificationChannel channel = new NotificationChannel(
                    channelId, title, NotificationManager.IMPORTANCE_DEFAULT);

            if (notificationManager != null) {
                notificationManager.createNotificationChannel(channel);

                Notification notification = new Notification.Builder(context, channelId)
                        .setContentTitle(title)
                        // android標準アイコンから
                        .setSmallIcon(android.R.drawable.ic_media_play)
                        .setContentText("MediaPlay")
                        .setAutoCancel(true)
                        .setContentIntent(pendingIntent)
                        .setWhen(System.currentTimeMillis())
                        .build();

                // startForeground
                startForeground(1, notification);

            }
        }

        String address = intent.getStringExtra("BT_address");

        if(mBluetoothConnection == null){
                connectDevice(address);
        } else {
            mBluetoothConnection.close();
            mBluetoothConnection = null;
                connectDevice(address);
        }

        return START_NOT_STICKY;
    }
    @Override
    public void onDestroy(){
        thread.cancel();
    }

    private void connectDevice(String address){
        // Get the BluetoothDevice object
        Log.d("connectDevice ", "address = " + address);
        mBluetoothConnection = new BluetoothConnection(this, address);
        mBluetoothConnection.setConnectionListener(this);
        mBluetoothConnection.connect();
    }

    @Override
    public void onBluetoothConnected() {
        thread = new ConnectedThread();
        thread.start();
    }

    @Override
    public void onBluetoothConnectFailed() {
        Log.d(getClass().getSimpleName(), "onBluetoothConnectFailed");
    }

    private class ConnectedThread extends Thread {
        boolean onActive = true;
        Obd2Connection mObd2Connection;
        Obd2LiveFrameGenerator mObd2LiveFrameGenerator;

        public void run() {
            Log.d("ConnectedThread run", "Thread name = " + Thread.currentThread().getName());
            mObd2Connection = new Obd2Connection(mBluetoothConnection);
            try {
                mObd2LiveFrameGenerator = new Obd2LiveFrameGenerator(mObd2Connection);
                File myDataFile = new File(getApplicationContext().getFilesDir(), "test.csv");
                FileWriter myDataFileWriter = new FileWriter(myDataFile);
                boolean needTitle = true;
                while (onActive) {
                    JSONObject jsonObject = mObd2LiveFrameGenerator.generate();
                    Log.d("ConnectedThread run", "jsonObject = " + jsonObject.toString());

                    long timestamp = jsonObject.getLong("timestamp");
                    JSONArray datas = jsonObject.getJSONArray("data");

                    StringBuilder titles = new StringBuilder();
                    StringBuilder values = new StringBuilder();
                    titles.append("timestamp");
                    Date datetime = new Date(timestamp);
                    SimpleDateFormat sdf = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss.SSS", Locale.JAPAN);
                    values.append(sdf.format(datetime));

                    if(datas.length() > 0){
                        for(int j = 0; j < datas.length(); j++){
                            JSONObject data = datas.getJSONObject(j);
                            titles.append(",").append(data.get("id"));
                            values.append(",").append(data.get("value"));
                        }

                        if(needTitle){
                            myDataFileWriter.write(titles + "\r\n");
                            needTitle = false;
                        }
                        myDataFileWriter.write(values + "\r\n");
                    }
                }
                myDataFileWriter.close();

            } catch (IOException | InterruptedException | JSONException e) {
                e.printStackTrace();
                onActive = false;
                Log.d(getClass().getSimpleName(), "onBluetoothDisconnected");
                e.printStackTrace();
            }
            //  正常終了時   異常時はonBluetoothDisconnected()の方が早い→mBluetoothConnection = nullの可能性もあり
            if (mBluetoothConnection != null) {
                mBluetoothConnection.close();
                Log.d("ConnectedThread", "mBluetoothConnection.close()");
                mBluetoothConnection = null;
            }

            Log.d("ConnectedThread", "end");
        }
        public void cancel(){
            onActive = false;
            Log.d("ConnectedThread", "cancel");
        }
    }
```
## 統合後の構成
統合後のファイル構成は以下のようになりました。
![obd2-lib統合後.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2335076/0913bb0f-0d4d-0797-0f47-c6123a650c2c.png)

# 動作確認

## PCでの確認

車での動作確認をする前に、PCを相手に通信接続、コマンド送信が出来ているか、確認します(※Teratermの設定は[前回の記事](https://qiita.com/naoto_deuxchats/items/e6a9af6c7a9ee40f0fb9#bluetooth%E6%8E%A5%E7%B6%9A%E3%81%AE%E5%AE%9F%E6%A9%9F%E3%83%86%E3%82%B9%E3%83%88)と同じです)。

アプリのオプションメニュー →「Connect a device」→ 受信側のPCを選択

をすると、一度最初の画面に戻るので、STARTボタン押します。

Teratermの画面に「ATD」と表示されていれば、接続と、`Obd2Connection`のインスタンスの生成、コンストラクタにあるELM327の設定まではたどり着いています。
![ATD.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2335076/447865a6-46e8-5665-7783-bb545dc013f4.png)

Teratermのコンソールから`>`を入力すると`Obd2Connection`の`String[] initCommands`の内容が次々に送信されてきます。 "ATZ", "AT E0", "AT L0", "AT S0", "AT H0", "AT SP 0"まで順番に送信されてくるので、同じく`>`を入力すると、次は`Obd2LiveFrameGenerator`のインスタンスが生成され、Support PIDの確認に進みます。
![0100まで.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2335076/1c6ea69b-7720-2b96-704f-f3de944b5a3f.png)

"0100"が送信されてくるので、[「OBD-II PIDs」](https://en.wikipedia.org/wiki/OBD-II_PIDs)にならって、`4100BE1FA813` と打ち込みます。
![ATCommands_supportPIDs.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2335076/62f8f378-1801-24fa-f88b-328670d0a30a.png)

Android Studio の Logcatもしくは、debugのコンソール画面に

	I/Obd2LiveFrameGenerator: connectionPids = [32, 1, 3, 4, 5, 6, 7, 12, 13, 14, 15, 16, 17, 19, 21, 28, 31]
    apiIntegerPids=[3, 5, 10, 92, 12, 13, 31]
    apiFloatPids = [17, 4, 70, 6, 7, 8, 9, 47]
    mIntegerCommands = [com.example.myapplication.Obd2Command$LiveFrameCommand@f687555, ...(以下、省略)
    mFloatCommands = [com.example.myapplication.Obd2Command$LiveFrameCommand@48811d1, ...(以下、省略)

と表示されました。`connectionPids`に出てきている数字は、[「OBD-II PIDs」](https://en.wikipedia.org/wiki/OBD-II_PIDs)の

> So, supported PIDs are: 01, 03, 04, 05, 06, 07, 0C, 0D, 0E, 0F, 10, 11, 13, 15, 1C, 1F and 20

と一部順番が違いますが一致しています。`connectionPids`は10進、こっちは16進ですね。

次の`apiIntegerPids`と`apiFloatPids`は`commands`に用意されているPIDのうち、戻り値がInteger扱いのPID、Float扱いのPIDがそれぞれ表示されています。

	apiIntegerPids=[3, 5, 10, 92, 12, 13, 31]	→ 16進で 03, 05, 0A, 5C, 0C, 0D, 1F
    apiFloatPids = [17, 4, 70, 6, 7, 8, 9, 47]	→ 同じく 11, 04, 46, 06, 07, 08, 09, 2F


mIntegerCommands、mFloatCommands は`apiIntegerPids`と`apiFloatPids`に含まれるPIDのうち、サポートされているPIDのListになっているはずです。ところが、オリジナルのコードではLogの出力にハッシュ値が出力されていて、ちゃんと動いているかわからないで`Obd2Command`クラスにtoStringメソッドをオーバーライドして
```java:Obd2Command.java
    @Override
    public String toString(){
        return Integer.toString(mSemanticHandler.getPid());
    }
```
を追加すると、

    mIntegerCommands = [3, 5, 12, 13, 31]
    mFloatCommands = [17, 4, 6, 7]

となって、supported PIDs に含まれるPIDだけがちゃんと抽出されているのが確認できました。

TeraTermのコンソール上に`0103`が最後に表示されていますが、`mIntegerCommands`、`mFloatCommands`の順でそれぞれの配列に含まれるPIDの取得要求を出していくことになっているので、その最初のPIDが`03`、このPIDの現在の値を要求するコマンドは`01 03`なのでそれが出力されているということで、そこまでは予定通りに動いていることが確認できました。

## 車での確認
PCを使っての動作確認が出来ましたので、次は実際に車で確認します。

### 機材等
* 技適マーク付与されているOBD2アダプターとして「CARISTA OBD2 アダプタ」を使用
* トヨタ ヴォクシー(ZRR70)

車の故障診断コネクターにOBD2アダプターを取り付け、エンジンを始動します。次に、スマホのBleutooth設定画面から、OBD2アダプターをペア設定し、接続します。スマホアプリを起動し、オプションメニュー →「Connect a device」→ ペア設定したOBD2アダプターを選択、STARTボタンを押します。正常に動作していると、画面の左上に`ForegroundService`で使用される`Notification`で設定したアイコンが表示されます。

しばらく待って、STOPボタンを押し、Android Studioが作動しているPCに接続していない場合は接続し、Android Studioの右下にある「[Device File Explorer](https://developer.android.com/studio/debug/device-file-explorer.html)」から
 `data/data/[package_name]/files`を開くと、`Test.csv`が出来ていました。
![Test_csv.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2335076/66673dbb-12a3-d039-e38c-a688fedebe66.png)



右クリックでメニューが出て、「Save As...」でデスクトップ等に保存して内容を確認すると、先程のサポートされているPIDの値が計測開始からの時間と一緒に保存されていました。一行目は、項目名です。PIDの数字だけではわかりにくいですが、そこは別途考えましょう。

PID 12(10進)はEngine speed(エンジン回転数、RPM)、5はEngine coolant temperature(エンジン冷却水温度)ですので、グラフにしてみるとエンジン始動直後のエンジン回転数の動き、エンジン冷却水温度が少しずつ上昇していく様子が分かりました。
![graph.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2335076/3b2ec763-9a7c-505b-5675-198c6b3f26c8.png)

※本記事は車のOBD(故障診断)コネクターへのOBD2アダプター装着を推奨したり、安全を保証するものではありません。取扱説明書等で装着を認めていない車もあります。

# 使い方

今回のような車のデータを取るアプリで何が分かるか調べた結果を別途まとめました。

https://naotoduexchats.blogspot.com/

# 参考にした記事等

https://www.elmelectronics.com/wp-content/uploads/2016/07/ELM327DSF.pdf

https://android.googlesource.com/platform/packages/services/Car/+/refs/heads/master/obd2-lib/

https://en.wikipedia.org/wiki/OBD-II_PIDs