Uma introdução ao WebBluetooth

Publicados: 2022-03-10
Resumo rápido ↬ Com Progressive Web Apps, agora você pode usar a web para criar aplicativos completos. Graças a uma enorme quantidade de novas especificações e recursos, podemos fazer coisas com a web para as quais você costumava escrever aplicativos nativos. No entanto, falar com dispositivos de hardware ainda era uma ponte muito longe até agora. Graças ao WebBluetooth, agora podemos construir PWAs que podem controlar suas luzes, dirigir um carro ou até mesmo controlar um drone.

Com os Progressive Web Apps, a web tem se aproximado cada vez mais dos aplicativos nativos. No entanto, com os benefícios adicionais inerentes à web, como privacidade e compatibilidade entre plataformas.

A web tem sido tradicionalmente fantástica para falar com servidores na rede e especificamente com servidores na Internet. Agora que a web está se movendo em direção aos aplicativos, também precisamos dos mesmos recursos que os aplicativos nativos têm.

A quantidade de novas especificações e recursos que foram implementados nos últimos anos em navegadores é impressionante. Temos especificações para lidar com 3D, como WebGL e a próxima WebGPU. Podemos transmitir e gerar áudio, assistir a vídeos e usar a webcam como dispositivo de entrada. Também podemos executar código em velocidades quase nativas usando WebAssembly. Além disso, apesar de inicialmente ser um meio apenas de rede, a web mudou para suporte offline com service workers.

Isso é ótimo e tudo, mas uma área tem sido quase o domínio exclusivo para aplicativos nativos: a comunicação com dispositivos. Esse é um problema que estamos tentando resolver há muito tempo, e é algo que todo mundo provavelmente já encontrou em algum momento. A web é excelente para conversar com servidores, mas não para conversar com dispositivos . Pense, por exemplo, em tentar configurar um roteador em sua rede. É provável que você tenha que inserir um endereço IP e usar uma interface da Web em uma conexão HTTP simples sem qualquer segurança. Isso é apenas uma experiência ruim e segurança ruim. Além disso, como você sabe qual é o endereço IP correto?

Mais depois do salto! Continue lendo abaixo ↓

O HTTP também é o primeiro problema que encontramos quando tentamos criar um Progressive Web App que tenta se comunicar com um dispositivo. Os PWAs são apenas HTTPS e os dispositivos locais são sempre apenas HTTP. Você precisa de um certificado para HTTPS e, para obter um certificado, precisa de um servidor disponível publicamente com um nome de domínio (estou falando de dispositivos em nossa rede local que estão fora de alcance).

Portanto, para muitos dispositivos, você precisa de aplicativos nativos para configurar os dispositivos e usá-los, porque os aplicativos nativos não estão vinculados às limitações da plataforma da Web e podem oferecer uma experiência agradável para seus usuários. No entanto, não quero baixar um aplicativo de 500 MB para fazer isso. Talvez o dispositivo que você possui já tenha alguns anos e o aplicativo nunca tenha sido atualizado para ser executado no seu novo telefone. Talvez você queira usar um computador desktop ou laptop, e o fabricante criou apenas um aplicativo móvel. Também não é uma experiência ideal.

WebBluetooth é uma nova especificação que foi implementada no Chrome e Samsung Internet que nos permite comunicar diretamente com dispositivos Bluetooth Low Energy a partir do navegador. Os Progressive Web Apps em combinação com o WebBluetooth oferecem a segurança e a conveniência de um aplicativo da Web com o poder de falar diretamente com os dispositivos.

O Bluetooth tem um nome muito ruim devido ao alcance limitado, má qualidade de áudio e problemas de emparelhamento. Mas, praticamente todos esses problemas são uma coisa do passado. O Bluetooth Low Energy é uma especificação moderna que pouco tem a ver com as antigas especificações do Bluetooth , além de usar o mesmo espectro de frequência. Mais de 10 milhões de dispositivos são enviados com suporte a Bluetooth todos os dias. Isso inclui computadores e telefones, mas também uma variedade de dispositivos, como monitores de frequência cardíaca e glicose, dispositivos IoT, como lâmpadas e brinquedos, como carros e drones controlados remotamente.

Leitura recomendada : Noções básicas sobre plataformas baseadas em API: um guia para gerentes de produto

A parte teórica chata

Como o Bluetooth em si não é uma tecnologia da web, ele usa algum vocabulário que pode parecer desconhecido para nós. Então, vamos ver como o Bluetooth funciona e algumas das terminologias.

Cada dispositivo Bluetooth é um 'dispositivo central' ou um 'periférico'. Apenas os dispositivos centrais podem iniciar a comunicação e só podem falar com os periféricos. Um exemplo de dispositivo central seria um computador ou um telefone celular.

Um periférico não pode iniciar a comunicação e só pode falar com um dispositivo central. Além disso, um periférico só pode falar com um dispositivo central ao mesmo tempo. Um periférico não pode falar com outro periférico.

um telefone no meio, falando com vários periféricos, como um drone, um brinquedo robô, um monitor de frequência cardíaca e uma lâmpada
Um dispositivo central pode se comunicar com vários periféricos. (Visualização grande)

Um dispositivo central pode falar com vários periféricos ao mesmo tempo e pode retransmitir mensagens se quiser. Portanto, um monitor de frequência cardíaca não pode falar com suas lâmpadas, no entanto, você pode escrever um programa que seja executado em um dispositivo central que receba sua frequência cardíaca e acenda as luzes vermelhas se a frequência cardíaca ultrapassar um determinado limite.

Quando falamos de WebBluetooth, estamos falando de uma parte específica da especificação Bluetooth chamada Generic Attribute Profile, que possui a abreviação muito óbvia GATT. (Aparentemente, o GAP já foi obtido.)

No contexto do GATT, não estamos mais falando de dispositivos centrais e periféricos, mas de clientes e servidores. Suas lâmpadas são servidores. Isso pode parecer contra-intuitivo, mas na verdade faz sentido se você pensar sobre isso. A lâmpada oferece um serviço, ou seja, luz. Assim como quando o navegador se conecta a um servidor na Internet, seu telefone ou computador é um cliente que se conecta ao servidor GATT na lâmpada.

Cada servidor oferece um ou mais serviços. Alguns desses serviços são oficialmente parte do padrão, mas você também pode definir o seu próprio. No caso do monitor de frequência cardíaca, existe um serviço oficial definido na especificação. No caso da lâmpada, não existe, e praticamente todos os fabricantes tentam reinventar a roda. Cada serviço tem uma ou mais características. Cada característica tem um valor que pode ser lido ou escrito. Por enquanto, seria melhor pensar nele como uma matriz de objetos, com cada objeto tendo propriedades que possuem valores.

a hierarquia de serviços e características em comparação com construções mais familiares de JavaScript - um servidor é semelhante a uma matriz de objetos, um serviço a um objeto nessa matriz, uma característica a uma propriedade desse objeto e ambos têm valores
Uma hierarquia simplificada de serviços e características. (Visualização grande)

Ao contrário das propriedades dos objetos, os serviços e as características não são identificados por uma string. Cada serviço e característica tem um UUID único que pode ter 16 ou 128 bits. Oficialmente, o UUID de 16 bits é reservado para padrões oficiais, mas praticamente ninguém segue essa regra. Finalmente, cada valor é uma matriz de bytes. Não há tipos de dados sofisticados no Bluetooth.

Um olhar mais atento a uma lâmpada Bluetooth

Então, vamos olhar para um dispositivo Bluetooth real: um Mipow Playbulb Sphere. Você pode usar um aplicativo como BLE Scanner ou nRF Connect para se conectar ao dispositivo e ver todos os serviços e características. Nesse caso, estou usando o aplicativo BLE Scanner para iOS.

A primeira coisa que você vê quando se conecta à lâmpada é uma lista de serviços. Existem alguns padronizados, como o serviço de informações do dispositivo e o serviço de bateria. Mas também existem alguns serviços personalizados. Estou particularmente interessado no serviço com o UUID de 16 bits de 0xff0f . Se você abrir este serviço, poderá ver uma longa lista de características. Não faço ideia do que a maioria dessas características faz, pois são identificadas apenas por um UUID e porque infelizmente fazem parte de um serviço personalizado; eles não são padronizados e o fabricante não forneceu nenhuma documentação.

A primeira característica com o UUID de 0xfffc parece particularmente interessante. Tem um valor de quatro bytes. Se alterarmos o valor desses bytes de 0x00000000 para 0x00ff0000 , a lâmpada fica vermelha. Alterá-lo para 0x0000ff00 torna a lâmpada verde e 0x000000ff azul. Estas são cores RGB e correspondem exatamente às cores hexadecimais que usamos em HTML e CSS.

O que esse primeiro byte faz? Bem, se mudarmos o valor para 0xff000000 , a lâmpada fica branca. A lâmpada contém quatro LEDs diferentes e, alterando o valor de cada um dos quatro bytes, podemos criar todas as cores que desejarmos.

A API WebBluetooth

É fantástico que possamos usar um aplicativo nativo para mudar a cor de uma lâmpada, mas como fazemos isso no navegador? Acontece que com o conhecimento sobre Bluetooth e GATT que acabamos de aprender, isso é relativamente simples graças à API WebBluetooth. São necessárias apenas algumas linhas de JavaScript para alterar a cor de uma lâmpada.

Vamos examinar a API WebBluetooth.

Conectando a um dispositivo

A primeira coisa que precisamos fazer é conectar do navegador ao dispositivo. Chamamos a função navigator.bluetooth.requestDevice() e fornecemos à função um objeto de configuração. Esse objeto contém informações sobre qual dispositivo queremos usar e quais serviços devem estar disponíveis para nossa API.

No exemplo a seguir, estamos filtrando pelo nome do dispositivo, pois queremos ver apenas dispositivos que contenham o prefixo PLAYBULB no nome. Também estamos especificando 0xff0f como um serviço que queremos usar. Como a função requestDevice() retorna uma promessa, podemos aguardar o resultado.

 let device = await navigator.bluetooth.requestDevice({ filters: [ { namePrefix: 'PLAYBULB' } ], optionalServices: [ 0xff0f ] });

Quando chamamos esta função, aparece uma janela com a lista de dispositivos que estão em conformidade com os filtros que especificamos. Agora temos que selecionar o dispositivo ao qual queremos nos conectar manualmente. Esse é um passo essencial para segurança e privacidade e dá controle ao usuário. O usuário decide se o aplicativo da Web tem permissão para se conectar e, claro, a qual dispositivo ele pode se conectar. O aplicativo da web não pode obter uma lista de dispositivos ou se conectar sem que o usuário selecione manualmente um dispositivo.

o navegador Chrome com a janela que o usuário precisa usar para se conectar a um dispositivo, com a lâmpada visível na lista de dispositivos
O usuário deve se conectar manualmente selecionando um dispositivo. (Visualização grande)

Após obtermos acesso ao dispositivo, podemos nos conectar ao servidor GATT chamando a função connect() na propriedade gatt do dispositivo e aguardar o resultado.

 let server = await device.gatt.connect();

Uma vez que temos o servidor, podemos chamar getPrimaryService() no servidor com o UUID do serviço que queremos usar como parâmetro e aguardar o resultado.

 let service = await server.getPrimaryService(0xff0f);

Em seguida, chame getCharacteristic() no serviço com o UUID da característica como parâmetro e aguarde novamente o resultado.

Agora temos nossas características que podemos usar para escrever e ler dados:

 let characteristic = await service.getCharacteristic(0xfffc);

Gravando dados

Para escrever dados, podemos chamar a função writeValue() na característica com o valor que queremos escrever como ArrayBuffer, que é um método de armazenamento para dados binários. A razão pela qual não podemos usar um array regular é que arrays regulares podem conter dados de vários tipos e podem até ter buracos vazios.

Como não podemos criar ou modificar um ArrayBuffer diretamente, estamos usando um 'array tipado'. Cada elemento de um array tipado é sempre do mesmo tipo e não tem buracos. No nosso caso, vamos usar um Uint8Array , que não tem sinal, portanto não pode conter nenhum número negativo; um inteiro, portanto não pode conter frações; e é de 8 bits e pode conter apenas valores de 0 a 255. Em outras palavras: um array de bytes.

 characteristic.writeValue( new Uint8Array([ 0, r, g, b ]) );

Já sabemos como essa lâmpada em particular funciona. Temos que fornecer quatro bytes, um para cada LED. Cada byte tem um valor entre 0 e 255, e neste caso, queremos usar apenas os LEDs vermelho, verde e azul, então deixamos o LED branco apagado, usando o valor 0.

Lendo dados

Para ler a cor atual da lâmpada, podemos usar a função readValue() e aguardar o resultado.

 let value = await characteristic.readValue(); let r = value.getUint8(1); let g = value.getUint8(2); let b = value.getUint8(3);

O valor que recebemos de volta é um DataView de um ArrayBuffer e oferece uma maneira de obter os dados do ArrayBuffer. No nosso caso, podemos usar a função getUint8() com um índice como parâmetro para extrair os bytes individuais do array.

Como ser notificado de alterações

Por fim, também há uma maneira de ser notificado quando o valor de um dispositivo for alterado. Isso não é realmente útil para uma lâmpada, mas para nosso monitor de frequência cardíaca temos valores em constante mudança e não queremos pesquisar o valor atual manualmente a cada segundo.

 characteristic.addEventListener( 'characteristicvaluechanged', e => { let r = e.target.value.getUint8(1); let g = e.target.value.getUint8(2); let b = e.target.value.getUint8(3); } ); characteristic.startNotifications();

Para obter um retorno de chamada sempre que um valor for alterado, temos que chamar a função addEventListener() na característica com o parâmetro characteristicvaluechanged e uma função de retorno de chamada. Sempre que o valor mudar, a função callback será chamada com um objeto de evento como parâmetro, e podemos obter os dados da propriedade value do destino do evento. E, finalmente, extraia os bytes individuais novamente do DataView do ArrayBuffer.

Como a largura de banda na rede Bluetooth é limitada, temos que iniciar manualmente esse mecanismo de notificação chamando startNotifications() na característica. Caso contrário, a rede será inundada por dados desnecessários. Além disso, como esses dispositivos geralmente usam bateria, cada byte que não precisamos enviar melhorará definitivamente a vida útil da bateria do dispositivo, pois o rádio interno não precisa ser ligado com tanta frequência.

Conclusão

Já ultrapassamos 90% da API WebBluetooth. Com apenas algumas chamadas de função e enviando 4 bytes, você pode criar um aplicativo da web que controla as cores de suas lâmpadas. Se você adicionar mais algumas linhas, poderá até controlar um carro de brinquedo ou pilotar um drone. Com cada vez mais dispositivos Bluetooth chegando ao mercado, as possibilidades são infinitas.

Recursos adicionais

  • Bluetooth.rocks! Demonstrações | (Código fonte no GitHub)
  • “Web Bluetooth Specification,” Web Bluetooth Community Group
  • Open GATT Registry Uma coleção não oficial de documentação para serviços de atributo genérico para dispositivos Bluetooth Low Energy.