Projet Réveil-Matin (partie V) - Gestion de l'heure (classe Time)

Dans un projet d'horloge, il est évident que la donnée de basse à gérer est l'heure. Il faudrait donc disposer d'un type de données, appelons-le Time, permettant de manipuler ce concept. Or, ce type de donnée n'existe pas en natif pour l'Arduino. De plus, dans le dernier article traitant de l'implémentation de la classe Clock du projet horloge, il avait été mis en évidence la nécessité d'une classe Time dotée de trois propriétés HH, MN et SS, pour implémenter l'heure,  ainsi que d'une interface publique exposant les méthodes incHH(), incMN(), decHH() et decMN() pour procéder aux réglages.
L'objet de cet article est de présenter cette classe Time, telle qu'elle sera utilisée dans le projet Horloge. Comme pour toute classe C++,  la développement nécessite un fichier .h de définition et un fichier .cpp pour l'implémentation. La définition de la classe Time est contenue dans le fichier Time.h, et son implémentation dans le fichier Time.cpp.

Définition de la classe Time (fichier Time.h)

/* 1 */
#ifndef Time_h
#define Time_h

/* 2 */
#define T_UCHAR unsigned char
#define T_UINT unsigned int
#define T_ULONG unsigned long

class Time {
/* 3 */
  void __incValue(T_UCHAR&, T_UCHAR);
  void __decValue(T_UCHAR&, T_UCHAR);

/* 4 */
  protected:
  T_UCHAR _HH;
  T_UCHAR _MN;
  T_UCHAR _SS;

  public:
/* 5 */
  static const T_UCHAR MODULO_HH = 24;
  static const T_UCHAR MODULO_MN = 60;
  static const T_UCHAR MODULO_SS = 60;
/* 6 */
  static const T_ULONG K_SECOND = 1000L;
  static const T_ULONG K_MINUTE = K_SECOND * MODULO_SS;
  static const T_ULONG K_HOUR = K_MINUTE * MODULO_MN;
  static const T_ULONG K_DAY = K_HOUR * MODULO_HH;

/* 7 */
  Time(T_UCHAR=0, T_UCHAR=0,T_UCHAR=0);
  Time(T_ULONG);
/* 8 */
  T_UCHAR HH() { return this->_HH; }
  T_UCHAR MN() { return this->_MN; }
  T_UCHAR SS() { return this->_SS; }
/* 9 */
  void setMillis(T_ULONG);
  void incHH() { this->__incValue(this->_HH, MODULO_HH); }
  void incMN() { this->__incValue(this->_MN, MODULO_MN); }
  void decHH() { this->__decValue(this->_HH, MODULO_HH); }
  void decMN() { this->__decValue(this->_MN, MODULO_MN); }
/* 10 */
  bool operator==(Time&);
};

/* 1 */
#endif // Time_h
  1. Comme pour tous les fichiers de définition, le couple de directives #ifndef et #endif permet d'éviter les définitions multiples d'une classe lorsque celle-ci est utilisées dans des projets mutlti-fichiers en définissant une macro Time_h.
  2. La classe utilise plusieurs type de données entières unsigned. La définition de macros pour ces types de données permet de soulager la syntaxe. Elle sont préfixées T_ (T comme  Time) pour éviter des conflits avec des macros équivalentes présentes dans d'autres bibliothèques.
    • T_UCHAR définit un entier non signé sur un octet (0..255).
    • T_UINT définit un entier non signé sur deux octets (0..65 535).
    • T_ULONG définit un entier non signé sur quatre octets (0..4 294 967 295).
  3. Dans l'interface privée de la classe Time, deux méthodes permettent respectivement d'incrémenter ou de décrémenter les attributs _HH, _MN, _SS de la classe.
    • incValue() permet d'incrémenter un attribut sans qu'il dépasse sa valeur de modulo.
    • decValue() permet de décrémenter un attribut sans qu'il dépasse son modulo.
  4. Les trois attributs constituant les données de base de la classe sont placés dans l'interface protégée de la classe. En effet, dans le projet horloge, il est prévu de dériver cette classe pour créer la classe ClockTime destinée à générer les tics de l'horloge.Par convention, les attributs protégés d'une classe sont préfixés avec un seul caractère souligné. (Il est rappelé aussi que les attributs privés sont eux préfixés par deux caractères soulignés.). Ces attribut sont codés sur un seul octet.
    • _HH contient la valeur des heures (0..23).
    • _MN contient la valeur des minutes (0..59).
    • _SS contient la valeur de secondes (0..59).
La définition de la classe se poursuit par la déclaration de plusieurs constantes de classes déclarées static utilisées pour les calculs.
  1. Trois constantes, codées sur un seul octet, paramètrent les modulos des attributs pour une journée de 24 heures, une heure de soixante minutes et une minute de soixante secondes  :
    • MODULO_HH, dont la valeur est 24, définit le modulo de l'attribut _HH.
    • MODULO_MN, dont la valeur est 60, définit le modulo de l'attribut _MN.
    • MODULO_SS, dont la valeur est 60 également, définit le modulo de l'attribut _SS.
  2. Quatre constantes, codées sur quatre octets, paramètrent la valeur en millisecondes de l'unité de chaque attribut :
    • K_SECOND vaut 1000 millisecondes pour une seconde.
    • K_MINUTE vaut 60 000 millisecondes pour une minute.
    • K_HOUR vaut 3 600 000 millisecondes pour une heure.
    • K_DAY vaut 86 400 000 millisecondes pour un jour.
La définition de la classe Time se termine par les méthodes de l'interface publique :
  1. La classe Time possède deux constructeur :
    • Time(T_UCHAR=0, T_UCHAR=0,T_UCHAR=0)
      Ce constructeur permet d'initialiser un objet Time à partir des trois valeurs HH, MN et SS passées en paramètre. Si les valeurs passées ne sont pas compatibles avec leur modulo respectif, l'attribut est initialisé à 0.
    • Time(T_ULONG)
      Ce constructeur permet d'initialiser un objet Time par une valeur en milliseconde passée en paramètre. Moyennant, leur modulo respectif, les attributs _HH, _MN et _SS sont calculés à partir de cette valeur.
  2. Les trois méthodes suivantes sont les accesseurs publiques en lecture seule des trois attributs protégés. Cette manière de faire empêche les programmeurs qui vont utiliser la classe Time de modifier les attributs internes autrement que par les modifieurs prévus à cet effet. D'autant que, pour ces attributs aient une valeur cohérente, ils sont soumis à des modulos.  Les trois méthodes sont directement implémentées dans le fichier Time.h :
    • HH() retourne la valeur de l'attribut _HH.
    • MN() retourne la valeur de l'attribut _MN.
    • SS() retourne la valeur de l'attribut _SS.
  3. Puis viennent les modifieurs de la classe Time. C'est-à-dire les méthodes capables de modifier les attributs internes _HH, _MN et _SS de l'extérieur, tout en assurant leur cohérences en respectant leurs modulos respectif : _HH doit être compris entre 0 et 23, _MN et _SS doivent être compris entre 0 et 59.
    • setMillis() permet de modifier _HH, _MN et _SS à partir d'une valeur entière sur quatre octets correspondant à un nombre de millisecondes. Cette méthode est elle-même invoqué par l'un des constructeur de la classe Time. Ce qui permet de construire une instance de celle-ci à partir d'une valeur lue par la fonction millis() de l'Arduino.
    • incHH() permet d'incrémenter _HH. Elle invoque la méthode privée __incValue() pour respecter le modulo de 24 en garantissant que _HH reste compris entre 0 et 23. Cet méthode, comme les suivantes, est utilisée pour les réglages de l'horloge.
    • incMN() permet d'incrémenter _MN en respectant le modulo de 60 pour que _MN reste compris entre 0 et 59.
    • decHH() permet de décrémenter _HH. Elle invoque la méthode privée __decValue() pour respecter le modulo de 24 en garantissant que _HH reste compris entre 0 et 23. 
    • decMN() permet de décrémenter _MN en respectant le modulo de 60 pour que _MN reste compris entre 0 et 59.
  4. La classe Time a besoin de comparer deux instances pour vérifier que la valeur de l'heure est la même pour chacune d'elles (notamment pour pouvoir déclencher le signal d'alarme lorsque l'heure courante est égale à l'heure de déclenchement).L'utilisation de l'opérateur == est délicate. En effet, celui-ci retournera false (faux) pour deux instances différentes de la classe Time, même si leurs attributs internes _HH, _MN et _SS ont la même valeur (et true dans le cas improbable où il s'agisse de la même instance).
    Heureusement, le C++ permet la surcharge des opérateurs pour permettre un fonctionnement spécifique pour ceux-ci.
    L'opérateur == surchargé dans la classe Time, renvoie true (vrai), si les attributs _HH, _MN et _SS ont respectivement la même valeur dans les deux instances passées en opérandes et false (faux) si l'une au moins est différente.

Implémentation de la classe Time

Les méthodes déclarées dans le fichier Time.h doivent être implémentées dans un fichier Time.cpp.

Constructeurs de la classe Time

Time::Time(T_UCHAR hh, T_UCHAR mn, T_UCHAR ss) {
  if (hh < MODULO_HH) this->_HH = hh;
  if (mn < MODULO_MN) this->_MN = mn;
  if (ss < MODULO_SS) this->_SS = ss;
}
Ce constructeur permet d'initialiser une instance de Time sur la base des trois paramètres hh, mn et ss. Ces paramètres sont soumis à des modulos (24 pour _HH et 60 pour _MN et _SS). Ces modulos sont contrôlés par le constructeurs. S'il ne sont pas respecté, c'et leur valeur 0 par défaut qui est utilisée.
Pour être rigoureux en programmation objet, il faudrait empêcher la construction en déclenchant une exception avec l'instruction throw lorsqu'un paramètre ne répond pas aux conditions de cohérence imposées par les spécification de la classe. Malheureusement, les exceptions C++ ne sont pas supportées par le compilateur de l'Arduino (celui-ci est lancé avec l'option  -fno-exceptions). Dans un sens, ce n'est pas plus mal, car les exceptions sont assez délicates à gérer correctement en C++. En effet, ce langage ne dispose pas de garbage collector (ramasse-miette). L'instruction catch effectuant un goto inconditionnel, cela peut provoquer des  fuites de mémoire si toutes les instances créées dynamiquement par  new ne sont pas effectivement détruites par delete.
Time::Time(T_ULONG millisTime) {
  this->setMillis(millisTime);
}
Ce constructeur permet d'initialiser une instance Time  à partir d'une valeur exprimée en millisecondes passée en paramètre. Cette valeur est convertie en heures, minutes et secondes, par la méthode publique setMillis().

Méthode setMillis()

void Time::setMillis(T_ULONG millisTime) {
  millisTime %= K_DAY;
  this->_HH = millisTime / K_HOUR;
  millisTime %= K_HOUR;
  this->_MN = millisTime / K_MINUTE;
  millisTime %=  K_MINUTE;
  this->_SS = millisTime / K_SECOND;
}
Cette méthode permet d'initialiser les attributs privés _HH, _MN et _SS à partir d'une valeur exprimée en millisecondes. Elle est elle-même invoquée dans le constructeur de la classe Time.

Méthodes privées de la classe Time

void Time::__incValue(T_UCHAR &value, T_UCHAR modulo) {
  value++;
  if (value >= modulo) value = 0;
}
Cette méthode permet d'incrémenter le premier paramètre value passé par référence (avec le caractère &) moyennant une valeur de modulo passée en second paramètre. Lorsque la valeur dépasse le modulo, elle repasse à 0. Cette méthode privée est utilisé par les méthodes publiques incHH() et incMN()
void Time::__decValue(T_UCHAR &value, T_UCHAR modulo) {
  value = ((value == 0) ? modulo : value) - 1;
}
Cette méthode permet de décrémenter le premier paramètre value passé par référence (avec le caractère &) moyennant une valeur de modulo passée en second paramètre. Lorsque la valeur arrive à 0, elle repart à partir de modulo. Cette méthode privée est utilisé par les méthodes publiques decHH() et decMN().

Surcharge de l'opérateur ==

Lors de l'implémentation de la classe Clock, on a constaté le besoin de pouvoir comparer les valeurs de deux instances différente de la classe Time, pour pouvoir déclencher le signal sonore de l'alarme, par exemple, lorsque l'heure de déclenchement de celle-ci est égale à l'heure courante. Or, par défaut, l'opérateur == teste si deux expressions font référence à la même instance (ce qui est peu probable) et non si les valeurs respectives de ces deux instances sont les mêmes.
Ce dont on a besoin ici, c'est de tester si les attributs internes _HH, _MN et _SS sont respectivement égaux ans les deux expressions.
Heureusement, le langage C++ dispose d'une syntaxe pour la surcharge des opérateurs. Ce qui permet de modifier le fonctionnement standard des opérateurs.
bool Time::operator==(Time& time) {
  return this->_SS == time._SS && this->_MN == time._MN && this->_HH == time._HH;
}
En C++ les opérateurs, à part la syntaxe particulière utilisant l'instruction operator précédant le symbole de l'opérateur, fonctionnent comme des méthodes. A savoir qu'elle reçoivent des paramètres et fournissent un résultat. Les paramètres d'un opérateur s'appellent opérandes. Certains opérateurs nécessitent deux opérandes comme le + pour l'addition. D'autres n'ont qu'une seule opérande. Ce qui génère quelques fois des ambiguïtés de syntaxe lorsqu'un même symbole est utilisé pour deux opérateurs différents, comme par exemple le symbole - qui, utilisé avec deux opérandes, signifie soustraction, et avec une seule opérande, signifie changement de signe. Ou le symbole *, qui pris avec deux opérandes, signifie multiplication, mais, pris avec une seule opérande, signifie pointé par.  Ou encore, avec le symbole & qui, pris avec deux opérandes, effectue un ET binaire entre elles, alors qu'avec une seule opérande, il signifie adresse de.
Le symbole & est cité ici car, en C++, il a une signification supplémentaire lors du passage des paramètres dans les fonctions, pour indiquer que ceux-ci sont passés par référence et non pas recopiés dans la pile de la fonction. Dans ce dernier cas, les instances recopiées n'ont d'existence que pendant l'exécution de la fonction, puis sont détruits une fois le résultat fourni au programme appelant. Alors que, lorsque le paramètre est passé par référence, c'est l'instance du programme appelant elle-même qui est manipulée. Le paramètre modifié dans la fonction l'est aussi dans le programme appelant.  
L'opérateur == teste si deux expressions sont égales. L'opération a donc un résultat booléen. La méthode de surcharge est donc déclarée bool. La première opérande, celle de gauche, est l'instance sur laquelle est invoquée la méthode de surcharge. operator== est donc une méthode publique de la classe Time et doit donc être déclarée dans son interface publique. La deuxième opérande est passée en paramètre de la méthode de surcharge. Elle ne doit pas être instanciée par recopie dans la pile des paramètres de la méthode. Elle est donc passée par référence. Ce qu'on indique en C++ par un symbole & précédant le nom du paramètre.
Pour calculer le résultat renvoyé par l'opération, un ET logique (opérateur &&) est effectué entre les trois comparaisons des trois attributs _HH, _MN et _SS de l'instance sur laquelle est invoqué l'opérateur et l'instance passée en paramètre.

Conclusion

Une fois la classe Time développée, elle peut être utilisée pour instancier les attributs privés __setupTime et __alarmTime de la classe Clock, comme s'il s’agissait d'un nouveau type de données. De plus, l'opérateur == ayant été surchargé, il est possible de tester l'expression this->__clockTime == this->__alarmTime pour déclencher le signal sonore de l'alarme. A signaler toutefois que l'attribut __clockTime de la classe Clock est une instance de la classe ClockTime, alors que l'attribut __alarmTime est une instance de classe Time. Cette syntaxe n'est possible que si la classe ClockTime dérive la classe Time.
La développement de la classe ClockTime par dérivation de la classe Time, pour créer un générateur de tics pour la classe Clock, fera justement l'objet de l'articule suivant.

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