Find USB drive letter from VID/PID (Needed for XP and higher)

12,109

Solution 1

I may be mistaken but it seems WMI doesn't know about the parent-child relation that exists in the Windows device setup API.

So, I have created a small Device utility class that can add this missing link from the native Setup API. Here is how you would use it in your original USBDeviceInfo class:

class USBDeviceInfo
{
    public USBDeviceInfo(string deviceID, string pnpDeviceID, string description)
    {
        this.DeviceID = deviceID;
        this.PnpDeviceID = pnpDeviceID;
        this.Description = description;
    }

    public string DeviceID { get; private set; }
    public string PnpDeviceID { get; private set; }
    public string Description { get; private set; }

    public IEnumerable<string> GetDiskNames()
    {
        using (Device device = Device.Get(PnpDeviceID))
        {
            // get children devices
            foreach (string childDeviceId in device.ChildrenPnpDeviceIds)
            {
                // get the drive object that correspond to this id (escape the id)
                foreach (ManagementObject drive in new ManagementObjectSearcher("SELECT DeviceID FROM Win32_DiskDrive WHERE PNPDeviceID='" + childDeviceId.Replace(@"\", @"\\") + "'").Get())
                {
                    // associate physical disks with partitions
                    foreach (ManagementObject partition in new ManagementObjectSearcher("ASSOCIATORS OF {Win32_DiskDrive.DeviceID='" + drive["DeviceID"] + "'} WHERE AssocClass=Win32_DiskDriveToDiskPartition").Get())
                    {
                        // associate partitions with logical disks (drive letter volumes)
                        foreach (ManagementObject disk in new ManagementObjectSearcher("ASSOCIATORS OF {Win32_DiskPartition.DeviceID='" + partition["DeviceID"] + "'} WHERE AssocClass=Win32_LogicalDiskToPartition").Get())
                        {
                            yield return (string)disk["DeviceID"];
                        }
                    }
                }
            }
        }
    }
}

And here is the new Device class:

public sealed class Device : IDisposable
{
    private IntPtr _hDevInfo;
    private SP_DEVINFO_DATA _data;

    private Device(IntPtr hDevInfo, SP_DEVINFO_DATA data)
    {
        _hDevInfo = hDevInfo;
        _data = data;
    }

    public static Device Get(string pnpDeviceId)
    {
        if (pnpDeviceId == null)
            throw new ArgumentNullException("pnpDeviceId");

        IntPtr hDevInfo = SetupDiGetClassDevs(IntPtr.Zero, pnpDeviceId, IntPtr.Zero, DIGCF.DIGCF_ALLCLASSES | DIGCF.DIGCF_DEVICEINTERFACE);
        if (hDevInfo == (IntPtr)INVALID_HANDLE_VALUE)
            throw new Win32Exception(Marshal.GetLastWin32Error());

        SP_DEVINFO_DATA data = new SP_DEVINFO_DATA();
        data.cbSize = Marshal.SizeOf(data);
        if (!SetupDiEnumDeviceInfo(hDevInfo, 0, ref data))
        {
            int err = Marshal.GetLastWin32Error();
            if (err == ERROR_NO_MORE_ITEMS)
                return null;

            throw new Win32Exception(err);
        }

        return new Device(hDevInfo, data) {PnpDeviceId = pnpDeviceId};
    }

    public void Dispose()
    {
        if (_hDevInfo != IntPtr.Zero)
        {
            SetupDiDestroyDeviceInfoList(_hDevInfo);
            _hDevInfo = IntPtr.Zero;
        }
    }

    public string PnpDeviceId { get; private set; }

    public string ParentPnpDeviceId
    {
        get
        {
            if (IsVistaOrHiger)
                return GetStringProperty(DEVPROPKEY.DEVPKEY_Device_Parent);

            uint parent;
            int cr = CM_Get_Parent(out parent, _data.DevInst, 0);
            if (cr != 0)
                throw new Exception("CM Error:" + cr);

            return GetDeviceId(parent);
        }
    }

    private static string GetDeviceId(uint inst)
    {
        IntPtr buffer = Marshal.AllocHGlobal(MAX_DEVICE_ID_LEN + 1);
        int cr = CM_Get_Device_ID(inst, buffer, MAX_DEVICE_ID_LEN + 1, 0);
        if (cr != 0)
            throw new Exception("CM Error:" + cr);

        try
        {
            return Marshal.PtrToStringAnsi(buffer);
        }
        finally
        {
            Marshal.FreeHGlobal(buffer);
        }
    }

    public string[] ChildrenPnpDeviceIds
    {
        get
        {
            if (IsVistaOrHiger)
                return GetStringListProperty(DEVPROPKEY.DEVPKEY_Device_Children);

            uint child;
            int cr = CM_Get_Child(out child, _data.DevInst, 0);
            if (cr != 0)
                return new string[0];

            List<string> ids = new List<string>();
            ids.Add(GetDeviceId(child));
            do
            {
                cr = CM_Get_Sibling(out child, child, 0);
                if (cr != 0)
                    return ids.ToArray();

                ids.Add(GetDeviceId(child));
            }
            while (true);
        }
    }

    private static bool IsVistaOrHiger
    {
        get
        {
            return (Environment.OSVersion.Platform == PlatformID.Win32NT && Environment.OSVersion.Version.CompareTo(new Version(6, 0)) >= 0);
        }
    }

    private const int INVALID_HANDLE_VALUE = -1;
    private const int ERROR_NO_MORE_ITEMS = 259;
    private const int MAX_DEVICE_ID_LEN = 200;

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

    [Flags]
    private enum DIGCF : uint
    {
        DIGCF_DEFAULT = 0x00000001,
        DIGCF_PRESENT = 0x00000002,
        DIGCF_ALLCLASSES = 0x00000004,
        DIGCF_PROFILE = 0x00000008,
        DIGCF_DEVICEINTERFACE = 0x00000010,
    }

    [DllImport("setupapi.dll", SetLastError = true)]
    private static extern bool SetupDiEnumDeviceInfo(IntPtr DeviceInfoSet, uint MemberIndex, ref SP_DEVINFO_DATA DeviceInfoData);

    [DllImport("setupapi.dll", CharSet = CharSet.Unicode, SetLastError = true)]
    private static extern IntPtr SetupDiGetClassDevs(IntPtr ClassGuid, string Enumerator, IntPtr hwndParent, DIGCF Flags);

    [DllImport("setupapi.dll")]
    private static extern int CM_Get_Parent(out uint pdnDevInst, uint dnDevInst, uint ulFlags);

    [DllImport("setupapi.dll")]
    private static extern int CM_Get_Device_ID(uint dnDevInst, IntPtr Buffer, int BufferLen, uint ulFlags);

    [DllImport("setupapi.dll")]
    private static extern int CM_Get_Child(out uint pdnDevInst, uint dnDevInst, uint ulFlags);

    [DllImport("setupapi.dll")]
    private static extern int CM_Get_Sibling(out uint pdnDevInst, uint dnDevInst, uint ulFlags);

    [DllImport("setupapi.dll")]
    private static extern bool SetupDiDestroyDeviceInfoList(IntPtr DeviceInfoSet);

    // vista and higher
    [DllImport("setupapi.dll", SetLastError = true, EntryPoint = "SetupDiGetDevicePropertyW")]
    private static extern bool SetupDiGetDeviceProperty(IntPtr DeviceInfoSet, ref SP_DEVINFO_DATA DeviceInfoData, ref DEVPROPKEY propertyKey, out int propertyType, IntPtr propertyBuffer, int propertyBufferSize, out int requiredSize, int flags);

    [StructLayout(LayoutKind.Sequential)]
    private struct DEVPROPKEY
    {
        public Guid fmtid;
        public uint pid;

        // from devpkey.h
        public static readonly DEVPROPKEY DEVPKEY_Device_Parent = new DEVPROPKEY { fmtid = new Guid("{4340A6C5-93FA-4706-972C-7B648008A5A7}"), pid = 8 };
        public static readonly DEVPROPKEY DEVPKEY_Device_Children = new DEVPROPKEY { fmtid = new Guid("{4340A6C5-93FA-4706-972C-7B648008A5A7}"), pid = 9 };
    }

    private string[] GetStringListProperty(DEVPROPKEY key)
    {
        int type;
        int size;
        SetupDiGetDeviceProperty(_hDevInfo, ref _data, ref key, out type, IntPtr.Zero, 0, out size, 0);
        if (size == 0)
            return new string[0];

        IntPtr buffer = Marshal.AllocHGlobal(size);
        try
        {
            if (!SetupDiGetDeviceProperty(_hDevInfo, ref _data, ref key, out type, buffer, size, out size, 0))
                throw new Win32Exception(Marshal.GetLastWin32Error());

            List<string> strings = new List<string>();
            IntPtr current = buffer;
            do
            {
                string s = Marshal.PtrToStringUni(current);
                if (string.IsNullOrEmpty(s))
                    break;

                strings.Add(s);
                current += (1 + s.Length) * 2;
            }
            while (true);
            return strings.ToArray();
        }
        finally
        {
            Marshal.FreeHGlobal(buffer);
        }
    }

    private string GetStringProperty(DEVPROPKEY key)
    {
        int type;
        int size;
        SetupDiGetDeviceProperty(_hDevInfo, ref _data, ref key, out type, IntPtr.Zero, 0, out size, 0);
        if (size == 0)
            return null;

        IntPtr buffer = Marshal.AllocHGlobal(size);
        try
        {
            if (!SetupDiGetDeviceProperty(_hDevInfo, ref _data, ref key, out type, buffer, size, out size, 0))
                throw new Win32Exception(Marshal.GetLastWin32Error());

            return Marshal.PtrToStringUni(buffer);
        }
        finally
        {
            Marshal.FreeHGlobal(buffer);
        }
    }
}

Solution 2

I've had the same problem and also browsing through the WMI did not help me out, really.

But I ended up with these few lines of code, and it works great for me:

private string GetAvailableStorageDrive()
{
    foreach (var info in System.IO.DriveInfo.GetDrives())
    {
        if (info.DriveType == System.IO.DriveType.Removable && 
             info.IsReady && 
            !info.Name.Equals("A:\\"))
        {
            return info.Name;
        }
    }
    return string.Empty;
}

Basically the above function looks if the DriveType is Removable andalso if the Drive is ready. I also exclude the drive letter 'A' because in default windows environments, this is the Floppy.

Description of DriveType.Removable:

The drive is a removable storage device, such as a floppy disk drive or a USB flash drive.

Note: As CodeCaster pointed out, this function will also return removable storage devices such as SATA. So if that's the case, you will have to look into more complex solutions as provided by others.

Share:
12,109

Related videos on Youtube

tmwoods
Author by

tmwoods

Code writer (a bit of everything, mostly .NET), server admin (Linux/Windows), database (a bit of everything, mostly SQL-based). Spend most of my time herding cats and trying to get them to document their code.

Updated on September 15, 2022

Comments

  • tmwoods
    tmwoods over 1 year

    So I thought I would include the final answer here so you don't have to make sense of this post. Big thanks to Simon Mourier for taking the time to figure this one out.

    MY WORKING CODE

            try
            {
                //Get a list of available devices attached to the USB hub
                List<string> disks = new List<string>();
                var usbDevices = GetUSBDevices();
    
                //Enumerate the USB devices to see if any have specific VID/PID
                foreach (var usbDevice in usbDevices)
                {
                    if (usbDevice.DeviceID.Contains(USB_PID) && usbDevice.DeviceID.Contains(USB_VID))
                    {
                        foreach (string name in usbDevice.GetDiskNames())
                        {
                            //Open dialog to show file names
                            textbox1.Text = name.ToString();
                        }
                    }                   
                }
    

    So just use GetUSBDevices from my original question and then include the two classes shown by Simon Mourier's answer and it should be good to go!


    ORIGINAL QUESTION

    I know this question has been asked before (see here) but none of them have a confirmed answer and I've tried all of the suggested answers. Unfortunately those threads are long dead and I was hoping someone could give a better answer here.

    So far I have two 'starting points', each of which I'll show below.


    OPTION 1: (gets VID/PID but not drive letter)

    I have an embedded device which I connect to through an application. I have code which succesfully scans any USB devices and checks the VID/PID. I successfully detect my device but I have no idea how to get the drive letter. Can someone help me out? I feel like I could add another line in the class but when I go through Device Manager I can't find any property there that describes the drive letter.

    Thanks!

    I'll include my code so far below.

    private void tsDownload_Click(object sender, EventArgs e)
        {
            var usbDevices = GetUSBDevices();
    
            foreach (var usbDevice in usbDevices)
            {
                if (usbDevice.DeviceID.Contains(USB_PID) && usbDevice.DeviceID.Contains(USB_VID))
                {                    
                    //Find drive letter here
                }
            }
        }
    

    where those functions are:

     static List<USBDeviceInfo> GetUSBDevices()
        {
          List<USBDeviceInfo> devices = new List<USBDeviceInfo>();
    
          ManagementObjectCollection collection;
          using (var searcher = new ManagementObjectSearcher(@"Select * From Win32_USBHub"))
            collection = searcher.Get();      
    
          foreach (var device in collection)
          {
            devices.Add(new USBDeviceInfo(
            (string)device.GetPropertyValue("DeviceID"),
            (string)device.GetPropertyValue("PNPDeviceID"),
            (string)device.GetPropertyValue("Description")            
            ));
          }
    
          collection.Dispose();
          return devices;
        }      
    

    and the class is:

    class USBDeviceInfo
    {
        public USBDeviceInfo(string deviceID, string pnpDeviceID, string description)
        {
          this.DeviceID = deviceID;
          this.PnpDeviceID = pnpDeviceID;
          this.Description = description;
        }
        public string DeviceID { get; private set; }
        public string PnpDeviceID { get; private set; }
        public string Description { get; private set; }
    }
    

    OPTION 2: (get drive letter but not VID/PID)

    foreach (ManagementObject drive in new ManagementObjectSearcher("select * from Win32_DiskDrive where InterfaceType='USB'").Get())
                {
                    foreach(ManagementObject partition in new ManagementObjectSearcher("ASSOCIATORS OF {Win32_DiskDrive.DeviceID='" + drive["DeviceID"] + "'} WHERE AssocClass = Win32_DiskDriveToDiskPartition").Get())
                    {
                        foreach (ManagementObject disk in new ManagementObjectSearcher("ASSOCIATORS OF {Win32_DiskPartition.DeviceID='" + partition["DeviceID"] + "'} WHERE AssocClass = Win32_LogicalDiskToPartition").Get())
                        {
                            textBox1.Text = disk["Name"].ToString();
    
                        }
                    }
                }
    

    I'm going to guess the VID/PID is in one of the disk object properties but I just can't find which one.

    • Geeky Guy
      Geeky Guy
      +1 for the research effort. I hope you get the answer you're looking for. I'm curious about it too.
  • CodeCaster
    CodeCaster almost 11 years
    I know of certain card readers and internal SATA disks (or their drivers) that also report themselves as being a removable disk. Those are usually not the type of removable USB mass storage devices OP seems to be looking for.
  • Fabian Bigler
    Fabian Bigler almost 11 years
    @CodeCaster Good point. In my environment this makes perfectly sense because I'm getting the storagedevice-letter. If the OP is happy with this, it's much easier than other approaches (like Simon Mourier's Answer).
  • Fabian Bigler
    Fabian Bigler almost 11 years
    @CodeCaster It also depends what the OP intends to do, and if he himself got SATA disks in his environment. Maybe he's lucky. :)
  • tmwoods
    tmwoods almost 11 years
    Sorry, this isn't really what I'm looking for. I am a custom electronics developer and I need to detect my particular device among potentially many. I need more than just the drive letters; I need find out which drive letters relate to specific VID/PID.
  • Fabian Bigler
    Fabian Bigler almost 11 years
    @tmwoods Ah, that's a pity. I already hoped this would do the trick for you. Then Simon Mourier's Answer will be what you're looking for.
  • tmwoods
    tmwoods almost 11 years
    I haven't tried this yet but the Vista or higher thing is a bit of an issue. This is for a BMS which will be used in industrial environments, many of which run XP. I will take a look-see though. Thanks a lot; this looks very thorough :)
  • Simon Mourier
    Simon Mourier almost 11 years
    I have updated the Device class code to support Windows XP, but not tested.
  • tmwoods
    tmwoods almost 11 years
    Awesome! I should have time to start testing this within the next few days. Thanks a lot!
  • tmwoods
    tmwoods almost 11 years
    So I had a chance to start using this but I'm still a little lost as to what to do next (I'm still very much a beginner). How do I use the GetDiskNames function? I can call it but I don't know which type of variable should receive the return (or if that is even what I am supposed to be doing).
  • Simon Mourier
    Simon Mourier almost 11 years
    Just do a foreach(string name in usbDeviceInfo.GetDiskNames()) Console.WriteLine(name) for example.
  • tmwoods
    tmwoods almost 11 years
    You're a boss. Thank you so much! Merci!
  • Sredni
    Sredni over 10 years
    I just tried this code but it does not seem to be working. The PNPDeviceID returned by the Query on Win32_DiskDrive and that returned by the "Device" class are different. For example in my case the Query returns something like - PNPDeviceID: USBSTOR\DISK&VEN_ABCD&PROD_1234&REV_0001\8&2C3C9390&0 while the Device class returns the actual VID/PID combination --> USB\\VID_4568&PID_QWER&MI_00\\7&15b8d7f0&3&0000, Any idea on how to resolve this, my device is a USB Mass Storage Removable device.