Cómo hablar con el Control de Stadia mediante WebHID

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

Desde que se dio de baja Stadia, muchos temieron que el control termine como una pieza de hardware inútil en el vertedero. Afortunadamente, el equipo de Stadia decidió abrir el Control de Stadia proporcionando un firmware personalizado que puedes escribir en la memoria flash del control desde la página Modo Bluetooth de Stadia. Esto hace que el Control de Stadia parezca un control de juegos estándar al que puedes conectarte con un cable USB o de forma inalámbrica por Bluetooth. Con orgullo, se muestra en la presentación de la API de Project Fugu. La página de Bluetooth de Stadia usa WebHID y WebUSB, pero este no es el tema de este artículo. En esta publicación, quiero explicar cómo puedes hablar con el Control de Stadia a través de WebHID.

El Control de Stadia como control de juegos estándar

Después de la instalación, el control se muestra al sistema operativo como un control de juegos estándar. En la siguiente captura de pantalla, puedes ver una disposición común de botones y ejes en un control de juegos estándar. Como se definió en la especificación de la API de Gamepad, los controles de juegos 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 para verificadores de controles de juegos, notarás que funciona muy bien.

Esquema de un control de juegos estándar con diferentes ejes y botones etiquetados.

Sin embargo, si cuentas los botones del Control de Stadia, hay 19. Si los pruebas uno por uno de manera sistemática en el control de juegos, verás que los botones Asistente y Capturar no funcionan. Incluso si el atributo buttons del control de juegos, como se define en la especificación de controles de juegos, es abierto, ya que el Control de Stadia aparece como un control de juegos estándar, solo se asignan los botones del 0 al 16. 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 17 y 18 que faltan. Y si lo deseas, 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 informa el Control de Stadia al 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 de dispositivos sin filtros desde la API de WebHID. Luego, elige de forma manual el Control de Stadia para una inspección más detallada. Para obtener una lista sin filtros de dispositivos, simplemente pasa 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 el vendorID en la tabla oficial de IDs del proveedor USB, verás que 6353 se asigna a lo que esperas: Google Inc..

En la consola de Herramientas para desarrolladores de Chrome, se 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 sobre 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 por el dispositivo WebHID correcto.

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

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

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

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

await stadiaController.open();

Vuelve a registrar el HIDDevice, y la marca opened se configurará como 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, adjunta un objeto de escucha de eventos para escuchar los eventos inputreport entrantes.

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

Cuando presionas y sueltas el botón Asistente en el control, se registran dos eventos en la consola. Son eventos de "botón hacia abajo del Asistente" y "botón de botón hacia arriba del Asistente". Aparte de timeStamp, los dos eventos parecen indistinguibles a primera vista.

La consola de Herramientas para desarrolladores de Chrome muestra cómo se registran objetos HIDInputReportEvent.

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

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

Cuando vuelves a registrar los datos de eventos del informe de entrada, las cosas tienen más sentido, y los eventos "Asistente hacia abajo" y los eventos de "botón de Asistente hacia arriba" empiezan a ser descifrados. El primer número entero (8 en ambos eventos) parece estar relacionado con las presiones de botones, y el segundo (2 y 0) parece estar relacionado con si se presiona o no el botón Asistente.

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

Presiona el botón Capture en lugar del botón de Assistant, y verás que el segundo valor entero cambia de 1 cuando se presiona el botón hasta 0 cuando se suelta. Esto te permite escribir un "conductor" muy simple que te permite usar los dos botones que faltan.

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, averiguar cómo comunicarte con el Control de Stadia con WebHID. Una vez que te familiarices, el resto es un trabajo casi mecánico de mapeo de números enteros.

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 realizar la experiencia del selector inicial una vez para trabajar con un dispositivo WebHID como el control de Stadia. Para conexiones futuras, 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 que se controla de forma conjunta con la API de Gamepad y la API de WebHID en una demostración que armé. 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 e Y (controlados por la API de Gamepad), así como los botones Assistant y Capture (controlados por la API de WebHID). Debajo de la imagen del control, puedes ver los datos sin procesar de WebHID, por lo que podrás tener una idea de todos los botones y ejes del control.

La app de demostración en https://stadia-controller-webhid-gamepad.glitch.me/ muestra los botones A, B, X y Y que controla la API de Gamepad, y la API de WebHID controla el Asistente y los botones Capture.

Conclusiones

Gracias al nuevo firmware, el Control de Stadia ahora se puede usar como control de juegos estándar con 17 botones. Esto, en la mayoría de los casos, es más que suficiente para controlar juegos web comunes. Si, por cualquier motivo, necesitas datos de los 19 botones del controlador, 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 completo de WebHID después de leer este artículo, comunícate conmigo y con gusto vincularé tu proyecto aquí. ¡Feliz WebHIDing!

Agradecimientos

François Beaufort revisó este artículo.