Created a HIDReport instead of only a byte[]
This commit is contained in:
parent
f8603b8ce7
commit
1b20217def
|
@ -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);
|
||||
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.");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
await _device.WriteOutputReportAsync(outputReport);
|
||||
AppendEventLog($"Output report sent: {BitConverter.ToString(outputReport)}", Color.DarkGreen);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
AppendEventLog("Write failed: " + ex.Message, Color.DarkRed);
|
||||
}
|
||||
var outputReport = new HidReport(hidReportId, dataBuffer);
|
||||
|
||||
await _device.WriteOutputReportAsync(outputReport);
|
||||
|
||||
AppendEventLog($"Output report sent (Report ID: 0x{hidReportId:X2}): {ByteHelper.ByteArrayToHexString(dataBuffer)}", Color.DarkGreen);
|
||||
}
|
||||
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)
|
||||
{
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
{
|
||||
|
|
|
@ -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);
|
||||
_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;
|
||||
}
|
||||
|
||||
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,91 +241,119 @@ 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.");
|
||||
|
||||
int fd = NativeMethods.open(_devicePath, NativeMethods.O_RDWR);
|
||||
if (fd < 0)
|
||||
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)
|
||||
{
|
||||
OnError?.Invoke(this, new HidErrorEventArgs(this, new IOException($"Failed to open device: {_devicePath}")));
|
||||
return;
|
||||
Array.Copy(report.Data, 0, buffer, 1, report.Data.Length);
|
||||
}
|
||||
|
||||
try
|
||||
await Task.Run(() =>
|
||||
{
|
||||
int size = data.Length;
|
||||
IntPtr buffer = Marshal.AllocHGlobal(size);
|
||||
Marshal.Copy(data, 0, buffer, size);
|
||||
|
||||
int request = NativeMethods._IOC(NativeMethods._IOC_WRITE, 'H', 0x06, size); // HIDIOCSFEATURE
|
||||
int result = NativeMethods.ioctl(fd, request, buffer);
|
||||
if (result < 0)
|
||||
int fd = NativeMethods.open(_devicePath, NativeMethods.O_RDWR);
|
||||
if (fd < 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;
|
||||
}
|
||||
|
||||
Marshal.FreeHGlobal(buffer);
|
||||
}
|
||||
finally
|
||||
{
|
||||
NativeMethods.close(fd);
|
||||
}
|
||||
IntPtr unmanagedBuffer = IntPtr.Zero;
|
||||
try
|
||||
{
|
||||
unmanagedBuffer = Marshal.AllocHGlobal(size);
|
||||
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;
|
||||
byte[] buffer = new byte[maxFeatureSize];
|
||||
buffer[0] = reportId;
|
||||
|
||||
int fd = NativeMethods.open(_devicePath, NativeMethods.O_RDWR);
|
||||
if (fd < 0)
|
||||
return await Task.Run(() =>
|
||||
{
|
||||
OnError?.Invoke(this, new HidErrorEventArgs(this, new IOException($"Failed to open device: {_devicePath}")));
|
||||
return Array.Empty<byte>();
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
IntPtr bufPtr = Marshal.AllocHGlobal(buffer.Length);
|
||||
Marshal.Copy(buffer, 0, bufPtr, buffer.Length);
|
||||
|
||||
int request = NativeMethods._IOC(NativeMethods._IOC_READ, 'H', 0x07, buffer.Length); // HIDIOCGFEATURE
|
||||
int result = NativeMethods.ioctl(fd, request, bufPtr);
|
||||
if (result < 0)
|
||||
int fd = NativeMethods.open(_devicePath, NativeMethods.O_RDWR);
|
||||
if (fd < 0)
|
||||
{
|
||||
OnError?.Invoke(this, new HidErrorEventArgs(this, new IOException("ioctl HIDIOCGFEATURE failed")));
|
||||
return Array.Empty<byte>();
|
||||
OnError?.Invoke(this, new HidErrorEventArgs(this, new IOException($"Failed to open device: {_devicePath}")));
|
||||
return new HidReport(0, Array.Empty<byte>());
|
||||
}
|
||||
|
||||
Marshal.Copy(bufPtr, buffer, 0, buffer.Length);
|
||||
Marshal.FreeHGlobal(bufPtr);
|
||||
IntPtr bufPtr = IntPtr.Zero;
|
||||
try
|
||||
{
|
||||
bufPtr = Marshal.AllocHGlobal(buffer.Length);
|
||||
Marshal.Copy(buffer, 0, bufPtr, buffer.Length);
|
||||
|
||||
return await Task.FromResult(buffer);
|
||||
}
|
||||
finally
|
||||
{
|
||||
NativeMethods.close(fd);
|
||||
}
|
||||
int request = NativeMethods._IOC(NativeMethods._IOC_READ, 'H', 0x07, buffer.Length); // HIDIOCGFEATURE
|
||||
int result = NativeMethods.ioctl(fd, request, bufPtr);
|
||||
if (result < 0)
|
||||
{
|
||||
OnError?.Invoke(this, new HidErrorEventArgs(this, new IOException("ioctl HIDIOCGFEATURE failed")));
|
||||
return 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)
|
||||
|
@ -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
|
||||
{
|
||||
|
|
|
@ -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
|
||||
{
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
var device = new HidMac(devicePtr);
|
||||
device.Setup();
|
||||
OnDeviceInserted?.Invoke(this, new HidEventArgs(device));
|
||||
try
|
||||
{
|
||||
var device = new HidMac(devicePtr);
|
||||
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)
|
||||
{
|
||||
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()
|
||||
|
|
|
@ -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();
|
||||
if (eventType == DBT_DEVICEARRIVAL)
|
||||
int eventType = wParam.ToInt32();
|
||||
|
||||
if (eventType == DBT_DEVICEARRIVAL || eventType == DBT_DEVICEREMOVECOMPLETE)
|
||||
{
|
||||
// Device inserted
|
||||
var devBroadcast = Marshal.PtrToStructure<DEV_BROADCAST_HDR>(lParam);
|
||||
if (devBroadcast.dbch_devicetype == DBT_DEVTYP_DEVICEINTERFACE)
|
||||
var hdr = Marshal.PtrToStructure<DEV_BROADCAST_HDR>(lParam);
|
||||
if (hdr.dbch_devicetype == DBT_DEVTYP_DEVICEINTERFACE)
|
||||
{
|
||||
// Treat all HID devices or filter if needed
|
||||
OnDeviceInserted?.Invoke(this, new HidEventArgs(null));
|
||||
}
|
||||
}
|
||||
else if (eventType == DBT_DEVICEREMOVECOMPLETE)
|
||||
{
|
||||
var devBroadcast = Marshal.PtrToStructure<DEV_BROADCAST_HDR>(lParam);
|
||||
if (devBroadcast.dbch_devicetype == DBT_DEVTYP_DEVICEINTERFACE)
|
||||
{
|
||||
OnDeviceRemoved?.Invoke(this, new HidEventArgs(null));
|
||||
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)
|
||||
{
|
||||
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);
|
||||
}
|
||||
|
||||
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
|
||||
}
|
|
@ -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>();
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue