Updated Write output

This commit is contained in:
Jeroen Saey 2025-07-28 13:52:56 +02:00
parent fe283384af
commit 19e713eb21
8 changed files with 619 additions and 650 deletions

View File

@ -40,9 +40,9 @@ namespace EonaCat.HID.Analyzer
_deviceManager.OnDeviceRemoved += Hid_Removed; _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) private void MainForm_Load(object sender, System.EventArgs e)
@ -61,7 +61,7 @@ namespace EonaCat.HID.Analyzer
{ {
try try
{ {
RefreshDevices(); RefreshDevicesAsync();
UpdateDeviceList(); UpdateDeviceList();
toolStripStatusLabel1.Text = "Please select device and click open to start."; toolStripStatusLabel1.Text = "Please select device and click open to start.";
} }
@ -182,7 +182,7 @@ namespace EonaCat.HID.Analyzer
{ {
try try
{ {
RefreshDevices(); RefreshDevicesAsync();
UpdateDeviceList(); UpdateDeviceList();
} }
catch (Exception ex) catch (Exception ex)
@ -203,7 +203,7 @@ namespace EonaCat.HID.Analyzer
vid = ushort.Parse(str[0], NumberStyles.AllowHexSpecifier); vid = ushort.Parse(str[0], NumberStyles.AllowHexSpecifier);
pid = ushort.Parse(str[1], NumberStyles.AllowHexSpecifier); pid = ushort.Parse(str[1], NumberStyles.AllowHexSpecifier);
} }
RefreshDevices(vid, pid); RefreshDevicesAsync(vid, pid);
UpdateDeviceList(); UpdateDeviceList();
} }
catch (Exception ex) catch (Exception ex)

View File

@ -34,7 +34,7 @@ namespace EonaCat.HID.Example
Console.WriteLine($"Removed Device --> VID: {e.Device.VendorId:X4}, PID: {e.Device.ProductId:X4}"); Console.WriteLine($"Removed Device --> VID: {e.Device.VendorId:X4}, PID: {e.Device.ProductId:X4}");
}; };
RefreshDevices(); RefreshDevicesAsync();
if (!_deviceList.Any()) 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() static void DisplayDevices()

View File

@ -7,7 +7,7 @@
<Copyright>Copyright 2024 EonaCat (Jeroen Saey)</Copyright> <Copyright>Copyright 2024 EonaCat (Jeroen Saey)</Copyright>
<LangVersion>latest</LangVersion> <LangVersion>latest</LangVersion>
<PackageId>EonaCat.HID</PackageId> <PackageId>EonaCat.HID</PackageId>
<Version>1.0.4</Version> <Version>1.0.5</Version>
<Title>EonaCat.HID</Title> <Title>EonaCat.HID</Title>
<Authors>EonaCat (Jeroen Saey)</Authors> <Authors>EonaCat (Jeroen Saey)</Authors>
<Description>HID Devices</Description> <Description>HID Devices</Description>

View File

@ -1,472 +1,469 @@
using EonaCat.HID.EventArguments; using EonaCat.HID.EventArguments;
using EonaCat.HID.Managers.Windows; using EonaCat.HID.Managers.Windows;
using EonaCat.HID.Models; using EonaCat.HID.Models;
using Microsoft.Win32.SafeHandles; using Microsoft.Win32.SafeHandles;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.ComponentModel; using System.ComponentModel;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Text; using System.Text;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using static EonaCat.HID.Managers.Windows.NativeMethods; using static EonaCat.HID.Managers.Windows.NativeMethods;
namespace EonaCat.HID namespace EonaCat.HID
{ {
// This file is part of the EonaCat project(s) which is released under the Apache License. // 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. // See the LICENSE file or go to https://EonaCat.com/license for full license details.
internal sealed class HidWindows : IHid internal sealed class HidWindows : IHid
{ {
private SafeFileHandle _deviceHandle; private SafeFileHandle _deviceHandle;
private FileStream _deviceStream; private FileStream _deviceStream;
private IntPtr _preparsedData; private IntPtr _preparsedData;
private bool _isOpen; private bool _isOpen;
private readonly string _devicePath; private readonly string _devicePath;
public string DevicePath => _devicePath; public string DevicePath => _devicePath;
public ushort VendorId { get; private set; } public ushort VendorId { get; private set; }
public ushort ProductId { get; private set; } public ushort ProductId { get; private set; }
public string SerialNumber { get; private set; } public string SerialNumber { get; private set; }
public string Manufacturer { get; private set; } public string Manufacturer { get; private set; }
public string ProductName { get; private set; } public string ProductName { get; private set; }
public int InputReportByteLength { get; private set; } public int InputReportByteLength { get; private set; }
public int OutputReportByteLength { get; private set; } public int OutputReportByteLength { get; private set; }
public int FeatureReportByteLength { get; private set; } public int FeatureReportByteLength { get; private set; }
public bool IsConnected => _isOpen; public bool IsConnected => _isOpen;
public IDictionary<string, object> Capabilities { get; } = new Dictionary<string, object>(); public IDictionary<string, object> Capabilities { get; } = new Dictionary<string, object>();
public bool IsReadingSupport { get; private set; } public bool IsReadingSupport { get; private set; }
public bool IsWritingSupport { get; private set; } public bool IsWritingSupport { get; private set; }
public event EventHandler<HidDataReceivedEventArgs> OnDataReceived; public event EventHandler<HidDataReceivedEventArgs> OnDataReceived;
public event EventHandler<HidErrorEventArgs> OnError; public event EventHandler<HidErrorEventArgs> OnError;
private CancellationTokenSource _listeningCts; private CancellationTokenSource _listeningCts;
private readonly object _lockObject = new object(); private readonly object _lockObject = new object();
public HidWindows(string devicePath) public HidWindows(string devicePath)
{ {
_devicePath = devicePath ?? throw new ArgumentNullException(nameof(devicePath)); _devicePath = devicePath ?? throw new ArgumentNullException(nameof(devicePath));
} }
internal void Setup() internal void Setup()
{ {
Open(); Open();
LoadDeviceAttributes(); LoadDeviceAttributes();
Close(); Close();
} }
private void LoadDeviceAttributes() private void LoadDeviceAttributes()
{ {
Manufacturer = GetStringDescriptor(HidD_GetManufacturerString); Manufacturer = GetStringDescriptor(HidD_GetManufacturerString);
ProductName = GetStringDescriptor(HidD_GetProductString); ProductName = GetStringDescriptor(HidD_GetProductString);
SerialNumber = GetStringDescriptor(HidD_GetSerialNumberString); SerialNumber = GetStringDescriptor(HidD_GetSerialNumberString);
HidDeviceAttributes attr = GetDeviceAttributes(); HidDeviceAttributes attr = GetDeviceAttributes();
VendorId = attr.VendorID; VendorId = attr.VendorID;
ProductId = attr.ProductID; ProductId = attr.ProductID;
} }
/// <summary> /// <summary>
/// Open the device for I/O /// Open the device for I/O
/// </summary> /// </summary>
public void Open() public void Open()
{ {
if (_isOpen) if (_isOpen)
return; return;
FileAccess access = FileAccess.ReadWrite; FileAccess access = FileAccess.ReadWrite;
SafeFileHandle handle = TryOpenDevice(GENERIC_READ | GENERIC_WRITE); SafeFileHandle handle = TryOpenDevice(GENERIC_READ | GENERIC_WRITE);
if (handle == null || handle.IsInvalid) if (handle == null || handle.IsInvalid)
{ {
handle = TryOpenDevice(GENERIC_READ); handle = TryOpenDevice(GENERIC_READ);
if (handle != null && !handle.IsInvalid) if (handle != null && !handle.IsInvalid)
access = FileAccess.Read; access = FileAccess.Read;
} }
if ((handle == null || handle.IsInvalid) && Environment.Is64BitOperatingSystem) if ((handle == null || handle.IsInvalid) && Environment.Is64BitOperatingSystem)
{ {
handle = TryOpenDevice(GENERIC_WRITE); handle = TryOpenDevice(GENERIC_WRITE);
if (handle != null && !handle.IsInvalid) if (handle != null && !handle.IsInvalid)
access = FileAccess.Write; access = FileAccess.Write;
} }
if (handle == null || handle.IsInvalid) if (handle == null || handle.IsInvalid)
{ {
int err = Marshal.GetLastWin32Error(); int err = Marshal.GetLastWin32Error();
OnError?.Invoke(this, new HidErrorEventArgs(this, OnError?.Invoke(this, new HidErrorEventArgs(this,
new Win32Exception(err, $"Cannot open device {_devicePath} with any access mode"))); new Win32Exception(err, $"Cannot open device {_devicePath} with any access mode")));
return; return;
} }
_deviceHandle = handle; _deviceHandle = handle;
_deviceStream = new FileStream(_deviceHandle, access, bufferSize: 64, isAsync: true); _deviceStream = new FileStream(_deviceHandle, access, bufferSize: 64, isAsync: true);
_isOpen = true; _isOpen = true;
// HID descriptor // HID descriptor
if (!HidD_GetPreparsedData(_deviceHandle.DangerousGetHandle(), out _preparsedData)) if (!HidD_GetPreparsedData(_deviceHandle.DangerousGetHandle(), out _preparsedData))
throw new Win32Exception(Marshal.GetLastWin32Error(), "Failed HidD_GetPreparsedData"); throw new Win32Exception(Marshal.GetLastWin32Error(), "Failed HidD_GetPreparsedData");
HIDP_CAPS caps; HIDP_CAPS caps;
int capsRes = HidP_GetCaps(_preparsedData, out caps); int capsRes = HidP_GetCaps(_preparsedData, out caps);
if (capsRes != NativeMethods.HIDP_STATUS_SUCCESS) if (capsRes != NativeMethods.HIDP_STATUS_SUCCESS)
throw new Win32Exception(capsRes, "Failed HidP_GetCaps"); throw new Win32Exception(capsRes, "Failed HidP_GetCaps");
InputReportByteLength = caps.InputReportByteLength; InputReportByteLength = caps.InputReportByteLength;
OutputReportByteLength = caps.OutputReportByteLength; OutputReportByteLength = caps.OutputReportByteLength;
FeatureReportByteLength = caps.FeatureReportByteLength; FeatureReportByteLength = caps.FeatureReportByteLength;
Capabilities["Usage"] = caps.Usage; Capabilities["Usage"] = caps.Usage;
Capabilities["UsagePage"] = caps.UsagePage; Capabilities["UsagePage"] = caps.UsagePage;
Capabilities["InputReportByteLength"] = InputReportByteLength; Capabilities["InputReportByteLength"] = InputReportByteLength;
Capabilities["OutputReportByteLength"] = OutputReportByteLength; Capabilities["OutputReportByteLength"] = OutputReportByteLength;
Capabilities["FeatureReportByteLength"] = FeatureReportByteLength; Capabilities["FeatureReportByteLength"] = FeatureReportByteLength;
Manufacturer = GetStringDescriptor(HidD_GetManufacturerString); Manufacturer = GetStringDescriptor(HidD_GetManufacturerString);
ProductName = GetStringDescriptor(HidD_GetProductString); ProductName = GetStringDescriptor(HidD_GetProductString);
SerialNumber = GetStringDescriptor(HidD_GetSerialNumberString); SerialNumber = GetStringDescriptor(HidD_GetSerialNumberString);
IsReadingSupport = (access == FileAccess.Read || access == FileAccess.ReadWrite); IsReadingSupport = (access == FileAccess.Read || access == FileAccess.ReadWrite);
IsWritingSupport = (access == FileAccess.Write || access == FileAccess.ReadWrite); IsWritingSupport = (access == FileAccess.Write || access == FileAccess.ReadWrite);
HidDeviceAttributes attr = GetDeviceAttributes(); HidDeviceAttributes attr = GetDeviceAttributes();
VendorId = attr.VendorID; VendorId = attr.VendorID;
ProductId = attr.ProductID; ProductId = attr.ProductID;
} }
private SafeFileHandle TryOpenDevice(int access) private SafeFileHandle TryOpenDevice(int access)
{ {
var handle = CreateFile(_devicePath, var handle = CreateFile(_devicePath,
access, access,
FILE_SHARE_READ | FILE_SHARE_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE,
IntPtr.Zero, IntPtr.Zero,
OPEN_EXISTING, OPEN_EXISTING,
FILE_FLAG_OVERLAPPED, FILE_FLAG_OVERLAPPED,
IntPtr.Zero); IntPtr.Zero);
return handle; return handle;
} }
/// <summary> /// <summary>
/// Close the device /// Close the device
/// </summary> /// </summary>
public void Close() public void Close()
{ {
lock (_lockObject) lock (_lockObject)
{ {
if (!_isOpen) if (!_isOpen)
{ {
return; return;
} }
StopListening(); StopListening();
if (_preparsedData != IntPtr.Zero) if (_preparsedData != IntPtr.Zero)
{ {
HidD_FreePreparsedData(_preparsedData); HidD_FreePreparsedData(_preparsedData);
_preparsedData = IntPtr.Zero; _preparsedData = IntPtr.Zero;
} }
_deviceStream?.Dispose(); _deviceStream?.Dispose();
_deviceStream = null; _deviceStream = null;
_deviceHandle?.Dispose(); _deviceHandle?.Dispose();
_deviceHandle = null; _deviceHandle = null;
_isOpen = false; _isOpen = false;
} }
} }
private HidDeviceAttributes GetDeviceAttributes() private HidDeviceAttributes GetDeviceAttributes()
{ {
HidDeviceAttributes attr = new HidDeviceAttributes HidDeviceAttributes attr = new HidDeviceAttributes
{ {
Size = Marshal.SizeOf<HidDeviceAttributes>() Size = Marshal.SizeOf<HidDeviceAttributes>()
}; };
if (!HidD_GetAttributes(_deviceHandle.DangerousGetHandle(), ref attr)) if (!HidD_GetAttributes(_deviceHandle.DangerousGetHandle(), ref attr))
{ {
throw new Win32Exception(Marshal.GetLastWin32Error(), "Failed HidD_GetAttributes"); throw new Win32Exception(Marshal.GetLastWin32Error(), "Failed HidD_GetAttributes");
} }
return attr; return attr;
} }
private string GetStringDescriptor(Func<IntPtr, IntPtr, int, bool> stringFunc) private string GetStringDescriptor(Func<IntPtr, IntPtr, int, bool> stringFunc)
{ {
var buffer = new byte[126 * 2]; // Unicode max buffer var buffer = new byte[126 * 2]; // Unicode max buffer
GCHandle gc = GCHandle.Alloc(buffer, GCHandleType.Pinned); GCHandle gc = GCHandle.Alloc(buffer, GCHandleType.Pinned);
try try
{ {
bool success = stringFunc(_deviceHandle.DangerousGetHandle(), gc.AddrOfPinnedObject(), buffer.Length); bool success = stringFunc(_deviceHandle.DangerousGetHandle(), gc.AddrOfPinnedObject(), buffer.Length);
if (!success) if (!success)
{ {
return null; return null;
} }
string str = Encoding.Unicode.GetString(buffer); string str = Encoding.Unicode.GetString(buffer);
int idx = str.IndexOf('\0'); int idx = str.IndexOf('\0');
if (idx >= 0) if (idx >= 0)
{ {
str = str.Substring(0, idx); str = str.Substring(0, idx);
} }
return str; return str;
} }
finally finally
{ {
gc.Free(); gc.Free();
} }
} }
public void Dispose() public void Dispose()
{ {
Close(); Close();
} }
public async Task WriteOutputReportAsync(HidReport report) public async Task WriteOutputReportAsync(HidReport report)
{ {
if (!_isOpen) if (!_isOpen || _deviceStream == null)
{ {
OnError?.Invoke(this, new HidErrorEventArgs(this, new InvalidOperationException("Device not open"))); OnError?.Invoke(this, new HidErrorEventArgs(this, new InvalidOperationException("Device not open or stream is null")));
return; return;
} }
if (report == null) if (report?.Data == null || report.Data.Length == 0)
{ {
OnError?.Invoke(this, new HidErrorEventArgs(this, new ArgumentNullException(nameof(report)))); OnError?.Invoke(this, new HidErrorEventArgs(this, new ArgumentException("Invalid report or data")));
return; return;
} }
if (report.Data == null || report.Data.Length == 0) try
{ {
OnError?.Invoke(this, new HidErrorEventArgs(this, new ArgumentException("Data cannot be null or empty", nameof(report)))); int reportLength = report.Data.Length + 1;
return; var buffer = new byte[reportLength];
} buffer[0] = report.ReportId;
Array.Copy(report.Data, 0, buffer, 1, report.Data.Length);
try
{ await _deviceStream.WriteAsync(buffer, 0, buffer.Length);
// Combine reportId and data into one buffer for sending }
var buffer = new byte[1 + report.Data.Length]; catch (IOException ioEx)
buffer[0] = report.ReportId; {
Array.Copy(report.Data, 0, buffer, 1, report.Data.Length); OnError?.Invoke(this, new HidErrorEventArgs(this, ioEx));
}
await _deviceStream.WriteAsync(buffer, 0, buffer.Length); catch (Exception ex)
await _deviceStream.FlushAsync(); {
} OnError?.Invoke(this, new HidErrorEventArgs(this, ex));
catch (Exception ex) }
{ }
OnError?.Invoke(this, new HidErrorEventArgs(this, ex));
throw;
} public async Task<HidReport> ReadInputReportAsync()
} {
if (!_isOpen)
public async Task<HidReport> ReadInputReportAsync() {
{ OnError?.Invoke(this, new HidErrorEventArgs(this, new InvalidOperationException("Device not open")));
if (!_isOpen) return new HidReport(0, Array.Empty<byte>());
{ }
OnError?.Invoke(this, new HidErrorEventArgs(this, new InvalidOperationException("Device not open")));
return new HidReport(0, Array.Empty<byte>()); return await Task.Run(async () =>
} {
var buffer = new byte[InputReportByteLength];
return await Task.Run(async () =>
{ try
var buffer = new byte[InputReportByteLength]; {
int read = await _deviceStream.ReadAsync(buffer, 0, buffer.Length);
try if (read == 0)
{ {
int read = await _deviceStream.ReadAsync(buffer, 0, buffer.Length); OnError?.Invoke(this, new HidErrorEventArgs(this, new IOException("No data read from device")));
if (read == 0) return new HidReport(0, Array.Empty<byte>());
{ }
OnError?.Invoke(this, new HidErrorEventArgs(this, new IOException("No data read from device")));
return new HidReport(0, Array.Empty<byte>()); byte reportId = buffer[0];
} byte[] data = buffer.Skip(1).Take(read - 1).ToArray();
byte reportId = buffer[0]; return new HidReport(reportId, data);
byte[] data = buffer.Skip(1).Take(read - 1).ToArray(); }
catch (Exception ex)
return new HidReport(reportId, data); {
} OnError?.Invoke(this, new HidErrorEventArgs(this, ex));
catch (Exception ex) throw;
{ }
OnError?.Invoke(this, new HidErrorEventArgs(this, ex)); });
throw; }
}
});
} public async Task SendFeatureReportAsync(HidReport report)
{
if (!_isOpen)
public async Task SendFeatureReportAsync(HidReport report) {
{ OnError?.Invoke(this, new HidErrorEventArgs(this, new InvalidOperationException("Device not open")));
if (!_isOpen) return;
{ }
OnError?.Invoke(this, new HidErrorEventArgs(this, new InvalidOperationException("Device not open")));
return; if (report == null)
} throw new ArgumentNullException(nameof(report));
if (report == null) // Prepare buffer with ReportId + Data
throw new ArgumentNullException(nameof(report)); var data = new byte[1 + report.Data.Length];
data[0] = report.ReportId;
// Prepare buffer with ReportId + Data Array.Copy(report.Data, 0, data, 1, report.Data.Length);
var data = new byte[1 + report.Data.Length];
data[0] = report.ReportId; await Task.Run(() =>
Array.Copy(report.Data, 0, data, 1, report.Data.Length); {
bool success = HidD_SetFeature(_deviceHandle.DangerousGetHandle(), data, data.Length);
await Task.Run(() => if (!success)
{ {
bool success = HidD_SetFeature(_deviceHandle.DangerousGetHandle(), data, data.Length); var err = Marshal.GetLastWin32Error();
if (!success) var ex = new Win32Exception(err, "HidD_SetFeature failed");
{ OnError?.Invoke(this, new HidErrorEventArgs(this, ex));
var err = Marshal.GetLastWin32Error(); throw ex;
var ex = new Win32Exception(err, "HidD_SetFeature failed"); }
OnError?.Invoke(this, new HidErrorEventArgs(this, ex)); });
throw ex; }
}
});
} public async Task<HidReport> GetFeatureReportAsync(byte reportId)
{
if (!_isOpen)
public async Task<HidReport> GetFeatureReportAsync(byte reportId) {
{ OnError?.Invoke(this, new HidErrorEventArgs(this, new InvalidOperationException("Device not open")));
if (!_isOpen) return new HidReport(0, Array.Empty<byte>());
{ }
OnError?.Invoke(this, new HidErrorEventArgs(this, new InvalidOperationException("Device not open")));
return new HidReport(0, Array.Empty<byte>()); return await Task.Run(() =>
} {
var buffer = new byte[FeatureReportByteLength];
return await Task.Run(() => buffer[0] = reportId;
{
var buffer = new byte[FeatureReportByteLength]; bool success = HidD_GetFeature(_deviceHandle.DangerousGetHandle(), buffer, buffer.Length);
buffer[0] = reportId; if (!success)
{
bool success = HidD_GetFeature(_deviceHandle.DangerousGetHandle(), buffer, buffer.Length); var err = Marshal.GetLastWin32Error();
if (!success) var ex = new Win32Exception(err, "HidD_GetFeature failed");
{ OnError?.Invoke(this, new HidErrorEventArgs(this, ex));
var err = Marshal.GetLastWin32Error(); return new HidReport(0, Array.Empty<byte>());
var ex = new Win32Exception(err, "HidD_GetFeature failed"); }
OnError?.Invoke(this, new HidErrorEventArgs(this, ex));
return new HidReport(0, Array.Empty<byte>()); byte[] data = buffer.Skip(1).ToArray();
} return new HidReport(reportId, data);
});
byte[] data = buffer.Skip(1).ToArray(); }
return new HidReport(reportId, data);
});
} /// <summary>
/// Begin async reading loop raising OnDataReceived events on data input
/// </summary>
/// <summary> /// <param name="cancellationToken"></param>
/// Begin async reading loop raising OnDataReceived events on data input /// <returns></returns>
/// </summary> public async Task StartListeningAsync(CancellationToken cancellationToken)
/// <param name="cancellationToken"></param> {
/// <returns></returns> if (!_isOpen)
public async Task StartListeningAsync(CancellationToken cancellationToken) {
{ OnError?.Invoke(this, new HidErrorEventArgs(this, new NotSupportedException("Device is not open.")));
if (!_isOpen) return;
{ }
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.")));
if (_listeningCts != null) return;
{ }
OnError?.Invoke(this, new HidErrorEventArgs(this, new NotSupportedException("Already listening on this device.")));
return; _listeningCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
}
try
_listeningCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); {
var token = _listeningCts.Token;
try
{ while (!token.IsCancellationRequested)
var token = _listeningCts.Token; {
try
while (!token.IsCancellationRequested) {
{ var inputReport = await ReadInputReportAsync(token);
try
{ if (inputReport?.Data?.Length > 0)
var inputReport = await ReadInputReportAsync(token); {
OnDataReceived?.Invoke(this, new HidDataReceivedEventArgs(this, inputReport));
if (inputReport?.Data?.Length > 0) }
{ }
OnDataReceived?.Invoke(this, new HidDataReceivedEventArgs(this, inputReport)); catch (OperationCanceledException)
} {
} break;
catch (OperationCanceledException) }
{ catch (NotSupportedException)
break; {
} OnError?.Invoke(this, new HidErrorEventArgs(this, new NotSupportedException("Reading input reports is not supported on this device.")));
catch (NotSupportedException) break;
{ }
OnError?.Invoke(this, new HidErrorEventArgs(this, new NotSupportedException("Reading input reports is not supported on this device."))); catch (Exception ex)
break; {
} if (token.IsCancellationRequested)
catch (Exception ex) {
{ break;
if (token.IsCancellationRequested) }
{
break; OnError?.Invoke(this, new HidErrorEventArgs(this, ex));
} }
}
OnError?.Invoke(this, new HidErrorEventArgs(this, ex)); }
} finally
} {
} _listeningCts.Dispose();
finally _listeningCts = null;
{ }
_listeningCts.Dispose(); }
_listeningCts = null;
} private Task<HidReport> ReadInputReportAsync(CancellationToken cancellationToken)
} {
var tcs = new TaskCompletionSource<HidReport>(TaskCreationOptions.RunContinuationsAsynchronously);
private Task<HidReport> ReadInputReportAsync(CancellationToken cancellationToken)
{ var buffer = new byte[InputReportByteLength];
var tcs = new TaskCompletionSource<HidReport>(TaskCreationOptions.RunContinuationsAsynchronously);
// Start async read
var buffer = new byte[InputReportByteLength]; _deviceStream.BeginRead(buffer, 0, buffer.Length, ar =>
{
// Start async read try
_deviceStream.BeginRead(buffer, 0, buffer.Length, ar => {
{ int bytesRead = _deviceStream.EndRead(ar);
try
{ if (bytesRead == 0)
int bytesRead = _deviceStream.EndRead(ar); {
// No data read, reportId 0 and empty data
if (bytesRead == 0) tcs.SetResult(new HidReport(0, Array.Empty<byte>()));
{ }
// No data read, reportId 0 and empty data else
tcs.SetResult(new HidReport(0, Array.Empty<byte>())); {
} // First byte is reportId, rest is data
else byte reportId = buffer[0];
{ byte[] data = bytesRead > 1 ? buffer.Skip(1).Take(bytesRead - 1).ToArray() : Array.Empty<byte>();
// First byte is reportId, rest is data tcs.SetResult(new HidReport(reportId, data));
byte reportId = buffer[0]; }
byte[] data = bytesRead > 1 ? buffer.Skip(1).Take(bytesRead - 1).ToArray() : Array.Empty<byte>(); }
tcs.SetResult(new HidReport(reportId, data)); catch (Exception ex)
} {
} tcs.SetException(ex);
catch (Exception ex) }
{ }, null);
tcs.SetException(ex);
} cancellationToken.Register(() =>
}, null); {
tcs.TrySetCanceled();
cancellationToken.Register(() => });
{
tcs.TrySetCanceled(); return tcs.Task;
}); }
return tcs.Task; private void StopListening()
} {
if (_listeningCts != null)
private void StopListening() {
{ _listeningCts.Cancel();
if (_listeningCts != null) _listeningCts.Dispose();
{ _listeningCts = null;
_listeningCts.Cancel(); }
_listeningCts.Dispose(); }
_listeningCts = null; }
}
}
}
} }

View File

@ -1,6 +1,7 @@
using EonaCat.HID.EventArguments; using EonaCat.HID.EventArguments;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Threading.Tasks;
namespace EonaCat.HID namespace EonaCat.HID
{ {
@ -15,7 +16,7 @@ namespace EonaCat.HID
/// <summary> /// <summary>
/// Enumerate all connected HID devices matching optional VendorId/ProductId filters /// Enumerate all connected HID devices matching optional VendorId/ProductId filters
/// </summary> /// </summary>
IEnumerable<IHid> Enumerate(ushort? vendorId = null, ushort? productId = null); Task<IEnumerable<IHid>> EnumerateAsync(ushort? vendorId = null, ushort? productId = null);
/// <summary> /// <summary>
/// Event is raised when a HID device is inserted /// Event is raised when a HID device is inserted

View File

@ -18,7 +18,12 @@ namespace EonaCat.HID.Managers.Linux
public event EventHandler<HidEventArgs> OnDeviceRemoved; public event EventHandler<HidEventArgs> OnDeviceRemoved;
public event EventHandler<string> OnDeviceError; public event EventHandler<string> OnDeviceError;
public IEnumerable<IHid> Enumerate(ushort? vendorId = null, ushort? productId = null) public Task<IEnumerable<IHid>> EnumerateAsync(ushort? vendorId = null, ushort? productId = null)
{
return Task.Run(() => Enumerate(vendorId, productId));
}
private IEnumerable<IHid> Enumerate(ushort? vendorId = null, ushort? productId = null)
{ {
var hidrawDir = "/sys/class/hidraw/"; var hidrawDir = "/sys/class/hidraw/";

View File

@ -2,6 +2,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Threading.Tasks;
using static EonaCat.HID.Managers.Mac.NativeMethods; using static EonaCat.HID.Managers.Mac.NativeMethods;
namespace EonaCat.HID.Managers.Mac namespace EonaCat.HID.Managers.Mac
@ -48,7 +49,12 @@ namespace EonaCat.HID.Managers.Mac
IOHIDManagerRegisterDeviceRemovalCallback(_hidManager, _deviceRemovedCallback, IntPtr.Zero); IOHIDManagerRegisterDeviceRemovalCallback(_hidManager, _deviceRemovedCallback, IntPtr.Zero);
} }
public IEnumerable<IHid> Enumerate(ushort? vendorId = null, ushort? productId = null) public async Task<IEnumerable<IHid>> EnumerateAsync(ushort? vendorId = null, ushort? productId = null)
{
return await Task.Run(() => Enumerate(vendorId, productId));
}
private IEnumerable<IHid> Enumerate(ushort? vendorId = null, ushort? productId = null)
{ {
var devices = new List<IHid>(); var devices = new List<IHid>();
IntPtr cfSet = IOHIDManagerCopyDevices(_hidManager); IntPtr cfSet = IOHIDManagerCopyDevices(_hidManager);

View File

@ -5,6 +5,7 @@ using System.Collections.Generic;
using System.ComponentModel; using System.ComponentModel;
using System.IO; using System.IO;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Threading.Tasks;
using static EonaCat.HID.Managers.Windows.NativeMethods; using static EonaCat.HID.Managers.Windows.NativeMethods;
namespace EonaCat.HID.Managers.Windows namespace EonaCat.HID.Managers.Windows
@ -112,13 +113,9 @@ namespace EonaCat.HID.Managers.Windows
var hdr = Marshal.PtrToStructure<DEV_BROADCAST_HDR>(lParam); var hdr = Marshal.PtrToStructure<DEV_BROADCAST_HDR>(lParam);
if (hdr.dbch_devicetype == DBT_DEVTYP_DEVICEINTERFACE) if (hdr.dbch_devicetype == DBT_DEVTYP_DEVICEINTERFACE)
{ {
var devInterface = Marshal.PtrToStructure<DEV_BROADCAST_DEVICEINTERFACE>(lParam); var devInterface = Marshal.PtrToStructure<DEV_BROADCAST_DEVICEINTERFACE>(lParam);
string devicePath = Marshal.PtrToStringUni((IntPtr)((long)lParam + Marshal.OffsetOf<DEV_BROADCAST_DEVICEINTERFACE>("dbcc_classguid").ToInt64() + Marshal.SizeOf<Guid>()));
// Calculate pointer to string
IntPtr stringPtr = IntPtr.Add(lParam, Marshal.SizeOf<DEV_BROADCAST_DEVICEINTERFACE>());
// Read null-terminated string from unmanaged memory
string devicePath = Marshal.PtrToStringUni(stringPtr);
if (!string.IsNullOrEmpty(devicePath)) if (!string.IsNullOrEmpty(devicePath))
{ {
try try
@ -175,170 +172,133 @@ namespace EonaCat.HID.Managers.Windows
} }
} }
public IEnumerable<IHid> Enumerate(ushort? vendorId = null, ushort? productId = null) public Task<IEnumerable<IHid>> EnumerateAsync(ushort? vendorId = null, ushort? productId = null)
{ {
var list = new List<IHid>(); return Task.Run(() => Enumerate(vendorId, productId));
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 void Dispose() private IEnumerable<IHid> Enumerate(ushort? vendorId = null, ushort? productId = null)
{ {
if (_deviceNotificationHandle != IntPtr.Zero) var list = new List<IHid>();
{
UnregisterDeviceNotification(_deviceNotificationHandle); IntPtr devInfo = SetupDiGetClassDevs(
_deviceNotificationHandle = IntPtr.Zero; 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 its 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 internal static class NativeMethods