From 115bac01ca8d761761be34e2a4552da99106e2ba Mon Sep 17 00:00:00 2001 From: EonaCat Date: Sat, 12 Jul 2025 22:40:29 +0200 Subject: [PATCH] Updated so we also support linux and mac --- Analyzer/EonaCat.HID.Analyzer.csproj | 10 +- Analyzer/MainForm.cs | 334 ++++--- Analyzer/MainForm.resx | 89 +- Analyzer/mainForm.Designer.cs | 1 + EonaCat.HID.sln | 33 +- EonaCat.HID/Attributes.cs | 24 - EonaCat.HID/Capabilities.cs | 44 - EonaCat.HID/Device.cs | 842 ------------------ EonaCat.HID/DeviceData.cs | 32 - EonaCat.HID/DeviceEventMonitor.cs | 54 -- EonaCat.HID/Devices.cs | 180 ---- EonaCat.HID/Enumerator.cs | 40 - EonaCat.HID/EonaCat.HID.csproj | 27 +- .../HidDataReceivedEventArgs.cs | 19 + .../EventArguments/HidErrorEventArgs.cs | 19 + EonaCat.HID/EventArguments/HidEventArgs.cs | 17 + EonaCat.HID/Helpers/DeviceModeHelper.cs | 13 - EonaCat.HID/HidFactory.cs | 68 ++ EonaCat.HID/HidLinux.cs | 392 ++++++++ EonaCat.HID/HidMac.cs | 412 +++++++++ EonaCat.HID/HidWindows.cs | 461 ++++++++++ EonaCat.HID/IDevice.cs | 100 --- EonaCat.HID/IEnumerator.cs | 21 - EonaCat.HID/IHid.cs | 76 ++ EonaCat.HID/IHidManager.cs | 27 + EonaCat.HID/Managers/HidManagerLinux.cs | 176 ++++ EonaCat.HID/Managers/HidManagerMac.cs | 156 ++++ EonaCat.HID/Managers/HidManagerWindows.cs | 517 +++++++++++ EonaCat.HID/NativeMethods.cs | 225 ----- EonaCat.HID/ReadDevice.cs | 39 - EonaCat.HID/ReadEnumerator.cs | 46 - EonaCat.HID/Report.cs | 86 -- EonaCat.HID/StateAsync.cs | 21 - 33 files changed, 2617 insertions(+), 1984 deletions(-) delete mode 100644 EonaCat.HID/Attributes.cs delete mode 100644 EonaCat.HID/Capabilities.cs delete mode 100644 EonaCat.HID/Device.cs delete mode 100644 EonaCat.HID/DeviceData.cs delete mode 100644 EonaCat.HID/DeviceEventMonitor.cs delete mode 100644 EonaCat.HID/Devices.cs delete mode 100644 EonaCat.HID/Enumerator.cs create mode 100644 EonaCat.HID/EventArguments/HidDataReceivedEventArgs.cs create mode 100644 EonaCat.HID/EventArguments/HidErrorEventArgs.cs create mode 100644 EonaCat.HID/EventArguments/HidEventArgs.cs delete mode 100644 EonaCat.HID/Helpers/DeviceModeHelper.cs create mode 100644 EonaCat.HID/HidFactory.cs create mode 100644 EonaCat.HID/HidLinux.cs create mode 100644 EonaCat.HID/HidMac.cs create mode 100644 EonaCat.HID/HidWindows.cs delete mode 100644 EonaCat.HID/IDevice.cs delete mode 100644 EonaCat.HID/IEnumerator.cs create mode 100644 EonaCat.HID/IHid.cs create mode 100644 EonaCat.HID/IHidManager.cs create mode 100644 EonaCat.HID/Managers/HidManagerLinux.cs create mode 100644 EonaCat.HID/Managers/HidManagerMac.cs create mode 100644 EonaCat.HID/Managers/HidManagerWindows.cs delete mode 100644 EonaCat.HID/NativeMethods.cs delete mode 100644 EonaCat.HID/ReadDevice.cs delete mode 100644 EonaCat.HID/ReadEnumerator.cs delete mode 100644 EonaCat.HID/Report.cs delete mode 100644 EonaCat.HID/StateAsync.cs diff --git a/Analyzer/EonaCat.HID.Analyzer.csproj b/Analyzer/EonaCat.HID.Analyzer.csproj index 8bd9a63..1258cdd 100644 --- a/Analyzer/EonaCat.HID.Analyzer.csproj +++ b/Analyzer/EonaCat.HID.Analyzer.csproj @@ -96,13 +96,13 @@ - - {9e8f1d50-74ea-4c60-bd5c-ab2c5b53bc66} - EonaCat.HID - + - + + {00403bd6-7a26-4971-29d3-8a7849aac770} + EonaCat.HID + VID {0:X4}, PID {0:X4}", e.Device.VendorId, e.Device.ProductId); + AppendEventLog(str); + } + catch (Exception ex) + { + PopupException(ex.Message); + } + } + + private void Hid_Inserted(object sender, HidEventArgs e) + { + try + { + var str = string.Format("Inserted Device --> VID {0:X4}, PID {0:X4}", e.Device.VendorId, e.Device.ProductId); + AppendEventLog(str, Color.Orange); } catch (Exception ex) { @@ -235,63 +316,30 @@ namespace EonaCat.HID.Analyzer } } - private void ButtonReadInput_Click(object sender, EventArgs e) + private async void ButtonReadInput_Click(object sender, EventArgs e) { try { - var len = _device.Capabilities.InputReportByteLength; + if (_device == null) + { + AppendEventLog("No device connected. Please select a device and click 'Connect'.", Color.Red); + return; + } + + var len = (int)_device.InputReportByteLength; if (len <= 0) { throw new Exception("This device has no Input Report support!"); } - _device.ReadReport(Device_InputReportReceived); - AppendEventLog("Hid Input Report Callback Started."); - } - catch (Exception ex) - { - PopupException(ex.Message); - } - } - - private void ButtonWriteOutput_Click(object sender, EventArgs e) - { - try - { - var hidReportId = byte.Parse(comboBoxReportId.Text); - var buf = ByteHelper.HexStringToByteArray(textBoxWriteData.Text); - - var report = _device.CreateReport(); - if (buf.Length > report.Data.Length) + var buffer = await _device.ReadInputReportAsync(); + if (buffer.Length < 2) { - throw new Exception("Output Report Length Exceed"); + AppendEventLog("Received report is too short to contain a valid Report ID.", Color.Red); + return; } - - report.ReportId = hidReportId; - Array.Copy(buf, report.Data, buf.Length); - _device.WriteReport(report, WriteReportTimeout); - var str = string.Format("Tx Output Report [{0}] --> ID:{1}, {2}", report.Data.Length + 1, report.ReportId, ByteHelper.ByteArrayToHexString(report.Data)); - AppendEventLog(str, Color.DarkGreen); - } - catch (Exception ex) - { - PopupException(ex.Message); - } - } - - private void ButtonReadFeature_Click(object sender, EventArgs e) - { - try - { - var hidReportId = byte.Parse(comboBoxReportId.Text); - var len = _device.Capabilities.FeatureReportByteLength; - if (len <= 0) - { - throw new Exception("This device has no Feature Report support!"); - } - - _device.ReadFeatureData(out byte[] buf, hidReportId); - var str = string.Format("Rx Feature Report [{0}] <-- {1}", buf.Length, ByteHelper.ByteArrayToHexString(buf)); + + var str = string.Format("Rx Input Report [{0}] <-- {1}", buffer.Length, BitConverter.ToString(buffer)); AppendEventLog(str, Color.Blue); } catch (Exception ex) @@ -300,21 +348,70 @@ namespace EonaCat.HID.Analyzer } } - private void ButtonWriteFeature_Click(object sender, EventArgs e) + private async void ButtonWriteOutput_Click(object sender, EventArgs e) + { + try + { + byte hidReportId = byte.Parse(comboBoxReportId.Text.Trim()); + byte[] buf = ByteHelper.HexStringToByteArray(textBoxWriteData.Text.Trim()); + + // Combine report ID and buffer + byte[] outputReport = new byte[buf.Length + 1]; + outputReport[0] = hidReportId; + Array.Copy(buf, 0, outputReport, 1, buf.Length); + + try + { + await _device.WriteOutputReportAsync(outputReport); + AppendEventLog($"Output report sent: {BitConverter.ToString(outputReport)}", Color.DarkGreen); + } + catch (Exception ex) + { + AppendEventLog("Write failed: " + ex.Message, Color.DarkRed); + } + } + catch (Exception ex) + { + PopupException("Error preparing output report: " + ex.Message); + } + } + + private async void ButtonReadFeature_Click(object sender, EventArgs e) + { + try + { + var hidReportId = byte.Parse(comboBoxReportId.Text); + var len = _device.FeatureReportByteLength; + if (len <= 0) + { + throw new Exception("This device has no Feature Report support!"); + } + + var buffer = await _device.GetFeatureReportAsync(hidReportId); + var str = string.Format("Rx Feature Report [{0}] <-- {1}", buffer.Length, ByteHelper.ByteArrayToHexString(buffer)); + AppendEventLog(str, Color.Blue); + } + catch (Exception ex) + { + PopupException(ex.Message); + } + } + + private async void ButtonWriteFeature_Click(object sender, EventArgs e) { try { var hidReportId = byte.Parse(comboBoxReportId.Text); var buf = ByteHelper.HexStringToByteArray(textBoxWriteData.Text); - var len = _device.Capabilities.FeatureReportByteLength; + var len = _device.FeatureReportByteLength; if (buf.Length > len) { throw new Exception("Write Feature Report Length Exceed"); } Array.Resize(ref buf, len); - _device.WriteFeatureData(buf); + await _device.SendFeatureReportAsync(buf); var str = string.Format("Tx Feature Report [{0}] --> {1}", buf.Length, ByteHelper.ByteArrayToHexString(buf)); AppendEventLog(str, Color.DarkGreen); } @@ -328,53 +425,15 @@ namespace EonaCat.HID.Analyzer { try { - if (dataGridView1.SelectedRows.Count <= 0) return; + if (dataGridView1.SelectedRows.Count <= 0) + { + return; + } + var index = dataGridView1.SelectedRows[0].Index; - var devinfo = _deviceList[index].Info; - toolStripTextBoxVidPid.Text = string.Format("{0:X4}:{1:X4}", devinfo.VendorId, devinfo.ProductId); - } - catch (Exception ex) - { - PopupException(ex.Message); - } - } - - private void HidDevice_Inserted() - { - try - { - var str = string.Format("Inserted Device --> VID {0:X4}, PID {0:X4}", _device.Info.VendorId, _device.Info.ProductId); - AppendEventLog(str, Color.Orange); - _device.ReadReport(Device_InputReportReceived); - } - catch (Exception ex) - { - PopupException(ex.Message); - } - } - - private void HidDevice_Removed() - { - try - { - var str = string.Format("Removed Device --> VID {0:X4}, PID {0:X4}", _device.Info.VendorId, _device.Info.ProductId); - AppendEventLog(str); - } - catch (Exception ex) - { - PopupException(ex.Message); - } - } - - private void Device_InputReportReceived(Report report) - { - try - { - if (!_device.IsConnected || !_device.IsOpen || report.Data == null) return; - var str = $"Rx Input Report [{report.Data.Length + 1}] <-- ID:{report.ReportId}, {ByteHelper.ByteArrayToHexString(report.Data)}"; - AppendEventLog(str, Color.Blue); - _device.ReadReport(Device_InputReportReceived); + var info = _deviceList.ElementAt(index); + toolStripTextBoxVidPid.Text = string.Format("{0:X4}:{1:X4}", info.VendorId, info.ProductId); } catch (Exception ex) { @@ -384,7 +443,12 @@ namespace EonaCat.HID.Analyzer private void dataGridView1_DoubleClick(object sender, EventArgs e) { - ConnectToSelectedDevice(); + ConnectToSelectedDeviceAsync().ConfigureAwait(false); + } + + private void toolStripTextBoxVidPid_Enter(object sender, EventArgs e) + { + ToolStripButtonFilter_Click(sender, e); } } } \ No newline at end of file diff --git a/Analyzer/MainForm.resx b/Analyzer/MainForm.resx index e5e3c96..173f685 100644 --- a/Analyzer/MainForm.resx +++ b/Analyzer/MainForm.resx @@ -151,60 +151,59 @@ iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8 - YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAI4SURBVDhPjZNPaNNQHMcfjNGBVTpB2GUOtyDYwzbZsKV2 - fUkKJZd6292Tf27CDh6zHSxpCoJuDlrYLuqlF1FhBz20ox62NdU6JzotzDavzYpddy9ofL+Xl9l1CvvC - j+R93+/3ye/3kqD/KTcy4sv5/d7tcHgQgtunUxahvrejlw/zl4R4ORi68+Ha9S/g10QRs4ReaVvNULJo - 3YfQjPpF8HJjY8KaIHi+KYrn0/TMcA1jgYhyh2DpmTE11c8KXam5vQF9s1HXDctOFS2R2ydEMJ6kEIuI - 0lNuOaKFs4mNekfftAoAaH3PLLc/z9vtSsaJ4s3d6vZddg4c0iGSdIMVg5KGtaUZjcdOJ82hg0o6dwxQ - vmcflOfe8HRkYmmJYHmdLR68b1xIbJi/NWM/yAyqI0DpthP0Hjy+TQE4+APjX7VY7DyiLQdgdnWn6eX7 - DoAXupBuQEtRztExbBKJXkV6aX8cAMl3P8/y/b+ALkg3YA9jHwBMSZpAqbJ1BkaATvj+ccDOwokR6AGG - YISq+4HRDvK60VhkCyoGcA9w96Hd/jjHAPTJQzQGTFFeJhE5z9MRSpWsuPMaySSsW5XMkyMAD/BMHBVN - USqw1xiR46zYlV600gmDWC7kXwIAO7yZCIFOuO0obdj9dIwV6CRRagrTq+vDyqM1j0DDr74UIKcajY7S - T1mDqEtSjBX2iv4LYbgGVvNfAyuFW+MLr+JX5l8fzmazfSzhtAo/LwxC+NWsd0J94eN2jxD6A3Zieio6 - Qs3XAAAAAElFTkSuQmCC + YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAIqSURBVDhPfZJNaBNBFMcXSknBKKkg9FKL7SKSQ1tpMSEm + mZ0NhFzirXdPftyEHjxueohsNiDUj0IC7UW95CIq5NDLpkQo7U401hatBkKys5kG0/Qe0JW3ycZsWvuH + P8u89+bHe2+W4/4jdWrKo3q97r1gcBw8nD9XOY4b2Zy+flK4xsfL/sCDz7duf4N4XRDQcK0lebcZSGns + MVgmxlWIqTMzfJ7nXT9iMdfXxdBkHSGeCmKHIvyaLCyMOgCSWh1TdhqGQpiZ1pjgSA6IIjRPBZFRAb9y + JBTClpLbRkfZYUUAtH5m19oHCbNdyXat3T2s7T209tCDdCjGd/qAFGG7Mmk863bSnDiuZFQHoPzIPC4v + b9r1OsIvKBK3rEPyU+NKcrv+RyZHfrugDyjd7/ogYULsHwD5qwj9rkejl7m0xnwwu7TfdDsAvYs2ZBDQ + isUuUUE0aThyk1NKR7MASH38dfEUYAAyCKgi5AGAjvEcly6zCzACdHImYH/l1AgU4wCMULN/MIWwgkIa + zx0Ae4GHT832l2ULUEVooorQmC6IazQsFux6Ll1i8e4z0nk4tyrZl31AzxDTUUTQBVy0njEsxvsAkKKx + TJJQZkPOEgCs5YVCFDpxJDPEHFVIYx06eVJq8osbW5Ox1byLX827vNI7Hmpqkcg0RVgGGxhHHQBbMjGC + 8PVtFL771ov3Zlfex28kPpws5XIjw7XnKvimOA72Sjn3nPTWM5y39RdV/noYsphYLAAAAABJRU5ErkJg + gg== iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8 - YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAIzSURBVDhPY5BuvNMjXXPjhWTJubsiuUffCOccfS2Re8qe - AQrEiy8oihWcuSGeeXw2VAgVSNXfOSBdde2/ZNml/+J5J8FYJPtEAVSaASjeDJITyjj8HSqECggZIFF2 - uQQkJ5Zy+DNUCBUQMkC0/gqPVPqWU6K5J2KhQqiAkAEgYOTsHW/i6uNr4uytZOjoJc9QX88ElWJgkGm8 - u1+6GtUA8ZzjeVBpODBy9XUCGpAGwsbO3lOMjY1ZwRJSjXecpWpvLJXL2bFcPPfoWrHck6vEM46KgeTm - 3/9vUH/lu0r9pe9KyNg1ubTQu3GaO9gAGDB19U0wd/dXgHIZFt3/cbj3xq//jVcwcd2Fr/9Tjn/+m3zq - RyVUOaYByx/9eJ85ddV/n8Ts/3bhySjYwMnnf9KqU/9jT/zYD1UOCihfHyM3b2MoF2xAclP/f6fAiP/J - 1r7/5xv5/V9m4Pffy8Hnv5aF3f+Q3mX/4098PwBVzsBg5uHBBwyc7tDQUGYQH9mADjO//9+VAv5/VQ74 - H2vri90AEDBx9FM3dvFpM3Hxrurfc+kTzABTF5//iwz9/leZ+/03AbJxGoAMQC5IaZv23zU4GqwJGWua - 2f6PmrmNsAENl378L9t9+3/5wccIfODR/8JtN/6XXviJ34Cl9368bbz667/FpHv/Y7e8/e846+H/0NUv - /3svfgbGJed//o89/mMPVDkmmH/nZ9+k27+eNF35dRcY9yi4/vKve8mnfzxJPv3DCwAg0ohxfV2WKwAA - AABJRU5ErkJggg== + YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAIoSURBVDhPY5BuvNMjXXPjhWTJubsiuUfeCOccfS2Re8qe + AQrEiy8oihWcuSGeeXw2TAwFSNXfOSBdde2/ZNml/+J5J8FYJPtEAUxesuxSM0hOMOPwd1SdUEDIAImy + yyUgObHkw59RdUIBIQNE66/wSKVvOSWaeyIWVScUEDIABIycveNNXH18TZy9lQwdveQZ6uuZ4JIyjXf3 + S1ejGiCeczwP2QAQMHL1dTJx9k4DYWNn7ynGxsasYAmpxjvOUrU3lsrl7Fgunnt0rVjuyVXiGUfFQHLz + 7/83qL/yXaX+0nclZOyaXFro3TjNHcUGU1ffBHN3fwUYf9H9H4d7b/z633gFE9dd+Po/+dinv8mnflTi + 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= iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8 - YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAJSSURBVDhPY8AGbPa9SLPZ+yISysUP0upncmWU93anV/ZZ - Q4UYbPY+e2qz68UNKBc/SC/vXZ9Z0fs/taTrc1Z5jw1IzHrPiy6bPS/qwAoIgczyngUgA5ANsTj2iHP2 - jl6huZOq3/V21/04sqnEDaocE9TX17MAvbAaZkhUbsOv1OLGvKkTar/BxNra6v/9Op8O9yIGABmSVta9 - Jja35X9Yet3/lJIOsEYQLq3r+H9va+T/n9eL3vx71KoD1YIJ4uvrOSKz65+kFKNqvr3Q5f+fqwX//z9q - /f/7XuOXfw+bw6FaUMGV/Vk8kyfUwZ1dWtv+/97m8P+/L+X8/3e/EWwAArcs+PewSRuqFQIm9VV9Rbb5 - 7qbQ/183OQE1N6BpRuB/D1vioNoZGIpqOxHOXuD8//eV/P9/b1X//3u9DK4BxP9zuRDJgDZE7GxaXLa7 - rB7obGCA/b6Y/f/vzUq4Qjh+2PL/5+Ho/993+///usPn3f+n9VxQ7RDw9mja/B+Xi/5haATiP1dL/v88 - kQhm/77X+hVrjNjvfy8AdFY8UMFfuOYHTcBwaAKzf51O/f9tmyfQG/mZrjufc9vv/88C1crA4LL7Hb/N - 3udP7Pe/lAA6NQFmyL+79f+/bfcC+r/q/+9HrT//3aouA6m33vN8ms3uF0VgzSBgs+d5BDAH/rfa8yID - xP/3oNnpz6PWs78fNf76db/+07ddfpeBLjEEyXlsu8VusfvFO6u9L86C+GAQuuo/M9CAbfb773NAhcDg - //96pv//VzFDuXAAzHAFtvte+AMAGv7JUh+nLuoAAAAASUVORK5CYII= + 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= iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8 - YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAEZSURBVDhPY6AKmFApJh7mpvHV1NT0PzE4zE3967RicTGo - dgaGiZUK9h5Ohv+/7uL8//8gO178dRfXf5DaKeUKdlDtEJAfo361KlUVqyYY/neA/X9Fsur/vFiNS1Bt - CDCzXpIrwl/n04pWaayaQXhJk8z/IF/1LxNzhfig2lDBxDJ5C0d7w7/nFwhiaL60SPC/A1AOw+nooCtP - oczL2eD/683ccM1vt3L/B4m15yi0QZXhByVJKocL4tThBuTFaPwviVc5DJUmDKbUi/KYm5vCDQCxQWEE - lSYM6usZmEDxDTMAxIZKEQcGlwGfdnD9t7QwId0AkKY9k8X/+7kZ/C9LVDkClSIeVCSq7o/x0frQma9Q - DhWiBWBgAAClAOU+yA6HfwAAAABJRU5ErkJggg== + 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= @@ -213,12 +212,12 @@ iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8 - YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAERSURBVDhPrZDbSgJRGIXnpewd6jXsjSQvIrwoI0RQMChU - 0iiDPCGiE3ZCRkvR8VzTeBhnyR5/ccaZNnPhB4t9sdf6Ln5hb8QeathNJFVFKF5C8DqL4ksDVHWGDf7j - LHyPg6NjviSaFqlu5yQYR+KpupaIkrMknCxT3Y7v/NYYb0ITK1c3BarbWWhLQ7IR0cTKReyZ6lZ0XYei - ztHpK4bAc+h1FgQijzSxMptrGIxVSO0xX3AaStFki7bUMVFmaMm/eJMGfIH/MkGzLep0AXn4h/r3CJV3 - mS9gn2bY4UY/UzQ7E9TqfeTFtnuB+XAfzSHKr11kSl/uBebDiZ89ZCst3OUkdwL28sIVsE83ock+EIQV - 2Mz2wxeg6/UAAAAASUVORK5CYII= + YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAENSURBVDhPrZHbSgJRGEZ9KX2HfI18I9ELkS5KCREMDBSN + VLRA0xDREQ+FTJai47mm8TDOF7NlhulHNwO14LvarHXxb4fjv4jnW6CLZRqIpKoI3Zbw3HwDdX6hC6cI + RnNwuT38yE1WoJ6JL5RC+rFxiAji8Uj0rkY9E+9lksnGqMu4TlSoZ7JT9yxihKjLuIoXqcfQNA2yssVo + KrOA8+z8eOAiVqAuY7NVMVsqEIdLfiAQyVAX6l7DSt5gIH2hI874AX84TX0o6x2k+Td6HwvUuxI/oD9a + 0Q+3+FyjP1qh1ZuiLAztB6yHe+nPUWuP8VB9tx+wHk54naBUH+D+SbQXMP771LgB/dHOqPsnfgDYzPbD + 7s5C8AAAAABJRU5ErkJggg== diff --git a/Analyzer/mainForm.Designer.cs b/Analyzer/mainForm.Designer.cs index 153c442..5f02e70 100644 --- a/Analyzer/mainForm.Designer.cs +++ b/Analyzer/mainForm.Designer.cs @@ -264,6 +264,7 @@ this.toolStripTextBoxVidPid.Size = new System.Drawing.Size(100, 25); this.toolStripTextBoxVidPid.Text = "0483:0400"; this.toolStripTextBoxVidPid.TextBoxTextAlign = System.Windows.Forms.HorizontalAlignment.Center; + this.toolStripTextBoxVidPid.Enter += new System.EventHandler(this.toolStripTextBoxVidPid_Enter); // // menuStrip1 // diff --git a/EonaCat.HID.sln b/EonaCat.HID.sln index 069fc85..00d167c 100644 --- a/EonaCat.HID.sln +++ b/EonaCat.HID.sln @@ -2,10 +2,10 @@ Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 17 VisualStudioVersion = 17.10.34928.147 MinimumVisualStudioVersion = 10.0.40219.1 -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EonaCat.HID", "EonaCat.HID\EonaCat.HID.csproj", "{9E8F1D50-74EA-4C60-BD5C-AB2C5B53BC66}" -EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EonaCat.HID.Analyzer", "Analyzer\EonaCat.HID.Analyzer.csproj", "{61994020-DB89-4621-BA4B-7347A2142CFF}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EonaCat.HID", "EonaCat.HID\EonaCat.HID.csproj", "{00403BD6-7A26-4971-29D3-8A7849AAC770}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -18,19 +18,6 @@ Global Release|x86 = Release|x86 EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution - {9E8F1D50-74EA-4C60-BD5C-AB2C5B53BC66}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {9E8F1D50-74EA-4C60-BD5C-AB2C5B53BC66}.Debug|Any CPU.Build.0 = Debug|Any CPU - {9E8F1D50-74EA-4C60-BD5C-AB2C5B53BC66}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU - {9E8F1D50-74EA-4C60-BD5C-AB2C5B53BC66}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU - {9E8F1D50-74EA-4C60-BD5C-AB2C5B53BC66}.Debug|x64.ActiveCfg = Debug|Any CPU - {9E8F1D50-74EA-4C60-BD5C-AB2C5B53BC66}.Debug|x64.Build.0 = Debug|Any CPU - {9E8F1D50-74EA-4C60-BD5C-AB2C5B53BC66}.Debug|x86.ActiveCfg = Debug|Any CPU - {9E8F1D50-74EA-4C60-BD5C-AB2C5B53BC66}.Release|Any CPU.ActiveCfg = Release|Any CPU - {9E8F1D50-74EA-4C60-BD5C-AB2C5B53BC66}.Release|Any CPU.Build.0 = Release|Any CPU - {9E8F1D50-74EA-4C60-BD5C-AB2C5B53BC66}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU - {9E8F1D50-74EA-4C60-BD5C-AB2C5B53BC66}.Release|Mixed Platforms.Build.0 = Release|Any CPU - {9E8F1D50-74EA-4C60-BD5C-AB2C5B53BC66}.Release|x64.ActiveCfg = Release|Any CPU - {9E8F1D50-74EA-4C60-BD5C-AB2C5B53BC66}.Release|x86.ActiveCfg = Release|Any CPU {61994020-DB89-4621-BA4B-7347A2142CFF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {61994020-DB89-4621-BA4B-7347A2142CFF}.Debug|Any CPU.Build.0 = Debug|Any CPU {61994020-DB89-4621-BA4B-7347A2142CFF}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU @@ -47,6 +34,22 @@ Global {61994020-DB89-4621-BA4B-7347A2142CFF}.Release|x64.Build.0 = Release|Any CPU {61994020-DB89-4621-BA4B-7347A2142CFF}.Release|x86.ActiveCfg = Release|Any CPU {61994020-DB89-4621-BA4B-7347A2142CFF}.Release|x86.Build.0 = Release|Any CPU + {00403BD6-7A26-4971-29D3-8A7849AAC770}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {00403BD6-7A26-4971-29D3-8A7849AAC770}.Debug|Any CPU.Build.0 = Debug|Any CPU + {00403BD6-7A26-4971-29D3-8A7849AAC770}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {00403BD6-7A26-4971-29D3-8A7849AAC770}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {00403BD6-7A26-4971-29D3-8A7849AAC770}.Debug|x64.ActiveCfg = Debug|Any CPU + {00403BD6-7A26-4971-29D3-8A7849AAC770}.Debug|x64.Build.0 = Debug|Any CPU + {00403BD6-7A26-4971-29D3-8A7849AAC770}.Debug|x86.ActiveCfg = Debug|Any CPU + {00403BD6-7A26-4971-29D3-8A7849AAC770}.Debug|x86.Build.0 = Debug|Any CPU + {00403BD6-7A26-4971-29D3-8A7849AAC770}.Release|Any CPU.ActiveCfg = Release|Any CPU + {00403BD6-7A26-4971-29D3-8A7849AAC770}.Release|Any CPU.Build.0 = Release|Any CPU + {00403BD6-7A26-4971-29D3-8A7849AAC770}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {00403BD6-7A26-4971-29D3-8A7849AAC770}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {00403BD6-7A26-4971-29D3-8A7849AAC770}.Release|x64.ActiveCfg = Release|Any CPU + {00403BD6-7A26-4971-29D3-8A7849AAC770}.Release|x64.Build.0 = Release|Any CPU + {00403BD6-7A26-4971-29D3-8A7849AAC770}.Release|x86.ActiveCfg = Release|Any CPU + {00403BD6-7A26-4971-29D3-8A7849AAC770}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/EonaCat.HID/Attributes.cs b/EonaCat.HID/Attributes.cs deleted file mode 100644 index 4f8301b..0000000 --- a/EonaCat.HID/Attributes.cs +++ /dev/null @@ -1,24 +0,0 @@ -namespace EonaCat.HID -{ - public class Attributes - { - // This file is part of the EonaCat project(s) which is released under the Apache License. - // See the LICENSE file or go to https://EonaCat.com/License for full license details. - - internal Attributes(NativeMethods.HIDD_ATTRIBUTES attributes) - { - VendorId = attributes.VendorID; - ProductId = attributes.ProductID; - Version = attributes.VersionNumber; - - VendorIdHex = "0x" + attributes.VendorID.ToString("X4"); - ProductIdHex = "0x" + attributes.ProductID.ToString("X4"); - } - - public int VendorId { get; private set; } - public int ProductId { get; private set; } - public int Version { get; private set; } - public string VendorIdHex { get; set; } - public string ProductIdHex { get; set; } - } -} \ No newline at end of file diff --git a/EonaCat.HID/Capabilities.cs b/EonaCat.HID/Capabilities.cs deleted file mode 100644 index 00ef07f..0000000 --- a/EonaCat.HID/Capabilities.cs +++ /dev/null @@ -1,44 +0,0 @@ -namespace EonaCat.HID -{ - public class Capabilities - { - // This file is part of the EonaCat project(s) which is released under the Apache License. - // See the LICENSE file or go to https://EonaCat.com/License for full license details. - internal Capabilities(NativeMethods.Capabilities capabilities) - { - Usage = capabilities.Usage; - UsagePage = capabilities.UsagePage; - InputReportByteLength = capabilities.InputReportByteLength; - OutputReportByteLength = capabilities.OutputReportByteLength; - FeatureReportByteLength = capabilities.FeatureReportByteLength; - Reserved = capabilities.Reserved; - NumberLinkCollectionNodes = capabilities.NumberLinkCollectionNodes; - NumberInputButtonCaps = capabilities.NumberInputButtonCaps; - NumberInputValueCaps = capabilities.NumberInputValueCaps; - NumberInputDataIndices = capabilities.NumberInputDataIndices; - NumberOutputButtonCaps = capabilities.NumberOutputButtonCaps; - NumberOutputValueCaps = capabilities.NumberOutputValueCaps; - NumberOutputDataIndices = capabilities.NumberOutputDataIndices; - NumberFeatureButtonCaps = capabilities.NumberFeatureButtonCaps; - NumberFeatureValueCaps = capabilities.NumberFeatureValueCaps; - NumberFeatureDataIndices = capabilities.NumberFeatureDataIndices; - } - - public short Usage { get; private set; } - public short UsagePage { get; private set; } - public short InputReportByteLength { get; private set; } - public short OutputReportByteLength { get; private set; } - public short FeatureReportByteLength { get; private set; } - public short[] Reserved { get; private set; } - public short NumberLinkCollectionNodes { get; private set; } - public short NumberInputButtonCaps { get; private set; } - public short NumberInputValueCaps { get; private set; } - public short NumberInputDataIndices { get; private set; } - public short NumberOutputButtonCaps { get; private set; } - public short NumberOutputValueCaps { get; private set; } - public short NumberOutputDataIndices { get; private set; } - public short NumberFeatureButtonCaps { get; private set; } - public short NumberFeatureValueCaps { get; private set; } - public short NumberFeatureDataIndices { get; private set; } - } -} \ No newline at end of file diff --git a/EonaCat.HID/Device.cs b/EonaCat.HID/Device.cs deleted file mode 100644 index 793594e..0000000 --- a/EonaCat.HID/Device.cs +++ /dev/null @@ -1,842 +0,0 @@ -using EonaCat.HID.Helpers; -using System; -using System.Runtime.InteropServices; -using System.Threading; -using System.Threading.Tasks; - -namespace EonaCat.HID -{ - // This file is part of the EonaCat project(s) which is released under the Apache License. - // See the LICENSE file or go to https://EonaCat.com/License for full license details. - public class Device : IDevice - { - public event InsertedEventHandler OnInserted; - - public event RemovedEventHandler OnRemoved; - - private const int BUFFER_SIZE = 254; - private readonly string _description; - private readonly string _path; - private readonly Attributes _attributes; - - private readonly Capabilities _capabilities; - private readonly DeviceEventMonitor _deviceEventMonitor; - - private bool _monitorDeviceEvents; - - protected delegate DeviceData ReadDelegate(int timeout); - - protected delegate Report ReadReportDelegate(int timeout); - - private delegate bool WriteDelegate(byte[] data, int timeout); - - private delegate bool WriteReportDelegate(Report report, int timeout); - - internal Device(string devicePath, string description = null) - { - _deviceEventMonitor = new DeviceEventMonitor(this); - _deviceEventMonitor.Inserted += DeviceEventMonitorInserted; - _deviceEventMonitor.Removed += DeviceEventMonitorRemoved; - - _path = devicePath; - _description = description; - - try - { - _deviceModeHelper = new DeviceModeHelper(); - - var handle = OpenDeviceIO(_path, NativeMethods.ACCESS_NONE); - _attributes = GetDeviceAttributes(handle); - _capabilities = GetDeviceCapabilities(handle); - CloseDeviceIO(handle); - } - catch (Exception exception) - { - throw new Exception($"EonaCat HID: Error querying device '{devicePath}'.", exception); - } - } - - public bool IsOpen { get; private set; } - public bool IsConnected - { get { return Devices.IsConnected(_path); } } - public string Description - { get { return _description; } } - public Capabilities Capabilities - { get { return _capabilities; } } - public Attributes Info - { get { return _attributes; } } - public string DevicePath - { get { return _path; } } - - public bool MonitorDeviceEvents - { - get { return _monitorDeviceEvents; } - set - { - if (value & !_monitorDeviceEvents) - { - _deviceEventMonitor.Init(); - } - - _monitorDeviceEvents = value; - } - } - - private DeviceModeHelper _deviceModeHelper { get; } - - public override string ToString() - { - return $"VendorID={_attributes.VendorIdHex}, ProductID={_attributes.ProductIdHex}, Version={_attributes.Version}, DevicePath={_path}"; - } - - public void OpenDevice() - { - OpenDevice(DeviceMode.NonOverlapped, DeviceMode.NonOverlapped, ShareMode.ShareRead | ShareMode.ShareWrite); - } - - public void OpenDevice(DeviceMode readMode, DeviceMode writeMode, ShareMode shareMode) - { - if (IsOpen) - { - return; - } - - _deviceModeHelper.ReadMode = readMode; - _deviceModeHelper.WriteMode = writeMode; - _deviceModeHelper.ShareMode = shareMode; - - try - { - _deviceModeHelper.ReadHandle = OpenDeviceIO(_path, readMode, NativeMethods.GENERIC_READ, shareMode); - _deviceModeHelper.WriteHandle = OpenDeviceIO(_path, writeMode, NativeMethods.GENERIC_WRITE, shareMode); - } - catch (Exception exception) - { - IsOpen = false; - throw new Exception("EonaCat HID: Error opening device.", exception); - } - - IsOpen = (_deviceModeHelper.ReadHandle.ToInt32() != NativeMethods.INVALID_HANDLE_VALUE && _deviceModeHelper.WriteHandle.ToInt32() != NativeMethods.INVALID_HANDLE_VALUE); - } - - public void CloseDevice() - { - if (!IsOpen) - { - return; - } - - CloseDeviceIO(_deviceModeHelper.ReadHandle); - CloseDeviceIO(_deviceModeHelper.WriteHandle); - IsOpen = false; - } - - public DeviceData Read() - { - return Read(0); - } - - public DeviceData Read(int timeout) - { - if (IsConnected) - { - if (!IsOpen) - { - OpenDevice(_deviceModeHelper.ReadMode, _deviceModeHelper.WriteMode, _deviceModeHelper.ShareMode); - } - - try - { - return ReadData(timeout); - } - catch - { - return new DeviceData(DeviceData.ReadStatus.ReadError); - } - } - return new DeviceData(DeviceData.ReadStatus.NotConnected); - } - - public void Read(ReadCallback callback) - { - Read(callback, 0); - } - - public void Read(ReadCallback callback, int timeout) - { - var readDelegate = new ReadDelegate(Read); - var asyncState = new StateAsync(readDelegate, callback); - readDelegate.BeginInvoke(timeout, EndRead, asyncState); - } - - public async Task ReadAsync(int timeout = 0) - { - var readDelegate = new ReadDelegate(Read); -#if NET20 || NET35 || NET5_0_OR_GREATER - return await Task.Factory.StartNew(() => readDelegate.Invoke(timeout)); -#else - return await Task.Factory.FromAsync(readDelegate.BeginInvoke, readDelegate.EndInvoke, timeout, null); -#endif - } - - public Report ReadReport(int timeout = 0) - { - return new Report(Capabilities.InputReportByteLength, Read(timeout)); - } - - public void ReadReport(ReadReportCallback callback, int timeout = 0) - { - var readReportDelegate = new ReadReportDelegate(ReadReport); - var asyncState = new StateAsync(readReportDelegate, callback); - readReportDelegate.BeginInvoke(timeout, EndReadReport, asyncState); - } - - public async Task ReadReportAsync(int timeout = 0) - { - var readReportDelegate = new ReadReportDelegate(ReadReport); -#if NET20 || NET35 || NET5_0_OR_GREATER - return await Task.Factory.StartNew(() => readReportDelegate.Invoke(timeout)); -#else - return await Task.Factory.FromAsync(readReportDelegate.BeginInvoke, readReportDelegate.EndInvoke, timeout, null); -#endif - } - - public Report ReadReportSync(byte reportId) - { - byte[] commandBuffer = new byte[Capabilities.InputReportByteLength]; - commandBuffer[0] = reportId; - bool bSuccess = NativeMethods.HidD_GetInputReport(_deviceModeHelper.ReadHandle, commandBuffer, commandBuffer.Length); - DeviceData deviceData = new DeviceData(commandBuffer, bSuccess ? DeviceData.ReadStatus.Success : DeviceData.ReadStatus.NoDataRead); - return new Report(Capabilities.InputReportByteLength, deviceData); - } - - public bool ReadFeatureData(out byte[] data, byte reportId = 0) - { - if (_capabilities.FeatureReportByteLength <= 0) - { - data = new byte[0]; - return false; - } - - data = new byte[_capabilities.FeatureReportByteLength]; - - var buffer = CreateFeatureOutputBuffer(); - buffer[0] = reportId; - - IntPtr hidHandle = IntPtr.Zero; - bool success = false; - try - { - if (IsOpen) - { - hidHandle = _deviceModeHelper.ReadHandle; - } - else - { - hidHandle = OpenDeviceIO(_path, NativeMethods.ACCESS_NONE); - } - - success = NativeMethods.HidD_GetFeature(hidHandle, buffer, buffer.Length); - - if (success) - { - Array.Copy(buffer, 0, data, 0, Math.Min(data.Length, _capabilities.FeatureReportByteLength)); - } - } - catch (Exception exception) - { - throw new Exception(string.Format("EonaCat HID: Error accessing device '{0}'.", _path), exception); - } - finally - { - if (hidHandle != IntPtr.Zero && hidHandle != _deviceModeHelper.ReadHandle) - { - CloseDeviceIO(hidHandle); - } - } - - return success; - } - - public string ReadProductName() - { - if (ReadProductName(out byte[] bytes)) - { - return ByteHelper.GetFilledBytesString(bytes); - } - return string.Empty; - } - - public bool ReadProductName(out byte[] data) - { - data = new byte[BUFFER_SIZE]; - IntPtr handle = IntPtr.Zero; - bool success = false; - try - { - if (IsOpen) - { - handle = _deviceModeHelper.ReadHandle; - } - else - { - handle = OpenDeviceIO(_path, NativeMethods.ACCESS_NONE); - } - - success = NativeMethods.HidD_GetProductString(handle, data, data.Length); - } - catch (Exception exception) - { - throw new Exception(string.Format("EonaCat HID: Error accessing device '{0}'.", _path), exception); - } - finally - { - if (handle != IntPtr.Zero && handle != _deviceModeHelper.ReadHandle) - { - CloseDeviceIO(handle); - } - } - - return success; - } - - public string ReadManufacturer() - { - if (ReadManufacturer(out byte[] bytes)) - { - return ByteHelper.GetFilledBytesString(bytes); - } - return string.Empty; - } - - public bool ReadManufacturer(out byte[] data) - { - data = new byte[BUFFER_SIZE]; - IntPtr handle = IntPtr.Zero; - bool success = false; - try - { - if (IsOpen) - { - handle = _deviceModeHelper.ReadHandle; - } - else - { - handle = OpenDeviceIO(_path, NativeMethods.ACCESS_NONE); - } - - success = NativeMethods.HidD_GetManufacturerString(handle, data, data.Length); - } - catch (Exception exception) - { - throw new Exception(string.Format("EonaCat HID: Error accessing device '{0}'.", _path), exception); - } - finally - { - if (handle != IntPtr.Zero && handle != _deviceModeHelper.ReadHandle) - { - CloseDeviceIO(handle); - } - } - - return success; - } - - public string ReadSerialNumber() - { - if (ReadSerialNumber(out byte[] bytes)) - { - return ByteHelper.GetFilledBytesString(bytes); - } - return string.Empty; - } - - public bool ReadSerialNumber(out byte[] data) - { - data = new byte[BUFFER_SIZE]; - IntPtr hidHandle = IntPtr.Zero; - bool success = false; - try - { - if (IsOpen) - { - hidHandle = _deviceModeHelper.ReadHandle; - } - else - { - hidHandle = OpenDeviceIO(_path, NativeMethods.ACCESS_NONE); - } - - success = NativeMethods.HidD_GetSerialNumberString(hidHandle, data, data.Length); - } - catch (Exception exception) - { - throw new Exception(string.Format("EonaCat HID: Error accessing device '{0}'.", _path), exception); - } - finally - { - if (hidHandle != IntPtr.Zero && hidHandle != _deviceModeHelper.ReadHandle) - { - CloseDeviceIO(hidHandle); - } - } - - return success; - } - - public bool Write(byte[] data) - { - return Write(data, 0); - } - - public bool Write(byte[] data, int timeout) - { - if (IsConnected) - { - if (!IsOpen) - { - OpenDevice(_deviceModeHelper.ReadMode, _deviceModeHelper.WriteMode, _deviceModeHelper.ShareMode); - } - - try - { - return WriteData(data, timeout); - } - catch - { - return false; - } - } - return false; - } - - public void Write(byte[] data, WriteCallback callback) - { - Write(data, callback, 0); - } - - public void Write(byte[] data, WriteCallback callback, int timeout) - { - var writeDelegate = new WriteDelegate(Write); - var asyncState = new StateAsync(writeDelegate, callback); - writeDelegate.BeginInvoke(data, timeout, EndWrite, asyncState); - } - - public async Task WriteAsync(byte[] data, int timeout = 0) - { - var writeDelegate = new WriteDelegate(Write); -#if NET20 || NET35 || NET5_0_OR_GREATER - return await Task.Factory.StartNew(() => writeDelegate.Invoke(data, timeout)); -#else - return await Task.Factory.FromAsync(writeDelegate.BeginInvoke, writeDelegate.EndInvoke, data, timeout, null); -#endif - } - - public bool WriteReport(Report report) - { - return WriteReport(report, 0); - } - - public bool WriteReport(Report report, int timeout) - { - return Write(report.GetBytes(), timeout); - } - - public void WriteReport(Report report, WriteCallback callback) - { - WriteReport(report, callback, 0); - } - - public void WriteReport(Report report, WriteCallback callback, int timeout) - { - var writeReportDelegate = new WriteReportDelegate(WriteReport); - var asyncState = new StateAsync(writeReportDelegate, callback); - writeReportDelegate.BeginInvoke(report, timeout, EndWriteReport, asyncState); - } - - /// - /// Handle data transfers on the control channel. - /// This method places data on the control channel for devices - /// that do not support the interupt transfers - /// - /// The outbound HID report - /// The result of the tranfer request: true if successful otherwise false - /// - public bool WriteReportSync(Report report) - { - if (null != report) - { - byte[] buffer = report.GetBytes(); - return (NativeMethods.HidD_SetOutputReport(_deviceModeHelper.WriteHandle, buffer, buffer.Length)); - } - else - { - throw new ArgumentException("The output report is null, it must be allocated before you call this method", "report"); - } - } - - public async Task WriteReportAsync(Report report, int timeout = 0) - { - var writeReportDelegate = new WriteReportDelegate(WriteReport); -#if NET20 || NET35 || NET5_0_OR_GREATER - return await Task.Factory.StartNew(() => writeReportDelegate.Invoke(report, timeout)); -#else - return await Task.Factory.FromAsync(writeReportDelegate.BeginInvoke, writeReportDelegate.EndInvoke, report, timeout, null); -#endif - } - - public Report CreateReport() - { - return new Report(Capabilities.OutputReportByteLength); - } - - public bool WriteFeatureData(byte[] data) - { - if (_capabilities.FeatureReportByteLength <= 0) - { - return false; - } - - var buffer = CreateFeatureOutputBuffer(); - - Array.Copy(data, 0, buffer, 0, Math.Min(data.Length, _capabilities.FeatureReportByteLength)); - - IntPtr hidHandle = IntPtr.Zero; - bool success = false; - try - { - if (IsOpen) - { - hidHandle = _deviceModeHelper.WriteHandle; - } - else - { - hidHandle = OpenDeviceIO(_path, NativeMethods.ACCESS_NONE); - } - - //var overlapped = new NativeOverlapped(); - success = NativeMethods.HidD_SetFeature(hidHandle, buffer, buffer.Length); - } - catch (Exception exception) - { - throw new Exception(string.Format("EonaCat HID: Error accessing device '{0}'.", _path), exception); - } - finally - { - if (hidHandle != IntPtr.Zero && hidHandle != _deviceModeHelper.WriteHandle) - { - CloseDeviceIO(hidHandle); - } - } - return success; - } - - protected static void EndRead(IAsyncResult ar) - { - var hidAsyncState = (StateAsync)ar.AsyncState; - var callerDelegate = (ReadDelegate)hidAsyncState.CallerDelegate; - var callbackDelegate = (ReadCallback)hidAsyncState.CallbackDelegate; - var data = callerDelegate.EndInvoke(ar); - - if ((callbackDelegate != null)) - { - callbackDelegate.Invoke(data); - } - } - - protected static void EndReadReport(IAsyncResult ar) - { - var hidAsyncState = (StateAsync)ar.AsyncState; - var callerDelegate = (ReadReportDelegate)hidAsyncState.CallerDelegate; - var callbackDelegate = (ReadReportCallback)hidAsyncState.CallbackDelegate; - var report = callerDelegate.EndInvoke(ar); - - if ((callbackDelegate != null)) - { - callbackDelegate.Invoke(report); - } - } - - private static void EndWrite(IAsyncResult ar) - { - var hidAsyncState = (StateAsync)ar.AsyncState; - var callerDelegate = (WriteDelegate)hidAsyncState.CallerDelegate; - var callbackDelegate = (WriteCallback)hidAsyncState.CallbackDelegate; - var result = callerDelegate.EndInvoke(ar); - - if ((callbackDelegate != null)) - { - callbackDelegate.Invoke(result); - } - } - - private static void EndWriteReport(IAsyncResult ar) - { - var hidAsyncState = (StateAsync)ar.AsyncState; - var callerDelegate = (WriteReportDelegate)hidAsyncState.CallerDelegate; - var callbackDelegate = (WriteCallback)hidAsyncState.CallbackDelegate; - var result = callerDelegate.EndInvoke(ar); - - if ((callbackDelegate != null)) - { - callbackDelegate.Invoke(result); - } - } - - private byte[] CreateInputBuffer() - { - return CreateBuffer(Capabilities.InputReportByteLength - 1); - } - - private byte[] CreateOutputBuffer() - { - return CreateBuffer(Capabilities.OutputReportByteLength - 1); - } - - private byte[] CreateFeatureOutputBuffer() - { - return CreateBuffer(Capabilities.FeatureReportByteLength - 1); - } - - private static byte[] CreateBuffer(int length) - { - byte[] buffer = null; - Array.Resize(ref buffer, length + 1); - return buffer; - } - - private static Attributes GetDeviceAttributes(IntPtr hidHandle) - { - var deviceAttributes = default(NativeMethods.HIDD_ATTRIBUTES); - deviceAttributes.Size = Marshal.SizeOf(deviceAttributes); - NativeMethods.HidD_GetAttributes(hidHandle, ref deviceAttributes); - return new Attributes(deviceAttributes); - } - - private static Capabilities GetDeviceCapabilities(IntPtr hidHandle) - { - var capabilities = default(NativeMethods.Capabilities); - var preparsedDataPointer = default(IntPtr); - - if (NativeMethods.HidD_GetPreparsedData(hidHandle, ref preparsedDataPointer)) - { - NativeMethods.HidP_GetCaps(preparsedDataPointer, ref capabilities); - NativeMethods.HidD_FreePreparsedData(preparsedDataPointer); - } - return new Capabilities(capabilities); - } - - private bool WriteData(byte[] data, int timeout) - { - if (_capabilities.OutputReportByteLength <= 0) - { - return false; - } - - var buffer = CreateOutputBuffer(); - uint bytesWritten; - - Array.Copy(data, 0, buffer, 0, Math.Min(data.Length, _capabilities.OutputReportByteLength)); - - if (_deviceModeHelper.WriteMode == DeviceMode.Overlapped) - { - var security = new NativeMethods.SECURITY_ATTRIBUTES(); - var overlapped = new NativeOverlapped(); - - var overlapTimeout = timeout <= 0 ? NativeMethods.WAIT_INFINITE : timeout; - - security.lpSecurityDescriptor = IntPtr.Zero; - security.bInheritHandle = true; - security.nLength = Marshal.SizeOf(security); - - overlapped.OffsetLow = 0; - overlapped.OffsetHigh = 0; - overlapped.EventHandle = NativeMethods.CreateEvent(ref security, Convert.ToInt32(false), Convert.ToInt32(true), ""); - - try - { - NativeMethods.WriteFile(_deviceModeHelper.WriteHandle, buffer, (uint)buffer.Length, out bytesWritten, ref overlapped); - } - catch { return false; } - - var result = NativeMethods.WaitForSingleObject(overlapped.EventHandle, overlapTimeout); - - switch (result) - { - case NativeMethods.WAIT_OBJECT_0: - return true; - - case NativeMethods.WAIT_TIMEOUT: - return false; - - case NativeMethods.WAIT_FAILED: - return false; - - default: - return false; - } - } - else - { - try - { - var overlapped = new NativeOverlapped(); - return NativeMethods.WriteFile(_deviceModeHelper.WriteHandle, buffer, (uint)buffer.Length, out bytesWritten, ref overlapped); - } - catch { return false; } - } - } - - protected DeviceData ReadData(int timeout) - { - var buffer = new byte[] { }; - var status = DeviceData.ReadStatus.NoDataRead; - IntPtr nonManagedBuffer; - - if (_capabilities.InputReportByteLength > 0) - { - uint bytesRead; - - buffer = CreateInputBuffer(); - nonManagedBuffer = Marshal.AllocHGlobal(buffer.Length); - - if (_deviceModeHelper.ReadMode == DeviceMode.Overlapped) - { - var security = new NativeMethods.SECURITY_ATTRIBUTES(); - var overlapped = new NativeOverlapped(); - var overlapTimeout = timeout <= 0 ? NativeMethods.WAIT_INFINITE : timeout; - - security.lpSecurityDescriptor = IntPtr.Zero; - security.bInheritHandle = true; - security.nLength = Marshal.SizeOf(security); - - overlapped.OffsetLow = 0; - overlapped.OffsetHigh = 0; - overlapped.EventHandle = NativeMethods.CreateEvent(ref security, Convert.ToInt32(false), Convert.ToInt32(true), string.Empty); - - try - { - var success = NativeMethods.ReadFile(_deviceModeHelper.ReadHandle, nonManagedBuffer, (uint)buffer.Length, out bytesRead, ref overlapped); - - if (success) - { - status = DeviceData.ReadStatus.Success; // No check here to see if bytesRead > 0 . Perhaps not necessary? - } - else - { - var result = NativeMethods.WaitForSingleObject(overlapped.EventHandle, overlapTimeout); - - switch (result) - { - case NativeMethods.WAIT_OBJECT_0: - status = DeviceData.ReadStatus.Success; - NativeMethods.GetOverlappedResult(_deviceModeHelper.ReadHandle, ref overlapped, out bytesRead, false); - break; - - case NativeMethods.WAIT_TIMEOUT: - status = DeviceData.ReadStatus.WaitTimedOut; - buffer = new byte[] { }; - break; - - case NativeMethods.WAIT_FAILED: - status = DeviceData.ReadStatus.WaitFail; - buffer = new byte[] { }; - break; - - default: - status = DeviceData.ReadStatus.NoDataRead; - buffer = new byte[] { }; - break; - } - } - Marshal.Copy(nonManagedBuffer, buffer, 0, (int)bytesRead); - } - catch { status = DeviceData.ReadStatus.ReadError; } - finally - { - CloseDeviceIO(overlapped.EventHandle); - Marshal.FreeHGlobal(nonManagedBuffer); - } - } - else - { - try - { - var overlapped = new NativeOverlapped(); - - NativeMethods.ReadFile(_deviceModeHelper.ReadHandle, nonManagedBuffer, (uint)buffer.Length, out bytesRead, ref overlapped); - status = DeviceData.ReadStatus.Success; - Marshal.Copy(nonManagedBuffer, buffer, 0, (int)bytesRead); - } - catch { status = DeviceData.ReadStatus.ReadError; } - finally { Marshal.FreeHGlobal(nonManagedBuffer); } - } - } - return new DeviceData(buffer, status); - } - - private static IntPtr OpenDeviceIO(string devicePath, uint deviceAccess) - { - return OpenDeviceIO(devicePath, DeviceMode.NonOverlapped, deviceAccess, ShareMode.ShareRead | ShareMode.ShareWrite); - } - - private static IntPtr OpenDeviceIO(string devicePath, DeviceMode deviceMode, uint deviceAccess, ShareMode shareMode) - { - var security = new NativeMethods.SECURITY_ATTRIBUTES(); - var flags = 0; - - if (deviceMode == DeviceMode.Overlapped) - { - flags = NativeMethods.FILE_FLAG_OVERLAPPED; - } - - security.lpSecurityDescriptor = IntPtr.Zero; - security.bInheritHandle = true; - security.nLength = Marshal.SizeOf(security); - - return NativeMethods.CreateFile(devicePath, deviceAccess, (int)shareMode, ref security, NativeMethods.OPEN_EXISTING, flags, hTemplateFile: IntPtr.Zero); - } - - private static void CloseDeviceIO(IntPtr handle) - { - if (Environment.OSVersion.Version.Major > 5) - { - NativeMethods.CancelIoEx(handle, IntPtr.Zero); - } - NativeMethods.CloseHandle(handle); - } - - private void DeviceEventMonitorInserted() - { - if (!IsOpen) - { - OpenDevice(_deviceModeHelper.ReadMode, _deviceModeHelper.WriteMode, _deviceModeHelper.ShareMode); - } - - OnInserted?.Invoke(); - } - - private void DeviceEventMonitorRemoved() - { - if (IsOpen) - { - CloseDevice(); - } - - OnRemoved?.Invoke(); - } - - public void Dispose() - { - if (MonitorDeviceEvents) - { - MonitorDeviceEvents = false; - } - - if (IsOpen) - { - CloseDevice(); - } - } - } -} \ No newline at end of file diff --git a/EonaCat.HID/DeviceData.cs b/EonaCat.HID/DeviceData.cs deleted file mode 100644 index 1904488..0000000 --- a/EonaCat.HID/DeviceData.cs +++ /dev/null @@ -1,32 +0,0 @@ -namespace EonaCat.HID -{ - // This file is part of the EonaCat project(s) which is released under the Apache License. - // See the LICENSE file or go to https://EonaCat.com/License for full license details. - public class DeviceData - { - public enum ReadStatus - { - Success = 0, - WaitTimedOut = 1, - WaitFail = 2, - NoDataRead = 3, - ReadError = 4, - NotConnected = 5 - } - - public DeviceData(ReadStatus status) - { - Data = new byte[] { }; - Status = status; - } - - public DeviceData(byte[] data, ReadStatus status) - { - Data = data; - Status = status; - } - - public byte[] Data { get; private set; } - public ReadStatus Status { get; private set; } - } -} \ No newline at end of file diff --git a/EonaCat.HID/DeviceEventMonitor.cs b/EonaCat.HID/DeviceEventMonitor.cs deleted file mode 100644 index a22574d..0000000 --- a/EonaCat.HID/DeviceEventMonitor.cs +++ /dev/null @@ -1,54 +0,0 @@ -using System.Threading.Tasks; - -namespace EonaCat.HID -{ - // This file is part of the EonaCat project(s) which is released under the Apache License. - // See the LICENSE file or go to https://EonaCat.com/License for full license details. - internal class DeviceEventMonitor - { - public event InsertedEventHandler Inserted; - - public event RemovedEventHandler Removed; - - public delegate void InsertedEventHandler(); - - public delegate void RemovedEventHandler(); - - private readonly Device _device; - private bool _wasConnected; - - public DeviceEventMonitor(Device device) - { - _device = device; - } - - public void Init() - { - Task.Run(EventMonitor); - } - - private async Task EventMonitor() - { - while (_device.MonitorDeviceEvents) - { - var isConnected = _device.IsConnected; - - if (isConnected != _wasConnected) - { - if (isConnected) - { - Inserted?.Invoke(); - } - else - { - Removed?.Invoke(); - } - - _wasConnected = isConnected; - } - - await Task.Delay(100); - } - } - } -} \ No newline at end of file diff --git a/EonaCat.HID/Devices.cs b/EonaCat.HID/Devices.cs deleted file mode 100644 index 31c52d0..0000000 --- a/EonaCat.HID/Devices.cs +++ /dev/null @@ -1,180 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Runtime.InteropServices; - -namespace EonaCat.HID -{ - public class Devices - { - private static Guid _hidClassGuid = Guid.Empty; - - public static bool IsConnected(string devicePath) - { - return EnumerateDevices().Any(x => x.Path == devicePath); - } - - public static Device GetDevice(string devicePath) - { - return Enumerate(devicePath).FirstOrDefault(); - } - - public static IEnumerable Enumerate() - { - return EnumerateDevices().Select(x => new Device(x.Path, x.Description)); - } - - public static IEnumerable Enumerate(string devicePath) - { - return EnumerateDevices().Where(x => x.Path == devicePath).Select(x => new Device(x.Path, x.Description)); - } - - public static IEnumerable Enumerate(int vendorId, params int[] productIds) - { - return EnumerateDevices().Select(x => new Device(x.Path, x.Description)).Where(x => x.Info.VendorId == vendorId && - productIds.Contains(x.Info.ProductId)); - } - - public static IEnumerable Enumerate(int vendorId, int productId, ushort UsagePage) - { - return EnumerateDevices().Select(x => new Device(x.Path, x.Description)).Where(x => x.Info.VendorId == vendorId && - productId == (ushort)x.Info.ProductId && (ushort)x.Capabilities.UsagePage == UsagePage); - } - - public static IEnumerable Enumerate(int vendorId) - { - return EnumerateDevices().Select(x => new Device(x.Path, x.Description)).Where(x => x.Info.VendorId == vendorId); - } - - public static IEnumerable Enumerate(ushort UsagePage) - { - return EnumerateDevices().Select(x => new Device(x.Path, x.Description)).Where(x => (ushort)x.Capabilities.UsagePage == UsagePage); - } - - internal class DeviceInfo - { public string Path { get; set; } public string Description { get; set; } } - - internal static IEnumerable EnumerateDevices() - { - var devices = new List(); - var hidClass = HidClassGuid; - var deviceInfoSet = NativeMethods.SetupDiGetClassDevs(ref hidClass, null, hwndParent: IntPtr.Zero, NativeMethods.DIGCF_PRESENT | NativeMethods.DIGCF_DEVICEINTERFACE); - - if (deviceInfoSet.ToInt64() != NativeMethods.INVALID_HANDLE_VALUE) - { - var deviceInfoData = CreateDeviceInfoData(); - var deviceIndex = 0; - - while (NativeMethods.SetupDiEnumDeviceInfo(deviceInfoSet, deviceIndex, ref deviceInfoData)) - { - deviceIndex += 1; - - var deviceInterfaceData = new NativeMethods.SP_DEVICE_INTERFACE_DATA(); - deviceInterfaceData.cbSize = Marshal.SizeOf(deviceInterfaceData); - var deviceInterfaceIndex = 0; - - while (NativeMethods.SetupDiEnumDeviceInterfaces(deviceInfoSet, ref deviceInfoData, ref hidClass, deviceInterfaceIndex, ref deviceInterfaceData)) - { - deviceInterfaceIndex++; - var devicePath = GetDevicePath(deviceInfoSet, deviceInterfaceData); - var description = GetBusReportedDeviceDescription(deviceInfoSet, ref deviceInfoData) ?? - GetDeviceDescription(deviceInfoSet, ref deviceInfoData); - devices.Add(new DeviceInfo { Path = devicePath, Description = description }); - } - } - NativeMethods.SetupDiDestroyDeviceInfoList(deviceInfoSet); - } - return devices; - } - - private static NativeMethods.SP_DEVINFO_DATA CreateDeviceInfoData() - { - var deviceInfoData = new NativeMethods.SP_DEVINFO_DATA(); - - deviceInfoData.cbSize = Marshal.SizeOf(deviceInfoData); - deviceInfoData.DevInst = 0; - deviceInfoData.ClassGuid = Guid.Empty; - deviceInfoData.Reserved = IntPtr.Zero; - - return deviceInfoData; - } - - private static string GetDevicePath(IntPtr deviceInfoSet, NativeMethods.SP_DEVICE_INTERFACE_DATA deviceInterfaceData) - { - var bufferSize = 0; - var interfaceDetail = new NativeMethods.SP_DEVICE_INTERFACE_DETAIL_DATA { Size = IntPtr.Size == 4 ? 4 + Marshal.SystemDefaultCharSize : 8 }; - - NativeMethods.SetupDiGetDeviceInterfaceDetail(deviceInfoSet, ref deviceInterfaceData, IntPtr.Zero, 0, ref bufferSize, IntPtr.Zero); - - return NativeMethods.SetupDiGetDeviceInterfaceDetail(deviceInfoSet, ref deviceInterfaceData, ref interfaceDetail, bufferSize, ref bufferSize, IntPtr.Zero) ? - interfaceDetail.DevicePath : null; - } - - private static Guid HidClassGuid - { - get - { - if (_hidClassGuid.Equals(Guid.Empty)) - { - NativeMethods.HidD_GetHidGuid(ref _hidClassGuid); - } - - return _hidClassGuid; - } - } - - private static string GetDeviceDescription(IntPtr deviceInfoSet, ref NativeMethods.SP_DEVINFO_DATA devinfoData) - { - unsafe - { - const int charCount = 1024; - var descriptionBuffer = stackalloc char[charCount]; - - var requiredSize = 0; - var type = 0; - - if (NativeMethods.SetupDiGetDeviceRegistryProperty(deviceInfoSet, - ref devinfoData, - NativeMethods.SPDRP_DEVICEDESC, - ref type, - descriptionBuffer, - propertyBufferSize: charCount * sizeof(char), - ref requiredSize)) - { - return new string(descriptionBuffer); - } - - return null; - } - } - - private static string GetBusReportedDeviceDescription(IntPtr deviceInfoSet, ref NativeMethods.SP_DEVINFO_DATA devinfoData) - { - unsafe - { - const int charCount = 1024; - var descriptionBuffer = stackalloc char[charCount]; - - if (Environment.OSVersion.Version.Major > 5) - { - uint propertyType = 0; - var requiredSize = 0; - - if (NativeMethods.SetupDiGetDeviceProperty(deviceInfoSet, - ref devinfoData, - ref NativeMethods.DEVPKEY_Device_BusReportedDeviceDesc, - ref propertyType, - descriptionBuffer, - propertyBufferSize: charCount * sizeof(char), - ref requiredSize, - 0)) - { - return new string(descriptionBuffer); - } - } - - return null; - } - } - } -} \ No newline at end of file diff --git a/EonaCat.HID/Enumerator.cs b/EonaCat.HID/Enumerator.cs deleted file mode 100644 index cc208f4..0000000 --- a/EonaCat.HID/Enumerator.cs +++ /dev/null @@ -1,40 +0,0 @@ -using System.Collections.Generic; -using System.Linq; - -namespace EonaCat.HID -{ - // This file is part of the EonaCat project(s) which is released under the Apache License. - // See the LICENSE file or go to https://EonaCat.com/License for full license details. - public class Enumerator : IEnumerator - { - public bool IsConnected(string devicePath) - { - return Devices.IsConnected(devicePath); - } - - public IDevice GetDevice(string devicePath) - { - return Devices.GetDevice(devicePath); - } - - public IEnumerable Enumerate() - { - return Devices.Enumerate().Select(x => x as IDevice); - } - - public IEnumerable Enumerate(string devicePath) - { - return Devices.Enumerate(devicePath).Select(x => x as IDevice); - } - - public IEnumerable Enumerate(int vendorId, params int[] productIds) - { - return Devices.Enumerate(vendorId, productIds).Select(x => x as IDevice); - } - - public IEnumerable Enumerate(int vendorId) - { - return Devices.Enumerate(vendorId).Select(x => x as IDevice); - } - } -} \ No newline at end of file diff --git a/EonaCat.HID/EonaCat.HID.csproj b/EonaCat.HID/EonaCat.HID.csproj index cb7b965..c7e3a77 100644 --- a/EonaCat.HID/EonaCat.HID.csproj +++ b/EonaCat.HID/EonaCat.HID.csproj @@ -1,12 +1,13 @@  - net45;netstandard2 + net46;net6.0 true EonaCat (Jeroen Saey) Copyright 2024 EonaCat (Jeroen Saey) - + latest EonaCat.HID + 1.0.3 EonaCat.HID EonaCat (Jeroen Saey) HID Devices @@ -19,17 +20,6 @@ README.md - - 1.0.2+{chash:10}.{c:ymd} - true - true - v[0-9]* - true - git - true - true - - True @@ -46,10 +36,13 @@ - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - + + + + + + ..\..\..\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETFramework\v4.8\System.Windows.Forms.dll + diff --git a/EonaCat.HID/EventArguments/HidDataReceivedEventArgs.cs b/EonaCat.HID/EventArguments/HidDataReceivedEventArgs.cs new file mode 100644 index 0000000..41719a5 --- /dev/null +++ b/EonaCat.HID/EventArguments/HidDataReceivedEventArgs.cs @@ -0,0 +1,19 @@ +using System; + +namespace EonaCat.HID.EventArguments +{ + /// + /// Event args for received data + /// + public class HidDataReceivedEventArgs : EventArgs + { + public IHid Device { get; } + public byte[] Data { get; } + + public HidDataReceivedEventArgs(IHid device, byte[] data) + { + Device = device; + Data = data; + } + } +} \ No newline at end of file diff --git a/EonaCat.HID/EventArguments/HidErrorEventArgs.cs b/EonaCat.HID/EventArguments/HidErrorEventArgs.cs new file mode 100644 index 0000000..97c76ab --- /dev/null +++ b/EonaCat.HID/EventArguments/HidErrorEventArgs.cs @@ -0,0 +1,19 @@ +using System; + +namespace EonaCat.HID.EventArguments +{ + /// + /// Event args for error events + /// + public class HidErrorEventArgs : EventArgs + { + public IHid Device { get; } + public Exception Exception { get; } + + public HidErrorEventArgs(IHid device, Exception ex) + { + Device = device; + Exception = ex; + } + } +} \ No newline at end of file diff --git a/EonaCat.HID/EventArguments/HidEventArgs.cs b/EonaCat.HID/EventArguments/HidEventArgs.cs new file mode 100644 index 0000000..bd431de --- /dev/null +++ b/EonaCat.HID/EventArguments/HidEventArgs.cs @@ -0,0 +1,17 @@ +using System; + +namespace EonaCat.HID.EventArguments +{ + /// + /// Event arguments with HID device info. + /// + public class HidEventArgs : EventArgs + { + public IHid Device { get; } + + public HidEventArgs(IHid device) + { + Device = device; + } + } +} \ No newline at end of file diff --git a/EonaCat.HID/Helpers/DeviceModeHelper.cs b/EonaCat.HID/Helpers/DeviceModeHelper.cs deleted file mode 100644 index ccc2a7c..0000000 --- a/EonaCat.HID/Helpers/DeviceModeHelper.cs +++ /dev/null @@ -1,13 +0,0 @@ -using System; - -namespace EonaCat.HID.Helpers -{ - internal class DeviceModeHelper - { - public IntPtr ReadHandle { get; set; } - public IntPtr WriteHandle { get; set; } - public DeviceMode ReadMode { get; set; } = DeviceMode.NonOverlapped; - public DeviceMode WriteMode { get; set; } = DeviceMode.NonOverlapped; - public ShareMode ShareMode = ShareMode.ShareRead | ShareMode.ShareWrite; - } -} \ No newline at end of file diff --git a/EonaCat.HID/HidFactory.cs b/EonaCat.HID/HidFactory.cs new file mode 100644 index 0000000..e416a7a --- /dev/null +++ b/EonaCat.HID/HidFactory.cs @@ -0,0 +1,68 @@ +using System; +using System.Runtime.InteropServices; + +namespace EonaCat.HID +{ + /// + /// Main static factory class to create platform-specific implementations. + /// + public static class HidFactory + { + public static string GetPlatform() + { + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + return "Windows"; + } + + if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) + { + return "macOS"; + } + + if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + { + return "Linux"; + } + + return "Unknown"; + } + + public static bool IsWindows() + { + return RuntimeInformation.IsOSPlatform(OSPlatform.Windows); + } + + public static bool IsLinux() + { + return RuntimeInformation.IsOSPlatform(OSPlatform.Linux); + } + + public static bool IsMacOS() + { + return RuntimeInformation.IsOSPlatform(OSPlatform.OSX); + } + + public static IHidManager CreateDeviceManager() + { + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + return new Managers.Windows.HidManagerWindows(); + } + + if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + { + return new Managers.Linux.HidManagerLinux(); + } + + if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) + { + return new Managers.Mac.HidManagerMac(); + } + else + { + throw new PlatformNotSupportedException("Unsupported platform"); + } + } + } +} \ No newline at end of file diff --git a/EonaCat.HID/HidLinux.cs b/EonaCat.HID/HidLinux.cs new file mode 100644 index 0000000..39e9753 --- /dev/null +++ b/EonaCat.HID/HidLinux.cs @@ -0,0 +1,392 @@ +using EonaCat.HID.EventArguments; +using EonaCat.HID.Managers; +using EonaCat.HID.Managers.Linux; +using System; +using System.Collections.Generic; +using System.IO; +using System.Runtime.InteropServices; +using System.Threading; +using System.Threading.Tasks; + +namespace EonaCat.HID +{ + internal sealed class HidLinux : IHid + { + private readonly string _devicePath; + private FileStream _stream; + private readonly object _lock = new object(); + private CancellationTokenSource _cts; + + public string DevicePath => _devicePath; + public ushort VendorId { get; private set; } + public ushort ProductId { get; private set; } + public string SerialNumber { get; private set; } + public string Manufacturer { get; private set; } + public string ProductName { get; private set; } + public int InputReportByteLength { get; private set; } + public int OutputReportByteLength { get; private set; } + public int FeatureReportByteLength { get; private set; } + public IDictionary Capabilities { get; private set; } = new Dictionary(); + + public event EventHandler OnDataReceived; + public event EventHandler OnError; + + public HidLinux(string devicePath) + { + _devicePath = devicePath ?? throw new ArgumentNullException(nameof(devicePath)); + } + + internal void Setup() + { + Open(); + LoadDeviceInfo(); + LoadReportLengths(); + Close(); + } + + public void Open() + { + lock (_lock) + { + if (_stream != null) + { + return; + } + + // Open device for read/write + var fd = NativeMethods.open(_devicePath, NativeMethods.O_RDWR | NativeMethods.O_NONBLOCK); + if (fd < 0) + { + OnError?.Invoke(this, new HidErrorEventArgs(this, new IOException($"Failed to open device: {_devicePath}"))); + return; + } + + var safeHandle = new Microsoft.Win32.SafeHandles.SafeFileHandle(new IntPtr(fd), ownsHandle: true); + _stream = new FileStream(safeHandle, FileAccess.ReadWrite, bufferSize: 64, isAsync: false); + } + } + + public void Close() + { + lock (_lock) + { + StopListening(); + + if (_stream != null) + { + _stream.Dispose(); + _stream = null; + } + } + } + + public void Dispose() + { + Close(); + } + + private void LoadDeviceInfo() + { + string sysPath = GetSysfsDevicePath(_devicePath); + if (string.IsNullOrEmpty(sysPath)) + { + return; + } + + VendorId = ReadHexFileAsUShort(Path.Combine(sysPath, "idVendor")); + ProductId = ReadHexFileAsUShort(Path.Combine(sysPath, "idProduct")); + SerialNumber = ReadTextFile(Path.Combine(sysPath, "serial")); + Manufacturer = ReadTextFile(Path.Combine(sysPath, "manufacturer")); + ProductName = ReadTextFile(Path.Combine(sysPath, "product")); + } + + private void LoadReportLengths() + { + // As Linux/hidraw interface does not provide direct API for report lengths, + // We fallback to standard max USB HID report size or fixed size + // TODO: Maybe we should use hidapi or libusb bindings? + + InputReportByteLength = 64; // usually max 64 on USB HID + OutputReportByteLength = 64; + FeatureReportByteLength = 64; + } + + private string GetSysfsDevicePath(string devPath) + { + // Mapping /dev/hidrawX to sysfs device path: /sys/class/hidraw/hidrawX/device/ + var devName = Path.GetFileName(devPath); + var sysDevicePath = Path.Combine("/sys/class/hidraw", devName, "device"); + if (Directory.Exists(sysDevicePath)) + { + return sysDevicePath; + } + + return null; + } + + private ushort ReadHexFileAsUShort(string path) + { + try + { + if (File.Exists(path)) + { + string text = File.ReadAllText(path).Trim().ToLowerInvariant(); + if (text.StartsWith("0x")) + { + text = text.Substring(2); + } + + if (ushort.TryParse(text, System.Globalization.NumberStyles.HexNumber, null, out ushort val)) + { + return val; + } + } + } + catch { } + return 0; + } + + private string ReadTextFile(string path) + { + try + { + if (File.Exists(path)) + { + return File.ReadAllText(path).Trim(); + } + } + catch { } + return null; + } + + public async Task WriteOutputReportAsync(byte[] data) + { + if (data == null) + { + OnError?.Invoke(this, new HidErrorEventArgs(this, new ArgumentNullException(nameof(data)))); + return; + } + + if (_stream == null) + { + OnError?.Invoke(this, new HidErrorEventArgs(this, new InvalidOperationException("Device not open"))); + return; + } + + await Task.Run(() => + { + lock (_lock) + { + try + { + _stream.Write(data, 0, data.Length); + _stream.Flush(); + } + catch (Exception ex) + { + OnError?.Invoke(this, new HidErrorEventArgs(this, ex)); + } + } + }); + } + + public async Task ReadInputReportAsync() + { + if (_stream == null) + { + OnError?.Invoke(this, new HidErrorEventArgs(this, new InvalidOperationException("Device not open"))); + return Array.Empty(); + } + + return await Task.Run(() => + { + byte[] buffer = new byte[InputReportByteLength]; + int bytesRead = 0; + + lock (_lock) + { + try + { + bytesRead = _stream.Read(buffer, 0, buffer.Length); + } + catch (Exception ex) + { + OnError?.Invoke(this, new HidErrorEventArgs(this, ex)); + } + } + + if (bytesRead <= 0) + { + return Array.Empty(); + } + + byte[] result = new byte[bytesRead]; + Array.Copy(buffer, result, bytesRead); + return result; + }); + } + + public async Task SendFeatureReportAsync(byte[] data) + { + if (data == null || data.Length == 0) + throw new ArgumentException("Feature report data must not be null or empty."); + + int fd = NativeMethods.open(_devicePath, NativeMethods.O_RDWR); + if (fd < 0) + { + OnError?.Invoke(this, new HidErrorEventArgs(this, new IOException($"Failed to open device: {_devicePath}"))); + return; + } + + try + { + int size = data.Length; + IntPtr buffer = Marshal.AllocHGlobal(size); + Marshal.Copy(data, 0, buffer, size); + + int request = NativeMethods._IOC(NativeMethods._IOC_WRITE, 'H', 0x06, size); // HIDIOCSFEATURE + int result = NativeMethods.ioctl(fd, request, buffer); + if (result < 0) + { + OnError?.Invoke(this, new HidErrorEventArgs(this, new IOException("ioctl HIDIOCSFEATURE failed"))); + return; + } + + Marshal.FreeHGlobal(buffer); + } + finally + { + NativeMethods.close(fd); + } + + await Task.CompletedTask; + } + + public async Task GetFeatureReportAsync(byte reportId) + { + const int maxFeatureSize = 256; + byte[] buffer = new byte[maxFeatureSize]; + buffer[0] = reportId; + + int fd = NativeMethods.open(_devicePath, NativeMethods.O_RDWR); + if (fd < 0) + { + OnError?.Invoke(this, new HidErrorEventArgs(this, new IOException($"Failed to open device: {_devicePath}"))); + return Array.Empty(); + } + + try + { + IntPtr bufPtr = Marshal.AllocHGlobal(buffer.Length); + Marshal.Copy(buffer, 0, bufPtr, buffer.Length); + + int request = NativeMethods._IOC(NativeMethods._IOC_READ, 'H', 0x07, buffer.Length); // HIDIOCGFEATURE + int result = NativeMethods.ioctl(fd, request, bufPtr); + if (result < 0) + { + OnError?.Invoke(this, new HidErrorEventArgs(this, new IOException("ioctl HIDIOCGFEATURE failed"))); + return Array.Empty(); + } + + Marshal.Copy(bufPtr, buffer, 0, buffer.Length); + Marshal.FreeHGlobal(bufPtr); + + return await Task.FromResult(buffer); + } + finally + { + NativeMethods.close(fd); + } + } + + public async Task StartListeningAsync(CancellationToken cancellationToken) + { + if (_stream == null) + { + OnError?.Invoke(this, new HidErrorEventArgs(this, new NotSupportedException("Device is not open."))); + return; + } + + lock (_lock) + { + if (_cts != null) + { + OnError?.Invoke(this, new HidErrorEventArgs(this, new NotSupportedException("Already listening on this device."))); + return; + } + + _cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); + } + + try + { + var token = _cts.Token; + + while (!token.IsCancellationRequested) + { + byte[] buffer = new byte[InputReportByteLength]; + int bytesRead; + + try + { + bytesRead = await _stream.ReadAsync(buffer, 0, buffer.Length, token); + } + catch (OperationCanceledException) + { + break; + } + catch (NotSupportedException) + { + OnError?.Invoke(this, new HidErrorEventArgs(this, new NotSupportedException("Reading input reports is not supported on this device."))); + break; // Exit if reading is not supported + } + catch (Exception ex) + { + OnError?.Invoke(this, new HidErrorEventArgs(this, ex)); + break; + } + + if (bytesRead > 0) + { + var data = new byte[bytesRead]; + Array.Copy(buffer, data, bytesRead); + + OnDataReceived?.Invoke(this, new HidDataReceivedEventArgs(this, data)); + } + else + { + try + { + await Task.Delay(5, token); + } + catch (OperationCanceledException) + { + break; + } + } + } + } + finally + { + lock (_lock) + { + _cts?.Dispose(); + _cts = null; + } + } + } + + private void StopListening() + { + lock (_lock) + { + if (_cts != null) + { + _cts.Cancel(); + _cts.Dispose(); + _cts = null; + } + } + } + } +} \ No newline at end of file diff --git a/EonaCat.HID/HidMac.cs b/EonaCat.HID/HidMac.cs new file mode 100644 index 0000000..1e1cf16 --- /dev/null +++ b/EonaCat.HID/HidMac.cs @@ -0,0 +1,412 @@ +using EonaCat.HID.EventArguments; +using System; +using System.Collections.Generic; +using System.Runtime.InteropServices; +using System.Threading; +using System.Threading.Tasks; + +namespace EonaCat.HID +{ + internal sealed class HidMac : IHid + { + private IntPtr _deviceHandle; + + public HidMac(IntPtr deviceHandle) + { + _deviceHandle = deviceHandle; + CFRetain(_deviceHandle); + } + + internal void Setup() + { + InitializeDeviceInfo(); + } + + public string DevicePath => null; + public ushort VendorId { get; private set; } + public ushort ProductId { get; private set; } + public string SerialNumber { get; private set; } + public string Manufacturer { get; private set; } + public string ProductName { get; private set; } + + public int InputReportByteLength { get; private set; } = 64; + public int OutputReportByteLength { get; private set; } = 64; + public int FeatureReportByteLength { get; private set; } = 64; + + public IDictionary Capabilities { get; private set; } = new Dictionary(); + + public event EventHandler OnDataReceived; + public event EventHandler OnError; + + public void Open() + { + IOHIDDeviceOpen(_deviceHandle, 0); + } + + public void Close() + { + IOHIDDeviceClose(_deviceHandle, 0); + } + + public void Dispose() + { + StopListening(); + Close(); + CFRelease(_deviceHandle); + _deviceHandle = IntPtr.Zero; + } + + public void StopListening() + { + _listeningCts?.Cancel(); + } + + public async Task WriteOutputReportAsync(byte[] data) + { + if (data is null) + { + throw new ArgumentNullException(nameof(data)); + } + + await Task.Run(() => + { + byte reportId = data.Length > 0 ? data[0] : (byte)0; + IntPtr buffer = Marshal.AllocHGlobal(data.Length); + + try + { + Marshal.Copy(data, 0, buffer, data.Length); + int res = IOHIDDeviceSetReport(_deviceHandle, 1, reportId, buffer, data.Length); + if (res != 0) + { + OnError?.Invoke(this, new HidErrorEventArgs(this, new Exception($"IOHIDDeviceSetReport (Output) failed: {res}"))); + return; + } + } + finally + { + Marshal.FreeHGlobal(buffer); + } + }); + } + + public async Task SendFeatureReportAsync(byte[] data) + { + if (data is null) + { + throw new ArgumentNullException(nameof(data)); + } + + await Task.Run(() => + { + byte reportId = data.Length > 0 ? data[0] : (byte)0; + IntPtr buffer = Marshal.AllocHGlobal(data.Length); + + try + { + Marshal.Copy(data, 0, buffer, data.Length); + int res = IOHIDDeviceSetReport(_deviceHandle, 2, reportId, buffer, data.Length); + if (res != 0) + { + OnError?.Invoke(this, new HidErrorEventArgs(this, new Exception($"IOHIDDeviceSetReport (Feature) failed: {res}"))); + return; + } + } + finally + { + Marshal.FreeHGlobal(buffer); + } + }); + } + + public async Task GetFeatureReportAsync(byte reportId) + { + return await Task.Run(() => + { + int length = FeatureReportByteLength; + IntPtr buffer = Marshal.AllocHGlobal(length); + + try + { + int res = IOHIDDeviceGetReport(_deviceHandle, 2, reportId, buffer, ref length); + if (res != 0) + { + OnError?.Invoke(this, new HidErrorEventArgs(this, new Exception($"IOHIDDeviceGetReport (Feature) failed: {res}"))); + return Array.Empty(); + } + + byte[] outBuf = new byte[length]; + Marshal.Copy(buffer, outBuf, 0, length); + return outBuf; + } + finally + { + Marshal.FreeHGlobal(buffer); + } + }); + } + + public Task ReadInputReportAsync() + { + var tcs = new TaskCompletionSource(); + byte[] buffer = new byte[InputReportByteLength]; + + InputReportCallback callback = null; + + callback = (ctx, result, sender, report, reportLength) => + { + byte[] output = new byte[reportLength.ToInt32()]; + Array.Copy(report, output, output.Length); + tcs.TrySetResult(output); + }; + + GCHandle.Alloc(callback); + IOHIDDeviceRegisterInputReportCallback(_deviceHandle, buffer, (IntPtr)buffer.Length, callback, IntPtr.Zero); + + return tcs.Task; + } + + + public Task StartListeningAsync(CancellationToken ct) + { + if (_listeningTask != null && !_listeningTask.IsCompleted) + { + OnError?.Invoke(this, new HidErrorEventArgs(this, new NotSupportedException("Already listening on this device."))); + return null; + } + + _listeningCts = CancellationTokenSource.CreateLinkedTokenSource(ct); + + _listeningTask = Task.Run(() => + { + try + { + byte[] buffer = new byte[InputReportByteLength]; + InputReportCallback callback = (ctx, result, sender, report, reportLength) => + { + byte[] data = new byte[reportLength.ToInt32()]; + Array.Copy(report, data, data.Length); + + OnDataReceived?.Invoke(this, new HidDataReceivedEventArgs(this, data)); + }; + + GCHandle.Alloc(callback); + IOHIDDeviceRegisterInputReportCallback(_deviceHandle, buffer, (IntPtr)buffer.Length, callback, IntPtr.Zero); + IOHIDDeviceScheduleWithRunLoop(_deviceHandle, CFRunLoopGetCurrent(), IntPtr.Zero); + + while (!_listeningCts.Token.IsCancellationRequested) + { + CFRunLoopRun(); + } + + CFRunLoopStop(CFRunLoopGetCurrent()); + } + catch (OperationCanceledException) + { + return; // Exit gracefully if cancellation was requested + } + catch (NotSupportedException) + { + OnError?.Invoke(this, new HidErrorEventArgs(this, new NotSupportedException("Reading input reports is not supported on this device."))); + _listeningCts.Cancel(); + } + catch (Exception ex) + { + // Handle exceptions during reading + if (_listeningCts.IsCancellationRequested) + { + // Exit if cancellation was requested + } + OnError?.Invoke(this, new HidErrorEventArgs(this, ex)); + } + }, _listeningCts.Token); + + return _listeningTask; + } + + private void InitializeDeviceInfo() + { + VendorId = GetIntProperty(kIOHIDVendorIDKey); + ProductId = GetIntProperty(kIOHIDProductIDKey); + Manufacturer = GetStringProperty(kIOHIDManufacturerKey); + ProductName = GetStringProperty(kIOHIDProductKey); + SerialNumber = GetStringProperty(kIOHIDSerialNumberKey); + } + + private enum CFStringEncoding : uint + { + UTF8 = 0x08000100, + } + + private ushort GetIntProperty(string key) + { + var cfKey = CFStringCreateWithCString(IntPtr.Zero, key, CFStringEncoding.UTF8); + IntPtr val = IOHIDDeviceGetProperty(_deviceHandle, cfKey); + CFRelease(cfKey); + + if (val == IntPtr.Zero) + { + return 0; + } + + if (CFGetTypeID(val) == CFNumberGetTypeID()) + { + int i = 0; + bool success = CFNumberGetValue(val, CFNumberType.kCFNumberIntType, out i); + if (success) + { + return (ushort)i; + } + } + + return 0; + } + + private string GetStringProperty(string key) + { + var cfKey = CFStringCreateWithCString(IntPtr.Zero, key, CFStringEncoding.UTF8); + IntPtr val = IOHIDDeviceGetProperty(_deviceHandle, cfKey); + CFRelease(cfKey); + if (val == IntPtr.Zero) + { + return null; + } + + string s = CFStringToString(val); + CFRelease(val); + return s; + } + + private string CFStringToString(IntPtr cfStr) + { + if (cfStr == IntPtr.Zero) + { + return null; + } + + IntPtr ptr = CFStringGetCStringPtr(cfStr, CFStringEncoding.UTF8); + if (ptr != IntPtr.Zero) + { + // Manually read null-terminated UTF8 string from pointer + return PtrToStringUTF8Manual(ptr); + } + + int len = CFStringGetLength(cfStr); + int max = CFStringGetMaximumSizeForEncoding(len, CFStringEncoding.UTF8) + 1; + byte[] buf = new byte[max]; + + bool success = CFStringGetCString(cfStr, buf, max, CFStringEncoding.UTF8); + if (success) + { + return System.Text.Encoding.UTF8.GetString(buf).TrimEnd('\0'); + } + + return null; + } + + private string PtrToStringUTF8Manual(IntPtr ptr) + { + if (ptr == IntPtr.Zero) + { + return null; + } + + // Find length of null-terminated string + int len = 0; + while (Marshal.ReadByte(ptr, len) != 0) + { + len++; + } + + if (len == 0) + { + return string.Empty; + } + + byte[] buffer = new byte[len]; + Marshal.Copy(ptr, buffer, 0, len); + return System.Text.Encoding.UTF8.GetString(buffer); + } + + // Constants for IOHIDDevice properties + private const string kIOHIDVendorIDKey = "VendorID"; + private const string kIOHIDProductIDKey = "ProductID"; + private const string kIOHIDManufacturerKey = "Manufacturer"; + private const string kIOHIDProductKey = "Product"; + private const string kIOHIDSerialNumberKey = "SerialNumber"; + + [DllImport("/System/Library/Frameworks/IOKit.framework/IOKit")] + static extern IntPtr IOHIDDeviceGetProperty(IntPtr dev, IntPtr key); + [DllImport("/System/Library/Frameworks/IOKit.framework/IOKit")] + static extern uint IOHIDDeviceOpen(IntPtr dev, uint opt); + [DllImport("/System/Library/Frameworks/IOKit.framework/IOKit")] + static extern uint IOHIDDeviceClose(IntPtr dev, uint opt); + [DllImport("/System/Library/Frameworks/IOKit.framework/IOKit")] + static extern int IOHIDDeviceSetReport(IntPtr dev, int type, byte id, IntPtr buf, int len); + [DllImport("/System/Library/Frameworks/IOKit.framework/IOKit")] + static extern int IOHIDDeviceGetReport(IntPtr dev, int type, byte id, IntPtr buf, ref int len); + + [DllImport("/System/Library/Frameworks/CoreFoundation.framework/CoreFoundation")] + static extern IntPtr CFStringCreateWithCString(IntPtr alloc, string s, CFStringEncoding enc); + [DllImport("/System/Library/Frameworks/CoreFoundation.framework/CoreFoundation")] + static extern void CFRelease(IntPtr cf); + [DllImport("/System/Library/Frameworks/CoreFoundation.framework/CoreFoundation")] + static extern IntPtr CFRetain(IntPtr cf); + [DllImport("/System/Library/Frameworks/CoreFoundation.framework/CoreFoundation")] + static extern IntPtr CFStringGetCStringPtr(IntPtr str, CFStringEncoding enc); + [DllImport("/System/Library/Frameworks/CoreFoundation.framework/CoreFoundation")] + static extern int CFStringGetLength(IntPtr str); + [DllImport("/System/Library/Frameworks/CoreFoundation.framework/CoreFoundation")] + static extern int CFStringGetMaximumSizeForEncoding(int len, CFStringEncoding enc); + [DllImport("/System/Library/Frameworks/CoreFoundation.framework/CoreFoundation")] + static extern bool CFStringGetCString(IntPtr str, byte[] buf, int bufSize, CFStringEncoding enc); + + [DllImport("/System/Library/Frameworks/CoreFoundation.framework/CoreFoundation")] + static extern IntPtr CFGetTypeID(IntPtr cf); + [DllImport("/System/Library/Frameworks/CoreFoundation.framework/CoreFoundation")] + static extern IntPtr CFNumberGetTypeID(); + [DllImport("/System/Library/Frameworks/CoreFoundation.framework/CoreFoundation")] + static extern bool CFNumberGetValue(IntPtr num, CFNumberType type, out int value); + + [DllImport("/System/Library/Frameworks/IOKit.framework/IOKit")] + private static extern void IOHIDDeviceRegisterInputReportCallback( + IntPtr device, + byte[] report, + IntPtr reportLength, + InputReportCallback callback, + IntPtr context); + + [DllImport("/System/Library/Frameworks/IOKit.framework/IOKit")] + private static extern void IOHIDDeviceScheduleWithRunLoop( + IntPtr device, + IntPtr runLoop, + IntPtr runLoopMode); + + [DllImport("/System/Library/Frameworks/CoreFoundation.framework/CoreFoundation")] + private static extern IntPtr CFRunLoopGetCurrent(); + + [DllImport("/System/Library/Frameworks/CoreFoundation.framework/CoreFoundation")] + private static extern void CFRunLoopRun(); + + [DllImport("/System/Library/Frameworks/CoreFoundation.framework/CoreFoundation")] + private static extern void CFRunLoopStop(IntPtr runLoop); + + // Input report callback delegate + private delegate void InputReportCallback( + IntPtr context, + IntPtr result, + IntPtr sender, + byte[] report, + IntPtr reportLength); + + // For managing lifetime of listening loop + private CancellationTokenSource _listeningCts; + private Task _listeningTask; + + + private enum CFNumberType : int + { + kCFNumberIntType = 9 + } + } +} diff --git a/EonaCat.HID/HidWindows.cs b/EonaCat.HID/HidWindows.cs new file mode 100644 index 0000000..5043638 --- /dev/null +++ b/EonaCat.HID/HidWindows.cs @@ -0,0 +1,461 @@ +using EonaCat.HID.EventArguments; +using EonaCat.HID.Managers.Windows; +using Microsoft.Win32.SafeHandles; +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.IO; +using System.Linq; +using System.Runtime.InteropServices; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using static EonaCat.HID.Managers.Windows.NativeMethods; + +namespace EonaCat.HID +{ + internal sealed class HidWindows : IHid + { + private SafeFileHandle _deviceHandle; + private FileStream _deviceStream; + private IntPtr _preparsedData; + + private bool _isOpen; + private readonly string _devicePath; + + public string DevicePath => _devicePath; + public ushort VendorId { get; private set; } + public ushort ProductId { get; private set; } + public string SerialNumber { get; private set; } + public string Manufacturer { get; private set; } + public string ProductName { get; private set; } + public int InputReportByteLength { get; private set; } + public int OutputReportByteLength { get; private set; } + public int FeatureReportByteLength { get; private set; } + + public IDictionary Capabilities { get; } = new Dictionary(); + + public event EventHandler OnDataReceived; + public event EventHandler OnError; + + private CancellationTokenSource _listeningCts; + + private readonly object _lockObject = new object(); + + public HidWindows(string devicePath) + { + _devicePath = devicePath ?? throw new ArgumentNullException(nameof(devicePath)); + } + + internal void Setup() + { + Open(); + LoadDeviceAttributes(); + Close(); + } + + private void LoadDeviceAttributes() + { + Manufacturer = GetStringDescriptor(HidD_GetManufacturerString); + ProductName = GetStringDescriptor(HidD_GetProductString); + SerialNumber = GetStringDescriptor(HidD_GetSerialNumberString); + HidDeviceAttributes attr = GetDeviceAttributes(); + VendorId = attr.VendorID; + ProductId = attr.ProductID; + } + + /// + /// Open the device for I/O + /// + public void Open() + { + if (_isOpen) + return; + + FileAccess access = FileAccess.ReadWrite; + SafeFileHandle handle = TryOpenDevice(GENERIC_READ | GENERIC_WRITE); + + if (handle == null || handle.IsInvalid) + { + handle = TryOpenDevice(GENERIC_READ); + if (handle != null && !handle.IsInvalid) + access = FileAccess.Read; + } + + if ((handle == null || handle.IsInvalid) && Environment.Is64BitOperatingSystem) + { + handle = TryOpenDevice(GENERIC_WRITE); + if (handle != null && !handle.IsInvalid) + access = FileAccess.Write; + } + + if (handle == null || handle.IsInvalid) + { + int err = Marshal.GetLastWin32Error(); + OnError?.Invoke(this, new HidErrorEventArgs(this, new Win32Exception(err, $"Cannot open device {_devicePath} with any access mode"))); + return; + } + + _deviceHandle = handle; + _deviceStream = new FileStream(_deviceHandle, access, 64, true); + _isOpen = true; + + // HID descriptor parsing + if (!HidD_GetPreparsedData(_deviceHandle.DangerousGetHandle(), out _preparsedData)) + throw new Win32Exception(Marshal.GetLastWin32Error(), "Failed HidD_GetPreparsedData"); + + HIDP_CAPS caps; + int res = HidP_GetCaps(_preparsedData, out caps); + if (res == 0) + throw new Win32Exception(res, "Failed HidP_GetCaps"); + + InputReportByteLength = caps.InputReportByteLength; + OutputReportByteLength = caps.OutputReportByteLength; + FeatureReportByteLength = caps.FeatureReportByteLength; + + Capabilities["Usage"] = caps.Usage; + Capabilities["UsagePage"] = caps.UsagePage; + Capabilities["InputReportByteLength"] = InputReportByteLength; + Capabilities["OutputReportByteLength"] = OutputReportByteLength; + Capabilities["FeatureReportByteLength"] = FeatureReportByteLength; + + Manufacturer = GetStringDescriptor(HidD_GetManufacturerString); + ProductName = GetStringDescriptor(HidD_GetProductString); + SerialNumber = GetStringDescriptor(HidD_GetSerialNumberString); + + HidDeviceAttributes attr = GetDeviceAttributes(); + VendorId = attr.VendorID; + ProductId = attr.ProductID; + } + + + private SafeFileHandle TryOpenDevice(int access) + { + var handle = CreateFile(_devicePath, + access, + FILE_SHARE_READ | FILE_SHARE_WRITE, + IntPtr.Zero, + OPEN_EXISTING, + FILE_FLAG_OVERLAPPED, + IntPtr.Zero); + + return handle; + } + + private bool CanRead(SafeFileHandle handle) + { + try + { + using (var stream = new FileStream(handle, FileAccess.Read, 1, true)) + { + byte[] test = new byte[1]; + stream.Read(test, 0, 1); + return true; + } + } + catch { return false; } + } + + private bool CanWrite(SafeFileHandle handle) + { + try + { + using (var stream = new FileStream(handle, FileAccess.Write, 1, true)) + { + byte[] test = new byte[1]; + stream.Write(test, 0, 1); + return true; + } + } + catch { return false; } + } + + /// + /// Close the device + /// + public void Close() + { + lock (_lockObject) + { + if (!_isOpen) + { + return; + } + + StopListening(); + + if (_preparsedData != IntPtr.Zero) + { + HidD_FreePreparsedData(_preparsedData); + _preparsedData = IntPtr.Zero; + } + + _deviceStream?.Dispose(); + _deviceStream = null; + + _deviceHandle?.Dispose(); + _deviceHandle = null; + + _isOpen = false; + } + } + private HidDeviceAttributes GetDeviceAttributes() + { + HidDeviceAttributes attr = new HidDeviceAttributes + { + Size = Marshal.SizeOf() + }; + if (!HidD_GetAttributes(_deviceHandle.DangerousGetHandle(), ref attr)) + { + throw new Win32Exception(Marshal.GetLastWin32Error(), "Failed HidD_GetAttributes"); + } + return attr; + } + + private string GetStringDescriptor(Func stringFunc) + { + var buffer = new byte[126 * 2]; // Unicode max buffer + GCHandle gc = GCHandle.Alloc(buffer, GCHandleType.Pinned); + try + { + bool success = stringFunc(_deviceHandle.DangerousGetHandle(), gc.AddrOfPinnedObject(), buffer.Length); + if (!success) + { + return null; + } + + string str = Encoding.Unicode.GetString(buffer); + int idx = str.IndexOf('\0'); + if (idx >= 0) + { + str = str.Substring(0, idx); + } + + return str; + } + finally + { + gc.Free(); + } + } + + public void Dispose() + { + Close(); + } + + public async Task WriteOutputReportAsync(byte[] data) + { + if (!_isOpen) + { + OnError?.Invoke(this, new HidErrorEventArgs(this, new InvalidOperationException("Device not open"))); + return; + } + + if (data == null || data.Length == 0) + { + OnError?.Invoke(this, new HidErrorEventArgs(this, new ArgumentNullException(nameof(data), "Data cannot be null or empty"))); + return; + } + + try + { + await _deviceStream.WriteAsync(data, 0, data.Length); + await _deviceStream.FlushAsync(); + } + catch (Exception ex) + { + OnError?.Invoke(this, new HidErrorEventArgs(this, ex)); + throw; + } + } + + public async Task ReadInputReportAsync() + { + if (!_isOpen) + { + OnError?.Invoke(this, new HidErrorEventArgs(this, new InvalidOperationException("Device not open"))); + return Array.Empty(); + } + + return await Task.Run(async () => + { + var buffer = new byte[InputReportByteLength]; + + try + { + int read = await _deviceStream.ReadAsync(buffer, 0, buffer.Length); + if (read == 0) + { + OnError?.Invoke(this, new HidErrorEventArgs(this, new IOException("No data read from device"))); + return Array.Empty(); + } + + return buffer.Take(read).ToArray(); + } + catch (Exception ex) + { + OnError?.Invoke(this, new HidErrorEventArgs(this, ex)); + throw; + } + }); + } + + public async Task SendFeatureReportAsync(byte[] data) + { + if (!_isOpen) + { + OnError?.Invoke(this, new HidErrorEventArgs(this, new InvalidOperationException("Device not open"))); + return; + } + + if (data == null || data.Length == 0) + { + throw new ArgumentNullException(nameof(data)); + } + + await Task.Run(() => + { + bool success = HidD_SetFeature(_deviceHandle.DangerousGetHandle(), data, data.Length); + if (!success) + { + var err = Marshal.GetLastWin32Error(); + var ex = new Win32Exception(err, "HidD_SetFeature failed"); + OnError?.Invoke(this, new HidErrorEventArgs(this, ex)); + throw ex; + } + }); + } + + public async Task GetFeatureReportAsync(byte reportId) + { + if (!_isOpen) + { + OnError?.Invoke(this, new HidErrorEventArgs(this, new InvalidOperationException("Device not open"))); + return Array.Empty(); + } + + return await Task.Run(() => + { + var buffer = new byte[FeatureReportByteLength]; + buffer[0] = reportId; + + bool success = HidD_GetFeature(_deviceHandle.DangerousGetHandle(), buffer, buffer.Length); + if (!success) + { + var err = Marshal.GetLastWin32Error(); + var ex = new Win32Exception(err, "HidD_GetFeature failed"); + OnError?.Invoke(this, new HidErrorEventArgs(this, ex)); + return Array.Empty(); + } + + return buffer; + }); + } + + /// + /// Begin async reading loop raising OnDataReceived events on data input + /// + /// + /// + public async Task StartListeningAsync(CancellationToken cancellationToken) + { + if (!_isOpen) + { + OnError?.Invoke(this, new HidErrorEventArgs(this, new NotSupportedException("Device is not open."))); + return; + } + + if (_listeningCts != null) + { + OnError?.Invoke(this, new HidErrorEventArgs(this, new NotSupportedException("Already listening on this device."))); + return; + } + + _listeningCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); + + try + { + while (!_listeningCts.Token.IsCancellationRequested) + { + try + { + byte[] data = await ReadInputReportAsync(_listeningCts.Token); + + if (data != null && data.Length > 0) + { + OnDataReceived?.Invoke(this, new HidDataReceivedEventArgs(this, data)); + } + } + catch (OperationCanceledException) + { + break; + } + catch (NotSupportedException) + { + OnError?.Invoke(this, new HidErrorEventArgs(this, new NotSupportedException("Reading input reports is not supported on this device."))); + break; // Exit if reading is not supported + } + catch (Exception ex) + { + // Handle exceptions during reading + if (_listeningCts.IsCancellationRequested) + { + break; // Exit if cancellation was requested + } + OnError?.Invoke(this, new HidErrorEventArgs(this, ex)); + } + } + } + finally + { + _listeningCts.Dispose(); + _listeningCts = null; + } + } + + private Task ReadInputReportAsync(CancellationToken cancellationToken) + { + var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + + // Use overlapped IO pattern from FileStream + var buffer = new byte[InputReportByteLength]; + _deviceStream.BeginRead(buffer, 0, buffer.Length, ar => + { + try + { + int bytesRead = _deviceStream.EndRead(ar); + if (bytesRead == 0) + { + tcs.SetResult(Array.Empty()); + } + else + { + tcs.SetResult(buffer.Take(bytesRead).ToArray()); + } + } + catch (Exception ex) + { + tcs.SetException(ex); + } + }, null); + + cancellationToken.Register(() => + { + tcs.TrySetCanceled(); + }); + + return tcs.Task; + } + + private void StopListening() + { + if (_listeningCts != null) + { + _listeningCts.Cancel(); + _listeningCts.Dispose(); + _listeningCts = null; + } + } + } +} \ No newline at end of file diff --git a/EonaCat.HID/IDevice.cs b/EonaCat.HID/IDevice.cs deleted file mode 100644 index 50c3633..0000000 --- a/EonaCat.HID/IDevice.cs +++ /dev/null @@ -1,100 +0,0 @@ -using System; -using System.Threading.Tasks; - -namespace EonaCat.HID -{ - // This file is part of the EonaCat project(s) which is released under the Apache License. - // See the LICENSE file or go to https://EonaCat.com/License for full license details. - - public delegate void InsertedEventHandler(); - - public delegate void RemovedEventHandler(); - - public enum DeviceMode - { - NonOverlapped = 0, - Overlapped = 1 - } - - [Flags] - public enum ShareMode - { - Exclusive = 0, - ShareRead = NativeMethods.FILE_SHARE_READ, - ShareWrite = NativeMethods.FILE_SHARE_WRITE - } - - public delegate void ReadCallback(DeviceData data); - - public delegate void ReadReportCallback(Report report); - - public delegate void WriteCallback(bool success); - - public interface IDevice : IDisposable - { - event InsertedEventHandler OnInserted; - - event RemovedEventHandler OnRemoved; - - bool IsOpen { get; } - bool IsConnected { get; } - string Description { get; } - Capabilities Capabilities { get; } - Attributes Info { get; } - string DevicePath { get; } - - bool MonitorDeviceEvents { get; set; } - - void OpenDevice(); - - void OpenDevice(DeviceMode readMode, DeviceMode writeMode, ShareMode shareMode); - - void CloseDevice(); - - DeviceData Read(); - - void Read(ReadCallback callback, int timeout = 0); - - Task ReadAsync(int timeout = 0); - - DeviceData Read(int timeout); - - void ReadReport(ReadReportCallback callback, int timeout = 0); - - Task ReadReportAsync(int timeout = 0); - - Report ReadReport(int timeout = 0); - - bool ReadFeatureData(out byte[] data, byte reportId = 0); - - bool ReadProductName(out byte[] data); - - bool ReadManufacturer(out byte[] data); - - bool ReadSerialNumber(out byte[] data); - - void Write(byte[] data, WriteCallback callback); - - bool Write(byte[] data); - - bool Write(byte[] data, int timeout); - - void Write(byte[] data, WriteCallback callback, int timeout); - - Task WriteAsync(byte[] data, int timeout = 0); - - void WriteReport(Report report, WriteCallback callback); - - bool WriteReport(Report report); - - bool WriteReport(Report report, int timeout); - - void WriteReport(Report report, WriteCallback callback, int timeout); - - Task WriteReportAsync(Report report, int timeout = 0); - - Report CreateReport(); - - bool WriteFeatureData(byte[] data); - } -} \ No newline at end of file diff --git a/EonaCat.HID/IEnumerator.cs b/EonaCat.HID/IEnumerator.cs deleted file mode 100644 index 1d0ed36..0000000 --- a/EonaCat.HID/IEnumerator.cs +++ /dev/null @@ -1,21 +0,0 @@ -using System.Collections.Generic; - -namespace EonaCat.HID -{ - // This file is part of the EonaCat project(s) which is released under the Apache License. - // See the LICENSE file or go to https://EonaCat.com/License for full license details. - public interface IEnumerator - { - bool IsConnected(string devicePath); - - IDevice GetDevice(string devicePath); - - IEnumerable Enumerate(); - - IEnumerable Enumerate(string devicePath); - - IEnumerable Enumerate(int vendorId, params int[] productIds); - - IEnumerable Enumerate(int vendorId); - } -} \ No newline at end of file diff --git a/EonaCat.HID/IHid.cs b/EonaCat.HID/IHid.cs new file mode 100644 index 0000000..88ed556 --- /dev/null +++ b/EonaCat.HID/IHid.cs @@ -0,0 +1,76 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using System.Threading; +using EonaCat.HID.EventArguments; + +namespace EonaCat.HID +{ + /// + /// Interface abstraction for a HID device. + /// + public interface IHid : IDisposable + { + string DevicePath { get; } + ushort VendorId { get; } + ushort ProductId { get; } + string SerialNumber { get; } + string Manufacturer { get; } + string ProductName { get; } + int InputReportByteLength { get; } + int OutputReportByteLength { get; } + int FeatureReportByteLength { get; } + + IDictionary Capabilities { get; } + + /// + /// Opens the device for communication + /// + void Open(); + + /// + /// Closes the device + /// + void Close(); + + /// + /// Writes an output report to the device + /// + /// Complete report data including ReportID + Task WriteOutputReportAsync(byte[] data); + + /// + /// Reads an input report + /// + /// Input report data + Task ReadInputReportAsync(); + + /// + /// Sends a feature report + /// + /// Complete feature report data including ReportID + Task SendFeatureReportAsync(byte[] data); + + /// + /// Gets a feature report + /// + /// Feature report data + Task GetFeatureReportAsync(byte reportId); + + /// + /// Asynchronously read input reports and raise OnDataReceived event + /// + /// + Task StartListeningAsync(CancellationToken cancellationToken); + + /// + /// Occurs when data is received from the device asynchronously + /// + event EventHandler OnDataReceived; + + /// + /// Errors occurring on device operations + /// + event EventHandler OnError; + } +} \ No newline at end of file diff --git a/EonaCat.HID/IHidManager.cs b/EonaCat.HID/IHidManager.cs new file mode 100644 index 0000000..4789e52 --- /dev/null +++ b/EonaCat.HID/IHidManager.cs @@ -0,0 +1,27 @@ +using EonaCat.HID.EventArguments; +using System; +using System.Collections.Generic; + +namespace EonaCat.HID +{ + /// + /// Interface for manager to enumerate devices and monitor connect/disconnect + /// + public interface IHidManager + { + /// + /// Enumerate all connected HID devices matching optional VendorId/ProductId filters + /// + IEnumerable Enumerate(ushort? vendorId = null, ushort? productId = null); + + /// + /// Event is raised when a HID device is inserted + /// + event EventHandler OnDeviceInserted; + + /// + /// Event is raised when a HID device is removed + /// + event EventHandler OnDeviceRemoved; + } +} \ No newline at end of file diff --git a/EonaCat.HID/Managers/HidManagerLinux.cs b/EonaCat.HID/Managers/HidManagerLinux.cs new file mode 100644 index 0000000..ca088ed --- /dev/null +++ b/EonaCat.HID/Managers/HidManagerLinux.cs @@ -0,0 +1,176 @@ +using EonaCat.HID.EventArguments; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Runtime.InteropServices; +using System.Threading; +using System.Threading.Tasks; +using static EonaCat.HID.Managers.Linux.NativeMethods; + +namespace EonaCat.HID.Managers.Linux +{ + internal sealed class HidManagerLinux : IHidManager + { + public event EventHandler OnDeviceInserted; + public event EventHandler OnDeviceRemoved; + public event EventHandler OnDeviceError; + + public IEnumerable Enumerate(ushort? vendorId = null, ushort? productId = null) + { + var hidrawDir = "/sys/class/hidraw/"; + + if (!Directory.Exists(hidrawDir)) + { + yield break; + } + + var hidrawEntries = Directory.GetDirectories(hidrawDir).Where(d => d.Contains("hidraw")); + + foreach (var hidrawEntry in hidrawEntries) + { + var devName = Path.GetFileName(hidrawEntry); + + var devPath = "/dev/" + devName; + if (!File.Exists(devPath)) + { + continue; + } + + var dev = new HidLinux(devPath); + dev.Setup(); + + if (vendorId.HasValue && dev.VendorId != vendorId) + { + continue; + } + + if (productId.HasValue && dev.ProductId != productId) + { + continue; + } + + yield return dev; + } + } + + private void DeviceInserted(IHid device) + { + OnDeviceInserted?.Invoke(this, new HidEventArgs(device)); + } + + private void DeviceRemoved(IHid device) + { + OnDeviceRemoved?.Invoke(this, new HidEventArgs(device)); + } + + public HidManagerLinux() + { + Task.Run(() => MonitorDevices()); + } + + private void MonitorDevices() + { + var previousDevices = new Dictionary(); + + const int maxErrors = 10; + TimeSpan errorWindow = TimeSpan.FromMinutes(5); + Queue errorTimestamps = new Queue(); + + while (true) + { + try + { + var currentDevices = Enumerate().ToList(); + var currentDeviceDict = currentDevices.ToDictionary(d => d.DevicePath); + + // Detect new devices + foreach (var kvp in currentDeviceDict) + { + if (!previousDevices.ContainsKey(kvp.Key)) + { + DeviceInserted(kvp.Value); + } + } + + // Detect removed devices + foreach (var kvp in previousDevices) + { + if (!currentDeviceDict.ContainsKey(kvp.Key)) + { + DeviceRemoved(kvp.Value); + } + } + + previousDevices = currentDeviceDict; + + // Clear error log on success + errorTimestamps.Clear(); + } + catch (Exception ex) + { + OnDeviceError?.Invoke(this, $"[MonitorDevices] Error: {ex.Message}"); + + var now = DateTime.UtcNow; + errorTimestamps.Enqueue(now); + + // Remove timestamps outside the 5-minute window + while (errorTimestamps.Count > 0 && now - errorTimestamps.Peek() > errorWindow) + { + errorTimestamps.Dequeue(); + } + + if (errorTimestamps.Count >= maxErrors) + { + Console.Error.WriteLine($"[MonitorDevices] Too many errors ({errorTimestamps.Count}) in the last 5 minutes. Monitoring stopped."); + break; + } + } + Thread.Sleep(1000); // Poll every second + } + } + } + + internal static class NativeMethods + { + public const int O_RDWR = 0x0002; + public const int O_NONBLOCK = 0x800; + + [DllImport("libc", SetLastError = true)] + public static extern int open(string pathname, int flags); + + [DllImport("libc", SetLastError = true)] + public static extern int close(int fd); + + [DllImport("libc", SetLastError = true)] + public static extern int ioctl(int fd, int request, IntPtr data); + + // _IOC macro emulation + public const int _IOC_NRBITS = 8; + public const int _IOC_TYPEBITS = 8; + public const int _IOC_SIZEBITS = 14; + public const int _IOC_DIRBITS = 2; + + public const int _IOC_NRMASK = (1 << _IOC_NRBITS) - 1; + public const int _IOC_TYPEMASK = (1 << _IOC_TYPEBITS) - 1; + public const int _IOC_SIZEMASK = (1 << _IOC_SIZEBITS) - 1; + public const int _IOC_DIRMASK = (1 << _IOC_DIRBITS) - 1; + + public const int _IOC_NRSHIFT = 0; + public const int _IOC_TYPESHIFT = _IOC_NRSHIFT + _IOC_NRBITS; + public const int _IOC_SIZESHIFT = _IOC_TYPESHIFT + _IOC_TYPEBITS; + public const int _IOC_DIRSHIFT = _IOC_SIZESHIFT + _IOC_SIZEBITS; + + public const int _IOC_NONE = 0; + public const int _IOC_WRITE = 1; + public const int _IOC_READ = 2; + + public static int _IOC(int dir, int type, int nr, int size) + { + return ((dir & _IOC_DIRMASK) << _IOC_DIRSHIFT) | + ((type & _IOC_TYPEMASK) << _IOC_TYPESHIFT) | + ((nr & _IOC_NRMASK) << _IOC_NRSHIFT) | + ((size & _IOC_SIZEMASK) << _IOC_SIZESHIFT); + } + } +} \ No newline at end of file diff --git a/EonaCat.HID/Managers/HidManagerMac.cs b/EonaCat.HID/Managers/HidManagerMac.cs new file mode 100644 index 0000000..3bc65ed --- /dev/null +++ b/EonaCat.HID/Managers/HidManagerMac.cs @@ -0,0 +1,156 @@ +using EonaCat.HID.EventArguments; +using System; +using System.Collections.Generic; +using System.Runtime.InteropServices; +using static EonaCat.HID.Managers.Mac.NativeMethods; + +namespace EonaCat.HID.Managers.Mac +{ + internal sealed class HidManagerMac : IHidManager, IDisposable + { + private IntPtr _hidManager; + private readonly IOHIDDeviceCallback _deviceAddedCallback; + private readonly IOHIDDeviceCallback _deviceRemovedCallback; + + public event EventHandler OnDeviceInserted; + public event EventHandler OnDeviceRemoved; + + public HidManagerMac() + { + _deviceAddedCallback = DeviceAddedCallback; + _deviceRemovedCallback = DeviceRemovedCallback; + + _hidManager = IOHIDManagerCreate(IntPtr.Zero, 0); + if (_hidManager == IntPtr.Zero) + { + throw new InvalidOperationException("Failed to create IOHIDManager"); + } + + CFRetain(_hidManager); + + IOHIDManagerScheduleWithRunLoop( + _hidManager, + CFRunLoopGetCurrent(), + CFRunLoopModeDefault + ); + + if (IOHIDManagerOpen(_hidManager, 0) != 0) + { + throw new InvalidOperationException("Failed to open IOHIDManager"); + } + + IOHIDManagerRegisterDeviceMatchingCallback(_hidManager, _deviceAddedCallback, IntPtr.Zero); + IOHIDManagerRegisterDeviceRemovalCallback(_hidManager, _deviceRemovedCallback, IntPtr.Zero); + } + + public IEnumerable Enumerate(ushort? vendorId = null, ushort? productId = null) + { + var devices = new List(); + IntPtr cfSet = IOHIDManagerCopyDevices(_hidManager); + if (cfSet == IntPtr.Zero) + { + return devices; + } + + int count = CFSetGetCount(cfSet); + IntPtr[] values = new IntPtr[count]; + CFSetGetValues(cfSet, values); + foreach (var ptr in values) + { + var dev = new HidMac(ptr); + dev.Setup(); + if (vendorId.HasValue && dev.VendorId != vendorId) + { + continue; + } + + if (productId.HasValue && dev.ProductId != productId) + { + continue; + } + + devices.Add(dev); + } + CFRelease(cfSet); + return devices; + } + + private void DeviceAddedCallback(IntPtr context, IntPtr result, IntPtr sender, IntPtr devicePtr) + { + if (devicePtr == IntPtr.Zero) + { + return; // Ignore null devices + } + + // Create the device and invoke the event + var device = new HidMac(devicePtr); + device.Setup(); + OnDeviceInserted?.Invoke(this, new HidEventArgs(device)); + } + + private void DeviceRemovedCallback(IntPtr context, IntPtr result, IntPtr sender, IntPtr devicePtr) + { + if (devicePtr == IntPtr.Zero) + { + return; // Ignore null devices + } + + OnDeviceRemoved?.Invoke(this, new HidEventArgs(null)); + } + + public void Dispose() + { + if (_hidManager != IntPtr.Zero) + { + IOHIDManagerUnscheduleFromRunLoop(_hidManager, CFRunLoopGetCurrent(), CFRunLoopModeDefault); + IOHIDManagerClose(_hidManager, 0); + CFRelease(_hidManager); + _hidManager = IntPtr.Zero; + } + } + } + + internal static class NativeMethods + { + public delegate void IOHIDDeviceCallback(IntPtr context, IntPtr result, IntPtr sender, IntPtr device); + + [DllImport("/System/Library/Frameworks/IOKit.framework/IOKit")] + public static extern IntPtr IOHIDManagerCreate(IntPtr allocator, uint options); + [DllImport("/System/Library/Frameworks/IOKit.framework/IOKit")] + public static extern void IOHIDManagerScheduleWithRunLoop(IntPtr manager, IntPtr runLoop, IntPtr runLoopMode); + [DllImport("/System/Library/Frameworks/IOKit.framework/IOKit")] + public static extern void IOHIDManagerUnscheduleFromRunLoop(IntPtr manager, IntPtr runLoop, IntPtr runLoopMode); + [DllImport("/System/Library/Frameworks/IOKit.framework/IOKit")] + public static extern uint IOHIDManagerOpen(IntPtr manager, uint options); + [DllImport("/System/Library/Frameworks/IOKit.framework/IOKit")] + public static extern uint IOHIDManagerClose(IntPtr manager, uint options); + [DllImport("/System/Library/Frameworks/IOKit.framework/IOKit")] + public static extern IntPtr IOHIDManagerCopyDevices(IntPtr manager); + [DllImport("/System/Library/Frameworks/IOKit.framework/IOKit")] + public static extern void IOHIDManagerRegisterDeviceMatchingCallback(IntPtr manager, IOHIDDeviceCallback callback, IntPtr context); + [DllImport("/System/Library/Frameworks/IOKit.framework/IOKit")] + public static extern void IOHIDManagerRegisterDeviceRemovalCallback(IntPtr manager, IOHIDDeviceCallback callback, IntPtr context); + + [DllImport("/System/Library/Frameworks/CoreFoundation.framework/CoreFoundation")] + public static extern IntPtr CFRunLoopGetCurrent(); + [DllImport("/System/Library/Frameworks/CoreFoundation.framework/CoreFoundation")] + public static extern IntPtr CFRetain(IntPtr cf); + [DllImport("/System/Library/Frameworks/CoreFoundation.framework/CoreFoundation")] + public static extern void CFRelease(IntPtr cf); + [DllImport("/System/Library/Frameworks/CoreFoundation.framework/CoreFoundation")] + public static extern int CFSetGetCount(IntPtr cfset); + [DllImport("/System/Library/Frameworks/CoreFoundation.framework/CoreFoundation")] + public static extern void CFSetGetValues(IntPtr cfset, [Out] IntPtr[] values); + [DllImport("/System/Library/Frameworks/CoreFoundation.framework/CoreFoundation")] + public static extern IntPtr CFStringCreateWithCString(IntPtr alloc, string str, CFStringEncoding encoding); + + public static readonly IntPtr CFRunLoopModeDefault = CFStringCreateWithCString( + IntPtr.Zero, "kCFRunLoopDefaultMode", CFStringEncoding.UTF8 + ); + + public enum CFStringEncoding : uint + { + UTF8 = 0x08000100 + } + } +} diff --git a/EonaCat.HID/Managers/HidManagerWindows.cs b/EonaCat.HID/Managers/HidManagerWindows.cs new file mode 100644 index 0000000..cf84c7d --- /dev/null +++ b/EonaCat.HID/Managers/HidManagerWindows.cs @@ -0,0 +1,517 @@ +using EonaCat.HID.EventArguments; +using Microsoft.Win32.SafeHandles; +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.IO; +using System.Runtime.InteropServices; +using static EonaCat.HID.Managers.Windows.NativeMethods; + +namespace EonaCat.HID.Managers.Windows +{ + internal sealed class HidManagerWindows : IHidManager, IDisposable + { + private const int DIGCF_PRESENT = 0x00000002; + private const int DIGCF_DEVICEINTERFACE = 0x00000010; + private const ushort HID_USAGE_PAGE_GENERIC = 0x01; + private const ushort HID_USAGE_GENERIC_MOUSE = 0x02; + + private static Guid GUID_DEVINTERFACE_HID = new Guid("4d1e55b2-f16f-11cf-88cb-001111000030"); + + private readonly object _lock = new object(); + + // Monitor devices for connect/disconnect + private IntPtr _deviceNotificationHandle; + private WndProc _windowProcDelegate; + private IntPtr _messageWindowHandle; + + public event EventHandler OnDeviceInserted; + public event EventHandler OnDeviceRemoved; + + public HidManagerWindows() + { + InitializeMessageWindow(); + RegisterForDeviceNotifications(); + } + + private void InitializeMessageWindow() + { + // Create hidden window to receive device change messages for insert/remove events + _windowProcDelegate = new WndProc(WindowProc); + WNDCLASS wc = new WNDCLASS() + { + lpfnWndProc = _windowProcDelegate, + lpszClassName = "HidDeviceNotificationWindow_" + Guid.NewGuid(), + style = 0, + cbClsExtra = 0, + cbWndExtra = 0, + hInstance = GetModuleHandle(null), + hbrBackground = IntPtr.Zero, + hCursor = IntPtr.Zero, + hIcon = IntPtr.Zero, + lpszMenuName = null + }; + + ushort classAtom = RegisterClass(ref wc); + if (classAtom == 0) + { + throw new Win32Exception(Marshal.GetLastWin32Error(), "Failed to register window class"); + } + + _messageWindowHandle = CreateWindowEx( + 0, + wc.lpszClassName, + "", + 0, + 0, 0, 0, 0, + IntPtr.Zero, + IntPtr.Zero, + wc.hInstance, + IntPtr.Zero); + + if (_messageWindowHandle == IntPtr.Zero) + { + throw new Win32Exception(Marshal.GetLastWin32Error(), "Failed to create message-only window"); + } + } + + private void RegisterForDeviceNotifications() + { + DEV_BROADCAST_DEVICEINTERFACE devInterface = new DEV_BROADCAST_DEVICEINTERFACE + { + dbcc_size = Marshal.SizeOf(), + dbcc_devicetype = DBT_DEVTYP_DEVICEINTERFACE, + dbcc_classguid = GUID_DEVINTERFACE_HID + }; + + IntPtr buffer = Marshal.AllocHGlobal(Marshal.SizeOf(devInterface)); + Marshal.StructureToPtr(devInterface, buffer, false); + + _deviceNotificationHandle = RegisterDeviceNotification(_messageWindowHandle, buffer, DEVICE_NOTIFY_WINDOW_HANDLE); + + Marshal.FreeHGlobal(buffer); + + if (_deviceNotificationHandle == IntPtr.Zero) + { + throw new Win32Exception(Marshal.GetLastWin32Error(), "Failed to register for device notifications"); + } + } + + private IntPtr WindowProc(IntPtr hwnd, uint msg, IntPtr wParam, IntPtr lParam) + { + if (msg == WM_DEVICECHANGE) + { + var eventType = wParam.ToInt32(); + if (eventType == DBT_DEVICEARRIVAL) + { + // Device inserted + var devBroadcast = Marshal.PtrToStructure(lParam); + if (devBroadcast.dbch_devicetype == DBT_DEVTYP_DEVICEINTERFACE) + { + // Treat all HID devices or filter if needed + OnDeviceInserted?.Invoke(this, new HidEventArgs(null)); + } + } + else if (eventType == DBT_DEVICEREMOVECOMPLETE) + { + var devBroadcast = Marshal.PtrToStructure(lParam); + if (devBroadcast.dbch_devicetype == DBT_DEVTYP_DEVICEINTERFACE) + { + OnDeviceRemoved?.Invoke(this, new HidEventArgs(null)); + } + } + } + return DefWindowProc(hwnd, msg, wParam, lParam); + } + + public IEnumerable Enumerate(ushort? vendorId = null, ushort? productId = null) + { + var list = new List(); + IntPtr devInfo = SetupDiGetClassDevs( + ref GUID_DEVINTERFACE_HID, + null, + IntPtr.Zero, + DIGCF_PRESENT | DIGCF_DEVICEINTERFACE); + + if (devInfo == IntPtr.Zero || devInfo.ToInt64() == -1) + { + throw new Win32Exception(Marshal.GetLastWin32Error(), "SetupDiGetClassDevs failed"); + } + + try + { + var iface = new SP_DEVICE_INTERFACE_DATA + { + cbSize = Marshal.SizeOf(typeof(SP_DEVICE_INTERFACE_DATA)) + }; + + for (uint index = 0; ; index++) + { + bool ok = SetupDiEnumDeviceInterfaces( + devInfo, IntPtr.Zero, ref GUID_DEVINTERFACE_HID, index, ref iface); + + if (!ok) + { + int error = Marshal.GetLastWin32Error(); + if (error == ERROR_NO_MORE_ITEMS) + { + break; + } + + throw new Win32Exception(error, "SetupDiEnumDeviceInterfaces failed"); + } + + // Step 1: Get required size + uint requiredSize = 0; + bool sizeResult = SetupDiGetDeviceInterfaceDetail( + devInfo, + ref iface, + IntPtr.Zero, + 0, + ref requiredSize, + IntPtr.Zero); + + // This call should fail with ERROR_INSUFFICIENT_BUFFER + int sizeError = Marshal.GetLastWin32Error(); + if (sizeError != ERROR_INSUFFICIENT_BUFFER || requiredSize == 0) + { + continue; + } + + // Step 2: Allocate buffer for detail data + IntPtr detailDataBuffer = Marshal.AllocHGlobal((int)requiredSize); + try + { + // Step 3: Set cbSize at start of allocated memory + // CRITICAL: cbSize must be size of SP_DEVICE_INTERFACE_DETAIL_DATA structure + // On x86: 6 bytes (4 for cbSize + 2 for alignment) + // On x64: 8 bytes (4 for cbSize + 4 for alignment) + int cbSize = IntPtr.Size == 8 ? 8 : 6; + Marshal.WriteInt32(detailDataBuffer, cbSize); + + // Step 4: Now get the device interface detail + bool success = SetupDiGetDeviceInterfaceDetail( + devInfo, + ref iface, + detailDataBuffer, + requiredSize, + ref requiredSize, + IntPtr.Zero); + + if (!success) + { + int detailError = Marshal.GetLastWin32Error(); + throw new Win32Exception(detailError, $"SetupDiGetDeviceInterfaceDetail failed with error {detailError}"); + } + + // Step 5: Read device path string (starts at offset 4) + // The DevicePath is a null-terminated string that starts at offset 4 + IntPtr pDevicePathName = IntPtr.Add(detailDataBuffer, 4); + string devicePath = Marshal.PtrToStringAuto(pDevicePathName); + + if (string.IsNullOrEmpty(devicePath)) + { + continue; + } + + // Step 6: Create device, filter and add + try + { + // First try to open the device with minimal access to check if it's accessible + using (var testHandle = CreateFile(devicePath, 0, // No access requested + FileShare.ReadWrite, IntPtr.Zero, FileMode.Open, 0, IntPtr.Zero)) + { + if (testHandle.IsInvalid) + { + // Device not accessible, skip it + continue; + } + } + + var device = new HidWindows(devicePath); + device.Setup(); + + if (vendorId.HasValue && device.VendorId != vendorId.Value || + productId.HasValue && device.ProductId != productId.Value) + { + device.Dispose(); + continue; + } + + list.Add(device); + } + catch (UnauthorizedAccessException) + { + // Device is in use or access denied - skip silently + continue; + } + catch (Exception ex) when (ex.Message.Contains("HidP_GetCaps") || + ex.Message.Contains("The parameter is incorrect") || + ex.Message.Contains("Access is denied")) + { + // Common HID access failures - skip these devices + System.Diagnostics.Debug.WriteLine($"Skipping inaccessible HID device {devicePath}: {ex.Message}"); + continue; + } + catch (Exception ex) + { + // Other unexpected errors - log but continue + System.Diagnostics.Debug.WriteLine($"Failed to create device for path {devicePath}: {ex.Message}"); + continue; + } + } + finally + { + Marshal.FreeHGlobal(detailDataBuffer); + } + } + } + finally + { + SetupDiDestroyDeviceInfoList(devInfo); + } + + return list; + } + + public void Dispose() + { + if (_deviceNotificationHandle != IntPtr.Zero) + { + UnregisterDeviceNotification(_deviceNotificationHandle); + _deviceNotificationHandle = IntPtr.Zero; + } + + if (_messageWindowHandle != IntPtr.Zero) + { + DestroyWindow(_messageWindowHandle); + _messageWindowHandle = IntPtr.Zero; + } + } + } + + #region Native Methods and structs + + internal static class NativeMethods + { + public const int ERROR_INSUFFICIENT_BUFFER = 122; + public const int ERROR_NO_MORE_ITEMS = 259; + + public const int FILE_FLAG_OVERLAPPED = 0x40000000; + public const int FILE_SHARE_READ = 0x00000001; + public const int FILE_SHARE_WRITE = 0x00000002; + + public const int OPEN_EXISTING = 3; + public const int GENERIC_READ = unchecked((int)0x80000000); + public const int GENERIC_WRITE = 0x40000000; + public const int DEVICE_NOTIFY_WINDOW_HANDLE = 0x00000000; + + public const int WM_DEVICECHANGE = 0x0219; + public const int DBT_DEVICEARRIVAL = 0x8000; + public const int DBT_DEVICEREMOVECOMPLETE = 0x8004; + public const int DBT_DEVTYP_DEVICEINTERFACE = 5; + + [StructLayout(LayoutKind.Sequential)] + public struct WNDCLASS + { + public uint style; + [MarshalAs(UnmanagedType.FunctionPtr)] + public WndProc lpfnWndProc; + public int cbClsExtra; + public int cbWndExtra; + public IntPtr hInstance; + public IntPtr hIcon; + public IntPtr hCursor; + public IntPtr hbrBackground; + [MarshalAs(UnmanagedType.LPTStr)] + public string lpszMenuName; + [MarshalAs(UnmanagedType.LPTStr)] + public string lpszClassName; + } + + [StructLayout(LayoutKind.Sequential)] + public struct DEV_BROADCAST_HDR + { + public int dbch_size; + public int dbch_devicetype; + public int dbch_reserved; + } + + [StructLayout(LayoutKind.Sequential)] + public struct DEV_BROADCAST_DEVICEINTERFACE + { + public int dbcc_size; + public int dbcc_devicetype; + public int dbcc_reserved; + public Guid dbcc_classguid; + + [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 255)] + public string dbcc_name; + } + + [StructLayout(LayoutKind.Sequential)] + public struct SP_DEVICE_INTERFACE_DATA + { + public int cbSize; + public Guid InterfaceClassGuid; + public int Flags; + public IntPtr Reserved; + } + + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)] + public struct SP_DEVICE_INTERFACE_DETAIL_DATA + { + public int cbSize; + + [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 256)] + public string DevicePath; + } + + + // Declare delegate for WndProc + public delegate IntPtr WndProc(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam); + + [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)] + public static extern ushort RegisterClass(ref WNDCLASS lpWndClass); + + [DllImport("user32.dll", SetLastError = true)] + public static extern IntPtr CreateWindowEx( + int dwExStyle, + string lpClassName, + string lpWindowName, + int dwStyle, + int x, int y, int nWidth, int nHeight, + IntPtr hWndParent, + IntPtr hMenu, + IntPtr hInstance, + IntPtr lpParam); + + [DllImport("user32.dll")] + public static extern bool DestroyWindow(IntPtr hWnd); + + [DllImport("kernel32.dll", CharSet = CharSet.Auto)] + public static extern IntPtr GetModuleHandle(string lpModuleName); + + [DllImport("user32.dll")] + public static extern IntPtr DefWindowProc(IntPtr hwnd, uint uMsg, IntPtr wParam, IntPtr lParam); + + [DllImport("setupapi.dll", SetLastError = true)] + public static extern IntPtr SetupDiGetClassDevs( + ref Guid ClassGuid, + [MarshalAs(UnmanagedType.LPTStr)] string Enumerator, + IntPtr hwndParent, + uint Flags); + + [DllImport("setupapi.dll", SetLastError = true, CharSet = CharSet.Auto)] + public static extern bool SetupDiEnumDeviceInterfaces( + IntPtr DeviceInfoSet, + IntPtr DeviceInfoData, + ref Guid InterfaceClassGuid, + uint MemberIndex, + ref SP_DEVICE_INTERFACE_DATA DeviceInterfaceData); + + [DllImport("setupapi.dll", SetLastError = true, CharSet = CharSet.Auto)] + public static extern bool SetupDiGetDeviceInterfaceDetail( + IntPtr DeviceInfoSet, + ref SP_DEVICE_INTERFACE_DATA DeviceInterfaceData, + IntPtr DeviceInterfaceDetailData, + uint DeviceInterfaceDetailDataSize, + ref uint RequiredSize, + IntPtr DeviceInfoData); + + [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Auto)] + public static extern SafeFileHandle CreateFile( + string lpFileName, + uint dwDesiredAccess, + FileShare dwShareMode, + IntPtr lpSecurityAttributes, + FileMode dwCreationDisposition, + uint dwFlagsAndAttributes, + IntPtr hTemplateFile); + + [DllImport("setupapi.dll", SetLastError = true)] + public static extern bool SetupDiDestroyDeviceInfoList(IntPtr DeviceInfoSet); + + [DllImport("user32.dll", SetLastError = true)] + public static extern IntPtr RegisterDeviceNotification(IntPtr hRecipient, IntPtr NotificationFilter, uint Flags); + + [DllImport("user32.dll", SetLastError = true)] + public static extern bool UnregisterDeviceNotification(IntPtr Handle); + + [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Auto)] + public static extern SafeFileHandle CreateFile( + string lpFileName, + int dwDesiredAccess, + int dwShareMode, + IntPtr lpSecurityAttributes, + int dwCreationDisposition, + int dwFlagsAndAttributes, + IntPtr hTemplateFile); + + // HID APIs + [DllImport("hid.dll", SetLastError = true)] + public static extern bool HidD_GetAttributes(IntPtr hidDeviceObject, ref HidDeviceAttributes attributes); + + [DllImport("hid.dll", SetLastError = true)] + public static extern bool HidD_GetManufacturerString(IntPtr hidDeviceObject, IntPtr buffer, int bufferLength); + + [DllImport("hid.dll", SetLastError = true)] + public static extern bool HidD_GetProductString(IntPtr hidDeviceObject, IntPtr buffer, int bufferLength); + + [DllImport("hid.dll", SetLastError = true)] + public static extern bool HidD_GetSerialNumberString(IntPtr hidDeviceObject, IntPtr buffer, int bufferLength); + + [DllImport("hid.dll", SetLastError = true)] + public static extern bool HidD_GetPreparsedData(IntPtr hidDeviceObject, out IntPtr preparsedData); + + [DllImport("hid.dll", SetLastError = true)] + public static extern bool HidD_FreePreparsedData(IntPtr preparsedData); + + [DllImport("hid.dll", SetLastError = true)] + public static extern bool HidD_SetFeature(IntPtr hidDeviceObject, byte[] reportBuffer, int reportBufferLength); + + [DllImport("hid.dll", SetLastError = true)] + public static extern bool HidD_GetFeature(IntPtr hidDeviceObject, byte[] reportBuffer, int reportBufferLength); + + [DllImport("hid.dll", SetLastError = true)] + public static extern bool HidD_GetInputReport(IntPtr hidDeviceObject, byte[] buffer, int bufferLength); + + // HIDP status + [DllImport("hid.dll")] + public static extern int HidP_GetCaps(IntPtr preparsedData, out HIDP_CAPS capabilities); + + + [StructLayout(LayoutKind.Sequential)] + public struct HIDP_CAPS + { + public ushort Usage; + public ushort UsagePage; + public ushort InputReportByteLength; + public ushort OutputReportByteLength; + public ushort FeatureReportByteLength; + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 17)] + public ushort[] Reserved; + public ushort NumberLinkCollectionNodes; + public ushort NumberInputButtonCaps; + public ushort NumberInputValueCaps; + public ushort NumberInputDataIndices; + public ushort NumberOutputButtonCaps; + public ushort NumberOutputValueCaps; + public ushort NumberOutputDataIndices; + public ushort NumberFeatureButtonCaps; + public ushort NumberFeatureValueCaps; + public ushort NumberFeatureDataIndices; + } + + } + + [StructLayout(LayoutKind.Sequential)] + internal struct HidDeviceAttributes + { + public int Size; + public ushort VendorID; + public ushort ProductID; + public ushort VersionNumber; + } + #endregion +} \ No newline at end of file diff --git a/EonaCat.HID/NativeMethods.cs b/EonaCat.HID/NativeMethods.cs deleted file mode 100644 index 18a4bc5..0000000 --- a/EonaCat.HID/NativeMethods.cs +++ /dev/null @@ -1,225 +0,0 @@ -using System; -using System.Runtime.InteropServices; - -[module: DefaultCharSet(CharSet.Unicode)] - -namespace EonaCat.HID -{ - // This file is part of the EonaCat project(s) which is released under the Apache License. - // See the LICENSE file or go to https://EonaCat.com/License for full license details. - - internal static class NativeMethods - { - internal const int FILE_FLAG_OVERLAPPED = 0x40000000; - internal const short FILE_SHARE_READ = 0x1; - internal const short FILE_SHARE_WRITE = 0x2; - internal const uint GENERIC_READ = 0x80000000; - internal const uint GENERIC_WRITE = 0x40000000; - internal const int ACCESS_NONE = 0; - internal const int INVALID_HANDLE_VALUE = -1; - internal const short OPEN_EXISTING = 3; - internal const int WAIT_TIMEOUT = 0x102; - internal const uint WAIT_OBJECT_0 = 0; - internal const uint WAIT_FAILED = 0xffffffff; - - internal const int WAIT_INFINITE = -1; - - [StructLayout(LayoutKind.Sequential)] - internal struct SECURITY_ATTRIBUTES - { - public int nLength; - public IntPtr lpSecurityDescriptor; - public bool bInheritHandle; - } - - [DllImport("kernel32.dll", SetLastError = true, ExactSpelling = true)] - internal static extern bool CancelIoEx(IntPtr hFile, IntPtr lpOverlapped); - - [DllImport("kernel32.dll", SetLastError = true, ExactSpelling = true)] - internal static extern bool CloseHandle(IntPtr hObject); - - [DllImport("kernel32.dll")] - internal static extern IntPtr CreateEvent(ref SECURITY_ATTRIBUTES securityAttributes, int bManualReset, int bInitialState, string lpName); - - [DllImport("kernel32.dll", SetLastError = true)] - internal static extern IntPtr CreateFile(string lpFileName, uint dwDesiredAccess, int dwShareMode, ref SECURITY_ATTRIBUTES lpSecurityAttributes, int dwCreationDisposition, int dwFlagsAndAttributes, IntPtr hTemplateFile); - - [DllImport("kernel32.dll", SetLastError = true)] - internal static extern bool ReadFile(IntPtr hFile, IntPtr lpBuffer, uint nNumberOfBytesToRead, out uint lpNumberOfBytesRead, [In] ref System.Threading.NativeOverlapped lpOverlapped); - - [DllImport("kernel32.dll")] - internal static extern uint WaitForSingleObject(IntPtr hHandle, int dwMilliseconds); - - [DllImport("kernel32.dll", SetLastError = true)] - internal static extern bool GetOverlappedResult(IntPtr hFile, [In] ref System.Threading.NativeOverlapped lpOverlapped, out uint lpNumberOfBytesTransferred, bool bWait); - - [DllImport("kernel32.dll")] - internal static extern bool WriteFile(IntPtr hFile, byte[] lpBuffer, uint nNumberOfBytesToWrite, out uint lpNumberOfBytesWritten, [In] ref System.Threading.NativeOverlapped lpOverlapped); - - internal const short DIGCF_PRESENT = 0x2; - internal const short DIGCF_DEVICEINTERFACE = 0x10; - internal const int DIGCF_ALLCLASSES = 0x4; - - internal const int SPDRP_ADDRESS = 0x1c; - internal const int SPDRP_BUSNUMBER = 0x15; - internal const int SPDRP_BUSTYPEGUID = 0x13; - internal const int SPDRP_CAPABILITIES = 0xf; - internal const int SPDRP_CHARACTERISTICS = 0x1b; - internal const int SPDRP_CLASS = 7; - internal const int SPDRP_CLASSGUID = 8; - internal const int SPDRP_COMPATIBLEIDS = 2; - internal const int SPDRP_CONFIGFLAGS = 0xa; - internal const int SPDRP_DEVICE_POWER_DATA = 0x1e; - internal const int SPDRP_DEVICEDESC = 0; - internal const int SPDRP_DEVTYPE = 0x19; - internal const int SPDRP_DRIVER = 9; - internal const int SPDRP_ENUMERATOR_NAME = 0x16; - internal const int SPDRP_EXCLUSIVE = 0x1a; - internal const int SPDRP_FRIENDLYNAME = 0xc; - internal const int SPDRP_HARDWAREID = 1; - internal const int SPDRP_LEGACYBUSTYPE = 0x14; - internal const int SPDRP_LOCATION_INFORMATION = 0xd; - internal const int SPDRP_LOWERFILTERS = 0x12; - internal const int SPDRP_MFG = 0xb; - internal const int SPDRP_PHYSICAL_DEVICE_OBJECT_NAME = 0xe; - internal const int SPDRP_REMOVAL_POLICY = 0x1f; - internal const int SPDRP_REMOVAL_POLICY_HW_DEFAULT = 0x20; - internal const int SPDRP_REMOVAL_POLICY_OVERRIDE = 0x21; - internal const int SPDRP_SECURITY = 0x17; - internal const int SPDRP_SECURITY_SDS = 0x18; - internal const int SPDRP_SERVICE = 4; - internal const int SPDRP_UI_NUMBER = 0x10; - internal const int SPDRP_UI_NUMBER_DESC_FORMAT = 0x1d; - - internal const int SPDRP_UPPERFILTERS = 0x11; - - [StructLayout(LayoutKind.Sequential)] - internal struct SP_DEVICE_INTERFACE_DATA - { - internal int cbSize; - internal System.Guid InterfaceClassGuid; - internal int Flags; - internal IntPtr Reserved; - } - - [StructLayout(LayoutKind.Sequential)] - internal struct SP_DEVINFO_DATA - { - internal int cbSize; - internal Guid ClassGuid; - internal int DevInst; - internal IntPtr Reserved; - } - - [StructLayout(LayoutKind.Sequential)] - internal struct SP_DEVICE_INTERFACE_DETAIL_DATA - { - internal int Size; - - [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 256)] - internal string DevicePath; - } - - [StructLayout(LayoutKind.Sequential)] - internal struct DEVPROPKEY - { - public Guid fmtid; - public uint pid; - } - - internal static DEVPROPKEY DEVPKEY_Device_BusReportedDeviceDesc = new DEVPROPKEY { fmtid = new Guid(0x540b947e, 0x8b40, 0x45bc, 0xa8, 0xa2, 0x6a, 0x0b, 0x89, 0x4c, 0xbd, 0xa2), pid = 4 }; - - [DllImport("setupapi.dll")] - public static extern unsafe bool SetupDiGetDeviceRegistryProperty(IntPtr deviceInfoSet, ref SP_DEVINFO_DATA deviceInfoData, int propertyVal, ref int propertyRegDataType, void* propertyBuffer, int propertyBufferSize, ref int requiredSize); - - [DllImport("setupapi.dll", SetLastError = true)] - public static extern unsafe bool SetupDiGetDeviceProperty(IntPtr deviceInfo, ref SP_DEVINFO_DATA deviceInfoData, ref DEVPROPKEY propkey, ref uint propertyDataType, void* propertyBuffer, int propertyBufferSize, ref int requiredSize, uint flags); - - [DllImport("setupapi.dll")] - internal static extern bool SetupDiEnumDeviceInfo(IntPtr deviceInfoSet, int memberIndex, ref SP_DEVINFO_DATA deviceInfoData); - - [DllImport("setupapi.dll")] - internal static extern int SetupDiDestroyDeviceInfoList(IntPtr deviceInfoSet); - - [DllImport("setupapi.dll")] - internal static extern bool SetupDiEnumDeviceInterfaces(IntPtr deviceInfoSet, ref SP_DEVINFO_DATA deviceInfoData, ref Guid interfaceClassGuid, int memberIndex, ref SP_DEVICE_INTERFACE_DATA deviceInterfaceData); - - [DllImport("setupapi.dll")] - internal static extern IntPtr SetupDiGetClassDevs(ref System.Guid classGuid, string enumerator, IntPtr hwndParent, int flags); - - [DllImport("setupapi.dll")] - internal static extern bool SetupDiGetDeviceInterfaceDetail(IntPtr deviceInfoSet, ref SP_DEVICE_INTERFACE_DATA deviceInterfaceData, IntPtr deviceInterfaceDetailData, int deviceInterfaceDetailDataSize, ref int requiredSize, IntPtr deviceInfoData); - - [DllImport("setupapi.dll")] - internal static extern bool SetupDiGetDeviceInterfaceDetail(IntPtr deviceInfoSet, ref SP_DEVICE_INTERFACE_DATA deviceInterfaceData, ref SP_DEVICE_INTERFACE_DETAIL_DATA deviceInterfaceDetailData, int deviceInterfaceDetailDataSize, ref int requiredSize, IntPtr deviceInfoData); - - [StructLayout(LayoutKind.Sequential)] - internal struct HIDD_ATTRIBUTES - { - internal int Size; - internal ushort VendorID; - internal ushort ProductID; - internal short VersionNumber; - } - - [StructLayout(LayoutKind.Sequential)] - internal struct Capabilities - { - internal short Usage; - internal short UsagePage; - internal short InputReportByteLength; - internal short OutputReportByteLength; - internal short FeatureReportByteLength; - - [MarshalAs(UnmanagedType.ByValArray, SizeConst = 17)] - internal short[] Reserved; - - internal short NumberLinkCollectionNodes; - internal short NumberInputButtonCaps; - internal short NumberInputValueCaps; - internal short NumberInputDataIndices; - internal short NumberOutputButtonCaps; - internal short NumberOutputValueCaps; - internal short NumberOutputDataIndices; - internal short NumberFeatureButtonCaps; - internal short NumberFeatureValueCaps; - internal short NumberFeatureDataIndices; - } - - [DllImport("hid.dll")] - internal static extern bool HidD_GetAttributes(IntPtr hidDeviceObject, ref HIDD_ATTRIBUTES attributes); - - [DllImport("hid.dll")] - internal static extern bool HidD_GetFeature(IntPtr hidDeviceObject, byte[] lpReportBuffer, int reportBufferLength); - - [DllImport("hid.dll")] - internal static extern bool HidD_GetInputReport(IntPtr hidDeviceObject, byte[] lpReportBuffer, int reportBufferLength); - - [DllImport("hid.dll")] - internal static extern void HidD_GetHidGuid(ref Guid hidGuid); - - [DllImport("hid.dll")] - internal static extern bool HidD_GetPreparsedData(IntPtr hidDeviceObject, ref IntPtr preparsedData); - - [DllImport("hid.dll")] - internal static extern bool HidD_FreePreparsedData(IntPtr preparsedData); - - [DllImport("hid.dll")] - internal static extern bool HidD_SetFeature(IntPtr hidDeviceObject, byte[] lpReportBuffer, int reportBufferLength); - - [DllImport("hid.dll")] - internal static extern bool HidD_SetOutputReport(IntPtr hidDeviceObject, byte[] lpReportBuffer, int reportBufferLength); - - [DllImport("hid.dll")] - internal static extern int HidP_GetCaps(IntPtr preparsedData, ref Capabilities capabilities); - - [DllImport("hid.dll")] - internal static extern bool HidD_GetProductString(IntPtr hidDeviceObject, byte[] lpReportBuffer, int ReportBufferLength); - - [DllImport("hid.dll")] - internal static extern bool HidD_GetManufacturerString(IntPtr hidDeviceObject, byte[] lpReportBuffer, int ReportBufferLength); - - [DllImport("hid.dll")] - internal static extern bool HidD_GetSerialNumberString(IntPtr hidDeviceObject, byte[] lpReportBuffer, int reportBufferLength); - } -} \ No newline at end of file diff --git a/EonaCat.HID/ReadDevice.cs b/EonaCat.HID/ReadDevice.cs deleted file mode 100644 index d36d45b..0000000 --- a/EonaCat.HID/ReadDevice.cs +++ /dev/null @@ -1,39 +0,0 @@ -using System.Threading.Tasks; - -namespace EonaCat.HID -{ - // This file is part of the EonaCat project(s) which is released under the Apache License. - // See the LICENSE file or go to https://EonaCat.com/License for full license details. - public class ReadDevice : Device - { - internal ReadDevice(string devicePath, string description = null) - : base(devicePath, description) { } - - public DeviceData FastRead(int timeout = 0) - { - try - { - return ReadData(timeout); - } - catch - { - return new DeviceData(DeviceData.ReadStatus.ReadError); - } - } - - public async Task FastReadAsync(int timeout = 0) - { - return await Task.Run(() => FastRead(timeout)); - } - - public Report FastReadReport(int timeout = 0) - { - return new Report(Capabilities.InputReportByteLength, FastRead(timeout)); - } - - public async Task FastReadReportAsync(int timeout = 0) - { - return await Task.Run(() => FastReadReport(timeout)); - } - } -} \ No newline at end of file diff --git a/EonaCat.HID/ReadEnumerator.cs b/EonaCat.HID/ReadEnumerator.cs deleted file mode 100644 index 3524558..0000000 --- a/EonaCat.HID/ReadEnumerator.cs +++ /dev/null @@ -1,46 +0,0 @@ -using System.Collections.Generic; -using System.Linq; - -namespace EonaCat.HID -{ - // This file is part of the EonaCat project(s) which is released under the Apache License. - // See the LICENSE file or go to https://EonaCat.com/License for full license details. - public class ReadEnumerator : IEnumerator - { - public bool IsConnected(string devicePath) - { - return Devices.IsConnected(devicePath); - } - - public IDevice GetDevice(string devicePath) - { - return Enumerate(devicePath).FirstOrDefault(); - } - - public IEnumerable Enumerate() - { - return Devices.EnumerateDevices(). - Select(d => new ReadDevice(d.Path, d.Description) as IDevice); - } - - public IEnumerable Enumerate(string devicePath) - { - return Devices.EnumerateDevices().Where(x => x.Path == devicePath). - Select(d => new ReadDevice(d.Path, d.Description) as IDevice); - } - - public IEnumerable Enumerate(int vendorId, params int[] productIds) - { - return Devices.EnumerateDevices().Select(d => new ReadDevice(d.Path, d.Description)). - Where(f => f.Info.VendorId == vendorId && productIds.Contains(f.Info.ProductId)). - Select(d => d as IDevice); - } - - public IEnumerable Enumerate(int vendorId) - { - return Devices.EnumerateDevices().Select(d => new ReadDevice(d.Path, d.Description)). - Where(f => f.Info.VendorId == vendorId). - Select(d => d as IDevice); - } - } -} \ No newline at end of file diff --git a/EonaCat.HID/Report.cs b/EonaCat.HID/Report.cs deleted file mode 100644 index 19d2a34..0000000 --- a/EonaCat.HID/Report.cs +++ /dev/null @@ -1,86 +0,0 @@ -using System; - -namespace EonaCat.HID -{ - // This file is part of the EonaCat project(s) which is released under the Apache License. - // See the LICENSE file or go to https://EonaCat.com/License for full license details. - public class Report - { - private byte _reportId; - private byte[] _data; - private readonly DeviceData.ReadStatus _status; - - public Report(int reportSize) - { - if (reportSize > 0) - { - _data = new byte[reportSize - 1]; - } - else - { - Exists = false; - return; - } - } - - public Report(int reportSize, DeviceData deviceData) - { - _status = deviceData.Status; - if (reportSize > 0) - { - _data = new byte[reportSize - 1]; - } - else - { - Exists = false; - return; - } - - if (deviceData?.Data == null || deviceData.Data.Length == 0) - { - Exists = false; - return; - } - - _reportId = deviceData.Data[0]; - Exists = true; - - var dataLength = Math.Min(deviceData.Data.Length - 1, reportSize - 1); - if (dataLength > 0) - { - Array.Copy(deviceData.Data, 1, _data, 0, dataLength); - } - } - - public bool Exists { get; private set; } - public DeviceData.ReadStatus ReadStatus => _status; - - public byte ReportId - { - get => _reportId; - set - { - _reportId = value; - Exists = true; - } - } - - public byte[] Data - { - get => _data; - set - { - _data = value; - Exists = true; - } - } - - public byte[] GetBytes() - { - var data = new byte[_data.Length + 1]; - data[0] = _reportId; - Array.Copy(_data, 0, data, 1, _data.Length); - return data; - } - } -} \ No newline at end of file diff --git a/EonaCat.HID/StateAsync.cs b/EonaCat.HID/StateAsync.cs deleted file mode 100644 index 7d1ffae..0000000 --- a/EonaCat.HID/StateAsync.cs +++ /dev/null @@ -1,21 +0,0 @@ -namespace EonaCat.HID -{ - // This file is part of the EonaCat project(s) which is released under the Apache License. - // See the LICENSE file or go to https://EonaCat.com/License for full license details. - public class StateAsync - { - private readonly object _callerDelegate; - private readonly object _callbackDelegate; - - public StateAsync(object callerDelegate, object callbackDelegate) - { - _callerDelegate = callerDelegate; - _callbackDelegate = callbackDelegate; - } - - public object CallerDelegate - { get { return _callerDelegate; } } - public object CallbackDelegate - { get { return _callbackDelegate; } } - } -} \ No newline at end of file