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 độc đáo.

François Beaufort
François Beaufort

Có một danh sách dài các thiết bị giao diện 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 lạ, quá mới, quá cũ hoặc quá hiếm để trình điều khiển thiết bị của hệ thống có thể truy cập. API WebHID giải quyết vấn đề này bằng cách cung cấp một cách để triển khai logic dành riêng cho thiết bị trong JavaScript.

Các 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 dữ liệu đầ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 cho phép truy cập vào các thiết bị này trên máy tính để bàn 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 vào các thiết bị HID không phổ biến đặc biệt khó khăn khi phải dùng các 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 đáo. Những tay điều khiển trò chơi được thiết kế cho máy tính thường sử dụng HID cho đầu vào của tay điều khiển trò chơi (nút, cần điều khiển, còi điều khiển) và đầu ra (đèn LED, tiếng ồn ào). 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à 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 việc hỗ trợ kém cho phần đuôi dài của các 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 đặc điểm kỳ lạ trong hành vi của một số 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à chỉ số mô tả báo cáo. Báo cáo là dữ liệu được trao đổi giữa một thiết bị và ứng dụng phần mềm. Phần 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 người dùng) 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. Tên này cũng đề cập đến giao thức HID, một tiêu chuẩn giao tiếp hai chiều giữa máy chủ và thiết bị được thiết kế để đơn giản hoá quy trình cài đặt. Giao thức HID ban đầu được phát triển cho các thiết bị USB, nhưng kể từ đó đã đượ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 Mô tả
Nhập báo cáo Dữ liệu được gửi từ thiết bị đến ứng dụng (ví dụ: nhấn nút).
Báo cáo đầu ra Dữ liệu được gửi từ ứng dụng đến 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 dành riêng cho từng thiết bị.

Chỉ số mô tả báo cáo mô tả định dạng tệp nhị phân của báo cáo mà thiết bị hỗ trợ. Cấu trúc của báo cáo này có dạng phân cấp và có thể nhóm các báo cáo lại với nhau dưới dạng các tuyển tập riêng biệt trong tuyển tậ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.

Mức sử dụng HID là một giá trị số đề cập đến đầ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 của thiết bị và mục đích của từng trường trong báo cáo. Ví dụ: một mã được xác định cho nút bên trái của chuột. Các trường hợp 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 được thiết kế không đồng bộ để ngăn giao diện người dùng trang web chặn khi chờ dữ liệu đầu vào. Điều này rất quan trọng vì dữ liệu HID có thể được nhận bất cứ lúc nào, đòi hỏi phải có cách để nghe dữ liệu đó.

Để mở kết nối HID, trước tiên hãy truy cập vào một đối tượng HIDDevice. Để thực hiện việc 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ị trong navigator.hid.getDevices(). Phương thức này sẽ trả về danh sách các thiết bị mà trang web đã được cấp quyền truy cập trước đó.

Hàm navigator.hid.requestDevice() nhận một đối tượng bắt buộc xác định bộ lọc. Các giá trị này được dùng để so khớp mọi thiết bị được kết nối với giá trị nhận dạng nhà cung cấp USB (vendorId), giá trị 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ị này 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 do 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 về 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, chẳng hạn như đ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 giá trị nhận dạng sản phẩm và nhà cung cấp USB để nhận dạng thiết bị. Thuộc tính collections của thuộc tính này đượ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à phải được 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 đầ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 sắp tới bằng cách theo dõi các sự kiện "inputreport" từ thiết bị. Các sự kiện đó chứa dữ liệu HID dưới dạng đối tượng DataView (data), thiết bị HID mà dữ liệu đó thuộc về (device) và mã báo cáo 8 bit liên kết với báo cáo đầu vào (reportId).

Ảnh Nintendo Switch màu đỏ và xanh dương.
Thiết bị Nintendo Switch Joy-Con.

Tiếp tục với ví dụ trước, đoạn mã dưới đây sẽ hướng dẫn bạn cách phát hiện nút mà người dùng đã nhấn trên thiết bị Joy-Con Right. Nhờ vậy, bạn có thể thử dùng 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 đầu ra

Để gửi báo cáo đầu ra đến 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ẽ 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ụ bên dưới áp dụng cho thiết bị Joy-Con và cho bạn biết cách tạo tiếng ồn cho thiết bị 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. Các giao thức này cho phép các thiết bị và ứng dụng HID trao đổi dữ liệu HID không 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 một báo cáo tính năng tới một thiết bị HID, hãy truyền mã báo cáo 8 bit được 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) tới device.sendFeatureReport(). Lời hứa được trả về sẽ phân giải 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ụ bên dưới minh hoạ cách 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ở thiết bị đó và làm cho 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) đến device.receiveFeatureReport(). Lời hứa được trả về sẽ 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...

Nghe thông báo 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 thiết bị HID

Trang web có thể xoá các quyền truy cập vào thiết bị HID mà trang web không còn muốn giữ lại 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 được dùng trên máy tính dùng chung với nhiều thiết bị, một lượng lớn quyền do người dùng tạo tích luỹ sẽ tạo ra trải nghiệm người dùng kém.

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ợ trong những phiên bản sau đây không:

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

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

Bạn có thể dễ dàng gỡ lỗi HID trong Chrome bằng trang nội bộ about://device-log. Tại đây, bạn có thể xem tất cả các sự kiện liên quan đến thiết bị HID và 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 để kết xuất thông tin thiết bị HID vào một định dạng mà con người có thể đọc được. Tệp này ánh xạ từ các giá trị sử dụng đến tên cho từng cách sử dụng HID.

Trên hầu hết các hệ thống Linux, các thiết bị HID được liên kết với quyền chỉ có thể đọc theo mặc định. Để cho phép Chrome mở một thiết bị HID, bạn cần thêm một 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} để có quy tắc cụ thể hơn. Hãy đảm bảo user của bạn là thành viên trong 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ử

Một số bản minh hoạ WebHID được liệt kê tại web.dev/hid-examples. Hãy xem thử!

Bảo mật và quyền riêng tư

Các tác giả thông số kỹ thuật đã thiết kế và triển khai API WebHID bằng cách sử dụng các nguyên tắc cốt lõi nêu trong bài viết 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 cả quyền kiểm soát của người dùng, tính minh bạch và công thái học. Khả năng sử dụng API này chủ yếu được kiểm soát bởi một mô hình 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 chủ động thực hiện các bước để chọn một thiết bị HID cụ thể.

Để hiểu rõ những đánh đổi về bảo mật, hãy xem phần Những cân nhắc về bảo mật và quyền riêng tư trong thông số kỹ thuật 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 sử dụng được bảo vệ (ví dụ: bàn phím, chuột chung), 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ệ được cung cấp 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ư thiết bị HID FIDO dùng để xác thực mạnh hơn) cũng bị chặn trong Chrome. Xem tệp danh sách chặn USBdanh sách chặn HID.

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ề WebHID API.

Giới thiệu cho chúng tôi về thiết kế API

API có 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 để triển khai ý tưởng không?

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

Báo cáo sự cố về triển khai

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

Hãy xem bài viết Cách báo cáo lỗi WebHID. Hãy nhớ cung cấp càng nhiều thông tin chi tiết càng tốt, cung cấp hướng dẫn đơn giản để tái hiện lỗi và đặt Components (Thành phần) thành Blink>HID. Glitch rất phù hợp để chia sẻ các bản tái hiện nhanh chóng và dễ dàng.

Thể hiện sự ủng hộ

Bạn có dự định sử dụng API WebHID không? Sự ủng hộ 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 thấy tầm quan trọng của việc hỗ trợ các tính năng đó.

Gửi một tweet đến @ChromiumDev bằng hashtag #WebHID và cho chúng tôi biết bạn đang sử dụng hashtag này ở đâu và như thế nào.

Đường liên kết hữu ích

Lời cảm ơn

Cảm ơn Matt ReynoldsJoe Medley đã xem xét bài viết này. Ảnh chụp 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.