Подключение к необычным HID-устройствам

WebHID API позволяет веб-сайтам получать доступ к альтернативным вспомогательным клавиатурам и экзотическим геймпадам.

Франсуа Бофор
François Beaufort

Существует длинный хвост устройств пользовательского интерфейса (HID), таких как альтернативные клавиатуры или экзотические геймпады, которые слишком новые, слишком старые или слишком необычные, чтобы быть доступными для системных драйверов устройств. API WebHID решает эту проблему, предоставляя способ реализации логики конкретного устройства в JavaScript.

Рекомендуемые варианты использования

Устройство HID принимает входные данные или предоставляет выходные данные людям. Примеры устройств включают клавиатуры, указывающие устройства (мыши, сенсорные экраны и т. д.) и геймпады. Протокол HID позволяет получить доступ к этим устройствам на настольных компьютерах с помощью драйверов операционной системы. Веб-платформа поддерживает устройства HID, используя эти драйверы.

Отсутствие доступа к необычным HID-устройствам особенно болезненно, когда речь идет об альтернативных вспомогательных клавиатурах (например, Elgato Stream Deck , гарнитурах Jabra , X-keys ) и поддержке экзотических геймпадов. Геймпады, предназначенные для настольных компьютеров, часто используют HID для входов геймпада (кнопки, джойстики, триггеры) и выходов (светодиоды, вибрация). К сожалению, входы и выходы геймпада недостаточно стандартизированы, а веб-браузерам часто требуется специальная логика для конкретных устройств. Это неустойчиво и приводит к плохой поддержке длинного хвоста старых и необычных устройств. Это также приводит к тому, что браузер зависит от особенностей поведения конкретных устройств.

Терминология

HID состоит из двух фундаментальных понятий: отчетов и дескрипторов отчетов. Отчеты — это данные, которыми обмениваются устройство и программный клиент. Дескриптор отчета описывает формат и значение данных, которые поддерживает устройство.

HID (Human Interface Device) — это тип устройства, которое принимает входные данные или предоставляет выходные данные людям. Это также относится к протоколу HID, стандарту двунаправленной связи между хостом и устройством, предназначенному для упрощения процедуры установки. Протокол HID изначально был разработан для USB-устройств, но с тех пор был реализован во многих других протоколах, включая Bluetooth.

Приложения и HID-устройства обмениваются двоичными данными с помощью трех типов отчетов:

Тип отчета Описание
Входной отчет Данные, которые отправляются с устройства в приложение (например, нажатие кнопки).
Выходной отчет Данные, которые отправляются из приложения на устройство (например, запрос на включение подсветки клавиатуры).
Отчет о функциях Данные, которые могут быть отправлены в любом направлении. Формат зависит от устройства.

Дескриптор отчета описывает двоичный формат отчетов, поддерживаемый устройством. Его структура является иерархической и позволяет группировать отчеты в отдельные коллекции внутри коллекции верхнего уровня. Формат дескриптора определяется спецификацией HID.

Использование HID — это числовое значение, относящееся к стандартизированному вводу или выводу. Значения использования позволяют устройству описывать предполагаемое использование устройства и назначение каждого поля в отчетах. Например, один определен для левой кнопки мыши. Использование также организовано в страницы использования, на которых указана категория высокого уровня устройства или отчета.

Использование API WebHID

Обнаружение функций

Чтобы проверить, поддерживается ли WebHID API, используйте:

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

Открыть HID-соединение

API WebHID является асинхронным по своей конструкции, чтобы предотвратить блокировку пользовательского интерфейса веб-сайта при ожидании ввода. Это важно, поскольку данные HID могут быть получены в любое время, поэтому требуется способ их прослушивания.

Чтобы открыть HID-соединение, сначала обратитесь к объекту HIDDevice . Для этого вы можете либо предложить пользователю выбрать устройство, вызвав navigator.hid.requestDevice() , либо выбрать его из navigator.hid.getDevices() , который возвращает список устройств, к которым веб-сайту был предоставлен доступ ранее.

Функция navigator.hid.requestDevice() принимает обязательный объект, определяющий фильтры. Они используются для сопоставления любого устройства, подключенного к идентификатору поставщика USB ( vendorId ), идентификатору продукта USB ( productId ), значению страницы использования ( usagePage ) и значению использования ( usage ). Вы можете получить их из репозитория USB ID и документа «Таблицы использования HID» .

Несколько объектов HIDDevice возвращаемых этой функцией, представляют несколько интерфейсов HID на одном физическом устройстве.

// 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();
Снимок экрана с приглашением устройства HID на веб-сайте.
Запрос пользователя на выбор Nintendo Switch Joy-Con.

Вы также можете использовать дополнительный ключ exclusionFilters в navigator.hid.requestDevice() чтобы исключить из средства выбора браузера некоторые устройства, о которых известно, что они работают неправильно.

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

Объект HIDDevice содержит идентификаторы производителя и продукта USB для идентификации устройства. Его атрибут collections инициализируется иерархическим описанием форматов отчетов устройства.

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
}

Устройства HIDDevice по умолчанию возвращаются в «закрытом» состоянии и должны быть открыты с помощью вызова open() прежде чем данные смогут быть отправлены или получены.

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

Получать отчеты о вводе

После установки HID-соединения вы можете обрабатывать входящие отчеты о вводе, прослушивая события "inputreport" с устройства. Эти события содержат данные HID в виде объекта DataView ( data ), HID-устройства, которому они принадлежат ( device ), и 8-битный идентификатор отчета, связанный с входным отчетом ( reportId ).

Красно-синее фото переключателя Nintendo.
Устройства Nintendo Switch Joy-Con.

Продолжая предыдущий пример, приведенный ниже код показывает, как определить, какую кнопку пользователь нажал на устройстве Joy-Con Right, чтобы вы могли попробовать это дома.

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

Отправлять выходные отчеты

Чтобы отправить выходной отчет на устройство HID, передайте 8-битный идентификатор отчета, связанный с выходным отчетом ( reportId ), и байты в качестве BufferSource ( data ) в device.sendReport() . Возвращенное обещание разрешается после отправки отчета. Если устройство HID не использует идентификаторы отчетов, установите для reportId значение 0.

Пример ниже относится к устройству Joy-Con и показывает, как заставить его вибрировать с помощью выходных отчетов.

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

Отправлять и получать отчеты о функциях

Отчеты об объектах — единственный тип отчетов с данными HID, которые могут передаваться в обоих направлениях. Они позволяют устройствам и приложениям HID обмениваться нестандартизированными данными HID. В отличие от отчетов о вводе и выводе, отчеты о функциях не принимаются и не отправляются приложением на регулярной основе.

Черно-серебряное фото портативного компьютера.
Клавиатура ноутбука

Чтобы отправить отчет о функции на устройство HID, передайте 8-битный идентификатор отчета, связанный с отчетом о функции ( reportId ), и байты в качестве BufferSource ( data ) в device.sendFeatureReport() . Возвращенное обещание разрешается после отправки отчета. Если устройство HID не использует идентификаторы отчетов, установите для reportId значение 0.

Пример ниже иллюстрирует использование отчетов о функциях, показывая, как запросить устройство подсветки клавиатуры Apple, открыть его и заставить его мигать.

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

Чтобы получить отчет о функции от устройства HID, передайте 8-битный идентификатор отчета, связанный с отчетом о функции ( reportId ), в device.receiveFeatureReport() . Возвращенное обещание разрешается с помощью объекта DataView , содержащего содержимое отчета о функциях. Если устройство HID не использует идентификаторы отчетов, установите для reportId значение 0.

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

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

Слушайте соединение и отключение

Когда веб-сайту предоставлено разрешение на доступ к устройству HID, он может активно получать события подключения и отключения, прослушивая события "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.
});

Отменить доступ к HID-устройству

Веб-сайт может очистить разрешения на доступ к устройству HID, в сохранении которого он больше не заинтересован, вызвав функцию forget() в экземпляре HIDDevice . Например, для образовательного веб-приложения, используемого на общем компьютере со многими устройствами, большое количество накопленных разрешений, созданных пользователями, ухудшает взаимодействие с пользователем.

Вызов forget() на одном экземпляре HIDDevice аннулирует доступ ко всем интерфейсам HID на одном физическом устройстве.

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

Поскольку forget() доступна в Chrome 100 и более поздних версиях, проверьте, поддерживается ли эта функция, с помощью следующего:

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

Советы разработчикам

Отладка HID в Chrome упрощается благодаря внутренней странице about://device-log , где вы можете увидеть все события, связанные с HID и USB-устройствами, в одном месте.

Скриншот внутренней страницы для отладки HID.
Внутренняя страница в Chrome для отладки HID.

Воспользуйтесь HID Explorer , чтобы преобразовать информацию об устройстве HID в удобочитаемый формат. Он сопоставляет значения использования с именами для каждого использования HID.

В большинстве систем Linux устройства HID по умолчанию сопоставлены с разрешениями только на чтение. Чтобы разрешить Chrome открывать HID-устройство, вам нужно будет добавить новое правило udev . Создайте файл /etc/udev/rules.d/50-yourdevicename.rules со следующим содержимым:

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

В строке выше [yourdevicevendor]057e , если ваше устройство — это, например, Nintendo Switch Joy-Con. ATTRS{idProduct} также можно добавить для более конкретного правила. Убедитесь, что ваш user является членом группы plugdev . Затем просто повторно подключите устройство.

Поддержка браузера

API WebHID доступен на всех настольных платформах (ChromeOS, Linux, macOS и Windows) в Chrome 89.

Демо

Некоторые демо-версии WebHID перечислены по адресу web.dev/hid-examples . Иди посмотри!

Безопасность и конфиденциальность

Авторы спецификации разработали и реализовали API WebHID, используя основные принципы, определенные в разделе «Управление доступом к мощным функциям веб-платформы» , включая пользовательский контроль, прозрачность и эргономику. Возможность использования этого API в первую очередь ограничивается моделью разрешений, которая предоставляет доступ только к одному HID-устройству одновременно. В ответ на запрос пользователя пользователь должен предпринять активные действия для выбора конкретного HID-устройства.

Чтобы понять компромиссы в области безопасности, ознакомьтесь с разделом «Вопросы безопасности и конфиденциальности» спецификации WebHID.

Кроме того, Chrome проверяет использование каждой коллекции верхнего уровня, и если коллекция верхнего уровня имеет защищенное использование (например, универсальную клавиатуру, мышь), веб-сайт не сможет отправлять и получать какие-либо отчеты, определенные в эта коллекция. Полный список защищенных вариантов использования находится в открытом доступе .

Обратите внимание, что чувствительные к безопасности устройства HID (например, устройства FIDO HID, используемые для более строгой аутентификации) также блокируются в Chrome. См. файлы черного списка USB и черного списка HID .

Обратная связь

Команда Chrome будет рада услышать ваши мысли и опыт использования WebHID API.

Расскажите нам о дизайне API

Что-то в API работает не так, как ожидалось? Или вам не хватает методов или свойств, необходимых для реализации вашей идеи?

Сообщите о проблеме спецификации в репозитории WebHID API GitHub или добавьте свои мысли к существующей проблеме.

Сообщить о проблеме с реализацией

Вы нашли ошибку в реализации Chrome? Или реализация отличается от спецификации?

Ознакомьтесь с разделом «Как сообщить об ошибках WebHID» . Обязательно укажите как можно больше подробностей, предоставьте простые инструкции по воспроизведению ошибки и установите для параметра «Компоненты» значение Blink>HID . Glitch отлично подходит для быстрого и простого обмена репродукциями.

Показать поддержку

Планируете ли вы использовать WebHID API? Ваша публичная поддержка помогает команде Chrome расставлять приоритеты в функциях и показывает другим поставщикам браузеров, насколько важно их поддерживать.

Отправьте твит @ChromiumDev, используя хэштег #WebHID , и сообщите нам, где и как вы его используете.

Полезные ссылки

Благодарности

Спасибо Мэтту Рейнольдсу и Джо Медли за рецензии на эту статью. Красно-синяя фотография Nintendo Switch, сделанная Сарой Курфесс , и черно-серебряная фотография ноутбука, сделанная Атулом Сириаком Аджаем на Unsplash.