Conversando com o Controle Stadia usando o WebHID

O controle Stadia com flash funciona como um gamepad padrão, o que significa que nem todos os botões podem ser acessados usando a API Gamepad. Com a WebHID, agora é possível acessar os botões que não estavam aparecendo.

Desde que o Stadia foi desativado, muitos temiam que o controle acabasse como um hardware inútil em um aterro sanitário. Felizmente, a equipe do Stadia decidiu abrir o Controle Stadia, fornecendo um firmware personalizado que pode ser instalado no controle acessando a página do modo Bluetooth do Stadia. Assim, o Controle Stadia aparece como um gamepad padrão que pode ser conectado por cabo USB ou sem fio por Bluetooth. Destaque na vitrine de APIs do Projeto Fugu, a página do Bluetooth do Stadia usa WebHID e WebUSB, mas esse não é o assunto deste artigo. Nesta postagem, quero explicar como você pode conversar com o Controle Stadia usando a WebHID.

O Controle Stadia como um gamepad padrão

Depois da atualização, o controlador aparece como um gamepad padrão para o sistema operacional. Confira a captura de tela a seguir para ver um botão comum e um arranjo de eixos em um gamepad padrão. Conforme definido na especificação da API Gamepad, os gamepads padrão têm botões de 0 a 16, totalizando 17 (o d-pad conta como quatro botões). Se você testar o controle do Stadia na demonstração do testador de gamepad, vai perceber que ele funciona perfeitamente.

Um esquema de um gamepad padrão com os vários eixos e botões rotulados.

No entanto, se você contar os botões do Controle Stadia, vai encontrar 19. Se você testar um por um no testador de gamepad, vai perceber que os botões Assistente e Capturar não funcionam. Mesmo que o atributo buttons do gamepad, conforme definido na especificação do Gamepad, seja aberto, como o controle do Stadia aparece como um gamepad padrão, apenas os botões de 0 a 16 são mapeados. Você ainda pode usar os outros botões, mas a maioria dos jogos não espera que eles existam.

WebHID ao resgate

Graças à API WebHID, você pode conversar com os botões 17 e 18 ausentes. E, se quiser, você pode até receber dados sobre todos os outros botões e eixos já disponíveis na API Gamepad. A primeira etapa é descobrir como o Controle Stadia se reporta ao sistema operacional. Uma maneira de fazer isso é abrir o console do Chrome DevTools em qualquer página aleatória e solicitar uma lista não filtrada de dispositivos da API WebHID. Em seguida, escolha manualmente o Controle Stadia para mais inspeções. Para receber uma lista sem filtros, basta transmitir uma matriz de opções filters vazia.

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

No seletor, a penúltima entrada parece o Controle Stadia.

O seletor de dispositivos da API WebHID mostrando alguns dispositivos não relacionados e o Controle Stadia na penúltima posição.

Depois de selecionar o dispositivo "Stadia Controller rev. A", registre o objeto HIDDevice resultante no console. Isso revela o productId (37888, que é 0x9400 em hexadecimal) e o vendorId (6353, que é 0x18d1 em hexadecimal) do Controle Stadia. Se você pesquisar o vendorID na tabela oficial de IDs de fornecedor USB, vai descobrir que 6353 mapeia para o que você esperaria: Google Inc..

Console do Chrome DevTools mostrando a saída do registro do objeto HIDDevice.

Uma alternativa ao fluxo descrito acima é navegar até chrome://device-log/ na barra de URL, pressionar o botão Limpar, conectar o Controle Stadia e pressionar Atualizar. Isso fornece as mesmas informações.

A interface de depuração chrome://device-log mostrando informações sobre o Controle Stadia conectado.

Outra alternativa é usar a ferramenta HID Explorer, que permite conferir ainda mais detalhes dos dispositivos HID conectados ao computador.

Use esses dois IDs, o vendorId e o productId, para refinar o que é mostrado no seletor, filtrando corretamente o dispositivo WebHID certo.

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

Agora o ruído de todos os dispositivos não relacionados desapareceu, e apenas o Controle Stadia aparece.

O seletor de dispositivos da API WebHID mostrando apenas o Controle Stadia.

Em seguida, abra o HIDDevice chamando o método open().

await stadiaController.open();

Faça login novamente, e a flag opened será definida como true.HIDDevice

O console do Chrome DevTools mostrando a saída do registro do objeto HIDDevice depois de aberto.

Com o dispositivo aberto, ouça os eventos inputreport recebidos anexando um listener de eventos.

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

Quando você pressiona e solta o botão Assistente no controlador, dois eventos são registrados no console. Pense neles como eventos "botão do Assistente pressionado" e "botão do Assistente solto". Além do timeStamp, os dois eventos parecem indistinguíveis à primeira vista.

O console do Chrome DevTools mostrando objetos HIDInputReportEvent sendo registrados.

A propriedade reportId da interface HIDInputReportEvent retorna o prefixo de identificação de um byte para esse relatório ou 0 se a interface HID não usar IDs de relatório. Neste caso, é 3. O segredo está na propriedade data, que é representada como um DataView de tamanho 10. Um DataView fornece uma interface de baixo nível para leitura e gravação de vários tipos de números em um ArrayBuffer binário. Para ter algo mais fácil de entender dessa representação, crie um Uint8Array com base no ArrayBuffer para ver os números inteiros não assinados de 8 bits individuais.

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

Quando você registra os dados do evento de relatório de entrada novamente, as coisas começam a fazer mais sentido, e os eventos "Botão Assistente pressionado" e "Botão Assistente solto" começam a ficar decifráveis. O primeiro número inteiro (8 nos dois eventos) parece estar relacionado a toques de botão, e o segundo (2 e 0) parece estar relacionado ao fato de o botão do Assistente ter sido pressionado ou não.

O console do Chrome DevTools mostrando objetos Uint8Array sendo registrados para cada HIDInputReportEvent.

Pressione o botão Capturar em vez do botão Assistente. Você vai notar que o segundo número inteiro alterna de 1 quando o botão é pressionado para 0 quando ele é solto. Isso permite que você escreva um "driver" muito simples que possibilita o uso dos dois botões ausentes.

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

Usando uma abordagem de engenharia reversa como essa, você pode, botão por botão e eixo por eixo, descobrir como se comunicar com o Controle Stadia usando o WebHID. Depois de pegar o jeito, o restante é quase um trabalho mecânico de mapeamento de números inteiros.

O que está faltando agora é a experiência de conexão tranquila que a API Gamepad oferece. Por motivos de segurança, você sempre precisa passar pela experiência inicial do seletor uma vez para trabalhar com um dispositivo WebHID, como o Controle Stadia. No entanto, para conexões futuras, é possível se reconectar a dispositivos conhecidos. Para fazer isso, chame o método getDevices().

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

Demonstração

Você pode conferir o controle do Stadia controlado em conjunto pela API Gamepad e pela API WebHID em uma demonstração que eu criei. Confira o código-fonte, que se baseia nos snippets deste artigo. Para simplificar, só mostro os botões A, B, X e Y (controlados pela API Gamepad) e os botões Assistente e Capturar (controlados pela API WebHID). Abaixo da imagem do controle, você pode ver os dados brutos do WebHID para ter uma ideia de todos os botões e eixos do controle.

O app de demonstração do controle Stadia mostrando os botões A, B, X e Y sendo controlados pela API Gamepad, e os botões Assistente e Capturar sendo controlados pela API WebHID.

Conclusões

Graças ao novo firmware, o Controle Stadia agora pode ser usado como um gamepad padrão com 17 botões, o que, na maioria dos casos, é mais do que suficiente para controlar jogos comuns da Web. Se, por qualquer motivo, você precisar de dados de todos os 19 botões do controlador, a WebHID permitirá o acesso a relatórios de entrada de baixo nível que podem ser decifrados por engenharia reversa, um por um. Se você escrever um driver WebHID completo depois de ler este artigo, entre em contato comigo. Vou adorar vincular seu projeto aqui. Aproveite o WebHID!

Agradecimentos

Este artigo foi revisado por François Beaufort.