Jeu labyrinthe avec touch pad : Différence entre versions
Ligne 68 : | Ligne 68 : | ||
</syntaxhighlight> | </syntaxhighlight> | ||
====JavaScript==== | ====JavaScript==== | ||
− | <syntaxhighlight lang="JavaScript" line highlight=" | + | <syntaxhighlight lang="JavaScript" line highlight="8,18"> |
<script> | <script> | ||
const pad = document.getElementById('pad'); | const pad = document.getElementById('pad'); | ||
+ | // 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 | ||
const rect = pad.getBoundingClientRect(); | const rect = pad.getBoundingClientRect(); | ||
// Permet de stocker la vitesse sur les axes x et y par rapport à la position du doigt sur le touch pad | // Permet de stocker la vitesse sur les axes x et y par rapport à la position du doigt sur le touch pad | ||
Ligne 86 : | Ligne 87 : | ||
// Définit la taille du touch pad en pixel - Valeur modifiable | // Définit la taille du touch pad en pixel - Valeur modifiable | ||
let taillePad = 180; | let taillePad = 180; | ||
+ | // Détermine le ratio qu'occupe chaque vitesse sur le touch pad au niveau du diamètre | ||
let pallierVitesse = taillePad / nbVitesses; | let pallierVitesse = taillePad / nbVitesses; | ||
let timerId = null; | let timerId = null; | ||
+ | // Applique la taille au touch pad | ||
pad.style.width = pad.style.height = taillePad + 'px'; | pad.style.width = pad.style.height = taillePad + 'px'; | ||
+ | // Fonction envoyant la consigne de vitesse sur les axes X et Y au D1 mini Wemos | ||
function envoiMouvement() { | function envoiMouvement() { | ||
const xhr = new XMLHttpRequest(); | const xhr = new XMLHttpRequest(); | ||
Ligne 97 : | Ligne 101 : | ||
} | } | ||
+ | /******************************************************************* | ||
+ | Evènement qui se déclenche lorsque le doigt touche le touch pad | ||
+ | 1/ Récupère la position relative du doigt par rapport au | ||
+ | touch pad et on ne garde que la partie entière | ||
+ | 2/ Convertit les positions X et Y du doigt en vitesses demandées | ||
+ | 3/ Envoie les consignes de vitesses au D1 mini Wemos | ||
+ | 4/ Déclenche un timer afin d'adresser toutes les 10ms les consignes | ||
+ | de vitesses au D1 mini Wemos (voir section explications infra | ||
+ | *******************************************************************/ | ||
pad.addEventListener('touchstart', (position) => { | pad.addEventListener('touchstart', (position) => { | ||
vitesse[0] = Math.trunc((position.touches[0].clientX - rect.left)/pallierVitesse) - vitesseMax; | vitesse[0] = Math.trunc((position.touches[0].clientX - rect.left)/pallierVitesse) - vitesseMax; |
Version du 29 mars 2024 à 15:19
Sommaire
Description
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 de faire pencher 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
- 1 x Module Wifi D1 Mini Wemos
- 2 x Servomoteurs 0-180°
- Câbles électriques, connecteurs et 1 câble USB/mini USB pour connecter le D1 mini Wemos à l'ordinateur
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 :
- Initier les impressions 3D selon les modèles de fichiers listés dans la section Impression 3D ;
- 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 ;
- Télécharger le programme dans le D1 mini Wemos ;
- Jouer.
Schéma
Code
Le code nécessaire à ce projet se sépare en deux parties :
- 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 ;
- C++ (Arduino) permettant de programmer le D1 mini Wemos pour qu'il ouvre une connexion WIFI et d'adresser les commandes reçues aux servomoteurs contrôlant l'inclinaison du plateau du labyrinthe.
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 /***********************************************************
10 Détermine le nombre de vitesses qu'il y a sur chaque axe
11 Il faut prévoir aussi bien le mouvement en positif qu'en
12 négatif, et ne pas oublier 0 (pas de mouvement).
13 Avec vitesseMax = 4, on obtient 9 vitesses possibles :
14 [-4 ; -3 ; -2 ; -1 ; 0 ; 1 ; 2 ; 3 ; 4]
15 ***********************************************************/
16 let nbVitesses = vitesseMax * 2 + 1;
17 // Définit la taille du touch pad en pixel - Valeur modifiable
18 let taillePad = 180;
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 on 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 10ms les consignes
40 de vitesses au D1 mini Wemos (voir section explications infra
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, 10);
47 });
48
49 pad.addEventListener('touchmove', (position) => {
50 if(position.touches[0].clientX < rect.left || position.touches[0].clientX > (taillePad + rect.left) ||
51 position.touches[0].clientY < rect.top || position.touches[0].clientY > (taillePad + rect.top))
52 return;
53
54 vitesse[0] = Math.trunc((position.touches[0].clientX - rect.left)/pallierVitesse) - vitesseMax;
55 vitesse[1] = vitesseMax - Math.trunc((position.touches[0].clientY - rect.top)/pallierVitesse);
56
57 });
58
59 pad.addEventListener('touchend', (position) => {
60 vitesse[0] = vitesse[1] = 0;
61 clearInterval(timerId);
62 envoiMouvement();
63 timerId = null;
64 });
65 </script>
C++
1 #include <ESP8266WiFi.h>
2 #include <ESP8266WebServer.h>
3 #include <Servo.h>
4 /*****************************************************/
5 /*** CODE PERMETTANT DE JOUER AU JEU DU LABYRINTHE ***/
6 /*** CE CODE EST PARAMETRE POUR FONCTIONNER AVEC ***/
7 /*** UN MODULE WIFI D1 MINI. ***/
8 /*** ELEMENTS A PERSONNALISER CI-DESSOUS ***/
9 /*** LIGNES : 16, 18, 30, 202, 205 ***/
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
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 let supportsPassive = false;\
64 try {\
65 let opts = Object.defineProperty({}, 'passive', {\
66 get: function() {\
67 supportsPassive = true;\
68 }\
69 });\
70 window.addEventListener('testPassive', null, opts);\
71 window.removeEventListener('testPassive', null, opts);\
72 } catch (e) {}\
73 const pad = document.getElementById('pad');\
74 const rect = pad.getBoundingClientRect();\
75 let vitesseMax = 4;\
76 let taillePad = 360;\
77 let vitesse = [0, 0];\
78 let nbVitesses = vitesseMax * 2 + 1;\
79 let pallierVitesse = taillePad / nbVitesses;\
80 let timerId = null;\
81 pad.style.width = pad.style.height = taillePad + 'px';\
82 function envoiMouvement() {\
83 const xhr = new XMLHttpRequest();\
84 xhr.open('Mouvement', 'mouvement?x=' + vitesse[0] + '&y=' + vitesse[1], true);\
85 xhr.send();\
86 }\
87 pad.addEventListener('touchstart', (position) => {\
88 vitesse[0] = Math.trunc((position.touches[0].clientX - rect.left)/pallierVitesse) - vitesseMax;\
89 vitesse[1] = vitesseMax - Math.trunc((position.touches[0].clientY - rect.top)/pallierVitesse);\
90 envoiMouvement();\
91 timerId = setInterval(envoiMouvement, 30);\
92 }, supportsPassive ? { passive: true } : false);\
93 pad.addEventListener('touchmove', (position) => {\
94 if(position.touches[0].clientX < rect.left || position.touches[0].clientX > (taillePad + rect.left) ||\
95 position.touches[0].clientY < rect.top || position.touches[0].clientY > (taillePad + rect.top))\
96 return;\
97 vitesse[0] = Math.trunc((position.touches[0].clientX - rect.left)/pallierVitesse) - vitesseMax;\
98 vitesse[1] = vitesseMax - Math.trunc((position.touches[0].clientY - rect.top)/pallierVitesse);\
99 }, supportsPassive ? { passive: true } : false);\
100 pad.addEventListener('touchend', (position) => {\
101 vitesse[0] = vitesse[1] = 0;\
102 clearInterval(timerId);\
103 envoiMouvement();\
104 timerId = null;\
105 });\
106 </script>\
107 </html>";
108 }
109
110 // permet d'envoyer au client la page html permettant de commander les servo moteur
111 void handleIndex() {
112 Serial.println("index");
113 // prépare le html qui sera envoyé au client
114 pageIndex();
115 // envoie la page HTML au client
116 server.send(200,"text/html", pageWeb);
117 }
118
119 // Applique les nouveaux angles aux servomoteurs
120 void majMouvement() {
121 // On vérifie qu'on a des arguments qui ont été envoyés
122 if(!server.args()) return;
123 // Le premier argument correspond au mouvement sur l'axe x.
124 // On vérifie que c'est bien le cas, et on récupère la valeur du mouvement.
125 if(server.argName(0) == String("x"))
126 {
127 // Conversion de la valeur reçue sous format texte en nombre entier et on l'ajoute à la valeur de l'angle du servoO
128 angleServo[0] += server.arg(0).toInt();
129 // On vérifie si la valeur reçue est positive ou négative
130 if(server.arg(0).toInt() > 0) {
131 // si l'angle est supérieur à 180, on la ramène à 180
132 if(angleServo[0] > 180)
133 angleServo[0] = 180;
134 }
135 else
136 // la valeur reçue est négative
137 // La variable angleServo est déclarée avec un type "byte". Ce typage correspond à une valeur non signée pouvant recevoir des valeurs comprises entre 0 et 255 inclus.
138 // Cela signifie qu'elle ne peut pas être négative. Aussi, lorsqu'elle passe en-dessous de 0 lors d'une opération mathématique,
139 // elle ne sera pas négative, mais correspondra à la valeur max qu'elle peut stockée (255 ici) moins la valeur négative calculée.
140 // Cela est plus facile à visualiser si on utilise les nombres binaires (0-1).
141 // Dans notre cas, lorsque la valeur reçue est négative, on la soustrait à "angleServo". Si la soustraction donne un résultat négatif,
142 // "angleServo" vaudra 255 moins ce résultat négatif (donc supérieure à 18à). Si c'est le cas, alors on assigne la valeur 0 à "angleServo".
143 if(angleServo[0] > 180)
144 angleServo[0] = 0;
145 }
146
147 // on refait le même travail, mais pour l'axe y cette fois-ci.
148 if(server.argName(1) == String("y"))
149 {
150 angleServo[1] += server.arg(1).toInt();
151 if(server.arg(1).toInt() > 0) {
152 if(angleServo[1] > 180)
153 angleServo[1] = 180;
154 }
155 else
156 if(angleServo[1] > 180)
157 angleServo[1] = 0;
158 }
159 // on applique les nouveaux angles aux servo moteurs.
160 monServo[0].write(angleServo[0]);
161 monServo[1].write(angleServo[1]);
162 }
163
164 // pour le debug
165 void handleInconnu() {
166 }
167
168 // crée le point d'accès wifi
169 void creerAccesWifi() {
170 WiFi.mode(WIFI_AP);
171 WiFi.softAP(nomDuReseau, motDePasse);
172 Serial.print("@IP du module wifi : ");
173 // permet d'afficher l'@IP du point d'accès sur le port série
174 Serial.println(WiFi.softAPIP());
175 // permet de traiter les requêtes HTML reçues
176 server.on("/", handleIndex);
177 server.on("/mouvement", majMouvement);
178 server.onNotFound(handleInconnu);
179 // on démarre le point d'accès
180 server.begin();
181 // Configure la broche D4 (GPIO 2) sur la quelle est branchée la LED_BUILTIN en sortie
182 pinMode(LED_BUILTIN, OUTPUT);
183 // Allume la LED_BUILTIN, signifiant que le wifi est activé
184 digitalWrite(LED_BUILTIN, LOW);
185 }
186
187 void setup() {
188 // on utilise le port série pour afficher l'adresse ip du point d'accès wifi (normalement : 192.168.4.1)
189 Serial.begin(9600);
190 // on crée le point d'accès wifi
191 creerAccesWifi();
192 // On initialise les servo moteurs pour avoir les plateaux à plat
193 // Particularité avec les D1 mini, 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.
194 // Cela est dû à la fréquence PWM du D1 mini qui est inconnue de la bibliothèque <Servo.h>, et qui applique du coup une configuration par défaut lorsqu'on appelle la fonction "Servo::attach"
195 // Pour le labyrinthe, il n'est pas nécessaire d'avoir toute cette amplitude de débattement (0° - 180°).
196 // 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).
197 // 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.
198 // 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.
199 // Cela a pour conséquences :
200 // 1- qu'en envoyant aux servos la commande Servo::write(90), les plateaux du labyrinthe seront sur leurs 0° ;
201 // 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)
202 monServo[0].attach(pinServo[0], 1410, 1530);
203 monServo[0].write(angleServo[0]);
204 monServo[1].attach(pinServo[1], 1530, 1650);
205 monServo[1].write(angleServo[1]);
206 }
207
208 void loop() {
209 // permet de gérer les connexions des clients
210 server.handleClient();
211 }