Initial commit

This commit is contained in:
2026-07-03 16:31:37 +07:00
commit 899c7c637d
1939 changed files with 641750 additions and 0 deletions

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

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

View File

@@ -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;
}
}

View File

@@ -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;
}

View File

@@ -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;
}
}

View File

@@ -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
}

View File

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

View File

@@ -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; }
}

View File

@@ -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

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

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

View File

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