Vous cherchez peut être les archives ?

Marvin, de A à Z

Vous avez été plusieurs à me poser plein de questions un peu technique sur Marvin, et ça m'a donné envie de l'écrire ce billet technique. Je vais tenter d'expliquer l'ensemble de son fonctionnement et la démarche pour arriver au résultat final, de A à Z.

Commençons par le chassie. Il est en bois, rien d'original. Le bois est très simple à usiner avec des outils standards et peu cher : perceuse, scie sauteuse, et papier de verre. J'ai utilisé deux types de bois différents. Du contreplaqué de 8mm comme support, et un autre, plus fin, qui sert habituellement aux fonds de meubles, pour relier les deux planchettes de contreplaqué. Coupé en petite dimensions, ce bois est plutôt rigide.

La structure du robot est en fait organisée en deux étages, chaque étage possède deux « surfaces » pour fixer des composants. La surface inférieure, celle qui se trouve face au sol, supporte les quatre moteurs et les accus du robot (huit accus AA). La face opposée supporte elle une protoboard et la carte Arduino (je reviendrais la dessus plus bas). L'autre étage supporte lui sur sa surface inférieur l'AlixBoard (qui prend toute la place), ainsi que sur sa surface supérieur les capteurs comme le radar, la caméra, le capteur de luminosité etc. Bien sur, les surfaces verticales sont aussi utilisées, pour fixer l'antenne, des boutons, un écran, les capteurs d'obstacle etc.

L'alimentation électrique utilise huit accus AA de 1.2V ce qui me donne donc une tension de 9.5V. L'AlixBoard doit être alimentée entre 7 et 20V, donc pas de problèmes avec elle, l'Arduino (et tous les capteurs qui passent par son alimentation) est alimentée par l'USB connecté à l'AlixBoard, donc pas non plus de problème à ce niveau. Les moteurs eux, sont alimentés en 6V, j'ai donc du utiliser un régulateur 7806 pour obtenir cette tension de 6V. Le 7806 supporte une intensité de 1.5A seulement, heureusement, mes moteurs sont bien en dessous de ce seuil avec une consommation de 200mA environ par moteur.

Les batteries

Marvin utilise un micro-contrôleur ATmega1280 de la carte Arduino Mega. Ce micro-contrôleur sert pour toutes les opérations bas niveau comme interrogation des capteurs, la gestion des moteurs, et elle possède peu de logique. En fait, la seule opération de décision qu'elle doit faire c'est de stopper les moteurs en cas d'obstacle. Elle gère cette opération elle même car le temps entre la détection de l'obstacle et l'arrêt des moteurs doit être très court.

Schéma général

Sur ce micro-contrôleur est connecté tout une tripotée de capteurs et autre gadgets électroniques : capteur d'obstacle, radar, sonde température, sonde luminosité, écran LCD etc. Ils servent majoritairement à la prise de décision du robot (comme je l'ai dit plus haut). Des codeurs sont aussi installés sur les roues, ils observent les mouvement de la roue pour coder un mouvement. Ces capteurs servent à mesurer la distance parcourue par les moteurs et a les arrêter une fois l'objectif atteint. Tout ceci est géré directement par le micro-contrôleur ainsi les messages à lui envoyer pour faire avancer les moteurs sont du type « avance de 100 unités à la vitesse maximum ».

Les batteries

Le micro-contrôleur contrôle aussi trois servomoteurs. Pour ceux qui ne connaissent pas, un servomoteur est un moteur qui permet d'assurer un mouvement très précis. Il comporte un moteur (logique), un capteur de position du moteur et un jeu d'engrenages pour augmenter le couple du moteur (au détriment de la vitesse donc). Il suffit de donner un ordre au moteur (une position) pour qu'il s'y rende et la maintienne. Si on force, il forcera dans le sens inverse en retour. Ce genre de moteur est très utilisé en modélisme, pour la direction par exemple. Dans mon cas, j'en utilise un pour contrôler l'angle du radar, et deux autres pour la « visée » de la caméra (un pour le pan, l'autre pour le tilt).

Le micro-contrôleur est directement interfacé avec l'AlixBoard par USB, et plus précisément du Serial over USB. Le composant intégré à la carte Arduino simule un simple port série. Du coté de l'AlixBoard, qui tourne sous Linux, on a l'impression d'avoir affaire à un simple port série. J'utilise d'ailleurs la bibliothèque pySerial pour communiquer avec le port.

J'ai bien sûr dû définir un protocole pour la communication des deux cartes. L'USB étant un protocole fiable, mon protocole ne s'occupe pas du tout de la détection et de la correction d'erreurs. Il est donc extrêmement simple et ne possède que deux (en plus des données) champs : le type de la trame envoyée, et la longueur de ses données. Et éventuellement, un champ data de la taille de la valeur du champ précédant (qui peut être de zéro).

Un petit schéma pour mieux se représenter la chose :

Type
(1 octet)
Taille données
(1 octets )
Données
(0+ octets)

Chaque trame est analysée et correspond à un morceau de code en fonction du type. Les données transmissent peuvent être n'importe quoi. Ce morceau de code leur donnent ensuite un sens.

Concernant les moteurs, ils sont drivés par une carte a part. Celle-ci communique avec l'Arduino par port Série aussi (le protocole est lui propre à la carte). J'ai choisi d'utiliser un driver tout fait pour éviter les prises de tête « électroniques », ce domaine n'étant pas vraiment ce qu'on pourrait appeler une spécialité pour moi (surtout l'électronique analogique). Il gère donc l'énergie envoyée aux moteurs et permet de moduler leur vitesse.

Les batteries

L'AlixBoard s'occupe de la partie logique du robot. Bon, pour l'instant la logique concise simplement à suivre les ordres de l'utilisateur, mais j'envisage plus tard de faire de la vision par ordinateur. L'AlixBoard gère aussi les communications WiFi qui servent à commander le robot à distance, ainsi que la webcam qui permettra de faire du pilotage en immersion. Elle tourne sous Linux Debian Lenny stocké sur une simple carte compact flash de un giga. On peut accéder au système par SSH pour la configuration et autres tâches de compilation du firmware de l'Arduino, mais toute la partie robotique est gérée par un daemon en Python sobrement appelé « marvind ».

Les batteries

Le daemon fait le pont entre les ordres de l'utilisateur (via WiFi) et la carte Arduino (via l'USB). Il peut aussi faire de la prise de décision.

La communication avec le daemon du point de vue de l'utilisateur se fait via un protocole de RPC maison. En fait, j'ai d'abord commencé par définir un protocole de communication très simple (encore une fois) à base de données JSON encapsulées dans du TCP. Une trame de ce protocole ressemble à ceci :

Taille données
(4 octets )
Données JSON
(0+ octets)

Je pouvais difficilement faire plus simple :-).

Cela me permit de converser facilement en communiquant des structures typées complexes. La bibliothèque qui implémente ce protocole s'appelle « zaphod ». Je la releaserais peut être un jour...

Au dessus de ça, j'ai créé mon protocole de RPC, « zaphodrpc ». Il permet au serveur de publier des fonctions distantes (je parle bien de fonction, on récupère une valeur de sortie), mais aussi une communication bidirectionnelle d'envoi de notification. Le serveur peut donc « pusher » des informations aux clients connectés (genre, attention, j'ai faillis me manger un mur, mais je me suis arrêté).

Je ne détaille pas plus sur ce RPC car même si il est assez simple comparé aux autres, il y a quand même beaucoup à dire la dessus.

Reste ensuite le client, je n'ai pas encore décidé ce à quoi il aurait l'air pour le moment.

Le projet à pas mal avancé depuis le dernier billet. L'AlixBoard est intégrée, et je suis en train de programmer l'API du robot au travers le RPC. Il manque encore la caméra et beaucoup de code à terminer avant de rendre le robot utilisable, mais ça ne serais tarder... Cela dit, il est déjà capable de fonctionner en autonomie (c-à-d sur batterie) et d'être contrôlé à distance via le WiFi, donc on peut dire que le plus gros est fait :-).

Marvin #1

Quand j'étais petit, je voulais construire un robot. En fait, j'en avais même fait un, construit avec une vieille télécommande et un jouet radio commandé. Le problème c'est qu'il ne fonctionnait pas vraiment, étant donné que j'avais juste scotché la télécommande au jouet. J'étais bien déçu.

Depuis un moment, l'idée me trottait à nouveau dans la tête. Cette fois je savais un peu plus ce que j'avais à faire. J'ai donc commencé par réfléchir au problème. Quatre roues motrices avec une direction différentielle (pour la simplicité mécanique), une carte Arduino pour toutes les opérations « bas niveau » (contrôle des moteurs, récupération des informations provenant des capteurs...), et une carte Alix (géode 500Mhz, afin de faire tourner toute la « logique », utiliser la connectivité WiFi, et y brancher une webcam). Au fur et à mesure, le projet se construisait dans ma tête.

J'ai ensuite mis environ deux mois à me décider à me lancer : tout ça représente en effet une certaine somme d'argent, d'autant plus que je voulais éviter la récup' pour être sûr d'avoir quelque chose qui fonctionne correctement au final. Sans parler de l'intérêt assez faible de l'objet en lui-même...

Finalement, me restant quelques euros après mon voyage de cet été, j'ai décidé de me lancer.

Je voulais quand même éviter d'acheter du matériel pour rien si je n'arrivais pas à terminer le projet. Je l'ai donc divisé en trois étapes :

  • Construction du chassie et ajout des éléments de bas niveaux (moteurs, carte Arduino, capteurs).
  • Ajout de l'Alix Board pour les opérations de haut niveau (WiFi...).
  • Ajout de la vision au robot (En gros Webcam, mais aussi servo moteurs pour la diriger).

J'en suis pour l'instant à la première étape, elle n'est pas terminée, mais j'ai déjà construit le chassie, ajouté les moteurs, la carte Arduino, et tout ce petit monde roule correctement. Et il peut s'arrêter s'il détecte un obstacle. J'ai même terminé de spécifier le protocole de communication entre l'Alix Board (qui est pour l'instant mon PC) et l'Arduino. Les prochains travaux seront la mise en place des autres capteurs (Radar à ultrasons, capteur de luminosité, température...).

Pour terminer, voici quelques photos et vidéos de l'engin :

Vue d'ensemble Fais le beau ! Électronique Vue de devant

Un autre billet plus technique suivra (peut être) au sujet de Marvin.

Comment fonctionne le format d'images PNG ?

Je ne détaillerais pas le cheminement qui m'a amené à devoir lire la RFC 2083 qui décrit le format d'image PNG, mais maintenant que c'est fait, je trouvais intéressant d'écrire un petit article la dessus pour ceux que ça intéresse.

Je vais donc, sous forme de didactitiel pas à pas, tenter d'expliquer très globalement comment fonctionne le format, et comment créer une image avec.

Tout au long de l'article, j'utilise un interpréteur interactif Python pour automatiser certaines tâches très légèrement fastidieuses. Même si le but n'est pas de programmer un encodeur d'image, quelques rudiments de programmation seront tout de même utiles pour tout comprendre.

La matière première

L'image utilisée dans cet article est affichée ci-dessous (grossie 100x). J'ai pris une image relativement petite de 16x16px afin de pouvoir la manipuler plus facilement.

Mouton grossi 100x

Nous n'allons pas partir totalement de rien. J'ai préalablement créé une structure Python très simple pour décrire mon image. Comme tout le monde le sait, une image est composée de pixels. Un pixel, c'est l'élément le plus petit d'une image. Selon le type d'image (noir et blanc, couleur, avec transparence ou non), un pixel peut être décrit par plusieurs valeurs différentes, on parle de canaux.

Dans notre cas, l'image est en noir et blanc, mais nous allons coder les couleurs en RVB, et utiliser la transparence (RVBA donc, A pour Alpha, ce qui correspond au canal de la transparence). Nous avons donc 4 valeurs qui définissent notre pixel : le rouge, le vert, le bleu, et la transparence qui mélangés donnent une couleur, avec sa transparence. Toutes ces valeurs sont codées sur un octet, soit 8 bits. Cela permet 256 valeurs différentes, de 0 à 255. Le format PNG permet aussi de coder les canaux sur 2 octets pour obtenir de plus grandes nuances de couleurs, mais nous n'utiliserons pas cette fonctionnalité. Le tableau ci-dessous montre comment le mélange de couleurs permet de former le pixel, et comment l'assemblage de tous les pixels permet de former l'image.

RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA
RV
BA

Pour représenter ça en Python, j'ai donc pris chacune des quatres valeurs qui définissent un pixel pour les regrouper dans un tuple. Chacun des pixels d'une ligne est regroupé dans un tuple, et chaque tuple de ligne est regroupé dans un autre qui contient toutes les lignes de l'image.

Petit exemple, pour cette image de 2x2 pixels :

Image de 2x2 pixels

La représentation en Python sera :

(
	( (0, 0, 0, 255), (255, 255, 255, 127) ),
	( (255, 255, 255, 127), (0, 0, 0, 255) )
)

Vous pouvez télécharger le module Python qui contient la structure représentant mon image ou fabriquer votre propre image représentée sous cette forme.

Parlons PNG

La première chose que nous allons faire est de représenter notre image sous une autre forme plus "PNGesque". En fait, nous allons simplifier notre structures actuelle pour représenter une ligne sous forme de chaine d'octets. Chaque octet étant une des valeurs d'un pixel (R, V, B ou A) directement. La fonction Python chr() permet de transformer un entier en octet (ou caractère, ou encore byte).

Le code pour faire cette transformation est celui-ci :

>>> from data import image_brute
>>> image = [''.join([''.join([chr(v) for v in px]) for px in ligne]) for ligne in image_brute]

Vous pouvez afficher image, ce qui vous donnera une liste de charabia incompréhensible correspondant à chaque ligne de notre image.

Le filtrage

Avant de compresser notre belle image avec algorithme bien connu de compression (inflate), nous devons l'optimiser un peu afin de maximiser la compression : on appelle cela le filtrage.

Il existe actuellement une seule méthode de filtrage qui répertorie cinq types de filtres différents. Une méthode de filtre est un genre de "pack" de filtres que l'on peut ensuite, au choix, utiliser sur chacune des lignes. Le choix d'un filtre pour une ligne peut se faire de différentes manières. La RFC n'impose pas la manière de faire cela (elle donne juste quelques indications), c'est donc à ceux qui implémentent un encodeur de déterminer eux même un algorithme pour cette tâche. Une des manières proposée par la RFC est de filtrer la ligne avec chacun des filtres successivement, puis de garder celui qui compresse le mieux. J'imagine que c'est l'algorithme utilisé la plupart du temps dans les encodeurs.

Vu que le but n'est pas de coder un encodeur, ni de rendre notre image la plus réduite possible, nous allons utiliser le filtre 0, celui qui consiste à ne rien faire. Il faut toutefois, au début de chaque ligne, ajouter le numéro du filtre utilisé :

>>> image = [chr(0)+ligne for ligne in image]

Compression !

Enfin, nous allons compresser notre image. L'algorithme de compression utilisé par le format PNG est inflate. Sans entrer dans les détails, inflate est un algorithme de compression sans perte qui combine l'algorithme LZ77 et le codage de Huffman. Ceux qui voudront plus de détails sur cette partie trouveront abondamment de la documentation sur leur moteur de recherche préféré.

Pour effectuer cette tâche nous n'allons donc pas implémenter l'algorithme, mais utiliser la fameuse zlib qui s'en chargera très bien. La zlib est utilisable en Python au travers du module zlib (logique) fournit en standard.

Nous allons dans un premier temps mettre tous les éléments de notre liste bout à bout puisque nous n'avons plus besoin d'effectuer des opérations par ligne de l'image :

>>> image = ''.join(image)

La compression en elle même est très simple avec la zlib :

>>> import zlib
>>> image_compressee = zlib.compress(image)

Nous pouvons aussi constater que inflate compresse très bien notre image :

>>> print 'Compression à %.1f%%' % (100-len(image_compressee)*100.0/len(image))

La structure d'un fichier PNG

Nous avons maintenant notre image compressée avec l'algorithme qui va bien, mais le PNG ce n'est pas que ça, c'est aussi l'enrobage. Cet enrobage donne tout plein d'informations sur l'image, certaines sont indispensables comme la méthode de filtrage utilisée, le type de codage de la couleur etc. D'autres informations sont optionnelles, comme par exemple les commentaires que l'on pourrait définir sur une image.

Ces informations sont regroupées sous forme de bloc dans le jargon PNGien. Un bloc possède la forme suivante :

Longueur
(4 octets)
Type
(4 octets)
Données
(0+ octets)
CRC
(4 octets)

Le champ longueur indique tout simplement la longueur du champ donnée qui est lui même variable. Le champ type est un peu comme le nom du bloc. Il utilise des valeurs de la table ASCII, on trouve alors des blocs de type IDAT ou IHDR que nous détailleront plus tard. Les données dépendent du type de bloc (il peut ne pas y en avoir). Enfin le champ CRC est un controle d'intégrité du bloc et se calcule à partir des champs type et données.

Il existe naturellement plusieurs types de blocs PNG. Certains sont obligatoires comme les blocs IHDR (image header, il indique des informations caractéristiques de l'image comme ses dimensions), PLTE (palette, qui contient la palette de couleurs de l'image si l'image utilise le mode par couleur indéxées), IDAT (image data, qui contient l'image en elle même, compressée tel que nous l'avont fait plus haut), et IEND (marqueur de fin de l'image). L'ordonnancement de ces blocs est libre, à l'exception de IHDR qui doit se trouver au début de l'image, et de IEND qui doit se trouver à la fin de l'image. Cependant, certains blocs auxiliaires peuvent necessiter un ordonnancement particulier.

D'autres blocs, appelés blocs auxiliaires sont utilisables. La tableau ci-dessous récapitule tous les blocs auxiliaires définis par la spécification PNG.

Nom de type Description
bKGD Spécifie une couleur de fond par défaut pour l'affichage de l'image.
cHRM Spécifie les coordonnées x, y du modèle chromatique CIE 1931 pour les couleurs rouge, vert, et bleu primaires, ainsi que la référence du point blanc.
gAMA Spécifie le gamma de la caméra (réelle ou virtuelle) qui a produit l'image.
hIST Donne une fréquence approximative de l'utilisation de chaque couleur de la palette.
pHYs Spécifie la taille du pixel et son rapport dimensionnel.
sBIT Spécifie le nombre de bits significatifs d'origine.
tEXt Informations textuelles attachées à l'image.
tIME Date et heure de la dernière modification de l'image.
tRNS Spécifie que l'image utilise la transparence simple.
zTXt Informations textuelles compressées attachées à l'image.

Pour notre image, nous n'allons utiliser que les blocs absolument obligatoires :  IHDR, IDAT et IEND. PLTE étant inutile puisque nous n'utilisont pas le mode par couleurs indéxées.

Dernière chose, un fichier PNG possède une signature, une suite d'octets qui sert à la détection du format d'un fichier par les programmes. Cette signature doit positionner au tout début du fichier, juste avant le bloc IHDR. Elle est la suivante (en décimal) :

137 80 78 71 13 10 26 10

Le bloc IHDR

Commençont par le bloc qui sera le premier de notre fichier. Ce bloc donne donc les caractéristiques de l'image : sa largeur, sa hauteur, sont échantillonage, le type de couleurs utilisées, la méthode de compression, la méthode de filtrage, et enfin, la méthode d'entrelacement. Voici la forme de la partie données du bloc :

Largeur
(4 octets)
Hauteur
(4 octets)
Echantillonage
(1 octets)
Couleur
(1 octets)
Compression
(1 octets)
Filtrage
(1 octets)
Entrelacement
(1 octets)

Définissons chacune de ces valeurs avant de les coder.

Les dimensions de notre images sont de 16x16 pixels. Pour l'échantillonage, nous l'avons dit plus haut, nous utilisons des canaux de couleurs codés sur un octet, soit 8 bits. Le modèle de couleur à utiliser pour une image qui utilise le RVBA est le numéro 6. Je n'invente rien à ce niveau, tout est spécifié dans la RFC.

Pour la compression ainsi que le filtrage, à ce jour, seul une seule méthode existe, nous renseigneront donc 0. Nous n'entrelaçons pas, nous utiliseront donc aussi la valeur 0 pour ce champ.

Pour coder ces données en Python, nous utilisont le module struct qui sert justement à ça :

>>> import struct
>>> IHDR = ['', '', '', ''] # Les 4 éléments d'un bloc
>>> IHDR[1] = u'IHDR'.encode('ascii')
>>> IHDR[2] = struct.pack('>IIBBBBB', 16, 16, 8, 6, 0, 0, 0)
>>> IHDR[0] = struct.pack('>I', len(IHDR[2]))

N'oublions pas le CRC, nous allons à nouveau utiliser ce cher module zlib pour ça, puisqu'il contient une fonction tout prete cette tâche :

>>> IHDR[3] = struct.pack('>i', zlib.crc32(''.join(IHDR[1:3])))

Le bloc IDAT

La partie donnée du bloc contient simplement l'image sous la forme compressée que nous avont créé plus haut, la création de se bloc sera donc assez simple :

>>> IDAT = ['', '', '', '']
>>> IDAT[1] = u'IDAT'.encode('ascii')
>>> IDAT[2] = image_compressee
>>> IDAT[0] = struct.pack('>I', len(IDAT[2]))
>>> IDAT[3] = struct.pack('>i', zlib.crc32(''.join(IDAT[1:3])))

Le bloc IEND

Encore plus simple que le bloc IDAT, le bloc IEND qui ne contient aucune donnée puisqu'il n'est qu'un marqueur de fin de fichier. Sa création se fait ainsi :

>>> IEND = ['', '', '', '']
>>> IEND[1] = u'IEND'.encode('ascii')
>>> IEND[0] = struct.pack('>I', len(IEND[2]))
>>> IEND[3] = struct.pack('>i', zlib.crc32(''.join(IEND[1:3])))

Signature du fichier

Comme expliqué plus haut, la signature du fichier sert à la détection du format par les programmes, sa création est des plus simples :

>>> signature = chr(137) + chr(80) + chr(78) + chr(71) + chr(13) + chr(10) + chr(26) + chr(10)

Et on colle tout ça :

Nous allons maintenant concaténer tous ces éléments dans l'ordre approprié, c'est à dire la signature en premier, suivie du bloc IHDR, suivi du bloc IDAT, et pour terminer du bloc IEND :

>>> png = signature + ''.join(IHDR) + ''.join(IDAT) + ''.join(IEND)

Finalement, nous enregistrons tout ceci dans un beau fichier :

>>> f = open('mon_beau.png', 'w')
>>> f.write(png)
>>> f.close()

Vous pouvez alors ouvrir le fichier avec votre lecteur préféré, il devrait normalement s'ouvrir sans encombre :-).

Pycon 2009

Cette année encore je me suis rendu à la Pycon, un weekend de conférences sur le langage Python, ou plutôt à un morceau de Pycon, puisque je n'y ai été que le samedi, et pas à toutes les sessions.

Pycon
(Oui la photo est moche, mais j'avais la flemme et le 50mm était trop long pour la taille de la salle)

Cette année j'ai donc assisté à une présentation du fonctionnement du développement de CPython, l'implémentation principale (en C) de Python, par Victor Stinner. Il a passé en revue les différents outils utilisés par les développeurs de Python pour communiquer, et donné des recommandations à ceux qui voudraient soumettre des bugs.

La seconde conférences à laquelle j'ai assisté concernait PyQuery, présenté par Olivier Lauzanne. PyQuery est une bibliothèque qui reprend l'API de JQuery pour l'interrogation de documents xml ou html. Ça ressemble à ça :

>>> from pyquery import PyQuery as pq
>>> journaux = pq(url='http://inaps.org/journal/')
>>> print [j.text_content() for j in journaux('h2')]
['Vacances...', 'Programmation avec Curses en Python', "Escaline2 : la marmotte s'est remise au travail", "L'algorithme de diksta... dijstrak... DIJKSTRA !", 'Mon premier module sur le CheeseShop', "OMG, une nouvelle version d'I, NaPs ?!", "Qui n'en veuuuuxx ? :)", 'Et c\'est qui qui dit "fail" ?', 'PyCon fr 08', u'Mais o\xc3\xb9 en est Bearnaise ?']

Personnellement, je trouve la syntaxe assez sympa et carrément plus pratique que la manipulation directe d'un arbre dom. Je pense que ça va potentiellement être très utile pour mon travail si un partenaire nous demande à nouveau de nous démerder avec son site au lieu de filer un flux xml de ses données. Merci donc au conférencier (qui semble aussi être l'auteur de la bibliothèque) de m'avoir fait connaître ça.

Dans la conférence suivant, Gael Pasgrimaud nous parlait de Deliverance qui est, si j'ai bien compris, un proxy, qui se place devant le serveur web et qui permet de modifier les pages servies par celui-ci à la volée. Le conférencier à évoqué plusieurs utilisations, j'ai surtout retenu la possibilité de faire des templates que le proxy traitera à la volée. J'ai globalement pas trop suivi cette conf, étant plutôt occupé à tenter de monter mon vpn sur le réseau tout filtré de la cité des sciences (ouh lala).

Benoît Chesneau nous a ensuite présenté une série de bases de données non sql et l'implémentation de leur bibliothèque d'accès en Python : bdd à clé/valeurs comme Memcache, Redis ou encore Tokyo cabinet. Bdd orientées documents comme CouchDB ou MongoDB. Les bdd orientées colonne (WTF ??!) comme Hbase. Ou encore les bdd objet, comme Zope et sa ZoDB. Une présentation assez intéressante qui nous montre qu'il n'y a pas que MySQL dans la vie. Pour ma part, je m'intéressais déjà à CouchDB depuis quelque temps, j'attendais d'ailleurs pas mal de la conférence dédiée à CouchDBKit (bibliothèque CouchDB en Python) de l'après midi dont je parle juste en dessous.

Après un petit passage au Quick, retour à la cyber-base.

Les sessions de l'après midi commencent par une grosse conférence d'1h sur Python en usage scientifique par Gael Varoquaux. Globalement intéressant, ça parlait d'IPython, le shell Python avancé, et des modules matplotlib et scipy. Je n'ai par contre pas tout suivis, quand ça commençait à partir sur des algorithmes pointus appliqués à des images. Cependant selon l'auteur, ces modules Python lui permettent d'implémenter en quelques lignes des algorithmes très complexes, là ou d'autres s'arrachent les cheveux avec du C.

La conférence suivant était une présentation de l'OLPC (de l'ordinateur en lui même et du projet) par Jean-François Cauche, et plus particulièrement de son interface graphique, Sugar, réalisée en Python. Une conférence assez intéressante qui donne envie d'acquérir un nouveau jouet (ou préciiieuuux, comme dises certains).

Entre deux, un petit Lightning Talk de 10mn de Michael Scherer sur Python dans les environnement mobiles. Michael à évoqué les plates-formes existantes et les possibilités d'y coder en Python dessus. En résumé : Androïd bof, iPhone bof, Windows CE moyen mais api windows relou, Symbian pas mal, mais le mieux était selon lui les plates-formes complètement ouvertes comme open-moko.

S'en est suivit d'un retour d'expérience de Marie-Josee Cros, chercheuse à l'INRA (Institut National de la Recherche Agronomique) de Toulouse. Son projet est la mise en place d'une plate-forme RNA-Space d'annotation génomique d'ARN non-codant-protéine. Leur plate-forme sert en fait à collecter auprès des chercheurs des données à traiter, puis à retourner les résultats à ceux-ci. Elle est programmée en Python et utilise le framework web CherryPy. Marie-Josee Cros à donc expliqué les choix de son équipé et commenté les problèmes auxquels ils se sont confrontés lors du développement de leur programme.

Enfin, la dernière conférence auquel j'ai assisté portait sur CouchDBKit, une bibliothèque qui permet de se connecter à une base de donnée CouchDB. Présenté par son auteur, Benoît Chesneau, le module est en fait formé d'une série d'helpers qui permettent de grandement simplifier l'accès à la base. L'auteur nous a présenté son module en développant en live une petite application web de ToDo list.

Pour terminer, un grand merci aux organisateurs de l'AFPY de proposer chaque années des conférences intéressantes (et gratuites) sur Python.

Vacances...

Billet de train

Programmation avec Curses en Python

Ce texte est une traduction du HowTo publié dans la documentation Python, écrit par A.M. Kuchling et Eric S. Raymond. Elle est basée sur la version 2.03 de ce document.

Qu'est ce que curses ?

La bibliothèque curses permet une gestion indépendante du terminal des impressions sur l'écran, et de la gestion du clavier pour les terminaux textes comme les VT100s, les consoles linux ou les terminaux virtuels X11 comme xterm ou rxvt. L'affichage des terminaux supportent de multiples codes de contrôles utilisés pour des opérations comme le déplacement du curseur, le scrolling de l'écran et l'effacement de zones sur celui-ci. Les différents terminaux utilisent chacun une large variété de codes de contrôles différents, et chacun nécessitent des petites bidouilles qui lui sont propres.

Dans le monde de l'affichage graphique sous X, certains vous diraient « Et alors ? ». Il est vrai que les terminaux à affichage texte sont une technologie obsolète, mais certaines personnes arrivent encore à en faire des choses élégantes. Certaines d'entre elles sont celles qui recherchent de faibles empreintes mémoire ou qui veulent utiliser des Unices dans l'embarqué dont la présente d'un serveur X n'est pas requise. D'autres sont celles qui installent les systèmes d'exploitations, qui configurent les noyaux, et opèrent bien avant que le serveur X soit disponible.

La bibliothèque curses cache tous les détails propre à chaque terminal, et offre aux programmeur une abstraction de l'affichage, contenant de multiples fenêtres qui ne se chevauchent pas. Le contenu d'une fenêtre peut être modifié de plusieurs manières, en ajoutant du texte, en le supprimant, ou en changeant son apparence, et la bibliothèque curses décidera automagiquement quels sont les codes de contrôles qui seront envoyés au terminal pour produire la sortie adéquate.

La bibliothèque curses fût initialement écrite pour les Unices BSD; plus tard, les versions System V d'AT&T y ajoutèrent plusieurs améliorations et de nouvelles fonctions. La version BSD de curses n'est maintenant plus maintenue, et a été remplacée par ncurses, qui est une implémentation open-source de l'interface d'AT&T. Si vous utilisez un Unix open-source (ndt : et libre) comme Linux ou FreeBSD, votre système utilisera certainement ncurses. Depuis que les versions commerciales d'Unix ne sont plus basées sur le code de Système V, toutes les fonctions décrites ici sont probablement disponibles. Cependant, les vieilles version de curses installées par certains Unices propriétaires ne supporteront peut être pas tout.

Personne n'a fait un portage sous Windows du module curses. Sur les plateformes Windows, essayez le module Console écrit par Fredrik Lundh. Le module console permet la gestion d'un curseur adressable, ainsi qu'une gestion de la souris et du clavier, et il est disponible depuis http://effbot.org/zone/console-index.htm.

Le module curses pour Python

Le module Python n'est qu'une simple glue autour des fonctions C proposées par curses; si vous êtes déjà famillier avec la programmation avec curses en C, ce sera très facile de transposer ce que vous connaissez sur Python. La plus grande différence est que l'interface Python rend les choses plus simple, en regroupant différentes fonctions C comme addstr(), mvaddstr(), mvwaddstr(), dans une seule méthode addstr(). Tout ceci sera détaillé plus tard.

Ce HowTo est une simple introduction à l'écriture de programmes en mode textes avec curses et Python. Il ne cherche pas à être un guide complet sur l'API curses; pour cela, lisez la partie sur curses dans la documentation des modules Python, et les pages de manuel pour la version C de ncurses. Par contre, il vous donnera quelques pistes pour commencer.

Démarrer et terminer les applications curses

Avant de faire quoi que ce soit, curses doit être initialisé. Ceci est réalisé avec l'appel à la fonction initsrc(), qui va déterminer le type de terminal, lui enverra des codes de contrôles d'initialisation, et créeras un certain nombre de structure de données internes pour sa gestion. Si il réussit, initscr() retournera un objet « window » représentant l'écran entier; celui ci est habituellement appelé stdscr d'après le nom de la variable correspondante en C.

import curses
stdscr = curses.initscr()

Habituellement, les applications curses désactivent l'écho automatique des touches sur l'écran, ceci afin de pouvoir lire leurs valeur et de les afficher seulement sous certaines circonstances. Ceci nécessite l'appel à la fonction noecho().

curses.noecho()

Les applications vont aussi avoir besoin de réagir instantanément à l'appuis des touches, sans attendre que la touche Entrée soit appuyée; ceci est appelé « mode cbreak », en opposition à l'habituel mode d'entrée tamponné (ndt: buffered, ou buffurisé en franglais).

curses.cbreak()

Les terminaux retournent souvent les touches spéciales tels que les touches du curseur ou les touches de navigations comme Page UP et Home, comme des séquences d'échappement multi-octet. Vous pouvez programmer votre application pour qu'elle puisse gérer de tels séquences et effectuer les traitements correspondants, ou bien laisser curses faire cela pour vous, en retournant une valeur spéciale comme curses.KEY_LEFT. Pour permettre à curses de faire ce travail, vous devez activer le mode « keypad ».

stdscr.keypad(1)

Terminer une application curses et encore plus simple que d'en démarrer une. Tout ce que vous avez a appeler pour inverser les paramères adaptés à curses pour le terminal est :

curses.nocbreak(); stdscr.keypad(0); curses.echo()

Vous pouvez alors appeler la fonction endwin() pour restorer le mode de fonctionnement d'origine du terminal.

curses.endwin()

Un problème commun quand on débogue une application curses est de retrouver votre terminal complètement hors d'usage quand l'application est tuée sans restorer le terminal dans son état initial. Cela arrive souvent en Python quand votre code est bogué et qu'il lève une exception non gérée. Par exemple, les touches ne sont plus répétées sur l'écran lorsque vous tapez dessus, ce qui rend quelque peu difficile l'utilisation du shell.

En Python, vous pouvez éviter ces complications et permettre un débogage plus simple en important le module curses.wrapper. Il offre une fonction wrapper() qui prend un callable en paramètres. Il effectue les initialisation détaillées plus haut, et initialise aussi les couleurs si le support des couleurs est présent. Il va ensuite appeler votre callable, et finalement dé-initialiser correctement le terminal. Le callable est appelé à l'intérieur d'un try-except qui va intercepter les exceptions et permettre une dé-initialisation propre, pour ensuite vous repasser l'exception pour le débogage. Ainsi, votre terminal ne sera pas laissé dans un sale état après une exception.

Fenêtres et "Pads"

Les fenêtres sont les éléments d'abstraction de bases de curses. Un objet fenêtre représente une zone rectangulaire de l'écran, et supporte un certain nombre de méthode pour y afficher du texte, l'effacer, permettre à l'utilisateur d'y entrer du texte, et ainsi de suite.

L'objet stdscr retourné par la fonction initscr() est lui même un objet fenêtre qui recouvre l'écran entier. Beaucoup de programme ne nécessiteront que cette seule fenêtre, mais vous pouvez vouloir diviser l'écran en plusieurs fenêtres plus petites pour redessiner ou effacer chacune d'entre elle séparément. La fonction newwin() créera une nouvelle fenetre d'une taille donnée, et retourne un objet fenêtre.

begin_x = 20 ; begin_y = 7
height = 5 ; width = 40
win = curses.newwin(height, width, begin_y, begin_x)

Un mot à propos des coordonnées utilisées dans curses : elles sont toujours passée dans l'ordre y, x et le coin en haut à gauche possède les coordonnées (0, 0). Ceci casse les conventions habituellement pour la manipulations des coordonnées, où x est normalement définit en premier. Cela est une malheureuse différence avec la plupart des autres applications informatiques, mais c'est comme cela depuis que curses à été écrit, et il est trop tard pour le changer maintenant.

Quand vous appelez une méthode pour afficher ou effacer du texte, son effet est pas immédiat sur l'affichage. Cela provient du fait que curses à été initialement écrit pour des terminaux possédant des connexions lentes, à 300 bauds par seconde; avec ces terminaux, minimiser le temps requis pour redessiner l'écran est très important. Cela oblige curses à accumuler les modifications de l'affichage et à les dessiner de la manière la plus efficace. Par exemple, si votre programme affiche quelques caractères dans une fenêtre pour ensuite effacer la fenêtre, il n'y a pas besoin d'envoyer les caractères à afficher au terminal, puisque ceci ne seront de toute manière jamais visibles.

En conséquence, curses necessite que vous lui indiquiez de redéssiner les fenêtres, en utilisant la méthode refresh() des objets fenêtre. En pratique cela ne complique pas vraiment la programmation avec curses. La plus part des programmes effectuent une suite d'opérations, et font une pause en attente de l'appuis d'une touche ou d'une autre action de l'utilisateur. Tout ce que vous avez à faire est d'être sûr que l'écran sera bien redessiné avant l'attente de l'entrée utilisateur, en appelant simplement la méthode refresh() de la fenêtre en question.

Un pad est un cas spécial de fenêtres; il peut être plus grand que l'affichage actuel de l'écran, et seulement une portion de celui-ci sera affiché. Créer un pad requière simplement d'une hauteur et une largeur, et le rafraichissement du pad requière les coordonnées de la partie de l'écran où afficher une partie du pad.

pad = curses.newpad(100, 100)
# Cette boucle remplit le pad de lettres
# Cela est expliqué dans la prochaine partie
for y in range(0, 100):
    for x in range(0, 100):
        try: pad.addch(y,x, ord('a') + (x*x+y*y) % 26 )
        except curses.error: pass

#  Affiche une portion du pad sur l'écran
pad.refresh( 0,0, 5,5, 20,75)

L'appel à refresh() affichera une partie du pad dans un rectangle depuis les coordonnées (5, 5) aux coordonnées (20, 75) sur l'écran; le coin en haut à gauche de la partie affichée à les coordonnées (0, 0) sur le pad. Mis à part cette différence, les pads sont exactement comme les fenêtres ordinaires et possèdent des mêmes méthodes.

Si vous avez plusieurs fenêtres et pads sur un écran, il y a une manière plus efficace de faire, qui va empêcher d'irritants scintillements lors des rafraichissements. Utilisez la méthode noutrefresh() sur chacune des fenêtres pour mettre à jour la structure interne représentant les données sur l'écran; et ensuite, pour changer l'affichage sur l'écran physique, appelez la fonction doupdate(). La méthode classique refresh() appelle en réalité la fonction doupdate() avant de se terminer.

Afficher du texte

Du point de vue d'un programmeur C, curses peut ressembler à un nuage de fonctions, toutes subtillement différentes. Par exemple, addstr() affiche une chaine sur la position courante du curseur dans la fenêtre stdscr alors que mvaddstr() déplace le curseur au coordonnées indiquées avant d'afficher le texte. waddstr() est identique à addstr(), mais permet de spécifier la fenêtre à utiliser, à la place d'utiliser stdscr par défaut. mvwaddstr() suis le même schéma.

Heureusement, l'interface Python cache tous ces détails; stdscr est un objet fenêtre comme tous les autres, et les méthodes comme addstr() acceptent plusieurs formes d'arguments. Il y a habituellement quatre formes différentes :

Forme Description
str or ch Affiche la chaine str ou le caractère ch à la position courante
str or ch, attr Affiche la chaine str ou le caractère ch, en utilisant l'attribut attr à la position courante
y, x, str or ch Déplace le curseur à la position y, x et affiche la chaine str ou le caractère ch
y, x, str or ch, attr Déplace le curseur à la position y, x et affiche la chaine str ou le caractère ch, en utilisant l'attribut attr

Des attributs permettent d'afficher du texte en évidence, comme en gras, en souligné, inversé ou en couleur. Ils seront expliqués plus en détails dans la prochaine partie.

La méthode addstr() prendre une chaine en entrée pour l'afficher, alors que la méthode addch() prend un seul caractère, qui est représenté en Python sous la forme d'une chaine de taille 1, ou d'un entier. Si c'est une chaine, vous êtes limités à l'affichage des caractères compris entre 0 et 255. La version SV-r4 de curses apporte des constantes pour l'extension des caractères; ces constantes sont des entier plus grands que 255. Par exemple, ACS_PLMINUS est le symbole ±, et ACS_ULCORNER est le coin en haut à gauche d'une boite (pratique pour dessiner des bordures).

Les fenêtres se souviennent où le curseur à été laissé après la dernière opération, donc, si vous ne précisez pas les coordonnées y, x, la chaine ou le caractère seront affichées là où la dernière opération aura laissé le curseur. Vous pouvez aussi modifier ce dernier avec la méthode move(y, x). A cause de certains terminaux qui affichent en permanence un curseur clignotant, vous allez probablement vouloir vous assurer qu'il ne sera pas placé à un endroit où il sera distrayant pour l'utilisateur; ça peut être déroutant d'avoir un curseur qui clignote à des position apparemment aléatoires.

Si votre application ne nécessite pas un curseur clignotant, vous pouvez appeler curs_set(0) pour le rendre invisible. Pour des raisons de compatibilité avec les vieilles versions de curses, il y a une fonction leaveok(bool) équivalente. Quand bool est à True, la bibliothèque curses va tenter de supprimer le curseur clignotant, et vous n'aurez plus à vous soucier de sa position.

Attributs et couleurs

Les caractères peuvent être affichés de différentes manière. Les lignes d'état dans les applications textes sont communément affichées en mode inversé; un lecteur de texte peut avoir besoin de mettre en évidence certains mots. Curses supportes cela en vous permettant de spécifier un attribut pour chaque cellule sur l'écran.

Un attribut est un entier, chaque bit représente un attribut différent. Vous pouvez essayer d'afficher du texte avec plusieurs attributs à la fois, mais curses ne garantie pas que toutes les combinaisons sont possibles, ou qu'elles sont toutes différentes visuellement. Cela dépend des capacités du terminal utilisé, alors le plus sûr est encore d'utiliser les attributs les plus communs, listés ici :

Attribut Description
A_BLINK Texte clignotant
A_BOLD Texte plus brillant ou en gras
A_DIM Texte à moitier brillant
A_REVERSE Couleur inversé
A_STANDOUT Le meilleur mode de mise en évidence disponible
A_UNDERLINE Texte souligné

Ainsi, pour afficher une ligne d'état en haut de l'écran, vous pouvez écrire :

stdscr.addstr(0, 0, "Current mode: Typing mode", curses.A_REVERSE)
stdscr.refresh()

La bibliothèque curses supporte aussi les couleurs sur les terminaux qui le permettent. Le plus commun terminal est probablement la console Linux, suivis des versions couleur d'xterm.

Pour utiliser les couleurs, vous devez appeler la fonction start_color() juste après avoir appelé initscr(), afin d'initialiser l'ensemble de couleurs par défaut (la fonction curses.wrapper.wrapper() fait cela automatiquement). Une fois que cela est fait, la fonction has_color() retourne True si le terminal utilisé peut afficher des couleurs.

La bibliothèque curses entretient un certain nombre de paires de couleurs, contenant une couleur de premier plan (le texte) et une couleur de fond. Vous pouvez obtenir la valeur de l'attribut correspondant à une paire de couleur avec la fonction color_pair(); ceux-ci peuvent être mélangés à d'autres attributs comme un A_REVERSE avec une opération binaire OU, mais encore une fois, toutes les combinaisons ne fonctionneront pas forcément avec tous les terminaux.

Un exemple, qui affiche une ligne de texte en utilisant la paire de couleurs 1 :

stdscr.addstr( "Pretty text", curses.color_pair(1) )
stdscr.refresh()

Comme je l'ai dit précédement, une paire de couleurs consiste en une couleur de premier plan et une couleur de fond. start_color() initialise 8 couleurs de base lorsqu'il active le mode coloré. Il y a des numéros de couleur 0 à 7 qui correspondent respectivement aux couleurs noir, rouge, verte, jaune, bleue, magenta, cyan, et blanc. Le module curses définit des constantes pour chacune de ces couleurs : curses.COLOR_BLACK, curses.COLOR_RED, et ainsi de suite.

La fonction init_pair(n, f, b) change la définition de la paire n, pour la couleur de premier plan f, et la couleur de fond b. La paire de couleur 0 est définie « en dur » sur le noir et blanc, et ne peut être changée.

Essayons cela en changeant la paire de couleur 1 pour un texte route au fond blanc :

curses.init_pair(1, curses.COLOR_RED, curses.COLOR_WHITE)

Quand vous changez une paire de couleur, le texte déjà affiché qui utilise cette paire va changer de couleur. Vous pouvez aussi afficher un nouveau texte utilisant cette couleur avec :

stdscr.addstr(0,0, "RED ALERT!", curses.color_pair(1) )

Les terminaux les plus jolis permettent de changer les couleurs avec une valeur RVB. Cela vous permet de changer la couleur 1, qui est habituellement rouge, en violet, bleu, ou n'importe quel couleur que vous aimez. Malheureusement, la console Linux ne supporte pas cela, donc je ne peux essayer cela et fournir aucun exemple. Vous pouvez vérifier la compatibilité de votre terminal en appelant can_change_color() qui retournera True si le terminal le permet. Si vous avez la chance d'avoir un terminal aussi talentueux, vous pouvez consulter vos pages de manuel pour plus d'informations.

Entrées utilisateur

La bibliothèque curses par elle même n'offre que de simples mécanismes d'entrées utilisateur. Le module Python permet l'utilisation d'un widget d'entrée de texte qui permet de combler ce vide.

La plus commune manière d'obtenir une entrée dans une fenêtre est d'utiliser sa méthode getch(). getch() met en pause et attend que l'utilise appuis sur une touche, en affichant celle-ci si echo() a été appelé plus tôt. Vous pouvez également spécifier une coordonnée où déplacer le curseur avant la pause.

Il est possible de changer ce comportement avec la méthode nodelay(). Après nodelay(1), getch() va devenir non-bloquant et retourner curses.ERR quand aucune entrée n'est faite. Il y a aussi une fonction halfdelay() qui peut être utilisée pour définir un timeout sur chaque getch(); si aucune entrée n'est faite après le temps impartis (mesuré en dixième de seconde), curses déclenche une exception.

La méthode getch() retourne un entier; si il est compris entre 0 et 255, il représente le code ASCII de la touche pressée. Les valeurs supérieurs à 255 sont des touches spéciales comme Page UP, Home ou les touches du curseur. Vous pouvez comparer la valeur retournée à des constantes comme curses.KEY_PPAGE, curses.KEY_HOME ou curses.KEY_LEFT. Habituellement, la boucle principale de votre programme va ressembler à ça :

while 1:
    c = stdscr.getch()
    if c == ord('p'): PrintDocument()
    elif c == ord('q'): break  # Sortir de la boucle
    elif c == curses.KEY_HOME: x = y = 0

Le module curses.ascii apporte une classe ASCII possédant des méthodes statiques qui prennent en argument un entier ou une chaine d'un caractère; elles peuvent être pratiques pour écrire des tests plus lisibles pour vos interpréteurs de commandes. Elle apporte aussi une fonction de conversion qui prend à la fois en argument un entier ou une chaine d'un caractère et retourne le même type. Par exemple, curses.ascii.ctrl() retourne le caractère de contrôle correspondant à ses arguments.

Il y a aussi une méthode pour récupérer une chaine entière, getstr(). Elle est pas utilisée souvent car ses fonctionnalités sont un peu limités; les seuls touches d'éditions disponibles sont la touche d'effacement arrière et la touche entrée qui termine la chaine. Elle peut optionnellement être limité à un nombre fixe de caractères.

curses.echo()            # Activation de l'écho des touches

# Obtenir une chaine de 15 caractères avec le curseur au début de la ligne
s = stdscr.getstr(0,0, 15)

Le module Python curses.textpad fournit quelque chose de plus puissant. Avec lui, vous pouvez transformer une fenêtre en boite de texte qui supporte des fonctions d'éditions à-la-Emacs. Plusieurs méthodes de la classe Textbox supportent l'édition avec validation de l'entrée, et collecte des résultats avec ou non espaces. Lisez la documentation de la bibliothèque curses.textpad pour plus dé détails.

Pour plus d'informations

Ce How To ne couvre pas certains sujets avancés comme le screen-scraping, ou la capture des évènements de la souris depuis une instance d'xterm. Mais la page de la documentation Python de la bibliothèque curses est maintenant utilisable. Vous pouvez l'utiliser pour aller plus loin.

Si vous avez des doutes sur le comportement des points d'entrés de ncurses, consultez les pages de manuel de votre implémentation de curses, que ce soit pour ncurses ou une version provenant d'un Unix propriétaire. La page de manuel documentera n'importe quel bizarerie et apportera une liste complète des toutes les fonctions, attributs, et caractères ACS_* qui vous sont disponible.

Du fait de la grosseur de l'API de curses, certaines fonctions ne sont pas supportées avec l'interface Python, pas parce qu'elles sont difficile à implémenter, mais parce que personne n'en à jamais eu besoin. Vous êtes libre de les ajouter et de soumettre un patch. Il y a aussi peu de bibliothèque de menus ou de panneaux pour curses, n'hésitez pas à en faire une. Si vous écrivez un petit programme intéressant, contribuez en l'ajoutant en démos disponibles. Ca peut toujours en intéresser certains !

La FAQ ncurses (en anglais) : http://invisible-island.net/ncurses/ncurses.faq.html.

NDT : vous pouvez aussi jeter un oeil sur la documentation Python du module curses : http://www.python.org/doc/2.6/library/curses.html.

Escaline2 : la marmotte s'est remise au travail

Bon, voilà, je me suis enfin décidé de me sortir les doigts du derrière pour bosser un peu sur la nouvelle mouture d'Escaline. Pour ceux qui ne suivent pas d'assez près mes fabuleuses, bien que peu nombreuses, créations (ainsi que celles de Sunny en l'occurrence), Escaline est un shell pour le web. Il permet d'enregistrer des commandes qui seront des alias vers (par exemple) différents moteurs de recherche. Il est ensuite possible de l'installer sur son navigateur, au travers d'une configuration pour les navigateurs Gecko (Firefox, Epiphany) ou d'un plugin opensearch.

La version 2 d'Escaline est une refonte complète du site. On passe du framework web.py sur Python 2.4 au framework Django sur Python 2.5 (ou Python 2.6, il faut que je voie si Lenny sera à jour sur ce point). Mais ce n'est pas la seule nouveauté, car j'en ai profité pour implémenter tout un tas de fonctions cool-es que tout le monde attend (ou pas).

Tout d'abord, j'ai implémenté les commandes multi-arguments (fonction qui a réellement été demandée par "beaucoup de monde"). Il sera possible d'écrire des commandes beaucoup plus complexes que ce qui se fait maintenant. Par exemple, il sera possible d'écrire une URI qui permet de gérer une commande de ce style :

g lang:lr%3Dlang_fr type:image Escaline is good

L'URI en question pourrait être :

http://www.google.fr/{type:search}?q={0}[&meta={lang}]

Comme vous pouvez le deviner, le nouveau parser gère les arguments nommés ("lang", "type"), possédant une valeur par défaut (définie après les deux points, a droite du nom de l'argument), ou avec des crochets qui permettent de supprimer du texte autour de l'argument si celui-ci n'est pas renseigné.

Mais cette commande n'est pas très belle. Sérieusement, qui penserait à mettre "lr%3Dlang_fr" à coté de lang pour faire une recherche en français ? Et puis je suis bien bloqué avec mon type, car je n'ai finalement le choix qu'entre des images et du web... Le nouveau parser est certes mieux, mais je le trouve un peu limite pour certains sites un peu tordus. J'ai donc développé un nouveau système de commandes : les commandes "avancées", ou "programmées". Le principe est de passer par un langage de programmation pour définir une commande. Le parser permet toujours de parser les arguments qui entrent en ligne de commande (ce n'est pas obligatoire), mais son résultat au lieu d'être passé à un autre parser qui génère une uri est passé à un programme. Le langage choisit pour cela est le Lua, parce qu'il est hyper léger (l'interpreteur doit faire une centaine de Ko), parce qu'il est hyper facile à utiliser (sa syntaxe s'apprend en à peine 30mn, et encore) et parce qu'il est facilement embarquable et sandboxable (il est fait pour ça). Il sera donc possible d'écrire avec ça des vraies commandes de fous, avec tout plein d'arguments, ou des commandes qui ne retournent pas une URI vers qui se rediriger, mais qui retourne du texte à l'utilisateur (exemple, calc 14+3 retournerait "14+3 = 42").

Voici un exemple de ce que pourrait être le code de la commande passée plus haut :

#!lua
-- Recherche google

search_type = kwargv.type
language = kwargv.lang

search_type_mapping = { web = 'search', image = 'images' }
language_mapping = { fr = 'lr%3Dlang_fr', en = ''}

-- Valeurs par défaut
if search_type == nil then
	search_type = 'web'
end
if language == nil then
	language = 'fr'
end

-- Création de l'uri
uri = string.format('http://google.fr/%s?q=lala&meta=%s',
		search_type_mapping[search_type], language_mapping[language])

-- Retour du résultat
escaline.return_uri(uri)

La troisième nouveauté est plus mineure mais non pas moins utile (à par sur les EEEpc et autres écrans taillés pour les mouches) : il sera possible de chainer des commandes. Ceci est une fonctionnalité du nouveau parser, mais c'est un peu à part du reste. En clair, si vous tapez g Escaline && y Escaline, Escaline ouvrira les deux pages des deux moteurs de recherche, chacune dans une frame. Ceci permettra très facilement de comparer les résultats de deux (ou plus) moteurs.

Escaline en commandes multiples

La quatrième nouveauté est un système de ticket utilisateur. Ces tickets remplaceront un éventuel système de compte utilisateur (qui n'existe pas actuellement), mais de manière anonyme. Le principe c'est de se créer un ticket avec une commande. Le site vous retourne ensuite un numéro et enregistre un cookie sur le navigateur. Grâce au numéro, vous pouvez installer le cookie sur d'autres navigateurs (avec une autre commande). Une fois le cookie en place, vous pouvez toucher à un certain nombre d'options (avec une troisième commande) qui permettront par exemple de personnaliser le logo, de changer le moteur de recherche par défaut etc.

Enfin, la dernière nouveauté (qui sera révélée aujourd'hui) est la disponibilité des sources et ce dès maintenant. Vous pouvez suivre, récupérer et tester l'avancement du site qui est disponible sous forme de dépot git sur idevelop.org. Les sources seront bien entendus libres (releasées sous licence GPL3). Cependant, le principe d'Escaline étant d'être centralisé, on espère que les gens ne s'amuseront pas à créer 150 clones publiques. Pour terminer là-dessus, notez qu'en plus de suivre, récupérer ou tester Escaline, vous pouvez aussi y participer, surtout si vous avez quelques compétences en design, intégration ou Javascript. N'hésitez pas à me contacter si ça vous intéresse.

L'algorithme de diksta... dijstrak... DIJKSTRA !

Ce weekend j'avais envie de programmer un peu. Un truc nouveau (du moins pour moi), que je ne connais pas trop, et pas forcément très utile. Mon choix s'est porté sur l'implémentation de l'algorithme de Dijkstra permettant de rechercher quel est le plus court chemin entre deux nœuds dans un graphe. C'est un algorithme utilisé en autre par le protocole de routage dynamique OSPF (où chaque nœud est un routeur et chaque arc une liaison).

L'algorithme de Dijkstra permet d'attribuer à chaque arc du graph un poids. Le chemin le plus court n'est dont pas déterminé par le nombre de nœuds qui séparent ceux de départ et d'arrivée, mais par le poids des arcs. Par exemple, pour le graphe ci-dessous, pour aller de A à F, l'algorithme de Dijkstra prendra le chemin qui passe par A, C, D, E, F même si celui-ci traverse plus de nœuds.

Graphe

Mon implémentation du Dijkstra est appliquée au réseau du métro de Paris, chaque station étant un nœud et le chemin entre deux un arc. Le but est donc de rechercher le plus court chemin entre deux stations. Bien entendu, je ne cherche pas à concurrencer l'outil du site de la RATP pour calculer les itinéraires, ces derniers ayant accès à bien plus de facteurs que moi pour leur calcul (comme la durée de voyage entre deux stations, les éventuels incidents en cours, l'heure et la fréquence des trains sur les lignes, le temps de marche entre les correspondances etc.). Le but est juste de mettre en pratique l'algorithme.

Avant de continuer sur mon implémentation, je vais expliquer rapidement et en images comment fonctionne le Dijkstra.

La première phase est une sorte d'initialisation où l'on va compter la valeur des poids qui séparent deux nœuds en gardant la valeur la plus faible (en cas de chemins multiples) et en partant du nœud de départ. Pour cela, ajouter deux attributs à un nœud : son prédécesseur et la somme du chemin le plus court vers le nœud de départ.

La première chose à faire sera d'initialiser ces valeurs. Le point de départ aura une somme de chemin parcouru égale à zéro, et les autre à l'infini. Aucun n'aura de prédécesseur.

Graphe

On va ensuite passer par chaque nœud, mais dans un ordre particulier : en prenant à chaque fois celui qui à le chemin parcouru le plus court.

Pour chaque nœud (appelons le N1), on prends tous les autres nœuds qui lui sont connecté. Et pour chacun de ces nœuds (appelons les N2), on regarde si le chemin parcouru par N2 est supérieur à la somme du chemin parcouru par N1 et le poids de l'arc qui sépare les deux. Si c'est le cas, on définit la somme des chemins parcourus par N2 à la somme du chemins parcouru par N1 + la valeur du poids de l'arc, ainsi que le prédécesseur de N2 à N1.

Graphe

Au final, on se retrouve avec un graphe dont chaque nœud possède un prédécesseur qui est le chemin le plus court vers le nœud de départ.

La seconde phase consiste à récupérer le chemin à parcourir pour aller du nœud d'arrivée au nœud de départ. Il suffit de prendre le prédécesseur du nœud d'arrivée, puis le prédécesseur de ce prédécesseur, puis le prédécesseur du prédécesseur du prédécesseur... Pour enfin arriver au nœud de départ. A chaque fois, on stocke le nœud en cours au début d'une liste (par exemple), et au final la liste contiendra la liste de nœud à passer pour aller du départ à l'arrivée.

Dans mon cas (celui du métro pour ceux qui ne suivent pas), j'avais un autre facteur important à traiter : les lignes et par extension, les correspondances. Il faut en premier lieu savoir dans quel ligne est parcouru un nœud, car il faudra au final, lorsque l'on synthétise le trajet pouvoir indiquer à l'utilisateur les changements à faire. En second lieu, imposer un malus dans le poids lorsque l'on change de ligne.

J'ai donc ajouté un attribut aux arcs : la ligne (de métro) qu'ils représentent. Ensuite, lors du calcul des chemins parcourus, pour chaque arc entre chaque nœud N2 qui est relié à chaque nœud N1, je regarde si un changement de ligne sera effectué au passage de N1 à N2, et ajoute un malus de correspondance au poids si cela est vrai.

Exemple d'exécution :

naps@leon ~/D/l/path> python metro.py
Bienvenue dans le programme de calcul d'itinéraires
 
Entrez une station de départ
>lib
Station Liberté selectionnée pour le départ.
 
Entrez une station d'arrivée
>villette
Station Porte de la Villette selectionnée pour l'arrivée.
 
Itinéraire :
============
 
 - Entrez dans la station Liberté et prenez le métro (sans blague)
 - Changez à République et prendre la ligne 5
 - Changez à Stalingrad et prendre la ligne 7
 - Arrivée à Porte de la Villette (trop bien \o)

Pour terminer, le code est disponible sur mon Gist, pour Python 2.6 (mon premier 1!), et le fichier décrivant la liste des stations est au même endroit, plus bas dans la page. Je n'ai pas pris la peine de le commenter, le code n'est surement pas optimisé au mieux, et est donc un peu crado sur les bords, mais ceux que ça intéresse pourront, je l'espère, le ré-implémenter avec mes explications ci-haut.

Mon premier module sur le CheeseShop

Ça faisais longtemps que je voulais m'y intéresser : la distribution des modules Python est un truc qui m'a toujours paru hyper tordu. Il faut créer un fichier setup.py, il y a des eggs, zc.buildout qui est sensé être mieux que le reste (quel reste ?). Cependant, lors du Pycon 2008, une conférence sur le déploiement d'application Python avait été tenue, et tout ça me semblait très intéressant : création automatisée de l'egg, le fichier setup.py avait l'air d'être relativement simple, il était possible d'envoyer simplement son module sur le Pypi (ou CheeseShop, une base de donnée de modules en Python), et tel que présenté par le conférencier (Tarek Ziade), ça avait l'air plutôt facile.

Bon, au final, après m'être renseigné, c'est toujours un peu flou. J'ai laissé tombé zc.buildout, qui semble être réservé à des applications autrement plus avancées que la simple distribution d'un module, et je me suis porté sur l'écriture d'un fichier setup.py en utilisant les distutils.

Je ne vais pas trop m'avancer sur le sujet, justement parce que c'est encore trop flou. Donc je préviens : il est possible que je raconte nimporte quoi (m'enfin, ça marche alors bon...).

Pour commencer, voici l'arborescence de mon projet :

.
|-- COPYING
|-- README
|-- arprequest
|   |-- __init__.py
|   `-- arprequest.py
`-- setup.py
  • Le fichier COPYING contient la licence du module. C'est pas "techniquement" obligatoire mais c'est vivement conseillé.
  • Le fichier README contient une notice d'utilisation du module. J'utilise le contenu de ce fichier pour constituer la description de ma fiche sur le CheeseShop. Il est formaté en ReStructuredText (un langage de balisage léger), ce dernier étant utilisé par le CheeseShop.
  • arprequest est le dossier constituant le module en lui même. Il contient le fichier Python qui contient mes classes, et un fichier __init__.py qui sert à indiquer que le dossier est un module (ceci ne sera plus necessaire sous Py3K il me semble). Pour éviter de d'avoir une structure trop profonde pour accéder aux classes, j'ai ajouté dans ce fichier from arprequest import * qui permet d'importer les classes directement dans le module (from arprequest import ArpRequest et pas from arprequest.arprequest import ArpRequest). J'utilise cette "astuce" car je n'ai pas trouvé comment indiquer au setup.py que le module est un seul est unique fichier.
  • Enfin, le fichier setup.py que je vais détailler ci-dessous.

Fichier setup.py (je pense que les commentaires seront suffisants) :

from setuptools import setup
import os

# Comme expliqué plus haut, je récupère le contenu du fichier README :
ldesc = open(os.path.join(os.path.dirname(__file__), 'README')).read()

# On appelle juste la fonction setup() avec un certain nombre d'arguments :
setup(

    name='arprequest', # Le nom du module

    version='0.2a', # La version

    description=('A class which send an ARP Request to know if a '
                    'host is online on local networks'), # Une description courte

    long_description=ldesc, # La description un peu plus longue (mon fichier README)

    keywords='arp network ethernet', # Des mots clés

    author='Antoine Millet', # Le nom de l'auteur

    author_email='antoine@inaps-dies-spammers.org', # Je dois vraiment continuer ? :)

    license='WTFPL', 

    packages=['arprequest'], # Les différents modules, c'est ce sur quoi j'ai encore des doutes sur le fonctionnement

    url='http://dev.inaps.org/trac/wiki/ArpRequest',

    # Des "catégories", la liste est disponible http://pypi.python.org/pypi?%3Aaction=list_classifiers. 
    classifiers=[
        'Topic :: Communications',
        'Topic :: System :: Networking',
        'Topic :: System :: Networking :: Monitoring',
        'Development Status :: 3 - Alpha',
        'Intended Audience :: Developers',
        'Intended Audience :: System Administrators',
        'License :: Public Domain',
        'Operating System :: Unix',
        'Programming Language :: Python',
    ],
)

Une fois le fichier setup.py écrit, il faut l'appeler avec un certain nombre d'arguments :

python setup.py sdist
Créer une archive qui contient les sources du projet.
python setup.py bdist_egg
Créer un EGG (package python) qui contient le module (les sources C sont compilées).
python setup.py install
Permet d'installer le module.
python setup.py register
Enregistre le module sur le CheeseShop.
python setup.py upload
Envoyer les packages (sources, eggs) sur le CheeseShop.
python setup.py register sdist bdist_egg upload
Créer une archive des sources, un egg, enregistre le module et envois les packages.

Pour ma part, après l'exécution de cette dernière commande, j'ai eu le plaisir de constater que la page de mon module avait correctement été créée sur le CheeseShop :-).

MAJ : Bon après coup, j'ai trouvé comment définir le contenu du package avec de simples modules Python : il suffit d'utiliser py_modules=['nomdumodule'] à la place de packages=[...]. Ça m'apprendra à lire la doc comme un pied.

OMG, une nouvelle version d'I, NaPs ?!

Je vous avais pourtant demandé de me frapper le jour ou l'idée de refaire mon blog avec un truc home-made me reprenait, merci les gars.

Bon, bah ça y est, I, NaPs version 4 est terminé, et devant vos yeux. Déjà la 4ème version. Pour rappel, la première version s'appelait naps-systeme.net et tournait sous WordPress, c'était en 2005. Un peu moins d'un an plus tard, changement de nom, et de blogware pour un truc perso en Python sur framework WebPY, puis refonte de celui ci (version 2bis dirons-nous). Mais ce dernier n'a jamais été terminé par manque de temps, manquant de certaines fonctions bien utiles, je décide de repasser (une fois pour toute ?) sous WordPress, encore une fois, migration douloureuse.

Cette nouvelle version, je l'ai réalisée sous Django, après avoir pu apprécier la beauté du truc pendant un stage. Encore une fois, j'ai été surpris par le temps necessaire au développement : j'ai commencé durant aout, mais ais continué très irrégulièrement. Si j'ai travaillé en tout et pour tout pendant deux semaines là dessus, c'est un grand maximum.

Bref, nouveau blogware, nouveau design, mais globalement les mêmes pages, structures, fonctionnement etc. Je vous laisse apprécier tout ça, flemme de faire un ChangeLog. Par ailleurs, je n'ai pas beaucoup testé le site, il est très possible de tomber sur un bug, dans ce cas, merci de me le signaler (le lien pour me contacter se trouve toujours en bas des pages).

J'utilise Escaline 
!