A Internet das Coisas está na boca de todo mundo hoje em dia, e isso deixa os fazedores e programadores como eu muito animados. Nada é mais legal do que dar vida às suas próprias invenções e poder conversar com elas!
No entanto, os dispositivos de IoT que instalam apps que você raramente usa podem ser irritantes. Por isso, aproveitamos as próximas tecnologias da Web, como a Web física e o Bluetooth da Web, para tornar os dispositivos de IoT mais intuitivos e menos intrusivos.
Web e IoT, uma combinação para você
Ainda há muitos obstáculos a superar para que a Internet das Coisas possa fazer sucesso. Um obstáculo são as empresas e os produtos que exigem que as pessoas instalem apps para cada dispositivo que compram, o que sobrecarrega os smartphones dos usuários com uma infinidade de apps que eles raramente usam.
Por esse motivo, estamos muito animados com o projeto Web física, que permite que os dispositivos transmitam um URL para um site on-line de uma maneira não intrusiva. Em combinação com novas tecnologias da Web, como Web Bluetooth, Web USB e NFC na Web, os sites podem se conectar diretamente ao dispositivo ou, pelo menos, explicar a maneira adequada de fazer isso.
Embora o foco principal deste artigo seja o Web Bluetooth, alguns casos de uso podem ser mais adequados para Web NFC ou Web USB. Por exemplo, o USB da Web é a melhor opção se você precisar de uma conexão física por motivos de segurança.
O site também pode funcionar como um App Web Progressivo (PWA). Recomendamos que os leitores confiram a explicação do Google sobre PWA. Os PWAs são sites que têm uma experiência do usuário responsiva, semelhante a um app, podem funcionar off-line e podem ser adicionados à tela inicial do dispositivo.
Como prova de conceito, tenho criado um pequeno dispositivo usando a placa de breakout Arduino Intel® Edison. O dispositivo contém um sensor de temperatura (TMP36) e um atuador (cátodo LED colorido). O esquema desse dispositivo pode ser encontrado no final deste artigo.
O Intel Edison é um produto interessante porque pode executar uma distribuição completa do Linux*. Portanto, posso programá-lo facilmente usando o Node.js. O instalador permite instalar o Intel* XDK, o que facilita o início, embora você também possa programar e fazer upload manualmente no dispositivo.
Para meu app Node.js, precisei de três módulos de nó e de suas dependências:
eddystone-beacon
parse-color
johnny-five
O primeiro instala automaticamente noble
, que é o módulo de nó
que uso para conversar pelo Bluetooth de baixa energia.
O arquivo package.json
do projeto tem esta aparência:
{
"name": "edison-webbluetooth-demo-server",
"version": "1.0.0",
"main": "main.js",
"engines": {
"node": ">=0.10.0"
},
"dependencies": {
"eddystone-beacon": "^1.0.5",
"johnny-five": "^0.9.30",
"parse-color": "^1.0.0"
}
}
Anunciar o site
A partir da versão 49, o Chrome no Android oferece suporte à Web física, que permite que o Chrome veja URLs sendo transmitidos por dispositivos próximos. Há alguns requisitos de que o desenvolvedor precisa estar ciente, como a necessidade de os sites serem acessíveis publicamente e usar HTTPS.
O protocolo Eddystone tem um limite de tamanho de 18 bytes em URLs. Portanto, para que o URL do meu app de demonstração funcione (https://webbt-sensor-hub.appspot.com/), preciso usar um encurtador de URL.
Transmitir o URL é bem simples. Basta importar as bibliotecas necessárias e chamar algumas funções. Uma maneira de fazer isso é chamando advertiseUrl
quando o chip BLE está ativado:
var beacon = require("eddystone-beacon");
var bleno = require('eddystone-beacon/node_modules/bleno');
bleno.on('stateChange', function(state) {
if (state === 'poweredOn') {
beacon.advertiseUrl("https://goo.gl/9FomQC", {name: 'Edison'});
}
}
Realmente não poderia ser mais fácil. Na imagem abaixo, você vê que o Chrome encontra o dispositivo corretamente.
Como se comunicar com o sensor/atuador
Usamos Johnny-Five* para falar com nossas melhorias do conselho. O Johnny-Five tem uma boa abstração para se comunicar com o sensor TMP36.
Confira abaixo o código simples para receber notificações sobre mudanças de temperatura e definir a cor inicial do LED.
var five = require("johnny-five");
var Edison = require("edison-io");
var board = new five.Board({
io: new Edison()
});
board.on("ready", function() {
// Johnny-Five's Led.RGB class can be initialized with
// an array of pin numbers in R, G, B order.
// Reference: http://johnny-five.io/api/led.rgb/#parameters
var led = new five.Led.RGB([ 3, 5, 6 ]);
// Johnny-Five's Thermometer class provides a built-in
// controller definition for the TMP36 sensor. The controller
// handles computing a Celsius (also Fahrenheit & Kelvin) from
// a raw analog input value.
// Reference: http://johnny-five.io/api/thermometer/
var temp = new five.Thermometer({
controller: "TMP36",
pin: "A0",
});
temp.on("change", function() {
temperatureCharacteristic.valueChange(this.celsius);
});
colorCharacteristic._led = led;
led.color(colorCharacteristic._value);
led.intensity(30);
});
Você pode ignorar as variáveis *Characteristic
acima por enquanto. Elas
serão definidas na próxima seção sobre a interface com Bluetooth.
Como você pode notar na instanciação do objeto Thermometer, eu falo com
o TMP36 pela porta analógica A0
. As pernas de tensão no cátodo do LED colorido estão conectadas aos pinos digitais 3, 5 e 6, que são os pinos de modulação de largura de pulso (PWM, na sigla em inglês) na placa de expansão Edison Arduino.
Como usar o Bluetooth
Não há nada mais fácil do que se conectar ao Bluetooth com noble
.
No exemplo a seguir, criamos duas características de Bluetooth de baixa energia: uma para o LED e outra para o sensor de temperatura. O primeiro permite ler a cor atual do LED e definir uma nova cor. O segundo permite que nos inscrevamos em eventos de mudança de temperatura.
Com noble
, é muito fácil criar uma característica. Tudo o que você precisa fazer
é definir como a característica se comunica e definir um UUID. As
opções de comunicação são ler, gravar, notificar ou qualquer combinação delas.
A maneira mais fácil de fazer isso é criar um novo objeto e herdar de
bleno.Characteristic
.
O objeto característico resultante será semelhante ao seguinte:
var TemperatureCharacteristic = function() {
bleno.Characteristic.call(this, {
uuid: 'fc0a',
properties: ['read', 'notify'],
value: null
});
this._lastValue = 0;
this._total = 0;
this._samples = 0;
this._onChange = null;
};
util.inherits(TemperatureCharacteristic, bleno.Characteristic);
Estamos armazenando o valor da temperatura atual na variável
this._lastValue
. Precisamos adicionar um método onReadRequest
e codificar o valor
para que uma "leitura" funcione.
TemperatureCharacteristic.prototype.onReadRequest = function(offset, callback) {
var data = new Buffer(8);
data.writeDoubleLE(this._lastValue, 0);
callback(this.RESULT_SUCCESS, data);
};
Para "notificar", precisamos adicionar um método para processar assinaturas e cancelamentos. Basicamente, armazenamos um callback. Quando temos um novo motivo de temperatura que queremos enviar, chamamos esse callback com o novo valor (codificado como acima).
TemperatureCharacteristic.prototype.onSubscribe = function(maxValueSize, updateValueCallback) {
console.log("Subscribed to temperature change.");
this._onChange = updateValueCallback;
this._lastValue = undefined;
};
TemperatureCharacteristic.prototype.onUnsubscribe = function() {
console.log("Unsubscribed to temperature change.");
this._onChange = null;
};
Como os valores podem flutuar um pouco, precisamos suavizar os valores que obtemos do sensor TMP36. Optei por simplesmente usar a média de 100 amostras e enviar atualizações apenas quando a temperatura muda em pelo menos 1 grau.
TemperatureCharacteristic.prototype.valueChange = function(value) {
this._total += value;
this._samples++;
if (this._samples < NO_SAMPLES) {
return;
}
var newValue = Math.round(this._total / NO_SAMPLES);
this._total = 0;
this._samples = 0;
if (this._lastValue && Math.abs(this._lastValue - newValue) < 1) {
return;
}
this._lastValue = newValue;
console.log(newValue);
var data = new Buffer(8);
data.writeDoubleLE(newValue, 0);
if (this._onChange) {
this._onChange(data);
}
};
Esse foi o sensor de temperatura. O LED colorido é mais simples. O objeto e o método de "leitura" são mostrados abaixo. A característica é configurada para permitir operações de leitura e gravação e tem um UUID diferente da característica de temperatura.
var ColorCharacteristic = function() {
bleno.Characteristic.call(this, {
uuid: 'fc0b',
properties: ['read', 'write'],
value: null
});
this._value = 'ffffff';
this._led = null;
};
util.inherits(ColorCharacteristic, bleno.Characteristic);
ColorCharacteristic.prototype.onReadRequest = function(offset, callback) {
var data = new Buffer(this._value);
callback(this.RESULT_SUCCESS, data);
};
Para controlar o LED do objeto, adiciono um
membro this._led
, que uso para armazenar o objeto LED
do Johnny-Five. Também defini a cor do LED para o valor padrão (branco, ou #ffffff
).
board.on("ready", function() {
...
colorCharacteristic._led = led;
led.color(colorCharacteristic._value);
led.intensity(30);
...
}
O método "write" recebe uma string (assim como "read" envia
uma string), que pode consistir em um código de cor CSS (por exemplo, nomes CSS
como rebeccapurple
ou códigos hexadecimais como #ff00bb
). Eu uso um módulo de nó
chamado parse-color
para sempre receber o valor hexadecimal que o Johnny-Five espera.
ColorCharacteristic.prototype.onWriteRequest = function(data, offset, withoutResponse, callback) {
var value = parse(data.toString('utf8')).hex;
if (!value) {
callback(this.RESULT_SUCCESS);
return;
}
this._value = value;
console.log(value);
if (this._led) {
this._led.color(this._value);
}
callback(this.RESULT_SUCCESS);
};
Nada disso vai funcionar se não incluirmos o módulo bleno.
O eddystone-beacon
não vai funcionar com o bleno, a menos que você use a versão
noble
distribuída com ele. Felizmente, isso é muito simples:
var bleno = require('eddystone-beacon/node_modules/bleno');
var util = require('util');
Agora, tudo o que precisamos é que ele anuncie nosso dispositivo (UUID) e as características dele (outros UUIDs).
bleno.on('advertisingStart', function(error) {
...
bleno.setServices([
new bleno.PrimaryService({
uuid: 'fc00',
characteristics: [
temperatureCharacteristic, colorCharacteristic
]
})
]);
});
Criar o app da Web do cliente
Sem falar de muitas falhas sobre como as partes não Bluetooth do app cliente funcionam, podemos demonstrar uma interface do usuário responsiva criada no Polymer* como exemplo. O app resultante é mostrado abaixo:
O lado direito mostra uma versão anterior, que mostra um registro de erros simples que adicionei para facilitar o desenvolvimento.
O Web Bluetooth facilita a comunicação com dispositivos Bluetooth de baixa energia. Vamos conferir uma versão simplificada do meu código de conexão. Se você não sabe como as promessas funcionam, confira este recurso antes de continuar a leitura.
A conexão com um dispositivo Bluetooth envolve uma cadeia de promessas.
Primeiro, filtramos o dispositivo (UUID: FC00
, nome: Edison
). Isso
mostra uma caixa de diálogo para permitir que o usuário selecione o dispositivo
de acordo com o filtro. Em seguida, nos conectamos ao serviço GATT e obtemos o serviço
principal e as características associadas. Depois, lemos os
valores e configuramos callbacks de notificação.
A versão simplificada do nosso código abaixo só funciona com a API Web Bluetooth mais recente e, portanto, requer o Chrome Dev (M49) no Android.
navigator.bluetooth.requestDevice({
filters: [{ name: 'Edison' }],
optionalServices: [0xFC00]
})
.then(device => device.gatt.connect())
.then(server => server.getPrimaryService(0xFC00))
.then(service => {
let p1 = () => service.getCharacteristic(0xFC0B)
.then(characteristic => {
this.colorLedCharacteristic = characteristic;
return this.readLedColor();
});
let p2 = () => service.getCharacteristic(0xFC0A)
.then(characteristic => {
characteristic.addEventListener(
'characteristicvaluechanged', this.onTemperatureChange);
return characteristic.startNotifications();
});
return p1().then(p2);
})
.catch(err => {
// Catch any error.
})
.then(() => {
// Connection fully established, unless there was an error above.
});
Ler e gravar uma string de um DataView
/ ArrayBuffer
(o que
a API WebBluetooth usa) é tão fácil quanto usar Buffer
no
lado do Node.js. Só precisamos usar TextEncoder
e TextDecoder
:
readLedColor: function() {
return this.colorLedCharacteristic.readValue()
.then(data => {
// In Chrome 50+, a DataView is returned instead of an ArrayBuffer.
data = data.buffer ? data : new DataView(data);
let decoder = new TextDecoder("utf-8");
let decodedString = decoder.decode(data);
document.querySelector('#color').value = decodedString;
});
},
writeLedColor: function() {
let encoder = new TextEncoder("utf-8");
let value = document.querySelector('#color').value;
let encodedString = encoder.encode(value.toLowerCase());
return this.colorLedCharacteristic.writeValue(encodedString);
},
Processar o evento characteristicvaluechanged
para o sensor de
temperatura também é muito fácil:
onTemperatureChange: function(event) {
let data = event.target.value;
// In Chrome 50+, a DataView is returned instead of an ArrayBuffer.
data = data.buffer ? data : new DataView(data);
let temperature = data.getFloat64(0, /*littleEndian=*/ true);
document.querySelector('#temp').innerHTML = temperature.toFixed(0);
},
Resumo
Isso é tudo, pessoal! Como você pode notar, a comunicação com o Bluetooth Low Energy usando o Web Bluetooth no lado do cliente e o Node.js no Edison é muito fácil e muito eficiente.
Usando a Web física e o Bluetooth da Web, o Chrome encontra o dispositivo e permite que o usuário se conecte a ele facilmente sem instalar aplicativos raramente usados que o usuário não quer e que podem ser atualizados de tempos em tempos.
Demonstração
Teste o cliente para se inspirar e saber como criar seus próprios apps da Web para se conectar a dispositivos personalizados da Internet das Coisas.
Código-fonte
O código-fonte está disponível aqui. Fique à vontade para informar problemas ou enviar patches.
Esquete
Se você quiser se aventurar e reproduzir o que fiz, consulte o esboço do Edison e da placa de circuito abaixo: