Initial commit

This commit is contained in:
2026-07-13 09:25:40 +07:00
parent c08ff54676
commit bccfb156d7
1938 changed files with 641646 additions and 0 deletions

View File

@@ -0,0 +1,44 @@
using RobotNet.VDA5050.Type;
using RobotNet10.RobotApp.Interfaces;
namespace RobotNet10.RobotApp.Services.Robot.Actions;
/// <summary>
/// Tắt đèn camera (M918 - Light on OFF).
/// </summary>
[RobotAction(ActionType.CAMERA_LIGHT_OFF,
[ActionScope.INSTANT, ActionScope.NODE, ActionScope.EDGE],
[BlockingType.NONE, BlockingType.SOFT, BlockingType.HARD],
"Tắt đèn camera (M918 Light on OFF).",
"Đèn camera đã tắt (M918 Light on OFF).")]
public class CameraLightOffAction(IServiceProvider ServiceProvider) : RobotAction(ServiceProvider)
{
private IPlcController? PlcController;
private const int MAX_TIMEOUT = 4000;
private int CountTimeout = 0;
protected override Task StartAction()
{
PlcController = ServiceProvider.GetRequiredService<IPlcController>();
PlcController.SetLightOn(false); // M918 Light on OFF
return base.StartAction();
}
protected override Task ExecuteAction()
{
if (PlcController is not null && !PlcController.SetLightOnValue)
{
SetStatus(ActionEvent.FINISHED);
ResultDescription = string.IsNullOrEmpty(ActionAttribute.ResultDescription) ? ResultDescription : ActionAttribute.ResultDescription;
}
else if (CountTimeout++ > MAX_TIMEOUT / ActionInterval)
{
SetStatus(ActionEvent.FAILED);
ResultDescription = "Timeout action";
}
return base.ExecuteAction();
}
}

View File

@@ -0,0 +1,45 @@
using RobotNet.VDA5050.Type;
using RobotNet10.RobotApp.Interfaces;
namespace RobotNet10.RobotApp.Services.Robot.Actions;
/// <summary>
/// Bật đèn camera (M918 - coil 2966).
/// action tương tự các action PLC toggle khác: ghi ON, rồi chờ PLC phản hồi đã ON.
/// </summary>
[RobotAction(ActionType.CAMERA_LIGHT_ON,
[ActionScope.INSTANT, ActionScope.NODE, ActionScope.EDGE],
[BlockingType.NONE, BlockingType.SOFT, BlockingType.HARD],
"Bật đèn camera (M918 Light on).",
"Đèn camera đã bật (M918 Light on).")]
public class CameraLightOnAction(IServiceProvider ServiceProvider) : RobotAction(ServiceProvider)
{
private IPlcController? PlcController;
private const int MAX_TIMEOUT = 4000;
private int CountTimeout = 0;
protected override Task StartAction()
{
PlcController = ServiceProvider.GetRequiredService<IPlcController>();
PlcController.SetLightOn(true); // M918 Light on
return base.StartAction();
}
protected override Task ExecuteAction()
{
if (PlcController is not null && PlcController.SetLightOnValue)
{
SetStatus(ActionEvent.FINISHED);
ResultDescription = string.IsNullOrEmpty(ActionAttribute.ResultDescription) ? ResultDescription : ActionAttribute.ResultDescription;
}
else if (CountTimeout++ > MAX_TIMEOUT / ActionInterval)
{
SetStatus(ActionEvent.FAILED);
ResultDescription = "Timeout action";
}
return base.ExecuteAction();
}
}

View File

@@ -0,0 +1,77 @@
using RobotNet.VDA5050.Type;
using RobotNet10.RobotApp.Devices;
using RobotNet10.RobotApp.Services.Exceptions;
using RobotNet10.RobotApp.Interfaces;
namespace RobotNet10.RobotApp.Services.Robot.Actions;
/// <summary>
/// Bật đèn camera (M918 - coil 2966).
/// action tương tự các action PLC toggle khác: ghi ON, rồi chờ PLC phản hồi đã ON.
/// </summary>
[RobotAction(ActionType.CONTROL_LIGHT,
[ActionScope.INSTANT, ActionScope.NODE, ActionScope.EDGE],
[BlockingType.NONE, BlockingType.SOFT, BlockingType.HARD],
"Bật đèn camera (M918 Light on).",
"Đèn camera đã bật (M918 Light on).")]
public class ControlLightAction(IServiceProvider ServiceProvider) : RobotAction(ServiceProvider)
{
private IPlcController? PlcController;
private const int MAX_TIMEOUT = 4000;
private int CountTimeout = 0;
private bool IsLightOn = false;
protected override void Initialize()
{
base.Initialize();
var controlTypeParam = Action?.ActionParameters?.FirstOrDefault(p =>
string.Equals(p.Key, "CONTROL_TYPE", StringComparison.OrdinalIgnoreCase));
if (controlTypeParam is null || string.IsNullOrWhiteSpace(controlTypeParam.Value))
{
throw new ActionException("ControlLight requires actionParameter CONTROL_TYPE (CONTROL_ON/CONTROL_OFF).");
}
var controlType = controlTypeParam.Value.Trim();
if (string.Equals(controlType, "CONTROL_ON", StringComparison.OrdinalIgnoreCase))
{
IsLightOn = true;
}
else if (string.Equals(controlType, "CONTROL_OFF", StringComparison.OrdinalIgnoreCase))
{
IsLightOn = false;
}
else
{
throw new ActionException($"CONTROL_TYPE value '{controlType}' is invalid. Expected CONTROL_ON or CONTROL_OFF.");
}
CountTimeout = 0;
}
protected override Task StartAction()
{
PlcController = ServiceProvider.GetRequiredService<IPlcController>();
PlcController.SetLightOn(IsLightOn); // M918 Light on
return base.StartAction();
}
protected override Task ExecuteAction()
{
if (PlcController is not null && PlcController.SetLightOnValue == IsLightOn)
{
SetStatus(ActionEvent.FINISHED);
ResultDescription = string.IsNullOrEmpty(ActionAttribute.ResultDescription) ? ResultDescription : ActionAttribute.ResultDescription;
}
else if (CountTimeout++ > MAX_TIMEOUT / ActionInterval)
{
SetStatus(ActionEvent.FAILED);
ResultDescription = "Timeout action";
}
return base.ExecuteAction();
}
}

View File

@@ -0,0 +1,64 @@
using RobotNet.VDA5050.Type;
using RobotNet10.RobotApp.Devices;
namespace RobotNet10.RobotApp.Services.Robot.Actions;
/// <summary>
/// Action homing camera (lift): gọi trực tiếp xuống động cơ CiA402 (giống device/hub).
/// blockingType NONE: gửi lệnh homing và kết thúc ngay, không chờ hoàn thành.
/// </summary>
[RobotAction(ActionType.HOMING_CAMERA,
[ActionScope.INSTANT, ActionScope.NODE, ActionScope.EDGE],
[BlockingType.NONE],
"Homing camera (lift).",
"Homing camera requested.")]
public class HomingCameraAction(IServiceProvider ServiceProvider) : RobotAction(ServiceProvider)
{
protected override async Task StartAction()
{
try
{
var config = ServiceProvider.GetRequiredService<IConfiguration>();
var deviceProvider = ServiceProvider.GetRequiredService<IDeviceProvider>();
var deviceId = config.GetValue<string>("Modules:LiftModule:DeviceId") ?? "lift-motor";
var enable = config.GetValue<bool>("Modules:LiftModule:Enable", true);
if (!enable)
{
SetStatus(ActionEvent.FAILED);
ResultDescription = "Lift module is disabled in config.";
return;
}
var device = deviceProvider.GetDevice(deviceId);
if (device is not ICiA402Servo servo)
{
SetStatus(ActionEvent.FAILED);
ResultDescription = $"Device '{deviceId}' not found or is not ICiA402Servo.";
return;
}
var homingMethod = config.GetValue<byte>("Modules:LiftModule:HomingMethod", 21);
var homingSpeed = config.GetValue<int>("Modules:LiftModule:HomingSpeed", 30000);
var homingOffset = config.GetValue<int>("Modules:LiftModule:HomingOffset", 0);
// Giống device UI: bước 1 SetParam (Apply Params), bước 2 Start Homing
Logger?.LogInformation("HomingCamera: SetParam (method={Method}, speed={Speed}, offset={Offset}) then Start Homing.", homingMethod, homingSpeed, homingOffset);
await servo.SetHomingMethodAsync(homingMethod, CancellationToken.None);
await servo.SetHomingSpeedAsync(homingSpeed, CancellationToken.None);
await servo.SetHomingOffsetAsync(homingOffset, CancellationToken.None);
await Task.Delay(100, CancellationToken.None); // delay giữa SetParam và Start như khi bấm trên device
await servo.StartHomingAsync(homingMethod, homingSpeed, CancellationToken.None);
SetStatus(ActionEvent.FINISHED);
ResultDescription = "Homing camera requested (direct to drive).";
}
catch (Exception ex)
{
SetStatus(ActionEvent.FAILED);
ResultDescription = $"Homing camera failed: {ex.Message}";
}
await base.StartAction();
}
}

View File

@@ -0,0 +1,250 @@
// using RobotNet.VDA5050.Type;
// using RobotNet10.RobotApp.Devices;
// using RobotNet10.RobotApp.Services.Exceptions;
// using System.Globalization;
// namespace RobotNet10.RobotApp.Services.Robot.Actions;
// /// <summary>
// /// Action đưa camera (lift) tới vị trí theo chiều cao (m).
// /// Gọi trực tiếp xuống động cơ CiA402 (giống device/hub), không qua LiftModule state machine.
// /// actionParameters: HEIGHT (unit: m), ví dụ "0.825".
// /// blockingType NONE: gửi lệnh di chuyển và kết thúc ngay, không chờ hoàn thành.
// /// </summary>
// [RobotAction(ActionType.LIFT_CAMERA_BY_HEIGHT,
// [ActionScope.INSTANT, ActionScope.NODE, ActionScope.EDGE],
// [BlockingType.NONE, BlockingType.HARD, BlockingType.SOFT],
// "Lift camera with height (unit: m).",
// "Lift camera move requested.")]
// public class LiftCameraByHeightAction(IServiceProvider ServiceProvider) : RobotAction(ServiceProvider)
// {
// private double _heightM;
// private double _timeoutMs = 20 * 1000;
// protected override void Initialize()
// {
// base.Initialize();
// var heightParam = Action?.ActionParameters?.FirstOrDefault(p =>
// string.Equals(p.Key, "HEIGHT", StringComparison.OrdinalIgnoreCase));
// if (heightParam is null || string.IsNullOrWhiteSpace(heightParam.Value))
// {
// throw new ActionException("LiftCameraByHeight requires actionParameter HEIGHT (unit: m).");
// }
// if (!double.TryParse(heightParam.Value, NumberStyles.Any, CultureInfo.InvariantCulture, out _heightM))
// {
// throw new ActionException($"HEIGHT value '{heightParam.Value}' is not a valid number.");
// }
// }
// protected override async Task StartAction()
// {
// try
// {
// var config = ServiceProvider.GetRequiredService<IConfiguration>();
// var deviceProvider = ServiceProvider.GetRequiredService<IDeviceProvider>();
// var deviceId = config.GetValue<string>("Modules:LiftModule:DeviceId") ?? "lift-motor";
// var enable = config.GetValue<bool>("Modules:LiftModule:Enable", true);
// if (!enable)
// {
// SetStatus(ActionEvent.FAILED);
// ResultDescription = "Lift module is disabled in config.";
// return;
// }
// var device = deviceProvider.GetDevice(deviceId);
// if (device is not ICiA402Servo servo)
// {
// SetStatus(ActionEvent.FAILED);
// ResultDescription = $"Device '{deviceId}' not found or is not ICiA402Servo.";
// return;
// }
// // Đọc config để map height (m) -> position (encoder), giống LiftModuleService.GetPositionFromHeightM
// var minHeightM = config.GetValue<double>("Modules:LiftModule:MinHeightM", 0);
// var maxHeightM = config.GetValue<double>("Modules:LiftModule:MaxHeightM", 1);
// var minPosition = config.GetValue<int>("Modules:LiftModule:MinPosition", 0);
// var maxPosition = config.GetValue<int>("Modules:LiftModule:MaxPosition", 1000000);
// int position;
// if (maxHeightM <= minHeightM)
// {
// position = minPosition;
// }
// else
// {
// var t = Math.Clamp((_heightM - minHeightM) / (maxHeightM - minHeightM), 0, 1);
// position = minPosition + (int)(t * (maxPosition - minPosition));
// }
// var velocity = config.GetValue<uint>("Modules:LiftModule:ProfileVelocity", 100000);
// var acceleration = config.GetValue<uint>("Modules:LiftModule:ProfileAcceleration", 50000);
// var deceleration = config.GetValue<uint>("Modules:LiftModule:ProfileDeceleration", 50000);
// Logger?.LogInformation("LiftCameraByHeight: calling servo directly (deviceId={DeviceId}), MoveToPositionAsync(position={Position}, height={HeightM} m).", deviceId, position, _heightM);
// await servo.MoveToPositionAsync(position, velocity, acceleration, deceleration, CancellationToken.None);
// // Timeout = (int)_timeoutMs;
// SetStatus(ActionEvent.FINISHED);
// ResultDescription = $"Lift camera move to height {_heightM} m requested (direct to drive).";
// }
// catch (Exception ex)
// {
// SetStatus(ActionEvent.FAILED);
// ResultDescription = $"Lift camera by height failed: {ex.Message}";
// }
// await base.StartAction();
// }
// // protected override async Task ExecuteAction()
// // {
// // // to do: wait for the lift camera to reach the target height
// // }
// // protected override async Task CleanupAction()
// // {
// // // to do: cleanup the lift camera
// // }
// }
using RobotNet.VDA5050.Type;
using RobotNet10.CANOpen.CiA402.Enums;
using RobotNet10.RobotApp.Devices;
using RobotNet10.RobotApp.Services.Exceptions;
using System.Globalization;
namespace RobotNet10.RobotApp.Services.Robot.Actions;
/// <summary>
/// Action đưa camera (lift) tới vị trí theo chiều cao (m).
/// Gọi trực tiếp xuống động cơ CiA402 (giống device/hub), không qua LiftModule state machine.
/// actionParameters: HEIGHT (unit: m), ví dụ "0.825".
/// blockingType NONE: gửi lệnh di chuyển và kết thúc ngay, không chờ hoàn thành.
/// </summary>
[RobotAction(ActionType.LIFT_CAMERA_BY_HEIGHT,
[ActionScope.INSTANT, ActionScope.NODE, ActionScope.EDGE],
[BlockingType.NONE, BlockingType.HARD],
"Lift camera with height (unit: m).",
"Lift camera move requested.")]
public class LiftCameraByHeightAction(IServiceProvider ServiceProvider) : RobotAction(ServiceProvider)
{
private double _heightM;
protected override void Initialize()
{
base.Initialize();
var heightParam = Action?.ActionParameters?.FirstOrDefault(p =>
string.Equals(p.Key, "HEIGHT", StringComparison.OrdinalIgnoreCase));
if (heightParam is null || string.IsNullOrWhiteSpace(heightParam.Value))
{
throw new ActionException("LiftCameraByHeight requires actionParameter HEIGHT (unit: m).");
}
if (!double.TryParse(heightParam.Value, NumberStyles.Any, CultureInfo.InvariantCulture, out _heightM))
{
throw new ActionException($"HEIGHT value '{heightParam.Value}' is not a valid number.");
}
}
protected override async Task StartAction()
{
try
{
var config = ServiceProvider.GetRequiredService<IConfiguration>();
var deviceProvider = ServiceProvider.GetRequiredService<IDeviceProvider>();
var deviceId = config.GetValue<string>("Modules:LiftModule:DeviceId") ?? "lift-motor";
var enable = config.GetValue<bool>("Modules:LiftModule:Enable", true);
if (!enable)
{
SetStatus(ActionEvent.FAILED);
ResultDescription = "Lift module is disabled in config.";
return;
}
var device = deviceProvider.GetDevice(deviceId);
if (device is not ICiA402Servo servo)
{
SetStatus(ActionEvent.FAILED);
ResultDescription = $"Device '{deviceId}' not found or is not ICiA402Servo.";
return;
}
// Đọc config để map height (m) -> position (encoder), giống LiftModuleService.GetPositionFromHeightM
var minHeightM = config.GetValue<double>("Modules:LiftModule:MinHeightM", 0);
var maxHeightM = config.GetValue<double>("Modules:LiftModule:MaxHeightM", 1);
var minPosition = config.GetValue<int>("Modules:LiftModule:MinPosition", 0);
var maxPosition = config.GetValue<int>("Modules:LiftModule:MaxPosition", 1000000);
int position;
if (maxHeightM <= minHeightM)
{
position = minPosition;
}
else
{
var t = Math.Clamp((_heightM - minHeightM) / (maxHeightM - minHeightM), 0, 1);
position = minPosition + (int)(t * (maxPosition - minPosition));
}
var velocity = config.GetValue<uint>("Modules:LiftModule:ProfileVelocity", 100000);
var acceleration = config.GetValue<uint>("Modules:LiftModule:ProfileAcceleration", 50000);
var deceleration = config.GetValue<uint>("Modules:LiftModule:ProfileDeceleration", 50000);
var tolerance = config.GetValue<int>("Modules:LiftModule:ActionTargetTolerance", 900);
var checkIntervalMs = config.GetValue<int>("Modules:LiftModule:ActionStatusWordCheckIntervalMs", 100);
var timeoutMs = config.GetValue<int>("Modules:LiftModule:ActionMoveTimeoutMs", 300000);
Logger?.LogInformation("LiftCameraByHeight: calling servo directly (deviceId={DeviceId}), MoveToPositionAsync(position={Position}, height={HeightM} m).", deviceId, position, _heightM);
await servo.MoveToPositionAsync(position, velocity, acceleration, deceleration, CancellationToken.None);
await WaitForMovementCompletedAsync(servo, position, tolerance, checkIntervalMs, timeoutMs, CancellationToken.None);
SetStatus(ActionEvent.FINISHED);
ResultDescription = $"Lift camera reached height {_heightM} m (direct to drive).";
}
catch (Exception ex)
{
SetStatus(ActionEvent.FAILED);
ResultDescription = $"Lift camera by height failed: {ex.Message}";
}
await base.StartAction();
}
private static async Task WaitForMovementCompletedAsync(
ICiA402Servo servo,
int targetPosition,
int tolerance,
int checkIntervalMs,
int timeoutMs,
CancellationToken ct)
{
var startedAt = DateTime.UtcNow;
var pollInterval = Math.Max(10, checkIntervalMs);
var maxWait = TimeSpan.FromMilliseconds(Math.Max(1000, timeoutMs));
while (DateTime.UtcNow - startedAt < maxWait)
{
var statusword = await servo.GetStatuswordAsync(ct);
if (statusword.GetState() == DriveState.Fault)
{
throw new ActionException("Lift movement failed: servo entered fault state.");
}
var currentPosition = await servo.GetActualPositionAsync(ct);
if (statusword.TargetReached && Math.Abs(currentPosition - targetPosition) <= Math.Max(50, tolerance))
{
return;
}
await Task.Delay(pollInterval, ct);
}
throw new TimeoutException($"Lift movement timeout after {maxWait.TotalSeconds:F0}s.");
}
}

View File

@@ -0,0 +1,360 @@
using Appccelerate.StateMachine;
using Appccelerate.StateMachine.Machine;
using RobotNet.VDA5050;
using RobotNet.VDA5050.Type;
using RobotNet10.Common;
using RobotNet10.NavigationTune.Shared.Models;
using RobotNet10.RobotApp.Services.Exceptions;
using System.Diagnostics;
using System.Reflection;
namespace RobotNet10.RobotApp.Services.Robot.Actions;
public abstract class RobotAction : IAsyncDisposable
{
public ActionType Type { get; }
public string Id { get; private set; } = "";
public string? Description { get; private set; }
public BlockingType BlockingType { get; private set; }
public RobotNet.VDA5050.InstantAction.ActionParameter[] Parameters { get; protected set; } = [];
public ActionStatus Status => CurrentStatus;
public string ResultDescription { get; set; } = "";
public bool IsCompleted => CurrentStatus == ActionStatus.FINISHED || CurrentStatus == ActionStatus.FAILED;
public long CompletionTime { get; private set; } = 0;
public ActionScope ActionScope { get; set; }
public RobotActionAttribute ActionAttribute { get; }
public long SequenceNumber { get; internal set; }
private WatchThreadAsync<RobotAction>? ActionTimer;
protected const int ActionInterval = 100;
protected IServiceProvider ServiceProvider;
protected RobotNet.VDA5050.InstantAction.Action? Action;
protected ILogger<RobotAction>? Logger;
protected bool IsPaused = false;
protected ActionStatus HistoryStatus;
private bool _justResumed = false;
private bool _isDisposed = false;
private bool IsCancelAction = false;
private PassiveStateMachine<ActionStatus, ActionEvent>? _stateMachine;
private ActionStatus CurrentStatus;
private long StartTime;
private int Timeout;
private readonly Lock _lock = new();
public RobotAction(IServiceProvider serviceProvider)
{
var derivedType = GetType();
ActionAttribute = derivedType.GetCustomAttribute<RobotActionAttribute>()
?? throw new InvalidOperationException(
$"Class {derivedType.Name} must have RobotActionAttribute");
Type = ActionAttribute.ActionType;
ServiceProvider = serviceProvider;
Logger = ServiceProvider.GetRequiredService<ILogger<RobotAction>>();
InitializeStatus();
}
public void Initialize(ActionScope actionScope, RobotNet.VDA5050.InstantAction.Action action)
{
ActionScope = actionScope;
Action = action;
BlockingType = action.BlockingType;
Id = action.ActionId;
Description = action.ActionDescription;
Initialize();
}
public void Start()
{
lock (_lock)
{
if (Status != ActionStatus.WAITING) return;
ActionTimer = new(ActionInterval, ActionHandler, Logger);
SetStatus(ActionEvent.INITIALIZING);
ActionTimer.Start();
StartTime = GetCurrentTimeMs();
}
}
public void Pause()
{
if (_isDisposed) return;
lock (_lock)
{
if (IsCompleted) return;
HistoryStatus = Status;
SetStatus(ActionEvent.PAUSED);
IsPaused = true;
}
}
public void Resume()
{
if (_isDisposed) return;
lock (_lock)
{
if (Status == ActionStatus.PAUSED)
{
IsPaused = false;
_justResumed = true;
// Restore về trạng thái trước khi pause
ActionEvent resumeEvent = HistoryStatus switch
{
ActionStatus.RUNNING => ActionEvent.RUNNING,
ActionStatus.INITIALIZING => ActionEvent.INITIALIZING,
_ => ActionEvent.WAITING
};
SetStatus(resumeEvent);
}
}
}
public void Cancel()
{
if (_isDisposed) return;
lock (_lock)
{
if (!IsCompleted) IsCancelAction = true;
if (Status == ActionStatus.WAITING) _ = StopAction();
}
}
/// <summary>
/// VDA5050: Gracefully finish an action (e.g., when robot leaves an edge).
/// Sets status to FINISHED instead of FAILED (Cancel).
/// </summary>
public void Finish()
{
if (_isDisposed) return;
lock (_lock)
{
if (IsCompleted) return;
// State machine allows FINISHED from RUNNING and INITIALIZING
if (CurrentStatus == ActionStatus.RUNNING || CurrentStatus == ActionStatus.INITIALIZING)
{
SetStatus(ActionEvent.FINISHED);
if (string.IsNullOrEmpty(ResultDescription))
ResultDescription = "Action completed (edge transition).";
}
else
{
// WAITING or PAUSED: cancel as fallback
IsCancelAction = true;
}
}
}
protected virtual Task StartAction()
{
return Task.CompletedTask;
}
protected virtual Task StopAction()
{
SetStatus(ActionEvent.FAILED);
ResultDescription = "Action bị hủy bỏ.";
return Task.CompletedTask;
}
protected virtual Task CompleteAction()
{
return Task.CompletedTask;
}
/// <summary>
/// Giải phóng tài nguyên được sử dụng trong action.
/// Luôn được gọi trong DisposeAsync, đảm bảo cleanup cả khi FINISHED lẫn FAILED/Cancel.
/// </summary>
protected virtual Task CleanupAction()
{
return Task.CompletedTask;
}
protected virtual Task ExecuteAction()
{
return Task.CompletedTask;
}
protected virtual Task PauseAction()
{
return Task.CompletedTask;
}
protected virtual Task ResumeAction()
{
return Task.CompletedTask;
}
protected virtual void Initialize()
{
if (Action is null) throw new ActionException("Khởi tạo Action không tồn tại");
if (EnumHelper.TryParse(Action.ActionType, out ActionType type))
{
if (type != Type) throw new ActionException($"ActionType {Action.ActionType} không khớp với action hiện tại {Type}.");
}
else throw new ActionException($"ActionType {Action.ActionType} không hợp lệ.");
if (!ActionAttribute.BlockingTypes.Any(bt => bt == BlockingType)) throw new ActionException($"BlockingType {BlockingType} không được hỗ trợ cho action {Type}.");
if (!ActionAttribute.ActionScopes.Any(sp => sp == ActionScope)) throw new ActionException($"ActionScope {ActionScope} không được hỗ trợ cho action {Type}.");
if (Action.ActionParameters != null && Action.ActionParameters.Length > 0)
{
var para = Action.ActionParameters.FirstOrDefault(p => p.Key == "timeout");
if (para is not null && int.TryParse(para.Value, out int miliseconds) && miliseconds > 100) Timeout = miliseconds;
else Timeout = -1;
}
}
private async Task ActionHandler()
{
try
{
if (Timeout > 0)
{
long now = GetCurrentTimeMs();
if (now >= (StartTime + Timeout)) throw new TimeoutException($"Action [{Type} - {Id}] timeout. Timeout: {Timeout} ms ");
}
ActionStatus status;
bool isCancel = false;
bool justResumed = false;
lock (_lock)
{
status = CurrentStatus;
isCancel = IsCancelAction;
justResumed = _justResumed;
if (_justResumed) _justResumed = false;
}
if (isCancel)
{
await StopAction();
}
else
{
if (status == ActionStatus.INITIALIZING)
{
Logger?.LogInformation($"Executing action {Type}");
SetStatus(ActionEvent.RUNNING);
await StartAction();
}
else if (status == ActionStatus.RUNNING)
{
if (justResumed)
{
await ResumeAction();
}
await ExecuteAction();
}
else if (status == ActionStatus.PAUSED)
{
await PauseAction();
}
}
if (IsCompleted)
{
await CompleteAction();
await DisposeAsync();
}
}
catch (Exception ex)
{
Logger?.LogError($"Action [{Type} - {Id}] execution error: {ex.Message}");
lock (_lock)
{
SetStatus(ActionEvent.FAILED);
}
ResultDescription = $"Thực hiện action [{Type} - {Id}] xảy ra lỗi: {ex.Message}";
await DisposeAsync();
}
}
private void InitializeStatus()
{
var builder = new StateMachineDefinitionBuilder<ActionStatus, ActionEvent>();
builder.In(ActionStatus.WAITING)
.ExecuteOnEntry(() => { CurrentStatus = ActionStatus.WAITING; })
.On(ActionEvent.INITIALIZING).Goto(ActionStatus.INITIALIZING)
.On(ActionEvent.PAUSED).Goto(ActionStatus.PAUSED)
.On(ActionEvent.FAILED).Goto(ActionStatus.FAILED);
builder.In(ActionStatus.INITIALIZING)
.ExecuteOnEntry(() => { CurrentStatus = ActionStatus.INITIALIZING; })
.On(ActionEvent.RUNNING).Goto(ActionStatus.RUNNING)
.On(ActionEvent.PAUSED).Goto(ActionStatus.PAUSED)
.On(ActionEvent.FINISHED).Goto(ActionStatus.FINISHED)
.On(ActionEvent.FAILED).Goto(ActionStatus.FAILED);
builder.In(ActionStatus.RUNNING)
.ExecuteOnEntry(() => { CurrentStatus = ActionStatus.RUNNING; })
.On(ActionEvent.PAUSED).Goto(ActionStatus.PAUSED)
.On(ActionEvent.FINISHED).Goto(ActionStatus.FINISHED)
.On(ActionEvent.FAILED).Goto(ActionStatus.FAILED);
builder.In(ActionStatus.PAUSED)
.ExecuteOnEntry(() => { CurrentStatus = ActionStatus.PAUSED; })
.On(ActionEvent.WAITING).Goto(ActionStatus.WAITING)
.On(ActionEvent.INITIALIZING).Goto(ActionStatus.INITIALIZING)
.On(ActionEvent.RUNNING).Goto(ActionStatus.RUNNING)
.On(ActionEvent.FAILED).Goto(ActionStatus.FAILED);
builder.In(ActionStatus.FINISHED)
.ExecuteOnEntry(() =>
{
CurrentStatus = ActionStatus.FINISHED;
CompletionTime = GetCurrentTimeMs();
});
builder.In(ActionStatus.FAILED)
.ExecuteOnEntry(() =>
{
CurrentStatus = ActionStatus.FAILED;
CompletionTime = GetCurrentTimeMs();
});
_stateMachine = builder
.WithInitialState(ActionStatus.WAITING)
.Build()
.CreatePassiveStateMachine();
_stateMachine.Start();
}
private static long GetCurrentTimeMs()
{
return Stopwatch.GetTimestamp() * 1000 / Stopwatch.Frequency;
}
protected void SetStatus(ActionEvent eventStatus)
{
lock (_lock)
{
_stateMachine?.Fire(eventStatus);
}
}
public async ValueTask DisposeAsync()
{
bool shouldStop;
lock (_lock)
{
if (_isDisposed) return;
_isDisposed = true;
shouldStop = !IsCompleted;
}
if (shouldStop) await StopAction();
await CleanupAction();
ActionTimer?.Dispose();
ActionTimer = null;
GC.SuppressFinalize(this);
}
}

View File

@@ -0,0 +1,40 @@
using RobotNet.VDA5050.Factsheet;
using RobotNet.VDA5050.Type;
namespace RobotNet10.RobotApp.Services.Robot.Actions;
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)]
public class RobotActionAttribute : Attribute
{
public ActionType ActionType { get; }
public IReadOnlyList<ActionScope> ActionScopes { get; }
public IReadOnlyList<BlockingType> BlockingTypes { get; }
public string? ActionDescription { get; }
public string? ResultDescription { get; }
public RobotActionAttribute(
ActionType actionType,
ActionScope[] scopes,
BlockingType[] blockingTypes,
string? description = null,
string? resultDescription = null)
{
ArgumentNullException.ThrowIfNull(scopes);
ArgumentNullException.ThrowIfNull(blockingTypes);
if (!Enum.IsDefined(actionType))
{
throw new ArgumentException("Invalid action type.", nameof(actionType));
}
if (scopes.Length == 0)
throw new ArgumentException("Scopes cannot be empty.", nameof(scopes));
if (blockingTypes.Length == 0)
throw new ArgumentException("Blocking types cannot be empty.", nameof(blockingTypes));
ActionType = actionType;
ActionScopes = Array.AsReadOnly(scopes);
BlockingTypes = Array.AsReadOnly(blockingTypes);
ActionDescription = description;
ResultDescription = resultDescription;
}
}

View File

@@ -0,0 +1,71 @@
using RobotNet.VDA5050.Factsheet;
using RobotNet.VDA5050.Type;
using RobotNet10.RobotApp.Services.Exceptions;
using System.Collections.ObjectModel;
namespace RobotNet10.RobotApp.Services.Robot.Actions;
[RobotAction(ActionType.EXAMPLE,
[ActionScope.INSTANT, ActionScope.NODE, ActionScope.EDGE],
[BlockingType.NONE, BlockingType.SOFT, BlockingType.HARD],
"This is an example robot action.",
"Example robot action completed.")]
public class RobotActionExample(IServiceProvider serviceProvider) : RobotAction(serviceProvider)
{
private static readonly ReadOnlyCollection<ActionParameter> ActionParameters = new List<ActionParameter>
{
new() {
Key = "exampleParam",
ValueDataType = ValueDataType.STRING,
Description = "This is an example string parameter.",
IsOptional = true
},
}.AsReadOnly();
string paramExample = "";
protected override void Initialize()
{
base.Initialize();
if (ActionParameters.Count > 0)
{
foreach (var parameterStore in ActionParameters)
{
if (!parameterStore.IsOptional)
{
if (Action is null || Action.ActionParameters is null || !Action.ActionParameters.Any(a => a.Key == parameterStore.Key)) throw new ActionException($"Thiếu tham số bắt buộc '{parameterStore.Key}' cho action {Type}.");
}
}
Parameters = Action is null || Action.ActionParameters is null ? [] : Action.ActionParameters;
}
var para = Parameters.FirstOrDefault(p => p.Key == "exampleParam") ?? throw new ActionException($"Action {Type} không tìm thấy parameter key 'exampleParam'");
paramExample = para.Value?.ToString() ?? "";
}
protected override Task StartAction()
{
return base.StartAction();
}
protected override Task ExecuteAction()
{
return base.ExecuteAction();
}
protected override Task StopAction()
{
return base.StopAction();
}
protected override Task PauseAction()
{
return base.PauseAction();
}
protected override Task ResumeAction()
{
return base.ResumeAction();
}
}

View File

@@ -0,0 +1,105 @@
using RobotNet.VDA5050.Type;
using RobotNet10.RobotApp.Interfaces;
namespace RobotNet10.RobotApp.Services.Robot.Actions;
public class RobotActionProvider(Logger<RobotActionProvider> Logger, IServiceProvider ServiceProvider) : BackgroundService, IRobotActionProvider
{
public bool IsInitialized { get; private set; }
private Dictionary<ActionType, Type> Actions = [];
public RobotAction GetRobotAction(ActionType type)
{
if (!Actions.TryGetValue(type, out var actionType))
{
Logger.Error($"RobotAction not found for ActionType: {type}");
throw new InvalidOperationException($"RobotAction not found for ActionType: {type}");
}
try
{
var instance = ActivatorUtilities.CreateInstance(ServiceProvider, actionType);
if (instance is not RobotAction robotAction)
{
Logger.Error($"Type {actionType.Name} is not a RobotAction");
throw new InvalidOperationException($"Type {actionType.Name} is not a RobotAction");
}
return robotAction;
}
catch (Exception ex)
{
Logger.Error($"Error creating instance of {actionType.Name}: {ex.Message}");
throw;
}
}
private static Dictionary<ActionType, Type> DiscoverActions()
{
var actionTypes = new Dictionary<ActionType, Type>();
var assemblies = AppDomain.CurrentDomain.GetAssemblies().Where(a => a.FullName?.StartsWith("RobotNet10.RobotApp") == true);
foreach (var assembly in assemblies)
{
var types = assembly.GetTypes().Where(t => t.IsClass && !t.IsAbstract && t.IsSubclassOf(typeof(RobotAction)));
foreach (var type in types)
{
var attributes = type.GetCustomAttributes(typeof(RobotActionAttribute), false);
if (attributes.Length > 0)
{
foreach(var attribute in attributes)
{
if(attribute is RobotActionAttribute robotAttribute)
{
actionTypes[robotAttribute.ActionType] = type;
break;
}
}
}
}
}
return actionTypes;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
await Task.Yield();
try
{
Logger.Info("Initializing RobotActionProvider...");
Actions = DiscoverActions();
IsInitialized = true;
Logger.Info($"Discovered {Actions.Count} robot actions.");
}
catch (Exception ex)
{
Logger.Warning($"Failed to discover robot actions. {ex}");
throw;
}
}
public RobotAction[] GetRobotActions()
{
try
{
List<RobotAction> robotActions = [];
foreach (var actionType in Actions.Values)
{
var instance = ActivatorUtilities.CreateInstance(ServiceProvider, actionType);
if (instance is not RobotAction robotAction)
{
Logger.Error($"Type {actionType.Name} is not a RobotAction");
throw new InvalidOperationException($"Type {actionType.Name} is not a RobotAction");
}
robotActions.Add(robotAction);
}
return [.. robotActions];
}
catch (Exception ex)
{
throw new Exception ($"Error get RobotActions: {ex.Message}");
}
}
}

View File

@@ -0,0 +1,56 @@
using RobotNet.VDA5050.Type;
using RobotNet10.RobotApp.Interfaces;
namespace RobotNet10.RobotApp.Services.Robot.Actions;
[RobotAction(ActionType.CANCEL_ORDER,
[ActionScope.INSTANT],
[BlockingType.NONE, BlockingType.SOFT, BlockingType.HARD],
"Hủy bỏ Order hiện tại của robot.",
"Robot đã hủy bỏ Order hiện tại.")]
public class RobotCancelOrderAction(IServiceProvider ServiceProvider) : RobotAction(ServiceProvider)
{
private const int MAX_TIMEOUT = 4000;
private int CountTimeout = 0;
private IOrder? RobotOrder;
private IAction? RobotAction;
protected override Task StartAction()
{
RobotOrder = ServiceProvider.GetRequiredService<IOrder>();
RobotAction = ServiceProvider.GetRequiredService<IAction>();
RobotOrder.StopOrder();
RobotAction.StopOrderAction();
CountTimeout = 0;
return base.StartAction();
}
protected override Task StopAction()
{
return Task.CompletedTask;
}
protected override Task ExecuteAction()
{
if (RobotOrder is null || RobotAction is null)
{
SetStatus(ActionEvent.FAILED);
ResultDescription = $"Không thể tìm thấy module quản lý {(RobotOrder is null ? "Order" : RobotAction is null ? "Action" : "")}";
}
else
{
if (RobotOrder.NodeStates.Length == 0 && RobotOrder.EdgeStates.Length == 0 && !RobotAction.HasActionRunning)
{
SetStatus(ActionEvent.FINISHED);
ResultDescription = string.IsNullOrEmpty(ActionAttribute.ResultDescription) ? ResultDescription : ActionAttribute.ResultDescription;
}
else if(CountTimeout++ > MAX_TIMEOUT / ActionInterval)
{
SetStatus(ActionEvent.FAILED);
ResultDescription = "Timeout action";
}
}
return base.ExecuteAction();
}
}

View File

@@ -0,0 +1,194 @@
using Microsoft.EntityFrameworkCore;
using RobotNet.VDA5050.Factsheet;
using RobotNet.VDA5050.Type;
using RobotNet10.RobotApp.Data;
using RobotNet10.RobotApp.Detection;
using RobotNet10.RobotApp.Interfaces;
using RobotNet10.RobotApp.Services.Exceptions;
using RobotNet10.RobotApp.Shared.Enums;
using RobotNet10.Shared.Detection;
using RobotNet10.Shared.Enum;
using RobotNet10.Shared.Numbers;
using System.Collections.ObjectModel;
using System.Text.Json;
namespace RobotNet10.RobotApp.Services.Robot.Actions;
[RobotAction(ActionType.DOCK_TO,
[ActionScope.INSTANT, ActionScope.NODE],
[BlockingType.HARD],
"Robot di chuyển vào vị trí đặc biệt",
"Robot đã dock đến vị trí sạc hoặc bến đỗ.")]
public class RobotDockToAction(IServiceProvider ServiceProvider) : RobotAction(ServiceProvider)
{
private static readonly ReadOnlyCollection<ActionParameter> ActionParameters = new List<ActionParameter>
{
new()
{
Key = "stationId",
Description = "ID của vị trí dock.",
ValueDataType = ValueDataType.STRING,
IsOptional = false,
},
new()
{
Key = "direction",
Description = "Hướng di chuyển: FORWARD, BACKWARD",
ValueDataType = ValueDataType.STRING,
IsOptional = true,
},
}.AsReadOnly();
private IMarkerDetector? MarkerDetector;
private INavigation? Navigation;
private IDetectSession? DetectSession;
private string? StationId;
private RobotDirection? Direction = null;
private const int MAX_TIMEOUT = 60000 * 5;
private int CountTimeout = 0;
private bool IsFindedGoal = false;
private bool IsStartDockTo = false;
private bool IsHasLoad = false;
protected override async Task StartAction()
{
MarkerDetector = ServiceProvider.GetRequiredService<IMarkerDetector>();
Navigation = ServiceProvider.GetRequiredService<INavigation>();
if (!string.IsNullOrEmpty(StationId))
{
using var scope = ServiceProvider.CreateScope();
var dbContext = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
var config = await dbContext.DockStationConfigs
.Include(d => d.MarkerEntries)
.FirstOrDefaultAsync(d => d.StationId == StationId && d.IsActive);
if (config is not null)
{
var MarkersSearchRequest = new MarkersSearchRequest
{
X = config.X,
Y = config.Y,
Yaw = config.Yaw,
Width = config.Width,
Length = config.Length,
MarkerSearchRequests = [.. config.MarkerEntries
.OrderBy(m => m.Priority)
.Select(m => new MarkerEntry
{
MarkerId = m.MarkerId ?? string.Empty,
Type = (MarkerType)m.Type,
Priority = m.Priority,
DeviceId = m.DeviceId ?? string.Empty,
Code = m.Code ?? string.Empty,
ReferencePoints = DeserializeReferencePoints(m.ReferencePointsJson)
})]
};
DetectSession = await MarkerDetector.CreateSessionAsync(MarkersSearchRequest);
var plcController = scope.ServiceProvider.GetRequiredService<IPlcController>();
IsHasLoad = plcController.SetHasLoadValue;
//var StateMachine = scope.ServiceProvider.GetRequiredService<RobotStateMachine>();
//StateMachine.Fire(RobotEventType.StartDocking);
await base.StartAction();
}
}
else
{
SetStatus(ActionEvent.FAILED);
ResultDescription = "Cannot get Marker Detection";
}
}
private static Vector2[] DeserializeReferencePoints(string? json)
{
if (string.IsNullOrWhiteSpace(json)) return [];
try { return JsonSerializer.Deserialize<Vector2[]>(json) ?? []; }
catch { return []; }
}
protected override Task StopAction()
{
Navigation?.CancelMovement();
return base.StopAction();
}
protected override Task CleanupAction()
{
DetectSession?.Dispose();
DetectSession = null;
MarkerDetector?.Dispose();
MarkerDetector = null;
//using var scope = ServiceProvider.CreateAsyncScope();
//var StateMachine = scope.ServiceProvider.GetRequiredService<RobotStateMachine>();
//StateMachine.Fire(RobotEventType.CompleteDocking);
return base.CleanupAction();
}
protected override Task ExecuteAction()
{
if (DetectSession is null)
{
SetStatus(ActionEvent.FAILED);
ResultDescription = "Module Detect Marker is not existed";
}
else if (Navigation is null)
{
SetStatus(ActionEvent.FAILED);
ResultDescription = "Module Navigation is not existed";
}
else
{
if (!IsFindedGoal) IsFindedGoal = DetectSession.Goal is not null;
if (CountTimeout++ > MAX_TIMEOUT / ActionInterval)
{
Navigation?.CancelMovement();
SetStatus(ActionEvent.FAILED);
ResultDescription = "Timeout action";
}
else if (!IsFindedGoal) base.ExecuteAction();
else
{
if (!IsStartDockTo)
{
Navigation.DockTo(DetectSession, IsHasLoad, Direction);
IsStartDockTo = true;
}
if (Navigation.State == NavigationState.Completed)
{
SetStatus(ActionEvent.FINISHED);
ResultDescription = string.IsNullOrEmpty(ActionAttribute.ResultDescription) ? ResultDescription : ActionAttribute.ResultDescription;
}
else if (Navigation.State == NavigationState.Error)
{
SetStatus(ActionEvent.FAILED);
ResultDescription = $"Action Handle [{Type} - {Id}] is failed: Navigation Failed";
}
}
}
return base.ExecuteAction();
}
protected override void Initialize()
{
base.Initialize();
if (ActionParameters.Count > 0)
{
foreach (var parameterStore in ActionParameters)
{
if (!parameterStore.IsOptional)
{
if (Action is null || Action.ActionParameters is null || !Action.ActionParameters.Any(a => a.Key == parameterStore.Key)) throw new ActionException($"Thiếu tham số bắt buộc '{parameterStore.Key}' cho action {Type}.");
}
}
Parameters = Action is null || Action.ActionParameters is null ? [] : Action.ActionParameters;
}
var stationPara = Parameters.FirstOrDefault(p => p.Key == "stationId") ?? throw new ActionException($"Action {Type} không tìm thấy parameter key 'stationId'");
StationId = stationPara.Value;
var directionPara = Parameters.FirstOrDefault(p => p.Key == "direction");
Direction = directionPara is not null && Enum.TryParse<RobotDirection>(directionPara.Value, out var direction) ? direction : null;
}
}

View File

@@ -0,0 +1,90 @@
using RobotNet.VDA5050.Type;
using RobotNet10.RobotApp.Client.Pages;
using RobotNet10.RobotApp.Interfaces;
using RobotNet10.RobotApp.Modules;
namespace RobotNet10.RobotApp.Services.Robot.Actions;
[RobotAction(ActionType.DROP,
[ActionScope.INSTANT, ActionScope.NODE, ActionScope.EDGE],
[BlockingType.HARD],
"Hạ thấp pallet.",
"Robot đã hạ thấp pallet.")]
public class RobotDropAction(IServiceProvider ServiceProvider) : RobotAction(ServiceProvider)
{
private const int MAX_TIMEOUT = 60000;
private int CountTimeout = 0;
private ILoad? LoadManager;
private ILiftModule? LiftModule;
private IPlcController? PlcController;
private CancellationTokenSource CancellationToken = new();
protected override async Task StartAction()
{
try
{
LoadManager = ServiceProvider.GetRequiredService<ILoad>();
LiftModule = ServiceProvider.GetRequiredService<ILiftModule>();
PlcController = ServiceProvider.GetRequiredService<IPlcController>();
if (!LiftModule.IsReady)
{
SetStatus(ActionEvent.FAILED);
ResultDescription = "Lift module not ready";
return;
}
CancellationToken = new CancellationTokenSource();
PlcController.SetOperationState(OperationState.Lifting);
await LiftModule.LiftDownAsync(CancellationToken.Token);
CountTimeout = 0;
await base.StartAction();
}
catch (Exception ex)
{
SetStatus(ActionEvent.FAILED);
ResultDescription = $"Drop Failed: {ex.Message}";
}
}
protected override Task StopAction()
{
CancellationToken.Cancel();
return base.StopAction();
}
protected override Task CleanupAction()
{
CancellationToken.Dispose();
PlcController?.SetOperationState(OperationState.None);
return base.CleanupAction();
}
protected override async Task ExecuteAction()
{
if (LiftModule is null)
{
SetStatus(ActionEvent.FAILED);
ResultDescription = "Lift module not found";
}
else if (LiftModule.State == LiftModuleState.Error)
{
SetStatus(ActionEvent.FAILED);
ResultDescription = "Lift module error";
}
else if (LiftModule.Position == LiftPosition.Bottom)
{
LoadManager?.ClearLoad();
SetStatus(ActionEvent.FINISHED);
ResultDescription = string.IsNullOrEmpty(ActionAttribute.ResultDescription) ? "Robot has dropped the load." : ActionAttribute.ResultDescription;
}
else if (CountTimeout++ > MAX_TIMEOUT / ActionInterval)
{
CancellationToken.Cancel();
SetStatus(ActionEvent.FAILED);
ResultDescription = "Timeout action";
}
await base.ExecuteAction();
}
}

View File

@@ -0,0 +1,25 @@
using RobotNet.VDA5050.Type;
using RobotNet10.RobotApp.Interfaces;
namespace RobotNet10.RobotApp.Services.Robot.Actions;
[RobotAction(ActionType.FACTSHEET_REQUEST,
[ActionScope.INSTANT, ActionScope.NODE, ActionScope.EDGE],
[BlockingType.NONE, BlockingType.SOFT, BlockingType.HARD],
"Yêu cầu gửi Factsheet robot ngay lập tức.",
"Robot đã gửi Factsheet ngay lập tức.")]
public class RobotFactsheetRequestAction(IServiceProvider ServiceProvider) : RobotAction(ServiceProvider)
{
protected override async Task StartAction()
{
var RobotFactsheet = ServiceProvider.GetRequiredService<IFactsheet>();
await RobotFactsheet.PubFactsheet();
SetStatus(ActionEvent.FINISHED);
ResultDescription = string.IsNullOrEmpty(ActionAttribute.ResultDescription) ? ResultDescription : ActionAttribute.ResultDescription;
}
protected override Task ExecuteAction()
{
return base.ExecuteAction();
}
}

View File

@@ -0,0 +1,94 @@
using RobotNet.VDA5050.Factsheet;
using RobotNet.VDA5050.Type;
using RobotNet10.RobotApp.Interfaces;
using RobotNet10.RobotApp.Services.Exceptions;
using System.Collections.ObjectModel;
namespace RobotNet10.RobotApp.Services.Robot.Actions;
[RobotAction(ActionType.INIT_POSITION,
[ActionScope.INSTANT, ActionScope.NODE],
[BlockingType.HARD],
"Khởi tạo lại vị trí robot.",
"Robot đã khởi tạo lại vị trí.")]
public class RobotInitPositionAction(IServiceProvider ServiceProvider) : RobotAction(ServiceProvider)
{
private static readonly ReadOnlyCollection<ActionParameter> ActionParameters = new List<ActionParameter>
{
new()
{
Key = "x",
Description = "Tọa độ X của vị trí khởi tạo.",
ValueDataType = ValueDataType.FLOAT,
IsOptional = false,
},
new()
{
Key = "y",
Description = "Tọa độ Y của vị trí khởi tạo.",
ValueDataType = ValueDataType.FLOAT,
IsOptional = false,
},
new()
{
Key = "theta",
Description = "Góc quay (theta) của vị trí khởi tạo. (rad)",
ValueDataType = ValueDataType.FLOAT,
IsOptional = false,
}
}.AsReadOnly();
double X = 0;
double Y = 0;
double Theta = 0;
ILocalization? Localization;
protected override Task StartAction()
{
Localization = ServiceProvider.GetRequiredService<ILocalization>();
var initPose = Localization.SetInitializePosition(X, Y, Theta);
if (!initPose.IsSuccess)
{
SetStatus(ActionEvent.FAILED);
ResultDescription = initPose.Message;
}
else
{
SetStatus(ActionEvent.FINISHED);
ResultDescription = string.IsNullOrEmpty(ActionAttribute.ResultDescription) ? ResultDescription : ActionAttribute.ResultDescription;
}
return base.StartAction();
}
protected override void Initialize()
{
base.Initialize();
if (ActionParameters.Count > 0)
{
foreach (var parameterStore in ActionParameters)
{
if (!parameterStore.IsOptional)
{
if (Action is null || Action.ActionParameters is null || !Action.ActionParameters.Any(a => a.Key == parameterStore.Key)) throw new ActionException($"Thiếu tham số bắt buộc '{parameterStore.Key}' cho action {Type}.");
}
}
Parameters = Action is null || Action.ActionParameters is null ? [] : Action.ActionParameters;
}
var xPara = Parameters.FirstOrDefault(p => p.Key == "x") ?? throw new ActionException($"Action {Type} không tìm thấy parameter key 'x'");
var yPara = Parameters.FirstOrDefault(p => p.Key == "y") ?? throw new ActionException($"Action {Type} không tìm thấy parameter key 'y'");
var thetaPara = Parameters.FirstOrDefault(p => p.Key == "theta") ?? throw new ActionException($"Action {Type} không tìm thấy parameter key 'theta'");
var xParse = double.TryParse(xPara.Value, out double xData);
var yParse = double.TryParse(yPara.Value, out double yData);
var thetaParse = double.TryParse(thetaPara.Value, out double thetaData);
if (!xParse) throw new ActionException($"Action {Type} có parameter 'x' không đúng kiểu dữ liệu");
if (!yParse) throw new ActionException($"Action {Type} có parameter 'y' không đúng kiểu dữ liệu");
if (!thetaParse) throw new ActionException($"Action {Type} có parameter 'theta' không đúng kiểu dữ liệu");
X = xData;
Y = yData;
Theta = thetaData;
}
}

View File

@@ -0,0 +1,95 @@
using RobotNet.VDA5050.Factsheet;
using RobotNet.VDA5050.Type;
using RobotNet10.RobotApp.Interfaces;
using RobotNet10.RobotApp.Modules;
using RobotNet10.RobotApp.Services.Exceptions;
namespace RobotNet10.RobotApp.Services.Robot.Actions;
[RobotAction(ActionType.LIFT_ROTATE,
[ActionScope.INSTANT, ActionScope.NODE, ActionScope.EDGE],
[BlockingType.HARD],
"Xoay bàn nâng của robot.",
"Robot đã xoay bàn nâng.")]
public class RobotLiftRotateAction(IServiceProvider ServiceProvider) : RobotAction(ServiceProvider)
{
private static readonly IReadOnlyList<ActionParameter> ActionParameters = new List<ActionParameter>
{
new()
{
Key = "angle",
Description = "Góc xoay của bàn nâng. (rad)",
ValueDataType = ValueDataType.FLOAT,
IsOptional = false,
}
}.AsReadOnly();
private double Angle = 0; // Degree
private IRotationModule? RotationModule;
private IPlcController? PlcController;
private const int MAX_TIMEOUT = 60000 * 2;
private int CountTimeout = 0;
protected override Task StartAction()
{
RotationModule = ServiceProvider.GetRequiredService<IRotationModule>();
PlcController = ServiceProvider.GetRequiredService<IPlcController>();
PlcController.SetOperationState(OperationState.LiftRotating);
RotationModule.RotateToAngleAsync(Angle);
return base.StartAction();
}
protected override Task CleanupAction()
{
PlcController?.SetOperationState(OperationState.None);
return base.CleanupAction();
}
protected override async Task ExecuteAction()
{
if (RotationModule is null)
{
SetStatus(ActionEvent.FAILED);
ResultDescription = "Không tìm thấy mô-đun xoay.";
}
else if (RotationModule.State == RotationModuleState.Error)
{
SetStatus(ActionEvent.FAILED);
ResultDescription = "Mô-đun xoay gặp lỗi.";
}
else if (RotationModule.State == RotationModuleState.Ready && Math.Abs(await RotationModule.GetCurrentAngleAsync() - Angle) < 1)
{
SetStatus(ActionEvent.FINISHED);
ResultDescription = string.IsNullOrEmpty(ActionAttribute.ResultDescription) ? ResultDescription : ActionAttribute.ResultDescription;
}
else if (CountTimeout++ >= MAX_TIMEOUT / ActionInterval)
{
SetStatus(ActionEvent.FAILED);
ResultDescription = "Timeout action";
}
await base.ExecuteAction();
}
protected override void Initialize()
{
base.Initialize();
if (ActionParameters.Count > 0)
{
foreach (var parameterStore in ActionParameters)
{
if (!parameterStore.IsOptional)
{
if (Action is null || Action.ActionParameters is null || !Action.ActionParameters.Any(a => a.Key == parameterStore.Key)) throw new ActionException($"Thiếu tham số bắt buộc '{parameterStore.Key}' cho action {Type}.");
}
}
Parameters = Action is null || Action.ActionParameters is null ? [] : Action.ActionParameters;
}
var anglePara = Parameters.FirstOrDefault(p => p.Key == "angle") ?? throw new ActionException($"Action {Type} không tìm thấy parameter key 'angle'");
var angleParse = double.TryParse(anglePara.Value, out double angleData);
if (!angleParse) throw new ActionException($"Action {Type} có parameter 'angle' không đúng kiểu dữ liệu");
Angle = angleData * 180 / Math.PI;
}
}

View File

@@ -0,0 +1,128 @@
using RobotNet.VDA5050.Factsheet;
using RobotNet.VDA5050.Type;
using RobotNet10.RobotApp.Interfaces;
using RobotNet10.RobotApp.Services.Exceptions;
using RobotNet10.RobotApp.Shared.Enums;
using System.Collections.ObjectModel;
namespace RobotNet10.RobotApp.Services.Robot.Actions;
[RobotAction(ActionType.MOVE_STRAIGHT_TO_COOR,
[ActionScope.INSTANT],
[BlockingType.HARD],
"Di chuyển thẳng đến tọa độ xác định.",
"Robot đã di chuyển thẳng đến tọa độ xác định.")]
public class RobotMoveStraightToCoorAction(IServiceProvider ServiceProvider) : RobotAction(ServiceProvider)
{
private static readonly ReadOnlyCollection<ActionParameter> ActionParameters = new List<ActionParameter>
{
new()
{
Key = "x",
Description = "Tọa độ X đích đến.",
ValueDataType = ValueDataType.FLOAT,
IsOptional = false,
},
new()
{
Key = "y",
Description = "Tọa độ Y đích đến.",
ValueDataType = ValueDataType.FLOAT,
IsOptional = false,
},
new()
{
Key = "direction",
Description = "Hướng di chuyển: FORWARD, BACKWARD",
ValueDataType = ValueDataType.STRING,
IsOptional = true,
},
}.AsReadOnly();
private INavigation? Navigation;
private double TargetX;
private double TargetY;
private RobotDirection? Direction = null;
private const int MAX_TIMEOUT = 60000 * 5;
private int CountTimeout = 0;
private bool IsStartMoveStraight = false;
private bool IsHasLoad = false;
protected override Task StartAction()
{
Navigation = ServiceProvider.GetRequiredService<INavigation>();
using var scope = ServiceProvider.CreateScope();
var plcController = scope.ServiceProvider.GetRequiredService<IPlcController>();
IsHasLoad = plcController.SetHasLoadValue;
return base.StartAction();
}
protected override Task StopAction()
{
Navigation?.CancelMovement();
return base.StopAction();
}
protected override Task ExecuteAction()
{
if (Navigation is null)
{
SetStatus(ActionEvent.FAILED);
ResultDescription = "Module Navigation is not existed";
}
else
{
if (CountTimeout++ > MAX_TIMEOUT / ActionInterval)
{
Navigation?.CancelMovement();
SetStatus(ActionEvent.FAILED);
ResultDescription = "Timeout action";
}
else
{
if (!IsStartMoveStraight)
{
Navigation.MoveStraight(TargetX, TargetY, IsHasLoad, Direction);
IsStartMoveStraight = true;
}
if (Navigation.State == NavigationState.Completed)
{
SetStatus(ActionEvent.FINISHED);
ResultDescription = string.IsNullOrEmpty(ActionAttribute.ResultDescription) ? ResultDescription : ActionAttribute.ResultDescription;
}
else if (Navigation.State == NavigationState.Error)
{
SetStatus(ActionEvent.FAILED);
ResultDescription = $"Action Handle [{Type} - {Id}] is failed: Navigation Failed";
}
}
}
return base.ExecuteAction();
}
protected override void Initialize()
{
base.Initialize();
if (ActionParameters.Count > 0)
{
foreach (var parameterStore in ActionParameters)
{
if (!parameterStore.IsOptional)
{
if (Action is null || Action.ActionParameters is null || !Action.ActionParameters.Any(a => a.Key == parameterStore.Key))
throw new ActionException($"Thiếu tham số bắt buộc '{parameterStore.Key}' cho action {Type}.");
}
}
Parameters = Action is null || Action.ActionParameters is null ? [] : Action.ActionParameters;
}
var xPara = Parameters.FirstOrDefault(p => p.Key == "x") ?? throw new ActionException($"Action {Type} không tìm thấy parameter key 'x'");
var yPara = Parameters.FirstOrDefault(p => p.Key == "y") ?? throw new ActionException($"Action {Type} không tìm thấy parameter key 'y'");
TargetX = double.Parse(xPara.Value);
TargetY = double.Parse(yPara.Value);
var directionPara = Parameters.FirstOrDefault(p => p.Key == "direction");
Direction = directionPara is not null && Enum.TryParse<RobotDirection>(directionPara.Value, out var direction) ? direction : null;
}
}

View File

@@ -0,0 +1,132 @@
using RobotNet.VDA5050.Factsheet;
using RobotNet.VDA5050.Type;
using RobotNet10.RobotApp.Interfaces;
using RobotNet10.RobotApp.Services.Exceptions;
using RobotNet10.RobotApp.Shared.Enums;
using System.Collections.ObjectModel;
namespace RobotNet10.RobotApp.Services.Robot.Actions;
[RobotAction(ActionType.MOVE_STRAIGHT_WITH_DISTANCE,
[ActionScope.INSTANT],
[BlockingType.HARD],
"Di chuyển thẳng với khoảng cách xác định.",
"Robot đã di chuyển thẳng với khoảng cách xác định.")]
public class RobotMoveStraightWithDistanceAction(IServiceProvider ServiceProvider) : RobotAction(ServiceProvider)
{
private static readonly ReadOnlyCollection<ActionParameter> ActionParameters = new List<ActionParameter>
{
new()
{
Key = "distance",
Description = "Khoảng cách di chuyển. (m)",
ValueDataType = ValueDataType.FLOAT,
IsOptional = false,
},
new()
{
Key = "direction",
Description = "Hướng di chuyển: FORWARD, BACKWARD",
ValueDataType = ValueDataType.STRING,
IsOptional = true,
},
new()
{
Key = "angle",
Description = "Góc di chuyển so với hướng hiện tại của robot. (rad)",
ValueDataType = ValueDataType.FLOAT,
IsOptional = false,
}
}.AsReadOnly();
private INavigation? Navigation;
private ILocalization? Localization;
private double Distance;
private double Angle;
private RobotDirection? Direction = null;
private const int MAX_TIMEOUT = 60000 * 5;
private int CountTimeout = 0;
private bool IsStartMoveStraight = false;
private bool IsHasLoad = false;
protected override Task StartAction()
{
Navigation = ServiceProvider.GetRequiredService<INavigation>();
Localization = ServiceProvider.GetRequiredService<ILocalization>();
using var scope = ServiceProvider.CreateScope();
var plcController = scope.ServiceProvider.GetRequiredService<IPlcController>();
IsHasLoad = plcController.SetHasLoadValue;
return base.StartAction();
}
protected override Task StopAction()
{
Navigation?.CancelMovement();
return base.StopAction();
}
protected override Task ExecuteAction()
{
if (Navigation is null || Localization is null)
{
SetStatus(ActionEvent.FAILED);
ResultDescription = "Module Navigation or Localization is not existed";
}
else
{
if (CountTimeout++ > MAX_TIMEOUT / ActionInterval)
{
Navigation?.CancelMovement();
SetStatus(ActionEvent.FAILED);
ResultDescription = "Timeout action";
}
else
{
if (!IsStartMoveStraight)
{
double targetX = Localization.X + Distance * Math.Cos(Angle);
double targetY = Localization.Y + Distance * Math.Sin(Angle);
Navigation.MoveStraight(targetX, targetY, IsHasLoad, Direction);
IsStartMoveStraight = true;
}
if (Navigation.State == NavigationState.Completed)
{
SetStatus(ActionEvent.FINISHED);
ResultDescription = string.IsNullOrEmpty(ActionAttribute.ResultDescription) ? ResultDescription : ActionAttribute.ResultDescription;
}
else if (Navigation.State == NavigationState.Error)
{
SetStatus(ActionEvent.FAILED);
ResultDescription = $"Action Handle [{Type} - {Id}] is failed: Navigation Failed";
}
}
}
return base.ExecuteAction();
}
protected override void Initialize()
{
base.Initialize();
if (ActionParameters.Count > 0)
{
foreach (var parameterStore in ActionParameters)
{
if (!parameterStore.IsOptional)
{
if (Action is null || Action.ActionParameters is null || !Action.ActionParameters.Any(a => a.Key == parameterStore.Key))
throw new ActionException($"Thiếu tham số bắt buộc '{parameterStore.Key}' cho action {Type}.");
}
}
Parameters = Action is null || Action.ActionParameters is null ? [] : Action.ActionParameters;
}
var distancePara = Parameters.FirstOrDefault(p => p.Key == "distance") ?? throw new ActionException($"Action {Type} không tìm thấy parameter key 'distance'");
Distance = double.Parse(distancePara.Value);
var anglePara = Parameters.FirstOrDefault(p => p.Key == "angle") ?? throw new ActionException($"Action {Type} không tìm thấy parameter key 'angle'");
Angle = double.Parse(anglePara.Value);
var directionPara = Parameters.FirstOrDefault(p => p.Key == "direction");
Direction = directionPara is not null && Enum.TryParse<RobotDirection>(directionPara.Value, out var direction) ? direction : null;
}
}

View File

@@ -0,0 +1,39 @@
using RobotNet.VDA5050.Type;
using RobotNet10.RobotApp.Interfaces;
namespace RobotNet10.RobotApp.Services.Robot.Actions;
[RobotAction(ActionType.MUTED_BASE_OFF,
[ActionScope.INSTANT, ActionScope.NODE, ActionScope.EDGE],
[BlockingType.NONE, BlockingType.SOFT, BlockingType.HARD],
"Tắt chế độ muted base robot.",
"Robot đã tắt chế độ muted base.")]
public class RobotMutedBaseOffAction(IServiceProvider ServiceProvider) : RobotAction(ServiceProvider)
{
private IPlcController? PlcController;
private const int MAX_TIMEOUT = 4000;
private int CountTimeout = 0;
protected override Task StartAction()
{
PlcController = ServiceProvider.GetRequiredService<IPlcController>();
PlcController.SetMutedBase(false);
return base.StartAction();
}
protected override Task ExecuteAction()
{
if (PlcController is not null && !PlcController.MutedBase)
{
SetStatus(ActionEvent.FINISHED);
ResultDescription = string.IsNullOrEmpty(ActionAttribute.ResultDescription) ? ResultDescription : ActionAttribute.ResultDescription;
}
else if (CountTimeout++ > MAX_TIMEOUT / ActionInterval)
{
SetStatus(ActionEvent.FAILED);
ResultDescription = "Timeout action";
}
return base.ExecuteAction();
}
}

View File

@@ -0,0 +1,38 @@
using RobotNet.VDA5050.Type;
using RobotNet10.RobotApp.Interfaces;
namespace RobotNet10.RobotApp.Services.Robot.Actions;
[RobotAction(ActionType.MUTED_BASE_ON,
[ActionScope.INSTANT, ActionScope.NODE, ActionScope.EDGE],
[BlockingType.NONE, BlockingType.SOFT, BlockingType.HARD],
"Bật chế độ muted base robot.",
"Robot đã bật chế độ muted base.")]
public class RobotMutedBaseOnAction(IServiceProvider ServiceProvider) : RobotAction(ServiceProvider)
{
private IPlcController? PlcController;
private const int MAX_TIMEOUT = 4000;
private int CountTimeout = 0;
protected override Task StartAction()
{
PlcController = ServiceProvider.GetRequiredService<IPlcController>();
PlcController.SetMutedBase(true);
return base.StartAction();
}
protected override Task ExecuteAction()
{
if(PlcController is not null && PlcController.MutedBase)
{
SetStatus(ActionEvent.FINISHED);
ResultDescription = string.IsNullOrEmpty(ActionAttribute.ResultDescription) ? ResultDescription : ActionAttribute.ResultDescription;
}
else if (CountTimeout++ > MAX_TIMEOUT / ActionInterval)
{
SetStatus(ActionEvent.FAILED);
ResultDescription = "Timeout action";
}
return base.ExecuteAction();
}
}

View File

@@ -0,0 +1,37 @@
using RobotNet.VDA5050.Type;
using RobotNet10.RobotApp.Interfaces;
namespace RobotNet10.RobotApp.Services.Robot.Actions;
[RobotAction(ActionType.MUTED_LOAD_OFF,
[ActionScope.INSTANT, ActionScope.NODE, ActionScope.EDGE],
[BlockingType.NONE, BlockingType.SOFT, BlockingType.HARD],
"Tắt chế độ muted load robot.",
"Robot đã tắt chế độ muted load.")]
public class RobotMutedLoadOffAction(IServiceProvider ServiceProvider) : RobotAction(ServiceProvider)
{
private IPlcController? PlcController;
private const int MAX_TIMEOUT = 4000;
private int CountTimeout = 0;
protected override Task StartAction()
{
PlcController = ServiceProvider.GetRequiredService<IPlcController>();
PlcController.SetMutedLoad(false);
return base.StartAction();
}
protected override Task ExecuteAction()
{
if (PlcController is not null && !PlcController.MutedLoad)
{
SetStatus(ActionEvent.FINISHED);
ResultDescription = string.IsNullOrEmpty(ActionAttribute.ResultDescription) ? ResultDescription : ActionAttribute.ResultDescription;
}
else if (CountTimeout++ > MAX_TIMEOUT / ActionInterval)
{
SetStatus(ActionEvent.FAILED);
ResultDescription = "Timeout action";
}
return base.ExecuteAction();
}
}

View File

@@ -0,0 +1,38 @@
using RobotNet.VDA5050.Type;
using RobotNet10.RobotApp.Interfaces;
namespace RobotNet10.RobotApp.Services.Robot.Actions;
[RobotAction(ActionType.MUTED_LOAD_ON,
[ActionScope.INSTANT, ActionScope.NODE, ActionScope.EDGE],
[BlockingType.NONE, BlockingType.SOFT, BlockingType.HARD],
"Bật chế độ muted load robot.",
"Robot đã bật chế độ muted load.")]
public class RobotMutedLoadOnAction(IServiceProvider ServiceProvider) : RobotAction(ServiceProvider)
{
private IPlcController? PlcController;
private const int MAX_TIMEOUT = 4000;
private int CountTimeout = 0;
protected override Task StartAction()
{
PlcController = ServiceProvider.GetRequiredService<IPlcController>();
PlcController.SetMutedLoad(true);
return base.StartAction();
}
protected override Task ExecuteAction()
{
if (PlcController is not null && PlcController.MutedLoad)
{
SetStatus(ActionEvent.FINISHED);
ResultDescription = string.IsNullOrEmpty(ActionAttribute.ResultDescription) ? ResultDescription : ActionAttribute.ResultDescription;
}
else if (CountTimeout++ > MAX_TIMEOUT / ActionInterval)
{
SetStatus(ActionEvent.FAILED);
ResultDescription = "Timeout action";
}
return base.ExecuteAction();
}
}

View File

@@ -0,0 +1,89 @@
using NLog;
using RobotNet.VDA5050.Type;
using RobotNet10.RobotApp.Client.Pages;
using RobotNet10.RobotApp.Interfaces;
using RobotNet10.RobotApp.Modules;
namespace RobotNet10.RobotApp.Services.Robot.Actions;
[RobotAction(ActionType.PICK,
[ActionScope.INSTANT, ActionScope.NODE, ActionScope.EDGE],
[BlockingType.HARD],
"Nâng cao pallet.",
"Robot đã nâng cao pallet.")]
public class RobotPickAction(IServiceProvider ServiceProvider) : RobotAction(ServiceProvider)
{
private const int MAX_TIMEOUT = 60000;
private int CountTimeout = 0;
private ILoad? LoadManager;
private ILiftModule? LiftModule;
private IPlcController? PlcController;
private CancellationTokenSource CancellationToken = new();
protected override async Task StartAction()
{
try
{
LoadManager = ServiceProvider.GetRequiredService<ILoad>();
LiftModule = ServiceProvider.GetRequiredService<ILiftModule>();
PlcController = ServiceProvider.GetRequiredService<IPlcController>();
if (!LiftModule.IsReady)
{
SetStatus(ActionEvent.FAILED);
ResultDescription = "Lift module not ready";
return;
}
CancellationToken = new CancellationTokenSource();
PlcController.SetOperationState(OperationState.Lifting);
_ = LiftModule.LiftUpAsync(CancellationToken.Token);
CountTimeout = 0;
await base.StartAction();
}
catch (Exception ex)
{
SetStatus(ActionEvent.FAILED);
ResultDescription = "Pick failed: " + ex.Message;
}
}
protected override Task StopAction()
{
CancellationToken.Cancel();
return base.StopAction();
}
protected override Task CleanupAction()
{
CancellationToken.Dispose();
PlcController?.SetOperationState(OperationState.None);
return base.CleanupAction();
}
protected override async Task ExecuteAction()
{
if (LiftModule is null)
{
SetStatus(ActionEvent.FAILED);
ResultDescription = "Lift module not found";
}
else if (LiftModule.State == LiftModuleState.Error)
{
SetStatus(ActionEvent.FAILED);
ResultDescription = "Lift module error";
}
else if (LiftModule.Position == LiftPosition.Top)
{
LoadManager?.AddLoad(new());
SetStatus(ActionEvent.FINISHED);
ResultDescription = string.IsNullOrEmpty(ActionAttribute.ResultDescription) ? "Robot has picked up the load." : ActionAttribute.ResultDescription;
}
else if (CountTimeout++ > MAX_TIMEOUT / ActionInterval)
{
CancellationToken.Cancel();
SetStatus(ActionEvent.FAILED);
ResultDescription = "Timeout action";
}
await base.ExecuteAction();
}
}

View File

@@ -0,0 +1,96 @@
using MudBlazor.Extensions;
using RobotNet.VDA5050.Factsheet;
using RobotNet.VDA5050.Type;
using RobotNet10.RobotApp.Interfaces;
using RobotNet10.RobotApp.Services.Exceptions;
using System.Collections.ObjectModel;
namespace RobotNet10.RobotApp.Services.Robot.Actions;
[RobotAction(ActionType.ROTATE,
[ActionScope.INSTANT, ActionScope.NODE, ActionScope.EDGE],
[BlockingType.HARD],
"Xoay robot tại chỗ.",
"Robot đã xoay tại chỗ.")]
public class RobotRotateAction(IServiceProvider ServiceProvider) : RobotAction(ServiceProvider)
{
private static readonly ReadOnlyCollection<ActionParameter> ActionParameters = new List<ActionParameter>
{
new()
{
Key = "angle",
Description = "Góc xoay của robot. (rad)",
ValueDataType = ValueDataType.FLOAT,
IsOptional = false,
}
}.AsReadOnly();
private double Angle = 0;
private INavigation? RobotNavigation;
private ILocalization? Localization;
private const int MAX_TIMEOUT = 60000 * 2;
private int CountTimeout = 0;
protected override Task StartAction()
{
RobotNavigation = ServiceProvider.GetRequiredService<INavigation>();
Localization = ServiceProvider.GetRequiredService<ILocalization>();
RobotNavigation.Rotate(Angle);
CountTimeout = 0;
return base.StartAction();
}
protected override Task StopAction()
{
RobotNavigation?.CancelMovement();
return base.StopAction();
}
protected override Task ExecuteAction()
{
if (RobotNavigation is null)
{
SetStatus(ActionEvent.FAILED);
ResultDescription = "Module Navigation is not existed";
}
else if (RobotNavigation.State == NavigationState.Completed && Localization is not null && Math.Abs(Localization.Theta - Angle) * 180 / Math.PI < 5)
{
SetStatus(ActionEvent.FINISHED);
ResultDescription = string.IsNullOrEmpty(ActionAttribute.ResultDescription) ? ResultDescription : ActionAttribute.ResultDescription;
}
else if (RobotNavigation.State == NavigationState.Error)
{
SetStatus(ActionEvent.FAILED);
ResultDescription = $"Action Handle [{Type} - {Id}] - angle: {Angle} is failed";
}
else if (CountTimeout++ > MAX_TIMEOUT / ActionInterval)
{
RobotNavigation?.CancelMovement();
SetStatus(ActionEvent.FAILED);
ResultDescription = "Timeout action";
}
return base.ExecuteAction();
}
protected override void Initialize()
{
base.Initialize();
if (ActionParameters.Count > 0)
{
foreach (var parameterStore in ActionParameters)
{
if (!parameterStore.IsOptional)
{
if (Action is null || Action.ActionParameters is null || !Action.ActionParameters.Any(a => a.Key == parameterStore.Key)) throw new ActionException($"Thiếu tham số bắt buộc '{parameterStore.Key}' cho action {Type}.");
}
}
Parameters = Action is null || Action.ActionParameters is null ? [] : Action.ActionParameters;
}
var anglePara = Parameters.FirstOrDefault(p => p.Key == "angle") ?? throw new ActionException($"Action {Type} không tìm thấy parameter key 'angle'");
var angleParse = double.TryParse(anglePara.Value, out double angleData);
if (!angleParse) throw new ActionException($"Action {Type} có parameter 'angle' không đúng kiểu dữ liệu");
Angle = angleData;
}
}

View File

@@ -0,0 +1,59 @@
using RobotNet.VDA5050.Factsheet;
using RobotNet.VDA5050.Type;
using RobotNet10.RobotApp.Services.Exceptions;
using System.Collections.ObjectModel;
namespace RobotNet10.RobotApp.Services.Robot.Actions;
[RobotAction(ActionType.ROTATE_KEEP_LIFT,
[ActionScope.INSTANT, ActionScope.NODE, ActionScope.EDGE],
[BlockingType.HARD],
"Xoay robot tại chỗ giữ nguyên trạng thái bàn nâng.",
"Robot đã xoay tại chỗ giữ nguyên trạng thái bàn nâng.")]
public class RobotRotateKeepLift(IServiceProvider ServiceProvider) : RobotAction(ServiceProvider)
{
private static readonly ReadOnlyCollection<ActionParameter> ActionParameters = new List<ActionParameter>
{
new()
{
Key = "angle",
Description = "Góc xoay của robot. (rad)",
ValueDataType = ValueDataType.FLOAT,
IsOptional = false,
}
}.AsReadOnly();
double Angle = 0;
protected override Task StartAction()
{
SetStatus(ActionEvent.FINISHED);
ResultDescription = string.IsNullOrEmpty(ActionAttribute.ResultDescription) ? ResultDescription : ActionAttribute.ResultDescription;
return base.StartAction();
}
protected override Task ExecuteAction()
{
return base.ExecuteAction();
}
protected override void Initialize()
{
base.Initialize();
if (ActionParameters.Count > 0)
{
foreach (var parameterStore in ActionParameters)
{
if (!parameterStore.IsOptional)
{
if (Action is null || Action.ActionParameters is null || !Action.ActionParameters.Any(a => a.Key == parameterStore.Key)) throw new ActionException($"Thiếu tham số bắt buộc '{parameterStore.Key}' cho action {Type}.");
}
}
Parameters = Action is null || Action.ActionParameters is null ? [] : Action.ActionParameters;
}
var anglePara = Parameters.FirstOrDefault(p => p.Key == "angle") ?? throw new ActionException($"Action {Type} không tìm thấy parameter key 'angle'");
var angleParse = double.TryParse(anglePara.Value, out double angleData);
if (!angleParse) throw new ActionException($"Action {Type} có parameter 'angle' không đúng kiểu dữ liệu");
Angle = angleData;
}
}

View File

@@ -0,0 +1,57 @@
using RobotNet.VDA5050.Factsheet;
using RobotNet.VDA5050.Type;
using RobotNet10.RobotApp.Services.Exceptions;
using System.Collections.ObjectModel;
namespace RobotNet10.RobotApp.Services.Robot.Actions;
[RobotAction(ActionType.SCRIPT,
[ActionScope.INSTANT, ActionScope.NODE, ActionScope.EDGE],
[BlockingType.NONE, BlockingType.SOFT, BlockingType.HARD],
"This is an script robot action.",
"Script robot action completed.")]
public class RobotScriptAction(IServiceProvider serviceProvider) : RobotAction(serviceProvider)
{
private static readonly ReadOnlyCollection<ActionParameter> ActionParameters = new List<ActionParameter>
{
new() {
Key = "missionName",
ValueDataType = ValueDataType.STRING,
Description = "This is an mission name of script mission.",
IsOptional = true
},
}.AsReadOnly();
private string Name = "";
protected override void Initialize()
{
base.Initialize();
if (ActionParameters.Count > 0)
{
foreach (var parameterStore in ActionParameters)
{
if (!parameterStore.IsOptional)
{
if (Action is null || Action.ActionParameters is null || !Action.ActionParameters.Any(a => a.Key == parameterStore.Key)) throw new ActionException($"Thiếu tham số bắt buộc '{parameterStore.Key}' cho action {Type}.");
}
}
Parameters = Action is null || Action.ActionParameters is null ? [] : Action.ActionParameters;
}
var para = Parameters.FirstOrDefault(p => p.Key == "missionName") ?? throw new ActionException($"Action {Type} không tìm thấy parameter key 'missionName'");
if(string.IsNullOrEmpty(para.Value)) throw new ActionException($"Action {Type}, parameter 'missionName' có value rỗng");
Name = para.Value;
}
protected override Task StartAction()
{
return base.StartAction();
}
protected override Task ExecuteAction()
{
return base.ExecuteAction();
}
}

View File

@@ -0,0 +1,23 @@
using RobotNet.VDA5050.Type;
namespace RobotNet10.RobotApp.Services.Robot.Actions;
[RobotAction(ActionType.START_CHARGING,
[ActionScope.INSTANT, ActionScope.NODE],
[BlockingType.HARD],
"Bắt đầu quá trình sạc pin.",
"Robot đã bắt đầu sạc pin.")]
public class RobotStartChargingAction(IServiceProvider ServiceProvider) : RobotAction(ServiceProvider)
{
protected override Task StartAction()
{
SetStatus(ActionEvent.FINISHED);
ResultDescription = string.IsNullOrEmpty(ActionAttribute.ResultDescription) ? ResultDescription : ActionAttribute.ResultDescription;
return base.StartAction();
}
protected override Task ExecuteAction()
{
return base.ExecuteAction();
}
}

View File

@@ -0,0 +1,26 @@
using RobotNet.VDA5050.Type;
using RobotNet10.RobotApp.Interfaces;
namespace RobotNet10.RobotApp.Services.Robot.Actions;
[RobotAction(ActionType.START_PAUSE,
[ActionScope.INSTANT],
[BlockingType.NONE, BlockingType.SOFT, BlockingType.HARD],
"Tam dừng robot.",
"Robot đã tạm dừng.")]
public class RobotStartPauseAction(IServiceProvider ServiceProvider) : RobotAction(ServiceProvider)
{
protected override Task StartAction()
{
var RobotController = ServiceProvider.GetRequiredService<IRobotController>();
RobotController.Pause();
SetStatus(ActionEvent.FINISHED);
ResultDescription = string.IsNullOrEmpty(ActionAttribute.ResultDescription) ? ResultDescription : ActionAttribute.ResultDescription;
return base.StartAction();
}
protected override Task ExecuteAction()
{
return base.ExecuteAction();
}
}

View File

@@ -0,0 +1,25 @@
using RobotNet.VDA5050.Type;
using RobotNet10.RobotApp.Interfaces;
namespace RobotNet10.RobotApp.Services.Robot.Actions;
[RobotAction(ActionType.STATE_REQUEST,
[ActionScope.INSTANT, ActionScope.NODE, ActionScope.EDGE],
[BlockingType.NONE, BlockingType.SOFT, BlockingType.HARD],
"Yêu cầu gửi trạng thái robot ngay lập tức.",
"Robot đã gửi trạng thái ngay lập tức.")]
public class RobotStateRequestAction(IServiceProvider ServiceProvider) : RobotAction(ServiceProvider)
{
protected override async Task StartAction()
{
var RobotStates = ServiceProvider.GetRequiredService<IState>();
await RobotStates.PubState();
SetStatus(ActionEvent.FINISHED);
ResultDescription = string.IsNullOrEmpty(ActionAttribute.ResultDescription) ? ResultDescription : ActionAttribute.ResultDescription;
}
protected override Task ExecuteAction()
{
return base.ExecuteAction();
}
}

View File

@@ -0,0 +1,23 @@
using RobotNet.VDA5050.Type;
namespace RobotNet10.RobotApp.Services.Robot.Actions;
[RobotAction(ActionType.STOP_CHARGING,
[ActionScope.INSTANT, ActionScope.NODE],
[BlockingType.HARD],
"Kết thúc quá trình sạc pin.",
"Robot đã kết thúc sạc pin.")]
public class RobotStopChargingAction(IServiceProvider ServiceProvider) : RobotAction(ServiceProvider)
{
protected override Task StartAction()
{
SetStatus(ActionEvent.FINISHED);
ResultDescription = string.IsNullOrEmpty(ActionAttribute.ResultDescription) ? ResultDescription : ActionAttribute.ResultDescription;
return base.StartAction();
}
protected override Task ExecuteAction()
{
return base.ExecuteAction();
}
}

View File

@@ -0,0 +1,26 @@
using RobotNet.VDA5050.Type;
using RobotNet10.RobotApp.Interfaces;
namespace RobotNet10.RobotApp.Services.Robot.Actions;
[RobotAction(ActionType.STOP_PAUSE,
[ActionScope.INSTANT],
[BlockingType.NONE, BlockingType.SOFT, BlockingType.HARD],
"Tiếp tục hoạt động robot sau khi tạm dừng.",
"Robot đã tiếp tục hoạt động.")]
public class RobotStopPauseAction(IServiceProvider ServiceProvider) : RobotAction(ServiceProvider)
{
protected override Task StartAction()
{
var RobotController = ServiceProvider.GetRequiredService<IRobotController>();
RobotController.Resume();
SetStatus(ActionEvent.FINISHED);
ResultDescription = string.IsNullOrEmpty(ActionAttribute.ResultDescription) ? ResultDescription : ActionAttribute.ResultDescription;
return base.StartAction();
}
protected override Task ExecuteAction()
{
return base.ExecuteAction();
}
}