diff --git a/RobotApp.Client/MainLayout.razor b/RobotApp.Client/MainLayout.razor index 0e76f9d..5af009e 100644 --- a/RobotApp.Client/MainLayout.razor +++ b/RobotApp.Client/MainLayout.razor @@ -72,6 +72,7 @@ new(){Icon = "mdi-view-dashboard", Path="/", Label = "Dashboard", Match = NavLinkMatch.All}, // new(){Icon = "mdi-map-legend", Path="/maps-manager", Label = "Mapping", Match = NavLinkMatch.All}, new(){Icon = "mdi-application-cog", Path="/robot-config", Label = "Config", Match = NavLinkMatch.All}, + new(){Icon = "mdi-application-cog", Path="/robot-state", Label = "state", Match = NavLinkMatch.All}, ]; private bool collapseNavMenu = true; diff --git a/RobotApp.Client/Pages/EditNodeDialog.razor b/RobotApp.Client/Pages/EditNodeDialog.razor new file mode 100644 index 0000000..248c2c2 --- /dev/null +++ b/RobotApp.Client/Pages/EditNodeDialog.razor @@ -0,0 +1,212 @@ +@inherits MudComponentBase +@using RobotApp.VDA5050.InstantAction +@using System.Text.Json +@using System.Text.Json.Serialization +@using RobotApp.VDA5050.Order +@using RobotApp.VDA5050.Type + + + + Edit Node: @Node.NodeId + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Actions + + @foreach (var act in Node.Actions) + { + + + + + + + @foreach (var at in Enum.GetValues()) + { + + @at + + } + + + + + + NONE + SOFT + HARD + + + + + + + + Action Parameters + + @{ + var parameters = act.ActionParameters ?? Array.Empty(); + } + @foreach (var p in parameters.Cast().ToList()) + { + var param = p; // capture cho lambda + + + + + + + + + + + + } + + + Add Parameter + + + + + + Remove Action + + + } + + + Add Action + + + + + + + Cancel + Save + + + +@code { + [CascadingParameter] public IMudDialogInstance MudDialog { get; set; } = default!; + [Parameter] public Node Node { get; set; } = default!; + + private void Cancel() => MudDialog.Cancel(); + private void Submit() => MudDialog.Close(DialogResult.Ok(true)); + + private void RemoveAction(VDA5050.InstantAction.Action actToRemove) + { + Node.Actions = Node.Actions + .Where(a => a != actToRemove) + .ToArray(); + } + + + private void AddNewAction() + { + Node.Actions = Node.Actions + .Append(new VDA5050.InstantAction.Action + { + ActionId = Guid.NewGuid().ToString(), + ActionType = ActionType.startPause.ToString(), + BlockingType = "NONE", + ActionParameters = Array.Empty() + }) + .ToArray(); + } + + + + private void AddParameter(VDA5050.InstantAction.Action act) + { + var newParam = new UiActionParameter(); + + if (act.ActionParameters == null || act.ActionParameters.Length == 0) + { + act.ActionParameters = new[] { newParam }; + } + else + { + var list = act.ActionParameters.ToList(); + list.Add(newParam); + act.ActionParameters = list.ToArray(); + } + } + + private void RemoveParameter(VDA5050.InstantAction.Action act, UiActionParameter paramToRemove) + { + if (act.ActionParameters == null || act.ActionParameters.Length == 0) + return; + + act.ActionParameters = act.ActionParameters + .Where(p => p != paramToRemove) // so sánh reference + .ToArray(); + } + + // UiActionParameter vẫn giữ như cũ trong trang chính + public class UiActionParameter : ActionParameter + { + [JsonIgnore] + public string ValueString + { + get => Value?.ToString() ?? ""; + set => Value = value; + } + } +} \ No newline at end of file diff --git a/RobotApp.Client/Pages/Home.razor b/RobotApp.Client/Pages/Home.razor new file mode 100644 index 0000000..2d0d52f --- /dev/null +++ b/RobotApp.Client/Pages/Home.razor @@ -0,0 +1,652 @@ +@page "/" +@using System.Text.Json +@using System.Text.Json.Serialization +@using RobotApp.VDA5050.InstantAction +@using RobotApp.VDA5050.Order +@using RobotApp.VDA5050.Type +@using System.ComponentModel.DataAnnotations +@attribute [Authorize] +@rendermode InteractiveWebAssemblyNoPrerender +@inject IJSRuntime JS + +@inject IDialogService DialogService + + +
+ + + + + 🧾 VDA5050 Order Editor + + + + + + + + + + + + + 📍 Nodes + + Add Node + + + + +
+ + @foreach (var node in Order.Nodes) + { + + + + +
+ + @node.NodeId + + + +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Actions + + + @foreach (var act in node.Actions) + { + + + + + @foreach (var at in Enum.GetValues()) + { + + @at + + } + + + + + + NONE + SOFT + HARD + + + + + + + + + + Action Parameters + + + @foreach (var p in act.ActionParameters.Cast()) + { + + + + + + + + + + + + } + + + Add Parameter + + + + + + Remove Action + + + } + + + Add Action + + + + + +
+ } +
+
+ +
+
+ + + + + + 🔗 Edges + + Add Edge + + + + +
+ + @foreach (var edge in Order.Edges) + { + + + + + + @foreach (var node in Order.Nodes) + { + + @node.NodeId + + } + + + + + + @foreach (var node in Order.Nodes) + { + + @node.NodeId + + } + + + + + Remove Edge + + + + } + +
+
+
+
+
+ + + + + 📄 JSON Output (/order) + + + Send + + @if (!string.IsNullOrEmpty(sendResult)) + { + + @sendResult + + } + + + + @if (copied) + { + Copied! + } + else + { + Copy + } + + + + + +
+ +
+
+
+ +
+
+
+
+ + + +@code { + [Inject] HttpClient Http { get; set; } = default!; + + bool sending; + string? sendResult; + bool IsEditNodeOpen; + Node? EditingNode; + OrderMessage Order = new(); + + async Task SendOrderToServer() + { + if (sending) + return; + + sending = true; + sendResult = null; + StateHasChanged(); + + try + { + var response = await Http.PostAsJsonAsync( + "/api/order", + JsonSerializer.Deserialize(OrderJson) + ); + + if (response.IsSuccessStatusCode) + { + sendResult = "✅ Done!"; + } + else + { + sendResult = $"❌ Failed: {response.StatusCode}"; + } + } + catch (Exception ex) + { + sendResult = $"❌ Error: {ex.Message}"; + } + finally + { + sending = false; + StateHasChanged(); + } + } + + void AddNode() + { + Order.Nodes.Add(new Node + { + NodeId = $"NODE_{Order.Nodes.Count + 1}", + SequenceId = Order.Nodes.Count, + Released = true, + NodePosition = new NodePosition() + }); + } + + void RemoveNode(Node node) + { + Order.Nodes.Remove(node); + + Order.Edges.RemoveAll(e => + e.StartNodeId == node.NodeId || + e.EndNodeId == node.NodeId); + + for (int i = 0; i < Order.Nodes.Count; i++) + Order.Nodes[i].SequenceId = i; + } + + void RemoveAction(Node node, VDA5050.InstantAction.Action act) + { + node.Actions = node.Actions + .Where(a => a != act) + .ToArray(); + } + + + void AddAction(Node node) + { + node.Actions = node.Actions + .Append(new VDA5050.InstantAction.Action + { + ActionId = Guid.NewGuid().ToString(), + ActionType = ActionType.startPause.ToString(), + BlockingType = "NONE", + ActionParameters = Array.Empty() + }) + .ToArray(); + } + + + private void RemoveActionParameter(VDA5050.InstantAction.Action act, ActionParameter param) + { + if (act.ActionParameters == null) return; + act.ActionParameters = act.ActionParameters + .Where(p => p != param) + .ToArray(); + } + void AddActionParameter(VDA5050.InstantAction.Action act) + { + var newList = (act.ActionParameters ?? Array.Empty()).ToList(); + newList.Add(new UiActionParameter()); + act.ActionParameters = newList.ToArray(); + } + + void AddEdge() + { + if (Order.Nodes.Count < 2) return; + + Order.Edges.Add(new VDA5050.Order.Edge + { + EdgeId = $"EDGE_{Order.Edges.Count + 1}", + StartNodeId = Order.Nodes[^2].NodeId, + EndNodeId = Order.Nodes[^1].NodeId + }); + } + + bool copied = false; + CancellationTokenSource? _copyCts; + + async Task CopyJsonToClipboard() + { + _copyCts?.Cancel(); + _copyCts = new(); + + var success = await JS.InvokeAsync( + "copyText", + OrderJson + ); + + if (!success) + return; + + copied = true; + StateHasChanged(); + + try + { + await Task.Delay(1500, _copyCts.Token); + } + catch { } + + copied = false; + StateHasChanged(); + } + + + void RemoveEdge(VDA5050.Order.Edge edge) + { + Order.Edges.Remove(edge); + } + + string OrderJson => + JsonSerializer.Serialize( + Order.ToSchemaObject(), + new JsonSerializerOptions + { + WriteIndented = true, + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull + }); + + + public class UiActionParameter : ActionParameter + { + [JsonIgnore] + public string ValueString + { + get => Value?.ToString() ?? ""; + set => Value = value; + } + } + + public class OrderMessage + { + public int HeaderId { get; set; } + public string Timestamp { get; set; } = ""; + public string Version { get; set; } = "v1"; + public string Manufacturer { get; set; } = "PNKX"; + public string SerialNumber { get; set; } = "AMR-01"; + public string OrderId { get; set; } = Guid.NewGuid().ToString(); + public int OrderUpdateId { get; set; } + public string? ZoneSetId { get; set; } + public List Nodes { get; set; } = new(); + public List Edges { get; set; } = new(); + + public object ToSchemaObject() + { + int seq = 0; + + return new + { + headerId = HeaderId++, + + timestamp = string.IsNullOrWhiteSpace(Timestamp) + ? DateTime.UtcNow.ToString("O") + : Timestamp, + + version = Version, + manufacturer = Manufacturer, + serialNumber = SerialNumber, + orderId = OrderId, + orderUpdateId = OrderUpdateId, + + zoneSetId = string.IsNullOrWhiteSpace(ZoneSetId) + ? null + : ZoneSetId, + + // ================= NODES ================= + nodes = Nodes + .Select(n => new + { + nodeId = n.NodeId, + sequenceId = seq++, + released = n.Released, + + nodePosition = new + { + x = n.NodePosition.X, + y = n.NodePosition.Y, + theta = n.NodePosition.Theta, + + allowedDeviationXY = n.NodePosition.AllowedDeviationXY, + allowedDeviationTheta = n.NodePosition.AllowedDeviationTheta, + + mapId = string.IsNullOrWhiteSpace(n.NodePosition.MapId) + ? "MAP_01" + : n.NodePosition.MapId + }, + + actions = n.Actions + .Select(a => new + { + actionId = a.ActionId, + actionType = a.ActionType, + blockingType = a.BlockingType, + + actionParameters = a.ActionParameters != null + ? a.ActionParameters + .Select(p => new + { + key = p.Key, + value = p.Value + }) + .ToArray() + : Array.Empty() + }) + .ToArray() + }) + .ToArray(), // ✅ QUAN TRỌNG + + // ================= EDGES ================= + edges = Edges + .Select(e => new + { + edgeId = e.EdgeId, + sequenceId = seq++, + released = true, + + startNodeId = e.StartNodeId, + endNodeId = e.EndNodeId, + + actions = Array.Empty() + }) + .ToArray() // ✅ QUAN TRỌNG + }; + } + + } + + private async Task OpenEditNodeDialog(Node node) + { + var parameters = new DialogParameters + { + { x => x.Node, node } // Truyền trực tiếp reference gốc + }; + + var options = new DialogOptions + { + CloseButton = true, + MaxWidth = MaxWidth.Large, + FullWidth = true, + CloseOnEscapeKey = true + }; + + var dialog = await DialogService.ShowAsync($"Edit Node: {node.NodeId}", parameters, options); + await dialog.Result; // Đợi dialog đóng + + StateHasChanged(); + } +} \ No newline at end of file diff --git a/RobotApp.Client/Pages/State.razor b/RobotApp.Client/Pages/State.razor new file mode 100644 index 0000000..ec708c9 --- /dev/null +++ b/RobotApp.Client/Pages/State.razor @@ -0,0 +1,397 @@ +@page "/robot-state" +@using RobotApp.VDA5050.State +@rendermode InteractiveWebAssemblyNoPrerender + +@inject StateMsg RobotState + + + + + + +
+ 🤖 VDA 5050 Robot Dashboard + + @RobotState.Version • + @RobotState.Manufacturer + @RobotState.SerialNumber + +
+ @* @if (RobotState.Current != null) + { + + @(RobotState.Current.IsOnline ? "ONLINE" : "OFFLINE") + + } *@ +
+ + @if (RobotState == null) + { + + Waiting for robot state (VDA5050)... + + + } + else + { + var msg = RobotState; + + + + + + + + HeaderId + @msg.HeaderId + + + Timestamp (UTC) + @msg.Timestamp + + + Version + @msg.Version + + + OrderUpdateId + + @msg.OrderUpdateId + + + + + + + + + + + + + + + 📍 Position & Velocity + + + + NewBase: @(msg.NewBaseRequest ? "TRUE" : "FALSE") + + + + + + + X: @msg.AgvPosition.X.ToString("F2") m + Y: @msg.AgvPosition.Y.ToString("F2") m + θ: @msg.AgvPosition.Theta.ToString("F2") rad + + + Vx: @msg.Velocity.Vx.ToString("F2") m/s + Vy: @msg.Velocity.Vy.ToString("F2") m/s + Ω: @msg.Velocity.Omega.ToString("F3") rad/s + + + + + + + Initialized: @(msg.AgvPosition.PositionInitialized ? "TRUE" : "FALSE") + + + + + DeviationRange: @msg.AgvPosition.DeviationRange + + + + + + Localization Score: @(msg.AgvPosition.LocalizationScore * 100) % + + + + + + + + 🔋 Battery + + + + @msg.BatteryState.BatteryCharge:F1 % + + + @(msg.BatteryState.Charging ? "⚡ Charging" : "Discharging") + + + + + Voltage + @msg.BatteryState.BatteryVoltage.ToString("F1") V + + + Health + @msg.BatteryState.BatteryHealth % + + + Reach + @((int)msg.BatteryState.Reach) m + + + + + + + + + 🧭 Order & Path + + Order ID: @(msg.OrderId ?? "—") + Update ID: @msg.OrderUpdateId + + + Last Node: @msg.LastNodeId + Seq: @msg.LastNodeSequenceId + + Distance: @msg.DistanceSinceLastNode:F1 m + + + @{ + var nodeReleased = msg.NodeStates?.Count(n => n.Released) ?? 0; + var nodeTotal = msg.NodeStates?.Length ?? 0; + var edgeReleased = msg.EdgeStates?.Count(e => e.Released) ?? 0; + var edgeTotal = msg.EdgeStates?.Length ?? 0; + } + +
+ Nodes: @nodeReleased / @nodeTotal + Edges: @edgeReleased / @edgeTotal + + @(msg.Driving ? "DRIVING" : "STOPPED") + + + @(msg.Paused ? "PAUSED" : "ACTIVE") + +
+
+
+ + + + + + 🚨 Errors & Information + + + @{ + var rows = new List(); + + if (msg.Errors != null) + { + foreach (var err in msg.Errors) + { + rows.Add(new MessageRow( + err.ErrorType ?? "-", + err.ErrorLevel ?? "-", + err.ErrorDescription ?? "", + true + )); + } + } + + if (msg.Information != null) + { + foreach (var info in msg.Information) + { + rows.Add(new MessageRow( + info.InfoType ?? "-", + info.InfoLevel ?? "-", + info.InfoDescription ?? "", + false + )); + } + } + + var sortedMessages = rows + .OrderBy(r => r.IsError ? 0 : 1) // Errors trước + .ThenBy(r => r.Type) + .ToList(); + } + + + + + + + + + + + Type + Level + Description + + + + + + @context.Type + + + + + + @context.Level + + + + + + @context.Description + + + + + + + No errors or information messages + + + + + + + + + + + + + ⚙️ Actions + + + + + + + + + + + + Action + Action ID + Status + + + + + + @context.ActionType + + + + + + @context.ActionId + + + + + + @context.ActionStatus + + + + + + + No active actions + + + + + + + + + + + + 🛑 Safety + + + E-STOP: @msg.SafetyState.EStop + + + Field Violation: @(msg.SafetyState.FieldViolation ? "YES" : "NO") + + + + +
+ } +
+ +@code { + + private async Task OnStateChanged() + => await InvokeAsync(StateHasChanged); + + + private record MessageRow( + string Type, + string Level, + string Description, + bool IsError + ); +} \ No newline at end of file diff --git a/RobotApp.Client/RobotApp.Client.csproj b/RobotApp.Client/RobotApp.Client.csproj index cf5879a..8fc69d8 100644 --- a/RobotApp.Client/RobotApp.Client.csproj +++ b/RobotApp.Client/RobotApp.Client.csproj @@ -1,7 +1,7 @@ - net9.0 + net10.0 enable enable true @@ -12,11 +12,13 @@ - + + + diff --git a/RobotApp.Common.Shares/RobotApp.Common.Shares.csproj b/RobotApp.Common.Shares/RobotApp.Common.Shares.csproj index 125f4c9..b760144 100644 --- a/RobotApp.Common.Shares/RobotApp.Common.Shares.csproj +++ b/RobotApp.Common.Shares/RobotApp.Common.Shares.csproj @@ -1,7 +1,7 @@  - net9.0 + net10.0 enable enable diff --git a/RobotApp.VDA5050/RobotApp.VDA5050.csproj b/RobotApp.VDA5050/RobotApp.VDA5050.csproj index 125f4c9..b760144 100644 --- a/RobotApp.VDA5050/RobotApp.VDA5050.csproj +++ b/RobotApp.VDA5050/RobotApp.VDA5050.csproj @@ -1,7 +1,7 @@  - net9.0 + net10.0 enable enable diff --git a/RobotApp.sln b/RobotApp.sln index b17dbf7..db8a2ad 100644 --- a/RobotApp.sln +++ b/RobotApp.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 17 -VisualStudioVersion = 17.14.36511.14 +# Visual Studio Version 18 +VisualStudioVersion = 18.0.11205.157 d18.0 MinimumVisualStudioVersion = 10.0.40219.1 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RobotApp", "RobotApp\RobotApp.csproj", "{BF0BB137-2EF9-4E1B-944E-9BF41C5284F7}" EndProject diff --git a/RobotApp/Components/Pages/Home.razor b/RobotApp/Components/Pages/Home.razor index 8210272..23f2ee7 100644 --- a/RobotApp/Components/Pages/Home.razor +++ b/RobotApp/Components/Pages/Home.razor @@ -1,4 +1,4 @@ -@page "/" +@page "/home" @using Microsoft.AspNetCore.Authorization @rendermode InteractiveServer diff --git a/RobotApp/Controllers/OrderController.cs b/RobotApp/Controllers/OrderController.cs new file mode 100644 index 0000000..4115cb2 --- /dev/null +++ b/RobotApp/Controllers/OrderController.cs @@ -0,0 +1,36 @@ +using Microsoft.AspNetCore.Mvc; +using RobotApp.Services.Robot; +using RobotApp.VDA5050.Order; +using System.Text.Json; + +namespace RobotApp.Controllers; + +[ApiController] +[Route("api/order")] +public class OrderController : ControllerBase +{ + private readonly RobotOrderController robotOrderController; + + public OrderController(RobotOrderController robotOrderController) + { + this.robotOrderController = robotOrderController; + } + + [HttpPost] + public IActionResult SendOrder([FromBody] OrderMsg order) + { + Console.WriteLine("===== ORDER RECEIVED ====="); + Console.WriteLine(JsonSerializer.Serialize(order, new JsonSerializerOptions + { + WriteIndented = true + })); + + robotOrderController.UpdateOrder(order); + + return Ok(new + { + success = true, + message = "Order received" + }); + } +} \ No newline at end of file diff --git a/RobotApp/Program.cs b/RobotApp/Program.cs index 9657231..a4938c5 100644 --- a/RobotApp/Program.cs +++ b/RobotApp/Program.cs @@ -3,6 +3,7 @@ using Microsoft.AspNetCore.Identity; using Microsoft.EntityFrameworkCore; using MudBlazor.Services; using NLog.Web; +using RobotApp.Client; using RobotApp.Components; using RobotApp.Components.Account; using RobotApp.Data; @@ -27,7 +28,7 @@ builder.Services.AddAuthorization(); // Add Controllers for API endpoints builder.Services.AddControllers(); - +builder.Services.AddSignalR(); var connectionString = builder.Configuration.GetConnectionString("DefaultConnection") ?? throw new InvalidOperationException("Connection string 'DefaultConnection' not found."); Action appDbOptions = options => options.UseSqlite(connectionString, b => b.MigrationsAssembly("RobotApp")); @@ -76,6 +77,7 @@ app.UseAntiforgery(); app.MapStaticAssets(); + // Map API Controllers app.MapControllers(); diff --git a/RobotApp/RobotApp.csproj b/RobotApp/RobotApp.csproj index 9809486..38962ca 100644 --- a/RobotApp/RobotApp.csproj +++ b/RobotApp/RobotApp.csproj @@ -1,7 +1,7 @@  - net9.0 + net10.0 enable enable aspnet-RobotApp-1f61caa2-bbbb-40cd-88b6-409b408a84ea diff --git a/RobotApp/Services/Robot/RobotOrderController.cs b/RobotApp/Services/Robot/RobotOrderController.cs index ebe336b..8111e36 100644 --- a/RobotApp/Services/Robot/RobotOrderController.cs +++ b/RobotApp/Services/Robot/RobotOrderController.cs @@ -258,7 +258,7 @@ public class RobotOrderController(INavigation NavigationManager, } } - private void HandleUpdateOrder(OrderMsg order) + public void HandleUpdateOrder(OrderMsg order) { if (order.OrderId != OrderId) throw new OrderException(RobotErrors.Error1001(OrderId, order.OrderId)); if (order.OrderUpdateId <= OrderUpdateId) return;