800 lines
27 KiB
C#
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);
|
|
}
|
|
}
|