読者です 読者をやめる 読者になる 読者になる

古事連記帖

趣味のこと、技術的なこと、適当につらつら書きます。

Bluetooth で OBD2 と接続して Mikaboshi で表示したい

UWP Windows 10 Windows Phone プログラミング Advent Calendar

この記事は、Windows 10 Mobile / Windows Phone Advent Calendar 2016 の 3 日目の記事です
www.adventar.org

先日の記事で、開発・公開している Windows 10 アプリ「Mikaboshi (みかぼし)」について書きましたが、きょうは今後予定している機能のうちのひとつを、開発手法とあわせて紹介します。

Mikaboshi のダウンロードはこちらからどうぞ
www.microsoft.com


Mikaboshi は、主に車載向け (自動車・バイク・自転車など) を想定して作っていますが、GPS の情報に頼る性質上、例えば高い建物の間を移動していたり、トンネル内を移動していたりしたときは地図の更新も、推定移動速度も更新されなくなります。そんな中でも、移動速度だけは更新させたいという気持ちから、OBD2 を使った移動速度の検知を試してみました。

OBD2 とは、いわゆる「自動車の自己診断機能」のことで、本来は自動車の故障個所などを検知する目的で使われるもののようですが、その情報の中には移動速度なども含まれており、また車からの情報なので即時性・正確性が高く、これを利用したアプリや車載機器*1が多くあります。


実装するにあたり、電話で使うので Bluetooth 接続できた方がありがたいですよね。Bluetooth 接続できる OBD2 アダプターデバイスは結構多く存在します。適当に Amazon でポチポチしてもいいと思いますが、なぜか当たりはずれの大きいものばかりなので、既に Windows Phone 向けにも OBD2 を使った情報収集できるアプリ「OBD Info-san!」を開発した がんち さんが紹介している OBD II マルチメーター M-OBD-V01 が割と安定して使えるかなと思います。ほかのやつと比べるとちょっと高いですけどね。

Mikaboshi で OBD2 検知機能を実装するにあたり、がんちさんのアプリには大変お世話になりました。

Bluetoothバイスを見つけて接続する

まず Bluetooth 接続を実装する前に、Package.appxmanifest で Bluetooth を使うように宣言します。
開いて、「機能」タブから「Bluetooth」にチェックを入れてください。これでアプリで Bluetooth が使えるようになります。

次に、使える Bluetoothバイスを列挙します。何が OBD2 アダプターなのかわかりませんからね。

使えるデバイスの一覧を拾うには、 Windows.Devices.Enumeration.DeviceInformation が使えます。
こんな感じ。

using Windows.Devices.Bluetooth.Rfcomm;
using Windows.Devices.Enumeration;

public async Task<DeviceInformationCollection> GetAvailableDevicesAsync()
{
    var services = await DeviceInformation.FindAllAsync(RfcommDeviceService.GetDeviceSelector(RfcommServiceId.SerialPort));
    return services;
}

OBD2 アダプターがシリアルポートとして動くので、シリアルポートとして動くデバイスを探して列挙しておきます。

次に接続します。さきほど見つかったデバイスのうち、OBD2 アダプターデバイスを見つけておいてください*2。見つけたデバイスの DeviceInformation のインスタンスを保持しておきます。
そして接続します。こんな感じ。

using Windows.Devices.Bluetooth.Rfcomm;
using Windows.Devices.Enumeration;
using Windows.Networking.Sockets;
using Windows.Storage.Streams;

private StreamSocket socket;
private DataWriter writer;

public async Task ConnectAsync(DeviceInformation device)
{
    var service = await RfcommDeviceService.FromIdAsync(device.Id);

    this.socket = new StreamSocket();
    await this.socket.ConnectAsync(
        service.ConnectionHostName,
        service.ConnectionServiceName,
        SocketProtectionLevel.BluetoothEncryptionAllowNullAuthentication);
    this.writer = new DataWriter(this.socket.OutputStream);
}

StreamSocket.ConnectAsync() したとき、はじめての場合システムから許可を求められます。許可することで接続が確立します。2 回目以降は特にいわれませんので、ConnectAsync() してすぐに接続が確立します。

Bluetoothバイスとデータを送受信する (準備)

OBD2 アダプターデバイスは、データの送信と受信を一対一でおこないます。クライアントはデータを送信したら、OBD2 から受信を待機して、受け取った情報を解析するような流れをとります。
ですので、送信した後すぐ待機するようなコードを組むようにします。

using System.IO;
using Windows.Devices.Bluetooth.Rfcomm;
using Windows.Devices.Enumeration;
using Windows.Networking.Sockets;
using Windows.Storage.Streams;

// これらに、すでにインスタンスが上記コードを使って入ってるものとします
private StreamSocket socket;
private DataWriter writer;

public async Task SendCommandAsync(string message)
{
    // OBD2 のコマンド終端は \r
    var bytes = Encoding.ASCII.GetBytes(message + '\r');
    this.writer.WriteBytes(bytes);

    // OBD2 へここでデータを投げる
    var result = await this.writer.StoreAsync();

    if (result == bytes.Length)
    {
        // たぶん成功してる
        await AnalyzeAsync();
    }
}

private async Task AnalyzeAsync()
{
    var stream = this.socket.InputStream.AsStreamForRead();
    var result = new List<byte>();

    while (true)
    {
        var b = stream.ReadByte();

        if (b <= 0) continue;

        result.Add((byte)b);

        // > がきたら応答が終わったとみなす
        if ((char)b == '>') break;
    }
    // 以下省略、続きは次で
}

こんな感じ。これで形はできました。
OBD2 のインターフェースである ELM327 がプロンプト形式でやりとりするので、「>」はユーザーからの入力待ちモードになってるって感じです。ユーザーからコマンドと「\r」をもらったら、それに応じて情報を返して、「>」で終わるという具合。
なので、「>」がきたらデバイスからの応答は終わったとみなしてOKでしょう。

Bluetoothバイスとデータを送受信する (送信と解析)

実際に送信してみましょう。
コマンドについては ELM327 のデータシートも参考にしながら。
https://www.elmelectronics.com/wp-content/uploads/2016/07/ELM327DS.pdf

まずリセットコマンドを送信しておきます。
リセットコマンドは次の 3 つです。

at z
at l0
at e0

z は初期化コマンド、e0 はコマンドのエコーをするかどうか (これでエコーしないようになります)、l0 はラインフィードを追加するか (これで追加しないようになります) を設定します。

OBD2 へ送信するコマンドはいろいろあります。先ほどのデータシートにもありますが、英語版 Wikipedia にも一覧が載っています。

OBD-II PIDs - Wikipedia

速度情報は Mode 01 に書かれているので、コマンドはこんな感じ。

01 0D

これを送信することで、応答に速度情報が返ってきます。
これらを先ほどのコードで実装した SendCommandAsync() に投げます。

そしてこれらの応答を解析するコードを実装します。

// 上記コードからの続き

public int Speed { get; private set; }

private async Task AnalyzeAsync()
{
    var result = new List<byte>();
    // 省略
    result = /* OBD2 応答 */;

    // 応答をバイト配列から文字列にしておく
    var buffer = result.Where(w => w > 0).Select(s => (char)s).ToArray();
    var resultStr = new string(buffer);

    // 文字列にした応答を、半角スペースと改行で配列に分ける
    var data = resultStr.Split(new[] { ' ', '\r' }, StringSplitOptions.RemoveEmptyEntries).ToArray();

    // 初期化処理以外で 41 で始まるデータが返ってこなければ、それは失敗してる
    if (data[0] != "41")
        return;

    switch (data[1])
    {
        case "0D":
            // 速度情報
            break;
        default:
            return;
    }
}

private AnalyzeVehicleSpeed(string[] data)
{
    // 使うデータは 3 バイト目
    var speedData = data[2];
    var speedValue = 0;
    int.TryParse(speedData, NumberStyles.AllowHexSpecifier, CultureInfo.CurrentUICulture, out speedValue);

    this.Speed = speedValue;
}

こんな感じ。
初期化コマンドの応答については処理について特に書いてませんが、ちゃんと返ってきます。

> at z
ELM327 v1.5
> at l0
OK
> at e0
>

特に処理する必要もないですが、初期化してから各種コマンドを投げた方がいいです。初期化については「41」で始まる応答を返しませんが、エラー扱いにすると危ないので気を付けた方がいいです。

そして、送信した速度情報のコマンドの応答は

41 0D 28

となっています。成功を示す「41」と、速度情報に応答したと示す「0D」のあとに続く「28」を解析してやればOK。単に 16 進数ですので、数字に変換してやれば速度情報になります。
つまり、OBD2 で得られる速度情報は、0 ~ 255 km/h までということですね。


速度情報はたいていの車で返してもらえると思いますが、車によっては返してもらえなかったり、返す値が微妙に違う (同じ応答を 2 つ返すなど) するらしいので、その辺の情報がちょっとほしいなあと思ったりはしています。
また、ほかのコマンドを叩けば、その情報も得ることができますし、先述の Wikipedia の記事にはフォーマットもありますので、割と楽に取れるかと思います。
取得する車が何のコマンドが使えるかは、「OBD Info-san!」を使うと一覧化してくれます。試用版でもできるので、拾いたい情報をそこから探すという手もあります。


…というのを、Mikaboshi で実装していて、現在テスト中です。速度情報のほかにエンジン回転数も表示できるようにする予定でいます。
実際使ってみるとかなり楽しいというか、目指してたものが実現できたので割と満足しています。

割と古い開発中のころのスクリーンショットですが、こんな感じ。
f:id:ChiiAyano:20161202100208p:plain


少し気になるのが、OBD2 アダプターデバイスの個体差があるのかもしれませんが、OBD2 から得られる情報の更新頻度が急に落ちたり、応答がなくなったりすることがときたま起きます。
再接続したり、アダプターデバイスの電源を入れなおしたり、コマンド送信から受信までタイムアウト時間を設けて、一定時間返ってこなければ自動再接続したりするなど対策はしていますが、更新頻度の低下はうまく検知できてない状態です。
このあたりうまくやれる方法があれば教えていただけると助かります…。
また、まだテストしてる車が、家のホンダ フィット (GP6) しかなく、ほかの車でも動かせるかどうかを確認したいところです。
準備ができ次第リリースするので、協力いただける方がいればよろしくお願いします。

寄付をお願いします

1 日目に引き続いて恐縮ですが、アプリの「?」をタップして表示される「バージョン情報」に「作者に寄付する」ボタンがあります。実際に使ってみて、よければ寄付ください。アプリ内課金を利用しています。寄付額は選べませんが、寄付いただければ例えば Xamarin 使ったマルチプラットフォーム対応とかやる気が起きます。お願いします。

Advent Calendar でした

4 日目は od_10z さんです。誰もが息をのむような、すごい記事に期待しています。
twitter.com

*1:油圧メーターなどの付加メーターやレーダー探知機など

*2:先述のデバイスの場合、たいてい「SPP」といった名前で見つかると思います