Cómo hablar con el Control de Stadia mediante WebHID

El Control de Stadia actualizado funciona como un control de juegos estándar, lo que significa que no se puede acceder a todos sus botones con la API de Gamepad. Con WebHID, ahora puedes acceder a los botones faltantes.

Desde que se cerró Stadia, muchos temían que el control terminara siendo una pieza de hardware inútil en el vertedero. Por suerte, el equipo de Stadia decidió abrir el Control de Stadia y proporcionar un firmware personalizado que puedes instalar en el control si vas a la página del modo Bluetooth de Stadia. Esto hace que tu Control de Stadia aparezca como un control de juegos estándar que puedes conectar con un cable USB o de forma inalámbrica a través de Bluetooth. La página de Bluetooth de Stadia, que se muestra con orgullo en la presentación de APIs de Project Fugu, usa WebHID y WebUSB, pero este no es el tema de este artículo. En esta publicación, quiero explicarte cómo puedes comunicarte con el Control de Stadia a través de WebHID.

El Control de Stadia como control de juegos estándar

Después de la escritura, el control aparece como un gamepad estándar para el sistema operativo. En la siguiente captura de pantalla, se muestra una disposición común de los botones y los ejes en un gamepad estándar. Como se define en la especificación de la API de Gamepad, los gamepads estándar tienen botones del 0 al 16, por lo que hay 17 en total (el pad direccional cuenta como cuatro botones). Si pruebas el Control de Stadia en la demostración del verificador de controles de juegos, notarás que funciona de maravilla.

Esquema de un gamepad estándar con los distintos ejes y botones etiquetados.

Sin embargo, si cuentas los botones del control de Stadia, hay 19. Si los pruebas sistemáticamente uno por uno en el verificador de controles de juegos, te darás cuenta de que los botones de Asistente y Capturar no funcionan. Incluso si el atributo buttons del gamepad, como se define en la especificación de Gamepad, es abierto, dado que el Control de Stadia aparece como un gamepad estándar, solo se asignan los botones del 0 al 16. Aun así, puedes usar los otros botones, pero la mayoría de los juegos no esperarán que existan.

WebHID al rescate

Gracias a la API de WebHID, puedes comunicarte con los botones faltantes 17 y 18. Y, si realmente quieres, incluso puedes obtener datos sobre todos los demás botones y ejes que ya están disponibles a través de la API de Gamepad. El primer paso es averiguar cómo se identifica el Control de Stadia ante el sistema operativo. Una forma de hacerlo es abrir la consola de Herramientas para desarrolladores de Chrome en cualquier página aleatoria y solicitar una lista sin filtrar de dispositivos desde la API de WebHID. Luego, elige manualmente el Control de Stadia para una inspección más detallada. Para obtener una lista sin filtrar de dispositivos, solo tienes que pasar un array de opciones filters vacío.

const [device] = await navigator.hid.requestDevice({filters: []});

En el selector, la penúltima entrada se parece al Control de Stadia.

El selector de dispositivos de la API de WebHID muestra algunos dispositivos no relacionados y el Control de Stadia en la penúltima posición.

Después de seleccionar el dispositivo "Control de Stadia rev. A", registra el objeto HIDDevice resultante en la consola. Esto revela el productId (37888, que es 0x9400 en hexadecimal) y el vendorId (6353, que es 0x18d1 en hexadecimal) del Control de Stadia. Si buscas vendorID en la tabla oficial de IDs de proveedores de USB, verás que 6353 se asigna a lo que esperarías: Google Inc..

La consola de Herramientas para desarrolladores de Chrome muestra el resultado del registro del objeto HIDDevice.

Una alternativa al flujo descrito anteriormente es navegar a chrome://device-log/ en la barra de URL, presionar el botón Borrar, conectar el control de Stadia y, luego, presionar Actualizar. Esto te proporciona la misma información.

La interfaz de depuración chrome://device-log muestra información sobre el Control de Stadia conectado.

Otra alternativa es usar la herramienta HID Explorer, que te permite explorar aún más detalles de los dispositivos HID conectados a tu computadora.

Usa estos dos IDs, el vendorId y el productId, para definir mejor lo que se muestra en el selector. Para ello, filtra correctamente el dispositivo WebHID adecuado.

const [stadiaController] = await navigator.hid.requestDevice({filters: [{
  vendorId: 6353,
  productId: 37888,
}]});

Ahora, el ruido de todos los dispositivos no relacionados desapareció y solo aparece el Control de Stadia.

El selector de dispositivos de la API de WebHID que muestra solo el Control de Stadia.

A continuación, abre HIDDevice llamando al método open().

await stadiaController.open();

Vuelve a registrar HIDDevice y la marca opened se establecerá en true.

La consola de Herramientas para desarrolladores de Chrome muestra el resultado del registro del objeto HIDDevice después de abrirlo.

Con el dispositivo abierto, escucha los eventos inputreport entrantes adjuntando un objeto de escucha de eventos.

stadiaController.addEventListener('inputreport', (e) => {
  console.log(e);
});

Cuando presionas y sueltas el botón de Asistente en el control, se registran dos eventos en la consola. Puedes considerarlos como eventos de "botón de Asistente presionado" y "botón de Asistente liberado". Aparte del timeStamp, los dos eventos parecen indistinguibles a primera vista.

La consola de Herramientas para desarrolladores de Chrome muestra los objetos HIDInputReportEvent que se registran.

La propiedad reportId de la interfaz HIDInputReportEvent devuelve el prefijo de identificación de un byte para este informe o 0 si la interfaz HID no usa IDs de informes. En este caso, es 3. El secreto se encuentra en la propiedad data, que se representa como un DataView de tamaño 10. Un DataView proporciona una interfaz de bajo nivel para leer y escribir varios tipos de números en un ArrayBuffer binario. Para obtener algo más fácil de comprender a partir de esta representación, debes crear un Uint8Array a partir del ArrayBuffer, de modo que puedas ver los enteros sin signo de 8 bits individuales.

const data = new Uint8Array(event.data.buffer);

Cuando vuelvas a registrar los datos del evento de informe de entrada, todo comenzará a tener más sentido y los eventos "Botón de Asistente hacia abajo" y "Botón de Asistente hacia arriba" comenzarán a ser descifrables. El primer número entero (8 en ambos eventos) parece estar relacionado con las pulsaciones de botones, y el segundo número entero (2 y 0) parece estar relacionado con si se presiona o no el botón del Asistente.

La consola de Herramientas para desarrolladores de Chrome muestra objetos Uint8Array que se registran para cada HIDInputReportEvent.

Presiona el botón Capturar en lugar del botón Asistente y verás que el segundo número entero cambia de 1 cuando se presiona el botón a 0 cuando se suelta. Esto te permite escribir un "controlador" muy simple que te permite usar los dos botones faltantes.

stadia.addEventListener('inputreport', (event) => {
  if (!e.reportId === 3) {
    return;
  }
  const data = new Uint8Array(event.data.buffer);
  if (data[0] === 8) {
    if (data[1] === 1) {
      hidButtons[1].classList.add('highlight');
    } else if (data[1] === 2) {
      hidButtons[0].classList.add('highlight');
    } else if (data[1] === 3) {
      hidButtons[0].classList.add('highlight');
      hidButtons[1].classList.add('highlight');
    } else {
      hidButtons[0].classList.remove('highlight');
      hidButtons[1].classList.remove('highlight');
    }
  }
});

Con un enfoque de ingeniería inversa como este, puedes, botón por botón y eje por eje, descubrir cómo comunicarte con el Control de Stadia con WebHID. Una vez que le tomes la mano, el resto será un trabajo de asignación de números enteros casi mecánico.

Lo único que falta ahora es la experiencia de conexión fluida que te brinda la API de Gamepad. Si bien, por motivos de seguridad, siempre debes pasar por la experiencia inicial del selector una vez para trabajar con un dispositivo WebHID, como el Control de Stadia, para futuras conexiones, puedes volver a conectarte a dispositivos conocidos. Para ello, llama al método getDevices().

let stadiaController;
const [device] = await navigator.hid.getDevices();
if (device && device.vendorId === 6353 && device.productId === 37888) {
  stadiaController = device;
}

Demostración

Puedes ver el control de Stadia controlado conjuntamente por la API de Gamepad y la API de WebHID en una demostración que creé. Asegúrate de consultar el código fuente, que se basa en los fragmentos de este artículo. Para simplificar, solo muestro los botones A, B, X y Y (controlados por la API de Gamepad), y los botones Asistente y Capturar (controlados por la API de WebHID). Debajo de la imagen del controlador, puedes ver los datos sin procesar de WebHID, de modo que puedes tener una idea de todos los botones y ejes del controlador.

La app de demostración del Control de Stadia muestra los botones A, B, X y Y controlados por la API de Gamepad, y los botones de Asistente y Capturar controlados por la API de WebHID.

Conclusiones

Gracias al nuevo firmware, el Control de Stadia ahora se puede usar como un control de juegos estándar con 17 botones, lo que, en la mayoría de los casos, es más que suficiente para controlar los juegos web comunes. Si, por algún motivo, necesitas datos de los 19 botones del control, WebHID te permite acceder a informes de entrada de bajo nivel que puedes descifrar mediante ingeniería inversa uno por uno. Si escribes un controlador WebHID completo después de leer este artículo, no dudes en comunicarte conmigo y con gusto vincularé tu proyecto aquí. ¡Que disfrutes de WebHID!

Agradecimientos

François Beaufort revisó este artículo.