ぷろじぇくと、みすじら。

Filtered by Tag: UWP

UWP API経由でBluetooth LEの通知(Notify)をデスクトップアプリで受け取れない問題

Created at: | Tag: C# UWP Bluetooth

以前、CC2650STK SensorTagをUniversal Windows Platform APIから使うというエントリを書いたのですが、最近Creators Update以降(というかInsider Preview)で実行したところ通知が動かなくなっていました。

UWPアプリからは問題なく動くのですが、UWP APIを使う通常のいわゆるクラシックデスクトップアプリからはRead/Writeは正常に行えるもののデバイス側からNotify(通知)を受け取ることができなくなっていていました。

要するにコンソールアプリなどでは GattCharacteristic.WriteClientCharacteristicConfigurationDescriptorAsync(GattClientCharacteristicConfigurationDescriptorValue.Notify) を呼び出したにも関わらず ValueChanged イベントが発生しないという状況です。

解決方法

問題の解決方法ですが起動時に CoInitializeSecurity を呼び出すというおまじないを書くと動くようになります。

CoInitializeSecurity(IntPtr.Zero, -1, IntPtr.Zero, IntPtr.Zero, RpcAuthnLevel.Default, RpcImpLevel.Identify, IntPtr.Zero, EoAuthnCap.None, IntPtr.Zero);
[DllImport("ole32.dll")]
static extern int CoInitializeSecurity(IntPtr pVoid, int
    cAuthSvc, IntPtr asAuthSvc, IntPtr pReserved1, RpcAuthnLevel level,
    RpcImpLevel impers, IntPtr pAuthList, EoAuthnCap dwCapabilities, IntPtr
    pReserved3);

public enum RpcAuthnLevel
{
    Default = 0,
    None = 1,
    Connect = 2,
    Call = 3,
    Pkt = 4,
    PktIntegrity = 5,
    PktPrivacy = 6
}

public enum RpcImpLevel
{
    Default = 0,
    Anonymous = 1,
    Identify = 2,
    Impersonate = 3,
    Delegate = 4
}

public enum EoAuthnCap
{
    None = 0x00,
    MutualAuth = 0x01,
    StaticCloaking = 0x20,
    DynamicCloaking = 0x40,
    AnyAuthority = 0x80,
    MakeFullSIC = 0x100,
    Default = 0x800,
    SecureRefs = 0x02,
    AccessControl = 0x04,
    AppID = 0x08,
    Dynamic = 0x10,
    RequireFullSIC = 0x200,
    AutoImpersonate = 0x400,
    NoCustomMarshal = 0x2000,
    DisableAAA = 0x1000
}

あまり真面目に追っていないのでおまじない感がすごいですが一応通知が来るようになりました。ちなみにLINQPadからの実行の場合にはうまくいかない可能性が高いです(多分Windows Runtimeの初期化が先に走ってしまったり、再利用されたりなど)。

Windows 10 Insider Preview Build 16273以降

と、調べて解決して、このエントリーを書いた今日、Insider Previewの新しいビルドが来たので更新したところなんと CoInitializeSecurity 呼ばなくても動くように直っていました…oh…。

とりあえず何事もなく動くようになったのでよかったよかったということで…。

CC2650STK SensorTagをUniversal Windows Platform APIから使う

Created at: | Tag: Windows UWP CSharp Bluetooth

Texas InstrumentsのSensorTag (CC2650STK)という様々なセンサーを持ったBluetooth LEの開発者向けデバイスを結構前ですが入手したので、それをUniversal Windows PlatformのAPI(アプリとは言っていない)を使ってコンソールアプリケーションから読み取ってみました。

Texas Instruments SensorTag (CC2650STK)

SensorTagの詳細はTIのサイトにあるのですが、「光、デジタル・マイク、磁気センサ、湿度、圧力、加速度計、ジャイロスコープ、磁力計、物体の温度、周囲温度の検出が可能な 10 個のセンサ」を持ち、コイン電池で動作し、Bluetooth Low Energyでホストと通信できる小さなデバイスです。そしてこれだけ詰まって4,000円程度と比較的安価なのも特徴です。

マクニカオンラインストアTIのサイトから購入することができ、なによりちゃんと技適を通っているので合法です。ごうほうです!ご・う・ほ・う!

そんなステキデバイスをコンソールアプリケーション(というかLINQPad)から使ってみたいなーと思ったので、Universal Windows Platform APIを使ってアクセスしてみました。

まずはじめに

あらかじめWindowsの設定画面からペアリングをしておきます。というのもペアリングする処理を入れるのはとても面倒というかそもそもUWPアプリ以外からはできないのです。

参考: アドバタイズしているBluetooth LEデバイスをUniversal Windows Platform APIでペアリングしたい

GATTサービス/キャラクタリスティックを取得する

それではデバイスからデータをとるためのコードを書いていきます。

まずはWindowsが認識しているデバイスの一覧から利用したいセンサーのGATTサービスを探してくる必要があります。センサーのGATTサービスのUUIDはTIのWikiPDFにあります。

今回は湿度センサーを使ってみるのでそのUUIDをセンサーのドキュメントから調べておきます。

// GATTのUUID (ここではHumidityセンサー)
// http://processors.wiki.ti.com/index.php/SensorTag_User_Guide#Gatt_Server
// http://processors.wiki.ti.com/images/a/a8/BLE_SensorTag_GATT_Server.pdf
var uuidHumidityService = new Guid("f000aa20-0451-4000-b000-000000000000");
var uuidHumidityData = new Guid("f000aa21-0451-4000-b000-000000000000");
var uuidHumidityConfig = new Guid("f000aa22-0451-4000-b000-000000000000");
var uuidHumidityPeriod = new Guid("f000aa23-0451-4000-b000-000000000000");

UUIDが分かったら、デバイス一覧から対応するデバイスを探して、さらにGATTサービスと設定用、データ通知用のキャラクタリスティックを取得します。

// サービスを持つデバイスを探す
var gattHumidityServices = await DeviceInformation.FindAllAsync(
    GattDeviceService.GetDeviceSelectorFromUuid(uuidHumidityService), null);

// デバイスIDからGattDeviceServiceを取得する
var gattHumidityService = await GattDeviceService.FromIdAsync(gattHumidityServices[0].Id);
// データと設定のCharacteristicを取得する
var charHumidityData = gattHumidityService.GetCharacteristics(uuidHumidityData)[0];
var charHumidityConfig = gattHumidityService.GetCharacteristics(uuidHumidityConfig)[0];

データを受け取る

キャラクタリスティックを取得したら、データを取得するためにデータが流れてくるキャラクタリスティックにイベントハンドラーを設定します。

// データの通知を受け取るイベントハンドラーを設定する
charHumidityData.ValueChanged += (sender, eventArgs) =>
{
    // データの処理方法はWikiを参照のこと
    // 例: http://processors.wiki.ti.com/index.php/CC2650_SensorTag_User's_Guide#Data_3
    var data = eventArgs.CharacteristicValue.ToArray();
    var temperature = (BitConverter.ToUInt16(data, 0) / 65536d) * 165 - 40;
    var humidity = (BitConverter.ToUInt16(data, 2) / 65536d) * 100;

    Console.WriteLine($"Temperature={temperature:0.0}℃, Humidity={humidity:0.0}%");
};

イベントハンドラーではデータのバイト列を取得できるのでそれをセンサーごとの処理方法に従って処理して値を取り出します。例えば湿度センサーの場合にはunsigned int16が二つで、片方が温度、片方が湿度の元データとなります。

受信の準備ができたので最後は通知を有効にします。一つはCC2650の設定に値を書き込み、もう一つは通知を有効にする WriteClientCharacteristicConfigurationDescriptorAsync メソッドの呼び出しです。

// データの通知を有効にする
// CC2650に設定を書き込む(大抵の場合は1で有効、0で無効)
await charHumidityConfig.WriteValueAsync(new byte[] { 1 });
// データ通知を有効にする
await charHumidityData.WriteClientCharacteristicConfigurationDescriptorAsync(
    GattClientCharacteristicConfigurationDescriptorValue.Notify);

これで実行するとデータが流れてくるのを確認できたら出来上がりです。おめでとうございます。

ちなみにペアリングした後何もしていないとSensorTagの電源が落ちるのでボタンを押してあげてください。

というわけで以上のC#(LINQPad用)のコードの全体はGistに置いてあります。

SensorTag as Observable

ところでセンサーのデータはある一定の間隔で送られてValueChangedイベントハンドラーに流れてきます。 データが流れてきて処理するといえばみなさん大好きなReactive Extensions、いわゆるRxの出番です。

というわけでRxの形にしてみましょう…といっても ValueChanged イベントハンドラーがあるので Observable.FromEventPattern メソッドを使えば簡単です。

var humidityObservable = Observable.FromEventPattern<TypedEventHandler<GattCharacteristic, GattValueChangedEventArgs>, GattValueChangedEventArgs>(
    h => charHumidityData.ValueChanged += h,
    h => charHumidityData.ValueChanged -= h)
    .Select(x =>
    {
        // データの処理方法はWikiを参照のこと
        // 例: http://processors.wiki.ti.com/index.php/CC2650_SensorTag_User's_Guide#Data_3
        var data = x.EventArgs.CharacteristicValue.ToArray();
        var temperature = (BitConverter.ToUInt16(data, 0) / 65536d) * 165 - 40;
        var humidity = (BitConverter.ToUInt16(data, 2) / 65536d) * 100;

        return new { Temperature = temperature, Humidity = humidity };
    })
    .Publish()
    .RefCount();

要するにイベントハンドラーの代わりに FromEventPattern でObservableにして、Select オペレーターでデータを処理して整えた形のストリームに変換します。

Observableになったらあとは好きなオペレーターを組み合わせて、Subscribe するだけです。

humidityObservable
    .Buffer(TimeSpan.FromSeconds(10)) // 10秒蓄えて
    .Select(x => x.Average(y => y.Humidity)) // 湿度平均にして
    .DistinctUntilChanged(x => Math.Round(x, 1)) // 少数点第一位までで変更があったら流す
    .Subscribe(x =>
    {
        Console.WriteLine($"最近の平均湿度 {x:0.0}%");
    });

上記の例は湿度の平均を出力する例ですが、これをイベントハンドラーで実装しようとするとすっきりしないというのが想像できるかと思います。

var sumHumidity = 0;
var lastAvgHumidity = 0;
var count = 0;
charHumidityData.ValueChanged += (sender, eventArgs) =>
{
    // データの処理方法はWikiを参照のこと
    // 例: http://processors.wiki.ti.com/index.php/CC2650_SensorTag_User's_Guide#Data_3
    var data = eventArgs.CharacteristicValue.ToArray();
    var temperature = (BitConverter.ToUInt16(data, 0) / 65536d) * 165 - 40;
    var humidity = (BitConverter.ToUInt16(data, 2) / 65536d) * 100;

    sumHumidity += humidity;
    if (++count == 5)
    {
        var avgHumidity = sumHumidity / 5;
        if (avgHumidity != lastAvgHumidity)
        {
            lastAvgHumidity = avgHumidity;
            sumHumidity = 0;
            count = 0;
            Console.WriteLine($"最近の平均湿度 {avgHumidity:0.0}%");
        }
    }
};

このあたりデータが流れてくるセンサーというものの特性とRxの相性の良さを感じますね。センサーも一つではないですし、それ以外の外部のイベントもあるのでこういうものを組み合わせるときには実に強力です。

RxでSensorTagのセンサーのデータを一通り受信できるようにする雑なクラスも同じくあげてありますので参考になるかもしれません。 通知間隔を変更するといったものは実装してませんのでそのあたりも試してみると良さそうです。

アドバタイズしているBluetooth LEデバイスをUniversal Windows Platform APIでペアリングしたい

Created at: | Tag: Windows UWP CSharp Bluetooth

Windows 10以降にはBluetooth LEを扱うAPIが備わっていて、そのAPIを使うことでアドバタイジングパケットを送受信したり、ペアリングしたりできます。

そんなわけでBLEデバイスをペアリングしたいと思って試してみたのですが意外と罠が多いです。

ペアリングの流れ

UWP APIを利用してペアリングして使う流れは次のようになっています。

  1. BluetoothLEAdvertisementWatcher クラスでアドバタイジングしているデバイスを探す
  2. 見つけたらUIスレッド内でペアリングを実行する
    1. BluetoothLEDevice.FromBluetoothAddressAsync メソッドでBLEデバイスを取得する
    2. 取得したオブジェクトの DeviceInformation.Pairing.PairAsync メソッドでペアリング実行
  3. DeviceWatcher クラスで追加/更新されたデバイスを監視
  4. GATTサービスを取得する

まずUIスレッド内でペアリングを実行する必要がある、という点に注意が必要です。

次にペアリングが完了してもすぐ使えると思ったらそんなことはなく、接続完了するまで待たなければなりません。ペアリングを待って(await)も接続を待ったわけではないので今度はDeviceWatcherでWindows側の準備が完了をするのを待ちます。めんどくさい…。 ちなみにBluetoothConnectionStatusは利用可能な接続状態とは違うようです。

ここからはUIにデバイスリストを表示してそこにデバイスを表示して選択するのとおおよそ同じだと思います。

デバイスを監視しているとペアリング後になにやらいろいろ追加されます。主にGATTのサービスなどがデバイスとしてAdded/Updatedイベントにやってきます。 なおデバイスがインストールされると途中で名前が変更される可能性があるのでAddedとUpdated両方見た方がよさそうです。

しかしやってきたデバイスIDは BluetoothLEDevice や GattDeviceService として開けるかどうかは開いてみないとわかりません。なんというめんどくささ…。 実際StackOverflowにもtry/catchしときみたいな回答がついてます。

…のでDeviceWatcherで監視するときにFilterで利用したいGATTのUUIDを指定することをお勧めします。

DeviceInformation.CreateWatcher(GattDeviceService.GetDeviceSelectorFromUuid(new Guid("f000aa00-0451-4000-b000-000000000000")));

これであればAdded/Updatedイベントで確実に指定したもののデバイスIDが来るので取得できます。

var gattService = await GattDeviceService.FromIdAsync(new Guid("f000aa00-0451-4000-b000-000000000000"));

ちなみに GattDeviceService.FromIdAsync メソッドはデバイスが存在しないとExceptionを投げますが、誰か別なプロセスが利用中の場合 null を返すので完全に罠です。

いずれにせよペアリングとデバイス列挙は別なので、ペアリングしてもそのままの流れで使い始めることはできないということのようです。

コンソールアプリケーションで動かしたい

ところでUWPのAPIはコンソールアプリケーションからも扱えて、BLEで通信も確かにできるのですがコンソールアプリケーションからはペアリングできません。

想像するにペアリング用のUIを提供している都合なのだと思いますが DeviceInformationPairing.PairAsync メソッドを呼ぶと失敗します(そもそも元からDispatcher経由で呼ばないといけない)。

DeviceInformationCustomPairing クラスなら?と思ったのですが現状対応していないようで、将来的には DeviceInformationCustomPairing クラスで対応するらしいので現状はあらかじめペアリングしておくとかが必要です。

DNS-SD(Bonjour)をUniversal Windows Platformから利用してサービスを探す

Created at: | Tag: DNS-SD UWP

Windows 10からDNS-SD/mDNS、いわゆるAppleのBonjourをネイティブサポートするようになりました。 要するに何ができるかというとIPアドレスやホスト名を知らなくても同じネットワークにいるデバイスとサービスを探すことができる仕組みです。

例えば、iTunesが他のコンピューターのiTunes 共有ライブラリを見つけるといったことに利用されています。そのちょっと便利な機能をUWPのAPIを通して利用できるようになっています。

アプリからDNS-SDでサービスを探す

さて、どうやって使うかですがまあまあ簡単で、通常のデバイスを列挙するのと同様にDeviceInformation クラスを利用します……が、列挙しようと思ってFindAllAsyncメソッドを使うと何も返ってこず(awaitすると進まない)、暫くするとエラーが出ます。

この挙動が正しいのかどうかよくわかりませんが、ともかく何やらうまく動きません。

そこでDeviceInformation.CreateWatcher メソッドを利用します。 このメソッドはデバイスがネットワークに追加されたり削除されたりというのを監視する方法です。この方法でならば列挙できます。

Watcherを作ってStartメソッドを呼び、監視を始めるとデバイスが見つかりAddedイベントが呼び出されますのでそこで適当に情報を保持しておけばよいです。 ちなみに監視して列挙終了時の EnumerationCompleted イベントが発生しないので完了を待つというのはやめた方がいいでしょう(多分それがFindAllAsyncが返ってこない原因な気がします)。

とりあえず、ネットワークにあるAndroid TV(正確にはAndroid TVが持つリモコン用サービス)を列挙するソースコードはこんな感じなります。

using Windows.Devices.Enumeration;

// ...

// DNS-SDのサービス名
var serviceName = "_androidtvremote._tcp";
// DNS-SDを利用する
var protocolId = "{4526e8c1-8aac-4153-9b16-55e86ada0e54}";
// デバイス検索文字列
var aqsFilter = $"System.Devices.AepService.ProtocolId:={protocolId} AND System.Devices.Dnssd.Domain:=\"local\" AND System.Devices.Dnssd.ServiceName:=\"{serviceName}\"";
// 取得するプロパティ
var properties = new[] {
    "System.Devices.Dnssd.HostName", // ホスト名 (自称)
    "System.Devices.IpAddress", // IPアドレスの配列(string[])
};

var watcher = DeviceInformation.CreateWatcher(aqsFilter, properties, DeviceInformationKind.AssociationEndpointService);
watcher.Added += (sender, args) =>
{
    Debug.WriteLine("Added: " + args.Id);
    foreach (var prop in args.Properties)
    {
        var value = prop.Value is string[] ? (String.Join(", ", (string[])prop.Value)) : prop.Value + "";
        Debug.WriteLine($"  - {prop.Key}: {value}");
    }
};
watcher.Updated += (sender, args) =>
{
    Debug.WriteLine("Updated: " + args.Id);
    foreach (var prop in args.Properties)
    {
        var value = prop.Value is string[] ? (String.Join(", ", (string[])prop.Value)) : prop.Value + "";
        Debug.WriteLine($"  - {prop.Key}: {value}");
    }
};
watcher.Start();

実行するとこんな感じにホスト名やIPアドレスなどの情報が取れます。

DnsSd#KJ-55X8500C._androidtvremote._tcp.local#0
  - System.ItemNameDisplay: KJ-55X8500C
  - System.Devices.DeviceInstanceId: 
  - System.Devices.Icon: 
  - System.Devices.GlyphIcon: 
  - System.Devices.InterfaceEnabled: 
  - System.Devices.IsDefault: 
  - System.Devices.PhysicalDeviceLocation: 
  - System.Devices.Aep.CanPair: False
  - System.Devices.Aep.IsPaired: False
  - {A35996AB-11CF-4935-8B61-A6761081ECDF} 18: 
  - {A35996AB-11CF-4935-8B61-A6761081ECDF} 13: 
  - System.Devices.Dnssd.HostName: Android.local
  - System.Devices.IpAddress: 192.168.1.100

アプリはこの情報をもとにサーバー(サービス)に接続すれば、相手の名前をあらかじめ知っていなくてもアクセスできる、というわけです。

手順をおさらいすると、DeviceWatcherを作るCreateWatcherメソッドの引数に渡すAQS文字列と呼ばれる検索フィルター文字列に、 DNS-SDプロトコル({4526e8c1-8aac-4153-9b16-55e86ada0e54})を使うことを指定して、さらにDNS-SDのサービス名(_daap.tcpなど)を指定して、検索を実行すれば見つかるという形になっています。例えばサービス名に_daap.tcpを指定するとネットワーク上のiTunesの共有ライブラリを探すことができます。

ちなみにこのUniversalなAPIはいわゆる従来のデスクトップアプリケーションからも呼び出すことができます。

制限事項

得られたホスト名(.localで終わる名前)をHttpClientなどに渡しても名前解決されますが、たまに失敗するのでIPアドレスを使った方がいいかもしれません。

なお、どうやら試した範囲ではサービス名を指定せずにネットワークに存在するサービスをすべて列挙する、ということはできないようです。System.Devices.Dnssd.ServiceNameを指定しないで検索すると何も返ってこないことに注意が必要です。

また、Hyper-Vが有効の場合にも動作しないという話もあります。