Conexión a dispositivos HID poco comunes

La API de WebHID permite que los sitios web accedan a teclados auxiliares alternativos y gamepads exóticos.

François Beaufort
François Beaufort

Existe una gran variedad de dispositivos de interfaz humana (HID), como teclados alternativos o gamepads exóticos, que son demasiado nuevos, demasiado antiguos o demasiado poco comunes para que los controladores de dispositivos de los sistemas puedan acceder a ellos. La API de WebHID resuelve este problema, ya que proporciona una forma de implementar lógica específica del dispositivo en JavaScript.

Casos de uso sugeridos

Un dispositivo HID recibe entrada de personas o les proporciona salida. Algunos ejemplos de dispositivos son teclados, dispositivos apuntadores (mouse, pantallas táctiles, etcétera) y controles de juegos. El protocolo HID permite acceder a estos dispositivos en computadoras de escritorio con controladores del sistema operativo. La plataforma web admite dispositivos HID gracias a estos controladores.

La imposibilidad de acceder a dispositivos HID poco comunes es particularmente dolorosa cuando se trata de teclados auxiliares alternativos (p.ej., Elgato Stream Deck, auriculares Jabra, X-keys) y compatibilidad con gamepads exóticos. Los controles de juegos diseñados para computadoras de escritorio suelen usar HID para las entradas (botones, joysticks, gatillos) y salidas (luces LED, vibración) del control de juegos. Lamentablemente, las entradas y salidas de los controles de juegos no están bien estandarizadas, y los navegadores web suelen requerir lógica personalizada para dispositivos específicos. Esto es insostenible y genera una mala compatibilidad con la cola larga de dispositivos antiguos y poco comunes. También hace que el navegador dependa de peculiaridades en el comportamiento de dispositivos específicos.

Terminología

HID consta de dos conceptos fundamentales: informes y descriptores de informes. Los informes son los datos que se intercambian entre un dispositivo y un cliente de software. El descriptor del informe describe el formato y el significado de los datos que admite el dispositivo.

Un HID (dispositivo de interfaz humana) es un tipo de dispositivo que recibe entrada de humanos o proporciona salida a humanos. También hace referencia al protocolo HID, un estándar para la comunicación bidireccional entre un host y un dispositivo que está diseñado para simplificar el procedimiento de instalación. Originalmente, el protocolo HID se desarrolló para dispositivos USB, pero, desde entonces, se implementó en muchos otros protocolos, incluido Bluetooth.

Las aplicaciones y los dispositivos HID intercambian datos binarios a través de tres tipos de informes:

Tipo de informe Descripción
Informe de entrada Son los datos que se envían desde el dispositivo a la aplicación (p.ej., se presiona un botón).
Informe de salida Son los datos que se envían desde la aplicación al dispositivo (p.ej., una solicitud para encender la luz de fondo del teclado).
Informe de funciones Datos que se pueden enviar en cualquier dirección El formato es específico del dispositivo.

Un descriptor de informe describe el formato binario de los informes que admite el dispositivo. Su estructura es jerárquica y puede agrupar informes como colecciones distintas dentro de la colección de nivel superior. La especificación de HID define el formato del descriptor.

Un uso de HID es un valor numérico que hace referencia a una entrada o salida estandarizada. Los valores de uso permiten que un dispositivo describa el uso previsto del dispositivo y el propósito de cada campo en sus informes. Por ejemplo, se define uno para el botón izquierdo de un mouse. Los usos también se organizan en páginas de uso, que proporcionan una indicación de la categoría de alto nivel del dispositivo o el informe.

Cómo usar la API de WebHID

Detección de características

Para verificar si se admite la API de WebHID, usa el siguiente código:

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

Cómo abrir una conexión HID

La API de WebHID está diseñada para ser asíncrona y evitar que la IU del sitio web se bloquee mientras espera la entrada. Esto es importante porque los datos de HID se pueden recibir en cualquier momento, por lo que se requiere una forma de escucharlos.

Para abrir una conexión HID, primero accede a un objeto HIDDevice. Para ello, puedes solicitarle al usuario que seleccione un dispositivo llamando a navigator.hid.requestDevice() o elegir uno de navigator.hid.getDevices(), que devuelve una lista de dispositivos a los que el sitio web obtuvo acceso anteriormente.

La función navigator.hid.requestDevice() toma un objeto obligatorio que define filtros. Se usan para hacer coincidir cualquier dispositivo conectado con un identificador de proveedor USB (vendorId), un identificador de producto USB (productId), un valor de página de uso (usagePage) y un valor de uso (usage). Puedes obtenerlos del repositorio de ID de USB y del documento de tablas de uso de HID.

Los múltiples objetos HIDDevice que devuelve esta función representan múltiples interfaces HID en el mismo dispositivo físico.

// 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();
Captura de pantalla de un mensaje de dispositivo HID en un sitio web.
Instrucción del usuario para seleccionar un Joy-Con de Nintendo Switch.

También puedes usar la clave exclusionFilters opcional en navigator.hid.requestDevice() para excluir algunos dispositivos del selector de navegador que se sabe que no funcionan correctamente, por ejemplo.

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

Un objeto HIDDevice contiene identificadores de producto y proveedor USB para la identificación del dispositivo. Su atributo collections se inicializa con una descripción jerárquica de los formatos de informes del dispositivo.

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
}

Los dispositivos HIDDevice se devuelven de forma predeterminada en un estado "cerrado" y se deben abrir llamando a open() antes de que se puedan enviar o recibir datos.

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

Recibe informes de entrada

Una vez que se establece la conexión HID, puedes controlar los informes de entrada entrantes escuchando los eventos "inputreport" del dispositivo. Esos eventos contienen los datos de HID como un objeto DataView (data), el dispositivo HID al que pertenece (device) y el ID de informe de 8 bits asociado con el informe de entrada (reportId).

Foto de un Nintendo Switch rojo y azul.
Dispositivos Nintendo Switch Joy-Con.

Continuando con el ejemplo anterior, el siguiente código muestra cómo detectar qué botón presionó el usuario en un dispositivo Joy-Con derecho para que puedas probarlo en casa.

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

Consulta la demostración de Pen webhid-joycon-button.

Enviar informes de salida

Para enviar un informe de salida a un dispositivo HID, pasa el ID de informe de 8 bits asociado con el informe de salida (reportId) y los bytes como un BufferSource (data) a device.sendReport(). La promesa devuelta se resuelve una vez que se envía el informe. Si el dispositivo HID no usa IDs de informes, establece reportId en 0.

En el siguiente ejemplo, se aplica a un dispositivo Joy-Con y se muestra cómo hacerlo vibrar con informes de salida.

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

Consulta la demostración de Pen webhid-joycon-rumble.

Envía y recibe informes de funciones

Los informes de funciones son el único tipo de informes de datos HID que pueden viajar en ambas direcciones. Permiten que los dispositivos y las aplicaciones HID intercambien datos HID no estandarizados. A diferencia de los informes de entrada y salida, la aplicación no recibe ni envía informes de funciones de forma periódica.

Foto de una computadora portátil en blanco y negro.
Teclado de laptop

Para enviar un informe de función a un dispositivo HID, pasa el ID de informe de 8 bits asociado con el informe de función (reportId) y los bytes como un BufferSource (data) a device.sendFeatureReport(). La promesa devuelta se resuelve una vez que se envía el informe. Si el dispositivo HID no usa IDs de informes, establece reportId en 0.

En el siguiente ejemplo, se ilustra el uso de los informes de funciones, ya que se muestra cómo solicitar un dispositivo de luz de fondo del teclado de Apple, abrirlo y hacer que parpadee.

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

Consulta la demostración de Pen webhid-apple-keyboard-backlight.

Para recibir un informe de atributos de un dispositivo HID, pasa el ID de informe de 8 bits asociado con el informe de atributos (reportId) a device.receiveFeatureReport(). La promesa devuelta se resuelve con un objeto DataView que contiene el contenido del informe de funciones. Si el dispositivo HID no usa IDs de informes, establece reportId en 0.

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

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

Escucha la conexión y desconexión

Cuando se le otorga permiso al sitio web para acceder a un dispositivo HID, puede recibir de forma activa eventos de conexión y desconexión escuchando los eventos "connect" y "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.
});

Cómo revocar el acceso a un dispositivo HID

El sitio web puede limpiar los permisos para acceder a un dispositivo HID que ya no le interesa conservar llamando a forget() en la instancia de HIDDevice. Por ejemplo, en el caso de una aplicación web educativa que se usa en una computadora compartida con muchos dispositivos, una gran cantidad de permisos acumulados generados por el usuario crea una experiencia del usuario deficiente.

Si se llama a forget() en una sola instancia de HIDDevice, se revocará el acceso a todas las interfaces HID del mismo dispositivo físico.

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

Como forget() está disponible en Chrome 100 o versiones posteriores, verifica si esta función es compatible con lo siguiente:

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

Sugerencias para desarrolladores

Depurar HID en Chrome es fácil con la página interna about://device-log, en la que puedes ver todos los eventos relacionados con dispositivos HID y USB en un solo lugar.

Captura de pantalla de la página interna para depurar el HID.
Página interna de Chrome para depurar HID.

Consulta el explorador de HID para volcar la información del dispositivo HID en un formato legible. Asigna valores de uso a nombres para cada uso de HID.

En la mayoría de los sistemas Linux, los dispositivos HID se asignan con permisos de solo lectura de forma predeterminada. Para permitir que Chrome abra un dispositivo HID, deberás agregar una nueva regla de udev. Crea un archivo en /etc/udev/rules.d/50-yourdevicename.rules con el siguiente contenido:

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

En la línea anterior, [yourdevicevendor] es 057e si tu dispositivo es un Joy-Con de Nintendo Switch, por ejemplo. También se puede agregar ATTRS{idProduct} para una regla más específica. Asegúrate de que tu user sea miembro del grupo plugdev. Luego, solo vuelve a conectar el dispositivo.

Navegadores compatibles

La API de WebHID está disponible en todas las plataformas para computadoras (ChromeOS, Linux, macOS y Windows) en Chrome 89.

Demostraciones

En web.dev/hid-examples, se incluyen algunas demostraciones de WebHID. ¡Ve a echar un vistazo!

Seguridad y privacidad

Los autores de la especificación diseñaron y, luego, implementaron la API de WebHID según los principios básicos definidos en Controlling Access to Powerful Web Platform Features, incluidos el control del usuario, la transparencia y la ergonomía. La capacidad de usar esta API se limita principalmente a un modelo de permisos que otorga acceso a un solo dispositivo HID a la vez. En respuesta a una instrucción del usuario, este debe seguir pasos activos para seleccionar un dispositivo HID en particular.

Para comprender las compensaciones de seguridad, consulta la sección Consideraciones sobre la seguridad y la privacidad de la especificación de WebHID.

Además, Chrome inspecciona el uso de cada colección de nivel superior y, si una colección de nivel superior tiene un uso protegido (p.ej., teclado o mouse genéricos), un sitio web no podrá enviar ni recibir ningún informe definido en esa colección. La lista completa de usos protegidos está disponible públicamente.

Ten en cuenta que los dispositivos HID sensibles a la seguridad (como los dispositivos HID FIDO que se usan para una autenticación más sólida) también se bloquean en Chrome. Consulta los archivos de bloqueo de USB y de bloqueo de HID.

Comentarios

Al equipo de Chrome le encantaría conocer tus opiniones y experiencias con la API de WebHID.

Cuéntanos sobre el diseño de la API

¿Hay algo en la API que no funciona como se espera? ¿O faltan métodos o propiedades que necesitas para implementar tu idea?

Informa un problema de especificación en el repositorio de GitHub de la API de WebHID o agrega tus comentarios a un problema existente.

Informa un problema con la implementación

¿Encontraste un error en la implementación de Chrome? ¿O la implementación es diferente de las especificaciones?

Consulta Cómo presentar errores de WebHID. Asegúrate de incluir tantos detalles como puedas, proporcionar instrucciones simples para reproducir el error y configurar los Componentes como Blink>HID.

Mostrar apoyo

¿Planeas usar la API de WebHID? Tu apoyo público ayuda al equipo de Chrome a priorizar funciones y muestra a otros proveedores de navegadores lo importante que es admitirlas.

Envía un tweet a @ChromiumDev con el hashtag #WebHID y cuéntanos dónde y cómo lo usas.

Vínculos útiles

Agradecimientos

Gracias a Matt Reynolds y Joe Medley por sus revisiones de este artículo. Foto de Nintendo Switch rojo y azul de Sara Kurfeß, y foto de laptop negra y plateada de Athul Cyriac Ajay en Unsplash.