[Перевод] Программирование с PyUSB 1.0
От переводчика:
Это перевод руководства Programming with PyUSB 1.0
Данное руководство написано силами разработчиков PyUSB, однако быстро пробежавшись по коммитам я полагаю, что основной автор руководства — walac.
Позвольте мне представиться
PyUSB 1.0 — это библиотека Python обеспечивающая легкий доступ к USB. PyUSB предоставляет различные функции:
- На 100% написана на Python:
В отличии от версий 0.x, которые были написаны на C, версия 1.0 написанна на Python. Это позволяет программистам на Python без опыта работы на C лучше понять как работает PyUSB. - Нейтральность платформы:
Версия 1.0 включает в себя фронтенд-бэкенд схему. Она изолирует API от специфичных с точки зрения системы деталей реализации. Соединяет эти два слоя интерфейс IBackend. PyUSB идет вместе со встроенными бэкендами для libusb 0.1, libusb 1.0 и OpenUSB. Вы можете сами написать свой бэкенд, если хотите. - Портативность:
PyUSB должен запускаться на любой платформе с Python >= 2.4, ctypes и, по крайней мере, одним из поддерживаемых встроенных бэкендов. - Простота:
Взаимодействие с устройством USB никогда не было таким простым! USB — сложный протокол, а у PyUSB есть хорошие предустановки для наиболее распространенных конфигураций. - Поддержка изохронных передач:
PyUSB поддерживает изохронные передачи, если лежащий в основе бэкенд поддерживает их.
Несмотря на то, что PyUSB делает программирование USB менее болезненным, в этом туториале предполагается, что у Вас есть минимальные знания USB протокола. Если Вы ничего не знаете о USB, я рекомендую Вам прекрасную книгу Яна Аксельсона «Совершенный USB» (Jan Axelson «USB Complete»).
Довольно разговоров, давайте писать код!
Кто есть кто
Для начала, давайте дадим описание модулям PyUSB. Все модули PyUSB находятся под пекетом usb, с последующими модулями:
Модуль | Описание |
---|---|
core | Основной модуль USB. |
util | Вспомогательные функции. |
control | Стандартные запросы управления. |
legacy | Слой совместимости с версиями 0.x. |
backend | Субпакет содержащий встроенные бэкенды. |
К примеру, чтобы импортировать модуль core, введите следующее:
>>> import usb.core
>>> dev = usb.core.find()
Ну что ж начнём
Далее следует простенькая программа, которая посылает строку 'test' в первый найденный источник данных (endpoint OUT):
import usb.core
import usb.util
# находим наше устройство
dev = usb.core.find(idVendor=0xfffe, idProduct=0x0001)
# оно было найдено?
if dev is None:
raise ValueError('Device not found')
# поставим активную конфигурацию. Без аргументов, первая же
# конфигурация будет активной
dev.set_configuration()
# получим экземпляр источника
cfg = dev.get_active_configuration()
intf = cfg[(0,0)]
ep = usb.util.find_descriptor(
intf,
# сопоставим первый источник данных
custom_match = \
lambda e: \
usb.util.endpoint_direction(e.bEndpointAddress) == \
usb.util.ENDPOINT_OUT)
assert ep is not None
# записываем данные
ep.write('test')
Первые две строки импортируют модули пакета PyUSB. usb.core — основной модуль, а usb.util содержит вспомогательные функции. Следующая команда ищет наше устройство и возвращает экземпляр объекта, если находит. Если нет, возвращается None. Далее, мы устанавливаем конфигурацию, которую будем использовать. Заметьте: отсутствие аргументов означает, что нужная конфигурация была проставлена по-умолчанию. Как Вы увидите, во многих функциях PyUSB есть настройки по-умолчанию для большинства распространенных устройств. В этом случае, ставится первая найденная конфигурация.
Затем, мы ищем конечную точку в которой заинтересованы. Мы ищем ее внутри первого интерфейса, который у нас есть. После того как нашли эту точку мы посылаем в неё данные.
Если нам заранее известен адрес конечной точки, мы можем просто вызвать функцию write объекта device:
dev.write(1, 'test')
Здесь мы пишем строку 'test' в контрольную точку под адресом 1. Все эти функции будут разобраны лучше в последующих разделах.
Что не так?
Каждая функция в PyUSB вызывает исключение в случае ошибки. Помимо стандартных исключений Python, PyUSB определяет usb.core.USBError для ошибок связанных с USB.
Вы также можете использовать функции лога PyUSB. Он использует модуль logging. Для его использования определите переменную окружения PYUSB_DEBUG с одним из следующих уровней логирования: critical, error, warning, info или debug.
По-умолчанию сообщения посылаются в sys.stderr. Если хотите, Вы можете перенаправить сообщения лога в файл определив переменную окружения PYUSB_LOG_FILENAME. Если её значение — корректный путь к файлу, сообщения будут записываться туда, иначе они будут посылаться в sys.stderr.
Где ты?
Функция find () в модуле core используется чтобы найти и пронумеровать устройства присоединенные к системе. К примеру, скажем что у нашего устройства есть vendor ID со значением 0xfffe и product ID равный 0×0001. Если нам нужно найти это устройство мы сделаем так:
import usb.core
dev = usb.core.find(idVendor=0xfffe, idProduct=0x0001)
if dev is None:
raise ValueError('Our device is not connected')
Вот и всё, функция возвратит объект usb.core.Device, который представляет наше устройство. Если устройство не найдено оно возвратит None. На самом деле Вы можете использовать любое поле класса Device Descriptor, которое хотите. К примеру, что если мы захотим узнать есть ли USB-принтер подключенный к системе? Это очень легко:
# на самом деле это не всё, продолжайте читать
if usb.core.find(bDeviceClass=7) is None:
raise ValueError('No printer found')
7 — это код для класса принтеров в соответствии со спецификацией USB. О, постойте, что если я хочу пронумеровать все имеющиеся принтеры? Без проблем:
# это тоже ещё не всё...
printers = usb.core.find(find_all=True, bDeviceClass=7)
# Python 2, Python 3, быть или не быть
import sys
sys.stdout.write('There are ' + len(printers) + ' in the system\n.')
Что случилось? Что ж, время для небольшого объяснения… у find есть параметр, который называется find_all и по-умолчанию имеет значение False. Когда он имеет ложное значение [1], find будет возвращать первое устройство, которое подходит под указанные критерии (скоро об этом поговорим). Если Вы передадите параметру истинное значение, find вместо этого возвратит список из всех устройств подходящих по критериям. Вот и всё! Просто, не правда ли?
Мы закончили? Нет! Я ещё не всё рассказал: многие устройства на самом деле ставят свою информацию о классе в Interface Descriptor вместо Device Descriptor. Так что, чтобы по-настоящему найти все принтеры подключенные к системе, нам нужно будет перебрать все конфигурации, а также все интерфейсы и проверить — выставлено ли у одного из интерфейсов в bInterfaceClass значение 7. Если Вы программист как и я Вы можете задаться вопросом: есть ли путь полегче с помощью которого это можно реализовать. Ответ: да, он есть. Для начала давайте посмотрим на готовый код нахождения всех подключенных принтеров:
import usb.core
import usb.util
import sys
class find_class(object):
def __init__(self, class_):
self._class = class_
def __call__(self, device):
# для начала, проверим устройство
if device.bDeviceClass == self._class:
return True
# Ок, переберем все устройства, чтобы найти
# интерфейс, который подходит нашему классу
for cfg in device:
# find_descriptor: что это?
intf = usb.util.find_descriptor(
cfg,
bInterfaceClass=self._class
)
if intf is not None:
return True
return False
printers = usb.core.find(find_all=1, custom_match=find_class(7))
Параметр custom_match принимает любой вызываемый объект, который получает объект устройства. Он должен возвращать истинное значение для подходящего устройства и ложное — для неподходящего. Вы также можете скомбинировать custom_match с полями устройства, если захотите:
# найти все принтеры, которые принадлежат нашему поставщику:
printers = usb.core.find(find_all=1, custom_match=find_class(7), idVendor=0xfffe)
Здесь нас интересуют принтеры поставщика 0xfffe.
Опиши себя
Ок, мы нашли наше устройство, но перед тем как с ним взаимодействовать, нам хотелось бы узнать больше о нём. Ну Вы знаете, конфигурации, интерфейсы, конечные точки, типы потоков данных…
Если у Вас есть устройство, Вы можете получить доступ к любому полю дескриптора устройства как к свойствам объекта:
>>> dev.bLength
>>> dev.bNumConfigurations
>>> dev.bDeviceClass
>>> # ...
Для доступа к имеющимся конфигурациям в устройстве, Вы можете итерировать устройство:
for cfg in dev:
sys.stdout.write(str(cfg.bConfigurationValue) + '\n')
Таким же образом Вы можете итерировать конфигурацию для доступа к интерфейсам, а также итерировать интерфейсы для доступа к их контрольным точкам. У каждого типа объекта есть поля соответствующего дескриптора как атрибуты. Взглянем на пример:
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')
Вы также можете использовать индексы для произвольного доступа к дескрипторам, как здесь:
>>> # получаем доступ ко второй конфигурации
>>> cfg = dev[1]
>>> # получаем доступ к первому интерфейсу
>>> intf = cfg[(0,0)]
>>> # третья контрольная точка
>>> ep = intf[2]
Как вы можете увидеть индексы отсчитываются с 0. Но постойте! Есть что-то странное в том, как я получаю доступ к интерфейсу… Да, Вы правы, индекс для Configuration принимает ряд из двух значений, из которых первый — это индекс Interface’а, а второй — альтернативная настройка. В общем, чтобы получить доступ к первому интерфейсу, но со второй настройкой, мы напишем cfg[(0,1)].
Теперь время научиться мощному способу поиска дескрипторов — полезной функции find_descriptor. Мы уже видели её в примере поиска принтеров. find_descriptor работает практически также как и find, с двумя исключениями:
- find_descriptor получает как свой первый параметр исходный дискриптор, который Вы будете искать.
- В нем нет параметра backend [2].
К примеру, если у нас есть дескриптор конфигурации cfg, и мы хотим найти все альтернативные настройки интерфейса 1, мы сделаем так:
import usb.util
alt = usb.util.find_descriptor(cfg, find_all=True, bInterfaceNumber=1)
Заметьте, что find_descriptor находится в модуле usb.util. Он также принимает описанный ранее параметр custom_match.
Имеем дело с множественными идентичными устройствами
Иногда у Вас может быть два идентичных устройства подсоединенных к компьютеру. Как Вы можете различать их? Объекты Device идут с двумя дополнительными атрибутами, которые не являются частью спецификации USB, но очень полезны: атрибуты bus и address. Прежде всего, стоит сказать, что эти атрибуты идут от бэкенда, а бэкенд может и не поддерживать их — в этом случае они выставлены на None. Тем не менее эти атрибуты представляют номер и адрес шины устройства и, как Вы могли уже догадаться, могут быть использованы для того, чтобы различать два устройства с одинаковыми значениями атрибутов idVendor и idProduct.
Как я должен работать?
Устройства USB после подсоединения должны конфигурироваться с помощью нескольких стандартных запросов. Когда я начал изучать спецификацию USB, я был обескуражен дексрипторами, конфигурациями, интерфейсами, альтернативными настройками, типами передачи и всем этим… И что самое худшее — Вы не можете просто игнорировать их: устройство не работает без установки конфигурации, даже если оно одно! PyUSB пытается сделать Вашу жизнь настолько проще, насколько это возможно. К примеру, после получения Вашего объекта устройства, первым делом, перед тем как взаимодействовать с ним, нужно отправить запрос set_configuration. Параметр конфигурации для этого запроса, который Вас интересует — bConfigurationValue. У большинства устройств есть не более одной конфигурации, а отслеживание значения конфигурации для использования раздражает (хотя большинство кода, который я видел просто жестко кодировали это). Следовательно, в PyUSB, Вы можете просто отправить запрос set_configuration без аргументов. В этом случае он установит первую найденную конфигурацию (если у Вашего устройства она всего одна, Вам вообще не надо беспокоиться о значении конфигурации). К примеру, представим, что у Вас устройство с одним декриптором конфигурации, а его поле bConfigurationValue равно 5 [3], последующие запросы будут работать одинаково:
>>> dev.set_configuration(5)
# или
>>> dev.set_configuration() # мы предполагаем, что конфигурация 5 - первая
# или
>>> cfg = util.find_descriptor(dev, bConfigurationValue=5)
>>> cfg.set()
# или
>>> cfg = util.find_descriptor(dev, bConfigurationValue=5)
>>> dev.set_configuration(cfg)
Вау! Вы можете использовать объект Configuration как параметр для set_configuration! Да, также у него есть метод set для конфигурации самого себя в текущую конфигурацию.
Другая опция, которую Вам нужно или не нужно будет настроить — опция смены интерфейсов. Каждое устройство может иметь только одну активированную конфигурацию в один момент, и у каждой конфигурации может быть больше чем один интерфейс, а Вы можете использовать все интерфейсы в одно и то же время. Вам лучше понять эту концепцию, если Вы думаете о интерфейсе как о логическом устройстве. К примеру, давайте представим многофункциональный принтер, который в одно и то же время и принтер, и сканер. Чтобы не усложнять (или по крайней мере делать настолько просто насколько возможно), давайте будем считать, что у него есть всего одна конфигурация. Т.к. у нас есть принтер и сканер у конфигурации есть 2 интерфейса: один для принтера и один для сканера. Устройство с более чем одним интерфейсом называется композитным устройством. Когда Вы подключаете Ваш многофункциональный принтер к Вашему компьютеру, Операционная Система загрузит два разных драйвера: один для каждого «логического» периферического устройства, которое у Вас есть [4].
Что насчёт альтернытивных настроек интерфейса? Хорошо, что Вы спросили. У интерфейса есть один или более альтернытивных настроек. Интерфейс у которого только одна альтернативная настройка рассматривается как не имеющий альтернативных настроек [5]. Альтернативные настройки для интерфейсов как конфигурации для устройств, то есть на каждый интерфейс у Вас может быть только одна активная альтернативная настройка. К примеру, спецификация USB говорит о том, что у устройства не может быть изохронной контрольной точки в его основной альтернативной настройке [6], так что потоковое устройство должно иметь как минимум две альтернативные настройки, со второй настройкой, имеющей изохронную контрольную точку. Но, в отличии от конфигураций, интерфейсы только с одной альтернативной настройкой не нуждается в настройке [7]. Вы выбираете альтернативную настройку интерфейса с помощью функции set_interface_altsetting:
>>> dev.set_interface_altsetting(interface = 0, alternate_setting = 0)
Предупреждение
Спецификация USB говорит, что устройству позволяется возвращать ошибку в случае, если оно получает запрос SET_INTERFACE к интерфейсу у которого нет дополнительных альтернативных настроек. Так что, если Вы не уверены в том, что интерфейс имеет более одной альтернативной настройки или в том, что он принимает запрос SET_INTERFACE, наиболее безопасным методом будет вызвать set_interface_altsetting внутри блока try-except, как здесь:
try:
dev.set_interface_altsetting(...)
except USBError:
pass
Вы также можете использовать объект Interface как параметр функции, параметры interface и alternate_setting автоматически наследуются от полей bInterfaceNumber и bAlternateSetting. Пример:
>>> intf = find_descriptor(...)
>>> dev.set_interface_altsetting(intf)
>>> intf.set_altsetting() # Воу! У интерфейса тоже есть метод для этого
Предупреждение
Объект Interface должен принадлежать активному дескриптору конфигурации.
Поговори со мной, милая
А теперь для нас настало время понять как взаимодействовать c USB устройствами. У USB есть четыре типа потоков данных: массовый (bulk transfer), прерывающийся (interrupt transfer), изохронный (isochronous transfer) и управляющий (control transfer). Я не планирую объяснять назначение каждого потока и различия между ними. Поэтому я предполагаю, что у Вас есть по крайней мере базовые знания о потоках данных USB.
Управляющий поток данных — единственный поток, структура которого описана в спецификации, остальные просто отправляют и получают необработанные данные с точки зрения USB. Поэтому у Вас есть различные функции для работы с управляющими потоками, а остальные потоки обрабатываются одними и теми же функциями.
Вы можете обратиться к управляющему потоку данных посредством метода ctrl_transfer. Он используется как для исходящих (OUT) так и для входящих (IN) потоков. Направление потока определяет параметр bmRequestType.
Параметры ctrl_transfer практически совпадают со структурой управляющего запроса. Далее следует пример того, как организовывать управляющий поток данных [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
В этом примере предполагается, что наше устройство включает в себя два пользовательских управляющих запроса, которые действуют как loopback pipe. То, что Вы пишете с сообщением CTRL_LOOPBACK_WRITE, Вы можете прочитать с сообщением CTRL_LOOPBACK_READ.
Первые четыре параметра — bmRequestType, bmRequest, wValue и wIndex — поля стандартной структуры управляющего потока. Пятый параметр — это либо пересылаемые данные для исходящего потока данных или кол-во считываемых данных во входящем потоке. Пересылаемые данные могут быть любым типом последовательности, которая может быть подана в качестве параметра на вход метода __init__ для массива. Если нет пересылаемых данных параметр должен иметь значение None (или 0 в случае входящего потока данных). Есть ещё один опциональный параметр указывающий таймаут операции. Если Вы не передаете его, будет использоваться таймаут по-умолчанию (больше об этом дальше). В исходящем потоке данных возвращаемое значение — это количество байтов, реально посылаемое устройству. Во входящем потоке возвращаемое значение — массива со считанными данными.
Для других потоков Вы можете использовать методы write и read, соответственно, чтобы записывать и считывать данные. Вам не нужно беспокоиться о типе потока — он автоматически определяется по адресу контрольной точки. Вот наш пример loopback при условии, что у нас есть loopback pipe в контрольной точке 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
Первый и третий параметры одинаковы для обоих методов — это адрес контрольной точки и таймаут, соответственно. Второй параметр — пересылаемые данные (write) или количество байтов для считывания (read). Возвращенными данными будут либо экземпляр объекта массива для метода read, либо количество записанных байтов для метода write.
С бета 2 версии вместо количества байтов, Вы можете передать для read или ctrl_transfer объект массива, в который данные будут считываться. В этом случае, количество байтов для считывания будет длиной массива умноженной на значение array.itemsize.
В ctrl_transfer, параметр timeout опционален. Когда timeout опущено, используется свойство Device.default_timeout как операционный таймаут.
Контролируй себя
Кроме функций потоков данных модуль usb.control предоставляет функции, которые включают в себя стандартные управляющие запросы USB, а в модуле usb.util есть удобная функция get_string специально выводящая дескрипторы строк.
Дополнительные темы
За каждой великой абстракцией стоит великая реализация
Раньше был только libusb. Потом пришел libusb 1.0 и у нас были libusb 0.1 и 1.0. После этого мы создали OpenUSB и сейчас мы живем в Вавилонской Башне USB-библиотек [9]. Как PyUSB справляется с этим? Что ж, PyUSB — демократичная библиотека, Вы можете выбрать какую хотите бибилиотеку. На самом деле Вы можете написать вашу собственную библиотеку USB с нуля и сказать PyUSB использовать её.
Функция find имеет ещё один параметр, о котором я Вам не рассказал. Это параметр backend. Если Вы его не передаете — будет использоваться один из встроенных бэкендов. Бэкенд — это объект унаследованный от usb.backend.IBackend, ответственный за введение специфического для операционной системы хлама USB. Как Вы могли догадаться, встроенные libusb 0.1, libusb 1.0 и OpenUSB — бэкенды.
Вы можете написать свой собственный бэкенд и использовать его. Просто наследуйте от IBackend и включите необходимые методы. Вам может понадобиться посмотреть в документацию usb.backend, чтобы понять как это делается.
Не будьте эгоистичны
У Python есть то, что мы называем автоматическое управление памятью. Это значит, что виртуальная машина будет решать когда выгрузить объекты из памяти. Под капотом PyUSB управляет всеми низко-уровневыми ресурсами, с которыми необходимо работать (утверждение интерфейса, регулировки устройства, и т.д.) и большинству пользователей не нужно беспокоиться об этом. Но, из-за непредопределенной природы автоматического уничтожения объектов Python’ом, пользователи не могут предсказать когда выделенные ресурсы будут освобождены. Некоторые приложения нуждаются в том, чтобы выделить и освободить ресурсы детерминировано. Для таких приложений модуль usb.util предоставляет функции для взаимодействия с управлением ресурсами.
Если Вы хотите запрашивать и освобождать интерфейсы вручную, Вы можете использовать функции claim_interface и release_interface. Функция claim_interface будет запрашивать указанный интерфейс, если устройство до сих пор этого не сделало. Если устройство уже запросило интерфейс, она ничего не делает. Так же release_interface будет освобождать указанный интерфейс, если он запрошен. Если интерфейс не запрошен, она ничего не делает. Вы можете использовать ручное запрашивание интерфейсов, чтобы решить описанную в документации libusb проблему выбора конфигурации.
Если Вы хотите освободить все ресурсы выделенные объектом устройства (включая запрошенные интерфейсы), Вы можете использовать функцию dispose_resources. Она освобождает все выделенные ресурсы и переводит объект устройства (но не в аппаратные средства устройства самого по себе) в то состояние, в котором оно было возвращено после использования функции find.
Определение библиотек вручную
В общем, бэкенд — это обертка над общей библиотекой, которая реализует API для доступа к USB. По-умолчанию, бэкенд использует ctypes функцию find_library (). На Linux и других Unix-подобных Операционных Системах, find_library пытается запустить внешние программы (такие как /sbin/ldconfig, gcc и objdump) в целях нахождения файла библиотеки.
В системах в которых эти программы отсутствуют и/или кэш библиотек отключен, эта функция не может использоваться. Чтобы преодолеть ограничения, PyUSB позволяет Вам подавать пользовательскую функцию find_library () на бэкенд.
Примером такого сценария будет:
>>> 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)
Заметьте, что find_library — аргумент для функции get_backend (), в котором Вы поставляете функцию, которая ответственная за поиск правильной библиотеки для бэкенда.
Правила старой школы
Если Вы пишите приложение используя старые API PyUSB (0.что-то-там), Вы можете спрашивать себя, нужно ли Вам обновить Ваш код, чтобы использовать новый API. Что ж, Вам стоит это сделать, но это не обязательно. PyUSB 1.0 идет вместе с модулем совместимости usb.legacy. Он включает в себя старое API на основе нового API. «Что ж, должен ли я просто заменить мою строчку import usb на import usb.legacy as usb чтобы заставить моё приложение работать?», спросите Вы. Ответ — да, это будет работать, но это не обязательно. Если Вы запустите свое приложение неизмененным оно будет работать, потому что строчка import usb импортирует все публичные символы из usb.legacy. Если Вы сталкиваетесь с проблемой — скорее всего Вы нашли баг.
Помогите мне, пожалуйста
Если Вас нужна помощь, не пишите мне на e-mail, для этого есть список рассылки. Инструкции по подписке могут быть найдены на сайте PyUSB.
[1] Когда я пишу True или False (с большой буквы), я имею ввиду соответственные значения языка Python. А когда я говорю истинно (true) или ложно (false), я имею ввиду любое выражение Python, которое расценивается как истинное или ложное. (Данное сходство имело место в оригинале и помогает понять понятия истинного и ложного в переводе. — Прим.пер.):
[2] Смотрите конкретную документацию бэкенда.
[3] Спецификация USB не навязывает какое-либо определенное значение для значения конфигурации. То же истинно для номеров интерфейса и альтернативной настройки.
[4] На самом деле всё немного сложнее, но этого просто объяснения для нас хватит.
[5] Я знаю, что это звучит странно.
[6] Это потому, что если нету пропускной способности для изохронных потоков данных во время конфигурации устройства, оно может быть успешно пронумеровано.
[7] Этого не происходит для конфигурации, потому что устройству разрешено быть в несконфигурированном состоянии.
[8] В PyUSB управляющие потоки данных обращаются к контрольной точке 0. Очень очень очень редко устройство имеет альтернативную управляющую контрольную точку (Я никогда не встречал такого устройства).
[9] Это просто шутка, не принимайте это всерьез. Большой выбор лучше, чем без выбора.