WebUSB API 将 USB 引入到 Web 中,使其更安全、更易用。
如果我简单明了地说出“USB”,您很可能会立即想到键盘、鼠标、音频设备、视频设备和存储设备。没错,但您会发现其他类型的通用串行总线 (USB) 设备。
这些非标准化的 USB 设备要求硬件供应商编写针对具体平台的驱动程序和 SDK,以便您(开发者)能够充分利用它们。遗憾的是,这种平台专用代码长期以来一直阻止 Web 使用这些设备。这正是 WebUSB API 的用途之一:提供一种将 USB 设备服务公开到 Web 的方法。借助此 API,硬件制造商将能够为其设备构建跨平台 JavaScript SDK。
但最重要的是,这将通过将 USB 引入 Web 来提高其安全性并简化其使用。
我们来看看使用 WebUSB API 时可能出现的行为:
- 购买 USB 设备。
- 将其插入计算机。系统会立即显示一条通知,其中包含适用于此设备的正确网站。
- 点击此通知。网站已准备就绪,可以使用了!
- 点击连接,Chrome 中会显示一个 USB 设备选择器,您可以在其中选择设备。
大功告成!
如果没有 WebUSB API,此过程会是怎样的?
- 安装特定于平台的应用。
- 即使我的操作系统支持该应用,我也需要确认自己下载的是否为正确的应用。
- 安装这个东西。如果幸运的话,您不会收到可怕的操作系统提示或弹出式窗口,提醒您从互联网安装驱动程序/应用。如果您运气不佳,安装的驱动程序或应用可能会出现故障,并损害您的计算机。(请注意,网络本身就包含无法正常运行的网站。)
- 如果您只使用过此功能一次,则该代码会一直保留在您的计算机上,直到您想移除它为止。(在 Web 上,系统最终会回收未使用的空间。)
开始之前
本文假定您对 USB 的工作原理有基本的了解。如果没有,建议您阅读 USB in a NutShell。如需了解 USB 的背景信息,请参阅 USB 官方规范。
WebUSB API 在 Chrome 61 中提供。
可用于源试用
为了从实际使用 WebUSB API 的开发者那里获得尽可能多的反馈,我们之前在 Chrome 54 和 Chrome 57 中添加了此功能作为源试用。
最新的试用已于 2017 年 9 月成功结束。
隐私权和安全
仅限 HTTPS
由于此功能十分强大,它仅适用于安全上下文。这意味着您在构建时需要考虑 TLS。
需要用户手势
出于安全考虑,navigator.usb.requestDevice()
只能通过用户手势(例如轻触或点击鼠标)调用。
“权限”政策
权限政策是一种机制,可让开发者选择启用和停用各种浏览器功能和 API。您可以通过 HTTP 标头和/或 iframe“allow”属性来定义它。
您可以定义权限政策,用于控制是否在 Navigator 对象上公开 usb
属性,换句话说,就是您是否允许使用 WebUSB。
以下是不允许使用 WebUSB 的标头政策示例:
Feature-Policy: fullscreen "*"; usb "none"; payment "self" https://payment.example.com
下面是允许 USB 的另一个容器政策示例:
<iframe allowpaymentrequest allow="usb; fullscreen"></iframe>
开始编码
WebUSB API 在很大程度上依赖于 JavaScript Promise。如果您不熟悉 Promise,请参阅这篇出色的 Promise 教程。还有一点,() => {}
只是 ECMAScript 2015 箭头函数。
获取 USB 设备的访问权限
您可以使用 navigator.usb.requestDevice()
提示用户选择单个已连接的 USB 设备,也可以调用 navigator.usb.getDevices()
来获取已获授权访问的所有已连接 USB 设备的列表。
navigator.usb.requestDevice()
函数接受用于定义 filters
的强制性 JavaScript 对象。这些过滤器用于将任何 USB 设备与给定供应商 (vendorId
) 和(可选)产品标识符 (productId
) 标识符进行匹配。您还可以在其中定义 classCode
、protocolCode
、serialNumber
和 subclassCode
键。
例如,下面介绍了如何访问已配置为允许该来源的已连接 Arduino 设备。
navigator.usb.requestDevice({ filters: [{ vendorId: 0x2341 }] })
.then(device => {
console.log(device.productName); // "Arduino Micro"
console.log(device.manufacturerName); // "Arduino LLC"
})
.catch(error => { console.error(error); });
在您提出疑问之前,我先说明一下,这个 0x2341
十六进制数并非凭空而来。我就是在这个 USB ID 列表中搜索了“Arduino”一词。
上述已执行 promise 中返回的 USB device
包含一些基本但重要的设备信息,例如支持的 USB 版本、最大数据包大小、供应商和产品 ID,以及设备可能具有的可能配置数量。它基本上包含设备 USB 描述符中的所有字段。
// Get all connected USB devices the website has been granted access to.
navigator.usb.getDevices().then(devices => {
devices.forEach(device => {
console.log(device.productName); // "Arduino Micro"
console.log(device.manufacturerName); // "Arduino LLC"
});
})
顺便提一下,如果 USB 设备宣布支持 WebUSB 并定义了着陆页网址,Chrome 会在用户插入 USB 设备时显示一个永久性通知。点击此通知即可打开着陆页。
与 Arduino USB 板交谈
好的,现在我们来看看如何通过 USB 端口从与 WebUSB 兼容的 Arduino 开发板进行通信。请参阅 https://github.com/webusb/arduino 中的说明,为您的草稿启用 WebUSB。
别担心,我将在本文后面部分介绍所有 WebUSB 设备方法。
let device;
navigator.usb.requestDevice({ filters: [{ vendorId: 0x2341 }] })
.then(selectedDevice => {
device = selectedDevice;
return device.open(); // Begin a session.
})
.then(() => device.selectConfiguration(1)) // Select configuration #1 for the device.
.then(() => device.claimInterface(2)) // Request exclusive control over interface #2.
.then(() => device.controlTransferOut({
requestType: 'class',
recipient: 'interface',
request: 0x22,
value: 0x01,
index: 0x02})) // Ready to receive data
.then(() => device.transferIn(5, 64)) // Waiting for 64 bytes of data from endpoint #5.
.then(result => {
const decoder = new TextDecoder();
console.log('Received: ' + decoder.decode(result.data));
})
.catch(error => { console.error(error); });
请注意,我使用的 WebUSB 库只是实现一个示例协议(基于标准 USB 串行协议),制造商可以根据需要创建任何集合和类型的端点。控制传输对于小型配置命令特别有用,因为它们具有总线优先级和明确定义的结构。
这是上传到 Arduino 板的草图。
// Third-party WebUSB Arduino library
#include <WebUSB.h>
WebUSB WebUSBSerial(1 /* https:// */, "webusb.github.io/arduino/demos");
#define Serial WebUSBSerial
void setup() {
Serial.begin(9600);
while (!Serial) {
; // Wait for serial port to connect.
}
Serial.write("WebUSB FTW!");
Serial.flush();
}
void loop() {
// Nothing here for now.
}
上述示例代码中使用的第三方 WebUSB Arduino 库主要执行以下两项操作:
- 该设备充当 WebUSB 设备,让 Chrome 能够读取着陆页网址。
- 它会公开一个 WebUSB 串行 API,您可以使用该 API 替换默认 API。
再来看看 JavaScript 代码。在获得用户选择的 device
后,device.open()
会运行所有针对具体平台的步骤,以启动与 USB 设备的会话。然后,我必须使用 device.selectConfiguration()
选择一个可用的 USB 配置。请注意,配置用于指定设备的电源方式、最大功耗和接口数量。说到接口,我还需要使用 device.claimInterface()
请求独占访问权限,因为只有在声明接口后,数据才能传输到接口或关联的端点。最后,需要调用 device.controlTransferOut()
以使用适当的命令设置 Arduino 设备,以便通过 WebUSB 串行 API 进行通信。
然后,device.transferIn()
会对设备执行批量传输,以告知设备主机已准备好接收批量数据。然后,使用包含 DataView data
的 result
对象来实现该 promise,该 data
必须进行适当解析。
如果您熟悉 USB,那么所有这些都应该非常熟悉。
我想要更多
借助 WebUSB API,您可以与所有 USB 传输/端点类型进行交互:
- 用于向 USB 设备发送或接收配置或命令参数的 CONTROL 传输由
controlTransferIn(setup, length)
和controlTransferOut(setup, data)
处理。 - 用于少量具有时间敏感性的数据的 INTERRUPT 传输的处理方法与使用
transferIn(endpointNumber, length)
和transferOut(endpointNumber, data)
的 BULK 传输相同。 - 用于视频和音频等数据流的异步传输由
isochronousTransferIn(endpointNumber, packetLengths)
和isochronousTransferOut(endpointNumber, data, packetLengths)
处理。 - 批量传输用于以可靠的方式传输大量非实时数据,可通过
transferIn(endpointNumber, length)
和transferOut(endpointNumber, data)
进行处理。
您还可以查看 Mike Tsao 的 WebLight 项目,该项目提供了一个从头开始构建专为 WebUSB API 设计的 USB 控制 LED 设备的示例(此处不使用 Arduino)。您会看到硬件、软件和固件。
撤消对 USB 设备的访问权限
网站可以通过对 USBDevice
实例调用 forget()
来清理访问不再需要的 USB 设备的权限。例如,对于在多台设备的共享计算机上使用的教育性 Web 应用,用户生成的大量权限累积起来会带来糟糕的用户体验。
// Voluntarily revoke access to this USB device.
await device.forget();
由于 forget()
在 Chrome 101 或更高版本中提供,请通过以下方式检查设备是否支持此功能:
if ("usb" in navigator && "forget" in USBDevice.prototype) {
// forget() is supported.
}
转移大小限制
某些操作系统会对待处理的 USB 事务中可以包含的数据量施加限制。将数据拆分为较小的事务,并一次只提交几个事务,有助于避免这些限制。还可以减少内存使用量,并让应用在传输完成时报告进度。
由于提交到端点的多个传输始终按顺序执行,因此可以通过提交多个队列分块来提高吞吐量,以避免 USB 传输之间的延迟。每次传输完一个分块时,它都会通知您的代码应提供更多数据,如下面的辅助函数示例所记录。
const BULK_TRANSFER_SIZE = 16 * 1024; // 16KB
const MAX_NUMBER_TRANSFERS = 3;
async function sendRawPayload(device, endpointNumber, data) {
let i = 0;
let pendingTransfers = [];
let remainingBytes = data.byteLength;
while (remainingBytes > 0) {
const chunk = data.subarray(
i * BULK_TRANSFER_SIZE,
(i + 1) * BULK_TRANSFER_SIZE
);
// If we've reached max number of transfers, let's wait.
if (pendingTransfers.length == MAX_NUMBER_TRANSFERS) {
await pendingTransfers.shift();
}
// Submit transfers that will be executed in order.
pendingTransfers.push(device.transferOut(endpointNumber, chunk));
remainingBytes -= chunk.byteLength;
i++;
}
// And wait for last remaining transfers to complete.
await Promise.all(pendingTransfers);
}
提示
借助内部页面 about://device-log
,您可以更轻松地在 Chrome 中调试 USB,在一个位置查看所有与 USB 设备相关的事件。
内部页面 about://usb-internals
也很实用,可让您模拟虚拟 WebUSB 设备的连接和断开连接。这对于在不使用真实硬件的情况下进行界面测试非常有用。
在大多数 Linux 系统中,USB 设备默认映射为具有只读权限。如需允许 Chrome 打开 USB 设备,您需要添加新的 udev 规则。在 /etc/udev/rules.d/50-yourdevicename.rules
下创建一个文件,其中包含以下内容:
SUBSYSTEM=="usb", ATTR{idVendor}=="[yourdevicevendor]", MODE="0664", GROUP="plugdev"
其中 [yourdevicevendor]
为 2341
(例如,如果您的设备是 Arduino)。您还可以添加 ATTR{idProduct}
,以便获得更具体的规则。确保您的 user
是 plugdev
群组的成员。然后,只需重新连接设备即可。
资源
- Stack Overflow:https://stackoverflow.com/questions/tagged/webusb
- WebUSB API 规范:http://wicg.github.io/webusb/
- Chrome 功能状态:https://www.chromestatus.com/feature/5651917954875392
- 规范问题:https://github.com/WICG/webusb/issues
- 实现 bug:http://crbug.com?q=component:Blink>USB
- WebUSB ❤ ️Arduino:https://github.com/webusb/arduino
- IRC:W3C IRC 中的 #webusb
- WICG 邮寄名单:https://lists.w3.org/Archives/Public/public-wicg/
- WebLight 项目:https://github.com/sowbug/weblight
使用 #WebUSB
标签向 @ChromiumDev 发送推文,告诉我们您在哪里以及如何使用该工具。
致谢
感谢 Joe Medley 审核本文。