MRが楽しい

MRやVRについて学習したことを書き残す

AzureのSignalRServiceを使ってサーバからHoloLens2にデータを送信する その5(HoloLens2 用クライアントアプリの作成)

本日は Azure と HoloLens2 の技術調査枠です。
Azure の SignalRService を使ってサーバから HoloLens2 にデータを送信する方法を試したので作業記録を記事にします。
f:id:bluebirdofoz:20210629025910j:plain

前回記事の続きです。
bluebirdofoz.hatenablog.com

HoloLens2用クライアントアプリの作成

SignalR Service からデータを受信する HoloLens2 用クライアントアプリを作成します。

プロジェクトの作成

最初に以下の記事などを参考に、MRTK を用いて HoloLens2 用の基本シーンを構成します。
bluebirdofoz.hatenablog.com
f:id:bluebirdofoz:20210629025939j:plain

今回は XR パイプラインに XRSDK を利用しています。
f:id:bluebirdofoz:20210629025949j:plain

依存パッケージのインポート

Microsoft.AspNetCore.SignalR.Client の参照を解決するため、NuGetForUnity を利用して依存パッケージをインポートします。
NuGetForUnity の使い方は以下の記事などを参考にしてください。
bluebirdofoz.hatenablog.com
f:id:bluebirdofoz:20210629030005j:plain

Microsoft.AspNetCore.SignalR.Client を検索して[Install]を実行します。
f:id:bluebirdofoz:20210629030015j:plain

参照エラーの解消

筆者環境ではパッケージをインポートすると、参照ライブラリのバージョンが異なることを示す以下のエラーメッセージが発生しました。

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".

f:id:bluebirdofoz:20210629030305j:plain

UWP プラットフォームを指定している場合、"Assembly Version Validation"の設定を無効化することはできません。
f:id:bluebirdofoz:20210629030315j:plain

このエラーメッセージは全ての DLL を同じ階層のディレクトリに配置することで解消できるので、今回はこの方法でエラーを解消しました。
f:id:bluebirdofoz:20210629030336j:plain

バイトコードストリップの対処

また、本プロジェクトをこのままビルドした場合、バイトコードストリップの機能によって 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.

f:id:bluebirdofoz:20210629030512j:plain

この問題は 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>

f:id:bluebirdofoz:20210629030600j:plain

必要な型の抽出は以下の 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());
        }
    }
}

f:id:bluebirdofoz:20210629030813j:plain

シーンに結果表示用の Text オブジェクトと、Connect/Disconnect 用のボタンを配置します。
f:id:bluebirdofoz:20210629030825j:plain

シーンにスクリプトを配置し、Text オブジェクトへの参照を設定します。
f:id:bluebirdofoz:20210629030838j:plain

2つのボタンにはそれぞれ接続/切断処理の関数を実行する OnClick イベントを設定しておきます。
f:id:bluebirdofoz:20210629030850j:plain

HoloLens2へのインストールと動作確認

以下の記事などを参考に、作成したアプリを HoloLens2 にインストールします。
bluebirdofoz.hatenablog.com

インストールが完了したら、HoloLens2 上でアプリを起動します。
初期状態では Text オブジェクトにデフォルトのテキストメッセージが表示されます。
f:id:bluebirdofoz:20210629030933j:plain

接続処理を実行するボタンをクリックすると SignalR Service への接続が行われ、成功すると[Connected!]の文字が表示されます。
もし、前述のバイトコードストリッピングの問題が正しく行われていない場合はここで接続に成功しません。
f:id:bluebirdofoz:20210629030945j:plain

接続に成功したら、SignalR Service からデータを送信してみます。
前回記事で作成した PC 側のクライアントアプリを起動し、SignalR Service に接続して[broadcast]を実行します。
f:id:bluebirdofoz:20210629031009j:plain

HoloLens2 側で SignalR Service を通し、送信された時刻情報の文字列を受信できれば成功です。
f:id:bluebirdofoz:20210629031022j:plain