Files
Denso/srcs/RobotNet10/RobotApp/RobotNet10.RobotApp.Client/Pages/PlcController.razor
2026-07-03 16:31:37 +07:00

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>;
}