Qui n’a jamais rêvé d’avoir un robot en LEGO qui lui annonce la météo ? Et bien même si ce n’est pas le cas, nous l’avons fait pour vous ! Venez découvrir un robot capable de comprendre son environnement pour vous donner la météo.
Sommaire
Fonctionnalités du robot
Notre robot au doux nom de ROBKEY est capable de :
- déplacer ses bras et son bassin ;
- reconnaitre l’endroit où il se trouve pour donner précisément la météo ;
- afficher l’heure, la météo et la température ambiante ;
- d’avoir quelques réactions faciales ;
- et bien plus.
La conception du robot
Pour concevoir notre robot, nous avons dû prendre en compte plusieurs choses. Tout d’abord, les moteurs n’ont pas été particulièrement dur à installer car il ne font que la moitié de la taille de l’esp 32. Il nous a donc paru évident de mettre les moteurs des bras dans le torse du robot et le moteur du bassin dans le châssis de ses chenilles.
Ensuite il a fallu installer planche de test qui a eu plusieurs positions possibles. Premièrement dans son dos car cela aurait permis de pouvoir faire tourner le bassin du robot à 360°, mais les câbles auraient difficilement bien tenu à l’esp 32 et au robot lui-même. Nous nous sommes finalement entendu à l’idée de mettre la planche sur la batterie des chenilles bien que cela empêche le robot de faire des tours sur lui même.
Maintenant, le plus important : le placement de l’ESP32. Nous avions deux idées, l’une le plaçait dans le torse du robot, l’autre faisait en sorte qu’il soit la tête légèrement rentrée dans les épaules. Nous avons opté pour la seconde option car celle-ci nous permet d’afficher un petit visage sur l’écran du microcontrôleur.
Pour conceptualiser l’apparence de notre robot, nous avons donc réalisé un schéma en 3D très simple sur Paint 3D en regroupant nos idées pour imaginer ce à quoi il ressemblera.
Construction de la structure du robot
Pour construire le robot, j’ai (Sylvain) commencé par regarder les constructions Lego que j’avais précédemment réalisées et qui pouvaient être détruite pour commencer à récupérer leurs pièces. Avec toutes ses matières premières je me suis laissé porter par mon imagination pour construire le LEGO le plus ressemblant à un robot en essayant de faire en sorte qu’il soit plutôt résistant (bon d’accord j’ai peut-être failli à cette tâche).
C’est grâce aux joies de la maladie que j’ai pu entamer la construction. Pour commencer, j’ai récupéré le châssis d’une autochenille radiocommandée en la modifiant pour la rendre plus compacte et en laissant un espace pour le moteur du bassin.
Ensuite j’ai commencé à faire ledit bassin, que j’élargis à la fin de la construction car je l’avais fait trop fin. Suivi du torse, le plus simple car il devait être creux pour laisser la places aux câbles et aux moteurs. Il fallait aussi préparer les trous pour les axes des bras.
Après j’ai fait les bras qui sont vraiment très simplistes mais à ce moment je commençais sérieusement à manquer de pièces plus techniques et même des plus simples pour faire quelque chose de plus sympathique. Mais quoi qu’il en est l’un des deux bras représente un soleil pour quand il fait beau, tandis que l’autre représente un ciel nuageux.
Pour finir j’ai réalisé la cage de la planche de test et solidifié les éléments les moins solides et résolu les quelques problèmes mécaniques rencontrés lors des premiers tests des moteurs.
L’électronique du robot
En parallèle de la construction du LEGO, je (Thomas) me suis lancé dans la conception électronique de celui-ci.
Pour cela, j’ai d’abord listé tout ce que je voulais intégrer à l’électronique :
- 2 boutons,
- 1 capteur de température,
- 3 moteurs.
Pour m’aider dans la réalisation électronique, je me suis aidé de tous ces sites :
- Utiliser les entrées/sorties de l’ESP32 avec MicroPython + Même chose
- Pour utiliser les pins en PWM + Même chose
- Branchement d’un interrupteur
- Résistance pull-up/pull-down + Même chose
- Mesurer la température et le taux d’humidité avec un capteur DHT11
- Les servomoteurs
Allez c’est parti !
On commence par le branchement des boutons :
Les câbles blancs (entourés en rouge) récupèrent l’information de la pression du bouton et les câbles noirs (entourés en vert) s’occupe de fermer le circuit (de l’électricité passent dans le bouton, et il faut que l’électricité puisse faire une boucle). Si vous voulez, les boutons reçoivent de l’énergie par les câbles blancs et l’expulsent par les câbles noirs.
Concernant le branchement du capteur :
A droite à quoi ressemble le capteur de température de face et à gauche son branchement. La câble de gauche, en blanc récupère les mesures du capteur, celui du milieu en marron, alimente positivement le capteur (le capteur reçoit de l’énergie par ce câble) et le câble noir à droite du câble marron lui ferme la boucle (le capteur expulse l’énergie par ce câble.)
Et pour finir, les moteurs ont 3 câbles :
Le câble orange sert à contrôler le moteur, le rouge à l’alimenter et le marron à fermer la boucle. On refait ça 3 fois puisqu’on a 3 moteurs.
On branche tout ceci à la planche de test puis à l’ESP32, et voilà montage terminé :
Programmation du robot
Maintenant que le robot est construit et le montage électronique assemblé, il faut passer à la programmation de l’ESP32. Veuillez noter que depuis l’écriture de ce qui suit, le code a bénéficié de quelques mises à jours. Vous retrouverez l’archive en fin d’article avec les fichiers à jours.
Tout d’abord, il faut coder les briques de bases, qui vont nous permettre de manipuler différents outils, capteurs, actionneurs, api, etc.
- C’est pour cela que nous créons un fichier
buttons.py
qui contient une classe Button dans laquelle nous initialisons 4 boutons : les 2 du microcontrôleur et les deux boutons ajoutés. Dans cette classe, vous verrez que chaque boutons a des caractéristiques supplémentaires. En effet il est possible de rendre un bouton poussoir équivalent à un levier. Nous créons différentes fonctions pour récupérer ou modifier des informations en rapport avec les boutons.
from machine import Pin from time import sleep # Importer les modules est inutile car ils sont déjà importés dans boot.py class Buttons(): def __init__(self): #self.name = "t-display-s3" # NomDuBouton : [ PinUtilisé , mode , value ] # 1 : [ Pin 3 en mode INPUT avec une résistance Pull-Down , levier , 0 ] # 1 : [ Pin(3, mode=Pin.IN, pull=Pin.PULL_DOWN), 1, 0 ] # Note : le chiffre de value dans la liste n'est pas pris en compte si le bouton est en mode "poussoir" -> On ne peut pas mettre None car si on décide de changer le mode du bouton, la valeur devient importante. # Pour une raison inconnue, "left" et "right" ne fonctionne plus. self.list_button = {"left" : [Pin(0, Pin.IN, Pin.PULL_UP),0,1], "right" : [Pin(14, Pin.IN, Pin.PULL_UP),0,1], 1 : [Pin(2, mode=Pin.IN, pull=Pin.PULL_UP),0,1], 2 : [Pin(3, Pin.IN, Pin.PULL_UP),0,1] } def getButton(self, button): return self.list_button[button] def setButton(self, button, pin, mode, value=None): # Le set fait également add. self.list_button[button] = [pin,mode,value] def setMode(self, button, val:bool): # Ce mode change le mode du bouton -> soit "levier" soit "poussoir" if isinstance(val, bool) or val in (0,1): self.list_button[button][1] = val if not(val): self.list_button[button][2] = 1 return print("Mode définie sur : " + str(self.list_button[button][1])) else: raise ValueError("val must be a boolean or a integer included between 0 and 1.") def setValue(self, button, value): if self.list_button[button][1]: self.list_button[button][2] = value return print("Valeur définie sur : " + str(self.list_button[button][2])) else: print("Button must be a toogle button, not a push button.") return False def getValue(self, button): # Renvoie la valeur du bouton if not(self.list_button[button][1]): # Si le statut de mode_toogle est égal à 0 <=> Si le bouton est en mode "poussoir" et non "levier" return self.list_button[button][0].value() else: if not(self.list_button[button][0].value()): self.list_button[button][2] = 0 if self.list_button[button][2] else 1 sleep(0.25) return self.list_button[button][2] def isButtonPressed(self,button): # Renvoie True si le bouton est pressé (donc si value = 0) et False sinon return not(self.getValue(button))
- Continuons avec le fichier
temperature.py
dans lequel nous avons une simple fonction qui nous renvoie les données récupérées par le capteur de température et d’humidité.
from time import sleep #Inutile car importé dans boot.py def getTemperatureAndHumidity(capteur): # Capteur doit etre sous cette forme : capteur = dht.DHT11(Pin(17)) -> Ne pas oublier d'importer le module dht try: capteur.measure() # Met à jour les données de températures et d'humidités sleep(1) return (capteur.temperature(),capteur.humidity()) except: print("Valeurs non récupérées") return False
- Egalement, à l’aide des fichiers
motor.py
etservo.py
(trouvé sur internet), nous créons des fonctions qui réaliseront des déplacements particuliers :
from servo import Servo import time motor=Servo(pin=12) # A changer selon la broche utilisée motorbd=Servo(pin=10) motorbg=Servo(pin=11) def droit(t=1): motor.move(285) time.sleep(t) def bassin(t=2): motor.move(265) time.sleep(t) motor.move(305) time.sleep(t) def baissebg(t=0.5): motorbg.move(335) time.sleep(t) def levebg(t=0.5): motorbg.move(270) time.sleep(t) def baissebd(t=0.5): motorbd.move(335) time.sleep(t) def levebd(t=0.5): motorbd.move(270) time.sleep(t) droit()
- Pour finir, à l’aide du fichier
connection.py
, nous créons plusieurs fonctions en rapport avec la partie sans-fil de l’ESP32.
import network import st7789 #En théorie inutile car déjà importé dans boot.py from time import sleep def connect_to_wifi(display, font, ssid, mdp): wlan = network.WLAN(network.STA_IF) wlan.active(True) if not wlan.isconnected(): display.init() display.draw(font,"Connexion a un point",0,111) display.draw(font,"d'acces en cours",15,126) wlan.connect(ssid,mdp) while not wlan.isconnected(): pass display.fill_rect(0,107,170,25,st7789.BLACK) display.draw(font,'Connecte !',40,111) sleep(0.8) display.fill_rect(0,107,170,10,st7789.BLACK) display.deinit() def search_wlan(): station = network.WLAN(network.STA_IF) station.active(True) return station.scan()
Passons aux modules qui vont utiliser des apis :
- En premier lieu, nous avons le fichier
geoloc.py
dans lequel nous utilisons l’api de google qui à l’aide des réseaux wifi aux alentours est capable de situer l’ESP32. Dans ce fichier est présent une fonction qui récupère les coordonnées géographiques de l’ESP32.
from modules.connection import search_wlan import ustruct as struct import urequests as requests import ujson as json import modules.api_txt as api_txt def getLocation(api_key): # Avec Google maps API + sécurité sur le nombre de requête. if int(api_txt.get_api_counter()[0]) > 24000: print("Quota de demande dépassé !!! Vous ne pouvez pas faire de requête...") return False list_wlan = search_wlan() data = { "considerIp": False, "wifiAccessPoints": [] } for wifi in list_wlan: entry = { "macAddress": "%02x:%02x:%02x:%02x:%02x:%02x" % struct.unpack("BBBBBB", wifi[1]), "signalStrength": wifi[3], "channel": wifi[2] } data["wifiAccessPoints"].append(entry) headers = {"Content-Type": "application/json"} url = "https://www.googleapis.com/geolocation/v1/geolocate?key=" + api_key response = requests.post(url, headers=headers, data=json.dumps(data)) api_txt.add_api_counter(0) return json.loads(response.content)["location"]
- En second et dernier lieu, il reste le fichier
meteo.py
qui va être capable d’utiliser une api de météorologie pour récupérer, analyser et mettre en forme des données en rapport avec la météo.
import urequests import modules.api_txt as api_txt def get_meteo(latitude, longitude): # Sur la prochaine heure if int(api_txt.get_api_counter()[1]) > 10000: print("Quota de demande dépassé !!! Vous ne pouvez pas faire de requête...") return False url = "https://api.open-meteo.com/v1/forecast?latitude={0}&longitude={1}¤t=temperature_2m,precipitation&daily=temperature_2m_max,temperature_2m_min,precipitation_sum&timezone=auto&forecast_days=3".format(latitude,longitude) json_meteo = urequests.get(url).json() api_txt.add_api_counter(1) return {"current": {"temperature": [ json_meteo["current"]["temperature_2m"], json_meteo["current_units"]["temperature_2m"] ], "precipitation": [ json_meteo["current"]["precipitation"], json_meteo["current_units"]["precipitation"] ] }, "demain": {"temperature": [round((json_meteo["daily"]["temperature_2m_min"][1] + json_meteo["daily"]["temperature_2m_max"][1]) / 2, 1), json_meteo["daily_units"]["temperature_2m_min"]], "precipitation": [ json_meteo["daily"]["precipitation_sum"][1], json_meteo["daily_units"]["precipitation_sum"] ] }, "apres-demain": {"temperature": [round((json_meteo["daily"]["temperature_2m_min"][2] + json_meteo["daily"]["temperature_2m_max"][2]) / 2, 1), json_meteo["daily_units"]["temperature_2m_min"]], "precipitation":[ json_meteo["daily"]["precipitation_sum"][2], json_meteo["daily_units"]["precipitation_sum"] ] } }
Passons à la gestion graphique, nous avons récupérer le module tft_config.py
fournit par les développeurs du pilotes graphiques pour l’esp32. Celui-ci permet de créer facilement une gestion de l’écran avec les bonnes caractéristiques. Nous n’afficherons pas le code ici, car ce n’est pas nous qui l’avons codé. Vous pouvez le retrouver dans l’archive à la fin de cet article ou ici.
Nous avons créé le fichier affichage.py
qui contient un certain nombre de fonctions pour réaliser le rendu graphique pour l’ESP32. Il contient également le code qui récupère des images pour les animations du visage et des icones. Ces dessins et animations ont été réalisé par Ilana, ancienne première NSI.
import st7789 from time import sleep, localtime import modules.motor as motor last_temps = -1 def display_time(display,font,x,y): global last_temps temps = localtime() if last_temps < temps[5]: display.png("/images/icones/horloge.png",x,y,True) display.fill_rect(x+30,y+5,60+6,10,st7789.BLACK) display.draw(font, str(temps[3]) + ":" + str(temps[4]) + ":" + str(temps[5]),x+30,y+10) last_temps = temps[5] def display_meteo(display,font,infos_meteo, x, y): display.draw(font,str(infos_meteo["temperature"][0]),x,y) display.draw(font, font.LAST,x + display.draw_len(font,str(infos_meteo["temperature"][0])), y) display.draw(font, " C",x + display.draw_len(font,str(infos_meteo["temperature"][0])), y) display.draw(font,str(infos_meteo["precipitation"][0]),x+80,y) display.draw(font,str(infos_meteo["precipitation"][1]),x + 80 + display.draw_len(font,str(infos_meteo["precipitation"][0])),y) def display_temperature(display,font,infos_temperature,x,y): display.fill_rect(x,y,70,20,st7789.BLACK) display.png("/images/icones/temperature.png",x,y,True) display.draw(font,str(infos_temperature) + " C",x+30,y+9) display.draw(font,font.LAST,x+30+display.draw_len(font,str(infos_temperature)), + y+9) def meteo_icon(display,icone,x,y): display.fill_rect(x,y,50,50,st7789.BLACK) if icone == "pluie": display.png("/images/icones/mauvais_temps.png",x,y,True) #Nuage pluie gris pas content else: display.png("/images/icones/beau_temps.png",x,y,True) # Soleil content def meteo_widget(display,font,infos_meteo,x,y): display.fill_rect(x,y,170,200,st7789.BLACK) display.draw(font, "Auj.",x,y) display_meteo(display,font,infos_meteo["current"],x, y+20) #x,y+20) display.draw(font, "Dem.", x, y+40) #x+50, y) display_meteo(display,font,infos_meteo["demain"], x, y+60) #x+50,y+20) display.draw(font, "Apr-Dem.", x, y+80) #x+90, y) display_meteo(display,font,infos_meteo["apres-demain"], x, y+100) #x+90,y+20) def main_menu(display,font,infos_temp,infos_meteo): display_time(display,font, 10, 111) # x = 52 pour un semblant de centrage display_temperature(display,font,infos_temp,10,151) if infos_meteo["current"]["precipitation"][0] > 0: icone = "pluie" motor.baissebg() motor.levebd() else: icone = "soleil" motor.baissebd() motor.levebg() meteo_widget(display,font,infos_meteo,10,191) meteo_icon(display,icone,110,111) def animation(display,name:str,iteration:int,i_fixe:int): # list_anim : {"content":(2,1),"endormi":(4,1),"somnole":(3,1),"observe":(0,0),"monocle":(2,1)} display.fill_rect(0,0,170,105,st7789.BLACK) for i in range(2,iteration+1): display.png("/images/" + name + "/" + name + str(i) + ".png",0,0) if name == "endormi": sleep(0.3) for i in range(iteration,0,-1): display.png("/images/" + name + "/" + name + str(i) + ".png",0,0) if name == "endormi": sleep(0.3) display.png("/images/" + name + "/" + name + str(i_fixe) + ".png",0,0)
Pour finir nous avons le fichier api_txt.py
qui s’occupe de gérer des fichiers de textes importants pour le fonctionnement du programme, présents dans le dossier data
.
""" api[0] => Google Maps API api[1] => Météo API """ def get_api_counter(): fichier_r = open("data/counter-api.txt",'r') liste = fichier_r.read().split(",") fichier_r.close() return liste def add_api_counter(indice_api): # ne fonctionne qu'avec deux apis fichier_w = open("data/counter-api.txt",'w') val = get_api_counter() val[indice_api] = int(val[indice_api]) + 1 fichier_w.write(str(val[0]) + "," + str(val[1])) fichier_w.close() def set_api_counter(indice_api,value): # ne fonctionne qu'avec deux apis fichier_w = open("data/counter-api.txt",'w') api_value = get_api_counter() api_value[indice_api] = str(value) fichier_w.write(api_value[0] + "," + api_value[1]) fichier_w.close()
Maintenant que toutes nos briques sont construites, il ne reste qu’à écrire le fichier python principal qui va organiser tout le programme tel un chef d’orchestre.
Assemblage final
Maintenant, il faut simplement assembler notre montage électronique à notre construction LEGO. Puis à brancher notre ESP32 à un câble, lui injecter le code, et tout en le gardant brancher, le lancer, et la magie opère !
Fichiers
Veuillez noter que vous devrez ajouter votre propre clé api Google Maps API pour que la position soit mise à jour (dans boot.py
à la ligne 21). Sinon vous pouvez changer manuellement la position dans le fichier old-location.txt
se trouvant dans le dossier data
. Veuillez respecter l’ordre suivant et ne surtout rien ajouter d’autre dans le fichier, pas même un espace :
latitude,longitude
Vous devrez également ajouter vos propres informations pour un point d’accès dans le fichier boot.py
au niveau de la ligne 19 : wlan_info = ("SSID","MDP")
Le projet ne fonctionne pas sans une connexion à internet !!
Étudiant en spécialité NSI en classe de Terminal depuis 2023.
Sûrement dans les tréfonds du net… (restez à l’affût, on sait jamais)