Texas InstrumentsのSensorTag (CC2650STK) という様々なセンサーを持ったBluetooth LEの開発者向けデバイスを結構前ですが入手したので、それをUniversal Windows PlatformのAPI(アプリとは言っていない)を使ってコンソールアプリケーションから読み取ってみました。
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のWiki とPDF にあります。
今回は湿度センサーを使ってみるのでそのUUIDをセンサーのドキュメントから調べておきます。
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 );var gattHumidityService = await GattDeviceService.FromIdAsync(gattHumidityServices[0 ].Id);var charHumidityData = gattHumidityService.GetCharacteristics(uuidHumidityData)[0 ];var charHumidityConfig = gattHumidityService.GetCharacteristics(uuidHumidityConfig)[0 ];
データを受け取る キャラクタリスティックを取得したら、データを取得するためにデータが流れてくるキャラクタリスティックにイベントハンドラーを設定します。
charHumidityData.ValueChanged += (sender, eventArgs) => { var data = eventArgs.CharacteristicValue.ToArray(); var temperature = (BitConverter.ToUInt16(data, 0 ) / 65536 d) * 165 - 40 ; var humidity = (BitConverter.ToUInt16(data, 2 ) / 65536 d) * 100 ; Console.WriteLine($"Temperature={temperature:0.0 } ℃, Humidity={humidity:0.0 } %" ); };
イベントハンドラーではデータのバイト列を取得できるのでそれをセンサーごとの処理方法に従って処理して値を取り出します。例えば湿度センサーの場合にはunsigned int16 が二つで、片方が温度、片方が湿度の元データとなります。
受信の準備ができたので最後は通知を有効にします。一つはCC2650の設定に値を書き込み、もう一つは通知を有効にする WriteClientCharacteristicConfigurationDescriptorAsync
メソッドの呼び出しです。
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 => { var data = x.EventArgs.CharacteristicValue.ToArray(); var temperature = (BitConverter.ToUInt16(data, 0 ) / 65536 d) * 165 - 40 ; var humidity = (BitConverter.ToUInt16(data, 2 ) / 65536 d) * 100 ; return new { Temperature = temperature, Humidity = humidity }; }) .Publish() .RefCount();
要するにイベントハンドラーの代わりに FromEventPattern
でObservableにして、Select
オペレーターでデータを処理して整えた形のストリームに変換します。
Observableになったらあとは好きなオペレーターを組み合わせて、Subscribe
するだけです。
humidityObservable .Buffer(TimeSpan.FromSeconds(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) => { var data = eventArgs.CharacteristicValue.ToArray(); var temperature = (BitConverter.ToUInt16(data, 0 ) / 65536 d) * 165 - 40 ; var humidity = (BitConverter.ToUInt16(data, 2 ) / 65536 d) * 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のセンサーのデータを一通り受信できるようにする雑なクラス も同じくあげてありますので参考になるかもしれません。 通知間隔を変更するといったものは実装してませんのでそのあたりも試してみると良さそうです。