diff --git a/Analyzer/MainForm.cs b/Analyzer/MainForm.cs index a964828..5933258 100644 --- a/Analyzer/MainForm.cs +++ b/Analyzer/MainForm.cs @@ -1,5 +1,6 @@ using EonaCat.HID.EventArguments; using EonaCat.HID.Helpers; +using EonaCat.HID.Models; using System; using System.Collections.Generic; using System.Diagnostics; @@ -106,6 +107,8 @@ namespace EonaCat.HID.Analyzer deviceName = device.ProductName; deviceManufacturer = device.Manufacturer; deviceSerialNumber = device.SerialNumber; + var isWritingSupported = device.IsWritingSupport; + var isReadingSupported = device.IsReadingSupport; var row = new string[] { @@ -113,6 +116,8 @@ namespace EonaCat.HID.Analyzer deviceName, deviceManufacturer, deviceSerialNumber, + isReadingSupported.ToString(), + isWritingSupported.ToString(), device.InputReportByteLength.ToString(), device.OutputReportByteLength.ToString(), device.FeatureReportByteLength.ToString(), @@ -256,7 +261,7 @@ namespace EonaCat.HID.Analyzer { try { - var str = $"Rx Input Report from device {e.Device.ProductName} => {BitConverter.ToString(e.Data)}"; + var str = $"Rx Input Report from device {e.Device.ProductName} => {BitConverter.ToString(e.Report.Data)}"; AppendEventLog(str, Color.Blue); } catch (Exception ex) @@ -269,7 +274,7 @@ namespace EonaCat.HID.Analyzer { try { - var str = string.Format("Removed Device --> VID {0:X4}, PID {0:X4}", e.Device.VendorId, e.Device.ProductId); + var str = string.Format("Removed Device {0} --> VID {1:X4}, PID {2:X4}", e.Device.ProductName, e.Device.VendorId, e.Device.ProductId); AppendEventLog(str); } catch (Exception ex) @@ -282,7 +287,7 @@ namespace EonaCat.HID.Analyzer { try { - var str = string.Format("Inserted Device --> VID {0:X4}, PID {0:X4}", e.Device.VendorId, e.Device.ProductId); + var str = string.Format("Inserted Device {0} --> VID {1:X4}, PID {2:X4}", e.Device.ProductName, e.Device.VendorId, e.Device.ProductId); AppendEventLog(str, Color.Orange); } catch (Exception ex) @@ -332,14 +337,14 @@ namespace EonaCat.HID.Analyzer throw new Exception("This device has no Input Report support!"); } - var buffer = await _device.ReadInputReportAsync(); - if (buffer.Length < 2) + var report = await _device.ReadInputReportAsync(); + if (report == null || report.Data.Length < 2) { - AppendEventLog("Received report is too short to contain a valid Report ID.", Color.Red); + AppendEventLog("Received report is null or is too short to contain a valid Report ID.", Color.Red); return; } - var str = string.Format("Rx Input Report [{0}] <-- {1}", buffer.Length, BitConverter.ToString(buffer)); + var str = string.Format("Rx Input Report [{0}] <-- {1}", report.Data.Length, BitConverter.ToString(report.Data)); AppendEventLog(str, Color.Blue); } catch (Exception ex) @@ -352,27 +357,33 @@ namespace EonaCat.HID.Analyzer { try { + if (_device == null) + { + AppendEventLog("No device connected. Please select a device and click 'Connect'.", Color.Red); + return; + } + byte hidReportId = byte.Parse(comboBoxReportId.Text.Trim()); - byte[] buf = ByteHelper.HexStringToByteArray(textBoxWriteData.Text.Trim()); + byte[] dataBuffer = ByteHelper.HexStringToByteArray(textBoxWriteData.Text.Trim()); - // Combine report ID and buffer - byte[] outputReport = new byte[buf.Length + 1]; - outputReport[0] = hidReportId; - Array.Copy(buf, 0, outputReport, 1, buf.Length); + if (_device.OutputReportByteLength <= 0) + { + throw new Exception("This device has no Output Report support!"); + } + if (dataBuffer.Length > _device.OutputReportByteLength - 1) + { + throw new Exception("Output Report Length Exceeds allowed size."); + } - try - { - await _device.WriteOutputReportAsync(outputReport); - AppendEventLog($"Output report sent: {BitConverter.ToString(outputReport)}", Color.DarkGreen); - } - catch (Exception ex) - { - AppendEventLog("Write failed: " + ex.Message, Color.DarkRed); - } + var outputReport = new HidReport(hidReportId, dataBuffer); + + await _device.WriteOutputReportAsync(outputReport); + + AppendEventLog($"Output report sent (Report ID: 0x{hidReportId:X2}): {ByteHelper.ByteArrayToHexString(dataBuffer)}", Color.DarkGreen); } catch (Exception ex) { - PopupException("Error preparing output report: " + ex.Message); + PopupException("Error sending output report: " + ex.Message); } } @@ -387,8 +398,15 @@ namespace EonaCat.HID.Analyzer throw new Exception("This device has no Feature Report support!"); } - var buffer = await _device.GetFeatureReportAsync(hidReportId); - var str = string.Format("Rx Feature Report [{0}] <-- {1}", buffer.Length, ByteHelper.ByteArrayToHexString(buffer)); + HidReport report = await _device.GetFeatureReportAsync(hidReportId); + if (report == null || report.Data == null || report.Data.Length < 1) + { + AppendEventLog("Received feature report is null or too short.", Color.Red); + return; + } + + string hexString = $"{report.ReportId:X2} - {ByteHelper.ByteArrayToHexString(report.Data)}"; + var str = $"Rx Feature Report [{report.Data.Length + 1}] <-- {hexString}"; AppendEventLog(str, Color.Blue); } catch (Exception ex) @@ -397,23 +415,33 @@ namespace EonaCat.HID.Analyzer } } + private async void ButtonWriteFeature_Click(object sender, EventArgs e) { try { - var hidReportId = byte.Parse(comboBoxReportId.Text); - var buf = ByteHelper.HexStringToByteArray(textBoxWriteData.Text); - - var len = _device.FeatureReportByteLength; - if (buf.Length > len) + if (!byte.TryParse(comboBoxReportId.Text, out var hidReportId)) { - throw new Exception("Write Feature Report Length Exceed"); + throw new FormatException("Invalid Report ID format."); } - Array.Resize(ref buf, len); - await _device.SendFeatureReportAsync(buf); - var str = string.Format("Tx Feature Report [{0}] --> {1}", buf.Length, ByteHelper.ByteArrayToHexString(buf)); - AppendEventLog(str, Color.DarkGreen); + var data = ByteHelper.HexStringToByteArray(textBoxWriteData.Text); + + int maxLen = _device.FeatureReportByteLength - 1; + if (data.Length > maxLen) + { + throw new InvalidOperationException($"Feature report data length exceeds max allowed ({maxLen})."); + } + + var reportData = new byte[data.Length]; + Array.Copy(data, reportData, data.Length); + + var hidReport = new HidReport(hidReportId, reportData); + + await _device.SendFeatureReportAsync(hidReport); + + string logMsg = $"Tx Feature Report [{data.Length + 1}] --> {ByteHelper.ByteArrayToHexString(new[] { hidReportId }.Concat(reportData).ToArray())}"; + AppendEventLog(logMsg, Color.DarkGreen); } catch (Exception ex) { diff --git a/Analyzer/MainForm.resx b/Analyzer/MainForm.resx index 173f685..e39c655 100644 --- a/Analyzer/MainForm.resx +++ b/Analyzer/MainForm.resx @@ -129,6 +129,12 @@ True + + True + + + True + True @@ -151,59 +157,59 @@ iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8 - YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAIqSURBVDhPfZJNaBNBFMcXSknBKKkg9FKL7SKSQ1tpMSEm - mZ0NhFzirXdPftyEHjxueohsNiDUj0IC7UW95CIq5NDLpkQo7U401hatBkKys5kG0/Qe0JW3ycZsWvuH - P8u89+bHe2+W4/4jdWrKo3q97r1gcBw8nD9XOY4b2Zy+flK4xsfL/sCDz7duf4N4XRDQcK0lebcZSGns - MVgmxlWIqTMzfJ7nXT9iMdfXxdBkHSGeCmKHIvyaLCyMOgCSWh1TdhqGQpiZ1pjgSA6IIjRPBZFRAb9y - JBTClpLbRkfZYUUAtH5m19oHCbNdyXat3T2s7T209tCDdCjGd/qAFGG7Mmk863bSnDiuZFQHoPzIPC4v - b9r1OsIvKBK3rEPyU+NKcrv+RyZHfrugDyjd7/ogYULsHwD5qwj9rkejl7m0xnwwu7TfdDsAvYs2ZBDQ - isUuUUE0aThyk1NKR7MASH38dfEUYAAyCKgi5AGAjvEcly6zCzACdHImYH/l1AgU4wCMULN/MIWwgkIa - zx0Ae4GHT832l2ULUEVooorQmC6IazQsFux6Ll1i8e4z0nk4tyrZl31AzxDTUUTQBVy0njEsxvsAkKKx - TJJQZkPOEgCs5YVCFDpxJDPEHFVIYx06eVJq8osbW5Ox1byLX827vNI7Hmpqkcg0RVgGGxhHHQBbMjGC - 8PVtFL771ov3Zlfex28kPpws5XIjw7XnKvimOA72Sjn3nPTWM5y39RdV/noYsphYLAAAAABJRU5ErkJg - gg== + YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAIrSURBVDhPfZJPaBNBFMYXSknBKKkg9NIW20Uhh7bSYkJM + MjsbCLnEW++e/HMTevC4KUTZbEBo1UIC7UW95CIqBPSyKZFCm4nG2qLVYEh2NtNgmt4DuvI22ZhNaz/4 + WOa9Nz/ee7Mc9x+pk5Mu1e127vr9o+DB/JnKcNzQu6krx7nLfLTk9d39dP3GV4jXBAEN1pqSdxq+RIE9 + AMtEn4CYOj3NZ3ne8T0ScXxZCIzXEOKpILYpwi/I/PywDSCplRFlu64rhBnJAhNsyT5RhOaoIDIq4Oe2 + hELYYnxLbyvbLA+A5o/0Wms/ZrTK6Y4Ltw6qu/fMPXQhbYrxzR4gQdiOTOqrnU4aY0fllGoDlO4bR6Wl + 91a9hvBTisRN8/DwY/1SfKv6RyaHXqugByje6Xg/ZkDsHwB5fyL0uxYOX+SSBeaB2aW9htMG6F60IP2A + ZiRygQqiQYOha5xSPJwBQOLDr/MnAH2QfkAFIRcANIxnuWSJnYMRoJNTAXvLJ0agGPtghKr1gymE5RRS + f2IDWAs8eGy0Pi+ZgApCYxWERjRBXKNBMWfVc8kii3aekc7BuVlOP+sBuoaYhkKCJuC8+YxBMdoDgJQC + S8UJZRbkNAHAXF4gQKETWzJFjGGF1Nehk0fFBr+wsTkeWck6+JWswy295qGmGgpNUYRlsI5x2AawJBPd + D1/PRu6bZz1/e2b5TfRq7O3xYiYzNFh7pvwv86Ngt5RxzkqvXIN5S38BNmd6B/1xTKkAAAAASUVORK5C + YII= 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 - HF2OqgAAizrlNjwLhaIAAAAASUVORK5CYII= + YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAETSURBVDhPY2CgBphQKSYe5qL21dTU9D8xGKR2WrG4GNyA + iZUK9h5Ohv+/7uL8//8gO178dRfXf5DaKeUKdiiuyI9Rv1qVqoqhARn/O8D+vyJZ9X9erMYlFM0gMLNe + kivCX+fTilZpDI0wvKRJ5n+gr+qXiblCfOj6wWBimbyFvb3h3/MLBDE0X1ok+B8kh+F0dNCVp1Dm5Wzw + //Vmbrjmt1u5/4PE2nMU2tDVYwUlSSqHC+LU4QbkxWj8L4lXOYyuDieYUi/KY25uCjcAxAaFEbo6nKC+ + noEJFN8wA0BsdDV4weAy4NMOrv+WFiakGwDStGey+H8/N4P/ZYkqR9DVEAQViar7Y3y0PnTmK5Sjy1EV + AAB6heUx9GPlMgAAAABJRU5ErkJggg== diff --git a/Analyzer/mainForm.Designer.cs b/Analyzer/mainForm.Designer.cs index 5f02e70..0589a22 100644 --- a/Analyzer/mainForm.Designer.cs +++ b/Analyzer/mainForm.Designer.cs @@ -31,15 +31,6 @@ System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(MainForm)); this.textBoxWriteData = new System.Windows.Forms.TextBox(); this.dataGridView1 = new System.Windows.Forms.DataGridView(); - this.Column1 = new System.Windows.Forms.DataGridViewTextBoxColumn(); - this.Column9 = new System.Windows.Forms.DataGridViewTextBoxColumn(); - this.Column10 = new System.Windows.Forms.DataGridViewTextBoxColumn(); - this.Column11 = new System.Windows.Forms.DataGridViewTextBoxColumn(); - this.Column7 = new System.Windows.Forms.DataGridViewTextBoxColumn(); - this.Column4 = new System.Windows.Forms.DataGridViewTextBoxColumn(); - this.Column5 = new System.Windows.Forms.DataGridViewTextBoxColumn(); - this.Column2 = new System.Windows.Forms.DataGridViewTextBoxColumn(); - this.Column8 = new System.Windows.Forms.DataGridViewTextBoxColumn(); this.tableLayoutPanel1 = new System.Windows.Forms.TableLayoutPanel(); this.toolStrip1 = new System.Windows.Forms.ToolStrip(); this.toolStripSeparator = new System.Windows.Forms.ToolStripSeparator(); @@ -67,6 +58,17 @@ this.rtbEventLog = new System.Windows.Forms.RichTextBox(); this.statusStrip1 = new System.Windows.Forms.StatusStrip(); this.toolStripStatusLabel1 = new System.Windows.Forms.ToolStripStatusLabel(); + this.Column1 = new System.Windows.Forms.DataGridViewTextBoxColumn(); + this.Column9 = new System.Windows.Forms.DataGridViewTextBoxColumn(); + this.Column10 = new System.Windows.Forms.DataGridViewTextBoxColumn(); + this.Column11 = new System.Windows.Forms.DataGridViewTextBoxColumn(); + this.Column6 = new System.Windows.Forms.DataGridViewTextBoxColumn(); + this.Column3 = new System.Windows.Forms.DataGridViewTextBoxColumn(); + this.Column7 = new System.Windows.Forms.DataGridViewTextBoxColumn(); + this.Column4 = new System.Windows.Forms.DataGridViewTextBoxColumn(); + this.Column5 = new System.Windows.Forms.DataGridViewTextBoxColumn(); + this.Column2 = new System.Windows.Forms.DataGridViewTextBoxColumn(); + this.Column8 = new System.Windows.Forms.DataGridViewTextBoxColumn(); ((System.ComponentModel.ISupportInitialize)(this.dataGridView1)).BeginInit(); this.tableLayoutPanel1.SuspendLayout(); this.toolStrip1.SuspendLayout(); @@ -96,6 +98,8 @@ this.Column9, this.Column10, this.Column11, + this.Column6, + this.Column3, this.Column7, this.Column4, this.Column5, @@ -112,69 +116,6 @@ this.dataGridView1.SelectionChanged += new System.EventHandler(this.DataGridView1_SelectionChanged); this.dataGridView1.DoubleClick += new System.EventHandler(this.dataGridView1_DoubleClick); // - // Column1 - // - this.Column1.HeaderText = "No"; - this.Column1.Name = "Column1"; - this.Column1.ReadOnly = true; - this.Column1.Width = 87; - // - // Column9 - // - this.Column9.AutoSizeMode = System.Windows.Forms.DataGridViewAutoSizeColumnMode.Fill; - this.Column9.HeaderText = "Name"; - this.Column9.Name = "Column9"; - this.Column9.ReadOnly = true; - // - // Column10 - // - this.Column10.AutoSizeMode = System.Windows.Forms.DataGridViewAutoSizeColumnMode.Fill; - this.Column10.HeaderText = "Manufacturer"; - this.Column10.Name = "Column10"; - this.Column10.ReadOnly = true; - // - // Column11 - // - this.Column11.AutoSizeMode = System.Windows.Forms.DataGridViewAutoSizeColumnMode.Fill; - this.Column11.HeaderText = "SerialNo"; - this.Column11.Name = "Column11"; - this.Column11.ReadOnly = true; - // - // Column7 - // - this.Column7.HeaderText = "InputReport"; - this.Column7.Name = "Column7"; - this.Column7.ReadOnly = true; - this.Column7.Width = 86; - // - // Column4 - // - this.Column4.HeaderText = "OutputReport"; - this.Column4.Name = "Column4"; - this.Column4.ReadOnly = true; - this.Column4.Width = 87; - // - // Column5 - // - this.Column5.HeaderText = "FeatureReport"; - this.Column5.Name = "Column5"; - this.Column5.ReadOnly = true; - this.Column5.Width = 86; - // - // Column2 - // - this.Column2.AutoSizeMode = System.Windows.Forms.DataGridViewAutoSizeColumnMode.Fill; - this.Column2.HeaderText = "Info"; - this.Column2.Name = "Column2"; - this.Column2.ReadOnly = true; - // - // Column8 - // - this.Column8.AutoSizeMode = System.Windows.Forms.DataGridViewAutoSizeColumnMode.Fill; - this.Column8.HeaderText = "DevicePath"; - this.Column8.Name = "Column8"; - this.Column8.ReadOnly = true; - // // tableLayoutPanel1 // this.tableLayoutPanel1.ColumnCount = 1; @@ -473,6 +414,81 @@ this.toolStripStatusLabel1.Size = new System.Drawing.Size(118, 17); this.toolStripStatusLabel1.Text = "toolStripStatusLabel1"; // + // Column1 + // + this.Column1.HeaderText = "No"; + this.Column1.Name = "Column1"; + this.Column1.ReadOnly = true; + this.Column1.Width = 87; + // + // Column9 + // + this.Column9.AutoSizeMode = System.Windows.Forms.DataGridViewAutoSizeColumnMode.Fill; + this.Column9.HeaderText = "Name"; + this.Column9.Name = "Column9"; + this.Column9.ReadOnly = true; + // + // Column10 + // + this.Column10.AutoSizeMode = System.Windows.Forms.DataGridViewAutoSizeColumnMode.Fill; + this.Column10.HeaderText = "Manufacturer"; + this.Column10.Name = "Column10"; + this.Column10.ReadOnly = true; + // + // Column11 + // + this.Column11.AutoSizeMode = System.Windows.Forms.DataGridViewAutoSizeColumnMode.Fill; + this.Column11.HeaderText = "SerialNo"; + this.Column11.Name = "Column11"; + this.Column11.ReadOnly = true; + // + // Column6 + // + this.Column6.HeaderText = "CanRead"; + this.Column6.Name = "Column6"; + this.Column6.ReadOnly = true; + // + // Column3 + // + this.Column3.HeaderText = "CanWrite"; + this.Column3.Name = "Column3"; + this.Column3.ReadOnly = true; + // + // Column7 + // + this.Column7.HeaderText = "InputReport"; + this.Column7.Name = "Column7"; + this.Column7.ReadOnly = true; + this.Column7.Width = 86; + // + // Column4 + // + this.Column4.HeaderText = "OutputReport"; + this.Column4.Name = "Column4"; + this.Column4.ReadOnly = true; + this.Column4.Width = 87; + // + // Column5 + // + this.Column5.HeaderText = "FeatureReport"; + this.Column5.Name = "Column5"; + this.Column5.ReadOnly = true; + this.Column5.Width = 86; + // + // Column2 + // + this.Column2.AutoSizeMode = System.Windows.Forms.DataGridViewAutoSizeColumnMode.Fill; + this.Column2.HeaderText = "Info"; + this.Column2.Name = "Column2"; + this.Column2.ReadOnly = true; + // + // Column8 + // + this.Column8.AutoSizeMode = System.Windows.Forms.DataGridViewAutoSizeColumnMode.Fill; + this.Column8.HeaderText = "DevicePath"; + this.Column8.Name = "Column8"; + this.Column8.ReadOnly = true; + // // MainForm // this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F); @@ -537,6 +553,8 @@ private System.Windows.Forms.DataGridViewTextBoxColumn Column9; private System.Windows.Forms.DataGridViewTextBoxColumn Column10; private System.Windows.Forms.DataGridViewTextBoxColumn Column11; + private System.Windows.Forms.DataGridViewTextBoxColumn Column6; + private System.Windows.Forms.DataGridViewTextBoxColumn Column3; private System.Windows.Forms.DataGridViewTextBoxColumn Column7; private System.Windows.Forms.DataGridViewTextBoxColumn Column4; private System.Windows.Forms.DataGridViewTextBoxColumn Column5; diff --git a/EonaCat.HID.Console/Program.cs b/EonaCat.HID.Console/Program.cs index 2182867..a7bc3fd 100644 --- a/EonaCat.HID.Console/Program.cs +++ b/EonaCat.HID.Console/Program.cs @@ -1,4 +1,5 @@ using EonaCat.HID; +using EonaCat.HID.Models; using System.Globalization; namespace EonaCat.HID.Example @@ -46,7 +47,7 @@ namespace EonaCat.HID.Example _device.OnDataReceived += (s, e) => { - Console.WriteLine($"Rx Data: {BitConverter.ToString(e.Data)}"); + Console.WriteLine($"Rx Data: {BitConverter.ToString(e.Report.Data)}"); }; _device.OnError += (s, e) => @@ -62,14 +63,13 @@ namespace EonaCat.HID.Example Console.WriteLine("Listening... Press [Enter] to send test output report."); Console.ReadLine(); - // Example: Send output report + // Example: Send output report using HidReport var data = ByteHelper.HexStringToByteArray("01-02-03"); - byte[] outputReport = new byte[data.Length + 1]; - outputReport[0] = 0x00; // Report ID - Array.Copy(data, 0, outputReport, 1, data.Length); + var reportId = (byte)0x00; // Report ID + var outputReport = new HidReport(reportId, data); await _device.WriteOutputReportAsync(outputReport); - Console.WriteLine($"Sent output report: {BitConverter.ToString(outputReport)}"); + Console.WriteLine($"Sent output report: Report ID: {reportId}, Data: {BitConverter.ToString(data)}"); Console.WriteLine("Press [Enter] to exit..."); Console.ReadLine(); diff --git a/EonaCat.HID/EventArguments/HidDataReceivedEventArgs.cs b/EonaCat.HID/EventArguments/HidDataReceivedEventArgs.cs index fa18444..b0dd846 100644 --- a/EonaCat.HID/EventArguments/HidDataReceivedEventArgs.cs +++ b/EonaCat.HID/EventArguments/HidDataReceivedEventArgs.cs @@ -1,4 +1,5 @@ -using System; +using EonaCat.HID.Models; +using System; namespace EonaCat.HID.EventArguments { @@ -11,12 +12,12 @@ namespace EonaCat.HID.EventArguments public class HidDataReceivedEventArgs : EventArgs { public IHid Device { get; } - public byte[] Data { get; } + public HidReport Report { get; } - public HidDataReceivedEventArgs(IHid device, byte[] data) + public HidDataReceivedEventArgs(IHid device, HidReport report) { Device = device; - Data = data; + Report = report; } } } \ No newline at end of file diff --git a/EonaCat.HID/EventArguments/HidEventArgs.cs b/EonaCat.HID/EventArguments/HidEventArgs.cs index 125968c..89c67fe 100644 --- a/EonaCat.HID/EventArguments/HidEventArgs.cs +++ b/EonaCat.HID/EventArguments/HidEventArgs.cs @@ -11,6 +11,8 @@ namespace EonaCat.HID.EventArguments public class HidEventArgs : EventArgs { public IHid Device { get; } + public bool HasDevice => Device != null; + public bool IsConnected => HasDevice && Device.IsConnected; public HidEventArgs(IHid device) { diff --git a/EonaCat.HID/HidLinux.cs b/EonaCat.HID/HidLinux.cs index 3b8d04e..96ae9ae 100644 --- a/EonaCat.HID/HidLinux.cs +++ b/EonaCat.HID/HidLinux.cs @@ -1,6 +1,8 @@ using EonaCat.HID.EventArguments; using EonaCat.HID.Managers; using EonaCat.HID.Managers.Linux; +using EonaCat.HID.Models; +using Microsoft.Win32.SafeHandles; using System; using System.Collections.Generic; using System.IO; @@ -17,6 +19,7 @@ namespace EonaCat.HID { private readonly string _devicePath; private FileStream _stream; + private bool _isOpen; private readonly object _lock = new object(); private CancellationTokenSource _cts; @@ -29,6 +32,11 @@ namespace EonaCat.HID public int InputReportByteLength { get; private set; } public int OutputReportByteLength { get; private set; } public int FeatureReportByteLength { get; private set; } + + public bool IsReadingSupport { get; private set; } + public bool IsWritingSupport { get; private set; } + + public bool IsConnected => _isOpen; public IDictionary Capabilities { get; private set; } = new Dictionary(); public event EventHandler OnDataReceived; @@ -51,21 +59,33 @@ namespace EonaCat.HID { lock (_lock) { - if (_stream != null) - { + if (_stream != null || _isOpen) return; - } - // Open device for read/write - var fd = NativeMethods.open(_devicePath, NativeMethods.O_RDWR | NativeMethods.O_NONBLOCK); + // Open HID device in non-blocking read/write mode + int fd = NativeMethods.open(_devicePath, NativeMethods.O_RDWR | NativeMethods.O_NONBLOCK); if (fd < 0) { - OnError?.Invoke(this, new HidErrorEventArgs(this, new IOException($"Failed to open device: {_devicePath}"))); + OnError?.Invoke(this, new HidErrorEventArgs(this, + new IOException($"Failed to open device: {_devicePath}"))); return; } var safeHandle = new Microsoft.Win32.SafeHandles.SafeFileHandle(new IntPtr(fd), ownsHandle: true); - _stream = new FileStream(safeHandle, FileAccess.ReadWrite, bufferSize: 64, isAsync: false); + + try + { + _stream = new FileStream(safeHandle, FileAccess.ReadWrite, bufferSize: 64, isAsync: false); + _isOpen = true; + + IsReadingSupport = true; + IsWritingSupport = true; + } + catch (Exception ex) + { + safeHandle.Dispose(); + OnError?.Invoke(this, new HidErrorEventArgs(this, ex)); + } } } @@ -162,20 +182,24 @@ namespace EonaCat.HID return null; } - public async Task WriteOutputReportAsync(byte[] data) + public async Task WriteOutputReportAsync(HidReport report) { - if (data == null) + if (report == null) { - OnError?.Invoke(this, new HidErrorEventArgs(this, new ArgumentNullException(nameof(data)))); + var ex = new ArgumentNullException(nameof(report)); + OnError?.Invoke(this, new HidErrorEventArgs(this, ex)); return; } if (_stream == null) { - OnError?.Invoke(this, new HidErrorEventArgs(this, new InvalidOperationException("Device not open"))); + var ex = new InvalidOperationException("Device not open"); + OnError?.Invoke(this, new HidErrorEventArgs(this, ex)); return; } + var data = report.Data; + await Task.Run(() => { lock (_lock) @@ -188,17 +212,19 @@ namespace EonaCat.HID catch (Exception ex) { OnError?.Invoke(this, new HidErrorEventArgs(this, ex)); + throw; } } }); } - public async Task ReadInputReportAsync() + public async Task ReadInputReportAsync() { if (_stream == null) { - OnError?.Invoke(this, new HidErrorEventArgs(this, new InvalidOperationException("Device not open"))); - return Array.Empty(); + var ex = new InvalidOperationException("Device not open"); + OnError?.Invoke(this, new HidErrorEventArgs(this, ex)); + return null; } return await Task.Run(() => @@ -215,91 +241,119 @@ namespace EonaCat.HID catch (Exception ex) { OnError?.Invoke(this, new HidErrorEventArgs(this, ex)); + return null; } } if (bytesRead <= 0) { - return Array.Empty(); + return null; } - byte[] result = new byte[bytesRead]; - Array.Copy(buffer, result, bytesRead); - return result; + // First byte is report ID, rest is data + byte reportId = buffer[0]; + byte[] data = new byte[bytesRead - 1]; + Array.Copy(buffer, 1, data, 0, data.Length); + + return new HidReport(reportId, data); }); } - public async Task SendFeatureReportAsync(byte[] data) - { - if (data == null || data.Length == 0) - throw new ArgumentException("Feature report data must not be null or empty."); - int fd = NativeMethods.open(_devicePath, NativeMethods.O_RDWR); - if (fd < 0) + public async Task SendFeatureReportAsync(HidReport report) + { + if (report == null) + throw new ArgumentNullException(nameof(report)); + + // Prepare full report buffer: [reportId][reportData...] + int size = 1 + (report.Data?.Length ?? 0); + byte[] buffer = new byte[size]; + buffer[0] = report.ReportId; + if (report.Data != null && report.Data.Length > 0) { - OnError?.Invoke(this, new HidErrorEventArgs(this, new IOException($"Failed to open device: {_devicePath}"))); - return; + Array.Copy(report.Data, 0, buffer, 1, report.Data.Length); } - try + await Task.Run(() => { - int size = data.Length; - IntPtr buffer = Marshal.AllocHGlobal(size); - Marshal.Copy(data, 0, buffer, size); - - int request = NativeMethods._IOC(NativeMethods._IOC_WRITE, 'H', 0x06, size); // HIDIOCSFEATURE - int result = NativeMethods.ioctl(fd, request, buffer); - if (result < 0) + int fd = NativeMethods.open(_devicePath, NativeMethods.O_RDWR); + if (fd < 0) { - OnError?.Invoke(this, new HidErrorEventArgs(this, new IOException("ioctl HIDIOCSFEATURE failed"))); + OnError?.Invoke(this, new HidErrorEventArgs(this, new IOException($"Failed to open device: {_devicePath}"))); return; } - Marshal.FreeHGlobal(buffer); - } - finally - { - NativeMethods.close(fd); - } + IntPtr unmanagedBuffer = IntPtr.Zero; + try + { + unmanagedBuffer = Marshal.AllocHGlobal(size); + Marshal.Copy(buffer, 0, unmanagedBuffer, size); - await Task.CompletedTask; + int request = NativeMethods._IOC(NativeMethods._IOC_WRITE, 'H', 0x06, size); // HIDIOCSFEATURE + int result = NativeMethods.ioctl(fd, request, unmanagedBuffer); + if (result < 0) + { + OnError?.Invoke(this, new HidErrorEventArgs(this, new IOException("ioctl HIDIOCSFEATURE failed"))); + } + } + finally + { + if (unmanagedBuffer != IntPtr.Zero) + Marshal.FreeHGlobal(unmanagedBuffer); + NativeMethods.close(fd); + } + }); } - public async Task GetFeatureReportAsync(byte reportId) + + + + 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) + return await Task.Run(() => { - 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) + int fd = NativeMethods.open(_devicePath, NativeMethods.O_RDWR); + if (fd < 0) { - OnError?.Invoke(this, new HidErrorEventArgs(this, new IOException("ioctl HIDIOCGFEATURE failed"))); - return Array.Empty(); + OnError?.Invoke(this, new HidErrorEventArgs(this, new IOException($"Failed to open device: {_devicePath}"))); + return new HidReport(0, Array.Empty()); } - Marshal.Copy(bufPtr, buffer, 0, buffer.Length); - Marshal.FreeHGlobal(bufPtr); + IntPtr bufPtr = IntPtr.Zero; + try + { + bufPtr = Marshal.AllocHGlobal(buffer.Length); + Marshal.Copy(buffer, 0, bufPtr, buffer.Length); - return await Task.FromResult(buffer); - } - finally - { - NativeMethods.close(fd); - } + int request = NativeMethods._IOC(NativeMethods._IOC_READ, 'H', 0x07, buffer.Length); // HIDIOCGFEATURE + int result = NativeMethods.ioctl(fd, request, bufPtr); + if (result < 0) + { + OnError?.Invoke(this, new HidErrorEventArgs(this, new IOException("ioctl HIDIOCGFEATURE failed"))); + return new HidReport(0, Array.Empty()); + } + + byte[] actualBuffer = new byte[result]; + Marshal.Copy(bufPtr, actualBuffer, 0, result); + + byte actualReportId = actualBuffer.Length > 0 ? actualBuffer[0] : (byte)0; + byte[] reportData = actualBuffer.Length > 1 ? new byte[actualBuffer.Length - 1] : Array.Empty(); + if (reportData.Length > 0) + Array.Copy(actualBuffer, 1, reportData, 0, reportData.Length); + + return new HidReport(actualReportId, reportData); + } + finally + { + if (bufPtr != IntPtr.Zero) + Marshal.FreeHGlobal(bufPtr); + NativeMethods.close(fd); + } + }); } public async Task StartListeningAsync(CancellationToken cancellationToken) @@ -341,7 +395,7 @@ namespace EonaCat.HID catch (NotSupportedException) { OnError?.Invoke(this, new HidErrorEventArgs(this, new NotSupportedException("Reading input reports is not supported on this device."))); - break; // Exit if reading is not supported + break; } catch (Exception ex) { @@ -354,7 +408,22 @@ namespace EonaCat.HID var data = new byte[bytesRead]; Array.Copy(buffer, data, bytesRead); - OnDataReceived?.Invoke(this, new HidDataReceivedEventArgs(this, data)); + // Extract reportId and report data + byte reportId = data.Length > 0 ? data[0] : (byte)0; + + byte[] reportData; + if (data.Length > 1) + { + reportData = new byte[data.Length - 1]; + Array.Copy(data, 1, reportData, 0, data.Length - 1); + } + else + { + reportData = Array.Empty(); + } + + var hidReport = new HidReport(reportId, reportData); + OnDataReceived?.Invoke(this, new HidDataReceivedEventArgs(this, hidReport)); } else { diff --git a/EonaCat.HID/HidMac.cs b/EonaCat.HID/HidMac.cs index d80e032..da68b5a 100644 --- a/EonaCat.HID/HidMac.cs +++ b/EonaCat.HID/HidMac.cs @@ -1,6 +1,10 @@ using EonaCat.HID.EventArguments; +using EonaCat.HID.Models; +using Microsoft.Win32.SafeHandles; using System; using System.Collections.Generic; +using System.IO; +using System.Linq; using System.Runtime.InteropServices; using System.Threading; using System.Threading.Tasks; @@ -36,19 +40,49 @@ namespace EonaCat.HID public int OutputReportByteLength { get; private set; } = 64; public int FeatureReportByteLength { get; private set; } = 64; + public bool IsReadingSupport { get; private set; } + public bool IsWritingSupport { get; private set; } + + public bool IsConnected => _isOpen; + public IDictionary Capabilities { get; private set; } = new Dictionary(); public event EventHandler OnDataReceived; public event EventHandler OnError; + private const int KERN_SUCCESS = 0; + private const int kIOHIDOptionsTypeNone = 0; + public void Open() { - IOHIDDeviceOpen(_deviceHandle, 0); + if (_isOpen) + { + return; + } + + var result = IOHIDDeviceOpen(_deviceHandle, kIOHIDOptionsTypeNone); + if (result != KERN_SUCCESS) + { + throw new InvalidOperationException($"Failed to open HID device. IOHIDDeviceOpen returned: {result}"); + } + + IsReadingSupport = true; + IsWritingSupport = CheckOutputReportSupport(); + + _isOpen = true; + } + + private bool CheckOutputReportSupport() + { + // On macOS, there's no simple API to test writing directly. + // You typically assume support based on report length. + return OutputReportByteLength > 0; } public void Close() { IOHIDDeviceClose(_deviceHandle, 0); + _isOpen = false; } public void Dispose() @@ -64,26 +98,35 @@ namespace EonaCat.HID _listeningCts?.Cancel(); } - public async Task WriteOutputReportAsync(byte[] data) + public async Task WriteOutputReportAsync(HidReport report) { - if (data is null) + if (report == null) { - throw new ArgumentNullException(nameof(data)); + throw new ArgumentNullException(nameof(report)); + } + + if (report.Data == null || report.Data.Length == 0) + { + throw new ArgumentException("Data cannot be null or empty", nameof(report)); } await Task.Run(() => { - byte reportId = data.Length > 0 ? data[0] : (byte)0; - IntPtr buffer = Marshal.AllocHGlobal(data.Length); + // Total length includes reportId + data length + int length = 1 + report.Data.Length; + IntPtr buffer = Marshal.AllocHGlobal(length); try { - Marshal.Copy(data, 0, buffer, data.Length); - int res = IOHIDDeviceSetReport(_deviceHandle, 1, reportId, buffer, data.Length); + // First byte is reportId + Marshal.WriteByte(buffer, report.ReportId); + // Copy the rest of data after the reportId byte + Marshal.Copy(report.Data, 0, buffer + 1, report.Data.Length); + + int res = IOHIDDeviceSetReport(_deviceHandle, 1 /* kIOHIDReportTypeOutput */, report.ReportId, buffer, length); if (res != 0) { OnError?.Invoke(this, new HidErrorEventArgs(this, new Exception($"IOHIDDeviceSetReport (Output) failed: {res}"))); - return; } } finally @@ -93,26 +136,32 @@ namespace EonaCat.HID }); } - public async Task SendFeatureReportAsync(byte[] data) + public async Task SendFeatureReportAsync(HidReport report) { - if (data is null) + if (report == null) { - throw new ArgumentNullException(nameof(data)); + throw new ArgumentNullException(nameof(report)); + } + + if (report.Data == null || report.Data.Length == 0) + { + throw new ArgumentException("Data cannot be null or empty", nameof(report)); } await Task.Run(() => { - byte reportId = data.Length > 0 ? data[0] : (byte)0; - IntPtr buffer = Marshal.AllocHGlobal(data.Length); + int length = 1 + report.Data.Length; + IntPtr buffer = Marshal.AllocHGlobal(length); try { - Marshal.Copy(data, 0, buffer, data.Length); - int res = IOHIDDeviceSetReport(_deviceHandle, 2, reportId, buffer, data.Length); + Marshal.WriteByte(buffer, report.ReportId); + Marshal.Copy(report.Data, 0, buffer + 1, report.Data.Length); + + int res = IOHIDDeviceSetReport(_deviceHandle, 2 /* kIOHIDReportTypeFeature */, report.ReportId, buffer, length); if (res != 0) { OnError?.Invoke(this, new HidErrorEventArgs(this, new Exception($"IOHIDDeviceSetReport (Feature) failed: {res}"))); - return; } } finally @@ -122,7 +171,7 @@ namespace EonaCat.HID }); } - public async Task GetFeatureReportAsync(byte reportId) + public async Task GetFeatureReportAsync(byte reportId) { return await Task.Run(() => { @@ -131,16 +180,26 @@ namespace EonaCat.HID try { - int res = IOHIDDeviceGetReport(_deviceHandle, 2, reportId, buffer, ref length); + int res = IOHIDDeviceGetReport(_deviceHandle, 2 /* kIOHIDReportTypeFeature */, reportId, buffer, ref length); if (res != 0) { OnError?.Invoke(this, new HidErrorEventArgs(this, new Exception($"IOHIDDeviceGetReport (Feature) failed: {res}"))); - return Array.Empty(); + return new HidReport(reportId, Array.Empty()); } byte[] outBuf = new byte[length]; Marshal.Copy(buffer, outBuf, 0, length); - return outBuf; + + if (outBuf.Length > 0 && outBuf[0] == reportId) + { + // Data excludes report ID (first byte) + var dataOnly = outBuf.Skip(1).ToArray(); + return new HidReport(reportId, dataOnly); + } + else + { + return new HidReport(reportId, outBuf); + } } finally { @@ -149,18 +208,40 @@ namespace EonaCat.HID }); } - public Task ReadInputReportAsync() + public Task ReadInputReportAsync() { - var tcs = new TaskCompletionSource(); + 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); + int length = reportLength.ToInt32(); + if (report == null || length == 0) + { + tcs.TrySetResult(new HidReport(0, Array.Empty())); + return; + } + + byte[] output = new byte[length]; + Array.Copy(report, output, length); + + byte reportId = output.Length > 0 ? output[0] : (byte)0; + + byte[] reportData; + if (output.Length > 1) + { + reportData = new byte[output.Length - 1]; + Array.Copy(output, 1, reportData, 0, reportData.Length); + } + else + { + reportData = Array.Empty(); + } + + var hidReport = new HidReport(reportId, reportData); + tcs.TrySetResult(hidReport); }; GCHandle.Alloc(callback); @@ -169,13 +250,12 @@ namespace EonaCat.HID return tcs.Task; } - public Task StartListeningAsync(CancellationToken ct) { if (_listeningTask != null && !_listeningTask.IsCompleted) { OnError?.Invoke(this, new HidErrorEventArgs(this, new NotSupportedException("Already listening on this device."))); - return null; + return Task.CompletedTask; } _listeningCts = CancellationTokenSource.CreateLinkedTokenSource(ct); @@ -185,14 +265,26 @@ namespace EonaCat.HID try { byte[] buffer = new byte[InputReportByteLength]; - InputReportCallback callback = (ctx, result, sender, report, reportLength) => + InputReportCallback callback = (context, result, sender, report, reportLength) => { - byte[] data = new byte[reportLength.ToInt32()]; - Array.Copy(report, data, data.Length); + int len = reportLength.ToInt32(); + if (report == null || report.Length < len) + { + return; + } - OnDataReceived?.Invoke(this, new HidDataReceivedEventArgs(this, data)); + // Extract reportId (first byte) + byte reportId = len > 0 ? report[0] : (byte)0; + + // Extract the rest of the data after reportId + byte[] reportData = len > 1 ? report.Skip(1).Take(len - 1).ToArray() : Array.Empty(); + + var hidReport = new HidReport(reportId, reportData); + + OnDataReceived?.Invoke(this, new HidDataReceivedEventArgs(this, hidReport)); }; + GCHandle.Alloc(callback); IOHIDDeviceRegisterInputReportCallback(_deviceHandle, buffer, (IntPtr)buffer.Length, callback, IntPtr.Zero); IOHIDDeviceScheduleWithRunLoop(_deviceHandle, CFRunLoopGetCurrent(), IntPtr.Zero); @@ -206,7 +298,7 @@ namespace EonaCat.HID } catch (OperationCanceledException) { - return; // Exit gracefully if cancellation was requested + return; } catch (NotSupportedException) { @@ -405,7 +497,7 @@ namespace EonaCat.HID // For managing lifetime of listening loop private CancellationTokenSource _listeningCts; private Task _listeningTask; - + private bool _isOpen; private enum CFNumberType : int { diff --git a/EonaCat.HID/HidWindows.cs b/EonaCat.HID/HidWindows.cs index 8d9688a..b9f7fe7 100644 --- a/EonaCat.HID/HidWindows.cs +++ b/EonaCat.HID/HidWindows.cs @@ -1,5 +1,6 @@ using EonaCat.HID.EventArguments; using EonaCat.HID.Managers.Windows; +using EonaCat.HID.Models; using Microsoft.Win32.SafeHandles; using System; using System.Collections.Generic; @@ -35,8 +36,11 @@ namespace EonaCat.HID public int InputReportByteLength { get; private set; } public int OutputReportByteLength { get; private set; } public int FeatureReportByteLength { get; private set; } + public bool IsConnected => _isOpen; public IDictionary Capabilities { get; } = new Dictionary(); + public bool IsReadingSupport { get; private set; } + public bool IsWritingSupport { get; private set; } public event EventHandler OnDataReceived; public event EventHandler OnError; @@ -95,22 +99,23 @@ namespace EonaCat.HID if (handle == null || handle.IsInvalid) { int err = Marshal.GetLastWin32Error(); - OnError?.Invoke(this, new HidErrorEventArgs(this, new Win32Exception(err, $"Cannot open device {_devicePath} with any access mode"))); + OnError?.Invoke(this, new HidErrorEventArgs(this, + new Win32Exception(err, $"Cannot open device {_devicePath} with any access mode"))); return; } _deviceHandle = handle; - _deviceStream = new FileStream(_deviceHandle, access, 64, true); + _deviceStream = new FileStream(_deviceHandle, access, bufferSize: 64, isAsync: true); _isOpen = true; - // HID descriptor parsing + // HID descriptor if (!HidD_GetPreparsedData(_deviceHandle.DangerousGetHandle(), out _preparsedData)) throw new Win32Exception(Marshal.GetLastWin32Error(), "Failed HidD_GetPreparsedData"); HIDP_CAPS caps; - int res = HidP_GetCaps(_preparsedData, out caps); - if (res == 0) - throw new Win32Exception(res, "Failed HidP_GetCaps"); + int capsRes = HidP_GetCaps(_preparsedData, out caps); + if (capsRes != NativeMethods.HIDP_STATUS_SUCCESS) + throw new Win32Exception(capsRes, "Failed HidP_GetCaps"); InputReportByteLength = caps.InputReportByteLength; OutputReportByteLength = caps.OutputReportByteLength; @@ -126,12 +131,14 @@ namespace EonaCat.HID ProductName = GetStringDescriptor(HidD_GetProductString); SerialNumber = GetStringDescriptor(HidD_GetSerialNumberString); + IsReadingSupport = (access == FileAccess.Read || access == FileAccess.ReadWrite); + IsWritingSupport = (access == FileAccess.Write || access == FileAccess.ReadWrite); + HidDeviceAttributes attr = GetDeviceAttributes(); VendorId = attr.VendorID; ProductId = attr.ProductID; } - private SafeFileHandle TryOpenDevice(int access) { var handle = CreateFile(_devicePath, @@ -145,34 +152,6 @@ namespace EonaCat.HID return handle; } - private bool CanRead(SafeFileHandle handle) - { - try - { - using (var stream = new FileStream(handle, FileAccess.Read, 1, true)) - { - byte[] test = new byte[1]; - stream.Read(test, 0, 1); - return true; - } - } - catch { return false; } - } - - private bool CanWrite(SafeFileHandle handle) - { - try - { - using (var stream = new FileStream(handle, FileAccess.Write, 1, true)) - { - byte[] test = new byte[1]; - stream.Write(test, 0, 1); - return true; - } - } - catch { return false; } - } - /// /// Close the device /// @@ -247,7 +226,7 @@ namespace EonaCat.HID Close(); } - public async Task WriteOutputReportAsync(byte[] data) + public async Task WriteOutputReportAsync(HidReport report) { if (!_isOpen) { @@ -255,15 +234,26 @@ namespace EonaCat.HID return; } - if (data == null || data.Length == 0) + if (report == null) { - OnError?.Invoke(this, new HidErrorEventArgs(this, new ArgumentNullException(nameof(data), "Data cannot be null or empty"))); + OnError?.Invoke(this, new HidErrorEventArgs(this, new ArgumentNullException(nameof(report)))); + return; + } + + if (report.Data == null || report.Data.Length == 0) + { + OnError?.Invoke(this, new HidErrorEventArgs(this, new ArgumentException("Data cannot be null or empty", nameof(report)))); return; } try { - await _deviceStream.WriteAsync(data, 0, data.Length); + // Combine reportId and data into one buffer for sending + var buffer = new byte[1 + report.Data.Length]; + buffer[0] = report.ReportId; + Array.Copy(report.Data, 0, buffer, 1, report.Data.Length); + + await _deviceStream.WriteAsync(buffer, 0, buffer.Length); await _deviceStream.FlushAsync(); } catch (Exception ex) @@ -273,12 +263,12 @@ namespace EonaCat.HID } } - public async Task ReadInputReportAsync() + public async Task ReadInputReportAsync() { if (!_isOpen) { OnError?.Invoke(this, new HidErrorEventArgs(this, new InvalidOperationException("Device not open"))); - return Array.Empty(); + return new HidReport(0, Array.Empty()); } return await Task.Run(async () => @@ -291,10 +281,13 @@ namespace EonaCat.HID if (read == 0) { OnError?.Invoke(this, new HidErrorEventArgs(this, new IOException("No data read from device"))); - return Array.Empty(); + return new HidReport(0, Array.Empty()); } - return buffer.Take(read).ToArray(); + byte reportId = buffer[0]; + byte[] data = buffer.Skip(1).Take(read - 1).ToArray(); + + return new HidReport(reportId, data); } catch (Exception ex) { @@ -304,7 +297,8 @@ namespace EonaCat.HID }); } - public async Task SendFeatureReportAsync(byte[] data) + + public async Task SendFeatureReportAsync(HidReport report) { if (!_isOpen) { @@ -312,10 +306,13 @@ namespace EonaCat.HID return; } - if (data == null || data.Length == 0) - { - throw new ArgumentNullException(nameof(data)); - } + if (report == null) + throw new ArgumentNullException(nameof(report)); + + // Prepare buffer with ReportId + Data + var data = new byte[1 + report.Data.Length]; + data[0] = report.ReportId; + Array.Copy(report.Data, 0, data, 1, report.Data.Length); await Task.Run(() => { @@ -330,12 +327,13 @@ namespace EonaCat.HID }); } - public async Task GetFeatureReportAsync(byte reportId) + + public async Task GetFeatureReportAsync(byte reportId) { if (!_isOpen) { OnError?.Invoke(this, new HidErrorEventArgs(this, new InvalidOperationException("Device not open"))); - return Array.Empty(); + return new HidReport(0, Array.Empty()); } return await Task.Run(() => @@ -349,13 +347,15 @@ namespace EonaCat.HID var err = Marshal.GetLastWin32Error(); var ex = new Win32Exception(err, "HidD_GetFeature failed"); OnError?.Invoke(this, new HidErrorEventArgs(this, ex)); - return Array.Empty(); + return new HidReport(0, Array.Empty()); } - return buffer; + byte[] data = buffer.Skip(1).ToArray(); + return new HidReport(reportId, data); }); } + /// /// Begin async reading loop raising OnDataReceived events on data input /// @@ -379,15 +379,17 @@ namespace EonaCat.HID try { - while (!_listeningCts.Token.IsCancellationRequested) + var token = _listeningCts.Token; + + while (!token.IsCancellationRequested) { try { - byte[] data = await ReadInputReportAsync(_listeningCts.Token); + var inputReport = await ReadInputReportAsync(token); - if (data != null && data.Length > 0) + if (inputReport?.Data?.Length > 0) { - OnDataReceived?.Invoke(this, new HidDataReceivedEventArgs(this, data)); + OnDataReceived?.Invoke(this, new HidDataReceivedEventArgs(this, inputReport)); } } catch (OperationCanceledException) @@ -397,15 +399,15 @@ namespace EonaCat.HID catch (NotSupportedException) { OnError?.Invoke(this, new HidErrorEventArgs(this, new NotSupportedException("Reading input reports is not supported on this device."))); - break; // Exit if reading is not supported + break; } catch (Exception ex) { - // Handle exceptions during reading - if (_listeningCts.IsCancellationRequested) + if (token.IsCancellationRequested) { - break; // Exit if cancellation was requested + break; } + OnError?.Invoke(this, new HidErrorEventArgs(this, ex)); } } @@ -417,24 +419,30 @@ namespace EonaCat.HID } } - private Task ReadInputReportAsync(CancellationToken cancellationToken) + private Task ReadInputReportAsync(CancellationToken cancellationToken) { - var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - // Use overlapped IO pattern from FileStream var buffer = new byte[InputReportByteLength]; + + // Start async read _deviceStream.BeginRead(buffer, 0, buffer.Length, ar => { try { int bytesRead = _deviceStream.EndRead(ar); + if (bytesRead == 0) { - tcs.SetResult(Array.Empty()); + // No data read, reportId 0 and empty data + tcs.SetResult(new HidReport(0, Array.Empty())); } else { - tcs.SetResult(buffer.Take(bytesRead).ToArray()); + // First byte is reportId, rest is data + byte reportId = buffer[0]; + byte[] data = bytesRead > 1 ? buffer.Skip(1).Take(bytesRead - 1).ToArray() : Array.Empty(); + tcs.SetResult(new HidReport(reportId, data)); } } catch (Exception ex) diff --git a/EonaCat.HID/IHid.cs b/EonaCat.HID/IHid.cs index 21d7244..41ad017 100644 --- a/EonaCat.HID/IHid.cs +++ b/EonaCat.HID/IHid.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Threading.Tasks; using System.Threading; using EonaCat.HID.EventArguments; +using EonaCat.HID.Models; namespace EonaCat.HID { @@ -23,6 +24,10 @@ namespace EonaCat.HID int InputReportByteLength { get; } int OutputReportByteLength { get; } int FeatureReportByteLength { get; } + bool IsConnected { get;} + + bool IsReadingSupport { get; } + bool IsWritingSupport { get; } IDictionary Capabilities { get; } @@ -40,25 +45,25 @@ namespace EonaCat.HID /// Writes an output report to the device /// /// Complete report data including ReportID - Task WriteOutputReportAsync(byte[] data); + Task WriteOutputReportAsync(HidReport report); /// /// Reads an input report /// /// Input report data - Task ReadInputReportAsync(); + Task ReadInputReportAsync(); /// /// Sends a feature report /// /// Complete feature report data including ReportID - Task SendFeatureReportAsync(byte[] data); + Task SendFeatureReportAsync(HidReport report); /// /// Gets a feature report /// /// Feature report data - Task GetFeatureReportAsync(byte reportId); + Task GetFeatureReportAsync(byte reportId); /// /// Asynchronously read input reports and raise OnDataReceived event diff --git a/EonaCat.HID/Managers/HidManagerMac.cs b/EonaCat.HID/Managers/HidManagerMac.cs index 2825319..7b291f3 100644 --- a/EonaCat.HID/Managers/HidManagerMac.cs +++ b/EonaCat.HID/Managers/HidManagerMac.cs @@ -14,6 +14,8 @@ namespace EonaCat.HID.Managers.Mac private IntPtr _hidManager; private readonly IOHIDDeviceCallback _deviceAddedCallback; private readonly IOHIDDeviceCallback _deviceRemovedCallback; + private readonly Dictionary _knownDevices = new(); + public event EventHandler OnDeviceInserted; public event EventHandler OnDeviceRemoved; @@ -78,27 +80,57 @@ namespace EonaCat.HID.Managers.Mac return devices; } + private void DeviceInsertedInternal(IHid device) + { + if (!_knownDevices.ContainsKey(device.DevicePath)) + { + _knownDevices[device.DevicePath] = device; + OnDeviceInserted?.Invoke(this, new HidEventArgs(device)); + } + } + + private void DeviceRemovedInternal(IHid device) + { + if (_knownDevices.ContainsKey(device.DevicePath)) + { + device = _knownDevices[device.DevicePath]; + _knownDevices.Remove(device.DevicePath); + OnDeviceRemoved?.Invoke(this, new HidEventArgs(device)); + } + } + private void DeviceAddedCallback(IntPtr context, IntPtr result, IntPtr sender, IntPtr devicePtr) { if (devicePtr == IntPtr.Zero) - { - return; // Ignore null devices - } + return; - // Create the device and invoke the event - var device = new HidMac(devicePtr); - device.Setup(); - OnDeviceInserted?.Invoke(this, new HidEventArgs(device)); + try + { + var device = new HidMac(devicePtr); + device.Setup(); + DeviceInsertedInternal(device); + } + catch (Exception ex) + { + Console.Error.WriteLine($"DeviceAddedCallback error: {ex.Message}"); + } } + private void DeviceRemovedCallback(IntPtr context, IntPtr result, IntPtr sender, IntPtr devicePtr) { if (devicePtr == IntPtr.Zero) - { - return; // Ignore null devices - } + return; - OnDeviceRemoved?.Invoke(this, new HidEventArgs(null)); + try + { + var device = new HidMac(devicePtr); + DeviceRemovedInternal(device); + } + catch (Exception ex) + { + Console.Error.WriteLine($"DeviceRemovedCallback error: {ex.Message}"); + } } public void Dispose() diff --git a/EonaCat.HID/Managers/HidManagerWindows.cs b/EonaCat.HID/Managers/HidManagerWindows.cs index 203c6e8..9cdb69a 100644 --- a/EonaCat.HID/Managers/HidManagerWindows.cs +++ b/EonaCat.HID/Managers/HidManagerWindows.cs @@ -27,6 +27,7 @@ namespace EonaCat.HID.Managers.Windows private IntPtr _deviceNotificationHandle; private WndProc _windowProcDelegate; private IntPtr _messageWindowHandle; + private readonly Dictionary _knownDevices = new(); public event EventHandler OnDeviceInserted; public event EventHandler OnDeviceRemoved; @@ -104,29 +105,76 @@ namespace EonaCat.HID.Managers.Windows { if (msg == WM_DEVICECHANGE) { - var eventType = wParam.ToInt32(); - if (eventType == DBT_DEVICEARRIVAL) + int eventType = wParam.ToInt32(); + + if (eventType == DBT_DEVICEARRIVAL || eventType == DBT_DEVICEREMOVECOMPLETE) { - // Device inserted - var devBroadcast = Marshal.PtrToStructure(lParam); - if (devBroadcast.dbch_devicetype == DBT_DEVTYP_DEVICEINTERFACE) + var hdr = Marshal.PtrToStructure(lParam); + if (hdr.dbch_devicetype == DBT_DEVTYP_DEVICEINTERFACE) { - // Treat all HID devices or filter if needed - OnDeviceInserted?.Invoke(this, new HidEventArgs(null)); - } - } - else if (eventType == DBT_DEVICEREMOVECOMPLETE) - { - var devBroadcast = Marshal.PtrToStructure(lParam); - if (devBroadcast.dbch_devicetype == DBT_DEVTYP_DEVICEINTERFACE) - { - OnDeviceRemoved?.Invoke(this, new HidEventArgs(null)); + var devInterface = Marshal.PtrToStructure(lParam); + + // Calculate pointer to string + IntPtr stringPtr = IntPtr.Add(lParam, Marshal.SizeOf()); + + // Read null-terminated string from unmanaged memory + string devicePath = Marshal.PtrToStringUni(stringPtr); + if (!string.IsNullOrEmpty(devicePath)) + { + try + { + if (eventType == DBT_DEVICEARRIVAL) + { + using (var testHandle = CreateFile(devicePath, 0, + FileShare.ReadWrite, IntPtr.Zero, FileMode.Open, 0, IntPtr.Zero)) + { + if (testHandle.IsInvalid) + return DefWindowProc(hwnd, msg, wParam, lParam); + } + + var device = new HidWindows(devicePath); + device.Setup(); + DeviceInsertedInternal(device); + } + else if (eventType == DBT_DEVICEREMOVECOMPLETE) + { + var device = new HidWindows(devicePath); + DeviceRemovedInternal(device); + } + + return IntPtr.Zero; + } + catch (Exception ex) + { + System.Diagnostics.Debug.WriteLine($"[WindowProc] HID device change error: {ex.Message}"); + } + } } } } + return DefWindowProc(hwnd, msg, wParam, lParam); } + private void DeviceInsertedInternal(IHid device) + { + if (!_knownDevices.ContainsKey(device.DevicePath)) + { + _knownDevices[device.DevicePath] = device; + OnDeviceInserted?.Invoke(this, new HidEventArgs(device)); + } + } + + private void DeviceRemovedInternal(IHid device) + { + if (_knownDevices.ContainsKey(device.DevicePath)) + { + device = _knownDevices[device.DevicePath]; + _knownDevices.Remove(device.DevicePath); + OnDeviceRemoved?.Invoke(this, new HidEventArgs(device)); + } + } + public IEnumerable Enumerate(ushort? vendorId = null, ushort? productId = null) { var list = new List(); @@ -293,8 +341,6 @@ namespace EonaCat.HID.Managers.Windows } } - #region Native Methods and structs - internal static class NativeMethods { public const int ERROR_INSUFFICIENT_BUFFER = 122; @@ -308,6 +354,7 @@ namespace EonaCat.HID.Managers.Windows public const int GENERIC_READ = unchecked((int)0x80000000); public const int GENERIC_WRITE = 0x40000000; public const int DEVICE_NOTIFY_WINDOW_HANDLE = 0x00000000; + public const int HIDP_STATUS_SUCCESS = 0x110000; public const int WM_DEVICECHANGE = 0x0219; public const int DBT_DEVICEARRIVAL = 0x8000; @@ -347,11 +394,9 @@ namespace EonaCat.HID.Managers.Windows public int dbcc_devicetype; public int dbcc_reserved; public Guid dbcc_classguid; - - [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 255)] - public string dbcc_name; } + [StructLayout(LayoutKind.Sequential)] public struct SP_DEVICE_INTERFACE_DATA { @@ -516,5 +561,4 @@ namespace EonaCat.HID.Managers.Windows public ushort ProductID; public ushort VersionNumber; } - #endregion } \ No newline at end of file diff --git a/EonaCat.HID/Models/HidReport.cs b/EonaCat.HID/Models/HidReport.cs new file mode 100644 index 0000000..a8fc471 --- /dev/null +++ b/EonaCat.HID/Models/HidReport.cs @@ -0,0 +1,16 @@ +using System; + +namespace EonaCat.HID.Models +{ + public class HidReport + { + public byte ReportId { get; } + public byte[] Data { get; } + + public HidReport(byte reportId, byte[] data) + { + ReportId = reportId; + Data = data ?? Array.Empty(); + } + } +}