This commit is contained in:
Đăng Nguyễn 2025-10-13 13:17:32 +07:00
parent 511614df72
commit b2df5b22b7
66 changed files with 1957 additions and 376 deletions

View File

@ -5,7 +5,7 @@
display: flex; display: flex;
flex-direction: column; flex-direction: column;
position: relative; position: relative;
background-color: var(--mud-palette-surface); background-color: #808080;
border-radius: var(--mud-default-borderradius); border-radius: var(--mud-default-borderradius);
transition: box-shadow 300ms cubic-bezier(0.4, 0, 0.2, 1) 0ms; transition: box-shadow 300ms cubic-bezier(0.4, 0, 0.2, 1) 0ms;
box-shadow: var(--mud-elevation-10); box-shadow: var(--mud-elevation-10);

View File

@ -2,22 +2,29 @@
public enum RootStateType public enum RootStateType
{ {
Booting, System,
Operational, Auto,
Manual,
Service,
Stop,
Fault,
} }
public enum OperationalStateType public enum SystemStateType
{ {
Initializing,
Standby,
Shutting_Down,
} }
public enum AutomationStateType public enum AutoStateType
{ {
Idle, Idle,
Executing, Executing,
Paused, Paused,
Charging, Holding,
Error, Canceling,
Recovering,
Remote_Override, Remote_Override,
} }
@ -27,13 +34,60 @@ public enum ManualStateType
Active, Active,
} }
public enum SafetyStateType public enum ServiceStateType
{ {
Init, Idle,
Run_Ok, Active,
SS1, }
STO,
PDS, public enum StopStateType
SLS, {
Error, EMC,
Protective,
Manual,
}
public enum FaultStateType
{
Navigation,
Localization,
Shielf,
Battery,
Driver,
Peripherals,
Safety,
Communication,
}
public enum ExecutingStateType
{
Planning,
Moving,
ACT,
}
public enum ACTStateType
{
Docking,
Docked,
Charging,
Undocking,
Loading,
Unloading,
TechAction,
}
public enum MoveStateType
{
Navigation,
Avoidance,
Approach,
Tracking,
Repositioning,
}
public enum PlanStateType
{
Task,
Path,
} }

View File

@ -4,6 +4,14 @@ namespace RobotApp.Common.Shares;
public static class MathExtensions public static class MathExtensions
{ {
public static double NormalizeAngle(double angle)
{
angle = angle % 360;
if (angle > 180) angle -= 360;
else if (angle < -180) angle += 360;
return angle;
}
public static (double x, double y) CurveDegreeTwo(double t, double x1, double y1, double controlPointX, double controlPointY, double x2, double y2) public static (double x, double y) CurveDegreeTwo(double t, double x1, double y1, double controlPointX, double controlPointY, double x2, double y2)
{ {
var x = (1 - t) * (1 - t) * x1 + 2 * t * (1 - t) * controlPointX + t * t * x2; var x = (1 - t) * (1 - t) * x1 + 2 * t * (1 - t) * controlPointX + t * t * x2;
@ -77,4 +85,21 @@ public static class MathExtensions
} }
return distance; return distance;
} }
public static double GetVectorAngle(double originNodeX, double originNodeY, double vector1X, double vector1Y, double vector2X, double vector2Y)
{
double BA_x = vector1X - originNodeX;
double BA_y = vector1Y - originNodeY;
double BC_x = vector2X - originNodeX;
double BC_y = vector2Y - originNodeY;
// Tính độ dài của các vector AB và BC
double lengthAB = Math.Sqrt(BA_x * BA_x + BA_y * BA_y);
double lengthBC = Math.Sqrt(BC_x * BC_x + BC_y * BC_y);
// Tính tích vô hướng của AB và BC
double dotProduct = BA_x * BC_x + BA_y * BC_y;
if (lengthAB * lengthBC == 0) return 0;
if (dotProduct / (lengthAB * lengthBC) > 1) return 0;
if (dotProduct / (lengthAB * lengthBC) < -1) return 180;
return Math.Acos(dotProduct / (lengthAB * lengthBC)) * (180.0 / Math.PI);
}
} }

View File

@ -1,5 +1,31 @@
namespace RobotApp.Interfaces; namespace RobotApp.Interfaces;
public enum BatteryStatus
{
Unknown,
Charging,
Discharging,
Full,
NotCharging,
Fault,
Standby
}
public interface IBattery public interface IBattery
{ {
bool IsReady { get; }
double Voltage { get; }
double Current { get; }
double SOC { get; }
double SOH { get; }
BatteryStatus Status { get; }
int ChargeTime { get; }
int DischargeTime { get; }
double Temperature { get; }
double RemainingCapacity { get; }
double RemainingEnergy { get; }
int DischargeCycles { get; }
int ChargeCycles { get; }
bool IsCharging { get; }
string[] ErrorStatus { get; }
} }

View File

@ -6,4 +6,6 @@ public interface IError
{ {
bool HasFatalError { get; } bool HasFatalError { get; }
void AddError(Error error, TimeSpan? clearAfter = null); void AddError(Error error, TimeSpan? clearAfter = null);
void DeleteError(string errorType);
void ClearAllErrors();
} }

View File

@ -2,4 +2,5 @@
public interface IInfomation public interface IInfomation
{ {
} }

View File

@ -3,7 +3,7 @@ using Action = RobotApp.VDA5050.InstantAction.Action;
namespace RobotApp.Interfaces; namespace RobotApp.Interfaces;
public interface IInstanceActions public interface IInstantActions
{ {
ActionState[] ActionStates { get; } ActionState[] ActionStates { get; }
bool HasActionRunning { get; } bool HasActionRunning { get; }

View File

@ -1,7 +1,14 @@
namespace RobotApp.Interfaces; using RobotApp.Common.Shares;
namespace RobotApp.Interfaces;
public interface ILocalization public interface ILocalization
{ {
/// <summary>
/// Trạng thái sẵn sàng của hệ thống định vị.
/// </summary>
bool IsReady { get; }
/// <summary> /// <summary>
/// Vị trí hiện tại của robot trong hệ tọa độ toàn cục theo trục X (đơn vị: mét). /// Vị trí hiện tại của robot trong hệ tọa độ toàn cục theo trục X (đơn vị: mét).
/// </summary> /// </summary>
@ -13,10 +20,36 @@ public interface ILocalization
double Y { get; } double Y { get; }
/// <summary> /// <summary>
/// Hướng hiện tại của robot trong hệ tọa độ toàn cục (đơn vị: độ). /// Hướng hiện tại của robot trong hệ tọa độ toàn cục (đơn vị: radian).
/// </summary> /// </summary>
double Theta { get; } double Theta { get; }
/// <summary>
/// Trang thái SLAM hiện tại của robot.
/// </summary>
string SlamState { get; }
/// <summary>
/// Thông tin chi tiết về trạng thái SLAM hiện tại của robot.
/// </summary>
string SlamStateDetail { get; }
/// <summary>
/// Bản đồ đang được sử dụng để định vị hiện tại.
/// </summary>
string CurrentActiveMap { get; }
/// <summary>
/// Độ tin cậy của vị trí định vị hiện tại (0.0 - 100.0).
/// </summary>
double Reliability { get; }
/// <summary>
/// Độ phù hợp của vị trí định vị hiện tại so với bản đồ (0.0 - 100.0).
/// </summary>
double MatchingScore { get; }
/// <summary> /// <summary>
/// Khởi tạo vị trí của robot trong hệ tọa độ toàn cục. /// Khởi tạo vị trí của robot trong hệ tọa độ toàn cục.
/// </summary> /// </summary>
@ -24,5 +57,57 @@ public interface ILocalization
/// <param name="y">đơn vị: mét</param> /// <param name="y">đơn vị: mét</param>
/// <param name="theta">đơn vị: độ</param> /// <param name="theta">đơn vị: độ</param>
/// <returns></returns> /// <returns></returns>
bool InitializePosition(double x, double y, double theta); MessageResult SetInitializePosition(double x, double y, double theta);
/// <summary>
/// Bắt đầu quá trình lập bản đồ với độ phân giải chỉ định.
/// </summary>
/// <param name="resolution">đơn vị mét/px</param>
/// <returns></returns>
MessageResult StartMapping(double resolution);
/// <summary>
/// Dừng quá trình lập bản đồ hiện tại và lưu bản đồ với tên chỉ định.
/// </summary>
/// <param name="mapName"></param>
/// <returns></returns>
MessageResult StopMapping(string mapName);
/// <summary>
/// Bắt đầu quá trình định vị sử dụng bản đồ đã lưu.
/// </summary>
/// <returns></returns>
MessageResult StartLocalization();
/// <summary>
/// Dừng quá trình định vị hiện tại.
/// </summary>
/// <returns></returns>
MessageResult StopLocalization();
/// <summary>
/// Kích hoạt bản đồ đã lưu để sử dụng trong quá trình định vị.
/// </summary>
/// <param name="mapName"></param>
/// <returns></returns>
MessageResult ActivateMap(string mapName);
/// <summary>
/// Chuyển sang bản đồ đã lưu và khởi tạo vị trí của robot trên bản đồ đó.
/// </summary>
/// <param name="mapName"></param>
/// <param name="x"></param>
/// <param name="y"></param>
/// <param name="theta"></param>
/// <returns></returns>
MessageResult SwitchMap(string mapName, double x, double y, double theta);
/// <summary>
/// Thay đổi gốc tọa độ của bản đồ hiện tại.
/// </summary>
/// <param name="x"></param>
/// <param name="y"></param>
/// <param name="theta"></param>
/// <returns></returns>
MessageResult ChangeMapOrigin(double x, double y, double theta);
} }

View File

@ -5,11 +5,12 @@ namespace RobotApp.Interfaces;
public enum NavigationState public enum NavigationState
{ {
None, None,
Initializing,
Initialized,
Idle, Idle,
Initializing,
Waiting,
Moving, Moving,
Rotating, Rotating,
Canceled,
Paused, Paused,
Error Error
} }
@ -25,13 +26,19 @@ public enum NavigationProccess
public interface INavigation public interface INavigation
{ {
bool IsReady { get; }
bool Driving { get; } bool Driving { get; }
double VelocityX { get; }
double VelocityY { get; }
double Omega { get; }
NavigationState State { get; } NavigationState State { get; }
void Move(Node[] nodes, Edge[] edges); void Move(Node[] nodes, Edge[] edges);
void MoveStraight(double x, double y); void MoveStraight(double x, double y);
void Rotate(double angle); void Rotate(double angle);
void Paused(); void Paused();
void Resume(); void Resume();
void UpdateOrder(int lastBaseSequence); void UpdateOrder(string lastBaseNodeId);
void RefreshOrder(Node[] nodes, Edge[] edges);
void Refresh();
void CancelMovement(); void CancelMovement();
} }

View File

@ -1,14 +1,35 @@
namespace RobotApp.Interfaces; namespace RobotApp.Interfaces;
public enum OperatingMode public enum PeripheralMode
{ {
AUTOMATIC, AUTOMATIC,
MANUAL, MANUAL,
SEMIAUTOMATIC,
TEACHIN,
SERVICE, SERVICE,
} }
public interface IPeripheral public interface IPeripheral
{ {
OperatingMode OperatingMode { get; } bool IsReady { get; }
PeripheralMode PeripheralMode { get; }
bool Emergency { get; }
bool Bumper { get; }
bool LiftedUp { get; }
bool LiftedDown { get; }
bool LiftHome { get; }
bool LeftMotorReady { get; }
bool RightMotorReady { get; }
bool LiftMotorReady { get; }
bool SwitchLock { get; }
bool SwitchManual { get; }
bool SwitchAuto { get; }
bool ButtonStart { get; }
bool ButtonStop { get; }
bool ButtonReset { get; }
bool HasLoad { get; }
bool MutedBase { get; }
bool MutedLoad { get; }
bool StartedCharging { get; }
bool SetSytemState();
bool SetProccessState();
bool SetOnCharging();
} }

View File

@ -1,6 +1,29 @@
namespace RobotApp.Interfaces namespace RobotApp.Interfaces;
public enum RFHandlerMode
{ {
public class IRFHandler Default,
{ Maintenance,
} Override,
Unknown
}
public interface IRFHandler
{
bool IsConnected { get; }
bool IsEnabled { get; }
bool IsLocked { get; }
bool EStop { get; }
bool LiftUp { get; }
bool LiftDown { get; }
int Speed { get; }
bool Forward { get; }
bool Backward { get; }
bool RotateLeft { get; }
bool RotateRight { get; }
bool Left { get; }
bool Right { get; }
RFHandlerMode Mode { get; }
event Action<RFHandlerMode> OnStateChanged;
event Action<bool>? OnConnectionChanged;
} }

View File

@ -2,4 +2,17 @@
public interface ISafety public interface ISafety
{ {
bool SpeedVerySlow { get; }
bool SpeedSlow { get; }
bool SpeedNormal { get; }
bool SpeedMedium { get; }
bool SpeedOptimal { get; }
bool SpeedFast { get; }
bool SpeedVeryFast { get; }
bool LidarFrontProtectField { get; }
bool LidarBackProtectField { get; }
bool LidarFrontTimProtectField { get; }
bool SetMutedLoad(bool muted);
bool SetMutedBase(bool muted);
bool SetHorizontalLoad();
} }

View File

@ -5,6 +5,11 @@
/// </summary> /// </summary>
public interface ISensorIMU public interface ISensorIMU
{ {
/// <summary>
/// Trạng thái sẵn sàng của cảm biến IMU
/// </summary>
bool IsReady { get; }
/// <summary> /// <summary>
/// Góc xoay của robot (đơn vị độ) /// Góc xoay của robot (đơn vị độ)
/// </summary> /// </summary>

View File

@ -5,7 +5,9 @@ using MudBlazor.Services;
using RobotApp.Components; using RobotApp.Components;
using RobotApp.Components.Account; using RobotApp.Components.Account;
using RobotApp.Data; using RobotApp.Data;
using RobotApp.Services;
using RobotApp.Services.Robot; using RobotApp.Services.Robot;
using RobotApp.Services.Robot.Simulation;
var builder = WebApplication.CreateBuilder(args); var builder = WebApplication.CreateBuilder(args);
@ -46,7 +48,9 @@ builder.Services.AddSingleton<IEmailSender<ApplicationUser>, IdentityNoOpEmailSe
builder.Services.AddSingleton(typeof(RobotApp.Services.Logger<>)); builder.Services.AddSingleton(typeof(RobotApp.Services.Logger<>));
builder.Services.AddSingleton<RobotConnection>(); builder.Services.AddSingleton<RobotConnection>();
builder.Services.AddHostedService(sp => sp.GetRequiredService<RobotConnection>());
builder.Services.AddRobotSimulation();
builder.Services.AddRobot();
var app = builder.Build(); var app = builder.Build();
await app.Services.SeedApplicationDbAsync(); await app.Services.SeedApplicationDbAsync();

View File

@ -29,8 +29,4 @@
<PackageReference Include="MQTTnet" Version="5.0.1.1416" /> <PackageReference Include="MQTTnet" Version="5.0.1.1416" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<Folder Include="Services\Robot\Simulation\" />
</ItemGroup>
</Project> </Project>

View File

@ -1,5 +1,15 @@
namespace RobotApp.Services.Exceptions; using RobotApp.VDA5050.State;
public class ActionException : OrderException namespace RobotApp.Services.Exceptions;
public class ActionException : Exception
{ {
public ActionException(string message) : base(message) { }
public ActionException(string message, Exception inner) : base(message, inner) { }
public ActionException() : base() { }
public ActionException(Error error) : base()
{
Error = error;
}
public Error? Error { get; set; }
} }

View File

@ -1,16 +1,15 @@
namespace RobotApp.Services.Exceptions; using RobotApp.VDA5050.State;
namespace RobotApp.Services.Exceptions;
public class ModbusException : Exception public class ModbusException : Exception
{ {
public ModbusException() public ModbusException(string message) : base(message) { }
{ public ModbusException(string message, Exception inner) : base(message, inner) { }
} public ModbusException() : base() { }
public ModbusException(string message) public ModbusException(Error error) : base()
: base(message)
{
}
public ModbusException(string message, Exception inner)
: base(message, inner)
{ {
Error = error;
} }
public Error? Error { get; set; }
} }

View File

@ -0,0 +1,15 @@
using RobotApp.VDA5050.State;
namespace RobotApp.Services.Exceptions;
public class PathPlannerException : Exception
{
public PathPlannerException(string message) : base(message) { }
public PathPlannerException(string message, Exception inner) : base(message, inner) { }
public PathPlannerException() : base() { }
public PathPlannerException(Error error) : base()
{
Error = error;
}
public Error? Error { get; set; }
}

View File

@ -0,0 +1,15 @@
using RobotApp.VDA5050.State;
namespace RobotApp.Services.Exceptions;
public class SimulationException : Exception
{
public SimulationException(string message) : base(message) { }
public SimulationException(string message, Exception inner) : base(message, inner) { }
public SimulationException() : base() { }
public SimulationException(Error error) : base()
{
Error = error;
}
public Error? Error { get; set; }
}

View File

@ -13,11 +13,12 @@ public class ModbusTcpClient(string IpAddress, int Port, byte ClientId) : IDispo
private NetworkStream? stream; private NetworkStream? stream;
private bool disposed = false; private bool disposed = false;
private readonly Lock LockObject = new();
private uint transactionIdentifierInternal = 0; private uint transactionIdentifierInternal = 0;
private const int connectTimeout = 3000; private const int connectTimeout = 3000;
private const int readTimeout = 500; private const int readTimeout = 500;
private const int writeTimeout = 500; private const int writeTimeout = 500;
private int numberOfRetries { get; set; } = 3; private const int numberOfRetries = 3;
~ModbusTcpClient() ~ModbusTcpClient()
{ {
@ -100,10 +101,29 @@ public class ModbusTcpClient(string IpAddress, int Port, byte ClientId) : IDispo
disposed = true; disposed = true;
} }
} }
private uint GetNextTransactionId()
{
lock (LockObject)
{
return ++transactionIdentifierInternal;
}
}
private static bool ShouldRetry(Exception ex)
{
return ex is ConnectionAbortedException ||
ex is TimeoutException ||
ex is SocketException ||
ex is InvalidOperationException ||
(ex is IOException && ex.InnerException is SocketException);
}
private byte[] Write(byte functionCode, ushort startingAddress, ushort quantity, CancellationToken? cancellationToken = null) private byte[] Write(byte functionCode, ushort startingAddress, ushort quantity, CancellationToken? cancellationToken = null)
=> Write(functionCode, startingAddress, quantity, [], cancellationToken); => Write(functionCode, startingAddress, quantity, [], cancellationToken);
private byte[] Write(byte functionCode, ushort startingAddress, ushort quantity, byte[] multipleData, CancellationToken? cancellationToken = null) private byte[] Write(byte functionCode, ushort startingAddress, ushort quantity, byte[] multipleData, CancellationToken? cancellationToken = null)
=> Write(functionCode, startingAddress, quantity, multipleData, 0, cancellationToken);
private byte[] Write(byte functionCode, ushort startingAddress, ushort quantity, byte[] multipleData, int retryAttempt, CancellationToken? cancellationToken = null)
{ {
try try
{ {
@ -112,11 +132,11 @@ public class ModbusTcpClient(string IpAddress, int Port, byte ClientId) : IDispo
if (!Connect() || !IsConnected || stream is null) throw new ConnectionAbortedException(); if (!Connect() || !IsConnected || stream is null) throw new ConnectionAbortedException();
} }
transactionIdentifierInternal++; var transactionId = GetNextTransactionId();
byte[] writeData = new byte[12 + multipleData.Length]; byte[] writeData = new byte[12 + multipleData.Length];
var dataLength = multipleData.Length + 6; var dataLength = multipleData.Length + 6;
writeData[0] = (byte)(transactionIdentifierInternal >> 8); writeData[0] = (byte)(transactionId >> 8);
writeData[1] = (byte)(transactionIdentifierInternal & 0xFF); writeData[1] = (byte)(transactionId & 0xFF);
writeData[2] = 0x00; writeData[2] = 0x00;
writeData[3] = 0x00; writeData[3] = 0x00;
writeData[4] = (byte)(dataLength >> 8); writeData[4] = (byte)(dataLength >> 8);
@ -171,6 +191,11 @@ public class ModbusTcpClient(string IpAddress, int Port, byte ClientId) : IDispo
Array.Copy(readData, 9, receiveData, 0, receiveData.Length); Array.Copy(readData, 9, receiveData, 0, receiveData.Length);
return receiveData; return receiveData;
} }
catch (Exception ex) when (ShouldRetry(ex) && retryAttempt >= numberOfRetries)
{
Dispose(true);
return Write(functionCode, startingAddress, quantity, multipleData, retryAttempt + 1, cancellationToken);
}
catch (Exception ex) when (!(ex is ModbusException || ex is TimeoutException)) catch (Exception ex) when (!(ex is ModbusException || ex is TimeoutException))
{ {
throw new ModbusException("Communication error", ex); throw new ModbusException("Communication error", ex);

View File

@ -1,156 +0,0 @@
//namespace RobotApp.Services.Navigation.Algorithm;
//public class PurePursuit
//{
// private double MaxAngularVelocity = 1.5;
// private double LookaheadDistance = 0.5;
// private List<NavigationNode> Waypoints_Value = [];
// public int OnNodeIndex = 0;
// public int GoalIndex = 0;
// public NavigationNode? Goal;
// public PurePursuit WithLookheadDistance(double distance)
// {
// LookaheadDistance = distance;
// return this;
// }
// public PurePursuit WithMaxAngularVelocity(double vel)
// {
// MaxAngularVelocity = vel;
// return this;
// }
// public PurePursuit WithPath(NavigationNode[] path)
// {
// Waypoints_Value = [.. path];
// return this;
// }
// public void UpdatePath(NavigationNode[] path)
// {
// Waypoints_Value = [.. path];
// }
// public void UpdateGoal(NavigationNode goal)
// {
// Goal = goal;
// GoalIndex = Waypoints_Value.IndexOf(Goal);
// }
// public (NavigationNode node, int index) GetOnNode(double x, double y)
// {
// double minDistance = double.MaxValue;
// NavigationNode onNode = Waypoints_Value[0];
// int index = 0;
// for (int i = 1; i < Waypoints_Value.Count; i++)
// {
// var distance = Math.Sqrt(Math.Pow(x - Waypoints_Value[i].X, 2) + Math.Pow(y - Waypoints_Value[i].Y, 2));
// if (distance < minDistance)
// {
// onNode = Waypoints_Value[i];
// minDistance = distance;
// index = i;
// }
// }
// return (onNode, index);
// }
// private (NavigationNode? node, int index) OnNode(double x, double y)
// {
// if (Waypoints_Value is null || Waypoints_Value.Count == 0) return (null, 0);
// double minDistance = double.MaxValue;
// int index = 0;
// NavigationNode? onNode = null;
// if (Goal is null) return (onNode, index);
// for (int i = OnNodeIndex; i < Waypoints_Value.IndexOf(Goal); i++)
// {
// var distance = Math.Sqrt(Math.Pow(x - Waypoints_Value[i].X, 2) + Math.Pow(y - Waypoints_Value[i].Y, 2));
// if (distance < minDistance)
// {
// onNode = Waypoints_Value[i];
// minDistance = distance;
// index = i;
// }
// }
// return (onNode, index);
// }
// public double PurePursuit_step(double X_Ref, double Y_Ref, double Angle_Ref)
// {
// if (Waypoints_Value is null || Waypoints_Value.Count < 2) return 0;
// NavigationNode? lookaheadStartPt = null;
// var (onNode, index) = OnNode(X_Ref, Y_Ref);
// if (onNode is null || Goal is null) return 0;
// OnNodeIndex = index;
// double lookDistance = 0;
// for (int i = OnNodeIndex + 1; i < Waypoints_Value.IndexOf(Goal); i++)
// {
// lookDistance += Math.Sqrt(Math.Pow(Waypoints_Value[i - 1].X - Waypoints_Value[i].X, 2) + Math.Pow(Waypoints_Value[i - 1].Y - Waypoints_Value[i].Y, 2));
// if (lookDistance >= LookaheadDistance || Waypoints_Value[i].Direction != onNode.Direction)
// {
// lookaheadStartPt = Waypoints_Value[i];
// break;
// }
// }
// lookaheadStartPt ??= Goal;
// if (onNode.Direction == RobotDirection.BACKWARD)
// {
// if (Angle_Ref > Math.PI) Angle_Ref -= Math.PI * 2;
// else if (Angle_Ref < -Math.PI) Angle_Ref += Math.PI * 2;
// Angle_Ref += Math.PI;
// if (Angle_Ref > Math.PI) Angle_Ref -= Math.PI * 2;
// }
// var distance = Math.Atan2(lookaheadStartPt.Y - Y_Ref, lookaheadStartPt.X - X_Ref) - Angle_Ref;
// if (Math.Abs(distance) > Math.PI)
// {
// double minDistance;
// if (distance + Math.PI == 0.0) minDistance = 0.0;
// else
// {
// double data = (distance + Math.PI) / (2 * Math.PI);
// if (data < 0) data = Math.Round(data + 0.5);
// else data = Math.Round(data - 0.5);
// minDistance = distance + Math.PI - data * (2 * Math.PI);
// double checker = 0;
// if (minDistance != 0.0)
// {
// checker = Math.Abs((distance + Math.PI) / (2 * Math.PI));
// }
// if (!(Math.Abs(checker - Math.Floor(checker + 0.5)) > 2.2204460492503131E-16 * checker))
// {
// minDistance = 0.0;
// }
// else if (distance + Math.PI < 0.0)
// {
// minDistance += Math.PI * 2;
// }
// }
// if (minDistance == 0.0 && distance + Math.PI > 0.0)
// {
// minDistance = Math.PI * 2;
// }
// distance = minDistance - Math.PI;
// }
// var AngularVelocity = 2.0 * 0.5 * Math.Sin(distance) / LookaheadDistance;
// if (Math.Abs(AngularVelocity) > MaxAngularVelocity)
// {
// if (AngularVelocity < 0.0)
// {
// AngularVelocity = -1.0;
// }
// else if (AngularVelocity > 0.0)
// {
// AngularVelocity = 1.0;
// }
// else if (AngularVelocity == 0.0)
// {
// AngularVelocity = 0.0;
// }
// AngularVelocity *= MaxAngularVelocity;
// }
// return AngularVelocity;
// }
//}

View File

@ -1,22 +0,0 @@
using RobotApp.Interfaces;
namespace RobotApp.Services.Robot.Navigation;
public class DifferentialNavigation(Logger<NavigationController> navLogger,
Logger<DifferentialNavigation> Logger,
IDriver Driver,
ISafety Safety,
ISensorIMU SensorIMU) : NavigationController(navLogger)
{
protected override void NavigationHandler()
{
try
{
// Implement differential drive navigation logic here
}
catch (Exception ex)
{
Logger.Write($"Error in DifferentialNavigation: {ex.Message}", LogLevel.Error);
}
}
}

View File

@ -1,61 +0,0 @@
using RobotApp.Interfaces;
using RobotApp.VDA5050.Order;
namespace RobotApp.Services.Robot.Navigation;
public class NavigationController(Logger<NavigationController> Logger) : INavigation
{
public NavigationState State { get; private set; } = NavigationState.None;
public bool Driving { get; private set; }
protected const int CycleHandlerMilliseconds = 20;
private WatchTimer<NavigationController>? NavigationTimer;
protected Node[] Nodes = [];
protected Edge[] Edges = [];
protected void HandleNavigationStart()
{
NavigationTimer = new(CycleHandlerMilliseconds, NavigationHandler, Logger);
NavigationTimer.Start();
}
protected void HandleNavigationStop()
{
NavigationTimer?.Dispose();
}
protected virtual void NavigationHandler() { }
public void CancelMovement()
{
}
public void Move(Node[] nodes, Edge[] edges)
{
Nodes = nodes;
Edges = edges;
State = NavigationState.Initializing;
}
public void MoveStraight(double x, double y)
{
}
public void Paused()
{
}
public void Resume()
{
}
public void Rotate(double angle)
{
}
public void UpdateOrder(int lastBaseSequence)
{
State = NavigationState.Initialized;
}
}

View File

@ -1,6 +0,0 @@
namespace RobotApp.Services.Robot.Navigation
{
public class NavigationManager
{
}
}

View File

@ -3,7 +3,7 @@ using RobotApp.VDA5050.State;
namespace RobotApp.Services.Robot; namespace RobotApp.Services.Robot;
public class RobotAction : IInstanceActions public class RobotAction : IInstantActions
{ {
public ActionState[] ActionStates { get; private set; } = []; public ActionState[] ActionStates { get; private set; } = [];

View File

@ -1,6 +1,36 @@
namespace RobotApp.Services.Robot using RobotApp.Interfaces;
namespace RobotApp.Services.Robot;
public class RobotBattery : IBattery
{ {
public class RobotBattery public bool IsReady { get; private set; } = true;
{
} public double Voltage => throw new NotImplementedException();
public double Current => throw new NotImplementedException();
public double SOC => throw new NotImplementedException();
public double SOH => throw new NotImplementedException();
public BatteryStatus Status => throw new NotImplementedException();
public int ChargeTime => throw new NotImplementedException();
public int DischargeTime => throw new NotImplementedException();
public double Temperature => throw new NotImplementedException();
public double RemainingCapacity => throw new NotImplementedException();
public double RemainingEnergy => throw new NotImplementedException();
public int DischargeCycles => throw new NotImplementedException();
public int ChargeCycles => throw new NotImplementedException();
public bool IsCharging => throw new NotImplementedException();
public string[] ErrorStatus => throw new NotImplementedException();
} }

View File

@ -6,4 +6,17 @@ public class RobotConfiguration
{ {
public static string SerialNumber { get; set; } = "Robot-Demo"; public static string SerialNumber { get; set; } = "Robot-Demo";
public static NavigationType NavigationType { get; set; } = NavigationType.Differential; public static NavigationType NavigationType { get; set; } = NavigationType.Differential;
public static VDA5050.VDA5050Setting VDA5050Setting { get; set; } = new()
{
HostServer = "172.20.235.170",
Port = 1885,
UserName = "robotics",
Password = "robotics",
Manufacturer = "PhenikaaX",
Version = "0.0.1",
PublishRepeat = 2,
};
public static string PLCAddress { get; set; } = "127.0.0.1";
public static int PLCPort { get; set; } = 502;
public static int PLCUnitId { get; set; } = 1;
} }

View File

@ -6,17 +6,13 @@ using System.Text.Json;
namespace RobotApp.Services.Robot; namespace RobotApp.Services.Robot;
public class RobotConnection(Logger<RobotConnection> Logger, Logger<MQTTClient> MQTTClientLogger) : BackgroundService public class RobotConnection(Logger<RobotConnection> Logger, Logger<MQTTClient> MQTTClientLogger)
{ {
private readonly VDA5050Setting VDA5050Setting = new() private readonly VDA5050Setting VDA5050Setting = RobotConfiguration.VDA5050Setting;
{
HostServer = "172.20.235.170",
Port = 1886,
};
private MQTTClient? MqttClient; private MQTTClient? MqttClient;
public bool IsConnected => MqttClient is not null && MqttClient.IsConnected; public bool IsConnected => MqttClient is not null && MqttClient.IsConnected;
public readonly static string SerialNumber = "RobotDemo"; public readonly static string SerialNumber = RobotConfiguration.SerialNumber;
private void OrderChanged(string data) private void OrderChanged(string data)
{ {
@ -50,20 +46,17 @@ public class RobotConnection(Logger<RobotConnection> Logger, Logger<MQTTClient>
return new(false, "Chưa có kết nối tới broker"); return new(false, "Chưa có kết nối tới broker");
} }
protected override async Task ExecuteAsync(CancellationToken stoppingToken) public async Task StartConnection(CancellationToken cancellationToken)
{ {
await Task.Yield();
MqttClient = new MQTTClient(SerialNumber, VDA5050Setting, MQTTClientLogger); MqttClient = new MQTTClient(SerialNumber, VDA5050Setting, MQTTClientLogger);
MqttClient.OrderChanged += OrderChanged; MqttClient.OrderChanged += OrderChanged;
MqttClient.InstanceActionsChanged += InstanceActionsChanged; MqttClient.InstanceActionsChanged += InstanceActionsChanged;
await MqttClient.ConnectAsync(stoppingToken); await MqttClient.ConnectAsync(cancellationToken);
await MqttClient.SubscribeAsync(stoppingToken); await MqttClient.SubscribeAsync(cancellationToken);
} }
public override async Task StopAsync(CancellationToken cancellationToken) public async Task StopConnection()
{ {
if(MqttClient is not null) await MqttClient.DisposeAsync(); if (MqttClient is not null) await MqttClient.DisposeAsync();
await base.StopAsync(cancellationToken);
} }
} }

View File

@ -1,13 +1,14 @@
using RobotApp.Interfaces; using RobotApp.Interfaces;
using RobotApp.Services.Exceptions; using RobotApp.Services.Exceptions;
using RobotApp.Services.State;
using RobotApp.VDA5050.InstantAction;
using RobotApp.VDA5050.Order; using RobotApp.VDA5050.Order;
using RobotApp.VDA5050.State;
namespace RobotApp.Services.Robot; namespace RobotApp.Services.Robot;
public class RobotController(IOrder OrderManager, public class RobotController(IOrder OrderManager,
INavigation NavigationManager, INavigation NavigationManager,
IInstanceActions ActionManager, IInstantActions ActionManager,
IBattery BatteryManager, IBattery BatteryManager,
ILocalization Localization, ILocalization Localization,
IPeripheral PeripheralManager, IPeripheral PeripheralManager,
@ -15,7 +16,8 @@ public class RobotController(IOrder OrderManager,
IError ErrorManager, IError ErrorManager,
IInfomation InfomationManager, IInfomation InfomationManager,
Logger<RobotController> Logger, Logger<RobotController> Logger,
RobotConnection ConnectionManager) : BackgroundService RobotConnection ConnectionManager,
RobotStateMachine StateManager) : BackgroundService
{ {
private readonly Mutex NewOrderMutex = new(); private readonly Mutex NewOrderMutex = new();
private readonly Mutex NewInstanceMutex = new(); private readonly Mutex NewInstanceMutex = new();
@ -25,11 +27,25 @@ public class RobotController(IOrder OrderManager,
protected override Task ExecuteAsync(CancellationToken stoppingToken) protected override Task ExecuteAsync(CancellationToken stoppingToken)
{ {
InitializationingHandler();
UpdateStateTimer = new(UpdateStateInterval, UpdateStateHandler, Logger); UpdateStateTimer = new(UpdateStateInterval, UpdateStateHandler, Logger);
UpdateStateTimer.Start(); UpdateStateTimer.Start();
return Task.CompletedTask; return Task.CompletedTask;
} }
private void InitializationingHandler()
{
try
{
StateManager.InitializeHierarchyStates();
}
catch
{
}
}
private void UpdateStateHandler() private void UpdateStateHandler()
{ {
// xử lý cập nhật trạng thái robot và gửi thông tin qua kết nối // xử lý cập nhật trạng thái robot và gửi thông tin qua kết nối
@ -43,13 +59,13 @@ public class RobotController(IOrder OrderManager,
{ {
if (!string.IsNullOrEmpty(OrderManager.OrderId)) if (!string.IsNullOrEmpty(OrderManager.OrderId))
{ {
if (order.OrderId != OrderManager.OrderId) throw new OrderException(RobotErrors.CreateError(ErrorType.INITIALIZE_ORDER, "1001", ErrorLevel.WARNING, $"Có order đang được thực hiện. OrderId: {OrderManager.OrderId}, OrderId mới: {order.OrderId}")); if (order.OrderId != OrderManager.OrderId) throw new OrderException(RobotErrors.Error1001(OrderManager.OrderId, order.OrderId));
OrderManager.UpdateOrder(order.OrderUpdateId, order.Nodes, order.Edges); OrderManager.UpdateOrder(order.OrderUpdateId, order.Nodes, order.Edges);
} }
else if (PeripheralManager.OperatingMode != Interfaces.OperatingMode.AUTOMATIC) throw new OrderException(RobotErrors.CreateError(ErrorType.INITIALIZE_ORDER, "1006", ErrorLevel.WARNING, $"Không thể khởi tạo order mới khi chế độ vận hành không phải là TỰ ĐỘNG. Chế độ hiện tại: {PeripheralManager.OperatingMode}")); else if (PeripheralManager.PeripheralMode != PeripheralMode.AUTOMATIC) throw new OrderException(RobotErrors.Error1006(PeripheralManager.PeripheralMode));
else if(ActionManager.HasActionRunning) throw new OrderException(RobotErrors.CreateError(ErrorType.INITIALIZE_ORDER, "1007", ErrorLevel.WARNING, $"Không thể khởi tạo order mới khi có action đang thực hiện. Vui lòng chờ hoàn thành các action hiện tại.")); else if (ActionManager.HasActionRunning) throw new OrderException(RobotErrors.Error1007());
else if(ErrorManager.HasFatalError) throw new OrderException(RobotErrors.CreateError(ErrorType.INITIALIZE_ORDER, "1008", ErrorLevel.WARNING, $"Không thể khởi tạo order mới khi có lỗi nghiêm trọng. Vui lòng kiểm tra và xử lý lỗi.")); else if (ErrorManager.HasFatalError) throw new OrderException(RobotErrors.Error1008());
else if(NavigationManager.Driving) throw new OrderException(RobotErrors.CreateError(ErrorType.INITIALIZE_ORDER, "1009", ErrorLevel.WARNING, $"Không thể khởi tạo order mới khi robot đang di chuyển. Vui lòng dừng robot.")); else if (NavigationManager.Driving) throw new OrderException(RobotErrors.Error1009());
else OrderManager.StartOrder(order.OrderId, order.Nodes, order.Edges); else OrderManager.StartOrder(order.OrderId, order.Nodes, order.Edges);
} }
catch (OrderException orEx) catch (OrderException orEx)
@ -71,7 +87,7 @@ public class RobotController(IOrder OrderManager,
} }
} }
public void NewInstanceActionUpdated() public void NewInstanceActionUpdated(InstantActionsMsg action)
{ {
if (NewInstanceMutex.WaitOne(2000)) if (NewInstanceMutex.WaitOne(2000))
{ {

View File

@ -1,5 +1,6 @@
using RobotApp.VDA5050.State; using Microsoft.AspNetCore.Components;
using RobotApp.VDA5050.Type; using RobotApp.Interfaces;
using RobotApp.VDA5050.State;
namespace RobotApp.Services.Robot; namespace RobotApp.Services.Robot;
@ -38,4 +39,35 @@ public class RobotErrors
ErrorReferences = [] ErrorReferences = []
}; };
} }
public static Error Error1001(string oldOrderId, string newOrderId)
=> CreateError(ErrorType.INITIALIZE_ORDER, "1001", ErrorLevel.WARNING, $"Có order đang được thực hiện. OrderId: {oldOrderId}, OrderId mới: {newOrderId}");
public static Error Error1002(int nodesLength)
=> CreateError(ErrorType.INITIALIZE_ORDER, "1002", ErrorLevel.WARNING, $"Order Nodes không hợp lệ. Kích thước: {nodesLength}");
public static Error Error1003(int edgesLength)
=> CreateError(ErrorType.INITIALIZE_ORDER, "1003", ErrorLevel.WARNING, $"Order Edges không hợp lệ. Kích thước: {edgesLength}");
public static Error Error1004(int nodesLength, int edgesLength)
=> CreateError(ErrorType.INITIALIZE_ORDER, "1004", ErrorLevel.WARNING, $"Order không hợp lệ do kích thước giữa Nodes và Edges không phù hợp. Kích thước Edges: {edgesLength}, kích thước nodes: {nodesLength}");
public static Error Error1005()
=> CreateError(ErrorType.INITIALIZE_ORDER, "1005", ErrorLevel.WARNING, $"Không có order đang được thực hiện.");
public static Error Error1006(PeripheralMode peripheralMode)
=> CreateError(ErrorType.INITIALIZE_ORDER, "1006", ErrorLevel.WARNING, $"Không thể khởi tạo order mới khi chế độ vận hành không phải là TỰ ĐỘNG. Chế độ hiện tại: {peripheralMode}");
public static Error Error1007()
=> CreateError(ErrorType.INITIALIZE_ORDER, "1007", ErrorLevel.WARNING, $"Không thể khởi tạo order mới khi có action đang thực hiện. Vui lòng chờ hoàn thành các action hiện tại.");
public static Error Error1008()
=> CreateError(ErrorType.INITIALIZE_ORDER, "1008", ErrorLevel.WARNING, $"Không thể khởi tạo order mới khi có lỗi nghiêm trọng. Vui lòng kiểm tra và xử lý lỗi.");
public static Error Error1009()
=> CreateError(ErrorType.INITIALIZE_ORDER, "1009", ErrorLevel.WARNING, $"Không thể khởi tạo order mới khi robot đang di chuyển. Vui lòng dừng robot.");
public static Error Error1010(string nodeId, int sequenceId, int correctIndex)
=> CreateError(ErrorType.INITIALIZE_ORDER, "1010", ErrorLevel.WARNING, $"Order Nodes không đúng thứ tự. NodeId: {nodeId}, SequenceId: {sequenceId}, Vị trí đúng: {correctIndex}");
public static Error Error1011(string edgeId, int sequenceId, int correctIndex)
=> CreateError(ErrorType.INITIALIZE_ORDER, "1011", ErrorLevel.WARNING, $"Order Edges không đúng thứ tự. EdgeId: {edgeId}, SequenceId: {sequenceId}, Vị trí đúng: {correctIndex}");
public static Error Error1012(NavigationState state)
=> CreateError(ErrorType.INITIALIZE_ORDER, "1012", ErrorLevel.WARNING, $"Không thể khởi tạo order mới khi hệ thống điều hướng không ở trạng thái sẵn sàng. Trạng thái hiện tại: {state}");
public static Error Error1013()
=> new();
public static Error Error1014(string edgeId, string nodeId)
=> CreateError(ErrorType.INITIALIZE_ORDER, "1014", ErrorLevel.WARNING, $"Edge {edgeId} chứa StartNodeId {nodeId} không tồn tại trong Nodes");
public static Error Error1015(string edgeId, string nodeId)
=> CreateError(ErrorType.INITIALIZE_ORDER, "1015", ErrorLevel.WARNING, $"Edge {edgeId} chứa {nodeId} không tồn tại trong Nodes");
} }

View File

@ -1,6 +1,5 @@
namespace RobotApp.Services.Robot namespace RobotApp.Services.Robot;
public class RobotLoads
{ {
public class RobotLoads
{
}
} }

View File

@ -0,0 +1,83 @@
using RobotApp.Common.Shares;
using RobotApp.Interfaces;
using RobotApp.Services.Robot.Simulation;
namespace RobotApp.Services.Robot;
public class Xloc
{
public double X { get; set; }
public double Y { get; set; }
public double Theta { get; set; }
public string SlamState { get; set; } = string.Empty;
public string SlamStateDetail { get; set; } = string.Empty;
public string CurrentActiveMap { get; set; } = string.Empty;
public double Reliability { get; set; }
public double MatchingScore { get; set; }
public bool IsReady { get; set; }
}
public class RobotLocalization(IConfiguration Configuration, SimulationVisualization SimVisualization) : ILocalization
{
public double X => IsSimulation ? SimVisualization.X : Xloc.X;
public double Y => IsSimulation ? SimVisualization.Y : Xloc.Y;
public double Theta => IsSimulation ? SimVisualization.Theta * Math.PI / 180 : Xloc.Theta;
public string SlamState => IsSimulation ? "Localization" : Xloc.SlamState;
public string SlamStateDetail => IsSimulation ? "" : Xloc.SlamStateDetail;
public string CurrentActiveMap => IsSimulation ? "" : Xloc.CurrentActiveMap;
public double Reliability => IsSimulation ? 100 : Xloc.Reliability;
public double MatchingScore => IsSimulation ? 100 : Xloc.MatchingScore;
public bool IsReady => IsSimulation || Xloc.IsReady;
private readonly Xloc Xloc = new();
private readonly bool IsSimulation = Configuration.GetValue<bool>("Robot:Simulation", false);
public MessageResult ActivateMap(string mapName)
{
throw new NotImplementedException();
}
public MessageResult ChangeMapOrigin(double x, double y, double theta)
{
throw new NotImplementedException();
}
public MessageResult SetInitializePosition(double x, double y, double theta)
{
throw new NotImplementedException();
}
public MessageResult StartLocalization()
{
throw new NotImplementedException();
}
public MessageResult StartMapping(double resolution)
{
throw new NotImplementedException();
}
public MessageResult StopLocalization()
{
throw new NotImplementedException();
}
public MessageResult StopMapping(string mapName)
{
throw new NotImplementedException();
}
public MessageResult SwitchMap(string mapName, double x, double y, double theta)
{
throw new NotImplementedException();
}
}

View File

@ -0,0 +1,71 @@
using RobotApp.Interfaces;
using RobotApp.Services.Robot.Simulation;
using RobotApp.Services.Robot.Simulation.Navigation;
using RobotApp.VDA5050.Order;
namespace RobotApp.Services.Robot;
public class RobotNavigation(SimulationModel SimModel) : INavigation
{
public bool IsReady => IsSimulation;
public bool Driving => IsSimulation ? SimNavigation is not null ? SimNavigation.Driving : false : false;
public double VelocityX => IsSimulation ? SimNavigation is not null ? SimNavigation.VelocityX : 0 : 0;
public double VelocityY => IsSimulation ? SimNavigation is not null ? SimNavigation.VelocityY : 0 : 0;
public double Omega => IsSimulation ? SimNavigation is not null ? SimNavigation.Omega : 0 : 0;
public NavigationState State => IsSimulation ? SimNavigation is not null ? SimNavigation.State : NavigationState.None : NavigationState.None;
private SimulationNavigation? SimNavigation;
private bool IsSimulation => SimModel.Enable;
public void CancelMovement()
{
throw new NotImplementedException();
}
public void Move(Node[] nodes, Edge[] edges)
{
throw new NotImplementedException();
}
public void MoveStraight(double x, double y)
{
throw new NotImplementedException();
}
public void Paused()
{
throw new NotImplementedException();
}
public void Resume()
{
throw new NotImplementedException();
}
public void Rotate(double angle)
{
throw new NotImplementedException();
}
public void UpdateOrder(int lastBaseSequence)
{
throw new NotImplementedException();
}
public void RefreshOrder(Node[] nodes, Edge[] edges)
{
throw new NotImplementedException();
}
public void UpdateOrder(string lastBaseNodeId)
{
throw new NotImplementedException();
}
public void Refresh()
{
throw new NotImplementedException();
}
}

View File

@ -6,7 +6,7 @@ using Action = RobotApp.VDA5050.InstantAction.Action;
namespace RobotApp.Services.Robot; namespace RobotApp.Services.Robot;
public class RobotOrderController(INavigation NavigationManager, IInstanceActions ActionManager, IError ErrorManager, Logger<RobotOrderController> Logger) : IOrder public class RobotOrderController(INavigation NavigationManager, IInstantActions ActionManager, IError ErrorManager, Logger<RobotOrderController> Logger) : IOrder
{ {
public string OrderId { get; private set; } = string.Empty; public string OrderId { get; private set; } = string.Empty;
public int OrderUpdateId { get; private set; } public int OrderUpdateId { get; private set; }
@ -22,17 +22,18 @@ public class RobotOrderController(INavigation NavigationManager, IInstanceAction
private WatchTimer<RobotOrderController>? OrderTimer; private WatchTimer<RobotOrderController>? OrderTimer;
private int BaseSequenceId = 0; private int BaseSequenceId = 0;
public void StartOrder(string orderId, Node[] nodes, Edge[] edges) public void StartOrder(string orderId, Node[] nodes, Edge[] edges)
{ {
if (OrderMutex.WaitOne(2000)) if (OrderMutex.WaitOne(2000))
{ {
try try
{ {
if (!string.IsNullOrEmpty(OrderId) && orderId != OrderId) throw new OrderException(RobotErrors.CreateError(ErrorType.INITIALIZE_ORDER, "1001", ErrorLevel.WARNING, $"Có order đang được thực hiện. OrderId: {OrderId}, OrderId mới: {orderId}")); if (!string.IsNullOrEmpty(OrderId) && orderId != OrderId) throw new OrderException(RobotErrors.Error1001(OrderId, orderId));
if (nodes.Length < 2) throw new OrderException(RobotErrors.CreateError(ErrorType.INITIALIZE_ORDER, "1002", ErrorLevel.WARNING, $"Order Nodes không hợp lệ. Kích thước: {nodes.Length}")); if (nodes.Length < 2) throw new OrderException(RobotErrors.Error1002(nodes.Length));
if (edges.Length < 1) throw new OrderException(RobotErrors.CreateError(ErrorType.INITIALIZE_ORDER, "1003", ErrorLevel.WARNING, $"Order Edges không hợp lệ. Kích thước: {edges.Length}")); if (edges.Length < 1) throw new OrderException(RobotErrors.Error1003(edges.Length));
if (edges.Length != nodes.Length - 1) throw new OrderException(RobotErrors.CreateError(ErrorType.INITIALIZE_ORDER, "1004", ErrorLevel.WARNING, $"Order không hợp lệ do kích thước giữa Nodes và Edges không phù hợp. Kích thước Edges: {edges.Length}, kích thước nodes: {nodes.Length}")); if (edges.Length != nodes.Length - 1) throw new OrderException(RobotErrors.Error1004(nodes.Length, edges.Length));
if (NavigationManager.State != NavigationState.Idle) throw new OrderException(RobotErrors.CreateError(ErrorType.INITIALIZE_ORDER, "1012", ErrorLevel.WARNING, $"Không thể khởi tạo order mới khi hệ thống điều hướng không ở trạng thái sẵn sàng. Trạng thái hiện tại: {NavigationManager.State}")); if (NavigationManager.State != NavigationState.Idle) throw new OrderException(RobotErrors.Error1012(NavigationManager.State));
for (int i = 0; i < nodes.Length; i++) for (int i = 0; i < nodes.Length; i++)
{ {
@ -52,12 +53,12 @@ public class RobotOrderController(INavigation NavigationManager, IInstanceAction
} }
OrderActions.TryAdd(nodes[i].NodeId, edges[i].Actions); OrderActions.TryAdd(nodes[i].NodeId, edges[i].Actions);
} }
if (nodes[i].SequenceId != i) throw new OrderException(RobotErrors.CreateError(ErrorType.INITIALIZE_ORDER, "1010", ErrorLevel.WARNING, $"Order Nodes không đúng thứ tự. NodeId: {nodes[i].NodeId}, SequenceId: {nodes[i].SequenceId}, Vị trí đúng: {i}")); if (nodes[i].SequenceId != i) throw new OrderException(RobotErrors.Error1010(nodes[i].NodeId, nodes[i].SequenceId, i));
if (i < nodes.Length - 1 && edges[i].SequenceId != i) throw new OrderException(RobotErrors.CreateError(ErrorType.INITIALIZE_ORDER, "1011", ErrorLevel.WARNING, $"Order Edges không đúng thứ tự. EdgeId: {edges[i].EdgeId}, SequenceId: {edges[i].SequenceId}, Vị trí đúng: {i}")); if (i < nodes.Length - 1 && edges[i].SequenceId != i) throw new OrderException(RobotErrors.Error1011(edges[i].EdgeId, edges[i].SequenceId, i));
if (nodes[i].Released) BaseSequenceId = nodes[i].SequenceId; if (nodes[i].Released) BaseSequenceId = nodes[i].SequenceId;
} }
NodeStates = nodes.Select(n => new NodeState NodeStates = [.. nodes.Select(n => new NodeState
{ {
NodeId = n.NodeId, NodeId = n.NodeId,
Released = n.Released, Released = n.Released,
@ -70,15 +71,15 @@ public class RobotOrderController(INavigation NavigationManager, IInstanceAction
Theta = n.NodePosition.Theta, Theta = n.NodePosition.Theta,
MapId = n.NodePosition.MapId MapId = n.NodePosition.MapId
} }
}).ToArray(); })];
EdgeStates = edges.Select(e => new EdgeState EdgeStates = [.. edges.Select(e => new EdgeState
{ {
EdgeId = e.EdgeId, EdgeId = e.EdgeId,
Released = e.Released, Released = e.Released,
EdgeDescription = e.EdgeDescription, EdgeDescription = e.EdgeDescription,
SequenceId = e.SequenceId, SequenceId = e.SequenceId,
Trajectory = e.Trajectory Trajectory = e.Trajectory
}).ToArray(); })];
OrderId = orderId; OrderId = orderId;
ActionManager.AddOrderActions([.. OrderActions.Values.SelectMany(a => a)]); ActionManager.AddOrderActions([.. OrderActions.Values.SelectMany(a => a)]);
@ -111,7 +112,7 @@ public class RobotOrderController(INavigation NavigationManager, IInstanceAction
{ {
try try
{ {
if (string.IsNullOrEmpty(OrderId)) throw new OrderException(RobotErrors.CreateError(ErrorType.INITIALIZE_ORDER, "1005", ErrorLevel.WARNING, $"Không có order đang được thực hiện.")); if (string.IsNullOrEmpty(OrderId)) throw new OrderException(RobotErrors.Error1005());
if (orderUpdateId > OrderUpdateId) if (orderUpdateId > OrderUpdateId)
{ {
OrderUpdateId = orderUpdateId; OrderUpdateId = orderUpdateId;

View File

@ -2,7 +2,7 @@
using RobotApp.Common.Shares.Enums; using RobotApp.Common.Shares.Enums;
using RobotApp.Common.Shares.Model; using RobotApp.Common.Shares.Model;
using RobotApp.Services.Exceptions; using RobotApp.Services.Exceptions;
using RobotApp.Services.Robot.Navigation; using RobotApp.Services.Robot.Simulation.Navigation;
using RobotApp.VDA5050.Order; using RobotApp.VDA5050.Order;
using RobotApp.VDA5050.State; using RobotApp.VDA5050.State;
@ -14,20 +14,20 @@ public class RobotPathPlanner(IConfiguration Configuration)
public NavigationNode[] GetNavigationNode(double currentTheta, Node[] nodes, Edge[] edges) public NavigationNode[] GetNavigationNode(double currentTheta, Node[] nodes, Edge[] edges)
{ {
if (nodes.Length < 2) throw new OrderException(RobotErrors.CreateError(ErrorType.INITIALIZE_ORDER, "1002", ErrorLevel.WARNING, $"Order Nodes không hợp lệ. Kích thước: {nodes.Length}")); if (nodes.Length < 2) throw new PathPlannerException(RobotErrors.Error1002(nodes.Length));
if (edges.Length < 1) throw new OrderException(RobotErrors.CreateError(ErrorType.INITIALIZE_ORDER, "1003", ErrorLevel.WARNING, $"Order Edges không hợp lệ. Kích thước: {edges.Length}")); if (edges.Length < 1) throw new PathPlannerException(RobotErrors.Error1003(edges.Length));
if (edges.Length != nodes.Length - 1) throw new OrderException(RobotErrors.CreateError(ErrorType.INITIALIZE_ORDER, "1004", ErrorLevel.WARNING, $"Order không hợp lệ do kích thước giữa Nodes và Edges không phù hợp. Kích thước Edges: {edges.Length}, kích thước nodes: {nodes.Length}")); if (edges.Length != nodes.Length - 1) throw new PathPlannerException(RobotErrors.Error1004(nodes.Length, edges.Length));
return []; return [];
} }
public NavigationNode[] PathSplit(NavigationNode[] nodes, Edge[] edges) public NavigationNode[] PathSplit(NavigationNode[] nodes, Edge[] edges)
{ {
if (nodes.Length < 2) throw new OrderException(RobotErrors.CreateError(ErrorType.INITIALIZE_ORDER, "1002", ErrorLevel.WARNING, $"Order Nodes không hợp lệ. Kích thước: {nodes.Length}")); if (nodes.Length < 2) throw new PathPlannerException(RobotErrors.Error1002(nodes.Length));
if (edges.Length < 1) throw new OrderException(RobotErrors.CreateError(ErrorType.INITIALIZE_ORDER, "1003", ErrorLevel.WARNING, $"Order Edges không hợp lệ. Kích thước: {edges.Length}")); if (edges.Length < 1) throw new PathPlannerException(RobotErrors.Error1003(edges.Length));
if (edges.Length != nodes.Length - 1) throw new OrderException(RobotErrors.CreateError(ErrorType.INITIALIZE_ORDER, "1004", ErrorLevel.WARNING, $"Order không hợp lệ do kích thước giữa Nodes và Edges không phù hợp. Kích thước Edges: {edges.Length}, kích thước nodes: {nodes.Length}")); if (edges.Length != nodes.Length - 1) throw new PathPlannerException(RobotErrors.Error1004(nodes.Length, edges.Length));
List<NavigationNode> navigationNode = [new() List <NavigationNode> navigationNode = [new()
{ {
NodeId = nodes[0].NodeId, NodeId = nodes[0].NodeId,
X = nodes[0].X, X = nodes[0].X,
@ -40,8 +40,8 @@ public class RobotPathPlanner(IConfiguration Configuration)
{ {
var startNode = nodes.FirstOrDefault(n => n.NodeId == edge.StartNodeId); var startNode = nodes.FirstOrDefault(n => n.NodeId == edge.StartNodeId);
var endNode = nodes.FirstOrDefault(n => n.NodeId == edge.EndNodeId); var endNode = nodes.FirstOrDefault(n => n.NodeId == edge.EndNodeId);
if (startNode is null) throw new OrderException(RobotErrors.CreateError(ErrorType.INITIALIZE_ORDER, "1014", ErrorLevel.WARNING, $"Edge chứa StartNodeId không tồn tại trong Nodes . StartNodeId: {edge.StartNodeId}")); if (startNode is null) throw new PathPlannerException(RobotErrors.Error1014(edge.EdgeId, edge.StartNodeId));
if (endNode is null) throw new OrderException(RobotErrors.CreateError(ErrorType.INITIALIZE_ORDER, "1015", ErrorLevel.WARNING, $"Edge chứa EndNodeId không tồn tại trong Nodes . EndNodeId: {edge.EndNodeId}")); if (endNode is null) throw new PathPlannerException(RobotErrors.Error1014(edge.EdgeId, edge.EndNodeId));
var EdgeCalculatorModel = new EdgeCalculatorModel() var EdgeCalculatorModel = new EdgeCalculatorModel()
{ {

View File

@ -0,0 +1,77 @@
using RobotApp.Interfaces;
namespace RobotApp.Services.Robot;
public class RobotPeripheral : BackgroundService, IPeripheral, ISafety
{
public PeripheralMode PeripheralMode { get; private set; } = PeripheralMode.SERVICE;
public bool IsReady { get; private set; } = true;
public bool Emergency => throw new NotImplementedException();
public bool Bumper => throw new NotImplementedException();
public bool LiftedUp => throw new NotImplementedException();
public bool LiftedDown => throw new NotImplementedException();
public bool LiftHome => throw new NotImplementedException();
public bool LidarFrontProtectField => throw new NotImplementedException();
public bool LidarBackProtectField => throw new NotImplementedException();
public bool LidarFrontTimProtectField => throw new NotImplementedException();
public bool LeftMotorReady => throw new NotImplementedException();
public bool RightMotorReady => throw new NotImplementedException();
public bool LiftMotorReady => throw new NotImplementedException();
public bool SwitchLock => throw new NotImplementedException();
public bool SwitchManual => throw new NotImplementedException();
public bool SwitchAuto => throw new NotImplementedException();
public bool ButtonStart => throw new NotImplementedException();
public bool ButtonStop => throw new NotImplementedException();
public bool ButtonReset => throw new NotImplementedException();
public bool HasLoad => throw new NotImplementedException();
public bool MutedBase => throw new NotImplementedException();
public bool MutedLoad => throw new NotImplementedException();
public bool StartedCharging => throw new NotImplementedException();
public bool SpeedVerySlow => throw new NotImplementedException();
public bool SpeedSlow => throw new NotImplementedException();
public bool SpeedNormal => throw new NotImplementedException();
public bool SpeedMedium => throw new NotImplementedException();
public bool SpeedOptimal => throw new NotImplementedException();
public bool SpeedFast => throw new NotImplementedException();
public bool SpeedVeryFast => throw new NotImplementedException();
public bool SetHorizontalLoad()
{
throw new NotImplementedException();
}
public bool SetMutedBase(bool muted)
{
throw new NotImplementedException();
}
public bool SetMutedLoad(bool muted)
{
throw new NotImplementedException();
}
public bool SetOnCharging()
{
throw new NotImplementedException();
}
public bool SetProccessState()
{
throw new NotImplementedException();
}
public bool SetSytemState()
{
throw new NotImplementedException();
}
protected override Task ExecuteAsync(CancellationToken stoppingToken)
{
throw new NotImplementedException();
}
public override Task StopAsync(CancellationToken cancellationToken)
{
return base.StopAsync(cancellationToken);
}
}

View File

@ -1,6 +0,0 @@
namespace RobotApp.Services.Robot
{
public class RobotSafety
{
}
}

View File

@ -1,6 +1,34 @@
namespace RobotApp.Services.Robot using RobotApp.Interfaces;
using RobotApp.VDA5050.Visualization;
namespace RobotApp.Services.Robot;
public class RobotVisualization(ILocalization Localization)
{ {
public class RobotVisualization public VisualizationMsg Visualization => GetVisualizationMsg();
public string SerialNumber { get; set; } = string.Empty;
private uint HeaderId;
private VisualizationMsg GetVisualizationMsg()
{ {
return new VisualizationMsg()
{
HeaderId = HeaderId++,
SerialNumber = SerialNumber,
MapId = Localization.CurrentActiveMap,
MapDescription = string.Empty,
AgvPosition = new AgvPosition()
{
X = Localization.X,
Y = Localization.Y,
Theta = Localization.Theta
},
Velocity = new Velocity()
{
Vx = 0,
Vy = 0,
Omega = 0
}
};
} }
} }

View File

@ -1,4 +1,4 @@
namespace RobotApp.Services.Robot.Navigation.Algorithm; namespace RobotApp.Services.Robot.Simulation.Navigation.Algorithm;
public class FuzzyLogic public class FuzzyLogic
{ {

View File

@ -1,4 +1,4 @@
namespace RobotApp.Services.Robot.Navigation.Algorithm; namespace RobotApp.Services.Robot.Simulation.Navigation.Algorithm;
public class PID public class PID
{ {
@ -29,13 +29,12 @@ public class PID
return this; return this;
} }
public double PID_step(double error, double min, double max) public double PID_step(double error, double min, double max, double timeSample)
{ {
DateTime CurrentTime = DateTime.Now; DateTime CurrentTime = DateTime.Now;
double TimeSample = (CurrentTime - PreTime).TotalSeconds;
double P_part = Kp * (error - Pre_Error); double P_part = Kp * (error - Pre_Error);
double I_part = 0.5 * Ki * TimeSample * (error + Pre_Error); double I_part = 0.5 * Ki * timeSample * (error + Pre_Error);
double D_part = Kd / TimeSample * (error - 2 * Pre_Error + Pre_Pre_Error); double D_part = Kd / timeSample * (error - 2 * Pre_Error + Pre_Pre_Error);
double Out = Pre_Out + P_part + I_part + D_part; double Out = Pre_Out + P_part + I_part + D_part;
Pre_Pre_Error = Pre_Error; Pre_Pre_Error = Pre_Error;
Pre_Error = error; Pre_Error = error;

View File

@ -0,0 +1,158 @@
using RobotApp.Common.Shares.Enums;
namespace RobotApp.Services.Robot.Simulation.Navigation.Algorithm;
public class PurePursuit
{
private double MaxAngularVelocity = 1.5;
private double LookaheadDistance = 0.5;
private List<NavigationNode> Waypoints_Value = [];
public int OnNodeIndex = 0;
public int GoalIndex = 0;
public NavigationNode? Goal;
public PurePursuit WithLookheadDistance(double distance)
{
LookaheadDistance = distance;
return this;
}
public PurePursuit WithMaxAngularVelocity(double vel)
{
MaxAngularVelocity = vel;
return this;
}
public PurePursuit WithPath(NavigationNode[] path)
{
Waypoints_Value = [.. path];
return this;
}
public void UpdatePath(NavigationNode[] path)
{
Waypoints_Value = [.. path];
}
public void UpdateGoal(NavigationNode goal)
{
Goal = goal;
GoalIndex = Waypoints_Value.IndexOf(Goal);
}
public (NavigationNode node, int index) GetOnNode(double x, double y)
{
double minDistance = double.MaxValue;
NavigationNode onNode = Waypoints_Value[0];
int index = 0;
for (int i = 1; i < Waypoints_Value.Count; i++)
{
var distance = Math.Sqrt(Math.Pow(x - Waypoints_Value[i].X, 2) + Math.Pow(y - Waypoints_Value[i].Y, 2));
if (distance < minDistance)
{
onNode = Waypoints_Value[i];
minDistance = distance;
index = i;
}
}
return (onNode, index);
}
private (NavigationNode? node, int index) OnNode(double x, double y)
{
if (Waypoints_Value is null || Waypoints_Value.Count == 0) return (null, 0);
double minDistance = double.MaxValue;
int index = 0;
NavigationNode? onNode = null;
if (Goal is null) return (onNode, index);
for (int i = OnNodeIndex; i < Waypoints_Value.IndexOf(Goal); i++)
{
var distance = Math.Sqrt(Math.Pow(x - Waypoints_Value[i].X, 2) + Math.Pow(y - Waypoints_Value[i].Y, 2));
if (distance < minDistance)
{
onNode = Waypoints_Value[i];
minDistance = distance;
index = i;
}
}
return (onNode, index);
}
public double PurePursuit_step(double X_Ref, double Y_Ref, double Angle_Ref)
{
if (Waypoints_Value is null || Waypoints_Value.Count < 2) return 0;
NavigationNode? lookaheadStartPt = null;
var (onNode, index) = OnNode(X_Ref, Y_Ref);
if (onNode is null || Goal is null) return 0;
OnNodeIndex = index;
double lookDistance = 0;
for (int i = OnNodeIndex + 1; i < Waypoints_Value.IndexOf(Goal); i++)
{
lookDistance += Math.Sqrt(Math.Pow(Waypoints_Value[i - 1].X - Waypoints_Value[i].X, 2) + Math.Pow(Waypoints_Value[i - 1].Y - Waypoints_Value[i].Y, 2));
if (lookDistance >= LookaheadDistance || Waypoints_Value[i].Direction != onNode.Direction)
{
lookaheadStartPt = Waypoints_Value[i];
break;
}
}
lookaheadStartPt ??= Goal;
if (onNode.Direction == RobotDirection.BACKWARD)
{
if (Angle_Ref > Math.PI) Angle_Ref -= Math.PI * 2;
else if (Angle_Ref < -Math.PI) Angle_Ref += Math.PI * 2;
Angle_Ref += Math.PI;
if (Angle_Ref > Math.PI) Angle_Ref -= Math.PI * 2;
}
var distance = Math.Atan2(lookaheadStartPt.Y - Y_Ref, lookaheadStartPt.X - X_Ref) - Angle_Ref;
if (Math.Abs(distance) > Math.PI)
{
double minDistance;
if (distance + Math.PI == 0.0) minDistance = 0.0;
else
{
double data = (distance + Math.PI) / (2 * Math.PI);
if (data < 0) data = Math.Round(data + 0.5);
else data = Math.Round(data - 0.5);
minDistance = distance + Math.PI - data * (2 * Math.PI);
double checker = 0;
if (minDistance != 0.0)
{
checker = Math.Abs((distance + Math.PI) / (2 * Math.PI));
}
if (!(Math.Abs(checker - Math.Floor(checker + 0.5)) > 2.2204460492503131E-16 * checker))
{
minDistance = 0.0;
}
else if (distance + Math.PI < 0.0)
{
minDistance += Math.PI * 2;
}
}
if (minDistance == 0.0 && distance + Math.PI > 0.0)
{
minDistance = Math.PI * 2;
}
distance = minDistance - Math.PI;
}
var AngularVelocity = 2.0 * 0.5 * Math.Sin(distance) / LookaheadDistance;
if (Math.Abs(AngularVelocity) > MaxAngularVelocity)
{
if (AngularVelocity < 0.0)
{
AngularVelocity = -1.0;
}
else if (AngularVelocity > 0.0)
{
AngularVelocity = 1.0;
}
else if (AngularVelocity == 0.0)
{
AngularVelocity = 0.0;
}
AngularVelocity *= MaxAngularVelocity;
}
return AngularVelocity;
}
}

View File

@ -0,0 +1,78 @@
using RobotApp.Common.Shares.Enums;
using RobotApp.Interfaces;
namespace RobotApp.Services.Robot.Simulation.Navigation;
public class DifferentialNavigation : SimulationNavigation
{
private readonly Logger<DifferentialNavigation> Logger;
public DifferentialNavigation(IServiceProvider ServiceProvider) : base(ServiceProvider)
{
using var scope = ServiceProvider.CreateScope();
Logger = scope.ServiceProvider.GetRequiredService<Logger<DifferentialNavigation>>();
}
protected override void NavigationHandler()
{
try
{
if (NavState == NavigationState.Rotating)
{
if (RotatePID is not null)
{
double Error = Visualization.Theta - TargetAngle;
if (Error > 180) Error -= 360;
else if (Error < -180) Error += 360;
if (Math.Abs(Error) < 1)
{
if (NavigationPath is not null && NavigationPath.Length > 2) NavState = NavigationState.Moving;
else Dispose();
}
else
{
var SpeedCal = RotatePID.PID_step(Error * Math.PI / 180, AngularVelocity, -AngularVelocity, CycleHandlerMilliseconds / 1000.0);
VelocityController.SetSpeed(SpeedCal, SpeedCal, CycleHandlerMilliseconds / 1000.0);
}
}
}
else if (NavState == NavigationState.Moving)
{
if (NavigationPath is not null && SimulationOrderNodes.Length > 1 && CurrentBaseNode is not null)
{
if (MovePID is not null && MoveFuzzy is not null && MovePurePursuit is not null)
{
var DistanceToGoal = Math.Sqrt(Math.Pow(Visualization.X - SimulationOrderNodes[^1].X, 2) + Math.Pow(Visualization.Y - SimulationOrderNodes[^1].Y, 2));
var DistanceToCheckingNode = Math.Sqrt(Math.Pow(Visualization.X - CurrentBaseNode.X, 2) + Math.Pow(Visualization.Y - CurrentBaseNode.Y, 2));
var deviation = CurrentBaseNode.NodeId == SimulationOrderNodes[^1].NodeId ? 0.02 : 0.1;
if (DistanceToCheckingNode > deviation)
{
double SpeedTarget = MovePID.PID_step(DistanceToCheckingNode, SimulationModel.MaxVelocity, 0, CycleHandlerMilliseconds / 1000.0);
double AngularVel = MovePurePursuit.PurePursuit_step(Visualization.X, Visualization.Y, Visualization.Theta * Math.PI / 180);
AngularVel *= NavigationPath[MovePurePursuit.OnNodeIndex].Direction == RobotDirection.FORWARD ? 1 : -1;
(double AngularVelocityLeft, double AngularVelocityRight) = MoveFuzzy.Fuzzy_step(SpeedTarget, AngularVel, CycleHandlerMilliseconds / 1000.0);
if (NavigationPath[MovePurePursuit.OnNodeIndex].Direction == RobotDirection.FORWARD)
{
AngularVelocityLeft /= SimulationModel.RadiusWheel;
AngularVelocityRight = AngularVelocityRight / SimulationModel.RadiusWheel * -1;
}
else
{
AngularVelocityLeft = AngularVelocityLeft / SimulationModel.RadiusWheel * -1;
AngularVelocityRight /= SimulationModel.RadiusWheel;
}
VelocityController.SetSpeed(AngularVelocityLeft, AngularVelocityRight, CycleHandlerMilliseconds / 1000.0);
}
else if (DistanceToGoal < 0.02) Dispose();
else NavState = NavigationState.Waiting;
}
}
}
}
catch (Exception ex)
{
Logger.Write($"Error in DifferentialNavigation: {ex.Message}", LogLevel.Error);
}
}
}

View File

@ -0,0 +1,23 @@
namespace RobotApp.Services.Robot.Simulation.Navigation;
public class ForkliftNavigation : SimulationNavigation
{
private readonly Logger<ForkliftNavigation> Logger;
public ForkliftNavigation(IServiceProvider ServiceProvider) : base(ServiceProvider)
{
using var scope = ServiceProvider.CreateScope();
Logger = scope.ServiceProvider.GetRequiredService<Logger<ForkliftNavigation>>();
}
protected override void NavigationHandler()
{
try
{
// Implement differential drive navigation logic here
}
catch (Exception ex)
{
Logger.Write($"Error in ForkliftNavigationSevice: {ex.Message}", LogLevel.Error);
}
}
}

View File

@ -1,6 +1,6 @@
using RobotApp.Common.Shares.Enums; using RobotApp.Common.Shares.Enums;
namespace RobotApp.Services.Robot.Navigation; namespace RobotApp.Services.Robot.Simulation.Navigation;
public class NavigationNode public class NavigationNode
{ {
@ -9,5 +9,7 @@ public class NavigationNode
public double Y { get; set; } public double Y { get; set; }
public double Theta { get; set; } public double Theta { get; set; }
public RobotDirection Direction { get; set; } public RobotDirection Direction { get; set; }
public double AllowedDeviationXY { get; set; }
public double AllowedDeviationTheta { get; set; }
public string Description { get; set; } = string.Empty; public string Description { get; set; } = string.Empty;
} }

View File

@ -0,0 +1,192 @@
using Microsoft.AspNetCore.Components;
using RobotApp.Common.Shares;
using RobotApp.Common.Shares.Enums;
using RobotApp.Interfaces;
using RobotApp.Services.Exceptions;
using RobotApp.Services.Robot.Simulation.Navigation.Algorithm;
using RobotApp.VDA5050.Order;
using RobotApp.VDA5050.State;
using System.IO;
namespace RobotApp.Services.Robot.Simulation.Navigation;
public class SimulationNavigation : INavigation, IDisposable
{
public NavigationState State => NavState;
public bool Driving => NavDriving;
public bool IsReady => true;
public double VelocityX => Visualization.Vx;
public double VelocityY => Visualization.Vy;
public double Omega => Visualization.Omega;
protected NavigationState NavState = NavigationState.None;
protected bool NavDriving = false;
protected const int CycleHandlerMilliseconds = 50;
private WatchTimer<SimulationNavigation>? NavigationTimer;
protected double TargetAngle = 0;
protected PID? RotatePID;
protected readonly double AngularVelocity;
protected PID? MovePID;
protected FuzzyLogic? MoveFuzzy;
protected PurePursuit? MovePurePursuit;
protected Node[] OrderNodes = [];
protected Edge[] OrderEdges = [];
protected NavigationNode[] SimulationOrderNodes = [];
protected readonly SimulationVisualization Visualization;
protected readonly SimulationVelocity VelocityController;
protected readonly SimulationModel SimulationModel;
protected readonly RobotPathPlanner PathPlanner;
protected NavigationNode[] NavigationPath = [];
protected NavigationNode? CurrentBaseNode;
protected NavigationState ResumeState = NavigationState.None;
private readonly Logger<SimulationNavigation> Logger;
private bool IsIdle => NavState == NavigationState.Idle || NavState == NavigationState.Canceled;
public SimulationNavigation(IServiceProvider ServiceProvider)
{
using var scope = ServiceProvider.CreateScope();
Logger = scope.ServiceProvider.GetRequiredService<Logger<SimulationNavigation>>();
Visualization = scope.ServiceProvider.GetRequiredService<SimulationVisualization>();
SimulationModel = scope.ServiceProvider.GetRequiredService<SimulationModel>();
PathPlanner = scope.ServiceProvider.GetRequiredService<RobotPathPlanner>();
VelocityController = new(Visualization, SimulationModel);
}
protected void HandleNavigationStart()
{
NavigationTimer = new(CycleHandlerMilliseconds, NavigationHandler, Logger);
NavigationTimer.Start();
}
protected void HandleNavigationStop()
{
NavigationTimer?.Dispose();
}
protected virtual void NavigationHandler() { }
public void CancelMovement()
{
Dispose();
NavState = NavigationState.Canceled;
}
public void Move(Node[] nodes, Edge[] edges)
{
if (!IsIdle) throw new SimulationException(RobotErrors.Error1012(NavState));
NavState = NavigationState.Initializing;
if (nodes.Length < 2) throw new SimulationException(RobotErrors.Error1002(nodes.Length));
if (edges.Length < 1) throw new SimulationException(RobotErrors.Error1003(edges.Length));
if (edges.Length != nodes.Length - 1) throw new SimulationException(RobotErrors.Error1004(nodes.Length, edges.Length));
var pathDirection = PathPlanner.GetNavigationNode(Visualization.Theta, nodes, edges);
NavigationPath = PathPlanner.PathSplit(pathDirection, edges);
MovePID = new PID().WithKp(1).WithKi(0.0001).WithKd(0.6);
MoveFuzzy = new FuzzyLogic().WithGainP(1.1);
MovePurePursuit = new PurePursuit().WithLookheadDistance(0.35).WithPath([.. NavigationPath]);
double angleFoward = Math.Atan2(NavigationPath[1].Y - NavigationPath[0].Y, NavigationPath[1].X - NavigationPath[0].X) * 180 / Math.PI;
double angleBacward = Math.Atan2(NavigationPath[0].Y - NavigationPath[1].Y, NavigationPath[0].X - NavigationPath[1].X) * 180 / Math.PI;
OrderNodes = nodes;
OrderEdges = edges;
SimulationOrderNodes = pathDirection;
Rotate(SimulationOrderNodes[0].Direction == RobotDirection.FORWARD ? angleFoward : angleBacward);
}
public void MoveStraight(double x, double y)
{
if (!IsIdle) throw new SimulationException(RobotErrors.Error1012(NavState));
var headRobotNode = new NavigationNode()
{
X = Visualization.X * Math.Acos(Visualization.Theta * Math.PI / 180),
Y = Visualization.Y * Math.Asin(Visualization.Theta * Math.PI / 180),
};
var goalNode = new NavigationNode()
{
NodeId = "RobotGoal",
X = x,
Y = y,
};
var currentRobotNode = new NavigationNode()
{
NodeId = "RobotCurrentNode",
X = Visualization.X,
Y = Visualization.Y,
};
goalNode.Theta = MathExtensions.GetVectorAngle(currentRobotNode.X, currentRobotNode.Y, headRobotNode.X, headRobotNode.Y, goalNode.X, goalNode.Y) > 90 ?
Math.Atan2(currentRobotNode.Y - goalNode.Y, currentRobotNode.X - goalNode.X) :
Math.Atan2(goalNode.Y - currentRobotNode.Y, goalNode.X - currentRobotNode.X);
currentRobotNode.Theta = goalNode.Theta;
NavigationPath = PathPlanner.PathSplit([currentRobotNode, goalNode], [new Edge()
{
EdgeId = "Straight edge",
Trajectory = new Trajectory()
{
Degree = 1,
ControlPoints = []
},
StartNodeId = currentRobotNode.NodeId,
EndNodeId = goalNode.NodeId,
}]);
MovePID = new PID().WithKp(1).WithKi(0.0001).WithKd(0.6);
MoveFuzzy = new FuzzyLogic().WithGainP(1.1);
MovePurePursuit = new PurePursuit().WithLookheadDistance(0.35).WithPath(NavigationPath);
double Angle = Math.Atan2(NavigationPath[1].Y - NavigationPath[0].Y, NavigationPath[1].X - NavigationPath[0].X);
Rotate(Angle * 180 / Math.PI);
UpdateOrder(goalNode.NodeId);
}
public void Paused()
{
ResumeState = State;
NavState = NavigationState.Paused;
}
public void Resume()
{
NavState = ResumeState;
ResumeState = NavigationState.None;
}
public void Rotate(double angle)
{
if (!IsIdle) throw new SimulationException(RobotErrors.Error1012(NavState));
RotatePID = new PID().WithKp(10).WithKi(0.01).WithKd(0.1);
TargetAngle = MathExtensions.NormalizeAngle(angle);
NavState = NavigationState.Rotating;
HandleNavigationStart();
}
public void UpdateOrder(string lastBaseNodeId)
{
}
public void RefreshOrder(Node[] nodes, Edge[] edges)
{
throw new NotImplementedException();
}
public void Dispose()
{
HandleNavigationStop();
VelocityController.Stop();
OrderNodes = [];
OrderEdges = [];
SimulationOrderNodes = [];
NavDriving = false;
NavState = NavigationState.None;
GC.SuppressFinalize(this);
}
public void Refresh()
{
NavState = NavigationState.Idle;
}
}

View File

@ -0,0 +1,12 @@
using RobotApp.Common.Shares.Enums;
namespace RobotApp.Services.Robot.Simulation.Navigation;
public class SimulationNavigationManager
{
public static SimulationNavigation GetNavigation(NavigationType type, IServiceProvider ServiceProvider)
{
if (type == NavigationType.Forklift) return new ForkliftNavigation(ServiceProvider);
return new DifferentialNavigation(ServiceProvider);
}
}

View File

@ -0,0 +1,13 @@
using System.Diagnostics.CodeAnalysis;
namespace RobotApp.Services.Robot.Simulation;
public static class SimulationExtensions
{
public static IServiceCollection AddRobotSimulation(this IServiceCollection services)
{
services.AddSingleton<SimulationVisualization>();
services.AddSingleton<SimulationModel>();
return services;
}
}

View File

@ -0,0 +1,16 @@
using RobotApp.Common.Shares.Enums;
namespace RobotApp.Services.Robot.Simulation;
public class SimulationModel(IConfiguration Configuration)
{
public readonly bool Enable = Configuration.GetValue("Simulation:Enable", false);
public readonly double RadiusWheel = Configuration.GetValue("Simulation:RadiusWheel", 0.1);
public readonly double Width = Configuration.GetValue("Simulation:Width", 0.606);
public readonly double Length = Configuration.GetValue("Simulation:Length", 1.106);
public readonly double MaxVelocity = Configuration.GetValue("Simulation:MaxVelocity", 1.5);
public readonly double MaxAngularVelocity = Configuration.GetValue("Simulation:MaxAngularVelocity", 0.3);
public readonly double Acceleration = Configuration.GetValue("Simulation:Acceleration", 2);
public readonly double Deceleration = Configuration.GetValue("Simulation:Deceleration", 1);
public readonly NavigationType NavigationType = Configuration.GetValue("Simulation:NavigationType", NavigationType.Differential);
}

View File

@ -0,0 +1,56 @@
namespace RobotApp.Services.Robot.Simulation;
public class SimulationVelocity(SimulationVisualization Visualization, SimulationModel Model)
{
private readonly double Acceleration = Model.Acceleration;
private readonly double Deceleration = Model.Deceleration;
private double AngularVelLeft;
private double AngularVelRight;
private (double angularVelLeft, double angularVelRight) AccelerationCalculator(double wL, double wR, double wL_Current, double wR_Current)
{
var angularVelLeft = wL_Current;
var angularVelRight = wR_Current;
if (wL_Current == 0 || wL / wL_Current < 0)
{
if (wL != 0) angularVelLeft += wL / Math.Abs(wL) * Acceleration;
else angularVelLeft = wL;
}
else
{
if (Math.Abs(wL) - Math.Abs(wL_Current) > Acceleration) angularVelLeft += Acceleration * wL_Current / Math.Abs(wL_Current);
else if (Math.Abs(wL_Current) - Math.Abs(wL) > Deceleration) angularVelLeft -= Deceleration * wL_Current / Math.Abs(wL_Current);
else angularVelLeft = wL;
}
if (wR_Current == 0 || wR / wR_Current < 0)
{
if (wR != 0) angularVelRight += wR / Math.Abs(wR) * Acceleration;
else angularVelRight = wR;
}
else
{
if (Math.Abs(wR) - Math.Abs(wR_Current) > Acceleration) angularVelRight += Acceleration * wR_Current / Math.Abs(wR_Current);
else if (Math.Abs(wR_Current) - Math.Abs(wR) > Deceleration) angularVelRight -= Deceleration * wR_Current / Math.Abs(wR_Current);
else angularVelRight = wR;
}
if (Math.Abs(angularVelLeft) > Math.Abs(wL)) angularVelLeft = wL;
if (Math.Abs(angularVelRight) > Math.Abs(wR)) angularVelRight = wR;
return (angularVelLeft, angularVelRight);
}
public bool SetSpeed(double angularVelLeft, double angularVelRight, double sampleTime)
{
(AngularVelLeft, AngularVelRight) = AccelerationCalculator(angularVelLeft, angularVelRight, AngularVelLeft, AngularVelRight);
//Console.WriteLine($"AngularVelLeft = {AngularVelLeft:0.####}, AngularVelRight = {AngularVelRight:0.####}");
_ = Visualization.UpdatePosition(AngularVelLeft, AngularVelRight, sampleTime);
return true;
}
public void Stop()
{
(AngularVelLeft, AngularVelRight) = (0, 0);
_ = Visualization.UpdatePosition(AngularVelLeft, AngularVelRight, 0.05);
}
}

View File

@ -0,0 +1,39 @@
namespace RobotApp.Services.Robot.Simulation;
public class SimulationVisualization(SimulationModel Model)
{
public double X { get; private set; }
public double Y { get; private set; }
public double Theta { get; private set; }
public double Vx { get; private set; }
public double Vy { get; private set; }
public double Omega { get; private set; }
private readonly double RadiusWheel = Model.RadiusWheel;
private readonly double RadiusRobot = Model.Width / 2;
public (double x, double y, double angle) UpdatePosition(double wL, double wR, double time)
{
Theta = (Theta + time * (-wR - wL) * RadiusWheel / RadiusRobot * 180 / Math.PI) % 360;
X += time * (-wR + wL) * RadiusWheel * Math.Cos(Theta * Math.PI / 180) / 2;
Y += time * (-wR + wL) * RadiusWheel * Math.Sin(Theta * Math.PI / 180) / 2;
_ = UpdateVelocity(wL, wR);
if (Theta > 180) Theta -= 360;
else if (Theta < -180) Theta += 360;
return (X, Y, Theta);
}
public (double vx, double vy, double omega) UpdateVelocity(double wL, double wR)
{
Vx = (-wR + wL) * RadiusWheel / 2;
Omega = (-wR - wL) * RadiusWheel / RadiusRobot;
return (Vx, 0, Omega);
}
public void LocalizationInitialize(double x, double y, double theta)
{
X = x;
Y = y;
Theta = theta;
}
}

View File

@ -0,0 +1,39 @@
using RobotApp.Interfaces;
using RobotApp.Services.Robot;
using RobotApp.Services.State;
using System.Diagnostics.CodeAnalysis;
namespace RobotApp.Services;
public static class RobotExtensions
{
public static IServiceCollection AddRobot(this IServiceCollection services)
{
services.AddSingleton<RobotStateMachine>();
services.AddInterfaceServiceSingleton<ILocalization, RobotLocalization>();
return services;
}
public static IServiceCollection AddInterfaceServiceSingleton<TService, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] TImplementation>(this IServiceCollection services) where TService : class where TImplementation : class, TService
{
services.AddSingleton<TImplementation>();
services.AddSingleton<TService>(sp => sp.GetRequiredService<TImplementation>());
return services;
}
public static IServiceCollection AddHostedServiceSingleton<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] THostedService>(this IServiceCollection services) where THostedService : class, IHostedService
{
services.AddSingleton<THostedService>();
services.AddHostedService(sp => sp.GetRequiredService<THostedService>());
return services;
}
public static IServiceCollection AddHostedInterfaceServiceSingleton<TService, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] THostedService>(this IServiceCollection services) where TService : class where THostedService : class, IHostedService, TService
{
services.AddSingleton<THostedService>();
services.AddSingleton<TService>(sp => sp.GetRequiredService<THostedService>());
services.AddHostedService(sp => sp.GetRequiredService<THostedService>());
return services;
}
}

View File

@ -0,0 +1,7 @@
using RobotApp.Common.Shares.Enums;
namespace RobotApp.Services.State;
public class ACTState<T>(ACTStateType state, IRobotState? superState = null) : RobotState<T>(state, superState) where T : Enum
{
}

View File

@ -0,0 +1,7 @@
using RobotApp.Common.Shares.Enums;
namespace RobotApp.Services.State;
public class AutoState<T>(AutoStateType state, IRobotState? superState = null) : RobotState<T>(state, superState) where T : Enum
{
}

View File

@ -0,0 +1,7 @@
using RobotApp.Common.Shares.Enums;
namespace RobotApp.Services.State;
public class ExecutingState<T>(ExecutingStateType state, IRobotState? superState = null) : RobotState<T>(state, superState) where T : Enum
{
}

View File

@ -0,0 +1,7 @@
using RobotApp.Common.Shares.Enums;
namespace RobotApp.Services.State;
public class FaultState<T>(FaultStateType state, IRobotState? superState = null) : RobotState<T>(state, superState) where T : Enum
{
}

View File

@ -0,0 +1,21 @@
namespace RobotApp.Services.State;
public interface IRobotState
{
Enum Name { get; }
Type Type { get; }
IRobotState? SuperState { get; }
List<IRobotState> SubStates { get; set; }
IRobotState? ActiveSubState { get;}
IRobotState? HistoryState { get;}
bool HasSubStates => SubStates.Count > 0;
bool IsActive => ActiveSubState != null;
event Action<IRobotState>? OnEnter;
event Action<IRobotState>? OnExit;
event Action<IRobotState , IRobotState>? OnTransition;
void Enter();
void Exit();
bool CanTransitionTo(IRobotState targetState);
void Update();
}

View File

@ -0,0 +1,7 @@
using RobotApp.Common.Shares.Enums;
namespace RobotApp.Services.State;
public class ManualState<T>(ManualStateType state, IRobotState? superState = null) : RobotState<T>(state, superState) where T : Enum
{
}

View File

@ -0,0 +1,7 @@
using RobotApp.Common.Shares.Enums;
namespace RobotApp.Services.State;
public class MoveState<T>(MoveStateType state, IRobotState? superState = null) : RobotState<T>(state, superState) where T : Enum
{
}

View File

@ -0,0 +1,7 @@
using RobotApp.Common.Shares.Enums;
namespace RobotApp.Services.State;
public class PlanState<T>(PlanStateType state, IRobotState? superState = null) : RobotState<T>(state, superState) where T : Enum
{
}

View File

@ -1,8 +1,38 @@
namespace RobotApp.Services.State; namespace RobotApp.Services.State;
public abstract class RobotState<T>(T type) where T : Enum public abstract class RobotState<T>(Enum name, IRobotState? superState = null) : IRobotState where T : Enum
{ {
public readonly T Type = type; public Enum Name { get; private set; } = name;
public virtual void Enter() { } public Type Type { get; private set; } = typeof(T);
public virtual void Exit() { } public IRobotState? SuperState { get; private set; } = superState;
public List<IRobotState> SubStates { get; set; } = [];
public IRobotState? ActiveSubState { get; protected set; }
public IRobotState? HistoryState { get; protected set; }
public bool HasSubStates => SubStates.Count > 0;
public bool IsActive => ActiveSubState != null;
public override string ToString() => $"{GetType().Name}({Name})";
public event Action<IRobotState>? OnEnter;
public event Action<IRobotState>? OnExit;
public event Action<IRobotState, IRobotState>? OnTransition;
public virtual void Enter()
{
OnEnter?.Invoke(this);
}
public virtual void Exit()
{
OnExit?.Invoke(this);
}
public virtual bool CanTransitionTo(IRobotState targetState)
{
return true;
}
public virtual void Update()
{
ActiveSubState?.Update();
}
} }

View File

@ -0,0 +1,352 @@
using RobotApp.Common.Shares.Enums;
namespace RobotApp.Services.State;
public record RobotStateMachine(Logger<RobotStateMachine> Logger) : IDisposable
{
private readonly Dictionary<Type, Dictionary<Enum, IRobotState>> StateRegistry = [];
public IRobotState? CurrentState { get; private set; }
public event Action<IRobotState?, IRobotState>? OnStateChanged;
public bool IsInitialized = false;
private readonly Lock StateLock = new();
private void PrintStateAdded()
{
Console.WriteLine("=== All Registered States ===");
var allStates = StateRegistry.Values
.SelectMany(dict => dict.Values)
.Where(state => state.SuperState == null)
.OrderBy(state => state.Type.Name)
.ThenBy(state => state.Name.ToString());
foreach (var rootState in allStates)
{
PrintStateSimple(rootState, 0);
}
var totalStates = StateRegistry.Values.Sum(dict => dict.Count);
Console.WriteLine($"\nTotal: {totalStates} states registered");
Console.WriteLine("==============================\n");
}
private void PrintStateSimple(IRobotState state, int depth)
{
var indent = new string(' ', depth * 4);
var marker = state == CurrentState ? " [CURRENT]" : "";
var activeMarker = state.SuperState?.ActiveSubState == state ? " [ACTIVE]" : "";
Console.WriteLine($"{indent}- {state.Name}{marker}{activeMarker}");
foreach (var subState in state.SubStates.OrderBy(s => s.Name.ToString()))
{
PrintStateSimple(subState, depth + 1);
}
}
public void InitializeHierarchyStates()
{
if (IsInitialized) return;
RegisterState(new RootState<RootStateType>(RootStateType.System));
RegisterState(new RootState<RootStateType>(RootStateType.Auto));
RegisterState(new RootState<RootStateType>(RootStateType.Manual));
RegisterState(new RootState<RootStateType>(RootStateType.Service));
RegisterState(new RootState<RootStateType>(RootStateType.Stop));
RegisterState(new RootState<RootStateType>(RootStateType.Fault));
SetupSystemState();
SetupAutoState();
SetupManualState();
SetupServiceState();
SetupStopState();
SetupFaultState();
var systemState = GetState(RootStateType.System);
lock (StateLock)
{
systemState.Enter();
CurrentState = systemState;
}
PrintStateAdded();
IsInitialized = true;
Logger.Info($"Khởi tạo thành công State Machine với State hiện tại: {GetCurrentStatePath()}");
}
private void SetupSystemState()
{
var systemlState = GetState(RootStateType.System) ?? throw new InvalidOperationException($"Failed to get state RootStateType.Auto");
var initializingState = new SystemState<SystemStateType>(SystemStateType.Initializing, systemlState);
var standbyState = new SystemState<SystemStateType>(SystemStateType.Standby, systemlState);
var shutting_DownState = new SystemState<SystemStateType>(SystemStateType.Shutting_Down, systemlState);
systemlState.SubStates.AddRange([initializingState, standbyState, shutting_DownState]);
RegisterState(initializingState);
RegisterState(standbyState);
RegisterState(shutting_DownState);
}
private void SetupAutoState()
{
var autoState = GetState(RootStateType.Auto) ?? throw new InvalidOperationException($"Failed to get state RootStateType.Auto");
var idleState = new AutoState<AutoStateType>(AutoStateType.Idle, autoState);
var executingState = new AutoState<AutoStateType>(AutoStateType.Executing, autoState);
var pausedState = new AutoState<AutoStateType>(AutoStateType.Paused, autoState);
var holdingState = new AutoState<AutoStateType>(AutoStateType.Holding, autoState);
var cancelingState = new AutoState<AutoStateType>(AutoStateType.Canceling, autoState);
var recoveringState = new AutoState<AutoStateType>(AutoStateType.Recovering, autoState);
var remote_Override_State = new AutoState<AutoStateType>(AutoStateType.Remote_Override, autoState);
autoState.SubStates.AddRange([ idleState, executingState, pausedState, holdingState,
cancelingState, recoveringState, remote_Override_State ]);
RegisterState(idleState);
RegisterState(executingState);
RegisterState(pausedState);
RegisterState(holdingState);
RegisterState(cancelingState);
RegisterState(recoveringState);
RegisterState(remote_Override_State);
SetupExecutingState();
}
private void SetupManualState()
{
var manualState = GetState(RootStateType.Manual) ?? throw new InvalidOperationException($"Failed to get state RootStateType.Auto");
var idleState = new ManualState<ManualStateType>(ManualStateType.Idle, manualState);
var executingState = new ManualState<ManualStateType>(ManualStateType.Active, manualState);
manualState.SubStates.AddRange([idleState, executingState]);
RegisterState(idleState);
RegisterState(executingState);
}
private void SetupServiceState()
{
var serviceState = GetState(RootStateType.Service) ?? throw new InvalidOperationException($"Failed to get state RootStateType.Auto");
var idleState = new ServiceState<ServiceStateType>(ServiceStateType.Idle, serviceState);
var executingState = new ServiceState<ServiceStateType>(ServiceStateType.Active, serviceState);
serviceState.SubStates.AddRange([idleState, executingState]);
RegisterState(idleState);
RegisterState(executingState);
}
private void SetupStopState()
{
var stopState = GetState(RootStateType.Stop) ?? throw new InvalidOperationException($"Failed to get state RootStateType.Auto");
var emcStopState = new StopState<StopStateType>(StopStateType.EMC, stopState);
var protectiveState = new StopState<StopStateType>(StopStateType.Protective, stopState);
var manualStopState = new StopState<StopStateType>(StopStateType.Manual, stopState);
stopState.SubStates.AddRange([emcStopState, protectiveState, manualStopState]);
RegisterState(emcStopState);
RegisterState(protectiveState);
RegisterState(manualStopState);
}
private void SetupFaultState()
{
var faultState = GetState(RootStateType.Fault) ?? throw new InvalidOperationException($"Failed to get state RootStateType.Auto");
var navigationFaultState = new FaultState<FaultStateType>(FaultStateType.Navigation, faultState);
var localizationFaultState = new FaultState<FaultStateType>(FaultStateType.Localization, faultState);
var shelftFaultState = new FaultState<FaultStateType>(FaultStateType.Shielf, faultState);
var batteryFaultState = new FaultState<FaultStateType>(FaultStateType.Battery, faultState);
var driverFaultState = new FaultState<FaultStateType>(FaultStateType.Driver, faultState);
var peripheralsFaultState = new FaultState<FaultStateType>(FaultStateType.Peripherals, faultState);
var safetyFaultState = new FaultState<FaultStateType>(FaultStateType.Safety, faultState);
var comunicationFaultState = new FaultState<FaultStateType>(FaultStateType.Communication, faultState);
faultState.SubStates.AddRange([navigationFaultState, localizationFaultState, shelftFaultState,
batteryFaultState, driverFaultState, peripheralsFaultState, safetyFaultState,
comunicationFaultState]);
RegisterState(navigationFaultState);
RegisterState(localizationFaultState);
RegisterState(shelftFaultState);
RegisterState(batteryFaultState);
RegisterState(driverFaultState);
RegisterState(peripheralsFaultState);
RegisterState(safetyFaultState);
RegisterState(comunicationFaultState);
}
private void SetupExecutingState()
{
var executingState = GetState(AutoStateType.Executing) ?? throw new InvalidOperationException($"Failed to get state RootStateType.Auto");
var planningState = new ExecutingState<ExecutingStateType>(ExecutingStateType.Planning, executingState);
var movingState = new ExecutingState<ExecutingStateType>(ExecutingStateType.Moving, executingState);
var actStopState = new ExecutingState<ExecutingStateType>(ExecutingStateType.ACT, executingState);
executingState.SubStates.AddRange([planningState, movingState, actStopState]);
RegisterState(planningState);
RegisterState(movingState);
RegisterState(actStopState);
SetupACTState();
SetupMoveState();
SetupPlanState();
}
private void SetupACTState()
{
var ACTState = GetState(ExecutingStateType.ACT) ?? throw new InvalidOperationException($"Failed to get state RootStateType.Auto");
var dockingState = new ACTState<ACTStateType>(ACTStateType.Docking, ACTState);
var dockedState = new ACTState<ACTStateType>(ACTStateType.Docked, ACTState);
var chargingState = new ACTState<ACTStateType>(ACTStateType.Charging, ACTState);
var undockingState = new ACTState<ACTStateType>(ACTStateType.Undocking, ACTState);
var loadingState = new ACTState<ACTStateType>(ACTStateType.Loading, ACTState);
var unLoadingState = new ACTState<ACTStateType>(ACTStateType.Unloading, ACTState);
var techActionState = new ACTState<ACTStateType>(ACTStateType.TechAction, ACTState);
ACTState.SubStates.AddRange([dockingState, dockedState, chargingState, undockingState,
loadingState, unLoadingState, techActionState]);
RegisterState(dockingState);
RegisterState(dockedState);
RegisterState(chargingState);
RegisterState(undockingState);
RegisterState(loadingState);
RegisterState(unLoadingState);
RegisterState(techActionState);
}
private void SetupMoveState()
{
var moveState = GetState(ExecutingStateType.Moving) ?? throw new InvalidOperationException($"Failed to get state RootStateType.Auto");
var navigationState = new MoveState<MoveStateType>(MoveStateType.Navigation, moveState);
var avoidabceState = new MoveState<MoveStateType>(MoveStateType.Avoidance, moveState);
var approachState = new MoveState<MoveStateType>(MoveStateType.Approach, moveState);
var trackingState = new MoveState<MoveStateType>(MoveStateType.Tracking, moveState);
var repositioningState = new MoveState<MoveStateType>(MoveStateType.Repositioning, moveState);
moveState.SubStates.AddRange([navigationState, avoidabceState, approachState, trackingState, repositioningState]);
RegisterState(navigationState);
RegisterState(avoidabceState);
RegisterState(approachState);
RegisterState(trackingState);
RegisterState(repositioningState);
}
private void SetupPlanState()
{
var planState = GetState(ExecutingStateType.Planning) ?? throw new InvalidOperationException($"Failed to get state RootStateType.Auto");
var taskPlanState = new PlanState<PlanStateType>(PlanStateType.Task, planState);
var pathPlanState = new PlanState<PlanStateType>(PlanStateType.Path, planState);
planState.SubStates.AddRange([taskPlanState, pathPlanState]);
RegisterState(taskPlanState);
RegisterState(pathPlanState);
}
private void RegisterState<T>(RobotState<T> state) where T : Enum
{
var enumType = typeof(T);
StateRegistry.TryAdd(enumType, []);
StateRegistry[enumType][state.Name] = state;
}
public RobotState<T>? TryGetState<T>(T stateType) where T : Enum
{
var enumType = typeof(T);
if (StateRegistry.TryGetValue(enumType, out var states) && states.TryGetValue(stateType, out var state) && state is RobotState<T> robotState)
{
return robotState;
}
return null;
}
public RobotState<T> GetState<T>(T stateType) where T : Enum
{
return TryGetState(stateType) ?? throw new ArgumentException($"Failed to get state {stateType}");
}
public bool TransitionTo<T>(T stateType) where T : Enum
{
if (!IsInitialized)
{
Logger.Warning("State Machine chưa được khởi tạo");
return false;
}
lock (StateLock)
{
try
{
var targetState = TryGetState(stateType);
if (targetState == null)
{
Logger.Warning($"State {stateType} không được tìm thấy");
return false;
}
if (CurrentState != null)
{
if (!CurrentState.CanTransitionTo(targetState))
{
Logger.Warning($"Không thể chuyển State từ {CurrentState.Name} thành {targetState.Name}");
return false;
}
CurrentState.Exit();
}
var previousState = CurrentState;
CurrentState = targetState;
CurrentState.Enter();
Logger.Info($"Chuyển đổi State thành công: {GetStatePath(previousState)} -> {GetStatePath(targetState)}");
OnStateChanged?.Invoke(previousState, CurrentState);
return true;
}
catch (Exception ex)
{
Logger.Error($"Lỗi khi chuyển đổi State: {ex.Message}");
return false;
}
}
}
public void Update()
{
if (!IsInitialized)
{
Logger.Warning("State Machine chưa được khởi tạo");
return;
}
lock (StateLock)
{
CurrentState?.Update();
}
}
public string GetCurrentStatePath()
{
if (CurrentState == null) return "No State";
return GetStatePath(CurrentState);
}
private static string GetStatePath(IRobotState? state)
{
var path = new List<string>();
var current = state;
while (current != null)
{
path.Insert(0, current.Name.ToString());
current = current.SuperState;
}
return string.Join(".", path);
}
public void Dispose()
{
GC.SuppressFinalize(this);
}
}

View File

@ -0,0 +1,36 @@
using RobotApp.Common.Shares.Enums;
namespace RobotApp.Services.State;
public class RootState<T>(RootStateType state, IRobotState? superState = null) : RobotState<T>(state, superState) where T : Enum
{
public override void Enter()
{
switch(Name)
{
case RootStateType.System:
ActiveSubState = SubStates.FirstOrDefault(s => s.Name.Equals(SystemStateType.Initializing));
ActiveSubState?.Enter();
break;
case RootStateType.Auto:
if(HistoryState is not null) ActiveSubState = HistoryState;
else ActiveSubState = SubStates.FirstOrDefault(s => s.Name.Equals(AutoStateType.Idle));
ActiveSubState?.Enter();
break;
}
}
public override void Exit()
{
}
public override bool CanTransitionTo(IRobotState targetState)
{
return true;
}
public override void Update()
{
ActiveSubState?.Update();
}
}

View File

@ -0,0 +1,7 @@
using RobotApp.Common.Shares.Enums;
namespace RobotApp.Services.State;
public class ServiceState<T>(ServiceStateType state, IRobotState? superState = null) : RobotState<T>(state, superState) where T : Enum
{
}

View File

@ -0,0 +1,8 @@
using RobotApp.Common.Shares.Enums;
namespace RobotApp.Services.State
{
public class StopState<T>(StopStateType state, IRobotState? superState = null) : RobotState<T>(state, superState) where T : Enum
{
}
}

View File

@ -0,0 +1,21 @@
using RobotApp.Common.Shares.Enums;
namespace RobotApp.Services.State;
public class SystemState<T>(SystemStateType state, IRobotState? superState = null) : RobotState<T>(state, superState) where T : Enum
{
public override void Enter()
{
}
public override void Exit()
{
}
public override bool CanTransitionTo(IRobotState targetState)
{
return true;
}
public override void Update()
{
ActiveSubState?.Update();
}
}

View File

@ -5,8 +5,20 @@
"Logging": { "Logging": {
"LogLevel": { "LogLevel": {
"Default": "Information", "Default": "Information",
"Microsoft.AspNetCore": "Warning" "Microsoft.AspNetCore": "Warning",
"Microsoft.EntityFrameworkCore": "Warning"
} }
}, },
"AllowedHosts": "*" "AllowedHosts": "*",
"Simulation": {
"Enable": true,
"RadiusWheel": 0.1,
"Width": 0.606,
"Length": 1.106,
"MaxVelocity": 1.5,
"MaxAngularVelocity": 0.3,
"Acceleration": 2,
"Deceleration": 10,
"NavigationType": "Differential"
}
} }