HoloLensアプリ(Unity)とNode-REDで動くWebSocketサーバーをつなぐメモ

HoloLensアプリ(Unity)とWebSocketをつなぐメモです。先日はHoloLenからHTTP POST連携しましたが、IoTを学んだときも「データを送るだけじゃなくて、相互にやり取りしてみたい」という欲求からSocket通信の必要性も感じたのでやってみます。

やりたいこと

今回はNode-REDで動くWebSocketサーバーとHoloLensをやりとりしてみます。

image

このように、HoloLensオブジェクトをAirTapすると浮き上がるアニメーションをして別PCのNode-REDにWebSocketメッセージを送られます。情報はクリックされたオブジェクトの名前とクリックされた時刻を伝えます。
また、Node-REDからのWebSocketメッセージを受け取ると、先ほどのHoloLensオブジェクトがランダムに回転するアニメーションをします。

まずHoloLensで動くコードの確信を得た

こちらのサンプルをそのまま動作させてみたところ、HoloLensで2D UWPアプリとして問題なく動いたため、まずHoloLensで動くコードの確信を得れました。

Windows-universal-samples/Samples/WebSocket at master · Microsoft/Windows-universal-samples

image

なお、WebSocketサーバーは別PCのNode-REDで作りました。こういう時、便利。という気づきも!

image

このように送受信する簡単な仕組みです。いまは未接続なのでdisconnectです。

image

さて先ほどのUWPアプリ内でConnectを押します。

image

この瞬間に接続されdisconnectがconnectに変わって接続できます。

いざ実装

今回のサンプルで関数の登場人物がわかりました。いざ実装ということで動くまでに対応したことを列挙しておきます。

2D UWPアプリでまずチェックする感覚や、UNITY_UWPまわりの「UWPのコードは素直にUWPでしか動かなくする」ことが方向性として合ってるあたり、筆者の中村さんにFacebook上でも直接コメントいただいてフォローいただきました感謝いたします。

実際の動作

ということでソースコードは後に回して実際の動作です。

起動時でメッセージ

image

このように空間に立方体を配置されていて、こちらが動作します。

image

既に起動の段階でWebSocketに接続され、、、

image

一度メッセージが飛ばされます。

すでになんだろうこのワクワク感!

Node-REDからのWebSocketメッセージでHoloLensオブジェクトが動く

今度はNode-REDからのWebSocketメッセージでHoloLensオブジェクトが動かします。別PCのNode-REDをスマホで表示したのちNode-REDを操作してメッセージを送ってオブジェクトが動作するか確認します。

無事ランダムに動きました。魔法感…!

HoloLensの操作をWebSocketメッセージで送る

HoloLensの操作をWebSocketメッセージで送ります。オブジェクトをAirTapするとメッセージを送られます。

この動作をしてみてNode-REDをみてみます。

image

操作されたオブジェクトの名前や時間などが送られています!

ソースコード

若干、await/asyncの置き換えが果たして正しいのか不安なところがありますが、まず動いたので良しとします。

その他にiTweenでアニメーションが設定しやすくて良き学びとなりました!

using UnityEngine;
using System.Collections;
using UnityEngine.Networking;
using System;
using System.Net;
using System.Net.Sockets;
using System.Threading;
using System.Collections.Generic;
using MiniJSON;

#if UNITY_UWP
    using Windows.Foundation;
    using Windows.Networking.Sockets;
    using Windows.Security.Cryptography.Certificates;
    using Windows.Storage.Streams;
    using System.Threading.Tasks;
#else
    using System.Text;
#endif

public class SphereCommands2 : MonoBehaviour
{
    Vector3 originalPosition;
    Vector3 originalRotation;
    Vector3 originalScale;

    public bool flagOnOff = false;

#if UNITY_UWP
    private MessageWebSocket messageWebSocket;
    private DataWriter messageWriter;
    private bool busy;
#endif

    // Use this for initialization
    void Start()
    {
        // 初期の位置記憶
        originalPosition = this.transform.localPosition;

        originalRotation = this.transform.localRotation.eulerAngles;

        originalScale = this.transform.localScale;

#if UNITY_UWP
        // HoloLens実機でWebSocket接続開始
        OnConnect();
#endif
    }

    // Called by GazeGestureManager when the user performs a Select gesture
    void OnSelect()
    {
        if (flagOnOff)
        {
            flagOnOff = false;

            // もとに戻る移動の動作
            iTween.MoveTo(this.gameObject, iTween.Hash(
                "y", originalPosition.y,
                "time", 2f,
                "oncomplete", "AnimationEnd",
                "oncompletetarget", this.gameObject,
                "easeType", "easeInOutBack"
            ));

            // もとに戻る回転の動作
            iTween.RotateTo(this.gameObject, iTween.Hash(
                "x", originalRotation.x,
                "time", 2f
            ));

            // もとに戻る大きさの動作
            iTween.ScaleTo(this.gameObject, iTween.Hash(
                "x", originalScale.x,
                "y", originalScale.y,
                "z", originalScale.z,
                "time", 2f
            ));
        }
        else
        {
            flagOnOff = true;

            var dic = Json.Deserialize("{\"value1\":\"\",\"value2\":2,\"value3\":3}") as Dictionary<string, object>;
            dic["value1"] = System.DateTime.Now.ToString();
            dic["value2"] = this.gameObject.name;
            dic["value3"] = flagOnOff;

            var json = Json.Serialize(dic);
            Debug.Log(json);

#if UNITY_UWP
            Task.Run(async () =>
            {
                AppendOutputLine("Send JSON : " + json);

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

                    AppendOutputLine("InvokeOnAppThread : iTween");

                    // クリック時にふわっと浮く動作
                    iTween.MoveTo(this.gameObject, iTween.Hash(
                        "y", originalPosition.y + 0.3,
                        "time", 2f,
                        "oncomplete", "AnimationEnd",
                        "oncompletetarget", this.gameObject,
                        "easeType", "easeInOutBack"
                    ));

                }, true);

                await WebSock_SendMessage(messageWebSocket, json);
            });
#endif


        }
    }

    // Called by SpeechManager when the user says the "Reset world" command
    void OnReset()
    {
        
    }

    // Called by SpeechManager when the user says the "Drop sphere" command
    void OnDrop()
    {
        
    }

#if UNITY_UWP


    private void OnConnect()
    {

        AppendOutputLine("OnConnect");
        
        messageWebSocket = new MessageWebSocket();

        //In this case we will be sending/receiving a string so we need to set the MessageType to Utf8.
        messageWebSocket.Control.MessageType = SocketMessageType.Utf8;

        //Add the MessageReceived event handler.
        messageWebSocket.MessageReceived += WebSock_MessageReceived;

        //Add the Closed event handler.
        messageWebSocket.Closed += WebSock_Closed;

        Uri serverUri = new Uri("ws://192.168.1.6:1880"); // 別PCのNode-REDのWebSocketにつながる

        try
        {
            Task.Run(async () => {
                //Connect to the server.
                AppendOutputLine("Connect to the server...." + serverUri.ToString());
                await Task.Run(async () =>
                {
                    await messageWebSocket.ConnectAsync(serverUri);
                    AppendOutputLine("ConnectAsync OK");

                    await WebSock_SendMessage(messageWebSocket, "Connect Start");
                });
                
            });
        }
        catch (Exception ex)
        {
            AppendOutputLine("error : " + ex.ToString());

            //Add code here to handle any exceptions
        }
        
    }
    
    private async Task WebSock_SendMessage(MessageWebSocket webSock, string message)
    {
        AppendOutputLine("WebSock_SendMessage : " + message);

        DataWriter messageWriter = new DataWriter(webSock.OutputStream);
        messageWriter.WriteString(message);
        await messageWriter.StoreAsync();
    }

    private void WebSock_MessageReceived(MessageWebSocket sender, MessageWebSocketMessageReceivedEventArgs args)
    {
        DataReader messageReader = args.GetDataReader();
        messageReader.UnicodeEncoding = UnicodeEncoding.Utf8;
        string messageString = messageReader.ReadString(messageReader.UnconsumedBufferLength);
        AppendOutputLine("messageString : " + messageString);

        //Add code here to do something with the string that is received.

        Task.Run(async () =>
        {

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

                AppendOutputLine("InvokeOnAppThread : iTween");

                // WebSocket受信時に回転が変わる
                iTween.RotateTo(this.gameObject, iTween.Hash(
                    "x", UnityEngine.Random.Range(1, 5) * 20,
                    "y", UnityEngine.Random.Range(1, 5) * 20,
                    "z", UnityEngine.Random.Range(1, 5) * 20,
                    "time", 2f
                ));

                // WebSocket受信時に大きさが変わる
                var scale = UnityEngine.Random.Range(1, 5) * 0.02;
                iTween.ScaleTo(this.gameObject, iTween.Hash(
                    "x", originalScale.x + scale,
                    "y", originalScale.y + scale,
                    "z", originalScale.z + scale,
                    "time", 2f
                ));

            }, true);

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

    private void WebSock_Closed(IWebSocket sender, WebSocketClosedEventArgs args)
    {
        //Add code here to do something when the connection is closed locally or by the server
    }

    private void AppendOutputLine(string value)
    {
        // OutputField.Text += value + "\r\n";
        Debug.Log(value);
    }

#endif 


}

その他参考文献

おわりに

以前、IoTでSocket通信ができると楽しいという記事もありましたがSocket通信ができるとHTTP通信で苦手なサーバーからのデータ受信側の動きも作りやすくなります。

これができるとデバイスと連携したインタラクションや、サーバーでデータを料理して面白い反応をさせる可能性が広がりますね!

それでは、よきHoloLens x WebSocket x Unity Life を!