[C#/WinRT]Bluetooth v4(BLE)機器と通信する（C#で実装メモ）

# BLE関連記事

- [[C#/WinRT]Bluetooth v4(BLE)機器と通信する（BLE基礎メモ）](https://qiita.com/tera1707/items/e04d86a68a9f96e02fe9)
- [[C#/WinRT]Bluetooth v4(BLE)機器と通信する（C#で実装メモ）](https://qiita.com/tera1707/items/5505ed22b5ddf93795f4)
- [[C#/WinRT] WPFでBluetooth v4(BLE)機器とペアリングする](https://qiita.com/tera1707/items/1c066934774e0dd74cac)

# やりたいこと
[前回](https://qiita.com/tera1707/items/e04d86a68a9f96e02fe9)の記事で、何となくBluetooth v4で環境センサから値をとるためには、ここからとればよいというのが分かったので、それをWindowsで(WinRTで)実装するにはどう書けばよいかを調べていく。

# 前提
今回は、下記のものを使って実験をする。

|使うもの|内容|
|:--|:--|
|環境|WPF .net Framework 4.7.2|
|Nuget|Microsoft.Windows.SDK.Contracts　→　UWPのAPI(WinRTのAPI)を使うため|
|使用機器|OMRON 環境センサ 2JCIE-BL01|

- 使うクラス(UWP(WinRT)API)
 - Windows.Devices.Bluetooth;
 - Windows.Devices.Bluetooth.Advertisement;
 - Windows.Devices.Bluetooth.GenericAttributeProfile;
 - Windows.Devices.Enumeration;

基本的に、下記のサンプルをもとにしている。
https://github.com/microsoft/Windows-universal-samples/tree/master/Samples/BluetoothLE/cs
これをベースに、今回やろうとしていることに最低限必要な処理だけ抜き出したのが今回やったこと。

# 作成するもの
BLE環境センサから温度をとってきてアプリで使うということをしたいので、温度を採ってこれるライブラリ(dll)を作る。
まずは勉強のためにあまりエラー処理を入れずに、正常系だけで最低限必要な処理だけつくるイメージにする。機能としては、下記のようなイメージ。

1. ペアリング済みの環境センサと接続する
- 温度データ変化時に受け取る機能
- 温度データを好きなときに取得する機能
- 測定間隔を設定する機能


## 通信の流れ概要

前回の記事で、調べた限りの知識でとても乱暴に「どうやって通信するか」「温度を取得するか」を言うと、

- 各開発環境が用意している(今回だとWindowsのWinRTの)APIを使って、
- セントラル(WindowsPC)から、サービスのUUIDとキャラクタリスティックのUUIDを指定して接続し、
- ペリフェラル(センサ)にRead要求し、
- ペリフェラルからセントラルに帰ってきた情報を見て、温度を知る

ということをする。
測定間隔を設定する機能については、Read要求が「Write要求」になるイメージ。

# 通信の流れ
ペアリングは事前に手でやっておく前提で、下記のような流れになる。

### 事前準備
- Windowsの設定で、OMRONの環境センサ(EnvSensor-BL01またはEnv)をペアリングする。

###プログラムに実装する処理
- 共通処理
 - 環境センサを探す
 - 見つかった環境センサのデバイス情報を保存
 - デバイス情報からBluetoothLEDeviceを取得
- 温度データ変化時に受け取る機能/好きなときに取得する機能関連
 - BluetoothLEDeviceから温度取得に使いたいサービスを取得
 - サービスから温度取得に使いたいキャラクタリスティックを取得
 - キャラクタリスティックのディスクリプタを設定(Notify)
 - キャラクタリスティック変化時のハンドラを登録**→データ変化時に受け取る機能**
 - 温度取得に使いたいキャラクタリスティックに読み込み要求実施**→好きなときに取得する機能**
- 測定間隔を設定する機能関連
 - BluetoothLEDeviceから測定間隔設定に使いたいサービスを取得
 - サービスから測定間隔設定に使いたいキャラクタリスティックを取得
 - 測定間隔設定に使いたいキャラクタリスティックに書き込み要求実施

# 共通処理 の実装
値の読み込み、変化通知(Notify)、書き込みのすべてで共通して行う処理。

## 環境センサを探す
環境センサはWindowsの設定でペアリングされている前提。
下記のようにselectorを書いてウォッチャーを作成してスタートすると、デバイス情報更新時のハンドラとして登録されたメソッドにて、ペアリングされていて指定のUUIDを持つ機器のみ検索されてくる。

```cs
// ペアリングした環境センサを検索対象にする
// Guid SeosorServiceUuid = new Guid("0C4C3000-7700-46F4-AA96-D5E974E32A54");
string selector = "(" + GattDeviceService.GetDeviceSelectorFromUuid(SeosorServiceUuid) + ")";

// ウォッチャー(機器を監視、検索するやつ)を作成
// private DeviceWatcher DeviceWatcher { get; set; }
DeviceWatcher = DeviceInformation.CreateWatcher(selector);

// デバイス情報更新時のハンドラを登録
DeviceWatcher.Added += Watcher_DeviceAdded;

// ウォッチャーをスタート(検索開始)
DeviceWatcher.Start();
```

※ちなみに、selectorを下記のように書くと、ペアリングしていないBLE機器もまとめて検索されてくるっぽい。そこから、下の流れてデバイス情報を保存することもできるので、「ペアリングせずに対象機器を探してデータをとる」こともできる。(試したら実際できた)
ただ複数のセンサがある場合など、検索してどれを使うかを選ぶとかしないといけなくなりそうなため、やめた。**ペアリングは手動でやってもらうこととしたい。**

```cs
// BLEプロトコルを持つ機器全部
string selector = "(" + "System.Devices.Aep.ProtocolId:=\"{bb7bb05e-5972-42b5-94fc-76eaa7084d49}\"" + ")";
DeviceWatcher = DeviceInformation.CreateWatcher(selector, null, DeviceInformationKind.AssociationEndpoint);
```


## 見つかった環境センサのデバイス情報を保存
ウォッチャーが検索してくれた機器が、引数に載ってやってくるので、お目当ての機器かどうかを名前で判定して、当たっていたら保存する。

```cs
private async void Watcher_DeviceAdded(DeviceWatcher sender, DeviceInformation deviceInfo)
{
    if (deviceInfo.Name == "EnvSensor-BL01")
    {
        // デバイス情報を保存
        DeviceInformation = deviceInfo;

        // デバイス情報更新時のハンドラを解除しウォッチャーをストップ
        DeviceWatcher.Added -= Watcher_DeviceAdded;
        DeviceWatcher.Stop();
    }
}
```

※MSのサンプルだと`DeviceWatcher.Added`以外に、`Updated`、`Removed`、`EnumerationCompleted`、`Stopped`のハンドラも登録している。が、試したときにはAdded以外は使ってなかったので、今回は簡単のためにAddedだけにした。

## デバイス情報からBluetoothLEDeviceを取得
ウォッチャーで保存したデバイス情報に載っているIDをもとに、`BluetoothLEDevice`を取得する。

```cs
// デバイスを、ペアリングしている対象の機器のIDからとってくる
// private BluetoothLEDevice Device { get; set; }
Device = await BluetoothLEDevice.FromIdAsync(DeviceInformation.Id);
```

※APIの名前に「Connect」とつかないのでよくわからないが、`BluetoothLEDevice`を取得した時点でいわゆる「Connect」状態っぽい。（Device.ConnectionStatusが「Conected」になっている）


# 温度データ変化時に受け取る機能/好きなときに取得する機能 の実装
デバイス情報取得までは、データの取得、変化検出、設定に共通する処理。
これ以降は、取得したい値、設定したい値ごとに、対応するサービスやキャラクタリスティックを、UUIDを使って指定する。

## BluetoothLEDeviceから温度取得に使いたいサービスを取得
UUIDを指定して、温度の取得に必要なサービスを取得する。
今回の場合は、UUID=0x3000のサービスを取得する。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/352601/ed12c65f-d6be-8d96-e2ff-922f9da4a207.png)

```cs
// その機器のサービスをとる
// Guid SeosorServiceUuid = new Guid("0C4C3000-7700-46F4-AA96-D5E974E32A54");
var services = await Device.GetGattServicesForUuidAsync(SeosorServiceUuid);
```

※ベースのUUIDは`0C4CXXXX-7700-46F4-AA96-D5E974E32A54`なので、XXXXのところを3000に置き換える。
　以降、キャラクタリスティックのUUIDも同じようにする。

## サービスから温度取得に使いたいキャラクタリスティックを取得
サービスから、UUIDを指定してキャラクタリスティックを取得する。
今回の場合は、UUID=0x3001のキャラクタリスティックを取得する。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/352601/f1b9eec3-0c91-7083-7d08-2c3c21de9255.png)


```cs
// サービスから、目的のキャラクタリスティックのコレクションをとる
var characteristics = await services.Services[0].GetCharacteristicsForUuidAsync(LatestDataCharacteristicUuid);
// キャラクタリスティックを保存する
// private GattCharacteristic LatestDataCharacteristic { get; set; }
LatestDataCharacteristic = characteristics.Characteristics[0];
```

サービスもキャラクタリスティックも、取得の際はリストとして取得されてくるが、UUIDを指定しているので、取得されてくるのは1つだけのはず。なので、ここでは[0]を使っている。

## キャラクタリスティックのディスクリプタを設定(Notify)
初期状態(接続したての状態)では、ディスクリプタの値が「None」になっていて、値が変化しても検出しない(後で登録するハンドラを通らない)ので、「Notify」にしてやる。

```cs
// 温度のためのキャラクタリスティックを、変化検出(Notify)できるようにする
var status = await LatestDataCharacteristic.WriteClientCharacteristicConfigurationDescriptorAsync(GattClientCharacteristicConfigurationDescriptorValue.Notify);
```

## キャラクタリスティック変化時のハンドラを登録
変化検出したときに行うハンドラを登録する。
試したところ、この環境センサでは、後で設定する「計測間隔」の間隔でこのハンドラを通るっぽい。

```cs
// キャラクタリスティックの変化検出時のハンドラを登録
LatestDataCharacteristic.ValueChanged += ((s, a) =>
{
    // 値を読み込み
    var reader = DataReader.FromBuffer(a.CharacteristicValue);
    byte[] input = new byte[reader.UnconsumedBufferLength];
    reader.ReadBytes(input);

    // 読みこんだデータを整形
    double t = (double)(input[1] + 0x0100 * input[2]) / 100;    // 温度
    double h = (double)(input[3] + 0x0100 * input[4]) / 100;    // 湿度
    double i = (double)(input[5] + 0x0100 * input[6]);          // 照度
    double n = (double)(input[11] + 0x0100 * input[12]) / 100;  // 騒音
    Debug.WriteLine("温度：" + t + " 湿度：" + h + " 照度：" + i + " 騒音：" + n);
});
```

データの整形は、環境センサのキャラクタリスティックの仕様に則って行う。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/352601/e2099aa8-1bdf-0054-8770-ad5bc09e2eb2.png)


## 温度取得に使いたいキャラクタリスティックに読み込み要求実施
変化検出時ではなく、好きなときに値をとるために、下記の処理を書く。(両方やっても、どちらか片方だけでもOK)

```cs
// プロパティを取得して、読み込みがサポートされてるか判定
GattCharacteristicProperties properties = LatestDataCharacteristic.CharacteristicProperties;
if (properties.HasFlag(GattCharacteristicProperties.Read))
{
    // 値を読み込み
    GattReadResult r = await LatestDataCharacteristic.ReadValueAsync();
    if (r.Status == GattCommunicationStatus.Success)
    {
        // 成功したら、読み込んだデータを整形する
        var reader = DataReader.FromBuffer(r.Value);
        byte[] input = new byte[reader.UnconsumedBufferLength];
        reader.ReadBytes(input);

        // 整形は変化検出時と同じなので割愛
        // ・・・・
    }
}
```

※HasFlagでReadできるかどうか確認するのは、仕様書で確実にReadできることを確認ずみであれば、チェックしなくていい気がする。
(MSのサンプルでは、いろいろな機器のサービスやキャラクタリスティックを汎用的に読み書きできるようにしていたのでHasFlagでチェックしてるが、決め打ちのサービス/キャラクタリスティックであればいらないと思う)

# 測定間隔を設定する機能 の実装
今回使う温度センサは、デフォルトで5分間隔で温度やその他センサ値を計測し、最新のデータとしてキャラクタリスティックに反映させる。つまり、変化検出時のハンドラとして登録したメソッドが、5分に一回呼ばれることになる。
今回は、もうすこし早く値が更新されてほしいので、測定間隔を短く設定できるようにする。

## BluetoothLEDeviceから測定間隔設定に使いたいサービスを取得
→**「サービスから温度取得に使いたいキャラクタリスティックを取得」**と同じやり方で、UUIDだけ変えて行う。測定間隔設定用のサービスのUUIDは`0x3010`。

## サービスから測定間隔設定に使いたいキャラクタリスティックを取得
→**「サービスから温度取得に使いたいキャラクタリスティックを取得」**と同じやり方で、UUIDだけ変えて行う。測定間隔設定用のキャラクタリスティックのUUIDは`0x3011`。

## 測定間隔設定に使いたいキャラクタリスティックに書き込み要求実施
設定のためのキャラクタリスティックは、下記のような使用になっている。
今回は、5秒に設定することにする。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/352601/86709bb3-b96f-2065-86d6-4ba77dd89e17.png)

設定に必要なキャラクタリスティックを取得したうえで、下記を行う。

```cs
GattCharacteristicProperties properties = characteristic.CharacteristicProperties;

                    // 書き込みがサポートされてるか判定
                    if (properties.HasFlag(GattCharacteristicProperties.Write))
                    {
                        var writer = new DataWriter();
                        writer.ByteOrder = ByteOrder.LittleEndian;
                        writer.WriteInt16(5);// 設定値を作成(ここでは5秒)
                        GattCommunicationStatus r = await characteristic.WriteValueAsync(writer.DetachBuffer());
                        if (r == GattCommunicationStatus.Success)
                        {
                            Console.WriteLine("設定成功");
                        }
                    }
```

※今回の測定間隔は2バイトのデータのため、`writer.WriteInt16(5);`としている。
もし設定が1バイトのデータだった場合は、` writer.WriteByte(5);`にする必要がある。

# 以上
ここまでで、一応温度やその他のセンサの値をとるということはできた。
ここまでを調べたうえで分かったことや不明点を、以下にメモとして残す。

# 以下、メモ書き。

## 不明点メモ
- 「Connect」の反対の「Disconnect」のやり方が不明。たぶん、キャラクタリスティック、サービス、デバイスのインスタンスをdisposeしてイベントハンドラを-=したらよいと思われるが、本当にそれでよいのか未確認。
- Watcher_DeviceAdded以外(UpdatedとかRemovedとか)は、どういうときに呼ばれるのか不明。(遠くまでセンサもっていって通信が切れててもRemovedに来なかったし、再接続してもAddとかUpdated通らずに、普通にハンドラ通ってデータ取り出したので)
- selectorの書き方と効果が不明。サービスのUUIDをselectorで指定すると、なぜペアリングしたものだけになるのか？がわかってない(サービスのUUIDを渡しているGetDeviceSelectorFromUuidが返すAQSが、そういう文字列になっているのかも？=GetDeviceSelectorFromUuidの仕様かも？)(selectorに書いてるGUIDが何か、は[**ここ**](https://docs.microsoft.com/ja-jp/windows/uwp/devices-sensors/aep-service-class-ids)に書いてるっぽい)

## ペアリング関連メモ

- ペアリングなしでデータとりたい場合の流れ
 - デバイスを探す(watcherでセレクタをprotocolId=BLE全部(bb7bb05e-5972-42b5-94fc-76eaa7084d49にする))
 - 指定の名前のデバイス(EnvSensor-BL01)がAddされてきたら、そのデバイス情報を保存
 - BluetoothLEDeviceを取得
 - 使いたいサービスを取得
 - 使いたいキャラクタリスティックを取得
 - キャラクタリスティックのディスクリプタを設定(Notify)
 - キャラクタリスティック変化時のハンドラを登録

- ペアリングもアプリ内でやりたい場合の流れ
 - デバイスを探す(watcherでセレクタをprotocolId=BLE全部(bb7bb05e-5972-42b5-94fc-76eaa7084d49にする))
 - 指定の名前のデバイス(EnvSensor-BL01)がAddされてきたら、そのデバイス情報を保存
 - デバイス情報を使ってペアリングする
 - BluetoothLEDeviceを取得
 - 使いたいサービスを取得
 - 使いたいキャラクタリスティックを取得
 - キャラクタリスティックのディスクリプタを設定(Notify)
 - キャラクタリスティック変化時のハンドラを登録

## おすすめ仕様メモ
できれば、BLEで何かつくるときは、
「アプリを使う前に温度センサをペアリングしておいて、その機器とだけ通信する」仕様にしたい。
でないと、近所に同じセンサが複数あった時に、アプリ上でその機器をListにして、どれを使うか選択してもらわないといけないようなややこしい仕様になりそう。
(何でもアプリでやらず、Windowsの機能でやれるところ(使う機器の選択=ペアリング)はやってしまいたい)

## selectorについてメモ
・CreateWatcherで使う「selector」は、何をあらわしている？？
→https://docs.microsoft.com/ja-jp/windows/uwp/devices-sensors/aep-service-class-idsに書いてる。
このIDで、何を相手にするかを絞ってやることで、電池の寿命が延びたりする。
プロトコルIDを指定してCreateWatcherすることで、そのプロトコルを持つ機器をwatchできる。
下記に、セレクタの例がある。この例のようにセレクタを書けば、ウォッチするデバイスを絞れそう。
https://docs.microsoft.com/en-gb/windows/uwp/devices-sensors/build-a-device-selector#aqs-string-examples
https://docs.microsoft.com/ja-jp/windows/uwp/devices-sensors/enumerate-devices-over-a-network

# Windows Iot Core(ラズパイ3)について
WPFで実験用に作ったものを、ほぼ同じコードでUWPにして、PCのWindows10上で動かすと、うまく動作した。
ただ、ラズパイ3のWindows IOT core上で動かすと、うまく動かなかった。
具体的には、デバイス、サービス、キャラクタリスティックを取得するまではうまく動くが、Notifyをセットするところでほぼ毎回失敗し「Unreachable」になる。(「ほぼ」というのは、何度もやっていると、たまーにNotifyのsetが成功して、成功後は値をうまく取れ続ける、ということ。原因はわからなかった)
**Win Iot coreまたはラズパイ3ではいまのところそんなもん**として、今回はあきらめた。

# コード一式
https://github.com/tera1707/BleSample

# 参考
■GATT公式
公式ページトップ
https://www.bluetooth.com/

公式プロファイルとサービス一覧
https://www.bluetooth.com/specifications/gatt/

公式サービスとUUID一覧
https://www.bluetooth.com/specifications/gatt/services/

公式キャラクタリスティックとUUID一覧
https://www.bluetooth.com/specifications/gatt/characteristics/

公式UUID
https://www.bluetooth.com/specifications/assigned-numbers/

■実験につかうオムロンBL01の仕様
BAG型
https://www.omron.co.jp/ecb/product-detail?partId=73062
https://omronfs.omron.com/ja_JP/ecb/products/pdf/CDSC-015.pdf

USB型
https://www.omron.co.jp/ecb/product-detail?partId=73063

■MSページ
CreateWatcherで使うProtocolIdの一覧
https://docs.microsoft.com/ja-jp/windows/uwp/devices-sensors/aep-service-class-ids

AQS(ウォッチャーのselector)の書き方
https://docs.microsoft.com/ja-jp/windows/uwp/devices-sensors/enumerate-devices-over-a-network

■MSサンプル
サービス/キャラクタリスティック接続と取得/設定
https://github.com/microsoft/Windows-universal-samples/tree/master/Samples/BluetoothLE/cs

デバイス検索とペアリング
https://github.com/microsoft/Windows-universal-samples/tree/master/Samples/DeviceEnumerationAndPairing/cs


■非公式解説ページ
以下のページを見れば、だいたいBLEがどういう構造なのかわかる。
下記のページで勉強させていただきました。ありがとうございます。
どのページも、むちゃくちゃわかりやすいです。

BLEの説明
http://jellyware.jp/kurage/bluejelly/ble_guide.html

UUIDとは？
http://jellyware.jp/kurage/bluejelly/uuid.html

BLE v4とは
https://micro.rohm.com/jp/techweb_iot/knowledge/iot02/s-iot02/01-s-iot02/39

Gattの構造
https://micro.rohm.com/jp/techweb_iot/knowledge/iot02/s-iot02/04-s-iot02/3792

NotifyとIndicateの違い
http://yegang.hatenablog.com/entry/2014/08/09/195246

BLEの通信の手順(というか流れ)
http://www.microtechnica.tv/support/manual/clickble_man.pdf

アドバタイズパケットについて
https://qiita.com/gebo/items/2e51bebd3d26a3025d9f

アドバタイズパケットでデータをとる
https://qiita.com/komde/items/7209b36159da69ae79d2

.net4.7.0のときのAPIは、ペアリングしたBLE機器でないとServiceをとってこれないっぽい
https://codeday.me/jp/qa/20190629/1132145.html

MSブログの実装の説明
https://blogs.msdn.microsoft.com/shozoa/2016/02/28/windows-10-bluetooth/

Windowsのバージョンによって、ペアリングがいるとかいらないとか(15030以降は要らないとか)
https://docs.microsoft.com/ja-jp/windows/uwp/devices-sensors/bluetooth-dev-faq