概要
SwitchBotで何かできない?と上司に言われ「C#から操作できればHoloLensで遊べるんじゃね?(ピコーン」ってなったはいいけど、世の中に情報がなくて辛かったのでまとめ。
最初はWPFでやってたのですが、限界を感じてUWPでやりました。
WPF編はこちら → WPF(C#)でSwitchBot(BLEデバイス)を操作する - Qiita
参考
- Switch Bot が届いたのでSingle Board Computerから操作してみた - ブログ
- WindowsデスクトップアプリでBLEのGATTで体温計と血圧計と通信する - Qiita
- Windows でも BLE を扱うことは可能か?〜できるかどうかやってみた〜 | 芳和システムデザイン
- DSAS開発者の部屋:Bluetooth
- Bluetooth アドバタイズ - UWP applications | Microsoft Docs
環境
- Windows10
- Visual Studio 2017(C#)
- SwitchBot
まずはラズパイで
公式のコードがラズパイ向けなので、まずは動かしてみる。
が、動かない。
iOSアプリでデバイスのMACは取れたのでBLEスキャンしてるあたりを適当にデバッグ。
結果的に
service_uuid = '1bc5d5a50200b89fe6114d22000da2cb'
これを
service_uuid = 'cba20d00-224d-11e6-9fb8-0002a5d5c51b'
に直したら動いた。
UWPでスキャン
BLEで調べるとアドバタイズだのサービスだのキャラクタリスティックだのUUIDだの出てくる。
よくわからない。
とりあえずスキャンしてみる。addLogはデバッグ用のTextBoxに追記する関数。
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が取れない。なんでやねん。
なるほど。なるほど?
単なるスキャンでは基本的な情報しか取れず、詳細情報をデバイスにリクエストする必要があるらしい。(初期スキャンにUUIDが載ってくる場合もあるらしい)
ので追記。
MS公式曰く、消費電力が増えるらしいが、情報取れないんじゃ何もできないんだから仕方ない。
this.advWatcher.ScanningMode = BluetoothLEScanningMode.Active;
あとドバドバ出てくるのが心臓に悪いのでフィルタ。
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()));
}
}
}
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
の通り、UUIDがType=07に入っている。
でもってデコード前のバイト列をPythonのコードと比べてみると
service_uuid = '1bc5d5a50200b89fe6114d22000da2cb'
なので、Pythonのコードはデコード前のUUIDを期待してたっぽい。
サービスとキャラクタリスティック
サービスを取って、キャラクタリスティックを取って、というのがお作法っぽいのでとりあえず全部取って羅列してみる。
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})");
}
}
}
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
サービスは一種の名前空間みたいなもので、その中に個別のコマンド=キャラクタリスティックが入っていると理解。
コマンドを送る
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があるとどれかがランダムで動きますw
実際には裏にマスタを持って指定するのがよいでしょう。
途中、コマンドを間違えたらSwitchBotがただの箱になって焦ったが、蓋を開けてリセットボタンを押すことで復旧。(電池のON/OFFでもいいらしい)
できてないこと
- 長押し時間の切り替え
- 「押す」「スイッチ」モードの切り替え
- パスワード設定
アプリからは長押し時間を変更できるが、このコマンドが公開されていない?
「押す」と「スイッチ」の切り替え、パスワード設定も同様。
設定できそうなのは'cba20002-224d-11e6-9fb8-0002a5d5c51b'だけなので、ここに何らかのバイト列を送ればいいと思われる。
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で直接コントロールできるというのは嬉しいですね!
できれば他のコマンドも公開して欲しいところですが。。。