在宅勤務が続いていて、時間には余裕がある感じなのに心の余裕が今ひとつ感じられない今日この頃です。
先日入手したM5StackをBLEデバイスにして、Windows 10側からセンサー値を読み込むプログラムを作っていたのですが、細かいところでハマって1日くらい回り道したので、以後気をつけようと言うつもりでまとめています。
- M5Stack側にはesp-idfのサンプルであるble_spp_server(githubリポジトリはこちら)を元にした簡単なシリアル通信サーバを入れています(Arduino IDEがあるのにあえてesp-idfにしているのは、うまくいったらESP32-WROOMの開発ボードでも同じコードを使い回したいというセコい思惑があったからです)
- Windows 10側はUWPでプログラムを組んで、M5Stack側のサーバからのアドバタイズパケットを受信してGATT, キャラクタリスティックを取得します。キャラクタリスティック値のうち、データ通知(Notify)を選んで通知が来たらイベントハンドラが起動して通知された値を表示します
まぁ、はじめの一歩としては単純なので良いかと思ったのですが…。
Windows10側でNotifyが取れない
Windows 10側のソースコードとしては以下のような簡単なものです。
- BluetoothLEAdvertisementWatcherクラスを作成しておき、ボタン(Scan_Start)が押されたらアドバタイズパケット受信スタートする
- なにかパケットを受信できたらWatcher_Receivedハンドラが呼ばれる
- Watcher_Receivedハンドラ内でble_spp_serverにあったGUIDを持ったデバイス、GATTサーバ、キャラクタリスティックを順に検索する。サービスのGUIDとキャラクタリスティックのGUIDはそれぞれABF0, ABF2であるので、これらに対応したGUIDを与えて検索している
- キャラクタリスティックまで取得できたら、それはNotify属性が立っているので、その設定をしてNotifyが来たときのイベントハンドラを追加する
これだけのことだったのですが、Notifyを1秒に一回送り続けていてもcharacteristic_Changed_Valueメソッドに入ってこない…んですね。iPhoneにインストールしているBLE ScannerではNotifyちゃんと取れているので送信失敗しているわけではないことは見えています。
public sealed partial class MainPage : Page
{
private BluetoothLEAdvertisementWatcher advWatcher = null;
private GattDeviceService gatt = null;
private GattCharacteristic characteristic = null;
public MainPage()
{
this.InitializeComponent();
this.advWatcher = new BluetoothLEAdvertisementWatcher();
this.advWatcher.Received += Watcher_Received;
}
private void Scan_Start_Click(object sender, RoutedEventArgs e)
{
this.advWatcher.Start();
Scan_Start.IsEnabled = false;
}
private async void Watcher_Received(BluetoothLEAdvertisementWatcher sender,BluetoothLEAdvertisementReceivedEventArgs args)
{
bool isFound = false;
var bleSrvUUID = args.Advertisement.ServiceUuids;
BluetoothLEDevice dev = null;
foreach (var uuid in bleSrvUUID)
{
if (uuid == new Guid("0000abf0-0000-1000-8000-00805f9b34fb")) /* このデバイスのフルGUID値 */
{
this.advWatcher.Stop();
dev = await BluetoothLEDevice.FromBluetoothAddressAsync(args.BluetoothAddress);
GattDeviceServicesResult result = await dev.GetGattServicesForUuidAsync(new Guid("0000abf0-0000-1000-8000-00805f9b34fb"));
for (int i = 0; i < result.Services.Count; i++) { /* サービスを探す */
GattDeviceService srv = result.Services[i];
if (srv.Uuid.Equals(new Guid("0000abf0-0000-1000-8000-00805f9b34fb"))) {
this.gatt = srv;
GattCharacteristicsResult characteristic = await srv.GetCharacteristicsForUuidAsync(new Guid("0000abf2-0000-1000-8000-00805f9b34fb"));
if (characteristic.Characteristics.Count > 0) {
this.characteristic = characteristic.Characteristics.First();
if (this.characteristic.CharacteristicProperties.HasFlag(GattCharacteristicProperties.Notify)) { /* Notifyフラグは立てているはず */
await this.characteristic.WriteClientCharacteristicConfigurationDescriptorAsync(GattClientCharacteristicConfigurationDescriptorValue.Notify);
isFound = true;
this.characteristic.ValueChanged += characteristic_Changed_Value; /* イベントハンドラ追加 */
break;
}
}
}
}
}
}
/* キャラクタリスティックまで接続完了していたら、状態を切り替える */
if (isFound == true)
{
await Dispatcher.RunAsync(Windows.UI.Core.CoreDispatcherPriority.Normal, () =>
{
if (dev != null)
{
Connected_deviceName.Text = dev.Name;
}
Connect_Condition.Text = "接続中";
Connect_Condition_Background.Background = new SolidColorBrush(Colors.PaleGreen);
});
}
else
{
await Dispatcher.RunAsync(Windows.UI.Core.CoreDispatcherPriority.Normal, () =>
{
Connect_Condition.Text = "切断中";
Connect_Condition_Background.Background = new SolidColorBrush(Colors.PaleVioletRed);
Scan_Start.IsEnabled = true;
});
}
}
private async void characteristic_Changed_Value(GattCharacteristic sender, GattValueChangedEventArgs args)
/* Notifyがデバイスから送信されたら、本ハンドラが呼ばれるはず… */
{
byte[] data = new byte[args.CharacteristicValue.Length];
Windows.Storage.Streams.DataReader.FromBuffer(args.CharacteristicValue).ReadBytes(data);
return;
}
}
Notifyを書き込めないとエラーになるみたい
原因としては、esp-idf側のble_spp_server実装をいじっているうちに、Notifyキャラクタリスティックを設定するためのキャラクタリスティックを消しており、書き込み属性を持ったキャラクタリスティックが存在しなくなっていたのが原因みたい…。
static const uint8_t spp_data_notify_ccc[2] = {0x00, 0x00};
…中略…
// Full HRS Database Description - Used to add attributes into the database
static const esp_gatts_attr_db_t spp_gatt_db[SPP_IDX_NB] =
{
…中略…
//SPP - data notify characteristic - Client Characteristic Configuration Descriptor
[SPP_IDX_SPP_DATA_NTF_CFG] =
{{ESP_GATT_AUTO_RSP}, {ESP_UUID_LEN_16, (uint8_t *)&character_client_config_uuid, ESP_GATT_PERM_READ|ESP_GATT_PERM_WRITE,
sizeof(uint16_t),sizeof(spp_data_notify_ccc), (uint8_t *)spp_data_notify_ccc}},
};
この部分。ble_spp_serverでは書き込みに来られてもspp_data_notify_cccには書き込むけれど何もしていないので、別になくってもサーバの動きとしては何も変わらないのですが、Windows 10 側ではWriteClientCharacteristicConfigurationDescriptorAsync(GattClientCharacteristicConfigurationDescriptorValue.Notify);を明示的に呼んで成功しないとNotifyに対するイベントが発生しないみたいです。
ちなみに、ble_spp_server側ではWRITE_COMMANDイベントを受け取る箇所がありますが、これは全く無視してもNotifyは検知できるようになります。なんだこれは…
まとめると
- 読み書き可能なNOTIFY_CFGみたいなキャラクタリスティックはble_spp_server側に用意しておく
- Windows 10プログラム側ではWriteClientCharacteristicConfigurationDescriptorAsync()が成功していることは確認しよう
ってことですかね。それにしてもBLE Scannerはなぜ読み込みしか許可していないNotifyキャラクタリスティックだけでちゃんと値を表示してくるんだろう?というのはありますが。BLE Scannerがかなり親切なのでしょうかね。