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.

 

Raspberry : un feu de cheminée ou un aquarium

Introduction

Noël approche, qui n’a pas rêvé d’avoir un feu de cheminée. Je vous propose de transformer votre Raspberry en « boite à diffuser une vidéo de distraction », comme un feu de cheminée, un aquarium ou une jolie vue de ruisseau.

 

Configuration du Raspberry

Rien de plus qu’un Raspberry qui fonctionne et qui est connecté à un écran.

 

Choix de la vidéo

Youtube fourni un grand nombre de vidéos, en HD de feu de cheminée ou d’aquarium. Voici mon choix :

https://www.youtube.com/watch?v=0fYL_qiDYf0

La vidéo dure 2 heures, mais on n’a besoin de tout le fichier. Il faut passer par une outil de téléchargement (cherchez « youtube downloader firefox plugin ») pour récupérer le fichier MP4.

 

Extraction d’une portion de quelques minutes

Passez par ffmpeg pour extraire une partie de la vidéo. Notez la période : début et durée, puis lancer la ligne de commande suivante pour l’extraction :

ffmpeg -ss 00:30:00 -i feuDeCheminee-full.mp4 -t 00:02:00 -vcodec copy -acodec copy feu-portion.mp4

Sur la vidéo « feuDeCheminee-full.mp4 », je saute 30 minutes du début et je prends 2 minutes de vidéo finale. Le fichier « feu-portion.mp4 » contiendra le résultat : une vidéo de quelques dizaines de Mo (au lieu de plus d’un Go pour la vidéo complète).

 

Lire la vidéo avec le Raspberry

Tentez de lire la vidéo avec VLC : c’est très lent. Impossible de choisir cette solution. En allant voir sur le Net, le player conseillé est « omxplayer ». Cherchez à l’installer avec les bibliothèques pour être sûr du résultat.

Mon Raspberry est configuré en point d’accès Wifi, avec une application Web pour le piloter (depuis mon smartphone ou depuis un PC). J’ajoute cette fonctionnalité, en appelant un script pour lancer la vidéo :

#!/bin/bash

export DISPLAY=:0.0

SERVICE='omxplayer'
RES=0
trap "exit" INT
while [ $RES -eq 0 ]; do
  if ps ax | grep -v grep | grep $SERVICE > /dev/null
  then
    sleep 1
  else
    xterm -fullscreen -fg black -bg black -e omxplayer --win "0 0 1600 1200" -r $1
    RES=$?
    echo "RES: $RES"
  fi
done

xrefresh -display :0

 

Vous constaterez des lignes particulières (que j’utilise peu) :

trap "exit" INT

Cela signifie que si un sous-script est lancé et qu’il se fait killer, le script s’arrête aussi.

 

xrefresh -display :0

A la fin du script, je force le rafraîchissement de l’écran pour nettoyer ce qui peut rester.

 

xterm -fullscreen -fg black -bg black -e omxplayer --win "0 0 1600 1200" -r $1
RES=$?

La ligne de commande principale, qui lance la vidéo, dans un terminal (xterm), en plein écran, avec omxplayer en mode fenêtre, d’une taille de 1600×1200. La variable RES récupère le code retour du player. Si elle vaut 0, on boucle, sinon, on s’arrête (le CTRL-C fait un RES = 2).

Pour simuler la lecture sans fin, j’ai une boucle while autour de tout ça.

 

Arrêt de la lecture par un script

J’ai aussi un autre script qui arrête la lecture en boucle :

PID=$(ps aux | grep omxplayer | awk {'print $2'})
kill -9 $PID 2>/dev/null

xrefresh -display :0

Rien de très novateur. J’ai essayé la commande « killall -9 omxplayer », mais elle n’est pas satisfaisante.

Mon interface (depuis un PC) pour lancer quelques vidéos et un bouton en bas pour arrêter :

Arduino : premiers essais

Introduction

Après avoir complètement paramétré mon Raspberry Pi en point d’accès wifi, avec une webcam et une application web pour le piloter depuis mon smartphone (article complet), je voulais essayer jouer avec les PIN du GPIO. Mais je ne voulais pas refaire un autre Raspberry pour jouer avec les « sensors » (détecteur de température, détecteur de mouvement, etc.). Pour faire ça, il y a Arduino, en plus simple et plus économique…

 

 

Installation de l’environnement

Depuis longtemps, je suis habitué à l’IDE Eclipse pour mes développements. J’ai pris mes réflexes et je pense être moins désorienté dans ces environnement. C’est pour cette raison que j’installe un Eclipse dédié.

J’ai donc suivi quelques tutoriels, mais celui-ci me semble très complet pour tout savoir sur l’interface entre ma machine et la carte Adruino : http://www.codeproject.com/Articles/1003347/Creating-Arduino-programs-in-Eclipse

Avec Ubuntu, le port est à changer. Pour savoir quel port il faut configurer (« //./COM15″ dans le tutoriel), voici les étapes, depuis un terminal :

  1. Branchez votre carte Arduino sur le port USB et tapez « lsusb ». Si une ligne apparaît avec « Arduino », c’est bon, la carte est reconnue.
  2. Tapez « dmesg ». Vous devriez voir :
    [ 1013.247248] usb 8-2: Product: Arduino Uno
    [ 1013.247250] usb 8-2: Manufacturer: Arduino Srl
    [ 1013.247253] usb 8-2: SerialNumber: 85431303636351D0A101
    [ 1013.272419] cdc_acm 8-2:1.0: ttyACM0: USB ACM device
  3. Tapez « ls /dev/ttyACM* ». Vous devriez avoir le port de l’Arduino : /dev/ttyACM0
  4. Il reste à permettre à l’utilisateur d’écrire dans ce device : sudo chmod 777 /dev/ttyACM0

 

Premier script : « blink »

En suivant le tutoriel, je crée le programme « blink.cpp » en copiant / collant le code fourni, mais une erreur survient sur l’include :

#include "arduino.h"

 

Il faut la changer en :

#include "Arduino.h"

En consultant le fichier recherché dans mon code (version 1.6.6 d’Arduino), il y a un « A » (majuscule).

Le build se passe correctement et l’installation sur la carte aussi. La LED clignote bien.

 

Conclusion

Même si l’IDE fourni par Arduino fonctionne très bien, il m’a fallu un peu plus d’une heure pour installer quelques plugins et adapter le dernier. J’espère que cet article fera gagner du temps à ceux qui essaieront.

 

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.

 

Raspberry Pi + Camera : pilotage par un smartphone

Intérêt

Le raspberry est une petite boite contenant l’équivalent d’un PC assez puissant pour faire quelques petites choses intéressantes. L’objectif est de brancher le raspberry sur une source d’alimentation (dans la voiture, près de la télé, sous un télescope) et de piloter quelques actions depuis un smartphone.

Exemples d’application :

  • Près d’un point de surveillance de la maison : Caméra de surveillance, avec enregistrement des vidéos sur une clé USB
  • Dans la voiture : Enregistrement de la conduite de manière automatique. en cas de litiges, vous aurez un enregistrement vidéo
  • Sous un télescope : avec une webcam derrière l’objectif pour faire la mise au point et une série de prises de vue enregistrées sur la clé USB
  • Près d’un moniteur ou d’une télé : pour lancer une présentation PowerPoint / PDF, pour jouer des vidéos, avec la webcam pour prendre des photos

Tout ça, avec un smartphone en guise de télécommande Wifi.

 

Configuration matérielle pour le démarrage

Voici le matériel nécessaire pour configurer le Raspberry complètement :

  • Un clavier USB
  • Une souris USB
  • Un écran
  • Une clé USB
  • Un accès à Internet

 

Configuration logicielle pour le démarrage

  1. Installation de l’image Ubuntu Mate pour Raspberry
  2. Installation des services :
    1. LAMP (Apache + PHP + MySQL), avec changement du user apache
    2. openssh-server (pour accès shell par SSH)
    3. usbmount (auto-mount les clés USB)
  3. Configuration des points de montage : pour la clé USB :  « uid=1000,noauto,user »
  4. Désactivation du firewall Ubuntu
  5. Installation d’un serveur FTP et SSH pour déposer les fichiers depuis un autre PC et lancer des lignes de commandes

 

Configuration logicielle étape 2

Cette étape se fait depuis un PC pour plus de confort. On a donc mis le Raspberry sur un réseau interne :

  • Un PC connecté via RJ45 à un routeur
  • Le Raspberry connecté en RJ45 au routeur

Par la suite, le câble RJ45 sera supprimé.

 

Le clavier, la souris et l’écran peuvent être supprimés après avoir installé les serveurs SSH et FTP.

L’installation d »es logiciels continue :

  1. Installation d’une application web pour accéder au Raspberry depuis un smartphone (PHP avec CodeIgniter) – Réalisée par mes soins ; cf chapitre « Application web pour contrôler le Raspberry » ci-dessous
  2. Changement de la politique de sécurité pour autoriser l’arret et reboot par un utilisateur quelconque (/usr/share/polkit-1/actions/org.freedesktop.login1.policy)
  3. Connexion automatique d’un user (pour ne pas avoir de mire de connexion)
  4. Ajout d’un script à la connexion pour lancer « xhost + » (pour que d’autres machines puissent ouvrir des fenêtres dans la session ouverte)
  5. Activation du dongle Wifi en mode AccessPoint (point d’accès Wifi) et paramétrage du DHCP

 

Application web pour contrôler le Raspberry

Cette application web permettra de piloter le Rasbberry depuis un smartphone. Elle est développée en PHP avec le Framework CodeIgniter. Un template HTML permet d’avoir une charte graphique en Responsive Design : http://binarycart.com/bclivedemos/01-05-2014/v1/bs-binary-admin/index.html

 

Fonctionnalités basiques de l’application PHP :

  • Etat du raspberry : charge machine, type de processeur, version du noyau, version et nom du système (Ubuntu)
  • Lancement de scripts SH (exemple : lancer une présentation avec un fichier PDF précis)
  • Parcours dans les répertoires
  • Caméra : prise de photo, enregistrement d’une vidéo
  • Arrêt et relance du raspberry

 

Utilisation avec un smartphone

Après avoir mis sous tension le raspberry, le smartphone peut s’y connecter par wifi. Un simple navigateur permet d’accéder aux fonctionnalités. Le smartphone devient une télécommande.

 

Copies d’écran sur smartphone

Page de connexion

Page d’accueil : description de la machine

Menu

Page des scripts à lancer

Page des médias USB connectés et détectés

Parcours des fichiers du Raspberry

Page de la gestion de la caméra : prendre une photo, prendre des vidéos

Page d’arrêt / relance du Raspberry

Kivy : gestion de la fin d’une lecture audio

Introduction

Kivy ne propose pas d’interface élaborée pour surveiller le bon déroulement du fichier audio qui est en cours de lecture (durée, temps passé). Mais je vous propose de voir en détail ce qui est exploitable en utilisant le « player » associé à l’objet « son joué ».

 

Structure des données

Pour écouter un fichier MP3 (WAV ou OGG), il faut réaliser ceci :

filename = "youi.mp3"
self.currentSound = SoundLoader.load(filename)
self.currentSound.play()

L’objet « SoundLoader » ne propose pas beaucoup d’interaction. Il est possible de le lire (méthode play), de le forcer à s’arrêter (méthode stop) et de savoir s’il est arrêté ou s’il en lecture.

La méthode « on_stop » est appelée à la fin du son.

 

Description

Le fichier audio doit être joué par le scheduler et non directement :

filename = "youi.mp3"
self.currentSound = SoundLoader.load(filename)
Clock.schedule_once(self.play_current_sound, 0)

De plus, d’autres méthodes sont définies.

1. Méthode pour lancer la lecture du fichier audio :

    def play_current_sound(self, arg):
        self.currentSound.play()
        self.currentSound.bind(on_stop = self.stop_sound)

Attention à respecter cet ordre sinon à la fin du son, la méthode « on_stop » n’est pas appelée.

 

2. Méthode qui arrête la lecture du fichier audio (forcée par un bouton) ou qui est appelée en fin de lecture :

    def stop_sound(self,  arg = None):
        self.currentSound.unbind(on_stop = self.stop_sound)
        if self.currentSound is not None:
            self.currentSound.stop()

 

Conclusion

Avec ce principe, la méthode « stop_sound » est appelée lorsque le fichier est terminé ou lorsque l’utilisateur a volontairement arrêté la lecture. Il est possible de présenter un bouton [Stop] uniquement lorsqu’il y a un son qui est joué.

Cette fonctionnalité de lecture d’un son est implémentée de manière différente en le PC (sous Linux) et un SmartPhone Android. Il faudra faire les ajustements au dernier moment ou implémenter une interface…

 

Kivy : scrolling horizontal

Introduction

Dans une page web, un tel composant s’appelle un carrousel. Il permet de faire passer les éléments de la droite vers la gauche ou inversement. Dans Kivy, ce widget n’existe pas en tant que tel, mais un composant plus générique est disponible. Reste à le configurer correctement.

source de l’image : http://www.androidpatterns.com/uap_pattern/carousel

Tout est dans le kv

Le « main.py » de l’application de démo est très classique :

import kivy
kivy.require('1.8.0')

from kivy.config import Config
Config.set('graphics', 'width', '450')
Config.set('graphics', 'height', '800')

__version__ = '0.1'

from kivy.app import App
from kivy.uix.screenmanager import Screen

class HScroll(Screen):
    pass

class HScrollApp(App):
    def build(self):
        return HScroll()

if __name__ == '__main__':
    HScrollApp().run()

 

Le fichier « hscroll.kv » défini les éléments de manière statique. Si besoin, il faudra les dynamiser dans votre application. Dans cet exemple, il y a un widget Label avec le texte « Horizontal scrolling » et un widget ScrollView qui définira la zone de scrolling.

<HScroll>:
    BoxLayout:
        orientation: 'vertical'
        canvas.before:
            Color:
                rgba: 1,1,1, 1
            Rectangle:
                pos: self.pos
                size: self.size

        Label:
            text: "Horizontal scrolling"
            size_hint_y: None
            height: sp(25)
            font_size: '16sp'
            color: (0,0,0, 1)
        ScrollView:
            GridLayout:
                rows: 1
                spacing: 10
                size_hint_x: None
                width: sp( 7 * (100 + 10) )

                canvas.before:
                    Color:
                        rgba: 0.2,0.2,0.2, 1
                    Rectangle:
                        pos: self.pos
                        size: self.size
                Image:
                    source: "images/disc-large.png"
                    size_hint_x: None
                    width: sp(100)
                Image:
                    source: "images/disc-medium.png"
                    size_hint_x: None
                    width: sp(100)
                Image:
                    source: "images/disc-small.png"
                    size_hint_x: None
                    width: sp(100)
                Image:
                    source: "images/ic_action_accept.png"
                    size_hint_x: None
                    width: sp(100)
                Image:
                    source: "images/ic_action_back.png"
                    size_hint_x: None
                    width: sp(100)
                Image:
                    source: "images/ic_action_cancel.png"
                    size_hint_x: None
                    width: sp(100)
                Image:
                    source: "images/ic_action_cloud.png"
                    size_hint_x: None
                    width: sp(100)

 

Tout le travail du ScrollView est de tronquer un widget à l’intérieur, un GridLayout, plus grand (trop grand) et nécessitant un scrolling pour le voir entièrement.

Pour ce faire, il faut que le GridLayout ait une largeur définie. Dans cet exemple, c’est en dur : il y a 7 images de 100sp chacune. Sa largeur sera donc de 7 * (100 + 10 de spacing).

Il faut donc :

  1. Un widget ScrollView qui contient un GridLayout
  2. Une seule ligne dans le GridLayout (« rows: 1″)
  3. Définir la largeur du GridLayout (« size_hint_x: None » et width: sp(…) »)

Avec ces informations, vous pourrez faire un joli scrolling horizontal. Pour un scrolling vertical, il suffit de mettre une colonne (point #2) et de changer les largeurs en hauteurs (point #3).

 

Kivy : accès à l’appareil photo sous Android

Introduction

Depuis une application Kivy, il est possible d’appeler des objets Android et les faire interagir en utilisant Python. Pour ce faire, une interface est disponible sous le nom de « jnius ». En effet, c’est très pratique et génial…

Voici un exemple d’utilisation de l’appareil photo, le tout réutilisable dans vos applications Kivy, car dans un module à part (genre « Service »).

 

Appel de l’appareil photo dans l’application Kivy

Attention, avant de procéder à l’appel de caméra pour prendre une photo, il faut savoir une chose : l’application Kivy se mets en pause. Pour ce faire, il est nécessaire que l’instance de App définisse la méthode « pause » et renvoie « True ». C’est un peu comme si Kivy disant « je mets en pause l’appli, est-ce que j’en ait le droit ? ». Si « True », je passe à la suite. Si « False », je stoppe.

 

Le service d’appel de la photo est défini comme ceci (basé sur le tuto « takepicture ») :

from jnius import autoclass, cast
from android import activity
from functools import partial
from kivy.clock import Clock
import os

Intent = autoclass('android.content.Intent')
PythonActivity = autoclass('org.renpy.android.PythonActivity')
MediaStore = autoclass('android.provider.MediaStore')
Uri = autoclass('android.net.Uri')
Environment = autoclass('android.os.Environment') 

class CameraService(object):
    '''
    Service pour acceder a la camera du device.
    Attention, seules les images JPG sont autorisées.
    '''

    def __init__(self, savingPath = None, aFilename = None):
        '''
        Constructeur
        savingPath doit se terminer par le separateur de répertoire
        '''
        if savingPath is None:
            savingPath = Environment.getExternalStorageDirectory().getPath()
        self.path = savingPath
        self.filename = aFilename
        self.picture = None

    def __define_new_filename(self):
        index = 0
        while True:
            index += 1
            fn = os.path.join(self.path, 'takepicture{}.jpg'.format(index))
            if not os.path.exists(fn):
                return fn

    def take_picture(self, aCallbackFunction):
        self.callbackFunction = aCallbackFunction
        if self.filename is None:
            self.filename = self.__define_new_filename()
        uri = Uri.parse('file://' + os.path.join(self.path, self.filename) )
        uri = cast('android.os.Parcelable', uri)

        activity.bind(on_activity_result=self.__on_activity_result)

        intent = Intent(MediaStore.ACTION_IMAGE_CAPTURE)
        intent.putExtra(MediaStore.EXTRA_OUTPUT, uri)
        PythonActivity.mActivity.startActivityForResult(intent, 0x123)

    def __on_activity_result(self, requestCode, resultCode, intent):
        if requestCode == 0x123:
            Clock.schedule_once(partial(self.__add_picture, self.filename), 0)

    def __add_picture(self, fn, *args):
        self.picture = fn
        self.callbackFunction(self.picture)
        activity.unbind(on_activity_result=self.__on_activity_result)

 

Son appel se fait dans une instance de Screen, par exemple :

class myScreen(Screen):
    #...

    def btn_take_photo(self):
        camera = CameraService()
        camera.take_picture(self.set_picture)

    def set_picture(self, newPicture_name):
        self.logLabel.text = "Photo : %s" % str(newPicture_name)

Lorsqu’un bouton est cliqué, la méthode « btn_take_photo() » est appelée. Celle-ci fait appel au service (sans paramètre dans ce cas) et lance la prise de photo avec un paramètre : la méthode à appeler lorsque l’utilisateur aura terminé : « self.set_picture ».

Cette méthode prend un paramètre : le nom du fichier sauvegardé par l’appareil photo du smartphone. Attention, seul le format JPEG est autorisé…

 

Conclusion

Pour reprendre cette photo, je vous conseille de la réduire avec PIL pour éviter d’avoir une image en 4128 x 2322 pixels.

im = Image.open(filename)
width, height = im.size
im.thumbnail( (width/4,height/4) , Image.ANTIALIAS)
im.save(filename,quality=95)

Pill Chart : graphe en forme de pillule

Introduction

Suite à la création du sélecteur de couleur sous forme d’une petite barre, j’ai voulu faire un graphique simple sur une dimension. J’ai repris les mêmes bases que le plugin précédent pour celui-ci.

Description

La syntaxe en JS est assez simple, un « PillChart » est défini par :

  • le DIV qui sera utilisé pour afficher le plugin
  • les données à afficher, avec les clés suivantes :
    • « label » : libellé au survol de la souris
    • « value » : valeur à faire apparaître dans la case du graphe
    • « width » : largeur en pixel ou en pourcentage
    • « cssAttr » : texte à coller dans l’attribut CSS « style »
new PillChart({
            containerId: 'pill_1',
            data: [ {
                label: "moins de 10 ans",
                value: 20,
                width: '20px',
                cssAttr: "background-color: #69C8FF;"
              }, ...
           ],
            updateCallback: function(obj){alert("clicked on : "+obj.label );}
        }).draw();

 

Démo

La page de démo est disponible sur cette page : http://jc.specs.free.fr/pillChart/demo.html

 

Démo avec une légende (générée par le plugin) :

 

Plugin MultiSelect dans le générateur de code

Introduction

Dans le générateur de code (voir l’article qui le décrit), les fichiers classiques de l’entité sont générés, mais il en manque une partie. Lorsqu’on défini une liaison n-n d’une entité vers une autre, on utilise un « objet de liaison ». Cet objet se décrit par :

  • un identifiant qui est clé unique de l’objet de liaison
  • l’identifiant de l’objet A
  • l’identifiant de l’objet B

Grâce à cet objet, un objet A est relié à n objets B et inversement, un objet B est relié à n objets A.

Exemple : entité « Album de bande dessinée » et entité « Auteur » :

  • un album de BD peut avoir plusieurs auteurs
  • un auteur peut avoir fait plusieurs albums de BD

 

L’objectif est de générer automatiquement du code pour avoir un multiselect dans la création et d’édition d’une entité A ou B.

 

Application avec les BD et les auteurs

L’objet « LienAuteurAlbum » est décrit comme ceci :

  • identifiant du lien
  • identifiant de l’album
  • identifiant de l’auteur

Remarque : l’identifiant du lien permet de supprimer facilement et sereinement un enregistrement avec une seule information, en plus des fonctionnalités offertes avec les autres champs.

Après moult réflexion, j’ai ajouté des features sur chaque entité de liaison :

Ces modifications sont faites dans la génération du helper et du modèle. Deux nouvelles vues ont étés faire pour l’appel du plugin multiselect, sous forme d’un fragment (une vue qui s’appelle depuis une vue principale). Une vue pour la création et une vue pour la modification.

Par contre, il faut modifier manuellement les controller et les vues des entités (exemple : Album) pour utiliser cet objet de liaison.

Appel du code généré pour la vue en édition ou en création d’album

Lorsque je crée un album, je veux avoir le multiselect des auteurs. Il faut alors ajouter dans la vue de création d’un album le code suivant :

<?php 

$data["multiselectOf"] = 'Auteur';
$this->load->view('lienauteuralbum/createMultiselectLienAuteurAlbumFragment_view.php', $data);

?>

Le fichier « lienauteuralbum/createMultiselectLienAuteurAlbumFragment_view.php » est généré et ne doit pas être modifié. Dans le paramètre « multiselectOf », il faut spéficier que je veux un multiselect de … « Auteur », afin d’avoir une liste d’auteurs à choisir.

Idem dans la vue d’édition d’un Album.

 

Modification du controller d’édition d’album

Dans le controller d’édition, lorsqu’on sauve les données saisies, il faut [1] supprimer les anciennes relations dans « LienAuteurAlbum » dans lesuqelles il y a l’album édité et [2] créer des nouveaux objets « LienAuteurAlbum » avec cet album et les auteurs cochés.

Première partie :

$lienAuteurAlbum_autidaut = $this->input->post('LienAuteurAlbum_autidaut');
LienAuteurAlbum_model::deleteAllLienAuteurAlbumsBy_xaaidalb($this->db, $model->albidalb);

ligne 1 : récupération des liens avec des identifiants d’auteurs que l’utilisateur a coché dans le multiselect

ligne 2 : suppression des anciens liens à partir de l’identifiant de l’album

 

Deuxième partie :

foreach($lienAuteurAlbum_autidaut as $autidaut){
            $auteurModel = new LienAuteurAlbum_model();
            $auteurModel->xaaidalb = $model->albidalb;
            $auteurModel->xaaidaut = $autidaut;
            $auteurModel->save($this->db);
}

Pour chaque auteur coché, on créer un nouveau lien avec :

  • album : celui que je suis en train d’éditer
  • auteur : un auteur coché

Remarque : Dans le controller de création d’un album, la première partie destinée à supprimer les anciens liens avec les auteurs n’est pas à faire.

 

 Sans oublier le javascript

Le plugin JS est à charger après les différents appels de jQuery et du JS du plugin multiselect. J’ai choisi de le mettre manuellement dans le JS de la vue de l’objet édité ou créé.

$(function(){
    $("#LienAuteurAlbum_autidaut").multiselect();
});

 

Résultat

Dans cet exemple, le champ « Genre » est un select simple (déjà disponible dans le générateur). Le champ « Liste des auteurs » fait appel au plugin multiselect.

 

Conclusion

Finalement, la génération de code a ses limites, mais beaucoup de choses sont pré-mâchées. Pour aider les futurs développeurs à intégrer le plugin, j’ai inséré le code à copier-coller dans la vue et dans le JS.

Rien n’est généré pour le controller, il faudra lire cet article si vous ne savez pas comment faire (mais je trouve que ce n’est pas trop difficile).

 

Bootstrap typeahead & ajax

Introduction

Bootstrap dispose d’un champ d’auto-complete nommé « typeahead« , qui autorise de saisir un item parmi une liste figée ou récupérée par Ajax. Seul bémol : l’exemple fourni montre comment faire avec une liste de libellés seuls. Comment lui passer une liste composée de données avec un identifiant ET un libellé ? L’objectif étant de mettre l’identifiant dans un champ caché et le libellé dans le champ de texte. Lorsque le formulaire est envoyé, le champ caché contient la valeur à sauvegarder (le libellé n’étant pas utile).

Les promesses de Bootstrap Typeahead, sur le choix de la ville (avant l’adaptation) :

 

Sources

Voici les liens qui décrivent les opérations :

 

Résultat

La recherche par nom de ville :

 

La recherche par code postal :

 

Explications

En effet, l’avantage de « typeahead » est qu’il peut être surchargé, avec une nouvelle définition des méthodes. Pour notre cas, ce sera la méthode « updater » (lorsque la sélection de l’item est fait par le clic, la méthode décrit le comportement).

Pour la suite, il faut alimenter la liste avec des objets customisés (avec des méthodes particulières).

 

Détail de la solution

$('#cabidvil_text').typeahead({
    source: function (query, process) {
        return $.get(base_url()+'index.php/json/listvilles/findBy_villbnsc_vilcdcps/'+query,
        { /*query: no more parameters*/ }, function (dataIN_str) {
            data = new Array();
            var dataIN = JSON.parse(dataIN_str);
            for (i in dataIN) {
                var group;
                group = {
                    id: dataIN[i].vilidvil,
                    name: dataIN[i].vilcdcps + ' ' + dataIN[i].villbnsc,
                    toString: function () {
                        return JSON.stringify(this);
                    },
                    toLowerCase: function () {
                        return this.name.toLowerCase();
                    },
                    indexOf: function (string) {
                        return String.prototype.indexOf.apply(this.name, arguments);
                    },
                    replace: function (string) {
                        var value = '';
                        value +=  this.name;
                        if(typeof(this.level) != 'undefined') {
                            value += ' <span>';
                            value += this.level;
                            value += '</span>';
                        }
                        return String.prototype.replace.apply('<div>' + value + '</div>', arguments);
                    }
                };

                data.push( group );
            }
            return process(data);
        });
    },
    updater: function (item) {
        var item = JSON.parse(item);
        $('#cabidvil').val(item.id);
        return item.name;
    }

});

 

Les lignes importantes

1 – L’appel de l’URL par ajax qui peut prendre à la fois le code postal et la ville :

return $.get(base_url()+'index.php/json/listvilles/findBy_villbnsc_vilcdcps/'+query,

Coté controller, si la recherche par code postal ne retourne rien, faire la recherche par nom de ville.

 

2 – Définir les 2 variables de l’objet « group » :

id: dataIN[i].vilidvil,
name: dataIN[i].vilcdcps + ' ' + dataIN[i].villbnsc,

La variable « name » sera affichée dans la liste déroulante et dans le champ de texte une fois l’item sélectionné (vilidvil = identifant ; vilcdcps = code postal ; villbnsc = libellé en majuscule).

 

3 – Mise à jour du champ caché :

$('#cabidvil').val(item.id);

Le champ caché prend pour valeur l’identifiant de la ville sélectionnée par son code postal et son nom.

 

Bug sur le site

Suite à une opération dans le site (changement des permaliens), WordPress a écrasé le fichier « .htaccess » tout seul, ce qui a causé un cafouillage dans la configuration PHP de Free… Avec les fêtes, j’ai mis un peu de temps pour régler le problème. En plus, Free a subi une attaque, ce qui a provoqué un arrêt des serveurs FTP.

Enfin, les fichiers APK des applications mobiles (Android) ne sont pas encore téléchargeables.

A ce jour (30/15/2013), tout est revenu à la normale…

Sound Chooser en Kivy

Introduction

Dans quelques applications ayant une possibilité de personnalisé des sons, on trouve un écran de configuration où on peut cliquer que un titre et un son est joué. Voici comment je l’ai fait avec Kivy, en utilisant le fameux ListAdapter (qui n’est pas si simple d’utilisation).

Un autre  moyen est de créer les widgets dans le Python, comme expliqué dans cette démo : https://github.com/kivy/kivy/tree/master/examples/audio

 

Le fichier KV

Comme à mon habitude, la vue principale est un widget qui hérite de Screen, nommé « SoundChooserLayout ». Je me suis rendu compte que le rendu est celui que j’attends (contrairement à un BoxLayout).

 

<SoundChooserLayout>:
    containerListView: list_id
    BoxLayout:
        orientation: 'vertical'

        Label:
            text: "Choose a sound"
            size_hint_y: None
            height: sp(25)
            font_size: '16sp'

        ListView:
            id: list_id

        BoxLayout:
            size_hint_y: None
            height: sp(50)
            Button:
                text: "Done"
                font_size: sp(20)
                height: sp(40)

Le widget « ListView » fera tout le travail. Dans le fichier KV, il est juste défini et ce sera dans le Python qu’il sera complété.

Le bouton « Done » ne fait rien pour le moment. Lorsque cet écran sera dans une application, il définira le son choisi par l’utilisateur.

 

Le code Python – extraits

Définition de la classe et constructeur :

class SoundChooserLayout(Screen):
    def __init__(self):
        super(SoundChooserLayout, self).__init__()
        self.audioFiles = {}
        self.currentSound = None
        self.readFiles()
        self.updateDisplay()

Le constructeur défini le dictionnaire des fichiers audio, le son qui est en cours de lecture.

La méthode « readFiles() » récupère les fichiers audio et mets à jour « audioFiles » :

    def readFiles(self):
        os.chdir("sounds")
        for filename in glob.glob("*.mp3"):
            self.audioFiles[filename] = SoundLoader.load(filename)

 

La méthode « updateDisplay() » modifie le widget ListView (c’est le plus compliqué dans cette mini-appli) :

    def updateDisplay(self):
        list_item_args_converter = \
            lambda row_index, obj: {'text': "%s (%.2f sec)" % (obj, self.audioFiles[obj].length),
                                    'index': row_index,
                                    'is_selected': False }

        my_adapter = ListAdapter(data = self.audioFiles,
                                    args_converter=list_item_args_converter,
                                    selection_mode='single',
                                    allow_empty_selection=False,
                                    template='CustomListItem')

        my_adapter.bind(on_selection_change=self.selection_changed)
        self.containerListView.adapter = my_adapter

Ici, le dictionnaire « self.audioFiles » est passé dans le « data », mais dans la fonction utilisée en « args_converter », ce sont les clés du dictionnaire qui sont présentes en paramètre (nommé « obj » dans la fonction lambda).

Lorsque l’utilisateur clique sur un item de la liste, la méthode « self.selection_changed » est appelée :

    def selection_changed(self, adapter, *args):
        if self.currentSound is not None:
            self.currentSound.stop()
        if len(adapter.selection) == 0:
            return

        ## unselect item
        if isinstance(adapter.selection[0].parent, kivy.uix.gridlayout.GridLayout) :
            self.currentSound = None
        else:
            selectedItem = adapter.data[adapter.selection[0].parent.index]
            self.currentSound = self.audioFiles[selectedItem]

        if self.currentSound:
            self.currentSound.play()

Cette méthode se découpe en plusieurs parties :

  • arrêter le son en cours de lecture
  • si rien n’est sélectionné (au cas où), terminer la méthode
  • quand on de-sélectionne un item (la sélection porte sur le GridLayout), le son en cours est vide
  • quand on sélectionne un nouvel item, récupérer le son sélectionné (dans SelectedItem, qui une clé du dictionnaire) et le définir comme son en cours
  • Si le son en cours n’est pas vide, le jouer

 

Bug constaté (et connu)

Kivy retrouve la durée d’un MP3 sur mon PC, mais pas sur mon SmartPhone, qui me retourne systématiquement « 1 seconde ». Je ne sais pas il sera corrigé, ni si un palliatif est possible…

 

[ Télécharger l'APK ]

Appli Mobile Kivy : Compte à rebours

Introduction

Après avoir réalisé l’application qui présente en détail les thés, j’aimerai la compléter par une autre application qui fait un compte à rebours. Voici le détail de sa réalisation avec Kivy.

 

Design

Après mes premiers essais avec Kivy, j’ai constaté l’importance de la conception des fichiers KV avant même de coder en python. C’est un peu comme si on réfléchissait à la position des champs et boutons dans les écrans avant de concevoir la base de données… un peu déroutant.

Cette application sera en 2 écrans (widget « Screen ») :

  • Ecran #1 de saisie du nombre de minutes et secondes à décompter
  • Ecran #2 de présentation du décompte, avec principalement 2 boutons « Start » et « Stop »

 

Détail de l’écran #1

Le fichier KV se décompose en :

  • BoxLayout : layout principal
    • Label : « Simple Countdown »
    • GridLayout : en 2 colonnes, avec
      • TextInput : minutes à saisir
      • TextInput : secondes à saisie
      • Label : « Minutes »
      • Label : « Secondes »
    • Label pour le message d’erreur de formatage d’un des 2 champs de saisie
    • BoxLayout : pour avoir un « spacing » sur le widget contenu
      • Button : « Ready », qui valide les données et lance le 2ème écran

Résultat :

 

Détail de l’écran #2

Le fichier KV se décompose en :

  • BoxLayout : layout principal
    • Label : « Simple Countdown »
    • GridLayout : en 2 colonnes, avec
      • Label : décompte des minutes
      • Label : décompte des secondes
      • Label : « Minutes »
      • Label : « Secondes »
    • GridLayout : en 2 colonnes, avec
      • Button : « Start »
      • Button : « Stop »
    • BoxLayout :
      • Button : « Back », qui valide revient au premier écran

Résultat :

 

Conclusion

Difficultés :

  • Définir la bonne façon pour découper les écrans : vaut-t-il mieux utiliser un BoxLayout ou un GridLayout pour placer les objets en une colonne ?
  • Définir la bonne taille des objets : dans un GridLayout, il faut définir la hauteur de chaque case d’une ligne pour que la hauteur de la ligne soit prise en compte.
  • La taille de la police est différente entre mon Samsung S4 et le rendu sur mon PC. Des ajustements sont toujours à faire

La manipulation de l’objet « Clock » est plutôt bien documentée et cohérente. L’ajout de sons dans une application Kivy est très facile avec la classe SoundLoader (bien documentée aussi).

[ Télécharger l'APK ] [Télécharger les sources]

 

Application Back-office & Mobile : CodeIgniter & Kivy

Introduction

Après quelques essais avec Kivy pour faire une application mobile qui indique des données d’infusion sur les thés, je me lance pour faire une application en back-office pour gérer les données. L’application mobile récupère les données en JSON et stocke dans une base SQLite.

Voici les outils utilisés de chaque coté :

Back-office Appli Mobile
Générateur de code Écriture du code « à la main »
PHP, CodeIgniter Python, Kivy
MySQL SQLite3

 

Principe général :

 

Application Back-office

Avec le générateur de code (plus d’infos sur ce site), l’application est créée, déployée et les données ajoutées, le tout en 1 heure sur « free.fr ». Le plus long a été de copier / coller les données entre ma source (le site du palais des thés) et ma nouvelle application.

Le controller fournissant le flux JSON est lui aussi généré :

Les écrans n’ont pas été retravaillés suite à la génération de code, mais c’est suffisant pour tout faire fonctionner sur l’environnement de « free.fr » : http://jc.specs.free.fr/TeaTime/ (un identifiant et mot de passe sont nécessaires…). Le nouveau thème Ubuntu (en gris et orange a été choisi).

Écrans du back-office

Catégorie de thés :

Thés :

Application mobile

Avec Kivy, je disposais déjà d’une application qui lit un JSON sous forme de fichier. Il m’a suffit de lui permettre de lire des fichiers JSON par Internet, de remplir une base de données SQLite et de la remplir.

Voici les étapes de réalisation :

  1. Lire dans un processus parallèle le JSON. Au lieu d’attendre que l’URL réponde, les widgets de l’écran de l’appli mobile sont disponibles.
  2. Stocker le JSON dans une base SQLite embarquée avec l’application. Lorsque c’est fait, un widget de l’application est modifié : un label devient « Fichier chargé ».
  3. Lorsque l’utilisateur entre dans les écrans de l’appli mobile, il faut lire la base de données SQLite et valoriser les objets qui seront utilisés par Kivy.

Kyvi n’est d’aucune utilité pour réaliser ces opérations de synchronisation ; Python prend le dessus : gestion des processus asynchrones, lecture d’une URL, transformation de JSON en objet, accès à une base de données SQLite.

 

Écrans de l’appli mobile

   

Le fichier APK de l’application mobile est téléchargeable sur http://jc.specs.free.fr/TeaTime/

 

Bibliothèque JS pour gérer les vecteurs à une dimension

Suite à une réflexion autour des vecteurs et leur manipulation dans une espace vectoriel à une dimension, j’ai créé une bibliothèque qui permet de manipuler ces objets relativement simples.

Ce simple objet mathématique qu’est une sorte de trait sur une ligne, avec sa position et sa longueur m’a fait quand même pas mal de nœuds à la tête. Le plus difficile est la gestion des ensembles d’objets : union, intersection dans quelques cas.

Je vous laisse découvrir l’API complète en français (qui fait office de démo car la page s’appuie dessus) et une copie d’écran ci-dessous, si votre navigateur ne peut pas faire tourner mon code (qui est peut-être buggé…).

 

Je pense que les personnes intéressées sauront récupérer le JS depuis la page de démo.

 

 

Design avec Kivy

Introduction

Kivy ne propose pas encore d’interface pour designer les interfaces (dans le genre Glade). Voici mes essais de graphisme dans les écrans d’une application.

 

Résultats obtenus

Voici d’abord les résultats que j’ai obtenu avec le paramétrage de kivy (un maximum en kv afin de rester cohérent avec la description des pages).

1. Changement de police

Dans ce cas, il suffit de mettre le fichier TTF dans le répertoire des sources de l’application et d’y faire appel dans le KV :

<Welcome>:
    logLabel_id: log

    BoxLayout:
        orientation: 'vertical'
        Label:
            text: "TeaTime"
            font_size: '60sp'
            font_name: "font/segoe_ui_light.ttf"
...

Résultat :

Lors du build de l’application (avec buildozer),  il ne faut pas oublier de l’ajouter dans le package (dans les extensions à packager, il faut ajouter « ttf »).

 

2. Lignes et bordures, sans images

J’ai testé l’ajout de bordures en langage KV, sans utiliser d’images pour le moment. Cela me garantie que les images ne seront pas déformées selon la résolution du média et ça me semble plus proche du CSS pour le HTML.

A partir de cet exemple, avec les lignes en haut, dans le futur Ubuntu Phone :

Lien : http://www.ubuntu.com/phone

<Categories>:
    logLabel: logLabel_id
    containerListView: list_id
    canvas.before:
        Color:
            # 34 30 28
            rgba: 0.1333, 0.11764, 0.1098, 1
        Rectangle:
            pos: self.pos
            size: self.size
    BoxLayout:
        id: categoriesLayout
        orientation: 'vertical'

        Label:
            text: "TeaTime >"
            font_size: '34sp'
            size_hint_y: None
            height: '60sp'
            halign: 'left'
            text_size: self.size
            padding: '-10sp', '-8sp'
            font_name: "font/Ubuntu-L.ttf"
            color: (1, 1, 1, 0.90)

        Label:
            text: " " # empty line
            size_hint_y: None
            padding: '10sp', '10sp'
            height: '16sp'
            canvas.before:
                Color:
                    # 28 25 23
                    rgba: 0,0,0, 0.33
                Rectangle:
                    pos: self.pos
                    size: self.size

        Label:
            text: " " # empty line
            size_hint_y: None
            padding: '0sp', '0sp'
            height: '2sp'
            canvas.before:
                Color:
                    # 89 86 84
                    rgba: 1,1,1, 0.33
                Rectangle:
                    pos: self.pos
                    size: self.size

        Label:
            text: "Catégories de thés"
            font_size: '32sp'
            size_hint_y: None
            height: '100sp'
            font_name: "font/Ubuntu-L.ttf"
            color: (1, 1, 1, 0.95)

...

Résultat (correspondant aux premières lignes, avant la liste avec le picto de feuille) :

 

 

3. Design avec les images

Cette fois, je reprend un screenshot d’un iPhone pour l’intégrer dans Kivy…

Share albums

source : http://dribbble.com/shots/1231195-Share-albums

<Thes>:
    logLabel: logLabel_id
    containerListView: list_id
    nomcategorieLabel: nomcategorieLabel_id
    canvas.before:
        Color:
            # 247 244 240
            rgba: 0.96862, 0.95686, 0.94117, 1
        Rectangle:
            pos: self.pos
            size: self.size

    BoxLayout:
        id: categoriesLayout
        orientation: 'vertical'

        Label:
            canvas.before:
                Color:
                    rgba: 1,1,1, 1
                Rectangle:
                    pos: self.pos
                    size: self.size
            id: nomcategorieLabel_id
            text: "TeaTime > XXX"
            font_size: '16sp'
            size_hint_y: None
            height: '25sp'
            halign: 'left'
            text_size: self.size
            padding: '-10sp', '0sp'
            font_name: "font/Ubuntu-L.ttf"
            color: (0, 0, 0, 0.90)

        Label:
            canvas.before:
                Color:
                    rgba: 1,1,1, 1
                Rectangle:
                    pos: self.pos
                    size: self.size
            text: "Liste des thés"
            font_size: '32sp'
            font_name: "font/Ubuntu-L.ttf"
            color: (0, 0, 0, 0.90)
            size_hint_y: None
            height: '60sp'

        Label:
            canvas.before:
                Color:
                    rgba: 1,1,1, 1
                Rectangle:
                    source: "images/liner.png"
                    pos: self.pos
                    size: self.size
            text: " "
            size_hint_y: None
            height: '4sp'

        Label:
            canvas.before:
                Color:
                    rgba: 0.96862, 0.95686, 0.94117, 1
                Rectangle:
                    pos: self.pos
                    size: self.size
            text: " "
            size_hint_y: None
            height: '6sp'
...

Résultat :

 

Conclusions

  • Le design avec les images est quand même plus rapide et facile lorsqu’on dispose d’une maquette.
  • Il faut savoir que les images sont étirées lorsqu’elles sont en image de fond. Je n’ai pas encore trouvé la manière simple de dire à Kivy l’équivalent du « background-repeat » en CSS
  • Il y a une astuce qui n’est pas si évidente (et pas forcément bien expliquée) pour faire des boutons designés à partir d’une image. Kivy prend les bords de l’image pour reconstruire un bouton, en associant les propriétés « border » et « background_normal ».
  • L’objet « ListItemButton » reste pour moi un peu difficile à manipuler. Cet objet est un bouton dans la liste des items (la liste des thés dans le dernier screenshot ci-dessus). Il y a un layer de couleur qui est appliqué sur l’image de fond et je n’arrive pas à le supprimer… J’ai contourné le problème avec un autre objet en dessous qui a l’image de fond.

Deploy amélioré de kivy sur Android, avec buildozer

Introduction

Cet article fait suite à celui qui montre comment déployer sur le mobile une application kivy avec une VM. Le résultat était concluant, avec pas mal de manipulations pour déployer le fichier APK généré.

La taille des textes était très petite car la résolution du S4 est de 1080 x 1920 au lieu du test sur mon PC, en 600 x 800.

 

Correction de la taille des textes

Toutes les dimensions sont passée en unité « sp » dans les fichier kv.

Au lieu de :

font_size: 32

il y a :

font_size: '32sp'

La taille « 32 » devient indépendante de la résolution du device.

 

Déploiement avec buildozer

La procédure avec buildozer parait très avancée : une ligne de commande et mon application kivy est déployée sur mon mobile ? Un peu de magie ?

Sur mon PC (amd64 avec ubuntu), la procédure d’installation décrite sur cette page est insuffisante. Il faut que j’installe des bibliothèques supplémentaires (lib32ncurses5, lib32z1 lib32bz2-1.0, lib32stdc++6, libc6-i386).

Le paramétrage des variables d’environnement est fait comme indiqué :

export ANDROIDSDK=/home/julien/.buildozer/android/platform/android-sdk-21
export ANDROIDNDK=/home/julien/.buildozer/android/platform/android-ndk-r9
export ANDROIDNDKVER=r9
export ANDROIDAPI=21
export PATH=$ANDROIDNDK:$ANDROIDSDK/platform-tools:$ANDROIDSDK/tools:$PATH

Le fichier « buildozer.spec » est édité pour définir les paramètres de l’application (le chemin des sources, le nom du package, l’orientation, etc…). De plus, la version est maintenant portée par le « main.py ».

La magie peut maintenant opérer en une ligne de commande :

/usr/share/kivy/buildozer$ buildozer android debug deploy run

L’application est compilée, packagée (fichier APK généré), déployée sur le mobile et même lancée.

Le résultat est là :

Bravo à l’équipe Kivy pour ces outils.

Test de kivy sur Android (Samsung Galaxy S4)

Kivy, deploy sur le mobile

Suite à l’article sur un premier test de kivy, je me lance dans l’installation de l’application de test sur un smartphone… Est-ce que l’application sur mon PC est la même que sur le smartphone ? Est-ce que Kivy tient ses promesses ?

 

Opérations sur le PC

Il est possible de déposer ses fichier .py et .kv dans un dossier sur le smartphone et de tester avec « Kivy Launcher », mais pour le moment, ça ne fonctionne pas (pourtant le processus est détaillé, chez moi : sans succès). Je me lance donc vers le processus de création du package APK pour Android pour voir ce que ça donne.

Voici les étapes.

1. Récupération de la VirtualBoX

L’équipe kivy propose une VM avec les outils disponibles à la création du package. Ceci évide de devoir installer tous les outils sur le PC. Suivre le lien http://kivy.org/#download, section « Virtual Machine ».

Après avoir lancé la VM, il faut réaliser les opérations suivantes:

2. Compilation des package

cd ~/android/python-for-android/
./distribute.sh -m "kivy"

3. Copie des sources dans la VM

Avec l’option de drag-n-drop, copier les fichiers de l’application (.py, .kv, images, polices, etc…)

4. Lancement du build du package de l’application (génération de l’APK)

cd ~/android/python-for-android/dist/default
./build.py --dir ~/Public/build/ --name "Login Sample" --package org.test.login --version 0.1 --orientation portait debug installd

Le fichier APK est dans bin/

Il faut alors récupérer l’APK sur le PC et l’envoyer sur le mobile (dans le répertoire Download). Pour mon cas, le fichier fait 6Mo (pour les 4 fichiers décrits dans le premier article).

 

Opérations sur le mobile

Avant tout, il faut se mettre en mode développeur (et en debug). Une opération cachée le permet sur le Samsung S4 (dans la partie paramétrage). Une description est faite dans une vidéo.

Installer « Apk Manager+ » depuis google play.

Lancer « Apk Manager+ » et installer l’APK de l’application qui se trouve dans le répertoire de dépôt.

Lancer l’application : ca fonctionne… enfin c’est presque bon…

Les textes sont très petits. Il faudra régler la taille des textes (selon l’unité proposée : « pixel », « inch », « dp », « sp », etc…).

En conclusion, cela fonctionne plutôt bien, reste quelques réglages à faire et des bonnes pratiques à connaître.

Le mode de test en live (avec « Kivy Launcher ») ne fonctionne pas chez moi. Il apporte une plus-value énorme pour ces réglages.

Premiers pas avec Kivy

Introduction

Kivy est un framework récent basé sur le langage Python, dont l’objectif est de créer des applications mobiles sous Android, iOS, Windows Phone, ou pour un client lourd, sur PC habituel, sous Windows, Linux et MacOS. Ambitieux projet et en version 1 ; on va essuyer les plâtres. Voyons ce que ça donne, avec les forums et articles qu’on peut trouver sur le web…

Premier constat : l’API est très complète, les forums sont actifs (même pour cette version 1). En cherchant un peu et testant pas mal, on trouve une solution.

 

Installation

Avant de me lancer, j’installe kivy comme préconisé sur mon environnement ubuntu. tout se passe très bien.

J’installe aussi le plugin eclipse pyDev histoire d’avoir un IDE adéquat. Pas de soucis de ce coté là non plus. Il faut juste noter que Python3 (installé sur mon PC) n’est pas compatible, mais python 2.7 l’est. C’est précisé dans l’installation. je configure mon environnement eclipse en fonction.

Les exemples fournis (dans /usr/share/kivy-examples/) se lancent très bien. je peux commencer à faire ma propre application pour explorer ce framework.

 

Première application

Ma première application se contentera de présenter une page de connexion (login, password) et une page où on est connecté (avec un message et un bouton pour se déconnecter). 2 pages simples, en fait.

Voici le résultat après quelques heures de travail (dont pas mal de temps à essayer de comprendre et torturer ce framework).

Le projet se compose de 4 fichiers :

  • main.py : Fichier python de démarrage de l’application. Il définit la page de login
  • login.kv : Description dans le langage « kv » de l’interface utilisateur (= les widgets) de la page de login
  • connected.py : Fichier python de définition du comportement de la page de déconnexion
  • connected.kv : Description dans le langage « kv » de l’interface utilisateur (= les widgets) de la page où l’utilisateur est connecté. Il y a un libellé et un bouton pour se déconnecter

Le principe est d’avoir 2 vues dans un Screenmanager. La transition sera pilotée par les fichiers Python.

Fichier main.py

C’est le fichier principal de l’application.

import kivy
kivy.require('1.0.5')

from kivy.config import Config
Config.set('graphics', 'width', '400')
Config.set('graphics', 'height', '600')

from kivy.app import App
from kivy.uix.screenmanager import ScreenManager, Screen, SlideTransition

from connected import ConnectedApp

class Login(Screen):
	def do_login(self, loginText, passwordText):
		print("login : %s || pass : %s" % (loginText, passwordText) )
		self.manager.current = self.manager.next()

	def resetForm(self):
		self.ids['login'].text = ""
		self.ids['password'].text = ""

class LoginApp(App):
	def build(self):
		manager = ScreenManager()

		# ajout de l'instance de login
		manager.add_widget(Login(name='login'))

		# ajout de la vue 'connected'
		app = ConnectedApp()
		app.load_kv()
		connectedView = app.build()
		manager.add_widget(connectedView)

		manager.transition = SlideTransition(direction="left")
		return manager

if __name__ == '__main__':
	LoginApp().run()

Ce fichier défini :

  • La classe « Login », qui hérite de « Screen » (un Widget) afin de faire une page parmi d’autres dans l’application.
  • La classe « LoginApp », qui sera la classe de l’application. La méthode build() fait tout le boulot :
    • Elle défini un ScreenManager avec une instance de « Login ». Il n’y a rien à faire au niveau du fichier kv car il est automatiquement chargé (grâce à son nom).
    • Pour les classes « Connected » et « ConnectedApp », c’est plus compliqué… Il faut instancier « ConnectedApp », charger le fichier kv correspondant (le nom est automatiquement défini), récupérer le Widget héritant de Screen (via la méthode build()) et l’ajouter dans le ScreenManager avec un joli nom.
    • La transition de page à page (Screen à Screen) sera en mode « left » : la page prochaine page à afficher viendra vers la gauche pour recouvrir la page actuelle.

Quand on clique sur le bouton de connexion, le login et le mot de passe sont affichés et on présente la page suivante du manager. On imagine la vérification par Base de données SQLite ou par WebService.

 

Fichier login.kv

Ce fichier défini les Widgets et la façon dont ils sont arrangés dans la vue. Un peu de design est nécessaire (font_size, padding, spacing, etc).

#:kivy 1.0

# Report is a BoxLayout
## MUST USE <> because this is a rule, not the root widget
<Login>:
	BoxLayout
		id: login_layout
		orientation: 'vertical'
		padding: [10,50,10,50]
		spacing: 50

		Label:
			text: "Welcome"
			font_size: 32

		BoxLayout:
			orientation: "vertical"

			Label:
				text: "Login"
				font_size: 18
				halign: 'left'
				text_size: root.width-20, 20

			TextInput:
				id: login
				multiline:False
				font_size: 28

		BoxLayout:
			orientation: 'vertical'
			Label:
				text: "Password"
				halign: "left"
				font_size: 18
				text_size: root.width-20, 20

			TextInput:
				id: password
				multiline:False
				password:True
				font_size: 28

		Button:
			text: "Connexion"
			font_size: 24

			on_press: root.do_login(login.text, password.text)

Le plus important est dans la méthode « on_press » du bouton de connexion. Elle est appelée avec les deux données saisies dans les TextInput : « login.text » et « password.text ».

 

Fichier connected.py

Ce fichier contient le comportement de la page qui s’affiche une fois que le bouton de connexion est cliqué.

import kivy
kivy.require('1.0.5')

from kivy.app import App
from kivy.uix.screenmanager import Screen, SlideTransition

class Connected(Screen):
	def disconnect(self):
		self.manager.transition = SlideTransition(direction="right")
		self.manager.current = 'login'
		self.manager.get_screen('login').resetForm()

class ConnectedApp(App):
	def build(self):
		screen = Connected()
		screen.name = 'connected'
		return screen

# standalone usage
if __name__ == '__main__':
	ConnectedApp().run()

C’est la classe qui ne fait rien, à part la méthode disconnect() :

  • Définir une jolie transition : vers la droite
  • Présenter la page de login
  • dans la page de login, purger les champs

 

Fichier connected.kv

Ce fichier défini le message et le bouton de déconnexion.

#:kivy 1.0

<Connected>:
	id: connectedScreen
	BoxLayout:
		orientation: 'vertical'
		Label:
			text: "You are now connected"
			font_size: 32
		Button:
			text: "Disconnect"
			font_size: 24
			on_press: connectedScreen.disconnect()

Si vous avez suivi les étapes précédentes, il n’y a rien de neuf…

 

Lancement de l’application

Sous Eclipse, avec PyDev, il suffit de faire un « run python » sur le fichier « main.py » et l’application se lance. Depuis un terminal, « python main.py » fait la même chose.

Voici le résultat de cette petite application :

On saisi un login, un mot de passe, on clique sur le bouton… L’effet de transition est très fluide.

 

Et la page une fois connecté :

Un clic sur le bouton nous emmène vers la page de login avec les champs purgés.

 

Impressions, conclusion

Inconvénients :

  • En effet, on essuie un peu les plâtres pour faire des choses relativement simples. Expérience trop réduite ?
  • Il est très difficile de designer l’interface des écrans par le langage kv, l’auto-complétion n’est disponible. Une interface à la Glade serait bien venue ; faut-il penser à une transformation XSL pour le générer à partir d’un fichier issu de Glade ?
  • Un seul thème est disponible (sur fond noir) est il est relativement difficile de designer son application comme un site web. L’ajout d’images pour correspondre à un charte graphique risque d’être difficile. Mais c’est un beau défi à relever !

 

Avantages :

  • Très peu de lignes de code sont nécessaires pour faire fonctionner une application (Merci Python).
  • L’API est très complète et la communauté travaille à dégrossir ce framework.
  • Kivy accède directement aux pilotes OpenGL.
  • Pas d’émulateur à installer.
  • Même si je n’ai pas codé en Python depuis longtemps (et si je ne suis pas développeur dans ma vie professionnelle), j’arrive à un résultat convenable.

 

Je n’ai pas déployé cette application autre-part que sur mon PC (je n’ai pas de smartphone, sorry). Pour moi, l’uniformité du rendu est encore à démonter sur tous les supports…

En tout cas, les prochaines versions sont à suivre de près.

 

Test unitaires dans le générateur de code

Après avoir intégré avec succès les tests unitaires dans CodeIgniter avec Toast, j’ai ajouté un template de test unitaire dont l’objectif est de s’assurer que la couche modèle fonctionne. Ces tests sont indépendants de la BDD métier car ils utilisent une base SQLite3.

Pour ce faire, Toast permet de créer un controller dédié à cette tâche.

J’ai choisi une entité qui s’appelle « Groupe » et qui a 3 propriétés « grpidgrp = Identifiant », « grplblib = Libellé », « grpidsoc = Clé étrangère vers l’entité Société ». Voici en détail les tests réalisés (automatiquement générés avec le code de l’application, donc).
(Tout ce code est généré, rien n’a été retouché)

/*
 * Created by generator
 *
 */
require_once(APPPATH . '/controllers/test/Toast.php');

class GroupeTest extends Toast {

	function __construct(){
		parent::__construct();
		$this->load->database('test');

		$this->load->model('Groupe_model');

	}

	/**
	 * OPTIONAL; Anything in this function will be run before each test
	 * Good for doing cleanup: resetting sessions, renewing objects, etc.
	 */
	function _pre() {
		$groupes = Groupe_model::getAllGroupes($this->db);
		foreach ($groupes as $groupe) {
			Groupe_model::delete($this->db, $groupe->grpidgrp);
		}
	}

	/**
	 * OPTIONAL; Anything in this function will be run after each test
	 * I use it for setting $this->message = $this->My_model->getError();
	 */
	function _post() {
		$groupes = Groupe_model::getAllGroupes($this->db);
		foreach ($groupes as $groupe) {
			Groupe_model::delete($this->db, $groupe->grpidgrp);
		}
	}

	public function test_insert(){
		$this->message = "Tested methods: save, getGroupe, delete";
		// création d'un enregistrement
		$groupe_insert = new Groupe_model();
		// Nothing for field grpidgrp
		$groupe_insert->grplblib = 'test_0';
		$groupe_insert->grpidsoc = 0;
		$groupe_insert->save($this->db);
		// $groupe_insert->grpidgrp est maintenant affecté

		$groupe_select = Groupe_model::getGroupe($this->db, $groupe_insert->grpidgrp);

		$this->_assert_equals($groupe_select->grpidgrp, $groupe_insert->grpidgrp);
		Groupe_model::delete($this->db, $groupe_select->grpidgrp);
	}

	public function test_update(){
		$this->message = "Tested methods: save, update, getGroupe, delete";
		$groupe_insert = new Groupe_model();

		// Nothing for field grpidgrp
		$groupe_insert->grplblib = 'test_0';
		$groupe_insert->grpidsoc = 0;
		$groupe_insert->save($this->db);

		// Nothing for field grpidgrp
		$groupe_insert->grplblib = 'test1_0';
		$groupe_insert->grpidsoc = 90;
		$groupe_insert->update($this->db);

		$groupe_update = Groupe_model::getGroupe($this->db, $groupe_insert->grpidgrp);

		if(!$this->_assert_equals($groupe_insert->grpidgrp, $groupe_update->grpidgrp)) {
			return false;
		}
		if(!$this->_assert_equals($groupe_insert->grplblib, $groupe_update->grplblib)) {
			return false;
		}
		if(!$this->_assert_equals($groupe_insert->grpidsoc, $groupe_update->grpidsoc)) {
			return false;
		}

		Groupe_model::delete($this->db, $groupe_insert->grpidgrp);
	}

	public function test_count(){
		$this->message = "Tested methods: getCountGroupes, save, getGroupe, delete";

		// comptage pour vérification : avant
		$countGroupesAvant = Groupe_model::getCountGroupes($this->db);

		// création d'un enregistrement
		$groupe = new Groupe_model();
		// Nothing for field grpidgrp
		$groupe->grplblib = 'test_0';
		$groupe->grpidsoc = 0;
		$groupe->save($this->db);

		// comptage pour vérification : après insertion
		$countGroupesApres = Groupe_model::getCountGroupes($this->db);

		// verification d'ajout d'un enregistrement
		$this->_assert_equals($countGroupesAvant +1, $countGroupesApres);

		// recupération de l'objet par son  grpidgrp
		$groupe = Groupe_model::getGroupe($this->db, $groupe->grpidgrp);

		// suppression de l'enregistrement
		Groupe_model::delete($this->db, $groupe->grpidgrp);

		// comptage pour vérification : après suppression
		$countGroupesFinal = Groupe_model::getCountGroupes($this->db);
		$this->_assert_equals($countGroupesAvant, $countGroupesFinal);

	}

	function test_list(){
		$this->message = "Tested methods: save, getAllGroupes, delete";

		$groupe_insert = new Groupe_model();
		// Nothing for field grpidgrp
		$groupe_insert->grplblib = 'test_0';
		$groupe_insert->grpidsoc = 0;
		$groupe_insert->save($this->db);

		$groupes = Groupe_model::getAllGroupes($this->db);
		if( ! $this->_assert_not_empty($groupes) ) {
			return FALSE;
		}
		$found = 0;
		foreach ($groupes as $groupe) {
			if($groupe->grpidgrp == $groupe_insert->grpidgrp &&
					$this->_assert_equals($groupe->grplblib, $groupe_insert->grplblib ) &&
					$this->_assert_equals($groupe->grpidsoc, $groupe_insert->grpidsoc )
				){
				$found++;
			}
		}
		if( $found == 1 ){
			Groupe_model::delete($this->db, $groupe->grpidgrp);
			return TRUE;
		}else{
			return FALSE;
		}
	}

}

 

Il faut récupérer le script SQLite3 généré et l’intégrer dans la BDD de TU :

/*
 * Lancer la commande suivante pour insérer les données
 * cat cretab_groupe.sqlite | sqlite3 test_database.sdb
 */

CREATE TABLE expgrp (
	grpidgrp integer NOT NULL PRIMARY KEY AUTOINCREMENT ,
	grplblib varchar(255) NOT NULL ,
	grpidsoc integer NOT NULL
);

 

Dans un navigateur, voici le résultat sur l’URL du controller de TU :

A ce stade, aucune ligne de code n’a dû être écrite par un développeur. Il pourra commencer son application avec des bases saines. J’ose le terme de « Framework Métier », permettant de poser les briques du système à réaliser.

 

SQLite avec CodeIgniter

Introduction

Voici un descriptif des étapes à faire pour connecter son application CodeIgniter v2 à une base de données SQLite3. Cet article est à lier à celui sur les tests unitaires dans CodeIgniter avec Toast. L’intérêt est qu’une base de données de test est à disposition pour ne faire que des tests et laisser la base cible (de dev par exemple) en place, sans rien y changer.

Attention : CodeIgniter version 2.1.0 ne fonctionne pas avec SQLite3. Il faut passer à la version 2.1.3

 

Étapes

Installation des packages :

sudo apt-get install sqlite3 php5-sqlite php5-curl

Vérification des extensions PHP:

ls /etc/php5/apache2/conf.d

Il doit y a voir « pdo.ini » et « sqlite3.ini »

 

Définir des tables dans la base de données :

cat script.sql | sqlite3 test_database.sqlite

Le fichier « script.sql » contient les lignes suivantes :

CREATE TABLE expsoc (
    socidsoc integer NOT NULL PRIMARY KEY AUTOINCREMENT,
    soclblib varchar(255) NOT NULL
);

 

Autoriser l’accès au fichier « test_database.sqlite » :

chmod o+rw test_database.sqlite

 

Ajouter un fichier de langue (vide) dans /application/language/french/db_lang.php

 

Configurer la base de données dans CodeIgniter :

require_once(APPPATH . '/controllers/test/Toast.php');

class SocieteTest extends Toast {

	function __construct(){
		parent::__construct(__FILE__);
		$this->load->model('Societe_model');

		$config['hostname'] = "sqlite:".APPPATH."/database/test_database.sdb";
		$config['username'] = "";
		$config['password'] = "";
		$config['database'] = "";
		$config['dbdriver'] = "pdo";
		$config['dbprefix'] = "";
		$config['pconnect'] = TRUE;
		$config['db_debug'] = TRUE;
		$config['cache_on'] = FALSE;
		$config['cachedir'] = "";
		$config['char_set'] = "utf8";
		$config['dbcollat'] = "utf8_general_ci";
		$config['swap_pre'] = '';
		$config['autoinit'] = TRUE;
		$config['stricton'] = FALSE;

		$this->load->database($config);

	}
	...
}

 

Un test basique

Voici une méthode du controller de test (dont le début est ci-dessus) pour valider que les accès à la base de données sont toujours bons :

    public function test_count(){

        // comptage pour vérification : avant
        $countSocietesAvant = Societe_model::getCountSocietes($this->db);

        // création d'un enregistrement
        $societe = new Societe_model();
        $societe->socidsoc = 1;
        $societe->soclblib = "Ma société";
        $societe->save($this->db);

        // comptage pour vérification : après insertion
        $countSocietesApres = Societe_model::getCountSocietes($this->db);

        // verification d'ajout d'un enregistrement
        $this->_assert_equals($countSocietesAvant +1, $countSocietesApres);

        // recupération de la societe socidsoc=1
        $societe = Societe_model::getSociete($this->db, 1);

        // suppression de l'enregistrement
        Societe_model::delete($this->db, $societe->socidsoc);

        // comptage pour vérification : après suppression
        $countSocietesFinal = Societe_model::getCountSocietes($this->db);

        $this->_assert_equals($countSocietesAvant, $countSocietesFinal);

    }

On y fait :

  1. Décompte des enregistrements dans la base
  2. Ajout d’un enregistrement
  3. Décompte des enregistrements dans la base
  4. Vérification de l’ajout : [nb avant] + 1 == [nb après]
  5. Récupération de l’enregistrement ajouté
  6. Suppression de l’enregistrement
  7. Décompte des enregistrements dans la base
  8. Vérification de la suppression : [nb avant] == [nb à la fin]

 

Lancement du test

L’URL suivante permet de tester unitairement certains accès à la base :

http://localhost/monprojet/index.php/test/societetest/count

 

Voici le résultat :

 

Reste à faire

Il reste à intégrer ce genre de tests unitaires dans le générateur de code afin de disposer de ces tests dès que l’application est prête :

  • Un nouveau template pour le controller de test (1 controller de test par entité)
  • Nouveau template pour le SQL à passer dans SQLite3 (1 fichier sqlite par entité)

 

Test unitaires avec CodeIgniter

Introduction

Les tests unitaires, tout le monde connait. En Java, junit fait l’unanimité (en plus, il est intégré à Eclipse ce qui en fait une arme redoutable). En PHP, avec le framework CodeIgniter, beaucoup de choses sont proposées, mais sans tutoriel pour montrer comment ça fonctionne concrètement.

Voici ma méthode, avec PHPUnit, Toast et CodeIgniter v2. Bien sûr, ça n’a pas fonctionné du premier coup (ce serait trop facile). Les prérequis sont :

 

Il faut commencer par revoir l’installation de Toast

Juste un petit mot sur Toast : il est basé sur les classes de test unitaires du framework de Codeigniter (qui s’appuie sur PHPUnit pour les tests unitaires) ; il est composé de quelques fichiers seulement (facile à maintenir et à customiser) ; il permet de se connecter à une base données indépendantes, juste pour les tests. C’est donc un bon candidat pour faire ces tests unitaires.

  • Les fichiers Toast sont dans « monProjet/application/controllers/test/ » pour les contrôleurs (qui feront les tests) et dans « monProjet/application/views/test/ » pour le rendu graphique.
  • Fichier « Toast.php » : la classe doit être déclarée pour utiliser CodeIgniter v2 et il faut recâbler les accès au nom de la classe de test :
abstract class Toast extends CI_Controller {
  ...
  function __construct(){
		parent::__construct();
		$this->load->library('unit_test');
		$this->modelname = get_class($this); //$name;
		$this->modelname_short = get_class($this); //basename($name, '.php');
		$this->messages = array();
	}
  ...
}
  • Idem avec le fichier « Toast_all.php » :
class Toast_all extends CI_Controller {
  ...
  function __construct(){ 
      parent::__construct(); 
  }
  ...
}

 

Le premier test unitaire basique peut commencer

  • Créez un nouveau fichier « SimpleTest.php » :

Il faut préfixer les méthodes de test par « test_ » et seul la fin du nom de la méthode sera présentée dans le rendu graphique.

require_once(APPPATH . '/controllers/test/Toast.php');

class SimpleTest extends Toast {

	function __construct(){
		parent::__construct(__FILE__);
	}

	public function test_basic(){
		$my_var = 2 + 2;
		$this->_assert_equals($my_var, 4);
	}

	function test_false(){
		// Test code goes here
		$my_var = true;
		$this->_assert_false($my_var);
	}
}

Ce code est celui de Toast. Je pense faire une version plus correcte, mais ce sera pour plus tard…

 

Voir le résultat du test

Le test unitaire est vu comme un contrôleur du projet. Il suffit d’y accéder par son URL, dans son navigateur favori :

http://localhost/monProjet/index.php/test/simpletest

Le résultat est une page web qui présente les tests réalisés :

On peut cliquer sur le nom de la classe ou sur le nom de la méthode pour limiter l’affichage. Toast propose aussi un lancement de tous les tests disponibles.

Par la suite, on peut imaginer automatiser le lancement de ces tests avec un « wget » pour lancer le test suivi d’un « grep FAILED » pour filtrer le résultat. Je me demande si un rendu XML ou JSON peut être réalisé…

 

La suite

Pour clôturer ce sujet, je ferai un VRAI test unitaire dans mon projet, maintenant qu’on sait comment ça fonctionne. Si vous avez des idées de customisation du rendu graphique, postez-les !

J’intègrerai aussi certainement des tests unitaires dans mon générateur de code afin de pondre aussi des tests tout prêts.

 

Plugin jQuery pour choix de couleur

Si vous avez un sélecteur de couleur sous la forme d’un champ SELECT avec une palette de quelques couleurs, je vous propose de tester mon plugin jQuery qui le remplacera par une liste de blocs de couleurs sélectionnables. Je l’ai nommé « Short Color Selector » parce qu’il ne prend pas plus d’une ligne (d’où le « short » — Si vous avez un meilleur nom, je suis preneur !).

Voici un exemple :

Démo (avec syntaxe utilisée) : http://jc.specs.free.fr/shortColorSelector/demo.html

Dans le premier exemple, le champ SELECT est conservé. Dans le second, il est caché.

Pour plus de détail, voici les options possibles :

  • id du SELECT à utiliser pour reprendre les couleurs de la palette
  • largeur de chaque cellule de la palette (en pixels)
  • caractère de sélection de la couleur (par exemple, une étoile, check, etc…). Ce n’est pas une image car avec font-awesome, on peut trouver un picto qui ira très bien…
  • cacher ou pas le SELECT correspondant
  • appel d’une fonction JS pour déclencher un évènement lors de la sélection d’une couleur (le SELECT est mis à jour avec le choix de la couleur par défaut)

Actuellement, il n’est pas possible de choisir plusieurs couleurs en même temps, dans la même palette (je n’ai pas trouver l’intérêt que ça peut avoir — autant mettre 2 palettes pour les 2 couleurs à sélectionner…).

Merci de me donner votre avis et me dire ce qui manque.

inspiration :

Color Rotate

jc.specs à la mode Windows8

Microsoft prend une nouvelle tournure avec l’interface « MetroUI » ou »tiles » (= tuiles en français) de la version 8 de son système d’exploitation Windows8.

Plusieurs adaptations sont proposées pour les sites Web. J’ai essayé metroui.org.ua car il contient beaucoup de fonctionnalités et il est basé sur bootstrap :

  • CSS complète avec un bon choix des tailles et couleurs de tuiles
  • Icones en différentes tailles (adaptées avec le texte : h1, h2, …)
  • Des petites features bien sympas en JS (dont le scroll horizontal qui peut dérouter un peu)
  • Relativement léger à l’exécution, pas comme d’autres package « MetroUI styles »
  • Pas trop de features inutiles ou gourmandes en ressources

Voici un exemple d’installation pour le site jc.specs.free.fr, qui reprend mes essais (plus ou moins fructueux de mes projets personnels). Le choix de passer par cette interface n’a pas été sans mal, mais le résultat est plutôt original.

 

i18n dans le générateur de CRUD

Après l’article sur une amélioration de l’internationalisation dans CodeIgniter, j’ai intégré la gestion des traductions dans le générateur de CRUD pour CodeIgniter. Un fichier de plus est généré (un fichier de langue) par entité et tous les libellés sont maintenant appelés par cette interface. Les vues n’ont plus aucun libellé.

En conclusion, mon dernier template « Bootstrap » contient cette évolution, mais je ne pense pas le faire pour les autres templates, sauf si un nouveau projet sur un template voit le jour.

Voici le fichier de langue qui sera parsé par le générateur :

%[kind : lang]
%[file : messages_%%(self.obName.lower())%%_lang.php]
%[path : language/french]
<?php
/**
 * Message file for entity %%(self.obName)%%
 *
 * Please don't forget to load this file:
 *  Solution A : Use "/application/config/autoload.php"
 *               Add this line:
 *               $autoload['language'] = array(..., 'messages_%%(self.obName.lower())%%', ...);
 *
 *  Solution B : Load this message file anywhere you want.
 *  
 */

$lang['%%(self.obName.lower())%%.message.confirm.deleted'] = "%%(self.displayName)%% supprimé";
$lang['%%(self.obName.lower())%%.message.confirm.added'] = "%%(self.displayName)%% créé avec succès";
$lang['%%(self.obName.lower())%%.message.confirm.modified'] = "%%(self.displayName)%% mis à jour avec succès";

$lang['%%(self.obName.lower())%%.form.create.title'] = "Ajouter un %%(self.displayName.lower())%%";
$lang['%%(self.obName.lower())%%.form.edit.title'] = "Editer un %%(self.displayName.lower())%%";
$lang['%%(self.obName.lower())%%.form.list.title'] = "Liste des %%(self.displayName.lower())%%s";

$lang['%%(self.obName.lower())%%.menu.item'] = "%%(self.displayName)%%";

%%allAttributesCode = ""
for field in self.fields:
    attributeCode = """$lang['%(objectObName)s.form.%(dbName)s.label'] = "%(obName)s";
$lang['%(objectObName)s.form.%(dbName)s.description'] = "%(desc)s";  
""" % {    'dbName': field.dbName,
        'obName': field.obName,
        'objectObName':self.obName.lower(),
        'desc' : field.description
    }
    allAttributesCode += attributeCode
    
RETURN = allAttributesCode
%%

?>

 

Intégration du GanttCalendar

Suite à l’article sur le GanttCalendar, j’ai intégré ce plugin jquery dans une application web en PHP : « ExoPlanet ».

Intégration dans une application

Le plugin est tout à fait fonctionnel. Les évènements sont présentés sur 1 mois, le Gantt au dessus et la liste des évènements sous forme de tableau en dessous.

Le plugin a été intégré en quelques heures ; le paramétrage a été le plus long.