Acessar dispositivos USB na Web

A API WebUSB torna o USB mais seguro e fácil de usar, trazendo-o para a Web.

François Beaufort
François Beaufort

Se eu disse claramente "USB", é bem provável que você pense em teclados, mouses, áudio, vídeo e dispositivos de armazenamento. Você está mas existem outros tipos de dispositivos USB ali.

Esses dispositivos USB não padronizados exigem que os fornecedores de hardware gravem configurações drivers e SDKs para que você (o desenvolvedor) os aproveite. Infelizmente, esse código específico da plataforma sempre impediu o uso desses dispositivos pela Web. E esse é um dos motivos pelos quais a API WebUSB foi criada: para fornecem uma maneira de expor os serviços de dispositivos USB na Web. Com essa API, o hardware os fabricantes poderão criar SDKs JavaScript multiplataforma para os próprios dispositivos.

Mas, o mais importante, isso tornará o USB mais seguro e fácil de usar, trazendo para a Web.

Vamos conferir o comportamento esperado com a API WebUSB:

  1. Compre um dispositivo USB.
  2. Conecte-o ao computador. Uma notificação aparece imediatamente, com o botão direito para este dispositivo.
  3. Clique na notificação. O site está pronto para ser usado.
  4. Clique para conectar, e um seletor de dispositivo USB será exibido no Chrome, onde você poderá escolha seu dispositivo.

Pronto.

Como seria esse procedimento sem a API WebUSB?

  1. Instale um aplicativo específico da plataforma.
  2. Se meu sistema operacional oferece suporte a ele, verifique se fiz o download a coisa certa.
  3. Instale a coisa. Se você tiver sorte, não vai receber pop-ups ou solicitações assustadoras do SO avisar sobre a instalação de drivers/aplicativos da Internet. Se não tiver sorte, os drivers ou aplicativos instalados apresentarão mau funcionamento e danos seu computador. (Lembre-se, a web é construída para conter problemas sites).
  4. Se você usar o recurso apenas uma vez, o código permanecerá no seu computador até que você ou removê-lo. (Na Web, o espaço para o não utilizado é reclaimed.)

Antes de começar

Este artigo pressupõe que você tenha algum conhecimento básico sobre o funcionamento do USB. Caso contrário, eu recomendamos ler USB em um NutShell. Para mais informações sobre USB, confira as especificações oficiais de USB.

A API WebUSB está disponível no Chrome 61.

Disponível para testes de origem

Para receber o máximo de feedback possível dos desenvolvedores que usam a WebUSB no campo, esse recurso já foi adicionado ao Chrome 54 e Chrome 57 como um teste de origem.

O teste mais recente terminou em setembro de 2017.

Privacidade e segurança

Somente HTTPS

Devido à capacidade desse recurso, ele só funciona em contextos seguros. Isso significa que crie considerando o TLS.

Gesto do usuário necessário

Como precaução de segurança, o navigator.usb.requestDevice() só pode ser chamado por meio de um gesto do usuário, como um toque ou um clique do mouse.

Política de permissões

Uma política de permissões é um mecanismo que permite aos desenvolvedores ativar seletivamente e desativar vários recursos e APIs do navegador. Ela pode ser definida por uma solicitação HTTP cabeçalho e/ou um iframe "allow" .

É possível definir uma política de permissões que controle se o atributo usb é exposto no objeto Navigator ou, em outras palavras, se você permite WebUSB.

Veja abaixo um exemplo de política de cabeçalho em que a WebUSB não é permitida:

Feature-Policy: fullscreen "*"; usb "none"; payment "self" https://payment.example.com

Confira abaixo outro exemplo de uma política de contêiner em que o USB é permitido:

<iframe allowpaymentrequest allow="usb; fullscreen"></iframe>

Vamos começar a programar

A API WebUSB depende muito de promessas (link em inglês) do JavaScript. Se você não conhece com eles, confira este ótimo tutorial de promessas. Mais uma coisa, () => {} são simplesmente funções de seta do ECMAScript 2015.

Acessar dispositivos USB

Você pode solicitar que o usuário selecione um único dispositivo USB conectado usando navigator.usb.requestDevice() ou ligue para navigator.usb.getDevices() e receba uma lista de todos os dispositivos USB conectados ao site.

A função navigator.usb.requestDevice() usa um objeto JavaScript obrigatório que define filters. Esses filtros são usados para fazer a correspondência de qualquer dispositivo USB com o com os identificadores de fornecedor (vendorId) e, opcionalmente, de produto (productId). As chaves classCode, protocolCode, serialNumber e subclassCode podem também podem ser definidas lá.

Captura de tela da solicitação do usuário de dispositivo USB no Chrome
Prompt do usuário de dispositivo USB.

Por exemplo, confira como acessar um dispositivo Arduino conectado configurado. para permitir a origem.

navigator.usb.requestDevice({ filters: [{ vendorId: 0x2341 }] })
.then(device => {
  console.log(device.productName);      // "Arduino Micro"
  console.log(device.manufacturerName); // "Arduino LLC"
})
.catch(error => { console.error(error); });

Antes que você pergunte, eu não criei esse código hexadecimal 0x2341 número Eu só procurei a palavra "Arduino" nesta Lista de IDs de USB.

O USB device retornado na promessa cumprida acima tem algumas configurações básicas, informações importantes sobre o dispositivo, como a versão do USB compatível, tamanho máximo do pacote, fornecedores e IDs de produto, o número de solicitações as configurações que o dispositivo pode ter. Basicamente, ele contém todos os campos na Descritor USB do dispositivo.

// Get all connected USB devices the website has been granted access to.
navigator.usb.getDevices().then(devices => {
  devices.forEach(device => {
    console.log(device.productName);      // "Arduino Micro"
    console.log(device.manufacturerName); // "Arduino LLC"
  });
})

Aliás, se um dispositivo USB anunciar que é compatível com WebUSB, assim como definir um URL da página de destino, o Chrome mostrará uma notificação persistente quando o O dispositivo USB está conectado. Ao clicar nessa notificação, a página de destino será aberta.

Captura de tela da notificação WebUSB no Chrome
Notificação WebUSB.

Falar com uma placa USB Arduino

Agora vamos ver como é fácil se comunicar de um dispositivo compatível com WebUSB Placa Arduino na porta USB. Confira as instruções em https://github.com/webusb/arduino (link em inglês) para ativar seus sketches (em inglês) usando a WebUSB.

Não se preocupe, vamos abordar todos os métodos de dispositivos WebUSB mencionados abaixo neste artigo.

let device;

navigator.usb.requestDevice({ filters: [{ vendorId: 0x2341 }] })
.then(selectedDevice => {
    device = selectedDevice;
    return device.open(); // Begin a session.
  })
.then(() => device.selectConfiguration(1)) // Select configuration #1 for the device.
.then(() => device.claimInterface(2)) // Request exclusive control over interface #2.
.then(() => device.controlTransferOut({
    requestType: 'class',
    recipient: 'interface',
    request: 0x22,
    value: 0x01,
    index: 0x02})) // Ready to receive data
.then(() => device.transferIn(5, 64)) // Waiting for 64 bytes of data from endpoint #5.
.then(result => {
  const decoder = new TextDecoder();
  console.log('Received: ' + decoder.decode(result.data));
})
.catch(error => { console.error(error); });

A biblioteca WebUSB que estou usando está apenas implementando um protocolo de exemplo (com base no protocolo serial USB padrão) e que os fabricantes podem criar qualquer conjunto e tipo de endpoints que quiserem. As transferências de controle são especialmente boas para comandos pequenos de configuração, como eles obtêm prioridade de ônibus e têm uma estrutura bem definida.

Este é o esboço que foi enviado para a placa do Arduino.

// Third-party WebUSB Arduino library
#include <WebUSB.h>

WebUSB WebUSBSerial(1 /* https:// */, "webusb.github.io/arduino/demos");

#define Serial WebUSBSerial

void setup() {
  Serial.begin(9600);
  while (!Serial) {
    ; // Wait for serial port to connect.
  }
  Serial.write("WebUSB FTW!");
  Serial.flush();
}

void loop() {
  // Nothing here for now.
}

A biblioteca WebUSB Arduino de terceiros usada no exemplo de código acima duas coisas:

  • O dispositivo funciona como um dispositivo WebUSB, permitindo que o Chrome leia o URL da página de destino.
  • Ele expõe uma API WebUSB Serial que pode ser usada para substituir a padrão.

Confira o código JavaScript novamente. Quando o device for escolhido pelo usuário, O device.open() executa todas as etapas específicas da plataforma para iniciar uma sessão com o USB dispositivo. Em seguida, tenho que selecionar uma configuração USB disponível com device.selectConfiguration(): Lembre-se de que uma configuração especifica como o dispositivo está ligado, o consumo máximo de energia e o número de interfaces. Falando em interfaces, também preciso solicitar acesso exclusivo com device.claimInterface() porque os dados só podem ser transferidos para uma interface ou terminais associados quando a interface é reivindicada. Por fim, ligando O device.controlTransferOut() é necessário para configurar o dispositivo Arduino com o comandos apropriados para se comunicarem pela API WebUSB Serial.

Em seguida, o device.transferIn() faz uma transferência em massa para a dispositivo para informar que o host está pronto para receber dados em massa. Depois, promessa é atendida com um objeto result contendo uma data DataView que precisam ser analisados adequadamente.

Se você está familiarizado com USB, tudo isso deve parecer bastante familiar.

quero mais

A API WebUSB permite interagir com todos os tipos de endpoints/transferências USB:

  • Transferências CONTROL, usadas para enviar ou receber configurações ou comandos a um dispositivo USB, são processados com controlTransferIn(setup, length) e controlTransferOut(setup, data).
  • As transferências INTERRUPT, usadas para uma quantidade pequena de dados sensíveis ao tempo, são são tratados com os mesmos métodos das transferências em MASSA com transferIn(endpointNumber, length) e transferOut(endpointNumber, data).
  • Transferências ISOCHRONOUS, usadas para fluxos de dados como vídeo e som, são processado com isochronousTransferIn(endpointNumber, packetLengths) e isochronousTransferOut(endpointNumber, data, packetLengths).
  • Transferências em MASSA, usadas para transferir uma grande quantidade de dados não urgentes em de maneira confiável, são processados com transferIn(endpointNumber, length) e transferOut(endpointNumber, data).

Confira também o projeto WebLight de Mike Tsao, que fornece um exemplo simples da construção de um dispositivo LED controlado por USB projetado para a API WebUSB (sem um Arduino). Você encontrará hardware, software, e firmware.

Revogar acesso a um dispositivo USB

O site pode limpar as permissões para acessar um dispositivo USB que não é mais necessário chame forget() na instância de USBDevice. Por exemplo, para um aplicativo da Web educacional usado em um computador compartilhado com vários dispositivos, um o número acumulado de permissões geradas pelo usuário prejudica a experiência dele.

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

Como o forget() está disponível no Chrome 101 ou mais recente, verifique se esse recurso está compatível com:

if ("usb" in navigator && "forget" in USBDevice.prototype) {
  // forget() is supported.
}

Limites no tamanho da transferência

Alguns sistemas operacionais impõem limites quanto à quantidade de dados que podem fazer parte transações USB pendentes. Dividir seus dados em transações menores e apenas enviar alguns por vez ajuda a evitar essas limitações. Isso também reduz a quantidade de memória usada e permite que seu aplicativo relate o progresso à medida que transferências concluídas.

Como várias transferências enviadas para um endpoint sempre são executadas em ordem, é possível melhorar a capacidade de processamento enviando vários blocos na fila para evitar e a latência entre transferências USB. Cada vez que um bloco é totalmente transmitido, notifique seu código de que ele deve fornecer mais dados, conforme documentado no assistente exemplo de função abaixo.

const BULK_TRANSFER_SIZE = 16 * 1024; // 16KB
const MAX_NUMBER_TRANSFERS = 3;

async function sendRawPayload(device, endpointNumber, data) {
  let i = 0;
  let pendingTransfers = [];
  let remainingBytes = data.byteLength;
  while (remainingBytes > 0) {
    const chunk = data.subarray(
      i * BULK_TRANSFER_SIZE,
      (i + 1) * BULK_TRANSFER_SIZE
    );
    // If we've reached max number of transfers, let's wait.
    if (pendingTransfers.length == MAX_NUMBER_TRANSFERS) {
      await pendingTransfers.shift();
    }
    // Submit transfers that will be executed in order.
    pendingTransfers.push(device.transferOut(endpointNumber, chunk));
    remainingBytes -= chunk.byteLength;
    i++;
  }
  // And wait for last remaining transfers to complete.
  await Promise.all(pendingTransfers);
}

Dicas

A depuração USB no Chrome é mais fácil com a página interna about://device-log no qual você poderá ver todos os eventos relacionados a dispositivos USB em um único local.

Captura de tela da página de registro do dispositivo para depurar o WebUSB no Chrome
Página de registro do dispositivo no Chrome para depuração da API WebUSB.

A página interna about://usb-internals também é muito útil e permite que você para simular a conexão e a desconexão de dispositivos WebUSB virtuais. Isso é útil para realizar testes de IU sem hardware real.

Captura de tela da página interna para depurar o WebUSB no Chrome
Página interna no Chrome para depuração da API WebUSB.

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

SUBSYSTEM=="usb", ATTR{idVendor}=="[yourdevicevendor]", MODE="0664", GROUP="plugdev"

em que [yourdevicevendor] é 2341 se o dispositivo for um Arduino, por exemplo. ATTR{idProduct} também podem ser adicionados para uma regra mais específica. Assegure-se de que seus user é um membro do grupo plugdev. Depois, basta reconectar seu dispositivo.

Recursos

Envie um tweet para @ChromiumDev usando a hashtag #WebUSB e informe onde e como você o utiliza.

Agradecimentos

Agradecemos a João Medley por revisar este artigo.