Initial commit
This commit is contained in:
@@ -0,0 +1,337 @@
|
||||
@using RobotNet10.RobotApp.Client.Clients
|
||||
@using RobotNet10.RobotApp.Client.Shared.Modules
|
||||
@using MudBlazor
|
||||
|
||||
<MudPaper Class="pa-4" Style="height: 100%;">
|
||||
<MudStack Spacing="3">
|
||||
<MudStack Row="true" Justify="Justify.SpaceBetween" AlignItems="AlignItems.Center">
|
||||
<MudText Typo="Typo.h5">Lift Module</MudText>
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Refresh"
|
||||
Color="Color.Primary"
|
||||
Size="Size.Small"
|
||||
OnClick="RefreshStatus"
|
||||
Disabled="@(!IsHubReady || isLoading)" />
|
||||
</MudStack>
|
||||
|
||||
@* Lift Status Information *@
|
||||
@if (liftStatus != null)
|
||||
{
|
||||
<MudCard>
|
||||
<MudCardContent>
|
||||
<MudText Typo="Typo.h6" Class="mb-3">Module Status</MudText>
|
||||
<MudStack Spacing="2">
|
||||
<MudStack Row="true" Justify="Justify.SpaceBetween">
|
||||
<MudText>State:</MudText>
|
||||
<MudChip T="string" Color="@GetStateColor(liftStatus.State)" Size="Size.Small">
|
||||
@liftStatus.State
|
||||
</MudChip>
|
||||
</MudStack>
|
||||
<MudStack Row="true" Justify="Justify.SpaceBetween">
|
||||
<MudText>Ready:</MudText>
|
||||
<MudChip T="bool" Color="@(liftStatus.IsReady ? Color.Success : Color.Default)" Size="Size.Small">
|
||||
@liftStatus.IsReady
|
||||
</MudChip>
|
||||
</MudStack>
|
||||
<MudStack Row="true" Justify="Justify.SpaceBetween">
|
||||
<MudText>Current Position:</MudText>
|
||||
<MudText>
|
||||
@($"{liftStatus.CurrentPosition:N0} counts")
|
||||
(@($"{ConvertCountsToMeters(liftStatus.CurrentPosition):F3} m"))
|
||||
</MudText>
|
||||
</MudStack>
|
||||
</MudStack>
|
||||
</MudCardContent>
|
||||
</MudCard>
|
||||
}
|
||||
|
||||
@* Lift Control Buttons *@
|
||||
<MudStack Spacing="3">
|
||||
<MudText Typo="Typo.h6">Homing & Movement Controls</MudText>
|
||||
|
||||
<MudStack Row="true" Spacing="3" AlignItems="AlignItems.Center">
|
||||
<MudButton Variant="Variant.Filled"
|
||||
Color="Color.Primary"
|
||||
OnClick="LiftHome"
|
||||
Disabled="@(isLoading || !IsHubReady)">
|
||||
<MudIcon Icon="@Icons.Material.Filled.HomeRepairService" Class="mr-2" />
|
||||
<MudText>Homing</MudText>
|
||||
</MudButton>
|
||||
</MudStack>
|
||||
|
||||
<MudStack Row="true" Spacing="3" AlignItems="@AlignItems.Center">
|
||||
<MudButton Variant="Variant.Filled"
|
||||
Color="Color.Success"
|
||||
OnClick="LiftUp"
|
||||
Disabled="@(isLoading || !IsHubReady || liftStatus?.IsReady != true)">
|
||||
<MudIcon Icon="@Icons.Material.Filled.ArrowUpward" Class="mr-2" />
|
||||
<MudText>Up</MudText>
|
||||
</MudButton>
|
||||
|
||||
<MudButton Variant="Variant.Filled"
|
||||
Color="Color.Error"
|
||||
OnClick="LiftDown"
|
||||
Disabled="@(isLoading || !IsHubReady || liftStatus?.IsReady != true)">
|
||||
<MudIcon Icon="@Icons.Material.Filled.ArrowDownward" Class="mr-2" />
|
||||
<MudText>Down</MudText>
|
||||
</MudButton>
|
||||
|
||||
<MudButton Variant="Variant.Filled"
|
||||
Color="Color.Warning"
|
||||
OnClick="LiftStop"
|
||||
Disabled="@(!IsHubReady || liftStatus?.IsReady != true || liftStatus?.State != "Moving")">
|
||||
<MudIcon Icon="@Icons.Material.Filled.Stop" Class="mr-2" />
|
||||
<MudText>Stop</MudText>
|
||||
</MudButton>
|
||||
</MudStack>
|
||||
|
||||
<MudStack Row="true" Spacing="3" AlignItems="@AlignItems.Center">
|
||||
<MudNumericField @bind-Value="targetHeightMeters"
|
||||
Label="Target Height (m)"
|
||||
Variant="Variant.Outlined"
|
||||
Min="0"
|
||||
Class="flex-grow-1" />
|
||||
<MudButton Variant="Variant.Filled"
|
||||
Color="Color.Primary"
|
||||
OnClick="MoveToPosition"
|
||||
Disabled="@(isLoading || !IsHubReady || liftStatus?.IsReady != true)">
|
||||
<MudIcon Icon="@Icons.Material.Filled.LocationOn" Class="mr-2" />
|
||||
<MudText>Go</MudText>
|
||||
</MudButton>
|
||||
</MudStack>
|
||||
<MudText Typo="Typo.caption">
|
||||
Scale: 10,000 counts = 0.01 m (1,000,000 counts = 1 m)
|
||||
</MudText>
|
||||
</MudStack>
|
||||
</MudStack>
|
||||
</MudPaper>
|
||||
|
||||
@code {
|
||||
[Parameter, EditorRequired] public MotionHubClient HubClient { get; set; } = null!;
|
||||
[Parameter] public bool IsHubReady { get; set; }
|
||||
[Inject] private ISnackbar Snackbar { get; set; } = null!;
|
||||
[Inject] private CiA402ServoHubClient ServoHubClient { get; set; } = null!;
|
||||
|
||||
private LiftModuleStatusDto? liftStatus;
|
||||
private bool isLoading = false;
|
||||
private double targetHeightMeters = 0.0;
|
||||
private bool _previousIsHubReady = false;
|
||||
|
||||
protected override async Task OnParametersSetAsync()
|
||||
{
|
||||
// Detect when IsHubReady changes from false to true
|
||||
if (IsHubReady && !_previousIsHubReady)
|
||||
{
|
||||
await RefreshStatus();
|
||||
}
|
||||
_previousIsHubReady = IsHubReady;
|
||||
|
||||
// Đảm bảo kết nối tới CiA402ServoHub để homing trực tiếp
|
||||
if (!ServoHubClient.IsConnected)
|
||||
{
|
||||
try
|
||||
{
|
||||
await ServoHubClient.StartAsync();
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Nếu connect lỗi, homing sẽ báo lỗi qua Snackbar
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async Task RefreshStatus()
|
||||
{
|
||||
if (isLoading || !IsHubReady) return;
|
||||
|
||||
try
|
||||
{
|
||||
isLoading = true;
|
||||
liftStatus = await HubClient.GetLiftStatusAsync();
|
||||
if (liftStatus != null)
|
||||
{
|
||||
targetHeightMeters = ConvertCountsToMeters(liftStatus.CurrentPosition);
|
||||
}
|
||||
StateHasChanged();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Snackbar.Add($"Error refreshing lift status: {ex.Message}", Severity.Error);
|
||||
}
|
||||
finally
|
||||
{
|
||||
isLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
private Color GetStateColor(string state)
|
||||
{
|
||||
return state switch
|
||||
{
|
||||
"Ready" => Color.Success,
|
||||
"Moving" => Color.Info,
|
||||
"Error" => Color.Error,
|
||||
"Homing" => Color.Warning,
|
||||
"Initializing" => Color.Warning,
|
||||
_ => Color.Default
|
||||
};
|
||||
}
|
||||
|
||||
private async Task LiftUp()
|
||||
{
|
||||
if (!IsHubReady || isLoading || liftStatus?.IsReady != true)
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
isLoading = true;
|
||||
Snackbar.Add("Lifting up...", Severity.Info);
|
||||
|
||||
await HubClient.LiftUpAsync();
|
||||
|
||||
Snackbar.Add("Lift up command sent successfully", Severity.Success);
|
||||
|
||||
await Task.Delay(500);
|
||||
await RefreshStatus();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Snackbar.Add($"Error lifting up: {ex.Message}", Severity.Error);
|
||||
}
|
||||
finally
|
||||
{
|
||||
isLoading = false;
|
||||
StateHasChanged();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task LiftDown()
|
||||
{
|
||||
if (!IsHubReady || isLoading || liftStatus?.IsReady != true)
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
isLoading = true;
|
||||
Snackbar.Add("Lifting down...", Severity.Info);
|
||||
|
||||
await HubClient.LiftDownAsync();
|
||||
|
||||
Snackbar.Add("Lift down command sent successfully", Severity.Success);
|
||||
|
||||
await Task.Delay(500);
|
||||
await RefreshStatus();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Snackbar.Add($"Error lifting down: {ex.Message}", Severity.Error);
|
||||
}
|
||||
finally
|
||||
{
|
||||
isLoading = false;
|
||||
StateHasChanged();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task LiftStop()
|
||||
{
|
||||
if (!IsHubReady || liftStatus?.IsReady != true)
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
await HubClient.LiftStopAsync();
|
||||
Snackbar.Add("Lift stop command sent", Severity.Info);
|
||||
await Task.Delay(300);
|
||||
await RefreshStatus();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Snackbar.Add($"Error stopping lift: {ex.Message}", Severity.Error);
|
||||
}
|
||||
finally
|
||||
{
|
||||
StateHasChanged();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task MoveToPosition()
|
||||
{
|
||||
if (!IsHubReady || isLoading || liftStatus?.IsReady != true)
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
isLoading = true;
|
||||
var targetCounts = ConvertMetersToCounts(targetHeightMeters);
|
||||
Snackbar.Add($"Moving to height {targetHeightMeters:F3} m (~{targetCounts} counts)...", Severity.Info);
|
||||
|
||||
await HubClient.LiftToPositionAsync(targetCounts);
|
||||
|
||||
Snackbar.Add($"Move to height {targetHeightMeters:F3} m command sent successfully", Severity.Success);
|
||||
|
||||
await Task.Delay(500);
|
||||
await RefreshStatus();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Snackbar.Add($"Error moving to position: {ex.Message}", Severity.Error);
|
||||
}
|
||||
finally
|
||||
{
|
||||
isLoading = false;
|
||||
StateHasChanged();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task LiftHome()
|
||||
{
|
||||
if (isLoading)
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
isLoading = true;
|
||||
Snackbar.Add("Starting lift homing (direct device)...", Severity.Info);
|
||||
|
||||
const string liftDeviceId = "lift-motor";
|
||||
|
||||
// Dùng cùng tham số như appsettings (hoặc theo nhu cầu của bạn)
|
||||
const byte homingMethod = 21;
|
||||
const int homingSpeed = 20000;
|
||||
const int homingOffset = 0;
|
||||
|
||||
// Ghi homing params xuống drive giống CiA402ServoCard
|
||||
await ServoHubClient.SetHomingMethodAsync(liftDeviceId, homingMethod);
|
||||
await ServoHubClient.SetHomingSpeedAsync(liftDeviceId, homingSpeed);
|
||||
await ServoHubClient.SetHomingOffsetAsync(liftDeviceId, homingOffset);
|
||||
|
||||
// Start homing trực tiếp trên thiết bị
|
||||
await ServoHubClient.StartHomingAsync(liftDeviceId, homingMethod, homingSpeed);
|
||||
|
||||
Snackbar.Add("Lift homing command sent to lift-motor", Severity.Success);
|
||||
|
||||
await Task.Delay(500);
|
||||
await RefreshStatus();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Snackbar.Add($"Error homing lift: {ex.Message}", Severity.Error);
|
||||
}
|
||||
finally
|
||||
{
|
||||
isLoading = false;
|
||||
StateHasChanged();
|
||||
}
|
||||
}
|
||||
|
||||
private static double ConvertCountsToMeters(int counts)
|
||||
{
|
||||
// 10,000 counts = 0.01 m => 1,000,000 counts = 1 m
|
||||
return counts / 1_000_000.0;
|
||||
}
|
||||
|
||||
private static int ConvertMetersToCounts(double meters)
|
||||
{
|
||||
return (int)Math.Round(meters * 1_000_000.0);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
@using RobotNet10.RobotApp.Client.Clients
|
||||
@using MudBlazor
|
||||
|
||||
<MudPaper Class="pa-4" Style="height: 100%;">
|
||||
<MudStack Spacing="3">
|
||||
<MudStack Row="true" Justify="Justify.SpaceBetween" AlignItems="AlignItems.Center">
|
||||
<MudText Typo="Typo.h5">Manual Control</MudText>
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Refresh"
|
||||
Color="Color.Primary"
|
||||
Size="Size.Small"
|
||||
OnClick="RefreshStatus"
|
||||
Disabled="@(isLoading)" />
|
||||
</MudStack>
|
||||
|
||||
@* Enable / Disable PS5 Controller *@
|
||||
<MudStack Row="true" Spacing="3" AlignItems="@AlignItems.Center">
|
||||
<MudButton Variant="Variant.Filled"
|
||||
Color="@(ps5Enabled ? Color.Error : Color.Success)"
|
||||
OnClick="TogglePs5Control"
|
||||
Disabled="@isLoading">
|
||||
@if (ps5Enabled)
|
||||
{
|
||||
<MudIcon Icon="@Icons.Material.Filled.Stop" Class="mr-2" />
|
||||
<MudText>Disable PS5 Controller</MudText>
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudIcon Icon="@Icons.Material.Filled.PlayArrow" Class="mr-2" />
|
||||
<MudText>Enable PS5 Controller</MudText>
|
||||
}
|
||||
</MudButton>
|
||||
</MudStack>
|
||||
|
||||
@* Status of PS5 Controller *@
|
||||
@if (ps5State != null)
|
||||
{
|
||||
<MudCard>
|
||||
<MudCardContent>
|
||||
<MudText Typo="Typo.h6" Class="mb-3">PS5 Controller Status</MudText>
|
||||
<MudStack Spacing="2">
|
||||
<MudStack Row="true" Justify="Justify.SpaceBetween">
|
||||
<MudText>State:</MudText>
|
||||
<MudChip T="string" Color="@(ps5Enabled ? Color.Success : Color.Default)" Size="Size.Small">
|
||||
@ps5State
|
||||
</MudChip>
|
||||
</MudStack>
|
||||
</MudStack>
|
||||
</MudCardContent>
|
||||
</MudCard>
|
||||
}
|
||||
</MudStack>
|
||||
</MudPaper>
|
||||
|
||||
@code {
|
||||
[Parameter] public MotionHubClient? HubClient { get; set; }
|
||||
[Parameter] public bool IsHubReady { get; set; }
|
||||
[Inject] private HttpClient Http { get; set; } = null!;
|
||||
[Inject] private ISnackbar Snackbar { get; set; } = null!;
|
||||
|
||||
private string? ps5State;
|
||||
private bool ps5Enabled => string.Equals(ps5State, "Active", StringComparison.OrdinalIgnoreCase);
|
||||
private bool isLoading = false;
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
await RefreshStatus();
|
||||
}
|
||||
|
||||
public async Task RefreshStatus()
|
||||
{
|
||||
if (isLoading) return;
|
||||
|
||||
try
|
||||
{
|
||||
isLoading = true;
|
||||
var resp = await Http.GetAsync("/api/motion/ps5/status");
|
||||
if (resp.IsSuccessStatusCode)
|
||||
{
|
||||
var json = await resp.Content.ReadFromJsonAsync<Ps5StatusResponse>();
|
||||
ps5State = json?.State ?? "Unknown";
|
||||
}
|
||||
StateHasChanged();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Snackbar.Add($"Failed to refresh PS controller status: {ex.Message}", Severity.Error);
|
||||
}
|
||||
finally
|
||||
{
|
||||
isLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task TogglePs5Control()
|
||||
{
|
||||
if (isLoading) return;
|
||||
|
||||
try
|
||||
{
|
||||
isLoading = true;
|
||||
if (ps5Enabled)
|
||||
{
|
||||
var resp = await Http.PostAsync("/api/motion/ps5/disable", null);
|
||||
if (resp.IsSuccessStatusCode)
|
||||
{
|
||||
Snackbar.Add("PS controller disabled", Severity.Info);
|
||||
ps5State = "Disabled";
|
||||
}
|
||||
else
|
||||
Snackbar.Add("Failed to disable PS controller", Severity.Error);
|
||||
}
|
||||
else
|
||||
{
|
||||
var resp = await Http.PostAsync("/api/motion/ps5/enable", null);
|
||||
if (resp.IsSuccessStatusCode)
|
||||
{
|
||||
Snackbar.Add("PS controller enabled", Severity.Success);
|
||||
ps5State = "Active";
|
||||
}
|
||||
else
|
||||
Snackbar.Add("Failed to enable PS controller", Severity.Error);
|
||||
}
|
||||
|
||||
for (int i = 0; i < 3; i++)
|
||||
{
|
||||
await Task.Delay(100);
|
||||
await RefreshStatus();
|
||||
if (ps5State != null) break;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Snackbar.Add($"Error: {ex.Message}", Severity.Error);
|
||||
}
|
||||
finally
|
||||
{
|
||||
isLoading = false;
|
||||
StateHasChanged();
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class Ps5StatusResponse
|
||||
{
|
||||
public string State { get; set; } = string.Empty;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
@using RobotNet10.RobotApp.Client.Shared.Motion
|
||||
@using RobotNet10.Shared.Geometry
|
||||
@using RobotNet10.Shared.Numbers
|
||||
@using QuaternionGeometry = RobotNet10.Shared.Geometry.Quaternion
|
||||
@using QuaternionNumbers = RobotNet10.Shared.Numbers.Quaternion
|
||||
@using MudBlazor
|
||||
@implements IDisposable
|
||||
|
||||
<MudPaper Class="pa-4" Style="height: 100%;">
|
||||
<MudStack Spacing="3">
|
||||
<MudStack Row="true" Justify="Justify.SpaceBetween" AlignItems="AlignItems.Center">
|
||||
<MudText Typo="Typo.h5">Odometry</MudText>
|
||||
</MudStack>
|
||||
|
||||
@* Odometry Information *@
|
||||
@if (Odometry != null)
|
||||
{
|
||||
<MudCard>
|
||||
<MudCardContent>
|
||||
<MudText Typo="Typo.h6" Class="mb-3">Position</MudText>
|
||||
<MudStack Spacing="2">
|
||||
<MudStack Row="true" Justify="Justify.SpaceBetween">
|
||||
<MudText>X:</MudText>
|
||||
<MudText>@($"{Odometry.PositionX:F3} m")</MudText>
|
||||
</MudStack>
|
||||
<MudStack Row="true" Justify="Justify.SpaceBetween">
|
||||
<MudText>Y:</MudText>
|
||||
<MudText>@($"{Odometry.PositionY:F3} m")</MudText>
|
||||
</MudStack>
|
||||
<MudStack Row="true" Justify="Justify.SpaceBetween">
|
||||
<MudText>Z:</MudText>
|
||||
<MudText>@($"{Odometry.PositionZ:F3} m")</MudText>
|
||||
</MudStack>
|
||||
</MudStack>
|
||||
</MudCardContent>
|
||||
</MudCard>
|
||||
|
||||
<MudCard>
|
||||
<MudCardContent>
|
||||
<MudText Typo="Typo.h6" Class="mb-3">Orientation</MudText>
|
||||
<MudStack Spacing="2">
|
||||
<MudStack Row="true" Justify="Justify.SpaceBetween">
|
||||
<MudText>Yaw:</MudText>
|
||||
<MudText>@($"{yawDegrees:F2}°")</MudText>
|
||||
</MudStack>
|
||||
<MudStack Row="true" Justify="Justify.SpaceBetween">
|
||||
<MudText>Update Freq:</MudText>
|
||||
<MudChip T="double" Color="@(Odometry.UpdateFrequency > 0 ? Color.Success : Color.Default)" Size="Size.Small">
|
||||
@($"{Odometry.UpdateFrequency:F2} Hz")
|
||||
</MudChip>
|
||||
</MudStack>
|
||||
</MudStack>
|
||||
</MudCardContent>
|
||||
</MudCard>
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudAlert Severity="Severity.Info">No odometry data available.</MudAlert>
|
||||
}
|
||||
</MudStack>
|
||||
</MudPaper>
|
||||
|
||||
@code {
|
||||
[Parameter, EditorRequired] public OdometryDto? Odometry { get; set; }
|
||||
[Parameter] public bool IsHubReady { get; set; }
|
||||
[Inject] private ISnackbar Snackbar { get; set; } = null!;
|
||||
|
||||
private double _smoothedYawDegrees;
|
||||
private bool _hasYaw;
|
||||
|
||||
private double yawDegrees => _smoothedYawDegrees;
|
||||
|
||||
protected override void OnParametersSet()
|
||||
{
|
||||
if (Odometry == null)
|
||||
return;
|
||||
|
||||
// Nếu robot gần như đứng yên (ít quay, ít chạy thẳng) thì giữ nguyên yaw
|
||||
var angularZ = Odometry.AngularVelocityZ; // rad/s
|
||||
var linearX = Odometry.LinearVelocityX; // m/s
|
||||
const double angularDeadZone = 0.01; // ~0.57°
|
||||
const double linearDeadZone = 0.005; // 5 mm/s
|
||||
|
||||
var isAlmostStopped =
|
||||
Math.Abs(angularZ) < angularDeadZone &&
|
||||
Math.Abs(linearX) < linearDeadZone;
|
||||
|
||||
// Lần đầu vẫn phải init giá trị
|
||||
if (!_hasYaw)
|
||||
{
|
||||
var q0 = new QuaternionGeometry(Odometry.OrientationX, Odometry.OrientationY, Odometry.OrientationZ, Odometry.OrientationW);
|
||||
_smoothedYawDegrees = q0.ToYawDegrees();
|
||||
_hasYaw = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (isAlmostStopped)
|
||||
{
|
||||
// Robot đứng yên: không cập nhật yaw để tránh drift chậm
|
||||
return;
|
||||
}
|
||||
|
||||
// Robot đang quay / di chuyển: cho phép yaw thay đổi nhưng có lọc
|
||||
var quaternion = new QuaternionGeometry(Odometry.OrientationX, Odometry.OrientationY, Odometry.OrientationZ, Odometry.OrientationW);
|
||||
var newYaw = quaternion.ToYawDegrees();
|
||||
|
||||
// Normalize delta để tránh nhảy 360° khi wrap-around
|
||||
var delta = newYaw - _smoothedYawDegrees;
|
||||
while (delta > 180.0) delta -= 360.0;
|
||||
while (delta < -180.0) delta += 360.0;
|
||||
|
||||
// Low-pass filter để làm mượt (alpha càng nhỏ càng mượt)
|
||||
const double alpha = 0.3;
|
||||
_smoothedYawDegrees = _smoothedYawDegrees + alpha * delta;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
// Nothing to dispose currently
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,233 @@
|
||||
@using RobotNet10.RobotApp.Client.Clients
|
||||
@using RobotNet10.RobotApp.Client.Shared.Modules
|
||||
@using MudBlazor
|
||||
|
||||
<MudPaper Class="pa-4" Style="height: 100%;">
|
||||
<MudStack Spacing="3">
|
||||
<MudStack Row="true" Justify="Justify.SpaceBetween" AlignItems="AlignItems.Center">
|
||||
<MudText Typo="Typo.h5">Rotation Module</MudText>
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Refresh"
|
||||
Color="Color.Primary"
|
||||
Size="Size.Small"
|
||||
OnClick="RefreshStatus"
|
||||
Disabled="@(!IsHubReady || isLoading)" />
|
||||
</MudStack>
|
||||
|
||||
@* Rotation Status Information *@
|
||||
@if (rotationStatus != null)
|
||||
{
|
||||
<MudCard>
|
||||
<MudCardContent>
|
||||
<MudText Typo="Typo.h6" Class="mb-3">Module Status</MudText>
|
||||
<MudStack Spacing="2">
|
||||
<MudStack Row="true" Justify="Justify.SpaceBetween">
|
||||
<MudText>State:</MudText>
|
||||
<MudChip T="string" Color="@GetStateColor(rotationStatus.State)" Size="Size.Small">
|
||||
@rotationStatus.State
|
||||
</MudChip>
|
||||
</MudStack>
|
||||
<MudStack Row="true" Justify="Justify.SpaceBetween">
|
||||
<MudText>Ready:</MudText>
|
||||
<MudChip T="bool" Color="@(rotationStatus.IsReady ? Color.Success : Color.Default)" Size="Size.Small">
|
||||
@rotationStatus.IsReady
|
||||
</MudChip>
|
||||
</MudStack>
|
||||
<MudStack Row="true" Justify="Justify.SpaceBetween">
|
||||
<MudText>Current Angle:</MudText>
|
||||
<MudText>@($"{rotationStatus.CurrentAngle:F2}°")</MudText>
|
||||
</MudStack>
|
||||
</MudStack>
|
||||
</MudCardContent>
|
||||
</MudCard>
|
||||
}
|
||||
|
||||
@* Rotation Control Buttons *@
|
||||
<MudStack Spacing="3">
|
||||
<MudText Typo="Typo.h6">Rotation Controls</MudText>
|
||||
|
||||
@* Rotate to Absolute Angle *@
|
||||
<MudStack Row="true" Spacing="3" AlignItems="@AlignItems.Center">
|
||||
<MudNumericField @bind-Value="targetAngle"
|
||||
Label="Target Angle (°)"
|
||||
Variant="Variant.Outlined"
|
||||
Class="flex-grow-1" />
|
||||
<MudButton Variant="Variant.Filled"
|
||||
Color="Color.Primary"
|
||||
OnClick="RotateToAngle"
|
||||
Disabled="@(isLoading || !IsHubReady || rotationStatus?.IsReady != true)">
|
||||
<MudIcon Icon="@Icons.Material.Filled.LocationOn" Class="mr-2" />
|
||||
<MudText>Go</MudText>
|
||||
</MudButton>
|
||||
</MudStack>
|
||||
|
||||
@* Rotate Offset *@
|
||||
<MudStack Row="true" Spacing="3" AlignItems="@AlignItems.Center">
|
||||
<MudNumericField @bind-Value="angleOffset"
|
||||
Label="Offset (°)"
|
||||
Variant="Variant.Outlined"
|
||||
HelperText="+/- for CW/CCW"
|
||||
Class="flex-grow-1" />
|
||||
<MudButton Variant="Variant.Filled"
|
||||
Color="Color.Info"
|
||||
OnClick="RotateOffset"
|
||||
Disabled="@(isLoading || !IsHubReady || rotationStatus?.IsReady != true)">
|
||||
<MudIcon Icon="@Icons.Material.Filled.RotateRight" Class="mr-2" />
|
||||
<MudText>Offset</MudText>
|
||||
</MudButton>
|
||||
</MudStack>
|
||||
|
||||
@* Quick Rotation Buttons *@
|
||||
<MudStack Row="true" Spacing="3" AlignItems="@AlignItems.Center" Wrap="Wrap.Wrap">
|
||||
<MudButton Variant="Variant.Outlined"
|
||||
Color="Color.Secondary"
|
||||
OnClick="() => RotateOffsetQuick(-90)"
|
||||
Disabled="@(isLoading || !IsHubReady || rotationStatus?.IsReady != true)">
|
||||
<MudIcon Icon="@Icons.Material.Filled.RotateLeft" Class="mr-2" />
|
||||
<MudText>-90°</MudText>
|
||||
</MudButton>
|
||||
|
||||
<MudButton Variant="Variant.Outlined"
|
||||
Color="Color.Secondary"
|
||||
OnClick="() => RotateOffsetQuick(-45)"
|
||||
Disabled="@(isLoading || !IsHubReady || rotationStatus?.IsReady != true)">
|
||||
<MudIcon Icon="@Icons.Material.Filled.RotateLeft" Class="mr-2" />
|
||||
<MudText>-45°</MudText>
|
||||
</MudButton>
|
||||
|
||||
<MudButton Variant="Variant.Outlined"
|
||||
Color="Color.Secondary"
|
||||
OnClick="() => RotateOffsetQuick(45)"
|
||||
Disabled="@(isLoading || !IsHubReady || rotationStatus?.IsReady != true)">
|
||||
<MudIcon Icon="@Icons.Material.Filled.RotateRight" Class="mr-2" />
|
||||
<MudText>+45°</MudText>
|
||||
</MudButton>
|
||||
|
||||
<MudButton Variant="Variant.Outlined"
|
||||
Color="Color.Secondary"
|
||||
OnClick="() => RotateOffsetQuick(90)"
|
||||
Disabled="@(isLoading || !IsHubReady || rotationStatus?.IsReady != true)">
|
||||
<MudIcon Icon="@Icons.Material.Filled.RotateRight" Class="mr-2" />
|
||||
<MudText>+90°</MudText>
|
||||
</MudButton>
|
||||
</MudStack>
|
||||
</MudStack>
|
||||
</MudStack>
|
||||
</MudPaper>
|
||||
|
||||
@code {
|
||||
[Parameter, EditorRequired] public MotionHubClient HubClient { get; set; } = null!;
|
||||
[Parameter] public bool IsHubReady { get; set; }
|
||||
[Inject] private ISnackbar Snackbar { get; set; } = null!;
|
||||
|
||||
private RotationModuleStatusDto? rotationStatus;
|
||||
private bool isLoading = false;
|
||||
private double targetAngle = 0;
|
||||
private double angleOffset = 0;
|
||||
private bool _previousIsHubReady = false;
|
||||
|
||||
protected override async Task OnParametersSetAsync()
|
||||
{
|
||||
// Detect when IsHubReady changes from false to true
|
||||
if (IsHubReady && !_previousIsHubReady)
|
||||
{
|
||||
await RefreshStatus();
|
||||
}
|
||||
_previousIsHubReady = IsHubReady;
|
||||
}
|
||||
|
||||
public async Task RefreshStatus()
|
||||
{
|
||||
if (isLoading || !IsHubReady) return;
|
||||
|
||||
try
|
||||
{
|
||||
isLoading = true;
|
||||
rotationStatus = await HubClient.GetRotationStatusAsync();
|
||||
StateHasChanged();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Snackbar.Add($"Error refreshing rotation status: {ex.Message}", Severity.Error);
|
||||
}
|
||||
finally
|
||||
{
|
||||
isLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
private Color GetStateColor(string state)
|
||||
{
|
||||
return state switch
|
||||
{
|
||||
"Ready" => Color.Success,
|
||||
"Moving" => Color.Info,
|
||||
"Error" => Color.Error,
|
||||
"Homing" => Color.Warning,
|
||||
"Initializing" => Color.Warning,
|
||||
_ => Color.Default
|
||||
};
|
||||
}
|
||||
|
||||
private async Task RotateToAngle()
|
||||
{
|
||||
if (!IsHubReady || isLoading || rotationStatus?.IsReady != true)
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
isLoading = true;
|
||||
Snackbar.Add($"Rotating to angle {targetAngle}°...", Severity.Info);
|
||||
|
||||
await HubClient.RotateToAngleAsync(targetAngle);
|
||||
|
||||
Snackbar.Add($"Rotate to angle {targetAngle}° command sent successfully", Severity.Success);
|
||||
|
||||
await Task.Delay(500);
|
||||
await RefreshStatus();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Snackbar.Add($"Error rotating to angle: {ex.Message}", Severity.Error);
|
||||
}
|
||||
finally
|
||||
{
|
||||
isLoading = false;
|
||||
StateHasChanged();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task RotateOffset()
|
||||
{
|
||||
if (!IsHubReady || isLoading || rotationStatus?.IsReady != true)
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
isLoading = true;
|
||||
var offsetText = angleOffset >= 0 ? $"+{angleOffset}°" : $"{angleOffset}°";
|
||||
Snackbar.Add($"Rotating offset {offsetText}...", Severity.Info);
|
||||
|
||||
await HubClient.RotateOffsetAsync(angleOffset);
|
||||
|
||||
Snackbar.Add($"Rotate offset {offsetText} command sent successfully", Severity.Success);
|
||||
|
||||
await Task.Delay(500);
|
||||
await RefreshStatus();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Snackbar.Add($"Error rotating offset: {ex.Message}", Severity.Error);
|
||||
}
|
||||
finally
|
||||
{
|
||||
isLoading = false;
|
||||
StateHasChanged();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task RotateOffsetQuick(double offset)
|
||||
{
|
||||
angleOffset = offset;
|
||||
await RotateOffset();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user