Bibliothèque Button

La bibliothèque Button est une autre illustration de l'utilisation des événements en C++ sur l'Arduino en se basant sur le design pattern Observer. En plus de la classe Button permettant d'instancier un bouton, elle fournit également la classe ButtonEvent qui permet d'exploiter les interactions de l'utilisateur du le bouton. De plus, elle propose plusieurs listeners qui permettent de traiter ces interactions dans des contextes différents tout en garantissant un découplage parfait entre la classe Button et les classes réceptrices des interactions.

Enum ButtonStatus

L'énumération ButtonStatus définit l'état du bouton. Elle peut prendre deux valeurs :
  • BUTTON_RELEASED indique que le bouton est relâché. La tension de la pin de l'Arduino à laquelle le bouton est connecté est positionnée à +5V.
  • BUTTTON_PRESSED indique que le bouton est appuyé. La tention de la pin de l'Arduino à laquelle le bouton est connecté est positionnée à 0V.

Classe Button

Constructeur

  • Button(uint8_t pin)
    • pin est le numéro de pin de l'Arduino sur laquelle le bouton est connecté.

Méthodes publiques

  • void begin()
    Héritée de la classe Component, elle permet d’initialiser le bouton. Cette méthode doit être invoquée soit dans la méthode setup() de l'Arduino, soit dans la méthode begin() du composite dans lequel le bouton est agrégé.
  • void loop()
    Héritée de la classe Component, elle effectue une lecture en continu de l'état du bouton.  Dans le cas où un utilisateur appuie ou relâche le bouton, elle émet un événement ButtonEvent pour signaler aux objets récepteurs que l'état du bouton a changé.
    Cette méthode doit être invoquée soit dans la méthode setup() de l'Arduino, soit dans la méthode begin() du composite dans lequel le bouton est agrégé.
  • ButtonStatus readStatus()
    Indépendamment du fonctionnement événementiel du bouton, cette méthode permet d'exposer l'état du bouton par une lecture directe sur la valeur de la pin de l'Arduino. Le résultat de la méthode est de type ButtonStatus.

Attributs privés

  • __pin est le numéro de pin de l'Arduino sur laquelle le bouton est connecté.
  • __lastStatus est l'état courant du bouton. Cet attribut est mis à jour par la boucle loop() du bouton.

Classe ButtonEvent

La classe ButtonEvent implémente l'événement émis par le bouton lorsqu'il change d'état dans la boucle loop() du bouton.

Constructeur

  • ButtonEvent(Button* sender, ButtonStatus btnStatus)
    • sender est un pointeur sur l'instance de la classe Button à l'origine de l'événement.
    • btnStatus est l'état du bouton lu sur la pin de l'Arduino.

Méthodes publiques

  • Button* getSender()
    Cet méthode est un accesseur qui expose l'adresse du bouton émetteur en lecture seule. Si un objet récepteur est à l'écoute de plusieurs boutons, cette méthode permet de connaitre l'instance du bouton à l'origine de l'événement.
  • ButtonStatus getStatus()
    Cette méthode est un accesseur qui expose en lecture seule l'état du bouton porté par l'événement. 

Attributs privés

  • __sender est un pointeur sur l'instance de la classe Button à l'origine de l'événement.
  • __status est l'état du bouton porté par l'événement.

Interface ButtonListener

L'interface ButtonListener est l'interface la plus courante à mettre en oeuvre pour les récepteurs de la classe Button. Elle prévient ceux-ci, à chaque fois que l'état du bouton a changé.

Méthodes protégées

  • void receive(ButtonEvent* event)
    Cette méthode héritée du template EventListener est invoquée par la classe Buttton lorsqu'une interaction est effectuée sur un bouton.
    • event est un pointeur sur l'événement émis par le bouton.
  • onChangeStatus(Button* sender, ButtonStatus newStatus)
    Cette méthode est une méthode abstraite (virtuelle pure) qui doit être surchargée dans les classes réceptrices pour prendre en charge les traitements à effectuer à réception.
    • sender est un pointeur sur le bouton émetteur de l'événement.
    • newStatus est le nouvel état du bouton.

Interface PressButtonListener

L'interface PressButtonListener est une autre interface possible à mettre en oeuvre pour les récepteurs de la classe Button. Elle prévient ceux-ci, lorsqu'une pression a été effectuée sur un bouton. En réalité, l'événement est émis lorsque le bouton est relâché.

Méthodes protégées

  • void receive(ButtonEvent* event)
    Cette méthode héritée du template EventListener est invoquée par la classe Buttton lorsqu'une interaction est effectuée sur un bouton.
    • event est un pointeur sur l'événement émis par le bouton.
  • onPress(Button* sender)
    Cette méthode est une méthode abstraite (virtuelle pure) qui doit être surchargée dans les classes réceptrices pour prendre en charge les traitements à effectuer à réception.
    • sender est un pointeur sur le bouton émetteur de l'événement.

Interface LongPressButtonListener

L'interface LongPressButtonListener fonctionne de façon similaire à l'interface PressButtonListener, à la différence qu'elle permet de distinguer une pression brève d'un pression longue. la plus courante à mettre en oeuvre pour les récepteurs de la classe Button. La différence entre une pression brève et une pression longue est établie sur la base d'un seuil passé à la construction de l'interface.

Constructeur

  • LongPressButtonListener (unsigned  long threshold)
    • threshold est le seuil exprimé en millisecondes permettant de distinguer les pressions longues des pressions brèves. La valeur par défaut est de 1 s (1000 ms).

Méthodes protégées

  • void receive(ButtonEvent* event)
    Cette méthode héritée du template EventListener est invoquée par la classe Buttton lorsqu'une interaction est effectuée sur un bouton.
    • event est un pointeur sur l'événement émis par le bouton.
  • onPress(Button* sender)
    Cette méthode est une méthode abstraite (virtuelle pure) qui doit être surchargée dans les classes réceptrices pour prendre en charge les traitements à effectuer à réception. Elle est invoquée lorsque la durée de la pression est inférieure au seuil.
    • sender est un pointeur sur le bouton émetteur de l'événement.
  • onLongPress(Button* sender, unsigned long elapseTime)
  • Cette méthode est une méthode abstraite (virtuelle pure) qui doit être surchargée dans les classes réceptrices pour prendre en charge les traitements à effectuer à réception. Elle est invoquée lorsque la durée de la pression est supérieure au seuil.
    • sender est un pointeur sur le bouton émetteur de l'événement.
    • elapseTime est le temps passé, exprimé en millisecondes, entre le moment où le bouton est appuyé et le moment où il est relâché. En exploitant ce paramètre, on peut déterminer des traitements spécifiques sur la distinction de plusieurs seuils. 

Attributs privés

  • __millis0 contient la valeur de la fonction millis() au moment où le bouton est appuyé. Lorsque le bouton est relâché, une nouvelle lecture de millis() est effectuée pour déterminer la durée de la pression. Cette durée comparée au seuil permet de distinguer les pressions longues des brèves.
  • __threshold est le seuil qui permet la distinction entre les pressions longues et les pressions brèves.

Câblage

Le câblage du bouton est assez habituel en électronique. En voici le schéma ci-contre :
Le montage comprend trois composants :
  • Le bouton S1, objet de cet article.
  • Un condensateur C1 de 10 nF pour amortir les rebonds.
  • Un résistance R1 de pull-up de 10 kΩ.

A quoi sert le condensateur C1 ?

En principe, Lorsqu'on appuie sur le bouton, le bouton effectue un court-circuit sur la pin de l'Arduino (la pin 3 dans l'exemple).Ce qui produit une chute verticale de la tension à 0 volts sur celle-ci. Ça, c'est la théorie. 
Dans la réalité, le passage de 5 volts à 0, et inversement de 0 à 5 volts,  provoque une oscillation dont la représentation graphique ressemblerait à la courbe rouge sur le graphe -ci-dessous. Le problème majeur de ces oscillations est que l'état de la pin de l'Arduino, au moment de la lecture, risque de donner une valeur indéterminée, voir fausse, sans compter les inversions de polarité sur le front descendant, ou les surcharges en tension sur le front montant qui risquent de détériorer les autres composants électroniques.
Le rôle du condensateur est de servir d'amortisseur à ces oscillations (comme les amortisseurs d'une voiture) comme le représente la courbe verte sur le graphe. Le tout étant de choisir un condensateur dont le temps de charge et de décharge permette de se rapprocher le plus possible de la courbe bleue correspondant aux changements d'état verticaux de la théorie. Une valeur de 10 nF permet un temps de charge/décharge légèrement inférieur à la fréquence de lecture de la pin de l'Arduino dans la boucle loop(), tout en assurant un amortissement suffisant des oscillations.

A quoi sert la résistance R1 ?

Lorsque le circuit est fermé (bouton appuyé), la pin de l'Arduino est en court-circuit avec la pin GND correspondant à 0 volts. L'état du bouton est alors à LOW sans ambiguïté. En revanche, lorsque le circuit est ouvert, il est difficile d'établir avec certitude que la pin de l'Arduino reliée au bouton soit bien à +5 volts. Ce qui peut provoquer une lecture erronée de l'état du bouton. La résistance permet de tirer (d'où le terme anglais pull-up) la tension à +5 volts.
La valeur de la résistance doit être suffisante pour éviter un court-circuit entre les pins GND et +5 volts de l'Arduino lorsque le circuit est fermé. Pour cela, il faut prévoir une résistance d'au moins 10 KΩ.
En pratique, ajouter une résistance de pull-up au câblage du bouton est inutile. En effet, le circuit Arduino est déjà équipé de base de telles résistances. Il suffira, d’initialiser la pin de l'Arduino connectée au bouton à INPUT_PULLUP par un appel de pinMode() dans la fonction setup(). La pin sera alors positionnée à +5 V. Ce qui est effectué en standard par la méthode begin() de la classe Button.
Le câblage standard d'un bouton pourra se résumer à la figure ci-dessous :

Utilisation de la classe Button

La classe Button peut être utilisée comme n'importe quel émetteur d'événement soit directement dans un sketch Arduino, soit à l'intérieur d'un composite. Se référer à l'article sur le Timer pour consulter des exemples des ces deux utilisations.
Dans l'exemple ci-dessous, la classe Button est utilisée directement dans un sketch. Ce qui oblige d'instancier la classe abstraite du listener dans une classe anonyme C++.
/* 1 */
#include "Button.h"

/* 2 */
#define PIN_BUTTON 3

/* 3 */
struct : public ButtonListener {
  void onChangeStatus(Button* sender, ButtonStatus newStatus) {
    Serial.print("Status:");
    Serial.println((int)newStatus);
  }
} buttonListener;

/* 4 */
 Button button(PIN_BUTTON);

/* 5 */
void setup() {
  Serial.begin(9600);
  button.addListener(&buttonListener);
  button.begin();
}

/* 6 */
void loop() {
  button.loop();
}
  1. En premier, il faut inclure la définition des classes de la bibliothèque Button.
  2. Il est d'une bonne pratique que de déclarer le numéro de  pin auquel est connecté le bouton dans une constante (ici PIN_BUTTON correspondant à la pin 3 de l'Arduino).
  3. L'interface ButtonListener est un classe abstraite comportant une méthode virtual pure onChangeStatus(). Elle ne peut donc pas être instanciée en tant que telle. Il est nécessaire de dériver l'interface dans une classe anonyme C++ dans laquelle la méthode abstraite est implémentée. Cette méthode se contente d'afficher sur le canal série de l'Arduino l'état reçu dans l'événement ButtonEvent. Le listener ainsi instancié est affecté à la variable buttonListener.
  4. Une instance button de la classe Button est initialisée sur la pin 3 de l'Arduino.
  5. Dans la fonction setup(), le listener buttonListener est enregistré dans le bouton comme récepteur des événements ButtonEvent. Puis la méthode begin() du bouton est invoquée. La pin 3 de l'Arduino est initialisée en mode INPUT_PULLUP à ce moment là.
  6. Dans la fonction loop(), la méthode loop() du bouton est invoquée. C'est la que la lecture répétitive de l'état du bouton est effectuée et qu'un événement ButtonEvent est émis lorsque l'état du bouton change.

Fichiers sources de la bibliothèque Button

La bibliothèque Button est fournies dans deux fichiers, le fichier Button.h qui contient la définition des classes de la bibliothèque qui doit être inséré par la directive #include dans le programmes qui l'utilise, et le fichier Button.cpp qui contient l'implémentation des méthodes de la classe Button.

Contenu du fichier Button.h

#ifndef Button_h
#define Button_h

#include "Component.h"
#include "Event.h"

enum ButtonStatus  { BUTTON_RELEASED, BUTTON_PRESSED };

class Button;


class ButtonEvent{
  Button* __sender;
  ButtonStatus __status;
  public:
  ButtonEvent(Button* sender, ButtonStatus btnStatus){
    this->__sender = sender;
    this->__status = btnStatus;
  }
  Button* getSender() { return this->__sender; }
  ButtonStatus getStatus() { return this->__status; }
};

class ButtonListener: public EventListener<ButtonEvent>
{
  protected:
  virtual void receive(ButtonEvent*);
  virtual void onChangeStatus(Button*, ButtonStatus) = 0;
};

class PressButtonListener: public EventListener<ButtonEvent> {
  protected:
  void receive(ButtonEvent*);
  virtual void onPress(Button*) = 0;
};

class LongPressButtonListener: public EventListener<ButtonEvent>
{
  unsigned long __threshold;
  unsigned long __millis0;

  protected:
  virtual void receive(ButtonEvent*);
  virtual void onPress(Button*) = 0;
  virtual void onLongPress(Button*, unsigned long) = 0;

  public:
  LongPressButtonListener(unsigned long = 1000);
};

class Button : public Component, public EventListenable<ButtonEvent>
{
  uint8_t __pin;
  ButtonStatus __lastStatus;

  public:
  Button(uint8_t);
  virtual void begin();
  virtual void loop();
  ButtonStatus readStatus();
};

#endif //Button_h


Contenu du fichier Button.cpp

#include "Button.h"

Button::Button(uint8_t pin)
{
  this->__pin = pin;
  this->__lastStatus = BUTTON_RELEASED;
}

void Button::begin() {
  pinMode(this->__pin, INPUT_PULLUP);
}

ButtonStatus Button::readStatus() {
  return (digitalRead(this->__pin) == HIGH) ? BUTTON_RELEASED : BUTTON_PRESSED;
}

void Button::loop() {
  ButtonStatus btnStatus = this->readStatus();
  if (btnStatus != this->__lastStatus) {
    this->__lastStatus = btnStatus;
    this->_dispatch(new ButtonEvent(this, this->__lastStatus));
  }
}

void ButtonListener::receive(ButtonEvent* event) {
  onChangeStatus(event->getSender(), event->getStatus());
}

void PressButtonListener::receive(ButtonEvent* event) {
  if (event->getStatus() == BUTTON_RELEASED) {
    onPress(event->getSender());
  }
}

LongPressButtonListener::LongPressButtonListener(unsigned long threshold) {
  this->__threshold = threshold;
  this->__millis0 = 0;
}

void LongPressButtonListener::receive(ButtonEvent* event) {
  switch (event->getStatus()) {
    case BUTTON_PRESSED:
      this->__millis0 = millis();
      break;
    case BUTTON_RELEASED:
      unsigned long elapseTime = millis() - this->__millis0;
      if (elapseTime > this->__threshold) {
        onLongPress(event->getSender(), elapseTime);
      } else {
        onPress(event->getSender());
      }
      break;
  }
}

Conclusion

La bibliothèque Button est un nouvel exemple de l'utilisation de la gestion des événements en C++ par une implémentation du désign pattern Observer. A la différence de la classe Timer, elle propose plusieurs listeners permettant d'utiliser la classe Button dans différents contextes, mettant en évidence le parfait découplage existant entre la classe Button émettrice d'événement et les récepteurs de ceux-ci.

Commentaires

Posts les plus consultés de ce blog

Afficheur à LED 7 segments (classe SegmentLedDisplay)

Piloter un clavier matriciel sur le bus I2C avec un PCF8574

Utiliser Visual Studio Code pour développer sur Arduino