Gestion des événements en C++ sur Arduino

Lors de la conception d'applications objet, il est utile de disposer d'une bibliothèque de classes permettant de gérer les échanges d'informations entre les objets. Surtout lorsqu'il y a des interactions à prendre en charge entre le système électronique et son environnement réel. C'est d'autant plus vrai sur des applications robotiques où le système reçoit de nombreuses informations issues des capteurs qui équipent celui-ci. Malheureusement l'Arduino ne propose pas ce type de bibliothèque en natif.
Cet article propose une bibliothèque, facile à mettre en oeuvre, pour doter les objets d'un mécanisme d'échange d'événements. Seront présentés :
  • Dans un premier temps, une description complète de la conception de cette bibliothèque.
  • Puis, son utilisation pour la conception d'un objet émetteur d'événement (l'objet Timer).
  • Et enfin, l'utilisation de cet objet émetteur dans un récepteur, avec deux exemples : directement dans un programme Arduino, puis à l'intérieur d'un objet composite.

Conception de la bibliothèque Event

La bibliothèque Event est basée sur un design pattern Observer. Ce type de pattern est utilisé en Java pour implémenter la gestion des événements dans les applications graphiques. Il peut être représenté par le diagramme de classes UML ci-dessous :
  • Les classes bleues sont celles de la bibliothèque Event proprement dite. Ce sont des classe template C++, donc des classes génériques (abstraites) paramétrables par la classe de lévénement à gérer.
    • La classe EventListenable est la classe générique des émetteurs. Elle doit être dérivée par la classe émettrice et paramétrée avec la classe de l'événement émis.
    • La classe EventListener est la classe générique des récepteurs. Elle doit être dérivée par les classe destinées à recevoir les événements et paramétrée avec la classe de l'événement. En principe, elle est dérivée par une interface exposant une ou plusieurs méthodes abstraites qui doivent être implémentées dans la classe récepteur.
    • La classe EventTuple est une classe définissant un nœud qui associe un récepteur (listener) avec le nœud suivant. Cela permet d'implémenter une chaîne de récepteurs dans le cas où plusieurs récepteurs écoutent un même émetteur. Lorsque celui-ci émet un événement, il le communique que premier nœud qui le propage aux nœuds suivants.
  • Les classes vertes représentent l'implémentation des classes nécessaires à l'émission et à la réception d'un événement particulier. Dans l'exemple décrit dans le diagramme ci dessus, il s'agit d'un bouton-poussoir qui émet un événement communiquant à ses récepteurs le nouvel état du bouton lorsque celui-ci est appuyé ou relâché.
    • La classe ChangeStatusEvent est la classe qui implémente l'événement émis par le bouton lorsque celui-ci est appuyé ou relâché. Elle porte le nouvel état du bouton (attribut status). C'est cette classe qui sera utilisé dans l'implémentation concrète des classe template C++.
    • La classe Button est un exemple de classe émettrice. Dans cet exemple, la classe Button émet des événements ChangeStatusEvent. Elle doit donc dériver la classe EventListenable<ChangeStatusEvent>.
    • La classe ButtonListener définit l'interface que doit dériver toutes les classes réceptrices destinataires des événements émis par la classe Button. Elle doit dériver la classe EventLinstener<ChangeStatusEvent>.
      Elle peut exposer des méthodes abstraites (comme onChangeStatus) que doit implémenter la classe réceptrice pour traiter les informations communiquées par l'émetteur via l'événement transmis.
  • La classe rouge Composite est un exemple d'implémentation d'un classe réceptrice. Elle doit :
    • intégrer par composition le composant émetteur Button,
    • dériver la classe d'interface ButtonListener,
    • implémenter la méthode abstraite de celle-ci (onChangeStatus) pour traiter les informations communiquées par l'événement ChangeStatusEvent,
    • signaler à la construction au bouton qu'elle est destinataire des messages en invoquant la classe addListener de celui-ci.

La classe EventListener

template<class Event>
class EventListener
{
  public:
  virtual void receive(Event*) = 0;
};
  • L'instruction template indique que la classe Event utilisée dans la classe est paramétrable.
  • La méthode receive permet d'envoyer l'événement passé en paramètre à un récepteur. Cette méthode est abstraite (virtuelle pure) et doit être implémentée dans celui-ci.

La classe EventListenable

template<class Event>
class EventListenable
{
  EventTuple<Event>* __lastTuple;

  protected:
  void _dispatch(Event* event) {
    if (this->__lastTuple != NULL) {
      this->__lastTuple->peddle(event);
    } 
    delete event;
  }

  public:
  EventListenable() {  this->__lastTuple = NULL; }

  ~EventListenable() {
    if (this->__lastTuple != NULL) {
      delete this->__lastTuple;
    } 
  }

  void addListener(EventListener<Event>* listener) {
    EventTuple<Event>* tuple = new EventTuple<Event>(listener, this->__lastTuple);
    this->__lastTuple = tuple;   
  }
};
  • L'instruction template indique que la classe Event utilisée dans la classe est paramétrable.

Constructeur/Destructeur

  • EventListenable()
    Le constructeur initialise l'attribut __lastTuple à NULL. En effet, au départ la liste des récepteurs pris en charge par l'émetteur est vide.
  • ~EventListenable()
    Le destructeur détruit le dernier tuple (si la liste n'est pas vide) de la liste des récepteurs par l'invocation de l'instruction delete. En effet, la méthode addListener crée un nouveau tuple en mémoire dynamique. Le C++ ne disposant pas de ramasse-miettes, toutes les instances d'objet créées par new doivent être détruite par delete. La destruction des autres tuples de la liste est propagée automatiquement par le destructeur de la classe EventTuple.

Membre publique

  • void addListener(EventListener* listener)
    Cette méthode ajoute un nouveau récepteur dans la liste. Un nouveau tuple est créé en mémoire dynamique par l'instruction new en passant en paramètre le récepteur et la valeur de l'attribut privé __lastTuple. Ce dernier est mis à jour pour pointer sur le nouveau tuple créé.

Membre protégé

  • void _dispatch(Event* event)
    Cette méthode permet aux classes émettrices de diffuser un événement au récepteurs. L'événement est communiqué au premier tuple de la liste, chaque tuple le propageant au tuple suivant via la méthode peddle(). Une fois, la diffusion terminée, l'événement doit être détruit pas un appel à delete.
    En effet, l'objet événement a été créé par un appel à new dans l'implémentation de la classe émettrice. Il doit donc être détruit explicitement. Le fonctionnement du design pattern Observer étant synchrone, à la fin de l'exécution de la méthode _dispatch(), l'événement a bien été reçu et traité par tous les récepteurs. Il peut donc être supprimé sans problème.

Attribut privé

  • __lastTuple pointe sur le dernier tuple créé. La communication entre la classe EventListenable et les différents tuples chaînés dans la liste se passe par l'intermédiaire de ce dernier tuple créé, celui-ci se chargeant de propager l'action sur les tuples suivants dans la liste.

La classe EventTuple

La classe EventTuple ne fait pas partie du design pattern Observer à proprement parler. Cependant, en l’absence de classe de collection de type Vector en natif pour l'Arduino, elle permet d'implémenter simplement une liste chaînée des listeners qui écoutent la classe émettrice. Chaque tuple est composé de deux pointeurs, le premier, __listener, pointant sur un listener, le second, __next,  pointant sur le tuple suivant. Pour le dernier tuple de la chaîne, le second pointeur est NULL.
template <class Event>
class EventTuple {
  EventListener<Event>* __listener;
  EventTuple<Event>* __next;
  public:

  EventTuple(EventListener<Event>* listener, EventTuple<Event>* next) {
    this->__listener = listener;
    this->__next = next; 
  }

  ~EventTuple() {
    if (this->__next != NULL) {
      delete this->__next;
    }
  }

  EventListener<Event>* getListener() { return this->__listener; }

  void peddle(Event* event) {
    this->__listener->receive(event);
    if (this->__next != NULL) {
      this->__next->peddle(event);
    }
  }
};

Constructeur/Destructeur

  • EventTuple(EventListener<Event>* listener, EventTuple<Event>* next)
    Le constructeur initialise les deux attributs privés par les valeurs passées en paramètre.
    • listener est un pointeur sur une instance dérivée de EventListener.
    • next est un pointeur sur le tuple suivant. A l'enregistrement du premier listener dans la liste, celle-ci étant vide, la valeur de ce paramètre est NULL. Cela oblige la classe conteneur, dérivée de EventListenable, de maintenir un pointeur sur le dernier tuple créé (attribut __lastTuple).
  • ~EventTuple()
    Lorsque la liste doit être vidée par la classe conteneur EventListenable,  celle-ci invoque l'instruction delete sur __lastStatus. Le destructeur du dernier tuple est alors déclenché. Si le tuple pointe sur un tuple suivant, il doit le détruire explicitement par un appel explicite à l’instruction delete., propageant ainsi la destruction de tuple en tuple.
    A remarquer, que le destructeur du tuple NE DOIT PAS détruire le listener pointé.

Membres publiques

  • EventListener<Event>* getListener()
    Cette méthode fournit un accesseur en lecture seule sur le listener pointé par le tuple.
  • void peddle(Event* event)
    Cette méthode colporte l'événement event passé en paramètre en invoquant la méthode receive() du listener pointé par le tuple et le propage sur le tuple suivant de la liste.

Attributs privés

  • __listener contient l'adresse d'un listener, c'est à dire une instance d'une classe réceptrice implémentant l'interface EventListener.
  • __next contient l'adresse du tuple suivant, permettant ainsi de gérer la listes des listeners à l'écoute de l'émetteur des événements. 

Avantages et limites du design pattern Observer

L'avantage majeur du pattern Observer, c'est le découplage total entre la classe qui émet l'événement, le type d'événement et les classes réceptrices. Par exemple, une fois la classe Button développée, on peut créer autant de récepteur que l'on veut , sans modifier la classe émettrice.
Une autre illustration de ce découplage est la possibilité de créer autant de listener différent que l'on veut, pour modifier la façon d'écouter des récepteurs, des sans avoir à modifier la classe émettrice. Par exemple, outre le listener ButtonListener de la classe Button qui déclenche onChangeStatus(), on peut créer un autre listener PressButtonListener qui déclenche onPress() et même un listener LongPressListener, qui permet de distinguer une pression brève d'une pression longue du bouton en déclenchant alternativement onPress() ou onLongPress().
En revanche, le fonctionnement de ce pattern est synchrone. C'est à dire que la méthode dispatch() de l'émetteur, invoquée dans une méthode loop(), va invoquer successivement la méthode receive() de tous les récepteurs enregistrés dans la liste des listeners. De ce fait, si l'implémentation des méthodes abstraites des listeners prend beaucoup de temps, il est probables que certains événements déterminés par l'horloge soient perdus. En conséquence, l'usage d'une méthode comme delay() est à proscrire absolument. Et si des boucles longue sont nécessaires, il faudra équiper la classe réceptrice d'un mécanisme permettant de n’exécuter qu'une seule itération à la fois dans la boucle loop().
Mais cette limite est mineure pour les développeurs sur Arduino habitués à ne pas surcharger le fonctionnement de la fonction loop().

Conclusion

Ainsi se termine ce premier article consacré à la gestion des événements en C++ sur Arduino. La définition des trois classes EventListener, EventListenable et EventTuple est réunie dans un seul fichier Event.h qui doit être inséré avec la directive #include dans la définition des classes émettrices.
L'article suivant présentera la classe Timer qui émet un événement TickEvent périodiquement selon un intervalle de temps paramétrable.

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