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
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: QIOThese 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
#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
#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
#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
#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
#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
- Official docs: Espressif ESP32
- Advanced libs: ESP32Camera, LVGL for TFT UI
- Projects: Integrate with Blynk/Home Assistant
- Expert training: Check out our Learni IoT embedded courses
- Community: Reddit r/esp32, ESP32 Discord.