@implements IAsyncDisposable @using MudBlazor @using RobotNet10.RobotApp.Client.Clients @using RobotNet10.RobotApp.Client.Shared.Devices @inject RfHandleHubClient RfHandleHubClient @inject ISnackbar Snackbar @DeviceName @DeviceId Joystick Linear @AxisText(RfHandleData.Linear) Angular @AxisText(RfHandleData.Angular) Status Heartbeat RemoteReady EStop Last Update: @RfHandleData.LastUpdateTime.ToString("HH:mm:ss") Speed Speed: @RfHandleData.Speed% Motion LiftUp LiftDown Rot L Rot R Mode & Flags Mode: @RfHandleData.Mode ModeSelect Enable @code { // ===== Joystick visual helpers ===== private const double JoyRadius = 70f; // px private const double JoyCenter = 110f; private double SafeLinear => (!RfHandleData.RemoteReady || RfHandleData.EStop) ? 0 : Math.Clamp(RfHandleData.Linear, -1, 1); private double SafeAngular => (!RfHandleData.RemoteReady || RfHandleData.EStop) ? 0 : Math.Clamp(RfHandleData.Angular, -1, 1); private double JoyX => JoyCenter + SafeAngular * JoyRadius; private double JoyY => JoyCenter - SafeLinear * JoyRadius; private string JoyColor => (!RfHandleData.RemoteReady || RfHandleData.EStop) ? "#666" : "#bdbdbd"; private RfHandleDataDto RfHandleData = new(); private bool IsReloading = false; private bool AutoReloadEnabled = false; private string DeviceName = ""; private System.Threading.PeriodicTimer? _autoReloadTimer; private readonly System.Threading.CancellationTokenSource _cancellationTokenSource = new(); [Parameter] public string DeviceId { get; set; } = ""; private bool IsLoading => !RfHandleHubClient.IsConnected; protected override async Task OnAfterRenderAsync(bool firstRender) { if (!firstRender) return; try { await RfHandleHubClient.StartAsync(); var info = await RfHandleHubClient.GetDeviceInfoAsync(DeviceId); DeviceName = info?.DeviceName ?? DeviceId; RfHandleData = await RfHandleHubClient.GetRfHandleDataAsync(DeviceId); StateHasChanged(); } catch (Exception ex) { Snackbar.Add($"Connect failed: {ex.Message}", Severity.Error); } } private async Task ReloadRfHandleDataAsync() { if (!RfHandleHubClient.IsConnected || IsReloading) return; try { IsReloading = true; StateHasChanged(); RfHandleData = await RfHandleHubClient.GetRfHandleDataAsync(DeviceId); } catch (Exception ex) { Snackbar.Add($"Failed to reload RfHandle data: {ex.Message}", Severity.Error); } finally { IsReloading = false; StateHasChanged(); } } private async Task ToggleAutoReloadAsync() { if (AutoReloadEnabled) { await StopAutoReloadAsync(); } else { await StartAutoReloadAsync(); } } private async Task StartAutoReloadAsync() { if (AutoReloadEnabled || !RfHandleHubClient.IsConnected) return; AutoReloadEnabled = true; _autoReloadTimer = new System.Threading.PeriodicTimer(TimeSpan.FromSeconds(0.05)); _ = Task.Run(async () => { try { while (await _autoReloadTimer.WaitForNextTickAsync(_cancellationTokenSource.Token)) { if (!_cancellationTokenSource.Token.IsCancellationRequested && RfHandleHubClient.IsConnected) { await InvokeAsync(async () => { await ReloadRfHandleDataAsync(); }); } else { break; } } } catch (System.OperationCanceledException) { // Expected when cancelling } }); StateHasChanged(); } private async Task StopAutoReloadAsync() { if (!AutoReloadEnabled) return; AutoReloadEnabled = false; if (_autoReloadTimer != null) { _autoReloadTimer.Dispose(); _autoReloadTimer = null; } StateHasChanged(); } public async ValueTask DisposeAsync() { await StopAutoReloadAsync(); _cancellationTokenSource.Cancel(); _cancellationTokenSource.Dispose(); try { await RfHandleHubClient.StopAsync(); } catch { } } private int AxisToPercent(double v) { // v ∈ [-1, +1] → [0, 100] return (int)Math.Clamp((v + 1) * 50f, 0, 100f); } private string AxisText(double v) { return v.ToString("F2"); } }