本日は Azure と HoloLens2 の技術調査枠です。
Azure の SignalRService を使ってサーバから HoloLens2 にデータを送信する方法を試したので作業記録を記事にします。
前回記事の続きです。
bluebirdofoz.hatenablog.com
HoloLens2用クライアントアプリの作成
SignalR Service からデータを受信する HoloLens2 用クライアントアプリを作成します。
プロジェクトの作成
最初に以下の記事などを参考に、MRTK を用いて HoloLens2 用の基本シーンを構成します。
bluebirdofoz.hatenablog.com
今回は XR パイプラインに XRSDK を利用しています。
依存パッケージのインポート
Microsoft.AspNetCore.SignalR.Client の参照を解決するため、NuGetForUnity を利用して依存パッケージをインポートします。
NuGetForUnity の使い方は以下の記事などを参考にしてください。
bluebirdofoz.hatenablog.com
Microsoft.AspNetCore.SignalR.Client を検索して[Install]を実行します。
参照エラーの解消
筆者環境ではパッケージをインポートすると、参照ライブラリのバージョンが異なることを示す以下のエラーメッセージが発生しました。
Assembly references: X.X.X.X Found in project: Y.Y.Y.Y. Assembly Version Validation can be disabled in Player Settings "Assembly Version Validation".
UWP プラットフォームを指定している場合、"Assembly Version Validation"の設定を無効化することはできません。
このエラーメッセージは全ての DLL を同じ階層のディレクトリに配置することで解消できるので、今回はこの方法でエラーを解消しました。
バイトコードストリップの対処
また、本プロジェクトをこのままビルドした場合、バイトコードストリップの機能によって Microsoft.AspNetCore.SignalR 内の型が展開されない問題が発生します。
この問題が発生すると、ビルドは正常に通るものの HoloLens2 上では特定の型が利用できないエラーが発生して SignalR の接続が行えません。
System.InvalidOperationException: A suitable constructor for type 'Mycrosoft.AspNetCore.Http.Connections.Client.HttpConnectionFactory' could not be located. Ensure the type is concrete and services are registered for all parametaers of a public constructor.
この問題は Assets フォルダ内に link.xml ファイルを作成し、指定の型へのバイトコードストリップを無効化することで解消できます。
docs.unity3d.com
今回は以下の link.xml ファイルを作成し、DLL のフォルダに配置しました。
・link.xml
<linker> <assembly fullname="Microsoft.AspNetCore.Connections.Abstractions" preserve="all"/> <assembly fullname="Microsoft.AspNetCore.Http.Features" preserve="all"/> <assembly fullname="Microsoft.AspNetCore.SignalR.Common" preserve="all"/> <assembly fullname="Microsoft.AspNetCore.SignalR.Protocol.MessagePackHubProtocol" preserve="all"/> <assembly fullname="Microsoft.Extensions.DependencyInjection.Abstractions" preserve="all"/> <assembly fullname="Microsoft.Extensions.Options" preserve="all"/> <assembly fullname="Microsoft.Extensions.Primitives" preserve="all"/> <assembly fullname="Microsoft.Bcl.AsyncInterfaces" preserve="all"/> <assembly fullname="Microsoft.AspNetCore.Http.Connections.Client" preserve="all"/> <assembly fullname="Microsoft.AspNetCore.Http.Connections.Common" preserve="all"/> <assembly fullname="Microsoft.AspNetCore.SignalR.Client.Core" preserve="all"/> <assembly fullname="Microsoft.AspNetCore.SignalR.Client" preserve="all"/> <assembly fullname="Microsoft.AspNetCore.SignalR.Protocols.Json" preserve="all"/> <assembly fullname="Microsoft.Extensions.Configuration.Abstractions" preserve="all"/> <assembly fullname="Microsoft.Extensions.Configuration.Binder" preserve="all"/> <assembly fullname="Microsoft.Extensions.Configuration" preserve="all"/> <assembly fullname="Microsoft.Extensions.DependencyInjection" preserve="all"/> <assembly fullname="Microsoft.Extensions.Logging.Abstractions" preserve="all"/> <assembly fullname="Microsoft.Extensions.Logging" preserve="all"/> <assembly fullname="Microsoft.AspNetCore.SignalR.Protocols.NewtonsoftJson" preserve="all"/> </linker>
必要な型の抽出は以下の Digital Twin のプロジェクト内の link.xml を参考にしました。
bluebirdofoz.hatenablog.com
Microsoft.AspNetCore.SignalR でバイトコードストリップの問題が発生する理由については以下の記事が詳しいです。
blog.xin9le.net
以下、上記記事からの抜粋です。
IL2CPP ビルドには バイトコードストリップ という大きな特徴があります。 要は静的構文解析の結果として利用されていない型は C++ コードとして展開されないというものです。 ・明示的に型を利用しない限り消える ・リフレクション経由でインスタンス化されているものは型を「利用していない」判定される (中略) ASP.NET Core SignalR でバイトコードストリップが発生するのかと言うと、ASP.NET Core の内部で DI (= Dependency Injection) が利用されているためです。 つまりリフレクション経由でインスタンス生成をしているからなのですが、これが IL2CPP と非常に相性が悪いです。
サンプルシーンの作成
必要なパッケージをインポートしたのでシーンを構成します。
SignalR Service に接続してデータを受信し、Text コンポーネントに結果を表示する以下のスクリプトを作成しました。
・HoloSideSignalRConnection.cs
using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.UI; using System; using System.Net.Http; using Microsoft.AspNetCore.SignalR.Client; public class HoloSideSignalRConnection : MonoBehaviour { /// <summary> /// 表示UIテキスト /// </summary> [SerializeField, Tooltip("表示UIテキスト")] private Text UIText; private const string RootUrl = "https://xxxxxxxxxxxxxxxxxxxx.azurewebsites.net"; // 作った Azure Functions の URL private const string FuctionKey = "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"; // 作った Azure Functions の関数 Key private HubConnection Connection; public string ShowTextMessage = ""; private bool changeText = false; /// <summary> /// 起動時処理 /// </summary> void Start() { try { // negotiate 関数の URL と headers 情報を設定する this.Connection = new HubConnectionBuilder().WithUrl($"{RootUrl}/api", options => { options.Headers["x-functions-key"] = FuctionKey; }).Build(); // Azure SignalR Service から Push されてきたメッセージを受信する Connection.On<string>("Receive", data => { // 表示テキストとして保持する ShowTextMessage = data; changeText = true; }); } catch (Exception ex) { Debug.LogError(ex.ToString()); } } /// <summary> /// 定期処理 /// </summary> private void Update() { if (changeText) { // メインスレッドでテキストをシーンに反映する UIText.text = ShowTextMessage; changeText = false; } } /// <summary> /// 接続処理 /// </summary> public async void OnConnectClick() { Debug.Log("OnConnectClick"); try { await Connection.StartAsync(); // '/negotiate' から接続情報を取得して接続 ShowTextMessage = "Connected!"; changeText = true; } catch (Exception ex) { Debug.LogError(ex.ToString()); } } /// <summary> /// 切断処理 /// </summary> public async void OnDisconnectClick() { Debug.Log("OnDisconnectClick"); try { await Connection.StopAsync(); // 切断 ShowTextMessage = "Disconnected!"; changeText = true; } catch (Exception ex) { Debug.LogError(ex.ToString()); } } }
シーンに結果表示用の Text オブジェクトと、Connect/Disconnect 用のボタンを配置します。
シーンにスクリプトを配置し、Text オブジェクトへの参照を設定します。
2つのボタンにはそれぞれ接続/切断処理の関数を実行する OnClick イベントを設定しておきます。
HoloLens2へのインストールと動作確認
以下の記事などを参考に、作成したアプリを HoloLens2 にインストールします。
bluebirdofoz.hatenablog.com
インストールが完了したら、HoloLens2 上でアプリを起動します。
初期状態では Text オブジェクトにデフォルトのテキストメッセージが表示されます。
接続処理を実行するボタンをクリックすると SignalR Service への接続が行われ、成功すると[Connected!]の文字が表示されます。
もし、前述のバイトコードストリッピングの問題が正しく行われていない場合はここで接続に成功しません。
接続に成功したら、SignalR Service からデータを送信してみます。
前回記事で作成した PC 側のクライアントアプリを起動し、SignalR Service に接続して[broadcast]を実行します。
HoloLens2 側で SignalR Service を通し、送信された時刻情報の文字列を受信できれば成功です。