USB 设备

该文档描述如何使用 USB API 与 USB 设备通信。一些设备不能通过 USB API 访问(有关详情请参见下面的注意事项部分),Chrome 应用还可以连接到串行端口蓝牙设备。

示例:有关演示如何在 Chrome 应用中连接到硬件设备的例子,请参见 serialservousbzephyr_hxm 蓝牙示例。

有关 USB 的背景信息,请参见官方的 USB 规范(英文)。
USB 简介(英文)是一篇速成文章,可能对您有帮助。

清单文件的要求

USB API 要求清单文件中包含 "usb" 权限:

"permissions": [
  "usb"
]

此外,为了防止收集指纹信息,您必须在清单文件中声明需要访问的所有设备类型。每一种 USB 设备对应制造商标识符与产品标识符(VID/PID),您可以使用 usb.getDevices 根据 VID/PID 对枚举设备。

"permissions": [
  "usbDevices": [
    {
      "vendorId": 123,
      "productId": 456
    }
  ]
]

注意,JSON 格式中只允许十进制数,您不能在这些字段中使用十六进制数。

发现设备

要确定一个或多个设备是否连接到用户的系统,请使用 usb.getDevices 方法:

chrome.usb.getDevices(enumerateDevicesOptions, callback);

参数(类型) 描述
EnumerateDevicesOptions (object) 指定 vendorId (long) 和 productId (long) 的对象,用来寻找总线上正确类型的设备。您必须在清单文件的 usbDevices 权限中列出您的应用需要访问的所有设备对应的 vendorIddeviceId
callback (function) 设备枚举完成后调用。执行回调函数时传递的参数为 Device 对象的数组,包含三个属性:handlevendorIdproductIddevice 属性是已连接的设备对应的稳定标识符,在设备拔出之前不会更改。标识符的细节是不透明的,并且随时会更改,请不要依赖它目前的类型。
如果没有找到设备,该数组为空。

例如:

function onDeviceFound(devices) {
  this.devices=devices;
  if (devices) {
    if (devices.length > 0) {
      console.log("发现设备:"+devices.length);
    } else {
      console.log("找不到设备");
    }
  } else {
    console.log("拒绝访问。");
  }
}

chrome.usb.getDevices({"vendorId": vendorId, "productId": productId}, onDeviceFound);

打开设备

返回 Device 对象后,您就可以使用 usb.openDevice 打开设备并获取一个连接句柄。您必须通过连接句柄与 USB 设备通信。

属性 描述
device usb.getDevices 回调函数中接收到的对象。
data (arraybuffer) 如果为传入传输,则包含设备发送的数据。

例如:

var usbConnection = null;
var onOpenCallback = function(connection) {
  if (connection) {
    usbConnection = connection;
    console.log("设备已打开。");
  } else {
    console.log("无法打开设备。");
  }
};

chrome.usb.openDevice(device, onOpenCallback);

并不是任何设备都能成功打开。通常操作系统会锁定许多类型的 USB 设备(例如键盘和鼠标、大容量存储设备、网络摄像头等等),它们不能由用户应用程序占有。在 Linux 中(除了 Chrome OS),一旦设备的某一接口被操作系统锁定,即使该设备的其他接口理论上可以使用,整个设备都将锁定(因为所有接口共享同一设备文件)。在 Chrome OS 中,您可以使用 usb.requestAccess 方法请求访问解锁的设备。如果允许,权限控制器会为您解锁设备。

为了简化打开设备的过程,您可以使用 usb.findDevices 方法,通过一次调用就能枚举、请求访问并打开设备:

chrome.usb.findDevices({"vendorId": vendorId, "productId": productId, "interfaceId": interfaceId}, callback);

以上代码等价于:

chrome.usb.getDevices({"vendorId": vendorId, "productId": productId}, function (devices) {
  if (!devices) {
    console.log("无法枚举设备。");
    callback();
    return;
  }
  var connections = [], pendingAccessRequests = devices.length;
  devices.forEach(function (device) {
    chrome.usb.requestAccess(interfaceId, function () {
      // 这里不需要检查错误,即使发生了错误也不会有什么问题。
      // 您始终应该尝试打开设备。
      chrome.usb.openDevices(device, function (connection) {
        if (connection) connections.push(connection);
        pendingAccessRequests--;
        if (pendingAccessRequests == 0) {
          callback(connections);
        }
      });
    });
  })
});

USB 传输以及从设备接收数据

USB 协议定义了四种类型的传输:控制大块同步中断。这些传输方式将在下面描述:

传输可以双向进行:设备到主机(传入)和主机到设备(传出)。根据 USB 协议,传入与传出消息都必须由主机(运行 Chrome 应用的计算机)发起。对于传入(设备到主机)消息,主机(由您的 JavaScript 代码发起)向设备发送标记为“传入”的消息。消息的具体细节取决于设备,但是通常会以某种方式标识您请求的内容,然后设备以请求的数据响应。设备的响应由 Chrome 浏览器处理,并异步地传递给您在传输方法中指定的回调函数。传出(主机到设备)消息与之类似,但是响应不包含从设备返回的数据。

对于来自设备的每一条消息,指定的回调函数都会接收一个具有如下属性的事件对象:


属性 描述
resultCode (integer) 0 表示成功,其他值表示失败。表示失败时可以从 chrome.extension.lastError 读取错误字符串。
data (arraybuffer) 如果是传入传输则包含设备发送的数据。

例如:

var onTransferCallback = function(event) {
   if (event && event.resultCode === 0 && event.data) {
     console.log("收到 " + event.data.byteLength + " 字节");
   }
};

chrome.usb.bulkTransfer(connectionHandle, transferInfo, onTransferCallback);

控制传输

控制传输通常用来向 USB 设备发送或接收配置或命令参数。controlTransfer 方法始终从端点 0 发送/读取,不需要调用 claimInterface。该方法很简单,接收三个参数。

chrome.usb.controlTransfer(connectionHandle, transferInfo, transferCallback)

参数(类型) 描述
connectionHandle usb.openDevice 回调函数中接收的对象。
transferInfo 参数对象,包含来自下表的值。参考您的 USB 设备协议规范了解具体的值。
transferCallback() 传输完成时调用。

transferInfo 对象的值:

描述
requestType (string) "vendor"、"standard"、"class" 或 "reserved"
recipient (string) "device"、"interface"、"endpoint" 或 "other"
direction (string) "in" 或 "out"。"in"(输入)方向用于通知设备应该向主机发送信息,USB 总线中的所有通信都是主机启动的,所以使用 "in" 传输允许设备发回信息。
request (integer) 由您的设备协议定义。
value (integer) 由您的设备协议定义。
index (integer) 由您的设备协议定义。
length (integer) 只有当 direction 为 "in" 时才使用,通知设备主机期望的响应包含的数据量。
data (arraybuffer) 由您的设备协议定义,当 direction 为 "out" 时必选。

例如:

var transferInfo = {
  "requestType": "vendor",
   "recipient": "device",
  "direction": "out",
  "request":  0x31,
  "value": 120,
  "index": 0,
  // 注意,要使用 ArrayBuffer,而不是 TypedArray 本身
  "data": new Uint8Array([4, 8, 15, 16, 23, 42]).buffer
};
chrome.usb.controlTransfer(connectionHandle, transferInfo, optionalCallback);

同步传输

同步传输是最复杂的 USB 传输类型,通常用于数据流,例如视频与声音。要开始同步传输(无论是传入还是传出),您必须使用 usb.isochronousTransfer 方法:

chrome.usb.isochronousTransfer(connectionHandle, isochronousTransferInfo, transferCallback)

参数 描述
connectionHandle usb.openDevice 回调函数中接收的对象。
isochronousTransferInfo 参数对象,包含来自下表的值。
transferCallback() 传输完成时调用。

用于 isochronousTransferInfo 对象的值:

描述
transferInfo (object) 包含下列属性的参数对象:
direction (string): "in" 或 "out"。
endpoint (integer): 由您的设备定义,通常可以通过 USB 检测工具找到,例如 lsusb -v
length (integer): 只有当 direction 为 "in" 时才使用,通知设备主机期望的响应包含的数据量。
至少应该是 packets × packetLength
data (arraybuffer): 由您的设备协议定义,只有当 direction 为 "out" 时才使用。
packets (integer) 本次传输期望的数据包总数。
packetLength (integer) 本次传输期望的每一个数据包的长度。

例如:

var transferInfo = {
  "direction": "in",
  "endpoint": 1,
  "length": 2560
};

var isoTransferInfo = {
  "transferInfo": transferInfo,
  "packets": 20,
  "packetLength": 128
};

chrome.usb.isochronousTransfer(connectionHandle, isoTransferInfo, optionalCallback);

注意:一次同步传输将包含 isoTransferInfo.packets个数据包,每个数据包 isoTransferInfo.packetLength 字节。如果是传入传输(您的代码向设备请求数据),onUsbEvent 中的 data 字段会是一个大小为 transferInfo.length 的数组缓冲区(ArrayBuffer)。您应当分析该数组缓冲区,并提取不同的数据包,每个数据包以 isoTransferInfo.packetLength 字节的整数倍开始。

如果您期望从设备接收到数据流,请牢记对于您期望返回的每一次传输您都必须发送一次“传入”传输。除非主机显式通过“传入”传输请求,USB 设备不会向总线发送传输。

大块传输

大块传输是通常用于以可靠的方式传输大量数据的 USB 传输类型。usb.bulkTransfer 方法包含三个参数:

chrome.usb.bulkTransfer(connectionHandle, transferInfo, transferCallback);

参数 描述
connectionHandle usb.openDevice 回调函数中接收到的对象。
transferInfo 参数对象,包含来自下表的值。
transferCallback 传输完成时调用。

transferInfo 对象的值:

描述
direction (string) "in" 或 "out"
endpoint (integer) 由您的设备协议定义。
length (integer) 只有当 direction 为 "in" 时才使用,通知设备主机期望的响应包含的数据量。
data (ArrayBuffer) 由您的设备协议定义,只有当 direction 为 "out" 时才使用。

例如:

var transferInfo = {
  "direction": "out",
  "endpoint": 1,
  "data": new Uint8Array([4, 8, 15, 16, 23, 42]).buffer
};

中断传输

中断传输用于少量时间敏感的数据。由于所有 USB 通信都由主机启动,主机代码通常会周期性地查询设备,发送中断输入传输,如果中断队列(由设备维护)不为空的话使设备发回数据。usb.interruptTransfer 方法包含三个参数:

chrome.usb.interruptTransfer(connectionHandle, transferInfo, transferCallback);

参数 描述
connectionHandle usb.openDevice 的回调函数中接收到的对象。
transferInfo 参数对象,包含来自下表的值。
transferCallback 传输完成时调用。注意该回调函数并不包含设备的响应,回调函数的目的只是通知您的代码异步传输请求已处理。

transferInfo 对象的值:

描述
direction (string) "in" 或 "out"。
endpoint (integer) 由您的设备协议定义。
length (integer) 只有当 direction 为 "in" 时才使用,通知设备主机期望的响应包含的数据量。
data (ArrayBuffer) 由您的设备协议定义,只有当 direction 为 "out" 时才使用。

例如:

var transferInfo = {
  "direction": "in",
  "endpoint": 1,
  "length": 2
};
chrome.usb.interruptTransfer(connectionHandle, transferInfo, optionalCallback);

注意事项

并不是所有设备都能通过 USB API 访问。大体上,不能访问设备的原因包括操作系统内核或原生设备驱动程序阻止用户区代码访问,例如 OSX 系统上包含 HID 配置文件的设备以及 USB 闪存盘。

在大部分 Linux 系统上,USB 设备默认情况下以只读权限映射。要通过该 API 打开设备,您的用户也需要写入权限。一种简单的解决办法是设置 udev 规则。创建一个包含如下内容的文件:/etc/udev/rules.d/50-您的设备名称.rules

SUBSYSTEM=="usb", ATTR{idVendor}=="[您的设备名称]", MODE="0664", GROUP="plugdev"

然后只要重新启动 udev 服务:service udev restart。您可以通过以下步骤检查权限是否正确设置:

  • 运行 lsusb,找到总线和设备编号。
  • 运行 ls -al /dev/bus/usb/[bus]/[device]。该文件应该由“plugdev”组所有,并拥有写入权限。

您的应用不能自动完成这一步,因为这一过程需要管理员权限。我们建议您向最终用户提供指南,并链接到该网页的注意事项部分作为解释。

在 Chrome OS 中,您只需要调用 usb.requestAccess,权限控制器会为您完成这一步。