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

Interfejs WebHID API umożliwia witrynom dostęp do alternatywnych klawiatur pomocniczych i nietypowych gamepadów.

François Beaufort
François Beaufort

Opublikowano: 15 września 2020 r.

Browser Support

  • Chrome: 89.
  • Edge: 89.
  • Firefox: not supported.
  • Safari: not supported.

Source

Istnieje wiele urządzeń interfejsu (HID), takich jak alternatywne klawiatury czy nietypowe gamepady, które są zbyt nowe, zbyt stare lub zbyt rzadko używane, aby były dostępne dla sterowników urządzeń systemowych. Interfejs WebHID API rozwiązuje ten problem, umożliwiając implementację logiki specyficznej dla urządzenia w JavaScript.

Sugerowane przypadki użycia

Urządzenie HID przyjmuje dane wejściowe od użytkowników lub przekazuje im dane wyjściowe. Przykłady urządzeń to klawiatury, urządzenia wskazujące (myszy, ekrany dotykowe itp.) i gamepady. Protokół HID umożliwia dostęp do tych urządzeń na komputerach stacjonarnych za pomocą 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 w przypadku alternatywnych klawiatur pomocniczych (takich jak Elgato Stream Deck, słuchawki Jabra, X-keys) i obsługi egzotycznych gamepadów. Pady do gier przeznaczone na komputery stacjonarne często używają HID do przesyłania danych wejściowych (przyciski, joysticki, spusty) i wyjściowych (diody LED, wibracje).

Niestety wejścia i wyjścia gamepada nie są dobrze ustandaryzowane, a przeglądarki internetowe często wymagają niestandardowej logiki dla konkretnych urządzeń. Jest to nie do utrzymania i skutkuje słabą obsługą starszych i rzadko używanych urządzeń. Powoduje to również, że przeglądarka jest zależna od dziwnych zachowań konkretnych urządzeń.

Terminologia

Urządzenie interfejsu (HID) może przyjmować dane wejściowe od użytkowników lub przekazywać im dane wyjściowe. Istnieje protokół HID, czyli standard dwukierunkowej komunikacji między hostem a urządzeniem, który ma uprościć proces instalacji.

HID opiera się na 2 podstawowych koncepcjach: raportach i deskryptorach 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.

Aplikacje i urządzenia HID wymieniają dane binarne za pomocą 3 typów raportów:

Typ raportu Opis
Raport danych wejściowych dane wysyłane z urządzenia do aplikacji (np. naciśnięcie przycisku);
Raport wyjściowy Dane wysyłane z aplikacji na urządzenie (np. prośba o włączenie podświetlenia klawiatury).
Raport funkcji Dane, które mogą być wysyłane w obu kierunkach. Format zależy od urządzenia.

Deskryptor raportu opisuje format binarny raportów obsługiwanych przez urządzenie. Jego struktura jest hierarchiczna i umożliwia grupowanie raportów w postaci odrębnych kolekcji w ramach kolekcji najwyższego poziomu. Format deskryptora jest określony w specyfikacji HID.

Użycie HID to wartość liczbowa odnosząca się do standardowego wejścia lub wyjścia. Wartości użycia pozwalają urządzeniu opisać zamierzone zastosowanie urządzenia i przeznaczenie każdego pola w raportach. Na przykład jeden jest zdefiniowany dla lewego przycisku myszy. Sposoby użycia są też uporządkowane na stronach użycia, które zawierają informacje o kategorii urządzenia lub raportu.

Korzystanie z interfejsu WebHID API

Aby sprawdzić, czy interfejs WebHID API jest obsługiwany, użyj tego kodu:

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

Otwieranie połączenia HID

Interfejs WebHID API jest asynchroniczny, aby zapobiec blokowaniu interfejsu użytkownika witryny podczas oczekiwania na dane wejściowe. Jest to ważne, ponieważ dane HID mogą być odbierane w dowolnym momencie, co wymaga sposobu na ich nasłuchiwanie.

Aby otworzyć połączenie HID, najpierw uzyskaj dostęp do obiektu HIDDevice. W tym celu możesz poprosić użytkownika o wybranie urządzenia, wywołując funkcję navigator.hid.requestDevice(), lub wybrać urządzenie z listy navigator.hid.getDevices(), która zwraca listę urządzeń, do których witryna uzyskała wcześniej dostęp.

Funkcja navigator.hid.requestDevice() przyjmuje obowiązkowy obiekt, który definiuje filtry. Służą one do dopasowywania dowolnego urządzenia podłączonego za pomocą identyfikatora producenta USB (vendorId), identyfikatora produktu USB (productId), wartości strony o wykorzystaniu (usagePage) i wartości wykorzystania (usage). Możesz je uzyskać z repozytorium identyfikatorów USBdokumentu z tabelami wykorzystania HID.

Wiele obiektów HIDDevice zwracanych 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();
Prompt użytkownika dotyczący wyboru kontrolera Nintendo Switch Joy-Con.

Możesz też użyć opcjonalnego klucza exclusionFiltersnavigator.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 do identyfikacji urządzenia. Jego atrybut collections jest inicjowany za pomocą hierarchicznego opisu 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 HIDDevice są domyślnie zwracane w stanie „zamkniętym” i muszą zostać otwarte przez połączenie z numerem open(), zanim będzie można wysyłać lub odbierać dane.

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

Otrzymywanie raportów o danych wejściowych

Nintendo Switch Joy-Cons.

Po nawiązaniu 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 jako obiekt DataView (data), urządzenie HID, do którego należą (device), oraz 8-bitowy identyfikator raportu powiązany z raportem wejściowym (reportId).

W tym przykładzie kod pomaga wykryć, który przycisk użytkownik nacisnął na urządzeniu Joy-Con Right, aby można było wypróbować 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]}.`);
});

Skorzystaj z demonstracji w CodePen.

Wysyłanie raportów wyjściowych

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

Kolejny przykład dotyczy urządzenia Joy-Con i pokazuje, jak wywołać wibracje za pomocą raportów 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.
const rumbleData = [ /* ... */ ];
await device.sendReport(0x10, new Uint8Array(rumbleData));

Skorzystaj z demonstracji w CodePen.

Wysyłanie i odbieranie raportów o funkcjach

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

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

Ten przykład ilustruje użycie raportów o funkcjach. Pokazuje, jak wysłać żądanie podświetlenia klawiatury Apple, otworzyć je i sprawić, aby zaczęło migać.

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

Skorzystaj z demonstracji w CodePen.

Aby otrzymać raport o funkcji z urządzenia HID, przekaż 8-bitowy identyfikator raportu powiązany z raportem o funkcji (reportId) do device.receiveFeatureReport(). Zwrócony obiekt Promise jest rozwiązywany za pomocą obiektu DataView, który zawiera treść raportu o funkcjach. Jeśli urządzenie HID nie używa identyfikatorów raportów, ustaw wartość reportId na 0.

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

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

Słuchanie połączeń i rozłączeń

Gdy witryna uzyska uprawnienia dostępu do urządzenia HID, może aktywnie odbierać zdarzenia połączenia i rozłączenia, nasłuchując zdarzeń "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.
});

Cofanie dostępu do urządzenia HID

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

Wywołanie funkcji forget() na pojedynczej instancji HIDDevice spowoduje unieważnienie dostępu do wszystkich interfejsów HID na tym samym urządzeniu fizycznym.

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

forget() jest dostępny w Chrome 100 lub nowszym, więc sprawdź, czy ta funkcja jest obsługiwana:

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

Wskazówki dla programistów

Wewnętrzna strona do debugowania HID.

Debuguj urządzenia HID w Chrome za pomocą strony wewnętrznej about://device-log, na której możesz zobaczyć wszystkie zdarzenia związane z urządzeniami HID i USB w jednym miejscu.

Aby wyeksportować informacje o urządzeniu HID do formatu czytelnego dla człowieka, skorzystaj z eksploratora HID. Mapuje wartości użycia na nazwy dla każdego użycia HID.

W większości systemów 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. Utwórz plik w lokalizacji /etc/udev/rules.d/50-yourdevicename.rules z tą treścią:

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

W tym kodzie [yourdevicevendor] to 057e, na przykład jeśli urządzeniem jest kontroler Joy-Con Nintendo Switch. ATTRS{idProduct} można dodać, aby utworzyć bardziej szczegółową regułę. Upewnij się, że user jest członkiem grupy plugdev. Następnie ponownie podłącz urządzenie.

Przykłady

Przykłady niektórych demonstracji WebHID znajdziesz na stronie web.dev/hid-examples.

Bezpieczeństwo i prywatność

Autorzy specyfikacji zaprojektowali i wdrożyli interfejs WebHID API zgodnie z podstawowymi zasadami określonymi w artykule Controlling Access to Powerful Web Platform Features, w tym z zasadami kontroli użytkownika, przejrzystości i ergonomii. Możliwość korzystania z tego interfejsu API jest ograniczona głównie przez model uprawnień, który przyznaje dostęp tylko do jednego urządzenia HID naraz. W odpowiedzi na prośbę użytkownika musi on aktywnie wybrać konkretne urządzenie HID.

Aby poznać kompromisy w zakresie bezpieczeństwa, zapoznaj się z sekcją Security and Privacy Considerations (Względy bezpieczeństwa i prywatności) w specyfikacji WebHID.

Dodatkowo Chrome sprawdza użycie każdej kolekcji najwyższego poziomu.Jeśli kolekcja najwyższego poziomu ma chronione zastosowanie (np. klawiatura ogólnego przeznaczenia, mysz), witryna nie będzie mogła wysyłać ani odbierać żadnych raportów zdefiniowanych w tej kolekcji. Pełna lista chronionych zastosowań jest publicznie dostępna.

Pamiętaj, że w Chrome blokowane są też urządzenia HID, które są istotne z punktu widzenia bezpieczeństwa (np. urządzenia HID zgodne z FIDO używane do silniejszego uwierzytelniania). Zobacz pliki USB blocklistHID blocklist.

Prześlij opinię

Zespół Chrome chętnie pozna Twoje opinie i wrażenia związane z interfejsem WebHID API.

Opisz projekt interfejsu API

Czy w API jest coś, co nie działa zgodnie z oczekiwaniami? A może brakuje metod lub właściwości, których potrzebujesz do realizacji swojego pomysłu?

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

Zgłaszanie problemu z implementacją

Czy w implementacji Chrome występuje błąd? Czy implementacja różni się od specyfikacji?

Przeczytaj artykuł Jak zgłaszać błędy w WebHID. Podaj jak najwięcej szczegółów, opisz, jak odtworzyć błąd, i ustaw Komponenty na Blink>HID.

Przydatne linki

Podziękowania

Dziękujemy Mattowi ReynoldsowiJoe Medleyowi za ich opinie.