diff --git a/Analyzer/MainForm.cs b/Analyzer/MainForm.cs
index a8b0c2e..fc2ca95 100644
--- a/Analyzer/MainForm.cs
+++ b/Analyzer/MainForm.cs
@@ -40,9 +40,9 @@ namespace EonaCat.HID.Analyzer
_deviceManager.OnDeviceRemoved += Hid_Removed;
}
- public void RefreshDevices(ushort? vendorId = null, ushort? productId = null)
+ public async Task RefreshDevicesAsync(ushort? vendorId = null, ushort? productId = null)
{
- _deviceList = _deviceManager.Enumerate(vendorId, productId);
+ _deviceList = await _deviceManager.EnumerateAsync(vendorId, productId).ConfigureAwait(false);
}
private void MainForm_Load(object sender, System.EventArgs e)
@@ -61,7 +61,7 @@ namespace EonaCat.HID.Analyzer
{
try
{
- RefreshDevices();
+ RefreshDevicesAsync();
UpdateDeviceList();
toolStripStatusLabel1.Text = "Please select device and click open to start.";
}
@@ -182,7 +182,7 @@ namespace EonaCat.HID.Analyzer
{
try
{
- RefreshDevices();
+ RefreshDevicesAsync();
UpdateDeviceList();
}
catch (Exception ex)
@@ -203,7 +203,7 @@ namespace EonaCat.HID.Analyzer
vid = ushort.Parse(str[0], NumberStyles.AllowHexSpecifier);
pid = ushort.Parse(str[1], NumberStyles.AllowHexSpecifier);
}
- RefreshDevices(vid, pid);
+ RefreshDevicesAsync(vid, pid);
UpdateDeviceList();
}
catch (Exception ex)
diff --git a/EonaCat.HID.Console/Program.cs b/EonaCat.HID.Console/Program.cs
index c78b68e..f05197a 100644
--- a/EonaCat.HID.Console/Program.cs
+++ b/EonaCat.HID.Console/Program.cs
@@ -34,7 +34,7 @@ namespace EonaCat.HID.Example
Console.WriteLine($"Removed Device --> VID: {e.Device.VendorId:X4}, PID: {e.Device.ProductId:X4}");
};
- RefreshDevices();
+ RefreshDevicesAsync();
if (!_deviceList.Any())
{
@@ -83,9 +83,9 @@ namespace EonaCat.HID.Example
}
}
- static void RefreshDevices(ushort? vendorId = null, ushort? productId = null)
+ static async Task RefreshDevicesAsync(ushort? vendorId = null, ushort? productId = null)
{
- _deviceList = _deviceManager.Enumerate(vendorId, productId);
+ _deviceList = await _deviceManager.EnumerateAsync(vendorId, productId).ConfigureAwait(false);
}
static void DisplayDevices()
diff --git a/EonaCat.HID/EonaCat.HID.csproj b/EonaCat.HID/EonaCat.HID.csproj
index 7da64d7..70d8eb9 100644
--- a/EonaCat.HID/EonaCat.HID.csproj
+++ b/EonaCat.HID/EonaCat.HID.csproj
@@ -7,7 +7,7 @@
Copyright 2024 EonaCat (Jeroen Saey)
latest
EonaCat.HID
- 1.0.4
+ 1.0.5
EonaCat.HID
EonaCat (Jeroen Saey)
HID Devices
diff --git a/EonaCat.HID/HidWindows.cs b/EonaCat.HID/HidWindows.cs
index b9f7fe7..5c9cc23 100644
--- a/EonaCat.HID/HidWindows.cs
+++ b/EonaCat.HID/HidWindows.cs
@@ -1,472 +1,469 @@
-using EonaCat.HID.EventArguments;
-using EonaCat.HID.Managers.Windows;
-using EonaCat.HID.Models;
-using Microsoft.Win32.SafeHandles;
-using System;
-using System.Collections.Generic;
-using System.ComponentModel;
-using System.IO;
-using System.Linq;
-using System.Runtime.InteropServices;
-using System.Text;
-using System.Threading;
-using System.Threading.Tasks;
-using static EonaCat.HID.Managers.Windows.NativeMethods;
-
-namespace EonaCat.HID
-{
- // This file is part of the EonaCat project(s) which is released under the Apache License.
- // See the LICENSE file or go to https://EonaCat.com/license for full license details.
-
- internal sealed class HidWindows : IHid
- {
- private SafeFileHandle _deviceHandle;
- private FileStream _deviceStream;
- private IntPtr _preparsedData;
-
- private bool _isOpen;
- private readonly string _devicePath;
-
- public string DevicePath => _devicePath;
- public ushort VendorId { get; private set; }
- public ushort ProductId { get; private set; }
- public string SerialNumber { get; private set; }
- public string Manufacturer { get; private set; }
- public string ProductName { get; private set; }
- public int InputReportByteLength { get; private set; }
- public int OutputReportByteLength { get; private set; }
- public int FeatureReportByteLength { get; private set; }
- public bool IsConnected => _isOpen;
-
- public IDictionary Capabilities { get; } = new Dictionary();
- public bool IsReadingSupport { get; private set; }
- public bool IsWritingSupport { get; private set; }
-
- public event EventHandler OnDataReceived;
- public event EventHandler OnError;
-
- private CancellationTokenSource _listeningCts;
-
- private readonly object _lockObject = new object();
-
- public HidWindows(string devicePath)
- {
- _devicePath = devicePath ?? throw new ArgumentNullException(nameof(devicePath));
- }
-
- internal void Setup()
- {
- Open();
- LoadDeviceAttributes();
- Close();
- }
-
- private void LoadDeviceAttributes()
- {
- Manufacturer = GetStringDescriptor(HidD_GetManufacturerString);
- ProductName = GetStringDescriptor(HidD_GetProductString);
- SerialNumber = GetStringDescriptor(HidD_GetSerialNumberString);
- HidDeviceAttributes attr = GetDeviceAttributes();
- VendorId = attr.VendorID;
- ProductId = attr.ProductID;
- }
-
- ///
- /// Open the device for I/O
- ///
- public void Open()
- {
- if (_isOpen)
- return;
-
- FileAccess access = FileAccess.ReadWrite;
- SafeFileHandle handle = TryOpenDevice(GENERIC_READ | GENERIC_WRITE);
-
- if (handle == null || handle.IsInvalid)
- {
- handle = TryOpenDevice(GENERIC_READ);
- if (handle != null && !handle.IsInvalid)
- access = FileAccess.Read;
- }
-
- if ((handle == null || handle.IsInvalid) && Environment.Is64BitOperatingSystem)
- {
- handle = TryOpenDevice(GENERIC_WRITE);
- if (handle != null && !handle.IsInvalid)
- access = FileAccess.Write;
- }
-
- if (handle == null || handle.IsInvalid)
- {
- int err = Marshal.GetLastWin32Error();
- OnError?.Invoke(this, new HidErrorEventArgs(this,
- new Win32Exception(err, $"Cannot open device {_devicePath} with any access mode")));
- return;
- }
-
- _deviceHandle = handle;
- _deviceStream = new FileStream(_deviceHandle, access, bufferSize: 64, isAsync: true);
- _isOpen = true;
-
- // HID descriptor
- if (!HidD_GetPreparsedData(_deviceHandle.DangerousGetHandle(), out _preparsedData))
- throw new Win32Exception(Marshal.GetLastWin32Error(), "Failed HidD_GetPreparsedData");
-
- HIDP_CAPS caps;
- int capsRes = HidP_GetCaps(_preparsedData, out caps);
- if (capsRes != NativeMethods.HIDP_STATUS_SUCCESS)
- throw new Win32Exception(capsRes, "Failed HidP_GetCaps");
-
- InputReportByteLength = caps.InputReportByteLength;
- OutputReportByteLength = caps.OutputReportByteLength;
- FeatureReportByteLength = caps.FeatureReportByteLength;
-
- Capabilities["Usage"] = caps.Usage;
- Capabilities["UsagePage"] = caps.UsagePage;
- Capabilities["InputReportByteLength"] = InputReportByteLength;
- Capabilities["OutputReportByteLength"] = OutputReportByteLength;
- Capabilities["FeatureReportByteLength"] = FeatureReportByteLength;
-
- Manufacturer = GetStringDescriptor(HidD_GetManufacturerString);
- ProductName = GetStringDescriptor(HidD_GetProductString);
- SerialNumber = GetStringDescriptor(HidD_GetSerialNumberString);
-
- IsReadingSupport = (access == FileAccess.Read || access == FileAccess.ReadWrite);
- IsWritingSupport = (access == FileAccess.Write || access == FileAccess.ReadWrite);
-
- HidDeviceAttributes attr = GetDeviceAttributes();
- VendorId = attr.VendorID;
- ProductId = attr.ProductID;
- }
-
- private SafeFileHandle TryOpenDevice(int access)
- {
- var handle = CreateFile(_devicePath,
- access,
- FILE_SHARE_READ | FILE_SHARE_WRITE,
- IntPtr.Zero,
- OPEN_EXISTING,
- FILE_FLAG_OVERLAPPED,
- IntPtr.Zero);
-
- return handle;
- }
-
- ///
- /// Close the device
- ///
- public void Close()
- {
- lock (_lockObject)
- {
- if (!_isOpen)
- {
- return;
- }
-
- StopListening();
-
- if (_preparsedData != IntPtr.Zero)
- {
- HidD_FreePreparsedData(_preparsedData);
- _preparsedData = IntPtr.Zero;
- }
-
- _deviceStream?.Dispose();
- _deviceStream = null;
-
- _deviceHandle?.Dispose();
- _deviceHandle = null;
-
- _isOpen = false;
- }
- }
- private HidDeviceAttributes GetDeviceAttributes()
- {
- HidDeviceAttributes attr = new HidDeviceAttributes
- {
- Size = Marshal.SizeOf()
- };
- if (!HidD_GetAttributes(_deviceHandle.DangerousGetHandle(), ref attr))
- {
- throw new Win32Exception(Marshal.GetLastWin32Error(), "Failed HidD_GetAttributes");
- }
- return attr;
- }
-
- private string GetStringDescriptor(Func stringFunc)
- {
- var buffer = new byte[126 * 2]; // Unicode max buffer
- GCHandle gc = GCHandle.Alloc(buffer, GCHandleType.Pinned);
- try
- {
- bool success = stringFunc(_deviceHandle.DangerousGetHandle(), gc.AddrOfPinnedObject(), buffer.Length);
- if (!success)
- {
- return null;
- }
-
- string str = Encoding.Unicode.GetString(buffer);
- int idx = str.IndexOf('\0');
- if (idx >= 0)
- {
- str = str.Substring(0, idx);
- }
-
- return str;
- }
- finally
- {
- gc.Free();
- }
- }
-
- public void Dispose()
- {
- Close();
- }
-
- public async Task WriteOutputReportAsync(HidReport report)
- {
- if (!_isOpen)
- {
- OnError?.Invoke(this, new HidErrorEventArgs(this, new InvalidOperationException("Device not open")));
- return;
- }
-
- if (report == null)
- {
- OnError?.Invoke(this, new HidErrorEventArgs(this, new ArgumentNullException(nameof(report))));
- return;
- }
-
- if (report.Data == null || report.Data.Length == 0)
- {
- OnError?.Invoke(this, new HidErrorEventArgs(this, new ArgumentException("Data cannot be null or empty", nameof(report))));
- return;
- }
-
- try
- {
- // Combine reportId and data into one buffer for sending
- var buffer = new byte[1 + report.Data.Length];
- buffer[0] = report.ReportId;
- Array.Copy(report.Data, 0, buffer, 1, report.Data.Length);
-
- await _deviceStream.WriteAsync(buffer, 0, buffer.Length);
- await _deviceStream.FlushAsync();
- }
- catch (Exception ex)
- {
- OnError?.Invoke(this, new HidErrorEventArgs(this, ex));
- throw;
- }
- }
-
- public async Task ReadInputReportAsync()
- {
- if (!_isOpen)
- {
- OnError?.Invoke(this, new HidErrorEventArgs(this, new InvalidOperationException("Device not open")));
- return new HidReport(0, Array.Empty());
- }
-
- return await Task.Run(async () =>
- {
- var buffer = new byte[InputReportByteLength];
-
- try
- {
- int read = await _deviceStream.ReadAsync(buffer, 0, buffer.Length);
- if (read == 0)
- {
- OnError?.Invoke(this, new HidErrorEventArgs(this, new IOException("No data read from device")));
- return new HidReport(0, Array.Empty());
- }
-
- byte reportId = buffer[0];
- byte[] data = buffer.Skip(1).Take(read - 1).ToArray();
-
- return new HidReport(reportId, data);
- }
- catch (Exception ex)
- {
- OnError?.Invoke(this, new HidErrorEventArgs(this, ex));
- throw;
- }
- });
- }
-
-
- public async Task SendFeatureReportAsync(HidReport report)
- {
- if (!_isOpen)
- {
- OnError?.Invoke(this, new HidErrorEventArgs(this, new InvalidOperationException("Device not open")));
- return;
- }
-
- if (report == null)
- throw new ArgumentNullException(nameof(report));
-
- // Prepare buffer with ReportId + Data
- var data = new byte[1 + report.Data.Length];
- data[0] = report.ReportId;
- Array.Copy(report.Data, 0, data, 1, report.Data.Length);
-
- await Task.Run(() =>
- {
- bool success = HidD_SetFeature(_deviceHandle.DangerousGetHandle(), data, data.Length);
- if (!success)
- {
- var err = Marshal.GetLastWin32Error();
- var ex = new Win32Exception(err, "HidD_SetFeature failed");
- OnError?.Invoke(this, new HidErrorEventArgs(this, ex));
- throw ex;
- }
- });
- }
-
-
- public async Task GetFeatureReportAsync(byte reportId)
- {
- if (!_isOpen)
- {
- OnError?.Invoke(this, new HidErrorEventArgs(this, new InvalidOperationException("Device not open")));
- return new HidReport(0, Array.Empty());
- }
-
- return await Task.Run(() =>
- {
- var buffer = new byte[FeatureReportByteLength];
- buffer[0] = reportId;
-
- bool success = HidD_GetFeature(_deviceHandle.DangerousGetHandle(), buffer, buffer.Length);
- if (!success)
- {
- var err = Marshal.GetLastWin32Error();
- var ex = new Win32Exception(err, "HidD_GetFeature failed");
- OnError?.Invoke(this, new HidErrorEventArgs(this, ex));
- return new HidReport(0, Array.Empty());
- }
-
- byte[] data = buffer.Skip(1).ToArray();
- return new HidReport(reportId, data);
- });
- }
-
-
- ///
- /// Begin async reading loop raising OnDataReceived events on data input
- ///
- ///
- ///
- public async Task StartListeningAsync(CancellationToken cancellationToken)
- {
- if (!_isOpen)
- {
- OnError?.Invoke(this, new HidErrorEventArgs(this, new NotSupportedException("Device is not open.")));
- return;
- }
-
- if (_listeningCts != null)
- {
- OnError?.Invoke(this, new HidErrorEventArgs(this, new NotSupportedException("Already listening on this device.")));
- return;
- }
-
- _listeningCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
-
- try
- {
- var token = _listeningCts.Token;
-
- while (!token.IsCancellationRequested)
- {
- try
- {
- var inputReport = await ReadInputReportAsync(token);
-
- if (inputReport?.Data?.Length > 0)
- {
- OnDataReceived?.Invoke(this, new HidDataReceivedEventArgs(this, inputReport));
- }
- }
- catch (OperationCanceledException)
- {
- break;
- }
- catch (NotSupportedException)
- {
- OnError?.Invoke(this, new HidErrorEventArgs(this, new NotSupportedException("Reading input reports is not supported on this device.")));
- break;
- }
- catch (Exception ex)
- {
- if (token.IsCancellationRequested)
- {
- break;
- }
-
- OnError?.Invoke(this, new HidErrorEventArgs(this, ex));
- }
- }
- }
- finally
- {
- _listeningCts.Dispose();
- _listeningCts = null;
- }
- }
-
- private Task ReadInputReportAsync(CancellationToken cancellationToken)
- {
- var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
-
- var buffer = new byte[InputReportByteLength];
-
- // Start async read
- _deviceStream.BeginRead(buffer, 0, buffer.Length, ar =>
- {
- try
- {
- int bytesRead = _deviceStream.EndRead(ar);
-
- if (bytesRead == 0)
- {
- // No data read, reportId 0 and empty data
- tcs.SetResult(new HidReport(0, Array.Empty()));
- }
- else
- {
- // First byte is reportId, rest is data
- byte reportId = buffer[0];
- byte[] data = bytesRead > 1 ? buffer.Skip(1).Take(bytesRead - 1).ToArray() : Array.Empty();
- tcs.SetResult(new HidReport(reportId, data));
- }
- }
- catch (Exception ex)
- {
- tcs.SetException(ex);
- }
- }, null);
-
- cancellationToken.Register(() =>
- {
- tcs.TrySetCanceled();
- });
-
- return tcs.Task;
- }
-
- private void StopListening()
- {
- if (_listeningCts != null)
- {
- _listeningCts.Cancel();
- _listeningCts.Dispose();
- _listeningCts = null;
- }
- }
- }
+using EonaCat.HID.EventArguments;
+using EonaCat.HID.Managers.Windows;
+using EonaCat.HID.Models;
+using Microsoft.Win32.SafeHandles;
+using System;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.IO;
+using System.Linq;
+using System.Runtime.InteropServices;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+using static EonaCat.HID.Managers.Windows.NativeMethods;
+
+namespace EonaCat.HID
+{
+ // This file is part of the EonaCat project(s) which is released under the Apache License.
+ // See the LICENSE file or go to https://EonaCat.com/license for full license details.
+
+ internal sealed class HidWindows : IHid
+ {
+ private SafeFileHandle _deviceHandle;
+ private FileStream _deviceStream;
+ private IntPtr _preparsedData;
+
+ private bool _isOpen;
+ private readonly string _devicePath;
+
+ public string DevicePath => _devicePath;
+ public ushort VendorId { get; private set; }
+ public ushort ProductId { get; private set; }
+ public string SerialNumber { get; private set; }
+ public string Manufacturer { get; private set; }
+ public string ProductName { get; private set; }
+ public int InputReportByteLength { get; private set; }
+ public int OutputReportByteLength { get; private set; }
+ public int FeatureReportByteLength { get; private set; }
+ public bool IsConnected => _isOpen;
+
+ public IDictionary Capabilities { get; } = new Dictionary();
+ public bool IsReadingSupport { get; private set; }
+ public bool IsWritingSupport { get; private set; }
+
+ public event EventHandler OnDataReceived;
+ public event EventHandler OnError;
+
+ private CancellationTokenSource _listeningCts;
+
+ private readonly object _lockObject = new object();
+
+ public HidWindows(string devicePath)
+ {
+ _devicePath = devicePath ?? throw new ArgumentNullException(nameof(devicePath));
+ }
+
+ internal void Setup()
+ {
+ Open();
+ LoadDeviceAttributes();
+ Close();
+ }
+
+ private void LoadDeviceAttributes()
+ {
+ Manufacturer = GetStringDescriptor(HidD_GetManufacturerString);
+ ProductName = GetStringDescriptor(HidD_GetProductString);
+ SerialNumber = GetStringDescriptor(HidD_GetSerialNumberString);
+ HidDeviceAttributes attr = GetDeviceAttributes();
+ VendorId = attr.VendorID;
+ ProductId = attr.ProductID;
+ }
+
+ ///
+ /// Open the device for I/O
+ ///
+ public void Open()
+ {
+ if (_isOpen)
+ return;
+
+ FileAccess access = FileAccess.ReadWrite;
+ SafeFileHandle handle = TryOpenDevice(GENERIC_READ | GENERIC_WRITE);
+
+ if (handle == null || handle.IsInvalid)
+ {
+ handle = TryOpenDevice(GENERIC_READ);
+ if (handle != null && !handle.IsInvalid)
+ access = FileAccess.Read;
+ }
+
+ if ((handle == null || handle.IsInvalid) && Environment.Is64BitOperatingSystem)
+ {
+ handle = TryOpenDevice(GENERIC_WRITE);
+ if (handle != null && !handle.IsInvalid)
+ access = FileAccess.Write;
+ }
+
+ if (handle == null || handle.IsInvalid)
+ {
+ int err = Marshal.GetLastWin32Error();
+ OnError?.Invoke(this, new HidErrorEventArgs(this,
+ new Win32Exception(err, $"Cannot open device {_devicePath} with any access mode")));
+ return;
+ }
+
+ _deviceHandle = handle;
+ _deviceStream = new FileStream(_deviceHandle, access, bufferSize: 64, isAsync: true);
+ _isOpen = true;
+
+ // HID descriptor
+ if (!HidD_GetPreparsedData(_deviceHandle.DangerousGetHandle(), out _preparsedData))
+ throw new Win32Exception(Marshal.GetLastWin32Error(), "Failed HidD_GetPreparsedData");
+
+ HIDP_CAPS caps;
+ int capsRes = HidP_GetCaps(_preparsedData, out caps);
+ if (capsRes != NativeMethods.HIDP_STATUS_SUCCESS)
+ throw new Win32Exception(capsRes, "Failed HidP_GetCaps");
+
+ InputReportByteLength = caps.InputReportByteLength;
+ OutputReportByteLength = caps.OutputReportByteLength;
+ FeatureReportByteLength = caps.FeatureReportByteLength;
+
+ Capabilities["Usage"] = caps.Usage;
+ Capabilities["UsagePage"] = caps.UsagePage;
+ Capabilities["InputReportByteLength"] = InputReportByteLength;
+ Capabilities["OutputReportByteLength"] = OutputReportByteLength;
+ Capabilities["FeatureReportByteLength"] = FeatureReportByteLength;
+
+ Manufacturer = GetStringDescriptor(HidD_GetManufacturerString);
+ ProductName = GetStringDescriptor(HidD_GetProductString);
+ SerialNumber = GetStringDescriptor(HidD_GetSerialNumberString);
+
+ IsReadingSupport = (access == FileAccess.Read || access == FileAccess.ReadWrite);
+ IsWritingSupport = (access == FileAccess.Write || access == FileAccess.ReadWrite);
+
+ HidDeviceAttributes attr = GetDeviceAttributes();
+ VendorId = attr.VendorID;
+ ProductId = attr.ProductID;
+ }
+
+ private SafeFileHandle TryOpenDevice(int access)
+ {
+ var handle = CreateFile(_devicePath,
+ access,
+ FILE_SHARE_READ | FILE_SHARE_WRITE,
+ IntPtr.Zero,
+ OPEN_EXISTING,
+ FILE_FLAG_OVERLAPPED,
+ IntPtr.Zero);
+
+ return handle;
+ }
+
+ ///
+ /// Close the device
+ ///
+ public void Close()
+ {
+ lock (_lockObject)
+ {
+ if (!_isOpen)
+ {
+ return;
+ }
+
+ StopListening();
+
+ if (_preparsedData != IntPtr.Zero)
+ {
+ HidD_FreePreparsedData(_preparsedData);
+ _preparsedData = IntPtr.Zero;
+ }
+
+ _deviceStream?.Dispose();
+ _deviceStream = null;
+
+ _deviceHandle?.Dispose();
+ _deviceHandle = null;
+
+ _isOpen = false;
+ }
+ }
+ private HidDeviceAttributes GetDeviceAttributes()
+ {
+ HidDeviceAttributes attr = new HidDeviceAttributes
+ {
+ Size = Marshal.SizeOf()
+ };
+ if (!HidD_GetAttributes(_deviceHandle.DangerousGetHandle(), ref attr))
+ {
+ throw new Win32Exception(Marshal.GetLastWin32Error(), "Failed HidD_GetAttributes");
+ }
+ return attr;
+ }
+
+ private string GetStringDescriptor(Func stringFunc)
+ {
+ var buffer = new byte[126 * 2]; // Unicode max buffer
+ GCHandle gc = GCHandle.Alloc(buffer, GCHandleType.Pinned);
+ try
+ {
+ bool success = stringFunc(_deviceHandle.DangerousGetHandle(), gc.AddrOfPinnedObject(), buffer.Length);
+ if (!success)
+ {
+ return null;
+ }
+
+ string str = Encoding.Unicode.GetString(buffer);
+ int idx = str.IndexOf('\0');
+ if (idx >= 0)
+ {
+ str = str.Substring(0, idx);
+ }
+
+ return str;
+ }
+ finally
+ {
+ gc.Free();
+ }
+ }
+
+ public void Dispose()
+ {
+ Close();
+ }
+
+ public async Task WriteOutputReportAsync(HidReport report)
+ {
+ if (!_isOpen || _deviceStream == null)
+ {
+ OnError?.Invoke(this, new HidErrorEventArgs(this, new InvalidOperationException("Device not open or stream is null")));
+ return;
+ }
+
+ if (report?.Data == null || report.Data.Length == 0)
+ {
+ OnError?.Invoke(this, new HidErrorEventArgs(this, new ArgumentException("Invalid report or data")));
+ return;
+ }
+
+ try
+ {
+ int reportLength = report.Data.Length + 1;
+ var buffer = new byte[reportLength];
+ buffer[0] = report.ReportId;
+ Array.Copy(report.Data, 0, buffer, 1, report.Data.Length);
+
+ await _deviceStream.WriteAsync(buffer, 0, buffer.Length);
+ }
+ catch (IOException ioEx)
+ {
+ OnError?.Invoke(this, new HidErrorEventArgs(this, ioEx));
+ }
+ catch (Exception ex)
+ {
+ OnError?.Invoke(this, new HidErrorEventArgs(this, ex));
+ }
+ }
+
+
+ public async Task ReadInputReportAsync()
+ {
+ if (!_isOpen)
+ {
+ OnError?.Invoke(this, new HidErrorEventArgs(this, new InvalidOperationException("Device not open")));
+ return new HidReport(0, Array.Empty());
+ }
+
+ return await Task.Run(async () =>
+ {
+ var buffer = new byte[InputReportByteLength];
+
+ try
+ {
+ int read = await _deviceStream.ReadAsync(buffer, 0, buffer.Length);
+ if (read == 0)
+ {
+ OnError?.Invoke(this, new HidErrorEventArgs(this, new IOException("No data read from device")));
+ return new HidReport(0, Array.Empty());
+ }
+
+ byte reportId = buffer[0];
+ byte[] data = buffer.Skip(1).Take(read - 1).ToArray();
+
+ return new HidReport(reportId, data);
+ }
+ catch (Exception ex)
+ {
+ OnError?.Invoke(this, new HidErrorEventArgs(this, ex));
+ throw;
+ }
+ });
+ }
+
+
+ public async Task SendFeatureReportAsync(HidReport report)
+ {
+ if (!_isOpen)
+ {
+ OnError?.Invoke(this, new HidErrorEventArgs(this, new InvalidOperationException("Device not open")));
+ return;
+ }
+
+ if (report == null)
+ throw new ArgumentNullException(nameof(report));
+
+ // Prepare buffer with ReportId + Data
+ var data = new byte[1 + report.Data.Length];
+ data[0] = report.ReportId;
+ Array.Copy(report.Data, 0, data, 1, report.Data.Length);
+
+ await Task.Run(() =>
+ {
+ bool success = HidD_SetFeature(_deviceHandle.DangerousGetHandle(), data, data.Length);
+ if (!success)
+ {
+ var err = Marshal.GetLastWin32Error();
+ var ex = new Win32Exception(err, "HidD_SetFeature failed");
+ OnError?.Invoke(this, new HidErrorEventArgs(this, ex));
+ throw ex;
+ }
+ });
+ }
+
+
+ public async Task GetFeatureReportAsync(byte reportId)
+ {
+ if (!_isOpen)
+ {
+ OnError?.Invoke(this, new HidErrorEventArgs(this, new InvalidOperationException("Device not open")));
+ return new HidReport(0, Array.Empty());
+ }
+
+ return await Task.Run(() =>
+ {
+ var buffer = new byte[FeatureReportByteLength];
+ buffer[0] = reportId;
+
+ bool success = HidD_GetFeature(_deviceHandle.DangerousGetHandle(), buffer, buffer.Length);
+ if (!success)
+ {
+ var err = Marshal.GetLastWin32Error();
+ var ex = new Win32Exception(err, "HidD_GetFeature failed");
+ OnError?.Invoke(this, new HidErrorEventArgs(this, ex));
+ return new HidReport(0, Array.Empty());
+ }
+
+ byte[] data = buffer.Skip(1).ToArray();
+ return new HidReport(reportId, data);
+ });
+ }
+
+
+ ///
+ /// Begin async reading loop raising OnDataReceived events on data input
+ ///
+ ///
+ ///
+ public async Task StartListeningAsync(CancellationToken cancellationToken)
+ {
+ if (!_isOpen)
+ {
+ OnError?.Invoke(this, new HidErrorEventArgs(this, new NotSupportedException("Device is not open.")));
+ return;
+ }
+
+ if (_listeningCts != null)
+ {
+ OnError?.Invoke(this, new HidErrorEventArgs(this, new NotSupportedException("Already listening on this device.")));
+ return;
+ }
+
+ _listeningCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
+
+ try
+ {
+ var token = _listeningCts.Token;
+
+ while (!token.IsCancellationRequested)
+ {
+ try
+ {
+ var inputReport = await ReadInputReportAsync(token);
+
+ if (inputReport?.Data?.Length > 0)
+ {
+ OnDataReceived?.Invoke(this, new HidDataReceivedEventArgs(this, inputReport));
+ }
+ }
+ catch (OperationCanceledException)
+ {
+ break;
+ }
+ catch (NotSupportedException)
+ {
+ OnError?.Invoke(this, new HidErrorEventArgs(this, new NotSupportedException("Reading input reports is not supported on this device.")));
+ break;
+ }
+ catch (Exception ex)
+ {
+ if (token.IsCancellationRequested)
+ {
+ break;
+ }
+
+ OnError?.Invoke(this, new HidErrorEventArgs(this, ex));
+ }
+ }
+ }
+ finally
+ {
+ _listeningCts.Dispose();
+ _listeningCts = null;
+ }
+ }
+
+ private Task ReadInputReportAsync(CancellationToken cancellationToken)
+ {
+ var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
+
+ var buffer = new byte[InputReportByteLength];
+
+ // Start async read
+ _deviceStream.BeginRead(buffer, 0, buffer.Length, ar =>
+ {
+ try
+ {
+ int bytesRead = _deviceStream.EndRead(ar);
+
+ if (bytesRead == 0)
+ {
+ // No data read, reportId 0 and empty data
+ tcs.SetResult(new HidReport(0, Array.Empty()));
+ }
+ else
+ {
+ // First byte is reportId, rest is data
+ byte reportId = buffer[0];
+ byte[] data = bytesRead > 1 ? buffer.Skip(1).Take(bytesRead - 1).ToArray() : Array.Empty();
+ tcs.SetResult(new HidReport(reportId, data));
+ }
+ }
+ catch (Exception ex)
+ {
+ tcs.SetException(ex);
+ }
+ }, null);
+
+ cancellationToken.Register(() =>
+ {
+ tcs.TrySetCanceled();
+ });
+
+ return tcs.Task;
+ }
+
+ private void StopListening()
+ {
+ if (_listeningCts != null)
+ {
+ _listeningCts.Cancel();
+ _listeningCts.Dispose();
+ _listeningCts = null;
+ }
+ }
+ }
}
\ No newline at end of file
diff --git a/EonaCat.HID/IHidManager.cs b/EonaCat.HID/IHidManager.cs
index b021b65..ffdcded 100644
--- a/EonaCat.HID/IHidManager.cs
+++ b/EonaCat.HID/IHidManager.cs
@@ -1,6 +1,7 @@
using EonaCat.HID.EventArguments;
using System;
using System.Collections.Generic;
+using System.Threading.Tasks;
namespace EonaCat.HID
{
@@ -15,7 +16,7 @@ namespace EonaCat.HID
///
/// Enumerate all connected HID devices matching optional VendorId/ProductId filters
///
- IEnumerable Enumerate(ushort? vendorId = null, ushort? productId = null);
+ Task> EnumerateAsync(ushort? vendorId = null, ushort? productId = null);
///
/// Event is raised when a HID device is inserted
diff --git a/EonaCat.HID/Managers/HidManagerLinux.cs b/EonaCat.HID/Managers/HidManagerLinux.cs
index 0f8a28a..9aa5fda 100644
--- a/EonaCat.HID/Managers/HidManagerLinux.cs
+++ b/EonaCat.HID/Managers/HidManagerLinux.cs
@@ -18,7 +18,12 @@ namespace EonaCat.HID.Managers.Linux
public event EventHandler OnDeviceRemoved;
public event EventHandler OnDeviceError;
- public IEnumerable Enumerate(ushort? vendorId = null, ushort? productId = null)
+ public Task> EnumerateAsync(ushort? vendorId = null, ushort? productId = null)
+ {
+ return Task.Run(() => Enumerate(vendorId, productId));
+ }
+
+ private IEnumerable Enumerate(ushort? vendorId = null, ushort? productId = null)
{
var hidrawDir = "/sys/class/hidraw/";
diff --git a/EonaCat.HID/Managers/HidManagerMac.cs b/EonaCat.HID/Managers/HidManagerMac.cs
index 7b291f3..e8cd347 100644
--- a/EonaCat.HID/Managers/HidManagerMac.cs
+++ b/EonaCat.HID/Managers/HidManagerMac.cs
@@ -2,6 +2,7 @@
using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;
+using System.Threading.Tasks;
using static EonaCat.HID.Managers.Mac.NativeMethods;
namespace EonaCat.HID.Managers.Mac
@@ -48,7 +49,12 @@ namespace EonaCat.HID.Managers.Mac
IOHIDManagerRegisterDeviceRemovalCallback(_hidManager, _deviceRemovedCallback, IntPtr.Zero);
}
- public IEnumerable Enumerate(ushort? vendorId = null, ushort? productId = null)
+ public async Task> EnumerateAsync(ushort? vendorId = null, ushort? productId = null)
+ {
+ return await Task.Run(() => Enumerate(vendorId, productId));
+ }
+
+ private IEnumerable Enumerate(ushort? vendorId = null, ushort? productId = null)
{
var devices = new List();
IntPtr cfSet = IOHIDManagerCopyDevices(_hidManager);
diff --git a/EonaCat.HID/Managers/HidManagerWindows.cs b/EonaCat.HID/Managers/HidManagerWindows.cs
index 9cdb69a..f7c6e37 100644
--- a/EonaCat.HID/Managers/HidManagerWindows.cs
+++ b/EonaCat.HID/Managers/HidManagerWindows.cs
@@ -5,6 +5,7 @@ using System.Collections.Generic;
using System.ComponentModel;
using System.IO;
using System.Runtime.InteropServices;
+using System.Threading.Tasks;
using static EonaCat.HID.Managers.Windows.NativeMethods;
namespace EonaCat.HID.Managers.Windows
@@ -112,13 +113,9 @@ namespace EonaCat.HID.Managers.Windows
var hdr = Marshal.PtrToStructure(lParam);
if (hdr.dbch_devicetype == DBT_DEVTYP_DEVICEINTERFACE)
{
- var devInterface = Marshal.PtrToStructure(lParam);
-
- // Calculate pointer to string
- IntPtr stringPtr = IntPtr.Add(lParam, Marshal.SizeOf());
+ var devInterface = Marshal.PtrToStructure(lParam);
+ string devicePath = Marshal.PtrToStringUni((IntPtr)((long)lParam + Marshal.OffsetOf("dbcc_classguid").ToInt64() + Marshal.SizeOf()));
- // Read null-terminated string from unmanaged memory
- string devicePath = Marshal.PtrToStringUni(stringPtr);
if (!string.IsNullOrEmpty(devicePath))
{
try
@@ -175,170 +172,133 @@ namespace EonaCat.HID.Managers.Windows
}
}
- public IEnumerable Enumerate(ushort? vendorId = null, ushort? productId = null)
- {
- var list = new List();
- IntPtr devInfo = SetupDiGetClassDevs(
- ref GUID_DEVINTERFACE_HID,
- null,
- IntPtr.Zero,
- DIGCF_PRESENT | DIGCF_DEVICEINTERFACE);
-
- if (devInfo == IntPtr.Zero || devInfo.ToInt64() == -1)
- {
- throw new Win32Exception(Marshal.GetLastWin32Error(), "SetupDiGetClassDevs failed");
- }
-
- try
- {
- var iface = new SP_DEVICE_INTERFACE_DATA
- {
- cbSize = Marshal.SizeOf(typeof(SP_DEVICE_INTERFACE_DATA))
- };
-
- for (uint index = 0; ; index++)
- {
- bool ok = SetupDiEnumDeviceInterfaces(
- devInfo, IntPtr.Zero, ref GUID_DEVINTERFACE_HID, index, ref iface);
-
- if (!ok)
- {
- int error = Marshal.GetLastWin32Error();
- if (error == ERROR_NO_MORE_ITEMS)
- {
- break;
- }
-
- throw new Win32Exception(error, "SetupDiEnumDeviceInterfaces failed");
- }
-
- // Step 1: Get required size
- uint requiredSize = 0;
- bool sizeResult = SetupDiGetDeviceInterfaceDetail(
- devInfo,
- ref iface,
- IntPtr.Zero,
- 0,
- ref requiredSize,
- IntPtr.Zero);
-
- // This call should fail with ERROR_INSUFFICIENT_BUFFER
- int sizeError = Marshal.GetLastWin32Error();
- if (sizeError != ERROR_INSUFFICIENT_BUFFER || requiredSize == 0)
- {
- continue;
- }
-
- // Step 2: Allocate buffer for detail data
- IntPtr detailDataBuffer = Marshal.AllocHGlobal((int)requiredSize);
- try
- {
- // Step 3: Set cbSize at start of allocated memory
- // CRITICAL: cbSize must be size of SP_DEVICE_INTERFACE_DETAIL_DATA structure
- // On x86: 6 bytes (4 for cbSize + 2 for alignment)
- // On x64: 8 bytes (4 for cbSize + 4 for alignment)
- int cbSize = IntPtr.Size == 8 ? 8 : 6;
- Marshal.WriteInt32(detailDataBuffer, cbSize);
-
- // Step 4: Now get the device interface detail
- bool success = SetupDiGetDeviceInterfaceDetail(
- devInfo,
- ref iface,
- detailDataBuffer,
- requiredSize,
- ref requiredSize,
- IntPtr.Zero);
-
- if (!success)
- {
- int detailError = Marshal.GetLastWin32Error();
- throw new Win32Exception(detailError, $"SetupDiGetDeviceInterfaceDetail failed with error {detailError}");
- }
-
- // Step 5: Read device path string (starts at offset 4)
- // The DevicePath is a null-terminated string that starts at offset 4
- IntPtr pDevicePathName = IntPtr.Add(detailDataBuffer, 4);
- string devicePath = Marshal.PtrToStringAuto(pDevicePathName);
-
- if (string.IsNullOrEmpty(devicePath))
- {
- continue;
- }
-
- // Step 6: Create device, filter and add
- try
- {
- // First try to open the device with minimal access to check if it's accessible
- using (var testHandle = CreateFile(devicePath, 0, // No access requested
- FileShare.ReadWrite, IntPtr.Zero, FileMode.Open, 0, IntPtr.Zero))
- {
- if (testHandle.IsInvalid)
- {
- // Device not accessible, skip it
- continue;
- }
- }
-
- var device = new HidWindows(devicePath);
- device.Setup();
-
- if (vendorId.HasValue && device.VendorId != vendorId.Value ||
- productId.HasValue && device.ProductId != productId.Value)
- {
- device.Dispose();
- continue;
- }
-
- list.Add(device);
- }
- catch (UnauthorizedAccessException)
- {
- // Device is in use or access denied - skip silently
- continue;
- }
- catch (Exception ex) when (ex.Message.Contains("HidP_GetCaps") ||
- ex.Message.Contains("The parameter is incorrect") ||
- ex.Message.Contains("Access is denied"))
- {
- // Common HID access failures - skip these devices
- System.Diagnostics.Debug.WriteLine($"Skipping inaccessible HID device {devicePath}: {ex.Message}");
- continue;
- }
- catch (Exception ex)
- {
- // Other unexpected errors - log but continue
- System.Diagnostics.Debug.WriteLine($"Failed to create device for path {devicePath}: {ex.Message}");
- continue;
- }
- }
- finally
- {
- Marshal.FreeHGlobal(detailDataBuffer);
- }
- }
- }
- finally
- {
- SetupDiDestroyDeviceInfoList(devInfo);
- }
-
- return list;
+ public Task> EnumerateAsync(ushort? vendorId = null, ushort? productId = null)
+ {
+ return Task.Run(() => Enumerate(vendorId, productId));
}
- public void Dispose()
- {
- if (_deviceNotificationHandle != IntPtr.Zero)
- {
- UnregisterDeviceNotification(_deviceNotificationHandle);
- _deviceNotificationHandle = IntPtr.Zero;
- }
+ private IEnumerable Enumerate(ushort? vendorId = null, ushort? productId = null)
+ {
+ var list = new List();
+
+ IntPtr devInfo = SetupDiGetClassDevs(
+ ref GUID_DEVINTERFACE_HID,
+ null,
+ IntPtr.Zero,
+ DIGCF_PRESENT | DIGCF_DEVICEINTERFACE);
+
+ if (devInfo == IntPtr.Zero || devInfo == new IntPtr(-1))
+ throw new Win32Exception(Marshal.GetLastWin32Error(), "SetupDiGetClassDevs failed");
+
+ try
+ {
+ var iface = new SP_DEVICE_INTERFACE_DATA
+ {
+ cbSize = Marshal.SizeOf(typeof(SP_DEVICE_INTERFACE_DATA))
+ };
+
+ for (uint index = 0; ; index++)
+ {
+ if (!SetupDiEnumDeviceInterfaces(devInfo, IntPtr.Zero, ref GUID_DEVINTERFACE_HID, index, ref iface))
+ {
+ int error = Marshal.GetLastWin32Error();
+ if (error == ERROR_NO_MORE_ITEMS)
+ break;
+
+ throw new Win32Exception(error, "SetupDiEnumDeviceInterfaces failed");
+ }
+
+ uint requiredSize = 0;
+ SetupDiGetDeviceInterfaceDetail(devInfo, ref iface, IntPtr.Zero, 0, ref requiredSize, IntPtr.Zero);
+ if (Marshal.GetLastWin32Error() != ERROR_INSUFFICIENT_BUFFER || requiredSize == 0)
+ continue;
+
+ IntPtr detailDataBuffer = Marshal.AllocHGlobal((int)requiredSize);
+ try
+ {
+ int cbSize = IntPtr.Size == 8 ? 8 : 6;
+ Marshal.WriteInt32(detailDataBuffer, cbSize);
+
+ if (!SetupDiGetDeviceInterfaceDetail(devInfo, ref iface, detailDataBuffer, requiredSize, ref requiredSize, IntPtr.Zero))
+ {
+ System.Diagnostics.Debug.WriteLine($"Detail fetch failed: {Marshal.GetLastWin32Error()}");
+ continue;
+ }
+
+ IntPtr pDevicePathName = IntPtr.Add(detailDataBuffer, 4);
+ string devicePath = Marshal.PtrToStringAuto(pDevicePathName);
+
+ if (string.IsNullOrWhiteSpace(devicePath))
+ continue;
+
+ // Try to open with zero access to ensure it’s reachable
+ using (var testHandle = CreateFile(devicePath, 0, FileShare.ReadWrite, IntPtr.Zero, FileMode.Open, 0, IntPtr.Zero))
+ {
+ if (testHandle.IsInvalid)
+ continue;
+ }
+
+ HidWindows device;
+ try
+ {
+ device = new HidWindows(devicePath);
+ device.Setup();
+ }
+ catch (Exception ex)
+ {
+ System.Diagnostics.Debug.WriteLine($"Skipping inaccessible HID device {devicePath}: {ex.Message}");
+ continue;
+ }
+
+ if ((vendorId.HasValue && device.VendorId != vendorId.Value) ||
+ (productId.HasValue && device.ProductId != productId.Value))
+ {
+ device.Dispose();
+ continue;
+ }
+
+ list.Add(device);
+ }
+ finally
+ {
+ Marshal.FreeHGlobal(detailDataBuffer);
+ }
+ }
+ }
+ finally
+ {
+ SetupDiDestroyDeviceInfoList(devInfo);
+ }
+
+ return list;
+ }
+
+
+ public void Dispose()
+ {
+ lock (_lock)
+ {
+ if (_deviceNotificationHandle != IntPtr.Zero)
+ {
+ UnregisterDeviceNotification(_deviceNotificationHandle);
+ _deviceNotificationHandle = IntPtr.Zero;
+ }
+
+ if (_messageWindowHandle != IntPtr.Zero)
+ {
+ DestroyWindow(_messageWindowHandle);
+ _messageWindowHandle = IntPtr.Zero;
+ }
+
+ foreach (var device in _knownDevices.Values)
+ {
+ device.Dispose();
+ }
+
+ _knownDevices.Clear();
+ }
+ }
- if (_messageWindowHandle != IntPtr.Zero)
- {
- DestroyWindow(_messageWindowHandle);
- _messageWindowHandle = IntPtr.Zero;
- }
- }
}
internal static class NativeMethods