Données d’un capteur vers un serveur Web

Introduction

Lors de mon précédent article, j’évoque l’envoi des données via Wifi depuis un ensemble de composants autour d’une carte Arduino. Or, la programmation n’est pas très facile pour donner plus d’intelligence à ces capteurs. Je suis donc reparti du capteur de température et de pression DHT11, mais relié à un Raspberry PI.


DHT11 - Cảm biến nhiệt độ và độ ẩm https://images.duckduckgo.com/iu/?u=http%3A%2F%2Fwww.openmediacentre.com.au%2Ffileadmin%2Fuser_upload%2Ftx_onqcatalogue%2Fproduct%2Fraspberry-pi-model-b_80_thumbimg1.jpg&f=1
(les photos ne sont pas à l’échelle)
Prix des composants :

  • Raspberry Pi 2 starter kit (avec les câbles, l’alimentation, une carte SD, des câbles, des radiateurs, un boitier, etc) : 75 € sur Amazon
  • DHT11 (capteur nu) : 2.20 € sur Amazon
  • Dongle Wifi (sur port USB) : quelques Euros (sur eBay)

La programmation est faite en script SH et Python dans le Raspberry et je dispose d’un serveur PHP qui héberge les données (mon PC).

 

Vue du Raspberry avec les câbles pour clavier, souris, écran (inutiles en mode normal, mais nécessaire pour la programmation) + le composant DHT11, relié par 3 fils.

 

Vue des composants minimum pour que les données soient envoyées après avoir tout programmé. Il faudra juste prévoir une boite pour protéger le Raspberry et laisser le capteur « à l’air libre ».

 

Protocole de communication – 1. Souscription

Le Raspberry dispose d’un Dongle Wifi pour se connecter au serveur. J’utilise donc l’adresse MAC du Wifi pour souscrire au serveur selon ce principe :

Dans ces échanges, l’adresse MAC sert de clé. Chaque unité de communication devrait avoir une adresse MAC unique sur le réseau.

Une fois le capteur identifié sur le serveur, il n’est pas encore actif. Le serveur a un capteur en attente de paramétrage manuel (donner un nom compréhensible par un humain, un picto, un groupe, une catégorie, etc…). Tant que cette étape n’est pas faite, les données ne sont pas transmises. Le capteur passe à l’état « Activé » lorsque le paramétrage est terminé.

 

Protocole de communication – 2. Envoi des données

Le serveur est maintenant prêt à réceptionner les données du capteur.

Dans le cas du DHT11, il y a 2 données à chaque envoi : température (en °C) et humidité (en %). Le Raspberry ne fait qu’un appel au serveur pour ces 2 informations.

 

Montage électronique

Le montage sur les PINs du Raspberry sont faits directement car le DHT11 accepte un voltage de 3.3v, fourni par le PIN numéro 1. Si cette alimentation directe n’est pas assez stable, on pourra utiliser un régulateur de tension (AMS1117, 4.70 € les 10 pièces sur Amazon), prenant en entrée du 5v et donnant en sortie du 3.3v stabilisé (monté avec un condensateur de 470µF). Ce sera surtout le cas si on utilise une autre source d’énergie qu’une prise de courant (pile, accu, batterie de voiture, capteurs solaires, etc).

 

Le DHT11 comme capteur

Le DHT11 est un composant assez instable, qui ne garanti pas toujours la bonne lecture des données. Il semble que d’autres capteurs soient plus efficaces, peu onéreux, fonctionnant avec une plus grande plage de voltage : TMP36GT9, DS18B20, etc.

Le DHT11 est par contre bien documenté, pour les bidouilleurs comme moi, devant programmer en Python.

 

Copies d’écran du serveur

Liste des capteurs

 

 

Détail d’un capteur d’humidité

 

Pour aller plus loin

A partir de ce système, il est possible d’équiper toute la maison, d’avoir des capteurs partout et de suivre les courbes. Le budget à prévoir reste collé au prix du Raspberry (carte nue à 40 €), car les capteurs ne coûtent que quelques Euros. D’autres solutions voient le jour, avec des cartes électroniques programmables, comme TESSEL (avec Wifi intégré) : https://www.tessel.io/

D’autres capteurs pourront être ajoutés, sous réserve de pouvoir lire les données (sur le même principe), depuis les ports GPIO du Raspberry. Grâce au grand nombre de PINs disponibles, on pourrait ajouter d’autres capteurs à un même Raspberry, recueillant ainsi un lot de données. Pour une pièce de la maison : température, humidité, ensoleillement, concentration CO2, odeur (oui, pour faire remonter le niveau de puanteur du placard à chaussures), méthane, détecteur de mouvement, etc.

 

OpenCV pour mesurer la qualité des images – update

Introduction

Un premier article (OpenCV pour mesurer la qualité des images) a été rédigé il y a fort longtemps sur la mesure de qualité d’une image ou d’une photo, par rapport au flou ou au défaut de mise au point. A vrai dire, je n’étais pas complètement satisfait du résultat, en comparant 2 photos ayant de légères différences de netteté. Après d’autres recherches et tests, voici une version nettement améliorée.

 

Le script v2

import cv2, sys

filepath = sys.argv[1]
filename = sys.argv[2]

myCamera = cv2.VideoCapture(0)
if myCamera.isOpened():
    ret, frame = myCamera.read()
    gray = cv2.cvtColor( frame, cv2.COLOR_BGR2GRAY)
    variation = cv2.Laplacian(gray, cv2.CV_64F).var()

    finalFilename = "%s/%d-%s" % (filepath, variation, filename)
    cv2.imwrite(finalFilename, frame)

 

Notes et remarques

Ce script donne un autre résultat, en se basant sur une image en tons de gris et en calculant le laplacien. Ensuite, il calcule la variation sous forme d’un scalaire, donc facilement exploitable pour trier les photos nettes de celles qui le sont moins.

Ce script est utilisé pour un mode « rafale » de prises de vues sur une webcam (derrière un télescope), pour isoler les photos les plus nettes. L’indice de qualité est le préfixe du nom de fichier, donc c’est facile d’avoir les 50 meilleures photos et les 50 les plus floues.

 

Impressive : des slideshow en python

A propos d’impressive

Le logiciel « impressive » est écrit en python et permet de faire des slideshow en boucle avec un délai entre les slides et des effets de transition. C’est un peu comme MS PowerPoint, mais il n’y a pas de gestion d’effet de texte (qui arrive au fur et à mesure, par exemple).

En plus, le paramétrage se fait par script ou fichier de configuration. Il est donc réservé à des utilisateurs avertis, avec des notions de programmation.

Le programme accepte différents paramètres en entrée :

 

Exemple d’usage basique

Lors d’un tournoi de sport, un écran est disponible au public et plusieurs images sont présentées. Ces images tournent en boucle ; certaines sont affichées plus longtemps que d’autres. Vous allez utiliser Impressive avec un script pré-paramétré qui détaillera les images à présenter (l’ordre d’apparition, la durée d’affichage, l’effet de transition vers le slide suivant).

Exemple de script :

# -*- coding: iso-8859-1 -*-
PageProps = {
  1: {
    '_file': "01-presentation-01.png",
    'timeout': 10000,
    'transition': Crossfade
     },
  2: {
    '_file': "01-presentation-02.png",
    'timeout': 10000,
    'transition': SlideLeft
     },
  3: {
    '_file': "01-presentation-03.png",
    'timeout': 30000,
    'transition': WipeDown
     },
  4: {
    '_file': "02-deroulement-01.png",
    'timeout': 30000,
    'transition': ZoomOutIn
     },
  5: {
    '_file': "03-lots-01.png",
    'timeout': 10000,
    'transition': SlideLeft
     }
}

Syntaxe :

  • ‘_file’ : le nom de l’image à afficher
  • ‘timeout’ : durée d’affichage en millisecondes
  • ‘transition’ : effet de transition vers le slide suivant

Pour quitter le slideshow avec un code retour particulier, il faut utiliser la syntaxe suivante :

{
...
  11: {
    '_file': "06-club-02.png",
    'timeout': 10000,
    'OnLeave': lambda: Quit(10)
     }
}

 

Le programme sera lancé avec ce script, selon la syntaxe suivante :

impressive --wrap --nologo --script 01-accueil.info *.png

Le slideshow sera affiché en boucle, en prenant toutes les images du répertoire. Seules les images du script seront utilisées.

 

Aller plus loin

Avec cette syntaxe, le programme couvre beaucoup de cas. Mais imaginons que les équipes du tournoi sont classée (avec un système de points) et qu’il serait intéressant de présenter des slides avec le classement de chaque équipe.

Pour ce faire, je vous propose un script en python qui, à partir d’une image de fond, va lire un fichier et écrire du texte pour générer l’image finale à intégrer dans une présentation.

image de fond

L’image dispose d’un cadre blanc pour écrire du texte par dessus.

 

Fichier du classement

# classement général 1/4
1 | Equipe A | 25 pts
2 | Equipe B | 20 pts
3 | Equipe C | 19 pts
4 | Equipe D | 15 pts
5 | Equipe E | 10 pts
6 | Equipe F | 9 pts

Le fichier défini 3 colonnes et présente 6 équipes sur les 24 en lisse. Il y aura donc 4 slides à faire pour le classement complet.

 

Image finale

Le texte est écrit avec la même police que celle utilise pour l’image de fond.

 

Script Python

Le script utilise PIL pour lire l’image, écrire le texte avec la bonne police et sauver l’image finale.

#!/usr/bin/env python

from PIL import Image
from PIL import ImageFont
from PIL import ImageDraw
import sys

background = sys.argv[1]
textfile = sys.argv[2]
outfile = sys.argv[3]

# top left starting position
position = (260,260)
color = (0,0,0)
fontsize = 72

img = Image.open(background)
draw = ImageDraw.Draw(img)
font = ImageFont.truetype("GROBOLD.ttf", fontsize)

resultsFile = open(textfile, 'r')
for line in resultsFile:
    if len(line.strip()) == 0:
        continue
    if line.strip()[0] == "#":
        continue
    fragments = line.split('|')
    if len(fragments) < 2:
        continue
    rang = fragments[0].strip()
    equipe = fragments[1].strip()
    score = fragments[2].strip()
    draw.text(position, rang, color, font=font)
    draw.text((position[0]+200, position[1]), equipe, color, font=font)
    draw.text( (position[0]+1000, position[1]), score, color, font=font)
    position = (position[0], position[1]+(fontsize*1.75) )
img.save(outfile)

Ce petit script Python prend 3 paramètres :

  • l’image de fond
  • le fichier texte
  • l’image à sauvegarder

Il faut préciser que les images font 1920×1080 pixels, d’où les positions d’écriture des textes.

OpenCV pour mesurer la qualité des images

Introduction

J’ai passé une autre soirée astro pour prendre des photos de Jupiter. Environ 220 clichés de plus ou moins bonne qualité, en fonction de l’atmosphère sont à trier. En général, certaines photos sont trop floues.

Comment faire le tri entre toutes ces photos pour ne garder que les plus nettes ? OpenCV va vous sauver la vie !

 

Principe

L’objectif est d’avoir un chiffre « qualité du focus », calculé sur chaque photo qui me permettra de trier mes 220 photos pour n’en sortir que les 100 plus nettes.

Pour ce faire, la technique la plus en vogue sur le net (stack overflow + publications) et de faire 2 filtres Sobel sur l’image, calculer les pics de gradients et faire une somme globale. Plus la somme est élevée, plus il y a de netteté.

 

Rappel / info à savoir

Sobel est un des filtres de détection des bords les plus connus (expliqué à la fac, dans des TP, etc). On peut citer aussi le Laplacien dans cette famille de filtres. Depuis une image en couleurs, ces filtres vont faire une image en dégradé de gris, avec :

  • en noir : les régions où il n’y a pas de bord (aplat de couleur ou changement doux)
  • en blanc : changement brutal de couleur (= un bord)

Ce qui est pratique avec Sobel, c’est qu’on peut lui dire dans quel axe il doit détecter les bords. Pour nous, ce sera un dans l’axe X et un dans l’axe Y de chaque image. Les pics de gradients se feront sur ces 2 axes pour avoir une large gamme de détection.

 

OpenCV

Ce n’est pas le premier script Python posté sur mon site qui utilise OpenCV. Pour ceux qui ne le savaient pas, Python a été créé à l’époque (au siècle dernier) pour interfacer du C/C++, donc faire des appels de fonctions programmées en C et compilées sur le PC. Ceci avec un langage plus simple que le C : le Python !

Bref, OpenCV propose une tonne de fonctions pour manipuler les images. Toutes, très éprouvées, très optimisées, et très math… (désolé pour les non-matheux)

 

Le script

Voici le script Python qui prend en entrée une liste d’images et sort un tri par netteté.

#!/usr/bin/python

import cv2, sys
import operator

files = sys.argv[1:]
all_ratio = {}
for filename in files:
    orig = cv2.imread(filename)

    sobel_dx = cv2.Sobel(orig, cv2.CV_64F, 1, 0, ksize=5)
    sobel_dy = cv2.Sobel(orig, cv2.CV_64F, 0, 1, ksize=5)
    magnitude_image = cv2.magnitude(sobel_dx,sobel_dy,sobel_dx);
    mag, ang = cv2.cartToPolar(sobel_dx, sobel_dy, magnitude_image) 

    ratio = cv2.sumElems(mag[0])
    all_ratio[filename] = ratio[0]

sorted_ratio = sorted(all_ratio.items(), key=operator.itemgetter(1))
index = 1
print(" Rang | Fichier      | Valeur calculee")
print("------|--------------|----------------")
for (filename, ratio) in reversed(sorted_ratio):
    print(" %04d | %s | %d" % (index, filename, ratio))
    index += 1

 

Exemple

Voici les 3 images utilisées dans l’appel du script (cliquez pour voir la photo originale) :

  • Image « apero girl.png », originale :
  • image « apero girl-5.png », copie de l’image originale, avec un flou gaussien de 5px :
  • image « apero girl-20.png », copie de l’image originale, avec un flou gaussien de 20px :

Résultat du script :

 Rang | Fichier      | Valeur calculee
------|--------------|----------------
 0001 | apero girl.png | 154324
 0002 | apero girl-5.png | 146422
 0003 | apero girl-20.png | 142630

Conclusion : l’image « apero girl.png » est le plus net.

 

Remarque

En regardant le script « dans le blanc des yeux », on serait tenté de dire ceci : oui, mais ton script ne te donne de bons résultats uniquement sur des photos proches, d’une prise de vue d’un même objet, qui serait plus ou moins flou.

Et bien, en effet, si on prend 2 photos qui n’ont rien à voir, les indices calculés seront incohérents et le tri par « qualité de netteté » ne voudra rien dire.

Donc, restez vigilant à ne passer dans ce script que des photos d’un même objet, dont la netteté varie.

 

Astrophoto : Découpage autour d’une planète

Introduction

Jupiter est bien visible ce soir ; il n’y a pas trop de nuages et la lune est loin. Je sort rapidement le matériel et prend environ 140 photos plus ou moins floues de la planète, en espérant pouvoir en tirer quelque chose avec des scripts. Je ne mets pas d’oculaire pour grossir l’image, pour voir ce que je peux tirer de ce premier essai. La mise au point se fait tant bien que mal dans le viseur de l’appareil photo.

 

Clichés bruts

Les clichés représentent sur un fond noir la planète, sans ses satellites. Si la pose est plus longue, Jupiter apparaît comme un disque blanc indistinct. Je prend donc plusieurs séries de poses, en 1600 ISO, en 1/500 sec ; et en 800 ISO, en 1/320 sec ; en mode rafale de 10 poses.

Le problème est que selon la série de clichés, Jupiter n’est pas à la même place. Grâce à un script Python, en utilisant la bibliothèque OpenCV, il est possible de :

  • trouver où est la planète dans l’image : sa position
  • déterminer sa taille en pixels : le centre
  • faire un découpage de la zone (10x plus gros que la taille de la planète)

 

Voici le script :

#!/usr/bin/python

import cv, sys
import numpy as np

# grab image
files = sys.argv[1:]
filename = files[0]
filename_base = filename[:filename.rfind(".")]
orig = cv.LoadImage(filename)
orig_clean = cv.LoadImage(filename)

# create tmp images
grey_scale = cv.CreateImage(cv.GetSize(orig), 8, 1)
processed = cv.CreateImage(cv.GetSize(orig), 8, 1)

cv.Smooth(orig, orig, cv.CV_GAUSSIAN, 3, 3)

cv.CvtColor(orig, grey_scale, cv.CV_RGB2GRAY)

# do some processing on the grey scale image
cv.Erode(grey_scale, processed, None, 10)
cv.Dilate(processed, processed, None, 10)
cv.Canny(processed, processed, 5, 70, 3)
cv.Smooth(processed, processed, cv.CV_GAUSSIAN, 15, 15)

storage = cv.CreateMemStorage(0)

contours = cv.FindContours(processed, storage, cv.CV_RETR_EXTERNAL)
# N.B. 'processed' image is modified by this!

#contours = cv.ApproxPoly (contours, storage, cv.CV_POLY_APPROX_DP, 3, 1)
# If you wanted to reduce the number of points...

cv.DrawContours (orig, contours, cv.RGB(0,255,0), cv.RGB(255,0,0), 2, 3, cv.CV_AA, (0, 0)) 

def contour_iterator(contour):
  while contour:
    yield contour
    contour = contour.h_next()

spot_index = 0
for c in contour_iterator(contours):
  # Number of points must be more than or equal to 6 for cv.FitEllipse2
  if len(c) >= 6:
    # Copy the contour into an array of (x,y)s
    PointArray2D32f = cv.CreateMat(1, len(c), cv.CV_32FC2)

    for (i, (x, y)) in enumerate(c):
      PointArray2D32f[0, i] = (x, y)

    # Fits ellipse to current contour.
    (center, size, angle) = cv.FitEllipse2(PointArray2D32f)

    # Convert ellipse data from float to integer representation.
    center = (cv.Round(center[0]), cv.Round(center[1]))
    #print("Center : %dx%d" % center)
    size = (cv.Round(size[0] * 0.5), cv.Round(size[1] * 0.5))
    #print("Size : %dx%d" % size)
    (w,h) = (size[0]*10 , size[1]*10)
    (x,y) = (center[0]-w/2 , center[1]-h/2)
    im_crop = orig_clean[y: y + h, x: x + w]
    cv.SaveImage("%s-%02d.jpg" % (filename_base, spot_index) , im_crop)
    spot_index += 1

Il est inspiré de cette page : http://stackoverflow.com/questions/9860667/writing-robust-color-and-size-invariant-circle-detection-with-opencv-based-on

Image originale fait 4272×2848 pixels (retaillée en 800×533 pour l’insérer dans l’article) :

Voici une extraction par le script (la résolution est conservée) :

L’image est beaucoup plus petite, sans perdre la qualité de la portion qui m’intéresse.

Sur les 140 photos, je ne retient que 19 de bonne qualité. Les autres sont foules ou déformées. Les photos ainsi triées doivent être retaillées pour avoir la même taille et procéder à une fusion (un script imagemagick permet de retailler une image avec un bord noir).

Avec des scripts déjà utilisés dans un article précédent, voici le résultat de cette première expérience (image tournée à 90° pour voir les bandes horizontales) :

 

La suite ?

Sans agrandissement (autre que celui du télescope), la planète reste petite. Je pense qu’il ne sera pas possible d’exploiter mieux les photos pour tirer de meilleurs résultats.

La prochaine fois, je ferai le même procédé avec un grossissement plus fort et une mise au point manuelle optimum.

 

Scripts python pour modifier des images

Besoin : retouche de photo pour l’astronomie

Pendant une nuit de Janvier, la comète LoveJoy est passée dans notre ciel. Ne sachant où regarder et où pointer mon appareil photo, j’ai pris 8 clichés de 30 secondes d’une portion du ciel, avec beaucoup de pollution lumineuse.

Exemple d’image prise (3175 x 1881 px) :

 

Scripts

Après plusieurs essais avec TheGimp, voici les opérations faites sur les images :

  • Analyse visuelle : sur le lot, une image a tracé le passage d’un avion avec ses lumières clignotantes. ça en fait une de moins à traiter.

 

  • Alignement des 7 images, avec un programme spécial. Voici la ligne de commande (le fichier final n’est pas utile, mais les images alignées qui ont servies sont à garder) :
align_image_stack -a aligned -C *.JPG -o merged -v

Les images sont alignées et nommées « aligned0001.jpg » à « aligned0008.jpg ».

 

  • Empilement en mode « fusion » des 7 images. Les images étant assez claires (à cause de la pollution lumineuse), j’ai opté pour une fusion pour compenser ce qui ne serait pas dans une image…

Script :

#!/usr/bin/python

from PIL import ImageChops
import os, Image, glob
import sys

files = sys.argv[1:]
print("> Lighter...")
finalimage=Image.open("./"+files[0])
for i in range(1,len(files)):
    currentimage=Image.open("./"+files[i])
    print("  Processing : %s" % files[i])
    finalimage=ImageChops.lighter(finalimage, currentimage)

outFile = "all-lighter-br.jpg"
print("  Saving : %s" % outFile)
finalimage.save(outFile,"JPEG")

L’image composite est relativement proche d’une image de départ.

 

  • Création d’une image floue. L’objectif est de supprimer la pollution lumineuse. Je suis sont donc parti d’une image proche du fond clair (= l’image floue) pour l’enlever à l’image composite. Les différents paramètres d’ImageMagick ne m’ont pas convaincu, je suis alors passé par un autre script python (avec un rayon de flou à 30 px).
#!/usr/bin/python

import sys
import ImageFilter
from PIL import Image

class MyGaussianBlur(ImageFilter.Filter):
    name = "GaussianBlur"
    def __init__(self, radius=2):
        self.radius = radius

    def filter(self, image):
        return image.gaussian_blur(self.radius) 

inputFile = sys.argv[1]
outputFile = sys.argv[2]
print("> Blur...")
im = Image.open(inputFile)
im1 = im.filter(MyGaussianBlur(radius=30))
print("  Saving : %s" % outputFile)
im1.save(outputFile)

 

  • Soustraction de l’image floue à l’image composite. Là aussi, un script python propose cette fonctionnalité :
#!/usr/bin/python

import sys
from PIL import Image, ImageChops

inputFile_1 = sys.argv[1]
inputFile_2 = sys.argv[2]
outputFile = sys.argv[3]

print("> Substraction...")
im1 = Image.open(inputFile_1)
im2 = Image.open(inputFile_2)
out = ImageChops.subtract(im1,im2,1,0)
print("  Saving : %s" % outputFile)
out.save(outputFile)

 

  • Le résultat manque de peps et reste plutôt terne. Avec TheGimp, j’ai vu que le filtre « screen » d’une image sur elle-même éclaircit les étoiles et garde le fond sombre. Voici le script python pour le faire en une ligne de commande :
#!/usr/bin/python

import sys
from PIL import Image, ImageChops

inputFile_1 = sys.argv[1]
outputFile = sys.argv[2]

print("> Screen...")
im1 = Image.open(inputFile_1)
out = ImageChops.screen(im1,im1)
print("  Saving : %s" % outputFile)
out.save(outputFile)

 

Image finale

La comète est visible, oui, mais c’est un point bleuté parmi les étoiles de la constellation du taureau. J’ai déjà eu de la chance de pointer mon appareil photo vers l’endroit où elle se trouve.

 

Autre version

Voici un autre processus, qui donne de meilleurs résultats :

  • Alignement (comme avant)
  • Flou sur chaque image + suppression pour enlever la pollution lumineuse
  • Ajout de chaque image l’une sur l’autre, avec le script suivant :
#!/usr/bin/python

import os, Image, sys
import numpy as np

files = sys.argv[1:]
print("> Clip...")
image=Image.open("./"+files[0])
im=np.array(image,dtype=np.float32)
for i in range(1,len(files)):
    currentimage=Image.open("./"+files[i])
    im += np.array(currentimage, dtype=np.float32)
im /= len(files) * 0.25 # lowered brightness, with magic factor
# clip, convert back to uint8:
final_image = Image.fromarray(np.uint8(im.clip(0,255)))
final_image.save('all-clip.jpg', 'JPEG')

 

Le résultat est plus évident, mais la comète reste discrète :

Comparatif ionic / Kivy

Introduction

Après quelques applications réalisées avec Kivy, je me rends compte des possibilités et des limites de ce Framework pour réaliser des applications mobiles cross-platform. Je me suis orienté vers une technologie en vogue : Cordova, plus précisément ionic.

Remarque : cet article est le « match aller ». Un autre article « match retour » apporte des précisions.

 

Ionic : Description rapide

Ionic est un Framework pour les développeurs de Javascript. L’objectif est de faire un petit serveur web avec des pages HTML et du code JS. Cordova fourni le serveur web qui sera lancé sur mobile et encapsule le code. Les appels JS à destination du device sont interceptés pour être interprétés selon l’OS (Android, iOS, etc).

L’avantage est que beaucoup de développeurs connaissent le trio HTML + JS + CSS.

D’autres Frameworks sont disponibles avec ces technologies, comme jQuery Mobile et à coupler avec une surcouche, comme Backbone.js pour plus de rigueur. Après une tentative infructueuse, j’ai renoncé à me salir d’avantage les mains.

 

Comparatif de l’installation

Avec Kivy, l’installation est rapide sous Linux car Python est déjà installé. Le téléchargement des fichiers SDK d’Android sont assez rapide car Kivy cible ce qu’il faut télécharger. Ma note : 18/20

Avec ionic, l’émulateur est nécessaire et là, il faut prévoir de faire autre chose pendant les téléchargements. Ma note : 15/20

Mon vainqueur : Kivy

 

Editeur de code (ou IDE)

Il est facile de trouver un IDE : Eclipse avec les plugins pour coder en Python ou en HTML/JS/CSS. Ils peuvent cohabiter dans le même workspace sans souci.

Mon vainqueur : ex-equo

Ma note pour les deux : 18/20

 

Documentation et exemples

Kivy a élevé le niveau avec son API très complète disponible sur le site http://kivy.org ainsi que des projets complets open-source sur GitHub.

Malgré tout, ionic n’est pas en reste, avec un grande communauté de développeurs, le site qui présente son API est assez complète. Il faut parfois jongler entre l’API de Cordova et l’API de ionic pour trouver ses petits.

Mon vainqueur : ex-equo

Ma note pour les deux : 18/20

 

Le premier « Hello world »

Le site de Kivy présente les 6 lignes de code nécessaires à ce test basique. Le principe est facile à comprendre. Facile. Ma note : 18/20

Ionic propose de faire une installation d’une application basique en une ligne de commande. Facile à faire (tout est téléchargé et pré-configuré), moins facile à comprendre comment ca fonctionne dans le détail. Ma note : 15/20

Mon vainqueur : Kivy, d’une courte tête

 

Développement d’une application sur un PC

Kivy lance Python directement depuis Eclipse (après une configuration minimale). Les amateurs de Java ne seront pas déçus. Les logs sont récupérés dans la console d’Ecplise, le mode debug fonctionne à merveille (point d’arrêt, avancée pas à pas, consultation des valeurs des variables, etc). L’application Kivy s’ouvre dans une fenêtre à part pour qu’on puisse interagir. Ma note : 18/20

Pour ionic, il faut lancer une commande qui démarre un serveur web (sur un port libre) et qui ouvre le navigateur par défaut. Chrome est préconisé avec le plugin ADT (pour android). Toute interaction se fait alors dans Chrome. L’IDE devient inutile, il ne sert que d’éditeur de texte. Dommage. Je n’ai pas trouvé le moyen de mettre un point d’arrêt, de consulter les valeurs des variables en temps réel. Au moins, la console de Chrome fonctionne. Ma note : 12/20

Mon vainqueur : Kivy

 

Déploiement sur mobile (Android)

Un outil « magique » est associé à Kivy pour faire l’APK : buildozer. Il faut produire un fichier de configuration pour les autorisations d’accès, le nom du package, le splash screen, l’icone, la version, etc. (un fichier de base est généré avec beaucoup de doc, façon « httpd.conf », très facile à comprendre). A mon avis, un atout très fort. Ma note : 18/20

Ionic n’est pas en reste. Il a lui aussi sa ligne de commande avec le fichier XML de configuration. Moins simple qu’avec buildozer quand même. Ma note : 18/20

Le temps de packaging et l’installation sur le smartphone est aussi rapide.

Mon vainqueur : ex-equo (sans compter la configuration)

 

Développements sans appels au device

La puissance de Python permet de faire beaucoup de choses simplement, avec très peu de code. C’est encore une force de Kivy.

Exemples : lecture de fichier XML ou JSON, appels HTTP-GET ou HTTP-POST, manipulation de chaînes de caractères, tableaux, dictionnaires, appels aux serveurs de cartographie (Google maps, Open Street map, BlueMarble), etc…

Le découpage en couches est facile avec l’utilisation de modules et packages Python pour définir les classes. Ma note : 18/20

Pour ionic, le Javascript permet aussi de faire beaucoup sans faire appel à d’autres choses, peut-être avec un peu plus de complexité. Les bibliothèques JS sont très nombreuses pour aller plus vire. Ionic profite des années de développements JS pour le web.

Pour garantir un code maintenable, ionic s’appuie sur Angular, pas trop complexe à utiliser dans un premier temps. Ma note : 18/20

Mon vainqueur : ex-equo

 

Appels aux fonctionnalités du device

Dans cette catégorie, je comprends une base de données (SQLite), le GPS, le vibreur, la caméra, etc.

Selon le cas, Kivy penne à sortir son épingle du jeu. Il faut utiliser des modules complémentaires pour accéder aux spécificités du smartphone. Par exemple, « plyer » permet de faire beaucoup de choses simplement, mais il est encore en cours de développements pour proposer une panoplie complète. La page de support des fonctionnalités selon l’OS montre pas mal de trous. Ma note : 12/20

Testé et fonctionne très bien : Base de données SQLite, jouer un son MP3, prendre une photo, obtenir la position GPS, bouton Back d’Android

Testé et ne fonctionne pas (encore) : Réalité augmentée

Pour ionic, Cordova semble prendre en charge un grand nombre de fonctionnalités. Cette brique est indispensable, même si je ne pense pas qu’elle puisse faire comme une application native. Ma note : 18/20

Testé et fonctionne bien : jouer un son MP3

Mon vainqueur : ionic (il sera certainement plus performant que Kivy dans ce domaine)

 

Design et interface

Avis aux WebDesigners, Kivy est une torture. Pour ceux qui ont fait du CSS, c’est horrible. Le langage KV est obligatoire et rédhibitoire (tellement que je me suis intéressé à d’autres Frameworks). Il faut prévoir ÉNORMÉMENT de temps pour comprendre comment fonctionne le principe d’affichage. L’option est de faire des widgets (composant graphique) simples à composer entre eux pour composer l’écran. Ma note : 4/20

Avec ionic, on reste sur du CSS (presque) classique. En plus, avec les thèmes déjà tout prêts, les WebDesigners retrouvent leurs marques. Ma note : 18/20

Mon vainqueur : ionic, largement en tête

 

Conclusion

Après avoir réalisé une application similaire avec Kivy et ionic (soundbox), je suis encore resté sur ma faim avec ionic. Je pense qu’il faudra que j’utilise SQLite, la position GPS, et d’autres fonctionnalités pour affiner mes notes.

Si on compte les points, voici mon résultat :

  • Installation : Kivy 18/20 ; ionic : 15/20
  • IDE : 18/20 pour les deux
  • Documentation : 18/20 pour les deux
  • Hello World : Kivy : 18/20 ; ionic : 15/20
  • Développement sur PC : Kivy 18/20 ; ionic : 12/20
  • Déploiement sur mobile : 18/20 pour les deux
  • Développements sans appels aux device : 18/20 pour les deux
  • Appels aux fonctionnalités du device : Kivy 12/20 ; ionic : 18/20
  • Design et interface : Kivy 4/20 ; ionic : 18/20

Total : Kivy 142 pts ; ionic : 150 pts

 

Snippet kv : IconTextCounter

Présentation

Pour aller plus loin avec le widget IconText cité dans un article de ce site, j’ai ajouté un indicateur avec un compteur, tel qu’on peut le voir dans des applications. Kivy ne proposant que des widgets basiques, voici une construction qui répond au problème.

 

Composition

Ce widget hérite de FloatLayout pour placer les éléments de manière relative, l’un au dessus de l’autre. Il contient :

  • un widget IconText
  • un widget Label, avec en image de fond le bleu avec des coins arrondis

 

Code kv

<IconTextCounter>:
	_icontext: _icontext_id
	_counter: _counter_id
	IconText:
		id: _icontext_id
		text: "dummy"
		icon: "images/ic_action_star.png"
		pos_hint: {'x':0, 'top':1}

	Label:
		canvas.before:
			Color:
				rgba: 1,1,1, 1
			Rectangle:
				pos: self.pos
				size: self.size
				source: "kvx_widgets/images/counter_bg.png"
		id: _counter_id
		text: "x"
		pos_hint: {'center_x':0.95, 'top':0.65}
		size_hint: (None, None)
		size: (self.texture_size[0]+sp(16), self.texture_size[1]+sp(8))
		font_size: sp(12)

Une astuce pour avoir un fond qui prend toute la taille du libellé : définir la taille à partir de la texture + un petit espace.

size: (self.texture_size[0]+sp(16), self.texture_size[1]+sp(8))

 

Code Python

class IconTextCounter(FloatLayout):
    counter = StringProperty("")
    counter_position = StringProperty("")
    counter_background = StringProperty("")

    text = StringProperty("")
    icon = StringProperty("")
    icon_size = NumericProperty(sp(80))
    font_size = NumericProperty(sp(18))
    text_color = VariableListProperty([0,0,0,1])
    forced_width = NumericProperty(sp(80))

    def __init__(self, **kwargs):
        super(IconTextCounter, self).__init__(**kwargs)
        self.bind(counter = IconTextCounter.set_counter,
                counter_position = IconTextCounter.set_counter_position,
                counter_background = IconTextCounter.set_counter_background,
                text = IconTextCounter.set_text,
                icon = IconTextCounter.set_icon,
                icon_size = IconTextCounter.set_icon_size,
                font_size = IconTextCounter.set_font_size,
                text_color = IconTextCounter.set_text_color,
                forced_width = IconTextCounter.set_forced_width
                )

    def set_counter(self, aValue):
        self._counter.text = aValue

    def set_counter_position(self, aPosition):
        delta = 0.15
        center_x = 0.5
        top = 0.80
        if aPosition == 'top-left':
            center_x -= delta
            top += delta
        elif aPosition == 'top-right':
            center_x += delta
            top += delta
        elif aPosition == 'bottom-left':
            center_x -= delta
            top -= delta
        elif aPosition == 'bottom-right':
            center_x += delta
            top -= delta

        self._counter.pos_hint = {'center_x':center_x, 'top':top}

    def set_counter_background(self, aSourceImage):
        self._counter.canvas.before.children[1].source = aSourceImage

    ## define all IconText methods
    def set_text(self, aText):
        self._icontext.text = aText

    def set_icon(self, aSourceImage):
        self._icontext.icon = aSourceImage

    def set_icon_size(self, aWidthHeight):
        self._icontext.icon_size = aWidthHeight

    def set_font_size(self, aFontSize):
        self._icontext.font_size = aFontSize

    def set_text_color(self, aColor):
        self._icontext.text_color = aColor

    def set_forced_width(self, aWidth):
        self._icontext.forced_width = aWidth

 

Ce widget est utilisé dans une grille :

picto = IconTextCounter()
picto.text = aCategory.catlblib
picto.icon = 'images/category_%s.png' % aCategory.catcdcode
picto.counter = "%s" % nb_pois
picto.counter_position = 'top-right'
self.grid_widget.add_widget( picto )

 

Snippet kv : ListIconItemButton

Présentation

Kivy propose un widget qui sert d’item dans une liste : « ListItemButton« . Cet item est cliquable, mais son utilisation est relativement complexe. Surtout si on désire ajouter des icones à gauche ou à droite du libellé, ca commence à devenir un casse tête.

Voici une autre implémentation du même besoin, avec des icones de chaque côté du label :

 

Composition

Ce widget hérite de GridLayout, avec, en une ligne :

  • Un Label pour le padding à gauche
  • Un Label pour l’image de gauche
  • Un Label pour le libellé
  • Un Label pour l’image de droite
  • Un Label pour le padding à droite

Si l’image de gauche est spécifiée à None, aucune icône n’apparaît (1er exemple de cet article).

Ce Widget doit être inséré dans un GridLayout, lui même dans un ScrollView:

	ScrollView:
		GridLayout:
			id: list_items_id
			cols: 1
			size_hint_y: None

 

Code kv

<ListIconItemButton>:
	_label_text: _label_text_id
	_padding_right: _padding_right_id
	_padding_left: _padding_left_id
	_left_icon: _left_icon_id
	_right_icon: _right_icon_id
	rows: 1
	size_hint_y: None
	height: sp(50)

	Label:
		id: _padding_left_id
		text: ""
		size_hint_x: None
		width: sp(4)
	Label:
		canvas.before:
			Color:
				rgba: 1,1,1, 1
			Rectangle:
				pos: self.pos
				size: self.size
				source: "images/ic_action_star.png"
		id: _left_icon_id
		text: ""
		size_hint_x: None
		width: sp(50)

	Label:
		id: _label_text_id
		text: "dummy"
		size_hint_x: 1
		halign: 'left'
		valign: 'middle'
		text_size: self.size

	Label:
		canvas.before:
			Color:
				rgba: 1,1,1, 1
			Rectangle:
				pos: self.pos
				size: self.size
				source: "images/ic_action_next.png"
		id: _right_icon_id
		text: ""
		size_hint_x: None
		width: sp(25)

	Label:
		id: _padding_right_id
		text: ""
		size_hint_x: None
		width: sp(4)

 

Code Python

class ListIconItemButton(ButtonBehavior, GridLayout):
	text = StringProperty("")
	text_color = VariableListProperty([0,0,0,1])
	font_size = NumericProperty(sp(18))
	index = NumericProperty(0)
	padding = VariableListProperty([sp(2),sp(2)])
	left_icon_width = NumericProperty(sp(50))
	right_icon_width = NumericProperty(sp(50))
	left_icon = StringProperty("")
	right_icon = StringProperty("")

	def __init__(self, **kwargs):
		super(ListIconItemButton, self).__init__(**kwargs)
		self.bind(text = ListIconItemButton.set_text,
				text_color = ListIconItemButton.set_text_color,
				font_size = ListIconItemButton.set_font_size,
				index = ListIconItemButton.set_index,
				padding = ListIconItemButton.set_padding,
				left_icon_width = ListIconItemButton.set_left_icon_width,
				right_icon_width = ListIconItemButton.set_right_icon_width,
				left_icon = ListIconItemButton.set_left_icon,
				right_icon = ListIconItemButton.set_right_icon
				)

	def set_text(self, aText):
		self._label_text.text = aText

	def set_index(self, aNumber):
		self.item_index = aNumber

	def get_index(self):
		return self.item_index

	def set_padding(self, aPadding):
		if isinstance(aPadding, (float,int,long,float)):
			self._padding_left.width = aPadding
			self._padding_right.width = aPadding
		else:
			self._padding_left.width = aPadding[0]
			self._padding_right.width = aPadding[1]

	def set_font_size(self, aSize):
		self._label_text.font_size = aSize

	def set_text_color(self, aColor):
		self._label_text.color = aColor

	def set_left_icon_width(self, aWidth):
		self._left_icon.width = aWidth

	def set_right_icon_width(self, aWidth):
		self._right_icon.width = aWidth

	def set_left_icon(self, aSourceImage):
		if aSourceImage == "" or aSourceImage is None:
			self.remove_widget(self._left_icon)
		else:
			self._left_icon.canvas.before.children[1].source = aSourceImage

	def set_right_icon(self, aSourceImage):
		if aSourceImage == "" or aSourceImage is None:
			self.remove_widget(self._right_icon)
		else:
			self._right_icon.canvas.before.children[1].source = aSourceImage

Snippet kv : Scrollable text

Présentation

Kivy permet d’implémenter un texte trop long et qui nécessite de scroller vers le bas, mais avec une composition de beaucoup de widgets pour l’intégrer à une page ainsi qu’une configuration assez difficile.

 

Composition

Ce widget hérite de la classe ScrollView qui permet le scrolling et contient un GridLayout, sur 3 lignes :

  • 1ère ligne : un label pour faire le padding-top
  • 2ème ligne : un autre GridLayout, sur 3 colonne :
    • 1ère colonne : un label pour faire le padding-left
    • 2ème colonne : le texte sous forme d’un Label
    • 3ème colonne : un label pour faire le padding-right
  • 3ème ligne un label pour faire le padding-bottom

 

Code kv

<ScrollableText>:
	_text_widget: _text_widget_id
	_padding_top: _padding_top_id
	_padding_left: _padding_left_id
	_padding_right: _padding_right_id
	_padding_bottom: _padding_bottom_id
	canvas.before:
		Color:
			rgba: self.background_color
		Rectangle:
			pos: self.pos
			size: self.size
	GridLayout:
		cols: 1
		height: _text_widget_id.height + _padding_top_id.height + _padding_bottom_id.height
		size_hint_y: None

		Label:
			id: _padding_top_id
			text: " "
			size_hint_y: None
			height: sp(4)

		GridLayout:
			rows: 1
			Label:
				id: _padding_left_id
				text: " "
				size_hint_x: None
				width: sp(8)
			Label:
				id: _text_widget_id
				text: "no text yet"
				font_size: '18sp'
				color: (0.1,0.1,0.1, 1)
				valign: 'top'
				# make it scrollable
				text_size: (self.width, None)
				size_hint_y: None
				size: (self.parent.width, self.texture_size[1] )
			Label:
				id: _padding_right_id
				text: " "
				size_hint_x: None
				width: sp(8)
		Label:
			id: _padding_bottom_id
			text: " "
			size_hint_y: None
			height: sp(4)

Astuce : la propriété « background_color » est directement utilisée dans le KV et non dans le python (comme la taille du texte par exemple).

 

Code Python

class ScrollableText(ScrollView):
    text = StringProperty("")
    background_color = VariableListProperty([1,1,1,0])
    text_color = VariableListProperty([0,0,0,1])
    padding = VariableListProperty([sp(8), sp(8), sp(8), sp(8)])
    font_size = NumericProperty(sp(18))

    def __init__(self, **kwargs):
        super(ScrollableText, self).__init__(**kwargs)
        self.bind(text = ScrollableText.set_text,
                text_color = ScrollableText.set_text_color,
                padding = ScrollableText.set_padding,
                font_size = ScrollableText.set_font_size
                )

    def set_text(self, aText):
        self._text_widget.text = aText

    def set_text_color(self, aColor):
        self._text_widget.color = aColor

    def set_padding(self, aPadding):
        """Top, Right, Bottom, Left
        """
        if isinstance(aPadding, (float,int,long,float)):
            self._padding_top.height = aPadding
            self._padding_right.width = aPadding
            self._padding_bottom.height = aPadding
            self._padding_left.width = aPadding
        if len(aPadding) == 4:
            self._padding_top.height = aPadding[0]
            self._padding_right.width = aPadding[1]
            self._padding_bottom.height = aPadding[2]
            self._padding_left.width = aPadding[3]
        elif len(aPadding) == 2:
            self._padding_top.height = aPadding[0]
            self._padding_right.width = aPadding[1]
            self._padding_bottom.height = aPadding[0]
            self._padding_left.width = aPadding[1]

    def set_font_size(self, aSize):
        self._text_widget.font_size = aSize