Shopping List Machine : Python, Code-barres EAN13, SQLite et imprimante thermique

A l’ère du « drive », l’intérêt de cette Shopping List Machine peut laisser dubitatif mais une chose est sûre : au travers de la réalisation de ce projet, vous en apprendrez beaucoup sur des notions variées. Les concepts impliqués sont denses et ne pourront être que survolés mais l’essentiel nécessaire pour se lancer est ici synthétisé.

La machine Shopping List composée d’un Raspberry, d’un scanner sans fil, d’une enceinte et d’une imprimante thermique

Mais avant d’entrer dans les détails techniques, formulons les exigences de la Shopping List Machine…

Spécifications de la Shopping List Machine

Un exemple de Shopping List
Un exemple de Shopping List

La Shopping List Machine doit rester simple et pratique. Elle doit permettre traquer les codes-barres des produits en explorant librement les placards de la cuisine : pas question de faire des aller-retour vers la machine pour chacun des produits. Parallèlement, un retour de la part du Raspberry quant à l’exécution des traitements est incontournable, tout particulièrement au moment du scan d’un article. En effet, comment savoir si l’article est reconnu par la machine ? Un écran encombrant le plan de travail de la cuisine pour afficher des messages n’est clairement pas adapté. Nous allons plutôt faire parler le Raspberry pour annoncer ces messages, audibles même à l’autre bout de la cuisine. Enfin, le scanner sera le seul périphérique à utiliser pour l’ensemble des actions : ajouter des articles à la liste courante, imprimer la liste de courses ou encore réinitialiser la liste. Cerise sur le gâteau : une grille de sudoku sous la liste d’articles pour patienter au cas où l’on choisirait encore, la caisse qui avance moins vite que les autres.

Le code-barres EAN13

Un code-barres est un symbole représentant une donnée numérique ou alphanumérique. Les différentes symbologies sont nombreuses et pour cette brève introduction, nous nous limiterons à une description succincte du code-barres le plus couramment utilisé sur les produits de la vie quotidienne : le code-barres unidimensionnel EAN13 (European Article Numbering). Le numéro EAN13 permet d’identifier de manière univoque un article. Représentation graphique du numéro qu’il surplombe, le code-barres EAN13 est destiné à la lecture automatisée par un lecteur optique facilitant le contrôle du flux des marchandises.

Le code barre EAN13 comportent 13 chiffres. Chaque chiffre est représenté par l’alternance des 4 barres verticales noires et blanches, d’épaisseur variable. Chaque chiffre comporte nécessairement 2 barres noires et 2 barres blanches. Cet ensemble de 4 barres est lui-même dessiné sur une surface divisée en 7 bandes verticales de même largeur appelées « modules ». Ainsi, les barres peuvent être plus ou moins épaisses (de 1 à 4 modules consécutifs) et un même chiffre peut être représenté de plusieurs façon différentes, ce sont les ensembles d’encodage au sein desquels chaque chiffre possède une représentation unique.

Prenons un exemple concret, avec le code-barres ci-dessous :

Encodage d'un code EAN13 en code-barres
Encodage d’un code EAN13 en code-barres

Comme indiqué plus haut, le symbole est la représentation de la séquence de chiffres 3175681851849, dont la signification est la suivante :

  • Les deux premiers chiffres représentent le code pays (de 30 à 37 pour la France) ;
  • Les cinq suivants représentent l’identifiant de la société ;
  • Les cinq derniers chiffres représentent l’identifiant du produit ;
  • Le dernier chiffre est le check digit, il permet de s’assurer de l’exactitude des chiffres précédents selon une formule définie.

A la lecture de l’image ci-dessus, on remarquera que le 13ième chiffre n’est pas représenté dans le symbole. En effet, il est déduit à partir des ensembles d’encodage utilisé pour les 6 chiffres de la partie gauche. Les zones de gardes permettent quant à elle de calibrer le lecteur optique.

Le lecteur de code-barres

Scanner code-barres sans fil et sa documentation technique comportant des code-barres de configuration
Scanner code-barres sans fil et sa documentation technique comportant des code-barres de configuration

Après avoir connecté le scanner USB, affichons les messages du noyau depuis le démarrage, voici un extrait du retour de la commande dmesg :

Le lecteur code-barres est un périphérique USB. A l’aide des informations « new full-speed USB device number 6 » et « Keyboard [Sycreader Wireless]« , on comprend que le périphérique est relié au fichier 6 et qu’il est vu comme un clavier par le système. La commande lsusb permet d’obtenir des informations sur les bus USB du système et les matériels qui y sont connectés. Dans ce cas présent, davantage d’informations pourront être obtenus à l’aide de la commande :

Comme l’indique le retour des commandes précédentes, le lecteur de code-barres est un périphérique d’entrée et son fonctionnement est comparable à celui d’un clavier : une fois scanné, le code-barres est décodé et la chaine de caractère correspondante est envoyée à l’ordinateur sur lequel il est connecté. Le scanner utilisé est un modèle sans fil, ce qui répond aux exigences de simplicité de la machine : on peut scanner les articles présents dans un rayon de 30m (couverture annoncée par la documentation du scanner).

Scan d'un code-barres : le scanner est en quelque sorte un clavier permettant d'accélérer la saisie des codes EAN
Scan d’un code-barres : le scanner est en quelque sorte un clavier permettant d’accélérer la saisie des codes EAN

Selon le modèle, il se peut qu’en scannant un paquet de céréales Trésor, la chaine de caractères envoyée ne soit pas 5050083458255 comme attendu mais « (à(àà_»’(_é((« .

En observant le clavier de son ordinateur, on comprend rapidement ce qui se produit :

Correspondance des chiffres et caractères sur le clavier
Correspondance des chiffres et caractères sur le clavier

Le scanner envoie le caractère bas de la touche du chiffre attendu. Pour contourner ce problème, nous allons utiliser une fonction de conversion. Cette fonction prendra en paramètre le caractère à convertir et retournera le caractère correspondant. Le mapping sera réalisé à l’aide d’un dictionnaire (structure équivalente à une liste de couple clé/valeur).

Le référentiel d’articles en local

Bien entendu, une fois qu’un article sera scanné et que son identifiant sera envoyé au Raspberry, il sera question de « reconnaitre » l’article correspondant.
S’appuyer sur une API offrant le service est la première idée qui vient à l’esprit : on envoie un numéro EAN et on obtient le descriptif du produit associé en retour. Plusieurs API de ce type sont disponibles sur Internet mais je n’en ai trouvé aucune véritablement complète… à moins d’être payante. Dans ces conditions, je renonce donc à l’utilisation d’une API et je me résous à constituer mon propre référentiel pour gérer localement les données relatives aux produits.
S’agissant d’un faible volume de donnée (une centaine d’articles), on peut envisager d’embarquer les données dans l’application à l’aide d’un fichier texte. Toutes les données enregistrées sont accessibles de manière séquentielle et l’on peut effectuer des recherches sous réserve que les critères de recherche ne soient pas trop complexes. En effet, un simple fichier montrera très vite ses limites dès lors qu’on souhaitera :

  • effectuer des sélections et des tris sur les données ;
  • gérer des volumes de données importants ;
  • relier les données entre elles par des relations hiérarchiques ;
  • gérer les accès parallèles de plusieurs utilisateurs.

Néanmoins, avec ce type de BDD élémentaire (et non relationnelle), on sera très rapidement confronté à des problèmes de structuration de données et de performances.

Pour embarquer les données persistantes de manière relationnelle dans l’application Python, SQLite est la solution idéale.

SQLITE3 : MOTEUR DE BASE DE DONNEES RELATIONNELLES EMBARQUE

SQLite est un moteur de base de données relationnelles. Il ne fonctionne pas selon le modèle client/serveur, il est directement embarqué dans les applications qui l’utilisent. Les données ainsi que la structure des tables, les index et tout le nécessaire est stocké dans un fichier.
SQLite est libre, gratuit et fourni dans la bibliothèque standard de Python. Cela signifie qu’on peut développer une application en Python contenant son propre SGBDR intégré, sans aucune installation supplémentaire et que les performances seront au rendez-vous.

Voici les principales instructions Python nécessaires pour manipuler la base :

PEUPLEMENT DE LA BDD

Nous avons retenu la solution d’une BDD pour la persistance des données. Il est maintenant nécessaire de renseigner cette base. Pour cela, le workflow sera la suivant :

WorkFlow de peuplement de la BDD
WorkFlow de peuplement de la BDD

Le choix des champs est plus ou moins libre mais reste structurant pour la suite du développement. Il est préférable de faire les bons choix dès le départ. Par exemple, le champ PRIX a été prévu pour pouvoir anticiper la fonctionnalité « établir un devis ».
Le champ RAYON est destiné à caractériser chaque article selon leur rayon d’appartenance. Cette information permettra de créer des groupes cohérents d’articles au sein d’une même liste, selon le rayon dans lequel il se trouve, à l’intérieur du magasin. A l’édition, les groupes d’articles seront imprimés dans l’ordre selon lequel on parcourt les rayons. En effet, à moins d’être totalement désorganisé, on fait globalement toujours ses courses de manière cohérente en suivant le même circuit au sein du magasin. L’idée est de ne pas s’apercevoir que le dentifrice (situé à l’entrée du magasin) figure en fin de liste une fois arrivé au rayon animalerie (situé au fond du magasin) !
Le champ TTS permettra d’enregistrer un commentaire vocal potentiellement différent du nom du produit. Cela s’avère utile pour contourner la prononciation parfois perfectible du moteur TTS (exemple : la chaine Kinder est prononcé « quindé » et on pourra saisir la chaine « quinedeur » pour entendre une prononciation plus conventionnelle).
Le fichier .csv évoqué ci-dessus contient sur chaque ligne un enregistrement comportant l’ensemble des informations relatives à un produit. Pour la persistance en Base de Données, les informations seront stockées selon le modèle physique de données issu d’une méthode d’analyse type Merise. S’agissant d’un projet trivial, on devine intuitivement la mise en œuvre de 3 tables pour l’implémentation du système de stockage :

Modèle Physique de Données
Modèle Physique de Données

Côté Python, les requêtes SQL à exécuter sont les suivantes :

La mise en variable des requêtes SQL permet d’améliorer la lisibilité du code, puisque l’instruction pour créer la table article s’écrit :

A titre d’illustration, voici un focus sur la fonction populate_article() permettant de renseigner la table article à partir du fichier .csv.

Pour améliorer l’expérience utilisateur lors de l’initialisation de la Base De Données, le script init-BDD-articles.py fournit un retour sémantique. Comme illustré dans les images ci-dessous, le script est enrichi d’instructions supplémentaires pour afficher les données effectivement renseignées dans les tables rayon et article.

Insertion des rayons dans la table rayon
Insertion des rayons dans la table rayon
Insertion des articles dans la table article
Insertion des articles dans la table article

Faire parler la Shopping List Machine à l’aide d’un moteur Text To Speech (TTS)

Concernant ces messages, il s’agira simplement du nom de l’article scanné ou bien d’un message de d’alerte notifiant le non référencement de l’article le cas échéant. Pour faire parler le Raspberry, nous allons utiliser un moteur Text To Speech (TTS). SVOX Pico TTS est l’équivalent de Google TTS mais en local. Pour l’installer, lancer la commande suivante :

Une fois installé, on dispose de la commande pico2wave qui génère un fichier wave depuis le texte passé en paramètre. Pour entendre le texte parlé, il faudra évidemment lire le fichier wave généré, à l’aide de l’outil aplay. Nous allons utiliser un script pour simplifier la manipulation. Pour cela, taper la commande nano picoTTS et renseigner les lignes suivantes :

On ajoute les droits d’exécution avec la commande sudo chmod ugo+x picoTTS. Maintenant, nous pouvons tester la voix du Raspberry à l’aide de la commande :

Et voici le focus sur la fonction TTS :

 

Impression de la Shopping List sur imprimante thermique USB

Pour ce projet, c’est une imprimante thermique USB qui sera utilisée. Il s’agit d’une imprimante à commandes ESC/POS[1], modèle NT-5890K. Le code Python permettant d’exploiter le potentiel de l’imprimante est concentré dans un script dédié (THERMALRECEIPTPRINTER.py) qui sera importé dans le script principal en tant que librairie. Pour développer cette librairie, un travail de recherche et d’analyse de librairies similaires (concernant les imprimantes à commandes ESC/POS) a été nécessaire pour recouper informations (parfois erronées) de la documentation technique fournie.

[1] ESC est le caractère qui introduit une commande, POS est l’acronyme de Point Of Sales

Imprimante Thermique ESC/POS
Imprimante Thermique ESC/POS

Sous Linux, tout est considéré comme fichier : lorsqu’on connecte un périphérique USB sur le Raspberry, un fichier associé est créé dans le répertoire /dev (ce répertoire contient des fichiers associés à des matériels, device en anglais).
Une fois l’imprimante connectée au Raspberry, la commande dmesg nous renseigne sur le matériel détecté. S’agissant d’une imprimante, on filtrera sur la chaine printer :

L’imprimante est reconnue comme matériel USB et comme le suggère l’information « usblp0 » à la ligne n°4 dans le retour de la commande ci-dessus, l’imprimante est associée au fichier /dev/usb/lp0.
La commande ls –l /dev/usb nous indique qu’il s’agit en réalité d’un fichier spécial de type c désignant un character device. Ce type de fichier permet d’effectuer une opération sur périphérique à accès direct (sans buffer, caractère par caractère.
C’est le fichier /dev/usb/lp0 qu’il faudra utiliser pour piloter l’imprimante en y envoyant les caractères à imprimer et les commandes ESC (mise en forme, impression d’une image, sortie papier,…). La commande cat « Hello world ! » >/dev/usb/lp0 aura pour effet d’imprimer la chaine « Hello world ! » sur l’imprimante. Bien entendu, nous n’allons pas rédiger un script Shell, mais nous allons plutôt utiliser le langage Python.

Comme le montre l’extrait de la librairie THERMALRECEIPTPRINTER.py ci-dessous, la notion d’objet est utilisée :

La méthode __init__ est une méthode particulière : c’est le constructeur permettant d’instancier des objets de la classe ThermalPrinter. En plus du paramètre self, le constructeur attend un second argument : le fichier associé à l’imprimante. L’invocation de la classe permet de créer automatiquement un objet de type ThermalPrinter. L’instruction présente dans la définition du constructeur (self.printer = open(RTP, « w »)) déclare un attribut nommé printer qui est un pointeur vers un fichier (passé en paramètre) ouvert en écriture. Dans le script principal shopping.py, p est un objet de la classe ThermalPrinter, instancié à l’aide de l’instruction suivante :

L’objet p ainsi créé dispose donc d’un attribut printer qui est un pointeur vers le fichier /dev/usb/lp0. Par ailleurs, un certain nombre de méthodes sont définies dans la classe ThermalPrinter. Chacune de ces méthodes est destinée à écrire dans l’attribut printer (désignant le fichier spécial /dev /usb/lp0), ce qui permettra au final, d’imprimer des caractères sur l’imprimante ou de lui envoyer des commandes ESC/POS.

A titre d’exemple, voici la méthode permettant d’imprimer une chaine de caractère sur l’imprimante :

Pour fixer les idées, la méthode ci-dessous permet d’activer/désactiver le mode souligné :

La séquence (ESC 45 n) composant la commande est décrite dans la documentation de l’imprimante :

Commande ESC pour activer le mode "underline", extrait du manuel de l'imprimante thermique
Commande ESC pour activer le mode « underline », extrait du manuel de l’imprimante thermique

Dans le script principal, il est désormais possible d’activer/désactiver le mode souligné en appelant la méthode underline() sur l’objet p de la manière suivante :

Une grille de Sudoku sur la Shopping List

Comme évoqué dans les spécifications, nous allons ajouter en fin de liste, une grille de sudoku à résoudre. Pour cela, il existe un module Python proposé par Adafruit pour générer de tels puzzles. Suite à des erreurs rencontrées à l’impression, des modifications ont été apportées : le script sudoku.py se « contente » de générer une image .bmp (l’algorithme de backtracking mis en œuvre est assez complexe) et de l’enregistrer localement. Sans entrer les détails algorithmiques, le script sudoku.py part d’un modèle de grille vierge pour générer un puzzle.

Génération d'une grille de sudoku à partir d'une image initiale à l'aide de la librairie Image
Génération d’une grille de sudoku à partir d’une image initiale à l’aide de la librairie Image

L’image ainsi générée sera ensuite imprimée à l’aide de la librairie THERMALRECEIPTPRINTER.py dédiée à l’impression.

Autres précisions

Comme évoqué dans le chapitre formulant les exigences, l’utilisation la machine doit être la plus simple possible. Dans cette perspective, le scanner de code-barres doit être le seul périphérique nécessaire pour interagir avec la machine. La Shopping List Machine doit donc être opérationnelle dès le démarrage du Raspberry et pour cela, il sera nécessaire :

  1. d’activer l’auto-login de l’utilisateur pi au démarrage à l’aide de l’utilitaire de configuration raspi-config ;
  2. lancer le script au démarrage du Raspberry : voir l’article sur le sujet.

Mais la machine doit également permettre des actions élémentaires telles que l’impression de la liste ou encore sa réinitialisation. Le seul périphérique de saisie étant le scanner de code-barres, nous allons utiliser des codes-barres spécialement générés (http://barcode.tec-it.com/fr) à cet effet :

Code barre pour déclencher l'impression de la Shopping List
Code barre pour déclencher l’impression de la Shopping List
Code barre pour déclencher vider la Shopping List en cours
Code barre pour déclencher vider la Shopping List en cours
Code barre pour déclencher le redémarrage du Raspberry
Code barre pour déclencher le redémarrage du Raspberry
Code barre pour appeler une API météo
Code barre pour appeler une API météo

Ces code-barres représentent la chaine de caractères afférente et le code Python prévoit évidemment un traitement spécifique associé.

Pour conclure

Et pour finir, comment scanner un article en rupture de stock dans les placards de la cuisine ? Pour répondre à ce problème, on pourra coller les code-barres sur l’intérieur de la porte de chaque placard, pour chacun des articles présents dans le placard en question. S’il n’y a que des tiroirs dans la cuisine, on pourra également centraliser les code-barres dans un catalogue d’article. En plus d’être pertinent, ce projet ludique offre des perspectives d’apprentissage considérables. Vous n’avez décidemment aucune bonne raison pour de ne pas vous y mettre !
Je vous livre l’ensemble du code source.

 

2 réflexions sur « Shopping List Machine : Python, Code-barres EAN13, SQLite et imprimante thermique »

  1. Salut,

    Merci pour ce travail.
    Je rencontre pas mal de difficultés en appliquant ce projet, et c’est ça qui est génial.
    C’est très formateur.
    Un exemple, j’ai bossé au début sur Python 2.7.13 et rien ne fonctionnait, saisie du code barre renvoyait des erreurs, saisie d’une chaine vide plantait, j’ai mis dès heures à m’apercevoir que les scripts ont été développés en Python3. Ca va mieux mais j’ai encore des erreurs. Pas de demande d’ajout d’article, aucune lecture des codebarres print,flush etc. Je m’éclate et en apprend beaucoup.
    A+
    Florian

    1. Salut Florian,

      Merci pour l’intérêt que vous portez à ce projet !
      Résoudre de problèmes techniques pour en apprendre toujours un peu plus : c’est exactement ce qui me plait également. C’est très formateur de faire aboutir les projets par soi-même, à force de travail de recherche et d’analyse. J’ai également rencontré des problèmes à mes débuts avec Python entre les versions 2 et 3 et cela arrive encore aujourd’hui quand j’utilise certaines librairies. Je réalise désormais tout mes scripts en Python 3.
      N’hésitez pas à me solliciter pour toutes informations complémentaires si besoin.
      Bonne continuation.

      Chucky.

Laisser un commentaire

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