From 19e713eb21e8b6414f18571fd676797416b7e933 Mon Sep 17 00:00:00 2001 From: Jeroen Saey Date: Mon, 28 Jul 2025 13:52:56 +0200 Subject: [PATCH] Updated Write output --- Analyzer/MainForm.cs | 10 +- EonaCat.HID.Console/Program.cs | 6 +- EonaCat.HID/EonaCat.HID.csproj | 2 +- EonaCat.HID/HidWindows.cs | 939 +++++++++++----------- EonaCat.HID/IHidManager.cs | 3 +- EonaCat.HID/Managers/HidManagerLinux.cs | 7 +- EonaCat.HID/Managers/HidManagerMac.cs | 8 +- EonaCat.HID/Managers/HidManagerWindows.cs | 294 +++---- 8 files changed, 619 insertions(+), 650 deletions(-) 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