Utilisation du gestionnaire d'événements : Classe Timer.

Lors d'un précédent article, la conception d'un système de gestion des événements, basé sur le design pattern Observer,  a été présenté. Celui-ci propose deux classes template C++, EventListenable et EventListener que l'on doit dériver pour construire respectivement les émetteurs et les récepteurs d'événement.
Le présent article présente, en exemple d'usage de ce système — la classe Timer — qui émet un événement TimerEvent à intervalle régulier.

La classe TimerEvent

La première chose à faire lorsque l'on crée un émetteur d'événement, ce de définir la classe de cet événement. Pour l'émetteur Timer, ce sera TimerEvent.
class TimerEvent {
  Timer* __sender;
  public:
  TimerEvent(Timer* sender){
    this->__sender = sender;
  }
  Timer* getSender() { return this->__sender; }
};
La classe TimerEvent implémente l'événement émis par un Timer. Elle ne contient qu'un attribut __sender correspondant à l'adresse du Timer qui a émis l'événement. Cet attribut est exposé en lecture seule via l'accesseur getSender().

La classe TimerListener

La deuxième étape de la création d'un émetteur est de définir l'interface d'écoute qui pourra être dérivé par les classes réceptrices. Pour l'émetteur Timer, ce sera TimerListener.
class TimerListener: public EventListener<TimerEvent> {
  protected:
  virtual void receive(TimerEvent* event) {
    onTick(event->getSender());
  }
  virtual void onTick(Timer*) = 0;
};
La classe TimerListener est une classe abstraite, le C++ ne permettant pas de définir d'interfaces comme Java ou C#. Pour qu'une classe soit abstraite en C++, il faut qu'elle expose au moins une fonction virtuelle pure. Il s'agit ici de la méthode onTick(). Cette méthode devra impérativement être surchargée dans les classes réceptrice qui implémente l'interface TimeListener.
La classe TimerListener dérive (par héritage) la classe template EventListener<timerevent>.
La méthode onTick() est déclenchée par l'implémentation de la méthode receive() du polymorphisme EventListener. Cette méthode, étant elle-même une méthode virtuelle pure déclarée dans la classe EventListener, doit absolument être implémentée dans la classe TimerListener.

La classe Timer

La dernière étape, consiste enfin à créer la classe de l'émetteur de l'événement. Pour l'événement TimerEvent, ce sera Timer. Le code de cette classe sera réparti dans deux fichiers, Timer.h contenant la définition de la classe qui sera inclus dans les fichiers de code utilisant le composant Timer et le fichier Timer.cpp qui contient le code d'implémentation de la classe.

Fihier Timer.h

#ifndef Timer_h
#define Timer_h

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

class Timer;

class TimerEvent {
  Timer* __sender;
  public:
  TimerEvent(Timer* sender){
    this->__sender = sender;
  }
  Timer* getSender() { return this->__sender; }
};

class TimerListener: public EventListener<TimerEvent> {
  protected:
  virtual void receive(TimerEvent* event) {
    onTick(event->getSender());
  }
  virtual void onTick(Timer*) = 0;
};

class Timer: public Component, public EventListenable<TimerEvent> {
  unsigned long __millis0;
  unsigned long __interval;
  
  public:
  Timer(unsigned long interval) { this->__interval = interval; }
  virtual void begin() { this->__millis0 = millis(); }
  virtual void loop();
};

#endif // Timer_h
Le fichier Timer.h rassemble la définition de toute les classes nécessaires au fonctionnement d'un timer. Il doit être inclus dans le code des classes réceptrices.
La classe Timer dérive (par héritage) la classe template EventListenable<TimerEvent>.
  • Timer(unsigned long interval)
    Le constructeur de la classe Timer permet d'initialiser l'attribut interne __interval qui définit la fréquence d'émission de l'événement TimerEvent.
  • virtual void begin() 
    Héritée de la classe Component, elle permet d'initialiser le timer. 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 Timer est déclaré. La méthode initialise l'attribut __millis0 par une lecture de l'horloge interne de l'Arduino (fonction millis()) pour constituer une origine des temps.
  • virtual void loop()Héritée de la classe Component, elle constitue le cœur du fonctionnement du timer. L'implémentation, nécessitant plusieurs ligne, déportée dans le fichier Timer.cpp. Elle doit être invoquée soit dans la méthode loop() de l'Arduino, soit dans la méthode loop() du composite dans lequel le Timer est déclaré.

Fichier Timer.cpp

#include "Timer.h"

void Timer::loop() {
  unsigned long elapseTime = millis();
  if ((elapseTime - this->__millis0) > this->__interval) {
    this->__millis0 = elapseTime;
    this->_dispatch(new TimerEvent(this));
  }
}

Le fichier Timer.cpp, contient l'implémentation concrète des méthodes des classes définies dans Timer.h. Elle ne contient que l'implémentation de la méthode loop(), les autres méthodes étant toutes implémentées dans Timer.h.
La méthode loop() de la classe Timer est appelée en continu par la méthode loop() de l'Arduino.
Elle commence par effectuer une lecture de l'horloge interne de l'Arduino en invoquant la fonction millis(). Cette valeur est affectée à la variable elapseTime.
Soustraite à l'origine des temps, elle est comparée à l'attribut __interval. Lorsque l'intervalle de temps caractéristique du timer est dépassé, l'origine des temps __millis0 est mise à jour par la nouvelle valeur lue par millis() et un événement TimerEvent est envoyé aux récepteurs par l'invocation de la méthode _dispatch héritée de la classe EventListenable.

Utilisation de la classe Timer dans un sketch Arduino

Un listener est une classe abstraite. De ce fait, elle ne peut pas être instanciée sans passer par la création d'une nouvelle classe la dérivant. L'usage d'un listener dans un sketch Arduino passe par la solution des classes anonymes C++.  Voici un exemple de cet usage :
/* 1 */
#include "Timer.h"

/* 2 */
struct : public TimerListener {
  void onTick(Timer* sender) {
    Serial.println("Bip");
  }
} listener0;

/* 3 */
Timer timer0(1000);

/* 4 */
void setup() {
  Serial.begin(9600);
  timer0.addListener(&listener0);
  timer0.begin();
}

/* 5 */
void loop() {
  timer0.loop();
}
  1. En premier, il faut inclure la définition des trois classes Timer, TimerEvent et TimerListener  utilisées dans la gestion de l'événement TimerEvent. Ces classes sont définies dans le fichier Timer.h présenté ci-dessus.
  2. L'instanciation du listener TimerListener, il est nécessaire de passer par une classe anonyme C++ dans laquelle on implémente la méthode abstraite onTick(). Dans l'exemple, cette méthode affiche la chaîne "Bip" sur le port série de l'Arduino. Une instance de cette classe anonyme est affectée à la variable listener0.
  3. Une instance de la classe Timer est affectée à la variable timer0. Le délai du timer (1000 ms) est passé en paramètre du constructeur de la classe Timer.
  4. Dans la méthode setup() de l'Arduino, tous les composant doivent être initialisés. Ici il convient de faire la différence entre l'initialisation C++ et l’initialisation électronique des composants. En principe, l'initialisation C++ s'effectue dans le constructeur du composant, alors que l'initialisation électronique (affectation des bonne pin de l'Arduino par exemple) se passe dans dans la méthode begin() héritée de la classe Component. Dans un sketch,il n'y a pas de constructeur. Il convient donc de procéder aux initialisations C++ dans la fonction setup(). C'est pourquoi l'enregistrement du listener listener0 par addListener() invoquée sur le timer timer0 doit être effectué avant l'invocation de la méthode begin(). Attention à la syntaxe : le paramètre de la méthode addListener et un pointeur sur TimerListener, alors que listener0 est défini en statique par sa valeur. Il faut utiliser l'opérateur & sur listener0 pour en calculer l'adresse.
  5. De la même façon que la méthode begin() de tous les composants doit être invoquée dans la fonction setup(), la méthode loop() de tous les composants doit être invoquée dans la fonction loop() de l'Arduino.

Utilisation de la classe Timer dans un composite en dérivant l'interface TimerListener

L'utilisation la plus courante des événements met en oeuvre une classe composite qui agrège plusieurs composants. Classe composite qui se met à l'écoute d'un émetteur d’événements en dérivant la classe listener associée à l'événement.
Dans l'exemple ci dessous, il s'agit de créer une classe Composite, constituée d'un timer et d'une led pour faire clignoter celle-ci. Cela va passer par la création de deux fichiers : Composite.h contenant la définition de la classe Composite et Composite.cpp contenant l'implémetation de celle-ci.

Fichier Composite.h

/* 1 */
#ifndef Composite_h
#define Composite_h

/* 2 */
#include "Led.h"
#include "Timer.h"

/* 3 */
class Composite : public Component, public TimerListener {
  Led __led;
  Timer __timer;
  bool __ledState;

  public:
  Composite(uint8_t, unsigned long);
  void begin();
  void loop();
  void onTick(Timer*);
};

#endif // Composite_h
  1. La macro Composite_h est utilisée en prévention d'une déclaration multiple de la classe Composite. Ceci constitue une bonne pratique pour la gestion de projet multi-fichiers.
  2. La classe Composite agrège deux composants, une Led et un Timer. La définition respective de ces deux composant doit être incluse avec la directive #include pour pouvoir être utilisés dans  la définition de la classe Composite.
  3. Définition de la classe Composite :
    • La classe Composite, comme les autres composants, dérive l'interface Component. Ce qui oblige à fournir une implémentation pour les méthodes begin() et loop().
    • Pour pouvoir recevoir les événements générés par le Timer, la classe Composite doit dériver l'interface TimerListener. Ce qui oblige à fournir une implémentation à la méthode onTick().
    • Les deux composants agrégés figurent parmi les attributs privés :  __led et __timer.

Fichier Composite.cpp

#include "Composite.h"

/* 1 */
Composite::Composite(uint8_t pinLed, unsigned long delay)
  : __led(pinLed), __timer(delay)
{
  this->__ledState = false;
  this->__timer.addListener(this);
}

/* 2 */
void Composite::begin() {
  this->__led.begin();
  this->__timer.begin();
};

/* 3 */
void Composite::loop() {
  this->__led.loop();
  this->__timer.loop();
}

/* 4 */
void Composite::onTick(Timer* sender) {
  if (this->__ledState) {
    this->__led.off();
  } else {
    this->__led.on();
  }
  this->__ledState = !this->__ledState;
}
  1. La constructeur de la classe Composite doit effectuer l'initialisation C++ de l'objet. Il prend deux paramètres :  pinLed qui est la pin de l'Arduino sur laquelle est connectée la led et delay qui correspond à l'intervalle de temps entre deux tics du timer. Ces deux paramètres sont transmis aux constructeurs Led() et Timer() des deux composants agrégés.
    Le constructeur initialise un attribut privé __ledState  pour enregistrer l'état de la led entre deux tics du timer.
    Et le plus important, le constructeur enregistre le Composite dans la liste des listeners du timer en invoquant la méthode addListener sur le timer.
  2. La méthode begin() est héritée de l'interface Component. Elle effectue l’initialisation électronique de l'objet. Il suffit pour cela  d'invoquer la méthode begin() sur tous les composants du composite.
  3. La méthode loop() est héritée de l'interface Component. Elle doit invoquer la méthode loop() de tous les composants.
  4. La méthode onTick() est héritée de l'interface TimerListener. Elle est invoquée par la méthode dispatch() dans la boucle loop() de l'objet Timer. Elle définit le traitement à effectuer à la réception des événement TimerEvent envoyés par le timer. Dans cet exemple, il s'agit de faire clignoter la led.

Utilisation de la classe Composite dans un sketch Arduino

/* 1 */
#include "Composite.h"

#define PIN_LED 6
#define DELAY 1000

/* 2 */
Composite composite(PIN_LED, DELAY);

/* 3 */
void setup() {
  composite.begin();
}

/* 4 */
void loop() {
  composite.loop();
}
  1. Le sketch de l'Arduino utilise un objet de classe Composite. La définition C++ de cet objet doit être incluse par la directive #include.
  2. Une instance de la classe Composite est déclarée en passant en paramètre du constructeur le numéro de pin de l'Arduino sur laquelle est connectée la led et l'intervalle de temps entre les tics du timer. Toute l'initialisation C++ de l'objet est effectuée à cette étape.
  3. Dans la méthode setup(), il faut procéder à l'initialisation électronique du projet (fonction pinMode() par exemple). Pour cela il suffit d'invoquer la méthode begin() sur tous les composants du projet. Ici, il n'y a qu'un seul composant de classe Composite.
  4. Dans la méthode loop() du sketch, pour que le projet fonctionne, il faut que la méthode loop() de tous les composants soit invoquée.

Conclusion

La classe Timer constitue un exemple simple de la conception d'un émetteur d'événements en utilisant la bibliothèque Event. L'usage du désign pattern Observer utilisé pour la conception de la bibliothèque Event fournit un découplage total entre le développement des émetteurs et des récepteurs.
L'usage des événements en C++ permet de simplifier la conception des projets  sur l'Arduino en posant des étapes homogènes depuis la conception du sketch principal jusqu'à la conception de chaque composant utilisé.

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