Projet Réveil-Matin (partie VII) - Gestion de l'affichage de l'horloge (classe ClockDisplay)

Dans l'article traitant de l'implémentation de la classe Clock, le besoin d'une classe implémentant le système d'affichage avait été mise en évidence. La classe utilisée avait été appelée ClockDisplay. Et elle devait exposer trois méthodes dans son interface publique :
  • changeTime() par laquelle le dispositif d'affichage est notifié du changement de l'affichage de l'heure.
  • changeStatus() par laquelle le dispositif d'affichage est notifié du changement d'état de l'horloge et adapter l'affiche en fonction.
  • changeAlarm() par laquelle le dispositif d'affichage est notifié du changement d'activation/désactivation de l'alarme (pour afficher une petite cloche sur l'écran LCD lorsque l'alarme est activée par exemple). 
Mais le développement de la classe d'un système d'affichage dépend étroitement des composants électroniques utilisés. Afin d'assurer le découplage entre la classe relative au système d'affichage et la classe Clock relative à l'horloge, on vas créer une couche d'abstraction intermédiaire sous la forme d'une classe abstraite que l'on a déjà nommée ClockDisplay qui va exposer dans son interface publique le trois méthodes nécessaires. Cette classe abstraite pourra alors être dérivées pour gérer des systèmes d'affichages basé sur des composants divers comme un écran LCD ou un affichage sur des LED 7 segments. D'ailleurs, une classe concrète SerialClockDisplay sera présentée dans cet article, pour pouvoir tester la logique du système d'affichage et fournir une approche méthodologique pour le développement d'autres système d'affichage.

Définition de la classe abstraite ClockDisplay

/* 1 */
#ifndef ClockDisplay_h
#define ClockDisplay_h

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

/* 3 */
enum ClockStatus { HOUR_DISPLAY, CLOCK_SETUP, ALARM_SETUP, HOUR_SETUP, MINUTE_SETUP, ALARM_HOUR_SETUP, ALARM_MINUTE_SETUP, ALARM_ACTIVE };

/* 4 */
class ClockDisplay : Component, TimerListener {
/* 5 */
  static const int BLINK_FREQUENCY = 500;
/* 6 */
  Timer __blinkTimer;
  bool __blinkStatus;

/* 7 */
  void onTick(Timer*);

  protected:
/* 8 */
  Time _time;
  ClockStatus _clockStatus;
  bool _alarmStatus;

/* 9 */
  virtual void _displayTime()=0;
  virtual void _displayStatus()=0;
  virtual void _displayAlarm()=0;
  virtual void _doBlink(bool)=0;

  public:
/* 10 */
  ClockDisplay();
/* 11 */
  virtual void begin();
  virtual void loop();
/* 12 */
  virtual void changeTime(Time&);
  virtual void changeStatus(ClockStatus);
  virtual void changeAlarm(bool);
};

/* 1 */
#endif // ClockDisplay_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. Inclusion des définitions des classes des composants utilisés :
    • Timer.h, car la classe ClockDisplay utilise le composant Timer pour gérer le clignotement.
    • Time.h, car la classe ClockDisplay dérive la classe Time pour gérer l'heure.
  3. Il est nécessaire de créer un type de données ClockStatus pour implémenter l'état de l'horloge. Ce type de données est défini comme un enum C++ qui peut prendre les valeurs HOUR_DISPLAY, CLOCK_SETUP, ALARM_SETUP, HOUR_SETUP, MINUTE_SETUP, ALARM_HOUR_SETUP, ALARM_MINUTE_SETUP et ALARM_ACTIVE. Ces valeurs correspondent aux huit états de l'automate de l'horloge. Ce type de données est également utilisé pour l'attribut privé __clockStatus de la classe Clock
  4. Le dispositif d'affichage est un composant Arduino. Il doit implémenter l'interface Component. Ce qui l'oblige à implémenter les méthodes begin() et loop(). Elle utilise un composant Timer pour gérer le clignotement. Elle doit donc implémenter l'interface TimerListener. L’implémentation de la méthode onTick() n'est pas nécessaire dans la classe ClockDisplay, puisque celle-ci est une classe abstraite. En revanche, l'implémentation de cette méthode sera indispensable dans les classe concrètes dérivées pour pouvoir être instanciées.
  5. Un constante de classe BLINK_FREQUENCY est initialisée arbitrairement à 500 millisecondes.
  6. Deux attributs privés sont défini dans la classe ClockDisplay pour gérer le clignotement :
    • __blinkTimer est un Timer qui va générer de tics.
    • __blinkStatus est booléen pour gérer l'affichage clignotant. Si l'attribut vaut true, l'heure est affichée, s'il vaut false, l'heure est effacée. La fréquence de clignotement est fixée par la constante BLINK_FREQUENCY.
  7. La méthode onTick() est héritée de l'interface TimerListener. Elle est invoquée à la fréquence fixée pas la constante BLINK_FREQUENCY. A chaque tic reçu par le dispositif d'affichage, l'heure est alternativement affichée ou effacée.
  8. Dans l'interface protégée de la classe sont déclarés les attributs relatifs à l'horloge. Ils sont déclaré dans l'interface protégée car ils doivent être exploitables par les classes concrètes dérivées de ClockDisplay.
    • _time est un objet Time correspondant à la valeur de l'heure affichée. Il faut rappeler ici que ce peut être soit l'heure courante, soit l'heure en cours de réglage, soit l'heure de déclenchement de l'alarme, en fonction de l'état de l'horloge et de la valeur communiquée au dispositif d'affichage par la méthode publique changeTime().
    • _clockStatus est l'état de l'horloge (type ClockStatus). L'affichage peut différer en fonction de cet état. Par exemple, sur un écran LCD, l'heure courante est affichée au format HH:MN:SS alors que l'heure de réglage n'est affichée qu'au HH:MN.
    • _alarmStatus est l'état d'activation de l'alarme. Il ne faut pas confondre l'état d'activation de l'alarme qui signifie que l'alarme pourra se déclencher si l'heure courant est égale à l'heure de déclenchement et l'état indiquant que le signal sonore est déclenché qui lui est porté par l'attribut _clockStatus lorsqu'il a la valeur ALARME_ACTIVE.
  9. La classe abstraite ClockDisplay déclare quatre méthodes abstraites virtuelles pures dans l'interface protégée de la classe. Ces méthodes définissent comment sont affichées le différentes données. Elles doivent être implémentées dans les classes concrètes dérivées en fonction des composants électronique utilisés par le dispositif d'affichage.
    • _displayTime() définit comment est affichée l'heure.
    • _displayStatus() définit comment est affiché l'état de l'horloge.
    • _displayAlarm() définit comment est signalé que l'alarme est activée. Par exemple, sur l'écran LCD, l'activation de l'alarme est signalée par une petite cloche sur le coin en haut à droite.
    • _doBlink() définit comment s'effectue le clignotement.
  10. La classe abstraite ClockDisplay ne possède qu'un seul constructeur sans paramètre. En revanche, les classes dérivées devront disposer du constructeur permettant de passer tous les numéros de pins de l'Arduino utilisés par les composants électroniques concernés.  
  11. La classe ClockDisplay dérivant l'interface Component, elle implémente les méthodes begin() et loop() pour respectivement initialiser le Timer et gérer le clignotement. 
  12. Puis suit la déclaration des trois méthodes publiques exigées par la classe Clock. Il y a un distinction importante à remarquer entre ces trois méthodes est les quatre méthodes abstraites déclarée dans le point 9. Ces dernières définissent COMMENT s'effectue l'affichage.Ce qui dépend évidemment des composants électroniques utilisés. Celles-ci ne peuvent donc être implémentées quand dans les classes concrètes adaptée à ces composants. Alors que les trois méthodes publiques déclarées ici définissent CE QUI DOIT ETRE AFFICHE et QUAND cela doit ce produire. Ce qui est déterminé par la classe Clock.  Elle sont donc implémentée dans la classe abstraite ClockDisplay et invoqueront les méthodes du point 9 de façon appropriée. 

Implémentation de la classe ClockDisplay

Constructeur de la classe ClockDisplay

ClockDisplay::ClockDisplay()
  : __blinkTimer(BLINK_FREQUENCY)
{
  this->_clockStatus = HOUR_DISPLAY;
  this->_alarmStatus = false;
  this->__blinkTimer.addListener(this);
}
La classe ClockDisplay ne dispose que d'un seul constructeur sans paramètre. Ce constructeur commence par invoquer le constructeur du Timer en passant en paramètre BLINK_FREQUENCY dont la valeur a été fixée à 500 millisecondes. Puis il initialise les deux attributs privés _clockStatus et _alarmStatus respectivement à HOUR_DISPLAY, qui est l'état par défaut de l'horloge, et à false, indiquant que l'alarme n'a pas encore été activée. Enfin, le dispositif d'affichage d'enregistre auprès du Timer pour recevoir les événements TimerEvent en invoquant a méthode addListener() sur l'attribut __blinkTimer.

Méthode begin()

void ClockDisplay::begin() {
  this->__blinkTimer.begin();
}
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. La classe ClockDisplay propage l'initialisation en invoquant la méthode begin() de ses composants, ici l'attribut __blinkTimer de classe Timer.

Méthode loop()

void ClockDisplay::loop() {
  this->__blinkTimer.loop();
}
Cette méthode est invoquée en boucle pendant le fonctionnement du projet Arduino. Dans le cas du projet Horloge, elle est invoquée par la méthode loop() de la classe Clock et propagée sur l'attribut __blickTimer. Celui-ci est un Timer initialisé par la constante BLINK_FREQUENCY. Ce qui déclenche l'invocation de la méthode onTick() toutes les 500 millisecondes.

Méthode onTick()

void ClockDisplay::onTick(Timer* sender) {
  this->__blinkStatus = !this->__blinkStatus;
  this->_doBlink(this->__blinkStatus);
}
La méthode onTick() est invoquée par le Timer toutes les 500 millisecondes (valeur de la constante BLINK_FREQUENCY). Après avoir basculé l'attribut privé booléen __blinkStatus, elle invoque la méthode abstraite _doBlink(). Cette dernière doit être implémentée dans les classes concrètes dérivées pour définir comment le clignotement est pris en charge en fonction des composants électroniques utilisés pour l'affichage.

Méthode changeTime()

void ClockDisplay::changeTime(Time& time) {
  this->_time = time;
  this->_displayTime();
}
Cette méthode est invoquée à chaque fois qu'il faut modifier l'heure affichée. Le paramètre time passé est archivé dans l'attribut protégé _time. Puis la méthode abstraite _displayTime() est invoquée. Cette dernière doit être implémentée dans les classes concrètes dérivées pour définir comment cette nouvelle valeur de l'heure doit être affichée en fonction des composants électroniques utilisés. Dans le cas du projet Horloge, elle est invoquée par la classe Clock, et l'heure affichée peut être l'heure courante, l'heure de réglage ou l'heure de déclenchement de l'alarme en fonction de l'état de l'horloge.

Méthode changeStatus()

void ClockDisplay::changeStatus(ClockStatus newStatus) {
  this->_clockStatus = newStatus;
  this->_displayStatus();
}
Cette méthode est invoquée à chaque fois que l'état de l'horloge est modifié. Le paramètre newStatus passé est archivé dans l'attribut protégé _clockStatus. Puis la méthode abstraite _displayStatus() est invoquée. Cette dernière doit être implémentée dans les classes concrètes dérivées pour définir comment l'état  doit être affiché en fonction des composants électroniques utilisés. Dans le cas du projet Horloge, elle est invoquée par la classe Clock à chaque changement d'état de celle-ci.

Méthode changeAlarm()

void ClockDisplay::changeAlarm(bool newStatus) {
  this->_alarmStatus = newStatus;
  this->_displayAlarm();
}
Cette méthode invoquée lorsque l'alarme de l'horloge est activée ou désactivée. Le paramètre booléen newStatus passé est archivé dans l'attribut protégé _alarmStatus. Puis la méthode abstraite _displayAlarm() est invoquée. Cette dernière doit être implémentée dans les classes concrètes dérivées comment l'état d'activation de l'alarme doit être affiché en fonction des composants électroniques utilisés.

Exemple d'implémentation d'une classe concrète de dispositif d'affichage : la classe SerialClockDisplay

Avant d'aborder l'affichage sur un écran LCD qui nécessite quelques explications techniques, voici un exemple simple d'implémentation d'un classe concrète pour le dispositif d'affichage qui ne fait intervenir aucun élément électronique particulier. En effet, la classe SerialClockDisplay utilise le canal Série de l'Arduino pour afficher toutes les fonctions de l'horloge. Ce qui permet enfin de tester la logique du projet Horloge dans son ensemble.

Définition de la classe SerialClockDisplay

#include "ClockDisplay.h"

/* 1 */
class SerialClockDisplay : public ClockDisplay {
/* 2 */
  static const char *LABELS[];

/* 3 */
  protected:
  virtual void _displayTime();
  virtual void _displayStatus();
  virtual void _displayAlarm();
  virtual void _doBlink(bool);

/* 4 */
  public:
  SerialClockDisplay();
/* 5 */
  virtual void begin();
};
  1. La classe SerialClockDisplay dérive la classe ClockDisplay. Elle hérite donc des trois méthodes publiques changeTime(), changeStatus() et changeAlarm() invoquées par la classe Clock. En revanche, elle doit implémenter les quatre méthodes abstraites déclarées dans la classe de base.
  2. LABELS est un tableau de chaînes de caractères indicé par rapport à l'état de l'horloge. Il permet de rassembler tous les affichages dans une seule entité. Il est défini comme une constante de classe (static const). 
  3. Les quatre méthodes héritées de la classe  abstraite parente ClockDisplay, doivent être implémentées pour que la classe SerialClockDisplay soit instanciable.
  4. L'interface publique de la classe concrète doit implémenter un constructeur. La classe SerialClockDisplay n'utilise aucun composant électronique particulier utilisant des pins de l'Arduino. Il n'y a donc aucun paramètre à passer au constructeur.
  5. Le canal Série de l'Arduino doit être initialisé pour pouvoir être utilisé. La méthode begin() est implémentée dans la classe SerialClockDisplay à cet effet. En revanche, l'implémentation de la méthode loop() est prise en charge dans la classe de base ClockDisplay et ne nécessite aucune implémentation  particulière dans les classes dérivées.

Implémentation de la classe SerialClockDisplay

L'implémentation d'une classe concrète pour un dispositif d'affichage de l'horloge consiste à :
  • Créer un constructeur pour la classe concrète. Celui-ci doit permettre de recevoir en paramètres les numéros des pins de l'Arduino utilisées par les composants électroniques qui composent le dispositif d'affichage.
  • Surcharger la méthode begin() pour initialiser les composants électroniques du dispositif d'affichage et notamment définir les pins de l'Arduino en lecture ou en écriture par l'invocation de la  fonction pinMode(). ou éventuellement invoquer la méthode begin() des composants utilisés.
  • Implémenter les quatre méthodes abstraites de la classe ClockDisplay pour définir comment sont afficher les différents éléments de l'horloge.
#include "SerialClockDisplay.h"

/* 1 */
const char *SerialClockDisplay::LABELS[] = {
  "Mode normal",
  "Mise l'heure",
  "Réglage Alarme",
  "Réglage Heures",
  "Réglage Minutes",
  "Réglage Alarme Heures",
  "Réglage Alarme Minutes",
  "Alarme activée"
} ;

/* 2 */
SerialClockDisplay::SerialClockDisplay()
  : ClockDisplay()
{
}

/* 3 */
void SerialClockDisplay::begin() {
  ClockDisplay::begin();
  Serial.begin(9600);
  Serial.println("Initialisation de SerialCLockDisplay");
}

/* 4 */
void SerialClockDisplay::_displayTime() {
  char strTime[10];
  switch (this->_clockStatus) {
    case HOUR_DISPLAY:
    case ALARM_ACTIVE:
      sprintf(strTime, "%02d:%02d:%02d", this->_time.HH(), this->_time.MN(), this->_time.SS());
      break;
    default:
      sprintf(strTime, "%02d:%02d", this->_time.HH(), this->_time.MN());
      break;
  }
  Serial.println(strTime);
}

/* 5 */
void SerialClockDisplay::_displayStatus() {
  Serial.print("Status : ");
  Serial.println(LABELS[(int)this->_clockStatus]);
}

/* 6 */
void SerialClockDisplay::_displayAlarm() {
  Serial.print("Alarme ");
  Serial.println(this->_alarmStatus ? "ON" : "OFF");
}

/* 7 */
void SerialClockDisplay::_doBlink(bool) {}
  1. Le tableau de chaînes de caractère LABELS, déclaré dans la définition de la classe SerialClockDisplay dans le fichier SerialClockDisplay.h, doit être implémenté en extension dans le fichier SerialClockDisplay.cpp. Pour chaque état de l'énumération ClockStatus, il défini une chaîne de caractères qui peut être affichée sur le dispositif d'affichage.
  2. La classe SerialClockDisplay n'utilise aucune pin de l'Arduino. De ce fait, il n'y a aucun paramètre à initialiser dans le constructeur. La seule chose à faire est d'invoquer le constructeur de la classe parente ClockDisplay.
  3. Dans la méthode begin(), on commence par invoquer la méthode begin() de la classe de base ClockDisplay. Puis on initialise le canal Série de l'Arduino à 9600 bauds en invoquant la méthode begin(9600) de l'objet Serial qui implémente ce canal. On peut alors afficher la mention "Initialisation de SerialClockDisplay" ou n'importe quel autre texte à volonté en invoquant la méthode println().
  4. La méthode _displayTime doit afficher l'heure dans la valeur est contenue dans l'attribut protégé _time. L'affichage est mis en forme dans une variable locale strTime en utilisant la fonction C++ sprintf() pour la mise en forme. Le contenu de la variable strTime est passé en paramètre de la méthode println() de l'objet Serial. Il y a deux modes d'affichage :
    • HH:MN:SS lorsque l'horloge affiche l'heure courante. C'est-à-dire lorsque l'attribut _clockStatus vaut HOUR_DISPLAY ou ALARM_ACTIVE, états correspondant respectivement lorsque l'horloge est en mode normal ou lorsque le signal de l'alarme est émis.
    • HH:MN lorsque l'horloge est en mode réglage. C'est-à-dire dans tous les états autres que le deux états mentionnés précédemment. 
  5. La méthode _displayStatus() doit afficher l'état dans lequel se trouve l'horloge. Ici, elle affiche le libellé du tableau LABELS correspondant à l'état de l'horloge contenu dans l'attribut _alarmStatus.
  6. La méthode _displayAlarm() doit afficher si l'alarme est activée ou non. Ici, elle affiche ON si l'alarme est activée ou OFF sinon.
  7. Le clignotement n'est pas utilisé sur le canal Série de l'Arduino. Néanmoins, la méthode abstraite _doBlink() doit être instanciée pour que la classe SerialClockDisplay puisse être instanciée. Seulement ici, elle ne fait rien.

Utilisation de la classe SerialClockDisplay dans un sketch Arduino

#include "SerialClockDisplay.h"
#include "ClockAlarm.h"
#include "Clock.h"


// Déclaration des pins de connexion des boutons de commande.
#define BTN_CMD 5
#define BTN_UP 3
#define BTN_DOWN 4
// Déclaration de la pin de connexion du buzzer passif.
#define BUZZER 2

SerialClockDisplay clockDisplay;
BuzzerClockAlarm clockAlarm(BUZZER);
Clock myclock(clockDisplay, clockAlarm, BTN_CMD, BTN_UP, BTN_DOWN);


void setup() {
  myclock.begin();
}

void loop() {
  myclock.loop();
}
L'utilisation de la classe SerialClockDisplay dans un sketch Arduino est relativement simple. Elle consiste à déclarer une instance clockDisplay de cette classe et de la passer comme premier paramètre du constructeur de la classe Clock lors de l'instanciation de myclock.
Dès lors,l'horloge se met à fonctionner et on peut constater son fonctionnement sur le canal Série de l'Arduino.
Séquence de mise à l'heure
00:00:43
00:00:44
00:00:45
00:00:46
Status : Mise à l'heure
00:00
Status : Réglage Heures
00:00
01:00
02:00
03:00
. . .
16:00
17:00
18:00
19:00
18:00
Status : Réglage Minutes
18:00
18:01
18:02
18:03
. . .
18:21
18:22
18:23
18:24
18:25
18:24
18:23
Status : Mode normal
18:23:00
18:23:01
18:23:02

Séquence de réglage de l'alarme
18:29:39
18:29:40
Status : Mise l'heure
18:29
Status : Réglage Alarme
00:00
Status : Réglage Alarme Heures
00:00
23:00
22:00
21:00
20:00
19:00
18:00
Status : Réglage Alarme Minutes
18:00
18:01
18:02
. . .
18:31
18:32
Status : Mode normal
18:30:14
18:30:15
18:30:16

Séquence d'activation de l'alarme
18:30:24
18:30:25
18:30:26
18:30:27
Alarme ON
18:30:28
18:30:29
18:30:30
18:30:31

Séquence de déclenchement et d'arrêt du signal d'alarme
18:31:58
18:31:59
18:32:00
ClockAlarm::On
Status : Alarme activée
18:32:01
18:32:02
18:32:03
. . .
18:32:20
18:32:21
18:32:22
Status : Mode normal
18:32:23
18:32:24
18:32:25

Conclusion

Nous disposons maintenant d'un dispositif d'affichage opérationnel implémenté dans la classe concrète SerialClockDisplay. Ce qui nous a permis de valider le bon fonctionnement de la logique de l'automate de l'horloge. L’achèvement du projet passera par le développement d'une classe LCDClockDisplay permettant l'affichage de l'horloge sur un module LCD 1602 Elegoo. Ce qui fera l'objet d'un article ultérieur.
Mais auparavant, il va nous falloir disposer d'un dispositif d'alarme. Pour cela, nous allons procéder comme pour le dispositif d'affichage, à savoir définir une classe abstraite ClockAlarm pour ce dispositif, que l'on dérivera pour implémenter une classe concrète BuzzerClockAlarm utilisant un buzzer passif pour émettre le signal d'alarme. Ce fera l'objet du prochain article.

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