Composition et agrégation

Vu le principe d'encapsulation, il est formellement interdit d'atteindre directement les attributs d'une classe. Il faut systématiquement passer par une méthode. Cette technique est judicieuse puisque le changement d'état d'un objet passe d'abord par un changement de comportement. Il existe des méthodes adaptées pour résoudre les initialisations explicites pour que l'objet soit dans l'état désiré. Ces méthodes spécifiques s'appellent des constructeurs. La construction personnalisée des objets est très fréquente alors que la destruction l’est beaucoup moins. Nous verrons, malgré tout, des cas où il est absolument nécessaire de redéfinir le destructeur proposé par défaut. Avant cela, nous allons nous intéresser au pointeur this.

Liste des travaux pratiques

Afficheur (afficher un mot)

Nous allons maintenant nous intéresser au développement d'une application embarqué sur pcDuino afin de gérer l'afficheur AM-03127-LED. Nous avons déjà largement travaillé avec ce système, mais les programmes réalisés jusqu'à présent étaient sous forme structuré programmation fonctionnelle. L'objectif de ce TP est pour l'instant de proposer les mêmes fonctionnalités que précédemment mais avec une programmation plus modulaire en utilisant les objets. Le but est donc de mettre au point une nouvelle classe Afficheur qui s'occupe de la gestion de l'affichage de cet élément physique AM-03127-LED.

Diagramme des cas d'utilisation

Le programme à réaliser est extrêmement simple puisqu'il s'agit d'afficher un seul mot, sur une seule page de l'afficheur avec une durée limité de 10 secondes. Lorsque le temps est écoulé l'afficheur ne doit plus rien afficher. Mis à part le temps écoulé, nous avons déjà réaliser ce type de programme, mais cette fois-ci la conception est plus modulaire et respecte la modélisation objet.

Diagramme de déploiement

Le diagramme de déploiement nous permet de bien voir les différents systèmes mis en jeu. Vous devez mettre en oeuvre du cross-développement pour créer votre programme qui sera automatiquement déployé sur le système embarqué pcDuino à l'aide des trois fichiers nécessaires à l'élaboration du projet. La classe Afficheur sera développée dans les deux fichiers afficheur.h et afficheur.cpp. Vous placerez les fonctionnalités attendues dans le fichier principal main.cpp.

Diagramme des classes

D'après ce diagramme vous remarquez que la classe Afficheur est composée de deux attributs. D'une part l'attribut connexion qui nous précise si le système est bien en communication avec avec l'afficheur AM-03127-LED prise USB branchée. D'autre part l'attribut usb, issu de la classe serialib, qui représente l'interface série par connexion USB, avec ses trois méthodes utiles pour ce projet : l'ouverture du port de communication, sa fermeture et la possibilité d'envoyer des messages textuels.

Diagramme de séquence

Mise en oeuvre du codage

Respectez l'ensemble de ces diagrammes. Il est judicieux de s'occuper en premier de la classe Afficheur. Une fois que vous l'avez complètement constituée, vous vous préoccupez ensuite de la fonction principale main().

AfficheurClasses.pro
TEMPLATE = app
CONFIG += console c++11
CONFIG -= app_bundle qt

SOURCES += main.cpp afficheur.cpp serialib.cpp
HEADERS += afficheur.h serialib.h

TARGET = afficheur
target.path = /home/ubuntu/Public
INSTALLS += target
afficheur.h
#ifndef AFFICHEUR_H
#define AFFICHEUR_H

#include "serialib.h"
#include <string>
using namespace std;

class Afficheur
{
  serialib usb;
  bool connexion;
public:
  Afficheur()   { connexion = usb.Open("/dev/ttyUSB0", 9600) == 1; }
  ~Afficheur()  { affichePage(" "); usb.Close(); }
  bool etat()   { return connexion; }

  void affichePage(const string &message);
private:
  string checksum(string texte);
};

#endif // AFFICHEUR_H
afficheur.cpp
#include "afficheur.h"
#include <sstream>

void Afficheur::affichePage(const string &message)
{
  string commande = "<L1><PA><FE><MA><WC><FE><CE>";
  commande += message;
  ostringstream trame;
  trame << "<ID01>" << commande << checksum(commande) << "<E>";
  usb.WriteString(trame.str().c_str());
  usleep(50000);
}

string Afficheur::checksum(string texte)
{
  char calcul = 0;
  for (char octet : texte) calcul^=octet;
  char bas = calcul & 0b00001111;
  char haut = (calcul & 0b11110000) >> 4;
  bas = bas < 10 ? bas+=0x30 :bas+=0x41-10;
  haut = haut < 10 ? haut+=0x30 : haut+=0x41-10;
  return {haut, bas};
}
main.cpp
#include "afficheur.h"
#include #include "afficheur.h"
#include <iostream>
using namespace std;

int main()
{
  string mot;
  Afficheur lcd;
  if (lcd.etat())
  {
    cout << "Afficheur connecté" << endl;
    cout << "Tapez votre mot à afficher : ";
    cin >> mot;
    lcd.affichePage(mot);
    cout << "Fin du programme dans 10s !" << endl;
    cout << "L'afficheur va s'éteindre complètement..." << endl;
    sleep(10); // attente de 10s
  }
  else cout << "Afficheur non connecté" << endl;
}
Modification du programme principal

Modifiez la fonction principale, dans le fichier main.cpp, de telle sorte que nous voyons l'écoulement du temps des 10 secondes d'attente, comme cela vous est montré dans la figure ci-dessous :

main.cpp
#include "afficheur.h"
#include #include "afficheur.h"
#include <iostream>
using namespace std;

int main()
{
  string mot;
  Afficheur lcd;
  if (lcd.etat())
  {
    cout << "Afficheur connecté" << endl;
    cout << "Tapez votre mot à afficher : ";
    cin >> mot;
    lcd.affichePage(mot);
    cout << "Fin du programme dans 9s";
    cout.flush();
    for (int temps=8; temps>=0; temps--)
    {
      sleep(1); // attente de 1s
      cout << "\b\b" << temps << 's';
      cout.flush();
    }
    cout << endl;
  }
  else cout << "Afficheur non connecté" << endl;
}

Afficheur (afficher plusieurs pages)

Continuons à nous intéresser au développement de l'application embarqué sur le pcDuino afin de gérer l'afficheur AM-03127-LED.  Nous allons modifier la structure logicielle du projet précédent afin de permettre l'afiichage de plusieurs pages en même temps limitation de 5 pages sur 26 permises. Cette fois-ci, pour chaque message, nous pouvons choisir la couleur, le clignotement, le défilement ainsi que le temps d'affichage. Afin de permettre cet ensemble de fonctionnalités, l'idéal est de décomposer la gestion de l'affichage sur deux classes : la première, Afficheur qui gère la communication globale avec l'afficheur physique, la deuxième, Page qui comme son nom l'indique, s'occupe de la gestion de chaque message avec la couleur, le défilement, etc.

Diagramme des cas d'utilisation

Dans cette description du fonctionnement, vous remarquez que le système doit pouvoir afficher au moins une page sur l'afficheur, le nombre maximum étant limité à cinq. Au départ, pour éviter d'avoir l'affichage d'usine, il est souhaitable de proposer une seule page mais avec une chaîne de caractères vide. Ensuite, à tout moment, nous pouvons rajouter d'autres pages, d'en enlever une ou même de ne plus en afficher, sachant que nous avons toujours au moins une page et qu'il ne faut pas dépasser la limite de 5. Lorsque vous introduisez une nouvelle page, il est également possible de régler la façon de l'afficher défilement, clignotement, la couleur, le temps de repos.

Diagramme de déploiement

Le diagramme de déploiement nous permet de bien voir les différents systèmes mis en jeu. Vous devez mettre en oeuvre du cross-développement pour créer votre programme qui sera automatiquement déployé sur le système embarqué pcDuino à l'aide des sept fichiers nécessaires à l'élaboration du projet. Vous remarquez que le nom du processus programme qui est exécuté sur la pcDuino se nomme pages.

Diagramme des classes

Cette fois-ci, la structure des classes est très différente du projet précédent puisque nous pouvons maintenant afficher plusieurs pages avec, pour chacune d'entre elles,  la possibilité de faire des réglages particuliers, comme le choix de la couleur, le clignotement, etc. Afin de réaliser ce genre de traitement, nous avons maintenant une nouvelle classe Page dont l'objectif est de recenser les différentes possibilités d'affichage et finalement de fournir une trame bien constituée pour la classe Afficheur. En réalité, cette classe Page s'occupe uniquement de gérer correctement les différentes balises constituant la trame.

La classe Afficheur s'est étoffée sensiblement. Elle dispose, en plus de celles déjà existantes, des méthodes correspondant au fonctionnement attendu, défini par le diagramme de cas d'utilisation, comme ajouter(), enleverDernierePage() et neRienAfficher(). Elle dispose également d'une méthode privée choisirNombrePage() qui permet à chaque changement de situation de spécifier à l'afficheur les pages à afficher. Enfin, nous avons maintenant une méthode envoyerTrame() qui se préoccupe, comme son nom l'indique, d'envoyer les trames correspondant, soit à la nouvelle page à afficher ou soit à la commande correspondant au nombre de pages à afficher à ce moment là.

Le comportement de la classe Afficheur pour les phases de création et de destruction change pour ce projet. L'afficheur doit être éteint au démarrage du processus. Lorsque le programme se termine, l'afficheur reste dans l'état où il était juste avant la demande d'arrêt.
Diagramme de séquence - Ajouter une page

Mise en oeuvre du codage

Respectez l'ensemble de ces diagrammes. Il est judicieux de s'occuper en premier de la classe Page. Une fois que vous l'aurez complètement constituée, vous implémenterez uniquement le code correspondant à la séquence décrite ci-dessus Ajouter une page, sans vous préoccuper des autres fonctionnalités, comme Enlever dernière page, Ne plus rien afficher, Afficher une page vide, etc. Une fois que vous aurez validez la première partie, vous pourrez par la suite rajouter ces différentes fonctionnalités une à une.

AfficheurPages.pro
TEMPLATE = app
CONFIG += console c++11
CONFIG -= app_bundle qt

SOURCES += main.cpp afficheur.cpp serialib.cpp page.cpp
HEADERS += afficheur.h serialib.h page.h

TARGET = pages
target.path = /home/ubuntu/Public
INSTALLS += target
page.h
#ifndef PAGE_H
#define PAGE_H

#include <string>
using namespace std;

enum Couleur {Vert, Orange, Rouge, ArcEnCiel};
class Page { string numero; string message; string defilement = "<FE>"; string clignotement = "<MA>"; string temps = "<WC>"; string couleur = "<CE>"; public: Page(const string &texte); void setNumero(unsigned page); void setDefilement(bool actif); void setClignotement(bool actif); void setTemps(unsigned secondes); void setCouleur(Couleur c); void changerMessage(const string &texte); string trame() const; bool vide() const; }; #endif // PAGE_H
page.cpp
#include "page.h"

Page::Page(const string &texte) : message(texte) {}

void Page::setNumero(unsigned page)
{
  numero = "<L1><P";
  numero += 'A' + page - 1;
  numero += '>';
}

void Page::setDefilement(bool actif)
{
  defilement = "<F";
  defilement += actif ? "E>" : "A>";
}

void Page::setClignotement(bool actif)
{
  clignotement = "<M";
  clignotement += actif ? "B>" : "A>";
}

void Page::setTemps(unsigned secondes)
{
  temps = "<W";
  temps += 'A'+secondes;
  temps += '>';
}

void Page::setCouleur(unsigned c)
{
  char coul;
  switch (c) {
    default:
    case Vert      : coul='E'; break;
    case Orange    : coul='H'; break;
    case Rouge     : coul='B'; break;
    case ArcEnCiel : coul='S'; break;
  }
  couleur = "<C";
  couleur += coul;
  couleur += '>';
}

void Page::changerMessage(const string &texte) { message = texte; }

string Page::trame() const
{
  return numero+defilement+clignotement+temps+defilement+couleur+message;
}

bool Page::vide() const { return message.empty(); }
afficheur.h
#ifndef AFFICHEUR_H
#define AFFICHEUR_H

#include "serialib.h"
#include "page.h"
#include <string>
#include <vector>
using namespace std;

class Afficheur
{
  serialib usb;
  bool connexion;
  vector<Page> pages;
public:
  Afficheur();
  ~Afficheur();
  bool etat() const  { return connexion; } 
  void ajouter(Page &page);
  void enleverDernierePage();
  void neRienAfficher();
private:
  void choisirNombrePage(unsigned nombre);
  void envoyerTrame(const string &commande);
  string checksum(string texte);
};

#endif // AFFICHEUR_H
afficheur.cpp
#include "afficheur.h"

Afficheur::Afficheur()
{
  connexion = usb.Open("/dev/ttyUSB0", 9600) == 1;
  Page page("");
  ajouter(page);
}

Afficheur::~Afficheur()
{
  usb.Close();
}

void Afficheur::ajouter(Page &page)
{
  if (pages.size()==5) return;
  if (pages.size()==1 && pages[0].vide()) {
    pages.clear();
    page.setNumero(1);
    pages.push_back(page);
    envoyerTrame(page.trame());
  }
  else {
    unsigned nombrePage = pages.size()+1;
    page.setNumero(nombrePage);
    pages.push_back(page);
    choisirNombrePage(nombrePage);
    envoyerTrame(page.trame());
  }
}

void Afficheur::enleverDernierePage()
{
  if (pages.size()>1) {
    pages.pop_back();
    choisirNombrePage(pages.size());
  }
  else if (!pages[0].vide()) {
    pages[0].changerMessage("");
    envoyerTrame(pages[0].trame());
  }
}

void Afficheur::neRienAfficher()
{
  pages.clear();
  Page page("");
  ajouter(page);
}

void Afficheur::choisirNombrePage(unsigned nombre)
{
  string commande = "<TA>00000000009900000000";
  for (int i=0; i<nombre; i++) commande += 'A'+i;
  envoyerTrame(commande);
}

void Afficheur::envoyerTrame(const string &commande)
{
  string trame = "<ID01>";
  trame += commande;
  trame += checksum(commande);
  trame += "<E>";
  usb.WriteString(trame.c_str());
  usleep(100000); // Attente de 1/10ème de seconde entre chaque trame
}

string Afficheur::checksum(string texte)
{
  char calcul = 0;
  for (char octet : texte) calcul^=octet;
  char bas = calcul & 0b00001111;
  char haut = (calcul & 0b11110000) >> 4;
  bas = bas < 10 ? bas+=0x30 :bas+=0x41-10;
  haut = haut < 10 ? haut+=0x30 : haut+=0x41-10;
  return {haut, bas};
}
main.cpp
#include "afficheur.h"
#include <iostream>
using namespace std;

void menu()
{
  cout << "----------------------------" << endl;
  cout << "Ajouter un message ........1" << endl;
  cout << "Enlever dernier message ...2" << endl;
  cout << "Ne plus rien afficher .....3" << endl;
  cout << "Quitter le programme ......0" << endl;
  cout << "----------------------------" << endl;
  cout << "Votre choix ? ";
}

void reglerPage(Page &page)
{
  char choix;
  cout << "Souhaitez-vous faire défiler votre message (o/n) ? ";
  cin >> choix;
  page.setDefilement(choix=='o');
  cout << "Souhaitez-vous faire clignoter votre message (o/n) ? ";
  cin >> choix;
  page.setClignotement(choix=='o');

  unsigned temps;
  cout << "Durée d'affichage de votre message (1s minimum) ? ";
  cin >> temps;
  page.setTemps(temps);

  int couleur;
  cout << "Choisissez votre couleur (Vert=0, Orange=1, Rouge=2, Arc-en-ciel=3) ? ";
  cin >> couleur;
  page.setCouleur((Couleur)couleur);
}

int main()
{
  Afficheur lcd;
  int choix;
  char reglage;
  string message;

  if (lcd.etat())
  {
    cout << "Afficheur connecté" << endl;
    do {
       menu();
       cin >> choix;
       switch (choix) {
         case 1:
           {
             cout << "Quel message désirez-vous ajouter ? ";
             cin >> message;
             Page page(message);
             cout << "Désirez-vous faire des réglages sur votre message (o/n) ? ";
             cin >> reglage;
             if (reglage=='o') reglerPage(page);
             lcd.ajouter(page);
             break;
           }
         case 2:
           lcd.enleverDernierePage();
           break;
         case 3:
           lcd.neRienAfficher();
           break;
         case 0:
           cout << "Fin du programme, à bientôt!" << endl;
         default:
           break;
         }
    }
    while (choix!=0);
  }
  else cout << "Afficheur non connecté" << endl;
}

Entrées sorties sur pcDuino - GPIO

Dans ce chapitre nous allons nous consacrer à l'étude des entrées sorties digitales de la carte pcDuino. Dans cette première étape, notre intention sera de récupérer une impulsion provoquée par un bouton poussoir et d'activer une sortie de la carte afin qu'une led puisse s'éclairer. Vous voyez que notre démarche est très modeste. L'objectif est, comme les projets précédents, de créer une classe qui va encapsuler toute la démarche nécessaire pour que l'utilisation des entrées-sorties tout ou rien de la carte pcDuino soit extrêmement simple à utiliser. La mise en place sera ainsi intuitive et très rapide à expérimenter.

Caractéristiques des broches E/S numériques et analogiques du pcDuino

Nous retrouvons la plupart des possibilités de l'Arduino à quelques adaptation près.

Le brochage du pcDuino avec l'alimentation et les entrées analogiques

Le brochage des entrées-sorties tout-ou-rien

Caractéristiques électriques : Ces broches fonctionnent en 3,3V et pas en 5V et ne peuvent fournir que quelques mA... donc attention, prudence... !
Principe d'accès aux broches E/S

Il est possible de gérer les broches E/S comme de simples fichiers c'est un principe général sous Gnu/Linux.... Le fichier de chaque broche est alors de la forme gpioXX correspond au numéro de la broche. Il existe deux types de fichier de nature différentes :

  1. Les fichiers fixant le mode entrée ou sortie sont placés dans le répertoire : /sys/devices/virtual/misc/gpio/mode/

  2. Les fichiers contenant/définissant l'état des broches sont placés dans le répertoire : /sys/devices/virtual/misc/gpio/pin/

Fixer le sens d'une broche

Pour fixer le sens d'une broche, c'est simple : il suffit d'ouvrir le fichier et d'écrire la valeur voulue sachant :

Diagramme des cas d'utilisation

Après toutes ces considérations techniques qui nous ont permis de comprendre les différents fonctionnements, nous allons maintenant mettre en oeuvre un petit programme qui permet, dès le départ, d'activer une sortie du pcDuino qui est connectée à une simple led. Cette led reste affichée indéfiniment jusqu'à ce que l'utilisateur appuie sur un bouton poussoir. Cette action provoque l'arrêt du programme en même temps que l'extinction de la led.

Diagramme de déploiement

Comme précédemment, le diagramme de déploiement nous permet de bien voir les différents systèmes mis en jeu. Vous devez mettre en oeuvre du cross-développement pour créer votre programme qui sera automatiquement déployé sur le système embarqué pcDuino à l'aide des trois fichiers nécessaires à l'élaboration du projet. La classe GPIO sera développée dans les deux fichiers gpio.h et gpio.cpp. Vous placerez les fonctionnalités attendues dans le fichier principal main.cpp.

Diagramme des classes

D'après ce diagramme vous remarquez que la classe GPIO est composée de deux attributs :

  1. D'une part l'attribut broche qui nous permet d'être en relation avec le fichier représentant la broche utilisée en lecture et en écriture. C'est dans ce fichier que vous allez proposer la valeur 1 ou 0 suivant l'activation que vous souhaitez soumettre broche configurée en sortie, et c'est toujours dans ce fichier que vous pouvez récupérer l'état de l'activation broche configurée en entrée.
  2. D'autre part l'attribut mode, issu de l'énumération MODE, qui permet de fixer une fois pour toute si la broche concernée doit être considérée comme une entrée ou comme une sortie une fois que le sens de la broche est fixé, il ne doit plus être possible de le changer.

Cette classe GPIO possède uniquement quatre méthodes qui d'un point de vue utilisation sont très très simples à manipuler. Leurs noms sont très évocateurs et intuitifs. Toute la partie délicate à traiter se situe à l'intérieur des méthodes. Le développeur qui va utiliser cette classe pourra réaliser des algorithmes sophistiqués sans qu'il se rende compte de la difficulté interne. Toute la problématique est ainsi encapsulée à l'intérieur de la classe. C'est ce qui fait l'intérêt de la programmation objet.

Mise en oeuvre du codage

Dans un premier temps, vous pouvez réaliser des expériences afin de bien maîtriser cette notion de fichiers et de flot. Une fois que vous aurez compris tous les mécanisme en jeu, vous pouvez coder votre classe GPIO et respecter l'algorithme prévu pour élaborer votre projet.

gpio.pro
TEMPLATE = app
CONFIG += console c++11
CONFIG -= app_bundle qt

SOURCES += main.cpp gpio.cpp
HEADERS += gpio.h

target.path = /home/ubuntu/Public
INSTALLS += target
gpio.h
#ifndef GPIO_H
#define GPIO_H

#include <fstream>
using namespace std;

enum MODE {Entree, Sortie};

class GPIO
{
  fstream broche;
  const MODE mode;
public:
  GPIO(MODE m, unsigned numero);
  ~GPIO();
  bool etat();
  void activer(bool action);
};

#endif // GPIO_H
gpio.cpp
#include "gpio.h"

GPIO::GPIO(MODE m, unsigned numero) : mode(m)
{
  string localisation = "/sys/devices/virtual/misc/gpio/mode/gpio";
  localisation += '0' + numero;
  ofstream modeGPIO(localisation.c_str());
  modeGPIO << (mode==Sortie ? '1' : '0');
  modeGPIO.close();
  localisation = "/sys/devices/virtual/misc/gpio/pin/gpio";
  localisation += '0' + numero;
  broche.open(localisation.c_str());
}

GPIO::~GPIO()
{
  if (mode==Sortie) broche << '0';
  broche.close();
}

void GPIO::activer(bool action)
{
  if (mode==Sortie) {
    broche << (action ? '1' : '0');
    broche.flush();
  }
}

bool GPIO::etat()
{
  broche.seekg(0);
  return broche.peek()=='1';
}
main.cpp
#include <iostream>
#include <unistd.h>
using namespace std;

#include "gpio.h"

int main()
{
  GPIO bouton(Entree, 6);
  GPIO led(Sortie, 3);
  led.activer(true);
  while (!bouton.etat()) usleep(100000);
  return 0;
}
Conclusion sur la programmation orientée objet

Dans le fichier principal, vous remarquez que cette classe GPIO est très très simple à utiliser. Il suffit de déclarer les objets relatifs avec des noms évocateurs comme bouton et led. Toutes les phases délicates se situent dans le constructeur et le destructeur, mais ces deux méthodes particulières ne se voient pas puisqu'elles sont appelées implicitement. Du coup le code correspondant à l'algorithme de fonctionnement est très très court et rapide à construire. Je me répète, mais la programmation modulaire comme l'est la programmation orientée objet nous donne à la fois une conception intuitive et très rapide à mettre en oeuvre. Cela facilite le développement d'applications.