feat: updated blog to be in english
Signed-off-by: Louis Vallat <contact@louis-vallat.fr>
Before Width: | Height: | Size: 11 KiB |
@ -1,260 +0,0 @@
|
||||
+++
|
||||
title = "Git Workflow"
|
||||
description = "Git est un outil de versionnage très utile, voici comment je m'en sers."
|
||||
date = 2021-05-10
|
||||
|
||||
[taxonomies]
|
||||
tags = ["git", "workflow"]
|
||||
+++
|
||||
|
||||
Git est un outil de versionnage très utile, voici comment je m'en sers.
|
||||
|
||||
<!-- more -->
|
||||
|
||||
> À noter que ce guide est destiné aux personnes qui ont déjà utilisé git auparavant,
|
||||
> il ne s'agit pas ici d'un tutoriel pour débutants (bien que je sois ici assez
|
||||
> exhaustif dans mes explications).
|
||||
|
||||
[TL;DR](#tldr)
|
||||
|
||||
## Les fondations
|
||||
|
||||
Vous êtes bien installé devant votre écran, les doigts ne demandent qu'à
|
||||
furieusement taper les touches de votre clavier, mais avant le top départ, il faut
|
||||
préparer le terrain ! Voici quelques commandes **primordiales** (pour ne pas dire
|
||||
**obligatoires**) à réaliser avant de se lancer dans un projet.
|
||||
|
||||
Premièrement, je n'aime pas la manière qu'a git de faire des commits de fusion
|
||||
lors d'un pull qui se passe mal (divergence entre votre version locale et distante
|
||||
par exemple, aka. vous avez committé et quelqu'un d'autre aussi en même temps).
|
||||
Nous allons donc demander à git de ne pull que lorsque l'historique est linéaire
|
||||
(fast-forward), et sinon de faire automatiquement un rebase :
|
||||
|
||||
```bash
|
||||
git config pull.rebase true
|
||||
```
|
||||
|
||||
En cas de divergence, un simple `git rebase origin/branche_concernée` vous sortira
|
||||
d'affaire !
|
||||
|
||||
Ensuite, il nous arrive de vouloir avoir une vue plus graphique de notre historique
|
||||
git. Voici un alias, à placer dans votre `~/.gitconfig`, qui vous permet d'afficher
|
||||
un graphique de vos branches et vos commits de manière plus visuelle que le `git log`
|
||||
de base de git (honteusement volés [ici](https://stackoverflow.com/a/9074343)) :
|
||||
|
||||
```conf
|
||||
[alias]
|
||||
lg1 = log --graph --abbrev-commit --decorate --format=format:'%C(bold blue)%h%C(reset) - %C(bold green)(%ar)%C(reset) %C(white)%s%C(reset) %C(dim white)- %an%C(reset)%C(bold yellow)%d%C(reset)' --all
|
||||
lg2 = log --graph --abbrev-commit --decorate --format=format:'%C(bold blue)%h%C(reset) - %C(bold cyan)%aD%C(reset) %C(bold green)(%ar)%C(reset)%C(bold yellow)%d%C(reset)%n'' %C(white)%s%C(reset) %C(dim white)- %an%C(reset)' --all
|
||||
lg = !"git lg1"
|
||||
```
|
||||
|
||||
Si vous n'avez pas besoin d'autant de décorations, ou que vous ne pouvez pas définir
|
||||
d'alias, cette commande fait quelque chose d'assez similaire :
|
||||
|
||||
```bash
|
||||
git log --all --decorate --oneline --graph
|
||||
```
|
||||
|
||||
Pour avoir des messages de commits unifiés et cohérents avec de jolis alias :
|
||||
|
||||
```conf
|
||||
fix = "!f() { git commit -sm \"fix: $1\"; }; f"
|
||||
feat = "!f() { git commit -sm \"feat: $1\"; }; f"
|
||||
misc = "!f() { git commit -sm \"misc: $1\"; }; f"
|
||||
wip = "!f() { git commit -sm \"wip: $1\"; }; f"
|
||||
```
|
||||
|
||||
## Des branches, des branches, et encore des branches
|
||||
|
||||
Quand on travaille à plusieurs sur git, il devient presque obligatoire d'avoir
|
||||
des branches pour éviter de se sentir "à l'étroit" dans son `repo`. Pour pouvoir
|
||||
collaborer et avancer sur un projet en groupe sans se marcher sur les pieds les
|
||||
uns des autres, les branches deviennent l'outil indispensable de git.
|
||||
|
||||
J'utilise les branches de la manière suivante :
|
||||
|
||||
- les branches ont une durée de vie **très courte**
|
||||
- une fois que la branche a rempli son rôle et qu'elle a été fusionnée, elle doit être supprimée
|
||||
- une branche par fonctionnalité ou par résolution de problème
|
||||
- les noms des branches sont libres *tant qu'ils sont clairs*
|
||||
- c'est mieux d'avoir comme nom la fonctionnalité développée ou son rôle
|
||||
- lorsqu'une branche sert à développer une résolution de bug ou un `fix`, il est mieux de l'inclure dans le nom
|
||||
- lorsque vous travaillez à plusieurs sur la même fonctionnalité, il est conseillé de
|
||||
faire une branche par personne en partant de la branche princpale de cette fonctionnalité
|
||||
|
||||
Évidemment, une branche fait exception : `master` (ou `main`, peu importe),
|
||||
qui est une version "propre", "stable", et qui doit présenter le projet dans un état
|
||||
fonctionnel, tel qu'il serait montré à un client ou à un responsable.
|
||||
|
||||
Il est possible d'avoir une seconde branche sur les projets plus importants, qui se
|
||||
nomme alors `develop` et qui est la version en développement actif d'un projet,
|
||||
mais qui ne peut pas être fusionnée sur la branche princpale pour des raisons de
|
||||
stabilité par exemple, ou qui n'est pas prête pour la mise en production.
|
||||
|
||||
Pour créer une branche fille à partir de la branche sur laquelle vous êtes :
|
||||
|
||||
```bash
|
||||
git checkout -b nom_de_la_nouvelle_branche
|
||||
```
|
||||
|
||||
Et pour basculer de branche en branche :
|
||||
|
||||
```bash
|
||||
git checkout nom_de_la_branche
|
||||
```
|
||||
|
||||
Si vous ne savez pas sur quelle branche vous vous trouvez, vous pouvez faire :
|
||||
|
||||
```bash
|
||||
git branch
|
||||
```
|
||||
|
||||
## Rebaser, c'est la clef
|
||||
|
||||
> Cette partie peut être ignorée si vous ne pouvez pas réaliser de `push force`
|
||||
> (ou équivalent). En effet, un `rebase` modifie fondamentalement l'historique de
|
||||
> votre git. Si vous n'êtes pas sûr de vous, n'en faites pas. Je ne suis pas
|
||||
> responsable si vous perdez des données.
|
||||
|
||||
Votre fonctionnalité est développée et testée, vous voulez la fusionner avec votre
|
||||
branche parente ? Vous voulez récupérer ce qui est sur votre branche parente et
|
||||
que vous aimeriez récupérer sur la votre ? Il est temps de rebase !
|
||||
|
||||
Lorsque l'on crée une branche, celle-ci a un `commit` de "départ", depuis lequel
|
||||
elle se *base* pour faire du versionnage en parallèle des autres branches. Parfois
|
||||
il est nécessaire de récupérer du travail qui a été mis sur la branche mère dans
|
||||
notre branche de travail, par exemple la résolution d'un bogue handicapant.
|
||||
|
||||
Un `rebase` va faire changer votre commit de départ et l'échanger avec un autre.
|
||||
Prenons le schéma suivant :
|
||||
|
||||
![Schéma d'un rebase](images/git_rebase.svg)
|
||||
|
||||
Nous avons ici la branche `new_feature` qui part d'un commit de master. Disons
|
||||
que vous êtes en train de développer une fonctionnalité importante, et qu'un bug
|
||||
vous ralentit dans l'exécution de vos tests, mais qu'il a été réglé après la création
|
||||
de votre branche, et qu'il est disponible sur `master`. Pour le récupérer sur votre
|
||||
branche, vous n'avez qu'à `rebase` votre branche sur la dernière version de `master`.
|
||||
La commande pour faire ceci serait alors :
|
||||
|
||||
```bash
|
||||
git rebase nom_de_branche_parente
|
||||
```
|
||||
|
||||
Il est important de noter que vous risquez d'avoir des conflits au cours de cette
|
||||
opération. En cas de conflit, vous pouvez les régler, puis faire `git add fichier_en_conflit`
|
||||
et enfin `git rebase --continue`. Cela aura pour effet de **réécrire** l'historique
|
||||
git de votre branche, d'où l'importance de faire un push en force, puisque votre
|
||||
version locale et la/les version(s) distante(s) n'auront plus rien à voir.
|
||||
|
||||
### Quel rapport avec ce git flow
|
||||
|
||||
Comme dit plus haut, les branches ont une existence précise et bien définie dans
|
||||
le temps. Une branche n'a pas a être fusionnée dans sa branche parente tant que
|
||||
son contenu n'est pas complet, ou au moins utilisable. Cela ne fait pas vraiment
|
||||
de sens de "polluer" une branche parente dans le seul but d'en récupérer le nouveau
|
||||
contenu. Les opérations de rebase sont donc ici fréquentes.
|
||||
|
||||
## Fusion non préparée = fusion refusée
|
||||
|
||||
Votre fonctionnalité ou correctif est terminé(e), votre branche est prête à être
|
||||
fusionnée ? En vérité, pas tout à fait. Il faut maintenant préparer la fusion !
|
||||
|
||||
Afin de faciliter au maximum la fusion de votre branche, il va falloir faire un
|
||||
dernier rebase. Le but de ce rebase est ici de vérifier que le rôle de la branche
|
||||
et du contenu qu'elle propose sont toujours fonctionnels et qu'aucun bogue n'a été
|
||||
intruduit par les nouveautés disponibles sur la branche parente. Cela va aussi
|
||||
vous permettre de gérer les conflits avec la branche mère au sein même de la votre.
|
||||
Une fusion n'introduira donc pas de bogue, et vous restez pleinement responsable
|
||||
de votre branche.
|
||||
|
||||
Enfin, vous n'êtes pas responsables de la fusion, un collègue ou responsable va
|
||||
d'abord vérifier votre code et le contenu que vous apportez et approuver (ou rejeter)
|
||||
la fusion que vous demandez. Grâce aux opérations de rebasage successifs que vous
|
||||
avez fait durant la durée de vie de votre branche, la fusion se fera sans encombre.
|
||||
|
||||
À noter que si vous avez des outils tels que les [Merge Requests](https://docs.gitlab.com/ee/user/project/merge_requests/getting_started.html)
|
||||
chez GitLab, ou [Pull Requests](https://docs.github.com/en/github/collaborating-with-issues-and-pull-requests/about-pull-requests)
|
||||
chez GitHub, n'hésitez surtout pas à les utiliser ! Ces outils permettront de garder
|
||||
un suivi des fusions, et d'expliquer son but à un responsable qui ne les a pas
|
||||
forcément toutes en tête.
|
||||
|
||||
Pour fusionner une branche dans celle sur laquelle vous vous trouvez :
|
||||
|
||||
```bash
|
||||
git merge nom_de_la_branche_à_fusionner_dans_celle_sur_laquelle_vous_êtes
|
||||
```
|
||||
|
||||
## Au revoir vieille branche
|
||||
|
||||
> Les opérations décrites ici peuvent entraîner une perte de code. Je ne suis pas
|
||||
> responsable si vous supprimez une branche avant qu'elle n'ait été fusionnée.
|
||||
> Ne supprimez une branche que si vous êtes sûr qu'elle n'a plus d'utilité.
|
||||
|
||||
Votre branche est désormais fusionnée ? C'est la fin pour elle, il est désormais
|
||||
temps de lui dire au revoir !
|
||||
|
||||
Commençons d'abord par la supprimer sur le repo distant :
|
||||
|
||||
```bash
|
||||
git push -d origin nom_de_branche
|
||||
```
|
||||
|
||||
Ici, le serveur distant s'appelle `origin`, comme c'est le cas la plupart du temps.
|
||||
|
||||
Vos collègues peuvent synchroniser leurs branches et faire disparaître leurs
|
||||
références locales à des branches fantômes distantes avec la commande `git fetch -p` (le
|
||||
`-p` étant ici pour "prune").
|
||||
|
||||
Enfin, il est temps de supprimer votre copie locale de cette branche :
|
||||
|
||||
```bash
|
||||
git branch -d nom_de_branche
|
||||
```
|
||||
|
||||
À noter que le `-d` ne supprimera votre branche que si elle a été fusionnée. Il est
|
||||
possible de forcer la suppression d'une branche locale non fusionnée avec l'option
|
||||
`-D` qui équivaut à un `git --delete --force` sur la branche.
|
||||
|
||||
"Don't cry because it's over, smile because it happened." - *Dr. Seuss*
|
||||
|
||||
## TL;DR {#tldr}
|
||||
|
||||
On commence par demander à git de ne pull que s'il n'y a pas de divergence avec
|
||||
la version distante de notre branche, et sinon, de rebase notre branche automatiquement :
|
||||
|
||||
```bash
|
||||
git config pull.rebase true
|
||||
```
|
||||
|
||||
Ensuite on utilise une branche par fonctionnalité et par correctif. Une branche a
|
||||
un rôle précis et une durée de vie très courte. Lorsque plusieurs développeurs
|
||||
travaillent sur la même branche, ils ne doivent pas exclure la possibilité de créer
|
||||
des branches personnelles.
|
||||
|
||||
`master` est en tous temps propre et présentable. Une branche intermédiaire peut
|
||||
être utilisée pour éviter de polluer inutilement la branche principale. Elle pourra
|
||||
s'appeler `develop` par exemple.
|
||||
|
||||
Les rebase sont fréquents et servent à récupérer sur une branche fille du nouveau
|
||||
contenu de sa branche mère.
|
||||
|
||||
La fusion se déroule sans accroc, un rebase doit être fait avant pour gérer les
|
||||
conflits sur la branches fille.
|
||||
|
||||
Une fois la branche fusionnée, elle doit être supprimée.
|
||||
|
||||
## Informations supplémentaires
|
||||
|
||||
### Vocabulaire
|
||||
|
||||
- `repo` : raccourci de `repository`, qui est l'environnement de travail de git,
|
||||
c'est là dedans que l'on créé ses branches, et que l'on versionne son travail.
|
||||
|
||||
### Pour aller plus loin
|
||||
|
||||
- Le fonctionnement des objets, commits et arbres sur git (en profondeur) : [vidéo](https://youtu.be/MyvyqdQ3OjI).
|
||||
- Le fonctionnement des branches sur git (en profondeur) : [vidéo](https://youtu.be/mhZQRBp8dXE).
|
||||
- Comment git stocke son arborescence de fichiers : [article](https://book.git-scm.com/book/en/v2/Git-Internals-Git-Objects).
|
||||
- Les commandes associées à `git branch` : [documentation](https://git-scm.com/docs/git-branch).
|
@ -1,286 +0,0 @@
|
||||
+++
|
||||
title = "Robot timide – le robot qui fuit la lumière"
|
||||
description = "Création d'un robot timide pour un projet scolaire."
|
||||
date = 2022-05-19
|
||||
|
||||
[taxonomies]
|
||||
tags = ["maker", "fablab", "robotique"]
|
||||
+++
|
||||
|
||||
Création d'un robot timide pour un projet scolaire.
|
||||
|
||||
> Article cloné depuis [Eirlab](https://www.eirlab.net/2022/05/19/robot-timide-le-robot-qui-fuit-la-lumiere/)
|
||||
|
||||
Dans le cadre de l’option Makers proposée en deuxième année à l’ENSEIRB-MATMECA,
|
||||
en filière Informatique, un robot timide a vu le jour au sein d’Eirlab.
|
||||
Cet article est un guide pour expliquer comment reproduire notre travail, dans
|
||||
le plus pur esprit Maker.
|
||||
|
||||
<!-- more -->
|
||||
|
||||
![Une vue de 3/4 face du robot finalisé](images/robot_timide_header.jpg)
|
||||
|
||||
## Concept de base
|
||||
|
||||
Nous voulions mettre en oeuvre un robot qui détecterait en temps réel la
|
||||
luminosité autour de lui et ce dans le but de se déplacer vers l’endroit le
|
||||
plus sombre de la pièce dans lequel il se trouve, et de s’y cacher. Un autre
|
||||
mode est aussi disponible, permettant non pas d’aller vers l’endroit le plus
|
||||
sombre, mais à l’inverse de fuir l’endroit le plus lumineux, pour avoir deux
|
||||
manières de voir le problème.
|
||||
|
||||
![Une vue de face du robot timide dans sa forme finale.](images/robot_timide_enseirb.jpg)
|
||||
|
||||
## Du matériel
|
||||
|
||||
Avant de pouvoir travailler sur une partie logicielle du projet, il faut
|
||||
évidemment avoir une base physique pour accueillir et exécuter ce code.
|
||||
|
||||
À noter que ce projet n’a nécessité aucune dépense et a été réalisé entièrement
|
||||
avec du matériel déjà disponible au sein du Fablab.
|
||||
|
||||
### Liste du matériel
|
||||
|
||||
Pour pouvoir reproduire ce projet, il vous faudra avoir en votre possession les
|
||||
éléments suivants :
|
||||
|
||||
- 1x Arduino Uno (ou clone équivalent) – c’est la tête pensante de ce projet
|
||||
- 1x Arduino Motor Shield – pour contrôler les deux moteurs du robot
|
||||
- 2x moteurs à courant continu, ici des FT DC 130D – pour se mouvoir dans l’espace
|
||||
- 3x capteurs de distance à ultrasons HC-SR04 – pour éviter de rencontrer des murs trop souvent
|
||||
- 4x photo résistances – une par côté, pour pouvoir détecter la luminosité autour de lui
|
||||
- 4x résistances de 2kΩ – pour le circuit des photo résistances, permettant d’avoir une amplitude optimale
|
||||
- 1x pile 9 Volts – pour alimenter tout le système
|
||||
- 1x boîtier pour pile 9 Volts [OPTIONNEL] – pour éviter de perdre l’unique source d’alimentation du robot
|
||||
|
||||
### Base du châssis
|
||||
|
||||
Le châssis est une plaque en bois, ici du contreplaqué de 5 millimètres
|
||||
d’épaisseur, qui a été usinée à l’aide de la découpeuse laser du Fablab. Elle
|
||||
mesure 17×15,5 centimètres, et dispose de deux bords biseautés pour avoir une
|
||||
vue frontale des obstacles que le robot pourrait rencontrer. Ici, ces biseaux
|
||||
ont des angles de 20 degrés, sur 4,5 centimètres. La plaque de base est ensuite
|
||||
percée pour accueillir les différents modules du robot. Les éléments les plus
|
||||
légers ou risquant moins de se détacher sont fixés par un simple système de
|
||||
mâchoire, prenant en étau la plaque de bois dans une forme de U.
|
||||
|
||||
![La forme de U pour prendre la plaque de base en mâchoire et tenir un capteur par pression.](images/u_shape.jpg)
|
||||
|
||||
### Alimentation
|
||||
|
||||
Pour l’alimentation du robot, on vient envoyer 9 Volts d’une source quelconque,
|
||||
d’une pile ou d’une alimentation stabilisée par exemple, dans l’entrée `VIN`
|
||||
de l’Arduino, qui sera après régulée en interne pour donner les différents
|
||||
rails d’alimentation. Cette entrée d’alimentation est connectée en interne au
|
||||
port DC “barrel jack” de l’Arduino, donc avant les régulateurs de courant,
|
||||
et prend donc des tensions entre 5 et 9 Volts.
|
||||
|
||||
Dans ce projet, nous utilisons une pile 9 Volts rechargeable dans un boîtier
|
||||
vissé au châssis.
|
||||
|
||||
![Une vue de la pile 9 Volts que nous utilisons, rechargeable en micro USB.](images/pile9v.jpg)
|
||||
|
||||
### Moteurs
|
||||
|
||||
Les moteurs ici sont vissés au châssis, car juste pris en mâchoire ils ne
|
||||
tiennent pas et se détachent à la moindre accélération.
|
||||
|
||||
Ils sont directement alimentés et pilotés via la carte Arduino Motor Shield,
|
||||
qui permet de sélectionner leur vitesse sur une échelle de `0` à `255`,
|
||||
d’actionner les freins et de leur donner un sens de rotation, et ce très
|
||||
simplement dans le code.
|
||||
|
||||
Des roues ont été imprimées en 3D pour aller sur les embouts des moteurs, et un
|
||||
joint torique sert de pneu pour améliorer l’adhérence du robot et éviter les
|
||||
démarrage en “burn”.
|
||||
|
||||
### PCB
|
||||
|
||||
Au début de ce projet, nous avons utilisé une carte de prototypage électronique,
|
||||
aussi appelé breadboard. Dans un second temps et pour éviter toute déconnexion
|
||||
de câbles liée à une potentielle accélération violente, nous avons fait un PCB,
|
||||
soudé manuellement sur une carte de prototypage prévu à cet effet.
|
||||
|
||||
![Une vue du dessus de notre PCB fait main.](images/pcb_irl.jpg)
|
||||
|
||||
Au niveau électronique, le PCB est routé de la manière suivante :
|
||||
|
||||
![Le routage du PCB sur une carte de protypage de type Breadboard.](images/pcb_fritzing.png)
|
||||
|
||||
Les différents composants viennent se connecter sur le PCB au moyen de
|
||||
différentes broches femelles, pour que ce dernier puisse être détaché et modifié
|
||||
sans devoir dessouder le moindre composant.
|
||||
|
||||
## Et du logiciel
|
||||
|
||||
D’un point de vue logiciel, notre projet est assez simple. En pseudo-algorithme,
|
||||
on peut tout simplement le résumer à “on regarde parmi les 4 photo résistances
|
||||
et on tourne ou avance vers celle ayant la valeur la plus sombre” dans le cas
|
||||
où `followDark` est à `true`. Si au contraire cette valeur est à `false`, alors
|
||||
il ira dans la direction opposée à la photorésistance ayant la plus haute valeur.
|
||||
|
||||
Une mesure des distances en face des trois capteurs à ultrasons permet d’arrêter
|
||||
le robot (presque) avant collision frontale, et le robot ne se déplacera pas
|
||||
tant que l’objet en face est toujours présent, et donc en théorie tant qu’il a
|
||||
trouvé l’endroit le plus sombre.
|
||||
|
||||
```cpp
|
||||
#define echo1Pin 5
|
||||
#define trig1Pin 4
|
||||
#define echo2Pin 7
|
||||
#define trig2Pin 6
|
||||
#define echo3Pin 10
|
||||
#define trig3Pin 2
|
||||
#define rotMot1Pin 12
|
||||
#define brakeMot1Pin 9
|
||||
#define vitMot1Pin 3
|
||||
#define rotMot2Pin 13
|
||||
#define brakeMot2Pin 8
|
||||
#define vitMot2Pin 11
|
||||
#define photoRes1Pin A2
|
||||
#define photoRes2Pin A3
|
||||
#define photoRes3Pin A4
|
||||
#define photoRes4Pin A5
|
||||
|
||||
#define followDark false
|
||||
|
||||
long duration;
|
||||
int distance;
|
||||
|
||||
void setup() {
|
||||
Serial.begin(9600);
|
||||
pinMode(rotMot1Pin, OUTPUT);
|
||||
pinMode(brakeMot1Pin, OUTPUT);
|
||||
pinMode(vitMot1Pin, OUTPUT);
|
||||
|
||||
pinMode(rotMot2Pin, OUTPUT);
|
||||
pinMode(brakeMot2Pin, OUTPUT);
|
||||
pinMode(vitMot2Pin, OUTPUT);
|
||||
|
||||
pinMode(photoRes1Pin, INPUT);
|
||||
pinMode(photoRes2Pin, INPUT);
|
||||
pinMode(photoRes3Pin, INPUT);
|
||||
pinMode(photoRes4Pin, INPUT);
|
||||
|
||||
pinMode(trig1Pin, OUTPUT);
|
||||
pinMode(echo1Pin, INPUT);
|
||||
pinMode(trig2Pin, OUTPUT);
|
||||
pinMode(echo2Pin, INPUT);
|
||||
pinMode(trig3Pin, OUTPUT);
|
||||
pinMode(echo3Pin, INPUT);
|
||||
|
||||
delay(3000);
|
||||
}
|
||||
|
||||
void loop(){
|
||||
byte echoPins[3] = {echo1Pin,echo2Pin,echo3Pin};
|
||||
byte trigPins[3] = {trig1Pin,trig2Pin,trig3Pin};
|
||||
long int durations[3] = {};
|
||||
long int distances[3] = {};
|
||||
|
||||
for(int i = 0; i < 3; i++){
|
||||
digitalWrite(trigPins[i], LOW);
|
||||
delayMicroseconds(2);
|
||||
|
||||
digitalWrite(trigPins[i], HIGH);
|
||||
delayMicroseconds(10);
|
||||
digitalWrite(trigPins[i], LOW);
|
||||
durations[i] = pulseIn(echoPins[i], HIGH);
|
||||
distances[i] = durations[i] * 0.034 / 2;
|
||||
}
|
||||
|
||||
int photoRes[4] = {};
|
||||
Serial.print("Capteur infra : 1[");
|
||||
photoRes[0] = analogRead(photoRes1Pin);
|
||||
Serial.print(photoRes[0]);
|
||||
Serial.print("] 2[");
|
||||
photoRes[1] = analogRead(photoRes2Pin);
|
||||
Serial.print(photoRes[1]);
|
||||
Serial.print("] 3[");
|
||||
photoRes[2] = analogRead(photoRes3Pin);
|
||||
Serial.print(photoRes[2]);
|
||||
Serial.print("] 4[");
|
||||
photoRes[3] = analogRead(photoRes4Pin);
|
||||
Serial.print(photoRes[3]);
|
||||
|
||||
for(int i = 0; i < 3; i++){
|
||||
Serial.print("] - distance(cm) : ");
|
||||
Serial.print(distances[i]);
|
||||
}
|
||||
|
||||
Serial.println();
|
||||
|
||||
bool b = false;
|
||||
for(int i = 0; i < 3; i++){
|
||||
if (distances[i] < 30) {
|
||||
b= true;
|
||||
}
|
||||
}
|
||||
|
||||
int photoResRef = photoRes[0];
|
||||
bool photoStop = true;
|
||||
int photoResMinPos = 0;
|
||||
|
||||
if (followDark){
|
||||
int photoResMin = 1024;
|
||||
for (int i = 0; i < 4; i++){
|
||||
if (photoResMin > photoRes[i]){
|
||||
photoResMin = photoRes[i];
|
||||
photoResMinPos = i;
|
||||
}
|
||||
if (abs(photoResRef - photoRes[i]) > 20){
|
||||
photoStop = false;
|
||||
}
|
||||
}
|
||||
} else{
|
||||
int photoResMax = 0;
|
||||
for (int i = 0; i < 4; i++){
|
||||
if (photoResMax < photoRes[i]){
|
||||
photoResMax = photoRes[i];
|
||||
photoResMinPos = (i + 2)%4;
|
||||
}
|
||||
if (abs(photoResRef - photoRes[i]) > 20){
|
||||
photoStop = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
Serial.println(photoResMinPos);
|
||||
|
||||
if (b || photoStop){
|
||||
digitalWrite(brakeMot1Pin, HIGH);
|
||||
digitalWrite(brakeMot2Pin, HIGH);
|
||||
} else {
|
||||
digitalWrite(brakeMot1Pin, LOW);
|
||||
digitalWrite(brakeMot2Pin, LOW);
|
||||
}
|
||||
|
||||
if (photoResMinPos == 0){
|
||||
digitalWrite(rotMot1Pin, LOW);
|
||||
digitalWrite(rotMot2Pin, LOW);
|
||||
}
|
||||
if (photoResMinPos == 1){
|
||||
digitalWrite(rotMot1Pin, LOW);
|
||||
digitalWrite(rotMot2Pin, LOW);
|
||||
}
|
||||
if (photoResMinPos == 2){
|
||||
digitalWrite(rotMot1Pin, HIGH);
|
||||
digitalWrite(rotMot2Pin, HIGH);
|
||||
}
|
||||
if (photoResMinPos == 3){
|
||||
digitalWrite(rotMot1Pin, HIGH);
|
||||
digitalWrite(rotMot2Pin, LOW);
|
||||
}
|
||||
analogWrite(vitMot1Pin, 150);
|
||||
analogWrite(vitMot2Pin, 150);
|
||||
}
|
||||
```
|
||||
|
||||
## Remerciements
|
||||
|
||||
Merci au Fablab de l’ENSEIRB-MATMECA, Eirlab, pour nous avoir permis de
|
||||
travailler sur ce projet et d’avoir contribué matériellement au projet.
|
||||
|
||||
Merci aussi aux différents Fabmanagers qui nous ont aidés tout au long du projet
|
||||
en répondant à nos questions plus ou moins posées sous l’emprise de la fatigue.
|
||||
|
||||
À la mémoire de Kaitlin Rooke.
|
Before Width: | Height: | Size: 553 KiB After Width: | Height: | Size: 553 KiB |
Before Width: | Height: | Size: 1.4 MiB After Width: | Height: | Size: 1.4 MiB |
Before Width: | Height: | Size: 1.2 MiB After Width: | Height: | Size: 1.2 MiB |
Before Width: | Height: | Size: 1.2 MiB After Width: | Height: | Size: 1.2 MiB |
Before Width: | Height: | Size: 1.3 MiB After Width: | Height: | Size: 1.3 MiB |
Before Width: | Height: | Size: 2.1 MiB After Width: | Height: | Size: 2.1 MiB |
@ -0,0 +1,294 @@
|
||||
+++
|
||||
title = "Shy bot – the robot which is scared of the light"
|
||||
description = "Creating a shy robot for a school project."
|
||||
date = 2022-05-19
|
||||
|
||||
[taxonomies]
|
||||
tags = ["maker", "fablab", "robotics"]
|
||||
+++
|
||||
|
||||
Creating a shy robot for a university project.
|
||||
|
||||
> This is a translated article of mine, original can be found on
|
||||
[Eirlab](https://www.eirlab.net/2022/05/19/robot-timide-le-robot-qui-fuit-la-lumiere/)
|
||||
|
||||
As part of my computer science engeneering curriculum, I enrolled in an option called
|
||||
"Makers" during my fourth year. From this course within
|
||||
[ENSEIRB-MATMECA's makerlab](https://www.eirlab.net)
|
||||
is born a shy robot, one that hates light and loves the shadows.
|
||||
|
||||
This article is all about explaining how it works and how you could reproduce our work, in the
|
||||
most "maker" spirit.
|
||||
|
||||
<!-- more -->
|
||||
|
||||
![A 3/4th view of said robot](images/robot_timide_header.jpg)
|
||||
|
||||
## Base concept
|
||||
|
||||
We wanted to create a robot which would be able to detect, in real time,
|
||||
the brightness of its surroundings, and would then use it to go in the
|
||||
darkest place of the room it would be in, in order to hide (it's a shy one, remember?).
|
||||
|
||||
Instead of going to the darkest place it can detect, there's also another mode
|
||||
available, where instead of moving towards the darkest place, it tries to
|
||||
flee the brightest direction. Although looking similar, these two ways of thinking
|
||||
yield to different results.
|
||||
|
||||
![A view of the robot in its final form.](images/robot_timide_enseirb.jpg)
|
||||
|
||||
## Some hardware
|
||||
|
||||
Before working on the software part of this project, we needed to have some hardware
|
||||
to run our code on.
|
||||
|
||||
It should be noted that all of the components required for this project were
|
||||
supplied by the fablab. We tried to only use parts that were already available,
|
||||
without the need to spend a single euro for this project.
|
||||
|
||||
### Shopping list
|
||||
|
||||
In order to recreate this project, you would need these components:
|
||||
|
||||
- 1x Arduino Uno (or an equivalent clone) – it's the brains of this whole project
|
||||
- 1x Arduino Motor Shield – to control the motors that will move the robot
|
||||
- 2x DC motors (here FT DC 130D) – to actually move the robot
|
||||
- 3x ultrasonic distance sensors HC-SR04 – to avoid hitting walls too often
|
||||
- 4x photoresistors – one per side, to be able to detect brightness around it
|
||||
- 4x 2kΩ resistors – for the photocell circuit, to have a better detection amplitude
|
||||
- 1x 9V battery – because it needs power to do anything
|
||||
- 1x box for 9V batteries [optional] – so the battery is safely mounted
|
||||
|
||||
### Chassis
|
||||
|
||||
The chassis for this robot is a piece of plywood, 5mm thick to be precise. It
|
||||
has been machined using a laser cutter, availabe in the fablab. This plank
|
||||
is about 17 by 15.5 centimeters, and has two beveled corners, which are useful
|
||||
so the robot can see in front of it. The bevels have a 20 degrees angle, going on
|
||||
4.5cm.
|
||||
|
||||
The base plate is then pierced to accomodate for the different components that
|
||||
will need to be mounted on the said plate. The lighter components will be pressure
|
||||
mounted, using a u-shape that will fit around the plate's side.
|
||||
|
||||
![The u-shape to pressure-mount the component on the plate.](images/u_shape.jpg)
|
||||
|
||||
### Power
|
||||
|
||||
For powering the robot, we want to send 9 volts, from a battery or a power supply,
|
||||
in the power input `VIN` of our Arduino board. This tension will then be regulated
|
||||
on the PCB to create the different power rails that would be needed. This input is
|
||||
connected internally to the barrel jack of the Arduino, which can take power ranging
|
||||
from 5 to 9 volts.
|
||||
|
||||
In this project, we are using a 9V battery which is placed in a case that is bolted
|
||||
on the base plate.
|
||||
|
||||
![A picture of the rechargeable 9 volts battery that we are using.](images/pile9v.jpg)
|
||||
|
||||
### Motors
|
||||
|
||||
The motors are bolted to the chassis in this project, because when they were
|
||||
mounted just using pressure, they would run away each time the robot made a
|
||||
rough acceleration.
|
||||
|
||||
These motors are directly powered and controlled using the Arduino Motor Shield,
|
||||
which lets us select a speed ranging from `0` to `255` using a coding API.
|
||||
This API also lets us engage brakes or set the direction in which the wheels
|
||||
are turning.
|
||||
|
||||
The wheels for this project have been 3D printed and pressure-fit on the motors
|
||||
themselves, and an O-ring seal is then used to add some grip.
|
||||
|
||||
### PCB
|
||||
|
||||
In the beginning of this project, we used a breadboard for electronics prototyping.
|
||||
We had a lot of disconnect issues, so as soon as the hardware part could be frozen,
|
||||
we migrated the breadboard electronics to a hand-made PCB using a prototyping board.
|
||||
This made the robot way more resilient to a sharp acceleration curve.
|
||||
|
||||
![A top-view of our hand-made PCB.](images/pcb_irl.jpg)
|
||||
|
||||
On an electronic level, the PCB is routed such as:
|
||||
|
||||
![The PCB routing on a prototyping breadboard.](images/pcb_fritzing.png)
|
||||
|
||||
The different components are interfaced with the PCB using a kind of connector that
|
||||
lets us connect and disconnect things as we iterated on the sensors without having to
|
||||
use a soldering iron.
|
||||
|
||||
## And finally some software
|
||||
|
||||
From a software point-of-view, the project is quite simple.
|
||||
|
||||
On a high-level, the algorithm could be summed up as follows: check all four photocells,
|
||||
and move, using rotation and translation, towards the one that has the darkest value, if
|
||||
`followDark` is set to `true`. If this value is set to `false`, then the robot will move
|
||||
itself in the opposite direction to this brighest value.
|
||||
|
||||
A distance mesurement is made using the three ultrasonic distance sensor, so the robot is
|
||||
able to stop before hitting a wall (hopefully). These sensors act as "kill switches" that
|
||||
forbid the robot from ever moving while these sensors are detecting an obstacle.
|
||||
|
||||
The code is as follow:
|
||||
|
||||
```cpp
|
||||
#define echo1Pin 5
|
||||
#define trig1Pin 4
|
||||
#define echo2Pin 7
|
||||
#define trig2Pin 6
|
||||
#define echo3Pin 10
|
||||
#define trig3Pin 2
|
||||
#define rotMot1Pin 12
|
||||
#define brakeMot1Pin 9
|
||||
#define vitMot1Pin 3
|
||||
#define rotMot2Pin 13
|
||||
#define brakeMot2Pin 8
|
||||
#define vitMot2Pin 11
|
||||
#define photoRes1Pin A2
|
||||
#define photoRes2Pin A3
|
||||
#define photoRes3Pin A4
|
||||
#define photoRes4Pin A5
|
||||
|
||||
#define followDark false
|
||||
|
||||
long duration;
|
||||
int distance;
|
||||
|
||||
void setup() {
|
||||
Serial.begin(9600);
|
||||
pinMode(rotMot1Pin, OUTPUT);
|
||||
pinMode(brakeMot1Pin, OUTPUT);
|
||||
pinMode(vitMot1Pin, OUTPUT);
|
||||
|
||||
pinMode(rotMot2Pin, OUTPUT);
|
||||
pinMode(brakeMot2Pin, OUTPUT);
|
||||
pinMode(vitMot2Pin, OUTPUT);
|
||||
|
||||
pinMode(photoRes1Pin, INPUT);
|
||||
pinMode(photoRes2Pin, INPUT);
|
||||
pinMode(photoRes3Pin, INPUT);
|
||||
pinMode(photoRes4Pin, INPUT);
|
||||
|
||||
pinMode(trig1Pin, OUTPUT);
|
||||
pinMode(echo1Pin, INPUT);
|
||||
pinMode(trig2Pin, OUTPUT);
|
||||
pinMode(echo2Pin, INPUT);
|
||||
pinMode(trig3Pin, OUTPUT);
|
||||
pinMode(echo3Pin, INPUT);
|
||||
|
||||
delay(3000);
|
||||
}
|
||||
|
||||
void loop(){
|
||||
byte echoPins[3] = {echo1Pin,echo2Pin,echo3Pin};
|
||||
byte trigPins[3] = {trig1Pin,trig2Pin,trig3Pin};
|
||||
long int durations[3] = {};
|
||||
long int distances[3] = {};
|
||||
|
||||
for(int i = 0; i < 3; i++){
|
||||
digitalWrite(trigPins[i], LOW);
|
||||
delayMicroseconds(2);
|
||||
|
||||
digitalWrite(trigPins[i], HIGH);
|
||||
delayMicroseconds(10);
|
||||
digitalWrite(trigPins[i], LOW);
|
||||
durations[i] = pulseIn(echoPins[i], HIGH);
|
||||
distances[i] = durations[i] * 0.034 / 2;
|
||||
}
|
||||
|
||||
int photoRes[4] = {};
|
||||
Serial.print("Capteur infra : 1[");
|
||||
photoRes[0] = analogRead(photoRes1Pin);
|
||||
Serial.print(photoRes[0]);
|
||||
Serial.print("] 2[");
|
||||
photoRes[1] = analogRead(photoRes2Pin);
|
||||
Serial.print(photoRes[1]);
|
||||
Serial.print("] 3[");
|
||||
photoRes[2] = analogRead(photoRes3Pin);
|
||||
Serial.print(photoRes[2]);
|
||||
Serial.print("] 4[");
|
||||
photoRes[3] = analogRead(photoRes4Pin);
|
||||
Serial.print(photoRes[3]);
|
||||
|
||||
for(int i = 0; i < 3; i++){
|
||||
Serial.print("] - distance(cm) : ");
|
||||
Serial.print(distances[i]);
|
||||
}
|
||||
|
||||
Serial.println();
|
||||
|
||||
bool b = false;
|
||||
for(int i = 0; i < 3; i++){
|
||||
if (distances[i] < 30) {
|
||||
b= true;
|
||||
}
|
||||
}
|
||||
|
||||
int photoResRef = photoRes[0];
|
||||
bool photoStop = true;
|
||||
int photoResMinPos = 0;
|
||||
|
||||
if (followDark){
|
||||
int photoResMin = 1024;
|
||||
for (int i = 0; i < 4; i++){
|
||||
if (photoResMin > photoRes[i]){
|
||||
photoResMin = photoRes[i];
|
||||
photoResMinPos = i;
|
||||
}
|
||||
if (abs(photoResRef - photoRes[i]) > 20){
|
||||
photoStop = false;
|
||||
}
|
||||
}
|
||||
} else{
|
||||
int photoResMax = 0;
|
||||
for (int i = 0; i < 4; i++){
|
||||
if (photoResMax < photoRes[i]){
|
||||
photoResMax = photoRes[i];
|
||||
photoResMinPos = (i + 2)%4;
|
||||
}
|
||||
if (abs(photoResRef - photoRes[i]) > 20){
|
||||
photoStop = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
Serial.println(photoResMinPos);
|
||||
|
||||
if (b || photoStop){
|
||||
digitalWrite(brakeMot1Pin, HIGH);
|
||||
digitalWrite(brakeMot2Pin, HIGH);
|
||||
} else {
|
||||
digitalWrite(brakeMot1Pin, LOW);
|
||||
digitalWrite(brakeMot2Pin, LOW);
|
||||
}
|
||||
|
||||
if (photoResMinPos == 0){
|
||||
digitalWrite(rotMot1Pin, LOW);
|
||||
digitalWrite(rotMot2Pin, LOW);
|
||||
}
|
||||
if (photoResMinPos == 1){
|
||||
digitalWrite(rotMot1Pin, LOW);
|
||||
digitalWrite(rotMot2Pin, LOW);
|
||||
}
|
||||
if (photoResMinPos == 2){
|
||||
digitalWrite(rotMot1Pin, HIGH);
|
||||
digitalWrite(rotMot2Pin, HIGH);
|
||||
}
|
||||
if (photoResMinPos == 3){
|
||||
digitalWrite(rotMot1Pin, HIGH);
|
||||
digitalWrite(rotMot2Pin, LOW);
|
||||
}
|
||||
analogWrite(vitMot1Pin, 150);
|
||||
analogWrite(vitMot2Pin, 150);
|
||||
}
|
||||
```
|
||||
|
||||
## Thanks
|
||||
|
||||
We wanted to say thank you to the ENSEIRB-MATMECA, to our fablab, EIRLAB, for
|
||||
trusting us, helping us and providing the hardware we used throughout this project.
|
||||
|
||||
Thank you also to the fab-managers who helped us and answered all of our questions,
|
||||
more or less asked under fatigue.
|
||||
|
||||
In memory of Kaitlin Rooke.
|
@ -1,5 +1,5 @@
|
||||
+++
|
||||
title = "Articles archivés"
|
||||
title = "Archived articles"
|
||||
path = "archive"
|
||||
+++
|
||||
|
||||
|
@ -2,7 +2,7 @@
|
||||
|
||||
{% block content %}
|
||||
<div class="post">
|
||||
<h1 class="post-title">{% block heading %}Perdu ?{% endblock heading %}</h1>
|
||||
<p>{% block message %}Cette page n'existe pas.{% endblock message %}</p>
|
||||
<h1 class="post-title">{% block heading %}Lost?{% endblock heading %}</h1>
|
||||
<p>{% block message %}Looks like you're trying something that doesn't exist (anymore?).{% endblock message %}</p>
|
||||
</div>
|
||||
{% endblock content %}
|
||||
|
@ -24,14 +24,14 @@
|
||||
<span class="button previous">
|
||||
<a href="{{ paginator.previous | safe }}">
|
||||
<span class="button__icon">←</span>
|
||||
<span class="button__text">Posts plus récents</span>
|
||||
<span class="button__text">More recent posts</span>
|
||||
</a>
|
||||
</span>
|
||||
{% endif -%}
|
||||
{%- if paginator.next %}
|
||||
<span class="button next">
|
||||
<a href="{{ paginator.next | safe }}">
|
||||
<span class="button__text">Posts plus anciens</span>
|
||||
<span class="button__text">Older posts</span>
|
||||
<span class="button__icon">→</span>
|
||||
</a>
|
||||
</span>
|
||||
|
@ -1,108 +0,0 @@
|
||||
{% macro content(page, summary, show_only_description) %}
|
||||
{%- if show_only_description %}
|
||||
<div class="post-content">
|
||||
{{ page.description | safe }}
|
||||
</div>
|
||||
{% elif summary and page.summary %}
|
||||
<div class="post-content">
|
||||
{{ page.summary | safe }}
|
||||
</div>
|
||||
<div>
|
||||
<!-- ︎ -- force text style - some devices render this as emoji -->
|
||||
<a class="read-more button" href="{{ page.permalink | safe }}">
|
||||
<span class="button__text">En savoir plus</span>
|
||||
<span class="button__icon">↩︎</span>
|
||||
</a>
|
||||
</div>
|
||||
{% else %}
|
||||
{#- full content -#}
|
||||
<div class="post-content">
|
||||
{{ page.content | safe }}
|
||||
</div>
|
||||
{%- endif %}
|
||||
{% endmacro content %}
|
||||
|
||||
|
||||
{% macro date(page) %}
|
||||
<span class="post-date">
|
||||
{%- if page.date %}
|
||||
{{ page.date | date(format="%d-%m-%Y") }}
|
||||
{% endif -%}
|
||||
</span>
|
||||
{% endmacro post_date %}
|
||||
|
||||
|
||||
{% macro earlier_later(page) %}
|
||||
{%- if config.extra.enable_post_view_navigation and page.lower or page.higher %}
|
||||
<div class="pagination">
|
||||
<div class="pagination__title">
|
||||
<span class="pagination__title-h">{{ config.extra.post_view_navigation_prompt }}</span>
|
||||
<hr />
|
||||
</div>
|
||||
<div class="pagination__buttons">
|
||||
{%- if page.higher %}
|
||||
<span class="button previous">
|
||||
<a href="{{ page.higher.permalink | safe }}">
|
||||
<span class="button__icon">←</span>
|
||||
<span class="button__text">{{ page.higher.title }}</span>
|
||||
</a>
|
||||
</span>
|
||||
{% endif %}
|
||||
{% if page.lower %}
|
||||
<span class="button next">
|
||||
<a href="{{ page.lower.permalink | safe }}">
|
||||
<span class="button__text">{{ page.lower.title }}</span>
|
||||
<span class="button__icon">→</span>
|
||||
</a>
|
||||
</span>
|
||||
{% endif -%}
|
||||
</div>
|
||||
</div>
|
||||
{% endif -%}
|
||||
{% endmacro earlier_later %}
|
||||
|
||||
|
||||
{% macro header(page) %}
|
||||
<h1 class="post-title"><a href="{{ page.permalink | safe }}">{{ page.title }}</a></h1>
|
||||
<div class="post-meta-inline">
|
||||
{{ post_macros::date(page=page) }}
|
||||
</div>
|
||||
|
||||
{{ post_macros::tags(page=page) }}
|
||||
{% endmacro header %}
|
||||
|
||||
|
||||
{% macro list_posts(pages) %}
|
||||
<ul>
|
||||
{%- for page in pages %}
|
||||
{%- if page.draft %}
|
||||
{% continue %}
|
||||
{% endif -%}
|
||||
<li class="post-list">
|
||||
<a href="{{ page.permalink | safe }}">
|
||||
{{ post_macros::date(page=page) }}
|
||||
:: <span class="post-list-title">{{ page.title }}</span></a>
|
||||
{{ post_macros::tags(page=page, short=true) }}
|
||||
</li>
|
||||
{% endfor -%}
|
||||
</ul>
|
||||
{% endmacro list_posts %}
|
||||
|
||||
|
||||
{% macro tags(page, short=false) %}
|
||||
{%- if page.taxonomies and page.taxonomies.tags %}
|
||||
<span class="post-tags-inline">
|
||||
{%- if short %}
|
||||
::
|
||||
{%- set sep = "," -%}
|
||||
{% else %}
|
||||
:: tags:
|
||||
{%- set sep = " " -%}
|
||||
{% endif -%}
|
||||
{%- for tag in page.taxonomies.tags | sort | unique(case_sensitive=false) %}
|
||||
<a class="post-tag" href="{{ get_taxonomy_url(kind='tags', name=tag) | safe }}">#{{ tag }}</a>
|
||||
{%- if not loop.last %}{{ sep | safe }}{% endif -%}
|
||||
{% endfor -%}
|
||||
</span>
|
||||
{% endif -%}
|
||||
{% endmacro tags %}
|
@ -1,17 +0,0 @@
|
||||
{% extends "terminimal/templates/tags/list.html" %}
|
||||
|
||||
{% block content %}
|
||||
<div class="post">
|
||||
<h1 class="post-title">Liste des tags</h1>
|
||||
|
||||
<ul>
|
||||
{% for term in terms %}
|
||||
<li class="tag-list">
|
||||
<a href="{{ term.permalink | safe }}">
|
||||
{{ term.name }} ({{ term.pages | length }} post{{ term.pages | length | pluralize }})
|
||||
</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
{% endblock content %}
|
@ -1,17 +0,0 @@
|
||||
{% extends "terminimal/templates/tags/single.html" %}
|
||||
|
||||
|
||||
{% block content %}
|
||||
<div class="post">
|
||||
<h1 class="post-title">
|
||||
tag: #{{ term.name }}
|
||||
({{ term.pages | length }} post{{ term.pages | length | pluralize }})
|
||||
</h1>
|
||||
|
||||
<a href="{{ config.base_url | safe }}/tags">
|
||||
Voir tous les tags
|
||||
</a>
|
||||
|
||||
{{ post_macros::list_posts(pages=term.pages) }}
|
||||
</div>
|
||||
{% endblock content %}
|