Développement

FormationC++ d'une application Qt5 dans l'environnement Raspberry// Raspbian

4.5

PHILIPPE ANTOINE - CHRISTIAN HORTOLLAND - 2018

26/01/2019 Légende

Entrée du glossaire .

Abréviation 

Référence Bibliographique 

Référence générale 

Table des

matières

Objectifs 5

Introduction 7

I - Matériel nécessaire pour l'application finale 9

II - Cahier des charges de l'application finale 13

III - Préparation de la carte Raspberry 17

A. Préparation du système d'exploitation...... 17

B. Installations logicielles pour développer en Qt/++...... 22

IV - Prise en main de l'EDI Qt Creator 27

A. Présentation de L'EDI (Environnement de Développement Intégré) Qt-Creator. .27

B. Compilation de l'application...... 31

V - Description d'une application graphique par défaut Qt 33

A. Le fichier de projet appIhm1.pro...... 34

B. Les fichiers sources des classes de l'application...... 35

C. Le fichier contenant la fonction principale...... 37

D. Le fichier décrivant l'interface graphique...... 38

VI - Formation pas à pas ! 39

A. Conception d'une IHM Qt...... 39 1. Les objets graphiques Qt...... 39 2. Gestion des signaux/slots dans une application Qt...... 40 3. Ma première IHM...... 41 4. Construction de l'IHM de notre application...... 43 B. Tâches de mise en œuvre des techniques de programmation...... 43 1. Utilisation de l'objet QTimer pour effectuer des actions à intervalle régulier...... 43 2. Utilisation simple d'un objet QTimer...... 47 3. Entrées/sorties GPIO...... 48 4. Utilisation d'une sortie GPIO...... 53

3 5. Communication avec le bus SPI...... 55 6. Communication avec un capteur sur le bus SPI...... 59 7. La communication série...... 59 8. Communiquer par liaison série...... 66 9. Communications par le bus I2C...... 66 10. Lecture de la température et de l'humidité...... 73 11. Accès à une base de données MySQL...... 74 12. Lire et sauver des informations dans une base de données...... 81 13. Communications TCP par le réseau...... 84 14. Communication réseau client/serveur...... 89 15. Mise en place de traitements parallèles avec les threads...... 92 16. Traitements parallèles avec les threads...... 98 17. Utilisation d'un segment de mémoire partagé...... 98 18. Utilisation d'un segment de mémoire partagé entre processus...... 100

VII - L'application ! 101

A. Développement de l'application...... 101 1. L'axe des exigences...... 101 2. Axe dynamique...... 108 3. Axe fonctionnel / structurel...... 112 4. Axe transverse...... 117

VIII - Ressources annexes 119

A. Ressources logicielles pour la formation...... 119 1. Cahier des charges de l'application finale...... 119 2. Sources du client TCP distant...... 121 3. Source du serveur TCP distant...... 121 B. Description d'un objet...... 121 1. Entre la pensée et l'objet : la classe...... 122 2. Création d'un objet : l'instanciation...... 124 3. Accesseurs d'un objet...... 124 4. Destruction d'objets...... 126 5. Constructeur / destructeur d'un objet...... 127 6. Mes premiers objets...... 132 C. Les relations entre les objets...... 132 1. Héritage entre classe...... 132 2. Association entre classe...... 138 3. Composition entre classes...... 139 4. Agrégation entre classes...... 139 5. Contrôle d'un capteur de distance...... 140

IX - Activités liées à la fabrication et aux communications électroniques 143

A. Documents de fabrication, d'assemblage et de montage de la carte électronique ...... 143

B. Outils de visualisation et d'interprétation/décodage des trames logiques...... 159

C. Mesures sur le bus SPI...... 167

D. Mesures sur le bus I2C...... 178

E. Mesures sur la liaison UART...... 190

Solution des exercices 201

Références 247

4

Objectifs

Ce module d'auto formation a pour objectif de développer progressivement une application graphique communicante permettant la lecture et l'affichage de capteurs. Chaque point de formation fera l'objet d'une tâche de mise en œuvre individuelle. Ainsi, les objectifs suivants seront atteints :  Utiliser la bibliothèque des objets Qt (C++) dans l'environnement Linux/Qt-Creator pour créer une application graphique.  Donner un exemple d'organisation de classes pour communiquer avec différents bus et protocoles (I2C, SPI, GPIO, Réseau, RS232C).  une solution informatique multi-tâches en C+ +/Qt, centraliser des informations dans une mémoire partagée (IPC System V encapsulée dans Qt).  Faire le lien entre l'analyse formalisée par le langage SysML et la programmation.  Renforcer vos connaissances pratiques en programmation C+ +.  Effectuer des activités liées aux communications électroniques par l'étude électronique des bus utilisés. Bref, tout un programme...

5

Introduction

En STS SN, il est courant d'utiliser une carte Raspberry pour procéder à l'acquisition de mesures de tous types par différents bus de communication. Les enseignants de l'option EC et IR n'ont pas de difficultés particulières pour réaliser ces acquisitions. Il est souvent nécessaire de créer une carte fille facilitant l'intégration, l'adaptation et la connexion des capteurs/actionneurs. En revanche, développer une application graphique de supervision et de communication relève d'une compétence informatique précise pour exploiter la puissance du système, du langage et beaucoup apprécieraient d'être guidés dans ce domaine. C'est l'objet de cette formation qui présente UNE solution (parmi d'autres) avec la bibliothèque d'objets C++ Qt. Dans un premier temps, nous mettrons en configuration la carte Raspberry pour développer en C++/Qt. Nous prendrons ensuite connaissance de l'environnement de développement Qt-Creator, de la composition d'une application graphique Qt. Ensuite, nous étudierons indépendamment chaque brique nécessaire au développement final de notre application :  Mise en œuvre d'une IHM avec Qt, gestion des signaux/slots (spécifique Qt).  Mise en œuvre de timer (QTimer) afin de définir un fonctionnement à intervalle régulier.  Mise en œuvre d'une communication avec le SGBD MySQL (QSql...).  Mise en œuvre d'une communication réseau client avec un serveur (QTcpSocket...).  Mise en œuvre d'une communication série TTL ou RS232C (QSerial...).  Commande d'une LED sur une broche du GPIO (sans utiliser de bibliothèque existante).  Communication par la bus I2C (sans utiliser de bibliothèque existante).  Communication par le bus SPI (sans utiliser de bibliothèque existante).  Mise en œuvre de threads (QThread) pour permettre le multitâche.  Centralisation d'informations dans une mémoire partagée (QSharedMemory). A travers ces travaux, nous exploiterons abondamment le langage C++ et ferons des rappels. Au final, nous aurons les moyens de construire une application Qt graphique permettant l'acquisition, la commande et la communication des informations selon le cahier des charges fourni ci-après. Pour la pratique, nous travaillerons directement sur une carte Raspberry munie du système d'exploitation Raspbian Lite et le nécessaire de développement d'une application Qt en C++. Il est aussi possible de travailler en compilation croisée (cross compiling), c'est à dire en compilant notre programme en utilisant la puissance de notre ordinateur personnel puis en le déployant sur notre carte cible, la carte Raspberry. Bien sur, pour cela, il faudra installer une chaîne de compilation croisée sur notre PC. C'est long et fastidieux. Tous les sources examinés et plus encore sont disponibles dans les dépôts biblis et appFormQtCpp du site github.

7 Matériel nécessaire pour l'application finale

8

I - Matériel I nécessaire pour l'application finale

Une carte Raspberry Pi, sa carte SD 8Go minimum et le bloc d'alimentation Le modèle n'a pas d'importance même s'il faudra en tenir compte car il y a quelques différences entre les premières versions et les suivantes. En effet, par exemple, la Raspberry 3 utilise les ports séries de manière différente (voir la mise en œuvre de la liaison série).

Raspberry Pi 3 Model B

Broches GPIO utilisées par la carte fille

9 Matériel nécessaire pour l'application finale

Broches utilisées sur le bornier GPIO

Une carte fille d'évaluation Cette carte contient :  Un capteur I2C SHT20 (température et humidité).  Un capteur SPI TC72 (température).  Un afficheur LCD I2C GROVE 2x16 caractères avec rétro-éclairage RGB.  Une LED.  Un bouton poussoir.  Une interface de conversion des signaux série TTL en RS232C (~7.5V).  Des points de test. Le schéma structurel est disponible ci-dessous.

10 Matériel nécessaire pour l'application finale

Schéma électronique final

A noter, le connecteur série propose les signaux Tx, Rx, RTS, GND.

Un câble série La connecteur du câble côté carte fille est de type CANON SUB-D 9 points femelle. Le câble sera croisé si vous le connectez à une autre carte Raspberry ou ordinateur. Voir la documentation de l'appareil dans les autres cas.

Un câble HDMI Pour connecter la carte Raspberry à un écran.

Un cordon réseau Si vous n'utilisez pas une liaison WIFI.

11

II - Cahier des II charges de l'application finale

Cette application est développée en C++/Qt sur le système linux embarqué sur une carte Raspberry. L'application est dotée d'une interface graphique Qt (GUI en anglais ou IHM en français) sur la liaison HDMI et privilégiera l'utilisation des mécanismes de communication Qt (signaux/slots). Les fonctionnalités attendues de l'application finale sont les suivantes :  Mesurer la température et l'humidité dans la pièce (Bus I2C, composant SHT20).  Mesurer la température dans la pièce (Bus SPI, composant TC72).  Allumer une LED "défaut" en cas de dépassement des valeurs seuils (réglables sur l'IHM) des 2 capteurs (GPIO). Cette LED pourra être commandée manuellement s'il n'y a pas de dépassement.  Informer l'utilisateur en cas de dépassement des valeurs des mesures (IHM+écran LCD).  Afficher les mesures sur l'IHM, l'écran LCD et le terminal (période réglable sur l'IHM) (Bus I2C, liaison série, paramètres série réglables sur l'IHM).  Diffuser grâce au réseau (client TCP) les mesures vers un serveur TCP (local ou distant) qui les affichera (paramètres réglables sur l'IHM).  Mettre à disposition les mesures par un serveur TCP intégré (port réglable sur l'IHM).  Sauver les mesures dans une base de données locale ou distante (paramètres réglables sur l'IHM).

13 Cahier des charges de l'application finale

Diagramme de déploiement (dep) du système

14 Cahier des charges de l'application finale

IHM de l'application finale

15

III - Préparation de la III carte Raspberry

J'ai utilisé une carte Raspberry Pi 3 pour effectuer ce travail.

A. Préparation du système d'exploitation

Attention : Carte SD Si vous utilisez une carte SD déjà munie d'un système d'exploitation Raspbian, il faudra adapter le tutoriel ci-dessous. En effet, certains paquets peuvent ne pas être exactement les même, voire ne pas être nécessaires.

Réception de la carte Raspberry A réception d'une carte Raspberry neuve munie d'une carte SD d'origine, sa carte SD est préformatée avec le système NOOBS. Ce système permet de démarrer la carte SD, d'initialiser votre accès Internet et de choisir le système d'exploitation à installer dans une liste. Attention de bien paramétrer la connexion réseau s'il est nécessaire de passer par un proxy pour accéder à Internet. Pour notre cas d'étude, c'est le système Raspbian Lite qui est choisi. Principal avantage, il tient dans 1.6 Go contre 5.9 Go pour la version classique. Il faudra toutefois ajouter l'interface graphique et autres outils de développement. Globalement, l'installation finale prendra moins de place.

17 Préparation de la carte Raspberry

Page d'installation de NOOBS

Après installation, il faut s'authentifier. Par défaut : Login : pi Mot de passe : raspberry

Complément : Copie de carte SD Si vous voulez effectuer une copie d'une carte SD existante, à partir d'un poste Linux, veuillez taper la commande suivante :

1 dd if=/dev/nomdevotrecarte of=/chemin/de/la/sauvegarde bs=1M Cette commande effectue une copie bit à bit de la carte vers un fichier image sur le disque dur de l'ordinateur. Si vous ne possédez pas 2 lecteurs de carte SD (le plus souvent), il faudra donc procéder en 2 étapes :  Copier le contenu de la SD sur votre disque dur (image).  Copier l'image sur la nouvelle carte SD.

Complément : Copie de SD sous Windows Vous pouvez utiliser l'utilitaire Win32DiskImager qui est gratuit.

Configuration du réseau La carte Raspberry Pi3 propose une liaison WIFI ou filaire. Par défaut, les interfaces réseaux se nomment :  wlan0 pour le WIFI  eth0 pour la liaison filaire.

18 Préparation de la carte Raspberry

Complément : Commandes et fichiers pour le réseau

sudo commande permettant d'exécuter une autre commande avec la privilège d'administrateur. exemple : sudo reboot pour relancer la carte Raspberry.

ifconfig Permet d'obtenir les renseignement concernant les interfaces réseau.

ifdown Permet d'arrêter une interface réseau. Exemple : sudo ifdown eth0

ifup Permet la mise en route d'une interface réseau. Exemple : sudo ifup eth0

ip addr Donne des renseignements sur les adresses IP utilisées.

ping Permet de tester la connectivité d'une liaison réseau.

/ Fichier permettant de paramétrer les interfaces réseau. etc/network/interfac es

/etc/dhcpcd.conf Fichier de configuration du service DHCP.

/etc/resolv.conf Fichier utilisé par le service dhcpcd pour écrire l'adresse du serveur DNS reçu du serveur DHCP. Tableau 1 Commandes et fichiers de configuration

Configuration du système L'outil de configuration se nomme raspi-config.

1 sudo raspi-config

19 Préparation de la carte Raspberry

Menu principal raspi-config

Après avoir paramétré le pays, clavier, langue, fuseau horaire, etc. il faut activer les interfaces de communication de la carte :

20 Préparation de la carte Raspberry

Menu raspi-config d'activation des interfaces

Dans notre application, nous aurons besoin d'activer :  SSH si vous souhaitez permettre l'accès à distance via une communication réseau sécurisée.  VNC pour permettre l'administration graphique à distance.  SPI  I2C  1Wire non nécessaire pour cette activité.  Remote GPIO Il est aussi possible de mettre à jour raspi-config à partir du menu principal.

Attention : SERIAL Ne pas l'activer. Cela serait une gêne car cela autorise le mode ligne de commande sur la liaison série, ce qui empêcherait d'utiliser la liaison série pour nos besoins personnels.

Complément : Mise à jour du système Pour s'assurer que le système d'exploitation est bien à jour, procédez ainsi :

1 sudo apt-get update 2 sudo apt-get upgrade 3 sudo apt-get dist-upgrade 4 sudo reboot La première commande va mettre à jour les dépôts de paquets logiciels à partir d'Internet. La seconde met à jour les logiciels.

21 Préparation de la carte Raspberry La troisième met à jour la distribution Linux. Enfin, la dernière commande redémarre la carte pour appliquer les mises à jour.

Complément : NOOBS Bonus pour ceux qui utilisent Noobs sur leur Raspberry Pi. Si vous voulez utilisez Noobs sur votre Raspberry Pi, vous allez encore devoir effectuer une étape supplémentaire, cette étape est à effectuer depuis un ordinateur autre que la Raspberry Pi. Téléchargez la dernière version lite de Noobs et décompressez l'archive sur votre ordinateur. Copiez les fichiers ainsi obtenus sur votre carte SD fraîchement mise à jour à l'exception du fichier nommé « recovery.cmdline ». Voir le document : https://raspbian-france.fr/raspbian-raspberry-pi-3/1

Installation de l'interface graphique La version Raspbian Lite n'installe pas automatiquement d'interface graphique, ce que nous allons faire. Le tutoriel suivant explique la méthode : https://dadarevue.com/ajouter-gui- raspbian-lite/2. Il faut d'abord installer le serveur graphique Xorg :

1 sudo apt-get install --no-install-recommends xserver-xorg

Ensuite l'environnement de bureau PIXEL et le gestionnaire de fenêtre Openbox :

1 sudo apt-get install raspberrypi-ui-mods

Et enfin le gestionnaire de session Lightdm et un navigateur :

1 sudo apt-get install lightdm 2 sudo apt-get install midori 3 sudo reboot L'interface graphique s'affiche au redémarrage de la carte.

B. Installations logicielles pour développer en Qt/C+ +

Installation de Qt5 et QtCreator Procédez de la manière suivante :

1 sudo apt-get install // utilitaire cmake 2 sudo apt-get install qt5-default // Bibliothèque Qt5 (modules de base) 3 sudo apt-get install qtcreator // Environnement de développement 4 sudo apt-get install libqt5serialport5 // module Qt5 de gestion de la liaison série 5 sudo apt-get install libqt5serialport5-dev // module Qt5 de gestion de la liaison série partie développement pour Qt 6 sudo apt-get install libqt5sql5 // module Qt5 de gestion du SQL 7 sudo apt-get install libqt5sql5-mysql // Module Qt5 de communication avec le SGBD MySQL (mariadb)

1 - https://raspbian-france.fr/raspbian-raspberry-pi-3/ 2 - https://dadarevue.com/ajouter-gui-raspbian-lite/

22 Préparation de la carte Raspberry

8 sudo apt-get install libqt5sql5-sqlite // Module pour gérer une base de données SqlLite au lieu de MySQL

dev : Paquet contenant tous les outils et bibliothèques standard du C/C++. cmake : Paquet contenant l'utilitaire cmake utilisé par qtcreator. qtcreator : Paquet contenant l'environnement de développement d'application utilisant la bibliothèque Qt. libqt5serialport5 (et.dev) : Paquet contenant le module Qt de gestion du port série (classes QSerialPort, QSerialPortInfo). libqt5sql5-mysql : Paquet contenant le module Qt de gestion de la communication avec le SGBD MySQL (mariadb).

Conseil : Pour chercher un paquet logiciel ? Tapez la commande suivante :

1 sudo apt-cache search XXX (et touche TAB)

où XXX désigne les premières lettres du paquet que vous recherchez. Après avoir tapé ces lettres, appuyez sur la touche TAB pour voir afficher les paquets disponibles.

Conseil : L'utilitaire

1 sudo apt-get install git

Le paquet git permet de faire de la gestion et sauvegarde de versions, très apprécié lors du développement d'un projet. Le site www.github.com3 permet de sauvegarder vos développements sous forme de dépôts. L'utilitaire git permet de mettre à jour ou récupérer votre dépôt sur le site officiel. Attention toutefois, vos productions seront disponibles pour tous, à moins de payer pour disposer d'un espace privé.

Complément : git/github pour sauver un projet Procédure pour sauver un projet de développement sur github avec git : 1. A l'aide d'un navigateur, ouvrir un compte sur le site www.github.com4. 2. Créer un repository. C'est un espace qui permettra de sauver votre projet en conservant les différentes versions. Exemple de repository ayant pour nom : essai1. Votre repository est joignable à l'adresse : https://github.com/PhASnBenoit/essai1.git5. 3. Placez vous dans le dossier des sources de votre projet et tapez les commandes suivantes :

1 git init 2 git add . // pour tout le contenu du dossier 3 git commit -m "Commentaire identifiant le dépôt" 4 git remote add origin https://github.com/PhASnBenoit/essai1.git 5 git push -u origin master Il faudra fournir à chaque fois les paramètres du compte pour accéder en écriture au repository.

3 - www.github.com 4 - www.github.com 5 - https://github.com/PhASnBenoit/essai1.git

23 Préparation de la carte Raspberry Répétez les 4 dernières commandes à chaque fois que vous voulez publier sur le site github un nouveau dépôt. Il est aussi possible de configurer qt-creator (Environnement de développement) pour utiliser le gestionnaire de versions git.

Complément : git/github pour télécharger (download) un projet existant Il faut cloner le dépôt présent sur github dans un dossier de développement de notre ordinateur. Exemple pour notre projet :

1 git clone https://github.com/PhASnBenoit/appFormQtCpp Bien sur, pour mettre à jour sur github le projet modifié par vos soins, il est nécessaire de connaître le mot de passe de PhASnBenoit, ce que vous n'avez pas. La version clonée présente sur votre ordinateur n'est donc qu'une copie que vous pouvez modifier à volonté sans risque.

Configuration de qt-creator Cet EDI (environnement de développement intégré) est très configurable. Lors de la première utilisation, il faut s'assurer que tous les outils de construction de l'application soient opérationnels.

Paramétrage du kit Desktop

Il faut s'assurer de la présence du type de périphérique, du compilateur, CMake, , d'une version de Qt (onglet suivant).

24

IV - Prise en main de IV l'EDI Qt Creator

A. Présentation de L'EDI (Environnement de Développement Intégré) Qt-Creator

qt-creator qt-creator est un environnement multi-plateformes et multi-langages conçu pour le développement des applications utilisant la bibliothèque Qt. Il comprend les outils de développement :  éditeur  kits de développement permettant de définir pour quelle cible l'exécutable sera créé. Un kit comprend le choix du compilateur, linker, debogueur, la version de la bibliothèque Qt, etc.  Qt Designer pour créer facilement et de manière graphique une IHM  et bien d'autres outils que vous pourrez découvrir en temps utiles.

Créer son projet Pour créer une application graphique Qt, il est nécessaire de créer un projet. Un projet comprend tous les fichiers sources, bibliothèques, graphiques, instructions de compilation, etc. nécessaires à la fabrication du fichier exécutable de l'application.

Exécutez qt-creator.

Cliquez sur File/New Tableau 2 Activités

Vous obtenez la fenêtre ci-dessous :

25 Prise en main de l'EDI Qt Creator

Choix du type de projet

Après validation de ces choix, vous devez entrer le nom du projet (mettez appIhm1) ainsi que le dossier dans lequel il sera stocké. Dans votre espace personnel, créez le dossier devQt. Attention au choix de l'emplacement de stockage pour ne pas placer le projet n'importe où ! Vous pouvez ensuite valider la fenêtre et passer à la suivante.

Choix du kit de développement

Vous devez choisir votre kit de développement. Par défaut, le kit Desktop existe toujours. Sélectionnez-le. Il correspond aux choix par défaut effectués lors de l'installation de qt-creator. Cela signifie que l'exécutable sera compatible avec le système d'exploitation de votre machine de développement. Les autres kits seront créés selon vos besoins, particulièrement si vous souhaitez

26 Prise en main de l'EDI Qt Creator effectuer du cross-development (développement pour une autre cible que votre ordinateur, une carte Raspberry par exemple).

Définition du nom de la classe principale et de ses fichiers correspondants.

Mettez CIhmAppIhm1 comme nom de classe principale gérant l'IHM à la place de MainWindow. C'est plus parlant et indique que c'est la classe principale de l'application qui a la responsabilité de gérer la fenêtre principale, l'interface homme machine (IHM). Validez la fenêtre, la fenêtre suivante permet de définir l'outil de gestion de vos versions. Nous n'en utiliserons pas pour le moment. Validez la fenêtre pour passer à la suivante. Une synthèse s'affiche comme le montre la vue suivante. Les fichiers listés seront créés dans le dossier du projet. Vous pouvez cliquez sur Finish.

Synthèse des fichiers créés

27 Prise en main de l'EDI Qt Creator

débogage. Le symbole marteau permet de construire l'application sans l'exécuter. Après compilation (construction du programme exécutable), vous trouverez les fichiers dans le dossier devQt : drwxr-xr-x 2 philippe users 128 Jun 22 18:51 appIhm1 qui contient les sources du projet de votre application. Ces fichiers seront décrits dans le prochain chapitre. drwxr-xr-x 2 philippe users 173 Jan 30 14:49 build-appIhm1-Desktop- Debug qui contient tous les fichiers ayant servi à construire l'application et le programme exécutable correspondant.

Lancez la compilation du programme sans l'exécuter (marteau).

Exécutez le programme pour vérifier son fonctionnement. Fermez-le.

Contrôler la présence des fichiers dans les dossiers décrits ci-dessus. Tableau 3 Activités

28 Prise en main de l'EDI Qt Creator

La barre de menu de gauche partie haute vous permet :  EDIT : Accéder aux fichiers constituant votre application.  DESIGN : Accéder à votre éditeur d'interface graphique (Qt Designer).  PROJECTS : Accéder aux paramètres du ou des projets ouverts.  ANALYSE : Utilise QLM Profiler pour analyser votre application (non prévu).  HELP : Accès à l'aide s'il est installé correctement (paquet à installer).

Il ne reste plus qu'à définir le fonctionnement objet de votre application et la forme de votre interface graphique. C'est ce que nous allons voir dans les parties suivantes. Mais avant, voyons rapidement comment se passe la compilation, autrement dit la transformation en un programme exécutable.

B. Compilation de l'application

Étapes de compilation Lorsqu'on lance la construction du programme exécutable de notre application, il se passe les choses suivantes : 1. MOC 2. RCC (si utilisé) 3. UIC 4. Compilation de tous les fichiers sources générés par les étapes précédentes 5. Liage (linkage) avec les modules externes à l'application 6. Production de l'exécutable

Étape 1 : MOC (Meta Object ) Le MOC est un compilateur spécifique à Qt. Programmer avec Qt implique d'utiliser des déclarations (macros) dans le code source qui ne sont pas du C++ normalisé. Le MOC examine nos fichiers sources, les transforme en d'autres fichiers sources internes. Ce sont eux qui seront réellement compilés à l'étape 4.

Exemple : Q_OBJECT La macro Q_OBJECT qui épargne au programmeur des lignes de code complexes pour gérer les communications Qt entre objets. Il y en a d'autres.

29 Prise en main de l'EDI Qt Creator

Étape 2 : RCC (Resource Compiler) Le RCC est un compilateur de ressources. Il permet de compiler d'une certaine manière les ressources telles que des images, et de les intégrer à l'exécutable final.

Étape 3 : UIC (User Interface Compiler) Le UIC génère des fichiers C++ à partir de l'IHM construite et sauvées dans le fichier XML d'extension .ui.

Étape 4, 5 et 6 Les dernières étapes sont standard.

30

V - Description d'une V application graphique par défaut Qt

Examinons à présent les différents fichiers composant notre projet Qt.

31 Description d'une application graphique par défaut Qt A. Le fichier de projet appIhm1.pro

Les premières lignes définissent les modules Qt à intégrer à notre application. Les 2 modules de base core et gui sont présents. En effet, la bibliothèque Qt est développée sous forme modulaire. Par défaut, seuls les modules de base de Qt sont liés au projet pour ne pas alourdir inutilement le code exécutable. Par conséquent, l'utilisation de certaines classes Qt nécessitent l'ajout d'un module dans le projet. Les modules les plus courants pour nous :

Classe Module Qt QSerialPort serialport QSql... sql QTcpSocket network Tableau 4 Modules Qt

Vous trouverez dans la documentation Qt le module associé à chaque classe. Exemple pour la classe QserialPort ci-dessous. Le module serialport doit être ajouté dans le fichier de projet appIhm1.pro.

Quel module ajouter ?

32 Description d'une application graphique par défaut Qt B. Les fichiers sources des classes de l'application

Généralité Dans votre projet, les sections Headers et Sources vous donnent accès aux fichiers sources de l'application. Chaque classe créée est définie par deux fichiers :  Le fichier .h d'interface qui décrit la classe (déclaration de la classe, de ses attributs, méthodes).  Le fichier .cpp qui implémente (définit) les méthodes de cette classe. Le fichier .h est inclus au début du fichier .cpp.

Les fichiers .h

L'image ci-contre affiche le fichier mainwindow.h, la définition de la classe de l'application (le nom de la classe principale n'a pas été changé). Cette classe est particulière parce qu'elle est le container principal de l'IHM créée par défaut. D'ailleurs, MainWindow hérite de QMainWindow qui est une classe de gestion d'une fenêtre. A noter qu'une donnée membre ui est déclarée dans la section private de la classe. Elle permettra d'accéder à tous les éléments IHM de notre application depuis cette classe seulement. Pour les futurs développements, il faut veiller à ce que cette classe ne serve qu'à gérer la partie graphique, initialiser les objets métiers et communiquer avec eux. Cela permet de simplifier les éventuelles modifications de l'IHM durant le cycle de vie de l'application. La partie graphique est donc séparée de la partie métier.

Remarque : MainWindow Le nom de cette classe est par défaut lors de la création d'un projet Qt. Dans notre cas, si vous avez suivi les indications précédentes, cette classe se nommera CIhmAppIhm1.

33 Description d'une application graphique par défaut Qt

Les fichiers .cpp

Ces fichiers contiennent la définition des méthodes des classes. Dans le fichier mainwindow.cpp ci contre, nous ajouterons le code de personnalisation de notre IHM, initialiserons nos classes d'application, lancerons les traitements. Notez l'appel à la méthode setupUi() permettant d'initialiser l'accès aux widgets graphique de l'IHM représenté par le pointeur ui dans le constructeur de la classe. C'est à partir de ce moment là que nous pouvons ajouter des traitements graphiques dans le programme. ui étant un pointeur sur une zone de mémoire dynamique, il est nécessaire en fin d'application de libérer cette zone mémoire (delete dans le destructeur).

Rappel : Pointeur en C++ Un pointeur est une variable spéciale car elle contient une adresse de base de zone mémoire. Pour la déclarer :

1 int *var; 2 int var2 = 5; 3 var = &var2;

Dans le code ci-dessus, var est pointeur contenant l'adresse de la variables var2. Il est donc maintenant possible d'utiliser ce pointeur pour lire ou écrire dans la variable var2 :

1 *var = 6; // var2 prendra la valeur 6 2 cout << *var // lire la valeur de var2 Les pointeurs sont beaucoup utilisés pour représenter des objets. Exemple pour créer un objet widget graphique représentant un bouton :

1 QPushButton *pb1; // * signifie que c'est un pointeur 2 pb1 = new QPushButton(this); 3 pb1->setText("Valider"); Ligne 1 : Déclaration du pointeur représentant un objet bouton poussoir. Ligne 2 : Instanciation, création du bouton en mémoire. pb1 pointe sur cet espace mémoire représentant l'objet QPushButton. Ligne 3 : Accès à la méthode setText() du QPushButton. Les caractères -> sont l'opérateur d'accès à un attribut ou méthode d'un objet pointé. Sinon c'est le point. Exemple :

1 QPushButton pb1; // instanciation 2 pb1.setText("Valider");

34 Description d'une application graphique par défaut Qt C. Le fichier contenant la fonction principale

main.cpp

Chaque projet Qt dispose de ce fichier main.cpp. Il est le point d'entrée du programme. La fonction main est la première fonction appelée lors de l'exécution du programme. Explication : Ligne 1 et 2 : Inclusion des fichiers d'interface de la classe MainWindow et QApplication. Mainwindow est définie par notre programme. Le fichier d'interface se trouve dans le dossier du projet, c'est la raison pour laquelle son inclusion est entourée de double quotte, et non des symboles <>. QApplication est une interface définie par Qt et donc se trouve dans un dossier système où se trouvent tous les fichiers d'interface de la bibliothèque. Le chemin est connu du système, c'est la raison pour laquelle son inclusion est entourée des symboles < et >. Ligne 4 : Définition de la fonction main(). Les paramètres de cette fonction permettent de récupérer les arguments en ligne de commande. Ligne 6 : Création (instanciation) de l'objet statique a de type QApplication. Le constructeur de cet objet reçoit les éventuels paramètres en ligne de commande reçus par le fonction main. Ligne 7 et 8 : Instanciation de l'objet w de type MainWindow. Il s'agit de notre interface graphique. Une fois le constructeur exécuté, la méthode show() provoque l'ouverture de la fenêtre. Ligne 10 : Appel de la méthode exec() de notre application. Derrière cette méthode de la classe QApplication se cache le mécanisme de gestion des messages à l'intention de notre application. exec() fonctionne de manière résumée de la façon suivante : BOUCLE Attente d'un événement (souris, clavier, E/S, etc.) lié à notre application (signaux). Orientation de l'événement vers les instructions prévues en réponse à cet événement (slots). FIN BOUCLE Le programme se termine seulement s'il reçoit l’événement de terminaison. La plupart du temps, il n'y a pas lieu de modifier ce fichier.

35 Description d'une application graphique par défaut Qt D. Le fichier décrivant l'interface graphique

Le fichier .ui

fenêtre par défaut. Vous pourrez la redimensionner, changer ses caractéristiques en modifiant les attributs de sa classe de gestion sur la partie droite.  A gauche, vous avez accès aux objets graphiques que vous pouvez faire glisser sur votre fenêtre (form). La construction de votre interface graphique va automatiquement modifier le fichier mainwindow.ui. Ce fichier est au format XML.

Définition : Le format XML Le XML est un langage qui s'écrit grâce à des balises. Ces balises permettent de structurer de manière hiérarchisée et organisée les données d'un document. Il est ensuite plus facile de récupérer ces données. Dans notre cas, ce fichier sera utilisé pour fabriquer la classe d'accès à l'IHM lors de la compilation du projet .

36

VI - Formation pas à VI pas !

A. Conception d'une IHM Qt

1. Les objets graphiques Qt

Les composants graphiques principaux Les composants graphiques Qt (widgets) peuvent être placés directement dans la fenêtre en mode WYSIWYG (What You See Is What You Get). Chacun est décrit par une classe C++ offrant un grand nombre de méthodes et attributs (ou propriétés). Les objets graphiques les plus souvent utilisés sont :

QPushButton Bouton poussoir

QRadioButton Bouton de sélection rond

QCheckBox Case à cocher

QListView Zone de liste d'objets (textes ou autres)

QLineEdit Ligne de saisie de texte

QTextEdit Zone de saisie de texte

QLabel Afficher des labels/images devant des zones de saisie

QComboBox Ligne de saisie avec valeurs prédéfinies Tableau 5 Les objets graphiques Qt

D'autres objets graphiques existent que vous pourrez découvrir lors de vos travaux personnels.

Rappel : Notion de classe Une classe permet de définir les caractéristiques et comportements d'un objet. Si je peux me permettre une image, un moule à gâteaux permet de définir la forme

37 Formation pas à pas ! qu'auront tous les gâteaux. De la même manière, un objet est de la forme d'une classe. Chaque objet a une classe qui le définit. (Voir la formation sur la programmation objet).

2. Gestion des signaux/slots dans une application Qt

Fondamental : Objet Qt Un objet Qt est un objet qui hérite de QObject.

Les signaux/slots Qt fournit un moyen formidable pour communiquer entre les objets Qt : les signaux et les slots.

38 Formation pas à pas !

Attention : signal (Qt) et signal (IPC) Ne pas confondre les signaux Qt et les signaux du système Linux. Dans cette formation, nous ne parlerons que des signaux Qt sauf si c'est précisé.

Définition : Slot Qt C'est la méthode d'un objet Qt qui prendra en charge le travail à effectuer en cas de réception du signal. Il est pour cela nécessaire de signaler dans l'objet récepteur qu'on souhaite "capter" ce signal et l'"associer" au slot. Remarques concernant la figure 1 :  l'objet B a défini dans sa classe le slot seemsThatSomebodyHasClicked(QString who). Tout comme private, protected, public et signals, il existe aussi une section slots dans les objets Qt servant à définir les méthodes réceptrices des signaux reçus.  Le format de la méthode slot doit correspondre au format du signal associé. Ici, on retrouve le paramètre QString. Pour que le signal somebodyClicked(QString who) déclaré dans l'objet A soit "capté" par l'objet B, il faut le signifier dans le constructeur ou une autre méthode l'objet B de la manière suivante :

1 connect(ObjetA, SIGNAL(somebodyClicked(QString who)), this, SLOT(seemsThatSomebodyHasClicked(QString who)));

La méthode connect() des objets Qt permet de connecter un signal à un slot. 4 paramètres sont nécessaires : 1. L'adresse de l'objet émetteur du signal. 2. Le nom du signal à capter encapsulé par la macro instruction Qt SIGNAL. 3. L'adresse de l'objet récepteur (ici this pour indiquer l'objet en cours, c'est à dire l'objet B). 4. Le nom du slot à déclencher encapsulé par la macro instruction Qt SLOT. Cette ligne prépare l'objet B à recevoir le signal somebodyClicked(QString who). Lorsque l'objet A émettra ce signal, il sera pris en charge par le slot seemsThatSomebodyHasClicked(QString who).

3. Ma première IHM

Question [Solution n°1 p 201] Je vous propose de créer une application nommée appIHM1 avec une IHM contenant les champs suivants :  2 boutons (QPushButton nommés respectivement pbEffacer et pbAjouter)  une ligne de texte (QLineEdit nommée leTexte)  une zone de texte (QTextEdit nommée teTexte) 1. Le bouton pbEffacer doit provoquer l'effacement de la zone de texte. 2. Le bouton pbAjouter doit ajouter à teTexte la ligne saisie dans leTexte. Dans un second temps, vous vous apercevrez qu'il est possible d'entrer du texte directement. Supprimez cette possibilité. Indice :

 Le signal/slot du bouton pbEffacer peut être géré de manière uniquement graphique (sans ajout de ligne de code) dans l'éditeur d'IHM

39 Formation pas à pas !

40 Formation pas à pas ! 4. Construction de l'IHM de notre application

IHM de l'application finale

Créer l'IHM de notre système en prenant pour modèle l'image ci-dessus.

Exécutez le programme pour tester l'IHM. Tableau 6 Activités

A présent, nous allons implémenter les fonctionnalités attendues de notre application.

B. Tâches de mise en œuvre des techniques de programmation

1. Utilisation de l'objet QTimer pour effectuer des actions à intervalle régulier.

QTimer est la classe Qt permettant d'exécuter des instructions à intervalle régulier. La propriété importante est interval qui permet de définir l'intervalle de temps. Cette propriété est privée. Il n'est donc possible de la définir que par un accesseur en écriture, la méthode

41 Formation pas à pas ! setInterval()par exemple. Pour lancer le timer, la méthode start() existe. Pour l'arrêter, la méthode stop(). Une fois le timer lancé, à la fin de chaque intervalle de temps, l'objet émet le signal timeout(). Il suffit de connecter ce signal à un slot d'une classe de notre application.

42 Formation pas à pas !

Fondamental : Utilisation des classes Qt Pour chaque utilisation des classes Qt, vous devez :  Vérifier dans la documentation s'il ne faut pas ajouter un module Qt dans le fichier de projet.  Dans tous les cas ajouter l'inclusion du fichier d'interface de la classe dans le fichier d'interface de la classe utilisatrice.

Exemple : Utilisation de QTimer Ajouter au dessus de la définition de la classe (.h): #include Dans la définition de la classe (.h) :

private : QTimer *timer ; private slots : void onTimeout() ;

Dans le constructeur (ou dans une autre méthode) de la classe :

timer = new QTimer(this); timer->setInterval(1000); // 1000ms = 1s connect(timer, SIGNAL(timeout()), this, SLOT(onTimeout())); timer->start();

Dans le destructeur de la classe :

delete timer;

Dans le fichier .cpp de la classe :

void CAppTimer::onTimeout() { // choses à faire }

Commentaires sur les instructions Dans cet exemple, nous avons déclaré dans la classe un pointeur sur un timer. Ce pointeur est initialisé dans le constructeur ou une autre méthode de la classe. L'opérateur new a permis l'instanciation ou création d'un objet timer pointé par la variable timer. Avec la méthode connect(), nous avons demandé l'exécution du slot onTimeout() créé dans la classe à chaque émission du signal timeout() du timer.

43 Formation pas à pas !

Rappel : Pointeur sur un objet En C/C++, lorsqu'on dispose d'un pointeur sur un objet créé dynamiquement (new), l'accès aux attributs et méthodes de cet objet se fait par l'opérateur flèche (->). Lorsque l'instanciation est statique, l'opérateur est le point (.).

Attention : Destruction d'un objet dynamique Veillez à toujours provoquer l'effacement des objets créés dynamiquement, sans quoi, le système risque de manquer de mémoire. Cela se fait le plus souvent dans le destructeur de la classe par le mot clef delete.

Complément : Mot clef this en C++ Le mot clef C++ this désigne l'objet en cours.

2. Utilisation simple d'un objet QTimer

Question [Solution n°2 p 202] Créez un projet nommé appTimer disposant d'une IHM contenant :

Type d'élément Nom dans le code Info

Bouton pbSS Bouton start/stop du timer

Nombre LCD lcdNumber Affichage d'un nombre imitation LCD

Ligne de texte leInterval Réglage de l'intervalle du timer Tableau 7 IHM

et permettant :  De déclencher le timer lors de l'appui sur le bouton pbSS. Attention, le bouton ne peut être actif que si une durée cohérente (>500ms) est présente dans la ligne de texte.  Le bouton pbSS est une bascule et se transforme en bouton stop pour arrêter le timer.  Il n'est possible de changer l'intervalle du timer que s'il est arrêté. Indice :

Pour faire un bouton à bascule, servez vous du texte affiché dans le bouton pour connaître son état. Pour activer ou désactiver un élément graphique, il existe la méthode setEnabled(bool) . Le paramètre est true pour activer l'élément ou false pour le désactiver. Il reste visible.

44 Formation pas à pas !

3. Entrées/sorties GPIO

Les GPIO de la Raspberry Pi du BCM2835 (900 MHz, Pi 1) au 2837 (1.2 GHz, Pi 3)

Les GPIO (General Purpose Input/Output) sont très utilisées en STS SN pour communiquer avec les capteurs/actionneurs. Le composant électronique a évolué en fonction des versions : BCM2837 (Pi 3), BCM2836 (Pi 2) ou BCM2835 (Pi 1 et Zéro). Il existe plusieurs moyens d'utiliser les GPIO en programmation. Image 8 GPIO pins diagram Le plus simple pour les électroniciens est d'utiliser la bibliothèque wiringPi. Vous pouvez suivre un tutoriel d'Internet pour comprendre comment l'utiliser. Ce n'est pas la solution que nous allons choisir. Nous allons en fait utiliser les fichiers systèmes (sysfs) créés par Linux pour contrôler les GPIO. Ces fichiers spéciaux permettent d'utiliser le pilote système du media de communication.

Fichiers systèmes pour les GPIO La gestion des GPIO se trouvent dans le dossier /sys/class/gpio. Dans ce dossier, on trouve 2 fichiers : export et unexport. export donne accès au paramétrage et à la communication avec la broche choisie du GPIO. unexport permet de supprimer l'accès à la broche GPIO.

45 Formation pas à pas !

Quel numéro pour le GPIO ?

Il existe plusieurs manières de considérer une broche du GPIO. Le tableau ci-contre résume la situation. Accéder aux pins du GPIO par le système de fichiers utilise la numérotation du composant électronique BCMXXXX. Il s'agit donc de la colonne BCM. Si vous utilisez la bibliothèque wiringPi, vous devrez utiliser la colonne correspondante. Image 9 Gpio pins

Exemple : Exemple de programmation à partir du terminal de commande avec la GPIO4 La GPIO4 se trouve à la broche 7 du connecteur J8 de la Pi 3. A partir du Shell (terminal de commande), il faudra réaliser les étapes suivantes en tant qu'administrateur (sudo suivi d'une commande permet son exécution en tant qu'administrateur). Réalisez les actions suivantes : 1 - Choix de la broche GPIO4 :

1 sudo echo "4" > /sys/class/gpio/export 2 sudo chmod 777 /sys/class/gpio/gpio4

La ligne ci-dessus écrit la valeur 4 dans le fichier export qui sera utilisé par le driver système du BCM283X pour permettre l'accès à la broche. Un nouveau dossier nommé gpio4 est créé. Il contient les fichiers de paramétrage et de communication avec la GPIO4. ATTENTION, veillez à donner les droits d'accès à ce dossier à partir de votre application. ATTENTION car la création de ce dossier ne peut se faire qu'une fois. Dans votre application, vous devrez en tenir compte. 2 - Il faut ensuite définir la direction : in ou out.

1 sudo echo "out" > /sys/class/gpio/gpio4/direction

En écrivant "out" dans le fichier direction, on définit la GPIO4 comme étant une sortie. 3 - Il est maintenant possible de fixer la valeur de la sortie à 1 ou 0 :

1 sudo echo "1" > /sys/class/gpio/gpio4/value

Le fichier value permet de lire/écrire dans la GPIO4. Quand la sortie est à 1, la tension est de 3.3V). 4 - Une fois terminé, il faut libérer la GPIO de la manière suivante :

1 sudo echo "4" > /sys/class/gpio/unexport

46 Formation pas à pas !

Programmation en C++/Qt La méthode est la même en programmation. Créons une classe de gestion du GPIO dans un fichier cgpio.h :

1 #ifndef CGPIO_H 2 #define CGPIO_H 3 4 #include 5 #include 6 #include 7 #include 8 #include 9 10 #define IN 0 11 #define OUT 1 12 13 class CGpio : public QObject 14 { 15 Q_OBJECT 16 17 public : 18 explicit CGpio(int addr, int dir); 19 ~CGpio(); 20 int lire(); 21 int ecrire(int value); 22 23 private : 24 char mFilename[50]; 25 QString mNomFic; 26 int init(); 27 int gpioExport(); 28 int gpioUnexport(); 29 int gpioDirection(int dir); 30 int gpioRead(); 31 int gpioWrite(int value); 32 int mAddr; 33 34 signals: 35 void erreur(QString msg); 36 }; 37 38 #endif // CGPIO_H 39

La classe hérite de QObject afin de donner la capacité de communiquer par signal/slot. La classe dispose de 2 méthodes publiques : lire() et ecrire() car finalement, c'est tout ce qu'on attend d'une E/S GPIO. Le constructeur de la classe appelle une méthode privée init() permettant d'effectuer tout le processus d'accès à une GPIO, comme décrit ci-dessus. Remarquez que le constructeur de la classe est paramétré. Le paramètre attendu est le numéro de l'E/S GPIO (colonne BCM du diagramme). Voici maintenant l'implémentation des méthodes de la classe :

1 #include "cgpio.h" 2 3 CGpio::CGpio(int addr, int dir) 4 { 5 mAddr = addr; 6 gpioExport(); 7 init(); 8 gpioDirection(dir);

47 Formation pas à pas !

9 } // constructeur 10 11 CGpio::~CGpio() 12 { 13 gpioUnexport(); 14 } // destructeur 15 16 int CGpio::init() 17 { 18 sprintf(mFilename,"/sys/class/gpio/gpio%d/",mAddr); 19 QString command = "sudo chmod - 777 "+QString(mFilename); 20 QProcess *proc= new QProcess(this); 21 proc->start(command); 22 sleep(1); // laisse le temps du changement de droit 23 return 1; 24 } 25 26 int CGpio::gpioUnexport() 27 { 28 char buffer[3]; 29 30 QFile fUnexport("/sys/class/gpio/unexport"); 31 bool res = fUnexport.open(QIODevice::WriteOnly | QIODevice::Text); 32 if (!res) { 33 qDebug() << "CGpio::gpioUnexport: Erreur d'ouverture du fichier !"; 34 emit erreur("CGpio::gpioUnexport: Erreur d'ouverture du fichier !"); 35 return -1; 36 } // if erreur open 37 sprintf(buffer,"%d", mAddr); 38 int nbw = fUnexport.write(buffer, strlen(buffer)); 39 if (nbw != int(strlen(buffer))) { 40 qDebug() << "CGpio::gpioUnexport: Erreur écriture dans fichier !"; 41 emit erreur("CGPIO::gpioUnexport: Erreur écriture dans fichier !"); 42 return -1; 43 } // if nbw 44 fUnexport.close(); 45 return 0; 46 } 47 48 int CGpio::gpioDirection(int dir) 49 { 50 char buffer[3]; 51 QString ficDirection; 52 53 ficDirection = QString("/sys/class/gpio/gpio %1/direction").arg(mAddr,0,10); 54 QFile fDirection(ficDirection); 55 bool res = fDirection.open(QIODevice::WriteOnly | QIODevice::Text); 56 if (!res) { 57 qDebug() << "CGpio::gpioDirection: Erreur d'ouverture du fichier !"; 58 emit erreur("CGpio::gpioDirection: Erreur d'ouverture du fichier !"); 59 return -1; 60 } // if erreur open 61 if (dir==IN) 62 strcpy(buffer,"in"); 63 else 64 strcpy(buffer,"out"); 65 int nbw = fDirection.write(buffer, strlen(buffer)); 66 if (nbw != int(strlen(buffer))) { 67 qDebug() << "CGpio::gpioDirection: Erreur écriture dans fichier !";

48 Formation pas à pas !

68 emit erreur("CGPIO::gpioDirection: Erreur écriture dans fichier !"); 69 return -1; 70 } // if nbw 71 fDirection.close(); 72 return 0; 73 } 74 75 int CGpio::gpioExport() 76 { 77 char buffer[3]; 78 79 QFile fExport("/sys/class/gpio/export"); 80 bool res = fExport.open(QIODevice::WriteOnly | QIODevice::Text); 81 if (!res) { 82 qDebug() << "CGpio::gpioExport: Erreur d'ouverture du fichier !"; 83 emit erreur("CGpio::gpioExport: Erreur d'ouverture du fichier !"); 84 return -1; 85 } // if erreur open 86 sprintf(buffer,"%d", mAddr); 87 int nbw = fExport.write(buffer, strlen(buffer)); 88 if (nbw != int(strlen(buffer))) { 89 qDebug() << "CGpio::gpioExport: Erreur écriture dans fichier !"; 90 emit erreur("CGPIO::gpioExport: Erreur écriture dans fichier !"); 91 return -1; 92 } // if nbw 93 fExport.close(); 94 return 0; 95 } 96 97 98 int CGpio::lire() 99 { 100 char buffer[3]; 101 QString ficValue; 102 103 ficValue = QString("/sys/class/gpio/gpio %1/value").arg(mAddr,0,10); 104 QFile fValue(ficValue); 105 bool res = fValue.open(QIODevice::ReadOnly | QIODevice::Text); 106 if (!res) { 107 qDebug() << "CGpio::gpioLire: Erreur d'ouverture du fichier !"; 108 emit erreur("CGpio::gpioLire: Erreur d'ouverture du fichier !"); 109 return -1; 110 } // if erreur open 111 112 int nbr = fValue.read(buffer, sizeof(buffer)); 113 if (nbr == -1) { 114 qDebug() << "CGpio::gpioLire: Erreur lecture dans fichier !"; 115 emit erreur("CGPIO::gpioLire: Erreur lecture dans fichier !"); 116 return -1; 117 } // if nbw 118 buffer[nbr]=0; // car NULL 119 fValue.close(); 120 return atoi(buffer); 121 } 122 123 int CGpio::ecrire(int value) 124 { 125 char buffer[3]={'0', '1'}; 126 QString ficValue; 127

49 Formation pas à pas !

128 ficValue = QString("/sys/class/gpio/gpio %1/value").arg(mAddr,0,10); 129 QFile fValue(ficValue); 130 bool res = fValue.open(QIODevice::WriteOnly | QIODevice::Text); 131 if (!res) { 132 qDebug() << "CGpio::gpioEcrire: Erreur d'ouverture du fichier !"; 133 emit erreur("CGpio::gpioEcrire: Erreur d'ouverture du fichier !"); 134 return -1; 135 } // if erreur open 136 137 int nbw = fValue.write(&buffer[(value==0?0:1)], 1); 138 if (nbw == -1) { 139 qDebug() << "CGpio::gpioEcrire: Erreur écriture dans fichier !"; 140 emit erreur("CGPIO::gpioEcrire: Erreur écriture dans fichier !"); 141 return -1; 142 } // if nbw 143 fValue.close(); 144 return 0; 145 } 146

50 Formation pas à pas !

Remarque : GPIO entrée ou sortie ? ou les deux ? Cette classe ne permet l'utilisation d'une E/S que dans un seul sens de communication. Pour les périphériques nécessitant les 2 sens de communication, il sera nécessaire de modifier cette classe.

4. Utilisation d'une sortie GPIO

Question [Solution n°3 p 203] Créez un projet nommé appGpio disposant d'une IHM contrôlant une LED, contenant :

Type d'élément Nom dans le code Info

Bouton pbSS Bouton start/stop du GPIO

Label laImage Affichage de l'image représentant la LED allumée ou éteinte.

Combo box cbGpio Numéro de la broche GPIO à utiliser.

Bouton pbActiver Initialise la GPIO. Ce bouton doit ensuite être désactivé. Tableau 8 IHM et permettant :  De choisir la broche raccordée à la LED (cbGpio).  Le bouton pbSS est une bascule et se transforme en bouton stop pour allumer/éteindre la LED.  D'afficher une image dans le label laImage représentant l'état de la LED (donc 2 images). Indice :

IHM : Un label se crée à partir de la classe QLabel. La propriété pixmap permet de placer une image au lieu du texte. Attention toutefois aux type de l'image ! Certains formats ne sont pas reconnus. La propriété scaledContent permet d'ajuster le l'image aux dimensions du widget (laImage). Vous pouvez aussi afficher du texte avec la méthode setText(). APPLICATION : Vous devez utiliser la classe CGpio vue précédemment.

51 Formation pas à pas !

Exemple d'IHM pour l'application

52 Formation pas à pas ! 5. Communication avec le bus SPI

Le bus SPI

Le bus SPI est accessible par les broches du GPIO de la carte Raspberry.

No de broche Nom du signal Rôle

19 MOSI Master Output Slave Input. Informe du sens de communication.

21 MISO Master Input Slave Image 10 GPIO pins diagram Output. Informe du sens de communication.

23 CLK Horloge de synchronisation. La vitesse est réglable logiciellement.

24 CS0 Chip select. Permet d'activer le premier composant communiquant par ce bus.

26 CS1 Chip select. Permet d'activer le second composant communiquant par ce bus. Tableau 9 Le bus SPI de la Raspberry

Ouverture de la liaison de communication L'accès se fait par le système de fichiers, comme pour les autres bus. 2 fichiers existent, /dev/spidev0.0 et /dev/spidev0.1, respectivement pour adresser le composant validé par la broche CS0 et CS1. Après avoir ouvert le fichier, il faut configurer les paramètres de transmission :  Vitesse (la vitesse max dépend du composant).  Synchro de transmission du bit de donnée sur front montant ou descendant du signal CLK.  Niveau actif du CSx. Le driver système utilise plusieurs modes de fonctionnement permettant le réglage des paramètres ci-dessus. Exemple d'initialisation du mode 1 avec choix du niveau actif de CSx ci- dessous avec la méthode init() :

1 int CSpi::init() 2 { 3 char filename[20]; 4 5 sprintf(filename, "/dev/spidev0.%c", m_noCe); 6 m_fileSpi=open(filename, O_RDWR); 7 if(m_fileSpi==-1) { // ouvre le fichier virtuel d'accès à SPI 8 QString mess="CSpi::init Erreur ouverture acces au bus SPI";

53 Formation pas à pas !

9 emit sigErreur(mess); 10 return -1; 11 } // if open 12 quint8 mode=(m_csHigh?SPI_CS_HIGH:0)|SPI_MODE_1; 13 if (ioctl(m_fileSpi, SPI_IOC_WR_MODE, &mode) != 0) { 14 QString mess="CSpi::init Erreur ouverture acces au bus SPI"; 15 emit sigErreur(mess); 16 return -1; 17 } // if 18 if (ioctl(m_fileSpi, SPI_IOC_WR_MAX_SPEED_HZ, & m_speed) != 0) { 19 QString mess="CSpi::init Erreur ouverture acces au bus SPI"; 20 emit sigErreur(mess); 21 return -1; 22 } // if 23 return m_fileSpi; 24 } // init

Notez l'utilisation de la fonction ioctl() permettant d'agir sur le périphérique afin de régler les différents paramètres. La vitesse de transmission choisie doit s'accorder sur celle du composant SPI le moins rapide. Les paramètres utilisés dans la fonction ioctl() sont spécifiques à la nature du périphérique sous-jacent. Il faut donc se référer à la documentation système pour le bus SPI.

Lecture / écriture sur le bus Les lectures/écritures se font par les fonctions standard read() et write() :

1 char buffer[50]; 2 nb = write(mFileSpi, buffer, lg); // écriture 3 // ou 4 nb = read(mFileSpi, buffer, lg); lecture

Fermeture de la communication Utilisez la fonction close() afin de fermer le fichier de communication.

1 close(mFileSpi);

Conseil : Bibliothèque Il est fortement conseillé de créer une classe CSpi permettant la gestion de la communication avec le bus SPI. Les prochaines applications seront plus rapides à développer. En voici un exemple :

1 #ifndef CSPI_H 2 #define CSPI_H 3 4 #include 5 #include 6 #include 7 #include 8 #include 9 #include 10 #include 11 #include 12 #include 13 #include 14 #include 15 16 class CSpi : public QObject 17 { 18 Q_OBJECT

54 Formation pas à pas !

19 20 public: 21 explicit CSpi(QObject *parent = 0, char noCs = '0', int speed=7000000, bool csHigh = false, int mode = SPI_MODE_0); 22 int lireNOctets(quint8 *buffer, int n); 23 int ecrireNOctets(quint8 *buffer, int n); 24 int lireEcrire(quint8 *em, int nbTotal); // cs reste haut 25 26 private: 27 int init(); 28 //int m_addr; // Adresse du composant SPI 29 char m_noCe; // No du device CE/ 30 int m_speed; // vitesse du bus SPI pour CE0 31 int m_fileSpi; // descripteur du fichier Spi 32 bool m_csHigh; // état de CS au repos 33 int m_mode; // mode de fonctionnement du Spi 34 35 signals: 36 void sigErreur(QString msg); 37 }; 38 39 #endif // CSPI_H 40

1 #include "cspi.h" 2 3 CSpi::CSpi(QObject *parent, char noCs, int speed, bool csHigh, int mode) : 4 QObject(parent) 5 { 6 m_mode = mode; 7 m_csHigh = csHigh; 8 m_noCe = noCs; // chip select 9 m_speed = speed; 10 qDebug() << "Démarrage de l'objet CSpi"; 11 init(); 12 } // constructeur 13 14 ///////////////////////////////////////////////////////////////// 15 int CSpi::lireNOctets(quint8 *buffer, int n) 16 { 17 int nb = read(m_fileSpi, buffer, n); 18 if (nb != n) 19 emit sigErreur("CSpi::ecrireNOctets ERREUR lecture"); 20 // qDebug() << buffer[0] << " " << buffer[1]; 21 return nb; 22 } // lire 23 24 ///////////////////////////////////////////////////////////////// 25 int CSpi::ecrireNOctets(quint8 *buffer, int n) 26 { 27 int nb = write(m_fileSpi, buffer, n); 28 if (nb !=n) 29 emit sigErreur("CSpi::ecrireNOctets ERREUR écriture"); 30 return nb; 31 } 32 33 int CSpi::lireEcrire(quint8 *em, int nbTotal) 34 { 35 struct spi_ioc_transfer tr[5]; 36 37 for(int i=0 ; i

55 Formation pas à pas !

44 tr[i].delay_usecs= 0; // par défaut 45 tr[i].cs_change = 0; // ne pas changer le CS, reste haut. 46 } // for 47 int ret = ioctl(m_fileSpi, SPI_IOC_MESSAGE(nbTotal), &tr); 48 // qDebug() << "CSpi::lireEcrire retour ioctl : " << ret; 49 return ret; 50 } // lireecrire 51 52 ///////////////////////////////////////////////////////////////// 53 int CSpi::init() 54 { 55 char filename[20]; 56 57 sprintf(filename, "/dev/spidev0.%c", m_noCe); 58 m_fileSpi=open(filename, O_RDWR); 59 if(m_fileSpi==-1) { // ouvre le fichier virtuel d'accès à SPI 60 QString mess="CSpi::init Erreur ouverture acces au bus SPI"; 61 emit sigErreur(mess); 62 return -1; 63 } // if open 64 quint8 mode=(m_csHigh?SPI_CS_HIGH:0)|m_mode; 65 if (ioctl(m_fileSpi, SPI_IOC_WR_MODE, &mode) != 0) { 66 QString mess="CSpi::init Erreur ouverture acces au bus SPI"; 67 emit sigErreur(mess); 68 return -1; 69 } // if 70 if (ioctl(m_fileSpi, SPI_IOC_WR_MAX_SPEED_HZ, & m_speed) != 0) { 71 QString mess="CSpi::init Erreur ouverture acces au bus SPI"; 72 emit sigErreur(mess); 73 return -1; 74 } // if 75 return m_fileSpi; 76 } // init 77

56 Formation pas à pas !

Complément : Bus SPI Il existe plusieurs variétés de composant SPI.  Ceux qui nécessitent une activation du composant par la broche CE à l'état haut (état bas par défaut au repos). Pour ceux là, il n'est pas possible de les placer en parallèle afin de former un bus, à moins de disposer d'une interface électronique de bus. Un composant de la sorte se trouvera seul sur la ligne CSx du GPIO (cas du capteur de température TC72).  Ceux qui nécessitent une activation du composant par la broche CE à l'état bas. Il est alors possible de les placer un parallèle pour former un bus. Dans ce cas, il sera nécessaire que la classe CSpi devienne une classe singleton pour protéger le fichier d'accès au SPI contre un accès concurrent de plusieurs threads représentant les capteurs (voir chapitre concernant le bus i2c).

6. Communication avec un capteur sur le bus SPI

Question [Solution n°4 p 205] Concevez une application graphique Qt permettant d'afficher la température et l'ID fabricant du capteur (0x54). Le capteur de température TC72 est celui connecté au bus SPI (voir schéma structurel). L'affichage de la température se fera à l'écran en utilisant le widget LCD Number. Indice :

Widget LCD Number : Son fonctionnement est très simple. La méthode display() permet de définir l'affichage. Utilisez la classe CSpi vue précédemment. Il sera utile de concevoir une classe pour le capteur contenant les méthodes getTemperature() et getManufacturer().

7. La communication série

Présentation Qt fournit un interfaçage complet d'une liaison série à travers 2 classes :  QSerialPort qui hérite de QIODevice pour les opérations d'E/S  QSerialPortInfo Afin de pouvoir utiliser ces classes, il est nécessaire de joindre à notre projet de développement un module Qt additionnel : serialport.

57 Formation pas à pas !

Méthode : Ajouter le module Qt serialport

La documentation (extrait de l'image ci-contre) nous permet de connaître le nom du module à ajouter à notre projet : serialport. Pour cela, il suffit de l'ajouter dans le fichier de projet de notre application (.pro) selon l'exemple ci-dessous :

Image 11 Extrait de la documentation de QSerialPortInfo

1 #------2 # 3 # Project created by QtCreator 2016-02-27T15:26:58 4 # 5 #------6 7 QT += core gui serialport 8 9 greaterThan(QT_MAJOR_VERSION, 4): QT += widgets 10 11 TARGET = ditRasp 12 TEMPLATE = app 13 14 target.path = /home/pi 15 INSTALLS += target 16 17 SOURCES += main.cpp\ 18 mainwindow.cpp \

a) QSerialPortInfo

La classe QSerialPortInfo Cette classe fournit des informations concernant les ports séries existants. Une méthode statique availablePorts() permet de rapatrier les informations concernant les ports séries existants. L'exemple de code ci-dessous obtient la liste des ports série disponibles sur l'ordinateur :

1 QList listPsi; // liste des ports série existant 2 listPsi = QSerialPortInfo::availablePorts(); // récupère les ports série disponibles 3 for(int i=0 ; i

QList est une classe permettant de stocker toutes sortes d'objets. Dans notre cas, ce sera des objets de type QSerialPortInfo. Cet exemple ci-dessus permet d'afficher le nom de toutes les voies séries disponibles sur la console de débogage.

Complément : Méthode statique en C++ C'est une méthode qui est indépendante de tout objet. Elle s'appelle directement en faisant précéder du nom de la classe qui la contient suivi de l'opérateur d'accès

58 Formation pas à pas ! (: :). C'est utile lorsqu'un fonctionnement commun à tous les objets est requis. D'autres classes Qt disposent de méthodes statiques (ex : QSqlDatabase).

b) QSerialPort

La classe QSerialPort Cette classe fournit toutes les fonctionnalités de contrôle d'une liaison série. Considérons l'exemple de code ci-dessous :

1 // ouverture de la liaison data 2 mPs = new QSerialPort(this); 3 mPs->setPortName("/dev/ttyUSB0"); 4 mPs->setBaudRate(QSerialPort::Baud9600); 5 mPs->setDataBits(QSerialPort::Data8); 6 mPs->setParity(QSerialPort::NoParity); 7 mPs->setStopBits(QSerialPort::OneStop); 8 mPs->setFlowControl(QSerialPort::NoFlowControl); 9 if (mPs->open(QIODevice::ReadWrite)) { 10 mPs->setRequestToSend(false); 11 qDebug() << "Connected to ttyUSB0"; 12 connect(mPs, SIGNAL(readyRead()), this, SLOT(onReadyRead())); 13 } else 14 qDebug() << "Impossible to connect to ttyUSB0";

Ces lignes de code permettent d'ouvrir le port série ttyUSB0 en lecture/écriture, de fixer les paramètres de communication, le protocole de gestion de flux et même de commander individuellement la ligne RTS (d'autres sont possibles). Ci-dessous un exemple de méthode onReadyRead() d'une classe CCommuniquer qui est le slot de réception du signal ReadyRead() de l'objet QSerialPort.

1 void CCommuniquer::onReadyRead() 2 { 3 QByteArray car; 4 int nb = mPs->bytesAvailable(); 5 for(int i=0 ; iread(1); // exemple de lecture d'un caractère 7 qDebug() << "CCommuniquer::onReadyRead: Lecture d'un caractère : " << car; 8 } 9 }

La méthode bytesAvailable() renvoie le nombre d'octets disponibles dans la zone mémoire tampon de réception de la voie série. Dans cet exemple, les caractères sont lus un par un et affichés sur la console de débogage.

Attention : Signal readyRead Ce signal est généré par l'objet port série dès qu'un ou plusieurs caractères sont arrivés par le port série. Tant que les caractères arrivés ne sont pas lus (extraits du buffer de réception), le signal ne sera pas réémis, même si entre temps, de nouveaux caractères sont arrivés. Il faut donc être particulièrement vigilant et organisé pour gérer la liaison série avec cet événement.

Complément : Cas particulier de la Raspberry 3 Cette carte a subi une évolution dans sa conception.

59 Formation pas à pas ! Le port série est utilisé pour piloter le Bluetooth. De ce fait, il n'est pas possible de l'utiliser sans désactiver le Bluetooth. Il existe bien un autre port série appelé "mini port série" mais il n'est pas très fiable du fait qu'il dépend de l'horloge variable du processeur. La vitesse de transmission n'est donc pas du tout fiable. Il est déconseillé de l'utiliser. Voir l'article sur Internet6. Et aussi celui-là.7 Pour désactiver le Bluetooth, il faut modifier le fichier /boot/config.txt en indiquant :

1 dtoverlay=pi3-disable-bt

Il faut aussi modifier le fichier /boot/cmdline en supprimant :

1 console=serial0,115200

Exemple : Classe de gestion d'une liaison série Cette classe facilite l'utilisation de la classe QSerialPort. Voici CRs232c.h :

1 #ifndef CRS232C_H 2 #define CRS232C_H 3 4 #include 5 #include 6 #include 7 #include 8 9 #ifndef ERREUR 10 #define ERREUR (char)-1 11 #endif 12 #define OK (char)0 13 #define TO 3000 // 3s timeout réception 14 15 class CRs232c : public QObject 16 { 17 Q_OBJECT 18 19 public: 20 explicit CRs232c(QObject *parent = 0, const QString &nomPort = "/dev/ttyUSB0"); 21 ~CRs232c(); 22 int initialiser(QSerialPort::BaudRate vitesse, QSerialPort::DataBits data, 23 QSerialPort::Parity parity, QSerialPort::StopBits nbStop, 24 QSerialPort::FlowControl flow); 25 int ouvrirPort(); 26 char ecrire(const char *trame, int nbOctets); 27 28 private: 29 QSerialPort *m_Sp; 30 QObject *m_parent; 31 32 signals: 33 void sigErreur(QSerialPort::SerialPortError err); 34 void sigData(QByteArray ba); 35 36 public slots:

6 - http://www.framboise314.fr/le-port-serie-du-raspberry-pi-3-pas-simple/ 7 - http://www.poivron-robotique.fr/Liaison-UART-du-Raspberry-Pi.html

60 Formation pas à pas !

37 void onReadyRead(); 38 void onErreur(QSerialPort::SerialPortError err); 39 }; 40 41 #endif // CRS232C_H Cette classe gère 2 signaux pour faciliter les échanges avec les autres objets :  sigErreur() pour informer d'une erreur liée au port série.  sigData() pour communiquer les données reçues. Notez que la méthode lire() n'existe pas car elle est remplacée par l'émission du signal sigData(). Ci-dessous, le fichier CRs232c.cpp implémente les méthodes de la classe :

1 // crs232c.cpp 2 #include "crs232c.h" 3 4 CRs232c::CRs232c(QObject *parent, const QString &nomPort) 5 { 6 m_parent = parent; 7 m_Sp = new QSerialPort(parent); 8 m_Sp->setPortName(nomPort); 9 connect(m_Sp, SIGNAL(readyRead()), this, SLOT(onReadyRead())); 10 connect(m_Sp, SIGNAL(error(QSerialPort::SerialPortError)), this, SLOT(onErreur(QSerialPort::SerialPortError))); 11 qDebug() << "L'objet CRs232c est créé par " << m_parent- >thread(); 12 } 13 14 CRs232c::~CRs232c() 15 { 16 m_Sp->close(); 17 delete m_Sp; 18 qDebug() << "L'objet CRs232c est détruit par " << m_parent- >thread(); 19 } 20 21 int CRs232c::initialiser(QSerialPort::BaudRate vitesse, QSerialPort::DataBits data, 22 QSerialPort::Parity parity, QSerialPort::StopBits nbStop, 23 QSerialPort::FlowControl flow) 24 { 25 m_Sp->setBaudRate(vitesse); 26 m_Sp->setDataBits(data); 27 m_Sp->setParity(parity); 28 m_Sp->setStopBits(nbStop); 29 m_Sp->setFlowControl(flow); 30 return 0; 31 } 32 33 int CRs232c::ouvrirPort() 34 { 35 bool res=false; 36 res=m_Sp->open(QIODevice::ReadWrite); 37 if (!res) { 38 m_Sp->close(); 39 } // if res 40 return m_Sp->isOpen(); 41 } 42 43 char CRs232c::ecrire(const char *trame, int nbOctets) 44 { 45 int lg = m_Sp->write(trame, nbOctets); 46 if ( lg < nbOctets) { 47 return ERREUR; 48 } // if erreur

61 Formation pas à pas !

49 m_Sp->waitForBytesWritten(2000); // stop le thread actuel pour attendre max 2s écriture 50 return lg; 51 } 52 53 void CRs232c::onReadyRead() 54 { 55 QByteArray ba; 56 ba = m_Sp->readAll(); 57 emit sigData(ba); 58 } 59 60 void CRs232c::onErreur(QSerialPort::SerialPortError err) 61 { 62 emit sigErreur(err); 63 }

La méthode slot onReadyRead() lit tous les caractères arrivés et les transmet par le signal sigData(). Le rôle de cette classe n'est pas de gérer un protocole. Pour cela, une classe de plus haut niveau connectera le signal sigData() afin de gérer le protocole du périphérique série.

62 Formation pas à pas !

Attention : Lecture/Ecriture avec QSerialPort Les opérations de lecture/écriture (read(), write())de la classe QSerialPort hérite de QIODevice. La documentation indique que ces méthodes sont asynchrones. Cela signifie qu'une écriture sur le port série n'est effective qu'en sortie de la méthode appelante. Pour forcer l'écriture sur le port avant la sortie de la fonction, il faut utiliser la méthode waitForBytesWritten(2000), comme dans la méthode CRs232c : :ecrire(). Au bout de 2000 ms, si l'écriture n'a pas eu lieu, la méthode renvoie la valeur false sinon true et le signal bytesWritten() est émis.

8. Communiquer par liaison série

Question [Solution n°5 p 210] Le matériel fourni permet la communication série RS232C (ou presque) avec un autre matériel compatible. Réalisez un programme permettant à 2 Pi de communiquer ensemble (envoi et réception de données). Si vous êtes en formation, associez vous avec un collègue pour faire communiquer les 2 Raspberry. Vous êtes libre du choix de l'interface graphique. Indice :

Matériel : Attention au câble utilisé. Il devra inverser les signaux Rx et Tx. Logiciel : Dans l'ordre :  Choix du port série dans la liste.  Ouverture du port série (lecture et écriture).  Création d'un slot pour la réception des informations et affichage de celles-ci.  Possibilité d'émettre un message texte.

9. Communications par le bus I2C

Il n'est pas prévu d'expliquer ici le fonctionnement du bus I2C en détail, d'autant plus que la gestion de la communication sur le bus est assurée par un composant piloté par le driver système, ce qui rend transparent le fonctionnement bas niveau du bus (couche liaison de données).

63 Formation pas à pas !

64 Formation pas à pas ! Rappel : Le Bus I2C Il s'agit d'un bus série synchrone. 2 signaux sont utilisés : SDA et SCL. SDA : Permet la communication des données. SCL : Horloge.

Complément : Le bus I2C sur la Raspberry Les signaux se trouvent sur les bornes 3 et 5 du GPIO de la Raspberry.

GPIO pins diagram

a) Installation du bus I2C sur la Raspberry

Installation du module I2C pour le noyau Linux Vous avez 2 possibilités :  En utilisant l'utilitaire de configuration raspi-config. Il suffit de suivre les indications du logiciel.  A la main comme décrit ci-dessous.

Installation manuelle du module I2C pour le noyau Linux Il faut ouvrir le fichier modules pour y inscrire les modules téléchargés manquant :

1 sudo nano /etc/modules

65 Formation pas à pas ! Les modules à ajouter sont : i2c-bcm2708 et i2c-dev. Vous pouvez les ajouter à la fin du fichier en veillant à ne pas les inscrire deux fois.

Installation des utilitaires I2C Téléchargez les paquets suivants :

1 sudo apt-get install -y python-smbus 2 sudo apt-get install -y i2c-tools

Attention : Blacklist des modules Sur certaines distributions, il existe un fichier nommé raspi-blacklist.conf qui permet d'interdire l'utilisation de certains modules, combien même sont-ils déclarés dans le fichier modules. Il faut vérifier que les modules utiles ne soient pas interdits :

1 sudo nano /etc/modprobe.d/raspi-blacklist.conf Il suffit de placer en commentaire (ajout d'un # en début de ligne) les modules concernés :

1 # blacklist ! 2 3 #blacklist spi-bcm2708 4 #blacklist i2c-bcm2708

Attention : Configuration de la Raspberry au démarrage Au démarrage de la Raspberry, des paramètres de fonctionnement de la Raspberry sont lus à partir du fichier /boot/config.txt. Il faut que les lignes suivantes s'y trouvent pour configurer la Pi à utiliser le bus I2C :

1 dtparam=i2c1=on 2 dtparam=i2c_arm=on

66 Formation pas à pas !

Fondamental : Reboot Une fois ces opérations effectuées, il faut redémarrer la Raspberry :

1 sudo reboot

i Test d'une communication I2C à partir des outils du Shell

Présence d'esclaves I2C connectés à la Raspberry L'utilitaire i2cdetect permet d'identifier l'adresse des modules I2C connectés à la carte Raspberry :

1 sudo i2cdetect -y 1

Utilisation de i2cdetect

Ici, 2 esclaves sont connectés aux adresses 0x40 et 0x70 (0x = préfixe hexadécimale).

Attention : Piège ! Les premières Raspberry utilisent le port 0 au lieu du port 1. Dans ce cas, la commande devient :

1 sudo i2cdetect -y 0 Heureusement, cela ne concerne que les cartes ayant 256Mo de RAM. Les nôtres disposent toutes de 512 Mo de RAM, donc nous utilisons le port 1.

1 Programmation d'une communication par bus I2C

Comme pour les GPIO, nous utiliserons le système de fichiers comme interface de lecture/écriture dans le bus I2C.

67 Formation pas à pas ! Le fichier d'accès au bus I2C est : /dev/i2c-1. (sur les vieilles versions /dev/i2c- 0). Il faut tout d'abord ouvrir ce fichier en lecture et écriture :

1 char filename[20]; 2 sprintf(filename, "/dev/i2c-%c",mNoBus); 3 if((mFileI2c=open(filename, O_RDWR))==-1) { // ouvre le fichier virtuel d'accès à l'I2C 4 qDebug("Erreur ouverture acces au bus I2C"); 5 return -1; 6 } // if open

Définir le numéro de l'esclave cible Pour régler le numéro d'esclave avec lequel communiquer, il faut utiliser la fonction linux ioctl(), qui sert à effectuer des opérations d'entrées/sorties spécifiques à un périphérique, le contrôleur I2C dans notre cas. Le fragment de code suivant permet donc cela :

1 if(ioctl(mFileI2c, I2C_SLAVE, addr)!=0) { // Règle le driver I2C sur l'adresse. 2 qDebug("Erreur ioctl acces au bus I2C"); 3 return -1; 4 } // if ioctl

Le paramètre 2 de la fonction ioctl() (I2C_SLAVE) est la commande spécifique au composant I2C, et permet l'initialisation de l'esclave. Le paramètre 3 est le numéro de l'esclave choisi.

Lecture / écriture sur le bus. L'opération de lecture ou d'écriture sur le bus utilise les fonctions standards read() et write(). Exemple :

1 int nb=read(mFileI2c, buffer, lg);

lg : Nombre d'octets à lire. buffer : Adresse de stockage. Dans le cadre d'un développement C++, il est nécessaire de construire une classe I2C. En voici un exemple :

Le fichier ci2c.h

1 // ci2c.h 2 #ifndef CI2C_H 3 #define CI2C_H 4 5 #include 6 #include 7 #include 8 #include 9 #include 10 #include //Needed for I2C port 11 #include //Needed for I2C port 12 #include //Needed for I2C port 13 #include //Needed for I2C port 14 #include 15 16 class CI2c : public QObject 17 {

68 Formation pas à pas !

18 Q_OBJECT 19 20 public: 21 // creation destruction de l'objet 22 static CI2c *getInstance(QObject *parent = 0, char no = '1'); 23 static void freeInstance(); 24 25 int lire(unsigned char addr, unsigned char *buffer, int lg); 26 int ecrire(unsigned char addr, unsigned char *buffer, int lg); 27 int init(); 28 int getNbLink(); 29 QMutex mutexI2c; 30 31 private: 32 explicit CI2c(QObject *parent = 0, char noBus = '1'); 33 int mAddr; // Adresse du composant I2C 34 char mNoBus; // No d'accès au fichier /dev 35 int mFileI2c; // descripteur du fichier i2C 36 int mNbLink; 37 static CI2c *mSingleton; 38 }; 39 40 #endif // CI2C_H 41

Cette classe est exceptionnelle. Remarquez que :  Le constructeur est dans la section privée de la classe. Cela signifie que l'instanciation d'un objet ne sera pas habituelle mais passera par l'invocation obligatoire d'une méthode statique.  Qu'il existe 1 méthode statique publique pour l'instanciation de l'objet. Son rôle est d'assurer l'unicité de l'instanciation d'un objet CI2c. La raison en est expliquée plus bas.  Qu'il existe 1 méthode statique publique pour libérer l'objet. Son rôle est d'assurer la destruction de l'objet que lorsque plus aucun objet n'utilise le bus I2c.

Rappel : C++ Une méthode statique est indépendante d'un objet. Elle peut être appelée à tout moment de la manière suivante :

1 mI2c = CI2c::getInstance(this, no);

Méthode : Classe singleton Cette classe CI2c est une classe singleton. Cela signifie qu'il n'est possible de ne créer qu'un seul objet de cette classe. Les objets qui l'utilisent devront se partager l'unique objet CI2c. Pourquoi ? L'explication est ci-après. La méthode statique getInstance() procède la première fois à l'instanciation de l'objet, les fois suivantes à la délivrance de l'adresse de l'objet déjà existant. La classe gère le nombre d'objets utilisant l'objet CI2c. Lorsqu'un objet ne désire plus utiliser le bus I2C, un appel de la méthode freeInstance() est requis. La méthode statique freeIntance() se contente de décrémenter le compteur d'objets utilisateur. Lorsqu'il devient nul, l'objet unique est alors effacé de la mémoire.

69 Formation pas à pas !

Fondamental : Pourquoi l'utilisation d'une classe singleton ? Parce que cette classe est nécessaire pour protéger les accès concurrents au bus I2C, représenté par le fichier /dev/i2c-1. Ce fichier ne peut être ouvert qu'une seule fois (fonction open()). Imaginez le développement suivant :  2 capteurs I2C sont connectés au bus (numéro d'esclave différents).  1 objet pour chaque capteur. Chacun des objets initialisera un objet CI2c, provoquant l'ouverture du fichier /dev/i2c-1. L'accès au fichier /dev/i2c-1 sera donc en concurrence entre les 2 objets. Cela n'est pas possible. Afin d'empêcher 2 ouvertures de fichier simultanées, chaque objet utilisera le même objet I2C. Ainsi, l'appel à la fonction d'ouverture du fichier /dev/i2c-1 ne se fera qu'une fois. Mais cela n'est pas suffisant. La plupart du temps, les objets "capteur" sont des processus (ou thread) et par conséquent fonctionnent simultanément. Il est donc nécessaire de garantir l'accès exclusif au bus I2C pour chaque objet. C'est la raison d'être d'un Mutex, ressource système garantissant l'accès exclusif à une ressource. Globalement, la solution apportée par cette technique de développement a pour but de sérialiser les accès au bus I2C (les uns après les autres).

Le fichier ci2c.cpp

1 #include "ci2c.h" 2 3 CI2c::CI2c(QObject *parent, char noBus) : 4 QObject(parent) 5 { 6 mNoBus = noBus; 7 mNbLink=0; 8 } // constructeur 9 10 CI2c * CI2c::mSingleton = NULL; 11 12 int CI2c::lire(unsigned char addr, unsigned char *buffer, int lg) 13 { 14 if(ioctl(mFileI2c, I2C_SLAVE, addr)!=0) { // Règle le driver I2C sur l'adresse. 15 qDebug("Erreur ioctl acces au bus I2C"); 16 return -1; 17 } // if ioctl 18 bzero(buffer, lg); 19 QMutexLocker lock(&this->mutexI2c); // verrouillage du mutex. Il est libéré en sortie de méthode 20 21 int nb=read(mFileI2c, buffer, lg); 22 // qDebug() << "CI2c:lire: " << buffer[0] << " " << buffer[1] << buffer[2] << " " << buffer[3] << buffer[4] << " " << buffer[5]; 23 return nb; 24 } // lire 25 26 int CI2c::ecrire(unsigned char addr, unsigned char *buffer, int lg) 27 { 28 if(ioctl(mFileI2c, I2C_SLAVE, addr)!=0) { // Règle le driver I2C sur l'adresse. 29 qDebug("Erreur ioctl acces au bus I2C"); 30 return -1;

70 Formation pas à pas !

31 } // if ioctl 32 33 QMutexLocker lock(&this->mutexI2c); // verrouillage du mutex. Il est libéré en sortie de méthode 34 35 int nb=write(mFileI2c, buffer, lg); 36 // qDebug() << "CI2c:ecrire: nb=" << nb << " : " << buffer[0] << " " << buffer[1] << buffer[2]; 37 return nb; 38 } // ecrire 39 40 int CI2c::init() 41 { 42 char filename[20]; 43 sprintf(filename, "/dev/i2c-%c",mNoBus); 44 if((mFileI2c=open(filename, O_RDWR))==-1) { // ouvre le fichier virtuel d'accès à l'I2C 45 qDebug("Erreur ouverture acces au bus I2C"); 46 return -1; 47 } // if open 48 return mFileI2c; 49 } // init 50 51 int CI2c::getNbLink() 52 { 53 return mNbLink; 54 } // getNbLink 55 56 CI2c *CI2c::getInstance(QObject *parent, char no) 57 { 58 if (mSingleton == NULL) 59 { 60 qDebug("L'objet CI2c sera créé !"); 61 mSingleton = new CI2c(parent, no); 62 mSingleton->init(); 63 mSingleton->mNbLink=1; 64 } 65 else 66 { 67 mSingleton->mNbLink++; 68 qDebug("singleton already created!"); 69 } 70 return mSingleton; 71 } // getInstance 72 73 void CI2c::freeInstance() 74 { 75 if (mSingleton != NULL) 76 { 77 mSingleton->mNbLink--; 78 if (mSingleton->mNbLink==0) { 79 close(mSingleton->mFileI2c); 80 delete mSingleton; 81 mSingleton = NULL; 82 } // if mNbLink 83 } // if null 84 } // freeInstance 85

Remarques concernant le fichier ci-dessus :  La classe QMutexLocker permet l'utilisation du Mutex. Le déverrouillage du Mutex est automatique, en sortie de méthode.  La méthode getInstance() appelle le constructeur de la classe (procède donc à l'instanciation de l'objet) que lorsque l'objet qu'aucun objet de ce type n'a jamais été créé.  La méthode init() permet l'ouverture du fichier /dev/i2c-1.  Les méthodes lire() et ecrire() sont protégées par le Mutex.

71 Formation pas à pas !

10. Lecture de la température et de l'humidité

Question [Solution n°6 p 210] Concevez une application graphique Qt permettant d'afficher la température et l'humidité (capteur SHT20 sur le diagramme structurel). Indice :

Il faut utiliser la classe de gestion de l'I2C, créer une classe pour le capteur et enfin rassembler tout cela dans une application.

11. Accès à une base de données MySQL

Le SGBD MySQL est un SGBD (Système de Gestion de Base de Données). Il s'agit d'un logiciel serveur. Le serveur MySQL peut se trouver sur n'importe quel poste accessible du réseau. Il est nécessaire de disposer d'un logiciel client pour s'y connecter. Une fois connecté, le langage de communication avec le serveur est le SQL. Le plus souvent, nous utilisons le SQL pour extraire les données d'une base de données (BDD), préalablement créée ou pour y écrire des données.

Complément : Système embarqué Lorsqu'on désire gérer des données sans vouloir utiliser trop de ressource (mémoire, cpu), il est possible d'utiliser sqlite au lieu de MySQL. Il s'agit d'un fichier contenant la base de données sans serveur. Le driver existe pour Qt.

Base de données Une base de données contient une ou plusieurs tables. Chaque table contient des colonnes portant un titre et des caractéristiques sur le contenu (type d'information attendu). Un enregistrement correspond à une ligne dans une table.

Exemple : Table user

Id Nom prenom login mdp

1 ANTOINE Philippe antoine 652v4f65v24w 65

2 SILANUS Marc silanus dssdf654wf5dfs 6 Tableau 10 Table user

Cette table comporte 2 enregistrements. Le champ Id se nomme la clef primaire de la table. Chaque valeur est unique. Le champ login est unique. Il ne peut exister 2 valeurs identiques, on comprend pourquoi. Sinon me demander. Le données du champs mdp sont cryptées.

72 Formation pas à pas !

Administration d'une base de données Une base de données MySQL s'administre par un client comme dit précédemment. En ligne de commande, le client se nomme mysql (très original) et nécessite une très bonne connaissance du langage SQL pour discuter avec le SGBD (créer les droits d'accès au SGBD, créer une base de données, définir ses droits d'accès, son contenu). Heureusement, il existe un paquet nommé phpMyAdmin permettant l'administration graphique du SGBD. phpMyAdmin est construit sous la forme d'un site Web en PHP. Il nécessite donc pour fonctionner un serveur Web. Apache2 est le serveur Web qu'il nous faut pour le rendre accessible par n'importe quel navigateur Web.

Pour résumer, afin de disposer d'un SGBD administrable, il faut :  Le SGBD MySQL  Le serveur Web Apache2  Le site d'administration de MySQL phpMyAdmin. Ces trois entités doivent être installées et opérationnelles sur un ordinateur. Cela peut être votre poste de travail ou tout autre poste disponible sur le réseau.

Remarque : phpMyAdmin Heureusement, l'installation du paquet phpMyAdmin se fait dans le bon dossier du serveur Apache2 (/srv/www/htdocs). Ainsi, après son installation, il est directement accessible par un navigateur en tapant : http://AdresseIP/phpMyAdmin/ ou http://localhost/phpMyAdmin/ si l'installation se trouve sur votre ordinateur (ou Raspberry).

a) Mise en place de la BDD

Résumé des activités à effectuer côté serveur

En vous aidant de ce qui suit :  Installez si nécessaire les paquets MySQL, Apache2, phpMyAdmin  Mettez en service MySQL  Mettez en service Apache2  A l'aide d'un navigateur, ouvrez le site phpMyAdmin en tant qu'administrateur  Créez la base de données  Créez un utilisateur ayant les droits de base sur la base de données Tableau 11 Activités

Installation des paquets Sur une Raspberry, tapez : sudo apt-get install mysql mysql-client sqlite3 Sur un PC sous linux openSuSE, utilisez Yast2, l'utilitaire d'administration et

73 Formation pas à pas ! cherchez le serveur mariadb, nouveau nom pour MySQL.

Mise en service des serveurs MySQL et Apache2 Vous pouvez taper : sudo service apache2 start sudo service mysql start ou sudo systemctl start mysql.service sudo systemctl start apache2.service selon les systèmes.

Complément : Commande systemctl start pour lancer un service. stop pour stopper un service. restart pour redémarrer un service. enable pour lancer le service au démarrage du système. disable pour supprimer le lancement au démarrage du service.

Création de la base de données La BDD se nommera bddFormation. Réalisez les opérations suivante avec phpMyAdmin après vous être connecté en tant qu'administrateur :

Vue de phpMyAdmin une fois connecté

Cliquez sur New pour procéder à la création de la base de données.

74 Formation pas à pas !

Créer une base de données

Entrez les renseignements ci-dessus et validez.

Création d'une table

Créez la table nommée user contenant 5 colonnes et validez.

Créez les 5 champs tels que représentés sur l'image ci-dessus. Validez.

Table créée

Quelques remarques sur la création de la table user :

75 Formation pas à pas !  Le champs id est une clef primaire (petite clef jaune à côté du nom du champ). Le champ est en auto-incrémentation. Il n'y a donc pas lieu de le remplir. C'est le serveur MySQL qui se charge d'établir de manière unique sa valeur.  Le champs login dispose de la propriété UNIQUE. Cela signifie qu'il ne peut y avoir deux fois la même valeur.  Le type de données VARCHAR doit être suivi du nombre de caractère maximum que peut contenir le champ.

Création d'un utilisateur ayant tous les droits seulement sur la BDD bddFormation

C'est le bon moment pour créer un compte d'accès au SGBD avec tous les droits sur la BDD bddFormation.

Insérer des données dans la table

Insérez des données, au moins 1 enregistrement tel que représenté ci-dessus.

76 Formation pas à pas ! Notez :  Le champs id n'est pas rempli eu égard à ce que nous avons expliqué ci- dessus.  La valeur du champs mdp (mot de passe) est chiffrée en utilisant l'algorithme SHA1. Il sera indispensable d'utiliser cet algorithme dans toute requête ayant besoin de comparer le mot de passe.

Enregistrement inséré

 Le champs id s'est vu affecté la valeur 1 par le serveur MySQL.  Le mot de passe est crypté avec SHA1. i Et si on passait à la programmation maintenant ?

Connection à la base de données Il existe un objet Qt pour cela : QSqlDatabase.

Exemple : Exemple de connexion à la base de données

1 QSqlDatabase db = QSqlDatabase::addDatabase("QMYSQL"); // voir complément ci-dessous pour cette syntaxe particulière 2 db.setHostName("192.168.2.25"); // on peut mettre aussi localhost si le SGBD est local 3 db.setDatabaseName("test"); 4 db.setUserName("user"); 5 db.setPassword("user"); 6 bool ok = db.open();

La méthode addDatabase() gère la connexion et la communication avec un serveur MySQL. Les renseignements ci-dessus sont indispensables à fournir. N'oubliez pas de tester la variable ok pour vérifier que la connexion est ouverte.

Complément : addDatabase est une méthode statique En C++ une méthode static signifie qu'elle est globale. Il n'est pas nécessaire d'instancier un objet pour l'utiliser, elle est indépendante de tout objet. Dans notre exemple, nous utilisons directement la méthode static addDatabase() pour nous retourner une instance d'objet QSqlDatabase. D'où cette syntaxe un peu particulière.

77 Formation pas à pas !

Effectuer des requêtes SQL sur la base de données Les besoins les plus fréquents sont de récupérer/Ajouter/effacer/modifier des données de la BDD. Cela se traduit par des requêtes SQL qui ont la forme suivante :

Récupérer des données SELECT * FROM user ; Récupère toutes les lignes de toutes les colonnes de la table user.

Modifier des données UPDATE user SET Met à jour le nom d'un nom='toto' WHERE utilisateur. nom='ANTOINE';

Effacer des données DELETE FROM user Efface l'utilisateur. WHERE nom='ANTOINE' ;

Ajouter des données INSERT INTO user VALUES Ajoute un utilisateur en ('','TOTO', 'titi', 'toto', chiffrant le mot de passe. SHA1('toto')) ; Tableau 12 Requêtes SQL de base

Complément : Requêtes SQL La clause WHERE peut s'appliquer à la plupart des requêtes pour filtrer un contenu.

Effectuer une requête d'extraction de données C'est l'objet QSqlQuery qui sert à effectuer des requêtes sur la BDD. Voici un exemple :

1 model = new QStringListModel(this); 2 QStringList liste; 3 QSqlQuery q(db); 4 q.exec("SELECT * FROM user ORDER BY nom"); 5 while (q.next()) { 6 liste << q.value("login").toString(); 7 } // for 8 model->setStringList(liste); 9 ui->lvTable->setModel(model);

Ligne 3 : Déclaration d'un objet q relié à notre BDD. Ligne 4 : Utilisation de la méthode exec() avec comme paramètre la fameuse requête SQL. Cette requête récupère tous les enregistrements de la table user par ordre alphabétique des valeurs de la colonne nom. Ligne 5 : Pour accéder au premier enregistrement récupéré, il faut absolument invoquer la méthode next(). Boucle tant qu'il existe un enregistrement suivant. Ligne 6 : Sauvegarde dans liste de tous les valeurs de la colonne login de la table. liste est un objet QStringList qui permet le stockage sous forme de tableau de QString. Notez la méthode value() qui permet de récupérer la valeur de l'enregistrement pour une colonne dont le nom est passé en paramètre. Les deux dernières lignes permettent d'afficher les valeurs dans l'IHM (QListView).

Effectuer une requête d'ajout dans la table user

1 QSqlQuery q(db);

78 Formation pas à pas !

2 q.prepare("INSERT INTO user VALUES (NULL, :name, :pname, :login, SHA1(:mdp))"); 3 q.bindValue(":name", ui->leNom->text()); 4 q.bindValue(":pname", ui->lePnom->text()); 5 q.bindValue(":login", ui->leLogin->text()); 6 q.bindValue(":mdp", ui->leMdp->text()); 7 q.exec();

Ligne 2 : La méthode prepare() permet de former une requête SQL qui contient des paramètres. Les paramètres sont indiqués par le signe : suivi du nom du paramètre (exemple dans cette requête : :name). La fonction SQL SHA1() permet de chiffrer mot de passe avant de le sauver dans la BDD. Lignes 3 à 6 : La méthode bindValue() permet le remplacement d'un paramètre par sa valeur réelle.

Effectuer une requête de modification d'enregistrement

1 QSqlQuery q(db); 2 q.prepare("UPDATE user SET nam=:name, prenom=:pname, mdp=SHA1(:mdp) WHERE login=:login LIMIT 1"); 3 q.bindValue(":name", ui->leNom->text()); 4 q.bindValue(":pname", ui->lePnom->text()); 5 q.bindValue(":login", ui->leLogin->text()); 6 q.bindValue(":mdp", ui->leMdp->text()); 7 q.exec(); Ligne 2 : Ici, la requête contient des paramètres insérés comme vu précédemment. La clause finale LIMIT 1 permet de n'agir que sur un seul enregistrement, le premier trouvé. Puisqu'on modifie un mot de passe, il ne devrait de toute façon y en avoir qu'un. C'est donc un garde-fou.

Effectuer une requête d'effacement d'enregistrement

1 QSqlQuery q(db); 2 q.prepare("DELETE FROM user WHERE login=:login LIMIT 1"); 3 q.bindValue(":login", ui->leLogin->text()); 4 q.exec(); Ligne 2 : Rien de nouveau si ce n'est cet exemple de requête d'effacement.

Résumé Vous avez appris dans ce module à paramétrer le SGBD MySQL, créer une base de données, insérer des données grâce au site d'administration phpMyAdmin. Vous avez vu comment accéder à la BDD par la programmation Qt. Exerçons nous maintenant par un exercice.

b) Conception d'une base de données

Méthode MERISE Pour faire vite, cela consiste à :  Chercher les informations dans le cahier des charges à stocker dans la BDD.  Établir un Modèle Conceptuel de Données (MCD).  En déduire un Modèle Logique de Données (MLD).  Traduire le MLD en une BDD physique (langage SQL).

79 Formation pas à pas !

12. Lire et sauver des informations dans une base de données

Question [Solution n°7 p 217] 1. A l'aide de phpMyAdmin, créez la base de données nommée bddFormation (si ce n'est pas déjà fait !) et la table user conformément à la partie précédente "Accès à une base de données MySQL". 2. Remplissez la table avec des valeurs de votre choix. 3. Concevez un programme nommé appSgbd ayant une IHM composée de 2 parties. La première permet d'afficher la liste des noms de la table user. En cliquant sur un nom, il est possible de mettre à jour le nom et login ou d'effacer cet utilisateur. 4. La deuxième partie permet la création d'un nouvel utilisateur (vous pouvez vous inspirer de l'IHM ci-dessous).

IHM de l'application appSgbd

Indice :

Connexion à la base de données : Utilisation de la méthode statique de QSqlDatabase en précisant le pilote à utiliser :

1 db = QSqlDatabase::addDatabase("QMYSQL");

Widget QListView : Quand on clique sur une ligne de la QListView, le signal clicked(QModelIndex) est émis. Il faut capter ce signal et le raccorder à un slot de la classe.

1 connect(ui->lvTable, SIGNAL(clicked(QModelIndex)), this, SLOT(onSelectLineList(QModelIndex)));

Ici, le signal clicked(QModelIndex) du widget QListView est connecté au slot onSelectLineList(QModelIndex) créé dans la classe CIhmAppSgbd.

80 Formation pas à pas ! Le paramètre émis avec le signal (QModelIndex) désigne l'objet de gestion de la ligne sélectionnée.

1 void CIhmAppSgbd::onSelectLineList(QModelIndex mi) 2 { 3 QSqlQuery q(db); 4 q.prepare("SELECT * FROM user WHERE login=:login"); 5 q.bindValue(":login", mi.data().toString()); 6 q.exec(); 7 q.next(); 8 ui->leModifLogin->setText(q.value("login").toString()); 9 ui->leModifNom->setText(q.value("nom").toString()); 10 ui->lId->setText(q.value("id").toString()); 11 }

La méthode data() de l'objet mi permet d'accéder au contenu de la ligne.

13. Communications TCP par le réseau

Principe de la communication réseau

Les protocoles les plus répandus sur le réseau sont les protocoles des réseaux TCP/IP. TCP est une famille de protocoles standards et correspond à la couche transport de l'information. IP correspond à une autre famille de Image 12 Modèle OSI protocoles standards et correspond à la couche réseau (routage de l'information). Le modèle de communication est dit client/serveur. Le principe est le suivant : Le client se connecte au serveur à l'écoute sur le réseau. Une fois la connexion établie, les informations peuvent s'échanger selon un protocole de couche application (cela peut être un protocole maison).

81 Formation pas à pas !

Exemple : Maison / Entreprise

Pour rapidement comprendre l'essentiel concernant la programmation réseau, on peut comparer un ordinateur client à une maison, un ordinateur serveur à une Image 13 Analogie entreprise. La maison dispose d'une adresse postale. L'entreprise dispose aussi d'une adresse postale, peut-être même plusieurs. Lorsqu'on veut acheminer un courrier de la maison jusqu'à l'entreprise, il ne suffit pas d'indiquer l'adresse postale mais aussi le service concerné par ce courrier. Dès réception du courrier dans l'entreprise, il est acheminé jusqu'à la bonne porte du bureau de service concerné. ------Il en est de même en programmation réseau. L'adresse de la maison est l'adresse IP du poste client. L'adresse de l'entreprise est l'adresse IP du serveur. Le service concerné correspond au numéro de port (0-65535). ------Une information envoyée d'un poste client vers un poste serveur utilisera de part et d'autre un port (TCP) de communication. Côté client, le numéro de port est choisi dynamiquement par le système. Côté serveur, le numéro de port est défini par le service (logiciel serveur) à l'écoute.

TCP ou UDP ? Ces deux protocoles sont différents, donc pour un usage différent. TCP implique l'établissement d'une connexion entre le client et le serveur. Elle peut être refusée par le serveur ou mise en attente. Une fois les ordinateurs connectés, les échanges sont systématiquement acquittés par le récepteur. La fiabilité des informations échangées est assurée. Le contenu des informations est vérifiable par un CRC16. UDP ne demande pas de connexion au serveur. Il s'agit d'un envoi direct d'informations appelé datagramme. Pas d'acquittement par le récepteur. Le contenu des informations est vérifiable par un CRC16. ------La plupart des protocoles de couche application utilisent le protocole TCP (HTTP, FTP, etc.). Le protocole UDP est privilégié pour échanger des informations liées à un flux permanent (transmission d'une mesure par exemple). Ainsi, si un datagramme n'arrive pas à destination, cela ne remet pas en question la cohérence de la communication.

Socket de communication Pour communiquer sur un réseau TCP/IP, il faut définir un certains nombre de renseignements tels que l'adresse IP source/destination, le port source/destination, etc. Pour un serveur, l'information à personnaliser est le numéro de port d'écoute.

82 Formation pas à pas ! Pour un client, l'information à personnaliser est l'adresse IP du serveur et le numéro de port du service à atteindre. Les autres informations sont la plupart du temps définie automatiquement par le système. Ces renseignements sont dans des structures de données et forme "un package" appelé une socket de communication. Une socket est associée à un port de communication et c'est le moyen utilisé pour émettre ou recevoir des informations.

Complément : Module Qt pour programmer Pensez à ajouter le module network dans le fichier .pro à tout projet de développement utilisant les classes Qt de gestion du réseau.

a) Programmation côté serveur

L'objet QTcpServer La classe QTcpServer permet de faciliter grandement la programmation d'un serveur. La création d'un serveur se programme de la manière suivante :

1 serveur = new QTcpServer(this); 2 serveur->listen(QHostAddress::Any, 2222); 3 connect(serveur,SIGNAL(newConnection()), this, SLOT(onNewConnection())); 4

Ligne 1 : serveur désigne l'objet serveur TCP, rattaché à notre IHM. Ligne 2 : La méthode listen() permet de définir le port TCP d'écoute (2222), ainsi que les interfaces d'écoute (QHostAddress::Any). QHostAddress::Any ordonne que toutes les interfaces réseau disponibles sur le serveur soient à l'écoute. Il est aussi possible de limiter le serveur de manière à n'écouter que sur une seule interface réseau. Ligne 3 : Connexion du signal newConnection() au slot onNewConnection(). Le signal est émis par l'objet serveur lorsqu'un client se connecte. Ce slot permet de gérer la communication avec le client :

1 void CIhmAppServeurTcp::onNewConnection() 2 { 3 qDebug() << "onNewConnection"; 4 ui->teTexte->append("onNewConnection"); 5 6 sock = serveur->nextPendingConnection(); 7 connect(sock, SIGNAL(readyRead()), this, SLOT(onReadyRead())); 8 connect(sock, SIGNAL(disconnected()), this, SLOT(onDisConnected())); 9 } La méthode nextPendingConnection() permet de récupérer l'adresse d'un objet QTcpSocket disposant de toutes les informations du client pour communiquer avec lui. Il est ensuite possible de connecter le signal :  readyRead() pour être prévenu de l'arrivée de caractères en provenance du client.  disconnected() pour être prévenu si le client se déconnecte. Examinons un exemple de slot onReadyRead() :

83 Formation pas à pas !

1 void CIhmAppServeurTcp::onReadyRead() 2 { 3 ui->teTexte->append(sock->readAll()); 4 }

La méthode readAll() de l'objet QTcpSocket permet de lire les caractères reçus (envoyés par le client) et les affiche dans un widget QTexteEdit.

84 Formation pas à pas !

Conseil : Gestion des clients Un serveur gère en générale plusieurs clients. Pour les différencier, il est nécessaire de mémoriser les adresses des objets QTcpSocket dans une liste pour les distinguer. Cette liste doit être maintenue à jour en cas de déconnexion. Il est souvent utile que les clients soient gérés de manière indépendante. La solution est que chaque objet de gestion d'un client devienne un thread ou processus indépendant. Un serveur peut ainsi gérer simultanément plusieurs clients. C'est le cas d'Apache2, le serveur WEB bien connu. La commande ps -e vous permettra de découvrir les multiples processus formant Apache2. Chaque processus Apache2 gère un client connecté ou est en attente de connexion (pour un gain de temps). Un processus Apache2 reste à l'écoute d'une connexion entrante d'un client.

ps -e

La partie multi processus sera étudiée plus loin.

b) Programmation côté client

L'objet QTcpSocket L'objet QTcpSocket encapsule toutes les informations d'une socket et fourni des méthodes simplifiant la programmation d'un client réseau.

Initialisation du client Cela commence par la déclaration dans la classe IHM d'un pointeur sur l'objet QTcpSocket.

1 QTcpSocket *sock; L'instanciation peut se faire dans le constructeur de la classe IHM :

1 sock = new QTcpSocket(this);

85 Formation pas à pas !

2 connect(sock, SIGNAL(connected()), this, SLOT(onConnected())); 3 connect(sock, SIGNAL(disconnected()), this, SLOT(onDisconnected())); 4 connect(sock, SIGNAL(readyRead()), this, SLOT(onReadyRead())); 5 connect(sock, SIGNAL(error(QAbstractSocket::SocketError)), this, SLOT(onSocketError(QAbstractSocket::SocketError))); 6

Dès l'instanciation effectuée, les différents signaux sont connectés aux slots correspondants. onConnected() pour gérer l'après connexion au serveur. onDisconnected() pour gérer la déconnexion du serveur. onReadyRead() pour lire les informations émises par le le serveur. error(QAbstractSocket::SocketError) pour gérer les erreurs de communication. Le paramètre reçu détaille l'erreur qui est survenue.

Connexion au serveur Exemple après clique sur un bouton de connexion. Au préalable, l'adresse et le port du serveur ont été saisie dans des QLineEdit.

1 void CIhmAppClientTcp::on_pbConnecter_clicked() 2 { 3 sock->connectToHost(ui->leAdrServeur->text(), ui->lePortServeur- >text().toInt()); 4 } 5

Réception d'informations du serveur Le signal readyRead() permet d'être averti de l'arrivée d'informations. Exemple de slot :

1 void CIhmAppClientTcp::onReadyRead() 2 { 3 QByteArray qba = sock->readAll(); 4 ui->teReception->append(QString(qba)); 5 }

La méthode readAll() permet de lire les octets reçus et les stocker dans un tableau sous forme d'objet QByteArray. Heureusement, pour afficher le texte correspondant, il existe la possibilité de construire un QString à partir d'un QByteArray.

Envoi d'informations vers le serveur On utilise la méthode write() ou writeData(). Exemple de slot :

1 void CIhmAppClientTcp::on_pbEnvoyer_clicked() 2 { 3 sock->write(ui->leEmission->text().toStdString().c_str()); 4 }

Le paramètre attendu dans la méthode write() est une chaîne de caractères standard C. Heureusement, la classe QString le prévoit par la méthode toStdString() qui elle même dispose d'un moyen de récupérer l'adresse de la chaîne : c_str().

Gestion des erreurs réseau Le signal errorSocket() fourni la valeur de l'erreur. Cette valeur est récupérée

86 Formation pas à pas ! dans le slot correspondant. En voici un exemple :

1 void CIhmAppClientTcp::onSocketError(QAbstractSocket::SocketError error) 2 { 3 switch (error) { 4 case QAbstractSocket::ConnectionRefusedError: 5 ui->teReception->append("Connexion refusée par le serveur !"); 6 break; 7 case QAbstractSocket::NetworkError: 8 ui->teReception->append("Coupure de liaison réseau !"); 9 break; 10 default: 11 ui->teReception->append("Erreur réseau !"); 12 break; 13 } // sw 14 } Seuls sont décrits ici quelques cas d'erreurs possibles. Il en existe davantage décrits dans la documentation Qt.

14. Communication réseau client/serveur

Question [Solution n°8 p 224] Concevez une application serveur et une application cliente dont les IHM sont conformes aux suggestions ci-dessous. Le client comme le serveur doit pouvoir émettre et recevoir des informations. La gestion des erreurs de communication doit être effective.

IHM de l'application client TCP

87 Formation pas à pas !

IHM de l'application serveur TCP

Indice :

Tout est dans la formation (page précédente).

88 Formation pas à pas !

SD des deux programmes à réaliser (diagramme de classes)

15. Mise en place de traitements parallèles avec les threads

Définition : Thread La traduction informatique de thread signifie fil d'exécution. On appelle cela aussi un processus léger en français. Dans la pratique, il s'agit d'un bloc de code au sein d'une application dont l'exécution est parallèle au thread principal que représente le plus souvent l'IHM de l'application. Il est ainsi possible d'augmenter la vitesse d'exécution d'un programme en parallélisant les calculs. Le processus est dit léger car il ne nécessite pas l'allocation de segment mémoire comme pour la création d'un processus classique. Sa création est donc plus rapide,

89 Formation pas à pas ! mais il est dépendant du processus principale (thread principal). Si le processus principale cesse d'exister, les threads qui en dépendent aussi.

Fondamental : Thread Un thread appartient au même espace mémoire que le processus principal.

Thread et mémoire

Exemple : Utilisation des threads Un programme effectue l'acquisition de mesures sur des capteurs. Chaque capteur est géré par un objet le représentant. Chaque objet pourrait gérer un thread interrogeant cycliquement les capteurs et rendant disponibles les mesures, de manière à soulager le programme principale qui se concentrerait sur d'autres activités comme l'IHM. C'est la stratégie qui a été choisie dans l'application finale.

L'objet QThtread Qt fournit un objet QThread pour faciliter l'utilisation d'un thread. Cet objet permet la gestion d'un thread à partir du thread (processus) principal.

run() Point d'entrée du thread. C'est dans cette méthode que se trouvent les instructions du thread.

start() Lance l'exécution du thread. La méthode run() est invoquée automatiquement. Tableau 13 Méthodes et attributs remarquables d'un objet QThread

Il existe plusieurs moyens de créer un thread. Pour les comprendre, il faut savoir ce qu'est l'héritage en C++. C'est un concept de programmation objets qu'on rencontre dans tous les langages objets.

90 Formation pas à pas !

Complément : L'héritage en C++

Une classe peut hériter d'une autre. Analogie avec un humain qui hérite de son parent (on ne parlera pas de l’État qui prend sa part (- ; ). La classe de base est la classe dont on hérite. La classe qui hérite est la classe dérivée. Image 14 Symbolisme UML de l'héritage En cas d'héritage :  Toutes les méthodes et attributs publiques (public :) de la classe de base sont publiques dans la classe dérivée. On y accède comme s'ils étaient déclarés dans la classe dérivée.  Toutes les méthodes et attributs privés (private:) de la classe de base sont inaccessibles dans la classe dérivée. Le seul moyen est d'utiliser les accesseurs s'ils existent.  Toutes les méthodes et attributs protégés (protected:) de la classe de base sont accessibles dans la classe dérivée comme des membres privés.

Création d'un thread par héritage Une solution consiste à créer une classe qui hérite de QThread et à surcharger la méthode run(), c'est à dire redéfinir son contenu car par défaut, elle ne fait rien.

1 class WorkerThread : public QThread 2 { 3 Q_OBJECT 4 void run() override { 5 QString result; 6 /* ... here is the expensive or blocking operation ... */ 7 emit resultReady(result); 8 } 9 signals: 10 void resultReady(const QString &s); 11 }; 12 13 void MyObject::startWorkInAThread() 14 { 15 WorkerThread *workerThread = new WorkerThread(this); 16 connect(workerThread, &WorkerThread::resultReady, this, &MyObject::handleResults); 17 connect(workerThread, &WorkerThread::finished, workerThread, &QObject::deleteLater); 18 workerThread->start(); 19 }

Ici, la méthode run() est surchargée pour personnaliser l'exécution du thread. Notez qu'un thread peut avoir besoin de signifier qu'il a terminé son travail. Le code ci-dessus permet l'émission d'un signal resultReady(result). L'IHM peut ainsi récupérer le résultat.

Remarque : Quel est le thread ? Seule la méthode run() fera partie du thread nouvellement créé. L'instanciation d'un objet WorkerThread se faisant dans le thread principale, il appartient donc à ce thread et dispose de la même boucle de messages.

91 Formation pas à pas !

De manière pratique avec qt-creator Pour créer une nouvelle classe qui hérite de QThread, utilisez l'assistant de création de classe :  Cliquez bouton de droite sur le nom du projet dans le gestionnaire de projet.  Ajoutez une nouvelle classe C++ et cliquez validez la fenêtre.

Ajout d'une nouvelle classe

Remplissez la fenêtre suivante selon le modèle ci-dessous :

Paramètres de la nouvelle classe

Après validation, votre classe est visible dans le gestionnaire de projet. Pour l'utiliser, n'oubliez pas de placer la ligne suivante dans le fichier d'interface de classe qui l'utilisera :

1 #include "cboutonpoussoir.h"

Création d'un objet géré par la classe QThread Autre méthode : Imaginons la création d'une classe CWork (par exemple) dont un objet sera exécuté en tant que thread.

92 Formation pas à pas ! Pour cela, il faut que la classe CWork hérite de QObject, la classe de base de tous les objets Qt. Cela permet d'hériter de la méthode moveToThread(). Cette méthode permet de sous traiter son exécution à un objet QThread. Illustration de l'utilisation de la méthode moveToThread() :

1 QThread workerThread; 2 CWork *worker = new CWork; 3 worker->moveToThread(&workerThread); 4 workerThread.start();

Dans cet exemple, le lancement du thread exécutera l'objet worker. Voici un exemple plus complet :

1 class CWork : public QObject 2 { 3 Q_OBJECT 4 5 public slots: 6 void doWork(const QString ¶meter) { 7 QString result; 8 /* ... here is the expensive or blocking operation ... */ 9 emit resultReady(result); 10 } 11 12 signals: 13 void resultReady(const QString &result); 14 }; 15 16 class Controller : public QObject 17 { 18 Q_OBJECT 19 QThread workerThread; 20 public: 21 Controller() { 22 CWork *worker = new CWork; 23 worker->moveToThread(&workerThread); 24 connect(&workerThread, SIGNAL(&QThread::finished()), worker, SLOT(&QObject::deleteLater())); 25 connect(this, SIGNAL(&Controller::operate()), worker, SLOT(&Worker::doWork())); 26 connect(worker, SIGNAL(&Worker::resultReady()), this, SLOT(&Controller::handleResults())); 27 workerThread.start(); 28 } 29 ~Controller() { 30 workerThread.quit(); 31 workerThread.wait(); 32 } 33 public slots: 34 void handleResults(const QString &); 35 signals: 36 void operate(const QString &); 37 };

93 Formation pas à pas !

Attention : Instance de thread It is important to remember that a QThread instance lives in the old thread that instantiated it, not in the new thread that calls run(). This means that all of QThread's queued slots will execute in the old thread. Thus, a developer who wishes to invoke slots in the new thread must use the worker-object approach; new slots should not be implemented directly into a subclassed QThread. When subclassing QThread, keep in mind that the constructor executes in the old thread while run() executes in the new thread. If a member variable is accessed from both functions, then the variable is accessed from two different threads. Check that it is safe to do so.

94 Formation pas à pas !

Complément : Signaux/slots dans un thread

La classe QThread représente une classe de gestion d'un thread, pas le thread lui-même. Cela signifie que l'instance de cet objet appartient au thread "père", la plupart du temps, le thread principal. De ce fait tous les signaux échangés avec cette instance seront placés dans la file des messages du thread Image 15 Diagramme de classe d'exemple principal et non du nouveau thread créé. Cela peut poser un problème, notamment si dans un slot, vous attendez un signal ou qu'une donnée membre se mette à jour. Cette donnée sera mise à jour ou le signal traité qu'après sortie du slot. Examinez le diagramme de classes ci-contre : Dans la classe CIHM... on instancie l'objet CAppareil (m_app). Toujours dans la classe CIHM... on lance le thread : m_app->start() ; Seule la méthode CAppareil::run() est exécutée dans un thread différent. Les autres méthodes de la classe sont dans le thread initial. Imaginons que dans la méthode CAppareil::run(), on boucle pour attendre d'avoir reçu un certain nombre de caractères par la voie série avant de lire l'ensemble. Voilà illustré le problème : étant dans la méthode run() qui boucle, l'objet m_app appartient au thread principal. Il faudra attendre la sortie de la méthode run() pour que la mise à jour des attributs de classe soit effective. Pour des explications plus détaillées, voici le lien :

digia-thread.pdf Document 1

Oui je sais, c'est pas évident. C'est pas non plus essentiel pour la suite. Il faut pratiquer les threads et rencontrer les problèmes pour commencer à comprendre.

16. Traitements parallèles avec les threads

Question [Solution n°9 p 237] Je vous propose de créer une application permettant de lire la valeur du bouton poussoir connecté à la GPIO22 de la carte fille d'étude. Un thread effectuera la lecture permanente (mode pooling) de la valeur du bouton. En cas de changement d'état, un signal sera émis en direction de l'IHM. L'IHM affichera l'état du bouton. Indice :

Créer une classe CBoutonPoussoir qui hérite de QThread.

95 Formation pas à pas ! Surcharger la méthode run() pour lire en continu (boucle) la valeur du bouton poussoir sur la GPIO. Vous aurez besoin de la classe CGpio étudiée précédemment. La classe CBoutonPoussoir doit prévoir 1 signal véhiculant la valeur du bouton poussoir (1 ou 0). La classe CIhmAppThread doit disposer d'un slot pour recevoir le signal.

17. Utilisation d'un segment de mémoire partagé

Introduction Dans Linux, chaque processus se voit attribué un espace mémoire virtuel de 4Go. Bien sur, c'est virtuel, seuls les segments mémoires réellement utilisés sont reliés (mappés) par le système à la mémoire réelle. Le système gère totalement la mémoire réelle, nous n'y avons pas accès. Nous n'avons accès qu'à la mémoire virtuelle de chaque processus. Puisque chaque processus est confiné dans un segment mémoire, le système met à disposition une ressource permettant de mettre en commun une zone mémoire : le segment de mémoire partagé. Qt met à disposition la classe QSharedMemory.

Attention : Ressource commune En programmation, utiliser une ressource commune à plusieurs processus est toujours délicat car il faut contrôler l'accès à la ressource de manière protéger les données. Comme vous le savez maintenant, un processus peut à tout moment être interrompu par l'ordonnanceur du système pour que le processeur exécute le code d'un autre processus. Imaginez qu'une procédure d'écriture soit en cours sur la mémoire partagée, mais non terminée. Imaginez aussi que le processus suivant écrive dans ce segment de mémoire partagé. Les données pourraient être corrompues, invalides ou incohérentes. C'est la raison pour laquelle il faut absolument protéger le segment de mémoire partagé dès lors que plusieurs processus lecteur/écrivain y accèdent.

La classe QSharedMemory L'objet s'utilise de la manière suivante :  Instanciation de l'objet QSharedMemory.  Création (la première fois) du segment de mémoire partagé (create()).  Attachement du segment au processus en cours (attach()).  Utilisation protégée de la mémoire (pointeur).  Détachement du segment du processus en cours (detach()).  Destruction de l'objet. En plus de gérer un segment de mémoire partagé, la classe encapsule un objet sémaphore de protection accessible par les méthodes lock() et unlock().

96 Formation pas à pas !

Rappel : Le sémaphore Un sémaphore est une ressource mise à disposition par le système Linux pour permettre entre autre l'unicité d'accès à une ressource. Chaque processus utilisant cette ressource doit utiliser le même sémaphore afin de s'assurer de l'unicité d'accès à la ressource. En interne, un sémaphore est constitué d'un compteur et d'une file d'attente de processus. Lorsqu'un processus "prends le jeton" (lock()) du sémaphore, le compteur décroît. Lorsque le compteur atteint 0, les processus suivant qui "prendront le jeton" se verront placés en file d'attente jusqu'à ce que le compteur ait une valeur positive. Lorsqu'un processus "libère le jeton"(unlock()), le compteur est incrémenté, les processus en attente sont réveillés.

Exemple : Création d'un segment de mémoire partagé Il faut commencer par créer une structure de données contenant les informations qu'on souhaite partager entre les processus ou threads. Ci-dessous un exemple issu d'un projet. Cette structure se déclare dans un fichier d'interface (.h).

1 // exemple 2 typedef struct { 3 int noMes; // numéro de la mesure 4 int adrCapteur; // Adresse ou fichier d'accès au capteur en hexa sans particule 5 int posL; // N° ligne affichage incrustation 6 int posC; // N° colonne affichage incrustation 7 char nomClasse[AFFMAX]; // nom de la classe de gestion du capteur 8 char nomMes[AFFMAX]; // nom de la mesure 9 char symbUnit[AFFMAX]; // Symbole de l'unité de la mesure 10 char textUnit[AFFMAX]; // Texte de l'unité 11 char valMes[AFFMAX]; // Valeur instantannée de la mesure 12 } T_Mes;

Il faut ensuite créer l'objet QSharedMemory et le segment de mémoire partagé :

1 #define KEY "./ditRasp" 2 3 QSharedMemory *mShm; // segment de mémoire partagé 4 T_Mes *mData; // pointeur du segment de mémoire partagé 5 6 mShm = new QSharedMemory(KEY, this); 7 mShm->attach(); // tentative de s'attacher 8 if (!mShm->isAttached()) { // si existe pas alors création 9 res = mShm->create(9*sizeof(T_Mes)); // on réserve pour 9 capteurs max 10 if (res == false) 11 qDebug(mShm->errorString().toStdString().c_str()); 12 } // if isattached 13 14 mShm->attach(); // tentative de s'attacher 15 mData = (T_Mes *)mShm->data(); // obtient le pointeur sur la mémoire 16 // utilisation de la mémoire partagée 17 mShm->detach(); 18

 La constante symbolique KEY est une chaîne de caractères représentant le chemin et nom d'un fichier existant. Le système se sert de ce fichier existant pour calculer une clef unique désignant le segment de mémoire partagé parmi d'autres.

97 Formation pas à pas !  La mémoire partagée n'est pas forcément créée à l'instanciation de l'objet. Si l'attachement au processus n'est pas possible (cas du segment non créé) il faut donc créer le segment par la méthode create().  Dans cet exemple, le segment est créé d'une dimension pouvant contenir 9 structures de données de type T_Mes.  La méthode data() permet de récupérer l'adresse de base (dans l'espace virtuel du processus) du segment de mémoire partagé. Avec ce pointeur, il est possible de lire/écrire dans la mémoire.  Ne pas oublier de détacher le segment une fois son utilisation terminée. La destruction du segment est automatique dès que le dernier processus détache le segment.

18. Utilisation d'un segment de mémoire partagé entre processus

Question [Solution n°10 p 239] En vous servant du programme appGpio, appThread et appSpi, modifier les classes capteurs afin d'écrire les valeurs des capteurs dans un segment de mémoire partagé commun aux threads Ihm, capteurs. L'objectif est de disposer d'une classe CIhmAppShm permettant l'affichage d'une ou plusieurs mesures à intervalle régulier. Chaque capteur (I2C, SPI) est représenté par un objet thread lisant à intervalle régulier la valeur du capteur et la sauvant en mémoire partagée. Indice :

Il est avantageux de créer une classe de gestion du segment de mémoire partagé.

* * *

Vous disposez à présent de tous les éléments pour produire l'application correspondant au cahier des charges donnés en début de formation. L'activité suivante vous permettra de prendre connaissance de l'analyse système formalisée en SysML, avant de vous lancer dans le code.

98

VII - L'application ! VII

A. Développement de l'application

L'analyse d'un système est une vue abstraite de celui-ci. Le modèle devient plus précis une fois terminé et affiné par une phase de reverse engineering (qui consiste à s'assurer que les diagrammes sont en cohérence avec la programmation et le matériel). Plusieurs méthodes d'analyse existent utilisant le langage UML/SysML pour formaliser la pensée et communiquer entre les membres de l'équipe de projet. Celle choisie consiste à décrire le système selon 4 axes :  Axe des exigences.  Axe fonctionnel/structurel.  Axe dynamique.  Axe transverse.

1. L'axe des exigences

99 L'application !

Rappel : Cahier des charges N'hésitez pas à relire le cahier des charges pour vous imprégner du fonctionnement. Toute l'analyse en découle.

100 L'application !

Définition : Description de l'axe Cet axe permet de découvrir le système, avec ses contraintes. Les diagrammes utilisés sont :  Diagramme de déploiement (DEP). Présente les différents éléments matériels/Logiciels composant le système et leurs liaisons, moyens de communication.  Diagramme des cas d'utilisation (UCD). Rend compte des usages vues des acteurs principaux ou secondaires. Tout n'est pas forcément décrit.  Diagramme des exigences (REQ). Permet de mettre en évidence les exigences connues dans tous les domaines.

Déploiement du système

Diagramme de déploiement

Grâce à ce diagramme qui pourrait remplacer un synoptique, nous constatons un grand nombre de moyens de communication. Nous prenons conscience des liaisons entre les matériels. Nous réalisons la présence d'une carte d'interface et ce qui y est raccordé. On peut se servir de ce diagramme pour répartir le travail entre les membres d'une équipe, après avoir estimé l'ensemble des tâches à réaliser.

101 L'application !

Les cas d'utilisation (UC)

Diagramme des cas d'utilisation

Les cas d'utilisation constituent une première approche des besoins (ou exigences) du système en matière de logiciel et matériel. Ils permettent de synthétiser les interactions entre le système et les acteurs. Les cas d'utilisation sont exprimés le plus souvent par des verbes d'action. L'acteur principal est l'exploitant (le plus souvent situé à gauche). Les autres acteurs sont secondaires même si leur présence est indispensable. En lisant le diagramme, on apprend que l'exploitant peut interagir avec le système pour :  Mesurer les températures et humidité.  Commander la LED  Interagir (communiquer) avec différents acteurs (client, périphérique RS232C, Bouton Poussoir). Pour ce dernier, un simple message affichera l'état du bouton poussoir sur l'IHM. Notez également que :  Le UC Mesurer se spécialise en 2 UC Mesurer I2C et Mesurer SPI.  Le UC Commander est particulier car il interagit avec l'exploitant mais vient aussi étendre le UC Mesurer en cas de dépassement d'un seuil d'une des mesures. L'IHM est un besoin implicite. Cela peut correspondre aux liaisons entre l'exploitant et les cas d'utilisation. Les données manipulées par le système ne sont pas représentées sur le diagramme (implicite également), ce n'est pas le but et cela alourdirait le diagramme. Il s'agit d'une première approche des besoins (ou exigences) du système en matière de logiciel et matériel. On peut également se servir de ce diagramme pour répartir le travail au sein d'une équipe.

102 L'application !

Complément : Relation entre les UCs <> Indique que l'UC pointé est inclus dans le UC source. Son utilisation sera obligatoire dans l'UC le pointant. Cela permet de factoriser certains besoins, ou de faire apparaître un besoin important sous-jacent. <> Indique qu'à un certain moment (qui dépend d'une condition), le UC vient compléter le UC pointé.

Scenarii La lecture d'un UCD ne permet pas de comprendre tous les besoins du système mais c'est un bon point de départ pour l'analyser. Chaque UC est décrit par un ensemble de scenarii :  Scénario nominal qui décrit le fonctionnement le plus fréquent.  Scénario alternatif (si nécessaire) qui décrite une variante intéressante du scénario nominal.  Scénario d'erreur (si nécessaire) qui décrit comment réagit le système en cas d'erreur. Les scenarii seront ensuite décrits sous forme SysML par des diagrammes.

Scénario nominal de communiquer avec Bouton Poussoir

Lire l'état du bouton poussoir. Afficher l'état du bouton poussoir.

Scénario nominal de communiquer avec Périphérique RS232C

Initialiser la liaison série. Pré-Condition : Information arrivée. Post-Condition : Afficher le message. Pré-Condition : Information entrée. Post-Condition : ÉCRIRE le message.

Scénario nominal de communiquer avec client TCP/IP

Pré-Condition : client connecté au serveur. Post-Condition : ÉCRIRE message de bienvenue RÉPÉTER ÉCRIRE "Entrez le numéro de la mesure à lire : " LIRE numéro de la mesure ÉCRIRE valeur de la mesure TANT QUE client connecté Tableau 14 Scenarii de l'UC Communiquer

Scénario nominal de Mesurer I2C

BOUCLE LIRE la mesure de température Mémoriser (UC) la mesure dans le segment de mémoire partagé LIRE l'humidité Mémoriser (UC) la mesure dans le segment de mémoire partagé

103 L'application !

FIN BOUCLE

Scénario nominal de Mesurer SPI

BOUCLE LIRE la mesure de température Mémoriser (UC) la mesure dans le segment de mémoire partagé FIN BOUCLE

Scénario alternatif de Mesurer

Pré-Condition : Dépassement du seuil d'une des mesures Post-Condition : Forcer l'allumage de la LED Interdire la commande manuelle de la LED Pré-Condition : Toutes les mesures en dessous du seuil Post-Condition : Forcer l'extinction de la LED Autoriser la commande manuelle de la LED Tableau 15 Scenarii de l'UC Mesurer

Scénario nominal de transférer vers le serveur

Toutes les N secondes, émettre la valeur des mesures (protocole à préciser).

Scénario d'erreur de transférer vers le serveur

Pré-Condition : Impossible de se connecter au serveur. Post-Condition : Affichage d'un message d'erreur.

Scénario nominal de transférer vers l'écran LCD (I2C)

Toutes les N secondes, écrire la valeur des mesures (protocole à préciser).

Scénario nominal de transférer vers le SGBD

Connexion au SGBD. Requête d'écriture des mesures dans la base de données.

Scénario d'erreur de transférer vers le SGBD

Pré-Condition : Impossible de se connecter au SGBD. Post-Condition : Affichage d'un message d'erreur. Tableau 16 Scenarii de l'UC Transférer

Scénario nominal de Commander la LED

Pré-Condition : Commande manuelle autorisée ET ordre allumage Post-Condition : Allumage de la LED Pré-Condition : Commande manuelle autorisée ET ordre extinction Post-Condition : Extinction de la LED Tableau 17 Scénario de l'UC Commander

104 L'application !

Remarque : Initialisation Notez que les scenarii ne décrivent pas l'initialisation des différentes parties du système, à moins que cela soit une phase obligatoire ou délicate. Sinon, c'est également implicite.

Les exigences

Diagramme des exigences

Les exigences d'un système peuvent être nombreuses et précises dès le départ. On peut les classer dans différentes catégories :  Matérielle  Logicielle  Forme  Financière  etc. Lorsqu'il y en a beaucoup, il est intéressant de faire un diagramme par catégorie. Notre diagramme ci-dessus est simple et sûrement incomplet. En effet, au cours du développement peuvent apparaître d'autres exigences. Pour ne pas noyer le lecteur, seules les exigences importantes ayant un impact sur la réalisation seront indiquées.

105 L'application !

Diagramme de séquence système

Diagramme de séquence système (ssd)

Ce diagramme décrit le fonctionnement dynamique nominal (le plus courant ou plus intéressant) du système et donne une vue d'ensemble du fonctionnement du système. Il est utile afin de cerner rapidement le fonctionnement attendu, même si ce diagramme ne représente pas exhaustivement le fonctionnement du système. Les scenarii détaillés, seront décrits dans le reste de l'analyse.

2. Axe dynamique

106 L'application !

Définition : Description de l'axe Cet axe permet de décrire le fonctionnement de la partie logicielle pendant son fonctionnement. En fonction de la problématique, les diagrammes suivants sont utilisés :  Diagramme de séquence (sd). Il montre les appels entre les objets, les acteurs en suivant un scénario. Il peut être plus ou moins précis mais doit rester cohérent.  Diagramme d'états. Dans un système, ce diagramme montre clairement les états de fonctionnement, les conditions et événements ayant une incidence pour en changer.  Diagramme d'activités (act). Il montre l'activité intéressante d'une partie de l'application. Il met en évidence les parallélismes logiciel, les synchronisations entre processus ou threads.

107 L'application !

Diagramme états transitions du système général

Diagramme états transitions (stm)

Notre système comporte 2 états. Quel que soit l'état dans lequel il se trouve, il est possible d'arrêter le système. La transition start permet de lancer les tâches parallèles (threads) des capteurs. La transition stop permet de stopper les threads. L'état Mesurer effectue un certain nombre d'actions détaillées dans le diagramme d'activités associé suivant.

108 L'application !

Diagramme d'activités de l'état système Mesurer

Diagramme d'activités de l'état de fonctionnement Mesurer (act)

Ce diagramme met en évidence les besoins informatiques :  Le parallélisme des activités de lecture des capteurs et de gestion des périphériques de communication.  Le segment de mémoire partagé est central. Certaines activités ont besoin d'y écrire tandis que d'autres y lire. Comme les activités s'exécutent en parallèle, il sera nécessaire de protéger le segment de mémoire partagé des accès concurrents. La mise en place d'un mutex solutionnera le problème.  La nécessité de disposer de trois paramètres différents pour chaque intervalle de temps.

109 L'application !

Rappel : Mutex Un mutex (mutual exclusion) est un objet dépendant du système d'exploitation et réglementant l'accès à une ressource. Un processus qui utilise la ressource empêche les autres processus d'y accéder. Ils se mettent en veille jusqu'à libération de la ressource.

110 L'application !

Complément : Mutex et Qt Il existe la classe Qt QSharedMemory dont le rôle est de gérer un segment de mémoire partagé. Il se trouve que cette classe inclue un objet QMutex permettant précisément de protéger les accès. Pas mal non ?

Remarque : Cycle de développement Le développement d'une application est le plus souvent itératif et incrémental. Chaque itération a pour objectif de construire une partie du système.

Développement itératif et incrémental

Chaque itération est développée en suivant un cycle de développement en V.

111 L'application !

Cycle en V de développement d'une itération

Au cours d'une itération, les exigences sont étudiées, l'analyse est faite, formalisée par le langage SysML associé à une documentation, le codage effectué ainsi que les tests de validation, intégration et la recette finale. La fonctionnalité peut alors être déployée. Ce cycle recommence pour chaque itération afin de compléter toutes les fonctionnalités du système. Le dossier actuel ne rend pas compte de cette manière de développer car il ne s'agit que d'une synthèse à usage pédagogique orienté sur le développement informatique et non sur le génie logiciel.

112 L'application ! 3. Axe fonctionnel / structurel

Définition : Description de l'axe Cet axe permet de détailler les parties matérielles par les :  Diagrammes de blocs (bdd).  Diagrammes de blocs internes (ibd).  Diagrammes de paquetage (pkg). Pour décrire la partie logicielle, on utilisera le diagramme :  De classes du langage UML (cd). Décrivons la partie matériel :

Le diagramme de blocs (bdd)

Diagramme de blocs général

Ce diagramme montre de quoi est composé le système : les matériels représentés sous forme de blocs, leurs ports de communication, les parties de chaque bloc intéressant à détailler. Le diagramme de blocs internes ci-dessous met en évidence les liaisons internes entre blocs. Le bloc carte d'interface est intéressant à détailler car c'est lui qui fait l'objet de la réalisation électronique. On fera donc ensuite un diagramme de blocs internes pour ce bloc.

113 L'application !

Diagramme de blocs internes général (ibd)

Ce diagramme est assez proche du diagramme de déploiement d'UML montré précédemment.

114 L'application !

Le diagramme de blocs internes (ibd) de la carte d'interface

Diagramme de blocs internes du bloc carte d'interface (ibd)

On s'aperçoit que tous les moyens de communication proviennent de l'interface GPIO.

Conception de la carte d'interface Nous avons à présent tous les éléments pour établir le schéma structurel de la carte d'interface. Il ne faudra pas oublier de tenir compte des contraintes de dimension pour réaliser le routage de cette carte. En effet, la carte Raspberry peut disposer de radiateurs qui sont assez hauts, et d'un boitier qui limitent les dimensions de la carte d'interface.

115 L'application !

PCB de la carte d'interface

Choix des capteurs Nous avons choisi les capteurs en fonction de leur prix ainsi que du bus de communication avec lequel ils fonctionnaient pour disposer d'une diversité de moyens de communication. Le critère précision n'a pas été retenu car il s'agit simplement d'un système à usage pédagogique dédié à la programmation.

Diagramme de classe (cd) Ce diagramme est essentiel pour structurer le programme informatique. Il appartient au langage UML. Cependant, il peut être remplacé par un diagramme de blocs (bdd) dans lequel chaque bloc représente une classe.

Remarque : Structurer un programme informatique, quel défis ! Définir une organisation informatique sous forme de classes n'est pas facile, même lorsqu'on a de l'expérience. Voici une méthode simple car elle est proche de notre mode de pensée naturelle : 1. Lister les objets dont on aura besoin. On distingue les objets généraux (qui peuvent s'utiliser dans d'autres contextes) et les objets métiers (qui sont spécifiques au système). Un objet peut correspondre à un matériel, bus de communication, gestion mémoire, etc). 2. Définir les caractéristiques et comportements principaux de chaque objet (attributs et méthodes). 3. Définir les classes décrivant chaque objet. 4. Définir les relations entre les classes (association, composition, agrégation, héritage). 5. Enfin, caractérisez chaque relation en indiquant les directions, cardinalité, rôle. La structure informatique de l'application est née. Il ne reste plus qu'à coder. Vous vous apercevrez peut-être qu'il faudra affiner le modèle car :  Vous avez oublié un objet donc la classe le représentant.  Une classe se révèle inutile finalement.  Les relations finales entre les classes ont changé car vous avez acquis une idée plus précise en fin de développement. Bon ! maintenant, il faut se lancer !

116 L'application !

Définition des classes Si nous résumons, notre application :  Communique avec des périphériques (capteurs, écran, LED, bouton, client, serveur, SGBD), chacun ayant sa manière de communiquer (RS232C, I2C, SPI, GPIO).  Les données recueillies sont affichées à intervalle régulier sur l'IHM de l'application.  Les objets périphériques "capteurs" fonctionnent en tant que thread. Ainsi, chaque objet "capteur" s'exécute indépendamment.  Les mesures acquises sont stockées dans une mémoire partagée (entre les objets de l'application). Ainsi l'objet IHM pourra à sa fréquence propre lire les valeurs et les afficher. Voilà qui nous permet d'imaginer une organisation informatique sous forme de classes :  Notre classe de gestion de l'objet IHM (CIhmAppFormQtCpp). Elle se contente de gérer l'IHM et les interactions avec les autres objets (création, destruction, communication).  La classe de gestion du segment de mémoire partagé (CSharedMemory).  Nos classes périphériques : CClientTcp, CServeurTcp, CBdd, CLed, CBoutonPoussoir, CPeriphRs232, CCapteur_Spi_TC72, CCapteur_I2C_SHT20, CAff_i2c_GroveLcdRgb.  Nos classes de gestion des communications : CGpio, CRs232c, CSpi, CI2c. Le choix a été fait de considérer que la classe IHM (CIhmAppFormQtCpp) est composée des classes périphériques. Pour rappel, cela signifie que la classe est responsable de la création des objets (instanciation) mais aussi de leur destruction. Cela justifie l'utilisation de relation de composition (losange noir). On admettra également que les périphériques sont composés d'un moyen de communication. Chaque périphérique est responsable de la création du sous objet de communication et de sa destruction, SAUF pour les objets communiquant par le bus I2C. Cela justifie l'utilisation de relation de composition (losange noir). La classe I2C étant une classe singleton, seul UN objet sera responsable de la création de l'unique objet CI2c (le premier) et seul UN objet (ce ne sera pas forcément le même) procédera à sa destruction (le dernier utilisateur du sous objet CI2c). Le choix de relation entre les classes "capteurs I2C" et la classe CI2c est une agrégation (losange blanc). Fort de cette analyse succincte, notre diagramme de classe prend donc la forme suivante :

117 L'application !

Diagramme de classe (CD) de l'application

Ce diagramme apparaît comme par miracle ! Il est effectivement le résultat de l'expérience du développeur et bien sûr n'est pas forcément la seule solution. De plus, il a été affiné au fur et à mesure de la réalisation.

4. Axe transverse

Définition : Description de l'axe Cet axe permet de décrire le comportement physique d'un bloc. Il permet de matérialiser la formule ou traitement mathématique à coder informatiquement ou à réaliser électroniquement. Diagramme utilisé :  Diagramme paramétrique. Pour chaque bloc concerné, il établit la formule

118 L'application ! ou traitement mathématique.

* * *

Et voici maintenant le moment tant attendu, la livraison des programmes sources complets de l'application. Avant de compiler l'ensemble sous qt- creator, assurez-vous d'avoir placé le dossier biblis et appFormQtCpp dans le dossier /home/pi/devQt sans quoi des erreurs seront présentes. Servez-vous de l'image ci-contre pour comparer avec votre installation.

Image 16 Projet Qt de l'application finale Ci-dessous le lien de téléchargement des sources :

appFormQtCpp.zip Document 2 Sources de l'application

biblis.zip Document 3 Bibliothèques des bus de communication.

Vous pouvez bien sûr vous dispenser de la formation pas à pas précédente pour vous servir directement des sources dans vos futurs projets. Pour ceux intéressés par l'aspect analyse pour d'éventuels projets, la partie suivante vous présentera l'analyse SysML. Pour ceux intéressés par des activités orientées communications électroniques, la partie finale vous rendra service.

119

VIII - Ressources VIII annexes

A. Ressources logicielles pour la formation

1. Cahier des charges de l'application finale

Cette application est développée en C++/Qt sur le système linux embarqué sur une carte Raspberry. L'application est dotée d'une interface graphique Qt (GUI en anglais ou IHM en français) sur la liaison HDMI et privilégiera l'utilisation des mécanismes de communication Qt (signaux/slots). Les fonctionnalités attendues de l'application finale sont les suivantes :  Mesurer la température et l'humidité dans la pièce (Bus I2C, composant SHT20).  Mesurer la température dans la pièce (Bus SPI, composant TC72).  Allumer une LED "défaut" en cas de dépassement des valeurs seuils (réglables sur l'IHM) des 2 capteurs (GPIO). Cette LED pourra être commandée manuellement s'il n'y a pas de dépassement.  Informer l'utilisateur en cas de dépassement des valeurs des mesures (IHM+écran LCD).  Afficher les mesures sur l'IHM, l'écran LCD et le terminal (période réglable sur l'IHM) (Bus I2C, liaison série, paramètres série réglables sur l'IHM).  Diffuser grâce au réseau (client TCP) les mesures vers un serveur TCP (local ou distant) qui les affichera (paramètres réglables sur l'IHM).  Mettre à disposition les mesures par un serveur TCP intégré (port réglable sur l'IHM).  Sauver les mesures dans une base de données locale ou distante (paramètres réglables sur l'IHM).

121 Ressources annexes

Diagramme de déploiement (dep) du système

122 Ressources annexes

IHM de l'application finale

2. Sources du client TCP distant

appClientTcp.zip Document 4 Sources du client TCP distant

Décompactez ce fichier dans votre dossier personnel. Ouvrez le fichier .pro avec qt-creator.

3. Source du serveur TCP distant

appServeurTcp.zip Document 5 Sources du serveur TCP distant.

Décompactez ce fichier dans votre dossier personnel. Ouvrez le fichier .pro avec qt-creator.

B. Description d'un objet

On dit souvent qu'on arrive à bien parler une langue lorsqu'on pense dans la

123 Ressources annexes langue. Remarquez qu'avoir la compétence de penser dans une langue n'est pas une garantie que vous savez écrire la langue. Pour la programmation objet, il en va de même. Une chance, tous les jours, nous pensons "objet". C'est juste que nous ne savons pas encore écrire la langue objet. D'ailleurs, il en existe plusieurs : C++, C#, VBA, , PHP, etc. Nous pensons objet car tout notre univers est composé d'objets divers et variés. Nous les classons, les associons, les groupons. Certains objets sont communicants, d'autres peuvent changer de comportement, d'état.

1. Entre la pensée et l'objet : la classe

Exemple : L'ADN et le moule Osons la comparaison : la classe est à l'objet ce que l'ADN est à l'humain. La différence, c'est qu'on ne connais pas bien l'ADN, par contre on connaîtra parfaitement la classe puisque c'est nous qui la définissons. On peut parler aussi de moule. Un moule permet de faire plusieurs objets.

Ce qu'est une classe Une classe peut donner naissance à plusieurs objets. Mais que contient une classe ? Réponse : tout ce qui permettra d'initialiser un objet, ses caractéristiques, ses fonctionnalités (autrement appelées ses comportements). Un objet a des caractéristiques, appelées attributs. Un objet peut avoir également des comportements, précisés par des fonctions appelées méthodes ou opérations.

Exemple : La télévision Si on devait devait décrire un objet télévision, on pourrait définir ses caractéristiques principales telles que :  Sa marque  Sa technologie (LED, PLASMA, etc.)  Sa résolution (HDR, HD, UHD)  Sa diagonale d'écran et pour l'identifier de manière unique :  Son numéro de série On pourrait continuer sa description, définir des comportements de base attendus de toute télévision :  S'allumer  S'éteindre  Changer de chaîne (+, -, N°)  Changer le volume (+, -, val) Notez que les comportements sont identifiés par des verbes à l'infinitif exprimant une action. Certains comportements nécessitent des paramètres pour accomplir leur but. Voilà la classe permettant de représenter une télévision :

124 Ressources annexes

1 class CTelevision { 2 private : 3 string marque; 4 string techno; 5 string res; 6 int diagonale; 7 string noSerie ; 8 public : 9 void allumer(); 10 void eteindre(); 11 void changerChaine(int num); 12 void changerVolume(int val); 13 }; // class CTelevision

Représentation UML d'une classe

Vous distinguez dans cette classe CTelevision 2 parties :  La partie private définit tout ce qui sera interne à l'objet, non accessible par les autres objets interagissants. Seuls les méthodes internes à l'objet pourront accéder aux éléments private. En représentation UML, les données private sont représentées par le signe -.  La partie public définit tout ce qui sera accessible par les autres objets interagissants. En notation UML, les données public sont représentées par le signe +.

Dans cet exemple, notez que la classe contient des attributs et des méthodes ou opérations. Vous constatez que seules les opérations sont publiques. Cela signifie qu'un utilisateur ne peut interagir avec un objet de type CTelevision qu'avec ce qui est défini comme interface publique.

125 Ressources annexes

2. Création d'un objet : l'instanciation

Instanciation statique d'un objet Pour créer un objet télévision :

1 CTelevision television ; Cette action de création d'un objet se nomme l'instanciation. Pour allumer la télévision :

1 television.allumer();

Instanciation en mémoire dynamique Comme vous l'avez appris, lorsqu'un processus est en fonctionnement, il utilise divers portions mémoires appelées segments. Chaque segment est limité selon les options du système et de l'environnement de développement (EDI). Le segment de tas (heap segment) est disponible pour réserver des espaces mémoires dynamiquement (à la demande). Ces espaces mémoires peuvent contenir des variables ou des objets, c'est ce qui va nous intéresser. Pour instancier dynamiquement un objet :

1 CTelevision *maTele ; // déclaration d'un pointeur non initialisé 2 maTele = new CTelevision ; // instanciation de l'objet

maTele est un objet de type CTelevision. Grâce à l'opérateur C++ new, le système a attribué un espace mémoire dans le segment de tas pour y stocker l'objet. Cette réservation restera active tant que le processus ne libérera pas cet espace.

126 Ressources annexes

Attention : Objet dynamique L'opérateur d'accès à un attribut ou une méthode n'est plus le point (.) mais l'opérateur flèche ->.

1 maTele->allumer() ;

Il est aussi possible de créer des tableaux d'objets en mémoire dynamique :

1 mesTeles = new CTelevision[20] ; L'accès à une méthode peut se faire de la manière suivante pour allumer la 3ème télévision :

1 mesTeles[2].allumer() ;

Remarque Vous notez que l'opérateur point à été utilisé car bien que ce soit un pointeur, le formalisme tableau a été utilisé.

3. Accesseurs d'un objet

Rappel Il est impossible de lire ou d'écrire dans un membre de la classe définit comme private. Le seul moyen est de créer des méthodes appelées accesseurs en lecture ou écriture. Le plus souvent, ces méthodes commencent par les mots anglais get pour obtenir ou set pour modifier.

Accesseurs Les attributs définis dans l'objet television n'auront pas lieu de changer. Le numéro de série est fixe tout comme la diagonale écran. Imaginons que nous voulions conserver la valeur de la chaîne en cours et le niveau de réglage du volume. Créons pour cela 2 attributs supplémentaires private : chaine et volume. Puisqu'ils sont private, seuls les méthodes membres peuvent accéder à chaine et volume. Créons donc un accesseur en lecture que nous nommerons : getChaine() et getVolume(). La classe devient :

127 Ressources annexes

Accesseurs en lecture

Un accesseur ne s'occupe que de permettre l'accès en lecture ou écriture à un attribut privé. Pour obtenir la chaîne en cours :

1 int noch ; 2 noch = television.getChaine(); Nous venons de créer et utiliser une méthode accesseur en lecture d'un attribut private. Le seul moyen pour changer le volume est d'utiliser la méthode changerChaine() de l'objet.

Simulateur : Pourquoi ne pas déclarer l'attribut chaine comme public ? Pour permettre à la méthode accesseur de contrôler la valeur que prendra l'attribut. En donnant l'accès publique, un utilisateur pourrait fixer une valeur incohérente à l'attribut chaine, ce qui engendrerait un mauvais fonctionnement de l'objet.

Fondamental : Codage d'un accesseur en écriture Veillez à contrôler la valeur d'un attribut afin qu'elle soit conforme à ce que permet l'objet. Par exemple, une valeur 65003 pour une chaîne est incohérente. Dans ce cas, l'accesseur retournera une valeur d'erreur signifiant à l'utilisateur que la valeur est erronée.

128 Ressources annexes 4. Destruction d'objets

Destruction d'un objet Il faut toujours penser à détruire un objet créer dynamiquement. Sinon, le système pourrait se voir manquer de mémoire RAM.

1 delete maTele ; Si plusieurs objets sont créés sous forme de tableau en mémoire dynamique, l'opérateur de destruction est différent.

1 delete[] mesTeles ;

Rappel : Création d'un tableau d'objets dynamique un tableau d'objet dynamique se déclare de la manière suivante :

1 CTelevision *mesTeles; 2 mesTeles = new CTelevision[20];

5. Constructeur / destructeur d'un objet

Constructeur d'un objet

Lorsque vous mettez sous tension la première fois une télévision, elle dispose de paramètres par défaut. Le volume est peut être réglé à 30% du maximum. On peut imaginer qu'une recherche automatique des chaîne à lieu et que la première chaîne sera affichée. On parle donc des valeurs d'initialisation de l'objet. Le constructeur est une méthode exécutée obligatoirement lors de l'instanciation de l'objet. Le nom de cette méthode est le nom de la classe (voir la classe ci-contre). Image 17 Constructeur / Destructeur Dans le code du constructeur, nous pouvons initialiser notre objet avant toute utilisation, c'est à dire initialiser les attributs. Il est également possible d'effectuer des déclarations dynamique de mémoire pour le bon fonctionnement de l'objet. Il faudra absolument en tenir compte lors de la destruction de l'objet. L'exemple suivant montre comment l'initialisation de l'objet television :

1 ///////////////////////////////// 2 // fichier ctelevision.h 3 ///////////////////////////////// 4 class CTelevision { 5 private : 6 ...

129 Ressources annexes

7 ... 8 public : 9 CTelevision() ; 10 } ; // CTelevision 11 12 13 /////////////////////////////////////// 14 // fichier ctelevision.cpp 15 /////////////////////////////////////// 16 #include "ctelevision.h" 17 CTelevision ::CTelevision() { 18 this->marque = "MARQUE"; 19 this->techno = "LED" ; 20 this->res = "4K" ; 21 this->diagonale = 140 ; 22 this->noSerie = 123456789 ; 23 this->chaine = 1 ; 24 this->volume = 10 ; 25 } // constructeur 26 27 ///////////////////////////////////////// 28 // fichier appli.cpp 29 ///////////////////////////////////////// 30 #include 31 int main() { 32 CTelevision *maTv ; 33 34 maTv = new CTelevision() ; // appel automatique du constructeur lors de l'instanciation 35 cout << maTv->volume << endl ; // affichage du volume 36 delete maTv ; // destruction de l'objet 37 } // main

Destructeur d'un objet

Le destructeur est une méthode spécifique d'un objet qui porte son nom précédé du caractère tilde (voir classe ci-contre). Le destructeur est appelé automatiquement lorsque la destruction de l'objet est demandée. Le rôle du destructeur est libérée les zone mémoire réservée dynamiquement pendant le fonctionnement de l'objet. Le destructeur est donc particulièrement important pour réaliser des programmes en langage objets fiables. Image 18 Constructeur / Destructeur

1 ///////////////////////////////// 2 // fichier ctelevision.h 3 ///////////////////////////////// 4 class CTelevision { 5 private : 6 CTnt *tnt; 7 ... 8 public : 9 CTelevision(); 10 ~CTelevision();

130 Ressources annexes

11 } ; // CTelevision 12 13 14 /////////////////////////////////////// 15 // fichier ctelevision.cpp 16 /////////////////////////////////////// 17 #include "ctelevision.h" 18 CTelevision ::CTelevision() { 19 tnt = new CTnt(); // création d'un objet faisant partie intégrante de l'objet CTelevision 20 } // constructeur 21 CTelevision ::~CTelevision() { 22 delete tnt; // destruction d'un objet créé durant la vie d'un objet CTelevision 23 } // destructeur 24 25 ///////////////////////////////////////// 26 // fichier appli.cpp 27 ///////////////////////////////////////// 28 #include 29 int main() { 30 CTelevision *maTv ; 31 32 maTv = new CTelevision() ; // maTv intègre un objet CTnt 33 cout << maTv->volume << endl ; 34 delete maTv ; // Appel automatique du destructeur pour détruire l'objet CTnt 35 } // main

Dans cet exemple, nous comprenons qu'un objet tnt sera créé à l'instanciation de l'objet maTv. En cas de destruction de l'objet maTv, l'objet interne tnt sera automatiquement détruit par appel du destructeur de maTv.

Synthèse sur les constructeurs/destructeurs Vous notez donc que les appels au constructeur et destructeur d'un objet se font automatiquement, respectivement lors de l'instanciation et lors de la destruction de l'objet. Si le constructeur est quasiment indispensable pour initialiser un objet, le destructeur n'est utile que pour gérer les allocations dynamiques de la mémoire effectuées durant la vie de l'objet. Il est donc possible qu'une classe ne contienne pas de destructeur.

Constructeurs paramétrés Ce ne sont pas toutes les télévisions qui seront de la même marque ou qui auront les mêmes caractéristiques. Il est donc utile de pouvoir initialiser un objet avec des valeurs différentes de celles par défaut. Cela est possible en créant un autre constructeur qui contient les paramètres d'initialisation. Ce constructeur existera en plus du constructeur par défaut (celui qui ne contient pas de paramètres).

Exemple : Constructeur paramétré pour une chaîne et un volume différent Imaginons que nous voulions créer un objet CTelevision avec la chaîne réglée à 6 et le volume à 0 :

131 Ressources annexes

1 ///////////////////////////////// 2 // fichier ctelevision.h 3 ///////////////////////////////// 4 class CTelevision { 5 private : 6 CTnt *tnt; 7 ... 8 public : 9 CTelevision(); 10 CTelevision(int noch=1, int novol=10); // valeur par défaut des paramètres 11 ~CTelevision(); 12 } ; // CTelevision 13 /////////////////////////////////////// 14 // fichier ctelevision.cpp 15 /////////////////////////////////////// 16 #include "ctelevision.h" 17 CTelevision ::CTelevision(int noch, int novol) { 18 this->marque = "MARQUE"; 19 this->techno = "LED" ; 20 this->res = "4K" ; 21 this->diagonale = 140 ; 22 this->noSerie = 123456789 ; 23 this->chaine = noch; // init de la chaîne 24 this->volume = novol; // init du volume 25 } // constructeur 26 27 ///////////////////////////////////////// 28 // fichier appli.cpp 29 ///////////////////////////////////////// 30 #include 31 int main() { 32 CTelevision *maTv1, *maTv2; 33 34 maTv1 = new CTelevision(6,0); // appel automatique du constructeur paramétré 35 maTv2 = new CTelevision(4); // seul la chaîne est changée, le volume reste par défaut 36 cout << maTv->volume << endl ; 37 delete maTv1 ; 38 delete maTv2 ; 39 } // main

Nous avons également créé un objet maTv2 qui ne change que le numéro de la chaîne. Cela est rendu possible par la syntaxe du C++ décrite ci-dessus. Mémorisez que :  Le constructeur est paramétré.  Que les paramètres ont une valeur par défaut au cas ou un paramètre est manquant (cas de maTv2).  L'utilisation du constructeur par défaut reste possible.

132 Ressources annexes

Attention : Utilisation directe des attributs privés de classe Dans le code source précédent, le constructeur paramétré modifie directement les attributs de classe chaine et volume. C'est dangereux et donc non conseillé. Il vaut mieux utiliser les méthodes setChaine() et setVolume() (les créer au besoin) qui vérifieront les valeurs demandées.

Complément : Les fichiers définissant une classe Lorsque vous développez une classe, vous devez créer 2 fichiers. Exemple, pour la classe CTelevision la définition des membres de la classes se fera dans ctelevision.h :

1 ///////////////////////////////// 2 // fichier ctelevision.h 3 ///////////////////////////////// 4 class CTelevision { 5 private : 6 CTnt *tnt; 7 ... 8 public : 9 CTelevision(); 10 ~CTelevision(); 11 ... 12 } ; // CTelevision

La définition de toutes les méthodes se fera dans le fichier ctelevision.cpp :

1 ///////////////////////////////// 2 // fichier ctelevision.cpp 3 ///////////////////////////////// 4 #include "ctelevision.h" 5 6 CTelevision : :CTelevision() { 7 ... 8 } // constructeur 9 10 CTelevision : :~CTelevision() { 11 ... 12 } // destructeur

6. Mes premiers objets

Rectangle Écrire un programme utilisant une classe rectangle dont le constructeur prend deux paramètres, largeur et hauteur et qui offre les fonctions suivantes :  calcul du périmètre  calcul de la surface  affichage ainsi que les accesseurs (lecture et modification de la largeur et de la hauteur).

133 Ressources annexes C. Les relations entre les objets

1. Héritage entre classe

La puissance du langage C++ réside dans le fait de pouvoir réutiliser une classe existante et l'améliorer en ajoutant des attributs ou des méthodes.

Exemple : Toujours la télévision Les nouvelles télévisions sont communicantes. Elles disposent d'une interface réseau leur permettant de se connecter à Internet. Elles disposent également de toutes les caractéristiques des télévisions décrites précédemment. L'héritage permet de spécialiser la classe CTelevision en une classe CTeleInter.

1 class CTeleInter : public CTelevision { 2 private : 3 CComReseau com ; // objet permettant de communiquer par le réseau 4 } ;

Cette classe CTeleInter hérite de la classe CTelevision. La représentation UML/SysML est exprimée ci-contre. Il faut comprendre que CTeleInter est une sorte de CTelevision.

Image 19 Héritage (formalisme UML/SysML)

134 Ressources annexes

On dira que la classe CTeleInter est une spécialisation de la classe CTelevision. On peut aussi dire que la classe CTelevision est une généralisation de la classe . Image 20 Spécialisation / Généralisation CTeleInter On dit aussi que CTeleInter dérive la classe CTelevision. Le diagramme ci-contre illustre une généralisation.

Les 3 classes Square, Circle, Triangle ont une relation de généralisation avec la classe Shape. La classe Shape décrira les caractéristiques et comportements communs à toutes les formes. Chaque classe spécialisée décrira les caractéristiques et comportements spécifiques à une forme.

Mode d'héritage : Les spécificateurs Dans l'exemple de la télévision, L'héritage est public. L'autre possibilité est private. public est le spécificateur de l'héritage.

1 class CTeleInter : public CTelevision { 2 ... 3 } ;

Classe de base héritage public (classe héritage private (classe dérivée) dérivée)

Membres public accessibles et public accessibles et private

Membres private inaccessibles inaccessibles

Membres protected accessibles et protected accessibles et private (voir plus bas) Tableau 18 Les spécificateurs lors de l'héritage

Pour comprendre comment lire ce tableau avec les classes CTeleInter et CTelevision. 1ère ligne :  avec un spécificateur public d'héritage, les membres public de CTelevision seront accessibles directement à partir de CTeleInter.  Avec un spécificateur private, les membres private de CTelevision seront accessibles directement à partir de CTeleInter mais vue comme des membres private. De ce fait, en cas de spécialisation de CTeleInter, les membres public ne seront pas accessibles à la classe dérivée (tout comme les membres protected, ligne 3).

a) Le spécificateur protected pour un membre d'une classe

Le spécificateur protected a un sens proche de private.

135 Ressources annexes Un membre protected d'une classe est considéré comme private dans cette classe. On sait qu'un membre private d'une classe n'est pas accessible dans la classe dérivée. L'avantage d'un membre protected d'une classe de base est qu'il sera accessible dans la classe dérivée comme un membre private et donc non accessible en cas de nouvelle spécialisation. Si c'est pas clair, on en discute...

Complément : Représentation UML d'une membre protected Le symbole # est utilisé en préfixe de la déclaration du membre de la classe.

b) L'incidence de l'héritage sur les constructeurs

Les constructeurs et destructeur ne sont pas hérités lors d'une spécialisation. Dans la classe dérivée, il est nécessaire de définir les nouveaux constructeurs en tenant compte de ceux de la classe de base.

1 //////////////////////////////////////////// 2 // Fichier cteleinter.h 3 //////////////////////////////////////////// 4 #include "ctelevision.h" 5 class CTeleInter : public CTelevision { 6 private : 7 CComReseau com ; // objet permettant de communiquer par le réseau 8 public : 9 CTeleInter(int noch, int novol) ; 10 } ; 11 12 ///////////////////////////////////////////// 13 // Fichier cteleinter.cpp 14 ///////////////////////////////////////////// 15 #include "cteleinter.h" 16 CTeleInter : :CTeleInter(int noch, int novol) : CTelevision(noch, novol) { // CE QUI CHANGE ! 17 ... 18 } // constructeur Notez l'écriture C++ dans la définition du constructeur. Un appel est fait au constructeur de la classe de base en lui fournissant les valeurs demandées. Il ne reste plus qu'à définir l'initialisation spécifique aux objets CTeleInter.

c) Classe amie

Il peut s'avérer utile de permettre à une classe dérivée d'accéder aux membres private de sa classe de base. L'opérateur friend permet cela. Le concepteur de la classe de base doit déclarer explicitement les classes dérivées qui auront le droit d'accéder aux membres private de la classe de base.

1 ///////////////////////////////// 2 // fichier ctelevision.h 3 ///////////////////////////////// 4 class CTelevision { 5 private :

136 Ressources annexes

6 CTnt *tnt; 7 ... 8 public : 9 CTelevision(); 10 CTelevision(int noch, int novol) ; 11 ~CTelevision(); 12 friend class CTeleInter ; 13 } ; // CTelevision

Les objets CTeleInter auront ainsi accès aux membres private de Ctelevision. Par contre, l'inaccessibilité reste maintenue pour toute autre classe dérivée de CTelevision.

d) Polymorphisme, fonctions virtuelles, classes abstraites

Polymorphisme Considérez le code source suivant :

1 class CForme { 2 public : 3 void affiche() ; 4 } // CForme 5 6 void CForme : :affiche() { 7 cout << "Je suis une forme".endl; 8 } // affiche 9 class CTriangle : public CForme { 10 } // CTriangle 11 12 class CCercle : public CForme { 13 } // CCercle En exécutant le code suivant :

1 CForme f ; 2 CCercle c ; 3 f.affiche() ; 4 c.affiche() ; Ces instructions afficheront le même message :

1 Je suis une forme 2 Je suis une forme

Cela est dû au fait que la classe CCercle hérite de la méthode affiche() de CForme. Ce n'est pas forcément ce qu l'on souhaite. Si nous voulons obtenir un affichage précis correspondant à l'objet en cours, il faudra redéfinir la méthode affiche() dans chaque classe dérivée.

1 void CTriangle::affiche() { 2 cout << "Je suis un triangle" << endl; 3 } // affiche 4 void CCercle::affiche() { 5 cout << "Je suis un cercle" << endl; 6 } // affiche L'affichage à présent sera cohérent. Effectivement, il existe plusieurs fonctions affiche(), une dans chaque classe. Il n'y a pas de conflit, c'est le polymorphisme du C++ qui permet ce fonctionnement.

137 Ressources annexes

Fonctions virtuelles Nous voulons stocker plusieurs objets CForme dans un tableau de la manière suivante :

1 CForme f ; 2 CTriangle t ; 3 CCercle c ; 4 CForme *fs[3]={&t, &c, &f} ;

Le code ci-dessus crée 2 objets t et c, ainsi qu'un tableau de pointeur fs pour stocker les adresses des objets t et c. Si nous voulons afficher les méthodes affiche() :

1 for (int i=0 ; i<2 ; i++) { 2 fs[i]->affiche() ; 3 } // for Et là, problème...L'affichage est le suivant :

1 Je suis une forme 2 Je suis une forme 3 Je suis une forme

La tableau étant déclaré de type CForme, c'est une référence à la méthode affiche() de cette classe qui est invoquée. Pour que cela fonctionne, il est nécessaire de déclarer la méthode affiche() comme virtual dans la classe de base et dans les classes dérivées.

1 virtual void affiche() ;

Dans la définition de la méthode (fichier .cpp) :

1 virtual void CForme::affiche() { 2 ... 3 } //affiche Les affichages seront à présent les bons. Dans les classes dérivées de CForme, les méthodes affiche() seront également virtual.

Fondamental : Méthode virtuelle La méthode affiche() examinée plus haut est virtuelle. Elle est définit dans sa classe de base et éventuellement dans les classes dérivée. Grâce au polymorphisme (plusieurs formes), chaque classe dérivée peut utiliser ou pas une spécialisation de cette méthode.

Fonction virtuelle pure et classe abstraite Les fonctions virtuelles pures définissent des comportements qui devront impérativement être définis dans les classes dérivées. La syntaxe de déclaration dans la classe est la suivante :

1 class CVehicule { 2 virtual void dessiner()=0; // syntaxe d'une méthode virtuelle pure 3 } // CVehicule Cette classe laisse entendre qu'il sera nécessaire de dessiner le véhicule mais cela n'est pas possible pour le moment car cela dépend du type de véhicule, donc d'une

138 Ressources annexes spécialisation de cette classe.

139 Ressources annexes

Définition : Méthode virtuelle pure ? C'est une méthode dont le code n'est pas défini. Elle se déclare dans le corps de la classe précédée du mot clef virtual. La fonction est initialisée à 0.

140 Ressources annexes

Définition : Classe abstraite ? C'est une classe qui contient au moins une méthode virtuelle.

Attention : Instancier une classe abstraite Il n'est pas possible de créer un objet à partir d'une classe abstraite. L'instanciation n'est pas possible de fait qu'au moins une méthode n'est pas définie. La classe doit obligatoirement être dérivée et ses fonctions virtuelles définies.

2. Association entre classe

Une association décrit une relation unidirectionnelle ou bidirectionnelle entre 2 classes. Sa représentation UML/SysML est la suivante :

Association entre classes

Une association peut disposer de nom de rôle de par et d'autre.

En C++, cela peut se traduire par un appel de méthode d'un objet extérieur sur lequel on pointe. Lorsqu'on veut mettre en évidence que c'est une association unidirectionnelle, on ajoute une flèche dans le sens souhaité. Dans une association bidirectionnelle, les deux classes disposent du moyen d'accéder à un membre de chaque classe.

Une association est une relation très simple. En revanche, il est possible de coupler les classes d'une manière plus forte selon le fonctionnement qu'on souhaite exprimer. On parle de sémantique. A suivre dans la suite...

141 Ressources annexes 3. Composition entre classes

Complément : Sémantique Les parties suivantes demande de comprendre des notions à forte sémantique. La sémantique correspond à la valeur des mots. Vous verrez qu'il est possible de vouloir exprimer deux pensées différentes sur un diagramme UML/SysML alors que le codage en C++ est le même. Le diagramme permet de mettre en évidence une sémantique différente.

La relation de composition Pour bien comprendre ce concept, la relation de composition entre deux classes peut être remplacé par "est composé de" ou "contient comme partie". En UML/SysML, la représentation est la suivante :

Composition

CTelevision est composé de CTnt. CTelevision est nommé l'agrégat tandis que CTnt est un des composants de l'agrégat. Mémorisez le sens de tous ces mots pour les assimiler. Le losange foncé est côté agrégat. La flèche pointe sur le composant. Il est possible de rajouter une cardinalité ou multiplicité côté composant. Côté agrégat, c'est toujours 1.

Attention : Caractéristique de la composition Si l'agrégat est détruit, les composants le sont aussi. La multiplicité est de 1 côté agrégat.

Le codage en C++ d'une composition est la suivante :

1 class CTeleInter : public CTelevision { 2 private : 3 CComReseau com ; // objet permettant de communiquer par le réseau 4 } ; Nous avons déjà examiné ce code dans le cadre de l'héritage. La déclaration de l'objet com est statique, dans la même zone mémoire que celle de l'objet instance de CTeleInter.

4. Agrégation entre classes

Agrégation entre classes L'agrégation est une association non symétrique, qui exprime un couplage fort et une relation de subordination.

142 Ressources annexes Elle représente une relation de type "ensemble / élément". Nous avons vu avec l'agrégation forte (composition) que la destruction de l'agrégat impliquait la destruction des éléments ou composants. Ce n'est pas le cas avec la relation d'agrégation. C'est un couplage moins fort.

Exemple : Diagramme de classe pour un décrire un mail Dans le diagramme ci-dessous, notez que la cardinalité côté agrégat peut être différente de 1.

Agrégation entre classes

Le codage en C++ d'une agrégation est la suivante :

1 class CEnseignant {...}; 2 class CDepartement { 3 ... 4 private: 5 CEnseignant *enseignants[5]; // Agrégation 6 ... 7 }; 8 class CUniversite { 9 ... 10 private: 11 CDepartement facultes[20]; // Composition 12 ... 13 }; L'agrégation se code comme un tableau de pointeur sur des objets déclarés dynamiquement en mémoire. Soit ces objets ont été créés durant la vie de l'objet agrégat, auquel cas il faudra impérativement prévoir de les effacer (dans le code du destructeur par exemple). Soit ils ont été créés par un autre objet et pointe donc seulement dessus. Cela signifie qu'à la mort de l'objet, les éléments restent existants. Dans l'exemple ci-dessus, il est possible que les objets de type Eenseignants restent en mémoire, même si l'objet de type Departement est détruit. Ce n'est pas le cas de la composition de ce diagramme. En cas de destruction d'un objet de type CUniversite, les objets facultes sont détruits.

5. Contrôle d'un capteur de distance

Application Qt pour contrôler une distance Le principe est de faire l'acquisition d'une distance par un capteur connecté sur le port GPIO d'une carte Raspberry. Cette méthode est utilisée sur le logiciel embarqué sur un drone, projet de 2nde année.

143 Ressources annexes

Diagramme de classe

Comme le montre le diagramme de classe UML ci-dessus, notre application informatique est composée des classes suivantes :  MainApp. Cette classe gère l'IHM de notre application. Elle est composée de la classe CCapteurGpioPing_Dist représentant notre capteur.  CCapteurGpioPing_Dist hérite de la classe QThread et est composée de cla classe CGpio. Elle dispose aussi d'une relation d'agrégation avec QSharedMemory mais n'est pas responsable de sa création ni sa destruction. La classe contient à minima les méthodes run() et lireDistance() . Paramètres à définir.  QThread est la classe existante permettant de gérer un thread, c'est à dire l'exécution d'une fonction en parallèle avec l'application principale. Cette méthode se nomme run(). Elle doit être définie dans la classe CCapteurGpioPing_Dist  QSharedMemory est une classe existante permettant de mémoriser des informations pour plusieurs processus dans le même système. Cette mémoire partagée est créée par la classe MainApp et utilisée par la classe CCapteurGpioPing_Dist.  CGpio est une classe dédiée à l'accès du GPIO de la carte Raspberry. Le fonctionnement sera simulé par trois méthodes : init(), lire(), ecrire(). Les paramètres de fonctionnement sont à définir.

Question 1 Codez l'infrastructure logicielle (seulement) avec qt-creator sous Linux en vous servant du diagramme de classe ci-dessus. Les noms des classes sont à respecter. L'IHM permettra de lire la valeur (simulée) de la distance (elle doit pouvoir varier).

Question 2 Pour les plus avancés, codez réellement l'application en faisant des recherches sur le site github/ditRasp pour comprendre le fonctionnement. Vous aurez besoin d'une carte Raspberry et d'un capteur.

144

IX - Activités liées à la IX fabrication et aux communications électroniques

A. Documents de fabrication, d'assemblage et de montage de la carte électronique

Attention : Version Proteus 7.10 Version PROTEUS 7.10 !

145 Activités liées à la fabrication et aux communications électroniques

Complément : Documents de fabrication version PROTEUS 7.10

 Fichier Proteus Isis (7.10) (cf. formProfQtCpp_V1-2)  Version .pdf (cf. formProfQtCpp_V1-2)

 Fichier Proteus ARES (7.10) (cf. formProfQtCpp_V1-2_PCB)  Fichiers Gerber (cf. formProfQtCpp_V1-2 - CADCAM)

Attention : Version KiCad 5 Version KiCad 5 !

146 Activités liées à la fabrication et aux communications électroniques

Complément : Documents de fabrication version KiCad 5

 Version .pdf (cf. KiCad5_Schema_Structurel)

 (cf. formProfQtCpp_V1-2)Projet KiCad 5 (cf. KiCad5_Projet)  Fichiers Gerber (cf. FormQtCpp_KiCad5)  Modules (empreintes) spécifiques (cf. KiCad5_Modules_specifiques) (pour une installation standard sous Windows, à désarchiver et à coller ici : C:\Program Files\KiCad\share\kicad\modules)

147 Activités liées à la fabrication et aux communications électroniques  Packages 3D spécifiques (cf. KiCad5_Packages3D_specifiques) (pour une installation standard sous Windows, à désarchiver et à coller ici : C:\Program Files\KiCad\share\kicad\modules\packages3d)

Attention Tout ce qui suit est identique, quelle que soit la version !

Complément : Liste des composants = BOM (Bill Of Materials) Liste des composants_formProfQtCpp_1-2 (cf. Nomenclature_formProfQtCpp_1-2) A noter :  quelle que soit la version (Proteus ou KiCad) cette nomenclature est identique  cette liste est normalement exhaustive, et doit permettre la réalisation de l'intégralité de la réalisation,  les codes commandes chez les différents fournisseurs sont donnés à titre indicatif, il est bien sûr possible de trouver des équivalents ailleurs,  ! ! les composants ne sont pas toujours pérennes chez les distributeurs, c'est cette nomenclature qui contient les dernières mises à jour, les codes commande inclus dans les schémas structurels ne sont pas forcément actualisés.  les prix indiqués permettent d'avoir une estimation du coût global de la réalisation.

Remarque : PCB ajouré

Une ouverture a été prévue dans le circuit imprimé, pour les Raspberry Pi qui disposent d'un dissipateur thermique collé sur le BCM283x

148 Activités liées à la fabrication et aux communications électroniques

Méthode : Repérage des composants par type (CMS/traversant) et par côté de câblage

 Version .pdf (cf. formProfQtCpp_V1-2_Cablage_composants)

Méthode : Soudage des CMS

Cf. "Soudage des CMS d'un shield pédagogique pour Raspberry Pi (web_Video_Soudage_CMS)" Soudage des CMS d'un shield pédagogique pour Raspberry Pi

Conseil : Câblage des composants CMS Il est vivement conseillé de commander le stencil (pochoir) de la carte en même temps que les PCB, afin de souder les composants CMS avec de la crème à braser et un four à refusion. Cette recommandation est essentiellement liée au câblage de U2 et U3.

Stencil métallique utilisé :

Clichés pris avec une loupe des soudures des 2 capteurs CMS, SHT20 (boîtier DFN6) et TC72 (MSOP8), soudures effectuées avec pochoir + pâte à braser + four à

149 Activités liées à la fabrication et aux communications électroniques refusion :

Intérêt de cette technique : même si la pâte à braser n'est pas parfaitement disposée sur l'empreinte du composant (photo de gauche), après refusion les soudures sont parfaitement dissociées (photo de droite) :

Exemple : Chronologie de câblage : CMS CMS câblés côté TOP

150 Activités liées à la fabrication et aux communications

électroniques

CMS câblés côté BOTTOM

151 Activités liées à la fabrication et aux communications

é lectroniques

Méthode : Chronologie de câblage des composants traversants Ordre de câblage des composants traversants côté TOP :  Support U1  J3  Points de test  J2  SW1 Ordre de câblage des composants traversants côté BOTTOM :  J1  FU1 (après avoir détordu les broches)(Il peut également être soudé côté TOP) Carte côté TOP après câblage des composants traversants :

Idem mais avec SW1 en version : APEM POUSSOIR CI KSA TRAVERSANT LONG

152 Activités liées à la fabrication et aux communications

électroniques

153 Activités liées à la fabrication et aux communications électroniques

Ci-dessous carte côté BOTTOM après câblage des composants traversants, et ajout de la butée amortisseur adhésive (modèle ci- contre) qui permet au shield d'être à l'horizontal en prenant appui sur le "CSI Camera connector ". Légère découpe de la butée au cutter.

154 Activités liées à la fabrication et aux communications électroniques

Méthode : Assemblage de l'afficheur sur la carte

L'afficheur est livré avec un câble grove de 20cm, mais nous avons préféré utiliser un câble de 5cm, qui permet d'obtenir un montage plus compact, moins susceptible d'être accroché malencontreusement par des outils ou appareils de mesures, et plus pratique si l'on souhaite effectuer une mise en boîtier. Ci-dessous, liaison entre l'afficheur et le shield avec le câble grove A noter : il est plus pratique de connecter d'abord l'une des extrémités du câble sur l'afficheur, puis l'autre sur le shield

155 Activités liées à la fabrication et aux communications

é lectroniques

L'assemblage par visserie utilise l'association suivante :  Vis M2, 20 mm, Nylon 6.6 (ou vis acier inoxydable, photo du milieu)  Entretoise, Ronde, 3x4mm, Nylon 6.6  Entretoise laiton, M2, Hexagonale Femelle, 10 mm  Ecrou, Hexagonal, M2, Nylon 6.6 Cette association est répétée 3 fois pour obtenir une bonne rigidité de l'ensemble. A noter : pour éviter tout risque de perte des entretoises nylon (non filetées) lorsque l'on retire l'afficheur (par exemple pour faire des mesures sur les points de test), les 2 premiers montages ci-contre (entourés en vert) sont recommandés, le 3ème est déconseillé. A noter : si les vis nylon M2 20mm ne sont plus disponibles, des vis 25mm conviennent également et peuvent être raccourcies. Assemblage vu des 4 côtés :

156 Activités liées à la fabrication et aux communications

électroniques

157 Activités liées à la fabrication et aux communications électroniques Vue de dessus avec les 3 vis et le câble grove :

Vue de dessous :

Afficheur vissé sur le shield, le tout enfiché sur carte Raspberry Pi :

158 Activités liées à la fabrication et aux communications

électroniques

On peut éventuellement envisager une mise en boîtier personnalisée, réalisée avec une imprimante 3D :

Complément : Premiers essais du montage Après le câblage et l'association des différents éléments il peut être intéressant de faire quelques vérifications à minima et ne nécessitant pas la mise en œuvre de Qt.

159 Activités liées à la fabrication et aux communications électroniques

Après la mise sous tension, la LED jaune D2 témoigne de la présence de la tension 3,3V sur le GPIO.

Le MAX232 est le seul circuit du shield amené à communiquer avec un élément extérieur à la carte Rpi (tels qu'une autre carte Rpi, un ordinateur, etc ...). Une fausse manipulation destructrice n'est pas a exclure c'est la raison pour laquelle le composant a été choisi en version traversante, et disposé sur support pour pouvoir être changé facilement. A vérifier : lorsque la carte est sous tension (enfichée sur le GPIO de la carte Rpi) :  la tension sur la broche 2 (VS+) du MAX232 doit être d'environ 9,1V  la broche 6 (VS-) doit être à environ -8,7V

Vérification de la présence des composants I2C sur le bus du même nom : Il suffit d'installer la librairie libi2c-dev Ouvrir pour cela un terminal et installer le paquet :  ctrl+alt+t  sudo apt-get install libi2c-dev puis taper la commande :  sudo i2cdetect -y 1 les adresses des composants présents sur le bus I2C doivent apparaître. Parmi les 5 qui apparaissent, l'adresse 0x40 correspond au capteur SHT20, les 4 autres à l'afficheur LCD I2C

Complément Si vous vous lancez dans une fabrication en série, et que tout se passe bien, voilà ce que cela pourrait donner :

160 Activités liées à la fabrication et aux communications

électroniques

B. Outils de visualisation et d'interprétation/décodage des trames logiques

Fondamental Le BTS SN permet de développer des applications aussi bien logicielles que matérielles. Le lien entre ces 2 parties passe bien souvent par un bus de données. Il est donc nécessaire d'utiliser un outil de visualisation et d'interprétation de ces bus soit lors de la phase de mise au point de l'application, soit lors d'une intervention de dépannage.

161 Activités liées à la fabrication et aux communications électroniques

Ces outils peuvent être des oscilloscopes disposant de menus d'analyse de trames. Cependant ces appareils sont coûteux, d'un maniement relativement complexe, sont relativement encombrants, ils ne disposent que de 4 voies au maximum. De plus ils ne proposent souvent que l'interprétation de trames "classiques", et ne permettent pas de programmer ses propres interpréteurs. Bref, tout le contraire des analyseurs logiques qui sont d'un coût bien plus abordable. Donc si vous disposez d'un oscilloscope intégrant l'analyse de trame, les mesures qui suivent sont tout à fait réalisables, mais dans les pages qui suivent celles-ci seront illustrées par des relevés effectués avec des analyseurs logiques compacts.

Remarque : 3 analyseurs utilisés sur cette formation Il existe un large choix d'analyseurs logiques compacts avec une ergonomie, des fonctionnalités et des moyens de raccordement qui diffèrent légèrement d'un modèle à l'autre. Voici les 3 analyseurs qui ont été utilisés sur la carte développée, et dont les relevés illustrent ces pages :  Saleae : Logic 4,  Ikalogic : SQ100,  Digilent : Analog Discovery 2. peut-être avez-vous dans votre établissement un modèle de l'une de ces 3 familles. Pourquoi 3 analyseurs ?  pour ne pas être taxé de publicité clandestine ;-)  car ils ont plusieurs points communs (voir ci-dessous), mais aussi quelques spécificités qui seront illustrées à l'occasion. Pourquoi ces 3 là ?  l'un d'entre eux donne satisfaction dans notre section de BTS depuis plusieurs années; et il a été plutôt bien noté dans un article de vulgarisation relativement récent8,  un autre est de conception et de fabrication française, et l'entreprise en question recrute régulièrement des techniciens, tels que ceux que nous formons,  un autre encore est très prisé des collègues de physique appliquée, et doit donc être à disposition dans de nombreux établissements. Quelques points communs entre ces différents analyseurs :  le logiciel d'acquisition est gratuit, et compatible Windows, Linux, Mac ; de plus il dispose des interpréteurs UART, SPI, I2C et de bien d'autres encore.  Ces 3 logiciels ont été développés sous Qt !! Parmi les spécificités de certains d'entre eux :  la possibilité de générer des signaux de commande sur le bus, par exemple pour se substituer à un maître I2C afin de tester un circuit esclave,  la mise à disposition d'un framework (SDK) gratuit permettant de 8 - https://www.hackable.fr/?p=539

162 Activités liées à la fabrication et aux communications électroniques personnaliser un interpréteur existant, ou de créer de toute pièce un décodeur pour un nouveau protocole,  un déclenchement sur un événement particulier (par exemple une adresse ou une valeur spécifique circulant sur le bus),  des fréquences d'échantillonnage plus ou moins élevées,  la possibilité, ou pas, de capturer des trames en temps réel.

 une connectique livrée avec l'analyseur permettant de se raccorder par des grippes-fils,

 la possibilité également d'enlever les grippes-fils pour se brancher directement sur un point test

Analyseurs Saleae

 Lien site saleae.com/fr9 Entreprise située à San Francisco (USA)

9 - https://www.saleae.com/fr

163 Activités liées à la fabrication et aux communications

é lectroniques

Protocoles décodés par le logiciel Logic 1.2.18

164 Activités liées à la fabrication et aux communications électroniques

Analyseurs Ikalogic

 Lien site ikalogic.com10 Entreprise basée à Limoges (France)

Protocoles décodés par Scanastudio 3.0.14 A noter : sous Windows et avec Avast, il est nécessaire d'inhiber l'antivirus lors de l'installation de ScanaStudio

10 - https://ikalogic.com/

165 Activités liées à la fabrication et aux communications électroniques

Analyseurs Digilent

 Lien site Digilent11 Entreprise basée à Pullman (USA)

Logiciel WaveForms12

11 - https://store.digilentinc.com/analog-discovery-2-100msps-usb-oscilloscope-logic-analyzer-and-variable-power- supply/ 12 - https://store.digilentinc.com/waveforms-previously-waveforms-2015/

166 Activités liées à la fabrication et aux communications électroniques

Protocoles décodés par Waveforms 3.7.5

C. Mesures sur le bus SPI

Complément : Documentation du capteur TC72 et complément sur le bus SPI

 Documentation du capteur de température TC72 (cf. TC72)

Complément sur le BUS SPI :  Le bus SPI peut fonctionner dans 4 modes différents. Ces modes sont liés au front d'activation (CPHA = Clock Phase) du signal d'horloge (SCK) et au niveau de repos de ce même signal d'horloge (CPOL = Clock Polarity).  Le tableau et les chronogrammes ci-dessous (site d'origine13) illustrent ces différents modes.  Les circuits esclaves sont généralement compatibles avec seulement 1 ou 2 de ces 4 modes SPI. Le circuit maître doit donc s'aligner sur l'un de ces modes et générer en tant que maître les signaux correspondants.  Dans le cas où 2 circuits esclaves sont utilisés et qu'ils n'ont pas de mode SPI commun, il faut alors que le circuit maître reconfigure le bus SPI à chaque fois qu'il communique avec l'un ou l'autre des circuits.

13 - https://www.corelis.com/education/SPI_Tutorial.htm

167 Activités liées à la fabrication et aux communications

é lectroniques

Fondamental : Questions préliminaires  Donner la résolution du capteur.  Identifier dans la documentation du TC72 l'adresse des registres qui contiennent la température et le « Manufacturer ID »  Dans le constructeur de la classe Cspi identifier le nom des attributs qui correspondent : - à la vitesse du bus, - au niveau logique d'activation du CS - au MODE SPI utilisé.  Identifier dans le fichier « ccapteur_spi_tc72_nth.cpp » les valeurs données à chacun de ces attributs lors de l'instanciation du capteur  Vérifier que le mode sélectionné dans le fichier est compatible avec le TC72. Si un autre mode peut également permettre de communiquer avec le TC72, indiquer lequel.

Méthode : Mesures Le bus SPI ne concerne pas l'afficheur LCD, les mesures peuvent donc être effectuées sur le shield seul. Attention cependant à remettre les écrous après avoir retiré l'afficheur, afin de ne pas les perdre.  Relier l'analyseur aux 4 signaux du BUS ainsi qu'au 0V commun.  Configurer l'interpréteur SPI de l'analyseur logique utilisé pour le mode programmé dans l'application, de même pour le niveau d'activation du CS.  Effectuer une capture de la trame lors de la lecture de la température et lors de la lecture de l'identifiant du fabricant. Interprétation  Justifier les différents éléments de la trame, commande et données, en vous appuyant sur la documentation, et en comparant avec la valeur affichée sur l'interface graphique.  Vérifier par modification du programme si un, ou d'autres, MODE SPI sont utilisables.  Mesurer la vitesse du bus et la commenter. Conclure.

168 Activités liées à la fabrication et aux communications

électroniques Complément : Réponses attendues TC72 : Résolution de la température : 10 bits → 0,25°C/bit Registres : LSB Temperature → 0x01 ; MSB Temperature → 0x02 ; Manufacturer ID → 0x03 Attributs : int speed = 5000000Hz = 5MHz ; bool csHigh = TRUE ; int mode SPI_MODE_1 Mode 1 : horloge SCK active sur front descendant, avec un niveau de repos à l'état bas. Ce qui est cohérent avec la documentation du capteur :

169 Activités liées à la fabrication et aux communications électroniques

Autre mode possible : le MODE 3

Registres internes du TC72 et chronogramme de lecture d'une température :

170 Activités liées à la fabrication et aux communications

électroniques

Complément : Exemples de mesures avec l'analyseur Logic 4 Saleae

Mesures effectuées avec l'analyseur Logic 4 Saleae Liaisons par grippe-fil ou par insertion directe sur les picots, comme ci- contre :

Valeur affichées sur l'interface graphique Qt pour les mesures effectuées. Les valeurs 54 et 28,75 doivent être retrouvées dans les trames transmises

Configuration de l'interpréteur SPI sur Saleae : Mode 1 et CE = 1

171 Activités liées à la fabrication et aux communications

é lectroniques

Capture globale de la trame avec les 2 mesures :  Le déclenchement se fait sur un front montant de CE, la durée d'acquisition est prévue suffisamment longue pour capturer les 2 lectures. Les échanges effectués pour récupérer le Manufacturer ID et la température figurent dans la colonne de droite. Zoom sur : Manufacturer ID  La valeur 0x54 est interprétée.  On vérifie bien que SCL est active sur un front descendant, et a un niveau de repos bas → SPI MODE 1

Zoom sur la capture de la température : Les 10 bits de la température sont constitués de l'octet 0x1C pour la partie entière, auquel il faut ajouter les 2 bits de poids fort de 0xC0 pour les centièmes de degrés. Ce qui donne 28,75°C et correspond à la valeur affichée sur l'interface graphique.

172 Activités liées à la fabrication et aux communications

électroniques

La communication en MODE 3 est effectivement fonctionnelle, pour l'obtenir il faut intervenir :  dans le fichier "ccapteur_spi_tc72_nth.cpp"  sur la ligne "m_spi = new CSpi(this, m_ce, 5000000, true, SPI_MODE_3);" qui instancie la configuration du bus. Les modes 0 et 2 renvoient par contre des valeurs erronées. Les essais (les modes 1 et 3 viables, les modes 2 et 4 dysfonctionnent) sont donc cohérents avec l'analyse de la documentation. Mesure de la vitesse sur le bus SPI : 3MHz Conclusion : la carte Rpi devant gérer le système d'exploitation, la vitesse du bus SPI n'est pas garantie.

173 Activités liées à la fabrication et aux communications

é lectroniques

Complément : Exemples de mesures avec l'analyseur Ikalogic SQ100 Mesures effectuées avec l'analyseur Ikalogic SQ100 Câblage de l'analyseur : liaison par grippe-fil

Configuration de l'interpréteur : tout à fait similaire à celle de Saleae

174 Activités liées à la fabrication et aux communications

électroniques

Capture et interprétation du Manufacturer ID

Capture et interprétation de la température

175 Activités liées à la fabrication et aux communications

é lectroniques

Mesure de la fréquence : 3,125MHz (même constat que précédemment)

176 Activités liées à la fabrication et aux communications

électroniques Complément : Exemples de mesures avec l'analyseur Analog Discovery 2

Mesures effectuées avec l'analyseur Analog Discovery 2 Câblage de l'analyseur, liaisons par grippe-fil ou par insertion directe sur picots (comme ici)

Configuration de l'interpréteur SPI :  A noter : on ne peut pas interpréter simultanément MOSI et MISO, sur un même interpréteur, il faut donc en configurer 2 si besoin.  Exemple de capture du Manufacturer ID, avec mise en évidence de la configuration , et mesure de la fréquence d'horloge SPI.  Déclenchement sur le début d'une trame (Protocole → SPI Trigger → Start ).  Utilisation du mode Repeat + Run pour obtenir un rafraîchissement des données à chaque nouvelle émission d'une commande depuis le maître.

Mesure pour une température de 25,75°C :

177 Activités liées à la fabrication et aux communications

é lectroniques

D. Mesures sur le bus I2C

Complément : Documentation du capteur SHT20 et complément sur le bus I2C

 Documentation du capteur de température et d'humidité relative SHT20 (cf. SHT20)

Complément sur le BUS I2C :  Le bus I2C est composé de 2 signaux, l'horloge SCL et la donnée SDA.  Un échange sur le bus I2C débute par une séquence de START et se termine par une séquence de STOP.  Il est donc conseillé de déclencher l'analyseur logique sur un front descendant du signal de données SDA.

Fondamental : Questions préliminaires  Indiquer l'adresse du capteur sur le bus I2C en mode 7 bits.  Rappeler les 2 vitesses (standards) du bus I2C.  Donner la résolution par défaut de la mesure de température d'une part, et de l'humidité relative d'autre part.

Méthode : Mesures Les mesures qui suivent sur le bus I2C ne concernent que le capteur SHT20, pas

178 Activités liées à la fabrication et aux communications électroniques l'afficheur. Les mesures peuvent donc être effectuées sur le shield seul. Attention cependant à remettre les écrous après avoir retiré l'afficheur, afin de ne pas les perdre.  Relier l'analyseur aux 2 signaux du BUS ainsi qu'au 0V commun  Configurer l'interpréteur.  Lancer une capture lors de la lecture de la température et lors de la lecture de l'humidité relative. Interprétation  Justifier les différents éléments de la trame, adresse et données, en vous appuyant sur la documentation, et en comparant avec la valeur affichée sur l'interface graphique.  Indiquer la vitesse du bus parmi les 2 valeurs indiquées précédemment.

Complément : Réponses attendues SHT20 :  Adresse sur le bus I2C en mode 7 bits : 0x40  Vitesses de transmission standards les plus basses : 100KHz et 400KHz  Résolution par défaut de l'humidité relative : 12 bits, de la température : 14 bits ; voir le tableau 8 de la documentation. Pour vérifier que cette résolution est bien la bonne, taper en mode console (il faut avoir installé libi2c-dev au préalable) : "i2cget -y 1 0x40 0xE7 b" la réponse doit avoir ses bits 7 et 0 au niveau logique 0 ; par exemple 0x3A. Signaux échangés lors d'une mesure d'humidité relative en mode "no hold master" :

179 Activités liées à la fabrication et aux communications

é lectroniques

180 Activités liées à la fabrication et aux communications

électroniques Complément : Mesures avec l'analyseur Logic 4 Saleae

Mesures effectuées avec l'analyseur Logic 4 Saleae Liaisons par grippe-fil ou par insertion directe sur picots, comme ci-contre :

Interface graphique Qt pour les mesures effectuées sur le bus I2C : les valeurs 30,0 et 29,5 doivent être retrouvées dans les trames transmises

Mesure de la température :  configuration de l'interpréteur : désignation des broches et définition du format de l'adresse sur 7 bits

181 Activités liées à la fabrication et aux communications

é lectroniques

Vérification de la température mesurée :  on retrouve la commande de lecture de température 0xF3,  sur les 2 bytes récupérés, les 2 bits de poids faible qui correspondent au status (voir paragraphes 5.4 et 6 de la documentation) doivent être forcés à 0,  puis la formule T = -46.85 + 175.72*N/65536 du paragraphe 6.2 doit être appliquée,  pour les relevés ci-dessus cela donne : 30,0°C Mesure de l'humidité relative :

Vérification de la valeur de l'humidité relative :  idem pour les 2 bits de status ainsi que pour les 2 bits qui précèdent, car le résultat est sur 12 bits. Donc les 4 derniers bits doivent être forcés à 0,  puis la formule RH = -6 + 125*N/65536 du paragraphe 6.1 doit être

182 Activités liées à la fabrication et aux communications électroniques appliquée,  pour les relevés ci-dessus cela donne : 29,5%

Complément : Exemples de mesures avec l'analyseur Ikalogic SQ100

Mesures effectuées avec l'analyseur Ikalogic SQ100 Câblage de l'analyseur par grippe-fil :

Configuration de l'interpréteur I2C :

Configuration du déclenchement de la mesure :

183 Activités liées à la fabrication et aux communications

é lectroniques

Exemple de mesure de température et illustration de certaines possibilités du logiciel :  dans cette capture la température est de 27,8°C ;  la trame est interprétée,  le flux de données apparaît également dans la fenêtre de droite,  des marqueurs permettent d'afficher la fréquence moyenne, calculée sur le nombre de périodes sélectionnées (7 en l’occurrence).

Exemple de mesure de l'humidité relative et illustration d'autres possibilités du logiciel :  dans cette capture l'humidité relative est de 26,0%  déclenchement sur une lecture à l'adresse 0x40 (menu Trigger → Edit trigger),  les données sont affichées simultanément en hexadécimal et en binaire, ce

184 Activités liées à la fabrication et aux communications électroniques qui peut être pratique pour vérifier la conversion.

Plus intéressant encore : le logiciel ScanaStudio dispose d'un interpréteur I2C dédié aux capteurs de température et d'humidité tels que le SHT20.

185 Activités liées à la fabrication et aux communications

é lectroniques

Exemple de mesure de température avec le SHT20, pour la configuration du mode d'acquisition (avec interprétation), capture et affichage de la valeur mesurée :

186 Activités liées à la fabrication et aux communications

électroniques

Exemple de mesure de l'humidité relative avec le SHT20 :

187 Activités liées à la fabrication et aux communications

é lectroniques

A noter : cette famille d'analyseurs offre la possibilité de configurer individuellement les sorties parmi plusieurs structures proposées. Elle offre aussi la possibilité de configurer les broches pour qu'elles deviennent génératrices, permettant par exemple à l'analyseur de se comporter comme un circuit I2C maître, ce qui permettrait de dialoguer directement avec le capteur seul, sans la carte Rpi. Ce qui dans notre cas spécifique ne présente pas d'intérêt.

Complément : Exemples de mesures avec l'analyseur Analog Discovery 2

Mesures effectuées avec l'analyseur Analog Discovery 2 Câblage de l'analyseur : liaisons par grippe-fil ou par insertion directe sur picots (comme ici)

Exemple de mesure de température et d'utilisation des possibilités du logiciel :  dans cette capture la température est de 33,9°C,  fenêtre de configuration de l'interpréteur I2C,  déclenchement sur front descendant de SDA,  mesure de la fréquence de l'horloge (environ 64KHz au lieu de 100KHz,

188 Activités liées à la fabrication et aux communications électroniques gestion de l'OS oblige).

Exemple de mesure de l'humidité relative :  dans cette capture l'humidité relative est de 29,0%  déclenchement sur une lecture à l'adresse h40 (menu Protocol → I2C). La valeur des données n'est pas configurée car on ne préjuge pas de la valeur mesurée. A noter : la valeur de l'humidité relative varie assez vite, si l'on fixe une valeur de déclenchement relativement précise, par exemple 32 pour la partie entière, encore faut-il déclencher régulièrement une mesure sur l'interface graphique pour espérer capturer la bonne valeur.  visualisation également de la vitesse des données sur le bus.  la fenêtre "Events" affiche la chronologie des échanges sur le bus.

189 Activités liées à la fabrication et aux communications électroniques E. Mesures sur la liaison UART

Complément : Compléments sur la liaison UART

 Le shield dispose d'une sortie RS232, mais le signal généré par la carte Rpi, et qui pilote le MAX232, est de type UART.  Le niveau de repos de ce signal est un niveau haut de 3,3V. A noter : Le MAX232 ne fonctionne pas correctement sous 3,3V donc sur le shield il est alimenté par le 5V du GPIO, et fournit donc des signaux compatibles avec cette tension. Cependant les broches Tx et Rx du GPIO génèrent et sont censées recevoir des signaux compatibles avec 3,3V. Une solution aurait été d'utiliser un level-shifter (nécessite de rajouter 2 transistors MOS et 4 résistances). Mais il se trouve qu'un MAX232 alimenté sous 5V interprète correctement les signaux 0-3,3V provenant du Tx du GPIO. Si l'inverse est vrai également (les signaux 0-5V issus du MAX232 sont bien interprétés par l'entrée Rx de la carte Rpi) rien ne dit que le BCM283x supporte longtemps cette tension trop élevée. Donc un pont diviseur (2 résistances seulement pour cette solution) a été intercalé, permettant de ramener le potentiel de 5V à 3,3V. Comme les échanges sur UART se font à des fréquences relativement faibles, il n'y a pas de risque de déformation du signal, sinon la solution du level-shifter aurait finalement été retenue.

Méthode Les mesures qui suivent sur la liaison série asynchrone UART ne concernent pas l'afficheur, les mesures peuvent donc être effectuées sur le shield seul. Attention cependant à remettre les écrous après avoir retiré l'afficheur, afin de ne pas les perdre. Les mesures peuvent être effectuées par exemple entre 2 cartes Rpi, ou bien entre la carte Rpi équipée du shield et un PC, c'est cette solution qui est illustrée dans les documents qui suivent. Les éléments nécessaires à cette mise en œuvre seront :

190 Activités liées à la fabrication et aux communications électroniques

 Un ordinateur disposant d'un émulateur de terminal de type Tera Term, PuTTY, RealTerm ou autre. Ce programme sera configuré pour une communication compatible avec celle définie dans l'application : 8 bits, 9600 bps, 1 bit de stop, pas de bit de parité, ni de contrôle de flux.

 Un adaptateur USB RS232 sub DB9

 Un élément de raccordement entre les 2 DB9 mâles. Il existe au moins 3 possibilités : - un adaptateur Null Modem H-H (le plus simple et selon le modèle le moins volumineux), - un câble NULL Modem DB9 femelle-femelle, - 3 fils de liaison (wire jumper) femelle-femelle. Les signaux Rx et Tx devront être croisés, et les 0V reliés. A noter : c'est ce qu'il y a de moins cher, mais il faut un peut forcer pour que cela rentre, et ça fait un peu "bidouille".

191 Activités liées à la fabrication et aux communications

é lectroniques

Méthode : Mesures  Relier l'analyseur aux signaux Rx et Tx du BUS ainsi qu'au 0V commun  Configurer l'interpréteur pour qu'il déclenche lors de l'envoie d'une donnée.  Capturer une trame lors d'un échange entre l'application Rpi et l'ordinateur. Interprétation  Identifier les 2 messages dans la trame.  Vérifier la vitesse de transmission.

192 Activités liées à la fabrication et aux communications

électroniques Complément : Mesures attendues

Interface graphique Qt de l'application RS232 : dans cet exemple la carte Rpi envoie "Bonjour", le PC répond "Hello". A noter : il faut répondre assez rapidement sur le clavier du PC pour que l'analyseur puisse capturer les 2 échanges en une fois.

Mesures affectuées avec l'analyseur Logic 4 Saleae Liaisons par grippe-fil ou par insertion directe, comme ici.

Mesures avec SQ100

Configuration du protocole d'une part, et du déclenchement d'autre part :

193 Activités liées à la fabrication et aux communications

é lectroniques

Petite illustration des possibilités d'affichage du flux de données :

194 Activités liées à la fabrication et aux communications

électroniques

195 Activités liées à la fabrication et aux communications électroniques

Mesures avec Analog Discovery 2

Illustration de :  la configuration de la liaison,  des possibilités d'affichage du flux de données,  des mesures possibles (durée, fréquences)

Complément : Modification de l'application

On souhaite afficher sur l'interface graphique les données provenant d'un compteur EDF disposant d'une sortie de téléinformation (documentation (cf. compteur_mono)), compteur associé à une passerelle (documentation (cf. nu- infra-telinfo-4-voies-c-2015-02)) effectuant une démodulation et une adaptation au format RS232.  Effectuer les adaptations nécessaires sur l'application.  Associer le compteur et la passerelle puis les raccorder à la carte Rpi, ou bien (si vous ne disposez pas du matériel), simuler depuis l'émulateur de

196 Activités liées à la fabrication et aux communications électroniques terminal le flux de données issues de l'association "compteur + passerelle".  Visualiser et analyser les données transmises.

Complément : Réponses attendues  Dans le cas du compteur EDF avec sortie de téléinformation, le flux de données est unidirectionnel, donc sur l'application Qt la fenêtre de saisie des données peut être éventuellement supprimée.  Le format de transmission est : - 1200bits/sec, - 7 bits de données, - 1 bit de parité paire, - 1 bit de stop. Ces modifications doivent être prises en compte dans la méthode "void CIhmAppRs232c::on_pbOuvrir_clicked()" du fichier cihmapprs232c.cpp - m_rs->initialiser(QSerialPort::Baud1200, - QSerialPort::Data7, - QSerialPort::EvenParity, - QSerialPort::OneStop, - QSerialPort::NoFlowControl); Visualisation des données :

197 Activités liées à la fabrication et aux communications

é lectroniques

Les paramètres de l'analyseur logique doivent aussi prendre en compte ces nouvelles valeurs :

198 Activités liées à la fabrication et aux communications

électroniques

A noter : un simple optocoupleur SFH620 associé à 2 résistances (voir ci-dessous avec un shield "maison" dédié) peut permettre de raccorder directement une sortie de téléinformation à l'entrée Rx d'une carte Rpi, sans passer par une liaison RS232. Dans ce cas l'application Qt appRs232c (la version disposant des modifications effectuées précédemment) peut-être utilisée telle quelle.

199 Activités liées à la fabrication et aux communications

é lectroniques

200

Solution des

exercices

> Solution n°1 (exercice p. 39)

Effacement de la zone de texte En mode Design :  Cliquez sur le bouton Edit Signals/Slots (ou touche F4)  Cliquez sur le bouton souris en maintenant appuyé et déplacez-vous sur le zone de texte. Une flèche apparaît ainsi qu’une fenêtre.  Choisissez le signal souhaité du bouton (clicked()) et le slot existant de l'objet (clear())  Cliquez sur le bouton Edit Widgets (F3).

Signal/slot graphique

201 Solution des exercices

Ajouter du texte dans la zone dédiée  Commencez par cliquez droit sur le bouton et choisissez dans le menu contextuel Go to slot. Cela va automatiquement créer un slot de réaction au signal clicked() pour le bouton  Dans le code source du slot on_pbAjouter_clicked() créé, ajoutez la ligne de code suivante : ui->teTexte->append(ui->leTexte->text());

Qu'avons-nous écrit ? Nous avons invoqué la méthode append() de l'objet leTexte (zone de texte) et lui avons fourni en paramètre le texte à ajouter issu de la ligne de texte (accesseur text() de l'objet leTexte, la ligne de texte).

Rendre impossible l'ajout de texte dans la zone dédiée  Il suffit de cliquer sur la propriété readOnly de l'objet teTexte.

> Solution n°2 (exercice p. 44)

Fichier capptimer.h Par défaut, la classe Mainwindow gère la fenêtre d'IHM principale. Nous la renommons pour l'appeler CAppTimer. Ce n'est pas obligatoire.

1 #ifndef CAPPTIMER_H 2 #define CAPPTIMER_H 3 4 #include 5 #include 6 7 namespace Ui { 8 class CAppTimer; 9 } 10 11 class CAppTimer : public QMainWindow 12 { 13 Q_OBJECT 14 15 public: 16 explicit CAppTimer(QWidget *parent = 0); 17 ~CAppTimer(); 18 19 private: 20 Ui::CAppTimer *ui; 21 QTimer *timer; 22 double cpt; 23 24 private slots: 25 void onTimeout(); 26 void on_pbSS_clicked(); 27 }; 28 29 #endif // CAPPTIMER_H 30

Le fichier capptimer.cpp

1 #include "capptimer.h" 2 #include "ui_capptimer.h"

202 Solution des exercices

3 4 CAppTimer::CAppTimer(QWidget *parent) : 5 QMainWindow(parent), 6 ui(new Ui::CAppTimer) 7 { 8 ui->setupUi(this); 9 timer = new QTimer(this); 10 timer->setInterval(1000); 11 connect(timer, SIGNAL(timeout()), this, SLOT(onTimeout())); 12 cpt = 0; 13 } 14 15 CAppTimer::~CAppTimer() 16 { 17 delete timer; 18 delete ui; 19 } 20 21 void CAppTimer::onTimeout() 22 { 23 cpt++; 24 ui->lcdNumber->display(cpt); 25 } 26 27 void CAppTimer::on_pbSS_clicked() 28 { 29 if (ui->pbSS->text() == "Start") { 30 timer->start(ui->leInterval->text().toInt()); 31 ui->leInterval->setEnabled(false); 32 ui->pbSS->setText("Stop"); 33 } else { 34 timer->stop(); 35 ui->leInterval->setEnabled(true); 36 ui->pbSS->setText("Start"); 37 } // else 38 } 39

> Solution n°3 (exercice p. 51)

Fichier cihmappgpio.h

1 #ifndef CIHMAPPGPIO_H 2 #define CIHMAPPGPIO_H 3 4 #include 5 #include 6 #include "cgpio.h" 7 8 namespace Ui { 9 class CIhmAppGpio; 10 } 11 12 class CIhmAppGpio : public QMainWindow 13 { 14 Q_OBJECT 15 16 public: 17 explicit CIhmAppGpio(QWidget *parent = 0); 18 ~CIhmAppGpio(); 19 20 private slots: 21 void on_pbSS_clicked(); 22 void on_pbActiver_clicked(); 23 void onErreurGpio(QString str); 24

203 Solution des exercices

25 private: 26 Ui::CIhmAppGpio *ui; 27 CGpio *mGpio; 28 }; 29 30 #endif // CIHMAPPGPIO_H 31

Le fichier cihmappgpio.cpp

1 #include "cihmappgpio.h" 2 #include "ui_cihmappgpio.h" 3 4 CIhmAppGpio::CIhmAppGpio(QWidget *parent) : 5 QMainWindow(parent), 6 ui(new Ui::CIhmAppGpio) 7 { 8 mGpio=NULL; 9 ui->setupUi(this); 10 ui->cbGpio->addItem("4"); 11 ui->cbGpio->addItem("5"); 12 ui->cbGpio->addItem("6"); 13 ui->cbGpio->addItem("12"); 14 ui->cbGpio->addItem("13"); 15 ui->cbGpio->addItem("16"); 16 ui->cbGpio->addItem("17"); 17 ui->cbGpio->addItem("18"); 18 ui->cbGpio->addItem("19"); 19 ui->cbGpio->addItem("20"); 20 ui->cbGpio->addItem("21"); 21 ui->cbGpio->addItem("22"); 22 ui->cbGpio->addItem("23"); 23 ui->cbGpio->addItem("24"); 24 ui->cbGpio->addItem("25"); 25 ui->cbGpio->addItem("26"); 26 ui->cbGpio->addItem("27"); 27 ui->cbGpio->setCurrentIndex(16); 28 } 29 30 CIhmAppGpio::~CIhmAppGpio() 31 { 32 delete mGpio; 33 delete ui; 34 } 35 36 void CIhmAppGpio::on_pbSS_clicked() 37 { 38 if (ui->pbSS->text() == "Allumer") { 39 mGpio->ecrire(1); 40 ui->pbSS->setText("Eteindre"); 41 ui->laImage->setText("LED allumée"); 42 } else { 43 mGpio->ecrire(0); 44 ui->pbSS->setText("Allumer"); 45 ui->laImage->setText("LED éteinte"); 46 } // else 47 } 48 49 void CIhmAppGpio::on_pbActiver_clicked() 50 { 51 if (mGpio != NULL) { 52 delete mGpio; 53 } // if mGpio 54 mGpio = new CGpio(ui->cbGpio->currentText().toInt(), OUT); 55 connect(mGpio, SIGNAL(erreur(QString)), this, SLOT(onErreurGpio(QString))); 56 ui->pbActiver->setEnabled(false);

204 Solution des exercices

57 } 58 59 void CIhmAppGpio::onErreurGpio(QString str) 60 { 61 QMessageBox msgBox; 62 msgBox.setInformativeText(str); 63 msgBox.exec(); 64 } 65

> Solution n°4 (exercice p. 57)

Sources de l'application exemple Cette solution n'utilise qu'un QTextEdit pour l'affichage. Le fichier du projet apptc72.pro :

1 #------2 # 3 # Project created by QtCreator 2017-12-04T20:28:55 4 # 5 #------6 7 QT += core gui 8 9 greaterThan(QT_MAJOR_VERSION, 4): QT += widgets 10 11 TARGET = appTC72 12 TEMPLATE = app 13 14 # The following define makes your compiler emit warnings if you use 15 # any feature of Qt which as been marked as deprecated (the exact warnings 16 # depend on your compiler). Please consult the documentation of the 17 # deprecated API in order to know how to port your code away from it. 18 DEFINES += QT_DEPRECATED_WARNINGS 19 20 # You can also make your code fail to compile if you use deprecated APIs. 21 # In order to do so, uncomment the following line. 22 # You can also select to disable deprecated APIs only up to a certain version of Qt. 23 #DEFINES += QT_DISABLE_DEPRECATED_BEFORE=0x060000 # disables all the APIs deprecated before Qt 6.0.0 24 25 26 SOURCES += main.cpp\ 27 cihmapptc72.cpp \ 28 ../biblis/cspi.cpp \ 29 ccapteur_spi_tc72_nth.cpp 30 31 HEADERS += cihmapptc72.h \ 32 ../biblis/cspi.h \ 33 ccapteur_spi_tc72_nth.h 34 35 FORMS += cihmapptc72.ui Le fichier de description de l'IHM cihmapptc72.ui

1 2 3 CIhmAppTC72 4 5 6 7 0

205 Solution des exercices

8 0 9 256 10 315 11 12 13 14 CIhmAppTC72 15 16 17 18 19 20 10 21 10 22 121 23 29 24 25 26 27 id Fabricant 28 29 30 31 32 33 10 34 50 35 231 36 181 37 38 39 40 41 42 43 140 44 10 45 101 46 29 47 48 49 50 Lire Mesure 51 52 53 54 55 56 57 0 58 0 59 256 60 26 61 62 63 64 65 66 TopToolBarArea 67 68 69 false 70 71 72 73 74 75

206 Solution des exercices

76 77 Les fichiers décrivant la classe de gestion du capteur TC72 : ccapteur_spi_tc72_nth.h et ccapteur_spi_tc72_nth.cpp

1 #ifndef CCAPTEUR_SPI_TC72_NTH_H 2 #define CCAPTEUR_SPI_TC72_NTH_H 3 4 #include 5 #include 6 7 #include "/home/pi/devQt/biblis/cspi.h" 8 9 #define ADRESSE 0x80 10 #define REG_CTRL 0x00 11 #define REG_LSB 0x01 12 #define REG_MSB 0x02 13 #define REG_ID 0x03 // contient 0x54 14 #define W 0x80 15 #define RESET 0xfe 16 17 typedef enum { 18 CONTINUOUS=0, 19 ONESHOT=0x11 20 } T_ETAT; 21 22 class CCapteur_Spi_TC72_NTh : public QObject 23 { 24 Q_OBJECT 25 26 public: 27 explicit CCapteur_Spi_TC72_NTh(QObject *parent = 0, int ce = 0); 28 ~CCapteur_Spi_TC72_NTh(); 29 quint8 getManufacturer(); 30 float getTemperature(); 31 32 private: 33 CSpi *m_spi; 34 int m_ce; 35 T_ETAT m_etat; // état du capteur 36 37 int setMode(T_ETAT etat); // continous ou oneshot 38 int reset(); 39 quint8 getControleRegister(); 40 int setControleRegister(quint8 val); 41 42 signals: 43 void sigErreur(QString mess); 44 45 private slots: 46 void onErreur(QString mess); 47 48 }; 49 50 #endif // CCAPTEURTEMPSPI_NTH_H

1 #include "ccapteur_spi_tc72_nth.h" 2 3 CCapteur_Spi_TC72_NTh::CCapteur_Spi_TC72_NTh(QObject *parent, int ce) : 4 QObject(parent) 5 { 6 m_ce = ce; 7 m_etat = CONTINUOUS; // eco d'énergie 8 9 m_spi = new CSpi(this, m_ce, 5000000, true, SPI_MODE_1);

207 Solution des exercices

10 connect(m_spi, SIGNAL(sigErreur(QString)), this, SLOT(onErreur(QString))); 11 reset(); // reset soft du capteur 12 setMode(m_etat); // mode eco par défaut 13 qDebug() << "Objet CCapteur_Spi_TC72_NTh créé !"; 14 15 } 16 17 CCapteur_Spi_TC72_NTh::~CCapteur_Spi_TC72_NTh() 18 { 19 delete m_spi; 20 qDebug() << "Objet CCapteur_Spi_TC72_NTh détruit !"; 21 } 22 23 int CCapteur_Spi_TC72_NTh::setMode(T_ETAT etat) 24 { 25 quint8 trame[2]; 26 m_etat = etat; 27 trame[0]=REG_CTRL|W; // mode écriture 28 trame[1]=etat; 29 int nb=m_spi->ecrireNOctets(trame,2); 30 if (nb != 2) { 31 emit sigErreur("CCapteur_Spi_TC72_NTh::setMode ERREUR écriture"); 32 } // if nb 33 QThread::msleep(200); 34 return nb; 35 } 36 37 int CCapteur_Spi_TC72_NTh::reset() 38 { 39 quint8 trame[2]; 40 trame[0]=REG_CTRL|W; // mode écriture 41 trame[1]=RESET; 42 int nb=m_spi->ecrireNOctets(trame,2); 43 if (nb != 2) { 44 emit sigErreur("CCapteur_Spi_TC72_NTh::reset ERREUR écriture"); 45 } // if nb 46 QThread::msleep(10); 47 return nb; 48 } 49 50 float CCapteur_Spi_TC72_NTh::getTemperature() 51 { 52 quint8 data[4] = {REG_MSB}; 53 int nb=m_spi->lireEcrire(data,4); // demande lecture 54 if (nb != 4) { 55 emit sigErreur("CCapteur_Spi_TC72_NTh::getTemperature ERREUR écriture"); 56 } // if nb 57 float temp; 58 temp = (float)data[1]; // partie entière 59 if (data[2]&0x80==0x80) temp+=0.50; // précision 1/2 degré 60 if (data[2]&0x40==0x40) temp+=0.25; // précision au 1/4 degré 61 return temp; 62 } 63 64 quint8 CCapteur_Spi_TC72_NTh::getManufacturer() 65 { 66 quint8 data[] = {REG_ID,0}; 67 int nb=m_spi->lireEcrire(data,2); // demande lecture 68 if (nb != 2) { 69 emit sigErreur("CCapteur_Spi_TC72_NTh::getManufacturer ERREUR écriture"); 70 } // if nb 71 // qDebug() << "id=" << QString::number(data[1],16); 72 return data[1]; 73 }

208 Solution des exercices

74 75 quint8 CCapteur_Spi_TC72_NTh::getControleRegister() 76 { 77 quint8 adr[2] = {REG_CTRL}; 78 int nb=m_spi->ecrireNOctets(adr,1); // demande lecture 79 if (nb != 1) { 80 emit sigErreur("CCapteur_Spi_TC72_NTh::getControleRegister ERREUR écriture"); 81 } // if nb 82 //msleep(100); 83 quint8 cr; 84 m_spi->lireNOctets(&cr, 1); 85 if (nb != 1) { 86 emit sigErreur("CCapteur_Spi_TC72_NTh::getControleRegister ERREUR lecture"); 87 } // if nb 88 return cr; 89 } 90 91 int CCapteur_Spi_TC72_NTh::setControleRegister(quint8 val) 92 { 93 quint8 trame[2]; 94 trame[0]=REG_CTRL; 95 trame[1]=val; // val du CR 96 int nb=m_spi->ecrireNOctets(trame,2); 97 if (nb != 2) { 98 emit sigErreur("CCapteur_Spi_TC72_NTh::setControleRegister ERREUR écriture"); 99 } // if nb 100 return nb; 101 } 102 103 void CCapteur_Spi_TC72_NTh::onErreur(QString mess) 104 { 105 emit sigErreur(mess); 106 } Les fichiers décrivant le fonctionnement de l'application et l'IHM cihmapptc72.h cihmapptc72.cpp :

1 #ifndef CIHMAPPTC72_H 2 #define CIHMAPPTC72_H 3 4 #include 5 #include 6 #include "/home/pi/devQt/biblis/cspi.h" 7 #include "ccapteur_spi_tc72_nth.h" 8 9 namespace Ui { 10 class CIhmAppTC72; 11 } 12 13 class CIhmAppTC72 : public QMainWindow 14 { 15 Q_OBJECT 16 17 public: 18 explicit CIhmAppTC72(QWidget *parent = 0); 19 ~CIhmAppTC72(); 20 21 private slots: 22 void on_pbId_clicked(); 23 void on_pbMesure_clicked(); 24 void onErreur(QString mess); 25 26 private: 27 Ui::CIhmAppTC72 *ui; 28 CCapteur_Spi_TC72_NTh *m_tc72;

209 Solution des exercices

29 }; 30 31 #endif // CIHMAPPTC72_H

1 #include "cihmapptc72.h" 2 #include "ui_cihmapptc72.h" 3 4 CIhmAppTC72::CIhmAppTC72(QWidget *parent) : 5 QMainWindow(parent), 6 ui(new Ui::CIhmAppTC72) 7 { 8 ui->setupUi(this); 9 m_tc72 = new CCapteur_Spi_TC72_NTh(this, '0'); 10 connect(m_tc72, SIGNAL(sigErreur(QString)), this, SLOT(onErreur(QString))); 11 } 12 13 CIhmAppTC72::~CIhmAppTC72() 14 { 15 delete m_tc72; 16 delete ui; 17 } 18 19 void CIhmAppTC72::on_pbId_clicked() 20 { 21 quint8 ref; 22 23 ref = m_tc72->getManufacturer(); 24 onErreur("Id fabricant (hex) : "+QString::number(ref,16)); 25 } 26 27 void CIhmAppTC72::on_pbMesure_clicked() 28 { 29 float mesure; 30 31 mesure = m_tc72->getTemperature(); 32 onErreur("Mesure : "+QString::number(mesure,'f',1)+" °C"); 33 } 34 35 void CIhmAppTC72::onErreur(QString mess) 36 { 37 ui->textEdit->append(mess); 38 }

> Solution n°5 (exercice p. 63)

Solution C'est un exemple sous forme de ZIP. Si vous le testez, il faudra inclure la classe CRs232c car elle est située dans un autre dossier (biblis).

appRs232c-master.zip Document 6

> Solution n°6 (exercice p. 72)

Sources de l'application exemple Le fichier de projet appsht20.pro :

1 #------2 #

210 Solution des exercices

3 # Project created by QtCreator 2017-12-20T14:49:44 4 # 5 #------6 7 QT += core gui 8 9 greaterThan(QT_MAJOR_VERSION, 4): QT += widgets 10 11 TARGET = appSHT20 12 TEMPLATE = app 13 14 # The following define makes your compiler emit warnings if you use 15 # any feature of Qt which as been marked as deprecated (the exact warnings 16 # depend on your compiler). Please consult the documentation of the 17 # deprecated API in order to know how to port your code away from it. 18 DEFINES += QT_DEPRECATED_WARNINGS 19 20 # You can also make your code fail to compile if you use deprecated APIs. 21 # In order to do so, uncomment the following line. 22 # You can also select to disable deprecated APIs only up to a certain version of Qt. 23 #DEFINES += QT_DISABLE_DEPRECATED_BEFORE=0x060000 # disables all the APIs deprecated before Qt 6.0.0 24 25 26 SOURCES += main.cpp\ 27 cihmappsht20.cpp \ 28 ../biblis/ci2c.cpp \ 29 ccapteur_i2c_sht20_nth.cpp 30 31 HEADERS += cihmappsht20.h \ 32 ../biblis/ci2c.h \ 33 ccapteur_i2c_sht20_nth.h 34 35 FORMS += cihmappsht20.ui Le fichier décrivant l'interface graphique :

1 2 3 CIhmAppSHT20 4 5 6 7 0 8 0 9 225 10 391 11 12 13 14 CIhmAppSHT20 15 16 17 18 19 20 40 21 10 22 141 23 29 24 25 26 27 Lire Température 28

211 Solution des exercices

29 30 31 32 33 40 34 40 35 141 36 29 37 38 39 40 Lire Humidité 41 42 43 44 45 46 10 47 80 48 201 49 231 50 51 52 53 54 55 56 57 0 58 0 59 225 60 26 61 62 63 64 65 66 TopToolBarArea 67 68 69 false 70 71 72 73 74 75 76 77 Ces fichiers sont disponibles dans le dépôt biblis de github. Les fichiers de la classe I2C CI2c.h et CI2c.cpp :

1 #ifndef CI2C_H 2 #define CI2C_H 3 4 #include 5 #include 6 #include 7 #include 8 #include 9 #include //Needed for I2C port 10 #include //Needed for I2C port 11 #include //Needed for I2C port 12 #include //Needed for I2C port 13 #include

212 Solution des exercices

14 15 class CI2c : public QObject 16 { 17 Q_OBJECT 18 19 public: 20 // creation destruction de l'objet 21 static CI2c *getInstance(QObject *parent = 0, char no = '1'); 22 static void freeInstance(); 23 int lire(unsigned char addr, unsigned char *buffer, int lg); 24 int ecrire(unsigned char addr, unsigned char *buffer, int lg); 25 26 private: 27 explicit CI2c(QObject *parent = 0, char noBus = '1'); 28 int init(); 29 QObject *m_parent; 30 QMutex m_mutexI2c; 31 int getNbLink(); 32 int m_addr; // Adresse du composant I2C 33 char m_noBus; // No d'accès au fichier /dev 34 int m_fileI2c; // descripteur du fichier i2C 35 int m_nbLink; 36 static CI2c *m_singleton; 37 38 signals: 39 void sigErreur(QString msg); 40 }; 41 42 #endif // CI2C_H

1 #include "ci2c.h" 2 3 CI2c::CI2c(QObject *parent, char noBus) : 4 QObject(parent) 5 { 6 m_parent = parent; 7 m_noBus = noBus; 8 m_nbLink=0; 9 } // constructeur 10 11 CI2c * CI2c::m_singleton = NULL; 12 13 int CI2c::lire(unsigned char addr, unsigned char *buffer, int lg) 14 { 15 if(ioctl(m_fileI2c, I2C_SLAVE, addr)!=0) { // Règle le driver I2C sur l'adresse. 16 QString mess="CI2c::lire Erreur ioctl acces au bus I2C"; 17 // qDebug() << mess; 18 emit sigErreur(mess); 19 return -1; 20 } // if ioctl 21 bzero(buffer, lg); 22 QMutexLocker lock(&this->m_mutexI2c); // verrouillage du mutex. Il est libéré en sortie de méthode 23 24 int nb=read(m_fileI2c, buffer, lg); 25 // qDebug() << "CI2c:lire: " << buffer[0] << " " << buffer[1] << buffer[2] << " " << buffer[3] << buffer[4] << " " << buffer[5]; 26 return nb; 27 } // lire 28 29 int CI2c::ecrire(unsigned char addr, unsigned char *buffer, int lg) 30 { 31 if(ioctl(m_fileI2c, I2C_SLAVE, addr)!=0) { // Règle le driver I2C sur l'adresse. 32 QString mess="CI2c::ecrire Erreur ioctl acces au bus I2C"; 33 // qDebug() << mess; 34 emit sigErreur(mess);

213 Solution des exercices

35 return -1; 36 } // if ioctl 37 38 QMutexLocker lock(&this->m_mutexI2c); // verrouillage du mutex. Il est libéré en sortie de méthode 39 40 int nb=write(m_fileI2c, buffer, lg); 41 // qDebug() << "CI2c:ecrire: nb=" << nb << " : " << buffer[0] << " " << buffer[1] << buffer[2]; 42 return nb; 43 } // ecrire 44 45 int CI2c::init() 46 { 47 char filename[20]; 48 sprintf(filename, "/dev/i2c-%c",m_noBus); 49 if((m_fileI2c=open(filename, O_RDWR))==-1) { // ouvre le fichier virtuel d'accès à l'I2C 50 QString mess="CI2c::init Erreur ouverture acces au bus I2C"; 51 // qDebug() << mess; 52 emit sigErreur(mess); 53 return -1; 54 } // if open 55 return m_fileI2c; 56 } // init 57 58 int CI2c::getNbLink() 59 { 60 return m_nbLink; 61 } // getNbLink 62 63 CI2c *CI2c::getInstance(QObject *parent, char no) 64 { 65 if (m_singleton == NULL) 66 { 67 qDebug("L'objet CI2c est créé"); 68 m_singleton = new CI2c(parent, no); 69 m_singleton->init(); 70 m_singleton->m_nbLink=1; 71 } 72 else 73 { 74 m_singleton->m_nbLink++; 75 qDebug("singleton est déjà existant"); 76 } 77 return m_singleton; 78 } // getInstance 79 80 void CI2c::freeInstance() 81 { 82 if (m_singleton != NULL) 83 { 84 m_singleton->m_nbLink--; 85 if (m_singleton->m_nbLink==0) { 86 close(m_singleton->m_fileI2c); 87 delete m_singleton; 88 m_singleton = NULL; 89 } // if mNbLink 90 } // if null 91 } // freeInstance Les fichiers de la classe de gestion du capteur SHT20 ccapteur_i2c_sht20_nth.h et ccapteur_i2c_sht20_nth.cpp :

1 #ifndef CCAPTEUR_I2C_SHT20_NTH_H 2 #define CCAPTEUR_I2C_SHT20_NTH_H 3 4 #include

214 Solution des exercices

5 #include 6 #include "/home/pi/devQt/biblis/ci2c.h" 7 8 #define ADR 0x40 // 0x40 + bit LSB à 0 pour write 9 #define COM_MES_TEMP 0xf3 10 #define COM_MES_HUM 0xf5 11 #define COM_RESET 0xfe 12 #define COM_READ_REG 0xe7 13 #define COM_WRITE_REG 0xe6 14 15 class CCapteur_I2c_SHT20_NTh : public QObject 16 { 17 Q_OBJECT 18 19 public: 20 explicit CCapteur_I2c_SHT20_NTh(QObject *parent = 0); 21 ~CCapteur_I2c_SHT20_NTh(); 22 float lireMesureHum(); 23 float lireMesureTemp(); 24 25 private: 26 CI2c *m_i2c; 27 28 signals: 29 void sigErreur(QString mess); 30 31 private slots: 32 void onErreur(QString mess); 33 }; 34 35 #endif // CCAPTEURTEMPHUMI2C_H

1 #include "ccapteur_i2c_sht20_nth.h" 2 3 CCapteur_I2c_SHT20_NTh::CCapteur_I2c_SHT20_NTh(QObject *parent) : 4 QObject(parent) 5 { 6 m_i2c = CI2c::getInstance(this, '1'); 7 connect(m_i2c, SIGNAL(sigErreur(QString)), this, SLOT(onErreur(QString))); 8 qDebug() << "Objet CCapteur_I2c_SHT20_NTh créé !"; 9 } 10 11 CCapteur_I2c_SHT20_NTh::~CCapteur_I2c_SHT20_NTh() 12 { 13 CI2c::freeInstance(); 14 qDebug() << "Objet CCapteur_I2c_SHT20_NTh détruit !"; 15 } 16 17 18 void CCapteur_I2c_SHT20_NTh::onErreur(QString mess) 19 { 20 emit sigErreur(mess); 21 } 22 23 float CCapteur_I2c_SHT20_NTh::lireMesureHum() 24 { 25 float hum; 26 unsigned char lecture[3]; 27 unsigned char ecriture[1]; 28 int res; 29 30 ecriture[0] = COM_MES_HUM; 31 m_i2c->ecrire(ADR, ecriture, 1); 32 usleep(100000); 33 res=m_i2c->lire(ADR, lecture, 2); 34 if (res != 2) { 35 QString mess="CCapteur_I2c_SHT20_NTh::lireMesureHum ERREUR

215 Solution des exercices

Lecture"; 36 emit sigErreur(mess); 37 return -1; 38 } // if res 39 unsigned char MSB = lecture[0]; 40 unsigned char LSB = lecture[1]&0xFC; 41 hum=((MSB<<8)+LSB); 42 hum = -6+125*hum/65536; 43 return hum; 44 } // lireMesHum 45 46 float CCapteur_I2c_SHT20_NTh::lireMesureTemp() 47 { 48 float temp; 49 unsigned char lecture[2]; 50 unsigned char ecriture[1]; 51 int res; 52 53 ecriture[0] = COM_MES_TEMP; 54 m_i2c->ecrire(ADR, ecriture, 1); 55 usleep(100000); 56 res=m_i2c->lire(ADR, lecture, 2); 57 if (res != 2) { 58 QString mess="CCapteur_I2c_SHT20_NTh::lireMesureTemp ERREUR Lecture"; 59 emit sigErreur(mess); 60 return -1; 61 } // if res 62 unsigned char MSB = lecture[0]; 63 unsigned char LSB = lecture[1]&0xF0; 64 temp = ((MSB<<8)+LSB); 65 temp = -46.85+175.72*temp/65536; 66 return temp; 67 } // lire MesTemp Les fichiers de la classe de gestion de l'IHM de l'application :

1 #ifndef CIHMAPPSHT20_H 2 #define CIHMAPPSHT20_H 3 4 #include 5 #include "ccapteur_i2c_sht20_nth.h" 6 7 namespace Ui { 8 class CIhmAppSHT20; 9 } 10 11 class CIhmAppSHT20 : public QMainWindow 12 { 13 Q_OBJECT 14 15 public: 16 explicit CIhmAppSHT20(QWidget *parent = 0); 17 ~CIhmAppSHT20(); 18 19 private slots: 20 void on_pbTemp_clicked(); 21 void on_pbHum_clicked(); 22 void onErreur(QString mess); 23 24 private: 25 Ui::CIhmAppSHT20 *ui; 26 CCapteur_I2c_SHT20_NTh *m_sht20; 27 }; 28 29 #endif // CIHMAPPSHT20_H

1 #include "cihmappsht20.h"

216 Solution des exercices

2 #include "ui_cihmappsht20.h" 3 4 CIhmAppSHT20::CIhmAppSHT20(QWidget *parent) : 5 QMainWindow(parent), 6 ui(new Ui::CIhmAppSHT20) 7 { 8 ui->setupUi(this); 9 m_sht20 = new CCapteur_I2c_SHT20_NTh(this); 10 connect(m_sht20, SIGNAL(sigErreur(QString)), this, SLOT(onErreur(QString))); 11 } 12 13 CIhmAppSHT20::~CIhmAppSHT20() 14 { 15 delete m_sht20; 16 delete ui; 17 } 18 19 void CIhmAppSHT20::on_pbTemp_clicked() 20 { 21 float mesure; 22 mesure = m_sht20->lireMesureTemp(); 23 onErreur("Température : "+QString::number(mesure, 'f',1)+" °C"); 24 } 25 26 void CIhmAppSHT20::on_pbHum_clicked() 27 { 28 float mesure; 29 mesure = m_sht20->lireMesureHum(); 30 onErreur("Humidité : "+QString::number(mesure, 'f',1)+" %RH"); 31 32 } 33 34 void CIhmAppSHT20::onErreur(QString mess) 35 { 36 ui->textEdit->append(mess); 37 }

> Solution n°7 (exercice p. 80)

Le fichier appSgbd.pro

1 #------2 # 3 # Project created by QtCreator 2016-03-17T08:19:26 4 # 5 #------6 7 QT += core gui sql 8 9 greaterThan(QT_MAJOR_VERSION, 4): QT += widgets 10 11 TARGET = essaiQMYSQL 12 TEMPLATE = app 13 14 SOURCES += main.cpp \ 15 cihmappsgbd.cpp 16 17 HEADERS += \ 18 cihmappsgbd.h 19 20 FORMS += \ 21 cihmappsgbd.ui 22

N'oubliez pas d'ajouter le module Qt sql afin d'avoir accès aux classes de gestion

217 Solution des exercices d'un SGBD.

Le fichier cihmappsgbd.h

1 #ifndef MAINWINDOW_H 2 #define MAINWINDOW_H 3 4 #include 5 #include 6 #include 7 #include 8 #include 9 #include 10 #include 11 12 namespace Ui { 13 class CIhmAppSgbd; 14 } 15 16 class CIhmAppSgbd : public QMainWindow 17 { 18 Q_OBJECT 19 20 public: 21 explicit CIhmAppSgbd(QWidget *parent = 0); 22 ~CIhmAppSgbd(); 23 24 private slots: 25 void on_pbExtraire_clicked(); 26 void on_pbAjouter_clicked(); 27 void on_pbEffacer_clicked(); 28 void on_pbModifier_clicked(); 29 void onSelectLineList(QModelIndex mi); 30 31 private: 32 Ui::CIhmAppSgbd *ui; 33 QSqlDatabase db; 34 QStringListModel *model; 35 }; 36 37 #endif // MAINWINDOW_H 38

La méthode onSelectLineList(QModelIndex mi) est un slot invoqué lors du clique sur une ligne du QListView.

Le fichier cihmappsgbd.cpp

1 #include "cihmappsgbd.h" 2 #include "ui_cihmappsgbd.h" 3 4 CIhmAppSgbd::CIhmAppSgbd(QWidget *parent) : 5 QMainWindow(parent), 6 ui(new Ui::CIhmAppSgbd) 7 { 8 QSqlError err; 9 10 // init 11 ui->setupUi(this); 12 ui->lId->setVisible(false); 13 model=NULL; 14 15 // connexion à la bdd 16 db = QSqlDatabase::addDatabase("QMYSQL"); 17 db.setHostName("localhost"); 18 db.setDatabaseName("bddFormation");

218 Solution des exercices

19 db.setUserName("user"); 20 db.setPassword("user"); 21 bool ok = db.open(); 22 if (!ok) { 23 err = db.lastError(); 24 qDebug() << err.text(); 25 } else 26 qDebug() << "Connexion à la BDD " << ok; 27 28 // click dans zone de liste 29 connect(ui->lvTable, SIGNAL(clicked(QModelIndex)), this, SLOT(onSelectLineList(QModelIndex))); 30 } 31 32 CIhmAppSgbd::~CIhmAppSgbd() 33 { 34 delete model; 35 delete ui; 36 } 37 38 void CIhmAppSgbd::on_pbExtraire_clicked() 39 { 40 if (model != NULL) 41 delete model; 42 model = new QStringListModel(this); 43 QStringList liste; 44 QSqlQuery q(db); 45 q.exec("SELECT * FROM user ORDER BY nom"); 46 while (q.next()) { 47 liste << q.value("login").toString(); 48 } // for 49 model->setStringList(liste); 50 ui->lvTable->setModel(model); 51 } 52 53 void CIhmAppSgbd::on_pbAjouter_clicked() 54 { 55 QSqlQuery q(db); 56 q.prepare("INSERT INTO user VALUES (NULL, :name, :pname, :login, SHA1(:mdp))"); 57 q.bindValue(":name", ui->leNom->text()); 58 q.bindValue(":pname", ui->lePnom->text()); 59 q.bindValue(":login", ui->leLogin->text()); 60 q.bindValue(":mdp", ui->leMdp->text()); 61 q.exec(); 62 on_pbExtraire_clicked(); 63 ui->leNom->clear(); 64 ui->lePnom->clear(); 65 ui->leLogin->clear(); 66 ui->leMdp->clear(); 67 } 68 69 void CIhmAppSgbd::on_pbEffacer_clicked() 70 { 71 QSqlQuery q(db); 72 q.prepare("DELETE FROM user WHERE id=:id"); 73 q.bindValue(":id", ui->lId->text()); 74 q.exec(); 75 on_pbExtraire_clicked(); 76 ui->leModifNom->clear(); 77 ui->leModifLogin->clear(); 78 ui->lId->clear(); 79 } 80 81 void CIhmAppSgbd::on_pbModifier_clicked() 82 { 83 QSqlQuery q(db); 84 q.prepare("UPDATE user SET nom=:nom, login=:login WHERE id=:id"); 85 q.bindValue(":id", ui->lId->text().toInt());

219 Solution des exercices

86 q.bindValue(":nom", ui->leModifNom->text()); 87 q.bindValue(":login", ui->leModifLogin->text()); 88 q.exec(); 89 on_pbExtraire_clicked(); 90 } 91 92 void CIhmAppSgbd::onSelectLineList(QModelIndex mi) 93 { 94 QSqlQuery q(db); 95 q.prepare("SELECT * FROM user WHERE login=:login"); 96 q.bindValue(":login", mi.data().toString()); 97 q.exec(); 98 q.next(); 99 ui->leModifLogin->setText(q.value("login").toString()); 100 ui->leModifNom->setText(q.value("nom").toString()); 101 ui->lId->setText(q.value("id").toString()); 102 } 103

Le contenu du QListView est construit de la manière suivante (voir on_pbExtraire_clicked()) :  Construction d'un QListString, un objet gérant une liste de chaine de caractères.  Construction d'un QStringListModel, un objet gérant cette liste, mais compatible avec la QListView.  Enfin la méthode setModel() permettant d'intégrer la liste dans le widget d'affichage.

Le fichier cihmappsgbd.ui

1 2 3 CIhmAppSgbd 4 5 6 7 0 8 0 9 1352 10 813 11 12 13 14 appSgbd 15 16 17 18 19 20 20 21 90 22 551 23 421 24 25 26 27 28 29 30 20 31 30 32 541 33 48 34

220 Solution des exercices

35 36 37 Extraire les utilisateurs 38 39 40 41 42 43 650 44 30 45 651 46 421 47 48 49 50 Ajouter 51 52 53 54 55 290 56 60 57 348 58 211 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 QLineEdit::Password 75 76 77 78 79 80 81 82 83 120 84 330 85 391 86 48 87 88 89 90 Ajouter un utilisateur 91 92 93 94 95 96 20 97 60 98 250 99 211 100 101 102

221 Solution des exercices

103 104 105 106 Nom : 107 108 109 110 111 112 113 Prénom : 114 115 116 117 118 119 120 Login : 121 122 123 124 125 126 127 Mot de passe : 128 129 130 131 132 133 134 135 136 137 20 138 520 139 1021 140 181 141 142 143 144 Modifier 145 146 147 148 149 700 150 140 151 161 152 39 153 154 155 156 157 158 159 160 161 162 30 163 60 164 160 165 101 166 167 168 169 170

222 Solution des exercices

171 172 Nom : 173 174 175 176 177 178 179 Login : 180 181 182 183 184 185 186 187 188 210 189 60 190 348 191 103 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 580 207 80 208 411 209 50 210 211 212 213 214 215 216 Effacer 217 218 219 220 221 222 223 Modifier 224 225 226 227 228 229 230 231 232 233 234 0 235 0 236 1352 237 44 238

223 Solution des exercices

239 240 241 242 243 TopToolBarArea 244 245 246 false 247 248 249 250 251 252 253 254 255

> Solution n°8 (exercice p. 87)

Le fichier CServeurTcp.h Il s'agit de la définition de la classe de gestion du serveur TCP. CServeurTcp s'occupe :  D'initialiser/détruire le serveur TCP sur le port choisi.  Se met à l'écoute des clients.  Mémorise chaque client connecté.  Communique avec la classe IHM (CIhmAppServeurTcp) par des signaux/slots.

1 #ifndef CSERVEURTCP_H 2 #define CSERVEURTCP_H 3 4 #include 5 #include 6 #include 7 8 #define PORTPARDEFAUT 2222 9 10 class CServeurTcp : public QTcpServer 11 { 12 Q_OBJECT 13 public: 14 explicit CServeurTcp(QObject *parent = 0); 15 explicit CServeurTcp(QObject *parent = 0, quint16 noPort = 2222); 16 ~CServeurTcp(); 17 int emettreVersClients(QString mess); 18 19 signals: 20 void sigEvenementServeur(QString eve); 21 void sigErreur(QAbstractSocket::SocketError err); 22 void sigDataClient(QString adrIpClient, QString data); 23 void sigAdrClient(QString adrClient); 24 void sigMaJClients(QList liste); 25 26 public slots: 27 void onNewConnectionClient(); 28 void onDisconnectedClient(); 29 void onErreurReseau(QAbstractSocket::SocketError err); 30 void onReadyReadClient(); 31 32 private: 33 int init();

224 Solution des exercices

34 quint16 m_noPort; 35 QList listeClients; 36 }; 37 38 #endif // CSERVEURTCP_H Vous notez qu'il y a 2 constructeurs, dont 1 paramétré. Par défaut, c'est le port PORTPARDEFAUT qui sera utilisé. 5 signaux sont gérés :  sigEvenementServeur : Pour informer l'IHM d'une connexion/déconnexion d'un client.  sigDataClient : Pour envoyer les données reçues vers l'IHM.  sigErreur : Pour signaler une erreur réseau.  sigAdrClient : Pour envoyer l'adresse IP du client connecté.  sigMaJClients : Pour envoyer la liste des clients connectés au serveur. 4 slots sont gérés :  onNewConnectionClient() : S'exécute lors de la connexion d'un nouveau client.  onDisconnectedClient() : S'exécute lors de la déconnexion d'un client.  onErreurReseau() : S'exécute en cas d'erreur réseau.  onReadyReadClient() : S'exécute lorsqu'un client émet des caractères.

Le fichier CServeurTcp.cpp Il contient la définition des méthodes de la classe CServeurTcp.

1 #include "cserveurtcp.h" 2 3 CServeurTcp::CServeurTcp(QObject *parent) : 4 QTcpServer(parent) 5 { 6 m_noPort = PORTPARDEFAUT; 7 init(); 8 } 9 10 CServeurTcp::CServeurTcp(QObject *parent, quint16 noPort) : 11 QTcpServer(parent) 12 { 13 m_noPort = noPort; 14 init(); 15 } 16 17 CServeurTcp::~CServeurTcp() 18 { 19 // destruction optionnelle car déjà pris en charge par le serveur 20 for (int i=0 ; iclose(); 22 delete listeClients.at(i); 23 } // for i 24 } 25 26 int CServeurTcp::emettreVersClients(QString mess) 27 { 28 for (int i=0 ; iwrite(mess.toStdString().c_str()); 30 qDebug() << "Envoi vers " << listeClients.at(i); 31 } // for i 32 return 1; 33 } 34 35 int CServeurTcp::init()

225 Solution des exercices

36 { 37 listen(QHostAddress::Any, m_noPort); 38 connect(this,SIGNAL(newConnection()), this, SLOT(onNewConnectionClient())); 39 connect(this, SIGNAL(acceptError(QAbstractSocket::SocketError)), this, SLOT(onErreurReseau(QAbstractSocket::SocketError))); 40 return 1; 41 } 42 43 /////////////////// SLOTs //////////////////////// 44 45 void CServeurTcp::onNewConnectionClient() 46 { 47 QString mess="Un client vient de se connecter"; 48 qDebug() << mess; 49 QTcpSocket *newClient = this->nextPendingConnection(); 50 qDebug() << "Nouvelle connexion : " << newClient; 51 if (newClient == NULL) 52 emit sigErreur(QAbstractSocket::ConnectionRefusedError); 53 connect(newClient, SIGNAL(readyRead()), this, SLOT(onReadyReadClient())); 54 connect(newClient, SIGNAL(disconnected()), this, SLOT(onDisconnectedClient())); 55 listeClients.append(newClient); // sauve l'adresse de l'objet dans la liste 56 emit sigEvenementServeur("CON"); 57 emit sigAdrClient(newClient->localAddress().toString()); 58 emit sigMaJClients(listeClients); // pour IHM 59 } 60 61 void CServeurTcp::onDisconnectedClient() 62 { 63 QTcpSocket *client = (QTcpSocket *)sender(); // Déterminer quel client ? 64 emit sigEvenementServeur("DEC"); 65 listeClients.removeOne(client); 66 delete client; 67 emit sigMaJClients(listeClients); 68 } 69 70 void CServeurTcp::onErreurReseau(QAbstractSocket::SocketError err) 71 { 72 qDebug() << "Erreur réseau !"; 73 emit sigErreur(err); 74 } 75 76 void CServeurTcp::onReadyReadClient() 77 { 78 QByteArray ba; 79 // Déterminer quel client ? 80 QTcpSocket *client = (QTcpSocket *)sender(); 81 ba=client->readAll(); 82 qDebug() << "Client : " << client << ba.size() << " Caractères reçus."; 83 emit sigDataClient(client->localAddress().toString(), QString(ba)); 84 } 85 86

 Les constructeurs appellent tous deux la méthode privée init(). Cette méthode initialise le serveur et le prépare à gérer une connexion avec un client.  Le destructeur supprime tous les objets clients connecté avant que le serveur ne se termine. Cette action est optionnelle car la documentation signale que l'objet QServeurTcp est conçu pour supprimer automatiquement les objets qu'il a créé. Cependant, la documentation conseille malgré tout de le faire ! ! !

226 Solution des exercices  emettreVersClients() Permet d'émettre une information à partir du serveur vers tous les clients connectés.  onNewConnectionClient() mémorise l'adresse de l'objet du nouveau client connecté et la stocke dans une liste dynamique (QList). La méthode envoie ensuite les signaux pour signifier la connexion, l'adresse IP du client, la liste mise à jour de tous les clients connectés.  onDisconnectedClient() gère la déconnexion d'un client en le supprimant, en mettant à jour la liste des clients connectés. La méthode sender() permet d'obtenir l'adresse de l'objet ayant déclenché le slot.  onErreurReseau() ne fait qu'émettre le signal vers l'objet appelant (IHM).  onReadyReadClient() est exécuté lorsqu'un client envoi des octets au serveur. La méthode sender() est encore utilisée pour savoir quel client a émis l'information. Après lecture des informations reçues, le signal sigDataClient() est émis vers l'IHM.

Le fichier CIhmAppServeurTcp.h La classe CIhmAppServeurTcp permet de gérer l'IHM de notre application serveur.

1 #ifndef CIHMAPPSERVEURTCP_H 2 #define CIHMAPPSERVEURTCP_H 3 4 #include 5 #include 6 #include 7 #include 8 #include "cserveurtcp.h" 9 10 #define PORT 2222 11 12 namespace Ui { 13 class CIhmAppServeurTcp; 14 } 15 16 class CIhmAppServeurTcp : public QMainWindow 17 { 18 Q_OBJECT 19 20 public: 21 explicit CIhmAppServeurTcp(QWidget *parent = 0); 22 ~CIhmAppServeurTcp(); 23 24 private slots: 25 void on_pbEnvoyer_clicked(); 26 void onEvenementServeur(QString eve); 27 void onDataRecu(QString adrIpClient, QString data); 28 void onErreurServeur(QAbstractSocket::SocketError err); 29 void onAdrClient(QString adrClient); 30 void onListeMaJClients(QList liste); 31 32 private: 33 Ui::CIhmAppServeurTcp *ui; 34 CServeurTcp *serv; 35 }; 36 37 #endif // CIHMAPPSERVEURTCP_H 38

 on_pbEnvoyer_clicked() : Slot permettant l'envoi d'informations vers tous les clients connectés.  onEvenementServeur() : Slot permettant l'affichage d'états du serveur TCP (connexion/déconnexion).  onDataRecu() : Slot permettant l'affichage des informations reçues d'un

227 Solution des exercices client.  onErreurServeur() : Slot permettant d'afficher le texte d'une erreur de l'application.  onAdrClient() : Slot permettant l'affichage de l'adresse du client nouvellement connecté.  onListeMaJClients() : Slot mettant à jour la liste des client dans l'IHM.  serv : Pointeur vers le serveur TCP de l'application.

Le fichier CIhmAppServeurTcp.cpp Définition des méthodes de la classe CIhmAppServeurTcp.

1 #include "cihmappserveurtcp.h" 2 #include "ui_cihmappserveurtcp.h" 3 4 CIhmAppServeurTcp::CIhmAppServeurTcp(QWidget *parent) : 5 QMainWindow(parent), 6 ui(new Ui::CIhmAppServeurTcp) 7 { 8 ui->setupUi(this); 9 ui->pbEnvoyer->setEnabled(false); 10 11 serv = new CServeurTcp(this, PORT); 12 connect(serv,SIGNAL(sigEvenementServeur(QString)), this, SLOT(onEvenementServeur(QString))); 13 connect(serv, SIGNAL(sigDataClient(QString,QString)), this, SLOT(onDataRecu(QString,QString))); 14 connect(serv, SIGNAL(sigErreur(QAbstractSocket::SocketError)), this, SLOT(onErreurServeur(QAbstractSocket::SocketError))); 15 connect(serv, SIGNAL(sigAdrClient(QString)), this, SLOT(onAdrClient(QString))); 16 connect(serv, SIGNAL(sigMaJClients(QList)), this, SLOT(onListeMaJClients(QList))); 17 } 18 19 CIhmAppServeurTcp::~CIhmAppServeurTcp() 20 { 21 delete serv; 22 delete ui; 23 } 24 25 void CIhmAppServeurTcp::on_pbEnvoyer_clicked() 26 { 27 serv->emettreVersClients(ui->leTexte->text()); 28 } 29 30 void CIhmAppServeurTcp::onEvenementServeur(QString eve) 31 { 32 if (eve=="DEC") { 33 ui->teTexte->append("Déconnexion d'un client."); 34 } 35 if (eve=="CON") { 36 ui->teTexte->append("Connexion d'un client."); 37 ui->pbEnvoyer->setEnabled(true); 38 } 39 } 40 41 void CIhmAppServeurTcp::onDataRecu(QString adrIpClient, QString data) 42 { 43 ui->teTexte->append(adrIpClient+": "+data); 44 } 45 46 void CIhmAppServeurTcp::onErreurServeur(QAbstractSocket::SocketError err) 47 { 48 switch (err) {

228 Solution des exercices

49 case QAbstractSocket::ConnectionRefusedError: 50 ui->teTexte->append("Connexion refusée par le serveur !"); 51 break; 52 case QAbstractSocket::NetworkError: 53 ui->teTexte->append("Coupure de liaison réseau !"); 54 break; 55 default: 56 ui->teTexte->append("Erreur réseau à déterminer !"); 57 break; 58 } // sw 59 } 60 61 void CIhmAppServeurTcp::onAdrClient(QString adrClient) 62 { 63 ui->teTexte->append(adrClient+" est connecté."); 64 } 65 66 void CIhmAppServeurTcp::onListeMaJClients(QList liste) 67 { 68 ui->cbListe->clear(); 69 for (int i=0 ; icbListe->addItem(liste.at(i)->localAddress().toString()+ "->"+QString::number((unsigned long)liste.at(i))); 71 qDebug() << "CIhmAppServeurTcp::onListeMaJClients" 72 << liste.at(i)->localAddress().toString()+ "- >"+QString::number((unsigned long)liste.at(i)); 73 } // for i 74 if (liste.size()==0) 75 ui->pbEnvoyer->setEnabled(false); 76 } 77

 Constructeur : Instanciation du serveur TCP de l'application. Captage des signaux du serveur TCP.  onListeMaJClients() : Met à jour la liste des clients dans le widget QComboBox.  QList : Cette classe permet la gestion d'une liste de n'importe quoi ! C'est pourquoi il faut lui préciser entre <> le type des informations à mémoriser. Dans notre cas, il s'agit d'adresses d'objets clients TCP de type QTcpSocket.

Le fichier CIhmAppServeurTcp.ui Fichier XML contenant la description de l'IHM du serveur.

1 2 3 CIhmAppServeurTcp 4 5 6 7 0 8 0 9 1146 10 1002 11 12 13 14 Serveur TCP de base 15 16 17 18 19 20 20 21 60 22 861

229 Solution des exercices

23 61 24 25 26 27 28 29 30 20 31 220 32 1101 33 671 34 35 36 37 38 39 40 900 41 70 42 215 43 48 44 45 46 47 Envoyer 48 49 50 true 51 52 53 54 55 56 20 57 10 58 781 59 39 60 61 62 63 Ecoute sur le port 2222 64 65 66 67 68 69 570 70 140 71 551 72 51 73 74 75 76 77 78 79 30 80 140 81 541 82 39 83 84 85 86 Liste des clients connectés : 87 88 89 Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter 90

230 Solution des exercices

91 92 93 94 95 96 0 97 0 98 1146 99 44 100 101 102 103 104 105 TopToolBarArea 106 107 108 false 109 110 111 112 113 114 115 116 117

Le fichier CClientTcp.h Cette classe gère un client réseau.

1 #ifndef CCLIENTTCP_H 2 #define CCLIENTTCP_H 3 4 #include 5 #include 6 #include 7 8 class CClientTcp : public QObject 9 { 10 Q_OBJECT 11 public: 12 explicit CClientTcp(QObject *parent = 0); 13 ~CClientTcp(); 14 int emettre(QString mess); 15 int connecter(QString adr, QString port); 16 void deconnecter(); 17 18 signals: 19 void sigData(QString data); 20 void sigErreur(QAbstractSocket::SocketError); 21 void sigEvenement(QString eve); 22 23 public slots: 24 void onConnected(); 25 void onDisconnected(); 26 void onReadyRead(); 27 void onSocketError(QAbstractSocket::SocketError); 28 29 private: 30 QTcpSocket *sock; 31 32 }; 33 34 #endif // CCLIENTTCP_H 35

231 Solution des exercices 3 signaux sont gérés :  sigData() : Pour envoyer les données reçues vers l'IHM.  sigErreur() : Pour signaler une erreur réseau.  sigEvenement() : Pour informer l'IHM d'une connexion/déconnexion d'un client. 4 slots sont gérés :  onConnected() : S'exécute lors de la connexion au serveur.  onDisconnected() : S'exécute lors de la déconnexion du client.  onSocketError() : S'exécute en cas d'erreur réseau.  onReadyRead() : S'exécute lorsque le serveur envoi des informations au client.

Le fichier CClientTcp.cpp

1 #include "cclienttcp.h" 2 3 CClientTcp::CClientTcp(QObject *parent) : 4 QObject(parent) 5 { 6 sock = new QTcpSocket(this); 7 connect(sock, SIGNAL(connected()), this, SLOT(onConnected())); 8 connect(sock, SIGNAL(disconnected()), this, SLOT(onDisconnected())); 9 connect(sock, SIGNAL(readyRead()), this, SLOT(onReadyRead())); 10 connect(sock, SIGNAL(error(QAbstractSocket::SocketError)), this, SLOT(onSocketError(QAbstractSocket::SocketError))); 11 } 12 13 CClientTcp::~CClientTcp() 14 { 15 delete sock; 16 } 17 18 int CClientTcp::emettre(QString mess) 19 { 20 int nb = sock->write(mess.toStdString().c_str()); 21 if (nb == -1) 22 qDebug() << "CClientTcp::emettre Erreur écriture."; 23 return nb; 24 } 25 26 int CClientTcp::connecter(QString adr, QString port) 27 { 28 sock->connectToHost(adr, port.toUShort(), QIODevice::ReadWrite); 29 if (!sock->isOpen()) 30 qDebug() << "CClientTcp::connecter Erreur"; 31 return 1; 32 } 33 34 void CClientTcp::deconnecter() 35 { 36 sock->close(); 37 } 38 39 ///////////// SLOTs ///////////////////////////////////// 40 41 void CClientTcp::onConnected() 42 { 43 qDebug() << "Client connecté."; 44 emit sigEvenement("CON"); // communication avec IHM 45 } 46 47 void CClientTcp::onDisconnected()

232 Solution des exercices

48 { 49 qDebug() << "Client déconnecté."; 50 emit sigEvenement("DEC"); // communication avec IHM 51 } 52 53 void CClientTcp::onReadyRead() 54 { 55 int nb = sock->bytesAvailable(); 56 qDebug() << nb << " octets à lire : "; 57 58 QByteArray data; 59 data = sock->readAll(); 60 qDebug() << "CClientTcp::onReadyRead " << data; 61 emit sigData(QString(data)); // transmission à l'IHM par signal 62 } 63 64 void CClientTcp::onSocketError(QAbstractSocket::SocketError err) 65 { 66 qDebug() << "CClientTcp::onSocketError erreur !"; 67 emit sigErreur(err); 68 } 69

 Le constructeur crée un objet QTcpSocket permettant la gestion d'une communication avec le serveur. Le client TCP est préparé à gérer les signaux émis lors de la connexion/déconnexion au serveur, en cas d'erreur réseau, lorsque des informations sont reçues du serveur.  connecter() : Utilise la méthode ConnectToHost() pour se connecter au serveur.  onReadyRead() : Lit tous les caractères envoyés par le serveur. Le signal sigData() est ensuite émis pour avertir l'IHM de l'arrivée d'informations du serveur.

Le fichier CIhmAppClientTcp.h Classe de gestion de l'IHM de l'application.

1 #ifndef CIHMAPPCLIENTTCP_H 2 #define CIHMAPPCLIENTTCP_H 3 4 #include 5 #include 6 #include "cclienttcp.h" 7 8 namespace Ui { 9 class CIhmAppClientTcp; 10 } 11 12 class CIhmAppClientTcp : public QMainWindow 13 { 14 Q_OBJECT 15 16 public: 17 explicit CIhmAppClientTcp(QWidget *parent = 0); 18 ~CIhmAppClientTcp(); 19 20 private slots: 21 void on_pbConnecter_clicked(); 22 void on_pbEnvoyer_clicked(); 23 void onEvenementClient(QString eve); 24 void onDataClient(QString data); 25 void onErrorClient(QAbstractSocket::SocketError err); 26 27 private: 28 Ui::CIhmAppClientTcp *ui;

233 Solution des exercices

29 CClientTcp *client; 30 }; 31 32 #endif // CIHMAPPCLIENTTCP_H 33

Le fichier CIhmAppClientTcp.cpp

1 #include "cihmappclienttcp.h" 2 #include "ui_cihmappclienttcp.h" 3 4 CIhmAppClientTcp::CIhmAppClientTcp(QWidget *parent) : 5 QMainWindow(parent), 6 ui(new Ui::CIhmAppClientTcp) 7 { 8 ui->setupUi(this); 9 ui->pbConnecter->setEnabled(true); 10 ui->pbEnvoyer->setEnabled(false); 11 12 client = new CClientTcp(this); 13 connect(client, SIGNAL(sigEvenement(QString)), this, SLOT(onEvenementClient(QString))); 14 connect(client, SIGNAL(sigErreur(QAbstractSocket::SocketError)), this, SLOT(onErrorClient(QAbstractSocket::SocketError))); 15 connect(client, SIGNAL(sigData(QString)), this, SLOT(onDataClient(QString))); 16 } 17 18 CIhmAppClientTcp::~CIhmAppClientTcp() 19 { 20 delete client; 21 delete ui; 22 } 23 24 ////////////////////////////////////// SLOTs /////////////////////////////////////////// 25 /// IHM 26 void CIhmAppClientTcp::on_pbConnecter_clicked() 27 { 28 if (ui->pbConnecter->text() == "Connexion") { 29 client->connecter(ui->leAdrServeur->text(), ui- >lePortServeur->text()); 30 ui->pbConnecter->setText("Déconnexion"); 31 } else { 32 client->deconnecter(); 33 ui->pbConnecter->setText("Connexion"); 34 } // else 35 } 36 37 void CIhmAppClientTcp::on_pbEnvoyer_clicked() 38 { 39 client->emettre(ui->leEmission->text()); 40 ui->leEmission->clear(); 41 ui->leEmission->setFocus(); 42 } 43 44 45 /// AUTRES OBJETS 46 void CIhmAppClientTcp::onEvenementClient(QString eve) 47 { 48 if (eve == "CON") { 49 ui->pbEnvoyer->setEnabled(true); 50 ui->teReception->append("Connecté au serveur"); 51 } // CON 52 if (eve=="DEC") { 53 ui->pbEnvoyer->setEnabled(false); 54 ui->teReception->append("Perte de connexion du serveur");

234 Solution des exercices

55 } // DEC 56 } 57 58 void CIhmAppClientTcp::onDataClient(QString data) 59 { 60 ui->teReception->append(data); 61 qDebug() << "CIhmAppClientTcp::onDataClient " << data; 62 } 63 64 void CIhmAppClientTcp::onErrorClient(QAbstractSocket::SocketError err) 65 { 66 switch (err) { 67 case QAbstractSocket::ConnectionRefusedError: 68 ui->teReception->append("Connexion refusée par le serveur !"); 69 break; 70 case QAbstractSocket::NetworkError: 71 ui->teReception->append("Coupure de liaison réseau !"); 72 break; 73 default: 74 ui->teReception->append("Erreur réseau à déterminer !"); 75 break; 76 } // sw 77 } 78

Le fichier CIhmAppClientTcp.ui

1 2 3 CIhmAppClientTcp 4 5 6 7 0 8 0 9 1085 10 764 11 12 13 14 appClientTcp 15 16 17 18 19 20 10 21 20 22 353 23 151 24 25 26 27 28 29 30 Adresse du serveur : 31 32 33 Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter 34 35 36 37 38

235 Solution des exercices

39 40 Port du serveur : 41 42 43 Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter 44 45 46 47 48 49 50 51 52 360 53 20 54 281 55 151 56 57 58 59 60 61 62 999.999.999.999 63 64 65 192.168.1.47 66 67 68 69 70 71 72 99999 73 74 75 2222 76 77 78 79 80 81 82 83 84 670 85 37 86 391 87 121 88 89 90 91 Connexion 92 93 94 95 96 97 20 98 320 99 1041 100 321 101 102 103 104 105 106

236 Solution des exercices

107 20 108 210 109 811 110 71 111 112 113 114 115 116 117 850 118 210 119 211 120 71 121 122 123 124 Envoyer 125 126 127 128 129 130 131 0 132 0 133 1085 134 44 135 136 137 138 139 140 TopToolBarArea 141 142 143 false 144 145 146 147 148 149 150 151 152

> Solution n°9 (exercice p. 95)

La classe CBoutonPoussoir

1 #ifndef CBOUTONPOUSSOIR_H 2 #define CBOUTONPOUSSOIR_H 3 4 #include 5 #include "/home/pi/devQt/biblis/cgpio.h" 6 7 class CBoutonPoussoir : public QThread 8 { 9 Q_OBJECT 10 public: 11 explicit CBoutonPoussoir(QObject *parent = 0, int noGpio = 22); 12 ~CBoutonPoussoir(); 13 bool m_fin; 14 15 private:

237 Solution des exercices

16 CGpio *m_gpio; 17 void run(); 18 int m_valMem; // état mémoire 19 20 signals: 21 void sigEtatBouton(bool etat); 22 void sigErreur(QString mess); 23 24 private slots: 25 void onErreur(QString mess); 26 27 }; 28 29 #endif // CBOUTONPOUSSOIR_H

1 #include "cboutonpoussoir.h" 2 3 CBoutonPoussoir::CBoutonPoussoir(QObject *parent, int noGpio) : 4 QThread(parent) 5 { 6 m_gpio = new CGpio(this, noGpio, IN); 7 connect(m_gpio, SIGNAL(sigErreur(QString)), this, SLOT(onErreur(QString))); 8 m_valMem = false; 9 m_fin=false; 10 qDebug() << "Démarrage de l'objet CBoutonPoussoir"; 11 } 12 13 CBoutonPoussoir::~CBoutonPoussoir() 14 { 15 delete m_gpio; 16 qDebug() << "Objet CBoutonPoussoir détruit !"; 17 } 18 19 void CBoutonPoussoir::run() 20 { 21 int etat; 22 23 while(!m_fin) { 24 etat = m_gpio->lire(); 25 if (etat != m_valMem) { 26 m_valMem = etat; 27 emit sigEtatBouton(m_valMem); 28 // qDebug() << "etat bouton : " << m_valMem; 29 } // if 30 usleep(50000); 31 } // wh 32 } 33 34 void CBoutonPoussoir::onErreur(QString mess) 35 { 36 emit sigErreur(mess); 37 }

La classe hérite de QThread. Elle peut émettre 2 signaux :  sigEtatBouton(bool etat) pour transmettre à l'IHM la valeur de l'état du bouton.  sigErreur(QString mess) pour transmettre une erreur. Dans le constructeur de la classe, on prévoit de connecter le signal sigErreur(QString) de l'objet m_gpio au slot onErreur(QString) afin de faire suivre une éventuelle erreur survenue dans l'objet m_gpio (le signal sigErreur(QString mess) sera émis). La méthode run() lit toutes les 50ms l'état du bouton sur la GPIO (22 par défaut).

238 Solution des exercices Le signal sigEtatBouton(m_valMem) n'est émis que lorsque l'état du bouton change. Je vous laisse réaliser la partie affichage à votre convenance.

> Solution n°10 (exercice p. 98)

La classe CSharedMemory

1 #ifndef CSHAREDMEMORY_H 2 #define CSHAREDMEMORY_H 3 4 #include 5 #include 6 #include "global.h" 7 8 // définition de la mémoire partagée 9 typedef struct { 10 float temp_TC72; 11 float temp_SHT20; 12 float hum_SHT20; 13 } T_SHM_DATA; 14 15 class CSharedMemory : public QSharedMemory 16 { 17 Q_OBJECT 18 19 public: 20 explicit CSharedMemory(QObject *parent = 0, int size = 10); 21 ~CSharedMemory(); 22 int attacherOuCreer(); 23 int attacherSeulement(); 24 int ecrire(int no, float mesure); 25 float lire(int no); 26 27 private: 28 int m_taille; 29 float *m_adrBase; 30 QObject *m_parent; 31 32 signals: 33 void sigErreur(QString mess); 34 35 public slots: 36 37 }; 38 39 #endif // CSHAREDMEMORY_H

1 #include "csharedmemory.h" 2 3 CSharedMemory::CSharedMemory(QObject *parent, int size) : 4 QSharedMemory(parent) 5 { 6 m_parent = parent; 7 setKey(KEY); 8 m_taille = size; 9 m_adrBase = NULL; 10 qDebug() << "Objet CSharedMemory créé par " << m_parent- >thread(); 11 } 12 13 CSharedMemory::~CSharedMemory() 14 { 15 detach(); 16 qDebug() << "Objet CSharedMemory détruit par " << m_parent- >thread();

239 Solution des exercices

17 } 18 19 int CSharedMemory::attacherOuCreer() 20 { 21 int res; 22 23 attach(); // tentative de s'attacher 24 if (!isAttached()) { // si existe pas alors création 25 res = create(m_taille); // on réserve la place 26 if (!res) { 27 QString mess="CSharedMemory::attacherOuCreer Erreur de création de la mémoire partagée."; 28 emit sigErreur(mess); 29 return ERREUR; 30 } // if res 31 } // if isattached 32 attach(); 33 m_adrBase = (float *)data(); 34 return 0; 35 } 36 37 int CSharedMemory::attacherSeulement() 38 { 39 attach(); // tentative de s'attacher 40 if (!isAttached()) { // si existe pas 41 QString mess="CSharedMemory::attacherSeulement Erreur de création de la mémoire partagée."; 42 emit sigErreur(mess); 43 return ERREUR; 44 } // if isattached 45 m_adrBase = (float *)data(); 46 return 0; 47 } 48 49 int CSharedMemory::ecrire(int no, float mesure) 50 { 51 if ( (no<0) && (no>2) ) { 52 QString mess="CSharedMemory::ecrire ERREUR, indice de la mesure incorrecte."; 53 emit sigErreur(mess); 54 return ERREUR; 55 } // if no 56 if (!isAttached()) { // si existe pas 57 QString mess="CSharedMemory::ecrire Erreur mémoire partagée non attachée."; 58 emit sigErreur(mess); 59 return ERREUR; 60 } // if isattached 61 m_adrBase[no] = mesure; 62 return 0; 63 } 64 65 float CSharedMemory::lire(int no) 66 { 67 if ( (no<0) && (no>2) ) { 68 QString mess="CSharedMemory::lire ERREUR, indice de la mesure incorrecte."; 69 emit sigErreur(mess); 70 return ERREUR; 71 } // if no 72 if (!isAttached()) { // si existe pas alors création 73 QString mess="CSharedMemory::lire Erreur mémoire partagée non attachée. "; 74 emit sigErreur(mess); 75 return ERREUR; 76 } // if isattached 77 return m_adrBase[no]; 78 } 79

240 Solution des exercices Exemple avec la classe CCapteur_I2c_SHT20 incorporant la classe CSharedMemory

1 #ifndef CCAPTEUR_I2C_SHT20_H 2 #define CCAPTEUR_I2C_SHT20_H 3 4 #include 5 #include "global.h" 6 #include "csharedmemory.h" 7 #include "/home/pi/devQt/biblis/ci2c.h" 8 9 #define ADR 0x40 // 0x40 + bit LSB à 0 pour write 10 #define COM_MES_TEMP 0xf3 11 #define COM_MES_HUM 0xf5 12 #define COM_RESET 0xfe 13 #define COM_READ_REG 0xe7 14 #define COM_WRITE_REG 0xe6 15 16 class CCapteur_I2c_SHT20 : public QThread 17 { 18 Q_OBJECT 19 20 public: 21 explicit CCapteur_I2c_SHT20(QObject *parent = 0, int noMesBase = 1); 22 ~CCapteur_I2c_SHT20(); 23 bool m_fin; 24 25 private: 26 CSharedMemory *m_shm; 27 CI2c *m_i2c; 28 int m_noMesBase; 29 void run(); // méthode virtuelle à implémenter, contenu du thread 30 float lireMesureHum(); 31 float lireMesureTemp(); 32 signals: 33 void sigErreur(QString mess); 34 35 private slots: 36 void onErreur(QString mess); 37 38 }; 39 40 #endif // CCAPTEURTEMPHUMI2C_H 41

Notez l'attribut de classe m_shm. Il permettra l'instanciation de l'objet CSharedMemory qui rappelons le utilise le mécanisme système de gestion d'un segment de mémoire partagé. Donc, chaque objet CSharedMemory accédera au même segment mémoire pourvu que la clef soit la même.

1 #include "ccapteur_i2c_sht20.h" 2 3 CCapteur_I2c_SHT20::CCapteur_I2c_SHT20(QObject *parent, int noMesBase) : 4 QThread(parent) 5 { 6 m_shm = new CSharedMemory(this); 7 connect(m_shm, SIGNAL(sigErreur(QString)), this, SLOT(onErreur(QString))); 8 m_shm->attacherSeulement(); 9 10 m_i2c = CI2c::getInstance(this, '1'); 11 connect(m_i2c, SIGNAL(sigErreur(QString)), this, SLOT(onErreur(QString))); 12 m_fin=false; 13 m_noMesBase = noMesBase; 14 qDebug() << "Objet CCapteur_I2c_SHT20 créé !";

241 Solution des exercices

15 } 16 17 CCapteur_I2c_SHT20::~CCapteur_I2c_SHT20() 18 { 19 CI2c::freeInstance(); 20 m_shm->detach(); 21 delete m_shm; 22 qDebug() << "Objet CCapteur_I2c_SHT20 détruit !"; 23 24 } 25 26 void CCapteur_I2c_SHT20::run() 27 { 28 float mesureHum, mesureTemp; 29 30 while(!m_fin) { 31 // écriture de la mesure dans le segment de mémoire partagé 32 mesureHum = lireMesureHum(); 33 usleep(100000); 34 mesureTemp = lireMesureTemp(); 35 m_shm->lock(); // on prend la mémoire partagée 36 m_shm->ecrire(m_noMesBase, mesureTemp); // écriture dans la mémoire partagée 37 m_shm->ecrire(m_noMesBase+1, mesureHum); // écriture dans la mémoire partagée 38 m_shm->unlock(); // on libère la mémmoire partagée 39 sleep(1); // lecture toutes les s 40 } // while 41 } 42 43 void CCapteur_I2c_SHT20::onErreur(QString mess) 44 { 45 emit sigErreur(mess); 46 } 47 48 float CCapteur_I2c_SHT20::lireMesureHum() 49 { 50 float hum; 51 unsigned char lecture[3]; 52 unsigned char ecriture[1]; 53 int res; 54 55 ecriture[0] = COM_MES_HUM; 56 m_i2c->ecrire(ADR, ecriture, 1); 57 usleep(100000); 58 res=m_i2c->lire(ADR, lecture, 2); 59 if (res != 2) { 60 QString mess="CCapteur_I2c_SHT20::lireMesureHum ERREUR Lecture"; 61 emit sigErreur(mess); 62 return -1; 63 } // if res 64 unsigned char MSB = lecture[0]; 65 unsigned char LSB = lecture[1]&0xF0; 66 hum=((MSB<<8)+LSB); 67 hum = -6+125*hum/65536; 68 return hum; 69 } // lireMesHum 70 71 float CCapteur_I2c_SHT20::lireMesureTemp() 72 { 73 float temp; 74 unsigned char lecture[2]; 75 unsigned char ecriture[1]; 76 int res; 77 78 ecriture[0] = COM_MES_TEMP; 79 m_i2c->ecrire(ADR, ecriture, 1); 80 usleep(100000);

242 Solution des exercices

81 res=m_i2c->lire(ADR, lecture, 2); 82 if (res != 2) { 83 QString mess="CCapteur_I2c_SHT20::lireMesureTemp ERREUR Lecture"; 84 emit sigErreur(mess); 85 return -1; 86 } // if res 87 unsigned char MSB = lecture[0]; 88 unsigned char LSB = lecture[1]&0xFC; 89 temp = ((MSB<<8)+LSB); 90 temp = -46.85+175.72*temp/65536; 91 return temp; 92 } // lire MesTemp 93

Je vous laisse le soin de développer l'interface graphique de cette l'application.

243 Solution des exercices

Complément : Quelques explications concernant CCapteur_I2c_SHT20  Cette classe hérite de QThread. La méthode run() est donc un thread autonome.  L'instanciation de la classe CSharedMemory se fait dans le constructeur de CCapteur_I2c_SHT20.  Notez l'utilisation de la méthode CSharedMemory::lock() dans la méthode CCapteur_I2c_SHT20::run(). Elle permet de s'assurer que seul un objet aura au même moment le droit d'accéder à la mémoire partagé. CSharedMemory::unlock()libère le verrou d'accès à la mémoire pour les objets suivants ou en attente.

244 Solution des exercices

Remarque : CSharedMemory Cette classe est spécialisée pour notre application. Pour l'utiliser dans un autre contexte, il sera nécessaire de la modifier pour l'adapter à la structure de données du nouveau contexte.

245

Références

[Sitographie] Documentation Qt 5.614 Tutoriel Qt Creator15

14 - http://doc.qt.io/qt-5.6/classes.html 15 - https://openclassrooms.com/courses/programmez-avec-le-langage-c/compiler-votre-premiere-fenetre-qt

247