Web — serveur HTTP, API JSON, WebSocket#
← Réseau · Programme · Multitâche →
L'ESP32 peut servir une page web : réglages depuis un téléphone, tableau de bord, télécommande… Sans application à installer, depuis n'importe quel appareil sur le même réseau (ou connecté au point d'accès de l'ESP32).
Serveur HTTP et API JSON#
La librairie WebServer est incluse dans Arduino-ESP32. Ici : une page HTML (servie depuis la flash) qui interroge une petite API.
Montage : un capteur (ici un potentiomètre) sur GPIO 4.
Ouvrir ce montage dans Plot Circuits ↗
#include <Arduino.h>
#include <WiFi.h>
#include <ESPmDNS.h>
#include <WebServer.h>
const char* SSID = "MySSID";
const char* PASS = "Enjmin666";
WebServer server(80);
int threshold = 2000; // un réglage modifiable depuis le navigateur
// Page HTML stockée en flash (PROGMEM), avec un peu de JavaScript qui interroge l'API
const char PAGE[] PROGMEM = R"rawliteral(
<!doctype html><meta charset="utf-8"><meta name="viewport" content="width=device-width">
<title>ESP32</title>
<h1>Capteur : <span id="v">…</span></h1>
<p>Seuil : <input id="s" type="range" min="0" max="4095" oninput="setThreshold(this.value)"> <span id="sv"></span></p>
<script>
async function poll() {
const r = await fetch('/api/state'); // GET → JSON
const e = await r.json();
v.textContent = e.sensor + (e.sensor > e.threshold ? ' (au-dessus !)' : '');
if (document.activeElement !== s) { s.value = e.threshold; sv.textContent = e.threshold; }
}
function setThreshold(x) { sv.textContent = x; fetch('/api/threshold?value=' + x, { method: 'POST' }); }
setInterval(poll, 200);
</script>
)rawliteral";
void handleRoot() {
server.send_P(200, "text/html", PAGE);
}
void handleState() {
char json[64];
snprintf(json, sizeof json, "{\"sensor\":%d,\"threshold\":%d}", analogRead(4), threshold);
server.send(200, "application/json", json);
}
void handleThreshold() {
if (!server.hasArg("value")) {
server.send(400, "text/plain", "paramètre 'value' manquant");
return;
}
threshold = constrain(server.arg("value").toInt(), 0, 4095);
server.send(200, "text/plain", "ok");
}
void setup() {
Serial.begin(115200);
WiFi.begin(SSID, PASS);
while (WiFi.status() != WL_CONNECTED) delay(100);
MDNS.begin("enjmin-01");
Serial.printf("http://%s ou http://enjmin-01.local\n", WiFi.localIP().toString().c_str());
server.on("/", HTTP_GET, handleRoot);
server.on("/api/state", HTTP_GET, handleState);
server.on("/api/threshold", HTTP_POST, handleThreshold);
server.onNotFound([]() { server.send(404, "text/plain", "introuvable"); });
server.begin();
}
void loop() {
server.handleClient(); // à appeler sans arrêt
}
GET /api/staterenvoie du JSON,POST /api/threshold?value=…modifie un réglage : la même API est utilisable depuis un script, Godot (HTTPRequest), TouchDesigner…R"rawliteral( … )rawliteral": chaîne brute C++, pratique pour coller du HTML sans échapper les guillemets.- Pour des pages plus grosses (CSS, images, JS) : voir LittleFS ci-dessous.
- Pour garder le seuil après redémarrage : Preferences.
WebServertraite une requête à la fois ; pour beaucoup de clients simultanés, voir ESPAsyncWebServer.
Un vrai site : fichiers HTML, CSS et JS avec LittleFS#
Écrire le HTML dans une chaîne C++ devient vite pénible. Avec LittleFS, les fichiers du site vivent dans un dossier data/ du projet, et sont copiés dans la flash de l'ESP32 :
mon-projet/
├── platformio.ini ← ajouter : board_build.filesystem = littlefs
├── src/main.cpp
└── data/
├── index.html
├── style.css
└── app.js
#include <Arduino.h>
#include <WiFi.h>
#include <ESPmDNS.h>
#include <WebServer.h>
#include <LittleFS.h>
const char* SSID = "MySSID";
const char* PASS = "Enjmin666";
WebServer server(80);
void handleState() {
char json[48];
snprintf(json, sizeof json, "{\"sensor\":%d,\"uptime\":%lu}", analogRead(4), millis() / 1000);
server.send(200, "application/json", json);
}
void setup() {
Serial.begin(115200);
if (!LittleFS.begin(true)) { // true : formate la partition si elle est vide
Serial.println("LittleFS : erreur de montage");
return;
}
Serial.printf("LittleFS : %u Ko utilisés sur %u\n", LittleFS.usedBytes() / 1024, LittleFS.totalBytes() / 1024);
WiFi.begin(SSID, PASS);
while (WiFi.status() != WL_CONNECTED) delay(100);
MDNS.begin("enjmin-01");
Serial.printf("http://%s ou http://enjmin-01.local\n", WiFi.localIP().toString().c_str());
server.on("/api/state", HTTP_GET, handleState); // l'API d'abord…
server.serveStatic("/", LittleFS, "/", "max-age=60"); // …puis tous les fichiers du dossier data/
server.onNotFound([]() { server.send(404, "text/plain", "introuvable"); });
server.begin();
}
void loop() {
server.handleClient();
}
data/index.html :
<!doctype html>
<html lang="fr">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>ESP32</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<h1>Capteur</h1>
<div class="jauge"><div id="barre"></div></div>
<p><span id="valeur">…</span> · allumé depuis <span id="uptime">…</span> s</p>
<script src="app.js"></script>
</body>
</html>
data/style.css :
body { font-family: system-ui, sans-serif; background: #1d1b33; color: #fff; text-align: center; }
.jauge { width: 80%; height: 30px; margin: 2em auto; background: #333; border-radius: 15px; overflow: hidden; }
#barre { height: 100%; width: 0; background: orange; transition: width .1s; }
data/app.js :
async function update() {
const state = await (await fetch('/api/state')).json();
document.getElementById('barre').style.width = (state.sensor / 4095 * 100) + '%';
document.getElementById('valeur').textContent = state.sensor;
document.getElementById('uptime').textContent = state.uptime;
}
setInterval(update, 100);
- Envoi des fichiers :
pio run -t uploadfs(ou icône PlatformIO → Platform → Upload Filesystem Image). Le programme et les fichiers s'envoient séparément : modifier le HTML ne demande pas de recompiler. - La partition
default.csvréserve environ 1,4 Mo aux fichiers. LittleFSsert aussi à enregistrer des données :File f = LittleFS.open("/log.csv", "a"); f.println(...); f.close();.
Requêtes HTTP sortantes#
L'ESP32 peut aussi être client : interroger une API web, envoyer des mesures à un serveur… Avec HTTPClient (inclus) et ArduinoJson (lib_deps = bblanchon/ArduinoJson) pour lire et écrire du JSON :
#include <Arduino.h>
#include <WiFi.h>
#include <WiFiClientSecure.h>
#include <HTTPClient.h>
#include <ArduinoJson.h>
const char* SSID = "MySSID";
const char* PASS = "Enjmin666";
// API météo gratuite, sans clé : température actuelle à Angoulême
const char* WEATHER_URL =
"https://api.open-meteo.com/v1/forecast?latitude=45.65&longitude=0.16¤t=temperature_2m,wind_speed_10m";
float getTemperature() {
WiFiClientSecure client;
client.setInsecure(); // HTTPS sans vérifier le certificat (simple, mais moins sûr)
HTTPClient http;
http.begin(client, WEATHER_URL);
int code = http.GET();
if (code != HTTP_CODE_OK) {
Serial.printf("Erreur HTTP %d\n", code);
http.end();
return NAN;
}
JsonDocument doc; // ArduinoJson 7 : taille gérée automatiquement
DeserializationError err = deserializeJson(doc, http.getStream());
http.end();
if (err) {
Serial.printf("JSON invalide : %s\n", err.c_str());
return NAN;
}
Serial.printf("Vent : %.1f km/h\n", doc["current"]["wind_speed_10m"].as<float>());
return doc["current"]["temperature_2m"].as<float>();
}
void postValue(int value) {
// Envoi de données vers un serveur (ici un service de test qui renvoie ce qu'il reçoit)
WiFiClientSecure client;
client.setInsecure();
HTTPClient http;
http.begin(client, "https://httpbin.org/post");
http.addHeader("Content-Type", "application/json");
JsonDocument doc;
doc["device"] = "enjmin-01";
doc["sensor"] = value;
String body;
serializeJson(doc, body);
int code = http.POST(body);
Serial.printf("POST → %d\n", code);
http.end();
}
void setup() {
Serial.begin(115200);
WiFi.begin(SSID, PASS);
while (WiFi.status() != WL_CONNECTED) delay(100);
}
void loop() {
Serial.printf("Température : %.1f °C\n", getTemperature());
postValue(analogRead(4));
delay(60000); // pas plus d'une requête par minute : on ménage les API
}
http.getStream()fait lire le JSON directement par ArduinoJson, sans charger toute la réponse en mémoire.setInsecure()désactive la vérification du certificat HTTPS : acceptable pour un prototype, à éviter pour des données sensibles (setCACert(...)sinon).- Les requêtes HTTPS sont lentes (de 0,5 à 2 s) et bloquantes : dans un projet interactif, faites-les dans une tâche séparée.
WebSocket#
Le HTTP est une question / réponse. Le WebSocket garde une connexion ouverte dans les deux sens : idéal pour du temps réel (capteur à 30 Hz, commandes instantanées). Librairie : lib_deps = links2004/WebSockets.
Côté ESP32#
#include <Arduino.h>
#include <WiFi.h>
#include <WebSocketsServer.h>
const char* SSID = "MySSID";
const char* PASS = "Enjmin666";
WebSocketsServer ws(81); // ws://<ip-esp32>:81
void onWsEvent(uint8_t client, WStype_t type, uint8_t* data, size_t len) {
switch (type) {
case WStype_CONNECTED:
Serial.printf("client %u connecté depuis %s\n", client, ws.remoteIP(client).toString().c_str());
break;
case WStype_DISCONNECTED:
Serial.printf("client %u déconnecté\n", client);
break;
case WStype_TEXT: {
// Message attendu : "led r g b"
int r, g, b;
if (sscanf((const char*) data, "led %d %d %d", &r, &g, &b) == 3) {
neopixelWrite(48, r, g, b);
}
break;
}
default:
break;
}
}
void setup() {
Serial.begin(115200);
WiFi.begin(SSID, PASS);
while (WiFi.status() != WL_CONNECTED) delay(100);
Serial.printf("ws://%s:81\n", WiFi.localIP().toString().c_str());
ws.begin();
ws.onEvent(onWsEvent);
}
uint32_t lastSend = 0;
void loop() {
ws.loop();
// 30 fois par seconde : la valeur du capteur à tous les clients connectés
if (millis() - lastSend > 33) {
lastSend = millis();
char txt[32];
snprintf(txt, sizeof txt, "{\"sensor\":%d}", analogRead(4));
ws.broadcastTXT(txt);
}
}
Côté navigateur#
À ajouter dans une page servie par l'ESP32 (ou dans un fichier HTML local) :
<script>
const ws = new WebSocket('ws://' + location.hostname + ':81');
ws.onmessage = e => console.log(JSON.parse(e.data).capteur);
function led(r, g, b) { ws.send(`led ${r} ${g} ${b}`); }
</script>
Côté Godot#
Godot sait faire du WebSocket nativement (WebSocketPeer) :
extends Node2D
var url = "ws://172.20.10.5:81" # ← l'adresse affichée par l'ESP32
var ws = WebSocketPeer.new()
var sensor = 0 # dernière valeur du capteur reçue
var background_color = Color("#1d1b33")
func _ready():
RenderingServer.set_default_clear_color(background_color)
ws.connect_to_url(url)
func _process(delta):
ws.poll() # à appeler à chaque image : fait avancer la connexion
if ws.get_ready_state() == WebSocketPeer.STATE_OPEN:
while ws.get_available_packet_count() > 0:
var data = JSON.parse_string(ws.get_packet().get_string_from_utf8())
if data != null:
sensor = data["sensor"]
queue_redraw()
func _input(event):
# Clic : couleur au hasard sur la LED de l'ESP32
if event is InputEventMouseButton and event.pressed and ws.get_ready_state() == WebSocketPeer.STATE_OPEN:
var color = Color(randf(), randf(), randf())
ws.send_text("led %d %d %d" % [color.r8, color.g8, color.b8])
func _draw():
var radius = remap(sensor, 0, 4095, 10, 200)
draw_circle(get_viewport_rect().size / 2, radius, Color.ORANGE)
ws.poll()doit être appelé à chaque image ; les messages s'accumulent sinon.- Format des messages : du texte simple (
led r g b) ou du JSON (JSON.stringify/JSON.parse_string). Pour beaucoup de données,send/get_packeten binaire (PackedByteArray).