Máquina de estados finitos en Arduino

Vamos a ver cómo crear una máquina de estados finitos muy sencilla para Arduino o Node MCU. Debido a las limitaciones de ambas plataformas vamos a ver una idea muy sencilla.

Empezaremos por aclarar los conceptos básicos:

  • Estados: son los distintos valores que puede tomar la máquina de estados finitos.
  • Eventos: producen el cambio de un estado a otro.
  • Transiciones: definen como los eventos cambian los estados. Cada transición constan de tres elementos, un evento, un estado origen y un estado destino. Indica que cuando se produzca el evento si el estado actual es el estado origen este cambiará por el estado destino.

Una mef (máquina de estados finitos) está siempre en un único estado y hay eventos que producen el cambio de estado siguiendo unas reglas (las transiciones). Aunque con esto ya tenemos una mef, para que sea realmente útil tiene que facilitar la ejecución de funciones asociada a los distintos estados y eventos.

Para facilitar todo esto podemos usar la librería easy finite state machine que simplifica la programación usando macros del preprocesador.

Estados y eventos

Vamos a empezar por los eventos y estados, para ello podemos usar enum de C que nos permite escribir código de forma muy intuitiva.

enum efsmStates {start, step1, step2, finish};
enum efsmEvents {next, back};

O usando las macros de la libreria:

STATES {start, step1, step2, finish};
EVENTS {next, back};

También habrá que definir una variable que guarde el estado actual e inicializarla con el estado inicial.

enum States efsmState = start

Con la librería:

INIT(start)

Cambiar el valor del estado es muy sencillo, basta con hacer:

efsmState = start

O con las macros:

changeState(start)

Así como comparar el valor del estado:

isState(start)

Transiciones:

De todas formas lo habitual no es realizar los cambios de estados directamente sino a través de eventos. Los eventos vamos a gestionarlos con la función efsmEvent(event) que los aglutina todos. Esta función recibe el evento como parámetro y contiene las transiciones. Básicamente son un montón de “if”. Usando la librería se usa la macro TRANSITION

TRANSITION(evento, estadoOrigen, estadoDestino, funcion)

Su significado es: Si se lanza el evento y el estado es el estadoOrigen se pasa al estadoDestino y se llama a función. No es necesario poner una función asociada, se puede dejar en blanco.

Las transiciones se definen entre las macros:

START_TRANSITIONS y END_TRANSITIONS

Las transicione son una gran ayuda, aunque su funcionamiento se puede sustituir con isState(state) y changeState(state).

if(isState(estadoOrigen) ){
  changeState(estadoDestion);
  funcion();
}

Ejecuciones:

Cuando se ejecuta efsmExecute() verifica y llama a la función asociada a cada estados. Es decir, cuando se llama a esta función se verifica en qué estado está la máquina y llama a la función asociada a ese estado. Con la forma que tiene Arduino de funcionar usando una función loop que está en bucle constante la idea es colocar efsmExecute() en la función loop() para que se llame en cada iteración.

EXECUTION(executionState,function())

Si no se asocia ninguna función a un estado no se ejecutará nada.

Se definen entre las macros:

START_EXECUTIONS y END_EXECUTIONS

No es obligatorio definirlas. Por ejemlo cuando la mef solo tenga que lanzar funciones en los cambios de estado (transiciones).

Disparadores:

Los disparadores generan eventos asociados a distintos sucesos. Su idea es facilitar la programación de acciones habituales que lanzan eventos. Se ejecutan cuando se llama a la función efsmTriggers().

Para que un disparador se lance, el estado de la mef ha de ser el mismo que el que se le pasa como primer parámetro. Hay de tres tipos de disparadores:

Un condicional lanza un evento si el condicional especificado se cumple:

CONDITIONAL(state, condition, event)

Un contador lanza un evento después de haber llamado un número determinado de veces a la función efsmTriggers() tras al último evento válido.

COUNTER(state, number, event)

Un temporizador lanza un evento cuando ha pasado un determinado tiempo (en milisegundos) desde que se lanzó el último evento válido.

TIMER(state, number, event)

“Tras el último evento válido’ significa que cada vez que se lanza un evento y este produce una transición los contadores se reinician. Si se quisieran reiniciar a mano se pueden usar: resetTimer() y resetCounter()

Los disparadores se definen entre las macros:

START_TRIGGERS y END_TRIGGERS

No es necesario definir disparadores , si se quiere lanzar algún evento manualmente se puede hacer llamando directamente a efsmEvent(event).

En resumen:

  1. Se definen estados y eventos.
  2. Se establece el estado inicial.
  3. Se crean transiciones que indican que cambios entre estados se producen con cada evento asi como la función que se lanzara cuando se produzca esa transición.
  4. Se escriben la ejecuciones indicando que función se lanzará cuando la máquina este en cada estado.
  5. Se declaran los disparadores que lanzaran eventos (o se programa el lanzamiento a mano).
  6. Se añade a la función loop efsmExecute() y efsmTriggers()

Un ejemplo:

Para ver todo un poco más claro vamos a ver un ejemplo basado en el que incluye la librería. En el se modela la máquina de estados finitos representada en el siguiente diagrama:

Ejemplo de maquina de estados finitos
Máquina de estados finitos del ejemplo

El código es el siguiente:

#include <efsm.h>

STATES {start, step1, step2, finish};
EVENTS {next, back};
INIT(start)

START_TRANSITIONS
TRANSITION(next,start,step1,Serial.println("next")) //next start -> step1
TRANSITION(next,step1,step2,Serial.println("next")) //next step1 -> step2
TRANSITION(next,step2,finish,Serial.println("next")) //next step2 -> finish
TRANSITION(back,step2,step1,Serial.println("back")) //back step2 -> step1
TRANSITION(ANY_EVENT,ANY_STATE,ANY_STATE,Serial.println("FAIL!!!")) //in any other case
END_TRANSITIONS

START_EXECUTIONS
EXECUTION(start,Serial.println("start")) //state is start
EXECUTION(step1,Serial.println("step1")) //state is step1
EXECUTION(step2,Serial.println("step2")) //state is step2
EXECUTION(finish,Serial.println("finish")) //state is finish
END_EXECUTIONS

START_TRIGGERS
COUNTER(start,5,next); //State start wait 5 iterations and launch event next
TIMER(step1,2000,next); //State step1 wait 2 sg and launch event next
END_TRIGGERS

void setup() {
  Serial.begin(9600);
  resetTimer();
  resetCounter();  
}

void loop() {  
  efsmExecute();
    
  if(isState(step2)){
    efsmEvent(next);
  }
  efsmTriggers();
  
  delay(1000);
}