Do tradutor :
Esta é uma tradução do manual Programming with PyUSB 1.0
Este guia foi escrito pelos desenvolvedores do PyUSB, mas rapidamente executando os commits, acredito que walac é o autor principal.Deixe-me me apresentar
PyUSB 1.0 é uma biblioteca
Python que fornece fácil acesso ao
USB . O PyUSB fornece várias funções:
- 100% escrito em Python:
Diferentemente das versões 0.x que foram escritas em C, a versão 1.0 é escrita em Python. Isso permite que programadores Python sem experiência em C entendam melhor como o PyUSB funciona. - Neutralidade da plataforma:
A versão 1.0 inclui um esquema de back-end de front-end. Ele isola a API dos detalhes de implementação específicos do sistema. A interface do IBackend conecta essas duas camadas. O PyUSB vem com backends internos para o libusb 0.1, libusb 1.0 e OpenUSB. Você pode escrever seu back-end, se quiser. - Portabilidade:
O PyUSB deve ser executado em qualquer plataforma com Python> = 2.4, ctypes e pelo menos um dos back-ends internos suportados. - Simplicidade:
A interação com um dispositivo USB nunca foi tão fácil! O USB é um protocolo complexo e o PyUSB possui boas predefinições para as configurações mais comuns. - Suporte de engrenagem isócrona:
O PyUSB suporta transferências isócronas se o back-end subjacente as suportar.
Embora o PyUSB torne a programação USB menos dolorosa, este tutorial pressupõe que você tenha um conhecimento mínimo do protocolo USB. Se você não sabe nada sobre USB, recomendo
o excelente livro
USB Complete de Jan Axelson.
Chega de conversa, vamos escrever o código!
Quem é quem
Para começar, vamos dar uma descrição dos módulos PyUSB. Todos os módulos PyUSB estão sob
usb , com os seguintes módulos:
Módulo | Descrição do produto |
---|
núcleo | O principal módulo USB. |
util | Funções auxiliares. |
controlar | Solicitações de gerenciamento padrão. |
legado | Camada de compatibilidade da versão 0.x. |
back-end | Um subpacote contendo back-end internos. |
Por exemplo, para importar um módulo
principal , digite o seguinte:
>>> import usb.core >>> dev = usb.core.find()
Bem, vamos começar
A seguir, um programa simples que envia a string 'test' para a primeira fonte de dados encontrada (ponto de extremidade OUT):
import usb.core import usb.util
As duas primeiras linhas importam os módulos do pacote PyUSB.
usb.core é o módulo principal e
usb.util contém funções auxiliares. O comando a seguir procura nosso dispositivo e retorna uma instância do objeto, se encontrar. Caso contrário, retorna
Nenhum . Em seguida, definimos a configuração que usaremos. Nota: a ausência de argumentos significa que a configuração desejada foi definida por padrão. Como você verá, muitos recursos do PyUSB têm configurações padrão para os dispositivos mais comuns. Nesse caso, a primeira configuração encontrada é definida.
Em seguida, procuramos o ponto final no qual estamos interessados. Estamos procurando dentro da primeira interface que temos. Depois de encontrarmos esse ponto, enviamos dados para ele.
Se soubermos o endereço do terminal com antecedência, podemos simplesmente chamar a função de
gravação do objeto do dispositivo:
dev.write(1, 'test')
Aqui, escrevemos a string 'test' no ponto de interrupção no endereço
1 . Todas essas funções serão discutidas melhor nas seções a seguir.
O que está errado?
Cada função no PyUSB lança uma exceção em caso de erro. Além das
exceções padrão do Python , o PyUSB define
usb.core.USBError para erros relacionados ao USB.
Você também pode usar as funções de log do PyUSB. Ele usa o módulo de
log . Para usá-lo, defina a
variável de ambiente
PYUSB_DEBUG com um dos seguintes níveis de log:
crítico ,
erro ,
aviso ,
informações ou
depuração .
Por padrão, as mensagens são enviadas para
sys.stderr . Se desejar, você pode redirecionar as mensagens de log para um arquivo, definindo a variável de ambiente
PYUSB_LOG_FILENAME . Se o seu valor for o caminho correto para o arquivo, as mensagens serão gravadas nele, caso contrário, serão enviadas para
sys.stderr .
Onde voce esta
A função
find () no módulo
principal é usada para localizar e numerar dispositivos conectados ao sistema. Por exemplo, digamos que nosso dispositivo tenha um ID de fornecedor com o valor 0xfffe e um ID de produto 0x0001. Se precisarmos encontrar este dispositivo, faremos o seguinte:
import usb.core dev = usb.core.find(idVendor=0xfffe, idProduct=0x0001) if dev is None: raise ValueError('Our device is not connected')
Isso é tudo, a função retornará o objeto
usb.core.Device que representa nosso dispositivo. Se o dispositivo não for encontrado, ele retornará
Nenhum . De fato, você pode usar qualquer campo da classe do
Descritor de Dispositivos que desejar. Por exemplo, e se quisermos descobrir se há uma impressora USB conectada ao sistema? É muito fácil:
7 é o código para a classe de impressora de acordo com a especificação USB. Ah, espere, e se eu quiser numerar todas as impressoras disponíveis? Não tem problema:
O que aconteceu Bem, é hora de uma pequena explicação ... O
find tem um parâmetro chamado
find_all e o padrão é False. Quando é falso
[1] , o
find retornará o primeiro dispositivo que corresponder aos critérios especificados (falaremos sobre isso em breve). Se você passar um valor
verdadeiro para o parâmetro, o
find retornará uma lista de todos os dispositivos que correspondem aos critérios. Isso é tudo! Simples, certo?
Nós terminamos? Não! Ainda não contei tudo: muitos dispositivos colocam suas informações de classe no
descritor de interface, em vez de no
descritor de dispositivos. Portanto, para encontrar realmente todas as impressoras conectadas ao sistema, precisaremos passar por todas as configurações, assim como todas as interfaces, e verificar se uma delas está definida como bInterfaceClass 7. Se você é um
programador como eu, pode se perguntar: existe alguma maneira mais fácil com a qual isso pode ser implementado. Resposta: sim, ele é. Para começar, vejamos o código pronto para encontrar todas as impressoras conectadas:
import usb.core import usb.util import sys class find_class(object): def __init__(self, class_): self._class = class_ def __call__(self, device):
O parâmetro
custom_match aceita qualquer objeto chamado que recebe o objeto do dispositivo. Ele deve retornar verdadeiro para um dispositivo adequado e falso para um inapropriado. Você também pode combinar
custom_match com os campos do dispositivo, se desejar:
Aqui estamos interessados nas impressoras do fornecedor 0xfffe.
Descreva-se
Ok, encontramos nosso dispositivo, mas antes de interagir com ele, gostaríamos de saber mais sobre ele. Bem, você sabe, configurações, interfaces, terminais, tipos de fluxos de dados ...
Se você tiver um dispositivo, poderá acessar qualquer campo do descritor de dispositivo como propriedades do objeto:
>>> dev.bLength >>> dev.bNumConfigurations >>> dev.bDeviceClass >>>
Para acessar as configurações disponíveis no dispositivo, você pode iterar o dispositivo:
for cfg in dev: sys.stdout.write(str(cfg.bConfigurationValue) + '\n')
Da mesma maneira, você pode iterar a configuração para acessar as interfaces, bem como iterar as interfaces para acessar seus pontos de controle. Cada tipo de objeto possui campos do descritor correspondente como atributos. Veja um exemplo:
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')
Você também pode usar índices para acesso aleatório aos descritores, como aqui:
>>>
Como você pode ver, os índices são contados a partir de 0. Mas espere! Há algo de estranho na maneira como obtenho acesso à interface ... Sim, você está certo, o índice para Configuração usa uma série de dois valores, dos quais o primeiro é o índice da Interface e o segundo é uma configuração alternativa. Em geral, para acessar a primeira interface, mas com a segunda configuração, escreveremos
cfg [(0,1)] .
Agora é a hora de aprender uma maneira poderosa de procurar descritores - uma função útil
find_descriptor . Já vimos isso no exemplo de pesquisa da impressora.
O find_descriptor funciona quase da mesma forma que o
find , com duas exceções:
- O find_descriptor recebe como primeiro parâmetro o descritor de origem que você pesquisará.
- Não há parâmetro de back-end nele [2]
Por exemplo, se tivermos um descritor de configuração
cfg e desejarmos encontrar todas as configurações alternativas para a interface 1, faremos isso:
import usb.util alt = usb.util.find_descriptor(cfg, find_all=True, bInterfaceNumber=1)
Observe que o
find_descriptor está no módulo
usb.util . Ele também aceita o parâmetro
custom_match descrito anteriormente.
Lidamos com vários dispositivos idênticosÀs vezes, você pode ter dois dispositivos idênticos conectados ao computador. Como você pode distinguir entre eles?
Os objetos de
dispositivo vêm com dois atributos adicionais que não fazem parte da especificação USB, mas são muito úteis: os atributos de
barramento e
endereço . Antes de tudo, vale a pena dizer que esses atributos são provenientes do back-end, e o back-end pode não suportá-los - nesse caso, eles estão definidos como
Nenhum . No entanto, esses atributos representam o número e o endereço do barramento do dispositivo e, como você deve ter adivinhado, podem ser usados para distinguir entre dois dispositivos com os mesmos valores de atributo
idVendor e
idProduct .
Como devo trabalhar?
Após a conexão, os dispositivos USB devem ser configurados usando algumas consultas padrão. Quando comecei a estudar a especificação
USB , fiquei desanimado com os descritores, configurações, interfaces, configurações alternativas, tipos de transferência e tudo mais ... E o pior de tudo, você não pode simplesmente ignorá-las: o dispositivo não funciona sem definir a configuração, mesmo que seja uma! O PyUSB está tentando tornar sua vida o mais simples possível. Por exemplo, depois de receber o objeto do seu dispositivo, antes de tudo, antes de interagir com ele, você precisa enviar uma solicitação de
configuração_configuração . O parâmetro de configuração para esta consulta que lhe interessa é
bConfigurationValue . A maioria dos dispositivos não tem mais de uma configuração, e o rastreamento do valor da configuração a ser usado é irritante (embora a maior parte do código que eu vi tenha codificado isso). Portanto, no PyUSB, você pode simplesmente enviar uma solicitação
set_configuration sem argumentos. Nesse caso, ele instalará a primeira configuração encontrada (se o seu dispositivo tiver apenas uma, você não precisará se preocupar com o valor da configuração). Por exemplo, suponha que você tenha um dispositivo com um descritor de configuração e seu campo bConfigurationValue seja 5
[3] , as consultas subsequentes funcionarão da mesma maneira:
>>> dev.set_configuration(5)
Uau! Você pode usar o objeto
Configuration como um parâmetro para
set_configuration ! Sim, ele também possui um método
definido para se configurar na configuração atual.
Outra opção que você precisa ou não precisa configurar é a opção de alterar interfaces. Cada dispositivo pode ter apenas uma configuração ativada por vez, e cada configuração pode ter mais de uma interface e você pode usar todas as interfaces ao mesmo tempo. Você entende melhor esse conceito se pensar na interface como um dispositivo lógico. Por exemplo, vamos imaginar uma impressora multifuncional, que ao mesmo tempo é uma impressora e um scanner. Para não complicar (ou pelo menos torná-lo o mais simples possível), vamos supor que ele tenha apenas uma configuração. Porque temos uma impressora e um scanner, a configuração possui 2 interfaces: uma para a impressora e outra para o scanner. Um dispositivo com mais de uma interface é chamado de dispositivo composto. Quando você conecta sua impressora multifuncional ao computador, o sistema operacional carrega dois drivers diferentes: um para cada dispositivo periférico “lógico” que você possui
[4]E as configurações de interface alternativas? Que bom que você perguntou. Uma interface possui uma ou mais configurações alternativas. Uma interface com apenas uma configuração alternativa é considerada sem configurações alternativas
[5] Configurações alternativas para interfaces como uma configuração para dispositivos, ou seja, para cada interface, você pode ter apenas uma configuração alternativa ativa. Por exemplo, a especificação USB sugere que um dispositivo não pode ter um ponto de verificação isócrono em sua configuração alternativa primária
[6] , para que o dispositivo de streaming tenha pelo menos duas configurações alternativas, com uma segunda configuração com um ponto de verificação isócrono. Porém, diferentemente das configurações, as interfaces com apenas uma configuração alternativa não precisam ser configuradas
[7] Você seleciona uma configuração de interface alternativa usando a função
set_interface_altsetting :
>>> dev.set_interface_altsetting(interface = 0, alternate_setting = 0)
AdvertênciaA especificação USB diz que o dispositivo pode retornar um erro se receber uma solicitação SET_INTERFACE para uma interface que não possui configurações alternativas adicionais. Portanto, se você não tiver certeza de que a interface possui mais de uma configuração alternativa ou se aceita a solicitação SET_INTERFACE, o método mais seguro é chamar
set_interface_altsetting dentro do bloco try-except, como aqui:
try: dev.set_interface_altsetting(...) except USBError: pass
Você também pode usar o objeto
Interface como um parâmetro de função, os parâmetros
interface e
alternate_setting são herdados automaticamente dos
campos bInterfaceNumber e
bAlternateSetting . Um exemplo:
>>> intf = find_descriptor(...) >>> dev.set_interface_altsetting(intf) >>> intf.set_altsetting()
AdvertênciaO objeto
Interface deve pertencer ao descritor de configuração ativo.
Fale comigo querida
E agora é hora de entender como interagir com dispositivos USB. O USB possui quatro tipos de fluxos de dados: transferência em massa, transferência por interrupção, transferência isócrona e transferência de controle. Não pretendo explicar o objetivo de cada segmento e as diferenças entre eles. Portanto, presumo que você tenha pelo menos conhecimento básico de fluxos de dados USB.
O fluxo de dados de controle é o único fluxo cuja estrutura é descrita na especificação; o restante simplesmente envia e recebe dados brutos do ponto de vista USB. Portanto, você tem várias funções para trabalhar com fluxos de controle e o restante dos fluxos é processado pelas mesmas funções.
Você pode acessar o fluxo de dados de controle usando o método
ctrl_transfer . É usado para fluxos de saída (OUT) e de entrada (IN). A direção do fluxo é determinada pelo parâmetro
bmRequestType .
Os parâmetros
ctrl_transfer quase coincidem com a estrutura da solicitação de controle. A seguir, é apresentado um exemplo de como organizar um fluxo de dados de controle.
[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 exemplo assume que nosso dispositivo inclui duas solicitações de controle de usuário que agem como um pipe de loopback. O que você escreve com a mensagem
CTRL_LOOPBACK_WRITE , pode ler com a mensagem
CTRL_LOOPBACK_READ .
Os quatro primeiros parâmetros -
bmRequestType ,
bmRequest ,
wValue e
wIndex - são os campos da estrutura padrão do fluxo de controle. O quinto parâmetro é os dados que estão sendo transferidos para o fluxo de dados de saída ou o número de dados que estão sendo lidos no fluxo de entrada. Os dados transmitidos podem ser qualquer tipo de sequência, que pode ser alimentada como parâmetro para a entrada do método
__init__ para a
matriz . Se não houver dados sendo transferidos, o parâmetro deverá ser definido como
Nenhum (ou 0 no caso de um fluxo de dados de entrada). Há outro parâmetro opcional indicando o tempo limite da operação. Se você não passar, o tempo limite padrão será usado (mais sobre isso mais tarde). No fluxo de dados de saída, o valor retornado é o número de bytes realmente enviados ao dispositivo. No fluxo de entrada, o valor de retorno é uma
matriz com os dados lidos.
Para outros fluxos, você pode usar os métodos de
gravação e
leitura , respectivamente, para gravar e ler dados. Você não precisa se preocupar com o tipo de fluxo - ele é detectado automaticamente pelo endereço do ponto de verificação. Aqui está nosso exemplo de loopback, desde que tenhamos um canal de loopback no ponto de interrupção 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
O primeiro e o terceiro parâmetros são os mesmos para os dois métodos - este é o endereço do ponto de verificação e o tempo limite, respectivamente. O segundo parâmetro são os dados transmitidos (gravação) ou o número de bytes a serem lidos (lidos). Os dados retornados serão uma instância de um objeto de
matriz para o método de
leitura ou o número de bytes gravados para o método de
gravação .
Nas versões beta 2, em vez do número de bytes, você pode passar para
read ou
ctrl_transfer um objeto de
matriz no qual os dados serão lidos. Nesse caso, o número de bytes a serem lidos será o comprimento da matriz vezes o valor de
array.itemsize .
No
ctrl_transfer , o parâmetro
timeout é opcional. Quando o
tempo limite é omitido, a propriedade
Device.default_timeout é
usada como tempo limite operacional.
Controle-se
Além das funções de
fluxo de dados, o módulo
usb.control fornece funções que incluem solicitações de controle USB padrão e o módulo
usb.util possui uma conveniente função
get_string que exibe especificamente os descritores de linha.
Tópicos adicionais
Por trás de toda grande abstração há uma grande realização
Anteriormente, havia apenas
libusb . Depois veio o libusb 1.0 e tivemos o libusb 0.1 e 1.0. Depois disso, criamos o
OpenUSB e agora moramos na
Torre de Babel da Biblioteca USB
[9] Como o PyUSB lida com isso? Bem, PyUSB é uma biblioteca democrática, você pode escolher qual biblioteca deseja. De fato, você pode escrever sua própria biblioteca USB do zero e pedir ao PyUSB para usá-la.
A função
find possui outro parâmetro, sobre o qual não falei. Este é o parâmetro de
back -
end . Se você não transferi-lo, um dos back-ends internos será usado. Um back-end é um objeto herdado de
usb.backend.IBackend , responsável pela introdução de lixo USB específico do sistema operacional. Como você deve ter adivinhado, o libusb 0.1 embutido, o libusb 1.0 e o OpenUSB são back-end.
Você pode escrever seu próprio back-end e usá-lo. Apenas herde do
IBackend e ative os métodos necessários. Pode ser necessário consultar a documentação do
usb.backend para entender como isso é feito.
Não seja egoísta
Python tem o que chamamos de
gerenciamento automático de memória . Isso significa que a máquina virtual decidirá quando descarregar objetos da memória. Sob o capô, o PyUSB gerencia todos os recursos de baixo nível com os quais você precisa trabalhar (aprovação da interface, ajuste do dispositivo etc.) e a maioria dos usuários não precisa se preocupar com isso. Porém, devido à natureza indefinida da destruição automática de objetos pelo Python, os usuários não podem prever quando os recursos alocados serão liberados. Alguns aplicativos precisam alocar e liberar recursos deterministicamente. Para tais aplicativos, o módulo
usb.util fornece funções para interagir com o gerenciamento de recursos.
Se você desejar solicitar e liberar interfaces manualmente, poderá usar as
funções claim_interface e
release_interface .
A função Claim_interface solicitará a interface especificada se o dispositivo ainda não o fez. Se o dispositivo já solicitou uma interface, ele não faz nada. Apenas release_interface vai lançar a interface especificada, se for solicitado. Se a interface não for solicitada, ela não fará nada. Você pode usar a consulta manual da interface para resolver o problema de seleção de configuração descrito na documentação do libusb . Se você quiser liberar todos os recursos alocados pelo objeto do dispositivo (incluindo as interfaces solicitadas), poderá usar a função dispose_resources. Ele libera todos os recursos alocados e coloca o objeto do dispositivo (mas não no hardware do próprio dispositivo) no estado em que foi retornado após o uso da função find .Definição manual da biblioteca
Em geral, um back-end é um invólucro de uma biblioteca compartilhada que implementa uma API para acessar o USB. Por padrão, o back-end usa a função ctypes find_library () . No Linux e outros sistemas operacionais do tipo Unix, o find_library tenta executar programas externos (como / sbin / ldconfig , gcc e objdump ) para localizar o arquivo da biblioteca.Nos sistemas em que esses programas estão ausentes e / ou o cache da biblioteca está desativado, esta função não pode ser usada. Para superar as limitações, o PyUSB permite enviar a função personalizada find_library () para o back-end.Um exemplo desse cenário seria: >>> 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)
Observe que find_library é um argumento para a função get_backend () na qual você fornece a função responsável por encontrar a biblioteca certa para o back-end.Regras da velha escola
Se você estiver escrevendo um aplicativo usando as antigas APIs do PyUSB (0. alguma coisa lá), pode estar se perguntando se precisa atualizar seu código para usar a nova API. Bem, você deve fazê-lo, mas não é necessário. O PyUSB 1.0 vem com o módulo de compatibilidade usb.legacy . Inclui a API antiga com base na nova API. “Bem, devo substituir minha linha de importação usb por import usb.legacy como usb para fazer meu aplicativo funcionar?”, Você pergunta. A resposta é sim, funcionará, mas não é necessário. Se você executar o aplicativo sem alterações, ele funcionará porque a linha de importação usb importa todos os símbolos públicos de usb.legacy. Se você encontrar um problema - provavelmente você encontrou um bug.Me ajude por favor
Se precisar de ajuda, não me escreva um e-mail , há uma lista de e-mails para isso. As instruções de inscrição podem ser encontradas no site do PyUSB .[1] Quando escrevo True ou False (com letra maiúscula), quero dizer os valores correspondentes da linguagem Python. E quando digo verdadeiro (verdadeiro) ou falso (falso), quero dizer qualquer expressão Python que seja considerada verdadeira ou falsa. (Essa semelhança ocorreu no original e ajuda a entender os conceitos de verdadeiro e falso na tradução. - Nota. ) :[2] Consulte a documentação específica do back-end.[3] A especificação USB não impõe nenhum valor específico ao valor da configuração. O mesmo vale para números de interface e configurações alternativas.[4] Na verdade, tudo é um pouco mais complicado, mas isso é apenas uma explicação para nós.[5] Eu sei que isso parece estranho.[6] Isso ocorre porque, se não houver largura de banda para fluxos de dados isócronos durante a configuração do dispositivo, ele poderá ser numerado com sucesso.[7] Isso não acontece na configuração porque o dispositivo pode estar em um estado não configurado.[8] No PyUSB, os fluxos de dados de controle acessam o ponto de controle 0. Muito, muito, muito raramente, um dispositivo possui um ponto de controle de controle alternativo (nunca conheci esse dispositivo).[9] Isso é apenas uma piada, não leve a sério. Ótimas opções são melhores que nenhuma escolha.