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

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

Франсуа Бофор
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

Обнаружение особенностей

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

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 и таблиц использования 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 Switch.
Устройства 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]}.`);
});

Посмотрите демо-ролик Pen webhid-joycon-button .

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

Чтобы отправить отчёт о выходе на 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));

Посмотрите демо-версию Pen webhid-joycon-rumble .

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

Отчёты о функциях — единственный тип отчётов 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);
}

Посмотрите демонстрацию Pen webhid-apple-keyboard-backlight .

Чтобы получить отчёт о функции от 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 проверяет использование каждой коллекции верхнего уровня, и если в коллекции верхнего уровня есть защищённое использование (например, стандартная клавиатура, мышь), то веб-сайт не сможет отправлять и получать какие-либо отчёты, определённые в этой коллекции. Полный список защищённых использований доступен в открытом доступе .

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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