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

CC2650STK SensorTagをUniversal Windows Platform APIから使う

Created at:

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