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 :
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.
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 :
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.
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.
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.
La partie qui nous a le plus plu, à part qu’il a d’abord fallu passer par une étape qu’on ne maitrisait pas…
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.
Cependant on ne savait pas vraiment dessiner, et c‘était plutôt frustant quand t‘as comme source d‘inspiration :
(Image : https://www.davidrevoy.com/article603/rainy-days)
Et que tout ce que tu peux faire, c‘est :
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 :
C’est donc ce personnage que je vais garder. J’ai bien organisé les calques, j’en ai un pour :
Je devrais séparer les calques pour ensuite réassembler le personnage en pantin, et permettre à chaque segment de bouger librement.
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 :
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 :
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.
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.
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.
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.
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 :
Ces trois fichiers seront importés par mon application Javascript avec DragonBonesJs.
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.
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 :
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 :
Ensuite, il faut dire quels os contrôlent quelles sommets.
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 :
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.
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…
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/.
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.
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é :
Julien Maulny
]]>
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.
I had two sources of strings in my source code:
throw new IllegalMoveError('You have not enough money to buy an unit.');
<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).
Using npm:
npm install i18next --save
Which installed version ^10.5.0.
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,
});
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.
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.
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.
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 !
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.
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.
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.
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”.
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.
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.
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.
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:
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 !
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.
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.
Julien Maulny
]]>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.
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:
app = express()
app.post('/api/articles', function (request, response) {
response.status(201).send('Article created')
})
@app.route('/api/articles', methods=['POST'])
def post_article():
return 'Article created', 201
<?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.
Rest API means only request and response, so no real-time. Nonetheless, you often need real-time, even when you think you don’t!
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.
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.
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.
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.
The main purpose of Sandstone is abstracting these two points:
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
ChatTopicclass here.
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:
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:
<?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);
});
<?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,
]);
}
}
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:
Julien Maulny
]]>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 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 :
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.
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.
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.
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 :
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.confde 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 :
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 :
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é.
Voilà, vous avez pris la main sur votre Raspberry Pi depuis votre téléphone.
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.
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…
Julien Maulny
]]>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 !
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.
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.
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.
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.
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.
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.
Attention tout de même à ne pas fermer les mauvais interrupteurs et causer un court-circuit !
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.
Ce circuit intégré permet également d’alimenter les moteurs par une autre source d’alimentation que 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) :
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 :
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 :
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.
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 |
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.
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.
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.
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 :
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.
Mais ce contrôleur contient surtout :
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”.
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.
Étapes et explications :
Il est recommandé de faire le montage hors tension. Pour cela, débranchez la Raspberry et retirer au moins une pile du boîtier.
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.
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.
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
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.
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.
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.
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 :
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é.
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.
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
AetBdans le code source en fonction de comment vous aurez branché les moteurs et les entrées du contrôleur.
Lancez en ligne de commande sur la Raspberry :
python my-script.py
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()
Julien Maulny
]]>
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:
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.
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”.
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).
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
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)…
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/*orchown -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 edition has in fact 2 stacks:
app/RestApiApplication),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:
RestApiApplication, the RestApi stack, contains:
WebsocketApplication, the Websocket stack, contains:
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.
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/.
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…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);
}
}
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'));
};
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.
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.
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:
PUT /users/1/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).
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.
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
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:
Julien Maulny
]]>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.
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.
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.
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 !
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.
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/.
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.
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.
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.
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.
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.
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/).
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/.
À 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.
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.
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.
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 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 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…
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.
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.
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.
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
Julien Maulny
]]>