Conversando com o Controle Stadia usando o WebHID

O Controle Stadia atualizado funciona como um gamepad padrão, o que significa que nem todos os botões dele podem ser acessados usando a API Gamepad. Com o WebHID, agora você pode acessar os botões que estão faltando.

Desde que o Stadia foi desligado, muitos temiam que o controle se transformasse em um hardware inútil no aterro sanitário. Felizmente, a equipe do Stadia decidiu abrir o Controle Stadia fornecendo um firmware personalizado que pode ser atualizado no controle acessando a página Modo Bluetooth do Stadia. Isso faz com que o Controle Stadia pareça um gamepad padrão que você pode acessar usando um cabo USB ou uma conexão sem fio via Bluetooth. Com orgulho no Destaques da API Project Fugu, a própria página do Bluetooth do Stadia usa WebHID e WebUSB (links em inglês), mas esse não é o tópico deste artigo. Nesta postagem, quero explicar como falar com o Controle Stadia usando o WebHID.

O Controle Stadia como um gamepad padrão

Após a atualização, o controle aparece como um gamepad padrão para o sistema operacional. Veja a seguir uma captura de tela comum de um botão e um eixo 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, portanto, 17 no total (o botão direcional conta como quatro botões). Se você testar o Controle Stadia na demonstração do testador de gamepad, vai perceber que ele funciona muito bem.

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

No entanto, se você contar os botões no Controle Stadia, há 19. Se você testar cada um deles sistematicamente 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, seja aberto, já que o Controle Stadia aparece como um gamepad padrão, somente os botões de 0 a 16 são mapeados. Você ainda poderá usar os outros botões, mas a maioria dos jogos não vai esperar que eles existam.

WebHID ao resgate

Graças à API WebHID, é possível falar com os botões 17 e 18 que estão faltando. E, se você realmente quiser, pode até mesmo obter dados sobre todos os outros botões e eixos que já estão disponíveis por meio da 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, você escolhe manualmente o Controle Stadia para mais detalhes. Receba uma lista não filtrada de dispositivos simplesmente transmitindo uma matriz de opções filters vazia.

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

No seletor, a penúltima entrada se parece com 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ê procurar o vendorID na tabela oficial de IDs do fornecedor USB, vai descobrir que 6353 é mapeado para o que você espera: 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 Clear, 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 acessar ainda mais detalhes dos dispositivos HID conectados ao computador.

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

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

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

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

A seguir, abra o HIDDevice chamando o método open().

await stadiaController.open();

Registre o HIDDevice novamente, e a sinalização opened será definida como true.

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

Com o dispositivo aberto, anexe um listener de eventos para detectar os eventos inputreport recebidos.

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

Quando você pressiona e solta o botão Assistente no controle, dois eventos são registrados no console. Pense neles como eventos "Botão do Assistente para baixo" e "Botão do Assistente para cima". 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 usa IDs de relatório. Nesse caso, é 3. O secret está na propriedade data, que é representada como um DataView de tamanho 10. Uma DataView fornece uma interface de baixo nível para ler e gravar vários tipos de número em um ArrayBuffer binário. A maneira de conseguir algo mais compreensível nessa representação é criando um Uint8Array fora do ArrayBuffer, para que você possa ver os números inteiros não assinados de 8 bits individuais.

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

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

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

Pressione o botão Capture em vez do Assistente. O segundo número inteiro vai mudar de 1 quando o botão for pressionado para 0 quando ele for solto. Isso permite que você escreva um "driver" muito simples para usar os dois botões que faltam.

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

Com uma abordagem de engenharia reversa como essa, você descobre como falar com o Controle Stadia usando o WebHID, botão por botão e eixo por eixo. Depois de pegar o jeito, o resto é um mapeamento quase mecânico de números inteiros.

O que falta agora é a experiência de conexão suave que a API Gamepad oferece. Embora, por motivos de segurança, você sempre precise passar pela experiência do seletor inicial uma vez para trabalhar com um dispositivo WebHID, como o Controle Stadia, é possível se reconectar a dispositivos conhecidos em conexões futuras. 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 Stadia controlado em conjunto pelas APIs Gamepad e WebHID em uma demonstração que criei. Confira o código-fonte, que se baseia nos snippets deste artigo. Para simplificar, só exibimos os botões A, B, X e Y (controlados pela API Gamepad), além dos botões Assistente e Capturar (controlados pela API WebHID). Abaixo da imagem do controlador, é possível ver os dados brutos do WebHID para que você possa ter uma ideia de todos os botões e eixos do controle.

O app de demonstração em https://stadia-controller-webhid-gamepad.glitch.me/ mostrando os botões A, B, X e Y controlados pela API Gamepad e os botões "Assistente" e "Capturar" controlados pela API WebHID.

Conclusões

Graças ao novo firmware, o Controle Stadia 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 controle, o WebHID permitirá que você tenha acesso a relatórios de entrada de baixo nível, que podem ser decifrados com a engenharia reversa deles, um por um. Se você escrever um driver WebHID completo depois de ler este artigo, entre em contato comigo e eu vou vincular seu projeto aqui. Divirta-se com seu WebHIDing!

Agradecimentos

Este artigo foi revisado por François Beaufort.