Compare commits

...

7 Commits

Author SHA1 Message Date
Đăng Nguyễn
5c1851e92f update 2025-12-31 14:03:47 +07:00
Đăng Nguyễn
49c0c1ab39 update 2025-12-31 13:25:10 +07:00
Đăng Nguyễn
8362713dcc update 2025-12-31 08:56:53 +07:00
Đăng Nguyễn
15a61fd986 update 2025-12-30 17:38:43 +07:00
Đăng Nguyễn
b3f765d261 update 2025-12-30 16:59:08 +07:00
Đăng Nguyễn
2785a8f161 update 2025-12-30 15:17:42 +07:00
Đăng Nguyễn
a51cfe80c8 update 2025-12-23 09:52:42 +07:00
34 changed files with 520 additions and 839 deletions

View File

@ -1,2 +1,5 @@
# 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

View File

@ -17,12 +17,26 @@
<i class="mdi mdi-fit-to-screen-outline icon-button"></i>
</button>
</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 />
@if (MonitorData?.RobotPosition != null)
{
<div class="robot-position-info">
<i class="mdi mdi-map-marker"></i>
<span>X: @MonitorData.RobotPosition.X.ToString("F2")m | Y: @MonitorData.RobotPosition.Y.ToString("F2")m | θ: @((MonitorData.RobotPosition.Theta * 180 / Math.PI).ToString("F1"))°</span>
<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>
</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>
}
@* <MudChip T="string" Color="@(IsConnected? Color.Success: Color.Error)" Size="Size.Small">
@ -36,18 +50,52 @@
@onmousemove="HandleMouseMove"
@onmouseup="HandleMouseUp"
@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()">
@* 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)
{
@* @for (int i = 0; i < MonitorData.EdgeStates.Length; i++)
{
var edge = MonitorData.EdgeStates[i];
var (startX, startY, endX, endY) = GetEdgeEndpoints(i, edge);
<path d="@GetPathFromTrajectory(edge.Trajectory, startX, startY, endX, endY)"
fill="none"
stroke="#42A5F5"
stroke-width="0.08" />
} *@
<path d="@PathView"
fill="none"
stroke="#42A5F5"
@ -106,7 +154,7 @@
private ElementReference SvgRef;
private ElementReference SvgContainerRef;
private double ZoomScale = 2.0; // Zoom vào robot hơn khi mở
private double ZoomScale = 1.5; // Zoom vào robot hơn khi mở
private const double MIN_ZOOM = 0.1;
private const double MAX_ZOOM = 5.0;
private const double BASE_PIXELS_PER_METER = 50.0;
@ -124,14 +172,44 @@
private string PathView = "";
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)
{
if (firstRender)
{
var containerSize = await JS.InvokeAsync<ElementSize>("getElementSize", SvgContainerRef);
var containerSize = await JS.InvokeAsync<ElementSize>("robotMonitor.getElementSize", SvgContainerRef);
SvgWidth = containerSize.Width;
SvgHeight = containerSize.Height;
// Load map image and get dimensions
await LoadMapImage();
// Center view on robot if available with initial zoom
if (MonitorData?.RobotPosition != null)
{
@ -150,8 +228,37 @@
}
}
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()
{
// 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})";
}
@ -174,7 +281,7 @@
// Điều chỉnh kích thước dựa trên ZoomScale
// Tăng kích thước lên 1.5x để robot to hơn
double scaleFactor = 3 / ZoomScale; // Tăng kích thước hiển thị
double scaleFactor = 2 / ZoomScale; // Tăng kích thước hiển thị
double width = RobotWidthMeters * scaleFactor;
double height = RobotLengthMeters * scaleFactor;
@ -217,6 +324,53 @@
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()
{
if (MonitorData is not null && MonitorData.EdgeStates.Length > 0)
@ -291,14 +445,26 @@
PanStartY = e.ClientY - TranslateY;
}
private void HandleMouseMove(MouseEventArgs e)
private async Task 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)
{
TranslateX = e.ClientX - PanStartX;
TranslateY = e.ClientY - PanStartY;
StateHasChanged();
}
StateHasChanged();
}
private void HandleMouseUp(MouseEventArgs e)
@ -309,6 +475,9 @@
private void HandleMouseLeave(MouseEventArgs e)
{
IsPanning = false;
MouseWorldX = null;
MouseWorldY = null;
StateHasChanged();
}
private async Task HandleWheel(WheelEventArgs e)
@ -324,7 +493,7 @@
if (Math.Abs(ZoomScale - oldZoom) < 0.001) return;
// Zoom at mouse position
var svgRect = await JS.InvokeAsync<ElementBoundingRect>("getElementBoundingRect", SvgRef);
var svgRect = await JS.InvokeAsync<ElementBoundingRect>("robotMonitor.getElementBoundingRect", SvgRef);
double mouseX = e.ClientX - svgRect.X;
double mouseY = e.ClientY - svgRect.Y;

View File

@ -54,19 +54,39 @@
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 {
flex: 1;
overflow: hidden;
position: relative;
background-color: #fafafa;
background-color: #808080;
}
.svg-container svg {
width: 100%;
height: 100%;
cursor: grab;
background-color: dimgray;
}
.svg-container svg {
width: 100%;
height: 100%;
cursor: grab;
background-color: #808080;
}
.svg-container svg:active {
cursor: grabbing;

View File

@ -13,7 +13,7 @@
<!-- Header Dashboard -->
<MudPaper Class="pa-6 mb-4 d-flex align-center justify-space-between" Elevation="3">
<div>
<MudText Typo="Typo.h3" Class="mb-2" >Robot Dashboard</MudText>
<MudText Typo="Typo.h3" Class="mb-2">Robot Dashboard</MudText>
@if (CurrentState != null)
{
<MudText Typo="Typo.subtitle1">
@ -37,17 +37,15 @@
@if (CurrentState != null)
{
<MudChip T="string"
Icon="@(IsConnected
? Icons.Material.Filled.CheckCircle
: Icons.Material.Filled.Error)"
Size="Size.Large"
Color="@(IsConnected ? Color.Success : Color.Error)"
Variant="Variant.Filled"
Class="px-6 py-4 text-white"
Style="font-weight: bold; font-size: 1.1rem;">
@(IsConnected ? "ONLINE" : "OFFLINE")
</MudChip>
}
Icon="@(IsConnected ? Icons.Material.Filled.CheckCircle : Icons.Material.Filled.Error)"
Size="Size.Large"
Color="@(IsConnected ? Color.Success : Color.Error)"
Variant="Variant.Filled"
Class="px-6 py-4 text-white"
Style="font-weight: bold; font-size: 1.1rem;">
@(IsConnected ? "ONLINE" : "OFFLINE")
</MudChip>
}
</MudPaper>
@ -152,7 +150,7 @@
Rounded="true"
Striped="true"
Color="@(msg.BatteryState.BatteryCharge > 50 ? Color.Success :
msg.BatteryState.BatteryCharge > 20 ? Color.Warning : Color.Error)"
msg.BatteryState.BatteryCharge > 20 ? Color.Warning : Color.Error)"
Class="mb-4"
Style="height: 28px;" />
@ -266,13 +264,13 @@
<MudChip T="string"
Size="Size.Small"
Color="@(context.Level.Contains("ERROR", StringComparison.OrdinalIgnoreCase) ? Color.Error :
context.Level.Contains("WARN", StringComparison.OrdinalIgnoreCase) ? Color.Warning : Color.Info)"
context.Level.Contains("WARN", StringComparison.OrdinalIgnoreCase) ? Color.Warning : Color.Info)"
Variant="@Variant.Filled">
@context.Level
</MudChip>
</MudTd>
<MudTd>
<MudText Typo="Typo.body2" Class="text-truncate" Style="max-width: 300px;" Title="@context.Description">
<MudText Typo="Typo.body2" Class="text-truncate" Style="max-width: 300px;">
@context.Description
</MudText>
</MudTd>
@ -311,14 +309,14 @@
<MudTh Style="text-align:right">Status</MudTh>
</HeaderContent>
<RowTemplate>
<MudTd><MudText Typo="Typo.body2" Class="text-truncate" Title="@context.ActionType">@context.ActionType</MudText></MudTd>
<MudTd><MudText Typo="Typo.body2" Class="text-truncate">@context.ActionType</MudText></MudTd>
<MudTd><MudText Typo="Typo.caption">@context.ActionId</MudText></MudTd>
<MudTd Style="text-align:right">
<MudChip T="string"
Size="Size.Small"
Color="@(context.ActionStatus == "RUNNING" ? Color.Info :
context.ActionStatus == "FINISHED" ? Color.Success :
context.ActionStatus == "FAILED" ? Color.Error : Color.Default)"
context.ActionStatus == "FINISHED" ? Color.Success :
context.ActionStatus == "FAILED" ? Color.Error : Color.Default)"
Variant="@Variant.Filled">
@context.ActionStatus
</MudChip>
@ -373,45 +371,34 @@
private StateMsg? CurrentState;
private bool IsConnected;
private readonly string RobotSerial = "T800-002";
private List<MessageRow> MessageRows = new();
protected override async Task OnInitializedAsync()
protected override async Task OnAfterRenderAsync(bool firstRender)
{
await base.OnAfterRenderAsync(firstRender);
if (!firstRender) return;
RobotStateClient.OnStateReceived += OnRobotStateReceived;
RobotStateClient.OnRobotConnectionChanged += OnRobotConnectionChanged;
if (RobotStateClient.ConnectionState == RobotClientState.Disconnected)
{
await RobotStateClient.StartAsync();
}
await RobotStateClient.SubscribeRobotAsync(RobotSerial);
CurrentState = RobotStateClient.GetLatestState(RobotSerial);
await RobotStateClient.StartAsync();
CurrentState = RobotStateClient.GetLatestState();
IsConnected = RobotStateClient.IsRobotConnected;
UpdateMessageRows();
}
private void OnRobotConnectionChanged(bool connected)
{
InvokeAsync(() =>
{
IsConnected = connected;
StateHasChanged();
});
IsConnected = connected;
StateHasChanged();
}
private void OnRobotStateReceived(string serialNumber, StateMsg state)
{
if (serialNumber != RobotSerial) return;
InvokeAsync(() =>
{
CurrentState = state;
UpdateMessageRows();
StateHasChanged();
});
CurrentState = state;
UpdateMessageRows();
StateHasChanged();
}
private void UpdateMessageRows()

View File

@ -38,8 +38,7 @@
<MudIconButton Icon="@Icons.Material.Filled.Delete"
Color="Color.Error"
Size="Size.Small"
OnClick="@(() => RemoveEdgeAsync(edge))"
StopPropagation="true" />
OnClick="@(() => RemoveEdgeAsync(edge))" />
</div>
</TitleContent>
@ -88,47 +87,6 @@
}
</MudSelect>
</MudItem>
@* <!-- Radius -->
<MudItem xs="6">
<MudNumericField T="double"
Value="@edge.Radius"
ValueChanged="@((double v) => SetValue(() => edge.Radius = v))"
Immediate="true"
Min="0"
Step="0.1"
Label="Radius (0 = straight line)" />
</MudItem>
<!-- Quadrant -->
@if (edge.Radius > 0)
{
<MudItem xs="6">
<MudSelect T="Quadrant"
Value="@edge.Quadrant"
ValueChanged="@((Quadrant v) => SetValue(() => edge.Quadrant = v))"
Label="Quadrant">
<MudSelectItem Value="Quadrant.I">I</MudSelectItem>
<MudSelectItem Value="Quadrant.II">II</MudSelectItem>
<MudSelectItem Value="Quadrant.III">III</MudSelectItem>
<MudSelectItem Value="Quadrant.IV">IV</MudSelectItem>
</MudSelect>
</MudItem>
}
<!-- Apply Curve -->
@if (!edge.HasTrajectory && edge.Radius > 0 && !edge.Expanded)
{
<MudItem xs="12">
<MudButton Color="Color.Primary"
Variant="Variant.Outlined"
StartIcon="@Icons.Material.Filled.Merge"
OnClick="@(() => ApplyCurveAsync(edge))">
Apply Curve (generate node)
</MudButton>
</MudItem>
} *@
</MudGrid>
</ChildContent>
</MudExpansionPanel>
@ -141,8 +99,7 @@
[Parameter] public OrderMessage Order { get; set; } = default!;
[Parameter] public EventCallback OnAddEdge { get; set; }
[Parameter] public EventCallback<UiEdge> OnRemoveEdge { get; set; }
[Parameter] public EventCallback<UiEdge> OnApplyCurve { get; set; }
[Parameter] public EventCallback<VDA5050.Order.Edge> OnRemoveEdge { get; set; }
[Parameter] public EventCallback OnOrderChanged { get; set; }
@ -158,15 +115,9 @@
await OnOrderChanged.InvokeAsync();
}
private async Task RemoveEdgeAsync(UiEdge edge)
private async Task RemoveEdgeAsync(VDA5050.Order.Edge edge)
{
await OnRemoveEdge.InvokeAsync(edge);
await OnOrderChanged.InvokeAsync();
}
private async Task ApplyCurveAsync(UiEdge edge)
{
await OnApplyCurve.InvokeAsync(edge);
await OnOrderChanged.InvokeAsync();
}
}

View File

@ -132,17 +132,17 @@
try
{
using var doc = JsonDocument.Parse(JsonText);
var root = doc.RootElement;
// ===== BASIC STRUCTURE CHECK =====
if (!root.TryGetProperty("nodes", out _) ||
!root.TryGetProperty("edges", out _))
var order = JsonSerializer.Deserialize<OrderMsg>(JsonText, new JsonSerializerOptions
{
throw new Exception("Missing 'nodes' or 'edges' field.");
WriteIndented = true,
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);
MudDialog.Close(DialogResult.Ok(order));
@ -160,15 +160,15 @@
}
// ================= DOMAIN VALIDATION =================
private void ValidateOrder(OrderMessage order)
private void ValidateOrder(OrderMsg order)
{
if (order.Nodes.Count == 0)
if (order.Nodes.Length == 0)
throw new Exception("Order must contain at least one node.");
if (order.Nodes.Count != order.Edges.Count + 1)
if (order.Nodes.Length != order.Edges.Length + 1)
throw new Exception(
$"Invalid path structure: Nodes count ({order.Nodes.Count}) " +
$"must equal Edges count + 1 ({order.Edges.Count + 1})."
$"Invalid path structure: Nodes count ({order.Nodes.Length}) " +
$"must equal Edges count + 1 ({order.Edges.Length + 1})."
);
var nodeIds = order.Nodes

View File

@ -1,7 +1,7 @@
<MudPaper Class="pa-4 h-100 d-flex flex-column overflow-hidden" Elevation="2">
<MudStack Row AlignItems="AlignItems.Center" Justify="Justify.SpaceBetween"
Class="mb-4 flex-shrink-0">
<MudText Typo="Typo.h6">📄 JSON Output (/order)</MudText>
<MudText Typo="Typo.h6">Output (/order)</MudText>
<div class="d-flex gap-2">

View File

@ -21,13 +21,11 @@
<MudIconButton Icon="@Icons.Material.Filled.Edit"
Color="Color.Primary"
Size="Size.Small"
OnClick="@(() => EditNodeAsync(node))"
StopPropagation />
OnClick="@(() => EditNodeAsync(node))" />
<MudIconButton Icon="@Icons.Material.Filled.Delete"
Color="Color.Error"
Size="Size.Small"
OnClick="@(() => RemoveNodeAsync(node))"
StopPropagation />
OnClick="@(() => RemoveNodeAsync(node))" />
</div>
</div>
</TitleContent>
@ -78,22 +76,22 @@
Label="Y" />
</MudItem>
<MudItem xs="12">
@* <MudItem xs="12">
<MudTextField Value="@node.NodePosition.MapId"
ValueChanged="@((string v) => SetValue(() => node.NodePosition.MapId = v))"
Immediate="true"
Label="Map ID" />
</MudItem>
</MudItem> *@
<MudItem xs="6">
@* <MudItem xs="6">
<MudNumericField T="double"
Value="@node.NodePosition.Theta"
ValueChanged="@((double v) => SetValue(() => node.NodePosition.Theta = v))"
Immediate="true"
Label="Theta (rad)" />
</MudItem>
</MudItem> *@
<MudItem xs="6">
@* <MudItem xs="6">
<MudNumericField T="double"
Value="@node.NodePosition.AllowedDeviationXY"
ValueChanged="@((double v) => SetValue(() => node.NodePosition.AllowedDeviationXY = v))"
@ -107,7 +105,7 @@
ValueChanged="@((double v) => SetValue(() => node.NodePosition.AllowedDeviationTheta = v))"
Immediate="true"
Label="Allowed Dev Theta" />
</MudItem>
</MudItem> *@
<!-- Actions -->
<MudItem xs="12">

View File

@ -37,7 +37,6 @@
<EdgesPanel Order="Order"
OnAddEdge="AddEdge"
OnRemoveEdge="RemoveEdge"
OnApplyCurve="ApplyCurve"
OnOrderChanged="OnOrderChanged" />
</MudItem>
@ -84,9 +83,10 @@
new JsonSerializerOptions
{
WriteIndented = true,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
});
}
private async Task OpenImportDialog()
{
var dialog = await DialogService.ShowAsync<ImportOrderDialog>(
@ -99,9 +99,9 @@
var result = await dialog.Result;
if (!result.Canceled && result.Data is OrderMessage imported)
if (result is not null && !result.Canceled && result.Data is OrderMsg imported)
{
Order = imported;
Order.Import(imported);
RebuildOrderJson();
StateHasChanged();
}
@ -149,7 +149,7 @@
? Order.Nodes[1].NodeId
: start; // 👈 1 node thì start = end
Order.Edges.Add(new UiEdge
Order.Edges.Add(new VDA5050.Order.Edge
{
EdgeId = $"EDGE_{Order.Edges.Count + 1}",
StartNodeId = start,
@ -158,25 +158,10 @@
}
void RemoveEdge(UiEdge edge)
void RemoveEdge(VDA5050.Order.Edge edge)
{
Order.Edges.Remove(edge);
}
void ApplyCurve(UiEdge edge)
{
if (edge.Radius <= 0 || edge.Expanded) return;
var startNode = Order.Nodes.First(n => n.NodeId == edge.StartNodeId);
var newNode = OrderMessage.CreateCurveNode(startNode, edge);
Order.Nodes.Add(newNode);
edge.EndNodeId = newNode.NodeId;
edge.MarkExpanded(); // ✅
ResequenceNodes();
}
// ================= ACTION =================
void AddAction(Node node)
{
@ -193,22 +178,19 @@
void RemoveAction(Node node, VDA5050.InstantAction.Action action)
{
node.Actions = node.Actions?.Where(a => a != action).ToArray()
?? Array.Empty<VDA5050.InstantAction.Action>();
node.Actions = node.Actions?.Where(a => a != action).ToArray() ?? [];
}
void AddActionParameter(VDA5050.InstantAction.Action act)
{
var list = (act.ActionParameters ?? Array.Empty<ActionParameter>()).ToList();
var list = (act.ActionParameters ?? []).ToList();
list.Add(new UiActionParameter());
act.ActionParameters = list.ToArray();
}
void RemoveActionParameter(VDA5050.InstantAction.Action act, ActionParameter param)
{
act.ActionParameters =
act.ActionParameters?.Where(p => p != param).ToArray()
?? Array.Empty<ActionParameter>();
act.ActionParameters = act.ActionParameters?.Where(p => p != param).ToArray() ?? [];
}
// ================= SEND / COPY =================
@ -220,7 +202,12 @@
try
{
var orderMsg = JsonSerializer.Deserialize<OrderMsg>(OrderJson);
var orderMsg = JsonSerializer.Deserialize<OrderMsg>(OrderJson,
new JsonSerializerOptions
{
WriteIndented = true,
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
});
if (orderMsg is null)
{
Snackbar.Add("Unable to convert JSON to Order", Severity.Warning);
@ -251,20 +238,17 @@
}
}
var response = await Http.PostAsJsonAsync(
"/api/order",
JsonSerializer.Deserialize<OrderMsg>(OrderJson)
);
var response = await Http.PostAsJsonAsync("/api/order",orderMsg);
sendSuccess = response.IsSuccessStatusCode;
}
catch(JsonException jsonEx)
{
Snackbar.Add($"Json to Order failed: {jsonEx.Message}", Severity.Warning);
}
catch
catch (Exception ex)
{
Snackbar.Add($"Send Order failed: {ex.Message}", Severity.Warning);
sendSuccess = false;
}
@ -316,7 +300,7 @@
_copyCts?.Cancel();
_copyCts = new();
await JS.InvokeVoidAsync("navigator.clipboard.writeText", OrderJson);
await JS.InvokeVoidAsync("copyToClipboardFallback", OrderJson);
copied = true;
StateHasChanged();

View File

@ -17,6 +17,7 @@
protected override async Task OnAfterRenderAsync(bool firstRender)
{
await base.OnAfterRenderAsync(firstRender);
if (firstRender)
{
MonitorService.OnDataReceived += OnMonitorDataReceived;
@ -28,7 +29,8 @@
{
_monitorData = data;
RobotMonitorViewRef?.UpdatePath();
InvokeAsync(StateHasChanged);
RobotMonitorViewRef?.OnMonitorDataUpdated();
StateHasChanged();
}
public async ValueTask DisposeAsync()
@ -41,3 +43,9 @@

View File

@ -107,7 +107,7 @@ public sealed class RobotStateClient : IAsyncDisposable
// ================= SIGNALR HANDLERS =================
// VDA5050 State
_connection.On<string>("ReceiveState", HandleState);
_connection.On<StateMsg>("ReceiveState", HandleState);
// Robot connection (bool only)
_connection.On<bool>("ReceiveRobotConnection", HandleRobotConnection);
@ -125,22 +125,8 @@ public sealed class RobotStateClient : IAsyncDisposable
}
// ================= HANDLE STATE =================
private void HandleState(string stateJson)
private void HandleState(StateMsg state)
{
StateMsg? state;
try
{
state = JsonSerializer.Deserialize<StateMsg>(
stateJson,
JsonOptionExtends.Read
);
}
catch
{
return;
}
if (state?.SerialNumber == null)
return;
@ -189,10 +175,10 @@ public sealed class RobotStateClient : IAsyncDisposable
}
// ================= GET CACHE =================
public StateMsg? GetLatestState(string serialNumber)
public StateMsg? GetLatestState()
{
LatestStates.TryGetValue(serialNumber, out var state);
return state;
if (!LatestStates.IsEmpty) return LatestStates.First().Value;
return null;
}
// ================= DISPOSE =================

View File

@ -1,419 +1,33 @@
using RobotApp.VDA5050.InstantAction;
using RobotApp.VDA5050.Order;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace RobotApp.Client.Services;
// ======================================================
// EDGE UI
// ======================================================
public class UiEdge
{
public string EdgeId { get; set; } = "";
public int SequenceId { get; set; }
public bool Released { get; set; } = true;
public string StartNodeId { get; set; } = "";
public string EndNodeId { get; set; } = "";
// ===== CURVE (EDITOR GENERATED) =====
public double Radius { get; set; } = 0;
public Quadrant Quadrant { get; set; }
// ===== IMPORTED TRAJECTORY =====
public bool HasTrajectory { get; set; } = false;
public UiTrajectory? Trajectory { get; set; }
// ===== UI STATE =====
public bool Expanded { get; private set; } = false;
public void MarkExpanded()
{
Expanded = true;
}
}
public class UiTrajectory
{
public int Degree { get; set; }
public double[] KnotVector { get; set; } = Array.Empty<double>();
public List<Point> ControlPoints { get; set; } = new();
}
public enum Quadrant
{
I,
II,
III,
IV
}
// ======================================================
// GEOMETRY MODELS
// ======================================================
public record Point(double X, double Y);
public record QuarterResult(
Point EndPoint,
object Trajectory
);
// ======================================================
// GEOMETRY HELPER (QUARTER CIRCLE)
// ======================================================
public static class QuarterGeometry
{
private const double K = 0.5522847498307936;
public static QuarterResult BuildQuarterTrajectory(
Point A,
double r,
Quadrant q
)
{
Point P1, P2, C;
switch (q)
{
case Quadrant.I:
P1 = new(A.X, A.Y + K * r);
P2 = new(A.X + K * r, A.Y + r);
C = new(A.X + r, A.Y + r);
break;
case Quadrant.II:
P1 = new(A.X - K * r, A.Y);
P2 = new(A.X - r, A.Y + K * r);
C = new(A.X - r, A.Y + r);
break;
case Quadrant.III:
P1 = new(A.X, A.Y - K * r);
P2 = new(A.X - K * r, A.Y - r);
C = new(A.X - r, A.Y - r);
break;
case Quadrant.IV:
P1 = new(A.X + K * r, A.Y);
P2 = new(A.X + r, A.Y - K * r);
C = new(A.X + r, A.Y - r);
break;
default:
throw new ArgumentOutOfRangeException(nameof(q));
}
return new QuarterResult(
C,
new
{
degree = 3,
knotVector = new[] { 0, 0, 0, 0, 1, 1, 1, 1 },
controlPoints = new[]
{
new { x = A.X, y = A.Y }, // P0
new { x = P1.X, y = P1.Y }, // P1
new { x = P2.X, y = P2.Y }, // P2
new { x = C.X, y = C.Y } // P3
}
}
);
}
}
// ======================================================
// ORDER MESSAGE
// ======================================================
public class OrderMessage
{
public int HeaderId { get; set; }
public string Timestamp { get; set; } = "";
public string Version { get; set; } = "v1";
public string Manufacturer { get; set; } = "PNKX";
public string SerialNumber { get; set; } = "T800-002";
public string OrderId { get; set; }
public uint HeaderId { get; set; }
public string Timestamp { get; set; } = DateTime.UtcNow.ToString("yyyy-MM-ddTHH:mm:ss.fffZ");
public string Version { get; set; } = "2.1.0";
public string Manufacturer { get; set; } = "PhenikaaX";
public string SerialNumber { get; set; } = "T800-003";
public string OrderId { get; set; } = "";
public int OrderUpdateId { get; set; }
public string? ZoneSetId { get; set; }
public List<Node> Nodes { get; set; } = new();
public List<UiEdge> Edges { get; set; } = new();
public static Node CreateCurveNode(Node startNode, UiEdge edge)
{
var A = new Point(
startNode.NodePosition.X,
startNode.NodePosition.Y
);
public List<Node> Nodes { get; set; } = [];
public List<Edge> Edges { get; set; } = [];
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()
{
// ================= 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
{
HeaderId = (uint)HeaderId++,
Timestamp = string.IsNullOrWhiteSpace(Timestamp)
? DateTime.UtcNow.ToString("O")
: Timestamp,
Timestamp = DateTime.UtcNow.ToString("yyyy-MM-ddTHH:mm:ss.fffZ"),
Version = Version,
Manufacturer = Manufacturer,
@ -426,10 +40,24 @@ public class OrderMessage
? null
: ZoneSetId,
Nodes = nodeObjects,
Edges = edgeObjects,
Nodes = [..Nodes],
Edges = [..Edges],
};
}
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.

After

Width:  |  Height:  |  Size: 25 KiB

View File

@ -1 +1,10 @@

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);
}

View File

@ -55,8 +55,31 @@ window.robotMonitor = {
}
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;
});
}
};

View File

@ -2,7 +2,6 @@
namespace RobotApp.VDA5050.Order;
#nullable disable
public class Edge
{
@ -26,7 +25,7 @@ public class Edge
public bool RotationAllowed { get; set; }
public double MaxRotationSpeed { get; set; }
public double Length { get; set; }
public Trajectory Trajectory { get; set; }
public Trajectory? Trajectory { get; set; }
public Corridor Corridor { get; set; } = new();
[Required]
public InstantAction.Action[] Actions { get; set; } = [];

View File

@ -13,5 +13,5 @@ public class EdgeState
public string EdgeDescription { get; set; } = string.Empty;
[Required]
public bool Released { get; set; }
public Trajectory Trajectory { get; set; } = new();
public Trajectory? Trajectory { get; set; }
}

View File

@ -21,7 +21,7 @@ public enum ActionType
//liftUp,
//liftDown,
//liftRotate,
//rotate,
rotate,
//rotateKeepLift,
//mutedBaseOn,
//mutedBaseOff,

View File

@ -42,6 +42,7 @@
<script src="@Assets["_content/MudBlazor/MudBlazor.min.js"]"></script>
<script src="@Assets["js/canvas.js"]"></script>
<script src="@Assets["js/app.js"]"></script>
<script src="@Assets["js/robotMonitor.js"]"></script>
</body>
</html>

View File

@ -28,7 +28,6 @@ public class OrderController(IOrder robotOrderController, IInstantActions instan
{
robotOrderController.StopOrder();
instantActions.StopOrderAction();
return Ok(new
{
success = true,

View File

@ -21,8 +21,8 @@ namespace RobotApp.Hubs
// Phương thức này sẽ được gọi từ service để broadcast
public async Task SendState(string serialNumber, StateMsg state)
{
var json = JsonSerializer.Serialize(state, JsonOptionExtends.Write);
await Clients.Group(serialNumber).SendAsync("ReceiveState", json);
//var json = JsonSerializer.Serialize(state, JsonOptionExtends.Write);
await Clients.Group(serialNumber).SendAsync("ReceiveState", state);
}
}
}

View File

@ -12,3 +12,9 @@ public class RobotMonitorHub : Hub

View File

@ -63,7 +63,7 @@ builder.Services.AddRobot();
builder.Services.AddSingleton<RobotApp.Services.RobotMonitorService>();
builder.Services.AddHostedService(sp => sp.GetRequiredService<RobotApp.Services.RobotMonitorService>());
builder.Services.AddScoped<RobotStateClient>();
//builder.Services.AddScoped<RobotStateClient>();
builder.Services.AddHostedService<RobotStatePublisher>();
var app = builder.Build();

View File

@ -21,7 +21,7 @@ public class RobotActionStorage(IServiceProvider ServiceProvider)
ActionType.drop => new RobotDropAction(ServiceProvider),
ActionType.pick => new RobotPickAction(ServiceProvider),
//ActionType.liftRotate => new RobotLiftRotateAction(ServiceProvider),
//ActionType.rotate => new RobotRotateAction(ServiceProvider),
ActionType.rotate => new RobotRotateAction(ServiceProvider),
//ActionType.rotateKeepLift => new RobotRotateKeepLift(ServiceProvider),
//ActionType.mutedBaseOn => new RobotMutedBaseOnAction(ServiceProvider),
//ActionType.mutedBaseOff => new RobotMutedBaseOffAction(ServiceProvider),

View File

@ -26,12 +26,16 @@ public class RobotConnection(RobotConfiguration RobotConfiguration,
{
//Logger.Debug($"Nhận Order: {data}");
var msg = JsonSerializer.Deserialize<OrderMsg>(data, JsonOptionExtends.Read);
if (msg is null || string.IsNullOrEmpty(msg.SerialNumber) || msg.SerialNumber != RobotConfiguration.SerialNumber) return;
if (msg is null || string.IsNullOrEmpty(msg.SerialNumber) || msg.SerialNumber != RobotConfiguration.SerialNumber)
{
Logger.Warning($"SerialNumber cuả order không hợp lệ: message SerialNumber {msg?.SerialNumber}");
return;
}
OrderUpdated?.Invoke(msg);
}
catch (Exception ex)
{
Logger.Warning($"Nhận Order xảy ra lỗi: {ex.Message} - {ex.StackTrace}");
Logger.Warning($"Nhận Order xảy ra lỗi: {ex.Message}");
}
}
@ -41,12 +45,16 @@ public class RobotConnection(RobotConfiguration RobotConfiguration,
{
//Logger.Debug($"Nhận InstanceActions: {data}");
var msg = JsonSerializer.Deserialize<InstantActionsMsg>(data, JsonOptionExtends.Read);
if (msg is null || string.IsNullOrEmpty(msg.SerialNumber) || msg.SerialNumber != RobotConfiguration.SerialNumber) return;
if (msg is null || string.IsNullOrEmpty(msg.SerialNumber) || msg.SerialNumber != RobotConfiguration.SerialNumber)
{
Logger.Warning($"SerialNumber của action không hợp lệ: message SerialNumber {msg?.SerialNumber}");
return;
}
ActionUpdated?.Invoke(msg);
}
catch (Exception ex)
{
Logger.Warning($"Nhận InstanceActions xảy ra lỗi: {ex.Message} - {ex.StackTrace}");
Logger.Warning($"Nhận InstanceActions xảy ra lỗi: {ex.Message}");
}
}

View File

@ -105,7 +105,10 @@ 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}");
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}");
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()
=> 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)");

View File

@ -25,7 +25,7 @@ public class RobotFactsheet(RobotConnection RobotConnection, RobotConfiguration
{ ActionType.pick, Pick},
{ ActionType.drop, Drop},
//{ ActionType.liftRotate, LiftRotate},
//{ ActionType.rotate, Rotate},
{ ActionType.rotate, Rotate},
//{ ActionType.rotateKeepLift, RotateKeepLift},
//{ ActionType.mutedBaseOn, MutedBaseOn},
//{ ActionType.mutedBaseOff, MutedBaseOff},
@ -224,22 +224,22 @@ public class RobotFactsheet(RobotConnection RobotConnection, RobotConfiguration
// BlockingTypes = [BlockingType.HARD.ToString()],
//};
//public readonly static AgvAction Rotate = new()
//{
// ActionType = ActionType.rotate.ToString(),
// ActionDescription = "Xoay robot tại chỗ.",
// ActionScopes = [ActionScopes.INSTANT.ToString(), ActionScopes.NODE.ToString()],
// ActionParameters = [
// new()
// {
// Key = "angle",
// Description = "Góc xoay của robot. (rad)",
// ValueDataType = ValueDataType.FLOAT.ToString(),
// IsOptional = false,
// }],
// ResultDescription = "Robot đã xoay tại chỗ.",
// BlockingTypes = [BlockingType.HARD.ToString()],
//};
public readonly static AgvAction Rotate = new()
{
ActionType = ActionType.rotate.ToString(),
ActionDescription = "Xoay robot tại chỗ.",
ActionScopes = [ActionScopes.INSTANT.ToString(), ActionScopes.NODE.ToString()],
ActionParameters = [
new()
{
Key = "angle",
Description = "Góc xoay của robot. (rad)",
ValueDataType = ValueDataType.FLOAT.ToString(),
IsOptional = false,
}],
ResultDescription = "Robot đã xoay tại chỗ.",
BlockingTypes = [BlockingType.HARD.ToString()],
};
//public readonly static AgvAction RotateKeepLift = new()
//{

View File

@ -3,7 +3,7 @@ using RobotApp.VDA5050.State;
namespace RobotApp.Services.Robot;
public class RobotLoads(IPeripheral PeriperalManager) : ILoad
public class RobotLoads() : ILoad
{
//public Load[] Load => PeriperalManager.HasLoad ? [GetLoad()] : [];
public Load[] Load { get; private set; } = [];

View File

@ -274,32 +274,37 @@ public class RobotLocalization(RobotConfiguration RobotConfiguration, Simulation
{
try
{
var xyzw = QuaternionToXYZW(0, 0, theta);
var response = XlocClient.SetInitialPose(new SetInitialPoseRequest()
{
InitialPose = new Pose()
{
Position = new Point()
{
X = x,
Y = y,
Z = 0,
},
Orientation = new Quaternion()
{
X = xyzw.x,
Y = xyzw.y,
Z = xyzw.z,
W = xyzw.w
}
}
});
if (response.Status.Code == StatusResponse.Types.StatusCode.Ok) return new(true);
if (IsSimulation) SimVisualization.LocalizationInitialize(x, y, theta);
else
{
Logger.Warning("Khởi tạo vị trí cho robot thất bại. Kết quả trả về: {response.Status.Code} - {response.Status.Message}");
return new(false, "Khởi tạo vị trí cho robot thất bại");
var xyzw = QuaternionToXYZW(0, 0, theta);
var response = XlocClient.SetInitialPose(new SetInitialPoseRequest()
{
InitialPose = new Pose()
{
Position = new Point()
{
X = x,
Y = y,
Z = 0,
},
Orientation = new Quaternion()
{
X = xyzw.x,
Y = xyzw.y,
Z = xyzw.z,
W = xyzw.w
}
}
});
if (response.Status.Code == StatusResponse.Types.StatusCode.Ok) return new(true);
else
{
Logger.Warning("Khởi tạo vị trí cho robot thất bại. Kết quả trả về: {response.Status.Code} - {response.Status.Message}");
return new(false, "Khởi tạo vị trí cho robot thất bại");
}
}
return new(true);
}
catch (Exception ex)
{

View File

@ -9,6 +9,7 @@ using RobotApp.VDA5050.Order;
using RobotApp.VDA5050.State;
using System.Collections.Concurrent;
using System.Data;
using System.Xml.Linq;
using Action = RobotApp.VDA5050.InstantAction.Action;
namespace RobotApp.Services.Robot;
@ -247,11 +248,6 @@ public class RobotOrderController(INavigation NavigationManager,
UpdateState();
}
private bool IsNewPath()
{
return true;
}
private void ClearLastNode()
{
if (LastNode is null) return;
@ -292,6 +288,7 @@ public class RobotOrderController(INavigation NavigationManager,
private void HandleOrder()
{
if (Nodes.Length <= 0) return;
if (IsCancelOrder)
{
NavigationManager.CancelMovement();
@ -309,9 +306,13 @@ public class RobotOrderController(INavigation NavigationManager,
{
var action = FinalAction[0];
var robotAction = ActionManager[action.ActionId];
if (robotAction is null) return;
if (robotAction.IsCompleted) FinalAction.Remove(action);
if (robotAction.Status == ActionStatus.WAITING) ActionManager.StartOrderAction(action.ActionId);
if (robotAction is null)
{
FinalAction.Remove(action);
return;
}
if (robotAction.IsCompleted)
if (robotAction.Status == ActionStatus.WAITING) ActionManager.StartOrderAction(action.ActionId);
}
else
{
@ -398,6 +399,18 @@ public class RobotOrderController(INavigation NavigationManager,
if (NodeStates.Length != 0 || EdgeStates.Length != 0) HandleUpdateOrder(NewOrderHandler);
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);
}
}
@ -409,6 +422,7 @@ public class RobotOrderController(INavigation NavigationManager,
{
ErrorManager.AddError(orEx.Error, TimeSpan.FromSeconds(10));
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}");
}
@ -465,17 +479,23 @@ public class RobotOrderController(INavigation NavigationManager,
var edges = NewOrderEdges.ToList().GetRange(lastNodeIndex + 1, nodes.Count - 1);
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()
{
StartX = nodes[i].NodePosition.X,
StartY = nodes[i].NodePosition.Y,
EndX = nodes[i + 1].NodePosition.X,
EndY = nodes[i + 1].NodePosition.Y,
ControlPoint1X = edges[i].Trajectory is not null && edges[i].Trajectory.ControlPoints.Length > 2 ? edges[i].Trajectory.ControlPoints[1].X : 0,
ControlPoint1Y = edges[i].Trajectory is not null && edges[i].Trajectory.ControlPoints.Length > 2 ? edges[i].Trajectory.ControlPoints[1].Y : 0,
ControlPoint2X = edges[i].Trajectory is not null && edges[i].Trajectory.ControlPoints.Length > 3 ? edges[i].Trajectory.ControlPoints[2].X : 0,
ControlPoint2Y = edges[i].Trajectory is not null && edges[i].Trajectory.ControlPoints.Length > 3 ? edges[i].Trajectory.ControlPoints[2].Y : 0,
Degree = edges[i].Trajectory.Degree,
ControlPoint1X = controlPoints is { Length: > 2 } ? controlPoints[1].X : 0,
ControlPoint1Y = controlPoints is { Length: > 2 } ? controlPoints[1].Y : 0,
ControlPoint2X = controlPoints is { Length: > 3 } ? controlPoints[2].X : 0,
ControlPoint2Y = controlPoints is { Length: > 3 } ? controlPoints[2].Y : 0,
Degree = trajectory is null ? 1 : trajectory.Degree,
});
}
}

View File

@ -21,11 +21,11 @@ public class RobotPathPlanner(IConfiguration Configuration)
Y1 = inNode.NodePosition.Y,
X2 = futureNode.NodePosition.X,
Y2 = futureNode.NodePosition.Y,
ControlPoint1X = edge.Trajectory.ControlPoints.Length > 2 ? edge.Trajectory.ControlPoints[1].X : 0,
ControlPoint1Y = edge.Trajectory.ControlPoints.Length > 2 ? edge.Trajectory.ControlPoints[1].Y : 0,
ControlPoint2X = edge.Trajectory.ControlPoints.Length > 3 ? edge.Trajectory.ControlPoints[2].X : 0,
ControlPoint2Y = edge.Trajectory.ControlPoints.Length > 3 ? edge.Trajectory.ControlPoints[2].Y : 0,
TrajectoryDegree = edge.Trajectory.Degree == 1 ? TrajectoryDegree.One : edge.Trajectory.Degree == 2 ? TrajectoryDegree.Two : TrajectoryDegree.Three,
ControlPoint1X = edge.Trajectory is not null && 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,
ControlPoint2X = edge.Trajectory is not null && 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,
TrajectoryDegree = edge.Trajectory is null ? TrajectoryDegree.One : edge.Trajectory.Degree == 1 ? TrajectoryDegree.One : edge.Trajectory.Degree == 2 ? TrajectoryDegree.Two : TrajectoryDegree.Three,
});
(double robotx, double roboty) =
(
@ -61,30 +61,46 @@ public class RobotPathPlanner(IConfiguration Configuration)
navigationNodes[0].Direction = GetDirectionInNode(currentTheta, nodes[0], nodes[1], edges[0]);
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()
{
X1 = nodes[i - 1].NodePosition.X,
Y1 = nodes[i - 1].NodePosition.Y,
X2 = nodes[i].NodePosition.X,
Y2 = nodes[i].NodePosition.Y,
ControlPoint1X = edges[i - 1].Trajectory.ControlPoints.Length > 2 ? edges[i - 1].Trajectory.ControlPoints[1].X : 0,
ControlPoint1Y = edges[i - 1].Trajectory.ControlPoints.Length > 2 ? edges[i - 1].Trajectory.ControlPoints[1].Y : 0,
ControlPoint2X = edges[i - 1].Trajectory.ControlPoints.Length > 3 ? edges[i - 1].Trajectory.ControlPoints[2].X : 0,
ControlPoint2Y = edges[i - 1].Trajectory.ControlPoints.Length > 3 ? edges[i - 1].Trajectory.ControlPoints[2].Y : 0,
TrajectoryDegree = edges[i - 1].Trajectory.Degree == 1 ? TrajectoryDegree.One : edges[i - 1].Trajectory.Degree == 2 ? TrajectoryDegree.Two : TrajectoryDegree.Three,
ControlPoint1X = controlPoints is { Length: > 2 } ? controlPoints[1].X : 0,
ControlPoint1Y = controlPoints is { Length: > 2 } ? controlPoints[1].Y : 0,
ControlPoint2X = controlPoints is { Length: > 3 } ? controlPoints[2].X : 0,
ControlPoint2Y = controlPoints is { Length: > 3 } ? controlPoints[2].Y : 0,
TrajectoryDegree = trajectory?.Degree switch
{
1 => TrajectoryDegree.One,
2 => TrajectoryDegree.Two,
_ => TrajectoryDegree.Three
},
});
trajectory = edges[i].Trajectory;
controlPoints = trajectory?.ControlPoints;
(double futurex, double futurey) = MathExtensions.Curve(0.1, new()
{
X1 = nodes[i].NodePosition.X,
Y1 = nodes[i].NodePosition.Y,
X2 = nodes[i + 1].NodePosition.X,
Y2 = nodes[i + 1].NodePosition.Y,
ControlPoint1X = edges[i].Trajectory.ControlPoints.Length > 2 ? edges[i].Trajectory.ControlPoints[1].X : 0,
ControlPoint1Y = edges[i].Trajectory.ControlPoints.Length > 2 ? edges[i].Trajectory.ControlPoints[1].Y : 0,
ControlPoint2X = edges[i].Trajectory.ControlPoints.Length > 3 ? edges[i].Trajectory.ControlPoints[2].X : 0,
ControlPoint2Y = edges[i].Trajectory.ControlPoints.Length > 3 ? edges[i].Trajectory.ControlPoints[2].Y : 0,
TrajectoryDegree = edges[i].Trajectory.Degree == 1 ? TrajectoryDegree.One : edges[i].Trajectory.Degree == 2 ? TrajectoryDegree.Two : TrajectoryDegree.Three,
ControlPoint1X = controlPoints is { Length: > 2 } ? controlPoints[1].X : 0,
ControlPoint1Y = controlPoints is { Length: > 2 } ? controlPoints[1].Y : 0,
ControlPoint2X = controlPoints is { Length: > 3 } ? controlPoints[2].X : 0,
ControlPoint2Y = controlPoints is { Length: > 3 } ? controlPoints[2].Y : 0,
TrajectoryDegree = trajectory?.Degree switch
{
1 => TrajectoryDegree.One,
2 => TrajectoryDegree.Two,
_ => TrajectoryDegree.Three
},
});
var angle = MathExtensions.GetVectorAngle(
nodes[i].NodePosition.X,
nodes[i].NodePosition.Y,
@ -128,11 +144,11 @@ public class RobotPathPlanner(IConfiguration Configuration)
Y1 = startNode.Y,
X2 = endNode.X,
Y2 = endNode.Y,
ControlPoint1X = edge.Trajectory.ControlPoints.Length > 2 ? edge.Trajectory.ControlPoints[1].X : 0,
ControlPoint1Y = edge.Trajectory.ControlPoints.Length > 2 ? edge.Trajectory.ControlPoints[1].Y : 0,
ControlPoint2X = edge.Trajectory.ControlPoints.Length > 3 ? edge.Trajectory.ControlPoints[2].X : 0,
ControlPoint2Y = edge.Trajectory.ControlPoints.Length > 3 ? edge.Trajectory.ControlPoints[2].Y : 0,
TrajectoryDegree = edge.Trajectory.Degree == 1 ? TrajectoryDegree.One : edge.Trajectory.Degree == 2 ? TrajectoryDegree.Two : TrajectoryDegree.Three
ControlPoint1X = edge.Trajectory is not null && 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,
ControlPoint2X = edge.Trajectory is not null && 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,
TrajectoryDegree = edge.Trajectory is null ? TrajectoryDegree.One : edge.Trajectory.Degree == 1 ? TrajectoryDegree.One : edge.Trajectory.Degree == 2 ? TrajectoryDegree.Two : TrajectoryDegree.Three
};
double length = EdgeCalculatorModel.GetEdgeLength();
@ -194,11 +210,11 @@ public class RobotPathPlanner(IConfiguration Configuration)
Y1 = startNode.NodePosition.Y,
X2 = endNode.NodePosition.X,
Y2 = endNode.NodePosition.Y,
ControlPoint1X = edge.Trajectory.ControlPoints.Length > 2 ? edge.Trajectory.ControlPoints[1].X : 0,
ControlPoint1Y = edge.Trajectory.ControlPoints.Length > 2 ? edge.Trajectory.ControlPoints[1].Y : 0,
ControlPoint2X = edge.Trajectory.ControlPoints.Length > 3 ? edge.Trajectory.ControlPoints[2].X : 0,
ControlPoint2Y = edge.Trajectory.ControlPoints.Length > 3 ? edge.Trajectory.ControlPoints[2].Y : 0,
TrajectoryDegree = edge.Trajectory.Degree == 1 ? TrajectoryDegree.One : edge.Trajectory.Degree == 2 ? TrajectoryDegree.Two : TrajectoryDegree.Three
ControlPoint1X = edge.Trajectory is not null && 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,
ControlPoint2X = edge.Trajectory is not null && 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,
TrajectoryDegree = edge.Trajectory is null ? TrajectoryDegree.One : edge.Trajectory.Degree == 1 ? TrajectoryDegree.One : edge.Trajectory.Degree == 2 ? TrajectoryDegree.Two : TrajectoryDegree.Three
};
double length = EdgeCalculatorModel.GetEdgeLength();

View File

@ -12,164 +12,22 @@ using System.Text.Json;
namespace RobotApp.Services.Robot;
public class RobotStatePublisher : BackgroundService
public class RobotStatePublisher(
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
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)
{
while (await _timer.WaitForNextTickAsync(stoppingToken))
{
try
{
var serialNumber = _robotConfig.SerialNumber;
// ===== SEND STATE =====
var state = GetStateMsg();
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
);
}
await _hubContext.Clients.All.SendAsync("ReceiveState", _robotState.GetStateMsg(), stoppingToken);
await _hubContext.Clients.All.SendAsync("ReceiveRobotConnection", _robotConnection.IsConnected, stoppingToken);
}
catch
{
@ -177,10 +35,10 @@ public class RobotStatePublisher : BackgroundService
}
}
public override void Dispose()
public override Task StopAsync(CancellationToken cancellationToken)
{
_timer?.Dispose();
base.Dispose();
return base.StopAsync(cancellationToken);
}
}

View File

@ -36,7 +36,7 @@ public class RobotStates(RobotConfiguration RobotConfiguration,
catch { }
}
private StateMsg GetStateMsg()
public StateMsg GetStateMsg()
{
return new StateMsg
{

View File

@ -23,8 +23,8 @@ public class SimulationNavigation : INavigation, IDisposable
protected const int CycleHandlerMilliseconds = 50;
private const double Scale = 1;
//private WatchTimer<SimulationNavigation>? NavigationTimer;
private HighPrecisionTimer<SimulationNavigation>? NavigationTimer;
private WatchTimer<SimulationNavigation>? NavigationTimer;
//private HighPrecisionTimer<SimulationNavigation>? NavigationTimer;
protected double TargetAngle = 0;
protected PID? RotatePID;