Accessing USB HID devices as a file stream from C#

This is some example code that shows how to query for attached HID-driver USB devices and open a file handle to the device, allowing you to read and write to the device using a stream. The code below is demonstration quality and could use some cleanup.

Using the code below, you can enumerate all attached HID devices, and then create a FileStream object from the returned file handles.

For instance: IList devices = Enumerator.DiscoverDevices();

// DiscoverDevices opens a handle to all matching devices; close the unused ones.
// Really, the API should be reworked, but the code provided is good enough for demonstration. 
for( int i = 1; i < devices.Count; i++ ) { devices.FileHandle.Close(); } 

// Create a FileStream to the handle we have so we can write to the device.
// Don't forget to close it when you're done!
FileStream stream = new FileStream ( devices[0].FileHandle, FileAccess.ReadWrite );

Follows the code that performs the enumeration and opening:

Enumerator.cs:

public class Enumerator
{
    private static ushort VendorId = 1234;
    private static ushort ProductId = 5678;

    /// <summary>
    /// Returns a list of serial numbers of attached USB HID devices that have 
    //// the given vendor ID and product ID
    /// </summary>
    /// <returns></returns>
    public static IList<string> EnumerateAttachedDevices()
    {
        IList<DiscoveredDevice> devices;
        List<string> serialNumbers = new List<string>();

        devices = DiscoverDevices();

        foreach ( DiscoveredDevice device in devices )
        {
            if ( serialNumbers.Contains( device.SerialNumber ) == false )
            {
                serialNumbers.Add( device.SerialNumber );
            }

            device.FileHandle.Close();
            device.FileHandle.SetHandleAsInvalid();
        }

        return serialNumbers;
    }

    /// <summary>
    /// Reads the serial number from the HID device pointed to by the given file handle.
    /// </summary>
    /// <param name="handle"></param>
    /// <returns></returns>
    private static string GetSerialNum( SafeFileHandle handle )
    {
        StringBuilder serNumBuilder = new StringBuilder( 253 );
        bool success;
        string result;

        success = Win32Hid.HidD_GetSerialNumberString( handle, serNumBuilder, (uint)serNumBuilder.Capacity );

        if ( success )
        {
            result = serNumBuilder.ToString();
        }
        else
        {
            result = null;
        }

        return result;
    }

    /// <summary>
    /// Scans the device list for HID devices, returning open file handles to all
    /// all attached devices.
    /// </summary>
    /// <param name="serialNum"></param>
    /// <remarks>
    /// If serialNum is null and not empty, the search stops at the first matching
    /// device. Callers are responsible for closing the file handles.
    /// </remarks>
    /// <returns></returns>
    public static IList<DiscoveredDevice> DiscoverDevices()
    {
        Guid hidGuid = Guid.Empty;
        IntPtr hDevInfo; // TODO rename to hidClassInfo
        bool success;
        SetupDI.SP_DEVICE_INTERFACE_DATA interfaceData;
        SetupDI.SP_DEVICE_INTERFACE_DETAIL_DATA interfaceDetail;
        SetupDI.SP_DEVINFO_DATA devInfoData;
        uint size;
        uint index = 0;

        List<DiscoveredDevice> resultHandles = new List<DiscoveredDevice>();

        // Get the GUID for the HID device class
        Win32Hid.HidD_GetHidGuid( ref hidGuid );

        // Get a pointer to the device information set for the HID device class
        hDevInfo = SetupDI.SetupDiGetClassDevs(
            ref hidGuid,
            IntPtr.Zero,
            IntPtr.Zero,
            SetupDI.DiGetClassFlags.Present | SetupDI.DiGetClassFlags.DeviceInterface
        );

        index = 0;
        while ( true )
        {
            interfaceData = new SetupDI.SP_DEVICE_INTERFACE_DATA();
            interfaceData.cbSize = (uint)Marshal.SizeOf( interfaceData );

            // Using variable 'index', step through all of the devices in the HID device class.
            // Fills in device interface data.
            success = SetupDI.SetupDiEnumDeviceInterfaces( hDevInfo, IntPtr.Zero, ref hidGuid, index, ref interfaceData );
            index++;

            if ( success == false ) { break; }

            devInfoData = new SetupDI.SP_DEVINFO_DATA();
            devInfoData.cbSize = (uint)Marshal.SizeOf( devInfoData );

            interfaceDetail = new SetupDI.SP_DEVICE_INTERFACE_DETAIL_DATA();
            interfaceDetail.cbSize = IntPtr.Size == 8 ? (uint)8 : (uint)(4 + Marshal.SystemDefaultCharSize);

            // Read out the device instance and path, so that we can open the device as a file.
            // We don't actually need the devInfoData
            success = SetupDI.SetupDiGetDeviceInterfaceDetail(
                hDevInfo,
                ref interfaceData,
                ref interfaceDetail,
                256,
                out size,
                ref devInfoData
            );

            if ( success == false ) { continue; }

            // Open the device as a file so that we can query it with HID and read/write to it.
            SafeFileHandle devHandle = Kernel32.CreateFile(
                interfaceDetail.DevicePath,
                (uint)(Kernel32.AccessRights.GENERIC_READ | Kernel32.AccessRights.GENERIC_WRITE),
                (uint)(Kernel32.ShareModes.FILE_SHARE_READ | Kernel32.ShareModes.FILE_SHARE_WRITE),
                IntPtr.Zero,
                (uint)(Kernel32.CreationDispositions.OPEN_EXISTING),
                Kernel32.Overlapped,
                IntPtr.Zero
            );

            if ( devHandle.IsInvalid )
            {
                continue;
            }

            Win32Hid.HIDD_ATTRIBUTES hidAttribs = new Win32Hid.HIDD_ATTRIBUTES();

            success = Win32Hid.HidD_GetAttributes( devHandle, ref hidAttribs );

            if ( success && hidAttribs.VendorID == VendorId && hidAttribs.ProductID == ProductId )
            {
                DiscoveredDevice discoveredDevice;
                IntPtr preparsedDataPtr = new IntPtr();
                Win32Hid.HIDP_CAPS caps = new Win32Hid.HIDP_CAPS();
                bool goodDevice;

                // Get the device caps. We have to read out the 'preparsed data', feed that to
                // GetCaps, then free the 'preparsed data'.
                Win32Hid.HidD_GetPreparsedData( devHandle, ref preparsedDataPtr );
                Win32Hid.HidP_GetCaps( preparsedDataPtr, ref caps );
                Win32Hid.HidD_FreePreparsedData( ref preparsedDataPtr );

                // If Usage is 1, we found the right instance of the device (there's three of them).
                goodDevice = false;
                if ( caps.Usage == 1 )
                {
                    // Attempt to get the serial number. This could fail if the device was recently attached.
                    string serialNum = GetSerialNum( devHandle );

                    if ( serialNum != null )
                    {
                        goodDevice = true;

                        discoveredDevice = new DiscoveredDevice();
                        discoveredDevice.FileHandle = devHandle;
                        discoveredDevice.HidAttribs = hidAttribs;
                        discoveredDevice.Caps = caps;
                        discoveredDevice.SerialNumber = serialNum;

                        resultHandles.Add( discoveredDevice );
                    }
                }

                if ( goodDevice == false )
                {
                    devHandle.Close();
                    devHandle.SetHandleAsInvalid();
                }
            }
        }

        success = SetupDI.SetupDiDestroyDeviceInfoList( hDevInfo );
        hDevInfo = IntPtr.Zero;

        return resultHandles;
    }

    private class DiscoveredDevice
    {
        public Win32Hid.HIDD_ATTRIBUTES HidAttribs { get; set; }

        public Win32Hid.HIDP_CAPS Caps { get; set; }

        public SafeFileHandle FileHandle { get; set; }

        public string SerialNumber { get; set; }
    }
}

This is support code for accessing the USB HID API and opening device handles.

Kernel32.cs

static public class Kernel32
{
    public const uint Overlapped = 0x40000000;

    [Flags]
    public enum AccessRights : uint
    {
        GENERIC_READ = (0x80000000),
        GENERIC_WRITE = (0x40000000),
        GENERIC_EXECUTE = (0x20000000),
        GENERIC_ALL = (0x10000000)
    }

    [Flags]
    public enum ShareModes : uint
    {
        FILE_SHARE_READ = 0x00000001,
        FILE_SHARE_WRITE = 0x00000002,
        FILE_SHARE_DELETE = 0x00000004
    }

    public enum CreationDispositions
    {
        CREATE_NEW = 1,
        CREATE_ALWAYS = 2,
        OPEN_EXISTING = 3,
        OPEN_ALWAYS = 4,
        TRUNCATE_EXISTING = 5
    }

    [DllImport( "kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode )]
    public static extern SafeFileHandle CreateFile(
        string lpFileName,
        uint dwDesiredAccess,
        uint dwShareMode,
        IntPtr lpSecurityAttributes,
        uint dwCreationDisposition,
        uint dwFlagsAndAttributes,
        IntPtr hTemplateFile
    );


    [DllImport( "kernel32.dll", CharSet = CharSet.Unicode )]
    public static extern void CloseHandle(
        SafeHandle handle
    );

    [DllImport( "kernel32.dll" )]
    [return: MarshalAs( UnmanagedType.Bool )]
    public static extern bool CancelIo( SafeFileHandle hFile );

    public delegate void OverlappedCompletionDelegate( 
        UInt32 dwErrorCode, 
        UInt32 dwNumberOfBytesTransfered, 
        ref NativeOverlapped lpOverlapped 
    );

    [DllImport( "kernel32.dll", SetLastError=true )]
    public static extern bool LockFile( 
        SafeFileHandle hFile, 
        uint fileOffsetLow, 
        uint fileOffsetHigh, 
        uint numberOfBytesToLockLow, 
        uint numberOfBytesToLockHigh 
    );

    [DllImport( "kernel32.dll" )]
    public static extern bool UnlockFile(
        SafeFileHandle hFile, 
        uint fileOffsetLow,
        uint fileOffsetHigh, 
        uint numberOfBytesToUnlockLow,
        uint numberOfBytesToUnlockHigh 
    );

} 

SetupDI.cs

public static class SetupDI
{
    // SetupDiGetClassDevs
    [DllImport( "setupapi.dll", CharSet = CharSet.Auto )]
    public static extern IntPtr SetupDiGetClassDevs(
       ref Guid ClassGuid,
       IntPtr Enumerator,
       IntPtr hwndParent,
       DiGetClassFlags Flags
    );

    // SetupDiGetDeviceInterfaceDetail
    [DllImport( @"setupapi.dll", CharSet = CharSet.Auto, SetLastError = true )]
    public static extern Boolean SetupDiGetDeviceInterfaceDetail(
       IntPtr hDevInfo,
       ref SP_DEVICE_INTERFACE_DATA deviceInterfaceData,
       ref SP_DEVICE_INTERFACE_DETAIL_DATA deviceInterfaceDetailData,
       UInt32 deviceInterfaceDetailDataSize,
       out UInt32 requiredSize,
       ref SP_DEVINFO_DATA deviceInfoData
    );

    // SetupDiEnumDeviceInterfaces
    [DllImport( @"setupapi.dll", CharSet = CharSet.Auto, SetLastError = true )]
    public static extern Boolean SetupDiEnumDeviceInterfaces(
       IntPtr hDevInfo,
       ref SP_DEVINFO_DATA devInfo,
       ref Guid interfaceClassGuid,
       UInt32 memberIndex,
       ref SP_DEVICE_INTERFACE_DATA deviceInterfaceData
    );


    // SetupDiEnumDeviceInterfaces
    [DllImport( @"setupapi.dll", CharSet = CharSet.Auto, SetLastError = true )]
    public static extern Boolean SetupDiEnumDeviceInterfaces(
       IntPtr hDevInfo,
       IntPtr devInfo,
       ref Guid interfaceClassGuid,
       UInt32 memberIndex,
       ref SP_DEVICE_INTERFACE_DATA deviceInterfaceData
    );

    [DllImport( "setupapi.dll", SetLastError = true )]
    public static extern bool SetupDiDestroyDeviceInfoList( IntPtr deviceInfoSet );



    [StructLayout( LayoutKind.Sequential )]
    public struct SP_DEVINFO_DATA
    {
        public UInt32 cbSize;
        public Guid ClassGuid;
        public UInt32 DevInst;
        public IntPtr Reserved;
    }

    // Device interface data
    [StructLayout( LayoutKind.Sequential )]
    public struct SP_DEVICE_INTERFACE_DATA
    {
        public UInt32 cbSize;
        public Guid InterfaceClassGuid;
        public UInt32 Flags;
        public UIntPtr Reserved;
    }

    // Device interface detail data
    [StructLayout( LayoutKind.Sequential, CharSet = CharSet.Auto )]
    public struct SP_DEVICE_INTERFACE_DETAIL_DATA
    {
        public UInt32 cbSize;
        [MarshalAs( UnmanagedType.ByValTStr, SizeConst = 256 )]
        public string DevicePath;
    }


    [Flags]
    public enum DiGetClassFlags : int
    {
        Default = 0x00000001,  // only valid with DeviceInterface
        Present = 0x00000002,
        AllClasses = 0x00000004,
        Profile = 0x00000008,
        DeviceInterface = 0x00000010,
    }

}

Win32Hid.cs

public static class Win32Hid
{
    // HidD_GetHidGuid
    [DllImport( "hid.dll", SetLastError = true )]
    public static extern void HidD_GetHidGuid( ref Guid hidGuid );

    // HidD_GetAttributes
    [DllImport( "hid.dll", SetLastError = true )]
    public static extern Boolean HidD_GetAttributes(
        SafeFileHandle HidDeviceObject,
        ref HIDD_ATTRIBUTES Attributes );

    // HidD_GetSerialNumberString
    [DllImport( "hid.dll", SetLastError = true )]
    public static extern Boolean HidD_GetSerialNumberString(
        SafeFileHandle HidDeviceObject,
        [MarshalAs( UnmanagedType.LPWStr )]
        StringBuilder Buffer,
        UInt32 BufferLength );

    // HidD_GetPreparsedData
    [DllImport( "hid.dll", SetLastError = true )]
    public static extern bool HidD_GetPreparsedData( SafeFileHandle HidDeviceObject, ref IntPtr PreparsedData );

    [DllImport( "hid.dll", SetLastError = true )]
    public static extern bool HidD_FreePreparsedData( ref IntPtr PreparsedData );

    [DllImport( "hid.dll", SetLastError = true )]
    public static extern int HidP_GetCaps( IntPtr preparsedData, ref HIDP_CAPS capabilities );

    [DllImport( "hid.dll", SetLastError = true )]
    public static extern bool HidD_FlushQueue( SafeFileHandle HidDeviceObject );

    public struct HIDD_ATTRIBUTES
    {
        public Int32 Size;
        public Int16 VendorID;
        public Int16 ProductID;
        public Int16 VersionNumber;

    }

    [StructLayout( LayoutKind.Sequential )]
    public struct HIDP_CAPS
    {
        public short Usage;
        public short UsagePage;
        public short InputReportByteLength;
        public short OutputReportByteLength;
        public short FeatureReportByteLength;
        [MarshalAs( UnmanagedType.ByValArray, SizeConst = 17 )]
        public short[] Reserved;
        public short NumberLinkCollectionNodes;
        public short NumberInputButtonCaps;
        public short NumberInputValueCaps;
        public short NumberInputDataIndices;
        public short NumberOutputButtonCaps;
        public short NumberOutputValueCaps;
        public short NumberOutputDataIndices;
        public short NumberFeatureButtonCaps;
        public short NumberFeatureValueCaps;
        public short NumberFeatureDataIndices;

    }

}