Como se comunicar com dispositivos Bluetooth por JavaScript

A API Web Bluetooth permite que sites se comuniquem com dispositivos Bluetooth.

François Beaufort
François Beaufort

E se eu dissesse que os sites poderiam se comunicar com dispositivos Bluetooth próximos de maneira segura e preservando a privacidade? Dessa forma, monitores de frequência cardíaca, lâmpadas cantando e até tartarugas podem interagir diretamente com um site.

Até agora, a capacidade de interagir com dispositivos Bluetooth era possível apenas para apps específicos da plataforma. A API Web Bluetooth tem como objetivo mudar isso e a levar para os navegadores da Web também.

Antes de começar

Neste documento, presumimos que você tenha alguns conhecimentos básicos de como o Bluetooth de baixa energia (BLE) e o Perfil de atributo genérico funcionam.

Mesmo que a especificação da API Web Bluetooth ainda não tenha sido finalizada, os autores de especificações estão procurando ativamente desenvolvedores entusiasmados para testar essa API e comentem sobre a especificação e seu feedback sobre a implementação.

Um subconjunto da API Web Bluetooth está disponível no ChromeOS, no Chrome para Android 6.0, Mac (Chrome 56) e Windows 10 (Chrome 70). Isso significa que você vai conseguir solicitar e se conectar a dispositivos Bluetooth de baixa energia por perto, ler/gravar características do Bluetooth, receber notificações GATT, saber quando um dispositivo Bluetooth for desconectado e até ler e gravar nos descritores Bluetooth. Consulte a tabela Compatibilidade de navegadores do MDN para mais informações.

Para Linux e versões anteriores do Windows, ative a flag #experimental-web-platform-features em about://flags.

Disponível para testes de origem

Para receber o máximo de feedback possível de desenvolvedores que usam a API Web Bluetooth em campo, o Chrome adicionou esse recurso no Chrome 53 como um teste de origem para ChromeOS, Android e Mac.

O teste terminou em janeiro de 2017.

Requisitos de segurança

Para entender as vantagens e desvantagens de segurança, recomendo a postagem Modelo de segurança Bluetooth da Web, de Jeffrey Yasskin, engenheiro de software da equipe do Chrome, que trabalha na especificação da API Web Bluetooth.

Somente HTTPS

Como essa API experimental é um novo recurso avançado adicionado à Web, ela é disponibilizada apenas para contextos seguros. Isso significa que você precisa criar com o TLS em mente.

É necessário um gesto do usuário

Como recurso de segurança, a descoberta de dispositivos Bluetooth com navigator.bluetooth.requestDevice precisa ser acionada por um gesto do usuário, como um toque ou um clique do mouse. Estamos falando sobre ouvir eventos pointerup, click e touchend.

button.addEventListener('pointerup', function(event) {
  // Call navigator.bluetooth.requestDevice
});

Entrar no código

A API Web Bluetooth depende muito de promessas de JavaScript. Se você não estiver familiarizado com elas, confira este ótimo tutorial de promessas. Mais uma coisa, () => {} são funções de seta do ECMAScript 2015.

Solicitar dispositivos Bluetooth

Essa versão da especificação da API Web Bluetooth permite que sites em execução no papel Central se conectem a servidores GATT remotos por uma conexão BLE. Ele oferece suporte à comunicação entre dispositivos que implementam o Bluetooth 4.0 ou mais recente.

Quando um site solicita acesso a dispositivos por perto usando navigator.bluetooth.requestDevice, o navegador exibe ao usuário um seletor de dispositivo em que ele pode escolher um dispositivo ou cancelar a solicitação.

Solicitação do usuário de dispositivo Bluetooth.

A função navigator.bluetooth.requestDevice() usa um objeto obrigatório que define filtros. Esses filtros são usados para retornar apenas dispositivos que correspondem a alguns serviços Bluetooth GATT anunciados e/ou ao nome do dispositivo.

Filtro de serviços

Por exemplo, para solicitar a publicidade de dispositivos Bluetooth sobre o Serviço de bateria Bluetooth GATT:

navigator.bluetooth.requestDevice({ filters: [{ services: ['battery_service'] }] })
.then(device => { /* … */ })
.catch(error => { console.error(error); });

No entanto, se o serviço Bluetooth GATT não estiver na lista de serviços Bluetooth GATT padronizados, você poderá fornecer o UUID do Bluetooth completo ou um formulário curto de 16 ou 32 bits.

navigator.bluetooth.requestDevice({
  filters: [{
    services: [0x1234, 0x12345678, '99999999-0000-1000-8000-00805f9b34fb']
  }]
})
.then(device => { /* … */ })
.catch(error => { console.error(error); });

Filtro de nome

Também é possível solicitar dispositivos Bluetooth com base no nome do dispositivo anunciado com a chave de filtros name ou até mesmo um prefixo desse nome com a chave de filtros namePrefix. Observe que, nesse caso, você também precisará definir a chave optionalServices para acessar serviços não incluídos em um filtro de serviço. Caso contrário, você vai receber um erro mais tarde, quando tentar acessá-los.

navigator.bluetooth.requestDevice({
  filters: [{
    name: 'Francois robot'
  }],
  optionalServices: ['battery_service'] // Required to access service later.
})
.then(device => { /* … */ })
.catch(error => { console.error(error); });

Filtro de dados do fabricante

Também é possível solicitar dispositivos Bluetooth com base nos dados específicos do fabricante anunciados com a chave de filtros manufacturerData. Essa chave é um conjunto de objetos com uma chave obrigatória de identificador Bluetooth da empresa chamada companyIdentifier. Você também pode fornecer um prefixo de dados que filtra os dados do fabricante de dispositivos Bluetooth que começam com ele. Também é necessário definir a chave optionalServices para acessar serviços não incluídos em um filtro. Caso contrário, você vai receber um erro mais tarde ao tentar acessá-los.

// Filter Bluetooth devices from Google company with manufacturer data bytes
// that start with [0x01, 0x02].
navigator.bluetooth.requestDevice({
  filters: [{
    manufacturerData: [{
      companyIdentifier: 0x00e0,
      dataPrefix: new Uint8Array([0x01, 0x02])
    }]
  }],
  optionalServices: ['battery_service'] // Required to access service later.
})
.then(device => { /* … */ })
.catch(error => { console.error(error); });

Uma máscara também pode ser usada com um prefixo de dados para corresponder a alguns padrões nos dados do fabricante. Veja a explicação sobre filtros de dados Bluetooth para saber mais.

Filtros de exclusão

A opção exclusionFilters no navigator.bluetooth.requestDevice() permite excluir alguns dispositivos do seletor do navegador. Ele pode ser usado para excluir dispositivos que correspondem a um filtro mais amplo, mas não são compatíveis.

// Request access to a bluetooth device whose name starts with "Created by".
// The device named "Created by Francois" has been reported as unsupported.
navigator.bluetooth.requestDevice({
  filters: [{
    namePrefix: "Created by"
  }],
  exclusionFilters: [{
    name: "Created by Francois"
  }],
  optionalServices: ['battery_service'] // Required to access service later.
})
.then(device => { /* … */ })
.catch(error => { console.error(error); });

Sem filtros

Por fim, em vez de filters, você pode usar a tecla acceptAllDevices para mostrar todos os dispositivos Bluetooth por perto. Também será necessário definir a chave optionalServices para acessar alguns serviços. Caso contrário, você vai receber um erro mais tarde ao tentar acessá-los.

navigator.bluetooth.requestDevice({
  acceptAllDevices: true,
  optionalServices: ['battery_service'] // Required to access service later.
})
.then(device => { /* … */ })
.catch(error => { console.error(error); });

Conectar-se a um dispositivo Bluetooth

O que fazer agora que você tem uma BluetoothDevice? Vamos nos conectar ao servidor GATT remoto do Bluetooth que contém as definições de serviço e características.

navigator.bluetooth.requestDevice({ filters: [{ services: ['battery_service'] }] })
.then(device => {
  // Human-readable name of the device.
  console.log(device.name);

  // Attempts to connect to remote GATT Server.
  return device.gatt.connect();
})
.then(server => { /* … */ })
.catch(error => { console.error(error); });

Ler uma característica do Bluetooth

Aqui, nos conectamos ao servidor GATT do dispositivo Bluetooth remoto. Agora, queremos receber um serviço GATT principal e ler uma característica que pertence a esse serviço. Vamos tentar, por exemplo, ler o nível de carga atual da bateria do dispositivo.

No exemplo a seguir, battery_level é a característica do nível de bateria padronizado.

navigator.bluetooth.requestDevice({ filters: [{ services: ['battery_service'] }] })
.then(device => device.gatt.connect())
.then(server => {
  // Getting Battery Service…
  return server.getPrimaryService('battery_service');
})
.then(service => {
  // Getting Battery Level Characteristic…
  return service.getCharacteristic('battery_level');
})
.then(characteristic => {
  // Reading Battery Level…
  return characteristic.readValue();
})
.then(value => {
  console.log(`Battery percentage is ${value.getUint8(0)}`);
})
.catch(error => { console.error(error); });

Se você usa uma característica personalizada do Bluetooth GATT, pode fornecer o UUID do Bluetooth completo ou um formulário curto de 16 ou 32 bits para service.getCharacteristic.

Também é possível adicionar um listener de eventos characteristicvaluechanged a uma característica para processar a leitura do valor dela. Confira o Exemplo de alteração de valor de característica de leitura para saber como também processar as próximas notificações do GATT.

…
.then(characteristic => {
  // Set up event listener for when characteristic value changes.
  characteristic.addEventListener('characteristicvaluechanged',
                                  handleBatteryLevelChanged);
  // Reading Battery Level…
  return characteristic.readValue();
})
.catch(error => { console.error(error); });

function handleBatteryLevelChanged(event) {
  const batteryLevel = event.target.value.getUint8(0);
  console.log('Battery percentage is ' + batteryLevel);
}

Gravar em uma característica do Bluetooth

Gravar uma característica do Bluetooth GATT é tão fácil quanto ler. Desta vez, vamos usar o Ponto de Controle de Frequência Cardíaca para redefinir o valor do campo Energia gasta como 0 em um dispositivo de monitoramento de frequência cardíaca.

Eu prometo que não tem mágica. Tudo isso está explicado na página Característica do ponto de controle de frequência cardíaca.

navigator.bluetooth.requestDevice({ filters: [{ services: ['heart_rate'] }] })
.then(device => device.gatt.connect())
.then(server => server.getPrimaryService('heart_rate'))
.then(service => service.getCharacteristic('heart_rate_control_point'))
.then(characteristic => {
  // Writing 1 is the signal to reset energy expended.
  const resetEnergyExpended = Uint8Array.of(1);
  return characteristic.writeValue(resetEnergyExpended);
})
.then(_ => {
  console.log('Energy expended has been reset.');
})
.catch(error => { console.error(error); });

Receber notificações GATT

Agora, vamos conferir como receber uma notificação quando a característica da Medição de frequência cardíaca mudar no dispositivo:

navigator.bluetooth.requestDevice({ filters: [{ services: ['heart_rate'] }] })
.then(device => device.gatt.connect())
.then(server => server.getPrimaryService('heart_rate'))
.then(service => service.getCharacteristic('heart_rate_measurement'))
.then(characteristic => characteristic.startNotifications())
.then(characteristic => {
  characteristic.addEventListener('characteristicvaluechanged',
                                  handleCharacteristicValueChanged);
  console.log('Notifications have been started.');
})
.catch(error => { console.error(error); });

function handleCharacteristicValueChanged(event) {
  const value = event.target.value;
  console.log('Received ' + value);
  // TODO: Parse Heart Rate Measurement value.
  // See https://github.com/WebBluetoothCG/demos/blob/gh-pages/heart-rate-sensor/heartRateSensor.js
}

O Exemplo de notificações mostra como interromper as notificações usando stopNotifications() e remover corretamente o listener de eventos characteristicvaluechanged adicionado.

Desconectar-se de um dispositivo Bluetooth

Para oferecer uma melhor experiência do usuário, você pode detectar eventos de desconexão e convidar o usuário a se reconectar:

navigator.bluetooth.requestDevice({ filters: [{ name: 'Francois robot' }] })
.then(device => {
  // Set up event listener for when device gets disconnected.
  device.addEventListener('gattserverdisconnected', onDisconnected);

  // Attempts to connect to remote GATT Server.
  return device.gatt.connect();
})
.then(server => { /* … */ })
.catch(error => { console.error(error); });

function onDisconnected(event) {
  const device = event.target;
  console.log(`Device ${device.name} is disconnected.`);
}

Você também pode chamar device.gatt.disconnect() para desconectar seu app da Web do dispositivo Bluetooth. Isso acionará os listeners de eventos gattserverdisconnected existentes. Observe que a comunicação do dispositivo Bluetooth NÃO será interrompida se outro app já estiver se comunicando com o dispositivo Bluetooth. Confira os exemplos de desconexão de dispositivos e de reconexão automática (links em inglês) para mais detalhes.

Ler e gravar em descritores Bluetooth

Os descritores Bluetooth GATT são atributos que descrevem um valor de característica. Você pode ler e gravar esses dados de maneira semelhante às características do Bluetooth GATT.

Vamos conferir, por exemplo, como ler a descrição do usuário sobre o intervalo de medição do termômetro de integridade do dispositivo.

No exemplo abaixo, health_thermometer é o serviço Termômetro de integridade, measurement_interval a característica do intervalo de medição e gatt.characteristic_user_description é o descritor de descrição de características do usuário.

navigator.bluetooth.requestDevice({ filters: [{ services: ['health_thermometer'] }] })
.then(device => device.gatt.connect())
.then(server => server.getPrimaryService('health_thermometer'))
.then(service => service.getCharacteristic('measurement_interval'))
.then(characteristic => characteristic.getDescriptor('gatt.characteristic_user_description'))
.then(descriptor => descriptor.readValue())
.then(value => {
  const decoder = new TextDecoder('utf-8');
  console.log(`User Description: ${decoder.decode(value)}`);
})
.catch(error => { console.error(error); });

Agora que lemos a descrição do usuário sobre o intervalo de medição do termômetro de saúde do dispositivo, vamos aprender a atualizá-lo e a criar um valor personalizado.

navigator.bluetooth.requestDevice({ filters: [{ services: ['health_thermometer'] }] })
.then(device => device.gatt.connect())
.then(server => server.getPrimaryService('health_thermometer'))
.then(service => service.getCharacteristic('measurement_interval'))
.then(characteristic => characteristic.getDescriptor('gatt.characteristic_user_description'))
.then(descriptor => {
  const encoder = new TextEncoder('utf-8');
  const userDescription = encoder.encode('Defines the time between measurements.');
  return descriptor.writeValue(userDescription);
})
.catch(error => { console.error(error); });

Amostras, demonstrações e codelabs

Todos os exemplos de Web Bluetooth abaixo foram testados. Para aproveitar essas amostras ao máximo, recomendamos que você instale o [app Android do simulador de periféricos BLE], que simula um periférico BLE com um serviço de bateria, um serviço de frequência cardíaca ou um serviço de termômetro de saúde.

Iniciante

  • Informações do dispositivo: recupere informações básicas de um dispositivo BLE.
  • Battery Level: recupere informações da bateria de um dispositivo BLE que anuncia informações da bateria.
  • Reset Energy: redefine a energia gasta de um dispositivo BLE que anuncia a frequência cardíaca.
  • Characteristic Properties: exibe todas as propriedades de uma característica específica de um dispositivo BLE.
  • Notificações: iniciar e parar notificações características de um dispositivo BLE.
  • Desconexão de dispositivo: desconecte um dispositivo BLE e receba uma notificação caso ele se conecte a ele.
  • Ver características: confira todas as características de um serviço anunciado com um dispositivo BLE.
  • Acessar descritores: receba os descritores de todas as características de um serviço anunciado de um dispositivo BLE.
  • Filtro de dados do fabricante: recupere informações básicas de um dispositivo BLE que corresponde aos dados do fabricante.
  • Filtros de exclusão: recupere informações básicas de um dispositivo BLE com filtros de exclusão básicos.

Combinar várias operações

Confira também nossas demonstrações selecionadas da Web Bluetooth e os codelabs oficiais de Bluetooth da Web.

Bibliotecas

  • web-bluetooth-utils é um módulo npm que adiciona algumas funções de conveniência à API.
  • Um paliativo da API Web Bluetooth está disponível em noble, o módulo central de BLE do Node.js mais conhecido. Isso permite webpack/browserify sem a necessidade de um servidor WebSocket ou outros plug-ins.
  • O angular-web-bluetooth é um módulo do Angular que abstrai todo o padrão necessário para configurar a API Web Bluetooth.

Ferramentas

  • Começar a usar o Web Bluetooth é um app da Web simples que gera todo o código boilerplate do JavaScript para começar a interagir com um dispositivo Bluetooth. Insira um nome de dispositivo, um serviço ou uma característica, defina as propriedades e pronto.
  • Se você já for um desenvolvedor de Bluetooth, o plug-in do Studio para Bluetooth da Web também vai gerar o código JavaScript da Web Bluetooth para seu dispositivo Bluetooth.

Dicas

Uma página Bluetooth Internals está disponível no Chrome em about://bluetooth-internals para que você possa inspecionar tudo sobre dispositivos Bluetooth próximos: status, serviços, características e descritores.

Captura de tela da página interna para depurar o Bluetooth no Chrome
Página interna do Chrome para depuração de dispositivos Bluetooth.

Também recomendamos consultar a página oficial Como informar bugs do Bluetooth na Web, porque às vezes a depuração do Bluetooth pode ser difícil.

A seguir

Verifique primeiro o status de implementação do navegador e da plataforma para saber quais partes da API Web Bluetooth estão sendo implementadas.

Embora ele ainda não esteja completo, confira uma prévia do que esperar em um futuro próximo:

  • A procuração de anúncios de BLE por perto acontecerá com navigator.bluetooth.requestLEScan().
  • Um novo evento serviceadded vai rastrear os Serviços GATT Bluetooth recém-descobertos, enquanto o evento serviceremoved rastreará os removidos. Um novo evento servicechanged será disparado quando qualquer característica e/ou descritor for adicionado ou removido de um serviço Bluetooth GATT.

Mostrar suporte à API

Você planeja usar a API Web Bluetooth? Seu suporte público ajuda a equipe do Chrome a priorizar recursos e mostra a outros fornecedores de navegador como é fundamental oferecer suporte a eles.

Envie um tweet para @ChromiumDev usando a hashtag #WebBluetooth e conte para nós onde e como você está usando a hashtag.

Recursos

Agradecimentos

Agradecemos a Kayce Basques pela análise deste artigo. Imagem principal da SparkFun Electronics, Boulder, EUA.