Nawiązywanie połączeń z rzadkimi urządzeniami HID

Interfejs WebHID API umożliwia witrynom dostęp do alternatywnych klawiatur pomocniczych i egzotycznych padów do gier.

François Beaufort
François Beaufort

Istnieją rozbudowane urządzenia interfejsu (HID), takie jak alternatywne klawiatury czy egzotyczne pady do gier, które są zbyt nowe, zbyt stare lub zbyt rzadko dostępne dla sterowników urządzeń. Interfejs WebHID API rozwiązuje ten problem, udostępniając sposób implementacji logiki związanej z urządzeniem w JavaScript.

Sugerowane przypadki użycia

Urządzenie HID pobiera dane wejściowe od użytkowników lub udostępnia je użytkownikom. Przykładami urządzeń są klawiatury, urządzenia wskazujące (myszy, ekrany dotykowe itp.) oraz pady do gier. Protokół HID umożliwia dostęp do tych urządzeń na komputerach z użyciem sterowników systemu operacyjnego. Platforma internetowa obsługuje urządzenia HID dzięki tym sterownikom.

Brak dostępu do nietypowych urządzeń HID jest szczególnie uciążliwy, gdy chodzi o alternatywne klawiatury pomocnicze (np. Elgato Stream Deck, zestawy słuchawkowe Jabra, klawisze X) i egzotyczne pady do gier. Pady do gier przeznaczone na komputery często wykorzystują HID jako wejście (przyciski, joysticki, spusty) i wyjściowe wyjścia (diody LED, drżenie). Wejścia i wyjścia pada do gier nie są dobrze ustandaryzowane, a przeglądarki często wymagają niestandardowej logiki pod kątem konkretnych urządzeń. Jest to niemożliwe do zrównoważenia i skutkuje słabą obsługą starszych i rzadszych urządzeń. Sprawia to również, że przeglądarka zależy na nietypowych działaniach na konkretnych urządzeniach.

Terminologia

HID obejmuje 2 podstawowe koncepcje: raporty i deskryptory raportów. Raporty to dane wymieniane między urządzeniem a klientem oprogramowania. Deskryptor raportu opisuje format i znaczenie danych obsługiwanych przez urządzenie.

HID (Human Interface Device) to typ urządzenia, które pobiera dane wejściowe od użytkowników lub je dostarcza. Odnosi się również do protokołu HID – standardu dwukierunkowej komunikacji między hostem a urządzeniem, który ma na celu uproszczenie procedury instalacji. Protokół HID pierwotnie powstał z myślą o urządzeniach USB, ale obecnie jest wdrażany w ramach wielu innych protokołów, w tym Bluetooth.

Aplikacje i urządzenia HID wymieniają dane binarne, korzystając z 3 typów raportów:

Typ raportu Opis
Raport wejściowy Dane wysyłane z urządzenia do aplikacji (np. po naciśnięciu przycisku).
Raport wyjściowy Dane wysyłane z aplikacji do urządzenia (np. prośba o włączenie podświetlenia klawiatury).
Raport funkcji Dane, które mogą być wysyłane w dowolnym kierunku. Format jest przypisany do konkretnych urządzeń.

Deskryptor raportu opisuje binarny format raportów obsługiwanych przez urządzenie. Jej struktura jest hierarchiczna i może grupować raporty w odrębne zbiory w ramach kolekcji najwyższego poziomu. Format deskryptora jest określony w specyfikacji HID.

Wykorzystanie HID to wartość liczbowa odnosząca się do standaryzowanych danych wejściowych lub wyjściowych. Wartości wykorzystania umożliwiają urządzeniu określenie przeznaczenia urządzenia i przeznaczenie poszczególnych pól w raportach. Na przykład 1 jest zdefiniowana dla lewego przycisku myszy. Informacje o użytkowaniu są też uporządkowane na stronach wykorzystania, które wskazują ogólną kategorię urządzenia lub raportu.

Używanie interfejsu WebHID API

Wykrywanie funkcji

Aby sprawdzić, czy interfejs WebHID API jest obsługiwany, użyj tych elementów:

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

Otwieranie połączenia HID

Interfejs WebHID API jest z założenia asynchroniczny, aby zapobiegać blokowaniu przez interfejs użytkownika w przypadku oczekiwania na dane wejściowe. To ważne, ponieważ dane HID mogą być odbierane w dowolnym momencie, co wymaga sposobu na ich odsłuchiwanie.

Aby otworzyć połączenie HID, najpierw uzyskaj dostęp do obiektu HIDDevice. Aby to zrobić, możesz poprosić użytkownika o wybranie urządzenia, dzwoniąc pod numer navigator.hid.requestDevice(), albo wybrać urządzenie z listy navigator.hid.getDevices(), które zwróci listę urządzeń, do których witryna otrzymała wcześniej dostęp.

Funkcja navigator.hid.requestDevice() przyjmuje obowiązkowy obiekt, który określa filtry. Służą one do dopasowywania dowolnych urządzeń połączonych z identyfikatorem dostawcy USB (vendorId), identyfikatorem produktu USB (productId), wartością strony wykorzystania (usagePage) i wartością wykorzystania (usage). Te dane znajdziesz w repozytorium identyfikatorów USB i tabeli użytkowania HID.

Wiele obiektów HIDDevice zwróconych przez tę funkcję reprezentuje wiele interfejsów HID na tym samym urządzeniu fizycznym.

// 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();
Zrzut ekranu z komunikatem dotyczącym urządzenia HID na stronie internetowej.
Prompt dla użytkownika dotyczący wyboru konsoli Nintendo Switch Joy-Con.

Możesz też użyć opcjonalnego klucza exclusionFilters w elemencie navigator.hid.requestDevice(), aby wykluczyć z selektora przeglądarki niektóre urządzenia, o których wiadomo, że działają nieprawidłowo.

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

Obiekt HIDDevice zawiera identyfikatory dostawcy i produktu USB służące do identyfikacji urządzenia. Jej atrybut collections jest inicjowany hierarchicznym opisem formatów raportów urządzenia.

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
}

Urządzenia z HIDDevice są domyślnie zwracane w stanie „zamknięte”. Aby można było wysyłać lub odbierać dane, należy je otworzyć, wywołując open().

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

Otrzymywanie raportów wejściowych

Po ustanowieniu połączenia HID możesz obsługiwać przychodzące raporty wejściowe, nasłuchując zdarzeń "inputreport" z urządzenia. Te zdarzenia zawierają dane HID w postaci obiektu DataView (data), urządzenia HID, do którego należą (device) oraz 8-bitowego identyfikatora raportu powiązanego z raportem wejściowym (reportId).

Czerwono-niebieskie zdjęcie Nintendo Switch.
Urządzenia Nintendo Switch Joy-Con.

Nawiązując do poprzedniego przykładu, poniżej znajdziesz kod pokazujący, jak wykryć przycisk naciśnięty przez użytkownika na urządzeniu Joy-Con Right i wypróbowywać go w domu.

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

Wysyłaj raporty wyjściowe

Aby wysłać raport wyjściowy do urządzenia HID, przekaż 8-bitowy identyfikator raportu powiązany z raportem wyjściowym (reportId) oraz bajty jako BufferSource (data) do device.sendReport(). Zwrócona obietnica zniknie po wysłaniu raportu. Jeśli urządzenie HID nie korzysta z identyfikatorów raportów, ustaw reportId na 0.

Poniższy przykład dotyczy urządzenia Joy-Con i pokazuje, jak sprawić, by ta fala grała na danych w raportach wyjściowych.

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

Wysyłanie i odbieranie raportów funkcji

Raporty cech to jedyny typ raportów z danymi HID, które mogą być przesyłane w obu kierunkach. Umożliwiają one urządzeniom i aplikacjom HID wymianę niestandaryzowanych danych HID. W przeciwieństwie do raportów wejściowych i wyjściowych raporty dotyczące funkcji nie są regularnie odbierane ani wysyłane przez aplikację.

Czarno-srebrne zdjęcie laptopa.
Klawiatura laptopa

Aby wysłać raport funkcji do urządzenia HID, przekaż 8-bitowy identyfikator raportu powiązany z raportem cech (reportId) i bajty jako BufferSource (data) do device.sendFeatureReport(). Zwrócona obietnica zniknie po wysłaniu raportu. Jeśli urządzenie HID nie korzysta z identyfikatorów raportów, ustaw reportId na 0.

Przykład poniżej pokazuje, jak korzystać z raportów funkcji. W tym przykładzie pokazujemy, jak wysłać żądanie urządzenia podświetlanego z klawiaturą Apple, otworzyć je i mrugać.

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

Aby otrzymać raport na temat funkcji z urządzenia HID, przekaż 8-bitowy identyfikator raportu powiązany z raportem funkcji (reportId) do device.receiveFeatureReport(). Zwrócona obietnica znika dzięki obiektowi DataView, który zawiera treść raportu o funkcjach. Jeśli urządzenie HID nie korzysta z identyfikatorów raportów, ustaw reportId na 0.

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

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

Nasłuchiwanie połączenia i rozłączenia

Jeśli witryna otrzyma uprawnienia dostępu do urządzenia HID, może aktywnie odbierać zdarzenia dotyczące połączenia i rozłączania przez nasłuchiwanie zdarzeń "connect" i "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.
});

Anulowanie dostępu do urządzenia HID

Witryna może wyczyścić uprawnienia dostępu do urządzenia HID, które nie jest już potrzebne, wywołując forget() w instancji HIDDevice. Na przykład w przypadku edukacyjnej aplikacji internetowej używanej na wspólnym komputerze z wieloma urządzeniami duża liczba uprawnień generowanych przez użytkowników pogarsza komfort korzystania z usługi.

Wywołanie forget() w pojedynczej instancji HIDDevice cofnie dostęp do wszystkich interfejsów HID na tym samym urządzeniu fizycznym.

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

Usługa forget() jest dostępna w Chrome 100 i nowszych wersjach, więc sprawdź, czy obsługuje ją:

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

Wskazówki dla programistów

Debugowanie HID w Chrome jest proste dzięki wewnętrznej stronie about://device-log, na której w jednym miejscu znajdziesz wszystkie zdarzenia związane z HID i urządzeniami USB.

Zrzut ekranu przedstawiający stronę wewnętrzną do debugowania urządzenia HID.
Wewnętrzna strona w Chrome służąca do debugowania HID.

Jeśli chcesz przesłać informacje o urządzeniu HID do formatu zrozumiałego dla człowieka, skorzystaj z eksploratora HID. Mapuje on wartości wykorzystania na nazwy dla każdego użycia HID.

W większości systemów z systemem Linux urządzenia HID są domyślnie mapowane z uprawnieniami tylko do odczytu. Aby zezwolić Chrome na otwieranie urządzenia HID, musisz dodać nową regułę udev. W /etc/udev/rules.d/50-yourdevicename.rules utwórz plik z tą zawartością:

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

W wierszu powyżej [yourdevicevendor] ma wartość 057e, jeśli masz np. urządzenie Nintendo Switch Joy-Con. Można też dodać ATTRS{idProduct} dla bardziej szczegółowej reguły. Upewnij się, że user jest członkiem grupy plugdev. Potem po prostu ponownie podłącz urządzenie.

Obsługiwane przeglądarki

Interfejs WebHID API jest dostępny w Chrome 89 na wszystkich platformach komputerowych (ChromeOS, Linux, macOS i Windows).

Przykłady

Niektóre wersje demonstracyjne WebHID są wymienione na stronie web.dev/hid-examples. Spójrzmy!

Prywatność i bezpieczeństwo

Autorzy specyfikacji zaprojektowali i wdrożyli interfejs WebHID API zgodnie z podstawowymi zasadami określonymi w artykule Kontrolowanie dostępu do zaawansowanych funkcji platformy internetowej, w tym dotyczących kontroli użytkowników, przejrzystości i ergonomii. Dostęp do tego interfejsu API zależy głównie od modelu uprawnień, który w danym momencie przyznaje dostęp tylko jednemu urządzeniu HID. W odpowiedzi na prośbę użytkownika użytkownik musi podjąć aktywne działania, aby wybrać konkretne urządzenie HID.

Aby poznać wady i zalety zabezpieczeń, zapoznaj się z sekcją Uwagi na temat bezpieczeństwa i prywatności w specyfikacji WebHID.

Dodatkowo Chrome sprawdza wykorzystanie każdej kolekcji najwyższego poziomu.Jeśli kolekcja najwyższego poziomu jest chroniona (np. z użyciem klawiatury ogólnej, myszy), witryna nie może wysyłać ani odbierać raportów zdefiniowanych w zbiorze. Pełna lista chronionych zastosowań jest dostępna publicznie.

Pamiętaj, że urządzenia HID wrażliwe na zagrożenia (np. urządzenia FIDO HID używane do silniejszego uwierzytelniania) też są blokowane w Chrome. Zobacz pliki listy zablokowanych USB i listy zablokowanych HID.

Prześlij opinię

Zespół Chrome chętnie pozna Twoją opinię i doświadczenia związane z interfejsem WebHID API.

Opowiedz nam o projekcie interfejsu API

Czy jest coś, co nie działa w interfejsie API zgodnie z oczekiwaniami? A może brakuje metod lub właściwości, które są niezbędne do realizacji pomysłu?

Zgłoś problem ze specyfikacją w repozytorium WebHID API na GitHubie lub dodaj swoje uwagi do istniejącego problemu.

Zgłoś problem z implementacją

Czy wystąpił błąd związany z implementacją przeglądarki Chrome? A może implementacja różni się od specyfikacji?

Dowiedz się, jak zgłaszać błędy w WebHID. Podaj jak najwięcej szczegółów i proste instrukcje umożliwiające odtworzenie błędu, a także ustaw Komponenty na wartość Blink>HID. Glitch to świetny sposób na udostępnianie szybkich i łatwych replik.

Okaż wsparcie

Czy zamierzasz używać interfejsu WebHID API? Twoja publiczna pomoc pomaga zespołowi Chrome priorytetowo traktować funkcje i pokazuje innym dostawcom przeglądarek, jak ważne jest ich wsparcie.

Wyślij tweeta na adres @ChromiumDev, używając hashtagu #WebHID, i daj nam znać, gdzie i w jaki sposób go używasz.

Przydatne linki

Podziękowania

Dziękujemy Matt Reynolds i Joe Medley za opinię o tym artykule. Czerwono-niebieskie zdjęcie Nintendo Switch: Sara Kurfeß oraz czarno-srebrne zdjęcie komputera laptopa – Athul Cyriac Ajay w Unsplash.