WebUSB API 支持 Web,通过将其引入到 Web 中,提高了 USB 的安全性和易用性。
如果我简单直白地说“USB”,您很有可能会立即想到键盘、鼠标、音频、视频和存储设备。没错,不过您会发现其他种类的通用串行总线 (USB) 设备。
这些非标准化 USB 设备要求硬件���应商编写平台专用驱动程序和 SDK,以便您(开发者)利用它们。遗憾的是,这些平台专用代码长期以来阻止了 Web 使用这些设备。这也是创建 WebUSB API 的原因之一:提供一种方法来向网络提供 USB 设备服务。借助此 API,硬件制造商将能够为其设备构建跨平台 JavaScript SDK。
但最重要的是,将 USB 引入 Web 中,这将使 USB 更安全、更易于使用。
我们来看一看 WebUSB API 的预期行为:
- 购买 USB 设备。
- 将其插入计算机。系统会立即显示一条通知,其中包含此设备可以前往的正确网站。
- 点击此通知。网站已准备就绪,可以使用了!
- 点击即可连接,Chrome 中会出现一个 USB 设备选择器,您可以在其中选择设备。
好啦!
如果没有 WebUSB API,此过程会怎么样?
- 安装针对具体平台的应用。
- 如果我的操作系统甚至支持它,请验证我下载的内容是否正确。
- 安装游戏。如果幸运,您将不会收到可怕的操作系统提示,也不会看到关于从互联网安装驱动程序/应用的弹出式窗口。如果运气不好,安装的驱动程序或应用就会出现故障并损害您的计算机。(请注意,网络的初衷是包含运行异常的网站)。
- 如果您只使用一次该功能,该代码就会一直保留在您的计算机上,直到您考虑将其移除。(在 Web 上,未使用的空间最终会被回收)。
开始前须知
本文假定您对 USB 的工作原理有基本的了解。如果没有,建议您阅读 NutShell 中的 USB。如需了解有关 USB 的背景信息,请参阅官方 USB 规范。
Chrome 61 支持 WebUSB API。
可用于源试用
为了从实际使用 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 教程。此外,() => {}
只是 ECMAScript 2015 箭头函数。
获取 USB 设备的访问权限
您可以使用 navigator.usb.requestDevice()
提示用户选择连接的一台 USB ���备,也可以调用 navigator.usb.getDevices()
来获取网站有权访问的所有已连接 USB 设备的列表。
navigator.usb.requestDevice()
函数采用强制性的 JavaScript 对象来定义 filters
。这些过滤条件用于匹配具有指定供应商 (vendorId
) 标识符和(可选)产品 (productId
) 标识符的任何 USB 设备。也可以在其中定义 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 Serial API,您可以使用它来替换默认 API。
再次查看 JavaScript 代码。在用户选择 device
后,device.open()
会运行所有特定于平台的��骤,以启动与 USB 设备的会话。然后,我必须使用 device.selectConfiguration()
选择可用的 USB 配置。请注意,配置指定了设备的电源方式、最大功耗和接口数量。说到接口,我还需要使用 device.claimInterface()
请求独占访问权限,因为只有在声明接口时,数据才能传输到接口或关联端点。最后,需要调用 device.controlTransferOut()
,以使用适当的命令设置 Arduino 设备,以通过 WebUSB Serial API 进行通信。
然后,device.transferIn()
会将数据批量传输到设备上,以告知它主机已准备好接收批量数据。然后,使用包含必须正确解析的 DataView data
的 result
对象来执行 promise。
如果您熟悉 USB,那么所有这些看起来都会非常熟悉。
我想要更多
您可以通过 WebUSB API 与所有 USB 传输/端点类型进行交互:
- 用于向 USB 设备发送或接收配置或命令参数的 CONTROL 传输作业通过
controlTransferIn(setup, length)
和controlTransferOut(setup, data)
进行处理。 - INTERRUPT 传输用于处理少量时效性数据,其处理方法与使用
transferIn(endpointNumber, length)
和transferOut(endpointNumber, data)
的 BULK 传输的处理方式相同。 - ISOCHRONOUS 传输(用于视频和声音等数据流)通过
isochronousTransferIn(endpointNumber, packetLengths)
和isochronousTransferOut(endpointNumber, data, packetLengths)
处理。 - BULK 传输用于以可靠方式传输大量非时间敏感数据,它通过
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
,您可以在一个位置查看所有与 USB 设备相关的事件,从而更轻松地在 Chrome 中调试 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"
其中,如果您的设备是 Arduino,[yourdevicevendor]
为 2341
。
您还可以为更具体的规则添加 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
- 实现错误: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 审核本文。