連線至不常見的 HID 裝置

網站可以透過 WebHID API 存取替代輔助鍵盤和奇特的遊戲搖桿。

François Beaufort
François Beaufort

系統出現很長的人類介面裝置 (HID),例如替代鍵盤或異國遊戲手台,但該鍵盤太新、太舊,或太不常見,無法供系統裝置驅動程式存取。WebHID API 可以藉由在 JavaScript 中實作裝置專用邏輯的方式,解決這個問題。

建議用途

HID 裝置可接收輸入內容或提供給人類的輸出內容。例如鍵盤、指標裝置 (滑鼠、觸控螢幕等) 和遊戲手把。HID 通訊協定可讓電腦使用作業系統驅動程式,在桌上型電腦上存取這些裝置。網路平台仰賴這些驅動程式 來支援 HID 裝置

當改用其他輔助鍵盤 (例如 Elgato Stream DeckJabra 頭戴式裝置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();
網站上的 HID 裝置提示螢幕截圖。
選取 Nintendo Switch Joy-Con 的使用者提示。

您也可以使用 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。

紅藍和藍色的任天堂切換相片。
Nintendo Switch Joy-Con 裝置。

延續上一個範例,以下程式碼說明如何偵測使用者在 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 探索工具,以使用者可理解的格式轉儲 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}。確認您的userplugdev 群組的成員。然後重新連線裝置。

瀏覽器支援

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>HIDGlitch 適合用來分享快速簡易的提案。

展現支持

您打算使用 WebHID API 嗎?您的公開支援可協助 Chrome 團隊決定功能的優先順序,讓其他瀏覽器廠商瞭解這些功能有多重要。

使用主題標記 #WebHID 將推文傳送至 @ChromiumDev,告訴我們您的使用地點和方式。

實用連結

特別銘謝

感謝 Matt ReynoldsJoe Medley 對這篇文章的評論。 由 Sara Kurfeß 的紅色和藍色 Nintendo Switch 相片,以及 Unsplash 上由 Athul Cyriac Ajay 提供的黑色和銀筆電電腦相片。