Initial commit
This commit is contained in:
@@ -0,0 +1,29 @@
|
||||
using RobotNet10.RobotApp.Client.Shared.Devices;
|
||||
|
||||
namespace RobotNet10.RobotApp.Devices;
|
||||
|
||||
/// <summary>
|
||||
/// Event args khi trạng thái kết nối thay đổi
|
||||
/// </summary>
|
||||
public class ConnectionStateChangedEventArgs : EventArgs
|
||||
{
|
||||
public bool IsConnected { get; }
|
||||
public DeviceStatus PreviousStatus { get; }
|
||||
public DeviceStatus CurrentStatus { get; }
|
||||
public string? Message { get; }
|
||||
public DateTime Timestamp { get; }
|
||||
|
||||
public ConnectionStateChangedEventArgs(
|
||||
bool isConnected,
|
||||
DeviceStatus previousStatus,
|
||||
DeviceStatus currentStatus,
|
||||
string? message = null)
|
||||
{
|
||||
IsConnected = isConnected;
|
||||
PreviousStatus = previousStatus;
|
||||
CurrentStatus = currentStatus;
|
||||
Message = message;
|
||||
Timestamp = DateTime.UtcNow;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
using RobotNet10.RobotApp.Client.Shared.Devices;
|
||||
|
||||
namespace RobotNet10.RobotApp.Devices;
|
||||
|
||||
/// <summary>
|
||||
/// Attribute để đánh dấu và cung cấp metadata cho các class kế thừa từ DeviceBase
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Class, AllowMultiple = true, Inherited = false)]
|
||||
public sealed class DeviceAttribute : Attribute
|
||||
{
|
||||
/// <summary>
|
||||
/// Loại thiết bị
|
||||
/// </summary>
|
||||
public DeviceType DeviceType { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Thương hiệu/nhà sản xuất của thiết bị (ví dụ: "PhenikaaX", "SICK", "Hokuyo", etc.)
|
||||
/// </summary>
|
||||
public string Brand { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Tên driver/implementation của thiết bị (ví dụ: "SickLMS100", "HokuyoUST10", "ModbusTCPClient", etc.)
|
||||
/// </summary>
|
||||
public string DriverName { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Mô tả ngắn về thiết bị (optional)
|
||||
/// </summary>
|
||||
public string? Description { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Version của driver (optional)
|
||||
/// </summary>
|
||||
public string Version { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the DeviceAttribute class
|
||||
/// </summary>
|
||||
/// <param name="deviceType">Loại thiết bị</param>
|
||||
/// <param name="brand">Thương hiệu/nhà sản xuất</param>
|
||||
/// <param name="driverName">Tên driver/implementation</param>
|
||||
/// <param name="version">Tên version/implementation</param>
|
||||
/// <exception cref="ArgumentNullException">Thrown when brand or driverName is null or empty</exception>
|
||||
public DeviceAttribute(DeviceType deviceType, string brand, string driverName, string version)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(brand))
|
||||
throw new ArgumentNullException(nameof(brand), "Brand cannot be null or empty");
|
||||
|
||||
if (string.IsNullOrWhiteSpace(driverName))
|
||||
throw new ArgumentNullException(nameof(driverName), "DriverName cannot be null or empty");
|
||||
|
||||
if (string.IsNullOrWhiteSpace(version))
|
||||
throw new ArgumentNullException(nameof(version), "Version cannot be null or empty");
|
||||
|
||||
DeviceType = deviceType;
|
||||
Brand = brand;
|
||||
DriverName = driverName;
|
||||
Version = version;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,799 @@
|
||||
using Appccelerate.StateMachine;
|
||||
using Appccelerate.StateMachine.Machine;
|
||||
using RobotNet10.RobotApp.Client.Shared.Devices;
|
||||
|
||||
namespace RobotNet10.RobotApp.Devices;
|
||||
|
||||
/// <summary>
|
||||
/// Base class cho tất cả các device implementations trong hệ thống AMR
|
||||
/// Cung cấp state machine management và auto-reconnect logic
|
||||
/// Tất cả các thiết bị nên kế thừa từ class này
|
||||
/// </summary>
|
||||
public abstract class DeviceBase : IDisposable
|
||||
{
|
||||
private readonly PassiveStateMachine<DeviceStatus, DeviceTrigger> _stateMachine;
|
||||
private readonly Lock _lock = new();
|
||||
private DeviceStatus _currentStatus = DeviceStatus.Uninitialized;
|
||||
private DateTime _lastUpdateStateTime = DateTime.UtcNow;
|
||||
private DateTime? _lastConnectedTime;
|
||||
private DateTime? _lastDisconnectedTime;
|
||||
private Exception? _lastError;
|
||||
private int _reconnectAttemptCount;
|
||||
private CancellationTokenSource? _reconnectCts;
|
||||
private Task? _reconnectTask;
|
||||
private bool _disposed;
|
||||
private readonly Dictionary<string, string> _properties;
|
||||
|
||||
protected DeviceBase(string deviceId, string deviceName, DeviceType type, string? description = null)
|
||||
{
|
||||
DeviceId = deviceId ?? throw new ArgumentNullException(nameof(deviceId));
|
||||
DeviceName = deviceName ?? throw new ArgumentNullException(nameof(deviceName));
|
||||
Type = type;
|
||||
Description = description;
|
||||
|
||||
// Tạo PropertyDescriptions từ derived class
|
||||
var propertyDescriptions = CreatePropertyDescriptions().ToList();
|
||||
|
||||
// Validate PropertyDescriptions
|
||||
ValidatePropertyDescriptions(propertyDescriptions);
|
||||
|
||||
PropertyDescriptions = propertyDescriptions;
|
||||
|
||||
// Khởi tạo Properties dictionary với keys từ PropertyDescriptions
|
||||
_properties = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||
foreach (var propDesc in PropertyDescriptions)
|
||||
{
|
||||
_properties[propDesc.Key] = propDesc.DefaultValue ?? "0";
|
||||
}
|
||||
|
||||
AutoReconnectEnabled = true;
|
||||
ReconnectDelayMs = 3000; // 3 seconds default
|
||||
MaxReconnectAttempts = 0; // Unlimited by default
|
||||
|
||||
// Configure and create state machine
|
||||
var builder = new StateMachineDefinitionBuilder<DeviceStatus, DeviceTrigger>();
|
||||
ConfigureStateMachine(builder);
|
||||
|
||||
_stateMachine = builder
|
||||
.WithInitialState(DeviceStatus.Uninitialized)
|
||||
.Build()
|
||||
.CreatePassiveStateMachine();
|
||||
|
||||
_currentStatus = DeviceStatus.Uninitialized;
|
||||
_stateMachine.Start();
|
||||
}
|
||||
|
||||
private void ConfigureStateMachine(StateMachineDefinitionBuilder<DeviceStatus, DeviceTrigger> builder)
|
||||
{
|
||||
// Uninitialized state
|
||||
builder.In(DeviceStatus.Uninitialized)
|
||||
.ExecuteOnEntry(() => { _currentStatus = DeviceStatus.Uninitialized; UpdateLastUpdateStateTime(); })
|
||||
.On(DeviceTrigger.Initialize)
|
||||
.Goto(DeviceStatus.Initializing)
|
||||
.On(DeviceTrigger.Dispose)
|
||||
.Goto(DeviceStatus.Disposed);
|
||||
|
||||
// Initializing state
|
||||
builder.In(DeviceStatus.Initializing)
|
||||
.ExecuteOnEntry(() => { _currentStatus = DeviceStatus.Initializing; UpdateLastUpdateStateTime(); })
|
||||
.On(DeviceTrigger.InitializationCompleted)
|
||||
.Goto(DeviceStatus.Disconnected)
|
||||
.On(DeviceTrigger.ErrorOccurred)
|
||||
.Goto(DeviceStatus.Error)
|
||||
.Execute(() => { _currentStatus = DeviceStatus.Error; })
|
||||
.On(DeviceTrigger.Dispose)
|
||||
.Goto(DeviceStatus.Disposed);
|
||||
|
||||
// Disconnected state
|
||||
builder.In(DeviceStatus.Disconnected)
|
||||
.ExecuteOnEntry(() =>
|
||||
{
|
||||
_currentStatus = DeviceStatus.Disconnected;
|
||||
UpdateLastUpdateStateTime();
|
||||
_lastDisconnectedTime = DateTime.UtcNow;
|
||||
})
|
||||
.On(DeviceTrigger.Connect)
|
||||
.Goto(DeviceStatus.Connecting)
|
||||
.On(DeviceTrigger.StartReconnect)
|
||||
.Goto(DeviceStatus.Reconnecting)
|
||||
.On(DeviceTrigger.Dispose)
|
||||
.Goto(DeviceStatus.Disposed);
|
||||
|
||||
// Connecting state
|
||||
builder.In(DeviceStatus.Connecting)
|
||||
.ExecuteOnEntry(() => { _currentStatus = DeviceStatus.Connecting; UpdateLastUpdateStateTime(); })
|
||||
.On(DeviceTrigger.ConnectionCompleted)
|
||||
.Goto(DeviceStatus.Connected)
|
||||
.On(DeviceTrigger.ErrorOccurred)
|
||||
.Goto(DeviceStatus.Error)
|
||||
.Execute(() => { _currentStatus = DeviceStatus.Error; })
|
||||
.On(DeviceTrigger.Dispose)
|
||||
.Goto(DeviceStatus.Disposed);
|
||||
|
||||
// Connected state
|
||||
builder.In(DeviceStatus.Connected)
|
||||
.ExecuteOnEntry(() =>
|
||||
{
|
||||
_currentStatus = DeviceStatus.Connected;
|
||||
UpdateLastUpdateStateTime();
|
||||
_lastConnectedTime = DateTime.UtcNow;
|
||||
_reconnectAttemptCount = 0; // Reset counter on successful connection
|
||||
})
|
||||
.On(DeviceTrigger.Disconnect)
|
||||
.Goto(DeviceStatus.Disconnecting)
|
||||
.On(DeviceTrigger.ErrorOccurred)
|
||||
.Goto(DeviceStatus.Error)
|
||||
.Execute(() => { _currentStatus = DeviceStatus.Error; })
|
||||
.On(DeviceTrigger.Dispose)
|
||||
.Goto(DeviceStatus.Disposed);
|
||||
|
||||
// Disconnecting state
|
||||
builder.In(DeviceStatus.Disconnecting)
|
||||
.ExecuteOnEntry(() => { _currentStatus = DeviceStatus.Disconnecting; UpdateLastUpdateStateTime(); })
|
||||
.On(DeviceTrigger.DisconnectionCompleted)
|
||||
.Goto(DeviceStatus.Disconnected)
|
||||
.On(DeviceTrigger.ErrorOccurred)
|
||||
.Goto(DeviceStatus.Error)
|
||||
.Execute(() => { _currentStatus = DeviceStatus.Error; })
|
||||
.On(DeviceTrigger.Dispose)
|
||||
.Goto(DeviceStatus.Disposed);
|
||||
|
||||
// Reconnecting state
|
||||
builder.In(DeviceStatus.Reconnecting)
|
||||
.ExecuteOnEntry(() => { _currentStatus = DeviceStatus.Reconnecting; UpdateLastUpdateStateTime(); })
|
||||
.On(DeviceTrigger.ReconnectionCompleted)
|
||||
.Goto(DeviceStatus.Connected)
|
||||
.On(DeviceTrigger.ErrorOccurred)
|
||||
.Goto(DeviceStatus.Error)
|
||||
.Execute(() => { _currentStatus = DeviceStatus.Error; })
|
||||
.On(DeviceTrigger.StopReconnect)
|
||||
.Goto(DeviceStatus.Disconnected)
|
||||
.On(DeviceTrigger.Dispose)
|
||||
.Goto(DeviceStatus.Disposed);
|
||||
|
||||
// Error state
|
||||
builder.In(DeviceStatus.Error)
|
||||
.ExecuteOnEntry(() => { _currentStatus = DeviceStatus.Error; UpdateLastUpdateStateTime(); })
|
||||
.On(DeviceTrigger.StartReconnect)
|
||||
.Goto(DeviceStatus.Reconnecting)
|
||||
.On(DeviceTrigger.Disconnect)
|
||||
.Goto(DeviceStatus.Disconnecting)
|
||||
.On(DeviceTrigger.Dispose)
|
||||
.Goto(DeviceStatus.Disposed);
|
||||
|
||||
// Disposed state (terminal)
|
||||
builder.In(DeviceStatus.Disposed)
|
||||
.ExecuteOnEntry(() => { _currentStatus = DeviceStatus.Disposed; UpdateLastUpdateStateTime(); });
|
||||
}
|
||||
|
||||
// Device Properties
|
||||
|
||||
public string DeviceId { get; }
|
||||
public string DeviceName { get; }
|
||||
public DeviceType Type { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Mô tả thiết bị (dùng để hiển thị trên web UI)
|
||||
/// </summary>
|
||||
public string? Description { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Danh sách mô tả các properties của thiết bị (dùng để hiển thị trên web UI)
|
||||
/// Được tạo cố định từ constructor, không thay đổi trong runtime
|
||||
/// </summary>
|
||||
public IReadOnlyList<PropertyDescription> PropertyDescriptions { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Dictionary chứa các giá trị properties của thiết bị (dùng để hiển thị trên web UI)
|
||||
/// Key không phân biệt hoa thường, Value là string
|
||||
/// Chỉ đọc từ bên ngoài, chỉ có thể cập nhật giá trị thông qua SetProperty method
|
||||
/// </summary>
|
||||
public IReadOnlyDictionary<string, string> Properties => _properties;
|
||||
|
||||
public DeviceStatus Status
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
return _currentStatus;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsConnected => Status == DeviceStatus.Connected;
|
||||
|
||||
public DateTime LastUpdateStateTime
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
return _lastUpdateStateTime;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public DateTime? LastConnectedTime
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
return _lastConnectedTime;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public DateTime? LastDisconnectedTime
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
return _lastDisconnectedTime;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public Exception? LastError
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
return _lastError;
|
||||
}
|
||||
}
|
||||
protected set
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
_lastError = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool AutoReconnectEnabled { get; set; }
|
||||
public int ReconnectDelayMs { get; set; }
|
||||
|
||||
public int ReconnectAttemptCount
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
return _reconnectAttemptCount;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public int MaxReconnectAttempts { get; set; }
|
||||
|
||||
// Events
|
||||
|
||||
public event EventHandler<ConnectionStateChangedEventArgs>? ConnectionStateChanged;
|
||||
public event EventHandler<DeviceErrorEventArgs>? ErrorOccurred;
|
||||
public event EventHandler<DeviceStatusChangedEventArgs>? StatusChanged;
|
||||
|
||||
// State Machine Helper Methods
|
||||
|
||||
private static void ValidatePropertyDescriptions(List<PropertyDescription> propertyDescriptions)
|
||||
{
|
||||
var seenKeys = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
foreach (var propDesc in propertyDescriptions)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(propDesc.Key))
|
||||
{
|
||||
throw new ArgumentException("PropertyDescription.Key cannot be null or empty", nameof(propertyDescriptions));
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(propDesc.DisplayName))
|
||||
{
|
||||
throw new ArgumentException($"PropertyDescription.DisplayName cannot be null or empty for key '{propDesc.Key}'", nameof(propertyDescriptions));
|
||||
}
|
||||
|
||||
if (!seenKeys.Add(propDesc.Key))
|
||||
{
|
||||
throw new ArgumentException($"Duplicate PropertyDescription.Key found: '{propDesc.Key}'", nameof(propertyDescriptions));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateLastUpdateStateTime()
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
_lastUpdateStateTime = DateTime.UtcNow;
|
||||
}
|
||||
}
|
||||
|
||||
private void FireTrigger(DeviceTrigger trigger)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
ObjectDisposedException.ThrowIf(_disposed && trigger != DeviceTrigger.Dispose, this);
|
||||
|
||||
try
|
||||
{
|
||||
_stateMachine.Fire(trigger);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LastError = ex;
|
||||
OnErrorOccurred(ex, $"Failed to fire trigger {trigger}");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Abstract methods to be implemented by derived classes
|
||||
|
||||
/// <summary>
|
||||
/// Tạo danh sách PropertyDescriptions cho thiết bị
|
||||
/// Method này được gọi một lần trong constructor, kết quả được cache
|
||||
/// </summary>
|
||||
protected virtual IEnumerable<PropertyDescription> CreatePropertyDescriptions()
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Khởi tạo thiết bị (implementation specific)
|
||||
/// </summary>
|
||||
protected abstract Task OnInitializeAsync(CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Kết nối với thiết bị (implementation specific)
|
||||
/// </summary>
|
||||
protected abstract Task OnConnectAsync(CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Ngắt kết nối với thiết bị (implementation specific)
|
||||
/// </summary>
|
||||
protected abstract Task OnDisconnectAsync(CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Reset thiết bị (implementation specific)
|
||||
/// </summary>
|
||||
protected abstract Task OnResetAsync(CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Kiểm tra kết nối (implementation specific)
|
||||
/// </summary>
|
||||
protected abstract Task<bool> OnCheckConnectionAsync(CancellationToken cancellationToken);
|
||||
|
||||
// Public methods with state machine management
|
||||
|
||||
public virtual async Task InitializeAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
ThrowIfDisposed();
|
||||
|
||||
var previousStatus = Status;
|
||||
if (previousStatus != DeviceStatus.Uninitialized)
|
||||
return; // Already initialized
|
||||
|
||||
FireTrigger(DeviceTrigger.Initialize);
|
||||
|
||||
try
|
||||
{
|
||||
await OnInitializeAsync(cancellationToken);
|
||||
FireTrigger(DeviceTrigger.InitializationCompleted);
|
||||
OnStatusChanged(previousStatus, DeviceStatus.Disconnected);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LastError = ex;
|
||||
FireTrigger(DeviceTrigger.ErrorOccurred);
|
||||
OnErrorOccurred(ex, "Initialize failed");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public virtual async Task ConnectAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
ThrowIfDisposed();
|
||||
|
||||
var currentStatus = Status;
|
||||
if (currentStatus == DeviceStatus.Connected)
|
||||
return; // Already connected
|
||||
|
||||
// Auto-initialize if needed
|
||||
if (currentStatus == DeviceStatus.Uninitialized)
|
||||
{
|
||||
await InitializeAsync(cancellationToken);
|
||||
}
|
||||
|
||||
var previousStatus = Status;
|
||||
FireTrigger(DeviceTrigger.Connect);
|
||||
|
||||
try
|
||||
{
|
||||
await OnConnectAsync(cancellationToken);
|
||||
|
||||
// Verify connection
|
||||
var isConnected = await OnCheckConnectionAsync(cancellationToken);
|
||||
if (isConnected)
|
||||
{
|
||||
// Clear last error when connection succeeds
|
||||
lock (_lock)
|
||||
{
|
||||
_lastError = null;
|
||||
}
|
||||
|
||||
FireTrigger(DeviceTrigger.ConnectionCompleted);
|
||||
OnConnectionStateChanged(true, previousStatus, DeviceStatus.Connected, "Connected successfully");
|
||||
}
|
||||
else
|
||||
{
|
||||
FireTrigger(DeviceTrigger.ErrorOccurred);
|
||||
throw new InvalidOperationException("Connection check failed after connect");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LastError = ex;
|
||||
FireTrigger(DeviceTrigger.ErrorOccurred);
|
||||
OnErrorOccurred(ex, "Connect failed");
|
||||
|
||||
// Start auto-reconnect if enabled
|
||||
if (AutoReconnectEnabled && !_disposed)
|
||||
{
|
||||
StartAutoReconnect();
|
||||
}
|
||||
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public virtual async Task DisconnectAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (_disposed)
|
||||
return;
|
||||
|
||||
var currentStatus = Status;
|
||||
if (currentStatus == DeviceStatus.Disconnected || currentStatus == DeviceStatus.Disposed)
|
||||
return;
|
||||
|
||||
// Stop auto-reconnect
|
||||
StopAutoReconnect();
|
||||
|
||||
var previousStatus = currentStatus;
|
||||
FireTrigger(DeviceTrigger.Disconnect);
|
||||
|
||||
try
|
||||
{
|
||||
await OnDisconnectAsync(cancellationToken);
|
||||
FireTrigger(DeviceTrigger.DisconnectionCompleted);
|
||||
OnConnectionStateChanged(false, previousStatus, DeviceStatus.Disconnected, "Disconnected");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LastError = ex;
|
||||
FireTrigger(DeviceTrigger.ErrorOccurred);
|
||||
OnErrorOccurred(ex, "Disconnect failed");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public virtual async Task ResetAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
ThrowIfDisposed();
|
||||
|
||||
// Stop auto-reconnect
|
||||
StopAutoReconnect();
|
||||
|
||||
try
|
||||
{
|
||||
// Disconnect first if connected
|
||||
if (Status == DeviceStatus.Connected || Status == DeviceStatus.Connecting)
|
||||
{
|
||||
await DisconnectAsync(cancellationToken);
|
||||
}
|
||||
|
||||
// Reset implementation
|
||||
await OnResetAsync(cancellationToken);
|
||||
|
||||
// Reset state
|
||||
lock (_lock)
|
||||
{
|
||||
_reconnectAttemptCount = 0;
|
||||
_lastError = null;
|
||||
}
|
||||
|
||||
// If not already disconnected, transition to disconnected
|
||||
if (Status != DeviceStatus.Disconnected)
|
||||
{
|
||||
FireTrigger(DeviceTrigger.Disconnect);
|
||||
FireTrigger(DeviceTrigger.DisconnectionCompleted);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LastError = ex;
|
||||
FireTrigger(DeviceTrigger.ErrorOccurred);
|
||||
OnErrorOccurred(ex, "Reset failed");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public virtual async Task<bool> CheckConnectionAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
ThrowIfDisposed();
|
||||
|
||||
if (Status != DeviceStatus.Connected)
|
||||
return false;
|
||||
|
||||
try
|
||||
{
|
||||
// Capture current status before checking (may change during check)
|
||||
var currentStatusBeforeCheck = Status;
|
||||
var isConnected = await OnCheckConnectionAsync(cancellationToken);
|
||||
|
||||
// Check if status changed or connection lost
|
||||
var currentStatusAfterCheck = Status;
|
||||
if (!isConnected && currentStatusBeforeCheck == DeviceStatus.Connected && currentStatusAfterCheck == DeviceStatus.Connected)
|
||||
{
|
||||
// Connection lost but status hasn't changed yet
|
||||
FireTrigger(DeviceTrigger.ErrorOccurred);
|
||||
OnConnectionStateChanged(false, currentStatusBeforeCheck, DeviceStatus.Error, "Connection lost");
|
||||
|
||||
// Start auto-reconnect if enabled
|
||||
if (AutoReconnectEnabled)
|
||||
{
|
||||
StartAutoReconnect();
|
||||
}
|
||||
}
|
||||
|
||||
return isConnected;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LastError = ex;
|
||||
OnErrorOccurred(ex, "Check connection failed");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Auto-Reconnect Logic
|
||||
|
||||
private void StartAutoReconnect()
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
if (_reconnectTask != null && !_reconnectTask.IsCompleted)
|
||||
return; // Already reconnecting
|
||||
|
||||
if (!AutoReconnectEnabled || _disposed)
|
||||
return;
|
||||
|
||||
_reconnectCts?.Cancel();
|
||||
_reconnectCts = new CancellationTokenSource();
|
||||
_reconnectTask = Task.Run(() => AutoReconnectLoopAsync(_reconnectCts.Token));
|
||||
}
|
||||
}
|
||||
|
||||
private void StopAutoReconnect()
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
_reconnectCts?.Cancel();
|
||||
_reconnectCts?.Dispose();
|
||||
_reconnectCts = null;
|
||||
_reconnectTask = null;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task AutoReconnectLoopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
while (!cancellationToken.IsCancellationRequested && !_disposed)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Check if we should stop (read status in lock to avoid race condition)
|
||||
DeviceStatus currentStatus;
|
||||
lock (_lock)
|
||||
{
|
||||
if (_disposed)
|
||||
break;
|
||||
currentStatus = _currentStatus;
|
||||
}
|
||||
|
||||
if (currentStatus == DeviceStatus.Connected || currentStatus == DeviceStatus.Disposed)
|
||||
break;
|
||||
|
||||
// Check max attempts
|
||||
lock (_lock)
|
||||
{
|
||||
if (MaxReconnectAttempts > 0 && _reconnectAttemptCount >= MaxReconnectAttempts)
|
||||
{
|
||||
FireTrigger(DeviceTrigger.ErrorOccurred);
|
||||
OnErrorOccurred(
|
||||
new InvalidOperationException($"Max reconnect attempts ({MaxReconnectAttempts}) reached"),
|
||||
"Auto-reconnect stopped");
|
||||
break;
|
||||
}
|
||||
|
||||
_reconnectAttemptCount++;
|
||||
}
|
||||
|
||||
// Wait before reconnect
|
||||
await Task.Delay(ReconnectDelayMs, cancellationToken);
|
||||
|
||||
// Try to reconnect (read status in lock to avoid race condition)
|
||||
lock (_lock)
|
||||
{
|
||||
if (_disposed)
|
||||
break;
|
||||
currentStatus = _currentStatus;
|
||||
}
|
||||
|
||||
if (currentStatus == DeviceStatus.Error || currentStatus == DeviceStatus.Disconnected)
|
||||
{
|
||||
try
|
||||
{
|
||||
FireTrigger(DeviceTrigger.StartReconnect);
|
||||
|
||||
// Call implementation directly (not ConnectAsync to avoid state machine conflict)
|
||||
await OnConnectAsync(cancellationToken);
|
||||
|
||||
// Verify connection
|
||||
var isConnected = await OnCheckConnectionAsync(cancellationToken);
|
||||
if (isConnected)
|
||||
{
|
||||
// Clear last error when reconnection succeeds
|
||||
lock (_lock)
|
||||
{
|
||||
_lastError = null;
|
||||
}
|
||||
|
||||
FireTrigger(DeviceTrigger.ReconnectionCompleted);
|
||||
OnConnectionStateChanged(true, DeviceStatus.Reconnecting, DeviceStatus.Connected,
|
||||
$"Auto-reconnected (attempt {ReconnectAttemptCount})");
|
||||
break; // Success
|
||||
}
|
||||
else
|
||||
{
|
||||
FireTrigger(DeviceTrigger.ErrorOccurred);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LastError = ex;
|
||||
FireTrigger(DeviceTrigger.ErrorOccurred);
|
||||
OnErrorOccurred(ex, $"Auto-reconnect attempt {ReconnectAttemptCount} failed");
|
||||
// Continue loop to retry
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
break;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LastError = ex;
|
||||
OnErrorOccurred(ex, "Auto-reconnect loop error");
|
||||
// Wait before retrying
|
||||
await Task.Delay(ReconnectDelayMs, cancellationToken);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Event Handlers
|
||||
|
||||
protected virtual void OnConnectionStateChanged(bool isConnected, DeviceStatus previousStatus, DeviceStatus currentStatus, string? message = null)
|
||||
{
|
||||
ConnectionStateChanged?.Invoke(this, new ConnectionStateChangedEventArgs(isConnected, previousStatus, currentStatus, message));
|
||||
}
|
||||
|
||||
protected virtual void OnErrorOccurred(Exception exception, string? context = null)
|
||||
{
|
||||
ErrorOccurred?.Invoke(this, new DeviceErrorEventArgs(exception, Status, context));
|
||||
}
|
||||
|
||||
protected virtual void OnStatusChanged(DeviceStatus previousStatus, DeviceStatus currentStatus)
|
||||
{
|
||||
StatusChanged?.Invoke(this, new DeviceStatusChangedEventArgs(previousStatus, currentStatus));
|
||||
}
|
||||
|
||||
// Property Management
|
||||
|
||||
/// <summary>
|
||||
/// Cập nhật giá trị của một property
|
||||
/// Chỉ có thể cập nhật property đã được định nghĩa trong PropertyDescriptions
|
||||
/// </summary>
|
||||
protected void SetProperty(string key, string value)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(key))
|
||||
throw new ArgumentException("Key cannot be null or empty", nameof(key));
|
||||
|
||||
lock (_lock)
|
||||
{
|
||||
if (!_properties.ContainsKey(key))
|
||||
{
|
||||
throw new ArgumentException($"Property '{key}' is not defined in PropertyDescriptions", nameof(key));
|
||||
}
|
||||
|
||||
_properties[key] = value ?? string.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Lấy giá trị của một property
|
||||
/// </summary>
|
||||
protected string? GetProperty(string key)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(key))
|
||||
return null;
|
||||
|
||||
lock (_lock)
|
||||
{
|
||||
return _properties.TryGetValue(key, out var value) ? value : null;
|
||||
}
|
||||
}
|
||||
|
||||
// IDisposable
|
||||
|
||||
protected virtual void ThrowIfDisposed()
|
||||
{
|
||||
ObjectDisposedException.ThrowIf(_disposed, this);
|
||||
}
|
||||
|
||||
protected virtual void Dispose(bool disposing)
|
||||
{
|
||||
if (_disposed)
|
||||
return;
|
||||
|
||||
if (disposing)
|
||||
{
|
||||
// Stop auto-reconnect
|
||||
StopAutoReconnect();
|
||||
|
||||
// Disconnect if connected
|
||||
var currentStatus = Status;
|
||||
if (currentStatus == DeviceStatus.Connected || currentStatus == DeviceStatus.Connecting)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Use Task.Run to avoid deadlock in async contexts
|
||||
Task.Run(async () => await DisconnectAsync().ConfigureAwait(false)).GetAwaiter().GetResult();
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Ignore errors during dispose
|
||||
}
|
||||
}
|
||||
|
||||
// Transition to disposed state
|
||||
try
|
||||
{
|
||||
FireTrigger(DeviceTrigger.Dispose);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Ignore errors during dispose
|
||||
}
|
||||
|
||||
// Stop state machine
|
||||
try
|
||||
{
|
||||
_stateMachine.Stop();
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Ignore errors
|
||||
}
|
||||
}
|
||||
|
||||
_disposed = true;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
using RobotNet10.RobotApp.Client.Shared.Devices;
|
||||
|
||||
namespace RobotNet10.RobotApp.Devices;
|
||||
|
||||
/// <summary>
|
||||
/// Event args khi có lỗi xảy ra
|
||||
/// </summary>
|
||||
public class DeviceErrorEventArgs : EventArgs
|
||||
{
|
||||
public Exception Exception { get; }
|
||||
public DeviceStatus Status { get; }
|
||||
public string? Context { get; }
|
||||
public DateTime Timestamp { get; }
|
||||
|
||||
public DeviceErrorEventArgs(Exception exception, DeviceStatus status, string? context = null)
|
||||
{
|
||||
Exception = exception;
|
||||
Status = status;
|
||||
Context = context;
|
||||
Timestamp = DateTime.UtcNow;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,842 @@
|
||||
using RobotNet10.RobotApp.Client.Shared.Devices;
|
||||
using System.Reflection;
|
||||
|
||||
namespace RobotNet10.RobotApp.Devices;
|
||||
|
||||
/// <summary>
|
||||
/// Service quản lý và cung cấp truy xuất devices
|
||||
/// Tự động tạo devices từ configuration khi start
|
||||
/// Thread-safe implementation
|
||||
/// </summary>
|
||||
public class DeviceProvider(
|
||||
IConfiguration _configuration,
|
||||
IServiceProvider _serviceProvider,
|
||||
ILogger<DeviceProvider> _logger) : IDeviceProvider, IHostedService
|
||||
{
|
||||
private readonly Dictionary<string, DeviceBase> _devicesById = new(StringComparer.OrdinalIgnoreCase);
|
||||
private readonly Dictionary<DeviceType, List<DeviceBase>> _devicesByType = [];
|
||||
private readonly Lock _lock = new();
|
||||
private bool _devicesLoaded = false;
|
||||
private bool _devicesConnected = false;
|
||||
private readonly ManualResetEventSlim _devicesLoadedEvent = new(false);
|
||||
private readonly ManualResetEventSlim _devicesConnectedEvent = new(false);
|
||||
private Task? _connectMonitorTask;
|
||||
private CancellationTokenSource? _connectCts;
|
||||
|
||||
public async Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Discover all device types that implement DeviceBase and have DeviceAttribute
|
||||
var deviceTypes = DiscoverDeviceTypes();
|
||||
|
||||
// Log discovered device types
|
||||
foreach (var kvp in deviceTypes)
|
||||
{
|
||||
var attribute = kvp.Value.GetCustomAttribute<DeviceAttribute>();
|
||||
}
|
||||
|
||||
// Lấy collection các IConfigurationSection từ section "Devices"
|
||||
var sections = _configuration.GetSection("Devices").GetChildren();
|
||||
|
||||
// Validate duplicate DeviceIds before creating devices
|
||||
var deviceIds = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
foreach (var section in sections)
|
||||
{
|
||||
var enabled = section["Enabled"];
|
||||
if (enabled == null || !bool.Parse(enabled))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var deviceId = section["DeviceId"];
|
||||
if (!string.IsNullOrWhiteSpace(deviceId))
|
||||
{
|
||||
if (!deviceIds.Add(deviceId))
|
||||
{
|
||||
_logger.LogWarning("Duplicate DeviceId '{DeviceId}' found in configuration section {SectionKey}. Skipping duplicate.",
|
||||
deviceId, section.Key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Tạo devices từ configuration sections
|
||||
var createdDevices = new List<DeviceBase>();
|
||||
foreach (var section in sections)
|
||||
{
|
||||
try
|
||||
{
|
||||
var enabled = section["Enabled"];
|
||||
if (enabled == null || !bool.Parse(enabled))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
var device = CreateDeviceFromConfigurationSection(section, deviceTypes);
|
||||
if (device != null)
|
||||
{
|
||||
RegisterDeviceInternal(device);
|
||||
createdDevices.Add(device);
|
||||
// Subscribe to device status changes to update _devicesConnected
|
||||
device.StatusChanged += OnDeviceStatusChanged;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
var errorMessage = $"Failed to create device from configuration section '{section.Key}'. " +
|
||||
"This is a critical error and the application will stop.";
|
||||
_logger.LogError(ex, "{}", errorMessage);
|
||||
throw new InvalidOperationException(errorMessage, ex);
|
||||
}
|
||||
}
|
||||
|
||||
// Mark devices as loaded
|
||||
lock (_lock)
|
||||
{
|
||||
_devicesLoaded = true;
|
||||
}
|
||||
_devicesLoadedEvent.Set();
|
||||
DevicesLoaded?.Invoke(this, EventArgs.Empty);
|
||||
|
||||
// Initialize all devices first (synchronous, should be fast)
|
||||
if (createdDevices.Count > 0)
|
||||
{
|
||||
var initTasks = new List<Task>();
|
||||
foreach (var device in createdDevices)
|
||||
{
|
||||
initTasks.Add(InitializeDeviceAsync(device, cancellationToken));
|
||||
}
|
||||
|
||||
// Wait for all devices to initialize (with timeout)
|
||||
try
|
||||
{
|
||||
await Task.WhenAll(initTasks).WaitAsync(TimeSpan.FromSeconds(60), cancellationToken);
|
||||
}
|
||||
catch (TimeoutException)
|
||||
{
|
||||
_logger.LogWarning("Timeout waiting for all devices to initialize");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error during device initialization");
|
||||
}
|
||||
|
||||
// Connect all devices in background threads (non-blocking)
|
||||
var connectTasks = new List<Task>();
|
||||
foreach (var device in createdDevices)
|
||||
{
|
||||
// Fire and forget - connect in background thread
|
||||
var task = Task.Run(async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
await ConnectDeviceAsync(device, cancellationToken);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error connecting device {DeviceId} in background", device.DeviceId);
|
||||
}
|
||||
}, cancellationToken);
|
||||
connectTasks.Add(task);
|
||||
}
|
||||
|
||||
// Wait for all devices to connect in background (tracked for proper shutdown)
|
||||
_connectCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
|
||||
_connectMonitorTask = Task.Run(async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
await Task.WhenAll(connectTasks);
|
||||
|
||||
// Check if all devices are connected
|
||||
bool allConnected = true;
|
||||
lock (_lock)
|
||||
{
|
||||
foreach (var device in createdDevices)
|
||||
{
|
||||
if (device.Status != DeviceStatus.Connected)
|
||||
{
|
||||
allConnected = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (allConnected)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
_devicesConnected = true;
|
||||
}
|
||||
_devicesConnectedEvent.Set();
|
||||
DevicesConnected?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
_logger.LogWarning("Device connection monitoring cancelled");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error waiting for devices to connect");
|
||||
}
|
||||
}, _connectCts.Token);
|
||||
}
|
||||
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error starting DeviceProvider");
|
||||
throw;
|
||||
}
|
||||
|
||||
await Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Discover all device types that implement DeviceBase and have DeviceAttribute
|
||||
/// Key format: "DriverName:Version" or "DriverName" (if version is null)
|
||||
/// Scans all loaded assemblies, not just executing assembly
|
||||
/// </summary>
|
||||
private Dictionary<string, Type> DiscoverDeviceTypes()
|
||||
{
|
||||
var deviceTypes = new Dictionary<string, Type>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
// Scan all loaded assemblies, not just executing assembly
|
||||
var assemblies = AppDomain.CurrentDomain.GetAssemblies();
|
||||
|
||||
foreach (var assembly in assemblies)
|
||||
{
|
||||
try
|
||||
{
|
||||
foreach (var type in assembly.GetTypes())
|
||||
{
|
||||
// Check if type is a class, not abstract, and implements DeviceBase
|
||||
if (!type.IsClass || type.IsAbstract || !typeof(DeviceBase).IsAssignableFrom(type))
|
||||
continue;
|
||||
|
||||
// Check if type has DeviceAttribute
|
||||
var attribute = type.GetCustomAttribute<DeviceAttribute>();
|
||||
if (attribute == null)
|
||||
continue;
|
||||
|
||||
// Check if DriverName is provided
|
||||
if (string.IsNullOrWhiteSpace(attribute.DriverName))
|
||||
{
|
||||
_logger.LogWarning("Device type {TypeName} has DeviceAttribute but DriverName is empty, skipping", type.Name);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Create key with version if available
|
||||
var key = string.IsNullOrWhiteSpace(attribute.Version)
|
||||
? attribute.DriverName
|
||||
: $"{attribute.DriverName}:{attribute.Version}";
|
||||
|
||||
// Check for duplicate key
|
||||
if (deviceTypes.TryGetValue(key, out Type? value))
|
||||
{
|
||||
_logger.LogWarning("Duplicate device key '{Key}' found: {ExistingType} and {NewType}, using {ExistingType}",
|
||||
key, value.Name, type.Name, value.Name);
|
||||
continue;
|
||||
}
|
||||
|
||||
deviceTypes[key] = type;
|
||||
}
|
||||
}
|
||||
catch (ReflectionTypeLoadException ex)
|
||||
{
|
||||
// Some assemblies may fail to load types (e.g., native dependencies)
|
||||
_logger.LogWarning("Failed to load types from assembly {AssemblyName}: {Message}",
|
||||
assembly.FullName, ex.Message);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Ignore other exceptions during type discovery
|
||||
_logger.LogError("Error scanning assembly {AssemblyName}: {Message}",
|
||||
assembly.FullName, ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
return deviceTypes;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tìm device type theo driverName và version
|
||||
/// </summary>
|
||||
private static Type? FindDeviceType(Dictionary<string, Type> deviceTypes, string driverName, string? version)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(driverName))
|
||||
return null;
|
||||
|
||||
// Try to find with version first
|
||||
if (!string.IsNullOrWhiteSpace(version))
|
||||
{
|
||||
var keyWithVersion = $"{driverName}:{version}";
|
||||
if (deviceTypes.TryGetValue(keyWithVersion, out var typeWithVersion))
|
||||
{
|
||||
return typeWithVersion;
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to driverName only (without version)
|
||||
if (deviceTypes.TryGetValue(driverName, out var type))
|
||||
{
|
||||
return type;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tìm constructor phù hợp với tham số: deviceId (string), deviceName (string), IConfigurationSection, và optional IServiceProvider
|
||||
/// </summary>
|
||||
private static ConstructorInfo? FindMatchingConstructor(Type deviceType)
|
||||
{
|
||||
var constructors = deviceType.GetConstructors(BindingFlags.Public | BindingFlags.Instance);
|
||||
|
||||
foreach (var constructor in constructors)
|
||||
{
|
||||
var parameters = constructor.GetParameters();
|
||||
|
||||
// Check if constructor has 3 parameters: string, string, IConfigurationSection
|
||||
if (parameters.Length == 3)
|
||||
{
|
||||
var param1 = parameters[0];
|
||||
var param2 = parameters[1];
|
||||
var param3 = parameters[2];
|
||||
|
||||
if (param1.ParameterType == typeof(string) &&
|
||||
param2.ParameterType == typeof(string) &&
|
||||
param3.ParameterType == typeof(IConfigurationSection))
|
||||
{
|
||||
return constructor;
|
||||
}
|
||||
}
|
||||
|
||||
// Check if constructor has 4 parameters: string, string, IConfigurationSection, IServiceProvider
|
||||
if (parameters.Length == 4)
|
||||
{
|
||||
var param1 = parameters[0];
|
||||
var param2 = parameters[1];
|
||||
var param3 = parameters[2];
|
||||
var param4 = parameters[3];
|
||||
|
||||
if (param1.ParameterType == typeof(string) &&
|
||||
param2.ParameterType == typeof(string) &&
|
||||
param3.ParameterType == typeof(IConfigurationSection) &&
|
||||
param4.ParameterType == typeof(IServiceProvider))
|
||||
{
|
||||
return constructor;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tạo device từ configuration section
|
||||
/// </summary>
|
||||
private DeviceBase? CreateDeviceFromConfigurationSection(IConfigurationSection section, Dictionary<string, Type> deviceTypes)
|
||||
{
|
||||
// Đọc các thông tin từ section
|
||||
var deviceId = section["DeviceId"];
|
||||
var deviceName = section["DeviceName"];
|
||||
var driverName = section["DriverName"];
|
||||
var driverVersion = section["DriverVersion"];
|
||||
var connectionSection = section.GetSection("Connection");
|
||||
|
||||
// Validate required fields
|
||||
if (string.IsNullOrWhiteSpace(deviceId))
|
||||
{
|
||||
_logger.LogWarning("Configuration section {SectionKey} missing DeviceId, skipping", section.Key);
|
||||
return null;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(deviceName))
|
||||
{
|
||||
_logger.LogWarning("Configuration section {SectionKey} missing DeviceName, skipping", section.Key);
|
||||
return null;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(driverName))
|
||||
{
|
||||
_logger.LogWarning("Configuration section {SectionKey} missing DriverName, skipping", section.Key);
|
||||
return null;
|
||||
}
|
||||
|
||||
// Tìm device type theo driverName và version
|
||||
var deviceType = FindDeviceType(deviceTypes, driverName, driverVersion);
|
||||
if (deviceType == null)
|
||||
{
|
||||
var errorMessage = $"Device type not found for DriverName: '{driverName}' (Version: '{driverVersion ?? "null"}', DeviceId: '{deviceId}'). " +
|
||||
$"Please check that the driver class exists and has [Device] attribute with matching DriverName.";
|
||||
_logger.LogError("{}", errorMessage);
|
||||
throw new InvalidOperationException(errorMessage);
|
||||
}
|
||||
|
||||
// Kiểm tra DeviceType từ configuration có khớp với DeviceAttribute.DeviceType không
|
||||
var configDeviceTypeStr = section["DeviceType"];
|
||||
if (!string.IsNullOrWhiteSpace(configDeviceTypeStr))
|
||||
{
|
||||
if (Enum.TryParse<DeviceType>(configDeviceTypeStr, ignoreCase: true, out var configDeviceType))
|
||||
{
|
||||
var attribute = deviceType.GetCustomAttribute<DeviceAttribute>();
|
||||
if (attribute != null && attribute.DeviceType != configDeviceType)
|
||||
{
|
||||
_logger.LogWarning("DeviceType mismatch for device {DeviceId}: " +
|
||||
"Configuration specifies {ConfigDeviceType} but DeviceAttribute has {AttributeDeviceType}. Skipping.",
|
||||
deviceId, configDeviceType, attribute.DeviceType);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogWarning("Invalid DeviceType value '{DeviceType}' in configuration section {SectionKey} for device {DeviceId}. Skipping.",
|
||||
configDeviceTypeStr, section.Key, deviceId);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Tìm constructor phù hợp
|
||||
var constructor = FindMatchingConstructor(deviceType);
|
||||
if (constructor == null)
|
||||
{
|
||||
var errorMessage = $"No matching constructor found for device type '{deviceType.Name}' (DeviceId: '{deviceId}'). " +
|
||||
"Expected constructor with parameters: (string deviceId, string deviceName, IConfigurationSection connection) " +
|
||||
"or (string deviceId, string deviceName, IConfigurationSection connection, IServiceProvider serviceProvider). " +
|
||||
"Please check the driver class constructor signature.";
|
||||
_logger.LogError("{}", errorMessage);
|
||||
throw new InvalidOperationException(errorMessage);
|
||||
}
|
||||
|
||||
// Tạo device instance
|
||||
try
|
||||
{
|
||||
var parameters = constructor.GetParameters();
|
||||
object[] constructorArgs;
|
||||
|
||||
if (parameters.Length == 3)
|
||||
{
|
||||
// Constructor without IServiceProvider
|
||||
constructorArgs =
|
||||
[
|
||||
deviceId,
|
||||
deviceName,
|
||||
connectionSection
|
||||
];
|
||||
}
|
||||
else
|
||||
{
|
||||
// Constructor with IServiceProvider
|
||||
constructorArgs =
|
||||
[
|
||||
deviceId,
|
||||
deviceName,
|
||||
connectionSection,
|
||||
_serviceProvider
|
||||
];
|
||||
}
|
||||
|
||||
var device = (DeviceBase)constructor.Invoke(constructorArgs);
|
||||
|
||||
return device;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
var errorMessage = $"Failed to create device instance: DeviceId='{deviceId}', TypeName='{deviceType.Name}'. " +
|
||||
"Please check the driver class constructor and configuration.";
|
||||
_logger.LogError(ex, "{}", errorMessage);
|
||||
throw new InvalidOperationException(errorMessage, ex);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Cancel and wait for connection monitor task to finish
|
||||
if (_connectCts != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
_connectCts.Cancel();
|
||||
}
|
||||
catch (ObjectDisposedException)
|
||||
{
|
||||
_logger.LogError("DeviceProvider: Connection CTS already disposed");
|
||||
// Already disposed, ignore
|
||||
}
|
||||
}
|
||||
|
||||
if (_connectMonitorTask != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
await _connectMonitorTask.WaitAsync(TimeSpan.FromSeconds(5), cancellationToken);
|
||||
}
|
||||
catch (TimeoutException)
|
||||
{
|
||||
_logger.LogWarning("DeviceProvider: Timeout waiting for connection monitor task to finish");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "DeviceProvider: Error waiting for connection monitor task");
|
||||
}
|
||||
}
|
||||
|
||||
// Dispose connection CTS
|
||||
_connectCts?.Dispose();
|
||||
_connectCts = null;
|
||||
_connectMonitorTask = null;
|
||||
|
||||
// Get all devices and disconnect them first
|
||||
List<DeviceBase> devicesToStop;
|
||||
lock (_lock)
|
||||
{
|
||||
devicesToStop = [.. _devicesById.Values];
|
||||
}
|
||||
|
||||
if (devicesToStop.Count > 0)
|
||||
{
|
||||
// Disconnect all devices in parallel
|
||||
var disconnectTasks = new List<Task>();
|
||||
foreach (var device in devicesToStop)
|
||||
{
|
||||
disconnectTasks.Add(DisconnectDeviceAsync(device, cancellationToken));
|
||||
}
|
||||
|
||||
// Wait for all devices to disconnect (with timeout)
|
||||
try
|
||||
{
|
||||
await Task.WhenAll(disconnectTasks);
|
||||
}
|
||||
catch (TimeoutException)
|
||||
{
|
||||
_logger.LogWarning("DeviceProvider: Timeout waiting for all devices to disconnect");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "DeviceProvider: Error during device disconnection");
|
||||
}
|
||||
}
|
||||
|
||||
// Dispose all devices
|
||||
lock (_lock)
|
||||
{
|
||||
foreach (var device in devicesToStop)
|
||||
{
|
||||
try
|
||||
{
|
||||
device.Dispose();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "DeviceProvider: Error disposing device: {DeviceId}", device.DeviceId);
|
||||
}
|
||||
}
|
||||
|
||||
_devicesById.Clear();
|
||||
_devicesByType.Clear();
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "DeviceProvider: Error during StopAsync()");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initialize a device
|
||||
/// </summary>
|
||||
private async Task InitializeDeviceAsync(DeviceBase device, CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
await device.InitializeAsync(cancellationToken);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to initialize device: {DeviceId}", device.DeviceId);
|
||||
throw; // Re-throw để caller biết có lỗi
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Connect a device (runs in background thread)
|
||||
/// </summary>
|
||||
private async Task ConnectDeviceAsync(DeviceBase device, CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
await device.ConnectAsync(cancellationToken);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to connect device: {DeviceId}", device.DeviceId);
|
||||
// Don't throw - allow other devices to continue
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Disconnect a device
|
||||
/// </summary>
|
||||
private async Task DisconnectDeviceAsync(DeviceBase device, CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (device.Status == DeviceStatus.Connected || device.Status == DeviceStatus.Connecting)
|
||||
{
|
||||
await device.DisconnectAsync(cancellationToken);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to disconnect device: {DeviceId}", device.DeviceId);
|
||||
// Don't throw - allow other devices to continue
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private void RegisterDeviceInternal(DeviceBase device)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(device);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(device.DeviceId))
|
||||
throw new ArgumentException("Device.DeviceId cannot be null or empty", nameof(device));
|
||||
|
||||
lock (_lock)
|
||||
{
|
||||
// Kiểm tra deviceId đã tồn tại chưa
|
||||
if (_devicesById.ContainsKey(device.DeviceId))
|
||||
{
|
||||
_logger.LogWarning("Device with ID {DeviceId} already exists, skipping registration", device.DeviceId);
|
||||
return;
|
||||
}
|
||||
|
||||
// Đăng ký vào dictionary theo ID
|
||||
_devicesById[device.DeviceId] = device;
|
||||
|
||||
// Đăng ký vào dictionary theo Type
|
||||
if (!_devicesByType.TryGetValue(device.Type, out var devicesByType))
|
||||
{
|
||||
devicesByType = [];
|
||||
_devicesByType[device.Type] = devicesByType;
|
||||
}
|
||||
devicesByType.Add(device);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handler for device status changes - updates _devicesConnected when devices disconnect/reconnect
|
||||
/// </summary>
|
||||
private void OnDeviceStatusChanged(object? sender, DeviceStatusChangedEventArgs e)
|
||||
{
|
||||
if (sender is not DeviceBase device)
|
||||
return;
|
||||
|
||||
lock (_lock)
|
||||
{
|
||||
// If a device disconnected and we previously had all devices connected, reset the flag
|
||||
if (_devicesConnected && e.CurrentStatus != DeviceStatus.Connected)
|
||||
{
|
||||
_devicesConnected = false;
|
||||
_devicesConnectedEvent.Reset();
|
||||
}
|
||||
// If a device connected, check if all devices are now connected
|
||||
else if (!_devicesConnected && e.CurrentStatus == DeviceStatus.Connected)
|
||||
{
|
||||
// Check if all devices are now connected
|
||||
bool allConnected = true;
|
||||
foreach (var d in _devicesById.Values)
|
||||
{
|
||||
if (d.Status != DeviceStatus.Connected)
|
||||
{
|
||||
allConnected = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (allConnected)
|
||||
{
|
||||
_devicesConnected = true;
|
||||
_devicesConnectedEvent.Set();
|
||||
DevicesConnected?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public DeviceBase? GetDevice(string deviceId)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(deviceId))
|
||||
return null;
|
||||
|
||||
lock (_lock)
|
||||
{
|
||||
return _devicesById.TryGetValue(deviceId, out var device) ? device : null;
|
||||
}
|
||||
}
|
||||
|
||||
public Task<DeviceBase?> GetDeviceAsync(string deviceId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return Task.FromResult(GetDevice(deviceId));
|
||||
}
|
||||
|
||||
public DeviceBase? GetDeviceByType(DeviceType deviceType)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
if (_devicesByType.TryGetValue(deviceType, out var devices) && devices.Count > 0)
|
||||
{
|
||||
return devices[0];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public Task<DeviceBase?> GetDeviceByTypeAsync(DeviceType deviceType, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return Task.FromResult(GetDeviceByType(deviceType));
|
||||
}
|
||||
|
||||
public IReadOnlyList<DeviceBase> GetDevicesByType(DeviceType deviceType)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
if (_devicesByType.TryGetValue(deviceType, out var devices))
|
||||
{
|
||||
return devices.ToList().AsReadOnly();
|
||||
}
|
||||
return Array.Empty<DeviceBase>().ToList().AsReadOnly();
|
||||
}
|
||||
}
|
||||
|
||||
public Task<IReadOnlyList<DeviceBase>> GetDevicesByTypeAsync(DeviceType deviceType, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return Task.FromResult(GetDevicesByType(deviceType));
|
||||
}
|
||||
|
||||
public IReadOnlyList<DeviceBase> GetAllDevices()
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
return _devicesById.Values.ToList().AsReadOnly();
|
||||
}
|
||||
}
|
||||
|
||||
public Task<IReadOnlyList<DeviceBase>> GetAllDevicesAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
return Task.FromResult(GetAllDevices());
|
||||
}
|
||||
|
||||
public bool ContainsDevice(string deviceId)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(deviceId))
|
||||
return false;
|
||||
|
||||
lock (_lock)
|
||||
{
|
||||
return _devicesById.ContainsKey(deviceId);
|
||||
}
|
||||
}
|
||||
|
||||
public int GetDeviceCount()
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
return _devicesById.Count;
|
||||
}
|
||||
}
|
||||
|
||||
public int GetDeviceCountByType(DeviceType deviceType)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
if (_devicesByType.TryGetValue(deviceType, out var devices))
|
||||
{
|
||||
return devices.Count;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
public bool AreDevicesLoaded
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
return _devicesLoaded;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool AreDevicesConnected
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
if (!_devicesLoaded)
|
||||
return false;
|
||||
|
||||
// Use cached _devicesConnected value, but verify if it's true
|
||||
// If _devicesConnected is false, we know for sure not all are connected
|
||||
if (!_devicesConnected)
|
||||
return false;
|
||||
|
||||
// If _devicesConnected is true, verify all devices are still connected
|
||||
// (in case a device disconnected after initial connection)
|
||||
foreach (var device in _devicesById.Values)
|
||||
{
|
||||
if (device.Status != DeviceStatus.Connected)
|
||||
{
|
||||
// Update cached value if we find a disconnected device
|
||||
_devicesConnected = false;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<bool> WaitForDevicesLoadedAsync(TimeSpan timeout, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (AreDevicesLoaded)
|
||||
return true;
|
||||
|
||||
try
|
||||
{
|
||||
return await Task.Run(() => _devicesLoadedEvent.Wait(timeout, cancellationToken), cancellationToken);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<bool> WaitForDevicesConnectedAsync(TimeSpan timeout, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (AreDevicesConnected)
|
||||
return true;
|
||||
|
||||
// First wait for devices to be loaded
|
||||
if (!await WaitForDevicesLoadedAsync(timeout, cancellationToken))
|
||||
return false;
|
||||
|
||||
try
|
||||
{
|
||||
// Then wait for devices to be connected
|
||||
return await Task.Run(() => _devicesConnectedEvent.Wait(timeout, cancellationToken), cancellationToken);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public event EventHandler? DevicesLoaded;
|
||||
public event EventHandler? DevicesConnected;
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
using RobotNet10.RobotApp.Client.Shared.Devices;
|
||||
|
||||
namespace RobotNet10.RobotApp.Devices;
|
||||
|
||||
/// <summary>
|
||||
/// Event args khi trạng thái device thay đổi
|
||||
/// </summary>
|
||||
public class DeviceStatusChangedEventArgs : EventArgs
|
||||
{
|
||||
public DeviceStatus PreviousStatus { get; }
|
||||
public DeviceStatus CurrentStatus { get; }
|
||||
public string? Message { get; }
|
||||
public DateTime Timestamp { get; }
|
||||
|
||||
public DeviceStatusChangedEventArgs(
|
||||
DeviceStatus previousStatus,
|
||||
DeviceStatus currentStatus,
|
||||
string? message = null)
|
||||
{
|
||||
PreviousStatus = previousStatus;
|
||||
CurrentStatus = currentStatus;
|
||||
Message = message;
|
||||
Timestamp = DateTime.UtcNow;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
namespace RobotNet10.RobotApp.Devices;
|
||||
|
||||
/// <summary>
|
||||
/// Triggers cho state machine của Device
|
||||
/// </summary>
|
||||
public enum DeviceTrigger
|
||||
{
|
||||
/// <summary>
|
||||
/// Khởi tạo thiết bị
|
||||
/// </summary>
|
||||
Initialize,
|
||||
|
||||
/// <summary>
|
||||
/// Kết nối với thiết bị
|
||||
/// </summary>
|
||||
Connect,
|
||||
|
||||
/// <summary>
|
||||
/// Ngắt kết nối với thiết bị
|
||||
/// </summary>
|
||||
Disconnect,
|
||||
|
||||
/// <summary>
|
||||
/// Reset thiết bị
|
||||
/// </summary>
|
||||
Reset,
|
||||
|
||||
/// <summary>
|
||||
/// Bắt đầu auto-reconnect
|
||||
/// </summary>
|
||||
StartReconnect,
|
||||
|
||||
/// <summary>
|
||||
/// Dừng auto-reconnect
|
||||
/// </summary>
|
||||
StopReconnect,
|
||||
|
||||
/// <summary>
|
||||
/// Internal triggers - tự động fire khi operation hoàn thành
|
||||
/// </summary>
|
||||
InitializationCompleted,
|
||||
ConnectionCompleted,
|
||||
DisconnectionCompleted,
|
||||
ReconnectionCompleted,
|
||||
ResetCompleted,
|
||||
|
||||
/// <summary>
|
||||
/// Xảy ra lỗi
|
||||
/// </summary>
|
||||
ErrorOccurred,
|
||||
|
||||
/// <summary>
|
||||
/// Dispose thiết bị
|
||||
/// </summary>
|
||||
Dispose
|
||||
}
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
using RobotNet10.Shared.Sensor;
|
||||
|
||||
namespace RobotNet10.RobotApp.Devices;
|
||||
|
||||
/// <summary>
|
||||
/// Interface cho thiết bị Battery (Battery Management System) trong robot AGV
|
||||
/// Cung cấp thông tin battery theo chuẩn ROS sensor_msgs/BatteryState
|
||||
/// </summary>
|
||||
public interface IBattery
|
||||
{
|
||||
/// <summary>
|
||||
/// Battery state hiện tại được cache (thread-safe)
|
||||
/// </summary>
|
||||
BatteryState? CurrentBatteryState { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Đọc battery state từ thiết bị
|
||||
/// </summary>
|
||||
Task<BatteryState> ReadBatteryStateAsync(CancellationToken cancellationToken = default);
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
using RobotNet10.Shared.Geometry;
|
||||
|
||||
namespace RobotNet10.RobotApp.Devices;
|
||||
|
||||
/// <summary>
|
||||
/// Interface cho thiết bị Camera QR Code Detection
|
||||
/// </summary>
|
||||
public interface ICameraQr
|
||||
{
|
||||
bool IsConnected { get; }
|
||||
PoseStamped? this[string code] { get; }
|
||||
Dictionary<string, PoseStamped> Codes { get; }
|
||||
}
|
||||
@@ -0,0 +1,329 @@
|
||||
using RobotNet10.CANOpen.CiA402;
|
||||
using RobotNet10.CANOpen.CiA402.Enums;
|
||||
using RobotNet10.CANOpen.CiA402.Models;
|
||||
|
||||
namespace RobotNet10.RobotApp.Devices;
|
||||
|
||||
/// <summary>
|
||||
/// Interface cho CiA402 Servo - cung cấp tất cả chức năng điều khiển động cơ servo theo chuẩn CiA402
|
||||
/// Tập trung vào phần điều khiển động cơ, không bao gồm các chức năng liên quan đến CAN/PDO
|
||||
/// </summary>
|
||||
public interface ICiA402Servo
|
||||
{
|
||||
// Cached values (thread-safe, read-only)
|
||||
/// <summary>
|
||||
/// Statusword hiện tại (real-time, thread-safe)
|
||||
/// </summary>
|
||||
Statusword CachedStatusword { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Actual position hiện tại (thread-safe)
|
||||
/// </summary>
|
||||
int CachedPosition { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Actual velocity hiện tại (thread-safe)
|
||||
/// </summary>
|
||||
int CachedVelocity { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Actual torque hiện tại (thread-safe)
|
||||
/// </summary>
|
||||
short CachedTorque { get; }
|
||||
|
||||
// Events
|
||||
/// <summary>
|
||||
/// Event được kích hoạt khi Statusword thay đổi
|
||||
/// </summary>
|
||||
event EventHandler<StatuswordChangedEventArgs>? StatuswordChanged;
|
||||
|
||||
/// <summary>
|
||||
/// Event được kích hoạt khi Position thay đổi
|
||||
/// </summary>
|
||||
event EventHandler<PositionChangedEventArgs>? PositionChanged;
|
||||
|
||||
/// <summary>
|
||||
/// Event được kích hoạt khi Velocity thay đổi
|
||||
/// </summary>
|
||||
event EventHandler<VelocityChangedEventArgs>? VelocityChanged;
|
||||
|
||||
// Statusword & Controlword
|
||||
/// <summary>
|
||||
/// Get statusword hiện tại
|
||||
/// </summary>
|
||||
Task<Statusword> GetStatuswordAsync(CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Set controlword để điều khiển motor
|
||||
/// </summary>
|
||||
Task SetControlwordAsync(Controlword controlword, CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Get drive state từ statusword
|
||||
/// </summary>
|
||||
Task<DriveState> GetStateAsync(CancellationToken ct = default);
|
||||
|
||||
// Operation Mode
|
||||
/// <summary>
|
||||
/// Set operation mode (Profile Position, Profile Velocity, Profile Torque, etc.)
|
||||
/// </summary>
|
||||
Task SetOperationModeAsync(OperationMode mode, CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Get operation mode hiện tại
|
||||
/// </summary>
|
||||
Task<OperationMode> GetOperationModeAsync(CancellationToken ct = default);
|
||||
|
||||
// State Machine Control
|
||||
/// <summary>
|
||||
/// Reset fault trên motor
|
||||
/// </summary>
|
||||
Task FaultResetAsync(CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Shutdown command
|
||||
/// </summary>
|
||||
Task ShutdownAsync(CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Switch on command
|
||||
/// </summary>
|
||||
Task SwitchOnAsync(CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Enable operation command
|
||||
/// </summary>
|
||||
Task EnableOperationAsync(CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Disable operation command
|
||||
/// </summary>
|
||||
Task DisableOperationAsync(CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Quick stop command
|
||||
/// </summary>
|
||||
Task QuickStopAsync(CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Enable motor với automatic state transitions
|
||||
/// </summary>
|
||||
Task EnableAsync(CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Disable motor
|
||||
/// </summary>
|
||||
Task DisableAsync(CancellationToken ct = default);
|
||||
|
||||
// Position Control
|
||||
/// <summary>
|
||||
/// Get actual position hiện tại
|
||||
/// </summary>
|
||||
Task<int> GetActualPositionAsync(CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Set target position
|
||||
/// </summary>
|
||||
Task SetTargetPositionAsync(int position, CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Set profile speed
|
||||
/// </summary>
|
||||
Task SetProfileSpeedAsync(uint velocity, CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Set profile velocity
|
||||
/// </summary>
|
||||
Task SetProfileVelocityAsync(uint velocity, CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Set profile acceleration
|
||||
/// </summary>
|
||||
Task SetProfileAccelerationAsync(uint acceleration, CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Set profile deceleration
|
||||
/// </summary>
|
||||
Task SetProfileDecelerationAsync(uint deceleration, CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Get profile speed (0x6081) từ drive
|
||||
/// </summary>
|
||||
Task<uint> GetProfileSpeedAsync(CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Get profile acceleration (0x6083) từ drive
|
||||
/// </summary>
|
||||
Task<uint> GetProfileAccelerationAsync(CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Get profile deceleration (0x6084) từ drive
|
||||
/// </summary>
|
||||
Task<uint> GetProfileDecelerationAsync(CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Start position move (set new setpoint bit)
|
||||
/// </summary>
|
||||
Task StartPositionMoveAsync(CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Move to position với Profile Position mode
|
||||
/// </summary>
|
||||
Task MoveToPositionAsync(int position, uint velocity = 1000, uint acceleration = 5000, uint deceleration = 5000, CancellationToken ct = default);
|
||||
|
||||
// Velocity Control
|
||||
/// <summary>
|
||||
/// Get actual velocity hiện tại
|
||||
/// </summary>
|
||||
Task<int> GetActualVelocityAsync(CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Set target velocity
|
||||
/// </summary>
|
||||
Task SetTargetVelocityAsync(int velocity, CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Target velocity với Profile Velocity mode
|
||||
/// </summary>
|
||||
Task TargetVelocityAsync(int targetVelocity, uint acceleration = 5000, uint deceleration = 5000, CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Profile velocity với Profile Velocity mode
|
||||
/// </summary>
|
||||
Task ProfileVelocityAsync(int targetVelocity, uint acceleration = 5000, uint deceleration = 5000, CancellationToken ct = default);
|
||||
|
||||
// Torque Control
|
||||
/// <summary>
|
||||
/// Get actual torque hiện tại
|
||||
/// </summary>
|
||||
Task<short> GetActualTorqueAsync(CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Set target torque
|
||||
/// </summary>
|
||||
Task SetTargetTorqueAsync(short torque, CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Run torque với Profile Torque mode
|
||||
/// </summary>
|
||||
Task RunTorqueAsync(short torque, CancellationToken ct = default);
|
||||
|
||||
// Homing
|
||||
/// <summary>
|
||||
/// Set homing method
|
||||
/// </summary>
|
||||
Task SetHomingMethodAsync(byte method, CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Set homing speed (speed during search for switch, sub-index 1 of 0x6099).
|
||||
/// </summary>
|
||||
Task SetHomingSpeedAsync(int speed, CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Set homing offset (0x607C). Offset applied after homing is complete.
|
||||
/// </summary>
|
||||
Task SetHomingOffsetAsync(int offset, CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Đọc lại homing method từ drive (0x6098) để kiểm tra đã ghi xuống chưa.
|
||||
/// </summary>
|
||||
Task<byte> GetHomingMethodAsync(CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Đọc lại homing speed từ drive (0x6099 sub-index 1) để kiểm tra đã ghi xuống chưa.
|
||||
/// </summary>
|
||||
Task<int> GetHomingSpeedAsync(CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Đọc lại homing offset từ drive (0x607C) để kiểm tra đã ghi xuống chưa.
|
||||
/// </summary>
|
||||
Task<int> GetHomingOffsetAsync(CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Start homing procedure
|
||||
/// </summary>
|
||||
Task StartHomingAsync(byte method, int speed, CancellationToken ct = default);
|
||||
|
||||
// Target Position Checking
|
||||
/// <summary>
|
||||
/// Kiểm tra xem motor đã đến vị trí target chưa
|
||||
/// </summary>
|
||||
/// <param name="tolerance">Tolerance cho position comparison (encoder counts). Chỉ dùng nếu không có TargetReached bit.</param>
|
||||
/// <param name="useStatusword">True để dùng Statusword.TargetReached bit (recommended), False để so sánh position</param>
|
||||
bool IsAtTarget(int tolerance = 100, bool useStatusword = true);
|
||||
|
||||
/// <summary>
|
||||
/// Đợi cho đến khi motor đến vị trí target
|
||||
/// </summary>
|
||||
Task WaitUntilAtTargetAsync(int tolerance = 100, bool useStatusword = true, int checkIntervalMs = 10, CancellationToken ct = default);
|
||||
|
||||
// Error and Status Information
|
||||
/// <summary>
|
||||
/// Kiểm tra xem motor có đang ở trạng thái Fault không
|
||||
/// </summary>
|
||||
Task<bool> IsInFaultStateAsync(CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Đọc Error Register từ device
|
||||
/// </summary>
|
||||
Task<byte> GetErrorRegisterAsync(CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Đọc Pre-defined Error Field từ device (danh sách các error codes gần đây)
|
||||
/// </summary>
|
||||
Task<ushort[]> GetErrorHistoryAsync(CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Đọc error code mới nhất từ Error History
|
||||
/// </summary>
|
||||
Task<ushort> GetLatestErrorCodeAsync(CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Reset fault trên motor (nếu motor đang ở trạng thái Fault)
|
||||
/// </summary>
|
||||
Task<bool> TryFaultResetAsync(CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Kiểm tra xem motor có đang enabled (Operation Enabled state) không
|
||||
/// </summary>
|
||||
Task<bool> IsEnabledAsync(CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Kiểm tra xem motor có đang ready (Ready to Switch On hoặc Switched On) không
|
||||
/// </summary>
|
||||
Task<bool> IsReadyAsync(CancellationToken ct = default);
|
||||
}
|
||||
|
||||
#region Event Args
|
||||
|
||||
/// <summary>
|
||||
/// Event args cho StatuswordChanged event
|
||||
/// </summary>
|
||||
public class StatuswordChangedEventArgs(Statusword statusword, DriveState oldState, DriveState newState) : EventArgs
|
||||
{
|
||||
public Statusword Statusword { get; } = statusword;
|
||||
public DriveState OldState { get; } = oldState;
|
||||
public DriveState NewState { get; } = newState;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Event args cho PositionChanged event
|
||||
/// </summary>
|
||||
public class PositionChangedEventArgs(int position, int oldPosition) : EventArgs
|
||||
{
|
||||
public int Position { get; } = position;
|
||||
public int OldPosition { get; } = oldPosition;
|
||||
public int Delta => Position - OldPosition;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Event args cho VelocityChanged event
|
||||
/// </summary>
|
||||
public class VelocityChangedEventArgs(int velocity, int oldVelocity) : EventArgs
|
||||
{
|
||||
public int Velocity { get; } = velocity;
|
||||
public int OldVelocity { get; } = oldVelocity;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
using RobotNet10.RobotApp.Client.Shared.Devices;
|
||||
|
||||
namespace RobotNet10.RobotApp.Devices;
|
||||
|
||||
/// <summary>
|
||||
/// Interface cung cấp các chức năng để quản lý và truy xuất devices
|
||||
/// </summary>
|
||||
public interface IDeviceProvider
|
||||
{
|
||||
/// <summary>
|
||||
/// Lấy device theo deviceId
|
||||
/// </summary>
|
||||
/// <param name="deviceId">Định danh duy nhất của device</param>
|
||||
/// <returns>Device nếu tìm thấy, null nếu không tìm thấy</returns>
|
||||
DeviceBase? GetDevice(string deviceId);
|
||||
|
||||
/// <summary>
|
||||
/// Lấy device theo deviceId (async)
|
||||
/// </summary>
|
||||
/// <param name="deviceId">Định danh duy nhất của device</param>
|
||||
/// <param name="cancellationToken">Cancellation token</param>
|
||||
/// <returns>Device nếu tìm thấy, null nếu không tìm thấy</returns>
|
||||
Task<DeviceBase?> GetDeviceAsync(string deviceId, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Lấy device theo deviceType (lấy device đầu tiên tìm thấy)
|
||||
/// </summary>
|
||||
/// <param name="deviceType">Loại thiết bị</param>
|
||||
/// <returns>Device nếu tìm thấy, null nếu không tìm thấy</returns>
|
||||
DeviceBase? GetDeviceByType(DeviceType deviceType);
|
||||
|
||||
/// <summary>
|
||||
/// Lấy device theo deviceType (async)
|
||||
/// </summary>
|
||||
/// <param name="deviceType">Loại thiết bị</param>
|
||||
/// <param name="cancellationToken">Cancellation token</param>
|
||||
/// <returns>Device nếu tìm thấy, null nếu không tìm thấy</returns>
|
||||
Task<DeviceBase?> GetDeviceByTypeAsync(DeviceType deviceType, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Lấy tất cả devices theo deviceType
|
||||
/// </summary>
|
||||
/// <param name="deviceType">Loại thiết bị</param>
|
||||
/// <returns>Danh sách devices của loại được chỉ định</returns>
|
||||
IReadOnlyList<DeviceBase> GetDevicesByType(DeviceType deviceType);
|
||||
|
||||
/// <summary>
|
||||
/// Lấy tất cả devices theo deviceType (async)
|
||||
/// </summary>
|
||||
/// <param name="deviceType">Loại thiết bị</param>
|
||||
/// <param name="cancellationToken">Cancellation token</param>
|
||||
/// <returns>Danh sách devices của loại được chỉ định</returns>
|
||||
Task<IReadOnlyList<DeviceBase>> GetDevicesByTypeAsync(DeviceType deviceType, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Lấy tất cả devices
|
||||
/// </summary>
|
||||
/// <returns>Danh sách tất cả devices</returns>
|
||||
IReadOnlyList<DeviceBase> GetAllDevices();
|
||||
|
||||
/// <summary>
|
||||
/// Lấy tất cả devices (async)
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">Cancellation token</param>
|
||||
/// <returns>Danh sách tất cả devices</returns>
|
||||
Task<IReadOnlyList<DeviceBase>> GetAllDevicesAsync(CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Kiểm tra device có tồn tại không
|
||||
/// </summary>
|
||||
/// <param name="deviceId">Định danh của device</param>
|
||||
/// <returns>True nếu device tồn tại, false nếu không</returns>
|
||||
bool ContainsDevice(string deviceId);
|
||||
|
||||
/// <summary>
|
||||
/// Lấy số lượng devices
|
||||
/// </summary>
|
||||
/// <returns>Số lượng devices đã đăng ký</returns>
|
||||
int GetDeviceCount();
|
||||
|
||||
/// <summary>
|
||||
/// Lấy số lượng devices theo deviceType
|
||||
/// </summary>
|
||||
/// <param name="deviceType">Loại thiết bị</param>
|
||||
/// <returns>Số lượng devices của loại được chỉ định</returns>
|
||||
int GetDeviceCountByType(DeviceType deviceType);
|
||||
|
||||
/// <summary>
|
||||
/// Kiểm tra xem tất cả devices đã được load chưa
|
||||
/// </summary>
|
||||
bool AreDevicesLoaded { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Kiểm tra xem tất cả devices đã kết nối chưa
|
||||
/// </summary>
|
||||
bool AreDevicesConnected { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Đợi cho đến khi tất cả devices đã được load
|
||||
/// </summary>
|
||||
/// <param name="timeout">Timeout để đợi</param>
|
||||
/// <param name="cancellationToken">Cancellation token</param>
|
||||
/// <returns>True nếu devices đã load, false nếu timeout</returns>
|
||||
Task<bool> WaitForDevicesLoadedAsync(TimeSpan timeout, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Đợi cho đến khi tất cả devices đã kết nối
|
||||
/// </summary>
|
||||
/// <param name="timeout">Timeout để đợi</param>
|
||||
/// <param name="cancellationToken">Cancellation token</param>
|
||||
/// <returns>True nếu devices đã connect, false nếu timeout</returns>
|
||||
Task<bool> WaitForDevicesConnectedAsync(TimeSpan timeout, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Event được kích hoạt khi tất cả devices đã được load
|
||||
/// </summary>
|
||||
event EventHandler? DevicesLoaded;
|
||||
|
||||
/// <summary>
|
||||
/// Event được kích hoạt khi tất cả devices đã kết nối
|
||||
/// </summary>
|
||||
event EventHandler? DevicesConnected;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,223 @@
|
||||
using RobotNet10.Shared.Geometry;
|
||||
using RobotNet10.Shared.Sensor;
|
||||
|
||||
namespace RobotNet10.RobotApp.Devices;
|
||||
|
||||
// Type alias để giữ tương thích với code hiện tại
|
||||
using AccelerationData = AccelStamped;
|
||||
|
||||
/// <summary>
|
||||
/// Interface cho thiết bị IMU (Inertial Measurement Unit) trong robot AGV
|
||||
/// </summary>
|
||||
public interface IInertialMeasurementUnit
|
||||
{
|
||||
/// <summary>
|
||||
/// Trạng thái kết nối với thiết bị IMU
|
||||
/// </summary>
|
||||
bool IsConnected { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Trạng thái đã được calibrate hay chưa
|
||||
/// </summary>
|
||||
bool IsCalibrated { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Tần số lấy mẫu hiện tại (Hz)
|
||||
/// </summary>
|
||||
double SampleRate { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Dữ liệu gia tốc được cache (m/s²)
|
||||
/// </summary>
|
||||
AccelStamped CachedAcceleration { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Dữ liệu vận tốc góc được cache (rad/s)
|
||||
/// Roll, Pitch, Yaw được map sang X, Y, Z của Vector3
|
||||
/// </summary>
|
||||
Vector3Stamped CachedAngularVelocity { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Dữ liệu từ trường (magnetometer) được cache (µT hoặc Gauss)
|
||||
/// </summary>
|
||||
Vector3Stamped? CachedMagnetometer { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Dữ liệu hướng (Euler angles) được cache (rad)
|
||||
/// Roll, Pitch, Yaw được map sang X, Y, Z của Vector3
|
||||
/// </summary>
|
||||
Vector3Stamped CachedOrientation { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Dữ liệu quaternion được cache
|
||||
/// </summary>
|
||||
QuaternionStamped? CachedQuaternion { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Nhiệt độ cảm biến được cache (°C)
|
||||
/// </summary>
|
||||
double? CachedTemperature { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Timestamp của lần đọc dữ liệu gần nhất
|
||||
/// </summary>
|
||||
DateTime LastUpdateTime { get; }
|
||||
|
||||
// Events
|
||||
|
||||
/// <summary>
|
||||
/// Event được kích hoạt khi dữ liệu gia tốc thay đổi
|
||||
/// </summary>
|
||||
event EventHandler<AccelerationChangedEventArgs>? AccelerationChanged;
|
||||
|
||||
/// <summary>
|
||||
/// Event được kích hoạt khi dữ liệu vận tốc góc thay đổi
|
||||
/// </summary>
|
||||
event EventHandler<AngularVelocityChangedEventArgs>? AngularVelocityChanged;
|
||||
|
||||
/// <summary>
|
||||
/// Event được kích hoạt khi dữ liệu từ trường thay đổi
|
||||
/// </summary>
|
||||
event EventHandler<MagnetometerChangedEventArgs>? MagnetometerChanged;
|
||||
|
||||
/// <summary>
|
||||
/// Event được kích hoạt khi hướng (orientation) thay đổi
|
||||
/// </summary>
|
||||
event EventHandler<OrientationChangedEventArgs>? OrientationChanged;
|
||||
|
||||
// Connection Methods
|
||||
|
||||
/// <summary>
|
||||
/// Kết nối với thiết bị IMU
|
||||
/// </summary>
|
||||
Task ConnectAsync(CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Ngắt kết nối với thiết bị IMU
|
||||
/// </summary>
|
||||
Task DisconnectAsync(CancellationToken cancellationToken = default);
|
||||
|
||||
// Data Reading Methods
|
||||
|
||||
/// <summary>
|
||||
/// Đọc dữ liệu gia tốc (m/s²)
|
||||
/// </summary>
|
||||
Task<AccelStamped> ReadAccelerationAsync(CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Đọc dữ liệu vận tốc góc (rad/s)
|
||||
/// Roll, Pitch, Yaw được map sang X, Y, Z của Vector3
|
||||
/// </summary>
|
||||
Task<Vector3Stamped> ReadAngularVelocityAsync(CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Đọc dữ liệu từ trường (magnetometer) nếu có (µT hoặc Gauss)
|
||||
/// </summary>
|
||||
Task<Vector3Stamped?> ReadMagnetometerAsync(CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Đọc dữ liệu hướng (Euler angles) (rad)
|
||||
/// Roll, Pitch, Yaw được map sang X, Y, Z của Vector3
|
||||
/// </summary>
|
||||
Task<Vector3Stamped> ReadOrientationAsync(CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Đọc dữ liệu quaternion nếu có
|
||||
/// </summary>
|
||||
Task<QuaternionStamped?> ReadQuaternionAsync(CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Đọc tất cả dữ liệu IMU cùng lúc
|
||||
/// </summary>
|
||||
Task<Imu> ReadAllDataAsync(CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Đọc nhiệt độ cảm biến nếu có (°C)
|
||||
/// </summary>
|
||||
Task<double?> ReadTemperatureAsync(CancellationToken cancellationToken = default);
|
||||
|
||||
// Calibration Methods
|
||||
|
||||
/// <summary>
|
||||
/// Calibrate IMU (gyroscope và accelerometer)
|
||||
/// </summary>
|
||||
Task CalibrateAsync(CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Calibrate magnetometer (nếu có)
|
||||
/// </summary>
|
||||
Task CalibrateMagnetometerAsync(CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Reset calibration về mặc định
|
||||
/// </summary>
|
||||
Task ResetCalibrationAsync(CancellationToken cancellationToken = default);
|
||||
|
||||
// Configuration Methods
|
||||
|
||||
/// <summary>
|
||||
/// Thiết lập tần số lấy mẫu (Hz)
|
||||
/// </summary>
|
||||
Task SetSampleRateAsync(double sampleRate, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Thiết lập dải đo gia tốc (g)
|
||||
/// </summary>
|
||||
Task SetAccelerometerRangeAsync(double range, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Thiết lập dải đo vận tốc góc (rad/s)
|
||||
/// </summary>
|
||||
Task SetGyroscopeRangeAsync(double range, CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
// Event Args
|
||||
|
||||
/// <summary>
|
||||
/// Event args khi dữ liệu gia tốc thay đổi
|
||||
/// </summary>
|
||||
public class AccelerationChangedEventArgs(AccelStamped data) : EventArgs
|
||||
{
|
||||
public AccelStamped Data { get; } = data;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Event args khi dữ liệu vận tốc góc thay đổi
|
||||
/// </summary>
|
||||
public class AngularVelocityChangedEventArgs(Vector3Stamped data) : EventArgs
|
||||
{
|
||||
public Vector3Stamped Data { get; } = data;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Event args khi dữ liệu từ trường thay đổi
|
||||
/// </summary>
|
||||
public class MagnetometerChangedEventArgs(Vector3Stamped data) : EventArgs
|
||||
{
|
||||
public Vector3Stamped Data { get; } = data;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Event args khi hướng (orientation) thay đổi
|
||||
/// </summary>
|
||||
public class OrientationChangedEventArgs(Vector3Stamped data) : EventArgs
|
||||
{
|
||||
public Vector3Stamped Data { get; } = data;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Event args chứa toàn bộ dữ liệu IMU trong 1 sample (dùng cho SensorPipeline)
|
||||
/// </summary>
|
||||
public class ImuDataChangedEventArgs(
|
||||
AccelStamped acceleration,
|
||||
Vector3Stamped angularVelocity,
|
||||
Vector3Stamped magnetometer,
|
||||
Vector3Stamped orientation,
|
||||
DateTime timestamp) : EventArgs
|
||||
{
|
||||
public AccelStamped Acceleration { get; } = acceleration;
|
||||
public Vector3Stamped AngularVelocity { get; } = angularVelocity;
|
||||
public Vector3Stamped Magnetometer { get; } = magnetometer;
|
||||
public Vector3Stamped Orientation { get; } = orientation;
|
||||
public DateTime Timestamp { get; } = timestamp;
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
using RobotNet10.Shared.Sensor;
|
||||
|
||||
namespace RobotNet10.RobotApp.Devices;
|
||||
|
||||
/// <summary>
|
||||
/// Interface cho thiết bị LiDAR trong robot AGV (common interface for all Lidar brands)
|
||||
/// Không bao gồm các chức năng kết nối vì đã được xử lý trong DeviceBase
|
||||
/// </summary>
|
||||
public interface ILidar
|
||||
{
|
||||
// Scan Data Properties (common for all Lidar brands)
|
||||
|
||||
/// <summary>
|
||||
/// Measurement data hiện tại (scan points) - sử dụng LaserScan từ sensor_msgs
|
||||
/// </summary>
|
||||
LaserScan? CurrentMeasurementData { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Timestamp của scan data gần nhất
|
||||
/// </summary>
|
||||
DateTime? LastScanDataTimestamp { get; }
|
||||
|
||||
// Device Specifications (common for all Lidar brands)
|
||||
|
||||
/// <summary>
|
||||
/// Góc quét tối thiểu (radian) - góc bắt đầu của phạm vi quét
|
||||
/// </summary>
|
||||
double MinAngleRad { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Góc quét tối đa (radian) - góc kết thúc của phạm vi quét
|
||||
/// </summary>
|
||||
double MaxAngleRad { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Tầm quét tối thiểu (mét) - khoảng cách gần nhất có thể đo được
|
||||
/// </summary>
|
||||
double MinRangeM { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Tầm quét tối đa (mét) - khoảng cách xa nhất có thể đo được
|
||||
/// </summary>
|
||||
double MaxRangeM { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Độ phân giải góc (radian) - góc giữa hai điểm scan liên tiếp
|
||||
/// </summary>
|
||||
double? AngularResolutionRad { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Tần số quét (Hz) - số lần quét trong một giây
|
||||
/// </summary>
|
||||
double? ScanFrequencyHz { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Góc quét tổng (Field of View - FOV) (radian) - tổng góc quét của thiết bị
|
||||
/// </summary>
|
||||
double FieldOfViewRad { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Hỗ trợ đo intensity (cường độ phản xạ) hay không
|
||||
/// </summary>
|
||||
bool SupportsIntensity { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Độ chính xác đo khoảng cách (mét) - sai số đo khoảng cách
|
||||
/// </summary>
|
||||
double? AccuracyM { get; }
|
||||
|
||||
// Events
|
||||
|
||||
/// <summary>
|
||||
/// Event được kích hoạt khi có dữ liệu scan mới
|
||||
/// </summary>
|
||||
event EventHandler<LidarScanDataEventArgs>? ScanDataReceived;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// EventArgs cho event ScanDataReceived - common structure for all Lidar brands
|
||||
/// </summary>
|
||||
public class LidarScanDataEventArgs(DateTime timestamp, LaserScan measurementData) : EventArgs
|
||||
{
|
||||
/// <summary>
|
||||
/// Timestamp khi scan data được nhận
|
||||
/// </summary>
|
||||
public DateTime Timestamp { get; init; } = timestamp;
|
||||
|
||||
/// <summary>
|
||||
/// Measurement data từ scan (sử dụng LaserScan từ sensor_msgs)
|
||||
/// </summary>
|
||||
public LaserScan MeasurementData { get; init; } = measurementData;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,178 @@
|
||||
using RobotNet10.RobotApp.Client.Shared.Devices;
|
||||
|
||||
namespace RobotNet10.RobotApp.Devices;
|
||||
|
||||
public enum ModbusRegisterType
|
||||
{
|
||||
Holding,
|
||||
DiscreteInput,
|
||||
Input,
|
||||
Coil,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Interface cho thiết bị ModbusTCP
|
||||
/// </summary>
|
||||
public interface IModbusTcpDevice
|
||||
{
|
||||
/// <summary>
|
||||
/// IP Address của ModbusTCP server
|
||||
/// </summary>
|
||||
string IpAddress { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Port của ModbusTCP server (mặc định: 502)
|
||||
/// </summary>
|
||||
int Port { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Slave ID (Unit Identifier)
|
||||
/// </summary>
|
||||
byte SlaveId { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Trạng thái kết nối
|
||||
/// </summary>
|
||||
bool IsConnected { get; }
|
||||
|
||||
// Configuration Properties
|
||||
|
||||
event Action<ModbusRegisterType> DataRegisterChanged;
|
||||
|
||||
/// <summary>
|
||||
/// Danh sách các vùng Holding Registers đã được cấu hình
|
||||
/// </summary>
|
||||
IReadOnlyList<ModbusRangeData> HoldingRegisterRanges { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Danh sách các vùng Input Registers đã được cấu hình
|
||||
/// </summary>
|
||||
IReadOnlyList<ModbusRangeData> InputRegisterRanges { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Danh sách các vùng Coils đã được cấu hình
|
||||
/// </summary>
|
||||
IReadOnlyList<ModbusRangeData> CoilRanges { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Danh sách các vùng Discrete Inputs đã được cấu hình
|
||||
/// </summary>
|
||||
IReadOnlyList<ModbusRangeData> DiscreteInputRanges { get; }
|
||||
|
||||
// Holding Registers Methods
|
||||
|
||||
/// <summary>
|
||||
/// Đọc một holding register từ cache
|
||||
/// </summary>
|
||||
/// <param name="address">Địa chỉ register</param>
|
||||
/// <returns>Giá trị register (16-bit unsigned)</returns>
|
||||
/// <exception cref="ArgumentException">Nếu địa chỉ không nằm trong vùng đã khai báo</exception>
|
||||
ushort ReadHoldingRegister(ushort address);
|
||||
|
||||
/// <summary>
|
||||
/// Đọc nhiều holding registers từ cache
|
||||
/// </summary>
|
||||
/// <param name="startAddress">Địa chỉ bắt đầu</param>
|
||||
/// <param name="quantity">Số lượng registers cần đọc</param>
|
||||
/// <returns>Mảng giá trị registers</returns>
|
||||
/// <exception cref="ArgumentException">Nếu địa chỉ không nằm trong vùng đã khai báo</exception>
|
||||
ushort[] ReadHoldingRegisters(ushort startAddress, ushort quantity);
|
||||
|
||||
/// <summary>
|
||||
/// Ghi một holding register vào cache (sẽ được đồng bộ bởi vòng lặp)
|
||||
/// </summary>
|
||||
/// <param name="address">Địa chỉ register</param>
|
||||
/// <param name="value">Giá trị cần ghi</param>
|
||||
/// <exception cref="ArgumentException">Nếu địa chỉ không nằm trong vùng đã khai báo</exception>
|
||||
Task WriteHoldingRegisterAsync(ushort address, ushort value, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Ghi nhiều holding registers vào cache (sẽ được đồng bộ bởi vòng lặp)
|
||||
/// </summary>
|
||||
/// <param name="startAddress">Địa chỉ bắt đầu</param>
|
||||
/// <param name="values">Mảng giá trị cần ghi</param>
|
||||
/// <exception cref="ArgumentException">Nếu địa chỉ không nằm trong vùng đã khai báo</exception>
|
||||
Task WriteHoldingRegistersAsync(ushort startAddress, ushort[] values, CancellationToken cancellationToken = default);
|
||||
|
||||
// Input Registers Methods
|
||||
|
||||
/// <summary>
|
||||
/// Đọc một input register từ cache
|
||||
/// </summary>
|
||||
/// <param name="address">Địa chỉ register</param>
|
||||
/// <returns>Giá trị register (16-bit unsigned)</returns>
|
||||
/// <exception cref="ArgumentException">Nếu địa chỉ không nằm trong vùng đã khai báo</exception>
|
||||
ushort ReadInputRegister(ushort address);
|
||||
|
||||
/// <summary>
|
||||
/// Đọc nhiều input registers từ cache
|
||||
/// </summary>
|
||||
/// <param name="startAddress">Địa chỉ bắt đầu</param>
|
||||
/// <param name="quantity">Số lượng registers cần đọc</param>
|
||||
/// <returns>Mảng giá trị registers</returns>
|
||||
/// <exception cref="ArgumentException">Nếu địa chỉ không nằm trong vùng đã khai báo</exception>
|
||||
ushort[] ReadInputRegisters(ushort startAddress, ushort quantity);
|
||||
|
||||
// Coils Methods
|
||||
|
||||
/// <summary>
|
||||
/// Đọc một coil từ cache
|
||||
/// </summary>
|
||||
/// <param name="address">Địa chỉ coil</param>
|
||||
/// <returns>Giá trị coil (true/false)</returns>
|
||||
/// <exception cref="ArgumentException">Nếu địa chỉ không nằm trong vùng đã khai báo</exception>
|
||||
bool ReadCoil(ushort address);
|
||||
|
||||
/// <summary>
|
||||
/// Đọc nhiều coils từ cache
|
||||
/// </summary>
|
||||
/// <param name="startAddress">Địa chỉ bắt đầu</param>
|
||||
/// <param name="quantity">Số lượng coils cần đọc</param>
|
||||
/// <returns>Mảng giá trị coils</returns>
|
||||
/// <exception cref="ArgumentException">Nếu địa chỉ không nằm trong vùng đã khai báo</exception>
|
||||
bool[] ReadCoils(ushort startAddress, ushort quantity);
|
||||
|
||||
/// <summary>
|
||||
/// Ghi một coil vào cache (sẽ được đồng bộ bởi vòng lặp)
|
||||
/// </summary>
|
||||
/// <param name="address">Địa chỉ coil</param>
|
||||
/// <param name="value">Giá trị cần ghi</param>
|
||||
/// <exception cref="ArgumentException">Nếu địa chỉ không nằm trong vùng đã khai báo</exception>
|
||||
Task WriteCoilAsync(ushort address, bool value, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Ghi nhiều coils vào cache (sẽ được đồng bộ bởi vòng lặp)
|
||||
/// </summary>
|
||||
/// <param name="startAddress">Địa chỉ bắt đầu</param>
|
||||
/// <param name="values">Mảng giá trị cần ghi</param>
|
||||
/// <exception cref="ArgumentException">Nếu địa chỉ không nằm trong vùng đã khai báo</exception>
|
||||
Task WriteCoilsAsync(ushort startAddress, bool[] values, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Ghi một coil xuống PLC ngay lập tức (không qua queue). Dùng cho pulse (M815 alarm reset).
|
||||
/// </summary>
|
||||
/// <param name="address">Địa chỉ coil</param>
|
||||
/// <param name="value">Giá trị cần ghi</param>
|
||||
/// <param name="cancellationToken">Cancellation token</param>
|
||||
Task WriteCoilImmediateAsync(ushort address, bool value, CancellationToken cancellationToken = default);
|
||||
|
||||
// Discrete Inputs Methods
|
||||
|
||||
/// <summary>
|
||||
/// Đọc một discrete input từ cache
|
||||
/// </summary>
|
||||
/// <param name="address">Địa chỉ input</param>
|
||||
/// <returns>Giá trị input (true/false)</returns>
|
||||
/// <exception cref="ArgumentException">Nếu địa chỉ không nằm trong vùng đã khai báo</exception>
|
||||
bool ReadDiscreteInput(ushort address);
|
||||
|
||||
/// <summary>
|
||||
/// Đọc nhiều discrete inputs từ cache
|
||||
/// </summary>
|
||||
/// <param name="startAddress">Địa chỉ bắt đầu</param>
|
||||
/// <param name="quantity">Số lượng inputs cần đọc</param>
|
||||
/// <returns>Mảng giá trị inputs</returns>
|
||||
/// <exception cref="ArgumentException">Nếu địa chỉ không nằm trong vùng đã khai báo</exception>
|
||||
bool[] ReadDiscreteInputs(ushort startAddress, ushort quantity);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
using RobotNet10.RobotApp.Interfaces;
|
||||
using RobotNet10.Shared.Sensor;
|
||||
|
||||
namespace RobotNet10.RobotApp.Devices;
|
||||
|
||||
/// <summary>
|
||||
/// Interface cho thiết bị RF Handle (Remote Control Handle) - tay điều khiển RF với joystick
|
||||
/// </summary>
|
||||
public interface IRfHandle
|
||||
{
|
||||
/// <summary>
|
||||
/// Event được trigger khi dữ liệu được cập nhật
|
||||
/// </summary>
|
||||
event Action? Updated;
|
||||
|
||||
// ====================== STATES ===========================
|
||||
|
||||
DateTime LastUpdateTime { get; }
|
||||
|
||||
int Heartbeat { get; }
|
||||
bool RemoteReady { get; }
|
||||
bool EStop { get; }
|
||||
|
||||
bool LiftUp { get; }
|
||||
bool LiftDown { get; }
|
||||
bool RotateLeft { get; }
|
||||
bool RotateRight { get; }
|
||||
|
||||
bool ModeSelect { get; }
|
||||
bool Enable { get; }
|
||||
|
||||
int Speed { get; }
|
||||
double Linear { get; }
|
||||
double Angular { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Current RF Handle mode (Default, Maintenance, Override, None)
|
||||
/// </summary>
|
||||
RFMode Mode { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Joy state hiện tại được cache (thread-safe)
|
||||
/// </summary>
|
||||
Joy? CurrentJoyState { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Đọc joy state từ thiết bị
|
||||
/// </summary>
|
||||
Task<Joy> ReadJoyStateAsync(CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user