save
This commit is contained in:
parent
f52f0fd8da
commit
93599f5c95
|
|
@ -73,7 +73,7 @@
|
|||
// 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-application-cog", Path="/robot-config", Label = "Config", Match = NavLinkMatch.All},
|
||||
new(){Icon = "mdi-application-cog", Path="/robot-state", Label = "state", Match = NavLinkMatch.All},
|
||||
new(){Icon = "mdi-state-machine", Path="/robot-state", Label = "state", Match = NavLinkMatch.All},
|
||||
];
|
||||
|
||||
private bool collapseNavMenu = true;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
@page "/"
|
||||
@using System.Text.Json
|
||||
@using System.Text.Json.Serialization
|
||||
@using RobotApp.Client.Services
|
||||
@using RobotApp.VDA5050.InstantAction
|
||||
@using RobotApp.VDA5050.Order
|
||||
@using RobotApp.VDA5050.Type
|
||||
|
|
@ -41,7 +42,7 @@
|
|||
<MudExpansionPanels MultiExpansion="true">
|
||||
@foreach (var node in Order.Nodes)
|
||||
{
|
||||
<MudExpansionPanel @key="node">
|
||||
<MudExpansionPanel @key="node.NodeId">
|
||||
|
||||
<!-- ===== HEADER ===== -->
|
||||
<TitleContent>
|
||||
|
|
@ -226,51 +227,107 @@
|
|||
<!-- Cuộn nội bộ chỉ cho phần danh sách Edges -->
|
||||
<div class="flex-grow-1" style="overflow:auto;">
|
||||
<MudExpansionPanels MultiExpansion="true">
|
||||
|
||||
@foreach (var edge in Order.Edges)
|
||||
{
|
||||
<MudExpansionPanel Text="@($"{edge.EdgeId} ({edge.StartNodeId} → {edge.EndNodeId})")" @key="edge">
|
||||
<MudGrid Spacing="3">
|
||||
<MudItem xs="12"><MudTextField @bind-Value="edge.EdgeId" Label="Edge ID" /></MudItem>
|
||||
<MudItem xs="12">
|
||||
<MudSelect T="string"
|
||||
Label="Start Node"
|
||||
Dense="true"
|
||||
Required="true"
|
||||
@bind-Value="edge.StartNodeId">
|
||||
@foreach (var node in Order.Nodes)
|
||||
{
|
||||
<MudSelectItem Value="@node.NodeId">
|
||||
@node.NodeId
|
||||
</MudSelectItem>
|
||||
}
|
||||
</MudSelect>
|
||||
</MudItem>
|
||||
<MudExpansionPanel Text="@($"{edge.EdgeId} ({edge.StartNodeId} → {edge.EndNodeId})")"
|
||||
@key="edge.EdgeId">
|
||||
|
||||
<MudItem xs="12">
|
||||
<MudSelect T="string"
|
||||
Label="End Node"
|
||||
Dense="true"
|
||||
Required="true"
|
||||
@bind-Value="edge.EndNodeId">
|
||||
@foreach (var node in Order.Nodes)
|
||||
{
|
||||
<MudSelectItem Value="@node.NodeId"
|
||||
Disabled="@(node.NodeId == edge.StartNodeId)">
|
||||
@node.NodeId
|
||||
</MudSelectItem>
|
||||
}
|
||||
</MudSelect>
|
||||
</MudItem>
|
||||
<ChildContent>
|
||||
<MudGrid Spacing="3">
|
||||
|
||||
<!-- Edge ID -->
|
||||
<MudItem xs="12">
|
||||
<MudTextField @bind-Value="edge.EdgeId"
|
||||
Label="Edge ID" />
|
||||
</MudItem>
|
||||
|
||||
<!-- Start Node -->
|
||||
<MudItem xs="12">
|
||||
<MudSelect T="string"
|
||||
Label="Start Node"
|
||||
Dense="true"
|
||||
Required="true"
|
||||
@bind-Value="edge.StartNodeId">
|
||||
@foreach (var node in Order.Nodes)
|
||||
{
|
||||
<MudSelectItem Value="@node.NodeId">
|
||||
@node.NodeId
|
||||
</MudSelectItem>
|
||||
}
|
||||
</MudSelect>
|
||||
</MudItem>
|
||||
|
||||
<!-- End Node -->
|
||||
<MudItem xs="12">
|
||||
<MudSelect T="string"
|
||||
Label="End Node"
|
||||
Dense="true"
|
||||
Required="true"
|
||||
@bind-Value="edge.EndNodeId">
|
||||
@foreach (var node in Order.Nodes)
|
||||
{
|
||||
<MudSelectItem Value="@node.NodeId"
|
||||
Disabled="@(node.NodeId == edge.StartNodeId)">
|
||||
@node.NodeId
|
||||
</MudSelectItem>
|
||||
}
|
||||
</MudSelect>
|
||||
</MudItem>
|
||||
|
||||
<!-- Radius: =0 → đường thẳng -->
|
||||
<MudItem xs="6">
|
||||
<MudNumericField T="double"
|
||||
@bind-Value="edge.Radius"
|
||||
Label="Bán kính (0 = đường thẳng)"
|
||||
Min="0"
|
||||
Step="0.1" />
|
||||
</MudItem>
|
||||
|
||||
<!-- Quadrant chỉ hiện khi Radius > 0 -->
|
||||
@if (edge.Radius > 0)
|
||||
{
|
||||
<MudItem xs="6">
|
||||
<MudSelect T="Quadrant"
|
||||
@bind-Value="edge.Quadrant"
|
||||
Label="Góc phần tư">
|
||||
<MudSelectItem Value="Quadrant.I">Góc I</MudSelectItem>
|
||||
<MudSelectItem Value="Quadrant.II">Góc II</MudSelectItem>
|
||||
<MudSelectItem Value="Quadrant.III">Góc III</MudSelectItem>
|
||||
<MudSelectItem Value="Quadrant.IV">Góc IV</MudSelectItem>
|
||||
</MudSelect>
|
||||
</MudItem>
|
||||
}
|
||||
@if (edge.Radius > 0 && !edge.Expanded)
|
||||
{
|
||||
<MudItem xs="12">
|
||||
<MudButton Color="Color.Primary"
|
||||
Variant="Variant.Outlined"
|
||||
StartIcon="@Icons.Material.Filled.Merge"
|
||||
OnClick="@(() => ApplyCurve(edge))">
|
||||
Apply Curve (tạo node)
|
||||
</MudButton>
|
||||
</MudItem>
|
||||
}
|
||||
|
||||
<MudItem xs="12" Class="mt-4">
|
||||
<MudButton Color="Color.Error"
|
||||
Variant="Variant.Text"
|
||||
StartIcon="@Icons.Material.Filled.Delete"
|
||||
OnClick="@(() => RemoveEdge(edge))">
|
||||
Remove Edge
|
||||
</MudButton>
|
||||
</MudItem>
|
||||
|
||||
</MudGrid>
|
||||
</ChildContent>
|
||||
|
||||
<MudItem xs="12" Class="mt-4">
|
||||
<MudButton Color="Color.Error" Variant="Variant.Text" StartIcon="@Icons.Material.Filled.Delete"
|
||||
OnClick="@(() => RemoveEdge(edge))">Remove Edge</MudButton>
|
||||
</MudItem>
|
||||
</MudGrid>
|
||||
</MudExpansionPanel>
|
||||
}
|
||||
|
||||
</MudExpansionPanels>
|
||||
</div>
|
||||
|
||||
</MudPaper>
|
||||
</MudItem>
|
||||
</MudGrid>
|
||||
|
|
@ -369,6 +426,25 @@
|
|||
bool IsEditNodeOpen;
|
||||
Node? EditingNode;
|
||||
OrderMessage Order = new();
|
||||
void ApplyCurve(UiEdge edge)
|
||||
{
|
||||
if (edge.Radius <= 0 || edge.Expanded)
|
||||
return;
|
||||
|
||||
var startNode = Order.Nodes.First(n => n.NodeId == edge.StartNodeId);
|
||||
|
||||
// 1️⃣ Sinh node C (điểm kết thúc cung tròn)
|
||||
var endNode = OrderMessage.CreateCurveNode(startNode, edge);
|
||||
Order.Nodes.Add(endNode);
|
||||
|
||||
// 2️⃣ Edge trở thành A → C (đường cong)
|
||||
edge.EndNodeId = endNode.NodeId;
|
||||
edge.Expanded = true;
|
||||
|
||||
// 3️⃣ Resequence node (đúng chuẩn VDA5050)
|
||||
for (int i = 0; i < Order.Nodes.Count; i++)
|
||||
Order.Nodes[i].SequenceId = i;
|
||||
}
|
||||
|
||||
async Task SendOrderToServer()
|
||||
{
|
||||
|
|
@ -413,7 +489,7 @@
|
|||
NodeId = $"NODE_{Order.Nodes.Count + 1}",
|
||||
SequenceId = Order.Nodes.Count,
|
||||
Released = true,
|
||||
NodePosition = new NodePosition()
|
||||
NodePosition = new NodePosition { MapId = "MAP_01" }
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -469,7 +545,7 @@
|
|||
{
|
||||
if (Order.Nodes.Count < 2) return;
|
||||
|
||||
Order.Edges.Add(new VDA5050.Order.Edge
|
||||
Order.Edges.Add(new RobotApp.Client.Services.UiEdge
|
||||
{
|
||||
EdgeId = $"EDGE_{Order.Edges.Count + 1}",
|
||||
StartNodeId = Order.Nodes[^2].NodeId,
|
||||
|
|
@ -507,7 +583,7 @@
|
|||
}
|
||||
|
||||
|
||||
void RemoveEdge(VDA5050.Order.Edge edge)
|
||||
void RemoveEdge(RobotApp.Client.Services.UiEdge edge)
|
||||
{
|
||||
Order.Edges.Remove(edge);
|
||||
}
|
||||
|
|
@ -522,113 +598,6 @@
|
|||
});
|
||||
|
||||
|
||||
public class UiActionParameter : ActionParameter
|
||||
{
|
||||
[JsonIgnore]
|
||||
public string ValueString
|
||||
{
|
||||
get => Value?.ToString() ?? "";
|
||||
set => Value = value;
|
||||
}
|
||||
}
|
||||
|
||||
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; } = "AMR-01";
|
||||
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<VDA5050.Order.Edge> Edges { get; set; } = new();
|
||||
|
||||
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(), // ✅ QUAN TRỌNG
|
||||
|
||||
// ================= EDGES =================
|
||||
edges = Edges
|
||||
.Select(e => new
|
||||
{
|
||||
edgeId = e.EdgeId,
|
||||
sequenceId = seq++,
|
||||
released = true,
|
||||
|
||||
startNodeId = e.StartNodeId,
|
||||
endNodeId = e.EndNodeId,
|
||||
|
||||
actions = Array.Empty<object>()
|
||||
})
|
||||
.ToArray() // ✅ QUAN TRỌNG
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private async Task OpenEditNodeDialog(Node node)
|
||||
{
|
||||
var parameters = new DialogParameters<EditNodeDialog>
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
@page "/robot-state"
|
||||
@using RobotApp.Client.Services
|
||||
@using RobotApp.VDA5050.State
|
||||
@rendermode InteractiveWebAssemblyNoPrerender
|
||||
|
||||
@inject StateMsg RobotState
|
||||
@inject RobotStateClient RobotStateClient
|
||||
@implements IDisposable
|
||||
@rendermode InteractiveWebAssembly
|
||||
|
||||
<MudContainer MaxWidth="MaxWidth.False" Class="pa-4">
|
||||
<!-- ===================================================== -->
|
||||
|
|
@ -11,34 +12,46 @@
|
|||
<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>
|
||||
<MudText Typo="Typo.subtitle2" Color="Color.Secondary">
|
||||
@RobotState.Version •
|
||||
@RobotState.Manufacturer
|
||||
@RobotState.SerialNumber
|
||||
</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 (RobotState.Current != null)
|
||||
|
||||
@if (CurrentState != null)
|
||||
{
|
||||
<MudChip T="string" Size="Size.Large"
|
||||
Color="@(RobotState.Current ? Color.Success : Color.Error)">
|
||||
@(RobotState.Current.IsOnline ? "ONLINE" : "OFFLINE")
|
||||
Color="@(IsConnected ? Color.Success : Color.Error)"
|
||||
Variant="Variant.Filled">
|
||||
@(IsConnected ? "ONLINE" : "OFFLINE")
|
||||
</MudChip>
|
||||
} *@
|
||||
}
|
||||
</MudPaper>
|
||||
|
||||
@if (RobotState == null)
|
||||
@if (CurrentState == null)
|
||||
{
|
||||
<MudAlert Severity="Severity.Info" Variant="Variant.Outlined">
|
||||
Waiting for robot state (VDA5050)...
|
||||
<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 = RobotState;
|
||||
var msg = CurrentState;
|
||||
|
||||
<!-- ===================================================== -->
|
||||
<!-- 📨 MESSAGE META (DEBUG / TRACE) -->
|
||||
<!-- MESSAGE META -->
|
||||
<!-- ===================================================== -->
|
||||
<MudPaper Class="pa-3 mb-4" Elevation="1">
|
||||
<MudGrid Spacing="2">
|
||||
|
|
@ -56,9 +69,7 @@
|
|||
</MudItem>
|
||||
<MudItem xs="12" md="2">
|
||||
<MudText Typo="Typo.caption">OrderUpdateId</MudText>
|
||||
<MudChip T="string" Color="Color.Primary">
|
||||
@msg.OrderUpdateId
|
||||
</MudChip>
|
||||
<MudChip T="string" Color="Color.Primary">@msg.OrderUpdateId</MudChip>
|
||||
</MudItem>
|
||||
</MudGrid>
|
||||
</MudPaper>
|
||||
|
|
@ -104,7 +115,7 @@
|
|||
</MudItem>
|
||||
<MudItem xs="6" Class="d-flex justify-end">
|
||||
<MudText Typo="Typo.caption">
|
||||
DeviationRange: <b>@msg.AgvPosition.DeviationRange</b>
|
||||
Deviation: <b>@msg.AgvPosition.DeviationRange</b>
|
||||
</MudText>
|
||||
</MudItem>
|
||||
</MudGrid>
|
||||
|
|
@ -141,7 +152,7 @@
|
|||
<MudText><b>@msg.BatteryState.BatteryVoltage.ToString("F1")</b> V</MudText>
|
||||
</MudItem>
|
||||
<MudItem xs="4">
|
||||
<MudText Typo="Typo.caption">Health</MudText>
|
||||
<MudText Typo="Typo.caption">Health (SOH)</MudText>
|
||||
<MudText><b>@msg.BatteryState.BatteryHealth</b> %</MudText>
|
||||
</MudItem>
|
||||
<MudItem xs="4">
|
||||
|
|
@ -164,16 +175,14 @@
|
|||
Last Node: <b>@msg.LastNodeId</b>
|
||||
<MudText Typo="Typo.caption" Inline="true">Seq: @msg.LastNodeSequenceId</MudText>
|
||||
</MudText>
|
||||
<MudText>Distance: @msg.DistanceSinceLastNode:F1 m</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>
|
||||
|
|
@ -190,175 +199,89 @@
|
|||
<!-- 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" />
|
||||
|
||||
@{
|
||||
var rows = new List<MessageRow>();
|
||||
|
||||
if (msg.Errors != null)
|
||||
{
|
||||
foreach (var err in msg.Errors)
|
||||
{
|
||||
rows.Add(new MessageRow(
|
||||
err.ErrorType ?? "-",
|
||||
err.ErrorLevel ?? "-",
|
||||
err.ErrorDescription ?? "",
|
||||
true
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
if (msg.Information != null)
|
||||
{
|
||||
foreach (var info in msg.Information)
|
||||
{
|
||||
rows.Add(new MessageRow(
|
||||
info.InfoType ?? "-",
|
||||
info.InfoLevel ?? "-",
|
||||
info.InfoDescription ?? "",
|
||||
false
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
var sortedMessages = rows
|
||||
.OrderBy(r => r.IsError ? 0 : 1) // Errors trước
|
||||
.ThenBy(r => r.Type)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
<MudTable Items="@sortedMessages"
|
||||
<MudTable Items="@MessageRows"
|
||||
Dense="true"
|
||||
Hover="true"
|
||||
Bordered="true"
|
||||
Elevation="0"
|
||||
Height="180px"
|
||||
Breakpoint="Breakpoint.Sm"
|
||||
HorizontalScrollbar="true">
|
||||
|
||||
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 DataLabel="Type">
|
||||
<MudTd>
|
||||
<MudText Class="@(context.IsError ? "text-error" : "text-info")">
|
||||
<b>@context.Type</b>
|
||||
</MudText>
|
||||
</MudTd>
|
||||
|
||||
<MudTd DataLabel="Level">
|
||||
<MudChip T="string"
|
||||
Size="Size.Small"
|
||||
Color="@(context.Level == "ERROR"
|
||||
? Color.Error
|
||||
: context.Level == "WARNING"
|
||||
? Color.Warning
|
||||
: Color.Info)">
|
||||
@context.Level
|
||||
</MudChip>
|
||||
</MudTd>
|
||||
|
||||
<MudTd DataLabel="Description">
|
||||
<MudText Typo="Typo.caption"
|
||||
Class="text-truncate"
|
||||
Title="@context.Description">
|
||||
<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">
|
||||
<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"
|
||||
Dense="true"
|
||||
Hover="true"
|
||||
Bordered="true"
|
||||
Elevation="0"
|
||||
Height="160px"
|
||||
Breakpoint="Breakpoint.Sm"
|
||||
HorizontalScrollbar="true"
|
||||
Height="180px"
|
||||
FixedHeader="true">
|
||||
|
||||
<ColGroup>
|
||||
<col style="width: 40%" />
|
||||
<col style="width: 35%" />
|
||||
<col style="width: 25%" />
|
||||
</ColGroup>
|
||||
|
||||
<HeaderContent>
|
||||
<MudTh>Action</MudTh>
|
||||
<MudTh>Action ID</MudTh>
|
||||
<MudTh class="text-right">Status</MudTh>
|
||||
</HeaderContent>
|
||||
|
||||
<RowTemplate>
|
||||
<MudTd DataLabel="Action">
|
||||
<MudText Typo="Typo.body2"
|
||||
Class="text-truncate"
|
||||
Title="@context.ActionType">
|
||||
@context.ActionType
|
||||
</MudText>
|
||||
<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>
|
||||
|
||||
<MudTd DataLabel="Action ID">
|
||||
<MudText Typo="Typo.caption">
|
||||
@context.ActionId
|
||||
</MudText>
|
||||
</MudTd>
|
||||
|
||||
<MudTd DataLabel="Status" Class="text-right">
|
||||
<MudChip T="string"
|
||||
Size="Size.Small"
|
||||
Variant="Variant.Filled"
|
||||
Color="@(context.ActionStatus == "RUNNING"
|
||||
? Color.Info
|
||||
: context.ActionStatus == "FINISHED"
|
||||
? Color.Success
|
||||
: Color.Error)">
|
||||
@context.ActionStatus
|
||||
</MudChip>
|
||||
</MudTd>
|
||||
</RowTemplate>
|
||||
|
||||
</RowTemplate>
|
||||
<NoRecordsContent>
|
||||
<MudText Typo="Typo.caption"
|
||||
Color="Color.Secondary"
|
||||
Class="pa-4 text-center">
|
||||
<MudText Typo="Typo.caption" Color="Color.Secondary" Class="pa-4 text-center">
|
||||
No active actions
|
||||
</MudText>
|
||||
</NoRecordsContent>
|
||||
|
||||
</MudTable>
|
||||
|
||||
</MudPaper>
|
||||
</MudItem>
|
||||
|
||||
|
|
@ -377,21 +300,75 @@
|
|||
</MudChip>
|
||||
</MudPaper>
|
||||
</MudItem>
|
||||
|
||||
</MudGrid>
|
||||
}
|
||||
</MudContainer>
|
||||
|
||||
@code {
|
||||
private StateMsg? CurrentState;
|
||||
private bool IsConnected => RobotStateClient.LatestStates.ContainsKey(RobotSerial);
|
||||
|
||||
private async Task OnStateChanged()
|
||||
=> await InvokeAsync(StateHasChanged);
|
||||
// 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();
|
||||
|
||||
private record MessageRow(
|
||||
string Type,
|
||||
string Level,
|
||||
string Description,
|
||||
bool IsError
|
||||
);
|
||||
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);
|
||||
}
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
|
||||
using MudBlazor.Services;
|
||||
using RobotApp.Client.Services;
|
||||
using System.Globalization;
|
||||
|
||||
CultureInfo.DefaultThreadCurrentCulture = new CultureInfo("en-US");
|
||||
|
|
@ -11,6 +12,8 @@ builder.Services.AddAuthenticationStateDeserialization();
|
|||
|
||||
builder.Services.AddScoped(_ => new HttpClient() { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });
|
||||
builder.Services.AddScoped<RobotApp.Client.Services.RobotMonitorService>();
|
||||
builder.Services.AddScoped<RobotApp.Client.Services.RobotStateClient>();
|
||||
|
||||
builder.Services.AddMudServices(config =>
|
||||
{
|
||||
config.SnackbarConfiguration.VisibleStateDuration = 2000;
|
||||
|
|
|
|||
159
RobotApp.Client/Services/RobotStateClient.cs
Normal file
159
RobotApp.Client/Services/RobotStateClient.cs
Normal file
|
|
@ -0,0 +1,159 @@
|
|||
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;
|
||||
|
||||
public class RobotStateClient : IAsyncDisposable
|
||||
{
|
||||
private readonly NavigationManager _nav;
|
||||
private HubConnection? _connection;
|
||||
private bool _started;
|
||||
private readonly object _lock = new();
|
||||
public ConcurrentDictionary<string, StateMsg> LatestStates { get; } = new();
|
||||
|
||||
public event Action<string, StateMsg>? OnStateReceived;
|
||||
public event Action<StateMsg>? OnStateReceivedAny;
|
||||
|
||||
public RobotStateClient(NavigationManager nav)
|
||||
{
|
||||
_nav = nav;
|
||||
}
|
||||
|
||||
public async Task StartAsync(string hubPath = "/hubs/robot")
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
if (_started)
|
||||
return;
|
||||
|
||||
_started = true;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(hubPath))
|
||||
throw new ArgumentException("Hub path is empty", nameof(hubPath));
|
||||
|
||||
var hubUri = _nav.ToAbsoluteUri(hubPath);
|
||||
Console.WriteLine($"[SIGNALR] Connecting to {hubUri}");
|
||||
|
||||
_connection = new HubConnectionBuilder()
|
||||
.WithUrl(hubUri)
|
||||
.WithAutomaticReconnect(new[]
|
||||
{
|
||||
TimeSpan.Zero,
|
||||
TimeSpan.FromSeconds(2),
|
||||
TimeSpan.FromSeconds(10)
|
||||
})
|
||||
.Build();
|
||||
|
||||
_connection.Closed += async error =>
|
||||
{
|
||||
Console.WriteLine($"[SIGNALR] Connection closed: {error?.Message}");
|
||||
_started = false;
|
||||
await Task.Delay(3000);
|
||||
|
||||
if (_connection != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
await StartAsync(hubPath);
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
};
|
||||
|
||||
_connection.Reconnecting += error =>
|
||||
{
|
||||
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
|
||||
{
|
||||
await _connection.StartAsync();
|
||||
Console.WriteLine("[SIGNALR] Connected successfully!");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_started = false;
|
||||
Console.WriteLine($"❌ [SIGNALR] Connection failed: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
public async Task SubscribeRobotAsync(string serialNumber)
|
||||
{
|
||||
if (_connection?.State != HubConnectionState.Connected)
|
||||
throw new InvalidOperationException("SignalR is not connected");
|
||||
|
||||
await _connection.InvokeAsync("JoinRobot", serialNumber);
|
||||
Console.WriteLine($"[SIGNALR] Subscribed to {serialNumber}");
|
||||
}
|
||||
|
||||
public async Task UnsubscribeRobotAsync(string serialNumber)
|
||||
{
|
||||
if (_connection?.State == HubConnectionState.Connected)
|
||||
{
|
||||
await _connection.InvokeAsync("LeaveRobot", serialNumber);
|
||||
}
|
||||
|
||||
LatestStates.TryRemove(serialNumber, out _);
|
||||
Console.WriteLine($"[SIGNALR] Unsubscribed from {serialNumber}");
|
||||
}
|
||||
|
||||
public StateMsg? GetLatestState(string serialNumber)
|
||||
{
|
||||
LatestStates.TryGetValue(serialNumber, out var state);
|
||||
return state;
|
||||
}
|
||||
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
_started = false;
|
||||
|
||||
if (_connection != null)
|
||||
{
|
||||
await _connection.DisposeAsync();
|
||||
_connection = null;
|
||||
Console.WriteLine("[SIGNALR] Disposed");
|
||||
}
|
||||
}
|
||||
}
|
||||
257
RobotApp.Client/Services/UiEdge.cs
Normal file
257
RobotApp.Client/Services/UiEdge.cs
Normal file
|
|
@ -0,0 +1,257 @@
|
|||
using RobotApp.VDA5050.InstantAction;
|
||||
using RobotApp.VDA5050.Order;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace RobotApp.Client.Services;
|
||||
|
||||
// ======================================================
|
||||
// EDGE UI
|
||||
// ======================================================
|
||||
public class UiEdge : VDA5050.Order.Edge
|
||||
{
|
||||
public double Radius { get; set; } = 0.0;
|
||||
public Quadrant Quadrant { get; set; } = Quadrant.I;
|
||||
|
||||
// Đánh dấu đã expand chưa
|
||||
public bool Expanded { get; set; } = false;
|
||||
}
|
||||
|
||||
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 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 =>
|
||||
{
|
||||
// ---------- ĐƯỜNG THẲNG ----------
|
||||
if (e.Radius <= 0)
|
||||
{
|
||||
return new
|
||||
{
|
||||
edgeId = e.EdgeId,
|
||||
sequenceId = seq++,
|
||||
released = true,
|
||||
startNodeId = e.StartNodeId,
|
||||
endNodeId = e.EndNodeId,
|
||||
actions = Array.Empty<object>()
|
||||
};
|
||||
}
|
||||
|
||||
// ---------- ĐƯỜNG CONG 1/4 ----------
|
||||
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;
|
||||
}
|
||||
}
|
||||
29
RobotApp/Hubs/RobotHub.cs
Normal file
29
RobotApp/Hubs/RobotHub.cs
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -4,11 +4,13 @@ using Microsoft.EntityFrameworkCore;
|
|||
using MudBlazor.Services;
|
||||
using NLog.Web;
|
||||
using RobotApp.Client;
|
||||
using RobotApp.Client.Services;
|
||||
using RobotApp.Components;
|
||||
using RobotApp.Components.Account;
|
||||
using RobotApp.Data;
|
||||
using RobotApp.Hubs;
|
||||
using RobotApp.Services;
|
||||
using RobotApp.Services.Robot;
|
||||
using RobotApp.Services.Robot.Simulation;
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
|
@ -58,8 +60,11 @@ builder.Services.AddRobotSimulation();
|
|||
builder.Services.AddRobot();
|
||||
|
||||
// Add RobotMonitorService
|
||||
builder.Services.AddSingleton<RobotMonitorService>();
|
||||
builder.Services.AddHostedService(sp => sp.GetRequiredService<RobotMonitorService>());
|
||||
builder.Services.AddSingleton<RobotApp.Services.RobotMonitorService>();
|
||||
builder.Services.AddHostedService(sp => sp.GetRequiredService<RobotApp.Services.RobotMonitorService>());
|
||||
|
||||
builder.Services.AddScoped<RobotStateClient>();
|
||||
builder.Services.AddHostedService<RobotStatePublisher>();
|
||||
|
||||
var app = builder.Build();
|
||||
await app.Services.SeedApplicationDbAsync();
|
||||
|
|
@ -91,7 +96,7 @@ app.MapControllers();
|
|||
|
||||
// Map SignalR Hub
|
||||
app.MapHub<RobotMonitorHub>("/hubs/robotMonitor");
|
||||
|
||||
app.MapHub<RobotHub>("/hubs/robot");
|
||||
app.MapRazorComponents<App>()
|
||||
.AddInteractiveServerRenderMode()
|
||||
.AddInteractiveWebAssemblyRenderMode()
|
||||
|
|
|
|||
175
RobotApp/Services/Robot/RobotStatePublisher.cs
Normal file
175
RobotApp/Services/Robot/RobotStatePublisher.cs
Normal file
|
|
@ -0,0 +1,175 @@
|
|||
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