843 lines
30 KiB
C#
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;
|
|
}
|