Conversando com o Controle Stadia usando o WebHID

O controle Stadia piscando funciona como um gamepad padrão, o que significa que nem todos os botões são acessíveis usando a API Gamepad. Com o WebHID, agora é possível acessar os botões ausentes.

Desde que o Stadia foi desativado, muitos temiam que o controle se tornasse 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 instalado no controle. Acesse a página Modo Bluetooth do Stadia para fazer isso. Isso faz com que o Controle Stadia apareça como um gamepad padrão que pode ser conectado por cabo USB ou sem fio por Bluetooth. A página do Bluetooth do Stadia, apresentada no Showcase de APIs do Project Fugu, usa WebHID e WebUSB, mas esse não é o tema deste artigo. Neste post, vou explicar como você pode se comunicar com o controle do 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. Confira a captura de tela a seguir para ver uma disposição comum de botões e 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, ou seja, 17 no total (o direcional conta como quatro botões). Se você testar o controle do Stadia na demonstração do Gamepad Tester, vai perceber que ele funciona muito bem.

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

No entanto, se você contar os botões no Controle Stadia, são 19. Se você tentar sistematicamente um por um no testador de gamepad, vai perceber que os botões Assistente e Captura 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 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, é possível interagir com os botões 17 e 18 ausentes. E se você quiser, é possível até mesmo receber dados sobre todos os outros botões e eixos que já estão disponíveis na API Gamepad. A primeira etapa é descobrir como o controle do Stadia se informa 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 uma inspeção mais detalhada. Para receber uma lista não filtrada de dispositivos, basta transmitir 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 do 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 de fornecedores USB, vai descobrir que o 6353 é mapeado 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 vai fornecer 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, vendorId e 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();

Registre o HIDDevice novamente, e a flag opened será definida como true.

O console do Chrome DevTools mostrando a saída do registro do objeto HIDDevice após a abertura.

Com o dispositivo aberto, detecte eventos inputreport de entrada anexando um listener de eventos.

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. Eles podem ser considerados eventos de "botão Google Assistente pressionado" e "botão Google Assistente levantado". 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. Nesse 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 ler e gravar vários tipos de números em um ArrayBuffer binário. Para tornar essa representação mais fácil de digerir, crie um Uint8Array do ArrayBuffer para ver os números inteiros não assinados de 8 bits.

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

Quando você registra os dados de eventos do relatório de entrada novamente, as coisas começam a fazer mais sentido e os eventos "Botão do Google Assistente pressionado" e "Botão do Google Assistente pressionado" começam a ser decifrados. O primeiro número inteiro (8 nos dois eventos) parece estar relacionado a pressionamentos de botão, e o segundo número inteiro (2 e 0) parece estar relacionado ao botão Google Assistente pressionado ou não.

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

Pressione o botão Capture em vez do botão Assistente. O segundo número inteiro muda de 1 quando o botão é pressionado para 0 quando ele é liberado. Isso permite que você escreva um "driver" muito simples que permite usar os 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 descobrir como se comunicar com o controle do Stadia com o WebHID, botão por botão e eixo por eixo. Depois de entender o conceito, o resto é um trabalho de mapeamento de inteiros quase mecânico.

O que falta agora é a experiência de conexão tranquila que a API Gamepad oferece. Por motivos de segurança, você precisa passar pela experiência inicial do seletor 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 criei. Confira o código-fonte, que é baseado nos snippets deste artigo. Para simplificar, mostro apenas os botões A, B, X e Y (controlados pela API Gamepad) e os botões Assistente e Captura (controlados pela API WebHID). Abaixo da imagem do controle, você pode conferir os dados brutos do WebHID para 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/ mostra os botões A, B, X e Y sendo controlados pela API Gamepad e os botões do Assistente e de captura 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 da Web comuns. Se, por algum motivo, você precisar de dados de todos os 19 botões do controlador, o WebHID permite acessar 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 e vou vincular seu projeto aqui. Boas-vindas ao WebHID!

Agradecimentos

Este artigo foi revisado por François Beaufort.