在網路上存取 USB 裝置

WebUSB API 引進了 WebUSB,變得更安全、容易使用。

François Beaufort
François Beaufort

如果我簡單說是「USB」,應該立即能想到鍵盤、滑鼠、音訊、視訊和儲存裝置。您答對了,但這個頁面也提供其他種類的通用序列匯流排 (USB) 裝置。

這些非標準化 USB 裝置會要求硬體供應商編寫平台專屬驅動程式和 SDK,以便您 (開發人員) 使用。很遺憾,此平台專屬程式碼曾經禁止網路使用這些裝置。這是 WebUSB API 的創建原因之一:為提供將 USB 裝置服務公開在網路上的方式。透過這個 API,硬體製造商將可在裝置上建構跨平台 JavaScript SDK。

不過最重要的是,這樣會將 USB 帶入網路,讓 USB 更安全、更易於使用

以下是您可以使用 WebUSB API 處理的行為:

  1. 購買 USB 裝置。
  2. 插入電腦。然後會立即顯示通知,具有前往此裝置的正確網站。
  3. 按一下通知。網站已準備就緒,您可以開始使用了!
  4. 點選即可連線,Chrome 會顯示 USB 裝置選擇工具,您可以在中選取裝置。

好了!

如果沒有 WebUSB API,這個流程會是什麼?

  1. 安裝平台專用的應用程式。
  2. 如果我的作業系統支援此功能,請確認您已下載正確的應用程式。
  3. 安裝裝置。如果幸運的話,您不會收到可怕的 OS 提示或彈出式視窗,要求您從網際網路安裝驅動程式/應用程式。如果不幸好,已安裝的驅動程式或應用程式無法正常運作,並損害您的電腦。(提醒您,網頁版是含有故障網站的建置)。
  4. 如果只使用一次,程式碼會留在電腦,直到您想移除為止。(在網頁上,未使用的空間最終會收回)。

開始之前

本文假設您對 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」屬性定義。

您可以定義權限政策,以控制 usb 屬性是否要在 Navigator 物件中公開,或允許 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() 函式會採用定義 filters 的必要 JavaScript 物件。這些篩選器可用於比對任何 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 十六進位數字。我只是在 USB ID 清單中搜尋「Arduino」。

在上方已完成的承諾中傳回的 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 並定義到達網頁網址,當 USB 裝置接上電源時,Chrome 就會顯示常駐通知。按一下這則通知即可開啟到達網頁。

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

使用 Arduino USB 電路板

現在,我們來看看如何透過 USB 連接埠,使用與 WebUSB 相容的 Arduino 白板通訊,有多簡單。請前往 https://github.com/webusb/arduino 查看 WebUSB-enable素描的操作說明。

別擔心,本文將介紹本文稍後介紹的所有 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() 會對裝置執行大量轉移作業,通知主機已準備好接收大量資料。接著會使用 result 物件 (其中包含必須適當剖析的 DataView data) 完成承諾。

如果您熟悉 USB,應該對 USB 的操作方式十分熟悉。

我想要更多

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

  • 控制傳輸 (用於傳送或接收設定或指令參數至 USB 裝置) 會使用 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 裝置的存取權限。舉例來說,如果是在與許多裝置共用電腦上使用的教育網頁應用程式,則大量累積使用者產生權限會導致使用者體驗不佳。

// 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 可在同一處查看所有 USB 裝置相關事件,讓 Chrome 中的 USB 偵錯變得更輕鬆。

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

內部頁面 about://usb-internals 也非常實用,可讓您模擬虛擬 WebUSB 裝置的連線和中斷連線程序。如要在不使用實際硬體的情況下執行 UI 測試,這項功能就能派上用場。

在 Chrome 中對 WebUSB 偵錯的內部網頁螢幕截圖
用於對 WebUSB API 偵錯的 Chrome 內部頁面。

在大部分的 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 群組的成員。然後重新連結裝置。

資源

請使用主題標記 #WebUSB 將 Tweet 訊息傳送至 @ChromiumDev,並告訴我們您的使用地點和方式。

特別銘謝

感謝 Joe Medley 審查這篇文章。