Como se conectar a dispositivos HID incomuns

A API WebHID permite que os sites acessem teclados auxiliares alternativos e gamepads exóticos.

François Beaufort
François Beaufort

Há uma cauda longa de dispositivos de interface humana (HIDs, na sigla em inglês), como teclados alternativos ou gamepads exóticos, que são novos, muito antigos ou muito incomuns para serem acessíveis pelos drivers de dispositivos dos sistemas. A API WebHID resolve isso fornecendo uma maneira de implementar a lógica específica do dispositivo em JavaScript.

Casos de uso sugeridos

Um dispositivo HID recebe entradas ou gera saídas para humanos. Exemplos de dispositivos incluem teclados, dispositivos apontadores (mouse, touchscreen etc.) e gamepads. O protocolo HID permite acessar esses dispositivos em computadores desktop usando drivers do sistema operacional. A plataforma da Web oferece suporte a dispositivos HID usando esses drivers.

A incapacidade de acessar dispositivos HID incomuns é particularmente difícil quando se trata de teclados auxiliares alternativos (por exemplo, Elgato Stream Deck, headsets Jabra, X-keys) e suporte a gamepad exóticos. Gamepads projetados para computador geralmente usam o HID para entradas (botões, joysticks, gatilhos) e saídas (LEDs, sombreamento). Infelizmente, as entradas e saídas do gamepad não são bem padronizados, e os navegadores da Web geralmente exigem lógica personalizada para dispositivos específicos. Isso é insustentável e resulta em suporte ruim para a cauda longa de dispositivos mais antigos e incomuns. Isso também faz com que o navegador dependa de peculiaridades no comportamento de dispositivos específicos.

Terminologia

O HID consiste em dois conceitos fundamentais: relatórios e descritores de relatórios. Os relatórios são os dados trocados entre um dispositivo e um cliente de software. O descritor de relatório descreve o formato e o significado dos dados com suporte do dispositivo.

Um dispositivo de interface humana (HID, na sigla em inglês) é um tipo de dispositivo que recebe entradas ou envia saídas para humanos. Ele também se refere ao protocolo HID, um padrão de comunicação bidirecional entre um host e um dispositivo projetado para simplificar o procedimento de instalação. O protocolo HID foi originalmente desenvolvido para dispositivos USB, mas desde então foi implementado em muitos outros protocolos, incluindo Bluetooth.

Aplicativos e dispositivos HID trocam dados binários por meio de três tipos de relatório:

Tipo de relatório Descrição
Relatório de entrada Dados enviados do dispositivo para o aplicativo (por exemplo, quando um botão é pressionado).
Relatório de saída Dados enviados do aplicativo para o dispositivo (por exemplo, uma solicitação para ativar a luz de fundo do teclado).
Relatório de recursos Dados que podem ser enviados em qualquer direção. O formato é específico do dispositivo.

Um descritor de relatório descreve o formato binário dos relatórios compatíveis com o dispositivo. A estrutura dele é hierárquica e pode agrupar relatórios como coleções distintas na coleção de nível superior. O formato do descritor é definido pela especificação HID.

Um uso de HID é um valor numérico que se refere a uma entrada ou saída padronizada. Os valores de uso permitem que um dispositivo descreva o uso pretendido e a finalidade de cada campo nos relatórios. Por exemplo, um é definido para o botão esquerdo de um mouse. Os usos também são organizados em páginas de uso, que fornecem uma indicação da categoria de alto nível do dispositivo ou relatório.

Como usar a API WebHID

Detecção de recursos

Para verificar se a API WebHID é compatível, use:

if ("hid" in navigator) {
  // The WebHID API is supported.
}

Abrir uma conexão HID

A API WebHID é assíncrona por padrão para evitar que a interface do site seja bloqueada enquanto aguarda entrada. Isso é importante porque os dados HID podem ser recebidos a qualquer momento, exigindo uma maneira de ouvi-los.

Para abrir uma conexão HID, primeiro acesse um objeto HIDDevice. Para isso, é possível solicitar que o usuário selecione um dispositivo chamando navigator.hid.requestDevice() ou escolher um em navigator.hid.getDevices(), que retorna uma lista de dispositivos a que o site recebeu acesso anteriormente.

A função navigator.hid.requestDevice() usa um objeto obrigatório que define filtros. Eles são usados para corresponder a qualquer dispositivo conectado a um identificador de fornecedor USB (vendorId), um identificador de produto USB (productId), um valor de página de uso (usagePage) e um valor de uso (usage). Você pode encontrá-los no Repositório de IDs USB e no documento das tabelas de uso HID.

Os vários objetos HIDDevice retornados por essa função representam várias interfaces HID no mesmo dispositivo físico.

// Filter on devices with the Nintendo Switch Joy-Con USB Vendor/Product IDs.
const filters = [
  {
    vendorId: 0x057e, // Nintendo Co., Ltd
    productId: 0x2006 // Joy-Con Left
  },
  {
    vendorId: 0x057e, // Nintendo Co., Ltd
    productId: 0x2007 // Joy-Con Right
  }
];

// Prompt user to select a Joy-Con device.
const [device] = await navigator.hid.requestDevice({ filters });
// Get all devices the user has previously granted the website access to.
const devices = await navigator.hid.getDevices();
Captura de tela de um comando de dispositivo HID em um site.
Solicitação do usuário para selecionar um Joy-Con do Nintendo Switch.

Você também pode usar a chave exclusionFilters opcional em navigator.hid.requestDevice() para excluir do seletor do navegador alguns dispositivos que não funcionam corretamente, por exemplo.

// Request access to a device with vendor ID 0xABCD. The device must also have
// a collection with usage page Consumer (0x000C) and usage ID Consumer
// Control (0x0001). The device with product ID 0x1234 is malfunctioning.
const [device] = await navigator.hid.requestDevice({
  filters: [{ vendorId: 0xabcd, usagePage: 0x000c, usage: 0x0001 }],
  exclusionFilters: [{ vendorId: 0xabcd, productId: 0x1234 }],
});

Um objeto HIDDevice contém identificadores de fornecedor e produto USB para identificação do dispositivo. O atributo collections é inicializado com uma descrição hierárquica dos formatos de relatório do dispositivo.

for (let collection of device.collections) {
  // An HID collection includes usage, usage page, reports, and subcollections.
  console.log(`Usage: ${collection.usage}`);
  console.log(`Usage page: ${collection.usagePage}`);

  for (let inputReport of collection.inputReports) {
    console.log(`Input report: ${inputReport.reportId}`);
    // Loop through inputReport.items
  }

  for (let outputReport of collection.outputReports) {
    console.log(`Output report: ${outputReport.reportId}`);
    // Loop through outputReport.items
  }

  for (let featureReport of collection.featureReports) {
    console.log(`Feature report: ${featureReport.reportId}`);
    // Loop through featureReport.items
  }

  // Loop through subcollections with collection.children
}

Por padrão, os dispositivos HIDDevice são retornados em um estado "fechado" e precisam ser abertos chamando open() antes que os dados possam ser enviados ou recebidos.

// Wait for the HID connection to open before sending/receiving data.
await device.open();

Receber relatórios de entrada

Depois que a conexão HID for estabelecida, você vai poder processar os relatórios de entrada detectando os eventos "inputreport" do dispositivo. Esses eventos contêm os dados HID como um objeto DataView (data), o dispositivo HID a que ele pertence (device) e o ID do relatório de 8 bits associado ao relatório de entrada (reportId).

Foto vermelha e azul do Nintendo Switch.
Dispositivos Nintendo Switch Joy-Con.

Continuando com o exemplo anterior, o código abaixo mostra como detectar qual botão o usuário pressionou em um dispositivo Joy-Con à direita para testar em casa.

device.addEventListener("inputreport", event => {
  const { data, device, reportId } = event;

  // Handle only the Joy-Con Right device and a specific report ID.
  if (device.productId !== 0x2007 && reportId !== 0x3f) return;

  const value = data.getUint8(0);
  if (value === 0) return;

  const someButtons = { 1: "A", 2: "X", 4: "B", 8: "Y" };
  console.log(`User pressed button ${someButtons[value]}.`);
});

Enviar relatórios de saída

Para enviar um relatório de saída a um dispositivo HID, transmita o ID do relatório de 8 bits associado ao relatório de saída (reportId) e bytes como BufferSource (data) para device.sendReport(). A promessa retornada é resolvida quando o relatório é enviado. Se o dispositivo HID não usa IDs de relatórios, defina reportId como 0.

O exemplo abaixo se aplica a um dispositivo Joy-Con e mostra como usá-lo com relatórios de saída.

// First, send a command to enable vibration.
// Magical bytes come from https://github.com/mzyy94/joycon-toolweb
const enableVibrationData = [1, 0, 1, 64, 64, 0, 1, 64, 64, 0x48, 0x01];
await device.sendReport(0x01, new Uint8Array(enableVibrationData));

// Then, send a command to make the Joy-Con device rumble.
// Actual bytes are available in the sample below.
const rumbleData = [ /* ... */ ];
await device.sendReport(0x10, new Uint8Array(rumbleData));

Enviar e receber relatórios de recursos

Os relatórios de recursos são o único tipo de relatório de dados HID que pode viajar nas duas direções. Elas permitem que dispositivos e aplicativos HID troquem dados HID não padronizados. Ao contrário dos relatórios de entrada e saída, os relatórios de recursos não são recebidos ou enviados pelo aplicativo regularmente.

Foto de laptop preto e prateado.
Teclado de laptop

Para enviar um relatório de recursos a um dispositivo HID, transmita o ID do relatório de 8 bits associado ao relatório de recursos (reportId) e bytes como BufferSource (data) para device.sendFeatureReport(). A promessa retornada é resolvida quando o relatório é enviado. Se o dispositivo HID não usa IDs de relatórios, defina reportId como 0.

O exemplo abaixo ilustra o uso de relatórios de recursos mostrando como solicitar um dispositivo com luz de fundo do teclado da Apple, abri-lo e fazer com que ele pisque.

const waitFor = duration => new Promise(r => setTimeout(r, duration));

// Prompt user to select an Apple Keyboard Backlight device.
const [device] = await navigator.hid.requestDevice({
  filters: [{ vendorId: 0x05ac, usage: 0x0f, usagePage: 0xff00 }]
});

// Wait for the HID connection to open.
await device.open();

// Blink!
const reportId = 1;
for (let i = 0; i < 10; i++) {
  // Turn off
  await device.sendFeatureReport(reportId, Uint32Array.from([0, 0]));
  await waitFor(100);
  // Turn on
  await device.sendFeatureReport(reportId, Uint32Array.from([512, 0]));
  await waitFor(100);
}

Para receber um relatório de recurso de um dispositivo HID, transmita o ID do relatório de 8 bits associado ao relatório de recursos (reportId) para device.receiveFeatureReport(). A promessa retornada é resolvida com um objeto DataView que tem o conteúdo do relatório do recurso. Se o dispositivo HID não usa IDs de relatórios, defina reportId como 0.

// Request feature report.
const dataView = await device.receiveFeatureReport(/* reportId= */ 1);

// Read feature report contents with dataView.getInt8(), getUint8(), etc...

Ouvir conexão e desconexão

Quando o site recebe permissão para acessar um dispositivo HID, ele pode receber ativamente eventos de conexão e desconexão detectando eventos "connect" e "disconnect".

navigator.hid.addEventListener("connect", event => {
  // Automatically open event.device or warn user a device is available.
});

navigator.hid.addEventListener("disconnect", event => {
  // Remove |event.device| from the UI.
});

Revogar acesso a um dispositivo HID

O site pode limpar as permissões para acessar um dispositivo HID que ele não está mais interessado em reter, chamando forget() na instância HIDDevice. Por exemplo, para um aplicativo da Web educacional usado em um computador compartilhado com muitos dispositivos, um grande número de permissões acumuladas geradas pelo usuário cria uma experiência insatisfatória.

Chamar forget() em uma única instância de HIDDevice revogará o acesso a todas as interfaces HID no mesmo dispositivo físico.

// Voluntarily revoke access to this HID device.
await device.forget();

Como forget() está disponível no Chrome 100 ou mais recente, confira se esse recurso tem suporte ao seguinte:

if ("hid" in navigator && "forget" in HIDDevice.prototype) {
  // forget() is supported.
}

Dicas para desenvolvedores

A depuração de HID no Chrome é fácil com a página interna, about://device-log, onde você pode ver todos os eventos relacionados a dispositivos HID e USB em um único lugar.

Captura de tela da página interna para depurar o HID.
Página interna no Chrome para depurar o HID.

Confira o explorador HID para despejar informações do dispositivo HID em um formato legível. Ela mapeia de valores de uso para nomes para cada uso do HID.

Na maioria dos sistemas Linux, os dispositivos HID são mapeados com permissões somente leitura por padrão. Para permitir que o Chrome abra um dispositivo HID, é necessário adicionar uma nova regra udev. Crie um arquivo em /etc/udev/rules.d/50-yourdevicename.rules com o seguinte conteúdo:

KERNEL=="hidraw*", ATTRS{idVendor}=="[yourdevicevendor]", MODE="0664", GROUP="plugdev"

Na linha acima, [yourdevicevendor] será 057e se o dispositivo for um Nintendo Switch Joy-Con, por exemplo. Também é possível adicionar ATTRS{idProduct} para uma regra mais específica. Verifique se user é um membro do grupo plugdev. Depois, é só reconectar o dispositivo.

Suporte ao navegador

A API WebHID está disponível em todas as plataformas de computador (ChromeOS, Linux, macOS e Windows) no Chrome 89.

Demonstrações

Algumas demonstrações do WebHID estão listadas em web.dev/hid-examples. Confira!

Segurança e privacidade

Os autores das especificações projetaram e implementaram a API WebHID usando os princípios básicos definidos em Como controlar o acesso a recursos avançados da plataforma Web, incluindo controle do usuário, transparência e ergonomia. A capacidade de usar essa API é controlada principalmente por um modelo de permissão que concede acesso a apenas um dispositivo HID por vez. Em resposta a uma solicitação, o usuário precisa seguir etapas ativas para selecionar um dispositivo HID específico.

Para entender as compensações de segurança, confira a seção Considerações de segurança e privacidade da especificação WebHID.

Além disso, o Chrome inspeciona o uso de cada coleção de nível superior e, se uma coleção de nível superior tiver uso protegido (por exemplo, teclado genérico, mouse), o site não poderá enviar nem receber relatórios definidos nessa coleção. A lista completa de usos protegidos está disponível publicamente.

Os dispositivos HID sensíveis à segurança (como os FIDO HID usados para autenticação mais forte) também estão bloqueados no Chrome. Consulte os arquivos da lista de bloqueio USB e da lista de bloqueio HID.

Feedback

A equipe do Chrome adoraria saber sua opinião e saber mais sobre sua experiência com a API WebHID.

Fale sobre o design da API

Há algo na API que não funciona como esperado? Ou há métodos ou propriedades ausentes que você precisa para implementar sua ideia?

Registre um problema de especificação no repositório da API WebHID do GitHub (link em inglês) ou adicione suas ideias a um problema atual.

Informar um problema com a implementação

Você encontrou um bug na implementação do Chrome? Ou a implementação é diferente da especificação?

Confira Como registrar bugs do WebHID. Inclua o máximo de detalhes possível, forneça instruções simples para reproduzir o bug e defina Componentes como Blink>HID. O Glitch funciona muito bem para compartilhar réplicas rápidas e fáceis.

Mostrar apoio

Você planeja usar a API WebHID? Seu suporte público ajuda a equipe do Chrome a priorizar recursos e mostra aos outros fornecedores de navegador a importância do suporte a eles.

Envie um tweet para @ChromiumDev usando a hashtag #WebHID e informe onde e como você está usando a hashtag.

Links úteis

Agradecimentos

Agradecemos a Matt Reynolds e Joe Medley pelas análises deste artigo. Foto do Nintendo Switch em vermelho e azul, feita por Sara Kurfeß, e foto do laptop em preto e prata de Athul Cyriac Ajay no Unsplash.