Héritage & Polymorphisme Généralités Dans l’activité précédente, vous avez implémenté une classe Sht20 qui permet notamment de lire la température depuis le capteur I2C présent sur le HAT de la Raspberry Pi. Après réflexion, on peut se dire que ce capteur I2C n’est en fait qu'une sorte de capteur de température (relation is a en anglais) parmi ceux qu’on peut trouver dans le commerce. D’autres capteurs pourraient utiliser des interfaces de communication différentes (liaison série, bus SPI, éthernet…) pour communiquer la température relevée. On peut donc légitimement se dire qu’il serait logique de mettre en commun dans une classe C++ toutes les méthodes et/ou attributs commun à tous les capteurs de température comme l’opération qui consiste à fournir une température et pourquoi pas l’unité utilisée pour cette température. Il suffirait ensuite de spécialiser le comportement de cette classe générique — appelée classe mère ou classe parent — dans des classes spécialisées — appelées classes filles, classes dérivées ou classes enfants — pour lire une température fournie par un centrale météo disposant d’une liaison série, une température acquise par un capteur sur bus I2C, une température fournie pas un service de météo en ligne… La relation qui permet de bénéficier des services d’une classe de base tout en laissant la possibilité de les adapter ou les compléter se nomme l'héritage. Parfois, on parle de spécialisation quand on se réfère à l'héritage. L'héritage et la spécialisation sont 2 termes qui se rapportent au même concept en programmation objet. Ce n’est que le point de vue qui diffère. Ainsi, vue depuis une classe mère, la classe fille spécialise son comportement. Par contre, si on se place au niveau de la classe fille, celle-ci hérite des caractéristiques de sa classe mère. Pour illustrer ce fonctionnement, on fait évoluer le modèle UML élaboré dans la dernière activité. Il définit une nouvelle classe TempSensor dont vont dériver les classes Tc72 et Sht20. 🖮 Travail n° 1 Codage de la classe mère Implémenter la classe TempSensor dans les fichiers tempsensor.h et tempsensor.cpp à partir des consignes suivantes : le constructeur par défaut doit : instancier l’attribut _bcm depuis sa liste d’initialisation initialiser l’attribut unit avec la valeur TempSensor::CELSIUS. afficher un message “Construction d’un objet de type TempSensor() [classe mère]” sur le terminal pour informer l’utilisateur du moment auquel il est réellement appelé dans le programme final. le destructeur doit simplement afficher un message “Destruction d’un objet de type TempSensor()” les méthodes getUnit() (→ accesseur ou getter) et setUnit() (→ mutateur ou setter) doivent respectivement renvoyer et fixer la valeur de l’attribut _unit les méthodes de type convertXXX() précédées du symbole ‘#’ dans le modèle UML seront déclarées dans une section protected: du fichier tempsensor.h. Comme leurs noms l’indiquent, ces méthodes se contentent de convertir soit en °C soit en °F les valeurs qui leurs sont passées en argument. La méthode readTemperature() sera déclarée dans de la manière suivante : virtual double readTemperature() = 0; Ceci permet de déclarer readTemperature() en tant que méthode virtuelle pure. Méthode virtuelle, méthode virtuelle pure, classe abstaite Une méthode virtuelle (→ Ex. : virtual double readTemperature()) est une méthode susceptible d’être redéfinie dans les classes filles d’une classe mère. En UML, elle se caractérise par un nom de méthode écrit en italique. Une méthode virtuelle pure (→ Ex. : virtual double readTemperature() = 0) — contrairement à une méthode virtuelle basique — rend abstraite (→ abstract) la classe qui la contient. Une méthode virtuelle pure n’est pas implémentée dans la classe qui la déclare mais doit l’être dans les classes filles. Une classe abstraite est une classe qui n’est pas instanciable directement dans un programme. Seules ses classe filles peuvent l’être. En UML, elle se traduit en écrivant son nom en italique ou en lui appliquant le stéréotype <<abstract>> dans le compartiment supérieur de la classe. 🖮 Travail n° 2 Codage d’une classe fille On vous donne le code d’une classe fille de TempSensor qui permet d’obtenir la température depuis le capteur de température TC72 présent sur le HAT de la Raspberry Pi et qui s’interface avec elle grâce à un bus SPI ainsi qu’un code test qui permet de tester son bon fonctionnement Code source tc72.h #pragma once #include "tempsensor.h" class Tc72 : public TempSensor { (1) public: Tc72(); double readTemperature(); }; 1 Cette ligne spécifie que la classe Tc72 dérive de Tempsensor tc72.cpp #include <unistd.h> #include "tc72.h" Tc72::Tc72() { // TC72 fonctionne en mode 1 ou 3 (CPOL=0|1, CPHA=1) _bcm.spi_setDataMode(Bcm2835Wrapper::SPI_MODE_1); (1) _bcm.spi_setClock(Bcm2835Wrapper::SPI_SCLK_1MHz); // Sur le HAT, On se sert de GIO8(CE0) pour la selection de // boitier du TC72 _bcm.spi_chipSelect(Bcm2835Wrapper::SPI_CS0); // La datasheet du TC72 spécifie que le chip select est actif à // l'état haut _bcm.spi_setChipSelectPolarity(Bcm2835Wrapper::SPI_CS0 , Bcm2835Wrapper::LOGIC1 ); } double Tc72::readTemperature() { // On lance l'acquisition de la T° (One Shot Conversion) // par mise à 1 dans le control register // (->0x80 : Ctrl Reg Write Address) du bit 4 sachant que les // bits 0 (Shtudown) et 2 (Reserved) doivent rester à 1 (->0x15) // => 0x80 0x15 uint8_t cmd[] = {0x80, 0x15}; _bcm.spi_writenb(cmd, 2); // Attendre résultat conversion > 150ms usleep(2e5); // Lecture de la température par demande de lecture du registre // MSB Temperature (-> 0x02 : MSB Temperature Read address) uint8_t tbuf[] = {0x02, 0x00, 0x00, 0x00}; uint8_t rbuf[] = {0x00, 0x00, 0x00, 0x00}; _bcm.spi_transfernb(tbuf, rbuf, 4); // Conversion de la valeur brute en complément à 2 de la // température lue en °C // (Noter le transtypage en int16_t pour garder le signe lors du // décalage à droite) double celsiusTemp; celsiusTemp = ((int16_t)((rbuf[ 1 ] << 8) | rbuf[2]) >> 6) * 0.25; return getUnit() == FAHRENHEIT (1) ? convertCelsius2Fahrenheit(celsiusTemp) : celsiusTemp ; } 1 Du fait de l’héritage public, la classe dérivée peut accéder directement aux attributs protected de la classe mère. test-tempsensor.cpp #include <iostream> #include <iomanip> #include "tc72.h" using namespace std; int main(void) { Tc72 myTc72; double tTc72; myTc72.setUnit(TempSensor::CELSIUS); tTc72 = myTc72.readTemperature(); cout << "=> T : " << setw(7) << fixed << internal << showpos << setfill( '0' ) << setprecision( 2 ) << tTc72 << (myTc72.getUnit() == TempSensor::CELSIUS ? " °C" : " °F") << endl; return 0; } Étudier le code source fourni et coder la classe Sht20 en adaptant le code source créé dans l’activité précédente de façon à ce que celui-ci prenne en compte qu’elle est désormais une classe fille de TempSensor Compléter le code source de test pour afficher en fahrenheit (→ °F) la température mesurée par le SHT20 en faisant appel aux méthodes readTemperature(), setUnit() et getUnit() de la classe Sht20 codée à la question précédente. Télécharger le code de la classe Bcm2835Wrappper mis à jour pour prendre en charge la communication sur le bus SPI. 🖮 Travail n° 3 Polymorphisme Polymorphisme Le polymorphisme est un mécanisme qui permet de manipuler des objets d’une classe fille au travers de pointeurs ou de références sur une classe mère. Ceci permet par exemple, dans notre cas, de pouvoir rassembler dans un tableau les 2 types de capteurs présent sur le HAT de la Raspberry Pi de façon à afficher la température relevée par chacun d’eux à partir d’une boucle. Code source test-polymorphism.cpp #include <iostream> #include <iomanip> #include <vector> #include "sht20.h" #include "tc72.h" using namespace std; int main(void) { vector<TempSensor*> sensors; (1) vector<TempSensor*>::iterator it; (2) double temp; sensors.push_back( new Sht20()); (3) sensors.push_back( new Tc72()); (3) for(it = sensors.begin(); it != sensors.end(); it++) { (4) (*it)->setUnit(TempSensor::FAHRENHEIT); (5) temp = (*it)->readTemperature(); (5) cout << "=> T : " << setw(7) << fixed << internal << showpos << setfill( '0' ) << setprecision( 2 ) << temp << ((*it)->getUnit() == TempSensor::CELSIUS ? " °C" : " °F") (5) << endl; } for(auto p : sensors) { (6) delete p; } sensors.clear(); (7) return 0; } 1 On crée un vector - tableau dont la taille peut varier au cours du temps, contrairement aux tableaux du langage C — qui contiendra des pointeurs vers des objets dérivés de la classe TempSensor 2 on crée un itérateur qui permettra de parcourir le vector 3 on instancie dynamiquement les classes dérivées de TempSensor et on insère les adresses des objets créés dans le vector 4 on parcourt le vector avec l’itérateur 5 on accède aux méthodes des objets dérivés de la classe TempSensor via l’itérateur 6 on désalloue de la mémoire les objets créés dynamiquement 7 on vide le vector Tester le code fourni ci-dessus sur votre Raspberry Pi pour constater son bon fonctionnement Adapter le code de façon à demander à l’utilisateur dans quelle unité il désire afficher la température des 2 capteurs. 🖮 Travail n° 4 Synthèse Faire évoluer le code du travail précédent de façon à afficher non seulement la température des 2 capteurs présents sur le HAT de la Raspberry Pi mais également la température interne de la Raspberry Pi. Pour cela, créer une nouvelle classe CoreTemp, dérivée de TempSensor et dont le rôle sera de relever la température du cœur de la Raspberry Pi. Le code suivant permet d’obtenir la température interne de la Raspberry Pi : float getCoreTemp() { const char cmd[] = "vcgencmd measure_temp"; float tCelsius = 0; char buf[ 64 ]; FILE *fp = popen(cmd, "r"); (1) if (fgets(buf, sizeof(buf)-1, fp) != 0) { sscanf(buf, "%*[^=]=%f", &tCelsius); } fclose(fp); return tCelsius; } 1 la fonction popen(), déclarée dans <stdio.h>, permet d’exécuter la commande passée en paramètre et d’en récupérer le résultat. 🞄 🞄 🞄 Capteur SHT20 Application graphique