Initial commit
This commit is contained in:
@@ -0,0 +1,504 @@
|
||||
@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>;
|
||||
}
|
||||
Reference in New Issue
Block a user