From the translator :
This is a translation of the Programming with PyUSB 1.0 manual.
This tutorial was written by the PyUSB developers, but quickly running through the commits, I believe that the main author of the tutorial is walac .let me introduce myself
PyUSB 1.0 is a
Python library that provides easy access to
USB . PyUSB provides various functions:
- 100% written in Python:
Unlike versions 0.x, which were written in C, version 1.0 is written in Python. This allows Python programmers with no C experience to better understand how PyUSB works. - Platform Neutrality:
Version 1.0 includes a front-end backend scheme. It isolates the API from system-specific implementation details. The IBackend interface connects these two layers. PyUSB comes with built-in backends for libusb 0.1, libusb 1.0, and OpenUSB. You can write your own backend if you want. - Portability:
PyUSB should run on any platform with Python> = 2.4, ctypes and at least one of the supported built-in backends. - Simplicity:
Interacting with a USB device has never been easier! USB is a complex protocol, and PyUSB has good presets for the most common configurations. - Isochronous transfer support:
PyUSB supports isochronous transfers if the underlying backend supports them.
Although PyUSB makes USB programming less painful, this tutorial assumes that you have minimal knowledge of the USB protocol. If you don’t know anything about USB, I recommend you Jan Axelson
’s excellent “Perfect USB” book by Jan Axelson.
Enough talk, let's write code!
Who is who
First, let's describe the PyUSB modules. All PyUSB modules are under
usb , with subsequent modules:
Module | Description |
---|
core | The main USB module. |
util | Secondary functions. |
control | Standard management queries. |
legacy | 0.x. Compatibility Layer |
backend | Subpackage containing embedded backends. |
For example, to import a
core module, enter the following:
')
>>> import usb.core >>> dev = usb.core.find()
Well, let's start
The following is a simple program that sends the string 'test' to the first data source found (endpoint OUT):
import usb.core import usb.util
The first two lines import PyUSB package modules.
usb.core is the main module, and
usb.util contains auxiliary functions. The following command searches our device and returns an instance of the object if it finds one. If not,
None is returned. Next, we set the configuration we will use. Note: the absence of arguments means that the desired configuration was set as default. As you will see, many of the PyUSB functions have default settings for most common devices. In this case, the first found configuration is put.
Then, we look for the end point we’re interested in. We are looking for it inside the first interface that we have. After we found this point, we send data to it.
If we know the address of the end point in advance, we can simply call the device object's
write function:
dev.write(1, 'test')
Here we write the string 'test' at the control point under address
1 . All these functions will be disassembled better in the following sections.
What's wrong?
Each function in PyUSB raises an exception in case of an error. In addition to
standard Python exceptions , PyUSB defines
usb.core.USBError for USB-related errors.
You can also use the PyUSB log functions. It uses the
logging module. To use it, define the
PYUSB_DEBUG environment
variable with one of the following logging levels:
critical ,
error ,
warning ,
info or
debug .
By default, messages are sent to
sys.stderr . If you want, you can redirect log messages to a file by defining the environment variable
PYUSB_LOG_FILENAME . If its value is the correct path to the file, messages will be written there, otherwise they will be sent to
sys.stderr .
Where are you?
The
find () function in the
core module is used to find and number the devices attached to the system. For example, let's say that our device has a vendor ID with a value of 0xfffe and a product ID equal to 0x0001. If we need to find this device we will do this:
import usb.core dev = usb.core.find(idVendor=0xfffe, idProduct=0x0001) if dev is None: raise ValueError('Our device is not connected')
That's it, the function will return the
usb.core.Device object that represents our device. If the device is not found, it will return
None . In fact, you can use any field of the Device
Descriptor class you want. For example, what if we want to know if there is a USB printer connected to the system? It is very easy:
7 is the code for the printer class in accordance with the USB specification. Oh, wait, what if I want to number all the available printers? No problem:
What happened? Well, time for a little explanation ...
find has a parameter called
find_all and by default is False. When it is false
[1] ,
find will return the first device that fits the specified criteria (let's talk about this soon). If you pass a
true value to the parameter,
find instead will return a list of all devices matching the criteria. That's all! Simple, isn't it?
Are we done? Not! I haven't told everything yet: many devices actually put their class information in the Interface
Descriptor instead of the Device
Descriptor . So, in order to truly find all printers connected to the system, we will need to iterate through all configurations, as well as all interfaces, and check whether one of the interfaces has a bInterfaceClass value of 7. If you are a
programmer like me, you may ask yourself: Is there any easier way to do this? Answer: yes, he is. First, let's look at the ready code for finding all connected printers:
import usb.core import usb.util import sys class find_class(object): def __init__(self, class_): self._class = class_ def __call__(self, device):
The
custom_match parameter accepts any callable object that receives a device object. It must return a true value for a suitable device and a false value for an unsuitable one. You can also combine
custom_match with device fields if you want:
Here we are interested in 0xfffe supplier printers.
describe yourself
Ok, we found our device, but before interacting with it, we would like to know more about it. Well, you know, configurations, interfaces, endpoints, types of data streams ...
If you have a device, you can access any field in the device descriptor as object properties:
>>> dev.bLength >>> dev.bNumConfigurations >>> dev.bDeviceClass >>>
To access the available configurations in the device, you can iterate the device:
for cfg in dev: sys.stdout.write(str(cfg.bConfigurationValue) + '\n')
In the same way, you can iterate a configuration for accessing interfaces, and also iterate interfaces for accessing their control points. Each object type has fields of the corresponding descriptor as attributes. Take a look at an example:
for cfg in dev: sys.stdout.write(str(cfg.bConfigurationValue) + '\n') for intf in cfg: sys.stdout.write('\t' + \ str(intf.bInterfaceNumber) + \ ',' + \ str(intf.bAlternateSetting) + \ '\n') for ep in intf: sys.stdout.write('\t\t' + \ str(ep.bEndpointAddress) + \ '\n')
You can also use indexes for random access to handles, like this:
>>>
As you can see the indices are counted from 0. But wait! There is something strange about how I access the interface ... Yes, you are right, the index for the Configuration takes a series of two values, of which the first is the Interface index and the second is the alternative setting. In general, to get access to the first interface, but with the second setting, we write
cfg [(0,1)] .
Now is the time to learn a powerful way to search for handles - a useful
find_descriptor function. We have already seen it in the printer search example.
find_descriptor works almost as well as
find , with two exceptions:
- find_descriptor receives as its first parameter the initial form you will be looking for.
- It has no backend parameter. [2] .
For example, if we have a
cfg configuration descriptor, and we want to find all the alternative settings for interface 1, we will do this:
import usb.util alt = usb.util.find_descriptor(cfg, find_all=True, bInterfaceNumber=1)
Note that
find_descriptor is in the
usb.util module. It also accepts the
custom_match parameter described earlier.
We deal with multiple identical devices.Sometimes you can have two identical devices connected to a computer. How can you distinguish them?
Device objects come with two additional attributes that are not part of the USB specification, but are very useful: the
bus and
address attributes. First of all, it should be said that these attributes come from the backend, and the backend may not support them - in this case they are set to
None . However, these attributes represent the device bus number and address and, as you might have guessed, can be used to distinguish between two devices with the same values ​​for the
idVendor and
idProduct attributes .
How should i work?
Once connected, USB devices must be configured using several standard queries. When I began to study the
USB specification, I was discouraged by dexryptors, configurations, interfaces, alternative settings, transfer types and all that ... And worst of all, you can’t just ignore them: the device doesn't work without configuration, even if it is one! PyUSB is trying to make your life as simple as possible. For example, after receiving your device object, first of all, before interacting with it, you need to send a
set_configuration request. The configuration parameter for this request that interests you is
bConfigurationValue . Most devices have no more than one configuration, and tracking configuration values ​​for use is annoying (although most of the code I saw was simply hardcoded for this). Therefore, in PyUSB, you can simply send a
set_configuration request
with no arguments. In this case, it will install the first found configuration (if your device has only one, you don’t have to worry about the configuration value at all). For example, imagine that you have a device with one configuration decryptor, and its bConfigurationValue field is 5
[3] , subsequent requests will work the same way:
>>> dev.set_configuration(5)
Wow You can use the
Configuration object as a parameter for
set_configuration ! Yes, he also has a
set method for configuring himself into the current configuration.
Another option that you need or will not need to configure is the option to change interfaces. Each device can have only one activated configuration at a time, and each configuration can have more than one interface, and you can use all interfaces at the same time. You better understand this concept if you think of the interface as a logical device. For example, let's imagine a multifunctional printer, which is both a printer and a scanner at the same time. In order not to complicate (or at least make it as simple as possible), let's assume that it has only one configuration. Since we have a printer and a scanner; the configuration has 2 interfaces: one for the printer and one for the scanner. A device with more than one interface is called a composite device. When you connect your multifunction printer to your computer, the Operating System will load two different drivers: one for each “logical” peripheral device that you have
[4] .
What about alternative interface settings? Good thing you asked. The interface has one or more alternative settings. An interface with only one alternative setting is considered as having no alternative settings.
[5] . Alternative settings for interfaces as a configuration for devices, that is, for each interface you can have only one active alternative setting. For example, the USB specification says that a device cannot have an isochronous checkpoint in its main alternative setting.
[6] , so the streaming device should have at least two alternative settings, with the second setting having an isochronous checkpoint. But, unlike configurations, interfaces with only one alternative setting do not need to be configured.
[7] . You select an alternative interface setting using the
set_interface_altsetting function:
>>> dev.set_interface_altsetting(interface = 0, alternate_setting = 0)
A warningThe USB specification says that the device is allowed to return an error in case it receives a SET_INTERFACE request to an interface that does not have additional alternative settings. So, if you are not sure that the interface has more than one alternative setting or that it accepts a SET_INTERFACE request, the most secure method will be to call
set_interface_altsetting inside a try-except block like this:
try: dev.set_interface_altsetting(...) except USBError: pass
You can also use the
Interface object as a function parameter; the
interface and
alternate_setting parameters are automatically inherited from the
bInterfaceNumber and
bAlternateSetting fields . Example:
>>> intf = find_descriptor(...) >>> dev.set_interface_altsetting(intf) >>> intf.set_altsetting()
A warningThe
Interface object must belong to the active configuration descriptor.
Talk to me honey
And now it's time for us to understand how to interact with USB devices. USB has four types of data streams: bulk transfer, interrupt transfer, isochronous transfer, and control transfer. I do not plan to explain the purpose of each stream and the differences between them. Therefore, I assume that you have at least basic knowledge of USB data streams.
The control data stream is the only stream whose structure is described in the specification; the others simply send and receive raw data from a USB point of view. Therefore, you have different functions for working with control flows, and the remaining flows are processed by the same functions.
You can access the control data stream using the
ctrl_transfer method. It is used for both outgoing (OUT) and incoming (IN) streams. The flow direction is determined by the
bmRequestType parameter.
The
ctrl_transfer parameters almost coincide with the structure of the control request. The following is an example of how to organize the control data flow.
[8] :
>>> msg = 'test' >>> assert dev.ctrl_transfer(0x40, CTRL_LOOPBACK_WRITE, 0, 0, msg) == len(msg) >>> ret = dev.ctrl_transfer(0xC0, CTRL_LOOPBACK_READ, 0, 0, len(msg)) >>> sret = ''.join([chr(x) for x in ret]) >>> assert sret == msg
In this example, it is assumed that our device includes two user control requests that act as a loopback pipe. What you write with the message
CTRL_LOOPBACK_WRITE , you can read with the message
CTRL_LOOPBACK_READ .
The first four parameters —
bmRequestType ,
bmRequest ,
wValue, and
wIndex — are the fields of the standard control flow structure. The fifth parameter is either the data to be sent for the outgoing data stream or the number of read data in the incoming stream. The transferred data can be any type of sequence that can be submitted as a parameter to the input of the
__init__ method for an
array . If there is no data to be sent, the parameter must be
None (or 0 in the case of an incoming data stream). There is one more optional parameter specifying the operation timeout. If you do not pass it, the default timeout will be used (more on this later). In an outgoing data stream, the return value is the number of bytes actually sent to the device. In the incoming stream, the return value is an
array with read data.
For other threads, you can use the
write and
read methods, respectively, to write and read data. You do not need to worry about the type of flow - it is automatically determined by the address of the control point. Here is our loopback example, provided we have a loopback pipe at checkpoint 1:
>>> msg = 'test' >>> assert len(dev.write(1, msg, 100)) == len(msg) >>> ret = dev.read(0x81, len(msg), 100) >>> sret = ''.join([chr(x) for x in ret]) >>> assert sret == msg
The first and third parameters are the same for both methods — this is the control point address and timeout, respectively. The second parameter is the data to be sent (write) or the number of bytes to read (read). The returned data will be either an instance of an
array object for the
read method, or the number of bytes written for the
write method.
With beta 2 versions instead of the number of bytes, you can pass to
read or
ctrl_transfer an
array object into which data will be read. In this case, the number of bytes to read will be the length of the array multiplied by the value of
array.itemsize .
In
ctrl_transfer , the
timeout parameter is optional. When
timeout is omitted, the
Device.default_timeout property is
used as an operational timeout.
Control yourself
In addition to the data
stream functions, the
usb.control module provides functions that include standard USB control requests, and the
usb.util module has a convenient
get_string function that
delivers string descriptors.
Additional topics
Every great abstraction stands for a great realization.
There used to be only
libusb . Then libusb 1.0 came and we had libusb 0.1 and 1.0. After that we created
OpenUSB and now we live in the
Tower of Babel of USB libraries
[9] . How does PyUSB handle this? Well, PyUSB is a democratic library, you can choose which library you want. In fact, you can write your own USB library from scratch and tell PyUSB to use it.
The
find function has one more parameter which I did not tell you about. This is the
backend parameter. If you do not pass it, one of the built-in backends will be used. A backend is an object inherited from
usb.backend.IBackend , responsible for introducing OS-specific USB junk. As you might guess, the built-in libusb 0.1, libusb 1.0 and OpenUSB are backends.
You can write your own backend and use it. Just inherit from
iBackend and enable the necessary methods. You may need to look in the
usb.backend documentation to see how this is done.
Do not be selfish
Python has what we call
automatic memory management . This means that the virtual machine will decide when to unload objects from memory. Under the hood, PyUSB manages all the low-level resources you need to work with (interface approval, device adjustments, etc.) and most users don’t have to worry about it. But, due to the uncertain nature of the automatic destruction of objects by Python, users cannot predict when resources will be released. Some applications need to allocate and free resources deterministically. For such applications, the
usb.util module provides functions for interacting with resource management.
If you want to request and release interfaces manually, you can use the functions
claim_interface and
release_interface .
The function claim_interface will request the specified interface if the device has not yet done so. If the device has already requested an interface, it does nothing. Also release_interface will release the specified interface if requested. If the interface is not requested, it does nothing. You can use manual interface query to solve the configuration problem described in the libusb documentation . If you want to release all the resources allocated by the device object (including the requested interfaces), you can use the dispose_resources function. It frees all allocated resources and places the device object (but not the device hardware itself) into the state in which it was returned after using the find function .Manual Definition of Libraries
In general, the backend is a wrapper over a shared library that implements the USB access API. By default, the backend uses the ctypes function find_library () . On Linux and other Unix-like operating systems , find_library attempts to run external programs (such as / sbin / ldconfig , gcc, and objdump ) in order to find the library file.In systems where these programs are missing and / or the library cache is disabled, this feature cannot be used. To overcome the limitations, PyUSB allows you to submit the find_library () user-defined function to the backend.An example of such a scenario would be: >>> import usb.core >>> import usb.backend.libusb1 >>> >>> backend = usb.backend.libusb1.get_backend(find_library=lambda x: "/usr/lib/libusb-1.0.so") >>> dev = usb.core.find(..., backend=backend)
Note that find_library is an argument to the get_backend () function, in which you supply the function that is responsible for finding the right backend library.Old school rules
If you are writing an application using the old PyUSB APIs (0.this-there), you can ask yourself if you need to update your code to use the new API. Well, you should do it, but it is not necessary. PyUSB 1.0 comes with the usb.legacy compatibility module . It includes the old API based on the new API. “Well, should I just replace my import usb line with import usb.legacy as usb to make my application work?”, You ask. The answer is yes, it will work, but it is not necessary. If you start your application unchanged it will work, because the import usb line imports all public symbols from usb.legacy. If you encounter a problem - most likely you have found a bug.Help me please
If you need help, do not send me an e-mail , for this there is a mailing list. Subscription instructions can be found on the PyUSB website .[1] When I write True or False (with a capital letter), I mean the corresponding meanings of the Python language. And when I say true (true) or false (false), I mean any Python expression that is regarded as true or false. (This similarity occurred in the original and helps to understand the concepts of true and false in translation. - Approx. Per. ) :[2] See specific backend documentation.[3] The USB specification does not impose any particular value on the configuration value. The same is true for interface numbers and alternative settings.[4] In fact, everything is a little more complicated, but this is just an explanation for us enough.[5] I know it sounds weird.[6] This is because if there is no bandwidth for isochronous data streams during device configuration, it can be successfully numbered.[7] This does not happen for configuration, because the device is allowed to be in an unconfigured state.[8] In PyUSB, the control data flows refer to checkpoint 0. Very, very rarely, a device has an alternative control checkpoint (I have never met such a device).[9] This is just a joke, do not take it seriously. Great choice is better than no choice.