Bluetooth Low Energy com WEB (JavaScript)

Resultado de imagem para ble logo

O conteúdo deste post é baseado em uma nota que eu criei no Evernote enquanto estudava este assunto para continuar o desenvolvimento do meu TCC.
Esta nota foi criada com o intuito de armazenar conhecimento a respeito da utilização de ferramentas em JavaScript para comunicação com dispositivos Bluetooth, focando principalmente no acelerômetro desenvolvido pela HEDRO, que será usado no meu TCC.

Acrônimos e definições:

  • BLE -> Bluetooth Low Energy. Protocolo criado visando diminuir o gasto energético de dispositivos, para tornar viável a Internet Das Coisas (IOT).
  • GATT -> Generic Attribute Profile. Perfil genérico de atributo se refere a uma camada do protocolo BLE e será abordada em detalhes no texto.
  • TLS -> Transport Layer Security. Camada de segurança utilizada para transferência de dados pela WEB.
  • XMLHttpRequest -> É um objeto capaz de buscar dados de um servidor. Além disso é capaz de atualizar uma página web sem a necessidade de recarregar a página, requisitar dados de um servidor depois que a página foi carregada, receber dados de um servidor depois que a página foi carregada, enviar dados para um servidor no background.


    Segundo [1], a possibilidade de interagir com equipamentos através do Bluetooth usando um browser, como o Google Chrome por exemplo, foi liberada para todos os usuários em julho de 2015. Antes dessa data, só era possível interagir com estes equipamentos em aplicativos nativos.
    O artigo da referência [1] supõe que o leitor já tenha um conhecimento prévio sobre GATT (Generic Attribute Profile), então, buscando este conhecimento, chegou-se na referência [2]
    O GATT é uma camada do protocolo BLE (Bluetooth Low Energy) que é usada pela aplicação para comunicação de dados entre dois dispositivos conectados. Os dados são enviados e armazenados em forma de características que são registradas na memória de um dispositivo BLE. Do ponto de vista do GATT, quando dois dispositivos estão conectados então estes estão em um destes papéis:

  • O servidor GATT: O dispositivo contendo o banco de dados com as características que estão sendo lidas ou escritas por um cliente GATT.
  • O cliente GATT: O dispositivo que está lendo ou escrevendo dados de, ou para, um servidor GATT.

    A figura abaixo ilustra estes papéis, sendo neste caso o dispositivo periférico (CC2640R2) o servidor GATT, enquanto que o dispositivo central (que é o smartphone) é o cliente GATT.


Segurança (Apenas HTTP):

    Como este recurso é muito poderoso, o time de desenvolvimento do Google quis garantir que o mesmo só funcionaria em situações seguras. Isso significa que precisaremos criar nossas aplicações com TLS em mente.
    O protocolo TLS garante autorização, privacidade e integridade dos dados entre duas aplicações computacionais se comunicando. É o protocolo de segurança mais utilizado atualmente nos navegadores da WEB e em outras aplicações que necessitam que os dados sejam trocados seguramente em uma rede, como em seções de navegação pela WEB, transferência de arquivos, conexões VPN, sessões remotas em desktops e VoIP (Voice over IP).
    Durante o desenvolvimento será possível interagir com o Bluetooth pela WEB através do http://localhost, mas para tornar o código utilizável em um site, será necessário ter o HTTPS habilitado no servidor.
    Para adicionar o HTTPS ao servidor será necessário um certificado TLS. Segundo o artigo [1], é possível conseguir um certificado HTTPS gratuito no site:  https://letsencrypt.org/.
    Como uma ferramenta a mais para a segurança do usuário, para descobrir dispositivo Bluetooth com o comando navigator.bluetooth.requestDevice o usuário deve interagir com a aplicação de alguma forma, como por exemplo um clique de mouse. Exemplo de código para adicionar um evento que acionará a busca pelo dispositivo:

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

Mãos no código:

    A API de Bluetooth WEB está profundamente ancorada no conceito de Promises do JavaScript.

Promessas no JavaScript:

    O que são essas promessas do JavaScript? Segundo a referência [5], como o JavaScript usa uma única thread, duas partes de um script não podem ser executadas ao mesmo tempo, sendo necessário fazer em sequência. Anteriormente nos scripts em JavaScript, eram utilizadas funções específicas para determinar se uma ação ocorreu ou não, como por exemplo o carregamento de uma imagem no site, caso este tivesse ocorrido sem problema seria percorrida uma sequência de códigos, enquanto que caso ocorresse erro outras medidas seriam tomadas.
    Uma promessa pode ser:

  • fulfilled: a ação relacionada à promessa foi concluída.
  • rejected: ocorreu uma falha na ação relacionada à promessa.
  • pending: a ação ainda não foi realizada ou rejeitada.
  • settled: a ação foi concluída ou rejeitada.

    Sintaxe de uma promessa:

var promise = new Promise(function(resolve, reject) {
    // do a thing, possibly async, then...

    if (/* everything turned out fine */) {
        resolve("Stuff worked!");
    } else {
        reject(Error("It broke"));
    }
});

    O construtor de promessas aceita um argumento, que é um callback com dois parâmetros, "resolve" e "reject". Faça algo dentro do callback, talvez algo assíncrono, e chame "resolve" se tudo tiver funcionado bem. Caso contrário, chame "reject".
    Assim como o throw no JavaScript simples, é comum, mas não obrigatório, chamar "reject" com um objeto Error. A vantagem dos objetos Error é que eles capturam um rastreamento de pilha, aumentando a utilidade das ferramentas de depuração.
    Para utilizar essa promessa, podemos utilizar a seguinte sintaxe:

promise.then(function(result) {
  console.log(result); //"Stuff worked!"
}, function (err) {
    console.log(err); //Error: "It broke"
});

    then() aceita dois argumentos: um callback para a conclusão e outro para a falha. Ambos são opcionais.

    Compatibilidade de navegadores: A partir das versões Chrome 32, Opera 19, Firefox 29, Safari 8 & Microsoft Edge.

Exemplo de aplicação, simplificando o código assíncrono complexo:

    Digamos que precisamos fazer o seguinte:
  1. Iniciar um controle giratório para indicar um carregamento
  2. Buscar algum JSON referente a uma história que nos dará um título e URLs para cada capítulo desta história
  3. Adicionar o título à página
  4. Buscar cada capítulo
  5. Adicionar a história à página
  6. Interromper o controle giratório
  7. Informar ao usuário se ocorreu algum erro durante o processamento, nesse caso, interromper o controle giratório

    Neste exemplo iremos utilizar o XMLHttpRequest (que foi explicado no primeiro tópico deste arquivo) transformado em promessa para ilustrar esta situação. Iremos criar uma função simples para fazer uma solicitação GET:

function get(url) {
    // Return a new promise.
    return new Promise(function(resolve, reject){
        // Do the usual XHR stuff
        var req = new XMLHttpRequest();
        req.open('GET', url);

        req.onload = function() {
            // This is called even on 404 etc
            // so check the status
            if (req.status == 200) {
                // Resolve the promise with the response text
                resolve(req.response);
            } else {
                // Otherwise reject with the status text
                // which will hopefully be a meaningful error
                reject(Error(req.statusText));
            }
        };

        // Handle network errors
        req.onerror = function() {
            reject(Error("Network Error"));
        };

        // Make the request
        req.send();

    });
}

    Agora vamos usar esta função:

get('story.json').then(function(response) {
    console.log("Success!", response);
}, function(error) {
    console.log("Failed!", error);
})

Encadeamento:

    O then() não é o final da história. É possível encadear métodos then para transformar valores ou executar mais ações assíncronas em sequência. Exemplo de transformação de valores:

var promise = new Promise(function(resolve, reject) {
    resolve(1);
});

promise.then(function(val) {
    console.log(val); // 1
    return val + 2;
}).then(function(val) {
    console.log(val); // 3
})

    Também é possível encadear métodos then para executar ações assíncronas em sequência. O exemplo abaixo ilustra esta possibilidade:

getJson('story.json').then(function(story) {
    return getJson(story.chapterUrls[0]);
}).then(function(chapter1) {
    console.log("Got chapter 1!", chapter1);
})

    Neste código fizemos uma solicitação assíncrona para story.json, que retorna um conjunto de URLs a serem solicitadas. Em seguida, solicitamos a primeira URL. É aí que as promessas começam realmente a se destacar de simples padrões de callback.

Gerenciamento de erros:

    Para tratar erros em funções é possível usar duas sintaxes diferentes. A primeira já é conhecida e usa o segundo parâmetro da função then(), que é referente à situação de erro da promessa.

get('story.json').then(function(response) {
    console.log("Success!", response);
}, function(error) {
    console.log("Failed!", error)
})

    Além disso, também é possível usar o catch():

get('story.json').then(function(response) {
    console.log("Success!", response);
}).catch(function(error)) {
    console.log("Failed!", error);
})

    Não há nada especial sobre o catch(), é somente outra forma mais legível de pegar um erro.

Voltando à API Bluetooth em JavaScript...

Descobrindo dispositivos Bluetooth:

Esta versão da API de Bluetooth para WEB permite a conexão com um servidor GATT remoto em uma conexão BLE. É suportada a comunicação entre dispositivos que implementam o Bluetooth versão 4.0 ou superior.

Quando um website solicita acesso a dispositivos próximos usando navigator.bluetooth.requestDevice, o navegador do Google Chrome vai mostrar uma região em que o usuário poderá escolher o dispositivo a ser conectado ou cancelar a solicitação.

É possível encontrar o dispositivo Bluetooth automaticamente passando dados a respeito do dispositivo, como por exemplo o UUID completo, ou a versão comprimida de 16 ou 32 bits. Exemplo:

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

Além do parâmetro services, é possível usar um parâmetro baseado no nome do dispositivo com o filtro name, ou mesmo um prefixo deste nome com a chave namePrefix. Deve ser notado que neste caso, será necessário definir a chave optionalServices para acessar alguns serviços. Se isto não for feito, será ocasionado um erro posteriormente quando tentarmos acessar estes dispositivos. Exemplo:

navigator.bluetooth.requestDevice({
    filters: [{
        name: 'François robot'
    }],
    optionalServices: ['battery_service']
})
.then(device => { /* ... */ })
.then(error => { console.log(error); });

Finalmente, uma outra opção para acessar o dispositivo Bluetooth é usar a chave acceptAllDevices para mostrar todos os dispositivos Bluetooth das proximidades. Neste caso também será necessário usar a chave optionalServices para acessar alguns serviços. Se esta não for implementada, será dado um erro posteriormente.

navigator.bluetooth.requestDevice({
    acceptAllDevices: true,
    optionalServices: ['battery_service']
})
.then(device => { /* ... */ })
.catch(error => { console.log(error); });

Conectando a um dispositivo Bluetooth:

Então, o que fazer agora que nós temos um BluetoothDevice retornado da promessa navigator.bluetooth.requestDevice? Vamos nos conectar ao servidor GATT Bluetooth que armazena o serviço e as definições características. Exemplo:

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.log(error); });

Lendo uma características do Bluetooth:

Nesta parte nós estamos conectados ao servidor GATT do dispositivo Bluetooth remoto. Agora nós queremos receber um serviço GATT Primário e ler uma característica que pertence a este serviço. Vamos tentar, por exemplo, ler o nível de bateria atual da bateria do dispositivo.

No exemplo abaixo, battery_level é a característica normalizada 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 Batery 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.log(error); });

Se for usado um comando para requisitar uma característica customizada do GATT Bluetooth, é necessário informar o UUID completo do Bluetooth ou uma forma reduzida de 16 ou 32 bits para o service.getCharacteristic.

Observe que é possível adicionar um event listener characteristicvaluechanged em uma característica para lidar com a leitura deste valor. A referência [6] deve ser consultada para entender como usar este parâmetro.

Escrevendo uma característica Bluetooth:

Escrever para uma característica Bluetooth GATT é tão fácil quanto ler. Desta vez vamos usar o ponto de controle da razão cardíaca (Heart Rate Control Point) para resetar o valor do campo de energia utilizado para 0 em um dispositivo de monitoramento da razão cardíaca. O código abaixo ilustra esta possibilidade:

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
    var resetEnergyExpended = Uint8Array.of(1);
    return characteristic.writeValue(resetEnergyExpended);
})
.then(_ => {
    console.log('Energy expended has been reset.');
})
.catch(error => { console.log(error); });

Recebimento de notificações GATT:

Agora, vamos ver como ser notificado quando a medida da característica da razão cardíaca muda 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.log(error); });

function handleCharacteristicValueChanged(event) {
    var 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 (referência [6]) mostra como parar as notificações com stopNotifications() e remover o event listener adicionado characteristicvaluechanged.

Desconectando de um dispositivo Bluetooth:

Para melhorar a experiência do usuário, você deve querer mostrar uma mensagem de atenção se o dispositivo Bluetooth for desconectado para convidar o usuário a reconectar o dispositivo.

navigator.bluetooth.requestDevice({ filters: [{name: 'François 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.log(error); });

function onDisconnected(event) {
    let device = event.target;
    console.log('Device ' + device.name + ' is disconnected.');
}

É possível chamar apenas device.gatt.disconnect() para desconectar a aplicação WEB do dispositivo Bluetooth. Isto irá acionar o event listenner gattserverdisconnected. Note que isto não irá parar a comunicação do dispositivo Bluetooth caso outro aplicativo esteja já comunicando com este dispositivo.

Lendo e escrevendo os Bluetooth descriptors:

Os Bluetooth GATT descriptors são atributos que descrevem um valor de característica. Você pode ler e escrever neles em uma maneira similar às características do Bluetooth GATT.

Vamos ver, a título de exemplo, como ler a descrição de usuário de um dispositivo de medição de temperatura médica.

No exemplo abaixo, health_thermometer é o serviço médico de medição da temperatura, measurement_interval é a característica de intervalo de medição, e gatt.characteristic_user_description é o descriptor de descrição da característica 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_descriptor'))
.then(value => {
    let decoder = new TextDecoder('utf-8');
    console.log('User Description: ' + decoder.decode(value));
})
.catch(error => { console.log(error); });

Após ter sido apresentado os detalhes separadamente, agora será investigado um exemplo real, conforme apresentado em [7].

Exemplo de aplicação Bluetooth WEB:

Segundo [7], a sequência padrão para todas as aplicações WEB que interagem com Bluetooth é:

  1. Buscar (scan) por dispositivos relevantes;
  2. Conectar a este dispositivo;
  3. Obter o serviço que estamos interessados;
  4. Obter as características que estamos interessados;
  5. Ler, escrever ou sobrescrever as características.

Agora para exemplificar, um trecho de código útil:

// Step 1: Scan for a device with 0xffe5 service
navigator.bluetooth.requestDevice({
    filters: [{ services: [0xffe5] }]
})
.then(function(device) {
    // Step 2: Connect to it
    return device.gatt.connect();
})
.then(function(server) {
    // Step 3: Get the Service
    return server.getPrimaryService(0xffe5);
})
.then(function(service) {
    // Step 4: Get the Characteristic
    return service.getCharacteristic(0xffe9);
})
.then(function(characteristic) {
    // Step 5: Write to the characteristic
    var data = new Uint8Array([0xbb, 0x25, 0x05, 0x44]);
    return characteristic.writeValue(data);
})
.catch(function(error) {
    // And of course: error handling!
    console.log('Connection failed!', error);
});

Este código busca por um dispositivo com um número de serviço ffe5, em seguida pede por este serviço, então pede por um número na característica ffe9, e por fim escreve quatro bytes: bb 25 05 44, que pode ser um comando válido para o dispositivo sendo controlado, como por exemplo uma lâmpada, que com este comando fica variando lentamente suas cores.


Referências:

[1] -  Interact with Bluetooth devices on the WebGoogle Developers. Disponível em: <https://developers.google.com/web/updates/2015/07/interact-with-ble-devices-on-the-web>.


[3] - The Web Bluetooth Security ModelMedium. Disponível em: < https://medium.com/@jyasskin/the-web-bluetooth-security-model-666b4e7eed2>

[4] - Web BluetoothDraft Community Group Report. Disponível em: < https://webbluetoothcg.github.io/web-bluetooth/>

[5] - Promessas em JavaScript: uma introduçãoGoogle Developers. Disponível em: < https://developers.google.com/web/fundamentals/primers/promises>

[6] - Web Bluetooth / Read Characteristic Value Changed SampleGoogle Chrome Github. Disponível em: < https://googlechrome.github.io/samples/web-bluetooth/read-characteristic-value-changed.html>

[7] - Start Building with Web Bluetooth and Progressive Web Apps. Uri Shaked. Disponível em: < https://medium.com/@urish/start-building-with-web-bluetooth-and-progressive-web-apps-6534835959a6>



Comentários

Postagens mais visitadas deste blog

Dicas de música: Lo-fi hip hop

Pressupostos teóricos da Programação Neurolinguística