Compare commits
No commits in common. "f52f0fd8da6d836dfbf0583b785fe70dc67a252b" and "6cd32f8c98ad5c9d2fdeed48ab08339adf5db4b5" have entirely different histories.
f52f0fd8da
...
6cd32f8c98
|
|
@ -71,9 +71,7 @@
|
||||||
public NavModel[] Navs = [
|
public NavModel[] Navs = [
|
||||||
new(){Icon = "mdi-view-dashboard", Path="/", Label = "Dashboard", Match = NavLinkMatch.All},
|
new(){Icon = "mdi-view-dashboard", Path="/", Label = "Dashboard", Match = NavLinkMatch.All},
|
||||||
// new(){Icon = "mdi-map-legend", Path="/maps-manager", Label = "Mapping", Match = NavLinkMatch.All},
|
// new(){Icon = "mdi-map-legend", Path="/maps-manager", Label = "Mapping", Match = NavLinkMatch.All},
|
||||||
new(){Icon = "mdi-monitor", Path="/robot-monitor", Label = "Robot Monitor", Match = NavLinkMatch.All},
|
|
||||||
new(){Icon = "mdi-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-application-cog", Path="/robot-state", Label = "state", Match = NavLinkMatch.All},
|
|
||||||
];
|
];
|
||||||
|
|
||||||
private bool collapseNavMenu = true;
|
private bool collapseNavMenu = true;
|
||||||
|
|
|
||||||
|
|
@ -1,342 +0,0 @@
|
||||||
@inject IJSRuntime JS
|
|
||||||
|
|
||||||
<div class="robot-monitor-container">
|
|
||||||
<div class="toolbar">
|
|
||||||
<MudTooltip Text="Zoom In" role="button" Placement="Placement.Bottom" Color="Color.Info">
|
|
||||||
<button type="button" class="btn btn-secondary action-button" @onclick="ZoomIn">
|
|
||||||
<i class="mdi mdi-magnify-plus-outline icon-button"></i>
|
|
||||||
</button>
|
|
||||||
</MudTooltip>
|
|
||||||
<MudTooltip Text="Zoom Out" role="button" Placement="Placement.Bottom" Color="Color.Info">
|
|
||||||
<button type="button" class="btn btn-secondary action-button" @onclick="ZoomOut">
|
|
||||||
<i class="mdi mdi-magnify-minus-outline icon-button"></i>
|
|
||||||
</button>
|
|
||||||
</MudTooltip>
|
|
||||||
<MudTooltip Text="Reset View" role="button" Placement="Placement.Bottom" Color="Color.Info">
|
|
||||||
<button type="button" class="btn btn-secondary action-button" @onclick="ResetView">
|
|
||||||
<i class="mdi mdi-fit-to-screen-outline icon-button"></i>
|
|
||||||
</button>
|
|
||||||
</MudTooltip>
|
|
||||||
<MudSpacer />
|
|
||||||
@if (MonitorData?.RobotPosition != null)
|
|
||||||
{
|
|
||||||
<div class="robot-position-info">
|
|
||||||
<i class="mdi mdi-map-marker"></i>
|
|
||||||
<span>X: @MonitorData.RobotPosition.X.ToString("F2")m | Y: @MonitorData.RobotPosition.Y.ToString("F2")m | θ: @((MonitorData.RobotPosition.Theta * 180 / Math.PI).ToString("F1"))°</span>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
<MudChip T="string" Color="@(IsConnected? Color.Success: Color.Error)" Size="Size.Small">
|
|
||||||
@(IsConnected ? "Connected" : "Disconnected")
|
|
||||||
</MudChip>
|
|
||||||
</div>
|
|
||||||
<div @ref="SvgContainerRef" class="svg-container">
|
|
||||||
<svg @ref="SvgRef"
|
|
||||||
@onwheel="HandleWheel"
|
|
||||||
@onmousedown="HandleMouseDown"
|
|
||||||
@onmousemove="HandleMouseMove"
|
|
||||||
@onmouseup="HandleMouseUp"
|
|
||||||
@onmouseleave="HandleMouseLeave">
|
|
||||||
<g transform="@GetTransform()">
|
|
||||||
@if (MonitorData?.HasOrder == true)
|
|
||||||
{
|
|
||||||
@* @for (int i = 0; i < MonitorData.EdgeStates.Length; i++)
|
|
||||||
{
|
|
||||||
var edge = MonitorData.EdgeStates[i];
|
|
||||||
var (startX, startY, endX, endY) = GetEdgeEndpoints(i, edge);
|
|
||||||
<path d="@GetPathFromTrajectory(edge.Trajectory, startX, startY, endX, endY)"
|
|
||||||
fill="none"
|
|
||||||
stroke="#42A5F5"
|
|
||||||
stroke-width="0.08" />
|
|
||||||
} *@
|
|
||||||
<path d="@PathView"
|
|
||||||
fill="none"
|
|
||||||
stroke="#42A5F5"
|
|
||||||
stroke-width="0.08"
|
|
||||||
visibility="@PathIsNot" />
|
|
||||||
|
|
||||||
@foreach (var node in MonitorData.NodeStates)
|
|
||||||
{
|
|
||||||
<circle cx="@WorldToSvgX(node.NodePosition.X)"
|
|
||||||
cy="@WorldToSvgY(node.NodePosition.Y)"
|
|
||||||
r="@GetNodeRadius()"
|
|
||||||
fill="#66BB6A"
|
|
||||||
stroke="#555"
|
|
||||||
stroke-width="@GetNodeStrokeWidth()" />
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@* Render Robot *@
|
|
||||||
@if (MonitorData?.RobotPosition != null)
|
|
||||||
{
|
|
||||||
<g transform="@GetRobotTransform()">
|
|
||||||
@{
|
|
||||||
var (robotX, robotY, robotWidth, robotHeight) = GetRobotSize();
|
|
||||||
}
|
|
||||||
<image href="images/AMR-250.png"
|
|
||||||
x="@robotX"
|
|
||||||
y="@robotY"
|
|
||||||
width="@robotWidth"
|
|
||||||
height="@robotHeight"
|
|
||||||
preserveAspectRatio="xMidYMid meet" />
|
|
||||||
</g>
|
|
||||||
}
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
@code {
|
|
||||||
[Parameter] public RobotMonitorDto? MonitorData { get; set; }
|
|
||||||
[Parameter] public bool IsConnected { get; set; }
|
|
||||||
|
|
||||||
public class ElementSize
|
|
||||||
{
|
|
||||||
public double Width { get; set; }
|
|
||||||
public double Height { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public class ElementBoundingRect
|
|
||||||
{
|
|
||||||
public double X { get; set; }
|
|
||||||
public double Y { get; set; }
|
|
||||||
public double Width { get; set; }
|
|
||||||
public double Height { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
private ElementReference SvgRef;
|
|
||||||
private ElementReference SvgContainerRef;
|
|
||||||
|
|
||||||
private double ZoomScale = 2.0; // Zoom vào robot hơn khi mở
|
|
||||||
private const double MIN_ZOOM = 0.1;
|
|
||||||
private const double MAX_ZOOM = 5.0;
|
|
||||||
private const double BASE_PIXELS_PER_METER = 50.0;
|
|
||||||
|
|
||||||
private double TranslateX = 0;
|
|
||||||
private double TranslateY = 0;
|
|
||||||
|
|
||||||
private bool IsPanning = false;
|
|
||||||
private double PanStartX = 0;
|
|
||||||
private double PanStartY = 0;
|
|
||||||
|
|
||||||
private double SvgWidth = 800;
|
|
||||||
private double SvgHeight = 600;
|
|
||||||
|
|
||||||
private string PathView = "";
|
|
||||||
private string PathIsNot = "hidden";
|
|
||||||
|
|
||||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
|
||||||
{
|
|
||||||
if (firstRender)
|
|
||||||
{
|
|
||||||
var containerSize = await JS.InvokeAsync<ElementSize>("getElementSize", SvgContainerRef);
|
|
||||||
SvgWidth = containerSize.Width;
|
|
||||||
SvgHeight = containerSize.Height;
|
|
||||||
|
|
||||||
// Center view on robot if available with initial zoom
|
|
||||||
if (MonitorData?.RobotPosition != null)
|
|
||||||
{
|
|
||||||
// Zoom vào robot hơn một chút
|
|
||||||
ZoomScale = 2.5;
|
|
||||||
TranslateX = SvgWidth / 2 - MonitorData.RobotPosition.X * BASE_PIXELS_PER_METER * ZoomScale;
|
|
||||||
TranslateY = SvgHeight / 2 + MonitorData.RobotPosition.Y * BASE_PIXELS_PER_METER * ZoomScale;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
TranslateX = SvgWidth / 2;
|
|
||||||
TranslateY = SvgHeight / 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
StateHasChanged();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private string GetTransform()
|
|
||||||
{
|
|
||||||
return $"translate({TranslateX}, {TranslateY}) scale({ZoomScale * BASE_PIXELS_PER_METER})";
|
|
||||||
}
|
|
||||||
|
|
||||||
private string GetRobotTransform()
|
|
||||||
{
|
|
||||||
if (MonitorData?.RobotPosition == null) return "";
|
|
||||||
var x = WorldToSvgX(MonitorData.RobotPosition.X);
|
|
||||||
var y = WorldToSvgY(MonitorData.RobotPosition.Y);
|
|
||||||
// Theta là radian, convert sang độ
|
|
||||||
// SVG rotate quay theo chiều kim đồng hồ, cần đảo dấu
|
|
||||||
var angleDegrees = -MonitorData.RobotPosition.Theta * 180 / Math.PI;
|
|
||||||
return $"translate({x}, {y}) rotate({angleDegrees})";
|
|
||||||
}
|
|
||||||
|
|
||||||
private (double x, double y, double width, double height) GetRobotSize()
|
|
||||||
{
|
|
||||||
// Kích thước robot trong world coordinates (mét)
|
|
||||||
const double RobotWidthMeters = 0.606;
|
|
||||||
const double RobotLengthMeters = 1.106;
|
|
||||||
|
|
||||||
// Điều chỉnh kích thước dựa trên ZoomScale
|
|
||||||
// Tăng kích thước lên 1.5x để robot to hơn
|
|
||||||
double scaleFactor = 3 / ZoomScale; // Tăng kích thước hiển thị
|
|
||||||
|
|
||||||
double width = RobotWidthMeters * scaleFactor;
|
|
||||||
double height = RobotLengthMeters * scaleFactor;
|
|
||||||
double x = -width / 2;
|
|
||||||
double y = -height / 2;
|
|
||||||
|
|
||||||
return (x, y, width, height);
|
|
||||||
}
|
|
||||||
|
|
||||||
private double GetNodeRadius()
|
|
||||||
{
|
|
||||||
// Kích thước node cơ bản trong world coordinates - tăng lên 1.5x
|
|
||||||
const double BaseNodeRadius = 0.15;
|
|
||||||
|
|
||||||
// Điều chỉnh theo ZoomScale tương tự robot
|
|
||||||
double scaleFactor = 1.5 / ZoomScale; // Tăng kích thước hiển thị
|
|
||||||
|
|
||||||
return BaseNodeRadius * scaleFactor;
|
|
||||||
}
|
|
||||||
|
|
||||||
private double GetNodeStrokeWidth()
|
|
||||||
{
|
|
||||||
// Stroke width cơ bản - tăng lên một chút
|
|
||||||
const double BaseStrokeWidth = 0.03;
|
|
||||||
|
|
||||||
// Điều chỉnh theo ZoomScale
|
|
||||||
double scaleFactor = 1.5 / ZoomScale;
|
|
||||||
|
|
||||||
return BaseStrokeWidth * scaleFactor;
|
|
||||||
}
|
|
||||||
|
|
||||||
private double WorldToSvgX(double worldX)
|
|
||||||
{
|
|
||||||
return worldX;
|
|
||||||
}
|
|
||||||
|
|
||||||
private double WorldToSvgY(double worldY)
|
|
||||||
{
|
|
||||||
// Flip Y axis: World Y↑ → SVG Y↓
|
|
||||||
return -worldY;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void UpdatePath()
|
|
||||||
{
|
|
||||||
if (MonitorData is not null && MonitorData.EdgeStates.Length > 0)
|
|
||||||
{
|
|
||||||
var path = MonitorData.EdgeStates.Select(e => new EdgeStateDto
|
|
||||||
{
|
|
||||||
Degree = e.Degree,
|
|
||||||
StartX = WorldToSvgX(e.StartX),
|
|
||||||
StartY = WorldToSvgY(e.StartY),
|
|
||||||
EndX = WorldToSvgX(e.EndX),
|
|
||||||
EndY = WorldToSvgY(e.EndY),
|
|
||||||
ControlPoint1X = WorldToSvgX(e.ControlPoint1X),
|
|
||||||
ControlPoint1Y = WorldToSvgY(e.ControlPoint1Y),
|
|
||||||
ControlPoint2X = WorldToSvgX(e.ControlPoint2X),
|
|
||||||
ControlPoint2Y = WorldToSvgY(e.ControlPoint2Y),
|
|
||||||
}).ToList();
|
|
||||||
var inPath = $"M {path[0].StartX} {path[0].StartY}";
|
|
||||||
for (int i = 0; i < path.Count; i++)
|
|
||||||
{
|
|
||||||
if (path[i].Degree == 1) inPath = $"{inPath} L {path[i].EndX} {path[i].EndY}";
|
|
||||||
else if (path[i].Degree == 2) inPath = $"{inPath} Q {path[i].ControlPoint1X} {path[i].ControlPoint1Y} {path[i].EndX} {path[i].EndY}";
|
|
||||||
else inPath = $"{inPath} C {path[i].ControlPoint1X} {path[i].ControlPoint1Y}, {path[i].ControlPoint2X} {path[i].ControlPoint2Y}, {path[i].EndX} {path[i].EndY}";
|
|
||||||
}
|
|
||||||
PathView = inPath;
|
|
||||||
PathIsNot = "visible";
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
PathView = "";
|
|
||||||
PathIsNot = "hidden";
|
|
||||||
}
|
|
||||||
StateHasChanged();
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task ZoomIn()
|
|
||||||
{
|
|
||||||
ZoomScale = Math.Min(MAX_ZOOM, ZoomScale * 1.15);
|
|
||||||
await Task.CompletedTask;
|
|
||||||
StateHasChanged();
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task ZoomOut()
|
|
||||||
{
|
|
||||||
ZoomScale = Math.Max(MIN_ZOOM, ZoomScale * 0.85);
|
|
||||||
await Task.CompletedTask;
|
|
||||||
StateHasChanged();
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task ResetView()
|
|
||||||
{
|
|
||||||
// Reset về zoom ban đầu (2.5x) khi có robot position
|
|
||||||
if (MonitorData?.RobotPosition != null)
|
|
||||||
{
|
|
||||||
ZoomScale = 2.5;
|
|
||||||
TranslateX = SvgWidth / 2 - MonitorData.RobotPosition.X * BASE_PIXELS_PER_METER * ZoomScale;
|
|
||||||
TranslateY = SvgHeight / 2 + MonitorData.RobotPosition.Y * BASE_PIXELS_PER_METER * ZoomScale;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
ZoomScale = 1.0;
|
|
||||||
TranslateX = SvgWidth / 2;
|
|
||||||
TranslateY = SvgHeight / 2;
|
|
||||||
}
|
|
||||||
await Task.CompletedTask;
|
|
||||||
StateHasChanged();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void HandleMouseDown(MouseEventArgs e)
|
|
||||||
{
|
|
||||||
IsPanning = true;
|
|
||||||
PanStartX = e.ClientX - TranslateX;
|
|
||||||
PanStartY = e.ClientY - TranslateY;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void HandleMouseMove(MouseEventArgs e)
|
|
||||||
{
|
|
||||||
if (IsPanning)
|
|
||||||
{
|
|
||||||
TranslateX = e.ClientX - PanStartX;
|
|
||||||
TranslateY = e.ClientY - PanStartY;
|
|
||||||
StateHasChanged();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void HandleMouseUp(MouseEventArgs e)
|
|
||||||
{
|
|
||||||
IsPanning = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void HandleMouseLeave(MouseEventArgs e)
|
|
||||||
{
|
|
||||||
IsPanning = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task HandleWheel(WheelEventArgs e)
|
|
||||||
{
|
|
||||||
const double zoomFactor = 0.1;
|
|
||||||
double oldZoom = ZoomScale;
|
|
||||||
|
|
||||||
if (e.DeltaY < 0)
|
|
||||||
ZoomScale = Math.Min(MAX_ZOOM, ZoomScale * (1 + zoomFactor));
|
|
||||||
else
|
|
||||||
ZoomScale = Math.Max(MIN_ZOOM, ZoomScale * (1 - zoomFactor));
|
|
||||||
|
|
||||||
if (Math.Abs(ZoomScale - oldZoom) < 0.001) return;
|
|
||||||
|
|
||||||
// Zoom at mouse position
|
|
||||||
var svgRect = await JS.InvokeAsync<ElementBoundingRect>("getElementBoundingRect", SvgRef);
|
|
||||||
double mouseX = e.ClientX - svgRect.X;
|
|
||||||
double mouseY = e.ClientY - svgRect.Y;
|
|
||||||
|
|
||||||
// Calculate world coordinates at mouse position
|
|
||||||
double worldX = (mouseX - TranslateX) / (oldZoom * BASE_PIXELS_PER_METER);
|
|
||||||
double worldY = -(mouseY - TranslateY) / (oldZoom * BASE_PIXELS_PER_METER);
|
|
||||||
|
|
||||||
// Adjust translate to keep world point under mouse
|
|
||||||
TranslateX = mouseX - worldX * ZoomScale * BASE_PIXELS_PER_METER;
|
|
||||||
TranslateY = mouseY + worldY * ZoomScale * BASE_PIXELS_PER_METER;
|
|
||||||
|
|
||||||
StateHasChanged();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
@ -1,74 +0,0 @@
|
||||||
.robot-monitor-container {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.toolbar {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
padding: 8px 16px;
|
|
||||||
background-color: #2d2d2d;
|
|
||||||
border-bottom: 1px solid #444;
|
|
||||||
gap: 8px;
|
|
||||||
min-height: 48px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.action-button {
|
|
||||||
padding: 8px;
|
|
||||||
border: none;
|
|
||||||
background: #3d3d3d;
|
|
||||||
border-radius: 4px;
|
|
||||||
cursor: pointer;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
color: #fff;
|
|
||||||
transition: background-color 0.2s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.action-button:hover {
|
|
||||||
background: #4d4d4d;
|
|
||||||
}
|
|
||||||
|
|
||||||
.action-button:active {
|
|
||||||
background: #5d5d5d;
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-button {
|
|
||||||
font-size: 20px;
|
|
||||||
color: #fff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.robot-position-info {
|
|
||||||
color: #fff;
|
|
||||||
font-size: 14px;
|
|
||||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
|
||||||
padding: 4px 12px;
|
|
||||||
background-color: #3d3d3d;
|
|
||||||
border-radius: 4px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.svg-container {
|
|
||||||
flex: 1;
|
|
||||||
overflow: hidden;
|
|
||||||
position: relative;
|
|
||||||
background-color: #fafafa;
|
|
||||||
}
|
|
||||||
|
|
||||||
.svg-container svg {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
cursor: grab;
|
|
||||||
background-color: dimgray;
|
|
||||||
}
|
|
||||||
|
|
||||||
.svg-container svg:active {
|
|
||||||
cursor: grabbing;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
@ -1,212 +0,0 @@
|
||||||
@inherits MudComponentBase
|
|
||||||
@using RobotApp.VDA5050.InstantAction
|
|
||||||
@using System.Text.Json
|
|
||||||
@using System.Text.Json.Serialization
|
|
||||||
@using RobotApp.VDA5050.Order
|
|
||||||
@using RobotApp.VDA5050.Type
|
|
||||||
|
|
||||||
<MudDialog>
|
|
||||||
<TitleContent>
|
|
||||||
<MudText Typo="Typo.h6">Edit Node: @Node.NodeId</MudText>
|
|
||||||
</TitleContent>
|
|
||||||
|
|
||||||
<DialogContent>
|
|
||||||
<MudGrid Spacing="3">
|
|
||||||
<MudItem xs="12">
|
|
||||||
<MudTextField @bind-Value="Node.NodeId" Label="Node ID" Required="true" />
|
|
||||||
</MudItem>
|
|
||||||
<MudItem xs="12">
|
|
||||||
<MudNumericField T="int" @bind-Value="Node.SequenceId" Label="Sequence ID" Required="true" />
|
|
||||||
</MudItem>
|
|
||||||
<MudItem xs="12">
|
|
||||||
<MudSwitch T="bool" @bind-Checked="Node.Released" Label="Released" />
|
|
||||||
</MudItem>
|
|
||||||
<MudItem xs="6">
|
|
||||||
<MudNumericField T="double" @bind-Value="Node.NodePosition.X" Label="X" />
|
|
||||||
</MudItem>
|
|
||||||
|
|
||||||
<MudItem xs="6">
|
|
||||||
<MudNumericField T="double" @bind-Value="Node.NodePosition.Y" Label="Y" />
|
|
||||||
</MudItem>
|
|
||||||
|
|
||||||
<MudItem xs="6">
|
|
||||||
<MudNumericField T="double" @bind-Value="Node.NodePosition.Theta" Label="Theta (rad)" />
|
|
||||||
</MudItem>
|
|
||||||
|
|
||||||
<MudItem xs="6">
|
|
||||||
<MudNumericField T="double" @bind-Value="Node.NodePosition.AllowedDeviationXY"
|
|
||||||
Label="Allowed Dev XY" />
|
|
||||||
</MudItem>
|
|
||||||
|
|
||||||
<MudItem xs="6">
|
|
||||||
<MudNumericField T="double" @bind-Value="Node.NodePosition.AllowedDeviationTheta"
|
|
||||||
Label="Allowed Dev Theta" />
|
|
||||||
</MudItem>
|
|
||||||
|
|
||||||
<MudItem xs="12">
|
|
||||||
<MudTextField @bind-Value="Node.NodePosition.MapId" Label="Map ID" />
|
|
||||||
</MudItem>
|
|
||||||
|
|
||||||
|
|
||||||
<MudItem xs="12">
|
|
||||||
<MudDivider Class="my-4" />
|
|
||||||
<MudText Typo="Typo.subtitle1" Class="mb-3">Actions</MudText>
|
|
||||||
|
|
||||||
@foreach (var act in Node.Actions)
|
|
||||||
{
|
|
||||||
<MudPaper Class="pa-3 mb-3" Outlined="true">
|
|
||||||
<MudGrid Spacing="3">
|
|
||||||
<MudItem xs="10">
|
|
||||||
<MudItem xs="12">
|
|
||||||
<MudSelect T="string"
|
|
||||||
Label="Action Type"
|
|
||||||
Dense="true"
|
|
||||||
Required="true"
|
|
||||||
@bind-Value="act.ActionType">
|
|
||||||
|
|
||||||
@foreach (var at in Enum.GetValues<ActionType>())
|
|
||||||
{
|
|
||||||
<MudSelectItem Value="@at.ToString()">
|
|
||||||
@at
|
|
||||||
</MudSelectItem>
|
|
||||||
}
|
|
||||||
</MudSelect>
|
|
||||||
</MudItem>
|
|
||||||
</MudItem>
|
|
||||||
<MudItem xs="12">
|
|
||||||
<MudSelect T="string" @bind-Value="act.BlockingType" Label="Blocking Type">
|
|
||||||
<MudSelectItem Value="@("NONE")">NONE</MudSelectItem>
|
|
||||||
<MudSelectItem Value="@("SOFT")">SOFT</MudSelectItem>
|
|
||||||
<MudSelectItem Value="@("HARD")">HARD</MudSelectItem>
|
|
||||||
</MudSelect>
|
|
||||||
</MudItem>
|
|
||||||
<MudItem xs="12">
|
|
||||||
<MudTextField @bind-Value="act.ActionId" Label="Action ID" />
|
|
||||||
</MudItem>
|
|
||||||
</MudGrid>
|
|
||||||
|
|
||||||
<MudText Typo="Typo.caption" Class="mt-3 mb-2">Action Parameters</MudText>
|
|
||||||
|
|
||||||
@{
|
|
||||||
var parameters = act.ActionParameters ?? Array.Empty<ActionParameter>();
|
|
||||||
}
|
|
||||||
@foreach (var p in parameters.Cast<UiActionParameter>().ToList())
|
|
||||||
{
|
|
||||||
var param = p; // capture cho lambda
|
|
||||||
<MudGrid Class="mt-1">
|
|
||||||
<MudItem xs="5">
|
|
||||||
<MudTextField @bind-Value="param.Key" Label="Key" />
|
|
||||||
</MudItem>
|
|
||||||
<MudItem xs="5">
|
|
||||||
<MudTextField @bind-Value="param.ValueString" Label="Value" />
|
|
||||||
</MudItem>
|
|
||||||
<MudItem xs="2">
|
|
||||||
<MudIconButton Icon="@Icons.Material.Filled.Delete"
|
|
||||||
Color="Color.Error"
|
|
||||||
Size="Size.Small"
|
|
||||||
OnClick="@(() => RemoveParameter(act, param))" />
|
|
||||||
</MudItem>
|
|
||||||
</MudGrid>
|
|
||||||
}
|
|
||||||
|
|
||||||
<MudButton Size="Size.Small"
|
|
||||||
StartIcon="@Icons.Material.Filled.Add"
|
|
||||||
Class="mt-3"
|
|
||||||
OnClick="@(() => AddParameter(act))">
|
|
||||||
Add Parameter
|
|
||||||
</MudButton>
|
|
||||||
|
|
||||||
<MudDivider Class="my-3" />
|
|
||||||
|
|
||||||
<MudButton Size="Size.Small"
|
|
||||||
Color="Color.Error"
|
|
||||||
Variant="Variant.Text"
|
|
||||||
StartIcon="@Icons.Material.Filled.Delete"
|
|
||||||
OnClick="@(() => RemoveAction(act))">
|
|
||||||
Remove Action
|
|
||||||
</MudButton>
|
|
||||||
</MudPaper>
|
|
||||||
}
|
|
||||||
|
|
||||||
<MudButton Size="Size.Small"
|
|
||||||
StartIcon="@Icons.Material.Filled.Add"
|
|
||||||
OnClick="AddNewAction">
|
|
||||||
Add Action
|
|
||||||
</MudButton>
|
|
||||||
</MudItem>
|
|
||||||
</MudGrid>
|
|
||||||
</DialogContent>
|
|
||||||
|
|
||||||
<DialogActions>
|
|
||||||
<MudButton OnClick="Cancel">Cancel</MudButton>
|
|
||||||
<MudButton Color="Color.Primary" Variant="Variant.Filled" OnClick="Submit">Save</MudButton>
|
|
||||||
</DialogActions>
|
|
||||||
</MudDialog>
|
|
||||||
|
|
||||||
@code {
|
|
||||||
[CascadingParameter] public IMudDialogInstance MudDialog { get; set; } = default!;
|
|
||||||
[Parameter] public Node Node { get; set; } = default!;
|
|
||||||
|
|
||||||
private void Cancel() => MudDialog.Cancel();
|
|
||||||
private void Submit() => MudDialog.Close(DialogResult.Ok(true));
|
|
||||||
|
|
||||||
private void RemoveAction(VDA5050.InstantAction.Action actToRemove)
|
|
||||||
{
|
|
||||||
Node.Actions = Node.Actions
|
|
||||||
.Where(a => a != actToRemove)
|
|
||||||
.ToArray();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private void AddNewAction()
|
|
||||||
{
|
|
||||||
Node.Actions = Node.Actions
|
|
||||||
.Append(new VDA5050.InstantAction.Action
|
|
||||||
{
|
|
||||||
ActionId = Guid.NewGuid().ToString(),
|
|
||||||
ActionType = ActionType.startPause.ToString(),
|
|
||||||
BlockingType = "NONE",
|
|
||||||
ActionParameters = Array.Empty<ActionParameter>()
|
|
||||||
})
|
|
||||||
.ToArray();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
private void AddParameter(VDA5050.InstantAction.Action act)
|
|
||||||
{
|
|
||||||
var newParam = new UiActionParameter();
|
|
||||||
|
|
||||||
if (act.ActionParameters == null || act.ActionParameters.Length == 0)
|
|
||||||
{
|
|
||||||
act.ActionParameters = new[] { newParam };
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
var list = act.ActionParameters.ToList();
|
|
||||||
list.Add(newParam);
|
|
||||||
act.ActionParameters = list.ToArray();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void RemoveParameter(VDA5050.InstantAction.Action act, UiActionParameter paramToRemove)
|
|
||||||
{
|
|
||||||
if (act.ActionParameters == null || act.ActionParameters.Length == 0)
|
|
||||||
return;
|
|
||||||
|
|
||||||
act.ActionParameters = act.ActionParameters
|
|
||||||
.Where(p => p != paramToRemove) // so sánh reference
|
|
||||||
.ToArray();
|
|
||||||
}
|
|
||||||
|
|
||||||
// UiActionParameter vẫn giữ như cũ trong trang chính
|
|
||||||
public class UiActionParameter : ActionParameter
|
|
||||||
{
|
|
||||||
[JsonIgnore]
|
|
||||||
public string ValueString
|
|
||||||
{
|
|
||||||
get => Value?.ToString() ?? "";
|
|
||||||
set => Value = value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,652 +0,0 @@
|
||||||
@page "/"
|
|
||||||
@using System.Text.Json
|
|
||||||
@using System.Text.Json.Serialization
|
|
||||||
@using RobotApp.VDA5050.InstantAction
|
|
||||||
@using RobotApp.VDA5050.Order
|
|
||||||
@using RobotApp.VDA5050.Type
|
|
||||||
@using System.ComponentModel.DataAnnotations
|
|
||||||
@attribute [Authorize]
|
|
||||||
@rendermode InteractiveWebAssemblyNoPrerender
|
|
||||||
@inject IJSRuntime JS
|
|
||||||
|
|
||||||
@inject IDialogService DialogService
|
|
||||||
|
|
||||||
<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;">
|
|
||||||
|
|
||||||
<!-- TIÊU ĐỀ -->
|
|
||||||
<MudText Typo="Typo.h4" Align="Align.Center" Class="mb-6 flex-shrink-0">
|
|
||||||
🧾 VDA5050 Order Editor
|
|
||||||
</MudText>
|
|
||||||
|
|
||||||
<MudGrid Spacing="4" Class="flex-grow-1" Style="overflow:hidden;">
|
|
||||||
|
|
||||||
<!-- ================= CỘT TRÁI (50%) ================= -->
|
|
||||||
<MudItem xs="12" md="7" Class="d-flex flex-column h-100" Style="gap:16px;">
|
|
||||||
<!-- Nodes và Edges chia 1x2, mỗi phần có scroll nội bộ -->
|
|
||||||
<MudGrid Spacing="4" Class="flex-grow-1" Style="overflow:hidden;">
|
|
||||||
<!-- Nodes (trái) -->
|
|
||||||
<MudItem xs="12" md="6" Class="h-100">
|
|
||||||
<MudPaper Class="pa-4 h-100 d-flex flex-column" Elevation="2">
|
|
||||||
<MudStack Row="true" 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="AddNode">
|
|
||||||
Add Node
|
|
||||||
</MudButton>
|
|
||||||
</MudStack>
|
|
||||||
|
|
||||||
<!-- Cuộn nội bộ chỉ cho phần danh sách Nodes -->
|
|
||||||
<div class="flex-grow-1" style="overflow:auto;">
|
|
||||||
<MudExpansionPanels MultiExpansion="true">
|
|
||||||
@foreach (var node in Order.Nodes)
|
|
||||||
{
|
|
||||||
<MudExpansionPanel @key="node">
|
|
||||||
|
|
||||||
<!-- ===== HEADER ===== -->
|
|
||||||
<TitleContent>
|
|
||||||
<div class="d-flex align-center justify-space-between w-100">
|
|
||||||
<MudText Typo="Typo.subtitle1" Class="fw-bold">
|
|
||||||
@node.NodeId
|
|
||||||
</MudText>
|
|
||||||
<MudIconButton Icon="@Icons.Material.Filled.Edit"
|
|
||||||
Color="Color.Primary"
|
|
||||||
Size="Size.Small"
|
|
||||||
OnClick="@(() => OpenEditNodeDialog(node))"
|
|
||||||
StopPropagation="true" />
|
|
||||||
<MudIconButton Icon="@Icons.Material.Filled.Delete"
|
|
||||||
Color="Color.Error"
|
|
||||||
Size="Size.Small"
|
|
||||||
OnClick="@(() => RemoveNode(node))"
|
|
||||||
StopPropagation="true" />
|
|
||||||
</div>
|
|
||||||
</TitleContent>
|
|
||||||
|
|
||||||
<!-- ===== BODY (BẮT BUỘC ChildContent) ===== -->
|
|
||||||
<ChildContent>
|
|
||||||
<MudGrid Spacing="3">
|
|
||||||
<MudItem xs="12">
|
|
||||||
<MudTextField @bind-Value="node.NodeId" Label="Node ID" />
|
|
||||||
</MudItem>
|
|
||||||
|
|
||||||
<MudItem xs="12">
|
|
||||||
<MudNumericField T="int"
|
|
||||||
@bind-Value="node.SequenceId"
|
|
||||||
Label="Sequence ID" />
|
|
||||||
</MudItem>
|
|
||||||
|
|
||||||
<MudItem xs="12">
|
|
||||||
<MudSwitch T="bool"
|
|
||||||
@bind-Checked="node.Released"
|
|
||||||
Label="Released" />
|
|
||||||
</MudItem>
|
|
||||||
|
|
||||||
|
|
||||||
<MudItem xs="6">
|
|
||||||
<MudNumericField T="double"
|
|
||||||
@bind-Value="node.NodePosition.X"
|
|
||||||
Label="X" />
|
|
||||||
</MudItem>
|
|
||||||
<MudItem xs="6">
|
|
||||||
<MudNumericField T="double"
|
|
||||||
@bind-Value="node.NodePosition.Y"
|
|
||||||
Label="Y" />
|
|
||||||
</MudItem>
|
|
||||||
<MudItem xs="12">
|
|
||||||
<MudTextField @bind-Value="node.NodePosition.MapId"
|
|
||||||
Label="Map ID" />
|
|
||||||
</MudItem>
|
|
||||||
<MudItem xs="6">
|
|
||||||
<MudNumericField T="double"
|
|
||||||
@bind-Value="node.NodePosition.Theta"
|
|
||||||
Label="Theta (rad)" />
|
|
||||||
</MudItem>
|
|
||||||
|
|
||||||
<MudItem xs="6">
|
|
||||||
<MudNumericField T="double"
|
|
||||||
@bind-Value="node.NodePosition.AllowedDeviationXY"
|
|
||||||
Label="Allowed Dev XY" />
|
|
||||||
</MudItem>
|
|
||||||
|
|
||||||
<MudItem xs="6">
|
|
||||||
<MudNumericField T="double"
|
|
||||||
@bind-Value="node.NodePosition.AllowedDeviationTheta"
|
|
||||||
Label="Allowed Dev Theta" />
|
|
||||||
</MudItem>
|
|
||||||
|
|
||||||
|
|
||||||
<MudItem xs="12">
|
|
||||||
<MudDivider Class="my-4" />
|
|
||||||
<MudText Typo="Typo.subtitle1" Class="mb-3">
|
|
||||||
Actions
|
|
||||||
</MudText>
|
|
||||||
|
|
||||||
@foreach (var act in node.Actions)
|
|
||||||
{
|
|
||||||
<MudPaper Class="pa-3 mb-3" Outlined>
|
|
||||||
<MudGrid Spacing="3">
|
|
||||||
<MudItem xs="12">
|
|
||||||
<MudSelect T="string"
|
|
||||||
Label="Action Type"
|
|
||||||
@bind-Value="act.ActionType"
|
|
||||||
Dense="true"
|
|
||||||
Required="true">
|
|
||||||
@foreach (var at in Enum.GetValues<ActionType>())
|
|
||||||
{
|
|
||||||
<MudSelectItem Value="@at.ToString()">
|
|
||||||
@at
|
|
||||||
</MudSelectItem>
|
|
||||||
}
|
|
||||||
</MudSelect>
|
|
||||||
</MudItem>
|
|
||||||
|
|
||||||
<MudItem xs="12">
|
|
||||||
<MudSelect T="string"
|
|
||||||
@bind-Value="act.BlockingType"
|
|
||||||
Label="Blocking Type">
|
|
||||||
<MudSelectItem Value="@("NONE")">NONE</MudSelectItem>
|
|
||||||
<MudSelectItem Value="@("SOFT")">SOFT</MudSelectItem>
|
|
||||||
<MudSelectItem Value="@("HARD")">HARD</MudSelectItem>
|
|
||||||
</MudSelect>
|
|
||||||
</MudItem>
|
|
||||||
|
|
||||||
<MudItem xs="12">
|
|
||||||
<MudTextField @bind-Value="act.ActionId"
|
|
||||||
Label="Action ID" />
|
|
||||||
</MudItem>
|
|
||||||
</MudGrid>
|
|
||||||
|
|
||||||
<MudText Typo="Typo.caption" Class="mt-3 mb-2">
|
|
||||||
Action Parameters
|
|
||||||
</MudText>
|
|
||||||
|
|
||||||
@foreach (var p in act.ActionParameters.Cast<UiActionParameter>())
|
|
||||||
{
|
|
||||||
<MudGrid Class="mt-1">
|
|
||||||
<MudItem xs="6">
|
|
||||||
<MudTextField @bind-Value="p.Key" Label="Key" />
|
|
||||||
</MudItem>
|
|
||||||
<MudItem xs="6">
|
|
||||||
<MudTextField @bind-Value="p.ValueString" Label="Value" />
|
|
||||||
</MudItem>
|
|
||||||
<MudItem xs="2">
|
|
||||||
<MudIconButton Icon="@Icons.Material.Filled.Delete"
|
|
||||||
Color="Color.Error"
|
|
||||||
OnClick="@(() => RemoveActionParameter(act, p))" />
|
|
||||||
</MudItem>
|
|
||||||
</MudGrid>
|
|
||||||
}
|
|
||||||
|
|
||||||
<MudButton Size="Size.Small"
|
|
||||||
StartIcon="@Icons.Material.Filled.Add"
|
|
||||||
Class="mt-3"
|
|
||||||
OnClick="@(() => AddActionParameter(act))">
|
|
||||||
Add Parameter
|
|
||||||
</MudButton>
|
|
||||||
|
|
||||||
<MudDivider Class="my-3" />
|
|
||||||
|
|
||||||
<MudButton Size="Size.Small"
|
|
||||||
Color="Color.Error"
|
|
||||||
Variant="Variant.Text"
|
|
||||||
StartIcon="@Icons.Material.Filled.Delete"
|
|
||||||
OnClick="@(() => RemoveAction(node, act))">
|
|
||||||
Remove Action
|
|
||||||
</MudButton>
|
|
||||||
</MudPaper>
|
|
||||||
}
|
|
||||||
|
|
||||||
<MudButton Size="Size.Small"
|
|
||||||
StartIcon="@Icons.Material.Filled.Add"
|
|
||||||
OnClick="@(() => AddAction(node))">
|
|
||||||
Add Action
|
|
||||||
</MudButton>
|
|
||||||
</MudItem>
|
|
||||||
</MudGrid>
|
|
||||||
</ChildContent>
|
|
||||||
|
|
||||||
</MudExpansionPanel>
|
|
||||||
}
|
|
||||||
</MudExpansionPanels>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</MudPaper>
|
|
||||||
</MudItem>
|
|
||||||
|
|
||||||
<!-- Edges (phải) -->
|
|
||||||
<MudItem xs="12" md="6" Class="h-100">
|
|
||||||
<MudPaper Class="pa-4 h-100 d-flex flex-column" Elevation="2">
|
|
||||||
<MudStack Row="true" 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="AddEdge">
|
|
||||||
Add Edge
|
|
||||||
</MudButton>
|
|
||||||
</MudStack>
|
|
||||||
|
|
||||||
<!-- 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>
|
|
||||||
|
|
||||||
<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>
|
|
||||||
|
|
||||||
<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>
|
|
||||||
</MudItem>
|
|
||||||
<!-- ================= CỘT PHẢI (50%) - JSON Output ================= -->
|
|
||||||
<MudItem xs="12" md="5" Class="h-100">
|
|
||||||
<MudPaper Class="pa-4 h-100 d-flex flex-column" Elevation="2">
|
|
||||||
<MudStack Row="true" AlignItems="AlignItems.Center" Justify="Justify.SpaceBetween" Class="mb-4 flex-shrink-0">
|
|
||||||
<MudText Typo="Typo.h6">📄 JSON Output (/order)</MudText>
|
|
||||||
|
|
||||||
<MudButton Variant="Variant.Filled"
|
|
||||||
Color="Color.Success"
|
|
||||||
StartIcon="@Icons.Material.Filled.Send"
|
|
||||||
OnClick="SendOrderToServer">
|
|
||||||
Send
|
|
||||||
</MudButton>
|
|
||||||
@if (!string.IsNullOrEmpty(sendResult))
|
|
||||||
{
|
|
||||||
<MudAlert Severity="@(sendResult.StartsWith("✅") ? Severity.Success : Severity.Error)"
|
|
||||||
Class="mt-3">
|
|
||||||
@sendResult
|
|
||||||
</MudAlert>
|
|
||||||
}
|
|
||||||
|
|
||||||
<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="CopyJsonToClipboard">
|
|
||||||
@if (copied)
|
|
||||||
{
|
|
||||||
<MudText Class="ml-2">Copied!</MudText>
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
<MudText Class="ml-2">Copy</MudText>
|
|
||||||
}
|
|
||||||
</MudButton>
|
|
||||||
</MudTooltip>
|
|
||||||
</MudStack>
|
|
||||||
|
|
||||||
<!-- Cuộn nội bộ cho 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-color: #1e1e1e;
|
|
||||||
color: #d4d4d4;" />
|
|
||||||
</div>
|
|
||||||
</MudPaper>
|
|
||||||
</MudItem>
|
|
||||||
|
|
||||||
</MudGrid>
|
|
||||||
</MudContainer>
|
|
||||||
</div>
|
|
||||||
</MudMainContent>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
window.copyText = async function (text) {
|
|
||||||
if (navigator.clipboard && window.isSecureContext) {
|
|
||||||
await navigator.clipboard.writeText(text);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
const textarea = document.createElement("textarea");
|
|
||||||
textarea.value = text;
|
|
||||||
textarea.style.position = "fixed";
|
|
||||||
textarea.style.opacity = "0";
|
|
||||||
|
|
||||||
document.body.appendChild(textarea);
|
|
||||||
textarea.focus();
|
|
||||||
textarea.select();
|
|
||||||
|
|
||||||
try {
|
|
||||||
document.execCommand("copy");
|
|
||||||
return true;
|
|
||||||
} catch (err) {
|
|
||||||
console.error("Copy failed", err);
|
|
||||||
return false;
|
|
||||||
} finally {
|
|
||||||
document.body.removeChild(textarea);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
@code {
|
|
||||||
[Inject] HttpClient Http { get; set; } = default!;
|
|
||||||
|
|
||||||
bool sending;
|
|
||||||
string? sendResult;
|
|
||||||
bool IsEditNodeOpen;
|
|
||||||
Node? EditingNode;
|
|
||||||
OrderMessage Order = new();
|
|
||||||
|
|
||||||
async Task SendOrderToServer()
|
|
||||||
{
|
|
||||||
if (sending)
|
|
||||||
return;
|
|
||||||
|
|
||||||
sending = true;
|
|
||||||
sendResult = null;
|
|
||||||
StateHasChanged();
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var response = await Http.PostAsJsonAsync(
|
|
||||||
"/api/order",
|
|
||||||
JsonSerializer.Deserialize<JsonElement>(OrderJson)
|
|
||||||
);
|
|
||||||
|
|
||||||
if (response.IsSuccessStatusCode)
|
|
||||||
{
|
|
||||||
sendResult = "✅ Done!";
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
sendResult = $"❌ Failed: {response.StatusCode}";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
sendResult = $"❌ Error: {ex.Message}";
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
sending = false;
|
|
||||||
StateHasChanged();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void AddNode()
|
|
||||||
{
|
|
||||||
Order.Nodes.Add(new Node
|
|
||||||
{
|
|
||||||
NodeId = $"NODE_{Order.Nodes.Count + 1}",
|
|
||||||
SequenceId = Order.Nodes.Count,
|
|
||||||
Released = true,
|
|
||||||
NodePosition = new NodePosition()
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
void RemoveNode(Node node)
|
|
||||||
{
|
|
||||||
Order.Nodes.Remove(node);
|
|
||||||
|
|
||||||
Order.Edges.RemoveAll(e =>
|
|
||||||
e.StartNodeId == node.NodeId ||
|
|
||||||
e.EndNodeId == node.NodeId);
|
|
||||||
|
|
||||||
for (int i = 0; i < Order.Nodes.Count; i++)
|
|
||||||
Order.Nodes[i].SequenceId = i;
|
|
||||||
}
|
|
||||||
|
|
||||||
void RemoveAction(Node node, VDA5050.InstantAction.Action act)
|
|
||||||
{
|
|
||||||
node.Actions = node.Actions
|
|
||||||
.Where(a => a != act)
|
|
||||||
.ToArray();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
void AddAction(Node node)
|
|
||||||
{
|
|
||||||
node.Actions = node.Actions
|
|
||||||
.Append(new VDA5050.InstantAction.Action
|
|
||||||
{
|
|
||||||
ActionId = Guid.NewGuid().ToString(),
|
|
||||||
ActionType = ActionType.startPause.ToString(),
|
|
||||||
BlockingType = "NONE",
|
|
||||||
ActionParameters = Array.Empty<ActionParameter>()
|
|
||||||
})
|
|
||||||
.ToArray();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private void RemoveActionParameter(VDA5050.InstantAction.Action act, ActionParameter param)
|
|
||||||
{
|
|
||||||
if (act.ActionParameters == null) return;
|
|
||||||
act.ActionParameters = act.ActionParameters
|
|
||||||
.Where(p => p != param)
|
|
||||||
.ToArray();
|
|
||||||
}
|
|
||||||
void AddActionParameter(VDA5050.InstantAction.Action act)
|
|
||||||
{
|
|
||||||
var newList = (act.ActionParameters ?? Array.Empty<ActionParameter>()).ToList();
|
|
||||||
newList.Add(new UiActionParameter());
|
|
||||||
act.ActionParameters = newList.ToArray();
|
|
||||||
}
|
|
||||||
|
|
||||||
void AddEdge()
|
|
||||||
{
|
|
||||||
if (Order.Nodes.Count < 2) return;
|
|
||||||
|
|
||||||
Order.Edges.Add(new VDA5050.Order.Edge
|
|
||||||
{
|
|
||||||
EdgeId = $"EDGE_{Order.Edges.Count + 1}",
|
|
||||||
StartNodeId = Order.Nodes[^2].NodeId,
|
|
||||||
EndNodeId = Order.Nodes[^1].NodeId
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
bool copied = false;
|
|
||||||
CancellationTokenSource? _copyCts;
|
|
||||||
|
|
||||||
async Task CopyJsonToClipboard()
|
|
||||||
{
|
|
||||||
_copyCts?.Cancel();
|
|
||||||
_copyCts = new();
|
|
||||||
|
|
||||||
var success = await JS.InvokeAsync<bool>(
|
|
||||||
"copyText",
|
|
||||||
OrderJson
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!success)
|
|
||||||
return;
|
|
||||||
|
|
||||||
copied = true;
|
|
||||||
StateHasChanged();
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
await Task.Delay(1500, _copyCts.Token);
|
|
||||||
}
|
|
||||||
catch { }
|
|
||||||
|
|
||||||
copied = false;
|
|
||||||
StateHasChanged();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
void RemoveEdge(VDA5050.Order.Edge edge)
|
|
||||||
{
|
|
||||||
Order.Edges.Remove(edge);
|
|
||||||
}
|
|
||||||
|
|
||||||
string OrderJson =>
|
|
||||||
JsonSerializer.Serialize(
|
|
||||||
Order.ToSchemaObject(),
|
|
||||||
new JsonSerializerOptions
|
|
||||||
{
|
|
||||||
WriteIndented = true,
|
|
||||||
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
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>
|
|
||||||
{
|
|
||||||
{ x => x.Node, node } // Truyền trực tiếp reference gốc
|
|
||||||
};
|
|
||||||
|
|
||||||
var options = new DialogOptions
|
|
||||||
{
|
|
||||||
CloseButton = true,
|
|
||||||
MaxWidth = MaxWidth.Large,
|
|
||||||
FullWidth = true,
|
|
||||||
CloseOnEscapeKey = true
|
|
||||||
};
|
|
||||||
|
|
||||||
var dialog = await DialogService.ShowAsync<EditNodeDialog>($"Edit Node: {node.NodeId}", parameters, options);
|
|
||||||
await dialog.Result; // Đợi dialog đóng
|
|
||||||
|
|
||||||
StateHasChanged();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,41 +0,0 @@
|
||||||
@page "/robot-monitor"
|
|
||||||
@rendermode InteractiveWebAssemblyNoPrerender
|
|
||||||
@attribute [Authorize]
|
|
||||||
@inject RobotApp.Client.Services.RobotMonitorService MonitorService
|
|
||||||
@implements IAsyncDisposable
|
|
||||||
|
|
||||||
<PageTitle>Robot Monitor</PageTitle>
|
|
||||||
|
|
||||||
<div class="d-flex w-100 h-100 overflow-hidden">
|
|
||||||
<RobotApp.Client.Pages.Components.Monitor.RobotMonitorView @ref="@RobotMonitorViewRef"
|
|
||||||
MonitorData="@_monitorData"
|
|
||||||
IsConnected="@MonitorService.IsConnected" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
@code {
|
|
||||||
private RobotMonitorDto? _monitorData;
|
|
||||||
private RobotApp.Client.Pages.Components.Monitor.RobotMonitorView? RobotMonitorViewRef;
|
|
||||||
|
|
||||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
|
||||||
{
|
|
||||||
if (firstRender)
|
|
||||||
{
|
|
||||||
MonitorService.OnDataReceived += OnMonitorDataReceived;
|
|
||||||
await MonitorService.StartAsync();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnMonitorDataReceived(RobotMonitorDto data)
|
|
||||||
{
|
|
||||||
_monitorData = data;
|
|
||||||
RobotMonitorViewRef?.UpdatePath();
|
|
||||||
InvokeAsync(StateHasChanged);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async ValueTask DisposeAsync()
|
|
||||||
{
|
|
||||||
MonitorService.OnDataReceived -= OnMonitorDataReceived;
|
|
||||||
await MonitorService.StopAsync();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
@ -1,397 +0,0 @@
|
||||||
@page "/robot-state"
|
|
||||||
@using RobotApp.VDA5050.State
|
|
||||||
@rendermode InteractiveWebAssemblyNoPrerender
|
|
||||||
|
|
||||||
@inject StateMsg RobotState
|
|
||||||
|
|
||||||
<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>
|
|
||||||
<MudText Typo="Typo.subtitle2" Color="Color.Secondary">
|
|
||||||
@RobotState.Version •
|
|
||||||
@RobotState.Manufacturer
|
|
||||||
@RobotState.SerialNumber
|
|
||||||
</MudText>
|
|
||||||
</div>
|
|
||||||
@* @if (RobotState.Current != null)
|
|
||||||
{
|
|
||||||
<MudChip T="string" Size="Size.Large"
|
|
||||||
Color="@(RobotState.Current ? Color.Success : Color.Error)">
|
|
||||||
@(RobotState.Current.IsOnline ? "ONLINE" : "OFFLINE")
|
|
||||||
</MudChip>
|
|
||||||
} *@
|
|
||||||
</MudPaper>
|
|
||||||
|
|
||||||
@if (RobotState == null)
|
|
||||||
{
|
|
||||||
<MudAlert Severity="Severity.Info" Variant="Variant.Outlined">
|
|
||||||
Waiting for robot state (VDA5050)...
|
|
||||||
<MudProgressLinear Indeterminate Class="mt-3" />
|
|
||||||
</MudAlert>
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
var msg = RobotState;
|
|
||||||
|
|
||||||
<!-- ===================================================== -->
|
|
||||||
<!-- 📨 MESSAGE META (DEBUG / TRACE) -->
|
|
||||||
<!-- ===================================================== -->
|
|
||||||
<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">
|
|
||||||
DeviationRange: <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</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: @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" />
|
|
||||||
|
|
||||||
@{
|
|
||||||
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"
|
|
||||||
Dense="true"
|
|
||||||
Hover="true"
|
|
||||||
Bordered="true"
|
|
||||||
Elevation="0"
|
|
||||||
Height="180px"
|
|
||||||
Breakpoint="Breakpoint.Sm"
|
|
||||||
HorizontalScrollbar="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">
|
|
||||||
<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">
|
|
||||||
@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="160px"
|
|
||||||
Breakpoint="Breakpoint.Sm"
|
|
||||||
HorizontalScrollbar="true"
|
|
||||||
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>
|
|
||||||
|
|
||||||
<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>
|
|
||||||
|
|
||||||
<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 async Task OnStateChanged()
|
|
||||||
=> await InvokeAsync(StateHasChanged);
|
|
||||||
|
|
||||||
|
|
||||||
private record MessageRow(
|
|
||||||
string Type,
|
|
||||||
string Level,
|
|
||||||
string Description,
|
|
||||||
bool IsError
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -10,7 +10,6 @@ builder.Services.AddCascadingAuthenticationState();
|
||||||
builder.Services.AddAuthenticationStateDeserialization();
|
builder.Services.AddAuthenticationStateDeserialization();
|
||||||
|
|
||||||
builder.Services.AddScoped(_ => new HttpClient() { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });
|
builder.Services.AddScoped(_ => new HttpClient() { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });
|
||||||
builder.Services.AddScoped<RobotApp.Client.Services.RobotMonitorService>();
|
|
||||||
builder.Services.AddMudServices(config =>
|
builder.Services.AddMudServices(config =>
|
||||||
{
|
{
|
||||||
config.SnackbarConfiguration.VisibleStateDuration = 2000;
|
config.SnackbarConfiguration.VisibleStateDuration = 2000;
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
<Project Sdk="Microsoft.NET.Sdk.BlazorWebAssembly">
|
<Project Sdk="Microsoft.NET.Sdk.BlazorWebAssembly">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net10.0</TargetFramework>
|
<TargetFramework>net9.0</TargetFramework>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
<NoDefaultLaunchSettingsFile>true</NoDefaultLaunchSettingsFile>
|
<NoDefaultLaunchSettingsFile>true</NoDefaultLaunchSettingsFile>
|
||||||
|
|
@ -12,13 +12,11 @@
|
||||||
<PackageReference Include="Excubo.Blazor.Canvas" Version="3.2.91" />
|
<PackageReference Include="Excubo.Blazor.Canvas" Version="3.2.91" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="9.0.1" />
|
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="9.0.1" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Authentication" Version="9.0.1" />
|
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Authentication" Version="9.0.1" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.SignalR.Client" Version="10.0.1" />
|
<PackageReference Include="MudBlazor" Version="8.12.0" />
|
||||||
<PackageReference Include="MudBlazor" Version="8.15.0" />
|
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\RobotApp.Common.Shares\RobotApp.Common.Shares.csproj" />
|
<ProjectReference Include="..\RobotApp.Common.Shares\RobotApp.Common.Shares.csproj" />
|
||||||
<ProjectReference Include="..\RobotApp.VDA5050\RobotApp.VDA5050.csproj" />
|
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|
|
||||||
|
|
@ -1,52 +0,0 @@
|
||||||
using Microsoft.AspNetCore.Components;
|
|
||||||
using Microsoft.AspNetCore.SignalR.Client;
|
|
||||||
using RobotApp.Common.Shares.Dtos;
|
|
||||||
|
|
||||||
namespace RobotApp.Client.Services;
|
|
||||||
|
|
||||||
public class RobotMonitorService : IAsyncDisposable
|
|
||||||
{
|
|
||||||
private HubConnection? _hubConnection;
|
|
||||||
private readonly string _hubUrl;
|
|
||||||
|
|
||||||
public event Action<RobotMonitorDto>? OnDataReceived;
|
|
||||||
public bool IsConnected => _hubConnection?.State == HubConnectionState.Connected;
|
|
||||||
|
|
||||||
public RobotMonitorService(NavigationManager navigationManager)
|
|
||||||
{
|
|
||||||
var baseUrl = navigationManager.BaseUri.TrimEnd('/');
|
|
||||||
_hubUrl = $"{baseUrl}/hubs/robotMonitor";
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task StartAsync()
|
|
||||||
{
|
|
||||||
if (_hubConnection is not null) return;
|
|
||||||
|
|
||||||
_hubConnection = new HubConnectionBuilder()
|
|
||||||
.WithUrl(_hubUrl)
|
|
||||||
.WithAutomaticReconnect()
|
|
||||||
.Build();
|
|
||||||
|
|
||||||
_hubConnection.On<RobotMonitorDto>("ReceiveRobotMonitorData", data =>
|
|
||||||
{
|
|
||||||
OnDataReceived?.Invoke(data);
|
|
||||||
});
|
|
||||||
|
|
||||||
await _hubConnection.StartAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task StopAsync()
|
|
||||||
{
|
|
||||||
if (_hubConnection is null) return;
|
|
||||||
|
|
||||||
await _hubConnection.StopAsync();
|
|
||||||
await _hubConnection.DisposeAsync();
|
|
||||||
_hubConnection = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async ValueTask DisposeAsync()
|
|
||||||
{
|
|
||||||
await StopAsync();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
@ -1,60 +0,0 @@
|
||||||
// Helper functions for Robot Monitor
|
|
||||||
|
|
||||||
window.robotMonitor = {
|
|
||||||
// Get element size
|
|
||||||
getElementSize: function (element) {
|
|
||||||
const rect = element.getBoundingClientRect();
|
|
||||||
return {
|
|
||||||
Width: rect.width,
|
|
||||||
Height: rect.height
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
// Get element bounding rect
|
|
||||||
getElementBoundingRect: function (element) {
|
|
||||||
const rect = element.getBoundingClientRect();
|
|
||||||
return {
|
|
||||||
X: rect.x,
|
|
||||||
Y: rect.y,
|
|
||||||
Width: rect.width,
|
|
||||||
Height: rect.height
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
// Convert trajectory to SVG path
|
|
||||||
trajectoryToPath: function (trajectory, startX, startY, endX, endY) {
|
|
||||||
if (!trajectory || !trajectory.ControlPoints || trajectory.ControlPoints.length === 0) {
|
|
||||||
// Linear path
|
|
||||||
return `M ${startX} ${startY} L ${endX} ${endY}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
const degree = trajectory.Degree || 1;
|
|
||||||
const controlPoints = trajectory.ControlPoints;
|
|
||||||
|
|
||||||
if (degree === 1) {
|
|
||||||
// Linear
|
|
||||||
return `M ${startX} ${startY} L ${endX} ${endY}`;
|
|
||||||
} else if (degree === 2) {
|
|
||||||
// Quadratic bezier
|
|
||||||
if (controlPoints.length > 0) {
|
|
||||||
const cp1 = controlPoints[0];
|
|
||||||
return `M ${startX} ${startY} Q ${cp1.X} ${cp1.Y} ${endX} ${endY}`;
|
|
||||||
}
|
|
||||||
return `M ${startX} ${startY} L ${endX} ${endY}`;
|
|
||||||
} else if (degree === 3) {
|
|
||||||
// Cubic bezier
|
|
||||||
if (controlPoints.length >= 2) {
|
|
||||||
const cp1 = controlPoints[0];
|
|
||||||
const cp2 = controlPoints[1];
|
|
||||||
return `M ${startX} ${startY} C ${cp1.X} ${cp1.Y}, ${cp2.X} ${cp2.Y}, ${endX} ${endY}`;
|
|
||||||
} else if (controlPoints.length === 1) {
|
|
||||||
const cp1 = controlPoints[0];
|
|
||||||
return `M ${startX} ${startY} Q ${cp1.X} ${cp1.Y} ${endX} ${endY}`;
|
|
||||||
}
|
|
||||||
return `M ${startX} ${startY} L ${endX} ${endY}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
return `M ${startX} ${startY} L ${endX} ${endY}`;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
@ -1,31 +0,0 @@
|
||||||
using RobotApp.VDA5050.State;
|
|
||||||
|
|
||||||
namespace RobotApp.Common.Shares.Dtos;
|
|
||||||
|
|
||||||
public class RobotMonitorDto
|
|
||||||
{
|
|
||||||
public RobotPositionDto RobotPosition { get; set; } = new();
|
|
||||||
public EdgeStateDto[] EdgeStates { get; set; } = [];
|
|
||||||
public NodeState[] NodeStates { get; set; } = [];
|
|
||||||
public bool HasOrder { get; set; }
|
|
||||||
public DateTime Timestamp { get; set; } = DateTime.UtcNow;
|
|
||||||
}
|
|
||||||
|
|
||||||
public class RobotPositionDto
|
|
||||||
{
|
|
||||||
public double X { get; set; }
|
|
||||||
public double Y { get; set; }
|
|
||||||
public double Theta { get; set; }
|
|
||||||
}
|
|
||||||
public class EdgeStateDto()
|
|
||||||
{
|
|
||||||
public double StartX { get; set; }
|
|
||||||
public double StartY { get; set; }
|
|
||||||
public double EndX { get; set; }
|
|
||||||
public double EndY { get; set; }
|
|
||||||
public double ControlPoint1X { get; set; }
|
|
||||||
public double ControlPoint1Y { get; set; }
|
|
||||||
public double ControlPoint2X { get; set; }
|
|
||||||
public double ControlPoint2Y { get; set; }
|
|
||||||
public int Degree { get; set; }
|
|
||||||
}
|
|
||||||
|
|
@ -1,13 +1,9 @@
|
||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net10.0</TargetFramework>
|
<TargetFramework>net9.0</TargetFramework>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<ProjectReference Include="..\RobotApp.VDA5050\RobotApp.VDA5050.csproj" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net10.0</TargetFramework>
|
<TargetFramework>net9.0</TargetFramework>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
|
|
||||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||||
# Visual Studio Version 18
|
# Visual Studio Version 17
|
||||||
VisualStudioVersion = 18.0.11205.157 d18.0
|
VisualStudioVersion = 17.14.36511.14
|
||||||
MinimumVisualStudioVersion = 10.0.40219.1
|
MinimumVisualStudioVersion = 10.0.40219.1
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RobotApp", "RobotApp\RobotApp.csproj", "{BF0BB137-2EF9-4E1B-944E-9BF41C5284F7}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RobotApp", "RobotApp\RobotApp.csproj", "{BF0BB137-2EF9-4E1B-944E-9BF41C5284F7}"
|
||||||
EndProject
|
EndProject
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
@page "/home"
|
@page "/"
|
||||||
@using Microsoft.AspNetCore.Authorization
|
@using Microsoft.AspNetCore.Authorization
|
||||||
|
|
||||||
@rendermode InteractiveServer
|
@rendermode InteractiveServer
|
||||||
|
|
|
||||||
|
|
@ -1,36 +0,0 @@
|
||||||
using Microsoft.AspNetCore.Mvc;
|
|
||||||
using RobotApp.Services.Robot;
|
|
||||||
using RobotApp.VDA5050.Order;
|
|
||||||
using System.Text.Json;
|
|
||||||
|
|
||||||
namespace RobotApp.Controllers;
|
|
||||||
|
|
||||||
[ApiController]
|
|
||||||
[Route("api/order")]
|
|
||||||
public class OrderController : ControllerBase
|
|
||||||
{
|
|
||||||
private readonly RobotOrderController robotOrderController;
|
|
||||||
|
|
||||||
public OrderController(RobotOrderController robotOrderController)
|
|
||||||
{
|
|
||||||
this.robotOrderController = robotOrderController;
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpPost]
|
|
||||||
public IActionResult SendOrder([FromBody] OrderMsg order)
|
|
||||||
{
|
|
||||||
Console.WriteLine("===== ORDER RECEIVED =====");
|
|
||||||
Console.WriteLine(JsonSerializer.Serialize(order, new JsonSerializerOptions
|
|
||||||
{
|
|
||||||
WriteIndented = true
|
|
||||||
}));
|
|
||||||
|
|
||||||
robotOrderController.UpdateOrder(order);
|
|
||||||
|
|
||||||
return Ok(new
|
|
||||||
{
|
|
||||||
success = true,
|
|
||||||
message = "Order received"
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,12 +0,0 @@
|
||||||
using Microsoft.AspNetCore.SignalR;
|
|
||||||
|
|
||||||
namespace RobotApp.Hubs;
|
|
||||||
|
|
||||||
public class RobotMonitorHub : Hub
|
|
||||||
{
|
|
||||||
public async Task SendRobotMonitorData(RobotApp.Common.Shares.Dtos.RobotMonitorDto data)
|
|
||||||
{
|
|
||||||
await Clients.All.SendAsync("ReceiveRobotMonitorData", data);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
using RobotApp.Common.Shares.Dtos;
|
using RobotApp.VDA5050.Order;
|
||||||
using RobotApp.VDA5050.Order;
|
|
||||||
using RobotApp.VDA5050.State;
|
using RobotApp.VDA5050.State;
|
||||||
|
|
||||||
namespace RobotApp.Interfaces;
|
namespace RobotApp.Interfaces;
|
||||||
|
|
@ -13,7 +12,6 @@ public interface IOrder
|
||||||
|
|
||||||
NodeState[] NodeStates { get; }
|
NodeState[] NodeStates { get; }
|
||||||
EdgeState[] EdgeStates { get; }
|
EdgeState[] EdgeStates { get; }
|
||||||
(NodeState[], EdgeStateDto[]) CurrentPath { get; }
|
|
||||||
|
|
||||||
void UpdateOrder(OrderMsg order);
|
void UpdateOrder(OrderMsg order);
|
||||||
void StopOrder();
|
void StopOrder();
|
||||||
|
|
|
||||||
|
|
@ -3,11 +3,9 @@ using Microsoft.AspNetCore.Identity;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using MudBlazor.Services;
|
using MudBlazor.Services;
|
||||||
using NLog.Web;
|
using NLog.Web;
|
||||||
using RobotApp.Client;
|
|
||||||
using RobotApp.Components;
|
using RobotApp.Components;
|
||||||
using RobotApp.Components.Account;
|
using RobotApp.Components.Account;
|
||||||
using RobotApp.Data;
|
using RobotApp.Data;
|
||||||
using RobotApp.Hubs;
|
|
||||||
using RobotApp.Services;
|
using RobotApp.Services;
|
||||||
using RobotApp.Services.Robot.Simulation;
|
using RobotApp.Services.Robot.Simulation;
|
||||||
|
|
||||||
|
|
@ -29,7 +27,7 @@ builder.Services.AddAuthorization();
|
||||||
|
|
||||||
// Add Controllers for API endpoints
|
// Add Controllers for API endpoints
|
||||||
builder.Services.AddControllers();
|
builder.Services.AddControllers();
|
||||||
builder.Services.AddSignalR();
|
|
||||||
var connectionString = builder.Configuration.GetConnectionString("DefaultConnection") ?? throw new InvalidOperationException("Connection string 'DefaultConnection' not found.");
|
var connectionString = builder.Configuration.GetConnectionString("DefaultConnection") ?? throw new InvalidOperationException("Connection string 'DefaultConnection' not found.");
|
||||||
Action<DbContextOptionsBuilder> appDbOptions = options => options.UseSqlite(connectionString, b => b.MigrationsAssembly("RobotApp"));
|
Action<DbContextOptionsBuilder> appDbOptions = options => options.UseSqlite(connectionString, b => b.MigrationsAssembly("RobotApp"));
|
||||||
|
|
||||||
|
|
@ -51,16 +49,9 @@ builder.Services.AddSingleton<IEmailSender<ApplicationUser>, IdentityNoOpEmailSe
|
||||||
|
|
||||||
builder.Services.AddSingleton(typeof(RobotApp.Services.Logger<>));
|
builder.Services.AddSingleton(typeof(RobotApp.Services.Logger<>));
|
||||||
|
|
||||||
// Add SignalR
|
|
||||||
builder.Services.AddSignalR();
|
|
||||||
|
|
||||||
builder.Services.AddRobotSimulation();
|
builder.Services.AddRobotSimulation();
|
||||||
builder.Services.AddRobot();
|
builder.Services.AddRobot();
|
||||||
|
|
||||||
// Add RobotMonitorService
|
|
||||||
builder.Services.AddSingleton<RobotMonitorService>();
|
|
||||||
builder.Services.AddHostedService(sp => sp.GetRequiredService<RobotMonitorService>());
|
|
||||||
|
|
||||||
var app = builder.Build();
|
var app = builder.Build();
|
||||||
await app.Services.SeedApplicationDbAsync();
|
await app.Services.SeedApplicationDbAsync();
|
||||||
|
|
||||||
|
|
@ -85,13 +76,9 @@ app.UseAntiforgery();
|
||||||
|
|
||||||
app.MapStaticAssets();
|
app.MapStaticAssets();
|
||||||
|
|
||||||
|
|
||||||
// Map API Controllers
|
// Map API Controllers
|
||||||
app.MapControllers();
|
app.MapControllers();
|
||||||
|
|
||||||
// Map SignalR Hub
|
|
||||||
app.MapHub<RobotMonitorHub>("/hubs/robotMonitor");
|
|
||||||
|
|
||||||
app.MapRazorComponents<App>()
|
app.MapRazorComponents<App>()
|
||||||
.AddInteractiveServerRenderMode()
|
.AddInteractiveServerRenderMode()
|
||||||
.AddInteractiveWebAssemblyRenderMode()
|
.AddInteractiveWebAssemblyRenderMode()
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net10.0</TargetFramework>
|
<TargetFramework>net9.0</TargetFramework>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
<UserSecretsId>aspnet-RobotApp-1f61caa2-bbbb-40cd-88b6-409b408a84ea</UserSecretsId>
|
<UserSecretsId>aspnet-RobotApp-1f61caa2-bbbb-40cd-88b6-409b408a84ea</UserSecretsId>
|
||||||
|
|
|
||||||
|
|
@ -54,6 +54,8 @@ public partial class RobotController
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else StateManager.TransitionTo(RootStateType.Auto);
|
else StateManager.TransitionTo(RootStateType.Auto);
|
||||||
|
|
||||||
|
ErrorManager.AddError(RobotErrors.Error2001());
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
using MudBlazor;
|
using MudBlazor;
|
||||||
using RobotApp.Common.Shares.Dtos;
|
|
||||||
using RobotApp.Common.Shares.Enums;
|
using RobotApp.Common.Shares.Enums;
|
||||||
using RobotApp.Interfaces;
|
using RobotApp.Interfaces;
|
||||||
using RobotApp.Services.Exceptions;
|
using RobotApp.Services.Exceptions;
|
||||||
|
|
@ -28,7 +27,6 @@ public class RobotOrderController(INavigation NavigationManager,
|
||||||
public EdgeState[] EdgeStates { get; private set; } = [];
|
public EdgeState[] EdgeStates { get; private set; } = [];
|
||||||
public string LastNodeId => LastNode is null ? "" : LastNode.NodeId;
|
public string LastNodeId => LastNode is null ? "" : LastNode.NodeId;
|
||||||
public int LastNodeSequenceId => LastNode is null ? 0 : LastNode.SequenceId;
|
public int LastNodeSequenceId => LastNode is null ? 0 : LastNode.SequenceId;
|
||||||
public (NodeState[], EdgeStateDto[]) CurrentPath => GetCurrentPath();
|
|
||||||
|
|
||||||
private const int CycleHandlerMilliseconds = 100;
|
private const int CycleHandlerMilliseconds = 100;
|
||||||
private WatchTimer<RobotOrderController>? OrderTimer;
|
private WatchTimer<RobotOrderController>? OrderTimer;
|
||||||
|
|
@ -42,8 +40,6 @@ public class RobotOrderController(INavigation NavigationManager,
|
||||||
private VDA5050.Order.Edge[] Edges = [];
|
private VDA5050.Order.Edge[] Edges = [];
|
||||||
private Node? CurrentBaseNode;
|
private Node? CurrentBaseNode;
|
||||||
private Node? LastNode;
|
private Node? LastNode;
|
||||||
private Node[] NewOrderNodes = [];
|
|
||||||
private VDA5050.Order.Edge[] NewOrderEdges = [];
|
|
||||||
|
|
||||||
private readonly Lock LockObject = new();
|
private readonly Lock LockObject = new();
|
||||||
|
|
||||||
|
|
@ -227,7 +223,7 @@ public class RobotOrderController(INavigation NavigationManager,
|
||||||
SafetyManager.OnSafetySpeedChanged += OnSafetySpeedChanged;
|
SafetyManager.OnSafetySpeedChanged += OnSafetySpeedChanged;
|
||||||
|
|
||||||
if (OrderActions.TryGetValue(order.Nodes[^1].NodeId, out Action[]? finalactions) && finalactions is not null && finalactions.Length > 0) FinalAction = [.. finalactions];
|
if (OrderActions.TryGetValue(order.Nodes[^1].NodeId, out Action[]? finalactions) && finalactions is not null && finalactions.Length > 0) FinalAction = [.. finalactions];
|
||||||
|
|
||||||
if (OrderActions.Count > 0) ActionManager.AddOrderActions([.. OrderActions.Values.SelectMany(a => a)]);
|
if (OrderActions.Count > 0) ActionManager.AddOrderActions([.. OrderActions.Values.SelectMany(a => a)]);
|
||||||
|
|
||||||
if (order.Nodes.Length > 1 && order.Edges.Length >= 0)
|
if (order.Nodes.Length > 1 && order.Edges.Length >= 0)
|
||||||
|
|
@ -240,8 +236,6 @@ public class RobotOrderController(INavigation NavigationManager,
|
||||||
OrderUpdateId = order.OrderUpdateId;
|
OrderUpdateId = order.OrderUpdateId;
|
||||||
Nodes = order.Nodes;
|
Nodes = order.Nodes;
|
||||||
Edges = order.Edges;
|
Edges = order.Edges;
|
||||||
NewOrderNodes = order.Nodes;
|
|
||||||
NewOrderEdges = order.Edges;
|
|
||||||
if (CurrentBaseNode is not null && CurrentBaseNode.NodeId != Nodes[0].NodeId && Nodes.Length > 1) NavigationManager.UpdateOrder(CurrentBaseNode.NodeId);
|
if (CurrentBaseNode is not null && CurrentBaseNode.NodeId != Nodes[0].NodeId && Nodes.Length > 1) NavigationManager.UpdateOrder(CurrentBaseNode.NodeId);
|
||||||
if (StateManager.CurrentStateName != AutoStateType.Executing.ToString()) StateManager.TransitionTo(AutoStateType.Executing);
|
if (StateManager.CurrentStateName != AutoStateType.Executing.ToString()) StateManager.TransitionTo(AutoStateType.Executing);
|
||||||
UpdateState();
|
UpdateState();
|
||||||
|
|
@ -264,7 +258,7 @@ public class RobotOrderController(INavigation NavigationManager,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void HandleUpdateOrder(OrderMsg order)
|
private void HandleUpdateOrder(OrderMsg order)
|
||||||
{
|
{
|
||||||
if (order.OrderId != OrderId) throw new OrderException(RobotErrors.Error1001(OrderId, order.OrderId));
|
if (order.OrderId != OrderId) throw new OrderException(RobotErrors.Error1001(OrderId, order.OrderId));
|
||||||
if (order.OrderUpdateId <= OrderUpdateId) return;
|
if (order.OrderUpdateId <= OrderUpdateId) return;
|
||||||
|
|
@ -418,68 +412,4 @@ public class RobotOrderController(INavigation NavigationManager,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private EdgeStateDto[] SplitChecking(Node lastNode, Node nearLastNode, VDA5050.Order.Edge edge)
|
|
||||||
{
|
|
||||||
List<EdgeStateDto> pathEdges = [];
|
|
||||||
var splitStartPath = RobotPathPlanner.PathSplit([lastNode, nearLastNode], [edge], 0.1);
|
|
||||||
if (splitStartPath is not null && splitStartPath.Length > 0)
|
|
||||||
{
|
|
||||||
int index = 0;
|
|
||||||
double minDistance = double.MaxValue;
|
|
||||||
for (int i = 0; i < splitStartPath.Length; i++)
|
|
||||||
{
|
|
||||||
var distance = Math.Sqrt(Math.Pow(Localization.X - splitStartPath[i].NodePosition.X, 2) +
|
|
||||||
Math.Pow(Localization.Y - splitStartPath[i].NodePosition.Y, 2));
|
|
||||||
if (distance < minDistance)
|
|
||||||
{
|
|
||||||
minDistance = distance;
|
|
||||||
index = i;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for (int i = index; i < splitStartPath.Length - 1; i++)
|
|
||||||
{
|
|
||||||
pathEdges.Add(new()
|
|
||||||
{
|
|
||||||
StartX = splitStartPath[i].NodePosition.X,
|
|
||||||
StartY = splitStartPath[i].NodePosition.Y,
|
|
||||||
EndX = splitStartPath[i + 1].NodePosition.X,
|
|
||||||
EndY = splitStartPath[i + 1].NodePosition.Y,
|
|
||||||
Degree = 1,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return [.. pathEdges];
|
|
||||||
}
|
|
||||||
|
|
||||||
private (NodeState[], EdgeStateDto[]) GetCurrentPath()
|
|
||||||
{
|
|
||||||
if (NodeStates.Length == 0 && EdgeStates.Length == 0) return ([], []);
|
|
||||||
|
|
||||||
List<EdgeStateDto> pathEdges = [];
|
|
||||||
var lastNodeIndex = Array.FindIndex(NewOrderNodes, n => n.NodeId == LastNodeId);
|
|
||||||
lastNodeIndex = lastNodeIndex != -1 ? lastNodeIndex : 0;
|
|
||||||
if (lastNodeIndex < NewOrderNodes.Length - 1) pathEdges = [.. SplitChecking(NewOrderNodes[lastNodeIndex], NewOrderNodes[lastNodeIndex + 1], NewOrderEdges[lastNodeIndex])];
|
|
||||||
if (lastNodeIndex < NewOrderNodes.Length - 2)
|
|
||||||
{
|
|
||||||
var nodes = NewOrderNodes.ToList().GetRange(lastNodeIndex + 1, NewOrderNodes.Length - lastNodeIndex - 1);
|
|
||||||
var edges = NewOrderEdges.ToList().GetRange(lastNodeIndex + 1, nodes.Count - 1);
|
|
||||||
for (int i = 0; i < nodes.Count - 1; i++)
|
|
||||||
{
|
|
||||||
pathEdges.Add(new()
|
|
||||||
{
|
|
||||||
StartX = nodes[i].NodePosition.X,
|
|
||||||
StartY = nodes[i].NodePosition.Y,
|
|
||||||
EndX = nodes[i + 1].NodePosition.X,
|
|
||||||
EndY = nodes[i + 1].NodePosition.Y,
|
|
||||||
ControlPoint1X = edges[i].Trajectory is not null && edges[i].Trajectory.ControlPoints.Length > 2 ? edges[i].Trajectory.ControlPoints[1].X : 0,
|
|
||||||
ControlPoint1Y = edges[i].Trajectory is not null && edges[i].Trajectory.ControlPoints.Length > 2 ? edges[i].Trajectory.ControlPoints[1].Y : 0,
|
|
||||||
ControlPoint2X = edges[i].Trajectory is not null && edges[i].Trajectory.ControlPoints.Length > 3 ? edges[i].Trajectory.ControlPoints[2].X : 0,
|
|
||||||
ControlPoint2Y = edges[i].Trajectory is not null && edges[i].Trajectory.ControlPoints.Length > 3 ? edges[i].Trajectory.ControlPoints[2].Y : 0,
|
|
||||||
Degree = edges[i].Trajectory.Degree,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return (NodeStates, [.. pathEdges]);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -164,72 +164,4 @@ public class RobotPathPlanner(IConfiguration Configuration)
|
||||||
}
|
}
|
||||||
return [.. navigationNode];
|
return [.. navigationNode];
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Node[] PathSplit(Node[] nodes, Edge[] edges, double resolutionSplit)
|
|
||||||
{
|
|
||||||
if (nodes.Length < 2) throw new PathPlannerException(RobotErrors.Error1002(nodes.Length));
|
|
||||||
if (edges.Length < 1) throw new PathPlannerException(RobotErrors.Error1003(edges.Length));
|
|
||||||
if (edges.Length != nodes.Length - 1) throw new PathPlannerException(RobotErrors.Error1004(nodes.Length, edges.Length));
|
|
||||||
|
|
||||||
List<Node> navigationNode = [new()
|
|
||||||
{
|
|
||||||
NodeId = nodes[0].NodeId,
|
|
||||||
NodePosition = new()
|
|
||||||
{
|
|
||||||
X = nodes[0].NodePosition.X,
|
|
||||||
Y = nodes[0].NodePosition.Y,
|
|
||||||
Theta = nodes[0].NodePosition.Theta,
|
|
||||||
}
|
|
||||||
}];
|
|
||||||
foreach (var edge in edges)
|
|
||||||
{
|
|
||||||
var startNode = nodes.FirstOrDefault(n => n.NodeId == edge.StartNodeId);
|
|
||||||
var endNode = nodes.FirstOrDefault(n => n.NodeId == edge.EndNodeId);
|
|
||||||
if (startNode is null) throw new PathPlannerException(RobotErrors.Error1014(edge.EdgeId, edge.StartNodeId));
|
|
||||||
if (endNode is null) throw new PathPlannerException(RobotErrors.Error1014(edge.EdgeId, edge.EndNodeId));
|
|
||||||
|
|
||||||
var EdgeCalculatorModel = new EdgeCalculatorModel()
|
|
||||||
{
|
|
||||||
X1 = startNode.NodePosition.X,
|
|
||||||
Y1 = startNode.NodePosition.Y,
|
|
||||||
X2 = endNode.NodePosition.X,
|
|
||||||
Y2 = endNode.NodePosition.Y,
|
|
||||||
ControlPoint1X = edge.Trajectory.ControlPoints.Length > 2 ? edge.Trajectory.ControlPoints[1].X : 0,
|
|
||||||
ControlPoint1Y = edge.Trajectory.ControlPoints.Length > 2 ? edge.Trajectory.ControlPoints[1].Y : 0,
|
|
||||||
ControlPoint2X = edge.Trajectory.ControlPoints.Length > 3 ? edge.Trajectory.ControlPoints[2].X : 0,
|
|
||||||
ControlPoint2Y = edge.Trajectory.ControlPoints.Length > 3 ? edge.Trajectory.ControlPoints[2].Y : 0,
|
|
||||||
TrajectoryDegree = edge.Trajectory.Degree == 1 ? TrajectoryDegree.One : edge.Trajectory.Degree == 2 ? TrajectoryDegree.Two : TrajectoryDegree.Three
|
|
||||||
};
|
|
||||||
|
|
||||||
double length = EdgeCalculatorModel.GetEdgeLength();
|
|
||||||
if (length <= 0) continue;
|
|
||||||
double step = resolutionSplit / length;
|
|
||||||
|
|
||||||
for (double t = step; t <= 1 - step; t += step)
|
|
||||||
{
|
|
||||||
(double x, double y) = EdgeCalculatorModel.Curve(t);
|
|
||||||
navigationNode.Add(new()
|
|
||||||
{
|
|
||||||
NodeId = string.Empty,
|
|
||||||
NodePosition = new()
|
|
||||||
{
|
|
||||||
X = x,
|
|
||||||
Y = y,
|
|
||||||
Theta = startNode.NodePosition.Theta,
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
navigationNode.Add(new()
|
|
||||||
{
|
|
||||||
NodeId = endNode.NodeId,
|
|
||||||
NodePosition = new()
|
|
||||||
{
|
|
||||||
X = endNode.NodePosition.X,
|
|
||||||
Y = endNode.NodePosition.Y,
|
|
||||||
Theta = endNode.NodePosition.Theta,
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return [.. navigationNode];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,84 +0,0 @@
|
||||||
using Microsoft.AspNetCore.SignalR;
|
|
||||||
using RobotApp.Common.Shares.Dtos;
|
|
||||||
using RobotApp.Hubs;
|
|
||||||
using RobotApp.Interfaces;
|
|
||||||
using RobotApp.VDA5050.State;
|
|
||||||
|
|
||||||
namespace RobotApp.Services;
|
|
||||||
|
|
||||||
public class RobotMonitorService(IHubContext<RobotMonitorHub> HubContext,
|
|
||||||
IOrder OrderManager,
|
|
||||||
ILocalization Localization,
|
|
||||||
Logger<RobotMonitorService> Logger) : BackgroundService
|
|
||||||
{
|
|
||||||
private WatchTimerAsync<RobotMonitorService>? UpdateTimer;
|
|
||||||
private const int UpdateInterval = 200; // 200ms
|
|
||||||
|
|
||||||
private async Task UpdateHandler()
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
// Lấy vị trí robot từ ILocalization (giống như RobotVisualization)
|
|
||||||
var robotPosition = new RobotPositionDto
|
|
||||||
{
|
|
||||||
X = Localization.X,
|
|
||||||
Y = Localization.Y,
|
|
||||||
Theta = Localization.Theta
|
|
||||||
};
|
|
||||||
|
|
||||||
// Kiểm tra có order không
|
|
||||||
bool hasOrder = OrderManager.NodeStates.Length > 0 || OrderManager.EdgeStates.Length > 0;
|
|
||||||
|
|
||||||
// Lấy CurrentPath từ IOrder
|
|
||||||
(NodeState[] nodeStates, EdgeStateDto[] edgeStates) = OrderManager.CurrentPath;
|
|
||||||
// Tạo DTO
|
|
||||||
var monitorDto = new RobotMonitorDto
|
|
||||||
{
|
|
||||||
RobotPosition = robotPosition,
|
|
||||||
NodeStates = nodeStates,
|
|
||||||
EdgeStates = [.. edgeStates],
|
|
||||||
HasOrder = hasOrder,
|
|
||||||
Timestamp = DateTime.UtcNow
|
|
||||||
};
|
|
||||||
|
|
||||||
// Broadcast qua SignalR Hub
|
|
||||||
await HubContext.Clients.All.SendAsync("ReceiveRobotMonitorData", monitorDto);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Logger.Warning($"Lỗi khi broadcast robot monitor data: {ex.Message}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
|
||||||
{
|
|
||||||
await Task.Yield();
|
|
||||||
|
|
||||||
// Đợi robot sẵn sàng
|
|
||||||
while (!stoppingToken.IsCancellationRequested)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
// Kiểm tra xem Localization có sẵn sàng không
|
|
||||||
if (Localization.IsReady) break;
|
|
||||||
}
|
|
||||||
catch { }
|
|
||||||
|
|
||||||
await Task.Delay(1000, stoppingToken);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!stoppingToken.IsCancellationRequested)
|
|
||||||
{
|
|
||||||
UpdateTimer = new(UpdateInterval, UpdateHandler, Logger);
|
|
||||||
UpdateTimer.Start();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public override Task StopAsync(CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
UpdateTimer?.Dispose();
|
|
||||||
UpdateTimer = null;
|
|
||||||
return base.StopAsync(cancellationToken);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Loading…
Reference in New Issue
Block a user