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

@@ -48,6 +48,7 @@
<Reference Include="System.Drawing" />
<Reference Include="System.Windows.Forms" />
<Reference Include="System.Xml" />
<Reference Include="WindowsBase" />
</ItemGroup>
<ItemGroup>
<Compile Include="AboutBox.cs">

View File

@@ -10,6 +10,7 @@ using System.Linq;
using System.Reflection;
using System.Threading.Tasks;
using System.Windows.Forms;
using System.Windows.Threading;
namespace EonaCat.HID.Analyzer
{
@@ -42,7 +43,8 @@ namespace EonaCat.HID.Analyzer
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)
@@ -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
{
RefreshDevicesAsync();
UpdateDeviceList();
await RefreshDevicesAsync().ConfigureAwait(false);
UpdateDeviceListAsync();
toolStripStatusLabel1.Text = "Please select device and click open to start.";
}
catch (Exception ex)
@@ -94,33 +96,27 @@ namespace EonaCat.HID.Analyzer
}));
}
private void UpdateDeviceList()
private async Task UpdateDeviceListAsync()
{
dataGridView1.SelectionChanged -= DataGridView1_SelectionChanged;
dataGridView1.Rows.Clear();
if (_deviceList == null || !_deviceList.Any())
{
return;
}
// Capture the data in a local variable so the UI update can happen in one go
var rows = new List<string[]>();
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.ProductName,
device.Manufacturer,
device.SerialNumber,
device.IsReadingSupport.ToString(),
device.IsWritingSupport.ToString(),
device.InputReportByteLength.ToString(),
device.OutputReportByteLength.ToString(),
device.FeatureReportByteLength.ToString(),
@@ -128,10 +124,34 @@ namespace EonaCat.HID.Analyzer
device.DevicePath
};
rows.Add(row);
}
if (dataGridView1.InvokeRequired)
{
dataGridView1.Invoke(new Action(() =>
{
dataGridView1.SelectionChanged -= DataGridView1_SelectionChanged;
dataGridView1.Rows.Clear();
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")
@@ -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
{
RefreshDevicesAsync();
UpdateDeviceList();
await RefreshDevicesAsync().ConfigureAwait(false);
UpdateDeviceListAsync();
}
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
{
@@ -203,8 +223,8 @@ namespace EonaCat.HID.Analyzer
vid = ushort.Parse(str[0], NumberStyles.AllowHexSpecifier);
pid = ushort.Parse(str[1], NumberStyles.AllowHexSpecifier);
}
RefreshDevicesAsync(vid, pid);
UpdateDeviceList();
await RefreshDevicesAsync(vid, pid).ConfigureAwait(false);
UpdateDeviceListAsync();
}
catch (Exception ex)
{

View File

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

View File

@@ -1,13 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net46;net6.0</TargetFrameworks>
<TargetFrameworks>net48;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.5</Version>
<Version>1.0.6</Version>
<Title>EonaCat.HID</Title>
<Authors>EonaCat (Jeroen Saey)</Authors>
<Description>HID Devices</Description>
@@ -36,15 +36,10 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.CSharp" Version="4.7.0" />
<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>
@@ -52,4 +47,10 @@
</None>
</ItemGroup>
<ItemGroup Condition="'$(TargetFramework)' == 'net6.0'">
<PackageReference Include="System.Reflection.DispatchProxy">
<Version>4.8.2</Version>
</PackageReference>
</ItemGroup>
</Project>

View File

@@ -50,7 +50,9 @@ namespace EonaCat.HID
{
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))

View File

@@ -60,7 +60,9 @@ namespace EonaCat.HID
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);
@@ -263,7 +265,9 @@ namespace EonaCat.HID
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);
@@ -299,7 +303,10 @@ namespace EonaCat.HID
finally
{
if (unmanagedBuffer != IntPtr.Zero)
{
Marshal.FreeHGlobal(unmanagedBuffer);
}
NativeMethods.close(fd);
}
});
@@ -343,14 +350,19 @@ namespace EonaCat.HID
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);
}
});

View File

@@ -77,7 +77,9 @@ namespace EonaCat.HID
public void Open()
{
if (_isOpen)
{
return;
}
FileAccess access = FileAccess.ReadWrite;
SafeFileHandle handle = TryOpenDevice(GENERIC_READ | GENERIC_WRITE);
@@ -86,15 +88,19 @@ namespace EonaCat.HID
{
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)
{
@@ -110,12 +116,16 @@ namespace EonaCat.HID
// 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;
@@ -304,7 +314,9 @@ namespace EonaCat.HID
}
if (report == null)
{
throw new ArgumentNullException(nameof(report));
}
// Prepare buffer with ReportId + Data
var data = new byte[1 + report.Data.Length];

View File

@@ -108,7 +108,9 @@ namespace EonaCat.HID.Managers.Mac
private void DeviceAddedCallback(IntPtr context, IntPtr result, IntPtr sender, IntPtr devicePtr)
{
if (devicePtr == IntPtr.Zero)
{
return;
}
try
{
@@ -126,7 +128,9 @@ namespace EonaCat.HID.Managers.Mac
private void DeviceRemovedCallback(IntPtr context, IntPtr result, IntPtr sender, IntPtr devicePtr)
{
if (devicePtr == IntPtr.Zero)
{
return;
}
try
{

View File

@@ -5,6 +5,7 @@ using System.Collections.Generic;
using System.ComponentModel;
using System.IO;
using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
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_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();
@@ -29,14 +33,14 @@ namespace EonaCat.HID.Managers.Windows
private WndProc _windowProcDelegate;
private IntPtr _messageWindowHandle;
private readonly Dictionary<string, IHid> _knownDevices = new();
private Thread _messageThread;
public event EventHandler<HidEventArgs> OnDeviceInserted;
public event EventHandler<HidEventArgs> OnDeviceRemoved;
public HidManagerWindows()
{
InitializeMessageWindow();
RegisterForDeviceNotifications();
// Do nothing
}
private void InitializeMessageWindow()
@@ -46,7 +50,7 @@ namespace EonaCat.HID.Managers.Windows
WNDCLASS wc = new WNDCLASS()
{
lpfnWndProc = _windowProcDelegate,
lpszClassName = "HidDeviceNotificationWindow_" + Guid.NewGuid(),
lpszClassName = WINDOW_CLASS_NAME,
style = 0,
cbClsExtra = 0,
cbWndExtra = 0,
@@ -69,17 +73,72 @@ namespace EonaCat.HID.Managers.Windows
"",
0,
0, 0, 0, 0,
IntPtr.Zero,
HWND_MESSAGE,
IntPtr.Zero,
wc.hInstance,
IntPtr.Zero);
if (_messageWindowHandle == IntPtr.Zero)
{
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()
{
DEV_BROADCAST_DEVICEINTERFACE devInterface = new DEV_BROADCAST_DEVICEINTERFACE
@@ -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)
{
@@ -126,8 +185,10 @@ namespace EonaCat.HID.Managers.Windows
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();
@@ -188,7 +249,9 @@ namespace EonaCat.HID.Managers.Windows
DIGCF_PRESENT | DIGCF_DEVICEINTERFACE);
if (devInfo == IntPtr.Zero || devInfo == new IntPtr(-1))
{
throw new Win32Exception(Marshal.GetLastWin32Error(), "SetupDiGetClassDevs failed");
}
try
{
@@ -203,7 +266,9 @@ namespace EonaCat.HID.Managers.Windows
{
int error = Marshal.GetLastWin32Error();
if (error == ERROR_NO_MORE_ITEMS)
{
break;
}
throw new Win32Exception(error, "SetupDiEnumDeviceInterfaces failed");
}
@@ -211,7 +276,9 @@ namespace EonaCat.HID.Managers.Windows
uint requiredSize = 0;
SetupDiGetDeviceInterfaceDetail(devInfo, ref iface, IntPtr.Zero, 0, ref requiredSize, IntPtr.Zero);
if (Marshal.GetLastWin32Error() != ERROR_INSUFFICIENT_BUFFER || requiredSize == 0)
{
continue;
}
IntPtr detailDataBuffer = Marshal.AllocHGlobal((int)requiredSize);
try
@@ -229,14 +296,18 @@ namespace EonaCat.HID.Managers.Windows
string devicePath = Marshal.PtrToStringAuto(pDevicePathName);
if (string.IsNullOrWhiteSpace(devicePath))
{
continue;
}
// 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))
{
if (testHandle.IsInvalid)
{
continue;
}
}
HidWindows device;
try
@@ -296,6 +367,35 @@ namespace EonaCat.HID.Managers.Windows
}
_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);
}
}
}
@@ -325,7 +425,6 @@ namespace EonaCat.HID.Managers.Windows
public struct WNDCLASS
{
public uint style;
[MarshalAs(UnmanagedType.FunctionPtr)]
public WndProc lpfnWndProc;
public int cbClsExtra;
public int cbWndExtra;
@@ -375,6 +474,17 @@ namespace EonaCat.HID.Managers.Windows
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
public delegate IntPtr WndProc(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam);
@@ -397,9 +507,6 @@ namespace EonaCat.HID.Managers.Windows
[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);
@@ -456,6 +563,21 @@ namespace EonaCat.HID.Managers.Windows
int dwFlagsAndAttributes,
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
[DllImport("hid.dll", SetLastError = true)]
public static extern bool HidD_GetAttributes(IntPtr hidDeviceObject, ref HidDeviceAttributes attributes);