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; // ================= SIGNALR CONNECTION STATE ================= public enum RobotClientState { Disconnected, Connecting, Connected, Reconnecting } // ================= ROBOT STATE CLIENT ================= public sealed class RobotStateClient : IAsyncDisposable { private readonly NavigationManager _nav; private HubConnection? _connection; private readonly object _lock = new(); private bool _started; // ================= STATE CACHE ================= public ConcurrentDictionary LatestStates { get; } = new(); // ================= ROBOT CONNECTION ================= private bool _isRobotConnected; public bool IsRobotConnected => _isRobotConnected; // ================= EVENTS ================= public event Action? OnStateReceived; public event Action? OnStateReceivedAny; public event Action? OnRobotConnectionChanged; 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 _ => { _started = false; SetState(RobotClientState.Disconnected); if (_connection != null) { try { await StartAsync(hubPath); } catch { } } }; // ================= SIGNALR HANDLERS ================= // VDA5050 State _connection.On("ReceiveState", HandleState); // Robot connection (bool only) _connection.On("ReceiveRobotConnection", HandleRobotConnection); try { await _connection.StartAsync(); SetState(RobotClientState.Connected); } catch { _started = false; SetState(RobotClientState.Disconnected); } } // ================= HANDLE STATE ================= private void HandleState(StateMsg state) { if (state?.SerialNumber == null) return; LatestStates[state.SerialNumber] = state; OnStateReceived?.Invoke(state.SerialNumber, state); OnStateReceivedAny?.Invoke(state); } // ================= HANDLE ROBOT CONNECTION ================= private void HandleRobotConnection(bool isConnected) { _isRobotConnected = isConnected; OnRobotConnectionChanged?.Invoke(isConnected); } // ================= 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 _); _isRobotConnected = false; } // ================= GET CACHE ================= public StateMsg? GetLatestState() { if (!LatestStates.IsEmpty) return LatestStates.First().Value; return null; } // ================= DISPOSE ================= public async ValueTask DisposeAsync() { _started = false; _isRobotConnected = false; SetState(RobotClientState.Disconnected); if (_connection != null) { await _connection.DisposeAsync(); _connection = null; } } }