夜風のMixedReality

xRと出会って変わった人生と出会った技術を書き残すためのGeekなHoloRangerの居場所

HoloLensのイベントをiPhoneからリモートで実行する HoloLens アドベントカレンダー4日目

本日はHoloLens枠です。

〇HoloLensアドベントカレンダー2023とは?

HoloLensアドベントカレンダーとは毎年12月に私の師である、がちもとさんを中心に開催されているアドベントカレンダー企画になります。

qiita.com

iPhoneからHoloLensアプリの関数を実行する

1日目はDebugLogを飛ばしました。

redhologerbera.hatenablog.com

本来であれば一方的にHoloLensからiPhoneへデータを飛ばしているだけなのでUDPをしようするほうがよさそうですが前回はTCPプロトコルを使用していました。

せっかくなので今回はiPhoneからHoloLensの操作を行っていきます。

今回は例としてiPhone側でボタンを押したら対応するUnityEventが発火するということをやっていきます。

昨日のコードではClient側からServer側にデータを送信していました。

今回はTCPプロトコルを採用しているため2つのデバイスの接続が確立した場合Server側のデバイスからClient側のデバイスにデータを送信することも可能です。

これを行うためにServer.csに次のメソッドを追加します。

   public void SendDataToClient(string message)
    {
        try
        {
            if (_connectedTcpClient != null)
            {
                NetworkStream clientStream = _connectedTcpClient.GetStream();
                byte[] messageBytes = Encoding.ASCII.GetBytes(message);//送信するデータをバイトに変換

                // Add header and send data
                byte[] dataToSend = new byte[messageBytes.Length + 6];
                Encoding.ASCII.GetBytes("FUN").CopyTo(dataToSend, 0);
                messageBytes.CopyTo(dataToSend, 3);

                clientStream.Write(dataToSend, 0, dataToSend.Length);
                Debug.Log($"Server sent message to client: {message}");
            }
            else
            {
                Debug.LogWarning("No connected client to send data to.");
            }
        }
        catch (Exception e)
        {
            Debug.LogError($"Error sending data from server: {e.Message}");
        }
    }

ここで引数に渡したString型のデータがClient側に送信される形になります。

Client側でFUNのヘッダーを検知してUnityEventsを発火させるには次のように実装します。

using UnityEngine.Events;
・・・

public UnityEvent onReceivedEvent;

    private void ProcessReceivedData(byte[] data, int length)
    {

        string header = Encoding.ASCII.GetString(data, 0, 3);
        // Only the part related to processing received data has been kept
        if (header == "LOG")
        {
            // Extract the log message from the received data
            string logMessage = Encoding.ASCII.GetString(data, 13, length - 13);
        }
        if(header == "FUN")
        {
            string functionMessage = Encoding.ASCII.GetString(data, 13, length - 13);

            MainThreadDispatcher.Enqueue(() => _access = true);
            MainThreadDispatcher.Execute();

        }
        else
        {
            Debug.Log($"Received unknown data with header: {header}");
        }
    }

以上でコア部分の実装が完了しました。

今回のアプローチとしてはメッセージをClientからDebugLogを送信したのと同じようにServer側からメッセージを送信し、メッセージの内容で任意の処理を実行するという感じになります。

注意点としてサブスレッドからメインスレッドの処理へアクセスすることはできません。

このため今回はbool値を変更し、Update関数で検知してメインスレッドの処理に落とし込むアプローチをとりました。(もっと良い方法ありそうですが…)

この際にはMainThredDispatcherを使用しています。

   MainThreadDispatcher.Enqueue(() => _access = true);

MainThreadDispatcherクラスは今回は以下のような性的クラスを作成しています。

これは前述の通りサブスレッドからメインスレッドの処理にアクセスできず、場合によってはクラッシュしてしまうので良しなにするためのものです。

public static class MainThreadDispatcher
{
    private static Queue<Action> _actions = new Queue<Action>();
    private static readonly object _lockObject = new object();

    public static void Enqueue(Action action)
    {
        lock (_lockObject)
        {
            _actions.Enqueue(action);
        }
    }

    public static void Execute()
    {
        lock (_lockObject)
        {
            while (_actions.Count > 0)
            {
                Action action = _actions.Dequeue();
                action?.Invoke();
            }
        }
    }
}

lockステートメントはその名の通り、ロックが保持されている間はほかスレッドからブロックされ、ロックが解放されるまでの間待機します。

これによってスレッドアクセスを同期するために使用されています。

このアクセスの仕組みの影響で筆者自身が未熟であるだけなのですが何度も実機でクラッシュさせてしまいました。

〇実機で確認

前回同様iPhoneのデザリングの機能を使用して同一のネットワークに接続しています。

iPhone側でボタンを押すとHoloLens側のCubeが表示/日表示されることが確認できました。

www.youtube.com

〇コード全文

〇サーバー側
using System;
using System.Collections;
using System.Collections.Generic;
using System.Net;
using System.Net.Sockets;
using System.Text;
using TMPro;
using UnityEngine;
using System.Threading;

public class Server : MonoBehaviour
{
    private TcpListener _tcpListener;
    private Thread _tcpListenerThread;
    private TcpClient _connectedTcpClient;
    public TextMeshProUGUI logText;

    private readonly Queue<string> _logQueue = new Queue<string>();
    private readonly object _lockObject = new object();

    [Tooltip("Port number")] public int _port = 9991; // Default is 9991

    private void Start()
    {
        _tcpListenerThread = new Thread(new ThreadStart(ListenForIncomingRequests));
        _tcpListenerThread.IsBackground = true;
        _tcpListenerThread.Start();

        // Start a coroutine to process logs from the main thread
        StartCoroutine(ProcessLogQueue());
    }

    private void Update()
    {
        // Process logs in the main thread
        MainThreadDispatcher.Execute();
    }

    private void ListenForIncomingRequests()
    {
        try
        {
            _tcpListener = new TcpListener(IPAddress.Any, _port);
            _tcpListener.Start();

            Debug.Log($"Server is listening on port {_port}");

            while (true)
            {
                _connectedTcpClient = _tcpListener.AcceptTcpClient();

                Thread clientThread = new Thread(new ParameterizedThreadStart(HandleClientComm));
                clientThread.Start(_connectedTcpClient);
            }
        }
        catch (Exception e)
        {
            Debug.LogError($"Error: {e.Message}");
        }
    }

    private void HandleClientComm(object clientObj)
    {
        TcpClient tcpClient = (TcpClient)clientObj;
        NetworkStream clientStream = tcpClient.GetStream();

        byte[] message = new byte[4096];
        int bytesRead;

        while (true)
        {
            bytesRead = 0;

            try
            {
                bytesRead = clientStream.Read(message, 0, 4096);
            }
            catch (Exception e)
            {
                Debug.LogError($"Error reading from client: {e.Message}");
                break;
            }

            if (bytesRead == 0)
                break;

            ProcessReceivedData(message, bytesRead);
        }

        tcpClient.Close();
    }

    private void ProcessReceivedData(byte[] data, int length)
    {
        string header = Encoding.ASCII.GetString(data, 0, 3);
        Debug.Log($"Received header: {header}");

        if (header == "LOG")
        {
            string logMessage = Encoding.ASCII.GetString(data, 3, length - 3);
            Debug.Log($"Received log message: {logMessage}");

            // Add log message to the queue for processing in the main thread
            lock (_lockObject)
            {
                _logQueue.Enqueue(logMessage);
            }
        }
        else
        {
            Debug.Log($"Received unknown data with header: {header}");
        }
    }

    public void SendDataToClient(string message)
    {
        try
        {
            if (_connectedTcpClient != null)
            {
                NetworkStream clientStream = _connectedTcpClient.GetStream();
                byte[] messageBytes = Encoding.ASCII.GetBytes(message);

                // Add header and send data
                byte[] dataToSend = new byte[messageBytes.Length + 6];
                Encoding.ASCII.GetBytes("FUN").CopyTo(dataToSend, 0);
                messageBytes.CopyTo(dataToSend, 3);

                clientStream.Write(dataToSend, 0, dataToSend.Length);
                Debug.Log($"Server sent message to client: {message}");
            }
            else
            {
                Debug.LogWarning("No connected client to send data to.");
            }
        }
        catch (Exception e)
        {
            Debug.LogError($"Error sending data from server: {e.Message}");
        }
    }

    private IEnumerator ProcessLogQueue()
    {
        while (true)
        {
            yield return null; // Wait for the next frame

            // Process logs in the main thread
            lock (_lockObject)
            {
                while (_logQueue.Count > 0)
                {
                    string logMessage = _logQueue.Dequeue();
                    AppendLogText(logMessage);
                }
            }
        }
    }

    private void AppendLogText(string message)
    {
        // Append the new log message to the existing log text using StringBuilder
        StringBuilder sb = new StringBuilder(logText.text);
        sb.AppendLine(message);
        logText.text = sb.ToString();
    }

    private void OnDestroy()
    {
        if (_tcpListener != null)
        {
            _tcpListener.Stop();
        }
    }
}

// Helper class to dispatch actions to the main thread
public static class MainThreadDispatcher
{
    private static Queue<Action> _actions = new Queue<Action>();
    private static readonly object _lockObject = new object();

    public static void Enqueue(Action action)
    {
        lock (_lockObject)
        {
            _actions.Enqueue(action);
        }
    }

    public static void Execute()
    {
        lock (_lockObject)
        {
            while (_actions.Count > 0)
            {
                Action action = _actions.Dequeue();
                action?.Invoke();
            }
        }
    }
}
〇クライアント側
using System;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using JetBrains.Annotations;
using UnityEditor;
using UnityEngine;
using UnityEngine.Events;




public class Client : MonoBehaviour
{
    [CanBeNull] private TcpClient _client;
    private NetworkStream _stream;

    [Tooltip("Port number")] public int _port = 9991; // Default is 9991
    [Tooltip("Address of the server")] public string _ipAddress = "localhost";
    MultiPlayEventManager multiPlayEvent;
    bool _access = false;

    public GameObject gameObject;
    public void StartConnection()
    {
        multiPlayEvent = this.gameObject.GetComponent<MultiPlayEventManager>();
        _client = new TcpClient(_ipAddress, _port);
        _stream = _client.GetStream();

        // Start a new thread to listen for incoming messages
        new Thread(() =>
        {
            var responseBytes = new byte[536870912];
            try
            {
                while (true)
                {
                    var bytesRead = _stream.Read(responseBytes, 0, responseBytes.Length);
                    if (bytesRead == 0) break;

                    // Process received data based on header
                    ProcessReceivedData(responseBytes, bytesRead);
                }
            }
            catch (Exception ex)
            {
                Debug.LogError($"Exception in StartConnection thread: {ex.Message}");
                // Handle the exception as needed
            }
            finally
            {
                // Clean up resources if necessary
                _client.Close();
            }
        }).Start();
    }

    private void ProcessReceivedData(byte[] data, int length)
    {

        string header = Encoding.ASCII.GetString(data, 0, 3);
        Debug.Log("62==="+header);
        Debug.Log("test");
        // Only the part related to processing received data has been kept
        if (header == "LOG")
        {
            // Extract the log message from the received data
            string logMessage = Encoding.ASCII.GetString(data, 13, length - 13);
            Debug.Log($"Received log message: {logMessage}");
        }
        if(header == "FUN")
        {
            string functionMessage = Encoding.ASCII.GetString(data, 13, length - 13);

            Debug.Log("functionMessage");

            MainThreadDispatcher.Enqueue(() => _access = true);
            MainThreadDispatcher.Execute();

        }
        else
        {
            Debug.Log($"Received unknown data with header: {header}");
        }
    }

    void Start()
    {
        StartConnection();
    }
    private readonly object _lockObject = new object();
    void Update()
    {
        if (_access)
        {
            Debug.Log("OKKKK");
            lock (_lockObject)
            {
                _access = false;
            }
            gameObject.SetActive(!gameObject.activeSelf);
            Debug.Log("OKKKK!!!!!!!");
            //MainThreadDispatcher.Enqueue(() => multiPlayEvent.isReceiveEvent = true);
        }
    }

    public void RequestDebugLog(string logMessage)
    {
        byte[] logBytes = Encoding.ASCII.GetBytes(logMessage);
        byte[] dataToSend = new byte[logBytes.Length + 13];
        Encoding.ASCII.GetBytes("LOG").CopyTo(dataToSend, 0);
        logBytes.CopyTo(dataToSend, 3); // 修正: 3からコピーするように変更
        _stream.Write(dataToSend, 0, dataToSend.Length);
    }

    private void OnDestroy()
    {
        // Unity エディターモードでのみ実行されるコード
#if UNITY_EDITOR
        if (!EditorApplication.isPlayingOrWillChangePlaymode)
        {
            // エディターモードのクリーンアップ処理
             EditorApplication.ExitPlaymode();
            return;
        }
#endif

        // 実行モード時のクリーンアップ処理
        _stream.Close();
        _client.Close();
    }
}