samedi 2 novembre 2019

ARDUINO : un moteur de machine à états finis


ARDUINO : un moteur de machine

à états finis


Dans une vie de développeur logiciel dans le monde embarqué il est rare que l'on n'aie pas besoin un jour ou l'autre de développer une machine à états finis.
Lorsque le nombre d'états possibles d'un système et le nombre d'événements à traiter sont importants c'est même indispensable, sous peine de se retrouver noyé dans la complexité d'une longue suite d'instructions if / else ou switch / case imbriquées.

Le but de cet article est de décrire une machine à états (ou moteur d'automate) générique, que l'on pourra utiliser dans de nombreuses applications. Ce moteur est développée sous forme de librairie bien entendu, et quelques exemples simples sont fournis.
Le titre parle d'ARDUINO, mais on peut aussi faire fonctionner ce moteur sur un ESP8266, un ESP32 ou unSTM32.

1. Principe

Tout système à états se trouve dans un état donné à un instant T. Il peut être :
  • en cours de démarrage
  • dans un état nominal
  • en panne
  • en défaut batterie
  • dans l'attente d'une action utilisateur
  • etc.
Un événement peut déclencher une action (ou non) et le faire changer d'état (ou non).

Un événement peut être de nature diverse :
  • un appui sur un bouton
  • le déclenchement d'un détecteur
  • le passage d'un badge RFID
  • un seuil de température atteint
  • une temporisation arrivant à échéance
  • l'état d'une tension, de batterie par exemple
  • etc.
Au départ le moteur est dans un état, nommé en général IDLE ou NOMINAL.
L'application poste les événements dans une file d'attente et le moteur d'automate les traite au fur et à mesure de leur arrivée.

2. Les librairies existantes

Il existe quelques implémentations de machine à états finis :

https://github.com/jrullan/StateMachine
Ce moteur utilise l'allocation dynamique, peu adapté à l'ARDUINO et sa petite quantité de mémoire.
Elle ne possède pas de notion d'événement; elle donc est incapable de prendre une décision.

https://github.com/jonblack/arduino-fsm
Ce moteur utilise aussi l'allocation dynamique.

https://github.com/bricofoy/yasm
Une approche où chaque fonction de l'automate prend la décision d'exécuter une action en fonction d'un événement, et décide elle-même de changer d'état. Il en résulte une dispersion des décisions qui ne favorise pas la lecture.
Je préfère de loin concentrer les prises de décision et les changements d'état dans une table d'état / transition, ce qui permet permet de les centraliser, et facilite la lecture.

La moisson n'est pas épaisse.

Implémenter une solution simple adaptée à l'ARDUINO ne semble pas farfelu. Je me suis inspiré de mon expérience professionnelle, longue et semée de difficultés.

3. Le besoin

J'ai cherché avant tout à éviter de créer une table d'états / transitions à l'aide de code. Mon approche se base plutôt sur une table statique. Cela permet de concentrer les prises de décisions et favorise la lecture.

Dans cette approche, les fonctions de l'automate exécutent des actions et se contentent de ce rôle. Il suffit de lire la table d'état / transition pour suivre le déroulement. Aucune prise de décision n'est prise ailleurs, sauf si l'on en décide autrement. En d'autres termes, une action peut très bien prendre la décision de poster un événement ou un autre (ou aucun) en fonction du contexte. C'est au goût de chacun.

4. L'interface de la librairie

La table d'états / transitions est composée d'un certain nombre de lignes (des structures) possédant les éléments suivants :
struct machineState
{
  int currentState;          //
état courant
  int event;                       // événement
  int arg;                           // argument
  int (*action)(int arg);    // action à exécuter : adresse d'une fonction du type int action(int arg) 
  int nextState;                // état suivant
};


La table d'états / transitions est terminée par une ligne dont au moins l'élément currentState vaut zéro, ce qui signale la fin de la table.

L'action à exécuter est stockée dans chaque ligne sous forme de pointeur de fonction. Toutes les actions doivent avoir ce prototype :

int action(int arg);

Il est nécessaire de déclarer un prototype de chaque fonction-action avant la table d'états / transition.

Un événement a la structure suivante :

struct event
{
  struct list_head entry;  // entrée dans la file d'attente

  int event;                         // ID de l'événement
  int arg;                            // argument
};


Un événement possède un argument. Par exemple, un événement tension peut avoir un état PRÉSENT ou ABSENT, ou bien CORRECTE ou FAIBLE, un événement BOUTON peut avoir un état APPUYÉ ou RELÂCHÉ.

Les fonctions de la librairie sont très peu nombreuses :

void startMachine(struct machineState *ms, int st);

Démarre la machine à états.
ms : l'adresse de la table d'états / transitions, terminée par zéro
st : l'état initial

void postEvent(struct event *e, int event, int arg);
Poste un événement dans la file d'attente.
e: l'adresse de la structure événement
event : l'identifiant de l'événement
arg : un argument optionnel

void processEvents(void); Prend un événement dans la file d'attente et le traite.
Cette fonction doit être appelée par la loop()

int getMachineState(void); Returns the current engine state

Le moteur, lorsqu'il a un événement à traiter, cherche l'état courant du système et le N° de l'événement ainsi que son argument dans la table de transitions.S'il trouve une ligne qui correspond, l'action est exécutée. Ensuite le système passe dans l'état spécifié par l'élément ÉTAT SUIVANT.
Une action peut bien entendu poster un événement dans la file d'attente. Cet événement sera traité ultérieurement, au minimum à la fin de l'action en cours.
L'intérêt d'une file d'attente est justement de poster et donc de stocker les événements pour qu'ils soient traités les uns après les autres.

Un état, un événement ou un argument peut être ignoré (IGNORE).
Par exemple : si le moteur trouve l'état et l'événement dans la table et que l'argument est noté IGNORE, il considérera que la ligne dans la table est celle qu'il doit traiter.

Une action peut être également ignorée (NO_ACTION). Il n'y a donc aucune action à exécuter.

Enfin un état suivant peut être ignoré (STATE_NOCHANGE), c'est à dire qu'il n'y pas à effectuer de changement d'état après avoir exécuté l'action.

Si le moteur ne trouve pas le trio ÉTAT + ÉVÈNEMENT + ARGUMENT dans la table, il n'exécute aucune action et ne change pas d'état (il affiche simplement "???").

D'un point de vue technique, je n'ai utilisé aucune allocation de mémoire dynamique. La table d'états / transitions est statique, de même que les événements.

5. La pratique

Les exemples suivants utilisent des boutons et une LED. Vous aurez besoin de la librairie Bounce2 :
https://github.com/thomasfredericks/Bounce2.git

5.1. Un bouton

Nous allons commencer pas un système simple à un bouton. On appuie sur le bouton pour allumer une LED, on appuie à nouveau pour éteindre :


#include <Bounce2.h>

#include "light-fsm.h"

#define BUTTON_PIN              2
#if defined ESP8266 || defined ESP32
#define LED_BUILTIN             4
#endif

#define buttonIsPressed()       (debouncer.read() == LOW ? true : false)

#define STATE_OFF               1
#define STATE_ON                2

#define EVENT_BTN               1

int ledOn(int arg);
int ledOff(int arg);

static struct machineState states[] =
{
  {STATE_OFF,  EVENT_BTN,    true,   ledOn,     STATE_ON},
  {STATE_ON,   EVENT_BTN,    true,   ledOff,    STATE_OFF},
  {IGNORE,     EVENT_BTN,    false,  NO_ACTION, STATE_NOCHANGE},
  0
};

Bounce debouncer = Bounce();
struct event buttonEvent;

int ledOn(int arg)
{
  Serial.println("LED ON");
  digitalWrite(LED_BUILTIN, HIGH);
}

int ledOff(int arg)
{
  Serial.println("LED OFF");
  digitalWrite(LED_BUILTIN, LOW);
}

void readButton(void)
{
  static int state = false;

  int btn = buttonIsPressed();
  if (state != btn) {
    postEvent(&buttonEvent, EVENT_BTN, btn);
    state = btn;
  }
}

void setup(void)
{
  Serial.begin(115200);
  pinMode(BUTTON_PIN, INPUT_PULLUP);
  pinMode(LED_BUILTIN, OUTPUT);
  debouncer.attach(BUTTON_PIN);
  debouncer.interval(5);
  Serial.println("LIGHT STATE MACHINE DEMO");
  startMachine(states, STATE_OFF);
}

void loop(void)
{
  debouncer.update();
  readButton();
  processEvents();
}


La fonction readButton() permet de lire l'état du bouton.
Elle poste un événement EVENT_BTN(true) si le bouton 1 est appuyé et EVENT_BTN(false) s'il est relâché.

La fonction ledOn() est une action. Elle allume la LED.
La fonction ledOff() est une action. Elle éteint la LED.

Dans la table d'états / transitions on peut voir que les événements bouton relâché (EVENT_BTN, false) sont ignorés (NO_ACTION, STATE_NOCHANGE), quel que soit l'état (IGNORE) :

{IGNORE,     EVENT_BTN,    false,  NO_ACTION, STATE_NOCHANGE},
 
C'est juste un exemple d'utilisation des mots clés IGNORE, NO_ACTION, STATE_NOCHANGE. On aurait pu faire la même chose en évitant de poster ces événements.

La variable buttonEvent est une variable globale ou statique, ce qui permet d'éviter l'allocation dynamique des événements.

Ce code peut ressembler à un canon pour tuer une mouche pour allumer et éteindre une LED, mais ce n'est qu'un exemple didactique. Lorsque le nombre d'états et d'événements augmentera, la complexité sera plus facile à gérer qu'avec un code classique sans moteur, avec simplement des instructions if, else ou switch / case.

5.2. Un bouton + mesure de tension batterie

Ce système simple à un bouton est maintenant alimenté par batterie. On appuie sur le bouton pour allumer une LED, on appuie à nouveau pour éteindre, mais si la batterie est trop faible l'allumage est impossible.
Si la batterie est trop faible, la LED clignotte (un flash de 50ms toutes les 4 secondes) :

#include <Bounce2.h>

#include "light-fsm.h"

#define BUTTON_PIN              2
#if defined ESP8266 || defined ESP32
#define LED_BUILTIN             4
#endif
#define V_BATT                  0
#define VREF                    1.078
#define BATT_LIMIT              3.0
#define BATT_DIVIDER            0.205

#define BLINK_ON_TIME           50
#define BLINK_OFF_TIME          4000

#define buttonIsPressed()       (debouncer.read() == LOW ? true : false)

#define STATE_OFF               1
#define STATE_ON                2
#define STATE_BATT_LOW_ON       3
#define STATE_BATT_LOW_OFF      4

#define EVENT_BTN               1
#define EVENT_BATT              2

int ledOn(int arg);
int ledOff(int arg);
int startBlink(void);
int stopBlinkOn(void);
int stopBlinkOff(void);

static unsigned long timer;

static struct machineState states[] =
{
  {STATE_OFF,           EVENT_BTN,    true,   ledOn,        STATE_ON},
  {STATE_ON,            EVENT_BTN,    true,   ledOff,       STATE_OFF},
  {STATE_OFF,           EVENT_BATT,   LOW,    startBlink,   STATE_BATT_LOW_OFF},
  {STATE_ON,            EVENT_BATT,   LOW,    startBlink,   STATE_BATT_LOW_ON},
  {STATE_BATT_LOW_ON,   EVENT_BATT,   HIGH,   stopBlinkOn,  STATE_ON},
  {STATE_BATT_LOW_OFF,  EVENT_BATT,   HIGH,   stopBlinkOff, STATE_OFF},
  {STATE_BATT_LOW_ON,   EVENT_BTN,    true,   NO_ACTION,    STATE_NOCHANGE},
  {STATE_BATT_LOW_OFF,  EVENT_BTN,    true,   NO_ACTION,    STATE_NOCHANGE},
  {IGNORE,              EVENT_BTN,    false,  NO_ACTION,    STATE_NOCHANGE},
  0
};

Bounce debouncer = Bounce();
struct event battEvent;
struct event buttonEvent;

int ledOn(int arg)
{
  Serial.println("*** LED ON");
  digitalWrite(LED_BUILTIN, HIGH);
}

int ledOff(int arg)
{
  Serial.println("*** LED OFF");
  digitalWrite(LED_BUILTIN, LOW);
}

int startBlink(void)
{
  Serial.println("*** START BLINK");
  digitalWrite(LED_BUILTIN, HIGH);
  timer = millis();
}

int stopBlinkOn(void)
{
  Serial.println("*** STOP BLINK & TURN ON");
  digitalWrite(LED_BUILTIN, HIGH);
  timer = 0;
}

int stopBlinkOff(void)
{
  Serial.println("*** STOP BLINK & TURN OFF");
  digitalWrite(LED_BUILTIN, LOW);
  timer = 0;
}

float getBatteryVoltage(void)
{
  unsigned int adc;

  adc = analogRead(V_BATT);
  return adc * VREF / 1023 / BATT_DIVIDER;
}

int readBatteryState(void)
{
  static int state = HIGH;
  float vBatt;

  vBatt = getBatteryVoltage();
  int batt = vBatt > BATT_LIMIT ? HIGH : LOW;
  if (batt != state) {
    state = batt;
    Serial.print(F("BATT: ")); Serial.print(vBatt); Serial.print(F("V")); Serial.println(vBatt < BATT_LIMIT ? F(" LOW") : F(" OK"));
    return batt;
  }
  return -1;
}

void readButton(void)
{
  static int state = false;

  int btn = buttonIsPressed();
  if (state != btn) {
    postEvent(&buttonEvent, EVENT_BTN, btn);
    state = btn;
  }
}

void controlLed(void)
{
  if (timer) {
    if (digitalRead(LED_BUILTIN) == LOW && millis() - timer > BLINK_OFF_TIME) {
      digitalWrite(LED_BUILTIN, HIGH);
      timer = millis();
    }
    if (digitalRead(LED_BUILTIN) == HIGH && millis() - timer > BLINK_ON_TIME) {
      digitalWrite(LED_BUILTIN, LOW);
      timer = millis();
    }
  }
}

void setup(void)
{
  Serial.begin(115200);
  pinMode(BUTTON_PIN, INPUT_PULLUP);
  pinMode(LED_BUILTIN, OUTPUT);
  debouncer.attach(BUTTON_PIN);
  debouncer.interval(5);
  Serial.println("LIGHT STATE MACHINE DEMO");
  analogReference(INTERNAL);
  startMachine(states, STATE_OFF);
}

void loop(void)
{
  int batt = readBatteryState();
  if (batt != -1) {
    postEvent(&battEvent, EVENT_BATT, batt);
  }
  controlLed();
  debouncer.update();
  readButton();
  processEvents();
}


La mesure de la batterie est effectuée à l'aide un pont diviseur 1MΩ (côté VCC) + 270KΩ (côté GND) avec le point milieu sur A0.Pour expérimenter on peut utiliser un potentiomètre. Attention à ne pas appliquer une tension supérieure à VREF (1.1V) sur A0).

La fonction readBatteryState() permet de récupérer l'état de la batterie :
  • HIGH : OK
  • LOW : faible
  • -1 : pas de changement
La fonction loop poste un événement EVENT_BATT(HIGH) ou EVENT_BATT(LOW) si l'état change )

Dans la table d'états / transition la ligne suivante permet de basculer dans l'état STATE_BATT_LOW_ON ou STATE_BATT_LOW_OFF si l'événement EVENT_BATT(LOW) est posté, quelque soit l'état courant (IGNORE).

  {STATE_OFF,           EVENT_BATT,   LOW,    startBlink,   STATE_BATT_LOW_OFF},
  {STATE_ON,            EVENT_BATT,   LOW,    startBlink,   STATE_BATT_LOW_ON},


L'état STATE_BATT_LOW_ON STATE_BATT_LOW_OFF permet de savoir que la LED était allumée ou éteinte lorsque l'automate est passé dans l'état "Batterie faible", ce qui permettra de l'allumer ou de la laisser éteinte suivant le cas lorsque la batterie sera rechargée.

  {STATE_BATT_LOW_ON,   EVENT_BATT,   HIGH,   stopBlinkOn,  STATE_ON},
  {STATE_BATT_LOW_OFF,  EVENT_BATT,   HIGH,   stopBlinkOff, STATE_OFF},


Si l'événement batterie faible EVENT_BATT(LOW) est reçu, le moteur appelle la fonction startBlink() qui allume la LED puis active le timer .
Si l'événement batterie faible EVENT_BATT(HIGH) est reçu, le moteur appelle la fonction stopBlinkOn() ou stopBlinkOff() qui allume ou éteint la LED puis désactive le timer.

La fonction controlLed() fait clignoter la LED si le timer est actif.

5.3. Un bouton + mesure de tension batterie (bis)

Cet exemple est une autre version du précédent. La fonction controlLed() disparaît et laisse le contrôle du clignotement de la LED à l'automate.

Une autre différence consiste à ne pas réalummer la LED lorsque la batterie est rechargée. C'est un choix.

#include <Bounce2.h>

#include "light-fsm.h"

#define BUTTON_PIN              2
#if defined ESP8266 || defined ESP32
#define LED_BUILTIN             4
#endif
#define V_BATT                  0
#define VREF                    1.078
#define BATT_LIMIT              3.0
#define BATT_DIVIDER            0.205

#define BLINK_ON_TIME           50
#define BLINK_OFF_TIME          4000

#define buttonIsPressed()       (debouncer.read() == LOW ? true : false)

#define STATE_OFF               1
#define STATE_ON                2
#define STATE_INACTIVE          3

#define EVENT_BTN               1
#define EVENT_BATT              2
#define EVENT_TIMER             3

int ledOn(int arg);
int ledOff(int arg);
int startBlink(void);
int stopBlink(void);
int blinkOn(void);
int blinkOff(void);

static unsigned long timer;
static unsigned long duration;

static struct machineState states[] =
{
  {STATE_OFF,       EVENT_BTN,    true,           ledOn,      STATE_ON},
  {STATE_ON,        EVENT_BTN,    true,           ledOff,     STATE_OFF},
  {IGNORE,          EVENT_BATT,   LOW,            startBlink, STATE_INACTIVE},
  {IGNORE,          EVENT_BATT,   HIGH,           stopBlink,  STATE_OFF},
  {STATE_INACTIVE,  EVENT_BTN,    true,           NO_ACTION,  STATE_NOCHANGE},
  {STATE_INACTIVE,  EVENT_TIMER,  BLINK_ON_TIME,  blinkOff,   STATE_NOCHANGE},
  {STATE_INACTIVE,  EVENT_TIMER,  BLINK_OFF_TIME, blinkOn,    STATE_NOCHANGE},
  {IGNORE,          EVENT_BTN,    false,          NO_ACTION,  STATE_NOCHANGE},
  0
};

Bounce debouncer = Bounce();
struct event battEvent;
struct event buttonEvent;

int ledOn(int arg)
{
  Serial.println("*** LED ON");
  digitalWrite(LED_BUILTIN, HIGH);
}

int ledOff(int arg)
{
  Serial.println("*** LED OFF");
  digitalWrite(LED_BUILTIN, LOW);
}

int startBlink(void)
{
  Serial.println("*** START BLINK");
  digitalWrite(LED_BUILTIN, HIGH);
  duration = BLINK_ON_TIME;
  timer = millis();
}

int stopBlink(void)
{
  Serial.println("*** STOP BLINK");
  digitalWrite(LED_BUILTIN, LOW);
  timer = 0;
}

int blinkOn(void)
{
  Serial.println("*** BLINK ON");
  digitalWrite(LED_BUILTIN, HIGH);
  duration = BLINK_ON_TIME;
}

int blinkOff(void)
{
  Serial.println("*** BLINK OFF");
  digitalWrite(LED_BUILTIN, LOW);
  duration = BLINK_OFF_TIME;
}

float getBatteryVoltage(void)
{
  unsigned int adc;

  adc = analogRead(V_BATT);
  return adc * VREF / 1023 / BATT_DIVIDER;
}

int readBatteryState(void)
{
  static int state = HIGH;
  float vBatt;

  vBatt = getBatteryVoltage();
  int batt = vBatt > BATT_LIMIT ? HIGH : LOW;
  if (batt != state) {
    state = batt;
    Serial.print(F("BATT: ")); Serial.print(vBatt); Serial.print(F("V")); Serial.println(vBatt < BATT_LIMIT ? F(" LOW") : F(" OK"));
    return batt;
  }
  return -1;
}

void readButton(void)
{
  static int state = false;

  int btn = buttonIsPressed();
  if (state != btn) {
    postEvent(&buttonEvent, EVENT_BTN, btn);
    state = btn;
  }
}

void runTimer(void)
{
  if (timer == 0) {
    return;
  }
  if (millis() - timer > duration) {
    postEvent(&battEvent, EVENT_TIMER, duration);
    timer = millis();
  }
}

void setup(void)
{
  Serial.begin(115200);
  pinMode(BUTTON_PIN, INPUT_PULLUP);
  pinMode(LED_BUILTIN, OUTPUT);
  debouncer.attach(BUTTON_PIN);
  debouncer.interval(5);
  Serial.println("LIGHT STATE MACHINE DEMO");
  analogReference(INTERNAL);
  startMachine(states, STATE_OFF);
}

void loop(void)
{
  int batt = readBatteryState();
  if (batt != -1) {
    postEvent(&battEvent, EVENT_BATT, batt);
  }
  runTimer();
  debouncer.update();
  readButton();
  processEvents();
}


La fonction runTimer(), si le timer est activé, poste un événement EVENT_TIMER(durée). La table d'états / transition est modifiée :

#define BLINK_ON_TIME           50
#define BLINK_OFF_TIME          4000
 

  {IGNORE,          EVENT_BATT,   LOW,            startBlink, STATE_INACTIVE},
  {IGNORE,          EVENT_BATT,   HIGH,           stopBlink,  STATE_OFF},
  {STATE_INACTIVE,  EVENT_BTN,    true,           NO_ACTION,  STATE_NOCHANGE},
  {STATE_INACTIVE,  EVENT_TIMER,  BLINK_ON_TIME,  blinkOff,   STATE_NOCHANGE},
  {STATE_INACTIVE,  EVENT_TIMER,  BLINK_OFF_TIME, blinkOn,    STATE_NOCHANGE},


Si l'événement batterie faible EVENT_BATT(LOW) est reçu, le moteur appelle la fonction startBlink() qui allume la LED puis active le timer pour une durée de 50ms.
Si l'événement EVENT_TIMER(50) est reçu, le moteur appelle la fonction blinkOff() qui éteint la LED puis active le timer pour une durée de 4s.Si l'événement EVENT_TIMER(4000) est reçu, le moteur appelle la fonction blinkOn() qui allume la LED puis active le timer pour une durée de 50ms.
Si l'événement batterie faible EVENT_BATT(HIGH) est reçu, le moteur appelle la fonction stopBlink() qui éteint la LED puis désactive le timer.

Cet exemple illuste l'utilisation d'arguments autre que 0, 1, true, false, HIGH ou LOW : BLINK_ON_TIME ou BLINK_OFF_TIME.

Il illustre aussi la possibilité de mélanger des notions haut-niveau (les fonctions liées au bouton) et bas niveau (un clignottement de LED).
Est-ce une façon correcte de procéder ? A mon sens, non, car cela complique les choses inutilement. Cela encombre inutilement la table d'états / transitions et rend la lecture moins aisée. Je préfère la version précédente.

Il vaut mieux éviter de confier à un automate fonctionnel des traitements insignifiants.

5.4. Deux boutons et une temporisation

Imaginons un système possédant :
  • deux boutons
  • une temporisation de 30 secondes
  • une LED
Dans cet exemple, les boutons sont câblés entre D2 et GND et D3 et GND, la LED est la LED D13 de la carte UNO, NANO ou MINI.

Un appui court sur l'un des deux boutons allume la LED.

La LED s'éteint si :
  • on appuie brièvement sur le bouton 2
  • on appuie plus de 2 secondes sur le bouton 1
  • le timer expire
L'application :

#include <Bounce2.h>

#include "light-fsm.h"

#define BUTTON1                 0
#define BUTTON2                 1
#define BUTTON1_PIN             2
#define BUTTON2_PIN             3
#if defined ESP8266 || defined ESP32
#define LED_BUILTIN             4
#endif

#define STATE_OFF               1
#define STATE_ON                2

#define EVENT_BTN1              1
#define EVENT_BTN2              2
#define EVENT_BTNLP1            3
#define EVENT_BTNLP2            4
#define EVENT_TIMER             5

int buttonEvtNumber[] = {EVENT_BTN1, EVENT_BTN2};
int buttonEvtLpNumber[] = {EVENT_BTNLP1, EVENT_BTNLP2};

int ledOn(int arg);
int ledOff(int arg);

static struct machineState states[] =
{
  {STATE_OFF,  EVENT_BTN1,     true,   ledOn,      STATE_ON},
  {STATE_OFF,  EVENT_BTN2,     true,   ledOn,      STATE_ON},
  {STATE_OFF,  EVENT_BTNLP1,   IGNORE, NO_ACTION,  STATE_NOCHANGE},
  {STATE_OFF,  EVENT_BTNLP2,   IGNORE, NO_ACTION,  STATE_NOCHANGE},
  {STATE_ON,   EVENT_BTN1,     IGNORE, NO_ACTION,  STATE_NOCHANGE},
  {STATE_ON,   EVENT_BTN2,     true,   ledOff,     STATE_OFF},
  {STATE_ON,   EVENT_BTNLP1,   true,   ledOff,     STATE_OFF},
  {STATE_ON,   EVENT_BTNLP2,   IGNORE, NO_ACTION,  STATE_NOCHANGE},
  {STATE_ON,   EVENT_TIMER,    IGNORE, ledOff,     STATE_OFF},
  0
};

Bounce debouncer1 = Bounce();
Bounce debouncer2 = Bounce();
Bounce *debouncer[] = {&debouncer1, &debouncer2};

#define buttonIsPressed(button) (debouncer[button]->read() == LOW ? true : false)

struct event buttonEvent;
struct event longPressEvent;
struct event timerEvent;

unsigned long timer;

int ledOn(int arg)
{
  Serial.println("LED ON");
  digitalWrite(LED_BUILTIN, HIGH);
  timer = millis();
}

int ledOff(int arg)
{
  Serial.println("LED OFF");
  digitalWrite(LED_BUILTIN, LOW);
  timer = 0;
}

void readButton(int button)
{
  static int state[2] = {false, false};
  static unsigned long pressTime[2];

  int btn = buttonIsPressed(button);
  if (state[button] != btn) {
    if (btn == true) {
      if (pressTime[button] == 0) {
        pressTime[button] = millis();
      }
    }
    else {
      if (pressTime[button] != 0 && millis() - pressTime[button] > 2000) {
        Serial.println(F("LONG PRESS"));
        postEvent(&longPressEvent, buttonEvtLpNumber[button], true);
        pressTime[button] = 0;
      }
      else {
        postEvent(&buttonEvent, buttonEvtNumber[button], true);
      }
      pressTime[button] = 0;
    }
    state[button] = btn;
  }
}

int readTimer(void)
{
  if (timer != 0 && millis() - timer > 30000) {
    postEvent(&timerEvent, EVENT_TIMER, 0);
  }
}

void setup(void)
{
  Serial.begin(115200);
  Serial.println("LIGHT STATE MACHINE DEMO");
  pinMode(BUTTON1_PIN, INPUT_PULLUP);
  pinMode(BUTTON2_PIN, INPUT_PULLUP);
  pinMode(LED_BUILTIN, OUTPUT);
  debouncer1.attach(BUTTON1_PIN);
  debouncer1.interval(5);
  debouncer2.attach(BUTTON2_PIN);
  debouncer2.interval(5);
  startMachine(states, STATE_OFF);
}

void loop(void)
{
  debouncer1.update();
  debouncer2.update();
  readButton(BUTTON1);
  readButton(BUTTON2);
  readTimer();
  processEvents();
}


La fonction readButton() permet de lire l'état d'un bouton.
Elle poste un événement EVENT_BTN1(true) si le bouton 1 est appuyé brièvement, et EVENT_BTN2(true) si le bouton 2 est appuyé brièvement.
Elles poste un événement EVENT_BTNLP1(true) si le bouton 1 est appuyé pendant plus de 2 secondes, et EVENT_BTNLP2(true) si le bouton 2 est appuyé pendant plus de 2 secondes.

La fonction ledOn() est une action. Elle allume la LED et arme la temporisation.
La fonction ledOff() est une action. Elle éteint la LED et remet la temporisation à ZÉRO.

La fonction readTimer() permet de lire l'état de la temporisation et envoie un événement EVENT_TIMER si la temporisation est écoulée.

Dans la table d'états / transitions deux événements permettent d'allumer la LED :
  • EVENT_BTN1(true)
  • EVENT_BTN2(true)
Trois événements permettent d'éteindre la LED :
  • EVENT_BTN2(true) : appui court sur le bouton 2
  • EVENT_BTNLP1(true) : appui long sur le bouton 1
  • EVENT_TIMER : la temporisation
Les variables buttonEvent, longPressEvent et timerEvent sont également des variables globales ou statiques.

6. Téléchargements

La librairie est disponible ici :
https://bitbucket.org/henri_bachetti/light-fsm.git

Cette page vous donne toutes les informations nécessaires :
https://riton-duino.blogspot.com/p/migration-sous-bitbucket.html

7. Conclusion

Cette librairie est née pendant le développement d'une alimentation UPS pour RASPBERRY PI, où pas moins de 9 états et 7 événements sont gérés.
Sans un moteur de ce type il m'aurait été très difficile d'en venir à bout.
Le sketch est composé de 400 lignes de code et occupe 8K de code (28%), et 550 octets de RAM (26%). Autant dire qu'il est plutôt léger.


Cordialement
Henri

8. Mises à jour

04/11/2019 : nouvel exemple 4.3. Un bouton + mesure de tension batterie (bis)
05/10/2019 : ajouts dans 4. L'interface de la librairie

2 commentaires:

  1. Je ne compte plus le nombre de fois où mes questions sur le développement arduino m'ont conduit sur votre site. Et à chaque fois j'y ai trouvé des réponses claires et détaillées.
    Merci pour ce formidable travail d'éducation collective, votre site est certainement une des références en matière d'arduino francophone.
    Vraiment, merci !

    RépondreSupprimer
    Réponses
    1. Non, il y en a d'autres : Yves Pelletier entre autres.
      Merci en tous cas.

      Supprimer