Compare commits
No commits in common. "5c1851e92ff29d4068e36498d3c1b129ccd8fc6a" and "e4e135e35f548a1a158c2c1421d3ec5be06440f2" have entirely different histories.
5c1851e92f
...
e4e135e35f
|
|
@ -1,5 +1,2 @@
|
||||||
# RobotApp
|
# RobotApp
|
||||||
|
|
||||||
docker build -t robotics.doc/robotnet/robotapp_dde:2.7 .
|
|
||||||
docker save -o robotapp-dde.2.7.tar robotics.doc/robotnet/robotapp_dde:2.7
|
|
||||||
scp .\robotapp-dde.2.7.tar robotics@172.20.235.176:~/DDE
|
|
||||||
|
|
@ -17,26 +17,12 @@
|
||||||
<i class="mdi mdi-fit-to-screen-outline icon-button"></i>
|
<i class="mdi mdi-fit-to-screen-outline icon-button"></i>
|
||||||
</button>
|
</button>
|
||||||
</MudTooltip>
|
</MudTooltip>
|
||||||
<div class="auto-follow-control">
|
|
||||||
<MudTooltip Text="Auto Follow Robot" Placement="Placement.Bottom" Color="Color.Info">
|
|
||||||
<MudSwitch T="bool" Value="AutoFollowRobot" ValueChanged="OnAutoFollowRobotChanged" Color="Color.Info" Size="Size.Small">
|
|
||||||
<span style="color: white; font-size: 14px; margin-left: 8px;">Follow Robot</span>
|
|
||||||
</MudSwitch>
|
|
||||||
</MudTooltip>
|
|
||||||
</div>
|
|
||||||
<MudSpacer />
|
<MudSpacer />
|
||||||
@if (MonitorData?.RobotPosition != null)
|
@if (MonitorData?.RobotPosition != null)
|
||||||
{
|
{
|
||||||
<div class="robot-position-info">
|
<div class="robot-position-info">
|
||||||
<i class="mdi mdi-map-marker"></i>
|
<i class="mdi mdi-map-marker"></i>
|
||||||
<span>Robot: X: @MonitorData.RobotPosition.X.ToString("F2")m | Y: @MonitorData.RobotPosition.Y.ToString("F2")m | θ: @((MonitorData.RobotPosition.Theta * 180 / Math.PI).ToString("F1"))°</span>
|
<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>
|
|
||||||
}
|
|
||||||
@if (MouseWorldX.HasValue && MouseWorldY.HasValue)
|
|
||||||
{
|
|
||||||
<div class="mouse-position-info">
|
|
||||||
<i class="mdi mdi-cursor-pointer"></i>
|
|
||||||
<span>Mouse: X: @MouseWorldX.Value.ToString("F2")m | Y: @MouseWorldY.Value.ToString("F2")m</span>
|
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
@* <MudChip T="string" Color="@(IsConnected? Color.Success: Color.Error)" Size="Size.Small">
|
@* <MudChip T="string" Color="@(IsConnected? Color.Success: Color.Error)" Size="Size.Small">
|
||||||
|
|
@ -50,52 +36,18 @@
|
||||||
@onmousemove="HandleMouseMove"
|
@onmousemove="HandleMouseMove"
|
||||||
@onmouseup="HandleMouseUp"
|
@onmouseup="HandleMouseUp"
|
||||||
@onmouseleave="HandleMouseLeave">
|
@onmouseleave="HandleMouseLeave">
|
||||||
@* Arrow markers for origin *@
|
|
||||||
<defs>
|
|
||||||
<marker id="arrowhead-x" markerWidth="10" markerHeight="10"
|
|
||||||
refX="9" refY="3" orient="auto" markerUnits="strokeWidth">
|
|
||||||
<path d="M0,0 L0,6 L9,3 z" fill="#FF0000" />
|
|
||||||
</marker>
|
|
||||||
<marker id="arrowhead-y" markerWidth="10" markerHeight="10"
|
|
||||||
refX="9" refY="3" orient="auto" markerUnits="strokeWidth">
|
|
||||||
<path d="M0,0 L0,6 L9,3 z" fill="#00FF00" />
|
|
||||||
</marker>
|
|
||||||
</defs>
|
|
||||||
<g transform="@GetTransform()">
|
<g transform="@GetTransform()">
|
||||||
@* Background Map Image *@
|
|
||||||
@if (MapImageLoaded && MapImageWidth > 0 && MapImageHeight > 0)
|
|
||||||
{
|
|
||||||
@* Image origin is at bottom-left corner (MapImageOriginX, MapImageOriginY) in world coordinates
|
|
||||||
In SVG (after Y flip), image top-left corner is at (MapImageOriginX, -MapImageOriginY - MapImageHeight)
|
|
||||||
So we render image at: x = MapImageOriginX, y = -MapImageOriginY - MapImageHeight *@
|
|
||||||
<image href="@MapImageUrl"
|
|
||||||
x="@WorldToSvgX(MapImageOriginX)"
|
|
||||||
y="@WorldToSvgY(MapImageOriginY + MapImageHeight)"
|
|
||||||
width="@MapImageWidth"
|
|
||||||
height="@MapImageHeight"
|
|
||||||
preserveAspectRatio="none"
|
|
||||||
opacity="0.8"
|
|
||||||
style="pointer-events: none; image-rendering: pixelated;"
|
|
||||||
id="map-background-image" />
|
|
||||||
}
|
|
||||||
|
|
||||||
@* Origin Marker (2 arrows: X+ and Y+) at (MapImageOriginX, MapImageOriginY) *@
|
|
||||||
<g transform="@GetOriginMarkerTransform()">
|
|
||||||
@* X+ Arrow (pointing right) *@
|
|
||||||
<line x1="0" y1="0" x2="@GetOriginMarkerSize()" y2="0"
|
|
||||||
stroke="#FF0000" stroke-width="@GetOriginMarkerStrokeWidth()"
|
|
||||||
marker-end="url(#arrowhead-x)" />
|
|
||||||
@* Y+ Arrow (pointing up in world, down in SVG) *@
|
|
||||||
<line x1="0" y1="0" x2="0" y2="@(-GetOriginMarkerSize())"
|
|
||||||
stroke="#00FF00" stroke-width="@GetOriginMarkerStrokeWidth()"
|
|
||||||
marker-end="url(#arrowhead-y)" />
|
|
||||||
@* Origin point *@
|
|
||||||
<circle cx="0" cy="0" r="@(GetOriginMarkerSize() * 0.12)"
|
|
||||||
fill="#FFFF00" stroke="#000" stroke-width="@(GetOriginMarkerStrokeWidth() * 0.5)" />
|
|
||||||
</g>
|
|
||||||
|
|
||||||
@if (MonitorData?.HasOrder == true)
|
@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"
|
<path d="@PathView"
|
||||||
fill="none"
|
fill="none"
|
||||||
stroke="#42A5F5"
|
stroke="#42A5F5"
|
||||||
|
|
@ -154,7 +106,7 @@
|
||||||
private ElementReference SvgRef;
|
private ElementReference SvgRef;
|
||||||
private ElementReference SvgContainerRef;
|
private ElementReference SvgContainerRef;
|
||||||
|
|
||||||
private double ZoomScale = 1.5; // Zoom vào robot hơn khi mở
|
private double ZoomScale = 2.0; // Zoom vào robot hơn khi mở
|
||||||
private const double MIN_ZOOM = 0.1;
|
private const double MIN_ZOOM = 0.1;
|
||||||
private const double MAX_ZOOM = 5.0;
|
private const double MAX_ZOOM = 5.0;
|
||||||
private const double BASE_PIXELS_PER_METER = 50.0;
|
private const double BASE_PIXELS_PER_METER = 50.0;
|
||||||
|
|
@ -172,44 +124,14 @@
|
||||||
private string PathView = "";
|
private string PathView = "";
|
||||||
private string PathIsNot = "hidden";
|
private string PathIsNot = "hidden";
|
||||||
|
|
||||||
// Mouse world coordinates
|
|
||||||
private double? MouseWorldX = null;
|
|
||||||
private double? MouseWorldY = null;
|
|
||||||
|
|
||||||
// Auto-follow robot
|
|
||||||
private bool AutoFollowRobot = false;
|
|
||||||
|
|
||||||
private void OnAutoFollowRobotChanged(bool value)
|
|
||||||
{
|
|
||||||
AutoFollowRobot = value;
|
|
||||||
if (AutoFollowRobot && MonitorData?.RobotPosition != null)
|
|
||||||
{
|
|
||||||
UpdateViewToFollowRobot();
|
|
||||||
StateHasChanged();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Map image properties
|
|
||||||
private const double MapImageOriginX = -20.0; // OriginX in world coordinates (meters)
|
|
||||||
private const double MapImageOriginY = -20.0; // OriginY in world coordinates (meters)
|
|
||||||
private const double MapImageResolution = 0.1; // Resolution: meters per pixel
|
|
||||||
private const string MapImageUrl = "images/gara20250309.png";
|
|
||||||
|
|
||||||
private bool MapImageLoaded = false;
|
|
||||||
private double MapImageWidth = 0; // Width in world coordinates (meters)
|
|
||||||
private double MapImageHeight = 0; // Height in world coordinates (meters)
|
|
||||||
|
|
||||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||||
{
|
{
|
||||||
if (firstRender)
|
if (firstRender)
|
||||||
{
|
{
|
||||||
var containerSize = await JS.InvokeAsync<ElementSize>("robotMonitor.getElementSize", SvgContainerRef);
|
var containerSize = await JS.InvokeAsync<ElementSize>("getElementSize", SvgContainerRef);
|
||||||
SvgWidth = containerSize.Width;
|
SvgWidth = containerSize.Width;
|
||||||
SvgHeight = containerSize.Height;
|
SvgHeight = containerSize.Height;
|
||||||
|
|
||||||
// Load map image and get dimensions
|
|
||||||
await LoadMapImage();
|
|
||||||
|
|
||||||
// Center view on robot if available with initial zoom
|
// Center view on robot if available with initial zoom
|
||||||
if (MonitorData?.RobotPosition != null)
|
if (MonitorData?.RobotPosition != null)
|
||||||
{
|
{
|
||||||
|
|
@ -228,37 +150,8 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task LoadMapImage()
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var imageDimensions = await JS.InvokeAsync<ElementSize>("robotMonitor.loadImageAndGetDimensions", MapImageUrl);
|
|
||||||
|
|
||||||
// Convert pixel dimensions to world coordinates (meters)
|
|
||||||
MapImageWidth = imageDimensions.Width * MapImageResolution;
|
|
||||||
MapImageHeight = imageDimensions.Height * MapImageResolution;
|
|
||||||
|
|
||||||
if (MapImageWidth > 0 && MapImageHeight > 0)
|
|
||||||
{
|
|
||||||
MapImageLoaded = true;
|
|
||||||
await InvokeAsync(StateHasChanged); // Force re-render after image is loaded
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
MapImageLoaded = false;
|
|
||||||
Console.WriteLine($"Failed to load map image: {ex.Message}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private string GetTransform()
|
private string GetTransform()
|
||||||
{
|
{
|
||||||
// Transform applies: first translate (in screen pixels), then scale (pixels per meter)
|
|
||||||
// World coordinates are in meters
|
|
||||||
// After transform: screenX = TranslateX + worldX * (ZoomScale * BASE_PIXELS_PER_METER)
|
|
||||||
// screenY = TranslateY + worldY * (ZoomScale * BASE_PIXELS_PER_METER)
|
|
||||||
// But we need to flip Y: screenY = TranslateY - worldY * (ZoomScale * BASE_PIXELS_PER_METER)
|
|
||||||
// This is handled by WorldToSvgY which flips Y before applying transform
|
|
||||||
return $"translate({TranslateX}, {TranslateY}) scale({ZoomScale * BASE_PIXELS_PER_METER})";
|
return $"translate({TranslateX}, {TranslateY}) scale({ZoomScale * BASE_PIXELS_PER_METER})";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -281,7 +174,7 @@
|
||||||
|
|
||||||
// Điều chỉnh kích thước dựa trên ZoomScale
|
// Đ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
|
// Tăng kích thước lên 1.5x để robot to hơn
|
||||||
double scaleFactor = 2 / ZoomScale; // Tăng kích thước hiển thị
|
double scaleFactor = 3 / ZoomScale; // Tăng kích thước hiển thị
|
||||||
|
|
||||||
double width = RobotWidthMeters * scaleFactor;
|
double width = RobotWidthMeters * scaleFactor;
|
||||||
double height = RobotLengthMeters * scaleFactor;
|
double height = RobotLengthMeters * scaleFactor;
|
||||||
|
|
@ -324,53 +217,6 @@
|
||||||
return -worldY;
|
return -worldY;
|
||||||
}
|
}
|
||||||
|
|
||||||
private string GetOriginMarkerTransform()
|
|
||||||
{
|
|
||||||
// Origin is at (MapImageOriginX, MapImageOriginY) in world coordinates (bottom-left corner of image)
|
|
||||||
// In SVG coordinates (after Y flip): (MapImageOriginX, -MapImageOriginY)
|
|
||||||
// Note: Image is rendered at (MapImageOriginX, -MapImageOriginY - MapImageHeight) in SVG
|
|
||||||
// So origin marker should be at (MapImageOriginX, -MapImageOriginY) in SVG
|
|
||||||
var x = WorldToSvgX(0);
|
|
||||||
var y = WorldToSvgY(0);
|
|
||||||
|
|
||||||
return $"translate({x}, {y})";
|
|
||||||
}
|
|
||||||
|
|
||||||
private double GetOriginMarkerSize()
|
|
||||||
{
|
|
||||||
// Marker size in world coordinates (meters)
|
|
||||||
const double BaseMarkerSize = 1; // 1 meter
|
|
||||||
double scaleFactor = 1.0 / ZoomScale; // Keep visual size constant
|
|
||||||
return BaseMarkerSize * scaleFactor;
|
|
||||||
}
|
|
||||||
|
|
||||||
private double GetOriginMarkerStrokeWidth()
|
|
||||||
{
|
|
||||||
// Stroke width in world coordinates
|
|
||||||
const double BaseStrokeWidth = 0.05; // 5cm
|
|
||||||
double scaleFactor = 1.0 / ZoomScale; // Keep visual size constant
|
|
||||||
return BaseStrokeWidth * scaleFactor;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void OnMonitorDataUpdated()
|
|
||||||
{
|
|
||||||
// Auto-follow robot when MonitorData changes
|
|
||||||
if (AutoFollowRobot && !IsPanning && MonitorData?.RobotPosition != null)
|
|
||||||
{
|
|
||||||
UpdateViewToFollowRobot();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void UpdateViewToFollowRobot()
|
|
||||||
{
|
|
||||||
if (MonitorData?.RobotPosition == null) return;
|
|
||||||
|
|
||||||
// Center view on robot
|
|
||||||
TranslateX = SvgWidth / 2 - MonitorData.RobotPosition.X * BASE_PIXELS_PER_METER * ZoomScale;
|
|
||||||
TranslateY = SvgHeight / 2 + MonitorData.RobotPosition.Y * BASE_PIXELS_PER_METER * ZoomScale;
|
|
||||||
StateHasChanged();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void UpdatePath()
|
public void UpdatePath()
|
||||||
{
|
{
|
||||||
if (MonitorData is not null && MonitorData.EdgeStates.Length > 0)
|
if (MonitorData is not null && MonitorData.EdgeStates.Length > 0)
|
||||||
|
|
@ -445,27 +291,15 @@
|
||||||
PanStartY = e.ClientY - TranslateY;
|
PanStartY = e.ClientY - TranslateY;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task HandleMouseMove(MouseEventArgs e)
|
private void HandleMouseMove(MouseEventArgs e)
|
||||||
{
|
{
|
||||||
// Calculate world coordinates of mouse
|
|
||||||
var svgRect = await JS.InvokeAsync<ElementBoundingRect>("robotMonitor.getElementBoundingRect", SvgRef);
|
|
||||||
double mouseX = e.ClientX - svgRect.X;
|
|
||||||
double mouseY = e.ClientY - svgRect.Y;
|
|
||||||
|
|
||||||
// Convert to world coordinates
|
|
||||||
// World X = (mouseX - TranslateX) / (ZoomScale * BASE_PIXELS_PER_METER)
|
|
||||||
MouseWorldX = (mouseX - TranslateX) / (ZoomScale * BASE_PIXELS_PER_METER);
|
|
||||||
// World Y = -(mouseY - TranslateY) / (ZoomScale * BASE_PIXELS_PER_METER) (flip Y axis)
|
|
||||||
MouseWorldY = -(mouseY - TranslateY) / (ZoomScale * BASE_PIXELS_PER_METER);
|
|
||||||
|
|
||||||
if (IsPanning)
|
if (IsPanning)
|
||||||
{
|
{
|
||||||
TranslateX = e.ClientX - PanStartX;
|
TranslateX = e.ClientX - PanStartX;
|
||||||
TranslateY = e.ClientY - PanStartY;
|
TranslateY = e.ClientY - PanStartY;
|
||||||
}
|
|
||||||
|
|
||||||
StateHasChanged();
|
StateHasChanged();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void HandleMouseUp(MouseEventArgs e)
|
private void HandleMouseUp(MouseEventArgs e)
|
||||||
{
|
{
|
||||||
|
|
@ -475,9 +309,6 @@
|
||||||
private void HandleMouseLeave(MouseEventArgs e)
|
private void HandleMouseLeave(MouseEventArgs e)
|
||||||
{
|
{
|
||||||
IsPanning = false;
|
IsPanning = false;
|
||||||
MouseWorldX = null;
|
|
||||||
MouseWorldY = null;
|
|
||||||
StateHasChanged();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task HandleWheel(WheelEventArgs e)
|
private async Task HandleWheel(WheelEventArgs e)
|
||||||
|
|
@ -493,7 +324,7 @@
|
||||||
if (Math.Abs(ZoomScale - oldZoom) < 0.001) return;
|
if (Math.Abs(ZoomScale - oldZoom) < 0.001) return;
|
||||||
|
|
||||||
// Zoom at mouse position
|
// Zoom at mouse position
|
||||||
var svgRect = await JS.InvokeAsync<ElementBoundingRect>("robotMonitor.getElementBoundingRect", SvgRef);
|
var svgRect = await JS.InvokeAsync<ElementBoundingRect>("getElementBoundingRect", SvgRef);
|
||||||
double mouseX = e.ClientX - svgRect.X;
|
double mouseX = e.ClientX - svgRect.X;
|
||||||
double mouseY = e.ClientY - svgRect.Y;
|
double mouseY = e.ClientY - svgRect.Y;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -54,39 +54,19 @@
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mouse-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;
|
|
||||||
}
|
|
||||||
|
|
||||||
.auto-follow-control {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
padding: 4px 8px;
|
|
||||||
background-color: #3d3d3d;
|
|
||||||
border-radius: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.svg-container {
|
.svg-container {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
position: relative;
|
position: relative;
|
||||||
background-color: #808080;
|
background-color: #fafafa;
|
||||||
}
|
}
|
||||||
|
|
||||||
.svg-container svg {
|
.svg-container svg {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
cursor: grab;
|
cursor: grab;
|
||||||
background-color: #808080;
|
background-color: dimgray;
|
||||||
}
|
}
|
||||||
|
|
||||||
.svg-container svg:active {
|
.svg-container svg:active {
|
||||||
cursor: grabbing;
|
cursor: grabbing;
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@
|
||||||
<!-- Header Dashboard -->
|
<!-- Header Dashboard -->
|
||||||
<MudPaper Class="pa-6 mb-4 d-flex align-center justify-space-between" Elevation="3">
|
<MudPaper Class="pa-6 mb-4 d-flex align-center justify-space-between" Elevation="3">
|
||||||
<div>
|
<div>
|
||||||
<MudText Typo="Typo.h3" Class="mb-2">Robot Dashboard</MudText>
|
<MudText Typo="Typo.h3" Class="mb-2" >Robot Dashboard</MudText>
|
||||||
@if (CurrentState != null)
|
@if (CurrentState != null)
|
||||||
{
|
{
|
||||||
<MudText Typo="Typo.subtitle1">
|
<MudText Typo="Typo.subtitle1">
|
||||||
|
|
@ -37,7 +37,9 @@
|
||||||
@if (CurrentState != null)
|
@if (CurrentState != null)
|
||||||
{
|
{
|
||||||
<MudChip T="string"
|
<MudChip T="string"
|
||||||
Icon="@(IsConnected ? Icons.Material.Filled.CheckCircle : Icons.Material.Filled.Error)"
|
Icon="@(IsConnected
|
||||||
|
? Icons.Material.Filled.CheckCircle
|
||||||
|
: Icons.Material.Filled.Error)"
|
||||||
Size="Size.Large"
|
Size="Size.Large"
|
||||||
Color="@(IsConnected ? Color.Success : Color.Error)"
|
Color="@(IsConnected ? Color.Success : Color.Error)"
|
||||||
Variant="Variant.Filled"
|
Variant="Variant.Filled"
|
||||||
|
|
@ -270,7 +272,7 @@
|
||||||
</MudChip>
|
</MudChip>
|
||||||
</MudTd>
|
</MudTd>
|
||||||
<MudTd>
|
<MudTd>
|
||||||
<MudText Typo="Typo.body2" Class="text-truncate" Style="max-width: 300px;">
|
<MudText Typo="Typo.body2" Class="text-truncate" Style="max-width: 300px;" Title="@context.Description">
|
||||||
@context.Description
|
@context.Description
|
||||||
</MudText>
|
</MudText>
|
||||||
</MudTd>
|
</MudTd>
|
||||||
|
|
@ -309,7 +311,7 @@
|
||||||
<MudTh Style="text-align:right">Status</MudTh>
|
<MudTh Style="text-align:right">Status</MudTh>
|
||||||
</HeaderContent>
|
</HeaderContent>
|
||||||
<RowTemplate>
|
<RowTemplate>
|
||||||
<MudTd><MudText Typo="Typo.body2" Class="text-truncate">@context.ActionType</MudText></MudTd>
|
<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><MudText Typo="Typo.caption">@context.ActionId</MudText></MudTd>
|
||||||
<MudTd Style="text-align:right">
|
<MudTd Style="text-align:right">
|
||||||
<MudChip T="string"
|
<MudChip T="string"
|
||||||
|
|
@ -371,34 +373,45 @@
|
||||||
|
|
||||||
private StateMsg? CurrentState;
|
private StateMsg? CurrentState;
|
||||||
private bool IsConnected;
|
private bool IsConnected;
|
||||||
|
private readonly string RobotSerial = "T800-002";
|
||||||
private List<MessageRow> MessageRows = new();
|
private List<MessageRow> MessageRows = new();
|
||||||
|
|
||||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
protected override async Task OnInitializedAsync()
|
||||||
{
|
{
|
||||||
await base.OnAfterRenderAsync(firstRender);
|
|
||||||
if (!firstRender) return;
|
|
||||||
RobotStateClient.OnStateReceived += OnRobotStateReceived;
|
RobotStateClient.OnStateReceived += OnRobotStateReceived;
|
||||||
RobotStateClient.OnRobotConnectionChanged += OnRobotConnectionChanged;
|
RobotStateClient.OnRobotConnectionChanged += OnRobotConnectionChanged;
|
||||||
|
|
||||||
|
if (RobotStateClient.ConnectionState == RobotClientState.Disconnected)
|
||||||
|
{
|
||||||
await RobotStateClient.StartAsync();
|
await RobotStateClient.StartAsync();
|
||||||
CurrentState = RobotStateClient.GetLatestState();
|
}
|
||||||
|
|
||||||
|
await RobotStateClient.SubscribeRobotAsync(RobotSerial);
|
||||||
|
|
||||||
|
CurrentState = RobotStateClient.GetLatestState(RobotSerial);
|
||||||
IsConnected = RobotStateClient.IsRobotConnected;
|
IsConnected = RobotStateClient.IsRobotConnected;
|
||||||
|
|
||||||
UpdateMessageRows();
|
UpdateMessageRows();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnRobotConnectionChanged(bool connected)
|
private void OnRobotConnectionChanged(bool connected)
|
||||||
|
{
|
||||||
|
InvokeAsync(() =>
|
||||||
{
|
{
|
||||||
IsConnected = connected;
|
IsConnected = connected;
|
||||||
StateHasChanged();
|
StateHasChanged();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private void OnRobotStateReceived(string serialNumber, StateMsg state)
|
private void OnRobotStateReceived(string serialNumber, StateMsg state)
|
||||||
|
{
|
||||||
|
if (serialNumber != RobotSerial) return;
|
||||||
|
InvokeAsync(() =>
|
||||||
{
|
{
|
||||||
CurrentState = state;
|
CurrentState = state;
|
||||||
UpdateMessageRows();
|
UpdateMessageRows();
|
||||||
StateHasChanged();
|
StateHasChanged();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private void UpdateMessageRows()
|
private void UpdateMessageRows()
|
||||||
|
|
|
||||||
|
|
@ -38,7 +38,8 @@
|
||||||
<MudIconButton Icon="@Icons.Material.Filled.Delete"
|
<MudIconButton Icon="@Icons.Material.Filled.Delete"
|
||||||
Color="Color.Error"
|
Color="Color.Error"
|
||||||
Size="Size.Small"
|
Size="Size.Small"
|
||||||
OnClick="@(() => RemoveEdgeAsync(edge))" />
|
OnClick="@(() => RemoveEdgeAsync(edge))"
|
||||||
|
StopPropagation="true" />
|
||||||
</div>
|
</div>
|
||||||
</TitleContent>
|
</TitleContent>
|
||||||
|
|
||||||
|
|
@ -87,6 +88,47 @@
|
||||||
}
|
}
|
||||||
</MudSelect>
|
</MudSelect>
|
||||||
</MudItem>
|
</MudItem>
|
||||||
|
|
||||||
|
@* <!-- Radius -->
|
||||||
|
<MudItem xs="6">
|
||||||
|
<MudNumericField T="double"
|
||||||
|
Value="@edge.Radius"
|
||||||
|
ValueChanged="@((double v) => SetValue(() => edge.Radius = v))"
|
||||||
|
Immediate="true"
|
||||||
|
Min="0"
|
||||||
|
Step="0.1"
|
||||||
|
Label="Radius (0 = straight line)" />
|
||||||
|
</MudItem>
|
||||||
|
|
||||||
|
<!-- Quadrant -->
|
||||||
|
@if (edge.Radius > 0)
|
||||||
|
{
|
||||||
|
<MudItem xs="6">
|
||||||
|
<MudSelect T="Quadrant"
|
||||||
|
Value="@edge.Quadrant"
|
||||||
|
ValueChanged="@((Quadrant v) => SetValue(() => edge.Quadrant = v))"
|
||||||
|
Label="Quadrant">
|
||||||
|
<MudSelectItem Value="Quadrant.I">I</MudSelectItem>
|
||||||
|
<MudSelectItem Value="Quadrant.II">II</MudSelectItem>
|
||||||
|
<MudSelectItem Value="Quadrant.III">III</MudSelectItem>
|
||||||
|
<MudSelectItem Value="Quadrant.IV">IV</MudSelectItem>
|
||||||
|
</MudSelect>
|
||||||
|
</MudItem>
|
||||||
|
}
|
||||||
|
|
||||||
|
<!-- Apply Curve -->
|
||||||
|
@if (!edge.HasTrajectory && edge.Radius > 0 && !edge.Expanded)
|
||||||
|
{
|
||||||
|
<MudItem xs="12">
|
||||||
|
<MudButton Color="Color.Primary"
|
||||||
|
Variant="Variant.Outlined"
|
||||||
|
StartIcon="@Icons.Material.Filled.Merge"
|
||||||
|
OnClick="@(() => ApplyCurveAsync(edge))">
|
||||||
|
Apply Curve (generate node)
|
||||||
|
</MudButton>
|
||||||
|
</MudItem>
|
||||||
|
} *@
|
||||||
|
|
||||||
</MudGrid>
|
</MudGrid>
|
||||||
</ChildContent>
|
</ChildContent>
|
||||||
</MudExpansionPanel>
|
</MudExpansionPanel>
|
||||||
|
|
@ -99,7 +141,8 @@
|
||||||
[Parameter] public OrderMessage Order { get; set; } = default!;
|
[Parameter] public OrderMessage Order { get; set; } = default!;
|
||||||
|
|
||||||
[Parameter] public EventCallback OnAddEdge { get; set; }
|
[Parameter] public EventCallback OnAddEdge { get; set; }
|
||||||
[Parameter] public EventCallback<VDA5050.Order.Edge> OnRemoveEdge { get; set; }
|
[Parameter] public EventCallback<UiEdge> OnRemoveEdge { get; set; }
|
||||||
|
[Parameter] public EventCallback<UiEdge> OnApplyCurve { get; set; }
|
||||||
|
|
||||||
[Parameter] public EventCallback OnOrderChanged { get; set; }
|
[Parameter] public EventCallback OnOrderChanged { get; set; }
|
||||||
|
|
||||||
|
|
@ -115,9 +158,15 @@
|
||||||
await OnOrderChanged.InvokeAsync();
|
await OnOrderChanged.InvokeAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task RemoveEdgeAsync(VDA5050.Order.Edge edge)
|
private async Task RemoveEdgeAsync(UiEdge edge)
|
||||||
{
|
{
|
||||||
await OnRemoveEdge.InvokeAsync(edge);
|
await OnRemoveEdge.InvokeAsync(edge);
|
||||||
await OnOrderChanged.InvokeAsync();
|
await OnOrderChanged.InvokeAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task ApplyCurveAsync(UiEdge edge)
|
||||||
|
{
|
||||||
|
await OnApplyCurve.InvokeAsync(edge);
|
||||||
|
await OnOrderChanged.InvokeAsync();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -132,17 +132,17 @@
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var order = JsonSerializer.Deserialize<OrderMsg>(JsonText, new JsonSerializerOptions
|
using var doc = JsonDocument.Parse(JsonText);
|
||||||
|
var root = doc.RootElement;
|
||||||
|
|
||||||
|
// ===== BASIC STRUCTURE CHECK =====
|
||||||
|
if (!root.TryGetProperty("nodes", out _) ||
|
||||||
|
!root.TryGetProperty("edges", out _))
|
||||||
{
|
{
|
||||||
WriteIndented = true,
|
throw new Exception("Missing 'nodes' or 'edges' field.");
|
||||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
|
||||||
});
|
|
||||||
if(order is null)
|
|
||||||
{
|
|
||||||
ShowWarning = true;
|
|
||||||
ErrorMessage = "Can not convert file to Order message";
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var order = OrderMessage.FromSchemaObject(root);
|
||||||
ValidateOrder(order);
|
ValidateOrder(order);
|
||||||
|
|
||||||
MudDialog.Close(DialogResult.Ok(order));
|
MudDialog.Close(DialogResult.Ok(order));
|
||||||
|
|
@ -160,15 +160,15 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
// ================= DOMAIN VALIDATION =================
|
// ================= DOMAIN VALIDATION =================
|
||||||
private void ValidateOrder(OrderMsg order)
|
private void ValidateOrder(OrderMessage order)
|
||||||
{
|
{
|
||||||
if (order.Nodes.Length == 0)
|
if (order.Nodes.Count == 0)
|
||||||
throw new Exception("Order must contain at least one node.");
|
throw new Exception("Order must contain at least one node.");
|
||||||
|
|
||||||
if (order.Nodes.Length != order.Edges.Length + 1)
|
if (order.Nodes.Count != order.Edges.Count + 1)
|
||||||
throw new Exception(
|
throw new Exception(
|
||||||
$"Invalid path structure: Nodes count ({order.Nodes.Length}) " +
|
$"Invalid path structure: Nodes count ({order.Nodes.Count}) " +
|
||||||
$"must equal Edges count + 1 ({order.Edges.Length + 1})."
|
$"must equal Edges count + 1 ({order.Edges.Count + 1})."
|
||||||
);
|
);
|
||||||
|
|
||||||
var nodeIds = order.Nodes
|
var nodeIds = order.Nodes
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
<MudPaper Class="pa-4 h-100 d-flex flex-column overflow-hidden" Elevation="2">
|
<MudPaper Class="pa-4 h-100 d-flex flex-column overflow-hidden" Elevation="2">
|
||||||
<MudStack Row AlignItems="AlignItems.Center" Justify="Justify.SpaceBetween"
|
<MudStack Row AlignItems="AlignItems.Center" Justify="Justify.SpaceBetween"
|
||||||
Class="mb-4 flex-shrink-0">
|
Class="mb-4 flex-shrink-0">
|
||||||
<MudText Typo="Typo.h6">Output (/order)</MudText>
|
<MudText Typo="Typo.h6">📄 JSON Output (/order)</MudText>
|
||||||
|
|
||||||
<div class="d-flex gap-2">
|
<div class="d-flex gap-2">
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -21,11 +21,13 @@
|
||||||
<MudIconButton Icon="@Icons.Material.Filled.Edit"
|
<MudIconButton Icon="@Icons.Material.Filled.Edit"
|
||||||
Color="Color.Primary"
|
Color="Color.Primary"
|
||||||
Size="Size.Small"
|
Size="Size.Small"
|
||||||
OnClick="@(() => EditNodeAsync(node))" />
|
OnClick="@(() => EditNodeAsync(node))"
|
||||||
|
StopPropagation />
|
||||||
<MudIconButton Icon="@Icons.Material.Filled.Delete"
|
<MudIconButton Icon="@Icons.Material.Filled.Delete"
|
||||||
Color="Color.Error"
|
Color="Color.Error"
|
||||||
Size="Size.Small"
|
Size="Size.Small"
|
||||||
OnClick="@(() => RemoveNodeAsync(node))" />
|
OnClick="@(() => RemoveNodeAsync(node))"
|
||||||
|
StopPropagation />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</TitleContent>
|
</TitleContent>
|
||||||
|
|
@ -76,22 +78,22 @@
|
||||||
Label="Y" />
|
Label="Y" />
|
||||||
</MudItem>
|
</MudItem>
|
||||||
|
|
||||||
@* <MudItem xs="12">
|
<MudItem xs="12">
|
||||||
<MudTextField Value="@node.NodePosition.MapId"
|
<MudTextField Value="@node.NodePosition.MapId"
|
||||||
ValueChanged="@((string v) => SetValue(() => node.NodePosition.MapId = v))"
|
ValueChanged="@((string v) => SetValue(() => node.NodePosition.MapId = v))"
|
||||||
Immediate="true"
|
Immediate="true"
|
||||||
Label="Map ID" />
|
Label="Map ID" />
|
||||||
</MudItem> *@
|
</MudItem>
|
||||||
|
|
||||||
@* <MudItem xs="6">
|
<MudItem xs="6">
|
||||||
<MudNumericField T="double"
|
<MudNumericField T="double"
|
||||||
Value="@node.NodePosition.Theta"
|
Value="@node.NodePosition.Theta"
|
||||||
ValueChanged="@((double v) => SetValue(() => node.NodePosition.Theta = v))"
|
ValueChanged="@((double v) => SetValue(() => node.NodePosition.Theta = v))"
|
||||||
Immediate="true"
|
Immediate="true"
|
||||||
Label="Theta (rad)" />
|
Label="Theta (rad)" />
|
||||||
</MudItem> *@
|
</MudItem>
|
||||||
|
|
||||||
@* <MudItem xs="6">
|
<MudItem xs="6">
|
||||||
<MudNumericField T="double"
|
<MudNumericField T="double"
|
||||||
Value="@node.NodePosition.AllowedDeviationXY"
|
Value="@node.NodePosition.AllowedDeviationXY"
|
||||||
ValueChanged="@((double v) => SetValue(() => node.NodePosition.AllowedDeviationXY = v))"
|
ValueChanged="@((double v) => SetValue(() => node.NodePosition.AllowedDeviationXY = v))"
|
||||||
|
|
@ -105,7 +107,7 @@
|
||||||
ValueChanged="@((double v) => SetValue(() => node.NodePosition.AllowedDeviationTheta = v))"
|
ValueChanged="@((double v) => SetValue(() => node.NodePosition.AllowedDeviationTheta = v))"
|
||||||
Immediate="true"
|
Immediate="true"
|
||||||
Label="Allowed Dev Theta" />
|
Label="Allowed Dev Theta" />
|
||||||
</MudItem> *@
|
</MudItem>
|
||||||
|
|
||||||
<!-- Actions -->
|
<!-- Actions -->
|
||||||
<MudItem xs="12">
|
<MudItem xs="12">
|
||||||
|
|
|
||||||
|
|
@ -37,6 +37,7 @@
|
||||||
<EdgesPanel Order="Order"
|
<EdgesPanel Order="Order"
|
||||||
OnAddEdge="AddEdge"
|
OnAddEdge="AddEdge"
|
||||||
OnRemoveEdge="RemoveEdge"
|
OnRemoveEdge="RemoveEdge"
|
||||||
|
OnApplyCurve="ApplyCurve"
|
||||||
OnOrderChanged="OnOrderChanged" />
|
OnOrderChanged="OnOrderChanged" />
|
||||||
</MudItem>
|
</MudItem>
|
||||||
|
|
||||||
|
|
@ -83,10 +84,9 @@
|
||||||
new JsonSerializerOptions
|
new JsonSerializerOptions
|
||||||
{
|
{
|
||||||
WriteIndented = true,
|
WriteIndented = true,
|
||||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task OpenImportDialog()
|
private async Task OpenImportDialog()
|
||||||
{
|
{
|
||||||
var dialog = await DialogService.ShowAsync<ImportOrderDialog>(
|
var dialog = await DialogService.ShowAsync<ImportOrderDialog>(
|
||||||
|
|
@ -99,9 +99,9 @@
|
||||||
|
|
||||||
var result = await dialog.Result;
|
var result = await dialog.Result;
|
||||||
|
|
||||||
if (result is not null && !result.Canceled && result.Data is OrderMsg imported)
|
if (!result.Canceled && result.Data is OrderMessage imported)
|
||||||
{
|
{
|
||||||
Order.Import(imported);
|
Order = imported;
|
||||||
RebuildOrderJson();
|
RebuildOrderJson();
|
||||||
StateHasChanged();
|
StateHasChanged();
|
||||||
}
|
}
|
||||||
|
|
@ -149,7 +149,7 @@
|
||||||
? Order.Nodes[1].NodeId
|
? Order.Nodes[1].NodeId
|
||||||
: start; // 👈 1 node thì start = end
|
: start; // 👈 1 node thì start = end
|
||||||
|
|
||||||
Order.Edges.Add(new VDA5050.Order.Edge
|
Order.Edges.Add(new UiEdge
|
||||||
{
|
{
|
||||||
EdgeId = $"EDGE_{Order.Edges.Count + 1}",
|
EdgeId = $"EDGE_{Order.Edges.Count + 1}",
|
||||||
StartNodeId = start,
|
StartNodeId = start,
|
||||||
|
|
@ -158,10 +158,25 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
void RemoveEdge(VDA5050.Order.Edge edge)
|
void RemoveEdge(UiEdge edge)
|
||||||
{
|
{
|
||||||
Order.Edges.Remove(edge);
|
Order.Edges.Remove(edge);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void ApplyCurve(UiEdge edge)
|
||||||
|
{
|
||||||
|
if (edge.Radius <= 0 || edge.Expanded) return;
|
||||||
|
|
||||||
|
var startNode = Order.Nodes.First(n => n.NodeId == edge.StartNodeId);
|
||||||
|
var newNode = OrderMessage.CreateCurveNode(startNode, edge);
|
||||||
|
|
||||||
|
Order.Nodes.Add(newNode);
|
||||||
|
edge.EndNodeId = newNode.NodeId;
|
||||||
|
edge.MarkExpanded(); // ✅
|
||||||
|
|
||||||
|
ResequenceNodes();
|
||||||
|
}
|
||||||
|
|
||||||
// ================= ACTION =================
|
// ================= ACTION =================
|
||||||
void AddAction(Node node)
|
void AddAction(Node node)
|
||||||
{
|
{
|
||||||
|
|
@ -178,19 +193,22 @@
|
||||||
|
|
||||||
void RemoveAction(Node node, VDA5050.InstantAction.Action action)
|
void RemoveAction(Node node, VDA5050.InstantAction.Action action)
|
||||||
{
|
{
|
||||||
node.Actions = node.Actions?.Where(a => a != action).ToArray() ?? [];
|
node.Actions = node.Actions?.Where(a => a != action).ToArray()
|
||||||
|
?? Array.Empty<VDA5050.InstantAction.Action>();
|
||||||
}
|
}
|
||||||
|
|
||||||
void AddActionParameter(VDA5050.InstantAction.Action act)
|
void AddActionParameter(VDA5050.InstantAction.Action act)
|
||||||
{
|
{
|
||||||
var list = (act.ActionParameters ?? []).ToList();
|
var list = (act.ActionParameters ?? Array.Empty<ActionParameter>()).ToList();
|
||||||
list.Add(new UiActionParameter());
|
list.Add(new UiActionParameter());
|
||||||
act.ActionParameters = list.ToArray();
|
act.ActionParameters = list.ToArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
void RemoveActionParameter(VDA5050.InstantAction.Action act, ActionParameter param)
|
void RemoveActionParameter(VDA5050.InstantAction.Action act, ActionParameter param)
|
||||||
{
|
{
|
||||||
act.ActionParameters = act.ActionParameters?.Where(p => p != param).ToArray() ?? [];
|
act.ActionParameters =
|
||||||
|
act.ActionParameters?.Where(p => p != param).ToArray()
|
||||||
|
?? Array.Empty<ActionParameter>();
|
||||||
}
|
}
|
||||||
|
|
||||||
// ================= SEND / COPY =================
|
// ================= SEND / COPY =================
|
||||||
|
|
@ -202,12 +220,7 @@
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var orderMsg = JsonSerializer.Deserialize<OrderMsg>(OrderJson,
|
var orderMsg = JsonSerializer.Deserialize<OrderMsg>(OrderJson);
|
||||||
new JsonSerializerOptions
|
|
||||||
{
|
|
||||||
WriteIndented = true,
|
|
||||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
|
||||||
});
|
|
||||||
if (orderMsg is null)
|
if (orderMsg is null)
|
||||||
{
|
{
|
||||||
Snackbar.Add("Unable to convert JSON to Order", Severity.Warning);
|
Snackbar.Add("Unable to convert JSON to Order", Severity.Warning);
|
||||||
|
|
@ -238,17 +251,20 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var response = await Http.PostAsJsonAsync("/api/order",orderMsg);
|
var response = await Http.PostAsJsonAsync(
|
||||||
|
"/api/order",
|
||||||
|
JsonSerializer.Deserialize<OrderMsg>(OrderJson)
|
||||||
|
);
|
||||||
|
|
||||||
sendSuccess = response.IsSuccessStatusCode;
|
sendSuccess = response.IsSuccessStatusCode;
|
||||||
|
|
||||||
}
|
}
|
||||||
catch(JsonException jsonEx)
|
catch(JsonException jsonEx)
|
||||||
{
|
{
|
||||||
Snackbar.Add($"Json to Order failed: {jsonEx.Message}", Severity.Warning);
|
Snackbar.Add($"Json to Order failed: {jsonEx.Message}", Severity.Warning);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch
|
||||||
{
|
{
|
||||||
Snackbar.Add($"Send Order failed: {ex.Message}", Severity.Warning);
|
|
||||||
sendSuccess = false;
|
sendSuccess = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -300,7 +316,7 @@
|
||||||
_copyCts?.Cancel();
|
_copyCts?.Cancel();
|
||||||
_copyCts = new();
|
_copyCts = new();
|
||||||
|
|
||||||
await JS.InvokeVoidAsync("copyToClipboardFallback", OrderJson);
|
await JS.InvokeVoidAsync("navigator.clipboard.writeText", OrderJson);
|
||||||
|
|
||||||
copied = true;
|
copied = true;
|
||||||
StateHasChanged();
|
StateHasChanged();
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,6 @@
|
||||||
|
|
||||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||||
{
|
{
|
||||||
await base.OnAfterRenderAsync(firstRender);
|
|
||||||
if (firstRender)
|
if (firstRender)
|
||||||
{
|
{
|
||||||
MonitorService.OnDataReceived += OnMonitorDataReceived;
|
MonitorService.OnDataReceived += OnMonitorDataReceived;
|
||||||
|
|
@ -29,8 +28,7 @@
|
||||||
{
|
{
|
||||||
_monitorData = data;
|
_monitorData = data;
|
||||||
RobotMonitorViewRef?.UpdatePath();
|
RobotMonitorViewRef?.UpdatePath();
|
||||||
RobotMonitorViewRef?.OnMonitorDataUpdated();
|
InvokeAsync(StateHasChanged);
|
||||||
StateHasChanged();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async ValueTask DisposeAsync()
|
public async ValueTask DisposeAsync()
|
||||||
|
|
@ -43,9 +41,3 @@
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -107,7 +107,7 @@ public sealed class RobotStateClient : IAsyncDisposable
|
||||||
// ================= SIGNALR HANDLERS =================
|
// ================= SIGNALR HANDLERS =================
|
||||||
|
|
||||||
// VDA5050 State
|
// VDA5050 State
|
||||||
_connection.On<StateMsg>("ReceiveState", HandleState);
|
_connection.On<string>("ReceiveState", HandleState);
|
||||||
|
|
||||||
// Robot connection (bool only)
|
// Robot connection (bool only)
|
||||||
_connection.On<bool>("ReceiveRobotConnection", HandleRobotConnection);
|
_connection.On<bool>("ReceiveRobotConnection", HandleRobotConnection);
|
||||||
|
|
@ -125,8 +125,22 @@ public sealed class RobotStateClient : IAsyncDisposable
|
||||||
}
|
}
|
||||||
|
|
||||||
// ================= HANDLE STATE =================
|
// ================= HANDLE STATE =================
|
||||||
private void HandleState(StateMsg state)
|
private void HandleState(string stateJson)
|
||||||
{
|
{
|
||||||
|
StateMsg? state;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
state = JsonSerializer.Deserialize<StateMsg>(
|
||||||
|
stateJson,
|
||||||
|
JsonOptionExtends.Read
|
||||||
|
);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (state?.SerialNumber == null)
|
if (state?.SerialNumber == null)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
|
@ -175,10 +189,10 @@ public sealed class RobotStateClient : IAsyncDisposable
|
||||||
}
|
}
|
||||||
|
|
||||||
// ================= GET CACHE =================
|
// ================= GET CACHE =================
|
||||||
public StateMsg? GetLatestState()
|
public StateMsg? GetLatestState(string serialNumber)
|
||||||
{
|
{
|
||||||
if (!LatestStates.IsEmpty) return LatestStates.First().Value;
|
LatestStates.TryGetValue(serialNumber, out var state);
|
||||||
return null;
|
return state;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ================= DISPOSE =================
|
// ================= DISPOSE =================
|
||||||
|
|
|
||||||
|
|
@ -1,33 +1,419 @@
|
||||||
using RobotApp.VDA5050.InstantAction;
|
using RobotApp.VDA5050.InstantAction;
|
||||||
using RobotApp.VDA5050.Order;
|
using RobotApp.VDA5050.Order;
|
||||||
|
using System.Text.Json;
|
||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
namespace RobotApp.Client.Services;
|
namespace RobotApp.Client.Services;
|
||||||
|
|
||||||
|
// ======================================================
|
||||||
|
// EDGE UI
|
||||||
|
// ======================================================
|
||||||
|
public class UiEdge
|
||||||
|
{
|
||||||
|
public string EdgeId { get; set; } = "";
|
||||||
|
public int SequenceId { get; set; }
|
||||||
|
public bool Released { get; set; } = true;
|
||||||
|
|
||||||
|
public string StartNodeId { get; set; } = "";
|
||||||
|
public string EndNodeId { get; set; } = "";
|
||||||
|
|
||||||
|
// ===== CURVE (EDITOR GENERATED) =====
|
||||||
|
public double Radius { get; set; } = 0;
|
||||||
|
public Quadrant Quadrant { get; set; }
|
||||||
|
|
||||||
|
// ===== IMPORTED TRAJECTORY =====
|
||||||
|
public bool HasTrajectory { get; set; } = false;
|
||||||
|
public UiTrajectory? Trajectory { get; set; }
|
||||||
|
|
||||||
|
// ===== UI STATE =====
|
||||||
|
public bool Expanded { get; private set; } = false;
|
||||||
|
|
||||||
|
public void MarkExpanded()
|
||||||
|
{
|
||||||
|
Expanded = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class UiTrajectory
|
||||||
|
{
|
||||||
|
public int Degree { get; set; }
|
||||||
|
public double[] KnotVector { get; set; } = Array.Empty<double>();
|
||||||
|
public List<Point> ControlPoints { get; set; } = new();
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum Quadrant
|
||||||
|
{
|
||||||
|
I,
|
||||||
|
II,
|
||||||
|
III,
|
||||||
|
IV
|
||||||
|
}
|
||||||
|
|
||||||
|
// ======================================================
|
||||||
|
// GEOMETRY MODELS
|
||||||
|
// ======================================================
|
||||||
|
public record Point(double X, double Y);
|
||||||
|
|
||||||
|
public record QuarterResult(
|
||||||
|
Point EndPoint,
|
||||||
|
object Trajectory
|
||||||
|
);
|
||||||
|
|
||||||
|
// ======================================================
|
||||||
|
// GEOMETRY HELPER (QUARTER CIRCLE)
|
||||||
|
// ======================================================
|
||||||
|
public static class QuarterGeometry
|
||||||
|
{
|
||||||
|
private const double K = 0.5522847498307936;
|
||||||
|
|
||||||
|
public static QuarterResult BuildQuarterTrajectory(
|
||||||
|
Point A,
|
||||||
|
double r,
|
||||||
|
Quadrant q
|
||||||
|
)
|
||||||
|
{
|
||||||
|
Point P1, P2, C;
|
||||||
|
|
||||||
|
switch (q)
|
||||||
|
{
|
||||||
|
case Quadrant.I:
|
||||||
|
P1 = new(A.X, A.Y + K * r);
|
||||||
|
P2 = new(A.X + K * r, A.Y + r);
|
||||||
|
C = new(A.X + r, A.Y + r);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case Quadrant.II:
|
||||||
|
P1 = new(A.X - K * r, A.Y);
|
||||||
|
P2 = new(A.X - r, A.Y + K * r);
|
||||||
|
C = new(A.X - r, A.Y + r);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case Quadrant.III:
|
||||||
|
P1 = new(A.X, A.Y - K * r);
|
||||||
|
P2 = new(A.X - K * r, A.Y - r);
|
||||||
|
C = new(A.X - r, A.Y - r);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case Quadrant.IV:
|
||||||
|
P1 = new(A.X + K * r, A.Y);
|
||||||
|
P2 = new(A.X + r, A.Y - K * r);
|
||||||
|
C = new(A.X + r, A.Y - r);
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
throw new ArgumentOutOfRangeException(nameof(q));
|
||||||
|
}
|
||||||
|
|
||||||
|
return new QuarterResult(
|
||||||
|
C,
|
||||||
|
new
|
||||||
|
{
|
||||||
|
degree = 3,
|
||||||
|
knotVector = new[] { 0, 0, 0, 0, 1, 1, 1, 1 },
|
||||||
|
controlPoints = new[]
|
||||||
|
{
|
||||||
|
new { x = A.X, y = A.Y }, // P0
|
||||||
|
new { x = P1.X, y = P1.Y }, // P1
|
||||||
|
new { x = P2.X, y = P2.Y }, // P2
|
||||||
|
new { x = C.X, y = C.Y } // P3
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
// ======================================================
|
// ======================================================
|
||||||
// ORDER MESSAGE
|
// ORDER MESSAGE
|
||||||
// ======================================================
|
// ======================================================
|
||||||
public class OrderMessage
|
public class OrderMessage
|
||||||
{
|
{
|
||||||
public uint HeaderId { get; set; }
|
public int HeaderId { get; set; }
|
||||||
public string Timestamp { get; set; } = DateTime.UtcNow.ToString("yyyy-MM-ddTHH:mm:ss.fffZ");
|
public string Timestamp { get; set; } = "";
|
||||||
public string Version { get; set; } = "2.1.0";
|
public string Version { get; set; } = "v1";
|
||||||
public string Manufacturer { get; set; } = "PhenikaaX";
|
public string Manufacturer { get; set; } = "PNKX";
|
||||||
public string SerialNumber { get; set; } = "T800-003";
|
public string SerialNumber { get; set; } = "T800-002";
|
||||||
public string OrderId { get; set; } = "";
|
public string OrderId { get; set; }
|
||||||
public int OrderUpdateId { get; set; }
|
public int OrderUpdateId { get; set; }
|
||||||
public string? ZoneSetId { get; set; }
|
public string? ZoneSetId { get; set; }
|
||||||
|
|
||||||
public List<Node> Nodes { get; set; } = [];
|
public List<Node> Nodes { get; set; } = new();
|
||||||
public List<Edge> Edges { get; set; } = [];
|
public List<UiEdge> Edges { get; set; } = new();
|
||||||
|
public static Node CreateCurveNode(Node startNode, UiEdge edge)
|
||||||
|
{
|
||||||
|
var A = new Point(
|
||||||
|
startNode.NodePosition.X,
|
||||||
|
startNode.NodePosition.Y
|
||||||
|
);
|
||||||
|
|
||||||
|
var result = QuarterGeometry.BuildQuarterTrajectory(
|
||||||
|
A,
|
||||||
|
edge.Radius,
|
||||||
|
edge.Quadrant
|
||||||
|
);
|
||||||
|
|
||||||
|
return new Node
|
||||||
|
{
|
||||||
|
NodeId = $"NODE_C{Guid.NewGuid():N}".Substring(0, 12),
|
||||||
|
Released = true,
|
||||||
|
NodePosition = new NodePosition
|
||||||
|
{
|
||||||
|
X = result.EndPoint.X,
|
||||||
|
Y = result.EndPoint.Y,
|
||||||
|
Theta = startNode.NodePosition.Theta,
|
||||||
|
MapId = startNode.NodePosition.MapId
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
public static OrderMessage FromSchemaObject(JsonElement root)
|
||||||
|
{
|
||||||
|
var order = new OrderMessage
|
||||||
|
{
|
||||||
|
HeaderId = root.GetProperty("headerId").GetInt32(),
|
||||||
|
Timestamp = root.GetProperty("timestamp").GetString(),
|
||||||
|
Version = root.GetProperty("version").GetString(),
|
||||||
|
Manufacturer = root.GetProperty("manufacturer").GetString(),
|
||||||
|
SerialNumber = root.GetProperty("serialNumber").GetString(),
|
||||||
|
OrderId = root.GetProperty("orderId").GetString(),
|
||||||
|
OrderUpdateId = root.GetProperty("orderUpdateId").GetInt32()
|
||||||
|
};
|
||||||
|
|
||||||
|
// ================= NODES =================
|
||||||
|
foreach (var n in root.GetProperty("nodes").EnumerateArray())
|
||||||
|
{
|
||||||
|
var node = new Node
|
||||||
|
{
|
||||||
|
NodeId = n.GetProperty("nodeId").GetString()!,
|
||||||
|
SequenceId = n.GetProperty("sequenceId").GetInt32(),
|
||||||
|
Released = n.GetProperty("released").GetBoolean(),
|
||||||
|
|
||||||
|
NodePosition = new NodePosition
|
||||||
|
{
|
||||||
|
X = n.GetProperty("nodePosition").GetProperty("x").GetDouble(),
|
||||||
|
Y = n.GetProperty("nodePosition").GetProperty("y").GetDouble(),
|
||||||
|
Theta = n.GetProperty("nodePosition").GetProperty("theta").GetDouble(),
|
||||||
|
AllowedDeviationXY = n.GetProperty("nodePosition").GetProperty("allowedDeviationXY").GetDouble(),
|
||||||
|
AllowedDeviationTheta = n.GetProperty("nodePosition").GetProperty("allowedDeviationTheta").GetDouble(),
|
||||||
|
MapId = n.GetProperty("nodePosition").GetProperty("mapId").GetString()
|
||||||
|
},
|
||||||
|
|
||||||
|
Actions = ParseActions(n)
|
||||||
|
};
|
||||||
|
|
||||||
|
order.Nodes.Add(node);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var e in root.GetProperty("edges").EnumerateArray())
|
||||||
|
{
|
||||||
|
var edge = new UiEdge
|
||||||
|
{
|
||||||
|
EdgeId = e.GetProperty("edgeId").GetString()!,
|
||||||
|
SequenceId = e.GetProperty("sequenceId").GetInt32(),
|
||||||
|
Released = e.GetProperty("released").GetBoolean(),
|
||||||
|
StartNodeId = e.GetProperty("startNodeId").GetString()!,
|
||||||
|
EndNodeId = e.GetProperty("endNodeId").GetString()!,
|
||||||
|
};
|
||||||
|
|
||||||
|
// ===== IMPORT TRAJECTORY =====
|
||||||
|
if (e.TryGetProperty("trajectory", out var traj))
|
||||||
|
{
|
||||||
|
edge.HasTrajectory = true;
|
||||||
|
edge.Trajectory = new UiTrajectory
|
||||||
|
{
|
||||||
|
Degree = traj.GetProperty("degree").GetInt32(),
|
||||||
|
KnotVector = traj.GetProperty("knotVector")
|
||||||
|
.EnumerateArray()
|
||||||
|
.Select(x => x.GetDouble())
|
||||||
|
.ToArray(),
|
||||||
|
|
||||||
|
ControlPoints = traj.GetProperty("controlPoints")
|
||||||
|
.EnumerateArray()
|
||||||
|
.Select(p => new Point(
|
||||||
|
p.GetProperty("x").GetDouble(),
|
||||||
|
p.GetProperty("y").GetDouble()
|
||||||
|
))
|
||||||
|
.ToList()
|
||||||
|
};
|
||||||
|
|
||||||
|
// 🔥 IMPORTED CURVE → LOCK APPLY
|
||||||
|
edge.MarkExpanded();
|
||||||
|
}
|
||||||
|
|
||||||
|
order.Edges.Add(edge);
|
||||||
|
}
|
||||||
|
|
||||||
|
return order;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ================= ACTION PARSER =================
|
||||||
|
private static VDA5050.InstantAction.Action[] ParseActions(JsonElement parent)
|
||||||
|
{
|
||||||
|
if (!parent.TryGetProperty("actions", out var acts))
|
||||||
|
return Array.Empty<VDA5050.InstantAction.Action>();
|
||||||
|
|
||||||
|
return acts.EnumerateArray().Select(a =>
|
||||||
|
new VDA5050.InstantAction.Action
|
||||||
|
{
|
||||||
|
ActionId = a.GetProperty("actionId").GetString(),
|
||||||
|
ActionType = a.GetProperty("actionType").GetString(),
|
||||||
|
BlockingType = a.GetProperty("blockingType").GetString(),
|
||||||
|
|
||||||
|
ActionParameters = a.TryGetProperty("actionParameters", out var ps)
|
||||||
|
? ps.EnumerateArray()
|
||||||
|
.Select(p => new ActionParameter
|
||||||
|
{
|
||||||
|
Key = p.GetProperty("key").GetString(),
|
||||||
|
Value = p.GetProperty("value").GetString()
|
||||||
|
})
|
||||||
|
.ToArray()
|
||||||
|
: Array.Empty<ActionParameter>()
|
||||||
|
}
|
||||||
|
).ToArray();
|
||||||
|
}
|
||||||
public OrderMsg ToSchemaObject()
|
public OrderMsg ToSchemaObject()
|
||||||
{
|
{
|
||||||
|
// ================= SORT NODES BY UI SEQUENCE =================
|
||||||
|
var orderedNodes = Nodes
|
||||||
|
.OrderBy(n => n.SequenceId)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
// ================= BUILD NODE OBJECTS =================
|
||||||
|
var nodeObjects = orderedNodes
|
||||||
|
.Select((n, index) => new Node
|
||||||
|
{
|
||||||
|
NodeId = n.NodeId,
|
||||||
|
SequenceId = index * 2, // ✅ NODE = EVEN
|
||||||
|
Released = n.Released,
|
||||||
|
|
||||||
|
NodePosition = new NodePosition
|
||||||
|
{
|
||||||
|
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 VDA5050.InstantAction.Action
|
||||||
|
{
|
||||||
|
ActionId = a.ActionId,
|
||||||
|
ActionType = a.ActionType,
|
||||||
|
BlockingType = a.BlockingType,
|
||||||
|
|
||||||
|
ActionParameters = a.ActionParameters?
|
||||||
|
.Select(p => new ActionParameter
|
||||||
|
{
|
||||||
|
Key = p.Key,
|
||||||
|
Value = p.Value
|
||||||
|
})
|
||||||
|
.ToArray()
|
||||||
|
?? []
|
||||||
|
})
|
||||||
|
.ToArray()
|
||||||
|
?? []
|
||||||
|
})
|
||||||
|
.ToArray();
|
||||||
|
|
||||||
|
// ================= BUILD EDGE OBJECTS =================
|
||||||
|
Edge[] edgeObjects = Edges
|
||||||
|
.Select((e, index) =>
|
||||||
|
{
|
||||||
|
int sequenceId = index * 2 + 1; // ✅ EDGE = ODD
|
||||||
|
|
||||||
|
// ---------- BASE ----------
|
||||||
|
var baseEdge = new
|
||||||
|
{
|
||||||
|
edgeId = e.EdgeId,
|
||||||
|
sequenceId,
|
||||||
|
released = true,
|
||||||
|
startNodeId = e.StartNodeId,
|
||||||
|
endNodeId = e.EndNodeId
|
||||||
|
};
|
||||||
|
|
||||||
|
// =================================================
|
||||||
|
// 1️⃣ IMPORTED TRAJECTORY
|
||||||
|
// =================================================
|
||||||
|
if (e.HasTrajectory && e.Trajectory != null)
|
||||||
|
{
|
||||||
|
return new Edge
|
||||||
|
{
|
||||||
|
EdgeId = baseEdge.edgeId,
|
||||||
|
SequenceId = baseEdge.sequenceId,
|
||||||
|
Released= baseEdge.released,
|
||||||
|
StartNodeId= baseEdge.startNodeId,
|
||||||
|
EndNodeId= baseEdge.endNodeId,
|
||||||
|
|
||||||
|
Trajectory = new Trajectory
|
||||||
|
{
|
||||||
|
Degree = e.Trajectory.Degree,
|
||||||
|
KnotVector = e.Trajectory.KnotVector,
|
||||||
|
ControlPoints = e.Trajectory.ControlPoints
|
||||||
|
.Select(p => new ControlPoint { X = p.X, Y = p.Y , Weight = 1})
|
||||||
|
.ToArray()
|
||||||
|
},
|
||||||
|
|
||||||
|
Actions = []
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// =================================================
|
||||||
|
// 2️⃣ STRAIGHT EDGE
|
||||||
|
// =================================================
|
||||||
|
if (e.Radius <= 0)
|
||||||
|
{
|
||||||
|
return new Edge
|
||||||
|
{
|
||||||
|
EdgeId = baseEdge.edgeId,
|
||||||
|
SequenceId = baseEdge.sequenceId,
|
||||||
|
Released = baseEdge.released,
|
||||||
|
StartNodeId = baseEdge.startNodeId,
|
||||||
|
EndNodeId = baseEdge.endNodeId,
|
||||||
|
|
||||||
|
Actions = []
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// =================================================
|
||||||
|
// 3️⃣ GENERATED CURVE (EDITOR)
|
||||||
|
// =================================================
|
||||||
|
var startNode = orderedNodes.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 Edge
|
||||||
|
{
|
||||||
|
EdgeId = baseEdge.edgeId,
|
||||||
|
SequenceId = baseEdge.sequenceId,
|
||||||
|
Released = baseEdge.released,
|
||||||
|
StartNodeId = baseEdge.startNodeId,
|
||||||
|
EndNodeId = baseEdge.endNodeId,
|
||||||
|
|
||||||
|
Actions = []
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.ToArray();
|
||||||
|
|
||||||
|
// ================= FINAL SCHEMA OBJECT =================
|
||||||
return new OrderMsg
|
return new OrderMsg
|
||||||
{
|
{
|
||||||
HeaderId = (uint)HeaderId++,
|
HeaderId = (uint)HeaderId++,
|
||||||
|
|
||||||
Timestamp = DateTime.UtcNow.ToString("yyyy-MM-ddTHH:mm:ss.fffZ"),
|
Timestamp = string.IsNullOrWhiteSpace(Timestamp)
|
||||||
|
? DateTime.UtcNow.ToString("O")
|
||||||
|
: Timestamp,
|
||||||
|
|
||||||
Version = Version,
|
Version = Version,
|
||||||
Manufacturer = Manufacturer,
|
Manufacturer = Manufacturer,
|
||||||
|
|
@ -40,24 +426,10 @@ public class OrderMessage
|
||||||
? null
|
? null
|
||||||
: ZoneSetId,
|
: ZoneSetId,
|
||||||
|
|
||||||
Nodes = [..Nodes],
|
Nodes = nodeObjects,
|
||||||
Edges = [..Edges],
|
Edges = edgeObjects,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Import(OrderMsg order)
|
|
||||||
{
|
|
||||||
HeaderId = order.HeaderId;
|
|
||||||
Timestamp = order.Timestamp;
|
|
||||||
Version = order.Version;
|
|
||||||
Manufacturer = order.Manufacturer;
|
|
||||||
SerialNumber = order.SerialNumber;
|
|
||||||
OrderId = order.OrderId;
|
|
||||||
ZoneSetId = order.ZoneSetId;
|
|
||||||
OrderUpdateId = order.OrderUpdateId;
|
|
||||||
Nodes = [.. order.Nodes];
|
|
||||||
Edges = [.. order.Edges];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ======================================================
|
// ======================================================
|
||||||
|
|
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 25 KiB |
|
|
@ -1,10 +1 @@
|
||||||
window.copyToClipboardFallback = function (text) {
|
|
||||||
const textarea = document.createElement('textarea');
|
|
||||||
textarea.value = text;
|
|
||||||
textarea.style.position = 'fixed';
|
|
||||||
textarea.style.opacity = '0';
|
|
||||||
document.body.appendChild(textarea);
|
|
||||||
textarea.select();
|
|
||||||
document.execCommand('copy');
|
|
||||||
document.body.removeChild(textarea);
|
|
||||||
}
|
|
||||||
|
|
@ -55,31 +55,8 @@ window.robotMonitor = {
|
||||||
}
|
}
|
||||||
|
|
||||||
return `M ${startX} ${startY} L ${endX} ${endY}`;
|
return `M ${startX} ${startY} L ${endX} ${endY}`;
|
||||||
},
|
|
||||||
|
|
||||||
// Load image and get dimensions
|
|
||||||
loadImageAndGetDimensions: function (imageUrl) {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
const img = new Image();
|
|
||||||
img.onload = () => {
|
|
||||||
resolve({
|
|
||||||
Width: img.naturalWidth || img.width,
|
|
||||||
Height: img.naturalHeight || img.height
|
|
||||||
});
|
|
||||||
};
|
|
||||||
img.onerror = () => {
|
|
||||||
reject(new Error(`Failed to load image: ${imageUrl}`));
|
|
||||||
};
|
|
||||||
img.src = imageUrl;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
namespace RobotApp.VDA5050.Order;
|
namespace RobotApp.VDA5050.Order;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
public class Edge
|
public class Edge
|
||||||
{
|
{
|
||||||
|
|
@ -25,7 +26,7 @@ public class Edge
|
||||||
public bool RotationAllowed { get; set; }
|
public bool RotationAllowed { get; set; }
|
||||||
public double MaxRotationSpeed { get; set; }
|
public double MaxRotationSpeed { get; set; }
|
||||||
public double Length { get; set; }
|
public double Length { get; set; }
|
||||||
public Trajectory? Trajectory { get; set; }
|
public Trajectory Trajectory { get; set; }
|
||||||
public Corridor Corridor { get; set; } = new();
|
public Corridor Corridor { get; set; } = new();
|
||||||
[Required]
|
[Required]
|
||||||
public InstantAction.Action[] Actions { get; set; } = [];
|
public InstantAction.Action[] Actions { get; set; } = [];
|
||||||
|
|
|
||||||
|
|
@ -13,5 +13,5 @@ public class EdgeState
|
||||||
public string EdgeDescription { get; set; } = string.Empty;
|
public string EdgeDescription { get; set; } = string.Empty;
|
||||||
[Required]
|
[Required]
|
||||||
public bool Released { get; set; }
|
public bool Released { get; set; }
|
||||||
public Trajectory? Trajectory { get; set; }
|
public Trajectory Trajectory { get; set; } = new();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@ public enum ActionType
|
||||||
//liftUp,
|
//liftUp,
|
||||||
//liftDown,
|
//liftDown,
|
||||||
//liftRotate,
|
//liftRotate,
|
||||||
rotate,
|
//rotate,
|
||||||
//rotateKeepLift,
|
//rotateKeepLift,
|
||||||
//mutedBaseOn,
|
//mutedBaseOn,
|
||||||
//mutedBaseOff,
|
//mutedBaseOff,
|
||||||
|
|
|
||||||
|
|
@ -42,7 +42,6 @@
|
||||||
<script src="@Assets["_content/MudBlazor/MudBlazor.min.js"]"></script>
|
<script src="@Assets["_content/MudBlazor/MudBlazor.min.js"]"></script>
|
||||||
<script src="@Assets["js/canvas.js"]"></script>
|
<script src="@Assets["js/canvas.js"]"></script>
|
||||||
<script src="@Assets["js/app.js"]"></script>
|
<script src="@Assets["js/app.js"]"></script>
|
||||||
<script src="@Assets["js/robotMonitor.js"]"></script>
|
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,7 @@ public class OrderController(IOrder robotOrderController, IInstantActions instan
|
||||||
{
|
{
|
||||||
robotOrderController.StopOrder();
|
robotOrderController.StopOrder();
|
||||||
instantActions.StopOrderAction();
|
instantActions.StopOrderAction();
|
||||||
|
|
||||||
return Ok(new
|
return Ok(new
|
||||||
{
|
{
|
||||||
success = true,
|
success = true,
|
||||||
|
|
|
||||||
|
|
@ -21,8 +21,8 @@ namespace RobotApp.Hubs
|
||||||
// Phương thức này sẽ được gọi từ service để broadcast
|
// Phương thức này sẽ được gọi từ service để broadcast
|
||||||
public async Task SendState(string serialNumber, StateMsg state)
|
public async Task SendState(string serialNumber, StateMsg state)
|
||||||
{
|
{
|
||||||
//var json = JsonSerializer.Serialize(state, JsonOptionExtends.Write);
|
var json = JsonSerializer.Serialize(state, JsonOptionExtends.Write);
|
||||||
await Clients.Group(serialNumber).SendAsync("ReceiveState", state);
|
await Clients.Group(serialNumber).SendAsync("ReceiveState", json);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -12,9 +12,3 @@ public class RobotMonitorHub : Hub
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -63,7 +63,7 @@ builder.Services.AddRobot();
|
||||||
builder.Services.AddSingleton<RobotApp.Services.RobotMonitorService>();
|
builder.Services.AddSingleton<RobotApp.Services.RobotMonitorService>();
|
||||||
builder.Services.AddHostedService(sp => sp.GetRequiredService<RobotApp.Services.RobotMonitorService>());
|
builder.Services.AddHostedService(sp => sp.GetRequiredService<RobotApp.Services.RobotMonitorService>());
|
||||||
|
|
||||||
//builder.Services.AddScoped<RobotStateClient>();
|
builder.Services.AddScoped<RobotStateClient>();
|
||||||
builder.Services.AddHostedService<RobotStatePublisher>();
|
builder.Services.AddHostedService<RobotStatePublisher>();
|
||||||
|
|
||||||
var app = builder.Build();
|
var app = builder.Build();
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@ public class RobotActionStorage(IServiceProvider ServiceProvider)
|
||||||
ActionType.drop => new RobotDropAction(ServiceProvider),
|
ActionType.drop => new RobotDropAction(ServiceProvider),
|
||||||
ActionType.pick => new RobotPickAction(ServiceProvider),
|
ActionType.pick => new RobotPickAction(ServiceProvider),
|
||||||
//ActionType.liftRotate => new RobotLiftRotateAction(ServiceProvider),
|
//ActionType.liftRotate => new RobotLiftRotateAction(ServiceProvider),
|
||||||
ActionType.rotate => new RobotRotateAction(ServiceProvider),
|
//ActionType.rotate => new RobotRotateAction(ServiceProvider),
|
||||||
//ActionType.rotateKeepLift => new RobotRotateKeepLift(ServiceProvider),
|
//ActionType.rotateKeepLift => new RobotRotateKeepLift(ServiceProvider),
|
||||||
//ActionType.mutedBaseOn => new RobotMutedBaseOnAction(ServiceProvider),
|
//ActionType.mutedBaseOn => new RobotMutedBaseOnAction(ServiceProvider),
|
||||||
//ActionType.mutedBaseOff => new RobotMutedBaseOffAction(ServiceProvider),
|
//ActionType.mutedBaseOff => new RobotMutedBaseOffAction(ServiceProvider),
|
||||||
|
|
|
||||||
|
|
@ -26,16 +26,12 @@ public class RobotConnection(RobotConfiguration RobotConfiguration,
|
||||||
{
|
{
|
||||||
//Logger.Debug($"Nhận Order: {data}");
|
//Logger.Debug($"Nhận Order: {data}");
|
||||||
var msg = JsonSerializer.Deserialize<OrderMsg>(data, JsonOptionExtends.Read);
|
var msg = JsonSerializer.Deserialize<OrderMsg>(data, JsonOptionExtends.Read);
|
||||||
if (msg is null || string.IsNullOrEmpty(msg.SerialNumber) || msg.SerialNumber != RobotConfiguration.SerialNumber)
|
if (msg is null || string.IsNullOrEmpty(msg.SerialNumber) || msg.SerialNumber != RobotConfiguration.SerialNumber) return;
|
||||||
{
|
|
||||||
Logger.Warning($"SerialNumber cuả order không hợp lệ: message SerialNumber {msg?.SerialNumber}");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
OrderUpdated?.Invoke(msg);
|
OrderUpdated?.Invoke(msg);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Logger.Warning($"Nhận Order xảy ra lỗi: {ex.Message}");
|
Logger.Warning($"Nhận Order xảy ra lỗi: {ex.Message} - {ex.StackTrace}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -45,16 +41,12 @@ public class RobotConnection(RobotConfiguration RobotConfiguration,
|
||||||
{
|
{
|
||||||
//Logger.Debug($"Nhận InstanceActions: {data}");
|
//Logger.Debug($"Nhận InstanceActions: {data}");
|
||||||
var msg = JsonSerializer.Deserialize<InstantActionsMsg>(data, JsonOptionExtends.Read);
|
var msg = JsonSerializer.Deserialize<InstantActionsMsg>(data, JsonOptionExtends.Read);
|
||||||
if (msg is null || string.IsNullOrEmpty(msg.SerialNumber) || msg.SerialNumber != RobotConfiguration.SerialNumber)
|
if (msg is null || string.IsNullOrEmpty(msg.SerialNumber) || msg.SerialNumber != RobotConfiguration.SerialNumber) return;
|
||||||
{
|
|
||||||
Logger.Warning($"SerialNumber của action không hợp lệ: message SerialNumber {msg?.SerialNumber}");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
ActionUpdated?.Invoke(msg);
|
ActionUpdated?.Invoke(msg);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Logger.Warning($"Nhận InstanceActions xảy ra lỗi: {ex.Message}");
|
Logger.Warning($"Nhận InstanceActions xảy ra lỗi: {ex.Message} - {ex.StackTrace}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -105,10 +105,7 @@ public class RobotErrors : IError
|
||||||
=> CreateError(ErrorType.INITIALIZE_ORDER, "Vui lòng kiểm tra lại order", ErrorLevel.WARNING, $"Order mới nhận được không phải là nối tiếp của order khi LastNodeSequenceId: {lastNodeSequenceId} mà node đầu tiên của order mới có sequence: {newStartNodeSequenceId}");
|
=> CreateError(ErrorType.INITIALIZE_ORDER, "Vui lòng kiểm tra lại order", ErrorLevel.WARNING, $"Order mới nhận được không phải là nối tiếp của order khi LastNodeSequenceId: {lastNodeSequenceId} mà node đầu tiên của order mới có sequence: {newStartNodeSequenceId}");
|
||||||
public static Error Error1018(int oldOrderUpdateId, int newOrderUpdateId)
|
public static Error Error1018(int oldOrderUpdateId, int newOrderUpdateId)
|
||||||
=> CreateError(ErrorType.INITIALIZE_ORDER, "Vui lòng kiểm tra lại OrderUpdateId", ErrorLevel.WARNING, $"OrderUpdateId {newOrderUpdateId} nhận được nhỏ hơn OrderUpdateId hiện tại là {oldOrderUpdateId}");
|
=> CreateError(ErrorType.INITIALIZE_ORDER, "Vui lòng kiểm tra lại OrderUpdateId", ErrorLevel.WARNING, $"OrderUpdateId {newOrderUpdateId} nhận được nhỏ hơn OrderUpdateId hiện tại là {oldOrderUpdateId}");
|
||||||
public static Error Error1019()
|
|
||||||
=> CreateError(ErrorType.INITIALIZE_ORDER, "Vui lòng kiểm tra lại Order", ErrorLevel.WARNING, "Order có node đầu tiên quá xa robot");
|
|
||||||
public static Error Error1020()
|
|
||||||
=> CreateError(ErrorType.INITIALIZE_ORDER, "", ErrorLevel.WARNING, "Robot đang ở đích của Order");
|
|
||||||
|
|
||||||
public static Error Error2001()
|
public static Error Error2001()
|
||||||
=> CreateError(ErrorType.READ_PERIPHERAL_FAILURE, "2001", ErrorLevel.FATAL, "Có lỗi xảy ra trong quá trình đọc tín hiệu từ hệ thống ngoại vi(PLC)");
|
=> CreateError(ErrorType.READ_PERIPHERAL_FAILURE, "2001", ErrorLevel.FATAL, "Có lỗi xảy ra trong quá trình đọc tín hiệu từ hệ thống ngoại vi(PLC)");
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,7 @@ public class RobotFactsheet(RobotConnection RobotConnection, RobotConfiguration
|
||||||
{ ActionType.pick, Pick},
|
{ ActionType.pick, Pick},
|
||||||
{ ActionType.drop, Drop},
|
{ ActionType.drop, Drop},
|
||||||
//{ ActionType.liftRotate, LiftRotate},
|
//{ ActionType.liftRotate, LiftRotate},
|
||||||
{ ActionType.rotate, Rotate},
|
//{ ActionType.rotate, Rotate},
|
||||||
//{ ActionType.rotateKeepLift, RotateKeepLift},
|
//{ ActionType.rotateKeepLift, RotateKeepLift},
|
||||||
//{ ActionType.mutedBaseOn, MutedBaseOn},
|
//{ ActionType.mutedBaseOn, MutedBaseOn},
|
||||||
//{ ActionType.mutedBaseOff, MutedBaseOff},
|
//{ ActionType.mutedBaseOff, MutedBaseOff},
|
||||||
|
|
@ -224,22 +224,22 @@ public class RobotFactsheet(RobotConnection RobotConnection, RobotConfiguration
|
||||||
// BlockingTypes = [BlockingType.HARD.ToString()],
|
// BlockingTypes = [BlockingType.HARD.ToString()],
|
||||||
//};
|
//};
|
||||||
|
|
||||||
public readonly static AgvAction Rotate = new()
|
//public readonly static AgvAction Rotate = new()
|
||||||
{
|
//{
|
||||||
ActionType = ActionType.rotate.ToString(),
|
// ActionType = ActionType.rotate.ToString(),
|
||||||
ActionDescription = "Xoay robot tại chỗ.",
|
// ActionDescription = "Xoay robot tại chỗ.",
|
||||||
ActionScopes = [ActionScopes.INSTANT.ToString(), ActionScopes.NODE.ToString()],
|
// ActionScopes = [ActionScopes.INSTANT.ToString(), ActionScopes.NODE.ToString()],
|
||||||
ActionParameters = [
|
// ActionParameters = [
|
||||||
new()
|
// new()
|
||||||
{
|
// {
|
||||||
Key = "angle",
|
// Key = "angle",
|
||||||
Description = "Góc xoay của robot. (rad)",
|
// Description = "Góc xoay của robot. (rad)",
|
||||||
ValueDataType = ValueDataType.FLOAT.ToString(),
|
// ValueDataType = ValueDataType.FLOAT.ToString(),
|
||||||
IsOptional = false,
|
// IsOptional = false,
|
||||||
}],
|
// }],
|
||||||
ResultDescription = "Robot đã xoay tại chỗ.",
|
// ResultDescription = "Robot đã xoay tại chỗ.",
|
||||||
BlockingTypes = [BlockingType.HARD.ToString()],
|
// BlockingTypes = [BlockingType.HARD.ToString()],
|
||||||
};
|
//};
|
||||||
|
|
||||||
//public readonly static AgvAction RotateKeepLift = new()
|
//public readonly static AgvAction RotateKeepLift = new()
|
||||||
//{
|
//{
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ using RobotApp.VDA5050.State;
|
||||||
|
|
||||||
namespace RobotApp.Services.Robot;
|
namespace RobotApp.Services.Robot;
|
||||||
|
|
||||||
public class RobotLoads() : ILoad
|
public class RobotLoads(IPeripheral PeriperalManager) : ILoad
|
||||||
{
|
{
|
||||||
//public Load[] Load => PeriperalManager.HasLoad ? [GetLoad()] : [];
|
//public Load[] Load => PeriperalManager.HasLoad ? [GetLoad()] : [];
|
||||||
public Load[] Load { get; private set; } = [];
|
public Load[] Load { get; private set; } = [];
|
||||||
|
|
|
||||||
|
|
@ -273,9 +273,6 @@ public class RobotLocalization(RobotConfiguration RobotConfiguration, Simulation
|
||||||
public MessageResult SetInitializePosition(double x, double y, double theta)
|
public MessageResult SetInitializePosition(double x, double y, double theta)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
|
||||||
if (IsSimulation) SimVisualization.LocalizationInitialize(x, y, theta);
|
|
||||||
else
|
|
||||||
{
|
{
|
||||||
var xyzw = QuaternionToXYZW(0, 0, theta);
|
var xyzw = QuaternionToXYZW(0, 0, theta);
|
||||||
var response = XlocClient.SetInitialPose(new SetInitialPoseRequest()
|
var response = XlocClient.SetInitialPose(new SetInitialPoseRequest()
|
||||||
|
|
@ -304,8 +301,6 @@ public class RobotLocalization(RobotConfiguration RobotConfiguration, Simulation
|
||||||
return new(false, "Khởi tạo vị trí cho robot thất bại");
|
return new(false, "Khởi tạo vị trí cho robot thất bại");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return new(true);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Logger.Warning($"Khởi tạo vị trí cho robot thất bại: {ex.Message}");
|
Logger.Warning($"Khởi tạo vị trí cho robot thất bại: {ex.Message}");
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,6 @@ using RobotApp.VDA5050.Order;
|
||||||
using RobotApp.VDA5050.State;
|
using RobotApp.VDA5050.State;
|
||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
using System.Data;
|
using System.Data;
|
||||||
using System.Xml.Linq;
|
|
||||||
using Action = RobotApp.VDA5050.InstantAction.Action;
|
using Action = RobotApp.VDA5050.InstantAction.Action;
|
||||||
|
|
||||||
namespace RobotApp.Services.Robot;
|
namespace RobotApp.Services.Robot;
|
||||||
|
|
@ -248,6 +247,11 @@ public class RobotOrderController(INavigation NavigationManager,
|
||||||
UpdateState();
|
UpdateState();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private bool IsNewPath()
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
private void ClearLastNode()
|
private void ClearLastNode()
|
||||||
{
|
{
|
||||||
if (LastNode is null) return;
|
if (LastNode is null) return;
|
||||||
|
|
@ -288,7 +292,6 @@ public class RobotOrderController(INavigation NavigationManager,
|
||||||
|
|
||||||
private void HandleOrder()
|
private void HandleOrder()
|
||||||
{
|
{
|
||||||
if (Nodes.Length <= 0) return;
|
|
||||||
if (IsCancelOrder)
|
if (IsCancelOrder)
|
||||||
{
|
{
|
||||||
NavigationManager.CancelMovement();
|
NavigationManager.CancelMovement();
|
||||||
|
|
@ -306,12 +309,8 @@ public class RobotOrderController(INavigation NavigationManager,
|
||||||
{
|
{
|
||||||
var action = FinalAction[0];
|
var action = FinalAction[0];
|
||||||
var robotAction = ActionManager[action.ActionId];
|
var robotAction = ActionManager[action.ActionId];
|
||||||
if (robotAction is null)
|
if (robotAction is null) return;
|
||||||
{
|
if (robotAction.IsCompleted) FinalAction.Remove(action);
|
||||||
FinalAction.Remove(action);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (robotAction.IsCompleted)
|
|
||||||
if (robotAction.Status == ActionStatus.WAITING) ActionManager.StartOrderAction(action.ActionId);
|
if (robotAction.Status == ActionStatus.WAITING) ActionManager.StartOrderAction(action.ActionId);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
|
|
@ -399,18 +398,6 @@ public class RobotOrderController(INavigation NavigationManager,
|
||||||
if (NodeStates.Length != 0 || EdgeStates.Length != 0) HandleUpdateOrder(NewOrderHandler);
|
if (NodeStates.Length != 0 || EdgeStates.Length != 0) HandleUpdateOrder(NewOrderHandler);
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
Node startNode = NewOrderHandler.Nodes[0];
|
|
||||||
var nodeDeviation = startNode.NodePosition.AllowedDeviationXY == 0.0 ? NewOrderHandler.Nodes.Length == 1 ? 0.3 : 0.5 : startNode.NodePosition.AllowedDeviationXY;
|
|
||||||
var distance = Localization.DistanceTo(startNode.NodePosition.X, startNode.NodePosition.Y);
|
|
||||||
if (distance > nodeDeviation) throw new OrderException(RobotErrors.Error1019());
|
|
||||||
|
|
||||||
if (NewOrderHandler.Nodes.Length > 1)
|
|
||||||
{
|
|
||||||
Node endNode = NewOrderHandler.Nodes[^1];
|
|
||||||
nodeDeviation = endNode.NodePosition.AllowedDeviationXY == 0.0 ? 0.2 : endNode.NodePosition.AllowedDeviationXY;
|
|
||||||
distance = Localization.DistanceTo(endNode.NodePosition.X, endNode.NodePosition.Y);
|
|
||||||
if (distance < nodeDeviation) throw new OrderException(RobotErrors.Error1020());
|
|
||||||
}
|
|
||||||
HandleNewOrder(NewOrderHandler);
|
HandleNewOrder(NewOrderHandler);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -422,7 +409,6 @@ public class RobotOrderController(INavigation NavigationManager,
|
||||||
{
|
{
|
||||||
ErrorManager.AddError(orEx.Error, TimeSpan.FromSeconds(10));
|
ErrorManager.AddError(orEx.Error, TimeSpan.FromSeconds(10));
|
||||||
Logger.Warning($"Lỗi khi xử lí Order: {orEx.Error.ErrorDescription}");
|
Logger.Warning($"Lỗi khi xử lí Order: {orEx.Error.ErrorDescription}");
|
||||||
if (Nodes.Length == 0) HandleOrderStop();
|
|
||||||
}
|
}
|
||||||
else Logger.Warning($"Lỗi khi xử lí Order: {orEx.Message}");
|
else Logger.Warning($"Lỗi khi xử lí Order: {orEx.Message}");
|
||||||
}
|
}
|
||||||
|
|
@ -479,23 +465,17 @@ public class RobotOrderController(INavigation NavigationManager,
|
||||||
var edges = NewOrderEdges.ToList().GetRange(lastNodeIndex + 1, nodes.Count - 1);
|
var edges = NewOrderEdges.ToList().GetRange(lastNodeIndex + 1, nodes.Count - 1);
|
||||||
for (int i = 0; i < nodes.Count - 1; i++)
|
for (int i = 0; i < nodes.Count - 1; i++)
|
||||||
{
|
{
|
||||||
if (edges[i] is null) return (NodeStates, [.. pathEdges]);
|
|
||||||
|
|
||||||
var trajectory = edges[i].Trajectory;
|
|
||||||
var controlPoints = trajectory?.ControlPoints;
|
|
||||||
|
|
||||||
|
|
||||||
pathEdges.Add(new()
|
pathEdges.Add(new()
|
||||||
{
|
{
|
||||||
StartX = nodes[i].NodePosition.X,
|
StartX = nodes[i].NodePosition.X,
|
||||||
StartY = nodes[i].NodePosition.Y,
|
StartY = nodes[i].NodePosition.Y,
|
||||||
EndX = nodes[i + 1].NodePosition.X,
|
EndX = nodes[i + 1].NodePosition.X,
|
||||||
EndY = nodes[i + 1].NodePosition.Y,
|
EndY = nodes[i + 1].NodePosition.Y,
|
||||||
ControlPoint1X = controlPoints is { Length: > 2 } ? controlPoints[1].X : 0,
|
ControlPoint1X = edges[i].Trajectory is not null && edges[i].Trajectory.ControlPoints.Length > 2 ? edges[i].Trajectory.ControlPoints[1].X : 0,
|
||||||
ControlPoint1Y = controlPoints is { Length: > 2 } ? controlPoints[1].Y : 0,
|
ControlPoint1Y = edges[i].Trajectory is not null && edges[i].Trajectory.ControlPoints.Length > 2 ? edges[i].Trajectory.ControlPoints[1].Y : 0,
|
||||||
ControlPoint2X = controlPoints is { Length: > 3 } ? controlPoints[2].X : 0,
|
ControlPoint2X = edges[i].Trajectory is not null && edges[i].Trajectory.ControlPoints.Length > 3 ? edges[i].Trajectory.ControlPoints[2].X : 0,
|
||||||
ControlPoint2Y = controlPoints is { Length: > 3 } ? controlPoints[2].Y : 0,
|
ControlPoint2Y = edges[i].Trajectory is not null && edges[i].Trajectory.ControlPoints.Length > 3 ? edges[i].Trajectory.ControlPoints[2].Y : 0,
|
||||||
Degree = trajectory is null ? 1 : trajectory.Degree,
|
Degree = edges[i].Trajectory.Degree,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -21,11 +21,11 @@ public class RobotPathPlanner(IConfiguration Configuration)
|
||||||
Y1 = inNode.NodePosition.Y,
|
Y1 = inNode.NodePosition.Y,
|
||||||
X2 = futureNode.NodePosition.X,
|
X2 = futureNode.NodePosition.X,
|
||||||
Y2 = futureNode.NodePosition.Y,
|
Y2 = futureNode.NodePosition.Y,
|
||||||
ControlPoint1X = edge.Trajectory is not null && edge.Trajectory.ControlPoints.Length > 2 ? edge.Trajectory.ControlPoints[1].X : 0,
|
ControlPoint1X = edge.Trajectory.ControlPoints.Length > 2 ? edge.Trajectory.ControlPoints[1].X : 0,
|
||||||
ControlPoint1Y = edge.Trajectory is not null && edge.Trajectory.ControlPoints.Length > 2 ? edge.Trajectory.ControlPoints[1].Y : 0,
|
ControlPoint1Y = edge.Trajectory.ControlPoints.Length > 2 ? edge.Trajectory.ControlPoints[1].Y : 0,
|
||||||
ControlPoint2X = edge.Trajectory is not null && edge.Trajectory.ControlPoints.Length > 3 ? edge.Trajectory.ControlPoints[2].X : 0,
|
ControlPoint2X = edge.Trajectory.ControlPoints.Length > 3 ? edge.Trajectory.ControlPoints[2].X : 0,
|
||||||
ControlPoint2Y = edge.Trajectory is not null && edge.Trajectory.ControlPoints.Length > 3 ? edge.Trajectory.ControlPoints[2].Y : 0,
|
ControlPoint2Y = edge.Trajectory.ControlPoints.Length > 3 ? edge.Trajectory.ControlPoints[2].Y : 0,
|
||||||
TrajectoryDegree = edge.Trajectory is null ? TrajectoryDegree.One : edge.Trajectory.Degree == 1 ? TrajectoryDegree.One : edge.Trajectory.Degree == 2 ? TrajectoryDegree.Two : TrajectoryDegree.Three,
|
TrajectoryDegree = edge.Trajectory.Degree == 1 ? TrajectoryDegree.One : edge.Trajectory.Degree == 2 ? TrajectoryDegree.Two : TrajectoryDegree.Three,
|
||||||
});
|
});
|
||||||
(double robotx, double roboty) =
|
(double robotx, double roboty) =
|
||||||
(
|
(
|
||||||
|
|
@ -61,46 +61,30 @@ public class RobotPathPlanner(IConfiguration Configuration)
|
||||||
navigationNodes[0].Direction = GetDirectionInNode(currentTheta, nodes[0], nodes[1], edges[0]);
|
navigationNodes[0].Direction = GetDirectionInNode(currentTheta, nodes[0], nodes[1], edges[0]);
|
||||||
for (int i = 1; i < nodes.Length - 1; i++)
|
for (int i = 1; i < nodes.Length - 1; i++)
|
||||||
{
|
{
|
||||||
var trajectory = edges[i - 1].Trajectory;
|
|
||||||
var controlPoints = trajectory?.ControlPoints;
|
|
||||||
(double lastx, double lasty) = MathExtensions.Curve(0.1, new()
|
(double lastx, double lasty) = MathExtensions.Curve(0.1, new()
|
||||||
{
|
{
|
||||||
X1 = nodes[i - 1].NodePosition.X,
|
X1 = nodes[i - 1].NodePosition.X,
|
||||||
Y1 = nodes[i - 1].NodePosition.Y,
|
Y1 = nodes[i - 1].NodePosition.Y,
|
||||||
X2 = nodes[i].NodePosition.X,
|
X2 = nodes[i].NodePosition.X,
|
||||||
Y2 = nodes[i].NodePosition.Y,
|
Y2 = nodes[i].NodePosition.Y,
|
||||||
ControlPoint1X = controlPoints is { Length: > 2 } ? controlPoints[1].X : 0,
|
ControlPoint1X = edges[i - 1].Trajectory.ControlPoints.Length > 2 ? edges[i - 1].Trajectory.ControlPoints[1].X : 0,
|
||||||
ControlPoint1Y = controlPoints is { Length: > 2 } ? controlPoints[1].Y : 0,
|
ControlPoint1Y = edges[i - 1].Trajectory.ControlPoints.Length > 2 ? edges[i - 1].Trajectory.ControlPoints[1].Y : 0,
|
||||||
ControlPoint2X = controlPoints is { Length: > 3 } ? controlPoints[2].X : 0,
|
ControlPoint2X = edges[i - 1].Trajectory.ControlPoints.Length > 3 ? edges[i - 1].Trajectory.ControlPoints[2].X : 0,
|
||||||
ControlPoint2Y = controlPoints is { Length: > 3 } ? controlPoints[2].Y : 0,
|
ControlPoint2Y = edges[i - 1].Trajectory.ControlPoints.Length > 3 ? edges[i - 1].Trajectory.ControlPoints[2].Y : 0,
|
||||||
TrajectoryDegree = trajectory?.Degree switch
|
TrajectoryDegree = edges[i - 1].Trajectory.Degree == 1 ? TrajectoryDegree.One : edges[i - 1].Trajectory.Degree == 2 ? TrajectoryDegree.Two : TrajectoryDegree.Three,
|
||||||
{
|
|
||||||
1 => TrajectoryDegree.One,
|
|
||||||
2 => TrajectoryDegree.Two,
|
|
||||||
_ => TrajectoryDegree.Three
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
trajectory = edges[i].Trajectory;
|
|
||||||
controlPoints = trajectory?.ControlPoints;
|
|
||||||
(double futurex, double futurey) = MathExtensions.Curve(0.1, new()
|
(double futurex, double futurey) = MathExtensions.Curve(0.1, new()
|
||||||
{
|
{
|
||||||
X1 = nodes[i].NodePosition.X,
|
X1 = nodes[i].NodePosition.X,
|
||||||
Y1 = nodes[i].NodePosition.Y,
|
Y1 = nodes[i].NodePosition.Y,
|
||||||
X2 = nodes[i + 1].NodePosition.X,
|
X2 = nodes[i + 1].NodePosition.X,
|
||||||
Y2 = nodes[i + 1].NodePosition.Y,
|
Y2 = nodes[i + 1].NodePosition.Y,
|
||||||
ControlPoint1X = controlPoints is { Length: > 2 } ? controlPoints[1].X : 0,
|
ControlPoint1X = edges[i].Trajectory.ControlPoints.Length > 2 ? edges[i].Trajectory.ControlPoints[1].X : 0,
|
||||||
ControlPoint1Y = controlPoints is { Length: > 2 } ? controlPoints[1].Y : 0,
|
ControlPoint1Y = edges[i].Trajectory.ControlPoints.Length > 2 ? edges[i].Trajectory.ControlPoints[1].Y : 0,
|
||||||
ControlPoint2X = controlPoints is { Length: > 3 } ? controlPoints[2].X : 0,
|
ControlPoint2X = edges[i].Trajectory.ControlPoints.Length > 3 ? edges[i].Trajectory.ControlPoints[2].X : 0,
|
||||||
ControlPoint2Y = controlPoints is { Length: > 3 } ? controlPoints[2].Y : 0,
|
ControlPoint2Y = edges[i].Trajectory.ControlPoints.Length > 3 ? edges[i].Trajectory.ControlPoints[2].Y : 0,
|
||||||
TrajectoryDegree = trajectory?.Degree switch
|
TrajectoryDegree = edges[i].Trajectory.Degree == 1 ? TrajectoryDegree.One : edges[i].Trajectory.Degree == 2 ? TrajectoryDegree.Two : TrajectoryDegree.Three,
|
||||||
{
|
|
||||||
1 => TrajectoryDegree.One,
|
|
||||||
2 => TrajectoryDegree.Two,
|
|
||||||
_ => TrajectoryDegree.Three
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
var angle = MathExtensions.GetVectorAngle(
|
var angle = MathExtensions.GetVectorAngle(
|
||||||
nodes[i].NodePosition.X,
|
nodes[i].NodePosition.X,
|
||||||
nodes[i].NodePosition.Y,
|
nodes[i].NodePosition.Y,
|
||||||
|
|
@ -144,11 +128,11 @@ public class RobotPathPlanner(IConfiguration Configuration)
|
||||||
Y1 = startNode.Y,
|
Y1 = startNode.Y,
|
||||||
X2 = endNode.X,
|
X2 = endNode.X,
|
||||||
Y2 = endNode.Y,
|
Y2 = endNode.Y,
|
||||||
ControlPoint1X = edge.Trajectory is not null && edge.Trajectory.ControlPoints.Length > 2 ? edge.Trajectory.ControlPoints[1].X : 0,
|
ControlPoint1X = edge.Trajectory.ControlPoints.Length > 2 ? edge.Trajectory.ControlPoints[1].X : 0,
|
||||||
ControlPoint1Y = edge.Trajectory is not null && edge.Trajectory.ControlPoints.Length > 2 ? edge.Trajectory.ControlPoints[1].Y : 0,
|
ControlPoint1Y = edge.Trajectory.ControlPoints.Length > 2 ? edge.Trajectory.ControlPoints[1].Y : 0,
|
||||||
ControlPoint2X = edge.Trajectory is not null && edge.Trajectory.ControlPoints.Length > 3 ? edge.Trajectory.ControlPoints[2].X : 0,
|
ControlPoint2X = edge.Trajectory.ControlPoints.Length > 3 ? edge.Trajectory.ControlPoints[2].X : 0,
|
||||||
ControlPoint2Y = edge.Trajectory is not null && edge.Trajectory.ControlPoints.Length > 3 ? edge.Trajectory.ControlPoints[2].Y : 0,
|
ControlPoint2Y = edge.Trajectory.ControlPoints.Length > 3 ? edge.Trajectory.ControlPoints[2].Y : 0,
|
||||||
TrajectoryDegree = edge.Trajectory is null ? TrajectoryDegree.One : edge.Trajectory.Degree == 1 ? TrajectoryDegree.One : edge.Trajectory.Degree == 2 ? TrajectoryDegree.Two : TrajectoryDegree.Three
|
TrajectoryDegree = edge.Trajectory.Degree == 1 ? TrajectoryDegree.One : edge.Trajectory.Degree == 2 ? TrajectoryDegree.Two : TrajectoryDegree.Three
|
||||||
};
|
};
|
||||||
|
|
||||||
double length = EdgeCalculatorModel.GetEdgeLength();
|
double length = EdgeCalculatorModel.GetEdgeLength();
|
||||||
|
|
@ -210,11 +194,11 @@ public class RobotPathPlanner(IConfiguration Configuration)
|
||||||
Y1 = startNode.NodePosition.Y,
|
Y1 = startNode.NodePosition.Y,
|
||||||
X2 = endNode.NodePosition.X,
|
X2 = endNode.NodePosition.X,
|
||||||
Y2 = endNode.NodePosition.Y,
|
Y2 = endNode.NodePosition.Y,
|
||||||
ControlPoint1X = edge.Trajectory is not null && edge.Trajectory.ControlPoints.Length > 2 ? edge.Trajectory.ControlPoints[1].X : 0,
|
ControlPoint1X = edge.Trajectory.ControlPoints.Length > 2 ? edge.Trajectory.ControlPoints[1].X : 0,
|
||||||
ControlPoint1Y = edge.Trajectory is not null && edge.Trajectory.ControlPoints.Length > 2 ? edge.Trajectory.ControlPoints[1].Y : 0,
|
ControlPoint1Y = edge.Trajectory.ControlPoints.Length > 2 ? edge.Trajectory.ControlPoints[1].Y : 0,
|
||||||
ControlPoint2X = edge.Trajectory is not null && edge.Trajectory.ControlPoints.Length > 3 ? edge.Trajectory.ControlPoints[2].X : 0,
|
ControlPoint2X = edge.Trajectory.ControlPoints.Length > 3 ? edge.Trajectory.ControlPoints[2].X : 0,
|
||||||
ControlPoint2Y = edge.Trajectory is not null && edge.Trajectory.ControlPoints.Length > 3 ? edge.Trajectory.ControlPoints[2].Y : 0,
|
ControlPoint2Y = edge.Trajectory.ControlPoints.Length > 3 ? edge.Trajectory.ControlPoints[2].Y : 0,
|
||||||
TrajectoryDegree = edge.Trajectory is null ? TrajectoryDegree.One : edge.Trajectory.Degree == 1 ? TrajectoryDegree.One : edge.Trajectory.Degree == 2 ? TrajectoryDegree.Two : TrajectoryDegree.Three
|
TrajectoryDegree = edge.Trajectory.Degree == 1 ? TrajectoryDegree.One : edge.Trajectory.Degree == 2 ? TrajectoryDegree.Two : TrajectoryDegree.Three
|
||||||
};
|
};
|
||||||
|
|
||||||
double length = EdgeCalculatorModel.GetEdgeLength();
|
double length = EdgeCalculatorModel.GetEdgeLength();
|
||||||
|
|
|
||||||
|
|
@ -12,22 +12,164 @@ using System.Text.Json;
|
||||||
|
|
||||||
namespace RobotApp.Services.Robot;
|
namespace RobotApp.Services.Robot;
|
||||||
|
|
||||||
public class RobotStatePublisher(
|
public class RobotStatePublisher : BackgroundService
|
||||||
IHubContext<RobotHub> _hubContext,
|
|
||||||
RobotStates _robotState,
|
|
||||||
RobotConnection _robotConnection) : 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 readonly RobotConnection _robotConnection;
|
||||||
|
private bool? _lastRobotConnectionState;
|
||||||
|
|
||||||
|
private uint _headerId = 0;
|
||||||
private readonly PeriodicTimer _timer = new(TimeSpan.FromMilliseconds(1000)); // 1 giây/lần
|
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,
|
||||||
|
RobotConnection robotConnection)
|
||||||
|
{
|
||||||
|
_hubContext = hubContext;
|
||||||
|
_robotConfig = robotConfig;
|
||||||
|
_orderManager = orderManager;
|
||||||
|
_actionManager = actionManager;
|
||||||
|
_peripheralManager = peripheralManager;
|
||||||
|
_infoManager = infoManager;
|
||||||
|
_errorManager = errorManager;
|
||||||
|
_localizationManager = localizationManager;
|
||||||
|
_batteryManager = batteryManager;
|
||||||
|
_loadManager = loadManager;
|
||||||
|
_navigationManager = navigationManager;
|
||||||
|
_stateManager = stateManager;
|
||||||
|
_robotConnection = robotConnection;
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
||||||
{
|
{
|
||||||
while (await _timer.WaitForNextTickAsync(stoppingToken))
|
while (await _timer.WaitForNextTickAsync(stoppingToken))
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
var serialNumber = _robotConfig.SerialNumber;
|
||||||
|
|
||||||
// ===== SEND STATE =====
|
// ===== SEND STATE =====
|
||||||
await _hubContext.Clients.All.SendAsync("ReceiveState", _robotState.GetStateMsg(), stoppingToken);
|
var state = GetStateMsg();
|
||||||
await _hubContext.Clients.All.SendAsync("ReceiveRobotConnection", _robotConnection.IsConnected, stoppingToken);
|
var json = JsonSerializer.Serialize(state, JsonOptionExtends.Write);
|
||||||
|
|
||||||
|
await _hubContext.Clients
|
||||||
|
.Group(serialNumber)
|
||||||
|
.SendAsync("ReceiveState", json, stoppingToken);
|
||||||
|
|
||||||
|
// ===== SEND ROBOT CONNECTION (ONLY WHEN CHANGED) =====
|
||||||
|
var isConnected = _robotConnection.IsConnected;
|
||||||
|
|
||||||
|
if (_lastRobotConnectionState != isConnected)
|
||||||
|
{
|
||||||
|
_lastRobotConnectionState = isConnected;
|
||||||
|
|
||||||
|
await _hubContext.Clients
|
||||||
|
.Group(serialNumber) // routing only
|
||||||
|
.SendAsync(
|
||||||
|
"ReceiveRobotConnection",
|
||||||
|
isConnected, // payload only bool
|
||||||
|
stoppingToken
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
|
|
@ -35,10 +177,10 @@ public class RobotStatePublisher(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public override Task StopAsync(CancellationToken cancellationToken)
|
|
||||||
|
public override void Dispose()
|
||||||
{
|
{
|
||||||
_timer?.Dispose();
|
_timer?.Dispose();
|
||||||
base.Dispose();
|
base.Dispose();
|
||||||
return base.StopAsync(cancellationToken);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -36,7 +36,7 @@ public class RobotStates(RobotConfiguration RobotConfiguration,
|
||||||
catch { }
|
catch { }
|
||||||
}
|
}
|
||||||
|
|
||||||
public StateMsg GetStateMsg()
|
private StateMsg GetStateMsg()
|
||||||
{
|
{
|
||||||
return new StateMsg
|
return new StateMsg
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -23,8 +23,8 @@ public class SimulationNavigation : INavigation, IDisposable
|
||||||
|
|
||||||
protected const int CycleHandlerMilliseconds = 50;
|
protected const int CycleHandlerMilliseconds = 50;
|
||||||
private const double Scale = 1;
|
private const double Scale = 1;
|
||||||
private WatchTimer<SimulationNavigation>? NavigationTimer;
|
//private WatchTimer<SimulationNavigation>? NavigationTimer;
|
||||||
//private HighPrecisionTimer<SimulationNavigation>? NavigationTimer;
|
private HighPrecisionTimer<SimulationNavigation>? NavigationTimer;
|
||||||
|
|
||||||
protected double TargetAngle = 0;
|
protected double TargetAngle = 0;
|
||||||
protected PID? RotatePID;
|
protected PID? RotatePID;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user