using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.SignalR.Client; using RobotApp.Common.Shares; using RobotApp.VDA5050.State; using System.Collections.Concurrent; using System.Text.Json; namespace RobotApp.Client.Services; public class RobotStateClient : IAsyncDisposable { private readonly NavigationManager _nav; private HubConnection? _connection; private bool _started; private readonly object _lock = new(); public ConcurrentDictionary LatestStates { get; } = new(); public event Action? OnStateReceived; public event Action? OnStateReceivedAny; public RobotStateClient(NavigationManager nav) { _nav = nav; } public async Task StartAsync(string hubPath = "/hubs/robot") { lock (_lock) { if (_started) return; _started = true; } if (string.IsNullOrWhiteSpace(hubPath)) throw new ArgumentException("Hub path is empty", nameof(hubPath)); var hubUri = _nav.ToAbsoluteUri(hubPath); Console.WriteLine($"[SIGNALR] Connecting to {hubUri}"); _connection = new HubConnectionBuilder() .WithUrl(hubUri) .WithAutomaticReconnect(new[] { TimeSpan.Zero, TimeSpan.FromSeconds(2), TimeSpan.FromSeconds(10) }) .Build(); _connection.Closed += async error => { Console.WriteLine($"[SIGNALR] Connection closed: {error?.Message}"); _started = false; await Task.Delay(3000); if (_connection != null) { try { await StartAsync(hubPath); } catch { } } }; _connection.Reconnecting += error => { Console.WriteLine($"[SIGNALR] Reconnecting... {error?.Message}"); return Task.CompletedTask; }; _connection.Reconnected += connectionId => { Console.WriteLine($"[SIGNALR] Reconnected: {connectionId}"); return Task.CompletedTask; }; _connection.On("ReceiveState", stateJson => { try { var state = JsonSerializer.Deserialize( stateJson, JsonOptionExtends.Read ); if (state?.SerialNumber == null) return; LatestStates[state.SerialNumber] = state; OnStateReceived?.Invoke(state.SerialNumber, state); OnStateReceivedAny?.Invoke(state); Console.WriteLine( $"[CLIENT] {state.SerialNumber} | " + $"X={state.AgvPosition?.X:F2}, " + $"Y={state.AgvPosition?.Y:F2}, " + $"Battery={state.BatteryState?.BatteryCharge:F1}%" ); } catch (Exception ex) { Console.WriteLine($"[CLIENT] Deserialize error: {ex.Message}"); } }); try { await _connection.StartAsync(); Console.WriteLine("[SIGNALR] Connected successfully!"); } catch (Exception ex) { _started = false; Console.WriteLine($"❌ [SIGNALR] Connection failed: {ex.Message}"); } } public async Task SubscribeRobotAsync(string serialNumber) { if (_connection?.State != HubConnectionState.Connected) throw new InvalidOperationException("SignalR is not connected"); await _connection.InvokeAsync("JoinRobot", serialNumber); Console.WriteLine($"[SIGNALR] Subscribed to {serialNumber}"); } public async Task UnsubscribeRobotAsync(string serialNumber) { if (_connection?.State == HubConnectionState.Connected) { await _connection.InvokeAsync("LeaveRobot", serialNumber); } LatestStates.TryRemove(serialNumber, out _); Console.WriteLine($"[SIGNALR] Unsubscribed from {serialNumber}"); } public StateMsg? GetLatestState(string serialNumber) { LatestStates.TryGetValue(serialNumber, out var state); return state; } public async ValueTask DisposeAsync() { _started = false; if (_connection != null) { await _connection.DisposeAsync(); _connection = null; Console.WriteLine("[SIGNALR] Disposed"); } } }