构建支持 WebUSB 的设备

构建一个能够充分利用 WebUSB API 的设备。

本文介绍了如何构建设备以充分利用 WebUSB API。如需简要了解该 API 本身,请参阅在 Web 上访问 USB 设备

背景

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

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

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

API 基础知识

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

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

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

字段 说明
配置描述符
0x09 bLength 此描述符的大小
0x02 bDescriptorType 配置描述符
0x0039 wTotalLength 此系列描述符的总长度
0x02 bNumInterfaces 接口数量
0x01 bConfigurationValue Configuration 1
0x00 iConfiguration 配置名称(无)
0b1010000 bmAttributes 具有远程唤醒功能的自供电设备
0x32 bMaxPower 最大功率以 2 mA 为增量表示
接口描述符
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 批量
0x0040 wMaxPacketSize 64 字节数据包
0x00 bInterval 批量端点不适用
端点描述符
0x07 bLength 此描述符的大小
0x05 bDescriptorType 端点描述符
0b00000011 bEndpointAddress 端点 3(输出)
0b00000010 bmAttributes 批量
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 平台 capability 描述符
0x18 bLength 此描述符的大小
0x10 bDescriptorType 设备功能描述符
0x05 bDevCapabilityType 平台 capability 描述符
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 设备的一致接口,但开发者仍应了解对应用施加的要求,例如 Web 浏览器要求,以便访问设备。

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(),系统也会再次显示此提示。

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

ChromeOS

ChromeOS 也是基于 Linux 的,并且无需对系统配置进行任何修改。permission_broker 服务控制对 USB 设备的访问权限,只要至少有一个未声明的接口,浏览器便可以访问这些设备。

Windows

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

驱动程序信息文件 (INF)

驱动程序信息文件会告知 Windows 在首次遇到设备时应执行的操作。由于用户的系统已包含 WinUSB 驱动程序,因此您只需让 INF 文件将您的供应商 ID 和产品 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 工具提供了一个简单的接口,用于将为 USB 接口加载的驱动程序替换为 WinUSB 驱动程序。

Microsoft 操作系统兼容性描述符

上述 INF 文件方法很麻烦,因为它需要预先配置每位用户的机器。Windows 8.1 及更高版本提供了一种通过使用自定义 USB 描述符的替代方案。当设备首次插入时,这些描述符会向 Windows 操作系统提供通常包含在 INF 文件中的信息。

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

字段 说明
Microsoft OS 2.0 平台 capability 描述符
0x1C bLength 此描述符的大小
0x10 bDescriptorType 设备功能描述符
0x05 bDevCapabilityType 平台 capability 描述符
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 值,以供与这些描述符相关的控制传输使用。在此示例中,我选择了 0x02。放置在 wIndex 中的 0x07 是用于从设备检索 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 ASCII 字符串已填充为 8 个字节
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 编码的 null 终止符

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

如需了解详情,请参阅 Microsoft 的博文,了解如何使用这些描述符。

示例

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