Crea un dispositivo IoT habilitado para la Web con Intel Edison

Kenneth Christiansen
Kenneth Christiansen

Actualmente, todo el mundo habla de la Internet de las cosas, y a los aficionados y programadores como yo nos entusiasma mucho. Nada es más genial que dar vida a tus propios inventos y poder hablar con ellos.

Sin embargo, los dispositivos de la IoT que instalan apps que rara vez usas pueden ser molestos, por lo que aprovechamos las próximas tecnologías web, como la Web física y el Bluetooth web, para que los dispositivos de la IoT sean más intuitivos y menos intrusivos.

Aplicación cliente

La Web y la IoT, una combinación perfecta

Todavía quedan muchos obstáculos por superar antes de que la Internet de las cosas sea un gran éxito. Un obstáculo son las empresas y los productos que requieren que las personas instalen apps para cada dispositivo que compran, lo que desordena los teléfonos de los usuarios con una gran cantidad de apps que rara vez usan.

Por este motivo, nos entusiasma mucho el proyecto de Web física, que permite que los dispositivos transmitan una URL a un sitio web en línea de una manera no invasiva. En combinación con tecnologías web emergentes, como Bluetooth web, USB web y NFC web, los sitios pueden conectarse directamente al dispositivo o, al menos, explicar la forma correcta de hacerlo.

Aunque en este artículo nos enfocamos principalmente en el Bluetooth web, algunos casos de uso podrían ser más adecuados para el NFC web o el USB web. Por ejemplo, se prefiere Web USB si necesitas una conexión física por motivos de seguridad.

El sitio web también puede funcionar como una app web progresiva (AWP). Recomendamos a los lectores que consulten la explicación de Google sobre las AWP. Las PWAs son sitios que tienen una experiencia del usuario responsiva y similar a la de una app, pueden funcionar sin conexión y se pueden agregar a la pantalla principal del dispositivo.

Como prueba de concepto, construí un dispositivo pequeño con la placa de Arduino Intel® Edison. El dispositivo contiene un sensor de temperatura (TMP36) y un actuador (cátodo LED de color). Los esquemas de este dispositivo se encuentran al final de este artículo.

Placa de pruebas.

Intel Edison es un producto interesante porque puede ejecutar una distribución completa de Linux*. Por lo tanto, puedo programarlo fácilmente con Node.js. El instalador te permite instalar el XDK de Intel*, lo que facilita el inicio, aunque también puedes programar y subir al dispositivo de forma manual.

Para mi app de Node.js, necesité tres módulos de nodos, así como sus dependencias:

  • eddystone-beacon
  • parse-color
  • johnny-five

El primero instala automáticamente noble, que es el módulo de nodo que uso para comunicarme a través de Bluetooth de bajo consumo.

El archivo package.json del proyecto se ve de la siguiente manera:

{
    "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"
    }
}

Anuncia el sitio web

A partir de la versión 49, Chrome para Android admite la Web física, lo que le permite ver las URLs que transmiten los dispositivos a su alrededor. Existen algunos requisitos que el desarrollador debe tener en cuenta, como la necesidad de que los sitios sean de acceso público y que usen HTTPS.

El protocolo Eddystone tiene un límite de tamaño de 18 bytes en las URLs. Por lo tanto, para que funcione la URL de mi app de demostración (https://webbt-sensor-hub.appspot.com/), debo usar un acortador de URLs.

Transmitir la URL es bastante sencillo. Solo debes importar las bibliotecas necesarias y llamar a algunas funciones. Una forma de hacerlo es llamar a advertiseUrl cuando el chip BLE está activado:

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

No podría ser más fácil. En la imagen de abajo, ves que Chrome encuentra bien el dispositivo.

Chrome anuncia píxeles contadores de la Web física cercanos.
Aparecerá la URL de la app web.

Cómo comunicarse con el sensor o actuador

Usamos Johnny-Five* para comunicarnos con las mejoras de la placa. Johnny-Five tiene una buena abstracción para comunicarse con el sensor TMP36.

A continuación, puedes encontrar el código simple para recibir notificaciones sobre los cambios de temperatura y establecer el color LED inicial.

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

Por ahora, puedes ignorar las variables *Characteristic anteriores, que se definirán en la sección posterior sobre la interfaz con Bluetooth.

Como puedes observar en la creación de la instancia del objeto Thermometer, hablo con el TMP36 a través del puerto analógico A0. Los extremos de voltaje en el cátodo del LED de color están conectados a los pines digitales 3, 5 y 6, que son los pines de modulación de ancho de pulso (PWM) en la placa de conexiones de Arduino Edison.

Placa Edison

Cómo hablar con Bluetooth

Hablar con Bluetooth no podría ser mucho más fácil que con noble.

En el siguiente ejemplo, creamos dos características de Bluetooth de baja energía: una para el LED y otra para el sensor de temperatura. El primero nos permite leer el color del LED actual y establecer un color nuevo. Este último nos permite suscribirnos a eventos de cambio de temperatura.

Con noble, crear una característica es bastante fácil. Todo lo que debes hacer es definir cómo se comunica la característica y definir un UUID. Las opciones de comunicación son lectura, escritura, notificación o cualquier combinación de ellas. La manera más fácil de hacerlo es crear un objeto nuevo y heredar de bleno.Characteristic.

El objeto de característica resultante se ve de la siguiente manera:

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

Almacenamos el valor de temperatura actual en la variable this._lastValue. Debemos agregar un método onReadRequest y codificar el valor para que funcione una "lectura".

TemperatureCharacteristic.prototype.onReadRequest = function(offset, callback) {
    var data = new Buffer(8);
    data.writeDoubleLE(this._lastValue, 0);
    callback(this.RESULT_SUCCESS, data);
};

Para “notificar”, debemos agregar un método para controlar las suscripciones y la cancelación de suscripciones. Básicamente, solo almacenamos una devolución de llamada. Cuando tenemos un nuevo motivo de temperatura que queremos enviar, llamamos a esa devolución de llamada con el valor nuevo (codificado como se indicó anteriormente).

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 los valores pueden fluctuar un poco, debemos suavizar los valores que obtenemos del sensor TMP36. Decidí tomar el promedio de 100 muestras y enviar actualizaciones solo cuando la temperatura cambie al menos 1 grado.

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

Ese fue el sensor de temperatura. El LED de color es más simple. A continuación, se muestran el objeto y el método “read”. La característica está configurada para permitir operaciones de "lectura" y "escritura", y tiene un UUID diferente al de la 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 el LED desde el objeto, agrego un miembro this._led que uso para almacenar el objeto LED Johnny-Five. También establecí el color de la luz LED en su valor predeterminada (blanco, también conocido como #ffffff).

board.on("ready", function() {
    ...
    colorCharacteristic._led = led;
    led.color(colorCharacteristic._value);
    led.intensity(30);
    ...
}

El método "write" recibe una cadena (al igual que "read" envía una cadena), que puede consistir en un código de color CSS (por ejemplo, nombres de CSS como rebeccapurple o códigos hexadecimales como #ff00bb). Uso un módulo de nodo llamado parse-color para obtener siempre el valor hexadecimal, que es lo que espera Johnny-Five.

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

Todo lo anterior no funcionará si no incluimos el módulo bleno. eddystone-beacon no funcionará con bleno, a menos que uses la versión de noble que se distribuye con él. Por suerte, hacerlo es bastante sencillo:

var bleno = require('eddystone-beacon/node_modules/bleno');
var util = require('util');

Ahora, solo necesitamos que anuncie nuestro dispositivo (UUID) y sus características (otros UUID).

bleno.on('advertisingStart', function(error) {
    ...
    bleno.setServices([
        new bleno.PrimaryService({
        uuid: 'fc00',
        characteristics: [
            temperatureCharacteristic, colorCharacteristic
        ]
        })
    ]);
});

Crea la app web cliente

Sin entrar en demasiados detalles sobre cómo funcionan las partes no Bluetooth de la app cliente, podemos demostrar una interfaz de usuario responsiva creada en Polymer* como ejemplo. La app resultante se muestra a continuación:

App cliente en el teléfono.
Mensaje de error

En el lado derecho, se muestra una versión anterior, que muestra un registro de errores simple que agregué para facilitar el desarrollo.

Web Bluetooth facilita la comunicación con dispositivos Bluetooth de bajo consumo, así que veamos una versión simplificada de mi código de conexión. Si no sabes cómo funcionan las promesas, consulta este recurso antes de continuar leyendo.

La conexión a un dispositivo Bluetooth implica una cadena de promesas. Primero, filtramos el dispositivo (UUID: FC00, nombre: Edison). Se mostrará un diálogo para permitir que el usuario seleccione el dispositivo según el filtro. Luego, nos conectamos al servicio GATT y obtenemos el servicio principal y las características asociadas. Luego, leemos los valores y configuramos las devoluciones de llamada de notificación.

La versión simplificada de nuestro código que se muestra a continuación solo funciona con la API de Web Bluetooth más reciente y, por lo tanto, requiere Chrome Dev (M49) en 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.
});

Leer y escribir una cadena desde un DataView o ArrayBuffer (lo que usa la API de WebBluetooth) es tan fácil como usar Buffer en el lado de Node.js. Solo necesitamos usar TextEncoder y 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);
},

Manejar el evento characteristicvaluechanged para el sensor de temperatura también es bastante 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);
},

Resumen

¡Eso fue todo, amigos! Como puedes ver, comunicarse con Bluetooth de bajo consumo a través de Bluetooth Web en el lado del cliente y Node.js en Edison es bastante fácil y muy potente.

Con la Web física y el Bluetooth web, Chrome encuentra el dispositivo y le permite al usuario conectarse a él fácilmente sin instalar aplicaciones que rara vez se usan y que tal vez no quiera, y que pueden actualizarse de vez en cuando.

Demostración

Puedes probar el cliente para obtener ideas sobre cómo crear tus propias apps web para conectarte a tus dispositivos de la Internet de las cosas personalizados.

Código fuente

El código fuente está disponible aquí. No dudes en informar problemas o enviar parches.

Boceto

Si eres muy aventurero y quieres reproducir lo que hice, consulta el siguiente boceto de Edison y de la placa de pruebas:

Boceto