using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Runtime.InteropServices; using System.Threading; using Microsoft.Win32.SafeHandles; // Minimal HID++ 1.0 transport for a Logitech receiver on Windows. // Opens the vendor-defined collections (usage page 0xFF00) of the receiver's // HID++ interface, reads every input report on a background thread, and writes // short (0x10, 7 bytes) / long (0x11, 20 bytes) reports. public class HidppCollection { public string Path; public ushort UsagePage, Usage, InLen, OutLen; } public class HidppReceiver : IDisposable { [DllImport("hid.dll")] static extern void HidD_GetHidGuid(out Guid g); [DllImport("hid.dll")] static extern bool HidD_GetPreparsedData(SafeFileHandle h, out IntPtr p); [DllImport("hid.dll")] static extern bool HidD_FreePreparsedData(IntPtr p); [DllImport("hid.dll")] static extern int HidP_GetCaps(IntPtr p, out HIDP_CAPS caps); [DllImport("setupapi.dll", CharSet = CharSet.Unicode, SetLastError = true)] static extern IntPtr SetupDiGetClassDevs(ref Guid g, IntPtr e, IntPtr h, int flags); [DllImport("setupapi.dll", SetLastError = true)] static extern bool SetupDiEnumDeviceInterfaces(IntPtr s, IntPtr d, ref Guid g, int i, ref SP_DEVICE_INTERFACE_DATA did); [DllImport("setupapi.dll", CharSet = CharSet.Unicode, SetLastError = true)] static extern bool SetupDiGetDeviceInterfaceDetail(IntPtr s, ref SP_DEVICE_INTERFACE_DATA did, IntPtr buf, int size, out int req, IntPtr dev); [DllImport("setupapi.dll")] static extern bool SetupDiDestroyDeviceInfoList(IntPtr s); [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)] static extern SafeFileHandle CreateFile(string n, uint access, uint share, IntPtr sa, uint disp, uint flags, IntPtr t); [DllImport("kernel32.dll", SetLastError = true)] static extern bool ReadFile(SafeFileHandle h, byte[] b, int n, out int r, IntPtr ov); [DllImport("kernel32.dll", SetLastError = true)] static extern bool WriteFile(SafeFileHandle h, byte[] b, int n, out int w, IntPtr ov); [DllImport("kernel32.dll")] static extern bool CancelIoEx(SafeFileHandle h, IntPtr ov); [StructLayout(LayoutKind.Sequential)] struct SP_DEVICE_INTERFACE_DATA { public int cbSize; public Guid g; public int flags; public IntPtr r; } [StructLayout(LayoutKind.Sequential)] struct HIDP_CAPS { public ushort Usage, UsagePage, InputReportByteLength, OutputReportByteLength, FeatureReportByteLength; [MarshalAs(UnmanagedType.ByValArray, SizeConst = 17)] public ushort[] Reserved; public ushort n1, n2, n3, n4, n5, n6, n7, n8, n9, n10; } public static List Find(string vidPidFragment) { var list = new List(); Guid g; HidD_GetHidGuid(out g); IntPtr set = SetupDiGetClassDevs(ref g, IntPtr.Zero, IntPtr.Zero, 0x12); try { for (int i = 0; ; i++) { var did = new SP_DEVICE_INTERFACE_DATA(); did.cbSize = Marshal.SizeOf(did); if (!SetupDiEnumDeviceInterfaces(set, IntPtr.Zero, ref g, i, ref did)) break; int req; SetupDiGetDeviceInterfaceDetail(set, ref did, IntPtr.Zero, 0, out req, IntPtr.Zero); IntPtr buf = Marshal.AllocHGlobal(req); try { Marshal.WriteInt32(buf, IntPtr.Size == 8 ? 8 : 6); if (!SetupDiGetDeviceInterfaceDetail(set, ref did, buf, req, out req, IntPtr.Zero)) continue; string path = Marshal.PtrToStringUni(new IntPtr(buf.ToInt64() + 4)); if (path.IndexOf(vidPidFragment, StringComparison.OrdinalIgnoreCase) < 0) continue; using (var h = CreateFile(path, 0, 3, IntPtr.Zero, 3, 0, IntPtr.Zero)) { if (h.IsInvalid) continue; IntPtr pp; if (!HidD_GetPreparsedData(h, out pp)) continue; HIDP_CAPS caps; HidP_GetCaps(pp, out caps); HidD_FreePreparsedData(pp); list.Add(new HidppCollection { Path = path, UsagePage = caps.UsagePage, Usage = caps.Usage, InLen = caps.InputReportByteLength, OutLen = caps.OutputReportByteLength }); } } finally { Marshal.FreeHGlobal(buf); } } } finally { SetupDiDestroyDeviceInfoList(set); } return list; } class Open { public HidppCollection C; public SafeFileHandle R, W; public Thread T; } readonly List opens = new List(); public readonly BlockingCollection Inbox = new BlockingCollection(); volatile bool stopping; public HidppReceiver(IEnumerable cols) { foreach (var c in cols) { if (c.UsagePage < 0xFF00) continue; var o = new Open { C = c }; o.R = CreateFile(c.Path, 0x80000000, 3, IntPtr.Zero, 3, 0, IntPtr.Zero); o.W = CreateFile(c.Path, 0x40000000, 3, IntPtr.Zero, 3, 0, IntPtr.Zero); if (o.R.IsInvalid || o.W.IsInvalid) throw new Exception("Could not open " + c.Path + " (error " + Marshal.GetLastWin32Error() + ")"); o.T = new Thread(() => ReadLoop(o)) { IsBackground = true }; o.T.Start(); opens.Add(o); } if (opens.Count == 0) throw new Exception("No vendor (0xFF00) HID++ collections found"); } void ReadLoop(Open o) { var buf = new byte[o.C.InLen]; while (!stopping) { int n; if (!ReadFile(o.R, buf, buf.Length, out n, IntPtr.Zero)) { if (stopping) return; Thread.Sleep(50); continue; } var copy = new byte[n]; Array.Copy(buf, copy, n); Inbox.Add(copy); } } // Writes a report; picks the collection whose output length matches (7 = short 0x10, 20 = long 0x11). public void Send(byte[] report) { int want = report[0] == 0x10 ? 7 : 20; foreach (var o in opens) { if (o.C.OutLen != want) continue; var b = new byte[o.C.OutLen]; Array.Copy(report, b, Math.Min(report.Length, b.Length)); int w; if (!WriteFile(o.W, b, b.Length, out w, IntPtr.Zero)) throw new Exception("WriteFile failed, error " + Marshal.GetLastWin32Error()); return; } throw new Exception("No collection with output length " + want); } public byte[] Next(int timeoutMs) { byte[] r; return Inbox.TryTake(out r, timeoutMs) ? r : null; } public string Describe() { var s = ""; foreach (var o in opens) s += string.Format(" page 0x{0:X4} usage 0x{1:X2} in {2} out {3}\n", o.C.UsagePage, o.C.Usage, o.C.InLen, o.C.OutLen); return s; } public void Dispose() { stopping = true; foreach (var o in opens) { CancelIoEx(o.R, IntPtr.Zero); o.R.Dispose(); o.W.Dispose(); } } public static string Hex(byte[] b) { return b == null ? "(none)" : BitConverter.ToString(b).Replace("-", " "); } }