LoginSignup
41

More than 3 years have passed since last update.

WindowsデスクトップアプリでBLEのGATTで体温計と血圧計と通信する

Last updated at Posted at 2018-09-16

はじめに

BLEのGATT仕様で体温計と血圧計の測定値を取得するデスクトップアプリのサンプルです。

2018.10.03-- 大幅改定しました

参考

環境

プログラム

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 Characteristics

※Requirement=Mのものだけ表にしています。

Characteristic Name UUID Mandatory Properties 備考
Temperature Measurement 0x2A1C Indicate フォーマット

ソース

GitHubからの抜粋Source_Health_Thermometer
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について

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 Characteristics

Characteristic Name UUID Mandatory Properties 備考
Blood Pressure Measurement 0x2A35 Indicate フォーマット
Blood Pressure Feature 0x2A49 Read フォーマット

※Requirement=Mustのもの

ソース

解説

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、これが世界標準の手強さなのか・・・

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
41