Photobooth : inputs GPIO, affichage LCD et impression

LA GESTION DES INPUTS SUR LES GPIO

Les résistances Pull-up/Pull-down

Si une GPIO configurée en entrée n’est connectée à rien, alors elle sera « flottante ». Cela signifie que l’état de la GPIO est instable puisqu’elle n’est connectée à rien jusqu’à l’appui d’un switch réalisant le contact à la masse ou au +3.3v. Dans cet état instable, les interférences électriques induites par l’alimentation secteur influent fortement la valeur de la GPIO.

Pour éviter que la GPIO ne « flotte », il est nécessaire de la fixer à une valeur stable. Pour cela, on utilise une résistance (10Kohms est couramment utilisée) et deux options sont envisageables :

  1. Résistance pull-down : connectée à la masse par l’intermédiaire de la résistance, la GPIO est maintenue à l’état bas ;
  2. Résistance pull-up : connectée au +3.3v par l’intermédiaire de la résistance, la GPIO est maintenue à l’état haut.
Résistance pull-up et résistance pull-down permettant de maintenir une GPIO en INPUT à un état stable
Résistance pull-up et résistance pull-down permettant de maintenir une GPIO en INPUT à un état stable

Il est également possible de mettre en place une résistance pull-up/pull-down de manière logicielle puisque le module RPi.GPIO permet de configurer les GPIO du Raspberry à l’aide des instructions suivantes :

La méthode basique du polling

Si vous avez lu l’article sur la machine à blagues Chuck NORRIS, pour lire l’état des GPIOs en entrée, nous avions utilisé la méthode de polling qui consiste à tester à intervalles réguliers, l’état du GPIO à un instant précis. Dans le script Python, les instructions afférentes sont les suivantes :

Cette méthode est perfectible car un changement d’état sur la GPIO peut potentiellement échapper au test si le script Python sonde l’état de la broche au « mauvais moment », c’est-à-dire juste avant ou juste après le pulse électrique. Par ailleurs, lorsque l’on souhaite attendre l’appui sur un switch, le polling s’inscrit dans une boucle infinie.

Malgré l’instruction time.sleep(0.01), la méthode de polling peut s’avérer lourde pour la ressource CPU.

La méthode des interruptions

L’autre méthode, plus élégante et surtout plus efficace consiste à utiliser les interruptions, déclenchées à la détection des fronts (edge) sur le GPIO. Plutôt que de mesurer la valeur de la GPIO, nous allons ici nous intéresser aux changements d’états, appelés évènements. Ces évènements désignant le passage d’un état à l’autre, deux cas de figure se distinguent :

  • De HIGH vers LOW : falling edge ;
  • De LOW vers HIGH : rising edge.

Pour ne pas manquer un appui sur le bouton pendant que le script est occupé à exécuter un autre traitement, 3 fonctions sont à notre disposition :

  1. wait_for_edge() ;
  2. event_detected() ;
  3. une fonction de « callback », exécutée dans un thread parallèle au script principal lorsqu’un front est détecté.

La fonction wait_for_edge()

La fonction wait_for_edge() permet de bloquer l’exécution du script jusqu’à ce qu’un edge soit détecté. Nous avons eu recours à cette fonction au sein de la fonction waitForRedButtonPush(delai) (voir paragraphe sur le Bouton rouge ci-dessus) pour attendre l’appui :

On en conviendra, c’est nettement plus élégant que la boucle while de la méthode de polling évoquée ci-dessus et surtout la ressource CPU est épargnée.

Remarques :

  • La méthode permet de détecter :
    • Les fronts montants : GPIO.RISING ;
    • Les fronts descendants : GPIO.FALLING ;
    • Les fronts montants ET descendants : GPIO.BOTH.
  • Le paramètre timeout permet de fixer un délai (en ms) au bout duquel le script passera à l’instruction suivante : dans ce cas, le retour de la fonction wait_for_edge() est None ;
  • Le paramètre bouncetime permet d’éviter le phénomène de rebond : à la détection d’un edge, les fronts suivants seront ignorés pendant le délai spécifié pour éviter le parasitage.

Le phénomène de rebond est parasitage lié à la génération de plusieurs edge alors que le switch n’est appuyé qu’une seule fois. Il est également possible d’utiliser un condensateur pour déparasiter les rebonds.

La fonction event_detected()

La fonction add_event_detected() joue le même rôle que la boucle while de polling mais elle apporte la garantie de détection de chaque évènement, même à un moment où le CPU serait occupé.

Les fonctions Threaded callback

La librairie RPi.GPIO permet de déclencher l’exécution de fonctions callback en parallèle du script principal (dans un nouveau thread), immédiatement après la détection d’un front sur la GPIO en input. Les lignes suivantes extraites du script photobooth.py permettent de définir les fonctions (stopPhotobooth() et haltPhotobooth()) à appeler sur détection de fronts montants sur les GPIO connectées aux switchs jaune et bleu :

En dupliquant les lignes ci-dessus, il est possible d’ajouter plusieurs fonctions de callback sur une même GPIO. Un seul thread étant créé pour l’exécution des fonctions de callback, celles-ci s’exécuteront séquentiellement (et non pas de manière concurrente) dans l’ordre de où elles sont ajoutées. Enfin, il est possible de désactiver la détection de front sur une GPIO à l’aide de l’instruction :

Quelle que soit l’issue de la fonction waitForRedButtonPush(delai), cette instruction est exécutée pour libérer la GPIO RED_BUTTON et éviter les erreurs du type :

En effet, la fonction waitForRedButtonPush(delai) commence par activer la détection des edges sur le GPIO RED_BUTTON avec l’instruction :

Lorsque que cette fonction s’appelle récursivement, la nouvelle instance de la fonction active également la détection de front alors que l’instance appelante l’avait déjà activé sur le même GPIO. Enfin, précisons que la fonction add_event_detect est également utilisée dans la librairie COINSELECTOR.py pour capter les pulses sur la ligne COIN du monnayeur. Exécutée dans un thread concurrent, la fonction de callback coinEventHandler(self, pin_number) (voir extrait de code ci-dessous) permet de gérer l’insertion de pièce par un utilisateur à n’importe quel instant, parallèlement à l’exécution du script principal.

L’écran LCD HD44780 20×04 et l’interface I2C

Nous avons déjà abordé le fonctionnement de l’écran LCD HD44780 dans un précédent article (voir article sur la machine à blagues Chuck NORRIS), nous ne nous y attarderons donc pas davantage pour se concentrer sur l’interface I2C. En effet, l’écran peut être interfacé de deux manières pour la transmission de données (en plus de l’alimentation et le contraste de l’écran) :

  1. en reliant directement les broches Data sur les GPIOs du Raspberry, ce qui suppose d’hypothéquer 4 GPIOs en mode 4 bits voire 8 GPIOs en mode 8 bits ;
  2. par l’intermédiaire d’un module I2C n’exigeant que deux lignes pour les données : SDL et SDA.

Pour ce projet, les connexions sont déjà nombreuses sur les GPIOs et nous retenons l’utilisation d’un module I2C pour simplifier l’interfaçage de l’écran LCD HD44780 20×04 (20 colonnes, 4 lignes).

Ecran LCD HD44780 avec module I2C
Ecran LCD HD44780 avec module I2C

Puisqu’il s’agit d’utiliser le bus I2C, commençons par rappeler ce que signifie ce mot galvaudé. Un bus est un dispositif de transmission de données partagé entre plusieurs composants par l’intermédiaire d’un canal de transmission commun. Le concept englobe donc à la fois :

  • le matériel sur lequel repose la communication ;
  • le protocole qui régit les échanges.

On distingue généralement un bus d’une part d’une liaison point à point, qui ne concerne que deux composants qui en ont l’usage exclusif, et d’autre part d’un réseau, qui implique des participants indépendants entre eux, c’est-à-dire pouvant fonctionner de manière autonome, et qui comprend plusieurs canaux permettant des communications simultanées.

Comme l’illustre le schéma ci-dessous, un bus comporte 3 sous-ensembles :

  1. Les lignes de données pour envoyer les données à transmettre ;
  2. Les lignes d’adresse pour indiquer quel composant doit émettre ou recevoir l’information présente sur le bus de données ;
  3. Les lignes de contrôle pour indiquer l’opération à effectuer.
Bus d'échanges de données
Bus d’échanges de données

Maintenant que l’on sait à peu près de quoi on parle, revenons aux GPIOs. Sur certaines d’entre elles, le Raspberry propose l’activation de bus/protocole de communication, à savoir :

  • I2C (Inter-Integrated Circuit) : il s’agit d’un bus série synchrone bidirectionnel half-duplex. Plusieurs équipements (master/slave) peuvent être connectés au bus, à l’aide de 2 lignes :
    • SDA (Serial Data Line) : données bidirectionnelles (GPIO n°3) ;
    • SCL (Serial Clock Line) : horloge de synchronisation bidirectionnelle (GPIO n°5) ;
  • SPI (Serial Peripheral Interface) : bus série synchrone full-duplex. La communication entre plusieurs équipements (schéma master/slaves) est réalisée à l’aide de 4 signaux logiques :
    • SCLK : Serial Clock (GPIO n°23) ;
    • MOSI : Master Output, Slave Input (GPIO n°19) ;
    • MISO : Master Input, Slave Output (GPIO n°21) ;
    • SS : Slave Select.

Parmi ces deux des bus série fonctionnant selon le schéma maitre/esclave, nous allons utiliser le bus I2C. Commençons tout d’abord par l’activer à l’aide de l’utilitaire raspi-config dans le menu « 5 Interfacing Options » puis redémarrer le Raspberry. Pour la transmission des données, le protocole de communication I2C s’appuie sur l’adresse des composants connectés au bus. Nous devons donc déterminer l’adresse de l’écran. Une fois l’ensemble d’outil I2C installé à l’aide de la commande sudo apt-get install i2c-tools, nous disposons de l’utilitaire i2cdetect permettant de détecter les esclaves présents sur le bus I2C :

Scan du bus I2C pour déterminer les adresses des périphériques présents sur le bus
Scan du bus I2C pour déterminer les adresses des périphériques présents sur le bus

L’adresse I2C du LCD est donc dans notre cas : 3F. Cette adresse sera utilisée dans la librairie Python RPi_I2C_driver.py destinée à piloter l’écran, dans laquelle on adaptera la ligne suivante :

La librairie RPi_I2C_driver.py

Commençons par télécharger la libraire RPi_I2C_driver.py (Disponible à l’adresse https://github.com/emcniece/rpi-lcd). L’exemple fourni (example.py) permet de comprendre sans grande difficulté comment exploiter les fonctionnalités de la librairie. Cette librairie s’appuyant sur le module smbus (import smbus), il devra être installé à l’aide de la commande :

L’affichage à obtenir est le suivant :

Informations affichées sur l'écran LCD HD44780 20x04
Informations affichées sur l’écran LCD HD44780 20×04

L’extrait de code ci-dessous illustre ainsi comment afficher les chaines sur les différentes lignes, charger en mémoire un « custom symbol » (ici, le symbole €) pour ensuite l’afficher sur les digits adéquats.

L’utilisateur pouvant introduire des pièces à n’importe quel moment, l’affichage de la dernière ligne CREDIT sera prise en charge par un thread destiné à actualiser en continu le crédit courant. Un zoom sur le code afférent est fourni ci-dessous.

Les variables moneybox et mylcd étant initialement créées dans le script principal, il est nécessaire de les déclarer avec le préfixe global dans la classe checkAndUpdateCredit(threading.Thread) afin qu’elles puissent être manipulées par le thread.

L’impression thermique

Rappels sur la connexion et résolution des problèmes électriques

Comme nous l’avions déjà fait avec la machine à blagues Chuck NORRIS (voir le numéro 2 du magazine), l’impression des photos sera réalisée à l’aide d’une imprimante série. La distribution Raspbian ayant évolué depuis, l’interfaçage est nettement plus simple : la désactivation du login shell et des messages du noyau envoyés sur le port série est désormais possible via l’outil raspi-config (lancer l’utilitaire à l’aide de la commande sudo raspi-config, puis naviguer dans les menus 5 Interfacing options > P6 serial). Pour tester le bon fonctionnement de l’imprimante, il est possible d’imprimer un « self-test ticket » sur lequel sera renseigné la table de caractère ainsi qu’un certain nombre d’informations techniques relatives au périphérique. Pour cela, il suffit de maintenir le bouton « feed paper » enfoncé à la mise sous tension. Lors du tout premier test, l’imprimante était provisoirement reliée à l’alimentation 5V à l’aide de câbles Dupont. Comparés à ceux fournis avec l’imprimante, les câbles Dupont sont nettement plus fins et leur résistance électrique de facto, plus importante (voir détails précédemment évoqués dans le chapitre « Problème d’alimentation du raspberry »). Comme le montre l’image ci-dessous (partie gauche), la capacité de chauffe du périphérique ainsi alimenté est atténuée et le résultat dégradé.

Capacité de chauffe de l'imprimante réduite avec des câbles d'alimentation trop fins
Capacité de chauffe de l’imprimante réduite avec des câbles d’alimentation trop fins

Heureusement, la connectique fournie avec l’imprimante permet d’obtenir un qualité d’impression satisfaisante (à droite sur la photo ci-dessus) et le problème est rapidement résolu. En revanche, il est possible qu’un autre phénomène vienne perturber l’impression selon le matériel que vous utilisez… Plutôt que de formuler de longues phrases sibyllines, je vous propose d’observer l’image ci-dessous…

Perturbations de la ligne TX par des interférences induites par l'alimentation low-cost de Raspberry
Perturbations de la ligne TX par des interférences induites par l’alimentation low-cost de Raspberry

Sur cette photo se trouvent les impressions obtenues à l’aide de la librairie py-thermal-printer (disponible à l’adresse https://github.com/luopio/py-thermal-printer) dans deux contextes différents :

  • A gauche : le raspberry était alimenté avec l’alimentation noire sans marque ;
  • A droite : le raspberry était alimenté avec l’alimentation blanche de marque SAMSUNG.

Identifier l’origine du phénomène n’étant pas nécessairement triviale, je vous livre l’explication sans détour… Si l’impression est dégradée (caractères inattendus et image décalée) cela signifie que la transmission des données (à imprimer) sur la ligne TX (entre le Raspberry et l’imprimante) n’est pas fiable. Et si la transmission n’est pas fiable, c’est parce qu’elle subit des perturbations (sur la ligne TX et GND) dues aux interférences émises… par l’alimentation (sans marque et de qualité moyenne) du Raspberry !

éloigner l’imprimante de l’alimentation du Raspberry suffit à supprimer les interférences, c’est d’ailleurs pour cela que le problème n’était pas survenu sur la machine à blague Chuck NORRIS. Mais pour ce projet, tout doit être intégré à l’intérieur du photobooth et nous sommes contraints de recourir à une alimentation de qualité (tel qu’un chargeur SAMSUNG) pour résoudre le problème.

Utiliser une librairie Python 2 depuis un script Python 3 : le module PRINT.py

Pour exploiter le potentiel de l’imprimante série depuis le script Python, nous allons installer les librairies suivantes :

Téléchargeons la librairie mise à disposition par Adafruit à l’aide de la commande :

Parmi les fichiers présents dans le répertoire Python-Thermal-Printer créé à l’issue de la commande précédente, nous allons plus particulièrement nous intéresser au fichier Adafruit_Thermal.py. C’est ce fichier que nous allons utiliser depuis notre script photobooth.py. Cette bibliothèque est développée en Python 2 et l’absence de shebang indique que c’est l’interpréteur /usr/bin/python (sous-entendu, version 2). Le script photobooth.py est codé quant à lui avec la version 3 du langage et son exécution fait appel fait appel à l’interpréteur en version 3 comme le précise le shebang # !/usr/bin/python3.

Ainsi, si l’on importe directement la librairie Adafruit_Thermal.py dans le script photobooth.py, le code de la librairie sera également parsé avec l’interpréteur version 3, ce qui conduira naturellement à la levée d’erreurs puisque du code Python 2 ne se conformes pas se conforme pas aux exigences de syntaxe introduites par Python 3. L’instruction import Adafruit_Thermal.py en début du script photobooth.py ne peut donc pas être envisagée. Pour s’affranchir d’un laborieux travail d’évolution de la librairie vers la version 3 du langage, nous allons contourner ce problème en créant un module PRINT.py écrit en Python 2 dans lequel sera importé le fichier Adafruit_Thermal.py. Le code chargé de l’impression (en utilisant les fonctionnalités de la librairie) sera déporté dans ce module. Enfin, il suffira d’appeler le script PRINT.py depuis photobooth.py avec l’interpréteur Python 2 à l’aide de l’instruction :

Le shebang est l’information placée sur la toute première ligne du fichier (exemple : # !/usr/bin/python) permettant d’indiquer le chemin de l’interpréteur à utiliser pour exécuter les instructions.

L’instruction subprocess.call (contrairement à subprocess.popen(), subprocess.call() bloque l’exécution du script appelant jusqu’au la fin du processus appelé) permet de créer un nouveau processus chargé d’exécuter la commande passée en paramètres : ici, il s’agit de lancer l’interpréteur Python auquel on fournit en argument le script PRINT.py, auquel on fournit en argument, le nom du fichier à imprimer.

Traitement de l’image avant impression

La librairie PRINT.py permet traitement de l’image et l’impression, notamment, il s’agit de :

  • Image horizontal flip pour effet miroir puis retour avant capture ;
  • Preview et affichage photo miniature à la résolution de l’écran ;
  • Résolution au maximum avant capture ;
  • 2claircissage de l’image avant impression (fonction image.point() et lambda).

LANCER LE SCRIPT PHOTOBOOTH.PY AU DEMARRAGE

Editer le fichier /etc/rc.local à l’aide de la commande :

Et ajouter (avant la ligne exit 0) la ligne suivante :

Le caractère « & » permet de lancer le script autostartphotobooth.sh en tâche de fond. Editons dès à présent le script autostartphotobooth.sh (nano autostartphotobooth.sh) et ajoutons les lignes suivantes :

La gestion des images

Dans cet ultime chapitre, nous allons passer en revue un certain nombre de notions relatives à l’affichage et à la manipulation des images. Tout d’abord, pour un affichage sur l’écran HDMI, les modifications suivantes seront apportées dans le fichier /boot/config.txt :

L’affichage graphique est réalisé à l’aide de la bibliothèque Pygame qui sera installée à l’aide de la commande suivante :

Assez simple à aborder, nous ne nous attarderons pas sur l’utilisation de Pygame. La lecture des deux premiers cours OpenClassroom sur le sujet vous suffira pour mettre en place rapidement l’affichage rudimentaire utilisé dans le photobooth. Intéressons-nous tout de même rapidement aux lignes suivantes extraites du script photobooth.py :

Une fois la fenêtre Pygame (il s’agit d’un objet de classe Surface) affichée en plein écran, le terminal sera masqué et les messages affichés sur la sortie standard ne seront plus visibles. Passée en paramètre au script photobooth.py, l’option –debug permettra de désactiver l’affichage de la fenêtre Pygame sans occulter les messages affichés par les instructions print() au fil de l’exécution du script. Cette option se révèlera bien utile pour débuguer le code. Côté camera, la capture d’image fera l’objet d’ajustements au fil de l’exécution d’une session photo pour améliorer l’expérience utilisateur. Lorsque la caméra est à sa résolution maximum (s’agissant de ma PiCamera v2 : 2592×1944), le ratio largeur/hauteur (≈ 1.3) ne correspond pas à celui de l’écran (≈ 1.6) et l’affichage du flux vidéo laissent apparaitre des bandes noires assez peu esthétiques.

Les ajustements opérés chronologiquement sont les suivants :

  1. la prévisualisation de la camera sera réalisée :
    1. à la résolution de l’écran (800×480) afin que le flux vidéo utilise la totalité de l’écran ;
    2. avec un « horizontal flip », permettant de réaliser l’affichage en reproduisant l’effet d’un miroir, plus naturel à l’utilisation du photobooth ;
  2. les clichés seront capturés avec la résolution maximum de la caméra pour des photos de qualité (l’adaptation est réalisée juste avant la capture, pendant l’affichage de l’écran SMILE ;
  3. un copie miniature de l’image capturée sera réalisée à la résolution 800×480 pour affichage à l’écran ;
  4. pour finir, l’image haute résolution est envoyée au script PRINT.py pour impression une fois les manipulations effectuées :
    1. redimensionnement de l’image à 384×288 (384 étant la largeur maximum d’impression) ;
    2. éclaircissement de l’image pour un meilleur rendu sur le papier thermique.

Extrait de la fonction shooting() du script photobooth.py :

Extrait du script PRINT.py :

 

 

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *