HoloLens2 で MQTTnet ライブラリを使って MQTT 通信できたメモ

HoloLens2 で MQTTnet ライブラリを使って MQTT 通信できたメモです。

HoloLens 1 ではできていたが HoloLens 2 で出来てなかった

私の知見の範囲の話です。MQTT のデータのやり取り、 HoloLensとNode-REDでMQTTをやり取りするメモ でHoloLens 1 ではできていたんですが、実は HoloLens 2 で出来てなかったんです。

というのも、

  • 以前はビルド後の Visual Studio プロジェクトに Assembly-CSharp 内で M2Mqtt をパッケージマネージャーからインストールするやり方だった。若干手間はかかっていたが、うまく行っていた。
  • HoloLens2 のビルド環境が、基本 LL2CCP ベースになってできなくなってしまい、このやり方をする手順が失われて代替手段が良く分からなくなっていた(Assembly-CSharp 内で M2Mqtt をパッケージマネージャーから後乗せできない)
  • M2Mqtt の DLL ファイルを入れれば良さそうとは思っていたが、確証がなく、複合的な技術が交錯し深刻にハマりそうなので手をこまねいていた

といった状況でした。

MQTTnet ライブラリが良さそう

そんな中、こちらの記事 を見てUnity Editor でも MQTT 実行することができて DLL での設置も分かりやすそうな chkr1011/MQTTnet が良さそうだなということで調べてみたところ、

image

Supported frameworks で UWP が明記されていて、Supported platforms にも ARM があり、しかも、await/async の非同期な記述もサポートされて書きやすそうです。

ということで、これを使ってみます。

事前手順

まず、プロジェクトの初期化と最初のアプリケーションの配置 を済ませてから MQTTnet を設置します。

MQTTnet を Unity で使う – Qiita を参考に進めます

NuGet Gallery から MQTTnet 2.8.5 のパッケージをダウンロードし、展開(.nupkg -> .zip にリネームして展開しました)して .NET 4.7.2 向けの DLL ファイルを、プロジェクトにインポートします。

こちらのとおりで

image

Assets > Plugins のフォルダに MQTTnet.dll を配置します。

実際にやり取りするソースコード

適当な GameObject を用意して、実際にやり取りするソースコードを設置しました。

using System;
using System.Text;

using System.Threading.Tasks;
using System.Threading;

using MQTTnet;
using MQTTnet.Client;
using UnityEngine;
using UnityEngine.EventSystems;

public class Main : MonoBehaviour
{
    IMqttClient mqttClient;

    private SynchronizationContext context;

    async void Start()
    {
        Debug.Log("Start");

        // Unity Editor でメインスレッドに戻れるよう SynchronizationContext.Current 記録
        context = SynchronizationContext.Current;

        var factory = new MqttFactory();
        mqttClient = factory.CreateMqttClient();

        var options = new MqttClientOptionsBuilder()
            .WithTcpServer("<MQTT Server>")
            // .WithCredentials("ID", "PASS") // MQTT に ID/PASS が必要な場合は WithCredentials 記述を
            .Build();

        mqttClient.Connected += (s, e) =>
        {
             LogMessage("mqttClient.Connected");
        };

        mqttClient.Disconnected += async (s, e) =>
        {

            LogMessage("mqttClient.Disconnected");

            if (e.Exception == null)
            {
                LogMessage("Exception Disconnected");

                return;
            }

            UnityEngine.WSA.Application.InvokeOnAppThread(() => {
                LogMessage("Unexception disconnected");
            }, true);

            await Task.Delay(TimeSpan.FromSeconds(5));

            try
            {
                await mqttClient.ConnectAsync(options);
            }
            catch
            {
                LogMessage("Reconnect failed.");
            }
        };

        // メッセージ受信時の処理
        mqttClient.ApplicationMessageReceived += (s, e) =>
        {
            var stringBuilder = new StringBuilder();
            stringBuilder.AppendLine("ApplicationMessageReceived");
            stringBuilder.AppendLine($"Topic = {e.ApplicationMessage.Topic}");
            stringBuilder.AppendLine($"Payload = {Encoding.UTF8.GetString(e.ApplicationMessage.Payload)}");
            stringBuilder.AppendLine($"QoS = {e.ApplicationMessage.QualityOfServiceLevel}");
            stringBuilder.AppendLine($"Retain = {e.ApplicationMessage.Retain}");
            LogMessage(stringBuilder);
        };

        // 接続
        await mqttClient.ConnectAsync(options);

        // メッセージ受信する購読 subscribe
        await mqttClient.SubscribeAsync(new TopicFilterBuilder().WithTopic("hololens2/firststep").Build());

        // メッセージの送信するための設定
        var message = new MqttApplicationMessageBuilder()
            .WithTopic("hololens2/firststep")
            .WithPayload("Hello HoloLens2")
            .WithExactlyOnceQoS()
            .Build();
        // メッセージの送信 publish
        await mqttClient.PublishAsync(message);
    }

    async void LogMessage(object message)
    {
#if WINDOWS_UWP

        await Task.Run(() =>
        {
            // メインスレッドに戻す UWP の場合
            UnityEngine.WSA.Application.InvokeOnAppThread(() =>
            {
                Debug.Log(message);

                // GameObjectなどで描画するならこの中に処理を書く UWP の場合

            }, true);
        });
#elif UNITY_EDITOR
        await Task.Run(() =>
        {

            SynchronizationContext.SetSynchronizationContext(context);

            // メインスレッドに戻す Unty Editor の場合
            context.Post((state) =>
            {
                // 単純にログに出すだけならメインスレッドに戻さず書ける
                Debug.Log(message);

                // GameObjectなどで描画するならこの中に処理を書く

            }, null);
        });
#endif
    }
}

MQTT サーバーの設定

つなげたい MQTT サーバーのアドレスを記述しましょう。

        var options = new MqttClientOptionsBuilder()
            .WithTcpServer("<MQTT Server>")
            // .WithCredentials("ID", "PASS") // MQTT に ID/PASS が必要な場合は WithCredentials 記述を
            .Build();

パスワードなしならWithTcpServerだけでOKです。もし、MQTT サーバーとつなぐときに ID/PASS が必要な場合は WithCredentials 記述を追加します。

購読 subscribe 時の処理

MQTTnet で購読 subscribe して、データを受信するときは非同期になっています。この状態で、GameObjectといった表示系のオブジェクトを触ってしまうとエラーになります。なので、メインスレッドに処理を戻してから表示系の処理をする必要があります。

具体的には UnityEngine.WSA.Application.InvokeOnAppThread では HoloLens 2 のとき、SynchronizationContext.SetSynchronizationContext(context);context.Post((state) => では Unity Editor のときにメインスレッドに処理を戻しています。

実をいうと Unity Editor で実行し Debug.Log を使うだけなら非同期を気にする必要はないので良いのですが、制作を進めれば何かしら動かすようになってくるので、今回のサンプルにも記載しています。

私の場合ですが、HoloLens2 で実行するときには素直にログが出せないので Application.logMessageReceived += logMessageReceived; で全ログ出力をイベントで引っ掛けてテキストエリアに出力してデバッグを行っています。この場合、いきなりこのメインスレッドに戻す処理が必要になってきます。また、その際にテキストエリアが TMPro なので日本語表示に手間がかかるので、ログ内容を英語オンリーにしています。

非同期でメインスレッドに戻す処理に関しては、以前の記事でまとめてあるので気になる方は見てみてください。

ということで、もっとシンプルにログだけ出す検証は出来るけれど、表示系を見据えて非同期処理の対応をしているものになります。

動かしてみる

まず、Unity Editor で実行してみると hololens2/firststep というトピックに接続直後に Hello HoloLens2 という文字列データを送ります。そして、この hololens2/firststep は購読もしているので、

image

このように、ApplicationMessageReceived というコンソールのメッセージとともに受信内容が表示されます。

image

私の場合、HoloLens 2 のなかに、テキストエリア付つきのデバッグログを表示しているので、このように起動直後に同様にメッセージが表示されています。

このあたりは皆さんのつくられているコンテンツで変わってくるので、それぞれの方法でチェックしてみてください。