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

HoloLensアプリ(Unity)とWebSocketをつなぐメモです。先日はHoloLenからHTTP POST連携しましたが、IoTを学んだときも「データを送るだけじゃなくて、相互にやり取りしてみたい」という欲求からSocket通信の必要性も感じたのでやってみます。
やりたいこと
今回はNode-REDで動くWebSocketサーバーとHoloLensをやりとりしてみます。
このように、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
なお、WebSocketサーバーは別PCのNode-REDで作りました。こういう時、便利。という気づきも!
このように送受信する簡単な仕組みです。いまは未接続なのでdisconnectです。
さて先ほどのUWPアプリ内でConnectを押します。
この瞬間に接続されdisconnectがconnectに変わって接続できます。
いざ実装
今回のサンプルで関数の登場人物がわかりました。いざ実装ということで動くまでに対応したことを列挙しておきます。
- WebSocket - UWP app developer のソースをひとまず移植。
- この記述を入れたらawait/asyncたusing Windows.~~~ などUWP特有のコードがUnityエディタ上でエラーになる
- UNITY_UWP記述の分岐を使って上記のUWP特有の動作を逃がす
- using Windows.~~~ あたりのエラーが回避される
- await/asyncの記述がエラーは以下の記事を参考にして修正
- WebSocketへのデータ送信をMiniJSONで整備
- JSONのデコード
2D UWPアプリでまずチェックする感覚や、UNITY_UWPまわりの「UWPのコードは素直にUWPでしか動かなくする」ことが方向性として合ってるあたり、筆者の中村さんにFacebook上でも直接コメントいただいてフォローいただきました感謝いたします。
実際の動作
ということでソースコードは後に回して実際の動作です。
起動時でメッセージ
このように空間に立方体を配置されていて、こちらが動作します。
既に起動の段階でWebSocketに接続され、、、
一度メッセージが飛ばされます。
すでになんだろうこのワクワク感!
Node-REDからのWebSocketメッセージでHoloLensオブジェクトが動く
今度はNode-REDからのWebSocketメッセージでHoloLensオブジェクトが動かします。別PCのNode-REDをスマホで表示したのちNode-REDを操作してメッセージを送ってオブジェクトが動作するか確認します。
無事ランダムに動きました。魔法感...!
HoloLensの操作をWebSocketメッセージで送る
HoloLensの操作をWebSocketメッセージで送ります。オブジェクトをAirTapするとメッセージを送られます。
この動作をしてみてNode-REDをみてみます。
操作されたオブジェクトの名前や時間などが送られています!
ソースコード
若干、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
}
その他参考文献
- UWP
- iTween
おわりに
以前、IoTでSocket通信ができると楽しいという記事もありましたがSocket通信ができるとHTTP通信で苦手なサーバーからのデータ受信側の動きも作りやすくなります。
これができるとデバイスと連携したインタラクションや、サーバーでデータを料理して面白い反応をさせる可能性が広がりますね!
それでは、よきHoloLens x WebSocket x Unity Life を!