UWP（C#）でSwitchBot（BLEデバイス）を操作する

# 概要
SwitchBotで何かできない？と上司に言われ「C#から操作できればHoloLensで遊べるんじゃね？(ﾋﾟｺｰﾝ」ってなったはいいけど、世の中に情報がなくて辛かったのでまとめ。
最初はWPFでやってたのですが、限界を感じてUWPでやりました。
WPF編はこちら　→　[WPF（C#）でSwitchBot（BLEデバイス）を操作する - Qiita](https://qiita.com/natsu_san/items/3b95f21ceae6f8bb3af0)

#  参考
- [Switch Bot が届いたのでSingle Board Computerから操作してみた - ブログ](http://xx-prime.hatenablog.com/entry/2017/07/01/211509)
- [WindowsデスクトップアプリでBLEのGATTで体温計と血圧計と通信する - Qiita](https://qiita.com/gebo/items/41da7474936845d77d06)
- [Windows でも BLE を扱うことは可能か？〜できるかどうかやってみた〜 | 芳和システムデザイン](https://houwa-js.co.jp/blog/2019/12/20191212/)
- [DSAS開発者の部屋:Bluetooth](http://dsas.blog.klab.org/archives/cat_50050560.html)
- [Bluetooth アドバタイズ - UWP applications | Microsoft Docs](https://docs.microsoft.com/ja-jp/windows/uwp/devices-sensors/ble-beacon)

# 環境
- Windows10
- Visual Studio 2017(C#)
- SwitchBot

# まずはラズパイで
公式のコードがラズパイ向けなので、まずは動かしてみる。

- [GitHub - OpenWonderLabs/python-host: The python code running on Raspberry Pi or other Linux based boards to control SwitchBot.](https://github.com/OpenWonderLabs/python-host)

が、動かない。
iOSアプリでデバイスのMACは取れたのでBLEスキャンしてるあたりを適当にデバッグ。
結果的に

```Python
service_uuid = '1bc5d5a50200b89fe6114d22000da2cb'
```
これを

```Python
service_uuid = 'cba20d00-224d-11e6-9fb8-0002a5d5c51b'
```
に直したら動いた。

# UWPでスキャン
BLEで調べるとアドバタイズだのサービスだのキャラクタリスティックだのUUIDだの出てくる。
よくわからない。
とりあえずスキャンしてみる。addLogはデバッグ用のTextBoxに追記する関数。

```C#
private async Task StartScan()
        {
            //ウォッチャー作成
            this.advWatcher = new BluetoothLEAdvertisementWatcher();
            this.advWatcher.SignalStrengthFilter.SamplingInterval = TimeSpan.FromMilliseconds(1000);
            this.advWatcher.Received += this.Watcher_Received;
            // スキャン開始
            this.advWatcher.Start();
        }

        private async void Watcher_Received(BluetoothLEAdvertisementWatcher sender, BluetoothLEAdvertisementReceivedEventArgs args)
        {
            var mac = args.BluetoothAddress.ToString("x");
            var bleServiceUUID = args.Advertisement.ServiceUuids.FirstOrDefault();
            addLog("MAC " + mac);
            addLog("uuid " + bleServiceUUID);
        }
```
嵐のように出てくる。
あとUUIDが取れない。なんでやねん。

- [BLEアドバタイズパケットの中身を調べてみた - Qiita](https://qiita.com/gebo/items/2e51bebd3d26a3025d9f)

なるほど。なるほど？
単なるスキャンでは基本的な情報しか取れず、詳細情報をデバイスにリクエストする必要があるらしい。（初期スキャンにUUIDが載ってくる場合もあるらしい）
ので追記。
MS公式曰く、消費電力が増えるらしいが、情報取れないんじゃ何もできないんだから仕方ない。


```C#
this.advWatcher.ScanningMode = BluetoothLEScanningMode.Active;
```

あとﾄﾞﾊﾞﾄﾞﾊﾞ出てくるのが心臓に悪いのでフィルタ。

```C#
private async void Watcher_Received(BluetoothLEAdvertisementWatcher sender, BluetoothLEAdvertisementReceivedEventArgs args)
        {
            var targetMac = "*****"; // アプリで調べたMAC
            var mac = args.BluetoothAddress.ToString("x2");
            var bleServiceUUID = args.Advertisement.ServiceUuids.FirstOrDefault();
            var name = args.Advertisement.LocalName;
            //目当てのデバイスだけ
            if (mac == targetMac)
            {
                addLog("name " + name);
                addLog("uuid " + bleServiceUUID);
                //アドバタイズデータを羅列
                foreach (var adv in args.Advertisement.DataSections)
                {
                    addLog(" " + adv.DataType.ToString("x") + ":" + BitConverter.ToString(adv.Data.ToArray()));
                }
            }
        }
```

```txt:実行結果
name 
uuid 00000000-0000-0000-0000-000000000000
 01:06
 ff:59-00-FD-CF-4B-A5-38-C4
name 
uuid cba20d00-224d-11e6-9fb8-0002a5d5c51b
 07:1B-C5-D5-A5-02-00-B8-9F-E6-11-4D-22-00-0D-A2-CB
 16:00-0D-48-10-64
```

複数回受信して、内容が変わっているのが分かる。
[Advertising(解説) - BLE Docs](https://sites.google.com/a/gclue.jp/ble-docs/advertising-1/advertising#TOC-Ad-Type)
の通り、UUIDがType=07に入っている。
でもってデコード前のバイト列をPythonのコードと比べてみると

```Python
service_uuid = '1bc5d5a50200b89fe6114d22000da2cb'
```

なので、Pythonのコードはデコード前のUUIDを期待してたっぽい。

# サービスとキャラクタリスティック
サービスを取って、キャラクタリスティックを取って、というのがお作法っぽいのでとりあえず全部取って羅列してみる。

```C#
private async void Watcher_Received(BluetoothLEAdvertisementWatcher sender, BluetoothLEAdvertisementReceivedEventArgs args)
        {
            var targetMac = "*****"; // アプリで調べたMAC
            var mac = args.BluetoothAddress.ToString("x");
            var bleServiceUUID = args.Advertisement.ServiceUuids.FirstOrDefault();
            var name = args.Advertisement.LocalName;
            //目当てのデバイスだけ
            if (mac == targetMac)
            {
                addLog("name " + name);
                addLog("uuid " + bleServiceUUID);
                //アドバタイズデータを羅列
                foreach (var adv in args.Advertisement.DataSections)
                {
                    addLog(" " + adv.DataType.ToString("x") + ":" + BitConverter.ToString(adv.Data.ToArray()));
                }
            }
            //目当てのデバイスのUUIDが取れたら
            if (mac == targetMac && bleServiceUUID != Guid.Empty)
            {
                advWatcher.Stop();
                try
                {
                    //接続
                    BluetoothLEDevice device = await BluetoothLEDevice.FromBluetoothAddressAsync(args.BluetoothAddress);
                    addLog("get service");
                    var services = await device.GetGattServicesAsync();
                    foreach (var serv in services.Services)
                    {
                        addLog(serv.Uuid.ToString());
                        addLog("  get characteristics");
                        var charcteristic = await serv.GetCharacteristicsAsync();
                        foreach (var ch in charcteristic.Characteristics)
                        {
                            addLog("     " + ch.Uuid.ToString() + "  " + ch.CharacteristicProperties.ToString());
                            //読めそうなら読み取ってデコードしてみる
                            if (ch.CharacteristicProperties.HasFlag(GattCharacteristicProperties.Read))
                            {
                                var val = await ch.ReadValueAsync();
                                var data = val.Value.ToArray();
                                string text = System.Text.Encoding.UTF8.GetString(data);
                                addLog("     value " + BitConverter.ToString(data) + " >> " + text);
                            }
                        }
                    }         
                }
                catch (Exception ex)
                {
                    Console.WriteLine($"Exception...{ex.Message})");
                }
            }
        }
```

```txt:実行結果
name 
uuid 00000000-0000-0000-0000-000000000000
 01:06
 ff:59-00-FD-CF-4B-A5-38-C4
name 
uuid 00000000-0000-0000-0000-000000000000
 01:06
 ff:59-00-FD-CF-4B-A5-38-C4
name 
uuid cba20d00-224d-11e6-9fb8-0002a5d5c51b
 07:1B-C5-D5-A5-02-00-B8-9F-E6-11-4D-22-00-0D-A2-CB
 16:00-0D-48-10-64
get service
00001800-0000-1000-8000-00805f9b34fb //汎用情報
  get characteristics
     00002a00-0000-1000-8000-00805f9b34fb  Read, Write
     value 57-6F-48-61-6E-64 >> WoHand //デバイス名
     00002a01-0000-1000-8000-00805f9b34fb  Read
     value 00-00 >>      
     00002a04-0000-1000-8000-00805f9b34fb  Read
     value 06-00-18-00-00-00-90-01 >> 
00001801-0000-1000-8000-00805f9b34fb
  get characteristics
0000fee7-0000-1000-8000-00805f9b34fb // Custom UUID of Tencent Holdings Limited
  get characteristics
     0000fec8-0000-1000-8000-00805f9b34fb  Indicate // Custom UUID of Apple, Inc.
     0000fec7-0000-1000-8000-00805f9b34fb  Write    // Custom UUID of Apple, Inc.
     0000fec9-0000-1000-8000-00805f9b34fb  Read     // Custom UUID of Apple, Inc.
     value FD-CF-4B-A5-38-C4 >>   K 8 
cba20d00-224d-11e6-9fb8-0002a5d5c51b // ***目的のUUID***
  get characteristics
     cba20003-224d-11e6-9fb8-0002a5d5c51b  Notify
     cba20002-224d-11e6-9fb8-0002a5d5c51b  WriteWithoutResponse, Write
```
サービスは一種の名前空間みたいなもので、その中に個別のコマンド＝キャラクタリスティックが入っていると理解。

# コマンドを送る

```C#
private Guid switchUUID = new Guid("cba20d00-224d-11e6-9fb8-0002a5d5c51b");
        private Guid commandUUID = new Guid("cba20002-224d-11e6-9fb8-0002a5d5c51b");
        private async void Watcher_Received(BluetoothLEAdvertisementWatcher sender, BluetoothLEAdvertisementReceivedEventArgs args)
        {
            var targetMac = "*******"; // アプリで調べたMAC
            var mac = args.BluetoothAddress.ToString("x");
            var bleServiceUUID = args.Advertisement.ServiceUuids.FirstOrDefault();
            var name = args.Advertisement.LocalName;
            //アドレスじゃなくてUUIDで選択
            if (bleServiceUUID == switchUUID)
            {
                advWatcher.Stop();
                try
                {
                    //接続
                    BluetoothLEDevice device = await BluetoothLEDevice.FromBluetoothAddressAsync(args.BluetoothAddress);
                    //サービスUUIDを使って目的のサービスを取得
                    var gattService = await device.GetGattServicesForUuidAsync(switchUUID);
                    if (gattService.Status == GattCommunicationStatus.Success)
                    {
                        //コマンド送信用UUIDを使ってキャラクタリスティックを取得
                        var characteristics = await gattService.Services.FirstOrDefault().GetCharacteristicsForUuidAsync(commandUUID);
                        if (characteristics.Status == GattCommunicationStatus.Success)
                        {
                            var command = characteristics.Characteristics.FirstOrDefault();
                            if (command.CharacteristicProperties.HasFlag(GattCharacteristicProperties.Write))
                            {
                                byte[] comPress = { 0x57, 0x01, 0x00 };
                                var res = await command.WriteValueAsync(comPress.AsBuffer(), GattWriteOption.WriteWithResponse);
                                addLog(res.ToString());
                            }
                        }
                    }
                }
                catch (Exception ex)
                {
                    Console.WriteLine($"Exception...{ex.Message})");
                }
            }
        }
```

ｷｭｲｯｷｭｲｯ
うごいたー！
複数のSwitchBotがあるとどれかがランダムで動きますｗ
実際には裏にマスタを持って指定するのがよいでしょう。

途中、コマンドを間違えたらSwitchBotがただの箱になって焦ったが、蓋を開けてリセットボタンを押すことで復旧。（電池のON/OFFでもいいらしい）

# できてないこと
- 長押し時間の切り替え
- 「押す」「スイッチ」モードの切り替え
- パスワード設定


アプリからは長押し時間を変更できるが、このコマンドが公開されていない？
「押す」と「スイッチ」の切り替え、パスワード設定も同様。
設定できそうなのは'cba20002-224d-11e6-9fb8-0002a5d5c51b'だけなので、ここに何らかのバイト列を送ればいいと思われる。

```Python
    if act == "Turn On":
        con.sendline('char-write-cmd ' + cmd_handle + ' 570101')
    elif act == "Turn Off":
        con.sendline('char-write-cmd ' + cmd_handle + ' 570102')
    elif act == "Press":
        con.sendline('char-write-cmd ' + cmd_handle + ' 570100')
    elif act == "Down":
        con.sendline('char-write-cmd ' + cmd_handle + ' 570103')
    elif act == "Up":
        con.sendline('char-write-cmd ' + cmd_handle + ' 570104')
```

公式のソースでは、5種類のコマンドがあるが、今の所呼べるのは「Press」のみ。。。

# 今後
C#から呼べるのは確定したので、HoloLensでバーチャルボタンを押したら現実のボタンが押される、みたいなことをしたい。
ホームオートメーション的なIoTデバイスが増えているものの、GoogleHome連携やAlexa連携しかインタフェースが無いものが大半なので、BLEで直接コントロールできるというのは嬉しいですね！
できれば他のコマンドも公開して欲しいところですが。。。