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

@ -228,41 +228,38 @@ namespace EonaCat.HID
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;
}
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; return;
} }
try try
{ {
// Combine reportId and data into one buffer for sending int reportLength = report.Data.Length + 1;
var buffer = new byte[1 + report.Data.Length]; var buffer = new byte[reportLength];
buffer[0] = report.ReportId; buffer[0] = report.ReportId;
Array.Copy(report.Data, 0, buffer, 1, report.Data.Length); Array.Copy(report.Data, 0, buffer, 1, report.Data.Length);
await _deviceStream.WriteAsync(buffer, 0, buffer.Length); await _deviceStream.WriteAsync(buffer, 0, buffer.Length);
await _deviceStream.FlushAsync(); }
catch (IOException ioEx)
{
OnError?.Invoke(this, new HidErrorEventArgs(this, ioEx));
} }
catch (Exception ex) catch (Exception ex)
{ {
OnError?.Invoke(this, new HidErrorEventArgs(this, ex)); OnError?.Invoke(this, new HidErrorEventArgs(this, ex));
throw;
} }
} }
public async Task<HidReport> ReadInputReportAsync() public async Task<HidReport> ReadInputReportAsync()
{ {
if (!_isOpen) if (!_isOpen)

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
@ -113,12 +114,8 @@ namespace EonaCat.HID.Managers.Windows
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,19 +172,23 @@ 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)
{
return Task.Run(() => Enumerate(vendorId, productId));
}
private IEnumerable<IHid> Enumerate(ushort? vendorId = null, ushort? productId = null)
{ {
var list = new List<IHid>(); var list = new List<IHid>();
IntPtr devInfo = SetupDiGetClassDevs( IntPtr devInfo = SetupDiGetClassDevs(
ref GUID_DEVINTERFACE_HID, ref GUID_DEVINTERFACE_HID,
null, null,
IntPtr.Zero, IntPtr.Zero,
DIGCF_PRESENT | DIGCF_DEVICEINTERFACE); DIGCF_PRESENT | DIGCF_DEVICEINTERFACE);
if (devInfo == IntPtr.Zero || devInfo.ToInt64() == -1) if (devInfo == IntPtr.Zero || devInfo == new IntPtr(-1))
{
throw new Win32Exception(Marshal.GetLastWin32Error(), "SetupDiGetClassDevs failed"); throw new Win32Exception(Marshal.GetLastWin32Error(), "SetupDiGetClassDevs failed");
}
try try
{ {
@ -198,92 +199,59 @@ namespace EonaCat.HID.Managers.Windows
for (uint index = 0; ; index++) for (uint index = 0; ; index++)
{ {
bool ok = SetupDiEnumDeviceInterfaces( if (!SetupDiEnumDeviceInterfaces(devInfo, IntPtr.Zero, ref GUID_DEVINTERFACE_HID, index, ref iface))
devInfo, IntPtr.Zero, ref GUID_DEVINTERFACE_HID, index, ref iface);
if (!ok)
{ {
int error = Marshal.GetLastWin32Error(); int error = Marshal.GetLastWin32Error();
if (error == ERROR_NO_MORE_ITEMS) if (error == ERROR_NO_MORE_ITEMS)
{
break; break;
}
throw new Win32Exception(error, "SetupDiEnumDeviceInterfaces failed"); throw new Win32Exception(error, "SetupDiEnumDeviceInterfaces failed");
} }
// Step 1: Get required size
uint requiredSize = 0; uint requiredSize = 0;
bool sizeResult = SetupDiGetDeviceInterfaceDetail( SetupDiGetDeviceInterfaceDetail(devInfo, ref iface, IntPtr.Zero, 0, ref requiredSize, IntPtr.Zero);
devInfo, if (Marshal.GetLastWin32Error() != ERROR_INSUFFICIENT_BUFFER || requiredSize == 0)
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; continue;
}
// Step 2: Allocate buffer for detail data
IntPtr detailDataBuffer = Marshal.AllocHGlobal((int)requiredSize); IntPtr detailDataBuffer = Marshal.AllocHGlobal((int)requiredSize);
try 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; int cbSize = IntPtr.Size == 8 ? 8 : 6;
Marshal.WriteInt32(detailDataBuffer, cbSize); Marshal.WriteInt32(detailDataBuffer, cbSize);
// Step 4: Now get the device interface detail if (!SetupDiGetDeviceInterfaceDetail(devInfo, ref iface, detailDataBuffer, requiredSize, ref requiredSize, IntPtr.Zero))
bool success = SetupDiGetDeviceInterfaceDetail(
devInfo,
ref iface,
detailDataBuffer,
requiredSize,
ref requiredSize,
IntPtr.Zero);
if (!success)
{ {
int detailError = Marshal.GetLastWin32Error(); System.Diagnostics.Debug.WriteLine($"Detail fetch failed: {Marshal.GetLastWin32Error()}");
throw new Win32Exception(detailError, $"SetupDiGetDeviceInterfaceDetail failed with error {detailError}"); continue;
} }
// 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); IntPtr pDevicePathName = IntPtr.Add(detailDataBuffer, 4);
string devicePath = Marshal.PtrToStringAuto(pDevicePathName); string devicePath = Marshal.PtrToStringAuto(pDevicePathName);
if (string.IsNullOrEmpty(devicePath)) if (string.IsNullOrWhiteSpace(devicePath))
{
continue; continue;
}
// Step 6: Create device, filter and add // Try to open with zero access to ensure its reachable
try using (var testHandle = CreateFile(devicePath, 0, FileShare.ReadWrite, IntPtr.Zero, FileMode.Open, 0, IntPtr.Zero))
{
// 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) if (testHandle.IsInvalid)
{
// Device not accessible, skip it
continue; 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;
} }
var device = new HidWindows(devicePath); if ((vendorId.HasValue && device.VendorId != vendorId.Value) ||
device.Setup(); (productId.HasValue && device.ProductId != productId.Value))
if (vendorId.HasValue && device.VendorId != vendorId.Value ||
productId.HasValue && device.ProductId != productId.Value)
{ {
device.Dispose(); device.Dispose();
continue; continue;
@ -291,26 +259,6 @@ namespace EonaCat.HID.Managers.Windows
list.Add(device); 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 finally
{ {
Marshal.FreeHGlobal(detailDataBuffer); Marshal.FreeHGlobal(detailDataBuffer);
@ -325,7 +273,10 @@ namespace EonaCat.HID.Managers.Windows
return list; return list;
} }
public void Dispose() public void Dispose()
{
lock (_lock)
{ {
if (_deviceNotificationHandle != IntPtr.Zero) if (_deviceNotificationHandle != IntPtr.Zero)
{ {
@ -338,7 +289,16 @@ namespace EonaCat.HID.Managers.Windows
DestroyWindow(_messageWindowHandle); DestroyWindow(_messageWindowHandle);
_messageWindowHandle = IntPtr.Zero; _messageWindowHandle = IntPtr.Zero;
} }
foreach (var device in _knownDevices.Values)
{
device.Dispose();
} }
_knownDevices.Clear();
}
}
} }
internal static class NativeMethods internal static class NativeMethods