Главная » Хабрахабр » [Перевод] Программирование с PyUSB 1.0

[Перевод] Программирование с 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.

Он использует модуль logging. Вы также можете использовать функции лога PyUSB. Для его использования определите переменную окружения PYUSB_DEBUG с одним из следующих уровней логирования: critical, error, warning, info или debug.

Если хотите, Вы можете перенаправить сообщения лога в файл определив переменную окружения PYUSB_LOG_FILENAME. По-умолчанию сообщения посылаются в sys.stderr. Если её значение — корректный путь к файлу, сообщения будут записываться туда, иначе они будут посылаться в sys.stderr.

Где ты?

Функция find() в модуле core используется чтобы найти и пронумеровать устройства присоединенные к системе. К примеру, скажем что у нашего устройства есть vendor ID со значением 0xfffe и product ID равный 0x0001. Если нам нужно найти это устройство мы сделаем так:

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 вместо этого возвратит список из всех устройств подходящих по критериям. Вот и всё! Просто, не правда ли?

Нет! Мы закончили? Так что, чтобы по-настоящему найти все принтеры подключенные к системе, нам нужно будет перебрать все конфигурации, а также все интерфейсы и проверить — выставлено ли у одного из интерфейсов в bInterfaceClass значение 7. Я ещё не всё рассказал: многие устройства на самом деле ставят свою информацию о классе в Interface Descriptor вместо Device Descriptor. Ответ: да, он есть. Если Вы программист как и я Вы можете задаться вопросом: есть ли путь полегче с помощью которого это можно реализовать. Для начала давайте посмотрим на готовый код нахождения всех подключенных принтеров:

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.

Имеем дело с множественными идентичными устройствами

Как Вы можете различать их? Иногда у Вас может быть два идентичных устройства подсоединенных к компьютеру. Прежде всего, стоит сказать, что эти атрибуты идут от бэкенда, а бэкенд может и не поддерживать их — в этом случае они выставлены на None. Объекты Device идут с двумя дополнительными атрибутами, которые не являются частью спецификации USB, но очень полезны: атрибуты bus и address. Тем не менее эти атрибуты представляют номер и адрес шины устройства и, как Вы могли уже догадаться, могут быть использованы для того, чтобы различать два устройства с одинаковыми значениями атрибутов 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], так что потоковое устройство должно иметь как минимум две альтернативные настройки, со второй настройкой, имеющей изохронную контрольную точку. Альтернативные настройки для интерфейсов как конфигурации для устройств, то есть на каждый интерфейс у Вас может быть только одна активная альтернативная настройка. Вы выбираете альтернативную настройку интерфейса с помощью функции set_interface_altsetting: Но, в отличии от конфигураций, интерфейсы только с одной альтернативной настройкой не нуждается в настройке [7].

>>> dev.set_interface_altsetting(interface = 0, alternate_setting = 0)

Предупреждение

Так что, если Вы не уверены в том, что интерфейс имеет более одной альтернативной настройки или в том, что он принимает запрос SET_INTERFACE, наиболее безопасным методом будет вызвать set_interface_altsetting внутри блока try-except, как здесь: Спецификация USB говорит, что устройству позволяется возвращать ошибку в случае, если оно получает запрос SET_INTERFACE к интерфейсу у которого нет дополнительных альтернативных настроек.

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.

Он используется как для исходящих (OUT) так и для входящих (IN) потоков. Вы можете обратиться к управляющему потоку данных посредством метода ctrl_transfer. Направление потока определяет параметр bmRequestType.

Далее следует пример того, как организовывать управляющий поток данных [8]: Параметры ctrl_transfer практически совпадают со структурой управляющего запроса.

>>> 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 — поля стандартной структуры управляющего потока. Если нет пересылаемых данных параметр должен иметь значение None (или 0 в случае входящего потока данных). Пересылаемые данные могут быть любым типом последовательности, которая может быть подана в качестве параметра на вход метода __init__ для массива. Если Вы не передаете его, будет использоваться таймаут по-умолчанию (больше об этом дальше). Есть ещё один опциональный параметр указывающий таймаут операции. Во входящем потоке возвращаемое значение — массива со считанными данными. В исходящем потоке данных возвращаемое значение — это количество байтов, реально посылаемое устройству.

Вам не нужно беспокоиться о типе потока — он автоматически определяется по адресу контрольной точки. Для других потоков Вы можете использовать методы 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.

В этом случае, количество байтов для считывания будет длиной массива умноженной на значение array.itemsize. С бета 2 версии вместо количества байтов, Вы можете передать для read или ctrl_transfer объект массива, в который данные будут считываться.

Когда timeout опущено, используется свойство Device.default_timeout как операционный таймаут. В ctrl_transfer, параметр timeout опционален.

Контролируй себя

Кроме функций потоков данных модуль usb.control предоставляет функции, которые включают в себя стандартные управляющие запросы USB, а в модуле usb.util есть удобная функция get_string специально выводящая дескрипторы строк.

Дополнительные темы

За каждой великой абстракцией стоит великая реализация

Раньше был только libusb. Потом пришел libusb 1.0 и у нас были libusb 0.1 и 1.0. После этого мы создали OpenUSB и сейчас мы живем в Вавилонской Башне USB-библиотек [9]. Как PyUSB справляется с этим? Что ж, PyUSB — демократичная библиотека, Вы можете выбрать какую хотите бибилиотеку. На самом деле Вы можете написать вашу собственную библиотеку USB с нуля и сказать PyUSB использовать её.

Это параметр backend. Функция find имеет ещё один параметр, о котором я Вам не рассказал. Бэкенд — это объект унаследованный от usb.backend. Если Вы его не передаете — будет использоваться один из встроенных бэкендов. Как Вы могли догадаться, встроенные libusb 0. IBackend, ответственный за введение специфического для операционной системы хлама USB. 0 и OpenUSB — бэкенды. 1, libusb 1.

Просто наследуйте от IBackend и включите необходимые методы. Вы можете написать свой собственный бэкенд и использовать его. Вам может понадобиться посмотреть в документацию usb.backend, чтобы понять как это делается.

Не будьте эгоистичны

У Python есть то, что мы называем автоматическое управление памятью. Это значит, что виртуальная машина будет решать когда выгрузить объекты из памяти. Под капотом PyUSB управляет всеми низко-уровневыми ресурсами, с которыми необходимо работать (утверждение интерфейса, регулировки устройства, и т.д.) и большинству пользователей не нужно беспокоиться об этом. Но, из-за непредопределенной природы автоматического уничтожения объектов Python'ом, пользователи не могут предсказать когда выделенные ресурсы будут освобождены. Некоторые приложения нуждаются в том, чтобы выделить и освободить ресурсы детерминировано. Для таких приложений модуль usb.util предоставляет функции для взаимодействия с управлением ресурсами.

Функция claim_interface будет запрашивать указанный интерфейс, если устройство до сих пор этого не сделало. Если Вы хотите запрашивать и освобождать интерфейсы вручную, Вы можете использовать функции claim_interface и release_interface. Так же release_interface будет освобождать указанный интерфейс, если он запрошен. Если устройство уже запросило интерфейс, она ничего не делает. Вы можете использовать ручное запрашивание интерфейсов, чтобы решить описанную в документации libusb проблему выбора конфигурации. Если интерфейс не запрошен, она ничего не делает.

Она освобождает все выделенные ресурсы и переводит объект устройства (но не в аппаратные средства устройства самого по себе) в то состояние, в котором оно было возвращено после использования функции find. Если Вы хотите освободить все ресурсы выделенные объектом устройства (включая запрошенные интерфейсы), Вы можете использовать функцию dispose_resources.

Определение библиотек вручную

В общем, бэкенд — это обертка над общей библиотекой, которая реализует 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.

А когда я говорю истинно (true) или ложно (false), я имею ввиду любое выражение Python, которое расценивается как истинное или ложное. [1] Когда я пишу True или False (с большой буквы), я имею ввиду соответственные значения языка Python. — Прим.пер.): (Данное сходство имело место в оригинале и помогает понять понятия истинного и ложного в переводе.

[2] Смотрите конкретную документацию бэкенда.

То же истинно для номеров интерфейса и альтернативной настройки. [3] Спецификация USB не навязывает какое-либо определенное значение для значения конфигурации.

[4] На самом деле всё немного сложнее, но этого просто объяснения для нас хватит.

[5] Я знаю, что это звучит странно.

[6] Это потому, что если нету пропускной способности для изохронных потоков данных во время конфигурации устройства, оно может быть успешно пронумеровано.

[7] Этого не происходит для конфигурации, потому что устройству разрешено быть в несконфигурированном состоянии.

Очень очень очень редко устройство имеет альтернативную управляющую контрольную точку (Я никогда не встречал такого устройства). [8] В PyUSB управляющие потоки данных обращаются к контрольной точке 0.

Большой выбор лучше, чем без выбора. [9] Это просто шутка, не принимайте это всерьез.


Оставить комментарий

Ваш email нигде не будет показан
Обязательные для заполнения поля помечены *

*

x

Ещё Hi-Tech Интересное!

Два успеха частной космонавтики

На прошлой неделе произошло два достаточно важных события для частной космонавтики. Прежде всего, два пилота Virgin Galactic могут сверлить дырки в своих костюмах под значки астронавтов — поднявшийся 13 декабря до 82,7 км SpaceShipTwo оказался выше линии 50 миль, которая ...

Дайджест свежих материалов из мира фронтенда за последнюю неделю №343 (10 — 16 декабря 2018)

Предлагаем вашему вниманию подборку с ссылками на новые материалы из области фронтенда и около него.     Медиа    |    Веб-разработка    |    CSS    |    Javascript    |    Браузеры Медиа • Подкаст «Frontend Weekend» #83 – Илья Климов о том, как и зачем был создан образовательный проект JavaScript.Ninja• Девшахта #61: TypeScript и его ...