Full text
Equation Chapter 1 Section 1 Trabajo Fin de Grado Grado en Ingeniería de las Tecnologías de Telecomunicación Integración y gestión de un nuevo tipo de dispositivo Raspberry en el IoT Server de WSO2 Autor: Luis Valencia Pichardo Tutor: Isabel Román Martínez Departamento de Ingeniería Telemática Escuela Técnica Superior de Ingeniería Universidad de Sevilla Sevilla, 2019
iii Trabajo Fin de Grado Grado en Ingeniería de las Tecnologías de Telecomunicación Integración y gestión de un nuevo tipo de dispositivo Raspberry en el IoT Server de WSO2 Autor: Luis Valencia Pichardo Tutor: Isabel Román Martínez Profesora colaboradora Departamento de Ingeniería Telemática Escuela Técnica Superior de Ingeniería Universidad de Sevilla Sevilla, 2019
v Trabajo Fin de Grado: Integración y gestión de un nuevo tipo de dispositivo Raspberry en el IoT Server de WSO2 Autor: Luis Valencia Pichardo Tutor: Isabel Román Martínez El tribunal nombrado para juzgar el Proyecto arriba indicado, compuesto por los siguientes miembros: Presidente: Vocales: Secretario: Acuerdan otorgarle la calificación de: Sevilla, 2019 El Secretario del Tribunal
vii A mi familia A mis maestros A mis compañeros y amigos
ix Agradecimientos En primer lugar, a mi familia, compañeros y amigos que durante este camino me ha apoyado y han creído en mi, pues sin su apoyo ni su ayuda no me hubiera sido posible llegar a este lugar. En segundo lugar, a mis profesores, sin los cuales no habría llegado a apreciar el camino recorrido, ni a mejorar como persona, pues como dijo Einstein “El arte supremo del maestro consiste en despertar el goce de la expresión creativa y del conocimiento.” Luis Valencia Pichardo Sevilla, 2019
ÍNDICE DE FIGURAS • Figura 2.1 - Barebone • Figura 2.2 - Arquitectura del servidor IoT de WSO2 • Figura 2.3 - Esquema del CDMF • Figura 2.4 - Ejemplo de configuración de Tmux • Figura 2.5 - Configuración inferior de Tmux • Figura 2.6 - Tema Agnoster de Oh-my-zsh! • Figura 2.7 - Ejemplo de API en Java con Swagger • Figura 2.8 - Diagrama de MQTT • Figura 2.9 – Arquitectura OSGi • Figura 3.1 – Sensor de temperatura DS18B20 y matriz de leds con driver MAX7219 • Figura 3.2 – GPIO de Raspberry Pi • Figura 3.3 – Conexión de los periféricos • Figura 3.4 - Arquitectura DAS • Figura 3.5 – Esquema del bróker de mensajería • Figura 3.6 - Portal de bienvenida • Figura 3.7 - Esquema de autenticación • Figura 3.8 - Esquema local • Figura 4.1 – Diagrama de despliegue del proyecto • Figura 4.2 – Diagrama de clases global • Figura 4.3 – Generación del arquetipo • Figura 4.4 – Directorio de trabajo • Figura 4.5 – Directorio API • Figura 4.6 – Directorio Plugin • Figura 4.7 – Directorio UI • Figura 4.8 – Directorio feature • Figura 4.9 – Diagrama UML de la API • Figura 4.10 – Esquema modular del servidor WSO2 IoT • Figura 4.11 – Diagrama UML de JsonUtils • Figura 4.12 – Interfaz de gestión de la API • Figura 4.13 – Diagrama UML de myRaspberryManagerService • Figura 4.14 – Diagrama UML de myRaspberryManager • Figura 4.15 – Diagrama de Secuencia del Agente • Figura 4.16 – Vista principal type-view • Figura 4.17 – Portal principal y sección devices • Figura 4.18 – Secciones del deviceType-view • Figura 4.19 – Vista de la gráfica de datos
xvii ÍNDICE DE CÓDIGO • Código 4.1 – Fichero web.xml • Código 4.2 – Swagger definition – myRaspberryService.java • Código 4.3 – Recurso sensor – myRaspberryService.java • Código 4.4.1 – Método changeTime – myRaspberryService.java • Código 4.4.2 - Método changeTime – myRaspberryService.java • Código 4.4.3 - Método changeTime – myRaspberryService.java • Código 4.4.4 - Método changeTime – myRaspberryService.java • Código 4.5.1 – Clase jsonUtils – jsonUtils.java • Código 4.5.2 – Clase jsonUtils – jsonUtils.java • Código 4.5.3 – Clase jsonUtils – jsonUtils.java • Código 4.5.4 – Clase jsonUtils – jsonUtils.java • Código 4.6 – Método getType – myRaspberryManagerService.java • Código 4.7 – Constructor de myRaspberryManagerService – myRaspberryManagerService.java • Código 4.8 – Método getPushNotificationConfig – myRaspberryManagerService.java • Código 4.9 – Clase myRaspberryManager – myRaspberryManager.java • Código 4.10 – Método modifyEnrollment – myRaspberryManagerService.java • Código 4.11 – Método disenrollDevice – myRaspberryManagerService.java • Código 4.12 – Clase myRaspberryFeatureManager – myRaspberryFeatureManager.java • Código 4.13 – Método createFeature – myRaspberryFeatureManager.java • Código 4.14 – Método addFeature – myRaspberryFeatureManager.java • Código 4.15 – Método getFeature – myRaspberryFeatureManager.java • Código 4.16 – Fichero configure.sh • Código 4.17 – Configuración de rc de Linux – agentd.sh • Código 4.18 – Funciones start y stop – agentd.sh • Código 4.19 – Comprobaciones iniciales – agentScript.sh • Código 4.20 – Comprobación de paho.mqtt – agentScript.sh • Código 4.21 – Refresco del token – agentScript.sh • Código 4.22 – Clase IOTLogger – agent.py
• Código 4.23 – Función configureLogger – agent.py • Código 4.24 – Lanzamiento del hilo de gestión de MQTT – agent.py • Código 4.25 – Bucle principal – agent.py • Código 4.26 – Fichero IotUtils.py • Código 4.27 – Get y Set de valores de sensor y leds – iotUtils.py • Código 4.28 – Configuración de MQTT – mqttHandler.py • Código 4.29 – Instanciación y definición de callbacks de paho.mqtt – mqttHandler.py • Código 4.30 – Subscripción al topic MQTT – mqttHandler.py • Código 4.31 – Fichero config.json • Código 4.32 – UI del histórico de operaciones • Código 4.33 – Analytics-view.hbs • Código 4.34 – Analytics-view.js • Código 4.37 – Analytics-view.hbs, realtime • Código 4.36 – Analytics-view.js, realtime
xix Notación código Código de un fichero Inglés Palabra en inglés comando Comando de bash Fichero Nombre de fichero
1 1 INTRODUCCIÓN esde que en 1999 en el Instituto Tecnológico de Massachusetts (MIT) [1] se definiera el concepto de internet de las cosas, se han ido desarrollando distintas tecnologías destinadas a facilitar el desarrollo de software y estandarizar grandes aspectos del internet de las cosas, así como dispositivos cada vez más económicos y con mejores especificaciones. Como ejemplo de ello, podemos poner uno de los más famosos dispositivos, la Raspberry Pi, que desde que fuera lanzada en 2012 por la Fundación Raspberry Pi [2], con el objetivo de destinarlo a la enseñanza, ha pasado de tener un procesador single-core a 700 MHz (en la primera versión) a un procesador de nueva generación con 1,4GHz (en su versión 3). Que estos dispositivos tengan una gran compatibilidad con distintos sensores y puedan correr sistemas operativos basados en Linux, ha hecho que surja una gran comunidad en internet dedicada a seguir y publicar distintos proyectos que usen esta plataforma. Por otra parte, WSO2 [3], es una empresa especializada en software de código abierto, que está enfocada en facilitar la gestión de sistemas basados en SOA [4]. Entre sus productos de código abierto, se encuentra un servidor enfocado en la temática de IoT que nos otorga la flexibilidad necesaria para usarlo en la gestión y monitorización de dispositivos, entre ellos Raspberry Pi. Este sector se encuentra en auge, tiene una gran flexibilidad de opciones y un enorme espacio de aplicación para ayudar en el día a día de las personas. Estas y otras son las razones de ser de este trabajo de fin de grado, que se centra en el uso de dispositivos Raspberry Pi como plataforma a gestionar y monitorizar mediante un servidor proporcionado por WSO2. 1.1 Motivación Este trabajo de fin de grado, surge de la curiosidad propia por gestionar uno o múltiples dispositivos Raspberry Pi con algún sistema Front End, para lo que se ha elegido un sistema ya maduro e innovador como es WSO2. El caso de uso elegido es el de control remoto y en tiempo real de la temperatura de un entorno. Este sería aplicable tanto si se necesita poca precisión, como por ejemplo un hogar, como cuando se necesite un alto nivel de precisión en la medida, como sería por ejemplo la monitorización de la temperatura de un ser vivo. A pesar de que el alcance del proyecto quede limitado a este ámbito, cabe destacar que el resultado es totalmente exportable a otros tipos de sensores con un mínimo esfuerzo de desarrollo. D Nuestra recompensa se encuentra en el esfuerzo y no en el resultado. Un esfuerzo total es una victoria completa. - Mahatma Gandhi-
Introducción 2 2 1.2 Objetivos El objetivo a perseguir por este trabajo de fin de grado es la integración y gestión de un nuevo tipo de dispositivo Raspberry pi para el IoT Server de WSO2, así como el desarrollo de un agente Raspberry pi donde se implementará toda la lógica de sensores. El escenario de pruebas se situará en un entorno real en el hogar, donde el sensor pueda controlar durante un periodo de tiempo extenso la temperatura y mandar la información al servidor. También podremos controlar el dispositivo desde el servidor; reiniciándolo, configurando el tiempo de muestreo o apagando/encendiendo la matriz de leds. 1.3 Metodología Para el desarrollo del proyecto, vamos a necesitar un conjunto de elementos que vamos a enumerar y detallaremos en secciones posteriores. Para el código vamos a necesitar un IDE potente y multipropósito, ya que usaremos una gran variedad de lenguajes como Java, Javascript, Json, Handlebars y Python. Usaremos Java, Javascript y Json para la parte del servidor, ya que WSO2 está implementado en Java. Usaremos Javascript para tareas en la parte del cliente, mientras que Handlebars [4] es el esquema usado junto con Jaggery [5] por WSO2 para la interfaz de usuario del servidor. Python será usado en el módulo del agente en la Raspberry Pi, puesto que es un lenguaje a la vez potente y sencillo de aprender, así como multiplataforma y que cuenta con una extensa documentación y librerías, incluyendo las necesarias para la gestión de sensores. El proyecto constará de dos partes bien distinguidas, por un lado, el complemento a desarrollar en el servidor de WSO2 y en el otro lado, el agente Raspberry Pi, para el que se ha elegido una estructura multihilos. Uno de los hilos, el principal, se encargará del arranque, recolección de datos de sensores y su envío al servidor, mientras que un segundo hilo estará suscrito al topic MQTT, quedando a su cargo el tratar las operaciones entrantes y actuar en consecuencia. Para el proyecto hemos utilizado los siguientes elementos: • Raspberry Pi 2 ◦ Sistema Operativo: Raspbian 8 (Jessie) con Kernel: 4.1.7+. ◦ Entorno de ejecución: Terminal Unix de comandos Bash. ◦ Intérprete: Python 2.7.13 • Portátil Lenovo B590 – Servidor y entorno de desarrollo ◦ Sistema Operativo: Debian 9 (Stretch) con Kernel 4.9.110. ◦ Terminal avanzado: Guake con Tmux y zsh + powerline (con implementación de git) con atajos. ◦ Editor de texto: Vim, configuración avanzada con plugins (nerdtree, airline, emberhbs) ◦ IDE: Atom versión 1.29.0 1.4 Estructura de la Memoria Esta memoria se dividirá en 6 bloques, partiendo de esta misma introducción, pasando a descripción del sistema y herramientas, trabajo previo, trabajo realizado, pruebas y verificación, para terminar con las conclusiones finales. En el apartado 1, Introducción, trataremos información sobre el objetivo, motivo y alcance del proyecto. En el apartado 2, Descripción del sistema y herramientas, hablaremos brevemente del servidor, de las
3 tecnologías utilizadas en este proyecto, así como de las herramientas utilizadas en el desarrollo. En el apartado 3, Trabajo previo, se explicará aspectos de la configuración de la Raspberry pi, sus sensores y del servidor WSO2 IoT. En el apartado 4, Desarrollo de la solución, se analizará en profundidad el código desarrollado y usado en el proyecto. En el apartado 5, Pruebas y validación, pasaremos a analizar las distintas pruebas de validación realizadas. En el apartado 6, Conclusiones finales, haremos un análisis final del proyecto y comentaremos posibles mejoras y desarrollos futuros.
Estado de la técnica y herramientas 4 4 2 ESTADO DE LA TÉCNICA Y HERRAMIENTAS l diseño de un sistema agente-servidor, con una plataforma como la desarrollada por WSO2 para el internet de las cosas, nos lleva a tener que usar una variedad bastante compleja de tecnologías y lenguajes de programación. En esta sección, recorreremos los distintos elementos que componen el servidor, también describiremos los métodos de interacción entre agente y servidor, así como varias tecnologías que nos serán de utilidad para apartados posteriores. Como ya se ha comentado, una parte principal en este proyecto, es la comunicación entre el agente y el servidor, pues de ahí sale la información útil para el servidor y las órdenes a ejecutar por el agente, esto se logra gracias a un conjunto de protocolos, librerías y estilos que son usados por WSO2 para ese fin. Queremos que el agente sea lo más simple posible, para poder ejecutarlo de forma segura en dispositivos con baja capacidad de procesamiento, mientras que el servidor tiene unas prestaciones mucho mayores, lo que lleva a que necesitemos un hardware más exigente. 2.1. Arquitectura del Servidor WSO2 IoT El servidor de WSO2 tiene unos requisitos mínimos bastante exigentes, a continuación, se muestran los requisitos necesarios para poder desplegarlo: • Memoria RAM: 8GB con 1024MB de tamaño de pila para Java • Procesador: Dual Core o superior. • Disco Duro: 5GB mínimo. Como vemos, las características nos llevan a buscar un hardware potente para ello. En el caso de este trabajo fin de grado, para simular un escenario real, se ha usado como plataforma para el servidor un portátil, que cuenta con suficiente capacidad; dado que no es el hardware más indicado para ejercer de servidor, se propone usar para un despliegue real un barebone [6] con las características siguientes: • 8Gb de RAM • 128Gb de SSD • Procesador I5 3317U • Rango de precios: 350 – 400€ • Wifi: Antena dual 802.11 b/g/n Figura 2.1 - Barebone La arquitectura del servidor WSO2 se muestra en la figura 2.2, donde podemos distinguir 4 bloques principales que detallaremos: E Una experiencia nunca es un fracaso, pues viene a demostrar algo. - Thomas Alva Edison -
5 • Connected Device Management Core – Núcleo de gestión de dispositivo conectado • API Layer – Capa de API • Security and Integration Layer – Capa de integración y seguridad • Interaction Layer – Capa de interacción Figura 2.2 – Arquitectura del servidor IoT de WSO2 2.1.1 Connected Device Management Core – Núcleo de Gestión de Dispositivo Conectado Es el núcleo del servidor, se encarga de controlar todas las funcionalidades que éste tiene. Es el cerebro del servidor y a su vez se divide en distintos módulos, que son totalmente extensibles comentaremos los que más relación tienen con este proyecto. Device Management – Gestión de Dispositivo Es el módulo encargado de registrar/eliminar los dispositivos en el servidor. Gestiona la información del dispositivo, como el estado. Device Type Management – Gestión de Tipo de Dispositivo Tiene la capacidad de añadir/retirar nuevos tipos de dispositivos en el servidor, permite describir el tipo de dispositivo, capacidades y operaciones que van a tener y/o realizar facilitando así la gestión de dispositivos por categorías. Para hacer uso de este módulo, tenemos el framework de gestión de dispositivos conectados (CDMF), escrito en java sobre el que se conectan los plugin de dispositivos, estos plugin son contenedores OSGi.
Estado de la técnica y herramientas 12 12 Maven define los Archetype o Arquetipos como la plantilla de un proyecto, esta plantilla, mediante la interacción con el usuario, genera el esqueleto de un proyecto funcional con las necesidades básicas del usuario. De esta forma, podemos partir de plantillas definidas con la estructura necesaria para así poder facilitar el desarrollo conforme a las necesidades del proyecto. Se pueden generar distintos arquetipos para Maven, tanto para proyecto completo, como para partes modulares del proyecto, un ejemplo de esto lo encontraremos en este Trabajo de Fin de Grado, donde se partirá de un arquetipo definido por WSO2. 2.3.5 OSGi – Open Service Gateway initiative Creado en marzo de 1999 con el objetivo de definir especificaciones de software para plataformas compatibles, inicialmente pensado para redes domésticas. OSGi define su propia arquitectura, que podemos observar en la figura 2.9. Figura 2.9 – Arquitectura OSGi OSGi es una capa que permite la creación bundles o componentes y que estos puedan comunicarse entre sí en tiempo de ejecución. En OSGi, cada componente (parecido a un jar tradicional + metadatos) se diferencia del resto por su propio classpath. La arquitectura OSGi está orientada a servicios donde estos pueden consumirse o registrarse dentro de la plataforma base OSGi ofrece una plataforma para la gestión de módulos en tiempo de ejecución, encargándose de la instalación, arranque, parada, actualización y la desinstalación sin necesitar parar la plataforma base. Finalmente, OSGi nos permite reducir la complejidad del desarrollo, pues se basa en los componentes que ocultan su contenido entre sí, comunicándose mediante servicios e interfaces bien definidas. También nos otorga la capacidad de reutilizar los componentes desarrollados, gracias a la independencia entre componentes.
13 3 TRABAJO PREVIO Cabe hacer especial hincapié en esta parte, tal vez sea con diferencia la parte más pesada y dura del proyecto, pues requiere de hacer una gran labor de investigación tanto de la documentación oficial del proyecto del servidor de IoT como del código contenido en los repositorios oficiales del proyecto en GitHub. 3.1 Preparación de Raspberry Pi Lo primero a realizar desde el punto de vista de la Raspberry Pi, es investigar toda la documentación existente para ser capaz de interconectar el sensor y la matriz de leds, utilizando los pines de entrada/salida existentes en todo modelo de Raspberry Pi, para ello nos vamos a valer de los datasheet de cada dispositivo y de la documentación oficial de Raspberry Pi. La Raspberry Pi a usar es la versión 2, cabe destacar que todo lo realizado con esta versión, es compatible con otros modelos de Raspberry Pi, puesto que los pines que usamos son iguales en todos los modelos y la arquitectura del dispositivo se mantiene. Los periféricos a usar en el proyecto consistirán en un sensor DS18B20 (modelo acuático) y una matriz de leds 8x8 con un driver MAX7219. Estos periféricos son económicos y fácilmente accesibles en el mercado, también cuenta con una comunidad a sus espaldas que ha desarrollado librerías para Python. En internet podemos encontrar el datasheet de estos elementos y esquemas para el cableado necesario. En la siguiente figura (figura 3.1) podemos ver el aspecto de los periféricos a usar. Figura 3.1 – Sensor de temperatura DS18B20 y matriz de leds con driver MAX7219 3.1.1 Conexión a la Raspberry Pi La Raspberry Pi cuenta con unos pines de entrada y salida conocidos como GPIO, estos pines sirven para interconectar, alimentar y comunicarse con una gran variedad de periféricos electrónicos. Algunos de estos pines son para alimentación 3,3v y otros a 5v. También tiene pines para la comunicación SPI o I2C. A continuación (figura 3.2) mostramos el conjunto de pines que incluye la Raspberry Pi 2, que aumenta considerablemente con respecto a su modelo anterior.
Trabajo Previo 14 14 Figura 3.2 – GPIO de Raspberry Pi A continuación (figura 3.3), veremos el esquema de cableado para conectar los periféricos de interés a la Raspberry Pi, sabiendo que la matriz de leds, usa los pines de SPI y el sensor de temperatura necesita una resistencia de pull-up de 4,7KOhmios, además, usará el pin GPIO4 aparte de la alimentación y tierra. Figura 3.3 – Conexión de los periféricos 3.1.2 Pines Utilizados Vamos a enumerar y a nombrar para qué sirven los distintos pines que hemos usado en la conexión de los dos elementos a la Raspberry Pi. Para conectar el sensor de temperatura, hemos usado 3 pines: • VCC – Alimentación a 3,3V. • GND – Conexión a tierra. • GPIO4 – Es el pin por defecto para dispositivos 1wire [9]. Para poder usar la matriz de leds y el driver MAX7219, hemos realizado la siguiente conexión: • VCC – Alimentación a 5V. • GND – Conexión a tierra. • DIN (Data IN) - Conexión de Serial Peripheral Interface (SPI), se conecta al pin Master Output, Slave Input (MOSI). • CS (Channel Selection) - Selección de esclavo, corresponde con el pin GPIO8 (CEO#)
15 • SCLK – Es el pin que marca la sincronización entre el master y el esclavo en SPI. 3.1.3 Configuración Software Una vez que tenemos la configuración del hardware, es necesario que activemos los distintos pines que van a ser usados mediante software, ya que, por defecto, tanto SPI como 1Wire vienen desactivados. Para lograrlo, tenemos que modificar el fichero /boot/config.txt para activarlos al iniciar el dispositivo. Sudo vim /boot/config.txt Añadimos las siguientes líneas al fichero dtoverlay=w1-gpio #Activamos 1Wire dtparam=spi=on #Activamos SPI Con esto ya solo nos queda comprobar al iniciar el dispositivo que se cargan los módulos necesarios: lsmod | grep w1-gpio #Comprobamos que se ha cargado el módulo 1Wire. ls /dev/spidev0.0 #Si existe el fichero, se ha cargado el módulo. 3.2 Servidor WSO2 de IoT utilizado Se ha utilizado la versión 3.3.0 del servidor IoT de WSO2, el servidor viene comprimido en formato zip y se puede descargar gratuitamente (https://wso2.com/iot/install/download/?type=downloader) bajo licencia Apache 2.0. El servidor viene divido en 3 partes, el núcleo Carbon está en el directorio /bin/, el servidor de analytics se encuentra en wso2/analytics/ y para finalizar, el bróker de MQTT está contenido en wso2/broker/, cada uno es un componente del servidor que se ejecuta de forma separada. También tenemos varios directorios de interés para este proyecto: • Sample: Trae proyectos de ejemplo, como connectedcup que es una implementación para una cafetería, el cual me sirve de manera extensa para analizar el funcionamiento y los distintos componentes de la implementación en el servidor. • Scripts: Varios scripts para cambiar la IP del servidor, que por defecto usa como dirección IP de escucha localhost y también para cambiar las credenciales del superusuario del servidor. • Bin: Scripts de ejecutables del servidor: o iot-server.sh: Script de arranque del servidor Carbon de WSO2, véase Anexo I. o broker.sh: Script de arranque del bróker MQTT, véase Anexo II. o analytics.sh: Script de arranque del servidor de análisis de datos, Véase Anexo III. 3.2.1 Análisis del servidor Por defecto, el servidor se ejecuta para la IP local y los puertos 9443 (consola de gestión de dispositivo), 9445 (DAS) y 9446 (Message Broker). Se deben ejecutar en el siguiente orden: primero el script bin/broker.sh después el núcleo del servidor mediante bin/iot-server.sh y finalmente el servidor de análisis de datos bin/analytics.sh. Para poder cambiar la IP del servidor, disponemos de un script bajo el directorio scripts que nos permite cambiar todos los ficheros de configuración XML y JSON necesarios dentro del proyecto. Script change-ip.sh El script de Bash, nos pide por línea de comando los valores necesarios siguientes:
Trabajo Previo 16 16 • IP para ser sobrescrita en los ficheros de configuración, es la misma IP que actualmente tiene configurado el servidor, cabe destacar que la primera vez, este valor será localhost. • IP que queremos configurar como la del servidor a partir de la ejecución, es necesario tener en cuenta que esta IP debe estar configurada en alguna interfaz. Podemos usar un hostname en vez de una IP, siempre que configuremos el fichero hosts: 127.0.0.1 localhost.localdomain localhost x.y.z.w machinename.domain machinename • Valores para el nuevo certificado autofirmado SSL: o Common name: IP/Hostname o País o Localización o Organización o Correo El script se encarga de modificar los ficheros de configuración de los distintos módulos del servidor 3.2.2 Servidor de Análisis de Datos, DAS: El análisis de datos se refiere al módulo del servidor IoT que se dedica a la agregación o recolección de datos, análisis o manipulación y presentación visual de los resultados obtenidos de los datos. La arquitectura del servidor de análisis de datos es como sigue, con el flujo de datos representado: Figura 3.4 – Arquitectura DAS
17 El servidor DAS, se puede configurar mediante el fichero XML contenido en wso2/analytics/conf/carbon.xml en él vienen definidos parámetros como el puerto, parámetros de autenticación como el contenedor de certificados, tiempo máximo de un hilo para ejecutarse, véase el anexo IV. 3.2.3 Servidor bróker de mensajes Un bróker de mensajería es el encargado de distribuir los mensajes publicados a un topic a las entidades subscritas a él, también tiene la capacidad de traducir mensajes de un sistema a otro, ya sea con varios lenguajes, mediante un medio de comunicación entre sistemas. Permite el intercambio de mensajes de forma sencilla, se encarga de la comunicación con los agentes y de gestionar los topics de MQTT para la comunicación, apreciemos a continuación la arquitectura del servidor de bróker de mensajes. Al igual que en el caso del DAS, la configuración está contenida en el fichero: wso2/broker/conf/carbon.xml. Figura 3.5 – Esquema del bróker de mensajería 3.2.4 Consola de administración de dispositivos Es la interfaz de usuario para la gestión de dispositivos, para acceder a ella una vez se ha arrancado el servidor, hay que acceder a la dirección IP configurada que llamaremos <DirIP> y en concreto a la URL: https://<DirIP>:9443 La pantalla que nos recibe se puede apreciar en la siguiente figura, donde se nos requiere las credenciales de usuario.
Trabajo Previo 18 18 Figura 3.6 – Portal de bienvenida Se accede introduciendo las credenciales, que por defecto son admin/admin, pero que obviamente y por motivos de seguridad deberían ser cambiadas a una contraseña con mayor seguridad. 3.2.5 Autenticación de dispositivos Para autenticar dispositivos en el servidor, se utiliza el estándar OAuth[18], que está definido para permitir flujo de datos entre dos entidades, donde una es el proveedor de un servicio y la otra la consumidora, garantizando la protección de la comunicación. Esta autenticación, se realiza mediante tokens, estos son cadenas de caracteres generados por un servidor de autenticación (en nuestro caso, la labor de servidor de autenticación la ejerce el servidor WSO2 IoT.), para ello primero necesitamos tener unas credenciales, en la figura 3.7 podemos distinguir el esquema para la autenticación.
19 Figura 3.7 – Esquema de autenticación Como podemos observar en la figura anterior, para generar el token de acceso necesitamos las credenciales del gestor o entidad responsable. El procedimiento obtenerlo es el siguiente: • Hay que codificar en base64 las credenciales del gestor, para ello lo hacemos con el siguiente comando: echo -n <GESTOR>:<PASSWORD> | base64 • A continuación, es necesario obtener el Client ID y el Secret ID, esta combinación es proporcionada por el servidor de autenticación, mediante una petición http, para ello necesitamos pasarle al servidor las credenciales codificadas del gestor y esto lo logramos usando el siguiente comando: curl -k -X POST https://<IOTS_HOST>:8243/api-application- registration/register -H 'authorization: Basic <BASE 64 ENCODED GESTOR:PASSWORD>' -H 'content-type: application/json' -d '{ "applicationName":"appName", "tags":["device_management"]}' • Necesitamos repetir el proceso de codificación, ahora con Client ID y Secret ID proporcionados por el servidor de autenticación. echo -n <Client ID>:<Secret ID> | base64 • Por último, tenemos que generar el token de acceso mediante una petición http, donde debemos indicar los scopes a los que solicitamos acceso, por ejemplo “perm:myRaspberry:enroll”. Estos scopes están definidos en la API desarrollada (myRaspberryService.java). Usaremos la cabecera http “Authorization” para proporcionarle al servidor de autenticación el par ClientID, SecretID codificado. Con el siguiente comando logramos el acceso para toda la API desarrollada: curl -v -k -d "grant_type=password&username=<USERNAME>&password=<PASS> &scope= perm:myRaspberry:enroll perm:devices:view perm:devices:delete" - H "Authorization: Basic <BASE 64 ENCODED CLIENT_ID:CLIENT_SECRET>" -H "Content-Type: application/x-www-form-urlencoded" https://<IOTS_HOST>:8243/token
Trabajo Previo 20 20 3.3 GitHub del servidor IoT de WSO2 Una gran cantidad de trabajo recae en la necesidad de entender los métodos de las clases usadas en el proyecto, una vez entendidas las clases, la labor de desarrollo del plugin sobre ellas se hace una tarea menos faraónica. En esta sección, describiremos las clases y métodos usados por las clases implementadas, su labor y su funcionalidad y el porqué de su uso, así como un análisis más profundo del código de las mismas. 3.4 Esquema de red Para dar mayor flexibilidad al sistema, se ha tenido en cuenta tanto el uso de una red de ámbito local, como el uso de Internet, esto es posible dado que MQTT usa un sistema de encriptación, que nos asegura que la comunicación entre agente y servidor sea lo más segura posible, evitando que pueda ser fácilmente desencriptada por terceros. Podemos recurrir a un sistema de DNS si queremos usar el servidor mediante nombre de dominio, lo que nos otorgaría escalabilidad al sistema. Por otro lado, podríamos usarlo en un entorno cerrado y totalmente aislado que es lo que hemos hecho para el entorno del proyecto, haciendo uso de un router como en la figura, modelo Asus AC RT1200G+. Figura 3.8 – Esquema local Por otro lado, la máquina utilizada para desarrollo, ha sido un portátil Lenovo con las siguientes características: • SSD 128GB • 8GB de memoria RAM • Intel I3-3110 @ 2,4Ghz • HDD 500GB • Tarjeta Wifi dual band. • Distribución Debian Stretch Que ha hecho las veces de barebone, durante las pruebas y desarrollo. Resultado de autenticación, operaciones Autenticación (Auth-token), Datos de sensor
21 4 DESARROLLO DE LA SOLUCIÓN 4.1 Creación de la estructura del proyecto El ámbito del proyecto abarca los desarrollos en el servidor y en la Raspberry. Desde el punto de vista del servidor necesitamos desarrollar el plugin de Carbon, la API y el módulo de Interfaz de Usuario y desde el lado de los dispositivos, necesitamos desarrollar el agente, tal y como vemos en la figura 4.1. Figura 4.1 – Diagrama de despliegue del proyecto Usaremos Maven y su utilidad de arquetipos para generar la estructura y el esqueleto del proyecto que posteriormente procederemos a ampliar y desarrollar según las necesidades del mismo, esto es posible, ya que WSO2 define unos arquetipos con los que los desarrolladores pueden trabajar sin tener que crear desde cero el proyecto, facilitando así el desarrollo de nuevas funcionalidades y plugins de dispositivos. El diagrama de clases del proyecto, incluyendo plugin y API es el que vemos en la figura 4.2.
Desarrollo de la solución 28 28 Como ya hemos visto en apartados anteriores, la API es una capa de abstracción entre la entidad gestora y otros módulos software, dando formato y comprobando los parámetros, así como representando la salida. Especificando para el caso de WSO2, la API controla y gestiona el uso del plugin del dispositivo, siendo así la API el punto de entrada para operar con él, tal y como podemos observar en la siguiente figura: Figura 4.10 – Esquema modular del servidor WSO2 IoT Para desarrollar la API, hacemos uso del directorio y del esqueleto ya generado gracias al arquetipo, esto nos otorga por un lado la carpeta webapp, en ella se encuentran distintos tipos de ficheros que no se quieren servir al usuario y que tienen alguna utilidad en el servidor, como por ejemplo ficheros de configuración, etc. Se dividen en la carpeta WEB-INF y META-INF. Veamos el fichero de configuración web.xml dentro de WEB-INF: <web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
29 version="2.5" xmlns="http://java.sun.com/xml/ns/javaee" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd" metadata-complete="true"> <display-name>WSO2 IoT Server</display-name> <description>WSO2 IoT Server</description> <servlet> <servlet-name>CXFServlet</servlet-name> <servletclass>org.apache.cxf.transport.servlet.CXFServlet</servlet-class> <load-on-startup>1</load-on-startup> </servlet> <servlet-mapping> <servlet-name>CXFServlet</servlet-name> <url-pattern>/*</url-pattern> </servlet-mapping> <context-param> <param-name>doAuthentication</param-name> <param-value>true</param-value> </context-param> <context-param> <param-name>isSharedWithAllTenants</param-name> <param-value>false</param-value> </context-param> <!--publish to apim--> <context-param> <param-name>managed-api-enabled</param-name> <param-value>true</param-value> </context-param> </web-app> Código 4.1 – Fichero web.xml Se ha generado mediante Apache CXF, que nos proporciona ayuda a la hora de desplegar y desarrollar APIs de Front-end como por ejemplo JAX-RS, que es el caso que nos atañe. Para ver más a fondo los ficheros xml contenidos en el directorio webapp véase el anexo V. Tenemos también el directorio src/main/java/sensorboard/raspberry/api donde está el código de la API que hace uso de swagger y JAX-RS (API de java que proporciona métodos y clases para desarrollo de APIs Rest), las dos clases principales son las ya comentadas con anterioridad. Veamos la interfaz myRaspberryService en el fichero myRaspberryService.java la importancia de esta interfaz, reside en que usa la notación swagger para definir las peticiones HTTP a tratar y cómo van a ser tratadas. myRaspberryService.java: Como ya hemos indicado, se definen las distintas opciones y parámetros de la API, por un lado, definimos el scope, que necesitamos a la hora de generar el token de acceso, mediante la notación @scope y por otro, definimos cómo se va a tratar una llamada; con @path definimos la URI a la que se asocia. Lo primero que debemos hacer es definir el contexto sobre el que se va a agrupar el resto de las definiciones de funcionalidades, como vemos en el código 4.2. @SwaggerDefinition( info = @Info( version = "1.0.0", title = "myRaspberry",
Desarrollo de la solución 30 30 extensions = { @Extension(properties = { @ExtensionProperty(name="name",value=myRaspberryConstants. DEVICE_TYPE), @ExtensionProperty(name="context",value="/"+myRaspberryConstants. DEVICE_TYPE), }) } ), tags = { @Tag(name = myRaspberryConstants.DEVICE_TYPE+",device_management", description = "") } ) @Scopes( scopes = { @Scope( name = "Enroll device", description = "", key = "perm:" + myRaspberryConstants.DEVICE_TYPE + ":enroll", permissions = {"/device-mgt/devices/enroll/" + myRaspberryConstants.DEVICE_TYPE } ) } ) Código 4.2 – Swagger Definition – myRaspberryService.java La clase myRaspberryConstants contiene una serie de constantes que van a ser usadas por todo el código. Una de esas constantes es DEVICE_TYPE (véase Anexo VI) que tiene el nombre del dispositivo desarrollado (myRaspberry). Haciendo uso de este valor, tal y como vemos en el código 4.2, definimos la raíz para las peticiones a la API como la concatenación de la constante DEVICE_TYPE / versión. Particularizando sería: “/myRaspberry/1.0.0” donde “1.0.0” corresponde al campo version definida en el código 4.2 como la versión de la API, gracias a esto, podríamos implementar varias versiones de una API y distinguirlas. Posteriormente, se crea la interfaz myRaspberryService para implementarla con la clase myRaspberryServiceImpl. Los distintos recursos que va a tener disponible la API se van a implementar también con notación Swagger para APIs. Analicemos en el código 4.3 la implementación de uno de estos recursos, como es “sensor” que sigue una implementación lo suficientemente genérica para entender el resto y su valor indica el tiempo de muestreo del sensor de temperatura. /** * @param deviceId unique identifier for given device type instance * @param state new time value */ @Path("device/{deviceId}/sensor") @POST @ApiOperation( consumes = MediaType.APPLICATION_JSON, httpMethod = "PUT", value = "Tiempo de muestreo del sensor", notes = "", response = Response.class, tags = DeviceTypeConstants.DEVICE_TYPE , extensions = { @Extension(properties = {
31 @ExtensionProperty(name = SCOPE, value = "perm:" + DeviceTypeConstants.DEVICE_TYPE + ":enroll") }) } ) Response changeTime(@PathParam("deviceId") String deviceId, @QueryParam("tiempo") int state, @Context HttpServletResponse response); Código 4.3 – Recurso “sensor” – myRaspberryService.java Como podemos apreciar en el código 4.3, gracias a @ApiOperation estamos definiendo el tratamiento por parte de la API que va a hacer de la petición entrante http/https. En el caso a tratar, vemos que debe ser una petición con un método PUT, Json como parámetros y por último que el @path equivale a la URI a donde deben ser dirigidas las peticiones, en cuyo caso es (“device/{deviceId}/sensor”). Este esquema es el mismo para el resto de métodos de la interfaz, pero el método downloadSketch cambia totalmente el funcionamiento ya que no es convencional, pues devuelve un fichero zip con el agente, pero también registra el dispositivo en el servidor: @Path("/device/download") @GET @Produces("application/zip") myRaspberryServiceImpl.java: Es la clase que implementa la previamente comentada interfaz myRaspberryService desarrollando los métodos que introduce la interfaz para darle funcionalidad real a las peticiones de la API. Esta implementación es el código encargado de gestionar las peticiones entrantes al servidor, que tienen como objetivo (URI) la API que vamos a crear. Se encarga de invocar las acciones necesarias y devolver el resultado deseado, entre las acciones que realiza se encuentra usar el bróker MQTT para invocar cambios en los agentes registrados en el servidor. Analicemos el método introducido anteriormente changeTime: public Response changeTime(@PathParam("deviceId") String deviceId, @QueryParam("tiempo") int time, @Context HttpServletResponse response) {//throws Exception { try { //Vemos si está autorizado el usuario para el dispositivo if (!APIUtil.getDeviceAccessAuthorizationService().isUserAuthorized(new DeviceIdentifier(deviceId, DeviceTypeConstants.DEVICE_TYPE))) { return Response.status(Response.Status.UNAUTHORIZED.getStatusCode()).build(); } Código 4.4.1 – Método ChangeTime – myRaspberryServiceImpl.java Lo primero que hacemos en el código 4.4.1 es comprobar si la entidad que está llamando a la API está autorizado a realizar esta llamada, en caso de que no lo estuviera, se devolvería un código de error http 401 (Unauthorized). //Vemos si el string introducido por el usuario está vacío o no. if( time < 0 || time == 0 ) { log.error("The requested time value for the sensor retrieving must be >0"); return Response.status(Response.Status.BAD_REQUEST.getStatusCode()).build(); } //Configuremos la operación para mandarla Map<String, String> dynamicProperties = new HashMap<>(); String publishTopic =
Desarrollo de la solución 32 32 APIUtil.getAuthenticatedUserTenantDomain() + "/" + DeviceTypeConstants.DEVICE_TYPE + "/command"; dynamicProperties.put(DeviceTypeConstants.ADAPTER_TOPIC_PROPERTY, publishTopic); Operation commandOp = new CommandOperation(); commandOp.setCode("change-time"); commandOp.setType(Operation.Type.COMMAND); commandOp.setEnabled(true); commandOp.setPayLoad("timeRequest:"+Integer.toString(time)); Código 4.4.2 – Método changeTime – myRaspberryServiceImpl.java En el código 4.4.2 pasamos a hacer comprobaciones de los parámetros de entrada, si el nuevo valor de tiempo es menor o igual que 0 procederemos a devolver un error http 400 (Bad Request), en caso contrario, seleccionamos el topic MQTT sobre el que vamos a enviar las peticiones a los agentes y almacenamos la operación. //Método propio que almacena los comandos try { JsonUtils.saveOperation("Cambiar Tiempo del Sensor",deviceId,"Tiempo introducido:" + Integer.toString(time)); }catch(Exception e) { System.out.println("ERROR" + e.toString()); } Código 4.4.3 – Método changeTime – myRaspberryServiceImpl.java Se ha creado la clase JsonUtils que almacena en un Json un histórico de las operaciones realizadas, para poder luego mostrarlas en la interfaz de usuario. //Configuramos MQTT Properties props = new Properties(); props.setProperty("mqtt.adapter.topic", publishTopic); commandOp.setProperties(props); Dejamos configuradas las propiedades necesarias de MQTT. List<DeviceIdentifier> deviceIdentifiers = new ArrayList<>(); deviceIdentifiers.add(new DeviceIdentifier(deviceId, DeviceTypeConstants.DEVICE_TYPE)); //Aqui metemos la operación a la lista de operaciones para ser usadas, a través de git hub podemos //ver las distintas clases involucradas en https://github.com/wso2/carbondevice-mgt/ APIUtil.getDeviceManagementService().addOperation(DeviceTypeConstants.DEV ICE_TYPE, commandOp, deviceIdentifiers); return Response.ok().build(); } catch (DeviceAccessAuthorizationException e) { log.error(e.getErrorMessage(), e); return Response.status(Response.Status.INTERNAL_SERVER_ERROR).build(); } catch (OperationManagementException e) { String msg = "Error occurred while executing command operation upon changing the sensor time";
33 log.error(msg, e); return Response.status(Response.Status.INTERNAL_SERVER_ERROR).build(); } catch (InvalidDeviceException e) { String msg = "Error occurred while executing command operation upon changing the sensor time"; log.error(msg, e); return Response.status(Response.Status.BAD_REQUEST).build(); } } Código 4.4.4 – Método changeTime – myRaspberryServiceImpl.java Por último, usamos la clase DeviceManagementService, disponible en la librería de WSO2, para encolar las operaciones. Hay que tener en cuenta que esta clase está implementada por WSO2, para tener más información sobre el proceso de encolado y almacenamiento de operaciones ha hecho falta una inversión de tiempo y esfuerzo recorriendo el repositorio en GitHub que contiene el código de las clases heredadas. En el caso de que todo haya sido satisfactorio, devolvemos un código http 200 (OK), indicando que todo ha sido correcto a la entidad que haya realizado la petición a la API. JsonUtils.java: Esta clase usa la librería Gson de Google para crear un Json y al igual que deviceOperationJson ha sido desarrollada desde 0 para almacenar las distintas operaciones que el usuario realice, posteriormente este Json será servido y mostrado a través de la interfaz de usuario en forma de histórico de operaciones realizadas. Esta clase usa a su vez la clase deviceOperationJson que define las características y métodos de la estructura de datos. Figura 4.11 – Diagrama UML de JsonUtils Veamos el código de la clase: public class JsonUtils { private static String jsonPath = "repository/deployment/server/jaggeryapps/devicemgt/sampleData.json"; private static File fileJson = new File( jsonPath ); private static JsonObject jsonObject = new JsonObject(); Código 4.5.1 – Clase jsonUtils – jsonUtils.java En el código 4.5.1, observamos las variables de la clase, son las necesarias para crear o modificar el fichero Json, como por ejemplo fileJson, que es el descriptor del fichero Json. public static void saveOperation ( String operationId, String deviceId, String payload ){ /* * Vamos a configurar la variable pendingDeviceOperation la cual va a almacenar los datos
Desarrollo de la solución 34 34 * de la última operación enviada al dispositivo identificado por deviceId * */ deviceOperationJson pendingDeviceOperation = new deviceOperationJson(); Gson gson = new Gson(); String jsonString; pendingDeviceOperation.setName(operationId); pendingDeviceOperation.setDate(); pendingDeviceOperation.setPayload(payload); Código – 4.5.2 Clase jsonUtils – jsonUtils.java Con el código 4.5.2 creamos una nueva instancia de la clase deviceOperationJson que representa a la operación a almacenar y le damos valores. // Si el fichero json no existe lo inicializamos y metemos la nueva (y unica) operación, // si el fichero existe, tenemos que parsear las operaciones existente y almacenar // en la del deviceId if (fileJson.exists()) { try { //Creamos un parser, que nos va a servir para leer el fichero JSON existente, //con el vamos a leer como un jsonElement, sin determinar que tipo json va a ser Path path = new File(jsonPath).toPath(); Reader reader = Files.newBufferedReader(path, StandardCharsets.UTF_8); JsonParser parser = new JsonParser(); JsonElement jsonElement = parser.parse(new FileReader( jsonPath )); //Pasamos todo el contenido del fichero json (en jsonElement) a JsonObject, //el cual nos da métodos para poder usarlo. jsonObject = jsonElement.getAsJsonObject(); //Comprobamos si la estructura json tiene un miembro ya existente del //deviceId que deseamos añadir. if ( !jsonObject.has(deviceId) ) { //Si no tiene un miembro del dispositivo que ha ejecutado la operación, //entonces necesitamos crear y añadir el nuevo miembro y luego añadirle //la operación. FileWriter writer = new FileWriter(jsonPath); //Las operaciones se guardan y se leen del fichero como JsonArray ArrayList<deviceOperationJson> operationsArrayList = new ArrayList<deviceOperationJson>( ); operationsArrayList.add(pendingDeviceOperation); //Pasamos el array a la estructura JSON y lo añadimos al fichero leido y //almacenado en jsonObject jsonObject.add(deviceId, gson.toJsonTree(operationsArrayList)); //Gson nos proporciona el siguiente método para pasar de estructura
35 JSON //a un string y así poder escribirlo en el fichero jsonString = gson.toJson(jsonObject); writer.write(jsonString); writer.close(); } Código 4.5.3 – Clase jsonUtils – jsonUtils.java En el código 4.5.3 comprobamos si existe el fichero Json, en caso de que exista, vamos a recorrerlo y leerlo para comprobar si existe ya un elemento con el id del dispositivo que ha generado la operación. En caso de que no existiera, lo crearíamos y añadiríamos a la lista. else { //Hemos comprobado que el dispositivo ya tiene una estructura creada //en el fichero JSON, lo cual nos evita tener que crearla de nuevo, //solo tenemos que seleccionar en un array el campo de JSON que tenga //por nombre el deviceId. deviceOperationJson[] operationArray = gson.fromJson(jsonObject.get(deviceId),deviceOperationJson[].class ); //Pasamos a un arraylist, ya que el array nos limita a un tamaño fijo. ArrayList<deviceOperationJson> operationsArrayList = new ArrayList<deviceOperationJson>(Arrays.asList( operationArray )); //Al ArrayList de todas las operaciones del dispositivo que queremos //le añadimos la operación nueva que queremos almacenar y sobreescribimos //el campo del dispositivo en el fichero JSON. operationsArrayList.add(pendingDeviceOperation); jsonObject.add(deviceId, gson.toJsonTree(operationsArrayList)); jsonString = gson.toJson(jsonObject); FileWriter writer = new FileWriter(jsonPath); writer.write(jsonString); writer.close(); } Código 4.5.3 – Clase jsonUtils - jsonUtils.java En caso de que ya exista (código 4.5.3), necesitamos almacenar la lista de operaciones de ese dispositivo, añadirle el nuevo y por último sobreescribir en el fichero. } catch (Exception e1) { e1.printStackTrace(); } } //Si el fichero no existe, debemos llamar a la función que lo crea y luego añadirle el //dispositivo, esto ocurre solamente la primera vez, cuando se inicia todo el sistema, //tras enrolar un dispositivo y ejecutar una operación. else { try {
Desarrollo de la solución 36 36 fileJson.createNewFile(); //JsonObject object = new JsonObject(); FileWriter writer = new FileWriter(jsonPath); ArrayList<deviceOperationJson> operationsArrayList = new ArrayList<deviceOperationJson>( ); operationsArrayList.add(pendingDeviceOperation); //object.add(deviceId, gson.toJsonTree(operationsArrayList)); jsonObject.add(deviceId, gson.toJsonTree(operationsArrayList)); //String jsonArray = gson.toJson(jsonObject); jsonString = gson.toJson(jsonObject); writer.write(jsonString); writer.close(); }catch (Exception e) { e.printStackTrace(); } } } Código 4.5.4 – Clase jsonUtils - jsonUtils.java En caso de que no exista el fichero (código 4.5.4), primero procederemos a crearlo, para luego añadirle la primera operación del dispositivo que la ha ejecutado. Finalmente, tenemos disponible una interfaz de gestión para la API del dispositivo, donde vemos las distintas llamadas posibles y la URI para invocarlas. Figura 4.12 – Interfaz de gestión de la API 4.3.2 Plugin del servidor WSO2 IoT Como ya hemos indicado con anterioridad, un plugin en nuestro caso es un paquete OSGi que se conecta con el CDMF del servidor WSO2. Permite desarrollar un nuevo tipo de dispositivo en el servidor e interconectarlo con el framework de dispositivo conectado (CDMF) y así tener control sobre los distintos agentes registrados (por ejemplo, permitir que se registren, borrarlos, definir funcionalidades, etc) y para gestionar los datos enviados por los agentes.
37 Cuando el servidor WSO2 se inicia, se registran en el CDMF todos los tipos de dispositivos o categorías de servicios que existen en el servidor gracias a OSGi. OSGi viene implementado en WSO2 para que los reconozca en tiempo de ejecución. Para que nuestro tipo de dispositivo sea reconocido por OSGi necesitamos que implemente una interfaz OSGi. Con este objetivo hemos desarrollado la clase myRaspberryManagerService, esta clase define como se maneja e interacciona con el tipo de dispositivo al que hace referencia, extendiendo también a la interfaz desarrollada por WSO2 con este proposito: org.wso2.carbon.device.mgt.common.spi.DeviceManagementService (https://github.com/wso2/carbon-device-mgt/tree/master/components/devicemgt/org.wso2.carbon.device.mgt.common/src/main/java/org/wso2/carbon/device/mgt/common/spi) Figura 4.13 – Diagrama UML de myRaspberryManagerService myRaspberryManagerService.java: Analicemos a continuación los métodos más interesantes de la clase myRaspberryManagerService: public class myRaspberryManagerService implements DeviceManagementService { private DeviceManager deviceManager; private OperationMonitoringTaskConfig operationMonitoringTaskConfig; @Override public String getType() { return DeviceTypeConstants.DEVICE_TYPE; } Código 4.6 – Método getType – myRaspberryManagerService.java En el código 4.6 vemos el método getType() que sirve para devolver el tipo del dispositivo, almacenado en la clase myRaspberryConstants que contiene las constantes. @Override public void init() throws DeviceManagementException { this.deviceManager = new myRaspberryManager(); this.operationMonitoringTaskConfig = new
Desarrollo de la solución 44 44 la matriz de leds. Podemos dividir el agente en tres partes claramente diferenciadas: • Fichero de configuración del dispositivo: Sigue un estilo de fichero ini, que se caracterizan por organizarse según una categoría entre corchetes, como pares de la forma parámetro=valor. Veamos el fichero deviceConfig.properties, este fichero, es tomado como plantilla por la API para generar la configuración inicial de un agente a la hora de generar el fichero zip, leyendo las marcas y modificándolas en función de lo que corresponda. Por otro lado, el agente lee el fichero y se configura según los valores que tenga o los modifica si fuera necesario, como pasa con leds-state y push-interval. El sistema de autenticación entre agente-servidor como ya hemos visto, se hace mediante un token generado previamente, este token tiene un tiempo de vida y un token de refresco que se usa para volver a generarlo. De esta forma logramos autenticar al agente y una comunicación segura entre el agente y el servidor, siendo toda esta comunicación cifrada extremo-extremo. [Device-Configurations] owner={DEVICE_OWNER} deviceId={DEVICE_ID} device-name={DEVICE_NAME} server-name={SERVER_NAME} controller-context=/myRaspberry device-type=myRaspberry mqtt-ep={MQTT_EP} https-ep={HTTPS_EP} auth-method=token auth-token={DEVICE_TOKEN} refresh-token={DEVICE_REFRESH_TOKEN} push-interval=15 application-key={API_APPLICATION_KEY} leds-state=OFF • Scripts de arranque del agente y el script del servicio: Por un lado, tenemos tres scripts en Bash con funcionalidades distintas: o configure.sh: se crea con el objetivo de configurar de forma autónoma el script de servicio agentd.sh, el cual define el arranque de servicios en los distintos niveles de ejecución. Este script toma el valor del directorio actual y lo configura en el script de servicio agentd,sh, luego copia en el lugar de destino el script del servicio y configura el rc de Linux, encargado de gestionar la lógica de scripts de servicio. #!/bin/bash PATH_DIR=`echo $PWD/$0 | sed 's/configure.sh//g'` sed -i "s+PATH_DIR+$PATH_DIR+g" agentd.sh sudo cp ./agentd.sh /etc/init.d/ sudo update-rc.d agentd.sh defaults Código 4.16 – Fichero configure.sh o agentd.sh: Este script sigue el estándar de creación de scripts de servicio de Linux, los cuales son usados para definir la ejecución del servicio en los distintos niveles de ejecución del sistema operativo. Para ello se usa tras el shebang una línea como podemos ver en el código 4.17, en la que se define los niveles de ejecución de Linux en los que se arranca y aquellos en los que se para. Se ha definido el valor 5 para su arranque (cuando arranca totalmente el sistema) y para el resto, que esté parado. Esto con prioridad 90, aquellos scripts de servicio con mayor prioridad serán arrancados antes que él.
45 ### BEGIN INIT INFO # Provides: agentd # Required-Start: # Required-Stop: # Default-Start: 90 5 # Default-Stop: 20 0 1 2 3 4 6 # Short-Description: agentd initscript # Description: Script de servicio para el agente de WSO2 IoT ### END INIT INFO Código 4.17 – Cónfiguración de rc de Linux – agentd.sh Se han creado las funciones start y stop, para que arranquen el servicio o lo paren y así tener control sobre el agente, tal y como vemos en el código 4.18. # Start the service agent start() { ### Create the lock file ### if [ -e /var/lock/subsys/agentScript ]; then echo "[ERROR] The agent is already initialized" else cd PATH_DIR sleep 60 && sudo PATH_DIR/agentScript.sh & touch /var/lock/subsys/agentScript fi } # Stop the service stop() { if [ -e /var/lock/subsys/agentScript ]; then PROCESS_PID=`ps aux | grep agentScript.sh | head -n 1 | awk '{print $2}'` PYTHON_PID=`ps aux | grep agent.py | head -n 1 | awk '{print $2}'` sudo kill $PROCESS_PID 2&>/dev/null sudo kill $PYTHON_PID 2&>/dev/null ### Now, delete the lock file ### rm -f /var/lock/subsys/agentScript else echo "[ERROR] The agent is not initialized" fi } ### main logic ### case "$1" in start) start ;; stop) stop ;; restart|reload|condrestart) stop start ;; *) echo $"Usage: $0 {start|stop|restart|reload|status}" exit 1 esac exit 0 Código 4.18 – Funciones start y stop – agentd.sh
Desarrollo de la solución 46 46 ◦ agentScript.sh: Este script en Bash tiene toda la lógica de inicialización del agente, primero comprueba si existe la librería paho.mqtt, en caso de que no exista la descarga, tras esto refresca el token de acceso y finalmente copia el fichero de configuración en la carpeta del código en Python para seguidamente lanzar el script agent.py. currentDir=$PWD LOG_FILE="agent.log" for f in ./deviceConfig.properties; do ## Check if the glob gets expanded to existing files. ## If not, f here will be exactly the pattern above ## and the exists test will evaluate to false. if [ -e "$f" ]; then CURRENT_DAY=`date +"%Y-%m-%d"` CURRENT_TIME=`date +"%H:%M:%S"` echo "$CURRENT_DAY $CURRENT_TIME,000 INFO Configuration field found $f" >> $LOG_FILE else CURRENT_DAY=`date +"%Y-%m-%d"` CURRENT_TIME=`date +"%H:%M:%S"` echo "$CURRENT_DAY $CURRENT_TIME,000 ERROR 'deviceConfig.properties' file does not exist in current path. Exiting installation" >> $LOG_FILE; exit; fi ## We can exit the loop, there is a file and it is not empty. break done Código 4.19 – Comprobaciones iniciales – agentScript.sh En el código 4.19, vemos la configuración de log, así como la comprobación de que el fichero de configuración existe. PAHO_DIR="./paho.mqtt.python" if [ ! -d $PAHO_DIR ]; then CURRENT_DAY=`date +"%Y-%m-%d"` CURRENT_TIME=`date +"%H:%M:%S"` echo "$CURRENT_DAY $CURRENT_TIME,000 INFO Cloning into directory the MQTT paho git project" >> $LOG_FILE #install mqtt dependency if the paho directory does not exists. git clone https://github.com/eclipse/paho.mqtt.python.git cd ./paho.mqtt.python sudo python setup.py install fi cd $currentDir Código 4.20 – Comprobación de paho.mqtt – agentScript.sh En el código 4.20 se descarga la librería de eclipse paho.mqtt, que es necesaria para gestionar en el agente en Python las operaciones sobre MQTT. REFRESH_TOKEN=`cat ./deviceConfig.properties | grep refresh| awk -F "=" '{print $2}'` DEVICE_ID=`cat ./deviceConfig.properties | grep deviceId | awk -F "=" '{print $2}'` BASIC_ENCODED=`cat ./deviceConfig.properties | grep application | awk -F "=" '{print $2}'` DEVICE_TYPE=`cat ./deviceConfig.properties | grep device-type | awk -F "="
47 '{print $2}'` SERVER=`cat ./deviceConfig.properties | grep https-ep | awk -F "=" '{print $2}'| awk -F "/" '{print $3}' | awk -F ":" '{print $1}'` AUTH_TOKEN=`curl --silent -k -d "grant_type=refresh_token&refresh_token=$REFRESH_TOKEN&scope=device_type_ $DEVICE_TYPE device_$DEVICE_ID" -H "Authorization: Basic $BASIC_ENCODED" -H "Content-Type: application/x-www-form-urlencoded" https://$SERVER:9443/oauth2/token | awk -F "," '{print $1}'| awk -F ":" '{print $2}' | sed 's/\"//g'` sed -i "s/auth-token=.*/auth-token=$AUTH_TOKEN/g" deviceConfig.properties Código 4.21 – Refresco del token – agentScript.sh A continuación, tal y como vemos en el código 4.21, se refresca el token de autenticación en caso de que hubiera expirado. Por último, ejecuta el agente en Python. ./src/agent.py --log $LOG_FILE • Agente en Python: Es realmente el agente que incorpora toda la lógica de sensores, de control de MQTT, de gestión de fichero de configuración, podemos observar su complejidad en el diagrama de secuencia siguiente: Figura 4.15 – Diagrama de Secuencia del Agente Para facilitar la explicación, podemos dividir el agente en Python en cuatro bloques principales: ◦ Módulo principal: Tras realizar la inicialización del sistema de log, pasa a un bucle infinito donde se queda leyendo el sensor y mostrando los valores en la matriz si esta estuviera configurada como encendida, a la vez que actualiza el valor tiempo de muestreo si se ha recibido una actualización a través de MQTT y el estado de los leds si se hubiera mandado por MQTT la orden de cambiar su estado. Esto se logra gracias a las variables globales ledsUpdate y sensorUpdate que valen 1 cuando se ha recibido una actualización de alguno. Veamos esas subtareas en el código:
Desarrollo de la solución 48 48 ▪ Gestionar el sistema de log: LOG_FILENAME = "agent.log" logging_enabled = True LOG_LEVEL = logging.INFO # Could be e.g. "DEBUG" or "WARNING" ledsUpdate = 0 sensorUpdate = 0 # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~ # A class we can use to capture stdout and sterr in the log # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~ class IOTLogger(object): def __init__(self, logger, level): """Needs a logger and a logger level.""" self.logger = logger self.level = level def write(self, message): if message.rstrip() != "": # Only log if there is a message (not just a new line) self.logger.log(self.level, message.rstrip()) Código 4.22 – Clase IOTLogger – agent.py def configureLogger(loggerName): logger = logging.getLogger(loggerName) logger.setLevel(LOG_LEVEL) # Set the log level to LOG_LEVEL handler = logging.handlers.TimedRotatingFileHandler(LOG_FILENAME, when="midnight", backupCount=3) # Handler that writes to a file, # ~~~make new file at midnight and keep 3 backups formatter = logging.Formatter( '%(asctime)s %(levelname)-8s %(message)s') # Format each log message like this handler.setFormatter(formatter) # Attach the formatter to the handler logger.addHandler(handler) # Attach the handler to the logger if (logging_enabled): sys.stdout = IOTLogger(logger, logging.INFO) # Replace stdout with logging to file at INFO level sys.stderr = IOTLogger(logger, logging.ERROR) # Replace stderr with logging to file at ERROR level Código 4.23 – Función configureLogger – agent.py ▪ Lanzar el hilo de gestión de MQTT (código 4.23), con esto se logra que de forma asíncrona cree otro hilo que se quede gestionando el topic MQTT para en caso de que venga una orden desde el servidor, actúe en consecuencia modificando los campos que sean necesarios (ledsstate o push-interval) en el fichero de configuración, que serán leídos por el bucle principal si su variable de actualización ha sido modificada. Se llama a esta función, una vez antes del bucle infinito. #
49 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~ # This is a Thread object for listening for MQTT Messages # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~ class ListenMQTTThread(object): def __init__(self): thread = threading.Thread(target=self.run, args=()) thread.daemon = True # Daemonize thread thread.start() # Start the execution def run(self): global sensorUpdate; global ledsUpdate; mqttHandler.main() Código 4.24 – Lanzamiento del hilo de gestión de MQTT – agent.py ▪ Bucle principal infinito (código 4.25), se encarga de actualizar las variables de estado del tiempo de muestreo y del estado de la matriz de leds en caso de que se hubiera modificado, leyendo los nuevos valores del fichero de configuración, lee el sensor, publica el valor en el topic MQTT y en caso de que estén activos los leds, lo muestra en la matriz. Finalmente espera el tiempo que pongamos de muestreo, antes de reiniciar el bucle. def main(): configureLogger("agent") ListenMQTTThread() device = led.matrix() Instancia la matriz de leds, usando la librería max7219. while True: try: if sensorUpdate == 1: iotUtils.PUSH_INTERVAL = iotUtils.getPushValue() PUSH_INTERVAL = iotUtils.PUSH_INTERVAL sensorUpdate = 0 if ledsUpdate == 1: iotUtils.LEDS_STATE = iotUtils.getLedsValue().upper() LEDS_STATE = iotUtils.LEDS_STATE ledsUpdate=0 currentTime = calendar.timegm(time.gmtime()) sensorValue = iotUtils.getSensorValue() if( LEDS_STATE == "ON" ): device.show_message(str(sensorValue)) PUSH_DATA_TO_STREAM_1 = iotUtils.SENSOR_STATS_SENSOR1.format(currentTime, sensorValue) mqttHandler.sendSensorValue(PUSH_DATA_TO_STREAM_1) print '~~~~~~~~~~~~~~~~~~~~~~~~ Publishing Device-Data ~~~~~~~~~~~~~~~~~~~~~~~~~' print ('PUBLISHED DATA STREAM 1: ' + PUSH_DATA_TO_STREAM_1) print '~~~~~~~~~~~~~~~~~~~~~~~~ End of Publishing Data ~~~~~~~~~~~~~~~~~~~~~~~~~' time.sleep(PUSH_INTERVAL) Código 4.25 – Bucle principal – agent.py ▪ En caso de que se interrumpa la ejecución, llama al script de servicio para pararlo. except (KeyboardInterrupt, Exception) as e: print "agentStats: Exception in AgentThread (either
Desarrollo de la solución 50 50 KeyboardInterrupt or Other)" print ("agentStats: " + str(e)) os.system("sudo /etc/init.d/agentd.sh stop") print '~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~' sys.exit(0) pass ◦ Fichero iotUtils.py (código 4.26): Encargado de toda la lógica de sensores. Lee y almacena las variables del fichero de configuración: import ConfigParser import os import random from w1thermsensor import W1ThermSensor # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~ # Device specific info when pushing data to server # Read from a file "deviceConfig.properties" in the same folder level # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~ configParser = ConfigParser.RawConfigParser() configFilePath = os.path.join(os.path.dirname(__file__), './deviceConfig.properties') configParser.read(configFilePath) DEVICE_OWNER = configParser.get('Device-Configurations', 'owner') DEVICE_ID = configParser.get('Device-Configurations', 'deviceId') DEVICE_NAME = configParser.get('Device-Configurations', 'device-name') DEVICE_TYPE = configParser.get('Device-Configurations', 'device-type') SERVER_NAME = configParser.get('Device-Configurations', 'server-name') MQTT_EP = configParser.get('Device-Configurations', 'mqtt-ep') AUTH_TOKEN = configParser.get('Device-Configurations', 'auth-token') PUSH_INTERVAL = float(configParser.get('Device-Configurations','pushinterval')) LEDS_STATE = configParser.get('Device-Configurations','leds-state') CONTROLLER_CONTEXT = configParser.get('Device-Configurations', 'controller-context') DEVICE_INFO = '{"owner":"' + DEVICE_OWNER + '","deviceId":"' + DEVICE_ID + '",' HTTPS_EP = configParser.get('Device-Configurations', 'https-ep') DEVICE_DATA = '"sensorValue":"{sensorValue}"' SENSOR_STATS_SENSOR1 = '{{"event":{{"metaData":{{"owner":"' + DEVICE_OWNER + '","deviceType":"' + DEVICE_TYPE \ + '","deviceId":"' + DEVICE_ID + '","time":{}}},"payloadData":{{"temperature":{:.2f}}}}}}}' Código 4.26 – Fichero IotUtils.py Por otro lado, también definimos, en el código 4.27, funciones para obtener o cambiar el valor de variables como por ejemplo el estado de los leds, el tiempo de muestreo del sensor o leer el sensor, hay que tener presente que se usa la librería w1thermsensor para leer el sensor (ver Anexo VII). def getPushValue(): configParserAux = ConfigParser.RawConfigParser()
51 configFilePathAux = os.path.join(os.path.dirname(__file__), './deviceConfig.properties') configParserAux.read(configFilePathAux) PUSH_INTERVAL = float(configParser.get('Device-Configurations','pushinterval')) return PUSH_INTERVAL def setPushValue ( value ): cfgfile = open(os.path.join(os.path.dirname(__file__), './deviceConfig.properties'),'w') configParser.set('Device-Configurations','push-interval',value) configParser.write(cfgfile) cfgfile = open(os.path.join(os.path.dirname(__file__), '../deviceConfig.properties'),'w') configParser.set('Device-Configurations','push-interval',value) configParser.write(cfgfile) return True def getLedsValue(): configParserAux = ConfigParser.RawConfigParser() configFilePathAux = os.path.join(os.path.dirname(__file__), './deviceConfig.properties') configParserAux.read(configFilePathAux) LEDS_STATE = configParser.get('Device-Configurations','leds-state') return LEDS_STATE def setLedsValue ( value ): cfgfile = open(os.path.join(os.path.dirname(__file__), './deviceConfig.properties'),'w') configParser.set('Device-Configurations','leds-state',value) configParser.write(cfgfile) cfgfile = open(os.path.join(os.path.dirname(__file__), '../deviceConfig.properties'),'w') configParser.set('Device-Configurations','leds-state',value) configParser.write(cfgfile) return True def getSensorValue (): sensor = W1ThermSensor() temperature = sensor.get_temperature() return temperature Código 4.27 – Get y Set de valores de sensor y leds – iotUtils.py • mqttHandler.py: Gestión de MQTT por parte del agente, como ya hemos visto, este módulo, se ejecuta en un hilo separado, con una ejecución independiente del bucle principal, la interacción entre los mismos se realiza a través del fichero de configuración, que es modificado por uno y leído por otro (bucle principal). Cuando se ejecuta, se llama a la función main que veremos a en los siguientes códigos, esta función, se encarga primero de configurar las variables de MQTT (véase el código 4.28), como el topic de subscripción para recibir peticiones del servidor, o al que mandar los datos del sensor de temperatura. MQTT_ENDPOINT = iotUtils.MQTT_EP.split(":") MQTT_IP = MQTT_ENDPOINT[1].replace('//', '') MQTT_PORT = int(MQTT_ENDPOINT[2]) global TOPIC_TO_SUBSCRIBE TOPIC_TO_SUBSCRIBE = TANENT_DOMAIN + "/" + DEV_TYPE + "/command" global TOPIC_TO_PUBLISH_STREAM1 TOPIC_TO_PUBLISH_STREAM1 = TANENT_DOMAIN + "/" + DEV_TYPE + "/" + DEV_ID + "/temperature"
Desarrollo de la solución 52 52 Código 4.28 – Configuración de MQTT – mqttHandler.py Tras esto, en el código 4.29 se instancia el cliente MQTT de la librería paho.mqtt definiendo las funciones callback para cuando se conecta con el topic y cuando recibe un mensaje desde el topic subscrito. global mqttClient mqttClient.username_pw_set(iotUtils.AUTH_TOKEN, password="") mqttClient.on_connect = on_connect mqttClient.on_message = on_message Código 4.29 – Instanciación y definición de callbacks de paho.mqtt – mqttHandler.py Acto seguido se conecta con la función on_connect, tal y como vemos en el código 4.30. mqttClient.connect(MQTT_IP, MQTT_PORT, def on_connect(mqttClient, userdata, flags, rc): print("MQTT_LISTENER: Connected with result code " + str(rc)) # Subscribing in on_connect() means that if we lose the connection and # reconnect then subscriptions will be renewed. print ("MQTT_LISTENER: Subscribing with topic " + TOPIC_TO_SUBSCRIBE) mqttClient.subscribe(TOPIC_TO_SUBSCRIBE) Código 4.30 - Subscripción al topic MQTT – mqttHandler.py Finalmente se ejecuta un bucle infinito del cliente MQTT. mqttClient.loop_forever() Cuando se recibe un mensaje, la función encargada de gestionarlo, es la definida como callback on_message, esta función lee el mensaje recibido, separando el método recibido de los parámetros. def on_message(mqttClient, userdata, msg): print("MQTT_LISTENER: " + msg.topic + " " + str(msg.payload)) #Let's split the message content for further operations method,parameter=str(msg.payload).split(":",1); Según el método, ejecutamos una acción u otra, los métodos posibles a recibir por el servidor, son los configurados durante la creación de la API: ◦ ledsRequest: petición de encender o apagar la matriz de leds, activamos la flag para indicar al bucle principal. if method == "ledsRequest" : print("Received a request to turn ON/OFF the led matrix") iotUtils.LEDS_STATE = iotUtils.setLedsValue(parameter) ledsUpdate = 1 ◦ TimeRequest: petición de cambiar el tiempo de muestreo, activamos la flag para indicar al bucle principal. elif method == 'timeRequest': print("Received a request to change the sensor push time") iotUtils.PUSH_INTERVAL = iotUtils.setPushValue(parameter) sensorUpdate = 1 ◦ Reboot: Se ha recibido una petición de reiniciar el dispositivo. elif method == "reboot" : print("Received a request to reboot the device") os.system("sleep "+parameter) os.system("reboot") ◦ Shutdown: Se ha recibido una petición para apagar el dispositivo.
53 elif method == "shutdown" : print("Received a request to shutdown the device") os.system("sleep "+parameter) os.system("shutdown now") ◦ Bash: Se ha recibido un comando en Bash, esta funcionalidad es experimental y debe de tenerse cuidado al usarla, pues se tiene acceso a todo el sistema de fichero del dispositivo. elif method == "bash": print("Received a bash command") if "_" in parameter: command, arguments = str( parameter ).split("_", 1) if "_" in arguments: parsedArguments = arguments.replace("_", " ") else: parsedArguments = arguments os.system( command + " " + parsedArguments ) else: os.system( command ) Por último, las funciones usadas por agent.py para el envío de datos al servidor, hacen uso de la librería paho.mqtt y de la clase instanciada y es como sigue: def on_publish(mqttClient, stream1PlayLoad): mqttClient.publish(TOPIC_TO_PUBLISH_STREAM1, stream1PlayLoad) def sendSensorValue(stream1PlayLoad): global mqttClient on_publish(mqttClient, stream1PlayLoad) 4.3.4 Interfaz de usuario, UI La interfaz de usuario del servidor WSO2 de IoT está construida usando el framework de interfaz de usuario unificado (UUF [19]), que permite de forma fácil y modular el desarrollo de interfaz de usuario. UUF está desarrollado sobre Jaggery, desde el punto de vista de Carbon es una aplicación más de Jaggery. Para desarrollar sobre él, es necesario hacer uso de handlebars. La estructura de directorio es la vista con anterioridad, dentro de la carpeta app/units, que contiene las unidades de interfaz de usuario. Cada unidad de interfaz de usuario debe contener obligatoriamente una vista handlebars, un fichero de configuración json y de manera opcional un controlador en javascript. Las unidades de interfaz de usuario que tenemos son: • Type-view: Es la unidad que se muestra cuando se accede a la lista de tipos de dispositivos, muestra una descripción del dispositivo, permite descargar el fichero zip con el código del agente (lo que también lo registra mediante la llamada a la API a downloadSketch). ◦ Device-view.hbs: Definimos en handlebars la estructura de y el código de lo que será la interfaz de usuario y su representación. ◦ Type-view.json: configuración, indicando la versión (1.0.0) vamos a obviarlo para el resto de vistas, ya que el contenido es igual. ◦ private/config.json: Guarda la configuración de las operaciones definidas en la API, podemos definir cosas como el tipo de icono, el tipo de objeto, el valor que tienen y el nombre, veamos el código 4.31. { "deviceType": { "label": "myRaspberry", "category": "iot", "downloadAgentUri": "myRaspberry/device/download",
Desarrollo de la solución 60 60 Figura 4.19 – Vista de la gráfica de datos • Realtime.analytics-view: Es la página encargada de crear y dar formato a la gráfica en tiempo real que se muestra en device-view. Volvemos a encontrarnos con la misma estructura, por un lado, la vista, el controlador y el fichero Json de configuración que es igual a los anteriores. También tenemos un directorio public, que contiene los ficheros javascript implementando la librería Rickshaw [20] (que proporciona un conjunto de herramientas para el diseño de gráficas) para la gráfica en tiempo real. ◦ analytics-view.hbs (código 4.35): Se encarga de mostrar la gráfica generada por los ficheros javascript contenidos en el directorio public. {{unit "cdmf.unit.lib.rickshaw-graph"}} <div id="div-chart-sensorType1" datawebsocketurlStream="{{websocketEndpointForStream1}}"> <div class="chartWrapper" id="chartWrapper"> <divid="yAxisSensorType1" class="custom_y_axis">Temperatura</div> <div class="legend_container"> <div id="smoother" title="Smoothing"></div> <div id="legend"></div> </div> <div id="chartSensorType1" class="custom_rickshaw_graph"></div> <div class="custom_x_axis">Tempo</div> </div> </div> <a class="padding-left" href="{{@app.context}}/device/{{device.type}}/analytics?deviceId={{device .deviceIdentifier}} &deviceName={{device.name}}"> <span class="fw-stack"> <i class="fw fw-ring fw-stack-2x"></i> <i class="fw fw-statistics fw-stack-1x"></i> </span> Ver Estadisticas Del Dispositivo </a> <!-- /statistics --> {{#zone "bottomJs"}} {{js "js/moment.min.js"}} {{js "js/socket.io.min.js"}}
61 {{js "js/device-stats.js"}} {{/zone}} Código 4.35 – Analytics-view.hbs, realtime. ◦ analytics-view.js: Es el controlador de la vista anterior, retorna el dispositivo y el socket para la comunicación de los datos del sensor. function onRequest(context) { var log = new Log("stats.js"); var carbonServer = require("carbon").server; var device = context.unit.params.device; var devicemgtProps = require("/app/modules/conf-reader/main.js")["conf"]; var constants = require("/app/modules/constants.js"); var websocketEndpoint = devicemgtProps["wssURL"].replace("https", "wss"); var jwtService = carbonServer.osgiService( 'org.wso2.carbon.identity.jwt.client.extension.service.JWTClientManagerSe rvice'); var jwtClient = jwtService.getJWTClient(); var encodedClientKeys = session.get(constants["ENCODED_TENANT_BASED_WEB_SOCKET_CLIENT_CREDENTIALS "]); var token = ""; var user = session.get(constants.USER_SESSION_KEY); if (!user) { log.error("User object was not found in the session"); throw constants.ERRORS.USER_NOT_FOUND; } if (encodedClientKeys) { var tokenUtil = require("/app/modules/oauth/token-handler- utils.js")["utils"]; var resp = tokenUtil.decode(encodedClientKeys).split(":"); if (user.domain == "carbon.super") { var tokenPair = jwtClient.getAccessToken(resp[0], resp[1], context.user.username , "default", {}); if (tokenPair) { token = tokenPair.accessToken; } var websocketEndpointForStream1 = websocketEndpoint + "/securedwebsocket/org.wso2.iot.devices.temperature/1.0.0?" + "deviceId=" + device.deviceIdentifier + "&deviceType=" + device.type + "&websocketToken=" + token; } else { var tokenPair = jwtClient.getAccessToken(resp[0], resp[1], context.user.username + "@" + user.domain, "default", {}); if (tokenPair) { token = tokenPair.accessToken; } var websocketEndpointForStream1 = websocketEndpoint + "/securedwebsocket/t/" + user.domain + "/org.wso2.iot.devices.temperature/1.0.0?" + "deviceId=" + device.deviceIdentifier + "&deviceType=" + device.type + "&websocketToken=" + token; } } return {"device": device, "websocketEndpointForStream1": websocketEndpointForStream1};} Código 4.36 – Analytics-view.js, realtime. public/device-stats.js: La implementación de la librería Rickshaw mediante el uso de websockets (tecnología que permite abrir socket entre cliente y servidor sobre la tecnología de capa 7).
Desarrollo de la solución 62 62
63 5 PRUEBAS Y VALIDACIÓN En esta sección, se va a exponer el sistema de pruebas desarrollado con la intención de comprobar el correcto funcionamiento del sistema y todos sus componentes (API, agente, Interfaz de usuario, etc). Dado el carácter software del proyecto, es necesario invertir una gran cantidad de tiempo y esfuerzo con el objetivo de comprobar las funcionalidades del mismo y en caso de que hubiera, la corrección de errores. Esta sección se dividirá en varios apartados, por un lado, la interfaz de usuario y por el otro, la API y el comportamiento del agente para lo que se ha desarrollado un sistema independiente para realizar las pruebas, ya que el funcionamiento de ambos está muy relacionado. Seguiremos el siguiente esquema para detallar las pruebas realizadas: Módulo API / Agente / Interfaz de Usuario Objetivo Lo que se quiere probar Descripción Resumen de lo que realiza la prueba Resultado Para las pruebas se ha generado un entorno aislado, con el siguiente esquema de red: 5.1 Verificación de la Interfaz de Usuario Dado el alcance limitado del proyecto, se ha decidido no desarrollar un entorno de pruebas específico para la interfaz de usuario, sin embargo, se ha hecho la verificación de forma manual y visual, dando por supuesto que el servidor WSO2 IoT es un producto lo suficientemente maduro como para no tener que probar su funcionamiento, teniéndonos que centrar en los componentes carbon desarrollados. Router + Punto de acceso 802.11 IP: 192.168.3.0/24 Servidor WSO2 IoT Agente Raspberry Pi
Pruebas y validación 64 64 5.1.1 Verificación de la vista type-view Módulo Interfaz de Usuario Objetivo Botón “ver la API” Descripción Se comprueba el correcto funcionamiento del botón, viendo que redirecciona a la página que contiene información relativa a la API del dispositivo. Resultado Exitoso Módulo Interfaz de Usuario Objetivo Botón “Descargar Agente” Descripción Se comprueba el funcionamiento añadiendo una palabra de más de 3 caracteres y haciendo clic para descargar. Resultado Exitoso Módulo Interfaz de Usuario Objetivo Botón “Descargar Agente” Descripción Se comprueba el funcionamiento añadiendo una palabra de menos de 3 caracteres y haciendo clic para descargar, lo que debe ser erróneo. Resultado Exitoso Módulo Interfaz de Usuario Objetivo Botón “Descargar Agente” Descripción Se comprueba el funcionamiento añadiendo una palabra de más de 3 caracteres y usando la tecla intro, en vez de hacer clic. Resultado Exitoso 5.1.2 Verificación de la vista device-view Módulo Interfaz de Usuario Objetivo Gráfica en tiempo real
65 Descripción Se ve los valores que la gráfica va mostrando después de un periodo de 30 segundos. Se compara con los valores enviados, almacenados en los logs del agente. Resultado Exitoso para Firefox, fallido para Chromium, no se actualiza. Módulo Interfaz de Usuario Objetivo Operación “Cambiar tiempo del sensor” Descripción Se da valores correctos de tiempo, al sensor, mayor que 0, positivo y numérico. Se hace clic en “Send to device” Resultado Exitoso Módulo Interfaz de Usuario Objetivo Operación “Cambiar tiempo del sensor” Descripción Se da valores incorrectos de tiempo, al sensor, negativos o letras. Se hace clic en “Send to device” Resultado Exitoso, no permite esos valores. Módulo Interfaz de Usuario Objetivo Operación “Cambiar tiempo del sensor” Descripción Se da valores correctos de tiempo, al sensor, mayor que 0, positivo y numérico. Se usa la tecla intro para enviar. Resultado Fallido: WSO2 IoT Server no reconoce la tecla intro como forma de enviar. Módulo Interfaz de Usuario Objetivo Operación “Matriz Leds on/off” Descripción Se da valores correctos (on y off) y se usa el botón “Send to device” Resultado Exitoso Módulo Interfaz de Usuario
Pruebas y validación 66 66 Objetivo Operación “Matriz Leds on/off” Descripción Se da valores incorrectos (distintos de on y off) y se usa el botón “Send to device” Resultado Exitoso, no permite esos valores Módulo Interfaz de Usuario Objetivo Operación “Matriz Leds on/off” Descripción Se da valores correctos (de on y off) y se usa la tecla intro Resultado Fallido: WSO2 IoT Server no reconoce la tecla intro como forma de enviar. Módulo Interfaz de Usuario Objetivo Operación “Mandar orden” Descripción Se prueba cada opción disponible Resultado Exitoso Módulo Interfaz de Usuario Objetivo Histórico de operaciones Descripción Se comprueban que todas las operaciones anteriormente realizadas aparecen. Resultado Exitoso 5.1.3 Verificación de la vista analytics-view Módulo Interfaz de Usuario Objetivo Histórico de datos Descripción Se deja el sistema encendido durante un día entero, almacenando datos y se pasa a comprobar el filtrado de datos y si los muestra correctamente. Resultado Exitoso
67 Conclusión: El servidor WSO2 IoT presenta un bug a la hora de enviar una operación usando la tecla intro, así como a la hora de mostrar la gráfica en tiempo real en ciertos exploradores como Chromium. 5.2 Verificación de la API y del agente Dada la complejidad para verificar tanto las llamadas a la API como el funcionamiento del agente, se ha decidido crear un proyecto en el lenguaje Perl, que nos aporta una gran flexibilidad a la hora de analizar ficheros de texto, así como buscar patrones, pero el factor decisivo para decidirnos por este lenguaje es la incorporación de módulos nativos para pruebas y verificación, como sería Test::Simple[21] y Test::More[22]. El proyecto de prueba se ha diseñado y estructurado de la siguiente forma: • Directorio config: Se incluyen ficheros de configuración, siguiendo un estilo INI ya visto anteriormente, en nuestro caso, tenemos el fichero config.ini con los siguientes valores: [WSO2] IP= USER= ADMIN= [RPI] NAME= IP= deviceId= La sección WSO2 describe las variables relativas al servidor, como su dirección IP, su usuario y su contraseña. La sección RPI, presenta los valores del agente, hacia el que se dirigirán las peticiones http. • Directorio cmd: Contiene los módulos principales de las pruebas, así como el script encargado de incluir módulos necesarios y lanzar los test definidos en un fichero de extensión .t: o Fichero core.pl: Contiene la lógica básica de las pruebas ▪ generate_token(): Que negocia con el servidor el token de autenticación para obtener los permisos de llamada a la API del dispositivo. ▪ check_body(): Comprueba que el Json a usar para las peticiones http tiene los campos que le corresponde según el tipo de petición. ▪ launch_test(): Es el encargado de realizar la petición http mediante curl y comprobar el resultado de la misma o realizar otro tipo de operaciones, como comprobaciones mediante Bash en la configuración del agente. o Fichero test.pl: Se llama con el test a realizar como único parámetro y se encarga de cargar los módulos, las definiciones de tests y por último a llamar a funciones del fichero core.pl. • Directorio definitions: Contiene las definiciones de los tests, estas definiciones se corresponden con los parámetros que va a tener la petición http, por ejemplo, para cambiar el tiempo de muestreo del sensor, es necesario que el verbo http sea POST, el ID del agente y el nuevo valor de tiempo. • Directorio test: En este directorio se engloban los ficheros de extensión .t que contienen la lógica de pruebas, por ejemplo, para registrar un nuevo agente en el servidor, incluye la petición correspondiente de la API y otras operaciones, como el despliegue del agente en una Raspberry PI. • Directorio scrips: En este directorio se incluyen scripts en Bash que realizan tareas específicas, como es el caso del script reset_server.sh encargado de redesplegar el servidor para realizar las pruebas en un entorno limpio. A continuación, veremos los resultados de las pruebas realizadas.
Pruebas y validación 68 68 5.2.1 Verificación del registro de agente En esta sección, se ha intentado comprobar el funcionamiento de la llamada a la API encargada de registrar en el CDMF un nuevo dispositivo. Módulo API Objetivo URI: /myRaspberry/1.0.0/device/download?deviceName=NOMBRE&sketchType=myRaspberry Descripción Se lanza la petición para registrar un nuevo dispositivo que a su vez generará un fichero .zip con el código del agente para el dispositivo Resultado Exitoso Módulo API Objetivo Fichero .zip con Código del agente Descripción Se intenta descomprimir el fichero del agente en la Raspberry PI con el objetivo de comprobar que es realmente un fichero .zip Resultado Exitoso 5.2.2 Verificación de la operación para cambiar tiempo del sensor Módulo API Objetivo URI: /myRaspberry/1.0.0/device/ID/sensor?tiempo=4 Descripción Se lanza una petición a la API para cambiar el tiempo de muestreo del sensor a 4 segundos Resultado Exitoso Módulo Agente Objetivo Tiempo de muestreo del sensor sea 4 en el agente Descripción Se conecta al agente y lee el campo del fichero de configuración que contiene el tiempo de muestreo del sensor, debe ser 4 segundos. Resultado Exitoso
69 Módulo API Objetivo URI: /myRaspberry/1.0.0/device/ID/sensor?tiempo=10 Descripción Se lanza una petición a la API para cambiar el tiempo de muestreo del sensor a 10 segundos Resultado Exitoso Módulo Agente Objetivo Tiempo de muestreo del sensor sea 10 en el agente Descripción Se conecta al agente y lee el campo del fichero de configuración que contiene el tiempo de muestreo del sensor, debe ser 10 segundos. Resultado Exitoso 5.2.3 Verificación de la operación para cambiar el estado de la matriz de leds Módulo API Objetivo URI: /myRaspberry/1.0.0/device/ID/leds?estado=on Descripción Se lanza una petición a la API para cambiar el estado de la matriz de leds a on. Resultado Exitoso Módulo Agente Objetivo Estado de la matriz sea on en el agente Descripción Se conecta al agente y lee el campo del fichero de configuración que contiene el estado de la matriz de leds, debe ser on. Resultado Exitoso
Anexos 76 76 fi elif [ "$c" = "--stop" ] || [ "$c" = "-stop" ] || [ "$c" = "stop" ]; then CMD="stop" elif [ "$c" = "--start" ] || [ "$c" = "-start" ] || [ "$c" = "start" ]; then CMD="start" elif [ "$c" = "--version" ] || [ "$c" = "-version" ] || [ "$c" = "version" ]; then CMD="version" elif [ "$c" = "--restart" ] || [ "$c" = "-restart" ] || [ "$c" = "restart" ]; then CMD="restart" elif [ "$c" = "--test" ] || [ "$c" = "-test" ] || [ "$c" = "test" ]; then CMD="test" else args="$args $c" fi done if [ "$CMD" = "--debug" ]; then if [ "$PORT" = "" ]; then echo " Please specify the debug port after the --debug option" exit 1 fi if [ -n "$JAVA_OPTS" ]; then echo "Warning !!!. User specified JAVA_OPTS will be ignored, once you give the --debug option." fi CMD="RUN" JAVA_OPTS="-Xdebug -Xnoagent -Djava.compiler=NONE - Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=$PORT" echo "Please start the remote debugging client to continue..." elif [ "$CMD" = "start" ]; then if [ -e "$CARBON_HOME/wso2carbon.pid" ]; then if ps -p $PID > /dev/null ; then echo "Process is already running" exit 0 fi fi export CARBON_HOME=$CARBON_HOME # using nohup sh to avoid erros in solaris OS.TODO nohup sh $CARBON_HOME/bin/iot-server.sh $args > /dev/null 2>&1 & exit 0 elif [ "$CMD" = "stop" ]; then export CARBON_HOME=$CARBON_HOME kill -term `cat $CARBON_HOME/wso2carbon.pid` exit 0 elif [ "$CMD" = "restart" ]; then export CARBON_HOME=$CARBON_HOME kill -term `cat $CARBON_HOME/wso2carbon.pid` process_status=0 pid=`cat $CARBON_HOME/wso2carbon.pid` while [ "$process_status" -eq "0" ]
77 do sleep 1; ps -p$pid 2>&1 > /dev/null process_status=$? done # using nohup sh to avoid erros in solaris OS.TODO nohup sh $CARBON_HOME/bin/iot-server.sh $args > /dev/null 2>&1 & exit 0 elif [ "$CMD" = "test" ]; then JAVACMD="exec "$JAVACMD"" elif [ "$CMD" = "version" ]; then cat $CARBON_HOME/bin/version.txt cat $CARBON_HOME/bin/wso2carbon-version.txt exit 0 fi # ---------- Handle the SSL Issue with proper JDK version ----------- --------- jdk_17=`$JAVA_HOME/bin/java -version 2>&1 | grep "1.[7|8]"` if [ "$jdk_17" = "" ]; then echo " Starting WSO2 Carbon (in unsupported JDK)" echo " [ERROR] CARBON is supported only on JDK 1.7 and 1.8" fi CARBON_XBOOTCLASSPATH="" for f in "$CARBON_HOME"/wso2/lib/xboot/*.jar do if [ "$f" != "$CARBON_HOME/wso2/lib/xboot/*.jar" ];then CARBON_XBOOTCLASSPATH="$CARBON_XBOOTCLASSPATH":$f fi done JAVA_ENDORSED_DIRS="$CARBON_HOME/wso2/lib/endorsed":"$JAVA_HOME/jre/l ib/endorsed":"$JAVA_HOME/wso2/lib/endorsed" CARBON_CLASSPATH="" if [ -e "$JAVA_HOME/lib/tools.jar" ]; then CARBON_CLASSPATH="$JAVA_HOME/lib/tools.jar" fi for f in "$CARBON_HOME"/bin/*.jar do if [ "$f" != "$CARBON_HOME/bin/*.jar" ];then CARBON_CLASSPATH="$CARBON_CLASSPATH":$f fi done for t in "$CARBON_HOME"/wso2/lib/commons-lang*.jar do CARBON_CLASSPATH="$CARBON_CLASSPATH":$t done # For Cygwin, switch paths to Windows format before running java if $cygwin; then JAVA_HOME=`cygpath --absolute --windows "$JAVA_HOME"` CARBON_HOME=`cygpath --absolute --windows "$CARBON_HOME"` AXIS2_HOME=`cygpath --absolute --windows "$CARBON_HOME"` CLASSPATH=`cygpath --path --windows "$CLASSPATH"` JAVA_ENDORSED_DIRS=`cygpath --path --windows "$JAVA_ENDORSED_DIRS"`
Anexos 78 78 CARBON_CLASSPATH=`cygpath --path --windows "$CARBON_CLASSPATH"` CARBON_XBOOTCLASSPATH=`cygpath --path --windows "$CARBON_XBOOTCLASSPATH"` fi # ----- Execute The Requested Command ------------------------------- ---------- echo JAVA_HOME environment variable is set to $JAVA_HOME echo CARBON_HOME environment variable is set to $CARBON_HOME cd "$CARBON_HOME" TMP_DIR=$CARBON_HOME/tmp if [ -d "$TMP_DIR" ]; then rm -rf "$TMP_DIR" fi START_EXIT_STATUS=121 status=$START_EXIT_STATUS #To monitor a Carbon server in remote JMX mode on linux host machines, set the below system property. # -Djava.rmi.server.hostname="your.IP.goes.here" while [ "$status" = "$START_EXIT_STATUS" ] do $JAVACMD \ -Xbootclasspath/a:"$CARBON_XBOOTCLASSPATH" \ -Xms256m -Xmx1024m -XX:MaxPermSize=512m \ -XX:+HeapDumpOnOutOfMemoryError \ -XX:HeapDumpPath="$CARBON_HOME/repository/logs/heap-dump.hprof" \ $JAVA_OPTS \ -Dcom.sun.management.jmxremote \ -classpath "$CARBON_CLASSPATH" \ -Djava.endorsed.dirs="$JAVA_ENDORSED_DIRS" \ -Djava.io.tmpdir="$CARBON_HOME/wso2/tmp" \ -Dcatalina.base="$CARBON_HOME/wso2/lib/tomcat" \ -Dwso2.server.standalone=true \ -Dcarbon.registry.root=/ \ -Djava.command="$JAVACMD" \ -Dcarbon.home="$CARBON_HOME" \ -Dlogger.server.name="IoT-Core" \ -Djava.util.logging.manager=org.apache.juli.ClassLoaderLogManager \ -Dcarbon.config.dir.path="$CARBON_HOME/conf" \ -Dcarbon.components.dir.path="$CARBON_HOME/wso2/components" \ -Dcarbon.dropins.dir.path="$CARBON_HOME/dropins" \ -Dcarbon.external.lib.dir.path="$CARBON_HOME/lib" \ -Dcarbon.patches.dir.path="$CARBON_HOME/patches" \ -Dcarbon.servicepacks.dir.path="$CARBON_HOME/servicepacks" \ -Dcarbon.internal.lib.dir.path="$CARBON_HOME/wso2/lib" \ -Djava.util.logging.config.file="$CARBON_HOME/conf/etc/loggingbridge.properties" \ -Dcomponents.repo="$CARBON_HOME/wso2/components/plugins" \
79 -Dconf.location="$CARBON_HOME/conf"\ - Dcom.atomikos.icatch.file="$CARBON_HOME/wso2/lib/transactions.properti es" \ -Dcom.atomikos.icatch.hide_init_file_path=true \ -Dorg.apache.jasper.compiler.Parser.STRICT_QUOTE_ESCAPING=false \ -Dorg.apache.jasper.runtime.BodyContentImpl.LIMIT_BUFFER=true \ -Dcom.sun.jndi.ldap.connect.pool.authentication=simple \ -Dcom.sun.jndi.ldap.connect.pool.timeout=3000 \ -Dorg.terracotta.quartz.skipUpdateCheck=true \ -Djava.security.egd=file:/dev/./urandom \ -Dfile.encoding=UTF8 \ -Djava.net.preferIPv4Stack=true \ -Dcom.ibm.cacheLocalHost=true \ -DworkerNode=false \ -Dorg.wso2.ignoreHostnameVerification=true \ -Dorg.opensaml.httpclient.https.disableHostnameVerification=true \ -Diot.analytics.host="localhost" \ -Diot.analytics.https.port="9445" \ -Diot.manager.host="localhost" \ -Diot.manager.https.port="9443" \ -Dmqtt.broker.host="localhost" \ -Dmqtt.broker.port="1886" \ -Diot.core.host="localhost" \ -Diot.core.https.port="9443" \ -Diot.keymanager.host="localhost" \ -Diot.keymanager.https.port="9443" \ -Diot.gateway.host="localhost" \ -Diot.gateway.https.port="8243" \ -Diot.gateway.http.port="8280" \ -Diot.gateway.carbon.https.port="9443" \ -Diot.gateway.carbon.http.port="9763" \ -Diot.apimpublisher.host="localhost" \ -Diot.apimpublisher.https.port="9443" \ -Diot.apimstore.host="localhost" \ -Diot.apimstore.https.port="9443" \ -Dmqtt.broker.https.port="9446" \ -Denable-api-scopes-sharing="true" \ org.wso2.carbon.bootstrap.Bootstrap $* status=$? done
Anexos 80 80 Anexo II: Script analytics.sh #!/bin/sh # analytics.sh # ------------------------------------------------------------------- --------- # Copyright 2016 WSO2, Inc. http://www.wso2.org # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. cygwin=false; darwin=false; os400=false; mingw=false; case "`uname`" in CYGWIN*) cygwin=true;; MINGW*) mingw=true;; OS400*) os400=true;; Darwin*) darwin=true if [ -z "$JAVA_VERSION" ] ; then JAVA_VERSION="CurrentJDK" else echo "Using Java version: $JAVA_VERSION" fi if [ -z "$JAVA_HOME" ] ; then JAVA_HOME=/System/Library/Frameworks/JavaVM.framework/Versions/${JAVA_ VERSION}/Home fi ;; esac # resolve links - $0 may be a softlink PRG="$0" while [ -h "$PRG" ]; do ls=`ls -ld "$PRG"` link=`expr "$ls" : '.*-> \(.*\)$'` if expr "$link" : '.*/.*' > /dev/null; then PRG="$link" else PRG=`dirname "$PRG"`/"$link" fi
81 done # Get standard environment variables PRGDIR=`dirname "$PRG"` # Only set CARBON_HOME if not already set [ -z "$CARBON_HOME" ] && CARBON_HOME=`cd "$PRGDIR/.." ; pwd` ##################################################################### ###### NAME=start-analytics # Daemon name, where is the actual executable ANALYTICS_INIT_SCRIPT="$CARBON_HOME/wso2/analytics/bin/wso2server.sh" # If the daemon is not there, then exit. $ANALYTICS_INIT_SCRIPT $* exit;
Anexos 82 82 Anexo III: Script broker.sh #!/bin/sh # broker.sh # ------------------------------------------------------------------- --------- # Copyright 2016 WSO2, Inc. http://www.wso2.org # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. cygwin=false; darwin=false; os400=false; mingw=false; case "`uname`" in CYGWIN*) cygwin=true;; MINGW*) mingw=true;; OS400*) os400=true;; Darwin*) darwin=true if [ -z "$JAVA_VERSION" ] ; then JAVA_VERSION="CurrentJDK" else echo "Using Java version: $JAVA_VERSION" fi if [ -z "$JAVA_HOME" ] ; then JAVA_HOME=/System/Library/Frameworks/JavaVM.framework/Versions/${JAVA_ VERSION}/Home fi ;; esac # resolve links - $0 may be a softlink PRG="$0" while [ -h "$PRG" ]; do ls=`ls -ld "$PRG"` link=`expr "$ls" : '.*-> \(.*\)$'` if expr "$link" : '.*/.*' > /dev/null; then PRG="$link" else PRG=`dirname "$PRG"`/"$link" fi
83 done # Get standard environment variables PRGDIR=`dirname "$PRG"` # Only set CARBON_HOME if not already set [ -z "$CARBON_HOME" ] && CARBON_HOME=`cd "$PRGDIR/.." ; pwd` ##################################################################### ###### NAME=start-broker # Daemon name, where is the actual executable BROKER_INIT_SCRIPT="$CARBON_HOME/wso2/broker/bin/wso2server.sh" # If the daemon is not there, then exit. $BROKER_INIT_SCRIPT $* exit;
Anexos 84 84 Anexo IV: Configuración del DAS <?xml version="1.0" encoding="ISO-8859-1"?> <!-- ~ Copyright (c) 2016, WSO2 Inc. (http://www.wso2.org) All Rights Reserved. ~ ~ WSO2 Inc. licenses this file to you under the Apache License, ~ Version 2.0 (the "License"); you may not use this file except ~ in compliance with the License. ~ You may obtain a copy of the License at ~ ~ http://www.apache.org/licenses/LICENSE-2.0 ~ ~ Unless required by applicable law or agreed to in writing, ~ software distributed under the License is distributed on an ~ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY ~ KIND, either express or implied. See the License for the ~ specific language governing permissions and limitations ~ under the License. --> <!-- This is the main server configuration file ${carbon.home} represents the carbon.home system property. Other system properties can be specified in a similar manner. --> <Server xmlns="http://wso2.org/projects/carbon/carbon.xml"> <!-- Product Name --> <Name>WSO2 IoT Server</Name> <!-- machine readable unique key to identify each product --> <ServerKey>IoT</ServerKey> <!-- Product Version --> <Version>3.3.0</Version> <!-- Host name or IP address of the machine hosting this server e.g. www.wso2.org, 192.168.1.10 This is will become part of the End Point Reference of the services deployed on this server instance. --> <!--HostName>www.wso2.org</HostName--> <!--
85 Host name to be used for the Carbon management console --> <!--MgtHostName>mgt.wso2.org</MgtHostName--> <!-- The URL of the back end server. This is where the admin services are hosted and will be used by the clients in the front end server. This is required only for the Front-end server. This is used when seperating BE server from FE server --> <ServerURL>local:/${carbon.context}/services/</ServerURL> <!-- <ServerURL>https://${carbon.local.ip}:${carbon.management.port}${carbo n.context}/services/</ServerURL> --> <!-- The URL of the index page. This is where the user will be redirected after signing in to the carbon server. --> <!-- IndexPageURL>/carbon/admin/index.jsp</IndexPageURL--> <!-- For cApp deployment, we have to identify the roles that can be acted by the current server. The following property is used for that purpose. Any number of roles can be defined here. Regular expressions can be used in the role. Ex : <Role>.*</Role> means this server can act any role --> <ServerRoles> <Role>ComplexEventProcessor</Role> <Role>DataAnalyticsServer</Role> <Role>GeoDashboard</Role> </ServerRoles> <!-- uncommnet this line to subscribe to a bam instance automatically --> <!-- <BamServerURL>https://bamhost:bamport/services/</BamServerURL>--> <!-- The fully qualified name of the server --> <Package>org.wso2.carbon</Package> <!-- Webapp context root of WSO2 Carbon management console. --> <WebContextRoot>/</WebContextRoot> <!-- Prox
Anexos 92 92 <!-- Enable following config to allow Emails as usernames. --> <!--EnableEmailUserName>true</EnableEmailUserName--> <!-- Security configurations --> <Security> <!-- KeyStore which will be used for encrypting/decrypting passwords and other sensitive information. --> <KeyStore> <!-- Keystore file location--> <Location>${carbon.home}/repository/resources/security/wso2carbon.jks< /Location> <!-- Keystore type (JKS/PKCS12 etc.)--> <Type>JKS</Type> <!-- Keystore password--> <Password>wso2carbon</Password> <!-- Private Key alias--> <KeyAlias>wso2carbon</KeyAlias> <!-- Private Key password--> <KeyPassword>wso2carbon</KeyPassword> </KeyStore> <!-- System wide trust-store which is used to maintain the certificates of all the trusted parties. --> <TrustStore> <!-- trust-store file location --> <Location>${carbon.home}/repository/resources/security/clienttruststore.jks</Location> <!-- trust-store type (JKS/PKCS12 etc.) --> <Type>JKS</Type> <!-- trust-store password --> <Password>wso2carbon</Password> </TrustStore> <!-- The Authenticator configuration to be used at the JVM level. We extend the java.net.Authenticator to make it possible to authenticate to given servers and proxies. --> <NetworkAuthenticatorConfig> <!--
93 Below is a sample configuration for a single authenticator. Please note that all child elements are mandatory. Not having some child elements would lead to exceptions at runtime. --> <!-- <Credential> --> <!-- the pattern that would match a subset of URLs for which this authenticator would be used --> <!-- <Pattern>regularExpression</Pattern> --> <!-- the type of this authenticator. Allowed values are: 1. server 2. proxy --> <!-- <Type>proxy</Type> --> <!-- the username used to log in to server/proxy --> <!-- <Username>username</Username> --> <!-- the password used to log in to server/proxy --> <!-- <Password>password</Password> --> <!-- </Credential> --> </NetworkAuthenticatorConfig> <!-- The Tomcat realm to be used for hosted Web applications. Allowed values are; 1. UserManager 2. Memory If this is set to 'UserManager', the realm will pick users & roles from the system's WSO2 User Manager. If it is set to 'memory', the realm will pick users & roles from CARBON_HOME/repository/conf/tomcat/tomcat-users.xml --> <TomcatRealm>UserManager</TomcatRealm> <!-- Option to disable storing of tokens issued by STS--> <Dis ableTokenStore>false</DisableTokenStore> <!-- Security token store class name. If this is not set, default class will be org.wso2.carbon.security.util.SecurityTokenStore --> <!-- TokenStoreClassName>org.wso2.carbon.identity.sts.store.DBTokenStore</T okenStoreClassName--> <XSSPreventionConfig> <Enabled>true</Enabled> <Rule>allow</Rule>
Anexos 94 94 <Patterns> <!--Pattern></Pattern--> </Patterns> </XSSPreventionConfig> <!-- Configurations to avoid Cross Site Request Forgery vulnerabilities --> <CSRFPreventionConfig> <!-- CSRFPreventionFilter configurations that adopts Synchronizer Token Pattern --> <CSRFPreventionFilter> <!-- Set below to true to enable the CSRFPreventionFilter --> <Enabled>false</Enabled> <!-- Url Pattern to skip application of CSRF protection--> <SkipUrlPattern>(.*)(/images|/css|/js|/docs)(.*)</SkipUrlPattern> </CSRFPreventionFilter> </CSRFPreventionConfig> <!-- Configuration to enable or disable CR and LF sanitization filter--> <CRLFPreventionConfig> <!--Set below to true to enable the CRLFPreventionFilter- -> <Enabled>true</Enabled> </CRLFPreventionConfig> </Security> <!-- The temporary work directory --> <WorkDirectory>${carbon.home}/tmp/work</WorkDirectory> <!-- House-keeping configuration --> <HouseKeeping> <!-- true - Start House-keeping thread on server startup false - Do not start House-keeping thread on server startup. The user will run it manually as and when he wishes. --> <AutoStart>true</AutoStart> <!-- The interval in *minutes*, between house-keeping runs --> <Interval>10</Interval> <!-- The maximum time in *minutes*, temp files are allowed to live
95 in the system. Files/directories which were modified more than "MaxTempFileLifetime" minutes ago will be removed by the house-keeping task --> <MaxTempFileLifetime>30</MaxTempFileLifetime> </HouseKeeping> <!-- Configuration for handling different types of file upload & other file uploading related config parameters. To map all actions to a particular FileUploadExecutor, use <Action>*</Action> --> <FileUploadConfig> <!-- The total file upload size limit in MB --> <TotalFileSizeLimit>100</TotalFileSizeLimit> <Mapping> <Actions> <Action>keystore</Action> <Action>certificate</Action> <Action>*</Action> </Actions> <Class>org.wso2.carbon.ui.transports.fileupload.AnyFileUploadExecutor< /Class> </Mapping> <Mapping> <Actions> <Action>jarZip</Action> </Actions> <Class>org.wso2.carbon.ui.transports.fileupload.JarZipUploadExecutor</ Class> </Mapping> <Mapping> <Actions> <Action>dbs</Action> </Actions> <Class>org.wso2.carbon.ui.transports.fileupload.DBSFileUploadExecutor< /Class> </Mapping> <Mapping> <Actions> <Action>tools</Action> </Actions> <Class>org.wso2.carbon.ui.transports.fileupload.ToolsFileUploadExecuto r</Class> </Mapping> <Mapping>
Anexos 96 96 <Actions> <Action>toolsAny</Action> </Actions> <Class>org.wso2.carbon.ui.transports.fileupload.ToolsAnyFileUploadExec utor</Class> </Mapping> </FileUploadConfig> <!-- FileNameRegEx is used to validate the file input/upload/writeout names. e.g. <FileNameRegEx>^(?!(?:CON|PRN|AUX|NUL|COM[1-9]|LPT[1- 9])(?:\.[^.])?$)[^<>:"/\\|?*\x00-\x1F][^<>:"/\\|?*\x00-\x1F\ .]$</FileNameRegEx> --> <!--<FileNameRegEx></FileNameRegEx>--> <!-- Processors which process special HTTP GET requests such as ?wsdl, ?policy etc. In order to plug in a processor to handle a special request, simply add an entry to this section. The value of the Item element is the first parameter in the query string(e.g. ?wsdl) which needs special processing The value of the Class element is a class which implements org.wso2.carbon.transport.HttpGetRequestProcessor --> <HttpGetRequestProcessors> <Processor> <Item>info</Item> <Class>org.wso2.carbon.core.transports.util.InfoProcessor</Class> </Processor> <Processor> <Item>wsdl</Item> <Class>org.wso2.carbon.core.transports.util.Wsdl11Processor</Class> </Processor> <Processor> <Item>wsdl2</Item> <Class>org.wso2.carbon.core.transports.util.Wsdl20Processor</Class> </Processor> <Processor> <Item>xsd</Item> <Class>org.wso2.carbon.core.transports.util.XsdProcessor</Class> </Processor> </HttpGetRequestProcessors>
97 <!-- Deployment Synchronizer Configuration. t Enabled value to true when running with "svn based" dep sync. In master nodes you need to set both AutoCommit and AutoCheckout to true and in worker nodes set only AutoCheckout to true. --> <DeploymentSynchronizer> <Enabled>false</Enabled> <AutoCommit>false</AutoCommit> <AutoCheckout>true</AutoCheckout> <RepositoryType>svn</RepositoryType> <SvnUrl>http://svnrepo.example.com/repos/</SvnUrl> <SvnUser>username</SvnUser> <SvnPassword>password</SvnPassword> <SvnUrlAppendTenantId>true</SvnUrlAppendTenantId> </DeploymentSynchronizer> <!-- Deployment Synchronizer Configuration. Uncomment the following section when running with "registry based" dep sync. In master nodes you need to set both AutoCommit and AutoCheckout to true and in worker nodes set only AutoCheckout to true. --> <!--<DeploymentSynchronizer> <Enabled>true</Enabled> <AutoCommit>false</AutoCommit> <AutoCheckout>true</AutoCheckout> </DeploymentSynchronizer>--> <!-- Mediation persistence configurations. Only valid if mediation features are available i.e. ESB --> <!--<MediationConfig> <LoadFromRegistry>false</LoadFromRegistry> <SaveToFile>false</SaveToFile> <Persistence>enabled</Persistence> <RegistryPersistence>enabled</RegistryPersistence> </MediationConfig>--> <!-- Server intializing code, specified as implementation classes of org.wso2.carbon.core.ServerInitializer. This code will be run when the Carbon server is initialized --> <ServerInitializers> <!--<Initializer></Initializer>--> </ServerInitializers> <!-- Indicates whether the Carbon Servlet is required by the system, and whether it should be registered --> <RequireCarbonServlet>${require.carbon.servlet}</RequireCarbonServlet>
Anexos 98 98 <!-- Carbon H2 OSGI Configuration By default non of the servers start. name="web" - Start the web server with the H2 Console name="webPort" - The port (default: 8082) name="webAllowOthers" - Allow other computers to connect name="webSSL" - Use encrypted (HTTPS) connections name="tcp" - Start the TCP server name="tcpPort" - The port (default: 9092) name="tcpAllowOthers" - Allow other computers to connect name="tcpSSL" - Use encrypted (SSL) connections name="pg" - Start the PG server name="pgPort" - The port (default: 5435) name="pgAllowOthers" - Allow other computers to connect name="trace" - Print additional trace information; for all servers name="baseDir" - The base directory for H2 databases; for all servers --> <!--H2DatabaseConfiguration> <property name="web" /> <property name="webPort">8082</property> <property name="webAllowOthers" /> <property name="webSSL" /> <property name="tcp" /> <property name="tcpPort">9092</property> <property name="tcpAllowOthers" /> <property name="tcpSSL" /> <property name="pg" /> <property name="pgPort">5435</property> <property name="pgAllowOthers" /> <property name="trace" /> <property name="baseDir">${carbon.home}</property> </H2DatabaseConfiguration--> <!--Disabling statistics reporter by default--> <StatisticsReporterDisabled>true</StatisticsReporterDisabled> <!-- Enable accessing Admin Console via HTTP --> <!-- EnableHTTPAdminConsole>true</EnableHTTPAdminConsole --> <!-- Default Feature Repository of WSO2 Carbon. --> <FeatureRepository> <RepositoryName>default repository</RepositoryName> <RepositoryURL>http://productdist.wso2.com/p2/carbon/releases/wilkes/</RepositoryURL> </FeatureRepository> <!-- Conf igure API Management --> <APIManagement>
99 <!-- Uses the embedded API Manager by default. If you want to use an external API Manager instance to manage APIs, configure below externalAPIManager--> <Ena bled>true</Enabled> <!-- Uncomment and configure API Gateway and Publ isher URLs to use external API Manager instance--> <!-- ExternalAPIManager> <API GatewayURL>http://localhost:8281</APIGatewayURL> <API PublisherURL>http://localhost:8281/publisher</APIPublisherURL> </Ex ternalAPIManager--> <Loa dAPIContextsInServerStartup>true</LoadAPIContextsInServerStartup> </APIManagement> </Server> Anexo V: Ficheros de META-INF y WEB-INF permissions.xml <PermissionConfiguration> <APIVersion></APIVersion> <!-- Device related APIs --> <Permission> <name>Get device</name> <path>/device-mgt/myRaspberry/user</path> <url>/device/*</url> <method>GET</method> <scope>myRaspberry_user</scope> </Permission> <Permission> <name>Remove device</name> <path>/device-mgt/myRaspberry/user</path> <url>/device/*</url> <method>DELETE</method> <scope>myRaspberry_user</scope> </Permission> <Permission> <name>Download device</name> <path>/device-mgt/myRaspberry/user</path> <url>/device/download</url>
Anexos 100 100 <method>GET</method> <scope>myRaspberry_user</scope> </Permission> <Permission> <name>Update device</name> <path>/device-mgt/myRaspberry/user</path> <url>/device/*</url> <method>POST</method> <scope>myRaspberry_user</scope> </Permission> <Permission> <name>Get Devices</name> <path>/device-mgt/myRaspberry/user</path> <url>/device</url> <method>GET</method> <scope>myRaspberry_user</scope> </Permission> <Permission> <name>Register Device</name> <path>/device-mgt/myRaspberry/user</path> <url>/device/register</url> <method>POST</method> <scope>myRaspberry_device</scope> </Permission> <Permission> <name>Control Sensor</name> <path>/device-mgt/myRaspberry/user</path> <url>/device/*/change-status</url> <method>POST</method> <scope>myRaspberry_user</scope> </Permission> <Permission> <name>Get Stats</name> <path>/device-mgt/myRaspberry/user</path> <url>/device/stats/*</url> <method>GET</method> <scope>myRaspberry_device</scope> </Permission> </PermissionConfiguration> webapp-classloading.xml <Classloading xmlns="http://wso2.org/projects/as/classloading"> <!-- Parent-first or child-first. Default behaviour is child-first.-- > <ParentFirst>false</ParentFirst>
101 <!-- Defa ult environments that contains provides to all the webapps. This can be overridden by individual webapps by specifing required environments Tomc at environment is the default and every webapps gets it even if they didn't specify it. e.g. If a webapps requires CXF, they will get both Tomcat and CXF. --> <Environments>CXF,Carbon</Environments> </Classloading> cxf-servlet.xml <beans xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:jaxrs="http://cxf.apache.org/jaxrs" xmlns="http://www.springframework.org/schema/beans" xsi:schemaLocation=" http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd http://cxf.apache.org/jaxrs http://cxf.apache.org/schemas/jaxrs.xsd"> <jaxrs:server id="myRaspberry" address="/"> <jaxrs:serviceBeans> <bean id="DeviceTypeService" class="sensorboard.raspberry.api.DeviceTypeServiceImpl"> </bean> </jaxrs:serviceBeans> <jaxrs:providers> <bean class="org.codehaus.jackson.jaxrs.JacksonJsonProvider"/> </jaxrs:providers> </jaxrs:server> </beans> web.xml p://www.w3.org/2001/XMLSchema-instance" version="2.5" xmlns="http://java.sun.com/xml/ns/javaee" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd" metadata-complete="true"> <display-name>WSO2 IoT Server</display-name> <description>WSO2 IoT Server</description> <servlet> <servlet-name>CXFServlet</servlet-name> <servletclass>org.apache.cxf.transport.servlet.CXFServlet</servlet-class> <load-on-startup>1</load-on-startup> </servlet> <servlet-mapping> <servlet-name>CXFServlet</servlet-name> <url-pattern>/*</url-pattern> </servlet-mapping> <context-param>
Glosario 108 108 IoT: Internet of Things – Internet de las cosas REST: Representational State Transfer – Transferencia de estado representacional API: Application Programing Interface – Interfaz de programación de aplicaciones IDE: Integrated Development Environment – Entorno de Desarrollo Integrado M2M: Machine to machine – De maquina a maquina UUF: Unified User Interface Framework – Framework de interfaz de usuario unificada CDMF: Connected Device Managemenet Framworkd – Framework de gestion de dispositivo conectado
109 BIBLIOGRAFÍA Documentación MQTT: http://docs.oasis-open.org/mqtt/mqtt/v3.1.1/os/mqtt-v3.1.1-os.html Notación Swagger: https://github.com/swagger-api/swagger-core/wiki/annotations