Updated console message loop

This commit is contained in:
Jeroen Saey
2025-10-08 11:38:16 +02:00
parent 19e713eb21
commit d7e29c7ee4
9 changed files with 452 additions and 278 deletions

View File

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

View File

@@ -10,6 +10,7 @@ using System.Linq;
using System.Reflection; using System.Reflection;
using System.Threading.Tasks; using System.Threading.Tasks;
using System.Windows.Forms; using System.Windows.Forms;
using System.Windows.Threading;
namespace EonaCat.HID.Analyzer namespace EonaCat.HID.Analyzer
{ {
@@ -42,7 +43,8 @@ namespace EonaCat.HID.Analyzer
public async Task RefreshDevicesAsync(ushort? vendorId = null, ushort? productId = null) public async Task RefreshDevicesAsync(ushort? vendorId = null, ushort? productId = null)
{ {
_deviceList = await _deviceManager.EnumerateAsync(vendorId, productId).ConfigureAwait(false); toolStripStatusLabel1.Text = "Refreshing devices, this may take a while...";
_deviceList = await _deviceManager.EnumerateAsync(vendorId, productId);
} }
private void MainForm_Load(object sender, System.EventArgs e) private void MainForm_Load(object sender, System.EventArgs e)
@@ -57,12 +59,12 @@ namespace EonaCat.HID.Analyzer
} }
} }
private void MainForm_Shown(object sender, System.EventArgs e) private async void MainForm_Shown(object sender, System.EventArgs e)
{ {
try try
{ {
RefreshDevicesAsync(); await RefreshDevicesAsync().ConfigureAwait(false);
UpdateDeviceList(); UpdateDeviceListAsync();
toolStripStatusLabel1.Text = "Please select device and click open to start."; toolStripStatusLabel1.Text = "Please select device and click open to start.";
} }
catch (Exception ex) catch (Exception ex)
@@ -94,44 +96,62 @@ namespace EonaCat.HID.Analyzer
})); }));
} }
private void UpdateDeviceList() private async Task UpdateDeviceListAsync()
{ {
dataGridView1.SelectionChanged -= DataGridView1_SelectionChanged; if (_deviceList == null || !_deviceList.Any())
dataGridView1.Rows.Clear(); {
return;
for (int i = 0; i < _deviceList.Count(); i++) }
{
IHid device = _deviceList.ElementAt(i); // Capture the data in a local variable so the UI update can happen in one go
var rows = new List<string[]>();
var deviceName = ""; for (int i = 0; i < _deviceList.Count(); i++)
var deviceManufacturer = ""; {
var deviceSerialNumber = ""; IHid device = _deviceList.ElementAt(i);
deviceName = device.ProductName; var row = new string[]
deviceManufacturer = device.Manufacturer; {
deviceSerialNumber = device.SerialNumber; (i + 1).ToString(),
var isWritingSupported = device.IsWritingSupport; device.ProductName,
var isReadingSupported = device.IsReadingSupport; device.Manufacturer,
device.SerialNumber,
var row = new string[] device.IsReadingSupport.ToString(),
{ device.IsWritingSupport.ToString(),
(i + 1).ToString(), device.InputReportByteLength.ToString(),
deviceName, device.OutputReportByteLength.ToString(),
deviceManufacturer, device.FeatureReportByteLength.ToString(),
deviceSerialNumber, $"Vendor:{device.VendorId:X4} Product:{device.ProductId:X4} Revision:{device.VendorId:X4}",
isReadingSupported.ToString(), device.DevicePath
isWritingSupported.ToString(), };
device.InputReportByteLength.ToString(),
device.OutputReportByteLength.ToString(), rows.Add(row);
device.FeatureReportByteLength.ToString(), }
$"Vendor:{device.VendorId:X4} Product:{device.ProductId:X4} Revision:{device.VendorId:X4}",
device.DevicePath if (dataGridView1.InvokeRequired)
}; {
dataGridView1.Invoke(new Action(() =>
dataGridView1.Rows.Add(row); {
} dataGridView1.SelectionChanged -= DataGridView1_SelectionChanged;
dataGridView1.SelectionChanged += DataGridView1_SelectionChanged; dataGridView1.Rows.Clear();
DataGridView1_SelectionChanged(this, null); foreach (var row in rows)
{
dataGridView1.Rows.Add(row);
}
dataGridView1.SelectionChanged += DataGridView1_SelectionChanged;
DataGridView1_SelectionChanged(this, null);
}));
}
else
{
dataGridView1.SelectionChanged -= DataGridView1_SelectionChanged;
dataGridView1.Rows.Clear();
foreach (var row in rows)
{
dataGridView1.Rows.Add(row);
}
dataGridView1.SelectionChanged += DataGridView1_SelectionChanged;
DataGridView1_SelectionChanged(this, null);
}
} }
private void PopupException(string message, string caption = "Exception") private void PopupException(string message, string caption = "Exception")
@@ -178,12 +198,12 @@ namespace EonaCat.HID.Analyzer
} }
} }
private void ToolStripButtonReload_Click(object sender, EventArgs e) private async void ToolStripButtonReload_Click(object sender, EventArgs e)
{ {
try try
{ {
RefreshDevicesAsync(); await RefreshDevicesAsync().ConfigureAwait(false);
UpdateDeviceList(); UpdateDeviceListAsync();
} }
catch (Exception ex) catch (Exception ex)
{ {
@@ -191,7 +211,7 @@ namespace EonaCat.HID.Analyzer
} }
} }
private void ToolStripButtonFilter_Click(object sender, EventArgs e) private async void ToolStripButtonFilter_Click(object sender, EventArgs e)
{ {
try try
{ {
@@ -203,8 +223,8 @@ namespace EonaCat.HID.Analyzer
vid = ushort.Parse(str[0], NumberStyles.AllowHexSpecifier); vid = ushort.Parse(str[0], NumberStyles.AllowHexSpecifier);
pid = ushort.Parse(str[1], NumberStyles.AllowHexSpecifier); pid = ushort.Parse(str[1], NumberStyles.AllowHexSpecifier);
} }
RefreshDevicesAsync(vid, pid); await RefreshDevicesAsync(vid, pid).ConfigureAwait(false);
UpdateDeviceList(); UpdateDeviceListAsync();
} }
catch (Exception ex) catch (Exception ex)
{ {
@@ -341,12 +361,12 @@ namespace EonaCat.HID.Analyzer
} }
var report = await _device.ReadInputReportAsync(); var report = await _device.ReadInputReportAsync();
if (report == null || report.Data.Length < 2) if (report == null || report.Data.Length < 2)
{ {
AppendEventLog("Received report is null or is too short to contain a valid Report ID.", Color.Red); AppendEventLog("Received report is null or is too short to contain a valid Report ID.", Color.Red);
return; return;
} }
var str = string.Format("Rx Input Report [{0}] <-- {1}", report.Data.Length, BitConverter.ToString(report.Data)); var str = string.Format("Rx Input Report [{0}] <-- {1}", report.Data.Length, BitConverter.ToString(report.Data));
AppendEventLog(str, Color.Blue); AppendEventLog(str, Color.Blue);
} }

View File

@@ -34,7 +34,7 @@ namespace EonaCat.HID.Example
Console.WriteLine($"Removed Device --> VID: {e.Device.VendorId:X4}, PID: {e.Device.ProductId:X4}"); Console.WriteLine($"Removed Device --> VID: {e.Device.VendorId:X4}, PID: {e.Device.ProductId:X4}");
}; };
RefreshDevicesAsync(); await RefreshDevicesAsync().ConfigureAwait(false);
if (!_deviceList.Any()) if (!_deviceList.Any())
{ {

View File

@@ -1,55 +1,56 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFrameworks>net46;net6.0</TargetFrameworks> <TargetFrameworks>net48;net6.0</TargetFrameworks>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks> <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<Company>EonaCat (Jeroen Saey)</Company> <Company>EonaCat (Jeroen Saey)</Company>
<Copyright>Copyright 2024 EonaCat (Jeroen Saey)</Copyright> <Copyright>Copyright 2024 EonaCat (Jeroen Saey)</Copyright>
<LangVersion>latest</LangVersion> <LangVersion>latest</LangVersion>
<PackageId>EonaCat.HID</PackageId> <PackageId>EonaCat.HID</PackageId>
<Version>1.0.5</Version> <Version>1.0.6</Version>
<Title>EonaCat.HID</Title> <Title>EonaCat.HID</Title>
<Authors>EonaCat (Jeroen Saey)</Authors> <Authors>EonaCat (Jeroen Saey)</Authors>
<Description>HID Devices</Description> <Description>HID Devices</Description>
<PackageLicenseFile>LICENSE</PackageLicenseFile> <PackageLicenseFile>LICENSE</PackageLicenseFile>
<PackageProjectUrl></PackageProjectUrl> <PackageProjectUrl></PackageProjectUrl>
<PackageIcon>icon.png</PackageIcon> <PackageIcon>icon.png</PackageIcon>
<PackageProjectUrl>https://www.nuget.org/packages/EonaCat.HID/</PackageProjectUrl> <PackageProjectUrl>https://www.nuget.org/packages/EonaCat.HID/</PackageProjectUrl>
<PackageTags>usb; hid; Jeroen;Saey</PackageTags> <PackageTags>usb; hid; Jeroen;Saey</PackageTags>
<GeneratePackageOnBuild>True</GeneratePackageOnBuild> <GeneratePackageOnBuild>True</GeneratePackageOnBuild>
<PackageReadmeFile>README.md</PackageReadmeFile> <PackageReadmeFile>README.md</PackageReadmeFile>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<None Include="..\icon.png"> <None Include="..\icon.png">
<Pack>True</Pack> <Pack>True</Pack>
<PackagePath>\</PackagePath> <PackagePath>\</PackagePath>
</None> </None>
<None Include="..\LICENSE"> <None Include="..\LICENSE">
<Pack>True</Pack> <Pack>True</Pack>
<PackagePath>\</PackagePath> <PackagePath>\</PackagePath>
</None> </None>
<None Include="..\README.md"> <None Include="..\README.md">
<Pack>True</Pack> <Pack>True</Pack>
<PackagePath>\</PackagePath> <PackagePath>\</PackagePath>
</None> </None>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="System.Runtime.InteropServices.RuntimeInformation" Version="4.3.0" /> <PackageReference Include="Microsoft.CSharp" Version="4.7.0" />
</ItemGroup> <PackageReference Include="System.Runtime.InteropServices.RuntimeInformation" Version="4.3.0" />
</ItemGroup>
<ItemGroup>
<Reference Include="System.Windows.Forms"> <ItemGroup>
<HintPath>..\..\..\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETFramework\v4.8\System.Windows.Forms.dll</HintPath> <None Update="icon.png">
</Reference> <Pack>True</Pack>
</ItemGroup> <PackagePath>\</PackagePath>
</None>
<ItemGroup> </ItemGroup>
<None Update="icon.png">
<Pack>True</Pack> <ItemGroup Condition="'$(TargetFramework)' == 'net6.0'">
<PackagePath>\</PackagePath> <PackageReference Include="System.Reflection.DispatchProxy">
</None> <Version>4.8.2</Version>
</ItemGroup> </PackageReference>
</ItemGroup>
</Project>
</Project>

View File

@@ -50,7 +50,9 @@ namespace EonaCat.HID
{ {
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{ {
return new Managers.Windows.HidManagerWindows(); var deviceManager = new Managers.Windows.HidManagerWindows();
deviceManager.Start();
return deviceManager;
} }
if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))

View File

@@ -59,10 +59,12 @@ namespace EonaCat.HID
{ {
lock (_lock) lock (_lock)
{ {
if (_stream != null || _isOpen) if (_stream != null || _isOpen)
return; {
return;
// Open HID device in non-blocking read/write mode }
// Open HID device in non-blocking read/write mode
int fd = NativeMethods.open(_devicePath, NativeMethods.O_RDWR | NativeMethods.O_NONBLOCK); int fd = NativeMethods.open(_devicePath, NativeMethods.O_RDWR | NativeMethods.O_NONBLOCK);
if (fd < 0) if (fd < 0)
{ {
@@ -262,10 +264,12 @@ namespace EonaCat.HID
public async Task SendFeatureReportAsync(HidReport report) public async Task SendFeatureReportAsync(HidReport report)
{ {
if (report == null) if (report == null)
throw new ArgumentNullException(nameof(report)); {
throw new ArgumentNullException(nameof(report));
// Prepare full report buffer: [reportId][reportData...] }
// Prepare full report buffer: [reportId][reportData...]
int size = 1 + (report.Data?.Length ?? 0); int size = 1 + (report.Data?.Length ?? 0);
byte[] buffer = new byte[size]; byte[] buffer = new byte[size];
buffer[0] = report.ReportId; buffer[0] = report.ReportId;
@@ -298,8 +302,11 @@ namespace EonaCat.HID
} }
finally finally
{ {
if (unmanagedBuffer != IntPtr.Zero) if (unmanagedBuffer != IntPtr.Zero)
Marshal.FreeHGlobal(unmanagedBuffer); {
Marshal.FreeHGlobal(unmanagedBuffer);
}
NativeMethods.close(fd); NativeMethods.close(fd);
} }
}); });
@@ -342,15 +349,20 @@ namespace EonaCat.HID
byte actualReportId = actualBuffer.Length > 0 ? actualBuffer[0] : (byte)0; byte actualReportId = actualBuffer.Length > 0 ? actualBuffer[0] : (byte)0;
byte[] reportData = actualBuffer.Length > 1 ? new byte[actualBuffer.Length - 1] : Array.Empty<byte>(); byte[] reportData = actualBuffer.Length > 1 ? new byte[actualBuffer.Length - 1] : Array.Empty<byte>();
if (reportData.Length > 0) if (reportData.Length > 0)
Array.Copy(actualBuffer, 1, reportData, 0, reportData.Length); {
Array.Copy(actualBuffer, 1, reportData, 0, reportData.Length);
}
return new HidReport(actualReportId, reportData); return new HidReport(actualReportId, reportData);
} }
finally finally
{ {
if (bufPtr != IntPtr.Zero) if (bufPtr != IntPtr.Zero)
Marshal.FreeHGlobal(bufPtr); {
Marshal.FreeHGlobal(bufPtr);
}
NativeMethods.close(fd); NativeMethods.close(fd);
} }
}); });

View File

@@ -77,7 +77,9 @@ namespace EonaCat.HID
public void Open() public void Open()
{ {
if (_isOpen) if (_isOpen)
{
return; return;
}
FileAccess access = FileAccess.ReadWrite; FileAccess access = FileAccess.ReadWrite;
SafeFileHandle handle = TryOpenDevice(GENERIC_READ | GENERIC_WRITE); SafeFileHandle handle = TryOpenDevice(GENERIC_READ | GENERIC_WRITE);
@@ -86,14 +88,18 @@ namespace EonaCat.HID
{ {
handle = TryOpenDevice(GENERIC_READ); handle = TryOpenDevice(GENERIC_READ);
if (handle != null && !handle.IsInvalid) if (handle != null && !handle.IsInvalid)
{
access = FileAccess.Read; access = FileAccess.Read;
}
} }
if ((handle == null || handle.IsInvalid) && Environment.Is64BitOperatingSystem) if ((handle == null || handle.IsInvalid) && Environment.Is64BitOperatingSystem)
{ {
handle = TryOpenDevice(GENERIC_WRITE); handle = TryOpenDevice(GENERIC_WRITE);
if (handle != null && !handle.IsInvalid) if (handle != null && !handle.IsInvalid)
{
access = FileAccess.Write; access = FileAccess.Write;
}
} }
if (handle == null || handle.IsInvalid) if (handle == null || handle.IsInvalid)
@@ -110,12 +116,16 @@ namespace EonaCat.HID
// HID descriptor // HID descriptor
if (!HidD_GetPreparsedData(_deviceHandle.DangerousGetHandle(), out _preparsedData)) if (!HidD_GetPreparsedData(_deviceHandle.DangerousGetHandle(), out _preparsedData))
{
throw new Win32Exception(Marshal.GetLastWin32Error(), "Failed HidD_GetPreparsedData"); throw new Win32Exception(Marshal.GetLastWin32Error(), "Failed HidD_GetPreparsedData");
}
HIDP_CAPS caps; HIDP_CAPS caps;
int capsRes = HidP_GetCaps(_preparsedData, out caps); int capsRes = HidP_GetCaps(_preparsedData, out caps);
if (capsRes != NativeMethods.HIDP_STATUS_SUCCESS) if (capsRes != NativeMethods.HIDP_STATUS_SUCCESS)
{
throw new Win32Exception(capsRes, "Failed HidP_GetCaps"); throw new Win32Exception(capsRes, "Failed HidP_GetCaps");
}
InputReportByteLength = caps.InputReportByteLength; InputReportByteLength = caps.InputReportByteLength;
OutputReportByteLength = caps.OutputReportByteLength; OutputReportByteLength = caps.OutputReportByteLength;
@@ -304,7 +314,9 @@ namespace EonaCat.HID
} }
if (report == null) if (report == null)
{
throw new ArgumentNullException(nameof(report)); throw new ArgumentNullException(nameof(report));
}
// Prepare buffer with ReportId + Data // Prepare buffer with ReportId + Data
var data = new byte[1 + report.Data.Length]; var data = new byte[1 + report.Data.Length];

View File

@@ -107,9 +107,11 @@ namespace EonaCat.HID.Managers.Mac
private void DeviceAddedCallback(IntPtr context, IntPtr result, IntPtr sender, IntPtr devicePtr) private void DeviceAddedCallback(IntPtr context, IntPtr result, IntPtr sender, IntPtr devicePtr)
{ {
if (devicePtr == IntPtr.Zero) if (devicePtr == IntPtr.Zero)
return; {
return;
}
try try
{ {
var device = new HidMac(devicePtr); var device = new HidMac(devicePtr);
@@ -125,9 +127,11 @@ namespace EonaCat.HID.Managers.Mac
private void DeviceRemovedCallback(IntPtr context, IntPtr result, IntPtr sender, IntPtr devicePtr) private void DeviceRemovedCallback(IntPtr context, IntPtr result, IntPtr sender, IntPtr devicePtr)
{ {
if (devicePtr == IntPtr.Zero) if (devicePtr == IntPtr.Zero)
return; {
return;
}
try try
{ {
var device = new HidMac(devicePtr); var device = new HidMac(devicePtr);

View File

@@ -5,6 +5,7 @@ using System.Collections.Generic;
using System.ComponentModel; using System.ComponentModel;
using System.IO; using System.IO;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using static EonaCat.HID.Managers.Windows.NativeMethods; using static EonaCat.HID.Managers.Windows.NativeMethods;
@@ -20,7 +21,10 @@ namespace EonaCat.HID.Managers.Windows
private const ushort HID_USAGE_PAGE_GENERIC = 0x01; private const ushort HID_USAGE_PAGE_GENERIC = 0x01;
private const ushort HID_USAGE_GENERIC_MOUSE = 0x02; private const ushort HID_USAGE_GENERIC_MOUSE = 0x02;
private static Guid GUID_DEVINTERFACE_HID = new Guid("4d1e55b2-f16f-11cf-88cb-001111000030"); private static Guid GUID_DEVINTERFACE_HID = new Guid("4D1E55B2-F16F-11CF-88CB-001111000030");
private static readonly IntPtr HWND_MESSAGE = new IntPtr(-3);
private const string WINDOW_CLASS_NAME = "EonaCat_HidDeviceNotificationWindow";
private readonly object _lock = new object(); private readonly object _lock = new object();
@@ -28,15 +32,15 @@ namespace EonaCat.HID.Managers.Windows
private IntPtr _deviceNotificationHandle; private IntPtr _deviceNotificationHandle;
private WndProc _windowProcDelegate; private WndProc _windowProcDelegate;
private IntPtr _messageWindowHandle; private IntPtr _messageWindowHandle;
private readonly Dictionary<string, IHid> _knownDevices = new(); private readonly Dictionary<string, IHid> _knownDevices = new();
private Thread _messageThread;
public event EventHandler<HidEventArgs> OnDeviceInserted; public event EventHandler<HidEventArgs> OnDeviceInserted;
public event EventHandler<HidEventArgs> OnDeviceRemoved; public event EventHandler<HidEventArgs> OnDeviceRemoved;
public HidManagerWindows() public HidManagerWindows()
{ {
InitializeMessageWindow(); // Do nothing
RegisterForDeviceNotifications();
} }
private void InitializeMessageWindow() private void InitializeMessageWindow()
@@ -46,7 +50,7 @@ namespace EonaCat.HID.Managers.Windows
WNDCLASS wc = new WNDCLASS() WNDCLASS wc = new WNDCLASS()
{ {
lpfnWndProc = _windowProcDelegate, lpfnWndProc = _windowProcDelegate,
lpszClassName = "HidDeviceNotificationWindow_" + Guid.NewGuid(), lpszClassName = WINDOW_CLASS_NAME,
style = 0, style = 0,
cbClsExtra = 0, cbClsExtra = 0,
cbWndExtra = 0, cbWndExtra = 0,
@@ -61,24 +65,79 @@ namespace EonaCat.HID.Managers.Windows
if (classAtom == 0) if (classAtom == 0)
{ {
throw new Win32Exception(Marshal.GetLastWin32Error(), "Failed to register window class"); throw new Win32Exception(Marshal.GetLastWin32Error(), "Failed to register window class");
} }
_messageWindowHandle = CreateWindowEx(
0,
wc.lpszClassName,
"",
0,
0, 0, 0, 0,
HWND_MESSAGE,
IntPtr.Zero,
wc.hInstance,
IntPtr.Zero);
_messageWindowHandle = CreateWindowEx(
0,
wc.lpszClassName,
"",
0,
0, 0, 0, 0,
IntPtr.Zero,
IntPtr.Zero,
wc.hInstance,
IntPtr.Zero);
if (_messageWindowHandle == IntPtr.Zero) if (_messageWindowHandle == IntPtr.Zero)
{ {
throw new Win32Exception(Marshal.GetLastWin32Error(), "Failed to create message-only window"); throw new Win32Exception(Marshal.GetLastWin32Error(), "Failed to create message-only window");
} }
} }
public void Start()
{
_messageThread = new Thread(MessageThreadProc)
{
IsBackground = true
};
_messageThread.SetApartmentState(ApartmentState.STA);
_messageThread.Start();
}
public Task StartAsync() => Task.Run(Start);
private void OnWpfMessage(object sender, dynamic e)
{
if (e.Message == WM_DEVICECHANGE)
{
WindowProc(IntPtr.Zero, e.Message, e.WParam, e.LParam);
}
}
private void MessageThreadProc()
{
InitializeMessageWindow();
RegisterForDeviceNotifications();
// Start message loop
MSG msg;
while (GetMessage(out msg, IntPtr.Zero, 0, 0))
{
TranslateMessage(ref msg);
DispatchMessage(ref msg);
}
}
private void StartMessageLoop()
{
Thread thread = new Thread(() =>
{
InitializeMessageWindow();
RegisterForDeviceNotifications();
MSG msg;
while (GetMessage(out msg, IntPtr.Zero, 0, 0))
{
TranslateMessage(ref msg);
DispatchMessage(ref msg);
}
});
thread.IsBackground = true;
thread.Start();
}
private void RegisterForDeviceNotifications() private void RegisterForDeviceNotifications()
{ {
@@ -102,7 +161,7 @@ namespace EonaCat.HID.Managers.Windows
} }
} }
private IntPtr WindowProc(IntPtr hwnd, uint msg, IntPtr wParam, IntPtr lParam) internal IntPtr WindowProc(IntPtr hwnd, uint msg, IntPtr wParam, IntPtr lParam)
{ {
if (msg == WM_DEVICECHANGE) if (msg == WM_DEVICECHANGE)
{ {
@@ -125,8 +184,10 @@ namespace EonaCat.HID.Managers.Windows
using (var testHandle = CreateFile(devicePath, 0, using (var testHandle = CreateFile(devicePath, 0,
FileShare.ReadWrite, IntPtr.Zero, FileMode.Open, 0, IntPtr.Zero)) FileShare.ReadWrite, IntPtr.Zero, FileMode.Open, 0, IntPtr.Zero))
{ {
if (testHandle.IsInvalid) if (testHandle.IsInvalid)
return DefWindowProc(hwnd, msg, wParam, lParam); {
return DefWindowProc(hwnd, msg, wParam, lParam);
}
} }
var device = new HidWindows(devicePath); var device = new HidWindows(devicePath);
@@ -188,7 +249,9 @@ namespace EonaCat.HID.Managers.Windows
DIGCF_PRESENT | DIGCF_DEVICEINTERFACE); DIGCF_PRESENT | DIGCF_DEVICEINTERFACE);
if (devInfo == IntPtr.Zero || devInfo == new IntPtr(-1)) if (devInfo == IntPtr.Zero || devInfo == new IntPtr(-1))
{
throw new Win32Exception(Marshal.GetLastWin32Error(), "SetupDiGetClassDevs failed"); throw new Win32Exception(Marshal.GetLastWin32Error(), "SetupDiGetClassDevs failed");
}
try try
{ {
@@ -203,7 +266,9 @@ namespace EonaCat.HID.Managers.Windows
{ {
int error = Marshal.GetLastWin32Error(); int error = Marshal.GetLastWin32Error();
if (error == ERROR_NO_MORE_ITEMS) if (error == ERROR_NO_MORE_ITEMS)
{
break; break;
}
throw new Win32Exception(error, "SetupDiEnumDeviceInterfaces failed"); throw new Win32Exception(error, "SetupDiEnumDeviceInterfaces failed");
} }
@@ -211,7 +276,9 @@ namespace EonaCat.HID.Managers.Windows
uint requiredSize = 0; uint requiredSize = 0;
SetupDiGetDeviceInterfaceDetail(devInfo, ref iface, IntPtr.Zero, 0, ref requiredSize, IntPtr.Zero); SetupDiGetDeviceInterfaceDetail(devInfo, ref iface, IntPtr.Zero, 0, ref requiredSize, IntPtr.Zero);
if (Marshal.GetLastWin32Error() != ERROR_INSUFFICIENT_BUFFER || requiredSize == 0) if (Marshal.GetLastWin32Error() != ERROR_INSUFFICIENT_BUFFER || requiredSize == 0)
{
continue; continue;
}
IntPtr detailDataBuffer = Marshal.AllocHGlobal((int)requiredSize); IntPtr detailDataBuffer = Marshal.AllocHGlobal((int)requiredSize);
try try
@@ -229,13 +296,17 @@ namespace EonaCat.HID.Managers.Windows
string devicePath = Marshal.PtrToStringAuto(pDevicePathName); string devicePath = Marshal.PtrToStringAuto(pDevicePathName);
if (string.IsNullOrWhiteSpace(devicePath)) if (string.IsNullOrWhiteSpace(devicePath))
{
continue; continue;
}
// Try to open with zero access to ensure its reachable // Try to open with zero access to ensure its reachable
using (var testHandle = CreateFile(devicePath, 0, FileShare.ReadWrite, IntPtr.Zero, FileMode.Open, 0, IntPtr.Zero)) using (var testHandle = CreateFile(devicePath, 0, FileShare.ReadWrite, IntPtr.Zero, FileMode.Open, 0, IntPtr.Zero))
{ {
if (testHandle.IsInvalid) if (testHandle.IsInvalid)
{
continue; continue;
}
} }
HidWindows device; HidWindows device;
@@ -296,6 +367,35 @@ namespace EonaCat.HID.Managers.Windows
} }
_knownDevices.Clear(); _knownDevices.Clear();
if (_messageThread != null && _messageThread.IsAlive)
{
// Post a quit message to end the message loop
_messageThread.Join();
_messageThread = null;
}
if (_windowProcDelegate != null)
{
_windowProcDelegate = null;
}
if (_messageWindowHandle != IntPtr.Zero)
{
DestroyWindow(_messageWindowHandle);
_messageWindowHandle = IntPtr.Zero;
}
if (_deviceNotificationHandle != IntPtr.Zero)
{
UnregisterDeviceNotification(_deviceNotificationHandle);
_deviceNotificationHandle = IntPtr.Zero;
}
if ( _messageWindowHandle != IntPtr.Zero)
{
DestroyWindow(_messageWindowHandle);
}
} }
} }
@@ -321,22 +421,21 @@ namespace EonaCat.HID.Managers.Windows
public const int DBT_DEVICEREMOVECOMPLETE = 0x8004; public const int DBT_DEVICEREMOVECOMPLETE = 0x8004;
public const int DBT_DEVTYP_DEVICEINTERFACE = 5; public const int DBT_DEVTYP_DEVICEINTERFACE = 5;
[StructLayout(LayoutKind.Sequential)] [StructLayout(LayoutKind.Sequential)]
public struct WNDCLASS public struct WNDCLASS
{ {
public uint style; public uint style;
[MarshalAs(UnmanagedType.FunctionPtr)] public WndProc lpfnWndProc;
public WndProc lpfnWndProc; public int cbClsExtra;
public int cbClsExtra; public int cbWndExtra;
public int cbWndExtra; public IntPtr hInstance;
public IntPtr hInstance; public IntPtr hIcon;
public IntPtr hIcon; public IntPtr hCursor;
public IntPtr hCursor; public IntPtr hbrBackground;
public IntPtr hbrBackground; [MarshalAs(UnmanagedType.LPTStr)]
[MarshalAs(UnmanagedType.LPTStr)] public string lpszMenuName;
public string lpszMenuName; [MarshalAs(UnmanagedType.LPTStr)]
[MarshalAs(UnmanagedType.LPTStr)] public string lpszClassName;
public string lpszClassName;
} }
[StructLayout(LayoutKind.Sequential)] [StructLayout(LayoutKind.Sequential)]
@@ -373,9 +472,20 @@ namespace EonaCat.HID.Managers.Windows
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 256)] [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 256)]
public string DevicePath; public string DevicePath;
}
[StructLayout(LayoutKind.Sequential)]
public struct MSG
{
public IntPtr hwnd;
public uint message;
public IntPtr wParam;
public IntPtr lParam;
public uint time;
public int pt_x;
public int pt_y;
} }
// Declare delegate for WndProc // Declare delegate for WndProc
public delegate IntPtr WndProc(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam); public delegate IntPtr WndProc(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam);
@@ -397,9 +507,6 @@ namespace EonaCat.HID.Managers.Windows
[DllImport("user32.dll")] [DllImport("user32.dll")]
public static extern bool DestroyWindow(IntPtr hWnd); public static extern bool DestroyWindow(IntPtr hWnd);
[DllImport("kernel32.dll", CharSet = CharSet.Auto)]
public static extern IntPtr GetModuleHandle(string lpModuleName);
[DllImport("user32.dll")] [DllImport("user32.dll")]
public static extern IntPtr DefWindowProc(IntPtr hwnd, uint uMsg, IntPtr wParam, IntPtr lParam); public static extern IntPtr DefWindowProc(IntPtr hwnd, uint uMsg, IntPtr wParam, IntPtr lParam);
@@ -454,7 +561,22 @@ namespace EonaCat.HID.Managers.Windows
IntPtr lpSecurityAttributes, IntPtr lpSecurityAttributes,
int dwCreationDisposition, int dwCreationDisposition,
int dwFlagsAndAttributes, int dwFlagsAndAttributes,
IntPtr hTemplateFile); IntPtr hTemplateFile);
[DllImport("user32.dll", SetLastError = true)]
public static extern bool GetMessage(out MSG lpMsg, IntPtr hWnd, uint wMsgFilterMin, uint wMsgFilterMax);
[DllImport("user32.dll")]
public static extern bool TranslateMessage([In] ref MSG lpMsg);
[DllImport("user32.dll")]
public static extern IntPtr DispatchMessage([In] ref MSG lpMsg);
[DllImport("kernel32.dll", SetLastError = true)]
public static extern IntPtr GetModuleHandle(string lpModuleName);
[DllImport("user32.dll", SetLastError = true)]
public static extern IntPtr RegisterDeviceNotification(IntPtr hRecipient, IntPtr NotificationFilter, int Flags);
// HID APIs // HID APIs
[DllImport("hid.dll", SetLastError = true)] [DllImport("hid.dll", SetLastError = true)]