Compare commits
No commits in common. "7e0c6af9d5fdcac3bcb6d553fd82911ba4b29a12" and "a8296063f54e4cda758f16b20fff0d3b25c289d7" have entirely different histories.
7e0c6af9d5
...
a8296063f5
|
|
@ -72,9 +72,8 @@
|
||||||
new(){Icon = "mdi-view-dashboard", Path="/", Label = "Dashboard", Match = NavLinkMatch.All},
|
new(){Icon = "mdi-view-dashboard", Path="/", Label = "Dashboard", Match = NavLinkMatch.All},
|
||||||
// new(){Icon = "mdi-map-legend", Path="/maps-manager", Label = "Mapping", Match = NavLinkMatch.All},
|
// new(){Icon = "mdi-map-legend", Path="/maps-manager", Label = "Mapping", Match = NavLinkMatch.All},
|
||||||
new(){Icon = "mdi-monitor", Path="/robot-monitor", Label = "Robot Monitor", Match = NavLinkMatch.All},
|
new(){Icon = "mdi-monitor", Path="/robot-monitor", Label = "Robot Monitor", Match = NavLinkMatch.All},
|
||||||
new(){Icon = "mdi-state-machine", Path="/robot-order", Label = "order", Match = NavLinkMatch.All},
|
|
||||||
new(){Icon = "mdi-application-cog", Path="/robot-config", Label = "Config", Match = NavLinkMatch.All },
|
new(){Icon = "mdi-application-cog", Path="/robot-config", Label = "Config", Match = NavLinkMatch.All },
|
||||||
new(){Icon = "mdi-math-log", Path="/logs", Label = "Logs", Match = NavLinkMatch.All},
|
new(){Icon = "mdi-math-log", Path="/logs", Label = "Logs", Match = NavLinkMatch.All}
|
||||||
];
|
];
|
||||||
|
|
||||||
private bool collapseNavMenu = true;
|
private bool collapseNavMenu = true;
|
||||||
|
|
|
||||||
|
|
@ -1,423 +0,0 @@
|
||||||
@page "/"
|
|
||||||
@attribute [Authorize]
|
|
||||||
@using RobotApp.Client.Services
|
|
||||||
@using RobotApp.VDA5050.State
|
|
||||||
@using MudBlazor
|
|
||||||
@inject RobotStateClient RobotStateClient
|
|
||||||
@implements IDisposable
|
|
||||||
@rendermode InteractiveWebAssembly
|
|
||||||
|
|
||||||
<MudContainer MaxWidth="MaxWidth.False" Class="pa-4">
|
|
||||||
<!-- 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 -->
|
|
||||||
<!-- 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:F1<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:F1 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 => RobotStateClient.LatestStates.ContainsKey(RobotSerial);
|
|
||||||
private readonly string RobotSerial = "T800-002";
|
|
||||||
private List<MessageRow> MessageRows = new();
|
|
||||||
|
|
||||||
protected override async Task OnInitializedAsync()
|
|
||||||
{
|
|
||||||
RobotStateClient.OnStateReceived += OnRobotStateReceived;
|
|
||||||
if (RobotStateClient.LatestStates.Count == 0)
|
|
||||||
{
|
|
||||||
await RobotStateClient.StartAsync();
|
|
||||||
}
|
|
||||||
await RobotStateClient.SubscribeRobotAsync(RobotSerial);
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
@ -1,172 +0,0 @@
|
||||||
<MudPaper Class="pa-4 h-100 d-flex flex-column" Elevation="2">
|
|
||||||
<MudStack Row AlignItems="AlignItems.Center" Justify="Justify.SpaceBetween"
|
|
||||||
Class="mb-4 flex-shrink-0">
|
|
||||||
<MudText Typo="Typo.h6">🔗 Edges</MudText>
|
|
||||||
|
|
||||||
<MudButton Variant="Variant.Filled"
|
|
||||||
Color="Color.Primary"
|
|
||||||
StartIcon="@Icons.Material.Filled.Add"
|
|
||||||
OnClick="AddEdgeAsync">
|
|
||||||
Add Edge
|
|
||||||
</MudButton>
|
|
||||||
</MudStack>
|
|
||||||
|
|
||||||
<div class="flex-grow-1" style="overflow:auto;">
|
|
||||||
<MudExpansionPanels MultiExpansion>
|
|
||||||
@foreach (var edge in Order.Edges)
|
|
||||||
{
|
|
||||||
<MudExpansionPanel @key="edge">
|
|
||||||
<!-- ================= HEADER ================= -->
|
|
||||||
<TitleContent>
|
|
||||||
<div class="d-flex align-center justify-space-between w-100">
|
|
||||||
|
|
||||||
<!-- LEFT: Edge information -->
|
|
||||||
<div class="d-flex align-center gap-3">
|
|
||||||
<MudText Typo="Typo.subtitle1" Class="fw-bold">
|
|
||||||
@edge.EdgeId
|
|
||||||
</MudText>
|
|
||||||
|
|
||||||
<MudChip T="string"
|
|
||||||
Size="Size.Small"
|
|
||||||
Color="Color.Info"
|
|
||||||
Variant="Variant.Outlined">
|
|
||||||
@edge.StartNodeId → @edge.EndNodeId
|
|
||||||
</MudChip>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- RIGHT: Delete -->
|
|
||||||
<MudIconButton Icon="@Icons.Material.Filled.Delete"
|
|
||||||
Color="Color.Error"
|
|
||||||
Size="Size.Small"
|
|
||||||
OnClick="@(() => RemoveEdgeAsync(edge))"
|
|
||||||
StopPropagation="true" />
|
|
||||||
</div>
|
|
||||||
</TitleContent>
|
|
||||||
|
|
||||||
<!-- ================= BODY ================= -->
|
|
||||||
<ChildContent>
|
|
||||||
<MudGrid Spacing="3">
|
|
||||||
|
|
||||||
<!-- Edge ID -->
|
|
||||||
<MudItem xs="12">
|
|
||||||
<MudTextField Value="@edge.EdgeId"
|
|
||||||
ValueChanged="@((string v) => SetValue(() => edge.EdgeId = v))"
|
|
||||||
Immediate="true"
|
|
||||||
Label="Edge ID" />
|
|
||||||
</MudItem>
|
|
||||||
|
|
||||||
<!-- Start Node -->
|
|
||||||
<MudItem xs="12">
|
|
||||||
<MudSelect T="string"
|
|
||||||
Value="@edge.StartNodeId"
|
|
||||||
ValueChanged="@((string v) => SetValue(() => edge.StartNodeId = v))"
|
|
||||||
Dense
|
|
||||||
Label="Start Node"
|
|
||||||
Required="true">
|
|
||||||
@foreach (var n in Order.Nodes)
|
|
||||||
{
|
|
||||||
<MudSelectItem Value="@n.NodeId">
|
|
||||||
@n.NodeId
|
|
||||||
</MudSelectItem>
|
|
||||||
}
|
|
||||||
</MudSelect>
|
|
||||||
</MudItem>
|
|
||||||
|
|
||||||
<!-- End Node -->
|
|
||||||
<MudItem xs="12">
|
|
||||||
<MudSelect T="string"
|
|
||||||
Value="@edge.EndNodeId"
|
|
||||||
ValueChanged="@((string v) => SetValue(() => edge.EndNodeId = v))"
|
|
||||||
Dense
|
|
||||||
Label="End Node"
|
|
||||||
Required="true">
|
|
||||||
@foreach (var n in Order.Nodes)
|
|
||||||
{
|
|
||||||
<MudSelectItem Value="@n.NodeId">
|
|
||||||
@n.NodeId
|
|
||||||
</MudSelectItem>
|
|
||||||
}
|
|
||||||
</MudSelect>
|
|
||||||
</MudItem>
|
|
||||||
|
|
||||||
<!-- Radius -->
|
|
||||||
<MudItem xs="6">
|
|
||||||
<MudNumericField T="double"
|
|
||||||
Value="@edge.Radius"
|
|
||||||
ValueChanged="@((double v) => SetValue(() => edge.Radius = v))"
|
|
||||||
Immediate="true"
|
|
||||||
Min="0"
|
|
||||||
Step="0.1"
|
|
||||||
Label="Radius (0 = straight line)" />
|
|
||||||
</MudItem>
|
|
||||||
|
|
||||||
<!-- Quadrant -->
|
|
||||||
@if (edge.Radius > 0)
|
|
||||||
{
|
|
||||||
<MudItem xs="6">
|
|
||||||
<MudSelect T="Quadrant"
|
|
||||||
Value="@edge.Quadrant"
|
|
||||||
ValueChanged="@((Quadrant v) => SetValue(() => edge.Quadrant = v))"
|
|
||||||
Label="Quadrant">
|
|
||||||
<MudSelectItem Value="Quadrant.I">I</MudSelectItem>
|
|
||||||
<MudSelectItem Value="Quadrant.II">II</MudSelectItem>
|
|
||||||
<MudSelectItem Value="Quadrant.III">III</MudSelectItem>
|
|
||||||
<MudSelectItem Value="Quadrant.IV">IV</MudSelectItem>
|
|
||||||
</MudSelect>
|
|
||||||
</MudItem>
|
|
||||||
}
|
|
||||||
|
|
||||||
<!-- Apply Curve -->
|
|
||||||
@if (!edge.HasTrajectory && edge.Radius > 0 && !edge.Expanded)
|
|
||||||
{
|
|
||||||
<MudItem xs="12">
|
|
||||||
<MudButton Color="Color.Primary"
|
|
||||||
Variant="Variant.Outlined"
|
|
||||||
StartIcon="@Icons.Material.Filled.Merge"
|
|
||||||
OnClick="@(() => ApplyCurveAsync(edge))">
|
|
||||||
Apply Curve (generate node)
|
|
||||||
</MudButton>
|
|
||||||
</MudItem>
|
|
||||||
}
|
|
||||||
|
|
||||||
</MudGrid>
|
|
||||||
</ChildContent>
|
|
||||||
</MudExpansionPanel>
|
|
||||||
}
|
|
||||||
</MudExpansionPanels>
|
|
||||||
</div>
|
|
||||||
</MudPaper>
|
|
||||||
|
|
||||||
@code {
|
|
||||||
[Parameter] public OrderMessage Order { get; set; } = default!;
|
|
||||||
|
|
||||||
[Parameter] public EventCallback OnAddEdge { get; set; }
|
|
||||||
[Parameter] public EventCallback<UiEdge> OnRemoveEdge { get; set; }
|
|
||||||
[Parameter] public EventCallback<UiEdge> OnApplyCurve { get; set; }
|
|
||||||
|
|
||||||
[Parameter] public EventCallback OnOrderChanged { get; set; }
|
|
||||||
|
|
||||||
private async Task SetValue(System.Action setter)
|
|
||||||
{
|
|
||||||
setter();
|
|
||||||
await OnOrderChanged.InvokeAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task AddEdgeAsync()
|
|
||||||
{
|
|
||||||
await OnAddEdge.InvokeAsync();
|
|
||||||
await OnOrderChanged.InvokeAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task RemoveEdgeAsync(UiEdge edge)
|
|
||||||
{
|
|
||||||
await OnRemoveEdge.InvokeAsync(edge);
|
|
||||||
await OnOrderChanged.InvokeAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task ApplyCurveAsync(UiEdge edge)
|
|
||||||
{
|
|
||||||
await OnApplyCurve.InvokeAsync(edge);
|
|
||||||
await OnOrderChanged.InvokeAsync();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,207 +0,0 @@
|
||||||
@inherits MudComponentBase
|
|
||||||
|
|
||||||
<MudDialog>
|
|
||||||
<TitleContent>
|
|
||||||
<MudText Typo="Typo.h6">Edit Node: @Node.NodeId</MudText>
|
|
||||||
</TitleContent>
|
|
||||||
|
|
||||||
<DialogContent>
|
|
||||||
<MudGrid Spacing="3">
|
|
||||||
<MudItem xs="12">
|
|
||||||
<MudTextField @bind-Value="Node.NodeId" Label="Node ID" Required="true" />
|
|
||||||
</MudItem>
|
|
||||||
<MudItem xs="12">
|
|
||||||
<MudNumericField T="int" @bind-Value="Node.SequenceId" Label="Sequence ID" Required="true" />
|
|
||||||
</MudItem>
|
|
||||||
<MudItem xs="12">
|
|
||||||
<MudSwitch T="bool" @bind-Checked="Node.Released" Label="Released" />
|
|
||||||
</MudItem>
|
|
||||||
<MudItem xs="6">
|
|
||||||
<MudNumericField T="double" @bind-Value="Node.NodePosition.X" Label="X" />
|
|
||||||
</MudItem>
|
|
||||||
|
|
||||||
<MudItem xs="6">
|
|
||||||
<MudNumericField T="double" @bind-Value="Node.NodePosition.Y" Label="Y" />
|
|
||||||
</MudItem>
|
|
||||||
|
|
||||||
<MudItem xs="6">
|
|
||||||
<MudNumericField T="double" @bind-Value="Node.NodePosition.Theta" Label="Theta (rad)" />
|
|
||||||
</MudItem>
|
|
||||||
|
|
||||||
<MudItem xs="6">
|
|
||||||
<MudNumericField T="double" @bind-Value="Node.NodePosition.AllowedDeviationXY"
|
|
||||||
Label="Allowed Dev XY" />
|
|
||||||
</MudItem>
|
|
||||||
|
|
||||||
<MudItem xs="6">
|
|
||||||
<MudNumericField T="double" @bind-Value="Node.NodePosition.AllowedDeviationTheta"
|
|
||||||
Label="Allowed Dev Theta" />
|
|
||||||
</MudItem>
|
|
||||||
|
|
||||||
<MudItem xs="12">
|
|
||||||
<MudTextField @bind-Value="Node.NodePosition.MapId" Label="Map ID" />
|
|
||||||
</MudItem>
|
|
||||||
|
|
||||||
|
|
||||||
<MudItem xs="12">
|
|
||||||
<MudDivider Class="my-4" />
|
|
||||||
<MudText Typo="Typo.subtitle1" Class="mb-3">Actions</MudText>
|
|
||||||
|
|
||||||
@foreach (var act in Node.Actions)
|
|
||||||
{
|
|
||||||
<MudPaper Class="pa-3 mb-3" Outlined="true">
|
|
||||||
<MudGrid Spacing="3">
|
|
||||||
<MudItem xs="10">
|
|
||||||
<MudItem xs="12">
|
|
||||||
<MudSelect T="string"
|
|
||||||
Label="Action Type"
|
|
||||||
Dense="true"
|
|
||||||
Required="true"
|
|
||||||
@bind-Value="act.ActionType">
|
|
||||||
|
|
||||||
@foreach (var at in Enum.GetValues<ActionType>())
|
|
||||||
{
|
|
||||||
<MudSelectItem Value="@at.ToString()">
|
|
||||||
@at
|
|
||||||
</MudSelectItem>
|
|
||||||
}
|
|
||||||
</MudSelect>
|
|
||||||
</MudItem>
|
|
||||||
</MudItem>
|
|
||||||
<MudItem xs="12">
|
|
||||||
<MudSelect T="string" @bind-Value="act.BlockingType" Label="Blocking Type">
|
|
||||||
<MudSelectItem Value="@("NONE")">NONE</MudSelectItem>
|
|
||||||
<MudSelectItem Value="@("SOFT")">SOFT</MudSelectItem>
|
|
||||||
<MudSelectItem Value="@("HARD")">HARD</MudSelectItem>
|
|
||||||
</MudSelect>
|
|
||||||
</MudItem>
|
|
||||||
<MudItem xs="12">
|
|
||||||
<MudTextField @bind-Value="act.ActionId" Label="Action ID" />
|
|
||||||
</MudItem>
|
|
||||||
</MudGrid>
|
|
||||||
|
|
||||||
<MudText Typo="Typo.caption" Class="mt-3 mb-2">Action Parameters</MudText>
|
|
||||||
|
|
||||||
@{
|
|
||||||
var parameters = act.ActionParameters ?? Array.Empty<ActionParameter>();
|
|
||||||
}
|
|
||||||
@foreach (var p in parameters.Cast<UiActionParameter>().ToList())
|
|
||||||
{
|
|
||||||
var param = p; // capture cho lambda
|
|
||||||
<MudGrid Class="mt-1">
|
|
||||||
<MudItem xs="5">
|
|
||||||
<MudTextField @bind-Value="param.Key" Label="Key" />
|
|
||||||
</MudItem>
|
|
||||||
<MudItem xs="5">
|
|
||||||
<MudTextField @bind-Value="param.ValueString" Label="Value" />
|
|
||||||
</MudItem>
|
|
||||||
<MudItem xs="2">
|
|
||||||
<MudIconButton Icon="@Icons.Material.Filled.Delete"
|
|
||||||
Color="Color.Error"
|
|
||||||
Size="Size.Small"
|
|
||||||
OnClick="@(() => RemoveParameter(act, param))" />
|
|
||||||
</MudItem>
|
|
||||||
</MudGrid>
|
|
||||||
}
|
|
||||||
|
|
||||||
<MudButton Size="Size.Small"
|
|
||||||
StartIcon="@Icons.Material.Filled.Add"
|
|
||||||
Class="mt-3"
|
|
||||||
OnClick="@(() => AddParameter(act))">
|
|
||||||
Add Parameter
|
|
||||||
</MudButton>
|
|
||||||
|
|
||||||
<MudDivider Class="my-3" />
|
|
||||||
|
|
||||||
<MudButton Size="Size.Small"
|
|
||||||
Color="Color.Error"
|
|
||||||
Variant="Variant.Text"
|
|
||||||
StartIcon="@Icons.Material.Filled.Delete"
|
|
||||||
OnClick="@(() => RemoveAction(act))">
|
|
||||||
Remove Action
|
|
||||||
</MudButton>
|
|
||||||
</MudPaper>
|
|
||||||
}
|
|
||||||
|
|
||||||
<MudButton Size="Size.Small"
|
|
||||||
StartIcon="@Icons.Material.Filled.Add"
|
|
||||||
OnClick="AddNewAction">
|
|
||||||
Add Action
|
|
||||||
</MudButton>
|
|
||||||
</MudItem>
|
|
||||||
</MudGrid>
|
|
||||||
</DialogContent>
|
|
||||||
|
|
||||||
<DialogActions>
|
|
||||||
<MudButton OnClick="Cancel">Cancel</MudButton>
|
|
||||||
<MudButton Color="Color.Primary" Variant="Variant.Filled" OnClick="Submit">Save</MudButton>
|
|
||||||
</DialogActions>
|
|
||||||
</MudDialog>
|
|
||||||
|
|
||||||
@code {
|
|
||||||
[CascadingParameter] public IMudDialogInstance MudDialog { get; set; } = default!;
|
|
||||||
[Parameter] public Node Node { get; set; } = default!;
|
|
||||||
|
|
||||||
private void Cancel() => MudDialog.Cancel();
|
|
||||||
private void Submit() => MudDialog.Close(DialogResult.Ok(true));
|
|
||||||
|
|
||||||
private void RemoveAction(VDA5050.InstantAction.Action actToRemove)
|
|
||||||
{
|
|
||||||
Node.Actions = Node.Actions
|
|
||||||
.Where(a => a != actToRemove)
|
|
||||||
.ToArray();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private void AddNewAction()
|
|
||||||
{
|
|
||||||
Node.Actions = Node.Actions
|
|
||||||
.Append(new VDA5050.InstantAction.Action
|
|
||||||
{
|
|
||||||
ActionId = Guid.NewGuid().ToString(),
|
|
||||||
ActionType = ActionType.startPause.ToString(),
|
|
||||||
BlockingType = "NONE",
|
|
||||||
ActionParameters = Array.Empty<ActionParameter>()
|
|
||||||
})
|
|
||||||
.ToArray();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
private void AddParameter(VDA5050.InstantAction.Action act)
|
|
||||||
{
|
|
||||||
var newParam = new UiActionParameter();
|
|
||||||
|
|
||||||
if (act.ActionParameters == null || act.ActionParameters.Length == 0)
|
|
||||||
{
|
|
||||||
act.ActionParameters = new[] { newParam };
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
var list = act.ActionParameters.ToList();
|
|
||||||
list.Add(newParam);
|
|
||||||
act.ActionParameters = list.ToArray();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void RemoveParameter(VDA5050.InstantAction.Action act, UiActionParameter paramToRemove)
|
|
||||||
{
|
|
||||||
if (act.ActionParameters == null || act.ActionParameters.Length == 0)
|
|
||||||
return;
|
|
||||||
|
|
||||||
act.ActionParameters = act.ActionParameters
|
|
||||||
.Where(p => p != paramToRemove) // so sánh reference
|
|
||||||
.ToArray();
|
|
||||||
}
|
|
||||||
|
|
||||||
// UiActionParameter vẫn giữ như cũ trong trang chính
|
|
||||||
public class UiActionParameter : ActionParameter
|
|
||||||
{
|
|
||||||
[JsonIgnore]
|
|
||||||
public string ValueString
|
|
||||||
{
|
|
||||||
get => Value?.ToString() ?? "";
|
|
||||||
set => Value = value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,104 +0,0 @@
|
||||||
@using System.Text.Json
|
|
||||||
@using MudBlazor
|
|
||||||
|
|
||||||
<MudDialog>
|
|
||||||
<TitleContent>
|
|
||||||
<MudText Typo="Typo.h6">Import Order JSON</MudText>
|
|
||||||
</TitleContent>
|
|
||||||
|
|
||||||
<DialogContent>
|
|
||||||
|
|
||||||
@if (ShowWarning)
|
|
||||||
{
|
|
||||||
<MudAlert Severity="Severity.Warning"
|
|
||||||
Variant="Variant.Outlined"
|
|
||||||
Class="mb-4">
|
|
||||||
Only valid <b>VDA 5050 Order JSON</b> is accepted.<br />
|
|
||||||
Invalid structure will be rejected.
|
|
||||||
</MudAlert>
|
|
||||||
}
|
|
||||||
|
|
||||||
<MudTextField @bind-Value="JsonText"
|
|
||||||
Label="Paste Order JSON"
|
|
||||||
Lines="20"
|
|
||||||
Variant="Variant.Filled"
|
|
||||||
Immediate
|
|
||||||
Style="font-family: monospace" />
|
|
||||||
|
|
||||||
@if (!string.IsNullOrEmpty(ErrorMessage))
|
|
||||||
{
|
|
||||||
<MudAlert Severity="Severity.Error" Class="mt-3">
|
|
||||||
@ErrorMessage
|
|
||||||
</MudAlert>
|
|
||||||
}
|
|
||||||
|
|
||||||
</DialogContent>
|
|
||||||
|
|
||||||
<DialogActions>
|
|
||||||
<MudButton OnClick="Cancel">Cancel</MudButton>
|
|
||||||
<MudButton Variant="Variant.Filled"
|
|
||||||
Color="Color.Primary"
|
|
||||||
OnClick="ValidateAndImport">
|
|
||||||
Import
|
|
||||||
</MudButton>
|
|
||||||
</DialogActions>
|
|
||||||
</MudDialog>
|
|
||||||
@code {
|
|
||||||
[CascadingParameter] public IMudDialogInstance MudDialog { get; set; } = default!;
|
|
||||||
|
|
||||||
public string JsonText { get; set; } = "";
|
|
||||||
public string? ErrorMessage;
|
|
||||||
public bool ShowWarning { get; set; } = false;
|
|
||||||
|
|
||||||
private void Cancel() => MudDialog.Cancel();
|
|
||||||
|
|
||||||
private void ValidateAndImport()
|
|
||||||
{
|
|
||||||
ErrorMessage = null;
|
|
||||||
ShowWarning = false;
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
using var doc = JsonDocument.Parse(JsonText);
|
|
||||||
var root = doc.RootElement;
|
|
||||||
|
|
||||||
// ===== BASIC STRUCTURE CHECK =====
|
|
||||||
if (!root.TryGetProperty("nodes", out _) ||
|
|
||||||
!root.TryGetProperty("edges", out _))
|
|
||||||
throw new Exception("Missing 'nodes' or 'edges' field.");
|
|
||||||
|
|
||||||
var order = OrderMessage.FromSchemaObject(root);
|
|
||||||
|
|
||||||
ValidateOrder(order);
|
|
||||||
|
|
||||||
MudDialog.Close(DialogResult.Ok(order));
|
|
||||||
}
|
|
||||||
catch (JsonException)
|
|
||||||
{
|
|
||||||
ShowWarning = true;
|
|
||||||
ErrorMessage = "Invalid JSON format.";
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
ShowWarning = true;
|
|
||||||
ErrorMessage = ex.Message;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void ValidateOrder(OrderMessage order)
|
|
||||||
{
|
|
||||||
if (order.Nodes.Count == 0)
|
|
||||||
throw new Exception("Order must contain at least one node.");
|
|
||||||
|
|
||||||
var nodeIds = order.Nodes.Select(n => n.NodeId).ToHashSet();
|
|
||||||
|
|
||||||
foreach (var e in order.Edges)
|
|
||||||
{
|
|
||||||
if (!nodeIds.Contains(e.StartNodeId) ||
|
|
||||||
!nodeIds.Contains(e.EndNodeId))
|
|
||||||
throw new Exception(
|
|
||||||
$"Edge '{e.EdgeId}' references unknown node."
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,84 +0,0 @@
|
||||||
<MudPaper Class="pa-4 h-100 d-flex flex-column" Elevation="2">
|
|
||||||
<MudStack Row AlignItems="AlignItems.Center" Justify="Justify.SpaceBetween"
|
|
||||||
Class="mb-4 flex-shrink-0">
|
|
||||||
<MudText Typo="Typo.h6">📄 JSON Output (/order)</MudText>
|
|
||||||
|
|
||||||
<div class="d-flex gap-2">
|
|
||||||
|
|
||||||
<!-- IMPORT -->
|
|
||||||
<MudButton Variant="Variant.Outlined"
|
|
||||||
Color="Color.Secondary"
|
|
||||||
Size="Size.Small"
|
|
||||||
StartIcon="@Icons.Material.Filled.UploadFile"
|
|
||||||
OnClick="OnImport">
|
|
||||||
Import JSON
|
|
||||||
</MudButton>
|
|
||||||
|
|
||||||
<!-- SEND -->
|
|
||||||
<MudButton Variant="Variant.Filled"
|
|
||||||
Color="@SendButtonColor"
|
|
||||||
StartIcon="@SendButtonIcon"
|
|
||||||
OnClick="OnSend"
|
|
||||||
Disabled="@(string.IsNullOrEmpty(OrderJson.Trim()))">
|
|
||||||
@SendButtonText
|
|
||||||
</MudButton>
|
|
||||||
|
|
||||||
<!-- COPY -->
|
|
||||||
<MudTooltip Text="@(Copied ? "Copied!" : "Copy to clipboard")">
|
|
||||||
<MudButton Variant="Variant.Filled"
|
|
||||||
Color="@(Copied ? Color.Success : Color.Primary)"
|
|
||||||
Size="Size.Small"
|
|
||||||
StartIcon="@(Copied ? Icons.Material.Filled.Check : Icons.Material.Filled.ContentCopy)"
|
|
||||||
OnClick="OnCopy">
|
|
||||||
@(Copied ? "Copied!" : "Copy")
|
|
||||||
</MudButton>
|
|
||||||
</MudTooltip>
|
|
||||||
</div>
|
|
||||||
</MudStack>
|
|
||||||
|
|
||||||
<div class="flex-grow-1" style="overflow:auto;">
|
|
||||||
<MudTextField Value="@OrderJson"
|
|
||||||
ReadOnly
|
|
||||||
Variant="Variant.Filled"
|
|
||||||
Lines="70"
|
|
||||||
Class="h-100"
|
|
||||||
Style="font-family: 'Roboto Mono', Consolas, monospace;
|
|
||||||
font-size: 0.85rem;
|
|
||||||
background:#1e1e1e;
|
|
||||||
color:#d4d4d4;" />
|
|
||||||
</div>
|
|
||||||
</MudPaper>
|
|
||||||
|
|
||||||
@code {
|
|
||||||
[Parameter] public string OrderJson { get; set; } = "";
|
|
||||||
[Parameter] public bool Copied { get; set; }
|
|
||||||
[Parameter] public bool? SendSuccess { get; set; }
|
|
||||||
|
|
||||||
[Parameter] public EventCallback OnCopy { get; set; }
|
|
||||||
[Parameter] public EventCallback OnSend { get; set; }
|
|
||||||
[Parameter] public EventCallback OnImport { get; set; }
|
|
||||||
|
|
||||||
private string SendButtonText =>
|
|
||||||
SendSuccess switch
|
|
||||||
{
|
|
||||||
true => "Done",
|
|
||||||
false => "Error",
|
|
||||||
_ => "Send"
|
|
||||||
};
|
|
||||||
|
|
||||||
private Color SendButtonColor =>
|
|
||||||
SendSuccess switch
|
|
||||||
{
|
|
||||||
true => Color.Success,
|
|
||||||
false => Color.Error,
|
|
||||||
_ => Color.Success
|
|
||||||
};
|
|
||||||
|
|
||||||
private string SendButtonIcon =>
|
|
||||||
SendSuccess switch
|
|
||||||
{
|
|
||||||
true => Icons.Material.Filled.CheckCircle,
|
|
||||||
false => Icons.Material.Filled.Error,
|
|
||||||
_ => Icons.Material.Filled.Send
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
@ -1,275 +0,0 @@
|
||||||
<MudPaper Class="pa-4 h-100 d-flex flex-column" Elevation="2">
|
|
||||||
<MudStack Row AlignItems="AlignItems.Center" Justify="Justify.SpaceBetween" Class="mb-4 flex-shrink-0">
|
|
||||||
<MudText Typo="Typo.h6">📍 Nodes</MudText>
|
|
||||||
<MudButton Variant="Variant.Filled"
|
|
||||||
Color="Color.Primary"
|
|
||||||
StartIcon="@Icons.Material.Filled.Add"
|
|
||||||
OnClick="AddNodeAsync">
|
|
||||||
Add Node
|
|
||||||
</MudButton>
|
|
||||||
</MudStack>
|
|
||||||
|
|
||||||
<div class="flex-grow-1" style="overflow:auto;">
|
|
||||||
<MudExpansionPanels MultiExpansion>
|
|
||||||
@foreach (var node in Order.Nodes)
|
|
||||||
{
|
|
||||||
<MudExpansionPanel @key="node.NodeId">
|
|
||||||
<TitleContent>
|
|
||||||
<div class="d-flex align-center justify-space-between w-100">
|
|
||||||
<MudText Typo="Typo.subtitle1" Class="fw-bold">@node.NodeId</MudText>
|
|
||||||
<div>
|
|
||||||
<MudIconButton Icon="@Icons.Material.Filled.Edit"
|
|
||||||
Color="Color.Primary"
|
|
||||||
Size="Size.Small"
|
|
||||||
OnClick="@(() => EditNodeAsync(node))"
|
|
||||||
StopPropagation />
|
|
||||||
<MudIconButton Icon="@Icons.Material.Filled.Delete"
|
|
||||||
Color="Color.Error"
|
|
||||||
Size="Size.Small"
|
|
||||||
OnClick="@(() => RemoveNodeAsync(node))"
|
|
||||||
StopPropagation />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</TitleContent>
|
|
||||||
|
|
||||||
<ChildContent>
|
|
||||||
<MudGrid Spacing="3">
|
|
||||||
|
|
||||||
<!-- Node ID -->
|
|
||||||
<MudItem xs="12">
|
|
||||||
<MudTextField Value="@node.NodeId"
|
|
||||||
ValueChanged="@((string v) => SetValue(() => node.NodeId = v))"
|
|
||||||
Immediate="true"
|
|
||||||
Label="Node ID" />
|
|
||||||
</MudItem>
|
|
||||||
|
|
||||||
<!-- Sequence -->
|
|
||||||
<MudItem xs="12">
|
|
||||||
<MudNumericField T="int"
|
|
||||||
Value="@node.SequenceId"
|
|
||||||
ValueChanged="@((int v) => SetValue(() => node.SequenceId = v))"
|
|
||||||
Immediate="true"
|
|
||||||
Label="Sequence ID" />
|
|
||||||
</MudItem>
|
|
||||||
|
|
||||||
<!-- Released -->
|
|
||||||
<MudItem xs="12">
|
|
||||||
<MudSwitch T="bool"
|
|
||||||
Checked="@node.Released"
|
|
||||||
CheckedChanged="@((bool v) => SetValue(() => node.Released = v))"
|
|
||||||
Label="Released" />
|
|
||||||
</MudItem>
|
|
||||||
|
|
||||||
<!-- Position -->
|
|
||||||
<MudItem xs="6">
|
|
||||||
<MudNumericField T="double"
|
|
||||||
Value="@node.NodePosition.X"
|
|
||||||
ValueChanged="@((double v) => SetValue(() => node.NodePosition.X = v))"
|
|
||||||
Immediate="true"
|
|
||||||
Label="X" />
|
|
||||||
</MudItem>
|
|
||||||
|
|
||||||
<MudItem xs="6">
|
|
||||||
<MudNumericField T="double"
|
|
||||||
Value="@node.NodePosition.Y"
|
|
||||||
ValueChanged="@((double v) => SetValue(() => node.NodePosition.Y = v))"
|
|
||||||
Immediate="true"
|
|
||||||
Label="Y" />
|
|
||||||
</MudItem>
|
|
||||||
|
|
||||||
<MudItem xs="12">
|
|
||||||
<MudTextField Value="@node.NodePosition.MapId"
|
|
||||||
ValueChanged="@((string v) => SetValue(() => node.NodePosition.MapId = v))"
|
|
||||||
Immediate="true"
|
|
||||||
Label="Map ID" />
|
|
||||||
</MudItem>
|
|
||||||
|
|
||||||
<MudItem xs="6">
|
|
||||||
<MudNumericField T="double"
|
|
||||||
Value="@node.NodePosition.Theta"
|
|
||||||
ValueChanged="@((double v) => SetValue(() => node.NodePosition.Theta = v))"
|
|
||||||
Immediate="true"
|
|
||||||
Label="Theta (rad)" />
|
|
||||||
</MudItem>
|
|
||||||
|
|
||||||
<MudItem xs="6">
|
|
||||||
<MudNumericField T="double"
|
|
||||||
Value="@node.NodePosition.AllowedDeviationXY"
|
|
||||||
ValueChanged="@((double v) => SetValue(() => node.NodePosition.AllowedDeviationXY = v))"
|
|
||||||
Immediate="true"
|
|
||||||
Label="Allowed Dev XY" />
|
|
||||||
</MudItem>
|
|
||||||
|
|
||||||
<MudItem xs="6">
|
|
||||||
<MudNumericField T="double"
|
|
||||||
Value="@node.NodePosition.AllowedDeviationTheta"
|
|
||||||
ValueChanged="@((double v) => SetValue(() => node.NodePosition.AllowedDeviationTheta = v))"
|
|
||||||
Immediate="true"
|
|
||||||
Label="Allowed Dev Theta" />
|
|
||||||
</MudItem>
|
|
||||||
|
|
||||||
<!-- Actions -->
|
|
||||||
<MudItem xs="12">
|
|
||||||
<MudDivider Class="my-4" />
|
|
||||||
<MudText Typo="Typo.subtitle1" Class="mb-3">Actions</MudText>
|
|
||||||
|
|
||||||
@foreach (var act in node.Actions ?? Array.Empty<VDA5050.InstantAction.Action>())
|
|
||||||
{
|
|
||||||
<MudPaper Class="pa-3 mb-3" Outlined>
|
|
||||||
<MudGrid Spacing="3">
|
|
||||||
|
|
||||||
<MudItem xs="12">
|
|
||||||
<MudSelect T="string"
|
|
||||||
Value="@act.ActionType"
|
|
||||||
ValueChanged="@((string v) => SetValue(() => act.ActionType = v))"
|
|
||||||
Dense
|
|
||||||
Label="Action Type">
|
|
||||||
@foreach (var at in Enum.GetValues<ActionType>())
|
|
||||||
{
|
|
||||||
<MudSelectItem Value="@at.ToString()">@at</MudSelectItem>
|
|
||||||
}
|
|
||||||
</MudSelect>
|
|
||||||
</MudItem>
|
|
||||||
|
|
||||||
<MudItem xs="12">
|
|
||||||
<MudSelect T="string"
|
|
||||||
Value="@act.BlockingType"
|
|
||||||
ValueChanged="@((string v) => SetValue(() => act.BlockingType = v))"
|
|
||||||
Label="Blocking Type">
|
|
||||||
<MudSelectItem Value="@("NONE")">NONE</MudSelectItem>
|
|
||||||
<MudSelectItem Value="@("SOFT")">SOFT</MudSelectItem>
|
|
||||||
<MudSelectItem Value="@("HARD")">HARD</MudSelectItem>
|
|
||||||
</MudSelect>
|
|
||||||
</MudItem>
|
|
||||||
|
|
||||||
<MudItem xs="12">
|
|
||||||
<MudTextField Value="@act.ActionId"
|
|
||||||
ValueChanged="@((string v) => SetValue(() => act.ActionId = v))"
|
|
||||||
Immediate="true"
|
|
||||||
Label="Action ID" />
|
|
||||||
</MudItem>
|
|
||||||
</MudGrid>
|
|
||||||
|
|
||||||
<MudText Typo="Typo.caption" Class="mt-3 mb-2">Action Parameters</MudText>
|
|
||||||
|
|
||||||
@foreach (var p in act.ActionParameters.Cast<UiActionParameter>())
|
|
||||||
{
|
|
||||||
<MudGrid Class="mt-1">
|
|
||||||
<MudItem xs="6">
|
|
||||||
<MudTextField Value="@p.Key"
|
|
||||||
ValueChanged="@((string v) => SetValue(() => p.Key = v))"
|
|
||||||
Immediate="true"
|
|
||||||
Label="Key" />
|
|
||||||
</MudItem>
|
|
||||||
|
|
||||||
<MudItem xs="6">
|
|
||||||
<MudTextField Value="@p.ValueString"
|
|
||||||
ValueChanged="@((string v) => SetValue(() => p.ValueString = v))"
|
|
||||||
Immediate="true"
|
|
||||||
Label="Value" />
|
|
||||||
</MudItem>
|
|
||||||
|
|
||||||
<MudItem xs="2">
|
|
||||||
<MudIconButton Icon="@Icons.Material.Filled.Delete"
|
|
||||||
Color="Color.Error"
|
|
||||||
OnClick="@(() => RemoveActionParameterAsync(act, p))" />
|
|
||||||
</MudItem>
|
|
||||||
</MudGrid>
|
|
||||||
}
|
|
||||||
|
|
||||||
<MudButton Size="Size.Small"
|
|
||||||
StartIcon="@Icons.Material.Filled.Add"
|
|
||||||
Class="mt-3"
|
|
||||||
OnClick="@(() => AddActionParameterAsync(act))">
|
|
||||||
Add Parameter
|
|
||||||
</MudButton>
|
|
||||||
|
|
||||||
<MudDivider Class="my-3" />
|
|
||||||
|
|
||||||
<MudButton Size="Size.Small"
|
|
||||||
Color="Color.Error"
|
|
||||||
Variant="Variant.Text"
|
|
||||||
StartIcon="@Icons.Material.Filled.Delete"
|
|
||||||
OnClick="@(() => RemoveActionAsync(node, act))">
|
|
||||||
Remove Action
|
|
||||||
</MudButton>
|
|
||||||
</MudPaper>
|
|
||||||
}
|
|
||||||
|
|
||||||
<MudButton Size="Size.Small"
|
|
||||||
StartIcon="@Icons.Material.Filled.Add"
|
|
||||||
OnClick="@(() => AddActionAsync(node))">
|
|
||||||
Add Action
|
|
||||||
</MudButton>
|
|
||||||
</MudItem>
|
|
||||||
</MudGrid>
|
|
||||||
</ChildContent>
|
|
||||||
</MudExpansionPanel>
|
|
||||||
}
|
|
||||||
</MudExpansionPanels>
|
|
||||||
</div>
|
|
||||||
</MudPaper>
|
|
||||||
|
|
||||||
@code {
|
|
||||||
[Parameter] public OrderMessage Order { get; set; } = default!;
|
|
||||||
[Parameter] public EventCallback OnAddNode { get; set; }
|
|
||||||
[Parameter] public EventCallback<Node> OnRemoveNode { get; set; }
|
|
||||||
[Parameter] public EventCallback<Node> OnEditNode { get; set; }
|
|
||||||
[Parameter] public EventCallback<Node> OnAddAction { get; set; }
|
|
||||||
[Parameter] public EventCallback<NodeActionWrapper> OnRemoveAction { get; set; }
|
|
||||||
[Parameter] public EventCallback<VDA5050.InstantAction.Action> OnAddActionParameter { get; set; }
|
|
||||||
[Parameter] public EventCallback<ActionParamWrapper> OnRemoveActionParameter { get; set; }
|
|
||||||
[Parameter] public EventCallback OnOrderChanged { get; set; }
|
|
||||||
|
|
||||||
// 🔥 helper realtime – KHÔNG ambiguous
|
|
||||||
private async Task SetValue(System.Action setter)
|
|
||||||
{
|
|
||||||
setter();
|
|
||||||
await OnOrderChanged.InvokeAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task AddNodeAsync()
|
|
||||||
{
|
|
||||||
await OnAddNode.InvokeAsync();
|
|
||||||
await OnOrderChanged.InvokeAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task RemoveNodeAsync(Node node)
|
|
||||||
{
|
|
||||||
await OnRemoveNode.InvokeAsync(node);
|
|
||||||
await OnOrderChanged.InvokeAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task EditNodeAsync(Node node)
|
|
||||||
{
|
|
||||||
await OnEditNode.InvokeAsync(node);
|
|
||||||
await OnOrderChanged.InvokeAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task AddActionAsync(Node node)
|
|
||||||
{
|
|
||||||
await OnAddAction.InvokeAsync(node);
|
|
||||||
await OnOrderChanged.InvokeAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task RemoveActionAsync(Node node, VDA5050.InstantAction.Action action)
|
|
||||||
{
|
|
||||||
await OnRemoveAction.InvokeAsync(new NodeActionWrapper(node, action));
|
|
||||||
await OnOrderChanged.InvokeAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task AddActionParameterAsync(VDA5050.InstantAction.Action act)
|
|
||||||
{
|
|
||||||
await OnAddActionParameter.InvokeAsync(act);
|
|
||||||
await OnOrderChanged.InvokeAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task RemoveActionParameterAsync(VDA5050.InstantAction.Action act, ActionParameter param)
|
|
||||||
{
|
|
||||||
await OnRemoveActionParameter.InvokeAsync(new ActionParamWrapper(act, param));
|
|
||||||
await OnOrderChanged.InvokeAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
public record NodeActionWrapper(Node Node, VDA5050.InstantAction.Action Action);
|
|
||||||
public record ActionParamWrapper(VDA5050.InstantAction.Action Action, ActionParameter Parameter);
|
|
||||||
}
|
|
||||||
|
|
@ -1,287 +0,0 @@
|
||||||
@page "/robot-order"
|
|
||||||
|
|
||||||
@attribute [Authorize]
|
|
||||||
@rendermode InteractiveWebAssemblyNoPrerender
|
|
||||||
|
|
||||||
@using System.Text.Json
|
|
||||||
@using System.Text.Json.Serialization
|
|
||||||
|
|
||||||
@inject IJSRuntime JS
|
|
||||||
@inject IDialogService DialogService
|
|
||||||
@inject HttpClient Http
|
|
||||||
|
|
||||||
<MudMainContent Class="pa-0 ma-0">
|
|
||||||
<div style="height:100vh; overflow:hidden;">
|
|
||||||
<MudContainer MaxWidth="MaxWidth.False"
|
|
||||||
Class="pa-4"
|
|
||||||
Style="max-width:100%; height:100%; display:flex; flex-direction:column;">
|
|
||||||
<MudGrid Spacing="4" Class="flex-grow-1" Style="overflow:hidden;">
|
|
||||||
|
|
||||||
<!-- ================= LEFT ================= -->
|
|
||||||
<MudItem xs="12" md="7" Class="d-flex flex-column h-100" Style="gap:16px;">
|
|
||||||
<MudGrid Spacing="4" Class="flex-grow-1" Style="overflow:hidden;">
|
|
||||||
|
|
||||||
<MudItem xs="12" md="6" Class="h-100">
|
|
||||||
<NodesPanel Order="Order"
|
|
||||||
OnAddNode="AddNode"
|
|
||||||
OnRemoveNode="RemoveNode"
|
|
||||||
OnEditNode="OpenEditNodeDialog"
|
|
||||||
OnAddAction="AddAction"
|
|
||||||
OnRemoveAction="@(w => RemoveAction(w.Node, w.Action))"
|
|
||||||
OnAddActionParameter="AddActionParameter"
|
|
||||||
OnRemoveActionParameter="@(w => RemoveActionParameter(w.Action, w.Parameter))"
|
|
||||||
OnOrderChanged="OnOrderChanged" />
|
|
||||||
</MudItem>
|
|
||||||
|
|
||||||
<MudItem xs="12" md="6" Class="h-100">
|
|
||||||
<EdgesPanel Order="Order"
|
|
||||||
OnAddEdge="AddEdge"
|
|
||||||
OnRemoveEdge="RemoveEdge"
|
|
||||||
OnApplyCurve="ApplyCurve"
|
|
||||||
OnOrderChanged="OnOrderChanged" />
|
|
||||||
</MudItem>
|
|
||||||
|
|
||||||
</MudGrid>
|
|
||||||
</MudItem>
|
|
||||||
|
|
||||||
<!-- ================= RIGHT ================= -->
|
|
||||||
<MudItem xs="12" md="5" Class="h-100">
|
|
||||||
<JsonOutputPanel OrderJson="@OrderJson"
|
|
||||||
Copied="@copied"
|
|
||||||
SendSuccess="@sendSuccess"
|
|
||||||
OnCopy="CopyJsonToClipboard"
|
|
||||||
OnSend="SendOrderToServer"
|
|
||||||
OnImport="OpenImportDialog" />
|
|
||||||
|
|
||||||
</MudItem>
|
|
||||||
|
|
||||||
</MudGrid>
|
|
||||||
</MudContainer>
|
|
||||||
</div>
|
|
||||||
</MudMainContent>
|
|
||||||
|
|
||||||
@code {
|
|
||||||
// ================= STATE =================
|
|
||||||
private OrderMessage Order { get; set; } = new();
|
|
||||||
private string OrderJson = ""; // 🔥 CACHE JSON (QUAN TRỌNG)
|
|
||||||
private bool copied;
|
|
||||||
private bool? sendSuccess;
|
|
||||||
private bool sending;
|
|
||||||
private CancellationTokenSource? _copyCts;
|
|
||||||
|
|
||||||
// ================= INIT =================
|
|
||||||
protected override void OnInitialized()
|
|
||||||
{
|
|
||||||
RebuildOrderJson();
|
|
||||||
}
|
|
||||||
|
|
||||||
// ================= CORE FIX =================
|
|
||||||
private void RebuildOrderJson()
|
|
||||||
{
|
|
||||||
OrderJson = JsonSerializer.Serialize(
|
|
||||||
Order.ToSchemaObject(),
|
|
||||||
new JsonSerializerOptions
|
|
||||||
{
|
|
||||||
WriteIndented = true,
|
|
||||||
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
|
|
||||||
});
|
|
||||||
}
|
|
||||||
private async Task OpenImportDialog()
|
|
||||||
{
|
|
||||||
var dialog = await DialogService.ShowAsync<ImportOrderDialog>(
|
|
||||||
"Import Order JSON",
|
|
||||||
new DialogOptions
|
|
||||||
{
|
|
||||||
FullWidth = true,
|
|
||||||
MaxWidth = MaxWidth.Large
|
|
||||||
});
|
|
||||||
|
|
||||||
var result = await dialog.Result;
|
|
||||||
|
|
||||||
if (!result.Canceled && result.Data is OrderMessage imported)
|
|
||||||
{
|
|
||||||
Order = imported;
|
|
||||||
RebuildOrderJson();
|
|
||||||
StateHasChanged();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnOrderChanged()
|
|
||||||
{
|
|
||||||
RebuildOrderJson(); // 🔥 JSON luôn rebuild
|
|
||||||
StateHasChanged(); // 🔥 ép render
|
|
||||||
}
|
|
||||||
|
|
||||||
// ================= NODE =================
|
|
||||||
void AddNode()
|
|
||||||
{
|
|
||||||
Order.Nodes.Add(new Node
|
|
||||||
{
|
|
||||||
NodeId = $"NODE_{Order.Nodes.Count + 1}",
|
|
||||||
SequenceId = Order.Nodes.Count,
|
|
||||||
Released = true,
|
|
||||||
NodePosition = new VDA5050.Order.NodePosition { MapId = "MAP_01" }
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
void RemoveNode(Node node)
|
|
||||||
{
|
|
||||||
Order.Nodes.Remove(node);
|
|
||||||
Order.Edges.RemoveAll(e => e.StartNodeId == node.NodeId || e.EndNodeId == node.NodeId);
|
|
||||||
ResequenceNodes();
|
|
||||||
}
|
|
||||||
|
|
||||||
void ResequenceNodes()
|
|
||||||
{
|
|
||||||
for (int i = 0; i < Order.Nodes.Count; i++)
|
|
||||||
Order.Nodes[i].SequenceId = i;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ================= EDGE =================
|
|
||||||
void AddEdge()
|
|
||||||
{
|
|
||||||
if (Order.Nodes.Count == 0)
|
|
||||||
return;
|
|
||||||
|
|
||||||
var start = Order.Nodes[0].NodeId;
|
|
||||||
var end = Order.Nodes.Count > 1
|
|
||||||
? Order.Nodes[1].NodeId
|
|
||||||
: start; // 👈 1 node thì start = end
|
|
||||||
|
|
||||||
Order.Edges.Add(new UiEdge
|
|
||||||
{
|
|
||||||
EdgeId = $"EDGE_{Order.Edges.Count + 1}",
|
|
||||||
StartNodeId = start,
|
|
||||||
EndNodeId = end
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
void RemoveEdge(UiEdge edge)
|
|
||||||
{
|
|
||||||
Order.Edges.Remove(edge);
|
|
||||||
}
|
|
||||||
|
|
||||||
void ApplyCurve(UiEdge edge)
|
|
||||||
{
|
|
||||||
if (edge.Radius <= 0 || edge.Expanded) return;
|
|
||||||
|
|
||||||
var startNode = Order.Nodes.First(n => n.NodeId == edge.StartNodeId);
|
|
||||||
var newNode = OrderMessage.CreateCurveNode(startNode, edge);
|
|
||||||
|
|
||||||
Order.Nodes.Add(newNode);
|
|
||||||
edge.EndNodeId = newNode.NodeId;
|
|
||||||
edge.MarkExpanded(); // ✅
|
|
||||||
|
|
||||||
ResequenceNodes();
|
|
||||||
}
|
|
||||||
|
|
||||||
// ================= ACTION =================
|
|
||||||
void AddAction(Node node)
|
|
||||||
{
|
|
||||||
var list = node.Actions?.ToList() ?? new();
|
|
||||||
list.Add(new VDA5050.InstantAction.Action
|
|
||||||
{
|
|
||||||
ActionId = Guid.NewGuid().ToString(),
|
|
||||||
ActionType = ActionType.startPause.ToString(),
|
|
||||||
BlockingType = "NONE",
|
|
||||||
ActionParameters = Array.Empty<ActionParameter>()
|
|
||||||
});
|
|
||||||
node.Actions = list.ToArray();
|
|
||||||
}
|
|
||||||
|
|
||||||
void RemoveAction(Node node, VDA5050.InstantAction.Action action)
|
|
||||||
{
|
|
||||||
node.Actions = node.Actions?.Where(a => a != action).ToArray()
|
|
||||||
?? Array.Empty<VDA5050.InstantAction.Action>();
|
|
||||||
}
|
|
||||||
|
|
||||||
void AddActionParameter(VDA5050.InstantAction.Action act)
|
|
||||||
{
|
|
||||||
var list = (act.ActionParameters ?? Array.Empty<ActionParameter>()).ToList();
|
|
||||||
list.Add(new UiActionParameter());
|
|
||||||
act.ActionParameters = list.ToArray();
|
|
||||||
}
|
|
||||||
|
|
||||||
void RemoveActionParameter(VDA5050.InstantAction.Action act, ActionParameter param)
|
|
||||||
{
|
|
||||||
act.ActionParameters =
|
|
||||||
act.ActionParameters?.Where(p => p != param).ToArray()
|
|
||||||
?? Array.Empty<ActionParameter>();
|
|
||||||
}
|
|
||||||
|
|
||||||
// ================= SEND / COPY =================
|
|
||||||
async Task SendOrderToServer()
|
|
||||||
{
|
|
||||||
// reset trạng thái trước khi gửi
|
|
||||||
sendSuccess = null;
|
|
||||||
StateHasChanged();
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var response = await Http.PostAsJsonAsync(
|
|
||||||
"/api/order",
|
|
||||||
JsonSerializer.Deserialize<JsonElement>(OrderJson)
|
|
||||||
);
|
|
||||||
|
|
||||||
sendSuccess = response.IsSuccessStatusCode;
|
|
||||||
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
sendSuccess = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
StateHasChanged();
|
|
||||||
|
|
||||||
// 🔥 AUTO RESET SAU 2 GIÂY
|
|
||||||
_ = Task.Run(async () =>
|
|
||||||
{
|
|
||||||
await Task.Delay(2000);
|
|
||||||
|
|
||||||
// quay về trạng thái Send
|
|
||||||
sendSuccess = null;
|
|
||||||
|
|
||||||
await InvokeAsync(StateHasChanged);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
async Task CopyJsonToClipboard()
|
|
||||||
{
|
|
||||||
_copyCts?.Cancel();
|
|
||||||
_copyCts = new();
|
|
||||||
|
|
||||||
await JS.InvokeVoidAsync("navigator.clipboard.writeText", OrderJson);
|
|
||||||
|
|
||||||
copied = true;
|
|
||||||
StateHasChanged();
|
|
||||||
|
|
||||||
try { await Task.Delay(1500, _copyCts.Token); } catch { }
|
|
||||||
copied = false;
|
|
||||||
StateHasChanged();
|
|
||||||
}
|
|
||||||
|
|
||||||
// ================= DIALOG =================
|
|
||||||
async Task OpenEditNodeDialog(Node node)
|
|
||||||
{
|
|
||||||
var parameters = new DialogParameters<EditNodeDialog>
|
|
||||||
{
|
|
||||||
{ x => x.Node, node }
|
|
||||||
};
|
|
||||||
|
|
||||||
var options = new DialogOptions
|
|
||||||
{
|
|
||||||
CloseButton = true,
|
|
||||||
FullWidth = true,
|
|
||||||
MaxWidth = MaxWidth.Large
|
|
||||||
};
|
|
||||||
|
|
||||||
var dialog = await DialogService.ShowAsync<EditNodeDialog>(
|
|
||||||
$"Edit Node: {node.NodeId}", parameters, options);
|
|
||||||
|
|
||||||
await dialog.Result;
|
|
||||||
OnOrderChanged(); // 🔥 cập nhật JSON sau dialog
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -15,12 +15,4 @@
|
||||||
@using RobotApp.Common.Shares
|
@using RobotApp.Common.Shares
|
||||||
@using RobotApp.Common.Shares.Dtos
|
@using RobotApp.Common.Shares.Dtos
|
||||||
@using Excubo.Blazor.Canvas
|
@using Excubo.Blazor.Canvas
|
||||||
@using Excubo.Blazor.Canvas.Contexts
|
@using Excubo.Blazor.Canvas.Contexts
|
||||||
@using System.Text.Json
|
|
||||||
@using System.Text.Json.Serialization
|
|
||||||
@using RobotApp.Client.Pages.Order
|
|
||||||
@using RobotApp.Client.Services
|
|
||||||
@using RobotApp.VDA5050.InstantAction
|
|
||||||
@using RobotApp.VDA5050.Order
|
|
||||||
@using RobotApp.VDA5050.Type
|
|
||||||
@using System.ComponentModel.DataAnnotations
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
|
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
|
||||||
using MudBlazor.Services;
|
using MudBlazor.Services;
|
||||||
using RobotApp.Client.Services;
|
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
|
|
||||||
CultureInfo.DefaultThreadCurrentCulture = new CultureInfo("en-US");
|
CultureInfo.DefaultThreadCurrentCulture = new CultureInfo("en-US");
|
||||||
|
|
@ -12,8 +11,6 @@ builder.Services.AddAuthenticationStateDeserialization();
|
||||||
|
|
||||||
builder.Services.AddScoped(_ => new HttpClient() { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });
|
builder.Services.AddScoped(_ => new HttpClient() { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });
|
||||||
builder.Services.AddScoped<RobotApp.Client.Services.RobotMonitorService>();
|
builder.Services.AddScoped<RobotApp.Client.Services.RobotMonitorService>();
|
||||||
builder.Services.AddScoped<RobotApp.Client.Services.RobotStateClient>();
|
|
||||||
|
|
||||||
builder.Services.AddMudServices(config =>
|
builder.Services.AddMudServices(config =>
|
||||||
{
|
{
|
||||||
config.SnackbarConfiguration.VisibleStateDuration = 2000;
|
config.SnackbarConfiguration.VisibleStateDuration = 2000;
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
<Project Sdk="Microsoft.NET.Sdk.BlazorWebAssembly">
|
<Project Sdk="Microsoft.NET.Sdk.BlazorWebAssembly">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net10.0</TargetFramework>
|
<TargetFramework>net9.0</TargetFramework>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
<NoDefaultLaunchSettingsFile>true</NoDefaultLaunchSettingsFile>
|
<NoDefaultLaunchSettingsFile>true</NoDefaultLaunchSettingsFile>
|
||||||
|
|
@ -9,17 +9,15 @@
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Blazored.LocalStorage" Version="4.5.0" />
|
|
||||||
<PackageReference Include="Excubo.Blazor.Canvas" Version="3.2.91" />
|
<PackageReference Include="Excubo.Blazor.Canvas" Version="3.2.91" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="9.0.1" />
|
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="9.0.1" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Authentication" Version="9.0.1" />
|
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Authentication" Version="9.0.1" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.SignalR.Client" Version="10.0.1" />
|
<PackageReference Include="Microsoft.AspNetCore.SignalR.Client" Version="9.0.1" />
|
||||||
<PackageReference Include="MudBlazor" Version="8.15.0" />
|
<PackageReference Include="MudBlazor" Version="8.12.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\RobotApp.Common.Shares\RobotApp.Common.Shares.csproj" />
|
<ProjectReference Include="..\RobotApp.Common.Shares\RobotApp.Common.Shares.csproj" />
|
||||||
<ProjectReference Include="..\RobotApp.VDA5050\RobotApp.VDA5050.csproj" />
|
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|
|
||||||
|
|
@ -1,188 +0,0 @@
|
||||||
using Microsoft.AspNetCore.Components;
|
|
||||||
using Microsoft.AspNetCore.SignalR.Client;
|
|
||||||
using RobotApp.Common.Shares;
|
|
||||||
using RobotApp.VDA5050.State;
|
|
||||||
using System.Collections.Concurrent;
|
|
||||||
using System.Text.Json;
|
|
||||||
|
|
||||||
namespace RobotApp.Client.Services;
|
|
||||||
|
|
||||||
// ================= CONNECTION STATE =================
|
|
||||||
public enum RobotClientState
|
|
||||||
{
|
|
||||||
Disconnected,
|
|
||||||
Connecting,
|
|
||||||
Connected,
|
|
||||||
Reconnecting
|
|
||||||
}
|
|
||||||
|
|
||||||
// ================= CLIENT =================
|
|
||||||
public sealed class RobotStateClient : IAsyncDisposable
|
|
||||||
{
|
|
||||||
private readonly NavigationManager _nav;
|
|
||||||
private HubConnection? _connection;
|
|
||||||
|
|
||||||
private readonly object _lock = new();
|
|
||||||
private bool _started;
|
|
||||||
|
|
||||||
public ConcurrentDictionary<string, StateMsg> LatestStates { get; } = new();
|
|
||||||
|
|
||||||
// ================= EVENTS =================
|
|
||||||
public event Action<string, StateMsg>? OnStateReceived;
|
|
||||||
public event Action<StateMsg>? OnStateReceivedAny;
|
|
||||||
public event Action<RobotClientState>? OnConnectionStateChanged;
|
|
||||||
|
|
||||||
public RobotClientState ConnectionState { get; private set; } = RobotClientState.Disconnected;
|
|
||||||
|
|
||||||
// ================= CTOR =================
|
|
||||||
public RobotStateClient(NavigationManager nav)
|
|
||||||
{
|
|
||||||
_nav = nav;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ================= STATE HELPER =================
|
|
||||||
private void SetState(RobotClientState state)
|
|
||||||
{
|
|
||||||
if (ConnectionState == state) return;
|
|
||||||
|
|
||||||
ConnectionState = state;
|
|
||||||
OnConnectionStateChanged?.Invoke(state);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ================= START =================
|
|
||||||
public async Task StartAsync(string hubPath = "/hubs/robot")
|
|
||||||
{
|
|
||||||
lock (_lock)
|
|
||||||
{
|
|
||||||
if (_started) return;
|
|
||||||
_started = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
SetState(RobotClientState.Connecting);
|
|
||||||
|
|
||||||
_connection = new HubConnectionBuilder()
|
|
||||||
.WithUrl(_nav.ToAbsoluteUri(hubPath))
|
|
||||||
.WithAutomaticReconnect(new[]
|
|
||||||
{
|
|
||||||
TimeSpan.Zero,
|
|
||||||
TimeSpan.FromSeconds(2),
|
|
||||||
TimeSpan.FromSeconds(10)
|
|
||||||
})
|
|
||||||
.Build();
|
|
||||||
|
|
||||||
_connection.Reconnecting += _ =>
|
|
||||||
{
|
|
||||||
SetState(RobotClientState.Reconnecting);
|
|
||||||
return Task.CompletedTask;
|
|
||||||
};
|
|
||||||
|
|
||||||
_connection.Reconnected += _ =>
|
|
||||||
{
|
|
||||||
SetState(RobotClientState.Connected);
|
|
||||||
return Task.CompletedTask;
|
|
||||||
};
|
|
||||||
|
|
||||||
_connection.Closed += async error =>
|
|
||||||
{
|
|
||||||
_started = false;
|
|
||||||
|
|
||||||
if (_connection != null)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
await StartAsync(hubPath);
|
|
||||||
}
|
|
||||||
catch { }
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
_connection.On<string>("ReceiveState", HandleState);
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
await _connection.StartAsync();
|
|
||||||
SetState(RobotClientState.Connected);
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
_started = false;
|
|
||||||
SetState(RobotClientState.Disconnected);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ================= HANDLE STATE =================
|
|
||||||
private void HandleState(string stateJson)
|
|
||||||
{
|
|
||||||
StateMsg? state;
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
state = JsonSerializer.Deserialize<StateMsg>(
|
|
||||||
stateJson,
|
|
||||||
JsonOptionExtends.Read
|
|
||||||
);
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (state?.SerialNumber == null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
LatestStates[state.SerialNumber] = state;
|
|
||||||
|
|
||||||
OnStateReceived?.Invoke(state.SerialNumber, state);
|
|
||||||
OnStateReceivedAny?.Invoke(state);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ================= SUBSCRIBE =================
|
|
||||||
public async Task SubscribeRobotAsync(string serialNumber)
|
|
||||||
{
|
|
||||||
if (_connection?.State != HubConnectionState.Connected)
|
|
||||||
return;
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
await _connection.InvokeAsync("JoinRobot", serialNumber);
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
// ignore – reconnect sẽ tự join lại
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task UnsubscribeRobotAsync(string serialNumber)
|
|
||||||
{
|
|
||||||
if (_connection?.State == HubConnectionState.Connected)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
await _connection.InvokeAsync("LeaveRobot", serialNumber);
|
|
||||||
}
|
|
||||||
catch { }
|
|
||||||
}
|
|
||||||
|
|
||||||
LatestStates.TryRemove(serialNumber, out _);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ================= GET CACHE =================
|
|
||||||
public StateMsg? GetLatestState(string serialNumber)
|
|
||||||
{
|
|
||||||
LatestStates.TryGetValue(serialNumber, out var state);
|
|
||||||
return state;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ================= DISPOSE =================
|
|
||||||
public async ValueTask DisposeAsync()
|
|
||||||
{
|
|
||||||
_started = false;
|
|
||||||
SetState(RobotClientState.Disconnected);
|
|
||||||
|
|
||||||
if (_connection != null)
|
|
||||||
{
|
|
||||||
await _connection.DisposeAsync();
|
|
||||||
_connection = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,420 +0,0 @@
|
||||||
using RobotApp.VDA5050.InstantAction;
|
|
||||||
using RobotApp.VDA5050.Order;
|
|
||||||
using System.Text.Json;
|
|
||||||
using System.Text.Json.Serialization;
|
|
||||||
|
|
||||||
namespace RobotApp.Client.Services;
|
|
||||||
|
|
||||||
// ======================================================
|
|
||||||
// EDGE UI
|
|
||||||
// ======================================================
|
|
||||||
public class UiEdge
|
|
||||||
{
|
|
||||||
public string EdgeId { get; set; } = "";
|
|
||||||
public int SequenceId { get; set; }
|
|
||||||
public bool Released { get; set; } = true;
|
|
||||||
|
|
||||||
public string StartNodeId { get; set; } = "";
|
|
||||||
public string EndNodeId { get; set; } = "";
|
|
||||||
|
|
||||||
// ===== CURVE (EDITOR GENERATED) =====
|
|
||||||
public double Radius { get; set; } = 0;
|
|
||||||
public Quadrant Quadrant { get; set; }
|
|
||||||
|
|
||||||
// ===== IMPORTED TRAJECTORY =====
|
|
||||||
public bool HasTrajectory { get; set; } = false;
|
|
||||||
public UiTrajectory? Trajectory { get; set; }
|
|
||||||
|
|
||||||
// ===== UI STATE =====
|
|
||||||
public bool Expanded { get; private set; } = false;
|
|
||||||
|
|
||||||
public void MarkExpanded()
|
|
||||||
{
|
|
||||||
Expanded = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public class UiTrajectory
|
|
||||||
{
|
|
||||||
public int Degree { get; set; }
|
|
||||||
public double[] KnotVector { get; set; } = Array.Empty<double>();
|
|
||||||
public List<Point> ControlPoints { get; set; } = new();
|
|
||||||
}
|
|
||||||
|
|
||||||
public enum Quadrant
|
|
||||||
{
|
|
||||||
I,
|
|
||||||
II,
|
|
||||||
III,
|
|
||||||
IV
|
|
||||||
}
|
|
||||||
|
|
||||||
// ======================================================
|
|
||||||
// GEOMETRY MODELS
|
|
||||||
// ======================================================
|
|
||||||
public record Point(double X, double Y);
|
|
||||||
|
|
||||||
public record QuarterResult(
|
|
||||||
Point EndPoint,
|
|
||||||
object Trajectory
|
|
||||||
);
|
|
||||||
|
|
||||||
// ======================================================
|
|
||||||
// GEOMETRY HELPER (QUARTER CIRCLE)
|
|
||||||
// ======================================================
|
|
||||||
public static class QuarterGeometry
|
|
||||||
{
|
|
||||||
private const double K = 0.5522847498307936;
|
|
||||||
|
|
||||||
public static QuarterResult BuildQuarterTrajectory(
|
|
||||||
Point A,
|
|
||||||
double r,
|
|
||||||
Quadrant q
|
|
||||||
)
|
|
||||||
{
|
|
||||||
Point P1, P2, C;
|
|
||||||
|
|
||||||
switch (q)
|
|
||||||
{
|
|
||||||
case Quadrant.I:
|
|
||||||
P1 = new(A.X, A.Y + K * r);
|
|
||||||
P2 = new(A.X + K * r, A.Y + r);
|
|
||||||
C = new(A.X + r, A.Y + r);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case Quadrant.II:
|
|
||||||
P1 = new(A.X - K * r, A.Y);
|
|
||||||
P2 = new(A.X - r, A.Y + K * r);
|
|
||||||
C = new(A.X - r, A.Y + r);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case Quadrant.III:
|
|
||||||
P1 = new(A.X, A.Y - K * r);
|
|
||||||
P2 = new(A.X - K * r, A.Y - r);
|
|
||||||
C = new(A.X - r, A.Y - r);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case Quadrant.IV:
|
|
||||||
P1 = new(A.X + K * r, A.Y);
|
|
||||||
P2 = new(A.X + r, A.Y - K * r);
|
|
||||||
C = new(A.X + r, A.Y - r);
|
|
||||||
break;
|
|
||||||
|
|
||||||
default:
|
|
||||||
throw new ArgumentOutOfRangeException(nameof(q));
|
|
||||||
}
|
|
||||||
|
|
||||||
return new QuarterResult(
|
|
||||||
C,
|
|
||||||
new
|
|
||||||
{
|
|
||||||
degree = 3,
|
|
||||||
knotVector = new[] { 0, 0, 0, 0, 1, 1, 1, 1 },
|
|
||||||
controlPoints = new[]
|
|
||||||
{
|
|
||||||
new { x = A.X, y = A.Y }, // P0
|
|
||||||
new { x = P1.X, y = P1.Y }, // P1
|
|
||||||
new { x = P2.X, y = P2.Y }, // P2
|
|
||||||
new { x = C.X, y = C.Y } // P3
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// ======================================================
|
|
||||||
// ORDER MESSAGE
|
|
||||||
// ======================================================
|
|
||||||
public class OrderMessage
|
|
||||||
{
|
|
||||||
public int HeaderId { get; set; }
|
|
||||||
public string Timestamp { get; set; } = "";
|
|
||||||
public string Version { get; set; } = "v1";
|
|
||||||
public string Manufacturer { get; set; } = "PNKX";
|
|
||||||
public string SerialNumber { get; set; } = "T800-002";
|
|
||||||
public string OrderId { get; set; } = Guid.NewGuid().ToString();
|
|
||||||
public int OrderUpdateId { get; set; }
|
|
||||||
public string? ZoneSetId { get; set; }
|
|
||||||
|
|
||||||
public List<Node> Nodes { get; set; } = new();
|
|
||||||
public List<UiEdge> Edges { get; set; } = new();
|
|
||||||
public static Node CreateCurveNode(Node startNode, UiEdge edge)
|
|
||||||
{
|
|
||||||
var A = new Point(
|
|
||||||
startNode.NodePosition.X,
|
|
||||||
startNode.NodePosition.Y
|
|
||||||
);
|
|
||||||
|
|
||||||
var result = QuarterGeometry.BuildQuarterTrajectory(
|
|
||||||
A,
|
|
||||||
edge.Radius,
|
|
||||||
edge.Quadrant
|
|
||||||
);
|
|
||||||
|
|
||||||
return new Node
|
|
||||||
{
|
|
||||||
NodeId = $"NODE_C{Guid.NewGuid():N}".Substring(0, 12),
|
|
||||||
Released = true,
|
|
||||||
NodePosition = new NodePosition
|
|
||||||
{
|
|
||||||
X = result.EndPoint.X,
|
|
||||||
Y = result.EndPoint.Y,
|
|
||||||
Theta = startNode.NodePosition.Theta,
|
|
||||||
MapId = startNode.NodePosition.MapId
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
public static OrderMessage FromSchemaObject(JsonElement root)
|
|
||||||
{
|
|
||||||
var order = new OrderMessage
|
|
||||||
{
|
|
||||||
HeaderId = root.GetProperty("headerId").GetInt32(),
|
|
||||||
Timestamp = root.GetProperty("timestamp").GetString(),
|
|
||||||
Version = root.GetProperty("version").GetString(),
|
|
||||||
Manufacturer = root.GetProperty("manufacturer").GetString(),
|
|
||||||
SerialNumber = root.GetProperty("serialNumber").GetString(),
|
|
||||||
OrderId = root.GetProperty("orderId").GetString(),
|
|
||||||
OrderUpdateId = root.GetProperty("orderUpdateId").GetInt32()
|
|
||||||
};
|
|
||||||
|
|
||||||
// ================= NODES =================
|
|
||||||
foreach (var n in root.GetProperty("nodes").EnumerateArray())
|
|
||||||
{
|
|
||||||
var node = new Node
|
|
||||||
{
|
|
||||||
NodeId = n.GetProperty("nodeId").GetString()!,
|
|
||||||
SequenceId = n.GetProperty("sequenceId").GetInt32(),
|
|
||||||
Released = n.GetProperty("released").GetBoolean(),
|
|
||||||
|
|
||||||
NodePosition = new NodePosition
|
|
||||||
{
|
|
||||||
X = n.GetProperty("nodePosition").GetProperty("x").GetDouble(),
|
|
||||||
Y = n.GetProperty("nodePosition").GetProperty("y").GetDouble(),
|
|
||||||
Theta = n.GetProperty("nodePosition").GetProperty("theta").GetDouble(),
|
|
||||||
AllowedDeviationXY = n.GetProperty("nodePosition").GetProperty("allowedDeviationXY").GetDouble(),
|
|
||||||
AllowedDeviationTheta = n.GetProperty("nodePosition").GetProperty("allowedDeviationTheta").GetDouble(),
|
|
||||||
MapId = n.GetProperty("nodePosition").GetProperty("mapId").GetString()
|
|
||||||
},
|
|
||||||
|
|
||||||
Actions = ParseActions(n)
|
|
||||||
};
|
|
||||||
|
|
||||||
order.Nodes.Add(node);
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var e in root.GetProperty("edges").EnumerateArray())
|
|
||||||
{
|
|
||||||
var edge = new UiEdge
|
|
||||||
{
|
|
||||||
EdgeId = e.GetProperty("edgeId").GetString()!,
|
|
||||||
SequenceId = e.GetProperty("sequenceId").GetInt32(),
|
|
||||||
Released = e.GetProperty("released").GetBoolean(),
|
|
||||||
StartNodeId = e.GetProperty("startNodeId").GetString()!,
|
|
||||||
EndNodeId = e.GetProperty("endNodeId").GetString()!,
|
|
||||||
};
|
|
||||||
|
|
||||||
// ===== IMPORT TRAJECTORY =====
|
|
||||||
if (e.TryGetProperty("trajectory", out var traj))
|
|
||||||
{
|
|
||||||
edge.HasTrajectory = true;
|
|
||||||
edge.Trajectory = new UiTrajectory
|
|
||||||
{
|
|
||||||
Degree = traj.GetProperty("degree").GetInt32(),
|
|
||||||
KnotVector = traj.GetProperty("knotVector")
|
|
||||||
.EnumerateArray()
|
|
||||||
.Select(x => x.GetDouble())
|
|
||||||
.ToArray(),
|
|
||||||
|
|
||||||
ControlPoints = traj.GetProperty("controlPoints")
|
|
||||||
.EnumerateArray()
|
|
||||||
.Select(p => new Point(
|
|
||||||
p.GetProperty("x").GetDouble(),
|
|
||||||
p.GetProperty("y").GetDouble()
|
|
||||||
))
|
|
||||||
.ToList()
|
|
||||||
};
|
|
||||||
|
|
||||||
// 🔥 IMPORTED CURVE → LOCK APPLY
|
|
||||||
edge.MarkExpanded();
|
|
||||||
}
|
|
||||||
|
|
||||||
order.Edges.Add(edge);
|
|
||||||
}
|
|
||||||
|
|
||||||
return order;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ================= ACTION PARSER =================
|
|
||||||
private static VDA5050.InstantAction.Action[] ParseActions(JsonElement parent)
|
|
||||||
{
|
|
||||||
if (!parent.TryGetProperty("actions", out var acts))
|
|
||||||
return Array.Empty<VDA5050.InstantAction.Action>();
|
|
||||||
|
|
||||||
return acts.EnumerateArray().Select(a =>
|
|
||||||
new VDA5050.InstantAction.Action
|
|
||||||
{
|
|
||||||
ActionId = a.GetProperty("actionId").GetString(),
|
|
||||||
ActionType = a.GetProperty("actionType").GetString(),
|
|
||||||
BlockingType = a.GetProperty("blockingType").GetString(),
|
|
||||||
|
|
||||||
ActionParameters = a.TryGetProperty("actionParameters", out var ps)
|
|
||||||
? ps.EnumerateArray()
|
|
||||||
.Select(p => new ActionParameter
|
|
||||||
{
|
|
||||||
Key = p.GetProperty("key").GetString(),
|
|
||||||
Value = p.GetProperty("value").GetString()
|
|
||||||
})
|
|
||||||
.ToArray()
|
|
||||||
: Array.Empty<ActionParameter>()
|
|
||||||
}
|
|
||||||
).ToArray();
|
|
||||||
}
|
|
||||||
public object ToSchemaObject()
|
|
||||||
{
|
|
||||||
int seq = 0;
|
|
||||||
|
|
||||||
return new
|
|
||||||
{
|
|
||||||
headerId = HeaderId++,
|
|
||||||
|
|
||||||
timestamp = string.IsNullOrWhiteSpace(Timestamp)
|
|
||||||
? DateTime.UtcNow.ToString("O")
|
|
||||||
: Timestamp,
|
|
||||||
|
|
||||||
version = Version,
|
|
||||||
manufacturer = Manufacturer,
|
|
||||||
serialNumber = SerialNumber,
|
|
||||||
orderId = OrderId,
|
|
||||||
orderUpdateId = OrderUpdateId,
|
|
||||||
|
|
||||||
zoneSetId = string.IsNullOrWhiteSpace(ZoneSetId)
|
|
||||||
? null
|
|
||||||
: ZoneSetId,
|
|
||||||
|
|
||||||
// ================= NODES =================
|
|
||||||
nodes = Nodes.Select(n => new
|
|
||||||
{
|
|
||||||
nodeId = n.NodeId,
|
|
||||||
sequenceId = seq++,
|
|
||||||
released = n.Released,
|
|
||||||
|
|
||||||
nodePosition = new
|
|
||||||
{
|
|
||||||
x = n.NodePosition.X,
|
|
||||||
y = n.NodePosition.Y,
|
|
||||||
theta = n.NodePosition.Theta,
|
|
||||||
|
|
||||||
allowedDeviationXY = n.NodePosition.AllowedDeviationXY,
|
|
||||||
allowedDeviationTheta = n.NodePosition.AllowedDeviationTheta,
|
|
||||||
|
|
||||||
mapId = string.IsNullOrWhiteSpace(n.NodePosition.MapId)
|
|
||||||
? "MAP_01"
|
|
||||||
: n.NodePosition.MapId
|
|
||||||
},
|
|
||||||
|
|
||||||
actions = n.Actions.Select(a => new
|
|
||||||
{
|
|
||||||
actionId = a.ActionId,
|
|
||||||
actionType = a.ActionType,
|
|
||||||
blockingType = a.BlockingType,
|
|
||||||
|
|
||||||
actionParameters = a.ActionParameters != null
|
|
||||||
? a.ActionParameters
|
|
||||||
.Select(p => new { key = p.Key, value = p.Value })
|
|
||||||
.ToArray()
|
|
||||||
: Array.Empty<object>()
|
|
||||||
}).ToArray()
|
|
||||||
}).ToArray(),
|
|
||||||
|
|
||||||
// ================= EDGES =================
|
|
||||||
edges = Edges.Select<UiEdge, object>(e =>
|
|
||||||
{
|
|
||||||
// =================================================
|
|
||||||
// 1️⃣ IMPORTED TRAJECTORY (ƯU TIÊN CAO NHẤT)
|
|
||||||
// =================================================
|
|
||||||
if (e.HasTrajectory && e.Trajectory != null)
|
|
||||||
{
|
|
||||||
return new
|
|
||||||
{
|
|
||||||
edgeId = e.EdgeId,
|
|
||||||
sequenceId = seq++,
|
|
||||||
released = true,
|
|
||||||
|
|
||||||
startNodeId = e.StartNodeId,
|
|
||||||
endNodeId = e.EndNodeId,
|
|
||||||
|
|
||||||
trajectory = new
|
|
||||||
{
|
|
||||||
degree = e.Trajectory.Degree,
|
|
||||||
knotVector = e.Trajectory.KnotVector,
|
|
||||||
controlPoints = e.Trajectory.ControlPoints
|
|
||||||
.Select(p => new { x = p.X, y = p.Y })
|
|
||||||
.ToArray()
|
|
||||||
},
|
|
||||||
|
|
||||||
actions = Array.Empty<object>()
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// =================================================
|
|
||||||
// 2️⃣ STRAIGHT EDGE (KHÔNG CÓ CURVE)
|
|
||||||
// =================================================
|
|
||||||
if (e.Radius <= 0)
|
|
||||||
{
|
|
||||||
return new
|
|
||||||
{
|
|
||||||
edgeId = e.EdgeId,
|
|
||||||
sequenceId = seq++,
|
|
||||||
released = true,
|
|
||||||
|
|
||||||
startNodeId = e.StartNodeId,
|
|
||||||
endNodeId = e.EndNodeId,
|
|
||||||
|
|
||||||
actions = Array.Empty<object>()
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// =================================================
|
|
||||||
// 3️⃣ EDITOR GENERATED CURVE (RADIUS + QUADRANT)
|
|
||||||
// =================================================
|
|
||||||
var startNode = Nodes.First(n => n.NodeId == e.StartNodeId);
|
|
||||||
|
|
||||||
var A = new Point(
|
|
||||||
startNode.NodePosition.X,
|
|
||||||
startNode.NodePosition.Y
|
|
||||||
);
|
|
||||||
|
|
||||||
var result = QuarterGeometry.BuildQuarterTrajectory(
|
|
||||||
A,
|
|
||||||
e.Radius,
|
|
||||||
e.Quadrant
|
|
||||||
);
|
|
||||||
|
|
||||||
return new
|
|
||||||
{
|
|
||||||
edgeId = e.EdgeId,
|
|
||||||
sequenceId = seq++,
|
|
||||||
released = true,
|
|
||||||
|
|
||||||
startNodeId = e.StartNodeId,
|
|
||||||
endNodeId = e.EndNodeId,
|
|
||||||
|
|
||||||
trajectory = result.Trajectory,
|
|
||||||
actions = Array.Empty<object>()
|
|
||||||
};
|
|
||||||
}).ToArray()
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ======================================================
|
|
||||||
// UI ACTION PARAM
|
|
||||||
// ======================================================
|
|
||||||
public class UiActionParameter : ActionParameter
|
|
||||||
{
|
|
||||||
[JsonIgnore]
|
|
||||||
public string ValueString
|
|
||||||
{
|
|
||||||
get => Value?.ToString() ?? "";
|
|
||||||
set => Value = value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -12,7 +12,4 @@
|
||||||
@using Microsoft.AspNetCore.Authorization
|
@using Microsoft.AspNetCore.Authorization
|
||||||
@using MudBlazor
|
@using MudBlazor
|
||||||
@using RobotApp.Common.Shares.Dtos
|
@using RobotApp.Common.Shares.Dtos
|
||||||
@using RobotApp.Common.Shares.Enums
|
@using RobotApp.Common.Shares.Enums
|
||||||
@using Blazored.LocalStorage
|
|
||||||
@using RobotApp.Client.Services
|
|
||||||
@using RobotApp.VDA5050.State
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net10.0</TargetFramework>
|
<TargetFramework>net9.0</TargetFramework>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net10.0</TargetFramework>
|
<TargetFramework>net9.0</TargetFramework>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
|
|
||||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||||
# Visual Studio Version 18
|
# Visual Studio Version 17
|
||||||
VisualStudioVersion = 18.0.11205.157
|
VisualStudioVersion = 17.14.36511.14
|
||||||
MinimumVisualStudioVersion = 10.0.40219.1
|
MinimumVisualStudioVersion = 10.0.40219.1
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RobotApp", "RobotApp\RobotApp.csproj", "{BF0BB137-2EF9-4E1B-944E-9BF41C5284F7}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RobotApp", "RobotApp\RobotApp.csproj", "{BF0BB137-2EF9-4E1B-944E-9BF41C5284F7}"
|
||||||
EndProject
|
EndProject
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
@page "/home"
|
@page "/"
|
||||||
@using Microsoft.AspNetCore.Authorization
|
@using Microsoft.AspNetCore.Authorization
|
||||||
|
|
||||||
@rendermode InteractiveServer
|
@rendermode InteractiveServer
|
||||||
|
|
|
||||||
|
|
@ -1,36 +0,0 @@
|
||||||
using Microsoft.AspNetCore.Mvc;
|
|
||||||
using RobotApp.Services.Robot;
|
|
||||||
using RobotApp.VDA5050.Order;
|
|
||||||
using System.Text.Json;
|
|
||||||
|
|
||||||
namespace RobotApp.Controllers;
|
|
||||||
|
|
||||||
[ApiController]
|
|
||||||
[Route("api/order")]
|
|
||||||
public class OrderController : ControllerBase
|
|
||||||
{
|
|
||||||
private readonly RobotOrderController robotOrderController;
|
|
||||||
|
|
||||||
public OrderController(RobotOrderController robotOrderController)
|
|
||||||
{
|
|
||||||
this.robotOrderController = robotOrderController;
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpPost]
|
|
||||||
public IActionResult SendOrder([FromBody] OrderMsg order)
|
|
||||||
{
|
|
||||||
Console.WriteLine("===== ORDER RECEIVED =====");
|
|
||||||
Console.WriteLine(JsonSerializer.Serialize(order, new JsonSerializerOptions
|
|
||||||
{
|
|
||||||
WriteIndented = true
|
|
||||||
}));
|
|
||||||
|
|
||||||
robotOrderController.UpdateOrder(order);
|
|
||||||
|
|
||||||
return Ok(new
|
|
||||||
{
|
|
||||||
success = true,
|
|
||||||
message = "Order received"
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,29 +0,0 @@
|
||||||
using Microsoft.AspNetCore.SignalR;
|
|
||||||
using RobotApp.Common.Shares;
|
|
||||||
using RobotApp.VDA5050.State;
|
|
||||||
using System.Text.Json;
|
|
||||||
|
|
||||||
namespace RobotApp.Hubs
|
|
||||||
{
|
|
||||||
public class RobotHub : Hub
|
|
||||||
{
|
|
||||||
// Client gọi để theo dõi robot cụ thể
|
|
||||||
public async Task JoinRobot(string serialNumber)
|
|
||||||
{
|
|
||||||
await Groups.AddToGroupAsync(Context.ConnectionId, serialNumber);
|
|
||||||
Console.WriteLine($"Client {Context.ConnectionId} joined robot group: {serialNumber}");
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task LeaveRobot(string serialNumber)
|
|
||||||
{
|
|
||||||
await Groups.RemoveFromGroupAsync(Context.ConnectionId, serialNumber);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Phương thức này sẽ được gọi từ service để broadcast
|
|
||||||
public async Task SendState(string serialNumber, StateMsg state)
|
|
||||||
{
|
|
||||||
var json = JsonSerializer.Serialize(state, JsonOptionExtends.Write);
|
|
||||||
await Clients.Group(serialNumber).SendAsync("ReceiveState", json);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -3,14 +3,11 @@ using Microsoft.AspNetCore.Identity;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using MudBlazor.Services;
|
using MudBlazor.Services;
|
||||||
using NLog.Web;
|
using NLog.Web;
|
||||||
using RobotApp.Client;
|
|
||||||
using RobotApp.Client.Services;
|
|
||||||
using RobotApp.Components;
|
using RobotApp.Components;
|
||||||
using RobotApp.Components.Account;
|
using RobotApp.Components.Account;
|
||||||
using RobotApp.Data;
|
using RobotApp.Data;
|
||||||
using RobotApp.Hubs;
|
using RobotApp.Hubs;
|
||||||
using RobotApp.Services;
|
using RobotApp.Services;
|
||||||
using RobotApp.Services.Robot;
|
|
||||||
using RobotApp.Services.Robot.Simulation;
|
using RobotApp.Services.Robot.Simulation;
|
||||||
|
|
||||||
var builder = WebApplication.CreateBuilder(args);
|
var builder = WebApplication.CreateBuilder(args);
|
||||||
|
|
@ -31,7 +28,7 @@ builder.Services.AddAuthorization();
|
||||||
|
|
||||||
// Add Controllers for API endpoints
|
// Add Controllers for API endpoints
|
||||||
builder.Services.AddControllers();
|
builder.Services.AddControllers();
|
||||||
builder.Services.AddSignalR();
|
|
||||||
var connectionString = builder.Configuration.GetConnectionString("DefaultConnection") ?? throw new InvalidOperationException("Connection string 'DefaultConnection' not found.");
|
var connectionString = builder.Configuration.GetConnectionString("DefaultConnection") ?? throw new InvalidOperationException("Connection string 'DefaultConnection' not found.");
|
||||||
Action<DbContextOptionsBuilder> appDbOptions = options => options.UseSqlite(connectionString, b => b.MigrationsAssembly("RobotApp"));
|
Action<DbContextOptionsBuilder> appDbOptions = options => options.UseSqlite(connectionString, b => b.MigrationsAssembly("RobotApp"));
|
||||||
|
|
||||||
|
|
@ -60,11 +57,8 @@ builder.Services.AddRobotSimulation();
|
||||||
builder.Services.AddRobot();
|
builder.Services.AddRobot();
|
||||||
|
|
||||||
// Add RobotMonitorService
|
// Add RobotMonitorService
|
||||||
builder.Services.AddSingleton<RobotApp.Services.RobotMonitorService>();
|
builder.Services.AddSingleton<RobotMonitorService>();
|
||||||
builder.Services.AddHostedService(sp => sp.GetRequiredService<RobotApp.Services.RobotMonitorService>());
|
builder.Services.AddHostedService(sp => sp.GetRequiredService<RobotMonitorService>());
|
||||||
|
|
||||||
builder.Services.AddScoped<RobotStateClient>();
|
|
||||||
builder.Services.AddHostedService<RobotStatePublisher>();
|
|
||||||
|
|
||||||
var app = builder.Build();
|
var app = builder.Build();
|
||||||
await app.Services.SeedApplicationDbAsync();
|
await app.Services.SeedApplicationDbAsync();
|
||||||
|
|
@ -90,13 +84,12 @@ app.UseAntiforgery();
|
||||||
|
|
||||||
app.MapStaticAssets();
|
app.MapStaticAssets();
|
||||||
|
|
||||||
|
|
||||||
// Map API Controllers
|
// Map API Controllers
|
||||||
app.MapControllers();
|
app.MapControllers();
|
||||||
|
|
||||||
// Map SignalR Hub
|
// Map SignalR Hub
|
||||||
app.MapHub<RobotMonitorHub>("/hubs/robotMonitor");
|
app.MapHub<RobotMonitorHub>("/hubs/robotMonitor");
|
||||||
app.MapHub<RobotHub>("/hubs/robot");
|
|
||||||
app.MapRazorComponents<App>()
|
app.MapRazorComponents<App>()
|
||||||
.AddInteractiveServerRenderMode()
|
.AddInteractiveServerRenderMode()
|
||||||
.AddInteractiveWebAssemblyRenderMode()
|
.AddInteractiveWebAssemblyRenderMode()
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net10.0</TargetFramework>
|
<TargetFramework>net9.0</TargetFramework>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
<UserSecretsId>aspnet-RobotApp-1f61caa2-bbbb-40cd-88b6-409b408a84ea</UserSecretsId>
|
<UserSecretsId>aspnet-RobotApp-1f61caa2-bbbb-40cd-88b6-409b408a84ea</UserSecretsId>
|
||||||
|
|
|
||||||
|
|
@ -264,7 +264,7 @@ public class RobotOrderController(INavigation NavigationManager,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void HandleUpdateOrder(OrderMsg order)
|
private void HandleUpdateOrder(OrderMsg order)
|
||||||
{
|
{
|
||||||
if (order.OrderId != OrderId) throw new OrderException(RobotErrors.Error1001(OrderId, order.OrderId));
|
if (order.OrderId != OrderId) throw new OrderException(RobotErrors.Error1001(OrderId, order.OrderId));
|
||||||
if (order.OrderUpdateId <= OrderUpdateId) return;
|
if (order.OrderUpdateId <= OrderUpdateId) return;
|
||||||
|
|
|
||||||
|
|
@ -1,175 +0,0 @@
|
||||||
using Microsoft.AspNetCore.SignalR;
|
|
||||||
using Microsoft.Extensions.Hosting;
|
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
using RobotApp.Common.Shares;
|
|
||||||
using RobotApp.Hubs;
|
|
||||||
using RobotApp.Interfaces;
|
|
||||||
using RobotApp.Services.State;
|
|
||||||
using RobotApp.VDA5050.State;
|
|
||||||
using RobotApp.VDA5050.Type;
|
|
||||||
using RobotApp.VDA5050.Visualization;
|
|
||||||
using System.Text.Json;
|
|
||||||
|
|
||||||
namespace RobotApp.Services.Robot;
|
|
||||||
|
|
||||||
public class RobotStatePublisher : BackgroundService
|
|
||||||
{
|
|
||||||
private readonly IHubContext<RobotHub> _hubContext;
|
|
||||||
private readonly RobotConfiguration _robotConfig;
|
|
||||||
private readonly IOrder _orderManager;
|
|
||||||
private readonly IInstantActions _actionManager;
|
|
||||||
private readonly IPeripheral _peripheralManager;
|
|
||||||
private readonly IInfomation _infoManager;
|
|
||||||
private readonly IError _errorManager;
|
|
||||||
private readonly ILocalization _localizationManager;
|
|
||||||
private readonly IBattery _batteryManager;
|
|
||||||
private readonly ILoad _loadManager;
|
|
||||||
private readonly INavigation _navigationManager;
|
|
||||||
private readonly RobotStateMachine _stateManager;
|
|
||||||
|
|
||||||
private uint _headerId = 0;
|
|
||||||
private readonly PeriodicTimer _timer = new(TimeSpan.FromMilliseconds(1000)); // 1 giây/lần
|
|
||||||
|
|
||||||
public RobotStatePublisher(
|
|
||||||
IHubContext<RobotHub> hubContext,
|
|
||||||
RobotConfiguration robotConfig,
|
|
||||||
IOrder orderManager,
|
|
||||||
IInstantActions actionManager,
|
|
||||||
IPeripheral peripheralManager,
|
|
||||||
IInfomation infoManager,
|
|
||||||
IError errorManager,
|
|
||||||
ILocalization localizationManager,
|
|
||||||
IBattery batteryManager,
|
|
||||||
ILoad loadManager,
|
|
||||||
INavigation navigationManager,
|
|
||||||
RobotStateMachine stateManager)
|
|
||||||
{
|
|
||||||
_hubContext = hubContext;
|
|
||||||
_robotConfig = robotConfig;
|
|
||||||
_orderManager = orderManager;
|
|
||||||
_actionManager = actionManager;
|
|
||||||
_peripheralManager = peripheralManager;
|
|
||||||
_infoManager = infoManager;
|
|
||||||
_errorManager = errorManager;
|
|
||||||
_localizationManager = localizationManager;
|
|
||||||
_batteryManager = batteryManager;
|
|
||||||
_loadManager = loadManager;
|
|
||||||
_navigationManager = navigationManager;
|
|
||||||
_stateManager = stateManager;
|
|
||||||
}
|
|
||||||
|
|
||||||
private StateMsg GetStateMsg()
|
|
||||||
{
|
|
||||||
return new StateMsg
|
|
||||||
{
|
|
||||||
HeaderId = _headerId++,
|
|
||||||
Timestamp = DateTime.UtcNow.ToString("o"), // ISO 8601
|
|
||||||
Manufacturer = _robotConfig.VDA5050Setting.Manufacturer,
|
|
||||||
Version = _robotConfig.VDA5050Setting.Version,
|
|
||||||
SerialNumber = _robotConfig.SerialNumber,
|
|
||||||
Maps = [],
|
|
||||||
OrderId = _orderManager.OrderId,
|
|
||||||
OrderUpdateId = _orderManager.OrderUpdateId,
|
|
||||||
ZoneSetId = "",
|
|
||||||
LastNodeId = _orderManager.LastNodeId,
|
|
||||||
LastNodeSequenceId = _orderManager.LastNodeSequenceId,
|
|
||||||
Driving = Math.Abs(_navigationManager.VelocityX) > 0.01 || Math.Abs(_navigationManager.Omega) > 0.01,
|
|
||||||
Paused = false,
|
|
||||||
NewBaseRequest = true,
|
|
||||||
DistanceSinceLastNode = 0,
|
|
||||||
OperatingMode = _peripheralManager.PeripheralMode.ToString(),
|
|
||||||
NodeStates = _orderManager.NodeStates,
|
|
||||||
EdgeStates = _orderManager.EdgeStates,
|
|
||||||
ActionStates = _actionManager.ActionStates,
|
|
||||||
Information = [General, .. _infoManager.InformationState],
|
|
||||||
Errors = _errorManager.ErrorsState,
|
|
||||||
AgvPosition = new AgvPosition
|
|
||||||
{
|
|
||||||
X = _localizationManager.X,
|
|
||||||
Y = _localizationManager.Y,
|
|
||||||
Theta = _localizationManager.Theta,
|
|
||||||
LocalizationScore = _localizationManager.MatchingScore,
|
|
||||||
MapId = _localizationManager.CurrentActiveMap,
|
|
||||||
DeviationRange = _localizationManager.Reliability,
|
|
||||||
PositionInitialized = _localizationManager.IsReady,
|
|
||||||
},
|
|
||||||
BatteryState = new BatteryState
|
|
||||||
{
|
|
||||||
Charging = _batteryManager.IsCharging,
|
|
||||||
BatteryHealth = _batteryManager.SOH,
|
|
||||||
Reach = 0,
|
|
||||||
BatteryVoltage = _batteryManager.Voltage,
|
|
||||||
BatteryCharge = _batteryManager.SOC,
|
|
||||||
},
|
|
||||||
Loads = _loadManager.Load,
|
|
||||||
Velocity = new Velocity
|
|
||||||
{
|
|
||||||
Vx = _navigationManager.VelocityX,
|
|
||||||
Vy = _navigationManager.VelocityY,
|
|
||||||
Omega = _navigationManager.Omega,
|
|
||||||
},
|
|
||||||
SafetyState = new SafetyState
|
|
||||||
{
|
|
||||||
FieldViolation = _peripheralManager.LidarBackProtectField ||
|
|
||||||
_peripheralManager.LidarFrontProtectField ||
|
|
||||||
_peripheralManager.LidarFrontTimProtectField,
|
|
||||||
EStop = (_peripheralManager.Emergency || _peripheralManager.Bumper)
|
|
||||||
? EStop.AUTOACK.ToString()
|
|
||||||
: EStop.NONE.ToString(),
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private Information General => new()
|
|
||||||
{
|
|
||||||
InfoType = InformationType.robot_general.ToString(),
|
|
||||||
InfoDescription = "Thông tin chung của robot",
|
|
||||||
InfoLevel = InfoLevel.INFO.ToString(),
|
|
||||||
InfoReferences =
|
|
||||||
[
|
|
||||||
new InfomationReferences
|
|
||||||
{
|
|
||||||
ReferenceKey = InformationReferencesKey.robot_state.ToString(),
|
|
||||||
ReferenceValue = _stateManager.CurrentStateName,
|
|
||||||
}
|
|
||||||
]
|
|
||||||
};
|
|
||||||
|
|
||||||
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
|
||||||
{
|
|
||||||
Console.WriteLine("[RobotStatePublisher] Started - Publishing state every 1 second via SignalR");
|
|
||||||
|
|
||||||
while (await _timer.WaitForNextTickAsync(stoppingToken) && !stoppingToken.IsCancellationRequested)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var state = GetStateMsg();
|
|
||||||
var serialNumber = _robotConfig.SerialNumber;
|
|
||||||
|
|
||||||
var json = JsonSerializer.Serialize(state, JsonOptionExtends.Write);
|
|
||||||
|
|
||||||
// Push đến tất cả client đang theo dõi robot này
|
|
||||||
await _hubContext.Clients
|
|
||||||
.Group(serialNumber)
|
|
||||||
.SendAsync("ReceiveState", json, stoppingToken);
|
|
||||||
|
|
||||||
//Console.WriteLine($"[RobotStatePublisher] Published state for {serialNumber} | " +
|
|
||||||
// $"HeaderId: {state.HeaderId} | " +
|
|
||||||
// $"Pos: ({state.AgvPosition.X:F2}, {state.AgvPosition.Y:F2}) | " +
|
|
||||||
// $"Battery: {state.BatteryState.BatteryCharge:F1}%");
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Console.WriteLine($"[RobotStatePublisher] Error publishing state: {ex.Message}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Console.WriteLine("[RobotStatePublisher] Stopped.");
|
|
||||||
}
|
|
||||||
|
|
||||||
public override void Dispose()
|
|
||||||
{
|
|
||||||
_timer?.Dispose();
|
|
||||||
base.Dispose();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Loading…
Reference in New Issue
Block a user