YASM - Yet Another State Machine library

Bonjour à tous

Voila, pour reprendre un ancien projet j'ai eu besoin de formater un peu clairement des machines à état. Du coup j'ai cherché des librairies, et j'ai rien trouvé qui m'allait bien, alors j'en ai fait une autre. Basée sur le principe des pointeurs de fonction.

C'est très basique mais c'est le but recherché : un truc simple et d'usage clair. Il me manque encore une ou deux fonctions de minuterie, mais sinon c'est fonctionnel.

Si vous voulez y jeter un oeil, si vous avez des idées d'améliorations...

Je dois encore rajouter des commentaires dans le code et des exemples plus poussés.

EDIT : version à jour sur github : https://github.com/bricofoy/yasmhttps://github.com/bricofoy/yasm

EDIT 2 : Librairie maintenant disponible directement dans le Library Manager, bien plus simple pour l'installation !

Désolé, j'ai répondu dans l'autre rubrique.

pas de soucis. En fait j'ai ouvert ce sujet après en me disant que ça méritait sans doute un sujet à part entière.

du coup je vais continuer la discussion ici

bilbo83:
Bonjour,

Je viens de regarder le code de ta classe YASM.
C'est simple et clair.
J'utilise très souvent les MAE (FSM en Anglais) de manière artisanale avec des "switch case" pour les états et "millis()" pour gérer le temps.
Je ne sais pas si utiliser les timers apportera de la clarté au code.
Par contre un tableau de pointeurs sur des propriétés, permettrait de diminuer, voire éliminer les variables globales que l'on utilise pour gérer les transitions d'état.
Je ne sais pas si je me fais bien comprendre; les variables "i" et "j" de ton exemple.

en fait les variables i et j de l'exemple dans la réalité avec l'usage que j'en ai ça n'arrive quasi jamais, les changement d'état se font soit sur des conditions extérieures (valeurs lues sur des entrées) soit sur expirations de tempos, en général. d'où la méthode elapsed() qui me permet de faire des choses comme

if Machine.elapsed(tempo) Machine.next(etat_suivant);

un tableau de pointeurs ça fait il me semble des machines du type "Automaton" (une librairie existante dans le playground) qui est super complete et bien faite et tout... mais d'une complexité qui en rebutera plus d'un, moi le premier...

Alors en fait, je viens de mettre le doigt sur ce qui me chiffonne avec mon système : je n'ai aucun moyen de savoir depuis l'extérieur de la machine dans quel état elle est. Je pourrais faire une méthode qui me retourne le pointeur sur l'état en cours d'exé, mais... et après ? je me retrouve de toutes manières à faire à un moment un tableau qui associe tel pointeur à tel état et je retombe dans la complexité que justement je voulais éviter !
Ou alors il faut que les états au lieu d'être une simple fonction void soient eux-même des instanciations d'une classe avec dedans la fonction d'état et une fonction qui retourne une valeur associée à l'état... mais bordel, là aussi je retrouve une complexité que je voulais éviter !! enfer !!

bon, dans l'immédiat ça va faire mais... manifestement j'ai un truc pas au point :confused:

Oui, d'où mon premier post concernant les propriétés.
J'avais moi aussi réfléchi à automatiser mes MAE, mais j'ai rapidement abandonné car je me retrouvais avec quelque chose de plus complexe à utiliser que la méthode artisanale que je cite dans mon premier post.
Mais bon, courage, persévères, je sent que tu va avoir les bonnes idées.

C'est chouette, cette petite bibliothèque! Ca répond en partie à des interrogations que j'avais ces jours-ci! J'espère que je ne vais pas vouloir intégrer quelque chose de similaire à mon projet en cours, autrement il ne sera jamais prêt à être mis sous le sapin samedi soir!! :smiley:

Pour les états, tu voudrais faire quoi? Simplement savoir dans quel état est la machine? Ou bien avoir une possible lecture de l'état ET de la fonction en cours?
Dans le second cas, ça dépasse mes compétences, par contre dans le premier il serait en effet assez facile (et pas trop complexe, je crois), de définir une fonction getState() qui renvoit un int ou un char, et de définir dans chaque fonction un état correspondant.

#define STATE_IDLE            0
#define STATE_STOPPED    1
#define STATE_RUNNING     2
//etc.
char YASM::_state = STATE_IDLE;

char YASM::getState(){
    return _state;
}

void YASM::run(){
    _state = STATE_RUNNING;
    //code methode
    return _state;
}

void YASM::stop(){
    _state = STATE_STOPPED;
    //code méthode
    return _state;
}

//etc.

On peut même imaginer que chacune des méthodes renvoie son état propre, et éventuellement dise si elle a rencontré un problème...

oui... mais non ! en effet on peut faire une liste d'état avec chacun une valeur, etc... mais dans ce cas on perd complètement la simplicité du système à pointeurs de fonctions ! finalement on se retrouve avec le système habituel avec une variable qui contient la valeur de l'état, et une seule énorme fonction qui contient toute la machine à états avec un gros switch case pour différencier les états. C'est ce que je faisais jusqu'a maintenant. Ça fonctionne, mais c'est moche, lourd, source de bugs vu que tout est dans la me fonction on a vite fait de mélanger des variables, etc etc

ici l'idée c'est d'avoir un état = une fonction. Donc lire létat ou lire la fonction en cours c'est exactement la même chose.
Il faudrait donc arriver à partir de la valeur du pointeur d'exécution à avoir un mécanisme qui permet de donner le nom de la fonction cible du pointeur... et ça je sais pas faire, ça dépasse de loin mes maigres rudiments de C++ (si seulement c'est faisable ? )
Ou alors il faut que chaque état se nomme dans une variable globale à la première exécution. c'est faisable mais là aussi je perds une partie de la simplicité du truc avec des fonctions d'état qui doivent nécéssairement faire des trucs en plus.

Je me suis souvent demandé si on pouvait obtenir d'une manière ou d'une autre le nom d'une fonction depuis le programme. Et mes connaissances sont aussi trop limitées pour avoir la réponse. Cela dit je crois que non. D'abord parce qu'à chaque fois que j'ai voulu faire ça, l'idée sous-jacente pouvait se résumer justement à un pointeur de fonction. Mais je ne le savais pas faut de savoir ce qu'était un pointeur, et encore mieux un pointeur de foncction. Maintenant que je le sais, je me dis que connaître le nom d'une fonction n'a que peu d'intérêt, à part dans un cas bien particulier comme le tiens.
Ensuite, comme ce nom de fonction devrait être accessible depuis le programme, et que précisément, le programme est compilé (et donc que tout ce que nous avons défini avec des mots est transformé en assembleur, donc en instructions et adresses mémoire), je ne pense qu'il serait accessible...

Du coup, à part la solution que j'ai proposé plus haut, je ne vois pas. Elle ne me parait pas si compliquée, après tout rien n'empêche de séparer la définition de ces états dans un fichier et/ou une classe dédié, et de simplement les oublier.

Je me dis que tu pourrais passer le nom de ta fonction en toutes lettres en même temps que le pointeur dans ta fonction next(), mais là aussi ça complexifie un peu le système.

Cela dit, est-ce que complexifier est vraiment un problème? Je veux dire, ma maigre expérience m'a amené à lire pas mal de choses ces derniers temps, en ce moment j'ai tendance à faire des classes pour un peu tout, parce que je commence à comprendre le mécanisme. Quoi que mes projets passent de 1 fichier à rapidement une dizaine, voire plus, j'ai vraiment le sentiment que l'ensemble va dans le sens d'une simplification considérable. Justement parce que chaque fonction (disons chaque classe) se cantonne à ce qu'elle doit faire, peut être testée indépendamment, et du coup le code principal est limpide et facilement débogable...

Quel serait l'intérêt pratique de savoir quel fonction est en cours sur ton projet? Quel est le contexte?

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

pour l'utilisation que j'ai pour le moment de ma lib, c'est là :wink:

en fait savoir où on est dans le programme c'est surtout pour faire du débug... mais j'ai résolu partiellement le problème en rajoutant un lcd :stuck_out_tongue:

Le LCD, c'est une chouette solution!
Je dois en recevoir deux pour un projet que j'ai démarré, je pense que l'un des deux sera dévolu à faire du débugage sur des projets où il est délicat d'utiliser la liaison série pour le faire. :slight_smile:

Je n'ai pas eu le courage de lire les neufs page du sujet en lien, mais ça semble être bien complet. Et visiblement un projet de longue haleine. C'est quelque chose que tu as commercialisé?

Hello,

Pour rester dans la simplicité, on pourrait ajouter deux méthode SetStateName et GetStateName à la classe de base.

  • SetStateName stocke, dans un champ privé de la classe, la valeur passée en argument.
  • GetStateName renvoie cette valeur stockée.

La première instruction d'une fonction (S1 ou S2 dans l'exemple donné) serait d'appeler SetStateName.
(Ce qui n'est pas pire que de faire un envoi sur la ligne série ;))

L'avantage de la solution est qu'il n'y a pas de contrainte. Une application qui n'a pas besoin de savoir dans quel état on se trouve n'appelle pas les fonctions, tout simplement.

Petites améliorations possibles :

  1. Mofidier Next() afin qu'il réinitalise StateName. Ainsi, on ne risque pas de garder le nom d'un état alors que l'on change d'état, si ce dernier oublie d'appeler SetStateName

  2. Prévoir deux pointeurs de fonction a appeler lors du Next et lors du SetStateName. Cela permet à une autre partie du programme d'être prévenue d'un changement...

Cela ne coûterait vraiment pas cher à implémenter, à mon humble avis.
:slight_smile:

Coyotte

troisiemetype:
Le LCD, c'est une chouette solution!
Je dois en recevoir deux pour un projet que j'ai démarré, je pense que l'un des deux sera dévolu à faire du débugage sur des projets où il est délicat d'utiliser la liaison série pour le faire. :slight_smile:

Je n'ai pas eu le courage de lire les neufs page du sujet en lien, mais ça semble être bien complet. Et visiblement un projet de longue haleine. C'est quelque chose que tu as commercialisé?

J'utilise un lcd avec un module de conversion I2C, comme ça je perds pas trop de broches.

Oui, c'est un truc que je commercialise, pour la troisième fois pour être précis, voila le succès commercial ! :smiley:
Là ça y est ma dernière carte est posée après 2 nuits blanches à coder, et pour la première fois depuis le début du projet ça fonctionné du premier coup, j'ai pas eu à sortir l'ordi chez le client ! incroyable ! Surtout alors que le soft est une refonte complète, je n'ai gardé de l'ancienne version que les definitions.

coyotte:
Hello,

Pour rester dans la simplicité, on pourrait ajouter deux méthode SetStateName et GetStateName à la classe de base.

  • SetStateName stocke, dans un champ privé de la classe, la valeur passée en argument.
  • GetStateName renvoie cette valeur stockée.

La première instruction d'une fonction (S1 ou S2 dans l'exemple donné) serait d'appeler SetStateName.
(Ce qui n'est pas pire que de faire un envoi sur la ligne série ;))

L'avantage de la solution est qu'il n'y a pas de contrainte. Une application qui n'a pas besoin de savoir dans quel état on se trouve n'appelle pas les fonctions, tout simplement.

Petites améliorations possibles :

  1. Mofidier Next() afin qu'il réinitalise StateName. Ainsi, on ne risque pas de garder le nom d'un état alors que l'on change d'état, si ce dernier oublie d'appeler SetStateName

  2. Prévoir deux pointeurs de fonction a appeler lors du Next et lors du SetStateName. Cela permet à une autre partie du programme d'être prévenue d'un changement...

Cela ne coûterait vraiment pas cher à implémenter, à mon humble avis.
:slight_smile:

Coyotte

Certes, merci pour ces avis :slight_smile: j'aurais juste voulu que ce soit automatique, mais en fait ça finira sans doute comme tu dis.
Depuis il y a eu la fin du développement du reste du soft, et du coup ça a donné lieu à des améliorations et des corrections de bugs, en particulier une feinte dans les fonctions next() et run() pour que isFirstTime soit positionné correctement même si next() est appelé depuis la fonction en cours d'exé dans run() (une bonne prise de tête à pas comprendre ce qui ne marchait pas, ce truc... vers 6h du matin c'était moyen...)

et j'ai aussi rajouté la fonction periodic() pour lancer un truc à intervalles réguliers (en l’occurrence ici le rafraîchissement du LCD)
Il faut toutefois que j'améliore cette fonction car là elle ne fonctionne qu'une fois par état (avec une seule période, je veux dire)

Nouvelle version jointe.

Par contre j'ai des problèmes avec les pointeurs et la version d'arduino... mon code ne fonctionne qu'avec les versions 1.6.x, les versions précédentes ne compilent pas : ça fait des erreurs (que je n'ai pas notées) avec les pointeurs de fonctions.
Et parallèlement la librairie LiquidCrystal_I2C elle ne fonctionne que jusqu’à la 1.6.0, pas plus loin... ouf y'a UNE version qui fonctionne !!

YASM.zip (1.96 KB)

HS:
Je l'avais bien dit que ça me travaillerait pour mon projet en cours, ta machine et tes pointeurs de fonction.
Bingo, ça n'a pas raté, je viens d'ajouter la possibilité d'attacher une fonction à ma bibliothèque qui gère un encodeur rotatif. Comme j'ai une boucle principale qui peut changer de contexte (c'est un instrument de musique: il y a un simple clavier, un step sequencer, etc. le tout commandé alternativement depuis toujours la même boucle), ca devient magique! quand je change de contexte il me suffit de changer la fonction attachée à l'encodeur et roule! :slight_smile:

La bibliothèque est ici.

elle me plait bien ta lib :slight_smile: je vais peut-être m'en servir pour un prochain projet : une régulation de chauffage avec départ radiateur mélangé, capteur solaire et cuisinière à bois à bouilleur. Mais j'ai peur qu'avec mon stock de machines à états (il va y en avoir au minimum 4 qui s'exécutent ensemble) la fréquence de rafraichissement soit trop faible pour lire correctement sans utiliser d'interruptions... je ferai des essais :slight_smile:

Merci!!

Bon, peu après avoir posté ce message, je me suis rendu compte que je ne peut y attacher que des fonctions statiques. Donc pas de méthodes membres de classes. Dommage, c'était ce que j'aurais aimé.
Je pense que j'essaierai d'ajouter une mise à jour de la lecture via une interruption (sur PCINT0, 1 et 2). Mais il faudra que je vois comment récupéré les caractéristiques des ports dans les fichiers Arduino, pour ne pas avoir à tout réécrire. Et également que je vois comment partager cette interruption entre l'encodeur et la bibliothèque bouton que j'ai écris en même temps ces derniers jours.

Ce sera pour la rentrée, probablement, ou pendant les vacances si je me sens désœuvré! :wink:

si tu peux mais il faut déclarer ta méthode avec "static". Regarde ma méthode "nop()" dans la classe yasm. bon celà dit c'est dans la me$e classe, peut-être qu'avec une classe différente ça ne fonctionne pas, je ne sais pas.

Oui, à ce que j'ai compris des pointeurs sur fonction, si la méthode est statique on peut la pointer. Cela dit dans mon cas cela présente un inconvénient: la méthode deviendra accessible depuis l'extérieur de la classe, or si elle est appelée sans que la méthode begin() ait été appelée avant, j'aurais droit à un beau plantage.
Ce qui montre bien que les principes d'encapsulation ne sont pas des vaines choses. :slight_smile:

J'ai ouvert un fil de discussion sur ce problème, . Les réponses me dépassent un peu, mais il semblerait que l'héritage soit une solution plus propre au problème. A suivre.

Nouvelle version, avec une bonne grosse vilaine correction d'un sale bug qui m'a pris la nuit et la journée à trouver !

Maintenant même en cas de changement d'état demandé par l'état lui-même au premier passage dedans, l'état de isFirstTime et de runCount sont corrects.

Il manque juste deux fonctions pour détecter un front montant et un front descendant et ce sera la version 1.0 :slight_smile:

nouvelle version 0.9.0

YASM_090.zip (17.6 KB)

un exemple de gestion de bouton pour avoir les infos "click" et "longclick" et piloter un menu sur lcd avec ma librairie yasm

#include <OneWire.h>
#include <DallasTemperature.h>
#include <Wire.h> 
#include <LiquidCrystal_I2C.h>
#include <EEPROM.h>
#include <yasm.h>

// Data wire is plugged into port 2 on the Arduino
#define ONE_WIRE_BUS 9
#define TEMPERATURE_PRECISION 9

#define EEPROM_BASE_ADR 0
#define pin_sw 10
#define NBR_SONDE 9 //nombre de sondes à rechercher et stocker en eeprom
uint8_t numSonde=1;

#define BTN_NONE 0
#define BTN_CLICK 1
#define BTN_LONGCLICK 2

// Setup a oneWire instance to communicate with any OneWire devices (not just Maxim/Dallas temperature ICs)
OneWire oneWire(ONE_WIRE_BUS);

// Pass our oneWire reference to Dallas Temperature.
DallasTemperature sensors(&oneWire);

// arrays to hold device addresses
DeviceAddress adrSonde;

LiquidCrystal_I2C lcd(0x20, 4, 5, 6, 0, 1, 2, 3, 7, NEGATIVE);

YASM menu;
YASM btn;

void setup(void)
{
  // start serial port
  Serial.begin(9600);
  
  pinMode(pin_sw,INPUT_PULLUP);

  // Start up the library
  sensors.begin();
  lcd.begin(4,20);
  lcd.backlight();
  lcd.clear();
  
  menu.next(menu_start);
  btn.next(btn_wait);


}

uint8_t flagbtn=0;
uint8_t btnstate=BTN_NONE;

void loop(void)
{
	flagbtn = !digitalRead(pin_sw);
	menu.run();
	btn.run();
	
}

/////////////button state machine///////////////

void btn_wait()
{
	if(btn.elapsed(100)) if(flagbtn) btn.next(btn_debounce);
}

void btn_debounce()
{
	if(!flagbtn) btn.next(btn_wait);
	if(btn.elapsed(5)) btn.next(btn_check);
}

void btn_check()
{
	if(btn.elapsed(1E3)) { 
		btn.next(btn_longpress); 
		btnstate = BTN_LONGCLICK;
		return;
	}
	if(!flagbtn) {
		btn.next(btn_wait);
		btnstate = BTN_CLICK;
	}
}

void btn_longpress()
{
	if(!flagbtn) btn.next(btn_wait);
}

///////////menu state machine////////////

void menu_start()
{
	if(menu.isFirstRun) {
		lcd.clear();
		lcd.print("Appuyez pour rechercher sonde ");
		lcd.print(numSonde);
		lcd.print("/");
		lcd.print(NBR_SONDE);
	}
	if(btnstate==BTN_CLICK) {
		btnstate=BTN_NONE;
		menu.next(menu_recherche);
		lcd.clear();
	}	
	if(btnstate==BTN_LONGCLICK) {
		btnstate=BTN_NONE;
		menu.next(menu_affiche);
		numSonde=1;
		//lcd.clear();
	}
	if(numSonde>NBR_SONDE) menu.next(menu_fin);
}

void menu_affiche()
{
	if(menu.isFirstRun) {
		lcd.clear();
		for(uint8_t j=0; j<4;j++)
		{
			lcd.setCursor(0,j);
			lcd.print(numSonde);
			lcd.print("  ");
			EEPROM.get(EEPROM_BASE_ADR + (sizeof(adrSonde)*(numSonde-1)) , adrSonde);
			for (uint8_t i = 0; i < 8; i++)
			{
				// zero pad the address if necessary
				if (adrSonde[i] < 16) lcd.print("0");
				lcd.print(adrSonde[i], HEX);
			}
			numSonde++;
		}
	}
	if(btnstate==BTN_CLICK) {
		btnstate=BTN_NONE;
		menu.next(menu_affiche_next);
		lcd.clear();
	}
}

void menu_affiche_next()
{
		menu.next(menu_affiche);
}

void menu_fin()
{
	if(menu.isFirstRun) {
		lcd.clear();
		lcd.print("Fin d'enregistrement");
	}
}

void menu_recherche()
{
	if(menu.periodic(1.5E3)){
		//sensors.begin();
		lcd.clear();
		sensors.getDeviceCount();
		if (!sensors.getAddress(adrSonde, 0)) lcd.print("erreur sonde");
		else {
			sensors.setResolution(adrSonde, TEMPERATURE_PRECISION);
			lcd.print("sonde ");
			lcd.print(numSonde);
			lcd.print("/");
			lcd.print(NBR_SONDE);
			lcd.print("   ");
			sensors.getTempC(adrSonde);
			lcd.print(sensors.getTempC(adrSonde));
			lcd.setCursor(0,1);
			for (uint8_t i = 0; i < 8; i++){
				// zero pad the address if necessary
				if (adrSonde[i] < 16) lcd.print("0");
				lcd.print(adrSonde[i], HEX);
			}
			lcd.setCursor(0,2);
			lcd.print("Appui court SAVE");
			lcd.setCursor(0,3);
			lcd.print("Appui long SUIVANTE");
		}
	}
	if(btnstate==BTN_CLICK) {
		btnstate=BTN_NONE;
		menu.next(menu_save);
	}
	if(btnstate==BTN_LONGCLICK) {
		btnstate=BTN_NONE;
		menu.next(menu_start);
		numSonde++;
	}
}

void menu_save()
{
	if(menu.isFirstRun){
		lcd.clear();
		lcd.print("enreg. sonde ");
		lcd.print(numSonde);
		lcd.print("/");
		lcd.print(NBR_SONDE);
		lcd.print(" ?");
		lcd.setCursor(0,1);
		lcd.print("long=SAVE, court=RETOUR");
	}
	if(btnstate==BTN_CLICK){
		btnstate=BTN_NONE;
		menu.next(menu_start);
	}	
	if(btnstate==BTN_LONGCLICK){
		btnstate=BTN_NONE;
		EEPROM.put(EEPROM_BASE_ADR + (sizeof(adrSonde)*(numSonde-1)), adrSonde);
		numSonde++;
		menu.next(menu_start);
	}
}

librairie sur github, ça sera plus pratique :