WebUSB API 可將 USB 裝置帶入網路,讓使用者更安全、更輕鬆地使用 USB。
如果我輕而易舉地說「USB」,很可能會馬上地想起鍵盤、滑鼠、音訊、視訊和儲存裝置。你說得沒錯,但市面上也有其他類型的通用序列匯流排 (USB) 裝置。
這些非標準 USB 裝置需要硬體供應商編寫平台專屬的驅動程式和 SDK,才能讓您 (開發人員) 加以利用。很遺憾,這些平台專屬程式碼在過去一直阻止這些裝置使用網路。這也是我們建立 WebUSB API 的原因之一:提供一種方法,讓 USB 裝置服務可公開至網路。有了這個 API,硬體製造商就能為自家裝置建構跨平台 JavaScript SDK。
但最重要的是,將 USB 帶入網路,可讓 USB 更安全、更容易使用。
以下是 WebUSB API 可能會發生的行為:
- 購買 USB 裝置。
- 將點字顯示器連接至電腦。畫面會立即顯示通知,並提供這部裝置的專用網站。
- 按一下通知。網站已上線,可以開始使用了!
- 點選「連線」後,Chrome 會顯示 USB 裝置選擇器,讓你選擇裝置。
好了!
如果沒有 WebUSB API,這個程序會是什麼樣子?
- 安裝平台專屬應用程式。
- 即使我的作業系統支援,也請確認我下載的東西是否正確。
- 安裝裝置。如果運氣好,您不會看到嚇人的 OS 提示或彈出式視窗,警告您從網際網路安裝驅動程式/應用程式。不幸的是,安裝的驅動程式或應用程式可能會發生故障,並損害電腦。(請注意,網路的設計目的是包含故障的網站)。
- 如果您只使用這項功能一次,程式碼會保留在電腦上,直到您決定移除為止。(在網路上,未使用的空間最終會回收)。
開始前
本文假設您已具備一些 USB 運作方式的基本知識。如果沒有,建議您閱讀「USB 一覽無遺」。如需 USB 的背景資訊,請參閱官方 USB 規格。
WebUSB API 可在 Chrome 61 中使用。
適用於原始測試
為了盡可能從在該領域使用 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()
函式會採用定義 filters
的強制 JavaScript 物件。這些篩選器用於比對任何具備指定廠商 (vendorId
) 和選用產品 (productId
) ID 的 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」這個字詞即可。
在上述的執行要求中,傳回的 USB device
有一些關於裝置的基本但重要資訊,例如支援的 USB 版本、封包大小上限、供應商和產品 ID,以及裝置的可能設定數量。基本上,它包含裝置 USB Descriptor 中的所有欄位。
// 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 覆寫預設 API。
再次查看 JavaScript 程式碼。取得使用者挑選的 device
後,device.open()
就會執行所有平台專屬步驟,透過 USB 裝置啟動工作階段。然後,使用 device.selectConfiguration()
選取可用的 USB 設定。請注意,設定會指定裝置的供電方式、最大耗電量和介面數量。談到介面,我還需要使用 device.claimInterface()
要求專屬存取權,因為只有在宣告介面時,資料才能傳輸至介面或相關聯的端點。最後必須呼叫 device.controlTransferOut()
,才能使用適當的指令設定 Arduino 裝置,以便透過 WebUSB Serial API 通訊。
接著,device.transferIn()
會對裝置執行大量傳輸作業,通知主機已準備好接收大量資料。接著,使用含有 DataView data
的 result
物件來履行承諾,並適當地剖析該物件。
如果您熟悉 USB,這些都應該不陌生。
我想要更多
您可以使用 WebUSB API 與所有 USB 傳輸/端點類型互動:
- 用於傳送或接收設定或指令參數至 USB 裝置的控制傳輸,會由
controlTransferIn(setup, length)
和controlTransferOut(setup, data)
處理。 - 中斷傳輸 (用於少量時間敏感資料) 的處理方式與使用
transferIn(endpointNumber, length)
和transferOut(endpointNumber, data)
的大量傳輸相同。 - 用於視訊和音訊等資料串流的等時傳輸,會由
isochronousTransferIn(endpointNumber, packetLengths)
和isochronousTransferOut(endpointNumber, data, packetLengths)
處理。 - BULK 傳輸 (用於以可靠方式轉移大量非時效性資料) 是由
transferIn(endpointNumber, length)
和transferOut(endpointNumber, data)
處理。
您也可以參考 Mike Tsao 的 WebLight 專案,其中提供從頭開始建立 USB 控制 LED 裝置的範例,這個裝置是專為 WebUSB API 設計 (這裡不使用 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
可更輕鬆地在 Chrome 中進行 USB 偵錯,因為您可以在單一位置查看所有 USB 裝置相關事件。
內部頁面 about://usb-internals
也很實用,可讓您模擬虛擬 WebUSB 裝置的連線和斷線情形。這在進行 UI 測試時相當實用,而且不需要使用實際的硬體。
在大部分的 Linux 系統中,USB 裝置預設會對應唯讀權限。如要讓 Chrome 開啟 USB 裝置,您必須新增 udev 規則。在 /etc/udev/rules.d/50-yourdevicename.rules
建立檔案,並在其中加入下列內容:
SUBSYSTEM=="usb", ATTR{idVendor}=="[yourdevicevendor]", MODE="0664", GROUP="plugdev"
其中 [yourdevicevendor]
是 2341
,例如如果裝置是 Arduino。您也可以新增 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:#webusb (W3C 的 IRC)
- WICG 郵寄清單:https://lists.w3.org/Archives/Public/public-wicg/
- WebLight 專案:https://github.com/sowbug/weblight
使用主題標記 #WebUSB
發送推文給 @ChromiumDev,告訴我們你在何處使用這項功能,以及使用方式。
特別銘謝
感謝 Joe Medley 審查本文。