User Guide¶
Installing¶
usbx can be installed with pip:
$ python -m pip install usbx
Enumerating USB devices¶
usb is a global object and serves as a starting point for most operations.
get_devices() returns the list of currently connected USB devices
(instances of Device).
from usbx import usb
for device in usb.get_devices():
print(device)
USD device 4295228548, vid=0x2109, pid=0x0813, manufacturer=VIA Labs, Inc., product=USB3.0 Hub, serial=None
USD device 4295228665, vid=0x0b0e, pid=0x0412, manufacturer=None, product=Jabra SPEAK 410 USB, serial=70398DBDD13Cx011200
Querying information about devices¶
For each USB device, descriptive information and details about the interfaces and endpoints are available
through the properties and methods of the Device class:
from usbx import usb
for device in usb.get_devices():
print(f'VID: 0x{device.vid:04x}, PID: 0x{device.pid:04x}')
print(f'Version: {device.device_version}')
print(f'Interfaces: {len(device.configuration.interfaces)}')
VID: 0x2109, PID: 0x0813
Version: 144.1.1
Interfaces: 1
VID: 0x0bda, PID: 0x0412
Version: 1.7.0
Interfaces: 1
Enumerating specific device classes¶
Device information can be used to identify USB devices with certain features. The below code looks for video cameras.
USB video cameras provide an interface with USB class 0x0E and subclass 0x01.
To detect them, one needs to dig into the details of device configuration
(also see configuration() and Configuration).
Each device might have multiple interfaces. So a for loop inspects all of them.
The class and subclass codes are found on the currently active alternate setting
(as an interface might have multiple alternate settings).
CC_VIDEO = 0x0E
SC_VIDEOCONTROL = 0x01
def is_video_device(device: Device) -> bool:
for interface in device.configuration.interfaces:
alternate = interface.current_alternate
if alternate.class_code == CC_VIDEO and alternate.subclass_code == SC_VIDEOCONTROL:
return True
return False
for device in usb.find_devices(match = is_video_device):
print(device)
USD device 4295228856, vid=0x413c, pid=0xc015, manufacturer=Chicony Tech. Inc., product=Dell Webcam WB7022, serial=3489BB02D84A
find_devices() returns the matching devices. It can take different arguments
to identify the relevant devices. One such argument is match, which takes a function or
lambda expression.
Notifications about connected and disconnected devices¶
To be notified when USB device is plugged in or removed from the computer,
callback functions can registered. The functions receive the Device instance
as the only parameter:
def connected(device: Device) -> None:
print(f'Connected: {device}')
def disconnected(device: Device) -> None:
print(f'Disconnected: {device}')
usb.on_connected(connected)
usb.on_disconnected(disconnected)
print("Press Enter to exit...\n")
for dev in usb.find_devices():
print(f'Present: {dev}')
input()
In the above example connected and disconnected are the callback
functions that are registered with on_connected() and
on_disconnected().
Remembering a particular device¶
If multiple devices of the same type are used, the triple of vendor ID, product ID and serial number can be used to uniquely identify a device. This can be useful to save settings related to a particular device.
@dataclass
class DeviceId:
vid: int
pid: int
serial: str
def matches(self, device: Device) -> bool:
return (device.vid == self.vid and device.pid == self.pid
and device.serial == self.serial)
remembered_device_id = DeviceId(0xcafe, 0xceaf, "35A737883336")
for device in usb.get_devices():
if remembered_device_id.matches(device):
print(f'Remembered device: {device}')
else:
print(f'Other device: {device}')
Using control requests¶
To send a control request, the device is opened first. control_transfer_out() takes
a ControlTransfer instance as its first argument, consisting of request type, recipient, value,
index and data. The purpose of these values depends on the USB device, and device-specific documentation
is usually provided by the device manufacturer. For devices implementing an official USB class,
it is documented in the USB class specification.
In the below example, the test device for testing this library is used. The possible values and their functionality are documented on GitHub on the Loopback device page.
test_device = usb.find_device(vid = 0xcafe, pid = 0xceaf)
test_device.open()
transfer = ControlTransfer(RequestType.VENDOR, Recipient.DEVICE, 0x01, 1234, 0)
test_device.control_transfer_out(transfer)
test_device.close()
Note that the ControlTransfer instance uses separate fields for request type and recipient
while they are combined in bmRequestType field in the USB specification.
Control requests are always initiated from the host computer, and the host always waits for an answer from the device. For transfer outs, the response is just an acknowledgement from the device, typically indicating that the device has executed the command.
For transfer ins – executed with control_transfer_in(), the response
consists of data.
Sending interrupt transfers¶
Interrupt transfers are data packets that are transmitted irregularly, either from the host to the device or vice versa. Their purpose depends on the implemented USB class or vendor-specific functionality.
Interrupt transfers are addressed to USB endpoints. Endpoints belong to a USB interface. Before using interrupt transfers, the USB device must be opened and the endpoint’s interface must be claimed.
test_device = usb.find_device(vid = 0xcafe, pid = 0xceaf)
test_device.open()
test_device.claim_interface(0)
test_device.transfer_out(3, bytes([0x56, 0x78, 0x9a, 0xbc]))
test_device.close()
Closing the device will also release the interface.
Receiving interrupt transfers¶
Interrupt transfers from the device to the host are used to communicate irregular events. A host (or rather a thread of the host) will typically wait for any interrupt transfers.
The below code uses the test device, which will echo all received interrupt transfers (on IN endpoint 3) twice on the outgoing endpoint (OUT endpoint 3).
test_device = usb.find_device(vid = 0xcafe, pid = 0xceaf)
test_device.open()
test_device.claim_interface(0)
test_device.transfer_out(3, bytes([0x56, 0x78, 0x9a, 0xbc]))
data = test_device.transfer_in(3)
print(f'Received {len(data)} bytes')
data = test_device.transfer_in(3)
print(f'Received {len(data)} bytes')
test_device.close()
Note that usbx uses endpoint numbers without a direction bit to specify endpoints. So the IN and OUT endpoints use the same number in the above example. Other USB software might use the addresses 3 and 131 instead.
transfer_in() is a blocking method. If the device
does not send an interrupt transfer, it will wait forever.
It is also possible to specify a timeout duration as the second
parameter. If the timeout is reached, a class:~TimeoutException
is raised.
Sending data to bulk endpoints¶
Bulk endpoints can transmit large amounts of data from and to devices. They implement a stream concept. The chunks of data submitted for transmission do not directly translate to the transmitted packets as the operating system can and will join the chunks and divide the resulting stream into packets as it sees fit.
In the code, sending data to bulk endpoints does not look any different from sending interrupt transfers (except for the endpoint number).
Whether an endpoint is a bulk or interrupt endpoint is defined in the device’s configuration.
test_device = usb.find_device(vid = 0xcafe, pid = 0xceaf)
test_device.open()
test_device.claim_interface(0)
test_device.transfer_out(1, "Hello, world!\n".encode())
test_device.close()
Receiving data from bulk endpoints¶
Receiving data from a bulk endpoint is similar to reading data
from a binary file. It is read in chunks. The chunk size cannot be
chosen; transfer_in() will return whatever is
currently available.
The below code uses the test device, which implements a loop back from IN endpoint 1 to OUT endpoint 2.
Since the division into packets and chunks cannot be controlled, the code will join multiple chunks if needed.
test_device = usb.find_device(vid = 0xcafe, pid = 0xceaf)
test_device.open()
test_device.claim_interface(0)
test_device.transfer_out(1, "Hello, world!\n".encode())
received = bytearray()
while len(received) < 14:
chunk = test_device.transfer_in(2)
received.extend(chunk)
print(f'Received: {received.decode()}')
test_device.close()