Cómo hablar con el Control de Stadia mediante WebHID

El Control de Stadia con firmware actúa 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 como un 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 actualizar en el control. Para ello, ve a la página Modo Bluetooth de Stadia. Esto hace que el Control de Stadia aparezca como un control de juegos estándar al que puedes conectarte con un cable USB o de forma inalámbrica a través de Bluetooth. La página de Bluetooth de Stadia, que se presenta con orgullo en la demostración de la API 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 actualización, el control aparece como un control de juego estándar para el sistema operativo. Consulta la siguiente captura de pantalla para ver una disposición común de botones y ejes en un mando de juegos estándar. Como se define en la especificación de la API de Gamepad, los gamepads estándar tienen botones del 0 al 16, es decir, 17 en total (el mando de dirección 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.

Un esquema de un gamepad estándar con los diversos ejes y botones etiquetados.

Sin embargo, si cuentas los botones del Control de Stadia, hay 19. Si los pruebas de forma sistemática uno por uno en el probador de controles de juegos, te darás cuenta de que los botones Asistente y Captura no funcionan. Incluso si el atributo buttons del control de juegos, como se define en la especificación del control de juegos, es abierto, como el Control de Stadia aparece como un control de juegos estándar, solo se asignan los botones del 0 al 16. Puedes seguir usando 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 filtrar de la API de WebHID. Luego, eliges manualmente el Control de Stadia para una inspección más detallada. Para obtener una lista de dispositivos sin filtrar, simplemente pasa un array de opciones filters vacío.

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

En el selector, la penúltima entrada se ve como el 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 "Stadia Controller 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..

Consola de Chrome DevTools que 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, vendorId y productId, para definir mejor lo que se muestra en el selector filtrando correctamente el dispositivo WebHID correcto.

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 solo muestra el Control de Stadia

A continuación, llama al método open() para abrir HIDDevice.

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, adjunta un objeto de escucha de eventos para detectar eventos inputreport entrantes.

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

Cuando presionas y sueltas el botón Asistente del control, se registran dos eventos en la consola. Puedes considerarlos como eventos de "botón presionado" y "botón levantado" de Asistente. Aparte de 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 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 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. La forma de obtener algo más comprensible de esta representación es crear un Uint8Array a partir del ArrayBuffer, de modo que puedas ver los enteros individuales de 8 bits sin signo.

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

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

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

Presiona el botón Captura 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 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 descubrir cómo comunicarte con el Control de Stadia con WebHID, botón por botón y eje por eje. Una vez que le agarres el ritmo, el resto es un trabajo casi mecánico de asignación 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 pasar por la experiencia inicial del selector para trabajar con un dispositivo WebHID, como el Control de Stadia, en 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 controlado conjuntamente por la API de Gamepad y la API de WebHID en una demo que creé. Asegúrate de revisar 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 Assistant y Capture (controlados por la API de WebHID). Debajo de la imagen del controlador, puedes ver los datos sin procesar de WebHID para que puedas tener una idea de todos los botones y ejes del controlador.

La app de demostración en https://stadia-controller-webhid-gamepad.glitch.me/ en la que se muestran los botones A, B, X e Y controlados por la API de Gamepad, y los botones Asistente y Captura 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, que, en la mayoría de los casos, es más que suficiente para controlar juegos web comunes. Si, por algún motivo, necesitas datos de los 19 botones del controlador, WebHID te permite obtener acceso 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, asegúrate de comunicarte conmigo y con gusto vincularé tu proyecto aquí. ¡Disfruta de WebHID!

Agradecimientos

François Beaufort revisó este artículo.