Programmation Orientée Objets en embarqué

Nous commençons une série d’articles qui présentent la mise en œuvre d’une approche par objets en programmation embarquée.

Le but de ce premier article est de présenter succinctement les concepts indispensables à la compréhension des programmes que nous allons réaliser. Une fois ces notions présentées, nous effectuerons une présentation du cahier des charges et une approche par objets, puis nous « réaliserons » chaque objet pour remplir le cahier des charges.

Le sujet choisi est la réalisation d’un chenillard à led avec une carte Nano Pi ou Raspberry Pi en langage C++. Comme nous utiliserons la librairie piduino dont l’API est compatible avec Arduino, nous pourrons aussi effectuer ses activités avec une carte Arduino (avec quelques adaptations qui seront signalées).

On parle souvent de POO pour Programmation Orientée Objet. Je préfère le concept d’approche par objets car cette problématique ne concerne pas que la programmation, elle concerne toute la démarche de projet. D’ailleurs ont pourra noter que le langage SysML est une extension pluri-technologique du langage UML initialement utilisé en informatique. L’informatique a donc permis de faire évoluer d’ingénierie d’une façon générale.

Cette série d’articles suppose que vous ayez quelques connaissances en C. Les particularités de C++ pourront être approfondies par la lecture du cours C++ fait par Henri Garreta. Pour ceux qui ont un peu plus de temps et qui souhaitent une approche plus « tuto », je vous conseille le cours C++ sur OpenClassroom.

Alors que ces cours se concentrent sur l’apprentissage du langage, nous, nous allons nous concentrer sur la méthode d’approche par objets. Comme nous souhaitons que la méthode soit translatable sur n’importe quelle plateforme ou langage, nous introduirons l’utilisation du langage de modélisation graphique UML.

Cette série d’articles s’adresse à des débutants qui ont déjà réalisé des programmes dans un contexte de « Faire soi-même » (DIY) sur une carte Arduino, Raspberry Pi, Nano Pi Pi … et qui souhaitent améliorer leurs méthodes de programmation. Ces articles pourront aussi être utiles à des étudiants en sciences du numérique souhaitant mettre en œuvre rapidement cette démarche sur un cas pratique.

 Qu’est-ce que l’approche par objets ?

L’approche par objets vise à décrire un problème en identifiant les objets qui le compose.

Les objets seront décrits en terme de fonctionnalités et de propriétés. L’approche par objets ne doit pas être confondue avec l’utilisation d’un langage objets (comme C++, Java …) car on peut très bien utiliser un langage objets sans jamais faire d’approche par objets. Beaucoup de débutants en C++ pensent qu’il suffit d’utiliser ce langage pour faire de l’objet, ce n’est pas le cas.

Bien évidement, l’utilisation d’un langage objets est préférable dans une approche par objets ! Ils ont juste été inventé pour ça… On peut noter qu’il existe des projets utilisant l’approche par objets qui n’utilisent pas de langage objet, le plus connus est la boite à outils GTK qui est développée en langage C. GTK est utilisée par exemple dans le projet GNOME.

Le but de cette méthode est d’organiser la programmation afin de la rendre plus efficace, plus solide et plus évolutive.

Le but est de décrire un programme avec des concepts qui se rapprochent du concret. Dans notre quotidien nous manipulons des objets « réels » en permanence, nous utiliserons souvent ce parallèle avec la réalité pour expliquer des concepts « abstraits » de la POO.

Concepts de base de l’approche par objets

 L’encapsulation

L’approche par objets repose sur le principe d’encapsulation, on parle aussi de concept de « boîtes noires », c’est à dire, qu’on va décrire l’interface d’un objet qui sera accessible du public, la programmation en elle-même (on parle de l’implémentation) sera cachée du public (elle est privée, private en anglais). Un langage objets disposera donc de ce concept sous forme de mots clés, en C++ c’est public et private.

L’héritage

Le principe est que, sauf correction de bug ou d’erreur de conception, on ne modifie pas l’implémentation d’un objet. Il faut donc disposer d’un mécanisme qui va permettre d’étendre les fonctionnalités d’un objet. Prenons un exemple concret, un vélo électrique est un vélo, on s’attachera donc à modéliser un vélo, puis on créera un nouveau modèle de vélo, un vélo électrique qui est dérivé de l’objet initial. Ce mécanisme s’appelle l’héritage. Si un programme est prévu pour utiliser un vélo, il pourra quand même utiliser un vélo électrique dans ses fonctions de base (celles définies pour l’objet vélo).

 La classe

Un autre concept essentiel à comprendre en programmation orientée objet est le concept de classe. Une classe permet de modéliser un objet en décrivant ses fonctionnalités, on parle de méthodes, et ses propriétés qui sont des valeurs que l’on peut lire et/ou modifier. Les méthodes et les propriétés sont rattachés à une classe, on dit qu’ils sont les membres de la classe. On peut aussi utiliser le terme fonction membre pour désigner une méthode.

En C++, il y a 2 mots clés pour définir une classe : class et struct. Ceux qui connaissent le langage C auront notés que struct est utilisé dans ce langage pour décrire les structures (de données uniquement en C). Cela montre que le concept de classe est une extension aux fonctions de la notion de structure. Tous les membres d’une classe définie avec struct sont par défaut publics, alors que ceux d’une classe définie avec class sont par défaut privés. Un bon principe pour respecter l’encapsulation est donc de se limiter à n’utiliser que le mot clé class. C’est ce que nous ferons dans ces articles.

La classe est une description statique d’un objet. Un objet est la « concrétisation » dynamique d’une classe, on dit qu’un objet est une instance de classe, dans les faits un objet est l’association d’une classe et d’un bloc de variables stockées en mémoire vive (RAM donc…).

 Constructeur et destructeur de classe

Comme un objet est l’association d’une classe et d’un blocs de variables, il faut bien initialiser ces variables afin de mettre l’objet dans un état connu. C’est le rôle du constructeur de classe. Il s’agit d’une fonction membre particulière, chargée d’initialiser ses variables.

Parfois, le constructeur va « consommer » des ressources de l’ordinateur (mémoire dynamique, entrées-sorties …), il faut donc prévoir un mécanisme de libération de ses ressources, c’est le rôle du destructeur de classe.

En C++, les constructeurs sont des fonctions membres qui portent le même nom que la classe et ne renvoient aucun paramètre (même pas void), le destructeur est une fonction membre qui portent le même nom que la classe mais précédé du caractère ~ et ne renvoient aucun paramètre.

Le polymorphisme

Implicitement, je viens de dire dans la phrase précédente qu’il peut exister plusieurs constructeurs de classe, c’est vrai, mais cela ne concerne pas que les constructeurs. Toutes les fonctions membres, sauf le destructeur, peuvent avoir plusieurs « versions » ou « formes », on parle de polymorphisme ou surcharge. on pourra par exemple définir une fonction d’écriture write(...) qui prend un paramètre de type entier et une autre qui porte le même nom et qui prend un paramètre flottant. Comme le compilateur choisira automatiquement la « version » à utiliser, il faut que deux fonctions « surchargées » prennent des paramètres de nature différentes.

Voilà nous sommes arrivés à la fin de ce premier article, dans le prochain nous passerons à la pratique.

Utiliser facilement les librairies avec CodeLite

Nous avons vu l’utilisation de CodeLite dans un article précédent.

En programmation, on utilise souvent des librairies.

Une librairie fournit un ensemble de fonctions ou d’objets permettant de remplir des tâches complexes. On pourra citer dans le domaine de l’embarqué: libmodbus, libgammu, piduino

Pour utiliser une librairie en langage C ou C++, il faut disposer de 2 choses:

  • Un ou plusieurs fichiers .h qui décrivent l’interface d’utilisation (API) et qui doivent être ajoutés au début des fichiers où on utilise la librairie (avec un #include)
  • Un fichier contenant le code exécutable et les données statiques. Dans Linux, on trouve 2 formats:
    • le fichier statique (extension .a) qui permettra d’inclure les informations au moment de la compilation
    • le fichier dynamique (extension .so) qui chargera les informations au moment de l’exécution.

Il faut préciser qu’il est parfois nécessaire pour compiler un programme correctement avec une librairie d’ajouter des options sur la ligne de commande du compilateur (gcc).

Lorsqu’on souhaite utiliser CodeLite, il faut donc lui donc lui donner toutes ses informations, ce qui peut devenir fastidieux !

Heureusement, la plupart du temps, les librairies sont fournies avec des fichiers qui décrivent ses informations. Pour lire ses informations, on utilise le programme pkg-config.

Faisons un essai avec libmodbus (qu’on suppose installée…):

$ pkg-config --cflags --libs libmodbus
-I/usr/local/include -L/usr/local/lib -lmodbus
--cflags permet de demander les options à passer au compilateur, alors que --libs permet de demander les options à passer à l’éditeur de liens (linker).

Dans CodeLite, nous pouvons utiliser pkg-config pour configurer automatiquement les options de compilation et d’édition de liens.

En faisant, un clic-droit sur le dossier du projet dans le bandeau de gauche, on sélectionne Settings, puis Compiler:

On sélectionne C Compiler Options, puis on clic sur les 3 petits points

On entre dans la boite blanche la commande $(shell pkg-config --cflags libmodbus)

Puis on sélectionne Linker

On entre dans la boite blanche la commande $(shell pkg-config --libs libmodbus)

On valide par Ok

On peut alors voir lors de la compilation, que les options ont été automatiquement passées au compilateur:

Utiliser CodeLite en sudo

Il est parfois nécessaire de lancer CodeLite en sudo pour effectuer la mise au point d’un programme qui nécessite les permissions d’accès root.

Si l’on lance CodeLite avec sudo, cela échoue avec le message :

X11 connection rejected because of wrong authentication.
23:20:43: Error: Unable to initialize GTK+, is DISPLAY set properly?

Par défaut, X-Window refuse d’exécuter un connexion en root.

Il est possible de feinter le système de la façon suivante:

sudo -s
xauth merge /home/pascal/.Xauthority
exit

Dans les commandes ci-dessus, on commence par passer en root, puis on récupère les informations de connexion de l’utilisateur courant (pascal ici), puis on revient en utilisateur normal.

On peut maintenant lancer la commande

sudo codelite &

CodeLite se lance sans problème…

Utilisation de CodeLite

CodeLite est environnement de développement très léger qui peut facilement être utilisé sur NanoPi (ou toute autre carte Pi).

Dans ce tutoriel, nous allons utiliser Codelite pour créer un programme en langage C simple sur un NanoPi Neo sous ArmBian 5.41 (basé sur une Debian Stretch). Notre programme contiendra un simple printf, pour l’instant, mais 3 autres articles utiliserons ce premier tutoriel comme base afin d’effectuer des opérations sur MODBUS…

Dans un premier temps nous allons nous connecter au NanoPi par SSH à partir d’un système Linux (pour faire simple). Il faut savoir qu’il est possible d’effectuer les mêmes opérations sous MS Windows à condition d’installer un client SSH comme PuTTY, de le configurer pour utiliser le X11 Forwarding et d’installer un serveur X-Window comme Xming. Comme nous voulons faire simple, nous allons nous connecter à partir de Linux 😉

Connexion au NanoPi

Pour se connecter au NanoPi à l’adresse 192.168.122.70 avec mon identifiant (pascal), je tape :

    $ ssh pascal@192.168.122.70

Avant de lancer CodeLite, il faut l’installer ainsi que quelques paquets nécessaires (mais non installés automatiquement) :

    $ sudo apt upate
    $ sudo apt install codelite codelite-plugins hicolor-icon-theme gtk2-engines-pixbuf lxterminal libatk-adaptor libgail-common libcanberra-gtk-module xauth

Une fois les installations effectuées, on se déconnecte, puis on se reconnecte en ajoutant l’option -X et on peut alors lancer CodeLite :

    $ exit
    $ ssh pascal@192.168.122.70 -X
    $ codelite &

La fenêtre ci-dessous s’ouvre.

Création de l’espace de travail

CodeLite utilise des espaces de travail qui permettent de regrouper un ensemble de projets. Pour un espace de travail, on utilise le menu File, puis New, puis Workspace. La fenêtre ci-dessous s’ouvre.

On remplit le nom de notre nouveau workspace (libmodbus-tutorial), on choisit le dossier en cochant la case demandant de créer un nouveau répertoire.

Après avoir cliqué sur Ok, on se retrouve avec le nouvel espace de travail présent dans le bandeau latéral à gauche de la fenêtre de CodeLite:

Création du projet

Dans CodeLite, un programme correspond à un projet. Il existe un grand nombre de modèles présent, mais dans notre cas, nous allons créer un simple programme C en ligne de commande.

Pour créer un nouveau projet, on utilise le menu File, puis New, puis Project (on peut aussi faire un clic-droit sur l’espace de travail dans le bandeau de gauche).

La fenêtre suivante nous permet de choisir le modèle de projet (Simple executable (gcc)):

Il faut alors choisir un nom pour notre projet (hello-world ici) et cocher la case demandant de créer un nouveau répertoire dans celui de l’espace de travail:

Le choix de la chaîne de compilation doit laisser par défaut, il suffit donc de cliquer sur Finish dans la fenêtre suivante:

On voit alors dans le bandeau de gauche notre nouveau projet, en cliquant sur le petit triangle à gauche du dossier bleu hello-world, on peut voir qu’il contient un dossier « virtuel » nommé src que l’on peut lui même dérouler. On voit que le projet contient un seul fichier main.c Un double clic que le fichier main.c permet de l’ouvrir:

Compilation du projet

Pour compiler le programme il faut utiliser le menu Build, puis Build Project. On peut aussi utiliser la touche F7 ou l’icône .

Un rapport en bas de la fenêtre indique que la compilation s’est effectuée avec succès.

Exécution du programme

Pour exécuter le programme on utilise le menu Build, puis Run. On peut aussi utiliser la touche Ctrl+F5 ou l’icône .

Le programme se lance, une fenêtre de terminal s’ouvre et nous affiche le message correspondant à notre prinf:

Un appui sur la touche Entrée met fin au programme.

Exécution en mode pas à pas

Il est possible d’exécuter le programme en mode pas à pas.

Pour lancer l’exécution en mode pas à pas, on utilise le menu Debugger puis Start/Continue Debugger. On peut aussi utiliser la touche F5 ou l’icône

Le mode permet :

  • d’exécuter instruction par instruction (touche F10)
  • d’exécuter instruction par instruction en rentrant dans les fonctions (touche F11)
  • de placer des points d’arrêts afin de stoper le programme lorsqu’il exécute une instruction à une ligne donnée (touche F9)
  • de lancer le programme jusqu’au prochain point d’arrêt ou jusqu’à la fin (touche F5)
  • de relancer le programme (touche Shift+Ctrl+F5)
  • de surveiller le contenu des variables locales (bandeau de droite, onglet Locals)
  • de surveiller d’autres variables: globales … (bandeau de droite, onglet Watch)

Et bien d’autres choses.

Toutes ses actions sont disponible à partir de la barre d’outils dans la bandeau: