[Partage] YASM, bibliothèque de création de machines à état très simple d'emploi

[u][b]YASM[/b][/u] ( Yet Another State Machine library )

Une librairie permettant la mise en place extrêmement simple de machines à états, sur le principe un état = une fonction, et basée sur les pointeurs de fonction.
Cela permet d'avoir un code facile à comprendre et à maintenir.

Avant d'aller plus loin, si vous ne savez pas ce qu'est une machine à état ou automate fini, ni en quoi cela peut vous aider à résoudre efficacement votre problème, je vous conseille de lire le très très bon tuto de J-M-L sur la question.
Vous verrez ensuite en quoi l'utilisation de la librairie simplifie sa mise en œuvre.

Une explication détaillée des fonctions disponibles est disponible en anglais ici : GitHub - bricofoy/yasm: Yet Another State Machine library - a function pointer based state machine library for arduino
Je vais la reprendre ici en français.

Installation

La librairie est disponible directement dans le gestionnaire de librairies de l'IDE arduino. Pour l'installer rien de plus simple donc, il suffit de la sélectionner dans la liste et cliquer sur installer.

Sinon, la dernière version de la librairie est disponible ici : https://github.com/bricofoy/yasm/archive/master.zip

Ce lien permet de télécharger un fichier yasm-master.zip
Quand on décompresse ce fichier soit manuellement soit via le menu "ajouter une bibliothèque zip" de l'IDE, on obtient un dossier "yasm-master", à renommer en "yasm" et à placer dans le dossier "libraries" d'arduino. (Il y est déja si on a décompressé via l'IDE, il faut juste renommer)

Une fois cela fait, la librairie devrait apparaître dans le gestionnaire de librairies de l'IDE arduino.

Utilisation

L'exemple le plus simple étant toujours le même : faire clignoter une led, on va commencer par là.
Cela permet de prendre contact avec la librairie, et avec sa fonction permettant de créer une temporisation.

Pour envisager la question sous l'angle des machines à état, nous voyons immédiatement que nous avons besoin de deux états : un état où la led est allumée, appelons-le ledOn, et un état où la led est éteinte, ledOff (comme c'est original...)
Ensuite nous aurons besoin de passer d'un état à l'autre à l'issue d'une temporisation, de manière à faire changer l'état de la led et obtenir son clignotement. Avec une subtilité tant qu'a faire, puisque ce n'est pas plus compliqué à mettre en place : le temps allumé et le temps éteint ne sont pas les mêmes.

Voici comment procéder :

D'abord, il faut inclure la librairie, soit depuis le menu, soit en écrivant directement au début du sketch :

#include <yasm.h>

On va ensuite définir deux constantes pour les délais allumé et éteint :

#define OnDelay  500 //500ms 
#define OffDelay 750 //750ms

Puis on va définir sur quelle broche de la carte la led est connectée. Ici, la broche 13 car la plupart des cartes ont déjà une led dessus, ce qui permet de tester le code sans utiliser de composants externes :

#define LedPin 13

Ensuite on rentre dans le vif du sujet avec la déclaration de la machine à états dont on a besoin, que nous allons très originalement nommer "led" :

YASM led;

Puis nous aurons les 2 fonctions habituelles d'un sketch arduino, setup() et loop().

Dans le setup, nous allons initialiser la broche utilisée pour la led en sortie, puis donner à la machine à états son état initial. Ici on veut que la led s'allume, on va donc initialiser la machine sur l'état "ledOn" :

void setup()
{
    pinMode(LedPin, OUTPUT); //declaration de la broche de la led comme sortie
    led.next(ledOn); //definition de l'etat initial de la machine
}

Ensuite vient la fonction loop, qui ne contient quasiment rien à part la mise à jour de la machine à états :

void loop()
{
    led.run();  //mise a jour de la machine a etats
}

Et enfin, il faut définir les état dont nous aurons besoin pour faire clignoter notre led, soit un état allumé : ledOn, et un état éteint : ledOff

void ledOn()
{
    digitalWrite(LedPin, HIGH); //ici c'est l'etat "on" donc nous allumons la led
 
    if(led.elapsed(OnDelay)) //on teste si le delai d'allumage est ecoule
         led.next(ledOff); //si le delai est ecoule on passe a l'etat "off"
}

void ledOff()
{
    digitalWrite(LedPin, LOW); //ici c'est l'etat "off" donc on eteint la led
 
    if(led.elapsed(OffDelay)) //on teste si le delai d'extinction est ecoule
         led.next(ledOn); //si le delai est ecoule on passe a l'etat "on"
}

Chaque état est une fonction indépendante, qui sera exécutée répétitivement par la machine à états tant que l'état qu'elle représente est actif. Ici le modèle des deux fonctions est exactement similaire : on commence par allumer (ou éteindre) la led, puis on vérifie si le délai voulu dans l'état est écoulé avec la fonction elapsed(délai en ms).
Si le délai n'est pas écoulé, elapsed(délai) retourne faux(false), la condition du if n'est pas validée donc la fonction se termine directement. Elle est ensuite relancée immédiatement au prochain cycle, nous restons donc dans cet état.

Si le délai est écoulé, elapsed(délai) retourne vrai(true), la condition du if est validée et on demande le changement d'état avec next(etat suivant);, puis la fonction se termine et c'est alors l'état suivant est exécuté au prochain cycle.

Le code complet de l'exemple BlinkLed (fourni avec la librairie, vous pouvez directement ouvrir l'exemple plutôt que de tout retaper) est donc :

/* 
   BlinkLed.ino example file for yasm library.
   
   The purpose of this arduino sketch is to blink a led on pin 13.
   
   It creates a state machine with two states : ledOn, ledOff, 
   and illustrate the use of elapsed(delay_in_ms) timing function to trigger 
   state change.                                                              
*/


#include <yasm.h> //include the yasm library

#define OnDelay 500 //500ms led "On" state delay
#define OffDelay 750 //750ms led "Off" state delay

#define LedPin 13  //pin 13 because most arduino boards provide a led here

YASM led; //declaration of the "led" state machine

void setup()
{
     pinMode(LedPin, OUTPUT); //declare the led pin as output
     led.next(ledOn); //set the initial state of the led state machine
}

void loop()
{
     led.run();  //update the state machine
}

//////////led state machine/////////////

void ledOn()
{
     digitalWrite(LedPin, HIGH); //this is the "On" state so we turn on the led
 
     if(led.elapsed(OnDelay)) //check if the delay for the "on" state is elapsed
         led.next(ledOff); //if the delay is elapsed we switch to the "off" state
}

void ledOff()
{
     digitalWrite(LedPin, LOW); //this is the "Off" state so we turn off the led
 
     if(led.elapsed(OffDelay)) //check if the delay for the "off" state is elapsed
         led.next(ledOn); //if the delay is elapsed we switch to the "on" state
}

Amusante cette petite lib

Je redonne le lien pour une consultation en ligne https://github.com/bricofoy/yasm/

salut bricoleau

c'est assez proche de ce qu'on peut faire avec ton ordonnanceur, finalement, mais vraiment orienté automatisme
et oui merci de donner mon lien, en effet j'ai détaill les fonctions disponibles sur la page github

Pour ma part, j'en suis resté à un enum pour les états, et l'appel depuis loop() d'une fonction chapeau qui contient un bon gros switch :slight_smile:

Mais dans ton approche, j'aime bien l'isolation du code qui en découle.

Par exemple, si on veut ajouter un nouvel état, il y a juste à ajouter une fonction qui lui est associée, et modifier les fonctions associées aux états précédents pour leur ajouter un nouveau débranchement.
Donc la probablité de régression sur d'autres états est quasi nulle.
Pour les machines à état complexes, cela peut être une sécurité appréciable lors de l'implémentation des évol.

Alors qu'avec la solution basique, il faut en plus ajouter une valeur dans le enum et modifier le switch.
Ce n'est ni complexe ni vraiment risqué, mais quand même on modifie une portion de code commune à tout le monde, donc le risque de regression est forcément supérieur.

En fait, ton yasm me donne des idées pour gérer des menus sur écran LCD.

Je suis actuellement sur un dev avec un menu très riche, et une bonne profondeur de sous-menus.
Et l'approche qui consiste à décrire dans un endroit unique l'arboresence complète de la navigation, est toujours un peu galère pour ajouter ou déplacer des ramifications. En plus certains états de navigation peuvent avoir plusieurs états amont possibles.

Ton approche permet une construction de code plus dynamique, étape par étape.

pour les menus, il y en a un semblant d'exemple dans les exemples "rec_sondes" de la lib, et il y en a un bon gros dans mon projet de régul de chauffage

j'ai prévu de faire un exemple de menu ici, mais le temps me manque un peu

pour la gestion des états, avec cette approche c'est (à mon sens) bien plus simple que même avec une enum. Un certain nombres de mes anciens projets basés sur une MET avec une enum, et que j'ai repris avec cette lib ont fonctionné du premier coup sans le moindre bug, alors que c'était la galère pour faire fonctionner les choses avec l'ancien système.

Et aucun problème pour avoir des enchaînements très complexes d'états, ni pour executer en // un grand nombre de MET, voire les imbriquer dans certains cas (les lignes horizontales du grafcet...)

En plus un autre gros intéret, c'est que en général quand on fait une erreur à l'écriture du programme, en particulier en rajoutant des états dans un truc existant, ça ne compile pas si on se plante dans les transitions (nom d'état inexistant)

Bonjour Bricofoy,

je suis un inconditionnel de machines à états finis et je vais très certainement utiliser ton YASM dans mes prochains projets.
Merci pour le partage.

PS: je note que tu n'as pas pu résoudre le problème de connaître l'état de la machine à l'exterieur de celle-ci.
Mais est-ce bien indispensable?

Après quelques minutes de réflexion, je pense que c'est dommage de ne pas connaître l'état de la machine.
Dans plusieurs de mes programmes je m'en sers pour superviser le fonctionnement des dites machines.

et non, en effet je n'ai pas réussi... c'est le prix de la simplicité d'emploi :confused:

Il reste possible que chaque état mette à jour une variable indiquant l'état à son lancement, mais c'est à gérer au cas par cas par l'utilisateur

Il faut que je rajoute la possibilité de re-rentrer dans l'état courant depuis celui-ci. Je m'étais bien bien pris la tête pour que lorsqu'on le fait, cela ne soit pas considéré comme un changement d'état... et je me rends compte à l'usage qu'il y a plein de fois ou j'en ai besoin :smiley: En particulier pour remettre à zéro le compteur de passages dans l'état ou les temporisations.

Mais ça c'est facile.

ben si

Puisque chaque état est associé à une fonction, il suffit d'ajouter une méthode qui expose la valeur de _pState (ou de rendre sa valeur publique).

Et après, depuis l'extérieur de la machine à état, vous pouvez coder :

if (yasm.pState == ledOn)
{
...
}

Ce qui revient bien à savoir dans quel état est la machine.

ha oui

ça rajoute un if externe à la lib, mais c'est vrai que vu comme ça, c'est simple

merci

voila, je viens de rajouter ça :

bool isInState(void (*pstate)()){return pstate==YASM::_pState;};

qui s'emploie donc ainsi :

if (led.isInState(ledOn))
{
...
}

En fait je suis en train d'expérimenter ton truc dans le cadre d'une gestion de menu.
C'est vrai que c'est très pratique.

Du coup comme je suis en train de coder ma propre classe pour mes besoins, je vois qques points d'amélio à ta lib :

y a un gros mélange de types autour de _runCount : tout mettre en unsigned long (et non en double ou unsigned int).

Tes méthodes qui permettent d'accéder en lecture seule à la valeur d'une variable privée, seraient plus précisément écrites avec un const, et même un inline. par exemple

inline bool isFirstRun() const {return YASM::_isFirstRun;}; // <== ce tout dernier point virgule est en trop

Le inline n'est pas très important. Il indique au compilateur d'insérer directement le code de la fonction à l'endroit où elle est appelée, plutôt que de passer par un appel de fonction (=débranchement vers l'adresse de la fonction). Et souvent (je ne sais pas si c'est le cas ici) le compilo s'en tape car il suit ses propres règles d'optimisation.
Mais le const indique clairement que la méthode ne modifie pas les propriétés de l'objet, et qu'on est donc sur une simple lecture. Cela rend le header plus lisible et compréhensible. On fait mieux la différence entre les méthodes qui agissent sur l'objet et celles qui sont neutres.

Le constructeur n'initialise pas toutes les propriétés.
Ca spa bien :slight_smile:
Par exemple : d'après la loi de Murphy, le néophyte moyen qui va utiliser ta lib, va appeler la méthode resume() en tout premier. Résultat : pState va prendre la valeur de pLastState (non initialisée), et au premier run() l'arduino va se figer => tcho elle est trop nulle cette lib elle marche pô !

C'est dommage que ton pLastState soit écrasé dès la première exécution du nouvel état.
Tu devrais organiser ton code un peu différemment pour que la valeur de pLastState soit conservée jusqu'au prochain changement d'état.

en gros déplacer le pLastState = pState dans ta méthode next
et modifier ton run() ainsi

...
//now machine is running
 void (*toto)();
 toto=_pState; //save the current state
 _pState(); //run the current state
 
 //if pState is the same now than before running it means we did not got state 
 //change request during _pState() execution so next time we again will run the 
 //same state, so we clear the flag and increment runs counter
 if (_pState==toto) {
 _isFirstRun = false; 
 _runCount++;      //  <= bizarre l'incrément à cet endroit. Je l'aurais vu en dehors du if.
 }

Avec un pLastState persistant, resume() n'est plus seulement une méthode à appeler après un stop() pour relancer la machine.
Elle peut aussi être appelée depuis n'importe quel état pour revenir à l'état précédent, ce qui peut être bien pratique lorsqu'il y a plusieurs états précédents possibles.
Au passage, il me semble que ton stop() peut être réduit à un simple next(yasm::nop)

Autres petits points :

Puisque tu intègre un compteur d'exécution dans ta classe, c'est un peu luxueux d'avoir en plus un booléen indiquant la première exécution.
J'aurais plutôt mis un truc du genre :

  private :
    inline bool _isFirstRun() const {return _runCount == 0;} //en supposant que _runCount est incrémenté après exécution

ça bouffe un octet de ram en moins par instance, y a pas de petite économie. :slight_smile:

Ta syntaxe YASM:: dans le header me perturbe un peu, mais je ne suis pas certain que mes propres habitudes soient académiques.
J'ai plutôt l'habitude d'utiliser Nom_de_classe:: uniquement pour les méthodes ou propriétés statiques.
Par exemple là tu as la méthode nop() déclarée en static, c'est-à-dire qu'elle est commune à l'ensemble des instances de la classe. Encore que, vu que c'est une fonction vide, je ne vois pas trop quelle différence cela fait.
Pour le reste, par exemple YASM::_pState :
_pState est une propriété de l'objet (objet = une instance de la classe).
Si j'ai deux machines à état dans mon programme, j'aurai bien deux _pState distincts.
Quand je veux accéder à _pState depuis une méthode de la classe, point n'est besoin de préfixer par YASM::
Dans le .h tu as mis du YASM:: partout, et dans le .cpp nulle part. Y a une raison particulière à cela?
Perso lorsque je veux essayer de coder proprement, je mets plutôt du this-> partout. Comme ça on fait bien la différence entre une variable locale et une propriété de l'objet.
Et le this-> indique bien que l'on pointe vers la ram associée à l'instance de classe que l'on est en train de manipuler.

hu ! en voila de bien bonnes remarques :slight_smile:

bricoleau:
y a un gros mélange de types autour de _runCount : tout mettre en unsigned long (et non en double ou unsigned int).

bien vu ! mais pourquoi du unsigned long ? juste parceque c'est le type le plus large ? du double ou du unsigned int suffit largement à mon avis et ça bouffera (un peu) moins de ram... qui peut bien avoir besoin de rester dans un état assez longtemps pour remplir un unsigned long ? (c'est vrai que ça c'est pas à moi de le décider... et que ça risque là aussi de conduire à un "elle marche pas cette lib")

Le constructeur n'initialise pas toutes les propriétés.
Ca spa bien :slight_smile:
Par exemple : d'après la loi de Murphy, le néophyte moyen qui va utiliser ta lib, va appeler la méthode resume() en tout premier. Résultat : pState va prendre la valeur de pLastState (non initialisée), et au premier run() l'arduino va se figer => tcho elle est trop nulle cette lib elle marche pô !

arghhh j'avais raté ça :stuck_out_tongue:
En fait il faudrait que par défaut _pLastState soit initialisé sur nop() mais je sais pas si c'est possible ? Cela dit que ça soit fait dans le constructeur ou à la compilation, ça ne va rien changer au final en taille du code machine, non ?

 //if pState is the same now than before running it means we did not got state 

//change request during _pState() execution so next time we again will run the
//same state, so we clear the flag and increment runs counter
if (_pState==toto) {
_isFirstRun = false;
_runCount++;      //  <= bizarre l'incrément à cet endroit. Je l'aurais vu en dehors du if.
}

l'incrément est là car sinon si il y a un appel à next() depuis l'état en cours d’exécution, runCount serait incrémenté une fois de trop, puisqu'il serait mis à zéro dans next(), puis incrémenté ici ensuite à la fin de run() de l'état en cours, et donc au premier run() de l'état suivant, la valeur serait déjà à 1 au lieu de 0.

C'est dommage que ton pLastState soit écrasé dès la première exécution du nouvel état.
Tu devrais organiser ton code un peu différemment pour que la valeur de pLastState soit conservée jusqu'au prochain changement d'état.
(...)
Avec un pLastState persistant, resume() n'est plus seulement une méthode à appeler après un stop() pour relancer la machine.
Elle peut aussi être appelée depuis n'importe quel état pour revenir à l'état précédent, ce qui peut être bien pratique lorsqu'il y a plusieurs états précédents possibles.

oui... mais alors dans ce cas, pourquoi ne pouvoir remonter que d'un seul cran en arrière ? ça n'a pas vraiment de sens, il faudrait presque créer un tableau (dynamique, sinon là encore sur quelle base en figer la taille ??) de pointeurs qui stockerait tous les états précédents, pour pouvoir remonter l'historique par appels successifs à resume(), qu'il faudrait alors appeler back() ou previous()... et garder une resume() qui fait seulement ce qu'elle fait maintenant. Mais là je crains fort de sortir avec fracas du cadre de "légèreté" de la lib voulu au départ. Ou alors c'est une option à activer avec un #define

Mais c'est certain que pour le cas particulier du menu, ce serait extrèmement pratique...

Au passage, il me semble que ton stop() peut être réduit à un simple next(yasm::nop)

Sans ta modif de run(), non. Avec ta modif, il me semble que oui, en effet, car on ne se base plus sur _pLastState pour vérifier si il faut dévalider le flag _isFirstRun donc ça doit fonctionner dans tous les cas.

bricoleau:
Autres petits points :

Puisque tu intègre un compteur d'exécution dans ta classe, c'est un peu luxueux d'avoir en plus un booléen indiquant la première exécution.
J'aurais plutôt mis un truc du genre :

  private :

inline bool _isFirstRun() const {return _runCount == 0;} //en supposant que _runCount est incrémenté après exécution



ça bouffe un octet de ram en moins par instance, y a pas de petite économie. :)

ha oui, en effet, bien vu là aussi !! oui _runCount est incrémenté à la fin de run() donc ça doit fonctionner. On doit même pouvoir faire [i]return !_runCount;[/i] directement mais c'est peut-être un peu sale, ça fait un cast sauvage.

pourquoi "const" employé ici ?
et il me semble que ce serait plutôt en public, ça remplace la méthode actuelle directement.

Par contre ça ne fonctionne pas dans le cas où dans un état il y a un appel à next(autreEtat) PUIS un appel à isFirstRun() à la suite, car elle retournera alors true même si ce n'est pas le cas puisque tout a été remis à zéro dans next(). Mais maintenant que tu m'y fais réfléchir, ça ne fonctionne pas non plus avec mon système actuel, c'est exactement le même problème... Ça pue le bug merdique dans certains cas, ça, mais je ne vois vraiment pas comment résoudre le problème...

Ta syntaxe YASM:: dans le header me perturbe un peu, mais je ne suis pas certain que mes propres habitudes soient académiques.
J'ai plutôt l'habitude d'utiliser Nom_de_classe:: uniquement pour les méthodes ou propriétés statiques.
Par exemple là tu as la méthode nop() déclarée en static, c'est-à-dire qu'elle est commune à l'ensemble des instances de la classe. Encore que, vu que c'est une fonction vide, je ne vois pas trop quelle différence cela fait.

Je ne sais pas au juste, mais si elle n'est pas static, ça ne compile/fonctionne pas je ne sais plus

Pour le reste, par exemple YASM::_pState :
_pState est une propriété de l'objet (objet = une instance de la classe).
Si j'ai deux machines à état dans mon programme, j'aurai bien deux _pState distincts.
Quand je veux accéder à _pState depuis une méthode de la classe, point n'est besoin de préfixer par YASM::
Dans le .h tu as mis du YASM:: partout, et dans le .cpp nulle part. Y a une raison particulière à cela?

oui, mon incompétence en la matière :stuck_out_tongue:
je t'avoue que l'écriture d'une classe était pour moi une première, et que je me suis inspiré des bibliothèques existantes sur mon système pour voir "comment qu'on fait", et de tutos sur le net. Du coup si les notations ne sont pas bien académiques c'est presque normal, je dirais. Pour être tout à fait honnête, j'ai "trafiqué" la syntaxe un peu au petit bonheur la chance jusqu’à avoir un truc qui compile...

Perso lorsque je veux essayer de coder proprement, je mets plutôt du this-> partout. Comme ça on fait bien la différence entre une variable locale et une propriété de l'objet.
Et le this-> indique bien que l'on pointe vers la ram associée à l'instance de classe que l'on est en train de manipuler.

Pour se prendre la tête en jonglant avec this-> regarde la classe BTN dans btn.h/.cpp .... ça m'a un peu piqué les neurones pour que ça fonctionne, ça

Toutes ces modifs suggérées (à part la persistance de _pLastState qui implique à mon avis trop de choses, ou alors faut encore qu'on discute :P) étant fort bien fondées, pourquoi ne pas créer un fork du projet sur github, comme ça tu peux faire les corrections et m'envoyer un pull request ? De la sorte ta paternité des améliorations sera connue dans l'historique :slight_smile:

Rapidement

unsigned long => révise tes types de données :slight_smile:
Dans notre ecosystème, double est un flottant sur 4 octets. Cela ne convient pas du tout pour stocker un compteur.
unisgned int va de 0 à 65535. Si tu evalues ta machine 1000 fois par seconde, tu vas faire le tour du compteur en à peine une minute.
=> unsigned long obligatoire.
Quel que soit le type, une fois déterminé, il faut avoir partout le même pour manipuler la donnée sans cast.
perso je préfère utiliser les types normalisés uint32_t etc. au lieu de byte int et long.

Le constructeur est une fonction exécutée lors de l'instanciation de chaque objet, juste après l'allocation de ram nécessaire aux données de l'objet.
Si celui-ci est déclaré comme une variable globale, l'exécution intervient immédiatement, avant même d'entrer dans setup() (et même init()).
Il n'est pas utile de mettre le corps du constructeur dans le header. Mieux dans le .cpp

Je pense que tu n'as pas besoin de nop()
pState est un pointeur, tu pourrais lui assigner la valeur NULL et tester cette valeur dans le run() avant appel, plutôt que de passer par une fonction vide.

Je maintiens qu'en organisant le code un peut différemment il ne devrait y avoir besoin que d'une seule ligne _runCount++ (sisi :slight_smile: )

pour la fonction retour à l'état précédent : je te rejoins tout à fait sur la complexité et l'inutilité de proposer un retour arrière sur plusieurs niveaux. Je dis juste qu'il est possible d'optimiser l'usage des données actuellement disponibles, pour proposer un service supplémentaire = le retour arrière sur un seul niveau.

fork / merge request github mwé pourquoi pas mais pas tout de suite :wink:

Tiens encore un suggestion dans la boîte à idées :

Définir les opérateurs = et ==
Ca coûte pas cher en lignes dans le header, et permet ensuite un coding qui peut être encore plus simple, par exemple

YASM machine;
...
machine = ledOn; //pour changer d'état
...
if (machine == ledOn) //pour tester l'état

Euh j'avance aussi dans mes rélexions.
Au final, si je devais construire une telle lib, je pense que j'adapterais un peu la sémantique.

L'objet serait plutôt un état qu'une machine, avec des primitives du style (en francais)

YASM etat;// fonction initiale à NULL lors de la construction
...
etat = ledOn; // pour changer d'état
..
if (etat == ledOn) // pour tester l'état
..
etat.actualiser(); // au lieu de run
etat.duree(); //retourne la durée écoulée en ms dans l'état actuel
etat.nbActualisations(); // retourne le nombre d'actualisations effectuées dans l'étata courant
etat.retour(); // revient à l'état précédent sur un seul niveau (un nouveau retour revient alors à l'état courant, en flip flop)
etat.premier(); // retourne un booléen qui indique qu'on est sur la première actualisation de l'état courant.

Et là on est pas mal il me semble pour faire facilement une machine à états.

bricoleau:
Rapidement

unsigned long => révise tes types de données :slight_smile:
Dans notre ecosystème, double est un flottant sur 4 octets. Cela ne convient pas du tout pour stocker un compteur.
unisgned int va de 0 à 65535. Si tu evalues ta machine 1000 fois par seconde, tu vas faire le tour du compteur en à peine une minute.
=> unsigned long obligatoire.

Ben en fait dans mon idée, l'utilisation du compteur n'avait de sens que pour des petites valeurs. En fait pour moi l’intérêt de ce compteur c'est de pouvoir faire l'équivalent d'une boucle for mais qui s'exécute à chaque exécution de l'état, pour pas que ça soit bloquant. Et je ne voyais pas le besoin d'avoir un truc qui fonctionne au delà de 65535, en fait :stuck_out_tongue:
D'autant qu'avec le flag indépendant pour _isFirstRun, même si le compteur déborde, bah comme c'est dans les cas où je ne m'en sert pas...

Mais bon, c'est sûr que vu que le compteur est là, il vaut mieux qu'il fonctionne vraiment. Mais ça bouffe de la ram pour probablement rien, au final...

Quel que soit le type, une fois déterminé, il faut avoir partout le même pour manipuler la donnée sans cast.
perso je préfère utiliser les types normalisés uint32_t etc. au lieu de byte int et long.

ça c'est certain. c'était pas voulu le changement de type, j'ai juste chié dans la colle

Je pense que tu n'as pas besoin de nop()
pState est un pointeur, tu pourrais lui assigner la valeur NULL et tester cette valeur dans le run() avant appel, plutôt que de passer par une fonction vide.

j'y avais pensé, mais je trouvais ça plus cohérent ainsi, et en plus il me semblait que dans le code machine généré il y avait des instructions en moins (un test de moins)

Je maintiens qu'en organisant le code un peut différemment il ne devrait y avoir besoin que d'une seule ligne _runCount++ (sisi :slight_smile: )

hu ? ça y est il va encore m'empécher de dormir comme hier....

pour la fonction retour à l'état précédent : je te rejoins tout à fait sur la complexité et l'inutilité de proposer un retour arrière sur plusieurs niveaux. Je dis juste qu'il est possible d'optimiser l'usage des données actuellement disponibles, pour proposer un service supplémentaire = le retour arrière sur un seul niveau.

Ben l'inutilité... moi je trouverais pas ça inutile. Par contre voilà le bordel...
Et en faisant un tableau dynamique, pour fragmenter la pile et que tout parte en c** ça serait parfait :stuck_out_tongue:

fork / merge request github mwé pourquoi pas mais pas tout de suite :wink:

Tiens encore un suggestion dans la boîte à idées :

Définir les opérateurs = et ==
Ca coûte pas cher en lignes dans le header, et permet ensuite un coding qui peut être encore plus simple, par exemple

ben pour les opérateurs, je suis pas fan. certes c'est plus simple, mais je trouve que c'est moins clair quand on code, il me semble plus clair d'appeler explicitement next()... c'est peut-être juste le temps que je me fasse à l'idée :stuck_out_tongue:

bricoleau:
Euh j'avance aussi dans mes rélexions.
Au final, si je devais construire une telle lib, je pense que j'adapterais un peu la sémantique.

L'objet serait plutôt un état qu'une machine, avec des primitives du style (en francais)

YASM etat;// fonction initiale à NULL lors de la construction

...
etat = ledOn; // pour changer d'état
..
if (etat == ledOn) // pour tester l'état
..
etat.actualiser(); // au lieu de run
etat.duree(); //retourne la durée écoulée en ms dans l'état actuel
etat.nbActualisations(); // retourne le nombre d'actualisations effectuées dans l'étata courant
etat.retour(); // revient à l'état précédent sur un seul niveau (un nouveau retour revient alors à l'état courant, en flip flop)
etat.premier(); // retourne un booléen qui indique qu'on est sur la première actualisation de l'état courant.




Et là on est pas mal il me semble pour faire facilement une machine à états.

Ben aux opérateurs près, tu arrives exactement à la même chose que moi, non ?

Pour actualiser au lieu d'exécuter... je trouve executer plus explicite.

En revanche si l'objet est un état, tu risques d'avoir des soucis en cas de plusieurs machines qui s'executent en //, non ? comment dans ce cas traiter les appels (depuis l'extérieur de la machine) à stop() ou resume() qui concernent la machine, pas un état particulier

ok ok
les goûts et les couleurs... :slight_smile:

Désolé d'avoir pollué un peu ton topic, mais je trouve la réflexion très intéressante.

et comme je m'en voudrais de te gâcher ton sommeil, il me semble que la solution à un seul _runCount++ serait juste d'ajouter un OU logique dans la méthode run()

  if (_pState==_pLastState || _pState==YASM::nop)
  {
    _isFirstRun = false; 
    _runCount++; 
  }

et ainsi on a : void stop() {_pState = YASM::nop;}