Macchina a stati finiti (FSM) basata su switch case

Voglio usare questo topic per descrivere una possibile implementazione della macchina a stati finiti impiegando uno più switch case. L'aromento è stato già trattato su arduino forum, alcuni post interessanti li elenco qui di seguito:

Sarebbe meglio leggere tutto il topic o se preferite partite da pagina 8.
irrorino

https://forum.arduino.cc/index.php?topic=641557.0

Il primo codice che posto è simile all'implementazione mostrata nei link precedenti, l'obbiettivo è introdurre l'implementazione passo per passo al fine di potere documentare i dettagli poco comprensibili ai principianti.

Prima di passare a leggere lo sketch N°2 è bene capire come funziona il N°1.

boolean oneShotSwitch;
uint32_t elapsedTime;
uint32_t mainTimer;
byte currentState = 0;
byte oldState = ~currentState;


void setup() {
  // put your setup code here, to run once:
  Serial.begin(9600);
}

void loop() {
  // put your main code here, to run repeatedly:
  oneShotSwitch = false;
  if (currentState != oldState) {
    oneShotSwitch = true;
    mainTimer = millis();
  }
  oldState = currentState;
  elapsedTime = millis() - mainTimer;

  switch (currentState) {
    case 0:
      if (oneShotSwitch) {
        // se oneShotSwitch == true stampa "case 0"
        Serial.println("case 0");
      }
      // se passano 10 secondi da quando si è entrati nel case 0
      // verrà selezionato come stato successivo il case 1
      if (elapsedTime >= 10000)
        currentState = 1;
      break;
    case 1:
      if (oneShotSwitch) {
        // se oneShotSwitch == true stampa "case 1"
        Serial.println("case 1");
      }
      // se passano 10 secondi da quando si è entrati nel case 1
      // verrà selezionato come stato successivo il case 2
      if (elapsedTime >= 10000)
        currentState = 2;
      break;
    case 2:
      if (oneShotSwitch) {
        // se oneShotSwitch == true stampa "case 2"
        Serial.println("case 2");
      }
      // se passano 10 secondi da quando si è entrati nel case 2
      // verrà selezionato come stato successivo il case 0
      if (elapsedTime >= 10000)
        currentState = 0;
      break;


  }
}

Il codice non fa altro che stampare nel monitor seriale la scritta "case n" dove n corrisponde al valore di currentState
la quale può assumere i seguenti valori: 0, 1, 2. Tuttavia qualunque valore nel range 0÷254 può essere assegnato.
La stampa avverrà solo al primo ingresso nel case n. Per 10 secondi il ciclo di loop eseguirà lo stesso case, trascorsi 10 secondi il case eseguito al prossimo ciclo di loop è quello specificato nell'assegnazione a currentState.

Quando si assegna un nuovo valore a currentState possiamo dire che prenotiamo quale sarà il case da eseguire al prossimo ciclo del loop o se preferite stabiliamo cosa deve accadere nel futuro. Il presente è il momento in cui il codice viene eseguito. Il passato viene valuto nel presente e ci dice come siamo arrivati qui, il passato viene salvato nella variabile oldState, il suo valore alle volte è utile valutarlo nel presente altre volte non torna utile.
Nello sketch mostrato sopra non riteniamo utile valutare il passato nel presente, ma oldState è comunque usato all'interno del loop prima della istruzione switch (currentState) {. All'interno del case oldState e currentState hanno lo stesso valore, non sempre ciò è desiderabile ed infatti ho scritto che questo sketch è una possibile implementazione, ciò vuol dire che non è l'unica e aggiungo che non c'è ne una giusta ma al massimo che ne esiste una adatta all'applicazione che si ha in mente di sviluppare.

Provate nel case 2 ad assegnare 254 a currentState anziché 0 (zero).
Il risultato è che nessuno dei case verrà eseguito e nulla verrà stampato nel serial monitor.

Nota: I nomi delle variabili sono stati scelti in modo arbitrario e pertanto lecito dargli un altro nome, purché sia
quanto più possibile autodocumentante.

Ho pronte altre varianti, ma se vole postate la vostra variante, possibilmente che non si discosti troppo da questa o che non sia troppo complessa in modo che si possa descrivere passo passo e via via introdurre la complessità.

Il prossimo sketch stampa quanto segue nel monitor seriale:

case 0-millis():290521000..........290531000
case 1-millis():290531000..........290541000
case 2-millis():290541000..........290551000
case 0-millis():290551000..........290561000
case 1-millis():290561000..........290571000
case 2-millis():290571000..........290581000

Un puntino viene stampato ogni secondo

Ciao.

perché?

viene meglio (in my Umile Opinione) usando un array di tempi da rispettare

a parte che così tu hai una macchina stati finiti a 6 stati 3 con il flag basso e tre co il flag alto, ma è ovvio che deve essere così

così mi piace di più,
quando si puo' facilmente espandere si dice scalabile, giusto?

#define STATI 10
unsigned long int tempi[STATI]={10000, 9000, 8000, 7000, 6000, 5000, 4000, 3000, 2000, 1000};
byte stato;
unsigned long int timestart;
void setup()
{
Serial.begin(966);
}

void loop()
{
   if (millis()-timestart<tempi[stato])
   {
      stampa(stato);
   }
   else
   {
      stato=++stato%STATI;
      timestart=millis();
   }
}

void stampa(int stato)
{
static byte vecchio=255;
if (stato!=vecchio)
   {
   vecchio=stato;
   Serial.print("inizio stato: ");
   Serial.println(stato);
   }
}

AmericanDreamer:
viene meglio (in my Umile Opinione) usando un array di tempi da rispettare

In generale non è detto che gli stati debbano avere valori in sequenza o in un qualsiasi ordine, e da qualsiasi stato si deve poter passare a qualsiasi altro stato. Cioè il fatto che lo stato sia una variabile numerica è solo una pura questione di implementazione e comodità. Per rendere gli stati e le transizioni più "parlanti" si possono usare delle costanti/define.

#define   INIZIO  100
#define   AVVIO   80
#define   PAUSA   200
#define   RIAVVIO 85
#define   ARRESTO 215
#define   ERRORE  220
#define   FINITO  33

guarda concordo in pieno con te

però non è il caso in esame

dato che ha creato una struttura hard-coded,
fa esattamente quello che fa,
ovvero passare da uno stato al successivo in circolo dopo aver atteso un tempo

e secondo me la mia implementazione è più flessibile, meglio espandibile e più breve

a questo proposito ho appena sviluppato la macchina a stati finiti universale

è parametrica e può "simulare" qualunque macchina a stati finiti, qualunque sia il numero di stati e la loro sequenza (beh, un massimo di c'è, lo stato deve essere int)

si basa sulla scansione di una serie di regole

le regole sono una struttura, che contiene:

lo stato al quale è applicabile

il tempo di attesa (se non c'è attesa, si usa 0

la condizione di uscita, sotto forma del nome della funzione che ritorna true quando la condizione è verificata

lo stato nel quale evolve, dopo l'attesa ed al verificarsi della condizione

va da se che se si mettono due o più regole che riferiscono allo stesso stato, la prima che viene soddisfatta "vale" e fa passare allo stato successivo da lei indicato

per stati che devono solo attendere basta usare come funzione che ritorna true una funzione che ritorna sempre 1

typedef bool (* verifica)(void);

struct Regola
{
    int stato;
    unsigned long int tempo;
    verifica condizione;
    int prossimo; 
} ;

byte stato = 0;
unsigned long int timestart;
bool vero()
{
    return 1;
}
bool pin4()
{
    pinMode(4, INPUT);
    return digitalRead(4);
}

bool pin8altopin9basso()
{
    pinMode(8, INPUT);
    pinMode(9, INPUT);
    return (digitalRead(8) && !digitalRead(9));
}

Regola regole[] = {{0, 10000, vero, 1}, {0, 0, pin4, 2}, {1, 2000, vero, 3}, {2, 1500, vero, 3}, {3, 1000, pin8altopin9basso, 0}};
#define REGOLE sizeof regole/ sizeof regole[0]


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

void loop()
{
    for (byte i = 0; i < REGOLE; i++)
    {
        if (regole[i].stato == stato)
        {
            if (millis() - timestart >= regole[i].tempo)
            {
                if (regole[i].condizione())
                {
                    stato = regole[i].prossimo;
                    timestart = millis();
                }
            }
        }
    }

    stampa(stato);
}




void stampa(int stato)
{
    static byte vecchio = 255;

    if (stato != vecchio)
    {
        vecchio = stato;
        Serial.print("inizio stato: ");
        Serial.println(stato);
    }
}

AmericanDreamer:
si basa sulla scansione di una serie di regole

Si dovrebbero anche specificare le azioni (eventualmente) da compiere al primo giro ogni volta che uno stato diventa attivo, quelle da (eventualmente) eseguire ad ogni giro, e (forse) quelle da eseguire al verificarsi di una condizione (che potrebbero anche essere le prime del giro successivo). Ma i modi di scrivere una FSM sono tantissimi, di quelli altamente parametrizzati forse nessuno è in grado di coprire tutte le casistiche necessarie per specifici casi. Ad esempio con il tuo sistema mi verrebbe difficile scrivere la funzione di lettura encoder seguente che contiene due macchine:

void readEncoder(void)
{
    static uint8_t  s = 0;               // stato processo lettura switch
    static uint32_t t;
    static uint8_t  f = 0;               // stato processo lettura fasi
    static uint8_t  precEnc = 3;         // lettura fasi precedente

    onClic   = false;                    // variabili globali
    onLpress = false;
    byte in  = digitalRead(ENCODER_SW);  // con debounce hardware
    if      (0 == s  &&  LOW == in)          { t = millis();     s = 1; }
    else if (1 == s  &&  HIGH == in)         { onClic = true;    s = 0; }
    else if (1 == s  &&  (millis()-t > 500)) { onLpress = true;  s = 2; }
    else if (2 == s  &&  HIGH == in)         {                   s = 0; }

    onUp = false;
    onDn = false;
    byte enc = (digitalRead(ENCODER_A) << 1) | digitalRead(ENCODER_B);
    if (enc != precEnc)
    {
            precEnc = enc;
            if      (0 == f  &&  2 == enc) {               f = 1; }
            else if (0 == f  &&  1 == enc) {               f = 4; }
            else if (1 == f  &&  3 == enc) {               f = 0; }
            else if (1 == f  &&  0 == enc) {               f = 2; }
            else if (2 == f  &&  2 == enc) {               f = 1; }
            else if (2 == f  &&  1 == enc) {               f = 3; }
            else if (3 == f  &&  0 == enc) {               f = 2; }
            else if (3 == f  &&  3 == enc) { onUp = true;  f = 0; }
            else if (4 == f  &&  3 == enc) {               f = 0; }
            else if (4 == f  &&  0 == enc) {               f = 5; }
            else if (5 == f  &&  2 == enc) {               f = 6; }
            else if (5 == f  &&  1 == enc) {               f = 4; }
            else if (6 == f  &&  0 == enc) {               f = 5; }
            else if (6 == f  &&  3 == enc) { onDn = true;  f = 0; }
    }
}

Il secondo sketch è simile al precedente.

/* Tre funzioni di supporto sono state create per snellire il contenuto di  ogni case: 
void printDot() 
boolean check10s()
void printOneShot();
 
Lo sketch è stato creato per mostrare cosa sia possibile fare con 
questa implementazione. Il destino di questo sketch e di essere modificato
per adattarlo alla proprie esigenze.
*/
boolean oneShotSwitch;      
uint32_t elapsedTime;      
uint32_t mainTimer;         

byte currentState = 0;      
byte oldState = ~currentState; 
byte timerCounter = 0;         

void setup() {
  // put your setup code here, to run once:
  Serial.begin(9600);
  delay(1000);

}

void printDot() {
  if (elapsedTime >= 1000) {
    mainTimer = millis();
    timerCounter++;
    Serial.print(".");
  }
}

boolean check10s() {
  if (timerCounter == 10) {
    Serial.print(millis());
    Serial.println("");
    return true;
  }
  return false;
}

void printOneShot() {
  Serial.print("case ");
  Serial.print(currentState);
  Serial.print("-millis():");
  Serial.print(millis());
}

void loop() {
  // put your main code here, to run repeatedly:
  oneShotSwitch = false;
  
  if (currentState != oldState) {
    oneShotSwitch = true;
    mainTimer = millis();
    timerCounter = 0;
  }
  
  oldState = currentState;
  elapsedTime = millis() - mainTimer;
  
  // main state machine switch case based
  switch (currentState) {
    case 0:
      if (oneShotSwitch) {
        // se oneShotSwitch == true stampa "case 1"
        printOneShot();
      }
      
      printDot();
      if (check10s()) {
        currentState = 1;
      }

      break;
    case 1:
      if (oneShotSwitch) {
        // se oneShotSwitch == true stampa "case 1"
        
        printOneShot();
      }
      
      printDot();

      if (check10s()) {
        currentState = 2;
      }

      break;
    case 2:
      if (oneShotSwitch) {
        // se oneShotSwitch == true stampa "case 2"
        
        printOneShot();
      }
      
      printDot();

      if (check10s()) {
        currentState = 0;
      }

      break;

  }
}

Come potete osservare grazie all'intervento di AmericanDreamer un macchina a stati si può implementare in diversi modi. Provate il primo sketch di AmericanDreamer che fa più o meno la stessa cosa del mio primo sketch. Per farlo funzionare nella versione 1.8.10 ho dovuto modificare la seguente riga:

else
   {
      stato = (stato+1) % STATI;   // prima era stato = ++stato % STATI;
      timestart=millis();

Diversamente non funziona. Il compilatore mi avverte che il valore di stato potrebbe essere indefinito, il motivo al momento non mi è chiaro del perché.

La seconda implementazione di AD (AmericanDreamer) è molto più flessibile della prima, c'è addirittura un puntatore a funzione per ogni stato e in ognuno di queste è possibile restituire true quando si vuole abbandonare lo stato corrente per quello successivo. Questa implementazione una volta estesa porta ad una macchina a stati generica che comprende anche gli stati di transizione. In alcune implementazioni l'ingresso nello stato viene assegnato ad una funzione detta di guardia la quale stabilisce se passare al prossimo stato. Ora visto che il topic è destinato al principiante va detto che introdurre le transizioni, la teoria, le possibili implementazione porta avere troppa carne al fuoco. Inoltre non è detto che sia necessario implementare una macchina a stati complessa inclusiva di transizioni ecc.

Gli interventi di AmericanDreamer ci fanno capire che quando sappiamo cosa deve fare una macchina a stati siamo
già a metà strada. Come implementarla, quanto ci pare chiara e mantenibile in parte è soggettivo, come dire ogni soluzione ha i suo vantaggi e svantaggi, cioè solo impiegando quella struttura di macchina a stati per risolvere un problema reale ci dirà quanto è flessibile, quanto è scalabile e quanto appare chiara.

Ciao.

quindi la mia prima implementazione funziona?!

bene, non la avevo provata (sempre fuori di stampella

infatti non mi ero accorto del problema dell'indterminatezza a causa di un side effect (++) in una operazione

quindi grazie

per quanto riguarda la seconda, che nemmeno quella ho provato, quello che ci manca è solo l'azione che eventualmente serve eseguire durante lo stato (o all'inizio)

anche questo lo vedo semplice, basta aggiungere una variabile azione alla struttura, che contenga la funzione necessaria

invece qunado una certa implementazione appare chiara è certamente soggettivo, dipende da come uno "vede" la cosa

ammetto poi che la ho messa giù piuttosto involuta

però la mantenibilità e la espandibilità sono molto meno soggettive
basta misurare il tempo necessario a cambiare le specifiche

aggiungere e/o cambiare qualche elemento di array è certamente più semplice che ri-scrivere la loop

(tutto questo viene dai miei pensieri per il famoso semaforo dei miei primi post)

si potrebbe anche pensare a una matrice di pin in input e una sequenza di matrici di condizioni da soddisfare....

AmericanDreamer:
si potrebbe anche pensare a una matrice di pin in input e una sequenza di matrici di condizioni da soddisfare....

Dal punto di vista della chiarezza secondo me la macchina dovrebbe assomigliare il più possibile alle frasi in italiano che descrivono il funzionamento:

SE fase riempimento:
    SE raggiunto livello: chiudi valvola, fase = decantazione
    ALTRIMENTI SE timeout 60s: chiudi valvola, accendi segnalazione, fase = errore

ALTRIMENTI SE fase decantazione:
    SE timeout 30 minuti: accendi riscaldatore, fase = riscaldamento
    ALTRIMENTI SE comando scarico: apri valvola scarico, fase = scarico

ecc ecc ecc

Così sarebbe chiarissimo d'accordo

Ma con tutto hard coded è anche il minimo dalla flessibilità

Serve di fare un trading off (compromesso, giusto?)

Secondo me lo sketch seguente anche se fa la stessa cosa del precedente appare già più complesso da capire specie per un principiante che non si è ancora confrontato con la realizzazione di una macchina a stati.

Lo sketch fa la stessa cosa del primo ma 2 volte, cioè ci sono due macchine a stati che ovviamente per semplificare fanno la stessa cosa. Appare evidente che più macchine a stati che fanno la stessa cosa è inutile, infatti lo è ma vi assicuro che trova applicazione pratica, infatti ho usato questa implementazione per un doppia plafoniera controllata in PWM dalle due uscite del TIMER1, il comportamente è identico a quello delle luci di cortesia che avete dentro la vostra automobile. Ci sono due tasti per entrare in modo On Demand (uno pulsante tattile per plafoniera) e due ingressi PCINT (pin changed) da collegare ai pulsanti delle portiere oppure se l'auto è moderna lo stato della portiera viene comunicato via protocollo LIN.

struct fsmData_t {
  boolean oneShotSwitch;
  uint32_t elapsedTime;       /**< Contiene la differenza in ms tra due chiamate a millis()*/
  uint32_t mainTimer;         /**< Contiene il valore in ms restituito da millis() */
  byte currentState = 0;
  byte oldState = ~currentState; /**< Stato precedente */
  byte timerCounter = 0;         /**< Contatore generico usato per contare i secondi trascorsi */
};
typedef fsmData_t fsmData_t;

fsmData_t fsmData0;
fsmData_t fsmData1;

void setup() {
  // put your setup code here, to run once:
  Serial.begin(9600);
  delay(1000);

}


void printDot(fsmData_t *fsm) {
  if (fsm->elapsedTime >= 1000) {
    fsm->mainTimer = millis();
    fsm->timerCounter++;
    Serial.print(".");
  }
}

boolean check10s(fsmData_t *fsm) {
  if (fsm->timerCounter == 10) {
    Serial.print(millis());
    Serial.println("");
    return true;
  }
  return false;
}


void printOneShot(fsmData_t *fsm) {
  Serial.print("case ");
  Serial.print(fsm->currentState);
  Serial.print("-millis():");
  Serial.print(millis());
}

void fsmUpdate(fsmData_t *fsm) {
  fsm->oneShotSwitch = false;
  if (fsm->currentState != fsm->oldState) {
    fsm->oneShotSwitch = true;
    fsm->mainTimer = millis();
    fsm->timerCounter = 0;
  }

  fsm->oldState = fsm->currentState;
  fsm->elapsedTime = millis() - fsm->mainTimer;
}


void fsmApp(fsmData_t *fsm) {
  fsmUpdate(fsm);
  switch (fsm->currentState) {
    case 0:
      if (fsm->oneShotSwitch) {
        // se oneShotSwitch == true stampa "case 1"
        printOneShot(fsm);
      }

      printDot(fsm);
      if (check10s(fsm)) {
        fsm->currentState = 1;
      }

      break;
    case 1:
      if (fsm->oneShotSwitch) {
        // se oneShotSwitch == true stampa "case 1"
        printOneShot(fsm);
      }

      printDot(fsm);
      if (check10s(fsm)) {
        fsm->currentState = 2;
      }

      break;
    case 2:
      if (fsm->oneShotSwitch) {
        // se oneShotSwitch == true stampa "case 1"
        printOneShot(fsm);
      }

      printDot(fsm);
      if (check10s(fsm)) {
        fsm->currentState = 0;
      }

      break;
  }
}

void loop() {
  // put your main code here, to run repeatedly:
  fsmApp(&fsmData0);
  fsmApp(&fsmData1);
}

Chi si dovesse prendere la briga di testare questo codice noterà che l'output su serial monitor appare
confuso. Potremmo però pensare di sostituire i Serial.println con funzioni specifiche che inviano qualcosa
che identifichi la macchina a stati. Il ricevente allora sarà un applicazione sul pc che sostanzialmente divide
lo schermo del serial monitor in due. Sicuramente dovremmo modificare la struct fsmData_t, oppure aggiungergli
un puntatore ad una struttura aggregata. Mi sono spinto oltre ciò che desideravo, pertanto mi fermo qui al momento,
anche perché credo ci sia ancora da dire sulla prima e seconda implementazione.

bene, non la avevo provata (sempre fuori di stampella

infatti non mi ero accorto del problema dell'indterminatezza a causa di un side effect (++) in una operazione

quindi grazie

per quanto riguarda la seconda, che nemmeno quella ho provato, quello che ci manca è solo l'azione che eventualmente serve eseguire durante lo stato (o all'inizio)

Ok, mi sta bene tutto, tranne questo, capisco che non sempre si ha la possibilità di testare il codice, ma in
tal caso è meglio specificarlo prima. Come dire uomo avvisato mezzo salvato. Pensa se lo avesse sperimentato
un principiante, cosa ne avrebbe dedotto?

Ciao.

Ne avrebbe dedotto, giustamente, che si tratta di spunti e non pappa fatta

Considerando le possibili 'azioni' su un pin in uscita

Che sono 3, non due

Accendi, spegni, lascia inalterato

Anche per gli ingressi le condizioni sono 3
Deve essere alto, deve essere basso, ignorato

Che cosa mi consigliate per stivare un array di azioni?
Fossero solo 2 userei un numero binario
Deve essere una cosa facile da scrive e e da comprendere

Belle le luci di cortesia, ma senza per ci provo

Ne avrebbe dedotto, giustamente, che si tratta di spunti e non pappa fatta

Che male c'è a fornire degli esempio per implementare una macchina a stati funzionante, macchina che poi dovrà comunque adattare all'applicazione e per farlo dovrà capire come funziona, per cui non si tratta di pappa fatta.

Torno indietro e mi pongo la seguente domanda: Come si divide una applicazione in stati?
Mi rispondo che non lo so, accade lavorando di fantasia, di certo c'è un inizio, potrebbe anche esserci una fine, e nel mezzo ci sono tanti stati quanti ne servono. Questo punto è fondamentale per cui si dovrebbe provare a scrivere qualcosa in merito.

Belle le luci di cortesia, ma senza per ci provo

::slight_smile:

Il mio è solo un esempio al fine di non giudicare totalmente inutile ciò che appare inutile.

Ciao.

Maurotec:
Torno indietro e mi pongo la seguente domanda: Come si divide una applicazione in stati?

Uh, allora vale anche: Come si divide una applicazione in macchine? Come comunicano? Come si sincronizzano ?

Mi rispondo che non lo so, accade lavorando di fantasia, di certo c'è un inizio, potrebbe anche esserci una fine, e nel mezzo ci sono tanti stati quanti ne servono.

Questo mi sembra semplice: uno stato per ogni diversa situazione di attesa (in cui si può permanere da un solo ciclo all'infinito a seconda degli eventi che accadono).

AmericanDreamer:
Che cosa mi consigliate per stivare un array di azioni?
Fossero solo 2 userei un numero binario
Deve essere una cosa facile da scrive e e da comprendere

È qui il difficile... dentro uno stato si può aver bisogno di fare un sacco di cose (rimanendo sempre nello stesso stato), non solo attendere un evento e cambiare stato, ad esempio il mio stato di modifica ora dell'orologio non capisco come possa essere facilmente parametrizzato

Si Claudio_FF, grazie per l'intervento, in effetti sono domande che sorgono spontanee quando affronti il problema, si può tentare di inventare qualcosa al momento per sincronizzarle ma alla fine ci si accorge che tutto è già stato inventato e sperimentato.

Ora può sembrare facile dividere una applicazione in stati e ancora più semplice se esiste una sola macchina a stati detta appunto Main Machine. Ma tu e io sappiamo benissimo che non è sempre così evidente specie per il principiante (e non solo).

Allora potrei dire che abbiamo almeno due strade da seguire:

  1. Pianifichiamo tutto a tavolino, scriviamo codice di test, scriviamo simulatori, scriviamo diagrammi, ci lavoriamo in tanti fino a quando non appare chiaro che la macchina a stati funzionerà come ci si aspetta. Tradotto vuol dire che investiamo la maggiore parte del tempo per pianificare, per assurdo potremmo ritrovarci a lavora per 2 mesi senza avere scritto una riga di codice.

  2. Studiamo il problema in modo superficiale, scriviamo la macchina a stati inserendo gli stati che ci appaiono evidenti, due, tre, quattro comunque pochi. Ritorniamo a studiare il problema e modifichiamo se necessario il codice e ripetiamo questo processo fino a quando otteniamo il risultato desiderato.

Il secondo metodo è quello che si intraprende istintivamente, il primo richiede tanto lavoro, tanta documentazione da produrre e chissà cos'altro prima di potere scrivere una riga di codice.

Per coloro i quali voglio osservare una applicazione e immaginarsi gli stati di questa, consiglio di mettere avanti la proprio lavatrice.

PS: Fate ciò quando siete soli, perché solitamente accende il televisore per guardarlo, avviare la lavatrice per guardarla lavorare può allarmare i vostri cari. :smiley:

Ciao.

mi sono risposto da solo
per memorizzare semplicemente più di 2 possibili azioni su un pin serve un enum
che tanto è un intero

allora tantovale un byte

byte per byte ho pensato prima ad un carattere e poi ad una stringa

typedef bool (* verifica)(void);
typedef void (* esegui)(void);

struct Regola
{
    int stato;
    esegui azione;
    unsigned long int tempo;
    verifica condizione;
    char output[11];
    char input[11];

    int prossimo;
} ;

byte pinoutput[10] = {6, 7, 13, 14};
byte pininput[10] = {2, 3, 4, 5};



byte stato = 0; // lo stato attuale
unsigned long int timestart;


bool vero()
{
    return 1;
}
void nulla()
{
    return;
}

Regola regole[] = {{0, nulla, 10000, vero, "111", "10", 1 }};
#define REGOLE sizeof regole / sizeof regole[0]


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

void loop()
{
    for (byte i = 0; i < REGOLE; i++)
    {
        if (regole[i].stato == stato)
        {
            if (millis() - timestart >= regole[i].tempo)
            {
                byte j = 0;
                byte test = 1;

                while (regole[i].input[j])
                {
                    if (regole[i].input[j] == '1' && digitalRead(pininput[j] == LOW))
                    {
                        test = 0;
                    }

                    if (regole[i].input[j] == '0' && digitalRead(pininput[j] == HIGH))
                    {
                        test = 0;
                    }

                    j++;

                    if (regole[i].condizione() || test == 1)
                    {
                        stato = regole[i].prossimo;
                        timestart = millis();
                    }
                }
            }
        }

        agisci(stato);
    }
}
void agisci(int stato)
{
    static byte vecchio = 255;

    if (stato != vecchio)
    {
        vecchio = stato;
        Serial.print("inizio stato: ");
        Serial.println(stato);
        regole[stato].azione();
        byte i = 0;

        while (regole[stato].output[i])
        {
            if (regole[stato].output[i] == '1')
            {
                digitalWrite(pinoutput[i], HIGH);
            }

            if (regole[stato].output[i] == '0')
            {
                digitalWrite(pinoutput[i], LOW);
            }

            i++;
        }
    }
}

come vedete ho aggiunto un puntatore a funzione per l'azione dello stato, che verrà eseguita assieme alla stampa

e poi due stringhe, di numeri, 1 vale alto, 0 vale basso, qualsiasi altra cosa (compreso stringa terminata prima della lunghezza massima) vale NON TOCCARE NON INFLUENTE

all'inizio di uno stato assieme alle stampe viene eseguita una volta sola
la stampa
la funzione di ingresso
e un ciclo che legge la stringa degli output e li aggiorna, senza toccare quelli che sono su NON TOCCARE

durante lo stato viene testata sia
la funzione di uscita che
un ciclo che confronta i piedini di ingresso con la stringa di riferimento, ignorando quelli che sono su NON INFLUENTE

per correttezza non lo ho provato, e anche mancano i pinMode, gli stati successivi al primo e via così

è solo uno spunto, ripeto, che comunque la compilazione va a buon fine

anche questo viene dal mio pensare al mio semaforo

prossimo passo
farne una classe, istanziabile (si dice così?) più volte
per avere più macchine in contemporanea, anche che potrebbero interagire, leggendosi tra di loro i piedini
(oddio, scritto così semba una cosa oscena...

Claudio_FF:
.....ad esempio il mio stato di modifica ora dell'orologio non capisco come possa essere facilmente parametrizzato

guarda, non mi ci metto nemmeno

non mi piace come è scritto

ho già abbastanza problemi a leggere il c come raccontavano i vecchi (K&R)

ma scritto come lo hai scritto tu non è leggibile per me, mi spiace

Secondo me ogni regola dovrebbe avere un identificativo come primo byte, l'idea è di potere passare un puntatore void all'attuatore di regole il quale ricava il primo byte è pertanto sa come castare il puntatore void, cioè almeno lo deve sapere. Cioè una cosa simile a ciò che si fa con le macchine event driver, dove un evento ha un ID e da questo si ricava la struttura che non è detto sia unica per tutti gli eventi.

Comunque partire dalle regole mi mette in difficoltà le stesse che ho dovuto affrontare al tempo quando ho dovuto scrivere un generatore di makefile.

Ora cosa vogliamo ottenere mi sfugge, forse una macchina a stati generica governata da regole?
Mi pare così, interessante è interessante ma non ho mai visto la cosa dalla parte opposta, cioè prima scrivo le regole
le carico nel gestore delle regole e avvio la macchina a stati.

Ciao.

questo pensavo
un realizzatore di macchine
che basta caricare un array di regole e accendere
magari invece di un array un file su scheda, si potrà?
prossimo passo