using RobotNet10.RobotApp.Client.Shared.Devices; using System.Reflection; namespace RobotNet10.RobotApp.Devices; /// /// Service quản lý và cung cấp truy xuất devices /// Tự động tạo devices từ configuration khi start /// Thread-safe implementation /// public class DeviceProvider( IConfiguration _configuration, IServiceProvider _serviceProvider, ILogger _logger) : IDeviceProvider, IHostedService { private readonly Dictionary _devicesById = new(StringComparer.OrdinalIgnoreCase); private readonly Dictionary> _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(); } // 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(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(); 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(); 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(); 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; } /// /// 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 /// private Dictionary DiscoverDeviceTypes() { var deviceTypes = new Dictionary(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(); 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; } /// /// Tìm device type theo driverName và version /// private static Type? FindDeviceType(Dictionary 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; } /// /// Tìm constructor phù hợp với tham số: deviceId (string), deviceName (string), IConfigurationSection, và optional IServiceProvider /// 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; } /// /// Tạo device từ configuration section /// private DeviceBase? CreateDeviceFromConfigurationSection(IConfigurationSection section, Dictionary 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(configDeviceTypeStr, ignoreCase: true, out var configDeviceType)) { var attribute = deviceType.GetCustomAttribute(); 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 devicesToStop; lock (_lock) { devicesToStop = [.. _devicesById.Values]; } if (devicesToStop.Count > 0) { // Disconnect all devices in parallel var disconnectTasks = new List(); 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; } } /// /// Initialize a device /// 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 } } /// /// Connect a device (runs in background thread) /// 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 } } /// /// Disconnect a device /// 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); } } /// /// Handler for device status changes - updates _devicesConnected when devices disconnect/reconnect /// 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 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 GetDeviceByTypeAsync(DeviceType deviceType, CancellationToken cancellationToken = default) { return Task.FromResult(GetDeviceByType(deviceType)); } public IReadOnlyList GetDevicesByType(DeviceType deviceType) { lock (_lock) { if (_devicesByType.TryGetValue(deviceType, out var devices)) { return devices.ToList().AsReadOnly(); } return Array.Empty().ToList().AsReadOnly(); } } public Task> GetDevicesByTypeAsync(DeviceType deviceType, CancellationToken cancellationToken = default) { return Task.FromResult(GetDevicesByType(deviceType)); } public IReadOnlyList GetAllDevices() { lock (_lock) { return _devicesById.Values.ToList().AsReadOnly(); } } public Task> 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 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 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; }