f6ripaphqf9h5io-medium

En los anteriores artículos de esta serie vimos cómo crear clientes y servidores para Arduino con IceC, utilizando el puerto Serie. En esta sección veremos los primeros pasos necesarios para ampliar la conectividad usando IDM.

Breve Introducción

La mayor ventaja que nos ofrece usar el puerto serie en Arduino es que la mayoría de éstos cuentan con uno, desde el pequeño Arduino Pro Mini, hasta los más comunes Arduino UNO, Fio o Zero. Esto implica que no es necesario emplear hardware accesorio (como un transceiver de XBee o de RS-485). El mayor problema radica en que sólo podremos hablar con el dispositivo que esté directamente conectado a ese puerto, lo que limita gravemente su utilidad.

Para resolver este problema, podemos emplear IDM. Si conectamos un router IDM al puerto serie, podremos mandar mensajes a cualquier punto de la red, esté donde esté. En esta sección analizaremos cómo.

Nota: Para una explicación más detallada sobre porqué nació IDM, cómo funciona, sus ventajas o la arquitectura subyacente, véase Infraestructura IoT con IDM.

Anunciamientos

Cualquier objeto que pertenezca a la red IDM, ha de ser accesible (y será accedido) por su router ‘principal’. Para que este router conozca los endpoints de un objeto en particular, el objeto ha de ser anunciado en dicho router. ¿Cómo? Veamos la interfaz que implementa el router (sólo los métodos que nos interesan):

module IDM {
  [...]
  module NeighborDiscovery {

    interface Listener {
      void adv(string proxy);
      void bye(string oid);
    };
[...]

Nota: esta interfaz está definida en el fichero /usr/share/slice/idm/idm.ice, que a su vez, está en el paquete idm. Si tienes el repositorio Debian de Pike, puedes instalarlo con:

sudo apt-get install idm

La interfaz NeighborDiscovery está implementada en todos los routers IDM. Consta de dos métodos: adv bye. En este contexto, el método que nos interesa es el empleado para publicar la existencia de objetos en la red: adv (elipsis de advertisement). El parámetro que pasaremos será una representación textual del proxy del objeto.

Nota: En el caso del endpoint serie, sabemos que el router usará el servicio SerialService, por lo que los endpoints que empleará para contactar con el objeto en el Arduino han de contar con esa información, y por ello hemos de incluirla en el anunciamiento. Estos endpoints son conocidos como Published Endpoints. Para más información acerca del concepto de Published Endpoints, véase ZeroC Ice: Published Object Adapter Endpoints (en inglés).

Así, a modo de ejemplo, supongamos que tienes un objeto llamado LED en tu Arduino (lo cual será cierto si seguiste los pasos indicados en Comunicación Serie con Arduino: servidor mínimo), y quieres que cualquiera pueda acceder a él, por lo que anuncias su proxy. La representación textual de ese proxy sería:

LED -e 1.0 -d:serial -h 127.0.0.1 -p 1793

Que, como vemos, es la misma que la que se empleó en el artículo anterior para el cliente en Python (al margen de que, como veremos, esa identidad sea o no válida para IDM). De hecho, en este post partirás de ese mismo ejemplo. Puedes copiar todos los ficheros del proyecto (la carpeta entera) a idm-advs:

cp -r led-server/ idm-advs
cd idm-advs/
mv led-server.ino idm-advs.ino

Veamos las modificaciones que tendrás que hacer tanto en el sketch como en el cliente para usar IDM.

Nota: puedes descargarte todo el código fuente y los archivos de configuración para este ejemplo (y los demás de esta serie de artículos) desde el repositorio de bitbucket:

hg clone https://bitbucket.org/arco_group/arduino-icec-idm

Sketch de Arduino

Antes de nada, dado que vas a usar la interfaz de IDM para hacer los anunciamientos, necesitarás traducir la especificación en Slice a código en C que puedas incluir en tu sketch. El proceso es el mismo que para tu interfaz example.ice (recuerda que el fichero idm.ice está en el paquete Debian idm):

slice2c /usr/share/slice/idm/idm.ice

Esto generará un fichero idm.h, que debes incluir en el proyecto:

#include "idm.h"

Una vez visto eso, lo siguiente que tendrás que hacer es crear el proxy al router para anunciar tus objetos. Un router IDM tiene como identidad su dirección IDM, que (en el caso que nos ocupa) está formada por dos bytes, en hexadecimal. El primer byte indica la red IDM a la que pertenece el objeto, y el segundo identifica al objeto en sí.

Para este ejemplo, puedes asignar estos valores arbitrariamente; por ejemplo, utiliza A0 para la red, y 01 para el objeto que hay en el router (debes recordar estos datos, pues los utilizarás al configurar el router más tarde). El proxy de ese router sería:

A001 -d:serial

Sencillo, ¿verdad? Pues con esa información, ya puedes crear el proxy. Recuerda que necesitarás una instancia de ObjectPrx creada en el ámbito global, y utilizar el método del broker para construir el proxy en el setup. Modifica el sketch de la siguiente forma:

Ice_ObjectPrx router;
[...]

void setup() {
  [...]
  Ice_Communicator_stringToProxy(&ic, "A001 -d:serial", &router);

Un detalle que debes tener en cuenta es la identidad del objeto que tenías en el ejemplo. Antes se llamaba simplemente LED. Este nombre, sin embargo, no es válido si estás usando IDM, pues necesita ser una dirección IDM válida. Como has usado la red A0 para el router, tu objeto debe formar parte de esa misma red. Como identificador del objeto puedes elegir cualquier número que esté libre, por ejemplo 02, con lo que su dirección sería A002. Así, el registro del sirviente cambia a lo siguiente:

Ice_ObjectAdapter_add(&adapter, (Ice_ObjectPtr)&servant, "A002");

Ahora necesitas mandar los anunciamientos de forma periódica. Para ello, es conveniente que crees una función que encapsule esta tarea, y que la llames desde el bucle de eventos. La función simplemente tendrá que usar el método adv para mandar el mensaje. El único parámetro que tiene este método es el stringfied proxy, que es de tipo Ice_String. Una posible implementación sería esta:

void send_advs() {
  const char* strprx = "A002 -d:serial -h 127.0.0.1 -p 1793";
  Ice_String ice_strprx;
  Ice_String_init(ice_strprx, strprx);
  IDM_NeighborDiscovery_Listener_adv(&router, ice_strprx);
}

Nota: Ice_String_init no copia la cadena, por lo que deberás asegurarte de que sigue existiendo mientras estés usando el Ice_String.

Una vez tengas la función de anunciamientos, tienes que llamarla de forma regular, con una periodicidad que dependerá de la aplicación (en este ejemplo serán 5 segundos, pero generalmente entre 3 y 5 minutos sería suficiente). Esto es útil, pues permite al router determinar qué objetos han dejado de estar disponibles (por un corte de corriente, un cambio de red, etc.) simplemente con un algoritmo de inanición.

Para hacer los anunciamientos periódicos, puedes utilizar la función millis() de Arduino, y determinar cuándo ha pasado cierto lapso de tiempo. Por otro lado, dado que en el bucle de eventos (loop) necesitas ejecutar una segunda tarea, no puedes llamar a la función bloqueante del broker para que procese los mensajes. Debes cambiarla por la función no-bloqueante. Todo esto implica que el bucle de eventos queda de la siguiente forma:

unsigned long t1 = millis();

void loop() {
  // every 5 seconds 
  if (millis() - t1 > 5000) {
    t1 = millis();
    send_advs();
  }

  // as frequent as possible 
  Ice_Communicator_loopIteration(&ic);
}

Dadas esas modificaciones, ya puedes subir el sketch al Arduino. Para probarlo, necesitarás cambiar ligeramente el cliente de Python, y lanzar el router de IDM. Empecemos con éste último.

Router IDM

Ya tienes a tu Arduino anunciándose en el router, por lo que lo siguiente que necesitas es lanzar el router de IDM, que está disponible en el paquete Debian de idm. Para ello, crea un fichero de configuración donde especificarás algunos parámetros (llámalo router.config). Lo primero que deberás añadir es el soporte para el endpoint serie, pues en esta ocasión será el router quién lo utilice:

Ice.Plugin.PyEndpoint = IcePyEndpoint:addPyEndpointSupport
PyEndpoint.Module = SerialEndpoint
PyEndpoint.Factory = EndpointFactoryI

El primer parámetro específico que puedes incluir es la identidad (o identidades, separadas por ‘|’) del router, que obviamente debe ser la misma que la que usaste en el sketch para el router:

Router.Ids = A001

Por otro lado, es imperativo que indiques los endpoints en donde quieres que el router esté disponible. En concreto, necesitas que use el puerto serie para hablar con el Arduino, y un socket TCP para comunicarse con el resto de clientes (por convenio, se usa el puerto 6140):

Router.Adapter.Endpoints = 
   tcp -h 127.0.0.1 -p 6140 : serial -h 127.0.0.1 -p 1793

Nota: como puedes apreciar, todavía necesitarás el SerialService, pero en este caso será el router quién lo utilice, no el cliente.

El router almacena en un fichero la tabla de rutas, por lo que también deberás especificar donde puede guardar ese fichero. Por ejemplo:

Router.Table.Path = router.table

Y listo. Aunque, antes de lanzar el router, necesitas lanzar el SerialService. Como utilizas la misma configuración que en el ejemplo del que partimos, solo tienes que ejecutar:

serial-service --Ice.Config=serial-service.config

Por fin, lanza el router:

idm-router --Ice.Config=router.config

Si todo ha ido bien, deberás ver un registro de los anunciamientos del Arduino (uno cada 5 segundos), algo similar a la siguiente captura:

screenshot-from-2016-10-19-14-26-52

Cliente de Python

El cliente que usaste en la entrada anterior utilizaba el endpoint serie para comunicarse directamente con el Arduino. En este caso, sin embargo, la invocación será reenviada por el Router IDM. Una vez que el sirviente ha sido anunciado en el router, cualquier cliente puede acceder a él, utilizando o bien el mismo router, o cualquier otro de la red. Deberás construir el proxy teniendo en cuenta lo siguiente:

  • Se utiliza la identidad del objeto remoto, en este caso A002
  • Se construye el proxy con los endpoints del router que se vaya a emplear.

Nuestro ejemplo está en un entorno muy sencillo, que solo tiene un router, por lo que será el que emplee el cliente. Así, el proxy queda de la siguiente forma:

A002 -e 1.0 -t:tcp -h 127.0.0.1 -p 6140

Como puedes observer, hemos prescindido de los endpoints serie del router en este proxy, pues no son necesarios para acceder a él. Podemos conectar directamente a través de TCP. Esto implica que la configuración del broker del cliente no necesita añadir soporte para el endpoint serie; puedes ignorar (o borrar) la configuración del cliente que usaste en el ejemplo anterior:

rm client.config

Ahora, edita el fichero client.py para cambiar el proxy del objeto remoto, y emplear el que acabamos de describir:

str_prx = "A002 -e 1.0 -t:tcp -h 127.0.0.1 -p 6140"

Y por último, puedes ejecutar el cliente y ver como, de nuevo, cambia el estado del LED en el Arduino:

python client.py 1
python client.py 0

¡Enhorabuena! Acabas de implementar tu primer ejemplo con IDM, rompiendo las barreras del puerto serie 🙂