Sunday, July 7, 2013

Python Print String Descriptors for the USB HID Devices

1 Inctroduction

In this tutorial, we are going to write two small python programs that prints the String Descriptors for all the USB HID devices in the system. To do this, there are many choices of libraries for assistence. I will introduce two of them - PyUSB and PyWin32. PyUSB supports both Linux and Windows, and PyWin32 supports Windows only.

2 PyUSB - easy USB access to Python

2.1 Installation under Windows

Follow the instructions to download and install libusb-win32 first.

Install PyUSB in order to leverage its easy USB access to python language: Download the PyUSB package from sourceforge and execute the setup.py in the package,

C:\> python setup.py

2.2 Installation under Debian Linux

Thanks to the help of packages management systems for dealing with the dependencies, normally it is easier to install the package under Linux. In the Debian distribution for example,

% sudo apt-get install python-usb

2.3 Implementation

#!/usr/bin/env python

from __future__ import print_function
import argparse
import sys
import usb

Import the modules we need. Here i also import print_function to use the print() function instead of python2 print statement, so that it will be easier to port the program to python3.

busses = usb.busses()
for bus in busses:
    devices = bus.devices
    for dev in devices:

Iterate for all the busses and devices in the system. It calls find() to find out every usb.Device class in each usb.Bus class.

This usb.Device class contains all of the Device Descriptor according to the USB Spec. We may access them as class properties.

handle = dev.open()

Open the USB device. The handle is of type usb.DeviceHandle. We will invoke getString() method in the usb.DeviceHandle class to get the String Descriptors.

print("Device:", dev.filename)
print("  VID: 0x{:04x}".format(dev.idVendor))
print("  PID: 0x{:04x}".format(dev.idProduct))

Print the Vendor Identifier and Product Identifier of the USB device for information.

print("  Manufacturer: 0x{:x}".format(dev.iManufacturer), end='')

iManufacturer property is an 8-bit unsigned integer, telling us the index of the string descriptors. If it equals to zero, there is no string descriptor for this field.

if dev.iManufacturer == 0:
    print('')
else:
    print(" --> '{}'".format(handle.getString(dev.iManufacturer, 255)))

Call getString() and print the string. You can also specify a language ID other than US English in the 3rd parameter.

Alternately, you may choose to call getDescriptor() with descriptor type equal to 3.

Besides using the getString() method, you can import usb.util and use the get_string() method as well.

print("  Product: 0x{:x}".format(dev.iProduct), end='')
if dev.iProduct == 0:
    print('')
else:
    print(" --> '{}'".format(handle.getString(dev.iProduct, 255)))

print("  Serial Number: 0x{:x}".format(dev.iSerialNumber), end='')
if dev.iSerialNumber == 0:
    print('')
else:
    print(" --> '{}'".format(handle.getString(dev.iSerialNumber, 255)))

Printing the strings of product and serial number is similar.

3 PyWin32 - Python for Windows extensions

Download and install pywin32 from sourceforge. Make sure to download the correct pywin32 package according to the version (and architecture) of python that is installed in your system. Please reference README.txt.

3.1 Implementation

#!/usr/bin/env python

from __future__ import print_function
import argparse
import string
import struct
import sys

import win32api
import win32file
import pywintypes

Import the print_function for the same portable reason.

def CTL_CODE(DeviceType, Function, Method, Access):
    return (DeviceType << 16) | (Access << 14) | (Function << 2) | Method
def USB_CTL(id):
    # CTL_CODE(FILE_DEVICE_USB, (id), METHOD_BUFFERED, FILE_ANY_ACCESS)
    return CTL_CODE(0x22, id, 0, 0)

Reference WDK usbioctl.h and define the function that returns the USB_CTL value. The USB_CTL() will be used to define USB IOCTL codes.

IOCTL_USB_GET_ROOT_HUB_NAME = USB_CTL(258)                   # HCD_GET_ROOT_HUB_NAME
IOCTL_USB_GET_NODE_INFORMATION = USB_CTL(258)                # USB_GET_NODE_INFORMATION
IOCTL_USB_GET_NODE_CONNECTION_INFORMATION = USB_CTL(259)     # USB_GET_NODE_CONNECTION_INFORMATION
IOCTL_USB_GET_NODE_CONNECTION_DRIVERKEY_NAME = USB_CTL(264)  # USB_GET_NODE_CONNECTION_DRIVERKEY_NAME
IOCTL_USB_GET_NODE_CONNECTION_NAME = USB_CTL(261)            # USB_GET_NODE_CONNECTION_NAME
IOCTL_USB_GET_DESCRIPTOR_FROM_NODE_CONNECTION = USB_CTL(260) # USB_GET_DESCRIPTOR_FROM_NODE_CONNECTION

Define USB IoControl codes according to WDK usbiodef.h. For example, it defines HCD_GET_ROOT_HUB_NAME to 258.

USB_CONFIGURATION_DESCRIPTOR_TYPE = 2
USB_STRING_DESCRIPTOR_TYPE = 3
USB_INTERFACE_DESCRIPTOR_TYPE = 4
MAXIMUM_USB_STRING_LENGTH = 255

Define the type of the descriptors. They are the wValues field in USB setup packets used in Get Descriptor requests.

def open_dev(name):
    try:
        handle = win32file.CreateFile(name,
                                      win32file.GENERIC_WRITE,
                                      win32file.FILE_SHARE_WRITE,
                                      None,
                                      win32file.OPEN_EXISTING,
                                      0,
                                      None)
    except pywintypes.error as e:
        return None
    return handle

To open the USB Host Controller devices and USB Hub devices. Please reference the MSDN Creating and Opening Files and the usbview source codes in WDK. win32file.CreateFile is a wrapper of Win32 API CreateFile function.

def get_root_hub_name(handle):
    buf = win32file.DeviceIoControl(handle,
                                    IOCTL_USB_GET_ROOT_HUB_NAME,
                                    None,
                                    6,
                                    None)
    act_len, _ = struct.unpack('LH', buf)
    buf = win32file.DeviceIoControl(handle,
                                    IOCTL_USB_GET_ROOT_HUB_NAME,
                                    None,
                                    act_len,
                                    None)
    return buf[4:].decode('utf-16le')

Similarily, win32file.DeviceIoControl is a wrapper of Win32 DeviceIoControl. In order to get the USB Root Hub Name,

typedef struct _USB_ROOT_HUB_NAME {
    ULONG ActualLength;
    WCHAR RootHubName[1];
} USB_ROOT_HUB_NAME, *PUSB_ROOT_HUB_NAME;

we need to call DeviceIoControl twice: to get the ActualLength of the USB_ROOT_HUB_NAME structure at the first time, then get the structure including the name at the second time.

The same technique is also used to retrieve the driver registry key name (USB_NODE_CONNECTION_DRIVERKEY_NAME) that is associated with the USB device, the symbolic link name of the USB Hub (USB_NODE_CONNECTION_NAME) that is attached to the downstream port, and so forth.

Take get_ext_hub_name() for an example,

def get_ext_hub_name(handle, index):
    hub_name = chr(index) + '\0'*9
    buf = win32file.DeviceIoControl(handle,
                                    IOCTL_USB_GET_NODE_CONNECTION_NAME,
                                    hub_name,
                                    10,
                                    None)
    _, act_len, _ = struct.unpack('LLH', buf)
    buf = win32file.DeviceIoControl(handle,
                                    IOCTL_USB_GET_NODE_CONNECTION_NAME,
                                    hub_name,
                                    act_len,
                                    None)
    return buf[8:].decode('utf-16le')

We (at the first time) allocate an 10-byte buffer for hub_name with NodeName is of lenght one wide character.

typedef struct _USB_NODE_CONNECTION_NAME {
    ULONG ConnectionIndex;
    ULONG ActualLength;
    WCHAR NodeName[1];
} USB_NODE_CONNECTION_NAME, *PUSB_NODE_CONNECTION_NAME;

To get string descriptors, we prepare a request of structure USB_DESCRIPTOR_REQUEST and initialize wValue equal to Descriptor Type (high byte) and Descriptor Index (low byte).

def get_str_desc(handle, conn_idx, str_idx):
    req = struct.pack('LBBHHH',
                      conn_idx,
                      0,
                      0,
                      (USB_STRING_DESCRIPTOR_TYPE<<8) | str_idx,
                      win32api.GetSystemDefaultLangID(),
                      12+MAXIMUM_USB_STRING_LENGTH)
    try:
        buf = win32file.DeviceIoControl(handle,
                                        IOCTL_USB_GET_DESCRIPTOR_FROM_NODE_CONNECTION,
                                        req,
                                        12+MAXIMUM_USB_STRING_LENGTH,
                                        None)
    except pywintypes.error as e:
        return 'ERROR: no String Descriptor for index {}'.format(str_idx)
    if len(buf) > 16:
        return buf[14:].decode('utf-16le')
    return ''

We must inititialize fields: wValue, wIndex, and wLength of USB_DESCRIPTOR_REQUEST structure:

typedef struct _USB_DESCRIPTOR_REQUEST {
    ULONG ConnectionIndex;
    struct {
        UCHAR bmRequest;
        UCHAR bRequest;
        USHORT wValue;
        USHORT wIndex;
        USHORT wLength;
    } SetupPacket;
    UCHAR Data[0];
} USB_DESCRIPTOR_REQUEST, *PUSB_DESCRIPTOR_REQUEST;

The string comes after the request so we prepare the MAXIMUM_USB_STRING_LENGTH for Data array.

Date: 2012-07-07 Sun

Author: Winfred Lu

Org version 7.9.2 with Emacs version 24

14 comments:

Curt Hagenbuch said...

I tested-Printing string descriptors with PyWin32 under Windows
On WIN8.1: works great.
Excellent article. Easy to understand!
I searched for many hours until I found this, needing to identify which USB port/jack is being actively used by a device.
Summary: "WINFRED LU - FOR THE WIN! "

MagicBlogger said...
This comment has been removed by the author.
MagicBlogger said...

After learning of C99 flexible array, I have found a problem and solution.
For the size of flexible array, "12", which is the size of struct, is not necessary.

Error:
req = struct.pack('LBBHHH',
conn_idx,
0,
0,
(USB_STRING_DESCRIPTOR_TYPE<<8) | str_idx,
win32api.GetSystemDefaultLangID(),
12+MAXIMUM_USB_STRING_LENGTH)

Correct:
req = struct.pack('LBBHHH',
conn_idx,
0,
0,
(USB_STRING_DESCRIPTOR_TYPE<<8) | str_idx,
win32api.GetSystemDefaultLangID(),
MAXIMUM_USB_STRING_LENGTH)

Vita Chiou said...
This comment has been removed by the author.
Vita Chiou said...

RootHub: USB#ROOT_HUB20#4&2c86c80d&0#{f18a0e88-c30c-11d0-8815-00a0c906bed8}
Port2] {36fc9e60-c465-11cf-8056-444553540000}\0009
Vendor ID: 0x2232
Product ID: 0x6001
Device BCD: 0x0002
Manufacturer (0x1) -> "ERROR: no String Descriptor for index 1"
Product (0x2) -> "ERROR: no String Descriptor for index 2"

I cannot get the iManufacturer number and iProduct number, How can I do?

Winfred said...

Hi Vita,

It is possible that this device doesn't provide any string descriptor. Although the index of the string descriptors should be zero in this case, many devices are seen reporting incorrect index like what you saw. I believe there is not much you can do except to ask for vendor's help to upgrade its firmware.

You have a device produced by Silicon Motion according to Vendor ID (0x2232), but i can't be sure what the product (0x6001) it is.

Winfred said...

Thanks a lot to MagicBlogger for correcting me. :)

Vita Chiou said...

Dear Winfred:
Device Descriptor:
bcdUSB: 0x0200
bDeviceClass: 0xEF
bDeviceSubClass: 0x02
bDeviceProtocol: 0x01
bMaxPacketSize0: 0x40 (64)
idVendor: 0x2232
idProduct: 0x6001
bcdDevice: 0x0002
iManufacturer: 0x01
0x0409: "XXXXXX00010200144W11111"
iProduct: 0x02
0x0409: "XXX-XX02"
iSerialNumber: 0x00
bNumConfigurations: 0x01

I can use USBView to get iManufacturer & iProduct numbers of Microsoft, but I cannot extract from the program, because it is too huge. Do you have any suggest?

Winfred said...

OK. What did you mean by "cannot extract from the program"?
The maximum length of string representing iManufacturer and iProduct would up to 255, and the script should be able to handle it.
Would you print the buffer length returning by get_str_desc()?
And also please try to hex dump the buffer instead of just print the strings.

哭哭 said...
This comment has been removed by the author.
Vita Chiou said...

Dear Winfred
How can I do this? This is my first time to use Python, I am so confused.:(

Winfred said...

Don't be confused, Vita. This script is very simple. Please start from print_str_or_hex() function.

Anonymous said...

Hi Winfred,

I want to access the usb using Location parameter i.e hub and port.When i open the device manager i can view the below given parameter for my usb device:

Device Type: libusb-win32 devices
Manufacturer: Insert Manufacturer name
Location : Port_#0005.Hub_#0004.

Can you please tell me whether the below given output of your code,which contains 4 in the middle is same as Hub,i have mentioned in the location id:

USB#ROOT_HUB20#4&116fe3f7&0#{f18a0e88-c30c-11d0-8815-00a0c906bed8}

Thanks & Regards,
Sumit

Winfred said...

Hi Sumit,

No.
The device located at Port_#0005.Hub_#0004 is a generic Hub. The USB#ROOT_HUB20#4&116fe3f7 is one Root Hub, which supports USB 2.0 and the string you see is its device Instance Id.

The (given) generic Hub could be port #5 of that Root Hub, but we can't be sure according to the limited information. You may run the Windows Device Manager | View | Devices by connections, to view the hierarchic relationship of them.