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.
En este codelab, usted construirá un interruptor y una luz que integren IoTivity. Su foquito va a:
Su interruptor va a:
|
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:
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:
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.
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
# nano también crea archivos pero al crearlos permite editarlos
$ nano wpa_supplicant.conf
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
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.
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:
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
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
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:
iotivity.OCMode.OC_CLIENT
iotivity.OCMode.OC_SERVER
iotivity.OCMode.OC_CLIENT_SERVER
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')
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:
ocf_svr_db_server_RFOTM.dat
El archivo RFNOP, es de un dispositivo ya provisionado:
ocf_svr_db_server_RFNOP.dat
$ 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:
(https://github.com/openconnectivityfoundation/development-support/tree/master/otgc
)
Es una aplicación que funciona como aprovisionador y cliente genérico para hacer debugging.
Descargar e instalar Device Spy (Doble clic o enter al archivo descargado):
Descargar e instalar OTGC (pasos de instalación arriba):
Ejecutar el siguiente comando en un terminal:
$ curl https://openconnectivityfoundation.github.io/development-support/otgc/linux/install.sh | bash
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:
.json
indicando los ACLs, credenciales y estado del aprovisionamiento y convertirlo a archivo .dat mediante la herramienta json2cbor
localizada en iotivity/out/linux/armv7l/release/examples/OCFSecure/
(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:
ocf
: carga el paquete de iotivity.client
: instancia un cliente de iotivity.var ocf = require('iotivity-node'),
client = ocf.client;
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:
// Stores the resource info of each light switch found
var switchesFound;
Agregamos unas funciones para mostrar información y ayudarnos a ver que sucede.
// 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.
// 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.
// 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.
// 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.
// 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
// 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.
// 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
<!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.package.json
para indicar el punto de entrada de nuestra aplicación de escritorio. $ cd ~/iot
$ nano 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
:
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" );