Conception et Programmation Orientée Objet par l'exemple sous Arduino.


Comme dans tous les articles à vocation pédagogique, nous allons essayer de présenter les concepts de base de l'analyse-conception et de la programmation orientée objet qui peuvent rapidement devenir difficiles, sur un exemple concret et simplissimes.

Piloter une ou plusieurs leds sur des ports de l'Arduino, et une des premières choses que l'on apprend quand on découvre cet environnement et, en général, on écrit le code correspondant directement dans le programme principal. Pourtant, nous allons voir que le fait d'aborder les choses de façon plus rigoureuse en définissant le concept de LED en tant que classe d'objets, va permettre de simplifier considérablement la mise en œuvre, et surtout les modifications et la réutilisation du code.

Le concept de LED sur Arduino

cablage

D'un point de vue conceptuel, une LED peut être représentée par le numéro du port sur lequel elle est connectée, son état (allumée ou éteinte), mais également la manière de la connecter au port. En effet, on peut commander une LED, soit en reliant l'anode au +5V et la cathode au port, ou l'inverse (cf. image ci-dessus). Dans le premier cas nous dirons que l'on effectue une commande en logique "0", c'est à dire qu'il faut mettre un niveau bas sur le port pour l'allumer, dans le second en logique "1". Il est à noter que le choix du port utilisé n'est pas totalement libre. Il est important de bien vérifier, en fonction du type de carte (uno, nano, .etc), le nombre et la disponibilité de ces derniers.

Nous allons définir une classe LED possédant trois attributs privés :

  • state, un booléen représentant l'état de la LED (true=allumée et false=eteinte)

  • pin, un entier représentant le numéro de la broche Arduino (dépend du type de carte)

  • oneLogic, un booléen représentant le type de connexion électrique

Les attributs étant privés, quatre accesseurs peuvent être définis (getter/setter) :

  • get_pin, et set_pin pour obtenir et modifier le numéro de la broche

  • get_state pour obtenir l'état de la LED.

  • set_oneLogic pour modifier le mode de branchement électrique

Nous allons également définir deux constructeurs :

  • le constructeur par défaut mettra la valeur -1 dans le numéro de broche (broche indéfinie), true dans le mode de commande (logique "1") et false dans l'état (LED éteinte).

  • un constructeur permettant de préciser le numéro de broche (la validité doit être vérifiée ; une méthode privée valid_pin sera définie pour cela en fonction du type de carte) et, enfin, le mode de commande (logique "1" par défaut)

La classe aura également quatre méthodes :

  • lightOn, qui allume la LED

  • lightOff, qui l'éteind

  • invert, qui inverse son état

  • flash, qui émet un flash lumineux d'une durée spécifiée en argument (millisecondes)

La représentation UML ci-dessous décrit la classe LED telle qu'on l'a décrite précédemment :

umlled

Pour la modélisation UML, nous avons utilisé le logiciel bouml permettant de générer du code c++ automatiquement.

Le code généré se décompose en deux fichiers, un fichier d'entête contenant de déclaration de la classe (led.h) et un fichier d'implémentation contenant le codes des méthodes (Led.cpp).

Fichier d'entête :

#ifndef _LED_H
#define _LED_H
//La classe Led représente une led connectée sur une pin d'une carte arduino
class Led {
  private:
    //L'attribut state représente l'état de la led (true=allumée, false=éteinte)
    bool state;
    //l'attribut pin représentele numéro de la pin Arduino sur laquelle la led est branchée
    int pin;
    //L'attribut oneLogic décrit le mode de cablage (alumage par un niveau haut ou par un niveau bas)
    //oneLogic = true si allumage niveau haut
    bool oneLogic;
  public:
    inline const int get_pin() const;
    inline const bool get_state() const;
    void set_pin(int value);
    void set_oneLogic(bool value);
    //constructeur par défaut: pin = -1 (non définie, state=false, oneLogic=true
    Led();
    //constructeur:  pin =numéro de la pin Arduino, , oneLogic= type de logique de commande (high ou low)
    Led(int pin, bool oneLogic = true);
    //allume la led
    void lightOn();
    //Cette méthode éteint la led
    void lightOff();
    //Cette méthode inverse l'état de la led
    void invert();
    //Cette méthode allume la led pendant ms millisecondes puis l'éteint
    void flash(int ms = 100);
  private:
    //cette méthode vérifie si le n° de pin est valide (en fonction du type de carte: nano, uno, .etc.)
    bool validPin(int pin);
};
inline const int Led::get_pin() const {
  return pin;
}
inline const bool Led::get_state() const {
  return state;
}
#endif

Fichier d'implémentation (led.cpp):

#include <Arduino.h>
#include "Led.h"
void Led::set_pin(int value) {
  if (validPin(value)){         // si le n° de pin n'est pas valid, on ne fait rien
          pinMode(value, OUTPUT);
          pin = value;
  }
}
void Led::set_oneLogic(bool value) {
  oneLogic = value;
}
//constructeur par défaut: pin = -1 (non définie, state=false, oneLogic=true
Led::Led(){
  pin=-1;           // -1 = pin invalid a priori
  state=0;
  oneLogic=true;    // logique "1" par défaut
}
//constructeur:  pin =numéro de la pin Arduino, oneLogic= type de logique de commande (high ou low)
Led::Led(int pin, bool oneLogic){
  set_pin(pin);
  state=0;          // eteint par défaut
  this->oneLogic=oneLogic;
}
//Cette méthode allume la led
void Led::lightOn() {
  if (pin==-1) return;      // pin invalide, on ne fait rien
  digitalWrite(pin, (oneLogic ? HIGH : LOW));
  state=true;
}
//Cette méthode éteint la led
void Led::lightOff() {
  if (pin==-1) return;      // pin invalide, on ne fait rien
  digitalWrite(pin, (oneLogic ? LOW : HIGH));
  state=false;
}
//Cette méthode inverse l'état de la led
void Led::invert() {
     if (state)lightOff(); else lightOn();
}
//Cette méthode allume la led pendant ms millisecondes puis l'éteint
void Led::flash(int ms) {
     if (state) return;     // déjà allumé
     lightOn();
     delay(ms);
     lightOff();
}

//cette méthode vérifie si le n° de pin est valide (en fonction du type de carte: nano, uno, .etc.)
// pour en changer, il faut modifier la ligne: #define ARDUINO_VALID_PIN dans le header de la classe
#define ARDUINO_VALID_PIN pin>=2 && pin<=12         // nano
bool Led::validPin(int pin) {
    return ARDUINO_VALID_PIN;
}

Un Feu de signalisation composé de trois LED

supposons maintenant que l'on veuille simuler le fonctionnement d'un feu de signalisation (feu rouge) basé sur trois LED (verte, orange et rouge) connectées comme précédemment à une carte Arduino.

Nous allons créer une classe Feu, représentant le concept de feu de signalisation, caractérisée par les attributs :

  • trois instances de la classe Led (sous la forme d'un tableau leds avec leds[0]=vert, leds[1]=orange, leds[2]=rouge)

  • un entier state représentant l'état courant du Feu (0=vert, 1=orange, 2=rouge)

  • trois entiers représentant les durées d'allumage respectives des Led (sous la forme d'un tableau, durations)

  • un entier mode représentant le mode de fonctionnement du Feu (0=manuel, 1=automatique, 2=orange clignotant)

mais également par les méthodes (fonctions membres) :

  • un constructeur permettant d'initialiser un Feu à partir de trois instances de Led, avec state =0(vert), et durations[i] =1000 ms, i=0..2, pour les trois LED

  • deux accesseurs (getter/setter) get_state() et set_state permettant de récupérer et de modifier l'état courant du Feu (0=vert, 1=orange, 2=rouge),

  • deux accesseurs permettant get_mode et set_mode permettant de récupérer et de modifier le mode de fonctionnement courant (0=manuel, 1=automatique, 2=orange clignotant)

-deux accesseurs set_duration permettant de modifier la durée accosiée à une, ou toutes les couleurs

  • une méthode non-bloquante sync qui permet de synchroniser le feu. Elle doit être appelée dans la boucle principale du programme.

La représentation UML ci-dessous décrit les deux classes Led et Feu :

umlled

Vous noterez la relation de composition décrivant le fait que "un Feu est composé de trois Led"

Le code c++ de la déclaration de la classe Feu (feu.h) serait alors:

#ifndef _FEU_H
#define _FEU_H


#include "Led.h"
#define VERT 0
#define ORANGE 1
#define ROUGE 2
#define MODE_MANUEL 0
#define MODE_AUTO 1
#define MODE_ORANGE_CLI 2

//Cette classe représente un feu de signalisation basé sur trois leds directement connectées sur des ports d'une carte arduino
class Feu {
  private:
    //les trois leds du feu de signalisation: //leds[0]=vert, leds[1]=orange, leds[2]=rouge
    Led leds[3];

    //cet attribut représente l'état du feu: 0=vert, 1=orange, 2=rouge
    int state;

    //cet attribut est un tableau qui contiendra les durées d'allumage des différentes leds (0=vert, 1=orange, 2=rouge)
    int durations[3];

  public:
    //Constructeur permettant d'initialiser un Feu à partir de trois Led - state=0(vert), durations=1000 (ms pour les trois)
    Feu(Led vert, Led orange, Led rouge);

    //getter: permet de récupérer l'état courant du Feu (0=vert, 1=orange, 2=rouge)
    inline const int get_state() const;

    //setter: permet de modifier l'état courant du Feu(0=vert, 1=orange, 2=rouge)
    void set_state(int value);

  private:
    //mode de fonctionnement du Feu (0=manuel, 1=automatique, 2=orange clignotant)
    int mode;

  public:
    //getter: renvoir le mode de fonctionnement courant (0=manuel, 1=automatique, 2=orange clignotant)
    inline const int get_mode() const;

    //setter: permet de modifier le mode de fonctionnement courant (0=manuel, 1=automatique, 2=orange clignotant)
    void set_mode(int value);

    //setter: permet de régler la durée d'une couleur
    void set_duration(int number, int duration);

    //setter: permet de régler la durée des trois couleurs vert, orange, rouge
    void set_duration(int vert, int orange, int rouge);


    //Cette méthode permet de synchroniser le feu avec la boucle principal. Pour que le Feu fonctionne automatiquement, il faut l'appeler dans la boucle principale du programme. La fonction sync n'est pas bloquante.
    void sync();

};
//getter: permet de récupérer l'état courant du Feu (0=vert, 1=orange, 2=rouge)
inline const int Feu::get_state() const {
  return state;
}

//getter: renvoir le mode de fonctionnement courant (0=manuel, 1=automatique, 2=orange clignotant)
inline const int Feu::get_mode() const {
  return mode;
}

#endif

Le code c++ d'implémentation des méthodes (feu.cpp) serait:

#include "Feu.h"
#include <Arduino.h>

//Constructeur permettant d'initialiser un Feu à partir de trois Led - state=0(vert), durations=1000(ms pour les trois)
Feu::Feu(Led vert, Led orange, Led rouge) {
    leds[0]=vert; leds[1]=orange ; leds[2]= rouge;
    state=0;        // vert
    mode=0;         // manuel
    for (int i=0 ; i<3 ; i++)durations[i]=1000;     // 1 seconde à tout le monde
}

//setter: permet de modifier l'état (0=vert, 1=orange, 2=rouge)
void Feu::set_state(int value) {
    if (value>=0 && value<=2) {     // un état valide
        state = value;
        for (int i=0 ; i<3 ; i++)       // on met les leds dans cet état
            if (i==state)leds[i].lightOn();
                else leds[i].lightOff();
    }
}

//setter: permet de modifier le mode de fonctionnement courant (0=manuel, 1=automatique, 2=orange clignotant)
void Feu::set_mode(int value) {
    if (value>=0 && value <=2)  mode = value;
    if (mode == MODE_ORANGE_CLI) set_state(ORANGE);
}

//setter: permet de régler la durée d'une couleur
void Feu::set_duration(int number, int duration){
    if (number>=0 && number<=2)durations[number] = duration;
}

//setter: permet de régler la durée des trois couleurs vert, orange, rouge
void Feu::set_duration(int vert, int orange, int rouge){
    durations[VERT]=vert;
    durations[ORANGE]=orange;
    durations[ROUGE]=rouge;
}

//Cette méthode permet de synchroniser le feu avec la boucle principal. Pour que le Feu fonctionne automatiquement, il faut l'appeler dans la boucle principale du programme. La fonction sync n'est pas bloquante.
void Feu::sync() {
    static long lastMillis=0;
    long now = millis();
    switch(mode){
    case 0:     // manuel on ne fait rien
    case 1:     // automatique on change d'état
        if (now-lastMillis>=durations[state]){      // c'est fini pour cet état
            set_state((state+1)%3);                 // on passe au suivant
            lastMillis = now;
        }
        break;
    case 2:     // orange clignotant
        if (now-lastMillis>=durations[1]){
                leds[1].invert();     // l'orange clignote
                lastMillis = now;
        }
        break;
    }
}

Conclusion

Si vous découvrez tout ceci pour la première fois, je comprends que vous demandiez pourquoi mettre en oeuvre tant de complexité pour un problème aussi simple.

Peut être la simplicité du programme principal vous convaincra-t'elle :

#include <Arduino.h>
#include "Led.h"
#include "Feu.h"
int main()
{
    // Initialise la bibliothèque Arduino
    init();
    Serial.begin(115200);

    // instancie trois Leds connectées
    Led ledR(4, true);      // pin4, oneLogic
    Led ledV(2, true);      // pin2, oneLogic
    Led ledO(3, false);     // pin3, zeroLogic

    // instancie un Feu composé des trois Led
    Feu f(ledV, ledO, ledR);            
    f.set_duration(4000, 1000, 4000);   // durées des allumages
    f.set_mode(MODE_AUTO);              // mode automatique

    // changement de mode à partir de la liaison série
    while (1)       // superloop, ou loop() de Arduino
    {
        char c = Serial.read();
        switch (c){
        case 'M': f.set_mode(MODE_MANUEL); break;
        case 'A': f.set_mode(MODE_AUTO); break;
        case 'O': f.set_mode(MODE_ORANGE_CLI);break;
        }
        f.sync();           // synchronisation du Feu
    }
}

Voilà, si vous avez réussi à suivre ce tutoriel jusqu'ici, vous devez avoir une idée plus claire de l'intérêt de mener une analyse Orientée Objet avant de se lancer dans la programmation, y compris pour les applications embarquées sur Arduino. Le code en sera d'autant simplifié et facile à maintenir.

Si vous nêtes toujours pas convaincus, imaginez si nous avions voulu commander deux ou trois Feu simultanément avec des durées différentes, directement dans le programme principal!

Ici, cela se ferait très rapidement en rajoutant quelques lignes uniquement à la fonction main, ci-dessus.

Je vous laisse les écrire et les tester .....

Les codes sources des deux classes et du programme principal sont téléchargeables ici