505 lines
22 KiB
Plaintext
505 lines
22 KiB
Plaintext
@page "/plc/controller"
|
|
@rendermode InteractiveWebAssemblyNoPrerender
|
|
@implements IAsyncDisposable
|
|
|
|
@using RobotNet10.RobotApp.Client.Clients
|
|
@using RobotNet10.RobotApp.Client.Shared.Plc
|
|
@using Microsoft.AspNetCore.SignalR.Client
|
|
@using Microsoft.Extensions.DependencyInjection
|
|
@using MudBlazor
|
|
|
|
<PageTitle>PLC Controller</PageTitle>
|
|
|
|
<div style="height: 100%; display: flex; flex-direction: column; overflow: hidden;">
|
|
@* Sticky Header Section *@
|
|
<div style="flex-shrink: 0; background: var(--mud-palette-background); z-index: 10;">
|
|
<MudContainer MaxWidth="MaxWidth.ExtraLarge" Class="py-3">
|
|
<MudStack Spacing="2">
|
|
<MudText Typo="Typo.h5">PLC Controller Status</MudText>
|
|
|
|
@* Connection Status *@
|
|
<MudAlert Severity="@(hubClient?.IsConnected == true ? Severity.Success : Severity.Warning)" Dense>
|
|
@if (hubClient?.IsConnected == true)
|
|
{
|
|
<MudText>Connected to PlcControllerHub</MudText>
|
|
}
|
|
else
|
|
{
|
|
<MudText>Disconnected from PlcControllerHub</MudText>
|
|
}
|
|
</MudAlert>
|
|
|
|
@* Control Section *@
|
|
<MudPaper Class="pa-3">
|
|
<MudStack Row="true" Spacing="2" AlignItems="@AlignItems.Center">
|
|
<MudButton Variant="Variant.Filled"
|
|
Color="Color.Success"
|
|
OnClick="EnableUpdate"
|
|
Disabled="@(isUpdateEnabled || !isHubReady)">
|
|
<MudIcon Icon="@Icons.Material.Filled.PlayArrow" Size="Size.Small" Class="mr-1" />
|
|
Enable Update
|
|
</MudButton>
|
|
|
|
<MudButton Variant="Variant.Filled"
|
|
Color="Color.Error"
|
|
OnClick="DisableUpdate"
|
|
Disabled="@(!isUpdateEnabled)">
|
|
<MudIcon Icon="@Icons.Material.Filled.Stop" Size="Size.Small" Class="mr-1" />
|
|
Disable Update
|
|
</MudButton>
|
|
|
|
<MudChip T="string" Color="@(isUpdateEnabled? Color.Success: Color.Default)" Size="Size.Small">
|
|
@(isUpdateEnabled ? "Updating (2Hz)" : "Stopped")
|
|
</MudChip>
|
|
</MudStack>
|
|
</MudPaper>
|
|
</MudStack>
|
|
</MudContainer>
|
|
</div>
|
|
|
|
@* Scrollable Content Area *@
|
|
<div style="flex: 1; overflow-y: auto; overflow-x: hidden;">
|
|
<MudContainer MaxWidth="MaxWidth.ExtraLarge" Class="py-3">
|
|
@if (status != null)
|
|
{
|
|
<MudGrid Spacing="2">
|
|
@* System Status Card *@
|
|
<MudItem xs="12" md="6" lg="4">
|
|
<MudCard Style="height: 100%;">
|
|
<MudCardHeader Class="py-2">
|
|
<CardHeaderContent>
|
|
<MudText Typo="Typo.subtitle1"><strong>System Status</strong></MudText>
|
|
</CardHeaderContent>
|
|
<CardHeaderActions>
|
|
<MudChip T="string" Color="@(status.IsReady? Color.Success: Color.Error)" Size="Size.Small">
|
|
@(status.IsReady ? "Ready" : "Not Ready")
|
|
</MudChip>
|
|
</CardHeaderActions>
|
|
</MudCardHeader>
|
|
<MudCardContent Class="py-2">
|
|
<MudStack Spacing="1">
|
|
<MudStack Row="true" Justify="Justify.SpaceBetween" Class="mb-1">
|
|
<MudText Typo="Typo.body2">Peripheral Mode:</MudText>
|
|
<MudChip T="string" Color="@GetModeColor(status.PeripheralMode)" Size="Size.Small">
|
|
@status.PeripheralMode
|
|
</MudChip>
|
|
</MudStack>
|
|
<MudStack Row="true" Justify="Justify.SpaceBetween" Class="mb-1">
|
|
<MudText Typo="Typo.body2">Safety Speed:</MudText>
|
|
<MudChip T="string" Color="@GetSpeedColor(status.SafetySpeed)" Size="Size.Small">
|
|
@status.SafetySpeed
|
|
</MudChip>
|
|
</MudStack>
|
|
<MudStack Row="true" Justify="Justify.SpaceBetween" Class="mb-1">
|
|
<MudText Typo="Typo.body2">Stop State:</MudText>
|
|
<MudChip T="string" Color="@GetStopStateColor(status.StopState)" Size="Size.Small">
|
|
@status.StopState
|
|
</MudChip>
|
|
</MudStack>
|
|
</MudStack>
|
|
</MudCardContent>
|
|
</MudCard>
|
|
</MudItem>
|
|
|
|
@* Lift State Card *@
|
|
<MudItem xs="12" md="6" lg="4">
|
|
<MudCard Style="height: 100%;">
|
|
<MudCardHeader Class="py-2">
|
|
<CardHeaderContent>
|
|
<MudText Typo="Typo.subtitle1"><strong>Lift State</strong></MudText>
|
|
</CardHeaderContent>
|
|
</MudCardHeader>
|
|
<MudCardContent Class="py-2">
|
|
<MudStack Spacing="1">
|
|
@StatusIndicator(("Lifted Up", status.LiftedUp, false))
|
|
@StatusIndicator(("Lifted Down", status.LiftedDown, false))
|
|
@StatusIndicator(("Lift Home", status.LiftHome, false))
|
|
</MudStack>
|
|
</MudCardContent>
|
|
</MudCard>
|
|
</MudItem>
|
|
|
|
@* (Lift Module control removed; now use LiftModuleCard in Motion/ManualControl page) *@
|
|
|
|
@* Motor State Card *@
|
|
<MudItem xs="12" md="6" lg="4">
|
|
<MudCard Style="height: 100%;">
|
|
<MudCardHeader Class="py-2">
|
|
<CardHeaderContent>
|
|
<MudText Typo="Typo.subtitle1"><strong>Motor State</strong></MudText>
|
|
</CardHeaderContent>
|
|
</MudCardHeader>
|
|
<MudCardContent Class="py-2">
|
|
<MudStack Spacing="1">
|
|
@StatusIndicator(("Left Motor", status.LeftMotorReady, false))
|
|
@StatusIndicator(("Right Motor", status.RightMotorReady, false))
|
|
@StatusIndicator(("Lift Motor", status.LiftMotorReady, false))
|
|
</MudStack>
|
|
</MudCardContent>
|
|
</MudCard>
|
|
</MudItem>
|
|
|
|
@* Safety Sensors Card *@
|
|
<MudItem xs="12" md="6" lg="4">
|
|
<MudCard Style="height: 100%;">
|
|
<MudCardHeader Class="py-2">
|
|
<CardHeaderContent>
|
|
<MudText Typo="Typo.subtitle1"><strong>Safety Sensors</strong></MudText>
|
|
</CardHeaderContent>
|
|
</MudCardHeader>
|
|
<MudCardContent Class="py-2">
|
|
<MudStack Spacing="1">
|
|
@StatusIndicator(("Emergency", status.Emergency, true))
|
|
@StatusIndicator(("Bumper", status.Bumper, true))
|
|
@StatusIndicator(("Lidar Front", status.LidarFrontProtectField, true))
|
|
@StatusIndicator(("Lidar Back", status.LidarBackProtectField, true))
|
|
@StatusIndicator(("Lidar Tim", status.LidarFrontTimProtectField, true))
|
|
</MudStack>
|
|
</MudCardContent>
|
|
</MudCard>
|
|
</MudItem>
|
|
|
|
|
|
@* Button State Card *@
|
|
<MudItem xs="12" md="6" lg="4">
|
|
<MudCard Style="height: 100%;">
|
|
<MudCardHeader Class="py-2">
|
|
<CardHeaderContent>
|
|
<MudText Typo="Typo.subtitle1"><strong>Button State</strong></MudText>
|
|
</CardHeaderContent>
|
|
</MudCardHeader>
|
|
<MudCardContent Class="py-2">
|
|
<MudStack Spacing="1">
|
|
@StatusIndicator(("Start Button", status.ButtonStart, false))
|
|
@StatusIndicator(("Stop Button", status.ButtonStop, true))
|
|
@StatusIndicator(("Reset Button", status.ButtonReset, false))
|
|
</MudStack>
|
|
</MudCardContent>
|
|
</MudCard>
|
|
</MudItem>
|
|
|
|
@* Other State Card *@
|
|
<MudItem xs="12" md="6" lg="4">
|
|
<MudCard Style="height: 100%;">
|
|
<MudCardHeader Class="py-2">
|
|
<CardHeaderContent>
|
|
<MudText Typo="Typo.subtitle1"><strong>Other State</strong></MudText>
|
|
</CardHeaderContent>
|
|
</MudCardHeader>
|
|
<MudCardContent Class="py-2">
|
|
<MudStack Spacing="1">
|
|
@StatusIndicator(("Has Load", status.HasLoad, false))
|
|
@StatusIndicator(("Charger Enabled", status.EnabledCharger, false))
|
|
@StatusIndicator(("Charging", status.Charging, false))
|
|
@StatusIndicator(("Muted Base", status.MutedBase, false))
|
|
@StatusIndicator(("Muted Load", status.MutedLoad, false))
|
|
</MudStack>
|
|
</MudCardContent>
|
|
</MudCard>
|
|
</MudItem>
|
|
|
|
@* Command State Card - Các lệnh đã ghi xuống PLC *@
|
|
<MudItem xs="12" md="6" lg="4">
|
|
<MudCard Style="height: 100%;">
|
|
<MudCardHeader Class="py-2">
|
|
<CardHeaderContent>
|
|
<MudText Typo="Typo.subtitle1"><strong>Command State</strong></MudText>
|
|
</CardHeaderContent>
|
|
</MudCardHeader>
|
|
<MudCardContent Class="py-2">
|
|
<MudStack Spacing="1">
|
|
<MudStack Row="true" Justify="Justify.SpaceBetween" Class="mb-1">
|
|
<MudText Typo="Typo.body2">System State:</MudText>
|
|
<MudChip T="string" Color="@GetSystemStateColor(status.CurrentSystemState)" Size="Size.Small">
|
|
@status.CurrentSystemState
|
|
</MudChip>
|
|
</MudStack>
|
|
<MudStack Row="true" Justify="Justify.SpaceBetween" Class="mb-1">
|
|
<MudText Typo="Typo.body2">Operation State:</MudText>
|
|
<MudChip T="string" Color="@GetOperationStateColor(status.CurrentOperationState)" Size="Size.Small">
|
|
@status.CurrentOperationState
|
|
</MudChip>
|
|
</MudStack>
|
|
<MudStack Row="true" Justify="Justify.SpaceBetween" Class="mb-1">
|
|
<MudText Typo="Typo.body2">RF Mode:</MudText>
|
|
<MudChip T="string" Color="@GetRFModeColor(status.CurrentRFMode)" Size="Size.Small">
|
|
@status.CurrentRFMode
|
|
</MudChip>
|
|
</MudStack>
|
|
</MudStack>
|
|
</MudCardContent>
|
|
</MudCard>
|
|
</MudItem>
|
|
|
|
@* Command Values Card - Các giá trị điều khiển đã ghi *@
|
|
<MudItem xs="12" md="6" lg="4">
|
|
<MudCard Style="height: 100%;">
|
|
<MudCardHeader Class="py-2">
|
|
<CardHeaderContent>
|
|
<MudText Typo="Typo.subtitle1"><strong>Command Values</strong></MudText>
|
|
</CardHeaderContent>
|
|
</MudCardHeader>
|
|
<MudCardContent Class="py-2">
|
|
<MudStack Spacing="1">
|
|
@StatusIndicator(("Horizontal Load", status.SetHorizontalLoadValue, false))
|
|
@StatusIndicator(("Muted Base (Set)", status.SetMutedBaseValue, false))
|
|
@StatusIndicator(("Muted Load (Set)", status.SetMutedLoadValue, false))
|
|
</MudStack>
|
|
</MudCardContent>
|
|
</MudCard>
|
|
</MudItem>
|
|
|
|
@* Command Values Card - Các giá trị điều khiển đã ghi *@
|
|
<MudItem xs="12" md="6" lg="4">
|
|
<MudCard Style="height: 100%;">
|
|
<MudCardHeader Class="py-2">
|
|
<CardHeaderContent>
|
|
<MudText Typo="Typo.subtitle1"><strong>Command Values</strong></MudText>
|
|
</CardHeaderContent>
|
|
<CardHeaderActions>
|
|
<MudIcon Icon="@Icons.Material.Filled.Settings" Color="Color.Primary" Size="Size.Small" />
|
|
</CardHeaderActions>
|
|
</MudCardHeader>
|
|
<MudCardContent Class="py-2">
|
|
<MudStack Spacing="1">
|
|
@StatusIndicator(("Charger Enable (Set)", status.SetEnableChargerValue, false))
|
|
@StatusIndicator(("Has Load (Set)", status.SetHasLoadValue, false))
|
|
@StatusIndicator(("RF E-Stop (Set)", status.SetRFEStopValue, true))
|
|
@StatusIndicator(("Batter Low (Set)", status.SetBatteryLowValue, true))
|
|
</MudStack>
|
|
</MudCardContent>
|
|
</MudCard>
|
|
</MudItem>
|
|
</MudGrid>
|
|
}
|
|
</MudContainer>
|
|
</div>
|
|
</div>
|
|
|
|
<MudThemeProvider IsDarkMode />
|
|
<MudPopoverProvider />
|
|
<MudDialogProvider />
|
|
<MudSnackbarProvider />
|
|
|
|
@code {
|
|
[Inject] private IServiceProvider ServiceProvider { get; set; } = null!;
|
|
[Inject] private ISnackbar Snackbar { get; set; } = null!;
|
|
[Inject] private HttpClient Http { get; set; } = null!;
|
|
|
|
private PlcControllerHubClient? hubClient;
|
|
private PlcControllerStatusDto? status;
|
|
private bool isLoading = false;
|
|
private bool isHubReady = false;
|
|
private bool isUpdateEnabled = false;
|
|
private System.Threading.Timer? updateTimer;
|
|
private bool _disposed = false;
|
|
|
|
protected override async Task OnInitializedAsync()
|
|
{
|
|
hubClient = ServiceProvider.GetService<PlcControllerHubClient>();
|
|
if (hubClient is null)
|
|
{
|
|
isHubReady = false;
|
|
return; // PLC hub client not registered (disconnected)
|
|
}
|
|
|
|
// Subscribe to connection state changes
|
|
hubClient.ConnectionStateChanged += OnConnectionStateChanged;
|
|
|
|
try
|
|
{
|
|
await hubClient.StartAsync();
|
|
isHubReady = hubClient.IsConnected;
|
|
|
|
// Wait a bit for connection to establish
|
|
await Task.Delay(500);
|
|
isHubReady = hubClient.IsConnected;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Snackbar.Add($"Error initializing: {ex.Message}", Severity.Error);
|
|
isHubReady = false;
|
|
}
|
|
}
|
|
|
|
private void OnConnectionStateChanged(HubConnectionState state)
|
|
{
|
|
isHubReady = state == HubConnectionState.Connected;
|
|
|
|
// Stop update if disconnected
|
|
if (!isHubReady && isUpdateEnabled)
|
|
{
|
|
DisableUpdate();
|
|
}
|
|
|
|
InvokeAsync(StateHasChanged);
|
|
}
|
|
|
|
private void EnableUpdate()
|
|
{
|
|
if (_disposed || isUpdateEnabled || !isHubReady)
|
|
return;
|
|
|
|
isUpdateEnabled = true;
|
|
|
|
// Start timer at 2Hz (500ms)
|
|
updateTimer = new System.Threading.Timer(
|
|
async _ => await RefreshStatus(),
|
|
null,
|
|
TimeSpan.Zero,
|
|
TimeSpan.FromMilliseconds(500));
|
|
|
|
StateHasChanged();
|
|
}
|
|
|
|
private void DisableUpdate()
|
|
{
|
|
if (!isUpdateEnabled)
|
|
return;
|
|
|
|
isUpdateEnabled = false;
|
|
|
|
// Stop and dispose timer
|
|
updateTimer?.Change(Timeout.Infinite, Timeout.Infinite);
|
|
updateTimer?.Dispose();
|
|
updateTimer = null;
|
|
|
|
StateHasChanged();
|
|
}
|
|
|
|
private async Task RefreshStatus()
|
|
{
|
|
if (_disposed || isLoading || hubClient is null || !isHubReady)
|
|
return;
|
|
|
|
try
|
|
{
|
|
isLoading = true;
|
|
status = await hubClient.GetStatusAsync();
|
|
await InvokeAsync(StateHasChanged);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Snackbar.Add($"Error refreshing status: {ex.Message}", Severity.Error);
|
|
isHubReady = false;
|
|
DisableUpdate();
|
|
}
|
|
finally
|
|
{
|
|
isLoading = false;
|
|
}
|
|
}
|
|
|
|
private Color GetModeColor(string mode)
|
|
{
|
|
return mode switch
|
|
{
|
|
"AUTOMATIC" => Color.Success,
|
|
"MANUAL" => Color.Warning,
|
|
"SERVICE" => Color.Info,
|
|
_ => Color.Default
|
|
};
|
|
}
|
|
|
|
private Color GetSpeedColor(string speed)
|
|
{
|
|
return speed switch
|
|
{
|
|
"Very_Slow" => Color.Error,
|
|
"Slow" => Color.Warning,
|
|
"Normal" => Color.Default,
|
|
"Medium" => Color.Info,
|
|
"Optimal" => Color.Success,
|
|
"Fast" => Color.Primary,
|
|
"Very_Fast" => Color.Secondary,
|
|
_ => Color.Default
|
|
};
|
|
}
|
|
|
|
private Color GetStopStateColor(string state)
|
|
{
|
|
return state switch
|
|
{
|
|
"None" => Color.Success,
|
|
"EMC" => Color.Error,
|
|
"Bumper" => Color.Error,
|
|
"FrontProtective" => Color.Warning,
|
|
"BackProtective" => Color.Warning,
|
|
"TimProtective" => Color.Warning,
|
|
_ => Color.Default
|
|
};
|
|
}
|
|
|
|
private Color GetSystemStateColor(string state)
|
|
{
|
|
return state switch
|
|
{
|
|
"INIT" => Color.Info,
|
|
"IDLE" => Color.Default,
|
|
"PAUSED" => Color.Warning,
|
|
"PROCCESSING" => Color.Primary,
|
|
"DOCKING" => Color.Secondary,
|
|
"CHARGING" => Color.Tertiary,
|
|
"MAINTENANCE" => Color.Warning,
|
|
"MANUAL" => Color.Info,
|
|
"OVERRIDE" => Color.Warning,
|
|
"ERROR" => Color.Error,
|
|
_ => Color.Default
|
|
};
|
|
}
|
|
|
|
private Color GetOperationStateColor(string state)
|
|
{
|
|
return state switch
|
|
{
|
|
"Move" => Color.Primary,
|
|
"Lifting" => Color.Info,
|
|
"LiftRotating" => Color.Secondary,
|
|
"None" => Color.Default,
|
|
_ => Color.Default
|
|
};
|
|
}
|
|
|
|
private Color GetRFModeColor(string mode)
|
|
{
|
|
return mode switch
|
|
{
|
|
"Default" => Color.Success,
|
|
"Maintenance" => Color.Warning,
|
|
"Override" => Color.Error,
|
|
"None" => Color.Default,
|
|
_ => Color.Default
|
|
};
|
|
}
|
|
|
|
public async ValueTask DisposeAsync()
|
|
{
|
|
if (_disposed)
|
|
return;
|
|
|
|
_disposed = true;
|
|
|
|
// Stop update timer
|
|
DisableUpdate();
|
|
|
|
// Unsubscribe from events
|
|
if (hubClient != null)
|
|
{
|
|
hubClient.ConnectionStateChanged -= OnConnectionStateChanged;
|
|
}
|
|
}
|
|
}
|
|
|
|
@* Status Indicator Component *@
|
|
@code {
|
|
private RenderFragment<(string Label, bool Value, bool DangerWhenTrue)> StatusIndicator => context =>
|
|
@<MudStack Row="true" Justify="Justify.SpaceBetween" Class="mb-1">
|
|
<MudText Typo="Typo.body2">@context.Label:</MudText>
|
|
<MudChip T="bool"
|
|
Color="@(context.Value ? (context.DangerWhenTrue ? Color.Error : Color.Success) : Color.Default)"
|
|
Size="Size.Small">
|
|
@(context.Value ? "ON" : "OFF")
|
|
</MudChip>
|
|
</MudStack>;
|
|
}
|