444 lines
21 KiB
Plaintext
444 lines
21 KiB
Plaintext
@page "/dashboard"
|
|
|
|
@using RobotApp.Client.Services
|
|
@using RobotApp.VDA5050.State
|
|
@using MudBlazor
|
|
|
|
@implements IDisposable
|
|
@attribute [Authorize]
|
|
|
|
@inject RobotStateClient RobotStateClient
|
|
@rendermode InteractiveWebAssemblyNoPrerender
|
|
|
|
<MudContainer MaxWidth="MaxWidth.False" Class="pa-4" Style="overflow-y:auto">
|
|
<!-- Header Dashboard -->
|
|
<MudPaper Class="pa-6 mb-4 d-flex align-center justify-space-between" Elevation="3">
|
|
<div>
|
|
<MudText Typo="Typo.h3" Class="mb-2" >Robot Dashboard</MudText>
|
|
@if (CurrentState != null)
|
|
{
|
|
<MudText Typo="Typo.subtitle1">
|
|
<MudIcon Icon="@Icons.Material.Filled.Memory" Class="mr-2" />
|
|
@CurrentState.Version •
|
|
<MudIcon Icon="@Icons.Material.Filled.Business" Class="mr-2 ml-4" />
|
|
@CurrentState.Manufacturer •
|
|
<MudIcon Icon="@Icons.Material.Filled.Tag" Class="mr-2 ml-4" />
|
|
@CurrentState.SerialNumber
|
|
</MudText>
|
|
}
|
|
else
|
|
{
|
|
<MudText Typo="Typo.subtitle1">
|
|
<MudIcon Icon="@Icons.Material.Filled.Sync" Class="mr-2" />
|
|
Connecting to robot...
|
|
</MudText>
|
|
}
|
|
</div>
|
|
|
|
@if (CurrentState != null)
|
|
{
|
|
<MudChip T="string"
|
|
Icon="@(IsConnected
|
|
? Icons.Material.Filled.CheckCircle
|
|
: Icons.Material.Filled.Error)"
|
|
Size="Size.Large"
|
|
Color="@(IsConnected ? Color.Success : Color.Error)"
|
|
Variant="Variant.Filled"
|
|
Class="px-6 py-4 text-white"
|
|
Style="font-weight: bold; font-size: 1.1rem;">
|
|
@(IsConnected ? "ONLINE" : "OFFLINE")
|
|
</MudChip>
|
|
}
|
|
|
|
</MudPaper>
|
|
|
|
@{
|
|
var msg = CurrentState ?? EmptyState;
|
|
}
|
|
|
|
<!-- Thông tin header state -->
|
|
<MudPaper Class="pa-5 mb-6 rounded-lg" Elevation="4">
|
|
<MudGrid Spacing="4" Class="align-center">
|
|
<MudItem xs="12" sm="4">
|
|
<MudText Typo="Typo.caption" Color="Color.Tertiary">Header ID</MudText>
|
|
<MudText Typo="Typo.h6">@msg.HeaderId</MudText>
|
|
</MudItem>
|
|
<MudItem xs="12" sm="4">
|
|
<MudText Typo="Typo.caption" Color="Color.Tertiary">Timestamp (UTC)</MudText>
|
|
<MudText Typo="Typo.h6">@msg.Timestamp</MudText>
|
|
</MudItem>
|
|
<MudItem xs="12" sm="2">
|
|
<MudText Typo="Typo.caption" Color="Color.Tertiary">Version</MudText>
|
|
<MudText Typo="Typo.h6">@msg.Version</MudText>
|
|
</MudItem>
|
|
<MudItem xs="12" sm="2" Class="d-flex justify-center">
|
|
<MudText Typo="Typo.caption" Color="Color.Tertiary">Order Update ID</MudText>
|
|
<MudChip T="string" Color="Color.Primary" Variant="@Variant.Filled" Class="mt-2">
|
|
@msg.OrderUpdateId
|
|
</MudChip>
|
|
</MudItem>
|
|
</MudGrid>
|
|
</MudPaper>
|
|
|
|
<MudGrid Spacing="4">
|
|
<!-- POSITION + VELOCITY -->
|
|
<MudItem xs="12" md="6" lg="4">
|
|
<MudCard Elevation="6" Class="h-100 rounded-lg">
|
|
<MudCardHeader>
|
|
<CardHeaderContent>
|
|
<MudText Typo="Typo.h6">
|
|
<MudIcon Icon="@Icons.Material.Filled.LocationOn" Class="mr-2" />
|
|
Position & Velocity
|
|
</MudText>
|
|
</CardHeaderContent>
|
|
<CardHeaderActions>
|
|
<MudChip T="string"
|
|
Size="Size.Small"
|
|
Color="@(msg.NewBaseRequest ? Color.Success : Color.Error)"
|
|
Variant="@Variant.Filled">
|
|
NewBase: @(msg.NewBaseRequest ? "TRUE" : "FALSE")
|
|
</MudChip>
|
|
</CardHeaderActions>
|
|
</MudCardHeader>
|
|
<MudCardContent Class="pa-4">
|
|
<MudGrid Spacing="3">
|
|
<MudItem xs="6">
|
|
<MudText>X: <strong>@msg.AgvPosition.X.ToString("F2")</strong> m</MudText>
|
|
<MudText>Y: <strong>@msg.AgvPosition.Y.ToString("F2")</strong> m</MudText>
|
|
<MudText>θ: <strong>@msg.AgvPosition.Theta.ToString("F2")</strong> rad</MudText>
|
|
</MudItem>
|
|
<MudItem xs="6">
|
|
<MudText>Vx: <strong>@msg.Velocity.Vx.ToString("F2")</strong> m/s</MudText>
|
|
<MudText>Vy: <strong>@msg.Velocity.Vy.ToString("F2")</strong> m/s</MudText>
|
|
<MudText>Ω: <strong>@msg.Velocity.Omega.ToString("F3")</strong> rad/s</MudText>
|
|
</MudItem>
|
|
</MudGrid>
|
|
<MudDivider Class="my-4" />
|
|
<div class="d-flex justify-space-between align-center">
|
|
<MudChip T="string"
|
|
Size="Size.Small"
|
|
Color="@(msg.AgvPosition.PositionInitialized ? Color.Success : Color.Error)"
|
|
Variant="@Variant.Filled">
|
|
@(msg.AgvPosition.PositionInitialized ? "Initialized" : "Not Initialized")
|
|
</MudChip>
|
|
<MudText Typo="Typo.caption">
|
|
Deviation: <strong>@msg.AgvPosition.DeviationRange</strong>
|
|
</MudText>
|
|
</div>
|
|
<MudProgressLinear Value="@(msg.AgvPosition.LocalizationScore * 100)"
|
|
Color="Color.Success"
|
|
Class="mt-4 rounded"
|
|
Style="height: 10px;" />
|
|
<MudText Typo="Typo.caption" Class="mt-2 text-center">
|
|
Localization Score: <strong>@(msg.AgvPosition.LocalizationScore * 100)%</strong>
|
|
</MudText>
|
|
</MudCardContent>
|
|
</MudCard>
|
|
</MudItem>
|
|
|
|
<!-- BATTERY -->
|
|
<MudItem xs="12" md="6" lg="4">
|
|
<MudCard Elevation="6" Class="h-100 rounded-lg">
|
|
<MudCardHeader>
|
|
<CardHeaderContent>
|
|
<MudText Typo="Typo.h6">
|
|
<MudIcon Icon="@Icons.Material.Filled.BatteryFull" Class="mr-2" />
|
|
Battery Status
|
|
</MudText>
|
|
</CardHeaderContent>
|
|
</MudCardHeader>
|
|
<MudCardContent Class="pa-6">
|
|
<MudProgressLinear Value="@msg.BatteryState.BatteryCharge"
|
|
Size="Size.Large"
|
|
Rounded="true"
|
|
Striped="true"
|
|
Color="@(msg.BatteryState.BatteryCharge > 50 ? Color.Success :
|
|
msg.BatteryState.BatteryCharge > 20 ? Color.Warning : Color.Error)"
|
|
Class="mb-4"
|
|
Style="height: 28px;" />
|
|
|
|
<!-- Phần mới: % pin và trạng thái Charging/Discharging cùng dòng -->
|
|
<div class="d-flex align-center justify-space-between mb-6">
|
|
<MudText Typo="Typo.h3" Class="mb-0">
|
|
@msg.BatteryState.BatteryCharge<span class="mud-typography-h4">%</span>
|
|
</MudText>
|
|
|
|
<MudChip T="string"
|
|
Icon="@(msg.BatteryState.Charging ? Icons.Material.Filled.Bolt : Icons.Material.Filled.PowerOff)"
|
|
Size="Size.Large"
|
|
Color="@(msg.BatteryState.Charging ? Color.Info : Color.Default)"
|
|
Variant="@Variant.Filled"
|
|
Class="px-5 py-3">
|
|
@(msg.BatteryState.Charging ? "Charging" : "Discharging")
|
|
</MudChip>
|
|
</div>
|
|
|
|
<!-- Các thông tin phụ -->
|
|
<MudGrid Spacing="3">
|
|
<MudItem xs="4">
|
|
<MudText Typo="Typo.caption">Voltage</MudText>
|
|
<MudText Typo="Typo.h6">@msg.BatteryState.BatteryVoltage:F1 V</MudText>
|
|
</MudItem>
|
|
<MudItem xs="4">
|
|
<MudText Typo="Typo.caption">Health (SOH)</MudText>
|
|
<MudText Typo="Typo.h6">@msg.BatteryState.BatteryHealth%</MudText>
|
|
</MudItem>
|
|
<MudItem xs="4">
|
|
<MudText Typo="Typo.caption">Reach</MudText>
|
|
<MudText Typo="Typo.h6">@((int)msg.BatteryState.Reach) m</MudText>
|
|
</MudItem>
|
|
</MudGrid>
|
|
</MudCardContent>
|
|
</MudCard>
|
|
</MudItem>
|
|
<!-- ORDER & PATH -->
|
|
<MudItem xs="12" md="6" lg="4">
|
|
<MudCard Elevation="6" Class="h-100 rounded-lg">
|
|
<MudCardHeader>
|
|
<CardHeaderContent>
|
|
<MudText Typo="Typo.h6">
|
|
<MudIcon Icon="@Icons.Material.Filled.Navigation" Class="mr-2" />
|
|
Order & Path
|
|
</MudText>
|
|
</CardHeaderContent>
|
|
</MudCardHeader>
|
|
<MudCardContent Class="pa-4">
|
|
<MudText>Order ID: <strong>@(msg.OrderId ?? "—")</strong></MudText>
|
|
<MudText>Update ID: <strong>@msg.OrderUpdateId</strong></MudText>
|
|
<MudDivider Class="my-4" />
|
|
<MudText>Last Node: <strong>@msg.LastNodeId</strong> <span class="mud-typography-caption">(Seq: @msg.LastNodeSequenceId)</span></MudText>
|
|
<MudText>Distance since last: <strong>@msg.DistanceSinceLastNode m</strong></MudText>
|
|
<MudDivider Class="my-4" />
|
|
@{
|
|
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 flex-wrap gap-3 mt-3">
|
|
<MudChip T="string" Color="Color.Primary" Variant="@Variant.Filled">Nodes: @nodeReleased/@nodeTotal</MudChip>
|
|
<MudChip T="string" Color="Color.Secondary" Variant="@Variant.Filled">Edges: @edgeReleased/@edgeTotal</MudChip>
|
|
<MudChip T="string" Size="Size.Small" Color="@(msg.Driving? Color.Success: Color.Default)" Variant="@Variant.Filled">
|
|
@(msg.Driving ? "DRIVING" : "STOPPED")
|
|
</MudChip>
|
|
<MudChip T="string" Size="Size.Small" Color="@(msg.Paused? Color.Warning: Color.Success)" Variant="@Variant.Filled">
|
|
@(msg.Paused ? "PAUSED" : "ACTIVE")
|
|
</MudChip>
|
|
</div>
|
|
</MudCardContent>
|
|
</MudCard>
|
|
</MudItem>
|
|
|
|
<!-- ERRORS + INFORMATION -->
|
|
<MudItem xs="12" lg="6">
|
|
<MudCard Elevation="6" Class="h-100 rounded-lg">
|
|
<MudCardHeader>
|
|
<CardHeaderContent>
|
|
<MudText Typo="Typo.h6">
|
|
<MudIcon Icon="@Icons.Material.Filled.WarningAmber" Class="mr-2" />
|
|
Errors & Information
|
|
</MudText>
|
|
</CardHeaderContent>
|
|
</MudCardHeader>
|
|
<MudCardContent>
|
|
<MudTable Items="@MessageRows"
|
|
Dense="true"
|
|
Hover="true"
|
|
Bordered="true"
|
|
FixedHeader="true"
|
|
Height="250px">
|
|
<ColGroup>
|
|
<col style="width:30%" />
|
|
<col style="width:15%" />
|
|
<col />
|
|
</ColGroup>
|
|
<HeaderContent>
|
|
<MudTh><MudText Typo="Typo.subtitle2">Type</MudText></MudTh>
|
|
<MudTh><MudText Typo="Typo.subtitle2">Level</MudText></MudTh>
|
|
<MudTh><MudText Typo="Typo.subtitle2">Description</MudText></MudTh>
|
|
</HeaderContent>
|
|
<RowTemplate>
|
|
<MudTd>
|
|
<MudText Color="@(context.IsError? Color.Error: Color.Info)" Typo="Typo.body1">
|
|
<strong>@context.Type</strong>
|
|
</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)"
|
|
Variant="@Variant.Filled">
|
|
@context.Level
|
|
</MudChip>
|
|
</MudTd>
|
|
<MudTd>
|
|
<MudText Typo="Typo.body2" Class="text-truncate" Style="max-width: 300px;" Title="@context.Description">
|
|
@context.Description
|
|
</MudText>
|
|
</MudTd>
|
|
</RowTemplate>
|
|
<NoRecordsContent>
|
|
<MudAlert Severity="Severity.Info" Class="mx-4 my-8" Variant="@Variant.Text">
|
|
<MudText>No errors or information messages</MudText>
|
|
</MudAlert>
|
|
</NoRecordsContent>
|
|
</MudTable>
|
|
</MudCardContent>
|
|
</MudCard>
|
|
</MudItem>
|
|
|
|
<!-- ACTIONS -->
|
|
<MudItem xs="12" md="6" lg="3">
|
|
<MudCard Elevation="6" Class="h-100 rounded-lg">
|
|
<MudCardHeader>
|
|
<CardHeaderContent>
|
|
<MudText Typo="Typo.h6">
|
|
<MudIcon Icon="@Icons.Material.Filled.Settings" Class="mr-2" />
|
|
Active Actions
|
|
</MudText>
|
|
</CardHeaderContent>
|
|
</MudCardHeader>
|
|
<MudCardContent>
|
|
<MudTable Items="msg.ActionStates"
|
|
Dense="true"
|
|
Hover="true"
|
|
Bordered="true"
|
|
FixedHeader="true"
|
|
Height="220px">
|
|
<HeaderContent>
|
|
<MudTh>Action</MudTh>
|
|
<MudTh>ID</MudTh>
|
|
<MudTh Style="text-align: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 Style="text-align: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)"
|
|
Variant="@Variant.Filled">
|
|
@context.ActionStatus
|
|
</MudChip>
|
|
</MudTd>
|
|
</RowTemplate>
|
|
<NoRecordsContent>
|
|
<MudText Class="pa-8 text-center" Color="Color.Secondary">No active actions</MudText>
|
|
</NoRecordsContent>
|
|
</MudTable>
|
|
</MudCardContent>
|
|
</MudCard>
|
|
</MudItem>
|
|
|
|
<!-- SAFETY -->
|
|
<MudItem xs="12" md="6" lg="3">
|
|
<MudCard Elevation="6" Class="h-100 rounded-lg">
|
|
<MudCardHeader>
|
|
<CardHeaderContent>
|
|
<MudText Typo="Typo.h6">
|
|
<MudIcon Icon="@Icons.Material.Filled.Security" Class="mr-2" />
|
|
Safety State
|
|
</MudText>
|
|
</CardHeaderContent>
|
|
</MudCardHeader>
|
|
<MudCardContent Class="pa-6 d-flex flex-column gap-4">
|
|
<MudChip T="string"
|
|
Icon="@(msg.SafetyState.EStop == "NONE" ? Icons.Material.Filled.CheckCircle : Icons.Material.Filled.ErrorOutline)"
|
|
Size="Size.Large"
|
|
Color="@(msg.SafetyState.EStop == "NONE" ? Color.Success : Color.Error)"
|
|
Variant="@Variant.Filled"
|
|
Class="py-6">
|
|
E-STOP: @msg.SafetyState.EStop
|
|
</MudChip>
|
|
<MudChip T="string"
|
|
Icon="@(msg.SafetyState.FieldViolation ? Icons.Material.Filled.Warning : Icons.Material.Filled.Shield)"
|
|
Size="Size.Large"
|
|
Color="@(msg.SafetyState.FieldViolation ? Color.Error : Color.Success)"
|
|
Variant="@Variant.Filled"
|
|
Class="py-6">
|
|
Field Violation: @(msg.SafetyState.FieldViolation ? "YES" : "NO")
|
|
</MudChip>
|
|
</MudCardContent>
|
|
</MudCard>
|
|
</MudItem>
|
|
</MudGrid>
|
|
</MudContainer>
|
|
|
|
@code {
|
|
private static readonly StateMsg EmptyState = new()
|
|
{
|
|
};
|
|
|
|
private StateMsg? CurrentState;
|
|
private bool IsConnected;
|
|
private readonly string RobotSerial = "T800-002";
|
|
private List<MessageRow> MessageRows = new();
|
|
|
|
protected override async Task OnInitializedAsync()
|
|
{
|
|
RobotStateClient.OnStateReceived += OnRobotStateReceived;
|
|
RobotStateClient.OnRobotConnectionChanged += OnRobotConnectionChanged;
|
|
|
|
if (RobotStateClient.ConnectionState == RobotClientState.Disconnected)
|
|
{
|
|
await RobotStateClient.StartAsync();
|
|
}
|
|
|
|
await RobotStateClient.SubscribeRobotAsync(RobotSerial);
|
|
|
|
CurrentState = RobotStateClient.GetLatestState(RobotSerial);
|
|
IsConnected = RobotStateClient.IsRobotConnected;
|
|
|
|
UpdateMessageRows();
|
|
}
|
|
private void OnRobotConnectionChanged(bool connected)
|
|
{
|
|
InvokeAsync(() =>
|
|
{
|
|
IsConnected = connected;
|
|
StateHasChanged();
|
|
});
|
|
}
|
|
|
|
|
|
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;
|
|
RobotStateClient.OnRobotConnectionChanged -= OnRobotConnectionChanged;
|
|
}
|
|
|
|
private record MessageRow(string Type, string Level, string Description, bool IsError);
|
|
} |