Primeros pasos con NodeJS y Raspberry Pi

Con el auge de los dispositivos móviles y la explosión de HTML5, Javascript ha ido incrementando su presencia en todos los ámbitos del desarrollo front y back end, desde su aparición hasta la actualidad.

Frente al desarrollo tradicional de aplicaciones móviles nativas, Javascript se constituye como una firme alternativa para unificar el desarrollo de aplicaciones multidispositivo bajo una sola plataforma. Sus principales virtudes, el ahorro de costes en el desarrollo, y el hecho de que es el lenguaje de la web, siendo conocido por prácticamente la totalidad de desarrolladores. Por el contrario, una aplicación móvil desarrollada en Javascript, no es nativa. Hay que tener muy claro este punto, porque dependiendo de las circunstancias del proyecto en cuestión, puede convertir nuestra aplicación móvil escrita en Javascript, en un éxito o en un rotundo fracaso.

En los últimos dos años, Javascript ha ido ganando popularidad en el terreno del back-end, frente a plataformas tan maduras como Java y .Net. Esto ha sido posible gracias a la aparición de Node.js, el framework de facto en la actualidad para aplicaciones de servidor. Este framework nos permitirá crear aplicaciones que pueden ser fácilmente escalables, con un altísimo rendimiento, y especialmente útiles en escenarios que requieren de comunicación en tiempo real, siendo un candidato idóneo para implementar escenarios de la Internet Of Things

Existen casos de éxito de grandes compañías que han cambiado sus plataformas a NodeJS:

  • PayPal, migró sus aplicaciones desarrolladas en Java, a NodeJS, con un resultado de un 35% de reducción de los tiempos de carga
  • La cadena de centros comerciales WalMart, desplegó sus aplicaciones de NodeJS en pleno Black Friday en 2013. La utilización de CPU en sus servidores no subió más de un uno por ciento de CPU. Sí, un uno por ciento.
  • Groupon cambió su plataforma desarrollada en Ruby On Rails resultando en un 50% de reducción en los tiempos de carga de las páginas
  • LinkedIn redujo la utilización de recursos alrededor de un 90% logrando en algunos casos una mejora de 20 veces la velocidad anterior.
  • Yahoo utiliza NodeJS para servir alrededor de 2 millones de páginas por minuto

Actualmente, Microsoft nos provee de herramientas para poder llevar a cabo proyectos completos escritos en Javascript, mediante el uso de herramientas de desarrollo que se integran en Visual Studio, y servicios en Windows Azure.

A continuación se describirá una posible implementación de un escenario de aplicaciones cliente – servidor, desarrollados en Javascript. Podremos desplegar nuestros componentes de JS, como Roles de servidor en Windows Azure (http://azure.microsoft.com/es-es/develop/nodejs/), aunque en este caso, para simplificar el escenario, se desplegará sobre un dispositivo on-premise, en este caso, una Raspberry Pi.

La gente de Microsoft, se ha puesto las pilas, y existirá una versión de Windows 10 para Raspberry Pi. Junto con la versión para Intel Galileo, Microsoft se está situando estratégicamente para crear una serie de dispositivos y aplicaciones que definirán la Internet Of Things del mañana.

Escenario

Para demostrar las posibilidades de esta tecnología en los entornos cliente y servidor, se propone la implementación de un conjunto de aplicaciones que puedan dar cabida a un escenario de seguimiento de flotas.

El caso práctico sería el de dos aplicaciones clientes que realizaran los siguientes roles dentro del sistema:

  • Una de ellas se encargaría de notificar la posición resuelta por el proveedor de localización del terminal o el propio navegador web, a un servicio que almacenaría dicha posición.
  • La otra aplicación se encargaría de implementar la parte desde la que se consumirían los datos. Desde aquí un usuario, podría consultar cuales son los dispositivos que actualmente están notificando su posición. A medida que se van almacenando las posiciones de los terminales, el cliente es notificado en tiempo real mediante el uso de web sockets, siendo posible visualizar las posiciones en un mapa.

Para el rol de servidor, se implementará una serie de servicios, que den cabida a la funcionalidad del almacenamiento de posiciones, además de notificar a los clientes.

Los frameworks concretos en los que se implementará el escenario descrito son los siguientes:

  • Clientes:
    • Angular: Las aplicaciones de Javascript, al crecer en complejidad, necesitan de un framework que permita paliar dicho aspecto. Para ello, Angular nos permite dotar a nuestras aplicaciones de un patrón MVC, con el que la complejidad en el mantenimiento, disminuye notablemente, permitiendo realizar aplicaciones SPA de gran calidad(http://angularjs.org)
    • Socket.IO: Nos permitirá implementar el canal de comunicaciones vía web socket, de forma que obtendremos notificaciones desde el servidor de forma casi “automágica”. Este framework, puede invocarse desde cliente o servidor de forma prácticamente idéntica. Dependiendo del navegador del cliente, Socket.IO realizará el transporte mediante web sockets (en el caso ideal), o cualquier otro mecanismo de transporte disponible (long-polling, etc). Esto es transparente para el programador, facilitando en gran medida el desarrollo de este tipo de aplicaciones. Es el equivalente a SignalR, en el mundo de .Net (http://socket.io)
    • Servidor:
      • Node.js: Servidor construido sobre el motor de Javascript de Chrome, el cual se ejecuta bajo un modelo basado en eventos, y que no bloquea los recursos de E/S, lo que lo hace perfecto en cualquier escenario, especialmente en aquellos que requieren comunicación en tiempo real. El hecho de que se pueda ejecutar en cualquier dispositivo también lo convierte en el candidato idóneo para aplicaciones de tipo IoT (Internet of Things). (http://nodejs.org). Para este escenario, desplegaremos la aplicación en una Raspberry Pi, como alternativa al clásico despliegue on premise, en entornos de tipo Windows.
      • MongoDB: Para explorar las posibilidades de la persistencia no relacional, se usará un gestor de base de datos NoSQL, como MongoDB, que constituye el actual gestor de base de datos de FourSquare, entre otros.
      • Esta aplicación de Node.js, sería equivalente a crear una aplicación ASP.Net WebApi, que implementaría las mismas funciones (añadiendo SignalR, como componente de notificación en tiempo real)

Gracias a los plugins que la comunidad open-source ha desarrollado para Visual Studio, podremos desarrollar este tipo de aplicaciones utilizando el mejor entorno de desarrollo disponible en la actualidad, y en el cual Kabel ya posee muchísima experiencia.

Los plugins que utilizaremos para Visual Studio son los siguientes:

  • Node.js Tools for Visual Studio: Nos permitirá crear aplicaciones Node.js, siendo posible añadir diferentes módulos en función de las necesidades, como persistencia en BBDD, comunicación vía web-sockets, etc, utilizando el gestor de paquetes de Node.js (NPM), cuya funcionalidad sería similar a NuGet, para una aplicación de .Net

Este plugin, además, nos permitirá al finalizar cualquier desarrollo, el empaquetado para desplegar en un rol de Windows Azure. (https://nodejstools.codeplex.com/)

Aplicación NodeJS

En primer lugar, crearemos una aplicación de Node.JS, mediante el plugin que comentábamos anteriormente:

Una vez que hayamos creado la aplicación, instalaremos los módulos necesarios mediante el gestor de paquetes de NodeJS (Node Package Manager, ó NPM). Necesitaremos los siguientes módulos para poder desarrollar nuestra prueba de concepto:

  • Express.js: Es el MVC de Node.JS. En esta prueba de concepto, usaremos la aplicación de NodeJS como servicio REST, por lo que prescindiremos del renderizado propio de las vistas. (http://expressjs.com/)
  • Body-Parser: Módulo para el parseo de los mensajes JSON. Nos permitirá acceder d manera sencilla, a los datos en formato JSON, que se incluirán en cada request (https://github.com/expressjs/body-parser)
  • Mongoose: Este sería el “Entity Framework” para MongoDB (http://mongoosejs.com/)
  • Socket.IO: Como comentábamos anteriormente, este es el módulo responsable de la comunicación en tiempo real (http://socket.io)
  • Geocoder: Módulo para realizar el “geocoding inverso”, cada vez que Node reciba una latitud y longitud válidas (https://github.com/nchaulet/node-geocoder)

Seleccionaremos el elemento “npm”, del proyecto que acabamos de crear y seleccionaremos la opción “Install new npm packages…”, lo que nos mostrará una interfaz desde la que podremos gestionar cómodamente los paquetes que queremos añadir:

Una vez instaladas las dependencias necesarias, “arrancaremos” nuestra aplicación de Node.js, para que escuche peticiones por el puerto 8080:

(function () {

    var express = require("express");
    var bodyParser = require("body-parser");

    var port = 8080;
    var app = express();

    //CORS middleware
    app.use(function (req, res, next) {
        res.header("Access-Control-Allow-Origin", "*");
        res.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept");
        next();
    });

    app.use(bodyParser.json());
    app.use(bodyParser.urlencoded({ extended: false }));
    var server = app.listen(port);  

})();

A continuación, crearemos la instancia de Socket.IO, que gestionará la comunicación en tiempo real entre los clientes y el servidor:

(function () {

    var connectedAssets = [];

    var connectedAsset = {
        assetId: null,
        socketId: null,
        channel: null
    };

    var io = require("socket.io");

    function addAsset(socket) {
        var asset = Object.create(connectedAsset);
        asset.assetId = socket.handshake.query.assetId;
        asset.socketId = socket.id;
        asset.channel = socket;
        connectedAssets[asset.socketId] = asset;
        console.log("Connected asset: " + asset.assetId + " with socketId: " + asset.socketId);
    }

    function removeAsset(socket) {
        console.log("Asset: " + connectedAssets[socket.id].assetId + " disconnected");
        var asset = connectedAssets[socket.id];
        delete connectedAssets[socket.id];

        console.log("Assets already connected:");
        for (var key in connectedAssets) {
            if (connectedAssets.hasOwnProperty(key)) {
                console.log(connectedAssets[key].assetId);
            }
        }

        return asset;
    }

    module.exports = function (server) {
        io = io(server);

        io.of("/recorder").on("connection", function (socket) {
            addAsset(socket);
            io.of("/listener").emit('assetOnline', connectedAssets[socket.id].assetId);
            socket.on('disconnect', function () {
                var asset = removeAsset(this);
                io.of("/listener").emit('assetOffline', asset.assetId);
            });
        });

        io.of("/listener").on("connection", function (socket) {
            console.log("listener connected");
        });

        return io;
    }

})();

Esta instancia de Socket.IO, nos permitirá mantener una colección interna de los clientes que progresivamente se vayan conectando a la aplicación de servidor, y emitirá eventos de conexión y desconexión de los mismos, de forma que sería posible monitorizar en tiempo real, qué clientes se conectan y desconectan. Posteriormente se utilizará esta instancia de Socket.IO para lanzar eventos cuando se haya recibido una posición y se haya determinado su dirección mediante el módulo de Geocoder.

Seguidamente, crearemos el “Controller” (por analogía con los controllers de ASP.Net WebAPI), que manejará las peticiones que realicemos a una URL concreta. Este concepto se conoce en Node.JS como Router, gestionado por el módulo Express.

Definiremos un método dentro de este Router, el cual recibirá la posición enviada por la aplicación cliente encargada de grabar el posicionamiento.

router.post("/:id/position", function (request, response) {
response.sendStatus(200);
});

En este punto, necesitaremos modelar la información que recibirá este Controlador, para poder construir un objeto en memoria y guardar la información asociada en MongoDB

Como vemos, los parámetros se definen anteponiendo un “:”, para la url que estamos definiendo. Por tanto, si hiciéramos una petición a la url http://localhost/asset/asset123/position, el parámetro correspondiente a :id, sería asset123.

Por tanto modelaremos nuestro objeto, con las siguientes propiedades:

  • Latitud
  • Longitud
  • Identificador del cliente conectado
  • Fecha del posicionamiento
  • Dirección, obtenida mediante geocoding inverso
  • Precisión: Si utilizáramos un cliente móvil para resolver la posición enviada, se puede usar esta propiedad para indicar la precisión del posicionamiento (WiFi, GPS, etc.)

Definiremos, pues, el esquema de nuestro objeto:

(function () {

    var mongoose = require("mongoose");
    mongoose.connect("mongodb://localhost/tracker");

    module.exports = function () {

        var models = {};

        models.GeoPoint = mongoose.model("GeoPoint", {
            latitude: Number,
            longitude: Number,
            accuracy: String,
            assetId: String,
            address: String,
            timeStamp: { type: Date, default: Date.now }
        });

   });
   return models;
})();

Una vez realizado este paso, estaríamos en disposición de implementar el código que comentábamos anteriormente. Las siguientes operaciones deben ser realizadas:

  • Extraer del cuerpo de la petición, los datos que formarán parte de nuestro “objeto de negocio”
  • Resolver la dirección especificada por la latitud y longitud especificada en dicho objeto
  • Guardar el objeto en MongoDB
  • Notificar un evento mediante la instancia de Socket.IO, con el fin de que aquellos clientes conectados, recibirán la información en tiempo real.
(function () {

    var express = require("express");
    var geocoder = require("geocoder");

    var router = express.Router();
    var baseUrl = "/asset/";

    var _socketManager;

    var models = require("../db/models");

    var geoPointModel = models().GeoPoint;

    router.get("/:id/position", function (request, response) {
        var userId = request.params.id;
        console.log(userId);

        geoPointModel.find(function(err, points) {
            response.json(points);
        });
    });

    router.post("/:id/position", function (request, response) {
        var point = new geoPointModel(request.body);
        geocoder.reverseGeocode(point.latitude, point.longitude, function (err, data) {
            if (!err) {

                if (data && data.results[0]) {
                    point.address = data.results[0].formatted_address;
                    console.log("Geocoder resolved address: " + point.address);
                }

                point.save(function (err) {
                    if (err)
                        console.log("Error saving position: " + err); //TODO handle exceptions properly       

                    _socketManager.of("/listener").emit("locationChanged", point);
                    response.sendStatus(200);
                });
            } else {
                console.log("Geocoder Failed!" + err);
            }
        });
    });

    router.url = baseUrl;

    module.exports = function (socketManager) {
        _socketManager = socketManager;
        return router;
    }

})();

Una vez implementados todos estos pasos, estaremos en disposición de arrancar nuestra aplicación de NodeJS y empezar a grabar posicionamientos y recibirlos en tiempo real.

Despliegue en una Raspberry Pi

Para desplegar nuestra aplicación de Node.JS, utilizaremos una Raspberry Pi, como se comentaba anteriormente. Para ello deberemos instalar una distribución de Linux optimizada para este dispositivo, desde este enlace: http://www.raspbian.org/, o desde el sitio oficial de Raspberry Pi

Una vez instalado el sistema operativo, seremos capaces de conectarnos de manera remota, utilizando SSH. Para ello usaremos un viejo conocido, Putty, para poder iniciar sesión como administrador en nuestra Raspberry Pi. Una vez hayamos conectado, debemos instalar tanto Node.JS como MongoDB

Existen gran cantidad de tutoriales en internet, que nos guiarán en el proceso. Sin embargo, la mayoría de ellos, indican únicamente, cómo descargar el código fuente de los mismos y compilarlo en el propio dispositivo. Puesto que esta tarea puede llevarnos unas cuantas horas, podemos recurrir a descargar ya una imagen compilada de los mismos. La forma más inmediata de realizar este proceso la encontraremos en el siguiente enlace

(Nota: Esto sería sólo aplicable a escenarios orientados a pruebas de concepto o similares. Nunca se debe confiar en una imagen de terceros para un entorno de producción)

Al finalizar la instalación, copiaremos el directorio que contenga todos los fuentes con los que hemos desarrollado nuestra aplicación de NodeJS, a nuestra Raspberry Pi.

De la misma forma que en nuestro entorno de desarrollo, agregamos las dependencias necesarias mediante NPM, también deberemos realizar esta tarea mediante la misma herramienta, pero esta vez desde línea de comandos:

sudo npm install

Finalmente, arrancaremos, nuestra aplicación, escribiendo el siguiente comando

sudo node server.js

Nuestra Raspberry Pi, aceptará las peticiones realizadas por el puerto 8080 procesando las mismas según el enrutado definido por los Routers (controllers) de Express.

Guardado de la posición actual

Para simular un dispositivo que sea capaz de transmitir su posición actual, implementaremos una sencillísima aplicación web que nos permita de manera rápida “postear” posicionamientos válidos al servicio de NodeJS

En este caso utilizaremos AngularJS como framework, aunque para una aplicación de este estilo bastaría con un simple frontal de HTML.

Incluiremos un mapa de Google, desde el cual, al hacer click en cualquier punto, capturará la latitud y longitud del mapa, enviando una posición a la aplicación de NodeJS. Dicho envío se realizará efectuando un POST al servicio REST de NodeJS definido por su enrutado. Para identificar el “dispositivo” conectado, usaremos el enrutado de Angular con el fin de poder parametrizar el identificador del mismo:

app.config(['$routeProvider', function ($routeProvider) {
        $routeProvider.when('/asset/:assetId', {
            templateUrl: 'views/mapView.html',
            controller: 'RecorderController'
        }).when('/', {
            templateUrl: 'views/index.html',
            controller: 'HomeController'
        }).otherwise({ redirectTo: '/'});
    }]);

 

Cuando carguemos la url “/asset/asset123”, el enrutado de Angular cargará la vista correspondiente, pasándole al controlador los parámetros de la url.

En este punto, se utilizará un cliente de Socket.IO, para conectar con el servicio de NodeJS. En el servidor, se lanzarán eventos que indicarán cuando el cliente se conecta y desconecta, para poder enviar este estado a la aplicación cliente que escucha los cambios de posicionamiento:

(function () {

    angular.module("recorder").factory('socket', SocketController);

    SocketController.$inject = ['$rootScope', 'config', '$routeParams']

    function SocketController($rootScope, config, $routeParams) {
        var socket = io.connect(config.baseUrl + config.socketNamespace, { query: "assetId=" + $routeParams.assetId });
        return {
            on: function (eventName, callback) {
                socket.on(eventName, function () {
                    var args = arguments;
                    $rootScope.$apply(function () {
                        callback.apply(socket, args);
                    });
                });
            },
            emit: function (eventName, data, callback) {
                socket.emit(eventName, data, function () {
                    var args = arguments;
                    $rootScope.$apply(function () {
                        if (callback) {
                            callback.apply(socket, args);
                        }
                    });
                })
            }
        };
    }
})();

Notificaciones en tiempo real

Esta aplicación será la responsable de representar la posición en tiempo real, notificada por la aplicación anterior:

  • Añadiremos un marker al mapa, modificando su latitud y longitud a medida que estas vayan siendo notificadas por el servidor. Se indicará además, la dirección resuelta anteriormente, para mostrarla como información sobre el mapa
  • Se mostrará el estado del “dispositivo” (en este escenario, la aplicación del apartado anterior), indicando si está conectado (online) o desconectado (offline)
  • Se usará adicionalmente, un servicio del API de Google, que nos indica cual sería el tiempo y la distancia estimados, para una posición de referencia en el mapa. Dicha posición de referencia se establece de manera fija en este escenario. Para escenarios reales, podría utilizarse la posición resuelta por un dispositivo móvil, para calcular la distancia y el tiempo estimados hasta la posición donde se encuentra un usuario.

Por ejemplo, un escenario real, podría ser el de un usuario que precisa de Asistencia en Carretera, solicitando para ello un servicio de Grúa  a su entidad aseguradora. A medida que la Grúa circula en dirección a la localización del usuario, ésta notifica periódicamente su posición a la aplicación de servidor. El usuario obtiene actualizaciones automáticas, siendo informado de la posición y dirección de la Grúa, así como del tiempo y la distancia estimados hasta su posición

La aplicación será muy parecida a la realizada en el apartado anterior, con la salvedad de que no necesitaremos el enrutado de Angular, puesto que cargaremos el mapa, en la página de inicio

Al iniciar la aplicación conectaremos vía Socket.IO al servidor de NodeJS, y nos suscribiremos al evento de notificación de nuevo posicionamiento.

Cuando la aplicación sea notificada, se añadirá el marker al mapa (o se modificará su posición si éste ya existe), y se calculará la distancia y tiempo estimados al punto de referencia.

   socket.on("assetOnline", function (data) {
            $scope.message = data + " is online";
            $scope.status = "online";
            $scope.showStatus = true;
        });

        socket.on("assetOffline", function (data) {
            $scope.message = data + " is offline";
            $scope.status = "offline";
            $scope.showStatus = true;
        });

        socket.on("locationChanged", function (data) {

            var marker = createMarker(data.assetId, { latitude: data.latitude, longitude: data.longitude, address: data.address });

            //$scope.markers.push(marker);
            function getMarker(id) {
                for (var i = 0; i < $scope.markers.length; i++) {
                    if ($scope.markers[i].id === marker.id)
                        return $scope.markers[i];
                }
            }

            function setMarker(marker) {
                for (var i = 0; i < $scope.markers.length; i++) {
                    if ($scope.markers[i].id === marker.id) {
                        $scope.markers[i].latitude = marker.latitude;
                        $scope.markers[i].longitude = marker.longitude;
                        break;
                    }
                }
            }

            function setMarkerInfo(marker) {
                for (var i = 0; i < $scope.markers.length; i++) {
                    if ($scope.markers[i].id === marker.id) {
                        $scope.markers[i].arrival = marker.arrival;
                        break;
                    }
                }
            }

            if (!getMarker(marker.id)) {
                $scope.markers.push(marker);
            } else {
                setMarker(marker);
            }

            $scope.currentPosition = marker;
            $scope.map.center = { latitude: data.latitude, longitude: data.longitude };

            var origin = new google.maps.LatLng(marker.latitude, marker.longitude);
            var destination = new google.maps.LatLng($scope.myPosition.latitude, $scope.myPosition.longitude)

            var distanceMatrixService = new google.maps.DistanceMatrixService();

            distanceMatrixService.getDistanceMatrix(
            {
                origins: [origin],
                destinations: [destination],
                travelMode: google.maps.TravelMode.DRIVING,
            }, function callback(response, status) {
                if (status == google.maps.DistanceMatrixStatus.OK) {
                    $scope.arrivals = [];
                    $scope.showInfo = true;
                    var results = response.rows[0].elements;
                    for (var j = 0; j < results.length; j++) {
                        var element = results[j];
                        if (element.distance.value < 1000) {
                            $scope.map.zoom = 17;
                        } else if (element.distance.value < 500) {
                            $scope.map.zoom = 18;
                        } else {
                            $scope.map.zoom = 13;
                        }

                        var arrival = {
                            distance: element.distance.text,
                            duration: element.duration.text,
                        }

                        $scope.arrivals.push(arrival);
                        var currentMarker = getMarker(marker.id);
                        currentMarker.arrival = arrival;
                        setMarkerInfo(currentMarker);
                    }

                } else {
                    console.log("Google Distance Matrix Error: " + status);
                    $scope.markers.push(marker);
                }
            });

Una vez resueltos los datos de tiempo y distancia estimados, representamos la información en el mapa, visualizando como el marker se desplaza en tiempo real, sin que sea necesaria ninguna interacción del usuario para refrescar la información

<section ng-controller="ListenerController">
        <header ng-show="showInfo">
            <div class="currentPoint">
                <h5>Current position of <strong>{{currentPosition.id}}</strong></h5>
                <dl>
                    <dd>{{currentPosition.address}}</dd>
                </dl>
            </div>
            <div class="statusMessages {{status}}" ng-show="showStatus">
                <span>{{message}}</span>
            </div>
        </header>
        <ui-gmap-google-map center='map.center' zoom='map.zoom'>
            <ui-gmap-markers models="markers" coords="'self'" icon="'icon'" onclick="'marker.showInfo()'">
                <ui-gmap-windows show="show">
                    <div ng-non-bindable>
                        Asset:{{id}}
                        Distance:{{arrival.distance}}
                        ETA:{{arrival.duration}}
                    </div>
                </ui-gmap-windows>
            </ui-gmap-markers>

        </ui-gmap-google-map>
        <footer ng-show="showInfo">
            <div class="currentPoint">
                <h5>Estimated Arrival</h5>
                <ul>
                    <li ng-repeat="arrival in arrivals">
                        <dl>
                            <dt>Distance:</dt>
                            <dd>{{arrival.distance}}</dd>
                            <dt>Duration:</dt>
                            <dd>{{arrival.duration}}</dd>
                        </dl>
                    </li>
                </ul>
            </div>
        </footer>
    </section>

Si utilizamos Ripple como emulador móvil, obtendremos una representación de cómo quedaría esta aplicación si la visualizáramos en un Smartphone, o similar:

Happy Coding!!! Y Feliz Semana Santa!!!