Bluetooth SensorTagとHoloLens連携してジャイロセンサー取得するメモ

Bluetooth SensorTagとHoloLens連携してジャイロセンサー取得するメモです。

Bluetooth SensorTagで試したかったこととして、環境センサー部分だけでなく動きを捉えるジャイロ(角速度)がありました。これをどう活かせるかということで、HoloLensのBluetooth接続の勉強も兼ねてやってみます。

やりたいこと

image

ネタを準備。

image

このようにHoloLensはBluetoothから情報が取れるのでBluetooth SensorTagを連携し、SensorTagを強く振るとHoloLens内のSensorTagを模したポリゴンにジャイロの変化値が渡って回転するようにします。

まず確認したこと

まずちゃんとうまくいくかの確認で、UWPアプリのサンプルを試してみます。

Windows-universal-samples/Samples/BluetoothLEClient at dev · Microsoft/Windows-universal-samples

こちらをHoloLensに送り込んだところ、データはよく分かりませんがデータが上手く読まれている模様です。

image

ソースコードを見てみて、だいたいの必要な関数が分かりました。

Windows-universal-samples/Scenario2_ConnectToServer.xaml.cs at dev · Microsoft/Windows-universal-samples

参考にしたこと

参考にしたことは数知れず。脇道まで紹介するとキリがないので、すいませんがメインのところを。

さきほどのUWPサンプルはxamlのUIと連携したり、ペアリングもできてしまう優れものではあったのですが、それを除いたメインの挙動というところでは以下の記事が参考になりました。

CC2650STK SensorTagをUniversal Windows Platform APIから使う – ぷろじぇくと、みすじら。 – Misuzilla.org

ただ、こちらはHumidityの値をシンプルに取得するものでしたので、ジャイロの値を取得する部分は別途追いました。

まずは本家の資料ということで、

SensorTag User Guide – Texas Instruments Wiki

を良く読みつつも、受け取った後のセンサー値の計算式まわりはC#の枠を越えてしまいJavaScriptのソースも参考に。

とっつきやすさ重視でNodeJSのSensorTagのソースコードを参考にしました。

node-sensortag/cc2650.js at master · sandeepmistry/node-sensortag

このあたりを元に出来上がったコードはこちらです。

ソースコード

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

#if UNITY_UWP
using System;
using System.Runtime.InteropServices;
using System.Runtime.InteropServices.WindowsRuntime;
using System.Threading.Tasks;
using Windows.Devices.Bluetooth;
using Windows.Devices.Bluetooth.Advertisement;
using Windows.Devices.Bluetooth.GenericAttributeProfile;
using Windows.Devices.Enumeration;
using Windows.Foundation;
using Windows.Storage.Streams;
using Windows.UI.Core;
#endif

public class ActionBluetooth : MonoBehaviour
{

    // Use this for initialization
    void Start()
    {
        onConnect();
    }

    // Update is called once per frame
    void Update()
    {

    }


    void onConnect()
    {

#if UNITY_UWP
        Debug.Log("onConnect");

        // 接続待ちアニメーション
        iTween.MoveBy(this.gameObject, iTween.Hash(
            "y", 0.2,
            "time", 1.0,
            "loopType", "pingPong",
            "islocal", true
        ));

        Task.Run(async () =>
        {

            var UUID_GYR_SERV = new Guid("f000aa80-0451-4000-b000-000000000000");
            var UUID_GYR_DATA = new Guid("f000aa81-0451-4000-b000-000000000000");
            var UUID_GYR_CONF = new Guid("f000aa82-0451-4000-b000-000000000000");

            var gattGyroServices = await DeviceInformation.FindAllAsync(
                GattDeviceService.GetDeviceSelectorFromUuid(UUID_GYR_SERV), null);

            Debug.Log("gattGyroServices");
            var gattGyroService = await GattDeviceService.FromIdAsync(gattGyroServices[0].Id);

            Debug.Log("gattGyroService");
            var charGyroData = gattGyroService.GetCharacteristics(UUID_GYR_DATA)[0];
            var charGyroConfig = gattGyroService.GetCharacteristics(UUID_GYR_CONF)[0];

            GattReadResult Resultat = await charGyroConfig.ReadValueAsync();
            var Output = Resultat.Value.ToArray();
            Debug.Log(Output.Length);
            Debug.Log("Registre 0:" + Output[0].ToString());
            Debug.Log("Registre 1:" + Output[1].ToString());

            charGyroData.ValueChanged += (sender, eventArgs) =>
            {
                var data = eventArgs.CharacteristicValue.ToArray();

                // Gyro
                var gyroX = (BitConverter.ToInt16(data, 0) * 1.0) / (65536 / 500);
                var gyroY = (BitConverter.ToInt16(data, 2) * 1.0) / (65536 / 500);
                var gyroZ = (BitConverter.ToInt16(data, 4) * 1.0) / (65536 / 500);

                Debug.Log("gyroX : " + gyroX + " gyroY : " + gyroY + " gyroZ : " + gyroZ);

                if ( Math.Abs( gyroX ) < 20 && Math.Abs(gyroY) < 20 && Math.Abs(gyroZ) < 20 )
                {
                    // 小さい動き静止とする。影響を与えない。
                }
                else
                {
                    Task.Run(async () =>
                    {

                        UnityEngine.WSA.Application.InvokeOnAppThread(() => {

                            // ジャイロの変化値をX・Y・Zごとに加算回転
                            iTween.RotateAdd(this.gameObject, iTween.Hash(
                                "x", (float)gyroX,
                                "y", (float)gyroY,
                                "z", (float)gyroY,
                                "time", 1.0,
                                "islocal", true
                            ));

                        }, true);

                        await Task.Delay(100);
                    });
                }
                
            };

            Output[0] = 0x7F; // センサーの取得方法を与える 7

            var status = await charGyroConfig.WriteValueAsync(Output.AsBuffer());
            if (status == GattCommunicationStatus.Unreachable)
            {
                // 接続失敗
                Debug.Log("Initialize failed");
                
                UnityEngine.WSA.Application.InvokeOnAppThread(() =>
                {
                    // 待ちアニメloop停止
                    iTween.Stop(this.gameObject);
                    // しおしおと小さくなる
                    iTween.ScaleAdd(this.gameObject, iTween.Hash(
                        "x", -0.1f,
                        "y", -0.1f,
                        "z", -0.1f,
                        "time", 1.0,
                        "islocal", true,
                        "delay" , 0.05f
                    ));

                }, true);
                //
                await Task.Delay(100);
            } else {
                // 成功
                Debug.Log("Initialize Success!!");
                
                UnityEngine.WSA.Application.InvokeOnAppThread(() =>
                {
                    // 待ちアニメloop停止
                    iTween.Stop(this.gameObject);
                    // 接続うまくいったよ回転
                    iTween.RotateAdd(this.gameObject, iTween.Hash(
                        "x", 360,
                        "time", 0.5,
                        "islocal", true,
                        "delay", 0.05f
                    ));

                }, true);

                // データ通知を有効にする
                await charGyroData.WriteClientCharacteristicConfigurationDescriptorAsync(
                            GattClientCharacteristicConfigurationDescriptorValue.Notify);
            }
            
        });
#endif
    }

}

実際に動かしてみる

早速動かしてみましょう。今回はペアリングはされていて、SensorTagもONになって接続を待っている状況です。00:03あたりでクルンと前方に回転するのが接続できた合図です。

うまくいってます!

ハマったこと

いろいろハマりまくったのですが、列記していきます。

まず、ジャイロセンサーと加速度センサーの用途。めちゃくちゃ忘れてた。いや、センサー知らなかったころは、ほとんど区別して考えてなかった。

スマートフォンのジャイロセンサーと加速度センサーの違いとは何ですか? – … – Yahoo!知恵袋

を見て、そういえば加速度センサーはSensorTagの向きで、ジャイロセンサー(角速度センサー)は回転した方向を見るというのを理解し直しました。

そして、Bluetoothを扱う時、C#での値取得がNotifyやWriteValueAsyncで行うとよいかについては以下を参考にしました。

Windows10でBLEデバイスとGATTで通信するメモ – Qiita

加えて、GattCommunicationStatusでのエラー判定はこちらを参考にしました。

WPFからUWPのAPIを使ってBLEの操作をしよう – かずきのBlog@hatena

とはいいつつ、なにより一番ハマったのは、該当のセンサー情報を下ろすために1~7を伝えるところ。どうもByteで必要があるのですが、その値をそうやってByteで与えるかは以下がめちゃくちゃ参考になりました。

Bluetooth LE GATT code overview | Windows IoT

csharp
// Special value for Gyroscope to enable all 3 axes
 if (sensor == GYROSCOPE)
    writer.WriteByte((Byte)0x07);
else
    writer.WriteByte((Byte)0x01); 

こうやるんだ!

このコードを見て以前konashi連携でByte送る知見を思い出し対応できました。手始めには、int 整数をカジュアルに送って何も応答せずにエラーになって心が折れそうになった。

今後改善していきたいところ

  • 今回は既にペアリングされている前提での動作にしているが、自動検索して探していくような仕組みも入れたい
  • たまに初回から接続がコケるので再トライするソースコードも入れたい
  • 途中で切れるときもあるので検出して再接続したい
  • charGyroConfig.ReadValueAsyncでベースとなるバイナリ型もらっているが独自に作ったほうが勉強になりそうなのでやってみたい
  • 意識的な切断・再接続・他の人が使っているというBusy検出を実現できるようにしたい

おわりに

ということで連携できました。

BluetoothでIoT的なセンサーが受け取れると、周辺の状況を認識する仕組みをつくりやすいことや、はたまた、HoloLensで捉えにくい類の身体の動きを補助デバイスで肩代わりできたりと、用途が広がりますね。

さらにいうと、IoTについても、ネットワークを介して蓄積・分析したほうが良いものほうが良いものもあれば、このように直接やりとりして即答性を大事にしたもの、やりたいことにあわせて柔軟に制作できそうです。

それでは、よきHoloLens & Bluetooth & SensorTag Lifeを!