Del traductor :
Esta es una traducción del manual de Programación con PyUSB 1.0
Esta guía fue escrita por los desarrolladores de PyUSB, pero rápidamente ejecutándose a través de los commits, creo que walac es el autor principal.Déjame presentarme
PyUSB 1.0 es una biblioteca de
Python que proporciona un fácil acceso a
USB . PyUSB proporciona varias funciones:
- 100% escrito en Python:
A diferencia de las versiones 0.x que se escribieron en C, la versión 1.0 se escribe en Python. Esto permite a los programadores de Python sin experiencia en C comprender mejor cómo funciona PyUSB. - Neutralidad de plataforma:
La versión 1.0 incluye un esquema de back-end front-end. Aísla la API de los detalles de implementación específicos del sistema. La interfaz IBackend conecta estas dos capas. PyUSB viene con backends integrados para libusb 0.1, libusb 1.0 y OpenUSB. Puede escribir su propio backend si lo desea. - Portabilidad:
PyUSB debe ejecutarse en cualquier plataforma con Python> = 2.4, ctypes y al menos uno de los backends integrados compatibles. - Simplicidad:
¡La interacción con un dispositivo USB nunca ha sido tan fácil! USB es un protocolo complejo, y PyUSB tiene buenos ajustes preestablecidos para las configuraciones más comunes. - Soporte de engranaje isócrono:
PyUSB admite transferencias isócronas si el backend subyacente las admite.
Aunque PyUSB hace que la programación de USB sea menos dolorosa, este tutorial asume que tiene un conocimiento mínimo del protocolo USB. Si no sabe nada sobre USB, le recomiendo
el excelente libro
USB Complete de Jan Axelson.
Basta de hablar, ¡vamos a escribir el código!
Quien es quien
Para comenzar, demos una descripción de los módulos PyUSB. Todos los módulos PyUSB están bajo
usb , con los siguientes módulos:
Modulo | Descripción |
---|
núcleo | El módulo USB principal. |
util | Funciones auxiliares |
control | Solicitudes de gestión estándar. |
legado | Capa de compatibilidad de la versión 0.x. |
backend | Un subpaquete que contiene backends integrados. |
Por ejemplo, para importar un módulo
central , ingrese lo siguiente:
>>> import usb.core >>> dev = usb.core.find()
Bueno, empecemos
El siguiente es un programa simple que envía la cadena 'prueba' al primer origen de datos encontrado (punto final OUT):
import usb.core import usb.util
Las dos primeras líneas importan los módulos del paquete PyUSB.
usb.core es el módulo principal, y
usb.util contiene funciones auxiliares. El siguiente comando busca nuestro dispositivo y devuelve una instancia del objeto si lo encuentra. Si no, devuelve
Ninguno . A continuación, establecemos la configuración que utilizaremos. Nota: la ausencia de argumentos significa que la configuración deseada se estableció de forma predeterminada. Como verá, muchas características de PyUSB tienen configuraciones predeterminadas para los dispositivos más comunes. En este caso, se establece la primera configuración encontrada.
Luego, buscamos el punto final en el que estamos interesados. Lo estamos buscando dentro de la primera interfaz que tenemos. Después de encontrar este punto, le enviamos datos.
Si conocemos la dirección del punto final de antemano, simplemente podemos llamar a la función de
escritura del objeto del dispositivo:
dev.write(1, 'test')
Aquí escribimos la cadena 'prueba' en el punto de interrupción en la dirección
1 . Todas estas funciones se discutirán mejor en las siguientes secciones.
Que esta mal
Cada función en PyUSB arroja una excepción en caso de error. Además de las
excepciones estándar de Python , PyUSB define
usb.core.USBError para errores relacionados con USB.
También puede usar las funciones de registro de PyUSB. Utiliza el módulo de
registro . Para usarlo, defina la
variable de entorno
PYUSB_DEBUG con uno de los siguientes niveles de registro:
crítico ,
error ,
advertencia ,
información o
depuración .
Por defecto, los mensajes se envían a
sys.stderr . Si lo desea, puede redirigir los mensajes de registro a un archivo definiendo la variable de entorno
PYUSB_LOG_FILENAME . Si su valor es la ruta correcta al archivo, los mensajes se escribirán allí; de lo contrario, se enviarán a
sys.stderr .
Donde estas
La función
find () en el módulo
central se usa para buscar y numerar dispositivos conectados al sistema. Por ejemplo, supongamos que nuestro dispositivo tiene una ID de proveedor con un valor de 0xfffe y una ID de producto de 0x0001. Si necesitamos encontrar este dispositivo haremos esto:
import usb.core dev = usb.core.find(idVendor=0xfffe, idProduct=0x0001) if dev is None: raise ValueError('Our device is not connected')
Eso es todo, la función devolverá el objeto
usb.core.Device que representa nuestro dispositivo. Si no se encuentra el dispositivo, devolverá
Ninguno . De hecho, puede usar cualquier campo de la clase
Descriptor de dispositivo que desee. Por ejemplo, ¿qué pasa si queremos saber si hay una impresora USB conectada al sistema? Es muy fácil:
7 es el código para la clase de impresora según la especificación USB. Oh, espera, ¿y si quiero numerar todas las impresoras disponibles? No hay problema
Que paso Bueno, es hora de una pequeña explicación ...
find tiene un parámetro llamado
find_all y su valor predeterminado es False. Cuando es falso
[1] ,
find devolverá el primer dispositivo que coincida con los criterios especificados (hablaremos de esto pronto). Si pasa un valor
verdadero al parámetro,
find en su lugar devolverá una lista de todos los dispositivos que coinciden con los criterios. Eso es todo! Simple, ¿verdad?
Hemos terminado? No! Todavía no lo he contado todo: muchos dispositivos ponen su información de clase en el
Descriptor de interfaz en lugar del
Descriptor de dispositivo. Entonces, para encontrar verdaderamente todas las impresoras conectadas al sistema, necesitaremos revisar todas las configuraciones, así como todas las interfaces, y verificar si una de las interfaces está configurada en bInterfaceClass 7. Si usted es un
programador como yo, puede preguntarse: ¿Hay alguna manera más fácil de implementar esto? Respuesta: sí, lo es. Para comenzar, veamos el código listo para encontrar todas las impresoras conectadas:
import usb.core import usb.util import sys class find_class(object): def __init__(self, class_): self._class = class_ def __call__(self, device):
El parámetro
custom_match acepta cualquier objeto llamado que recibe el objeto del dispositivo. Debe devolver verdadero para un dispositivo adecuado y falso para un inadecuado. También puede combinar
custom_match con campos de dispositivo si desea:
Aquí nos interesan las impresoras del proveedor 0xfffe.
Describete a ti mismo
Ok, encontramos nuestro dispositivo, pero antes de interactuar con él, nos gustaría saber más sobre él. Bueno, ya sabes, configuraciones, interfaces, puntos finales, tipos de flujos de datos ...
Si tiene un dispositivo, puede acceder a cualquier campo del descriptor del dispositivo como propiedades del objeto:
>>> dev.bLength >>> dev.bNumConfigurations >>> dev.bDeviceClass >>>
Para acceder a las configuraciones disponibles en el dispositivo, puede iterar el dispositivo:
for cfg in dev: sys.stdout.write(str(cfg.bConfigurationValue) + '\n')
Del mismo modo, puede iterar la configuración para acceder a las interfaces, así como iterar las interfaces para acceder a sus puntos de control. Cada tipo de objeto tiene campos del descriptor correspondiente como atributos. Echa un vistazo a un ejemplo:
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')
También puede usar índices para acceso aleatorio a descriptores, como aquí:
>>>
Como puede ver, los índices se cuentan desde 0. ¡Pero espere! Hay algo extraño en la forma en que obtengo acceso a la interfaz ... Sí, tiene razón, el índice de Configuración toma una serie de dos valores, de los cuales el primero es el índice de la Interfaz, y el segundo es una configuración alternativa. En general, para acceder a la primera interfaz, pero con la segunda configuración, escribiremos
cfg [(0,1)] .
Ahora es el momento de aprender una forma poderosa de buscar descriptores, una útil función
find_descriptor . Ya lo hemos visto en el ejemplo de búsqueda de impresoras.
find_descriptor funciona casi igual que
find , con dos excepciones:
- find_descriptor recibe como primer parámetro el descriptor de origen que buscará.
- No hay ningún parámetro de fondo en él [2]
Por ejemplo, si tenemos un descriptor de configuración de
cfg y queremos encontrar todas las configuraciones alternativas para la interfaz 1, haremos esto:
import usb.util alt = usb.util.find_descriptor(cfg, find_all=True, bInterfaceNumber=1)
Tenga en cuenta que
find_descriptor está en el módulo
usb.util . También acepta el parámetro
custom_match descrito anteriormente.
Nos ocupamos de múltiples dispositivos idénticosA veces puede tener dos dispositivos idénticos conectados a una computadora. ¿Cómo puedes distinguir entre ellos?
Los objetos del
dispositivo vienen con dos atributos adicionales que no forman parte de la especificación USB, pero son muy útiles: los atributos de
bus y
dirección . En primer lugar, vale la pena decir que estos atributos provienen del backend, y el backend puede no admitirlos, en este caso están configurados en
Ninguno . Sin embargo, estos atributos representan el número y la dirección del bus del dispositivo y, como habrás adivinado, se pueden usar para distinguir entre dos dispositivos con los mismos valores de atributo
idVendor e
idProduct .
¿Como debería trabajar?
Después de conectarse, los dispositivos USB deben configurarse utilizando algunas consultas estándar. Cuando comencé a estudiar la especificación
USB , los descriptores, configuraciones, interfaces, configuraciones alternativas, tipos de transferencia y todo eso me desanimaron ... Y lo peor de todo es que no puedes ignorarlos: ¡el dispositivo no funciona sin establecer la configuración, incluso si es una! PyUSB está tratando de hacer tu vida lo más simple posible. Por ejemplo, después de recibir el objeto de su dispositivo, en primer lugar, antes de interactuar con él, debe enviar una solicitud
set_configuration . El parámetro de configuración para esta consulta que le interesa es
bConfigurationValue . La mayoría de los dispositivos no tienen más de una configuración, y el seguimiento del valor de configuración a usar es molesto (aunque la mayoría del código que vi acaba de codificar esto). Por lo tanto, en PyUSB, simplemente puede enviar una solicitud
set_configuration sin argumentos. En este caso, instalará la primera configuración encontrada (si su dispositivo tiene solo una, no necesita preocuparse por el valor de la configuración). Por ejemplo, suponga que tiene un dispositivo con un descriptor de configuración, y su campo bConfigurationValue es 5
[3] , las consultas posteriores funcionarán igual:
>>> dev.set_configuration(5)
Wow! ¡Puede usar el objeto de
configuración como parámetro para
configurar_configuración ! Sí, también tiene un método
establecido para configurarse en la configuración actual.
Otra opción que necesita o no necesitará configurar es la opción de cambiar las interfaces. Cada dispositivo puede tener solo una configuración activada a la vez, y cada configuración puede tener más de una interfaz, y puede usar todas las interfaces al mismo tiempo. Comprenderá mejor este concepto si piensa en la interfaz como un dispositivo lógico. Por ejemplo, imaginemos una impresora multifunción, que al mismo tiempo es tanto una impresora como un escáner. Para no complicarlo (o al menos hacerlo lo más simple posible), supongamos que solo tiene una configuración. Porque Tenemos una impresora y un escáner, la configuración tiene 2 interfaces: una para la impresora y otra para el escáner. Un dispositivo con más de una interfaz se llama dispositivo compuesto. Cuando conecta su impresora multifunción a su computadora, el sistema operativo cargará dos controladores diferentes: uno para cada dispositivo periférico "lógico" que tenga
[4]¿Qué pasa con la configuración de interfaz alternativa? Qué bueno que preguntaste. Una interfaz tiene una o más configuraciones alternativas. Se considera que una interfaz con solo una configuración alternativa no tiene configuraciones alternativas
[5] Configuraciones alternativas para interfaces como una configuración para dispositivos, es decir, para cada interfaz solo puede tener una configuración alternativa activa. Por ejemplo, la especificación USB sugiere que un dispositivo no puede tener un punto de control isócrono en su configuración alternativa primaria
[6] , de modo que el dispositivo de transmisión debe tener al menos dos configuraciones alternativas, con una segunda configuración que tenga un punto de control isócrono. Pero, a diferencia de las configuraciones, las interfaces con una sola configuración alternativa no necesitan ser configuradas
[7] Seleccione una configuración de interfaz alternativa utilizando la función
set_interface_altsetting :
>>> dev.set_interface_altsetting(interface = 0, alternate_setting = 0)
AdvertenciaLa especificación USB dice que el dispositivo puede devolver un error si recibe una solicitud SET_INTERFACE para una interfaz que no tiene configuraciones alternativas adicionales. Entonces, si no está seguro de que la interfaz tiene más de una configuración alternativa o de que acepta la solicitud SET_INTERFACE, el método más seguro es llamar a
set_interface_altsetting dentro del bloque try-except, como aquí:
try: dev.set_interface_altsetting(...) except USBError: pass
También puede usar el objeto
Interface como parámetro de función, la
interfaz y
los parámetros
alternate_setting se heredan automáticamente de los
campos bInterfaceNumber y
bAlternateSetting . Un ejemplo:
>>> intf = find_descriptor(...) >>> dev.set_interface_altsetting(intf) >>> intf.set_altsetting()
AdvertenciaEl objeto de
interfaz debe pertenecer al descriptor de configuración activo.
Háblame cariño
Y ahora es hora de que comprendamos cómo interactuar con dispositivos USB. USB tiene cuatro tipos de flujos de datos: transferencia masiva, transferencia de interrupción, transferencia isócrona y transferencia de control. No planeo explicar el propósito de cada hilo y las diferencias entre ellos. Por lo tanto, supongo que tiene al menos conocimientos básicos de flujos de datos USB.
El flujo de datos de control es el único flujo cuya estructura se describe en la especificación, el resto simplemente envía y recibe datos sin procesar desde un punto de vista USB. Por lo tanto, tiene varias funciones para trabajar con flujos de control, y el resto de los flujos son procesados por las mismas funciones.
Puede acceder a la secuencia de datos de control utilizando el método
ctrl_transfer . Se utiliza tanto para flujos salientes (OUT) como entrantes (IN). La dirección del flujo está determinada por el parámetro
bmRequestType .
Los parámetros
ctrl_transfer casi coinciden con la estructura de la solicitud de control. El siguiente es un ejemplo de cómo organizar un flujo de datos de control.
[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
Este ejemplo supone que nuestro dispositivo incluye dos solicitudes de control de usuario que actúan como una tubería de bucle invertido. Lo que escribes con el mensaje
CTRL_LOOPBACK_WRITE , puedes leerlo con el mensaje
CTRL_LOOPBACK_READ .
Los primeros cuatro parámetros (
bmRequestType ,
bmRequest ,
wValue y
wIndex ) son los campos de la estructura estándar de la secuencia de control. El quinto parámetro son los datos que se transfieren para el flujo de datos salientes o el número de datos que se leen en el flujo entrante. Los datos transmitidos pueden ser cualquier tipo de secuencia, que se puede alimentar como parámetro a la entrada del método
__init__ para la
matriz . Si no se transfieren datos, el parámetro debe establecerse en
Ninguno (o 0 en el caso de un flujo de datos entrante). Hay otro parámetro opcional que indica el tiempo de espera de la operación. Si no lo pasa, se usará el tiempo de espera predeterminado (más sobre esto más adelante). En el flujo de datos salientes, el valor de retorno es el número de bytes realmente enviados al dispositivo. En la secuencia entrante, el valor de retorno es una
matriz con los datos leídos.
Para otras transmisiones, puede usar los métodos de
escritura y
lectura , respectivamente, para escribir y leer datos. No necesita preocuparse por el tipo de transmisión: la dirección del punto de control lo detecta automáticamente. Aquí está nuestro ejemplo de bucle invertido, siempre que tengamos una tubería de bucle invertido en el punto de interrupción 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
El primer y el tercer parámetro son los mismos para ambos métodos: esta es la dirección del punto de control y el tiempo de espera, respectivamente. El segundo parámetro son los datos transmitidos (escritura) o el número de bytes a leer (leer). Los datos devueltos serán una instancia de un objeto de
matriz para el método de
lectura o el número de bytes escritos para el método de
escritura .
Con las versiones beta 2, en lugar de la cantidad de bytes, puede pasar para
leer o
ctrl_transferir un objeto de
matriz en el que se leerán los datos. En este caso, el número de bytes a leer será la longitud de la matriz multiplicada por el valor de
array.itemsize .
En
ctrl_transfer , el parámetro de
tiempo de espera es opcional. Cuando
se omite el
tiempo de espera , la propiedad
Device.default_timeout se
usa como tiempo de espera operativo.
Contrólate
Además de las funciones de
flujo de datos, el módulo
usb.control proporciona funciones que incluyen solicitudes de control USB estándar, y el módulo
usb.util tiene una conveniente función
get_string que muestra específicamente descriptores de línea.
Temas adicionales
Detrás de cada gran abstracción hay una gran realización
Anteriormente, solo había
libusb . Luego vino libusb 1.0 y teníamos libusb 0.1 y 1.0. Después de eso, creamos
OpenUSB y ahora vivimos en la
Torre de Babel de la Biblioteca USB
[9] ¿Cómo maneja PyUSB esto? Bueno, PyUSB es una biblioteca democrática, puedes elegir qué biblioteca quieres. De hecho, puede escribir su propia biblioteca USB desde cero y decirle a PyUSB que la use.
La función de
búsqueda tiene otro parámetro, del que no te hablé. Este es el parámetro de fondo. Si no lo transfiere, se utilizará uno de los backends integrados. Un backend es un objeto heredado de
usb.backend.IBackend , responsable de la introducción de basura USB específica del sistema operativo. Como habrás adivinado, el libusb 0.1 incorporado, libusb 1.0 y OpenUSB son backends.
Puedes escribir tu propio backend y usarlo. Simplemente herede de
IBackend y habilite los métodos necesarios. Es posible que deba consultar la documentación de
usb.backend para comprender cómo se hace.
No seas egoísta
Python tiene lo que llamamos
administración automática de memoria . Esto significa que la máquina virtual decidirá cuándo descargar objetos de la memoria. Bajo el capó, PyUSB gestiona todos los recursos de bajo nivel con los que necesita trabajar (aprobando la interfaz, ajustando el dispositivo, etc.) y la mayoría de los usuarios no necesitan preocuparse por ello. Pero, debido a la naturaleza indefinida de la destrucción automática de objetos por Python, los usuarios no pueden predecir cuándo se liberarán los recursos asignados. Algunas aplicaciones necesitan asignar y liberar recursos de manera determinista. Para tales aplicaciones, el módulo
usb.util proporciona funciones para interactuar con la gestión de recursos.
Si desea solicitar y liberar interfaces manualmente, puede usar las
funciones Claim_interface y
release_interface .
La función Claim_interface solicitará la interfaz especificada si el dispositivo aún no lo ha hecho. Si el dispositivo ya ha solicitado una interfaz, no hace nada. También release_interface lanzará la interfaz especificada, si se solicita. Si no se solicita la interfaz, no hace nada. Puede usar la consulta manual de la interfaz para resolver el problema de selección de configuración descrito en la documentación de libusb . Si desea liberar todos los recursos asignados por el objeto del dispositivo (incluidas las interfaces solicitadas), puede utilizar la función dispose_resources. Libera todos los recursos asignados y coloca el objeto del dispositivo (pero no en el hardware del propio dispositivo) en el estado en el que se devolvió después de usar la función de búsqueda .Definición de biblioteca manual
En general, un back-end es un contenedor sobre una biblioteca compartida que implementa una API para acceder a USB. Por defecto, los usos de back-end ctypes cuentan find_library () . En Linux y otros sistemas operativos similares a Unix, find_library intenta ejecutar programas externos (como / sbin / ldconfig , gcc y objdump ) para encontrar el archivo de la biblioteca.En los sistemas en los que faltan estos programas y / o el caché de la biblioteca está deshabilitado, esta función no se puede usar. Para superar las limitaciones, PyUSB le permite enviar la función personalizada find_library () al backend.Un ejemplo de tal escenario sería: >>> 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)
Tenga en cuenta que find_library es un argumento para la función get_backend (), en la que proporciona la función responsable de encontrar la biblioteca correcta para el backend.Reglas de la vieja escuela
Si está escribiendo una aplicación utilizando las antiguas API de PyUSB (0. algo allí), es posible que se pregunte si necesita actualizar su código para usar la nueva API. Bueno, deberías hacerlo, pero no es necesario. PyUSB 1.0 viene con el módulo de compatibilidad usb.legacy . Incluye la antigua API basada en la nueva API. "Bueno, ¿debería reemplazar mi línea de importación usb con import usb.legacy como usb para que mi aplicación funcione?", Pregunta. La respuesta es sí, funcionará, pero no es necesario. Si ejecuta su aplicación sin cambios, funcionará porque la línea de importación de USB importa todos los símbolos públicos de usb.legacy. Si encuentra un problema, lo más probable es que haya encontrado un error.Ayudame por favor
Si necesita ayuda, no me escriba un correo electrónico , hay una lista de correo para esto. Las instrucciones de suscripción se pueden encontrar en el sitio web de PyUSB .[1] Cuando escribo Verdadero o Falso (con una letra mayúscula), me refiero a los valores correspondientes del lenguaje Python. Y cuando digo verdadero (verdadero) o falso (falso), me refiero a cualquier expresión de Python que se considere verdadera o falsa. (Esta similitud tuvo lugar en el original y ayuda a comprender los conceptos de verdadero y falso en la traducción. - Nota. ) :[2] Consulte la documentación específica del backend.[3] La especificación USB no impone ningún valor específico en el valor de configuración. Lo mismo es cierto para los números de interfaz y configuraciones alternativas.[4] En realidad, todo es un poco más complicado, pero esto es solo una explicación para nosotros.[5] Sé que esto suena raro.[6] Esto se debe a que si no hay ancho de banda para los flujos de datos isócronos durante la configuración del dispositivo, puede numerarse con éxito.[7] Esto no sucede para la configuración porque el dispositivo puede estar en un estado no configurado.[8] En PyUSB, los flujos de datos de control acceden al punto de control 0. Muy, muy raramente, un dispositivo tiene un punto de control de control alternativo (nunca he conocido un dispositivo de este tipo).[9] Esto es solo una broma, no te lo tomes en serio. Las mejores opciones son mejores que no tener opciones.