Kết nối với các thiết bị HID không phổ biến

API WebHID cho phép các trang web truy cập vào bàn phím phụ thay thế và tay điều khiển trò chơi ngoại lai.

François Beaufort
François Beaufort

Có rất nhiều thiết bị có giao diện dành cho người dùng (HID), chẳng hạn như bàn phím thay thế hoặc tay điều khiển trò chơi ngoại lai quá mới, quá cũ hoặc quá không phổ biến mà trình điều khiển thiết bị của hệ thống không thể truy cập được. API WebHID giải quyết vấn đề này bằng cách cung cấp cách triển khai logic dành riêng cho thiết bị trong JavaScript.

Trường hợp sử dụng được đề xuất

Thiết bị HID nhận dữ liệu đầu vào hoặc cung cấp đầu ra cho con người. Ví dụ về các thiết bị bao gồm bàn phím, thiết bị trỏ (chuột, màn hình cảm ứng, v.v.) và tay điều khiển trò chơi. Giao thức HID giúp bạn có thể truy cập vào các thiết bị này trên máy tính bằng trình điều khiển hệ điều hành. Nền tảng web hỗ trợ các thiết bị HID bằng cách dựa vào các trình điều khiển này.

Việc không thể truy cập các thiết bị HID không phổ biến sẽ đặc biệt gây khó chịu khi liên quan đến bàn phím phụ thay thế (ví dụ: Elgato Stream Deck, tai nghe Jabra, phím X) và tính năng hỗ trợ tay điều khiển trò chơi đặc biệt. Các tay điều khiển trò chơi được thiết kế cho máy tính thường sử dụng HID để làm các thao tác đầu vào (nút, cần điều khiển, kích hoạt) và đầu ra (LED, tiếng ồn). Rất tiếc, đầu vào và đầu ra của tay điều khiển trò chơi không được chuẩn hoá tốt và các trình duyệt web thường yêu cầu logic tuỳ chỉnh cho các thiết bị cụ thể. Điều này là không bền vững và dẫn đến khả năng hỗ trợ kém cho những thiết bị cũ và không phổ biến. Điều này cũng khiến trình duyệt phụ thuộc vào các điểm tương thích trong hành vi của các thiết bị cụ thể.

Thuật ngữ

HID bao gồm hai khái niệm cơ bản: báo cáo và phần mô tả báo cáo. Báo cáo là dữ liệu được trao đổi giữa thiết bị và ứng dụng phần mềm. Chỉ số mô tả báo cáo mô tả định dạng và ý nghĩa của dữ liệu mà thiết bị hỗ trợ.

HID (Thiết bị giao diện con người) là một loại thiết bị nhận dữ liệu đầu vào hoặc cung cấp dữ liệu đầu ra cho con người. Đây cũng là giao thức HID, một tiêu chuẩn cho hoạt động giao tiếp hai chiều giữa máy chủ lưu trữ và một thiết bị được thiết kế để đơn giản hoá quy trình cài đặt. Ban đầu, giao thức HID được phát triển cho các thiết bị USB, nhưng sau đó đã được triển khai trên nhiều giao thức khác, bao gồm cả Bluetooth.

Các ứng dụng và thiết bị HID trao đổi dữ liệu nhị phân thông qua 3 loại báo cáo:

Loại báo cáo Nội dung mô tả
Báo cáo đầu vào Dữ liệu được gửi từ thiết bị đến ứng dụng (ví dụ: nhấn một nút).
Báo cáo đầu ra Dữ liệu được gửi từ ứng dụng tới thiết bị (ví dụ: yêu cầu bật đèn nền bàn phím).
Báo cáo tính năng Dữ liệu có thể được gửi theo một trong hai hướng. Định dạng này là tuỳ theo thiết bị.

Phần mô tả báo cáo mô tả định dạng nhị phân của báo cáo mà thiết bị hỗ trợ. Cấu trúc của thư viện này mang tính phân cấp và có thể nhóm các báo cáo lại với nhau dưới dạng bộ sưu tập riêng biệt trong tập hợp cấp cao nhất. Định dạng của chỉ số mô tả được xác định theo thông số kỹ thuật HID.

Trường hợp sử dụng HID là một giá trị số tham chiếu đến một dữ liệu đầu vào hoặc đầu ra đã được chuẩn hoá. Giá trị sử dụng cho phép thiết bị mô tả mục đích sử dụng thiết bị và mục đích của từng trường trong báo cáo. Ví dụ: một thuộc tính được xác định cho nút bên trái của chuột. Dữ liệu sử dụng cũng được sắp xếp thành các trang sử dụng để cho biết danh mục cấp cao của thiết bị hoặc báo cáo.

Sử dụng API WebHID

Phát hiện tính năng

Để kiểm tra xem API WebHID có được hỗ trợ hay không, hãy dùng:

if ("hid" in navigator) {
  // The WebHID API is supported.
}

Mở kết nối HID

API WebHID không đồng bộ theo thiết kế để ngăn giao diện người dùng của trang web chặn khi đang chờ dữ liệu đầu vào. Điều này rất quan trọng vì bạn có thể nhận dữ liệu HID bất cứ lúc nào, cần có cách để nghe dữ liệu đó.

Để mở kết nối HID, trước tiên, hãy truy cập vào đối tượng HIDDevice. Đối với trường hợp này, bạn có thể nhắc người dùng chọn một thiết bị bằng cách gọi navigator.hid.requestDevice() hoặc chọn một thiết bị từ navigator.hid.getDevices(). Phương thức này trả về danh sách thiết bị mà trang web đã được cấp quyền truy cập trước đó.

Hàm navigator.hid.requestDevice() lấy một đối tượng bắt buộc xác định các bộ lọc. Các giá trị này được dùng để khớp với mọi thiết bị được kết nối với mã nhận dạng nhà cung cấp USB (vendorId), mã nhận dạng sản phẩm USB (productId), giá trị trang sử dụng (usagePage) và giá trị sử dụng (usage). Bạn có thể lấy các giá trị đó từ Kho lưu trữ mã nhận dạng USBtài liệu về bảng sử dụng HID.

Nhiều đối tượng HIDDevice mà hàm này trả về đại diện cho nhiều giao diện HID trên cùng một thiết bị thực.

// 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();
Ảnh chụp màn hình lời nhắc trên thiết bị HID trên một trang web.
Lời nhắc người dùng chọn Nintendo Switch Joy-Con.

Bạn cũng có thể sử dụng khoá exclusionFilters không bắt buộc trong navigator.hid.requestDevice() để loại trừ một số thiết bị khỏi bộ chọn của trình duyệt (ví dụ: được biết là đang gặp sự cố).

// 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 }],
});

Đối tượng HIDDevice chứa nhà cung cấp USB và mã nhận dạng sản phẩm để nhận dạng thiết bị. Thuộc tính collections của thiết bị được khởi chạy bằng nội dung mô tả phân cấp của các định dạng báo cáo của thiết bị.

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
}

Theo mặc định, các thiết bị HIDDevice được trả về ở trạng thái "đóng" và bạn phải mở bằng cách gọi open() trước khi có thể gửi hoặc nhận dữ liệu.

// Wait for the HID connection to open before sending/receiving data.
await device.open();

Nhận báo cáo dữ liệu đầu vào

Sau khi thiết lập kết nối HID, bạn có thể xử lý các báo cáo đầu vào đến bằng cách theo dõi các sự kiện "inputreport" từ thiết bị. Những sự kiện đó chứa dữ liệu HID dưới dạng đối tượng DataView (data), thiết bị HID chứa dữ liệu đó (device) và mã báo cáo 8 bit liên kết với báo cáo dữ liệu đầu vào (reportId).

Ảnh chuyển đổi màu đỏ và màu xanh dương của nintendo.
Thiết bị Nintendo Switch Joy-Con.

Tiếp tục với ví dụ trước, mã dưới đây cho bạn biết cách phát hiện nút mà người dùng đã nhấn trên thiết bị Joy-Con Right để bạn có thể dùng thử tại nhà.

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]}.`);
});

Gửi báo cáo kết quả

Để gửi một báo cáo đầu ra đến một thiết bị HID, hãy truyền mã báo cáo 8 bit liên kết với báo cáo đầu ra (reportId) và các byte dưới dạng BufferSource (data) đến device.sendReport(). Lời hứa được trả về sẽ được giải quyết sau khi báo cáo được gửi. Nếu thiết bị HID không sử dụng mã báo cáo, hãy đặt reportId thành 0.

Ví dụ dưới đây áp dụng cho thiết bị Joy-Con và cho bạn thấy cách làm cho thiết bị rầm rộ bằng các báo cáo đầu ra.

// 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));

Gửi và nhận báo cáo tính năng

Báo cáo tính năng là loại báo cáo dữ liệu HID duy nhất có thể di chuyển theo cả hai hướng. Chúng cho phép các thiết bị và ứng dụng HID trao đổi dữ liệu HID không được chuẩn hoá. Không giống như báo cáo đầu vào và đầu ra, ứng dụng không thường xuyên nhận hoặc gửi báo cáo tính năng.

Ảnh chụp máy tính xách tay màu đen và bạc.
Bàn phím máy tính xách tay

Để gửi báo cáo tính năng tới thiết bị HID, hãy truyền mã báo cáo 8 bit liên kết với báo cáo tính năng (reportId) và các byte dưới dạng BufferSource (data) cho device.sendFeatureReport(). Lời hứa được trả về sẽ được giải quyết sau khi báo cáo được gửi. Nếu thiết bị HID không sử dụng mã báo cáo, hãy đặt reportId thành 0.

Ví dụ dưới đây minh hoạ việc sử dụng báo cáo tính năng bằng cách hướng dẫn bạn cách yêu cầu thiết bị đèn nền bàn phím Apple, mở và làm thiết bị đó nhấp nháy.

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);
}

Để nhận báo cáo tính năng từ thiết bị HID, hãy truyền mã báo cáo 8 bit liên kết với báo cáo tính năng (reportId) cho device.receiveFeatureReport(). Lời hứa được trả về sẽ được phân giải bằng một đối tượng DataView chứa nội dung của báo cáo tính năng. Nếu thiết bị HID không sử dụng mã báo cáo, hãy đặt reportId thành 0.

// Request feature report.
const dataView = await device.receiveFeatureReport(/* reportId= */ 1);

// Read feature report contents with dataView.getInt8(), getUint8(), etc...

Theo dõi trạng thái kết nối và ngắt kết nối

Khi được cấp quyền truy cập vào một thiết bị HID, trang web có thể chủ động nhận các sự kiện kết nối và ngắt kết nối bằng cách theo dõi các sự kiện "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.
});

Thu hồi quyền truy cập vào một thiết bị HID

Trang web có thể xoá các quyền truy cập vào một thiết bị HID mà nó không muốn giữ lại nữa bằng cách gọi forget() trên thực thể HIDDevice. Ví dụ: đối với một ứng dụng web giáo dục dùng trên một máy tính dùng chung có nhiều thiết bị, việc tích luỹ một lượng lớn quyền do người dùng tạo sẽ tạo ra trải nghiệm kém cho người dùng.

Việc gọi forget() trên một thực thể HIDDevice sẽ thu hồi quyền truy cập vào tất cả giao diện HID trên cùng một thiết bị thực.

// Voluntarily revoke access to this HID device.
await device.forget();

forget() đã có trong Chrome 100 trở lên, hãy kiểm tra xem tính năng này có được hỗ trợ hay không:

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

Mẹo cho nhà phát triển

Bạn có thể dễ dàng gỡ lỗi HID trong Chrome thông qua trang nội bộ, about://device-log. Tại đây, bạn có thể xem tất cả sự kiện liên quan đến thiết bị HID và thiết bị USB ở cùng một nơi.

Ảnh chụp màn hình trang nội bộ để gỡ lỗi HID.
Trang nội bộ trong Chrome để gỡ lỗi HID.

Hãy xem trình khám phá HID để chuyển thông tin thiết bị HID sang định dạng mà con người có thể đọc được. API này liên kết từ các giá trị sử dụng đến tên của mỗi lần sử dụng HID.

Theo mặc định, trên hầu hết các hệ thống Linux, các thiết bị HID được liên kết bằng quyền chỉ có thể đọc. Để cho phép Chrome mở thiết bị HID, bạn cần thêm quy tắc udev mới. Tạo một tệp tại /etc/udev/rules.d/50-yourdevicename.rules có nội dung sau:

KERNEL=="hidraw*", ATTRS{idVendor}=="[yourdevicevendor]", MODE="0664", GROUP="plugdev"

Trong dòng trên, [yourdevicevendor]057e nếu thiết bị của bạn là Nintendo Switch Joy-Con. Bạn cũng có thể thêm ATTRS{idProduct} cho một quy tắc cụ thể hơn. Đảm bảo user của bạn là thành viên của nhóm plugdev. Sau đó, bạn chỉ cần kết nối lại thiết bị.

Hỗ trợ trình duyệt

API WebHID có trên tất cả các nền tảng máy tính (ChromeOS, Linux, macOS và Windows) trong Chrome 89.

Bản thu thử

Bạn có thể xem một số bản minh hoạ WebHID tại web.dev/hid-examples. Hãy cùng khám phá!

Mức độ bảo mật và quyền riêng tư

Tác giả của bản đặc tả kỹ thuật đã thiết kế và triển khai API WebHID theo các nguyên tắc cốt lõi được xác định trong Kiểm soát quyền truy cập vào các tính năng nền tảng web mạnh mẽ, bao gồm quyền kiểm soát của người dùng, độ trong suốt và hiệu quả. Khả năng sử dụng API này chủ yếu chịu sự kiểm soát của một mô hình cấp quyền chỉ cấp quyền truy cập vào một thiết bị HID tại một thời điểm. Để phản hồi lời nhắc của người dùng, người dùng phải thực hiện các bước chủ động để chọn một thiết bị HID cụ thể.

Để tìm hiểu các đánh đổi về bảo mật, hãy xem phần Những điều cần cân nhắc về bảo mật và quyền riêng tư trong thông số kỹ thuật của WebHID.

Ngoài ra, Chrome sẽ kiểm tra mức sử dụng của từng bộ sưu tập cấp cao nhất và nếu một bộ sưu tập cấp cao nhất có mục đích sử dụng được bảo vệ (ví dụ: bàn phím chung, chuột), thì trang web sẽ không thể gửi và nhận bất kỳ báo cáo nào được xác định trong bộ sưu tập đó. Danh sách đầy đủ các trường hợp sử dụng được bảo vệ sẽ công khai.

Xin lưu ý rằng các thiết bị HID nhạy cảm về bảo mật (chẳng hạn như các thiết bị FIDO HID dùng để xác thực mạnh mẽ hơn) cũng bị chặn trong Chrome. Xem các tệp danh sách chặn USBdanh sách chặn HID.

Ý kiến phản hồi

Nhóm Chrome rất muốn biết suy nghĩ và trải nghiệm của bạn với API WebHID.

Cho chúng tôi biết về thiết kế của API

Có vấn đề nào về API không hoạt động như mong đợi không? Hay có phương thức hoặc thuộc tính nào bị thiếu mà bạn cần triển khai ý tưởng của mình không?

Gửi vấn đề về thông số kỹ thuật trên kho lưu trữ GitHub API WebHID hoặc thêm ý kiến của bạn vào vấn đề hiện tại.

Báo cáo sự cố với quá trình triển khai

Bạn có phát hiện thấy lỗi khi triển khai Chrome không? Hay cách triển khai có khác với thông số kỹ thuật không?

Hãy xem bài viết Cách báo cáo lỗi WebHID. Hãy nhớ cung cấp nhiều chi tiết nhất có thể, đưa ra hướng dẫn đơn giản để tái tạo lỗi và đặt Components (Thành phần) thành Blink>HID. Sự kiện không mong muốn hoạt động hiệu quả để chia sẻ các bản sửa lại nhanh chóng và dễ dàng.

Hiển thị sự ủng hộ

Bạn có định sử dụng API WebHID không? Sự hỗ trợ công khai của bạn giúp nhóm Chrome ưu tiên các tính năng và cho các nhà cung cấp trình duyệt khác biết tầm quan trọng của việc hỗ trợ họ.

Hãy gửi một dòng tweet đến @ChromiumDev bằng hashtag #WebHID và cho chúng tôi biết địa điểm cũng như cách bạn sử dụng bài đăng này.

Các đường liên kết hữu ích

Xác nhận

Cảm ơn Matt ReynoldsJoe Medley đã đánh giá bài viết này. Ảnh Nintendo Switch màu đỏ và xanh dương của Sara Kurfeß và ảnh chụp máy tính xách tay màu đen và bạc của Athul Cyriac Ajay trên Unsplash.