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 podem se comunicar com dispositivos Bluetooth próximos de maneira segura e que preserva a privacidade? Dessa forma, monitores de frequência cardíaca, lâmpadas cantores 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 também é compatível com navegadores da Web.

Antes de começar

Este documento pressupõe que você tenha algum conhecimento básico sobre como o Bluetooth Low Energy (BLE) e o perfil de atributo genérico funcionam.

Embora a especificação da API Web Bluetooth ainda não esteja finalizada, os autores da especificação estão procurando ativamente desenvolvedores entusiasmados para testar essa API e dar feedback sobre a especificação e sobre a implementação.

Um subconjunto da API Web Bluetooth está disponível no ChromeOS, no Chrome para Android 6.0, no Mac (Chrome 56) e no Windows 10 (Chrome 70). Isso significa que você precisa ser capaz de solicitar e se conectar a dispositivos Bluetooth de baixa energia próximos, ler/write características do Bluetooth, receber notificações GATT, saber quando um dispositivo Bluetooth é desconectado e até mesmo ler e gravar em descriptores do Bluetooth. Consulte a tabela de compatibilidade com navegadores do MDN para mais informações.

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

Disponível para testes de origem

Para receber o máximo de feedback possível dos desenvolvedores que usam a API Bluetooth da Web, 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 os prós e contras da segurança, recomendo a postagem Modelo de segurança Web Bluetooth 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 poderoso adicionado à Web, ela só é disponibilizada para contextos seguros. Isso significa que você precisa criar pensando na TLS.

Gesto do usuário necessário

Como um 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 os eventos pointerup, click e touchend.

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

Acessar o código

A API Web Bluetooth depende muito das promessas do JavaScript. Se você não tem familiaridade com eles, 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 na função 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 próximos usando navigator.bluetooth.requestDevice, o navegador mostra ao usuário um seletor de dispositivos, em que ele pode escolher um dispositivo ou cancelar a solicitação.

Instrução do usuário para o 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 GATT anunciados pelo Bluetooth e/ou ao nome do dispositivo.

Filtro de serviços

Por exemplo, para solicitar dispositivos Bluetooth que anunciam 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 GATT padronizados para Bluetooth, você poderá fornecer o UUID completo do Bluetooth ou um formato 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. Nesse caso, também será necessário definir a chave optionalServices para acessar os serviços que não estão incluídos em um filtro de serviço. Caso contrário, você vai receber um erro mais tarde ao 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 que estão sendo anunciados com a chave de filtros manufacturerData. Essa chave é uma matriz de objetos com uma chave obrigatória de identificador da empresa Bluetooth chamada companyIdentifier. Você também pode fornecer um prefixo de dados que filtre os dados do fabricante dos dispositivos Bluetooth que começam com ele. Você também precisa definir a chave optionalServices para acessar os serviços que não estão incluídos em um filtro de serviço. 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. Confira a explicação sobre os filtros de dados do Bluetooth para saber mais.

Filtros de exclusão

A opção exclusionFilters em navigator.bluetooth.requestDevice() permite excluir alguns dispositivos do seletor de navegadores. Ele pode ser usado para excluir dispositivos que correspondem a um filtro mais amplo, mas não têm suporte.

// 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); });

Não há filtros

Por fim, em vez de filters, use a chave acceptAllDevices para mostrar todos os dispositivos Bluetooth próximos. Também será necessário definir a chave optionalServices para acessar alguns serviços. Caso contrário, você 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 você faz agora que tem uma BluetoothDevice? Vamos nos conectar ao servidor GATT remoto do Bluetooth, que contém as definições de serviço e característica.

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 obter um serviço GATT primário 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 padronizada do nível da bateria.

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ê usar uma característica GATT Bluetooth personalizada, poderá fornecer o UUID completo do Bluetooth ou uma forma curta de 16 ou 32 bits para service.getCharacteristic.

Também é possível adicionar um listener de evento characteristicvaluechanged a uma característica para processar a leitura do valor dela. Confira o exemplo de mudança de valor da característica de leitura para saber como processar opcionalmente as próximas notificações 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 em uma característica do Bluetooth GATT é tão fácil quanto ler. Dessa vez, vamos usar o ponto de controle da frequência cardíaca para redefinir o valor do campo "Energia gasta" para 0 em um dispositivo de monitor de frequência cardíaca.

Não há mágica aqui. Tudo isso é explicado na página Característica do ponto de controle da 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 ver como receber uma notificação quando a característica Heart Rate Measurement 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 com stopNotifications() e remover corretamente o listener de eventos characteristicvaluechanged adicionado.

Desconectar de um dispositivo Bluetooth

Para oferecer uma melhor experiência do usuário, ouça eventos de desconexão e convide 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 vai acionar os listeners de evento gattserverdisconnected atuais. Ele NÃO vai interromper a comunicação do dispositivo Bluetooth se outro app já estiver se comunicando com ele. Confira o exemplo de desconexão do dispositivo e o exemplo de reconexão automática para saber mais.

Ler e gravar em descritores de Bluetooth

Os descritores GATT do Bluetooth são atributos que descrevem um valor de característica. Eles podem ser lidos e gravados de maneira semelhante às características do Bluetooth GATT.

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

No exemplo abaixo, health_thermometer é o serviço do Health Thermometer, measurement_interval é a característica Intervalo de medição e gatt.characteristic_user_description é o descriptor de descrição do usuário da característica.

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 do intervalo de medição do termômetro de integridade do dispositivo, vamos ver como atualizá-lo e gravar 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); });

Exemplos, demonstrações e codelabs

Todas as amostras de Bluetooth da Web abaixo foram testadas. Para aproveitar ao máximo esses exemplos, recomendamos instalar o [app Android do simulador de periférico 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: extrai informações básicas de um dispositivo BLE.
  • Nível da bateria: extrai informações da bateria de um dispositivo BLE que anuncia informações da bateria.
  • Redefinir energia: redefina a energia gasta de um dispositivo BLE que anuncia a frequência cardíaca.
  • Propriedades da característica: mostra todas as propriedades de uma característica específica de um dispositivo BLE.
  • Notificações: inicie e pare notificações de características de um dispositivo BLE.
  • Dispositivo desconectado: se um dispositivo BLE for desconectado, recebe uma notificação quando se conecta a ele.
  • Get Characteristics: recebe todas as características de um serviço anunciado de um dispositivo BLE.
  • Get Descriptors: recebe todos os descritores de 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 corresponda aos dados do fabricante.
  • Filtros de exclusão: extrai 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 do Web Bluetooth e os codelabs oficiais do Web Bluetooth.

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 no noble, o módulo central BLE mais usado do Node.js. Isso permite que você use o webpack/browserify noble sem precisar de um servidor WebSocket ou outros plug-ins.
  • angular-web-bluetooth é um módulo do Angular que abstrai todo o boilerplate necessário para configurar a API Web Bluetooth.

Ferramentas

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

Dicas

A página Bluetooth Internals está disponível no Chrome em about://bluetooth-internals para que você possa inspecionar tudo sobre os 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 no Chrome para depuração de dispositivos Bluetooth.

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

A seguir

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

Embora ainda não esteja completo, confira uma prévia do que esperar no futuro:

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

Mostrar suporte à API

Você planeja usar a API Web Bluetooth? Seu apoio público ajuda a equipe do Chrome a priorizar recursos e mostra a outros fornecedores de navegadores a importância de oferecer suporte a eles.

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

Recursos

Agradecimentos

Agradecemos a Kayce Basques por revisar este artigo. Imagem principal da SparkFun Electronics, de Boulder, EUA.