連線至不常見的 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" 事件,處理傳入的輸入報告。這些事件會以 DataView 物件 (data) 的形式包含 HID 資料、所屬 HID 裝置 (device),以及與輸入報表相關聯的 8 位元報表 ID (reportId)。

紅色和藍色的任天堂 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()。報表傳送後,系統會傳回的 promise 會解析。如果 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 中,WebHID API 可在所有電腦平台 (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 上發布。