File size: 3,399 Bytes
19f07b8
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
// XrosClient.cs — WebSocket client for the XROS spatial-shell backend.
//
// Connects to the Python backend (xros_server.py) and exposes a simple API:
//   SendCommand(text)  -> run a spatial-shell command, get text output back
//   Send(msg)          -> send an arbitrary JSON message
//   OnMessage          -> event fired for every JSON message from the server
//
// Threading: the receive loop runs on a background task and pushes parsed
// messages into a thread-safe queue; Update() drains it on the main thread so
// Unity API calls (spawning GameObjects, updating UI) are always safe.
//
// Dependencies: Newtonsoft.Json (com.unity.nuget.newtonsoft-json in the
// Package Manager). No other packages required.

using System;
using System.Collections.Concurrent;
using System.Net.WebSockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Newtonsoft.Json.Linq;
using UnityEngine;

public class XrosClient : MonoBehaviour
{
    [Tooltip("Backend URL, e.g. ws://192.168.1.50:8765 (your machine's LAN IP).")]
    public string serverUrl = "ws://192.168.1.50:8765";

    public event Action<JObject> OnMessage;

    private ClientWebSocket ws;
    private CancellationTokenSource cts;
    private readonly ConcurrentQueue<JObject> incoming = new ConcurrentQueue<JObject>();

    public bool IsConnected => ws != null && ws.State == WebSocketState.Open;

    public async void Connect()
    {
        cts = new CancellationTokenSource();
        ws = new ClientWebSocket();
        try
        {
            await ws.ConnectAsync(new Uri(serverUrl), cts.Token);
            Debug.Log($"[XrosClient] connected to {serverUrl}");
            _ = ReceiveLoop();
        }
        catch (Exception e)
        {
            Debug.LogError($"[XrosClient] connect failed: {e.Message}");
        }
    }

    public async void SendCommand(string text)
    {
        await Send(new JObject { ["op"] = "command", ["text"] = text });
    }

    public async Task Send(JObject msg)
    {
        if (!IsConnected) { Debug.LogWarning("[XrosClient] not connected"); return; }
        var bytes = Encoding.UTF8.GetBytes(msg.ToString(Newtonsoft.Json.Formatting.None));
        await ws.SendAsync(new ArraySegment<byte>(bytes), WebSocketMessageType.Text, true, cts.Token);
    }

    private async Task ReceiveLoop()
    {
        var buffer = new byte[8192];
        var sb = new StringBuilder();
        try
        {
            while (ws.State == WebSocketState.Open)
            {
                sb.Clear();
                WebSocketReceiveResult result;
                do
                {
                    result = await ws.ReceiveAsync(new ArraySegment<byte>(buffer), cts.Token);
                    sb.Append(Encoding.UTF8.GetString(buffer, 0, result.Count));
                } while (!result.EndOfMessage);

                if (result.MessageType == WebSocketMessageType.Text)
                {
                    incoming.Enqueue(JObject.Parse(sb.ToString()));
                }
            }
        }
        catch (Exception e)
        {
            Debug.Log($"[XrosClient] receive loop ended: {e.Message}");
        }
    }

    private void Update()
    {
        while (incoming.TryDequeue(out var msg))
        {
            OnMessage?.Invoke(msg);
        }
    }

    private void OnDestroy()
    {
        cts?.Cancel();
        ws?.Dispose();
    }
}