Initial commit
This commit is contained in:
@@ -0,0 +1,277 @@
|
||||
using RobotNet.VDA5050;
|
||||
using RobotNet.VDA5050.State;
|
||||
using RobotNet.VDA5050.Type;
|
||||
using RobotNet10.Common;
|
||||
using RobotNet10.RobotApp.Interfaces;
|
||||
using RobotNet10.RobotApp.Services.Exceptions;
|
||||
using RobotNet10.RobotApp.Services.Robot.Actions;
|
||||
using RobotNet10.RobotApp.Services.Robot.Helper;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace RobotNet10.RobotApp.Services.Robot;
|
||||
|
||||
public class RobotActionController(ILogger<RobotActionController> Logger,
|
||||
IRobotActionProvider RobotActionProvider,
|
||||
IError ErrorManager,
|
||||
INavigation NavigationManager,
|
||||
IServiceScopeFactory ServiceScopeFactory) : BackgroundService, IAction
|
||||
{
|
||||
public ActionState[] ActionStates => [.. Actions.Values.OrderBy(a => a.SequenceNumber).Select(a => new ActionState
|
||||
{
|
||||
ActionId = a.Id,
|
||||
ActionType = a.Type.ToJsonString(),
|
||||
ActionDescription = a.Description,
|
||||
ActionStatus = a.Status,
|
||||
ResultDescription = a.ResultDescription,
|
||||
})];
|
||||
public bool HasActionRunning => !ActionQueue.IsEmpty || Actions.Values.Any(a => a.Type != ActionType.CANCEL_ORDER && !a.IsCompleted);
|
||||
public bool HasActionWaitting => !ActionQueue.IsEmpty;
|
||||
|
||||
private readonly ConcurrentDictionary<string, RobotAction> Actions = [];
|
||||
private readonly ConcurrentQueue<(ActionScope scope, RobotNet.VDA5050.InstantAction.Action action)> ActionQueue = [];
|
||||
private readonly ActionConflictDetector _conflictDetector = new();
|
||||
|
||||
private WatchThread<RobotActionController>? HandlerTimer;
|
||||
private const int HandlerInterval = 200;
|
||||
private const int CompletedActionRetentionMs = 300000; // 5 minutes
|
||||
private int _cleanupCounter = 0;
|
||||
private const int CleanupIntervalIterations = 50; // Cleanup every 50 iterations (10 seconds)
|
||||
private volatile bool _isClearing = false;
|
||||
private long _sequenceCounter = 0;
|
||||
|
||||
public RobotAction? this[string actionId] => Actions.TryGetValue(actionId, out RobotAction? action) && action is not null ? action : null;
|
||||
|
||||
public void AddInstantAction(RobotNet.VDA5050.InstantAction.Action[] actions)
|
||||
{
|
||||
using var scope = ServiceScopeFactory.CreateAsyncScope();
|
||||
var OrderManager = scope.ServiceProvider.GetRequiredService<IOrder>();
|
||||
foreach (var action in actions)
|
||||
{
|
||||
if (Actions.TryGetValue(action.ActionId, out _)) continue;
|
||||
|
||||
// VDA5050: Check for conflicts with ALL running actions (ORDER + INSTANT)
|
||||
var runningActions = Actions.Values.Where(a => !a.IsCompleted).ToList();
|
||||
bool isOrderActive = OrderManager.NodeStates.Length > 0 || OrderManager.EdgeStates.Length > 0;
|
||||
bool isDriving = NavigationManager.Driving;
|
||||
|
||||
var conflictResult = _conflictDetector.CheckConflict(action, runningActions, isOrderActive, isDriving);
|
||||
|
||||
if (conflictResult.HasConflict)
|
||||
{
|
||||
// Reject instant action and report error
|
||||
var error = new RobotError
|
||||
{
|
||||
ErrorType = "instantActionConflict",
|
||||
ErrorLevel = ErrorLevel.WARNING,
|
||||
ErrorDescription = $"INSTANT action {action.ActionType} bị từ chối: {conflictResult.Description}",
|
||||
ErrorReferences = [
|
||||
new() { ReferenceKey = "actionId", ReferenceValue = action.ActionId },
|
||||
new() { ReferenceKey = "conflictType", ReferenceValue = conflictResult.Type.ToString() }
|
||||
]
|
||||
};
|
||||
|
||||
if (!string.IsNullOrEmpty(conflictResult.ConflictingActionId))
|
||||
{
|
||||
error.ErrorReferences = [
|
||||
.. error.ErrorReferences,
|
||||
new() { ReferenceKey = "conflictingActionId", ReferenceValue = conflictResult.ConflictingActionId }
|
||||
];
|
||||
}
|
||||
|
||||
ErrorManager.AddError(error, TimeSpan.FromSeconds(10));
|
||||
Logger.LogWarning($"INSTANT action {action.ActionId} (type: {action.ActionType}) rejected due to conflict: {conflictResult.Description}");
|
||||
continue; // Skip this action
|
||||
}
|
||||
|
||||
// No conflict - add to queue
|
||||
ActionQueue.Enqueue((ActionScope.INSTANT, action));
|
||||
}
|
||||
}
|
||||
|
||||
public void AddOrderActions(RobotNet.VDA5050.InstantAction.Action[] actions, ActionScope scope = ActionScope.NODE)
|
||||
{
|
||||
foreach (var action in actions)
|
||||
{
|
||||
if (Actions.TryGetValue(action.ActionId, out _)) continue;
|
||||
ActionQueue.Enqueue((scope, action));
|
||||
}
|
||||
}
|
||||
|
||||
public IEnumerable<RobotAction> GetRunningActions()
|
||||
{
|
||||
return Actions.Values.Where(a => !a.IsCompleted);
|
||||
}
|
||||
|
||||
public void StartOrderAction(string actionId)
|
||||
{
|
||||
if (Actions.TryGetValue(actionId, out RobotAction? robotAction) && robotAction is not null)
|
||||
{
|
||||
robotAction.Start();
|
||||
}
|
||||
}
|
||||
|
||||
public void StopOrderAction(string actionId = "")
|
||||
{
|
||||
if (string.IsNullOrEmpty(actionId))
|
||||
{
|
||||
foreach (var action in Actions.Values)
|
||||
{
|
||||
if (!action.IsCompleted && action.Type != ActionType.CANCEL_ORDER) action.Cancel();
|
||||
}
|
||||
}
|
||||
else if (Actions.TryGetValue(actionId, out RobotAction? robotAction) && robotAction is not null) robotAction.Cancel();
|
||||
}
|
||||
|
||||
public void FinishAction(string actionId)
|
||||
{
|
||||
if (Actions.TryGetValue(actionId, out RobotAction? robotAction) && robotAction is not null)
|
||||
{
|
||||
robotAction.Finish();
|
||||
}
|
||||
}
|
||||
|
||||
public void PauseActions()
|
||||
{
|
||||
foreach (var action in Actions.Values)
|
||||
{
|
||||
action.Pause();
|
||||
}
|
||||
}
|
||||
|
||||
public void ResumeActions()
|
||||
{
|
||||
foreach (var action in Actions.Values)
|
||||
{
|
||||
action.Resume();
|
||||
}
|
||||
}
|
||||
|
||||
private void ActionHandler()
|
||||
{
|
||||
if (_isClearing) return;
|
||||
|
||||
while (!ActionQueue.IsEmpty)
|
||||
{
|
||||
if (!ActionQueue.TryDequeue(out var result)) continue;
|
||||
if (Actions.ContainsKey(result.action.ActionId)) continue;
|
||||
|
||||
RobotAction? robotAction = null;
|
||||
try
|
||||
{
|
||||
if (EnumHelper.TryParse(result.action.ActionType, out ActionType actionType))
|
||||
{
|
||||
robotAction = RobotActionProvider.GetRobotAction(actionType);
|
||||
if (robotAction is not null)
|
||||
{
|
||||
robotAction.Initialize(result.scope, result.action);
|
||||
robotAction.SequenceNumber = Interlocked.Increment(ref _sequenceCounter);
|
||||
Actions.TryAdd(result.action.ActionId, robotAction);
|
||||
if (result.scope == ActionScope.INSTANT) robotAction.Start();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
var error = new RobotError
|
||||
{
|
||||
ErrorType = "actionTypeInvalid",
|
||||
ErrorLevel = RobotNet.VDA5050.Type.ErrorLevel.WARNING,
|
||||
ErrorDescription = $"ActionType không hợp lệ: {result.action.ActionType}",
|
||||
ErrorReferences = [new() { ReferenceKey = "actionId", ReferenceValue = result.action.ActionId }]
|
||||
};
|
||||
ErrorManager.AddError(error, TimeSpan.FromSeconds(10));
|
||||
Logger.LogWarning("ActionType không hợp lệ: {ActionType} cho action {ActionId}", result.action.ActionType, result.action.ActionId);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
var errorMsg = ex is RobotException rex && rex.Error is not null ? rex.Error.ErrorDescription : ex.Message;
|
||||
|
||||
// Nếu robotAction đã tạo, mark FAILED và add vào Actions để FM nhận được trạng thái
|
||||
if (robotAction is not null)
|
||||
{
|
||||
robotAction.Cancel();
|
||||
robotAction.ResultDescription = $"Khởi tạo action thất bại: {errorMsg}";
|
||||
robotAction.SequenceNumber = Interlocked.Increment(ref _sequenceCounter);
|
||||
Actions.TryAdd(result.action.ActionId, robotAction);
|
||||
}
|
||||
|
||||
var error = new RobotError
|
||||
{
|
||||
ErrorType = "actionInitializationFailed",
|
||||
ErrorLevel = RobotNet.VDA5050.Type.ErrorLevel.WARNING,
|
||||
ErrorDescription = $"Action {result.action.ActionId} ({result.action.ActionType}) khởi tạo thất bại: {errorMsg}",
|
||||
ErrorReferences = [new() { ReferenceKey = "actionId", ReferenceValue = result.action.ActionId }]
|
||||
};
|
||||
ErrorManager.AddError(error, TimeSpan.FromSeconds(10));
|
||||
Logger.LogWarning("Action {ActionId} (type: {ActionType}) initialization failed: {Error}", result.action.ActionId, result.action.ActionType, errorMsg);
|
||||
}
|
||||
}
|
||||
|
||||
// Cleanup completed actions periodically to prevent memory leak
|
||||
_cleanupCounter++;
|
||||
if (_cleanupCounter >= CleanupIntervalIterations)
|
||||
{
|
||||
_cleanupCounter = 0;
|
||||
CleanupCompletedActions();
|
||||
}
|
||||
}
|
||||
|
||||
private void CleanupCompletedActions()
|
||||
{
|
||||
try
|
||||
{
|
||||
long currentTime = Stopwatch.GetTimestamp() * 1000 / Stopwatch.Frequency;
|
||||
var actionsToRemove = Actions.Where(kvp =>
|
||||
kvp.Value.IsCompleted &&
|
||||
kvp.Value.CompletionTime > 0 &&
|
||||
(currentTime - kvp.Value.CompletionTime) > CompletedActionRetentionMs
|
||||
).Select(kvp => kvp.Key).ToList();
|
||||
|
||||
foreach (var actionId in actionsToRemove)
|
||||
{
|
||||
if (Actions.TryGetValue(actionId, out var action))
|
||||
{
|
||||
_ = action.DisposeAsync(); // Fire and forget disposal
|
||||
Actions.TryRemove(actionId, out _);
|
||||
Logger.LogDebug($"Cleaned up completed action: {actionId} (Type: {action.Type})");
|
||||
}
|
||||
}
|
||||
|
||||
if (actionsToRemove.Count > 0)
|
||||
{
|
||||
Logger.LogInformation($"Cleaned up {actionsToRemove.Count} completed actions");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogWarning($"Error during action cleanup: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
public async Task ClearActions()
|
||||
{
|
||||
_isClearing = true;
|
||||
ActionQueue.Clear();
|
||||
var disposeTasks = Actions.Values.Select(action => action.DisposeAsync().AsTask()).ToList();
|
||||
await Task.WhenAll(disposeTasks).ConfigureAwait(false);
|
||||
Actions.Clear();
|
||||
ActionQueue.Clear(); // Clear lần nữa phòng trường hợp có action mới enqueue trong lúc dispose
|
||||
Interlocked.Exchange(ref _sequenceCounter, 0);
|
||||
_isClearing = false;
|
||||
}
|
||||
|
||||
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
||||
{
|
||||
await Task.Yield();
|
||||
HandlerTimer = new(HandlerInterval, ActionHandler, Logger);
|
||||
HandlerTimer.Start();
|
||||
}
|
||||
|
||||
public override Task StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
HandlerTimer?.Dispose();
|
||||
HandlerTimer = null;
|
||||
return base.StopAsync(cancellationToken);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user