Initial version

This commit is contained in:
EonaCat 2025-07-17 21:43:48 +02:00
parent fa9b594574
commit fe283384af
42 changed files with 10806 additions and 42 deletions

107
Analyzer/AboutBox.cs Normal file
View File

@ -0,0 +1,107 @@
using System;
using System.Diagnostics;
using System.Reflection;
using System.Windows.Forms;
namespace EonaCat.HID.Analyzer
{
// 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 partial class AboutBox : Form
{
public AboutBox()
{
InitializeComponent();
this.Text = String.Format("About {0}", AssemblyTitle);
this.labelProductName.Text = AssemblyProduct;
this.labelVersion.Text = String.Format("Version {0}", AssemblyVersion);
this.labelCopyright.Text = AssemblyCopyright;
this.labelCompanyName.Text = AssemblyCompany;
this.linkLabelAboutUrl.Text = "";
this.textBoxDescription.Text = AssemblyDescription;
}
private void LinkLabelAboutUrl_LinkClicked(object sender, LinkLabelLinkClickedEventArgs e)
{
Process.Start(linkLabelAboutUrl.Text);
}
public string AssemblyTitle
{
get
{
object[] attributes = Assembly.GetExecutingAssembly().GetCustomAttributes(typeof(AssemblyTitleAttribute), false);
if (attributes.Length > 0)
{
AssemblyTitleAttribute titleAttribute = (AssemblyTitleAttribute)attributes[0];
if (titleAttribute.Title != "")
{
return titleAttribute.Title;
}
}
return System.IO.Path.GetFileNameWithoutExtension(Assembly.GetExecutingAssembly().CodeBase);
}
}
public string AssemblyVersion
{
get
{
return Assembly.GetExecutingAssembly().GetName().Version.ToString();
}
}
public string AssemblyDescription
{
get
{
object[] attributes = Assembly.GetExecutingAssembly().GetCustomAttributes(typeof(AssemblyDescriptionAttribute), false);
if (attributes.Length == 0)
{
return "";
}
return ((AssemblyDescriptionAttribute)attributes[0]).Description;
}
}
public string AssemblyProduct
{
get
{
object[] attributes = Assembly.GetExecutingAssembly().GetCustomAttributes(typeof(AssemblyProductAttribute), false);
if (attributes.Length == 0)
{
return "";
}
return ((AssemblyProductAttribute)attributes[0]).Product;
}
}
public string AssemblyCopyright
{
get
{
object[] attributes = Assembly.GetExecutingAssembly().GetCustomAttributes(typeof(AssemblyCopyrightAttribute), false);
if (attributes.Length == 0)
{
return "";
}
return ((AssemblyCopyrightAttribute)attributes[0]).Copyright;
}
}
public string AssemblyCompany
{
get
{
object[] attributes = Assembly.GetExecutingAssembly().GetCustomAttributes(typeof(AssemblyCompanyAttribute), false);
if (attributes.Length == 0)
{
return "";
}
return ((AssemblyCompanyAttribute)attributes[0]).Company;
}
}
}
}

203
Analyzer/AboutBox.designer.cs generated Normal file
View File

@ -0,0 +1,203 @@
namespace EonaCat.HID.Analyzer
{
partial class AboutBox
{
/// <summary>
/// Required designer variable.
/// </summary>
private System.ComponentModel.IContainer components = null;
/// <summary>
/// Clean up any resources being used.
/// </summary>
protected override void Dispose(bool disposing)
{
if (disposing && (components != null))
{
components.Dispose();
}
base.Dispose(disposing);
}
#region Windows Form Designer generated code
/// <summary>
/// Required method for Designer support - do not modify
/// the contents of this method with the code editor.
/// </summary>
private void InitializeComponent()
{
System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(AboutBox));
this.tableLayoutPanel = new System.Windows.Forms.TableLayoutPanel();
this.logoPictureBox = new System.Windows.Forms.PictureBox();
this.labelProductName = new System.Windows.Forms.Label();
this.labelVersion = new System.Windows.Forms.Label();
this.labelCopyright = new System.Windows.Forms.Label();
this.labelCompanyName = new System.Windows.Forms.Label();
this.textBoxDescription = new System.Windows.Forms.TextBox();
this.okButton = new System.Windows.Forms.Button();
this.linkLabelAboutUrl = new System.Windows.Forms.LinkLabel();
this.tableLayoutPanel.SuspendLayout();
((System.ComponentModel.ISupportInitialize)(this.logoPictureBox)).BeginInit();
this.SuspendLayout();
//
// tableLayoutPanel
//
this.tableLayoutPanel.ColumnCount = 2;
this.tableLayoutPanel.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 33F));
this.tableLayoutPanel.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 67F));
this.tableLayoutPanel.Controls.Add(this.logoPictureBox, 0, 0);
this.tableLayoutPanel.Controls.Add(this.labelProductName, 1, 0);
this.tableLayoutPanel.Controls.Add(this.labelVersion, 1, 1);
this.tableLayoutPanel.Controls.Add(this.labelCopyright, 1, 2);
this.tableLayoutPanel.Controls.Add(this.labelCompanyName, 1, 3);
this.tableLayoutPanel.Controls.Add(this.textBoxDescription, 1, 5);
this.tableLayoutPanel.Controls.Add(this.okButton, 1, 6);
this.tableLayoutPanel.Controls.Add(this.linkLabelAboutUrl, 1, 4);
this.tableLayoutPanel.Dock = System.Windows.Forms.DockStyle.Fill;
this.tableLayoutPanel.Location = new System.Drawing.Point(9, 9);
this.tableLayoutPanel.Name = "tableLayoutPanel";
this.tableLayoutPanel.RowCount = 7;
this.tableLayoutPanel.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 10F));
this.tableLayoutPanel.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 10F));
this.tableLayoutPanel.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 10F));
this.tableLayoutPanel.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 10F));
this.tableLayoutPanel.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 10F));
this.tableLayoutPanel.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 40F));
this.tableLayoutPanel.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 10F));
this.tableLayoutPanel.Size = new System.Drawing.Size(417, 265);
this.tableLayoutPanel.TabIndex = 0;
//
// logoPictureBox
//
this.logoPictureBox.Dock = System.Windows.Forms.DockStyle.Fill;
this.logoPictureBox.Image = ((System.Drawing.Image)(resources.GetObject("logoPictureBox.Image")));
this.logoPictureBox.Location = new System.Drawing.Point(3, 3);
this.logoPictureBox.Name = "logoPictureBox";
this.tableLayoutPanel.SetRowSpan(this.logoPictureBox, 7);
this.logoPictureBox.Size = new System.Drawing.Size(131, 259);
this.logoPictureBox.SizeMode = System.Windows.Forms.PictureBoxSizeMode.Zoom;
this.logoPictureBox.TabIndex = 12;
this.logoPictureBox.TabStop = false;
//
// labelProductName
//
this.labelProductName.Dock = System.Windows.Forms.DockStyle.Fill;
this.labelProductName.Location = new System.Drawing.Point(143, 0);
this.labelProductName.Margin = new System.Windows.Forms.Padding(6, 0, 3, 0);
this.labelProductName.MaximumSize = new System.Drawing.Size(0, 17);
this.labelProductName.Name = "labelProductName";
this.labelProductName.Size = new System.Drawing.Size(271, 17);
this.labelProductName.TabIndex = 19;
this.labelProductName.Text = "Product Name";
this.labelProductName.TextAlign = System.Drawing.ContentAlignment.MiddleLeft;
//
// labelVersion
//
this.labelVersion.Dock = System.Windows.Forms.DockStyle.Fill;
this.labelVersion.Location = new System.Drawing.Point(143, 26);
this.labelVersion.Margin = new System.Windows.Forms.Padding(6, 0, 3, 0);
this.labelVersion.MaximumSize = new System.Drawing.Size(0, 17);
this.labelVersion.Name = "labelVersion";
this.labelVersion.Size = new System.Drawing.Size(271, 17);
this.labelVersion.TabIndex = 0;
this.labelVersion.Text = "Version";
this.labelVersion.TextAlign = System.Drawing.ContentAlignment.MiddleLeft;
//
// labelCopyright
//
this.labelCopyright.Dock = System.Windows.Forms.DockStyle.Fill;
this.labelCopyright.Location = new System.Drawing.Point(143, 52);
this.labelCopyright.Margin = new System.Windows.Forms.Padding(6, 0, 3, 0);
this.labelCopyright.MaximumSize = new System.Drawing.Size(0, 17);
this.labelCopyright.Name = "labelCopyright";
this.labelCopyright.Size = new System.Drawing.Size(271, 17);
this.labelCopyright.TabIndex = 21;
this.labelCopyright.Text = "Copyright";
this.labelCopyright.TextAlign = System.Drawing.ContentAlignment.MiddleLeft;
//
// labelCompanyName
//
this.labelCompanyName.Dock = System.Windows.Forms.DockStyle.Fill;
this.labelCompanyName.Location = new System.Drawing.Point(143, 78);
this.labelCompanyName.Margin = new System.Windows.Forms.Padding(6, 0, 3, 0);
this.labelCompanyName.MaximumSize = new System.Drawing.Size(0, 17);
this.labelCompanyName.Name = "labelCompanyName";
this.labelCompanyName.Size = new System.Drawing.Size(271, 17);
this.labelCompanyName.TabIndex = 22;
this.labelCompanyName.Text = "Company Name";
this.labelCompanyName.TextAlign = System.Drawing.ContentAlignment.MiddleLeft;
//
// textBoxDescription
//
this.textBoxDescription.Dock = System.Windows.Forms.DockStyle.Fill;
this.textBoxDescription.Location = new System.Drawing.Point(143, 133);
this.textBoxDescription.Margin = new System.Windows.Forms.Padding(6, 3, 3, 3);
this.textBoxDescription.Multiline = true;
this.textBoxDescription.Name = "textBoxDescription";
this.textBoxDescription.ReadOnly = true;
this.textBoxDescription.ScrollBars = System.Windows.Forms.ScrollBars.Both;
this.textBoxDescription.Size = new System.Drawing.Size(271, 100);
this.textBoxDescription.TabIndex = 23;
this.textBoxDescription.TabStop = false;
this.textBoxDescription.Text = "Description";
//
// okButton
//
this.okButton.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right)));
this.okButton.DialogResult = System.Windows.Forms.DialogResult.Cancel;
this.okButton.Location = new System.Drawing.Point(339, 239);
this.okButton.Name = "okButton";
this.okButton.Size = new System.Drawing.Size(75, 23);
this.okButton.TabIndex = 24;
this.okButton.Text = "&OK";
//
// linkLabelAboutUrl
//
this.linkLabelAboutUrl.AutoSize = true;
this.linkLabelAboutUrl.Dock = System.Windows.Forms.DockStyle.Fill;
this.linkLabelAboutUrl.Location = new System.Drawing.Point(143, 104);
this.linkLabelAboutUrl.Margin = new System.Windows.Forms.Padding(6, 0, 3, 0);
this.linkLabelAboutUrl.Name = "linkLabelAboutUrl";
this.linkLabelAboutUrl.Size = new System.Drawing.Size(271, 26);
this.linkLabelAboutUrl.TabIndex = 25;
this.linkLabelAboutUrl.TabStop = true;
this.linkLabelAboutUrl.Text = "About Url";
this.linkLabelAboutUrl.LinkClicked += new System.Windows.Forms.LinkLabelLinkClickedEventHandler(this.LinkLabelAboutUrl_LinkClicked);
//
// AboutBox
//
this.AcceptButton = this.okButton;
this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F);
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
this.ClientSize = new System.Drawing.Size(435, 283);
this.Controls.Add(this.tableLayoutPanel);
this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedDialog;
this.MaximizeBox = false;
this.MinimizeBox = false;
this.Name = "AboutBox";
this.Padding = new System.Windows.Forms.Padding(9);
this.ShowIcon = false;
this.ShowInTaskbar = false;
this.StartPosition = System.Windows.Forms.FormStartPosition.CenterParent;
this.Text = "AboutBox";
this.tableLayoutPanel.ResumeLayout(false);
this.tableLayoutPanel.PerformLayout();
((System.ComponentModel.ISupportInitialize)(this.logoPictureBox)).EndInit();
this.ResumeLayout(false);
}
#endregion
private System.Windows.Forms.TableLayoutPanel tableLayoutPanel;
private System.Windows.Forms.PictureBox logoPictureBox;
private System.Windows.Forms.Label labelProductName;
private System.Windows.Forms.Label labelVersion;
private System.Windows.Forms.Label labelCopyright;
private System.Windows.Forms.Label labelCompanyName;
private System.Windows.Forms.TextBox textBoxDescription;
private System.Windows.Forms.Button okButton;
private System.Windows.Forms.LinkLabel linkLabelAboutUrl;
}
}

1347
Analyzer/AboutBox.resx Normal file

File diff suppressed because it is too large Load Diff

6
Analyzer/App.config Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<startup>
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.8" />
</startup>
</configuration>

View File

@ -0,0 +1,115 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="12.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
<ProjectGuid>{61994020-DB89-4621-BA4B-7347A2142CFF}</ProjectGuid>
<OutputType>WinExe</OutputType>
<AppDesignerFolder>Properties</AppDesignerFolder>
<RootNamespace>EonaCat.HID</RootNamespace>
<AssemblyName>EonaCat HID Analyzer</AssemblyName>
<TargetFrameworkVersion>v4.8</TargetFrameworkVersion>
<FileAlignment>512</FileAlignment>
<TargetFrameworkProfile />
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<PlatformTarget>AnyCPU</PlatformTarget>
<DebugSymbols>true</DebugSymbols>
<DebugType>full</DebugType>
<Optimize>false</Optimize>
<OutputPath>bin\Debug\</OutputPath>
<DefineConstants>DEBUG;TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<PlatformTarget>AnyCPU</PlatformTarget>
<DebugType>pdbonly</DebugType>
<Optimize>true</Optimize>
<OutputPath>bin\Release\</OutputPath>
<DefineConstants>TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<PropertyGroup>
<ApplicationIcon>
</ApplicationIcon>
</PropertyGroup>
<ItemGroup>
<Reference Include="System" />
<Reference Include="System.Core" />
<Reference Include="System.Management" />
<Reference Include="System.Xml.Linq" />
<Reference Include="System.Data.DataSetExtensions" />
<Reference Include="Microsoft.CSharp" />
<Reference Include="System.Data" />
<Reference Include="System.Deployment" />
<Reference Include="System.Drawing" />
<Reference Include="System.Windows.Forms" />
<Reference Include="System.Xml" />
</ItemGroup>
<ItemGroup>
<Compile Include="AboutBox.cs">
<SubType>Form</SubType>
</Compile>
<Compile Include="AboutBox.designer.cs">
<DependentUpon>AboutBox.cs</DependentUpon>
</Compile>
<Compile Include="MainForm.cs">
<SubType>Form</SubType>
</Compile>
<Compile Include="MainForm.Designer.cs">
<DependentUpon>MainForm.cs</DependentUpon>
</Compile>
<Compile Include="Program.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<EmbeddedResource Include="AboutBox.resx">
<DependentUpon>AboutBox.cs</DependentUpon>
<SubType>Designer</SubType>
</EmbeddedResource>
<EmbeddedResource Include="MainForm.resx">
<DependentUpon>MainForm.cs</DependentUpon>
<SubType>Designer</SubType>
</EmbeddedResource>
<EmbeddedResource Include="Properties\Resources.resx">
<Generator>ResXFileCodeGenerator</Generator>
<LastGenOutput>Resources.Designer.cs</LastGenOutput>
<SubType>Designer</SubType>
</EmbeddedResource>
<Compile Include="Properties\Resources.Designer.cs">
<AutoGen>True</AutoGen>
<DependentUpon>Resources.resx</DependentUpon>
<DesignTime>True</DesignTime>
</Compile>
<None Include="Properties\Settings.settings">
<Generator>SettingsSingleFileGenerator</Generator>
<LastGenOutput>Settings.Designer.cs</LastGenOutput>
</None>
<Compile Include="Properties\Settings.Designer.cs">
<AutoGen>True</AutoGen>
<DependentUpon>Settings.settings</DependentUpon>
<DesignTimeSharedInput>True</DesignTimeSharedInput>
</Compile>
</ItemGroup>
<ItemGroup>
<None Include="App.config" />
</ItemGroup>
<ItemGroup>
<Content Include="icon.ico" />
</ItemGroup>
<ItemGroup>
<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.
Other similar extension points exist, see Microsoft.Common.targets.
<Target Name="BeforeBuild">
</Target>
<Target Name="AfterBuild">
</Target>
-->
</Project>

485
Analyzer/MainForm.cs Normal file
View File

@ -0,0 +1,485 @@
using EonaCat.HID.EventArguments;
using EonaCat.HID.Helpers;
using EonaCat.HID.Models;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Drawing;
using System.Globalization;
using System.Linq;
using System.Reflection;
using System.Threading.Tasks;
using System.Windows.Forms;
namespace EonaCat.HID.Analyzer
{
// 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 partial class MainForm : Form
{
IHidManager _deviceManager;
private IHid _device;
private IEnumerable<IHid> _deviceList;
public MainForm()
{
InitializeComponent();
CreateDeviceManager();
}
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
{
rtbEventLog.Font = new Font("Consolas", 9);
}
catch (Exception ex)
{
PopupException(ex.Message);
}
}
private void MainForm_Shown(object sender, System.EventArgs e)
{
try
{
RefreshDevices();
UpdateDeviceList();
toolStripStatusLabel1.Text = "Please select device and click open to start.";
}
catch (Exception ex)
{
PopupException(ex.Message);
}
}
private void AppendEventLog(string result, Color? color = null, bool appendNewLine = true)
{
var currentColor = color ?? Color.Black;
if (appendNewLine)
{
result += Environment.NewLine;
}
// update from UI thread
Invoke(new MethodInvoker(() =>
{
rtbEventLog.SelectionStart = rtbEventLog.TextLength;
rtbEventLog.SelectionLength = 0;
rtbEventLog.SelectionColor = currentColor;
rtbEventLog.AppendText(result);
if (!rtbEventLog.Focused)
{
rtbEventLog.ScrollToCaret();
}
}));
}
private void UpdateDeviceList()
{
dataGridView1.SelectionChanged -= DataGridView1_SelectionChanged;
dataGridView1.Rows.Clear();
for (int i = 0; i < _deviceList.Count(); i++)
{
IHid device = _deviceList.ElementAt(i);
var deviceName = "";
var deviceManufacturer = "";
var deviceSerialNumber = "";
deviceName = device.ProductName;
deviceManufacturer = device.Manufacturer;
deviceSerialNumber = device.SerialNumber;
var isWritingSupported = device.IsWritingSupport;
var isReadingSupported = device.IsReadingSupport;
var row = new string[]
{
(i + 1).ToString(),
deviceName,
deviceManufacturer,
deviceSerialNumber,
isReadingSupported.ToString(),
isWritingSupported.ToString(),
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;
DataGridView1_SelectionChanged(this, null);
}
private void PopupException(string message, string caption = "Exception")
{
Invoke(new Action(() =>
{
MessageBox.Show(message, caption, MessageBoxButtons.OK, MessageBoxIcon.Error);
}));
}
private void NewToolStripMenuItem_Click(object sender, System.EventArgs e)
{
try
{
Process.Start(Assembly.GetExecutingAssembly().Location);
}
catch (Exception ex)
{
PopupException(ex.Message);
}
}
private void ExitToolStripMenuItem_Click(object sender, System.EventArgs e)
{
try
{
this.Close();
}
catch (Exception ex)
{
PopupException(ex.Message);
}
}
private void AboutToolStripMenuItem_Click(object sender, EventArgs e)
{
try
{
HelpToolStripButton_Click(sender, e);
}
catch (Exception ex)
{
PopupException(ex.Message);
}
}
private void ToolStripButtonReload_Click(object sender, EventArgs e)
{
try
{
RefreshDevices();
UpdateDeviceList();
}
catch (Exception ex)
{
PopupException(ex.Message);
}
}
private void ToolStripButtonFilter_Click(object sender, EventArgs e)
{
try
{
ushort? vid = null;
ushort? pid = null;
var str = toolStripTextBoxVidPid.Text.Split(':');
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)
{
PopupException(ex.Message);
}
}
private void ToolStripButtonConnect_Click(object sender, EventArgs e)
{
ConnectToSelectedDeviceAsync().ConfigureAwait(false);
}
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
{
var str = $"Device error for {e.Device.ProductName} => {e.Exception.Message}";
AppendEventLog(str, Color.Red);
}
catch (Exception ex)
{
PopupException(ex.Message);
}
}
private void OnDataReceived(object sender, HidDataReceivedEventArgs e)
{
try
{
var str = $"Rx Input Report from device {e.Device.ProductName} => {BitConverter.ToString(e.Report.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 {0} --> VID {1:X4}, PID {2:X4}", e.Device.ProductName, 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 {0} --> VID {1:X4}, PID {2:X4}", e.Device.ProductName, e.Device.VendorId, e.Device.ProductId);
AppendEventLog(str, Color.Orange);
}
catch (Exception ex)
{
PopupException(ex.Message);
}
}
private void ToolStripButtonClear_Click(object sender, EventArgs e)
{
try
{
rtbEventLog.Clear();
}
catch (Exception ex)
{
PopupException(ex.Message);
}
}
private void HelpToolStripButton_Click(object sender, EventArgs e)
{
try
{
var aboutbox = new AboutBox();
aboutbox.ShowDialog();
}
catch (Exception ex)
{
PopupException(ex.Message);
}
}
private async void ButtonReadInput_Click(object sender, EventArgs e)
{
try
{
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!");
}
var report = await _device.ReadInputReportAsync();
if (report == null || report.Data.Length < 2)
{
AppendEventLog("Received report is null or is too short to contain a valid Report ID.", Color.Red);
return;
}
var str = string.Format("Rx Input Report [{0}] <-- {1}", report.Data.Length, BitConverter.ToString(report.Data));
AppendEventLog(str, Color.Blue);
}
catch (Exception ex)
{
PopupException(ex.Message);
}
}
private async void ButtonWriteOutput_Click(object sender, EventArgs e)
{
try
{
if (_device == null)
{
AppendEventLog("No device connected. Please select a device and click 'Connect'.", Color.Red);
return;
}
byte hidReportId = byte.Parse(comboBoxReportId.Text.Trim());
byte[] dataBuffer = ByteHelper.HexStringToByteArray(textBoxWriteData.Text.Trim());
if (_device.OutputReportByteLength <= 0)
{
throw new Exception("This device has no Output Report support!");
}
if (dataBuffer.Length > _device.OutputReportByteLength - 1)
{
throw new Exception("Output Report Length Exceeds allowed size.");
}
var outputReport = new HidReport(hidReportId, dataBuffer);
await _device.WriteOutputReportAsync(outputReport);
AppendEventLog($"Output report sent (Report ID: 0x{hidReportId:X2}): {ByteHelper.ByteArrayToHexString(dataBuffer)}", Color.DarkGreen);
}
catch (Exception ex)
{
PopupException("Error sending 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!");
}
HidReport report = await _device.GetFeatureReportAsync(hidReportId);
if (report == null || report.Data == null || report.Data.Length < 1)
{
AppendEventLog("Received feature report is null or too short.", Color.Red);
return;
}
string hexString = $"{report.ReportId:X2} - {ByteHelper.ByteArrayToHexString(report.Data)}";
var str = $"Rx Feature Report [{report.Data.Length + 1}] <-- {hexString}";
AppendEventLog(str, Color.Blue);
}
catch (Exception ex)
{
PopupException(ex.Message);
}
}
private async void ButtonWriteFeature_Click(object sender, EventArgs e)
{
try
{
if (!byte.TryParse(comboBoxReportId.Text, out var hidReportId))
{
throw new FormatException("Invalid Report ID format.");
}
var data = ByteHelper.HexStringToByteArray(textBoxWriteData.Text);
int maxLen = _device.FeatureReportByteLength - 1;
if (data.Length > maxLen)
{
throw new InvalidOperationException($"Feature report data length exceeds max allowed ({maxLen}).");
}
var reportData = new byte[data.Length];
Array.Copy(data, reportData, data.Length);
var hidReport = new HidReport(hidReportId, reportData);
await _device.SendFeatureReportAsync(hidReport);
string logMsg = $"Tx Feature Report [{data.Length + 1}] --> {ByteHelper.ByteArrayToHexString(new[] { hidReportId }.Concat(reportData).ToArray())}";
AppendEventLog(logMsg, Color.DarkGreen);
}
catch (Exception ex)
{
PopupException(ex.Message);
}
}
private void DataGridView1_SelectionChanged(object sender, EventArgs e)
{
try
{
if (dataGridView1.SelectedRows.Count <= 0)
{
return;
}
var index = dataGridView1.SelectedRows[0].Index;
var info = _deviceList.ElementAt(index);
toolStripTextBoxVidPid.Text = string.Format("{0:X4}:{1:X4}", info.VendorId, info.ProductId);
}
catch (Exception ex)
{
PopupException(ex.Message);
}
}
private void dataGridView1_DoubleClick(object sender, EventArgs e)
{
ConnectToSelectedDeviceAsync().ConfigureAwait(false);
}
private void toolStripTextBoxVidPid_Enter(object sender, EventArgs e)
{
ToolStripButtonFilter_Click(sender, e);
}
}
}

4470
Analyzer/MainForm.resx Normal file

File diff suppressed because it is too large Load Diff

21
Analyzer/Program.cs Normal file
View File

@ -0,0 +1,21 @@
using System;
using System.Windows.Forms;
namespace EonaCat.HID.Analyzer
{
// 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 Program
{
/// <summary>
/// The main entry point for the application.
/// </summary>
[STAThread]
private static void Main()
{
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
Application.Run(new MainForm());
}
}
}

View File

@ -0,0 +1,35 @@
using System.Reflection;
using System.Runtime.InteropServices;
// General Information about an assembly is controlled through the following
// set of attributes. Change these attribute values to modify the information
// associated with an assembly.
[assembly: AssemblyTitle("USB HID Analyzer")]
[assembly: AssemblyDescription("HID Devices")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("EonaCat (Jeroen Saey)")]
[assembly: AssemblyProduct("USB HID Analyzer")]
[assembly: AssemblyCopyright("Copyright © 2024")]
[assembly: AssemblyTrademark("EonaCat (Jeroen Saey)")]
[assembly: AssemblyCulture("")]
// Setting ComVisible to false makes the types in this assembly not visible
// to COM components. If you need to access a type in this assembly from
// COM, set the ComVisible attribute to true on that type.
[assembly: ComVisible(false)]
// The following GUID is for the ID of the typelib if this project is exposed to COM
[assembly: Guid("185412ac-91cc-4e99-9a8e-dd2af6b817ba")]
// Version information for an assembly consists of the following four values:
//
// Major Version
// Minor Version
// Build Number
// Revision
//
// You can specify all the values or you can default the Build and Revision Numbers
// by using the '*' as shown below:
// [assembly: AssemblyVersion("1.0.*")]
[assembly: AssemblyVersion("1.0.0.0")]
[assembly: AssemblyFileVersion("1.0.0.0")]

View File

@ -0,0 +1,63 @@
//------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by a tool.
// Runtime Version:4.0.30319.42000
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------
namespace EonaCat.HID.Properties {
using System;
/// <summary>
/// A strongly-typed resource class, for looking up localized strings, etc.
/// </summary>
// This class was auto-generated by the StronglyTypedResourceBuilder
// class via a tool like ResGen or Visual Studio.
// To add or remove a member, edit your .ResX file then rerun ResGen
// with the /str option, or rebuild your VS project.
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
internal class Resources {
private static global::System.Resources.ResourceManager resourceMan;
private static global::System.Globalization.CultureInfo resourceCulture;
[global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
internal Resources() {
}
/// <summary>
/// Returns the cached ResourceManager instance used by this class.
/// </summary>
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
internal static global::System.Resources.ResourceManager ResourceManager {
get {
if (object.ReferenceEquals(resourceMan, null)) {
global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("EonaCat.HID.Properties.Resources", typeof(Resources).Assembly);
resourceMan = temp;
}
return resourceMan;
}
}
/// <summary>
/// Overrides the current thread's CurrentUICulture property for all
/// resource lookups using this strongly typed resource class.
/// </summary>
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
internal static global::System.Globalization.CultureInfo Culture {
get {
return resourceCulture;
}
set {
resourceCulture = value;
}
}
}
}

View File

@ -0,0 +1,117 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
</root>

26
Analyzer/Properties/Settings.Designer.cs generated Normal file
View File

@ -0,0 +1,26 @@
//------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by a tool.
// Runtime Version:4.0.30319.42000
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------
namespace EonaCat.HID.Properties {
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "17.9.0.0")]
internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase {
private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings())));
public static Settings Default {
get {
return defaultInstance;
}
}
}
}

View File

@ -0,0 +1,7 @@
<?xml version='1.0' encoding='utf-8'?>
<SettingsFile xmlns="http://schemas.microsoft.com/VisualStudio/2004/01/settings" CurrentProfile="(Default)">
<Profiles>
<Profile Name="(Default)" />
</Profiles>
<Settings />
</SettingsFile>

BIN
Analyzer/icon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 248 KiB

BIN
Analyzer/icons/clean.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 784 B

BIN
Analyzer/icons/device.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 679 B

BIN
Analyzer/icons/filter.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 453 B

BIN
Analyzer/icons/info.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 516 B

BIN
Analyzer/icons/refresh.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 754 B

565
Analyzer/mainForm.Designer.cs generated Normal file
View File

@ -0,0 +1,565 @@
namespace EonaCat.HID.Analyzer
{
partial class MainForm
{
/// <summary>
/// Required designer variable.
/// </summary>
private System.ComponentModel.IContainer components = null;
/// <summary>
/// Clean up any resources being used.
/// </summary>
/// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
protected override void Dispose(bool disposing)
{
if (disposing && (components != null))
{
components.Dispose();
}
base.Dispose(disposing);
}
#region Windows Form Designer generated code
/// <summary>
/// Required method for Designer support - do not modify
/// the contents of this method with the code editor.
/// </summary>
private void InitializeComponent()
{
System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(MainForm));
this.textBoxWriteData = new System.Windows.Forms.TextBox();
this.dataGridView1 = new System.Windows.Forms.DataGridView();
this.tableLayoutPanel1 = new System.Windows.Forms.TableLayoutPanel();
this.toolStrip1 = new System.Windows.Forms.ToolStrip();
this.toolStripSeparator = new System.Windows.Forms.ToolStripSeparator();
this.toolStripButtonReload = new System.Windows.Forms.ToolStripButton();
this.toolStripButtonOpen = new System.Windows.Forms.ToolStripButton();
this.toolStripButtonClear = new System.Windows.Forms.ToolStripButton();
this.toolStripButtonFilter = new System.Windows.Forms.ToolStripButton();
this.toolStripTextBoxVidPid = new System.Windows.Forms.ToolStripTextBox();
this.menuStrip1 = new System.Windows.Forms.MenuStrip();
this.fileToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.newToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.exitToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.helpToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.aboutToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.groupBox1 = new System.Windows.Forms.GroupBox();
this.comboBoxReportId = new System.Windows.Forms.ComboBox();
this.label2 = new System.Windows.Forms.Label();
this.label1 = new System.Windows.Forms.Label();
this.buttonWriteFeature = new System.Windows.Forms.Button();
this.buttonReadFeature = new System.Windows.Forms.Button();
this.buttonWriteOutput = new System.Windows.Forms.Button();
this.buttonReadInput = new System.Windows.Forms.Button();
this.groupBox2 = new System.Windows.Forms.GroupBox();
this.groupBox3 = new System.Windows.Forms.GroupBox();
this.rtbEventLog = new System.Windows.Forms.RichTextBox();
this.statusStrip1 = new System.Windows.Forms.StatusStrip();
this.toolStripStatusLabel1 = new System.Windows.Forms.ToolStripStatusLabel();
this.Column1 = new System.Windows.Forms.DataGridViewTextBoxColumn();
this.Column9 = new System.Windows.Forms.DataGridViewTextBoxColumn();
this.Column10 = new System.Windows.Forms.DataGridViewTextBoxColumn();
this.Column11 = new System.Windows.Forms.DataGridViewTextBoxColumn();
this.Column6 = new System.Windows.Forms.DataGridViewTextBoxColumn();
this.Column3 = new System.Windows.Forms.DataGridViewTextBoxColumn();
this.Column7 = new System.Windows.Forms.DataGridViewTextBoxColumn();
this.Column4 = new System.Windows.Forms.DataGridViewTextBoxColumn();
this.Column5 = new System.Windows.Forms.DataGridViewTextBoxColumn();
this.Column2 = new System.Windows.Forms.DataGridViewTextBoxColumn();
this.Column8 = new System.Windows.Forms.DataGridViewTextBoxColumn();
((System.ComponentModel.ISupportInitialize)(this.dataGridView1)).BeginInit();
this.tableLayoutPanel1.SuspendLayout();
this.toolStrip1.SuspendLayout();
this.menuStrip1.SuspendLayout();
this.groupBox1.SuspendLayout();
this.groupBox2.SuspendLayout();
this.groupBox3.SuspendLayout();
this.statusStrip1.SuspendLayout();
this.SuspendLayout();
//
// textBoxWriteData
//
this.textBoxWriteData.Location = new System.Drawing.Point(104, 45);
this.textBoxWriteData.Name = "textBoxWriteData";
this.textBoxWriteData.Size = new System.Drawing.Size(192, 20);
this.textBoxWriteData.TabIndex = 1;
this.textBoxWriteData.Text = "02";
//
// dataGridView1
//
this.dataGridView1.AllowUserToAddRows = false;
this.dataGridView1.AllowUserToDeleteRows = false;
this.dataGridView1.BackgroundColor = System.Drawing.SystemColors.ButtonFace;
this.dataGridView1.ColumnHeadersHeightSizeMode = System.Windows.Forms.DataGridViewColumnHeadersHeightSizeMode.AutoSize;
this.dataGridView1.Columns.AddRange(new System.Windows.Forms.DataGridViewColumn[] {
this.Column1,
this.Column9,
this.Column10,
this.Column11,
this.Column6,
this.Column3,
this.Column7,
this.Column4,
this.Column5,
this.Column2,
this.Column8});
this.dataGridView1.Dock = System.Windows.Forms.DockStyle.Fill;
this.dataGridView1.Location = new System.Drawing.Point(3, 16);
this.dataGridView1.MultiSelect = false;
this.dataGridView1.Name = "dataGridView1";
this.dataGridView1.ReadOnly = true;
this.dataGridView1.SelectionMode = System.Windows.Forms.DataGridViewSelectionMode.FullRowSelect;
this.dataGridView1.Size = new System.Drawing.Size(822, 286);
this.dataGridView1.TabIndex = 0;
this.dataGridView1.SelectionChanged += new System.EventHandler(this.DataGridView1_SelectionChanged);
this.dataGridView1.DoubleClick += new System.EventHandler(this.dataGridView1_DoubleClick);
//
// tableLayoutPanel1
//
this.tableLayoutPanel1.ColumnCount = 1;
this.tableLayoutPanel1.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 100F));
this.tableLayoutPanel1.Controls.Add(this.toolStrip1, 0, 1);
this.tableLayoutPanel1.Controls.Add(this.menuStrip1, 0, 0);
this.tableLayoutPanel1.Controls.Add(this.groupBox1, 0, 2);
this.tableLayoutPanel1.Controls.Add(this.groupBox2, 0, 3);
this.tableLayoutPanel1.Controls.Add(this.groupBox3, 0, 4);
this.tableLayoutPanel1.Controls.Add(this.statusStrip1, 0, 5);
this.tableLayoutPanel1.Dock = System.Windows.Forms.DockStyle.Fill;
this.tableLayoutPanel1.Location = new System.Drawing.Point(0, 0);
this.tableLayoutPanel1.Name = "tableLayoutPanel1";
this.tableLayoutPanel1.RowCount = 6;
this.tableLayoutPanel1.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Absolute, 25F));
this.tableLayoutPanel1.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Absolute, 25F));
this.tableLayoutPanel1.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Absolute, 89F));
this.tableLayoutPanel1.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 48.18182F));
this.tableLayoutPanel1.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 51.81818F));
this.tableLayoutPanel1.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Absolute, 25F));
this.tableLayoutPanel1.Size = new System.Drawing.Size(834, 811);
this.tableLayoutPanel1.TabIndex = 18;
//
// toolStrip1
//
this.toolStrip1.Items.AddRange(new System.Windows.Forms.ToolStripItem[] {
this.toolStripSeparator,
this.toolStripButtonReload,
this.toolStripButtonOpen,
this.toolStripButtonClear,
this.toolStripButtonFilter,
this.toolStripTextBoxVidPid});
this.toolStrip1.Location = new System.Drawing.Point(0, 25);
this.toolStrip1.Name = "toolStrip1";
this.toolStrip1.Size = new System.Drawing.Size(834, 25);
this.toolStrip1.TabIndex = 1;
this.toolStrip1.Text = "toolStrip1";
//
// toolStripSeparator
//
this.toolStripSeparator.Name = "toolStripSeparator";
this.toolStripSeparator.Size = new System.Drawing.Size(6, 25);
//
// toolStripButtonReload
//
this.toolStripButtonReload.Image = ((System.Drawing.Image)(resources.GetObject("toolStripButtonReload.Image")));
this.toolStripButtonReload.ImageTransparentColor = System.Drawing.Color.Magenta;
this.toolStripButtonReload.Name = "toolStripButtonReload";
this.toolStripButtonReload.Size = new System.Drawing.Size(63, 22);
this.toolStripButtonReload.Text = "Reload";
this.toolStripButtonReload.Click += new System.EventHandler(this.ToolStripButtonReload_Click);
//
// toolStripButtonOpen
//
this.toolStripButtonOpen.Image = ((System.Drawing.Image)(resources.GetObject("toolStripButtonOpen.Image")));
this.toolStripButtonOpen.ImageTransparentColor = System.Drawing.Color.Magenta;
this.toolStripButtonOpen.Name = "toolStripButtonOpen";
this.toolStripButtonOpen.Size = new System.Drawing.Size(56, 22);
this.toolStripButtonOpen.Text = "Open";
this.toolStripButtonOpen.Click += new System.EventHandler(this.ToolStripButtonConnect_Click);
//
// toolStripButtonClear
//
this.toolStripButtonClear.Image = ((System.Drawing.Image)(resources.GetObject("toolStripButtonClear.Image")));
this.toolStripButtonClear.ImageTransparentColor = System.Drawing.Color.Magenta;
this.toolStripButtonClear.Name = "toolStripButtonClear";
this.toolStripButtonClear.Size = new System.Drawing.Size(54, 22);
this.toolStripButtonClear.Text = "Clear";
this.toolStripButtonClear.Click += new System.EventHandler(this.ToolStripButtonClear_Click);
//
// toolStripButtonFilter
//
this.toolStripButtonFilter.Alignment = System.Windows.Forms.ToolStripItemAlignment.Right;
this.toolStripButtonFilter.Image = ((System.Drawing.Image)(resources.GetObject("toolStripButtonFilter.Image")));
this.toolStripButtonFilter.ImageTransparentColor = System.Drawing.Color.Magenta;
this.toolStripButtonFilter.Name = "toolStripButtonFilter";
this.toolStripButtonFilter.Size = new System.Drawing.Size(53, 22);
this.toolStripButtonFilter.Text = "Filter";
this.toolStripButtonFilter.Click += new System.EventHandler(this.ToolStripButtonFilter_Click);
//
// toolStripTextBoxVidPid
//
this.toolStripTextBoxVidPid.Alignment = System.Windows.Forms.ToolStripItemAlignment.Right;
this.toolStripTextBoxVidPid.BorderStyle = System.Windows.Forms.BorderStyle.FixedSingle;
this.toolStripTextBoxVidPid.Font = new System.Drawing.Font("Segoe UI", 9F);
this.toolStripTextBoxVidPid.Name = "toolStripTextBoxVidPid";
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
//
this.menuStrip1.Items.AddRange(new System.Windows.Forms.ToolStripItem[] {
this.fileToolStripMenuItem,
this.helpToolStripMenuItem});
this.menuStrip1.Location = new System.Drawing.Point(0, 0);
this.menuStrip1.Name = "menuStrip1";
this.menuStrip1.Size = new System.Drawing.Size(834, 24);
this.menuStrip1.TabIndex = 0;
this.menuStrip1.Text = "menuStrip1";
//
// fileToolStripMenuItem
//
this.fileToolStripMenuItem.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] {
this.newToolStripMenuItem,
this.exitToolStripMenuItem});
this.fileToolStripMenuItem.Name = "fileToolStripMenuItem";
this.fileToolStripMenuItem.Size = new System.Drawing.Size(37, 20);
this.fileToolStripMenuItem.Text = "&File";
//
// newToolStripMenuItem
//
this.newToolStripMenuItem.Image = ((System.Drawing.Image)(resources.GetObject("newToolStripMenuItem.Image")));
this.newToolStripMenuItem.ImageTransparentColor = System.Drawing.Color.Magenta;
this.newToolStripMenuItem.Name = "newToolStripMenuItem";
this.newToolStripMenuItem.ShortcutKeys = ((System.Windows.Forms.Keys)((System.Windows.Forms.Keys.Control | System.Windows.Forms.Keys.N)));
this.newToolStripMenuItem.Size = new System.Drawing.Size(141, 22);
this.newToolStripMenuItem.Text = "&New";
this.newToolStripMenuItem.Click += new System.EventHandler(this.NewToolStripMenuItem_Click);
//
// exitToolStripMenuItem
//
this.exitToolStripMenuItem.Name = "exitToolStripMenuItem";
this.exitToolStripMenuItem.Size = new System.Drawing.Size(141, 22);
this.exitToolStripMenuItem.Text = "E&xit";
this.exitToolStripMenuItem.Click += new System.EventHandler(this.ExitToolStripMenuItem_Click);
//
// helpToolStripMenuItem
//
this.helpToolStripMenuItem.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] {
this.aboutToolStripMenuItem});
this.helpToolStripMenuItem.Name = "helpToolStripMenuItem";
this.helpToolStripMenuItem.Size = new System.Drawing.Size(44, 20);
this.helpToolStripMenuItem.Text = "&Help";
//
// aboutToolStripMenuItem
//
this.aboutToolStripMenuItem.Name = "aboutToolStripMenuItem";
this.aboutToolStripMenuItem.Size = new System.Drawing.Size(116, 22);
this.aboutToolStripMenuItem.Text = "&About...";
this.aboutToolStripMenuItem.Click += new System.EventHandler(this.AboutToolStripMenuItem_Click);
//
// groupBox1
//
this.groupBox1.Controls.Add(this.comboBoxReportId);
this.groupBox1.Controls.Add(this.label2);
this.groupBox1.Controls.Add(this.label1);
this.groupBox1.Controls.Add(this.buttonWriteFeature);
this.groupBox1.Controls.Add(this.buttonReadFeature);
this.groupBox1.Controls.Add(this.buttonWriteOutput);
this.groupBox1.Controls.Add(this.buttonReadInput);
this.groupBox1.Controls.Add(this.textBoxWriteData);
this.groupBox1.Dock = System.Windows.Forms.DockStyle.Fill;
this.groupBox1.Location = new System.Drawing.Point(3, 53);
this.groupBox1.Name = "groupBox1";
this.groupBox1.Size = new System.Drawing.Size(828, 83);
this.groupBox1.TabIndex = 2;
this.groupBox1.TabStop = false;
this.groupBox1.Text = "Read/Write Operations";
//
// comboBoxReportId
//
this.comboBoxReportId.FormattingEnabled = true;
this.comboBoxReportId.Items.AddRange(new object[] {
"0",
"1",
"2",
"3",
"4",
"5",
"6",
"7",
"8",
"9",
"10",
"11",
"12",
"13",
"14",
"15",
"16",
"17",
"18",
"19",
"20"});
this.comboBoxReportId.Location = new System.Drawing.Point(104, 19);
this.comboBoxReportId.Name = "comboBoxReportId";
this.comboBoxReportId.Size = new System.Drawing.Size(81, 21);
this.comboBoxReportId.TabIndex = 0;
this.comboBoxReportId.Text = "0";
//
// label2
//
this.label2.AutoSize = true;
this.label2.Location = new System.Drawing.Point(23, 22);
this.label2.Name = "label2";
this.label2.Size = new System.Drawing.Size(75, 13);
this.label2.TabIndex = 6;
this.label2.Text = "Hid Report ID:";
//
// label1
//
this.label1.AutoSize = true;
this.label1.Location = new System.Drawing.Point(40, 48);
this.label1.Name = "label1";
this.label1.Size = new System.Drawing.Size(58, 13);
this.label1.TabIndex = 7;
this.label1.Text = "Write Data";
//
// buttonWriteFeature
//
this.buttonWriteFeature.Location = new System.Drawing.Point(428, 43);
this.buttonWriteFeature.Name = "buttonWriteFeature";
this.buttonWriteFeature.Size = new System.Drawing.Size(120, 22);
this.buttonWriteFeature.TabIndex = 5;
this.buttonWriteFeature.Text = "Write Feature Report";
this.buttonWriteFeature.UseVisualStyleBackColor = true;
this.buttonWriteFeature.Click += new System.EventHandler(this.ButtonWriteFeature_Click);
//
// buttonReadFeature
//
this.buttonReadFeature.Location = new System.Drawing.Point(428, 17);
this.buttonReadFeature.Name = "buttonReadFeature";
this.buttonReadFeature.Size = new System.Drawing.Size(120, 22);
this.buttonReadFeature.TabIndex = 4;
this.buttonReadFeature.Text = "Read Feature Report";
this.buttonReadFeature.UseVisualStyleBackColor = true;
this.buttonReadFeature.Click += new System.EventHandler(this.ButtonReadFeature_Click);
//
// buttonWriteOutput
//
this.buttonWriteOutput.Location = new System.Drawing.Point(302, 43);
this.buttonWriteOutput.Name = "buttonWriteOutput";
this.buttonWriteOutput.Size = new System.Drawing.Size(120, 22);
this.buttonWriteOutput.TabIndex = 3;
this.buttonWriteOutput.Text = "Write Output Report";
this.buttonWriteOutput.UseVisualStyleBackColor = true;
this.buttonWriteOutput.Click += new System.EventHandler(this.ButtonWriteOutput_Click);
//
// buttonReadInput
//
this.buttonReadInput.Location = new System.Drawing.Point(302, 17);
this.buttonReadInput.Name = "buttonReadInput";
this.buttonReadInput.Size = new System.Drawing.Size(120, 22);
this.buttonReadInput.TabIndex = 2;
this.buttonReadInput.Text = "Read Input Report";
this.buttonReadInput.UseVisualStyleBackColor = true;
this.buttonReadInput.Click += new System.EventHandler(this.ButtonReadInput_Click);
//
// groupBox2
//
this.groupBox2.Controls.Add(this.dataGridView1);
this.groupBox2.Dock = System.Windows.Forms.DockStyle.Fill;
this.groupBox2.Location = new System.Drawing.Point(3, 142);
this.groupBox2.Name = "groupBox2";
this.groupBox2.Size = new System.Drawing.Size(828, 305);
this.groupBox2.TabIndex = 3;
this.groupBox2.TabStop = false;
this.groupBox2.Text = "Devices";
//
// groupBox3
//
this.groupBox3.Controls.Add(this.rtbEventLog);
this.groupBox3.Dock = System.Windows.Forms.DockStyle.Fill;
this.groupBox3.Location = new System.Drawing.Point(3, 453);
this.groupBox3.Name = "groupBox3";
this.groupBox3.Size = new System.Drawing.Size(828, 329);
this.groupBox3.TabIndex = 4;
this.groupBox3.TabStop = false;
this.groupBox3.Text = "Log";
//
// rtbEventLog
//
this.rtbEventLog.Dock = System.Windows.Forms.DockStyle.Fill;
this.rtbEventLog.Location = new System.Drawing.Point(3, 16);
this.rtbEventLog.Name = "rtbEventLog";
this.rtbEventLog.ReadOnly = true;
this.rtbEventLog.Size = new System.Drawing.Size(822, 310);
this.rtbEventLog.TabIndex = 0;
this.rtbEventLog.Text = "";
//
// statusStrip1
//
this.statusStrip1.Items.AddRange(new System.Windows.Forms.ToolStripItem[] {
this.toolStripStatusLabel1});
this.statusStrip1.Location = new System.Drawing.Point(0, 789);
this.statusStrip1.Name = "statusStrip1";
this.statusStrip1.Size = new System.Drawing.Size(834, 22);
this.statusStrip1.TabIndex = 5;
this.statusStrip1.Text = "statusStrip1";
//
// toolStripStatusLabel1
//
this.toolStripStatusLabel1.Name = "toolStripStatusLabel1";
this.toolStripStatusLabel1.Size = new System.Drawing.Size(118, 17);
this.toolStripStatusLabel1.Text = "toolStripStatusLabel1";
//
// Column1
//
this.Column1.HeaderText = "No";
this.Column1.Name = "Column1";
this.Column1.ReadOnly = true;
this.Column1.Width = 87;
//
// Column9
//
this.Column9.AutoSizeMode = System.Windows.Forms.DataGridViewAutoSizeColumnMode.Fill;
this.Column9.HeaderText = "Name";
this.Column9.Name = "Column9";
this.Column9.ReadOnly = true;
//
// Column10
//
this.Column10.AutoSizeMode = System.Windows.Forms.DataGridViewAutoSizeColumnMode.Fill;
this.Column10.HeaderText = "Manufacturer";
this.Column10.Name = "Column10";
this.Column10.ReadOnly = true;
//
// Column11
//
this.Column11.AutoSizeMode = System.Windows.Forms.DataGridViewAutoSizeColumnMode.Fill;
this.Column11.HeaderText = "SerialNo";
this.Column11.Name = "Column11";
this.Column11.ReadOnly = true;
//
// Column6
//
this.Column6.HeaderText = "CanRead";
this.Column6.Name = "Column6";
this.Column6.ReadOnly = true;
//
// Column3
//
this.Column3.HeaderText = "CanWrite";
this.Column3.Name = "Column3";
this.Column3.ReadOnly = true;
//
// Column7
//
this.Column7.HeaderText = "InputReport";
this.Column7.Name = "Column7";
this.Column7.ReadOnly = true;
this.Column7.Width = 86;
//
// Column4
//
this.Column4.HeaderText = "OutputReport";
this.Column4.Name = "Column4";
this.Column4.ReadOnly = true;
this.Column4.Width = 87;
//
// Column5
//
this.Column5.HeaderText = "FeatureReport";
this.Column5.Name = "Column5";
this.Column5.ReadOnly = true;
this.Column5.Width = 86;
//
// Column2
//
this.Column2.AutoSizeMode = System.Windows.Forms.DataGridViewAutoSizeColumnMode.Fill;
this.Column2.HeaderText = "Info";
this.Column2.Name = "Column2";
this.Column2.ReadOnly = true;
//
// Column8
//
this.Column8.AutoSizeMode = System.Windows.Forms.DataGridViewAutoSizeColumnMode.Fill;
this.Column8.HeaderText = "DevicePath";
this.Column8.Name = "Column8";
this.Column8.ReadOnly = true;
//
// MainForm
//
this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F);
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
this.ClientSize = new System.Drawing.Size(834, 811);
this.Controls.Add(this.tableLayoutPanel1);
this.Icon = ((System.Drawing.Icon)(resources.GetObject("$this.Icon")));
this.MainMenuStrip = this.menuStrip1;
this.Name = "MainForm";
this.StartPosition = System.Windows.Forms.FormStartPosition.CenterScreen;
this.Text = "EonaCat HID";
this.Load += new System.EventHandler(this.MainForm_Load);
this.Shown += new System.EventHandler(this.MainForm_Shown);
((System.ComponentModel.ISupportInitialize)(this.dataGridView1)).EndInit();
this.tableLayoutPanel1.ResumeLayout(false);
this.tableLayoutPanel1.PerformLayout();
this.toolStrip1.ResumeLayout(false);
this.toolStrip1.PerformLayout();
this.menuStrip1.ResumeLayout(false);
this.menuStrip1.PerformLayout();
this.groupBox1.ResumeLayout(false);
this.groupBox1.PerformLayout();
this.groupBox2.ResumeLayout(false);
this.groupBox3.ResumeLayout(false);
this.statusStrip1.ResumeLayout(false);
this.statusStrip1.PerformLayout();
this.ResumeLayout(false);
}
#endregion
private System.Windows.Forms.TextBox textBoxWriteData;
private System.Windows.Forms.DataGridView dataGridView1;
private System.Windows.Forms.TableLayoutPanel tableLayoutPanel1;
private System.Windows.Forms.MenuStrip menuStrip1;
private System.Windows.Forms.ToolStripMenuItem fileToolStripMenuItem;
private System.Windows.Forms.ToolStripMenuItem newToolStripMenuItem;
private System.Windows.Forms.ToolStripMenuItem exitToolStripMenuItem;
private System.Windows.Forms.ToolStripMenuItem helpToolStripMenuItem;
private System.Windows.Forms.ToolStripMenuItem aboutToolStripMenuItem;
private System.Windows.Forms.ToolStrip toolStrip1;
private System.Windows.Forms.GroupBox groupBox1;
private System.Windows.Forms.GroupBox groupBox2;
private System.Windows.Forms.GroupBox groupBox3;
private System.Windows.Forms.StatusStrip statusStrip1;
private System.Windows.Forms.ToolStripStatusLabel toolStripStatusLabel1;
private System.Windows.Forms.RichTextBox rtbEventLog;
private System.Windows.Forms.ToolStripButton toolStripButtonClear;
private System.Windows.Forms.ToolStripButton toolStripButtonOpen;
private System.Windows.Forms.ToolStripSeparator toolStripSeparator;
private System.Windows.Forms.Button buttonWriteFeature;
private System.Windows.Forms.Button buttonReadFeature;
private System.Windows.Forms.Button buttonWriteOutput;
private System.Windows.Forms.Button buttonReadInput;
private System.Windows.Forms.Label label1;
private System.Windows.Forms.Label label2;
private System.Windows.Forms.ComboBox comboBoxReportId;
private System.Windows.Forms.ToolStripButton toolStripButtonReload;
private System.Windows.Forms.ToolStripTextBox toolStripTextBoxVidPid;
private System.Windows.Forms.ToolStripButton toolStripButtonFilter;
private System.Windows.Forms.DataGridViewTextBoxColumn Column1;
private System.Windows.Forms.DataGridViewTextBoxColumn Column9;
private System.Windows.Forms.DataGridViewTextBoxColumn Column10;
private System.Windows.Forms.DataGridViewTextBoxColumn Column11;
private System.Windows.Forms.DataGridViewTextBoxColumn Column6;
private System.Windows.Forms.DataGridViewTextBoxColumn Column3;
private System.Windows.Forms.DataGridViewTextBoxColumn Column7;
private System.Windows.Forms.DataGridViewTextBoxColumn Column4;
private System.Windows.Forms.DataGridViewTextBoxColumn Column5;
private System.Windows.Forms.DataGridViewTextBoxColumn Column2;
private System.Windows.Forms.DataGridViewTextBoxColumn Column8;
}
}

View File

@ -0,0 +1,14 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\EonaCat.HID\EonaCat.HID.csproj" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,116 @@
using EonaCat.HID;
using EonaCat.HID.Models;
using System.Globalization;
namespace EonaCat.HID.Example
{
// 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 Program
{
static IHidManager _deviceManager;
static IHid _device;
static IEnumerable<IHid> _deviceList;
static async Task Main(string[] args)
{
try
{
_deviceManager = HidFactory.CreateDeviceManager();
if (_deviceManager == null)
{
Console.WriteLine("Failed to create HID manager.");
return;
}
_deviceManager.OnDeviceInserted += (s, e) =>
{
Console.WriteLine($"Inserted Device --> VID: {e.Device.VendorId:X4}, PID: {e.Device.ProductId:X4}");
};
_deviceManager.OnDeviceRemoved += (s, e) =>
{
Console.WriteLine($"Removed Device --> VID: {e.Device.VendorId:X4}, PID: {e.Device.ProductId:X4}");
};
RefreshDevices();
if (!_deviceList.Any())
{
Console.WriteLine("No HID found.");
return;
}
DisplayDevices();
Console.Write("Select a device index to connect: ");
int index = int.Parse(Console.ReadLine());
_device = _deviceList.ElementAt(index);
_device.OnDataReceived += (s, e) =>
{
Console.WriteLine($"Rx Data: {BitConverter.ToString(e.Report.Data)}");
};
_device.OnError += (s, e) =>
{
Console.WriteLine($"Error: {e.Exception.Message}");
};
_device.Open();
Console.WriteLine($"Connected to {_device.ProductName}");
await _device.StartListeningAsync(default);
Console.WriteLine("Listening... Press [Enter] to send test output report.");
Console.ReadLine();
// Example: Send output report using HidReport
var data = ByteHelper.HexStringToByteArray("01-02-03");
var reportId = (byte)0x00; // Report ID
var outputReport = new HidReport(reportId, data);
await _device.WriteOutputReportAsync(outputReport);
Console.WriteLine($"Sent output report: Report ID: {reportId}, Data: {BitConverter.ToString(data)}");
Console.WriteLine("Press [Enter] to exit...");
Console.ReadLine();
}
catch (Exception ex)
{
Console.WriteLine($"[EXCEPTION] {ex.Message}");
}
}
static void RefreshDevices(ushort? vendorId = null, ushort? productId = null)
{
_deviceList = _deviceManager.Enumerate(vendorId, productId);
}
static void DisplayDevices()
{
int i = 0;
foreach (var dev in _deviceList)
{
Console.WriteLine($"[{i++}] {dev.ProductName} | VID:PID = {dev.VendorId:X4}:{dev.ProductId:X4}");
}
}
}
public static class ByteHelper
{
public static byte[] HexStringToByteArray(string hex)
{
return hex
.Replace("-", "")
.Replace(" ", "")
.ToUpper()
.Where(c => Uri.IsHexDigit(c))
.Select((c, i) => new { c, i })
.GroupBy(x => x.i / 2)
.Select(g => byte.Parse($"{g.First().c}{g.Last().c}", NumberStyles.HexNumber))
.ToArray();
}
}
}

78
EonaCat.HID.sln Normal file
View File

@ -0,0 +1,78 @@
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.10.34928.147
MinimumVisualStudioVersion = 10.0.40219.1
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
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EonaCat.HID.Console", "EonaCat.HID.Console\EonaCat.HID.Console.csproj", "{E8FD1273-26F3-462B-8EDF-4DA044D10D59}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Debug|Mixed Platforms = Debug|Mixed Platforms
Debug|x64 = Debug|x64
Debug|x86 = Debug|x86
Release|Any CPU = Release|Any CPU
Release|Mixed Platforms = Release|Mixed Platforms
Release|x64 = Release|x64
Release|x86 = Release|x86
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{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
{61994020-DB89-4621-BA4B-7347A2142CFF}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU
{61994020-DB89-4621-BA4B-7347A2142CFF}.Debug|x64.ActiveCfg = Debug|Any CPU
{61994020-DB89-4621-BA4B-7347A2142CFF}.Debug|x64.Build.0 = Debug|Any CPU
{61994020-DB89-4621-BA4B-7347A2142CFF}.Debug|x86.ActiveCfg = Debug|Any CPU
{61994020-DB89-4621-BA4B-7347A2142CFF}.Debug|x86.Build.0 = Debug|Any CPU
{61994020-DB89-4621-BA4B-7347A2142CFF}.Release|Any CPU.ActiveCfg = Release|Any CPU
{61994020-DB89-4621-BA4B-7347A2142CFF}.Release|Any CPU.Build.0 = Release|Any CPU
{61994020-DB89-4621-BA4B-7347A2142CFF}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
{61994020-DB89-4621-BA4B-7347A2142CFF}.Release|Mixed Platforms.Build.0 = Release|Any CPU
{61994020-DB89-4621-BA4B-7347A2142CFF}.Release|x64.ActiveCfg = Release|Any CPU
{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
{E8FD1273-26F3-462B-8EDF-4DA044D10D59}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{E8FD1273-26F3-462B-8EDF-4DA044D10D59}.Debug|Any CPU.Build.0 = Debug|Any CPU
{E8FD1273-26F3-462B-8EDF-4DA044D10D59}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
{E8FD1273-26F3-462B-8EDF-4DA044D10D59}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU
{E8FD1273-26F3-462B-8EDF-4DA044D10D59}.Debug|x64.ActiveCfg = Debug|Any CPU
{E8FD1273-26F3-462B-8EDF-4DA044D10D59}.Debug|x64.Build.0 = Debug|Any CPU
{E8FD1273-26F3-462B-8EDF-4DA044D10D59}.Debug|x86.ActiveCfg = Debug|Any CPU
{E8FD1273-26F3-462B-8EDF-4DA044D10D59}.Debug|x86.Build.0 = Debug|Any CPU
{E8FD1273-26F3-462B-8EDF-4DA044D10D59}.Release|Any CPU.ActiveCfg = Release|Any CPU
{E8FD1273-26F3-462B-8EDF-4DA044D10D59}.Release|Any CPU.Build.0 = Release|Any CPU
{E8FD1273-26F3-462B-8EDF-4DA044D10D59}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
{E8FD1273-26F3-462B-8EDF-4DA044D10D59}.Release|Mixed Platforms.Build.0 = Release|Any CPU
{E8FD1273-26F3-462B-8EDF-4DA044D10D59}.Release|x64.ActiveCfg = Release|Any CPU
{E8FD1273-26F3-462B-8EDF-4DA044D10D59}.Release|x64.Build.0 = Release|Any CPU
{E8FD1273-26F3-462B-8EDF-4DA044D10D59}.Release|x86.ActiveCfg = Release|Any CPU
{E8FD1273-26F3-462B-8EDF-4DA044D10D59}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {3EA279F1-3C81-4710-A932-87EE335DC024}
EndGlobalSection
EndGlobal

View File

@ -0,0 +1,55 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<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.4</Version>
<Title>EonaCat.HID</Title>
<Authors>EonaCat (Jeroen Saey)</Authors>
<Description>HID Devices</Description>
<PackageLicenseFile>LICENSE</PackageLicenseFile>
<PackageProjectUrl></PackageProjectUrl>
<PackageIcon>icon.png</PackageIcon>
<PackageProjectUrl>https://www.nuget.org/packages/EonaCat.HID/</PackageProjectUrl>
<PackageTags>usb; hid; Jeroen;Saey</PackageTags>
<GeneratePackageOnBuild>True</GeneratePackageOnBuild>
<PackageReadmeFile>README.md</PackageReadmeFile>
</PropertyGroup>
<ItemGroup>
<None Include="..\icon.png">
<Pack>True</Pack>
<PackagePath>\</PackagePath>
</None>
<None Include="..\LICENSE">
<Pack>True</Pack>
<PackagePath>\</PackagePath>
</None>
<None Include="..\README.md">
<Pack>True</Pack>
<PackagePath>\</PackagePath>
</None>
</ItemGroup>
<ItemGroup>
<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>
<None Update="icon.png">
<Pack>True</Pack>
<PackagePath>\</PackagePath>
</None>
</ItemGroup>
</Project>

View File

@ -0,0 +1,23 @@
using EonaCat.HID.Models;
using System;
namespace EonaCat.HID.EventArguments
{
// 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.
/// <summary>
/// Event args for received data
/// </summary>
public class HidDataReceivedEventArgs : EventArgs
{
public IHid Device { get; }
public HidReport Report { get; }
public HidDataReceivedEventArgs(IHid device, HidReport report)
{
Device = device;
Report = report;
}
}
}

View File

@ -0,0 +1,22 @@
using System;
namespace EonaCat.HID.EventArguments
{
// 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.
/// <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;
}
}
}

View File

@ -0,0 +1,22 @@
using System;
namespace EonaCat.HID.EventArguments
{
// 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.
/// <summary>
/// Event arguments with HID device info.
/// </summary>
public class HidEventArgs : EventArgs
{
public IHid Device { get; }
public bool HasDevice => Device != null;
public bool IsConnected => HasDevice && Device.IsConnected;
public HidEventArgs(IHid device)
{
Device = device;
}
}
}

View File

@ -0,0 +1,35 @@
using System;
using System.Linq;
using System.Text;
namespace EonaCat.HID.Helpers
{
// 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 static class ByteHelper
{
public static string ByteArrayToHexString(byte[] bytes, string separator = "")
{
return BitConverter.ToString(bytes).Replace("-", separator);
}
public static byte[] HexStringToByteArray(string hexString)
{
hexString.Trim();
hexString = hexString.Replace("-", "");
hexString = hexString.Replace(" ", "");
return Enumerable.Range(0, hexString.Length)
.Where(x => x % 2 == 0)
.Select(x => Convert.ToByte(hexString.Substring(x, 2), 16))
.ToArray();
}
public static string GetFilledBytesString(byte[] bytes)
{
var buffer = Encoding.Unicode.GetString(bytes).ToArray();
int index = Array.IndexOf(buffer, '\0');
return new string(buffer, 0, index >= 0 ? index : buffer.Length);
}
}
}

71
EonaCat.HID/HidFactory.cs Normal file
View File

@ -0,0 +1,71 @@
using System;
using System.Runtime.InteropServices;
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.
/// <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");
}
}
}
}

464
EonaCat.HID/HidLinux.cs Normal file
View File

@ -0,0 +1,464 @@
using EonaCat.HID.EventArguments;
using EonaCat.HID.Managers;
using EonaCat.HID.Managers.Linux;
using EonaCat.HID.Models;
using Microsoft.Win32.SafeHandles;
using System;
using System.Collections.Generic;
using System.IO;
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.
internal sealed class HidLinux : IHid
{
private readonly string _devicePath;
private FileStream _stream;
private bool _isOpen;
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 bool IsReadingSupport { get; private set; }
public bool IsWritingSupport { get; private set; }
public bool IsConnected => _isOpen;
public IDictionary<string, object> Capabilities { get; private set; } = new Dictionary<string, object>();
public event EventHandler<HidDataReceivedEventArgs> OnDataReceived;
public event EventHandler<HidErrorEventArgs> OnError;
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 || _isOpen)
return;
// Open HID device in non-blocking read/write mode
int fd = NativeMethods.open(_devicePath, NativeMethods.O_RDWR | NativeMethods.O_NONBLOCK);
if (fd < 0)
{
OnError?.Invoke(this, new HidErrorEventArgs(this,
new IOException($"Failed to open device: {_devicePath}")));
return;
}
var safeHandle = new Microsoft.Win32.SafeHandles.SafeFileHandle(new IntPtr(fd), ownsHandle: true);
try
{
_stream = new FileStream(safeHandle, FileAccess.ReadWrite, bufferSize: 64, isAsync: false);
_isOpen = true;
IsReadingSupport = true;
IsWritingSupport = true;
}
catch (Exception ex)
{
safeHandle.Dispose();
OnError?.Invoke(this, new HidErrorEventArgs(this, ex));
}
}
}
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(HidReport report)
{
if (report == null)
{
var ex = new ArgumentNullException(nameof(report));
OnError?.Invoke(this, new HidErrorEventArgs(this, ex));
return;
}
if (_stream == null)
{
var ex = new InvalidOperationException("Device not open");
OnError?.Invoke(this, new HidErrorEventArgs(this, ex));
return;
}
var data = report.Data;
await Task.Run(() =>
{
lock (_lock)
{
try
{
_stream.Write(data, 0, data.Length);
_stream.Flush();
}
catch (Exception ex)
{
OnError?.Invoke(this, new HidErrorEventArgs(this, ex));
throw;
}
}
});
}
public async Task<HidReport> ReadInputReportAsync()
{
if (_stream == null)
{
var ex = new InvalidOperationException("Device not open");
OnError?.Invoke(this, new HidErrorEventArgs(this, ex));
return null;
}
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));
return null;
}
}
if (bytesRead <= 0)
{
return null;
}
// First byte is report ID, rest is data
byte reportId = buffer[0];
byte[] data = new byte[bytesRead - 1];
Array.Copy(buffer, 1, data, 0, data.Length);
return new HidReport(reportId, data);
});
}
public async Task SendFeatureReportAsync(HidReport report)
{
if (report == null)
throw new ArgumentNullException(nameof(report));
// Prepare full report buffer: [reportId][reportData...]
int size = 1 + (report.Data?.Length ?? 0);
byte[] buffer = new byte[size];
buffer[0] = report.ReportId;
if (report.Data != null && report.Data.Length > 0)
{
Array.Copy(report.Data, 0, buffer, 1, report.Data.Length);
}
await Task.Run(() =>
{
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;
}
IntPtr unmanagedBuffer = IntPtr.Zero;
try
{
unmanagedBuffer = Marshal.AllocHGlobal(size);
Marshal.Copy(buffer, 0, unmanagedBuffer, size);
int request = NativeMethods._IOC(NativeMethods._IOC_WRITE, 'H', 0x06, size); // HIDIOCSFEATURE
int result = NativeMethods.ioctl(fd, request, unmanagedBuffer);
if (result < 0)
{
OnError?.Invoke(this, new HidErrorEventArgs(this, new IOException("ioctl HIDIOCSFEATURE failed")));
}
}
finally
{
if (unmanagedBuffer != IntPtr.Zero)
Marshal.FreeHGlobal(unmanagedBuffer);
NativeMethods.close(fd);
}
});
}
public async Task<HidReport> GetFeatureReportAsync(byte reportId)
{
const int maxFeatureSize = 256;
byte[] buffer = new byte[maxFeatureSize];
buffer[0] = reportId;
return await Task.Run(() =>
{
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 new HidReport(0, Array.Empty<byte>());
}
IntPtr bufPtr = IntPtr.Zero;
try
{
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 new HidReport(0, Array.Empty<byte>());
}
byte[] actualBuffer = new byte[result];
Marshal.Copy(bufPtr, actualBuffer, 0, result);
byte actualReportId = actualBuffer.Length > 0 ? actualBuffer[0] : (byte)0;
byte[] reportData = actualBuffer.Length > 1 ? new byte[actualBuffer.Length - 1] : Array.Empty<byte>();
if (reportData.Length > 0)
Array.Copy(actualBuffer, 1, reportData, 0, reportData.Length);
return new HidReport(actualReportId, reportData);
}
finally
{
if (bufPtr != IntPtr.Zero)
Marshal.FreeHGlobal(bufPtr);
NativeMethods.close(fd);
}
});
}
public async Task StartListeningAsync(CancellationToken cancellationToken)
{
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;
}
catch (Exception ex)
{
OnError?.Invoke(this, new HidErrorEventArgs(this, ex));
break;
}
if (bytesRead > 0)
{
var data = new byte[bytesRead];
Array.Copy(buffer, data, bytesRead);
// Extract reportId and report data
byte reportId = data.Length > 0 ? data[0] : (byte)0;
byte[] reportData;
if (data.Length > 1)
{
reportData = new byte[data.Length - 1];
Array.Copy(data, 1, reportData, 0, data.Length - 1);
}
else
{
reportData = Array.Empty<byte>();
}
var hidReport = new HidReport(reportId, reportData);
OnDataReceived?.Invoke(this, new HidDataReceivedEventArgs(this, hidReport));
}
else
{
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;
}
}
}
}
}

507
EonaCat.HID/HidMac.cs Normal file
View File

@ -0,0 +1,507 @@
using EonaCat.HID.EventArguments;
using EonaCat.HID.Models;
using Microsoft.Win32.SafeHandles;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
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 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 bool IsReadingSupport { get; private set; }
public bool IsWritingSupport { get; private set; }
public bool IsConnected => _isOpen;
public IDictionary<string, object> Capabilities { get; private set; } = new Dictionary<string, object>();
public event EventHandler<HidDataReceivedEventArgs> OnDataReceived;
public event EventHandler<HidErrorEventArgs> OnError;
private const int KERN_SUCCESS = 0;
private const int kIOHIDOptionsTypeNone = 0;
public void Open()
{
if (_isOpen)
{
return;
}
var result = IOHIDDeviceOpen(_deviceHandle, kIOHIDOptionsTypeNone);
if (result != KERN_SUCCESS)
{
throw new InvalidOperationException($"Failed to open HID device. IOHIDDeviceOpen returned: {result}");
}
IsReadingSupport = true;
IsWritingSupport = CheckOutputReportSupport();
_isOpen = true;
}
private bool CheckOutputReportSupport()
{
// On macOS, there's no simple API to test writing directly.
// You typically assume support based on report length.
return OutputReportByteLength > 0;
}
public void Close()
{
IOHIDDeviceClose(_deviceHandle, 0);
_isOpen = false;
}
public void Dispose()
{
StopListening();
Close();
CFRelease(_deviceHandle);
_deviceHandle = IntPtr.Zero;
}
public void StopListening()
{
_listeningCts?.Cancel();
}
public async Task WriteOutputReportAsync(HidReport report)
{
if (report == null)
{
throw new ArgumentNullException(nameof(report));
}
if (report.Data == null || report.Data.Length == 0)
{
throw new ArgumentException("Data cannot be null or empty", nameof(report));
}
await Task.Run(() =>
{
// Total length includes reportId + data length
int length = 1 + report.Data.Length;
IntPtr buffer = Marshal.AllocHGlobal(length);
try
{
// First byte is reportId
Marshal.WriteByte(buffer, report.ReportId);
// Copy the rest of data after the reportId byte
Marshal.Copy(report.Data, 0, buffer + 1, report.Data.Length);
int res = IOHIDDeviceSetReport(_deviceHandle, 1 /* kIOHIDReportTypeOutput */, report.ReportId, buffer, length);
if (res != 0)
{
OnError?.Invoke(this, new HidErrorEventArgs(this, new Exception($"IOHIDDeviceSetReport (Output) failed: {res}")));
}
}
finally
{
Marshal.FreeHGlobal(buffer);
}
});
}
public async Task SendFeatureReportAsync(HidReport report)
{
if (report == null)
{
throw new ArgumentNullException(nameof(report));
}
if (report.Data == null || report.Data.Length == 0)
{
throw new ArgumentException("Data cannot be null or empty", nameof(report));
}
await Task.Run(() =>
{
int length = 1 + report.Data.Length;
IntPtr buffer = Marshal.AllocHGlobal(length);
try
{
Marshal.WriteByte(buffer, report.ReportId);
Marshal.Copy(report.Data, 0, buffer + 1, report.Data.Length);
int res = IOHIDDeviceSetReport(_deviceHandle, 2 /* kIOHIDReportTypeFeature */, report.ReportId, buffer, length);
if (res != 0)
{
OnError?.Invoke(this, new HidErrorEventArgs(this, new Exception($"IOHIDDeviceSetReport (Feature) failed: {res}")));
}
}
finally
{
Marshal.FreeHGlobal(buffer);
}
});
}
public async Task<HidReport> GetFeatureReportAsync(byte reportId)
{
return await Task.Run(() =>
{
int length = FeatureReportByteLength;
IntPtr buffer = Marshal.AllocHGlobal(length);
try
{
int res = IOHIDDeviceGetReport(_deviceHandle, 2 /* kIOHIDReportTypeFeature */, reportId, buffer, ref length);
if (res != 0)
{
OnError?.Invoke(this, new HidErrorEventArgs(this, new Exception($"IOHIDDeviceGetReport (Feature) failed: {res}")));
return new HidReport(reportId, Array.Empty<byte>());
}
byte[] outBuf = new byte[length];
Marshal.Copy(buffer, outBuf, 0, length);
if (outBuf.Length > 0 && outBuf[0] == reportId)
{
// Data excludes report ID (first byte)
var dataOnly = outBuf.Skip(1).ToArray();
return new HidReport(reportId, dataOnly);
}
else
{
return new HidReport(reportId, outBuf);
}
}
finally
{
Marshal.FreeHGlobal(buffer);
}
});
}
public Task<HidReport> ReadInputReportAsync()
{
var tcs = new TaskCompletionSource<HidReport>();
byte[] buffer = new byte[InputReportByteLength];
InputReportCallback callback = null;
callback = (ctx, result, sender, report, reportLength) =>
{
int length = reportLength.ToInt32();
if (report == null || length == 0)
{
tcs.TrySetResult(new HidReport(0, Array.Empty<byte>()));
return;
}
byte[] output = new byte[length];
Array.Copy(report, output, length);
byte reportId = output.Length > 0 ? output[0] : (byte)0;
byte[] reportData;
if (output.Length > 1)
{
reportData = new byte[output.Length - 1];
Array.Copy(output, 1, reportData, 0, reportData.Length);
}
else
{
reportData = Array.Empty<byte>();
}
var hidReport = new HidReport(reportId, reportData);
tcs.TrySetResult(hidReport);
};
GCHandle.Alloc(callback);
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 Task.CompletedTask;
}
_listeningCts = CancellationTokenSource.CreateLinkedTokenSource(ct);
_listeningTask = Task.Run(() =>
{
try
{
byte[] buffer = new byte[InputReportByteLength];
InputReportCallback callback = (context, result, sender, report, reportLength) =>
{
int len = reportLength.ToInt32();
if (report == null || report.Length < len)
{
return;
}
// Extract reportId (first byte)
byte reportId = len > 0 ? report[0] : (byte)0;
// Extract the rest of the data after reportId
byte[] reportData = len > 1 ? report.Skip(1).Take(len - 1).ToArray() : Array.Empty<byte>();
var hidReport = new HidReport(reportId, reportData);
OnDataReceived?.Invoke(this, new HidDataReceivedEventArgs(this, hidReport));
};
GCHandle.Alloc(callback);
IOHIDDeviceRegisterInputReportCallback(_deviceHandle, buffer, (IntPtr)buffer.Length, callback, IntPtr.Zero);
IOHIDDeviceScheduleWithRunLoop(_deviceHandle, CFRunLoopGetCurrent(), IntPtr.Zero);
while (!_listeningCts.Token.IsCancellationRequested)
{
CFRunLoopRun();
}
CFRunLoopStop(CFRunLoopGetCurrent());
}
catch (OperationCanceledException)
{
return;
}
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 bool _isOpen;
private enum CFNumberType : int
{
kCFNumberIntType = 9
}
}
}

472
EonaCat.HID/HidWindows.cs Normal file
View File

@ -0,0 +1,472 @@
using EonaCat.HID.EventArguments;
using EonaCat.HID.Managers.Windows;
using EonaCat.HID.Models;
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
{
// 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 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 bool IsConnected => _isOpen;
public IDictionary<string, object> Capabilities { get; } = new Dictionary<string, object>();
public bool IsReadingSupport { get; private set; }
public bool IsWritingSupport { get; private set; }
public event EventHandler<HidDataReceivedEventArgs> OnDataReceived;
public event EventHandler<HidErrorEventArgs> OnError;
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, bufferSize: 64, isAsync: true);
_isOpen = true;
// HID descriptor
if (!HidD_GetPreparsedData(_deviceHandle.DangerousGetHandle(), out _preparsedData))
throw new Win32Exception(Marshal.GetLastWin32Error(), "Failed HidD_GetPreparsedData");
HIDP_CAPS caps;
int capsRes = HidP_GetCaps(_preparsedData, out caps);
if (capsRes != NativeMethods.HIDP_STATUS_SUCCESS)
throw new Win32Exception(capsRes, "Failed HidP_GetCaps");
InputReportByteLength = caps.InputReportByteLength;
OutputReportByteLength = caps.OutputReportByteLength;
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);
IsReadingSupport = (access == FileAccess.Read || access == FileAccess.ReadWrite);
IsWritingSupport = (access == FileAccess.Write || access == FileAccess.ReadWrite);
HidDeviceAttributes attr = GetDeviceAttributes();
VendorId = attr.VendorID;
ProductId = attr.ProductID;
}
private SafeFileHandle TryOpenDevice(int access)
{
var handle = CreateFile(_devicePath,
access,
FILE_SHARE_READ | FILE_SHARE_WRITE,
IntPtr.Zero,
OPEN_EXISTING,
FILE_FLAG_OVERLAPPED,
IntPtr.Zero);
return handle;
}
/// <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(HidReport report)
{
if (!_isOpen)
{
OnError?.Invoke(this, new HidErrorEventArgs(this, new InvalidOperationException("Device not open")));
return;
}
if (report == null)
{
OnError?.Invoke(this, new HidErrorEventArgs(this, new ArgumentNullException(nameof(report))));
return;
}
if (report.Data == null || report.Data.Length == 0)
{
OnError?.Invoke(this, new HidErrorEventArgs(this, new ArgumentException("Data cannot be null or empty", nameof(report))));
return;
}
try
{
// Combine reportId and data into one buffer for sending
var buffer = new byte[1 + report.Data.Length];
buffer[0] = report.ReportId;
Array.Copy(report.Data, 0, buffer, 1, report.Data.Length);
await _deviceStream.WriteAsync(buffer, 0, buffer.Length);
await _deviceStream.FlushAsync();
}
catch (Exception ex)
{
OnError?.Invoke(this, new HidErrorEventArgs(this, ex));
throw;
}
}
public async Task<HidReport> ReadInputReportAsync()
{
if (!_isOpen)
{
OnError?.Invoke(this, new HidErrorEventArgs(this, new InvalidOperationException("Device not open")));
return new HidReport(0, 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 new HidReport(0, Array.Empty<byte>());
}
byte reportId = buffer[0];
byte[] data = buffer.Skip(1).Take(read - 1).ToArray();
return new HidReport(reportId, data);
}
catch (Exception ex)
{
OnError?.Invoke(this, new HidErrorEventArgs(this, ex));
throw;
}
});
}
public async Task SendFeatureReportAsync(HidReport report)
{
if (!_isOpen)
{
OnError?.Invoke(this, new HidErrorEventArgs(this, new InvalidOperationException("Device not open")));
return;
}
if (report == null)
throw new ArgumentNullException(nameof(report));
// Prepare buffer with ReportId + Data
var data = new byte[1 + report.Data.Length];
data[0] = report.ReportId;
Array.Copy(report.Data, 0, data, 1, report.Data.Length);
await Task.Run(() =>
{
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<HidReport> GetFeatureReportAsync(byte reportId)
{
if (!_isOpen)
{
OnError?.Invoke(this, new HidErrorEventArgs(this, new InvalidOperationException("Device not open")));
return new HidReport(0, 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 new HidReport(0, Array.Empty<byte>());
}
byte[] data = buffer.Skip(1).ToArray();
return new HidReport(reportId, data);
});
}
/// <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
{
var token = _listeningCts.Token;
while (!token.IsCancellationRequested)
{
try
{
var inputReport = await ReadInputReportAsync(token);
if (inputReport?.Data?.Length > 0)
{
OnDataReceived?.Invoke(this, new HidDataReceivedEventArgs(this, inputReport));
}
}
catch (OperationCanceledException)
{
break;
}
catch (NotSupportedException)
{
OnError?.Invoke(this, new HidErrorEventArgs(this, new NotSupportedException("Reading input reports is not supported on this device.")));
break;
}
catch (Exception ex)
{
if (token.IsCancellationRequested)
{
break;
}
OnError?.Invoke(this, new HidErrorEventArgs(this, ex));
}
}
}
finally
{
_listeningCts.Dispose();
_listeningCts = null;
}
}
private Task<HidReport> ReadInputReportAsync(CancellationToken cancellationToken)
{
var tcs = new TaskCompletionSource<HidReport>(TaskCreationOptions.RunContinuationsAsynchronously);
var buffer = new byte[InputReportByteLength];
// Start async read
_deviceStream.BeginRead(buffer, 0, buffer.Length, ar =>
{
try
{
int bytesRead = _deviceStream.EndRead(ar);
if (bytesRead == 0)
{
// No data read, reportId 0 and empty data
tcs.SetResult(new HidReport(0, Array.Empty<byte>()));
}
else
{
// First byte is reportId, rest is data
byte reportId = buffer[0];
byte[] data = bytesRead > 1 ? buffer.Skip(1).Take(bytesRead - 1).ToArray() : Array.Empty<byte>();
tcs.SetResult(new HidReport(reportId, data));
}
}
catch (Exception ex)
{
tcs.SetException(ex);
}
}, null);
cancellationToken.Register(() =>
{
tcs.TrySetCanceled();
});
return tcs.Task;
}
private void StopListening()
{
if (_listeningCts != null)
{
_listeningCts.Cancel();
_listeningCts.Dispose();
_listeningCts = null;
}
}
}
}

84
EonaCat.HID/IHid.cs Normal file
View File

@ -0,0 +1,84 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using System.Threading;
using EonaCat.HID.EventArguments;
using EonaCat.HID.Models;
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.
/// <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; }
bool IsConnected { get;}
bool IsReadingSupport { get; }
bool IsWritingSupport { 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(HidReport report);
/// <summary>
/// Reads an input report
/// </summary>
/// <returns>Input report data</returns>
Task<HidReport> ReadInputReportAsync();
/// <summary>
/// Sends a feature report
/// </summary>
/// <param name="data">Complete feature report data including ReportID</param>
Task SendFeatureReportAsync(HidReport report);
/// <summary>
/// Gets a feature report
/// </summary>
/// <returns>Feature report data</returns>
Task<HidReport> 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;
}
}

View File

@ -0,0 +1,30 @@
using EonaCat.HID.EventArguments;
using System;
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.
/// <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;
}
}

View File

@ -0,0 +1,178 @@
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;
namespace EonaCat.HID.Managers.Linux
{
// 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 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);
}
}
}

View File

@ -0,0 +1,191 @@
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
{
// 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 sealed class HidManagerMac : IHidManager, IDisposable
{
private IntPtr _hidManager;
private readonly IOHIDDeviceCallback _deviceAddedCallback;
private readonly IOHIDDeviceCallback _deviceRemovedCallback;
private readonly Dictionary<string, IHid> _knownDevices = new();
public event EventHandler<HidEventArgs> OnDeviceInserted;
public event EventHandler<HidEventArgs> OnDeviceRemoved;
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 DeviceInsertedInternal(IHid device)
{
if (!_knownDevices.ContainsKey(device.DevicePath))
{
_knownDevices[device.DevicePath] = device;
OnDeviceInserted?.Invoke(this, new HidEventArgs(device));
}
}
private void DeviceRemovedInternal(IHid device)
{
if (_knownDevices.ContainsKey(device.DevicePath))
{
device = _knownDevices[device.DevicePath];
_knownDevices.Remove(device.DevicePath);
OnDeviceRemoved?.Invoke(this, new HidEventArgs(device));
}
}
private void DeviceAddedCallback(IntPtr context, IntPtr result, IntPtr sender, IntPtr devicePtr)
{
if (devicePtr == IntPtr.Zero)
return;
try
{
var device = new HidMac(devicePtr);
device.Setup();
DeviceInsertedInternal(device);
}
catch (Exception ex)
{
Console.Error.WriteLine($"DeviceAddedCallback error: {ex.Message}");
}
}
private void DeviceRemovedCallback(IntPtr context, IntPtr result, IntPtr sender, IntPtr devicePtr)
{
if (devicePtr == IntPtr.Zero)
return;
try
{
var device = new HidMac(devicePtr);
DeviceRemovedInternal(device);
}
catch (Exception ex)
{
Console.Error.WriteLine($"DeviceRemovedCallback error: {ex.Message}");
}
}
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
}
}
}

View File

@ -0,0 +1,564 @@
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
{
// 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 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;
private readonly Dictionary<string, IHid> _knownDevices = new();
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)
{
int eventType = wParam.ToInt32();
if (eventType == DBT_DEVICEARRIVAL || eventType == DBT_DEVICEREMOVECOMPLETE)
{
var hdr = Marshal.PtrToStructure<DEV_BROADCAST_HDR>(lParam);
if (hdr.dbch_devicetype == DBT_DEVTYP_DEVICEINTERFACE)
{
var devInterface = Marshal.PtrToStructure<DEV_BROADCAST_DEVICEINTERFACE>(lParam);
// Calculate pointer to string
IntPtr stringPtr = IntPtr.Add(lParam, Marshal.SizeOf<DEV_BROADCAST_DEVICEINTERFACE>());
// Read null-terminated string from unmanaged memory
string devicePath = Marshal.PtrToStringUni(stringPtr);
if (!string.IsNullOrEmpty(devicePath))
{
try
{
if (eventType == DBT_DEVICEARRIVAL)
{
using (var testHandle = CreateFile(devicePath, 0,
FileShare.ReadWrite, IntPtr.Zero, FileMode.Open, 0, IntPtr.Zero))
{
if (testHandle.IsInvalid)
return DefWindowProc(hwnd, msg, wParam, lParam);
}
var device = new HidWindows(devicePath);
device.Setup();
DeviceInsertedInternal(device);
}
else if (eventType == DBT_DEVICEREMOVECOMPLETE)
{
var device = new HidWindows(devicePath);
DeviceRemovedInternal(device);
}
return IntPtr.Zero;
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"[WindowProc] HID device change error: {ex.Message}");
}
}
}
}
}
return DefWindowProc(hwnd, msg, wParam, lParam);
}
private void DeviceInsertedInternal(IHid device)
{
if (!_knownDevices.ContainsKey(device.DevicePath))
{
_knownDevices[device.DevicePath] = device;
OnDeviceInserted?.Invoke(this, new HidEventArgs(device));
}
}
private void DeviceRemovedInternal(IHid device)
{
if (_knownDevices.ContainsKey(device.DevicePath))
{
device = _knownDevices[device.DevicePath];
_knownDevices.Remove(device.DevicePath);
OnDeviceRemoved?.Invoke(this, new HidEventArgs(device));
}
}
public IEnumerable<IHid> Enumerate(ushort? vendorId = null, ushort? productId = null)
{
var list = new List<IHid>();
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;
}
}
}
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 HIDP_STATUS_SUCCESS = 0x110000;
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;
}
[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;
}
}

View File

@ -0,0 +1,18 @@
using System;
namespace EonaCat.HID.Models
{
// 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 HidReport
{
public byte ReportId { get; }
public byte[] Data { get; }
public HidReport(byte reportId, byte[] data)
{
ReportId = reportId;
Data = data ?? Array.Empty<byte>();
}
}
}

BIN
EonaCat.HID/icon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 248 KiB

213
LICENSE
View File

@ -1,73 +1,204 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
https://EonaCat.com/license/
1. Definitions. TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
OF SOFTWARE BY EONACAT (JEROEN SAEY)
"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. 1. Definitions.
"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. "Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. "Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: 3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
(a) You must give any other recipients of the Work or Derivative Works a copy of this License; and 4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(b) You must cause any modified files to carry prominent notices stating that You changed the files; and (a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. (c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. (d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. 8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
END OF TERMS AND CONDITIONS 9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
APPENDIX: How to apply the Apache License to your work. END OF TERMS AND CONDITIONS
To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. APPENDIX: How to apply the Apache License to your work.
Copyright 2025 EonaCat To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Licensed under the Apache License, Version 2.0 (the "License"); Copyright [yyyy] [name of copyright owner]
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0 Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
Unless required by applicable law or agreed to in writing, software http://www.apache.org/licenses/LICENSE-2.0
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. Unless required by applicable law or agreed to in writing, software
See the License for the specific language governing permissions and distributed under the License is distributed on an "AS IS" BASIS,
limitations under the License. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

124
README.md
View File

@ -1,3 +1,125 @@
# EonaCat.HID # EonaCat.HID
EonaCat.HID Cross platform library for HID.
Works on Windows, Linux and macOS.
Example Code:
```csharp
using EonaCat.HID;
using EonaCat.HID.Models;
using System.Globalization;
namespace EonaCat.HID.Example
{
// 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 Program
{
static IHidManager _deviceManager;
static IHid _device;
static IEnumerable<IHid> _deviceList;
static async Task Main(string[] args)
{
try
{
_deviceManager = HidFactory.CreateDeviceManager();
if (_deviceManager == null)
{
Console.WriteLine("Failed to create HID manager.");
return;
}
_deviceManager.OnDeviceInserted += (s, e) =>
{
Console.WriteLine($"Inserted Device --> VID: {e.Device.VendorId:X4}, PID: {e.Device.ProductId:X4}");
};
_deviceManager.OnDeviceRemoved += (s, e) =>
{
Console.WriteLine($"Removed Device --> VID: {e.Device.VendorId:X4}, PID: {e.Device.ProductId:X4}");
};
RefreshDevices();
if (!_deviceList.Any())
{
Console.WriteLine("No HID found.");
return;
}
DisplayDevices();
Console.Write("Select a device index to connect: ");
int index = int.Parse(Console.ReadLine());
_device = _deviceList.ElementAt(index);
_device.OnDataReceived += (s, e) =>
{
Console.WriteLine($"Rx Data: {BitConverter.ToString(e.Report.Data)}");
};
_device.OnError += (s, e) =>
{
Console.WriteLine($"Error: {e.Exception.Message}");
};
_device.Open();
Console.WriteLine($"Connected to {_device.ProductName}");
await _device.StartListeningAsync(default);
Console.WriteLine("Listening... Press [Enter] to send test output report.");
Console.ReadLine();
// Example: Send output report using HidReport
var data = ByteHelper.HexStringToByteArray("01-02-03");
var reportId = (byte)0x00; // Report ID
var outputReport = new HidReport(reportId, data);
await _device.WriteOutputReportAsync(outputReport);
Console.WriteLine($"Sent output report: Report ID: {reportId}, Data: {BitConverter.ToString(data)}");
Console.WriteLine("Press [Enter] to exit...");
Console.ReadLine();
}
catch (Exception ex)
{
Console.WriteLine($"[EXCEPTION] {ex.Message}");
}
}
static void RefreshDevices(ushort? vendorId = null, ushort? productId = null)
{
_deviceList = _deviceManager.Enumerate(vendorId, productId);
}
static void DisplayDevices()
{
int i = 0;
foreach (var dev in _deviceList)
{
Console.WriteLine($"[{i++}] {dev.ProductName} | VID:PID = {dev.VendorId:X4}:{dev.ProductId:X4}");
}
}
}
public static class ByteHelper
{
public static byte[] HexStringToByteArray(string hex)
{
return hex
.Replace("-", "")
.Replace(" ", "")
.ToUpper()
.Where(c => Uri.IsHexDigit(c))
.Select((c, i) => new { c, i })
.GroupBy(x => x.i / 2)
.Select(g => byte.Parse($"{g.First().c}{g.Last().c}", NumberStyles.HexNumber))
.ToArray();
}
}
}
```

BIN
icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB