Piloter Arduino depuis un PC avec Python et Boa Constructor

Hervé Troadec

Les platines « Arduino » sont, de par leur conception basée sur un « microcontrôleur », destinées à travailler de manière autonome, embarquées sur un système, quel qu’il soit.

Ceci dit, on peut très bien les utiliser comme interface physique sur un ordinateur pour piloter directement des matériels ou récupérer, à fin de traitement, des informations issues de capteurs. Les données, qu’elles soient des commandes ou des informations, transiteront par la connexion USB. C’est de cette façon que sont pilotées les imprimantes 3D, style « RepRap », ou les petites machines « CNC », de gravure de circuit imprimé par exemple.

Pour cela, deux programmes sont nécessaires : le « donneur d’ordre », tournant sur l’ordinateur, et le « pilote », animant le microcontrôleur, qui, comme son nom l’indique, pilotera les matériels en réponse aux ordres reçus.

Le programme animant le microcontrôleur utilisera le « Langage Arduino » tandis que celui tournant sur l’ordinateur (PC) sera écrit, dans ce tutoriel, en « Python ».

Ce tutoriel se propose de vous montrer, à l’aide d’une réalisation très simple, comment réaliser ces deux programmes de façon à les faire interagir.

Commentez

- Titre : Piloter Arduino depuis un PC avec Python et Boa Constructor

- Auteur : Hervé Troadec

- Parution : 19 janvier 2019

- Public visé :

- Licence : Le contenu de cet article est rédigé par Hervé Troadec et est mis à disposition selon les termes de la Licence Creative Commons Attribution - Pas d'Utilisation Commerciale - Partage dans les Mêmes Conditions 3.0 non transposé. Les logos Developpez.com, en-tête, pied de page, css, et look & feel de l'article sont Copyright ® 2013 Developpez.com.

Sommaire

- I. Avertissement

- I-A. Boa Constructor

- I-B. Cartes Arduino

- II. Avant-propos

- III. Prérequis

- III-A. Environnement matériel

- III-B. Environnement logiciel

- IV. Contexte

- V. Introduction

- VI. Le programme Arduino

- VI-A. Structure du sketch Arduino

- VI-B. Utilisation de la LED intégrée

- VI-C. Changement d’état d’une broche

- VI-D. Utilisation de la bibliothèque « Serial »

- VI-D-1. Initialisation

- VI-D-2. Réception des données

- VI-D-3. Traitement du message

- VII. Programme console Python

- VII-A. Structure d’un script Python

- VII-B. Le programme Python

- VII-B-1. Importation des bibliothèques

- VII-B-2. Établissement de la liaison série

- VII-B-2-a. Nommage de l’instance

- VII-B-2-b. Identifiant système du port

- VII-B-2-c. Vitesse de transmission des données.

- VII-B-2-d. Initialisation

- VII-B-2-e. Ouverture du port

- VII-B-3. Un premier script opérationnel

- VII-B-4. Intégration du retour d’informations

- VII-B-5. Un script amélioré

- VIII. Boa Constructor

- VIII-A. RAD Boa Constructor

- VIII-B. Création d’une application

- VIII-C. Réalisation de la maquette

- VIII-C-1. Premiers pas

- VIII-C-2. Utilisation des composants

- VIII-C-2-a. Sélection et personnalisation des composants

- VIII-C-2-b. Les gestionnaires d’évènement

- VIII-C-2-c. Affichage d’une image à partir d’un fichier

- VIII-D. La liaison série

- VIII-E. Traitement du retour d’informations

- VIII-F. Pour aller plus loin

- VIII-F-1. Optimisation du code

- VIII-F-2. Style de la fenêtre

- VIII-F-3. En bonus

- IX. Annexe

- IX-A. Écriture classique du GUI

- IX-B. Autorisation d’accès au port série

- IX-C. Identifiant de port

- X. Conclusion

- XI. Remerciements

I - Avertissement

I-A - Boa Constructor

Comme je l’expliquerai dans la section concernant Boa Constructor, l’avenir de ce RAD est incertain, et il peut sembler peu judicieux, comme on m’en a fait la remarque tout à fait justifiée, de l’incorporer à un tutoriel. J’en suis parfaitement conscient, mais il s’agit, pour moi, d’un choix rédactionnel, destiné à essayer de faire connaître cet environnement de développement. Plus il sera téléchargé, plus il y aura de retours, et plus les développeurs seront motivés pour le faire évoluer.

Gardez cependant bien à l’esprit que ce tutoriel ne dépend pas de BOA Constructor, lequel n’est là que pour réaliser une interface graphique optionnelle, qui est indépendante des mécanismes de pilotage de l’Arduino UNO depuis un script Python.

Toutefois, dans le cas où la réalisation de cette interface graphique serait votre principale motivation pour suivre ce tutoriel mais que l’initiation à Boa Constructor ne vous intéresse pas, ce qui est parfaitement légitime, je joins, au chapitre IX.A.Écriture classique du GUI, un script Python réalisant la même interface que celle réalisée avec Boa Constructor, utilisant également la boite à outils graphique wxPython, mais écrite de manière classique. Comme ce script n’utilise que des bibliothèques parfaitement maintenues, sa pérennité et celle des scripts que vous développerez sur le même modèle est assurée.

I-B - Cartes Arduino

Les cartes Arduino ciblées en premier lieu par ce tutoriel, sont celles qui sont basées sur le microcontrôleur Atmel ATMega328, présent notamment sur la carte Arduino UNO, carte utilisée ici, et fonctionnent correctement en gardant le paramétrage par défaut de la liaison série (bibliothèque pySerial) dans le script Python. J’ai été informé que les cartes basées sur un autre microcontrôleur, comme l’ATmega32u4, peuvent nécessiter un paramétrage différent sous peine de plantage possible du script.

N’ayant que des cartes Arduino UNO à ma disposition, je ne peux pas effectuer les tests de paramétrage avec ces autres cartes, et vous serez donc peut-être contraint à effectuer ces tests vous-même en cas de dysfonctionnement.

II - Avant-propos

Ce tutoriel s’adresse à des néophytes dans l’univers Arduino ou en programmation Python. L’EDI Arduino et l’interpréteur Python étant multiplateformes, de même que le RAD Boa-Constructor et wxPython, il pourra être utilisé dans les environnements courants, moyennant un petit effort d’adaptation.

PRÉCISION : ce n’est pas un cours de programmation. Il faut plutôt voir ce tutoriel comme un mode d’emploi. Je vais toutefois m’efforcer de décrire au moins les instructions utilisées. Vous trouverez sur developpez.com des tutoriels Python et un aperçu de la plateforme Arduino qui vous permettront d’acquérir ou de consolider vos connaissances, sans oublier bien entendu, sur la toile, les incontournables et indispensables Référence Arduino et Documentation Python, en anglais malheureusement.

Ce tutoriel, et c’est le cas pour nombre d’entre eux, n’a aucunement la prétention d’inventer quelque chose. Des recherches sur la toile et sur developpez.com permettraient sans aucun doute de s’en passer. Il offre simplement l’opportunité à ceux qui seront intéressés, outre de gagner du temps, d’aborder trois aspects de la programmation regroupés dans un même article pour une réalisation fonctionnelle, peu coûteuse, simple mais extrapolable à l’envi. Il suffira d’un peu d’imagination (et de travail).

Si je pars du principe que vous êtes néophyte dans l’univers Arduino ou en programmation Python, je pars également du principe que vous connaissez votre système d’exploitation. Je n’entrerai donc pas (ou peu) dans les détails pour les manipulations liées à ce dernier.

III - Prérequis

III-A - Environnement matériel

Côté PC, plusieurs possibilités s’offrent à vous :

- ordinateur de bureau ;

- ordinateur portable ;

- nano-ordinateur (Raspberry Pi, Orange Pi) ;

- et d’une manière générale tout ordinateur tournant, par ordre alphabétique, sous GNU-Linux, macOS© ou Windows©, à base de processeur AMD©, ARM© ou Intel©, également par ordre alphabétique, sur lequel l’EDI (Environnement de Développement Intégré) Arduino et l’interpréteur Python peuvent être installés, et muni d’au moins une prise USB libre.

Côté Arduino, vous avez également le choix :

- Arduino UNO, carte avec laquelle ce tutoriel a été réalisé, et son câble USB ;

- et d’une manière générale toute carte Arduino généraliste, sous réserve d’une éventuelle adaptation du texte. La carte Arduino UNO a été choisie en raison de son utilisation à priori préférentielle par les débutants et son adaptation à une multitude de projets, en raison de son faible coût (on la trouve actuellement pour une dizaine d’euros), et aussi, et surtout, parce que c’est la carte avec laquelle je fais travailler mes neurones.

Voici un exemple d’environnement :

III-B - Environnement logiciel

Outre le système d’exploitation que vous utilisez, vous aurez besoin de :

- l’EDI Arduino ;

- l’interpréteur Python 2.7 ;

- la bibliothèque wxPython 3.0.2 ou supérieure ;

- le RAD Boa‑Constructor 0.6.1 ou Boa‑Constructor 0.7.2.

Nous allons considérer pour ce tutoriel que l’EDI Arduino est installé et opérationnel, et que, sans être forcément à l’aise avec, vous savez suffisamment vous en servir pour écrire et téléverser un sketch (voir le chapitre VI pour plus de précision sur ces termes).

Nous allons également considérer que l’environnement Python, wxPython et Boa Constructor sont installés. Vous devrez disposer d’un éditeur de texte pour saisir le code Python. En ce qui concerne le RAD (Rapide Application Development) Boa Constructor, destiné à élaborer l’interface graphique, l’EDI fourni dispose de son propre éditeur (par définition), et donc le problème ne se pose pas. Pour le script « console », vous pourrez soit utiliser l’éditeur de Boa Constructor, soit :

- si vous êtes sous GNU-Linux, chaque distribution en propose généralement plusieurs, comme Geany, Kate, Gedit, etc. ;

- si vous êtes sous Windows©, vous avez au moins le Bloc-notes, qui n’est pas très sexy comme disent les Anglo-saxons mais qui, vu la simplicité du code que vous allez saisir, peut suffire. Sinon, vous avez l’excellent Notepad++ que j’utilise personnellement toujours avec plaisir quand je suis sous ce système ;

- sous macOS©, vous avez, entre autres, CotEditor.

Vous pouvez bien entendu utiliser l’éditeur IDLE (ou IDLE3 en fonction de votre version de Python) qui est automatiquement installé en même temps que Python, mais que je trouve un peu vieillissant. C’est une opinion personnelle.

Attention à ne pas utiliser de logiciel de traitement de texte comme LibreOffice, pour ne citer que lui. En effet, des informations de formatage sont incorporées au texte, rendant votre code totalement incompréhensible pour un compilateur ou un interpréteur.

IV - Contexte

Ce tutoriel a été réalisé dans les conditions suivantes :

- PC de bureau standard ;

- GNU-Linux Debian Stretch 9.5 ;

- EDI Arduino 1.8.5 ;

- Python 2.7.13 ;

- wxPython 3.0.2 ;

- EDI Boa Constructor 0.6.1.

V - Introduction

Dans ce tutoriel, vous allez allumer et éteindre une LED (Light Emitting Diode ⇔ Diode Électroluminescente). Pas très original me direz-vous et vous aurez parfaitement raison. Mais il va y avoir ceci de différent par rapport à l’utilisation standard d’Arduino UNO, c’est que cet allumage et cette extinction seront commandés non pas avec un bouton depuis une plaque d’expérimentation, ou en réponse aux données fournies par un capteur, mais depuis votre PC (ou équivalent), en utilisant la liaison série (USB).

Ce tutoriel va se dérouler en trois phases :

1. Écriture du code Arduino (généralement appelé sketch) destiné à allumer ou éteindre la LED intégrée à la carte (voir l’encadré ci-dessous) dès la réception d’une commande envoyée depuis le moniteur série fourni par l’EDI. Cette phase doit être la première dans la mesure où il ne sert à rien d’élaborer une chaîne de commande s’il n’y a rien à commander ;

2. Écriture du script « console » Python destiné à remplacer le moniteur série Arduino par une console standard, ce qui permettra, en plus d’offrir d’importantes possibilités de traitement de l’information, ce que ne permet pas le moniteur série Arduino, de se passer de l’EDI Arduino pour commander l’Arduino UNO. Cette phase arrive logiquement en seconde position, car elle permet d’écrire un code simple et facilement lisible, puisque dépouillé de toute gestion graphique, ce qui en facilitera la compréhension et la mise au point. Ce code sera réutilisé, après adaptation, dans la troisième phase ;

3. Création d'une interface graphique en Python grâce à l’utilisation du RAD Boa Constructor pour offrir une meilleure ergonomie et un affichage plus visuel du retour d’informations.

Vous trouverez dans cette archive la version finale des sketchs Arduino et des scripts Python réalisés dans ce tutoriel.

Les cartes Arduino n’ont pas toutes, d’origine, une LED intégrée connectée à une broche. Si vous êtes dans ce cas, vous devrez faire un petit montage et adapter le code. Dans le montage exemple suivant, la broche utilisée est la broche no 9.

VI - Le programme Arduino

Avant tout, quelques remarques en ce qui concerne le vocabulaire Arduino :

- le terme sketch désigne à la fois l’ensemble du code saisi dans l’éditeur (code source), et son résultat compilé destiné à être écrit dans la mémoire flash du microcontrôleur (code objet). Ce terme me semble plus relever du show-biz que de l’informatique, mais c’est le terme consacré, donc nous l’utiliserons. Il pourra m’arriver de parler de programme, de code ou de microcode, cela fera toujours référence au sketch ;

- téléverser un sketch consiste à en lancer la compilation puis, si celle-ci s’est bien déroulée, à en écrire (uploader) le résultat dans la mémoire flash du microcontrôleur équipant la carte, en passant par la liaison USB. Donc, je ne dirai jamais « compilez le sketch et téléversez-le » ;

- je parlerai, le cas échéant, du « langage Arduino ». Ce terme, contesté par certains, est effectivement un peu abusif, car le langage utilisé est le langage C/C++. Toutefois, on peut légitimement considérer que les ajouts spécifiques à la plateforme, notamment concernant la gestion des broches GPIO, et les bibliothèques dédiées, en font un « méta-langage ». De toute façon, cette appellation est déjà tellement répandue qu’il serait un peu vain de vouloir la changer, d’autant qu’il s’agit bien de l’appellation officielle.

VI-A - Structure du sketch Arduino

Voici comment se présente un sketch Arduino :

/* Déclarations - Optionnel

Bibliothèques utilisées à l’aide de la directive #include
exemple : #include <Ethernet.h>

Constantes
exemple : const float pi = 3.14;

Variables
exemple : int var1;  var1 est un nombre entier et n’est pas initialisé
exemple : String chaine1 = "toto";  chaine1 est une variable de type String et est initialisée avec la valeur toto

*/

void setup()  // Obligatoire même vide - Exécuté une seule fois lors de la mise sous tension ou de la réinitialisation
{
// Initialisations
}

void loop()  // Obligatoire même vide - Exécuté en boucle permanente sauf blocage par le programme
{
// Corps du programme
}

// Procédures et fonctions (optionnel)

Les sections void setup() {} et void loop() {} sont obligatoires et constituent le code minimum pour pouvoir compiler sans problème. Ce code ne fait rien à part boucler sans fin, mais il est valide.

VI-B - Utilisation de la LED intégrée

Comme c’est le cas pour beaucoup d’exercices, nous utiliserons la LED (intégrée à la platine) qui est pilotée en parallèle avec la broche 13. Cela permet de ne pas s’encombrer d’un montage, puisqu’ici, le travail se fait au niveau logiciel.

La LED intégrée des cartes Arduino n’est pas toujours raccordée à la même broche. Pour rendre les programmes qui l’utilisent plus universels, une constante a été introduite pour y faire référence, quelle que soit la carte utilisée, et donc sans avoir besoin de connaître la broche concernée. Cette constante s’appelle « LED_BUILTIN ».

On peut s’interroger sur la réelle utilité de cette constante, dans la mesure où, si l’on ne connaît pas la broche concernée, on risque de faire deux fois référence à cette broche dans le programme, une fois en l’utilisant explicitement pour une fonctionnalité précise, et une autre fois en l’invoquant à l’aide de la constante prédéfinie pour une autre fonctionnalité.

De plus, c’est contre-productif dans les exemples destinés aux débutants, comme « Blink », car ça masque la façon dont on référence les broches GPIO avec leur numéro, ce qui est l’utilisation fondamentale de la plateforme.

Pour cette raison, je ferai toujours référence à cette LED de manière explicite.

La broche 13 fait partie des broches GPIO de l’Arduino UNO. GPIO signifie, en anglais, General Purpose Input Output, soit « entrée-sortie multi-usage » en bon « françois ». On en déduit qu’elle peut servir à beaucoup de choses, d’une part, et que d’autre part elle peut servir soit d’entrée, soit de sortie, au choix, ne pouvant faire les deux en même temps.

Vous utiliserez une broche en entrée quand elle servira à récupérer une information d’un capteur, l’état d’un bouton par exemple, et en sortie quand elle devra agir sur un actionneur (au sens large), allumer ou éteindre une LED, par exemple.

Par actionneur, on comprendra ici tout dispositif subissant une modification mécanique ou d’état, en réponse à une modification de l’état d’une broche de l’Arduino.

Dans la mesure ou une broche peut être utilisée soit en entrée, soit en sortie, il faut pouvoir indiquer au programme quelle utilisation on compte en faire. La ligne suivante vous indique la méthode générique :

pinMode(numBroche, mode);

Où numBroche est le numéro de la broche GPIO concernée, allant théoriquement de 0 à 13, et en pratique de 2 à 13, et mode une constante prédéfinie valant INPUT pour configurer la broche en entrée, et OUTPUT pour la configurer en sortie.

Arduino UNO offre 20 broches, numérotées de 0 à 19, pour communiquer avec du matériel. En pratique, on ne considérera comme broches GPIO que les broches 2 à 13, les broches 0 et 1 étant réservées à la liaison série. Les broches 14 à 19, bien qu’utilisables en tant que broches GPIO, seront réservées pour des utilisations analogiques.

Comme il s’agit ici d’alimenter une LED en électricité, la broche sera configurée en sortie. Comme, de plus, il s’agit de la LED intégrée sur une carte Arduino UNO, la broche sera la broche n°13. La ligne de configuration devient :

pinMode(13, OUTPUT);

Le langage « C/C++ » sur lequel est basé le langage Arduino fait la distinction entre les majuscules et les minuscules. Si vous saisissez pinmode ou output, vous aurez une erreur de syntaxe. Soyez vigilant à cet égard. C’est une source d’erreurs assez classique.

Cette ligne de configuration d’une broche doit se trouver dans la partie void setup() du code.

Cette ligne est tout à fait valide, mais n’est pas représentative de la meilleure pratique. En effet, imaginez que votre code fasse référence une dizaine de fois à cette broche, et que, pour utiliser le bus SPI, par exemple, vous vous trouviez dans l’obligation de la libérer et donc d’utiliser une autre broche à la place, vous devrez alors faire une dizaine de modifications dans le code.

La solution réside dans la déclaration d’une constante pour représenter cette broche. L’avantage, outre ce qui vient d’être dit, est que vous pouvez donner un nom représentatif du matériel relié à cette broche. Ici, on ne va pas se compliquer la vie, on va nommer cette constante led.

Voici sa déclaration :

const int led = 13;

Cette ligne doit se trouver dans la zone des déclarations entre la clause include, s’il y en a, et la fonction void setup(), qui, elle, est toujours là.

- const indique que l’identifiant en passe d’être déclaré doit recevoir une valeur, et que cette valeur ne pourra pas être modifiée dans le cours du programme.

- int indique que l’identifiant qui suit représente un entier (integer) et le symbole « = » entraîne l’affectation de la valeur qui le suit à l’identifiant qui le précède.

- N’oubliez pas le symbole « ; » qui doit obligatoirement suivre toute instruction.

À chaque fois que vous aurez besoin d’agir sur la broche 13 avec votre code, vous utiliserez son nom, à savoir led, puisque led est équivalent à 13.

Vous pourrez trouver, si vous avez la curiosité d’étudier des codes de la bibliothèque, la directive #define utilisée en lieu et place de const pour déclarer les constantes.

Je vous déconseille cette pratique, à moins de bien avoir compris le fonctionnement de #define pour éviter les problèmes. Excepté le cas où vous êtes vraiment pris à la gorge par le manque de place, c’est-à-dire que votre programme fait 32 Kio à quelques octets près, taille rarement atteinte par les programmes quand on débute (et même après car c’est toujours risqué de frôler les limites), les quelques octets gagnés ne justifieront pas son utilisation.

Vous pouvez maintenant modifier la ligne de configuration de la broche comme suit :

pinMode(led, OUTPUT);

VI-C - Changement d’état d’une broche

L’état d’une broche GPIO configurée en sortie peut prendre deux valeurs :

- HIGH, ou état haut, dans lequel la broche est maintenue au potentiel de 5 V, dans la limite des possibilités de l’électronique de la carte, soit 40 mA sur une broche pour l’Arduino UNO, ce qui est largement suffisant pour une LED ;

- LOW, ou état bas, dans lequel la broche est maintenue au potentiel de 0 V, dans la limite des possibilités de l’électronique de la carte, soit 40 mA sur une broche pour l’Arduino UNO, ce qui est largement suffisant pour une LED ;

La limite de 40 mA que peut fournir une broche GPIO masque une autre limite : celle du courant maximum que peut fournir le microcontrôleur lui-même, qui est de 200 mA sur une carte Arduino UNO, soit 10 mA par broche, en moyenne. Par ailleurs, veillez à bien connaître les consommations des matériels connectés pour éviter les surcharges, que ce soit au niveau d’une broche, du microcontrôleur ou au niveau de la carte elle-même.

Vous avez donc deux possibilités pour alimenter une LED :

- Soit connecter sa cathode à la masse (donc l’anode à la broche), et, par conséquent, il faudra appliquer un niveau haut sur la broche pour l’allumer ;

- Soit connecter son anode au +5 V (donc la cathode à la broche), et, dans ce cas, il faudra appliquer un niveau bas sur la broche pour l’allumer.

Voici, pour info, le brochage des LED :

En ce qui nous concerne, la LED intégrée étant précâblée avec sa cathode à la masse, nous n’avons pas le choix : son allumage nécessitera d’appliquer un état haut sur la broche 13 et, par voie de conséquence, un état bas pour l’éteindre.

La ligne de commande sera :

digitalWrite(led, HIGH);

ou

digitalWrite(led, LOW);

selon qu’il s’agira d’allumer ou d’éteindre la LED.

La fonction digitalWrite(numLed, état) admet deux paramètres :

- numLed, un entier qui représente le numéro de la broche sur laquelle on souhaite agir ;

- état, qui prend les valeurs HIGH ou LOW selon le résultat que l’on désire obtenir.

Ces lignes se retrouvent en général dans la section void loop() du programme. Toutefois, si l’on souhaite, par exemple, que la LED soit allumée au démarrage du programme, on peut insérer la ligne correspondante dans la section void setup().

VI-D - Utilisation de la bibliothèque « Serial »

Maintenant, nous voulons allumer ou éteindre cette LED en réponse à un message reçu via la liaison série (USB). Ce message sera émis et donc reçu sous forme d’une chaîne de caractères. L’EDI Arduino fournit en standard les protocoles permettant de réaliser ce type de liaison. En effet, contrairement aux autres bibliothèques qui nécessitent un référencement, ou une déclaration, explicite, la bibliothèque Serial est implicitement référencée, et cela que vous l’utilisiez ou non. D’où un encombrement mémoire inutile dans bien des cas, mais bon ! comme ici nous devons l’utiliser…

VI-D-1 - Initialisation

Le fait de n’avoir pas besoin d’inclure la bibliothèque Serial, puisqu’elle l’est d’origine, ne nous exonère pas d’avoir à initialiser la liaison série. Ceci va se passer dans la zone de setup() et les lignes suivantes vont s’en charger :

Serial.begin(115200); // Initialise le port série à 115 200 Bd
Serial.flush(); // On vide le tampon de réception par précaution

Avec les matériels actuels, l’utilisation du port série à 115 200Bd ne pose aucun problème. Il ne faudra pas oublier de répercuter cette valeur sur le moniteur série sous peine d’avoir des dysfonctionnements.

L’instruction destinée à vider le tampon peut être omise ici. C’est normalement sans conséquence. Comme ça ne coûte rien d’être prudent, et à moins de manquer de place mémoire, laissez-la.

VI-D-2 - Réception des données

Deux instructions fournies par la bibliothèque Serial vont nous permettre de réceptionner les données. Celles-ci arrivent sur le port série sous forme d’octets, lesquels sont stockés dans un tampon pour permettre leur récupération à fin de traitement. Le tampon peut stocker jusqu’à 64 octets. Même si cela peut sembler faible au regard des données que nous sommes habitués à manipuler, ça devrait suffire pour le type d’application que nous sommes censés développer sur cette plateforme.

L’instruction :

Serial.read();

renvoie l’octet se trouvant en « sortie » de tampon et le supprime de celui-ci. On comprend tout de suite que faire appel à cette instruction autant de fois qu’il y aura d’octets dans le tampon permettra d’en récupérer la totalité, sous forme de nombres entiers, tout en le vidant.

Imaginez, pour comprendre le processus, un distributeur de gobelets en plastique. Les gobelets sont empilés dans un tube, le « tampon », en partant du haut, et vous les prenez en retirant séquentiellement celui qui se présente à la base.

Si chacun de ces gobelets contient une lettre d’un message et a été empilé dans le bon ordre, en les prélevant par-dessous, vous pouvez reconstituer le message tout en vidant le tube.

Cet octet peut représenter différentes choses. Comme le message reçu est censé représenter une chaîne de caractères, il faudra bien sûr convertir cet octet en caractère pour construire la chaîne. Cette conversion sera assurée par la fonction char(octet) qui retourne le caractère codé par l’octet passé en argument.

Veillez à n’envoyer que des caractères ASCII (codés sur un seul octet) si vous ne voulez pas de problèmes. Les caractères étendus codés sur plusieurs octets peuvent bien sûr être utilisés, mais ils nécessitent un traitement qui dépasse le cadre de ce tutoriel, sans rien y apporter de pertinent.

L’instruction :

Serial.available()

retourne le nombre d’octets présents dans le tampon. Cette instruction va nous permettre d’utiliser l’instruction précédente dans une boucle pour récupérer le contenu exact du tampon.

Voici à quoi va ressembler cette boucle :

while (Serial.available() > 0)
{
reception = Serial.read();
commande += char(reception);
delay(1);
}

La ligne delay(1) n’est pas imposée par le protocole de réception des données. Elle est seulement là pour empêcher qu’une lecture du tampon ait lieu avant que la donnée suivante ait eu le temps d’arriver.

La valeur de ce délai dépend à la fois de la vitesse de la liaison série, que vous avez fixée dans le « setup », et de la durée de la boucle, liée, entre autres, au code exécuté dans cette boucle.

Dans les conditions de réalisation de ce tutoriel, il est possible de descendre le délai à 70 µs, en utilisant la fonction « delayMicroseconds(70); ». En paramétrant la vitesse de la liaison série à 1 000 000 Bd, on peut se passer du délai.

Notez l’usage des accolades ouvrante et fermante { }. Elles servent en C/C++, langage sur lequel est basé le langage Arduino, à délimiter un bloc d’instructions. Ici, par exemple, la boucle while() exécute les trois instructions comprises entre les accolades tant que le critère « il reste un octet dans le tampon » est respecté. S’il n’y avait pas de délimiteur de bloc d’instructions, while() ne porterait que sur l’instruction placée immédiatement après, c’est-à-dire jusqu’au premier point-virgule suivant. Donc, ici, while() portera sur les instructions comprises entre la première accolade ouvrante qui le suit et la première accolade fermante de même niveau. Il peut en effet y avoir d’autres blocs d’instructions à l’intérieur d’un bloc, chacun délimité également par une accolade ouvrante et une accolade fermante de son niveau.

Cette boucle doit impérativement se trouver dans la boucle principale void loop(), qui contient le corps du programme, et qui est parcourue séquentiellement en permanence. Le programme peut provoquer une rupture de séquence pour effectuer un traitement particulier, comme une boucle while() par exemple. Quand ce traitement est terminé, la séquence reprend depuis l’endroit où elle avait été quittée.

À chaque boucle du programme principal, la ligne while (Serial.available() > 0) sera exécutée, et si le test indique la réception d’un message (au moins un octet dans le tampon), les instructions qui suivent seront à leur tour exécutées afin de transférer ce message dans la variable commande, pour pouvoir l’utiliser par la suite, et le tampon sera vidé. C’est la détection du tampon vide qui met fin à la boucle. Cerise sur le gâteau, le tampon étant vide, il peut accepter un nouveau message.

Si vous avez bien suivi, vous avez peut-être remarqué une « anomalie » dans la ligne while (Serial.available() > 0) : elle ne se termine pas par ;, contrairement à ce que j’ai indiqué plus haut. Eh bien ! en fait, c’est normal, car cette ligne ne constitue pas, à proprement parler, une instruction. C’est une structure de branchement indiquant au programme dans quelles conditions il doit exécuter une instruction (ou un bloc d’instructions).

Bien entendu, les variables reception et commande devront être déclarées en début de programme (dans la zone déclarations) de la manière suivante :

int reception;
String commande = "";  // Chaîne vide

Nous avons vu int plus haut.

String (n’oubliez pas la majuscule) permet de déclarer une chaîne de caractères. En fait, String c’est beaucoup plus que ça, mais ici, nous ne verrons que cet aspect de son utilisation. Je vous invite naturellement à consulter la « référence » du langage Arduino en cliquant sur l’entrée éponyme dans le menu « Aide » de l’EDI. Elle s’affichera dans votre navigateur.

Une chaîne de caractères est délimitée classiquement par les guillemets anglais « " », et donc la chaîne « "" » est une chaîne vide puisqu’il n’y a rien entre les guillemets.

On peut également délimiter une chaîne de caractères avec des apostrophes « ' », mais cela impose un bricolage quand le texte constituant cette chaîne comprend lui-même des apostrophes.

On pourrait objecter que l’inverse est aussi vrai, mais la présence de guillemets dans un texte est nettement moins fréquente que celle d’apostrophes, alors autant prendre tout de suite de bonnes habitudes.

VI-D-3 - Traitement du message

Maintenant que vous avez récupéré le message, il va vous falloir le traiter. Comme je l’ai dit plus haut, le message sera une chaîne de caractères. C’est à vous de décider quelle chaîne vous allez utiliser. Comme il s’agit d’allumer et d’éteindre une LED, je vous propose d’utiliser deux vocables qui, s’ils ne font pas partie du vocabulaire français, ont au moins le mérite d’être quasiment universellement compris (si tant est qu’on puisse réduire l’Univers à la planète Terre) : il s’agit de « on » et « off ».

Vous allez donc devoir tester si la commande reçue est « on », auquel cas vous allumerez la LED, ou « off », et alors il faudra l’éteindre.

Pour ce faire, nous allons utiliser une structure quasiment incontournable en programmation : la structure if…else. Cette structure est une structure de branchement conditionnel, à savoir que, si une condition est réalisée, on fait quelque chose, et si elle ne l’est pas, on fait autre chose (ou rien).

En pseudo-langage, notre code pourrait ressembler à ceci :

si message = on
on allume la LED
sinon
on éteint la LED

Vous pourriez m’objecter, à juste titre, que l’on n’a donc pas besoin de « off ». Alors dans ce cas précis, effectivement, il suffit d’envoyer « on » pour allumer la LED, et « n’importe quoi » pour l’éteindre. Mais on va essayer de faire mieux directement, en traitant par exemple la réception d’une commande erronée.

Voici ce que ça pourrait donner en pseudo-langage :

si message = on
on allume la LED
sinon
si message = off
on éteint la LED
sinon
on retourne « commande inconnue » au donneur d’ordre

et en langage Arduino :

if (commande == "on")
{
digitalWrite(led, HIGH);
}
else if (commande == "off")
{
digitalWrite(led, LOW);
}
else
{
// Traitement de la commande inconnue.
}

Nous verrons le « traitement de la commande inconnue » un peu plus loin.

Outre le branchement conditionnel, vous découvrez ici un nouveau symbole : == (deux signes « égal » accolés). Ce symbole permet de comparer deux éléments pour en vérifier l’égalité.

Il s’agit là de bien comprendre la différence entre une « affectation » et une « comparaison » :

- =, symbole d’affectation. Syntaxe : variable = valeur, valeur devant être du même type que variable. Une fois que cette affectation est exécutée, variable vaudra valeur, et toute référence à cette variable équivaudra à faire référence à cette valeur.
L’affectation modifie la valeur de la variable ;

- ==, symbole de comparaison. Syntaxe : (argument1 == argument2). Cette expression est de type boolean (booléen en français) et vaut TRUE (vrai) si les deux arguments sont égaux, et FALSE (faux) dans le cas contraire. argument1 et argument2 doivent être de même type.
La comparaison ne modifie pas la valeur des éléments comparés.

Pour donner un petit exemple, après :

int var;

…

var = 5;

l’expression :

(var == 5)

sera vraie, alors que l’expression :

(var == 3)

sera fausse.

Voici à quoi ressemble notre programme à présent :

/* Ne pas hésiter à donner ici des renseignements sur le programme qui suit,
à la fois pour vous et pour d’éventuels utilisateurs.
*/

const int led = 13;

int reception;
String commande = "";

void setup()
{
pinMode(led, OUTPUT);
Serial.begin(115200);
Serial.flush();
}

void loop()
{
while (Serial.available() > 0)
{
reception = Serial.read();
commande += char(reception);
delay(5);
}
if (commande == "on")
{
digitalWrite(led, HIGH);
}
else if (commande == "off")
{
digitalWrite(led, LOW);
}
else
{
// Traitement de la commande inconnue.
}
}

À priori, ça devrait fonctionner !

Copiez ce code dans l’EDI Arduino et téléversez le sketch. Les LED TX et RX vont clignoter, ce qui est bon signe, et la zone de notification devrait vous afficher « avrdude done. Thank you. » en orange sur fond noir, signe que tout s’est bien passé.

Cliquez sur le bouton en haut à droite de l’EDI pour ouvrir le moniteur série. Vérifiez en bas du moniteur que vous êtes en mode « Pas de fin de ligne », et que le débit est bien réglé sur 115 200 Bds.

Dans la zone de saisie, saisissez « on » et cliquez sur « Envoyer » ou appuyez sur « Entrée ».

Victoire : la LED s’allume.

Ne vous arrêtez pas en si bon chemin et réitérez la manipulation, mais en saisissant « off », cette fois-ci.

Et paf : plus victoire. Tout s’effondre autour de vous. La LED ne s’éteint pas. Que se passe-t-il ?

Le problème ne vient pas de la réception de la commande puisque la LED s’allume quand on envoie « on ». Quant aux branchements conditionnels, ils sont tout ce qu’il y a de simple et une erreur à ce niveau semble exclue.

La seule chose qui a changé entre l’envoi de « on » et l’envoi de « off » est la variable commande. Suivons son parcours. À la réception du premier message, à savoir « on », la variable commande, initialisée comme chaîne vide lors de sa déclaration, est utilisée dans la boucle while() pour stocker le dit message. En sortie de boucle, commande vaut « on ». On traite la commande, ce qui conduit à l’allumage de la LED puisque la comparaison (commande == "on") vaut TRUE. Jusque-là tout va bien.

À réception du second message, c’est-à-dire « off », commande est à nouveau utilisée dans la boucle while() pour récupérer ce nouveau message. Mais commande n’est plus une chaîne vide ! commande vaut « on » ! Ce qui fait qu’après son utilisation par la boucle while(), commande vaut « onoff ». Le nouveau message a simplement été ajouté au précédent. Et donc, la comparaison (commande == "off") vaut FALSE puisque « onoff » est à l’évidence différent de « off » : La LED ne sera donc pas éteinte. L’erreur vient de là. Il ne faut pas oublier de réinitialiser la variable commande.

Voici le code retouché :

/* Ne pas hésiter à donner ici des renseignements sur le programme qui suit,
à la fois pour vous et pour d’éventuels utilisateurs.
*/

const int led = 13;

int reception;
String commande = "";

void setup()
{
pinMode(led, OUTPUT);
Serial.begin(115200);
Serial.flush();
}

void loop()
{
while (Serial.available() > 0)
{
reception = Serial.read();
commande += char(reception);
delay(5);
}
if (commande == "on")
{
digitalWrite(led, HIGH);
}
else if (commande == "off")
{
digitalWrite(led, LOW);
}
else
{
// Traitement de la commande inconnue.
}
commande = "";
}

Ouvrez le moniteur série de l’EDI et saisissez successivement « on » et « off ». La LED s’allume et s’éteint : tout va bien.

Vous allez à présent modifier ce code pour le rendre à la fois plus lisible et plus facile à manipuler en déplaçant la partie chargée du traitement de la commande dans une fonction que l’on va appeler traitement(), faisons simple, et en invoquant cette fonction dans le programme principal void loop(). Voici le code :

/* Ne pas hésiter à donner ici des renseignements sur le programme qui suit,
à la fois pour vous et pour d’éventuels utilisateurs.
*/

const int led = 13;

int reception;
String commande = "";

void setup()
{
pinMode(led, OUTPUT);
Serial.begin(115200);
Serial.flush();
}

void loop()
{
while (Serial.available() > 0)
{
reception = Serial.read();
commande += char(reception);
delay(5);
}
traitement();  // Appel de la fonction
}

// Fonctions

void traitement()  // Fonction de traitement de la commande
{
if (commande == "on")
{
digitalWrite(led, HIGH);
}
else if (commande == "off")
{
digitalWrite(led, LOW);
}
else
{
// Traitement de la commande inconnue.
}
commande = "";
}

La déclaration de la fonction commence par le mot clé void (vide en anglais), car la fonction ne retourne rien. Donc, en toute rigueur, ce n’est pas une fonction. L’appellation « procédure » serait plus indiquée, mais bon ! Son nom est traitement() selon notre choix et les parenthèses sont vides, car elle n’attend pas de paramètre. Le code de cette fonction commence par une accolade ouvrante et se termine par une accolade fermante, lesquelles délimitent un bloc d’instructions qui lui-même peut être composé de plusieurs blocs d’instructions, etc.

Cette déclaration se situe en dehors de la boucle principale void loop(), et vous pouvez déclarer autant de fonctions que vous le désirez, la seule limite étant la taille de la mémoire dont vous disposez.

Ce code, s’il fonctionne, n’est pas satisfaisant.

En premier lieu, examinons le programme principal :

Ce programme, à savoir ce qui se trouve dans la boucle void loop(), nous sommes d’accord, va être parcouru séquentiellement, en première approximation, du début jusqu’à la fin, et va boucler, c’est-à-dire se répéter, tant que votre Arduino sera alimenté en électricité. Donc, à chaque passage, dont la fréquence dépend de la charge de votre programme, le test de réception (notre boucle while()) va être effectué, ce qui est parfait puisqu’on n’a à priori aucun autre moyen de savoir quand une commande va être reçue. C’est le moyen le plus simple (voire le seul) pour « rester à l’écoute ».

Par contre, et là, c’est moins bien, la fonction traitement() va également être appelée à la même fréquence, mais sans aucun intérêt tant qu’une nouvelle commande nécessitant une actualisation de l’état de la LED n’aura pas été reçue. Dans l’état actuel du programme, cela n’a aucune incidence, mais nous verrons un peu plus loin que ce n’est pas toujours le cas.

Il va donc falloir trouver un moyen pour ne permettre l’appel à cette fonction que quand c’est utile, à savoir à la réception d’un message.

En second lieu, il serait souhaitable de mettre le donneur d’ordre au courant de la bonne (ou mauvaise) exécution de ses consignes. Bien sûr, dans le cas présent, vous êtes devant votre ordinateur, la carte Arduino est entre votre clavier et votre écran (c’est du moins mon cas), et vous voyez immédiatement le résultat de votre commande. Mais dans la « vraie vie », le résultat de votre commande ne sera pas forcément visible immédiatement, et vous voudriez peut-être bien être malgré tout informé du bon déroulement des opérations.

Un retour d’information de l’Arduino vers le PC sera donc le bien venu, et, par conséquent, il va falloir l’implémenter.

Eh bien ! nous allons justement commencer par cette implémentation, car cela va nous permettre de mettre en évidence la nécessité de n’autoriser l’appel à la fonction traitement() que lorsque c’est nécessaire.

Vous avez vu un peu plus haut l’utilisation de la bibliothèque Serial pour recevoir une chaîne de caractères au travers de la liaison série (USB). Vous allez voir maintenant son utilisation pour l’envoi d’une chaîne de caractères. Comme au lieu de « lire », vous devez « écrire », au lieu de read() vous utiliserez print() ou println(), selon que vous souhaitez que les écritures successives se fassent sur une même ligne ou chacune sur une nouvelle ligne. Pas compliqué !

Voici l’instruction à utiliser :

Serial.print("texte");

ou

Serial.println("texte");

où « texte » représente la chaîne de caractères que vous voulez envoyer.

Maintenant, on pourrait être tenté d’envoyer le message dans le même bloc d’instructions que celui qui code pour l’actualisation de la LED. Par exemple :

if (commande == "on")
{
digitalWrite(led, HIGH);
Serial.println("ledOn");
}

en imaginant qu’on va envoyer le message « ledOn » pour indiquer que la LED est allumée, ce qui n’est pas absurde.

Mais quelle est la valeur de cette information ? Bon ! Elle n’est pas nulle, mais elle indique seulement que la commande a bien été transmise. C’est déjà ça, me direz-vous, mais on peut faire mieux. Nous allons tester l’état de la broche concernée, et si cette broche est à l’état haut (HIGH), on enverra « ledOn », et dans le cas contraire on enverra « ledOff ».

Vous allez me dire qu’après l’instruction digitalWrite(led, HIGH); , si la broche n’est pas à l’état haut, c’est que la carte Arduino UNO a un sérieux problème, et c’est tout à fait vrai. Les deux informations auront ici la même valeur. Mais dans le cadre d’une application réelle, ce n’est bien sûr pas l’état de la broche de commande que vous allez tester, mais, par exemple, l’état d’une autre broche, reliée à un capteur lié au matériel piloté par la broche de commande. Au niveau de l’exercice, cela revient exactement au même.

Vous allez donc compléter la fonction traitement() comme suit :

void traitement()  // Fonction de traitement de la commande
{
if (commande == "on")
{
digitalWrite(led, HIGH);
}
else if (commande == "off")
{
digitalWrite(led, LOW);
}
else
{
// Traitement de la commande inconnue.
}
commande = "";
if (digitalRead(led) == HIGH)
{
Serial.println("ledOn");
}
else
{
Serial.println("ledOff");
}
}

Vous remarquerez la nouvelle instruction digitalRead(numBroche) qui renvoie HIGH si la broche passée en argument est à l’état haut, et LOW dans le cas contraire.

Téléversez ce sketch sur votre carte. Ouvrez le moniteur série, et constatez le défilement de « ledOff ». Saisissez « on » : la LED va s’allumer, signe que ça fonctionne, et le moniteur affiche maintenant un défilement de « ledOn ».

Pourquoi ce défilement : tout simplement parce que la fonction « traitement() » est appelée à chaque boucle du programme principal, et elle envoie à chaque fois sur la liaison série le compte rendu de l’état de la LED, au lieu de l’envoyer uniquement lors d’un changement d’état. Vous pouvez constater que la LED TX de votre carte Arduino UNO est allumée en permanence (en réalité elle clignote très vite) car la carte envoie en permanence des données vers le PC. Et donc, comme vu plus haut, il faut trouver un moyen pour n’appeler cette fonction que lorsqu’un nouveau message est reçu.

La solution est extrêmement simple. Il suffit d’utiliser le branchement conditionnel suivant :

if (commande != "")
{
traitement();
}

L’opérateur de comparaison != signifie « différent de », et donc ce code signifie :

- si commande est différent dune chaîne vide, c’est-à-dire si on a reçu un message, on exécute la fonction traitement() ;

- comme on n’a rien de spécifique à faire dans le cas contraire, il n’y a pas de clause else.

Ce qui nous donne :

/* Ne pas hésiter à donner ici des renseignements sur le programme qui suit,
à la fois pour vous et pour d’éventuels utilisateurs.
*/

const int led = 13;

int reception;
String commande = "";

void setup()
{
pinMode(led, OUTPUT);
Serial.begin(115200);
Serial.flush();
}

void loop()
{
while (Serial.available() > 0)
{
reception = Serial.read();
commande += char(reception);
delay(5);
}
if (commande != "")
{
traitement(); // Appel de la fonction
}
}

// Fonctions

void traitement() // Fonction de traitement de la commande
{
if (commande == "on")
{
digitalWrite(led, HIGH);
}
else if (commande == "off")
{
digitalWrite(led, LOW);
}
else
{
// Traitement de la commande inconnue.
}
commande = "";
if (digitalRead(led) == HIGH)
{
Serial.println("ledOn");
}
else
{
Serial.println("ledOff");
}
}

Lancez le moniteur série et entrez « on », « off » et « toto » à plusieurs reprises dans un ordre quelconque et regardez ce qui se passe :

- Quand vous saisissez « on », la LED s’allume et le message « ledOn » s’affiche une seule fois.

- Quand vous saisissez « off », la LED s’éteint et le message « ledOff » s’affiche une seule fois.

- Quand vous saisissez « toto » ou quoi que ce soit de différent de « on » et « off », il n’y a pas de modification de l’état de la LED, et le message affiche l’état inchangé de la LED une seule fois.

- Entre deux commandes, la liaison série est inactive, ce qui était le but à atteindre.

Il ne nous reste plus qu’à traiter le cas de la commande inconnue.

On va se contenter ici d’envoyer un message indiquant que la commande reçue est inconnue en saisissant dans la clause else prévue à cet effet :

Serial.print("Commande '" + commande + "' inconnue : ");

L’instruction Serial.print() fait la même chose que Serial.println(), mais ne provoque pas de « retour chariot » ou « saut de ligne », ce qui fait que la chaîne reçue suivante sera affichée sur la même ligne. Cette différence sera à l’origine d’un petit problème quand nous réaliserons l’interface graphique avec Boa Constructor, mais elle va nous permettre ici d’obtenir l’affichage voulu.

Notez la différence entre l’instruction Serial.print("Commande"), qui affichera la chaîne « Commande », et l’instruction Serial.print(commande) qui affichera « le contenu de la variable commande ».

Notez également la concaténation (assemblage) des chaînes obtenue à l’aide de l’opérateur +.

Voici votre sketch finalisé :

/* Ne pas hésiter à donner ici des renseignements sur le programme qui suit,
à la fois pour vous et pour d’éventuels utilisateurs.
*/

const int led = 13;

int reception;
String commande = "";

void setup()
{
pinMode(led, OUTPUT);
Serial.begin(115200);
Serial.flush();
}

void loop()
{
while (Serial.available() > 0)
{
reception = Serial.read();
commande += char(reception);
delay(5);
}
if (commande != "")
{
traitement(); // Appel de la fonction
}
}

// Fonctions

void traitement() // Fonction de traitement de la commande
{
if (commande == "on")
{
digitalWrite(led, HIGH);
}
else if (commande == "off")
{
digitalWrite(led, LOW);
}
else
{
Serial.print("Commande '" + commande + "' inconnue : ");// Traitement de la commande inconnue.
}
commande = "";
if (digitalRead(led) == HIGH)
{
Serial.println("ledOn");
}
else
{
Serial.println("ledOff");
}
}

Téléversez-le, lancez le moniteur série et testez.

Par exemple, la saisie successive de « on », « off », « toto », « on » et « toto » doit vous donner ce qui suit :

Votre sketch est opérationnel, à savoir qu’il fait ce qu’on lui demande de faire, et ce n’est déjà pas si mal. Il va vous permettre maintenant d’écrire et de tester votre script Python.

VII - Programme console Python

VII-A - Structure d’un script Python

Contrairement à un programme Arduino qui exige une structure particulière, pas très compliquée toutefois, il n’y a pas de structure à proprement parler dans un script Python (ce qui ne veut pas dire que l’on puisse faire n’importe quoi).

Python est un langage interprété, c’est-à-dire qu’il a besoin d’un programme spécifique, appelé « interpréteur », installé sur votre ordinateur, pour pouvoir être exécuté. Le script Python, un simple fichier texte en fait, va être « ouvert » par ce programme, comme un fichier texte serait ouvert par un traitement de texte, mais au lieu d’afficher le texte du fichier à l’écran, l’interpréteur va, pour faire simple, lire ce fichier ligne après ligne et exécuter, au fur et à mesure de sa lecture, les instructions contenues dans chaque ligne, pour peu qu’il les comprenne.

Dans ces conditions, il n’y a pas besoin d’une structure définie. Il suffit d’être cohérent et, par exemple, ne pas essayer d’appeler une fonction si celle‑ci n’a pas été écrite en amont dans le script, car dans ce cas, l’interpréteur ne sait pas encore qu’elle existe, et il ne peut donc pas l’utiliser. De même, et pour la même raison, l’importation d’une bibliothèque, ou d’un module, devra se faire avant son utilisation, mais pas nécessairement en début de programme.

Le programme qui sait lire (interpréter) les scripts Python s’appelle… « Python » ou « interpréteur Python ». Il est logique de supposer que, partout où Python pourra être installé, on pourra utiliser des programmes écrits en langage Python. Comme Python est multiplateforme, il apparaît clairement que les programmes écrits en langage Python seront « multiplateformes ».

Il y aura toutefois deux petites entorses faites à cette « absence » de structure évoquée plus haut :

- sous GNU-Linux, votre script devra impérativement commencer par la ligne #!/usr/bin/python ou #!/usr/bin/env python, à défaut de quoi le script ne saura pas où trouver l’interpréteur nécessaire à son exécution. Sous Windows©, à l’installation de Python, le chemin vers l’interpréteur « python.exe » est ajouté aux variables d’environnement, et l’exécutable est automatiquement associé aux fichiers « *.py » : le problème ne se pose donc pas ;

- Vous devrez ajouter la ligne # coding: utf8 si vous souhaitez utiliser cet encodage pour les caractères étendus, accentués entre autres. Vous pourrez trouver également la ligne # -*- coding : utf8 -*- pour un résultat identique. Pourquoi faire simple quand on peut faire compliqué ? À vous de choisir !

VII-B - Le programme Python

En ce qui concerne le programme Python, le schéma sera le même que pour le code Arduino.

En effet, que doit faire ce programme ?

- Envoyer des données sur le port série ;

- Réceptionner des données sur le port série et les traiter.

C’est ce que vous venez de faire avec Arduino, et, quoique le langage soit différent, vous allez constater qu’il y a bien des similitudes. C’est d’ailleurs rassurant en soi. Il s’agit toujours de programmation et les spécificités « visibles » du langage jouent essentiellement sur de petites différences de vocabulaire et de syntaxe.

Nous allons donc suivre à peu près la même démarche en commençant par l’importation des bibliothèques.

VII-B-1 - Importation des bibliothèques

Comme pour le programme Arduino, nous aurons besoin d’une bibliothèque gérant la liaison série (USB). Cette bibliothèque s’appelle pySerial. Comme celle-ci ne fait pas partie intégrante du langage, ce qui est logique, car vous n’en avez pas forcément besoin, il va falloir l’importer. Contrairement à la clause include du langage Arduino, la clause équivalente du langage Python, import, ne doit pas nécessairement être invoquée au début du programme. Le seul impératif est qu’elle soit invoquée avant que la bibliothèque à laquelle elle fait référence soit utilisée.

Personnellement, je mets mes clauses import en début de programme, ce qui permet d’un coup d’œil de voir quelles sont les bibliothèques importées. Cela évite d’importer deux fois la même bibliothèque dans le cours du programme.

On peut également importer plusieurs bibliothèques sur une même ligne, mais je trouve plus lisible d’en mettre une par ligne. Tout ça est affaire de goût.

Cette invocation se fait tout simplement de la manière suivante :

import serial

Deux remarques :

- La bibliothèque pySerial n’est pas installée lors de l’installation standard de Python. Il est donc possible qu’elle ne soit pas disponible sur votre système. Vous devrez dans ce cas l’installer. Pour ce faire, rendez-vous à cette adresse.

- Le nom d’import de la bibliothèque pySerial est serial. Ce n’est pas une erreur. Comme vous allez le constater dans ce qui suit, le nom d’import de la bibliothèque time est également time, ce qui correspond au cas général.

Comme pour le programme Arduino, et pour les mêmes raisons, nous aurons besoin d’une temporisation, et pour la mettre en place, nous devrons importer la bibliothèque dédiée à la « gestion du temps ». Cette bibliothèque s’appelle time. Elle est installée en standard avec Python, et est donc immédiatement disponible : vous n’aurez pas besoin de la télécharger.

Il peut sembler « lourd » d’avoir à importer des bibliothèques pour un oui ou pour un non, mais cela permet une meilleure gestion de l’encombrement mémoire. On peut même affiner en n’important d’une bibliothèque que ce qu’on veut utiliser. L’inconvénient de l’utilisation de bibliothèques non fournies en standard est que vous ne saurez pas toujours si elles sont présentes sur le système cible. Vous devrez donc les fournir avec le script, sous peine de voir échouer le démarrage du programme.

Par voie de conséquence, dans le cas où elles seraient déjà présentes, vous aurez des doublons. Avec la capacité des disques actuels, cela ne pose pas vraiment de problème, mais si vous êtes perfectionniste, vous pourrez toujours écrire un script « batch » qui testera si les bibliothèques en cause sont, ou non, présentes, et les installera si besoin.

Vous pouvez également utiliser l’excellent utilitaire de gestion de package python PIP pour vérifier la présence de la bibliothèque pySerial en tapant pip show pySerial dans un terminal (pas de remarque graveleuse), suivi de pip install pySerial en cas d’absence.

Comme ça dépasse largement le cadre de ce tutoriel, nous n’irons pas plus loin sur ce sujet.

Notre script commencera donc comme suit :

#!/usr/bin/python
# coding: utf8

import serial
import time

VII-B-2 - Établissement de la liaison série

Comme avec Arduino, vous devrez établir une liaison série. IL va cependant y avoir quelques différences dans la façon de procéder.

L’établissement de la liaison série consiste à créer une « instance » de l’objet serial, qui va vous fournir des « méthodes » et des « propriétés » pour exploiter le port série que vous allez utiliser, puis à ouvrir le port concerné.

Pour créer cette instance, vous avez besoin d’au moins trois informations.

VII-B-2-a - Nommage de l’instance

Il s’agit du nom que vous allez donner à l’instance que vous êtes en train de créer, et qui vous permettra d’y faire référence dans votre script, chaque fois que vous en aurez besoin. Vous avez le libre choix de ce nommage, dans la limite du respect des normes de nommage des variables dans Python.

Je vous propose portCom : ce n’est pas très original, et ce n’est bien sûr qu’une suggestion.

VII-B-2-b - Identifiant système du port

Un port série est attribué automatiquement par votre système quand vous connectez un périphérique série.

Quand je parle ici de « port série » ou de « périphérique série », je fais uniquement référence aux ports ou périphériques série à la norme USB (Universal Serial Bus). Le système peut gérer théoriquement jusqu’à 127 périphériques série.

La carte Arduino UNO est un périphérique série. Quand vous la connectez, des données vont être échangées entre la carte et le système, qui vont permettre à celui-ci d’identifier le périphérique et d’attribuer le nom sous lequel le port sera référencé.

Sous GNU-Linux, les fichiers correspondant aux ports série se trouvent dans le dossier « /dev/ » et ont le préfixe « tty ». Pour les visualiser, entrez la ligne suivante dans un terminal :

ls /dev/tty*

S’agissant d’une carte Arduino, l’identité attribuée sera de la forme « ttyACMX », où « X » est un nombre entier unique permettant de discriminer les différentes cartes éventuellement raccordées, et donc, en entrant la ligne suivante dans un terminal :

ls /dev/ttyACM*

vous aurez la liste des cartes Arduino connectées.

Nous allons considérer pour ce tutoriel qu’il n’y a qu’une carte connectée. L’identité du port attribué par le système sera donc « ttyACM0 ». Vous pouvez consulter le chapitre IX.C.Identifiant de port pour plus de précisions à ce sujet.

Si la connexion vous est refusée, cela signifie probablement que vous ne disposez pas des droits requis. Consultez le chapitre IX.B.Autorisation d’accès au port série pour régler ce problème.

VII-B-2-c - Vitesse de transmission des données.

Vous aurez également besoin de renseigner le système sur la vitesse de transmission à laquelle il doit communiquer avec la carte. Cette vitesse est imposée par la carte, et c’est vous l’avez fixée à « 115 200 Bd » lors de l’initialisation de la liaison série dans votre code Arduino.

VII-B-2-d - Initialisation

Voici ce que cela peut donner :

portCom = serial.Serial("/dev/ttyACM0", 115200)

Comme vous pouvez le constater, cette ligne ressemble à la ligne d’initialisation de la liaison série sur l’Arduino UNO, si l’on fait abstraction des identifiants de ports, lesquels sont inutiles sur cette carte qui ne dispose que d’un seul port série (matériel).

Je dis « Voici ce que cela peut donner » car vous pouvez également écrire :

portCom = serial.Serial()
portCom.port = "/dev/ttyACM0"
portCom.baudrate = 115200

Je préfère cette écriture car, si elle est moins compacte que la précédente, elle me semble plus facile à comprendre et montre comment modifier le paramétrage en cours d’utilisation. Pour plus de renseignements, vous pouvez consulter Short introduction dans la documentation de la bibliothèque pySerial.

Le rôle de la première ligne est de créer une instance de l’objet « serial » en nommant cette instance. Comme vu plus haut, nous avons choisi de l’appeler « portCom ». S’agissant d’un objet, « serial » est doté de « méthodes » (procédures et fonctions) et de « propriétés ». « portCom » étant une instance de cet objet, il hérite ses méthodes et propriétés, et vous permet d’y accéder en les préfixant de la manière suivante :

portCom.propriété
portCom.méthode

La seconde et la troisième ligne illustrent ce mécanisme avec les propriétés port et baudrate en leur attribuant les valeurs adéquates.

Cette écriture permet, grâce à la seconde ligne, de mettre en évidence le lien créé entre le port système « /dev/ttyACM0 » et l’instance logicielle du port portCom par l’intermédiaire de sa propriété portCom.port. Elle permet en outre, en cours d’exécution, une modification aisée des propriétés du port, pour peu que vous ayez implémenté cette fonctionnalité dans votre programme.

Vous pouvez également constater une première différence de syntaxe avec le langage Arduino, et avec la plupart des langages de programmation : les instructions ne sont suivies d’aucune ponctuation.

Il faut savoir que terminer une instruction avec « ; » (point-virgule) ne constitue pas une faute et ne déclenchera pas d’erreur. C’est simplement inutile en Python.

Si, par contre, vous voulez mettre plusieurs instructions sur une même ligne, vous devrez l’utiliser comme séparateur.

VII-B-2-e - Ouverture du port

Maintenant que votre port série existe au niveau logiciel, vous devez l’ouvrir pour pouvoir l’utiliser. Dans ce but, vous allez utiliser la méthode open() de l’objet serial que vous venez d’instancier sous le nom portCom :

portCom.open()

La méthode open() n’attend aucun argument et ne retourne rien.

VII-B-3 - Un premier script opérationnel

Maintenant que la liaison série est opérationnelle, on va pouvoir l’exploiter. Pour allumer et éteindre la LED, la carte Arduino UNO « attend » la réception de la chaîne « on » ou de la chaîne « off » : vous avez expérimenté cela précédemment dans ce tutoriel, en utilisant le moniteur série de l’EDI Arduino. Eh bien ! vous allez faire la même chose, mais à partir d’une console standard.

Dans la mesure où vous devez envoyer une chaîne de caractères, il vous faut une « zone de saisie ». Dans une console, il n’y a pas, à proprement parler de zone de saisie, comme celles que l’on trouve dans les applications graphiques. Elle sera remplacée par un texte expliquant ce qu’on attend de l’utilisateur, suivi de la classique « invite de commande » (ou prompt), matérialisée assez souvent par un rectangle plein clignotant (mais ça peut dépendre du paramétrage de votre console). À la suite de ce prompt, vous saisirez « on » ou « off » puis presserez la touche « Entrée », comme vous l’auriez fait avec le moniteur série Arduino.

Comment réaliser ceci ? Le plus simplement du monde, en utilisant une instruction Python dédiée à l’entrée de chaînes de caractères (string) en mode console. Voici cette instruction :

chaine = raw_input(texte)

où « chaine » représente la variable qui recevra la chaîne que vous aurez saisie, et « texte » la chaîne explicative affichée avant le prompt. Cette instruction est bloquante, c’est-à-dire que, tant que vous n’aurez pas appuyé sur la touche entrée, il ne se passera rien.

La ligne de code suivante fera très bien l’affaire :

commande = raw_input("Entrez une commande : ")

qui permettra de stocker dans la variable « commande » le texte que vous aurez saisi.

Maintenant, cette commande, il vous faut l’envoyer. La bibliothèque serial fournit la méthode serial.write() pour réaliser cela en toute simplicité. Cette méthode, une fonction, prend les données à envoyer comme paramètre, ici une chaîne de caractères (string), mais plus généralement des octets ou des tableaux d’octets, et retourne le nombre d’octets envoyés, à des fins de contrôle par exemple, fonctionnalité que nous n’utiliserons pas dans ce tutoriel.

Voici comment se présentera cette instruction dans notre script :

portCom.write(commande)

Le corps du programme, très simple ici, sera donc composé de ces deux lignes :

commande = raw_input("Entrez une commande : ")
portCom.write(commande)

Nous pouvons d’ores et déjà, avec ce que nous venons de voir, écrire le script suivant :

#!/usr/bin/python
# coding: utf8

import serial
import time

portCom = serial.Serial()
portCom.port = "/dev/ttyACM0"
portCom.baudrate = 115200

portCom.open()

commande = raw_input("Entrez une commande : ")
portCom.write(commande)

Je vous propose de le copier dans un fichier que vous nommerez comme bon vous semblera, et que je nommerai pour ma part « test-serial_1.py ».

Sauvegardez ce fichier et modifiez ses permissions pour le rendre exécutable, par exemple en entrant cette ligne dans un terminal :

chmod +x test-serial_1.py

Maintenant, lancez l’exécution du script. Si vous avez « tout bien fait comme je vous ai dit », vous devriez avoir quelque chose comme ça :

Si la console s’ouvre et se ferme presque immédiatement, commencez par vérifier que le script copié dans votre fichier est bien conforme à celui du tutoriel. Tous les scripts que je fournis ici ont été testés après un « copier-coller », et sont donc, normalement, exempts d’erreur.

Si le problème ne vient pas de là, le port série est très probablement en cause. Commencez par vérifier que votre carte est bien branchée, et si oui, que le port « /dev/ttyACM0 » existe bien. Si ce port n’existe pas, débranchez votre carte, saisissez « ls /dev/tty* » dans une console, puis refaites la même manipulation après avoir rebranché la carte. En comparant les deux résultats, vous devriez pouvoir déterminer le port attaché à votre carte. Modifiez le script en conséquence et relancez-le.

Si la liste des ports reste inchangée avec et sans la carte, c’est que celle-ci n’est pas reconnue, et que le problème est peut-être matériel. Là, je ne peux pas vous aider.

Saisissez « on », puis appuyez sur « Entrée ». La LED de votre carte s’allume : tout va bien.

Alors, oui et non. Oui, parce que ça fonctionne, et non, parce que la console se ferme, et que, par conséquent, vous vous retrouvez dans l’impossibilité d’envoyer d’autres commandes. La console se ferme, car l’interpréteur arrive en fin de fichier et considère que le programme est terminé, ce qui est techniquement le cas.

Pour pallier ce problème, il faut empêcher l’interpréteur d’arriver en fin de fichier. On va utiliser une méthode très classique : mettre le corps du programme dans une boucle while().

Modifiez votre script comme suit :

#!/usr/bin/python
# coding: utf8

import serial
import time

portCom = serial.Serial()
portCom.port = "/dev/ttyACM0"
portCom.baudrate = 115200

portCom.open()

while True:
commande = str(raw_input("Entrez une commande : "))
portCom.write(commande)

Si, jusqu’à présent, les lignes de code saisies n’étaient pas vraiment différentes de ce qu’elles auraient pu être avec un autre langage, mis à part l’absence du « ; » en fin d’instruction, nous abordons ici une caractéristique particulière de la syntaxe Python concernant les blocs d’instructions. La notion de bloc d’instructions n’est, bien sûr, pas spécifique à Python, mais c’est la façon de délimiter ce bloc qui est originale.

En Python, un bloc n’est pas délimité, comme dans beaucoup d’autres langages courants, par les accolades {…} du C, ni par le begin…end; du Pascal, par exemple : il est ouvert par une ligne de code se terminant par « : », et les instructions qui le composent doivent être indentées uniformément par rapport à cette ligne. Le bloc se termine par un retour au niveau de l’indentation de la ligne qui l’a ouvert ou à un niveau inférieur.

Un bloc peut, naturellement, contenir d’autres blocs, chacun respectant la règle ci-dessus, par exemple :

structure-ouvrante-1:        # Début du bloc 1
instruction-1-1            # Indentation niveau 1
instruction-1-2
structure-ouvrante-2:      # Début du bloc 2 (fait partie du bloc 1)
instruction-2-1          # Indentation niveau 2
instruction-2-2
…
instruction-2-n          # Dernière instruction du bloc 2
instruction-1-3            # Retour à l’indentation niveau 1 (fin du bloc 2)
…
instruction-1-n            # Dernière instruction du bloc 1
suite-du-programme           # Retour à l’indentation niveau 0 (fin du bloc 1)

La structure ouvrante peut être une structure de contrôle de flux comme if ou while, une déclaration de méthode comme def… terminée par « : », ce qui oblige à passer à l’indentation supérieure sous peine de déclencher une erreur. Le bloc 2 est un bloc imbriqué dans le bloc 1. On voit qu’on quitte un bloc d’instructions en revenant à un niveau d’indentation inférieur. Il n’y a pas d’ambiguïté.

Pour ceux qui ont l’habitude d’indenter leur code, ce qui est une pratique conseillée, notamment pour une meilleure lisibilité, cette syntaxe permet de faire l’économie des symboles ouvrant et fermant, quels qu’ils soient, sans changer leurs habitudes. C’est plutôt bien vu.

Ici, notre structure de contrôle est while True:. Cette boucle ne s’arrête jamais, car l’expression True est, à l’évidence, toujours vraie. Le bloc d’instructions concerné par cette boucle est le corps du programme en lui-même, et donc le programme va se répéter indéfiniment. Vous avez peut-être remarqué que while n’était pas suivi de parenthèses. En Python, celles-ci ne sont pas obligatoires. Attention toutefois, elles peuvent être nécessaires dans des expressions complexes pour lever toute ambiguïté.

Collez ce script dans un fichier appelé « test-serial_2.py » et exécutez-le. Cette fois-ci, ça fonctionne de façon satisfaisante, en ce sens que vous êtes à même d’allumer et d’éteindre la LED à votre convenance.

Vous pourriez vous arrêter là. Si votre but est simplement de commander individuellement les éclairages de votre bureau et de piloter le ventilateur, ce script, étendu au nombre de broches utilisées, fera l’affaire, l’allumage d’un spot étant un retour d’information suffisant.

Toutefois, comme on a implémenté le retour d’information dans le sketch Arduino, il serait dommage de ne pas l’utiliser, d’autant qu’en fonction de votre projet, cette fonctionnalité pourrait être indispensable.

VII-B-4 - Intégration du retour d’informations

Dans un premier temps, et même si ça ne se voit pas, il faut bien être conscient du fait qu’à chaque commande envoyée depuis votre PC, votre carte UNO poste en retour l’information concernant l’état de la LED. Elle ne peut pas faire autrement, le sketch que vous avez écrit pour elle l’impose.

Cette information est disponible dans le tampon de réception de votre port série, simplement votre script actuel ne l’utilise pas, ce qui ne l’empêche pas, bien sûr, de fonctionner. Nous allons remédier à cela.

Retour vers la bibliothèque serial : si elle nous a fourni une méthode pour écrire sur le port série, elle devrait bien nous fournir une méthode pour lire sur ce même port. Eh bien ! oui. Cette méthode existe (heureusement) et s’appelle read(). C’est une fonction qui demande en paramètre le nombre d’octets à lire parmi ceux qui sont présents dans le tampon, et qui retourne les octets lus, qui, ici, représentent une chaîne de caractères, et que l’on va récupérer dans une variable. Si aucun paramètre n’est fourni, cette fonction lira (et donc retournera) par défaut un seul octet à chaque appel.

C’est bien gentil tout ça, mais on ne sait pas forcément combien d’octets se trouvent dans le tampon. Pas de panique, tout (ou presque) est prévu. La bibliothèque serial propose la propriété ad hoc : in_waiting.

Pour ceux dont la version de pySerial est inférieure à 3.0, la propriété in_waiting doit être remplacée par la fonction inWaiting(). L’utilisation reste la même.

Ajoutons dans notre script ces deux lignes :

nbCar = portCom.in_waiting
retour = portCom.read(nbCar)

dans lesquelles nbCar est la variable stockant le nombre d’octets présents dans le tampon, et retour la variable stockant la chaîne représentée par ces octets.

Un programme ne faisant jamais que ce qu’on lui demande de faire, il ne faut pas oublier de lui demander d’afficher la chaîne à l’écran, sinon il n’y aura rien de fait. Pour ce faire, l’instruction Python print() est ce qu’il nous faut. Dans son utilisation la plus triviale, cette instruction envoie la chaîne passée en argument sur la sortie standard, à savoir l’écran dans notre cas. C’est cette fonctionnalité qui nous intéresse, et qui va s’écrire sous cette forme :

print(retour)

que vous allez ajouter à votre script.

Voici votre script actuellement :

#!/usr/bin/python
# coding: utf8

import serial
import time

portCom = serial.Serial()
portCom.port = "/dev/ttyACM0"
portCom.baudrate = 115200

portCom.open()

while True:
commande = raw_input("Entrez une commande : ")
portCom.write(commande)
nbCar = portCom.in_waiting
retour = portCom.read(nbCar)
print(retour)

Sauvegardez-le sous le nom «test-serial_3.py » par exemple, exécutez-le puis entrez une série de commandes « on » et « off » (pas cinquante, quatre ou cinq suffiront). Vous allez constater le phénomène suivant :

- commande « on » : la LED s’allume mais pas de retour ;

- commande « off » : la LED s’éteint et le retour est « ledOn » ce qui est contraire à la réalité ;

- commande « on » : la LED s’allume et le retour est « ledOff » ce qui est contraire à la réalité ;

- commande « off » : la LED s’éteint et le retour est « ledOn » ce qui est contraire à la réalité ;

- commande « on » : la LED s’allume et le retour est « ledOff » ce qui est contraire à la réalité ;

- …

Inutile d’aller plus loin : vous avez compris qu’il y avait un décalage entre l’état de la LED et le retour d’information concernant cet état. Cela est dû au fait que les opérations effectuées sur le port série ne sont pas instantanées, et que, en l’occurrence, la lecture du tampon intervient avant la réception de l’information.

- À la première commande « on » envoyée, la lecture du tampon se faisant avant la réception de l’information « ledOn », il n’y a rien dedans, et donc rien n’est affiché à l’écran.

- À la commande « off » qui suit, la lecture du tampon se faisant également avant la réception de « ledOff », le problème est le même, mais le tampon n’est plus vide. Il contient le « ledOn » reçu en réponse à la commande précédente, trop tard, certes, en regard du traitement informatique, mais bien avant que vous ayez tapé une nouvelle commande. Donc, c’est « ledOn » qui va s’afficher, alors que la LED est éteinte.

- À la commande « on » qui suit, bis repetita placent, et c’est donc « ledOff » qui va s’afficher alors que la LED est allumée.

- …

La solution consiste à faire une pause entre l’envoi de la commande et le traitement de la réception. Une méthode de la bibliothèque time va nous permettre de faire cela, et c’est pourquoi je vous ai fait importer cette bibliothèque précédemment. La méthode en question est time.sleep(), une procédure à laquelle vous devez fournir un paramètre : la durée du délai souhaité, exprimée en secondes. Cette durée sera fixée à 0,05 s, valeur couramment employée et fonctionnant, à priori, dans tous les cas.

Cette durée dépend de plusieurs facteurs dont le système d’exploitation que vous utilisez fait partie. Vous pouvez, si vous le souhaitez, la déterminer expérimentalement pour l’optimiser, mais dans ce type d’application, ce n’est pas crucial.

Sachez de plus que cette procédure n’offre pas un décompte précis du temps écoulé, et que lorsque la précision est nécessaire, il ne faut pas l’utiliser.

Voici le script finalisé :

#!/usr/bin/python
# coding: utf8

import serial
import time

portCom = serial.Serial()
portCom.port = "/dev/ttyACM0"
portCom.baudrate = 115200

portCom.open()

while True:
commande = str(raw_input("Entrez une commande : "))
portCom.write(commande)
time.sleep(0.05)
nbCar = portCom.in_waiting
retour = portCom.read(nbCar)
print(retour)

Le cahier des charges est respecté, à savoir que ce script réalise ce qu’on faisait avec le moniteur série fourni par l’EDI Arduino, à ceci près que nous sommes ici dans un environnement complet de programmation, et que par conséquent, on n’est plus du tout limité au seul affichage du retour.

L’avantage n’est pas évident au premier abord, étant donné la simplicité de l’exemple, mais le problème est différent si vous avez en retour des données plus complexes et variables à traiter, comme des températures, des débits, des pressions…

Si vous devez adapter les commandes en fonction, par exemple, de l’évolution de ces données ou de leur moyenne, vous comprendrez vite l’intérêt du système, et les 32 Kio de la mémoire flash de la carte Arduino UNO ne seront plus une limite, d’autant que vous pouvez piloter plusieurs cartes, autant que de ports disponibles en fait, et, par exemple, réserver une carte aux capteurs et une autre aux actionneurs.

Bien entendu, dans ce type d’utilisation, nous ne sommes à priori plus sur de l’embarqué, et la carte Arduino devient une interface entre le PC et les entités pilotées. Cela dit, si le PC est un nano-ordinateur, l’embarqué reste d’actualité.

VII-B-5 - Un script amélioré

Ce script est très basique : il respecte juste le cahier des charges.

Vous avez pu constater, par exemple, au chapitre VII.B.3, que l’absence du port série empêchait le démarrage du programme. Dans notre cas, ça n’avait pas de réelle importance mais ce n’est pas toujours le cas. Il ne faut pas qu’une erreur à l’exécution interdise l’utilisation du programme : il faut simplement traiter cette erreur.

De plus, pour arrêter ce programme, la seule solution est de fermer la console. Ce type d’arrêt « à la hussarde », s’il est efficace, n’est pas satisfaisant.

Je vous propose donc ce script amélioré, sans pour autant le détailler car cela sort du cadre de ce tutoriel. En vous penchant un peu sur la question, vous devriez comprendre les mécanismes mis en jeu. Je vous propose de jeter un œil ici pour avoir de plus amples renseignements sur l’utilisation de try, quant aux autres modifications, les explications précédentes devraient permettre de les comprendre.

#!/usr/bin/python
# coding: utf8

import serial
import time

print("Initialisation du port")
portCom = serial.Serial()
portCom.port = "/dev/ttyACM0"
portCom.baudrate = 115200

print("Ouverture du port...")
try:
portCom.open()
except serial.SerialException:
print(u"Un problème s'est produit à l'ouverture du port.\n"
"Vérifiez que le port utilisé par la carte Arduino est\n"
"bien '/dev/ttyACM0'. Si ce n'est pas le cas, modifiez\n"
"le script Python en conséquence.")
saisie = ""
while saisie != "q":
saisie = str(raw_input("Entrer 'q' pour quitter: "))

while portCom.is_open:
commande = str(raw_input("Saisir une commande ('q' pour quitter): "))
if commande != "q":
portCom.write(commande)
time.sleep(0.05)
nbCar = portCom.in_waiting
retour = portCom.read(nbCar)
print(retour)
portCom.reset_input_buffer()
else:
portCom.close()

Sauvegardez ce script dans un fichier et exécutez-le avec et sans la carte Arduino raccordée à votre PC, pour voir ce qui se passe. Ici, le traitement de l’erreur est réduit à l’affichage d’un message, mais dans un cas réel, ce traitement peut, par exemple, vous fournir la possibilité de modifier le port ou ses paramètres, et, d’une manière générale, vous fournir la possibilité de régler le problème sans avoir à quitter le programme, si c’est possible.

Une dernière petite chose avant de clore ce chapitre : nous avons « traîné » depuis le début la ligne # coding: utf8 en tête de script. Je la mets systématiquement, mais jusqu’à présent, il faut bien avouer qu’elle ne servait à rien. Pour ce dernier script, par contre, cette ligne est nécessaire, car nous utilisons des caractères accentués (non ASCII) dans un des messages. Pour vous en persuader, supprimez-la et exécutez le script. Vous déclencherez une erreur.

En Python2, vous devrez insérer la lettre « u » avant chaque chaîne que vous voulez encoder en UTF-8.

Vous avez également la possibilité d’ajouter la ligne « from __future__ import unicode literals »

#!/usr/bin/python
# coding: utf8
from __future__ import unicode literals

Toutes les chaînes de votre script utiliseront l’encodage unicode que vous avez choisi.

Python gère une centaine de codecs, mais il est fortement conseillé d’opter pour l’UTF-8. C’est le plus universel.

En Python3, UTF-8 étant l’encodage par défaut, le problème ne se pose pas.

VIII - Boa Constructor

À partir de maintenant, nous allons changer de paradigme. Jusqu’à présent, avec Arduino et Python, nous avons tout fait « à la main », et l’interface de notre programme était affichée dans une console, donc en mode texte. Nous allons ici utiliser un système de conception plus évolué pour créer une interface graphique : le « RAD » Boa Constructor. Le principe consiste à faire une maquette de l’interface que vous voulez obtenir en manipulant des objets, et de laisser l’EDI écrire le code correspondant, le reste du traitement étant évidemment à votre charge, il ne faut pas rêver.

Cette interface graphique que nous allons créer peut tout à fait être écrite à la main, car c’est toujours du texte : il n’y a rien de magique. Simplement, la génération automatique d’une partie du code facilitera le développement en procurant un gain de temps certain et en limitant les risques d’erreur. De plus, la conception « wysiwyg » (‘what you see is what you get’ ⇔ ‘ce que vous voyez est ce que vous obtiendrez’) permet de limiter le nombre d’essais et de réglages nécessaires à la réalisation d’une interface correcte. Enfin, et surtout, ça masque d’une certaine manière l’utilisation de wxPython et donc ça supprime la nécessité d’effectuer son apprentissage.

Le côté pervers de ce procédé, et que beaucoup condamnent, réside, entre autres, dans l’utilisation d’un code généré que l’on ne comprend pas forcément et dont on n’est pas maître. Alors, c’est vrai ! mais, si on regarde bien, c’est exactement ce qui se passe quand on utilise une bibliothèque : on n’a pas besoin de comprendre son code, ni de pouvoir le modifier, pour l’utiliser, la seule différence étant qu’ici, le code généré et le script saisi manuellement sont mélangés dans le même fichier. Il faut donc bien séparer « intellectuellement » les deux entités et ne pas toucher au code généré, celui-ci ne devant être modifié qu’au travers du RAD. Cette règle n’est en réalité pas aussi stricte que ça, mais il faut bien savoir ce qu’on fait. Quant au script saisi à la main, il doit simplement satisfaire à la syntaxe Python.

De toute manière, à quoi sert l’informatique : essentiellement à automatiser des tâches, quelles qu’elles soient. La génération automatique de code est une tâche comme une autre et est donc un pur produit de l’informatique.

VIII-A - RAD Boa Constructor

Le RAD (Rapid Application Development ⇔ Développement Rapide d’Application) Boa Constructor, écrit en Python par Riaan Booysen, a pour vocation d’offrir, pour ce langage, une interface de conception proche de celle de « Delphi© », dont les qualités ne sont plus à démontrer. Outre un EDI complet, il fournit un ensemble de composants graphiques paramétrables prêts à l’emploi (boutons, zones de saisie, cases à cocher, etc.) destinés à être déposés sur une fiche, pour permettre la conception visuelle de cette fiche.

Boa Constructor utilise la boite à outil graphique « wxPython » qui interface, pour Python, la boite à outils « wxWidgets » écrite en « C++ ». Il est donc indispensable que wxPython soit installé sur votre machine pour utiliser ce RAD.

Pour ne leurrer personne, ce n’est pas le but, il faut savoir que Boa Constructor n’est pas abouti. Il est actuellement en version « bêta » (0.6.1 et 0.7.2) et rien ne dit que la « version 1 » sortira un jour. Toutefois, il est opérationnel, et, malgré quelques défauts, il permettra d’élaborer des interfaces graphiques (GUI) classiques de bonne qualité. Pour les applications nécessitant une interface sophistiquée, il faudra, comme on dit, mettre les mains dans le cambouis, et instancier à la main les objets graphiques wxPython non fournis par Boa Constructor.

Comme stipulé au début, ce tutoriel n’est pas un cours de programmation. De la même manière, ce n’est pas non plus un mode d’emploi de Boa Constructor, qui mériterait un tutoriel dédié. Je vais simplement décrire les fonctionnalités que nous allons utiliser, au fur et à mesure de leur utilisation, comme je l’ai fait pour le sketch Arduino et pour le script console. Cela permettra à ceux qui le désirent de tester l’outil, et, pourquoi pas, d’en faire un de leurs outils de développement favoris.

Démarrez Boa Constructor. L’interface est composée de trois fenêtres dont la position et les dimensions ne sont pas optimisées. Je vous conseillerai donc de les modifier pour obtenir quelque chose qui ressemble à ça :

Une fois la configuration souhaitée obtenue, que ce soit celle que je vous propose ou non, je vous conseille également de la sauvegarder, à défaut de quoi vous devrez refaire la manipulation à chaque démarrage. Pour cela, il suffit de faire Fenêtre ⇒ Toutes les dimensions ⇒ Enregistrer dans le menu de l’éditeur.

Vous voilà prêt à commencer !

VIII-B - Création d’une application

D’une manière générale, la première des choses à faire quand vous créez une nouvelle application, c’est de créer un dossier destiné à recueillir le fruit de votre travail : pour ce tutoriel, ce sera /chemin/boa/, mais, comme toujours, vous avez le libre choix du nom et de l’emplacement.

Boa Constructor ne gère pas la notion de projets. Pour travailler sur un projet, il faut et il suffit que le fichier exécutable du projet soit ouvert dans l’EDI. Si le projet comporte plusieurs fichiers représentant différents modules, leur présence n’est pas nécessaire dans l’éditeur. Vous n’avez donc besoin d’ouvrir que ceux sur lesquels vous voulez travailler. Chaque fichier est ouvert dans un onglet différent.

Vous pouvez avoir plusieurs projets ouverts simultanément dans l’EDI, la seule limite étant d’ordre pratique : un nombre excessif de fichiers ouverts encombrera inutilement l’EDI et ne facilitera pas le travail. L’onglet actif déterminera le projet qui sera exécuté quand vous cliquerez sur .

La fenêtre principale du RAD Boa Constructor contient, entre autres, la palette des composants regroupés par thème dans des onglets différents.

Une application Boa Constructor standard est multifichier et repose sur la création d’un fichier exécutable accompagné du fichier codant la fenêtre principale, laquelle est un composant wx.frame. Ceci se fait en cliquant sur l’icône . Si cette pratique est parfaitement justifiée dans un projet étoffé comportant plusieurs fenêtres, elle ne l’est pas en ce qui concerne notre petite application. Boa Constructor permet cependant de rendre exécutable le composant wx.frame et donc de créer des applications monofichiers. C’est ce que nous allons faire ensemble.

Dans l’onglet Nouveau, cliquez sur l’icône (wx.Frame) pour créer le squelette de votre nouvelle application.

#Boa:Frame:Frame1

import wx

def create(parent):
return Frame1(parent)

[wxID_FRAME1] = [wx.NewId() for _init_ctrls in range(1)]

class Frame1(wx.Frame):
def _init_ctrls(self, prnt):
wx.Frame.__init__(self, style=wx.DEFAULT_FRAME_STYLE, name='', parent=prnt, title='Frame1', pos=(320, 231), id=wxID_FRAME1, size=(853, 616))

def __init__(self, parent):
self._init_ctrls(parent)

En plus des onglets Shell et Explorateur toujours présents dans l’éditeur, un nouvel onglet a été ajouté : *(Frame1)*. Cet onglet va vous donner accès à tout ce qui concerne le cadre Frame1 par l’intermédiaire de trois autres onglets : Source, Naviguer et Événements, l’onglet Source étant sélectionné. Ce cadre sera la fenêtre de votre application.

La première chose à faire consiste à sauvegarder le code source de ce cadre dans un fichier. Pour ça, dans le menu, faites Fichier⇒Enregistrer sous… :

Sélectionnez le dossier que vous avez créé, saisissez le nom que vous voulez donner à ce fichier puis cliquez sur Enregistrer.

Je vous propose ComLED.py, mais vous pouvez laisser le nom proposé par défaut, Frame1.py, ou opter pour un autre nom à votre convenance.

Cela dit, selon mon expérience personnelle, il est plus facile, pour suivre un tutoriel, de garder la même nomenclature.

Vous pouvez constater que l’onglet Frame1 a été renommé en ComLED.

En l’état, ce script n’est pas exécutable. Il ne contient que le code de base d’une fenêtre vide. Pour le rendre exécutable, dans le menu, faites : Édition⇒Ajouter un exécuteur de module.

Les lignes suivantes sont ajoutées au code existant :

if __name__ == '__main__':
app = wx.PySimpleApp()
frame = create(None)
frame.Show()

app.MainLoop()

Ces lignes doivent rester les dernières de votre script. Grâce à elles, votre module cesse d’être un simple module pour devenir un programme exécutable.

L’instruction wx.PySimpleApp() est marquée deprecated depuis la version « wxPython 2.9 », et, bien qu’elle fonctionne toujours, compatibilité ascendante oblige, il est préférable de la remplacer dorénavant par l’instruction wx.App(). Par conséquent, la ligne :

app = wx.PySimpleApp()

devient :

app = wx.App()

Sous Windows, vous pouvez exécuter directement ce script. Sous GNU-Linux, vous devez préciser, à la première ligne, le chemin vers le dossier contenant l’interpréteur Python, comme vous l’avez fait précédemment pour le script console. De la même manière, je vous propose également d’indiquer au programme d’utiliser l’encodage UTF-8 pour coder les caractères non ASCII : ce n’est pas obligatoire, naturellement, mais, et de plus en plus, vivement conseillé.

#!/usr/bin/python
# coding: utf8

Tout votre script doit impérativement se trouver entre ces deux portions de code, à défaut de quoi il risque de ne pas fonctionner correctement, voire de ne pas fonctionner du tout.

Voici, à titre indicatif, ce que ça donne dans l’éditeur :

Le nom de l’onglet doit à présent se présenter ainsi : *ComLED*, les deux * signifiant que le script a été modifié mais pas encore sauvegardé.

Sauvegardez-le donc en cliquant sur puis cliquez à présent sur pour lancer le script :

Vous venez de créer la fenêtre qui va servir de cadre à votre application : elle ne demande plus qu’à être personnalisée, ce que nous allons faire maintenant. Fermez-la depuis le menu système.

Vous pouvez, pour gagner du temps ultérieurement, enregistrer le fichier ComLED.py sous template.py par exemple. Il vous servira de base de départ pour vous perfectionner dans la réalisation d’interfaces utilisateur.

VIII-C - Réalisation de la maquette

Le principe d’élaboration d’une interface graphique avec un RAD tel que Boa Constructor réside dans l’utilisation de composants prêts à l’emploi mis à disposition dans la palette de composants, et qu’il suffit de déposer sur la fenêtre de prototypage, que j’appellerai « fiche » dans ce tutoriel, représentant la future fenêtre de l’application.

Ces composants, des objets au sens informatique du terme, possèdent des propriétés que vous pourrez paramétrer pour ajuster leur intégration à votre interface, et fournissent des méthodes que vous pourrez utiliser.

VIII-C-1 - Premiers pas

La première des choses à faire est donc d’accéder à cette fiche. Rien de plus simple. Si ce n’est déjà fait, sélectionnez l’onglet du fichier sur lequel vous désirez travailler (ici, ComLed) et cliquez sur l’icône de la barre d’outils de l’éditeur. Si ce fichier ne représente pas une fenêtre, par exemple dans le cas où vous auriez créé un module d’utilitaires pour votre programme, cette icône sera absente.

Quand vous cliquez sur l’icône , vous entrez en mode d’édition graphique, et vous accédez à la fiche. Les icônes et apparaissent dans la barre d’outils de l’éditeur et remplacent les icônes et , inactives jusqu’à présent, de l’inspecteur de propriétés.

Dans ce mode, la saisie manuelle est désactivée dans l’éditeur. Donc, pour quitter le mode d’édition graphique (fiche) et repasser en édition manuelle (source), vous devez obligatoirement cliquer soit sur , soit sur .

La fiche est identique à la fenêtre de l’application que vous venez de fermer. Alors, quand je dis identique, comme vous pouvez le constater, ce n’est pas tout à fait vrai. Elle comporte en effet un quadrillage qui va servir de repère pour déposer les composants, et qui n’existe qu’au niveau de la maquette, heureusement ! Toutes les modifications que vous apporterez à cette fiche seront répercutées à l’identique dans la fenêtre de l’application. C’est ce qui fait l’intérêt du système.

L’accès à la fiche, donc à l’édition graphique, entraîne le peuplement de l’inspecteur de propriétés avec différents paramètres concernant l’objet sélectionné, ici la fenêtre de l’application. C’est en jouant sur ces paramètres que vous allez personnaliser cette fenêtre :

Je vous propose, dans un premier temps, pour vous échauffer, de modifier les propriétés Name et Title. Il suffira pour ça de cliquer sur la ligne contenant la propriété à modifier, puis de remplacer la valeur sélectionnée par la nouvelle valeur. Modifiez les valeurs pour obtenir ceci :

puis cliquez sur pour valider vos modifications, c’est-à-dire les écrire dans le code, et sur pour les sauvegarder dans le fichier.

Quand vous modifiez une propriété « texte » comme Name ou Title, ne saisissez que le texte : le formatage « u’...’ » nécessaire à l’unicode sera ajouté automatiquement.

N’oubliez pas de valider puis de sauvegarder les modifications effectuées dans l’inspecteur de propriétés, sans quoi les résultats obtenus ne seront pas ceux escomptés, et l’édition manuelle du fichier source restera bloquée.

Vous pouvez également, au besoin, abandonner vos modifications en cliquant sur . Attention toutefois, car cette commande n’est pas sélective. Toutes les modifications effectuées dans l’inspecteur de propriétés depuis la dernière validation seront annulées.

Regardez les modifications engendrées sur la fiche, mais également dans le code. Il est important de garder le contact avec ce code, même s’il est généré automatiquement. Il enseigne beaucoup de choses.

VIII-C-2 - Utilisation des composants

Avant de déposer des composants sur la fiche, il convient de préciser ce dont on a besoin. En l’occurrence, nos besoins ne sont pas dispendieux :

- un bouton pour quitter l’application ;

- un bouton pour allumer la LED ;

- un bouton pour éteindre la LED ;

- un voyant pour rendre compte de l’état de la LED.

On pourrait même faire plus simple en utilisant un unique bouton à bascule pour allumer et éteindre la LED. On pourrait également se passer du voyant qui peut ne pas être nécessaire, ainsi que du bouton destiné à quitter l’application, le bouton système de fermeture étant là pour ça. Donc, techniquement, un seul bouton pourrait suffire. Cela dit, il y aura déjà si peu de choses sur la fiche que cette économie de moyens n’a pas de réelle raison d’être. Et puis ceci est un tutoriel. Nous ne cherchons pas forcément l’optimisation à tout prix, quelle qu’elle soit, mais plutôt à utiliser le matériel.

VIII-C-2-a - Sélection et personnalisation des composants

Vous allez donc déposer ces quatre composants sur la fiche.

Dans l’onglet Boutons de la palette de composants, cliquez sur l’icône (wx.lib.buttons.GenButton) puis n’importe où sur la fiche. Répétez cette opération deux fois encore pour avoir trois boutons sur la fiche.

Dans l’onglet Composants de base de la palette de composants, cliquez sur (wx.StaticBitmap) puis quelque part sur la fiche.

Votre maquette devrait avoir un aspect proche de ceci :

Tous ces éléments sont déplaçables et redimensionnables :

- soit à l’aide de la souris ;

- soit en modifiant les paramètres Position et Size dans l’inspecteur de propriétés ;

- soit en utilisant les touches <Maj> ou <Ctrl> conjointement avec les touches fléchées pour modifier respectivement les dimensions ou la position.

Vous pouvez également modifier le texte affiché sur les boutons en modifiant leur propriété Label. Enfin, vous pouvez leur donner un nom explicite, par lequel ils seront connus dans le code, en modifiant leur propriété Name, toujours dans l’inspecteur de propriétés.

Détaillons le processus pour le bouton « genButton1 » :

- sélectionnez le bouton à l’aide de la souris ;

- allez dans l’onglet « Constr » de l’inspecteur de propriétés :

- modifiez les propriétés Label et Name comme suit :

- cliquez sur puis sur pour réactiver le mode graphique ;

- placez le bouton en haut à gauche de la fiche en faisant un glisser-déposer avec la souris pour obtenir quelque chose comme ça :

Répétez ces opérations avec les boutons genButton2 et genButton3 en modifiant leur propriété Label respective en « Éteindre » et « Quitter », et leur propriété Name respective en Eteindre et Quitter. Notez que la valeur de la propriété Name n’est pas une chaîne de caractères mais un nom de variable, et que, comme telle, elle doit respecter les règles de nommage de Python qui, en l’occurrence, interdisent les caractères accentués. Par conséquent, affecter la valeur Éteindre à cette propriété déclencherait une erreur à l’interprétation.

Dans cet exemple, le label et le nom sont (quasiment) identiques, mais ce n’est absolument pas nécessaire. C’est simplement logique et pratique (c’est du moins mon opinion).

Sélectionnez maintenant le composant (wx.StaticBitmap )que vous avez posé sur votre fiche. Ce composant a pour fonction d’afficher une image. Cette image peut provenir de différentes sources et exister sous différents formats. En ce qui nous concerne, nous allons utiliser deux images contenues dans des fichiers PNG (Portable Network Graphics). Ces images étant au format 32 x 32 pixels, nous allons redimensionner ce composant qui, par défaut, est au format 16 x 16 pixels. Retour à l’inspecteur de propriétés :

Cliquez sur le carré bleu en regard de Size, puis paramétrez Height et Width à 32 pixels.

N’oubliez pas de valider vos modifications en cliquant sur .

Déplacez à présent vos composants et redimensionnez la fenêtre pour obtenir quelque chose qui ressemble à ceci :

Cliquez maintenant sur pour enregistrer le fichier, puis sur pour lancer l’application. Peut-être avez-vous oublié de cliquer sur  ? Si la fenêtre de votre application ne reflète pas les derniers changements, ce doit être le cas. Fermez simplement cette fenêtre, validez vos modifications, enregistrez le fichier et relancez le programme : voilà qui est mieux.

Deux constatations s’imposent :

- le composant image a disparu. Alors, en fait non. Simplement, on ne lui a pas encore affecté d’image, et par conséquent il n’affiche rien ;

- de plus, si vous êtes curieux, vous avez pu vous apercevoir que, quand vous cliquez sur un bouton, il ne se passe rien. C’est tout à fait normal, et pour tout dire, rassurant. En effet, il serait pour le moins bizarre que votre application fasse des choses que vous ne lui avez pas demandées de faire : bizarre et inquiétant.

Nous allons donc devoir traiter ces deux points.

Je voudrais avant tout faire une remarque : jusqu’à présent, vous n’avez pas saisi une seule ligne de code, et vous avez déjà la fenêtre définitive, sur le plan interface s’entend, de votre application. Ce n’est quand même pas mal.

Vous m’objecterez que vous avez modifié une ligne de code en bas du script, et ajouté deux lignes d’entête. Certes, mais il s’agit là plus de patchs destinés à combler certaines lacunes de Boa Constructor que de codage à proprement parler. Si vous avez créé le fichier template.py dont je vous ai parlé plus haut, vous n’aurez plus besoin de faire ces retouches, et votre maquette ne demandera réellement plus une ligne de code, du moins si on se contente des composants wxPython fournis par Boa Constructor. Donc, on peut dire que, techniquement, tout ce que vous avez fait pour l’instant, c’est paramétrer quelques composants : point.

Comme je l’ai dit plus haut, il arrive un moment où il faut mettre les mains dans le cambouis.

Par quoi va-t-on commencer ? Par mettre des gants ! Non, ça ne devrait pas être nécessaire ici, du moins si votre clavier est propre. Je vous propose plutôt de nous occuper d’abord du bouton « Quitter ». Comme il sera probablement présent dans la plupart de vos applications, vous pouvez même l’ajouter au contenu du fichier template.py.

VIII-C-2-b - Les gestionnaires d’évènement

Pour qu’un bouton déclenche une action, quelle qu’elle soit, il faut satisfaire deux conditions :

- cliquer sur ce bouton : cliquer sur un bouton constitue ce que l’on appelle un « évènement » (event en anglais). Il est important de comprendre que l’évènement est à la base du fonctionnement de l’interface utilisateur. Sans évènement, il ne se passe rien (du moins en première approximation). Un évènement, ça peut être beaucoup de choses. Par exemple :

- un clic de souris (droite ou gauche),

- le survol d’un élément par la souris,

- la frappe au clavier (une touche ou plusieurs touches simultanément),

- le changement du contenu d’une zone de saisie de texte,

- liste non exhaustive, loin de là…

- avoir associé à cet évènement le code déterminant l’action à effectuer : cette association se fait à l’aide d’une procédure appelée « gestionnaire d’évènement ».

Cliquer sur un bouton, je suppose que vous savez faire. Nous allons donc nous concentrer sur le second point.

Là encore, Boa Constructor va nous faciliter la tâche, dans la mesure où il va écrire pour nous le squelette du gestionnaire d’évènement, nous laissant la responsabilité d’écrire le code associé.

Marche à suivre :

- cliquez sur le bouton « Quitter » (de la maquette, bien sûr) ;

- dans l’inspecteur de propriété, affichez l’onglet Evts (Events). La colonne de gauche vous propose différents types d’évènements, dont « ButtonEvent », le premier, qui est, vous l’avez deviné, celui qui va nous intéresser. Cliquez dessus ;

- la colonne de droite affiche alors la liste des évènements, liés aux boutons, disponibles fournis par wxPython (qui interface wxWidgets).

- comme apparemment il n’y en a qu’un, wx.EVT_BUTTON, pas besoin de se casser la tête, faites un double clic dessus :

- le gestionnaire d’évènement OnQuitterButton est associé automatiquement à cet événement. Le nom des gestionnaires d’évènement commence toujours par On et se termine par le nom générique de l’objet auquel il est lié, ici Button. Ce nom est construit à partir du texte que vous avez attribué à la propriété Name, ce qui vous indique deux choses :

- il est important que le nom de cette propriété soit représentatif de l’objet qu’il concerne car cela permet de repérer facilement le gestionnaire d’évènement associé à cet objet dans le code,

- il est cohérent que ce nom commence par une majuscule pour améliorer la lisibilité du nommage ;

- cliquez sur pour insérer ce gestionnaire dans votre code. N’oubliez pas que, tant que vous n’avez pas cliqué sur cette icône, le code n’est pas mis à jour, ce qui vous permet d’ailleurs d’annuler vos modifications.

À partir de maintenant, vous êtes obligé d’aller voir du côté du code. Je suppose que vous avez déjà dû y jeter un coup d’œil, ne serait-ce que pour le fun, mais à présent, comme il vous faut modifier le « source », ce n’est plus une option. Deux fragments de code, complémentaires, ont été ajoutés :

1. Le premier réalise l’association entre le bouton Quitter, l’évènement traité à savoir wx.EVT_BUTTON, et le gestionnaire d’évènement self.OnQuitterButton à l’aide de la procédure Bind() :

self.Quitter.Bind(wx.EVT_BUTTON, self.OnQuitterButton, id=wxID_FBASEQUITTER)

2. Cette ligne, placée immédiatement après le code d’insertion du bouton, ne nécessite aucune intervention de votre part, Boa Constructor disposant de tous les éléments nécessaires à son élaboration ;

3. Le second va vous permettre d’implémenter le gestionnaire d’évènement OnQuitterButton() en lui-même. Il s’agit d’une déclaration de méthode tout à fait standard, recevant les arguments imposés pour un gestionnaire d’évènement. L’appel de cette méthode lors d’un clic sur le bouton entraîne l’exécution de l’instruction event.Skip() (littéralement, « sauter l’évènement ») dont la fonction est, si on peut dire, de ne rien faire.

La présence par défaut de l’instruction event.Skip() est justifiée par la nécessité de fournir un code valide. Une déclaration de méthode sans code à exécuter entraînerait automatiquement une erreur à l’exécution en raison d’une indentation incorrecte du bloc suivant. Or, en cours de développement, il n’est pas forcément pertinent de coder les gestionnaires d’évènement au fur et à mesure de l’ajout de contrôles à une fiche, la première raison étant qu’on peut être amené à supprimer un ou plusieurs de ces contrôles, et que par conséquent les codes concernés auront été écrits pour rien.

def OnQuitterButton(self, event):
event.Skip()

- Contrairement au fragment précédent, votre intervention est ici nécessaire pour remplacer cette instruction provisoire par le code que vous souhaitez voir exécuter lorsque l’utilisateur clique sur le bouton.

Comme vous pouvez encore le constater, mis à part le codage de l’action à réaliser, et qu’il ne peut pas deviner, Boa Constructor s’est occupé de tout. Vous savez maintenant comment implémenter un gestionnaire d’évènement, ce que vous allez vous empresser de faire pour les deux autres boutons.

Revenons à notre bouton « Quitter ». Je lève enfin le suspens concernant l’utilisation de ce bouton. On va s’en servir pour quitter l’application. Oh ! Est-ce possible ? Bon, soyons sérieux. L’instruction utilisée dans de nombreux langages pour quitter une application est Close(). Python faisant preuve de solidarité, cette instruction est également valable ici. Utilisons-la :

def OnQuitterButton(self, event):
self.Close()

Modifiez votre code en conséquence. Sauvegardez le fichier et exécutez le script. Vous devriez constater que votre bouton « Quitter » est à présent fonctionnel. Au passage, je vous fais remarquer que c’est la première vraie ligne de code que vous saisissez.

Je vous propose, pour contrôle, une petite récapitulation de ce qui a été fait jusqu’à maintenant.

#!/usr/bin/python
# coding: utf8
#Boa:Frame:FBase

import wx
import wx.lib.buttons

def create(parent):
return FBase(parent)

[wxID_FBASE, wxID_FBASEALLUMER, wxID_FBASEETEINDRE, wxID_FBASEQUITTER,
wxID_FBASESTATICBITMAP1,
] = [wx.NewId() for _init_ctrls in range(5)]

class FBase(wx.Frame):
def _init_ctrls(self, prnt):
# generated method, don't edit
wx.Frame.__init__(self, id=wxID_FBASE, name=u'FBase', parent=prnt,
pos=wx.Point(542, 387), size=wx.Size(227, 168),
style=wx.DEFAULT_FRAME_STYLE, title=u'Commande LED')
self.SetClientSize(wx.Size(223, 135))

self.Allumer = wx.lib.buttons.GenButton(id=wxID_FBASEALLUMER,
label=u'Allumer', name=u'Allumer', parent=self, pos=wx.Point(16,
16), size=wx.Size(86, 31), style=0)

self.Eteindre = wx.lib.buttons.GenButton(id=wxID_FBASEETEINDRE,
label=u'\xc9teindre', name=u'Eteindre', parent=self,
pos=wx.Point(16, 56), size=wx.Size(86, 31), style=0)

self.Quitter = wx.lib.buttons.GenButton(id=wxID_FBASEQUITTER,
label=u'Quitter', name=u'Quitter', parent=self, pos=wx.Point(128,
96), size=wx.Size(86, 31), style=0)
self.Quitter.Bind(wx.EVT_BUTTON, self.OnQuitterButton,
id=wxID_FBASEQUITTER)

self.staticBitmap1 = wx.StaticBitmap(bitmap=wx.NullBitmap,
id=wxID_FBASESTATICBITMAP1, name='staticBitmap1', parent=self,
pos=wx.Point(152, 32), size=wx.Size(32, 32), style=0)

def __init__(self, parent):
self._init_ctrls(parent)

def OnQuitterButton(self, event):
self.Close()

if __name__ == '__main__':
app = wx.App()
frame = create(None)
frame.Show()

app.MainLoop()

Si vous avez fait le choix de suivre précisément ce tutoriel, votre code devrait être très proche de celui présenté ci-dessus : seules quelques valeurs numériques sont susceptibles d’être différentes dans la mesure où il y a peu de chances pour que vous ayez positionné les widgets et redimensionné la fenêtre exactement comme moi.

Si vous avez fait cavalier seul, ça ne pose aucun problème tant que vous vous y retrouvez. Toutefois, si vous êtes débutant, ce n’est pas forcément la meilleure idée. Il est préférable, de mon point de vue, de mener à bien cette réalisation puis d’apporter ensuite toutes les modifications qui vous plairont, et de travailler de préférence sur une copie, pour pouvoir revenir si besoin est à une version fonctionnelle, ou tout au moins pour garder une référence.

Quoi qu’il en soit, je vais vous laisser bosser tout seul pour créer les gestionnaires d’évènement associés aux deux autres boutons. Laissez pour le moment l’instruction event.Skip() fournie par défaut, car nous n’avons pas encore tous les éléments pour écrire le code attaché. C’est d’ailleurs intéressant car ça démontre parfaitement la raison d’être de cette instruction.

VIII-C-2-c - Affichage d’une image à partir d’un fichier

Avant d’aller plus loin, vous allez devoir télécharger les deux images dont nous allons nous servir. Pour cela, il vous suffit de cliquer sur ces deux liens :

- voyant_on.png :

- voyant_off.png :

Vous allez récupérer ces deux fichiers dans le dossier cible de vos téléchargements, qui s’appelle assez souvent « Téléchargements », et les copier, pour simplifier les chemins, dans le dossier où se trouve votre application ComLED.py, à savoir /chemin/boa/ si vous avez utilisé le nom que je vous ai proposé.

De retour sur votre fiche, cliquez sur le composant wxStaticBitmap puis affichez l’onglet Constr de l’inspecteur de propriétés, si ce n’est déjà fait :

Le nom actuel de ce composant, StaticBitmap1, n’est pas vraiment parlant. Vous allez le renommer « Voyant », de la même façon que pour renommer les boutons, comme nous avons vu plus haut. Je n’insiste pas là-dessus.

Plus intéressant, vous allez maintenant cliquer sur la ligne Bitmap :

Cliquez sur le bouton à droite de la ligne, un dialogue de sélection de fichier s’ouvre. Déplacez-vous vers le dossier de votre application et sélectionnez le fichier « voyant_off.png » que vous venez de télécharger :

Validez puis cliquez sur pour enregistrer les changements. Sauvegardez le fichier avec puis exécutez le programme en cliquant sur  :

Voilà ! Votre composant image affiche le voyant éteint, et toujours sans saisir de code . Je sais, je suis lourd à toujours vous faire cette remarque, mais c’est pour bien vous persuader de l’intérêt d’utiliser ce RAD. C’est quand même l’un des buts de ce tutoriel, et si vous utilisez Boa Constructor ne serait-ce que de temps en temps, j’estimerai ce but atteint.

L’inspecteur de propriétés de Boa Constructor ne permet pas de saisir « à la main » le nom du fichier. Il ouvre automatiquement une boite de dialogue. L’inconvénient est que cette boite de dialogue renvoie le chemin absolu du fichier. Cela rend l’application peu portable.

En ce qui me concerne, quand c’est possible naturellement, je mets tout ce qui concerne mon application dans le même dossier, et je remplace manuellement dans l’éditeur les chemins absolus par des chemins relatifs à ce dossier.

Dans notre cas, si vous avez suivi mes propositions, il vous suffit donc de remplacer la chaîne « /home/…/boa/voyant_off.png » par « voyant_off.png », dans la ligne qui commence par « self.Voyant = ».

En ce qui concerne la maquette, l’IHM (interface homme-machine), comme on dirait si on n’avait pas peur du ridicule, on a fait le tour. Les composants sont en place et paramétrés, on ne devrait pas avoir à y revenir.

À partir de maintenant, pour ce que je vais appeler le code fonctionnel, on va revenir à une programmation qui va peut-être vous être plus habituelle.

VIII-D - La liaison série

Là, c’est facile. Vous venez de le faire pour votre script console. Nous avons détaillé cette procédure à cette occasion et nous n’avons pas besoin d’y revenir. La marche à suivre est exactement la même. Vous pouvez faire un copier-coller sans aucune arrière-pensée. La seule question que vous pourriez vous poser, c’est « Où vais-je placer ce code ? ». Je vous ferai le même commentaire qu’au début de la section Python de ce tutoriel : il suffit de placer ce code avant les procédures qui vont l’utiliser.

Donc, pour ne pas se casser la tête, on va le placer en début de script, après l’« en tête » et la section concernant les importations, à savoir :

#!/usr/bin/python
# coding: utf8
#Boa:Frame:FBase

import wx
import wx.lib.buttons

Voici le code à insérer :

import serial
import time

portCom = serial.Serial()
portCom.port = '/dev/ttyACM0'
portCom.baudrate = 115200
portCom.open()

Ce qui va donner ce début de script :

#!/usr/bin/python
# coding: utf8
#Boa:Frame:FBase

import wx
import wx.lib.buttons
import serial
import time

portCom = serial.Serial()
portCom.port = '/dev/ttyACM0'
portCom.baudrate = 115200
portCom.open()

À partir d’ici, la liaison série est opérationnelle : portCom est instancié, paramétré et ouvert.

Le sketch Arduino n’ayant pas changé, vous devez envoyer « on » pour allumer la LED et « off » pour l’éteindre. L’instruction à utiliser pour cela est la même, portCom.write(commande), la bibliothèque utilisée étant toujours pySerial, à ceci près que le passage de l’argument à l’aide d’une variable n’est plus nécessaire, du moins dans un premier temps. Nous y aurons recours plus loin pour optimiser le code.

Comment déclencher l’exécution de cette instruction en cliquant sur les boutons « Allumer » et « Éteindre » : tout simplement en remplaçant l’instruction event.Skip() par portCom.write() dans leurs gestionnaires d’évènement que vous avez créés plus haut. Voici ce que ça donne :

def OnAllumerButton(self, event):
portCom.write("on")

def OnEteindreButton(self, event):
portCom.write("off")

Mettez votre code à jour, enregistrez le fichier et… n’oubliez pas de raccorder votre Arduino UNO s’il ne l’est pas, à défaut de quoi l’ouverture du port déclenchera une exception et le programme ne démarrera pas. Vous pouvez maintenant lancer votre programme et le tester. Les boutons sont opérationnels, la LED s’allume et s’éteint à la demande.

VIII-E - Traitement du retour d’informations

Comme pour la liaison série, nous allons simplement reprendre le traitement effectué pour le programme console, en l’adaptant au nouveau contexte. Si vous vous souvenez, nous avions traité le retour comme suit :

time.sleep(0.05)
nbCar = portCom.in_waiting
retour = portCom.read(nbCar)
print(retour)

Les explications ayant déjà été fournies, je ne m’attarde pas. La seule modification à apporter se trouve au niveau de l’affichage du retour, c’est-à-dire la ligne :

print(retour)

Dans le nouveau contexte, il ne s’agit plus d’afficher sur l’écran le texte renvoyé par l’Arduino UNO, encore qu’on puisse parfaitement le faire, mais d’utiliser ce texte pour déterminer si le voyant doit être « allumé » ou non, c’est-à-dire si le composant image doit afficher le fichier « voyant_on.png » ou le fichier « voyant_off.png », ce qui est visuellement plus efficace.

Nous avons vu plus haut comment afficher une image à partir d’un fichier, le reste est un simple branchement conditionnel qui ne devrait vous poser aucun problème. Je vous propose donc, en lieu et place de la ligne ci-dessus, de saisir le code suivant :

if (retour == "ledOn"):
bitmap=wx.Bitmap(u"voyant_on.png",wx.BITMAP_TYPE_PNG)
else:
bitmap=wx.Bitmap(u"voyant_off.png",wx.BITMAP_TYPE_PNG)
self.Voyant.SetBitmap(bitmap)

Contrairement à ce que permet le script console, il n’est pas possible ici d’envoyer autre chose que « on » ou « off » à l’Arduino UNO : ces valeurs sont codées « en dur ». On ne recevra donc jamais le message indiquant qu’une commande inconnue a été reçue par l’Arduino UNO. Vous pouvez donc modifier le sketch en conséquence si vous le désirez, mais ce n’est pas obligatoire pour la suite du tutoriel. Dans la vraie vie, il faudrait le faire, car ce n’est pas bon du tout de laisser du code inutile dans un programme.

Comme vous ne pourrez recevoir que « ledOn » ou « ledOff », il vous suffit de tester et traiter la réception de l’une des deux valeurs dans une clause if et de traiter le cas contraire dans la clause else associée.

Le code complet du gestionnaire d’évènement, pour le bouton « Allumer » devient :

portCom.write("on")
time.sleep(0.05)
nbCar = portCom.in_waiting
retour = portCom.read(nbCar)
if (retour == "ledOn"):
bitmap=wx.Bitmap(u"voyant_on.png",wx.BITMAP_TYPE_PNG)
else:
bitmap=wx.Bitmap(u"voyant_off.png",wx.BITMAP_TYPE_PNG)
self.Voyant.SetBitmap(bitmap)

et vous remplacez bien sûr « on » par « off » dans la première ligne pour le gestionnaire d’évènement du bouton « Éteindre ».

Complétez vos gestionnaires d’évènement comme montré ci-dessus, enregistrez le fichier et lancez l’application. Cliquez sur le bouton « Allumer ». Si la LED s’allume bien, signe que l’ordre est bien transmis, le voyant reste de marbre. Comment cela se fait-ce ?

Eh bien ! cela vient de notre sketch Arduino. Souvenez-vous, j’ai fait allusion à ce problème quand nous l’avons écrit. Ne fermez pas votre application, nous allons régler ça au niveau de l’Arduino UNO.

Pour des raisons de présentation à l’écran, j’avais utilisé l’instruction Serial.println() au lieu de Serial.print() pour que l’affichage de « ledOn » ou de « ledOff » à l’écran se fasse après un saut de ligne. Cette instruction fonctionne en incorporant le caractère « retour chariot » à la chaîne passée en argument. Comme ce caractère n’est pas imprimable, il n’apparaissait pas à l’écran. Mais il était quand même là.

Or, à présent, nous n’affichons plus l’une de ces deux chaînes, mais nous les comparons à une chaîne donnée en référence, à savoir « ledOn » : si la chaîne reçue est « ledOn », la comparaison renvoie True et on « allume » le voyant, dans le cas contraire, on l’« éteint ». Le problème est qu’on ne reçoit jamais la chaîne « ledOn ». Quand le sketch envoie « ledOn », il envoie en réalité « ledOn + retour chariot », et donc la comparaison renvoie False parce que « ledOn » est différent de « ledOn + retour chariot ».

Vous devez donc remplacer le fragment de sketch suivant :

commande = "";
if (digitalRead(led) == HIGH)
{
Serial.println("ledOn");
}
else
{
Serial.println("ledOff");
}

par celui-ci :

commande = "";
if (digitalRead(led) == HIGH)
{
Serial.print("ledOn");
}
else
{
Serial.print("ledOff");
}

et téléverser le nouveau sketch dans votre Arduino UNO.

Cliquez à nouveau sur les boutons de votre application : ça y est, tout fonctionne. Enfin, normalement. Bon, j’aurais pu vous éviter ce mélodrame : il n’était pas nécessaire au tutoriel. Je pense cependant qu’il n’était pas inutile de mettre en évidence ce genre d’erreurs auxquelles il n’est pas rare d’être confronté, et qu’il n’est pas toujours facile de détecter. Pour être honnête, c’est ce qui m’est arrivé.

En première approximation, on pourrait dire que le contrat est honoré. Nous sommes parvenus à nos fins. Allons-nous nous quitter là-dessus ? Ben non ! Sinon je ne poserais pas la question.

VIII-F - Pour aller plus loin

Est-ce qu’on peut faire mieux ? Ben oui ! Sinon je ne poserais pas la question.

VIII-F-1 - Optimisation du code

Si vous regardez bien le code de gestion des évènements des boutons « Allumer » et « Éteindre », une chose devrait vous sauter aux yeux : ils sont quasiment identiques. En informatique, on essaye au maximum d’éviter les redondances ou les doublons. Le même code étant utilisé deux fois, vous allez en faire une procédure qui sera appelée par les deux gestionnaires.

Ici, c’est très facile. La procédure sera « copie conforme » du code commun aux deux gestionnaires. Ce n’est pas toujours aussi simple. Voici la procédure que j’ai nommée majLed(self) pour « mise à jour LED » que je vous propose :

def majLed(self)
time.sleep(0.05)
nbCar = portCom.in_waiting
retour = portCom.read(nbCar)
if (retour == "ledOn"):
bitmap=wx.Bitmap(u"voyant_on.png",wx.BITMAP_TYPE_PNG)
else:
bitmap=wx.Bitmap(u"voyant_off.png",wx.BITMAP_TYPE_PNG)
self.Voyant.SetBitmap(bitmap)

Nous allons placer cette procédure avant les gestionnaires d’évènement qui vont l’utiliser, comme d’habitude, ce qui va donner :

def majLed(self)
time.sleep(0.05)
nbCar = portCom.in_waiting
retour = portCom.read(nbCar)
if (retour == "ledOn"):
bitmap=wx.Bitmap(u"voyant_on.png",wx.BITMAP_TYPE_PNG)
else:
bitmap=wx.Bitmap(u"voyant_off.png",wx.BITMAP_TYPE_PNG)
self.Voyant.SetBitmap(bitmap)

def OnAllumerButton(self, event):
portCom.write("on")
self.majLed()

def OnEteindreButton(self, event):
portCom.write("off")
self.majLed()

On peut même faire encore mieux en passant la commande en paramètre à la procédure majLed(self) qui devient majLed(self, commande) et modifier le code ci-dessus de la façon suivante :

def majLed(self, commande)
portCom.write(self, commande)
time.sleep(0.05)
nbCar = portCom.in_waiting
retour = portCom.read(nbCar)
if (retour == "ledOn"):
bitmap=wx.Bitmap(u"voyant_on.png",wx.BITMAP_TYPE_PNG)
else:
bitmap=wx.Bitmap(u"voyant_off.png",wx.BITMAP_TYPE_PNG)
self.Voyant.SetBitmap(bitmap)

def OnAllumerButton(self, event):
self.majLed("on")

def OnEteindreButton(self, event):
self.majLed("off")

Voici le code opérationnel définitif (pour l’instant) :

#!/usr/bin/python
# coding: utf8
#Boa:Frame:FBase

import wx
import wx.lib.buttons
import serial
import time

portCom = serial.Serial()
portCom.port = '/dev/ttyACM0'
portCom.baudrate = 115200
portCom.open()

def create(parent):
return FBase(parent)

[wxID_FBASE, wxID_FBASEALLUMER, wxID_FBASEETEINDRE, wxID_FBASEQUITTER,
wxID_FBASEVOYANT,
] = [wx.NewId() for _init_ctrls in range(5)]

class FBase(wx.Frame):
def _init_ctrls(self, prnt):
# generated method, don't edit
wx.Frame.__init__(self, id=wxID_FBASE, name=u'FBase', parent=prnt,
pos=wx.Point(542, 387), size=wx.Size(227, 168),
style=wx.MINIMIZE_BOX | wx.CLOSE_BOX | wx.CAPTION,
title=u'Commande LED')
self.SetClientSize(wx.Size(223, 135))

self.Allumer = wx.lib.buttons.GenButton(id=wxID_FBASEALLUMER,
label=u'Allumer', name=u'Allumer', parent=self, pos=wx.Point(16,
16), size=wx.Size(86, 31), style=0)
self.Allumer.Bind(wx.EVT_BUTTON, self.OnAllumerButton,
id=wxID_FBASEALLUMER)

self.Eteindre = wx.lib.buttons.GenButton(id=wxID_FBASEETEINDRE,
label=u'\xc9teindre', name=u'Eteindre', parent=self,
pos=wx.Point(16, 56), size=wx.Size(86, 31), style=0)
self.Eteindre.Bind(wx.EVT_BUTTON, self.OnEteindreButton,
id=wxID_FBASEETEINDRE)

self.Quitter = wx.lib.buttons.GenButton(id=wxID_FBASEQUITTER,
label=u'Quitter', name=u'Quitter', parent=self, pos=wx.Point(128,
96), size=wx.Size(86, 31), style=0)
self.Quitter.Bind(wx.EVT_BUTTON, self.OnQuitterButton,
id=wxID_FBASEQUITTER)

self.Voyant = wx.StaticBitmap(bitmap=wx.Bitmap(u'voyant_off.png',
wx.BITMAP_TYPE_PNG), id=wxID_FBASEVOYANT, name=u'Voyant',
parent=self, pos=wx.Point(152, 32), size=wx.Size(32, 32),
style=0)

def __init__(self, parent):
self._init_ctrls(parent)

def OnQuitterButton(self, event):
self.Close()

def majLed(self, commande):
portCom.write(commande)
time.sleep(0.05)
nbCar = portCom.in_waiting
retour = portCom.read(nbCar)
if (retour == "ledOn"):
bitmap=wx.Bitmap(u"voyant_on.png",wx.BITMAP_TYPE_PNG)
else:
bitmap=wx.Bitmap(u"voyant_off.png",wx.BITMAP_TYPE_PNG)
self.Voyant.SetBitmap(bitmap)

def OnAllumerButton(self, event):
self.majLed("on")

def OnEteindreButton(self, event):
self.majLed("off")

if __name__ == '__main__':
app = wx.App()
frame = create(None)
frame.Show()

app.MainLoop()

Techniquement, dans ce code, il n’y a que 18 lignes saisies à la main. Boa Constructor s’est chargé du reste.

VIII-F-2 - Style de la fenêtre

Je ne sais pas si vous l’avez remarqué, mais la fenêtre de votre application est redimensionnable. Il y a des applications pour lesquelles c’est nécessaire, d’autres où c’est simplement utile, et d’autres, c’est le cas de celle-ci, où c’est franchement dommageable. Agrandissez ou rapetissez votre fenêtre et vous le constaterez par vous-même. Comme je suis d’un naturel serviable, je le fais pour vous. Que ce soit ça :

ou ça :

ce n’est pas très joli ! Si je vous en parle, c’est qu’on peut aisément empêcher cela.

Sélectionnez la fiche, et dans l’onglet Constr de l’inspecteur de propriétés, déroulez la propriété Style (clic sur le carré bleu).

Comme vous pouvez le constater, un certain nombre de constantes, dont le rôle est de modifier l’affichage ou le comportement de la fenêtre, sont prédéfinies. Par défaut, quand vous instanciez un objet wx.Frame, une fenêtre donc, la constante DEFAULT_FRAME_STYLE est affectée à la propriété Style. Cette constante, définissant le comportement d’une fenêtre d’usage général, est en elle-même le regroupement de plusieurs constantes définissant chacune un comportement spécifique :

DEFAULT_FRAME_STYLE = MINIMIZE_BOX | MAXIMIZE_BOX | RESIZE_BORDER | SYSTEM_MENU | CAPTION | CLOSE_BOX | CLIP_CHILDREN

Nous n’avons pas besoin de tout ça. Je vous propose donc de mettre cette propriété globale à False et de mettre individuellement à True les propriétés suivantes :

- CAPTION : affiche le titre de la fenêtre ;

- CLOSE_BOX : affiche le bouton système de fermeture ;

- MINIMIZE_BOX : active le bouton système de réduction de la fenêtre.

Ce qui donne :

Comme d’habitude, suivi de suivi de , mais ça doit être devenu un réflexe maintenant : vérifiez le résultat.

La seule chose visible directement est que le bouton système « Maximiser » a disparu de la barre de titre. Vous constaterez également que la fenêtre n’est plus redimensionnable avec la souris.

Je vous engage bien entendu à tester d’autres configurations. Une petite remarque : contrairement à ce qui se passe quand vous élaborez votre maquette, les modifications que vous effectuez ici ne sont pas répercutées visuellement sur la fiche. Elles n’apparaîtront qu’au lancement du programme.

Avant d’effectuer ces tests, je vous conseille d’implémenter un bouton « Quitter » sur votre fiche. En effet, en fonction de vos réglages, vous pouvez vous retrouver avec une fenêtre n’ayant plus de bouton système de fermeture, voire plus de barre de titre du tout. Si en plus vous avez activé la propriété STAY_ON_TOP, vous voilà avec une application « infermable » proprement, « inmasquable » et indéplaçable. Sur le bureau, ça fait tache !

VIII-F-3 - En bonus

Comme pour votre script « console », je vous propose ici, sans le commenter, le même script intégrant la gestion d’une éventuelle erreur à l’ouverture du port. Cette gestion s’accompagne de l’affichage d’une fenêtre de dialogue en cas d’erreur détectée. Je vous fournis ci-après les deux scripts : celui de l’application et celui de la fenêtre de dialogue. Vous devrez les mettre dans le même dossier, avec les fichiers PNG.

Code de l’application :

#!/usr/bin/python
# coding: utf8
#Boa:Frame:FBase

import wx
import wx.lib.buttons
import serial
import time

portCom = serial.Serial()
portCom.port = '/dev/ttyACM0'
portCom.baudrate = 115200

def create(parent):
return FBase(parent)

[wxID_FBASE, wxID_FBASEALLUMER, wxID_FBASEETEINDRE, wxID_FBASEQUITTER,
wxID_FBASEVOYANT,
] = [wx.NewId() for _init_ctrls in range(5)]

class FBase(wx.Frame):
def _init_ctrls(self, prnt):
# generated method, don't edit
wx.Frame.__init__(self, id=wxID_FBASE, name=u'FBase', parent=prnt,
pos=wx.Point(542, 376), size=wx.Size(227, 168),
style=wx.DEFAULT_FRAME_STYLE | wx.CLOSE_BOX | wx.MINIMIZE_BOX | wx.CAPTION,
title=u'Commande LED')
self.SetClientSize(wx.Size(223, 135))

self.Allumer = wx.lib.buttons.GenButton(id=wxID_FBASEALLUMER,
label=u'Allumer', name=u'Allumer', parent=self, pos=wx.Point(16,
16), size=wx.Size(86, 31), style=0)
self.Allumer.Bind(wx.EVT_BUTTON, self.OnAllumerButton,
id=wxID_FBASEALLUMER)

self.Eteindre = wx.lib.buttons.GenButton(id=wxID_FBASEETEINDRE,
label=u'\xc9teindre', name=u'Eteindre', parent=self,
pos=wx.Point(16, 56), size=wx.Size(86, 31), style=0)
self.Eteindre.Bind(wx.EVT_BUTTON, self.OnEteindreButton,
id=wxID_FBASEETEINDRE)

self.Quitter = wx.lib.buttons.GenButton(id=wxID_FBASEQUITTER,
label=u'Quitter', name=u'Quitter', parent=self, pos=wx.Point(128,
96), size=wx.Size(86, 31), style=0)
self.Quitter.Bind(wx.EVT_BUTTON, self.OnQuitterButton,
id=wxID_FBASEQUITTER)

self.Voyant = wx.StaticBitmap(bitmap=wx.Bitmap(u'voyant_off.png',
wx.BITMAP_TYPE_PNG), id=wxID_FBASEVOYANT, name=u'Voyant',
parent=self, pos=wx.Point(152, 32), size=wx.Size(32, 32),
style=0)

def __init__(self, parent):
self._init_ctrls(parent)
try:
portCom.open()
except serial.SerialException:
import Dialog1
dlg = Dialog1.Dialog1(self)
try:
dlg.ShowModal()
finally:
dlg.Destroy()

def OnQuitterButton(self, event):
self.Close()

def majLed(self, commande):
portCom.write(commande)
time.sleep(0.05)
nbCar = portCom.in_waiting
retour = portCom.read(nbCar)
if (retour == "ledOn"):
bitmap=wx.Bitmap(u"voyant_on.png",wx.BITMAP_TYPE_PNG)
else:
bitmap=wx.Bitmap(u"voyant_off.png",wx.BITMAP_TYPE_PNG)
self.Voyant.SetBitmap(bitmap)

def OnAllumerButton(self, event):
self.majLed("on")

def OnEteindreButton(self, event):
self.majLed("off")

if __name__ == '__main__':
app = wx.App()
frame = create(None)
frame.Show()

app.MainLoop()

Code de la fenêtre de dialogue :

#Boa:Dialog:Dialog1

import wx

def create(parent):
return Dialog1(parent)

[wxID_DIALOG1, wxID_DIALOG1BITMAPBUTTON1, wxID_DIALOG1STATICTEXT1,
wxID_DIALOG1STATICTEXT2,
] = [wx.NewId() for _init_ctrls in range(4)]

class Dialog1(wx.Dialog):
def _init_ctrls(self, prnt):
# generated method, don't edit
wx.Dialog.__init__(self, id=wxID_DIALOG1, name='', parent=prnt,
pos=wx.Point(461, 347), size=wx.Size(365, 189),
style=wx.DEFAULT_DIALOG_STYLE, title='Dialog1')
self.SetClientSize(wx.Size(361, 156))

self.bitmapButton1 = wx.BitmapButton(bitmap=wx.Bitmap(u'ok_btn.png',
wx.BITMAP_TYPE_PNG), id=wxID_DIALOG1BITMAPBUTTON1,
name='bitmapButton1', parent=self, pos=wx.Point(140, 120),
size=wx.Size(80, 28), style=wx.BU_AUTODRAW)
self.bitmapButton1.Center(wx.HORIZONTAL)
self.bitmapButton1.Bind(wx.EVT_BUTTON, self.OnBitmapButton1Button,
id=wxID_DIALOG1BITMAPBUTTON1)

self.staticText1 = wx.StaticText(id=wxID_DIALOG1STATICTEXT1,
label='staticText1', name='staticText1', parent=self,
pos=wx.Point(8, 8), size=wx.Size(203, 18), style=0)
self.staticText1.SetLabelText(u"Probl\xe8me \xe0 l'ouverture du port")
self.staticText1.SetFont(wx.Font(10, wx.SWISS, wx.NORMAL, wx.BOLD, True,
u'Noto Sans'))
self.staticText1.SetForegroundColour(wx.Colour(117, 27, 27))

self.staticText2 = wx.StaticText(id=wxID_DIALOG1STATICTEXT2,
label='staticText2', name='staticText2', parent=self,
pos=wx.Point(32, 40), size=wx.Size(300, 72), style=0)
self.staticText2.SetLabelText(u'Les deux causes suivantes sont les plus probables:\n\t- La carte n\'est pas connect\xe9e\n \t- Le port syst\xe8me n\'est pas "/dev/ttyACM0"\n')

def __init__(self, parent):
self._init_ctrls(parent)

def OnBitmapButton1Button(self, event):
self.Close()

Vous devrez également télécharger le fichier PNG de l’image affichée par le bouton de la boite de dialogue et le coller dans le dossier de votre application.

La façon la plus simple de tester cette nouvelle fonctionnalité est de déconnecter votre carte et de lancer l’application.

Je n’ai pas personnalisé cette fenêtre de dialogue, vous laissant le soin de le faire si vous le souhaitez. Vous devrez l’enregistrer sous « Dialog1.py » pour qu’elle soit reconnue par l’application. Vous pouvez bien sûr changer ce nom, à condition de faire la modification équivalente dans le script « ComLED.py ». À vous de jouer.

IX - Annexe

IX-A - Écriture classique du GUI

Comme promis, pour ceux qui ne souhaitent pas essayer Boa Constructor mais sont malgré tout intéressés par cette réalisation, voici le script python classique équivalent au programme ci-dessus. Je vous recommande toutefois de lire la partie concernant Boa Constructor pour les explications, lesquelles sont aisément transposables. Vous pouvez télécharger ici les fichiers correspondant à cette version, au format 7z.

#!/usr/bin/env python
# coding: utf8

# Importation des bibliothèques

import wx
import wx.lib.buttons
import serial
import time

# Instanciation et initialisation du port série

portCom = serial.Serial()
portCom.port = '/dev/ttyACM0'
portCom.baudrate = 115200

# Dérivation d'une instance de la classe wx.Frame

class FBase(wx.Frame):
def __init__(self, parent, title):
wx.Frame.__init__(self, parent, title=title, size=(227,168))
self.Show(True)

# Gestion des exceptions : ici, on teste un éventuel problème de liaison série

try:
portCom.open()
except serial.SerialException:
import Dialog1
dlg = Dialog1.Dialog1(self)
try:
dlg.ShowModal()
finally:
dlg.Destroy()

# Création des wxWidgets : ici, 3 boutons (wxButton)  et 1 voyant (wxStaticBitmap)

allumerBtn = wx.Button(self, wx.ID_ANY, "Allumer", pos=(16, 16), size=(86, 31))
eteindreBtn = wx.Button(self, wx.ID_ANY, "Éteindre", pos=(16, 56), size=(86, 31))
quitterBtn = wx.Button(self, wx.ID_ANY, "Quitter", pos=(128, 96), size=(86, 31))
voyant = wx.StaticBitmap(self, wx.ID_ANY, bitmap = wx.Bitmap("voyant_off.png",
wx.BITMAP_TYPE_PNG), pos=(152, 32), size=(32, 32), style=0, name="voyant")

# Définition des fonctions générales : ici, une seule fonction

def majLed(commande):
portCom.write(commande)
time.sleep(0.1)
nbCar = portCom.in_waiting
retour = portCom.read(nbCar)
if (retour == "ledOn"):
bitmap = wx.Bitmap("voyant_on.png",wx.BITMAP_TYPE_PNG)
else:
bitmap = wx.Bitmap("voyant_off.png",wx.BITMAP_TYPE_PNG)
voyant.SetBitmap(bitmap)

# Définition des gestionnaires d'évènement

def onAllumerBtn(event):
majLed("on")

def onEteindreBtn(event):
majLed("off")

def onQuitterBtn(event):
self.Close()

# Connexion entre les boutons et leur gestionnaire d'évènement respectif

allumerBtn.Bind(wx.EVT_BUTTON, onAllumerBtn, allumerBtn)
eteindreBtn.Bind(wx.EVT_BUTTON, onEteindreBtn, eteindreBtn)
quitterBtn.Bind(wx.EVT_BUTTON, onQuitterBtn, quitterBtn)

# Lancement de l'application

app = wx.App(False)
frame = FBase(None, "Commande LED")
app.MainLoop()

IX-B - Autorisation d’accès au port série

Pour pouvoir accéder au port série sous GNU-Linux, vous devez faire partie du groupe dialout. Si, lors de votre connexion, vous obtenez un message qui ressemble à :

[Errno 13] could not open port /dev/ttyACM0: [Errno 13] Permission denied: '/dev/ttyACM0

ça veut dire que ce n’est pas le cas.

Pour vérifier, saisissez la ligne suivante dans un terminal :

groups

Vous devriez obtenir quelque chose qui ressemble à ça :

user@debian:~$ groups
user tty cdrom floppy sudo audio dip video plugdev netdev bluetooth lpadmin scanner
user@debian:~$

Pour remédier à cela, saisissez la ligne suivante dans un terminal :

sudo usermod -a -G dialout user

ligne dans laquelle vous remplacerez user par votre nom d’utilisateur, c’est-à-dire le nom figurant avant l’arobase (@) du prompt. Vous devrez, bien entendu, disposer du mot de passe utilisateur.

Redémarrez votre session pour que les changements soient pris en compte et vérifiez à nouveau avec la commande groups :

user@debian:~$ groups
user tty dialout cdrom floppy sudo audio dip video plugdev netdev bluetooth lpadmin scanner
user@debian:~$

L’entrée dialout a été ajoutée à la liste des groupes dont vous faites partie.

IX-C - Identifiant de port

Dans ce tutoriel, le port que j’utilise pour la connexion série est le port /dev/ttyACM0. Je n’y suis pour rien, il est attribué automatiquement par le système. Outre le fait, déjà mentionné, que, en fonction du nombre de cartes connectées, le nom du port peut être différent, par exemple /dev/ttyACM3, il peut ne pas respecter cette forme. En effet, dans des versions moins récentes de l’IDE Arduino, le nommage du port est de la forme /dev/ttyUSB0. De plus, si vous êtes sous Windows, le nommage du port aura la forme COM1.

Donc, le plus simple pour connaître le nom du port auquel votre carte est connectée, est la suivante :

- sous GNU-Linux :

- sous Windows© :

- et, comme d’habitude, désolé pour les utilisateurs de MacOS : ne disposant pas de cette plateforme, je ne peux pas vous offrir une capture d’écran, mais vous aurez compris le principe.

Ne tenez pas compte du statut de la connexion situé en bas de la fenêtre. Il n’est pas mis à jour automatiquement quand vous branchez votre carte et l’information peut donc être erronée. C’est le cas, par exemple, quand vous connectez une seconde carte. Il indiquera le dernier port que vous avez sélectionné, pas celui attribué à la carte nouvellement connectée. Et c’est normal : vous seul savez avec quelle carte vous voulez travailler, et c’est donc à vous de sélectionner le port correspondant à la carte sur laquelle vous voulez téléverser votre sketch. Soyez attentif à ce que vous faites à ce niveau, car vous risquez d’écraser un sketch si vous vous trompez de carte.

X - Conclusion

Vous voici arrivé à la fin de ce tutoriel. J’espère que vous avez eu autant de plaisir à le lire que j’en ai eu à l’écrire. Pour vous remercier d’être allé jusqu’au bout, voici une mise en pratique un peu plus sophistiquée, qui va vous permettre de faire varier la luminosité d’une LED en manipulant, à l’aide de la souris, un curseur virtuel affiché sur l’écran de votre PC. Vous trouverez dans cette archive le script Python et le sketch Arduino de cette application, et ci-dessous, une capture du GUI correspondant :

J’ai essayé de faire en sorte que ce tutoriel soit facile à suivre, que vous soyez néophyte dans l’utilisation de la plateforme Arduino ou en programmation Python. Comme précisé au début, l’objet de ce tutoriel n’est pas d’enseigner la programmation, mais plutôt de fournir un ensemble près à l’emploi et extrapolable facilement pour l’adapter à une utilisation spécifique différente de celle fournie en exemple. J’espère avoir atteint mon but.

N’hésitez pas à expérimenter. C’est le moyen le plus efficace pour avancer. L’avantage avec l’informatique, du moins dans le contexte présent, c’est-à-dire une fois la carte Arduino UNO acquise et tant qu’on peut se passer de circuits annexes, c’est que l’expérimentation est gratuite, ou du moins, elle ne coûte que du temps. Elle permet d’explorer de nombreuses pistes avant de passer à des réalisations concrètes.

Comme je l’ai dit précédemment, Boa Constructor, bien que tout à fait opérationnel, de mon point de vue, pour des projets raisonnables, n’est pas abouti. Cependant je l’aime bien. Il allie une ergonomie s’inspirant de celle de Delphi, que j’utilise depuis sa version 1, à l’utilisation d’un langage très en vogue et très intéressant : Python.

Je n’ai pas encore fait le tour du RAD Boa Constructor, mais, jusqu’à présent, je n’ai pas constaté de problème de fond. Toutefois, je suis obligé d’admettre qu’il y a des petits dysfonctionnements agaçants, parmi lesquels :

- la coloration syntaxique : elle disparaît parfois aléatoirement sur des portions de code. Le rafraîchissement de l’éditeur ne résout pas le problème. La seule solution que j’ai trouvée est de quitter et de relancer l’EDI ;

- les boutons (wx.button), (wx.BitmapButton) et (wx.ToggleButton) ne sont pas déplaçables à la souris, et c’est la raison qui fait que je n’ai pas utilisé le bouton (wx.button) pour ce tutoriel, le but étant avant tout de montrer la facilité du placement des widgets sur la fiche. On peut bien entendu, et c’est heureux, les positionner et les redimensionner comme on le souhaite, soit en saisissant leurs coordonnées/dimensions dans l’inspecteur de propriétés, soit à l’aide des touches ctrl/maj et des touches ←, ↑, → et ↓, mais ce n’est pas satisfaisant. J’ignore la raison de cet état de fait. Ce sont à priori les seuls composants à poser ce problème. C’est dommage car, sur le plan esthétique, ils sont bien plus modernes que les boutons que j’ai utilisés ici ;

- problèmes d’affichage incomplet dans les comboBox ;

- etc.

Ces problèmes ne sont pas rédhibitoires, et pourraient probablement être réglés assez facilement, mais comme le développement de ce RAD est un peu en stand-by, je ne sais pas ce que réserve l’avenir. Ça reste malgré tout un outil intéressant à plus d’un titre, et qui peut rendre de bons services à qui voudra faire l’effort, très raisonnable, de le prendre en main.

XI - Remerciements

Je remercie vivement f-leb pour sa relecture technique ainsi que chrtophe, Auteur et Vincent PETIT pour leurs remarques pertinentes.

Je remercie également f-leb pour sa relecture orthographique.