Réseau — Wi-Fi, mDNS, OSC, ESP-NOW#

← Bases · Programme · Web →

Le réseau de l'école isole souvent les appareils entre eux : partagez plutôt une connexion depuis un téléphone, ou passez l'ESP32 en point d'accès. Le Wi-Fi de l'ESP32 est en 2,4 GHz uniquement.


Wi-Fi client, événements et mDNS#

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

const char* SSID = "MySSID";
const char* PASS = "Enjmin666";
const char* HOSTNAME  = "enjmin-01";   // → joignable en http://enjmin-01.local

// Les événements Wi-Fi arrivent dans une tâche système : on garde le traitement court
void onWiFiEvent(WiFiEvent_t event, WiFiEventInfo_t info) {
  switch (event) {
    case ARDUINO_EVENT_WIFI_STA_GOT_IP:
      Serial.printf("Connecté : %s (signal %d dBm)\n", WiFi.localIP().toString().c_str(), WiFi.RSSI());
      break;
    case ARDUINO_EVENT_WIFI_STA_DISCONNECTED:
      Serial.printf("Déconnecté (raison %d), reconnexion automatique…\n", info.wifi_sta_disconnected.reason);
      break;
    default:
      break;
  }
}

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

  WiFi.onEvent(onWiFiEvent);
  WiFi.mode(WIFI_STA);
  WiFi.setHostname(HOSTNAME);
  WiFi.setAutoReconnect(true);
  WiFi.begin(SSID, PASS);

  // On attend au plus 10 s, sans bloquer pour toujours
  uint32_t start = millis();
  while (WiFi.status() != WL_CONNECTED && millis() - start < 10000) delay(100);

  if (MDNS.begin(HOSTNAME)) {
    MDNS.addService("http", "tcp", 80);   // annonce un service (utile avec un serveur web)
  }
}

void loop() {
  // le reste du programme tourne, connecté ou non
  delay(1000);
}

Point d'accès#

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

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

  // L'ESP32 crée son propre réseau (pas besoin de routeur)
  WiFi.mode(WIFI_AP);
  WiFi.softAP("ESP32-Enjmin-01", "motdepasse");   // mot de passe : 8 caractères minimum
  Serial.printf("Réseau créé, IP de l'ESP32 : %s\n", WiFi.softAPIP().toString().c_str());   // 192.168.4.1
}

void loop() {
  Serial.printf("Appareils connectés : %d\n", WiFi.softAPgetStationNum());
  delay(2000);
}

WiFi.mode(WIFI_AP_STA) permet d'être à la fois point d'accès et client.

Choisir le réseau sans recompiler : WiFiManager#

Mettre le SSID et le mot de passe dans le code oblige à recompiler à chaque changement de lieu. Avec WiFiManager (lib_deps = tzapu/WiFiManager), l'ESP32 retient le dernier réseau ; s'il ne le trouve pas, il crée un point d'accès avec une page de configuration :

#include <Arduino.h>
#include <WiFiManager.h>

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

  WiFiManager wm;
  // wm.resetSettings();                    // décommenter pour oublier le réseau enregistré

  // Ajout d'un champ personnalisé dans la page de configuration
  WiFiManagerParameter godotIp("godot", "IP de l'ordinateur Godot", "172.20.10.3", 16);
  wm.addParameter(&godotIp);

  wm.setConfigPortalTimeout(180);           // au bout de 3 min sans configuration : on continue sans Wi-Fi
  // Si aucun réseau connu n'est joignable, l'ESP32 crée le point d'accès « ESP32-Setup » :
  // s'y connecter avec un téléphone ouvre automatiquement la page de choix du réseau (portail captif)
  bool connected = wm.autoConnect("ESP32-Setup", "Enjmin666");

  if (connected) {
    Serial.printf("Connecté à %s, IP %s\n", WiFi.SSID().c_str(), WiFi.localIP().toString().c_str());
    Serial.printf("IP Godot saisie : %s\n", godotIp.getValue());   // à enregistrer avec Preferences
  } else {
    Serial.println("Pas de réseau configuré");
  }
}

void loop() {}

UDP avec l'ESP32#

#include <Arduino.h>
#include "AsyncUDP.h"
#include "WiFi.h"

const char* ssid = "WIFI_NAME";
const char* pass = "WIFI_PASSWORD";
const int relay = 23;
AsyncUDP udp;

void setup() {
  Serial.begin(115200);
  pinMode(relay, OUTPUT);
  WiFi.disconnect(true);
  WiFi.mode(WIFI_STA);
  WiFi.begin(ssid, pass);
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }
  if (udp.listen(1234)) {
    Serial.print("WiFi connected. UDP Listening on IP: ");
    Serial.println(WiFi.localIP());
    udp.onPacket([](AsyncUDPPacket packet) {
      Serial.print("UDP Packet Type: ");
      Serial.print(packet.isBroadcast()   ? "Broadcast"
                   : packet.isMulticast() ? "Multicast"
                                          : "Unicast");
      Serial.print(", From: ");
      Serial.print(packet.remoteIP());
      Serial.print(":");
      Serial.print(packet.remotePort());
      Serial.print(", To: ");
      Serial.print(packet.localIP());
      Serial.print(":");
      Serial.print(packet.localPort());
      Serial.print(", Length: ");
      Serial.print(packet.length());  // dlzka packetu
      Serial.print(", Data: ");
      Serial.write(packet.data(), packet.length());
      Serial.println();
      String myString = (const char*)packet.data();
      if (myString.startsWith("ON")) {
        Serial.println("Turning on relay");
        digitalWrite(relay, LOW);
      } else if (myString.startsWith("OFF")) {
        Serial.println("Turning off relay");
        digitalWrite(relay, HIGH);
      }
      packet.printf("Got %u bytes of data", packet.length());
    });
  }
}

void loop() {
  delay(1000);
  udp.broadcast("Anyone here?");
}

OSC avec Godot#

On utilise ArduinoOSC côté ESP32 (lib_deps = hideakitai/ArduinoOSC).

envoyer un message OscWiFi.send(ip, port, "/address", v1, v2…)
envoyer une variable en continu OscWiFi.publish(ip, port, "/address", variable)->setFrameRate(hz)
recevoir OscWiFi.subscribe(port, "/address", [](const OscMessage& m) { m.arg<int>(0); … })
faire tourner OscWiFi.update() à chaque tour de loop()

Côté ESP32#

Montage : potentiomètre sur GPIO 4, bouton sur GPIO 6.

Potentiomètre et bouton

Ouvrir ce montage dans Plot Circuits ↗

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

const char* SSID = "MySSID";
const char* PASS = "Enjmin666";
const char* COMPUTER_IP = "172.20.10.3";   // l'ordinateur qui fait tourner Godot
const int PORT_GODOT = 9999;           // Godot écoute ici
const int PORT_ESP32 = 8888;           // l'ESP32 écoute ici

int sensorValue = 0;
bool buttonState = false;

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

  WiFi.begin(SSID, PASS);
  while (WiFi.status() != WL_CONNECTED) delay(100);
  Serial.println(WiFi.localIP());

  // Réception : /led r g b (subscribe APRÈS la connexion Wi-Fi)
  OscWiFi.subscribe(PORT_ESP32, "/led", [](const OscMessage& m) {
    neopixelWrite(48, m.arg<int>(0), m.arg<int>(1), m.arg<int>(2));
  });

  // Envoi automatique d'une variable à 30 Hz
  OscWiFi.publish(COMPUTER_IP, PORT_GODOT, "/sensor", sensorValue)->setFrameRate(30.f);
}

void loop() {
  sensorValue = analogRead(4);

  // Envoi ponctuel, seulement quand l'état du bouton change
  bool b = digitalRead(6) == LOW;
  if (b != buttonState) {
    buttonState = b;
    OscWiFi.send(COMPUTER_IP, PORT_GODOT, "/button", (int) b);
  }

  OscWiFi.update();   // envoie et reçoit : à appeler à chaque tour, sans long delay()
}

subscribe doit être appelé après la connexion Wi-Fi : sinon, il est ignoré.

Côté Godot#

Pas besoin d'addon : un message OSC est un petit paquet UDP. Ce script autonome envoie et reçoit des entiers, des flottants et du texte. Enregistrez-le dans le projet sous osc.gd :

extends Node
## OSC minimal pour Godot : envoi et réception de messages (entiers, flottants, texte).
## Les bundles OSC (#bundle) ne sont pas gérés.

signal message_received(address: String, values: Array)

@export var listen_port := 9999
var udp := PacketPeerUDP.new()

func _ready():
	udp.bind(listen_port)

func _process(_delta):
	while udp.get_available_packet_count() > 0:
		var m = decode(udp.get_packet())
		if m != null:
			message_received.emit(m[0], m[1])

func send(ip: String, port: int, address: String, values: Array = []):
	var b := StreamPeerBuffer.new()
	b.big_endian = true
	var types := ","
	for v in values:
		types += "i" if v is int else ("f" if v is float else "s")
	_write_string(b, address)
	_write_string(b, types)
	for v in values:
		if v is int:
			b.put_32(v)
		elif v is float:
			b.put_float(v)
		else:
			_write_string(b, str(v))
	udp.set_dest_address(ip, port)
	udp.put_packet(b.data_array)

func decode(packet: PackedByteArray):
	var b := StreamPeerBuffer.new()
	b.big_endian = true
	b.data_array = packet
	var address := _read_string(b)
	if not address.begins_with("/"):
		return null
	var types := _read_string(b)
	var values := []
	for t in types.substr(1):
		match t:
			"i": values.append(b.get_32())
			"f": values.append(b.get_float())
			"s": values.append(_read_string(b))
	return [address, values]

# Un texte OSC se termine par 1 à 4 octets nuls (taille multiple de 4)
func _write_string(b: StreamPeerBuffer, text: String):
	var bytes := text.to_utf8_buffer()
	b.put_data(bytes)
	for i in 4 - bytes.size() % 4:
		b.put_u8(0)

func _read_string(b: StreamPeerBuffer) -> String:
	var data := b.data_array
	var start := b.get_position()
	var end := start
	while end < data.size() and data[end] != 0:
		end += 1
	b.seek(min(start + ((end - start + 4) & ~3), data.size()))
	return data.slice(start, end).get_string_from_utf8()

Et un exemple d'utilisation : le cercle grossit avec le capteur, un clic change la couleur de la LED de l'ESP32 :

extends Node2D

const IP_ESP32 = "172.20.10.5"    # ← l'adresse affichée par l'ESP32
const PORT_ESP32 = 8888

var osc
var sensor = 0
var color = Color.ORANGE

func _ready():
	RenderingServer.set_default_clear_color(Color("#1d1b33"))
	osc = preload("res://osc.gd").new()   # port d'écoute : 9999 par défaut
	add_child(osc)
	osc.message_received.connect(_on_message)

func _on_message(address, values):
	match address:
		"/sensor":
			sensor = values[0]
		"/button":
			print("bouton : ", values[0])

func _input(event):
	if event is InputEventMouseButton and event.pressed:
		color = Color(randf(), randf(), randf())
		osc.send(IP_ESP32, PORT_ESP32, "/led", [color.r8, color.g8, color.b8])

func _process(_delta):
	queue_redraw()

func _draw():
	draw_circle(get_viewport_rect().size / 2, remap(sensor, 0, 4095, 10, 250), color)

Alternative : l'addon GodOSC de l'AssetLib (nœuds OSCServer / OSCClient).


ESP-NOW : ESP32 ↔ ESP32 sans routeur#

ESP-NOW est un protocole d'Espressif : des messages de 250 octets max, envoyés directement d'une carte à l'autre par la radio Wi-Fi, sans réseau ni routeur, avec une latence de l'ordre de la milliseconde et une portée de plusieurs dizaines de mètres. Idéal pour des contrôleurs sans fil : plusieurs ESP32 « manettes » envoient à une ESP32 « réceptrice » branchée en USB à l'ordinateur.

Émetteur#

Potentiomètre et bouton

Ouvrir ce montage dans Plot Circuits ↗

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

// Adresse du récepteur (affichée par son programme), ou diffusion à tous :
uint8_t receiver[] = {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF};

// Le message : une simple struct, identique des deux côtés
struct Message {
  uint16_t sensor;
  bool button;
};

void onSent(const uint8_t* mac, esp_now_send_status_t status) {
  if (status != ESP_NOW_SEND_SUCCESS) Serial.println("échec d'envoi");
}

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

  WiFi.mode(WIFI_STA);              // ESP-NOW utilise la radio Wi-Fi, sans routeur
  if (esp_now_init() != ESP_OK) {
    Serial.println("ESP-NOW : erreur d'initialisation");
    return;
  }
  esp_now_register_send_cb(onSent);

  esp_now_peer_info_t peer = {};
  memcpy(peer.peer_addr, receiver, 6);
  peer.channel = 0;                 // canal Wi-Fi courant
  peer.encrypt = false;
  esp_now_add_peer(&peer);
}

void loop() {
  Message msg;
  msg.sensor = analogRead(4);
  msg.button = digitalRead(6) == LOW;
  esp_now_send(receiver, (uint8_t*) &msg, sizeof(msg));
  delay(20);                        // 50 messages par seconde
}

Récepteur#

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

struct Message {
  uint16_t sensor;
  bool button;
};

volatile Message latest;           // écrit dans le callback, lu dans loop()
volatile bool hasNew = false;

// Appelé par la tâche Wi-Fi à chaque message reçu : traitement court !
void onReceive(const uint8_t* mac, const uint8_t* data, int len) {
  if (len == sizeof(Message)) {
    memcpy((void*) &latest, data, sizeof(Message));
    hasNew = true;
  }
}

void setup() {
  Serial.begin(115200);
  WiFi.mode(WIFI_STA);
  Serial.printf("Mon adresse MAC : %s\n", WiFi.macAddress().c_str());   // à copier dans l'émetteur

  esp_now_init();
  esp_now_register_recv_cb(onReceive);
}

void loop() {
  if (hasNew) {
    hasNew = false;
    Serial.printf("capteur=%4u bouton=%d\n", latest.sensor, latest.button);
    neopixelWrite(48, latest.button ? 60 : 0, map(latest.sensor, 0, 4095, 0, 60), 0);
  }
}
Éditer