L'ESP32 en réseau — OSC et serveur web#

← ALT in&out XP · Programme

Pour la semaine ALT in&out XP, avec les programmeur.euse.s. On part des bases du Jour 2 et du HID du Jour 3.

  1. Par le Wi-Fi (OSC) — ESP32 → Godot et Godot → ESP32, sans câble
  2. L'ESP32 comme petit serveur web — réglages depuis un téléphone

1. Communication par le Wi-Fi (OSC)#

On peut passer par le réseau local pour communiquer entre le microcontrôleur et Godot : plus besoin de câble.

Open Sound Control (OSC) : Open Sound Control (OSC) is an open, transport-independent, message-based encoding developed for communication among computers, sound synthesizers, and other multimedia devices.

Côté ESP32 : la librairie OSC#

On utilise la librairie OSC du CNMAT (Adrian Freed). Elle est déjà dans lib_deps du platformio.ini : rien à installer.

Le réseau#

Il faut un réseau local partagé entre le PC qui fait tourner Godot et l'ESP32. Le réseau de l'école ne le permet pas forcément : partagez plutôt une connexion depuis votre téléphone.

Il faut aussi connaître l'adresse IP du PC sur ce réseau local (dans l'exemple : 172.20.10.3). Dans le terminal :

Système Commande
macOS ipconfig getifaddr en0
Linux hostname -I \| awk '{print $1}'
Windows for /f "tokens=2 delims=:" %f in ('ipconfig ^\| findstr "IPv4"') do @echo %f

Attention : cette adresse peut changer, après un redémarrage par exemple.

Côté ESP32#

#include <Arduino.h>
#include <WiFi.h>
#include <WiFiUdp.h>
#include <OSCMessage.h>

char ssid[] = "PRob";      // nom du réseau
char pass[] = "zzzzzzzz";  // mot de passe

WiFiUDP Udp;
const IPAddress outIp(172, 20, 10, 3);  // ← changez par l'IP de l'ordinateur !!
const unsigned int outPort = 9999;      // port sur lequel Godot écoute
const unsigned int localPort = 8888;    // port local de l'ESP32

void setup() {
  Serial.begin(115200);

  // Connexion au réseau Wi-Fi
  Serial.println();
  Serial.println();
  Serial.print("Connecting to ");
  Serial.println(ssid);
  WiFi.begin(ssid, pass);

  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }
  Serial.println("");

  Serial.println("WiFi connected");
  Serial.println("IP address: ");
  Serial.println(WiFi.localIP());

  Serial.println("Starting UDP");
  Udp.begin(localPort);
  Serial.print("Local port: ");
  Serial.println(localPort);
}

void loop() {
  OSCMessage msg("/test");
  msg.add("hello, osc.");
  Udp.beginPacket(outIp, outPort);
  msg.send(Udp);
  Udp.endPacket();
  msg.empty();
  delay(500);
}

Pour envoyer la valeur d'un capteur à la place du texte : msg.add(analogRead(4)); (utilisez les GPIO 1 à 10 : les autres entrées analogiques ne marchent pas avec le Wi-Fi).

Côté Godot : l'addon GodOSC#

  1. Téléchargez l'addon GodOSC : https://godotengine.org/asset-library/asset/2538 (ou directement dans Godot : onglet AssetLib → cherchez « GodOSC »)
  2. Décompressez-le et placez le contenu dans le dossier addons à la racine du projet.
  3. Projet → Paramètres du projet → Plugins : activez GodOSC.

Dossier addons/godOSC

AssetLib

GodOSC dans l'AssetLib

Activer GodOSC

Le serveur OSC#

Créez un nœud et ajoutez-lui le script existant de la librairie OSCServer : addons → godOSC → Scripts → OSCServer.

Nœud OSCServer

Charger le script

Une nouvelle propriété Port apparaît : remplacez-la par la valeur du code de l'ESP32 (const unsigned int outPort = 9999;).

Port 9999

Le récepteur#

Il faut maintenant un autre nœud qui écoute les messages d'une adresse (channel) précise. Ici, le microcontrôleur envoie "hello, osc." sur /test.

Dans la même scène que l'OSCServer, sur n'importe quel nœud, ajoutez un script avec ce code :

extends OSCReceiver

## Code exécuté quand Parent Control est réglé sur "custom".
func _custom_control(address: String, vals: Array, time):
    if vals != []:
        print(address)
        print(vals)
    pass

OSCReceiver dans l'inspecteur

Si tout se passe bien, vous devriez voir apparaître les logs !!

Logs OSC

Dans l'autre sens : Godot → ESP32, changer la couleur de la LED#

Jusqu'ici, l'ESP32 parlait et Godot écoutait. On inverse : Godot envoie un message OSC /led avec trois nombres (rouge, vert, bleu entre 0 et 255), et l'ESP32 allume sa LED intégrée de cette couleur. Le jeu peut ainsi faire réagir l'objet physique : clignoter quand on perd, changer de couleur selon le niveau…

Cette fois, c'est Godot qui doit connaître l'adresse IP de l'ESP32 : l'ESP32 l'affiche dans le moniteur série au démarrage.

Côté ESP32#

Même réseau Wi-Fi que plus haut. L'ESP32 écoute sur le port 8888 :

#include <Arduino.h>
#include <WiFi.h>
#include <WiFiUdp.h>
#include <OSCMessage.h>
#include <Adafruit_NeoPixel.h>

char ssid[] = "PRob";      // nom du réseau
char pass[] = "zzzzzzzz";  // mot de passe

WiFiUDP Udp;
const unsigned int localPort = 8888;  // port sur lequel l'ESP32 écoute

Adafruit_NeoPixel led(1, 48, NEO_GRB + NEO_KHZ800);  // LED RGB intégrée

// Appelée à chaque message reçu sur l'adresse /led
void changerCouleur(OSCMessage &msg) {
  int r = msg.getInt(0);
  int g = msg.getInt(1);
  int b = msg.getInt(2);
  led.setPixelColor(0, led.Color(r, g, b));
  led.show();
  Serial.printf("Couleur reçue : %d %d %d\n", r, g, b);
}

void setup() {
  Serial.begin(115200);
  led.begin();
  led.setBrightness(80);  // la LED est très puissante : on la calme un peu
  led.show();

  WiFi.begin(ssid, pass);
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }
  Serial.println();
  Serial.print("Adresse IP de l'ESP32 : ");
  Serial.println(WiFi.localIP());  // ← à recopier dans Godot

  Udp.begin(localPort);
}

void loop() {
  int taille = Udp.parsePacket();  // un message est-il arrivé ?
  if (taille > 0) {
    OSCMessage msg;
    while (taille--) {
      msg.fill(Udp.read());        // on lit le message octet par octet
    }
    if (!msg.hasError()) {
      msg.dispatch("/led", changerCouleur);  // si l'adresse est /led → changerCouleur
    }
  }
}

Côté Godot#

Pour envoyer, pas besoin d'addon : un message OSC n'est qu'une petite suite d'octets envoyée en UDP. On la fabrique nous-mêmes, ce qui permet de voir à quoi ressemble un message OSC.

À chaque clic, la baballe prend une couleur au hasard, et l'ESP32 prend la même :

extends Node2D

var ip_esp32 = "172.20.10.5"   # ← l'adresse affichée par l'ESP32
var port_esp32 = 8888

var udp = PacketPeerUDP.new()

var rayon = 40
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)
    position_cercle = get_viewport_rect().size / 2
    udp.set_dest_address(ip_esp32, port_esp32)

func _input(event):
    if event is InputEventMouseButton and event.pressed and event.button_index == MOUSE_BUTTON_LEFT:
        couleur = Color(randf(), randf(), randf())
        queue_redraw()
        # Les couleurs de Godot vont de 0 à 1, la LED attend 0 à 255
        envoyer_osc("/led", [couleur.r8, couleur.g8, couleur.b8])

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

# ----- Fabriquer et envoyer un message OSC -----
# Un message OSC = une adresse ("/led"), la liste des types (",iii" : 3 entiers),
# puis les valeurs. Chaque morceau fait un multiple de 4 octets.

func envoyer_osc(adresse, valeurs):
    var paquet = StreamPeerBuffer.new()
    paquet.big_endian = true   # l'OSC range les octets « poids fort d'abord »
    ecrire_texte(paquet, adresse)
    ecrire_texte(paquet, "," + "i".repeat(valeurs.size()))
    for v in valeurs:
        paquet.put_32(v)
    udp.put_packet(paquet.data_array)

func ecrire_texte(paquet, texte):
    var octets = texte.to_ascii_buffer()
    paquet.put_data(octets)
    # On termine le texte par 1 à 4 octets nuls pour arriver à un multiple de 4
    for i in range(4 - octets.size() % 4):
        paquet.put_u8(0)

Cliquez : la LED de l'ESP32 prend la couleur de la baballe, et le moniteur série affiche Couleur reçue : ….

Rien ne se passe ? Vérifiez que l'ordinateur et l'ESP32 sont sur le même réseau, que l'IP dans Godot est bien celle affichée par l'ESP32, et que le pare-feu de l'ordinateur ne bloque pas Godot.

Défis

  1. La couleur suit la souris : utilisez Color.from_hsv(x / largeur, 1, 1) avec la position de la souris (get_global_mouse_position()), et envoyez au plus 20 messages par seconde.
  2. La LED devient rouge quand la baballe touche le bord de l'écran.
  3. Ajoutez une adresse /luminosite avec une seule valeur, et msg.dispatch("/luminosite", …) côté ESP32 pour appeler led.setBrightness(...).
Solution du défi 1 : la couleur suit la souris

Côté ESP32, rien à changer. Côté Godot, on remplace _input par un _process. Gardez les fonctions envoyer_osc et ecrire_texte en bas du script.

var temps = 0.0   # temps écoulé depuis le dernier envoi, en secondes

func _process(delta):
    var largeur = get_viewport_rect().size.x
    var souris = get_global_mouse_position()

    # La position de la souris (de 0 à 1 sur la largeur) choisit la teinte
    var teinte = clamp(souris.x / largeur, 0, 1)
    couleur = Color.from_hsv(teinte, 1, 1)
    queue_redraw()

    # Au plus 20 messages par seconde : un envoi toutes les 0,05 s
    temps += delta
    if temps >= 0.05:
        temps = 0
        envoyer_osc("/led", [couleur.r8, couleur.g8, couleur.b8])
Solution du défi 2 : rouge au bord de l'écran

Ici, la baballe suit la souris sans sortir de l'écran. On garde en mémoire si elle touchait déjà le bord, pour n'envoyer un message que quand ça change (et pas 60 fois par seconde).

var au_bord = false   # la baballe touchait-elle le bord à l'image précédente ?

func _process(delta):
    var taille = get_viewport_rect().size
    var marge = Vector2(rayon, rayon)
    position_cercle = get_global_mouse_position().clamp(marge, taille - marge)   # reste dans l'écran
    queue_redraw()

    var touche = position_cercle.x <= rayon or position_cercle.y <= rayon or position_cercle.x >= taille.x - rayon or position_cercle.y >= taille.y - rayon

    if touche and not au_bord:
        envoyer_osc("/led", [255, 0, 0])                           # elle arrive au bord : rouge !
    if not touche and au_bord:
        envoyer_osc("/led", [couleur.r8, couleur.g8, couleur.b8])  # elle repart : sa couleur
    au_bord = touche

clamp(minimum, maximum) bloque la position entre deux limites : la baballe ne peut pas dépasser le bord, elle s'y colle.

Solution du défi 3 : régler la luminosité

Côté ESP32 — une deuxième fonction pour l'adresse /luminosite. On retient la dernière couleur reçue pour la réafficher avec la nouvelle luminosité :

int led_r = 0, led_g = 0, led_b = 0;   // dernière couleur reçue

void changerCouleur(OSCMessage &msg) {
  led_r = msg.getInt(0);
  led_g = msg.getInt(1);
  led_b = msg.getInt(2);
  led.setPixelColor(0, led.Color(led_r, led_g, led_b));
  led.show();
}

void changerLuminosite(OSCMessage &msg) {
  int luminosite = constrain(msg.getInt(0), 0, 255);
  led.setBrightness(luminosite);
  led.setPixelColor(0, led.Color(led_r, led_g, led_b));   // on réaffiche la couleur
  led.show();
  Serial.printf("Luminosité reçue : %d\n", luminosite);
}

Et dans loop(), on aiguille chaque adresse vers sa fonction :

    if (!msg.hasError()) {
      msg.dispatch("/led", changerCouleur);
      msg.dispatch("/luminosite", changerLuminosite);
    }

Côté Godot — les flèches ↑ et ↓ changent la luminosité. On ajoute une variable en haut du script, et deux tests dans _input :

var luminosite = 80   # même valeur de départ que l'ESP32

func _input(event):
    if event is InputEventMouseButton and event.pressed and event.button_index == MOUSE_BUTTON_LEFT:
        couleur = Color(randf(), randf(), randf())
        queue_redraw()
        envoyer_osc("/led", [couleur.r8, couleur.g8, couleur.b8])
    if event.is_action_pressed("ui_up"):
        luminosite = min(luminosite + 25, 255)
        envoyer_osc("/luminosite", [luminosite])
    if event.is_action_pressed("ui_down"):
        luminosite = max(luminosite - 25, 5)
        envoyer_osc("/luminosite", [luminosite])

Un message OSC peut contenir une seule valeur : envoyer_osc écrit alors ",i" comme liste des types.


2. L'ESP32 comme petit serveur web#

Hello World#

L'ESP32 crée son propre réseau Wi-Fi (mode Access Point) et sert une page web.

#include <Arduino.h>
#include <WiFi.h>
#include <WebServer.h>

const char* ssid = "wifi-Demo";  // nom du réseau créé par l'ESP32
const char* password = "";       // sans mot de passe

WebServer server(80);

void handleRoot() {
  server.send(200, "text/plain", "Hello World");
}

void setup() {
  WiFi.mode(WIFI_AP);
  WiFi.softAP(ssid, password);

  Serial.begin(115200);
  Serial.print("IP: ");
  Serial.println(WiFi.softAPIP());  // en général 192.168.4.1

  server.on("/", handleRoot);
  server.begin();
}

void loop() {
  server.handleClient();
}

Connectez votre téléphone au réseau wifi-Demo, puis ouvrez l'adresse IP affichée dans un navigateur.

Contrôler la couleur de la LED intégrée (GPIO 48) depuis une page web#

#include <Arduino.h>
#include <WiFi.h>
#include <WebServer.h>
#include <Adafruit_NeoPixel.h>

const char* ssid = "ESP32-RGB";
const char* password = "";

WebServer server(80);
Adafruit_NeoPixel led(1, 48, NEO_GRB + NEO_KHZ800);

// Variables globales pour les couleurs
int r = 0, g = 0, b = 0;

void handleRoot() {
  String html = R"rawliteral(
<html>
<body style="text-align:center;padding:50px;font-family:Arial;">
<h2>LED RGB Control</h2>
Rouge: <input type="range" min="0" max="255" value="0" id="r" oninput="update()"><br><br>
Vert: <input type="range" min="0" max="255" value="0" id="g" oninput="update()"><br><br>
Bleu: <input type="range" min="0" max="255" value="0" id="b" oninput="update()"><br><br>
<script>
function update() {
  fetch('/set?r=' + document.getElementById('r').value +
        '&g=' + document.getElementById('g').value +
        '&b=' + document.getElementById('b').value);
}
</script>
</body>
</html>
)rawliteral";
  server.send(200, "text/html", html);
}

void handleSet() {
  // Stocker les valeurs dans les variables globales
  r = server.arg("r").toInt();
  g = server.arg("g").toInt();
  b = server.arg("b").toInt();
  server.send(200, "text/plain", "OK");
}

void setup() {
  Serial.begin(115200);
  led.begin();
  WiFi.mode(WIFI_AP);
  WiFi.softAP(ssid, password);
  Serial.println(WiFi.softAPIP());
  server.on("/", handleRoot);
  server.on("/set", handleSet);
  server.begin();
}

void loop() {
  server.handleClient();

  // Mettre à jour la LED dans la loop
  led.setPixelColor(0, led.Color(r, g, b));
  led.show();

  delay(10);  // petite pause
}

Capteur piézo : calibration et réglage par page web#

Même montage que le piézo du Jour 2 : piézo entre GND et le GPIO 1, avec une résistance de 1 MΩ en parallèle.

Montage piézo

Ouvrir ce montage dans Plot Circuits ↗

#include <Arduino.h>

// Capteur piézo ESP32-S3 Super Mini + interface web simple
// Piézo connecté entre GND et GPIO 1 (entrée analogique compatible Wi-Fi)
#include <WiFi.h>
#include <WebServer.h>

// Configuration Wi-Fi Access Point
const char* ap_ssid = "ESP32-Piezo";
const char* ap_password = "piezo123";  // 8 caractères minimum

// Configuration piézo
const int PIEZO_PIN = 1;
const int CALIBRATION_SAMPLES = 200;
const int CALIBRATION_DELAY = 25;

int baseline_max = 0;
int detection_threshold = 0;
int THRESHOLD_MARGIN = 20;

int last_hit = 0;           // valeur du dernier coup
unsigned long hit_count = 0;

WebServer server(80);  // serveur web

void calibratePiezoMaxThreshold() {
  int maxValue = 0;
  for (int i = 0; i < CALIBRATION_SAMPLES; i++) {
    int currentValue = analogRead(PIEZO_PIN);
    if (currentValue > maxValue) {
      maxValue = currentValue;
    }
    delay(CALIBRATION_DELAY);
  }
  baseline_max = maxValue;
  // Seuil = valeur max observée + marge de sécurité
  detection_threshold = baseline_max + THRESHOLD_MARGIN;
}

// Page principale : état + formulaire de réglage
void handleRoot() {
  String html = "<html><head><meta name='viewport' content='width=device-width'></head>";
  html += "<body style='text-align:center;padding:30px;font-family:Arial;'>";
  html += "<h2>Capteur piezo</h2>";
  html += "<p>Max au repos : " + String(baseline_max) + "</p>";
  html += "<p>Seuil : " + String(detection_threshold) + "</p>";
  html += "<p>Coups : " + String(hit_count) + " (dernier : " + String(last_hit) + ")</p>";
  html += "<form action='/set'>Marge : ";
  html += "<input type='number' name='margin' value='" + String(THRESHOLD_MARGIN) + "'>";
  html += " <input type='submit' value='OK'></form>";
  html += "<p><a href='/calibrate'>Recalibrer</a> (ne pas toucher le capteur)</p>";
  html += "</body></html>";
  server.send(200, "text/html", html);
}

// Changer la marge depuis le téléphone
void handleSet() {
  if (server.hasArg("margin")) {
    THRESHOLD_MARGIN = server.arg("margin").toInt();
    detection_threshold = baseline_max + THRESHOLD_MARGIN;
  }
  server.sendHeader("Location", "/");
  server.send(303);
}

// Relancer la calibration
void handleCalibrate() {
  calibratePiezoMaxThreshold();
  server.sendHeader("Location", "/");
  server.send(303);
}

void setupWiFiAndWeb() {
  WiFi.mode(WIFI_AP);
  WiFi.softAP(ap_ssid, ap_password);
  Serial.print("Réseau créé : ");
  Serial.println(ap_ssid);
  Serial.print("Adresse IP : ");
  Serial.println(WiFi.softAPIP());

  server.on("/", handleRoot);
  server.on("/set", handleSet);
  server.on("/calibrate", handleCalibrate);
  server.begin();
}

void setup() {
  Serial.begin(115200);
  delay(1000);

  // Calibration
  Serial.println("CALIBRATION CAPTEUR PIEZO");
  calibratePiezoMaxThreshold();
  Serial.println("\n=== CALIBRATION TERMINEE ===");
  Serial.printf("Seuil de détection: %d\n", detection_threshold);

  // Configuration Wi-Fi et serveur web
  setupWiFiAndWeb();
  Serial.println("Détection active !");
}

void loop() {
  server.handleClient();

  int rawValue = analogRead(PIEZO_PIN);
  if (rawValue > detection_threshold) {
    last_hit = rawValue;
    hit_count++;
    Serial.printf("COUP DETECTE! Valeur: %d, Intensité: %d\n",
                  rawValue, rawValue - detection_threshold);
    delay(150);  // anti-rebond
  }
  delay(5);
}

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.