HoloLens2 と Node-RED を WebSocket でやり取りするメモ

HoloLens2 と Node-RED を WebSocket でやり取りするメモです。

「おおむね」以前の HoloLens1 の知識をベースにできた

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

こちらの記事をベースに動かせるか試してみまして、結果としては、以下のように無事成功しました!

ですが、ここまでだと、「おおむね」成功。

  • HoloLens2 が Node-RED の WebSocket 初回接続時にメッセージを送る
  • Node-RED が HoloLens2 にタイムスタンプ文字列を送って HoloLens2 側で受けつける

の2つだけしているので、あともう一歩です。実は残りの挙動である「HoloLens2 の Cube をタッチすると Node-RED の WebSocket にメッセージを送る」の実装に、少し苦労しました。

つくる前に心配していたところ

HoloLens2 エミュレーターでMRTK2.0を動かすまでのメモ

こちらで、ある程度雰囲気はつかんでいるものの、実機へのビルドというものが、いまいちピンと来ていませんでした。

既存のアプリを HoloLens 2 で利用できるようにするドキュメントにあるように、

  • x86ベースではなくてARM32 および ARM64 ビルド
  • プロジェクトの IL2CPP への切り替え
  • 果たして、以前のUWPのコードがうまく動くのかどうか

というあたりが心配でしたが、上記の記事でだいぶ把握は追いついており、HoloLens1と同様にHoloLens2をPCに挿して、Unityで書きだされたVIsual Studio プロジェクトからデバイスにビルドすればうまくいきました。

事前準備

環境は

  • Unity 2019.4.1.f1
  • Visual Studio Tools for Unity 4.6.1.0
  • Microsoft Visual Studio Community 2019 Version 16.6.3

で進めています。

以下の準備を完了している状態で進めます。

ボタンにする形状は薄く小さく

image

まずはじめは Scale を X:0.1 Y:0.1 Z:0.1 で立方体にしたときに、実際に指で押すと、当たり判定が瞬間的に複数出てしまうことがありました。

image

さんざん試行錯誤したのですが、ボタンの形状の話がこちらに出ていたので、Scale を X:0.1 Y:0.1 Z:0.2 にしたところ、うまく反応するようにできました。つまり、 Z:0.2 によって厚さを薄くしました。

タッチイベントの設定

image

このように、 NearInteractionまわりのドキュメント を参考にしつつ、上記のように、

  • NearInteractionTouchable の設定
  • TouchHander の割り当て
  • Cube自体に独自の CubeTouchEvent.cs を加える

といったことをしています。TouchHander から OnTouchStarted , OnTouchCompleted のイベントを、CubeTouchEventのTouchStarted , TouchCompletedに関連付けています。

CubeTouchEvent.cs の中身

こちらの記事をベースにつくられた CubeTouchEvent.cs の中身です。

using System.Collections;
using System.Collections.Generic;
using Microsoft.MixedReality.Toolkit.Input;
using UnityEngine;
using UnityEngine.Networking;
using System;
using System.Net;
using System.Net.Sockets;
using System.Threading;

#if WINDOWS_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 CubeTouchEvent : MonoBehaviour
{

#if WINDOWS_UWP
    private MessageWebSocket messageWebSocket;
#endif

    void Start()
    {
#if WINDOWS_UWP
        // HoloLens2 実機でWebSocket接続開始
        OnConnect();
#endif
    }

    void Update()
    {
        
    }

    public void TouchStarted(HandTrackingInputEventData eventData)
    {
        // TouchStartedが2度送られるときはボタンの形状を薄くしたり小さめにすると1度だけになった

        Debug.Log("TouchStarted");

        // 直接タッチするとちょっと小さくなる
        this.transform.localScale = new Vector3(0.09f, 0.09f, 0.02f);

#if WINDOWS_UWP
        try
        {
            Task.Run(async () => {
                await WebSock_SendMessage(messageWebSocket, "Touched!! 1");
            });
        }
        catch (Exception ex)
        {
            Debug.Log("error : " + ex.ToString());
        }
#endif


    }

    public void TouchCompleted(HandTrackingInputEventData eventData)
    {
        Debug.Log("TouchCompleted");

        // 元に戻る
        this.transform.localScale = new Vector3(0.1f, 0.1f, 0.02f);
    }

#if WINDOWS_UWP
 
 
    private void OnConnect()
    {
 
        Debug.Log("OnConnect");
         
        messageWebSocket = new MessageWebSocket();

        messageWebSocket.Control.MessageType = SocketMessageType.Utf8;
        messageWebSocket.MessageReceived += WebSock_MessageReceived;
        messageWebSocket.Closed += WebSock_Closed;
 
        Uri serverUri = new Uri("ws://192.168.1.105:1880"); // 別PCのNode-REDのWebSocketにつながる
 
        try
        {
            Task.Run(async () =>
            {
                await messageWebSocket.ConnectAsync(serverUri);

                Debug.Log("Connect to the server...." + serverUri.ToString());
                Debug.Log("ConnectAsync OK");
 
                await WebSock_SendMessage(messageWebSocket, "Connect Start");
            });
        }
        catch (Exception ex)
        {
            Debug.Log("error : " + ex.ToString());
        }
         
    }
     
    private async Task WebSock_SendMessage(MessageWebSocket webSock, string message)
    {
        DataWriter messageWriter = new DataWriter(webSock.OutputStream);
        messageWriter.WriteString(message);
        await messageWriter.StoreAsync();
        messageWriter.DetachStream(); // ストリームの破棄。今回加えたら何度も送られるようになった。加えないと一度だけしか送られない。

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

        Task.Run(async () =>
        {
 
            UnityEngine.WSA.Application.InvokeOnAppThread(() => {
                // Node-RED からデータが来るとなんだか細くなる
                this.transform.localScale = new Vector3(0.05f, 0.05f, 0.1f);
            }, true);
 
            await Task.Delay(100);
        });
    }
 
    private void WebSock_Closed(IWebSocket sender, WebSocketClosedEventArgs args)
    {
        // WebSock_Closed
    }
 
#endif

}

実は以前の記事のままのコードでは、「HoloLens2 の Cube をタッチすると Node-RED の WebSocket にメッセージを送る」ときに、タッチ時に1度だけ送られる、あるいはまったく送られない症状に遭遇して、困りました。

さまざまな試行錯誤の末、対応したのが以下の修正です。

    private async Task WebSock_SendMessage(MessageWebSocket webSock, string message)
    {
        DataWriter messageWriter = new DataWriter(webSock.OutputStream);
        messageWriter.WriteString(message);
        await messageWriter.StoreAsync();
        messageWriter.DetachStream(); // ストリームの破棄。今回加えたら何度も送られるようになった。加えないと一度だけしか送られない。

    }

送り終えたら messageWriter.DetachStream(); で明確に破棄するように対応したら、今まで通り、押すたびに送られるようになりました。どうもC++でコンパイルされるせいか、振る舞いが微妙に変わっているのかもしれません。いい知見になりました。

Node-RED の準備

WebSocket をやり取りする Node-RED のフローです。

image

ごくシンプルに、ルートで受け取ったデータを debug ノードに表示したり、タイムスタンプを送ることができます。

フローのJSONはこちらです。

js
[{"id":"5b36a107.f83ad","type":"websocket in","z":"856fbae5.267df8","name":"","server":"b994b2c6.576df","client":"","x":410,"y":160,"wires":[["92080b90.7a2fb8"]]},{"id":"8d6d839.be84a8","type":"websocket out","z":"856fbae5.267df8","name":"","server":"b994b2c6.576df","client":"","x":650,"y":220,"wires":[]},{"id":"92080b90.7a2fb8","type":"debug","z":"856fbae5.267df8","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":670,"y":160,"wires":[]},{"id":"b6be4a64.8113d8","type":"inject","z":"856fbae5.267df8","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":380,"y":220,"wires":[["8d6d839.be84a8"]]},{"id":"b994b2c6.576df","type":"websocket-listener","z":"","path":"/","wholemsg":"false"}]

実際に動かしてみると、

image

まず、 Connect Start と接続されたことがわかります。接続数 1 と出ていることでも接続がわかります。

このように実際にCubeをタッチしてデータが送られます。

image

まとめると、

  • タッチ時に複数タッチ判定が出てしまって苦労したが、これはスクリプト側が問題でなくUnityでのオブジェクトの作り方にコツが必要だった
  • IL2CPP でスクリプトやアセンブリからの IL コードを C++ に変換するために、以前の挙動と少し違う場合がある模様。あいまい。
    • とにもかくにも、今回で言うと毎回のデータ送信時の破棄に関して、以前は何もしなくてよかったが明示的に破棄するとうまくいった。

といったところで、何事もやってみないと見えてこないですね。試せてよかったです。