Este Codelab lo guiará en la creación de sus propios objetos inteligentes con IoTivity.

Se lo guiará con la compilación de IoTivity, la configuración de los dispositivos y la ejecución de los mismos, para asegurar que su primera experiencia con IoTivity sea enriquecedora y que cumpla con los lineamientos propuestos por la OCF.

¿Qué construirás?

En este codelab, usted construirá un interruptor y una luz que integren IoTivity.

Su foquito va a:

  • Ejecutarse en un Raspberry Pi.
  • Ser escrito en C++.
  • Apagarse y encenderse remotamente por otro dispositivo de iotivity.
  • Comunicarse de manera segura.

Su interruptor va a:

  • Ejecutarse en un celular Android o una PC.
  • (Opcional) Ser escrito en Node.js y operado mediante una interfaz gráfica.
  • Utilizar y demostrar las capacidades básicas de Iotivity.
  • Encender y apagar un LED remotamente.
  • Comunicarse de manera segura.

¿Qué aprenderás?

¿Qué necesitarás?

1 Raspberry Pi con:

Elementos de electrónica:

1 computador con Ubuntu 16.04 o superior.

1 editor de texto (en este tutorial la edición de texto se realiza con nano por defecto, si tiene uno de preferencia puede reemplazarlo en los comandos por nano)

Conocimiento básico de C++, JavaScript, shell.

Si la red no soporta Multicast: configurar una red WPA2 usando un router adicional.

Este Codelab está enfocado en IoTivity. Los conceptos no relevantes y bloques de código son provistos.

ctrl + c para copiar

ctrl + shift + v para pegar o clic derecho y pegar.

Vamos a conectar dos objetos inteligentes, pero ustedes se preguntarán:

¿Qué es un objeto inteligente?

Un objeto inteligente es un objeto cotidiano con capacidad de procesamiento y comunicación. Puede comunicarse con otros objetos de manera autónoma o interactuar con el usuario mediante una interfaz.

Y la conexión se realizará con IoTivity, lo que nos lleva a preguntarnos:

¿Qué es Iotivity?

Iotivity es un proyecto de código abierto que implementa el estándar de la Open Connectivity Foundation (OCF) para la comunicación entre objetos inteligentes independiente del protocolo. El estándar cuenta con versión 2.0, disponible en este enlace. La especificación se basa en una arquitectura REST, en la que existe un cliente que puede descubrir e interactuar con recursos en el servidor. En nuestro caso el servidor será un foquito y el cliente será el interruptor virtual o un celular. Definidos por la OCF como:

Un dispositivo OCF puede tener uno o ambos roles.

Tomado de OCF Specification Overview Core Technology Specification

Un Servidor OCF contiene uno o más recursos para describir el mundo real. Cada recurso contiene propiedades que describen un aspecto del recurso, algunas son opcionales y dependen del recurso, y otras son mandatorias para todos como las siguientes:

IoTivity busca lograr:

En este Tutorial vamos a crear una luz inteligente, compuesta por un Raspberry Pi y elementos electrónicos.

El Raspberry Pi es una computadora de bajo costo del tamaño de una tarjeta de crédito, que puede usarse con un monitor por HDMI o aparte a través de conexión de red. Es capaz de hacer todo lo que una computadora puede, pero lo que lo hace especial es la habilidad de interactuar con el mundo exterior por medio de pines llamados GPIO, a los que se le puede conectar sensores o actuadores y usar protocolos como SPI, UART, entre otros. La desventaja es que estos pines son digitales y para conectar un sensor análogico se debe usar un convertidor análogico a digital (ADC: Analog to Digital converter). En la siguiente imagen se muestran los pines como referencia:

Nombres de pines del Raspberry Pi. Tomado de www.element14.com.

Conectar los elementos electrónicos como se muestra en la siguiente imagen (GPIO11, PIN23):

El Raspberry Pi siendo una computadora necesita de un sistema operativo sobre el cual ejecutar programas. La lógica de nuestro foquito será escrita en un programa C++ que se ejecutará dentro del Raspberry Pi. Existen varias opciones de sistemas operativos para Raspberry Pi, pero usaremos la oficial, llamada Raspbian. En nuestro caso usaremos la versión Raspbian Stretch Lite, que viene sin interfaz gráfica.

Instalar Raspbian

  1. Descargar Raspbian Stretch Lite

Descargar .ZIP

  1. Instalar Etcher (Windows o Linux)
  2. Usar Etcher para quemar la imagen de Raspbian a la tarjeta SD. Para esto Insertamos la tarjeta SD a la PC. Ejecutamos Etcher y seleccionamos el volumen de la tarjeta SD insertada, por lo general se selecciona automáticamente. A continuación procedemos a quemar, con el botón respectivo.
  3. Activar el ssh en el Raspberry Pi: crear un archivo en /boot llamado "ssh" no "ssh.txt". Puede abrir un terminal y ejecutar lo siguiente, cambiando el [path-to-volume] por su nombre de tarjeta SD:
$ cd [path-to-volume]/boot
$ touch ssh # touch sirve para crear archivos
  1. Activar el Wi-Fi, para lo cual creamos un archivo con la configuración de la red (ssid y clave).
# nano también crea archivos pero al crearlos permite editarlos
$ nano wpa_supplicant.conf 

Configurar red

Si recién se quemo la tarjeta. Esto se escribe dentro del archivo wpa_supplicant.conf en el /boot, reemplazando "Your network SSID" y "Your WPA/WPA2 security key" con el ssid y clave de su red::

wpa_supplicant.conf

ctrl_interface=DIR=/var/run/wpa_supplicant GROUP=netdev
update_config=1
country=US

network={
        ssid="Your network SSID"
        psk="Your WPA/WPA2 security key"
        key_mgmt=WPA-PSK
}

ctrl + x para salir y guardar

Si ya se usó la tarjeta con un Raspberry Pi, editar desde la pc:.

$ sudo nano /etc/wpa_supplicant/wpa_supplicant.conf

Habilitar UART

Para habilitar uart, editamos config.txt:

$ nano config.txt

Y agregamos la siguiente línea al final::

wpa_supplicant.conf

`enable_uart=1` to config.txt.

IoTivity puede compilarse en Windows, Ubuntu, Raspbian entre otros sistemas operativos. Nos enfocamos en los más accesibles y con mayor soporte, los cuales usaremos en nuestro proyecto: Ubuntu y Raspberry Pi. Adicionalmente mostramos la instalación en CentOS, por si desean instalarlo en un servidor.

Instalar dependencias en Raspberry Pi

4.1 Este paso asume que el Raspberry Pi ya está configurado con red y ssh. Para conectarse con el Raspberry Pi por ssh a través de la red, ejecutamos desde un terminal los siguientes comandos:

$ ping raspberrypi2.local
$ ssh pi@raspberrypi2.local
user: pi
passwd: raspberry
$ passwd  # para cambiar el password

Estas instrucciones de instalación han sido probadas en Raspbian Stretch Lite.

4.2 Actualizar paquetes (Buscar actualizaciones disponibles y luego instalar):

$ sudo apt-get update
$ sudo apt-get -qy upgrade

4.3 Instalar los paquetes:

$ sudo apt-get -qy install build-essential git scons libtool \
autoconf valgrind doxygen wget unzip cmake libboost-dev \
libboost-program-options-dev libboost-thread-dev uuid-dev \
libexpat1-dev libglib2.0-dev libsqlite3-dev libcurl4-gnutls-dev

MRAA

MRAA es una librería escrita en C con bindings para C++, Java, Python y Node.js (Javascript) que permite acceder a sensores y actuadores a través de una variedad de plataformas de hardware. La usaremos en nuestro proyecto para acceder al pin del Raspberry Pi donde se conectará el foquito, y enviarle una señal de Alto o Bajo.

4.5 Descargar mraa dentro de la carpeta iot, ejecutar lo siguiente en un terminal:

$ mkdir ~/iot
$ cd ~/iot
$ git clone https://github.com/intel-iot-devkit/mraa.git 

4.6 Compilar mraa:

$ mkdir mraa/build && cd mraa/build
$ cmake .. && make && sudo make install 

4.7 Probar que mraa funciona, ejecutando un ejemplo de encender un LED:

$ cd ~/iot/mraa/build/examples/c++ && sudo ./gpio_cpp 23 #7

Si todo sale bien, el LED colocado al Raspberry Pi debería encenderse y apagarse.

A continuación debemos configurar el mismo ambiente en la PC con Ubuntu, que contendrá nuestro interruptor virtual:

Instalar dependencias en Linux

Ubuntu

Estas instrucciones de instalación han sido probadas en Ubuntu 16.04 LTS.

4.8 Actualizar paquetes (Buscar actualizaciones disponibles y luego instalar):

$ sudo apt-get update
$ sudo apt-get -qy upgrade

4.9 Instalar herramientas de compilación:

$ sudo apt-get -qy install \
 build-essential git scons ssh valgrind doxygen wget chrpath \
 libtool autoconf pkg-config unzip gcovr  

4.10 Instalar soporte de desarrollo para librerías externas:

$ sudo apt-get -qy install libboost-all-dev libsqlite3-dev uuid-dev libexpat1-dev libglib2.0-dev libcurl4-gnutls-dev libbz2-dev 

4.11 Crear una carpeta para almacenar los proyectos de IoT:

$ mkdir ~/iot
$ cd ~/iot

4.12 Si desea instalar soporte para Android:

$ sudo apt-get -qy install openjdk-8-jdk icedtea-plugin

CentOS

Estas instrucciones de instalación han sido probadas en CentOS 7.

4.13 Agregar repositorios adicionales para paquetes de CentOS7 64 bit:

// Alternativa 1 (Instalada en el servidor)
sudo rpm -i http://cbs.centos.org/kojifiles/packages/scons/2.5.1/4.el7/noarch/scons-2.5.1-4.el7.noarch.rpm
// Alternativa 2
sudo rpm -i tools/tizen/scons-2.1.0-3.1.slp.noarch.rpm
// Alternativa 3
$ sudo yum install epel-release
$ sudo rpm -ivh http://repo.okay.com.mx/centos/7/x86_64/release/okay-release-1-1.noarch.rpm

4.14 Actualizar repositorios:

$ sudo yum update

4.15 Instalar dependencias:

$ sudo yum install \
      expat-devel  libcurl-devel  openssl-devel \
      boost-devel  boost-thread boost-filesystem \
      glib2-devel  sqlite-devel  libuuid-devel \
      boost-program-options libsqlite3x-devel \
      perl-CBOR-XS wget unzip \
      gcc gcc-c++ scons \
      autoconf make automake \
      git libtool valgrind doxygen

4.16 Permitir tráfico de mensajes udp a través del firewall:

$ sudo firewall-cmd --permanent --add-protocol=udp
$ sudo firewall-cmd reload

4.17 Crear una carpeta para almacenar los proyectos de IoT:

$ mkdir ~/iot
$ cd ~/iot

IoTivity es un proyecto open-source y como tal su código se encuentra disponible en Github para descargarlo fácilmente.

5.1 Clonar el repositorio de IoTivity en la carpeta iot (está carpeta fue creada en el paso anterior), en un terminal ejecutar:

$ cd ~/iot
$ git clone https://github.com/iotivity/iotivity.git
$ cd ~/iot/iotivity

5.2 Ejecutar:

$ scons examples/OCFSecure -j 2 TARGET_TRANSPORT=IP

5.3 Se muestra un mensaje que dice que clonemos unos repositorios, primero de mbedtls-2.4.2 y luego tinycbor. Podemos evitar estos mensajes ejecutando los comandos solicitados antes de ejecutar scons:

$ git clone https://github.com/ARMmbed/mbedtls.git extlibs/mbedtls/mbedtls -b mbedtls-2.4.2
$ git clone https://github.com/intel/tinycbor.git extlibs/tinycbor/tinycbor -b v0.5.1

¿Qué es scons y cómo funciona?

scons es una herramienta de construcción de software parecido a make, pero que se escribe en python, lo que nos brinda todas las ventajas de un lenguaje de programación real para construir nuestros proyectos.

scons busca por defecto un archivo SConstruct, que a su vez puede llamar a archivos de configuración conocidos como SConscript. Estos archivos SConscript puede nombrarse como gusten, mientras sea registrado con la función SConscript en algún archivo de configuración como lo muestra el siguiente extracto:

# Load common build config
SConscript('build_common/SConscript')
# Load extra options 
SConscript('extra_options.scons') 

Estos archivos de configuración contienen los archivos a ser construidos (targets) y opcionalmente las reglas para construirlos. scons lee y ejecuta los archivos SConscript como scripts de Python, pudiendo utilizar las capacidades de Python (como control de flujo, manipulación de datos, e importar librerías).

De vuelta a la acción:

5.4 En el Raspberry Pi hay que editar el SConscript para indicarle el pin que vamos a usar con nuestro LED, editamos el archivo con nano:

$ nano ~/iot/iotivity/examples/OCFSecure/SConscript

En la línea 85, cambiamos el 7 por 23:

examples/OCFSecure/SConscript

elif not joule and raspberry_pi: cpp_defines.append('LED_PIN=23') # antes 'LED_PIN=7'

ctrl + x para guardar

5.5 Para ejecutar scons hay que colocarnos en la carpeta que contiene el SConstruct. Nos cambiamos a ese directorio (~/iot/iotivity) y compilamos IoTivity en el Raspberry Pi y realizamos lo mismo en Ubuntu:

# Ubuntu
$ cd ~/iot/iotivity
$ scons examples/OCFSecure -j 2 TARGET_TRANSPORT=IP
# Raspberry Pi
$ cd ~/iot/iotivity
$ scons examples/OCFSecure -j 2 TARGET_TRANSPORT=IP
scons: done building targets.

-j: parámetro de scons para construir en paralelo.

El comando ejecutado compila los archivos de la carpeta examples/OCFSecure que están definidos en el SConscript de esa carpeta y es referenciado en el SConstruct del directorio root del proyecto (~/iot/iotivity).

Si la compilación es exitosa, deberíamos ver un mensaje al final que diga:

scons: done building targets.

IoTivity viene configurado por defecto con seguridad, si se la desea desactivar hay que agregar el parámetro SECURED=0. Esto es útil si se quiere hacer un prototipo rápido. El comando a ejecutar sería:

$ scons examples/OCFSecure -j 2 TARGET_TRANSPORT=IP SECURED=0

Un dispositivo IoTivity contiene los siguientes recursos:

Tomado de OCF Specification Overview Core Technology Specification

La comunicación se realiza a través de CoAP (Constrained Application Protocol) que es un protocolo REST parecido a HTTP pero reducido. Los mensajes son serializados en formato CBOR (Concise Binary Object Representation) que es un formato binario reducido parecido a JSON.

Tomado de OCF Specification Overview Core Technology Specification

Existen ciertos recursos que son mandatorios, por ejemplo los recursos esenciales de platform, device y /oic/res. En la siguiente imagen, vemos que hay un luz inteligente (oic.d.light), que se puede representar con dos recursos extras de los cuales uno es mandatorio que es oic.r.switch.binary el cual permite encender o apagar la luz y es fundamental para toda luz, en cambio oic.r.light.brightness es opcional ya que no todas las luces cuentan con ajuste de intensidad. Para nuestro ejemplo solo necesitamos encender y apagar la luz y usaremos oic.r.switch.binary .

Tomado de OCF Specification Overview Core Technology Specification

6.0 Opcionalmente podemos inspeccionar el código, ejecutando en un terminal lo siguiente:

$ cd ~/iot/iotivity/examples
$ nano server.cpp

6.1 Los objetos se acceden por medio de uris, pero están definidos lógicamente como clases o estructuras en el código. En la línea 47, se define una estructura para representar a la luz, se llama switch porque es el interruptor interno de encendido y apagado del foquito. Vemos que cuenta con las propiedades de OC_DISCOVERABLE | OC_SECURE, para poder ser descubierto y comunicarse de manera segura. Se define el tipo como oic.r.switch.binary, la interfaz de actuador es oic.if.a, el uri para realizar las peticiones será "/switch" y contiene una propiedad booleana value, que representará el estado de encendido de la luz, con verdadero o falso.

examples/OCFSecure/server.cpp

typedef struct
{
        OCResourceHandle handle;
        const char* type = "oic.r.switch.binary";
        const char * interface = OC_RSRVD_INTERFACE_ACTUATOR;
        const char* uri = "/switch";
        uint8_t properties = OC_DISCOVERABLE | OC_SECURE;
        bool value = false;

} BinarySwitch;

A continuación hay unas funciones decorativas para agregar información de Plataforma y de Dispositivo como lo son SetPlatformInfo y SetDeviceInfo respectivamente.

6.2 Cuando se hace un requerimiento al servidor, se espera una respuesta, esta respuesta viene con un payload. En la línea 203, se encuentra la función encargada de crear el payload de la respuesta del requerimiento. Se crea un payload con OCRepPayloadCreate y se le agrega información adicional con OCRepPayloadAdd[tipo-de-recurso], por ejemplo si se quiere agregar una propiedad booleana se utiliza OCRepPayloadSetPropBool, y se coloca el nombre y valor de la propiedad.

examples/OCFSecure/server.cpp

OCRepPayload*
CreateResponsePayload(BinarySwitch resource)
{
        OCRepPayload* payload = OCRepPayloadCreate();
     [...]
     OCRepPayloadAddInterface(payload, OC_RSRVD_INTERFACE_DEFAULT);
        OCRepPayloadAddInterface(payload, resource.interface);
        OCRepPayloadAddResourceType(payload, resource.type);
        OCRepPayloadSetPropBool(payload, "value", resource.value);

        return payload;
}

6.3 En la línea 231, se define la función ProcessGetRequest que se encargará de manejar una petición GET (RETRIEVE). Lo que hace es crear un payload con la información del interruptor del foquito, dicha información es una variable global SWITCH definida en la línea 54. Si se crea correctamente retorna OC_EH_OK, sino retorna OC_EH_ERROR.

examples/OCFSecure/server.cpp

static BinarySwitch SWITCH;

OCEntityHandlerResult ProcessGetRequest(OCRepPayload **payload)
{
        OCEntityHandlerResult eh_res = OC_EH_ERROR;

        OCRepPayload *ResponsePayload = CreateResponsePayload(SWITCH);

        if (ResponsePayload)
        {
            *payload = ResponsePayload;
            eh_res = OC_EH_OK;
        }

        return eh_res;
}

6.4 Así como se creó un GET para obtener el estado del foquito, se creará un POST para cambiar su estado. En la línea 257, se define la función para manejar un requerimiento POST, ProcessPostRequest. Es similar al GET, pero en este caso se actualiza la propiedad value con el valor recibido en el requerimiento, y se usa la función de MRAA para encender el LED, enviando un booleano (1: HIGH, 0: LOW) al pin GPIO (23) y al final se crea el payload con el valor actualizado.

examples/OCFSecure/server.cpp

OCEntityHandlerResult
ProcessPostRequest(OCEntityHandlerRequest *ehRequest,
                       OCRepPayload **payload)
{
        OCEntityHandlerResult eh_res = OC_EH_ERROR;
        
        // casting the request payload into a OCRepPayload data type to
        // read the value property
        const OCRepPayload* requestPayload = (OCRepPayload*)(ehRequest->payload);
        bool value;
        if (OCRepPayloadGetPropBool(requestPayload, "value", &value))
        {
            SWITCH.value = value;
     #ifdef WITH_MRAA
            GPIO->write(value);
     #endif
        }
        else
        {
            return eh_res;
        }

        OCRepPayload *ResponsePayload = CreateResponsePayload(SWITCH);

        if (ResponsePayload)
        {
            *payload = ResponsePayload;
            eh_res = OC_EH_OK;
        }

        return eh_res;
}

6.5 En la línea 340, se encuentra un extracto de la función OCEntityHandlerCallBack que es un callback que se registrará al registrar el recurso en IoTivity. Esta función sirve para definir las métodos soportadas por el recurso, en nuestro caso GET y POST, y podemos apreciar que se usan las funciones anteriormente descritas para manejar esos requerimientos.

examples/OCFSecure/server.cpp : OCEntityHandlerCallBack

if (OC_REST_GET == requestMethod)
{
    eh_res = ProcessGetRequest(&payload);
}
else if (OC_REST_POST == requestMethod)
{
     eh_res = ProcessPostRequest(ehRequest, &payload);
}
else
{
     eh_res = OC_EH_ERROR;
} 

6.6 En la línea 360, más abajo en la misma función OCEntityHandlerCallBack, se obtiene el payload creado en el manejo de POST o GET, y se lo envía como respuesta al cliente por medio de OCDoResponse. La información necesaria se obtiene del requerimiento recibido inicialmente.

examples/OCFSecure/server.cpp : OCEntityHandlerCallBack

if (eh_res == OC_EH_OK)
{
   ehResponse.requestHandle = ehRequest->requestHandle;
   ehResponse.resourceHandle = ehRequest->resource;
   ehResponse.ehResult = eh_res;
   ehResponse.payload = (OCPayload*)(payload);
   ehResponse.numSendVendorSpecificHeaderOptions = 0;
   memset(ehResponse.sendVendorSpecificHeaderOptions, 0,
   sizeof(ehResponse.sendVendorSpecificHeaderOptions));
   memset(ehResponse.resourceUri,
      0,
      sizeof(ehResponse.resourceUri));
   ehResponse.persistentBufferFlag = 0;

   // Send the ehResponse
   stackResult = OCDoResponse(&ehResponse);
   i{f (stackResult != OC_STACK_OK)
   {
        eh_res = OC_EH_ERROR;
   }
}
else
{

6.7 El main() es el punto de inicio de nuestro programa C++, por lo tanto en él vamos a definir los recursos a exponer por el servidor y el loop de procesamiento de IoTivity.

Dentro del main(), en la línea 465, se encuentra la parte encargada de definir el pin del LED (foquito). Este pin podrá ser usado por medio de la variable GPIO.

examples/OCFSecure/server.cpp : main()

#if defined(WITH_MRAA) 
#if defined(RAW_GPIO)
GPIO = new mraa::Gpio(LED_PIN, true, true);
#else
GPIO = new mraa::Gpio(LED_PIN);
#endif

GPIO->dir(mraa::DIR_OUT);
#endif

6.8 En la línea 465, se usa OCRegisterPersistentStorageHandler(), necesaria para la seguridad porque define un método para acceder a archivos como la base de datos de seguridad oic_db_server.dat o switch_introspection.dat

examples/OCFSecure/server.cpp : main()

OCPersistentStorage ps = {ServerFOpen, fread, fwrite, fclose, unlink};
OCRegisterPersistentStorageHandler(&ps);

Para iniciar IoTivity hay tres modos:

Para iniciar un cliente seguro se debe usar

OC_CLIENT_SERVER 

debido a un bug del cliente al momento de cargar la base de datos.

6.9 Usamos la siguiente función para iniciar IoTivity:

examples/OCFSecure/server.cpp : main

stack_res = OCInit(NULL, 0, OC_SERVER);

6.10 En la línea 479, se registra el interruptor del foquito como recurso de IoTivity, para lo cual se coloca el tipo, interfaz, uri y propiedades los cuales fueron definidos en la estructura BinarySwitch que fue instanciada como una variable global llamada SWITCH. También se coloca el OCEntityHandlerCallBack para manejar las peticiones solicitadas al uri (/switch) del recurso.

examples/OCFSecure/server.cpp : main

{{stack_res = OCCreateResource(&(SWITCH.handle),
                                     SWITCH.type,
                                     SWITCH.interface,
                                     SWITCH.uri,
                                     OCEntityHandlerCallBack,
                                     NULL,
SWITCH.properties);

6.11 En la línea 542, se define el loop del programa, que se encarga de llamar a OCProcess para procesar las peticiones y ejecutar el respectivo callback según el recurso.

examples/OCFSecure/server.cpp : main

while (!STOP)
{
            stack_res = OCProcess();
     
     nanosleep(&timeout, NULL);
}

6.12 Los archivos construidos se generan en /out/<os>/<architecture>/<debug/release> y luego la ruta mostrada en el directorio raíz de IoTivity en nuestro caso /examples/OCFSecure/

6.13 Ejecutar lo siguiente en un terminal, para probar que el foquito funciona:

$ ~/iot/iotivity/out/linux/armv7l/release/examples/OCFSecure/server

Posible error:

pi@raspberrypi:~/iot/iotivity/out/linux/armv7l/release/examples/OCFSecure 
$ ./server
./server: error while loading shared libraries: libmraa.so.2: cannot open shared object file: No such file or directory

Solución:

$ cd /usr/lib
$ ls 

Si está la librería libmraa.so.2:

$ cd ~/iot/iotivity/out/linux/armv7l/release/examples/OCFSecure
$ export LD_LIBRARY_PATH="/usr/lib"
$ sudo ldconfig
$ ./server

Si no está la librería libmraa.so.2 en /usr/lib:

$ sudo find / -name libmraa.so.2

y usar el path que le entregue como

LD_LIBRARY_PATH

Los dispositivos IoTivity pueden indicar a otros dispositivos que quieren ser notificados de su estado cuando les suceda un cambio. Para ello, hay que declarar el método OBSERVE agregando las siguientes partes de código:

En el paso anterior, compilamos y ejecutamos un ejemplo prefabricado.

7.1 Para crear nuestros propios dispositivos debemos declararlos en el SConscript, en este ejemplo nuestra luz se llamará light. Ya que està basado en el ejemplo anterior, vamos a copiar ese archivo y usarlo como plantilla, se llamará light.cpp:

$ cd examples/OCFSecure
$ cp server.cpp light.cpp
$ nano SConscript

7.2 Declaramos el nuevo archivo en el SConscript:

light = samples_env.Program('light', [light.cpp'])

Y a su vez lo agregamos a la lista de targets a ser construidos:

list_of_samples = [client, client_dat, client_dev, server, server_dat, light]

7.3 Cambiar 'LED_PIN=7' por 'LED_PIN=23' :

cpp_defines.append('LED_PIN=23')

examples/OCFSecure/SConscript

if env.get('SECURED') == '1':
   samples_env.AppendUnique(LIBS=['mbedtls', 'mbedx509', 'mbedcrypto', 'wiringPi'])

introspection_dat = samples_env.Install(
        build_dir + examples_dir, src_dir + examples_dir + 'switch_introspection.dat')
light = samples_env.Program('light', ['light.cpp'])

list_of_samples = [client, client_dat, client_dev, server, server_dat, light]

Alias("secureExamples", list_of_samples)

7.4 Ahora procederemos a editar el archivo light.cpp:

$ nano light.cpp

iotivity/examples/OCFSecure/light.cpp

7.5. Agregar esta línea al inicio del documento en la declaración de librerías:

#include <stdio.h>

7.6. Agregar las siguientes constantes y variables,:

// Define el número máximo de observers
#define SAMPLE_MAX_NUM_OBSERVATIONS 8 

// Bandera a ser usada en el loop de observación
int gLightUnderObservation = 0; 

7.7. Para que pueda ser observado, el dispositivo debe contar con la propiedad OC_OBSERVABLE que le informa al descubridor de que el dispositivo encontrado puede ser observado. Para esto hay que modificar la estructura binarySwitch, agregando OC_OBSERVABLE a sus propiedades (properties):

uint8_t properties = OC_DISCOVERABLE | OC_SECURE | OC_OBSERVABLE; 

Agregar tambien esta variable:

bool lastValue = false;

7.8. Definimos una estructura llamada Observers que mantendrá información del observer como su ID, la función a ejecutarse cuando se solicite una observación (resourceHandle) y una bandera para saber si es válido:

/* Structure to represent the observers */
typedef struct
{
        OCObservationId observationId;
        bool valid;
        OCResourceHandle resourceHandle;
} Observers;

7.9. Cada vez que se reciba una petición de observer, se creará un Observer con la estructura anteriormente definida. Estos Observers serán almacenados en el siguiente arreglo:

/* Lista de observers registrados */
Observers interestedObservers[SAMPLE_MAX_NUM_OBSERVATIONS];

7.10. Añadimos la función encargada de procesar la petición REGISTER OBSERVE. Esta función se encarga de sacar un observer de interestedObservers, colocarle los datos a un observer inválido y volverlo válido:

/* Función a ejecutar cuando se registra un nuevo observer*/
void ProcessObserveRegister (OCEntityHandlerRequest *ehRequest)
{
        OIC_LOG_V(DEBUG, TAG, "Received observation registration request with observation Id [%i]", ehRequest->obsInfo.obsId);
        for (uint8_t i = 0; i < SAMPLE_MAX_NUM_OBSERVATIONS; i++)
        {
            if (interestedObservers[i].valid == false)
            {
                interestedObservers[i].observationId = ehRequest->obsInfo.obsId;
                interestedObservers[i].valid = true;
                gLightUnderObservation = 1;
                break;
            }
        }
}

7.11. Añadimos la función encargada de deregistrar los observers cuando se recibe la petición de DEREGISTER OBSERVE. Lo que hace es cambiar la bandera valid de válido a inválido del observer indicado. Si ya no hay ningún observer observando, gLightUnderObservation quedará en falso, lo que impedirá que la función ChangeSwitchRepresentation notifique a los observers:

/* Función a ejecutar cuando se deregistra el observer*/
void ProcessObserveDeregister (OCEntityHandlerRequest *ehRequest)
{
        bool clientStillObserving = false;
        OIC_LOG_V(DEBUG, TAG, "Received observation deregistration request for observation Id  [%i]", ehRequest->obsInfo.obsId);
        for (uint8_t i = 0; i < SAMPLE_MAX_NUM_OBSERVATIONS; i++)
        {
            if (interestedObservers[i].observationId == ehRequest->obsInfo.obsId)
            {
                interestedObservers[i].valid = false;
            }
            if (interestedObservers[i].valid == true)
            {
                // Even if there is one single client observing we continue notifying entity handler
                clientStillObserving = true;
            }
        }
        if (clientStillObserving == false)
            gLightUnderObservation = 0;
}

7.12. Dentro de la función OCEntityHandlerCallback manejamos el evento de recibir una petición de OBSERVE, llamando a la función correcta de acuerdo a si la acción es register o deregister observer:

/* Colocar dentro del OCEntityHandlerCallback*/
        if (flag & OC_OBSERVE_FLAG)
        {
            OIC_LOG_V(INFO, TAG, "Flag includes OC_OBSERVE_FLAG");

            if (OC_OBSERVE_REGISTER == ehRequest->obsInfo.action)
            {
                OIC_LOG_V(INFO, TAG,"Received OC_OBSERVE_REGISTER from client");
                ProcessObserveRegister (ehRequest);
            }
            else if (OC_OBSERVE_DEREGISTER == ehRequest->obsInfo.action)
            {
                OIC_LOG_V(INFO, TAG,"Received OC_OBSERVE_DEREGISTER from client");
                ProcessObserveDeregister (ehRequest);
            }
        }

7.13. Agregamos la función ChangeSwitchRepresentation que será ejecutada en un thread y se encargará de notificar a los observers cuando haya un cambio en la luz:

/* Función a ser usada en la creación de un thread para observar*/ 
void *ChangeSwitchRepresentation (void *param)
{
        (void)param;
        OCStackResult result = OC_STACK_ERROR;

        //uint8_t numNotifies = (SAMPLE_MAX_NUM_OBSERVATIONS)/2;//OCObservationId obsNotify[numNotifies];

        while (!STOP)
        {
            sleep(3);
            if (SWITCH.value != SWITCH.lastValue){
                if (gLightUnderObservation)
                {
                    OIC_LOG_V(INFO, TAG, "=====> Notifying stack of new switch state: [%i]",SWITCH.value);
                    // Notifying all observers
                    result = OCNotifyAllObservers (SWITCH.handle, OC_NA_QOS);
                    SWITCH.lastValue = SWITCH.value;
                    if (OC_STACK_NO_OBSERVERS == result)
                    {
                        OIC_LOG_V(ERROR, TAG,"=====> No more observers exist, stop sending observations");
                        gLightUnderObservation = 0;
                    }
                }                
            }
        }
        return NULL;
}

7.14. Dentro de la función main() (al final del archivo) colocamos :

/*
* Esto va al principio del main, donde se declaran las variables
*/
pthread_t threadId;

.
.
.

/*
* Esto va después del mensaje: "Server is running"
*/
// Initialize observations data structure for the resource
for (uint8_t i = 0; i < SAMPLE_MAX_NUM_OBSERVATIONS; i++)
{
    interestedObservers[i].valid = false;
}

//Create a thread for changing the representation of the Light
pthread_create (&threadId, NULL, ChangeSwitchRepresentation, (void *)NULL);

.
.
.

/*
*  Esto va antes del mensaje: "Stopping IoTivity server"
*/
// Cancel the Light thread and wait for it to terminate
pthread_cancel(threadId);
pthread_join(threadId, NULL);

7.15. Compilar:

$ scons examples/OCFSecure -j 2 TARGET_TRANSPORT=IP
scons: done building targets.

7.16. Probarlo:

$ ~/iot/iotivity/out/linux/armv7l/release/examples/OCFSecure/light

Iotivity busca ser seguro, por lo cual define varios pasos para serlo.

El primero es onboarding, en el que se establece quién es el dueño del dispositivo. Por lo general es llamado transferencia de dueño, siendo el dueño original el fabricante y el dueño actual el usuario que lo compro.

Una vez establecido el dueño, se procede al provisioning. El provisioning es un paso en el que se provisiona al dispositivo con la información necesaria para ser usado. Entre esta información se encuentran los access control entries (ACEs) que son recursos ubicados en el access control list (aclist2), que definen los permisos de acceso a los recursos. Cada ACE puede dar permisos por tipo de conexión (conntype), rol, o por un dispositivo en específico (UUID). Si es por conntype, esta puede ser anónima y sin encriptar (anon-clear) o autenticada y encriptada (auth-crypt). Para poder conectarse de manera autenticada, se debe poseer una forma de verificar la identidad, por ejemplo las claves compartidas.

A continuación se muestra un típico ACE, que contiene recursos accesibles por cualquier dispositivo, /oic/res debe serlo para que sus recursos sean descubiertos. Al final se define el permiso (permission), CRUDN como un valor binario del 0 al 32: C(1) + R(2)+U(4)+D(8)+ N(16), Por ende los recursos de abajo solo permiten ser leídos (RETRIEVE: 2).

"acl": {
            "aclist2": [
                {
                    "aceid": 1,
                    "subject": { "conntype": "anon-clear" },
                    "resources": [
                        { "href": "/oic/res" },
                        { "href": "/oic/d" },
                        { "href": "/oic/p" }
                    ],
                    "permission": 2
                },

Vamos a compilar unas herramientas para editar archivos de seguridad. Para ello ejecutamos en un terminal lo siguiente:

$ cd ~/iot/iotivity/
$ scons resource/csdk/security -j 2 TARGET_TRANSPORT=IP

Podemos verificar los permisos de nuestra luz por medio de la herramienta svrdbeditor. Los archivos de seguridad se guardan en formato cbor con extensión de archivo .dat.

$ ~/iot/iotivity/out/linux/armv7l/release/resource/csdk/security/tool/svrdbeditor ~/iot/iotivity/out/linux/armv7l/release/examples/OCFSecure/ocf_svr_db_server.dat 

También se pueden generar los archivos de seguridad .dat a partir de un archivo.json, usando la herramienta json2cbor:

$ ~/iot/iotivity/out/linux/armv7l/release/resource/csdk/security/tool/json2cbor ~/iot/iotivity/examples/OCFSecure/ocf_svr_db_server_RFOTM.json ocf_svr_db_server.dat

Si se quiere resetear el estado del dispositivo para hacerle onboarding, se puede usar el archivo RFOTM:

El archivo RFNOP, es de un dispositivo ya provisionado:

$ cp ~/iot/iotivity/examples/OCFSecure/ocf_svr_db_server_RFOTM.dat ~/iot/iotivity/out/linux/armv7l/release/examples/OCFSecure/ocf_svr_db_server.dat
$ rm ~/iot/iotivity/out/linux/armv7l/release/examples/OCFSecure/switch_introspection.dat 

De vuelta a nuestro ejemplo:

Inspeccionando el archivo oic_svr_db.json, podemos ver que cuenta con credenciales. Posee una credencial por cada dispositivo con el que se puede conectar de forma segura. La credencial contiene el UUID del dispositivo dueño de la credencial, los datos y la codificación:

 "cred":{
            "creds": [
                {
                    "credid": 1,
                    "subjectuuid": "12345678-1234-1234-1234-123456789012",
                    "credtype": 1,
                    "privatedata":{
                        "data":"AAAAAAAAAAAAAAAA",
                        "encoding": "oic.sec.encoding.raw"
                    }
                }],
            "rowneruuid": "32323232-3232-3232-3232-323232323232"
}

Ready for provisioning:

        "pstat": {
            "dos": { "s": 1, "p": false },
            "isop": false,
            "cm": 2,
            "tm": 0,
            "om": 4,
            "sm": 4,
            "rowneruuid": ""
        },
        "doxm": {
            "oxms": [0],
            "oxmsel": 0,
            "sct": 1,
            "owned": false,
            "deviceuuid": "00000000-0000-0000-0000-000000000000",
            "devowneruuid": "",
            "rowneruuid": ""
        }
}

Provisioned:

"pstat": {
            "dos": { "s": 3, "p": false },
            "isop": true,
            "cm": 0,
            "tm": 0,
            "om": 4,
            "sm": 4,
            "rowneruuid": "32323232-3232-3232-3232-323232323232"
},
"doxm": {
            "oxms": [0],
            "oxmsel": 0,
            "sct": 1,
            "owned": true,
            "deviceuuid": "32323232-3232-3232-3232-323232323232",
            "devowneruuid": "32323232-3232-3232-3232-323232323232",
            "rowneruuid": "32323232-3232-3232-3232-323232323232"
},
sct:

supported credential type

Hay otras formas de hacer provisioning:

Onboarding Tool Generic Client (OTGC)

(https://github.com/openconnectivityfoundation/development-support/tree/master/otgc)

Es una aplicación que funciona como aprovisionador y cliente genérico para hacer debugging.

Windows

Descargar e instalar Device Spy (Doble clic o enter al archivo descargado):

Descargar DeviceSpy

Android (5.0.1+)

Descargar e instalar OTGC (pasos de instalación arriba):

Descargar OTGC

Linux

Ejecutar el siguiente comando en un terminal:

$ curl https://openconnectivityfoundation.github.io/development-support/otgc/linux/install.sh | bash

Linux Iotivity provisioner

https://github.com/iotivity/iotivity/tree/cabc8d2e24ac1a406f9f70d3fcd066c5daedad80/resource/provisioning

Compilar el proyecto de IoTivity, y dentro de los resultados en la carpeta out/ se encuentra el provisioning client:

$ cd iotivity && scons resource/provisioning -j 2
$ ~/iot/iotivity/out/linux/x86_64/release/resource/provisioning/examples/provisioningclient

Copiar el oic_svr_db_client.dat a la carpeta donde se ejecuta el programa, puede ser:

/iotivity/out/linux/armv7l/release/examples/OCFSecure/

Provisioning manualmente

Hay dos alternativas:

Android Smart Home Demo

(https://git.cti.espol.edu.ec/labproto/ariotivity-provisioner.git) Esta aplicación fue usada inicialmente para hacer provisioning, pero fue descontinuada y ya no existe su repositorio en Github. Se instala descargando el apk y siguiendo los pasos de arriba.

Una de las ventajas de IoTivity es que es multiplataforma, facilitandonos el uso desde servidores hasta dispositivos con recursos limitados como un ESP32Thing. También cuenta con bindings para algunos lenguajes como lo son C, C++, Java, Node.js. Para crear nuestro interruptor virtual vamos a hacer uso de iotivity-node,que es IoTivity en Node.js (Javascript).

Para instalar node, vamos a hacer uso de un manejador de versiones de Node.js, para lo cual hay que ejecutar el siguiente comando para instalarlo:

$ curl https://raw.githubusercontent.com/creationix/nvm/v0.33.8/install.sh | bash

Cerrar y abrir el terminal para usar nvm. Instalar la versión lts/dubnium de node.js por medio de nvm:

$ nvm install lts/dubnium

Instalar iotivity-node:

$ npm i iotivity-node

Al final, se muestran unos warn, no se preocupen.

Para ejecutar un ejemplo sencillo y comprobar su correcta instalación nos cambiamos al directorio ~/node_modules/iotivity-node/js y ejecutamos un server:

$ cd ~/node_modules/iotivity-node/js
$ node server.get.js
Starting OCF stack in server mode
Registering resource
Server ready

No es necesario instalar el repositorio de iotivity cuando vamos a usar iotivity-node, pero a veces es útil tenerlo para usar sus herramientas.

Instalar nw.js

Usamos nwjs para ejecutar una aplicación de nodejs como una aplicación de escritorio.

$ npm i -g nwjs # no funciona, no usar
$ cd ~
$ wget -qO- nwjs.zip https://dl.nwjs.io/v0.35.4/nwjs-sdk-v0.35.4-linux-x64.tar.gz | tar xvz

Creamos el archivo interruptor.js y lo editamos en nano:

$ cd ~
$ mkdir iot
$ nano interruptor.js

Declaramos variables especificas a iotivity:

interruptor.js

var ocf = require('iotivity-node'),
    client = ocf.client;

Configuramos nuestro cliente para que use la especificación 1.1.0:

ocf.device = Object.assign(ocf.device, {
    coreSpecVersion: 'ocf.1.1.0',
    dataModels: ['res.1.1.0']
});

Creamos una variable que va a almacenar la información de recurso de las luces encontradas:

interruptor.js

// Stores the resource info of each light switch found
var switchesFound;

Agregamos unas funciones para mostrar información y ayudarnos a ver que sucede.

interruptor.js

// The html id of each device is formed by resource.deviceId + ':' + resource.resourcePath;

// Show text in the bottom status bar
function log(string) {
    document.getElementById('statusBar').innerHTML = string;
}

function serverError(error) {
    log('Server return error:', error.message);
}

Registramos una función que se encargará de agregar las representaciones de la luz en forma de switch a la interfaz gráfica. Al agregar registrar un listener al interruptor, que cuando sea activado actualice el estado en el interruptor de manera remota.

interruptor.js

// Add card to UI
function addResourceHolderToUI(resource) {
    var cards = document.getElementById('resources'),
        node = cards.firstElementChild;
    if (cards.style.display === 'none') {
        cards.style.display = 'block';
    } else {
        node = node.cloneNode(true);
        cards.appendChild(node);
    }
    node.getElementsByClassName('resourceUUID')[0].innerHTML = resource.deviceId;
    node.getElementsByClassName('resourcePath')[0].innerHTML = resource.resourcePath;
    
    // Register a listener to update (make a post) the resource when activated
    var checkbox = node.getElementsByClassName('checkbox')[0];
    checkbox.id = resource.deviceId + ':' + resource.resourcePath;
    checkbox.onclick = function() {
        var resource = switchesFound[this.id];
        resource.properties.value = this.checked;
        client.update(resource);
    }
}

Declaramos la función purgeAllResourceHoldersFromUI que hace lo opuesto a la anterior, eliminar todos los interruptores.

interruptor.js

// Delete all switch cards from UI
function purgeAllResourceHoldersFromUI() {
    var cards = document.getElementById('resources');
    while (cards.childElementCount > 1)
        cards.removeChild(cards.lastElementChild);
    cards.style.display = 'none';
}

Agregamos una función que se encarga de eliminar un recurso. Lo elimina de la interfaz gráfica y de la lista de recursos, también elimina sus listeners.

interruptor.js

// Delete resource from UI
function deleteResource(resource) {
    var id = this.deviceId + ':' + this.resourcePath;
    log('deleteResource(' + this.resourcePath + ')');

    var resource = switchesFound[id];
    if (resource) {
        // Remove listeners associated to the resource to be deleted
        resource.removeListener('update', observeResource);
        resource.removeListener('delete', deleteResource);
        // Delete resource info
        delete switchesFound[id];

        var child = document.getElementById(id);
        var parent = child.parentElement;
        while (parent.id != 'resources')
            child = parent, parent = parent.parentElement;
        if (parent.childElementCount > 1)
            parent.removeChild(child);
        else {
            // remove from layout instead of from DOM
            parent.style.display = 'none';
        }
    }
}

Creamos una función para actualizar al interruptor con el valor actual.

interruptor.js

// Observe resource
function observeResource(resource) {
    // Validates that the resource has the 'value' property
    if (('properties' in resource) && ('value' in resource.properties)) {
        var id = resource.deviceId + ':' + resource.resourcePath;
        // Update visual switch with the actual value of the resource
        document.getElementById(id).checked = resource.properties.value;
    }
}

La función discoverBinarySwitch se ejecuta al presionar el botón Discover. Lo que hace es eliminar todo y comenzar de cero. Realice una petición multicast en busca del recurso de tipo 'oic.r.switch.binary', cuando alguien le responde ejecuta la función resourceFound

interruptor.js

// Executed when the discover button is activated
function discoverBinarySwitch() {
    log('Discovering...');
    // Start from zero, delete everything
    purgeAllResourceHoldersFromUI();
    switchesFound = {};
    client.on('error', serverError)
          .findResources({ 'resourceType': ['oic.r.switch.binary'] }, resourceFound).then(
        function() {
            log('findResources() successful');
        },
        function(error) {
            log('findResources() failed with ' + error);
        });
}

Finalmente, agregamos la función previamente mencionada. Esta función crea un elemento HTML con id resultante de la concatenación del deviceID y path del recurso (deviceID:path). De igual manera se registran los listeners para update y delete.

interruptor.js

// Handler to be executed when a resource is found
function resourceFound(resource) {
    log('Resource found: ' + resource.deviceId);
    var id = resource.deviceId + ':' + resource.resourcePath;
    // If it wasn't previously found, we add it to the switches found and add listeners to it.
    if (!switchesFound[id]) {
        switchesFound[id] = resource;
        resource.addListener('update', observeResource);
        resource.addListener('delete', deleteResource);
        resource.addListener('error' , serverError);
        addResourceHolderToUI(resource);
    }
}

Ctrl+x , y y luego Enter para guardar y salir.

Creamos el archivo y lo editamos en nano:

$ cd ~/iot
$ nano interruptor.js

En el tag HEAD, añadimos un SCRIPT tag que llame al código javascript del interruptor.

interruptor.html

<!DOCTYPE html>
<html>
<head>
  <title>IoTivity Simple Client</title>
  <link rel="stylesheet" type="text/css" href="styler.css"/>
  <script src="interruptor.js">
</script>
</head>
<body>
  <div>
    Client UUID: <label id="uuid"></label>
  </div>
  <div>
    <button onclick="discoverBinarySwitch()">Discover</button>
  </div>
  <hr>
  <div id="resources" style="display: none">
    <div class="resourceHolder">
      <label class="resourceUUID"></label>
      <div>
        <label class="switch">
          <input type="checkbox" class="checkbox"><span class="slider round"></span>
        </label>
        <label class="resourcePath"></label>
      </div>
    </div>
  </div>

  <div id="statusBar"></div>
  <script type="text/javascript">
    document.getElementById("uuid").innerHTML = ocf.device.uuid;
  </script>
</body>

</html>

Ctrl+x , y y luego Enter para guardar y salir.

Creamos un package.json para indicar el punto de entrada de nuestra aplicación de escritorio.

$ cd ~/iot
$ nano package.json

package.json

{
  "name": "interruptor",
  "main": "interruptor.html"
}

Ctrl+x , y y luego Enter para guardar y salir.

Hora de probarlo:

$ ~/nwjs-sdk-v0.35.4-linux-x64/nw .

La luz representada como un interruptor aparece en la interfaz gráfica, pero al activarla no sucede nada con el foquito, dado que no contiene las claves compartidas. Pero puede activar dispositivos no seguros como por ejemplo el smart plug.

Información sobre iotivity con Raspberry Pi:

https://openconnectivity.org/developer/developer-kit

Videos de OCF Developer training:

https://www.youtube.com/watch?v=Ug6yUpVU2VM&list=PLz9Us2KgBfxBB1EBtjHdkrMM16ynCykLA

Instalar node-red:

$ npm -g install node-red
$ node-red
17:03:57 - [info] Server now running at http://127.0.0.1:1880/

Instalar iotivity-node localmente en node-red para poder usarlo:

$ cd ~/.node-red/node_modules
$ git clone https://github.com/otcshare/iotivity-node
$ cd iotivity-node
$ npm install

Edit settings.js file:

$ sudo find / -name "settings.js"
$ vi ~/.node-red/settings.js 

Insertar la siguiente línea dentro de la función functionGlobalContext:

Settings.js

functionGlobalContext: {
      iotivity:require("iotivity-node/lowlevel")
},

Ejecutar node-red:

$ node-red

Para escribir funciones en node-red que usen Iotivity, hay que usar global.get, en node.js puro se hace como en la línea comentada:

//iotivity = require( "iotivity-node/lowlevel" );
iotivity = global.get('iotivity');

Para mostrar mensajes en node-red usamos la función node.log:

node.log( "Starting OCF stack in client mode" );