Introduction
Most flash-based USB disk devices have a unique serial number assigned by the manufacturer. (However, some earlier v1.1-based USB devices may not support a serial number)
There are generally two different methods for getting the serial number of a USB-based device... an "easy" way using Windows Management Instrumentation (WMI) and a "hard" way using the Win32 APIs. There are advantages and disadvantages for both methods... one is slow but simple to implement, the other is fast (and potentially provides more information) but is difficult to implement.
Method 1: Using Windows Management Instrumentation (WMI)
This might be a good time to review a companion article on Introduction to Windows Management Instrumentation.
The WMI technique uses a series of "relationships" that exist between several WMI classes. We start with the Win32_LogicalDisk class, track it to the Win32_DiskPartition class (which itself is just a relationship class), and then track that to the Win32_DiskDrive class. Note: Although it is generally accepted as true, there is no documentation to guarantee that the PnPDeviceID will continue to contain the Serial Number.
Public Function GetSerialNumber(ByVal DriveLetter As String) As String Dim wmi_ld, wmi_dp, wmi_dd As ManagementObject Dim temp, parts(), ans As String ans = "" ' get the Logical Disk for that drive letter wmi_ld = New ManagementObject("Win32_LogicalDisk.DeviceID='" & _ DriveLetter.TrimEnd("\"c) & "'") ' get the associated DiskPartition For Each wmi_dp In wmi_ld.GetRelated("Win32_DiskPartition") ' get the associated DiskDrive For Each wmi_dd In wmi_dp.GetRelated("Win32_DiskDrive") ' the serial number is embedded in the PnPDeviceID temp = wmi_dd("PnPDeviceID").ToString If Not temp.StartsWith("USBSTOR") Then Throw New ApplicationException(DriveLetter & " doesn't appear to be USB Device") End If parts = temp.Split("\&".ToCharArray) ' The serial number should be the next to the last element ans = parts(parts.Length - 2) Next Next Return ans End Function
Note: There is a bug in the Windows Vista "provider" for the Win32_Disk class... and the above technique won't work on Vista. So, use the following as a work around:
Public Function GetSerialNumber(ByVal DriveLetter As String) As String Dim wmi_ld, wmi_dp, wmi_dd As ManagementObject Dim temp, parts(), ans As String ans = "" ' get the Logical Disk for that drive letter wmi_ld = New ManagementObject("Win32_LogicalDisk.DeviceID='" & _ DriveLetter.TrimEnd("\"c) & "'") ' get the associated DiskPartition For Each wmi_dp In wmi_ld.GetRelated("Win32_DiskPartition") ' get the associated DiskDrive For Each wmi_dd In wmi_dp.GetRelated("Win32_DiskDrive") ' There is a bug in WinVista that corrupts some of the fields ' of the Win32_DiskDrive class if you instantiate the class via ' its primary key (as in the example above) and the device is ' a USB disk. Oh well... so we have go thru this extra step Dim wmi As New ManagementClass("Win32_DiskDrive") ' loop thru all of the instances. This is silly, we shouldn't ' have to loop thru them all, when we know which one we want. For Each obj As ManagementObject In wmi.GetInstances ' do the DeviceID fields match? If obj("DeviceID").ToString = wmi_dd("DeviceID").ToString Then ' the serial number is embedded in the PnPDeviceID temp = obj("PnPDeviceID").ToString If Not temp.StartsWith("USBSTOR") Then Throw New ApplicationException(DriveLetter & _ " doesn't appear to be USB Device") End If parts = temp.Split("\&".ToCharArray) ' The serial number should be the next to the last element ans = parts(parts.Length - 2) End If Next Next Next Return ans End Function
I think you need to look at the Win32 API method to truly appreciate the simplicity of the WMI method.
Method 2: Using the Win32 APIs
OK, now let's do it the "hard" way. The API method uses functions from SetupAPI.DLL for listing devices and getting device parameters. It also uses the normal DeviceIoControl API function from Kernel.DLL to talk to the hardware devices. I'll admit, the technique is a bit convoluted, so let's break it down into steps:
- Step 1: Find the correct disk device in the "device tree" by searching for a unique device number
- Step 2: Get the InstanceID of the device and "walk the device tree" upwards to get the DevicePath of the Hub
- Step 3: Loop thru each of the ports on the Hub to find the matching InstanceID
- Step 4: Get the iSerialNumber "index" from the DeviceDescriptor
- Step 5: Get the string value for StringDescriptor
Note: I have not included the API Consts, Enums, Structures, and Declares in this web article (because they took up too much room). However, they are in the downloadable sample code (see the link at the bottom of the article).
This top-level routine follows the steps outlined above. The FindDiskDevice() function searches the device tree for DeviceNumber that matches that of the drive letter. It returns (via it passed by reference parameters) the full HubDevicePath for the USB Hub and the unique InstanceID of the drive. The GetPortCount() function merely returns the number of USB ports on the Hub. Next it uses the GetDriverKeyName() and FindInstanceIDByKeyName() functions to find the correct port number on the Hub. Next, the GetDeviceDescriptor() method returns the DeviceDescriptor so we know the "index" of the Serial Number string. And lastly, the GetStringDescriptor() function returns the actual serial number string.
Function GetSerialNumber(ByVal DriveLetter As String) As String Dim SerialNumber As String = "" Dim InstanceID As String = "" Dim HubDevicePath As String = "" Dim HubPortCount As Integer ' HubDevicePath and InstanceID are passed by reference If Not FindDiskDevice(DriveLetter, HubDevicePath, InstanceID) Then Throw New ApplicationException("Can't find the device instance") End If If Not InstanceID.StartsWith("USB\") Then Throw New ApplicationException("This drive doesn't appear to be a USB device") End If ' how many ports are there? HubPortCount = GetPortCount(HubDevicePath) If HubPortCount = 0 Then Throw New ApplicationException("Can't get the number of ports on the hub") End If ' loop thru all of the ports hunting for a matching InstanceID ' BTW: Port numbers start at 1 For i As Integer = 1 To HubPortCount ' does the device match the InstanceID we're looking for? If FindInstanceIDByKeyName(GetDriverKeyName(HubDevicePath, i)) = InstanceID Then ' get the "index" for the serial number Dim DeviceDescriptor As USB_DEVICE_DESCRIPTOR = _ GetDeviceDescriptor(HubDevicePath, i) ' a iSerialNumber of 0 means there is no serial number If DeviceDescriptor.iSerialNumber > 0 Then SerialNumber = GetStringDescriptor(HubDevicePath, i, _ DeviceDescriptor.iSerialNumber) Exit For End If End If Next Return SerialNumber End Function
Every "Storage Device" is assigned a unique number based upon its device type. We use this feature to allow us to see if two different device paths (such as "\\.\E:" and "\\\\?\\usbstor#disk&ven_lexar&prod_jd_lightning&rev_3000#33000001928000002345&0#{53f56307-b6bf-11d0-94f2-00a0c91efb8b}") are actually pointing to the same device. Since the STORAGE_DEVICE_NUMBER.DeviceNumber field is only unique with its STORAGE_DEVICE_NUMBER.DeviceType, we "fold" the two numbers together.
Private Function GetDeviceNumber(ByVal DevicePath As String) As Integer Dim ans As Integer = -1 Dim h As IntPtr = CreateFile(DevicePath.TrimEnd("\"c), 0, 0, Nothing, _ OPEN_EXISTING, 0, Nothing) If h.ToInt32 <> INVALID_HANDLE_VALUE Then Dim reqSize As Integer = 0 Dim Sdn As New STORAGE_DEVICE_NUMBER Dim nBytes As Integer = Marshal.SizeOf(Sdn) Dim ptrSdn As IntPtr = Marshal.AllocHGlobal(nBytes) If DeviceIoControl(h, IOCTL_STORAGE_GET_DEVICE_NUMBER, IntPtr.Zero, 0, _ ptrSdn, Marshal.SizeOf(Sdn), reqSize, Nothing) Then Sdn = CType(Marshal.PtrToStructure(ptrSdn, GetType( _ STORAGE_DEVICE_NUMBER)), STORAGE_DEVICE_NUMBER) ' just my way of combining the relevant parts of the ' STORAGE_DEVICE_NUMBER into a single number ans = (Sdn.DeviceType << 8) + Sdn.DeviceNumber End If Marshal.FreeHGlobal(ptrSdn) CloseHandle(h) End If Return ans End Function
Next, we need to find the full "symbolic name" of the USB Hub where are disk is located (an average PC might have 2-3 internal USB hubs), and we also need the "Instance ID" of the device itself (sorta like a device driver name). We start by getting the DeviceNumber of the drive letter assigned to the USB disk. Next, we search the entire "device tree" for a device that matches that device number. After we found a match, we need to "walk the device tree" upwards to get path to the USB Hub. These two strings are passed "by reference", so that we can make changes to them inside this function.
Private Function FindDiskDevice(ByVal DriveLetter As String, ByRef HubDevicePath As _ String, ByRef InstanceID As String) As Boolean Dim ans As Boolean = False ' Get the DeviceNumber using the drive letter (without a trailing ' backslash, i.e. "C:"). We'll use this later to match the DeviceNumber ' of each of the device's Symbolic Name Dim DeviceNumber As Integer = GetDeviceNumber("\\.\" & DriveLetter.TrimEnd("\"c)) If DeviceNumber < 0 Then Return ans End If Dim DiskGUID As New Guid(GUID_DEVINTERFACE_DISK) ' We start at the "root" of the device tree and look for all ' devices that match the interface GUID of a disk Dim hSetup As IntPtr = SetupDiGetClassDevs(DiskGUID, 0, IntPtr.Zero, _ DIGCF_PRESENT Or DIGCF_DEVICEINTERFACE) If hSetup.ToInt32 <> INVALID_HANDLE_VALUE Then Dim Success As Boolean Dim i As Integer = 0 do ' create a Device Interface Data structure Dim dia As New SP_DEVICE_INTERFACE_DATA dia.cbSize = Marshal.SizeOf(dia) ' start the enumeration Success = SetupDiEnumDeviceInterfaces(hSetup, IntPtr.Zero, DiskGUID, i, dia) If Success Then ' prepare a Devinfo Data structure Dim da As New SP_DEVINFO_DATA da.cbSize = Marshal.SizeOf(da) ' prepare a Device Interface Detail Data structure Dim didd As New SP_DEVICE_INTERFACE_DETAIL_DATA didd.cbSize = 4 + Marshal.SystemDefaultCharSize ' trust me :) ' now we can get some more detailed information Dim nBytes As Integer = BUFFER_SIZE Dim nRequiredSize As Integer = 0 If SetupDiGetDeviceInterfaceDetail(hSetup, dia, didd, nBytes, nRequiredSize, da) Then ' OK, let's get the Device Number again... this time using ' the device's "Symbolic Name". If the numbers match, we've ' found the one we're looking for. If GetDeviceNumber(didd.DevicePath) = DeviceNumber Then ' This should get us to the USBSTOR "level" Dim ptrPrevious As IntPtr CM_Get_Parent(ptrPrevious, da.DevInst, 0) ' Get the InstanceID Dim ptrBuf As IntPtr = Marshal.AllocHGlobal(nBytes) CM_Get_Device_ID(ptrPrevious, ptrBuf, nBytes, 0) InstanceID = Marshal.PtrToStringAuto(ptrBuf) ' This should get us to the USB "level" (the USB hub) CM_Get_Parent(ptrPrevious, ptrPrevious, 0) ' Now get the ID of the hub CM_Get_Device_ID(ptrPrevious, ptrBuf, nBytes, 0) Dim temp As String = Marshal.PtrToStringAuto(ptrBuf) Marshal.FreeHGlobal(ptrBuf) ' Build the final string that represents the full "Symbolic Name" ' of the USB Hub using its ID HubDevicePath = "\\.\" & temp.Replace("\", "#") & "#{" & _ GUID_DEVINTERFACE_USB_HUB & "}" ans = True Exit Do End If End If End If i += 1 Loop While Success End If SetupDiDestroyDeviceInfoList(hSetup) Return ans End Function
Next we need to get the "Driver Key Name" of a device, given the full path to the USB Hub and the port number on the Hub. The Driver Key Name isn't used directly by this application, instead it's used as an intermediate value to enable us to get the device's Instance ID.
In USB programming, you rarely talk to the USB device directly... you talk to the Hub and ask the Hub to intercede on your behalf. You must know the port number to where the device is located in order to get any meaningful data from the USB Hub. If you don't know the port number, you're forced to try them all to find the one you want.
Private Function GetDriverKeyName(ByVal HubPath As String, ByVal PortNumber _ As Integer) As String Dim ans As String = "" ' open a handle to the USB hub Dim hHub As IntPtr = CreateFile(HubPath, GENERIC_WRITE, FILE_SHARE_WRITE, _ IntPtr.Zero, OPEN_EXISTING, 0, IntPtr.Zero) If hHub.ToInt32 <> INVALID_HANDLE_VALUE Then Dim nBytesReturned As Integer Dim nBytes As Integer = Marshal.SizeOf(GetType( _ USB_NODE_CONNECTION_INFORMATION_EX)) Dim ptrNodeConnection As IntPtr = Marshal.AllocHGlobal(nBytes) Dim NodeConnection As New USB_NODE_CONNECTION_INFORMATION_EX NodeConnection.ConnectionIndex = PortNumber Marshal.StructureToPtr(NodeConnection, ptrNodeConnection, True) ' let's check to see if there is something plugged in first If DeviceIoControl(hHub, IOCTL_USB_GET_NODE_CONNECTION_INFORMATION_EX, _ ptrNodeConnection, nBytes, ptrNodeConnection, nBytes, nBytesReturned, _ IntPtr.Zero) Then NodeConnection = CType(Marshal.PtrToStructure(ptrNodeConnection, _ GetType(USB_NODE_CONNECTION_INFORMATION_EX)), _ USB_NODE_CONNECTION_INFORMATION_EX) If NodeConnection.ConnectionStatus = _ USB_CONNECTION_STATUS.DeviceConnected Then ' now let's get the Driver Key Name Dim DriverKey As New USB_NODE_CONNECTION_DRIVERKEY_NAME DriverKey.ConnectionIndex = PortNumber nBytes = Marshal.SizeOf(DriverKey) Dim ptrDriverKey As IntPtr = Marshal.AllocHGlobal(nBytes) Marshal.StructureToPtr(DriverKey, ptrDriverKey, True) 'Use an IOCTL call to request the Driver Key Name If DeviceIoControl(hHub, IOCTL_USB_GET_NODE_CONNECTION_DRIVERKEY_NAME, _ ptrDriverKey, nBytes, ptrDriverKey, nBytes, nBytesReturned, IntPtr.Zero) Then DriverKey = CType(Marshal.PtrToStructure(ptrDriverKey, GetType( _ USB_NODE_CONNECTION_DRIVERKEY_NAME)), _ USB_NODE_CONNECTION_DRIVERKEY_NAME) ans = DriverKey.DriverKeyName End If Marshal.FreeHGlobal(ptrDriverKey) End If End If ' Clean up and go home Marshal.FreeHGlobal(ptrNodeConnection) CloseHandle(hHub) End If Return ans End Function
This next section of code returns the number of ports on a given hub.
Private Function GetPortCount(ByVal HubDevicePath As String) As Integer Dim ans As Integer = 0 ' open a connection to the HubDevice (that we just found) Dim hHub As IntPtr = CreateFile(HubDevicePath, GENERIC_WRITE, _ FILE_SHARE_WRITE, IntPtr.Zero, OPEN_EXISTING, 0, IntPtr.Zero) If hHub.ToInt32 <> INVALID_HANDLE_VALUE Then Dim nBytesReturned As Integer = 0 Dim NodeInfo As New USB_NODE_INFORMATION NodeInfo.NodeType = USB_HUB_NODE.UsbHub Dim nBytes As Integer = Marshal.SizeOf(NodeInfo) Dim ptrNodeInfo As IntPtr = Marshal.AllocHGlobal(nBytes) Marshal.StructureToPtr(NodeInfo, ptrNodeInfo, True) ' get the number of ports on the hub If DeviceIoControl(hHub, IOCTL_USB_GET_NODE_INFORMATION, ptrNodeInfo, _ nBytes, ptrNodeInfo, nBytes, nBytesReturned, IntPtr.Zero) Then NodeInfo = CType(Marshal.PtrToStructure(ptrNodeInfo, GetType( _ USB_NODE_INFORMATION)), USB_NODE_INFORMATION) ans = NodeInfo.HubInformation.HubDescriptor.bNumberOfPorts End If Marshal.FreeHGlobal(ptrNodeInfo) CloseHandle(hHub) End If Return ans End Function
We need to be able to compare two USB Instance IDs to see if they point to the same device. This is completely analogous to the technique we used to compare storage device numbers. There is no straight-forward technique for a converting a USB "Driver Key Name" into a "Instance ID". So, we're forced to use the SetupAPI again to examine each device in the device tree for a matching DriverKeyName, and when found, we use SetupDiGetDeviceInstanceId() to return the associated InstanceID.
Private Function FindInstanceIDByKeyName(ByVal DriverKeyName As String) As String Dim ans As String = "" Dim DevEnum As String = REGSTR_KEY_USB ' Use the "enumerator form" of the SetupDiGetClassDevs API ' to generate a list of all USB devices Dim h As IntPtr = SetupDiGetClassDevs(0, DevEnum, IntPtr.Zero, _ DIGCF_PRESENT Or DIGCF_ALLCLASSES) If h.ToInt32 <> INVALID_HANDLE_VALUE Then Dim ptrBuf As IntPtr = Marshal.AllocHGlobal(BUFFER_SIZE) Dim KeyName As String Dim Success As Boolean Dim i As Integer = 0 Do ' create a Device Interface Data structure Dim da As New SP_DEVINFO_DATA da.cbSize = Marshal.SizeOf(da) ' start the enumeration Success = SetupDiEnumDeviceInfo(h, i, da) If Success Then Dim RequiredSize As Integer = 0 Dim RegType As Integer = REG_SZ KeyName = "" ' get the driver key name If SetupDiGetDeviceRegistryProperty(h, da, SPDRP_DRIVER, RegType, ptrBuf, _ BUFFER_SIZE, RequiredSize) Then KeyName = Marshal.PtrToStringAuto(ptrBuf) End If ' do we have a match? If KeyName = DriverKeyName Then Dim nBytes As Integer = BUFFER_SIZE Dim sb As New StringBuilder(nBytes) SetupDiGetDeviceInstanceId(h, da, sb, nBytes, RequiredSize) ans = sb.ToString() Exit Do End If End If i += 1 Loop While Success CloseHandle(ptrBuf) SetupDiDestroyDeviceInfoList(h) End If Return ans End Function
Each USB Device has a "Device Descriptor" which contains (among other things) an "index" for its string values. The technique to retrieve the DeviceDescriptor requires a "request packet" which carries the USB_DEVICE_DESCRIPTOR structure as a payload. This requires some spooky pointer magic (and careful memory allocation).
Private Function GetDeviceDescriptor(ByVal HubPath As String, ByVal PortNumber As _
Integer) As USB_DEVICE_DESCRIPTOR
Dim ans As New USB_DEVICE_DESCRIPTOR
Dim hHub, ptrDescReq, ptrDevDesc As IntPtr
Dim DescReq As New USB_DESCRIPTOR_REQUEST
Dim DevDesc As New USB_DEVICE_DESCRIPTOR
Dim nBytesReturned, nBytes As Integer
' open a handle to the USB hub
hHub = CreateFile(HubPath, GENERIC_WRITE, FILE_SHARE_WRITE, IntPtr.Zero, _
OPEN_EXISTING, 0, IntPtr.Zero)
If hHub.ToInt32 <> INVALID_HANDLE_VALUE Then
nBytes = BUFFER_SIZE
' build a "request" packet for a Device Descriptor
DescReq = New USB_DESCRIPTOR_REQUEST
DescReq.ConnectionIndex = PortNumber ' the "port number" on the hub
DescReq.SetupPacket.wValue = USB_DEVICE_DESCRIPTOR_TYPE << 8
DescReq.SetupPacket.wLength = CShort(nBytes - Marshal.SizeOf(DescReq))
ptrDescReq = Marshal.AllocHGlobal(BUFFER_SIZE)
Marshal.StructureToPtr(DescReq, ptrDescReq, True)
' Use an IOCTL call to request the Device Descriptor
If DeviceIoControl(hHub, IOCTL_USB_GET_DESCRIPTOR_FROM_NODE_CONNECTION, _
ptrDescReq, nBytes, ptrDescReq, nBytes, nBytesReturned, IntPtr.Zero) Then
' the pointer to the Device Descriptor is just "off the edge" of
' the descriptor request packet.
ptrDevDesc = New IntPtr(ptrDescReq.ToInt32 + Marshal.SizeOf(DescReq))
ans = CType(Marshal.PtrToStructure(ptrDevDesc, GetType( _
USB_DEVICE_DESCRIPTOR)), USB_DEVICE_DESCRIPTOR)
End If
' Clean up and go home
Marshal.FreeHGlobal(ptrDescReq)
CloseHandle(hHub)
End If
Return ans
End Function
The Device Descriptor only contains the "index" for the string values, so we need to retrieve the String Descriptor to get the the value for the Serial Number string. We use the same technique above to create a "request packet" with a USB_STRING_DESCRIPTOR payload. However, this time the allocation of memory needs to be zero-filled (otherwise we'd get garbage at the end of the string). VB.Net doesn't have a direct technique for performing a zero-fill operation, so we use a bit of a hack.
Private Function GetStringDescriptor(ByVal HubPath As String, ByVal PortNumber As _
Integer, ByVal Index As Integer) As String
Dim ans As String = ""
Dim hHub, ptrDescReq, ptrStrDesc As IntPtr
Dim DescReq As New USB_DESCRIPTOR_REQUEST
Dim StrDesc As New USB_STRING_DESCRIPTOR
Dim nBytesReturned, nBytes As Integer
' open a handle to the USB hub
hHub = CreateFile(HubPath, GENERIC_WRITE, FILE_SHARE_WRITE, IntPtr.Zero, _
OPEN_EXISTING, 0, IntPtr.Zero)
If hHub.ToInt32 <> INVALID_HANDLE_VALUE Then
nBytes = BUFFER_SIZE
' build a "request" packet for a StringDescriptor
DescReq = New USB_DESCRIPTOR_REQUEST
DescReq.ConnectionIndex = PortNumber
DescReq.SetupPacket.wValue = CShort((USB_STRING_DESCRIPTOR_TYPE << 8) + _
Index)
DescReq.SetupPacket.wLength = CShort(nBytes - Marshal.SizeOf(DescReq))
DescReq.SetupPacket.wIndex = &H409 ' Language Code
' we have to be a bit creative on how we "zero out" the buffer
Dim NullString As New String(Chr(0), BUFFER_SIZE \ Marshal.SystemDefaultCharSize)
ptrDescReq = Marshal.StringToHGlobalAuto(NullString)
Marshal.StructureToPtr(DescReq, ptrDescReq, True)
' Use an IOCTL call to request the String Descriptor
If DeviceIoControl(hHub, IOCTL_USB_GET_DESCRIPTOR_FROM_NODE_CONNECTION, _
ptrDescReq, nBytes, ptrDescReq, nBytes, nBytesReturned, IntPtr.Zero) Then
' the pointer to the String Descriptor is just "off the edge" of
' the descriptor request packet.
ptrStrDesc = New IntPtr(ptrDescReq.ToInt32 + Marshal.SizeOf(DescReq))
StrDesc = CType(Marshal.PtrToStructure(ptrStrDesc, GetType( _
USB_STRING_DESCRIPTOR)), USB_STRING_DESCRIPTOR)
ans = StrDesc.bString
End If
' Clean up and go home
Marshal.FreeHGlobal(ptrDescReq)
CloseHandle(hHub)
End If
Return ans
End Function
Note: If you prefer a class-based example of using the Win32 API for USB, take a look at the link for a C# source code example at the end of this article
Additional Tools
The following tools from Microsoft are very helpful when dealing with USB device programming
- UVCView - Microsoft Diagnostic Tool for USB Hardware
- Windows Driver Development Kit (DDK)
Documentation Links
- CM_Get_Parent
- CM_Get_Device_ID
- CreateFile
- CloseHandle
- DeviceIoControl
- SetupDiGetClassDevs
- SetupDiEnumDeviceInterfaces
- SetupDiGetDeviceInterfaceDetail
- SetupDiGetDeviceInstanceId
- SetupDiEnumDeviceInfo
- SetupDiGetDeviceRegistryProperty
- SetupDiDestroyDeviceInfoList
Downloads/Links
Download the VB.Net Source code examples used in this article:
USB_SerialNumber.zip
Download the C# Source code for a class-based USB demonstration project:
USBView.zip
Read a related article on Introduction to Windows Management
Instrumentation