Machine à état, ou pas ?

Bonjour
Suite à la lecture du tutoriel de @J-M-L sur les machines à état, je me suis dit que j'allais en faire une : voici le code ( très simple, c'est juste pour découvrir :wink: ) - le code compile bien, testé sur tinkercad :slight_smile:

////////////////////////////////////////////////////////// code machine à états ///////////////////////
////////////////////////////////////////////////////////// Led sur la 11, et Btn sur la 12 ////////////
// tuto de @J-M-L : https://forum.arduino.cc/t/programmation-automate-fini-machine-a-etat/452532 //////

const byte pinBtn = 12 ;
const byte pinLED = 11 ;
bool etatLED = 0 ;
bool valBtn = 0 ;
unsigned long prevMillis = 0 ;
const int LED_delay = 500 ;

void setup()
{
  pinMode(pinBtn, INPUT_PULLUP);
  pinMode(pinLED, OUTPUT);
  digitalWrite(pinLED, HIGH) ;
  Serial.begin(115200);
}

void loop()
{
unsigned long MILLIS = millis() ;
  ///////////////////////////////////////////////// lecture Btn et inversion valeur car pullup

  valBtn = digitalRead(pinBtn);
  valBtn = ! valBtn ;

  ///////////////////////////////////////////////// test machine à état //////////////////////

  if (valBtn == 1 && etatLED == 0)
  {
    etatLED = !etatLED ;
    digitalWrite(pinLED, etatLED);
    Serial.println("Led allume") ;
  }
  else if (valBtn == 1 && etatLED == 1)
  {
    etatLED = ! etatLED ;
    digitalWrite(pinLED, etatLED);
    Serial.println("Led eteinte") ;
  }
  /////////////////////////////////////////// Normalement on entre pas dans cette boucle... /////////
  else
  {
    if ((MILLIS - prevMillis) > LED_delay)
    {
      etatLED = ! etatLED ;
      digitalWrite(pinLED, etatLED);
      prevMillis = MILLIS ;
      Serial.print("Led clignote - erreur !") ;
    }
  }
}

Puis je me suis demandé si ce que j'avais fait était vraiment une machine à état... c'est la raison pour laquelle j'ai crée ce topic.

Je veux juste savoir si c'est bien une machine à état, avant de voir plus compliqué...
Merci d'avance pour vos réponses :wink:

En parcourant très rapidement je dirais non.

Pour parler de machine à état on s'attendrait à minima à :

  • des états identifié typiquement avec un type enum
  • et très souvent une gestion des changement d'état avec un switch case la plus part du temps

Bonjour @dragonuino

Ok, je vais refaire un code
Merci pour ta réponse :slight_smile:

Cordialement
Pandaroux007

Ce genre de truc rend le code illisible et source d'erreur tôt ou tard.

Il est préférable de faire ça:

#define BOUTON_APPUYE LOW
#define BOUTON_RELACHE HIGH

void loop()
{
   valBtn = digitalRead(pinBtn);
   if (valBtn == BOUTON_APPUYE){
     ......
  }

Je ne comprends pas ce commentaire

Tu y passe au contraire presque tout le temps puisque tu tombes dans ce else si le bouton n'est pas appuyé

Ah, oui, c'est vrai, je corrige de suite. Merci @fdufnews
Voici le code selon vos conseils :

////////////////////////////////////////////////////////// code machine à états ///////////////////////
////////////////////////////////////////////////////////// Led sur la 11, et Btn sur la 12 ////////////
// tuto de @J-M-L : https://forum.arduino.cc/t/programmation-automate-fini-machine-a-etat/452532 //////

const byte pinBtn = 12 ;
const byte pinLED = 11 ;
bool etatLED = 0 ;
bool valBtn = 0 ;
unsigned long prevMillis = 0 ;
const int LED_delay = 500 ;
#define BOUTON_APPUYE LOW
#define BOUTON_RELACHE HIGH

void setup()
{
  pinMode(pinBtn, INPUT_PULLUP);
  pinMode(pinLED, OUTPUT);
  digitalWrite(pinLED, HIGH) ;
  Serial.begin(115200);
}

void loop()
{
  unsigned long MILLIS = millis() ;
  
  ///////////////////////////////////////////////// lecture Btn et inversion valeur car pullup

  valBtn = digitalRead(pinBtn);

  ///////////////////////////////////////////////// test machine à état //////////////////////

  if (valBtn == BOUTON_APPUYE && etatLED == 0)
  {
    etatLED = !etatLED ;
    digitalWrite(pinLED, etatLED);
    Serial.println("Led allume") ;
  }
  else if (valBtn == BOUTON_APPUYE && etatLED == 1)
  {
    etatLED = ! etatLED ;
    digitalWrite(pinLED, etatLED);
    Serial.println("Led eteinte") ;
  }
  /////////////////////////////////////////// on entre ici a chaque boucle ou le Btn n'est pas appuié /////////
  else
  {
    if ((MILLIS - prevMillis) > LED_delay)
    {
      etatLED = ! etatLED ;
      digitalWrite(pinLED, etatLED);
      prevMillis = MILLIS ;
      Serial.print("Led clignote - erreur !") ;
    }
  }
}

J'ai fait une modif entre temps.
Il vaut mieux utiliser HIGH et LOW qui sont les valeurs retournées par digitalRead()

Pour avoir une machine à état il ne suffit pas d'avoir une variable qui représente l'état de ta LED, même si on simplifie au maximum, c'est quand même valable.
En gros tu as bien un machine à deux états, mais tes états ne sont pas vraiment visible.
Après il y a la théorie pure, mais on peut laisser ça de coté au début, je trouve.

Du coup personnellement, je verrais bien tes IF avec uniquement tes état, sans prendre en compte les actions.
Pour rendre ça plus lisible.

Bonjour @terwal

Ok, donc ça veut dire que par exemple, je dois avoir un enum etat[]{etat1, etat2, etat3 }; (avec des noms plus explicites, bien sur)
Avec tous les états possibles, et crée une variable int etat qui dit dans le moniteur série a quel état je suis ? C'est ça ?

Merci beaucoup pour votre aide :grin:
Cordialement
Pandaroux007 / Rémi :fox_face:

P.S : j'aurai besoin d'un moyen d'avoir un truc qui a plusieurs d'état, du coup j'ai pensé a utiliser un potar avec map(valPOTAR, 0, 1023, 0, 10);, puis allumer une rangée de LED en fonction de la valeur avec une machine à état...

Ce n'est pas indispensable pour faire une machine à états
Imaginons que l'on a:

  • un bouton
  • trois LEDs
  • et 4 états : E_ETEINT, E_1LED, E_2LED, E_3LEDS

Au départ la variable etatCourant = E_ETEINT

dans loop() tu as quelque chose comme ça (ce n'est pas complètement fonctionnel, c'est un exemple):

// gestion des états
bouton = digitalRead(BOUTON);
switch(etatCourant){
   case E_ETEINT:
      if (bouton == BOUTON_APPUYE) etatCourant = E_1LED;
      break;
  case E_1LED:
      if (bouton == BOUTON_APPUYE) etatCourant = E_2LED;
      break;
  case E_2LED:
      if (bouton == BOUTON_APPUYE) etatCourant = E_3LED;
      break;
  case E_3LED:
      if (bouton == BOUTON_APPUYE) etatCourant = E_ETEINT;
      break;
  default:
     break;
}
// actions liées aux états
switch(etatCourant){
  case E_3LED:
     digitalWrite(LED3, HIGH);
     break;
  case E_2LED:
     digitalWrite(LED2, HIGH);
     break;
  case E_1LED:
     digitalWrite(LED1, HIGH);
     break;
  case E_ETEINT;
    digitalWrite(LED1, LOW);
    digitalWrite(LED2, LOW);
    digitalWrite(LED3, LOW);
    break;
   }
   while(digitalRead(BOUTON)==BOUTON_APPUYE) {};  // attente relâché du bouton

Dans cet exemple un actionneur qui n'a que deux états peut faire fonctionner une machine avec autant d'états que tu veux

Edit: quelques corrections sur le nom des états

Le enum est une façon de déclarer tout tes états en un seul endroit et de lui affecter automatiquement une valeur.
en gros c'est l'équivalent de faire

int etat1 = 0;
int etat2 = 1;
int etat3 = 2;

int etat = etat1;
int count = 1;
void setup() {
  Serial.begin(115200);
}

void loop(){
    if (etat == etat1) {
      count ++;
      if (count % 20 == 0) etat =etat2;
    } else if (etat == etat2){
      count += 2;
      if (count % 20 == 0) etat =etat3;;
    } else if (etat == etat3) {
      count += 5;
      if (count % 20 == 0) etat =etat1;;
    }

    Serial.print("Etat: ");Serial.print(etat);Serial.print(", count: ");Serial.print(count);Serial.print(", %: ");Serial.println(count %20);
    delay(1000);
}

du coup avec un enum ça devient

enum etatX {etat1, etat2, etat3};
etatX etat = etat1; 
int count = 1;
void setup() {
  Serial.begin(115200);
}

void loop(){
    if (etat == etat1) {
      count ++;
      if (count % 20 == 0) etat = etat2;
    } else if (etat == etat2){
      count += 2;
      if (count % 20 == 0) etat = etat3;;
    } else if (etat == etat3) {
      count += 5;
      if (count % 20 == 0) etat = etat1;
    }

    Serial.print("Etat: ");Serial.print(etat);Serial.print(", count: ");Serial.print(count);Serial.print(", %: ");Serial.println(count %20);
    delay(1000);
}

et avec un switch au lieu d'utiliser des if, cela ne change pas le code, mais la lisibilité.

enum etatX {etat1, etat2, etat3};
etatX etat = etat1; 
int count = 1;
void setup() {
  Serial.begin(115200);
}

void loop(){
    switch(etat) {
      case etat1:
        count ++;
        if (count % 20 == 0) etat = etat2;
        break;

      case etat2:
        count += 2;
        if (count % 20 == 0) etat = etat3;
        break;

      case etat3:
        count += 5;
        if (count % 20 == 0) etat = etat1;
        break;
    }

    Serial.print("Etat: ");Serial.print(etat);Serial.print(", count: ");Serial.print(count);Serial.print(", %: ");Serial.println(count %20);
    delay(1000);
}

Les trois codes sont équivalents, mais la lisibilité du code suivant les sensibilités sont différentes.
Après suivant ce que tu veux faire, une machine peut être une perte de temps dans un premier temps, mais devenir intéressante, si la complexité augmente.

Pour ton PS, oui pourquoi pas comme base c'est très bien.

Je vois que ça travaille dur :slight_smile:

La première question à se poser quand on veut faire une machine à états c’est quel est le système qu’on modélise et quels sont ses états. Ensuite on regarde les événements qui font changer d’état

1 Like

Bonsoir pandaroux007

L'exemple de @fdufnews est excellent pour démontrer une machine à états.
Dans cet exemple il a clairement séparé les 2 étapes d'une telle machine, la gestion des états et l'action liée aux états.
Ces 2 phases peuvent "dispersées" dans le programme, mais rarement une gestion d'état déclenche une action "à la suite"
Ce qui fait que les machines à états c'est super en programmation, est que gestion(s) et action(s) travaillent "chacun dans leur coin".

Cordialement
jpbbricole

C’est un moyen d’expliquer ce qu’il se passe mais C’est un peu plus que cela car déclarer un enum revient à déclarer un nouveau type différent de int.

Il y a toujours possibilité de passer d’une valeur de l’enum vers un entier (conversion implicite) mais la norme dit que l’inverse n’est pas vrai.

Donc si on a un enum, il faut toujours utiliser des variables du type déclaré et toujours utiliser les mots clés de l’enum pour les tests ou affectations. Pas de maths pour affecter une certaine valeur dans une variable de type énuméré.

Il faut faire attention à cela surtout sur les plateformes 32 bits ou le compilateur refusera de compiler s’il voit ce cas.

Attention il ne faut pas commencer un nom de variable ou d’énum par un chiffre. Toujours commencer par une lettre.

Oui, c'est le coté qui peu être un peu déroutant pour les débutants, qui rajouté à la notion de machine à état, donne un aspect "compliqué".
Il faut arriver à oublié que le nouveau type énuméré est stocké sous forme d'entier, tout en le gardant à l'esprit, surtout si des propriétés de ce type sont utilisées dans des structures envoyées entre deux programmes.

Cette liste d'états peut sembler fastidieuse mais en fait cela limite le nombre de configurations possibles et on peut ainsi maitriser comment est notre système malgré le fait qu'il y a de multiples actionneurs à gérer qui peuvent tous avoir des tas de configurations possibles.

Comme le souligne J-M-L la base est de définir les différents "états", les différents modes de fonctionnement que l'on peut/veut avoir sur le système. Pour chacun de ces modes on défini comment doivent être tous les actionneurs.

Ensuite le switch case permet de déterminer le mode dans lequel on se trouve. Ce sera peut être un mode PAR_DEFAUT ou SITUATION_INCONNUE mais on sera dans tous les cas dans un mode bien défini quelquepart ; les actionneurs ne feront jamais n'importe quoi.

Et si demain on ajoute un actionneur il ne fera pas n'importe quoi dans son coin ; il suffira de définir ce qu'il doit faire dans chaque état.

ce n'est pas une variable c'est juste une liste qui détaille les conditions d'utilisation du morceau de "code" qui suit. J'ai levé l’ambiguïté en ajoutant un tiret devant

Désolé si j’ai raté la modif (je lis sur mon iPhone - pas toujours avec mes lunettes :slight_smile: ) j’avais vu ce code

Au temps pour moi. Je corrige

Bonjour, j'ai commencé a faire un petit code comme l'exemple de @J-M-L pour allumer une led de plus a chaque appui sur un bouton, mais je ne comprend pas le code a cause de l'utilisation de la librairie OneButton.h... Par exemple la fonction :

// ------------------------------------------------------
// La fonction de call back, appellée automatiquement quand on clique
// ------------------------------------------------------
void simpleclick()
{
  switch (etatCourant) {
    case REPOS: // on était au repos et on a un appui, on allume la verte
      digitalWrite(pinLedVerte, HIGH); // LED verte alimentée
      etatCourant = ETAT_V; // on note le nouvel état de notre système
      break;

    case ETAT_V: // on était led verte allumée et on a un appui, on allume la jaune
      digitalWrite(pinLedJaune, HIGH); // LED jaune alimentée
      etatCourant = ETAT_VJ;// on note le nouvel état de notre système
      break;

    case ETAT_VJ: // vert et jaune allumées, on a un appui, on allume la orange
      digitalWrite(pinLedOrange, HIGH); // LED orange alimentée
      etatCourant = ETAT_VJO;// on note le nouvel état de notre système
      break;

    case ETAT_VJO:// vert, orange et jaune allumées, on a un appui, on allume la rouge
      digitalWrite(pinLedRouge, HIGH); // LED rouge alimentée
      etatCourant = ETAT_VJOR;// on note le nouvel état de notre système
      break;

    case ETAT_VJOR: // tout était allumé, on a un appui, on retourne au repos
      mettreAuRepos(); // on retourne à l'état initial
      break;
  }
}

J'ai compris une partie du code, mais comment faire pour l'appeler automatiquement quand on clique ?
Merci pour tous vos conseils, j'en prend note :wink:

Rémi

P.S : autre question : Une lampe de vélo, c'est aussi une machine à état ? quand on appuie, il y a d'abord un mode, puis un autre qui clignote, etc ?