using Appccelerate.StateMachine; using Appccelerate.StateMachine.Machine; using RobotNet10.RobotApp.Client.Shared.Devices; namespace RobotNet10.RobotApp.Devices; /// /// 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 /// public abstract class DeviceBase : IDisposable { private readonly PassiveStateMachine _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 _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(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(); ConfigureStateMachine(builder); _stateMachine = builder .WithInitialState(DeviceStatus.Uninitialized) .Build() .CreatePassiveStateMachine(); _currentStatus = DeviceStatus.Uninitialized; _stateMachine.Start(); } private void ConfigureStateMachine(StateMachineDefinitionBuilder 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; } /// /// Mô tả thiết bị (dùng để hiển thị trên web UI) /// public string? Description { get; set; } /// /// 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 /// public IReadOnlyList PropertyDescriptions { get; } /// /// 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 /// public IReadOnlyDictionary 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? ConnectionStateChanged; public event EventHandler? ErrorOccurred; public event EventHandler? StatusChanged; // State Machine Helper Methods private static void ValidatePropertyDescriptions(List propertyDescriptions) { var seenKeys = new HashSet(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 /// /// 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 /// protected virtual IEnumerable CreatePropertyDescriptions() { return []; } /// /// Khởi tạo thiết bị (implementation specific) /// protected abstract Task OnInitializeAsync(CancellationToken cancellationToken); /// /// Kết nối với thiết bị (implementation specific) /// protected abstract Task OnConnectAsync(CancellationToken cancellationToken); /// /// Ngắt kết nối với thiết bị (implementation specific) /// protected abstract Task OnDisconnectAsync(CancellationToken cancellationToken); /// /// Reset thiết bị (implementation specific) /// protected abstract Task OnResetAsync(CancellationToken cancellationToken); /// /// Kiểm tra kết nối (implementation specific) /// protected abstract Task 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 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 /// /// 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 /// 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; } } /// /// Lấy giá trị của một property /// 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); } }