Jour 3 — Godot et l'ESP32 parlent avec le monde#

← Jour 2 · Programme · Jour 4 →


1. L'ESP32 devient un clavier ou une manette (HID)#

La façon la plus simple de faire parler l'ESP32 avec Godot : l'ESP32 se fait passer pour un clavier ou une manette USB. C'est ce qu'on appelle un périphérique HID (Human Interface Device).

Préparer le projet PlatformIO#

Le mode HID a besoin d'un autre réglage de l'USB. Dans platformio.ini, remplacez la ligne :

  -DARDUINO_USB_MODE=1

par :

  -DARDUINO_USB_MODE=0  ; USB en mode TinyUSB : clavier, manette + port série

Le moniteur série fonctionne toujours dans ce mode. Pour les exemples sans clavier ni manette (sections 2 et 3), remettez 1 : l'Upload est plus fiable.

Si l'Upload échoue en mode HID : maintenez BOOT, appuyez sur RST, relâchez BOOT, puis relancez Upload.

1a — Premier test : l'ESP32 tape au clavier#

Ouvrez un éditeur de texte (Bloc-notes, TextEdit…), branchez l'ESP32 et appuyez sur le bouton BOOT de la carte : l'ESP32 tape « bonjour ».

#include <Arduino.h>
#include "USB.h"
#include "USBHIDKeyboard.h"

USBHIDKeyboard Keyboard;

#define BOUTON_PIN 0  // le bouton BOOT de la carte

void setup() {
  pinMode(BOUTON_PIN, INPUT_PULLUP);
  Keyboard.begin();
  USB.begin();
}

void loop() {
  if (digitalRead(BOUTON_PIN) == LOW) {  // bouton appuyé
    Keyboard.print("bonjour ");
    delay(500);  // pour ne pas taper 50 fois d'affilée
  }
}

Piège du clavier AZERTY : l'ESP32 envoie la position des touches sur un clavier américain (QWERTY). Sur un ordinateur réglé en AZERTY, a devient q, z devient w, m devient ,… « bonjour » a été choisi parce que toutes ses lettres sont au même endroit sur les deux claviers. Pour les jeux, on utilise des touches qui ne changent pas : flèches, Espace, Entrée.

Sur Mac, un « Assistant réglage du clavier » peut s'ouvrir au premier branchement : fermez-le.

1b — Un clavier pour jouer dans Godot#

Deux boutons et un capteur à effet Hall deviennent les flèches gauche, droite et la touche Espace.

Élément GPIO Touche envoyée
Bouton gauche (vers GND) 5 ←
Bouton droit (vers GND) 6 →
Capteur Hall A3144 (aimant) 4 Espace

Côté ESP32 :

#include <Arduino.h>
#include "USB.h"
#include "USBHIDKeyboard.h"

USBHIDKeyboard Keyboard;

#define BOUTON_GAUCHE 5
#define BOUTON_DROITE 6
#define HALL_PIN      4   // A3144 : LOW quand un aimant est proche

// On se souvient de l'état précédent de chaque entrée
bool avant_gauche = false;
bool avant_droite = false;
bool avant_hall = false;

// Appuie sur la touche quand l'entrée devient active,
// la relâche quand elle redevient inactive.
// (Le & permet à la fonction de modifier la variable "avant".)
void suivreTouche(bool actif, bool &avant, uint8_t touche) {
  if (actif && !avant) {
    Keyboard.press(touche);
  }
  if (!actif && avant) {
    Keyboard.release(touche);
  }
  avant = actif;
}

void setup() {
  pinMode(BOUTON_GAUCHE, INPUT_PULLUP);
  pinMode(BOUTON_DROITE, INPUT_PULLUP);
  pinMode(HALL_PIN, INPUT_PULLUP);
  Keyboard.begin();
  USB.begin();
}

void loop() {
  suivreTouche(digitalRead(BOUTON_GAUCHE) == LOW, avant_gauche, KEY_LEFT_ARROW);
  suivreTouche(digitalRead(BOUTON_DROITE) == LOW, avant_droite, KEY_RIGHT_ARROW);
  suivreTouche(digitalRead(HALL_PIN) == LOW, avant_hall, ' ');  // ' ' = Espace
  delay(10);
}

Pourquoi press / release et pas print ? print tape la touche une seule fois. Avec press, la touche reste enfoncée tant que le bouton est tenu : dans le jeu, la baballe avance tant qu'on appuie.

Côté Godot — la baballe du Jour 1, pilotée au clavier :

extends Node2D

var rayon = 20
var couleur = Color.ORANGE
var couleur_fond = Color("#1d1b33")
var position_cercle = Vector2(0, 0)   # où est la baballe (x, y)
var vitesse = 300   # pixels par seconde

func _ready():
    RenderingServer.set_default_clear_color(couleur_fond)
    position_cercle = get_viewport_rect().size / 2

func _process(delta):
    # -1 si ← est enfoncée, +1 si →, 0 sinon
    var direction = Input.get_axis("ui_left", "ui_right")
    position_cercle.x += direction * vitesse * delta

    # Espace (ou Entrée) : nouvelle couleur
    if Input.is_action_just_pressed("ui_accept"):
        couleur = Color(randf(), randf(), randf())

    queue_redraw()   # la baballe a bougé : on redessine

func _draw():
    draw_circle(position_cercle, rayon, couleur)

Testez d'abord avec le vrai clavier, puis avec l'ESP32 : le jeu ne voit pas la différence !

1c — Une manette pour Godot#

Une manette envoie des axes (joysticks, gâchettes : des valeurs continues) et des boutons. Ici, le capteur Hall SS49E devient un joystick : on approche un aimant d'un côté ou de l'autre pour aller à gauche ou à droite.

Élément GPIO Sur la manette
Capteur Hall SS49E (analogique, alimenté en 3,3 V) 4 stick gauche, axe X
Bouton (vers GND) 5 bouton 0

Côté ESP32 :

#include <Arduino.h>
#include "USB.h"
#include "USBHIDGamepad.h"

USBHIDGamepad Gamepad;

#define HALL_PIN   4   // SS49E : entrée analogique
#define BOUTON_PIN 5

int repos = 0;         // valeur du capteur sans aimant

void setup() {
  pinMode(BOUTON_PIN, INPUT_PULLUP);

  // Calibration : moyenne de 50 mesures sans aimant
  long somme = 0;
  for (int i = 0; i < 50; i++) {
    somme += analogRead(HALL_PIN);
    delay(10);
  }
  repos = somme / 50;

  Gamepad.begin();
  USB.begin();
}

void loop() {
  // Champ magnétique : négatif d'un côté, positif de l'autre
  int champ = analogRead(HALL_PIN) - repos;

  // Une manette envoie des valeurs entre -127 et 127
  int x = map(champ, -800, 800, -127, 127);   // ajustez 800 selon vos aimants
  x = constrain(x, -127, 127);                // on ne dépasse pas les limites
  Gamepad.leftStick(x, 0);

  if (digitalRead(BOUTON_PIN) == LOW) {
    Gamepad.pressButton(0);
  } else {
    Gamepad.releaseButton(0);
  }

  delay(10);
}

Côté Godot — étape 1 : que voit Godot ?

Chaque modèle de manette numérote ses boutons et ses axes à sa façon. Avant de coder le jeu, on regarde ce qu'envoie la nôtre :

extends Node2D

func _ready():
    for id in Input.get_connected_joypads():
        print("Manette ", id, " : ", Input.get_joy_name(id))

func _input(event):
    if event is InputEventJoypadButton:
        print("Bouton ", event.button_index, " appuyé : ", event.pressed)
    if event is InputEventJoypadMotion:
        print("Axe ", event.axis, " = ", event.axis_value)

Approchez l'aimant, appuyez sur le bouton, et notez les numéros qui s'affichent dans la Sortie. Les axes vont de -1 à 1.

Côté Godot — étape 2 : la baballe suit l'aimant

extends Node2D

var rayon = 20
var couleur = Color.ORANGE
var couleur_fond = Color("#1d1b33")
var position_cercle = Vector2(0, 0)   # où est la baballe (x, y)

func _ready():
    RenderingServer.set_default_clear_color(couleur_fond)

func _process(delta):
    var taille = get_viewport_rect().size

    # Axe X du stick gauche de la manette n°0 : entre -1 et 1
    var x = Input.get_joy_axis(0, JOY_AXIS_LEFT_X)
    position_cercle = Vector2(taille.x / 2 + x * 300, taille.y / 2)

    # Bouton enfoncé : la baballe grossit
    if Input.is_joy_button_pressed(0, JOY_BUTTON_A):
        rayon = 40
    else:
        rayon = 20
    queue_redraw()

func _draw():
    draw_circle(position_cercle, rayon, couleur)

Si la baballe ne réagit pas, remplacez JOY_AXIS_LEFT_X et JOY_BUTTON_A par les numéros notés à l'étape 1 (par exemple Input.get_joy_axis(0, 0)).

Ici, l'aimant fonctionne comme un curseur : la position de la baballe suit directement la position de l'aimant. Avec le clavier (1b), on donnait plutôt une direction.

1d — Une manette sans fil (Bluetooth)#

Même manette que 1c, mais sans câble : l'ESP32 devient une manette Bluetooth (BLE). Pratique pour cacher le contrôleur dans un objet qui bouge, qu'on lance, qu'on porte sur soi… Il suffit de l'alimenter avec une batterie USB.

Même montage que 1c : capteur Hall SS49E sur le GPIO 4, bouton sur le GPIO 5.

Préparer le projet :

Côté ESP32 :

#include <Arduino.h>
#include <BleGamepad.h>

// Le nom qui apparaîtra dans les réglages Bluetooth de l'ordinateur
BleGamepad manette("Manette Enjmin", "Enjmin", 100);

#define HALL_PIN   4   // SS49E : entrée analogique
#define BOUTON_PIN 5

int repos = 0;         // valeur du capteur sans aimant

void setup() {
  Serial.begin(115200);
  pinMode(BOUTON_PIN, INPUT_PULLUP);

  // Calibration : moyenne de 50 mesures sans aimant
  long somme = 0;
  for (int i = 0; i < 50; i++) {
    somme += analogRead(HALL_PIN);
    delay(10);
  }
  repos = somme / 50;

  // On enverra nous-mêmes les données, une fois par tour de loop
  BleGamepadConfiguration config;
  config.setAutoReport(false);
  manette.begin(&config);

  Serial.println("Manette prête : connectez-la dans les réglages Bluetooth.");
}

void loop() {
  if (manette.isConnected()) {
    // En Bluetooth, les axes vont de -32767 à 32767 (centre = 0)
    int champ = analogRead(HALL_PIN) - repos;
    int x = map(champ, -800, 800, -32767, 32767);  // ajustez 800 selon vos aimants
    x = constrain(x, -32767, 32767);
    manette.setX(x);

    if (digitalRead(BOUTON_PIN) == LOW) {
      manette.press(BUTTON_1);
    } else {
      manette.release(BUTTON_1);
    }

    manette.sendReport();  // envoie tout d'un coup
  }
  delay(10);
}

Connecter la manette :

  1. Téléversez le programme et ouvrez le moniteur série : « Manette prête » s'affiche.
  2. Sur l'ordinateur : Réglages Bluetooth → ajouter un appareil → Manette Enjmin.
  3. Lancez le jeu Godot.

Côté Godot : rien à changer ! Les deux scripts de 1c (« que voit Godot ? » puis « la baballe suit l'aimant ») marchent tels quels : pour Godot, une manette Bluetooth est une manette comme une autre.

À savoir :

Défis#

  1. Ajoutez un deuxième bouton qui envoie la touche ↑ (KEY_UP_ARROW) et fait sauter la baballe.
  2. Utilisez le capteur de distance VL53L0X comme axe Y de la manette : la main qui monte et descend au-dessus du capteur déplace la baballe.
  3. Remplacez les boutons par des capteurs tactiles (touchRead, voir Jour 2) : du papier alu ou un fruit devient une touche !
Solution du défi 1 : un bouton pour sauter

Côté ESP32 — le code de 1b, avec un troisième bouton sur le GPIO 7 :

#include <Arduino.h>
#include "USB.h"
#include "USBHIDKeyboard.h"

USBHIDKeyboard Keyboard;

#define BOUTON_GAUCHE 5
#define BOUTON_DROITE 6
#define BOUTON_HAUT   7   // nouveau bouton (vers GND)
#define HALL_PIN      4

bool avant_gauche = false;
bool avant_droite = false;
bool avant_haut = false;
bool avant_hall = false;

void suivreTouche(bool actif, bool &avant, uint8_t touche) {
  if (actif && !avant) {
    Keyboard.press(touche);
  }
  if (!actif && avant) {
    Keyboard.release(touche);
  }
  avant = actif;
}

void setup() {
  pinMode(BOUTON_GAUCHE, INPUT_PULLUP);
  pinMode(BOUTON_DROITE, INPUT_PULLUP);
  pinMode(BOUTON_HAUT, INPUT_PULLUP);
  pinMode(HALL_PIN, INPUT_PULLUP);
  Keyboard.begin();
  USB.begin();
}

void loop() {
  suivreTouche(digitalRead(BOUTON_GAUCHE) == LOW, avant_gauche, KEY_LEFT_ARROW);
  suivreTouche(digitalRead(BOUTON_DROITE) == LOW, avant_droite, KEY_RIGHT_ARROW);
  suivreTouche(digitalRead(BOUTON_HAUT) == LOW, avant_haut, KEY_UP_ARROW);
  suivreTouche(digitalRead(HALL_PIN) == LOW, avant_hall, ' ');
  delay(10);
}

Côté Godot — pour sauter, il faut une vitesse verticale et de la gravité qui fait retomber la baballe :

extends Node2D

var rayon = 20
var couleur = Color.ORANGE
var couleur_fond = Color("#1d1b33")
var position_cercle = Vector2(0, 0)   # où est la baballe (x, y)
var vitesse = 300       # vitesse horizontale, en pixels par seconde
var vitesse_y = 0       # vitesse verticale : négative = vers le haut
var gravite = 1500      # la gravité ramène la baballe vers le bas
var sol = 0             # hauteur du sol

func _ready():
    RenderingServer.set_default_clear_color(couleur_fond)
    position_cercle = get_viewport_rect().size / 2
    sol = position_cercle.y

func _process(delta):
    var direction = Input.get_axis("ui_left", "ui_right")
    position_cercle.x += direction * vitesse * delta

    # ↑ : on saute, seulement si on est posé sur le sol
    if Input.is_action_just_pressed("ui_up") and position_cercle.y >= sol:
        vitesse_y = -600

    vitesse_y += gravite * delta     # la gravité freine la montée, puis fait retomber
    position_cercle.y += vitesse_y * delta
    if position_cercle.y > sol:             # on ne passe pas à travers le sol
        position_cercle.y = sol
        vitesse_y = 0

    if Input.is_action_just_pressed("ui_accept"):
        couleur = Color(randf(), randf(), randf())

    queue_redraw()   # la baballe a bougé : on redessine

func _draw():
    draw_circle(position_cercle, rayon, couleur)

ui_up est l'action prédéfinie de la flèche ↑. Changez -600 (force du saut) et gravite pour régler la hauteur et la durée du saut.

Solution du défi 2 : la distance devient l'axe Y

Côté ESP32 — le code de 1c (manette USB), avec le VL53L0X branché comme au Jour 2 (SDA = GPIO 8, SCL = GPIO 9) :

#include <Arduino.h>
#include <Wire.h>
#include <VL53L0X.h>
#include "USB.h"
#include "USBHIDGamepad.h"

USBHIDGamepad Gamepad;
VL53L0X capteur;

#define HALL_PIN   4   // SS49E : axe X
#define BOUTON_PIN 5

int repos = 0;

void setup() {
  pinMode(BOUTON_PIN, INPUT_PULLUP);

  long somme = 0;
  for (int i = 0; i < 50; i++) {
    somme += analogRead(HALL_PIN);
    delay(10);
  }
  repos = somme / 50;

  // Capteur de distance (I²C)
  Wire.begin(8, 9);
  capteur.setTimeout(500);
  capteur.init();
  capteur.startContinuous();

  Gamepad.begin();
  USB.begin();
}

void loop() {
  // Axe X : l'aimant, comme en 1c
  int champ = analogRead(HALL_PIN) - repos;
  int x = map(champ, -800, 800, -127, 127);
  x = constrain(x, -127, 127);

  // Axe Y : la main au-dessus du capteur
  // 50 mm (main proche) → -127 (haut) ; 400 mm (main loin) → 127 (bas)
  int distance = capteur.readRangeContinuousMillimeters();
  int y = 0;                       // pas de main : le stick reste au centre
  if (distance < 2000) {
    y = map(distance, 50, 400, -127, 127);
    y = constrain(y, -127, 127);
  }

  Gamepad.leftStick(x, y);

  if (digitalRead(BOUTON_PIN) == LOW) {
    Gamepad.pressButton(0);
  } else {
    Gamepad.releaseButton(0);
  }
  delay(10);
}

Côté Godot — dans le script « la baballe suit l'aimant » de 1c, on lit aussi l'axe Y :

func _process(delta):
    var taille = get_viewport_rect().size

    var x = Input.get_joy_axis(0, JOY_AXIS_LEFT_X)
    var y = Input.get_joy_axis(0, JOY_AXIS_LEFT_Y)   # la main : entre -1 (haut) et 1 (bas)
    position_cercle = Vector2(taille.x / 2 + x * 300, taille.y / 2 + y * 200)

    if Input.is_joy_button_pressed(0, JOY_BUTTON_A):
        rayon = 40
    else:
        rayon = 20
    queue_redraw()

Ajustez 50 et 400 dans map selon la hauteur de main qui vous paraît confortable.

Solution du défi 3 : des touches tactiles

Côté ESP32 — le code de 1b, où les deux boutons deviennent deux touches tactiles. Un fil sur le GPIO 1 et un sur le GPIO 2 (GPIO tactiles : 1 à 14), reliés à du papier alu, un fruit…

#include <Arduino.h>
#include "USB.h"
#include "USBHIDKeyboard.h"

USBHIDKeyboard Keyboard;

#define TOUCH_GAUCHE 1
#define TOUCH_DROITE 2
#define HALL_PIN     4

bool avant_gauche = false;
bool avant_droite = false;
bool avant_hall = false;

int repos_gauche = 0;   // valeurs sans toucher, mesurées au démarrage
int repos_droite = 0;

void suivreTouche(bool actif, bool &avant, uint8_t touche) {
  if (actif && !avant) {
    Keyboard.press(touche);
  }
  if (!actif && avant) {
    Keyboard.release(touche);
  }
  avant = actif;
}

void setup() {
  pinMode(HALL_PIN, INPUT_PULLUP);

  // Calibration : ne touchez rien pendant le démarrage !
  delay(500);
  repos_gauche = touchRead(TOUCH_GAUCHE);
  repos_droite = touchRead(TOUCH_DROITE);

  Keyboard.begin();
  USB.begin();
}

void loop() {
  // Sur l'ESP32-S3, touchRead AUGMENTE quand on touche.
  // On considère qu'on touche au-delà de 1,3 fois la valeur de repos (ajustez si besoin).
  bool gauche = touchRead(TOUCH_GAUCHE) > repos_gauche * 1.3;
  bool droite = touchRead(TOUCH_DROITE) > repos_droite * 1.3;

  suivreTouche(gauche, avant_gauche, KEY_LEFT_ARROW);
  suivreTouche(droite, avant_droite, KEY_RIGHT_ARROW);
  suivreTouche(digitalRead(HALL_PIN) == LOW, avant_hall, ' ');
  delay(10);
}

Côté Godot : rien à changer, le jeu reçoit toujours les flèches.

Si une touche se déclenche toute seule ou pas du tout, regardez les valeurs avec le code Touch du Jour 2 et changez 1.3.


2. Après-midi : votre projet à partir du HID#

L'ESP32 sait maintenant être un clavier ou une manette : à vous de jouer ! En binôme ou trinôme, lancez un petit projet. Il continue au Jour 4.

  1. Une idée en une phrase : quel objet, quel geste, quel jeu ? Par exemple : « on souffle sur un moulin à vent pour faire avancer un bateau ».
  2. Un capteur, une action : commencez par un seul capteur relié à une seule touche ou un seul axe de manette. On complique ensuite.
  3. Le jeu Godot : partez de la baballe de 1b ou 1c, ou d'un exemple du Jour 1, et branchez-y votre contrôleur.
  4. L'objet : cachez l'ESP32 et le capteur dans quelque chose (carton, jouet, objet détourné…). Le contrôleur, c'est aussi ce qu'on tient dans les mains.

Des pistes avec les capteurs du Jour 2 :

Capteur Devient…
Touch (papier alu, fruit, eau…) une touche
Aimant + A3144 une touche (porte, roue qui tourne, objet posé)
Aimant + SS49E un axe de joystick
VL53L0X (distance) un axe : la main qui monte et descend
LDR (lumière) un axe : on cache ou on éclaire
Piézo une touche qu'on frappe

En fin de journée : chacun.e montre en deux minutes où il.elle en est.


Schémas de montage : Plot Circuits, avec les composants de la bibliothèque Fritzing (CC BY-SA 3.0, adaptés). Ces schémas sont partagés sous la même licence.