====== Le composant logiciel LED ====== [[in202:seance_5|Retour à la séance 5]] ===== Thinking Component ===== L'objectif du C++ n'est pas de faire le code le plus court, C++ a pour objectif de simplifier la lisibilité, la maintenance et la réutilisation du code. Sur le montage que vous avez, nous avons des composants physiques. Pour l'instant, nous n'avons que deux familles de composants : * les diodes (soit sur la breadboard), soit sur la carte ARDUINO, * les boutons (un bouton-poussoir sur la breadboard). L'idée est d'abstraire les composants par des classes qui offrent les fonctionalités d'une diode et d'un bouton poussoir en cachant l'ensemble des opérations de bas-niveaux que sont ''digitalRead()'', ''digitalWrite()'', ''pinMode()'' etc. ===== Etape 1 : Classe Led ===== En vous inspirant de ce qui a été fait sous Tinkercad, proposer une classe ''Led'' en définissant : * les constructeurs de la classe ''Led'', * les variables d'états de la classe ''Led'', * les méthodes de la classe ''Led'' et pourquoi pas proposer aussi des opérations : * opérations d'affectation ''operator = (bool)'' pour allumer éteindre la ''Led'', * opération de conversion ''operator bool() const'' pour récupérer l'état de la ''Led'', * opération de comparaison avec une autre ''Led'' ou avec une valeur booléenne : ''operator == ()'' et ''operator != ()'' * opération d'inversion, par exemple ''operator !()''. Tout ceci vous permettra de réaliser les opérations classiques sur la Led. L'état de la diode est caractérisé par deux variables : * le port sur lequel la diode est connectée (c'est un numéro de port, soit un nombre entier), * l'état de la diode, soit allumée, soit éteinte (c'est une valeur booléen, ''true'' est allumée, ''false'' est éteinte). Ceci donne deux champs internes de la classe ''Led'', soit : class Led { private: bool m_isSwitchedOn; uint32_t m_portNumber; }; Pour les constructeurs, il faut initialiser les variables d'états : * le port (essentiel) * l'état de la diode (soit on, soit off, par défaut, nous pouvons dire ''off''). Ceci nous donne deux constructeurs : class Led { private: bool m_isSwitchedOn; uint32_t m_portNumber; public: Led(uint32_t thePort): Led(thePort, false) {} Led(uint32_t thePort, bool switchOnWhenStarted): m_portNumber(thePort), m_isSwitchedOn(switchOnWhenStarted) {} }; Il faut ensuite ajouter les fonctions qui permette de questionner l'état de la diode. Il y en a deux, l'une qui demande l'état de la diode ''isSwitchOn() const'' et une autre qui récupère le port ''getPort() const''. Ces deux fonctions ne modifient pas l'objet ''Led'' et elle sont donc marquées comme constante. class Led { private: bool m_isSwitchedOn; uint32_t m_portNumber; public: Led(uint32_t thePort): Led(thePort, false) {} Led(uint32_t thePort, bool switchOnWhenStarted): m_portNumber(thePort), m_isSwitchedOn(switchOnWhenStarted) {} bool isSwitchOn() const { return m_isSwitchedOn; } bool getPort() const { return m_portNumber; } }; Ensuite, nous avons les fonctions correspondant à des actions que supporte le composant diode. Nous vons deux actions, l'une correspondant à //allumer la diode// et l'autre à //éteindre la diode//. class Led { private: bool m_isSwitchedOn; uint32_t m_portNumber; public: Led(uint32_t thePort): Led(thePort, false) {} Led(uint32_t thePort, bool switchOnWhenStarted): m_portNumber(thePort), m_isSwitchedOn(switchOnWhenStarted) {} bool isSwitchOn() const { return m_isSwitchedOn; } bool getPort() const { return m_portNumber; } void switchOn() { m_isSwitchedOn = true; } void switchOff() { m_isSwitchedOn = false; } }; Nous avons pour l'instant créer un composant ''virtuel'', il correspond à une diode mais ne commande pas la diode physique. Pour commander la diode physique, il reste deux étapes à faire : * Les constructeurs doivent initaliser la diode physique. * Les fonctions correspondant à des actions doivent exécuter l'action sur la diode physique. Pour ce faire, il faut modifier le constructeur principal en ajoutant la configuration du port : Led(uint32_t thePort, bool switchOn): m_portNumber(thePort), m_isSwitchedOn(switchOn) { pinMode(m_portNumber, OUTPUT); if(switchOnWhenStarted) switchOn(); } ainsi que les deux fonctions ''switchOn()'' et ''switchoff()'' : void switchOn() { if(!m_isSwitchedOn) { m_isSwitchedOn = true; digitalWrite(m_portNumber, HIGH); } } void switchOff() { if(m_isSwitchedOn) { m_isSwitchedOn = false; digitalWrite(m_portNumber, LOW); } } Et voilà, la classe est terminée. class Led { private: bool m_isSwitchedOn; uint32_t m_portNumber; public: Led(uint32_t thePort): Led(thePort, false) {} Led(uint32_t thePort, bool switchOnWhenStarted): m_portNumber(thePort), m_isSwitchedOn(switchOnWhenStarted) { pinMode(m_portNumber, OUTPUT); if(switchOnWhenStarted) switchOn(); } bool isSwitchOn() const { return m_isSwitchedOn; } bool getPort() const { return m_portNumber; } void switchOn() { if(!m_isSwitchedOn) { m_isSwitchedOn = true; digitalWrite(m_portNumber, HIGH); } } void switchOff() { if(m_isSwitchedOn) { m_isSwitchedOn = false; digitalWrite(m_portNumber, LOW); } } }; ===== Etape 2 : Implanter le code de la classe Led ===== - Implanter le code dans le programme ''blink'' que vous allez sauvegarder dans un répertoire. - Effectuer la modification du code dans les fonctions ''setup()'' et ''loop()''. Normalement, il ne devrait y avoir aucune information dans le code ''setup()''. - Compiler & télécharger le code sur la carte. En fait, nous allons désormais déclarer la diode ''builtinLed'' comme une variable globale. En effet, c'est un composant physique, ce n'est pas quelque chose que l'on va ajouter ou enlever. On peut donc le créer au moment du chargement du programme, puisque qu'il serait toujours accessible. class Led { private: bool m_isSwitchedOn; uint32_t m_portNumber; public: Led(uint32_t thePort): Led(thePort, false) {} Led(uint32_t thePort, bool switchOnWhenStarted): m_portNumber(thePort), m_isSwitchedOn(switchOnWhenStarted) { pinMode(m_portNumber, OUTPUT); if(switchOnWhenStarted) switchOn(); } bool isSwitchOn() const { return m_isSwitchedOn; } bool getPort() const { return m_portNumber; } void switchOn() { if(!m_isSwitchedOn) { m_isSwitchedOn = true; digitalWrite(m_portNumber, HIGH); } } void switchOff() { if(m_isSwitchedOn) { m_isSwitchedOn = false; digitalWrite(m_portNumber, LOW); } } }; Led builtinLed(LED_BUILTIN); Ensuite, la fonction ''setup()'' va être simplifié, il n'y a plus besoin d'initialiser le port, c'est le constructeur de la classe ''Led'' qui le fait. Donc le contenu de ''setup()'' devient trivial. void setup() {} Reste ensuite le code de la boucle où l'on appelle désormais les méthodes ''switchOn()'' et ''switchOff()'' de la classe ''Led''. void loop() { builtinLed.switchOn(); delay(1000); builtinLed.switchOff(); delay(1000); } C'est vrai que la lecture du code en devient évidente. Plus besoin même de commentaire. ===== Etape 3 : Ajouter une option ''debug'' à la classe ''Led'' ===== L'idée est que vous ajouter une option ''debug'' qui par défaut est à faux. De plus, il serait bien dans ce cas d'avoir aussi le nom de la "diode" pour pouvoir identifier le composant qui envoie des messages. * Faites évoluer la classe ''Led'' pour contenir les deux informations supplémentaires, le nom de la diode ainsi qu'un drapeau (une valeur booléenne) qui indique si le mode débogage est activé. * Ajouter ensuite les messages indiquant que la diode s'allume ou s'éteint lorsque le mode ''debug'' est activé sur la diode. Le message doit contenir le nom de la diode ainsi que le nouvel état de celle-ci. Pour ce faire, nous devons ajoutons des informations complémentaires notre classe ''Led'' : * le nom de la diode (une chaîne de caractères), * une variable booléenne qui indique si le mode //debug// est actif ou non. Donc nous ajoutons **deux champs privés** à la classe : const char* m_name; bool m_debug; Nous devons modifier les constructeurs pour initialiser ces champs supplémentaires. Led(uint32_t thePort, const char* theName, bool debugIsActive = false): Led(thePort, false, theName, debugIsActive) {} Led(uint32_t thePort, bool switchOnWhenStarted, const char* theName, bool debugIsActive = false): m_portNumber(thePort), m_isSwitchedOn(switchOnWhenStarted), m_name(theName), m_debug(debugIsActive) { pinMode(m_portNumber, OUTPUT); } Nous modifions aussi les actions puisque nous souhaitons envoyer un message si le mode **debug** est actif : void switchOn() { if(!m_isSwitchedOn) { m_isSwitchedOn = true; digitalWrite(m_portNumber, HIGH); if(m_debug) Serial << "Led: " << m_name << ": switch on"; } } void switchOff() { if(m_isSwitchedOn) { m_isSwitchedOn = false; digitalWrite(m_portNumber, LOW); if(m_debug) Serial << "Led: " << m_name << ": switch off"; } } Voilà, il ne reste plus qu'à penser à initialiser ''Serial'' dans la fonction ''setup()'', ce qui donne le code complet suivant : template UARTClass& operator << (UARTClass& theSerial, const T& theValue) { theSerial.print(theValue); return theSerial; } class Led { private: bool m_isSwitchedOn; uint32_t m_portNumber; const char* m_name; bool m_debug; public: Led(uint32_t thePort, const char* theName, bool debugIsActive = false): Led(thePort, false, theName, debugIsActive) {} Led(uint32_t thePort, bool switchOnWhenStarted, const char* theName, bool debugIsActive = false): m_portNumber(thePort), m_isSwitchedOn(switchOnWhenStarted), m_name(theName), m_debug(debugIsActive) { pinMode(m_portNumber, OUTPUT); } bool isSwitchOn() const { return m_isSwitchedOn; } bool getPort() const { return m_portNumber; } void switchOn() { if(!m_isSwitchedOn) { m_isSwitchedOn = true; digitalWrite(m_portNumber, HIGH); if(m_debug) Serial << "Led: " << m_name << ": switch on\n"; } } void switchOff() { if(m_isSwitchedOn) { m_isSwitchedOn = false; digitalWrite(m_portNumber, LOW); if(m_debug) Serial << "Led: " << m_name << ": switch off\n"; } } }; Led builtinLed(LED_BUILTIN, "Builtin", true); // the setup function runs once when you press reset or power the board void setup() { Serial.begin(9600); } // the loop function runs over and over again forever void loop() { builtinLed.switchOn(); delay(1000); builtinLed.switchOff(); delay(1000); } ===== Etape 4 : Profiter de l'héritage ===== En fait le mode débogage est un mode qui est intéressant pour tous les composants. On peut donc mutualiser partie du code pour tous les composants. L'idée est de proposer une classe de base que l'on appellera ''CustomComponent'' dont héritera l'ensemble des autres classes composants comme par exemple ''Led''. bool isSwitchOn() const { return m_isSwitchedOn; } bool getPort() const { return m_portNumber; } void switchOn() { if(!m_isSwitchedOn) { m_isSwitchedOn = true; digitalWrite(m_portNumber, HIGH); } } void switchOff() { if(m_isSwitchedOn) { m_isSwitchedOn = false; digitalWrite(m_portNumber, LOW); } } }; class CustomComponent { protected; CustomComponent(const char* name, bool debug) {} public: bool debugIsActive() const; const char* getName() const; const char* header() const; // Génère l'entête de message avec le nom du composant. }; * Compléter cette classe afin d'implanter les méthodes. * Faites évoluer la classe ''Led'' qui hérite désormais de cette classe de base. * Tester votre code. class CustomComponent { private: bool m_Debug; const char* m_Name; protected; CustomComponent(const char* theName, bool debug = false): Name(theName), m_Debug(debug) {} public: bool debugIsActive() const { return m_Debug; } bool EnableDebug() { m_Debug = true; } bool DisableDebug() { m_Debug = false; } const char* getName() const { return m_Name; } String header() const { return (String)m_Name + ": "; } }; class Led: public CustomComponent { private: bool m_isSwitchedOn; uint32_t m_portNumber; public: Led(uint32_t thePort, const char* theName): Led(thePort, theName, false) {} Led(uint32_t thePort, const char* theName, bool switchOnWhenStarted): CustomComponent(theName, false), m_portNumber(thePort), m_isSwitchedOn(switchOnWhenStarted) { pinMode(m_portNumber, OUTPUT); if(switchOnWhenStarted) switchOn(); } }; ====== Navigation ====== Précédent: [[.:serial|Obtenir des informations]] Suivant: [[.:breadboard|Gérer deux LEDs]]