En las anteriores entradas vimos como implementar un cliente de ejemplo usando IceC para Arduino. En este post veremos los pasos necesarios para implementar un servidor básico, junto con su cliente en Python.
Introducción
Uno de los usos más comunes que se le da a Arduino es el de modificar el estado de algún actuador: ya sea un LED incluido en la misma placa, un relé conectado al sistema de riego o un servo que modifica el ángulo de ataque de las palas de un drone.
En todos los casos anteriores, es necesario que en Arduino se implemente un servidor que acepte y procese las peticiones de sus clientes. Utilizando IceC, este servidor se reduce a la construcción de un sirviente que contenga los métodos definidos en la interfaz correspondiente.
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 para el Servidor
En este ejemplo mínimo vamos a controlar el LED integrado en la placa de Arduino. Para ello, como en el caso del cliente, definimos primero la interfaz Slice que se usará. Crea una carpeta para el proyecto, por ejemplo, led-server:
mkdir led-server cd led-server
La definición de la interfaz debe incluirse en un fichero con extensión .ice. Por ejemplo, example.ice. En este caso, solo contendrá un método destinado a establecer el estado del LED, pasando como parámetro un booleano. Quedaría así:
module Example { interface LED { void setState(bool s); }; };
Para generar el fichero que deberás incluir en tu sketch, utiliza el siguiente comando (esto generará un archivo llamado example.h, como ya vimos en la anterior entrada):
slice2c example.ice
A continuación, crea el fichero vacío de tu sketch, para poder abrirlo sin problemas desde el IDE de Arduino:
touch led-server.ino
Ábrelo con tu editor favorito (o con el IDE), e incluye el siguiente boilerplate (si seguiste las entradas anteriores, verás que es el mismo código básico que empleaste allí):
#include <IceC.h> #include <SerialEndpoint.h> #include "example.h" Ice_Communicator ic; void setup() { Ice_initialize(&ic); SerialEndpoint_init(&ic); } void loop() {}
Dado que no harás invocaciones desde tu Arduino, no es necesario construir ningún proxy. Así pues, si compilas el proyecto tal y como lo tienes hasta ahora, no tendrás problemas en la compilación, pero realmente no hará mucho. Todavía necesitas implementar el sirviente y registrarlo en el adaptador de objetos.
Sirviente
En la interfaz Slice has definido un único método, que posee una implementación por defecto que no hace nada (generada por la herramienta slice2c). Para establecer un comportamiento diferente, necesitas implementar el método setState.
Dado que IceC está desarrollando en su mayor parte en el lenguaje C, los métodos definidos en la interfaz se mapean a funciones en C. El nombre de la función contiene: el nombre del módulo, el nombre de la interfaz acabado en I y el nombre del método, separados por guiones bajos, «_». Como primer parámetro, acepta un puntero a una estructura que representa al sirviente (hablaremos de su uso en otra entrada). A continuación, se pasan los parámetros que se definieron en el Slice, siguiendo el mapping de tipos apropiado.
Por ejemplo, en el caso que nos ocupa, el nombre del método que has de implementar será Example_LEDI_setState. Como primer parámetro, recibirá un Example_LEDPtr, que es un puntero al sirviente. El siguiente será el booleano que indica el estado, y que en este caso, estará mapeado a Ice_Bool. La firma del método completa es:
void Example_LEDI_setState(Example_LEDPtr self, Ice_Bool s);
NOTA: Si tienes alguna duda de las funciones que tienes que implementar, siempre puedes buscar las declaraciones en el fichero que se genera a partir del Slice (en este caso, example.h). Busca las funciones que tengan __attribute__((weak)).
La implementación es bastante sencilla: simplemente cambia el estado del LED en función del valor de la invocación. Recuerda que antes necesitarás configurar el pin del LED como salida (OUTPUT). El siguiente código muestra los cambios necesarios:
[...] void Example_LEDI_setState(Example_LEDPtr self, Ice_Bool s) { digitalWrite(LED_BUILTIN, s ? HIGH : LOW); } void setup() { pinMode(LED_BUILTIN, OUTPUT); [...]
Registro del Sirviente
Una vez que has implementado el sirviente, necesitarás crear una instancia, y registrarla en un adaptador de objectos. Para ello, es necesario crear una variable de su tipo (del tipo de la interfaz), y un adaptador de objetos (créalos a nivel global, para evitar que se destruyan antes de tiempo):
[...] Ice_Communicator ic; Ice_ObjectAdapter adapter; Example_LED servant;
A continuación, en la función setup, debes inicializar el sirviente:
Example_LED_init(&servant);
Para crear el adaptador de objetos, se utiliza un método del communicator (o broker). Este método acepta como primer parámetro el broker. Después se ha de indicar el nombre del adaptador, sus endpoints, y por último un puntero al objeto adapter para inicializar su estado. En los endpoints, simplemente especificaremos serial, pues este endpoint no tiene parámetros. Así, la construcción sería (también en la función setup):
Ice_Communicator_createObjectAdapterWithEndpoints (&ic, "Adapter", "serial", &adapter);
Añadirlo al adaptador de objetos es bastante directo. Has de especificar el nombre (la identidad) del objeto en cuestión, en este caso LED. Como nota, es necesario hacer un casting a Ice_ObjectPtr al sirviente, pues la función *_add es genérica. También necesitas activar el adaptador de objetos, para hacer accesibles sus sirvientes. Quedaría como:
Ice_ObjectAdapter_add (&adapter, (Ice_ObjectPtr)&servant, "LED"); Ice_ObjectAdapter_activate(&adapter);
Por último, en el bucle de eventos (loop), es necesario llamar a una función que se encargue de comprobar si hay o no mensajes en la cola de entrada, y de procesarlos en su caso. Para ello, tenemos dos funciones, una es bloqueante y la otra no, y su uso dependerá de si necesitamos realizar otras tareas o no. El ejemplo que estamos tratando permite el uso de la función bloqueante:
void loop() { Ice_Communicator_waitForShutdown(&ic); }
Cliente mínimo en Python
Para probar que la implementación del servidor es correcta, necesitas un cliente. Dado que IceC es una implementación compatible con Ice, el cliente lo puedes escribir en el lenguaje que prefieras, dentro del conjunto soportado por Ice. En este post, usaremos Python.
El esqueleto básico para el cliente no difiere mucho del empleado en las entradas anteriores:
import sys import Ice Ice.loadSlice("example.ice") import Example class Client(Ice.Application): def run(self, args): ic = self.communicator() if __name__ == "__main__": Client().main(sys.argv)
Partiendo de ese esqueleto, lo primero que necesitas es construir el proxy al objeto remoto. Recuerda que debes incluir en el proxy los parámetros necesarios para que el endpoint contacte con el servicio SerialService; dado que usarás la misma configuración que usaste en el ejemplo anterior, los endpoints son los mismos (si necesitas más información, revisa el artículo Comunicación Serie con Arduino (II)). Por otro lado, la identidad que usaste en el sketch de Arduino fue LED, por lo que la versión textual del proxy sería:
LED -e 1.0 -d:serial -h 127.0.0.1 -p 1793
NOTA: El modo de las comunicaciones es datagram, -d, que es el soportado actualmente por el endpoint. Por otro lado, IceC solo implementa la versión 1.0 del protocolo IceP, por lo que es necesario especificarlo manualmente (-e 1.0).
Construir el proxy en el cliente implica crear el proxy indicado, y hacer un casting a la interfaz que vayas a usar (Example.LED):
strprx = "LED -e 1.0 -d:serial -h 127.0.0.1 -p 1793" proxy = ic.stringToProxy(strprx) led = Example.LEDPrx.uncheckedCast(proxy)
Por supuesto, tendrás que realizar la invocación a setState. En este ejemplo, usarás el primer parámetro que pases por la línea de órdenes como valor del estado del LED (1 o 0):
state = args[1] == "1" led.setState(state)
Probando el Servidor
Para ejecutar el cliente, necesitarás un fichero de configuración que habilite el soporte del endpoint serie. Puedes usar el mismo que en el artículo anterior (llámalo client.config):
Ice.Plugin.PyEndpoint = IcePyEndpoint:addPyEndpointSupport PyEndpoint.Module = SerialEndpoint PyEndpoint.Factory = EndpointFactoryI
También necesitas tener arrancado el servicio SerialHandler, para lo cual puedes utilizar de nuevo la configuración del ejemplo anterior (serial-service.config):
SerialService.Device = /dev/ttyS1 SerialService.Speed = 115200 SerialService.Host = 127.0.0.1 SerialService.Port = 1793
Ejecútalo con:
serial-service --Ice.Config=serial-service.config
Si tienes conectado el Arduino al puerto serie, con el sketch del servidor cargado, al ejecutar el cliente de la siguiente forma verás como cambia el estado del LED:
python client.py --Ice.Config=client.config 1 python client.py --Ice.Config=client.config 0
Y si lo has conseguido, ¡enhorabuena de nuevo! Acabas de implementar una aplicación distribuida usando IceC y Arduino 😉