Updated so we also support linux and mac
This commit is contained in:
parent
6eee19b885
commit
115bac01ca
|
@ -96,13 +96,13 @@
|
|||
<None Include="App.config" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\EonaCat.HID\EonaCat.HID.csproj">
|
||||
<Project>{9e8f1d50-74ea-4c60-bd5c-ab2c5b53bc66}</Project>
|
||||
<Name>EonaCat.HID</Name>
|
||||
</ProjectReference>
|
||||
<Content Include="icon.ico" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Content Include="icon.ico" />
|
||||
<ProjectReference Include="..\EonaCat.HID\EonaCat.HID.csproj">
|
||||
<Project>{00403bd6-7a26-4971-29d3-8a7849aac770}</Project>
|
||||
<Name>EonaCat.HID</Name>
|
||||
</ProjectReference>
|
||||
</ItemGroup>
|
||||
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
|
||||
<!-- To modify your build process, add your task inside one of the targets below and uncomment it.
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
using EonaCat.HID.Helpers;
|
||||
using EonaCat.HID.EventArguments;
|
||||
using EonaCat.HID.Helpers;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
|
@ -6,23 +7,41 @@ using System.Drawing;
|
|||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Forms;
|
||||
|
||||
namespace EonaCat.HID.Analyzer
|
||||
{
|
||||
public partial class MainForm : Form
|
||||
{
|
||||
private const int WriteReportTimeout = 3000;
|
||||
|
||||
private Device _device;
|
||||
private List<Device> _deviceList;
|
||||
IHidManager _deviceManager;
|
||||
private IHid _device;
|
||||
private IEnumerable<IHid> _deviceList;
|
||||
|
||||
public MainForm()
|
||||
{
|
||||
InitializeComponent();
|
||||
CreateDeviceManager();
|
||||
}
|
||||
|
||||
private void MainForm_Load(object sender, EventArgs e)
|
||||
private void CreateDeviceManager()
|
||||
{
|
||||
_deviceManager = HidFactory.CreateDeviceManager();
|
||||
if (_deviceManager == null)
|
||||
{
|
||||
throw new Exception("Failed to create HID manager.");
|
||||
}
|
||||
|
||||
_deviceManager.OnDeviceInserted += Hid_Inserted;
|
||||
_deviceManager.OnDeviceRemoved += Hid_Removed;
|
||||
}
|
||||
|
||||
public void RefreshDevices(ushort? vendorId = null, ushort? productId = null)
|
||||
{
|
||||
_deviceList = _deviceManager.Enumerate(vendorId, productId);
|
||||
}
|
||||
|
||||
private void MainForm_Load(object sender, System.EventArgs e)
|
||||
{
|
||||
try
|
||||
{
|
||||
|
@ -34,11 +53,11 @@ namespace EonaCat.HID.Analyzer
|
|||
}
|
||||
}
|
||||
|
||||
private void MainForm_Shown(object sender, EventArgs e)
|
||||
private void MainForm_Shown(object sender, System.EventArgs e)
|
||||
{
|
||||
try
|
||||
{
|
||||
_deviceList = Devices.Enumerate().ToList();
|
||||
RefreshDevices();
|
||||
UpdateDeviceList();
|
||||
toolStripStatusLabel1.Text = "Please select device and click open to start.";
|
||||
}
|
||||
|
@ -76,20 +95,17 @@ namespace EonaCat.HID.Analyzer
|
|||
dataGridView1.SelectionChanged -= DataGridView1_SelectionChanged;
|
||||
dataGridView1.Rows.Clear();
|
||||
|
||||
for (int i = 0; i < _deviceList.Count; i++)
|
||||
for (int i = 0; i < _deviceList.Count(); i++)
|
||||
{
|
||||
Device device = _deviceList[i];
|
||||
IHid device = _deviceList.ElementAt(i);
|
||||
|
||||
var deviceName = "";
|
||||
var deviceManufacturer = "";
|
||||
var deviceSerialNumber = "";
|
||||
|
||||
var info = device.Info;
|
||||
var capabilities = device.Capabilities;
|
||||
|
||||
deviceName = device.ReadProductName();
|
||||
deviceManufacturer = device.ReadManufacturer();
|
||||
deviceSerialNumber = device.ReadSerialNumber();
|
||||
deviceName = device.ProductName;
|
||||
deviceManufacturer = device.Manufacturer;
|
||||
deviceSerialNumber = device.SerialNumber;
|
||||
|
||||
var row = new string[]
|
||||
{
|
||||
|
@ -97,12 +113,13 @@ namespace EonaCat.HID.Analyzer
|
|||
deviceName,
|
||||
deviceManufacturer,
|
||||
deviceSerialNumber,
|
||||
capabilities.InputReportByteLength.ToString(),
|
||||
capabilities.OutputReportByteLength.ToString(),
|
||||
capabilities.FeatureReportByteLength.ToString(),
|
||||
$"Vendor:{info.VendorId:X4} Product:{info.ProductId:X4} Revision:{info.Version:X4}",
|
||||
device.InputReportByteLength.ToString(),
|
||||
device.OutputReportByteLength.ToString(),
|
||||
device.FeatureReportByteLength.ToString(),
|
||||
$"Vendor:{device.VendorId:X4} Product:{device.ProductId:X4} Revision:{device.VendorId:X4}",
|
||||
device.DevicePath
|
||||
};
|
||||
|
||||
dataGridView1.Rows.Add(row);
|
||||
}
|
||||
dataGridView1.SelectionChanged += DataGridView1_SelectionChanged;
|
||||
|
@ -117,7 +134,7 @@ namespace EonaCat.HID.Analyzer
|
|||
}));
|
||||
}
|
||||
|
||||
private void NewToolStripMenuItem_Click(object sender, EventArgs e)
|
||||
private void NewToolStripMenuItem_Click(object sender, System.EventArgs e)
|
||||
{
|
||||
try
|
||||
{
|
||||
|
@ -129,7 +146,7 @@ namespace EonaCat.HID.Analyzer
|
|||
}
|
||||
}
|
||||
|
||||
private void ExitToolStripMenuItem_Click(object sender, EventArgs e)
|
||||
private void ExitToolStripMenuItem_Click(object sender, System.EventArgs e)
|
||||
{
|
||||
try
|
||||
{
|
||||
|
@ -157,7 +174,7 @@ namespace EonaCat.HID.Analyzer
|
|||
{
|
||||
try
|
||||
{
|
||||
_deviceList = Devices.Enumerate().ToList();
|
||||
RefreshDevices();
|
||||
UpdateDeviceList();
|
||||
}
|
||||
catch (Exception ex)
|
||||
|
@ -170,10 +187,15 @@ namespace EonaCat.HID.Analyzer
|
|||
{
|
||||
try
|
||||
{
|
||||
ushort? vid = null;
|
||||
ushort? pid = null;
|
||||
var str = toolStripTextBoxVidPid.Text.Split(':');
|
||||
var vid = int.Parse(str[0], NumberStyles.AllowHexSpecifier);
|
||||
var pid = int.Parse(str[1], NumberStyles.AllowHexSpecifier);
|
||||
_deviceList = Devices.Enumerate(vid, pid).ToList();
|
||||
if (!string.IsNullOrEmpty(toolStripTextBoxVidPid.Text))
|
||||
{
|
||||
vid = ushort.Parse(str[0], NumberStyles.AllowHexSpecifier);
|
||||
pid = ushort.Parse(str[1], NumberStyles.AllowHexSpecifier);
|
||||
}
|
||||
RefreshDevices(vid, pid);
|
||||
UpdateDeviceList();
|
||||
}
|
||||
catch (Exception ex)
|
||||
|
@ -184,25 +206,84 @@ namespace EonaCat.HID.Analyzer
|
|||
|
||||
private void ToolStripButtonConnect_Click(object sender, EventArgs e)
|
||||
{
|
||||
ConnectToSelectedDevice();
|
||||
ConnectToSelectedDeviceAsync().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private void ConnectToSelectedDevice()
|
||||
private async Task ConnectToSelectedDeviceAsync()
|
||||
{
|
||||
await Task.Run(async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
_device = _deviceList.ElementAt(dataGridView1.SelectedRows[0].Index);
|
||||
if (_device == null)
|
||||
{
|
||||
throw new Exception("Could not find Hid USB Device with specified VID PID");
|
||||
}
|
||||
|
||||
var device = _device;
|
||||
device.OnDataReceived -= OnDataReceived;
|
||||
device.OnDataReceived += OnDataReceived;
|
||||
device.OnError -= OnError;
|
||||
device.OnError += OnError;
|
||||
device.Open();
|
||||
|
||||
AppendEventLog($"Connected to device {_device.ProductName}", Color.Green);
|
||||
AppendEventLog($"Started listening to device {_device.ProductName}", Color.Green);
|
||||
await device.StartListeningAsync(default).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
PopupException(ex.Message);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void OnError(object sender, HidErrorEventArgs e)
|
||||
{
|
||||
try
|
||||
{
|
||||
_device = _deviceList[dataGridView1.SelectedRows[0].Index];
|
||||
if (_device == null)
|
||||
{
|
||||
throw new Exception("Could not find Hid USB Device with specified VID PID");
|
||||
}
|
||||
var str = $"Device error for {e.Device.ProductName} => {e.Exception.Message}";
|
||||
AppendEventLog(str, Color.Red);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
PopupException(ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
// Open a device for reading and writing
|
||||
_device.OpenDevice(DeviceMode.Overlapped, DeviceMode.Overlapped, ShareMode.ShareRead | ShareMode.ShareWrite);
|
||||
_device.OnInserted += HidDevice_Inserted;
|
||||
_device.OnRemoved += HidDevice_Removed;
|
||||
_device.MonitorDeviceEvents = true;
|
||||
AppendEventLog($"Connected to device {_device.ReadProductName()}", Color.Green);
|
||||
private void OnDataReceived(object sender, HidDataReceivedEventArgs e)
|
||||
{
|
||||
try
|
||||
{
|
||||
var str = $"Rx Input Report from device {e.Device.ProductName} => {BitConverter.ToString(e.Data)}";
|
||||
AppendEventLog(str, Color.Blue);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
PopupException(ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
private void Hid_Removed(object sender, HidEventArgs e)
|
||||
{
|
||||
try
|
||||
{
|
||||
var str = string.Format("Removed Device --> 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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -151,60 +151,59 @@
|
|||
<data name="toolStripButtonReload.Image" type="System.Drawing.Bitmap, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
|
||||
<value>
|
||||
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==
|
||||
</value>
|
||||
</data>
|
||||
<data name="toolStripButtonOpen.Image" type="System.Drawing.Bitmap, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
|
||||
<value>
|
||||
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=
|
||||
</value>
|
||||
</data>
|
||||
<data name="toolStripButtonClear.Image" type="System.Drawing.Bitmap, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
|
||||
<value>
|
||||
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=
|
||||
</value>
|
||||
</data>
|
||||
<data name="toolStripButtonFilter.Image" type="System.Drawing.Bitmap, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
|
||||
<value>
|
||||
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=
|
||||
</value>
|
||||
</data>
|
||||
<metadata name="menuStrip1.TrayLocation" type="System.Drawing.Point, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
|
||||
|
@ -213,12 +212,12 @@
|
|||
<data name="newToolStripMenuItem.Image" type="System.Drawing.Bitmap, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
|
||||
<value>
|
||||
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==
|
||||
</value>
|
||||
</data>
|
||||
<metadata name="statusStrip1.TrayLocation" type="System.Drawing.Point, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
|
||||
|
|
|
@ -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
|
||||
//
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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; }
|
||||
}
|
||||
}
|
|
@ -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; }
|
||||
}
|
||||
}
|
|
@ -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<DeviceData> ReadAsync(int timeout = 0)
|
||||
{
|
||||
var readDelegate = new ReadDelegate(Read);
|
||||
#if NET20 || NET35 || NET5_0_OR_GREATER
|
||||
return await Task<DeviceData>.Factory.StartNew(() => readDelegate.Invoke(timeout));
|
||||
#else
|
||||
return await Task<DeviceData>.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<Report> ReadReportAsync(int timeout = 0)
|
||||
{
|
||||
var readReportDelegate = new ReadReportDelegate(ReadReport);
|
||||
#if NET20 || NET35 || NET5_0_OR_GREATER
|
||||
return await Task<Report>.Factory.StartNew(() => readReportDelegate.Invoke(timeout));
|
||||
#else
|
||||
return await Task<Report>.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<bool> WriteAsync(byte[] data, int timeout = 0)
|
||||
{
|
||||
var writeDelegate = new WriteDelegate(Write);
|
||||
#if NET20 || NET35 || NET5_0_OR_GREATER
|
||||
return await Task<bool>.Factory.StartNew(() => writeDelegate.Invoke(data, timeout));
|
||||
#else
|
||||
return await Task<bool>.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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handle data transfers on the control channel.
|
||||
/// This method places data on the control channel for devices
|
||||
/// that do not support the interupt transfers
|
||||
/// </summary>
|
||||
/// <param name="report">The outbound HID report</param>
|
||||
/// <returns>The result of the tranfer request: true if successful otherwise false</returns>
|
||||
///
|
||||
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<bool> WriteReportAsync(Report report, int timeout = 0)
|
||||
{
|
||||
var writeReportDelegate = new WriteReportDelegate(WriteReport);
|
||||
#if NET20 || NET35 || NET5_0_OR_GREATER
|
||||
return await Task<bool>.Factory.StartNew(() => writeReportDelegate.Invoke(report, timeout));
|
||||
#else
|
||||
return await Task<bool>.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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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; }
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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<Device> Enumerate()
|
||||
{
|
||||
return EnumerateDevices().Select(x => new Device(x.Path, x.Description));
|
||||
}
|
||||
|
||||
public static IEnumerable<Device> Enumerate(string devicePath)
|
||||
{
|
||||
return EnumerateDevices().Where(x => x.Path == devicePath).Select(x => new Device(x.Path, x.Description));
|
||||
}
|
||||
|
||||
public static IEnumerable<Device> 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<Device> 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<Device> Enumerate(int vendorId)
|
||||
{
|
||||
return EnumerateDevices().Select(x => new Device(x.Path, x.Description)).Where(x => x.Info.VendorId == vendorId);
|
||||
}
|
||||
|
||||
public static IEnumerable<Device> 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<DeviceInfo> EnumerateDevices()
|
||||
{
|
||||
var devices = new List<DeviceInfo>();
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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<IDevice> Enumerate()
|
||||
{
|
||||
return Devices.Enumerate().Select(x => x as IDevice);
|
||||
}
|
||||
|
||||
public IEnumerable<IDevice> Enumerate(string devicePath)
|
||||
{
|
||||
return Devices.Enumerate(devicePath).Select(x => x as IDevice);
|
||||
}
|
||||
|
||||
public IEnumerable<IDevice> Enumerate(int vendorId, params int[] productIds)
|
||||
{
|
||||
return Devices.Enumerate(vendorId, productIds).Select(x => x as IDevice);
|
||||
}
|
||||
|
||||
public IEnumerable<IDevice> Enumerate(int vendorId)
|
||||
{
|
||||
return Devices.Enumerate(vendorId).Select(x => x as IDevice);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,12 +1,13 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>net45;netstandard2</TargetFrameworks>
|
||||
<TargetFrameworks>net46;net6.0</TargetFrameworks>
|
||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||
<Company>EonaCat (Jeroen Saey)</Company>
|
||||
<Copyright>Copyright 2024 EonaCat (Jeroen Saey)</Copyright>
|
||||
|
||||
<LangVersion>latest</LangVersion>
|
||||
<PackageId>EonaCat.HID</PackageId>
|
||||
<Version>1.0.3</Version>
|
||||
<Title>EonaCat.HID</Title>
|
||||
<Authors>EonaCat (Jeroen Saey)</Authors>
|
||||
<Description>HID Devices</Description>
|
||||
|
@ -19,17 +20,6 @@
|
|||
<PackageReadmeFile>README.md</PackageReadmeFile>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<EVRevisionFormat>1.0.2+{chash:10}.{c:ymd}</EVRevisionFormat>
|
||||
<EVDefault>true</EVDefault>
|
||||
<EVInfo>true</EVInfo>
|
||||
<EVTagMatch>v[0-9]*</EVTagMatch>
|
||||
<EVRemoveTagV>true</EVRemoveTagV>
|
||||
<EVVcs>git</EVVcs>
|
||||
<EVCheckAllAttributes>true</EVCheckAllAttributes>
|
||||
<EVShowRevision>true</EVShowRevision>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Include="..\icon.png">
|
||||
<Pack>True</Pack>
|
||||
|
@ -46,10 +36,13 @@
|
|||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="EonaCat.Versioning" Version="1.0.5">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="System.Runtime.InteropServices.RuntimeInformation" Version="4.3.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Reference Include="System.Windows.Forms">
|
||||
<HintPath>..\..\..\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETFramework\v4.8\System.Windows.Forms.dll</HintPath>
|
||||
</Reference>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
|
|
@ -0,0 +1,19 @@
|
|||
using System;
|
||||
|
||||
namespace EonaCat.HID.EventArguments
|
||||
{
|
||||
/// <summary>
|
||||
/// Event args for received data
|
||||
/// </summary>
|
||||
public class HidDataReceivedEventArgs : EventArgs
|
||||
{
|
||||
public IHid Device { get; }
|
||||
public byte[] Data { get; }
|
||||
|
||||
public HidDataReceivedEventArgs(IHid device, byte[] data)
|
||||
{
|
||||
Device = device;
|
||||
Data = data;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
using System;
|
||||
|
||||
namespace EonaCat.HID.EventArguments
|
||||
{
|
||||
/// <summary>
|
||||
/// Event args for error events
|
||||
/// </summary>
|
||||
public class HidErrorEventArgs : EventArgs
|
||||
{
|
||||
public IHid Device { get; }
|
||||
public Exception Exception { get; }
|
||||
|
||||
public HidErrorEventArgs(IHid device, Exception ex)
|
||||
{
|
||||
Device = device;
|
||||
Exception = ex;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
using System;
|
||||
|
||||
namespace EonaCat.HID.EventArguments
|
||||
{
|
||||
/// <summary>
|
||||
/// Event arguments with HID device info.
|
||||
/// </summary>
|
||||
public class HidEventArgs : EventArgs
|
||||
{
|
||||
public IHid Device { get; }
|
||||
|
||||
public HidEventArgs(IHid device)
|
||||
{
|
||||
Device = device;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,68 @@
|
|||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace EonaCat.HID
|
||||
{
|
||||
/// <summary>
|
||||
/// Main static factory class to create platform-specific implementations.
|
||||
/// </summary>
|
||||
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");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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<string, object> Capabilities { get; private set; } = new Dictionary<string, object>();
|
||||
|
||||
public event EventHandler<HidDataReceivedEventArgs> OnDataReceived;
|
||||
public event EventHandler<HidErrorEventArgs> 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<byte[]> ReadInputReportAsync()
|
||||
{
|
||||
if (_stream == null)
|
||||
{
|
||||
OnError?.Invoke(this, new HidErrorEventArgs(this, new InvalidOperationException("Device not open")));
|
||||
return Array.Empty<byte>();
|
||||
}
|
||||
|
||||
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>();
|
||||
}
|
||||
|
||||
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<byte[]> 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<byte>();
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
IntPtr bufPtr = Marshal.AllocHGlobal(buffer.Length);
|
||||
Marshal.Copy(buffer, 0, bufPtr, buffer.Length);
|
||||
|
||||
int request = NativeMethods._IOC(NativeMethods._IOC_READ, 'H', 0x07, buffer.Length); // HIDIOCGFEATURE
|
||||
int result = NativeMethods.ioctl(fd, request, bufPtr);
|
||||
if (result < 0)
|
||||
{
|
||||
OnError?.Invoke(this, new HidErrorEventArgs(this, new IOException("ioctl HIDIOCGFEATURE failed")));
|
||||
return Array.Empty<byte>();
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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<string, object> Capabilities { get; private set; } = new Dictionary<string, object>();
|
||||
|
||||
public event EventHandler<HidDataReceivedEventArgs> OnDataReceived;
|
||||
public event EventHandler<HidErrorEventArgs> 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<byte[]> 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>();
|
||||
}
|
||||
|
||||
byte[] outBuf = new byte[length];
|
||||
Marshal.Copy(buffer, outBuf, 0, length);
|
||||
return outBuf;
|
||||
}
|
||||
finally
|
||||
{
|
||||
Marshal.FreeHGlobal(buffer);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public Task<byte[]> ReadInputReportAsync()
|
||||
{
|
||||
var tcs = new TaskCompletionSource<byte[]>();
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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<string, object> Capabilities { get; } = new Dictionary<string, object>();
|
||||
|
||||
public event EventHandler<HidDataReceivedEventArgs> OnDataReceived;
|
||||
public event EventHandler<HidErrorEventArgs> 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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Open the device for I/O
|
||||
/// </summary>
|
||||
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; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Close the device
|
||||
/// </summary>
|
||||
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<HidDeviceAttributes>()
|
||||
};
|
||||
if (!HidD_GetAttributes(_deviceHandle.DangerousGetHandle(), ref attr))
|
||||
{
|
||||
throw new Win32Exception(Marshal.GetLastWin32Error(), "Failed HidD_GetAttributes");
|
||||
}
|
||||
return attr;
|
||||
}
|
||||
|
||||
private string GetStringDescriptor(Func<IntPtr, IntPtr, int, bool> 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<byte[]> ReadInputReportAsync()
|
||||
{
|
||||
if (!_isOpen)
|
||||
{
|
||||
OnError?.Invoke(this, new HidErrorEventArgs(this, new InvalidOperationException("Device not open")));
|
||||
return Array.Empty<byte>();
|
||||
}
|
||||
|
||||
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<byte>();
|
||||
}
|
||||
|
||||
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<byte[]> GetFeatureReportAsync(byte reportId)
|
||||
{
|
||||
if (!_isOpen)
|
||||
{
|
||||
OnError?.Invoke(this, new HidErrorEventArgs(this, new InvalidOperationException("Device not open")));
|
||||
return Array.Empty<byte>();
|
||||
}
|
||||
|
||||
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<byte>();
|
||||
}
|
||||
|
||||
return buffer;
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Begin async reading loop raising OnDataReceived events on data input
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken"></param>
|
||||
/// <returns></returns>
|
||||
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<byte[]> ReadInputReportAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
var tcs = new TaskCompletionSource<byte[]>(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<byte>());
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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<DeviceData> ReadAsync(int timeout = 0);
|
||||
|
||||
DeviceData Read(int timeout);
|
||||
|
||||
void ReadReport(ReadReportCallback callback, int timeout = 0);
|
||||
|
||||
Task<Report> 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<bool> 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<bool> WriteReportAsync(Report report, int timeout = 0);
|
||||
|
||||
Report CreateReport();
|
||||
|
||||
bool WriteFeatureData(byte[] data);
|
||||
}
|
||||
}
|
|
@ -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<IDevice> Enumerate();
|
||||
|
||||
IEnumerable<IDevice> Enumerate(string devicePath);
|
||||
|
||||
IEnumerable<IDevice> Enumerate(int vendorId, params int[] productIds);
|
||||
|
||||
IEnumerable<IDevice> Enumerate(int vendorId);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,76 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using System.Threading;
|
||||
using EonaCat.HID.EventArguments;
|
||||
|
||||
namespace EonaCat.HID
|
||||
{
|
||||
/// <summary>
|
||||
/// Interface abstraction for a HID device.
|
||||
/// </summary>
|
||||
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<string, object> Capabilities { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Opens the device for communication
|
||||
/// </summary>
|
||||
void Open();
|
||||
|
||||
/// <summary>
|
||||
/// Closes the device
|
||||
/// </summary>
|
||||
void Close();
|
||||
|
||||
/// <summary>
|
||||
/// Writes an output report to the device
|
||||
/// </summary>
|
||||
/// <param name="data">Complete report data including ReportID</param>
|
||||
Task WriteOutputReportAsync(byte[] data);
|
||||
|
||||
/// <summary>
|
||||
/// Reads an input report
|
||||
/// </summary>
|
||||
/// <returns>Input report data</returns>
|
||||
Task<byte[]> ReadInputReportAsync();
|
||||
|
||||
/// <summary>
|
||||
/// Sends a feature report
|
||||
/// </summary>
|
||||
/// <param name="data">Complete feature report data including ReportID</param>
|
||||
Task SendFeatureReportAsync(byte[] data);
|
||||
|
||||
/// <summary>
|
||||
/// Gets a feature report
|
||||
/// </summary>
|
||||
/// <returns>Feature report data</returns>
|
||||
Task<byte[]> GetFeatureReportAsync(byte reportId);
|
||||
|
||||
/// <summary>
|
||||
/// Asynchronously read input reports and raise OnDataReceived event
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken"></param>
|
||||
Task StartListeningAsync(CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Occurs when data is received from the device asynchronously
|
||||
/// </summary>
|
||||
event EventHandler<HidDataReceivedEventArgs> OnDataReceived;
|
||||
|
||||
/// <summary>
|
||||
/// Errors occurring on device operations
|
||||
/// </summary>
|
||||
event EventHandler<HidErrorEventArgs> OnError;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
using EonaCat.HID.EventArguments;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace EonaCat.HID
|
||||
{
|
||||
/// <summary>
|
||||
/// Interface for manager to enumerate devices and monitor connect/disconnect
|
||||
/// </summary>
|
||||
public interface IHidManager
|
||||
{
|
||||
/// <summary>
|
||||
/// Enumerate all connected HID devices matching optional VendorId/ProductId filters
|
||||
/// </summary>
|
||||
IEnumerable<IHid> Enumerate(ushort? vendorId = null, ushort? productId = null);
|
||||
|
||||
/// <summary>
|
||||
/// Event is raised when a HID device is inserted
|
||||
/// </summary>
|
||||
event EventHandler<HidEventArgs> OnDeviceInserted;
|
||||
|
||||
/// <summary>
|
||||
/// Event is raised when a HID device is removed
|
||||
/// </summary>
|
||||
event EventHandler<HidEventArgs> OnDeviceRemoved;
|
||||
}
|
||||
}
|
|
@ -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<HidEventArgs> OnDeviceInserted;
|
||||
public event EventHandler<HidEventArgs> OnDeviceRemoved;
|
||||
public event EventHandler<string> OnDeviceError;
|
||||
|
||||
public IEnumerable<IHid> 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<string, IHid>();
|
||||
|
||||
const int maxErrors = 10;
|
||||
TimeSpan errorWindow = TimeSpan.FromMinutes(5);
|
||||
Queue<DateTime> errorTimestamps = new Queue<DateTime>();
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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<HidEventArgs> OnDeviceInserted;
|
||||
public event EventHandler<HidEventArgs> 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<IHid> Enumerate(ushort? vendorId = null, ushort? productId = null)
|
||||
{
|
||||
var devices = new List<IHid>();
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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<HidEventArgs> OnDeviceInserted;
|
||||
public event EventHandler<HidEventArgs> 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<DEV_BROADCAST_DEVICEINTERFACE>(),
|
||||
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<DEV_BROADCAST_HDR>(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<DEV_BROADCAST_HDR>(lParam);
|
||||
if (devBroadcast.dbch_devicetype == DBT_DEVTYP_DEVICEINTERFACE)
|
||||
{
|
||||
OnDeviceRemoved?.Invoke(this, new HidEventArgs(null));
|
||||
}
|
||||
}
|
||||
}
|
||||
return DefWindowProc(hwnd, msg, wParam, lParam);
|
||||
}
|
||||
|
||||
public IEnumerable<IHid> Enumerate(ushort? vendorId = null, ushort? productId = null)
|
||||
{
|
||||
var list = new List<IHid>();
|
||||
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
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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<DeviceData> 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<Report> FastReadReportAsync(int timeout = 0)
|
||||
{
|
||||
return await Task.Run(() => FastReadReport(timeout));
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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<IDevice> Enumerate()
|
||||
{
|
||||
return Devices.EnumerateDevices().
|
||||
Select(d => new ReadDevice(d.Path, d.Description) as IDevice);
|
||||
}
|
||||
|
||||
public IEnumerable<IDevice> Enumerate(string devicePath)
|
||||
{
|
||||
return Devices.EnumerateDevices().Where(x => x.Path == devicePath).
|
||||
Select(d => new ReadDevice(d.Path, d.Description) as IDevice);
|
||||
}
|
||||
|
||||
public IEnumerable<IDevice> 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<IDevice> 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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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; } }
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue