連線至不常見的 HID 裝置

WebHID API 可讓網站存取其他輔助鍵盤和特殊控制器。

François Beaufort
François Beaufort

人類介面裝置 (HID) 長尾 (例如替代鍵盤或奇特的遊戲墊) 太新、太舊,或系統裝置驅動程式不常見。透過 WebHID API,您可以設法在 JavaScript 中實作裝置專屬的邏輯,藉此解決這個問題。

建議用途

HID 裝置會接收輸入內容或提供輸出內容,裝置範例包括鍵盤、指標裝置 (滑鼠、觸控螢幕等) 和遊戲控制器。透過 HID 通訊協定,您可以使用作業系統驅動程式,在電腦上存取這些裝置。網頁平台會依賴這些驅動程式支援 HID 裝置。

無法存取非標準 HID 裝置,對於替代輔助鍵盤 (例如 Elgato Stream DeckJabra 耳機X-keys) 和特殊遊戲控制器的支援來說,特別令人頭痛。專為電腦設計的遊戲手把通常會使用 HID 做為遊戲手把輸入 (按鈕、搖桿、扳機) 和輸出 (LED、震動) 的輸入/輸出裝置。遺憾的是,遊戲手把的輸入和輸出內容並未經過標準化,而且網路瀏覽器通常需要針對特定裝置自訂邏輯。這無法持續下去,且會導致舊款和不常見裝置的長期支援不佳。這也會導致瀏覽器依賴特定裝置行為的異常情形。

術語

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 連線,請先存取 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" 事件來處理傳入的輸入報告。這些事件包含 HID 資料做為 DataView 物件 (data)、所屬 HID 裝置 (device),以及與輸入報表 (reportId) 相關聯的 8 位元報表 ID。

紅色和藍色的任天堂 Switch 相片。
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 裝置,請將與功能報告 (reportId) 相關聯的 8 位元報告 ID 和位元組,以 BufferSource (data) 的形式傳遞至 device.sendFeatureReport()。報表傳送後,系統會傳回的 promise 會解析。如果 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();

forget() 可在 Chrome 100 以上版本中使用,請確認是否支援下列功能:

if ("hid" in navigator && "forget" in HIDDevice.prototype) {
  // forget() is supported.
}

開發人員秘訣

您可以使用內部頁面 about://device-log 輕鬆在 Chrome 中偵錯 HID,在同一個位置查看所有 HID 和 USB 裝置相關事件。

用於偵錯 HID 的內部頁面螢幕截圖。
使用 Chrome 的內部頁面對 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 群組的成員。接著重新連線裝置即可

瀏覽器支援

Chrome 第 89 版適用於所有電腦平台 (ChromeOS、Linux、macOS 和 Windows)。

示範

如要查看部分 WebHID 示範,請前往 web.dev/hid-examples。去看看吧!

安全性和隱私權

規格作者根據「控管強大網路平台功能的存取權」一文中定義的核心原則,設計並實作 WebHID API,包括使用者控制、透明度和人體工學。這個 API 的使用方式主要取決於權限模型,一次只會授予單一 HID 裝置的存取權。使用者必須採取主動步驟,才能選取特定 HID 裝置,回應使用者提示。

如要瞭解安全性取捨,請參閱 WebHID 規格的「安全性和隱私權注意事項」一節。

此外,Chrome 會檢查每個頂層集合的使用情況,如果頂層集合是否具有受保護的用途 (例如一般鍵盤、滑鼠),那麼網站將無法傳送及接收該集合中定義的任何報表。完整的受保護用途清單可供大眾查閱。

請注意,安全性敏感的 HID 裝置 (例如用於更強驗證功能的 FIDO HID 裝置) 也會在 Chrome 中遭到封鎖。請參閱 USB 封鎖清單HID 封鎖清單檔案。

意見回饋

Chrome 團隊很樂意聽取你對 WebHID API 的想法和使用體驗。

請說明 API 設計

API 是否有任何功能無法正常運作?或者,您是否缺少實作想法所需的方法或屬性?

WebHID API GitHub 存放區中提出規格問題,或在現有問題中加入您的想法。

回報實作問題

您發現 Chrome 實作錯誤嗎?還是實作方式與規格不同?

請參閱「如何回報 WebHID 錯誤」一文。請務必盡可能提供詳細資訊,提供重現錯誤的簡單操作說明,並將「元件」設為 Blink>HIDGlitch 非常適合用來快速輕鬆地提出重新提案。

顯示支援

您打算使用 WebHID API 嗎?您的公開支援有助於 Chrome 團隊決定功能的優先順序,並向其他瀏覽器供應商顯示支援這些功能的重要性。

使用主題標記 #WebHID 將推文傳送到 @ChromiumDev,並告知我們您使用了這些標記的位置和方式。

實用連結

特別銘謝

感謝 Matt ReynoldsJoe Medley 審查本文。紅色和藍色的 Nintendo Switch 相片由 Sara Kurfeß 提供,黑色和銀色的筆記型電腦相片由 Athul Cyriac Ajay 提供,並在 Unsplash 上發布。