@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");
}
}