在網路上存取 USB 裝置

WebUSB API 可將 USB 帶到網路,讓 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. 安裝軟體。幸運的話,就不會得到可怕的 OS 提示或彈出式視窗 警告您從網際網路安裝驅動程式/應用程式。如果 你幸運,安裝的驅動程式或應用程式運作不正常 您也能選擇重設 VM 這類似於按下電腦的重設按鈕(切記,網路在設計時包含故障的問題 網站)。
  4. 如果該項功能僅使用一次,驗證碼會保存在電腦上,直到 並考慮移除該內容(在網路上,未使用的空間最終會 reclaimed.)

開始前

本文假設您已具備一些 USB 運作方式的基本知識。如果不是,我 建議您閱讀 NutShell 中的 USB。如需 USB 的背景資訊, 請參閱官方 USB 規格

WebUSB API 適用於 Chrome 61。

適用於來源試用

使用 WebUSB 盡可能從開發人員取得意見回饋 API 中,我們先前已在 Chrome 54 和 Chrome 中新增這項功能 57 為來源試用

最近一次的試用期已於 2017 年 9 月順利完成。

隱私權與安全性

僅限 HTTPS

由於這項功能相當強大,因此只能在安全內容上運作。也就是說 您在建構時應考量TLS

使用者必須做出手勢

基於安全考量,navigator.usb.requestDevice() 可能僅 可透過使用者手勢 (例如觸控或滑鼠點擊) 來呼叫。

權限政策

權限政策是一種機制,可讓開發人員選擇性地啟用 並停用各種瀏覽器功能和 API可以透過 HTTP 標頭和/或 iframe「允許」屬性。

您可以定義權限政策,控管 usb 屬性是否為 。

以下示例為禁止 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。這些篩選器可將任何 USB 裝置與 提供的供應商 (vendorId) 和產品 (productId) ID (選用)。 classCodeprotocolCodeserialNumbersubclassCode 鍵可以 也可以在這裡定義文字

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 清單

上述的執行要求中傳回的 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 裝置。按一下這則通知即可開啟到達網頁。

Chrome 中 WebUSB 通知的螢幕截圖
WebUSB 通知

與 Arduino USB 電路板交談

現在來看看如何透過與 WebUSB 相容,輕鬆進行通訊 Arduino 電路板會上方的 USB 連接埠。相關操作說明請參閱: 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 程式碼,取得使用者挑選的 devicedevice.open() 會執行所有平台專用步驟,透過 USB 啟動工作階段 裝置。接著,我需要選取可用的 USB 設定 device.selectConfiguration()。請注意,設定會指定 裝置有電源、最大耗電量和介面數量。 說到介面,我還需要向 device.claimInterface(),因為資料只能轉移至介面或 並宣告所有關聯端點最後通話 您必須擁有 device.controlTransferOut(),才能根據 使用適當指令來進行 WebUSB Serial API 通訊。

接著,device.transferIn() 會執行大量轉移作業,轉移至 裝置以通知主機已準備好接收大量資料。接著, 該保證會由包含 DataView dataresult 物件來履行 才能正確剖析

如果你熟悉 USB,所有介面應該都不陌生。

我想要更多

WebUSB API 可讓您與所有 USB 傳輸/端點類型互動:

  • 控制轉乘,用於傳送或接收設定或指令 這兩個參數新增至 USB 裝置,都是透過 controlTransferIn(setup, length)controlTransferOut(setup, data) 處理。
  • 內部傳輸用於少量具時效性的資料 使用的方法與 BULK 轉乘 《transferIn(endpointNumber, length)》和《transferOut(endpointNumber, data)》。
  • ISOCHRONOUS 傳輸 (用於影片和音訊等資料串流) 由 isochronousTransferIn(endpointNumber, packetLengths)isochronousTransferOut(endpointNumber, data, packetLengths)
  • BULK 轉移,用於在 是可靠的方式,由 transferIn(endpointNumber, length) 處理,並 transferOut(endpointNumber, data)

建議您也參考 Mike Tsao 的 WebLight 專案 提供了實際範例,說明如何建立採用 USB 控制的 LED 裝置 適用於 WebUSB API (這裡不是使用 Arduino)。你會找到軟硬體 和韌體。

撤銷 USB 裝置的存取權

網站可以清除網站不再需要的 USB 裝置權限 在 USBDevice 執行個體上呼叫 forget()。舉例來說 一個在許多裝置共用電腦上使用的教育網頁應用程式 因為使用者產生的權限數量過多都會對使用者體驗造成負面影響。

// 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 裝置相關事件。

在 Chrome 中針對 WebUSB 偵錯的裝置記錄頁面螢幕截圖
Chrome 的裝置記錄頁面:用於對 WebUSB API 偵錯。

你也可以參考 about://usb-internals 內部網頁 模擬虛擬 WebUSB 裝置的連線和中斷連線。 這對於在無需實際硬體的情況下執行 UI 測試非常實用。

在 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 撰寫這篇文章。