本文由 简悦SimpRead 转码,原文地址 web.dev
建立一个设备来充分利用WebUSB API。
构建一个设备来充分利用WebUSB API。
2018年12月20日 - 2020年12月3日更新
这篇文章解释了如何建立一个设备来充分利用WebUSB API的优势。关于API本身的简要介绍,请看在网络上访问USB设备。
背景 #
通用串行总线(USB)已经成为将外围设备连接到桌面和移动计算设备的最常见的物理接口。除了定义总线的电气特性和与设备通信的一般模型外,USB规范还包括一套设备类别规范。这些是设备制造商可以实现的特定类别设备的一般模型,如存储、音频、视频、网络等。这些设备类别规范的好处是,操作系统厂商可以根据类别规范("类别驱动")实现一个单一的驱动,任何实现该类别的设备都会得到支持。这比每个制造商都需要编写他们自己的设备驱动程序要好得多。
然而,有些设备并不适合这些标准化的设备类别之一。制造商可以选择将他们的设备标记为实现供应商特定的类别。在这种情况下,操作系统会根据供应商的驱动程序包中提供的信息来选择要加载的设备驱动程序,通常是一组供应商和产品的ID,它们被称为实现特定的供应商特定协议。
USB的另一个特点是,设备可以向它们所连接的主机提供多个接口。每个接口可以实现一个标准化的类,也可以是供应商特定的。当操作系统选择正确的驱动程序来处理设备时,每个接口都可以由不同的驱动程序声称。例如,一个USB网络摄像头通常提供两个接口,一个实现USB视频类(用于摄像头),一个实现USB音频类(用于麦克风)。操作系统不会加载一个单一的 "网络摄像头驱动程序",而是加载独立的视频和音频类驱动程序,负责设备的独立功能。这种接口类的组成提供了更大的灵活性。
API基础知识 #
许多标准的USB类都有相应的网络API。例如,一个页面可以使用getUserMedia()从视频类设备捕捉视频,或者通过监听键盘事件(KeyboardEvents)或指针事件(PointerEvents),或者使用游戏板或WebHID API,从人机界面(HID)类设备接收输入事件。正如不是所有的设备都实现了标准化的类定义一样,不是所有的设备都实现了与现有网络平台API相对应的功能。在这种情况下,WebUSB API可以填补这一空白,它为网站提供了一种方法,使其可以直接在其页面中声明一个供应商的特定接口并实现对它的支持。
由于操作系统管理USB设备的方式不同,各平台对设备通过WebUSB访问的具体要求也略有不同,但基本要求是,设备不应该已经有一个声称要控制该页面的接口的驱动程序。这可以是由操作系统供应商提供的通用类驱动程序,也可以是由供应商提供的设备驱动程序。由于USB设备可以提供多个接口,每个接口都可能有自己的驱动程序,因此有可能建立一个设备,其中一些接口由一个驱动程序声称,而其他接口则由浏览器访问。
例如,一个高端的USB键盘可以提供一个HID类接口,该接口将被操作系统的输入子系统要求,而一个供应商的特定接口仍可被WebUSB使用,供配置工具使用。这个工具可以在制造商的网站上提供,允许用户改变设备行为的各个方面,如宏键和灯光效果,而无需安装任何平台特定的软件。这样一个设备的配置描述符看起来会是这样的。
本文件中的数值和其他表格是以十六进制或二进制表示的,以更易读的为准。USB是一个小字节总线,因此任何大于1个字节的整数值都应该先发送最小有效字节。
| Value | Field | Description |
|---|---|---|
| Configuration descriptor | ||
0x09 | bLength | Size of this descriptor |
0x02 | bDescriptorType | Configuration descriptor |
0x0039 | wTotalLength | Total length of this series of descriptors |
0x02 | bNumInterfaces | Number of interfaces |
0x01 | bConfigurationValue | Configuration 1 |
0x00 | iConfiguration | Configuration name (none) |
0b1010000 | bmAttributes | Self-powered device with remote wakeup |
0x32 | bMaxPower | Max Power is expressed in 2 mA increments |
| Interface descriptor | ||
0x09 | bLength | Size of this descriptor |
0x04 | bDescriptorType | Interface descriptor |
0x00 | bInterfaceNumber | Interface 0 |
0x00 | bAlternateSetting | Alternate setting 0 (default) |
0x01 | bNumEndpoints | 1 endpoint |
0x03 | bInterfaceClass | HID interface class |
0x01 | bInterfaceSubClass | Boot interface subclass |
0x01 | bInterfaceProtocol | Keyboard |
0x00 | iInterface | Interface name (none) |
| HID descriptor | ||
0x09 | bLength | Size of this descriptor |
0x21 | bDescriptorType | HID descriptor |
0x0101 | bcdHID | HID version 1.1 |
0x00 | bCountryCode | Hardware target country |
0x01 | bNumDescriptors | Number of HID class descriptors to follow |
0x22 | bDescriptorType | Report descriptor type |
0x003F | wDescriptorLength | Total length of the Report descriptor |
| Endpoint descriptor | ||
0x07 | bLength | Size of this descriptor |
0x05 | bDescriptorType | Endpoint descriptor |
0b10000001 | bEndpointAddress | Endpoint 1 (IN) |
0b00000011 | bmAttributes | Interrupt |
0x0008 | wMaxPacketSize | 8 byte packets |
0x0A | bInterval | 10ms interval |
| Interface descriptor | ||
0x09 | bLength | Size of this descriptor |
0x04 | bDescriptorType | Interface descriptor |
0x01 | bInterfaceNumber | Interface 1 |
0x00 | bAlternateSetting | Alternate setting 0 (default) |
0x02 | bNumEndpoints | 2 endpoints |
0xFF | bInterfaceClass | Vendor-specific interface class |
0x00 | bInterfaceSubClass | |
0x00 | bInterfaceProtocol | |
0x00 | iInterface | Interface name (none) |
| Endpoint descriptor | ||
0x07 | bLength | Size of this descriptor |
0x05 | bDescriptorType | Endpoint descriptor |
0b10000010 | bEndpointAddress | Endpoint 1 (IN) |
0b00000010 | bmAttributes | Bulk |
0x0040 | wMaxPacketSize | 64 byte packets |
0x00 | bInterval | N/A for bulk endpoints |
| Endpoint descriptor | ||
0x07 | bLength | Size of this descriptor |
0x05 | bDescriptorType | Endpoint descriptor |
0b00000011 | bEndpointAddress | Endpoint 3 (OUT) |
0b00000010 | bmAttributes | Bulk |
0x0040 | wMaxPacketSize | 64 byte packets |
0x00 | bInterval | N/A for bulk endpoints |
配置描述符由多个描述符串联起来组成。每个描述符都以bLength和bDescriptorType字段开始,这样就可以识别它们。第一个接口是一个HID接口,有一个相关的HID描述符和一个用于向操作系统传递输入事件的单一端点。第二个接口是一个供应商特定的接口,有两个端点,可用于向设备发送命令并接收响应的回报。
WebUSB描述符 #
虽然WebUSB可以在不修改固件的情况下与许多设备一起工作,但通过在设备上标记特定的描述符来启用额外的功能,表明对WebUSB的支持。例如,你可以指定一个登陆页面的URL,当你的设备被插入时,浏览器可以引导用户进入。
(WebUSB通知。)
二进制设备对象存储(BOS)是USB3.0中引入的概念,但也作为2.1版本的一部分被回传到USB2.0设备中。声明对WebUSB的支持,首先要在BOS描述符中包括以下平台能力描述符。
| Value | Field | Description |
|---|---|---|
| Binary device Object Store descriptor | ||
0x05 | bLength | Size of this descriptor |
0x0F | bDescriptorType | Binary device Object Store descriptor |
0x001D | wTotalLength | Total length of this series of descriptors |
0x01 | bNumDeviceCaps | Number of device capability descriptors in the BOS |
| WebUSB platform capability descriptor | ||
0x18 | bLength | Size of this descriptor |
0x10 | bDescriptorType | Device capability descriptor |
0x05 | bDevCapabilityType | Platform capability descriptor |
0x00 | bReserved | |
{0x38, 0xB6, 0x08, 0x34, 0xA9, 0x09, 0xA0, 0x47, 0x8B, 0xFD, 0xA0, 0x76, 0x88, 0x15, 0xB6, 0x65} | PlatformCapablityUUID | WebUSB platform capability descriptor GUID in little-endian format |
0x0100 | bcdVersion | WebUSB descriptor version 1.0 |
0x01 | bVendorCode | bRequest value for WebUSB |
0x01 | iLandingPage | URL for landing page |
上面的UUID可以写成{3408b638-09a9-47a0-8bfd-a0768815b665};但是,当作为USB描述符的一部分发送时,它的组成字段必须以小-endian顺序发送。这种转换很复杂,所以这里给出正确的编码供参考。参见RFC4122。
平台能力UUID标识了这是一个WebUSB平台能力描述符,它提供了设备的基本信息。为了让浏览器获取更多的设备信息,它使用bVendorCode值来向设备发出额外的请求。目前唯一指定的请求是GET_URL,它返回一个URL描述符。这些描述符类似于字符串描述符,但被设计为以最少的字节对URL进行编码。一个 "google.com "的URL描述符看起来像这样。
| Value | Field | Description |
|---|---|---|
| URL descriptor | ||
0x0D | bLength | Size of this descriptor |
0x03 | bDescriptorType | URL descriptor |
0x01 | bScheme | https:// |
"google.com" | URL | UTF-8 encoded URL content |
当你的设备第一次插入时,浏览器通过发出这个标准的GET_DESCRIPTOR控制传输读取BOS描述符。
| bmRequestType | bRequest | wValue | wIndex | wLength | Data (response) |
|---|---|---|---|---|---|
0b10000000 | 0x06 | 0x0F00 | 0x0000 | * | The BOS descriptor |
这个请求通常要发出两次,第一次是用足够大的wLength,这样主机就能发现wTotalLength字段的值,而不需要承诺进行大的传输,然后在知道描述符的全部长度时再发出一次。
如果WebUSB平台能力描述符的iLandingPage字段被设置为非零值,浏览器就会通过发出控制传输来执行WebUSB特定的GET_URL请求,其中bRequest设置为平台能力描述符的bVendorCode值,wValue设置为iLandingPage值。GET_URL的请求代码(0x02)被放在wIndex中。
| bmRequestType | bRequest | wValue | wIndex | wLength | Data (response) |
|---|---|---|---|---|---|
0b11000000 | 0x01 | 0x0001 | 0x0002 | * | The URL descriptor |
同样,这个请求可能会被发出两次,以便首先探测被读取的描述符的长度。
当USB设备被插入时,对显示通知的支持在Android上还不能使用。
特定平台的考虑因素 #
虽然WebUSB API试图为访问USB设备提供一个一致的接口,但开发人员仍应注意对应用程序的要求,如网络浏览器的要求,以便访问设备。
macOS #
对于macOS来说,没有什么特别的需要。使用WebUSB的网站可以连接到设备,并要求任何没有被内核驱动或其他应用程序要求的接口。
Linux #
Linux和macOS一样,但在默认情况下,大多数发行版都没有设置用户账户的权限来打开USB设备。一个叫做udev的系统守护程序负责分配允许访问设备的用户和组。像这样的规则将把与给定的供应商和产品ID相匹配的设备的所有权分配给plugdev组,这是一个访问外围设备的用户的常用组。
SUBSYSTEM=="usb", ATTR{idVendor}=="XXXX", ATTR{idProduct}=="XXXX", GROUP="plugdev"
用你的设备的十六进制厂商和产品ID替换XXXX,例如ATTR{idVendor}=="18d1",ATTR{idProduct}=="4e11 "将匹配Nexus One手机。这些代码必须不含通常的 "0x "前缀,并且全部为小写字母才能被正确识别。要找到你设备的ID,请运行命令行工具lsusb。
这个规则应该放在/etc/udev/rules.d目录下的一个文件中,一旦设备插上电源就会生效。不需要重新启动udev。
Android #
安卓平台是基于Linux的,但不需要对系统配置进行任何修改。默认情况下,任何没有在操作系统中内置驱动程序的设备都可以被浏览器使用。然而,开发者应该意识到,用户在连接设备时将遇到一个额外的步骤。一旦用户根据对requestDevice()的调用选择了一个设备,Android会显示一个提示,询问是否允许Chrome访问该设备。如果用户返回到一个已经拥有连接设备权限的网站,并且该网站调用open(),该提示也会重新出现。
此外,与桌面Linux相比,更多的设备将在Android上被访问,因为默认情况下包含的驱动程序较少。例如,一个明显的遗漏是USB CDC-ACM类,该类通常由USB到串行适配器实现,因为在Android SDK中没有与串行设备通信的API。
Chrome OS #
Chrome OS也是基于Linux的,同样不需要对系统配置进行任何修改。permission_broker服务控制对USB设备的访问,只要有至少一个无人认领的接口,就会允许浏览器访问这些设备。
Windows #
Windows驱动模型引入了一个额外的要求。与上述平台不同,即使没有加载驱动程序,从用户应用程序中打开USB设备的能力也不是默认的。相反,有一个特殊的驱动,WinUSB,需要被加载,以提供应用程序用来访问设备的接口。这可以通过安装在系统上的自定义驱动程序信息文件(INF)或通过修改设备固件以在枚举期间提供微软操作系统兼容性描述符来实现。
驱动信息文件(INF) #
驱动程序信息文件告诉Windows在第一次遇到一个设备时应该做什么。由于用户的系统已经包含了WinUSB驱动,所有需要的就是INF文件将你的供应商和产品ID与这个新的安装规则联系起来。下面的文件是一个基本的例子。将其保存为扩展名为".inf "的文件,修改标有 "X "的部分,然后右击它,从上下文菜单中选择 "安装"。
拷贝代码拷贝代码拷贝代码
[Version]
Signature = "$Windows NT$"
Class = USBDevice
ClassGUID = {88BAE032-5A81-49f0-BC3D-A4FF138216D6}
Provider = %ManufacturerName%
CatalogFile = WinUSBInstallation.cat
DriverVer = 09/04/2012,13.54.20.543
; ========== Manufacturer/Models sections ===========
[Manufacturer]
%ManufacturerName% = Standard,NTx86,NTia64,NTamd64
[Standard.NTx86]
%USB\MyCustomDevice.DeviceDesc% = USB_Install,USB\VID_XXXX&PID_XXXX
[Standard.NTia64]
%USB\MyCustomDevice.DeviceDesc% = USB_Install,USB\VID_XXXX&PID_XXXX
[Standard.NTamd64]
%USB\MyCustomDevice.DeviceDesc% = USB_Install,USB\VID_XXXX&PID_XXXX
; ========== Class definition ===========
[ClassInstall32]
AddReg = ClassInstall_AddReg
[ClassInstall_AddReg]
HKR,,,,%ClassName%
HKR,,NoInstallClass,,1
HKR,,IconPath,%REG_MULTI_SZ%,"%systemroot%\system32\setupapi.dll,-20"
HKR,,LowerLogoVersion,,5.2
; =================== Installation ===================
[USB_Install]
Include = winusb.inf
Needs = WINUSB.NT
[USB_Install.Services]
Include = winusb.inf
Needs = WINUSB.NT.Services
[USB_Install.HW]
AddReg = Dev_AddReg
[Dev_AddReg]
HKR,,DeviceInterfaceGUIDs,0x10000,"{XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX}"
; =================== Strings ===================
[Strings]
ManufacturerName = "Your Company Name Here"
ClassName = "Your Company Devices"
USB\MyCustomDevice.DeviceDesc = "Your Device Name Here"
[Dev_AddReg]部分配置了设备的DeviceInterfaceGUIDs集。每个设备接口必须有一个GUID,以便应用程序通过Windows API找到并连接到它。使用New-GuidPowerShell cmdlet或在线工具来生成一个随机的GUID。
出于开发的目的,Zadig工具提供了一个简单的接口,用于用WinUSB驱动替换USB接口加载的驱动。
微软操作系统兼容性描述符#
上面的INF文件方法很麻烦,因为它需要提前配置每个用户的机器。Windows 8.1及更高版本通过使用自定义USB描述符提供了一个替代方案。这些描述符在设备首次插入时向Windows操作系统提供信息,这些信息通常会包含在INF文件中。
一旦你设置了WebUSB描述符,就很容易添加微软的操作系统兼容性描述符。首先用这个额外的平台能力描述符扩展BOS描述符。确保更新wTotalLength和bNumDeviceCaps以考虑到它。
| Value | Field | Description |
|---|---|---|
| Microsoft OS 2.0 platform capability descriptor | ||
0x1C | bLength | Size of this descriptor |
0x10 | bDescriptorType | Device capability descriptor |
0x05 | bDevCapabilityType | Platform capability descriptor |
0x00 | bReserved | |
{0xDF, 0x60, 0xDD, 0xD8, 0x89, 0x45, 0xC7, 0x4C, 0x9C, 0xD2, 0x65, 0x9D, 0x9E, 0x64, 0x8A, 0x9F} | PlatformCapablityUUID | Microsoft OS 2.0 platform compatibility descriptor GUID in little-endian format |
0x06030000 | dwWindowsVersion | Minimum compatible Windows version (Windows 8.1) |
0x00B2 | wMSOSDescriptorSetTotalLength | Total length of the descriptor set |
0x02 | bMS_VendorCode | bRequest value for retrieving further Microsoft descriptors |
0x00 | bAltEnumCode | Device does not support alternate enumeration |
和WebUSB描述符一样,你必须选择一个bRequest值,用于与这些描述符相关的控制传输。在这个例子中,我选择了0x02。0x07,放在wIndex中,是用来从设备中检索Microsoft OS 2.0描述符集的命令。
| bmRequestType | bRequest | wValue | wIndex | wLength | Data (response) |
|---|---|---|---|---|---|
0b11000000 | 0x02 | 0x0000 | 0x0007 | * | MS OS 2.0 Descriptor Set |
一个USB设备可以有多种功能,因此描述符集的第一部分描述了后面的属性与哪个功能相关。下面的例子配置了一个复合设备的接口1。描述符给了操作系统关于这个接口的两个重要信息。兼容的ID描述符告诉Windows,这个设备与WinUSB驱动兼容。注册表属性描述符的功能类似于上面INF例子中的[Dev_AddReg]部分,设置一个注册表属性来给这个函数分配一个设备接口GUID。
| Value | Field | Description |
|---|---|---|
| Microsoft OS 2.0 descriptor set header | ||
0x000A | wLength | Size of this descriptor |
0x0000 | wDescriptorType | Descriptor set header descriptor |
0x06030000 | dwWindowsVersion | Minimum compatible Windows version (Windows 8.1) |
0x00B2 | wTotalLength | Total length of the descriptor set |
| Microsoft OS 2.0 configuration subset header | ||
0x0008 | wLength | Size of this descriptor |
0x0001 | wDescriptorType | Configuration subset header desc. |
0x00 | bConfigurationValue | Applies to configuration 1 (indexed from 0 despite configurations normally indexed from 1) |
0x00 | bReserved | Must be set to 0 |
0x00A8 | wTotalLength | Total length of the subset including this header |
| Microsoft OS 2.0 function subset header | ||
0x0008 | wLength | Size of this descriptor |
0x0002 | wDescriptorType | Function subset header descriptor |
0x01 | bFirstInterface | First interface of the function |
0x00 | bReserved | Must be set to 0 |
0x00A0 | wSubsetLength | Total length of the subset including this header |
| Microsoft OS 2.0 compatible ID descriptor | ||
0x0014 | wLength | Size of this descriptor |
0x0003 | wDescriptorType | Compatible ID descriptor |
"WINUSB\0\0" | CompatibileID | ASCII string padded to 8 bytes |
"\0\0\0\0\0\0\0\0" | SubCompatibleID | ASCII string padded to 8 bytes |
| Microsoft OS 2.0 registry property descriptor | ||
0x0084 | wLength | Size of this descriptor |
0x0004 | wDescriptorType | Registry property descriptor |
0x0007 | wPropertyDataType | REG_MULTI_SZ |
0x002A | wPropertyNameLength | Length of the property name |
"DeviceInterfaceGUIDs\0" | PropertyName | Property name with null terminator encoded in UTF-16LE |
0x0050 | wPropertyDataLength | Length of the property value |
"{XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX}\0\0" | PropertyData | GUID plus two null terminators encoded in UTF-16LE |
Windows只会向设备查询此信息一次。如果设备没有回应有效的描述符,那么在下一次连接该设备时就不会再询问。微软提供了USB设备注册表项列表,描述了在列举设备时创建的注册表项。在测试时,删除为设备创建的条目,迫使Windows再次尝试读取描述符。
更多信息请查看微软的博文,了解如何使用这些描述符。
示例 #
在这些项目中可以找到实现WebUSB感知设备的示例代码,包括WebUSB描述符和微软操作系统描述符。