Updated Write output
This commit is contained in:
parent
fe283384af
commit
19e713eb21
|
@ -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)
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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;
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
|
@ -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
|
||||||
|
|
|
@ -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/";
|
||||||
|
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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 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
|
internal static class NativeMethods
|
||||||
|
|
Loading…
Reference in New Issue