This commit is contained in:
Đăng Nguyễn 2025-12-23 09:52:42 +07:00
parent e4e135e35f
commit a51cfe80c8
12 changed files with 80 additions and 194 deletions

View File

@ -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">
@ -38,8 +38,8 @@
{ {
<MudChip T="string" <MudChip T="string"
Icon="@(IsConnected Icon="@(IsConnected
? Icons.Material.Filled.CheckCircle ? Icons.Material.Filled.CheckCircle
: Icons.Material.Filled.Error)" : 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"
@ -152,7 +152,7 @@
Rounded="true" Rounded="true"
Striped="true" Striped="true"
Color="@(msg.BatteryState.BatteryCharge > 50 ? Color.Success : 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" Class="mb-4"
Style="height: 28px;" /> Style="height: 28px;" />
@ -266,7 +266,7 @@
<MudChip T="string" <MudChip T="string"
Size="Size.Small" Size="Size.Small"
Color="@(context.Level.Contains("ERROR", StringComparison.OrdinalIgnoreCase) ? Color.Error : 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"> Variant="@Variant.Filled">
@context.Level @context.Level
</MudChip> </MudChip>
@ -317,8 +317,8 @@
<MudChip T="string" <MudChip T="string"
Size="Size.Small" Size="Size.Small"
Color="@(context.ActionStatus == "RUNNING" ? Color.Info : Color="@(context.ActionStatus == "RUNNING" ? Color.Info :
context.ActionStatus == "FINISHED" ? Color.Success : context.ActionStatus == "FINISHED" ? Color.Success :
context.ActionStatus == "FAILED" ? Color.Error : Color.Default)" context.ActionStatus == "FAILED" ? Color.Error : Color.Default)"
Variant="@Variant.Filled"> Variant="@Variant.Filled">
@context.ActionStatus @context.ActionStatus
</MudChip> </MudChip>
@ -373,45 +373,34 @@
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 OnInitializedAsync() protected override async Task OnAfterRenderAsync(bool firstRender)
{ {
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();
{ CurrentState = RobotStateClient.GetLatestState();
await RobotStateClient.StartAsync();
}
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;
{ StateHasChanged();
IsConnected = connected;
StateHasChanged();
});
} }
private void OnRobotStateReceived(string serialNumber, StateMsg state) private void OnRobotStateReceived(string serialNumber, StateMsg state)
{ {
if (serialNumber != RobotSerial) return; CurrentState = state;
InvokeAsync(() => UpdateMessageRows();
{ StateHasChanged();
CurrentState = state;
UpdateMessageRows();
StateHasChanged();
});
} }
private void UpdateMessageRows() private void UpdateMessageRows()

View File

@ -17,6 +17,7 @@
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;
@ -28,7 +29,7 @@
{ {
_monitorData = data; _monitorData = data;
RobotMonitorViewRef?.UpdatePath(); RobotMonitorViewRef?.UpdatePath();
InvokeAsync(StateHasChanged); StateHasChanged();
} }
public async ValueTask DisposeAsync() public async ValueTask DisposeAsync()
@ -41,3 +42,7 @@

View File

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

View File

@ -60,3 +60,7 @@ window.robotMonitor = {

View File

@ -28,7 +28,6 @@ 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,

View File

@ -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", json); await Clients.Group(serialNumber).SendAsync("ReceiveState", state);
} }
} }
} }

View File

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

View File

@ -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();

View File

@ -105,7 +105,8 @@ 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 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)");

View File

@ -9,6 +9,7 @@ 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;
@ -247,11 +248,6 @@ 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;
@ -292,6 +288,7 @@ public class RobotOrderController(INavigation NavigationManager,
private void HandleOrder() private void HandleOrder()
{ {
if (Nodes.Length <= 0) return;
if (IsCancelOrder) if (IsCancelOrder)
{ {
NavigationManager.CancelMovement(); NavigationManager.CancelMovement();
@ -309,9 +306,13 @@ 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) return; if (robotAction is null)
if (robotAction.IsCompleted) FinalAction.Remove(action); {
if (robotAction.Status == ActionStatus.WAITING) ActionManager.StartOrderAction(action.ActionId); FinalAction.Remove(action);
return;
}
if (robotAction.IsCompleted)
if (robotAction.Status == ActionStatus.WAITING) ActionManager.StartOrderAction(action.ActionId);
} }
else else
{ {
@ -398,6 +399,10 @@ 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());
HandleNewOrder(NewOrderHandler); HandleNewOrder(NewOrderHandler);
} }
} }
@ -409,6 +414,7 @@ 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}");
HandleOrderStop();
} }
else Logger.Warning($"Lỗi khi xử lí Order: {orEx.Message}"); else Logger.Warning($"Lỗi khi xử lí Order: {orEx.Message}");
} }

View File

@ -12,148 +12,27 @@ using System.Text.Json;
namespace RobotApp.Services.Robot; 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 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 =====
var state = GetStateMsg(); var state = _robotState.GetStateMsg();
var json = JsonSerializer.Serialize(state, JsonOptionExtends.Write); //var json = JsonSerializer.Serialize(state, JsonOptionExtends.Write);
await _hubContext.Clients await _hubContext.Clients.All
.Group(serialNumber) .SendAsync("ReceiveState", state, stoppingToken);
.SendAsync("ReceiveState", json, stoppingToken);
// ===== SEND ROBOT CONNECTION (ONLY WHEN CHANGED) ===== // ===== SEND ROBOT CONNECTION (ONLY WHEN CHANGED) =====
var isConnected = _robotConnection.IsConnected; var isConnected = _robotConnection.IsConnected;
@ -162,8 +41,7 @@ public class RobotStatePublisher : BackgroundService
{ {
_lastRobotConnectionState = isConnected; _lastRobotConnectionState = isConnected;
await _hubContext.Clients await _hubContext.Clients.All
.Group(serialNumber) // routing only
.SendAsync( .SendAsync(
"ReceiveRobotConnection", "ReceiveRobotConnection",
isConnected, // payload only bool isConnected, // payload only bool
@ -177,10 +55,10 @@ public class RobotStatePublisher : BackgroundService
} }
} }
public override Task StopAsync(CancellationToken cancellationToken)
public override void Dispose()
{ {
_timer?.Dispose(); _timer?.Dispose();
base.Dispose(); base.Dispose();
return base.StopAsync(cancellationToken);
} }
} }

View File

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