HoloLens2 と Node-RED を WebSocket でやり取りするメモです。
「おおむね」以前の HoloLens1 の知識をベースにできた
こちらの記事をベースに動かせるか試してみまして、結果としては、以下のように無事成功しました!
https://twitter.com/1ft_seabass/status/1279602514992623616
ですが、ここまでだと、「おおむね」成功。
- HoloLens2 が Node-RED の WebSocket 初回接続時にメッセージを送る
- Node-RED が HoloLens2 にタイムスタンプ文字列を送って HoloLens2 側で受けつける
の2つだけしているので、あともう一歩です。実は残りの挙動である「HoloLens2 の Cube をタッチすると Node-RED の WebSocket にメッセージを送る」の実装に、少し苦労しました。
つくる前に心配していたところ
こちらで、ある程度雰囲気はつかんでいるものの、実機へのビルドというものが、いまいちピンと来ていませんでした。
既存のアプリを 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
で進めています。
以下の準備を完了している状態で進めます。
ボタンにする形状は薄く小さく
まずはじめは Scale を X:0.1 Y:0.1 Z:0.1 で立方体にしたときに、実際に指で押すと、当たり判定が瞬間的に複数出てしまうことがありました。
さんざん試行錯誤したのですが、ボタンの形状の話がこちらに出ていたので、Scale を X:0.1 Y:0.1 Z:0.2 にしたところ、うまく反応するようにできました。つまり、 Z:0.2 によって厚さを薄くしました。
タッチイベントの設定
このように、 NearInteractionまわりのドキュメント を参考にしつつ、上記のように、
- NearInteractionTouchable の設定
- TouchHandler の割り当て
- 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 のフローです。
ごくシンプルに、ルートで受け取ったデータを 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"}]
実際に動かしてみると、
まず、 Connect Start と接続されたことがわかります。接続数 1 と出ていることでも接続がわかります。
#HoloLens2 と Node-RED の WebSocket 双方向のやり取りのうち、以前の知見をベースにしてイケると思いきや、少々 HoloLens2 → Node-RED のほうで若干ハマったのだけど、やり取りできるようになった!直接、タッチで推せる体験はやっぱり素敵だ。 #nodered #noderedjp #WebSocket pic.twitter.com/JDWkLtj1P4
— Tanaka Seigo (@1ft_seabass) July 22, 2020
このように実際にCubeをタッチしてデータが送られます。
まとめると、
- タッチ時に複数タッチ判定が出てしまって苦労したが、これはスクリプト側が問題でなくUnityでのオブジェクトの作り方にコツが必要だった
- IL2CPP でスクリプトやアセンブリからの IL コードを C++ に変換するために、以前の挙動と少し違う場合がある模様。あいまい。
- とにもかくにも、今回で言うと毎回のデータ送信時の破棄に関して、以前は何もしなくてよかったが明示的に破棄するとうまくいった。
といったところで、何事もやってみないと見えてこないですね。試せてよかったです。