#はじめに
BLEのGATT仕様で体温計と血圧計の測定値を取得するデスクトップアプリのサンプルです。
2018.10.03-- 大幅改定しました
#参考
- オライリー Bluetooth Low Energyをはじめよう
- [応用編]JavaScriptでバイナリデータを扱ってみる~Bluetoothの温度データ形式を理解する~(1/3)
- [応用編]JavaScriptでバイナリデータを扱ってみる~IEEE-754とIEEE-11073の浮動小数点~(2/3)
- BLEアドバタイズパケットの中身を調べてみた
#環境
- OS=Windows10
- 開発環境=Visual Studio 2015 1709
- C#
- .Net Framework 4.5.2
- WPFアプリケーション
- BLEデバイス
- iPhone Light Blue-Explorer でのVirual Peripherals(BLEデバイスエミュレータ)
- A&D Bluetooth内蔵 体温計 UT-201BLE
#プログラム
GitHubにソース上げています。
コチラ
#前提
- Windows10のBLE周りのAPIはUWPアプリ専用というらしく、通常のデスクトップアプリでは使えないらしいが、UWPアプリなどというものはなじみなく、作法も随分違うようで 嫌 です。
- そういう人のためにデスクトップアプリでもUWP用APIが使えるアドオンがありますのでNuGetからWindows 10 WinRT API Packをインストールしましょう。詳細はコチラ
- 2020.9.2追記:UwpDesktopは現在更新が止まっています。
そういう人のためにデスクトップアプリでもUWP用APIが使えるアドオンがありますのでNuGetからUwpDesktopをインストールしましょう。詳細はコチラ -
※追記:環境によってはUwpDesktopが対応していないので、コチラも参照ください。 - BLEデバイス(体温計とか血圧計)とWindowsのペアリングは事前にやっておいてください。このプログラムでペアリングまでやるわけではありません。
#BLEデバイスとの接続方法
体温計などのBLEデバイスとの接続方法は2つあります
1.DeviceInformationから接続
- PCについているデバイス情報を取ってくるAPIなので普通にコールするとものすごい数がGETされます。
- そんななんで、UUIDで絞り込みしてGETする必要があります。
- 下サンプルではHealth Thermometer Service(体温計)のUUIDで絞り込みGETしています。(UUIDについては後述)
- この場合、PCにインストールされている(ペアリングされている)デバイスを取得することになるので、体温計が通信可能な状態なのかはまだわかりません。通信不可な場合はGattDeviceServiceがnullになるなど、どこかでエラーが発生します。
// DeviceInformation.FindAllAsync()でDeviceInformationCollectionクラス(リスト)をGETする
DeviceInformationCollection devices = await DeviceInformation.FindAllAsync(GattDeviceService.GetDeviceSelectorFromUuid(new Guid("00001809-0000-1000-8000-00805f9b34fb")));
//GattDeviceService.FromIdAsync()でGattDeviceServiceクラスを取得する
GattDeviceService service = await GattDeviceService.FromIdAsync(devices.First().Id);
// service使って処理する
・・・
2.アドバタイズパケットから接続
- BLEデバイスは自分が居ることを周囲に伝えるためにアドバタイズパケットを発信します。
- このアドバタイズパケット受信して接続するのがこの方法です。
- ちなみに、アドバタイズパケットは全てのBLEデバイスが四六時中を発信しているわけではなく、デバイスによって色々です。(MAMORIOなどのiBeaconデバイスは四六時中発信しています)
- アドバタイズパケットのスキャンをStartすると結構色々取れるので、ここでもUUIDで絞り込み検索する必要があります。
public async void Start()
{
// アドバタイズパケットの受信開始
this.advWatcher = new BluetoothLEAdvertisementWatcher();
this.advWatcher.Start();
}
private async void Watcher_Received(BluetoothLEAdvertisementWatcher sender, BluetoothLEAdvertisementReceivedEventArgs args)
{
// アドバタイズパケット受信→Health Thermometerサービスを検索
var bleServiceUUIDs = args.Advertisement.ServiceUuids;
foreach (var uuidone in bleServiceUUIDs) {
if (uuidone == new Guid("00001809-0000-1000-8000-00805f9b34fb")) {
// 発見
this.advWatcher.Stop();
BluetoothLEDevice dev = await BluetoothLEDevice.FromBluetoothAddressAsync(args.BluetoothAddress);
GattDeviceService service = dev.GetGattService(new Guid("00001809-0000-1000-8000-00805f9b34fb"));
// service使って処理する
break;
}
}
}
上記のどっちでもいいのか?
どちらも結局はGattDeviceService
をGETするのですが、両方試した結果
2.アドバタイズパケットから接続 の方が安定してアクセスできる
ようです。
なんでだかわかりませんが、『1.DeviceInformationから接続』の方は突然GattDeviceServiceが取れなくなったりします。
#体温計
GATTの仕様ではHealth Thermometerとして定義されています。
##Health Thermometer Service
ひとまず以下の情報をインプットしいたほうがいいかもしれません。
- Service UUID = 0x1809
- GATT 仕様 - Viewer - Health Thermometer
- GATT 仕様 - PDF - Health Thermometer Profile
- GATT 仕様 - PDF - Health PDF Thermometer Service
##Service Characteristics
※Requirement=Mのものだけ表にしています。
Characteristic Name | UUID | Mandatory Properties | 備考 |
---|---|---|---|
Temperature Measurement | 0x2A1C | Indicate | フォーマット |
##ソース
private GattDeviceService Service;
public async void Start()
{
// アドバタイズパケットの受信開始
this.advWatcher = new BluetoothLEAdvertisementWatcher();
this.advWatcher.Start();
}
private async void Watcher_Received(BluetoothLEAdvertisementWatcher sender, BluetoothLEAdvertisementReceivedEventArgs args)
{
// アドバタイズパケット受信→Health Thermometerサービスを検索
bool find = false;
var bleServiceUUIDs = args.Advertisement.ServiceUuids;
foreach (var uuidone in bleServiceUUIDs) {
if (uuidone == new Guid("00001809-0000-1000-8000-00805f9b34fb")) {
// 発見
this.advWatcher.Stop();
BluetoothLEDevice dev = await BluetoothLEDevice.FromBluetoothAddressAsync(args.BluetoothAddress);
this.Service = dev.GetGattService(new Guid("00001809-0000-1000-8000-00805f9b34fb"));
find = true;
break;
}
}
if (find) {
// Get Temperature Measurement Characteristic
// Requirement = M , Mandatory Properties = Indicate
{
var characteristics = Service.GetCharacteristics(new Guid("00002A1C-0000-1000-8000-00805f9b34fb"));
this.Characteristic_Temperature_Measurement = characteristics.First();
if (this.Characteristic_Temperature_Measurement.CharacteristicProperties.HasFlag(GattCharacteristicProperties.Indicate)) {
this.Characteristic_Temperature_Measurement.ValueChanged += characteristicChanged_Temperature_Measurement;
await this.Characteristic_Temperature_Measurement.WriteClientCharacteristicConfigurationDescriptorAsync(GattClientCharacteristicConfigurationDescriptorValue.Indicate);
}
}
// Get Intermediate Temperature Characteristic
// Requirement = O , Mandatory Properties = Notify
{
var characteristics = Service.GetCharacteristics(new Guid("00002A1E-0000-1000-8000-00805f9b34fb"));
this.Characteristic_Intermediate_Temperature = characteristics.First();
if (this.Characteristic_Intermediate_Temperature.CharacteristicProperties.HasFlag(GattCharacteristicProperties.Notify)) {
this.Characteristic_Intermediate_Temperature.ValueChanged += characteristicChanged_Intermediate_Temperature;
await this.Characteristic_Intermediate_Temperature.WriteClientCharacteristicConfigurationDescriptorAsync(GattClientCharacteristicConfigurationDescriptorValue.Notify);
}
}
}
}
// Temperature_Measurement 体温測定値
private GattCharacteristic Characteristic_Temperature_Measurement;
private void characteristicChanged_Temperature_Measurement(GattCharacteristic sender, GattValueChangedEventArgs eventArgs)
{
byte[] data = new byte[eventArgs.CharacteristicValue.Length];
Windows.Storage.Streams.DataReader.FromBuffer(eventArgs.CharacteristicValue).ReadBytes(data);
// Parse
byte[] c1 = data.Skip(1).Take(4).ToArray();
var temperature = Common.ConvertToFloat(c1, Common.ConvType.IEEE_11073_32bit_float);
return;
}
// Intermediate_Temperature 体温の変化通知
private GattCharacteristic Characteristic_Intermediate_Temperature;
private void characteristicChanged_Intermediate_Temperature(GattCharacteristic sender, GattValueChangedEventArgs eventArgs)
{
byte[] data = new byte[eventArgs.CharacteristicValue.Length];
Windows.Storage.Streams.DataReader.FromBuffer(eventArgs.CharacteristicValue).ReadBytes(data);
return;
}
##解説
###1.検索して接続→サービスを取得
var bleServiceUUIDs = args.Advertisement.ServiceUuids;
foreach (var uuidone in bleServiceUUIDs) {
if (uuidone == new Guid("00001809-0000-1000-8000-00805f9b34fb")) {
// 発見
this.advWatcher.Stop();
BluetoothLEDevice dev = await BluetoothLEDevice.FromBluetoothAddressAsync(args.BluetoothAddress);
this.Service = dev.GetGattService(new Guid("00001809-0000-1000-8000-00805f9b34fb"));
find = true;
break;
}
}
これなんですけど、UUIDに00001809-0000-1000-8000-00805f9b34fbを指定していて、これが体温計(Health Thermometer Service)という意味です。→体温計縛りでデバイスを検索している。
Health Thermometer Serviceは0x1809ってことなんですが、これから完全なUUIDを作るとこうなります。
この辺のルールについて少し詳しく解説します。
####Service UUIDについて
- BLEデバイスがどんな機能(サービス)を持っているかを示す
- 完全UUIDは128bit(16Byte)だが、16bit(2byte)、32bit(4byte)の短縮UUIDも定義されている→16bit(2byte)がよく使われている
- 短縮フォーマットから完全なUUIDへ変換する場合は以下のとおり
- 32bit(4byte)UUID → xxxxxxxx-0000-1000-8000-00805F9B34FB
- 16bit(2byte)UUID → 0000xxxx-0000-1000-8000-00805F9B34FB
- BLE仕様:GATT Services UUID定義16bit(2byte)
- クラゲのIoTテクノロジー:UUID詳細
- 参考:オライリー Bluetooth Low Energyをはじめよう → 4.GATT-UUID P58
※GattServiceUuids.HealthThermometer という指定方法もあるようです。これが一番スマートです。
###2.Temperature Measurement Characteristicと接続
- Indicateなやつはこうやってイベントを登録して別途発生するイベント内でデータを取ります。
- WriteClientCharacteristicConfigurationDescriptorAsyncというやたら長いメソッドがイベントをEnableにするおまじないです。(ここに GattClientCharacteristicConfigurationDescriptorValue.None を指定するとイベントがDisableになります)
var characteristics = Service.GetCharacteristics(new Guid("00002A1C-0000-1000-8000-00805f9b34fb"));
this.Characteristic_Temperature_Measurement = characteristics.First();
if (this.Characteristic_Temperature_Measurement.CharacteristicProperties.HasFlag(GattCharacteristicProperties.Indicate)) {
this.Characteristic_Temperature_Measurement.ValueChanged += characteristicChanged_Temperature_Measurement;
await this.Characteristic_Temperature_Measurement.WriteClientCharacteristicConfigurationDescriptorAsync(GattClientCharacteristicConfigurationDescriptorValue.Indicate);
}
###3.Intermediate Temperature Characteristicと接続
- Notifyなやつです。Indicateとほぼ同じなので説明は省略
- IndicateとNotifyの違いは・・・いまひとつわからないのですが、GATT仕様で決まっているということで。
###4.Temperature Measurementのイベントで体温測定値を取得
byte[] data = new byte[eventArgs.CharacteristicValue.Length];
Windows.Storage.Streams.DataReader.FromBuffer(eventArgs.CharacteristicValue).ReadBytes(data);
// Parse
byte[] c1 = data.Skip(1).Take(4).ToArray();
var temperature = Common.ConvertToFloat(c1, Common.ConvType.IEEE_1073_32bit_float);
ソースはこれだけなんですが、ここでかなりハマりました。
DataReader.FromBuffer()でbyte[]でデータを取得する、データの構造はGATT仕様を見ればいいとして、問題は Temperature Measurement Value (Celsius) FLOAT であります。
- Temperature Measurement Value (Celsius) = 測定値、摂氏、℃
- 4byteのバイナリ
- FLOATなので浮動小数点型
- ただし、IEEE 11073 32bit float形式
- 例:0x69 0x01 0x00 0xFF = 36.1 、 0x1F 0x0E 0x00 0xFE = 36.15
どこでハマるのか、というと IEEE 11073 32bit float ということ。
これのコンバートが、C#のライブラリになく、かなりアチコチ探しましたが見つからず、結局自分でゴリゴリやらなければならなくて苦労しました。たぶんバリバリのプログラマであれば簡単なことだと思いますけど・・・Common.ConvertToFloat()でやってます。(思考停止コピペなので適当ですが)
#血圧計
Blood Pressure Service
- Service UUID = 0x1810
- GATT 仕様 Viewer Blood Pressure
- GATT 仕様 PDF Blood Pressure Profile
- GATT 仕様 PDF Blood Pressure Service
Service Characteristics
Characteristic Name | UUID | Mandatory Properties | 備考 |
---|---|---|---|
Blood Pressure Measurement | 0x2A35 | Indicate | フォーマット |
Blood Pressure Feature | 0x2A49 | Read | フォーマット |
※Requirement=Mustのもの |
##ソース
- 体温計と大体同じです。
- BloodPressure.cs を参照ください。
##解説
Health Thermometerと比べて特記事項だけ。
###Blood Pressure Featureと接続してデータ取得
- Readなやつはイベントにする必要はなく、すぐデータを読み取れます。
var characteristics = Service.GetCharacteristics(Common.CreateFullUUID("2A49"));
var chara = characteristics.First();
if (chara.CharacteristicProperties.HasFlag(GattCharacteristicProperties.Read)) {
GattReadResult result = await chara.ReadValueAsync();
var reader = Windows.Storage.Streams.DataReader.FromBuffer(result.Value);
byte[] input = new byte[reader.UnconsumedBufferLength];
reader.ReadBytes(input);
...
}
###Blood Pressure Measurementのイベントで血圧測定値取得
- IEEE 11073 16bit floatです、2byteのFloat。
- ゴリゴリつくったConvertToFloat()で変換しています。
byte[] c1 = data.Skip(1).Take(2).ToArray();
var val = Common.ConvertToFloat(c1, Common.ConvType.IEEE_11073_16bit_float);
Console.WriteLine($"Blood Pressure Measurement Compound Value - Systolic(最高血圧) = {val}mmHg");
#おつかれさまでした
- 鬼門はFLOAT、これが世界標準の手強さなのか・・・