Jekyll2024-03-11T12:48:50+00:00https://alcalyn.github.io/feed.xmlAlcalyn’s blogAlcalyn's blog, about web, PHP and RaspberryPi.Julien MaulnyDécouverte de pixi.js pour animer un personnage avec Javascript2019-03-24T00:00:00+00:002019-03-24T00:00:00+00:00https://alcalyn.github.io/pixijs-dragonbones

Dans l’optique de faire un jeu style point and click sur navigateur, nous avons du dessiner et animer un personnage, et développer en Javascript son mouvement pour qu’il suive la souris.

Ce sont que des nouvelles méthodes et outils que nous avons découverts. Je vais montrer dans cet article comment nous avons réalisé la première étape de notre jeu, …

TL;DR:

L‘exemple ci-dessus est le rendu de la première étape qui a été d‘animer un personnage dans un navigateur, et de le lui faire suivre le pointeur (souris ou doigt).

Cette première étape qu‘on a pu accomplir après plusieurs recherches et essais, c‘est justement l‘objet de cet article. Je vais parler :

  • des librairies Javascript utilisées
  • des logiciels d‘animation testé et ceux que j‘ai retenus
  • de l‘animation de notre personnage
  • de la lecture des animations dans le navigateur.

Mais avant tout, comment animer un personnage ?

La première solution à laquelle j‘ai pensé a été de dessiner plusieurs images d‘un personnage avec à chaque fois un décalage des jambes. En affichant les images à la suite, ca animerait le personnage à la façon d‘un flip book.

Flip book
Dessiner chaque image puis les afficher rapidement à la suite.

En cherchant sur Internet si une librairie le faisait déjà, je suis tombé au hasard sur le site de Spine qui propose une solution totalement différente, ainsi que sur sa page de démo, assez auto-explicative :

http://fr.esotericsoftware.com/spine-demos

Les exemples sont très convainquant, on comprend vite l‘idée, et m‘a directement fait oublier la solution du flip book.

L‘idée est de ne pas dessiner tous les états du personnage, mais de le découper en segments pour chaque parties du corps, de les assembler comme un pantin, et ensuite de le faire marcher, courir, attendre…

Cette solution donne beaucoup d‘avantages :

  • Pas besoin de dessiner plusieurs fois le même personnage
  • Les segments du corps sont réutilisable pour toutes les marches
  • Beaucoup moins d‘images à télécharger donc optimale pour le web
Segments vs sprite sheets
En plus d’avoir une animation fluide au lieu de 30 images fixes par seconde, le découpage par segment prend beaucoup moins de place en mémoire.

Et encore d‘autres avantages, comme la déformation de maillage qui m‘a complètement séduit, concept illustré par cet exemple précis : http://fr.esotericsoftware.com/spine-demos#Mesh-deformations

La déformation de maillage permet de déformer une même image pour la tordre. On peut la tordre d‘une facon qui donne une impression de 3D, comme montré dans cet exemple. Mais je m‘en suis servi pour faire plier une jambe, ce qui m‘a permi de ne pas la séparer en 3 (fémur, tibia, pied), ou encore de faire plier une écharpe pour la faire pendre et lui donner une impression de flotte-au-vent.

Les logiciels testés et ceux que j‘ai finalement utilisés

En ce qui concerne le moteur de rendu Javascript, j’ai suivi les tutoriaux de Three.js et pixi.js pour au final choisir cette dernière, car l’API me semblait plus simpe de ce côté.

J’ai aussi découvert DragonBones, ainsi que son moteur Javascript en open-source (MIT) et son intégration à pixi.js. C’est tout trouvé, je vais utiliser pixi.js et l’intégration de DragonBonesJS à pixi.js.

Il me reste plus qu’a trouver le logiciel d’animation qui me permettrait d’animer mon personnage et d’exporter les sprites et animations au format attendu par DragonBonesJS.

Logiciels d‘animation pour pixi.js

Je parlais tout à l‘heure de Spine, ca tombe bien, c‘est le logiciel que je n‘ai pas utilisé pour faire marcher notre personnage !

Si je n‘ai pas utilisé Spine, c‘est principalement parce que c‘est un logiciel propriétaire, et que tout ce que j‘apprendrai dessus ou l‘aide que je pourrais éventuellement apporter aux autres sur ce logiciel serait en quelque sorte une contribution qui ne serait pas vraiment ouverte, mais resterait autour de Spine.

Néanmoins je l‘ai testé, et il donne effectivement envie. La version de test téléchargeable sur le site ne permet pas de sauvegarder ou d‘exporter quoique ce soit, la license personnelle coûte 69$, abordable, mais pas pour un projet experimental.

De plus, j‘ai plus tard découvert qu‘une librairie Javascript que j‘ai failli utiliser (pixi-spine) est sous cette même license fermée, nous obligeant à détenir une license payée pour pouvoir l‘utiliser. De ce fait, tout logiciel exportant des animations sous le même format JSON que Spine afin de réutiliser le moteur est donc inutilisable sans license, même si on n’utilise pas le logiciel Spine… Bref une impasse, cherchons ailleur.

Synfig studio est le premier logiciel que j‘ai testé, il semblait une bonne alternative à Spine mais je n‘ai pas trouvé comment exporter les animations pour les réutiliser dans pixi.js.

DragonBones Pro aurait pu être une piste, il est réalisé par les même personnes qui ont fait le moteur Javascript, et j’aurai pu exporter les animations directement au bon format pour pixi.js/DragonBonesJS, mais le logiciel nécessite de se créer un compte, et de toutes facons ne marche pas sous Linux…

Blender + plugin coa_tools (CutOut Animation) m‘a finalement sauvé. J‘ai eu du mal à trouver ce plugin, mais l‘auteur (Andreas Esau, @ndee85) a mis en ligne des vidéos de présentation de ce plugin, et c‘est finalement ce que j‘ai utilisé.

A propos de Blender
Blender permet de faire une vaste quantité de chose, mais cette polyvalence lui coûte d’être plutôt compliqué aux premiers abords. Cependant j‘ai déjà pu m‘y initier dans le passé, et j’ai maintenant une bonne occasion de l’utiliser à nouveau.

Animation du personnage

La partie qui nous a le plus plu, à part qu’il a d’abord fallu passer par une étape qu’on ne maitrisait pas…

Dessin du personnage

Il a fallu d‘abord dessiner le personnage.

J’ai donc utilisé Krita, un logiciel de dessin qui même quand on sait pas dessiner, donne envie de dessiner quelque chose. C’est un logiciel libre qui fonctionne sous Linux.

Krita screenshot
Capture d’écran du logiciel Krita.

Cependant on ne savait pas vraiment dessiner, et c‘était plutôt frustant quand t‘as comme source d‘inspiration :

Rainy days by David Revoy
License: CC-BY David Revoy, www.davidrevoy.com, 14 March 2017.

(Image : https://www.davidrevoy.com/article603/rainy-days)

Et que tout ce que tu peux faire, c‘est :

Girl
License: CC-BY Julien Maulny, Janvier 2019 environ.

Et finalement, avec une tablette graphique empruntée, je me suis tourné vers un style pas trop engageant et simple, ce qui est à ma portée, afin de laisser la possibilité d‘améliorer plus tard. Je me suis inspiré des illustrations qu’on peut trouver autour des méthodologies agile/scrum :

Girl agile scrum

C’est donc ce personnage que je vais garder. J’ai bien organisé les calques, j’en ai un pour :

  • la tête
  • les yeux : on voit ici les yeux ouverts et fermés superposés
  • l’écharpe
  • le corps
  • et un pour chaque jambe.

Je devrais séparer les calques pour ensuite réassembler le personnage en pantin, et permettre à chaque segment de bouger librement.

Animation du personnage sous Blender + coa tools

Cette partie ne sera pas un tutoriel pour animer un personnage avec Blender, cependant je vais montrer les étapes qui illustrent bien l’idée.

Pour animer personnage, je vais devoir :

  • importer les segments de mon personnage dessiné dans Blender
  • tracer un squelette et lier les os aux bon segments du personnage
  • créer les animations de marche et d’inactif
  • exporter les animations et sprites pour lire les animations avec Javascript

Création de l’armature

Je vais donc importer les sprites de mon personnage avec le plugin, tracer le squelette dessus, et lier chaque os à la ou les sprites qu’il est censé contrôller :

Blender COA Tools skeleton
Les os sont les triangles gris, ils peuvent pivoter autour de leur axe représenté par la boule au bout du petit triangle.

Le plus important ici est que les os sont reliés entre eux avec une relation père/fils, et en faisant pivoter un os, les os fils suivent le mouvement.

Blender COA Tools skeleton control
Cela permet de faire pivoter tout le haut du corps d’un coup.

Ce qui va m’aider ensuite à animer un mouvement de tête ou une marche sans devoir contrôller la rotation de tous les os individuellement.

J’ai aussi mis des os sur l’écharpe pour pouvoir la faire bouger.

Animations du personnage

Après avoir affecté tous les os à leurs sprites, je vais pouvoir animer la marche.

Pour cela je dois créer des images clés. Je peux indiquer dans une image clé la position et rotation des os à un moment précis. Ensuite, le plugin va créer les interpolations de mouvement en créant les transitions entre les images clés.

Blender timeline with COA tools
Lecture de l’animation

Après avoir ajusté mon animation de marche, je fait aussi l’animation “au repos” qui sera jouée lorsque le personnage ne bouge pas. Ca le rendra plus vivant.

Pour les yeux, j’ai mis les deux sprites (yeux ouverts et yeux fermés) dans un “slot”, un compartiment qui n’affiche qu’une seule sprite à la fois, et permet de choisir la sprite affichée.

Exporter mon animation pour Javascript

Une fois mon animation prête, le plugin coa tools me permet d’exporter toute l’animation au format JSON défini par DragonBones, et donc réutilisable par leur moteur Javascript.

L’export va me génerer :

  • une image avec tous les segments dedans,
  • un fichier JSON qui indique quel segment est représenté par quel rectangle dans l’unique sprite
  • un gros fichier JSON avec l’armature, le placement des os, les animations, les slots…

Ces trois fichiers seront importés par mon application Javascript avec DragonBonesJs.

Sprites de sprite
Une sprite de sprites !

Un peu plus loin dans Blender + coa tools

Je vais parler de deux détails technique un peu plus avancés par rapport à l’animaion du personnage. Ils ne sont pas nécessaire pour comprendre le reste, mais ils m’ont bien plu.

Kinématique inversée : pour ajouter un comportement naturel aux os

Je disais que les os fils héritent de la position et rotation du parent pour avoir un vrai pantin. Mais pour la marche, je n’ai pas eu à faire plier la jambe et tibia à chaque position.

En configurant une “kinématique inverse”, j’ai pu au contraire placer le pied à la bonne place, et la jambe se plie toute seule, en respectant les contraintes du squelette. Ceci aide encore plus pour animer la marche, tout en donnant un effet très naturel :

Blender COA tools IK bones
Déplacement du pied avec un os à kinématique inverse.

Déformation de maillage : pour faire plier les jambes et faire flotter l’écharpe dans le vent

Lorsque j’ai créé le squelette, vous pouvez remarquer que les trois os d’une jambe contrôllent un même sprite. En effet, la jambe est un unique trait, ca aurait été plus encombrant de séparer les 3 segments de la jambe/pied.

Dans ce cas, je dois assigner plusieurs os à une même sprite, mais alors, comment le logiciel sait comment faire bouger la sprite quand je bouge un des os ?

Par défaut, un os fait pivoter ou déforme pas directement la sprite, mais son “maillage”. Et au début, le maillage d’une sprite contient 4 sommets : les 4 coins de l’image. C’est insuffisant pour déformer l’image avec la précision souhaitée. Par exemple je ne pourrai pas pivoter un bout de l’écharpe, c’est toute l’image qui se redimensionnera.

Il faut donc mailler la sprite, en détourant l’image et en générant un maillage :

Blender COA tools mesh
J’ai détouré et généré automatiquement le maillage par triangulation.

Ensuite, il faut dire quels os contrôlent quelles sommets.

Blender COA tools defaults weights
Par défaut, Blender partage les sommets de manière naïve.

Ici, l’os de gauche contrôle la moitié gauche de l’écharpe, ce qui fait pivoter toute la partie gauche au lieu de bouger simplement le bout. Je dois donc éditer les poids avec un pinceau. J’ai donc réassigné de cette manière :

Blender COA tools weights edition tool
Maintenant, l’os de gauche contrôle seulement le bout de l’écharpe.

On peut utiliser la déformation de maillage de manière plus précise en redessinant par dessus les traits de la sprite, et affecter des poids de facon à générer de meilleurs rendus avec de simple images 2D. De plus, DragonBonesJs affiche les mêmes déformations dans le navigateur sans aucune lenteur, j’en ai été bluffé. J’en parle d’ailleur dans la prochaine partie.

Lecture des animations dans pixi.js / DragonBonesJS

J’ai donc créé l’animation de mon personnage dans Blender, et l’ai exportée au format DragonBones.

Je vais maintenant l’importer avec le moteur DragonBonesJS, plus précisement avec l’extension PixiJs.

https://github.com/DragonBones/DragonBonesJS/tree/master/Pixi

DragonBonesJs est écrit en TypeScript, et ils ont porté leur moteur pour pouvoir l’utiliser avec plusieurs autres librairies (ThreeJS, Cocos, Egret…). Ca veut dire une chose : je vais devoir me mettre à TypeScript…

Projet DragonBonesJs avec NPM

Pour l’utiliser avec pixi.js, DragonBones propose de cloner leur repository, de copier pixi.min.js dans ce repo, et de partir de cette base. Cela me plaît pas, j’ai plutôt l’habitude de partir d’un repository vide, et d’installer mes dépendances avec NPM. Ca me permet de garder les sources du moteur DragonBonesJs à jour, et de ne pas commiter des sources distantes.

Ca a été compliqué, mais j’ai pu créer mon repository vide avec mon l’environement NPM, TypeScript, et les dépendances pixi.js, DragonBonesJs. J’en ai créé un repo seed, ou skeleton :

https://gitlab.com/Alcalyn/pixi-dragonbones-skeleton

Ce skeleton contient l’environement TypeScript pour développer un projet pixi.js/DragonBonesJS, et le minimum qui permet de faire cette application “Hello World” : https://alcalyn.gitlab.io/pixi-dragonbones-skeleton/.

Importation et lecture de mon animation avec DragonBonesJs

J’ai donc animé mon personnage sous Blender, et ai exporté l’animation vers des fichiers au format DragonBones. J’ai donc mes 3 fichiers que je dois importer, interpreter et m’en servir pour générer l’animation avec Javascript.

Je me sers du Loader de PixiJs qui permet de charger des assets, et du dragonBones.PixiFactory qui permet de générer une animation à partir d’un export DragonBones :

// Add assets path to load
PIXI.loader.add('girl/dragonbones-export/Girl_ske.json');
PIXI.loader.add('girl/dragonbones-export/Girl_tex.json');
PIXI.loader.add('girl/dragonbones-export/Girl_tex.png');

PIXI.loader.once('complete', () => {

    const factory = new dragonBones.PixiFactory();

    // Parse skeleton and animations
    factory.parseDragonBonesData(resources['girl/dragonbones-export/Girl_ske.json'].data);

    // Parse sprites images
    factory.parseTextureAtlasData(
        resources['girl/dragonbones-export/Girl_tex.json'].data,
        resources['girl/dragonbones-export/Girl_tex.png'].texture,
    );

    // Generate armature and prepare animations (walk, idle)
    const armature = factory.buildArmatureDisplay('Armature');

    // Add armature sprite to the scene
    this.addChild(armature);
});

// Load all assets
PIXI.loader.load();

Je peux jouer l’animation walk ou idle que j’avais animé dans Blender :

armature.animation.play('walk');

setTimeout(() => armature.animation.play('idle'), 3000);

Je peux afficher les yeux ouverts ou fermé en récupérant le “slot” que j’avais créé dans Blender :

// Ouverts
armature.armature.getSlot('eyes').displayIndex = 0;

// Fermés
armature.armature.getSlot('eyes').displayIndex = 1;

Et donc j’ai utilisé tous ces outils pour faire marcher la fille jusque là où on clique avec la souris : https://alcalyn.gitlab.io/pixi-dragonbones-skeleton/

Pour faire bouger la fille dans le temps, pixi.js propose un Ticker qui peut appeller une fonction à chaque “tick”. Je m’en suis servir pour déplacer la fille vers la cible (là où l’utilisateur a cliqué), et mettre à jour l’animation (walk si elle était à l’arrêt, idle si elle s’arrête de marcher) :

/**
 * Function called at each images per second,
 * move girl to target, depending on WALK_SPEED,
 * play `walk` or `idle` animation if girl was waiting/walking.
 */
render(deltaTime: number): void {
    if (!this.walking) {
        return;
    }

    if (Math.abs(this.x - this.targetX) > this.WALK_SPEED) {
        // If target is at right/left
        const direction = this.x < this.targetX ? 1 : -1;

        this.x += deltaTime * this.WALK_SPEED * direction;
    } else if (Math.abs(this.y - this.targetY) > this.WALK_SPEED) {
        // If target is just below/above girl
        const direction = this.y < this.targetY ? 1 : -1;

        this.y += deltaTime * this.WALK_SPEED * direction;
    } else {
        // If girl is arrived to target
        this.x = this.targetX;
        this.y = this.targetY;
        this.armature.animation.play('idle');
        this.walking = false;
    }
}

// Register render() in PIXI.ticker
PIXI.ticker.add(deltaTime => render(deltaTime));

L’argument deltaTime correspond au temps depuis le dernier tick par rapport aux images par secondes. Il est en général autour de 1.0, mais si l’animation commence à lagguer, ce dernier sera plus grand car les ticks auront du mal à suivre les images par secondes. Je prend deltaTime en compte ici pour que la fille “rattrape” son retard si l’animation laggue.

Conclusion

Après avoir testé plusieurs outils, j’ai finalement pu trouver une suite d’outils compatibles et libres pour créer une animation avec Javascript :

Cet article n’était pas un tutoriel. J’étais finalement content d’avoir pu trouver un angle d’approche vers le monde de l’animation, et voulais faire partager les techniques que j’ai découvertes de mon point de vue de développeur.

Mais si cela vous interesse et souhaitez vous y mettre, voilà les tutoriels/documentations qui m’ont le plus aidé :

Licence Creative Commons

Julien Maulny

]]>
Julien Maulny
Set up continuous translation with Weblate and i18next2018-03-14T00:00:00+00:002018-03-14T00:00:00+00:00https://alcalyn.github.io/set-up-continuous-translation-i18next-weblate
Weblate screenshot
Weblate page of my project ‘OpenHex’.

I recently created a ReactJS project (Openhex).

I used i18next to translate it in English and French, and Weblate to allow contributors to help in translations through a web interface by translating sentences, or even add new languages.

Translating with i18next

I had two sources of strings in my source code:

  • In Javascript source code
throw new IllegalMoveError('You have not enough money to buy an unit.');
  • In ReactJS templates
<button onClick={ () => { this.buyUnit(); } }>
  Buy an unit
</button>

This is a front application (not a nodejs backend). I wanted to use translation keys, which means that I use a constant/unique key, then translate it in English, and in others languages:

throw new IllegalMoveError('You have not enough money to buy an unit.');

becomes:

throw new IllegalMoveError('cannot_buy_unit.not_enough_money');

I checked out many i18n libraries, and I finally used i18next for its great community, and all the existing modules, especially:

First thing, let’s translate Javascript strings (will translate the react templates later).

Installing i18next

Using npm:

npm install i18next --save

Which installed version ^10.5.0.

Configure i18next

I18next let you configure the main module instance, or a “created instance”. If you have only one translation file folder, you can use the main i18next instance, which mean you configure it like that:

src/i18next.js:

import i18next from 'i18next';
import { en, fr, es } from './engine/locales';

# Configure i18next
i18next.init({

  # Useful for debuging, displays which key is missing
  debug: true,

  # In which lang to translate (will be set dynamically later)
  lng: 'en',

  # If translation key is missing, which lang use instead
  fallbackLng: 'en',

  # Namespace to use by default, when not indicated
  defaultNs: 'translation',
});

# I load my translation files
i18next.addResourceBundle('en', 'translation', en);
i18next.addResourceBundle('fr', 'translation', fr);
i18next.addResourceBundle('es', 'translation', es);

src/engine/locales/index.js:

import en from './en.json';
import fr from './fr.json';
import es from './es.json';

export {
    en,
    fr,
    es,
};

src/engine/locales/en.json (using embed JSON):

{
  "cannot_buy_unit": {
    "not_enough_money": "You tried to buy or upgrade an unit, but you have only  gold, and an unit costs ."
  }
}

(and the same in fr.json, es.json, with translated strings).

You can now translate string in source code by reusing i18next module as the configuration has been loaded in the main instance with i18next.init():

import i18next from 'i18next';

i18next.t('cannot_buy_unit.not_enough_money', {
  playerMoney: 4,
  unitPrice: 10,
});

Using i18next.createInstance()

The example above works well for a “simple” translation unit (only one translation folder and configuration).

In my case, I had multiple translations folders, with different translations files formats (one in embed JSON, another in inline JSON).

Why using multiple translation folders ? I wanted to separate the engine translations with the user interface translations so that I could easily move the engine logic outside, in another repository. Also, I prefered multiple small translations units with differents concerns.

Let’s create an instance of i18next for the engine component. I just need to use i18next.createInstance().init({ ... }) in order to configure only a new instance of i18next instead of the main instance. Then, as soon as I created my own instance, I need to export the instance as a module to reuse it:

src/i18next.js:

import i18next from 'i18next';
import { en, fr, es } from './engine/locales';

# Create a fresh instance of i18next
const i18n = i18next.createInstance();

# same config...
i18n.init({
  debug: true,
  lng: 'en',
  fallbackLng: 'en',
  defaultNs: 'translation',
});

# loading my translation files
i18next.addResourceBundle('en', 'translation', en);
i18next.addResourceBundle('fr', 'translation', fr);
i18next.addResourceBundle('es', 'translation', es);

# Then, important thing, I export my instance
export default i18n;

Translating using my new instance is like:

src/myscript.js:

# Importing my i18next instance
import i18n from './i18next';

# using it
i18n.t('cannot_buy_unit.not_enough_money', {
  playerMoney: 4,
  unitPrice: 10,
});

This way, I can create another instance with a fully different configuration:

src/other-component/i18n/index.js:

import i18next from 'i18next';
import { en, fr } from './locales';

const i18n = i18next.createInstance();

i18n.init({
  debug: true,
  lng: 'fr',
  defaultNs: 'messages',
});

# loading my translation files
i18next.addResourceBundle('en', 'messages', en);
i18next.addResourceBundle('fr', 'messages', fr);

export default i18n;

Note about cloning instance: i18next allows to clone instance in order to reuse an existing instance and override some configuration.

Translate React templates

TLDR:

return (
  <button onClick={ () => { this.buyUnit(); } }>
    Buy an unit
  </button>
);

becomes:

# Import I18n react component
import { I18n } from 'react-i18next';

# Import your i18next instance
import i18n from './i18n';

return (
  <I18n i18n={ i18n }>
    {t => (
      <button onClick={ () => { buyUnit(); } }>
        { t('buy_unit') }
      </button>
    )}
  </I18n>
);

Installing and using this module didn’t caused so much troubles:

https://react.i18next.com/overview/getting-started.

The idea is to use the I18n react component, and pass your own i18next instance as prop: <I18n i18n={ i18n }>.

Then, you can use the t function to translate strings
in your template: <p>{ t('my_string') }</p>.

Here is how it looks like at the end on a real application: https://github.com/alcalyn/openhex/blob/f799a693914b0deae70612c5a78958423dcd0d60/src/components/GameMenu.js#L29-L47.

Dynamically translation guessing the user language

Install the i18next-browser-languageDetector module:

npm install --save i18next-browser-languagedetector

Which installed version ^2.1.0.

Then configure the module:

import LngDetector from 'i18next-browser-languagedetector';

i18next
    # Register the module
    .use(LngDetector)

    .init({
        debug: true,
        fallbackLng: 'en',
        defaultNs: 'translation',
        # lng: 'en',    <= Remove this or it will override auto detection

        # Configure the module
        detection: {
            # I want to guess from:
            #   - querystring first (when having in url ?lng=fr)
            #   - or navigator (the browser Accept-Language I guess)
            order: ['querystring', 'navigator'],

            # You can change the query string here to customize your urls
            lookupQuerystring: 'lng'
        }
    })
;

See module configuration reference here.

Automatically extract translation keys from source code

A last thing to automate a boring step: Copy all translations keys from source code and templates and paste them in all translation files.

I don’t want to look for any translation keys like t('home_page') or <h2>t('game_rules')</p>, and pase them manually to every files like en.json, fr.json, … and eventually new future languages.

So I used i18next-scanner to parse my source code and automatically dump translation strings into translation files.

By running i18next-scanner with a config file, it automatically update my translations file by adding new translations keys, keeping already translated strings…

I installed it with

npm install i18next-scanner --save-dev

Which installed the version 2.4.6.

I have translation strings from both javascript source code and react templates.

So I created 2 configuration files, here is one as example:

i18next-scanner.config.engine.js (in root folder):

module.exports = {
    options: {
        debug: true,
        // read strings from functions: IllegalMoveError('KEY') or t('KEY')
        func: {
            list: ['IllegalMoveError', 't'],
            extensions: ['.js'],
        },

        trans: false,

        // Create and update files `en.json`, `fr.json`, `es.json`
        lngs: ['en', 'fr', 'es'],

        ns: [
            // The namespace I use
            'translation',
        ],

        defaultLng: 'en',
        defaultNs: 'translation',

        // Put a blank string as initial translation
        // (useful for Weblate be marked as 'not yet translated', see later)
        defaultValue: (lng, ns, key) => '',

        // Location of translation files
        resource: {
            loadPath: 'src/engine/locales/.json',
            savePath: 'src/engine/locales/.json',
            jsonIndent: 4,
        },

        nsSeparator: ':',
        keySeparator: '.',
    },
};

I added in my package.json file the following script:

"scripts": {
  "translations-scan": "i18next-scanner --config i18next-scanner.config.engine.js src/engine/*.js"
}

So that I can run npm run translations-scan, and let the plugin scan my source code in src/engine/*.js and extract strings.

Then it created translations files in src/engine/locales/ with blank translations.

Here is my live example, with the configuration files in root folder: https://github.com/alcalyn/openhex

I don’t want to edit these files manually, as it would take so much time, and I don’t talk all languages. Instead I want to let people who want to contribute in their language to do it.

This is now the time to use Weblate !

Set up continuous translations with Weblate

Now that you have a translated application, you have to maintain and create new translations strings for all languages, and update every time you add a new string.

To do this as a developer, you need to clone your project, edit your translations files (json, po, xml files…) and commit them back.

This worklow requires your contributors to know how to use git. Also they need to edit translations strings in a format that is not always natural, and can lead to syntax errors (missing quotes in json, missing closing tag in xml, indentation error in yml…).

That’s why you need a translation interface: make anyone to easily translate your project in any language.

Many software exists for that, but I used Weblate for the following reasons.

What is Weblate

Weblate is a free (GNU-GPL) software you can try here.

You can install your own instance locally, or on your server, and avoid to be dependent to any external service.

This is useful as you can also freely install Weblate in your enterprise and use it for any software.

If you don’t want to install your own instance, Weblate has also a paid SaaS offer, but non-paid for free softwares.

I opted for installing my own instance.

Deploying my own instance

I used Docker to avoid to install required dependencies (PHP, python).

Note: Docker helps me to run an application without installing all packages it requires by running the application in a “container”, like a lightweight virtual machine, where all is installed. Then you just has to remove the Docker container, and no residual packages stay on your machine. You can install Docker and docker-compose here.

I installed it following the documentation to install Weblate with Docker:

git clone https://github.com/WeblateOrg/docker-compose.git weblate-docker
cd weblate-docker/

Then I created a docker-compose.override.yml file:

version: '3'
services:
  weblate:
    environment:
      # Any smtp server to enable mail sending
      - WEBLATE_EMAIL_HOST=smtp.example.com
      - WEBLATE_EMAIL_HOST_USER=user
      - WEBLATE_EMAIL_HOST_PASSWORD=pass

      # Configure "From:" emails
      - WEBLATE_SERVER_EMAIL=weblate@example.com
      - WEBLATE_DEFAULT_FROM_EMAIL=weblate@example.com
      - WEBLATE_ALLOWED_HOSTS=localhost

      # Your admin user login and password
      - WEBLATE_ADMIN_PASSWORD=mypassword
      - WEBLATE_ADMIN_EMAIL=weblate.admin@example.com

And I run:

docker-compose up

Then, go to http://localhost.

Note: You can use another port if your port 80 is already used. Change it in docker-compose.yml by replacing - 80:80 to - xxxx:80.

You can login with weblate.admin@example.com / mypassword, but it will be empty for now, you need to configure your projects.

Configure your translation workflow

Well, luckily, installing Weblate was not so hard with Docker. But now, let’s start the tricky part: configuration !

Weblate can handle tons of differents translation workflows: fetch sources from any repository, parse any translation files format (po, xml, json, embed json…), push modified strings to your repo on the branch you want, create a pull request or not, allow anonymous contributors to suggest correction, automatically submit suggestion once 3 people approved…

That’s why I took some time to figure how to set up my use case. But anyway, your use case is much likely covered by Weblate.

Let’s read the doc: I found that Weblate documentation is exhaustive, but not well organized: I don’t find easily what I am looking for.

But I will explain here only what I needed in the beginning.

The weblate admin interface is here: http://localhost/admin. Then go to “Weblate translations”, and you need to add “projects” and “components”.

Projects and components

First thing you need to setup your workflow: projects and components.

A project is a “folder” containing components.

A project represent your project, with an optional home page, has a source language.

A component represent a specific folder in your specific repository containing translation files in a specific format. It contains all the rules to fetch, parse translation files, and commit back to your repository.

Concrete example

In my project:

I want to weblatify the folder src/engine/locales. It contains embed json files: https://github.com/alcalyn/openhex/tree/master/src/engine/locales. Here is how a translation file looks like:

{
    "cannot_buy_unit": {
        "not_enough_money": "You tried to buy or upgrade an unit, but you have only  gold, and an unit costs .",
        "selection_not_empty": "You tried to buy a new unit, but you have a tower in your selection. Place your tower first."
    }
}

And how I display message in source code:

i18next.t('cannot_buy_unit.not_enough_money');

So let’s create a project called “Openhex”, and filling the required field project website with my project home page.

In http://localhost/admin/:

Weblate admin page
Add a project by clicking on ‘Add’ at the bottom of the page http://localhost/admin (You must be logged in as admin).
Weblate Add project page
Creating a new project.

Now, let’s create the “Engine” component in the project.

I can now create my first component inside this project. Go back to the main admin page at http://localhost/admin, and Add a new component.

Weblate admin page
Add a component by clicking on ‘Add’ at the bottom of the page http://localhost/admin (You must be logged in as admin).
Weblate Add component page
Creating a new component.

Here is what the fields means:

  • Component name: Set a name that represent which part of your application you are translating. it can be “User interface”, “Documentation page”, “Admin interface”, “Plugin whatever”, …

  • Url slug: Actually it is auto filling, so let it as is, or change it if you want another url than: http://localhost/projects/openhex/engine/.

  • Project: Place the component in the project you created before.

  • Version control system: Weblate handle other VCS, but my project is using git.

  • Source code repository: The git url where Weblate will clone and pull translation base files. Set your project Github url.

The URL beginning by git@github.com: is recommended because more secure and simpler, as soon as you added Github ssh key in the SSH keys admin page at http://localhost/admin/ssh. See Weblate documentation.

  • Repository push URL: Weblate allows to prevent push to your source repository, and instead let you push translation files manually. I prefered to do it through Weblate, so I set the same Git URL than Source code repository to enable the feature.

  • Repository browser: In the translation interface, Weblate displays a link to the source code where the translation string comes from. By filling this field, you can display a link to the source, but actually I didn’t see where the link is displayed… For a Github project, put https://github.com/YOUR_NAME/YOUR_REPO/blob/%(branch)s/%(file)s#L%(line)s

Let’s configure translation files:

Weblate Add component page
Creating a new component.
  • Repository branch: The branch of your project where translations will be fetch and push. I set here my main branch, which is master, but it could be develop.

I guess you could set here a release branch to translate only before release a version of your application, or another branch to prevent to merge on your development branch…

  • File mask: Maybe the most important field (with Source code repository), represents where your translation files are located. Weblate will parse files matching this glob. I set src/engine/locales/*.json, but I guess it could be locales/**/*.json if your lang are sorted by folders, or lang/messages-*.po

  • File format: Weblate can detect it automatically, but I didn’t tested the automatic detection. I picked my translation file format. I was surprised by how many formats Weblate handles…

We configured all required fields, and let others by default. You can submit. Weblate will immediately pull repository and parse files. If you are running Weblate with docker-compose up, you should see in logs in terminal when it is cloning the repository.

You should now see your project on the translation interface at http://localhost/projects. Open it, open your component, and you should be able to start translating !

Weblate screenshot
Weblate component page.

If you are seeing that your application is translated at 100% but in fact is not, it is because you probably configured i18next to pre-fill translated string with either a string like __STRING_NOT_TRANSLATED__ or the key. You must disable that, let an empty string for new strings, and fallback your translations to English language.

Weblate screenshot
Let’s translate my application in French !

Well, I let you discover the Weblate interface and features.

For example I enabled GitHub login by creating a GitHub application, I filled GitHub tokens in environment file at the root folder of Weblate Docker:

WEBLATE_SOCIAL_AUTH_GITHUB_KEY=537...............11
WEBLATE_SOCIAL_AUTH_GITHUB_SECRET=7aa..................................b61

Restarting docker-compose, and I can now authenticate with my GitHub account.

Licence Creative Commons

Julien Maulny

]]>
Julien Maulny
Sandstone explained to NodeJS, Python or PHP users2017-10-05T00:00:00+00:002017-10-05T00:00:00+00:00https://alcalyn.github.io/sandstone-explained-nodejs-python-php-users

Sandstone is a microframework that allows you to mount a Rest API, with also a real-time behaviour, thanks to its integrated websocket server.

I’m also describing it from scratch in What is Sandstone.

But I’ll try, in this post, to explain the benefit of Sandstone, assuming you already use a microframework in any programming language.

Rest API Microframeworks

If you’re a NodeJS, Python or PHP user, you may already know, respectively, expressJS, Flask, or Silex.

These microframeworks have a similar logic, and you can quickly bootstrap a Rest API in a minimalist way.

For example, here is how to create a POST route in every of these frameworks:

  • NodeJS
app = express()

app.post('/api/articles', function (request, response) {
    response.status(201).send('Article created')
})
  • Flask Python
@app.route('/api/articles', methods=['POST'])
def post_article():
    return 'Article created', 201
  • Silex PHP
<?php
$app = new Silex\Application();

$app->post('/api/articles', function () {
    return new Response('Article created', 201);
});

They are called microframework because they are designed to easily add routes, and may contain a whole Rest API in a single file.

Now, let’s talk about real-time Rest API.

You said a real-time Rest API?

Rest API means only request and response, so no real-time. Nonetheless, you often need real-time, even when you think you don’t!

Why may I need real time?

Let’s say that you have a blog Rest API where you can POST new articles by using the POST /api/articles route. In an other side, you want to display on a web interface a push notification once a new article has just been published.

A push notification is generally an asynchronous notification sent by the server to clients (web, mobile…), in order to notify them from an event that just happened.

You can use Ajax, and doing GET /api/articles every second, then check if a new article has been published. But this is a wrong pattern as every web client will ajax your server continuously, and may result as many useless requests per second. Useless because most of the response will be the same as the last one: no new article.

When to use a websocket connection

Generally, when you start to request every second your Rest Api to check whether state has changed, it’s time to reverse the connection direction: your server must send messages to client instead.

This is what websockets are designed for: keeping a bidirectionnal connection open so that both server and client can send messages to each other.

Separate the websocket connection into multiple topics

Topics are like “channels” for your websocket connection. Instead of sending a message through the single websocket connection from server to every websockets clients, you can create as many topics as you want. The websocket server has to send messages to a specific topic, and websocket clients subscribe only to topics they want to receive messages.

Topics allows to send messages to clients only if they subscribed to. You can define different callbacks for each topics, as messages formats should be different between topics.

Example using AutobahnJS 0.8, a WAMP/Websocket Javascript client:

session.subscribe('article_created', function (topic, event) {
    console.log('New Article:', event.article.title);
});

session.subscribe('chat', function (topic, event) {
    console.log('chat message:', event.message);
});

The WAMP protocol overlays the websocket protocol to add specifications on using topics in a websocket connection.

Let’s create my real time application!

Creating and running a websocket server is not as straitforward as mounting a Rest API, which is nowadays something trivial in many languages.

Sending push notifications is also tricky when they are triggered when someone requests the Rest API: it’s impossible to broadcast a websocket message from the Rest API stack as it is a different processus.

How Sandstone can help me to do that?

The main purpose of Sandstone is abstracting these two points:

  • declare a topic the same way as declaring a Rest API route,
  • uses the Symfony event dispatcher to send a push notification.

Declare topics like a Rest API route

Now imagine you can also declare a websocket topic as easy as declaring a route (here in a JavaScript-like language):

app.topic('/chat/{channel}', function (request, response) {
    return new function (channel) {
        this.onSubscribe = function () {
            this.send('Welcome on '+channel);
        }

        this.onPublish = function (message) {
            this.broadcast(message);
        }
    }
})

This way, web clients can subscribe to chat/general or chat/news topics to receive messages sent by other web clients.

Then, by running your application, it also run a websocket server you can connect to.

This is the first purpose of Sandstone. It is designed to declare websocket topics the same way as declaring a route and using route arguments:

<?php

$app->topic('chat/general', function ($topicPattern) {
    return new ChatTopic($topicPattern);
});

See an example of ChatTopic class here.

Sending push notification

Now, someone posts a new resource through the Rest API. Then it calls the controller, which persists the resource to the database, and returns a response.

Something trivial you can do with a microframework, illustrated by examples in the beginning of this article in many languages.

Let’s notify web clients that a new resource has been created. That means:

  • my controller send this resource to the websocket processus,
  • websocket server receive the resource and broadcast it to the right topic.

But your controllers shouldn’t depend to any websocket processus, or to a service that serialize a message to websocket server through a socket…

The second purpose of Sandstone is abstracting this workflow.

In fact, Sandstone uses the application event dispatcher. You can listen events from websocket server that your controller dispatched. It looks like that:

  • Rest API, controller:
<?php

$app->post('api/articles', function () use ($app) {
    $event = new ArticleEvent();

    $event->title = 'Unicorns spotted in Alaska';

    // Dispatch an event on article creation
    $app['dispatcher']->dispatch('article.created', $event);

    return new Response([], 201);
});
  • Websocket server, topic instance:
<?php

class MyWebsocketTopic extends Eole\Sandstone\Websocket\Topic implements EventSubscriberInterface
{
    public static function getSubscribedEvents()
    {
        return [
            'article.created' => 'onArticleCreated',
        ];
    }

    public function onArticleCreated(ArticleEvent $event)
    {
        // Broadcast message on this topic when an article has been created.
        $this->broadcast([
            'message' => 'An article has just been published: '.$event->title,
        ]);
    }
}

Get started

Well, that was the two main features of Sandstone, abstracting the creation of a websocket server, topics, and push notifications from Rest API.

Building a real time application with PHP is then possible. I’m showing in Creating a poker planning application with PHP and websockets how to create a simple one with Sandstone.

If you want to get started:

Licence Creative Commons

Julien Maulny

]]>
Julien Maulny
Contrôlez votre Raspberry Pi depuis votre téléphone pour la rendre plus portable2017-09-09T00:00:00+00:002017-09-09T00:00:00+00:00https://alcalyn.github.io/raspberry-connectee-telephone

Vous utilisez déjà SSH pour contrôler votre Raspberry en ligne de commande.

Pour cela, vous la connectez en Ethernet directement à votre PC, ou à distance en la connectant en wi-fi à votre box/router pour y accéder en réseau local.

Mais si vous vous déplacez avec votre Raspberry Pi car :

  • vous l’avez branchée à une batterie et vous voulez y accéder en voiture
  • vous l’avez embarquée dans un objet portable que vous emmenez hors de chez vous

vous n’aurez plus le réseau local de votre box, et vous n’allez pas garder votre PC avec vous pour la brancher.

J’ai récemment embarqué ma Raspberry dans un objet physique et voulu le transporter ailleurs que chez moi, en dehors de mon wi-fi.

Je vais expliquer dans cet article comment j’ai pu accéder à la Raspberry en SSH dans cette condition.

Il vous faudra :

  • un bon riz
  • votre Raspberry
  • un téléphone qui fait point d’accès wi-fi
  • une application sur votre téléphone qui fait terminal, par exemple ConnectBot.

Le point d’accès wi-fi

Actuellement, beaucoup de téléphones font point d’accès Wifi, ou “hot-spot”. Habituellement, on se sert du point d’accès pour diffuser un accès à Internet en l’utilisant en même temps que l’accès aux données mobiles.

En activant votre point d’accès wi-fi, votre téléphone devient une sorte de routeur, et permet à d’autres téléphones de s’y connecter en wi-fi, comme à une box, en connaissant le nom du réseau wi-fi, le SSID, et l’éventuel mot de passe.

Lorsqu’un téléphone se connecte à votre point d’accès, votre téléphone va lui attribuer une adresse IP locale, exactement comme le ferait votre box. Le téléphone peut donc rejoindre le réseau local créé par votre téléphone hôte.

Configurez votre point d’accès

Vous devez d’abord connaître ou définir le SSID du point d’accès wi-fi de votre téléphone. Avec un système Android, il faut aller dans les paramètres, saisir un nom (ou garder celui déjà en place), et configurer la sécurité, WPA ou aucun.

Capture d'écran Android point d'accès wi-fi Capture d'écran Android point d'accès wi-fi Capture d'écran Android point d'accès wi-fi Configuration point d'accès wi-fi
Manipulation pour activer son point d’accès wi-fi sur Android

Je n’ai pas mis de mot de passe car j’en ai pas besoin : je n’activerai le point d’accès que ponctuellement.

Vous connaissez maintenant votre SSID et votre éventuel mot de passe.

Configurez maintenant votre Raspberry.

Configurez votre Raspberry Pi

Vous avez donc mis votre téléphone en mode point d’accès wi-fi.

Ici, ce n’est pas pour offrir un accès Internet à d’autres personnes, mais pour que votre Raspberry s’y connecte. Elle sera donc dans le réseau local de votre téléphone, aura une adresse IP, et vous pourrez y accéder depuis votre téléphone.

Le but sera de faire en sorte que votre Raspberry Pi s’y connecte toute seule, et savoir quelle adresse IP votre téléphone lui a attribué pour s’y connecter en SSH avec l’application qui fait terminal.

Vous avez peut-être déjà fait cette manipulation quand vous avez configuré votre Raspberry pour qu’elle se connecte en wi-fi à votre box.

Il suffit, dans votre Raspberry, d’aller dans le fichier /etc/wpa_supplicant/wpa_supplicant.conf, et d’ajouter le SSID de votre téléphone :

sudo nano /etc/wpa_supplicant/wpa_supplicant.conf

Ajoutez votre téléphone avec le bon SSID dans le fichier :

network={
	ssid="JUJU"
	proto=RSN
	key_mgmt=NONE
}

Si vous avez déjà un autre network, par exemple celui de votre box, c’est pas grave. Ajoutez votre téléphone en dessous, et il faut ajouter un id_str pour différencier les deux network. Cet id_str est nécessaire lorsqu’il y en a plusieurs.

Voilà mon wpa_supplicant.conf complet :

country=GB
ctrl_interface=DIR=/var/run/wpa_supplicant GROUP=netdev
update_config=1

network={
	ssid="MaBox"
	psk="xxxxxxxx"
	id_str="maison"
}

network={
	ssid="JUJU"
	proto=RSN
	key_mgmt=NONE
	id_str="telephone"
}

Ça devrait être bon. Maintenant :

  • démarrez votre point d’accès wi-fi sur votre téléphone.
  • faîtes en sorte que votre Raspberry ne se connecte pas au wi-fi de votre box à la place de votre téléphone.
  • redémarrez votre Raspberry Pi.

Pour empêcher votre Raspberry de se connecter à votre box, plusieurs options :

  • soit vous coupez le wi-fi de votre box (plus de wi-fi chez vous)
  • soit, dans le fichier wpa_supplicant.conf de votre Raspberry, vous renommez le SSID de votre box en le suffixant par exemple avec _old (il faura le remettre pour avoir à nouveau Internet sur la Raspberry)
  • soit vous sortez de votre couverture wi-fi (ah, là, il faudra marcher)

Attendez qu’elle se démarre et qu’elle s’y connecte. Vous devrez voir votre Raspberry se connecter à votre point d’accès dans le menu :

Capture écran point d'accès wi-fi Android
La Raspberry vient de se connecter à notre point d’accès wi-fi

Connectez vous en SSH

Votre téléphone a dû attribuer une adresse IP à la Raspberry. Vous en avez besoin pour s’y connecter. Il est possible de la connaître en listant les périphériques connectés :

La Raspberry Pi s'est connectée à notre téléphone Liste des clients connéctés à notre point d'accès wi-fi Capture d'écran Android point d'accès wi-fi
Connaître l’address IP attribué à la Raspberry par le téléphone

Ensuite, il n’y a plus qu’à s’y connecter avec l’application terminal (ici ConnectBot), en saisissant l’adresse IP que j’ai récupéré et le nom d’utilisateur SSH (par défaut pi). Je saisis donc pi@192.168.43.113 dans le champ utilisateur@hôte port, je laisse les autres champs par défaut et je valide.

La Raspberry nous demande ensuite le mot de passe SSH, par défaut raspberry si vous ne l’avez pas changé.

Capture d'écran Android ConnectBot Raspberry Pi Capture d'écran Android ConnectBot Raspberry Pi Capture d'écran Android ConnectBot Raspberry Pi
Connexion SSH à la Raspberry Pi avec ConnectBot

Voilà, vous avez pris la main sur votre Raspberry Pi depuis votre téléphone.

Aller plus loin

On se sert dans cet article de la connexion wi-fi pour juste se connecter en SSH, mais on pourrait imaginer que la Raspberry héberge une application serveur, par exemple un serveur web, et la faire marcher avec le téléphone avec un simple navigateur web, ou une application qui se connecte à la Raspberry.

En embarquant la Raspberry dans un objet, on pourrait contrôler cet objet depuis notre téléphone, lui envoyer des ordres, récupérer des données des capteurs de la Raspberry.

Et si la Raspberry fournissait le point d’accès

Au lieu de configurer votre Raspberry pour qu’elle se connecte à votre téléphone, elle pourrait elle-même fournir le point d’accès. Je n’ai pas réussi à le faire, mais en suivant cet article, Créer un hotspot Wi-Fi en moins de 10 minutes avec la Raspberry Pi, on pourrait se connecter à son objet connecté depuis son téléphone, plutôt que l’inverse.

Cela serait plus simple d’un point de vue utilisateur, et vous pourriez même avoir un SSID du nom de votre objet connecté.

Un premier pas vers la domotique…

Licence Creative Commons

Julien Maulny

]]>
Julien Maulny
Contrôler un robot à deux roues avec Raspberry Pi2017-09-01T00:00:00+00:002017-09-01T00:00:00+00:00https://alcalyn.github.io/control-robot-two-enginesDébutant en électronique, dans le cadre d’un projet, je viens juste de réaliser un prototype d’un petit robot qui peut avancer, reculer, et tourner.

Cet article est fait pour ceux qui partent de zéro, qui souhaitent contrôler un ou deux moteurs afin de réaliser ce genre de robot, et comprendre comment ça marche !

Le moteur électrique

Pour avoir un mouvement de rotation, le moteur, mis sous tension, va transformer un courant électrique en mouvement de rotation.

Le sens du courant joue un rôle : selon le sens du courant, le moteur tourne dans un sens ou dans l’autre.

Le moteur va donc tourner si on lui applique une tension à ses bornes, mais ca marche aussi dans l’autre sens : si on fait tourner le moteur, cela va générer une tension à ses bornes.

Faites un arrêt rapide de votre moteur

Et donc, admettons qu’on court-circuite le moteur en reliant les bornes. En faisant tourner le moteur à la main, le moteur va génerer une tension. Mais vu que les bornes sont reliées, elles vont chacune recevoir de l’autre un courant opposé. Cela va freiner le mouvement.

Vous pouvez faire cette expérience en reliant les bornes de votre moteur, le faire tourner à la main. Vous constaterez que c’est plus difficile que quand les bornes ne sont pas reliées.

Le servomoteur

Il existe un type de moteur que l’on appelle un “servomoteur”. Ce type de moteur n’est pas plus intelligent (lol). Il tourne moins vite mais délivre un couple (une poussée) supérieur.

On s’en sert donc pour des tâches comme ouvrir une porte : on a besoin de faire qu’un tiers de tour, mais la porte est lourde, donc le moteur doit avoir un couple conséquent. Il y a aussi parfois des butées dans ce type de moteur lui évitant de tourner plus que nécessaire, mais on peut enlever cette butée sans problème pour faire avancer notre robot sur la distance.

On a vu

  • Le moteur transforme un courant en rotation
  • Il peut aussi transformer une rotation en courant
  • Si on court-circuite les bornes du moteur, ça le freine
  • Le sens du courant joue sur le sens de la rotation du moteur
  • Le servomoteur tourne moins vite mais a plus de couple

Le pont en H

C’est un montage assez commun et que vous retrouverez sûrement dés qu’il faut pouvoir faire tourner un moteur dans les deux sens.

Ce montage réutilise tous les principes vu dans le chapitre précédent, notamment le fait qu’on puisse inverser le sens du moteur en changeant le sens du courant, et le freiner en le court-circuitant.

Par exemple pour faire avancer ou reculer un robot, il faudra inverser le courant, mais vous ne pourrez pas démonter le robot et inverser le branchement bornes pendant qu’il roule !

Le pont en H permet justement de contrôler un moteur sans changer le circuit.

Schéma du pont en H
Schéma du pont en H

A l’aide des interrupteurs, on peut faire passer le courant dans le moteur d’un sens ou dans l’autre en basculant deux interrupteurs.

Pont H animé
Changer le sens de rotation du moteur avec le pont H

Servez vous du pont H pour freiner votre robot

Avec le même montage, on peut également relier les deux bornes à la masse en fermant les deux interrupteurs du bas. Cela qui va court-circuiter le moteur et le freiner. C’est utile pour faire en sorte que le robot s’arrête dés qu’on coupe le courant et éviter qu’il continu avec son inertie.

Pont H court-circuit
Freiner le moteur avec le pont H

Attention tout de même à ne pas fermer les mauvais interrupteurs et causer un court-circuit !

On a vu

  • Le pont en H est un montage commun pour contrôler un moteur
  • Il permet d’inverser facilement le sens du moteur
  • Il permet de freiner le moteur

Le circuit intégré L293D

Nous avons vu que le pont H permet de contrôler un moteur avec des interrupteurs. Mais nous n’allons pas courir derrière notre robot pour le contrôler en appuyant sur des interrupteurs !

Le circuit intégré L293D contient deux ponts H, et permet de contrôler jusqu’à deux moteurs bidirectionnels indépendamment sans se préoccuper de basculer les interrupteurs.

Circuit intégré L293D
Circuit intégré L293D

Ce circuit intégré permet également d’alimenter les moteurs par une autre source d’alimentation que la Raspberry Pi.

Ne branchez pas les moteurs sur la Raspberry Pi

En effet, si vous connectez le moteur sur les pins de la Raspberry Pi, il va consommer tellement de courant qu’il mettra la Raspberry à plat, et elle se redémarrera toutes les secondes. De plus, votre moteur a peut-être besoin de plus de 5V pour fonctionner. Vous devrez donc à la fois alimenter le circuit logique de la L293D avec les 5V de la Raspberry, et le circuit dédié aux moteurs avec des piles.

Il y a donc deux circuits différents avec deux tensions différentes, Vcc1 pour le circuit logique (par exemple 5V) et Vcc2 pour les moteurs (par exemple 9V), que nous devrons fournir sur deux pattes différentes. Nous verrons le montage plus tard.

On peux voir dans sa documentation (http://www.ti.com/lit/ds/symlink/l293.pdf) :

schéma L293D
Schéma complet du circuit L293D

Le schéma ci-dessus provient de la documentation du circuit. On peut y distinguer sur le schéma exhaustif les deux ponts H (si, en tournant la tête ça fait H).

Mais on n’a pas besoin de tout connaître de ce circuit, et on va se contenter du schéma simplifié fourni également dans la doc :

schéma L293D
Schéma simplifié du circuit L293D

Dans ce schéma simplifié, on voit à gauche les entrées logique (les A et EN). Elles seront connectés à des pins GPIO de la Raspberry et permettent de contrôler les moteurs. Les connexions de droite (les Y) sont les sorties du circuit. On y branchera les bornes des deux moteurs. Ils auront la tension fournie par les piles, Vcc2, en fonction des tensions logiques (Vcc1) qu’on met en entrée.

Les triangles sont des portes logiques ET. Ce qui veux dire qu’il y aura de la tension dans 1Y seulement si il y a une tension logique haute dans 1,2EN et 1A.

Pour savoir quelles pattes du circuit intégré correspondent à quelles entrées/sorties, nous pouvons nous fier à ce schéma qui provient de la documentation :

Schéma L293D
Les entrées et sorties de la L293D

L’encoche sur le circuit intégré permet de connaître le sens.

Nous pouvons donc relier un moteur au circuit intégré en branchant ses bornes aux sorties 1Y et 2Y.

Schéma de montage pour contrôler un moteur avec Raspberry Pi et le circuit intégré L293D
Schéma de montage pour contrôler un moteur avec Raspberry Pi et le circuit intégré L293D

Note sur la planche expérimentale : si vous ne connaissez pas encore, cette planche permet de prototyper des montages facilement sans rien souder. Les trous sont reliés d’une façon qu’il est simple de relier des composants et faire des dérivations. Les lignes sont reliées entre elles, et les deux colonnes des deux côtés sont également reliées. Cet article détail encore plus son utilisation : https://learn.sparkfun.com/tutorials/how-to-use-a-breadboard

Ensuite, pour faire tourner le moteur, on doit activer l’entrée 1,2EN (“enable 1 and 2”), et soit mettre une tension haute dans 1A et une tension basse dans 2A, soit l’inverse. Cela fera tourner le moteur dans un sens ou dans l’autre : cela dépend dans quel sens le moteur est branché sur les sorties 1Y et 2Y du circuit.

Si nous mettons la même tension logique dans 1A et 2A (haut/haut ou bas/bas), il n’y aura pas de tension aux bornes du moteur, il sera donc en roue libre. Or nous avons vu que le pont H permet de court-circuiter les bornes du moteur pour lui faire faire un arrêt rapide. Ceci est prévu par la L293D, il faut mettre une tension basse dans 1,2EN.

Pour résumer le contrôle d’un moteur :

Action 1,2EN 1A 2A  
Tourne d’un côté haut haut bas  
Tourne de l’autre haut bas haut  
Arrêt rapide bas - - peut importe les entrées en 1A et 2A
Rotation libre haut bas bas ou alors haut/haut

Attention au problème de surchauffe !

Notez que le circuit logique de la L293D doit être alimenté par les 5V de la Raspberry Pi, mais que le courant provenant des piles et destiné aux moteurs traverse aussi le circuit L293D. Cela va faire que le circuit peut dissiper beaucoup d’énergie pour sa pauvre petite taille, et le faire chauffer. Ce n’est pas un problème si les moteurs ne tournent pas longtemps et en continu, en revanche, il faudra penser à équiper la L293D de son radiateur si il est prévu de faire tourner les moteurs à temps plein.

L293D radiateur
L293D avec un radiateur

Contrôlez un deuxième moteur

Pour contrôler deux moteurs, il suffit d’en brancher un deuxième sur les sorties 3Y et 4Y du circuit, et d’appliquer la même logique sur les entrées 3A, 4A et 3,4EN.

Nous allons faire ce montage dans la dernière partie.

On a vu

  • On ne peut pas brancher un moteur directement sur la Raspberry Pi
  • Le circuit intégré L293D contient deux ponts H
  • Cela permet de contrôler deux moteurs en mettant sous tension certaines entrées du circuit
  • Comment contrôler un moteur bidirectionnel avec ce circuit
  • Il permet aussi d’alimenter les moteurs avec une autre source d’alimentation

C’est parti pour le montage de notre robot

Le matériel

  • Le châssis

Nous avons acheté un châssis de voiture pour 15€. Il comprend les deux roues et moteurs, boîtier de piles, et tout pour monter le châssis.

Chassis robot
Chassis de robot à trois roues
  • L’alimentation

Nous avons vu qu’il y a deux tensions différentes. Une pour les piles et moteurs (peut aller de 4V à plus de 30V), l’autre pour les circuits logiques (souvent 5V ou 3.3V). Il faut donc une batterie pour le circuit logique de la Raspberry Pi et de la L293D. Notez qu’il existe un battery hat concu pour la Raspberry et qu’on peut monter facilement dessus. On peut la voir sur l’image ci-dessous, en dessous de la Raspberry Pi :

Raspberry Pi battery hat
Raspberry Pi avec un battery hat
  • Le contrôleur des moteurs

Nous avons vu plus haut que le circuit L293D permet de contrôler deux moteurs. Dans le cadre de notre projet, nous avons opté pour un contrôleur plus complet qui contient un circuit similaire, le L298N.

L298N
Motor driver L298N

Mais ce contrôleur contient surtout :

  • de quoi brancher les fils du boîtier de piles avec des vis
  • des pins mâle pour brancher les entrées logiques du L298N à la Raspberry
  • le radiateur, utile si vous souhaitez utiliser les moteurs longuement
  • de protéger le circuit avec des diodes et condensateurs empêchant le reflux de courant des moteurs vers le circuit intégré (lorsque les moteurs génèrent du courant en tournant encore)

Nous avons donc choisi ce modèle car il est plus robuste, plus simple à brancher, et permet de se passer de la breadboard. Vous en trouverez facilement pour moins de 5€ avec les mots clés “motor driver controller L298”.

Réalisez votre montage

Je vais réaliser le montage suivant sur breadboard qui permet de contrôler deux moteurs avec Raspberry Pi et le circuit L293D.

Pour le faire avec le contrôleur L298N, voir plus bas.

Schéma montage pour contrôler deux moteurs avec Raspberry Pi et la L293D
Schéma de montage pour contrôler deux moteurs avec Raspberry Pi et la L293D

Étapes et explications :

  • Tout doit être hors de tension

Il est recommandé de faire le montage hors tension. Pour cela, débranchez la Raspberry et retirer au moins une pile du boîtier.

  • Je place la L293D au milieu

Il est commun de placer les circuits de cette façon : dans ce sens, les pins ne seront pas reliés à leurs voisins d’en face par la planche. On peut ensuite connecter les pins avec les trous de la même ligne.

  • J’alimente les deux lignes d’alimentation de la planche en reliant le rouge à 5V et bleu à la terre.

Utiliser les lignes d’alimentation va permettre de n’utiliser que deux pins de la Raspberry (5V et terre). Je pourrais n’utiliser qu’une ligne, mais en utiliser deux va permettre de faire moins survoler les fils d’un côté à l’autre.

  • J’alimente le circuit logique de la L293D

Les circuits logiques ont souvent besoin d’être sous tension (3.3V ou 5V, mais ici 5V) pour fonctionner. Ici, c’est le pin Vcc1 et les 4 pins GROUND au milieu. Les pins sont expliqués dans la documentation : http://www.ti.com/lit/ds/symlink/l293.pdf

Schéma L293D
Les entrées et sorties de la L293D
  • Connectez les sorties des 2 ponts H sur les deux moteurs

Les 4 pins de sortie, 1Y, 2Y, 3Y et 4Y sont autours des pins terre. Connectez le premier moteur à 1Y et 2Y, et l’autre à 3Y, 4Y. Le sens de rotation du moteur va dépendre du sens de branchement. Le plus simple est de brancher au hasard, vous pourrez de toutes façons ajuster lors de la programmation.

  • Connectez les entrées des 2 ponts H sur la Raspberry Pi

La Raspberry Pi va contrôler les ponts H, je connecte donc 1,2EN, 1A et 2A sur 3 pins GPIO pour le premier pont H. Ensuite je fais de même de l’autre côté, pour 3,4EN, 3A et 4A. J’utilise donc 6 pins GPIO pour contrôler les moteurs.

  • Branchez l’alimentation des moteurs

Il faut alimenter le circuit dédié aux moteurs sur Vcc2. Il faut aussi relier le pôle négatif des piles à la ligne terre pour que les deux circuits partagent la même masse.

Et avec le contrôleur L298N

On peut remplacer la L293D par ce contrôleur, et ainsi se passer de la breadboard. Le montage est le même, les entrées et sorties sont les suivantes :

L298N
Entrées et sorties du contrôleur L298N par rapport à la L293D
  • 1 : Les entrées logiques. IN1 et IN2 contrôlent le premier moteur, IN3 et IN4 le deuxième. Il faudra connecter ces 4 entrées à la Raspberry Pi.

Notez aussi ENA et ENB. Ce sont des entrées logiques optionnelles. Elles correspondent à 1,2EN et 3,4EN, qui doivent être sous tension logique haute pour pouvoir utiliser les moteurs, mais qu’on peux aussi mettre sous tension basse pour faire faire un arrêt rapide au moteur en le freinant.

Par défaut, il y a un jumper dessus qui les connectent à une tension logique haute. Donc par défaut, les moteurs sont toujours activés. Mais vous pouvez décider de retirer les jumpers et brancher les pins ENA et ENB à la Raspberry.

  • 2 : Les sorties qui correspondent à 1Y, 2Y, 3Y et 4Y. Il faut les connecter aux bornes des moteurs.

  • 3 : La mise sous tension. Il faut connecter aux 3 vis :

    • +5V qui correspond à Vcc1, il faut y mettre le 5V de la Raspberry
    • +12V qui correspond à Vcc2, il faut y brancher le pôle positif des piles (peut importe si ca ne fait pas 12V)
    • GND, la masse commune, il faut y mettre à la fois le 0V de la Raspberry et le pôle négatif des piles.
  • 4 : Le circuit intégré L298N et son radiateur.

  • 5 : Le régulateur

L’utilisation du régulateur est optionnelle. Il peut servir dans un cas : si votre alimentation externe pour les moteurs (ici les piles) fournit entre 5V et 12V, vous pouvez vous en servir pour alimenter également le circuit logique de la L298N.

Dans ce cas, il faut mettre le jumper dessus (par défaut il y est déjà), et vous économisez le fil 5V entre la Raspberry et le contrôleur.

Sinon, si votre alimentation externe fournit moins de 5V, ce sera pas suffisant, et si elle fournit plus de 12V, le régulateur risque de griller.

Dans tous les cas, étant donné que le régulateur n’a pas un rendement parfait, il est toujours mieux de ne pas s’en servir, de retirer le jumper et de connecter la vis 5V à un pin 5V de la Raspberry. Cela évitera de la perte lorsque le régulateur transformera 9V ou 12V en 5V : le reste est juste dissipé.

L298N RaspbberyPi motors
Schéma de montage avec le contrôleur L298N

Procédez au smoke test

Une fois les piles insérées et la Raspberry Pi sous tension, procédez au smoke test. Si il y a une erreur de branchement, il est possible que le circuit intégré se mette à chauffer. Dans ce cas il sera vite chaud au bout de quelques dizaine de secondes. Surveillez-le pendant les deux premières minutes après la mise sous tension.

Anecdote : j’ai grillée une L293D en la confondant avec un autre circuit très ressemblant, la SN74HC595N. En fait le circuit L293D était déjà sur la breadboard, et j’ai fait le montage en pensant que c’était la SN74HC595N. C’est environ deux minutes plus tard après la mise sous tension que j’ai commencé à sentir une odeur de plastique fondu. Le circuit intégré était tellement brulant qu’il a fait fondre la breadboard.

Donc oui, la L293D peut, mal connectée, causer des dégâts.

C’est parti pour la programmation

On va maintenant pouvoir activer les pins de la Raspberry avec un peu de programmation. Le but sera de faire avancer le robot et le faire tourner.

Créez un fichier sur la Raspberry, par exemple my-script.py, et codez le mouvement :

from time import sleep
import RPi.GPIO as GPIO

# Modifiez pour mettre les pins sur lesquels sont branchés les entrées de la L293D
MOTOR1_EN = 14
MOTOR1_A = 18
MOTOR1_B = 15

MOTOR2_EN = 25
MOTOR2_A = 8
MOTOR2_B = 7

try:

    # Configure les pins
    GPIO.setmode(GPIO.BCM)

    GPIO.setup(MOTOR1_EN, GPIO.OUT)
    GPIO.setup(MOTOR1_A, GPIO.OUT)
    GPIO.setup(MOTOR1_B, GPIO.OUT)

    GPIO.setup(MOTOR2_EN, GPIO.OUT)
    GPIO.setup(MOTOR2_A, GPIO.OUT)
    GPIO.setup(MOTOR2_B, GPIO.OUT)

    # AVANCE

    # Fais avancer le robot en faisant tourner les deux moteurs du même sens
    GPIO.output(MOTOR1_EN, GPIO.HIGH)
    GPIO.output(MOTOR1_A, GPIO.HIGH)
    GPIO.output(MOTOR1_B, GPIO.LOW)

    GPIO.output(MOTOR2_EN, GPIO.HIGH)
    GPIO.output(MOTOR2_A, GPIO.HIGH)
    GPIO.output(MOTOR2_B, GPIO.LOW)

    # Continu d'avancer pendant une seconde
    sleep(1)

    # Stoppe et freine les moteurs pendant une seconde
    GPIO.output(MOTOR1_EN, GPIO.LOW)
    GPIO.output(MOTOR2_EN, GPIO.LOW)
    sleep(1)

    # TOURNE A GAUCHE

    # Fais tourner le robot à gauche  en faisant tourner les deux moteurs à sens opposé
    GPIO.output(MOTOR1_EN, GPIO.HIGH)
    GPIO.output(MOTOR1_A, GPIO.LOW)
    GPIO.output(MOTOR1_B, GPIO.HIGH)

    GPIO.output(MOTOR2_EN, GPIO.HIGH)
    GPIO.output(MOTOR2_A, GPIO.HIGH)
    GPIO.output(MOTOR2_B, GPIO.LOW)

    sleep(0.5)

    # Stoppe et freine les moteurs pendant une seconde
    GPIO.output(MOTOR1_EN, GPIO.LOW)
    GPIO.output(MOTOR2_EN, GPIO.LOW)

    # On stoppe après une seconde
    sleep(1)

    GPIO.output(MOTOR1_EN, GPIO.LOW)
    GPIO.output(MOTOR2_EN, GPIO.LOW)

except KeyboardInterrupt:
    pass
except:
    GPIO.cleanup()
    raise

GPIO.cleanup()

Vous aurez probablement besoin d’inverser les numéros des pins A et B dans le code source en fonction de comment vous aurez branché les moteurs et les entrées du contrôleur.

Lancez la bête !

Lancez en ligne de commande sur la Raspberry :

python my-script.py

Et pour changer la vitesse des moteurs

Pour l’instant, on a mis seulement GPIO.HIGH ou GPIO.LOW en entrée, ce qui permet de soit faire tourner le moteur à 100%, soit l’arrêter complètement.

Pour faire tourner un moteur à par exemple 75%, on peut mettre une tension discontinue en entrée des pins EN. Cela veut dire que la Raspberry va activer le pin EN pendant 75% du temps, et le désactiver pendant 25%. En faisant ça très rapidement, le moteur semblera tourner en continu, mais moins vite.

On peut pour cela utiliser l’objet GPIO.PWM, qui va s’occuper du cycle de travail.

Le mode PWM permet de faire fonctionner un composant moins fort. Étant donné qu’on ne peut pas faire ca en envoyant moins de Volts, on le fait en alternant entre tension haute et tension basse rapidement. On peut faire ca sur la Raspberry en utilisant le mode PWM : https://fr.wikipedia.org/wiki/Modulation_de_largeur_d%27impulsion

En suivant la documentation de GPIO python, on peut par exemple faire sur le pin 14 :

import RPi.GPIO as GPIO

# Déclarer notre pin en mode sortie
GPIO.setmode(GPIO.BCM)
GPIO.setup(14, GPIO.OUT)

# Utiliser GPIO.PWM sur notre pin avec une fréquence de 100hz (100 cycles de travail par seconde)
pinGPIO = GPIO.PWM(14, 100)

# Démarrer les cycles de travail à 80%, donc le pin est activé 8ms, puis désactivé 2ms.
pinGPIO.start(80)

On peut donc faire tourner un des deux moteurs moins vite pour faire avancer le robot en le faisant tourner légèrement, ou faire tourner les deux moteurs moins vite pour que le robot avance moins vite en ligne droite :

from time import sleep
import RPi.GPIO as GPIO

# Modifiez pour mettre les pins sur lesquels sont branchés les entrées de la L293D
MOTOR1_EN = 14
MOTOR1_A = 18
MOTOR1_B = 15

MOTOR2_EN = 25
MOTOR2_A = 8
MOTOR2_B = 7

try:

    # Configure les pins
    GPIO.setmode(GPIO.BCM)

    GPIO.setup(MOTOR1_EN, GPIO.OUT)
    GPIO.setup(MOTOR1_A, GPIO.OUT)
    GPIO.setup(MOTOR1_B, GPIO.OUT)

    GPIO.setup(MOTOR2_EN, GPIO.OUT)
    GPIO.setup(MOTOR2_A, GPIO.OUT)
    GPIO.setup(MOTOR2_B, GPIO.OUT)

    motor1GPIO = GPIO.PWM(MOTOR1_EN, 100)
    motor2GPIO = GPIO.PWM(MOTOR2_EN, 100)

    # AVANCE

    # Fais avancer le robot lentement (50%) en ligne droite
    motor1GPIO.start(50)
    GPIO.output(MOTOR1_A, GPIO.HIGH)
    GPIO.output(MOTOR1_B, GPIO.LOW)

    motor2GPIO.start(50)
    GPIO.output(MOTOR2_A, GPIO.HIGH)
    GPIO.output(MOTOR2_B, GPIO.LOW)

    # Continu d'avancer pendant une seconde et demi
    sleep(1.5)

    # RECULE EN TOURNANT

    # Fais reculer le robot en dessinant une courbe
    motor1GPIO.start(100)
    GPIO.output(MOTOR1_A, GPIO.LOW)
    GPIO.output(MOTOR1_B, GPIO.HIGH)

    motor2GPIO.start(60)
    GPIO.output(MOTOR2_A, GPIO.LOW)
    GPIO.output(MOTOR2_B, GPIO.HIGH)

    # Continu de reculer pendant 2 secondes
    sleep(2)

    # Arrêt des moteurs en arrêtant les cycles de travail
    motor1GPIO.stop()
    motor2GPIO.stop()


except KeyboardInterrupt:
    pass
except:
    GPIO.cleanup()
    raise

GPIO.cleanup()

Licence Creative Commons

Julien Maulny

]]>
Julien Maulny
Creating a poker planning application with PHP and websockets2017-02-13T00:00:00+00:002017-02-13T00:00:00+00:00https://alcalyn.github.io/poker-planning-php-websockets
Poker planning application screenshot
Poker planning application screenshot

When searching in Github for real-time poker planning application, there is already multiple repositories using websockets on Github:

Written in Javascript (and nodejs in many cases), JAVA, Scala, C#…

The one written in PHP I can find on the first Github search page uses ajax requests every seconds Toxantron/scrumonline:

Firebug network tab showing ajax polling
Firebug network tab showing ajax polling

But does it means that PHP is not able to use websockets and make a real-time application ?

I often hear that PHP is not good for websockets, that it’s better to use nodejs blablabla

In fact, if I want to build a real-time application, I don’t care about using PHP or nodejs, or using websockets or something like long-polling…

But if you are already in a PHP eco-system, or if you love PHP, you may want to build your real-time application using PHP.

I’m not saying that PHP is a secondary choice, “because I don’t have the choice”, but this article will proove that it is possible to build a real time application using websockets over PHP.

I also do it in a structured code, and not only a websocket-server.php file you can find in all the “PHP chat with websockets” tutorials on the web.

Let’s build a real-time application

I’ll do it with Sandstone. This library extends Silex, so if you know Silex, you know already the half part of this tuto (the RestApi part).

Sandstone also adds websockets support, and provide some abstraction tools to create websocket topics the more simple way, or the more “Silex way”.

What are the application needs

My need is to build a planning poker application, where we can start a poker session, let teammates join it, and let them vote.

So, in a first part, I will build a RestApi with resources:

  • /teams: create a team, get a list of existing teams, join a team
  • /users: create an user with a pseudo, make an user vote (1, 2, 3, 5, 8, …).

Then, I will build the second part of the application, the real time vote using websockets.

I mean by real time vote the fact by once an user votes, all others user interfaces will be updated instantly: the user send the vote information to the server, then the server broadcast the vote information to others users in the team (no Ajax request every seconds).

Technical stack

  • Server:
    • Sandstone:
      • RestApi: create/join rooms
      • Websocket server: be notified when someone join our team, vote, or team vote is finished to refresh the view in real-time.
  • Client:
    • Bootstrap 4 + jQuery + Js app which uses api

I’ll use Docker and docker-compose, so that you don’t have to install PHP, ZMQ and ZMQ PHP extension.

Note: The application I’ll build in this article is available on Github: alcalyn/poker-planning

Part I: Rest Api with Silex

If you know Silex, you already know this part. We use Silex to build a light RestApi which handle the /teams and /users resources.

Silex is a microframework which allows to mount a web application easily, with a light router, a light service container (Pimple)…

Install Sandstone

Let’s bootstrap a Silex application, but not from scratch.

I will use here the Sandstone edition.

Sandstone edition is a Silex skeleton with websockets, Doctrine, JMS Serializer, web profiler…

Using Docker, following the documentation:

curl -L https://github.com/eole-io/sandstone-edition/archive/dev.tar.gz | tar xz
cd sandstone-edition-dev/

make

Docker helps us to mount our application web server, websocket server, … so we don’t have to install all that stuff.

Let’s check that it’s well installed by going to the diagnostic page: http://0.0.0.0:8480/hello/world.html.

If there is still orange or red boxes, you may just need to chmod -R 777 var/* or chown -R USER:GROUP .

I also have an access to the Symfony web profiler here: http://0.0.0.0:8480/index-dev.php/_profiler/.

The Sandstone edition structure

The edition has in fact 2 stacks:

  • The RestApi stack (app/RestApiApplication),
  • The websocket stack (app/WebsocketApplication).

These 2 stacks extend a common stack (app/Application).

By this way, we do not load RestApi controllers in the websocket server, and we do not load websocket topics in RestApi stack.

Application, the common stack, contains:

  • services
  • Doctrine mappings
  • serializer metadata

RestApiApplication, the RestApi stack, contains:

  • controllers
  • converters
  • events to forward

WebsocketApplication, the Websocket stack, contains:

  • websocket topics

Create database schema and entities with Doctrine

The basic scenario is:

The user enters his pseudo, then see the list of the teams. He selects a team, then see the users and can vote.

Poker planning DCM
Poker planning DCM

So let’s map User and Team entities with Doctrine, in yaml format:

src/App/Resources/doctrine/App.Entity.User.dcm.yml:

App\Entity\User:
    type: entity
    repositoryClass: App\Repository\UserRepository
    id:
        id:
            type: integer
            generator:
                strategy: AUTO
    fields:
        pseudo:
            type: string
        vote:
            type: smallint
            nullable: true
    manyToOne:
        team:
            targetEntity: Team
            reversedBy: users

src/App/Resources/doctrine/App.Entity.Team.dcm.yml:

App\Entity\Team:
    type: entity
    repositoryClass: App\Repository\TeamRepository
    id:
        id:
            type: integer
            generator:
                strategy: AUTO
    fields:
        title:
            type: string
        voteInProgress:
            type: boolean
    oneToMany:
        users:
            targetEntity: User
            mappedBy: team

Note: I don’t want to generate php model classes manually, so using yaml mapping allows me to generate them automatically.

Let’s tell Doctrine to use our yaml files (replace the 'annotation' one):

src/App/HelloProvider.php:

<?php

        $app->extend('doctrine.mappings', function ($mappings, $app) {
            $mappings []= [
                'type' => 'yml',
                'namespace' => 'App\\Entity',
                'path' => $app['project.root'].'/src/App/Resources/doctrine',
                'alias' => 'App',
            ];

            return $mappings;
        });

Then auto generate entities, and update the database schema:

# Entering in php container
make bash

# Generate php model classes
bin/console orm:generate-entities src/

# Update database schema
bin/console orm:schema-tool:update --force

You should see the generated entities in src/App/Entity/, and the tables in your phpmyadmin interface http://0.0.0.0:8481/.

Create Rest controllers

We now want a lot of routes such as:

  • POST /users route when a new user comes and enter his pseudo,
  • GET /teams for the teams list,
  • PUT /teams/{team}/users/{user} when an user joins a team,
  • POST /users/{user}/vote to allow a player vote…

Team controller

I won’t expose all controllers, but just the POST /users/{user}/vote one as an example.

Note: The whole application is available on Github, so check on Github src/App/Controller to see all others controllers.

Let’s implement the POST /users/{user}/vote route:

src/App/Controller/UserController.php

<?php

namespace App\Controller;

use Pimple\Container;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
use Symfony\Component\HttpKernel\Exception\ConflictHttpException;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Alcalyn\SerializableApiResponse\ApiResponse;
use DDesrosiers\SilexAnnotations\Annotations as SLX;
use App\Entity\User;
use App\Event\UserEvent;

/**
 * @SLX\Controller(prefix="/api")
 */
class UserController
{
    /**
     * @var Container
     */
    private $container;

    /**
     * @param Container $container
     */
    public function __construct(Container $container)
    {
        $this->container = $container;
    }

    /**
     * Vote. Vote number must be in body, and number in Fibonacci sequence.
     *
     * @SLX\Route(
     *      @SLX\Request(method="POST", uri="/users/{user}/vote"),
     *      @SLX\Convert(variable="user", callback="app.converter.user:convert")
     * )
     *
     * @param Request $request
     * @param User $user
     *
     * @throws BadRequestHttpException If vote is not in Fibonacci sequence.
     * @throws ConflictHttpException When trying to vote once the team has finished voting.
     *
     * @return ApiResponse
     */
    public function postVote(Request $request, User $user)
    {
        $pokerPlanning = $this->container['app.poker_planning'];
        $vote = intval($request->getContent());
        $team = $user->getTeam();

        if (!$pokerPlanning->isVoteFibonacci($vote)) {
            throw new BadRequestHttpException('Vote must be in Fibonacci sequence.');
        }

        if (!$team->getVoteInProgress()) {
            throw new ConflictHttpException('Cannot vote now, votes are closed.');
        }

        $user->setVote($vote);

        if ($pokerPlanning->hasTeamVoted($team)) {
            $team->setVoteInProgress(false);
        }

        $this->container['orm.em']->persist($user);
        $this->container['orm.em']->flush();

        return new ApiResponse(null, Response::HTTP_NO_CONTENT);
    }
}

Tiny controllers, thanks to converters

I don’t want to do a ->getRepository('Team')->findById($id) in my controllers.

So I can use converters (see Silex converters).

For example, src/App/Converter/TeamConverter.php:

<?php

namespace App\Converter;

use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use App\Repository\TeamRepository;

class TeamConverter
{
    /**
     * @var TeamRepository
     */
    private $teamRepository;

    /**
     * @param TeamRepository $teamRepository
     */
    public function __construct(TeamRepository $teamRepository)
    {
        $this->teamRepository = $teamRepository;
    }

    public function convert($id)
    {
        $team = $this->teamRepository->find($id);

        if (null === $team) {
            throw new NotFoundHttpException('Team not found.');
        }

        return $team;
    }
}

Then I magically get my instances in my controllers from arguments when I do:

<?php

    /**
     * Make an user joins a team.
     *
     * @SLX\Route(
     *      @SLX\Request(method="PUT", uri="/teams/{team}/users/{user}"),
     *      @SLX\Convert(variable="team", callback="app.converter.team:convert"),
     *      @SLX\Convert(variable="user", callback="app.converter.user:convert")
     * )
     *
     * @param Team $team
     * @param User $user
     *
     * @return ApiResponse
     */
    public function addUser(Team $team, User $user)
    {
        $team->addUser($user);
        $user->setTeam($team);

        $this->container['orm.em']->persist($team);
        $this->container['orm.em']->flush();

        return new ApiResponse($team, Response::HTTP_OK);
    }

where app.converter.team:convert and app.converter.user:convert are callbacks to the convert method in TeamConverter and UserConverter.

Sure, converters need to be registered first as a service in RestApi stack:

src/App/HelloRestApiProvider.php

<?php

use App\Converter\TeamConverter;
use App\Converter\UserConverter;

// ...

        $app['app.converter.team'] = function () use ($app) {
            return new TeamConverter($app['orm.em']->getRepository('App:Team'));
        };

        $app['app.converter.user'] = function () use ($app) {
            return new UserConverter($app['orm.em']->getRepository('App:User'));
        };

Move some logic from controller to service

As you know, business logic should be in services.

The service app.poker_planning contains some logic relative to Poker Planning voting.

src/App/Service/PokerPlanning.php:

<?php

namespace App\Service;

use App\Entity\Team;

class PokerPlanning
{
    /**
     * Check is a vote is in Fibonacci sequence.
     *
     * @param int $vote
     *
     * @return boolean
     */
    public function isVoteFibonacci($vote)
    {
        return in_array($vote, [1, 2, 3, 5, 8, 13, 21, 34]);
    }

    /**
     * Check whether all users voted.
     *
     * @param Team $team
     *
     * @return boolean
     */
    public function hasTeamVoted(Team $team)
    {
        foreach ($team->getUsers() as $user) {
            if (null === $user->getVote()) {
                return false;
            }
        }

        return true;
    }
}

Let’s register the service in common stack:

In src/App/HelloProvider.php:

<?php

use App\Service\PokerPlanning;

// ...

        $app['app.poker_planning'] = function () {
            return new PokerPlanning();
        };

Check your API by retriving all teams: http://0.0.0.0:8480/index-dev.php/api/teams

I also provide a Postman collection, so that you can play pre-saved requests. The collection is available here: poker-planning-postman-collection.json.

Part II: Real-time stuff with websocket

Well, we now have a working RestApi where we can retrieve teams, and post users votes.

Now, when someone votes, I want to keep updated all web clients and display that an user voted in real-time.

But I don’t want to request my RestApi every second to check if state has changed.

Display user vote in real time

The need: I want the RestApi notifies me when an user votes and when all users voted so I can reveal votes as soon as it is available.

How we will achieve that ?

The logic is:

  1. Someone votes, then calls PUT /users/1/vote.
  2. The RestApi handles the request,
  3. persist to user vote,
  4. then sends the 204 response to the client which has sent the vote.

But I also want that the RestApi notifies all my teammates that I voted.

The websocket solution:

All users should connect to a websocket, which is a connection that stays open so that both client and server can use it to send messages.

Then the RestApi will notify through this websocket that I voted.

Sandstone uses the WAMP protocol. So instead of connect to the websocket and listen all messages through an unique channel, we can create multiple channels, or “topics”, and subscribe to a topic.

Users will then receive messages from this topic only.

I want to create a topic for each team, teams/1, teams/2, …

And I will dispatch messages relative to the team on the team channel.

Then, every user who joins a team must also subscribes to the team channel in order to receive real-time notifications (someone joined my team, someone voted).

Creating the websocket topic

Once you understand the logic, let’s implement it with Sandstone.

A Eole\Sandstone\Websocket\Topic class is responsible of the logic behind a websocket topic.

It implements for example the method onSubscribe, called when an user subscribes to this topic.

Use case: Send a message to new subscribing users, for example last messages sent.

It also implements the onPublish method, when an user send a message to this topic.

Use case: For chats: broadcast back the message to every users who subscribed to this topic.

It also provides a broadcast method to broadcast a message to every subscribing users.

First create the topic class src/App/Topic/TeamTopic.php:

<?php

namespace App\Topic;

use Eole\Sandstone\Websocket\Topic;
use App\Event\UserEvent;

class TeamTopic extends Topic
{
    /**
     * @var int
     */
    private $teamId;

    /**
     * @param string $topicPattern
     * @param int $teamId
     */
    public function __construct($topicPattern, $teamId)
    {
        parent::__construct($topicPattern);

        $this->teamId = $teamId;
    }
}

And register the topic in websocket stack, in src/App/HelloWebsocketProvider.php:

<?php

namespace App;

use Pimple\ServiceProviderInterface;
use Pimple\Container;
use App\Topic\TeamTopic;

class HelloWebsocketProvider implements ServiceProviderInterface
{
    /**
     * {@InheritDoc}
     */
    public function register(Container $app)
    {
        $app
            ->topic('teams/{teamId}', function ($topicPattern, $arguments) {
                $teamId = intval($arguments['teamId']);

                return new TeamTopic($topicPattern, $teamId);
            })
            ->assert('teamId', '\d+')
        ;
    }
}

Web client in my team #1 can now subscribe to teams/1, but nothing will happens for now.

Forward vote event to websocket topic

I want my topic to listen to the UserEvent::VOTED, which should be dispatched from the RestApi controller when someone voted.

But this event is dispatched in the RestApi stack, whereas my topic is in the websocket stack, this is to different processus.

Sandstone allows to dispatch event from RestApi processus to the websocket processus by forwarding it.

So we will need to create an event class, in src/App/Event/UserEvent.php:

<?php

namespace App\Event;

use Symfony\Component\EventDispatcher\Event;
use App\Entity\User;

class UserEvent extends Event
{
    /**
     * An user voted or changed his vote.
     * The listener receives an instance of UserEvent.
     *
     * @var string
     */
    const VOTED = 'event.user.voted';

    /**
     * @var User
     */
    private $user;

    /**
     * @param User $user
     */
    public function __construct(User $user)
    {
        $this->user = $user;
    }

    /**
     * @return User
     */
    public function getUser()
    {
        return $this->user;
    }
}

Then, disptach the event from the controller, in src/App/Controller/UserController.php:

<?php

namespace App\Controller;

use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Alcalyn\SerializableApiResponse\ApiResponse;
use App\Entity\User;
use App\Event\UserEvent;

class UserController
{
    public function postVote(Request $request, User $user)
    {
        // ...

        // Upate the user vote
        $user->setVote($vote);

        // Persist the user
        $this->container['orm.em']->persist($user);
        $this->container['orm.em']->flush();

        // + Add this line, dispatch an event with the user entity
        $this->container['dispatcher']->dispatch(UserEvent::VOTED, new UserEvent($user));

        // Return the RestApi response
        return new ApiResponse(null, Response::HTTP_NO_CONTENT);
    }
}

Nothing new here, just dispatching an event using Symfony EventDispatcher.

Now, we want to listen this event from the Topic class. To make the event dispatch from RestApi processus to Websocket one, Sandstone allows us to forward it:

src/App/HelloRestApiProvider.php:

<?php

namespace App;

use Pimple\ServiceProviderInterface;
use Pimple\Container;
use App\Event\UserEvent;

class HelloRestApiProvider implements ServiceProviderInterface
{
    /**
     * {@InheritDoc}
     */
    public function register(Container $app)
    {
        // ...

        $app->forwardEventsToPushServer([
            UserEvent::VOTED,
        ]);
    }
}

Then magically listen it in the TeamTopic class:

By magically I mean that the event instance is serialized, sent to the websocket server thread through a Push server (ZeroMQ), then deserialized and redispatched in the websocket server application event dispatcher.

By this way you’re able to magically listen an event from another PHP thread.

<?php

namespace App\Topic;

use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Eole\Sandstone\Websocket\Topic;
use App\Event\UserEvent;

class TeamTopic extends Topic implements EventSubscriberInterface
{
    /**
     * {@InheritDoc}
     */
    public static function getSubscribedEvents()
    {
        return [
            UserEvent::VOTED => 'onUserVoted',
        ];
    }

    /**
     * @param UserEvent $event
     */
    public function onUserVoted(UserEvent $event)
    {
        // Check if the event is relative to this team
        if ($event->getUser()->getTeam()->getId() !== $this->teamId) {
            return;
        }

        // Broadcast to every websocket clients
        $this->broadcast([
            'type' => 'user_voted',
            'user' => $event->getUser(),
        ]);
    }
}

Note: Don’t forget to restart the websocket server when the source code changed with:

make restart_websocket_server

Connect to websocket server from your Javascript application

You’ll need to test this with a websocket Javascript client.

A JS library that can be used with Sandstone (WAMPv1/Websocket protocol) is AutobahnJS 0.8.

Note: Another one could be wamp1, which can be loaded with npm, but I don’t tested it yet.

To be short, it will be something like:

console.log('connecting to websocket server...');

ab.connect(
    'ws://0.0.0.0:8482',
    function (session) {
        console.log('connected to websocket server.');

        console.log('listening team topic');

        session.subscribe('teams/'+myTeam.id, function (topic, event) {
            console.log('something happens in my team');
            console.log(event.user.pseudo+' votes '+event.user.vote);
        });
    },
    function (code, reason, detail) {
        console.warn(code, reason, detail);
    }
);

The full front and back application can be installed and tested here:

Poker Planning

Licence Creative Commons

Julien Maulny

]]>
Julien Maulny
Initialisation d’un projet open source PHP2016-11-09T00:00:00+00:002016-11-09T00:00:00+00:00https://alcalyn.github.io/initialiser-projet-open-source-php

Libérer un projet existant ou en créer un nouveau de manière collaborative est une solution de plus en plus évidente. En effet, c’est une bonne méthode pour partager votre solution, et donc partager aussi les problèmes, et la citation “Un problème partagé est un problème à moitié résolu.” prend alors du sens.

Mais c’est aussi un ensemble de méthodes et de normes qui peuvent être nouvelles et peu évidentes pour une personne qui souhaiterait se lancer dans le mouvement.

Dans cet article, qui part de la naissance d’un projet ou d’une librairie PHP, jusqu’à la mise en place d’outils d’intégration continus pour faciliter les contributions extérieures, je vais détailler toutes les étapes et les questions que vous devez vous poser.

Naissance de votre projet

Parlez de l’idée, confrontez la, soyez à l’écoute des premiers feedbacks

Bien souvent, on aime avoir une idée pour soi, la développer et la faire connaître sous notre nom. Si vous souhaitez rendre votre projet open source et recevoir des contributions, il va falloir accepter l’idée que d’autres vont “salir” votre beau code source, encore vierge de tout bug.

“Adding a Bundle means that you are ok with all members messing with it”

— Second rule of Friends of Symfony. https://friendsofsymfony.github.io

Et ne gardez pas l’idée pour vous, partagez là ! Est-ce que d’autres vont utiliser votre projet, ou le trouver inutile ? Le fait d’en parler va vous permettre d’entendre les feedbacks et de vous challenger sur votre propre idée.

Faites une page de présentation en ligne

Avant même de commencer tout développement, créez une page de présentation du projet, ou une page d’exemple d’utilisation de la librairie.

En plus de montrer aux utilisateurs l’utilité du projet, cela mettra vos idées au clair et permettra aux contributeurs de partir dans la même direction.

Permettez et organisez également les commentaires et les échanges en utilisant les issues de Github, ou un outil externe comme Gitter (https://gitter.im) qui permet un chat en temps réel entre les contributeurs et les utilisateurs.

Création du dépôt

Créez un dépôt ouvert dès le début

Le dépôt est là où la version la plus à jour du code source de votre projet sera sauvegardée. C’est souvent un gestionnaire de versions (git, svn, …) installé sur un serveur en ligne (Github, Bitbucket, …) pour permettre à tous de récupérer et modifier le code source.

Un piège à éviter est de créer un dépôt fermé, interdit au public, et de se dire qu’on l’ouvrira plus tard, quand le projet sera “présentable”, “prêt”, ou encore “terminé”. En réalité, un projet n’est jamais terminé, et contiendra toujours de la dette technique : des bugs, du code à simplifier… Donc partir dans cette optique condamne le dépôt à être toujours fermé.

Alors ouvrez le tout de suite, faîtes qu’il soit toujours opérationnel, même s’il ne fait pas encore grand chose !

Le premier fichier de votre projet : Le Readme

Non, le premier fichier de votre projet PHP n’est pas un index.php, un .gitignore, ou un composer.json.

Si vous êtes utilisateur de Github, vous constaterez que le fichier Readme n’est plus le fichier ignoré d’antan qu’il y avait sur les CD-ROM. Github affiche le contenu de ce fichier sur la page d’accueil du projet, et c’est donc le premier fichier lu par une nouvelle personne qui arrive sur le projet.

Le fichier Readme doit contenir la description du projet, à qui il s’adresse, à quoi il sert… Personnellement, je me sers souvent d’un modèle d’elevator speech. C’est une description très courte d’un projet qui doit permettre à une tierce personne de comprendre l’utilité de celui-ci. L’idée est née aux USA, et le principe est de pouvoir expliquer son projet à un futur client pendant le temps de trajet de l’ascenseur.

Choisissez une licence open source

La licence indique si vous autorisez que votre projet soit réutilisé, modifié par d’autres personnes et redistribué…

Notez avant tout que si vous ne mettez pas de licence à votre projet, il sera par défaut privé, et interdit de toute réutilisation.

Les licences open source sont compliquées de part leurs détails, mais on peut faire simple. Jusque là, je n’utilisais que 2 licences : MIT et GNU-GPL.

MIT est une licence open source très laxiste et permet de tout faire avec le code source : Le réutiliser, l’intégrer dans un projet fermé… C’est une licence utile pour une petite librairie qu’on souhaite mettre à disposition sans se préoccuper de l’usage fait ensuite.

GNU-GPL est une licence open source qui force les projets utilisants du code GNU-GPL à être eux aussi sous cette même licence. Cela la rend contagieuse, et vient du trait militant de la Free Software Foundation. J’utilise cette licence pour les projets dont je souhaite que tous ceux qui les réutilisent pour un usage public mettent aussi leur projet en open source.

Donc à moins d’avoir une exigence juridique très spécifique, utiliser ces deux licences reste un choix simple et rapide, et ce sont les licences les plus utilisées pour les projets sur Github selon ces statistiques : https://github.com/blog/1964-open-source-license-usage-on-github-com.

Vous pouvez tout de même découvrir toutes les autres licences open source sur opensource.org, et les licences Free Software sur fsf.org. Il en existe plus d’une centaine, et certaines seront peut-être plus adaptées à votre projet. Exemple avec la licence AGPL vs GPL : https://www.gnu.org/licenses/why-affero-gpl.fr.html.

Il existe aussi un site qui peut vous aider quant au choix de la licence : http://choosealicense.com/.

Vous créez une librairie PHP ? Ajoutez là sur Packagist !

Votre projet ou librairie va peut-être dépendre d’autres librairies, et pourrait aussi être une brique d’un autre projet.

Pour éviter d’installer les dépendances à la main, utilisez un gestionnaire de dépendances.

En PHP, vous aurez besoin de Composer pour lister les librairies que votre projet va utiliser, et Composer va se charger de les installer, ainsi que les dépendances des dépendances…

Vous pourrez aussi les mettre à jour et ainsi éviter d’avoir des dépendances obsolètes dans lesquelles des vulnérabilités auront été découvertes.

Si votre projet est une librairie destinée à être utilisée comme une brique dans d’autres projets PHP, ajoutez là à Packagist !

Packagist est le référentiel principal des librairies utilisant Composer.

La mettre sur Packagist va permettre aux utilisateurs de votre librairie de l’ajouter facilement en tant que dépendance de leur projet. De plus, cela va faciliter l’installation de celle-ci, un plus pour la prise en main.

Entamez le développement

Ne vous embarquez pas dans un début de projet interminable

Une bonne approche est d’utiliser le MVP (Minimum Viable Product). Cette méthode a été développée par Eric Ries, dans son livre The Lean Startup. L’objectif est de commencer par les fonctionnalités les plus importantes de votre projet, celles que les gens vont adopter rapidement. Et pour cela, il est nécessaire de savoir quelles sont-elles.

Pour cela, n’hésitez pas à très vite proposer des premières versions, et comprendre les feedbacks.

N’oubliez pas d’instaurer une norme de codage dès le début

Chaque personne a un style de code différent. Cela peut rendre difficile la lecture d’un code d’une autre personne, et compliquer les contributions en ayant du mal à se mettre d’accord sur une façon d’écrire un même code.

Trancher sur la façon d’écrire le code

Un des avantages des normes de codage est de normaliser le style de code. Les normes contiennent des règles, comme le nombre d’espaces utilisé pour l’indentation, le placement des accolades ouvrantes sur la même ligne ou sur une nouvelle ligne…

Ces règles vont fluidifier les revues de code en évitant de longues discussions sur la meilleure façon de coder.

Limiter le nombre d’erreurs d’inattention

Mais les normes de codage permettent plus. Elles permettent aussi de limiter les erreurs d’inattention. Un exemple, je veux tester si une variable vaut 5 :

if ($a = 5) {}

Notez que j’ai malencontreusement oublié un “=”.

Cela aura pour effet d’affecter 5 à la variable $a, et d’entrer dans le if. Cela ne lèvera pas d’erreur car la syntaxe est correcte, bien que j’ai fait une erreur de typo.

C’est une erreur difficile à débugger car silencieuse, et qu’on a tous fait au moins une fois.

Certaines normes de codage imposent de placer la valeur à gauche de la variable. On appelle cette règle la “condition Yoda” (https://fr.wikipedia.org/wiki/Condition_Yoda), je vous laisse trouver la référence ;)

Si j’avais suivi cette règle, j’aurai dû écrire :

if (5 = $a) {}

Ce qui aurait levé une erreur de syntaxe, et que j’aurai tout de suite corrigé en :

if (5 == $a) {}

Donc comme le montre cet exemple, un autre avantage des normes de codage est d’imposer un style d’écriture qui va mettre certaines erreurs d’inattention en évidence.

Et en PHP ?

En PHP, la norme la plus utilisée est celle des PHP standards, la norme PSR-2 (http://www.php-fig.org/psr/psr-2/).

Mettez en place les tests unitaire et les tests d’intégration

Chaque nouvelle fonctionnalité que vous ajoutez à votre application risque de causer des régressions, de casser d’autres fonctionnalités. Il faudrait donc, en plus de tester votre nouvelle fonctionnalité, tester aussi toutes les autres fonctionnalité déjà implémentées, c’est le fameux cycle dont on ne prononce pas le nom.

Cela devient très vite impossible car d’une, votre application va offrir de plus en plus de fonctionnalité, et de deux, un nouveau contributeur ne connaîtra pas toute votre application, et ne pourra pas vérifier s’il a cassé une partie du code développé par un autre contributeur.

C’est là que les tests automatisés interviennent. Ils vont appeler des fonctions très ciblées du code, ou simuler l’utilisation d’une fonctionnalité, et vérifier que le résultat obtenu est toujours celui attendu. On pourra lancer ces tests automatisés à chaque modification de code, et vérifier que notre nouvelle portion de code ne casse rien.

Les tests automatisés sont important dans un projet open source, car ils vont empêcher d’autres contributeurs de causer des régressions, et donc faciliter les contributions.

Plus de détails à propos des tests, et une liste d’outils PHP ici : http://www.hongkiat.com/blog/automated-php-test/.

Gardez à jour la documentation, elle doit être opérationnelle à tout moment !

À tout moment, de nouveau utilisateurs vont arriver sur votre projet. Ils vont vouloir le tester, ou installer la librairie. Si à ce moment là, votre documentation n’est pas à jour et l’utilisateur ne peut pas tester, il pensera que le projet ne marche pas.

C’est pour cela que la documentation d’installation doit toujours être opérationnelle. Pour l’utilisateur, si l’installation ne fonctionne pas, le projet ne fonctionne pas, si la documentation d’installation est trop compliquée, le projet est trop compliqué…

Gardez à jour une documentation d’installation et d’utilisation fonctionnelle, mais aussi claire et simple, c’est à dire le minimum d’étapes d’installation, et de paramètres farfelus.

De plus, cela pourra lever une alerte si vous voyez que votre documentation devient compliquée, c’est qu’il y a peut-être un problème de conception, ou quelque chose à simplifier dans le code même.

Si vous n’arrivez pas à simplifier votre documentation sur l’utilisation car votre librairie est elle-même compliquée, essayez de faire de la documentation first. Mettez-vous à la place de l’utilisateur, et écrivez la doc avec des exemples de code de la manière qui semble la plus évidente. Implémentez ensuite votre librairie en faisant en sorte que les exemples fonctionnent.

Accueillez vos contributeurs

Pensez au modèle de branches

Vous êtes adepte du tout-sur-master (ou tout-sur-trunk pour les anciens) ? Très bien, vous avez souvent eu l’occasion de constater un bug longtemps après qu’il ait été commité, ou alors commité par erreur une fonctionnalité pas encore finie.

Faire des branches, ou faire différentes version du projet, permet de développer une fonctionnalité et de la fusionner une fois testée, revue, et bien terminée.

Mais ne faites pas des branches au hasard, pensez à un modèle adapté qui correspond à votre manière de sortir des nouvelles versions de votre application.

Un modèle simple et assez connu est git flow, qui intègre la branche master, la version stable de votre projet, une branche développement, là ou les dernières fonctionnalités sont fusionnées, des branches feature pour les fonctionnalités importantes, et quelques autres branches que vous pourrez découvrir ici : https://www.occitech.fr/blog/2014/12/un-modele-de-branches-git-efficace/.

Si ce modèle ne permet pas d’avoir plusieurs versions stable du projet (il y a seulement master), et que vous souhaitez maintenir plusieurs versions comme le ferait par exemple Symfony avec 2.7, 2.8, 3.0, … C’est là qu’il faudra choisir un autre modèle avec les autres contributeurs, et bien communiquer le modèle de branches que vous aurez choisi.

Utilisez les Pull Request

Utiliser une branche pour créer une nouvelle fonctionnalité est une bonne pratique. Vous pourrez la fusionner une fois terminée.

Mais au lieu de fusionner votre branche tout de suite et d’imposer vos modifications, choisissez plutôt de proposer vos modifications en créant une pull request, surtout si ce n’est pas votre projet et que vous contribuez à un autre projet.

Un outil comme Github ou Gitlab liste les pull request, permet de les commenter, ou commenter une ligne précise du code, de mettre à jour la pull request, et de la fusionner en intégrant les feedbacks des autres contributeurs.

Cela permettra aux autres contributeurs d’effectuer une revue de code, voir ce que vous avez créé, récupérer votre code et l’exécuter sur sa propre machine.

Une autre bonne pratique veut qu’on ne fusionne jamais notre propre code, et de s’obliger à laisser au moins une autre personne le relire et le fusionner. C’est une pratique qui s’inspire de l’extreme programming, et qui est assez efficace si on utilise un outil comme Github ou Gitlab.

Mettez en avant les outils d’intégration continue

Vous avez maintenant initialisé votre projet, il est en ligne, il y a une documentation, une norme de codage, des tests unitaire et des tests d’intégration.

Il existe certains outils que vous allez pouvoir intégrer à l’environnement de votre projet. Les outils que je vais présenter ici sont gratuits et adaptés à des projets open source. Ils vont automatiser le lancement des tests unitaires et d’intégration, vérifier le respect de la norme de codage, ou inspecter la qualité du code et remonter quelques bugs potentiels.

Travis

Travis est un outil d’intégration très connu. Il est lié à Github, et permet de lancer un script, ou “job”, après un push sur une branche du projet, ou même lorsqu’une pull request est créée.

On peut donc lancer des tests unitaires, des tests d’intégration, ou même lancer un job qui vérifie que la norme de codage a été respectée.

Grâce à son intégration à Github, Travis envoi ensuite l’état du build (aucun test cassé, code style correct), et Github affiche directement dans la pull request une coche verte ou une croix rouge en fonction de l’état.

C’est donc un outil gratuit pour les projets open source qui va pouvoir vérifier à chaque contribution si le code modifié ne casse rien et si le projet peut toujours s’installer.

Scrutinizer

Scrutinizer est un outil de qualité continue. Il va inspecter le code source, évaluer sa complexité, détecter les bugs potentiels, duplications, erreurs de syntaxe… Et va en sortir une note sur 10, ainsi qu’une liste de recommandations qui va nous permettre d’améliorer cette note.

Cependant, ce n’est pour moi pas une bible, et la note est juste un repère. Cet outil va indiquer si une modification de code risque d’intégrer un bug ou contient des fonctions avec une complexité trop élevée.

Cet outil nous permet donc de suivre l’évolution de la qualité du code en connaissant l’impact de chaque PR sur la note, et si elle introduit des issues.

On peut même recevoir des mails si un commit fait chuter la note, qui est l’auteur de ce commit…

SensioLabsInsight

SensioLabs propose aussi un outil d’inspection de code, mais celui-ci est orienté Symfony. C’est à dire qu’il propose, en plus des recommandations PHP, des bonnes pratiques à suivre sur des projets Symfony, Silex, ou même sur un bundle Symfony.

Badges

Si vous utilisez des outils comme ceux cités ci-dessus, notez qu’ils proposent ensuite des badges. Ce sont des petites images dynamiques, liées au résultat du build ou de l’inspection, que vous allez pouvoir ajouter dans le Readme de votre projet. Les badges indiqueront donc en temps réel si le build du projet n’est pas cassé, si les tests passent toujours, la qualité du code…

Les badges montrent en plus que vous vous souciez de la qualité du code, et veillez que les contributions ne causent pas de régressions.

Différents badges sur le README d'un projet PHP.
Différents badges sur le README d’un projet PHP.

Résumé

Vous avez vu dans cet article qu’un projet open source n’est pas seulement un dépôt ouvert. C’est aussi maintenir le code, relire les propositions de code et feedbacks proposés par la communauté, avoir une documentation, un readme…

Il existe une organisation, The PHP League, qui a créé un squelette pour une librairie PHP. C’est une bonne base de projet qui contient plusieurs fichiers qu’on retrouve généralement dans un projet PHP. C’est accessible sur Github : thephpleague/skeleton.

Aller plus loin

Vous pouvez consulter un des articles de Richard Stallman, “Le logiciel libre est encore plus essentiel maintenant” : https://www.gnu.org/philosophy/free-software-even-more-important.html, et naviguer ensuite sur le site et la foire aux questions qui détaillent de nombreux concept du logiciel libre.

Pour voir comment ca se passe actuellement au sein d’un projet open source, vous pouvez voir la conférence de Jordi Boggiano, initiateur de composer et packagist, sur les coulisses d’un projet open source, “Behind the Scenes of Maintaining an Open Source Project” : https://www.youtube.com/watch?v=hRZhYSiSfaY

Licence Creative Commons

Julien Maulny

]]>
Julien Maulny