用PyUSB 1.0编程

来自翻译
这是PyUSB 1.0编程手册的翻译
该指南是由PyUSB开发人员编写的,但是很快就会完成提交工作,我相信walac是该指南的主要作者。

让我自我介绍


PyUSB 1.0是一个Python库,可轻松访问USB 。 PyUSB提供各种功能:

  • 100%用Python编写:
    与用C编写的0.x版本不同,1.0版是使用Python编写的。 这使没有C经验的Python程序员可以更好地了解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一无所知,我会推荐Jan Axelson 出色的USB Complete书。

聊够了,让我们编写代码!


谁是谁


首先,让我们介绍一下PyUSB模块。 所有PyUSB模块都在usb下,包括以下模块:
模组内容描述
核心USB主模块。
效用辅助功能。
控制标准管理要求。
遗留物0.x版兼容性层。
后端包含内置后端的子包。

例如,要导入核心模块,请输入以下内容:

>>> import usb.core >>> dev = usb.core.find() 

好吧,让我们开始吧


以下是一个简单的程序,该程序将字符串“ test”发送到找到的第一个数据源(端点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函数:

 dev.write(1, 'test') 

在这里,我们在地址1的断点处写入字符串“ test”。 在以下各节中将更好地讨论所有这些功能。

怎么了


如果发生错误,PyUSB中的每个函数都会引发异常。 除了标准的Python例外 ,PyUSB 为USB相关错误定义了usb.core.USBError

您还可以使用PyUSB日志功能。 它使用日志记录模块。 要使用它,请使用以下日志记录级别之一定义PYUSB_DEBUG环境变量criticalerrorwarninginfodebug

默认情况下,消息发送到sys.stderr 。 如果需要,可以通过定义环境变量PYUSB_LOG_FILENAME将日志消息重定向到文件。 如果它的值是文件的正确路径,则消息将被写入那里,否则它们将被发送到sys.stderr

你在哪


核心模块中的find()函数用于查找和编号连接到系统的设备。 例如,假设我们的设备的供应商ID为0xfffe,产品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 。 实际上,您可以使用所需的设备描述符类的任何字段。 例如,如果我们要查找系统上是否连接了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会返回所有符合条件的设备的列表。 仅此而已! 简单吧?

完成了吗 不行 我还没有告诉所有事情:许多设备实际上将其类信息放在接口描述符中,而不是设备描述符中 。 因此,为了真正找到连接到系统的所有打印机,我们将需要检查所有配置以及所有接口,并检查是否将其中一个接口设置为bInterfaceClass7。如果您是像我这样的程序员 ,您可能想知道:有没有更简单的方法可以实现这一点。 答:是的,他是。 首先,让我们看一下用于查找所有已连接打印机的现成代码:

 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参数接受任何接收设备对象的被调用对象。 对于合适的设备,它应该返回true;对于不合适的设备,它应该返回false。 如果需要,还可以将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接收要搜索的源描述符作为其第一个参数。
  • 里面没有后端参数 [2]

例如,如果我们有一个cfg配置描述符,并且想要找到接口1的所有替代设置,我们将这样做:

 import usb.util alt = usb.util.find_descriptor(cfg, find_all=True, bInterfaceNumber=1) 

请注意, find_descriptorusb.util模块中。 它还接受前面描述的custom_match参数。

我们处理多个相同的设备

有时您可以将两个相同的设备连接到计算机。 您如何区分它们? 设备对象带有两个附加属性,它们不是USB规范的一部分,但非常有用: 总线地址属性。 首先,值得一提的是,这些属性来自后端,后端可能不支持它们-在这种情况下,它们被设置为None 。 但是,这些属性表示设备总线的数量和地址,并且您可能已经猜到了这些属性可用于区分具有相同idVendoridProduct属性值的两个设备。

我应该如何工作?


连接后,必须使用一些标准查询来配置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的参数! 是的,他还具有用于在当前配置中配置自己的设置方法。

您需要或不需要配置的另一个选项是更改接口的选项。 每个设备一次只能具有一个激活的配置,并且每个配置可以具有多个接口,并且您可以同时使用所有接口。 如果您将接口视为逻辑设备,则可以更好地理解此概念。 例如,让我们想象一台多功能打印机,它同时是打印机和扫描仪。 为了不复杂(或至少使其尽可能简单),我们假定他只有一种配置。 因为 我们有一个打印机和扫描仪,配置有2个接口:一个用于打印机,一个用于扫描仪。 具有多个接口的设备称为复合设备。 当您将多功能打印机连接到计算机时,操作系统将加载两种不同的驱动程序:每个具有“逻辑”外围设备的驱动程序 [4]

备用界面设置如何? 你问的好东西。 界面具有一个或多个备用设置。 只有一个替代设置的接口被认为没有替代设置 [5] 。 接口的替代设置作为设备的配置,也就是说,每个接口只能有一个活动的替代设置。 例如,USB规范建议设备在其主要替代配置中不能具有同步检查点 [6] ,以便流式传输设备必须至少具有两个替代设置,第二个设置具有同步检查点。 但是,与配置不同,仅需配置具有一种替代配置的接口 [7] 。 您可以使用set_interface_altsetting函数选择其他接口设置:

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

警告

USB规范说,如果设备收到针对没有其他替代设置的接口的SET_INTERFACE请求,则允许该设备返回错误。 因此,如果不确定该接口有多个替代设置还是可以接受SET_INTERFACE请求,则最安全的方法是在try-except块内调用set_interface_altsetting ,如下所示:

 try: dev.set_interface_altsetting(...) except USBError: pass 

您还可以将Interface对象用作函数参数, interfaceAlternate_setting参数是自动从bInterfaceNumberbAlternateSetting字段继承的。 一个例子:

 >>> intf = find_descriptor(...) >>> dev.set_interface_altsetting(intf) >>> intf.set_altsetting() # !        

警告

接口对象必须属于活动配置描述符。

和我说话亲爱的


现在是时候让我们了解如何与USB设备进行交互了。 USB具有四种类型的数据流:批量传输,中断传输,同步传输和控制传输。 我不打算解释每个线程的目的以及它们之间的区别。 因此,我假设您至少具有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 

此示例假设我们的设备包含两个用户控制请求,它们的作用类似于回送管道。 使用消息CTRL_LOOPBACK_WRITE编写的内容 ,可以使用消息CTRL_LOOPBACK_READ进行读取。

前四个参数-bmRequestTypebmRequestwValuewIndex-是控制流的标准结构的字段。 第五个参数是为传出数据流传输的数据或在传入流中读取的数据数。 传输的数据可以是任何类型的序列,可以将其作为参数提供给array__init__方法的输入。 如果没有要传输的数据,则必须将参数设置为None (对于传入的数据流,则设置为0)。 还有另一个可选参数指示操作超时。 如果您不通过它,将使用默认超时(稍后会详细介绍)。 在传出数据流中,返回值是实际发送到设备的字节数。 在输入流中,返回值是一个读取了数据的数组

对于其他流,可以分别使用writeread方法来写入和读取数据。 您无需担心流的类型-检查点的地址会自动检测到它。 这是我们的环回示例,假设我们在断点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 

这两种方法的第一个和第三个参数相同-分别是检查点地址和超时。 第二个参数是传输的数据(写入)或要读取(读取)的字节数。 返回的数据将是用于read方法的数组对象的实例,或者是为write方法写入的字节数。

对于beta 2版本,您可以传递要读取数据的数组对象进行读取ctrl_transfer ,而不是字节数。 在这种情况下,要读取的字节数将是数组的长度乘以array.itemsize的值。

ctrl_transfer中timeout参数是可选的。 当省略超时时Device.default_timeout属性用作操作超时。

控制自己


除了数据功能外, usb.control模块还提供了包含标准USB控制请求的功能, usb.util模块还具有方便的get_string函数,该函数专门显示行描述符。

其他主题


每个伟大的抽象背后都是一个伟大的实现


以前,只有libusb 。 然后是libusb 1.0,我们有了libusb 0.1和1.0。 之后,我们创建了OpenUSB ,现在我们住在USB库的Babel塔中 [9] 。 PyUSB如何处理呢? 好吧,PyUSB是一个民主图书馆,您可以选择所需的图书馆。 实际上,您可以从头开始编写自己的USB库,并告诉PyUSB使用它。

find函数有另一个参数,我没有告诉过您。 这是后端参数。 如果不传输,则将使用内置后端之一。 后端是从usb.backend.IBackend继承的对象,负责引入特定于操作系统的USB垃圾邮件 。 您可能已经猜到,内置的libusb 0.1,libusb 1.0和OpenUSB是后端。

您可以编写自己的后端并使用它。 只需继承IBackend并启用必要的方法即可。 您可能需要查看usb.backend文档以了解其操作方法。

不要自私


Python具有我们所谓的自动内存管理功能 。 这意味着虚拟机将决定何时从内存中卸载对象。 在后台,PyUSB管理您需要使用的所有低级资源(批准接口,调整设备等),大多数用户无需为此担心。 但是,由于Python自动销毁对象的不确定性,用户无法预测何时释放分配的资源。 一些应用程序需要确定地分配和释放资源。 对于此类应用程序, usb.util模块提供了用于与资源管理进行交互的功能。

如果要手动请求和释放接口,则可以使用claim_interfacerelease_interface函数如果设备尚未请求该接口,那么claim_interface函数将请求该接口。如果设备已经请求接口,则不执行任何操作。只是release_interface将释放指定的接口,如果被请求。如果不要求该接口,则不执行任何操作。您可以使用手动接口查询来解决libusb 文档中描述的配置选择问题

如果要释放设备对象分配的所有资源(包括请求的接口),则可以使用dispose_resources函数它释放所有分配的资源,并在使用find函数之后将设备对象(但不在设备本身的硬件中)置于返回状态

手动库定义


通常,后端是共享库的包装,该共享库实现了用于访问USB的API。默认情况下,后端使用ctypes函数find_library()。在Linux和其他类似Unix的操作系统上,find_library尝试运行外部程序(例如/ sbin / ldconfiggccobjdump)以查找库文件。

在缺少这些程序和/或禁用了库高速缓存的系统上,无法使用此功能。为了克服这些限制,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()函数的参数,在其中您可以提供负责为后端找到正确的库的函数。

老学校规则


如果您使用旧的PyUSB API(0.something-there)编写应用程序,则可能会问自己是否需要更新代码才能使用新的API。好吧,您应该这样做,但这不是必需的。PyUSB 1.0带有usb.legacy兼容性模块它包括基于新API的旧API。“好吧,我应该将我的导入usb行替换导入usb.legacy作为usb以使我的应用程序正常工作吗?”,您问。答案是肯定的,它将起作用,但不是必须的。如果您不更改应用程序的运行方式,那么它将起作用,因为import usb line usb.legacy导入所有公共符号如果您遇到问题-很有可能您发现了一个错误。

请帮帮我


如果您需要帮助,请不要给我写电子邮件,这里有一个邮件列表。订阅说明可在PyUSB网站上找到

[1]当我用大写字母写True或False时,是指Python语言的相应值。当我说“真”(true)或“假”(false)时,我的意思是任何被视为是或否的Python表达式。(这种相似性发生在原文中,有助于理解翻译中正确与错误的概念。- 注意。

[2]请参阅特定的后端文档。

[3] USB规范没有在配置值上强加任何特定的值。接口号和备用设置也是如此。

[4]实际上,一切都有些复杂,但这只是对我们的一种解释。

[5]我知道这听起来很奇怪。

[6]这是因为如果在设备配置期间没有同步数据流的带宽,则可以成功对其进行编号。

[7]对于配置来说,不会发生这种情况,因为允许设备处于未配置状态。

[8]在PyUSB中,控制数据流访问控制点0。很少,很少有设备具有备用控制控制点(我从未见过这样的设备)。

[9]这只是个玩笑,不要当真。好的选择总比没有选择好。

Source: https://habr.com/ru/post/zh-CN430528/


All Articles