RobotApp/RobotApp.Client/Pages/State.razor
2025-12-21 11:33:11 +07:00

374 lines
17 KiB
Plaintext

@page "/robot-state"
@using RobotApp.Client.Services
@using RobotApp.VDA5050.State
@inject RobotStateClient RobotStateClient
@implements IDisposable
@rendermode InteractiveWebAssembly
<MudContainer MaxWidth="MaxWidth.False" Class="pa-4">
<!-- ===================================================== -->
<!-- HEADER -->
<!-- ===================================================== -->
<MudPaper Class="pa-6 mb-4 d-flex align-center justify-space-between" Elevation="3">
<div>
<MudText Typo="Typo.h4">🤖 VDA 5050 Robot Dashboard</MudText>
@if (CurrentState != null)
{
<MudText Typo="Typo.subtitle2" Color="Color.Secondary">
@CurrentState.Version •
@CurrentState.Manufacturer •
@CurrentState.SerialNumber
</MudText>
}
else
{
<MudText Typo="Typo.subtitle2" Color="Color.Secondary">
Connecting to robot...
</MudText>
}
</div>
@if (CurrentState != null)
{
<MudChip T="string" Size="Size.Large"
Color="@(IsConnected ? Color.Success : Color.Error)"
Variant="Variant.Filled">
@(IsConnected ? "ONLINE" : "OFFLINE")
</MudChip>
}
</MudPaper>
@if (CurrentState == null)
{
<MudAlert Severity="Severity.Info" Variant="Variant.Outlined" Class="mb-4">
<MudAlertTitle>Waiting for robot state data...</MudAlertTitle>
Connecting to SignalR hub and subscribing to robot updates.
<MudProgressLinear Indeterminate Class="mt-3" />
</MudAlert>
}
else
{
var msg = CurrentState;
<!-- ===================================================== -->
<!-- MESSAGE META -->
<!-- ===================================================== -->
<MudPaper Class="pa-3 mb-4" Elevation="1">
<MudGrid Spacing="2">
<MudItem xs="12" md="3">
<MudText Typo="Typo.caption">HeaderId</MudText>
<MudText>@msg.HeaderId</MudText>
</MudItem>
<MudItem xs="12" md="5">
<MudText Typo="Typo.caption">Timestamp (UTC)</MudText>
<MudText>@msg.Timestamp</MudText>
</MudItem>
<MudItem xs="12" md="2">
<MudText Typo="Typo.caption">Version</MudText>
<MudText>@msg.Version</MudText>
</MudItem>
<MudItem xs="12" md="2">
<MudText Typo="Typo.caption">OrderUpdateId</MudText>
<MudChip T="string" Color="Color.Primary">@msg.OrderUpdateId</MudChip>
</MudItem>
</MudGrid>
</MudPaper>
<!-- ===================================================== -->
<!-- MAIN GRID -->
<!-- ===================================================== -->
<MudGrid Spacing="4">
<!-- POSITION + VELOCITY -->
<MudItem xs="12" md="6" lg="4">
<MudPaper Class="pa-5 h-100" Elevation="2">
<MudGrid AlignItems="AlignItems.Center">
<MudItem xs="8">
<MudText Typo="Typo.h6">📍 Position & Velocity</MudText>
</MudItem>
<MudItem xs="4" Class="d-flex justify-end">
<MudChip T="string" Size="Size.Small"
Color="@(msg.NewBaseRequest ? Color.Success : Color.Error)">
NewBase: @(msg.NewBaseRequest ? "TRUE" : "FALSE")
</MudChip>
</MudItem>
</MudGrid>
<MudDivider Class="my-3" />
<MudGrid Spacing="1">
<MudItem xs="6">
<MudText>X: <b>@msg.AgvPosition.X.ToString("F2")</b> m</MudText>
<MudText>Y: <b>@msg.AgvPosition.Y.ToString("F2")</b> m</MudText>
<MudText>θ: <b>@msg.AgvPosition.Theta.ToString("F2")</b> rad</MudText>
</MudItem>
<MudItem xs="6">
<MudText>Vx: <b>@msg.Velocity.Vx.ToString("F2")</b> m/s</MudText>
<MudText>Vy: <b>@msg.Velocity.Vy.ToString("F2")</b> m/s</MudText>
<MudText>Ω: <b>@msg.Velocity.Omega.ToString("F3")</b> rad/s</MudText>
</MudItem>
</MudGrid>
<MudDivider Class="my-3" />
<MudGrid Spacing="1">
<MudItem xs="6">
<MudChip T="string" Size="Size.Small"
Color="@(msg.AgvPosition.PositionInitialized ? Color.Success : Color.Error)">
Initialized: @(msg.AgvPosition.PositionInitialized ? "TRUE" : "FALSE")
</MudChip>
</MudItem>
<MudItem xs="6" Class="d-flex justify-end">
<MudText Typo="Typo.caption">
Deviation: <b>@msg.AgvPosition.DeviationRange</b>
</MudText>
</MudItem>
</MudGrid>
<MudProgressLinear Value="@(msg.AgvPosition.LocalizationScore * 100)"
Class="mt-3"
Color="Color.Success" />
<MudText Typo="Typo.caption">
Localization Score: @(msg.AgvPosition.LocalizationScore * 100) %
</MudText>
</MudPaper>
</MudItem>
<!-- BATTERY -->
<MudItem xs="12" md="6" lg="4">
<MudPaper Class="pa-5 h-100" Elevation="2">
<MudText Typo="Typo.h6">🔋 Battery</MudText>
<MudDivider Class="my-3" />
<MudProgressLinear Value="@msg.BatteryState.BatteryCharge"
Size="Size.Large"
Rounded
Color="@(msg.BatteryState.BatteryCharge > 50 ? Color.Success :
msg.BatteryState.BatteryCharge > 20 ? Color.Warning : Color.Error)" />
<MudText Typo="Typo.h4" Class="mt-2">
@msg.BatteryState.BatteryCharge:F1 %
</MudText>
<MudChip T="string" Size="Size.Small"
Color="@(msg.BatteryState.Charging ? Color.Info : Color.Default)">
@(msg.BatteryState.Charging ? "⚡ Charging" : "Discharging")
</MudChip>
<MudDivider Class="my-3" />
<MudGrid Spacing="1">
<MudItem xs="4">
<MudText Typo="Typo.caption">Voltage</MudText>
<MudText><b>@msg.BatteryState.BatteryVoltage.ToString("F1")</b> V</MudText>
</MudItem>
<MudItem xs="4">
<MudText Typo="Typo.caption">Health (SOH)</MudText>
<MudText><b>@msg.BatteryState.BatteryHealth</b> %</MudText>
</MudItem>
<MudItem xs="4">
<MudText Typo="Typo.caption">Reach</MudText>
<MudText><b>@((int)msg.BatteryState.Reach)</b> m</MudText>
</MudItem>
</MudGrid>
</MudPaper>
</MudItem>
<!-- ORDER & PATH -->
<MudItem xs="12" md="6" lg="4">
<MudPaper Class="pa-5 h-100" Elevation="2">
<MudText Typo="Typo.h6">🧭 Order & Path</MudText>
<MudDivider Class="my-3" />
<MudText>Order ID: <b>@(msg.OrderId ?? "—")</b></MudText>
<MudText>Update ID: <b>@msg.OrderUpdateId</b></MudText>
<MudDivider Class="my-2" />
<MudText>
Last Node: <b>@msg.LastNodeId</b>
<MudText Typo="Typo.caption" Inline="true">Seq: @msg.LastNodeSequenceId</MudText>
</MudText>
<MudText>Distance since last node: @msg.DistanceSinceLastNode:F1 m</MudText>
<MudDivider Class="my-3" />
@{
var nodeReleased = msg.NodeStates?.Count(n => n.Released) ?? 0;
var nodeTotal = msg.NodeStates?.Length ?? 0;
var edgeReleased = msg.EdgeStates?.Count(e => e.Released) ?? 0;
var edgeTotal = msg.EdgeStates?.Length ?? 0;
}
<div class="d-flex align-center flex-wrap gap-2">
<MudChip T="string" Color="Color.Info">Nodes: @nodeReleased / @nodeTotal</MudChip>
<MudChip T="string" Color="Color.Info">Edges: @edgeReleased / @edgeTotal</MudChip>
<MudChip T="string" Size="Size.Small" Color="@(msg.Driving ? Color.Success : Color.Default)">
@(msg.Driving ? "DRIVING" : "STOPPED")
</MudChip>
<MudChip T="string" Size="Size.Small" Color="@(msg.Paused ? Color.Warning : Color.Success)">
@(msg.Paused ? "PAUSED" : "ACTIVE")
</MudChip>
</div>
</MudPaper>
</MudItem>
<!-- ERRORS + INFORMATION -->
<MudItem xs="12" md="12" lg="6">
<MudPaper Class="pa-5 h-100" Elevation="2">
<MudText Typo="Typo.h6">🚨 Errors & Information</MudText>
<MudDivider Class="my-3" />
<MudTable Items="@MessageRows"
Dense="true"
Hover="true"
Bordered="true"
Elevation="0"
Height="200px"
FixedHeader="true">
<ColGroup>
<col style="width: 35%" />
<col style="width: 20%" />
<col />
</ColGroup>
<HeaderContent>
<MudTh>Type</MudTh>
<MudTh>Level</MudTh>
<MudTh>Description</MudTh>
</HeaderContent>
<RowTemplate>
<MudTd>
<MudText Class="@(context.IsError ? "text-error" : "text-info")">
<b>@context.Type</b>
</MudText>
</MudTd>
<MudTd>
<MudChip T="string" Size="Size.Small"
Color="@(context.Level.Contains("ERROR", StringComparison.OrdinalIgnoreCase) ? Color.Error :
context.Level.Contains("WARN", StringComparison.OrdinalIgnoreCase) ? Color.Warning :
Color.Info)">
@context.Level
</MudChip>
</MudTd>
<MudTd>
<MudText Typo="Typo.caption" Class="text-truncate" Title="@context.Description">
@context.Description
</MudText>
</MudTd>
</RowTemplate>
<NoRecordsContent>
<MudText Typo="Typo.caption" Color="Color.Secondary" Class="pa-4 text-center">
No errors or information messages
</MudText>
</NoRecordsContent>
</MudTable>
</MudPaper>
</MudItem>
<!-- ACTIONS -->
<MudItem xs="12" md="6" lg="3">
<MudPaper Class="pa-5 h-100" Elevation="2">
<MudText Typo="Typo.h6">⚙️ Actions</MudText>
<MudDivider Class="my-3" />
<MudTable Items="msg.ActionStates"
Dense="true"
Hover="true"
Bordered="true"
Elevation="0"
Height="180px"
FixedHeader="true">
<HeaderContent>
<MudTh>Action</MudTh>
<MudTh>Action ID</MudTh>
<MudTh class="text-right">Status</MudTh>
</HeaderContent>
<RowTemplate>
<MudTd><MudText Typo="Typo.body2" Class="text-truncate" Title="@context.ActionType">@context.ActionType</MudText></MudTd>
<MudTd><MudText Typo="Typo.caption">@context.ActionId</MudText></MudTd>
<MudTd Class="text-right">
<MudChip T="string" Size="Size.Small"
Color="@(context.ActionStatus == "RUNNING" ? Color.Info :
context.ActionStatus == "FINISHED" ? Color.Success :
context.ActionStatus == "FAILED" ? Color.Error : Color.Default)">
@context.ActionStatus
</MudChip>
</MudTd>
</RowTemplate>
<NoRecordsContent>
<MudText Typo="Typo.caption" Color="Color.Secondary" Class="pa-4 text-center">
No active actions
</MudText>
</NoRecordsContent>
</MudTable>
</MudPaper>
</MudItem>
<!-- SAFETY -->
<MudItem xs="12" md="6" lg="3">
<MudPaper Class="pa-5 h-100" Elevation="2">
<MudText Typo="Typo.h6">🛑 Safety</MudText>
<MudDivider Class="my-3" />
<MudChip T="string" Size="Size.Large" Class="w-100 mb-2"
Color="@(msg.SafetyState.EStop == "NONE" ? Color.Success : Color.Error)">
E-STOP: @msg.SafetyState.EStop
</MudChip>
<MudChip T="string" Size="Size.Large" Class="w-100"
Color="@(msg.SafetyState.FieldViolation ? Color.Error : Color.Success)">
Field Violation: @(msg.SafetyState.FieldViolation ? "YES" : "NO")
</MudChip>
</MudPaper>
</MudItem>
</MudGrid>
}
</MudContainer>
@code {
private StateMsg? CurrentState;
private bool IsConnected => RobotStateClient.LatestStates.ContainsKey(RobotSerial);
// Thay bằng serial number thật của robot bạn muốn theo dõi
private readonly string RobotSerial = "T800-002";
private List<MessageRow> MessageRows = new();
protected override async Task OnInitializedAsync()
{
// Subscribe sự kiện nhận state
RobotStateClient.OnStateReceived += OnRobotStateReceived;
// Bắt đầu kết nối SignalR (nếu chưa)
if (RobotStateClient.LatestStates.Count == 0)
{
await RobotStateClient.StartAsync();
}
// Đăng ký theo dõi robot cụ thể
await RobotStateClient.SubscribeRobotAsync(RobotSerial);
// Lấy state hiện tại nếu đã có
CurrentState = RobotStateClient.GetLatestState(RobotSerial);
UpdateMessageRows();
}
private void OnRobotStateReceived(string serialNumber, StateMsg state)
{
if (serialNumber != RobotSerial) return;
InvokeAsync(() =>
{
CurrentState = state;
UpdateMessageRows();
StateHasChanged();
});
}
private void UpdateMessageRows()
{
MessageRows.Clear();
if (CurrentState?.Errors != null)
{
foreach (var err in CurrentState.Errors)
{
MessageRows.Add(new MessageRow(err.ErrorType ?? "-", err.ErrorLevel ?? "ERROR", err.ErrorDescription ?? "", true));
}
}
if (CurrentState?.Information != null)
{
foreach (var info in CurrentState.Information)
{
MessageRows.Add(new MessageRow(info.InfoType ?? "-", info.InfoLevel ?? "INFO", info.InfoDescription ?? "", false));
}
}
}
public void Dispose()
{
RobotStateClient.OnStateReceived -= OnRobotStateReceived;
}
private record MessageRow(string Type, string Level, string Description, bool IsError);
}