Created a HIDReport instead of only a byte[]

This commit is contained in:
EonaCat 2025-07-14 20:19:52 +02:00
parent f8603b8ce7
commit 1b20217def
13 changed files with 676 additions and 355 deletions

View File

@ -1,5 +1,6 @@
using EonaCat.HID.EventArguments;
using EonaCat.HID.Helpers;
using EonaCat.HID.Models;
using System;
using System.Collections.Generic;
using System.Diagnostics;
@ -106,6 +107,8 @@ namespace EonaCat.HID.Analyzer
deviceName = device.ProductName;
deviceManufacturer = device.Manufacturer;
deviceSerialNumber = device.SerialNumber;
var isWritingSupported = device.IsWritingSupport;
var isReadingSupported = device.IsReadingSupport;
var row = new string[]
{
@ -113,6 +116,8 @@ namespace EonaCat.HID.Analyzer
deviceName,
deviceManufacturer,
deviceSerialNumber,
isReadingSupported.ToString(),
isWritingSupported.ToString(),
device.InputReportByteLength.ToString(),
device.OutputReportByteLength.ToString(),
device.FeatureReportByteLength.ToString(),
@ -256,7 +261,7 @@ namespace EonaCat.HID.Analyzer
{
try
{
var str = $"Rx Input Report from device {e.Device.ProductName} => {BitConverter.ToString(e.Data)}";
var str = $"Rx Input Report from device {e.Device.ProductName} => {BitConverter.ToString(e.Report.Data)}";
AppendEventLog(str, Color.Blue);
}
catch (Exception ex)
@ -269,7 +274,7 @@ namespace EonaCat.HID.Analyzer
{
try
{
var str = string.Format("Removed Device --> VID {0:X4}, PID {0:X4}", e.Device.VendorId, e.Device.ProductId);
var str = string.Format("Removed Device {0} --> VID {1:X4}, PID {2:X4}", e.Device.ProductName, e.Device.VendorId, e.Device.ProductId);
AppendEventLog(str);
}
catch (Exception ex)
@ -282,7 +287,7 @@ namespace EonaCat.HID.Analyzer
{
try
{
var str = string.Format("Inserted Device --> VID {0:X4}, PID {0:X4}", e.Device.VendorId, e.Device.ProductId);
var str = string.Format("Inserted Device {0} --> VID {1:X4}, PID {2:X4}", e.Device.ProductName, e.Device.VendorId, e.Device.ProductId);
AppendEventLog(str, Color.Orange);
}
catch (Exception ex)
@ -332,14 +337,14 @@ namespace EonaCat.HID.Analyzer
throw new Exception("This device has no Input Report support!");
}
var buffer = await _device.ReadInputReportAsync();
if (buffer.Length < 2)
var report = await _device.ReadInputReportAsync();
if (report == null || report.Data.Length < 2)
{
AppendEventLog("Received report is too short to contain a valid Report ID.", Color.Red);
AppendEventLog("Received report is null or is too short to contain a valid Report ID.", Color.Red);
return;
}
var str = string.Format("Rx Input Report [{0}] <-- {1}", buffer.Length, BitConverter.ToString(buffer));
var str = string.Format("Rx Input Report [{0}] <-- {1}", report.Data.Length, BitConverter.ToString(report.Data));
AppendEventLog(str, Color.Blue);
}
catch (Exception ex)
@ -352,27 +357,33 @@ namespace EonaCat.HID.Analyzer
{
try
{
if (_device == null)
{
AppendEventLog("No device connected. Please select a device and click 'Connect'.", Color.Red);
return;
}
byte hidReportId = byte.Parse(comboBoxReportId.Text.Trim());
byte[] buf = ByteHelper.HexStringToByteArray(textBoxWriteData.Text.Trim());
byte[] dataBuffer = 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
if (_device.OutputReportByteLength <= 0)
{
throw new Exception("This device has no Output Report support!");
}
if (dataBuffer.Length > _device.OutputReportByteLength - 1)
{
throw new Exception("Output Report Length Exceeds allowed size.");
}
var outputReport = new HidReport(hidReportId, dataBuffer);
await _device.WriteOutputReportAsync(outputReport);
AppendEventLog($"Output report sent: {BitConverter.ToString(outputReport)}", Color.DarkGreen);
AppendEventLog($"Output report sent (Report ID: 0x{hidReportId:X2}): {ByteHelper.ByteArrayToHexString(dataBuffer)}", Color.DarkGreen);
}
catch (Exception ex)
{
AppendEventLog("Write failed: " + ex.Message, Color.DarkRed);
}
}
catch (Exception ex)
{
PopupException("Error preparing output report: " + ex.Message);
PopupException("Error sending output report: " + ex.Message);
}
}
@ -387,8 +398,15 @@ namespace EonaCat.HID.Analyzer
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));
HidReport report = await _device.GetFeatureReportAsync(hidReportId);
if (report == null || report.Data == null || report.Data.Length < 1)
{
AppendEventLog("Received feature report is null or too short.", Color.Red);
return;
}
string hexString = $"{report.ReportId:X2} - {ByteHelper.ByteArrayToHexString(report.Data)}";
var str = $"Rx Feature Report [{report.Data.Length + 1}] <-- {hexString}";
AppendEventLog(str, Color.Blue);
}
catch (Exception ex)
@ -397,23 +415,33 @@ namespace EonaCat.HID.Analyzer
}
}
private async void ButtonWriteFeature_Click(object sender, EventArgs e)
{
try
{
var hidReportId = byte.Parse(comboBoxReportId.Text);
var buf = ByteHelper.HexStringToByteArray(textBoxWriteData.Text);
var len = _device.FeatureReportByteLength;
if (buf.Length > len)
if (!byte.TryParse(comboBoxReportId.Text, out var hidReportId))
{
throw new Exception("Write Feature Report Length Exceed");
throw new FormatException("Invalid Report ID format.");
}
Array.Resize(ref buf, len);
await _device.SendFeatureReportAsync(buf);
var str = string.Format("Tx Feature Report [{0}] --> {1}", buf.Length, ByteHelper.ByteArrayToHexString(buf));
AppendEventLog(str, Color.DarkGreen);
var data = ByteHelper.HexStringToByteArray(textBoxWriteData.Text);
int maxLen = _device.FeatureReportByteLength - 1;
if (data.Length > maxLen)
{
throw new InvalidOperationException($"Feature report data length exceeds max allowed ({maxLen}).");
}
var reportData = new byte[data.Length];
Array.Copy(data, reportData, data.Length);
var hidReport = new HidReport(hidReportId, reportData);
await _device.SendFeatureReportAsync(hidReport);
string logMsg = $"Tx Feature Report [{data.Length + 1}] --> {ByteHelper.ByteArrayToHexString(new[] { hidReportId }.Concat(reportData).ToArray())}";
AppendEventLog(logMsg, Color.DarkGreen);
}
catch (Exception ex)
{

View File

@ -129,6 +129,12 @@
<metadata name="Column11.UserAddedColumn" type="System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">
<value>True</value>
</metadata>
<metadata name="Column6.UserAddedColumn" type="System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">
<value>True</value>
</metadata>
<metadata name="Column3.UserAddedColumn" type="System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">
<value>True</value>
</metadata>
<metadata name="Column7.UserAddedColumn" type="System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">
<value>True</value>
</metadata>
@ -151,59 +157,59 @@
<data name="toolStripButtonReload.Image" type="System.Drawing.Bitmap, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>
iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8
YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAIqSURBVDhPfZJNaBNBFMcXSknBKKkg9FKL7SKSQ1tpMSEm
mZ0NhFzirXdPftyEHjxueohsNiDUj0IC7UW95CIq5NDLpkQo7U401hatBkKys5kG0/Qe0JW3ycZsWvuH
P8u89+bHe2+W4/4jdWrKo3q97r1gcBw8nD9XOY4b2Zy+flK4xsfL/sCDz7duf4N4XRDQcK0lebcZSGns
MVgmxlWIqTMzfJ7nXT9iMdfXxdBkHSGeCmKHIvyaLCyMOgCSWh1TdhqGQpiZ1pjgSA6IIjRPBZFRAb9y
JBTClpLbRkfZYUUAtH5m19oHCbNdyXat3T2s7T209tCDdCjGd/qAFGG7Mmk863bSnDiuZFQHoPzIPC4v
b9r1OsIvKBK3rEPyU+NKcrv+RyZHfrugDyjd7/ogYULsHwD5qwj9rkejl7m0xnwwu7TfdDsAvYs2ZBDQ
isUuUUE0aThyk1NKR7MASH38dfEUYAAyCKgi5AGAjvEcly6zCzACdHImYH/l1AgU4wCMULN/MIWwgkIa
zx0Ae4GHT832l2ULUEVooorQmC6IazQsFux6Ll1i8e4z0nk4tyrZl31AzxDTUUTQBVy0njEsxvsAkKKx
TJJQZkPOEgCs5YVCFDpxJDPEHFVIYx06eVJq8osbW5Ox1byLX827vNI7Hmpqkcg0RVgGGxhHHQBbMjGC
8PVtFL771ov3Zlfex28kPpws5XIjw7XnKvimOA72Sjn3nPTWM5y39RdV/noYsphYLAAAAABJRU5ErkJg
gg==
YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAIrSURBVDhPfZJPaBNBFMYXSknBKKkg9NIW20Uhh7bSYkJM
MjsbCLnEW++e/HMTevC4KUTZbEBo1UIC7UW95CIqBPSyKZFCm4nG2qLVYEh2NtNgmt4DuvI22ZhNaz/4
WOa9Nz/ee7Mc9x+pk5Mu1e127vr9o+DB/JnKcNzQu6krx7nLfLTk9d39dP3GV4jXBAEN1pqSdxq+RIE9
AMtEn4CYOj3NZ3ne8T0ScXxZCIzXEOKpILYpwi/I/PywDSCplRFlu64rhBnJAhNsyT5RhOaoIDIq4Oe2
hELYYnxLbyvbLA+A5o/0Wms/ZrTK6Y4Ltw6qu/fMPXQhbYrxzR4gQdiOTOqrnU4aY0fllGoDlO4bR6Wl
91a9hvBTisRN8/DwY/1SfKv6RyaHXqugByje6Xg/ZkDsHwB5fyL0uxYOX+SSBeaB2aW9htMG6F60IP2A
ZiRygQqiQYOha5xSPJwBQOLDr/MnAH2QfkAFIRcANIxnuWSJnYMRoJNTAXvLJ0agGPtghKr1gymE5RRS
f2IDWAs8eGy0Pi+ZgApCYxWERjRBXKNBMWfVc8kii3aekc7BuVlOP+sBuoaYhkKCJuC8+YxBMdoDgJQC
S8UJZRbkNAHAXF4gQKETWzJFjGGF1Nehk0fFBr+wsTkeWck6+JWswy295qGmGgpNUYRlsI5x2AawJBPd
D1/PRu6bZz1/e2b5TfRq7O3xYiYzNFh7pvwv86Ngt5RxzkqvXIN5S38BNmd6B/1xTKkAAAAASUVORK5C
YII=
</value>
</data>
<data name="toolStripButtonOpen.Image" type="System.Drawing.Bitmap, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>
iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8
YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAIoSURBVDhPY5BuvNMjXXPjhWTJubsiuUfeCOccfS2Re8qe
AQrEiy8oihWcuSGeeXw2TAwFSNXfOSBdde2/ZNml/+J5J8FYJPtEAUxesuxSM0hOMOPwd1SdUEDIAImy
yyUgObHkw59RdUIBIQNE66/wSKVvOSWaeyIWVScUEDIABIycveNNXH18TZy9lQwdveQZ6uuZ4JIyjXf3
YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAIoSURBVDhPY5BuvNMjXXPjhWTJubsiuYffCOccfS2Re8qe
AQrEiy8oihWcuSGeeXw2TAwFSNXfOSBdde2/ZNml/+J5J8FYJPtEAUxesuxSM0hOIOPwd1SdUEDIAImy
yyUgObGkw59RdUIBIQNE66/wSKVvOSWaeyIWVScUEDIABIycveNNXH18TZy9lQwdveQZ6uuZ4JIyjXf3
S1ejGiCeczwP2QAQMHL1dTJx9k4DYWNn7ynGxsasYAmpxjvOUrU3lsrl7Fgunnt0rVjuyVXiGUfFQHLz
7/83qL/yXaX+0nclZOyaXFro3TjNHcUGU1ffBHN3fwUYf9H9H4d7b/z633gFE9dd+Po/+dinv8mnflTi
7/83qL/yXaX+0nclZOyaXFro3TjNHcUGU1ffBHN3fwUYf9H9H4d7b/z633gFE9dd+Po/6ejHv8mnflTi
NGD5o+/vM6eu+u+TmP3fLjwZBRs4+fxPWnXqf+yJH/vhBhg5+/oYuXkbIxuQ3NT/3ykw4n+yte//+UZ+
/5cZ+P33cvD5r2Vh9z+kd9n/+BPfD8ANMPPw4DN29u4ODQ1lRjegw8zv/3elgP9flQP+x9r6YjcABEwc
/dSNXXzaTFy8q/p3X/wEM8DUxef/IkO//1Xmfv9NXHC4AB2AXJDSNu2/a3A0WBMy1jSz/R81cxthAxou
/fhftvv2//KDjxH4wKP/hdtu/C+98BO/AUvufn/bePXXf4tJ9/7Hbnn733HWw/+hq1/+9178DIxLzv/8
H3v8xx50fXAw/87Pvkm3fz1puvLrbiMarr/8617y6R9Pkk//8AIABXiIYedL+BwAAAAASUVORK5CYII=
/dSNXXzaTFy8q/p3XfgEM8DUxef/IkO//1Xmfv9NXHC4AB2AXJDSNu2/a3A0WBMy1jSz/R81cxthAxou
/fhftvv2//KDjxH4wKP/hdtu/C+98BO/AYvvfH/bePXXf4tJ9/7Hbnn733HWw/+hq1/+9178DIxLzv/8
H3v8xx50fXAw/87Pvkm3fz1puvLrbiMarr/8617y6R9Pkk//8AIA8QuIV6in+C4AAAAASUVORK5CYII=
</value>
</data>
<data name="toolStripButtonClear.Image" type="System.Drawing.Bitmap, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>
iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8
YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAJPSURBVDhPdZH/SxNhHMdHCpkFRT/1P9Qv/VS4i4FZlET+
ZDaiLJj7opk259Ya2zPnTm1uoURBRLToNwuCRcvcnfuhLwQhkVOsxTzv0PYEtaDadLft+cQtdttu6w0f
uOdzn9free4elapOqHmsp1isVfbrRo/uNhttgUmD/aa61KPYrxvUHP5UPfmfGGyBp6ZrAdBZfL97bX5K
6qkZ7KMY7FLO1o3J5g9KgkrJ4bfCjnuzgb33px0pv8+19TpkOa7k5CCEGo22wOOSRNuPxJ6hkSu3p5yZ
Uo+mERE/GORPrIkk0Vsnn5zv98IZgwt0lokiKNWwawJWn2tha8X8nQj0ASUrpxuhJm0fWtcNVcNfHrZB
fnkQQKBBTLj/EH60S8kWsxTt3XVryiUfe9g5DqvPuiC3eBkIN1IUlMsbJLxnf5VgOmBPV+6cCHVCOtQK
hHMr4HIR3ntBFpidN8rHDh6F3NIAFOIOKKxYZUBa52NXKwRj5dsJPbJGrGi8+MNyH/ug8NlesyPwXsi+
OgebkQ5Iz55KwQZqlgVSfrzRP9hcNJMaUKAhv2yB7LtLxWcxQafr3ogm+nMP4ce6xQRdkOE1DxDO8w98
3wOZ8EnIxwZMx14md2qi0CjDbZHUbopNrmui3/YB771YkpAEgsyLdijEr4Mo0FkSd1ileTWTvENFsFkW
UEzyLMViaGGwUVqTtdHWvEAviIJbzHLoV2budIxwnoPSuxPh+PZDEZxqYfGCLOicgQaKxWFNlGuSmyqV
CgBtA5hpqOxJUTN48Mg87vgL5YHJNRvDXwIAAAAASUVORK5CYII=
YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAJOSURBVDhPdZHtS1NRHMdHCpkFRa/6H+pNbyLcjYFZVES+
Mh1RFukeNNPm3DTZTs57fdoWgyiIiBa9syBYtMzd6170QBASNsVa03kv2k5QC6qt3T2cX9wLu9vu1hd+
cM73/L6f86TR1BC1gA0Uh/Vqv6YM6G6jye51G0duaosexX3doubxp8rO/8ho9z41D3vhsnXmd4/dQ0me
lsUzFIud6t6aMts9fglQDjn8Vthxb867975vNOmedmZeB6zH1TlFCKF6k937uAjp6EPZ7sGxq7d9jnTR
o2lEsh+MyhWrJEEMNveT8300nDU6ocs6JQelGnJOwfpzPWRWLd+JwBxQZxV1ItSg70WbXYOV4S8PWyC/
MgAgMCDG0B/Cj7ers7KWwz27bvmcyrGHHJOw/qwdch+vAImPyYBS0X7Cu/ZXAHye4VT5zmuBNkgFmoHE
b6jCpSI8fUEBWBzTpWP7j0JuuR8K0VEorNqUgDTPR66VASZKvxN4ZAvZ0KT8YLmlXih8HqnaEXgaxFfn
4G+oFVJzp5OwhRoVgKQfbwwP0ksWUhUUGMivWEF8d0keizEmVfNHdOGfewg/0SnGmIIS3nABibvkcfZ9
N6SDJyEf6Tcfe5nYqQtDvRJuCSV3U1xiUxf+tg94+mIRQtYQpF+cgkL0OogCI5LoqE3q17KJO1QIWxQA
xSY6KA5DE4tN0pxsjDfnBWZRFFA2E0e/0vNnIiTuOiitnQhGtx8K4WQThxcVQNss1FEcDurC8QbF1Gg0
AGgbwGxduSdJy+KBIwu49R+1rskaRvlY/wAAAABJRU5ErkJggg==
</value>
</data>
<data name="toolStripButtonFilter.Image" type="System.Drawing.Bitmap, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>
iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8
YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAEXSURBVDhPY2CgBphQKSYe5qr+1dTU9D8xOMxV7eu0YnEx
uAETKxXsPZwM/3/dxfn//0F2vPjrLq7/ILVTyhXsUFyRH6N+tSpVFUMDMv53gP1/RbLq/7xYjUsomkFg
Zr0kV4S/zqcVrdIYGmF4SZPM/0BftS8Tc4X40PWDwcQyeQsHe8O/5xcIYmi+tEjwv7294V8Mp6ODrjyF
Mi9ng/+vN3PDNb/dyv0fJNaeo9CGrh4rKElSOVwQpw43IC9G439JvMphdHU4wZR6UR5zc1O4ASA2KIzQ
1eEE9fUMTKD4hhkAYqOrwQsGlwGfdnD9t7QwId0AkKY9k8X/+7kZ/C9LVDmCroYgqEhU3R/jo/WhM1+h
HF2OqgAAizrlNjwLhaIAAAAASUVORK5CYII=
YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAETSURBVDhPY2CgBphQKSYe5qL21dTU9D8xGKR2WrG4GNyA
iZUK9h5Ohv+/7uL8//8gO178dRfXf5DaKeUKdiiuyI9Rv1qVqoqhARn/O8D+vyJZ9X9erMYlFM0gMLNe
kivCX+fTilZpDI0wvKRJ5n+gr+qXiblCfOj6wWBimbyFvb3h3/MLBDE0X1ok+B8kh+F0dNCVp1Dm5Wzw
//Vmbrjmt1u5/4PE2nMU2tDVYwUlSSqHC+LU4QbkxWj8L4lXOYyuDieYUi/KY25uCjcAxAaFEbo6nKC+
noEJFN8wA0BsdDV4weAy4NMOrv+WFiakGwDStGey+H8/N4P/ZYkqR9DVEAQViar7Y3y0PnTmK5Sjy1EV
AAB6heUx9GPlMgAAAABJRU5ErkJggg==
</value>
</data>
<metadata name="menuStrip1.TrayLocation" type="System.Drawing.Point, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">

View File

@ -31,15 +31,6 @@
System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(MainForm));
this.textBoxWriteData = new System.Windows.Forms.TextBox();
this.dataGridView1 = new System.Windows.Forms.DataGridView();
this.Column1 = new System.Windows.Forms.DataGridViewTextBoxColumn();
this.Column9 = new System.Windows.Forms.DataGridViewTextBoxColumn();
this.Column10 = new System.Windows.Forms.DataGridViewTextBoxColumn();
this.Column11 = new System.Windows.Forms.DataGridViewTextBoxColumn();
this.Column7 = new System.Windows.Forms.DataGridViewTextBoxColumn();
this.Column4 = new System.Windows.Forms.DataGridViewTextBoxColumn();
this.Column5 = new System.Windows.Forms.DataGridViewTextBoxColumn();
this.Column2 = new System.Windows.Forms.DataGridViewTextBoxColumn();
this.Column8 = new System.Windows.Forms.DataGridViewTextBoxColumn();
this.tableLayoutPanel1 = new System.Windows.Forms.TableLayoutPanel();
this.toolStrip1 = new System.Windows.Forms.ToolStrip();
this.toolStripSeparator = new System.Windows.Forms.ToolStripSeparator();
@ -67,6 +58,17 @@
this.rtbEventLog = new System.Windows.Forms.RichTextBox();
this.statusStrip1 = new System.Windows.Forms.StatusStrip();
this.toolStripStatusLabel1 = new System.Windows.Forms.ToolStripStatusLabel();
this.Column1 = new System.Windows.Forms.DataGridViewTextBoxColumn();
this.Column9 = new System.Windows.Forms.DataGridViewTextBoxColumn();
this.Column10 = new System.Windows.Forms.DataGridViewTextBoxColumn();
this.Column11 = new System.Windows.Forms.DataGridViewTextBoxColumn();
this.Column6 = new System.Windows.Forms.DataGridViewTextBoxColumn();
this.Column3 = new System.Windows.Forms.DataGridViewTextBoxColumn();
this.Column7 = new System.Windows.Forms.DataGridViewTextBoxColumn();
this.Column4 = new System.Windows.Forms.DataGridViewTextBoxColumn();
this.Column5 = new System.Windows.Forms.DataGridViewTextBoxColumn();
this.Column2 = new System.Windows.Forms.DataGridViewTextBoxColumn();
this.Column8 = new System.Windows.Forms.DataGridViewTextBoxColumn();
((System.ComponentModel.ISupportInitialize)(this.dataGridView1)).BeginInit();
this.tableLayoutPanel1.SuspendLayout();
this.toolStrip1.SuspendLayout();
@ -96,6 +98,8 @@
this.Column9,
this.Column10,
this.Column11,
this.Column6,
this.Column3,
this.Column7,
this.Column4,
this.Column5,
@ -112,69 +116,6 @@
this.dataGridView1.SelectionChanged += new System.EventHandler(this.DataGridView1_SelectionChanged);
this.dataGridView1.DoubleClick += new System.EventHandler(this.dataGridView1_DoubleClick);
//
// Column1
//
this.Column1.HeaderText = "No";
this.Column1.Name = "Column1";
this.Column1.ReadOnly = true;
this.Column1.Width = 87;
//
// Column9
//
this.Column9.AutoSizeMode = System.Windows.Forms.DataGridViewAutoSizeColumnMode.Fill;
this.Column9.HeaderText = "Name";
this.Column9.Name = "Column9";
this.Column9.ReadOnly = true;
//
// Column10
//
this.Column10.AutoSizeMode = System.Windows.Forms.DataGridViewAutoSizeColumnMode.Fill;
this.Column10.HeaderText = "Manufacturer";
this.Column10.Name = "Column10";
this.Column10.ReadOnly = true;
//
// Column11
//
this.Column11.AutoSizeMode = System.Windows.Forms.DataGridViewAutoSizeColumnMode.Fill;
this.Column11.HeaderText = "SerialNo";
this.Column11.Name = "Column11";
this.Column11.ReadOnly = true;
//
// Column7
//
this.Column7.HeaderText = "InputReport";
this.Column7.Name = "Column7";
this.Column7.ReadOnly = true;
this.Column7.Width = 86;
//
// Column4
//
this.Column4.HeaderText = "OutputReport";
this.Column4.Name = "Column4";
this.Column4.ReadOnly = true;
this.Column4.Width = 87;
//
// Column5
//
this.Column5.HeaderText = "FeatureReport";
this.Column5.Name = "Column5";
this.Column5.ReadOnly = true;
this.Column5.Width = 86;
//
// Column2
//
this.Column2.AutoSizeMode = System.Windows.Forms.DataGridViewAutoSizeColumnMode.Fill;
this.Column2.HeaderText = "Info";
this.Column2.Name = "Column2";
this.Column2.ReadOnly = true;
//
// Column8
//
this.Column8.AutoSizeMode = System.Windows.Forms.DataGridViewAutoSizeColumnMode.Fill;
this.Column8.HeaderText = "DevicePath";
this.Column8.Name = "Column8";
this.Column8.ReadOnly = true;
//
// tableLayoutPanel1
//
this.tableLayoutPanel1.ColumnCount = 1;
@ -473,6 +414,81 @@
this.toolStripStatusLabel1.Size = new System.Drawing.Size(118, 17);
this.toolStripStatusLabel1.Text = "toolStripStatusLabel1";
//
// Column1
//
this.Column1.HeaderText = "No";
this.Column1.Name = "Column1";
this.Column1.ReadOnly = true;
this.Column1.Width = 87;
//
// Column9
//
this.Column9.AutoSizeMode = System.Windows.Forms.DataGridViewAutoSizeColumnMode.Fill;
this.Column9.HeaderText = "Name";
this.Column9.Name = "Column9";
this.Column9.ReadOnly = true;
//
// Column10
//
this.Column10.AutoSizeMode = System.Windows.Forms.DataGridViewAutoSizeColumnMode.Fill;
this.Column10.HeaderText = "Manufacturer";
this.Column10.Name = "Column10";
this.Column10.ReadOnly = true;
//
// Column11
//
this.Column11.AutoSizeMode = System.Windows.Forms.DataGridViewAutoSizeColumnMode.Fill;
this.Column11.HeaderText = "SerialNo";
this.Column11.Name = "Column11";
this.Column11.ReadOnly = true;
//
// Column6
//
this.Column6.HeaderText = "CanRead";
this.Column6.Name = "Column6";
this.Column6.ReadOnly = true;
//
// Column3
//
this.Column3.HeaderText = "CanWrite";
this.Column3.Name = "Column3";
this.Column3.ReadOnly = true;
//
// Column7
//
this.Column7.HeaderText = "InputReport";
this.Column7.Name = "Column7";
this.Column7.ReadOnly = true;
this.Column7.Width = 86;
//
// Column4
//
this.Column4.HeaderText = "OutputReport";
this.Column4.Name = "Column4";
this.Column4.ReadOnly = true;
this.Column4.Width = 87;
//
// Column5
//
this.Column5.HeaderText = "FeatureReport";
this.Column5.Name = "Column5";
this.Column5.ReadOnly = true;
this.Column5.Width = 86;
//
// Column2
//
this.Column2.AutoSizeMode = System.Windows.Forms.DataGridViewAutoSizeColumnMode.Fill;
this.Column2.HeaderText = "Info";
this.Column2.Name = "Column2";
this.Column2.ReadOnly = true;
//
// Column8
//
this.Column8.AutoSizeMode = System.Windows.Forms.DataGridViewAutoSizeColumnMode.Fill;
this.Column8.HeaderText = "DevicePath";
this.Column8.Name = "Column8";
this.Column8.ReadOnly = true;
//
// MainForm
//
this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F);
@ -537,6 +553,8 @@
private System.Windows.Forms.DataGridViewTextBoxColumn Column9;
private System.Windows.Forms.DataGridViewTextBoxColumn Column10;
private System.Windows.Forms.DataGridViewTextBoxColumn Column11;
private System.Windows.Forms.DataGridViewTextBoxColumn Column6;
private System.Windows.Forms.DataGridViewTextBoxColumn Column3;
private System.Windows.Forms.DataGridViewTextBoxColumn Column7;
private System.Windows.Forms.DataGridViewTextBoxColumn Column4;
private System.Windows.Forms.DataGridViewTextBoxColumn Column5;

View File

@ -1,4 +1,5 @@
using EonaCat.HID;
using EonaCat.HID.Models;
using System.Globalization;
namespace EonaCat.HID.Example
@ -46,7 +47,7 @@ namespace EonaCat.HID.Example
_device.OnDataReceived += (s, e) =>
{
Console.WriteLine($"Rx Data: {BitConverter.ToString(e.Data)}");
Console.WriteLine($"Rx Data: {BitConverter.ToString(e.Report.Data)}");
};
_device.OnError += (s, e) =>
@ -62,14 +63,13 @@ namespace EonaCat.HID.Example
Console.WriteLine("Listening... Press [Enter] to send test output report.");
Console.ReadLine();
// Example: Send output report
// Example: Send output report using HidReport
var data = ByteHelper.HexStringToByteArray("01-02-03");
byte[] outputReport = new byte[data.Length + 1];
outputReport[0] = 0x00; // Report ID
Array.Copy(data, 0, outputReport, 1, data.Length);
var reportId = (byte)0x00; // Report ID
var outputReport = new HidReport(reportId, data);
await _device.WriteOutputReportAsync(outputReport);
Console.WriteLine($"Sent output report: {BitConverter.ToString(outputReport)}");
Console.WriteLine($"Sent output report: Report ID: {reportId}, Data: {BitConverter.ToString(data)}");
Console.WriteLine("Press [Enter] to exit...");
Console.ReadLine();

View File

@ -1,4 +1,5 @@
using System;
using EonaCat.HID.Models;
using System;
namespace EonaCat.HID.EventArguments
{
@ -11,12 +12,12 @@ namespace EonaCat.HID.EventArguments
public class HidDataReceivedEventArgs : EventArgs
{
public IHid Device { get; }
public byte[] Data { get; }
public HidReport Report { get; }
public HidDataReceivedEventArgs(IHid device, byte[] data)
public HidDataReceivedEventArgs(IHid device, HidReport report)
{
Device = device;
Data = data;
Report = report;
}
}
}

View File

@ -11,6 +11,8 @@ namespace EonaCat.HID.EventArguments
public class HidEventArgs : EventArgs
{
public IHid Device { get; }
public bool HasDevice => Device != null;
public bool IsConnected => HasDevice && Device.IsConnected;
public HidEventArgs(IHid device)
{

View File

@ -1,6 +1,8 @@
using EonaCat.HID.EventArguments;
using EonaCat.HID.Managers;
using EonaCat.HID.Managers.Linux;
using EonaCat.HID.Models;
using Microsoft.Win32.SafeHandles;
using System;
using System.Collections.Generic;
using System.IO;
@ -17,6 +19,7 @@ namespace EonaCat.HID
{
private readonly string _devicePath;
private FileStream _stream;
private bool _isOpen;
private readonly object _lock = new object();
private CancellationTokenSource _cts;
@ -29,6 +32,11 @@ namespace EonaCat.HID
public int InputReportByteLength { get; private set; }
public int OutputReportByteLength { get; private set; }
public int FeatureReportByteLength { get; private set; }
public bool IsReadingSupport { get; private set; }
public bool IsWritingSupport { get; private set; }
public bool IsConnected => _isOpen;
public IDictionary<string, object> Capabilities { get; private set; } = new Dictionary<string, object>();
public event EventHandler<HidDataReceivedEventArgs> OnDataReceived;
@ -51,21 +59,33 @@ namespace EonaCat.HID
{
lock (_lock)
{
if (_stream != null)
{
if (_stream != null || _isOpen)
return;
}
// Open device for read/write
var fd = NativeMethods.open(_devicePath, NativeMethods.O_RDWR | NativeMethods.O_NONBLOCK);
// Open HID device in non-blocking read/write mode
int 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}")));
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);
try
{
_stream = new FileStream(safeHandle, FileAccess.ReadWrite, bufferSize: 64, isAsync: false);
_isOpen = true;
IsReadingSupport = true;
IsWritingSupport = true;
}
catch (Exception ex)
{
safeHandle.Dispose();
OnError?.Invoke(this, new HidErrorEventArgs(this, ex));
}
}
}
@ -162,20 +182,24 @@ namespace EonaCat.HID
return null;
}
public async Task WriteOutputReportAsync(byte[] data)
public async Task WriteOutputReportAsync(HidReport report)
{
if (data == null)
if (report == null)
{
OnError?.Invoke(this, new HidErrorEventArgs(this, new ArgumentNullException(nameof(data))));
var ex = new ArgumentNullException(nameof(report));
OnError?.Invoke(this, new HidErrorEventArgs(this, ex));
return;
}
if (_stream == null)
{
OnError?.Invoke(this, new HidErrorEventArgs(this, new InvalidOperationException("Device not open")));
var ex = new InvalidOperationException("Device not open");
OnError?.Invoke(this, new HidErrorEventArgs(this, ex));
return;
}
var data = report.Data;
await Task.Run(() =>
{
lock (_lock)
@ -188,17 +212,19 @@ namespace EonaCat.HID
catch (Exception ex)
{
OnError?.Invoke(this, new HidErrorEventArgs(this, ex));
throw;
}
}
});
}
public async Task<byte[]> ReadInputReportAsync()
public async Task<HidReport> ReadInputReportAsync()
{
if (_stream == null)
{
OnError?.Invoke(this, new HidErrorEventArgs(this, new InvalidOperationException("Device not open")));
return Array.Empty<byte>();
var ex = new InvalidOperationException("Device not open");
OnError?.Invoke(this, new HidErrorEventArgs(this, ex));
return null;
}
return await Task.Run(() =>
@ -215,25 +241,41 @@ namespace EonaCat.HID
catch (Exception ex)
{
OnError?.Invoke(this, new HidErrorEventArgs(this, ex));
return null;
}
}
if (bytesRead <= 0)
{
return Array.Empty<byte>();
return null;
}
byte[] result = new byte[bytesRead];
Array.Copy(buffer, result, bytesRead);
return result;
// First byte is report ID, rest is data
byte reportId = buffer[0];
byte[] data = new byte[bytesRead - 1];
Array.Copy(buffer, 1, data, 0, data.Length);
return new HidReport(reportId, data);
});
}
public async Task SendFeatureReportAsync(byte[] data)
{
if (data == null || data.Length == 0)
throw new ArgumentException("Feature report data must not be null or empty.");
public async Task SendFeatureReportAsync(HidReport report)
{
if (report == null)
throw new ArgumentNullException(nameof(report));
// Prepare full report buffer: [reportId][reportData...]
int size = 1 + (report.Data?.Length ?? 0);
byte[] buffer = new byte[size];
buffer[0] = report.ReportId;
if (report.Data != null && report.Data.Length > 0)
{
Array.Copy(report.Data, 0, buffer, 1, report.Data.Length);
}
await Task.Run(() =>
{
int fd = NativeMethods.open(_devicePath, NativeMethods.O_RDWR);
if (fd < 0)
{
@ -241,46 +283,50 @@ namespace EonaCat.HID
return;
}
IntPtr unmanagedBuffer = IntPtr.Zero;
try
{
int size = data.Length;
IntPtr buffer = Marshal.AllocHGlobal(size);
Marshal.Copy(data, 0, buffer, size);
unmanagedBuffer = Marshal.AllocHGlobal(size);
Marshal.Copy(buffer, 0, unmanagedBuffer, size);
int request = NativeMethods._IOC(NativeMethods._IOC_WRITE, 'H', 0x06, size); // HIDIOCSFEATURE
int result = NativeMethods.ioctl(fd, request, buffer);
int result = NativeMethods.ioctl(fd, request, unmanagedBuffer);
if (result < 0)
{
OnError?.Invoke(this, new HidErrorEventArgs(this, new IOException("ioctl HIDIOCSFEATURE failed")));
return;
}
Marshal.FreeHGlobal(buffer);
}
finally
{
if (unmanagedBuffer != IntPtr.Zero)
Marshal.FreeHGlobal(unmanagedBuffer);
NativeMethods.close(fd);
}
await Task.CompletedTask;
});
}
public async Task<byte[]> GetFeatureReportAsync(byte reportId)
public async Task<HidReport> GetFeatureReportAsync(byte reportId)
{
const int maxFeatureSize = 256;
byte[] buffer = new byte[maxFeatureSize];
buffer[0] = reportId;
return await Task.Run(() =>
{
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>();
return new HidReport(0, Array.Empty<byte>());
}
IntPtr bufPtr = IntPtr.Zero;
try
{
IntPtr bufPtr = Marshal.AllocHGlobal(buffer.Length);
bufPtr = Marshal.AllocHGlobal(buffer.Length);
Marshal.Copy(buffer, 0, bufPtr, buffer.Length);
int request = NativeMethods._IOC(NativeMethods._IOC_READ, 'H', 0x07, buffer.Length); // HIDIOCGFEATURE
@ -288,18 +334,26 @@ namespace EonaCat.HID
if (result < 0)
{
OnError?.Invoke(this, new HidErrorEventArgs(this, new IOException("ioctl HIDIOCGFEATURE failed")));
return Array.Empty<byte>();
return new HidReport(0, Array.Empty<byte>());
}
Marshal.Copy(bufPtr, buffer, 0, buffer.Length);
Marshal.FreeHGlobal(bufPtr);
byte[] actualBuffer = new byte[result];
Marshal.Copy(bufPtr, actualBuffer, 0, result);
return await Task.FromResult(buffer);
byte actualReportId = actualBuffer.Length > 0 ? actualBuffer[0] : (byte)0;
byte[] reportData = actualBuffer.Length > 1 ? new byte[actualBuffer.Length - 1] : Array.Empty<byte>();
if (reportData.Length > 0)
Array.Copy(actualBuffer, 1, reportData, 0, reportData.Length);
return new HidReport(actualReportId, reportData);
}
finally
{
if (bufPtr != IntPtr.Zero)
Marshal.FreeHGlobal(bufPtr);
NativeMethods.close(fd);
}
});
}
public async Task StartListeningAsync(CancellationToken cancellationToken)
@ -341,7 +395,7 @@ namespace EonaCat.HID
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
break;
}
catch (Exception ex)
{
@ -354,7 +408,22 @@ namespace EonaCat.HID
var data = new byte[bytesRead];
Array.Copy(buffer, data, bytesRead);
OnDataReceived?.Invoke(this, new HidDataReceivedEventArgs(this, data));
// Extract reportId and report data
byte reportId = data.Length > 0 ? data[0] : (byte)0;
byte[] reportData;
if (data.Length > 1)
{
reportData = new byte[data.Length - 1];
Array.Copy(data, 1, reportData, 0, data.Length - 1);
}
else
{
reportData = Array.Empty<byte>();
}
var hidReport = new HidReport(reportId, reportData);
OnDataReceived?.Invoke(this, new HidDataReceivedEventArgs(this, hidReport));
}
else
{

View File

@ -1,6 +1,10 @@
using EonaCat.HID.EventArguments;
using EonaCat.HID.Models;
using Microsoft.Win32.SafeHandles;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
@ -36,19 +40,49 @@ namespace EonaCat.HID
public int OutputReportByteLength { get; private set; } = 64;
public int FeatureReportByteLength { get; private set; } = 64;
public bool IsReadingSupport { get; private set; }
public bool IsWritingSupport { get; private set; }
public bool IsConnected => _isOpen;
public IDictionary<string, object> Capabilities { get; private set; } = new Dictionary<string, object>();
public event EventHandler<HidDataReceivedEventArgs> OnDataReceived;
public event EventHandler<HidErrorEventArgs> OnError;
private const int KERN_SUCCESS = 0;
private const int kIOHIDOptionsTypeNone = 0;
public void Open()
{
IOHIDDeviceOpen(_deviceHandle, 0);
if (_isOpen)
{
return;
}
var result = IOHIDDeviceOpen(_deviceHandle, kIOHIDOptionsTypeNone);
if (result != KERN_SUCCESS)
{
throw new InvalidOperationException($"Failed to open HID device. IOHIDDeviceOpen returned: {result}");
}
IsReadingSupport = true;
IsWritingSupport = CheckOutputReportSupport();
_isOpen = true;
}
private bool CheckOutputReportSupport()
{
// On macOS, there's no simple API to test writing directly.
// You typically assume support based on report length.
return OutputReportByteLength > 0;
}
public void Close()
{
IOHIDDeviceClose(_deviceHandle, 0);
_isOpen = false;
}
public void Dispose()
@ -64,26 +98,35 @@ namespace EonaCat.HID
_listeningCts?.Cancel();
}
public async Task WriteOutputReportAsync(byte[] data)
public async Task WriteOutputReportAsync(HidReport report)
{
if (data is null)
if (report == null)
{
throw new ArgumentNullException(nameof(data));
throw new ArgumentNullException(nameof(report));
}
if (report.Data == null || report.Data.Length == 0)
{
throw new ArgumentException("Data cannot be null or empty", nameof(report));
}
await Task.Run(() =>
{
byte reportId = data.Length > 0 ? data[0] : (byte)0;
IntPtr buffer = Marshal.AllocHGlobal(data.Length);
// Total length includes reportId + data length
int length = 1 + report.Data.Length;
IntPtr buffer = Marshal.AllocHGlobal(length);
try
{
Marshal.Copy(data, 0, buffer, data.Length);
int res = IOHIDDeviceSetReport(_deviceHandle, 1, reportId, buffer, data.Length);
// First byte is reportId
Marshal.WriteByte(buffer, report.ReportId);
// Copy the rest of data after the reportId byte
Marshal.Copy(report.Data, 0, buffer + 1, report.Data.Length);
int res = IOHIDDeviceSetReport(_deviceHandle, 1 /* kIOHIDReportTypeOutput */, report.ReportId, buffer, length);
if (res != 0)
{
OnError?.Invoke(this, new HidErrorEventArgs(this, new Exception($"IOHIDDeviceSetReport (Output) failed: {res}")));
return;
}
}
finally
@ -93,26 +136,32 @@ namespace EonaCat.HID
});
}
public async Task SendFeatureReportAsync(byte[] data)
public async Task SendFeatureReportAsync(HidReport report)
{
if (data is null)
if (report == null)
{
throw new ArgumentNullException(nameof(data));
throw new ArgumentNullException(nameof(report));
}
if (report.Data == null || report.Data.Length == 0)
{
throw new ArgumentException("Data cannot be null or empty", nameof(report));
}
await Task.Run(() =>
{
byte reportId = data.Length > 0 ? data[0] : (byte)0;
IntPtr buffer = Marshal.AllocHGlobal(data.Length);
int length = 1 + report.Data.Length;
IntPtr buffer = Marshal.AllocHGlobal(length);
try
{
Marshal.Copy(data, 0, buffer, data.Length);
int res = IOHIDDeviceSetReport(_deviceHandle, 2, reportId, buffer, data.Length);
Marshal.WriteByte(buffer, report.ReportId);
Marshal.Copy(report.Data, 0, buffer + 1, report.Data.Length);
int res = IOHIDDeviceSetReport(_deviceHandle, 2 /* kIOHIDReportTypeFeature */, report.ReportId, buffer, length);
if (res != 0)
{
OnError?.Invoke(this, new HidErrorEventArgs(this, new Exception($"IOHIDDeviceSetReport (Feature) failed: {res}")));
return;
}
}
finally
@ -122,7 +171,7 @@ namespace EonaCat.HID
});
}
public async Task<byte[]> GetFeatureReportAsync(byte reportId)
public async Task<HidReport> GetFeatureReportAsync(byte reportId)
{
return await Task.Run(() =>
{
@ -131,16 +180,26 @@ namespace EonaCat.HID
try
{
int res = IOHIDDeviceGetReport(_deviceHandle, 2, reportId, buffer, ref length);
int res = IOHIDDeviceGetReport(_deviceHandle, 2 /* kIOHIDReportTypeFeature */, reportId, buffer, ref length);
if (res != 0)
{
OnError?.Invoke(this, new HidErrorEventArgs(this, new Exception($"IOHIDDeviceGetReport (Feature) failed: {res}")));
return Array.Empty<byte>();
return new HidReport(reportId, Array.Empty<byte>());
}
byte[] outBuf = new byte[length];
Marshal.Copy(buffer, outBuf, 0, length);
return outBuf;
if (outBuf.Length > 0 && outBuf[0] == reportId)
{
// Data excludes report ID (first byte)
var dataOnly = outBuf.Skip(1).ToArray();
return new HidReport(reportId, dataOnly);
}
else
{
return new HidReport(reportId, outBuf);
}
}
finally
{
@ -149,18 +208,40 @@ namespace EonaCat.HID
});
}
public Task<byte[]> ReadInputReportAsync()
public Task<HidReport> ReadInputReportAsync()
{
var tcs = new TaskCompletionSource<byte[]>();
var tcs = new TaskCompletionSource<HidReport>();
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);
int length = reportLength.ToInt32();
if (report == null || length == 0)
{
tcs.TrySetResult(new HidReport(0, Array.Empty<byte>()));
return;
}
byte[] output = new byte[length];
Array.Copy(report, output, length);
byte reportId = output.Length > 0 ? output[0] : (byte)0;
byte[] reportData;
if (output.Length > 1)
{
reportData = new byte[output.Length - 1];
Array.Copy(output, 1, reportData, 0, reportData.Length);
}
else
{
reportData = Array.Empty<byte>();
}
var hidReport = new HidReport(reportId, reportData);
tcs.TrySetResult(hidReport);
};
GCHandle.Alloc(callback);
@ -169,13 +250,12 @@ namespace EonaCat.HID
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;
return Task.CompletedTask;
}
_listeningCts = CancellationTokenSource.CreateLinkedTokenSource(ct);
@ -185,14 +265,26 @@ namespace EonaCat.HID
try
{
byte[] buffer = new byte[InputReportByteLength];
InputReportCallback callback = (ctx, result, sender, report, reportLength) =>
InputReportCallback callback = (context, result, sender, report, reportLength) =>
{
byte[] data = new byte[reportLength.ToInt32()];
Array.Copy(report, data, data.Length);
int len = reportLength.ToInt32();
if (report == null || report.Length < len)
{
return;
}
OnDataReceived?.Invoke(this, new HidDataReceivedEventArgs(this, data));
// Extract reportId (first byte)
byte reportId = len > 0 ? report[0] : (byte)0;
// Extract the rest of the data after reportId
byte[] reportData = len > 1 ? report.Skip(1).Take(len - 1).ToArray() : Array.Empty<byte>();
var hidReport = new HidReport(reportId, reportData);
OnDataReceived?.Invoke(this, new HidDataReceivedEventArgs(this, hidReport));
};
GCHandle.Alloc(callback);
IOHIDDeviceRegisterInputReportCallback(_deviceHandle, buffer, (IntPtr)buffer.Length, callback, IntPtr.Zero);
IOHIDDeviceScheduleWithRunLoop(_deviceHandle, CFRunLoopGetCurrent(), IntPtr.Zero);
@ -206,7 +298,7 @@ namespace EonaCat.HID
}
catch (OperationCanceledException)
{
return; // Exit gracefully if cancellation was requested
return;
}
catch (NotSupportedException)
{
@ -405,7 +497,7 @@ namespace EonaCat.HID
// For managing lifetime of listening loop
private CancellationTokenSource _listeningCts;
private Task _listeningTask;
private bool _isOpen;
private enum CFNumberType : int
{

View File

@ -1,5 +1,6 @@
using EonaCat.HID.EventArguments;
using EonaCat.HID.Managers.Windows;
using EonaCat.HID.Models;
using Microsoft.Win32.SafeHandles;
using System;
using System.Collections.Generic;
@ -35,8 +36,11 @@ namespace EonaCat.HID
public int InputReportByteLength { get; private set; }
public int OutputReportByteLength { get; private set; }
public int FeatureReportByteLength { get; private set; }
public bool IsConnected => _isOpen;
public IDictionary<string, object> Capabilities { get; } = new Dictionary<string, object>();
public bool IsReadingSupport { get; private set; }
public bool IsWritingSupport { get; private set; }
public event EventHandler<HidDataReceivedEventArgs> OnDataReceived;
public event EventHandler<HidErrorEventArgs> OnError;
@ -95,22 +99,23 @@ namespace EonaCat.HID
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")));
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);
_deviceStream = new FileStream(_deviceHandle, access, bufferSize: 64, isAsync: true);
_isOpen = true;
// HID descriptor parsing
// HID descriptor
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");
int capsRes = HidP_GetCaps(_preparsedData, out caps);
if (capsRes != NativeMethods.HIDP_STATUS_SUCCESS)
throw new Win32Exception(capsRes, "Failed HidP_GetCaps");
InputReportByteLength = caps.InputReportByteLength;
OutputReportByteLength = caps.OutputReportByteLength;
@ -126,12 +131,14 @@ namespace EonaCat.HID
ProductName = GetStringDescriptor(HidD_GetProductString);
SerialNumber = GetStringDescriptor(HidD_GetSerialNumberString);
IsReadingSupport = (access == FileAccess.Read || access == FileAccess.ReadWrite);
IsWritingSupport = (access == FileAccess.Write || access == FileAccess.ReadWrite);
HidDeviceAttributes attr = GetDeviceAttributes();
VendorId = attr.VendorID;
ProductId = attr.ProductID;
}
private SafeFileHandle TryOpenDevice(int access)
{
var handle = CreateFile(_devicePath,
@ -145,34 +152,6 @@ namespace EonaCat.HID
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>
@ -247,7 +226,7 @@ namespace EonaCat.HID
Close();
}
public async Task WriteOutputReportAsync(byte[] data)
public async Task WriteOutputReportAsync(HidReport report)
{
if (!_isOpen)
{
@ -255,15 +234,26 @@ namespace EonaCat.HID
return;
}
if (data == null || data.Length == 0)
if (report == null)
{
OnError?.Invoke(this, new HidErrorEventArgs(this, new ArgumentNullException(nameof(data), "Data cannot be null or empty")));
OnError?.Invoke(this, new HidErrorEventArgs(this, new ArgumentNullException(nameof(report))));
return;
}
if (report.Data == null || report.Data.Length == 0)
{
OnError?.Invoke(this, new HidErrorEventArgs(this, new ArgumentException("Data cannot be null or empty", nameof(report))));
return;
}
try
{
await _deviceStream.WriteAsync(data, 0, data.Length);
// Combine reportId and data into one buffer for sending
var buffer = new byte[1 + report.Data.Length];
buffer[0] = report.ReportId;
Array.Copy(report.Data, 0, buffer, 1, report.Data.Length);
await _deviceStream.WriteAsync(buffer, 0, buffer.Length);
await _deviceStream.FlushAsync();
}
catch (Exception ex)
@ -273,12 +263,12 @@ namespace EonaCat.HID
}
}
public async Task<byte[]> ReadInputReportAsync()
public async Task<HidReport> ReadInputReportAsync()
{
if (!_isOpen)
{
OnError?.Invoke(this, new HidErrorEventArgs(this, new InvalidOperationException("Device not open")));
return Array.Empty<byte>();
return new HidReport(0, Array.Empty<byte>());
}
return await Task.Run(async () =>
@ -291,10 +281,13 @@ namespace EonaCat.HID
if (read == 0)
{
OnError?.Invoke(this, new HidErrorEventArgs(this, new IOException("No data read from device")));
return Array.Empty<byte>();
return new HidReport(0, Array.Empty<byte>());
}
return buffer.Take(read).ToArray();
byte reportId = buffer[0];
byte[] data = buffer.Skip(1).Take(read - 1).ToArray();
return new HidReport(reportId, data);
}
catch (Exception ex)
{
@ -304,7 +297,8 @@ namespace EonaCat.HID
});
}
public async Task SendFeatureReportAsync(byte[] data)
public async Task SendFeatureReportAsync(HidReport report)
{
if (!_isOpen)
{
@ -312,10 +306,13 @@ namespace EonaCat.HID
return;
}
if (data == null || data.Length == 0)
{
throw new ArgumentNullException(nameof(data));
}
if (report == null)
throw new ArgumentNullException(nameof(report));
// Prepare buffer with ReportId + Data
var data = new byte[1 + report.Data.Length];
data[0] = report.ReportId;
Array.Copy(report.Data, 0, data, 1, report.Data.Length);
await Task.Run(() =>
{
@ -330,12 +327,13 @@ namespace EonaCat.HID
});
}
public async Task<byte[]> GetFeatureReportAsync(byte reportId)
public async Task<HidReport> GetFeatureReportAsync(byte reportId)
{
if (!_isOpen)
{
OnError?.Invoke(this, new HidErrorEventArgs(this, new InvalidOperationException("Device not open")));
return Array.Empty<byte>();
return new HidReport(0, Array.Empty<byte>());
}
return await Task.Run(() =>
@ -349,13 +347,15 @@ namespace EonaCat.HID
var err = Marshal.GetLastWin32Error();
var ex = new Win32Exception(err, "HidD_GetFeature failed");
OnError?.Invoke(this, new HidErrorEventArgs(this, ex));
return Array.Empty<byte>();
return new HidReport(0, Array.Empty<byte>());
}
return buffer;
byte[] data = buffer.Skip(1).ToArray();
return new HidReport(reportId, data);
});
}
/// <summary>
/// Begin async reading loop raising OnDataReceived events on data input
/// </summary>
@ -379,15 +379,17 @@ namespace EonaCat.HID
try
{
while (!_listeningCts.Token.IsCancellationRequested)
var token = _listeningCts.Token;
while (!token.IsCancellationRequested)
{
try
{
byte[] data = await ReadInputReportAsync(_listeningCts.Token);
var inputReport = await ReadInputReportAsync(token);
if (data != null && data.Length > 0)
if (inputReport?.Data?.Length > 0)
{
OnDataReceived?.Invoke(this, new HidDataReceivedEventArgs(this, data));
OnDataReceived?.Invoke(this, new HidDataReceivedEventArgs(this, inputReport));
}
}
catch (OperationCanceledException)
@ -397,15 +399,15 @@ namespace EonaCat.HID
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
break;
}
catch (Exception ex)
{
// Handle exceptions during reading
if (_listeningCts.IsCancellationRequested)
if (token.IsCancellationRequested)
{
break; // Exit if cancellation was requested
break;
}
OnError?.Invoke(this, new HidErrorEventArgs(this, ex));
}
}
@ -417,24 +419,30 @@ namespace EonaCat.HID
}
}
private Task<byte[]> ReadInputReportAsync(CancellationToken cancellationToken)
private Task<HidReport> ReadInputReportAsync(CancellationToken cancellationToken)
{
var tcs = new TaskCompletionSource<byte[]>(TaskCreationOptions.RunContinuationsAsynchronously);
var tcs = new TaskCompletionSource<HidReport>(TaskCreationOptions.RunContinuationsAsynchronously);
// Use overlapped IO pattern from FileStream
var buffer = new byte[InputReportByteLength];
// Start async read
_deviceStream.BeginRead(buffer, 0, buffer.Length, ar =>
{
try
{
int bytesRead = _deviceStream.EndRead(ar);
if (bytesRead == 0)
{
tcs.SetResult(Array.Empty<byte>());
// No data read, reportId 0 and empty data
tcs.SetResult(new HidReport(0, Array.Empty<byte>()));
}
else
{
tcs.SetResult(buffer.Take(bytesRead).ToArray());
// First byte is reportId, rest is data
byte reportId = buffer[0];
byte[] data = bytesRead > 1 ? buffer.Skip(1).Take(bytesRead - 1).ToArray() : Array.Empty<byte>();
tcs.SetResult(new HidReport(reportId, data));
}
}
catch (Exception ex)

View File

@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Threading.Tasks;
using System.Threading;
using EonaCat.HID.EventArguments;
using EonaCat.HID.Models;
namespace EonaCat.HID
{
@ -23,6 +24,10 @@ namespace EonaCat.HID
int InputReportByteLength { get; }
int OutputReportByteLength { get; }
int FeatureReportByteLength { get; }
bool IsConnected { get;}
bool IsReadingSupport { get; }
bool IsWritingSupport { get; }
IDictionary<string, object> Capabilities { get; }
@ -40,25 +45,25 @@ namespace EonaCat.HID
/// Writes an output report to the device
/// </summary>
/// <param name="data">Complete report data including ReportID</param>
Task WriteOutputReportAsync(byte[] data);
Task WriteOutputReportAsync(HidReport report);
/// <summary>
/// Reads an input report
/// </summary>
/// <returns>Input report data</returns>
Task<byte[]> ReadInputReportAsync();
Task<HidReport> ReadInputReportAsync();
/// <summary>
/// Sends a feature report
/// </summary>
/// <param name="data">Complete feature report data including ReportID</param>
Task SendFeatureReportAsync(byte[] data);
Task SendFeatureReportAsync(HidReport report);
/// <summary>
/// Gets a feature report
/// </summary>
/// <returns>Feature report data</returns>
Task<byte[]> GetFeatureReportAsync(byte reportId);
Task<HidReport> GetFeatureReportAsync(byte reportId);
/// <summary>
/// Asynchronously read input reports and raise OnDataReceived event

View File

@ -14,6 +14,8 @@ namespace EonaCat.HID.Managers.Mac
private IntPtr _hidManager;
private readonly IOHIDDeviceCallback _deviceAddedCallback;
private readonly IOHIDDeviceCallback _deviceRemovedCallback;
private readonly Dictionary<string, IHid> _knownDevices = new();
public event EventHandler<HidEventArgs> OnDeviceInserted;
public event EventHandler<HidEventArgs> OnDeviceRemoved;
@ -78,27 +80,57 @@ namespace EonaCat.HID.Managers.Mac
return devices;
}
private void DeviceInsertedInternal(IHid device)
{
if (!_knownDevices.ContainsKey(device.DevicePath))
{
_knownDevices[device.DevicePath] = device;
OnDeviceInserted?.Invoke(this, new HidEventArgs(device));
}
}
private void DeviceRemovedInternal(IHid device)
{
if (_knownDevices.ContainsKey(device.DevicePath))
{
device = _knownDevices[device.DevicePath];
_knownDevices.Remove(device.DevicePath);
OnDeviceRemoved?.Invoke(this, new HidEventArgs(device));
}
}
private void DeviceAddedCallback(IntPtr context, IntPtr result, IntPtr sender, IntPtr devicePtr)
{
if (devicePtr == IntPtr.Zero)
{
return; // Ignore null devices
}
return;
// Create the device and invoke the event
try
{
var device = new HidMac(devicePtr);
device.Setup();
OnDeviceInserted?.Invoke(this, new HidEventArgs(device));
DeviceInsertedInternal(device);
}
catch (Exception ex)
{
Console.Error.WriteLine($"DeviceAddedCallback error: {ex.Message}");
}
}
private void DeviceRemovedCallback(IntPtr context, IntPtr result, IntPtr sender, IntPtr devicePtr)
{
if (devicePtr == IntPtr.Zero)
{
return; // Ignore null devices
}
return;
OnDeviceRemoved?.Invoke(this, new HidEventArgs(null));
try
{
var device = new HidMac(devicePtr);
DeviceRemovedInternal(device);
}
catch (Exception ex)
{
Console.Error.WriteLine($"DeviceRemovedCallback error: {ex.Message}");
}
}
public void Dispose()

View File

@ -27,6 +27,7 @@ namespace EonaCat.HID.Managers.Windows
private IntPtr _deviceNotificationHandle;
private WndProc _windowProcDelegate;
private IntPtr _messageWindowHandle;
private readonly Dictionary<string, IHid> _knownDevices = new();
public event EventHandler<HidEventArgs> OnDeviceInserted;
public event EventHandler<HidEventArgs> OnDeviceRemoved;
@ -104,29 +105,76 @@ namespace EonaCat.HID.Managers.Windows
{
if (msg == WM_DEVICECHANGE)
{
var eventType = wParam.ToInt32();
int eventType = wParam.ToInt32();
if (eventType == DBT_DEVICEARRIVAL || eventType == DBT_DEVICEREMOVECOMPLETE)
{
var hdr = Marshal.PtrToStructure<DEV_BROADCAST_HDR>(lParam);
if (hdr.dbch_devicetype == DBT_DEVTYP_DEVICEINTERFACE)
{
var devInterface = Marshal.PtrToStructure<DEV_BROADCAST_DEVICEINTERFACE>(lParam);
// Calculate pointer to string
IntPtr stringPtr = IntPtr.Add(lParam, Marshal.SizeOf<DEV_BROADCAST_DEVICEINTERFACE>());
// Read null-terminated string from unmanaged memory
string devicePath = Marshal.PtrToStringUni(stringPtr);
if (!string.IsNullOrEmpty(devicePath))
{
try
{
if (eventType == DBT_DEVICEARRIVAL)
{
// Device inserted
var devBroadcast = Marshal.PtrToStructure<DEV_BROADCAST_HDR>(lParam);
if (devBroadcast.dbch_devicetype == DBT_DEVTYP_DEVICEINTERFACE)
using (var testHandle = CreateFile(devicePath, 0,
FileShare.ReadWrite, IntPtr.Zero, FileMode.Open, 0, IntPtr.Zero))
{
// Treat all HID devices or filter if needed
OnDeviceInserted?.Invoke(this, new HidEventArgs(null));
if (testHandle.IsInvalid)
return DefWindowProc(hwnd, msg, wParam, lParam);
}
var device = new HidWindows(devicePath);
device.Setup();
DeviceInsertedInternal(device);
}
else if (eventType == DBT_DEVICEREMOVECOMPLETE)
{
var devBroadcast = Marshal.PtrToStructure<DEV_BROADCAST_HDR>(lParam);
if (devBroadcast.dbch_devicetype == DBT_DEVTYP_DEVICEINTERFACE)
var device = new HidWindows(devicePath);
DeviceRemovedInternal(device);
}
return IntPtr.Zero;
}
catch (Exception ex)
{
OnDeviceRemoved?.Invoke(this, new HidEventArgs(null));
System.Diagnostics.Debug.WriteLine($"[WindowProc] HID device change error: {ex.Message}");
}
}
}
}
}
return DefWindowProc(hwnd, msg, wParam, lParam);
}
private void DeviceInsertedInternal(IHid device)
{
if (!_knownDevices.ContainsKey(device.DevicePath))
{
_knownDevices[device.DevicePath] = device;
OnDeviceInserted?.Invoke(this, new HidEventArgs(device));
}
}
private void DeviceRemovedInternal(IHid device)
{
if (_knownDevices.ContainsKey(device.DevicePath))
{
device = _knownDevices[device.DevicePath];
_knownDevices.Remove(device.DevicePath);
OnDeviceRemoved?.Invoke(this, new HidEventArgs(device));
}
}
public IEnumerable<IHid> Enumerate(ushort? vendorId = null, ushort? productId = null)
{
var list = new List<IHid>();
@ -293,8 +341,6 @@ namespace EonaCat.HID.Managers.Windows
}
}
#region Native Methods and structs
internal static class NativeMethods
{
public const int ERROR_INSUFFICIENT_BUFFER = 122;
@ -308,6 +354,7 @@ namespace EonaCat.HID.Managers.Windows
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 HIDP_STATUS_SUCCESS = 0x110000;
public const int WM_DEVICECHANGE = 0x0219;
public const int DBT_DEVICEARRIVAL = 0x8000;
@ -347,11 +394,9 @@ namespace EonaCat.HID.Managers.Windows
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
{
@ -516,5 +561,4 @@ namespace EonaCat.HID.Managers.Windows
public ushort ProductID;
public ushort VersionNumber;
}
#endregion
}

View File

@ -0,0 +1,16 @@
using System;
namespace EonaCat.HID.Models
{
public class HidReport
{
public byte ReportId { get; }
public byte[] Data { get; }
public HidReport(byte reportId, byte[] data)
{
ReportId = reportId;
Data = data ?? Array.Empty<byte>();
}
}
}