Go Down

Topic: [Partage] YASM, bibliothèque de création de machines à état très simple d'emploi (Read 1 time) previous topic - next topic

bricofoy

YASM ( 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 disponible est disponible en anglais ici : https://github.com/bricofoy/yasm
Je vais la reprendre ici en français.

Installation

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); //declare the led pin as output
    led.next(ledOn); //set the initial state of the led state machine
}



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

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


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); //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
}


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 :
Code: [Select]
/*
   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
}
-tu savais que si tu passe le CD de windows à l'envers, tu entends une chanson satanique ?
-non, mais il y a pire : à l'endroit, ça l'installe !

bricoleau

Tutoriels arduino : http://forum.arduino.cc/index.php?topic=398112.0

bricofoy

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
-tu savais que si tu passe le CD de windows à l'envers, tu entends une chanson satanique ?
-non, mais il y a pire : à l'endroit, ça l'installe !

bricoleau

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  :)

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.
Tutoriels arduino : http://forum.arduino.cc/index.php?topic=398112.0

bricoleau

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.
Tutoriels arduino : http://forum.arduino.cc/index.php?topic=398112.0

bricofoy

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)
-tu savais que si tu passe le CD de windows à l'envers, tu entends une chanson satanique ?
-non, mais il y a pire : à l'endroit, ça l'installe !

bilbo83

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?

bilbo83

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.

bricofoy

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

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 :D En particulier pour remettre à zéro le compteur de passages dans l'état ou les temporisations.

Mais ça c'est facile.

-tu savais que si tu passe le CD de windows à l'envers, tu entends une chanson satanique ?
-non, mais il y a pire : à l'endroit, ça l'installe !

bricoleau

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 :
Code: [Select]
if (yasm.pState == ledOn)
{
...
}


Ce qui revient bien à savoir dans quel état est la machine.
Tutoriels arduino : http://forum.arduino.cc/index.php?topic=398112.0

bricofoy

ha oui

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

merci
-tu savais que si tu passe le CD de windows à l'envers, tu entends une chanson satanique ?
-non, mais il y a pire : à l'endroit, ça l'installe !

bricofoy

voila, je viens de rajouter ça :
bool isInState(void (*pstate)()){return pstate==YASM::_pState;};

qui s'emploie donc ainsi :
Code: [Select]
if (led.isInState(ledOn))
{
...
}
-tu savais que si tu passe le CD de windows à l'envers, tu entends une chanson satanique ?
-non, mais il y a pire : à l'endroit, ça l'installe !

bricoleau

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
Code: [Select]
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  :)
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
Code: [Select]
...
//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)

Tutoriels arduino : http://forum.arduino.cc/index.php?topic=398112.0

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 :
Code: [Select]

  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.  :)


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.

Tutoriels arduino : http://forum.arduino.cc/index.php?topic=398112.0

bricofoy

#14
Feb 15, 2018, 12:58 am Last Edit: Feb 15, 2018, 01:48 am by bricofoy Reason: il manquait pleeein de trucs !!
hu ! en voila de bien bonnes remarques :)


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")

Quote
Le constructeur n'initialise pas toutes les propriétés.
Ca spa bien  :)
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 :P
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 ?
Quote
Code: [Select]

 //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.
Quote
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...
Quote
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.

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 :
Code: [Select]

  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 return !_runCount; 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...

Quote
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
Quote
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 :P
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...


Quote
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 :)

-tu savais que si tu passe le CD de windows à l'envers, tu entends une chanson satanique ?
-non, mais il y a pire : à l'endroit, ça l'installe !

Go Up