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.EventArguments;
using EonaCat.HID.Helpers; using EonaCat.HID.Helpers;
using EonaCat.HID.Models;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
@ -106,6 +107,8 @@ namespace EonaCat.HID.Analyzer
deviceName = device.ProductName; deviceName = device.ProductName;
deviceManufacturer = device.Manufacturer; deviceManufacturer = device.Manufacturer;
deviceSerialNumber = device.SerialNumber; deviceSerialNumber = device.SerialNumber;
var isWritingSupported = device.IsWritingSupport;
var isReadingSupported = device.IsReadingSupport;
var row = new string[] var row = new string[]
{ {
@ -113,6 +116,8 @@ namespace EonaCat.HID.Analyzer
deviceName, deviceName,
deviceManufacturer, deviceManufacturer,
deviceSerialNumber, deviceSerialNumber,
isReadingSupported.ToString(),
isWritingSupported.ToString(),
device.InputReportByteLength.ToString(), device.InputReportByteLength.ToString(),
device.OutputReportByteLength.ToString(), device.OutputReportByteLength.ToString(),
device.FeatureReportByteLength.ToString(), device.FeatureReportByteLength.ToString(),
@ -256,7 +261,7 @@ namespace EonaCat.HID.Analyzer
{ {
try 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); AppendEventLog(str, Color.Blue);
} }
catch (Exception ex) catch (Exception ex)
@ -269,7 +274,7 @@ namespace EonaCat.HID.Analyzer
{ {
try 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); AppendEventLog(str);
} }
catch (Exception ex) catch (Exception ex)
@ -282,7 +287,7 @@ namespace EonaCat.HID.Analyzer
{ {
try 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); AppendEventLog(str, Color.Orange);
} }
catch (Exception ex) catch (Exception ex)
@ -332,14 +337,14 @@ namespace EonaCat.HID.Analyzer
throw new Exception("This device has no Input Report support!"); throw new Exception("This device has no Input Report support!");
} }
var buffer = await _device.ReadInputReportAsync(); var report = await _device.ReadInputReportAsync();
if (buffer.Length < 2) 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; 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); AppendEventLog(str, Color.Blue);
} }
catch (Exception ex) catch (Exception ex)
@ -352,27 +357,33 @@ namespace EonaCat.HID.Analyzer
{ {
try 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 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 if (_device.OutputReportByteLength <= 0)
byte[] outputReport = new byte[buf.Length + 1]; {
outputReport[0] = hidReportId; throw new Exception("This device has no Output Report support!");
Array.Copy(buf, 0, outputReport, 1, buf.Length); }
if (dataBuffer.Length > _device.OutputReportByteLength - 1)
{
throw new Exception("Output Report Length Exceeds allowed size.");
}
try var outputReport = new HidReport(hidReportId, dataBuffer);
{
await _device.WriteOutputReportAsync(outputReport); 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) 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!"); throw new Exception("This device has no Feature Report support!");
} }
var buffer = await _device.GetFeatureReportAsync(hidReportId); HidReport report = await _device.GetFeatureReportAsync(hidReportId);
var str = string.Format("Rx Feature Report [{0}] <-- {1}", buffer.Length, ByteHelper.ByteArrayToHexString(buffer)); 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); AppendEventLog(str, Color.Blue);
} }
catch (Exception ex) catch (Exception ex)
@ -397,23 +415,33 @@ namespace EonaCat.HID.Analyzer
} }
} }
private async void ButtonWriteFeature_Click(object sender, EventArgs e) private async void ButtonWriteFeature_Click(object sender, EventArgs e)
{ {
try try
{ {
var hidReportId = byte.Parse(comboBoxReportId.Text); if (!byte.TryParse(comboBoxReportId.Text, out var hidReportId))
var buf = ByteHelper.HexStringToByteArray(textBoxWriteData.Text);
var len = _device.FeatureReportByteLength;
if (buf.Length > len)
{ {
throw new Exception("Write Feature Report Length Exceed"); throw new FormatException("Invalid Report ID format.");
} }
Array.Resize(ref buf, len); var data = ByteHelper.HexStringToByteArray(textBoxWriteData.Text);
await _device.SendFeatureReportAsync(buf);
var str = string.Format("Tx Feature Report [{0}] --> {1}", buf.Length, ByteHelper.ByteArrayToHexString(buf)); int maxLen = _device.FeatureReportByteLength - 1;
AppendEventLog(str, Color.DarkGreen); 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) 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"> <metadata name="Column11.UserAddedColumn" type="System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">
<value>True</value> <value>True</value>
</metadata> </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"> <metadata name="Column7.UserAddedColumn" type="System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">
<value>True</value> <value>True</value>
</metadata> </metadata>
@ -151,59 +157,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
YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAIqSURBVDhPfZJNaBNBFMcXSknBKKkg9FKL7SKSQ1tpMSEm YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAIrSURBVDhPfZJPaBNBFMYXSknBKKkg9NIW20Uhh7bSYkJM
mZ0NhFzirXdPftyEHjxueohsNiDUj0IC7UW95CIq5NDLpkQo7U401hatBkKys5kG0/Qe0JW3ycZsWvuH MjsbCLnEW++e/HMTevC4KUTZbEBo1UIC7UW95CIqBPSyKZFCm4nG2qLVYEh2NtNgmt4DuvI22ZhNaz/4
P8u89+bHe2+W4/4jdWrKo3q97r1gcBw8nD9XOY4b2Zy+flK4xsfL/sCDz7duf4N4XRDQcK0lebcZSGns WOa9Nz/ee7Mc9x+pk5Mu1e127vr9o+DB/JnKcNzQu6krx7nLfLTk9d39dP3GV4jXBAEN1pqSdxq+RIE9
MVgmxlWIqTMzfJ7nXT9iMdfXxdBkHSGeCmKHIvyaLCyMOgCSWh1TdhqGQpiZ1pjgSA6IIjRPBZFRAb9y AMtEn4CYOj3NZ3ne8T0ScXxZCIzXEOKpILYpwi/I/PywDSCplRFlu64rhBnJAhNsyT5RhOaoIDIq4Oe2
JBTClpLbRkfZYUUAtH5m19oHCbNdyXat3T2s7T209tCDdCjGd/qAFGG7Mmk863bSnDiuZFQHoPzIPC4v hELYYnxLbyvbLA+A5o/0Wms/ZrTK6Y4Ltw6qu/fMPXQhbYrxzR4gQdiOTOqrnU4aY0fllGoDlO4bR6Wl
b9r1OsIvKBK3rEPyU+NKcrv+RyZHfrugDyjd7/ogYULsHwD5qwj9rkejl7m0xnwwu7TfdDsAvYs2ZBDQ 91a9hvBTisRN8/DwY/1SfKv6RyaHXqugByje6Xg/ZkDsHwB5fyL0uxYOX+SSBeaB2aW9htMG6F60IP2A
isUuUUE0aThyk1NKR7MASH38dfEUYAAyCKgi5AGAjvEcly6zCzACdHImYH/l1AgU4wCMULN/MIWwgkIa ZiRygQqiQYOha5xSPJwBQOLDr/MnAH2QfkAFIRcANIxnuWSJnYMRoJNTAXvLJ0agGPtghKr1gymE5RRS
zx0Ae4GHT832l2ULUEVooorQmC6IazQsFux6Ll1i8e4z0nk4tyrZl31AzxDTUUTQBVy0njEsxvsAkKKx f2IDWAs8eGy0Pi+ZgApCYxWERjRBXKNBMWfVc8kii3aekc7BuVlOP+sBuoaYhkKCJuC8+YxBMdoDgJQC
TJJQZkPOEgCs5YVCFDpxJDPEHFVIYx06eVJq8osbW5Ox1byLX827vNI7Hmpqkcg0RVgGGxhHHQBbMjGC S8UJZRbkNAHAXF4gQKETWzJFjGGF1Nehk0fFBr+wsTkeWck6+JWswy295qGmGgpNUYRlsI5x2AawJBPd
8PVtFL771ov3Zlfex28kPpws5XIjw7XnKvimOA72Sjn3nPTWM5y39RdV/noYsphYLAAAAABJRU5ErkJg D1/PRu6bZz1/e2b5TfRq7O3xYiYzNFh7pvwv86Ngt5RxzkqvXIN5S38BNmd6B/1xTKkAAAAASUVORK5C
gg== YII=
</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
YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAIoSURBVDhPY5BuvNMjXXPjhWTJubsiuUfeCOccfS2Re8qe YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAIoSURBVDhPY5BuvNMjXXPjhWTJubsiuYffCOccfS2Re8qe
AQrEiy8oihWcuSGeeXw2TAwFSNXfOSBdde2/ZNml/+J5J8FYJPtEAUxesuxSM0hOMOPwd1SdUEDIAImy AQrEiy8oihWcuSGeeXw2TAwFSNXfOSBdde2/ZNml/+J5J8FYJPtEAUxesuxSM0hOIOPwd1SdUEDIAImy
yyUgObHkw59RdUIBIQNE66/wSKVvOSWaeyIWVScUEDIABIycveNNXH18TZy9lQwdveQZ6uuZ4JIyjXf3 yyUgObGkw59RdUIBIQNE66/wSKVvOSWaeyIWVScUEDIABIycveNNXH18TZy9lQwdveQZ6uuZ4JIyjXf3
S1ejGiCeczwP2QAQMHL1dTJx9k4DYWNn7ynGxsasYAmpxjvOUrU3lsrl7Fgunnt0rVjuyVXiGUfFQHLz 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+ NGD5o+/vM6eu+u+TmP3fLjwZBRs4+fxPWnXqf+yJH/vhBhg5+/oYuXkbIxuQ3NT/3ykw4n+yte//+UZ+
/5cZ+P33cvD5r2Vh9z+kd9n/+BPfD8ANMPPw4DN29u4ODQ1lRjegw8zv/3elgP9flQP+x9r6YjcABEwc /5cZ+P33cvD5r2Vh9z+kd9n/+BPfD8ANMPPw4DN29u4ODQ1lRjegw8zv/3elgP9flQP+x9r6YjcABEwc
/dSNXXzaTFy8q/p3X/wEM8DUxef/IkO//1Xmfv9NXHC4AB2AXJDSNu2/a3A0WBMy1jSz/R81cxthAxou /dSNXXzaTFy8q/p3XfgEM8DUxef/IkO//1Xmfv9NXHC4AB2AXJDSNu2/a3A0WBMy1jSz/R81cxthAxou
/fhftvv2//KDjxH4wKP/hdtu/C+98BO/AUvufn/bePXXf4tJ9/7Hbnn733HWw/+hq1/+9178DIxLzv/8 /fhftvv2//KDjxH4wKP/hdtu/C+98BO/AYvvfH/bePXXf4tJ9/7Hbnn733HWw/+hq1/+9178DIxLzv/8
H3v8xx50fXAw/87Pvkm3fz1puvLrbiMarr/8617y6R9Pkk//8AIABXiIYedL+BwAAAAASUVORK5CYII= H3v8xx50fXAw/87Pvkm3fz1puvLrbiMarr/8617y6R9Pkk//8AIA8QuIV6in+C4AAAAASUVORK5CYII=
</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
YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAJPSURBVDhPdZH/SxNhHMdHCpkFRT/1P9Qv/VS4i4FZlET+ YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAJOSURBVDhPdZHtS1NRHMdHCpkFRa/6H+pNbyLcjYFZVES+
ZDaiLJj7opk259Ya2zPnTm1uoURBRLToNwuCRcvcnfuhLwQhkVOsxTzv0PYEtaDadLft+cQtdttu6w0f Mh1RFukeNNPm3DTZTs57fdoWgyiIiBa9syBYtMzd6170QBASNsVa03kv2k5QC6qt3T2cX9wLu9vu1hd+
uOdzn9free4elapOqHmsp1isVfbrRo/uNhttgUmD/aa61KPYrxvUHP5UPfmfGGyBp6ZrAdBZfL97bX5K cM73/L6f86TR1BC1gA0Uh/Vqv6YM6G6jye51G0duaosexX3doubxp8rO/8ho9z41D3vhsnXmd4/dQ0me
6qkZ7KMY7FLO1o3J5g9KgkrJ4bfCjnuzgb33px0pv8+19TpkOa7k5CCEGo22wOOSRNuPxJ6hkSu3p5yZ lsUzFIud6t6aMts9fglQDjn8Vthxb867975vNOmedmZeB6zH1TlFCKF6k937uAjp6EPZ7sGxq7d9jnTR
Uo+mERE/GORPrIkk0Vsnn5zv98IZgwt0lokiKNWwawJWn2tha8X8nQj0ASUrpxuhJm0fWtcNVcNfHrZB o2lEsh+MyhWrJEEMNveT8300nDU6ocs6JQelGnJOwfpzPWRWLd+JwBxQZxV1ItSg70WbXYOV4S8PWyC/
fnkQQKBBTLj/EH60S8kWsxTt3XVryiUfe9g5DqvPuiC3eBkIN1IUlMsbJLxnf5VgOmBPV+6cCHVCOtQK MgAgMCDG0B/Cj7ers7KWwz27bvmcyrGHHJOw/qwdch+vAImPyYBS0X7Cu/ZXAHye4VT5zmuBNkgFmoHE
hHMr4HIR3ntBFpidN8rHDh6F3NIAFOIOKKxYZUBa52NXKwRj5dsJPbJGrGi8+MNyH/ug8NlesyPwXsi+ b6jCpSI8fUEBWBzTpWP7j0JuuR8K0VEorNqUgDTPR66VASZKvxN4ZAvZ0KT8YLmlXih8HqnaEXgaxFfn
OgebkQ5Iz55KwQZqlgVSfrzRP9hcNJMaUKAhv2yB7LtLxWcxQafr3ogm+nMP4ce6xQRdkOE1DxDO8w98 4G+oFVJzp5OwhRoVgKQfbwwP0ksWUhUUGMivWEF8d0keizEmVfNHdOGfewg/0SnGmIIS3nABibvkcfZ9
3wOZ8EnIxwZMx14md2qi0CjDbZHUbopNrmui3/YB771YkpAEgsyLdijEr4Mo0FkSd1ileTWTvENFsFkW N6SDJyEf6Tcfe5nYqQtDvRJuCSV3U1xiUxf+tg94+mIRQtYQpF+cgkL0OogCI5LoqE3q17KJO1QIWxQA
UEzyLMViaGGwUVqTtdHWvEAviIJbzHLoV2budIxwnoPSuxPh+PZDEZxqYfGCLOicgQaKxWFNlGuSmyqV xSY6KA5DE4tN0pxsjDfnBWZRFFA2E0e/0vNnIiTuOiitnQhGtx8K4WQThxcVQNss1FEcDurC8QbF1Gg0
CgBtA5hpqOxJUTN48Mg87vgL5YHJNRvDXwIAAAAASUVORK5CYII= AGgbwGxduSdJy+KBIwu49R+1rskaRvlY/wAAAABJRU5ErkJggg==
</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
YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAEXSURBVDhPY2CgBphQKSYe5qr+1dTU9D8xOMxV7eu0YnEx YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAETSURBVDhPY2CgBphQKSYe5qL21dTU9D8xGKR2WrG4GNyA
uAETKxXsPZwM/3/dxfn//0F2vPjrLq7/ILVTyhXsUFyRH6N+tSpVFUMDMv53gP1/RbLq/7xYjUsomkFg iZUK9h5Ohv+/7uL8//8gO178dRfXf5DaKeUKdiiuyI9Rv1qVqoqhARn/O8D+vyJZ9X9erMYlFM0gMLNe
Zr0kV4S/zqcVrdIYGmF4SZPM/0BftS8Tc4X40PWDwcQyeQsHe8O/5xcIYmi+tEjwv7294V8Mp6ODrjyF kivCX+fTilZpDI0wvKRJ5n+gr+qXiblCfOj6wWBimbyFvb3h3/MLBDE0X1ok+B8kh+F0dNCVp1Dm5Wzw
Mi9ng/+vN3PDNb/dyv0fJNaeo9CGrh4rKElSOVwQpw43IC9G439JvMphdHU4wZR6UR5zc1O4ASA2KIzQ //Vmbrjmt1u5/4PE2nMU2tDVYwUlSSqHC+LU4QbkxWj8L4lXOYyuDieYUi/KY25uCjcAxAaFEbo6nKC+
1eEE9fUMTKD4hhkAYqOrwQsGlwGfdnD9t7QwId0AkKY9k8X/+7kZ/C9LVDmCroYgqEhU3R/jo/WhM1+h noEJFN8wA0BsdDV4weAy4NMOrv+WFiakGwDStGey+H8/N4P/ZYkqR9DVEAQViar7Y3y0PnTmK5Sjy1EV
HF2OqgAAizrlNjwLhaIAAAAASUVORK5CYII= AAB6heUx9GPlMgAAAABJRU5ErkJggg==
</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">

View File

@ -31,15 +31,6 @@
System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(MainForm)); System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(MainForm));
this.textBoxWriteData = new System.Windows.Forms.TextBox(); this.textBoxWriteData = new System.Windows.Forms.TextBox();
this.dataGridView1 = new System.Windows.Forms.DataGridView(); 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.tableLayoutPanel1 = new System.Windows.Forms.TableLayoutPanel();
this.toolStrip1 = new System.Windows.Forms.ToolStrip(); this.toolStrip1 = new System.Windows.Forms.ToolStrip();
this.toolStripSeparator = new System.Windows.Forms.ToolStripSeparator(); this.toolStripSeparator = new System.Windows.Forms.ToolStripSeparator();
@ -67,6 +58,17 @@
this.rtbEventLog = new System.Windows.Forms.RichTextBox(); this.rtbEventLog = new System.Windows.Forms.RichTextBox();
this.statusStrip1 = new System.Windows.Forms.StatusStrip(); this.statusStrip1 = new System.Windows.Forms.StatusStrip();
this.toolStripStatusLabel1 = new System.Windows.Forms.ToolStripStatusLabel(); 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(); ((System.ComponentModel.ISupportInitialize)(this.dataGridView1)).BeginInit();
this.tableLayoutPanel1.SuspendLayout(); this.tableLayoutPanel1.SuspendLayout();
this.toolStrip1.SuspendLayout(); this.toolStrip1.SuspendLayout();
@ -96,6 +98,8 @@
this.Column9, this.Column9,
this.Column10, this.Column10,
this.Column11, this.Column11,
this.Column6,
this.Column3,
this.Column7, this.Column7,
this.Column4, this.Column4,
this.Column5, this.Column5,
@ -112,69 +116,6 @@
this.dataGridView1.SelectionChanged += new System.EventHandler(this.DataGridView1_SelectionChanged); this.dataGridView1.SelectionChanged += new System.EventHandler(this.DataGridView1_SelectionChanged);
this.dataGridView1.DoubleClick += new System.EventHandler(this.dataGridView1_DoubleClick); 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 // tableLayoutPanel1
// //
this.tableLayoutPanel1.ColumnCount = 1; this.tableLayoutPanel1.ColumnCount = 1;
@ -473,6 +414,81 @@
this.toolStripStatusLabel1.Size = new System.Drawing.Size(118, 17); this.toolStripStatusLabel1.Size = new System.Drawing.Size(118, 17);
this.toolStripStatusLabel1.Text = "toolStripStatusLabel1"; 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 // MainForm
// //
this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F); this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F);
@ -537,6 +553,8 @@
private System.Windows.Forms.DataGridViewTextBoxColumn Column9; private System.Windows.Forms.DataGridViewTextBoxColumn Column9;
private System.Windows.Forms.DataGridViewTextBoxColumn Column10; private System.Windows.Forms.DataGridViewTextBoxColumn Column10;
private System.Windows.Forms.DataGridViewTextBoxColumn Column11; 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 Column7;
private System.Windows.Forms.DataGridViewTextBoxColumn Column4; private System.Windows.Forms.DataGridViewTextBoxColumn Column4;
private System.Windows.Forms.DataGridViewTextBoxColumn Column5; private System.Windows.Forms.DataGridViewTextBoxColumn Column5;

View File

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

View File

@ -1,4 +1,5 @@
using System; using EonaCat.HID.Models;
using System;
namespace EonaCat.HID.EventArguments namespace EonaCat.HID.EventArguments
{ {
@ -11,12 +12,12 @@ namespace EonaCat.HID.EventArguments
public class HidDataReceivedEventArgs : EventArgs public class HidDataReceivedEventArgs : EventArgs
{ {
public IHid Device { get; } 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; Device = device;
Data = data; Report = report;
} }
} }
} }

View File

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

View File

@ -1,6 +1,8 @@
using EonaCat.HID.EventArguments; using EonaCat.HID.EventArguments;
using EonaCat.HID.Managers; using EonaCat.HID.Managers;
using EonaCat.HID.Managers.Linux; using EonaCat.HID.Managers.Linux;
using EonaCat.HID.Models;
using Microsoft.Win32.SafeHandles;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
@ -17,6 +19,7 @@ namespace EonaCat.HID
{ {
private readonly string _devicePath; private readonly string _devicePath;
private FileStream _stream; private FileStream _stream;
private bool _isOpen;
private readonly object _lock = new object(); private readonly object _lock = new object();
private CancellationTokenSource _cts; private CancellationTokenSource _cts;
@ -29,6 +32,11 @@ namespace EonaCat.HID
public int InputReportByteLength { get; private set; } public int InputReportByteLength { get; private set; }
public int OutputReportByteLength { get; private set; } public int OutputReportByteLength { get; private set; }
public int FeatureReportByteLength { get; private set; } public int FeatureReportByteLength { get; private set; }
public bool 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 IDictionary<string, object> Capabilities { get; private set; } = new Dictionary<string, object>();
public event EventHandler<HidDataReceivedEventArgs> OnDataReceived; public event EventHandler<HidDataReceivedEventArgs> OnDataReceived;
@ -51,21 +59,33 @@ namespace EonaCat.HID
{ {
lock (_lock) lock (_lock)
{ {
if (_stream != null) if (_stream != null || _isOpen)
{
return; return;
}
// Open device for read/write // Open HID device in non-blocking read/write mode
var fd = NativeMethods.open(_devicePath, NativeMethods.O_RDWR | NativeMethods.O_NONBLOCK); int fd = NativeMethods.open(_devicePath, NativeMethods.O_RDWR | NativeMethods.O_NONBLOCK);
if (fd < 0) 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; return;
} }
var safeHandle = new Microsoft.Win32.SafeHandles.SafeFileHandle(new IntPtr(fd), ownsHandle: true); var safeHandle = new Microsoft.Win32.SafeHandles.SafeFileHandle(new IntPtr(fd), ownsHandle: true);
_stream = new FileStream(safeHandle, FileAccess.ReadWrite, bufferSize: 64, isAsync: false);
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; 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; return;
} }
if (_stream == null) 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; return;
} }
var data = report.Data;
await Task.Run(() => await Task.Run(() =>
{ {
lock (_lock) lock (_lock)
@ -188,17 +212,19 @@ namespace EonaCat.HID
catch (Exception ex) catch (Exception ex)
{ {
OnError?.Invoke(this, new HidErrorEventArgs(this, ex)); OnError?.Invoke(this, new HidErrorEventArgs(this, ex));
throw;
} }
} }
}); });
} }
public async Task<byte[]> ReadInputReportAsync() public async Task<HidReport> ReadInputReportAsync()
{ {
if (_stream == null) if (_stream == null)
{ {
OnError?.Invoke(this, new HidErrorEventArgs(this, new InvalidOperationException("Device not open"))); var ex = new InvalidOperationException("Device not open");
return Array.Empty<byte>(); OnError?.Invoke(this, new HidErrorEventArgs(this, ex));
return null;
} }
return await Task.Run(() => return await Task.Run(() =>
@ -215,91 +241,119 @@ namespace EonaCat.HID
catch (Exception ex) catch (Exception ex)
{ {
OnError?.Invoke(this, new HidErrorEventArgs(this, ex)); OnError?.Invoke(this, new HidErrorEventArgs(this, ex));
return null;
} }
} }
if (bytesRead <= 0) if (bytesRead <= 0)
{ {
return Array.Empty<byte>(); return null;
} }
byte[] result = new byte[bytesRead]; // First byte is report ID, rest is data
Array.Copy(buffer, result, bytesRead); byte reportId = buffer[0];
return result; 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.");
int fd = NativeMethods.open(_devicePath, NativeMethods.O_RDWR); public async Task SendFeatureReportAsync(HidReport report)
if (fd < 0) {
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)
{ {
OnError?.Invoke(this, new HidErrorEventArgs(this, new IOException($"Failed to open device: {_devicePath}"))); Array.Copy(report.Data, 0, buffer, 1, report.Data.Length);
return;
} }
try await Task.Run(() =>
{ {
int size = data.Length; int fd = NativeMethods.open(_devicePath, NativeMethods.O_RDWR);
IntPtr buffer = Marshal.AllocHGlobal(size); if (fd < 0)
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"))); OnError?.Invoke(this, new HidErrorEventArgs(this, new IOException($"Failed to open device: {_devicePath}")));
return; return;
} }
Marshal.FreeHGlobal(buffer); IntPtr unmanagedBuffer = IntPtr.Zero;
} try
finally {
{ unmanagedBuffer = Marshal.AllocHGlobal(size);
NativeMethods.close(fd); Marshal.Copy(buffer, 0, unmanagedBuffer, size);
}
await Task.CompletedTask; int request = NativeMethods._IOC(NativeMethods._IOC_WRITE, 'H', 0x06, size); // HIDIOCSFEATURE
int result = NativeMethods.ioctl(fd, request, unmanagedBuffer);
if (result < 0)
{
OnError?.Invoke(this, new HidErrorEventArgs(this, new IOException("ioctl HIDIOCSFEATURE failed")));
}
}
finally
{
if (unmanagedBuffer != IntPtr.Zero)
Marshal.FreeHGlobal(unmanagedBuffer);
NativeMethods.close(fd);
}
});
} }
public async Task<byte[]> GetFeatureReportAsync(byte reportId)
public async Task<HidReport> GetFeatureReportAsync(byte reportId)
{ {
const int maxFeatureSize = 256; const int maxFeatureSize = 256;
byte[] buffer = new byte[maxFeatureSize]; byte[] buffer = new byte[maxFeatureSize];
buffer[0] = reportId; buffer[0] = reportId;
int fd = NativeMethods.open(_devicePath, NativeMethods.O_RDWR); return await Task.Run(() =>
if (fd < 0)
{ {
OnError?.Invoke(this, new HidErrorEventArgs(this, new IOException($"Failed to open device: {_devicePath}"))); int fd = NativeMethods.open(_devicePath, NativeMethods.O_RDWR);
return Array.Empty<byte>(); if (fd < 0)
}
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"))); OnError?.Invoke(this, new HidErrorEventArgs(this, new IOException($"Failed to open device: {_devicePath}")));
return Array.Empty<byte>(); return new HidReport(0, Array.Empty<byte>());
} }
Marshal.Copy(bufPtr, buffer, 0, buffer.Length); IntPtr bufPtr = IntPtr.Zero;
Marshal.FreeHGlobal(bufPtr); try
{
bufPtr = Marshal.AllocHGlobal(buffer.Length);
Marshal.Copy(buffer, 0, bufPtr, buffer.Length);
return await Task.FromResult(buffer); int request = NativeMethods._IOC(NativeMethods._IOC_READ, 'H', 0x07, buffer.Length); // HIDIOCGFEATURE
} int result = NativeMethods.ioctl(fd, request, bufPtr);
finally if (result < 0)
{ {
NativeMethods.close(fd); OnError?.Invoke(this, new HidErrorEventArgs(this, new IOException("ioctl HIDIOCGFEATURE failed")));
} return new HidReport(0, Array.Empty<byte>());
}
byte[] actualBuffer = new byte[result];
Marshal.Copy(bufPtr, actualBuffer, 0, result);
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) public async Task StartListeningAsync(CancellationToken cancellationToken)
@ -341,7 +395,7 @@ namespace EonaCat.HID
catch (NotSupportedException) catch (NotSupportedException)
{ {
OnError?.Invoke(this, new HidErrorEventArgs(this, new NotSupportedException("Reading input reports is not supported on this device."))); 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) catch (Exception ex)
{ {
@ -354,7 +408,22 @@ namespace EonaCat.HID
var data = new byte[bytesRead]; var data = new byte[bytesRead];
Array.Copy(buffer, data, 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 else
{ {

View File

@ -1,6 +1,10 @@
using EonaCat.HID.EventArguments; using EonaCat.HID.EventArguments;
using EonaCat.HID.Models;
using Microsoft.Win32.SafeHandles;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
@ -36,19 +40,49 @@ namespace EonaCat.HID
public int OutputReportByteLength { get; private set; } = 64; public int OutputReportByteLength { get; private set; } = 64;
public int FeatureReportByteLength { 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 IDictionary<string, object> Capabilities { get; private set; } = new Dictionary<string, object>();
public event EventHandler<HidDataReceivedEventArgs> OnDataReceived; public event EventHandler<HidDataReceivedEventArgs> OnDataReceived;
public event EventHandler<HidErrorEventArgs> OnError; public event EventHandler<HidErrorEventArgs> OnError;
private const int KERN_SUCCESS = 0;
private const int kIOHIDOptionsTypeNone = 0;
public void Open() 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() public void Close()
{ {
IOHIDDeviceClose(_deviceHandle, 0); IOHIDDeviceClose(_deviceHandle, 0);
_isOpen = false;
} }
public void Dispose() public void Dispose()
@ -64,26 +98,35 @@ namespace EonaCat.HID
_listeningCts?.Cancel(); _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(() => await Task.Run(() =>
{ {
byte reportId = data.Length > 0 ? data[0] : (byte)0; // Total length includes reportId + data length
IntPtr buffer = Marshal.AllocHGlobal(data.Length); int length = 1 + report.Data.Length;
IntPtr buffer = Marshal.AllocHGlobal(length);
try try
{ {
Marshal.Copy(data, 0, buffer, data.Length); // First byte is reportId
int res = IOHIDDeviceSetReport(_deviceHandle, 1, reportId, buffer, data.Length); 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) if (res != 0)
{ {
OnError?.Invoke(this, new HidErrorEventArgs(this, new Exception($"IOHIDDeviceSetReport (Output) failed: {res}"))); OnError?.Invoke(this, new HidErrorEventArgs(this, new Exception($"IOHIDDeviceSetReport (Output) failed: {res}")));
return;
} }
} }
finally 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(() => await Task.Run(() =>
{ {
byte reportId = data.Length > 0 ? data[0] : (byte)0; int length = 1 + report.Data.Length;
IntPtr buffer = Marshal.AllocHGlobal(data.Length); IntPtr buffer = Marshal.AllocHGlobal(length);
try try
{ {
Marshal.Copy(data, 0, buffer, data.Length); Marshal.WriteByte(buffer, report.ReportId);
int res = IOHIDDeviceSetReport(_deviceHandle, 2, reportId, buffer, data.Length); Marshal.Copy(report.Data, 0, buffer + 1, report.Data.Length);
int res = IOHIDDeviceSetReport(_deviceHandle, 2 /* kIOHIDReportTypeFeature */, report.ReportId, buffer, length);
if (res != 0) if (res != 0)
{ {
OnError?.Invoke(this, new HidErrorEventArgs(this, new Exception($"IOHIDDeviceSetReport (Feature) failed: {res}"))); OnError?.Invoke(this, new HidErrorEventArgs(this, new Exception($"IOHIDDeviceSetReport (Feature) failed: {res}")));
return;
} }
} }
finally 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(() => return await Task.Run(() =>
{ {
@ -131,16 +180,26 @@ namespace EonaCat.HID
try try
{ {
int res = IOHIDDeviceGetReport(_deviceHandle, 2, reportId, buffer, ref length); int res = IOHIDDeviceGetReport(_deviceHandle, 2 /* kIOHIDReportTypeFeature */, reportId, buffer, ref length);
if (res != 0) if (res != 0)
{ {
OnError?.Invoke(this, new HidErrorEventArgs(this, new Exception($"IOHIDDeviceGetReport (Feature) failed: {res}"))); 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]; byte[] outBuf = new byte[length];
Marshal.Copy(buffer, outBuf, 0, 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 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]; byte[] buffer = new byte[InputReportByteLength];
InputReportCallback callback = null; InputReportCallback callback = null;
callback = (ctx, result, sender, report, reportLength) => callback = (ctx, result, sender, report, reportLength) =>
{ {
byte[] output = new byte[reportLength.ToInt32()]; int length = reportLength.ToInt32();
Array.Copy(report, output, output.Length); if (report == null || length == 0)
tcs.TrySetResult(output); {
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); GCHandle.Alloc(callback);
@ -169,13 +250,12 @@ namespace EonaCat.HID
return tcs.Task; return tcs.Task;
} }
public Task StartListeningAsync(CancellationToken ct) public Task StartListeningAsync(CancellationToken ct)
{ {
if (_listeningTask != null && !_listeningTask.IsCompleted) if (_listeningTask != null && !_listeningTask.IsCompleted)
{ {
OnError?.Invoke(this, new HidErrorEventArgs(this, new NotSupportedException("Already listening on this device."))); OnError?.Invoke(this, new HidErrorEventArgs(this, new NotSupportedException("Already listening on this device.")));
return null; return Task.CompletedTask;
} }
_listeningCts = CancellationTokenSource.CreateLinkedTokenSource(ct); _listeningCts = CancellationTokenSource.CreateLinkedTokenSource(ct);
@ -185,14 +265,26 @@ namespace EonaCat.HID
try try
{ {
byte[] buffer = new byte[InputReportByteLength]; 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()]; int len = reportLength.ToInt32();
Array.Copy(report, data, data.Length); 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); GCHandle.Alloc(callback);
IOHIDDeviceRegisterInputReportCallback(_deviceHandle, buffer, (IntPtr)buffer.Length, callback, IntPtr.Zero); IOHIDDeviceRegisterInputReportCallback(_deviceHandle, buffer, (IntPtr)buffer.Length, callback, IntPtr.Zero);
IOHIDDeviceScheduleWithRunLoop(_deviceHandle, CFRunLoopGetCurrent(), IntPtr.Zero); IOHIDDeviceScheduleWithRunLoop(_deviceHandle, CFRunLoopGetCurrent(), IntPtr.Zero);
@ -206,7 +298,7 @@ namespace EonaCat.HID
} }
catch (OperationCanceledException) catch (OperationCanceledException)
{ {
return; // Exit gracefully if cancellation was requested return;
} }
catch (NotSupportedException) catch (NotSupportedException)
{ {
@ -405,7 +497,7 @@ namespace EonaCat.HID
// For managing lifetime of listening loop // For managing lifetime of listening loop
private CancellationTokenSource _listeningCts; private CancellationTokenSource _listeningCts;
private Task _listeningTask; private Task _listeningTask;
private bool _isOpen;
private enum CFNumberType : int private enum CFNumberType : int
{ {

View File

@ -1,5 +1,6 @@
using EonaCat.HID.EventArguments; using EonaCat.HID.EventArguments;
using EonaCat.HID.Managers.Windows; using EonaCat.HID.Managers.Windows;
using EonaCat.HID.Models;
using Microsoft.Win32.SafeHandles; using Microsoft.Win32.SafeHandles;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
@ -35,8 +36,11 @@ namespace EonaCat.HID
public int InputReportByteLength { get; private set; } public int InputReportByteLength { get; private set; }
public int OutputReportByteLength { get; private set; } public int OutputReportByteLength { get; private set; }
public int FeatureReportByteLength { get; private set; } public int FeatureReportByteLength { get; private set; }
public bool IsConnected => _isOpen;
public IDictionary<string, object> Capabilities { get; } = new Dictionary<string, object>(); public IDictionary<string, object> Capabilities { get; } = new Dictionary<string, object>();
public bool IsReadingSupport { get; private set; }
public bool IsWritingSupport { get; private set; }
public event EventHandler<HidDataReceivedEventArgs> OnDataReceived; public event EventHandler<HidDataReceivedEventArgs> OnDataReceived;
public event EventHandler<HidErrorEventArgs> OnError; public event EventHandler<HidErrorEventArgs> OnError;
@ -95,22 +99,23 @@ namespace EonaCat.HID
if (handle == null || handle.IsInvalid) if (handle == null || handle.IsInvalid)
{ {
int err = Marshal.GetLastWin32Error(); 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; return;
} }
_deviceHandle = handle; _deviceHandle = handle;
_deviceStream = new FileStream(_deviceHandle, access, 64, true); _deviceStream = new FileStream(_deviceHandle, access, bufferSize: 64, isAsync: true);
_isOpen = true; _isOpen = true;
// HID descriptor parsing // HID descriptor
if (!HidD_GetPreparsedData(_deviceHandle.DangerousGetHandle(), out _preparsedData)) if (!HidD_GetPreparsedData(_deviceHandle.DangerousGetHandle(), out _preparsedData))
throw new Win32Exception(Marshal.GetLastWin32Error(), "Failed HidD_GetPreparsedData"); throw new Win32Exception(Marshal.GetLastWin32Error(), "Failed HidD_GetPreparsedData");
HIDP_CAPS caps; HIDP_CAPS caps;
int res = HidP_GetCaps(_preparsedData, out caps); int capsRes = HidP_GetCaps(_preparsedData, out caps);
if (res == 0) if (capsRes != NativeMethods.HIDP_STATUS_SUCCESS)
throw new Win32Exception(res, "Failed HidP_GetCaps"); throw new Win32Exception(capsRes, "Failed HidP_GetCaps");
InputReportByteLength = caps.InputReportByteLength; InputReportByteLength = caps.InputReportByteLength;
OutputReportByteLength = caps.OutputReportByteLength; OutputReportByteLength = caps.OutputReportByteLength;
@ -126,12 +131,14 @@ namespace EonaCat.HID
ProductName = GetStringDescriptor(HidD_GetProductString); ProductName = GetStringDescriptor(HidD_GetProductString);
SerialNumber = GetStringDescriptor(HidD_GetSerialNumberString); SerialNumber = GetStringDescriptor(HidD_GetSerialNumberString);
IsReadingSupport = (access == FileAccess.Read || access == FileAccess.ReadWrite);
IsWritingSupport = (access == FileAccess.Write || access == FileAccess.ReadWrite);
HidDeviceAttributes attr = GetDeviceAttributes(); HidDeviceAttributes attr = GetDeviceAttributes();
VendorId = attr.VendorID; VendorId = attr.VendorID;
ProductId = attr.ProductID; ProductId = attr.ProductID;
} }
private SafeFileHandle TryOpenDevice(int access) private SafeFileHandle TryOpenDevice(int access)
{ {
var handle = CreateFile(_devicePath, var handle = CreateFile(_devicePath,
@ -145,34 +152,6 @@ namespace EonaCat.HID
return handle; 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> /// <summary>
/// Close the device /// Close the device
/// </summary> /// </summary>
@ -247,7 +226,7 @@ namespace EonaCat.HID
Close(); Close();
} }
public async Task WriteOutputReportAsync(byte[] data) public async Task WriteOutputReportAsync(HidReport report)
{ {
if (!_isOpen) if (!_isOpen)
{ {
@ -255,15 +234,26 @@ namespace EonaCat.HID
return; 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; return;
} }
try 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(); await _deviceStream.FlushAsync();
} }
catch (Exception ex) catch (Exception ex)
@ -273,12 +263,12 @@ namespace EonaCat.HID
} }
} }
public async Task<byte[]> ReadInputReportAsync() public async Task<HidReport> ReadInputReportAsync()
{ {
if (!_isOpen) if (!_isOpen)
{ {
OnError?.Invoke(this, new HidErrorEventArgs(this, new InvalidOperationException("Device not open"))); 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 () => return await Task.Run(async () =>
@ -291,10 +281,13 @@ namespace EonaCat.HID
if (read == 0) if (read == 0)
{ {
OnError?.Invoke(this, new HidErrorEventArgs(this, new IOException("No data read from device"))); 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) catch (Exception ex)
{ {
@ -304,7 +297,8 @@ namespace EonaCat.HID
}); });
} }
public async Task SendFeatureReportAsync(byte[] data)
public async Task SendFeatureReportAsync(HidReport report)
{ {
if (!_isOpen) if (!_isOpen)
{ {
@ -312,10 +306,13 @@ namespace EonaCat.HID
return; return;
} }
if (data == null || data.Length == 0) if (report == null)
{ throw new ArgumentNullException(nameof(report));
throw new ArgumentNullException(nameof(data));
} // 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(() => 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) if (!_isOpen)
{ {
OnError?.Invoke(this, new HidErrorEventArgs(this, new InvalidOperationException("Device not open"))); 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(() => return await Task.Run(() =>
@ -349,13 +347,15 @@ namespace EonaCat.HID
var err = Marshal.GetLastWin32Error(); var err = Marshal.GetLastWin32Error();
var ex = new Win32Exception(err, "HidD_GetFeature failed"); var ex = new Win32Exception(err, "HidD_GetFeature failed");
OnError?.Invoke(this, new HidErrorEventArgs(this, ex)); 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> /// <summary>
/// Begin async reading loop raising OnDataReceived events on data input /// Begin async reading loop raising OnDataReceived events on data input
/// </summary> /// </summary>
@ -379,15 +379,17 @@ namespace EonaCat.HID
try try
{ {
while (!_listeningCts.Token.IsCancellationRequested) var token = _listeningCts.Token;
while (!token.IsCancellationRequested)
{ {
try 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) catch (OperationCanceledException)
@ -397,15 +399,15 @@ namespace EonaCat.HID
catch (NotSupportedException) catch (NotSupportedException)
{ {
OnError?.Invoke(this, new HidErrorEventArgs(this, new NotSupportedException("Reading input reports is not supported on this device."))); 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) catch (Exception ex)
{ {
// Handle exceptions during reading if (token.IsCancellationRequested)
if (_listeningCts.IsCancellationRequested)
{ {
break; // Exit if cancellation was requested break;
} }
OnError?.Invoke(this, new HidErrorEventArgs(this, ex)); 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]; var buffer = new byte[InputReportByteLength];
// Start async read
_deviceStream.BeginRead(buffer, 0, buffer.Length, ar => _deviceStream.BeginRead(buffer, 0, buffer.Length, ar =>
{ {
try try
{ {
int bytesRead = _deviceStream.EndRead(ar); int bytesRead = _deviceStream.EndRead(ar);
if (bytesRead == 0) 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 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) catch (Exception ex)

View File

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

View File

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

View File

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