// 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 OnMessage; private ClientWebSocket ws; private CancellationTokenSource cts; private readonly ConcurrentQueue incoming = new ConcurrentQueue(); 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(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(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(); } }