构建支持 WebUSB 的设备

构建可充分利用 WebUSB API 的设备。

Reilly Grant
Reilly Grant

本文介绍了如何构建可充分利用 WebUSB API 的设备。如需简要了解该 API,请参阅通过 Web 访问 USB 设备

背景

通用串行总线 (USB) 已成为将外围设备连接到桌面设备和移动计算设备的最常用物理接口。除了定义总线的电气特性以及与设备通信的通用模型之外,USB 规范还包括一组设备类别规范。这些是设备制造商可以实现的适用于特定类别设备(例如存储设备、音频、视频、网络等)的通用模型。这些设备类规范的优势在于,操作系统供应商可以根据类规范实现单个驱动程序(“类驱动程序”),并且支持实现该类的所有设备。与需要编写自己的设备驱动程序的所有制造商相比,这是一个很大的改进。

但是,有些设备并不属于其中一个标准化设备类。而制造商也可以选择将其设备标记为实现供应商专用类。在这种情况下,操作系统会根据供应商驱动程序软件包中提供的信息选择要加载哪个设备驱动程序,通常是一组已知能够实现特定供应商专用协议的供应商 ID 和产品 ID。

USB 的另一个特性是,设备可能会为其连接的主机提供多个接口。每个接口可以实现标准化类或特定于供应商的类。当操作系统选择正确的驱动程序来处理设备时,每个接口都可以由不同的驱动程序来声明。例如,USB 摄像头通常会提供两个接口,一个实现 USB 视频类(针对摄像头),另一个实现 USB 音频类(针对麦克风)。操作系统不会加载单个“摄像头驱动程序”,而是加载独立的视频和音频类驱动程序,这些驱动程序负责设备的不同功能。这种接口类组合可提供更高的灵活性。

API 基础知识

许多标准 USB 类都有对应的 Web API。例如,网页可以使用 getUserMedia() 从视频类设备捕获视频,或者通过监听 KeyboardEventsPointerEvents,或者使用 GamepadWebHID API,从人机接口 (HID) 类设备接收输入事件。 正如并非所有设备都实现标准化类定义一样,并非所有设备都能实现与现有网络平台 API 对应的功能。在这种情况下,WebUSB API 可以为网站提供一种方法来声明供应商专用接口,并直接在其页面中实现对接口的支持,从而填补这一空白。

对于可通过 WebUSB 访问的设备的具体要求因平台而异,因为操作系统管理 USB 设备的方式有所不同,但基本要求是设备不应已有声明页面要控制的接口的驱动程序。该驱动程序可以是操作系统供应商提供的通用类驱动程序,也可以是供应商提供的设备驱动程序。由于 USB 设备可以提供多个接口,并且每个接口可能都有自己的驱动程序,因此在制作出此类设备时,某些接口可以由驱动程序声明,而其他接口则留给浏览器访问。

例如,高端 USB 键盘可以提供将由操作系统的输入子系统声明的 HID 类接口,以及仍可供 WebUSB 使用供配置工具使用的供应商专用接口。该工具可以在制造商的网站上提供,让用户能够更改设备行为的各个方面(例如宏键和照明效果),而无需安装任何平台专用软件。此类设备的配置描述符将如下所示:

字段 说明
配置描述符
0x09 bLength 此描述符的大小
0x02 bDescriptorType 配置描述符
0x0039 wTotalLength 此系列描述符的总长度
0x02 bNumInterfaces 接口数量
0x01 bConfigurationValue Configuration 1
0x00 iConfiguration 配置名称(无)
0b1010000 bmAttributes 具有远程唤醒功能的自供电设备
0x32 bMaxPower 最大功率以 2 毫安为增量表示
接口描述符
0x09 bLength 此描述符的大小
0x04 bDescriptorType 接口描述符
0x00 bInterfaceNumber 接口 0
0x00 bAlternateSetting 备用设置 0(默认)
0x01 bNumEndpoints 1 个端点
0x03 bInterfaceClass HID 接口类
0x01 bInterfaceSubClass 启动接口子类
0x01 bInterfaceProtocol 键盘
0x00 iInterface 接口名称(无)
HID 描述符
0x09 bLength 此描述符的大小
0x21 bDescriptorType HID 描述符
0x0101 bcdHID HID 版本 1.1
0x00 bCountryCode 硬件目标国家/地区
0x01 bNumDescriptors 要跟踪的 HID 类描述符数量
0x22 bDescriptorType 报告描述符类型
0x003F wDescriptorLength 报告描述符的总长度
端点描述符
0x07 bLength 此描述符的大小
0x05 bDescriptorType 端点描述符
0b10000001 bEndpointAddress 端点 1 (IN)
0b00000011 bmAttributes 中断
0x0008 wMaxPacketSize 8 字节数据包
0x0A bInterval 10 毫秒间隔
接口描述符
0x09 bLength 此描述符的大小
0x04 bDescriptorType 接口描述符
0x01 bInterfaceNumber 接口 1
0x00 bAlternateSetting 备用设置 0(默认)
0x02 bNumEndpoints 2 个端点
0xFF bInterfaceClass 特定于供应商的接口类
0x00 bInterfaceSubClass
0x00 bInterfaceProtocol
0x00 iInterface 接口名称(无)
端点描述符
0x07 bLength 此描述符的大小
0x05 bDescriptorType 端点描述符
0b10000010 bEndpointAddress 端点 1 (IN)
0b00000010 bmAttributes Bulk
0x0040 wMaxPacketSize 64 字节数据包
0x00 bInterval 不适用于批量端点
端点描述符
0x07 bLength 此描述符的大小
0x05 bDescriptorType 端点描述符
0b00000011 bEndpointAddress 端点 3 (OUT)
0b00000010 bmAttributes Bulk
0x0040 wMaxPacketSize 64 字节数据包
0x00 bInterval 不适用于批量端点

配置描述符由多个串联在一起的描述符组成。每个字段都以 bLengthbDescriptorType 字段开头,以便可以识别。第一个接口是 HID 接口,具有关联的 HID 描述符,以及用于向操作系统传递输入事件的单个端点。第二个接口是特定于供应商的接口,具有两个端点,可用于向设备发送命令并接收返回的响应。

WebUSB 描述符

虽然 WebUSB 可以在不修改固件的情况下与许多设备搭配使用,但可以通过使用表示支持 WebUSB 的特定描述符标记设备来启用额外的功能。例如,您可以指定在设备接通电源后浏览器可将用户定向到的着陆页网址。

Chrome 中的 WebUSB 通知的屏幕截图
WebUSB 通知。

二进制设备对象存储 (BOS) 是 USB 3.0 中引入的概念,但也已作为版本 2.1 的一部分向后移植到 USB 2.0 设备。如需声明对 WebUSB 的支持,首先要在 BOS 描述符中添加以下平台功能描述符:

字段 说明
二进制设备对象存储描述符
0x05 bLength 此描述符的大小
0x0F bDescriptorType 二进制设备对象存储描述符
0x001D wTotalLength 此系列描述符的总长度
0x01 bNumDeviceCaps BOS 中的设备功能描述符数量
WebUSB 平台功能描述符
0x18 bLength 此描述符的大小
0x10 bDescriptorType 设备功能描述符
0x05 bDevCapabilityType 平台功能描述符
0x00 bReserved
{0x38, 0xB6, 0x08, 0x34, 0xA9, 0x09, 0xA0, 0x47, 0x8B, 0xFD, 0xA0, 0x76, 0x88, 0x15, 0xB6, 0x65} PlatformCapablityUUID WebUSB 平台功能描述符 GUID(采用小端格式)
0x0100 bcdVersion WebUSB 描述符版本 1.0
0x01 bVendorCode WebUSB 的 bRequest 值
0x01 iLandingPage 着陆页网址

平台功能 UUID 会将其标识为 WebUSB 平台功能描述符,用于提供有关设备的基本信息。为了使浏览器获取有关设备的更多信息,它使用 bVendorCode 值向设备发出其他请求。目前指定的唯一请求是 GET_URL,它会返回一个网址描述符。这些格式与字符串描述符类似,但旨在以最少的字节对网址进行编码。"https://google.com" 的网址描述符将如下所示:

字段 说明
网址描述符
0x0D bLength 此描述符的大小
0x03 bDescriptorType 网址描述符
0x01 bScheme https://
"google.com" 网址 采用 UTF-8 编码的网址内容

当您的设备首次插入浏览器时,通过发出以下标准的 GET_DESCRIPTOR 控件传输来读取 BOS 描述符:

bmRequestType bRequest wValue wIndex wLength 数据(响应)
0b10000000 0x06 0x0F00 0x0000 * BOS 描述符

该请求通常会发出两次,第一次是足够大的 wLength,以便主机找到 wTotalLength 字段的值,而无需提交大量的传输,第二次在完整的描述符长度已知时再次获取。

如果 WebUSB 平台功能描述符将 iLandingPage 字段设置为非零值,则浏览器会执行 WebUSB 专用 GET_URL 请求,具体方法是发出控制传输,将 bRequest 设为平台功能描述符中的 bVendorCode 值,并将 wValue 设为 iLandingPage 值。GET_URL (0x02) 的请求代码位于 wIndex 中:

bmRequestType bRequest wValue wIndex wLength 数据(响应)
0b11000000 0x01 0x0001 0x0002 * 网址描述符

同样,该请求可能会发出两次,以便首先探测所读取的描述符的长度。

针对具体平台的注意事项

虽然 WebUSB API 力求为访问 USB 设备提供一致的接口,但开发者仍应了解应用需要满足的要求,例如网络浏览器要求才能访问设备。

macOS

对于 macOS 来说没有什么特别之处。使用 WebUSB 的网站可以连接到该设备,并声明没有由内核驱动程序或其他应用声明的所有接口的所有权。

Linux

Linux 与 macOS 类似,但默认情况下,大多数发行版都不会设置有权打开 USB 设备的用户帐号。名为 udev 的系统守护程序负责分配允许访问设备的用户和群组。这样的规则会将与给定供应商 ID 和产品 ID 匹配的设备的所有权分配给 plugdev 组,该组是有权访问外围设备的用户的通用组:

SUBSYSTEM=="usb", ATTR{idVendor}=="XXXX", ATTR{idProduct}=="XXXX", GROUP="plugdev"

XXXX 替换为您的设备的十六进制供应商 ID 和产品 ID,例如 ATTR{idVendor}=="18d1", ATTR{idProduct}=="4e11" 将与 Nexus One 手机匹配。写入时不能包含常用的“0x”前缀且全部为小写,才能正确识别。如需查找设备的 ID,请运行命令行工具 lsusb

此规则应放在 /etc/udev/rules.d 目录下的文件中,并会在设备插入后立即生效。您无需重启 udev。

Android

Android 平台基于 Linux,无需对系统配置进行任何修改。默认情况下,任何未内置驱动程序的设备都可供浏览器使用。不过,开发者应注意,连接到设备时,用户会遇到一个额外的步骤。在用户选择设备以响应对 requestDevice() 的调用后,Android 会显示一条提示,询问是否允许 Chrome 访问该设备。如果用户返回已授权连接到设备的网站,并且该网站调用 open(),此提示也会重新显示。

此外,由于默认包含的驱动程序更少,因此可在 Android 上访问的设备比在桌面 Linux 上访问的设备多。例如,值得注意的是,USB CDC-ACM 类通常由 USB 转串行适配器实现,因为 Android SDK 中没有用于与串行设备通信的 API。

ChromeOS

ChromeOS 也基于 Linux,无需对系统配置进行任何修改。Permissions_broker 服务可控制对 USB 设备的访问,并且只要存在至少一个未声明的接口,浏览器就允许访问这些设备。

Windows

Windows 驱动程序模型引入了一项额外的要求。与上述平台不同,即使未加载驱动程序,从用户应用打开 USB 设备的功能也不是默认功能。取而代之的是,一种特殊的驱动程序 WinUSB,您需要加载该驱动程序,才能提供应用用于访问设备的接口。为此,您可以使用系统中安装的自定义驱动程序信息文件 (INF),也可以修改设备固件以在枚举期间提供 Microsoft 操作系统兼容性描述符。

驱动程序信息文件 (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] 部分用于为设备配置 DeviceInterfaceGUID 集。每个设备接口都必须有一个 GUID,这样应用才能找到该接口并通过 Windows API 连接到该接口。使用 New-Guid PowerShell cmdlet 或在线工具生成随机 GUID。

为了便于开发,Zadig 工具提供了一个简单的接口,用于使用 WinUSB 驱动程序替换为 USB 接口加载的驱动程序。

Microsoft 操作系统兼容性描述符

上述 INF 文件方法非常繁琐,因为它需要提前配置每个用户的计算机。Windows 8.1 及更高版本通过使用自定义 USB 描述符提供了替代方案。首次连接设备时,这些描述符会向 Windows 操作系统提供信息,这些信息通常包含在 INF 文件中。

设置好 WebUSB 描述符后,您也可以轻松添加 Microsoft 的操作系统兼容性描述符。首先,使用此额外的平台功能描述符扩展 BOS 描述符。请务必更新 wTotalLengthbNumDeviceCaps 来反映此问题。

字段 说明
Microsoft OS 2.0 平台功能描述符
0x1C bLength 此描述符的大小
0x10 bDescriptorType 设备功能描述符
0x05 bDevCapabilityType 平台功能描述符
0x00 bReserved
{0xDF, 0x60, 0xDD, 0xD8, 0x89, 0x45, 0xC7, 0x4C, 0x9C, 0xD2, 0x65, 0x9D, 0x9E, 0x64, 0x8A, 0x9F} PlatformCapablityUUID 采用小端格式的 Microsoft OS 2.0 平台兼容性描述符 GUID
0x06030000 dwWindowsVersion 最低的兼容 Windows 版本 (Windows 8.1)
0x00B2 wMSOSDescriptorSetTotalLength 描述符集的总长度
0x02 bMS_VendorCode 用于检索更多 Microsoft 描述符的 bRequest 值
0x00 bAltEnumCode 设备不支持替代枚举

与 WebUSB 描述符一样,您必须选择一个 bRequest 值,以供与这些描述符相关的控制传输使用。在此示例中,我选择了 0x020x07 位于 wIndex 中,是用于从设备检索 Microsoft OS 2.0 描述符集的命令。

bmRequestType bRequest wValue wIndex wLength 数据(响应)
0b11000000 0x02 0x0000 0x0007 * MS OS 2.0 描述符组

USB 设备可以有多个函数,因此描述符集的第一部分描述了与跟随属性关联的函数。以下示例配置的是复合设备的接口 1。描述符为操作系统提供了有关此接口的两条重要信息。兼容的 ID 描述符可告知 Windows 此设备与 WinUSB 驱动程序兼容。该注册表属性描述符的功能与上述 INF 示例的 [Dev_AddReg] 部分类似,都会设置一个注册表属性,以便为此函数分配设备接口 GUID。

字段 说明
Microsoft OS 2.0 描述符集标头
0x000A wLength 此描述符的大小
0x0000 wDescriptorType 描述符组标头描述符
0x06030000 dwWindowsVersion 最低的兼容 Windows 版本 (Windows 8.1)
0x00B2 wTotalLength 描述符集的总长度
Microsoft OS 2.0 配置子集标题
0x0008 wLength 此描述符的大小
0x0001 wDescriptorType 配置子集标题说明
0x00 bConfigurationValue 适用于配置 1(尽管通常配置是从 1 开始索引的,但也从 0 开始)
0x00 bReserved 必须设为 0
0x00A8 wTotalLength 包括此标头的子集的总长度
Microsoft OS 2.0 函数子集标题
0x0008 wLength 此描述符的大小
0x0002 wDescriptorType 函数子集标头描述符
0x01 bFirstInterface 函数的第一个接口
0x00 bReserved 必须设为 0
0x00A0 wSubsetLength 包括此标头的子集的总长度
与 Microsoft OS 2.0 兼容的 ID 描述符
0x0014 wLength 此描述符的大小
0x0003 wDescriptorType 兼容的 ID 描述符
"WINUSB\0\0" CompatibileID 填充到 8 个字节的 ASCII 字符串
"\0\0\0\0\0\0\0\0" SubCompatibleID 填充到 8 个字节的 ASCII 字符串
Microsoft OS 2.0 注册表属性描述符
0x0084 wLength 此描述符的大小
0x0004 wDescriptorType 注册表属性描述符
0x0007 wPropertyDataType REG_MULTI_SZ
0x002A wPropertyNameLength 属性名称的长度
"DeviceInterfaceGUIDs\0" PropertyName 采用 UTF-16LE 编码且带有 null 终止符的属性名称
0x0050 wPropertyDataLength 属性值的长度
"{XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX}\0\0" PropertyData GUID 加两个采用 UTF-16LE 编码的空终止符

对于此类信息,Windows 只会查询设备一次。如果设备没有使用有效的描述符响应,则下次连接设备时,它不会再次询问。Microsoft 提供了 USB 设备注册表条目列表,其中介绍了枚举设备时创建的注册表条目。在测试时,请删除为设备创建的条目,以强制 Windows 再次尝试读取描述符。

如需了解详情,请查看 Microsoft 有关如何使用这些描述符的博文

示例

您可以在以下项目中找到实现 WebUSB 感知设备(同时包含 WebUSB 描述符和 Microsoft 操作系统描述符)的示例代码: