160 lines
4.5 KiB
C#
160 lines
4.5 KiB
C#
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<string, StateMsg> LatestStates { get; } = new();
|
|
|
|
public event Action<string, StateMsg>? OnStateReceived;
|
|
public event Action<StateMsg>? 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<string>("ReceiveState", stateJson =>
|
|
{
|
|
try
|
|
{
|
|
var state = JsonSerializer.Deserialize<StateMsg>(
|
|
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");
|
|
}
|
|
}
|
|
}
|