save
This commit is contained in:
parent
93599f5c95
commit
45082a98cd
|
|
@ -73,7 +73,7 @@
|
||||||
// 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-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-state-machine", Path="/robot-state", Label = "state", Match = NavLinkMatch.All},
|
new(){Icon = "mdi-state-machine", Path="/robot-order", Label = "order", Match = NavLinkMatch.All},
|
||||||
];
|
];
|
||||||
|
|
||||||
private bool collapseNavMenu = true;
|
private bool collapseNavMenu = true;
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load Diff
170
RobotApp.Client/Pages/Order/EdgesPanel.razor
Normal file
170
RobotApp.Client/Pages/Order/EdgesPanel.razor
Normal file
|
|
@ -0,0 +1,170 @@
|
||||||
|
<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 info -->
|
||||||
|
<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="Bán kính (0 = đường thẳng)" />
|
||||||
|
</MudItem>
|
||||||
|
|
||||||
|
<!-- Quadrant -->
|
||||||
|
@if (edge.Radius > 0)
|
||||||
|
{
|
||||||
|
<MudItem xs="6">
|
||||||
|
<MudSelect T="Quadrant"
|
||||||
|
Value="@edge.Quadrant"
|
||||||
|
ValueChanged="@((Quadrant v) => SetValue(() => edge.Quadrant = v))"
|
||||||
|
Label="Góc phần tư">
|
||||||
|
<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.Radius > 0 && !edge.Expanded)
|
||||||
|
{
|
||||||
|
<MudItem xs="12">
|
||||||
|
<MudButton Color="Color.Primary"
|
||||||
|
Variant="Variant.Outlined"
|
||||||
|
StartIcon="@Icons.Material.Filled.Merge"
|
||||||
|
OnClick="@(() => ApplyCurveAsync(edge))">
|
||||||
|
Apply Curve (tạo 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,9 +1,4 @@
|
||||||
@inherits MudComponentBase
|
@inherits MudComponentBase
|
||||||
@using RobotApp.VDA5050.InstantAction
|
|
||||||
@using System.Text.Json
|
|
||||||
@using System.Text.Json.Serialization
|
|
||||||
@using RobotApp.VDA5050.Order
|
|
||||||
@using RobotApp.VDA5050.Type
|
|
||||||
|
|
||||||
<MudDialog>
|
<MudDialog>
|
||||||
<TitleContent>
|
<TitleContent>
|
||||||
75
RobotApp.Client/Pages/Order/JsonOutputPanel.razor
Normal file
75
RobotApp.Client/Pages/Order/JsonOutputPanel.razor
Normal file
|
|
@ -0,0 +1,75 @@
|
||||||
|
<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">
|
||||||
|
|
||||||
|
<!-- ================= SEND BUTTON ================= -->
|
||||||
|
<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>
|
||||||
|
|
||||||
|
<!-- ================= JSON ================= -->
|
||||||
|
<div class="flex-grow-1" style="overflow:auto;">
|
||||||
|
<MudTextField Value="@OrderJson"
|
||||||
|
ReadOnly="true"
|
||||||
|
Variant="Variant.Filled"
|
||||||
|
Lines="70"
|
||||||
|
Class="h-100"
|
||||||
|
Style="font-family: 'Roboto Mono', Consolas, 'Courier New', monospace;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
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; }
|
||||||
|
|
||||||
|
// ================= SEND BUTTON STATE =================
|
||||||
|
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
|
||||||
|
};
|
||||||
|
}
|
||||||
275
RobotApp.Client/Pages/Order/NodesPanel.razor
Normal file
275
RobotApp.Client/Pages/Order/NodesPanel.razor
Normal file
|
|
@ -0,0 +1,275 @@
|
||||||
|
<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);
|
||||||
|
}
|
||||||
267
RobotApp.Client/Pages/Order/OrderMess.razor
Normal file
267
RobotApp.Client/Pages/Order/OrderMess.razor
Normal file
|
|
@ -0,0 +1,267 @@
|
||||||
|
@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" />
|
||||||
|
|
||||||
|
</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 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.Expanded = true;
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,374 +0,0 @@
|
||||||
@page "/robot-state"
|
|
||||||
@using RobotApp.Client.Services
|
|
||||||
@using RobotApp.VDA5050.State
|
|
||||||
@inject RobotStateClient RobotStateClient
|
|
||||||
@implements IDisposable
|
|
||||||
@rendermode InteractiveWebAssembly
|
|
||||||
|
|
||||||
<MudContainer MaxWidth="MaxWidth.False" Class="pa-4">
|
|
||||||
<!-- ===================================================== -->
|
|
||||||
<!-- HEADER -->
|
|
||||||
<!-- ===================================================== -->
|
|
||||||
<MudPaper Class="pa-6 mb-4 d-flex align-center justify-space-between" Elevation="3">
|
|
||||||
<div>
|
|
||||||
<MudText Typo="Typo.h4">🤖 VDA 5050 Robot Dashboard</MudText>
|
|
||||||
@if (CurrentState != null)
|
|
||||||
{
|
|
||||||
<MudText Typo="Typo.subtitle2" Color="Color.Secondary">
|
|
||||||
@CurrentState.Version •
|
|
||||||
@CurrentState.Manufacturer •
|
|
||||||
@CurrentState.SerialNumber
|
|
||||||
</MudText>
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
<MudText Typo="Typo.subtitle2" Color="Color.Secondary">
|
|
||||||
Connecting to robot...
|
|
||||||
</MudText>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
@if (CurrentState != null)
|
|
||||||
{
|
|
||||||
<MudChip T="string" Size="Size.Large"
|
|
||||||
Color="@(IsConnected ? Color.Success : Color.Error)"
|
|
||||||
Variant="Variant.Filled">
|
|
||||||
@(IsConnected ? "ONLINE" : "OFFLINE")
|
|
||||||
</MudChip>
|
|
||||||
}
|
|
||||||
</MudPaper>
|
|
||||||
|
|
||||||
@if (CurrentState == null)
|
|
||||||
{
|
|
||||||
<MudAlert Severity="Severity.Info" Variant="Variant.Outlined" Class="mb-4">
|
|
||||||
<MudAlertTitle>Waiting for robot state data...</MudAlertTitle>
|
|
||||||
Connecting to SignalR hub and subscribing to robot updates.
|
|
||||||
<MudProgressLinear Indeterminate Class="mt-3" />
|
|
||||||
</MudAlert>
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
var msg = CurrentState;
|
|
||||||
|
|
||||||
<!-- ===================================================== -->
|
|
||||||
<!-- MESSAGE META -->
|
|
||||||
<!-- ===================================================== -->
|
|
||||||
<MudPaper Class="pa-3 mb-4" Elevation="1">
|
|
||||||
<MudGrid Spacing="2">
|
|
||||||
<MudItem xs="12" md="3">
|
|
||||||
<MudText Typo="Typo.caption">HeaderId</MudText>
|
|
||||||
<MudText>@msg.HeaderId</MudText>
|
|
||||||
</MudItem>
|
|
||||||
<MudItem xs="12" md="5">
|
|
||||||
<MudText Typo="Typo.caption">Timestamp (UTC)</MudText>
|
|
||||||
<MudText>@msg.Timestamp</MudText>
|
|
||||||
</MudItem>
|
|
||||||
<MudItem xs="12" md="2">
|
|
||||||
<MudText Typo="Typo.caption">Version</MudText>
|
|
||||||
<MudText>@msg.Version</MudText>
|
|
||||||
</MudItem>
|
|
||||||
<MudItem xs="12" md="2">
|
|
||||||
<MudText Typo="Typo.caption">OrderUpdateId</MudText>
|
|
||||||
<MudChip T="string" Color="Color.Primary">@msg.OrderUpdateId</MudChip>
|
|
||||||
</MudItem>
|
|
||||||
</MudGrid>
|
|
||||||
</MudPaper>
|
|
||||||
|
|
||||||
<!-- ===================================================== -->
|
|
||||||
<!-- MAIN GRID -->
|
|
||||||
<!-- ===================================================== -->
|
|
||||||
<MudGrid Spacing="4">
|
|
||||||
<!-- POSITION + VELOCITY -->
|
|
||||||
<MudItem xs="12" md="6" lg="4">
|
|
||||||
<MudPaper Class="pa-5 h-100" Elevation="2">
|
|
||||||
<MudGrid AlignItems="AlignItems.Center">
|
|
||||||
<MudItem xs="8">
|
|
||||||
<MudText Typo="Typo.h6">📍 Position & Velocity</MudText>
|
|
||||||
</MudItem>
|
|
||||||
<MudItem xs="4" Class="d-flex justify-end">
|
|
||||||
<MudChip T="string" Size="Size.Small"
|
|
||||||
Color="@(msg.NewBaseRequest ? Color.Success : Color.Error)">
|
|
||||||
NewBase: @(msg.NewBaseRequest ? "TRUE" : "FALSE")
|
|
||||||
</MudChip>
|
|
||||||
</MudItem>
|
|
||||||
</MudGrid>
|
|
||||||
<MudDivider Class="my-3" />
|
|
||||||
<MudGrid Spacing="1">
|
|
||||||
<MudItem xs="6">
|
|
||||||
<MudText>X: <b>@msg.AgvPosition.X.ToString("F2")</b> m</MudText>
|
|
||||||
<MudText>Y: <b>@msg.AgvPosition.Y.ToString("F2")</b> m</MudText>
|
|
||||||
<MudText>θ: <b>@msg.AgvPosition.Theta.ToString("F2")</b> rad</MudText>
|
|
||||||
</MudItem>
|
|
||||||
<MudItem xs="6">
|
|
||||||
<MudText>Vx: <b>@msg.Velocity.Vx.ToString("F2")</b> m/s</MudText>
|
|
||||||
<MudText>Vy: <b>@msg.Velocity.Vy.ToString("F2")</b> m/s</MudText>
|
|
||||||
<MudText>Ω: <b>@msg.Velocity.Omega.ToString("F3")</b> rad/s</MudText>
|
|
||||||
</MudItem>
|
|
||||||
</MudGrid>
|
|
||||||
<MudDivider Class="my-3" />
|
|
||||||
<MudGrid Spacing="1">
|
|
||||||
<MudItem xs="6">
|
|
||||||
<MudChip T="string" Size="Size.Small"
|
|
||||||
Color="@(msg.AgvPosition.PositionInitialized ? Color.Success : Color.Error)">
|
|
||||||
Initialized: @(msg.AgvPosition.PositionInitialized ? "TRUE" : "FALSE")
|
|
||||||
</MudChip>
|
|
||||||
</MudItem>
|
|
||||||
<MudItem xs="6" Class="d-flex justify-end">
|
|
||||||
<MudText Typo="Typo.caption">
|
|
||||||
Deviation: <b>@msg.AgvPosition.DeviationRange</b>
|
|
||||||
</MudText>
|
|
||||||
</MudItem>
|
|
||||||
</MudGrid>
|
|
||||||
<MudProgressLinear Value="@(msg.AgvPosition.LocalizationScore * 100)"
|
|
||||||
Class="mt-3"
|
|
||||||
Color="Color.Success" />
|
|
||||||
<MudText Typo="Typo.caption">
|
|
||||||
Localization Score: @(msg.AgvPosition.LocalizationScore * 100) %
|
|
||||||
</MudText>
|
|
||||||
</MudPaper>
|
|
||||||
</MudItem>
|
|
||||||
|
|
||||||
<!-- BATTERY -->
|
|
||||||
<MudItem xs="12" md="6" lg="4">
|
|
||||||
<MudPaper Class="pa-5 h-100" Elevation="2">
|
|
||||||
<MudText Typo="Typo.h6">🔋 Battery</MudText>
|
|
||||||
<MudDivider Class="my-3" />
|
|
||||||
<MudProgressLinear Value="@msg.BatteryState.BatteryCharge"
|
|
||||||
Size="Size.Large"
|
|
||||||
Rounded
|
|
||||||
Color="@(msg.BatteryState.BatteryCharge > 50 ? Color.Success :
|
|
||||||
msg.BatteryState.BatteryCharge > 20 ? Color.Warning : Color.Error)" />
|
|
||||||
<MudText Typo="Typo.h4" Class="mt-2">
|
|
||||||
@msg.BatteryState.BatteryCharge:F1 %
|
|
||||||
</MudText>
|
|
||||||
<MudChip T="string" Size="Size.Small"
|
|
||||||
Color="@(msg.BatteryState.Charging ? Color.Info : Color.Default)">
|
|
||||||
@(msg.BatteryState.Charging ? "⚡ Charging" : "Discharging")
|
|
||||||
</MudChip>
|
|
||||||
<MudDivider Class="my-3" />
|
|
||||||
<MudGrid Spacing="1">
|
|
||||||
<MudItem xs="4">
|
|
||||||
<MudText Typo="Typo.caption">Voltage</MudText>
|
|
||||||
<MudText><b>@msg.BatteryState.BatteryVoltage.ToString("F1")</b> V</MudText>
|
|
||||||
</MudItem>
|
|
||||||
<MudItem xs="4">
|
|
||||||
<MudText Typo="Typo.caption">Health (SOH)</MudText>
|
|
||||||
<MudText><b>@msg.BatteryState.BatteryHealth</b> %</MudText>
|
|
||||||
</MudItem>
|
|
||||||
<MudItem xs="4">
|
|
||||||
<MudText Typo="Typo.caption">Reach</MudText>
|
|
||||||
<MudText><b>@((int)msg.BatteryState.Reach)</b> m</MudText>
|
|
||||||
</MudItem>
|
|
||||||
</MudGrid>
|
|
||||||
</MudPaper>
|
|
||||||
</MudItem>
|
|
||||||
|
|
||||||
<!-- ORDER & PATH -->
|
|
||||||
<MudItem xs="12" md="6" lg="4">
|
|
||||||
<MudPaper Class="pa-5 h-100" Elevation="2">
|
|
||||||
<MudText Typo="Typo.h6">🧭 Order & Path</MudText>
|
|
||||||
<MudDivider Class="my-3" />
|
|
||||||
<MudText>Order ID: <b>@(msg.OrderId ?? "—")</b></MudText>
|
|
||||||
<MudText>Update ID: <b>@msg.OrderUpdateId</b></MudText>
|
|
||||||
<MudDivider Class="my-2" />
|
|
||||||
<MudText>
|
|
||||||
Last Node: <b>@msg.LastNodeId</b>
|
|
||||||
<MudText Typo="Typo.caption" Inline="true">Seq: @msg.LastNodeSequenceId</MudText>
|
|
||||||
</MudText>
|
|
||||||
<MudText>Distance since last node: @msg.DistanceSinceLastNode:F1 m</MudText>
|
|
||||||
<MudDivider Class="my-3" />
|
|
||||||
@{
|
|
||||||
var nodeReleased = msg.NodeStates?.Count(n => n.Released) ?? 0;
|
|
||||||
var nodeTotal = msg.NodeStates?.Length ?? 0;
|
|
||||||
var edgeReleased = msg.EdgeStates?.Count(e => e.Released) ?? 0;
|
|
||||||
var edgeTotal = msg.EdgeStates?.Length ?? 0;
|
|
||||||
}
|
|
||||||
<div class="d-flex align-center flex-wrap gap-2">
|
|
||||||
<MudChip T="string" Color="Color.Info">Nodes: @nodeReleased / @nodeTotal</MudChip>
|
|
||||||
<MudChip T="string" Color="Color.Info">Edges: @edgeReleased / @edgeTotal</MudChip>
|
|
||||||
<MudChip T="string" Size="Size.Small" Color="@(msg.Driving ? Color.Success : Color.Default)">
|
|
||||||
@(msg.Driving ? "DRIVING" : "STOPPED")
|
|
||||||
</MudChip>
|
|
||||||
<MudChip T="string" Size="Size.Small" Color="@(msg.Paused ? Color.Warning : Color.Success)">
|
|
||||||
@(msg.Paused ? "PAUSED" : "ACTIVE")
|
|
||||||
</MudChip>
|
|
||||||
</div>
|
|
||||||
</MudPaper>
|
|
||||||
</MudItem>
|
|
||||||
|
|
||||||
<!-- ERRORS + INFORMATION -->
|
|
||||||
<MudItem xs="12" md="12" lg="6">
|
|
||||||
<MudPaper Class="pa-5 h-100" Elevation="2">
|
|
||||||
<MudText Typo="Typo.h6">🚨 Errors & Information</MudText>
|
|
||||||
<MudDivider Class="my-3" />
|
|
||||||
<MudTable Items="@MessageRows"
|
|
||||||
Dense="true"
|
|
||||||
Hover="true"
|
|
||||||
Bordered="true"
|
|
||||||
Elevation="0"
|
|
||||||
Height="200px"
|
|
||||||
FixedHeader="true">
|
|
||||||
<ColGroup>
|
|
||||||
<col style="width: 35%" />
|
|
||||||
<col style="width: 20%" />
|
|
||||||
<col />
|
|
||||||
</ColGroup>
|
|
||||||
<HeaderContent>
|
|
||||||
<MudTh>Type</MudTh>
|
|
||||||
<MudTh>Level</MudTh>
|
|
||||||
<MudTh>Description</MudTh>
|
|
||||||
</HeaderContent>
|
|
||||||
<RowTemplate>
|
|
||||||
<MudTd>
|
|
||||||
<MudText Class="@(context.IsError ? "text-error" : "text-info")">
|
|
||||||
<b>@context.Type</b>
|
|
||||||
</MudText>
|
|
||||||
</MudTd>
|
|
||||||
<MudTd>
|
|
||||||
<MudChip T="string" Size="Size.Small"
|
|
||||||
Color="@(context.Level.Contains("ERROR", StringComparison.OrdinalIgnoreCase) ? Color.Error :
|
|
||||||
context.Level.Contains("WARN", StringComparison.OrdinalIgnoreCase) ? Color.Warning :
|
|
||||||
Color.Info)">
|
|
||||||
@context.Level
|
|
||||||
</MudChip>
|
|
||||||
</MudTd>
|
|
||||||
<MudTd>
|
|
||||||
<MudText Typo="Typo.caption" Class="text-truncate" Title="@context.Description">
|
|
||||||
@context.Description
|
|
||||||
</MudText>
|
|
||||||
</MudTd>
|
|
||||||
</RowTemplate>
|
|
||||||
<NoRecordsContent>
|
|
||||||
<MudText Typo="Typo.caption" Color="Color.Secondary" Class="pa-4 text-center">
|
|
||||||
No errors or information messages
|
|
||||||
</MudText>
|
|
||||||
</NoRecordsContent>
|
|
||||||
</MudTable>
|
|
||||||
</MudPaper>
|
|
||||||
</MudItem>
|
|
||||||
|
|
||||||
<!-- ACTIONS -->
|
|
||||||
<MudItem xs="12" md="6" lg="3">
|
|
||||||
<MudPaper Class="pa-5 h-100" Elevation="2">
|
|
||||||
<MudText Typo="Typo.h6">⚙️ Actions</MudText>
|
|
||||||
<MudDivider Class="my-3" />
|
|
||||||
<MudTable Items="msg.ActionStates"
|
|
||||||
Dense="true"
|
|
||||||
Hover="true"
|
|
||||||
Bordered="true"
|
|
||||||
Elevation="0"
|
|
||||||
Height="180px"
|
|
||||||
FixedHeader="true">
|
|
||||||
<HeaderContent>
|
|
||||||
<MudTh>Action</MudTh>
|
|
||||||
<MudTh>Action ID</MudTh>
|
|
||||||
<MudTh class="text-right">Status</MudTh>
|
|
||||||
</HeaderContent>
|
|
||||||
<RowTemplate>
|
|
||||||
<MudTd><MudText Typo="Typo.body2" Class="text-truncate" Title="@context.ActionType">@context.ActionType</MudText></MudTd>
|
|
||||||
<MudTd><MudText Typo="Typo.caption">@context.ActionId</MudText></MudTd>
|
|
||||||
<MudTd Class="text-right">
|
|
||||||
<MudChip T="string" Size="Size.Small"
|
|
||||||
Color="@(context.ActionStatus == "RUNNING" ? Color.Info :
|
|
||||||
context.ActionStatus == "FINISHED" ? Color.Success :
|
|
||||||
context.ActionStatus == "FAILED" ? Color.Error : Color.Default)">
|
|
||||||
@context.ActionStatus
|
|
||||||
</MudChip>
|
|
||||||
</MudTd>
|
|
||||||
</RowTemplate>
|
|
||||||
<NoRecordsContent>
|
|
||||||
<MudText Typo="Typo.caption" Color="Color.Secondary" Class="pa-4 text-center">
|
|
||||||
No active actions
|
|
||||||
</MudText>
|
|
||||||
</NoRecordsContent>
|
|
||||||
</MudTable>
|
|
||||||
</MudPaper>
|
|
||||||
</MudItem>
|
|
||||||
|
|
||||||
<!-- SAFETY -->
|
|
||||||
<MudItem xs="12" md="6" lg="3">
|
|
||||||
<MudPaper Class="pa-5 h-100" Elevation="2">
|
|
||||||
<MudText Typo="Typo.h6">🛑 Safety</MudText>
|
|
||||||
<MudDivider Class="my-3" />
|
|
||||||
<MudChip T="string" Size="Size.Large" Class="w-100 mb-2"
|
|
||||||
Color="@(msg.SafetyState.EStop == "NONE" ? Color.Success : Color.Error)">
|
|
||||||
E-STOP: @msg.SafetyState.EStop
|
|
||||||
</MudChip>
|
|
||||||
<MudChip T="string" Size="Size.Large" Class="w-100"
|
|
||||||
Color="@(msg.SafetyState.FieldViolation ? Color.Error : Color.Success)">
|
|
||||||
Field Violation: @(msg.SafetyState.FieldViolation ? "YES" : "NO")
|
|
||||||
</MudChip>
|
|
||||||
</MudPaper>
|
|
||||||
</MudItem>
|
|
||||||
</MudGrid>
|
|
||||||
}
|
|
||||||
</MudContainer>
|
|
||||||
|
|
||||||
@code {
|
|
||||||
private StateMsg? CurrentState;
|
|
||||||
private bool IsConnected => RobotStateClient.LatestStates.ContainsKey(RobotSerial);
|
|
||||||
|
|
||||||
// Thay bằng serial number thật của robot bạn muốn theo dõi
|
|
||||||
private readonly string RobotSerial = "T800-002";
|
|
||||||
|
|
||||||
private List<MessageRow> MessageRows = new();
|
|
||||||
|
|
||||||
protected override async Task OnInitializedAsync()
|
|
||||||
{
|
|
||||||
// Subscribe sự kiện nhận state
|
|
||||||
RobotStateClient.OnStateReceived += OnRobotStateReceived;
|
|
||||||
|
|
||||||
// Bắt đầu kết nối SignalR (nếu chưa)
|
|
||||||
if (RobotStateClient.LatestStates.Count == 0)
|
|
||||||
{
|
|
||||||
await RobotStateClient.StartAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Đăng ký theo dõi robot cụ thể
|
|
||||||
await RobotStateClient.SubscribeRobotAsync(RobotSerial);
|
|
||||||
|
|
||||||
// Lấy state hiện tại nếu đã có
|
|
||||||
CurrentState = RobotStateClient.GetLatestState(RobotSerial);
|
|
||||||
UpdateMessageRows();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnRobotStateReceived(string serialNumber, StateMsg state)
|
|
||||||
{
|
|
||||||
if (serialNumber != RobotSerial) return;
|
|
||||||
|
|
||||||
InvokeAsync(() =>
|
|
||||||
{
|
|
||||||
CurrentState = state;
|
|
||||||
UpdateMessageRows();
|
|
||||||
StateHasChanged();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private void UpdateMessageRows()
|
|
||||||
{
|
|
||||||
MessageRows.Clear();
|
|
||||||
|
|
||||||
if (CurrentState?.Errors != null)
|
|
||||||
{
|
|
||||||
foreach (var err in CurrentState.Errors)
|
|
||||||
{
|
|
||||||
MessageRows.Add(new MessageRow(err.ErrorType ?? "-", err.ErrorLevel ?? "ERROR", err.ErrorDescription ?? "", true));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (CurrentState?.Information != null)
|
|
||||||
{
|
|
||||||
foreach (var info in CurrentState.Information)
|
|
||||||
{
|
|
||||||
MessageRows.Add(new MessageRow(info.InfoType ?? "-", info.InfoLevel ?? "INFO", info.InfoDescription ?? "", false));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Dispose()
|
|
||||||
{
|
|
||||||
RobotStateClient.OnStateReceived -= OnRobotStateReceived;
|
|
||||||
}
|
|
||||||
|
|
||||||
private record MessageRow(string Type, string Level, string Description, bool IsError);
|
|
||||||
}
|
|
||||||
|
|
@ -16,3 +16,11 @@
|
||||||
@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
|
||||||
|
|
@ -9,6 +9,7 @@
|
||||||
</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" />
|
||||||
|
|
|
||||||
|
|
@ -7,40 +7,61 @@ using System.Text.Json;
|
||||||
|
|
||||||
namespace RobotApp.Client.Services;
|
namespace RobotApp.Client.Services;
|
||||||
|
|
||||||
public class RobotStateClient : IAsyncDisposable
|
// ================= CONNECTION STATE =================
|
||||||
|
public enum RobotClientState
|
||||||
|
{
|
||||||
|
Disconnected,
|
||||||
|
Connecting,
|
||||||
|
Connected,
|
||||||
|
Reconnecting
|
||||||
|
}
|
||||||
|
|
||||||
|
// ================= CLIENT =================
|
||||||
|
public sealed class RobotStateClient : IAsyncDisposable
|
||||||
{
|
{
|
||||||
private readonly NavigationManager _nav;
|
private readonly NavigationManager _nav;
|
||||||
private HubConnection? _connection;
|
private HubConnection? _connection;
|
||||||
private bool _started;
|
|
||||||
private readonly object _lock = new();
|
private readonly object _lock = new();
|
||||||
|
private bool _started;
|
||||||
|
|
||||||
public ConcurrentDictionary<string, StateMsg> LatestStates { get; } = new();
|
public ConcurrentDictionary<string, StateMsg> LatestStates { get; } = new();
|
||||||
|
|
||||||
|
// ================= EVENTS =================
|
||||||
public event Action<string, StateMsg>? OnStateReceived;
|
public event Action<string, StateMsg>? OnStateReceived;
|
||||||
public event Action<StateMsg>? OnStateReceivedAny;
|
public event Action<StateMsg>? OnStateReceivedAny;
|
||||||
|
public event Action<RobotClientState>? OnConnectionStateChanged;
|
||||||
|
|
||||||
|
public RobotClientState ConnectionState { get; private set; } = RobotClientState.Disconnected;
|
||||||
|
|
||||||
|
// ================= CTOR =================
|
||||||
public RobotStateClient(NavigationManager nav)
|
public RobotStateClient(NavigationManager nav)
|
||||||
{
|
{
|
||||||
_nav = 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")
|
public async Task StartAsync(string hubPath = "/hubs/robot")
|
||||||
{
|
{
|
||||||
lock (_lock)
|
lock (_lock)
|
||||||
{
|
{
|
||||||
if (_started)
|
if (_started) return;
|
||||||
return;
|
|
||||||
|
|
||||||
_started = true;
|
_started = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(hubPath))
|
SetState(RobotClientState.Connecting);
|
||||||
throw new ArgumentException("Hub path is empty", nameof(hubPath));
|
|
||||||
|
|
||||||
var hubUri = _nav.ToAbsoluteUri(hubPath);
|
|
||||||
Console.WriteLine($"[SIGNALR] Connecting to {hubUri}");
|
|
||||||
|
|
||||||
_connection = new HubConnectionBuilder()
|
_connection = new HubConnectionBuilder()
|
||||||
.WithUrl(hubUri)
|
.WithUrl(_nav.ToAbsoluteUri(hubPath))
|
||||||
.WithAutomaticReconnect(new[]
|
.WithAutomaticReconnect(new[]
|
||||||
{
|
{
|
||||||
TimeSpan.Zero,
|
TimeSpan.Zero,
|
||||||
|
|
@ -49,11 +70,21 @@ public class RobotStateClient : IAsyncDisposable
|
||||||
})
|
})
|
||||||
.Build();
|
.Build();
|
||||||
|
|
||||||
|
_connection.Reconnecting += _ =>
|
||||||
|
{
|
||||||
|
SetState(RobotClientState.Reconnecting);
|
||||||
|
return Task.CompletedTask;
|
||||||
|
};
|
||||||
|
|
||||||
|
_connection.Reconnected += _ =>
|
||||||
|
{
|
||||||
|
SetState(RobotClientState.Connected);
|
||||||
|
return Task.CompletedTask;
|
||||||
|
};
|
||||||
|
|
||||||
_connection.Closed += async error =>
|
_connection.Closed += async error =>
|
||||||
{
|
{
|
||||||
Console.WriteLine($"[SIGNALR] Connection closed: {error?.Message}");
|
|
||||||
_started = false;
|
_started = false;
|
||||||
await Task.Delay(3000);
|
|
||||||
|
|
||||||
if (_connection != null)
|
if (_connection != null)
|
||||||
{
|
{
|
||||||
|
|
@ -65,95 +96,93 @@ public class RobotStateClient : IAsyncDisposable
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
_connection.Reconnecting += error =>
|
_connection.On<string>("ReceiveState", HandleState);
|
||||||
{
|
|
||||||
Console.WriteLine($"[SIGNALR] Reconnecting... {error?.Message}");
|
|
||||||
return Task.CompletedTask;
|
|
||||||
};
|
|
||||||
|
|
||||||
_connection.Reconnected += connectionId =>
|
|
||||||
{
|
|
||||||
Console.WriteLine($"[SIGNALR] Reconnected: {connectionId}");
|
|
||||||
return Task.CompletedTask;
|
|
||||||
};
|
|
||||||
|
|
||||||
_connection.On<string>("ReceiveState", stateJson =>
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var state = JsonSerializer.Deserialize<StateMsg>(
|
|
||||||
stateJson,
|
|
||||||
JsonOptionExtends.Read
|
|
||||||
);
|
|
||||||
|
|
||||||
if (state?.SerialNumber == null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
LatestStates[state.SerialNumber] = state;
|
|
||||||
|
|
||||||
OnStateReceived?.Invoke(state.SerialNumber, state);
|
|
||||||
OnStateReceivedAny?.Invoke(state);
|
|
||||||
|
|
||||||
Console.WriteLine(
|
|
||||||
$"[CLIENT] {state.SerialNumber} | " +
|
|
||||||
$"X={state.AgvPosition?.X:F2}, " +
|
|
||||||
$"Y={state.AgvPosition?.Y:F2}, " +
|
|
||||||
$"Battery={state.BatteryState?.BatteryCharge:F1}%"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Console.WriteLine($"[CLIENT] Deserialize error: {ex.Message}");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await _connection.StartAsync();
|
await _connection.StartAsync();
|
||||||
Console.WriteLine("[SIGNALR] Connected successfully!");
|
SetState(RobotClientState.Connected);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch
|
||||||
{
|
{
|
||||||
_started = false;
|
_started = false;
|
||||||
Console.WriteLine($"❌ [SIGNALR] Connection failed: {ex.Message}");
|
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)
|
public async Task SubscribeRobotAsync(string serialNumber)
|
||||||
{
|
{
|
||||||
if (_connection?.State != HubConnectionState.Connected)
|
if (_connection?.State != HubConnectionState.Connected)
|
||||||
throw new InvalidOperationException("SignalR is not connected");
|
return;
|
||||||
|
|
||||||
await _connection.InvokeAsync("JoinRobot", serialNumber);
|
try
|
||||||
Console.WriteLine($"[SIGNALR] Subscribed to {serialNumber}");
|
{
|
||||||
|
await _connection.InvokeAsync("JoinRobot", serialNumber);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// ignore – reconnect sẽ tự join lại
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task UnsubscribeRobotAsync(string serialNumber)
|
public async Task UnsubscribeRobotAsync(string serialNumber)
|
||||||
{
|
{
|
||||||
if (_connection?.State == HubConnectionState.Connected)
|
if (_connection?.State == HubConnectionState.Connected)
|
||||||
{
|
{
|
||||||
await _connection.InvokeAsync("LeaveRobot", serialNumber);
|
try
|
||||||
|
{
|
||||||
|
await _connection.InvokeAsync("LeaveRobot", serialNumber);
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
}
|
}
|
||||||
|
|
||||||
LatestStates.TryRemove(serialNumber, out _);
|
LatestStates.TryRemove(serialNumber, out _);
|
||||||
Console.WriteLine($"[SIGNALR] Unsubscribed from {serialNumber}");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ================= GET CACHE =================
|
||||||
public StateMsg? GetLatestState(string serialNumber)
|
public StateMsg? GetLatestState(string serialNumber)
|
||||||
{
|
{
|
||||||
LatestStates.TryGetValue(serialNumber, out var state);
|
LatestStates.TryGetValue(serialNumber, out var state);
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ================= DISPOSE =================
|
||||||
public async ValueTask DisposeAsync()
|
public async ValueTask DisposeAsync()
|
||||||
{
|
{
|
||||||
_started = false;
|
_started = false;
|
||||||
|
SetState(RobotClientState.Disconnected);
|
||||||
|
|
||||||
if (_connection != null)
|
if (_connection != null)
|
||||||
{
|
{
|
||||||
await _connection.DisposeAsync();
|
await _connection.DisposeAsync();
|
||||||
_connection = null;
|
_connection = null;
|
||||||
Console.WriteLine("[SIGNALR] Disposed");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,3 +13,6 @@
|
||||||
@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 @@
|
||||||
|
|
||||||
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 18
|
||||||
VisualStudioVersion = 18.0.11205.157 d18.0
|
VisualStudioVersion = 18.0.11205.157
|
||||||
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
|
||||||
|
|
|
||||||
|
|
@ -153,10 +153,10 @@ public class RobotStatePublisher : BackgroundService
|
||||||
.Group(serialNumber)
|
.Group(serialNumber)
|
||||||
.SendAsync("ReceiveState", json, stoppingToken);
|
.SendAsync("ReceiveState", json, stoppingToken);
|
||||||
|
|
||||||
Console.WriteLine($"[RobotStatePublisher] Published state for {serialNumber} | " +
|
//Console.WriteLine($"[RobotStatePublisher] Published state for {serialNumber} | " +
|
||||||
$"HeaderId: {state.HeaderId} | " +
|
// $"HeaderId: {state.HeaderId} | " +
|
||||||
$"Pos: ({state.AgvPosition.X:F2}, {state.AgvPosition.Y:F2}) | " +
|
// $"Pos: ({state.AgvPosition.X:F2}, {state.AgvPosition.Y:F2}) | " +
|
||||||
$"Battery: {state.BatteryState.BatteryCharge:F1}%");
|
// $"Battery: {state.BatteryState.BatteryCharge:F1}%");
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user