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:
Come detto, è una libreria che ho creato per me, ma se può essere utile anche ad altri, ne sarò contento.
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
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:
- Codice dello stato corrente
- Condizione necessaria per cambiare stato
- Azione richiesta se la condizione è soddisfatta
- Parametro intero opzionale (se non utilizzato, inserisci semplicemente uno zero)
- 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.