using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.SignalR.Client; using RobotNet.VDA5050.State; using RobotNet10.FleetManager.Shared.DTOs.Robot; namespace RobotNet10.FleetManager.Client.Services; /// /// SignalR Hub Client for robot state and visualization updates. /// Manages connection to the SignalR hub and provides methods to subscribe/unsubscribe to robot updates. /// /// /// This client automatically handles reconnection and provides events for state and visualization updates. /// Dispose the client when done to properly clean up the connection. /// public class RobotStateHubClient : IAsyncDisposable { private readonly HubConnection _hubConnection; private readonly ILogger? _logger; public event Action? OnMonitorBoardcastUpdate; public event Action? OnStateUpdate; public event Action? OnConnectionError; public event Action? OnMonitorDeactivated; public RobotStateHubClient(NavigationManager navigationManager, ILogger? logger = null) { _logger = logger; var hubUrl = navigationManager.ToAbsoluteUri("/hubs/robot-state"); _hubConnection = new HubConnectionBuilder() .WithUrl(hubUrl) .WithAutomaticReconnect() .Build(); // Register event handlers _hubConnection.On("OnStateUpdate", (state) => { OnStateUpdate?.Invoke(state); }); _hubConnection.On("OnMonitorUpdate", (state) => { OnMonitorBoardcastUpdate?.Invoke(state); }); _hubConnection.On("OnMonitorDeactivated", () => { OnMonitorDeactivated?.Invoke(); }); _hubConnection.Closed += async (error) => { if (error != null) { _logger?.LogError(error, "SignalR connection closed with error"); OnConnectionError?.Invoke(error.Message); } else { _logger?.LogInformation("SignalR connection closed"); } await Task.CompletedTask; }; _hubConnection.Reconnecting += async (error) => { _logger?.LogWarning(error, "SignalR connection reconnecting"); await Task.CompletedTask; }; _hubConnection.Reconnected += async (connectionId) => { _logger?.LogInformation("SignalR connection reconnected with ID {ConnectionId}", connectionId); await Task.CompletedTask; }; } /// /// Connect to the SignalR hub /// public async Task ConnectAsync() { try { if (_hubConnection.State == HubConnectionState.Disconnected) { await _hubConnection.StartAsync(); _logger?.LogInformation("SignalR hub connected"); } } catch (Exception ex) { _logger?.LogError(ex, "Error connecting to SignalR hub"); OnConnectionError?.Invoke(ex.Message); throw; } } /// /// Disconnect from the SignalR hub /// public async Task DisconnectAsync() { try { if (_hubConnection.State != HubConnectionState.Disconnected) { await _hubConnection.StopAsync(); _logger?.LogInformation("SignalR hub disconnected"); } } catch (Exception ex) { _logger?.LogError(ex, "Error disconnecting from SignalR hub"); throw; } } /// /// Subscribe to receive updates for a specific robot /// /// Robot identifier (serialNumber) public async Task SubscribeToRobotAsync(string robotId) { try { if (_hubConnection.State == HubConnectionState.Connected) { await _hubConnection.InvokeAsync("SubscribeToRobot", robotId); _logger?.LogInformation("Subscribed to robot {RobotId}", robotId); } else { _logger?.LogWarning("Cannot subscribe to robot {RobotId}: Hub not connected", robotId); } } catch (Exception ex) { _logger?.LogError(ex, "Error subscribing to robot {RobotId}", robotId); throw; } } /// /// Unsubscribe from updates for a specific robot /// /// Robot identifier (serialNumber) public async Task UnsubscribeFromRobotAsync(string robotId) { try { if (_hubConnection.State == HubConnectionState.Connected) { await _hubConnection.InvokeAsync("UnsubscribeFromRobot", robotId); _logger?.LogInformation("Unsubscribed from robot {RobotId}", robotId); } } catch (Exception ex) { _logger?.LogError(ex, "Error unsubscribing from robot {RobotId}", robotId); throw; } } /// /// Subscribe to receive monitor updates for a specific levelId /// Each connection can only subscribe to one levelId at a time. /// Maximum 5 connections per levelId (FIFO). /// /// Level identifier (LayoutLevel.Id) public async Task SubscribeToLevelForMonitorAsync(Guid levelId) { try { if (_hubConnection.State == HubConnectionState.Connected) { await _hubConnection.InvokeAsync("SubscribeToLevelForMonitor", levelId); _logger?.LogInformation("Subscribed to level {LevelId} for monitor", levelId); } else { _logger?.LogWarning("Cannot subscribe to level {LevelId}: Hub not connected", levelId); } } catch (Exception ex) { _logger?.LogError(ex, "Error subscribing to level {LevelId} for monitor", levelId); throw; } } /// /// Unsubscribe from monitor updates for the current level /// public async Task UnsubscribeFromLevelForMonitorAsync() { try { if (_hubConnection.State == HubConnectionState.Connected) { await _hubConnection.InvokeAsync("UnsubscribeFromLevelForMonitor"); _logger?.LogInformation("Unsubscribed from level for monitor"); } } catch (Exception ex) { _logger?.LogError(ex, "Error unsubscribing from level for monitor"); throw; } } /// /// Get the current connection state /// public HubConnectionState ConnectionState => _hubConnection.State; public async ValueTask DisposeAsync() { if (_hubConnection != null) { await DisconnectAsync(); await _hubConnection.DisposeAsync(); } GC.SuppressFinalize(this); } }