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.

Capteur 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
}

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);

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&current=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
}

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)
Éditer