XFE Git
XFE Studio Git
Git 首页 全局搜索
XFE 主站 文档 NuGet

LogiBoardToolBox

【WinUI】罗技鼠标板载驱动程序

公开
关注 0 Fork 0 Star 0
UTF-8
using System.ComponentModel;
using System.Runtime.InteropServices;
using System.Runtime.Versioning;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Channels;
using LogiBoardToolBox.Core.HidPlusPlus;
using Microsoft.Win32.SafeHandles;

namespace LogiBoardToolBox.Core.Windows;

public sealed record HidDeviceInfo(string Path, ushort ProductId, string ProductName, string SerialNumber,
    ushort UsagePage, ushort Usage, int InputReportLength, int OutputReportLength)
{
    public string DisplayName => $"{ProductName} · 046D:{ProductId:X4}";
    public string CollectionGroup => Regex.Replace(Regex.Replace(Path, "&col[0-9a-f]{2}", "", RegexOptions.IgnoreCase), "&[0-9a-f]{4}(?=#)", "", RegexOptions.IgnoreCase).ToUpperInvariant();
}

/// <summary>Uses Windows' existing HID driver; no filter driver or administrator access required.</summary>
[SupportedOSPlatform("windows")]
public sealed class WindowsHidTransport : IHidTransport
{
    private readonly FileStream stream;
    private readonly List<FileStream> streams = [];
    private readonly CancellationTokenSource lifetime = new();
    private readonly Channel<byte[]> reports = Channel.CreateBounded<byte[]>(new BoundedChannelOptions(256)
    { SingleReader = true, FullMode = BoundedChannelFullMode.DropOldest });
    public int InputReportLength { get; }
    public int OutputReportLength { get; }
    public WindowsHidTransport(HidDeviceInfo device)
    {
        stream = OpenStream(device.Path);
        streams.Add(stream);
        InputReportLength = device.InputReportLength;
        OutputReportLength = device.OutputReportLength;
        _ = PumpAsync(stream, device.InputReportLength);
        // Windows delivers 0x10 replies through the short-report collection, even for
        // some requests sent using 0x11. Read both collections of this physical interface.
        try
        {
            foreach (var sibling in EnumerateLogitechDevices().Where(d => d.Path != device.Path && d.CollectionGroup == device.CollectionGroup && d.InputReportLength < 20))
            {
                var input = OpenStream(sibling.Path);
                streams.Add(input);
                _ = PumpAsync(input, sibling.InputReportLength);
            }
        }
        catch { Dispose(); throw; }
    }

    private static FileStream OpenStream(string path)
    {
        var handle = Native.CreateFile(path, 0xC0000000, 3, IntPtr.Zero, 3, 0x40000000, IntPtr.Zero);
        if (handle.IsInvalid)
        {
            var error = Marshal.GetLastWin32Error();
            handle.Dispose();
            throw new Win32Exception(error, "无法打开罗技 HID++ 接口。");
        }
        try { return new FileStream(handle, FileAccess.ReadWrite, 1, isAsync: true); }
        catch { handle.Dispose(); throw; }
    }

    public async Task WriteAsync(byte[] report, CancellationToken cancellationToken) =>
        await stream.WriteAsync(report, cancellationToken).ConfigureAwait(false);

    private async Task PumpAsync(FileStream input, int length)
    {
        try
        {
            while (!lifetime.IsCancellationRequested)
            {
                var report = new byte[length];
                var count = await input.ReadAsync(report, lifetime.Token).ConfigureAwait(false);
                if (count == 0) throw new IOException("鼠标 HID 接口已断开。");
                await reports.Writer.WriteAsync(count == report.Length ? report : report[..count], lifetime.Token).ConfigureAwait(false);
            }
        }
        catch (Exception ex) when (ex is OperationCanceledException or ObjectDisposedException) { }
        catch (Exception ex) { reports.Writer.TryComplete(ex); }
    }
    public async Task<byte[]> ReadAsync(CancellationToken cancellationToken) => await reports.Reader.ReadAsync(cancellationToken).ConfigureAwait(false);
    public void Dispose()
    {
        lifetime.Cancel();
        foreach (var input in streams) input.Dispose();
        reports.Writer.TryComplete();
    }

    public static IReadOnlyList<HidDeviceInfo> EnumerateLogitechDevices()
    {
        Native.HidD_GetHidGuid(out var hidGuid);
        var set = Native.SetupDiGetClassDevs(ref hidGuid, null, IntPtr.Zero, 0x12);
        if (set == new IntPtr(-1)) throw new Win32Exception(Marshal.GetLastWin32Error());
        var devices = new List<HidDeviceInfo>();
        try
        {
            for (uint i = 0; ; i++)
            {
                var data = new Native.DeviceInterfaceData { Size = Marshal.SizeOf<Native.DeviceInterfaceData>() };
                if (!Native.SetupDiEnumDeviceInterfaces(set, IntPtr.Zero, ref hidGuid, i, ref data))
                {
                    var error = Marshal.GetLastWin32Error();
                    if (error == 259) break;
                    throw new Win32Exception(error);
                }
                Native.SetupDiGetDeviceInterfaceDetail(set, ref data, IntPtr.Zero, 0, out var size, IntPtr.Zero);
                if (size < 8 || size > 65536) continue;
                var detail = Marshal.AllocHGlobal((int)size);
                try
                {
                    Marshal.WriteInt32(detail, IntPtr.Size == 8 ? 8 : 6);
                    if (!Native.SetupDiGetDeviceInterfaceDetail(set, ref data, detail, size, out _, IntPtr.Zero)) continue;
                    var path = Marshal.PtrToStringUni(detail + 4);
                    if (path is null || !path.Contains("vid_046d", StringComparison.OrdinalIgnoreCase)) continue;
                    using var handle = Native.CreateFile(path, 0, 3, IntPtr.Zero, 3, 0, IntPtr.Zero);
                    if (handle.IsInvalid) continue;
                    var attributes = new Native.HidAttributes { Size = Marshal.SizeOf<Native.HidAttributes>() };
                    if (!Native.HidD_GetAttributes(handle, ref attributes) || attributes.VendorId != 0x046D) continue;
                    if (!Native.HidD_GetPreparsedData(handle, out var preparsed)) continue;
                    try
                    {
                        if (Native.HidP_GetCaps(preparsed, out var caps) < 0) continue;
                        if (caps.UsagePage < 0xFF00 || caps.InputReportByteLength < 7 || caps.OutputReportByteLength < 7) continue;
                        var name = ReadString(handle, Native.HidD_GetProductString);
                        var serial = ReadString(handle, Native.HidD_GetSerialNumberString);
                        devices.Add(new(path, attributes.ProductId, string.IsNullOrWhiteSpace(name) ? "Logitech Receiver" : name,
                            serial, caps.UsagePage, caps.Usage, caps.InputReportByteLength, caps.OutputReportByteLength));
                    }
                    finally { Native.HidD_FreePreparsedData(preparsed); }
                }
                finally { Marshal.FreeHGlobal(detail); }
            }
        }
        finally { Native.SetupDiDestroyDeviceInfoList(set); }
        return devices;
    }

    private delegate bool HidStringReader(SafeFileHandle handle, StringBuilder buffer, uint length);
    private static string ReadString(SafeFileHandle handle, HidStringReader read)
    {
        var buffer = new StringBuilder(256);
        return read(handle, buffer, 512) ? buffer.ToString() : "";
    }

    private static class Native
    {
        [StructLayout(LayoutKind.Sequential)]
        internal struct DeviceInterfaceData { internal int Size; internal Guid ClassGuid; internal uint Flags; internal UIntPtr Reserved; }
        [StructLayout(LayoutKind.Sequential)]
        internal struct HidAttributes { internal int Size; internal ushort VendorId; internal ushort ProductId; internal ushort Version; }
        [StructLayout(LayoutKind.Sequential)]
        internal struct HidCaps
        {
            internal ushort Usage, UsagePage, InputReportByteLength, OutputReportByteLength, FeatureReportByteLength;
            [MarshalAs(UnmanagedType.ByValArray, SizeConst = 17)] internal ushort[] Reserved;
            internal ushort LinkCollectionNodes, InputButtonCaps, InputValueCaps, InputDataIndices;
            internal ushort OutputButtonCaps, OutputValueCaps, OutputDataIndices, FeatureButtonCaps, FeatureValueCaps, FeatureDataIndices;
        }
        [DllImport("hid.dll")] internal static extern void HidD_GetHidGuid(out Guid guid);
        [DllImport("hid.dll", SetLastError = true)] [return: MarshalAs(UnmanagedType.U1)] internal static extern bool HidD_GetAttributes(SafeFileHandle handle, ref HidAttributes attributes);
        [DllImport("hid.dll", SetLastError = true)] [return: MarshalAs(UnmanagedType.U1)] internal static extern bool HidD_GetPreparsedData(SafeFileHandle handle, out IntPtr data);
        [DllImport("hid.dll")] [return: MarshalAs(UnmanagedType.U1)] internal static extern bool HidD_FreePreparsedData(IntPtr data);
        [DllImport("hid.dll")] internal static extern int HidP_GetCaps(IntPtr data, out HidCaps caps);
        [DllImport("hid.dll", CharSet = CharSet.Unicode)] [return: MarshalAs(UnmanagedType.U1)] internal static extern bool HidD_GetProductString(SafeFileHandle handle, StringBuilder text, uint size);
        [DllImport("hid.dll", CharSet = CharSet.Unicode)] [return: MarshalAs(UnmanagedType.U1)] internal static extern bool HidD_GetSerialNumberString(SafeFileHandle handle, StringBuilder text, uint size);
        [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)] internal static extern SafeFileHandle CreateFile(string path, uint access, uint share, IntPtr security, uint creation, uint flags, IntPtr template);
        [DllImport("setupapi.dll", CharSet = CharSet.Unicode, SetLastError = true)] internal static extern IntPtr SetupDiGetClassDevs(ref Guid guid, string? enumerator, IntPtr parent, uint flags);
        [DllImport("setupapi.dll", SetLastError = true)] [return: MarshalAs(UnmanagedType.Bool)] internal static extern bool SetupDiEnumDeviceInterfaces(IntPtr set, IntPtr device, ref Guid guid, uint index, ref DeviceInterfaceData data);
        [DllImport("setupapi.dll", CharSet = CharSet.Unicode, SetLastError = true)] [return: MarshalAs(UnmanagedType.Bool)] internal static extern bool SetupDiGetDeviceInterfaceDetail(IntPtr set, ref DeviceInterfaceData data, IntPtr detail, uint size, out uint required, IntPtr device);
        [DllImport("setupapi.dll")] [return: MarshalAs(UnmanagedType.Bool)] internal static extern bool SetupDiDestroyDeviceInfoList(IntPtr set);
    }
}