Skip to content
Learni
View all tutorials
Systèmes Embarqués

How to Implement an IoT Server on ESP32 in 2026

Lire en français

Introduction

The ESP32, Espressif's powerful microcontroller with built-in WiFi/Bluetooth, dominates IoT projects in 2026 thanks to its dual-core processor, 520 KB SRAM, and FreeRTOS support. This advanced tutorial guides you through building a complete IoT node: secure WiFi connection, DHT22 sensor reading (temperature/humidity), asynchronous web server for real-time dashboard, MQTT publishing to a remote broker, and OTA updates without cables. Unlike basic approaches, we use ESPAsyncWebServer for optimal responsiveness (non-blocking), robust error handling, and memory optimization. Ideal for home automation, industrial monitoring, or scalable prototypes. By the end, you'll have production-ready firmware that's bookmark-worthy for any embedded developer. Estimated time: 2 hours to implement and test.

Prerequisites

  • Arduino IDE 2.3.2+ installed (download from arduino.cc)
  • ESP32 boards package 3.0.5+ (via Boards Manager)
  • Libraries: DHT sensor library by Adafruit v1.4.6, ESPAsyncWebServer v3.1.0, AsyncTCP v1.1.1, PubSubClient v2.8
  • Hardware: ESP32 DevKit V1, DHT22 sensor, 4.7kΩ resistors (pull-up), breadboard, jumper cables
  • Free MQTT broker (e.g., broker.hivemq.com:1883 for testing)
  • Knowledge: C++, basic multitasking, TCP/IP networking

Installing ESP32 Support

terminal
cd ~
# Ouvrir Arduino IDE > File > Preferences
# Ajouter URL dans Additional Boards Manager: https://raw.githubusercontent.com/espressif/arduino-esp32/gh-pages/package_esp32_index.json

# Dans Tools > Board > Boards Manager, chercher "esp32" et installer "esp32 by Espressif Systems" v3.0.5

echo "Boards Manager URL ajoutée. Redémarrez IDE et sélectionnez: Tools > Board > ESP32 Arduino > ESP32 Dev Module"

# Vérifier ports: Tools > Port > /dev/cu.usbserial-* (Mac) ou COM* (Win)
# Upload speed: 921600, Flash freq: 80MHz, Flash mode: QIO

These commands and steps set up the IDE to compile and flash the ESP32. Use version 3.0+ for PSRAM compatibility and enhanced WiFi security (WPA3). Pitfall: without this URL, boards won't appear; always test with a Blink sketch after installation.

Basic Sketch: LED Blink

blink.ino
#include "Arduino.h"

#define LED_PIN 2

void setup() {
  Serial.begin(115200);
  pinMode(LED_PIN, OUTPUT);
  Serial.println("ESP32 Blink démarré");
}

void loop() {
  digitalWrite(LED_PIN, HIGH);
  Serial.println("LED ON");
  delay(1000);
  digitalWrite(LED_PIN, LOW);
  Serial.println("LED OFF");
  delay(1000);
}

This minimal sketch validates your installation: the onboard LED (GPIO2) blinks every second with Serial logs for debugging. Compile (Ctrl+R), upload (Ctrl+U). Advanced note: delay() is blocking; for real IoT, prefer millis() for non-blocking operation. Check the Serial Monitor at 115200 baud.

WiFi Connection and Basic Web Server

Now, connect the ESP32 to your WiFi network with WPA2/3 authentication. We're introducing ESPAsyncWebServer for an asynchronous HTTP server: unlike the synchronous WebServer, it handles 10x more requests without freezing the loop(). Analogy: like a multitasking bartender vs. one who serves one at a time. Configure your SSID/password in the code below.

WiFi + Async Web Server

wifi_webserver.ino
#include <WiFi.h>
#include <ESPAsyncWebServer.h>
#include <AsyncTCP.h>

const char* ssid = "VOTRE_SSID";
const char* password = "VOTRE_PASSWORD";

AsyncWebServer server(80);

void setup() {
  Serial.begin(115200);
  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED) {
    delay(1000);
    Serial.println("Connexion WiFi...");
  }
  Serial.println("IP: " + WiFi.localIP().toString());

  server.on("/", HTTP_GET, [](AsyncWebServerRequest *request){
    request->send(200, "text/plain", "Hello ESP32 IoT Server!");
  });

  server.begin();
  Serial.println("Serveur démarré");
}

void loop() {
  // Non-bloquant !
}

This code connects to WiFi (with auto-timeout), starts a server on port 80. Access the IP in your browser: "Hello". Async advantages: loop() stays free for future tasks. Pitfall: forget WiFi.mode(WIFI_STA); it's default OK, but check if firewall/router blocks port 80.

Integrating DHT22 Sensor

dht_webserver.ino
#include <WiFi.h>
#include <ESPAsyncWebServer.h>
#include <AsyncTCP.h>
#include <DHT.h>

#define DHT_PIN 4
#define DHT_TYPE DHT22
DHT dht(DHT_PIN, DHT_TYPE);

const char* ssid = "VOTRE_SSID";
const char* password = "VOTRE_PASSWORD";
AsyncWebServer server(80);

String processor(const String& var) {
  if (var == "TEMPERATURE") return String(dht.readTemperature());
  if (var == "HUMIDITY") return String(dht.readHumidity());
  return String();
}

void setup() {
  Serial.begin(115200);
  dht.begin();
  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED) delay(1000);
  Serial.println("IP: " + WiFi.localIP().toString());

  server.on("/", HTTP_GET, [](AsyncWebServerRequest *request){
    request->send_P(200, "text/html", R"raw(<!DOCTYPE html><html><body><h1>IoT Dashboard</h1><p>Temp: TEMPERATURE °C</p><p>Hum: HUMIDITY %</p></body></html>)raw", processor);
  });

  server.begin();
}

void loop() {}

Adds DHT22 on GPIO4 (4.7kΩ pull-up required). Dynamic HTML dashboard via processor(): reads sensor on each GET /. Non-blocking thanks to async. Pitfall: dht.read() times out after 2s if bus fails; add isNan() checks in production (e.g., if(isnan(temp)) return "N/A";).

MQTT Publishing and Remote Monitoring

MQTT enables pushing data to dashboards (Node-RED, Home Assistant). Here, it publishes temp/hum every 30s on topic "esp32/sensor". Use a public broker for testing, then your own (Mosquitto). Advanced: QoS 1 for reliability.

Complete IoT Server with MQTT

iot_mqtt_complete.ino
#include <WiFi.h>
#include <ESPAsyncWebServer.h>
#include <AsyncTCP.h>
#include <DHT.h>
#include <PubSubClient.h>

#define DHT_PIN 4
#define DHT_TYPE DHT22
DHT dht(DHT_PIN, DHT_TYPE);

const char* ssid = "VOTRE_SSID";
const char* password = "VOTRE_PASSWORD";
const char* mqtt_server = "broker.hivemq.com";

WiFiClient espClient;
PubSubClient client(espClient);
AsyncWebServer server(80);
unsigned long lastMsg = 0;

String processor(const String& var) {
  float t = dht.readTemperature();
  float h = dht.readHumidity();
  if (var == "TEMPERATURE") return isnan(t) ? "N/A" : String(t);
  if (var == "HUMIDITY") return isnan(h) ? "N/A" : String(h);
  return String();
}

void setup() {
  Serial.begin(115200);
  dht.begin();
  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED) { delay(500); Serial.print("."); }
  Serial.println("IP: " + WiFi.localIP().toString());

  client.setServer(mqtt_server, 1883);

  server.on("/", HTTP_GET, [](AsyncWebServerRequest *request){
    request->send_P(200, "text/html", R"raw(<!DOCTYPE html><html><body><h1>ESP32 IoT</h1><p>Temp: TEMPERATURE °C</p><p>Hum: HUMIDITY %</p></body></html>)raw", processor);
  });
  server.begin();
}

void loop() {
  if (!client.connected()) {
    while (!client.connect("ESP32Client")) { delay(5000); }
  }
  client.loop();

  unsigned long now = millis();
  if (now - lastMsg > 30000) {
    lastMsg = now;
    float t = dht.readTemperature();
    float h = dht.readHumidity();
    if (!isnan(t) && !isnan(h)) {
      client.publish("esp32/sensor", ("T:" + String(t) + " H:" + String(h)).c_str());
    }
  }
}

Complete firmware: web dashboard + MQTT publish every 30s (non-blocking with millis()). Auto MQTT reconnect. Test with MQTT Explorer on "esp32/sensor". Pitfall: String() can leak memory if overused; limit to cron-like tasks. QoS=0 for performance.

Adding OTA for Wireless Updates

iot_ota_final.ino
#include <WiFi.h>
#include <ESPAsyncWebServer.h>
#include <AsyncTCP.h>
#include <DHT.h>
#include <PubSubClient.h>
#include <ArduinoOTA.h>

#define DHT_PIN 4
#define DHT_TYPE DHT22
DHT dht(DHT_PIN, DHT_TYPE);

const char* ssid = "VOTRE_SSID";
const char* password = "VOTRE_PASSWORD";
const char* mqtt_server = "broker.hivemq.com";
const char* ota_password = "admin";

WiFiClient espClient;
PubSubClient client(espClient);
AsyncWebServer server(80);
unsigned long lastMsg = 0;

String processor(const String& var);

void setup() {
  Serial.begin(115200);
  dht.begin();
  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED) { delay(500); Serial.print("."); }
  Serial.println("IP: " + WiFi.localIP().toString());

  ArduinoOTA.setHostname("esp32-iot");
  ArduinoOTA.setPassword(ota_password);
  ArduinoOTA.onStart([]() { Serial.println("OTA Start"); });
  ArduinoOTA.onEnd([]() { Serial.println("OTA End"); });
  ArduinoOTA.onError([](ota_error_t error) { Serial.printf("OTA Error[%u]: ", error); });
  ArduinoOTA.begin();

  client.setServer(mqtt_server, 1883);

  server.on("/", HTTP_GET, [](AsyncWebServerRequest *request){
    request->send_P(200, "text/html", R"raw(<!DOCTYPE html><html><body><h1>ESP32 IoT OTA</h1><p>Temp: TEMPERATURE °C</p><p>Hum: HUMIDITY %</p></body></html>)raw", processor);
  });
  server.begin();
}

void loop() {
  ArduinoOTA.handle();
  if (!client.connected()) {
    while (!client.connect("ESP32Client")) { delay(5000); }
  }
  client.loop();

  unsigned long now = millis();
  if (now - lastMsg > 30000) {
    lastMsg = now;
    float t = dht.readTemperature();
    float h = dht.readHumidity();
    if (!isnan(t) && !isnan(h)) {
      client.publish("esp32/sensor", ("T:" + String(t) + " H:" + String(h)).c_str());
    }
  }
}

String processor(const String& var) {
  float t = dht.readTemperature();
  float h = dht.readHumidity();
  if (var == "TEMPERATURE") return isnan(t) ? "N/A" : String(t);
  if (var == "HUMIDITY") return isnan(h) ? "N/A" : String(h);
  return String();
}

Final version with OTA: update via IDE (Tools > Port > esp32-iot at IP). Password protected. Call handle() in loop(). Pitfall: select "Default 4MB with OTA" partition scheme in Tools; otherwise, risk bricking. Production: add HTTPS + auth.

Best Practices

  • Non-blocking everywhere: Use millis() over delay(), FreeRTOS tasks for heavy ops (e.g., xTaskCreatePinnedToCore).
  • Memory management: Avoid String concat; use char buffers. Monitor heap with Serial.println(ESP.getFreeHeap()).
  • Security: WPA3, MQTT TLS (WiFiClientSecure), API keys, upgrade port 80 to HTTPS (ESPmDNS).
  • Robust debugging: NTP for timestamps (NTPClient lib), crash dumps (ESP.getResetReason()).
  • Power optimization: Deep sleep for battery (esp_sleep_enable_timer_wakeup()).

Common Errors to Avoid

  • Watchdog reset: loop() blocked by long delay(); force yield() or vTaskDelay().
  • Infinite WiFi reconnect: Add timeout (WiFi.reconnect() + max 10 tries).
  • DHT22 NaN: Missing pull-up or cable >20cm; debounce with 3-read average.
  • OTA failure: Flash size mismatch or empty password; always test on breadboard first.

Next Steps