Initial commit
This commit is contained in:
@@ -0,0 +1,842 @@
|
||||
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;
|
||||
}
|
||||
Reference in New Issue
Block a user