網站可以透過 WebHID API 存取替代輔助鍵盤和奇特的遊戲搖桿。
系統出現很長的人類介面裝置 (HID),例如替代鍵盤或異國遊戲手台,但該鍵盤太新、太舊,或太不常見,無法供系統裝置驅動程式存取。WebHID API 可以藉由在 JavaScript 中實作裝置專用邏輯的方式,解決這個問題。
建議用途
HID 裝置可接收輸入內容或提供給人類的輸出內容。例如鍵盤、指標裝置 (滑鼠、觸控螢幕等) 和遊戲手把。HID 通訊協定可讓電腦使用作業系統驅動程式,在桌上型電腦上存取這些裝置。網路平台仰賴這些驅動程式 來支援 HID 裝置
當改用其他輔助鍵盤 (例如 Elgato Stream Deck、Jabra 頭戴式裝置、X-keys) 以及異國遊戲手把支援時,無法存取不常見的 HID 裝置會特別困難。專為電腦設計的遊戲搖桿通常會使用 HID 做為遊戲手把輸入裝置 (按鈕、搖桿、觸發事件) 和輸出 (LED 和 RB)。遺憾的是,遊戲手把的輸入和輸出並未妥善標準化,而且網路瀏覽器通常需要特定裝置專用的自訂邏輯。這種做法無法符合永續精神,對長尾裝置和不常見裝置的支援性也因此不佳。這也會導致瀏覽器依賴特定裝置的行為。
術語
HID 包含兩個基本概念:報表和報表描述元。報表是裝置和軟體用戶端之間交換的資料。報表描述元會說明裝置支援的資料格式和意義。
HID (人機介面裝置) 是一種裝置,可接收輸入內容或提供輸出內容給人類。這也是指 HID 通訊協定,這是主機與裝置之間的雙向通訊標準,用於簡化安裝程序。HID 通訊協定最初是為 USB 裝置開發的,但後來已導入許多其他通訊協定,包括藍牙。
應用程式與 HID 裝置透過三種報表類型交換二進位資料:
報表類型 | 說明 |
---|---|
輸入報表 | 從裝置傳送至應用程式的資料 (例如按下按鈕)。 |
輸出報表 | 從應用程式傳送至裝置的資料 (例如要求開啟鍵盤背光) |
功能報表 | 不限方向傳送的資料。格式視裝置而定。 |
報表描述元可說明裝置支援的二進位報表格式。其結構具有階層結構,可將報表分組,做為頂層集合中的不同集合。描述元的格式是由 HID 規格定義。
HID 用量是參照標準化輸入或輸出內容的數值。使用值可讓裝置說明裝置的預期用途,以及報表中每個欄位的用途。例如,滑鼠左鍵定義一個。用量也會整理成使用情況頁面,用於表示裝置或報表的概略類別。
使用 WebHID API
功能偵測
如要確認 WebHID API 是否受支援,請使用:
if ("hid" in navigator) {
// The WebHID API is supported.
}
開啟 HID 連線
WebHID API 採用非同步的設計,避免網站 UI 在等待輸入內容時遭到封鎖。這點非常重要,因為我們隨時都能接收 HID 資料,因此需要能夠監聽 HID 資料的方式。
如要開啟 HID 連線,請先存取 HIDDevice
物件。在這種情況下,您可以呼叫 navigator.hid.requestDevice()
提示使用者選取裝置,或是從 navigator.hid.getDevices()
中挑選一個,藉此傳回網站之前獲得存取權的裝置清單。
navigator.hid.requestDevice()
函式會使用定義篩選器的必要物件。這些屬性可以比對任何已連接以下裝置的裝置:USB 廠商 ID (vendorId
)、USB 產品 ID (productId
)、用量頁面值 (usagePage
) 和用量值 (usage
)。您可以在 USB ID 存放區和 HID 用量表文件中取得這些資料。
這個函式傳回的多個 HIDDevice
物件代表同一實體裝置上的多個 HID 介面。
// Filter on devices with the Nintendo Switch Joy-Con USB Vendor/Product IDs.
const filters = [
{
vendorId: 0x057e, // Nintendo Co., Ltd
productId: 0x2006 // Joy-Con Left
},
{
vendorId: 0x057e, // Nintendo Co., Ltd
productId: 0x2007 // Joy-Con Right
}
];
// Prompt user to select a Joy-Con device.
const [device] = await navigator.hid.requestDevice({ filters });
// Get all devices the user has previously granted the website access to.
const devices = await navigator.hid.getDevices();
您也可以使用 navigator.hid.requestDevice()
中的選用的 exclusionFilters
鍵,從瀏覽器挑選器中排除已知發生故障等情況的某些裝置。
// Request access to a device with vendor ID 0xABCD. The device must also have
// a collection with usage page Consumer (0x000C) and usage ID Consumer
// Control (0x0001). The device with product ID 0x1234 is malfunctioning.
const [device] = await navigator.hid.requestDevice({
filters: [{ vendorId: 0xabcd, usagePage: 0x000c, usage: 0x0001 }],
exclusionFilters: [{ vendorId: 0xabcd, productId: 0x1234 }],
});
HIDDevice
物件包含 USB 供應商和產品 ID,可用於裝置識別。它的 collections
屬性是以裝置報表格式的階層說明來初始化。
for (let collection of device.collections) {
// An HID collection includes usage, usage page, reports, and subcollections.
console.log(`Usage: ${collection.usage}`);
console.log(`Usage page: ${collection.usagePage}`);
for (let inputReport of collection.inputReports) {
console.log(`Input report: ${inputReport.reportId}`);
// Loop through inputReport.items
}
for (let outputReport of collection.outputReports) {
console.log(`Output report: ${outputReport.reportId}`);
// Loop through outputReport.items
}
for (let featureReport of collection.featureReports) {
console.log(`Feature report: ${featureReport.reportId}`);
// Loop through featureReport.items
}
// Loop through subcollections with collection.children
}
根據預設,HIDDevice
裝置會以「關閉」狀態傳回,而且必須先呼叫 open()
才能開啟,才能傳送或接收資料。
// Wait for the HID connection to open before sending/receiving data.
await device.open();
接收輸入報表
HID 連線建立完成後,您可以監聽裝置上的 "inputreport"
事件,處理傳入的輸入報表。這些事件包含做為 DataView
物件 (data
) 的 HID 資料、其所屬的 HID 裝置 (device
),以及與輸入報表 (reportId
) 相關聯的 8 位元報表 ID。
延續上一個範例,以下程式碼說明如何偵測使用者在 Joy-Con 右裝置上按下了哪個按鈕,以便您可以在家中試用。
device.addEventListener("inputreport", event => {
const { data, device, reportId } = event;
// Handle only the Joy-Con Right device and a specific report ID.
if (device.productId !== 0x2007 && reportId !== 0x3f) return;
const value = data.getUint8(0);
if (value === 0) return;
const someButtons = { 1: "A", 2: "X", 4: "B", 8: "Y" };
console.log(`User pressed button ${someButtons[value]}.`);
});
傳送輸出報告
如要將輸出報表傳送至 HID 裝置,請將與輸出報表 (reportId
) 和位元組相關聯的 8 位元報表 ID 做為 BufferSource
(data
) 傳遞至 device.sendReport()
。傳回的承諾會在報告送出後解除。如果 HID 裝置並未使用報表 ID,請將 reportId
設為 0。
下例適用於 Joy-Con 裝置,並示範如何讓輸出報表難以判斷。
// First, send a command to enable vibration.
// Magical bytes come from https://github.com/mzyy94/joycon-toolweb
const enableVibrationData = [1, 0, 1, 64, 64, 0, 1, 64, 64, 0x48, 0x01];
await device.sendReport(0x01, new Uint8Array(enableVibrationData));
// Then, send a command to make the Joy-Con device rumble.
// Actual bytes are available in the sample below.
const rumbleData = [ /* ... */ ];
await device.sendReport(0x10, new Uint8Array(rumbleData));
傳送及接收功能報告
只有功能報表是雙向的 HID 資料報表可以雙向行駛。它們允許 HID 裝置和應用程式交換未標準化的 HID 資料。與輸入和輸出報表不同的是,應用程式不會定期接收或傳送功能報表。
如要傳送功能報告給 HID 裝置,請將與功能報告相關的 8 位元報告 ID (reportId
) 和位元組格式傳遞為 BufferSource
(data
) 至
device.sendFeatureReport()
。傳送報告後,傳回的承諾會解決問題。如果 HID 裝置並未使用報表 ID,請將 reportId
設為 0。
以下範例說明如何使用功能報告,說明如何要求 Apple 鍵盤背光裝置、開啟該裝置並讓它閃爍。
const waitFor = duration => new Promise(r => setTimeout(r, duration));
// Prompt user to select an Apple Keyboard Backlight device.
const [device] = await navigator.hid.requestDevice({
filters: [{ vendorId: 0x05ac, usage: 0x0f, usagePage: 0xff00 }]
});
// Wait for the HID connection to open.
await device.open();
// Blink!
const reportId = 1;
for (let i = 0; i < 10; i++) {
// Turn off
await device.sendFeatureReport(reportId, Uint32Array.from([0, 0]));
await waitFor(100);
// Turn on
await device.sendFeatureReport(reportId, Uint32Array.from([512, 0]));
await waitFor(100);
}
如要接收 HID 裝置提供的功能報告,請將與功能報告 (reportId
) 相關聯的 8 位元報告 ID 傳遞至 device.receiveFeatureReport()
。傳回的承諾內容會使用含有功能報告內容的 DataView
物件解析。如果 HID 裝置並未使用報表 ID,請將 reportId
設為 0。
// Request feature report.
const dataView = await device.receiveFeatureReport(/* reportId= */ 1);
// Read feature report contents with dataView.getInt8(), getUint8(), etc...
留意連線中斷情形
網站獲得 HID 裝置的存取權後,可以透過監聽 "connect"
和 "disconnect"
事件,主動接收連線和中斷連線事件。
navigator.hid.addEventListener("connect", event => {
// Automatically open event.device or warn user a device is available.
});
navigator.hid.addEventListener("disconnect", event => {
// Remove |event.device| from the UI.
});
撤銷 HID 裝置的存取權
網站可藉由在 HIDDevice
執行個體呼叫 forget()
,清除不再需要存取 HID 裝置的權限。舉例來說,如果是在具有許多裝置的共用電腦上使用教育網頁應用程式,則大量累積使用者產生的權限會導致使用者體驗不佳。
如果在單一 HIDDevice
執行個體中呼叫 forget()
,將無法再存取同一實體裝置上所有 HID 介面的存取權。
// Voluntarily revoke access to this HID device.
await device.forget();
由於 Chrome 100 以上版本支援 forget()
,請檢查下列項目是否支援這項功能:
if ("hid" in navigator && "forget" in HIDDevice.prototype) {
// forget() is supported.
}
開發人員提示
在 Chrome 中為 HID 偵錯很簡單,內部頁面 about://device-log
可讓您集中查看所有 HID 和 USB 裝置相關事件。
請查看 HID 探索工具,以使用者可理解的格式轉儲 HID 裝置資訊。它會將使用值對應至各個 HID 用法的名稱。
在大部分的 Linux 系統中,根據預設,HID 裝置會對應至唯讀權限。如要允許 Chrome 開啟 HID 裝置,您必須新增 udev 規則。在 /etc/udev/rules.d/50-yourdevicename.rules
建立含有下列內容的檔案:
KERNEL=="hidraw*", ATTRS{idVendor}=="[yourdevicevendor]", MODE="0664", GROUP="plugdev"
在上一行中,如果裝置是 Nintendo Switch Joy-Con,則 [yourdevicevendor]
為 057e
。也可以為更明確的規則新增 ATTRS{idProduct}
。確認您的user
是 plugdev
群組的成員。然後重新連線裝置。
瀏覽器支援
WebHID API 適用於 Chrome 89 的所有電腦平台 (ChromeOS、Linux、macOS 和 Windows)。
試聽帶
如要查看部分 WebHID 示範,請前往 web.dev/hid-examples。快來看看!
安全性和隱私權
規格作者在設計與實作 WebHID API 時,是以「控管強大的網頁平台功能存取權」一文中定義的核心原則,包括使用者控制項、資訊公開和人體工學。這個 API 的使用權限主要受權限模型的管制,該模型一次只能授予一部 HID 裝置的存取權。按照使用者提示回應,使用者必須採取主動步驟,才能選取特定 HID 裝置。
如要瞭解安全性的取捨,請參閱 WebHID 規格的安全性與隱私權注意事項一節。
此外,Chrome 還會檢查每個頂層集合的使用情形,如果頂層集合有受保護的用途 (例如一般鍵盤、滑鼠),網站將無法傳送及接收該集合中定義的任何報表。這份保護用量的完整清單對外公開。
請注意,Chrome 也會封鎖安全性敏感 HID 裝置 (例如用於強化驗證機制的 FIDO HID 裝置)。請參閱 USB 封鎖清單和 HID 封鎖清單檔案。
意見回饋:
Chrome 團隊想瞭解您對 WebHID API 的想法和使用經驗。
告訴我們 API 設計
API 有沒有正常運作的問題嗎?或者您需要某些方法或屬性來實作構想嗎?
在 WebHID API GitHub 存放區上提交規格問題,或將您的想法新增至現有問題。
回報導入問題
您在執行 Chrome 時發現錯誤了嗎?還是實作與規格不同?
請參閱如何回報 WebHID 錯誤。請務必盡可能提供所有詳細資料,並提供重現錯誤的簡易操作說明,並將「Components」設為 Blink>HID
。Glitch 適合用來分享快速簡易的提案。
展現支持
您打算使用 WebHID API 嗎?您的公開支援可協助 Chrome 團隊決定功能的優先順序,讓其他瀏覽器廠商瞭解這些功能有多重要。
使用主題標記 #WebHID
將推文傳送至 @ChromiumDev,告訴我們您的使用地點和方式。
實用連結
特別銘謝
感謝 Matt Reynolds 和 Joe Medley 對這篇文章的評論。 由 Sara Kurfeß 的紅色和藍色 Nintendo Switch 相片,以及 Unsplash 上由 Athul Cyriac Ajay 提供的黑色和銀筆電電腦相片。