10
Help us understand the problem. What are the problem?

posted at

updated at

ANT+で脈拍数を取得してUnityで作ったVR空間に表示させる

大熊 元気と申します。
IoTとVRを組み合わせてデジタルツインとか、何か付加価値を出したいと思いながら日々 遊んでいます。

バイタルセンサとVRを組み合わせてみました

  1. ANT+で脈拍数の取得
  2. VR空間で脈拍数を表示


  3. 脈拍数に応じてエフェクトの変化


構成図

◆構成図はこんな感じ
image.png
◆ハードウェアはこんな感じ
IMG_20181128_190149.jpg
・スマートウォッチ:GARMIN vívosmart®4 (ANT+対応のものが必要で、GARMIN社がANT+の生みの親)
・ANT+ドングル:AZ4U (ANT+をPCで受けるために必要。適当に選定)
・PC、HMD:SteamVRが動くやつ(参考→https://qiita.com/GenkiOkuma/items/62d1c7f20f8e75d0662e)

◆Version
Unity : 2018.2.14f1
SteamVR : 2.0.1

ANT+のUSBドングルからNode.jsまで

USBドングルから脈拍数を取得するためにこちらのライブラリを使います。https://github.com/Loghorn/ant-plus
これをインストールして、USBドングルから脈拍数を取得するNoede.jsを書きます。

npm install ant-plus

サンプルに入っているtwo-sticks.jsを下記のように改造してUSBドングルからUnityへと脈拍数を中継するサーバとします。
<変更点>
・stick(ANT+のUSBドングル)2本を1本に変更
・deviceIDを区別するように変更

Node.js
const Ant = require('ant-plus');
const stick = new Ant.GarminStick2;
const sensor = new Ant.HeartRateSensor(stick);
var deviceID = null;

if (process.argv.length >= 3) {
    deviceID = process.argv[2];
}
sensor.on('hbData', function (data) {
    if (deviceID == null) {
        console.log(data.DeviceID, data.ComputedHeartRate);
    } else if (deviceID == data.DeviceID) {
        console.log(data.ComputedHeartRate);
    }
});

stick.on('startup', function () {
    sensor.attach(0, 0);
});

if (!stick.open()) {
    console.log('Stick not found!');
}

これでスマートウォッチからANT+のドングルを経由してPCまで脈拍数を持ってこれました。

Node.jsからUnityまで

凹み様のAssetを使います。https://github.com/hecomi/NodejsUnity
関連記事はこちら→http://tips.hecomi.com/entry/2014/04/21/001606
これによって、Unityのexeから外部のNode.jsを起動して連携できます。ここでは上記で作成した脈拍数を取得するNode.jsを仕込みます。

<使い方>
・UnityのAssetとしてNodejsUnityをインポート
・NodejsUnity/Assets/StreamingAssets/node.exe が古いので新しいものに差し替える
・起動したいNode.jsをUnity Project/Assets/StreamingAssetsの下に置く
image.png
NodeJs.csを下記のように改造する。
(変更点→起動したいNode.jsのパスを指定、出力先を2つ定義)

C#
    #region [ member variables ]
    public string DeviceID = "";
    private string scriptPath = "HeartRate/main.js";
    private System.Diagnostics.Process process_ = null;
    private List<string> logs_ = new List<string>();
    public int logLength = 10;
    public string log = "";
    public Text DisplayedText;
    public Text DisplayedText1;
    private SynchronizationContext Context;

(変更点→脈拍数をエフェクトの変化に使うためにSensorValueを定義、Contextでスレッドを分ける、脈拍数をテキスト2か所に出力させる)

C#
    void OnOutputData(object sender, System.Diagnostics.DataReceivedEventArgs e)
    {
        if (logs_.Count > logLength) logs_.RemoveAt(0);
        logs_.Add(e.Data);
        log = "";
        logs_.ForEach(line => { log += line + "\n"; });

            Context.Post(_ => {
                float.TryParse(e.Data, out SensorValue);
                if (DisplayedText != null)
                {
                    DisplayedText.text = e.Data;
                }
                if (DisplayedText1 != null)
                {
                    DisplayedText1.text = e.Data;
                }
            }, null);
    }

・表示先のテキストをドラッグする。
(ここでは、目の前に常に表示されるテキストと空間に固定されたテキストの2か所を作りました。)
image.png

スクリプト全体はこんな感じ

C#
using UnityEngine;
using System.Collections;
using System.Collections.Generic;
using UnityEngine.UI;
using System.Threading;

public class HeartRateSensorJs : SensorDeviceBase
{
    #region [ static members ]
#if UNITY_STANDALONE_WIN
    private static readonly string NodeBinPath = "node.exe";
#endif
#if UNITY_STANDALONE_OSX
    private static readonly string NodeBinPath = "node";
#endif
    private static readonly string NodeScriptPath = ".node/";
    private static string NodeBinFullPath;
    private static string NodeScriptFullPath;
    private static bool IsInitialized = false;
    #endregion

    #region [ member variables ]
    public string DeviceID = "";
    private string scriptPath = "HeartRate/main.js";
    private System.Diagnostics.Process process_ = null;
    private List<string> logs_ = new List<string>();
    public int logLength = 10;
    public string log = "";
    public Text DisplayedText;
    public Text DisplayedText1;
    private SynchronizationContext Context;


    #endregion

    #region [ getter / setter ]
    public bool isRunning { get; set; }
    #endregion

    #region [ member functions ]
    private void SetFullPath()
    {
        if (!IsInitialized)
        {
            NodeBinFullPath = System.IO.Path.Combine(Application.streamingAssetsPath, NodeBinPath);
            NodeScriptFullPath = System.IO.Path.Combine(Application.streamingAssetsPath, NodeScriptPath);
            IsInitialized = true;
        }
    }

    void Awake()
    {
        Context = SynchronizationContext.Current;
        isRunning = false;
        SetFullPath();
        Run();
    }

    void OnApplicationQuit()
    {
        if (process_ != null && !process_.HasExited)
        {
            process_.Kill();
            process_.Dispose();
        }
    }

    void Run()
    {
        if (isRunning)
        {
            Debug.LogError("Already Running: " + scriptPath);
            return;
        }

        process_ = new System.Diagnostics.Process();
        process_.StartInfo.WindowStyle = System.Diagnostics.ProcessWindowStyle.Hidden;
        process_.StartInfo.FileName = NodeBinFullPath;
        process_.StartInfo.Arguments = NodeScriptFullPath + scriptPath + " " + DeviceID;
        process_.StartInfo.CreateNoWindow = true;
        process_.StartInfo.RedirectStandardOutput = true;
        process_.StartInfo.RedirectStandardError = true;
        process_.StartInfo.UseShellExecute = false;
        process_.StartInfo.WorkingDirectory = NodeScriptFullPath;
        process_.OutputDataReceived += OnOutputData;
        //        process_.ErrorDataReceived += OnOutputData;
        process_.EnableRaisingEvents = true;
        process_.Exited += OnExit;
        process_.Start();
        process_.BeginOutputReadLine();
        process_.BeginErrorReadLine();
        isRunning = true;
    }

    void Stop()
    {
        if (!isRunning)
        {
            Debug.LogError("Already Stopping: " + scriptPath);
            return;
        }
        isRunning = true;
        process_.Kill();
        process_.Dispose();
    }

    void OnOutputData(object sender, System.Diagnostics.DataReceivedEventArgs e)
    {
        if (logs_.Count > logLength) logs_.RemoveAt(0);
        logs_.Add(e.Data);
        log = "";
        logs_.ForEach(line => { log += line + "\n"; });

            Context.Post(_ => {
                float.TryParse(e.Data, out SensorValue);
                if (DisplayedText != null)
                {
                    DisplayedText.text = e.Data;
                }
                if (DisplayedText1 != null)
                {
                    DisplayedText1.text = e.Data;
                }
            }, null);
    }

    void OnExit(object sender, System.EventArgs e)
    {
        isRunning = false;
        if (process_.ExitCode != 0)
        {
            Debug.LogError("Error! Exit Code: " + process_.ExitCode);
        }
    }

    private void OnDestroy()
    {
        if (process_ != null && !process_.HasExited)
        {
            process_.Kill();
            process_.Dispose();
            process_ = null;
        }

    }
    #endregion
}

UnityからHMDまで

ここでは割愛します。こちらの資料を参考にどうぞ。

脈拍数に比例してエフェクトやライトを変化させたりできます。ご要望があれば記事書きます。

まとめ&考察

・デジタル空間と物理空間がリアルタイムにシンクロしてデジタルツインな感じになった。
・プレイヤーの(自覚がない)状態によってVRコンテンツが動的に変化するイメージがわいた。
・何の役に立つかは不明...。とりあえず楽しい!

以上です。ありがとうございました。
アドベントカレンダー、この後も楽しいエンジニアが続きます!

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Sign upLogin
10
Help us understand the problem. What are the problem?