Jeu labyrinthe avec touch pad : Différence entre versions

De Les Fabriques du Ponant
Aller à : navigation, rechercher
Ligne 364 : Ligne 364 :
 
</syntaxhighlight>
 
</syntaxhighlight>
 
===Explications===
 
===Explications===
 +
====Chargement du programme sur le D1 mini Wemos====
 +
Voici les étapes à suivre afin de pourvoir charger [[#C++|programme]] sur le D1 mini Wemos :
 +
* Lancez le programme Arduino IDE ;
 +
* Ajoutez une carte supplémentaire à partir de menu Fichier > Préférences ;
 +
* Dans l'onglet "Paramètres" cliquez sur le bouton situé en face de "URL de gestionnaire de cartes supplémentaires" (voir image ci-dessous) ;
 +
[[Image:Config ide arduino esp8266.jpg|500px|center]]
 +
* Ajoutez le lien suivant dans la fenêtre qui s'est ouverte et validez : <nowiki>https://arduino.esp8266.com/stable/package_esp8266com_index.json</nowiki>
 +
* Copiez le [[#C++|code C++]] ;
 +
* Dans le menu "Outils > Carte", sélectionnez "esp8266 > LOLIN (WEMOS) D1 R2 & Mini", ainsi que le port série (menu Outils > Port);
 +
* Compilez et téléversez vers le D1 Mini Wemos.
 
====Fonctionnement général====
 
====Fonctionnement général====
 
====Servomoteurs====
 
====Servomoteurs====

Version du 2 avril 2024 à 09:26

Description

Labyrinthe-retro-game.jpg

Qui ne connaît pas le jeu du labyrinthe à bille : Un jeu qui consiste à faire sortir une bille d'un labyrinthe en utilisant deux molettes permettant d'incliner le plateau du labyrinthe vers le haut ou le bas, et vers la droite ou la gauche, le tout en évitant de faire tomber la bille dans les trous qui parsèment le chemin.

Ce projet a donc eu pour objectif de pouvoir jouer au jeu du labyrinthe en commandant les inclinaisons du plateau grâce à un touch pad depuis un téléphone portable.

Liste du matériel

Électronique

Impression 3D

Les fichiers nécessaires à l'impression 3D du modèle utilisé dans ce tutoriel sont accessibles A COMPLETER

Coût

Pour l'ensemble du projet, il faut compter un coût global électronique + impression 3D de 15 euros environ.

Réalisation du projet

La réalisation de ce projet est relativement simple. Elle se résume en 4 étapes :

  1. Initier les impressions 3D selon les modèles de fichiers listés dans la section Impression 3D ;
  2. Réaliser le câblage du D1 mini Wemos avec les servomoteurs selon le schéma de câblage fourni ci-dessous dans la partie Schéma ;
  3. Télécharger le programme dans le D1 mini Wemos ;
  4. Jouer.

Schéma

Code

Le code nécessaire à ce projet se sépare en deux parties :

  1. Web (HTML/CSS/JavaScript) permettant d'afficher un touch pad sur le téléphone portable et de commander l'inclinaison du plateau du labyrinthe ;
  2. C++ (Arduino) permettant de programmer le D1 mini Wemos pour qu'il ouvre une connexion WIFI et adresse les commandes reçues aux servomoteurs contrôlant l'inclinaison du plateau du labyrinthe.

Les codes HTML, CSS et Javascript sont déjà insérés dans le code C++ sans commentaire et sans mise en forme (C++ oblige). Cependant, les codes HTML, CSS et JavaScript ont été ventilés ci-dessous afin d'en faciliter la lecture et la compréhension ainsi que celles des commentaires.

Pour les codes Javascript et C++, certaines lignes sont surlignées en jaune. Ces lignes identifient les paramètres qui peuvent être modifiés sans risque afin de s'adapter aux spécificités et choix faits pour votre projet.

HTML

 1 <!DOCTYPE html>
 2 <html lang='fr'>
 3 <head>
 4     <meta charset='UTF-8'>
 5     <meta name='viewport' content='width=device-width, initial-scale=1.0'>
 6     <title>Jeu du labyrinthe</title>
 7 </head>
 8 <body>
 9     <!-- Affiche le touch pad -->
10     <div id='pad'>
11         <!-- Affiche une croix au centre du touch pad -->
12         <div id='centrePad'>+</div>
13     </div>
14 </body>
15 </html>

CSS

 1 <style>
 2     /* Dessine la forme/couleur du touch pad */
 3     #pad {
 4         background-color: #f0f0f0;
 5         border-radius: 50%;
 6         position: relative;
 7         user-select: none;
 8     }
 9     /* Positionne la croix au milieu du touch pad et définit la taille de la croix */
10     #centrePad {
11         position: absolute;
12         top: 50%;
13         left: 50%;
14         transform: translate(-50%, -50%);
15         font-size: 24px;
16     }
17 </style>

JavaScript

 1 <script>
 2     const pad = document.getElementById('pad');
 3     // Récupère le rectangle dans lequel s'inscrit le touch pad par rapport au 0,0 (angle supérieur gauche) de la page web
 4     const rect = pad.getBoundingClientRect();
 5     // Permet de stocker la vitesse sur les axes x et y par rapport à la position du doigt sur le touch pad
 6     let vitesse = [0, 0];
 7     // Définit la vitesse max autorisée pour les deux axes - Valeur modifiable
 8     let vitesseMax = 4;
 9     // Définit la taille du touch pad en pixel - Valeur modifiable
10     let taillePad = 360;
11     /***********************************************************
12      Détermine le nombre de vitesses qu'il y a sur chaque axe
13      Il faut prévoir aussi bien le mouvement en positif qu'en
14      négatif, et ne pas oublier 0 (pas de mouvement).
15      Avec vitesseMax = 4, on obtient 9 vitesses possibles :
16      [-4 ; -3 ; -2 ; -1 ; 0 ; 1 ; 2 ; 3 ; 4]
17     ***********************************************************/
18     let nbVitesses = vitesseMax * 2 + 1;
19     // Détermine le ratio qu'occupe chaque vitesse sur le touch pad au niveau du diamètre
20     let pallierVitesse = taillePad / nbVitesses;
21     let timerId = null;
22 
23     // Applique la taille au touch pad
24     pad.style.width = pad.style.height = taillePad + 'px';
25 
26     // Fonction envoyant la consigne de vitesse sur les axes X et Y au D1 mini Wemos
27     function envoiMouvement() {
28         const xhr = new XMLHttpRequest();
29         xhr.open('Mouvement', 'mouvement?x=' + vitesse[0] + '&y=' + vitesse[1], true);
30         xhr.send();
31     }
32 
33     /*******************************************************************
34      Evènement qui se déclenche lorsque le doigt touche le touch pad
35      1/ Récupère la position relative du doigt par rapport au
36      touch pad et ne garde que la partie entière
37      2/ Convertit les positions X et Y du doigt en vitesses demandées
38      3/ Envoie les consignes de vitesses au D1 mini Wemos
39      4/ Déclenche un timer afin d'adresser toutes les 30ms les consignes
40      de vitesses au D1 mini Wemos (voir la section "explications")
41     *******************************************************************/
42     pad.addEventListener('touchstart', (position) => {
43         vitesse[0] = Math.trunc((position.touches[0].clientX - rect.left)/pallierVitesse) - vitesseMax;
44         vitesse[1] = vitesseMax - Math.trunc((position.touches[0].clientY - rect.top)/pallierVitesse);
45         envoiMouvement();
46         timerId = setInterval(envoiMouvement, 30);
47     });
48 
49     /*******************************************************************
50      Evènement qui se déclenche lorsque le doigt bouge sur l'écran
51      1/ Vérifie si le doigt est toujours sur le touch pad
52      1.1/ Si non, ne fait rien
53      1.2/ Si oui, actualise les consignes de vitesse X et Y
54     *******************************************************************/
55     pad.addEventListener('touchmove', (position) => {
56         if(position.touches[0].clientX < rect.left || position.touches[0].clientX > (taillePad + rect.left) ||
57             position.touches[0].clientY < rect.top || position.touches[0].clientY > (taillePad + rect.top))
58                 return;
59 
60         vitesse[0] = Math.trunc((position.touches[0].clientX - rect.left)/pallierVitesse) - vitesseMax;
61         vitesse[1] = vitesseMax - Math.trunc((position.touches[0].clientY - rect.top)/pallierVitesse);
62     });
63 
64     /*******************************************************************
65      Evènement qui se déclenche lorsque le doigt ne touche plus l'écran
66      1/ Met les consignes de vitesses X et Y à 0
67      2/ Stoppe le timer qui envoie les consignes de vitesses au D1 mini
68      3/ Envoie les consignes de vitesses à 0 au D1 mini
69     *******************************************************************/
70     pad.addEventListener('touchend', (position) => {
71         vitesse[0] = vitesse[1] = 0;
72         clearInterval(timerId);
73         envoiMouvement();
74         timerId = null;
75     });
76 </script>

C++

Les lignes 65 et 66 du code C++ surlignées en jaune correspondent aux lignes 8 et 10 du code JavaScript ci-dessus.

  1 // Ajout des bibliothèques permettant de gérer le module D1 mini Wemos et de créer un point d'accès Wifi
  2 #include <ESP8266WiFi.h>
  3 #include <ESP8266WebServer.h>
  4 // Ajout de la bibliothèque permettant de contrôler les servomoteurs
  5 #include <Servo.h>
  6 /*****************************************************/
  7 /*** CODE PERMETTANT DE JOUER AU JEU DU LABYRINTHE ***/
  8 /*** CE CODE EST PARAMETRE POUR FONCTIONNER AVEC   ***/
  9 /*** UN MODULE WIFI D1 MINI WEMOS.                 ***/
 10 /*****************************************************/
 11 
 12 //declaration du serveur web sur le port 80
 13 ESP8266WebServer server(80);
 14 
 15 //Nom du réseau wifi local (Ce qu'on appelle le SSID).
 16 const char* nomDuReseau = "labyrinthe";
 17 //Mot de passe du réseau wifi local
 18 const char* motDePasse = "labyrinthe";
 19 
 20 // Contiendra la page web que l'on renverra au client
 21 String pageWeb;
 22 
 23 // nb de servo moteurs, 1 pour les mouvements sur l'axe X et 1 pour l'axe Y
 24 const byte nbServo = 2;
 25 // stocke l'angle des servo
 26 byte angleServo[nbServo] = {90, 90};
 27 // créer nbServo objets "monServo" pour les contrôler
 28 Servo monServo[nbServo];
 29 // Correspond aux pins des servo
 30 byte pinServo[nbServo] = {D2, D5}; // mettre le numéro des broches sur lesquelles sont connectées les servo moteurs
 31 
 32 // On forge la page html qui va permettre de commander les servo moteurs à distance via le wifi
 33 void pageIndex() {
 34   pageWeb = "<!DOCTYPE html>\
 35 <html lang='fr'>\
 36 <head>\
 37 <meta charset='UTF-8'>\
 38 <meta name='viewport' content='width=device-width, initial-scale=1.0'>\
 39 <title>Jeu du labyrinthe</title>\
 40 <style>\
 41 /* Styles pour le pad de direction */\
 42 #pad {\
 43 background-color: #f0f0f0;\
 44 border-radius: 50%;\
 45 position: relative;\
 46 user-select: none;\
 47 }\
 48 #centrePad {\
 49 position: absolute;\
 50 top: 50%;\
 51 left: 50%;\
 52 transform: translate(-50%, -50%);\
 53 font-size: 24px;\
 54 }\
 55 </style>\
 56 </head>\
 57 <body>\
 58 <div id='pad'>\
 59 <div id='centrePad'>+</div>\
 60 </div>\
 61 </body>\
 62 <script>\
 63 const pad = document.getElementById('pad');\
 64 const rect = pad.getBoundingClientRect();\
 65 let vitesseMax = 4;\
 66 let taillePad = 360;\
 67 let vitesse = [0, 0];\
 68 let nbVitesses = vitesseMax * 2 + 1;\
 69 let pallierVitesse = taillePad / nbVitesses;\
 70 let timerId = null;\
 71 pad.style.width = pad.style.height = taillePad + 'px';\
 72 function envoiMouvement() {\
 73 const xhr = new XMLHttpRequest();\
 74 xhr.open('Mouvement', 'mouvement?x=' + vitesse[0] + '&y=' + vitesse[1], true);\
 75 xhr.send();\
 76 }\
 77 pad.addEventListener('touchstart', (position) => {\
 78 vitesse[0] = Math.trunc((position.touches[0].clientX - rect.left)/pallierVitesse) - vitesseMax;\
 79 vitesse[1] = vitesseMax - Math.trunc((position.touches[0].clientY - rect.top)/pallierVitesse);\
 80 envoiMouvement();\
 81 timerId = setInterval(envoiMouvement, 30);\
 82 });\
 83 pad.addEventListener('touchmove', (position) => {\
 84 if(position.touches[0].clientX < rect.left || position.touches[0].clientX > (taillePad + rect.left) ||\
 85 position.touches[0].clientY < rect.top || position.touches[0].clientY > (taillePad + rect.top))\
 86 return;\
 87 vitesse[0] = Math.trunc((position.touches[0].clientX - rect.left)/pallierVitesse) - vitesseMax;\
 88 vitesse[1] = vitesseMax - Math.trunc((position.touches[0].clientY - rect.top)/pallierVitesse);\
 89 });\
 90 pad.addEventListener('touchend', (position) => {\
 91 vitesse[0] = vitesse[1] = 0;\
 92 clearInterval(timerId);\
 93 envoiMouvement();\
 94 timerId = null;\
 95 });\
 96 </script>\
 97 </html>";
 98 }
 99 
100 // Envoie au client la page html permettant de commander les servo moteurs
101 void handleIndex() {
102   Serial.println("index");
103   // prépare le html qui sera envoyé au client
104   pageIndex();     
105   // envoie la page HTML au client
106   server.send(200,"text/html", pageWeb);
107 }
108 
109 // Applique les nouveaux angles aux servomoteurs
110 void majMouvement() {
111   // On vérifie qu'on a des arguments qui ont été envoyés
112   if(!server.args()) return;
113   // Le premier argument correspond au mouvement sur l'axe x.
114   // On vérifie que c'est bien le cas, et on récupère la valeur du mouvement.
115   if(server.argName(0) == String("x"))
116   {
117     // Conversion de la valeur reçue sous format texte en nombre entier et on l'ajoute à la valeur de l'angle du servoO
118     angleServo[0] += server.arg(0).toInt();
119     // On vérifie si la valeur reçue est positive ou négative
120     if(server.arg(0).toInt() > 0) {
121       // si l'angle est supérieur à 180, on la ramène à 180
122       if(angleServo[0] > 180) 
123         angleServo[0] = 180;
124     }
125     else // la valeur reçue est négative (voir la section "Explications")
126       /**************************************************************************************
127        La variable angleServo est déclarée avec un type "byte". Ce typage correspond
128        à une valeur non signée pouvant recevoir des valeurs comprises entre 0 et 255 inclus.
129        Cela signifie qu'elle ne peut pas être négative. Aussi, lorsqu'elle passe en-dessous
130        de 0 lors d'une opération mathématique, elle ne sera pas négative, mais correspondra
131        à la valeur max qu'elle peut stockée (255 ici) moins la valeur négative calculée.
132        Dans notre cas, lorsque la valeur reçue est négative (c.a.d server.arg(x) < 0) :
133            1. elle est soustraite à "angleServo" (voir ligne 118 ci-dessus) ;
134            2. si cette soustraction est sensée donnée un résultat négatif (ex: -5) ;
135            3. alors ce n'est le résultat négatif qui sera retourné, mais 250 (255 - 5)
136            4. dans ce cas, la valeur 0 sera assignée à "angleServo".
137       **************************************************************************************/
138       if(angleServo[0] > 180) 
139         angleServo[0] = 0;
140   }
141   
142   // on refait le même travail, mais pour l'axe y cette fois-ci.
143   if(server.argName(1) == String("y"))
144   {
145     angleServo[1] += server.arg(1).toInt();
146     if(server.arg(1).toInt() > 0) {
147       if(angleServo[1] > 180) 
148         angleServo[1] = 180;
149     }
150     else
151       if(angleServo[1] > 180)
152         angleServo[1] = 0;
153   }
154   // on applique les nouveaux angles aux servo moteurs.
155   monServo[0].write(angleServo[0]);
156   monServo[1].write(angleServo[1]);
157 }
158 
159 // pour le debug
160 void handleInconnu() {
161 }
162 
163 // crée le point d'accès wifi
164 void creerAccesWifi() {
165   WiFi.mode(WIFI_AP);
166   WiFi.softAP(nomDuReseau, motDePasse);
167   Serial.print("@IP du module wifi : ");
168   // permet d'afficher l'@IP du point d'accès sur le port série
169   Serial.println(WiFi.softAPIP());
170   // permet de traiter les requêtes HTML reçues
171   server.on("/", handleIndex);
172   server.on("/mouvement", majMouvement);
173   server.onNotFound(handleInconnu);
174   // on démarre le point d'accès
175   server.begin();
176   // Configure la broche D4 (GPIO 2) sur la quelle est branchée la LED_BUILTIN en sortie
177   pinMode(LED_BUILTIN, OUTPUT);
178   // Allume la LED_BUILTIN, signifiant que le wifi est activé
179   digitalWrite(LED_BUILTIN, LOW);  
180 }
181 
182 void setup() {
183   // on utilise le port série pour afficher l'adresse ip du point d'accès wifi (normalement : 192.168.4.1)
184   Serial.begin(9600);
185   // on crée le point d'accès wifi
186   creerAccesWifi();
187   // On initialise les servo moteurs pour avoir les plateaux à plat
188   /**************************************************************************************
189    Particularité du D mini Wemos (voir la section "explications")
190    Pour que le servo ait un débattement d'angle entre 0° et 180°, il faut configurer
191    les délais d'impulsion des servos avec min à 500 et max à 2600.
192    Cela est dû à la fréquence PWM du D1 mini Wemos qui est inconnue de la bibliothèque
193    <Servo.h>, et qui applique ainsi une configuration par défaut lors de l'appel
194    de la fonction "Servo::attach".
195    De plus, pour le labyrinthe, il n'est pas nécessaire d'avoir toute cette amplitude
196    de débattement (0° - 180°). Donc, on va garder le nombre de pas de 0 à 180, mais sur
197    un débattement d'environ 10° seulement pour chaque plateau du labyrinthe (+/- 5° par
198    rapport aux 0° des plateaux).
199   **************************************************************************************/
200   monServo[0].attach(pinServo[0], 1410, 1530);
201   monServo[0].write(angleServo[0]);
202   monServo[1].attach(pinServo[1], 1530, 1650);
203   monServo[1].write(angleServo[1]);
204 }
205 
206 void loop() {
207   // permet de gérer les connexions des clients
208   server.handleClient();
209 }

Explications

Chargement du programme sur le D1 mini Wemos

Voici les étapes à suivre afin de pourvoir charger programme sur le D1 mini Wemos :

  • Lancez le programme Arduino IDE ;
  • Ajoutez une carte supplémentaire à partir de menu Fichier > Préférences ;
  • Dans l'onglet "Paramètres" cliquez sur le bouton situé en face de "URL de gestionnaire de cartes supplémentaires" (voir image ci-dessous) ;
Config ide arduino esp8266.jpg
  • Ajoutez le lien suivant dans la fenêtre qui s'est ouverte et validez : https://arduino.esp8266.com/stable/package_esp8266com_index.json
  • Copiez le code C++ ;
  • Dans le menu "Outils > Carte", sélectionnez "esp8266 > LOLIN (WEMOS) D1 R2 & Mini", ainsi que le port série (menu Outils > Port);
  • Compilez et téléversez vers le D1 Mini Wemos.

Fonctionnement général

Servomoteurs

Code JavaScript

Choix de la mise en place d'un timer

La solution initialement mise en place pour contrôler les plateaux du labyrinthe n'était pas adaptée. Il s'agissait de l'envoi des consignes de vitesses dès que l'utilisateur déplaçait sont doigt sur le touch pad. Le problème qui s'est présenté est que le D1 mini Wemos reçoit trop de requêtes par rapport à la vitesse d'exécution et surtout à la vitesse de déplacement des servomoteurs. Cela avait pour conséquence de mettre des requêtes en attente dans la mémoire du D1 mini Wemos et de créer au fur et à mesure une très forte latence entre le moment où le doigt était déplacé sur le touch pad et le moment où ce déplacement était effectivement réalisé par les servomoteurs.

Dux solutions étaient alors possibles :

  1. Déclenchement d'un timer Javascript envoyant à intervalle régulier ces consignes ;
  2. Régulation des requêtes web reçues par le D1 mini Wemos dans le code C++.

Première solution : la mise en place d'un timer. A intervalle régulier, le timer vient déclencher l'envoi des consignes de vitesses vers le D1 mini Wemos par rapport aux dernières informations connues quant à la position du doigt sur le touch pad. Après plusieurs essais, un intervalle de 30 millisecondes apparaît être un choix acceptable entre le taux de rafraîchissement de l'envoi des consignes, le temps de traitement de ces consignes par le D1 mini Wemos et surtout le temps de déplacement requis par les servomoteurs.

Quant à la seconde solution, puisque la première solution était satisfaisante, la régulation des requêtes web au niveau du D1 mini Wemos n'a pas été testée.

Voici ce que cela donne (extrait du code JavaScript) :

 1 pad.addEventListener('touchstart', (position) => {
 2     vitesse[0] = Math.trunc((position.touches[0].clientX - rect.left)/pallierVitesse) - vitesseMax;
 3     vitesse[1] = vitesseMax - Math.trunc((position.touches[0].clientY - rect.top)/pallierVitesse);
 4     envoiMouvement();
 5     timerId = setInterval(envoiMouvement, 30);
 6 });
 7 
 8 pad.addEventListener('touchmove', (position) => {
 9     if(position.touches[0].clientX < rect.left || position.touches[0].clientX > (taillePad + rect.left) ||
10         position.touches[0].clientY < rect.top || position.touches[0].clientY > (taillePad + rect.top))
11         return;
12     vitesse[0] = Math.trunc((position.touches[0].clientX - rect.left)/pallierVitesse) - vitesseMax;
13     vitesse[1] = vitesseMax - Math.trunc((position.touches[0].clientY - rect.top)/pallierVitesse);
14 });
  • Lignes 1 & 5 : à partir du moment où l'utilisateur touche la zone correspondant au touch pad, cela déclenche un évènement "touchstart" qui va, entre autres, mettre en place un timer qui exécutera toutes les 30 millisecondes l'envoi des consignes de vitesses vers le D1 mini Wemos.
  • Ligne 8 : dès que l'utilisateur bouge le doigt, cela déclenche un évènement "touchmove". Cet évènement va uniquement réaliser une mise à jour des consignes de vitesses. Aucun envoi de ces nouvelles consignes vers le D1 mini Wemos n'est réalisé par cet évènement. Cet envoi est effectué par le déclenchement du timer toutes les 30 millisecondes.

Code C++

Particularité du D mini Wemos

  Pour que le servo ait un débattement d'angle entre 0° et 180°, il faut configurer
  les délais d'impulsion des servos avec min à 500 et max à 2600.
  Cela est dû à la fréquence PWM du D1 mini Wemos qui est inconnue de la bibliothèque
  <Servo.h>, et qui applique ainsi une configuration par défaut lors de l'appel
  de la fonction "Servo::attach".
  De plus, pour le labyrinthe, il n'est pas nécessaire d'avoir toute cette amplitude
  de débattement (0° - 180°). Donc, on va garder le nombre de pas de 0 à 180, mais sur
  un débattement d'environ 10° seulement pour chaque plateau du labyrinthe (+/- 5° par
  rapport aux 0° des plateaux).
  Ici, le plateau commandé par le servo0 a un 0° correspondant au 83° du servo0 (lorsque le servo0 a un débattement entre 0° et 180°), donc on va
 // lui permettre un débattement entre 78° et 88° étalé sur 180 pas.
 // Pour le plateau commandé par le servo1, le 0° correspond au 93° du servo1, donc on va lui permettre un débattement entre 88° et 98° étalé sur 180 pas.
 // Cela a pour conséquences :
 //      1- qu'en envoyant aux servos la commande Servo::write(90), les plateaux du labyrinthe seront sur leurs 0° ;
 //      2- que le débattement d'angle des servos sur les 10° qu'on leur a autorisé sera étalé sur 180 pas (avec pour le servo0 : "servo0.write(0)" = 78°
 //         d'angle, et "servo0.write(180)" = 88° d'angle)

Pour aller plus loin