Windows10でBLEデバイスとGATTで通信するメモ

Windows PCでBLEデバイスとGATTなるプロトコルで通信する必要性があったので方法を調べてみた．BLE自体については「[【連載】Bluetooth LE (1) Bluetooth Low Energy の基礎](http://blog.fenrir-inc.com/jp/2013/09/bluetooth-le-firststep.html)」に詳しく書いてあったので参照した．どうやら，キャラクタリスティックと呼ばれるレジスタめいたものとそれを識別するアドレスみたいなのが用意されていて，そこを読んだり書いたりするらしい．とりあえず，手元のAndroidに[NordicのBLEモニタアプリ](https://play.google.com/store/apps/details?id=no.nordicsemi.android.mcp)を入れて挙動を確認すると，理解しやすかった．

#環境設定
WindowsでのBLEのサポートを使うには，MetroアプリケーションようのAPIであるところのWindowsランタイム(WinRT?)を使うらしい．
[MSDNのレファレンス](https://msdn.microsoft.com/en-us/library/windows/apps/windows.devices.bluetooth.genericattributeprofile.aspx)を見てみると，殆どがデバイスやキャラクタリスティックを検索するためのクラスで，実際に読んだり書いたりする[`GattCharacteristic`](https://msdn.microsoft.com/ja-jp/library/windows/apps/windows.devices.bluetooth.genericattributeprofile.gattcharacteristic)はやたらメソッドが少ないようで簡単そうである．
ちなみに，C#からWinRT APIを使う場合，VisualStudioのプロジェクトファイルを**外部のテキストエディタ**で編集したりコンポーネントへの参照を追加したりする必要があるらしい．やりかたは「[デスクトップ アプリからのWinRT API利用](https://ufcpp.wordpress.com/2012/09/18/%E3%83%87%E3%82%B9%E3%82%AF%E3%83%88%E3%83%83%E3%83%97-%E3%82%A2%E3%83%97%E3%83%AA%E3%81%8B%E3%82%89%E3%81%AEwinrt-api%E5%88%A9%E7%94%A8/)」を参照のこと．

#テスト実装
今回，通信したかった相手はNordicチップ上のUARTサービスだったので，サービスやキャラクタリスティックのUIDは[NordicのToolbox](https://github.com/NordicSemiconductor/Windows-nRF-Toolbox/blob/master/nRFToolbox.Service/ToolboxIdentification.cs)の定義ファイルから拝借した．
試しに，UARTサービスへのターミナルを書いてみた．

``` csharp
using System;
using System.Threading.Tasks;
using System.Runtime.InteropServices.WindowsRuntime;

using Windows.Devices.Enumeration;
using Windows.Devices.Bluetooth.GenericAttributeProfile;

using Common.Service;

namespace BLETest
{
    class Program
    {
        static void Main(string[] args) {
            communicate().Wait();
        }
        static void callback(GattCharacteristic sender, GattValueChangedEventArgs eventArgs) {
            Console.Write(System.Text.Encoding.ASCII.GetString(eventArgs.CharacteristicValue.ToArray()));
        }
        static async Task communicate() {
            var selector = GattDeviceService.GetDeviceSelectorFromUuid(ToolboxIdentifications.GattServiceUuids.Nordic_UART);
            var collection = await DeviceInformation.FindAllAsync(selector);
            foreach (DeviceInformation info in collection)
            {
                Console.WriteLine(string.Format("Name={0} IsEnabled={1}", info.Name, info.IsEnabled));

                var service = await GattDeviceService.FromIdAsync(info.Id);

                var tx_characteristics = service.GetCharacteristics(ToolboxIdentifications.GattCharacteristicsUuid.TX)[0];
                var rx_characteristics = service.GetCharacteristics(ToolboxIdentifications.GattCharacteristicsUuid.RX)[0];
                rx_characteristics.ValueChanged += callback;
                await rx_characteristics.WriteClientCharacteristicConfigurationDescriptorAsync(GattClientCharacteristicConfigurationDescriptorValue.Notify);
                while (true)
                {
                    var input = Console.ReadLine();
                    var result = await tx_characteristics.WriteValueAsync(System.Text.Encoding.ASCII.GetBytes(input + "\n").AsBuffer(), GattWriteOption.WriteWithoutResponse);
                }
            }
        }
    }
}
```

ソース内での通信手順は以下の通り．
1. `GattDeviceService.GetDeviceSelectorFromUuid`を使って目的のUIDを持つサービスを検索するための検索条件(`selector`)を作成．
2. `DeviceInformation.FindAllAsync`を使って検索条件に合致するサービスを持っているデバイスの一覧(`collection`)を取得．
2. `GattDeviceService.FromIdAsync`でデバイス内のサービスを取得．
3. `GetCharacteristics`メソッドを使ってサービスの中から目的のキャラクタリスティックの一覧を取得．今回はどっちも一つしか該当しないことがわかってるので，先頭の要素だけを利用した．
4. 受信用のキャラクタリスティックに受信時のコールバックメソッドを登録．
5. `WriteClientCharacteristicConfigurationDescriptorAsync(GattClientCharacteristicConfigurationDescriptorValue.Notify)`でデバイス→PCのデータが出来た時にPCへ通知するように設定．
6. `WriteValueAsync`を使って書き込み用のキャラクタリスティックに書き込み．

#詰まったところ
最終的なコードは結構短めになったがいくつか詰まった点があったのでまとめておく．
### 受信にはNotifyを使った方がいい
GATTによる通信はキャラクタリスティックへの書き込みや読み込みを通して行うため，定期的に受信を行うポーリングでは，デバイス→PCの通信で同じ内容が送られてきているのか新しい内容が送られてきていないのかが判別出来ない．
そこで，上記のソースのようにキャラクタリスティックの変更通知を有効にすることで，デバイス→PCの新しい内容が来たときにコールバックメソッドを呼ぶことが出来るようになる．これにより，同じ内容が2度送信された際にきちんと処理出来るようになる．
### NordicのUARTサービスへの送信は`WriteValueAsync`を使う
[MSDNのデモコード](https://msdn.microsoft.com/ja-jp/windows/uwp/devices-sensors/gatt-scenarios)では，PC→デバイスの通信で`GattReliableWriteTransaction`を使用している．これは，エラーチェック等の機能を持つリッチな通信らしくイマイチ速度が出ないらしい．今回，利用したNordicのUARTサービスは`GattReliableWriteTransaction`ではなくエラーチェック等のない非同期書き込み(`WriteValueAsync`)を利用していた．通信自体にエラーチェック等が無いためデータレートが稼げるらしいが，エラーチェック等を自前で実装する必要がありそうだ．（実際，高頻度でデバイスに向かって送信を行うとデータの読み落としが発生した．）
### 受信，送信共に`IBuffer`でやりとりするので，`String`からは変換が必要
おそらく，これはBLEの問題と言うよりC#に僕が慣れていないことによるものだが受信送信共に，メソッドはバイト列のやりとりに`IBuffer`クラスを利用していた．今回のターミナルソフトのように平文ASCIIを送受信したい場合`System.Text.Encoding.ASCII`クラスを使って文字列とバイト列を変換する必要があった．おそらく様々な文字コードに対応するためっぽいが，ASCIIの処理ぐらいもう少し簡潔に出来ないものだろうか・・・

#まとめ
とりあえずWindows上のC#からBLEデバイスと通信できたので当初の目的は達成できた．が，まだまだ需要が少ないせいかネット上の情報も少ないしもう少しとっとと流行ってほしい・・・（ポケGoPlusとかもBLEだし！）
個人的には慣れ親しんだPythonからWinRTを呼べればうれしいが，Async/Awaitなんかの非同期処理もあるし結構ハードルが高そう・・・とは言うものの，3.5で導入されたし近いうちに使えるようになったりするのかな・・・?