Projet Réveil-Matin (partie VI) - Générateur de tics (classe ClockTime)

Dans l'article précédent, une classe Time avait été présentée. Cette classe permet de manipuler le concept d'heure dans les programmes C++. Mais la classe Clock nécessite également d'un générateur de tics qui permette d'afficher l'heure courante, qui change toutes les secondes, sur un dispositif d'affichage. De plus, la valeur de ce générateur de tics doit pouvoir être comparée avec des objets Time pour déclencher certaines actions particulières, comme par exemple, le signal d'alarme lorsque l'heure courante est égale à l'heure de déclenchement. Ce qui implique que la classe du générateur de tics, appelons-la ClockTime, doit dériver la classe Time pour que cette comparaison soit possible, grâce à la surcharge de l'opérateur == surchargé dans cette dernière.
L'objet de cet article est de présenter cette nouvelle classe ClockTime, telle qu'elle sera utilisée dans le projet Horloge. Comme pour la classe Time, un fichier ClockTime.h contient la définition de la classe et un fichier ClockTime.cpp contient son implémentation.
La classe ClockTime fonctionne comme la classe Timer, dans le sens ou elle émet périodiquement des événements, mais à une différence près, toutefois. Dans la classe Timer, l'origine des temps est réinitialisée à chaque fois. En effet, dans cette classe, c'est l'intervalle de temps qui est important. Alors que pour la classe ClockTime, c'est l'heure qui est importante. De ce fait, l'origine des temps reste constante et n'est modifiée éventuellement que lors du réglage de l'horloge.
Cependant, comme la classe Timer, la classe ClockTime implémente le design pattern Observer en dérivant les interfaces de la bibliothèque Event. On va donc procéder de la même façon. C'est-à-dire définir la classe ClockTimeEvent des événements émis par la classe, définir la classe ClockTimeListener que doivent dériver les récepteurs de ces événements, et notamment la classe Clock, puis enfin procéder au développement de la classe ClockTime elle-même.

Définition de la classe ClockTimeEvent

class ClockTimeEvent {
  ClockTime* __sender;
  public:
  ClockTimeEvent(ClockTime* sender){
    this->__sender = sender;
  }
  ClockTime* getSender() { return this->__sender; }
};
La classe ClockTimeEvent implémente l'événement émis par un objet de classe ClockTime. Elle ne contient qu'un attribut __sender qui est l'adresse de l'objet émetteur, lui-même exposé en lecture seule par l'accesseur getSender()

Définition de la classe ClockTimeListener

class ClockTimeListener : public EventListener<ClockTimeEvent>
{
  protected:
  virtual void receive(ClockTimeEvent* event) {
    onTimeChanged(event->getSender());
  }
  virtual void onTimeChanged(ClockTime*) = 0;
};

La classe ClockTimeListener est une classe abstraite exposant une méthode virtuelle pure (signalée par =0 dans la syntaxe du C++)  onTimeChanged(). Cette méthode doit être implémentée par héritage dans les classes réceptrice. Ce qui a été effectué dans l'implémentation de la classe Clock, cette dernière dérivant la classe ClockTimeListener.

Définition de la classe ClockTime

Contenu du fichier ClockTime.h

/* 1 */
#ifndef ClockTime_h
#define ClockTime_h

/* 2 */
#include "Component.h"
#include "Event.h"
#include "Time.h"

/* 3 */
class ClockTime;

/* 4 */
class ClockTimeEvent {
  ClockTime* __sender;
  public:
  ClockTimeEvent(ClockTime* sender){
    this->__sender = sender;
  }
  ClockTime* getSender() { return this->__sender; }
};

/* 5 */
class ClockTimeListener : public EventListener<ClockTimeEvent>
{
  protected:
  virtual void receive(ClockTimeEvent* event) {
    onTimeChanged(event->getSender());
  }
  virtual void onTimeChanged(ClockTime*) = 0;
};

/* 6 */
class ClockTime : public Component,  public Time, public EventListenable<ClockTimeEvent>
{
/* 7 */
  T_ULONG __millis0;    // Temps origine de l'horloge.

  public:
/* 8 */
  ClockTime();

/* 9 */
  virtual void begin();
  virtual void loop();
/* 10 */
  void reset(Time&);
};

/* 1 */
#endif // ClockTime_h
  1. Application de la bonne pratique de définir les classes d'une bibliothèque entre les deux directives #ifndef et #endif en prévention de multiples définitions pour les projets composites.
  2. Le fichier de définition doit inclure toutes les définitions utilisées. A savoir :
    • La classe ClockTime est architecturée comme un composant Arduino. De ce fait elle hérite de l'interface Component définie dans Component.h. Ce qui l'oblige à implémenter les méthode begin() et loop(). Voir point 9. 
    • Les instances de la classe ClockTime doivent être comparées à des instances de la classe Time avec l'opérateur == surchargé dans cette dernière. La classe ClockTime doit donc hériter de la classe Time et inclure la définition de celle-ci contenue dans Time.h.
    • La classe ClockTime doit émettre événements ClockTimeEvent. Elle doit donc hériter de la classe template EventListenable<ClockTimeEvent> définie dans Event.h.
  3. La définition de la classe ClockTime a besoin de que la classe ClockTimeEvent soit définie avant. Mais la classe ClockTimeEvent utilise la classe ClockTime pour définir l'attribut __sender. Celle-ci doit donc être connue avant la définition de la classe ClockTimeEvent. Pour résoudre ce dilemme, le langage C++ permet de déclarer unique le nom de la classe, celle-ci étant définie plus loin en extension.
  4. Définition de la classe ClockTimeEvent des événements émis pas la classe ClockTime. Voir ci-dessus.
  5. Définition de l'interface ClockTimeListener que doivent implémenter les classes réceptrices. Voir ci-dessus.
  6. Déclaration de la classe ClockTime. Comme vu dans le point 2, celle-ci hérite des classes Component, Time et EventListenable.
  7. __millis0 est l'attribut privé utilisé pour l'origine des temps. Cet attribut ne peut être modifié que par la méthode publique reset() invoquée pour procéder à un réglage de l'horloge.
  8. La classe ne possède qu'un seul constructeur ClockTime() sans paramètre.
  9. La classe ClockTime dérivant l'interface Component doit obligatoirement implémenter les méthodes begin() et loop().
  10. La méthode reset() permet de modifier l'origine des temps (attribut interne __millis0) pour régler l'horloge sur l'objet Time passé en paramètre.

Implémentation de la classe ClockTime

Constructeur de la classe ClockTime

ClockTime::ClockTime() : Time()
{
  this->__lastMillis = 0;
}

La classe ClockTime n'implémente qu'un seul constructeur sans paramètre. Celui-ci invoque le constructeur Time() de la classe de base pour initialiser les attributs internes hérités _HH, _MN et _SS, et initialise l'origine des temps __millis0 à zéro.

Méthode begin()

void ClockTime::begin() {
  this->__millis0 = millis();
}
Cette méthode est invoquée lorsque le projet Arduino commence à fonctionner. Dans le cas du projet horloge, elle est invoquée dans la méthode begin() de la classe Clock. L'attribut __millis0 est initialisé avec la valeur en millisecondes lue sur l'horloge interne par la fonction millis().

Méthode loop()

void ClockTime::loop() {
  Time newTime(millis() - this->__millis0);
  if (this->_SS != newTime.SS()) {
    this->_HH = newTime.HH();
    this->_MN = newTime.MN();
    this->_SS = newTime.SS();
    this->_dispatch(new ClockTimeEvent(this));
  }
}
Cette méthode doit être invoquée en boucle pendant le fonctionnement du projet Arduino. Dans le cas du projet Horloge, elle est invoquée dans la méthode loop() de la classe Clock. A chaque itération, une instance de la classe Time est crée sur la base de l'intervalle de temps entre le temps lu sur l'horloge interne par millis() et l'origine des temps __millis0. Si la valeur _SS a changé, alors les attributs _HH, _MN et _SS sont remis à jour et un événement ClockTimeEvent est notifié aux récepteurs en invoquant la méthode _dispatch() héritée de la classe Listenable.
A remarquer que l'instance de la classe Time créée à la première instruction suivant la première accolade ouvrante est automatiquement détruite avant la dernière accolade fermante.

Méthode reset()

Cette méthode permet une remise à l'heure de l'objet ClockTime en recalculant la valeur de l'origine des temps __millis0 à partir du paramètre newTime passé en paramètre.
void ClockTime::reset(Time& newTime) {
  this->__millis0 = millis() - (newTime.HH() * K_HOUR + newTime.MN() * K_MINUTE);
}

Test de la classe ClockTime

La classe ClockTime dérivant l'interface Component, elle peut être testée directement dans un sketch Arduino. Le sketch ci-dessous génère des tics d'horloge à partir de 18:27.
/* 1 */
#include "ClockTime.h"

/* 2 */
ClockTime time;

/* 3 */
struct : public ClockTimeListener {
  void onTimeChanged(ClockTime* time) {
    char strTime[10];
    sprintf(strTime, "%02d:%02d:%02d", time->HH(), time->MN(), time->SS());
    Serial.println(strTime); 
  }
} timeListener;

/* 4 */
void setup() {
  Serial.begin(9600);
  time.addListener(&timeListener);
  time.begin();
  Time now(18,27);
  time.reset(now);
}

/* 5 */
void loop() {
  time.loop();
}
  1. La définition de la classe ClockTime est incluse dans le sketch par une directive #include.
  2. Une instance time de la classe ClockTime est déclarée dans le sketch.
  3. En l'absence d'une classe composite, une instanciation du listener ClockTimeListener est instanciée dans une classe anonyme dans laquelle on implémente la méthode abstraite onTimeChanged() pour afficher sur le canal Série de l'Arduino la valeur de l'heure transmise au format HH:MN:SS. L'instance créée ainsi est stockée dans la variable timeListener qui constitue le récepteur sur lequel on va tester la classe ClockTime.
  4. La méthode setup() du sketch effectue les opérations suivantes :
    • Elle initialise d'abord le canal Série de l'Arduino à 9600 bauds. 
    • Elle enregistre le récepteur timeListener, instancié via une classe anonyme, dans l'instance de ClockTime par l'invocation de la méthode addListener().
    • Elle invoque la méthode begin() de l'instance ClockTime pour en démarrer le fonctionnement.
    • Elle met à l'heure l'instance ClockTime en passant une instance now de la classe Time initialisée à 18:27 à la méthode reset(). A remarquer que l'invocation de la méthode reset() doit suivre l'invocation de la méthode begin(), cette dernière réinitialisant l'origine des temps de l'instance ClockTime.
  5. La méthode loop() du sketch se contente d'invoquer la méthode loop() de l'instance de ClockTime
Le résultat obtenu doit être celui-ci :

Limite de cette implémentation de la classe ClockTime

Cette implémentation de la classe ClockTime est largement suffisante pour la réalisation du prototype du projet Horloge. Cependant, telle quelle, l'horloge ne peut pas fonctionner perpétuellement.
La faiblesse de cette implémentation réside dans la fonction millis() utilisée pour mesurer le temps. En effet, le résultat de cette fonction est de type unsigned long, c'est-à-dire un entier long non signé codé sur quatre octets. Sa valeur maximum est donc de 4 294 967 295. Converti en jours, heures, minutes et seconde, cette valeur est de 49 j 17 h 8 mn 47,295 s. Que se passe-t-il lorsque cette valeur est atteinte ? Et bien, dans le cas où l'horloge ait été démarrée à minuit, le cinquantième jour, à 17:08;47, l'heure affichée passera brutalement à 00:00:00. Ce qui nécessitera une remise à l'heure de l'horloge.

Conclusion

La classe Clock dispose maintenant d'un générateur de tics permettant l'affichage correcte de l'heure pendant une cinquantaine de jour. Cette solution limitée peut paraître médiocre. Mais elle est suffisante pour tester le prototype du projet Horloge. Un article futur proposera une solution plus satisfaisante permettant de dépasser le seuil de cinquante jours en utilisant la classe DoubleLong permettant de gérer des grands nombres sur 64 bits. Cette solution est basée sur une astuce de programmation connue par les développeurs d'applications scientifiques pour l'astronomie, pour lesquelles il faut pouvoir manipuler des grands nombres dépassant les capacités des types standards entiers ou virgule flottante des langages de programmation.
L'étape d'après est de développer un système d'affichage pour le projet Horloge. Ce système d'affichage dépend des composants électroniques utilisés. L'article suivant traitera du développement d'un système d'affichage pour la classe Clock.

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