通过网页访问 USB 设备

WebUSB API 将 USB 引入 Web,使 USB 更安全、更易用。

François Beaufort
François Beaufort

如果我说得简单明了,“USB”的话,您很有可能会 用户会立刻想到键盘、鼠标、音频、视频和存储设备。您 但您可以在此网站上找到其他类型的通用串行总线 (USB)设备

这些非标准化 USB 设备需要硬件供应商编写平台专用代码 驱动程序和 SDK 以供您(开发者)使用。 遗憾的是,这个特定于平台的代码历来阻止使用这些设备 。这也是创建 WebUSB API 的原因之一: 提供了一种向网络公开 USB 设备服务的方法。借助此 API,硬件 制造商将能够为自己的产品开发跨平台 JavaScript SDK, 设备。

但最重要的是,这将使 USB 变得更安全、更易于使用 并发布到网络上

我们来看一下使用 WebUSB API 时可能会发生的行为:

  1. 购买 USB 设备。
  2. 将其插入计算机。系统会立即显示一条通知 要访问这个设备的网站
  3. 点击此通知。网站已经创建完毕,可以使用了!
  4. 点击即可连接,Chrome 中会显示 USB 设备选择器,以便您 选择你的设备。

好啦!

如果没有 WebUSB API,此过程会是怎样的?

  1. 安装针对具体平台的应用。
  2. 如果我的操作系统支持这项功能,请确认我已下载 正确。
  3. 安装这个东西。幸运的话,你不会看到可怕的操作系统提示或弹出式窗口 警告您有关从互联网安装驱动程序/应用的警告。如果 您很幸运,安装的驱动程序或应用程序会发生故障,造成危害 。(请注意,网络包含各种故障 网站)。
  4. 如果您只使用该功能一次,代码会一直保留在您的计算机上,直到您 建议将其移除。(在 Web 上,未使用的空间最终 reclaimed.)

准备工作

本文假定您对 USB 的工作原理有基本的了解。如果不是,我 建议阅读在 NutShell 中使用 USB。有关 USB 的背景信息 请查看官方 USB 规范

WebUSB API 可在 Chrome 61 中使用。

可用于源试用

为了从使用 WebUSB 的开发者那里获得尽可能多的反馈 API,但我们之前已在 Chrome 54 和 Chrome 中添加此功能 57 作为源试用

最新的试用已于 2017 年 9 月成功完成。

隐私权和安全

仅限 HTTPS

由于此功能十分强大,它仅适用于安全上下文。这意味着 因此您在构建应用时需要考虑 TLS

需要用户手势

出于安全方面的考虑,navigator.usb.requestDevice() 可以通过用户手势(例如触摸或点击鼠标)来调用。

权限政策

权限政策是一种机制,可让开发者有选择性地启用 以及停用各种浏览器功能和 API。它可以通过 标头和/或 iframe“允许”属性。

您可以定义一项权限政策,用于控制是否将 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 设备的访问权限

您可以提示用户选择单个已连接的 USB 设备。 navigator.usb.requestDevice()或调用 navigator.usb.getDevices() 以获取 网站有权访问的所有已连接 USB 设备的列表。

navigator.usb.requestDevice() 函数接受一个必需的 JavaScript 对象 ,用于定义 filters。这些过滤器用于匹配任何具有 指定的供应商 (vendorId) 和(可选)产品 (productId) 标识符。 classCodeprotocolCodeserialNumbersubclassCode 键 也应在此处定义

<ph type="x-smartling-placeholder">
</ph> Chrome 中的 USB 设备用户提示的屏幕截图
USB 设备用户提示。

例如,下面说明了如何访问已配置的已连接 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 数字。我直接搜索了“Arduino”一词USB ID 列表中列出。

上面已实现的 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 设备。点击此通知将打开着陆页。

<ph type="x-smartling-placeholder">
</ph> Chrome 中的 WebUSB 通知的屏幕截图
WebUSB 通知

与 Arduino USB 开发板对话

现在,我们来了解一下 USB 端口上的 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 的会话 设备。然后,我需要选择一个可用的 USB 配置 device.selectConfiguration()。请注意,配置指定 设备处于通电状态、其最大功耗以及接口数量。 说到接口,我还需要向 device.claimInterface(),因为数据只能传输到接口或 关联端点。最后调用 使用 device.controlTransferOut() 设置 Arduino 设备 通过 WebUSB Serial API 进行通信。

然后,device.transferIn() 会将数据批量传输到 以通知主机已准备好接收批量数据。然后, promise 执行时,返回一个 result 对象,该对象包含一个 DataView data, 必须进行适当解析

如果您熟悉 USB,会发现所有这些都非常熟悉。

我想要更多

通过 WebUSB API,您可以与所有 USB 传输/端点类型进行交互:

  • CONTROL 传输,用于发送或接收配置或命令 参数传递给 USB 设备,通过 controlTransferIn(setup, length)controlTransferOut(setup, data) 进行处理。
  • INTERRUPT 传输用于少量对时间敏感的数据, 处理方法与使用 BULK 传输 transferIn(endpointNumber, length)transferOut(endpointNumber, data)
  • ISOCHRONOUS 传输用于视频和声音等数据流, 已由 isochronousTransferIn(endpointNumber, packetLengths)isochronousTransferOut(endpointNumber, data, packetLengths)
  • 批量传输,用于在 通过 transferIn(endpointNumber, length) 进行处理,并且 transferOut(endpointNumber, data)

您可能还需要了解一下 Mike Tsao 的 WebLight 项目, 提供了一个构建 USB 控制 LED 设备的全面示例, (此处不使用 Arduino)。包括硬件、软件 和固件

撤消对 USB 设备的访问权限

网站可以清理不再需要的 USB 设备的访问权限 方法是对 USBDevice 实例调用 forget()。例如,对于 用于一台具有众多设备(大型语言)的共享计算机上的 累积的用户生成权限数量会造成糟糕的用户体验。

// Voluntarily revoke access to this USB device.
await device.forget();

由于 Chrome 101 或更高版本支持forget(),因此请检查此功能是否 支持以下各项:

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 设备相关的事件。

<ph type="x-smartling-placeholder">
</ph> 用于在 Chrome 中调试 WebUSB 的设备日志页面的屏幕截图
Chrome 中用于调试 WebUSB API 的设备日志页面。

内部页面 about://usb-internals 也非常方便, 来模拟虚拟 WebUSB 设备的连接和断开连接。 这对于在没有真实硬件的情况下进行界面测试非常有用。

<ph type="x-smartling-placeholder">
</ph> 在 Chrome 中调试 WebUSB 的内部页面的屏幕截图
Chrome 中用于调试 WebUSB API 的内部页面。

在大多数 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}。请确保您的 userplugdev 群组的成员。然后,重新连接设备即可。

资源

使用 # 标签向 @ChromiumDev 发送推文 #WebUSB 并告诉我们您使用它的地点和方式。

致谢

感谢 Joe Medley 审核本文。