L'ESP32 en réseau — OSC et serveur web#
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.
- Par le Wi-Fi (OSC) — ESP32 → Godot et Godot → ESP32, sans câble
- 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#
- Téléchargez l'addon GodOSC : https://godotengine.org/asset-library/asset/2538 (ou directement dans Godot : onglet AssetLib → cherchez « GodOSC »)
- Décompressez-le et placez le contenu dans le dossier
addonsà la racine du projet. - Projet → Paramètres du projet → Plugins : activez GodOSC.




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


Une nouvelle propriété Port apparaît : remplacez-la par la valeur du code de l'ESP32 (const unsigned int outPort = 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
- Cette fonction est déclenchée à chaque message reçu. Utilisez
valspour interagir !! - Le script fait apparaître dans l'inspecteur une propriété Osc Address : l'adresse à écouter, ici
/test.

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

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)
PacketPeerUDPenvoie des paquets sur le réseau, sans connexion préalable : si l'ESP32 n'est pas là, le message est simplement perdu (pas d'erreur).couleur.r8donne le rouge entre 0 et 255.
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
- 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. - La LED devient rouge quand la baballe touche le bord de l'écran.
- Ajoutez une adresse
/luminositeavec une seule valeur, etmsg.dispatch("/luminosite", …)côté ESP32 pour appelerled.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])
Color.from_hsv(teinte, saturation, luminosité): la teinte fait le tour de l'arc-en-ciel quand elle va de 0 à 1.- Sans limite, Godot enverrait un message à chaque image (60 par seconde ou plus) : le réseau et l'ESP32 n'ont pas besoin d'autant.
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.
Ouvrir ce montage dans Plot Circuits ↗
- Réseau créé :
ESP32-Piezo - Mot de passe :
piezo123 - Adresse IP :
192.168.4.1
#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.