Ciao a tutti.
Visto che uno dei consigli che si da più spesso per sviluppare delle sequenze di azioni più o meno complesse nei nostri progetti Arduino, è quello di usare una macchina (o automa) a stati finiti, volevo aprire una discussione specifica su questo tema se siete d'accordo.
Ad esempio io uso molto spesso questa tecnica, al punto che ho deciso di sviluppare una libreria specifica per non dover ogni volta reinventare la ruota (usando un'astrazione software indipendente dal modello stesso della macchina basato su stato e transizioni di stato).
Senza addentrarci troppo nei formalismi matematici che descrivono in modo rigoroso cos'è una macchina a stati finiti, mi piacerebbe sapere cosa ne pensate in merito.
Per me la macchina a stati è l'implementazione concreta di una più astratta logica a stati, o programmazione orientata agli stati. E temo che come non esiste l'automobile universale o l'alimentatore universale (ci saranno sempre casi reali in cui queste cose altrettanto reali, qualsiasi esse siano, si dimostreranno inadatte), così anche l'idea dell'implementazione universale per me va a naufragare. Ci può essere senz'altro l'implementazione più adatta alle proprie necessità, ma di fatto rimane principalmente un metodo di lavoro. Ricordo anch'io un thread passato su una FMS, tanto elegante e universale nell'idea, quanto all'atto pratico inutilizzabile per far funzionare la mia sveglia sul comodino.
In senso più formale si può dire che qualsiasi macchina in cui il comportamento in un determinato momento dipende non solo dagli ingressi attuali, ma anche da qualcosa avvenuto in momenti passati, è una macchina a stati. Un programma, scritto in qualsiasi modo, che tenga conto anche di un solo bit elaborato in un qualsiasi precedente ciclo di elaborazione, è una macchina a stati.
Questi stati possono essere impliciti ed "emergere dal funzionamento" (come nella normale programmazione procedurale), oppure espliciti. Tra l'altro le FMS di cui normalmente parliamo sono un caso limite di un insieme di possibili macchine, dove l'altro caso limite è che tutti gli stati possono essere contemporanemante attivi (la quantità massima di stati attivi contemporaneamente definisce la quantità di processi paralleli "contemporanei" gestiti dalla macchina).
Le nostre FMS su Arduino sono processi singoli (un solo stato attivo alla volta), e in ogni caso una macchina multiprocesso può essere scomposta in più macchine singolo processo comunicanti tramite variabili (impulsive, mantenute, messaggi). Per cui con le FMS base si può fare proprio tutto.
I motivi per cui per me sono necessarie/fondamentali/indispensabili ecc sono:
permettono in modo naturale, e con pochissime risorse, di realizzare il multitasking cooperativo (che non ha bisogno di nessun sistema operativo o schedulatore HW/SW di task)
Permettono di pensare ai processi in modo più naturale (i famosi predicati: se sono in questa situazione e succede questo faccio questo e passo a)
Permettono di scomporre processi complicati con troppi dettagli da considerare tutti insieme, in più processi semplici, indipendenti e cooperanti.
E naturalmente entrano in gioco, anche se magari non nella forma switch/case, ma anche con soli flag, ogni volta che occorra fare più di una sola cosa contemporaneamente.
Ci sono niubbi e niubbi.
Di sicuro c'è più di qualcuno che si ostina a postare sempre lo stesso tipo di richieste di aiuto, cosa che rende evidente che non hanno alcuna voglia di approfondire.
Con questi ovviamente si perde solo del tempo, ma ci sono anche tante persone, magari silenti nel forum, che invece si mettono in gioco e spesso con ottimi risultati.
Io non lo vedo come un discorso di universale oppure no; il metodo più diffuso con l'uso di switch/case ad esempio se vogliamo è rapido e universale come approccio (una volta capito il principio), solo che ogni volta va declinato secondo le specifiche.
Il problema che ho sempre riscontrato io con questo sistema è che il modello della macchina a stati finiti, non risulta immediatamente evidente dall'implementazione stessa del codice perché "il modo" in cui viene codificata la macchina in qualche modo "disperde" l'attenzione cosa che fa un po' a cazzotti con quelle che sono le ragioni principale per cui decido di usare una macchina a stati finiti:
chiarezza concettuale del funzionamento;
struttura modulare del codice;
gestione ordinata degli stati possibili;
efficienza del codice in termini di memoria e tempo di esecuzione;
debug e manutenzione del codice.
Ad esempio, prendiamo questo questo automa "scolastico" per la risposta al telefono:
Con uno switch case potrei modellarlo più o meno cosi:
enum States {Attesa, Chiamata, Conversazione};
int stato = Attesa;
switch (stato) {
case Attesa:
if (ricezione)
stato = Chiamata
break;
case Chiamata:
if (accettazione)
stato = Conversazione
if (rifiuto)
stato = Attesa
break;
case Conversazione:
if (conclusione)
stato = Attesa
break;
}
La relazione tra la rappresentazione grafica dell'automa ed il codice non mi risulta evidente (se non vado a spulciare il codice) e se voglio cambiarne il funzionamento (che ne so, magari mettendo lo stato in cui la segretaria mette in attesa il chiamante ) devo rimettere mano all'implementazione logica.
Usare un sistema basato su stati e transizioni di stato invece mi risulta più "congeniale" e posso definire il modello dell'automa secondo un paradigma dichiarativo:
// Definisco gli stati (i cerchi)
macchina.aggiungiStato(Attesa);
macchina.aggiungiStato(Chiamata);
macchina.aggiungiStato(Conversazione);
// Definisco le transizioni per ciascun stato (le frecce)
Attesa.aggiungiTransizione(Chiamata, ricezione); // Stato destinazione, evento
Chiamata.aggiungiTransizione(Attesa, rifiuto);
Chiamata.aggiungiTransizione(Conversazione, accettazione);
Conversazione.aggiungiTransizione(Attesa, conclusione);
La relazione ora è evidente e se voglio modificare il modello, mi basta dichiarare gli stati e le transizioni aggiuntive.
Che poi di fatto è anche come è stato implementato nel framework C++ cross-platform QT ovviamente in modo molto più esteso.
Concordo al 100% e proprio per questo io vorrei puntare il focus non tanto sul come implemento la FSM quanto piuttosto sul perché e sul processo di modellazione della FSM stessa a livello concettuale.
Beh nell'IDE Arduino c'è di tutto a dire il vero, comprese le mie librerie
Il fatto è che per aggiungere una libreria all'elenco non c'è alcun tipo di filtro o controllo, basta che il tool Arduino Lint non segnali errori. Ma il linter fa una verifica essenzialmente di forma del codice, e non di sostanza.
Se ci fosse almeno un sistema di feedback da parte di chi le usa (qualcosa tipo le classiche 5 stelle) sarebbe una gran cosa!
Oltre al fatto che la metà degli utenti Arduino non ha idea di cosa sia Github, non è proprio la stessa cosa.
Librerie di grande successo possono avere un numero notevole di issue segnalate perché sono tanti gli utenti che le usano, ma questo non è indice dell'apprezzamento che la libreria riscontra nella community.
Forse allora sarebbe meglio prendere in considerazione il numero delle "starred", ma vale lo stesso discorso dei grandi numeri.
Senza contare che poi una gran parte delle issue segnalate sono solo richieste di come usare questa o quella funzionalità, a me succede continuamente ad esempio
E' comunque un indice sulla libreria e su quanto rapidamente viene manutenuta ...
... che poi pochi qui usino Git, siamo d'accordo ... ma non dovunque è così e quindi, in mancanza d'altro, è comunque uno strumento per poter fare qualche minima valutazione
A me pare che tra le lib scaricabili da ide ci sono già alcune FSM. Ne ho contate almeno 18 sull'argomento fsm
la tua AgileStateMachine ad esempio mi pare ben fatta.
non credo però sia semplice per un niubbo. Anche a me, abituato a codice strutturato, mi è più semplice (e leggibile) una FSM castrata in un case che non un codice che segue il grafo.
Probabilmente è una questione di "abitudine". Non sono molto avvezzo a disegnare quei grafi stato/azione.
Buonasera a tutti
Interessante l'argomento ed interessante l'idea di una libreria.
Credo però che per "imparare" la cosa migliore sia vedere il codice e seguirlo in caso di problemi.
Una libreria nasconde tutti i ragionamenti che stanno dietro al concetto di Macchina a stati finiti.
Impari con il codice e quando sai bene di cosa si parla ben venga una libreria (che accelera la fase di sviluppo).
...parere mio
Ma una libreria non è che viene dal nulla, sempre di codice si tratta.
Anzi, secondo me si impara molto di più nello sviscerare una libreria cercando di comprenderne il funzionamento che dai presunti tutorial didattici che si trovano online.
Per questo io consiglio sempre di usare un IDE più avanzato del semplice Arduino IDE se si vuole imparare, in modo da arrivare con pochi click a visualizzare tutto il sorgente, librerie e core incluso.
Giusto per fare un esempio di quello che intendo, qui ho scritto volutamente una riga sbagliata e viene subito evidenziato l'errore in rosso ed il motivo per cui è un errore. Se faccio CTRL + Click sul metodo evidenziato, si apre immediatamente il file dove c'è la definizione della classe e posso vedere in 20 secondi cosa si aspetta.
Quando non capisco cosa si fa nella classe, con l'aiuto di San Google e del più giovincello San ChatGPT approfondisco e colmo le mie lacune.
Il perché non abbiano inserito queste funzionalità (che sono di base in qualsiasi ambiente di sviluppo degno di tale nome) in Arduino IDE 2.x.x per me rimane un mistero insondabile.
Lo spero
E' abbastanza recente in realtà e quindi ci sarà di sicuro qualche bug nascosto.
Ma infatti come diceva anche @Claudio_FF si tratta di un metodo di lavoro.
La mia necessità (da cui deriva il mio metodo di lavoro) era avere un sistema che mi consentisse di rimaneggiare un firmware (anche dopo molto tempo) senza dover ogni volta perdere mezza giornata a ricostruirne lo schema di base.
Se voglio aggiungere uno stato alla mia FSM, mi basta intervenire nella funzione dove viene modellata (che chiamo sempre setupStateMachine() negli esempi) e tutto il resto può rimanere grosso modo invariato; magari aggiungo qualche variabile di controllo bool per attivare o no altre funzioni legate al nuovo stato.
Salve, lascio qui i miei commenti per la creazione di una (semplice) macchina a stati finiti universale
il mio target non è di realizzare l'irrealizzzabile o ottenere la luna, ma solo di indicare un modo, il mio (nostro, io e il fratello) modo
per prima cosa una FSM non può essere slegata dal "lavoro sporco" che deve fare, serve di avere un obiettivo
io ho preso come risultato da ottenere questo:
roba recente e quindi tutti la abbiamo in mente
e come lavoro teorico di partenza questo:
che non è mio, ma mi sembrava una buona base, anche se piuttosto datato
una FSM (o anche AFSD, automa a stati finiti deterministico) è composto da:
un insieme finito di stati
un insieme finito di condizioni che provocano la commutazione tra uno stato (definito) verso un'altro, definito anch'esso
un insieme finito di azioni che lo stato compie al suo ingresso
un insieme finito di azioni che lo stato compie alla sua uscita
un insieme finito di azioni che lo stato compie nel tempo intercorrente tra il suo inizio e la sua fine (mantenimento)
con tutte le "libertà" possibili in queste 5 righe
ad esempio ci possono essere più condizioni di uscita da uno stadio al successivo
ci possono essere più stati che evovlvono (per condizioni uguali o differenti) nello stesso stato
uno stato può evolvere (per condizioni necessariamente differenti, altrimenti saremmo in una AFSND) in stati differenti
uno stato potrebbe non evolvere, ovvero non avere una condizione di uscita (stato di stop finale)
uno stato potrebbe non avere una condizione di ingresso (stato di start iniziale, sarebbe inizializzato nel preambolo del programma)
uno stato potrebbe mancare di una o tutte le azioni da svolgere, che potrebbero anche esssere doppie rispetto ad altri stati
e via così...
mi toccherà quindi creare tre typedef per le tre categorie di oggetti che ho citato
1 -> stati
2 -> condizioni
3 -> azioni, che possono essere di ingresso, di uscita oppure di mantenimento
le più semplici sono le ultime
con una funzione di callback me la sono cavata, si scrive semplicemente la lista della azioni come se fossero funzioni void
e le facciamo eseguire in callback
invece le condizioni sono leggeremnte più complesse
perchè da una parte serve di definire la condizione vera e propria, squadra che vince non si cambia e uso ancora una callback, non void ma bool
di una funzione che esegua il test prescritto
ma serve anche definire da che stato a che stato provocano la commutazione
quindi sarà un dato strutturato
gli stati saranno anche loro un dato strutturato
perchè devono contenere il richiamo delle tre funzioni delle quali parlavamo prima
quindi una struttura a 4 membri, stato intero, e tre puntatori a funzione
mi sembra di aver ben inquadrato una (possibile) strada
cosa ne pensate, comincio a buttar giù codice e lo mostro qui?
Mi sembra un interessante esperimento, anzi propongo a chi è interessato di implementare la propria versione con il metodo e/o la libreria che preferisce (compatibilmente con il tempo e gli impegni di ciascuno di noi) per poi discuterne in modo costruttivo.
Io, manco a dirlo, userò la mia libreria
Un possibile layout? Appena posso completo i collegamenti (anche se dal punto di vista elettrico è profondamente sbagliato, eviterei resistenze e driver per i led cosi da semplificare il disegno)
Se vuoi farlo realistico, non puoi mettere LED in parallelo: in Italia, il giallo si accende dal verde al rosso, ma non viceversa; inoltre, ci sono sempre due secondi di rosso per tutti prima di ogni verde, così come ci sono alcuni secondi di rosso per tutti quando i semafori, di notte, passano al giallo intermittente e quando, all'alba, riprendono il normale funzionamento. Pilotandoli indipendentemente, dopo un inizio semplice puoi fare ogni altra cosa, altrimenti no!
Ok allora modifico i collegamenti in tal senso.
Al massimo come display contasecondi aggiungo un oled così bastano solo 2 gpio oppure metto un Arduino mega.