Casos de estudio

Vamos a explicar someramente algunos de los ejemplos incluídos en el software que acompaña al taller. Se trata de ejemplos ligeramente más complejos que los ejemplos triviales, de forma que pueda apreciarse la ventaja de una arquitectura software sólida.

Estos casos de estudio tienen dos propósitos:

  • Que el alumno sea consciente de la complejidad real de los sistemas basados en Raspberry Pi. Los ejemplos más evolucionados apenas ocupan unos cientos de líneas de código con módulos de muy bajo acoplamiento y funciones que no superan las 20 líneas.

  • Que le sirvan al alumno como fuente de inspiración para que sus proyectos personales sean más creativos y evolucionados.

Antes de nada vamos a ver un mini-caso de estudio en el que realizaremos una de las operaciones más frecuentes cuando se diseña una nueva aplicación, la creación de un nuevo manejador. Esto se va a repetir multitud de veces en nuestros casos de estudio y en la vida real. Es la base de la programación de sistemas: la abstracción. Abstraer es eliminar detalle. Abstraemos para simplificar tanto el uso como la programación de los módulos.

  • Cada manejador trata un problema pequeño y nada mas. Por tanto su programación es relativamente sencilla y tiene poca relación con el resto del programa.

  • Cuando se usa el manejador no es preciso conocer los detalles de cómo se implementa. Solo lo estrictamente necesario.

Sensor analógico

Hemos visto cómo puede medirse una magnitud analógica utilizando un montaje con la pata MISO de la interfaz SPI y una entrada digital. Es hora de convertir esa técnica en un nuevo manejador de eventos para la automatización.

El reto es definir el manejador analog_handler para detectar cuando se pasan ciertos umbrales previamente configurados. Debe cumplir los siguientes requisitos:

  • Debe poder seleccionarse la pata digital, los umbrales de activación y desactivación, el periodo de muestreo y la frecuencia del reloj.

  • Debe medir periódicamente usando el método descrito en el capítulo sobre SPI.

  • Debe notificar cuando se supera el umbral de activación y cuando el valor queda por debajo del umbral de desactivación.

  • Una vez notificada una activación o una desactivación ya no vuelve a notificar nada hasta que cambie el estado (pasa de activado a desactivado o a la inversa).

Bien, el problema está claro, vamos a resolverlo paso a paso.

Empezando por el final

No me cansaré de repetir esto: si quieres que funcione, las pruebas primero. Antes de empezar a hacer nada del nuevo manejador tenemos que hacer un caso de prueba típico. Esto nos ayuda a entender completamente la interfaz de programación que se pretende crear y nos permitirá en el futuro tener cierta confianza en el correcto funcionamiento del manejador.

Es el primer ejemplo de Reactor, así que vamos a ayudar un poco. En la carpeta src/c/ejemplos/analog tienes un esqueleto de lo que queremos construir. Echa un vistazo a test_analog_handler.c. Como ves sigue el convenio de todas las pruebas de la biblioteca Reactor, un único archivo que comienza por test_ junto al nombre del módulo que queremos probar. Procura respetar este convenio, hace que cualquier usuario de tu código encuentre las cosas más fácilmente.

#include "analog_handler.h"
#include <reactor/reactor.h>
#include <wiringPi.h>
#include <stdio.h>

static void bajo(analog_handler* this) {
    puts("Bajo limite inferior");
}

static void alto(analog_handler* this) {
    puts("Sobre limite superior");
}

int main(int argc, char* argv[])
{
    wiringPiSetupGpio();
    reactor* r = reactor_new();
    analog_handler* in = analog_handler_new(25, 100, 200,
                                            bajo, alto);
    reactor_add(r, (event_handler*)in);
    reactor_run(r);
    reactor_destroy(r);
    return 0;
}

Construimos un analog_handler con funciones que simplemente imprimen un mensaje cada vez que detecta el paso de umbral. Solo queda compilarlo con un makefile. Como verás en la carpeta src/c/ejemplos/analog ya tienes uno:

CFLAGS=-pthread -std=c11 -Wall -D_POSIX_C_SOURCE=200809L -I../../reactor -ggdb \
    -fasynchronous-unwind-tables 
LDFLAGS=-L../../reactor/reactor -pthread -rdynamic
LDLIBS=-lreactor -lwiringPi -lpthread
CC=gcc

all: test_analog_handler

test_analog_handler: test_analog_handler.o analog_handler.o

clean:
    $(RM) *~ *.o test_analog_handler

Esqueleto del manejador

Es ahora y no antes cuando tenemos que plantearnos escribir los archivos analog_handler.h y analog_handler.c. Se parte de cualquiera de los que ya tienes en Reactor y se modifican de acuerdo a las necesidades. Lo primero es decidir qué tipo de event_handler es y para ello hay que fijarse en cómo va a notificar los eventos al programa. Como ya sabemos Reactor utiliza descriptores de archivo para recibir eventos, por lo que las notificaciones tienen que llegar por este medio. En analog_handler no hay descriptores de archivo, solo hay el pin MISO y una salida GPIO. Hay que realizar periódicamente un proceso ciertamente tedioso, en el que hay que contar bits. Todo esto nos da pistas.

Por un lado tenemos que sintetizar eventos en un descriptor de archivo, porque el problema como tal no tiene descriptores. Eso implica usar un pipe_handler. Por otro lado hay que realizar un proceso periódico al margen de la tarea principal. Eso nos lleva a un thread_handler o un process_handler. Dado que no hay que ejecutar nada externo nos decantaremos por un thread_handler, que es más eficiente. En Reactor tenemos un ejemplo de manejador similar, el de las entradas digitales, solo hay que copiarlo, sustituir input_handler por analog_handler y quitar todo el código específico.

Empezamos por la cabecera que quedaría así:

#ifndef ANALOG_HANDLER_H
#define ANALOG_HANDLER_H
#include <reactor/thread_handler.h>

typedef void (*analog_handler_function)(analog_handler* this);

typedef struct analog_handler_ analog_handler;
struct analog_handler_ {
    thread_handler parent;
    event_handler_function destroy_parent_members;

    // FIXME: Añade todos los atributos que necesites
};

analog_handler* analog_handler_new (int pin, int low, int high,
                                    analog_handler_function low_handler,
                                    analog_handler_function high_handler);
void analog_handler_init (analog_handler* this,
                          int pin, int low, int high,
                          analog_handler_function low_handler,
                          analog_handler_function high_handler);
void analog_handler_destroy (analog_handler* ev);

#endif

Esto es lo mínimo: el constructor con y sin reserva de memoria dinámica y el destructor. El nuevo tipo analog_handler declara que es una especialización de thread_handler porque su primer atributo (parent) es un thread_handler. Hemos dejado el atributo destroy_parent_members porque asumimos que alguna operación de destrucción puede ser necesaria. Ya veremos.

A continuación hacemos lo mismo con analog_handler.c:

#include "analog_handler.h"

// FIXME: declaraciones de funciones estáticas específicas

analog_handler* analog_handler_new (int pin, int low, int high,
                                    analog_handler_function low_handler,
                                    analog_handler_function high_handler)
{
    analog_handler* h = malloc(sizeof(analog_handler));
    analog_handler_init_members(h, pin, low, high, low_handler, high_handler);
    event_handler* ev = (event_handler*) h;
    ev->destroy_self = (event_handler_function) free;
    thread_handler_start (&h->parent, analog_handler_thread);
    return h;
}


void analog_handler_init (analog_handler* this,
                          int pin, int low, int high,
                          analog_handler_function low_handler,
                          analog_handler_function high_handler)
{
    analog_handler_init_members(this, pin, low, high, low_handler, high_handler);
    thread_handler_start (&this->parent, analog_handler_thread);
}


void analog_handler_destroy (analog_handler* this)
{
    event_handler_destroy((event_handler*)this);
}


// FIXME: Definiciones de funciones estáticas específicas

Un thread_handler es algo relativamente complejo, porque el orden en que se realizan las operaciones puede afectar al correcto funcionamiento en determinados casos. Procura respetar esta estructura tanto como sea posible para evitarte disgustos.

Los dos constructores (con y sin reserva de memoria) tienen que retrasar la ejecución del hilo hasta el último momento. Por eso extraemos la inicialización de los atributos a otra función diferente (analog_handler_init_members).

Hasta este punto lo tienes hecho en la carpeta src/c/ejemplos/analog. Sin embargo a partir de aquí te toca trabajar. ¡Manos a la obra!

Funciones específicas

Ya solo queda declarar e implementar las funciones específicas de este manejador. Primero declara las funciones que hemos utilizado en el esqueleto: analog_handler_thread y analog_handler_init_members. Si tienes dudas consulta input_handler.c en la biblioteca Reactor. Dado que son funciones que no se exportan al usuario decláralas como static.

A continuacion haz un esqueleto de ambas funciones justo debajo. Déjalas vacías de momento, solo queremos compilar para detectar problemas lo antes posible. Una vez definidas ejecuta make. Verás errores, seguro. Corrige los errores tipográficos, añade las directivas include necesarias y consigue que compile sin errores ni advertencias de ningún tipo. Usa man si no sabes en qué cabecera está declarada una función del sistema.

Una vez que el programa compila rellena las funciones vacías. ¿Que debe hacer analog_handler_init_members? Lo dice la propia función, inicializar los miembros. Añade a la declaración de la estructura en analog_handler.h los miembros que consideres necesarios e inicializa sus valores con los argumentos en analog_handler_init_members. No te olvides de configurar adecuadamente el pin MISO y el pin GPIO.

A continuación solo queda el hilo de exploración. El código ya lo tienes en el capítulo de SPI pero tienes que asegurarte de repetir periódicamente la medida. Usa tantas funciones como sea necesario, no hagas funciones muy largas. Si ves que tienes que usar un for y dentro un if y dentro otro if eso es ya señal de que tienes que dividir en funciones más pequeñas.

Mi sugerencia es copiar de input_handler.c tanto como sea posible. Por ejemplo, el hilo puede ser casi igual pero quitando todo lo que tiene que ver con el periodo de muestreo. No hace falta esperar entre muestra y muestra porque el método que vimos en el capítulo de SPI ya necesita esperar a la descarga completa del condensador.

static void* analog_handler_thread(thread_handler* h)
{
    analog_handler* in = (analog_handler*) h;
    while(!h->cancel)
        analog_handler_poll(in);
    return NULL;
}

Esto es evidentemente incompleto. Hay que definir analog_handler_poll. Pero su contenido es directamente lo que vimos en el capítulo de SPI.

Generalizando

Una vez que el programa de prueba esté funcionando llega el momento de hacer algo de autocrítica. ¿Es posible mejorar la interfaz de programación? ¿Es posible con poco trabajo hacer que sea más útil? Por ejemplo:

  • Los constructores tienen demasiados argumentos. Es engorroso y propenso a error. Una posible alternativa es pasar una estructura con los datos de configuración.

  • El tiempo necesario para estar seguros de que el condensador se ha descargado depende del valor del condensador, habría que ponerlo en un parámetro.

  • El reloj SPI a utilizar depende de la magnitud a medir, del tamaño máximo de los buffers pasados a wiringPiSPIDataRW y de la precisión necesaria. Habría que dejar también como parámetro de configuración tanto la frecuencia como el tamaño de los buffers.

  • Si el condensador no ha tenido tiempo de descargarse es posible que no haya ningún cero en el buffer. Esa condición debe notificarse como un error (elevar una excepción). Lo mismo ocurre si el condensador no ha tenido tiempo de cargarse hasta leer algún uno. Si el último byte leido no es distinto de cero debe notificarse como error (elevar excepción).

Valora tú cuáles de estas críticas deberían arreglarse y propón una solución.

Recapitulando

¿Lo conseguiste? No desistas al primer intento. Solo hay una forma de convertir esto en una rutina, repitiéndolo muchas veces.

De todas formas si te cansas tenemos el ejemplo resuelto. No es buena política leer directamente la solución pero si es tu deseo aquí tienes cómo hacerlo. La solución está en una rama del repositorio que se llama analog. Podemos sacarla directamente así:

pi@raspberrypi:~/src/c/ejemplos/analog $ git checkout analog -- analog_handler.c
pi@raspberrypi:~/src/c/ejemplos/analog $ git checkout analog -- analog_handler.h
pi@raspberrypi:~/src/c/ejemplos/analog $ ▂

Una vez generalizado el manejador es posible que resulte útil para un uso general. Es el momento de plantearse añadirlo a la propia biblioteca Reactor.

results matching ""

    No results matching ""