1. Introduction

J'ai toujours été intrigué par la manière dont les abeilles, parmi d'autres insectes, étaient capable de chercher chacune de leur côté une source de pollen, puis une fois qu'elle l'avait trouvée, de communiquer avec les autres insectes la position de la nourriture afin de créer une chaîne d'approvisionnement entre la ruche et le champ de fleurs. Même si les phéromones ou les différentes danses que les abeilles utilisent ne sont pas réplicables avec le Thymio. Je souhaiterai quand même essayer de répliquer ce comportement. Dans ce projet, le plus grand facteur limitant sera probablement la précision des Thymio car comme les capteurs ne sont pas très précis toutes les mesures associées ne le sont pas également. Ainsi les enjeux de se projets seront suivants : Se localiser dans « l’arène » pour revenir au point de départ, communiquer avec les autres Thymio et créer la chaine finale de Thymio.

2. Matériel et méthodes

2.1 Matériel

  • 1 Thymio
  • Plusieurs boîtes pour faire une arène

2.2 Méthode

Afin de mener à bien le projet, il faudra donc se poser les questions suivantes:

  • Comment le Thymio fera-t-il pour détecter la source de nourriture ?
  • Comment le Thymio fera-t-il pour se déplacer au hasard ?
  • Comment le Thymio fera-t-il pour se localiser dans la zone de recherche ?
  • Comment le Thymio fera-t-il pour communiquer sa position avec les autres et comment feront-ils pour se mettre en file ?

Pour ce projet je pensais initialement écrire le programme en Aseba, le langage native de l'environment Thymio. Mais pourtant je me suis rapidement rendu compte que les possibilités que l'Aseba offrait était relativement limité, peu de gens écrivent en effet dans ce langage, et c'est donc pourquoi j'ai opté d'utiliser une librairie nommé tdm client qui permet de coder en Python en utilisant le Thymio. Si Python offre un nombre de possibilité quasiment infini, je n'ai pourtant quasiment pas d'expérience dans ce langage et la libraire utilisant un vocabulaire très particulier j'étais un peu perdu au début. Maintenant que l'on sait quel langage l'on va utiliser pour coder, on peut commencer à répondre au différentes questions.

2.2.1 Généralités:

Avant de commencer à réfléchir au différents problèmes que l'on va résoudre, il est important de mettre en place l'environment que l'on va utiliser pour coder (je reviendrais plus en détail sur chaque variable et fonctions par la suite) Premièrement on va importer différentes librairies qui vont nous permettre d'utiliser différentes fonction plus tard dans le code. La plus importante est ClientAsync, elle nous permet de créer des fonctions qui utilisent des variables du robot et communiquent avec lui. On peut par exemple, contrôler les moteurs du Thymio ou lire les valeurs de certains capteurs.

from tdmclient import ClientAsync
import random
import pygame
import time
import math

On va ensuite définir certaine variables.

position_x = 0 
position_y = 0
position_variable = [0,0]
rotating_speed = 110
face_wall_speed = 50
arena_x = 50
arena_y = 50
actual_arena_x = arena_x - 20
actual_arena_y = arena_y - 20
error = 0.1 #on prend en compte 5% d'erreur
time0 = 0
angle = 0

On va ensuite définir plusieurs fonctions qui nous serons utiles par la suite afin de contrôler le comportement du robot.

def motors(left, right):
        return {
            "motor.left.target": [left],
            "motor.right.target": [right],
        }

async def thymio_run(): #permet au robot d'avancer
            with await client.lock() as node:
                #les moteurs étant mal calibré le moteur gauche 
                #tourne plus rapidement que le droite, il faut donc compenser
                await node.set_variables(motors(100, 102)) 

async def thymio_stop(): #permet au robot de s'arrêter
            with await client.lock() as node:
                await node.set_variables(motors(0, 0))

async def thymio_rotate_angle(): 
            #permet au robot de faire une rotation à une vitesse et dans sens spécifique
            with await client.lock() as node:
                await node.set_variables(motors(-rotating_speed, rotating_speed))

#permet au robot de faire la même chose que thymio_rotate_angle() 
#mais à une autre vitesse et dans le sens opposé
async def thymio_rotate_wall():  
            with await client.lock() as node:
                await node.set_variables(motors(face_wall_speed, -face_wall_speed))

Et on va finalement utiliser les fameuses fonctions asynchrones afin d'extraire les différentes valeurs provenant des capteurs du Thymio.

with ClientAsync() as client:
    async def thymio_variables():
            with await client.lock() as node:
                global prox_ground_0, prox_ground_1, prox_horizontal_1, \\
                   prox_horizontal_3, prox_horizontal_4
                await node.wait_for_variables({"prox.ground.delta"})
                await node.wait_for_variables({"prox.horizontal"})
                await node.wait_for_variables({"prox.horizontal"})
                prox_ground_0 = node.v.prox.ground.delta[0]
                prox_ground_1 = node.v.prox.ground.delta[1]
                prox_horizontal_1 = node.v.prox.horizontal[1]
                prox_horizontal_3 = node.v.prox.horizontal[3]
                prox_horizontal_4 = node.v.prox.horizontal[4]

2.2.2 Comment le Thymio fera-t-il pour détecter la source de nourriture ?

Afin de répliquer le champ de fleurs dans l'exemple des abeilles, nous allons utiliser un simple point noir imprimé sur du papier (dans ce cas là le point fais 4cm de rayon mais je sais que l'on peut descendre jusqu'à 2 centimètres de rayon)
IMG_9099.png, fév. 2022
Pourquoi noir on peut se demander ? En fait, ça nous permet d'utiliser une des variables natives du Thymio appelée "prox.ground.delta". Cette dernière est une soustraction entre les variables prox.ground.reflected et prox.ground ambiant qui mesurent toutes les deux l'intensité de la lumière dans les deux capteurs qui se trouvent sous le Thymio. En faisant la différence entre les deux variables on obtient une variable prox.ground.delta qui varie en fonction de la distance par rapport au sol et à sa couleur. Ceci nous est extrêmement utile car comme nous savons que le Thymio est à une distance constante du sol, cette variable ne change donc qu'en fonction de la couleur de ce dernier. Afin d'implémenter cela dans notre code nous allons donc faire la chose suivante:

def target_detection():
        if prox_ground_0 < 300 and prox_ground_1 < 300:
            client.run_async_program(thymio_stop)
            print("le point a été détecté")
            return True
        else:
            return False

while True:
    client.run_async_program(thymio_variables)
    target_detected = target_detection()
    if not target_detected:
        obstacle_detection()

Comme on peut le voir dans le code au dessus, on a créé une fonction target_detection() qui nous permet de détecter la source de nourriture en lisant les deux capteurs en dessous du Thymio pour la variable prox.ground.delta. Celle ci baissant si le sol devient plus foncé (ou si la distance devient plus grande ce qui n'est pas le cas pour nous), on rajoute un condition qui fait que lorsque le Thymio détecte le point cela déclenche un message qui dit: "le point a été détecté". On rajoute ensuite la fonction dans une boucle while afin que le programme ne soit pas simplement exécuté qu'une seul fois mais bien en boucle jusqu'à ce que le point soit trouvé. Finalement il ne faut pas oublier d'appeler la fonction asynchrone thymio_variables() dans la boucle while afin que les variables que l'on souhaite avoir soient tout le temps disponible et utilisables.

2.2.3 Comment le Thymio fera-t-il pour se déplacer au hasard ?

Dans la nature, lorsqu'elles recherchent une source de nourriture, les abeilles ont tendance à se déplacer dans une sorte de mouvement hasardeux jusqu'à ce qu'elles trouvent une fleur à polliniser (le processus est en réalité beaucoup plus complexe que ça). Le hasard qui peut paraître simple à répliquer aux premiers abords et en réalité bien compliqué à imiter. C'est dans cette tâche que la librairie random que nous avons auparavant installé va nous être utile.

def turn_angle():
    face_wall()
    global angle, position_variable, time0
    angle = random.randrange(10, 20, 10)
print("l'angle choisit est:", angle)

On va tout d'abord définir une variable angle qui sera égale à soit 10 ou 20 (j'expliquerai plus tard en détail pourquoi j'ai décidé de ne pas choisir d'autres angles) puis on va imprimer l'angle choisit afin de pouvoir voir si le programme fonctionne correctement.

    position_variable = position(angle, position_variable[0], position_variable[1], time0)
    rotating_time = 2*(angle/90) + 2 # *2 -> passage en secondes, + 2000 pour la rotation de 90
    print("le Thymio doit tourner pendant", rotating_time)

La prochaine étape va être de calculer en fonction de l'angle choisit combien de temps le Thymio va devoir tourner. Sachant, en ayant mesuré de nombreuses fois, qu'à une vitesse de 110 le Thymio tourne de 90 degrés en 2 secondes il nous suffit de faire la formule rotating_time() pour obtenir le temps de rotation nécéssaire. Il est important de noter 2 éléments:

  1. On veut que le Thymio fasse une rotation de 90 degrés avant de choisir son angle. Ceci est nécéssaire pour avoir des angles plus précis, mais aussi pour les calculs de positions que le Thymio fera plus tard et finalement évidemment aussi afin qu'il ne se retape pas contre le même mur qu'il vient de toucher.
  2. On veut que le temps de rotation soit en secondes pour la fonction time.sleep() que l'on va utiliser par la suite.
    client.run_async_program(thymio_rotate_angle)
    print("thymio is turning to reach the angle")
    time.sleep(rotating_time)
    print("thymio has reached the angle")
    time0 = pygame.time.get_ticks()
    return [position_variable, angle]

Finalement on va pouvoir passer à la rotation. Comme partout dans mon programme, les print() peuvent être ignorés comme ils ne servent qu'à comprendre ce que le robot est entrain de faire lorsque l'on l'utilise. On commence par utiliser la fonction asynchrone thymio_rotate_angle() qui nous permet de tourner sur place à une vitesse, rotating_speed. On va ensuite utiliser la fonction time.sleep qui nous permet de retarder l'exécution de la suite du programme par le nombre de seconde que l'on souhaite. Ainsi le robot commence à tourner sur lui même puis reste bloqué dans cet état le temps que l'on veut pour atteindre l'angle voulu (rotating_time). Une fois la ligne time.sleep passée le Thymio repart tout droit vers le prochain mur (pas visible dans cette partie du code, mais nous en parlerons dans la prochaine section). Ce système marche extrêmement bien pour des angles de 10 ou 20 degrés, l'imprécision n'étant pas visible à l'oeil nu (sur une rotation à 360 degrés, on à environ 2% d'erreur)

2.2.4 Comment le Thymio fera-t-il pour se localiser dans la zone de recherche ?

La première solution que j'avais envisagé afin de pouvoir se localiser dans la zone de recherche, consistait à lire les valeurs des variables prox se trouvant à l'avant du robot (plus la valeur est basse plus le mur se trouve loin). De cette manière il aurait suffit que je me mette successivement face aux quatre murs, en faisant de rotations de 90 degrés. Les deux problèmes principaux avec cette méthode sont que les capteurs ont sont plutôt imprécis mais surtout que la distance maximale à laquelle ces capteurs peuvent détecter des obstacles n'est que entre 11 et 12 centimètres.
((http://aseba.wdfiles.com/local--files/fr:thymioreflexpapier/reflex-papier-graph.png)

Je me suis donc porté vers une deuxième solution qui aurait consisté à placer une caméra au dessus de la zone de recherche, puis à l'aide de suivi d'image j'aurais pu déterminer où le Thymio se trouvait. Même si ce système est probablement plus précis, il est également beaucoup plus compliqué à mettre en place et n'ayant presque aucune expérience en Python j'ai préféré la prochaine solution.

Afin de mieux comprendre la méthode que fini par utilisé, on peut regarder le schéma ci-dessous:
Diagram_Thymio.png, fév. 2022

Le principe est le suivant: On sait qu'à chaque fois que l'on touche un mur, le Thymio sélectionne un angle au hasard. Le mouvement peut donc sembler completement hasardeux. C'est pour ça qu'on choisit au hasard seulement entre une rotation de 10 ou 20 degrés. Ces angles étant assez obtus le Thymio ne risque jamais de passer de passer du coté 1 à 3 ou 2 à 4 (Ceci n'est pas complètement vrai mais on en reparlera dans la discussion). Ceci peut paraitre inutile, mais si on sait que l'on passe toujours d'un coté à son voisin dans le sens anti-horaire, on peut commencer à faire de la trigonométrie. Ayant l'angle et voulant trouver les cotés adjacent et opposé il ne nous reste plus qu'à calculer l'hypoténuse. Pour cela on peut utiliser de la formule: distance = vitesse * temps. Comme on se déplace à des vitesse relativement basse (∿3,2cm/s), la période d'accélération est également négligeable dans nos calculs.
Pour résumer, nous savons maintenant qu'il faut qu'à chaque fois que l'on rencontre un mur, on regarde la position enregistrée (la position la dernière fois que l'on a touché un mur) afin de savoir si l'on doit soustraire ou additionner l'adjacent et l'opposé à la position sur x ou y.

def obstacle_detection():
    print(prox_horizontal_4)
    if prox_horizontal_4 > 3000:
        print("thymio has met an obstacle")
        turn_angle()
    else:
        client.run_async_program(thymio_run)
        pygame.init()

def target_detection():
        if prox_ground_0 < 300 and prox_ground_1 < 300:
            client.run_async_program(thymio_stop)
            print("le point a été détecté")
            return True
        else:
            return False

while True:
    client.run_async_program(thymio_variables)
    target_detected = target_detection()
    if not target_detected:
        obstacle_detection()

Il est donc logique que l'on commence par créer une fonction qui nous permet de détecter des obstacles à l'avant du Thymio. Un détail important est que l'on utilise pas le capteur du milieu mais plutôt celui le plus à droite. Après avoir testé le programme plusieurs fois j'ai remarqué qu'après avoir touché plusieurs murs le Thymio avait tendance à se placer de plus en plus en parallèle avec le mur. Sachant que l'on tourne toujours en sens anti-horaire j'ai donc simplement changé la détection d'obstacle du capteur avant vers le capteur 4 (le plus à droite). J'appelle ensuite cette fonction dans le bloc de code que nous avons vu plus tôt.
La boucle while commence toujours par appeler thymio_variables() afin que l'on puisse par la suite, utiliser les variables dans d'autres fonction, puis finalement grâce à des booléens, on obtient le comportement suivant: Si aucune source de nourriture n'est détecté au sol alors la détection d'obstacle s'enclenche, mais dès lors que la source de nourriture est détectée alors la fonction obstacle_detection() arrête d'être appelée.
On voit également que dès que obstacle_detection() est enclenché on active pygame.init() ce qui nous permet de commencer un chronomètre pour connaître la longueur de l'hypoténuse. Une fois l'obstacle détecté, la fonction turn_angle() que nous avons vu plus tôt est enclenchée afin de choisir un angle. Mais juste avant d'enclencher la rotation et de repartir vers le prochain le mur, le Thymio doit encore faire deux choses.

def face_wall():
    print(prox_horizontal_1, prox_horizontal_3)
    if 3200 < prox_horizontal_1 and 3200 < prox_horizontal_3:
        client.run_async_program(thymio_stop)
        print("thymio is facing the wall")
        position(angle, position_variable[0], position_variable[1])
    else:
        client.run_async_program(thymio_rotate_wall)
        print("thymio is rotating to face the wall")

Premièrement le Thymio va tourner sur lui même afin de se mettre en face du mur. En effet, afin d'augmenter la précision de l'angle choisi par le Thymio il est nécéssaire que le Thymio se place correctement face au mur. Ceci se fait, en faisant tourner le robot sur lui même jusqu'a ce que les prox horizontal 1 et 3 lisent une valeur plus hautes que 3200. Une fois le Thymio en face du mur, on appelle la fonction position().

def position(angle, position_x, position_y, time0 = 0):
    print("using the position system")
    time = pygame.time.get_ticks()
    print(time, time0)
    print("time traveled is:", time - time0)
    straight_length = 3.2*((time - time0)/1000) # /1000 pour millisecondes --> secondes
    print("straight length is", straight_length)
    angle = math.radians(angle) #passage en radians pour math.cos et math.sin
    adjacent = (math.cos(angle)) * straight_length
    opposite = (math.sin(angle)) * straight_length
    print("adjacent is:", adjacent, "opposite is:", opposite)

La fonction position() commence par pygame.time.get_ticks() afin de stopper le chronomètre et donc de savoir durant combien de temps le Thymio à avancé. La variable time0 (initialement égale à 0) nous permet de calculer l'intervalle entre la dernière fois que la fonction pygame.init() à été appelé et maintenant. Sans time0 le programme ne réinitialise pas le chronomètre et on finit avec avec des hypoténuses toujours plus grandes.
L'hypoténuse, appelée straight_length, est calculé avec la formule distance = vitesse * temps (le temps avec pygame.init() est en millisecondes, il faut donc le passer en secondes). Finalement on utilise les fonctions math.cos et math.sin (on passe l'angle en radians pour ces fonctions) de la libraire math afin de calculer le côté adjacent et l'opposé.

if position_x < actual_arena_x*error:
        print("thymio has touched side 3")
        position_x = position_x + opposite
        position_y = position_y + adjacent
    elif position_y < actual_arena_y*error:
        print("thymio has touched side 2")
        position_x = position_x - adjacent
        position_y = position_y + opposite
    elif position_y > actual_arena_y - actual_arena_y*error:
        print("thymio has touched side 4")
        position_x =+adjacent
        position_y =- opposite
    elif position_x > actual_arena_x - actual_arena_x*error:
        print("thymio has touched side 1")
        position_x =-opposite
        position_y =-adjacent
    print (("position_x is:", position_x, "position_y is:", position_y))
    return [position_x, position_y]

On peut maintenant finalement calculer la position grâce à chaque cas. On peut se référer au diagramme afin de savoir si il faut additionner ou soustraire l'adjacent ou l'opposé aux variables position_x et position_y.
On peut noter que l'on utilise pas les dimensions initiales de la zone de recherche mais des dimensions réduites de 20cm. J'ai pu constater lors des nombreux tests, qu'au départ on ne peut pas réellement mettre le Thymio au point (0;0) , sinon ce dernier ne pourrait pas se mettre en rotation étant bloqué par les boites. Il faut donc lui donner environ 10 cm de marges pour commencer. Les 10 cm supplémentaires sont expliqués par le faite que la fonction obstacle_detection() arrête normalement le Thymio avant de toucher le mur.

3. Résultats

Au niveau de la recherche de la source de nourriture, comme on peut le voir dans la vidéo, le Thymio est complétement fonctionnel et ne rate quasiment jamais la pastille noir. Le Thymio est aussi parfaitement capable de ce déplacer dans la zone de recherche aléatoirement sans se taper contre les murs et en choisissant un chemin légèrement différent à chaque. Finalement le Thymio se met également correctement en face du mur.

IMG_9115.png, fév. 2022

4. Discussion

Comme vous avez pût le remarqué mes résultats finales n'inclût pas plusieurs Thymio. En effet, le programme était plus compliqué que ce que je pensais et j'ai donc dût réduire la taille de mon projet dans ce sens là. Un autre défaut auquel il faudrait remédier est que le système de position du Thymio ne marche pas tout le temps précisément. Conceptuellement, le système est fonctionnel mais il arrive qu'il y ait des erreurs qui fasse tout dérégler et finalement le Thymio n'est pas capable de précisément connaître la position du point. En effet, comme la fonction position() n'est que appelée lorsqu'un mur est détecté ce n'est pas le cas pour le point et si on essaie de mettre la fonction position() dans la boucle while cela engendre de nombreux problèmes. Il arrive aussi des fois que le Thymio, après avoir tourner plusieurs fois devant des murs, le Thymio se retrouvent trop parallèle à un côté et repartent sur le côté en face plutôt que celui qui lui est adjacent.
Si j'avais eu plus de temps je pense que j'aurais pu implanter un système de position optimisé et ainsi d'être beaucoup plus précis. J'aurais également pu incorporer plusieurs Thymio dans le projet et ainsi me rapprocher un peu plus d'un vrai "essaim" de Thymio.
Je suis tout de même très heureux avec mon résultat final car même si il ne rempli pas tous les critères que je mettait mis initialement, le Thymio est quand même capable d'accomplir plusieurs différentes tâches relativement complexes. Je suis aussi content de mon progrès en Python sachant qu'avant de commencer se projet je ne savais quasiment pas coder en python et je suis maintenant capable d'utiliser des booléens, des fonctions, des variables, importer des librairies, etc...

5. Conclusion

En conclusion le ThymioSearch à remplit la plupart de ses objectifs même si la partie "swarm computing" a été omise. Avec plus de temps à disposition, il serait évidemment possible de rajouter plus de complexité à se projet et d'en améliorer sa précision.

Si je reviens un jour sur ce projet je trouverai intéressant de peut-être utiliser un système de tracking d'image comme c'est un domaine dans la programmation que je trouve très intéressant.

J'ai pris un grand plaisir à faire se projet (même si j'ai parfois passé de longs moments d'incompréhension) et ce dernier m'a permis de décupler mes capacités à coder en Python. J'ai maintenant l'envie, et surtout les moyens, de faire des projets par moi même.

Références