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; // ================= CONNECTION STATE ================= public enum RobotClientState { Disconnected, Connecting, Connected, Reconnecting } // ================= CLIENT ================= public sealed class RobotStateClient : IAsyncDisposable { private readonly NavigationManager _nav; private HubConnection? _connection; private readonly object _lock = new(); private bool _started; public ConcurrentDictionary LatestStates { get; } = new(); // ================= EVENTS ================= public event Action? OnStateReceived; public event Action? OnStateReceivedAny; public event Action? OnConnectionStateChanged; public RobotClientState ConnectionState { get; private set; } = RobotClientState.Disconnected; // ================= CTOR ================= public RobotStateClient(NavigationManager nav) { _nav = nav; } // ================= STATE HELPER ================= private void SetState(RobotClientState state) { if (ConnectionState == state) return; ConnectionState = state; OnConnectionStateChanged?.Invoke(state); } // ================= START ================= public async Task StartAsync(string hubPath = "/hubs/robot") { lock (_lock) { if (_started) return; _started = true; } SetState(RobotClientState.Connecting); _connection = new HubConnectionBuilder() .WithUrl(_nav.ToAbsoluteUri(hubPath)) .WithAutomaticReconnect(new[] { TimeSpan.Zero, TimeSpan.FromSeconds(2), TimeSpan.FromSeconds(10) }) .Build(); _connection.Reconnecting += _ => { SetState(RobotClientState.Reconnecting); return Task.CompletedTask; }; _connection.Reconnected += _ => { SetState(RobotClientState.Connected); return Task.CompletedTask; }; _connection.Closed += async error => { _started = false; if (_connection != null) { try { await StartAsync(hubPath); } catch { } } }; _connection.On("ReceiveState", HandleState); try { await _connection.StartAsync(); SetState(RobotClientState.Connected); } catch { _started = false; SetState(RobotClientState.Disconnected); } } // ================= HANDLE STATE ================= private void HandleState(string stateJson) { StateMsg? state; try { state = JsonSerializer.Deserialize( stateJson, JsonOptionExtends.Read ); } catch { return; } if (state?.SerialNumber == null) return; LatestStates[state.SerialNumber] = state; OnStateReceived?.Invoke(state.SerialNumber, state); OnStateReceivedAny?.Invoke(state); } // ================= SUBSCRIBE ================= public async Task SubscribeRobotAsync(string serialNumber) { if (_connection?.State != HubConnectionState.Connected) return; try { await _connection.InvokeAsync("JoinRobot", serialNumber); } catch { // ignore – reconnect sẽ tự join lại } } public async Task UnsubscribeRobotAsync(string serialNumber) { if (_connection?.State == HubConnectionState.Connected) { try { await _connection.InvokeAsync("LeaveRobot", serialNumber); } catch { } } LatestStates.TryRemove(serialNumber, out _); } // ================= GET CACHE ================= public StateMsg? GetLatestState(string serialNumber) { LatestStates.TryGetValue(serialNumber, out var state); return state; } // ================= DISPOSE ================= public async ValueTask DisposeAsync() { _started = false; SetState(RobotClientState.Disconnected); if (_connection != null) { await _connection.DisposeAsync(); _connection = null; } } }