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

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

François Beaufort
François Beaufort

Istnieje wiele urządzeń HID, takich jak alternatywne klawiatury czy nietypowe kontrolery gier, które są zbyt nowe, zbyt stare lub zbyt nietypowe, aby były dostępne dla sterowników urządzeń systemowych. Interfejs WebHID API rozwiązuje ten problem, zapewniając możliwość implementowania w języku JavaScript logiki specyficznej dla danego urządzenia.

Sugerowane zastosowania

Urządzenie HID przyjmuje dane wejściowe od użytkowników lub dostarcza dane wyjściowe do nich. Przykłady takich urządzeń to klawiatury, urządzenia wskazujące (myszy, ekrany dotykowe itp.) i kontrolery gier. Protokół HID umożliwia uzyskiwanie dostępu do tych urządzeń na komputerach z systemem operacyjnym za pomocą sterowników systemu. Platforma internetowa obsługuje urządzenia HID, korzystając z tych sterowników.

Brak dostępu do niestandardowych urządzeń HID jest szczególnie uciążliwy w przypadku alternatywnych klawiatur pomocniczych (np. Elgato Stream Deck, słuchawek Jabra, klawiszy X-keys) i nietypowych kontrolerów. Pady do gier przeznaczone do komputerów stacjonarnych często używają urządzeń HID do obsługi danych wejściowych (przycisków, joysticków i dźwigni) oraz danych wyjściowych (diod LED i wibracji). Niestety wejścia i wyjścia kontrolerów nie są dobrze znormalizowane, a przeglądarki internetowe często wymagają niestandardowej logiki w przypadku konkretnych urządzeń. Nie jest to trwałe rozwiązanie i nie zapewnia odpowiedniej obsługi dużej liczby starszych i rzadkich urządzeń. Sprawia też, że przeglądarka zależy od dziwacznych zachowań konkretnych urządzeń.

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.

Urządzenie HID (Human Interface Device) to urządzenie, które odbiera dane od użytkownika lub wysyła dane do użytkownika. Określenie to odnosi się też do protokołu HID, czyli standardu dwukierunkowej komunikacji między hostem a urządzeniem, który ma uprościć procedurę instalacji. Protokół HID został pierwotnie opracowany dla urządzeń USB, ale od tego czasu został zaimplementowany w ramach wielu innych protokołów, w tym Bluetooth.

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

Typ raportu Opis
Raport o wprowadzaniu Dane wysyłane z urządzenia do aplikacji (np. naciśnięcie przycisku).
Wygeneruj raport Dane wysyłane z aplikacji na urządzenie (np. prośba o włączenie podświetlenia klawiatury).
Raport o funkcjach Dane, które mogą być wysyłane w dowolnym kierunku. Format zależy od urządzenia.

Deskryptor raportu opisuje format binarny raportów obsługiwany przez urządzenie. Jego struktura jest hierarchiczna i umożliwia grupowanie raportów w odrębne kolekcje w ramach kolekcji najwyższego poziomu. Format deskryptora jest zdefiniowany w specyfikacji HID.

Użycie HID to wartość liczbowa odnosząca się do standardowego wejścia lub wyjścia. Wartości użycia umożliwiają urządzeniu opisywanie w raportach przeznaczenia urządzenia oraz celu każdego pola. Na przykład jeden z nich jest zdefiniowany dla lewego przycisku myszy. Dane o użytkowaniu są też uporządkowane na stronach, które wskazują ogólną kategorię urządzenia lub raportu.

Korzystanie z interfejsu WebHID API

Wykrywanie cech

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

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

Otwieranie połączenia HID

Interfejs WebHID API jest asynchroniczny, aby nie blokować interfejsu użytkownika witryny podczas oczekiwania na dane wejściowe. To ważne, ponieważ dane HID mogą być odbierane w dowolnym momencie, co wymaga sposobu ich odsłuchiwania.

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ć jedno z listy navigator.hid.getDevices(), która zwraca listę urządzeń, do których witryna miała wcześniej przyznany dostęp.

Funkcja navigator.hid.requestDevice() przyjmuje obowiązkowy obiekt, który definiuje filtry. Są one używane do dopasowywania dowolnego urządzenia z identyfikatorem producenta USB (vendorId), identyfikatorem produktu USB (productId), wartością strony o wykorzystaniu (usagePage) i wartością wykorzystania (usage). Możesz je pobrać 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();
Zrzut ekranu z prośbą o podłączenie urządzenia HID na stronie internetowej
Prośba o wybranie kontrolera Joy-Con do Nintendo Switch.

Możesz też użyć opcjonalnego klawisza exclusionFiltersnavigator.hid.requestDevice(), aby wykluczyć z selektora przeglądarki niektóre urządzenia, które np. nie działają prawidł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 producenta i produktu USB służące do identyfikacji urządzenia. Atrybut collections jest inicjowany hierarchicznej 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 HIDDevice są domyślnie zwracane w stanie „zamkniętym” i przed wysłaniem lub odebraniem danych należy je otworzyć, wywołując funkcję open().

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

otrzymywać raporty o wprowadzaniu danych,

Po nawiązaniu połączenia HID możesz obsługiwać przychodzące raporty o wejściu, nasłuchując zdarzeń "inputreport" na urządzeniu. 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 raportami wejściowymi (reportId).

Zdjęcie czerwonego i niebieskiego Nintendo Switch
Urządzenia Joy-Con Nintendo Switch.

W nawiązaniu do poprzedniego przykładu kod poniżej pokazuje, jak wykryć, który przycisk użytkownik nacisnął na urządzeniu Joy-Con Right, aby można było wypróbować to 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łanie raportów dotyczących danych wyjściowych

Aby wysłać raport wyjściowy do urządzenia HID, prześlij 8-bitowy identyfikator raportu powiązany z raportami wyjściowymi (reportId) i bajtami jako BufferSource (data) do device.sendReport(). Obietnica zostanie spełniona, gdy zgłoszenie zostanie wysłane. Jeśli urządzenie HID nie używa identyfikatorów raportów, ustaw wartość reportId na 0.

Przykład poniżej dotyczy urządzenia Joy-Con i pokazuje, jak ustawić wibracje 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 o funkcjach

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

Zdjęcie czarno-srebrnego laptopa.
Klawiatura laptopa

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

Przykład poniżej pokazuje, jak używać raportów funkcji, aby poprosić o urządzenie z podświetleniem klawiatury Apple, otworzyć je i sprawić, aby migało.

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 funkcji z urządzenia HID, prześlij 8-bitowy identyfikator raportu powiązany z raportami funkcji (reportId) do device.receiveFeatureReport(). Zwrócona obietnica jest realizowana za pomocą obiektu DataView zawierającego zawartość raportu funkcji. 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...

Odsłuchiwanie połączeń i rozłączeń

Gdy witryna ma przyznane 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 jest już potrzebne, wywołując funkcję forget() w przypadku instancji HIDDevice. Na przykład w przypadku edukacyjnej aplikacji internetowej używanej na współdzielonym komputerze z wieloma urządzeniami duża liczba gromadzonych uprawnień generowanych przez użytkowników może pogorszyć komfort korzystania z aplikacji.

Wywołanie funkcji forget() w jednym wystąpieniu HIDDevice spowoduje odebranie dostępu do wszystkich interfejsów HID na tym samym urządzeniu fizycznym.

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

Funkcja forget() jest dostępna w Chrome 100 lub nowszej wersji. Sprawdź, czy jest obsługiwana w przypadku tych funkcji:

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

Wskazówki dla programistów

Debugowanie urządzeń HID w Chrome jest łatwe dzięki wewnętrznej stronie about://device-log, na której w jednym miejscu możesz zobaczyć wszystkie zdarzenia związane z urządzeniami HID i USB.

Zrzut ekranu z wewnętrzną stroną do debugowania HID.
Wewnętrzna strona w Chrome do debugowania HID.

Aby wyodrębnić informacje o urządzeniu HID w formacie zrozumiałym dla człowieka, użyj eksploratora HID. Mapuje wartości użycia na nazwy poszczególnych zastosowań identyfikatora HID.

W większości systemów Linux urządzenia HID są domyślnie mapowane z dostępem tylko do odczytu. Aby umożliwić Chrome otwieranie urządzeń HID, musisz dodać nową regułę udev. Utwórz plik w folderze /etc/udev/rules.d/50-yourdevicename.rules z tą treścią:

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

W powyższym wierszu [yourdevicevendor] to 057e, jeśli Twoim urządzeniem jest np. kontroler Joy-Con konsoli Nintendo Switch. Aby utworzyć bardziej szczegółową regułę, możesz też dodać ATTRS{idProduct}. Upewnij się, że konto user jest członkiem grupy plugdev. Następnie ponownie podłącz urządzenie.

Obsługa przeglądarek

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

Prezentacje

Niektóre demonstracje WebHID można znaleźć na stronie web.dev/hid-examples. Sprawdź to.

Bezpieczeństwo i prywatność

Autorzy specyfikacji zaprojektowali i wdrożyli interfejs WebHID API, korzystając z podstawowych zasad zdefiniowanych w dokumentacji Kontrolowanie dostępu do zaawansowanych funkcji platformy internetowej, w tym 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 wykonać czynności, aby wybrać konkretne urządzenie HID.

Aby poznać kompromisy związane z bezpieczeństwem, przeczytaj sekcję Uwzględnienie zabezpieczeń i prywatności specyfikacji WebHID.

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

Pamiętaj, że w Chrome są też blokowane urządzenia HID o wysokim poziomie zabezpieczeń (np. urządzenia HID FIDO używane do uwierzytelniania). Zapoznaj się z plikami czarnej listy USBczarnej listy HID.

Prześlij opinię

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

Poinformuj nas o projektowaniu interfejsu API

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

Zgłoś problem ze specyfikacją w repozytorium GitHub interfejsu WebHID API lub podziel się opinią na temat istniejącego problemu.

Zgłaszanie problemów z implementacją

Czy znalazłeś/znalazłaś błąd w implementacji Chrome? Czy implementacja różni się od specyfikacji?

Zapoznaj się z informacjami o zgłaszaniu błędów WebHID. Pamiętaj, aby podać jak najwięcej szczegółów, dodać proste instrukcje odtwarzania błędu i ustawić wartość Blink>HID dla Components. Glitch to świetne narzędzie do szybkiego i łatwego udostępniania informacji o powtarzalności problemu.

Pokaż pomoc

Zamierzasz używać interfejsu WebHID API? Twoja publiczna pomoc pomaga zespołowi Chrome ustalać priorytety funkcji i pokazuje innym dostawcom przeglądarek, jak ważne jest ich wsparcie.

Wyślij tweeta do @ChromiumDev, używając hashtaga #WebHID, i podaj, gdzie i jak go używasz.

Przydatne linki

Podziękowania

Dziękujemy Mattowi ReynoldsowiJoe Medleyowi za sprawdzenie tego artykułu. Czerwono-niebieskie zdjęcie konsoli Nintendo Switch: Sara Kurfeß. Czarne i srebrne zdjęcie laptopa: Athul Cyriac Ajay, Unsplash.