Updated so we also support linux and mac

This commit is contained in:
EonaCat 2025-07-12 22:40:29 +02:00
parent 6eee19b885
commit 115bac01ca
33 changed files with 2617 additions and 1984 deletions

View File

@ -96,13 +96,13 @@
<None Include="App.config" /> <None Include="App.config" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\EonaCat.HID\EonaCat.HID.csproj"> <Content Include="icon.ico" />
<Project>{9e8f1d50-74ea-4c60-bd5c-ab2c5b53bc66}</Project>
<Name>EonaCat.HID</Name>
</ProjectReference>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<Content Include="icon.ico" /> <ProjectReference Include="..\EonaCat.HID\EonaCat.HID.csproj">
<Project>{00403bd6-7a26-4971-29d3-8a7849aac770}</Project>
<Name>EonaCat.HID</Name>
</ProjectReference>
</ItemGroup> </ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" /> <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<!-- To modify your build process, add your task inside one of the targets below and uncomment it. <!-- To modify your build process, add your task inside one of the targets below and uncomment it.

View File

@ -1,4 +1,5 @@
using EonaCat.HID.Helpers; using EonaCat.HID.EventArguments;
using EonaCat.HID.Helpers;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
@ -6,23 +7,41 @@ using System.Drawing;
using System.Globalization; using System.Globalization;
using System.Linq; using System.Linq;
using System.Reflection; using System.Reflection;
using System.Threading.Tasks;
using System.Windows.Forms; using System.Windows.Forms;
namespace EonaCat.HID.Analyzer namespace EonaCat.HID.Analyzer
{ {
public partial class MainForm : Form public partial class MainForm : Form
{ {
private const int WriteReportTimeout = 3000; IHidManager _deviceManager;
private IHid _device;
private Device _device; private IEnumerable<IHid> _deviceList;
private List<Device> _deviceList;
public MainForm() public MainForm()
{ {
InitializeComponent(); InitializeComponent();
CreateDeviceManager();
} }
private void MainForm_Load(object sender, EventArgs e) private void CreateDeviceManager()
{
_deviceManager = HidFactory.CreateDeviceManager();
if (_deviceManager == null)
{
throw new Exception("Failed to create HID manager.");
}
_deviceManager.OnDeviceInserted += Hid_Inserted;
_deviceManager.OnDeviceRemoved += Hid_Removed;
}
public void RefreshDevices(ushort? vendorId = null, ushort? productId = null)
{
_deviceList = _deviceManager.Enumerate(vendorId, productId);
}
private void MainForm_Load(object sender, System.EventArgs e)
{ {
try try
{ {
@ -34,11 +53,11 @@ namespace EonaCat.HID.Analyzer
} }
} }
private void MainForm_Shown(object sender, EventArgs e) private void MainForm_Shown(object sender, System.EventArgs e)
{ {
try try
{ {
_deviceList = Devices.Enumerate().ToList(); RefreshDevices();
UpdateDeviceList(); UpdateDeviceList();
toolStripStatusLabel1.Text = "Please select device and click open to start."; toolStripStatusLabel1.Text = "Please select device and click open to start.";
} }
@ -76,20 +95,17 @@ namespace EonaCat.HID.Analyzer
dataGridView1.SelectionChanged -= DataGridView1_SelectionChanged; dataGridView1.SelectionChanged -= DataGridView1_SelectionChanged;
dataGridView1.Rows.Clear(); dataGridView1.Rows.Clear();
for (int i = 0; i < _deviceList.Count; i++) for (int i = 0; i < _deviceList.Count(); i++)
{ {
Device device = _deviceList[i]; IHid device = _deviceList.ElementAt(i);
var deviceName = ""; var deviceName = "";
var deviceManufacturer = ""; var deviceManufacturer = "";
var deviceSerialNumber = ""; var deviceSerialNumber = "";
var info = device.Info; deviceName = device.ProductName;
var capabilities = device.Capabilities; deviceManufacturer = device.Manufacturer;
deviceSerialNumber = device.SerialNumber;
deviceName = device.ReadProductName();
deviceManufacturer = device.ReadManufacturer();
deviceSerialNumber = device.ReadSerialNumber();
var row = new string[] var row = new string[]
{ {
@ -97,12 +113,13 @@ namespace EonaCat.HID.Analyzer
deviceName, deviceName,
deviceManufacturer, deviceManufacturer,
deviceSerialNumber, deviceSerialNumber,
capabilities.InputReportByteLength.ToString(), device.InputReportByteLength.ToString(),
capabilities.OutputReportByteLength.ToString(), device.OutputReportByteLength.ToString(),
capabilities.FeatureReportByteLength.ToString(), device.FeatureReportByteLength.ToString(),
$"Vendor:{info.VendorId:X4} Product:{info.ProductId:X4} Revision:{info.Version:X4}", $"Vendor:{device.VendorId:X4} Product:{device.ProductId:X4} Revision:{device.VendorId:X4}",
device.DevicePath device.DevicePath
}; };
dataGridView1.Rows.Add(row); dataGridView1.Rows.Add(row);
} }
dataGridView1.SelectionChanged += DataGridView1_SelectionChanged; dataGridView1.SelectionChanged += DataGridView1_SelectionChanged;
@ -117,7 +134,7 @@ namespace EonaCat.HID.Analyzer
})); }));
} }
private void NewToolStripMenuItem_Click(object sender, EventArgs e) private void NewToolStripMenuItem_Click(object sender, System.EventArgs e)
{ {
try try
{ {
@ -129,7 +146,7 @@ namespace EonaCat.HID.Analyzer
} }
} }
private void ExitToolStripMenuItem_Click(object sender, EventArgs e) private void ExitToolStripMenuItem_Click(object sender, System.EventArgs e)
{ {
try try
{ {
@ -157,7 +174,7 @@ namespace EonaCat.HID.Analyzer
{ {
try try
{ {
_deviceList = Devices.Enumerate().ToList(); RefreshDevices();
UpdateDeviceList(); UpdateDeviceList();
} }
catch (Exception ex) catch (Exception ex)
@ -170,10 +187,15 @@ namespace EonaCat.HID.Analyzer
{ {
try try
{ {
ushort? vid = null;
ushort? pid = null;
var str = toolStripTextBoxVidPid.Text.Split(':'); var str = toolStripTextBoxVidPid.Text.Split(':');
var vid = int.Parse(str[0], NumberStyles.AllowHexSpecifier); if (!string.IsNullOrEmpty(toolStripTextBoxVidPid.Text))
var pid = int.Parse(str[1], NumberStyles.AllowHexSpecifier); {
_deviceList = Devices.Enumerate(vid, pid).ToList(); vid = ushort.Parse(str[0], NumberStyles.AllowHexSpecifier);
pid = ushort.Parse(str[1], NumberStyles.AllowHexSpecifier);
}
RefreshDevices(vid, pid);
UpdateDeviceList(); UpdateDeviceList();
} }
catch (Exception ex) catch (Exception ex)
@ -184,25 +206,84 @@ namespace EonaCat.HID.Analyzer
private void ToolStripButtonConnect_Click(object sender, EventArgs e) private void ToolStripButtonConnect_Click(object sender, EventArgs e)
{ {
ConnectToSelectedDevice(); ConnectToSelectedDeviceAsync().ConfigureAwait(false);
} }
private void ConnectToSelectedDevice() private async Task ConnectToSelectedDeviceAsync()
{
await Task.Run(async () =>
{
try
{
_device = _deviceList.ElementAt(dataGridView1.SelectedRows[0].Index);
if (_device == null)
{
throw new Exception("Could not find Hid USB Device with specified VID PID");
}
var device = _device;
device.OnDataReceived -= OnDataReceived;
device.OnDataReceived += OnDataReceived;
device.OnError -= OnError;
device.OnError += OnError;
device.Open();
AppendEventLog($"Connected to device {_device.ProductName}", Color.Green);
AppendEventLog($"Started listening to device {_device.ProductName}", Color.Green);
await device.StartListeningAsync(default).ConfigureAwait(false);
}
catch (Exception ex)
{
PopupException(ex.Message);
}
});
}
private void OnError(object sender, HidErrorEventArgs e)
{ {
try try
{ {
_device = _deviceList[dataGridView1.SelectedRows[0].Index]; var str = $"Device error for {e.Device.ProductName} => {e.Exception.Message}";
if (_device == null) AppendEventLog(str, Color.Red);
{ }
throw new Exception("Could not find Hid USB Device with specified VID PID"); catch (Exception ex)
} {
PopupException(ex.Message);
}
}
// Open a device for reading and writing private void OnDataReceived(object sender, HidDataReceivedEventArgs e)
_device.OpenDevice(DeviceMode.Overlapped, DeviceMode.Overlapped, ShareMode.ShareRead | ShareMode.ShareWrite); {
_device.OnInserted += HidDevice_Inserted; try
_device.OnRemoved += HidDevice_Removed; {
_device.MonitorDeviceEvents = true; var str = $"Rx Input Report from device {e.Device.ProductName} => {BitConverter.ToString(e.Data)}";
AppendEventLog($"Connected to device {_device.ReadProductName()}", Color.Green); AppendEventLog(str, Color.Blue);
}
catch (Exception ex)
{
PopupException(ex.Message);
}
}
private void Hid_Removed(object sender, HidEventArgs e)
{
try
{
var str = string.Format("Removed Device --> VID {0:X4}, PID {0:X4}", e.Device.VendorId, e.Device.ProductId);
AppendEventLog(str);
}
catch (Exception ex)
{
PopupException(ex.Message);
}
}
private void Hid_Inserted(object sender, HidEventArgs e)
{
try
{
var str = string.Format("Inserted Device --> VID {0:X4}, PID {0:X4}", e.Device.VendorId, e.Device.ProductId);
AppendEventLog(str, Color.Orange);
} }
catch (Exception ex) catch (Exception ex)
{ {
@ -235,63 +316,30 @@ namespace EonaCat.HID.Analyzer
} }
} }
private void ButtonReadInput_Click(object sender, EventArgs e) private async void ButtonReadInput_Click(object sender, EventArgs e)
{ {
try try
{ {
var len = _device.Capabilities.InputReportByteLength; if (_device == null)
{
AppendEventLog("No device connected. Please select a device and click 'Connect'.", Color.Red);
return;
}
var len = (int)_device.InputReportByteLength;
if (len <= 0) if (len <= 0)
{ {
throw new Exception("This device has no Input Report support!"); throw new Exception("This device has no Input Report support!");
} }
_device.ReadReport(Device_InputReportReceived); var buffer = await _device.ReadInputReportAsync();
AppendEventLog("Hid Input Report Callback Started."); if (buffer.Length < 2)
}
catch (Exception ex)
{
PopupException(ex.Message);
}
}
private void ButtonWriteOutput_Click(object sender, EventArgs e)
{
try
{
var hidReportId = byte.Parse(comboBoxReportId.Text);
var buf = ByteHelper.HexStringToByteArray(textBoxWriteData.Text);
var report = _device.CreateReport();
if (buf.Length > report.Data.Length)
{ {
throw new Exception("Output Report Length Exceed"); AppendEventLog("Received report is too short to contain a valid Report ID.", Color.Red);
return;
} }
report.ReportId = hidReportId; var str = string.Format("Rx Input Report [{0}] <-- {1}", buffer.Length, BitConverter.ToString(buffer));
Array.Copy(buf, report.Data, buf.Length);
_device.WriteReport(report, WriteReportTimeout);
var str = string.Format("Tx Output Report [{0}] --> ID:{1}, {2}", report.Data.Length + 1, report.ReportId, ByteHelper.ByteArrayToHexString(report.Data));
AppendEventLog(str, Color.DarkGreen);
}
catch (Exception ex)
{
PopupException(ex.Message);
}
}
private void ButtonReadFeature_Click(object sender, EventArgs e)
{
try
{
var hidReportId = byte.Parse(comboBoxReportId.Text);
var len = _device.Capabilities.FeatureReportByteLength;
if (len <= 0)
{
throw new Exception("This device has no Feature Report support!");
}
_device.ReadFeatureData(out byte[] buf, hidReportId);
var str = string.Format("Rx Feature Report [{0}] <-- {1}", buf.Length, ByteHelper.ByteArrayToHexString(buf));
AppendEventLog(str, Color.Blue); AppendEventLog(str, Color.Blue);
} }
catch (Exception ex) catch (Exception ex)
@ -300,21 +348,70 @@ namespace EonaCat.HID.Analyzer
} }
} }
private void ButtonWriteFeature_Click(object sender, EventArgs e) private async void ButtonWriteOutput_Click(object sender, EventArgs e)
{
try
{
byte hidReportId = byte.Parse(comboBoxReportId.Text.Trim());
byte[] buf = ByteHelper.HexStringToByteArray(textBoxWriteData.Text.Trim());
// Combine report ID and buffer
byte[] outputReport = new byte[buf.Length + 1];
outputReport[0] = hidReportId;
Array.Copy(buf, 0, outputReport, 1, buf.Length);
try
{
await _device.WriteOutputReportAsync(outputReport);
AppendEventLog($"Output report sent: {BitConverter.ToString(outputReport)}", Color.DarkGreen);
}
catch (Exception ex)
{
AppendEventLog("Write failed: " + ex.Message, Color.DarkRed);
}
}
catch (Exception ex)
{
PopupException("Error preparing output report: " + ex.Message);
}
}
private async void ButtonReadFeature_Click(object sender, EventArgs e)
{
try
{
var hidReportId = byte.Parse(comboBoxReportId.Text);
var len = _device.FeatureReportByteLength;
if (len <= 0)
{
throw new Exception("This device has no Feature Report support!");
}
var buffer = await _device.GetFeatureReportAsync(hidReportId);
var str = string.Format("Rx Feature Report [{0}] <-- {1}", buffer.Length, ByteHelper.ByteArrayToHexString(buffer));
AppendEventLog(str, Color.Blue);
}
catch (Exception ex)
{
PopupException(ex.Message);
}
}
private async void ButtonWriteFeature_Click(object sender, EventArgs e)
{ {
try try
{ {
var hidReportId = byte.Parse(comboBoxReportId.Text); var hidReportId = byte.Parse(comboBoxReportId.Text);
var buf = ByteHelper.HexStringToByteArray(textBoxWriteData.Text); var buf = ByteHelper.HexStringToByteArray(textBoxWriteData.Text);
var len = _device.Capabilities.FeatureReportByteLength; var len = _device.FeatureReportByteLength;
if (buf.Length > len) if (buf.Length > len)
{ {
throw new Exception("Write Feature Report Length Exceed"); throw new Exception("Write Feature Report Length Exceed");
} }
Array.Resize(ref buf, len); Array.Resize(ref buf, len);
_device.WriteFeatureData(buf); await _device.SendFeatureReportAsync(buf);
var str = string.Format("Tx Feature Report [{0}] --> {1}", buf.Length, ByteHelper.ByteArrayToHexString(buf)); var str = string.Format("Tx Feature Report [{0}] --> {1}", buf.Length, ByteHelper.ByteArrayToHexString(buf));
AppendEventLog(str, Color.DarkGreen); AppendEventLog(str, Color.DarkGreen);
} }
@ -328,53 +425,15 @@ namespace EonaCat.HID.Analyzer
{ {
try try
{ {
if (dataGridView1.SelectedRows.Count <= 0) return; if (dataGridView1.SelectedRows.Count <= 0)
{
return;
}
var index = dataGridView1.SelectedRows[0].Index; var index = dataGridView1.SelectedRows[0].Index;
var devinfo = _deviceList[index].Info; var info = _deviceList.ElementAt(index);
toolStripTextBoxVidPid.Text = string.Format("{0:X4}:{1:X4}", devinfo.VendorId, devinfo.ProductId); toolStripTextBoxVidPid.Text = string.Format("{0:X4}:{1:X4}", info.VendorId, info.ProductId);
}
catch (Exception ex)
{
PopupException(ex.Message);
}
}
private void HidDevice_Inserted()
{
try
{
var str = string.Format("Inserted Device --> VID {0:X4}, PID {0:X4}", _device.Info.VendorId, _device.Info.ProductId);
AppendEventLog(str, Color.Orange);
_device.ReadReport(Device_InputReportReceived);
}
catch (Exception ex)
{
PopupException(ex.Message);
}
}
private void HidDevice_Removed()
{
try
{
var str = string.Format("Removed Device --> VID {0:X4}, PID {0:X4}", _device.Info.VendorId, _device.Info.ProductId);
AppendEventLog(str);
}
catch (Exception ex)
{
PopupException(ex.Message);
}
}
private void Device_InputReportReceived(Report report)
{
try
{
if (!_device.IsConnected || !_device.IsOpen || report.Data == null) return;
var str = $"Rx Input Report [{report.Data.Length + 1}] <-- ID:{report.ReportId}, {ByteHelper.ByteArrayToHexString(report.Data)}";
AppendEventLog(str, Color.Blue);
_device.ReadReport(Device_InputReportReceived);
} }
catch (Exception ex) catch (Exception ex)
{ {
@ -384,7 +443,12 @@ namespace EonaCat.HID.Analyzer
private void dataGridView1_DoubleClick(object sender, EventArgs e) private void dataGridView1_DoubleClick(object sender, EventArgs e)
{ {
ConnectToSelectedDevice(); ConnectToSelectedDeviceAsync().ConfigureAwait(false);
}
private void toolStripTextBoxVidPid_Enter(object sender, EventArgs e)
{
ToolStripButtonFilter_Click(sender, e);
} }
} }
} }

View File

@ -151,60 +151,59 @@
<data name="toolStripButtonReload.Image" type="System.Drawing.Bitmap, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64"> <data name="toolStripButtonReload.Image" type="System.Drawing.Bitmap, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value> <value>
iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8 iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8
YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAI4SURBVDhPjZNPaNNQHMcfjNGBVTpB2GUOtyDYwzbZsKV2 YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAIqSURBVDhPfZJNaBNBFMcXSknBKKkg9FKL7SKSQ1tpMSEm
fUkKJZd6292Tf27CDh6zHSxpCoJuDlrYLuqlF1FhBz20ox62NdU6JzotzDavzYpddy9ofL+Xl9l1CvvC mZ0NhFzirXdPftyEHjxueohsNiDUj0IC7UW95CIq5NDLpkQo7U401hatBkKys5kG0/Qe0JW3ycZsWvuH
j+R93+/3ye/3kqD/KTcy4sv5/d7tcHgQgtunUxahvrejlw/zl4R4ORi68+Ha9S/g10QRs4ReaVvNULJo P8u89+bHe2+W4/4jdWrKo3q97r1gcBw8nD9XOY4b2Zy+flK4xsfL/sCDz7duf4N4XRDQcK0lebcZSGns
3YfQjPpF8HJjY8KaIHi+KYrn0/TMcA1jgYhyh2DpmTE11c8KXam5vQF9s1HXDctOFS2R2ydEMJ6kEIuI MVgmxlWIqTMzfJ7nXT9iMdfXxdBkHSGeCmKHIvyaLCyMOgCSWh1TdhqGQpiZ1pjgSA6IIjRPBZFRAb9y
0lNuOaKFs4mNekfftAoAaH3PLLc/z9vtSsaJ4s3d6vZddg4c0iGSdIMVg5KGtaUZjcdOJ82hg0o6dwxQ JBTClpLbRkfZYUUAtH5m19oHCbNdyXat3T2s7T209tCDdCjGd/qAFGG7Mmk863bSnDiuZFQHoPzIPC4v
vmcflOfe8HRkYmmJYHmdLR68b1xIbJi/NWM/yAyqI0DpthP0Hjy+TQE4+APjX7VY7DyiLQdgdnWn6eX7 b9r1OsIvKBK3rEPyU+NKcrv+RyZHfrugDyjd7/ogYULsHwD5qwj9rkejl7m0xnwwu7TfdDsAvYs2ZBDQ
DoAXupBuQEtRztExbBKJXkV6aX8cAMl3P8/y/b+ALkg3YA9jHwBMSZpAqbJ1BkaATvj+ccDOwokR6AGG isUuUUE0aThyk1NKR7MASH38dfEUYAAyCKgi5AGAjvEcly6zCzACdHImYH/l1AgU4wCMULN/MIWwgkIa
YISq+4HRDvK60VhkCyoGcA9w96Hd/jjHAPTJQzQGTFFeJhE5z9MRSpWsuPMaySSsW5XMkyMAD/BMHBVN zx0Ae4GHT832l2ULUEVooorQmC6IazQsFux6Ll1i8e4z0nk4tyrZl31AzxDTUUTQBVy0njEsxvsAkKKx
USqw1xiR46zYlV600gmDWC7kXwIAO7yZCIFOuO0obdj9dIwV6CRRagrTq+vDyqM1j0DDr74UIKcajY7S TJJQZkPOEgCs5YVCFDpxJDPEHFVIYx06eVJq8osbW5Ox1byLX827vNI7Hmpqkcg0RVgGGxhHHQBbMjGC
T1mDqEtSjBX2iv4LYbgGVvNfAyuFW+MLr+JX5l8fzmazfSzhtAo/LwxC+NWsd0J94eN2jxD6A3Zieio6 8PVtFL771ov3Zlfex28kPpws5XIjw7XnKvimOA72Sjn3nPTWM5y39RdV/noYsphYLAAAAABJRU5ErkJg
Qs3XAAAAAElFTkSuQmCC gg==
</value> </value>
</data> </data>
<data name="toolStripButtonOpen.Image" type="System.Drawing.Bitmap, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64"> <data name="toolStripButtonOpen.Image" type="System.Drawing.Bitmap, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value> <value>
iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8 iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8
YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAIzSURBVDhPY5BuvNMjXXPjhWTJubsiuUffCOccfS2Re8qe YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAIoSURBVDhPY5BuvNMjXXPjhWTJubsiuUfeCOccfS2Re8qe
AQrEiy8oihWcuSGeeXw2VAgVSNXfOSBdde2/ZNml/+J5J8FYJPtEAVSaASjeDJITyjj8HSqECggZIFF2 AQrEiy8oihWcuSGeeXw2TAwFSNXfOSBdde2/ZNml/+J5J8FYJPtEAUxesuxSM0hOMOPwd1SdUEDIAImy
uQQkJ5Zy+DNUCBUQMkC0/gqPVPqWU6K5J2KhQqiAkAEgYOTsHW/i6uNr4uytZOjoJc9QX88ElWJgkGm8 yyUgObHkw59RdUIBIQNE66/wSKVvOSWaeyIWVScUEDIABIycveNNXH18TZy9lQwdveQZ6uuZ4JIyjXf3
u1+6GtUA8ZzjeVBpODBy9XUCGpAGwsbO3lOMjY1ZwRJSjXecpWpvLJXL2bFcPPfoWrHck6vEM46KgeTm S1ejGiCeczwP2QAQMHL1dTJx9k4DYWNn7ynGxsasYAmpxjvOUrU3lsrl7Fgunnt0rVjuyVXiGUfFQHLz
3/9vUH/lu0r9pe9KyNg1ubTQu3GaO9gAGDB19U0wd/dXgHIZFt3/cbj3xq//jVcwcd2Fr/9Tjn/+m3zq 7/83qL/yXaX+0nclZOyaXFro3TjNHcUGU1ffBHN3fwUYf9H9H4d7b/z633gFE9dd+Po/+dinv8mnflTi
RyVUOaYByx/9eJ85ddV/n8Ts/3bhySjYwMnnf9KqU/9jT/zYD1UOCihfHyM3b2MoF2xAclP/f6fAiP/J NGD5o+/vM6eu+u+TmP3fLjwZBRs4+fxPWnXqf+yJH/vhBhg5+/oYuXkbIxuQ3NT/3ykw4n+yte//+UZ+
1r7/5xv5/V9m4Pffy8Hnv5aF3f+Q3mX/4098PwBVzsBg5uHBBwyc7tDQUGYQH9mADjO//9+VAv5/VQ74 /5cZ+P33cvD5r2Vh9z+kd9n/+BPfD8ANMPPw4DN29u4ODQ1lRjegw8zv/3elgP9flQP+x9r6YjcABEwc
H2vri90AEDBx9FM3dvFpM3Hxrurfc+kTzABTF5//iwz9/leZ+/03AbJxGoAMQC5IaZv23zU4GqwJGWua /dSNXXzaTFy8q/p3X/wEM8DUxef/IkO//1Xmfv9NXHC4AB2AXJDSNu2/a3A0WBMy1jSz/R81cxthAxou
2f6PmrmNsAENl378L9t9+3/5wccIfODR/8JtN/6XXviJ34Cl9368bbz667/FpHv/Y7e8/e846+H/0NUv /fhftvv2//KDjxH4wKP/hdtu/C+98BO/AUvufn/bePXXf4tJ9/7Hbnn733HWw/+hq1/+9178DIxLzv/8
/3svfgbGJed//o89/mMPVDkmmH/nZ9+k27+eNF35dRcY9yi4/vKve8mnfzxJPv3DCwAg0ohxfV2WKwAA H3v8xx50fXAw/87Pvkm3fz1puvLrbiMarr/8617y6R9Pkk//8AIABXiIYedL+BwAAAAASUVORK5CYII=
AABJRU5ErkJggg==
</value> </value>
</data> </data>
<data name="toolStripButtonClear.Image" type="System.Drawing.Bitmap, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64"> <data name="toolStripButtonClear.Image" type="System.Drawing.Bitmap, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value> <value>
iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8 iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8
YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAJSSURBVDhPY8AGbPa9SLPZ+yISysUP0upncmWU93anV/ZZ YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAJPSURBVDhPdZH/SxNhHMdHCpkFRT/1P9Qv/VS4i4FZlET+
Q4UYbPY+e2qz68UNKBc/SC/vXZ9Z0fs/taTrc1Z5jw1IzHrPiy6bPS/qwAoIgczyngUgA5ANsTj2iHP2 ZDaiLJj7opk259Ya2zPnTm1uoURBRLToNwuCRcvcnfuhLwQhkVOsxTzv0PYEtaDadLft+cQtdttu6w0f
jl6huZOq3/V21/04sqnEDaocE9TX17MAvbAaZkhUbsOv1OLGvKkTar/BxNra6v/9Op8O9yIGABmSVta9 uOdzn9free4elapOqHmsp1isVfbrRo/uNhttgUmD/aa61KPYrxvUHP5UPfmfGGyBp6ZrAdBZfL97bX5K
Jja35X9Yet3/lJIOsEYQLq3r+H9va+T/n9eL3vx71KoD1YIJ4uvrOSKz65+kFKNqvr3Q5f+fqwX//z9q 6qkZ7KMY7FLO1o3J5g9KgkrJ4bfCjnuzgb33px0pv8+19TpkOa7k5CCEGo22wOOSRNuPxJ6hkSu3p5yZ
/f/7XuOXfw+bw6FaUMGV/Vk8kyfUwZ1dWtv+/97m8P+/L+X8/3e/EWwAArcs+PewSRuqFQIm9VV9Rbb5 Uo+mERE/GORPrIkk0Vsnn5zv98IZgwt0lokiKNWwawJWn2tha8X8nQj0ASUrpxuhJm0fWtcNVcNfHrZB
7qbQ/183OQE1N6BpRuB/D1vioNoZGIpqOxHOXuD8//eV/P9/b1X//3u9DK4BxP9zuRDJgDZE7GxaXLa7 fnkQQKBBTLj/EH60S8kWsxTt3XVryiUfe9g5DqvPuiC3eBkIN1IUlMsbJLxnf5VgOmBPV+6cCHVCOtQK
rB7obGCA/b6Y/f/vzUq4Qjh+2PL/5+Ho/993+///usPn3f+n9VxQ7RDw9mja/B+Xi/5haATiP1dL/v88 hHMr4HIR3ntBFpidN8rHDh6F3NIAFOIOKKxYZUBa52NXKwRj5dsJPbJGrGi8+MNyH/ug8NlesyPwXsi+
kQhm/77X+hVrjNjvfy8AdFY8UMFfuOYHTcBwaAKzf51O/f9tmyfQG/mZrjufc9vv/88C1crA4LL7Hb/N OgebkQ5Iz55KwQZqlgVSfrzRP9hcNJMaUKAhv2yB7LtLxWcxQafr3ogm+nMP4ce6xQRdkOE1DxDO8w98
3udP7Pe/lAA6NQFmyL+79f+/bfcC+r/q/+9HrT//3aouA6m33vN8ms3uF0VgzSBgs+d5BDAH/rfa8yID 3wOZ8EnIxwZMx14md2qi0CjDbZHUbopNrmui3/YB771YkpAEgsyLdijEr4Mo0FkSd1ileTWTvENFsFkW
xP/3oNnpz6PWs78fNf76db/+07ddfpeBLjEEyXlsu8VusfvFO6u9L86C+GAQuuo/M9CAbfb773NAhcDg UEzyLMViaGGwUVqTtdHWvEAviIJbzHLoV2budIxwnoPSuxPh+PZDEZxqYfGCLOicgQaKxWFNlGuSmyqV
//96pv//VzFDuXAAzHAFtvte+AMAGv7JUh+nLuoAAAAASUVORK5CYII= CgBtA5hpqOxJUTN48Mg87vgL5YHJNRvDXwIAAAAASUVORK5CYII=
</value> </value>
</data> </data>
<data name="toolStripButtonFilter.Image" type="System.Drawing.Bitmap, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64"> <data name="toolStripButtonFilter.Image" type="System.Drawing.Bitmap, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value> <value>
iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8 iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8
YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAEZSURBVDhPY6AKmFApJh7mpvHV1NT0PzE4zE3967RicTGo YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAEXSURBVDhPY2CgBphQKSYe5qr+1dTU9D8xOMxV7eu0YnEx
dgaGiZUK9h5Ohv+/7uL8//8gO178dRfXf5DaKeUKdlDtEJAfo361KlUVqyYY/neA/X9Fsur/vFiNS1Bt uAETKxXsPZwM/3/dxfn//0F2vPjrLq7/ILVTyhXsUFyRH6N+tSpVFUMDMv53gP1/RbLq/7xYjUsomkFg
CDCzXpIrwl/n04pWaayaQXhJk8z/IF/1LxNzhfig2lDBxDJ5C0d7w7/nFwhiaL60SPC/A1AOw+nooCtP Zr0kV4S/zqcVrdIYGmF4SZPM/0BftS8Tc4X40PWDwcQyeQsHe8O/5xcIYmi+tEjwv7294V8Mp6ODrjyF
oczL2eD/683ccM1vt3L/B4m15yi0QZXhByVJKocL4tThBuTFaPwviVc5DJUmDKbUi/KYm5vCDQCxQWEE Mi9ng/+vN3PDNb/dyv0fJNaeo9CGrh4rKElSOVwQpw43IC9G439JvMphdHU4wZR6UR5zc1O4ASA2KIzQ
lSYM6usZmEDxDTMAxIZKEQcGlwGfdnD9t7QwId0AkKY9k8X/+7kZ/C9LVDkClSIeVCSq7o/x0frQma9Q 1eEE9fUMTKD4hhkAYqOrwQsGlwGfdnD9t7QwId0AkKY9k8X/+7kZ/C9LVDmCroYgqEhU3R/jo/WhM1+h
DhWiBWBgAAClAOU+yA6HfwAAAABJRU5ErkJggg== HF2OqgAAizrlNjwLhaIAAAAASUVORK5CYII=
</value> </value>
</data> </data>
<metadata name="menuStrip1.TrayLocation" type="System.Drawing.Point, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a"> <metadata name="menuStrip1.TrayLocation" type="System.Drawing.Point, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
@ -213,12 +212,12 @@
<data name="newToolStripMenuItem.Image" type="System.Drawing.Bitmap, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64"> <data name="newToolStripMenuItem.Image" type="System.Drawing.Bitmap, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value> <value>
iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8 iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8
YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAERSURBVDhPrZDbSgJRGIXnpewd6jXsjSQvIrwoI0RQMChU YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAENSURBVDhPrZHbSgJRGEZ9KX2HfI18I9ELkS5KCREMDBSN
0iiDPCGiE3ZCRkvR8VzTeBhnyR5/ccaZNnPhB4t9sdf6Ln5hb8QeathNJFVFKF5C8DqL4ksDVHWGDf7j VLRA0xDREQ+FTJai47mm8TDOF7NlhulHNwO14LvarHXxb4fjv4jnW6CLZRqIpKoI3Zbw3HwDdX6hC6cI
LHyPg6NjviSaFqlu5yQYR+KpupaIkrMknCxT3Y7v/NYYb0ITK1c3BarbWWhLQ7IR0cTKReyZ6lZ0XYei RnNwuT38yE1WoJ6JL5RC+rFxiAji8Uj0rkY9E+9lksnGqMu4TlSoZ7JT9yxihKjLuIoXqcfQNA2yssVo
ztHpK4bAc+h1FgQijzSxMptrGIxVSO0xX3AaStFki7bUMVFmaMm/eJMGfIH/MkGzLep0AXn4h/r3CJV3 KrOA8+z8eOAiVqAuY7NVMVsqEIdLfiAQyVAX6l7DSt5gIH2hI874AX84TX0o6x2k+Td6HwvUuxI/oD9a
mS9gn2bY4UY/UzQ7E9TqfeTFtnuB+XAfzSHKr11kSl/uBebDiZ89ZCst3OUkdwL28sIVsE83ock+EIQV 0Q+3+FyjP1qh1ZuiLAztB6yHe+nPUWuP8VB9tx+wHk54naBUH+D+SbQXMP771LgB/dHOqPsnfgDYzPbD
2Mz2wxeg6/UAAAAASUVORK5CYII= 7s5C8AAAAABJRU5ErkJggg==
</value> </value>
</data> </data>
<metadata name="statusStrip1.TrayLocation" type="System.Drawing.Point, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a"> <metadata name="statusStrip1.TrayLocation" type="System.Drawing.Point, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">

View File

@ -264,6 +264,7 @@
this.toolStripTextBoxVidPid.Size = new System.Drawing.Size(100, 25); this.toolStripTextBoxVidPid.Size = new System.Drawing.Size(100, 25);
this.toolStripTextBoxVidPid.Text = "0483:0400"; this.toolStripTextBoxVidPid.Text = "0483:0400";
this.toolStripTextBoxVidPid.TextBoxTextAlign = System.Windows.Forms.HorizontalAlignment.Center; this.toolStripTextBoxVidPid.TextBoxTextAlign = System.Windows.Forms.HorizontalAlignment.Center;
this.toolStripTextBoxVidPid.Enter += new System.EventHandler(this.toolStripTextBoxVidPid_Enter);
// //
// menuStrip1 // menuStrip1
// //

View File

@ -2,10 +2,10 @@ Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17 # Visual Studio Version 17
VisualStudioVersion = 17.10.34928.147 VisualStudioVersion = 17.10.34928.147
MinimumVisualStudioVersion = 10.0.40219.1 MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EonaCat.HID", "EonaCat.HID\EonaCat.HID.csproj", "{9E8F1D50-74EA-4C60-BD5C-AB2C5B53BC66}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EonaCat.HID.Analyzer", "Analyzer\EonaCat.HID.Analyzer.csproj", "{61994020-DB89-4621-BA4B-7347A2142CFF}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EonaCat.HID.Analyzer", "Analyzer\EonaCat.HID.Analyzer.csproj", "{61994020-DB89-4621-BA4B-7347A2142CFF}"
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EonaCat.HID", "EonaCat.HID\EonaCat.HID.csproj", "{00403BD6-7A26-4971-29D3-8A7849AAC770}"
EndProject
Global Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU Debug|Any CPU = Debug|Any CPU
@ -18,19 +18,6 @@ Global
Release|x86 = Release|x86 Release|x86 = Release|x86
EndGlobalSection EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution GlobalSection(ProjectConfigurationPlatforms) = postSolution
{9E8F1D50-74EA-4C60-BD5C-AB2C5B53BC66}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{9E8F1D50-74EA-4C60-BD5C-AB2C5B53BC66}.Debug|Any CPU.Build.0 = Debug|Any CPU
{9E8F1D50-74EA-4C60-BD5C-AB2C5B53BC66}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
{9E8F1D50-74EA-4C60-BD5C-AB2C5B53BC66}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU
{9E8F1D50-74EA-4C60-BD5C-AB2C5B53BC66}.Debug|x64.ActiveCfg = Debug|Any CPU
{9E8F1D50-74EA-4C60-BD5C-AB2C5B53BC66}.Debug|x64.Build.0 = Debug|Any CPU
{9E8F1D50-74EA-4C60-BD5C-AB2C5B53BC66}.Debug|x86.ActiveCfg = Debug|Any CPU
{9E8F1D50-74EA-4C60-BD5C-AB2C5B53BC66}.Release|Any CPU.ActiveCfg = Release|Any CPU
{9E8F1D50-74EA-4C60-BD5C-AB2C5B53BC66}.Release|Any CPU.Build.0 = Release|Any CPU
{9E8F1D50-74EA-4C60-BD5C-AB2C5B53BC66}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
{9E8F1D50-74EA-4C60-BD5C-AB2C5B53BC66}.Release|Mixed Platforms.Build.0 = Release|Any CPU
{9E8F1D50-74EA-4C60-BD5C-AB2C5B53BC66}.Release|x64.ActiveCfg = Release|Any CPU
{9E8F1D50-74EA-4C60-BD5C-AB2C5B53BC66}.Release|x86.ActiveCfg = Release|Any CPU
{61994020-DB89-4621-BA4B-7347A2142CFF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {61994020-DB89-4621-BA4B-7347A2142CFF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{61994020-DB89-4621-BA4B-7347A2142CFF}.Debug|Any CPU.Build.0 = Debug|Any CPU {61994020-DB89-4621-BA4B-7347A2142CFF}.Debug|Any CPU.Build.0 = Debug|Any CPU
{61994020-DB89-4621-BA4B-7347A2142CFF}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU {61994020-DB89-4621-BA4B-7347A2142CFF}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
@ -47,6 +34,22 @@ Global
{61994020-DB89-4621-BA4B-7347A2142CFF}.Release|x64.Build.0 = Release|Any CPU {61994020-DB89-4621-BA4B-7347A2142CFF}.Release|x64.Build.0 = Release|Any CPU
{61994020-DB89-4621-BA4B-7347A2142CFF}.Release|x86.ActiveCfg = Release|Any CPU {61994020-DB89-4621-BA4B-7347A2142CFF}.Release|x86.ActiveCfg = Release|Any CPU
{61994020-DB89-4621-BA4B-7347A2142CFF}.Release|x86.Build.0 = Release|Any CPU {61994020-DB89-4621-BA4B-7347A2142CFF}.Release|x86.Build.0 = Release|Any CPU
{00403BD6-7A26-4971-29D3-8A7849AAC770}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{00403BD6-7A26-4971-29D3-8A7849AAC770}.Debug|Any CPU.Build.0 = Debug|Any CPU
{00403BD6-7A26-4971-29D3-8A7849AAC770}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
{00403BD6-7A26-4971-29D3-8A7849AAC770}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU
{00403BD6-7A26-4971-29D3-8A7849AAC770}.Debug|x64.ActiveCfg = Debug|Any CPU
{00403BD6-7A26-4971-29D3-8A7849AAC770}.Debug|x64.Build.0 = Debug|Any CPU
{00403BD6-7A26-4971-29D3-8A7849AAC770}.Debug|x86.ActiveCfg = Debug|Any CPU
{00403BD6-7A26-4971-29D3-8A7849AAC770}.Debug|x86.Build.0 = Debug|Any CPU
{00403BD6-7A26-4971-29D3-8A7849AAC770}.Release|Any CPU.ActiveCfg = Release|Any CPU
{00403BD6-7A26-4971-29D3-8A7849AAC770}.Release|Any CPU.Build.0 = Release|Any CPU
{00403BD6-7A26-4971-29D3-8A7849AAC770}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
{00403BD6-7A26-4971-29D3-8A7849AAC770}.Release|Mixed Platforms.Build.0 = Release|Any CPU
{00403BD6-7A26-4971-29D3-8A7849AAC770}.Release|x64.ActiveCfg = Release|Any CPU
{00403BD6-7A26-4971-29D3-8A7849AAC770}.Release|x64.Build.0 = Release|Any CPU
{00403BD6-7A26-4971-29D3-8A7849AAC770}.Release|x86.ActiveCfg = Release|Any CPU
{00403BD6-7A26-4971-29D3-8A7849AAC770}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection EndGlobalSection
GlobalSection(SolutionProperties) = preSolution GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE HideSolutionNode = FALSE

View File

@ -1,24 +0,0 @@
namespace EonaCat.HID
{
public class Attributes
{
// 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 Attributes(NativeMethods.HIDD_ATTRIBUTES attributes)
{
VendorId = attributes.VendorID;
ProductId = attributes.ProductID;
Version = attributes.VersionNumber;
VendorIdHex = "0x" + attributes.VendorID.ToString("X4");
ProductIdHex = "0x" + attributes.ProductID.ToString("X4");
}
public int VendorId { get; private set; }
public int ProductId { get; private set; }
public int Version { get; private set; }
public string VendorIdHex { get; set; }
public string ProductIdHex { get; set; }
}
}

View File

@ -1,44 +0,0 @@
namespace EonaCat.HID
{
public class Capabilities
{
// 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 Capabilities(NativeMethods.Capabilities capabilities)
{
Usage = capabilities.Usage;
UsagePage = capabilities.UsagePage;
InputReportByteLength = capabilities.InputReportByteLength;
OutputReportByteLength = capabilities.OutputReportByteLength;
FeatureReportByteLength = capabilities.FeatureReportByteLength;
Reserved = capabilities.Reserved;
NumberLinkCollectionNodes = capabilities.NumberLinkCollectionNodes;
NumberInputButtonCaps = capabilities.NumberInputButtonCaps;
NumberInputValueCaps = capabilities.NumberInputValueCaps;
NumberInputDataIndices = capabilities.NumberInputDataIndices;
NumberOutputButtonCaps = capabilities.NumberOutputButtonCaps;
NumberOutputValueCaps = capabilities.NumberOutputValueCaps;
NumberOutputDataIndices = capabilities.NumberOutputDataIndices;
NumberFeatureButtonCaps = capabilities.NumberFeatureButtonCaps;
NumberFeatureValueCaps = capabilities.NumberFeatureValueCaps;
NumberFeatureDataIndices = capabilities.NumberFeatureDataIndices;
}
public short Usage { get; private set; }
public short UsagePage { get; private set; }
public short InputReportByteLength { get; private set; }
public short OutputReportByteLength { get; private set; }
public short FeatureReportByteLength { get; private set; }
public short[] Reserved { get; private set; }
public short NumberLinkCollectionNodes { get; private set; }
public short NumberInputButtonCaps { get; private set; }
public short NumberInputValueCaps { get; private set; }
public short NumberInputDataIndices { get; private set; }
public short NumberOutputButtonCaps { get; private set; }
public short NumberOutputValueCaps { get; private set; }
public short NumberOutputDataIndices { get; private set; }
public short NumberFeatureButtonCaps { get; private set; }
public short NumberFeatureValueCaps { get; private set; }
public short NumberFeatureDataIndices { get; private set; }
}
}

View File

@ -1,842 +0,0 @@
using EonaCat.HID.Helpers;
using System;
using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
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.
public class Device : IDevice
{
public event InsertedEventHandler OnInserted;
public event RemovedEventHandler OnRemoved;
private const int BUFFER_SIZE = 254;
private readonly string _description;
private readonly string _path;
private readonly Attributes _attributes;
private readonly Capabilities _capabilities;
private readonly DeviceEventMonitor _deviceEventMonitor;
private bool _monitorDeviceEvents;
protected delegate DeviceData ReadDelegate(int timeout);
protected delegate Report ReadReportDelegate(int timeout);
private delegate bool WriteDelegate(byte[] data, int timeout);
private delegate bool WriteReportDelegate(Report report, int timeout);
internal Device(string devicePath, string description = null)
{
_deviceEventMonitor = new DeviceEventMonitor(this);
_deviceEventMonitor.Inserted += DeviceEventMonitorInserted;
_deviceEventMonitor.Removed += DeviceEventMonitorRemoved;
_path = devicePath;
_description = description;
try
{
_deviceModeHelper = new DeviceModeHelper();
var handle = OpenDeviceIO(_path, NativeMethods.ACCESS_NONE);
_attributes = GetDeviceAttributes(handle);
_capabilities = GetDeviceCapabilities(handle);
CloseDeviceIO(handle);
}
catch (Exception exception)
{
throw new Exception($"EonaCat HID: Error querying device '{devicePath}'.", exception);
}
}
public bool IsOpen { get; private set; }
public bool IsConnected
{ get { return Devices.IsConnected(_path); } }
public string Description
{ get { return _description; } }
public Capabilities Capabilities
{ get { return _capabilities; } }
public Attributes Info
{ get { return _attributes; } }
public string DevicePath
{ get { return _path; } }
public bool MonitorDeviceEvents
{
get { return _monitorDeviceEvents; }
set
{
if (value & !_monitorDeviceEvents)
{
_deviceEventMonitor.Init();
}
_monitorDeviceEvents = value;
}
}
private DeviceModeHelper _deviceModeHelper { get; }
public override string ToString()
{
return $"VendorID={_attributes.VendorIdHex}, ProductID={_attributes.ProductIdHex}, Version={_attributes.Version}, DevicePath={_path}";
}
public void OpenDevice()
{
OpenDevice(DeviceMode.NonOverlapped, DeviceMode.NonOverlapped, ShareMode.ShareRead | ShareMode.ShareWrite);
}
public void OpenDevice(DeviceMode readMode, DeviceMode writeMode, ShareMode shareMode)
{
if (IsOpen)
{
return;
}
_deviceModeHelper.ReadMode = readMode;
_deviceModeHelper.WriteMode = writeMode;
_deviceModeHelper.ShareMode = shareMode;
try
{
_deviceModeHelper.ReadHandle = OpenDeviceIO(_path, readMode, NativeMethods.GENERIC_READ, shareMode);
_deviceModeHelper.WriteHandle = OpenDeviceIO(_path, writeMode, NativeMethods.GENERIC_WRITE, shareMode);
}
catch (Exception exception)
{
IsOpen = false;
throw new Exception("EonaCat HID: Error opening device.", exception);
}
IsOpen = (_deviceModeHelper.ReadHandle.ToInt32() != NativeMethods.INVALID_HANDLE_VALUE && _deviceModeHelper.WriteHandle.ToInt32() != NativeMethods.INVALID_HANDLE_VALUE);
}
public void CloseDevice()
{
if (!IsOpen)
{
return;
}
CloseDeviceIO(_deviceModeHelper.ReadHandle);
CloseDeviceIO(_deviceModeHelper.WriteHandle);
IsOpen = false;
}
public DeviceData Read()
{
return Read(0);
}
public DeviceData Read(int timeout)
{
if (IsConnected)
{
if (!IsOpen)
{
OpenDevice(_deviceModeHelper.ReadMode, _deviceModeHelper.WriteMode, _deviceModeHelper.ShareMode);
}
try
{
return ReadData(timeout);
}
catch
{
return new DeviceData(DeviceData.ReadStatus.ReadError);
}
}
return new DeviceData(DeviceData.ReadStatus.NotConnected);
}
public void Read(ReadCallback callback)
{
Read(callback, 0);
}
public void Read(ReadCallback callback, int timeout)
{
var readDelegate = new ReadDelegate(Read);
var asyncState = new StateAsync(readDelegate, callback);
readDelegate.BeginInvoke(timeout, EndRead, asyncState);
}
public async Task<DeviceData> ReadAsync(int timeout = 0)
{
var readDelegate = new ReadDelegate(Read);
#if NET20 || NET35 || NET5_0_OR_GREATER
return await Task<DeviceData>.Factory.StartNew(() => readDelegate.Invoke(timeout));
#else
return await Task<DeviceData>.Factory.FromAsync(readDelegate.BeginInvoke, readDelegate.EndInvoke, timeout, null);
#endif
}
public Report ReadReport(int timeout = 0)
{
return new Report(Capabilities.InputReportByteLength, Read(timeout));
}
public void ReadReport(ReadReportCallback callback, int timeout = 0)
{
var readReportDelegate = new ReadReportDelegate(ReadReport);
var asyncState = new StateAsync(readReportDelegate, callback);
readReportDelegate.BeginInvoke(timeout, EndReadReport, asyncState);
}
public async Task<Report> ReadReportAsync(int timeout = 0)
{
var readReportDelegate = new ReadReportDelegate(ReadReport);
#if NET20 || NET35 || NET5_0_OR_GREATER
return await Task<Report>.Factory.StartNew(() => readReportDelegate.Invoke(timeout));
#else
return await Task<Report>.Factory.FromAsync(readReportDelegate.BeginInvoke, readReportDelegate.EndInvoke, timeout, null);
#endif
}
public Report ReadReportSync(byte reportId)
{
byte[] commandBuffer = new byte[Capabilities.InputReportByteLength];
commandBuffer[0] = reportId;
bool bSuccess = NativeMethods.HidD_GetInputReport(_deviceModeHelper.ReadHandle, commandBuffer, commandBuffer.Length);
DeviceData deviceData = new DeviceData(commandBuffer, bSuccess ? DeviceData.ReadStatus.Success : DeviceData.ReadStatus.NoDataRead);
return new Report(Capabilities.InputReportByteLength, deviceData);
}
public bool ReadFeatureData(out byte[] data, byte reportId = 0)
{
if (_capabilities.FeatureReportByteLength <= 0)
{
data = new byte[0];
return false;
}
data = new byte[_capabilities.FeatureReportByteLength];
var buffer = CreateFeatureOutputBuffer();
buffer[0] = reportId;
IntPtr hidHandle = IntPtr.Zero;
bool success = false;
try
{
if (IsOpen)
{
hidHandle = _deviceModeHelper.ReadHandle;
}
else
{
hidHandle = OpenDeviceIO(_path, NativeMethods.ACCESS_NONE);
}
success = NativeMethods.HidD_GetFeature(hidHandle, buffer, buffer.Length);
if (success)
{
Array.Copy(buffer, 0, data, 0, Math.Min(data.Length, _capabilities.FeatureReportByteLength));
}
}
catch (Exception exception)
{
throw new Exception(string.Format("EonaCat HID: Error accessing device '{0}'.", _path), exception);
}
finally
{
if (hidHandle != IntPtr.Zero && hidHandle != _deviceModeHelper.ReadHandle)
{
CloseDeviceIO(hidHandle);
}
}
return success;
}
public string ReadProductName()
{
if (ReadProductName(out byte[] bytes))
{
return ByteHelper.GetFilledBytesString(bytes);
}
return string.Empty;
}
public bool ReadProductName(out byte[] data)
{
data = new byte[BUFFER_SIZE];
IntPtr handle = IntPtr.Zero;
bool success = false;
try
{
if (IsOpen)
{
handle = _deviceModeHelper.ReadHandle;
}
else
{
handle = OpenDeviceIO(_path, NativeMethods.ACCESS_NONE);
}
success = NativeMethods.HidD_GetProductString(handle, data, data.Length);
}
catch (Exception exception)
{
throw new Exception(string.Format("EonaCat HID: Error accessing device '{0}'.", _path), exception);
}
finally
{
if (handle != IntPtr.Zero && handle != _deviceModeHelper.ReadHandle)
{
CloseDeviceIO(handle);
}
}
return success;
}
public string ReadManufacturer()
{
if (ReadManufacturer(out byte[] bytes))
{
return ByteHelper.GetFilledBytesString(bytes);
}
return string.Empty;
}
public bool ReadManufacturer(out byte[] data)
{
data = new byte[BUFFER_SIZE];
IntPtr handle = IntPtr.Zero;
bool success = false;
try
{
if (IsOpen)
{
handle = _deviceModeHelper.ReadHandle;
}
else
{
handle = OpenDeviceIO(_path, NativeMethods.ACCESS_NONE);
}
success = NativeMethods.HidD_GetManufacturerString(handle, data, data.Length);
}
catch (Exception exception)
{
throw new Exception(string.Format("EonaCat HID: Error accessing device '{0}'.", _path), exception);
}
finally
{
if (handle != IntPtr.Zero && handle != _deviceModeHelper.ReadHandle)
{
CloseDeviceIO(handle);
}
}
return success;
}
public string ReadSerialNumber()
{
if (ReadSerialNumber(out byte[] bytes))
{
return ByteHelper.GetFilledBytesString(bytes);
}
return string.Empty;
}
public bool ReadSerialNumber(out byte[] data)
{
data = new byte[BUFFER_SIZE];
IntPtr hidHandle = IntPtr.Zero;
bool success = false;
try
{
if (IsOpen)
{
hidHandle = _deviceModeHelper.ReadHandle;
}
else
{
hidHandle = OpenDeviceIO(_path, NativeMethods.ACCESS_NONE);
}
success = NativeMethods.HidD_GetSerialNumberString(hidHandle, data, data.Length);
}
catch (Exception exception)
{
throw new Exception(string.Format("EonaCat HID: Error accessing device '{0}'.", _path), exception);
}
finally
{
if (hidHandle != IntPtr.Zero && hidHandle != _deviceModeHelper.ReadHandle)
{
CloseDeviceIO(hidHandle);
}
}
return success;
}
public bool Write(byte[] data)
{
return Write(data, 0);
}
public bool Write(byte[] data, int timeout)
{
if (IsConnected)
{
if (!IsOpen)
{
OpenDevice(_deviceModeHelper.ReadMode, _deviceModeHelper.WriteMode, _deviceModeHelper.ShareMode);
}
try
{
return WriteData(data, timeout);
}
catch
{
return false;
}
}
return false;
}
public void Write(byte[] data, WriteCallback callback)
{
Write(data, callback, 0);
}
public void Write(byte[] data, WriteCallback callback, int timeout)
{
var writeDelegate = new WriteDelegate(Write);
var asyncState = new StateAsync(writeDelegate, callback);
writeDelegate.BeginInvoke(data, timeout, EndWrite, asyncState);
}
public async Task<bool> WriteAsync(byte[] data, int timeout = 0)
{
var writeDelegate = new WriteDelegate(Write);
#if NET20 || NET35 || NET5_0_OR_GREATER
return await Task<bool>.Factory.StartNew(() => writeDelegate.Invoke(data, timeout));
#else
return await Task<bool>.Factory.FromAsync(writeDelegate.BeginInvoke, writeDelegate.EndInvoke, data, timeout, null);
#endif
}
public bool WriteReport(Report report)
{
return WriteReport(report, 0);
}
public bool WriteReport(Report report, int timeout)
{
return Write(report.GetBytes(), timeout);
}
public void WriteReport(Report report, WriteCallback callback)
{
WriteReport(report, callback, 0);
}
public void WriteReport(Report report, WriteCallback callback, int timeout)
{
var writeReportDelegate = new WriteReportDelegate(WriteReport);
var asyncState = new StateAsync(writeReportDelegate, callback);
writeReportDelegate.BeginInvoke(report, timeout, EndWriteReport, asyncState);
}
/// <summary>
/// Handle data transfers on the control channel.
/// This method places data on the control channel for devices
/// that do not support the interupt transfers
/// </summary>
/// <param name="report">The outbound HID report</param>
/// <returns>The result of the tranfer request: true if successful otherwise false</returns>
///
public bool WriteReportSync(Report report)
{
if (null != report)
{
byte[] buffer = report.GetBytes();
return (NativeMethods.HidD_SetOutputReport(_deviceModeHelper.WriteHandle, buffer, buffer.Length));
}
else
{
throw new ArgumentException("The output report is null, it must be allocated before you call this method", "report");
}
}
public async Task<bool> WriteReportAsync(Report report, int timeout = 0)
{
var writeReportDelegate = new WriteReportDelegate(WriteReport);
#if NET20 || NET35 || NET5_0_OR_GREATER
return await Task<bool>.Factory.StartNew(() => writeReportDelegate.Invoke(report, timeout));
#else
return await Task<bool>.Factory.FromAsync(writeReportDelegate.BeginInvoke, writeReportDelegate.EndInvoke, report, timeout, null);
#endif
}
public Report CreateReport()
{
return new Report(Capabilities.OutputReportByteLength);
}
public bool WriteFeatureData(byte[] data)
{
if (_capabilities.FeatureReportByteLength <= 0)
{
return false;
}
var buffer = CreateFeatureOutputBuffer();
Array.Copy(data, 0, buffer, 0, Math.Min(data.Length, _capabilities.FeatureReportByteLength));
IntPtr hidHandle = IntPtr.Zero;
bool success = false;
try
{
if (IsOpen)
{
hidHandle = _deviceModeHelper.WriteHandle;
}
else
{
hidHandle = OpenDeviceIO(_path, NativeMethods.ACCESS_NONE);
}
//var overlapped = new NativeOverlapped();
success = NativeMethods.HidD_SetFeature(hidHandle, buffer, buffer.Length);
}
catch (Exception exception)
{
throw new Exception(string.Format("EonaCat HID: Error accessing device '{0}'.", _path), exception);
}
finally
{
if (hidHandle != IntPtr.Zero && hidHandle != _deviceModeHelper.WriteHandle)
{
CloseDeviceIO(hidHandle);
}
}
return success;
}
protected static void EndRead(IAsyncResult ar)
{
var hidAsyncState = (StateAsync)ar.AsyncState;
var callerDelegate = (ReadDelegate)hidAsyncState.CallerDelegate;
var callbackDelegate = (ReadCallback)hidAsyncState.CallbackDelegate;
var data = callerDelegate.EndInvoke(ar);
if ((callbackDelegate != null))
{
callbackDelegate.Invoke(data);
}
}
protected static void EndReadReport(IAsyncResult ar)
{
var hidAsyncState = (StateAsync)ar.AsyncState;
var callerDelegate = (ReadReportDelegate)hidAsyncState.CallerDelegate;
var callbackDelegate = (ReadReportCallback)hidAsyncState.CallbackDelegate;
var report = callerDelegate.EndInvoke(ar);
if ((callbackDelegate != null))
{
callbackDelegate.Invoke(report);
}
}
private static void EndWrite(IAsyncResult ar)
{
var hidAsyncState = (StateAsync)ar.AsyncState;
var callerDelegate = (WriteDelegate)hidAsyncState.CallerDelegate;
var callbackDelegate = (WriteCallback)hidAsyncState.CallbackDelegate;
var result = callerDelegate.EndInvoke(ar);
if ((callbackDelegate != null))
{
callbackDelegate.Invoke(result);
}
}
private static void EndWriteReport(IAsyncResult ar)
{
var hidAsyncState = (StateAsync)ar.AsyncState;
var callerDelegate = (WriteReportDelegate)hidAsyncState.CallerDelegate;
var callbackDelegate = (WriteCallback)hidAsyncState.CallbackDelegate;
var result = callerDelegate.EndInvoke(ar);
if ((callbackDelegate != null))
{
callbackDelegate.Invoke(result);
}
}
private byte[] CreateInputBuffer()
{
return CreateBuffer(Capabilities.InputReportByteLength - 1);
}
private byte[] CreateOutputBuffer()
{
return CreateBuffer(Capabilities.OutputReportByteLength - 1);
}
private byte[] CreateFeatureOutputBuffer()
{
return CreateBuffer(Capabilities.FeatureReportByteLength - 1);
}
private static byte[] CreateBuffer(int length)
{
byte[] buffer = null;
Array.Resize(ref buffer, length + 1);
return buffer;
}
private static Attributes GetDeviceAttributes(IntPtr hidHandle)
{
var deviceAttributes = default(NativeMethods.HIDD_ATTRIBUTES);
deviceAttributes.Size = Marshal.SizeOf(deviceAttributes);
NativeMethods.HidD_GetAttributes(hidHandle, ref deviceAttributes);
return new Attributes(deviceAttributes);
}
private static Capabilities GetDeviceCapabilities(IntPtr hidHandle)
{
var capabilities = default(NativeMethods.Capabilities);
var preparsedDataPointer = default(IntPtr);
if (NativeMethods.HidD_GetPreparsedData(hidHandle, ref preparsedDataPointer))
{
NativeMethods.HidP_GetCaps(preparsedDataPointer, ref capabilities);
NativeMethods.HidD_FreePreparsedData(preparsedDataPointer);
}
return new Capabilities(capabilities);
}
private bool WriteData(byte[] data, int timeout)
{
if (_capabilities.OutputReportByteLength <= 0)
{
return false;
}
var buffer = CreateOutputBuffer();
uint bytesWritten;
Array.Copy(data, 0, buffer, 0, Math.Min(data.Length, _capabilities.OutputReportByteLength));
if (_deviceModeHelper.WriteMode == DeviceMode.Overlapped)
{
var security = new NativeMethods.SECURITY_ATTRIBUTES();
var overlapped = new NativeOverlapped();
var overlapTimeout = timeout <= 0 ? NativeMethods.WAIT_INFINITE : timeout;
security.lpSecurityDescriptor = IntPtr.Zero;
security.bInheritHandle = true;
security.nLength = Marshal.SizeOf(security);
overlapped.OffsetLow = 0;
overlapped.OffsetHigh = 0;
overlapped.EventHandle = NativeMethods.CreateEvent(ref security, Convert.ToInt32(false), Convert.ToInt32(true), "");
try
{
NativeMethods.WriteFile(_deviceModeHelper.WriteHandle, buffer, (uint)buffer.Length, out bytesWritten, ref overlapped);
}
catch { return false; }
var result = NativeMethods.WaitForSingleObject(overlapped.EventHandle, overlapTimeout);
switch (result)
{
case NativeMethods.WAIT_OBJECT_0:
return true;
case NativeMethods.WAIT_TIMEOUT:
return false;
case NativeMethods.WAIT_FAILED:
return false;
default:
return false;
}
}
else
{
try
{
var overlapped = new NativeOverlapped();
return NativeMethods.WriteFile(_deviceModeHelper.WriteHandle, buffer, (uint)buffer.Length, out bytesWritten, ref overlapped);
}
catch { return false; }
}
}
protected DeviceData ReadData(int timeout)
{
var buffer = new byte[] { };
var status = DeviceData.ReadStatus.NoDataRead;
IntPtr nonManagedBuffer;
if (_capabilities.InputReportByteLength > 0)
{
uint bytesRead;
buffer = CreateInputBuffer();
nonManagedBuffer = Marshal.AllocHGlobal(buffer.Length);
if (_deviceModeHelper.ReadMode == DeviceMode.Overlapped)
{
var security = new NativeMethods.SECURITY_ATTRIBUTES();
var overlapped = new NativeOverlapped();
var overlapTimeout = timeout <= 0 ? NativeMethods.WAIT_INFINITE : timeout;
security.lpSecurityDescriptor = IntPtr.Zero;
security.bInheritHandle = true;
security.nLength = Marshal.SizeOf(security);
overlapped.OffsetLow = 0;
overlapped.OffsetHigh = 0;
overlapped.EventHandle = NativeMethods.CreateEvent(ref security, Convert.ToInt32(false), Convert.ToInt32(true), string.Empty);
try
{
var success = NativeMethods.ReadFile(_deviceModeHelper.ReadHandle, nonManagedBuffer, (uint)buffer.Length, out bytesRead, ref overlapped);
if (success)
{
status = DeviceData.ReadStatus.Success; // No check here to see if bytesRead > 0 . Perhaps not necessary?
}
else
{
var result = NativeMethods.WaitForSingleObject(overlapped.EventHandle, overlapTimeout);
switch (result)
{
case NativeMethods.WAIT_OBJECT_0:
status = DeviceData.ReadStatus.Success;
NativeMethods.GetOverlappedResult(_deviceModeHelper.ReadHandle, ref overlapped, out bytesRead, false);
break;
case NativeMethods.WAIT_TIMEOUT:
status = DeviceData.ReadStatus.WaitTimedOut;
buffer = new byte[] { };
break;
case NativeMethods.WAIT_FAILED:
status = DeviceData.ReadStatus.WaitFail;
buffer = new byte[] { };
break;
default:
status = DeviceData.ReadStatus.NoDataRead;
buffer = new byte[] { };
break;
}
}
Marshal.Copy(nonManagedBuffer, buffer, 0, (int)bytesRead);
}
catch { status = DeviceData.ReadStatus.ReadError; }
finally
{
CloseDeviceIO(overlapped.EventHandle);
Marshal.FreeHGlobal(nonManagedBuffer);
}
}
else
{
try
{
var overlapped = new NativeOverlapped();
NativeMethods.ReadFile(_deviceModeHelper.ReadHandle, nonManagedBuffer, (uint)buffer.Length, out bytesRead, ref overlapped);
status = DeviceData.ReadStatus.Success;
Marshal.Copy(nonManagedBuffer, buffer, 0, (int)bytesRead);
}
catch { status = DeviceData.ReadStatus.ReadError; }
finally { Marshal.FreeHGlobal(nonManagedBuffer); }
}
}
return new DeviceData(buffer, status);
}
private static IntPtr OpenDeviceIO(string devicePath, uint deviceAccess)
{
return OpenDeviceIO(devicePath, DeviceMode.NonOverlapped, deviceAccess, ShareMode.ShareRead | ShareMode.ShareWrite);
}
private static IntPtr OpenDeviceIO(string devicePath, DeviceMode deviceMode, uint deviceAccess, ShareMode shareMode)
{
var security = new NativeMethods.SECURITY_ATTRIBUTES();
var flags = 0;
if (deviceMode == DeviceMode.Overlapped)
{
flags = NativeMethods.FILE_FLAG_OVERLAPPED;
}
security.lpSecurityDescriptor = IntPtr.Zero;
security.bInheritHandle = true;
security.nLength = Marshal.SizeOf(security);
return NativeMethods.CreateFile(devicePath, deviceAccess, (int)shareMode, ref security, NativeMethods.OPEN_EXISTING, flags, hTemplateFile: IntPtr.Zero);
}
private static void CloseDeviceIO(IntPtr handle)
{
if (Environment.OSVersion.Version.Major > 5)
{
NativeMethods.CancelIoEx(handle, IntPtr.Zero);
}
NativeMethods.CloseHandle(handle);
}
private void DeviceEventMonitorInserted()
{
if (!IsOpen)
{
OpenDevice(_deviceModeHelper.ReadMode, _deviceModeHelper.WriteMode, _deviceModeHelper.ShareMode);
}
OnInserted?.Invoke();
}
private void DeviceEventMonitorRemoved()
{
if (IsOpen)
{
CloseDevice();
}
OnRemoved?.Invoke();
}
public void Dispose()
{
if (MonitorDeviceEvents)
{
MonitorDeviceEvents = false;
}
if (IsOpen)
{
CloseDevice();
}
}
}
}

View File

@ -1,32 +0,0 @@
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.
public class DeviceData
{
public enum ReadStatus
{
Success = 0,
WaitTimedOut = 1,
WaitFail = 2,
NoDataRead = 3,
ReadError = 4,
NotConnected = 5
}
public DeviceData(ReadStatus status)
{
Data = new byte[] { };
Status = status;
}
public DeviceData(byte[] data, ReadStatus status)
{
Data = data;
Status = status;
}
public byte[] Data { get; private set; }
public ReadStatus Status { get; private set; }
}
}

View File

@ -1,54 +0,0 @@
using System.Threading.Tasks;
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 class DeviceEventMonitor
{
public event InsertedEventHandler Inserted;
public event RemovedEventHandler Removed;
public delegate void InsertedEventHandler();
public delegate void RemovedEventHandler();
private readonly Device _device;
private bool _wasConnected;
public DeviceEventMonitor(Device device)
{
_device = device;
}
public void Init()
{
Task.Run(EventMonitor);
}
private async Task EventMonitor()
{
while (_device.MonitorDeviceEvents)
{
var isConnected = _device.IsConnected;
if (isConnected != _wasConnected)
{
if (isConnected)
{
Inserted?.Invoke();
}
else
{
Removed?.Invoke();
}
_wasConnected = isConnected;
}
await Task.Delay(100);
}
}
}
}

View File

@ -1,180 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
namespace EonaCat.HID
{
public class Devices
{
private static Guid _hidClassGuid = Guid.Empty;
public static bool IsConnected(string devicePath)
{
return EnumerateDevices().Any(x => x.Path == devicePath);
}
public static Device GetDevice(string devicePath)
{
return Enumerate(devicePath).FirstOrDefault();
}
public static IEnumerable<Device> Enumerate()
{
return EnumerateDevices().Select(x => new Device(x.Path, x.Description));
}
public static IEnumerable<Device> Enumerate(string devicePath)
{
return EnumerateDevices().Where(x => x.Path == devicePath).Select(x => new Device(x.Path, x.Description));
}
public static IEnumerable<Device> Enumerate(int vendorId, params int[] productIds)
{
return EnumerateDevices().Select(x => new Device(x.Path, x.Description)).Where(x => x.Info.VendorId == vendorId &&
productIds.Contains(x.Info.ProductId));
}
public static IEnumerable<Device> Enumerate(int vendorId, int productId, ushort UsagePage)
{
return EnumerateDevices().Select(x => new Device(x.Path, x.Description)).Where(x => x.Info.VendorId == vendorId &&
productId == (ushort)x.Info.ProductId && (ushort)x.Capabilities.UsagePage == UsagePage);
}
public static IEnumerable<Device> Enumerate(int vendorId)
{
return EnumerateDevices().Select(x => new Device(x.Path, x.Description)).Where(x => x.Info.VendorId == vendorId);
}
public static IEnumerable<Device> Enumerate(ushort UsagePage)
{
return EnumerateDevices().Select(x => new Device(x.Path, x.Description)).Where(x => (ushort)x.Capabilities.UsagePage == UsagePage);
}
internal class DeviceInfo
{ public string Path { get; set; } public string Description { get; set; } }
internal static IEnumerable<DeviceInfo> EnumerateDevices()
{
var devices = new List<DeviceInfo>();
var hidClass = HidClassGuid;
var deviceInfoSet = NativeMethods.SetupDiGetClassDevs(ref hidClass, null, hwndParent: IntPtr.Zero, NativeMethods.DIGCF_PRESENT | NativeMethods.DIGCF_DEVICEINTERFACE);
if (deviceInfoSet.ToInt64() != NativeMethods.INVALID_HANDLE_VALUE)
{
var deviceInfoData = CreateDeviceInfoData();
var deviceIndex = 0;
while (NativeMethods.SetupDiEnumDeviceInfo(deviceInfoSet, deviceIndex, ref deviceInfoData))
{
deviceIndex += 1;
var deviceInterfaceData = new NativeMethods.SP_DEVICE_INTERFACE_DATA();
deviceInterfaceData.cbSize = Marshal.SizeOf(deviceInterfaceData);
var deviceInterfaceIndex = 0;
while (NativeMethods.SetupDiEnumDeviceInterfaces(deviceInfoSet, ref deviceInfoData, ref hidClass, deviceInterfaceIndex, ref deviceInterfaceData))
{
deviceInterfaceIndex++;
var devicePath = GetDevicePath(deviceInfoSet, deviceInterfaceData);
var description = GetBusReportedDeviceDescription(deviceInfoSet, ref deviceInfoData) ??
GetDeviceDescription(deviceInfoSet, ref deviceInfoData);
devices.Add(new DeviceInfo { Path = devicePath, Description = description });
}
}
NativeMethods.SetupDiDestroyDeviceInfoList(deviceInfoSet);
}
return devices;
}
private static NativeMethods.SP_DEVINFO_DATA CreateDeviceInfoData()
{
var deviceInfoData = new NativeMethods.SP_DEVINFO_DATA();
deviceInfoData.cbSize = Marshal.SizeOf(deviceInfoData);
deviceInfoData.DevInst = 0;
deviceInfoData.ClassGuid = Guid.Empty;
deviceInfoData.Reserved = IntPtr.Zero;
return deviceInfoData;
}
private static string GetDevicePath(IntPtr deviceInfoSet, NativeMethods.SP_DEVICE_INTERFACE_DATA deviceInterfaceData)
{
var bufferSize = 0;
var interfaceDetail = new NativeMethods.SP_DEVICE_INTERFACE_DETAIL_DATA { Size = IntPtr.Size == 4 ? 4 + Marshal.SystemDefaultCharSize : 8 };
NativeMethods.SetupDiGetDeviceInterfaceDetail(deviceInfoSet, ref deviceInterfaceData, IntPtr.Zero, 0, ref bufferSize, IntPtr.Zero);
return NativeMethods.SetupDiGetDeviceInterfaceDetail(deviceInfoSet, ref deviceInterfaceData, ref interfaceDetail, bufferSize, ref bufferSize, IntPtr.Zero) ?
interfaceDetail.DevicePath : null;
}
private static Guid HidClassGuid
{
get
{
if (_hidClassGuid.Equals(Guid.Empty))
{
NativeMethods.HidD_GetHidGuid(ref _hidClassGuid);
}
return _hidClassGuid;
}
}
private static string GetDeviceDescription(IntPtr deviceInfoSet, ref NativeMethods.SP_DEVINFO_DATA devinfoData)
{
unsafe
{
const int charCount = 1024;
var descriptionBuffer = stackalloc char[charCount];
var requiredSize = 0;
var type = 0;
if (NativeMethods.SetupDiGetDeviceRegistryProperty(deviceInfoSet,
ref devinfoData,
NativeMethods.SPDRP_DEVICEDESC,
ref type,
descriptionBuffer,
propertyBufferSize: charCount * sizeof(char),
ref requiredSize))
{
return new string(descriptionBuffer);
}
return null;
}
}
private static string GetBusReportedDeviceDescription(IntPtr deviceInfoSet, ref NativeMethods.SP_DEVINFO_DATA devinfoData)
{
unsafe
{
const int charCount = 1024;
var descriptionBuffer = stackalloc char[charCount];
if (Environment.OSVersion.Version.Major > 5)
{
uint propertyType = 0;
var requiredSize = 0;
if (NativeMethods.SetupDiGetDeviceProperty(deviceInfoSet,
ref devinfoData,
ref NativeMethods.DEVPKEY_Device_BusReportedDeviceDesc,
ref propertyType,
descriptionBuffer,
propertyBufferSize: charCount * sizeof(char),
ref requiredSize,
0))
{
return new string(descriptionBuffer);
}
}
return null;
}
}
}
}

View File

@ -1,40 +0,0 @@
using System.Collections.Generic;
using System.Linq;
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.
public class Enumerator : IEnumerator
{
public bool IsConnected(string devicePath)
{
return Devices.IsConnected(devicePath);
}
public IDevice GetDevice(string devicePath)
{
return Devices.GetDevice(devicePath);
}
public IEnumerable<IDevice> Enumerate()
{
return Devices.Enumerate().Select(x => x as IDevice);
}
public IEnumerable<IDevice> Enumerate(string devicePath)
{
return Devices.Enumerate(devicePath).Select(x => x as IDevice);
}
public IEnumerable<IDevice> Enumerate(int vendorId, params int[] productIds)
{
return Devices.Enumerate(vendorId, productIds).Select(x => x as IDevice);
}
public IEnumerable<IDevice> Enumerate(int vendorId)
{
return Devices.Enumerate(vendorId).Select(x => x as IDevice);
}
}
}

View File

@ -1,12 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFrameworks>net45;netstandard2</TargetFrameworks> <TargetFrameworks>net46;net6.0</TargetFrameworks>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks> <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<Company>EonaCat (Jeroen Saey)</Company> <Company>EonaCat (Jeroen Saey)</Company>
<Copyright>Copyright 2024 EonaCat (Jeroen Saey)</Copyright> <Copyright>Copyright 2024 EonaCat (Jeroen Saey)</Copyright>
<LangVersion>latest</LangVersion>
<PackageId>EonaCat.HID</PackageId> <PackageId>EonaCat.HID</PackageId>
<Version>1.0.3</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>
@ -19,17 +20,6 @@
<PackageReadmeFile>README.md</PackageReadmeFile> <PackageReadmeFile>README.md</PackageReadmeFile>
</PropertyGroup> </PropertyGroup>
<PropertyGroup>
<EVRevisionFormat>1.0.2+{chash:10}.{c:ymd}</EVRevisionFormat>
<EVDefault>true</EVDefault>
<EVInfo>true</EVInfo>
<EVTagMatch>v[0-9]*</EVTagMatch>
<EVRemoveTagV>true</EVRemoveTagV>
<EVVcs>git</EVVcs>
<EVCheckAllAttributes>true</EVCheckAllAttributes>
<EVShowRevision>true</EVShowRevision>
</PropertyGroup>
<ItemGroup> <ItemGroup>
<None Include="..\icon.png"> <None Include="..\icon.png">
<Pack>True</Pack> <Pack>True</Pack>
@ -46,10 +36,13 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="EonaCat.Versioning" Version="1.0.5"> <PackageReference Include="System.Runtime.InteropServices.RuntimeInformation" Version="4.3.0" />
<PrivateAssets>all</PrivateAssets> </ItemGroup>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> <ItemGroup>
<Reference Include="System.Windows.Forms">
<HintPath>..\..\..\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETFramework\v4.8\System.Windows.Forms.dll</HintPath>
</Reference>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View File

@ -0,0 +1,19 @@
using System;
namespace EonaCat.HID.EventArguments
{
/// <summary>
/// Event args for received data
/// </summary>
public class HidDataReceivedEventArgs : EventArgs
{
public IHid Device { get; }
public byte[] Data { get; }
public HidDataReceivedEventArgs(IHid device, byte[] data)
{
Device = device;
Data = data;
}
}
}

View File

@ -0,0 +1,19 @@
using System;
namespace EonaCat.HID.EventArguments
{
/// <summary>
/// Event args for error events
/// </summary>
public class HidErrorEventArgs : EventArgs
{
public IHid Device { get; }
public Exception Exception { get; }
public HidErrorEventArgs(IHid device, Exception ex)
{
Device = device;
Exception = ex;
}
}
}

View File

@ -0,0 +1,17 @@
using System;
namespace EonaCat.HID.EventArguments
{
/// <summary>
/// Event arguments with HID device info.
/// </summary>
public class HidEventArgs : EventArgs
{
public IHid Device { get; }
public HidEventArgs(IHid device)
{
Device = device;
}
}
}

View File

@ -1,13 +0,0 @@
using System;
namespace EonaCat.HID.Helpers
{
internal class DeviceModeHelper
{
public IntPtr ReadHandle { get; set; }
public IntPtr WriteHandle { get; set; }
public DeviceMode ReadMode { get; set; } = DeviceMode.NonOverlapped;
public DeviceMode WriteMode { get; set; } = DeviceMode.NonOverlapped;
public ShareMode ShareMode = ShareMode.ShareRead | ShareMode.ShareWrite;
}
}

68
EonaCat.HID/HidFactory.cs Normal file
View File

@ -0,0 +1,68 @@
using System;
using System.Runtime.InteropServices;
namespace EonaCat.HID
{
/// <summary>
/// Main static factory class to create platform-specific implementations.
/// </summary>
public static class HidFactory
{
public static string GetPlatform()
{
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
return "Windows";
}
if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
{
return "macOS";
}
if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
{
return "Linux";
}
return "Unknown";
}
public static bool IsWindows()
{
return RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
}
public static bool IsLinux()
{
return RuntimeInformation.IsOSPlatform(OSPlatform.Linux);
}
public static bool IsMacOS()
{
return RuntimeInformation.IsOSPlatform(OSPlatform.OSX);
}
public static IHidManager CreateDeviceManager()
{
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
return new Managers.Windows.HidManagerWindows();
}
if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
{
return new Managers.Linux.HidManagerLinux();
}
if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
{
return new Managers.Mac.HidManagerMac();
}
else
{
throw new PlatformNotSupportedException("Unsupported platform");
}
}
}
}

392
EonaCat.HID/HidLinux.cs Normal file
View File

@ -0,0 +1,392 @@
using EonaCat.HID.EventArguments;
using EonaCat.HID.Managers;
using EonaCat.HID.Managers.Linux;
using System;
using System.Collections.Generic;
using System.IO;
using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
namespace EonaCat.HID
{
internal sealed class HidLinux : IHid
{
private readonly string _devicePath;
private FileStream _stream;
private readonly object _lock = new object();
private CancellationTokenSource _cts;
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 IDictionary<string, object> Capabilities { get; private set; } = new Dictionary<string, object>();
public event EventHandler<HidDataReceivedEventArgs> OnDataReceived;
public event EventHandler<HidErrorEventArgs> OnError;
public HidLinux(string devicePath)
{
_devicePath = devicePath ?? throw new ArgumentNullException(nameof(devicePath));
}
internal void Setup()
{
Open();
LoadDeviceInfo();
LoadReportLengths();
Close();
}
public void Open()
{
lock (_lock)
{
if (_stream != null)
{
return;
}
// Open device for read/write
var fd = NativeMethods.open(_devicePath, NativeMethods.O_RDWR | NativeMethods.O_NONBLOCK);
if (fd < 0)
{
OnError?.Invoke(this, new HidErrorEventArgs(this, new IOException($"Failed to open device: {_devicePath}")));
return;
}
var safeHandle = new Microsoft.Win32.SafeHandles.SafeFileHandle(new IntPtr(fd), ownsHandle: true);
_stream = new FileStream(safeHandle, FileAccess.ReadWrite, bufferSize: 64, isAsync: false);
}
}
public void Close()
{
lock (_lock)
{
StopListening();
if (_stream != null)
{
_stream.Dispose();
_stream = null;
}
}
}
public void Dispose()
{
Close();
}
private void LoadDeviceInfo()
{
string sysPath = GetSysfsDevicePath(_devicePath);
if (string.IsNullOrEmpty(sysPath))
{
return;
}
VendorId = ReadHexFileAsUShort(Path.Combine(sysPath, "idVendor"));
ProductId = ReadHexFileAsUShort(Path.Combine(sysPath, "idProduct"));
SerialNumber = ReadTextFile(Path.Combine(sysPath, "serial"));
Manufacturer = ReadTextFile(Path.Combine(sysPath, "manufacturer"));
ProductName = ReadTextFile(Path.Combine(sysPath, "product"));
}
private void LoadReportLengths()
{
// As Linux/hidraw interface does not provide direct API for report lengths,
// We fallback to standard max USB HID report size or fixed size
// TODO: Maybe we should use hidapi or libusb bindings?
InputReportByteLength = 64; // usually max 64 on USB HID
OutputReportByteLength = 64;
FeatureReportByteLength = 64;
}
private string GetSysfsDevicePath(string devPath)
{
// Mapping /dev/hidrawX to sysfs device path: /sys/class/hidraw/hidrawX/device/
var devName = Path.GetFileName(devPath);
var sysDevicePath = Path.Combine("/sys/class/hidraw", devName, "device");
if (Directory.Exists(sysDevicePath))
{
return sysDevicePath;
}
return null;
}
private ushort ReadHexFileAsUShort(string path)
{
try
{
if (File.Exists(path))
{
string text = File.ReadAllText(path).Trim().ToLowerInvariant();
if (text.StartsWith("0x"))
{
text = text.Substring(2);
}
if (ushort.TryParse(text, System.Globalization.NumberStyles.HexNumber, null, out ushort val))
{
return val;
}
}
}
catch { }
return 0;
}
private string ReadTextFile(string path)
{
try
{
if (File.Exists(path))
{
return File.ReadAllText(path).Trim();
}
}
catch { }
return null;
}
public async Task WriteOutputReportAsync(byte[] data)
{
if (data == null)
{
OnError?.Invoke(this, new HidErrorEventArgs(this, new ArgumentNullException(nameof(data))));
return;
}
if (_stream == null)
{
OnError?.Invoke(this, new HidErrorEventArgs(this, new InvalidOperationException("Device not open")));
return;
}
await Task.Run(() =>
{
lock (_lock)
{
try
{
_stream.Write(data, 0, data.Length);
_stream.Flush();
}
catch (Exception ex)
{
OnError?.Invoke(this, new HidErrorEventArgs(this, ex));
}
}
});
}
public async Task<byte[]> ReadInputReportAsync()
{
if (_stream == null)
{
OnError?.Invoke(this, new HidErrorEventArgs(this, new InvalidOperationException("Device not open")));
return Array.Empty<byte>();
}
return await Task.Run(() =>
{
byte[] buffer = new byte[InputReportByteLength];
int bytesRead = 0;
lock (_lock)
{
try
{
bytesRead = _stream.Read(buffer, 0, buffer.Length);
}
catch (Exception ex)
{
OnError?.Invoke(this, new HidErrorEventArgs(this, ex));
}
}
if (bytesRead <= 0)
{
return Array.Empty<byte>();
}
byte[] result = new byte[bytesRead];
Array.Copy(buffer, result, bytesRead);
return result;
});
}
public async Task SendFeatureReportAsync(byte[] data)
{
if (data == null || data.Length == 0)
throw new ArgumentException("Feature report data must not be null or empty.");
int fd = NativeMethods.open(_devicePath, NativeMethods.O_RDWR);
if (fd < 0)
{
OnError?.Invoke(this, new HidErrorEventArgs(this, new IOException($"Failed to open device: {_devicePath}")));
return;
}
try
{
int size = data.Length;
IntPtr buffer = Marshal.AllocHGlobal(size);
Marshal.Copy(data, 0, buffer, size);
int request = NativeMethods._IOC(NativeMethods._IOC_WRITE, 'H', 0x06, size); // HIDIOCSFEATURE
int result = NativeMethods.ioctl(fd, request, buffer);
if (result < 0)
{
OnError?.Invoke(this, new HidErrorEventArgs(this, new IOException("ioctl HIDIOCSFEATURE failed")));
return;
}
Marshal.FreeHGlobal(buffer);
}
finally
{
NativeMethods.close(fd);
}
await Task.CompletedTask;
}
public async Task<byte[]> GetFeatureReportAsync(byte reportId)
{
const int maxFeatureSize = 256;
byte[] buffer = new byte[maxFeatureSize];
buffer[0] = reportId;
int fd = NativeMethods.open(_devicePath, NativeMethods.O_RDWR);
if (fd < 0)
{
OnError?.Invoke(this, new HidErrorEventArgs(this, new IOException($"Failed to open device: {_devicePath}")));
return Array.Empty<byte>();
}
try
{
IntPtr bufPtr = Marshal.AllocHGlobal(buffer.Length);
Marshal.Copy(buffer, 0, bufPtr, buffer.Length);
int request = NativeMethods._IOC(NativeMethods._IOC_READ, 'H', 0x07, buffer.Length); // HIDIOCGFEATURE
int result = NativeMethods.ioctl(fd, request, bufPtr);
if (result < 0)
{
OnError?.Invoke(this, new HidErrorEventArgs(this, new IOException("ioctl HIDIOCGFEATURE failed")));
return Array.Empty<byte>();
}
Marshal.Copy(bufPtr, buffer, 0, buffer.Length);
Marshal.FreeHGlobal(bufPtr);
return await Task.FromResult(buffer);
}
finally
{
NativeMethods.close(fd);
}
}
public async Task StartListeningAsync(CancellationToken cancellationToken)
{
if (_stream == null)
{
OnError?.Invoke(this, new HidErrorEventArgs(this, new NotSupportedException("Device is not open.")));
return;
}
lock (_lock)
{
if (_cts != null)
{
OnError?.Invoke(this, new HidErrorEventArgs(this, new NotSupportedException("Already listening on this device.")));
return;
}
_cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
}
try
{
var token = _cts.Token;
while (!token.IsCancellationRequested)
{
byte[] buffer = new byte[InputReportByteLength];
int bytesRead;
try
{
bytesRead = await _stream.ReadAsync(buffer, 0, buffer.Length, token);
}
catch (OperationCanceledException)
{
break;
}
catch (NotSupportedException)
{
OnError?.Invoke(this, new HidErrorEventArgs(this, new NotSupportedException("Reading input reports is not supported on this device.")));
break; // Exit if reading is not supported
}
catch (Exception ex)
{
OnError?.Invoke(this, new HidErrorEventArgs(this, ex));
break;
}
if (bytesRead > 0)
{
var data = new byte[bytesRead];
Array.Copy(buffer, data, bytesRead);
OnDataReceived?.Invoke(this, new HidDataReceivedEventArgs(this, data));
}
else
{
try
{
await Task.Delay(5, token);
}
catch (OperationCanceledException)
{
break;
}
}
}
}
finally
{
lock (_lock)
{
_cts?.Dispose();
_cts = null;
}
}
}
private void StopListening()
{
lock (_lock)
{
if (_cts != null)
{
_cts.Cancel();
_cts.Dispose();
_cts = null;
}
}
}
}
}

412
EonaCat.HID/HidMac.cs Normal file
View File

@ -0,0 +1,412 @@
using EonaCat.HID.EventArguments;
using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
namespace EonaCat.HID
{
internal sealed class HidMac : IHid
{
private IntPtr _deviceHandle;
public HidMac(IntPtr deviceHandle)
{
_deviceHandle = deviceHandle;
CFRetain(_deviceHandle);
}
internal void Setup()
{
InitializeDeviceInfo();
}
public string DevicePath => null;
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; } = 64;
public int OutputReportByteLength { get; private set; } = 64;
public int FeatureReportByteLength { get; private set; } = 64;
public IDictionary<string, object> Capabilities { get; private set; } = new Dictionary<string, object>();
public event EventHandler<HidDataReceivedEventArgs> OnDataReceived;
public event EventHandler<HidErrorEventArgs> OnError;
public void Open()
{
IOHIDDeviceOpen(_deviceHandle, 0);
}
public void Close()
{
IOHIDDeviceClose(_deviceHandle, 0);
}
public void Dispose()
{
StopListening();
Close();
CFRelease(_deviceHandle);
_deviceHandle = IntPtr.Zero;
}
public void StopListening()
{
_listeningCts?.Cancel();
}
public async Task WriteOutputReportAsync(byte[] data)
{
if (data is null)
{
throw new ArgumentNullException(nameof(data));
}
await Task.Run(() =>
{
byte reportId = data.Length > 0 ? data[0] : (byte)0;
IntPtr buffer = Marshal.AllocHGlobal(data.Length);
try
{
Marshal.Copy(data, 0, buffer, data.Length);
int res = IOHIDDeviceSetReport(_deviceHandle, 1, reportId, buffer, data.Length);
if (res != 0)
{
OnError?.Invoke(this, new HidErrorEventArgs(this, new Exception($"IOHIDDeviceSetReport (Output) failed: {res}")));
return;
}
}
finally
{
Marshal.FreeHGlobal(buffer);
}
});
}
public async Task SendFeatureReportAsync(byte[] data)
{
if (data is null)
{
throw new ArgumentNullException(nameof(data));
}
await Task.Run(() =>
{
byte reportId = data.Length > 0 ? data[0] : (byte)0;
IntPtr buffer = Marshal.AllocHGlobal(data.Length);
try
{
Marshal.Copy(data, 0, buffer, data.Length);
int res = IOHIDDeviceSetReport(_deviceHandle, 2, reportId, buffer, data.Length);
if (res != 0)
{
OnError?.Invoke(this, new HidErrorEventArgs(this, new Exception($"IOHIDDeviceSetReport (Feature) failed: {res}")));
return;
}
}
finally
{
Marshal.FreeHGlobal(buffer);
}
});
}
public async Task<byte[]> GetFeatureReportAsync(byte reportId)
{
return await Task.Run(() =>
{
int length = FeatureReportByteLength;
IntPtr buffer = Marshal.AllocHGlobal(length);
try
{
int res = IOHIDDeviceGetReport(_deviceHandle, 2, reportId, buffer, ref length);
if (res != 0)
{
OnError?.Invoke(this, new HidErrorEventArgs(this, new Exception($"IOHIDDeviceGetReport (Feature) failed: {res}")));
return Array.Empty<byte>();
}
byte[] outBuf = new byte[length];
Marshal.Copy(buffer, outBuf, 0, length);
return outBuf;
}
finally
{
Marshal.FreeHGlobal(buffer);
}
});
}
public Task<byte[]> ReadInputReportAsync()
{
var tcs = new TaskCompletionSource<byte[]>();
byte[] buffer = new byte[InputReportByteLength];
InputReportCallback callback = null;
callback = (ctx, result, sender, report, reportLength) =>
{
byte[] output = new byte[reportLength.ToInt32()];
Array.Copy(report, output, output.Length);
tcs.TrySetResult(output);
};
GCHandle.Alloc(callback);
IOHIDDeviceRegisterInputReportCallback(_deviceHandle, buffer, (IntPtr)buffer.Length, callback, IntPtr.Zero);
return tcs.Task;
}
public Task StartListeningAsync(CancellationToken ct)
{
if (_listeningTask != null && !_listeningTask.IsCompleted)
{
OnError?.Invoke(this, new HidErrorEventArgs(this, new NotSupportedException("Already listening on this device.")));
return null;
}
_listeningCts = CancellationTokenSource.CreateLinkedTokenSource(ct);
_listeningTask = Task.Run(() =>
{
try
{
byte[] buffer = new byte[InputReportByteLength];
InputReportCallback callback = (ctx, result, sender, report, reportLength) =>
{
byte[] data = new byte[reportLength.ToInt32()];
Array.Copy(report, data, data.Length);
OnDataReceived?.Invoke(this, new HidDataReceivedEventArgs(this, data));
};
GCHandle.Alloc(callback);
IOHIDDeviceRegisterInputReportCallback(_deviceHandle, buffer, (IntPtr)buffer.Length, callback, IntPtr.Zero);
IOHIDDeviceScheduleWithRunLoop(_deviceHandle, CFRunLoopGetCurrent(), IntPtr.Zero);
while (!_listeningCts.Token.IsCancellationRequested)
{
CFRunLoopRun();
}
CFRunLoopStop(CFRunLoopGetCurrent());
}
catch (OperationCanceledException)
{
return; // Exit gracefully if cancellation was requested
}
catch (NotSupportedException)
{
OnError?.Invoke(this, new HidErrorEventArgs(this, new NotSupportedException("Reading input reports is not supported on this device.")));
_listeningCts.Cancel();
}
catch (Exception ex)
{
// Handle exceptions during reading
if (_listeningCts.IsCancellationRequested)
{
// Exit if cancellation was requested
}
OnError?.Invoke(this, new HidErrorEventArgs(this, ex));
}
}, _listeningCts.Token);
return _listeningTask;
}
private void InitializeDeviceInfo()
{
VendorId = GetIntProperty(kIOHIDVendorIDKey);
ProductId = GetIntProperty(kIOHIDProductIDKey);
Manufacturer = GetStringProperty(kIOHIDManufacturerKey);
ProductName = GetStringProperty(kIOHIDProductKey);
SerialNumber = GetStringProperty(kIOHIDSerialNumberKey);
}
private enum CFStringEncoding : uint
{
UTF8 = 0x08000100,
}
private ushort GetIntProperty(string key)
{
var cfKey = CFStringCreateWithCString(IntPtr.Zero, key, CFStringEncoding.UTF8);
IntPtr val = IOHIDDeviceGetProperty(_deviceHandle, cfKey);
CFRelease(cfKey);
if (val == IntPtr.Zero)
{
return 0;
}
if (CFGetTypeID(val) == CFNumberGetTypeID())
{
int i = 0;
bool success = CFNumberGetValue(val, CFNumberType.kCFNumberIntType, out i);
if (success)
{
return (ushort)i;
}
}
return 0;
}
private string GetStringProperty(string key)
{
var cfKey = CFStringCreateWithCString(IntPtr.Zero, key, CFStringEncoding.UTF8);
IntPtr val = IOHIDDeviceGetProperty(_deviceHandle, cfKey);
CFRelease(cfKey);
if (val == IntPtr.Zero)
{
return null;
}
string s = CFStringToString(val);
CFRelease(val);
return s;
}
private string CFStringToString(IntPtr cfStr)
{
if (cfStr == IntPtr.Zero)
{
return null;
}
IntPtr ptr = CFStringGetCStringPtr(cfStr, CFStringEncoding.UTF8);
if (ptr != IntPtr.Zero)
{
// Manually read null-terminated UTF8 string from pointer
return PtrToStringUTF8Manual(ptr);
}
int len = CFStringGetLength(cfStr);
int max = CFStringGetMaximumSizeForEncoding(len, CFStringEncoding.UTF8) + 1;
byte[] buf = new byte[max];
bool success = CFStringGetCString(cfStr, buf, max, CFStringEncoding.UTF8);
if (success)
{
return System.Text.Encoding.UTF8.GetString(buf).TrimEnd('\0');
}
return null;
}
private string PtrToStringUTF8Manual(IntPtr ptr)
{
if (ptr == IntPtr.Zero)
{
return null;
}
// Find length of null-terminated string
int len = 0;
while (Marshal.ReadByte(ptr, len) != 0)
{
len++;
}
if (len == 0)
{
return string.Empty;
}
byte[] buffer = new byte[len];
Marshal.Copy(ptr, buffer, 0, len);
return System.Text.Encoding.UTF8.GetString(buffer);
}
// Constants for IOHIDDevice properties
private const string kIOHIDVendorIDKey = "VendorID";
private const string kIOHIDProductIDKey = "ProductID";
private const string kIOHIDManufacturerKey = "Manufacturer";
private const string kIOHIDProductKey = "Product";
private const string kIOHIDSerialNumberKey = "SerialNumber";
[DllImport("/System/Library/Frameworks/IOKit.framework/IOKit")]
static extern IntPtr IOHIDDeviceGetProperty(IntPtr dev, IntPtr key);
[DllImport("/System/Library/Frameworks/IOKit.framework/IOKit")]
static extern uint IOHIDDeviceOpen(IntPtr dev, uint opt);
[DllImport("/System/Library/Frameworks/IOKit.framework/IOKit")]
static extern uint IOHIDDeviceClose(IntPtr dev, uint opt);
[DllImport("/System/Library/Frameworks/IOKit.framework/IOKit")]
static extern int IOHIDDeviceSetReport(IntPtr dev, int type, byte id, IntPtr buf, int len);
[DllImport("/System/Library/Frameworks/IOKit.framework/IOKit")]
static extern int IOHIDDeviceGetReport(IntPtr dev, int type, byte id, IntPtr buf, ref int len);
[DllImport("/System/Library/Frameworks/CoreFoundation.framework/CoreFoundation")]
static extern IntPtr CFStringCreateWithCString(IntPtr alloc, string s, CFStringEncoding enc);
[DllImport("/System/Library/Frameworks/CoreFoundation.framework/CoreFoundation")]
static extern void CFRelease(IntPtr cf);
[DllImport("/System/Library/Frameworks/CoreFoundation.framework/CoreFoundation")]
static extern IntPtr CFRetain(IntPtr cf);
[DllImport("/System/Library/Frameworks/CoreFoundation.framework/CoreFoundation")]
static extern IntPtr CFStringGetCStringPtr(IntPtr str, CFStringEncoding enc);
[DllImport("/System/Library/Frameworks/CoreFoundation.framework/CoreFoundation")]
static extern int CFStringGetLength(IntPtr str);
[DllImport("/System/Library/Frameworks/CoreFoundation.framework/CoreFoundation")]
static extern int CFStringGetMaximumSizeForEncoding(int len, CFStringEncoding enc);
[DllImport("/System/Library/Frameworks/CoreFoundation.framework/CoreFoundation")]
static extern bool CFStringGetCString(IntPtr str, byte[] buf, int bufSize, CFStringEncoding enc);
[DllImport("/System/Library/Frameworks/CoreFoundation.framework/CoreFoundation")]
static extern IntPtr CFGetTypeID(IntPtr cf);
[DllImport("/System/Library/Frameworks/CoreFoundation.framework/CoreFoundation")]
static extern IntPtr CFNumberGetTypeID();
[DllImport("/System/Library/Frameworks/CoreFoundation.framework/CoreFoundation")]
static extern bool CFNumberGetValue(IntPtr num, CFNumberType type, out int value);
[DllImport("/System/Library/Frameworks/IOKit.framework/IOKit")]
private static extern void IOHIDDeviceRegisterInputReportCallback(
IntPtr device,
byte[] report,
IntPtr reportLength,
InputReportCallback callback,
IntPtr context);
[DllImport("/System/Library/Frameworks/IOKit.framework/IOKit")]
private static extern void IOHIDDeviceScheduleWithRunLoop(
IntPtr device,
IntPtr runLoop,
IntPtr runLoopMode);
[DllImport("/System/Library/Frameworks/CoreFoundation.framework/CoreFoundation")]
private static extern IntPtr CFRunLoopGetCurrent();
[DllImport("/System/Library/Frameworks/CoreFoundation.framework/CoreFoundation")]
private static extern void CFRunLoopRun();
[DllImport("/System/Library/Frameworks/CoreFoundation.framework/CoreFoundation")]
private static extern void CFRunLoopStop(IntPtr runLoop);
// Input report callback delegate
private delegate void InputReportCallback(
IntPtr context,
IntPtr result,
IntPtr sender,
byte[] report,
IntPtr reportLength);
// For managing lifetime of listening loop
private CancellationTokenSource _listeningCts;
private Task _listeningTask;
private enum CFNumberType : int
{
kCFNumberIntType = 9
}
}
}

461
EonaCat.HID/HidWindows.cs Normal file
View File

@ -0,0 +1,461 @@
using EonaCat.HID.EventArguments;
using EonaCat.HID.Managers.Windows;
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
{
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 IDictionary<string, object> Capabilities { get; } = new Dictionary<string, object>();
public event EventHandler<HidDataReceivedEventArgs> OnDataReceived;
public event EventHandler<HidErrorEventArgs> 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;
}
/// <summary>
/// Open the device for I/O
/// </summary>
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, 64, true);
_isOpen = true;
// HID descriptor parsing
if (!HidD_GetPreparsedData(_deviceHandle.DangerousGetHandle(), out _preparsedData))
throw new Win32Exception(Marshal.GetLastWin32Error(), "Failed HidD_GetPreparsedData");
HIDP_CAPS caps;
int res = HidP_GetCaps(_preparsedData, out caps);
if (res == 0)
throw new Win32Exception(res, "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);
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;
}
private bool CanRead(SafeFileHandle handle)
{
try
{
using (var stream = new FileStream(handle, FileAccess.Read, 1, true))
{
byte[] test = new byte[1];
stream.Read(test, 0, 1);
return true;
}
}
catch { return false; }
}
private bool CanWrite(SafeFileHandle handle)
{
try
{
using (var stream = new FileStream(handle, FileAccess.Write, 1, true))
{
byte[] test = new byte[1];
stream.Write(test, 0, 1);
return true;
}
}
catch { return false; }
}
/// <summary>
/// Close the device
/// </summary>
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<HidDeviceAttributes>()
};
if (!HidD_GetAttributes(_deviceHandle.DangerousGetHandle(), ref attr))
{
throw new Win32Exception(Marshal.GetLastWin32Error(), "Failed HidD_GetAttributes");
}
return attr;
}
private string GetStringDescriptor(Func<IntPtr, IntPtr, int, bool> 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(byte[] data)
{
if (!_isOpen)
{
OnError?.Invoke(this, new HidErrorEventArgs(this, new InvalidOperationException("Device not open")));
return;
}
if (data == null || data.Length == 0)
{
OnError?.Invoke(this, new HidErrorEventArgs(this, new ArgumentNullException(nameof(data), "Data cannot be null or empty")));
return;
}
try
{
await _deviceStream.WriteAsync(data, 0, data.Length);
await _deviceStream.FlushAsync();
}
catch (Exception ex)
{
OnError?.Invoke(this, new HidErrorEventArgs(this, ex));
throw;
}
}
public async Task<byte[]> ReadInputReportAsync()
{
if (!_isOpen)
{
OnError?.Invoke(this, new HidErrorEventArgs(this, new InvalidOperationException("Device not open")));
return Array.Empty<byte>();
}
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 Array.Empty<byte>();
}
return buffer.Take(read).ToArray();
}
catch (Exception ex)
{
OnError?.Invoke(this, new HidErrorEventArgs(this, ex));
throw;
}
});
}
public async Task SendFeatureReportAsync(byte[] data)
{
if (!_isOpen)
{
OnError?.Invoke(this, new HidErrorEventArgs(this, new InvalidOperationException("Device not open")));
return;
}
if (data == null || data.Length == 0)
{
throw new ArgumentNullException(nameof(data));
}
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<byte[]> GetFeatureReportAsync(byte reportId)
{
if (!_isOpen)
{
OnError?.Invoke(this, new HidErrorEventArgs(this, new InvalidOperationException("Device not open")));
return Array.Empty<byte>();
}
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 Array.Empty<byte>();
}
return buffer;
});
}
/// <summary>
/// Begin async reading loop raising OnDataReceived events on data input
/// </summary>
/// <param name="cancellationToken"></param>
/// <returns></returns>
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
{
while (!_listeningCts.Token.IsCancellationRequested)
{
try
{
byte[] data = await ReadInputReportAsync(_listeningCts.Token);
if (data != null && data.Length > 0)
{
OnDataReceived?.Invoke(this, new HidDataReceivedEventArgs(this, data));
}
}
catch (OperationCanceledException)
{
break;
}
catch (NotSupportedException)
{
OnError?.Invoke(this, new HidErrorEventArgs(this, new NotSupportedException("Reading input reports is not supported on this device.")));
break; // Exit if reading is not supported
}
catch (Exception ex)
{
// Handle exceptions during reading
if (_listeningCts.IsCancellationRequested)
{
break; // Exit if cancellation was requested
}
OnError?.Invoke(this, new HidErrorEventArgs(this, ex));
}
}
}
finally
{
_listeningCts.Dispose();
_listeningCts = null;
}
}
private Task<byte[]> ReadInputReportAsync(CancellationToken cancellationToken)
{
var tcs = new TaskCompletionSource<byte[]>(TaskCreationOptions.RunContinuationsAsynchronously);
// Use overlapped IO pattern from FileStream
var buffer = new byte[InputReportByteLength];
_deviceStream.BeginRead(buffer, 0, buffer.Length, ar =>
{
try
{
int bytesRead = _deviceStream.EndRead(ar);
if (bytesRead == 0)
{
tcs.SetResult(Array.Empty<byte>());
}
else
{
tcs.SetResult(buffer.Take(bytesRead).ToArray());
}
}
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;
}
}
}
}

View File

@ -1,100 +0,0 @@
using System;
using System.Threading.Tasks;
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.
public delegate void InsertedEventHandler();
public delegate void RemovedEventHandler();
public enum DeviceMode
{
NonOverlapped = 0,
Overlapped = 1
}
[Flags]
public enum ShareMode
{
Exclusive = 0,
ShareRead = NativeMethods.FILE_SHARE_READ,
ShareWrite = NativeMethods.FILE_SHARE_WRITE
}
public delegate void ReadCallback(DeviceData data);
public delegate void ReadReportCallback(Report report);
public delegate void WriteCallback(bool success);
public interface IDevice : IDisposable
{
event InsertedEventHandler OnInserted;
event RemovedEventHandler OnRemoved;
bool IsOpen { get; }
bool IsConnected { get; }
string Description { get; }
Capabilities Capabilities { get; }
Attributes Info { get; }
string DevicePath { get; }
bool MonitorDeviceEvents { get; set; }
void OpenDevice();
void OpenDevice(DeviceMode readMode, DeviceMode writeMode, ShareMode shareMode);
void CloseDevice();
DeviceData Read();
void Read(ReadCallback callback, int timeout = 0);
Task<DeviceData> ReadAsync(int timeout = 0);
DeviceData Read(int timeout);
void ReadReport(ReadReportCallback callback, int timeout = 0);
Task<Report> ReadReportAsync(int timeout = 0);
Report ReadReport(int timeout = 0);
bool ReadFeatureData(out byte[] data, byte reportId = 0);
bool ReadProductName(out byte[] data);
bool ReadManufacturer(out byte[] data);
bool ReadSerialNumber(out byte[] data);
void Write(byte[] data, WriteCallback callback);
bool Write(byte[] data);
bool Write(byte[] data, int timeout);
void Write(byte[] data, WriteCallback callback, int timeout);
Task<bool> WriteAsync(byte[] data, int timeout = 0);
void WriteReport(Report report, WriteCallback callback);
bool WriteReport(Report report);
bool WriteReport(Report report, int timeout);
void WriteReport(Report report, WriteCallback callback, int timeout);
Task<bool> WriteReportAsync(Report report, int timeout = 0);
Report CreateReport();
bool WriteFeatureData(byte[] data);
}
}

View File

@ -1,21 +0,0 @@
using System.Collections.Generic;
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.
public interface IEnumerator
{
bool IsConnected(string devicePath);
IDevice GetDevice(string devicePath);
IEnumerable<IDevice> Enumerate();
IEnumerable<IDevice> Enumerate(string devicePath);
IEnumerable<IDevice> Enumerate(int vendorId, params int[] productIds);
IEnumerable<IDevice> Enumerate(int vendorId);
}
}

76
EonaCat.HID/IHid.cs Normal file
View File

@ -0,0 +1,76 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using System.Threading;
using EonaCat.HID.EventArguments;
namespace EonaCat.HID
{
/// <summary>
/// Interface abstraction for a HID device.
/// </summary>
public interface IHid : IDisposable
{
string DevicePath { get; }
ushort VendorId { get; }
ushort ProductId { get; }
string SerialNumber { get; }
string Manufacturer { get; }
string ProductName { get; }
int InputReportByteLength { get; }
int OutputReportByteLength { get; }
int FeatureReportByteLength { get; }
IDictionary<string, object> Capabilities { get; }
/// <summary>
/// Opens the device for communication
/// </summary>
void Open();
/// <summary>
/// Closes the device
/// </summary>
void Close();
/// <summary>
/// Writes an output report to the device
/// </summary>
/// <param name="data">Complete report data including ReportID</param>
Task WriteOutputReportAsync(byte[] data);
/// <summary>
/// Reads an input report
/// </summary>
/// <returns>Input report data</returns>
Task<byte[]> ReadInputReportAsync();
/// <summary>
/// Sends a feature report
/// </summary>
/// <param name="data">Complete feature report data including ReportID</param>
Task SendFeatureReportAsync(byte[] data);
/// <summary>
/// Gets a feature report
/// </summary>
/// <returns>Feature report data</returns>
Task<byte[]> GetFeatureReportAsync(byte reportId);
/// <summary>
/// Asynchronously read input reports and raise OnDataReceived event
/// </summary>
/// <param name="cancellationToken"></param>
Task StartListeningAsync(CancellationToken cancellationToken);
/// <summary>
/// Occurs when data is received from the device asynchronously
/// </summary>
event EventHandler<HidDataReceivedEventArgs> OnDataReceived;
/// <summary>
/// Errors occurring on device operations
/// </summary>
event EventHandler<HidErrorEventArgs> OnError;
}
}

View File

@ -0,0 +1,27 @@
using EonaCat.HID.EventArguments;
using System;
using System.Collections.Generic;
namespace EonaCat.HID
{
/// <summary>
/// Interface for manager to enumerate devices and monitor connect/disconnect
/// </summary>
public interface IHidManager
{
/// <summary>
/// Enumerate all connected HID devices matching optional VendorId/ProductId filters
/// </summary>
IEnumerable<IHid> Enumerate(ushort? vendorId = null, ushort? productId = null);
/// <summary>
/// Event is raised when a HID device is inserted
/// </summary>
event EventHandler<HidEventArgs> OnDeviceInserted;
/// <summary>
/// Event is raised when a HID device is removed
/// </summary>
event EventHandler<HidEventArgs> OnDeviceRemoved;
}
}

View File

@ -0,0 +1,176 @@
using EonaCat.HID.EventArguments;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
using static EonaCat.HID.Managers.Linux.NativeMethods;
namespace EonaCat.HID.Managers.Linux
{
internal sealed class HidManagerLinux : IHidManager
{
public event EventHandler<HidEventArgs> OnDeviceInserted;
public event EventHandler<HidEventArgs> OnDeviceRemoved;
public event EventHandler<string> OnDeviceError;
public IEnumerable<IHid> Enumerate(ushort? vendorId = null, ushort? productId = null)
{
var hidrawDir = "/sys/class/hidraw/";
if (!Directory.Exists(hidrawDir))
{
yield break;
}
var hidrawEntries = Directory.GetDirectories(hidrawDir).Where(d => d.Contains("hidraw"));
foreach (var hidrawEntry in hidrawEntries)
{
var devName = Path.GetFileName(hidrawEntry);
var devPath = "/dev/" + devName;
if (!File.Exists(devPath))
{
continue;
}
var dev = new HidLinux(devPath);
dev.Setup();
if (vendorId.HasValue && dev.VendorId != vendorId)
{
continue;
}
if (productId.HasValue && dev.ProductId != productId)
{
continue;
}
yield return dev;
}
}
private void DeviceInserted(IHid device)
{
OnDeviceInserted?.Invoke(this, new HidEventArgs(device));
}
private void DeviceRemoved(IHid device)
{
OnDeviceRemoved?.Invoke(this, new HidEventArgs(device));
}
public HidManagerLinux()
{
Task.Run(() => MonitorDevices());
}
private void MonitorDevices()
{
var previousDevices = new Dictionary<string, IHid>();
const int maxErrors = 10;
TimeSpan errorWindow = TimeSpan.FromMinutes(5);
Queue<DateTime> errorTimestamps = new Queue<DateTime>();
while (true)
{
try
{
var currentDevices = Enumerate().ToList();
var currentDeviceDict = currentDevices.ToDictionary(d => d.DevicePath);
// Detect new devices
foreach (var kvp in currentDeviceDict)
{
if (!previousDevices.ContainsKey(kvp.Key))
{
DeviceInserted(kvp.Value);
}
}
// Detect removed devices
foreach (var kvp in previousDevices)
{
if (!currentDeviceDict.ContainsKey(kvp.Key))
{
DeviceRemoved(kvp.Value);
}
}
previousDevices = currentDeviceDict;
// Clear error log on success
errorTimestamps.Clear();
}
catch (Exception ex)
{
OnDeviceError?.Invoke(this, $"[MonitorDevices] Error: {ex.Message}");
var now = DateTime.UtcNow;
errorTimestamps.Enqueue(now);
// Remove timestamps outside the 5-minute window
while (errorTimestamps.Count > 0 && now - errorTimestamps.Peek() > errorWindow)
{
errorTimestamps.Dequeue();
}
if (errorTimestamps.Count >= maxErrors)
{
Console.Error.WriteLine($"[MonitorDevices] Too many errors ({errorTimestamps.Count}) in the last 5 minutes. Monitoring stopped.");
break;
}
}
Thread.Sleep(1000); // Poll every second
}
}
}
internal static class NativeMethods
{
public const int O_RDWR = 0x0002;
public const int O_NONBLOCK = 0x800;
[DllImport("libc", SetLastError = true)]
public static extern int open(string pathname, int flags);
[DllImport("libc", SetLastError = true)]
public static extern int close(int fd);
[DllImport("libc", SetLastError = true)]
public static extern int ioctl(int fd, int request, IntPtr data);
// _IOC macro emulation
public const int _IOC_NRBITS = 8;
public const int _IOC_TYPEBITS = 8;
public const int _IOC_SIZEBITS = 14;
public const int _IOC_DIRBITS = 2;
public const int _IOC_NRMASK = (1 << _IOC_NRBITS) - 1;
public const int _IOC_TYPEMASK = (1 << _IOC_TYPEBITS) - 1;
public const int _IOC_SIZEMASK = (1 << _IOC_SIZEBITS) - 1;
public const int _IOC_DIRMASK = (1 << _IOC_DIRBITS) - 1;
public const int _IOC_NRSHIFT = 0;
public const int _IOC_TYPESHIFT = _IOC_NRSHIFT + _IOC_NRBITS;
public const int _IOC_SIZESHIFT = _IOC_TYPESHIFT + _IOC_TYPEBITS;
public const int _IOC_DIRSHIFT = _IOC_SIZESHIFT + _IOC_SIZEBITS;
public const int _IOC_NONE = 0;
public const int _IOC_WRITE = 1;
public const int _IOC_READ = 2;
public static int _IOC(int dir, int type, int nr, int size)
{
return ((dir & _IOC_DIRMASK) << _IOC_DIRSHIFT) |
((type & _IOC_TYPEMASK) << _IOC_TYPESHIFT) |
((nr & _IOC_NRMASK) << _IOC_NRSHIFT) |
((size & _IOC_SIZEMASK) << _IOC_SIZESHIFT);
}
}
}

View File

@ -0,0 +1,156 @@
using EonaCat.HID.EventArguments;
using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using static EonaCat.HID.Managers.Mac.NativeMethods;
namespace EonaCat.HID.Managers.Mac
{
internal sealed class HidManagerMac : IHidManager, IDisposable
{
private IntPtr _hidManager;
private readonly IOHIDDeviceCallback _deviceAddedCallback;
private readonly IOHIDDeviceCallback _deviceRemovedCallback;
public event EventHandler<HidEventArgs> OnDeviceInserted;
public event EventHandler<HidEventArgs> OnDeviceRemoved;
public HidManagerMac()
{
_deviceAddedCallback = DeviceAddedCallback;
_deviceRemovedCallback = DeviceRemovedCallback;
_hidManager = IOHIDManagerCreate(IntPtr.Zero, 0);
if (_hidManager == IntPtr.Zero)
{
throw new InvalidOperationException("Failed to create IOHIDManager");
}
CFRetain(_hidManager);
IOHIDManagerScheduleWithRunLoop(
_hidManager,
CFRunLoopGetCurrent(),
CFRunLoopModeDefault
);
if (IOHIDManagerOpen(_hidManager, 0) != 0)
{
throw new InvalidOperationException("Failed to open IOHIDManager");
}
IOHIDManagerRegisterDeviceMatchingCallback(_hidManager, _deviceAddedCallback, IntPtr.Zero);
IOHIDManagerRegisterDeviceRemovalCallback(_hidManager, _deviceRemovedCallback, IntPtr.Zero);
}
public IEnumerable<IHid> Enumerate(ushort? vendorId = null, ushort? productId = null)
{
var devices = new List<IHid>();
IntPtr cfSet = IOHIDManagerCopyDevices(_hidManager);
if (cfSet == IntPtr.Zero)
{
return devices;
}
int count = CFSetGetCount(cfSet);
IntPtr[] values = new IntPtr[count];
CFSetGetValues(cfSet, values);
foreach (var ptr in values)
{
var dev = new HidMac(ptr);
dev.Setup();
if (vendorId.HasValue && dev.VendorId != vendorId)
{
continue;
}
if (productId.HasValue && dev.ProductId != productId)
{
continue;
}
devices.Add(dev);
}
CFRelease(cfSet);
return devices;
}
private void DeviceAddedCallback(IntPtr context, IntPtr result, IntPtr sender, IntPtr devicePtr)
{
if (devicePtr == IntPtr.Zero)
{
return; // Ignore null devices
}
// Create the device and invoke the event
var device = new HidMac(devicePtr);
device.Setup();
OnDeviceInserted?.Invoke(this, new HidEventArgs(device));
}
private void DeviceRemovedCallback(IntPtr context, IntPtr result, IntPtr sender, IntPtr devicePtr)
{
if (devicePtr == IntPtr.Zero)
{
return; // Ignore null devices
}
OnDeviceRemoved?.Invoke(this, new HidEventArgs(null));
}
public void Dispose()
{
if (_hidManager != IntPtr.Zero)
{
IOHIDManagerUnscheduleFromRunLoop(_hidManager, CFRunLoopGetCurrent(), CFRunLoopModeDefault);
IOHIDManagerClose(_hidManager, 0);
CFRelease(_hidManager);
_hidManager = IntPtr.Zero;
}
}
}
internal static class NativeMethods
{
public delegate void IOHIDDeviceCallback(IntPtr context, IntPtr result, IntPtr sender, IntPtr device);
[DllImport("/System/Library/Frameworks/IOKit.framework/IOKit")]
public static extern IntPtr IOHIDManagerCreate(IntPtr allocator, uint options);
[DllImport("/System/Library/Frameworks/IOKit.framework/IOKit")]
public static extern void IOHIDManagerScheduleWithRunLoop(IntPtr manager, IntPtr runLoop, IntPtr runLoopMode);
[DllImport("/System/Library/Frameworks/IOKit.framework/IOKit")]
public static extern void IOHIDManagerUnscheduleFromRunLoop(IntPtr manager, IntPtr runLoop, IntPtr runLoopMode);
[DllImport("/System/Library/Frameworks/IOKit.framework/IOKit")]
public static extern uint IOHIDManagerOpen(IntPtr manager, uint options);
[DllImport("/System/Library/Frameworks/IOKit.framework/IOKit")]
public static extern uint IOHIDManagerClose(IntPtr manager, uint options);
[DllImport("/System/Library/Frameworks/IOKit.framework/IOKit")]
public static extern IntPtr IOHIDManagerCopyDevices(IntPtr manager);
[DllImport("/System/Library/Frameworks/IOKit.framework/IOKit")]
public static extern void IOHIDManagerRegisterDeviceMatchingCallback(IntPtr manager, IOHIDDeviceCallback callback, IntPtr context);
[DllImport("/System/Library/Frameworks/IOKit.framework/IOKit")]
public static extern void IOHIDManagerRegisterDeviceRemovalCallback(IntPtr manager, IOHIDDeviceCallback callback, IntPtr context);
[DllImport("/System/Library/Frameworks/CoreFoundation.framework/CoreFoundation")]
public static extern IntPtr CFRunLoopGetCurrent();
[DllImport("/System/Library/Frameworks/CoreFoundation.framework/CoreFoundation")]
public static extern IntPtr CFRetain(IntPtr cf);
[DllImport("/System/Library/Frameworks/CoreFoundation.framework/CoreFoundation")]
public static extern void CFRelease(IntPtr cf);
[DllImport("/System/Library/Frameworks/CoreFoundation.framework/CoreFoundation")]
public static extern int CFSetGetCount(IntPtr cfset);
[DllImport("/System/Library/Frameworks/CoreFoundation.framework/CoreFoundation")]
public static extern void CFSetGetValues(IntPtr cfset, [Out] IntPtr[] values);
[DllImport("/System/Library/Frameworks/CoreFoundation.framework/CoreFoundation")]
public static extern IntPtr CFStringCreateWithCString(IntPtr alloc, string str, CFStringEncoding encoding);
public static readonly IntPtr CFRunLoopModeDefault = CFStringCreateWithCString(
IntPtr.Zero, "kCFRunLoopDefaultMode", CFStringEncoding.UTF8
);
public enum CFStringEncoding : uint
{
UTF8 = 0x08000100
}
}
}

View File

@ -0,0 +1,517 @@
using EonaCat.HID.EventArguments;
using Microsoft.Win32.SafeHandles;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.IO;
using System.Runtime.InteropServices;
using static EonaCat.HID.Managers.Windows.NativeMethods;
namespace EonaCat.HID.Managers.Windows
{
internal sealed class HidManagerWindows : IHidManager, IDisposable
{
private const int DIGCF_PRESENT = 0x00000002;
private const int DIGCF_DEVICEINTERFACE = 0x00000010;
private const ushort HID_USAGE_PAGE_GENERIC = 0x01;
private const ushort HID_USAGE_GENERIC_MOUSE = 0x02;
private static Guid GUID_DEVINTERFACE_HID = new Guid("4d1e55b2-f16f-11cf-88cb-001111000030");
private readonly object _lock = new object();
// Monitor devices for connect/disconnect
private IntPtr _deviceNotificationHandle;
private WndProc _windowProcDelegate;
private IntPtr _messageWindowHandle;
public event EventHandler<HidEventArgs> OnDeviceInserted;
public event EventHandler<HidEventArgs> OnDeviceRemoved;
public HidManagerWindows()
{
InitializeMessageWindow();
RegisterForDeviceNotifications();
}
private void InitializeMessageWindow()
{
// Create hidden window to receive device change messages for insert/remove events
_windowProcDelegate = new WndProc(WindowProc);
WNDCLASS wc = new WNDCLASS()
{
lpfnWndProc = _windowProcDelegate,
lpszClassName = "HidDeviceNotificationWindow_" + Guid.NewGuid(),
style = 0,
cbClsExtra = 0,
cbWndExtra = 0,
hInstance = GetModuleHandle(null),
hbrBackground = IntPtr.Zero,
hCursor = IntPtr.Zero,
hIcon = IntPtr.Zero,
lpszMenuName = null
};
ushort classAtom = RegisterClass(ref wc);
if (classAtom == 0)
{
throw new Win32Exception(Marshal.GetLastWin32Error(), "Failed to register window class");
}
_messageWindowHandle = CreateWindowEx(
0,
wc.lpszClassName,
"",
0,
0, 0, 0, 0,
IntPtr.Zero,
IntPtr.Zero,
wc.hInstance,
IntPtr.Zero);
if (_messageWindowHandle == IntPtr.Zero)
{
throw new Win32Exception(Marshal.GetLastWin32Error(), "Failed to create message-only window");
}
}
private void RegisterForDeviceNotifications()
{
DEV_BROADCAST_DEVICEINTERFACE devInterface = new DEV_BROADCAST_DEVICEINTERFACE
{
dbcc_size = Marshal.SizeOf<DEV_BROADCAST_DEVICEINTERFACE>(),
dbcc_devicetype = DBT_DEVTYP_DEVICEINTERFACE,
dbcc_classguid = GUID_DEVINTERFACE_HID
};
IntPtr buffer = Marshal.AllocHGlobal(Marshal.SizeOf(devInterface));
Marshal.StructureToPtr(devInterface, buffer, false);
_deviceNotificationHandle = RegisterDeviceNotification(_messageWindowHandle, buffer, DEVICE_NOTIFY_WINDOW_HANDLE);
Marshal.FreeHGlobal(buffer);
if (_deviceNotificationHandle == IntPtr.Zero)
{
throw new Win32Exception(Marshal.GetLastWin32Error(), "Failed to register for device notifications");
}
}
private IntPtr WindowProc(IntPtr hwnd, uint msg, IntPtr wParam, IntPtr lParam)
{
if (msg == WM_DEVICECHANGE)
{
var eventType = wParam.ToInt32();
if (eventType == DBT_DEVICEARRIVAL)
{
// Device inserted
var devBroadcast = Marshal.PtrToStructure<DEV_BROADCAST_HDR>(lParam);
if (devBroadcast.dbch_devicetype == DBT_DEVTYP_DEVICEINTERFACE)
{
// Treat all HID devices or filter if needed
OnDeviceInserted?.Invoke(this, new HidEventArgs(null));
}
}
else if (eventType == DBT_DEVICEREMOVECOMPLETE)
{
var devBroadcast = Marshal.PtrToStructure<DEV_BROADCAST_HDR>(lParam);
if (devBroadcast.dbch_devicetype == DBT_DEVTYP_DEVICEINTERFACE)
{
OnDeviceRemoved?.Invoke(this, new HidEventArgs(null));
}
}
}
return DefWindowProc(hwnd, msg, wParam, lParam);
}
public IEnumerable<IHid> Enumerate(ushort? vendorId = null, ushort? productId = null)
{
var list = new List<IHid>();
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()
{
if (_deviceNotificationHandle != IntPtr.Zero)
{
UnregisterDeviceNotification(_deviceNotificationHandle);
_deviceNotificationHandle = IntPtr.Zero;
}
if (_messageWindowHandle != IntPtr.Zero)
{
DestroyWindow(_messageWindowHandle);
_messageWindowHandle = IntPtr.Zero;
}
}
}
#region Native Methods and structs
internal static class NativeMethods
{
public const int ERROR_INSUFFICIENT_BUFFER = 122;
public const int ERROR_NO_MORE_ITEMS = 259;
public const int FILE_FLAG_OVERLAPPED = 0x40000000;
public const int FILE_SHARE_READ = 0x00000001;
public const int FILE_SHARE_WRITE = 0x00000002;
public const int OPEN_EXISTING = 3;
public const int GENERIC_READ = unchecked((int)0x80000000);
public const int GENERIC_WRITE = 0x40000000;
public const int DEVICE_NOTIFY_WINDOW_HANDLE = 0x00000000;
public const int WM_DEVICECHANGE = 0x0219;
public const int DBT_DEVICEARRIVAL = 0x8000;
public const int DBT_DEVICEREMOVECOMPLETE = 0x8004;
public const int DBT_DEVTYP_DEVICEINTERFACE = 5;
[StructLayout(LayoutKind.Sequential)]
public struct WNDCLASS
{
public uint style;
[MarshalAs(UnmanagedType.FunctionPtr)]
public WndProc lpfnWndProc;
public int cbClsExtra;
public int cbWndExtra;
public IntPtr hInstance;
public IntPtr hIcon;
public IntPtr hCursor;
public IntPtr hbrBackground;
[MarshalAs(UnmanagedType.LPTStr)]
public string lpszMenuName;
[MarshalAs(UnmanagedType.LPTStr)]
public string lpszClassName;
}
[StructLayout(LayoutKind.Sequential)]
public struct DEV_BROADCAST_HDR
{
public int dbch_size;
public int dbch_devicetype;
public int dbch_reserved;
}
[StructLayout(LayoutKind.Sequential)]
public struct DEV_BROADCAST_DEVICEINTERFACE
{
public int dbcc_size;
public int dbcc_devicetype;
public int dbcc_reserved;
public Guid dbcc_classguid;
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 255)]
public string dbcc_name;
}
[StructLayout(LayoutKind.Sequential)]
public struct SP_DEVICE_INTERFACE_DATA
{
public int cbSize;
public Guid InterfaceClassGuid;
public int Flags;
public IntPtr Reserved;
}
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)]
public struct SP_DEVICE_INTERFACE_DETAIL_DATA
{
public int cbSize;
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 256)]
public string DevicePath;
}
// Declare delegate for WndProc
public delegate IntPtr WndProc(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam);
[DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
public static extern ushort RegisterClass(ref WNDCLASS lpWndClass);
[DllImport("user32.dll", SetLastError = true)]
public static extern IntPtr CreateWindowEx(
int dwExStyle,
string lpClassName,
string lpWindowName,
int dwStyle,
int x, int y, int nWidth, int nHeight,
IntPtr hWndParent,
IntPtr hMenu,
IntPtr hInstance,
IntPtr lpParam);
[DllImport("user32.dll")]
public static extern bool DestroyWindow(IntPtr hWnd);
[DllImport("kernel32.dll", CharSet = CharSet.Auto)]
public static extern IntPtr GetModuleHandle(string lpModuleName);
[DllImport("user32.dll")]
public static extern IntPtr DefWindowProc(IntPtr hwnd, uint uMsg, IntPtr wParam, IntPtr lParam);
[DllImport("setupapi.dll", SetLastError = true)]
public static extern IntPtr SetupDiGetClassDevs(
ref Guid ClassGuid,
[MarshalAs(UnmanagedType.LPTStr)] string Enumerator,
IntPtr hwndParent,
uint Flags);
[DllImport("setupapi.dll", SetLastError = true, CharSet = CharSet.Auto)]
public static extern bool SetupDiEnumDeviceInterfaces(
IntPtr DeviceInfoSet,
IntPtr DeviceInfoData,
ref Guid InterfaceClassGuid,
uint MemberIndex,
ref SP_DEVICE_INTERFACE_DATA DeviceInterfaceData);
[DllImport("setupapi.dll", SetLastError = true, CharSet = CharSet.Auto)]
public static extern bool SetupDiGetDeviceInterfaceDetail(
IntPtr DeviceInfoSet,
ref SP_DEVICE_INTERFACE_DATA DeviceInterfaceData,
IntPtr DeviceInterfaceDetailData,
uint DeviceInterfaceDetailDataSize,
ref uint RequiredSize,
IntPtr DeviceInfoData);
[DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Auto)]
public static extern SafeFileHandle CreateFile(
string lpFileName,
uint dwDesiredAccess,
FileShare dwShareMode,
IntPtr lpSecurityAttributes,
FileMode dwCreationDisposition,
uint dwFlagsAndAttributes,
IntPtr hTemplateFile);
[DllImport("setupapi.dll", SetLastError = true)]
public static extern bool SetupDiDestroyDeviceInfoList(IntPtr DeviceInfoSet);
[DllImport("user32.dll", SetLastError = true)]
public static extern IntPtr RegisterDeviceNotification(IntPtr hRecipient, IntPtr NotificationFilter, uint Flags);
[DllImport("user32.dll", SetLastError = true)]
public static extern bool UnregisterDeviceNotification(IntPtr Handle);
[DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Auto)]
public static extern SafeFileHandle CreateFile(
string lpFileName,
int dwDesiredAccess,
int dwShareMode,
IntPtr lpSecurityAttributes,
int dwCreationDisposition,
int dwFlagsAndAttributes,
IntPtr hTemplateFile);
// HID APIs
[DllImport("hid.dll", SetLastError = true)]
public static extern bool HidD_GetAttributes(IntPtr hidDeviceObject, ref HidDeviceAttributes attributes);
[DllImport("hid.dll", SetLastError = true)]
public static extern bool HidD_GetManufacturerString(IntPtr hidDeviceObject, IntPtr buffer, int bufferLength);
[DllImport("hid.dll", SetLastError = true)]
public static extern bool HidD_GetProductString(IntPtr hidDeviceObject, IntPtr buffer, int bufferLength);
[DllImport("hid.dll", SetLastError = true)]
public static extern bool HidD_GetSerialNumberString(IntPtr hidDeviceObject, IntPtr buffer, int bufferLength);
[DllImport("hid.dll", SetLastError = true)]
public static extern bool HidD_GetPreparsedData(IntPtr hidDeviceObject, out IntPtr preparsedData);
[DllImport("hid.dll", SetLastError = true)]
public static extern bool HidD_FreePreparsedData(IntPtr preparsedData);
[DllImport("hid.dll", SetLastError = true)]
public static extern bool HidD_SetFeature(IntPtr hidDeviceObject, byte[] reportBuffer, int reportBufferLength);
[DllImport("hid.dll", SetLastError = true)]
public static extern bool HidD_GetFeature(IntPtr hidDeviceObject, byte[] reportBuffer, int reportBufferLength);
[DllImport("hid.dll", SetLastError = true)]
public static extern bool HidD_GetInputReport(IntPtr hidDeviceObject, byte[] buffer, int bufferLength);
// HIDP status
[DllImport("hid.dll")]
public static extern int HidP_GetCaps(IntPtr preparsedData, out HIDP_CAPS capabilities);
[StructLayout(LayoutKind.Sequential)]
public struct HIDP_CAPS
{
public ushort Usage;
public ushort UsagePage;
public ushort InputReportByteLength;
public ushort OutputReportByteLength;
public ushort FeatureReportByteLength;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 17)]
public ushort[] Reserved;
public ushort NumberLinkCollectionNodes;
public ushort NumberInputButtonCaps;
public ushort NumberInputValueCaps;
public ushort NumberInputDataIndices;
public ushort NumberOutputButtonCaps;
public ushort NumberOutputValueCaps;
public ushort NumberOutputDataIndices;
public ushort NumberFeatureButtonCaps;
public ushort NumberFeatureValueCaps;
public ushort NumberFeatureDataIndices;
}
}
[StructLayout(LayoutKind.Sequential)]
internal struct HidDeviceAttributes
{
public int Size;
public ushort VendorID;
public ushort ProductID;
public ushort VersionNumber;
}
#endregion
}

View File

@ -1,225 +0,0 @@
using System;
using System.Runtime.InteropServices;
[module: DefaultCharSet(CharSet.Unicode)]
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 static class NativeMethods
{
internal const int FILE_FLAG_OVERLAPPED = 0x40000000;
internal const short FILE_SHARE_READ = 0x1;
internal const short FILE_SHARE_WRITE = 0x2;
internal const uint GENERIC_READ = 0x80000000;
internal const uint GENERIC_WRITE = 0x40000000;
internal const int ACCESS_NONE = 0;
internal const int INVALID_HANDLE_VALUE = -1;
internal const short OPEN_EXISTING = 3;
internal const int WAIT_TIMEOUT = 0x102;
internal const uint WAIT_OBJECT_0 = 0;
internal const uint WAIT_FAILED = 0xffffffff;
internal const int WAIT_INFINITE = -1;
[StructLayout(LayoutKind.Sequential)]
internal struct SECURITY_ATTRIBUTES
{
public int nLength;
public IntPtr lpSecurityDescriptor;
public bool bInheritHandle;
}
[DllImport("kernel32.dll", SetLastError = true, ExactSpelling = true)]
internal static extern bool CancelIoEx(IntPtr hFile, IntPtr lpOverlapped);
[DllImport("kernel32.dll", SetLastError = true, ExactSpelling = true)]
internal static extern bool CloseHandle(IntPtr hObject);
[DllImport("kernel32.dll")]
internal static extern IntPtr CreateEvent(ref SECURITY_ATTRIBUTES securityAttributes, int bManualReset, int bInitialState, string lpName);
[DllImport("kernel32.dll", SetLastError = true)]
internal static extern IntPtr CreateFile(string lpFileName, uint dwDesiredAccess, int dwShareMode, ref SECURITY_ATTRIBUTES lpSecurityAttributes, int dwCreationDisposition, int dwFlagsAndAttributes, IntPtr hTemplateFile);
[DllImport("kernel32.dll", SetLastError = true)]
internal static extern bool ReadFile(IntPtr hFile, IntPtr lpBuffer, uint nNumberOfBytesToRead, out uint lpNumberOfBytesRead, [In] ref System.Threading.NativeOverlapped lpOverlapped);
[DllImport("kernel32.dll")]
internal static extern uint WaitForSingleObject(IntPtr hHandle, int dwMilliseconds);
[DllImport("kernel32.dll", SetLastError = true)]
internal static extern bool GetOverlappedResult(IntPtr hFile, [In] ref System.Threading.NativeOverlapped lpOverlapped, out uint lpNumberOfBytesTransferred, bool bWait);
[DllImport("kernel32.dll")]
internal static extern bool WriteFile(IntPtr hFile, byte[] lpBuffer, uint nNumberOfBytesToWrite, out uint lpNumberOfBytesWritten, [In] ref System.Threading.NativeOverlapped lpOverlapped);
internal const short DIGCF_PRESENT = 0x2;
internal const short DIGCF_DEVICEINTERFACE = 0x10;
internal const int DIGCF_ALLCLASSES = 0x4;
internal const int SPDRP_ADDRESS = 0x1c;
internal const int SPDRP_BUSNUMBER = 0x15;
internal const int SPDRP_BUSTYPEGUID = 0x13;
internal const int SPDRP_CAPABILITIES = 0xf;
internal const int SPDRP_CHARACTERISTICS = 0x1b;
internal const int SPDRP_CLASS = 7;
internal const int SPDRP_CLASSGUID = 8;
internal const int SPDRP_COMPATIBLEIDS = 2;
internal const int SPDRP_CONFIGFLAGS = 0xa;
internal const int SPDRP_DEVICE_POWER_DATA = 0x1e;
internal const int SPDRP_DEVICEDESC = 0;
internal const int SPDRP_DEVTYPE = 0x19;
internal const int SPDRP_DRIVER = 9;
internal const int SPDRP_ENUMERATOR_NAME = 0x16;
internal const int SPDRP_EXCLUSIVE = 0x1a;
internal const int SPDRP_FRIENDLYNAME = 0xc;
internal const int SPDRP_HARDWAREID = 1;
internal const int SPDRP_LEGACYBUSTYPE = 0x14;
internal const int SPDRP_LOCATION_INFORMATION = 0xd;
internal const int SPDRP_LOWERFILTERS = 0x12;
internal const int SPDRP_MFG = 0xb;
internal const int SPDRP_PHYSICAL_DEVICE_OBJECT_NAME = 0xe;
internal const int SPDRP_REMOVAL_POLICY = 0x1f;
internal const int SPDRP_REMOVAL_POLICY_HW_DEFAULT = 0x20;
internal const int SPDRP_REMOVAL_POLICY_OVERRIDE = 0x21;
internal const int SPDRP_SECURITY = 0x17;
internal const int SPDRP_SECURITY_SDS = 0x18;
internal const int SPDRP_SERVICE = 4;
internal const int SPDRP_UI_NUMBER = 0x10;
internal const int SPDRP_UI_NUMBER_DESC_FORMAT = 0x1d;
internal const int SPDRP_UPPERFILTERS = 0x11;
[StructLayout(LayoutKind.Sequential)]
internal struct SP_DEVICE_INTERFACE_DATA
{
internal int cbSize;
internal System.Guid InterfaceClassGuid;
internal int Flags;
internal IntPtr Reserved;
}
[StructLayout(LayoutKind.Sequential)]
internal struct SP_DEVINFO_DATA
{
internal int cbSize;
internal Guid ClassGuid;
internal int DevInst;
internal IntPtr Reserved;
}
[StructLayout(LayoutKind.Sequential)]
internal struct SP_DEVICE_INTERFACE_DETAIL_DATA
{
internal int Size;
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 256)]
internal string DevicePath;
}
[StructLayout(LayoutKind.Sequential)]
internal struct DEVPROPKEY
{
public Guid fmtid;
public uint pid;
}
internal static DEVPROPKEY DEVPKEY_Device_BusReportedDeviceDesc = new DEVPROPKEY { fmtid = new Guid(0x540b947e, 0x8b40, 0x45bc, 0xa8, 0xa2, 0x6a, 0x0b, 0x89, 0x4c, 0xbd, 0xa2), pid = 4 };
[DllImport("setupapi.dll")]
public static extern unsafe bool SetupDiGetDeviceRegistryProperty(IntPtr deviceInfoSet, ref SP_DEVINFO_DATA deviceInfoData, int propertyVal, ref int propertyRegDataType, void* propertyBuffer, int propertyBufferSize, ref int requiredSize);
[DllImport("setupapi.dll", SetLastError = true)]
public static extern unsafe bool SetupDiGetDeviceProperty(IntPtr deviceInfo, ref SP_DEVINFO_DATA deviceInfoData, ref DEVPROPKEY propkey, ref uint propertyDataType, void* propertyBuffer, int propertyBufferSize, ref int requiredSize, uint flags);
[DllImport("setupapi.dll")]
internal static extern bool SetupDiEnumDeviceInfo(IntPtr deviceInfoSet, int memberIndex, ref SP_DEVINFO_DATA deviceInfoData);
[DllImport("setupapi.dll")]
internal static extern int SetupDiDestroyDeviceInfoList(IntPtr deviceInfoSet);
[DllImport("setupapi.dll")]
internal static extern bool SetupDiEnumDeviceInterfaces(IntPtr deviceInfoSet, ref SP_DEVINFO_DATA deviceInfoData, ref Guid interfaceClassGuid, int memberIndex, ref SP_DEVICE_INTERFACE_DATA deviceInterfaceData);
[DllImport("setupapi.dll")]
internal static extern IntPtr SetupDiGetClassDevs(ref System.Guid classGuid, string enumerator, IntPtr hwndParent, int flags);
[DllImport("setupapi.dll")]
internal static extern bool SetupDiGetDeviceInterfaceDetail(IntPtr deviceInfoSet, ref SP_DEVICE_INTERFACE_DATA deviceInterfaceData, IntPtr deviceInterfaceDetailData, int deviceInterfaceDetailDataSize, ref int requiredSize, IntPtr deviceInfoData);
[DllImport("setupapi.dll")]
internal static extern bool SetupDiGetDeviceInterfaceDetail(IntPtr deviceInfoSet, ref SP_DEVICE_INTERFACE_DATA deviceInterfaceData, ref SP_DEVICE_INTERFACE_DETAIL_DATA deviceInterfaceDetailData, int deviceInterfaceDetailDataSize, ref int requiredSize, IntPtr deviceInfoData);
[StructLayout(LayoutKind.Sequential)]
internal struct HIDD_ATTRIBUTES
{
internal int Size;
internal ushort VendorID;
internal ushort ProductID;
internal short VersionNumber;
}
[StructLayout(LayoutKind.Sequential)]
internal struct Capabilities
{
internal short Usage;
internal short UsagePage;
internal short InputReportByteLength;
internal short OutputReportByteLength;
internal short FeatureReportByteLength;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 17)]
internal short[] Reserved;
internal short NumberLinkCollectionNodes;
internal short NumberInputButtonCaps;
internal short NumberInputValueCaps;
internal short NumberInputDataIndices;
internal short NumberOutputButtonCaps;
internal short NumberOutputValueCaps;
internal short NumberOutputDataIndices;
internal short NumberFeatureButtonCaps;
internal short NumberFeatureValueCaps;
internal short NumberFeatureDataIndices;
}
[DllImport("hid.dll")]
internal static extern bool HidD_GetAttributes(IntPtr hidDeviceObject, ref HIDD_ATTRIBUTES attributes);
[DllImport("hid.dll")]
internal static extern bool HidD_GetFeature(IntPtr hidDeviceObject, byte[] lpReportBuffer, int reportBufferLength);
[DllImport("hid.dll")]
internal static extern bool HidD_GetInputReport(IntPtr hidDeviceObject, byte[] lpReportBuffer, int reportBufferLength);
[DllImport("hid.dll")]
internal static extern void HidD_GetHidGuid(ref Guid hidGuid);
[DllImport("hid.dll")]
internal static extern bool HidD_GetPreparsedData(IntPtr hidDeviceObject, ref IntPtr preparsedData);
[DllImport("hid.dll")]
internal static extern bool HidD_FreePreparsedData(IntPtr preparsedData);
[DllImport("hid.dll")]
internal static extern bool HidD_SetFeature(IntPtr hidDeviceObject, byte[] lpReportBuffer, int reportBufferLength);
[DllImport("hid.dll")]
internal static extern bool HidD_SetOutputReport(IntPtr hidDeviceObject, byte[] lpReportBuffer, int reportBufferLength);
[DllImport("hid.dll")]
internal static extern int HidP_GetCaps(IntPtr preparsedData, ref Capabilities capabilities);
[DllImport("hid.dll")]
internal static extern bool HidD_GetProductString(IntPtr hidDeviceObject, byte[] lpReportBuffer, int ReportBufferLength);
[DllImport("hid.dll")]
internal static extern bool HidD_GetManufacturerString(IntPtr hidDeviceObject, byte[] lpReportBuffer, int ReportBufferLength);
[DllImport("hid.dll")]
internal static extern bool HidD_GetSerialNumberString(IntPtr hidDeviceObject, byte[] lpReportBuffer, int reportBufferLength);
}
}

View File

@ -1,39 +0,0 @@
using System.Threading.Tasks;
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.
public class ReadDevice : Device
{
internal ReadDevice(string devicePath, string description = null)
: base(devicePath, description) { }
public DeviceData FastRead(int timeout = 0)
{
try
{
return ReadData(timeout);
}
catch
{
return new DeviceData(DeviceData.ReadStatus.ReadError);
}
}
public async Task<DeviceData> FastReadAsync(int timeout = 0)
{
return await Task.Run(() => FastRead(timeout));
}
public Report FastReadReport(int timeout = 0)
{
return new Report(Capabilities.InputReportByteLength, FastRead(timeout));
}
public async Task<Report> FastReadReportAsync(int timeout = 0)
{
return await Task.Run(() => FastReadReport(timeout));
}
}
}

View File

@ -1,46 +0,0 @@
using System.Collections.Generic;
using System.Linq;
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.
public class ReadEnumerator : IEnumerator
{
public bool IsConnected(string devicePath)
{
return Devices.IsConnected(devicePath);
}
public IDevice GetDevice(string devicePath)
{
return Enumerate(devicePath).FirstOrDefault();
}
public IEnumerable<IDevice> Enumerate()
{
return Devices.EnumerateDevices().
Select(d => new ReadDevice(d.Path, d.Description) as IDevice);
}
public IEnumerable<IDevice> Enumerate(string devicePath)
{
return Devices.EnumerateDevices().Where(x => x.Path == devicePath).
Select(d => new ReadDevice(d.Path, d.Description) as IDevice);
}
public IEnumerable<IDevice> Enumerate(int vendorId, params int[] productIds)
{
return Devices.EnumerateDevices().Select(d => new ReadDevice(d.Path, d.Description)).
Where(f => f.Info.VendorId == vendorId && productIds.Contains(f.Info.ProductId)).
Select(d => d as IDevice);
}
public IEnumerable<IDevice> Enumerate(int vendorId)
{
return Devices.EnumerateDevices().Select(d => new ReadDevice(d.Path, d.Description)).
Where(f => f.Info.VendorId == vendorId).
Select(d => d as IDevice);
}
}
}

View File

@ -1,86 +0,0 @@
using System;
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.
public class Report
{
private byte _reportId;
private byte[] _data;
private readonly DeviceData.ReadStatus _status;
public Report(int reportSize)
{
if (reportSize > 0)
{
_data = new byte[reportSize - 1];
}
else
{
Exists = false;
return;
}
}
public Report(int reportSize, DeviceData deviceData)
{
_status = deviceData.Status;
if (reportSize > 0)
{
_data = new byte[reportSize - 1];
}
else
{
Exists = false;
return;
}
if (deviceData?.Data == null || deviceData.Data.Length == 0)
{
Exists = false;
return;
}
_reportId = deviceData.Data[0];
Exists = true;
var dataLength = Math.Min(deviceData.Data.Length - 1, reportSize - 1);
if (dataLength > 0)
{
Array.Copy(deviceData.Data, 1, _data, 0, dataLength);
}
}
public bool Exists { get; private set; }
public DeviceData.ReadStatus ReadStatus => _status;
public byte ReportId
{
get => _reportId;
set
{
_reportId = value;
Exists = true;
}
}
public byte[] Data
{
get => _data;
set
{
_data = value;
Exists = true;
}
}
public byte[] GetBytes()
{
var data = new byte[_data.Length + 1];
data[0] = _reportId;
Array.Copy(_data, 0, data, 1, _data.Length);
return data;
}
}
}

View File

@ -1,21 +0,0 @@
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.
public class StateAsync
{
private readonly object _callerDelegate;
private readonly object _callbackDelegate;
public StateAsync(object callerDelegate, object callbackDelegate)
{
_callerDelegate = callerDelegate;
_callbackDelegate = callbackDelegate;
}
public object CallerDelegate
{ get { return _callerDelegate; } }
public object CallbackDelegate
{ get { return _callbackDelegate; } }
}
}