Files
Denso/srcs/RobotNet10/RobotApp/RobotNet10.RobotApp/Devices/DeviceBase.cs
2026-07-03 16:31:37 +07:00

800 lines
27 KiB
C#

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