Files
BQP/srcs/RobotNet10/RobotApp/RobotNet10.RobotApp/Devices/DeviceProvider.cs
2026-07-13 09:25:40 +07:00

843 lines
30 KiB
C#

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