Libreria Macchina a Stati Finiti (FiniteState 2.0)

Poiché abbastanza spesso capita di dover implementare su Arduino una macchina a stati finiti, e non trovandomi bene con quel paio di librerie che mi è capitato di trovare in giro, tempo fa mi feci una mia libreria, per farne qualcosa di flessibile e relativamente semplice da usare.
Dato che ne ho realizzato una nuova versione, ho da poco uploadato su GitHub la libreria "FiniteState" che trovate su GitHub:

FiniteState 2.0

Come detto, è una libreria che ho creato per me, ma se può essere utile anche ad altri, ne sarò contento. :wink:

Riporto qui una versione in italiano della descrizione generale e qualche esempio di codice presenti nel "readme". Ovviamente sono ben accetti consigli per bug e possibili migliorie, o se doveste riscontrare qualche problema :wink:

Per prima cosa un consiglio "operativo": disegnate sempre un diagramma di stato usando dei cerchi per gli stati e degli archi che rappresentano le "transizioni". Ogni transizione, oltre ai due stati iniziale e finale, è una combinazione di tre elementi: una "condizione" (qualsiasi evento o risultato di un test) una "azione" (cosa fare quando si verifica l'evento) e lo "stato successivo" (il nuovo stato al quale passare dopo che l'azione è stata eseguita). Uno degli stati sarà lo stato "iniziale". Non iniziare a lavorare senza un diagramma di stato completo e logicamente testato, poiché un buon grafico rende l'implementazione piuttosto semplice invece del metodo per "tentativi ed errori".

Detto questo, dopo aver installato la libreria hai bisogno solo di scrivere un poco di codice.

Prima di tutto, devi definire come "#define" tutti i simboli per Stati, Condizioni e Azioni. Ti suggerisco di iniziare i nomi con "S_" per gli Stati, le condizioni con "C_" e le azioni con "A_" e assegnare quindi a questi alcuni valori interi (una sequenza che inizia da 1).

Ad esempio, supponiamo che tu voglia creare una semplice macchina con solo due stati per far lampeggiare un LED (come nel progetto di esempio di FSMBlink che troverete insieme alla libreria).

Lo stato iniziale si chiama START, quindi la condizione sarà un timer attivato (ad es. 1 secondo) e le azioni sono o accendere o spegnere il LED:

// State
#define S_START 1
#define S_LED 2
// Conditions
#define C_TIMER 0
// Actions
#define A_LED_OFF 0
#define A_LED_ON 1

Dopo aver creato una variabile globale per la macchina a stati finiti, "FiniteState fsm;", definiamo la nostra macchina nella funzione setup() in questo modo, ed impostiamo lo "stato iniziale":

// State definitions
  fsm.Write(S_START, C_TIMER, A_LED_ON, 1000, S_LED);
  fsm.Write(S_LED, ELSE, A_LED_OFF, 1000, S_START);
  // Set the callback functions
  fsm.SetFunctions(&TestCondition, &DoAction);
  // Set the machine to first state
  fsm.Set(S_START);

Con la prima istruzione definiamo una transizione dallo stato START allo stato LED, basato su un TIMER di 1000 ms. Ovviamente, la seconda rappresenta la transizione opposta, dallo stato LED a START dopo un altro secondo. Il terzo è il riferimento alle funzioni locali che vedremo dopo.
L'ultima istruzione imposta lo stato iniziale su START.

Come si può vedere, Write() ha 5 parametri:

  1. Codice dello stato corrente
  2. Condizione necessaria per cambiare stato
  3. Azione richiesta se la condizione è soddisfatta
  4. Parametro intero opzionale (se non utilizzato, inserisci semplicemente uno zero)
  5. Stato successivo alla fine dell'azione

Dopo questa inizializzazione è quindi necessario creare due funzioni: una per verificare le condizioni ("TestCondition"), e una per eseguire le azioni richieste ("DoAction"). Un riferimento a tali funzioni viene passato all'oggetto FiniteState chiamando il metodo "SetFunctions()" che abbiamo visto prima.

La funzione "TestConditions" viene richiamata dalla libreria passandole il codice della condizione (uno dei simboli C_ *) e deve semplicemente restituire un valore booleano che rappresenta il risultato del test: true se la condizione è soddisfatta (quindi verrà eseguita l'azione corrispondente ) o false in caso contrario. All'interno bisogna quindi solo implementare una "switch..case" con tutti i simboli delle condizioni che hai definito in precedenza.

boolean TestCondition(int condition) {
  switch ( condition ) {
  case C_TIMER: // Check if timer ends
    if ( millis() - millis0 > fsm.Param() ) {
      // End of time, reset start time
      millis0 = millis();
      return true;
    }
    return false;
  case C_ELSE:
    return true;
  }
  return false;
}

Nella "DoAction" bisogna solamente controllare l'azione richiesta, passata dalla libreria come parametro, e poi usarla in un altro blocco "switch..case" con tutti i simboli di azione definiti, ed al suo interno ciò che ogni azione dovrebbe eseguire.

Nel nostro esempio, il codice sarà il seguente:

void DoAction(int action) {
  switch ( action ) {
  case A_LED_OFF:
    digitalWrite(LED, LOW);
    break;
  case A_LED_ON:
    digitalWrite(LED, HIGH);
    break;
  }
  return;
}

Infine il nostro ciclo loop() sarà molto semplice, e praticamente identico per quasi qualsiasi progetto che utilizza FSM:

void loop() {
  // State execution
  fsm.Execute();
}

Il metodo "Execute" automaticamente effettua un ciclo su tutte le condizioni impostate per lo stato corrente, e se una di queste è verificata chiamerà automaticamente la funzione DoAction per eseguire le azioni previste.

Per accedere al reference delle funzioni vedere il Library reference Wiki.

Faccio una riflessione, a volte è meglio avere soluzioni diverse per problemi simili. Facciamo l'esempio della codifica caratteri, esistono tante codifiche, ascii, unicode ecc.., possiamo avere un unica codifica che si adatta a tutte le situazioni esempio l'unicode, ma risulta la soluzione migliore avere codifiche diverse in base alla situazione.
Mi sembra qualcosa di simile con le macchine a stati finiti, io non credo che creare una macchina a stati finiti universale, che possa essere usata in tutte le situazioni, sia la soluzione più efficiente. Credo invece a implementazioni specifiche per specifici problemi. :slight_smile:

torn24:
Faccio una riflessione, a volte è meglio avere soluzioni diverse per problemi simili.

Sicuramente :), infatti l'ho scritto che quella libreria l'avevo scritta per me per un progetto (un robottino) che realizzai tempo fa, quindi per un problema specifico.

Trovandomi però tra le mani anche altre librerie per implementare macchine a stati finiti e non trovandole adatte a ciò che immaginavo come utilizzo, ho deciso di creare qualcosa che, appunto, non avevo trovato in giro. :wink: In particolare volevo codificare l'intera rappresentazione di un grafo di una macchina a stati finiti, quindi non solo le transizioni ma anche eventi ed azioni.

Quindi, si, concordo, non è sicuramente la prima né l'ultima libreria del genere, e non c'è la pretesa che sia "la libreria definitiva" ossia universale. Se ad uno piace, la usa, se non gli piace, non la usa. Se gli piace ma trova che si possa migliorare, può scrivermi e farlo insieme, o scaricare i sorgenti e modificarla. E' lo spirito della comunità di sviluppatori e di questa in particolare, no? :slight_smile:

Grazie per la condivisione @docdoc.
Ho visto che tra gli examples il Test è con pulsanti.
Riesce anche a fare qualcosa a "tempo" ? supponiamo il classico semaforo, magari con tasto a chiamata passaggio pedonale. (esempio poco utile su un arduino ma mi pare abbastanza completo)

Piccole idee:
1.alcuni eventi sono a tempo, non si potrebbe in quei casi "nascondere" nella macchina stessa il contatore millis ?
2. il max_cond invece di una costante, non sarebbe meglio una funzione GetMaxConditions() della classe ?

nid69ita:
Riesce anche a fare qualcosa a "tempo" ? supponiamo il classico semaforo, magari con tasto a chiamata passaggio pedonale. (esempio poco utile su un arduino ma mi pare abbastanza completo)

Certo! Guarda l'esempio "FSMSingleBlink", quando si preme un pulsante si accende il LED che si spegne a tempo e torna allo stato iniziale. :wink:

1.alcuni eventi sono a tempo, non si potrebbe in quei casi "nascondere" nella macchina stessa il contatore millis ?

Hm, devo pensarci, ma non vorrei "appesantire" troppo la libreria con funzioni che servono magari in pochi casi. Vedi comunque anche l'esempio "FSMTest", quello più "complesso", nel quale all'ultimo stato se non si preme nulla entro 5 secondi torna all'inizio, se invece premi un tasto in quei 5 secondi accende i led e si ferma. In quel caso ho usato la SimpleTimer, che ho trovato comoda.

  1. il max_cond invece di una costante, non sarebbe meglio una funzione GetMaxConditions() della classe ?

Si, ci avevo pensato, ma il fatto è che quella determina la dimensione della sua matrice degli stati (il "database" interno) ed avevo anche pensato di renderla modificabile con una setMaxConditions() per risparmiare spazio quando si sa già che non serviranno mai 32 stati, ma dovrei fare dei malloc() che per ora ho voluto evitare. Ma non è detto che in una versione 2.1 non lo faccia :wink:

docdoc:
Si, ci avevo pensato, ma il fatto è che quella determina la dimensione della sua matrice degli stati (il "database" interno) ed avevo anche pensato di renderla modificabile con una setMaxConditions() per risparmiare spazio quando si sa già che non serviranno mai 32 stati, ma dovrei fare dei malloc() che per ora ho voluto evitare. Ma non è detto che in una versione 2.1 non lo faccia :wink:

Beh, internamente usi la costante ma per far vedere all'esterno questo numero usare un funzione membro

Si, ma attualmente è una costante ed un limite "cablato" quindi bisogna conoscerlo. :slight_smile: Poi come detto, magari nella prossima versione lo posso rendere public, e/o implementare il malloc() anche se è una cosa che tendo ad evitare in genere.