Read Serial Number from a USB Disk

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:

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

Documentation Links

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