ESP32 & IoT — Du Zéro à l'Expert
ESP32 WiFi · BLE · IoT 📡 MQTT 🖥️ NODE-RED 🌡️ BME280
Cours Complet · IoT · ESP32 + MQTT + Mosquitto + Node-RED

// GPIO · WiFi · MQTT · Mosquitto · Node-RED ESP32 & IoT

Architecture matérielle, GPIO, ADC, WiFi, protocole MQTT, broker Mosquitto, Node-RED et projet complet. Chaque bit expliqué. Zéro hypothèse.

13Modules
5Projets
240MHz Dual
40Quiz QCM
3.3VLogique
Module 01

Qu'est-ce que l' ESP32 ?

L'ESP32 est un microcontrôleur sur une seule puce intégrant nativement WiFi et Bluetooth. Pour moins de 5€, il mesure le monde physique ET l'envoie sur Internet.

CPU
Dual-Core 240 MHz
Xtensa LX6 × 2. Un cœur pour ton code, un pour le WiFi. FreeRTOS intégré.
RADIO
WiFi + BLE
802.11 b/g/n 2.4 GHz. Bluetooth 4.2 Classic + BLE. Antenne PCB intégrée.
I/O
34 GPIO
Digitales, ADC 12 bits, PWM, I2C, SPI, UART, DAC — tout en un seul circuit.
CONSO
10 µA Deep Sleep
Tourne des mois sur batterie. RTC maintient le temps pendant le sommeil.

Arduino Uno vs ESP32

CritèreArduino UnoESP32
CPUATmega328P 16 MHzXtensa LX6 × 2 à 240 MHz
RAM2 KB520 KB (260× plus)
Flash32 KB4 MB (125× plus)
WiFi❌ Absent✅ Intégré
Logique5 V3.3 V ⚠️
ADC10 bits12 bits
Prix~15 €~3–8 €

Architecture interne ESP32

CPU Xtensa LX6 Dual Core — 240 MHz — FreeRTOS← 2 cœurs gérés par OS temps réel
ROM 448 KB — Bootloader + bibliothèques bas-niveau← lecture seule, gravée usine
SRAM 520 KB — Variables + Stack + Heap← s'efface à chaque extinction
Flash SPI 4 MB — ton programme← persiste hors tension
WiFi 802.11 b/g/n — 2.4 GHz — Antenne PCB← radio partagée avec BT via FreeRTOS
Bluetooth 4.2 Classic + BLE 4.2← multiplexage temporel avec WiFi
ADC SAR 12 bits — 18 canaux — ADC1 + ADC2← ADC2 inutilisable avec WiFi actif !
UART×3 / I2C×2 / SPI×4 / LEDC 16 canaux PWM← interfaces série + PWM
RTC + ULP co-processeur 8 MHz← deep sleep + surveillance GPIO
🔴
Piège critique — 3.3V, jamais 5V !

L'ESP32 travaille en logique 3.3V. Brancher un signal 5V directement sur un GPIO peut griller définitivement la puce. Utiliser un diviseur de tension ou un level-shifter bidirectionnel pour tout capteur ou Arduino 5V.

Module 02

Pinout ESP32 — Carte des Broches

Chaque broche a un rôle principal et souvent plusieurs fonctions alternatives. Connaître les contraintes matérielles évite les bugs mystérieux.

3.3VGND 5V (VIN)EN (Reset)
GPIO2 ⚠️bootGPIO4GPIO5 GPIO12 ⚠️bootGPIO13GPIO14 GPIO15GPIO16GPIO17 GPIO18GPIO19GPIO23GPIO27
GPIO32 ADC1_4GPIO33 ADC1_5 GPIO34 ← INPUT ONLYGPIO35 ← INPUT ONLY GPIO36/VP ← INPUT ONLYGPIO39/VN ← INPUT ONLY
GPIO21 (SDA)GPIO22 (SCL) GPIO18 SCKGPIO19 MISOGPIO23 MOSI GPIO1 TX0GPIO3 RX0 GPIO25 DAC1GPIO26 DAC2
Rouge=AlimGris=GND Cyan=GPIOJaune=ADC Violet=I2COrange=SPI Vert=UARTRose=DAC
BrocheRôle principalRègle d'or
GPIO6–11Flash SPI interneJAMAIS UTILISER — crash garanti
GPIO34, 35, 36, 39ADC entrée seuleINPUT uniquement — pas OUTPUT ni pull-up interne
GPIO0Boot modeLOW au démarrage = mode flash. Pull-up 10 kΩ conseillé.
GPIO2LED DevKit + BootDoit être libre (flottant ou HIGH) pendant programmation
GPIO21 / 22SDA / SCL I2C par défautPull-up 4.7 kΩ externe vers 3.3V obligatoires
ADC2 (0,2,4,12–15,25–27)ADC partagé WiFianalogRead() retourne des erreurs quand WiFi est actif
Module 03

GPIO — Entrées & Sorties Digitales

GPIO = General Purpose Input/Output. En sortie tu commandes LED, relais, buzzers. En entrée tu lis boutons et capteurs digitaux. La règle d'or : jamais de delay() dans un projet IoT.

⚠️
Règle d'or IoT — delay() interdit en production

delay(1000) = ESP32 FIGÉ 1 seconde. WiFi coupé, MQTT perdu, rien ne répond. Solution : millis() — noter l'heure du dernier évènement et comparer sans bloquer.

blink_nonbloquant.ino — LED clignotante avec millis()
const int LED_PIN  = 2;      // GPIO2 = LED bleue intégrée DevKit
unsigned long lastTime = 0;
bool ledState = false;

void setup() {
  Serial.begin(115200);
  pinMode(LED_PIN, OUTPUT);
}

void loop() {
  unsigned long now = millis();
  if (now - lastTime >= 1000) {
    lastTime   = now;
    ledState   = !ledState;
    digitalWrite(LED_PIN, ledState);
    Serial.printf("LED: %s\n", ledState ? "ON" : "OFF");
  }
  // Ici : WiFi, MQTT, capteurs continuent de fonctionner !
}
bouton_antirebond.ino — INPUT_PULLUP + debounce
const int BTN = 15;               // GPIO15 → câbler vers GND
const long DEBOUNCE_MS = 50;
unsigned long lastDebounce = 0;
int lastRaw = HIGH, stableState = HIGH;

void setup() {
  Serial.begin(115200);
  pinMode(BTN, INPUT_PULLUP);
  // Résistance interne ~45 kΩ vers 3.3V
  // Repos = HIGH, Appuyé = LOW (logique inversée !)
}

void loop() {
  int raw = digitalRead(BTN);
  if (raw != lastRaw) lastDebounce = millis();
  if ((millis() - lastDebounce) > DEBOUNCE_MS) {
    if (raw != stableState) {
      stableState = raw;
      Serial.println(stableState == LOW ? "APPUYÉ" : "relâché");
    }
  }
  lastRaw = raw;
}
Module 04

ADC & PWM — Le Monde Analogique

Les capteurs réels produisent des tensions variables. L'ADC 12 bits les convertit en valeurs 0–4095. Le PWM simule une tension variable en sortie.

tension = valeur × V_REF / (212 − 1)  =  valeur × 3.3 / 4095

📊 ADC — Référence rapide

RésolutionNiveaux1 LSB @ 3.3V
8 bits25612.9 mV
10 bits1 0243.22 mV
12 bits (défaut)4 0960.806 mV
🔴
ADC2 mort avec WiFi !

GPIO0,2,4,12–15,25–27 = ADC2. analogRead() retourne des erreurs quand WiFi est actif. Toujours utiliser GPIO32–39 (ADC1) dans les projets IoT.

🌊 PWM — LEDC 16 canaux

Le PWM allume/éteint très rapidement → valeur moyenne = tension voulue. L'ESP32 dispose de 16 canaux LEDC indépendants.

Duty cycle 0% → éteint · 50% → mi-intensité · 100% → plein
FonctionRôle
ledcSetup(canal, freq, bits)Configure le canal PWM
ledcAttachPin(pin, canal)Attache GPIO au canal
ledcWrite(canal, duty)Définit le rapport cyclique
adc_pwm_complet.ino — Potentiomètre → LED dégradé
const int POT = 34;   // ADC1 — fonctionne avec WiFi !
const int LED = 2, CANAL = 0;

void setup() {
  Serial.begin(115200);
  analogReadResolution(12);         // 12 bits → 0 à 4095
  analogSetAttenuation(ADC_11db);    // plage 0–3.3V
  ledcSetup(CANAL, 5000, 8);        // 5 kHz, 8 bits (0–255)
  ledcAttachPin(LED, CANAL);
}

void loop() {
  int   val  = analogRead(POT);
  float volt = val * 3.3f / 4095.0f;
  int   pwm  = map(val, 0, 4095, 0, 255);
  ledcWrite(CANAL, pwm);
  Serial.printf("ADC:%4d  %.2fV  PWM:%d\n", val, volt, pwm);
  delay(100);
}
Module 05

I2C — Communiquer avec les Capteurs

I2C = 2 fils (SDA + SCL) pour connecter jusqu'à 127 appareils sur le même bus. Sur ESP32 : SDA = GPIO21, SCL = GPIO22. Pull-up 4.7 kΩ obligatoires sur les deux lignes.

ESP32 MAÎTRE I2C SDA=21 SCL=22 SDA SCL BME280 0x76 / 0x77 OLED SSD1306 0x3C / 0x3D ADS1115 ADC 16 bits Jusqu'à 127 esclaves
FIG. 5.1 — Bus I2C partagé : même SDA + SCL, chaque esclave a une adresse unique
bme280_i2c.ino — Température + Humidité + Pression
#include <Wire.h>
#include <Adafruit_BME280.h>    // Adafruit BME280 + Adafruit Unified Sensor

Adafruit_BME280 bme;

void setup() {
  Serial.begin(115200);
  Wire.begin(21, 22);      // SDA=21, SCL=22
  if (!bme.begin(0x76)) {
    Serial.println("❌ BME280 non trouvé ! Vérifier câblage et adresse I2C");
    while (true) delay(1000);
  }
  Serial.println("✅ BME280 initialisé");
}

void loop() {
  Serial.printf("Temp:%.1f°C  Humi:%.1f%%  Pres:%.1fhPa\n",
    bme.readTemperature(),
    bme.readHumidity(),
    bme.readPressure() / 100.0);
  delay(2000);
}

// Scanner I2C — trouver l'adresse d'un module inconnu :
// for (byte a=1; a<127; a++) {
//   Wire.beginTransmission(a);
//   if (Wire.endTransmission()==0) Serial.printf("Trouvé 0x%02X\n",a);
// }
Module 06

WiFi — Connexion au Réseau

Le WiFi intégré se connecte à ton routeur comme un smartphone — obtient une adresse IP, communique avec le broker MQTT sur ton réseau local ou Internet.

wifi_robuste.ino — Connexion avec timeout + vérification automatique
#include <WiFi.h>
const char* SSID = "Ton_Reseau";
const char* PASS = "Ton_Pass";

void connectWiFi() {
  if (WiFi.status() == WL_CONNECTED) return;
  WiFi.mode(WIFI_STA);
  WiFi.setAutoReconnect(true);
  WiFi.begin(SSID, PASS);
  Serial.print("WiFi");
  unsigned long t = millis();
  while (WiFi.status() != WL_CONNECTED) {
    if (millis() - t > 15000) { Serial.println("\nTimeout !"); return; }
    delay(500); Serial.print(".");
  }
  Serial.printf("\n✅ IP: %s | Signal: %d dBm\n",
    WiFi.localIP().toString().c_str(), WiFi.RSSI());
  // -50 dBm = excellent  |  -70 dBm = correct  |  -90 dBm = faible
}

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

unsigned long lastCheck = 0;
void loop() {
  if (millis() - lastCheck > 10000) {   // Vérifier toutes les 10s
    lastCheck = millis();
    if (WiFi.status() != WL_CONNECTED) connectWiFi();
  }
}
🔒
Sécurité — ne jamais mettre les identifiants dans le code public

Solutions : (1) WiFiManager — portail web de configuration, (2) config.h dans .gitignore, (3) NVS — stockage non-volatile interne à l'ESP32 (clé/valeur persistant hors tension).

Module 07

Architecture IoT Complète

Chaque système IoT suit le même pipeline. Comprendre chaque couche permet de diagnostiquer n'importe quel problème en 5 minutes.

🌡️ CAPTEUR Monde physique ⚡ ESP32 Mesure + WiFi 📡 MQTT Protocole léger 🖥️ MOSQUITTO Broker serveur 🎛️ NODE-RED Traitement + Dashboard
FIG. 7.1 — Pipeline IoT complet : du capteur physique au dashboard

🌐 HTTP vs MQTT — pourquoi MQTT pour l'IoT

CritèreHTTPMQTT
ModèleRequête → Réponse → FermetureConnexion persistante
En-tête200–800 octets2 octets minimum !
DonnéesPull — tu demandesPush — arrivent auto
Conçu pourNavigateurs webMillions de capteurs
ReconnexionNouvelle requêteAutomatique + LWT

📮 Analogie MQTT — le journal par abonnement

L'ESP32 (correspondant) envoie ses articles (données) à la rédaction (broker Mosquitto). Tous ceux abonnés au sujet "température salon" (Node-RED, app mobile) reçoivent automatiquement chaque nouveau message dès qu'il arrive. Pas besoin de demander "y a-t-il du nouveau ?"

Avantage décisif

En-tête 2 octets → MQTT fonctionne avec un signal GSM à 9600 bps ou une connexion satellite à 100 ms de latence.

Module 08

MQTT — Le Protocole IoT

MQTT = Message Queuing Telemetry Transport. Créé en 1999 par IBM pour la télémétrie de pipelines pétroliers via satellite. Standard mondial de l'IoT.

ConceptDéfinitionExemple concret
TopicAdresse hiérarchique, niveaux séparés par /maison/salon/temperature
PayloadContenu du message (texte, JSON, binaire){"temp":23.5,"humi":65.2}
QoS 0At most once — envoi unique sans confirmationMesures très fréquentes non critiques
QoS 1At least once — confirmé, peut arriver en doubleCommandes importantes
QoS 2Exactly once — 4 échanges, garanti une seule foisTransactions critiques
RetainBroker garde le dernier message, l'envoie aux nouveaux abonnésÉtat ON/OFF d'un appareil
LWTLast Will : message publié si déconnexion anormalemaison/salon/status → "offline"
mqtt_complet.ino — Publier + Recevoir + LWT + JSON
#include <WiFi.h>
#include <PubSubClient.h>
#include <ArduinoJson.h>

const char* WIFI_SSID = "Ton_Reseau";
const char* WIFI_PASS = "Ton_Pass";
const char* MQTT_HOST = "192.168.1.10";   // IP Raspberry Pi
const int   MQTT_PORT = 1883;
const char* CLIENT_ID = "esp32_salon_01";   // UNIQUE par appareil !
const char* T_DATA    = "maison/salon/meteo";
const char* T_STATUS  = "maison/salon/status";
const char* T_CMD     = "maison/salon/cmd";

WiFiClient   wc;
PubSubClient mqtt(wc);

void onMsg(char* topic, byte* payload, unsigned int len) {
  String msg = "";
  for (uint i=0; i<len; i++) msg += (char)payload[i];
  Serial.printf("[%s] → %s\n", topic, msg.c_str());
  if (String(topic) == T_CMD) {
    if (msg == "ON")  digitalWrite(2, HIGH);
    if (msg == "OFF") digitalWrite(2, LOW);
  }
}

void reconnectMQTT() {
  while (!mqtt.connected()) {
    Serial.print("MQTT connexion...");
    if (mqtt.connect(CLIENT_ID, nullptr, nullptr,
                     T_STATUS, 1, true, "offline")) {
      mqtt.publish(T_STATUS, "online", true);  // retain=true
      mqtt.subscribe(T_CMD);
      Serial.println(" ✅ OK");
    } else { Serial.printf(" ❌ err %d\n", mqtt.state()); delay(5000); }
  }
}

void setup() {
  Serial.begin(115200); pinMode(2, OUTPUT);
  WiFi.begin(WIFI_SSID, WIFI_PASS);
  while (WiFi.status() != WL_CONNECTED) delay(500);
  mqtt.setServer(MQTT_HOST, MQTT_PORT);
  mqtt.setCallback(onMsg);
  mqtt.setBufferSize(512);
  reconnectMQTT();
}

unsigned long lastPub = 0;
void loop() {
  if (!mqtt.connected()) reconnectMQTT();
  mqtt.loop();   // ⚠️ INDISPENSABLE — messages + keepalive
  if (millis() - lastPub >= 30000) {
    lastPub = millis();
    StaticJsonDocument<200> doc;
    doc["temp"]   = 23.5; doc["humi"]   = 65.2;
    doc["rssi"]   = WiFi.RSSI(); doc["uptime"] = millis() / 1000;
    char buf[200]; serializeJson(doc, buf);
    bool ok = mqtt.publish(T_DATA, buf);
    Serial.printf("Publié [%s]: %s\n", ok ? "OK" : "FAIL", buf);
  }
}
Module 09

Mosquitto — Le Serveur MQTT

Mosquitto est le broker MQTT open source le plus utilisé au monde. Léger, fiable, tourne parfaitement sur Raspberry Pi. C'est la centrale téléphonique de ton IoT.

Étape 01
INSTALLER
sudo apt install mosquitto mosquitto-clients — démarrage automatique au boot.
Étape 02
CONFIGURER
listener 1883, allow_anonymous true. En production : authentification + TLS 8883.
Étape 03
TESTER
mosquitto_sub -t "#" -v → voir tous les messages. mosquitto_pub pour publier.
Étape 04
SÉCURISER
allow_anonymous false + password_file + ACL par utilisateur. TLS port 8883.
mosquitto_install_config.sh — Installation + Configuration + Tests
# 1. Installation sur Raspberry Pi / Ubuntu / Debian
sudo apt update && sudo apt install -y mosquitto mosquitto-clients
sudo systemctl enable mosquitto && sudo systemctl start mosquitto
sudo systemctl status mosquitto   # → active (running) ✓

# 2. Configuration /etc/mosquitto/conf.d/maison.conf
listener 1883
allow_anonymous true
persistence true
persistence_location /var/lib/mosquitto/
log_type all
log_dest file /var/log/mosquitto/mosquitto.log

# 3. Tests — ouvrir 2 terminaux
mosquitto_sub -h localhost -t "#" -v    # Terminal 1 : écouter TOUT
mosquitto_pub -h localhost -t "test/hello" -m "Bonjour ESP32 !"

# 4. Diagnostic production
mosquitto_sub -h 192.168.1.10 -t "#" -v    # Voir tous les messages
mosquitto_pub -h 192.168.1.10 -t "salon/temp" -m "23.5" -r  # retain
sudo tail -f /var/log/mosquitto/mosquitto.log

# 5. Authentification (production)
sudo mosquitto_passwd -c /etc/mosquitto/passwd monuser   # créer
sudo mosquitto_passwd /etc/mosquitto/passwd user2        # ajouter
# Puis dans maison.conf :
# allow_anonymous false
# password_file /etc/mosquitto/passwd
☁️
Pas de Raspberry Pi ? Brokers gratuits pour tester

broker.hivemq.com:1883 — public · test.mosquitto.org:1883 — officiel Eclipse · EMQX Cloud — gratuit avec compte. ⚠️ Ne jamais envoyer de données personnelles sur un broker public.

Module 10

Node-RED — Programmation Visuelle

Node-RED est un outil de programmation par flux visuels développé par IBM. Tu assembles des blocs graphiques (nœuds) avec des fils pour créer des automatisations puissantes — en 10 minutes tu as un dashboard complet.

install_nodered.sh — Installation officielle
# Installation officielle (Raspberry Pi / Ubuntu / Debian)
bash <(curl -sL https://raw.githubusercontent.com/node-red/linux-installers/master/deb/update-nodejs-and-nodered)
sudo systemctl enable nodered && sudo systemctl start nodered

# Interface web : http://IP_DU_PI:1880
# Dashboard    : http://IP_DU_PI:1880/ui

# Installer le module Dashboard (jauges, graphiques, boutons)
cd ~/.node-red && npm install node-red-dashboard
# Puis : Menu → Restart dans l'interface web

# Installer InfluxDB pour l'historique des données
npm install node-red-contrib-influxdb
NœudCatégorieRôle exact
mqtt innetworkReçoit messages depuis un topic MQTT — entrée IoT principale
mqtt outnetworkPublie sur un topic MQTT — commande l'ESP32
functionfunctionJavaScript pour transformer/filtrer/enrichir msg.payload — le plus puissant
changefunctionModifie/copie/supprime propriétés du message — sans code
switchfunctionRoute le message vers sorties différentes selon conditions
injectinputInjecte un message manuellement ou par timer — tests
debugoutputAffiche la valeur en console debug — indispensable
ui_gaugedashboardJauge analogique temps réel
ui_chartdashboardGraphique historique des mesures
ui_switchdashboardBouton ON/OFF → envoie commande MQTT

🎛️ Flow visuel — Capteur → Dashboard + Alerte

mqtt in
maison/salon/meteo
function
Parser JSON + arrondir
ui_gauge
Temp °C
+
ui_chart
Historique 24h
switch
temp > 30 ?
email
Alerte chaleur
ui_switch
Lumière ON/OFF
change
Format ON/OFF
mqtt out
maison/salon/lumiere/cmd
function_nodered.js — Parser JSON + enrichir + historique
// Nœud Function : reçoit msg.payload = '{"temp":23.5,"humi":65.2}'
let data;
try { data = JSON.parse(msg.payload); }
catch(e) { node.warn("JSON invalide: " + msg.payload); return null; }

if (data.temp < -40 || data.temp > 85) { node.warn("Temp hors plage"); return null; }

msg.payload = data.temp;
msg.color = data.temp < 18 ? '#4cc9f0'   // bleu = froid
           : data.temp < 25 ? '#39ff7a'   // vert = confortable
           : data.temp < 30 ? '#ffb703'   // orange = chaud
           :                    '#ff4d6d'; // rouge = alerte

let hist = global.get('temp_hist') || [];
hist.push({ t: Date.now(), v: data.temp });
if (hist.length > 288) hist.shift();     // 288 = 24h à 5min d'intervalle
global.set('temp_hist', hist);

return msg;
Module 11 — Projet Complet

5 Projets Réels

De la station météo au joystick DMA — projets terrain avec code C complet. Toutes les solutions sont affichées directement.

🌡️
Projet 01 · Débutant
Station Météo IoT Complète
ESP32 + BME280 → MQTT → Node-RED Dashboard · ⭐ Débutant
📋 Objectif

ESP32 + capteur BME280 mesure température/humidité/pression toutes les 30 secondes. Publie en JSON sur MQTT. Node-RED affiche tout en temps réel et envoie une alerte si humidité > 80%.

✅ Code ESP32 Complet
station_meteo.ino
#include <WiFi.h>
#include <PubSubClient.h>
#include <Wire.h>
#include <Adafruit_BME280.h>
#include <ArduinoJson.h>

const char* SSID = "Ton_Reseau";  const char* PASS = "Ton_Pass";
const char* BROKER = "192.168.1.10";
const char* DEVICE = "station_salon";

WiFiClient wc; PubSubClient mqtt(wc);
Adafruit_BME280 bme;
unsigned long lastPub = 0;

void setup() {
  Serial.begin(115200);
  Wire.begin(21, 22);
  if (!bme.begin(0x76)) { Serial.println("❌ BME280!"); while(true); }
  WiFi.begin(SSID, PASS);
  while (WiFi.status() != WL_CONNECTED) delay(400);
  mqtt.setServer(BROKER, 1883);
  while (!mqtt.connect(DEVICE, nullptr, nullptr,
    "maison/salon/status", 1, true, "offline")) delay(3000);
  mqtt.publish("maison/salon/status", "online", true);
  Serial.println("✅ Prêt !");
}

void loop() {
  if (!mqtt.connected()) { mqtt.connect(DEVICE, nullptr, nullptr, "maison/salon/status", 1, true, "offline"); }
  mqtt.loop();
  if (millis() - lastPub >= 30000) {
    lastPub = millis();
    StaticJsonDocument<200> doc;
    doc["temp"] = round(bme.readTemperature() * 10) / 10.0;
    doc["humi"] = round(bme.readHumidity() * 10) / 10.0;
    doc["pres"] = round(bme.readPressure() / 10.0) / 10.0;
    doc["rssi"] = WiFi.RSSI(); doc["up"] = millis() / 1000;
    char buf[200]; serializeJson(doc, buf);
    mqtt.publish("maison/salon/meteo", buf);
    Serial.printf("📤 %s\n", buf);
  }
}
💡
Projet 02 · Intermédiaire
LDR → Seuil Automatique + Hystérésis
Diviseur de tension · Hystérésis · MQTT · ⭐⭐
📋 Objectif

LDR (photorésistance) + R fixe 10 kΩ en diviseur sur GPIO34 (ADC1). Si luminosité basse (ADC > 2800) → allumer LED + publier MQTT "ON". Si luminosité haute (ADC < 1200) → éteindre. Hystérésis pour éviter le clignotement.

✅ Solution Complète
ldr_hystéresis.ino
#define THR_DARK   2800   // seuil allumage (sombre)
#define THR_BRIGHT 1200   // seuil extinction (lumineux)
#define LED_PIN    2
static bool ledOn = false;

unsigned long lastRead = 0;
void loop() {
  mqtt.loop();
  if (millis() - lastRead >= 200) {
    lastRead = millis();
    int raw = analogRead(34);
    if (!ledOn && raw > THR_DARK) {
      ledOn = true;
      digitalWrite(LED_PIN, HIGH);
      mqtt.publish("maison/salon/lumiere", "ON", true);
    } else if (ledOn && raw < THR_BRIGHT) {
      ledOn = false;
      digitalWrite(LED_PIN, LOW);
      mqtt.publish("maison/salon/lumiere", "OFF", true);
    }
  }
}
🔋
Projet 03 · Intermédiaire
Batterie LiPo — Jauge 4 Niveaux MQTT
Diviseur /2 · Suréchantillonnage · MQTT retained · ⭐⭐
📋 Objectif

LiPo 1S (3.0V–4.2V) via diviseur R1=R2=100kΩ sur GPIO32. Suréchantillonnage ×16. Calculer tension réelle + SoC 0–100%. Publier MQTT retained. Clignoter LED si V < 3.2V.

✅ Solution Complète
lipo_gauge.ino
#define N_OS 16    // suréchantillonnage ×16
uint32_t sum = 0;
for (int i=0; i<N_OS; i++) sum += analogRead(32);

float v_adc = ((float)(sum / N_OS) / 4095.0f) * 3.3f;
float v_bat = v_adc * 2.0f;   // diviseur /2 → ×2 pour reconstruire
float soc   = (v_bat - 3.0f) / (4.2f - 3.0f) * 100.0f;
soc = constrain(soc, 0.0f, 100.0f);

StaticJsonDocument<80> doc;
doc["vbat"] = v_bat; doc["soc"] = (int)soc;
char buf[80]; serializeJson(doc, buf);
mqtt.publish("maison/batterie", buf, true);  // retained

if (v_bat < 3.2f) {   // Alerte batterie faible
  digitalWrite(2, HIGH); delay(150);
  digitalWrite(2, LOW);  delay(150);
}
💤
Projet 04 · Avancé
Station Météo Deep Sleep — Batterie Longue Durée
RTC_DATA_ATTR · esp_deep_sleep · ⭐⭐⭐
📋 Objectif

L'ESP32 se réveille toutes les 5 minutes, mesure, publie MQTT, puis dort. Consommation active ~250mA pendant ~4s, puis ~10µA en deep sleep → batterie dure des semaines. Variables RTC pour compter les réveils.

✅ Solution Complète
deep_sleep_station.ino
#include <esp_sleep.h>
#define SLEEP_US (uint64_t)300 * 1000000  // 5 minutes

RTC_DATA_ATTR int boot_count = 0;   // survit au deep sleep !
RTC_DATA_ATTR float last_temp = 0;

void setup() {
  Serial.begin(115200);
  boot_count++;
  Serial.printf("Réveil #%d\n", boot_count);

  // Toute la logique ici — loop() jamais atteint !
  Wire.begin(21, 22);
  Adafruit_BME280 bme;
  if (bme.begin(0x76)) {
    last_temp = bme.readTemperature();
    // ... WiFi + MQTT + publish ...
  }

  // Déconnexion propre avant sleep
  mqtt.disconnect();
  WiFi.disconnect(true); WiFi.mode(WIFI_OFF);
  delay(200);
  esp_sleep_enable_timer_wakeup(SLEEP_US);
  esp_deep_sleep_start();   // ← setup() sera rappelé au réveil
}
void loop(){}  // jamais atteint avec deep sleep
🕹️
Projet 05 · Expert
Joystick Double Axe — Contrôle à Distance
ADC DMA · Zone morte · MQTT bidirectionnel · ⭐⭐⭐
📋 Objectif

Joystick XY sur GPIO32/33 (ADC1). Normaliser -100% à +100%. Zone morte ±150 pts autour de 2048. Publier axes sur MQTT toutes les 50ms. Node-RED affiche les valeurs en jauge et contrôle un servo.

✅ Solution Complète
joystick_mqtt.ino
#define JOY_X  32
#define JOY_Y  33
#define CENTER 2048
#define DZ     150

float deadzone(int raw) {
  int e = raw - CENTER;
  if (e > -DZ && e < DZ) return 0.0f;
  float v = (float)(e - (e > 0 ? DZ : -DZ)) / (float)(CENTER - DZ);
  return constrain(v, -1.0f, 1.0f);
}

unsigned long lastPub = 0;
void loop() {
  mqtt.loop();
  if (millis() - lastPub >= 50) {
    lastPub = millis();
    float x = deadzone(analogRead(JOY_X)) * 100;
    float y = deadzone(analogRead(JOY_Y)) * 100;
    StaticJsonDocument<60> doc;
    doc["x"] = (int)x; doc["y"] = (int)y;
    char buf[60]; serializeJson(doc, buf);
    mqtt.publish("robot/joystick", buf);
  }
}
Module 12

📝 Quiz — 40 Questions

Du débutant à l'expert — ESP32, GPIO, WiFi, MQTT, Mosquitto, Node-RED. Réponses directes avec explication.

Q01 — Que signifie GPIO ?

Quel est le développement complet de GPIO dans le contexte des microcontrôleurs ?

General Purpose Input/Output — broche programmable en entrée ou en sortie à la volée.

Q02 — Tension logique ESP32

Un capteur 5V peut-il être directement branché sur un GPIO ESP32 ?

Non. L'ESP32 est en 3.3V. Brancher 5V = destruction irréversible. Utiliser un diviseur de tension ou un level-shifter.

Q03 — Pourquoi éviter delay() en IoT ?

Que se passe-t-il si on met delay(2000) dans loop() dans un projet MQTT ?

✅ L'ESP32 est figé 2s. mqtt.loop() n'est pas appelé → le broker déconnecte l'appareil (keepalive timeout). Utiliser millis().

Q04 — Résolution ADC

L'ESP32 lit la valeur brute 2048. VREF = 3.3V, 12 bits. Quelle tension ?

✅ V = 2048 × 3.3 / 4095 = 1.65V — exactement la moitié de VREF.

Q05 — ADC2 + WiFi

Pourquoi éviter analogRead() sur GPIO26 dans un projet IoT ?

✅ GPIO26 = ADC2, partagé avec le driver WiFi. analogRead() retourne des valeurs erronées quand WiFi est actif. Utiliser GPIO32–39 (ADC1).

Q06 — INPUT_PULLUP

Un bouton est câblé entre GPIO15 et GND. pinMode(15, INPUT_PULLUP). Valeur au repos ?

HIGH (3.3V via résistance interne ~45kΩ). Appuyé = LOW. Logique inversée — penser à la vérifier.

Q07 — I2C pull-up

Le bus I2C fonctionne-t-il sans résistances pull-up sur SDA et SCL ?

Non — le bus reste à niveau indéfini (ligne flottante). Toujours 4.7kΩ entre chaque ligne et 3.3V. Beaucoup de modules les incluent déjà.

Q08 — MQTT signification

Que signifie l'acronyme MQTT et pourquoi est-il adapté à l'IoT ?

Message Queuing Telemetry Transport. En-tête minimum 2 octets — conçu pour les connexions à faible bande passante et haute latence (satellite, GSM).

Q09 — Topic MQTT

Quelle est la différence entre le wildcard + et # dans les topics MQTT ?

+ remplace UN seul niveau : maison/+/temp reçoit toutes les températures. # remplace TOUT le reste : maison/# reçoit tout ce qui commence par maison/.

Q10 — Port MQTT

Quel port utilise MQTT non chiffré ? Et chiffré TLS ?

✅ Non chiffré : 1883. Chiffré TLS/SSL : 8883. En production sur Internet → toujours 8883 avec certificats.

Q11 — mqtt.loop()

Que fait mqtt.loop() et pourquoi l'appeler à chaque itération ?

✅ Lit le socket TCP, appelle la callback pour chaque message reçu, envoie les PINGREQ keepalive. Sans lui → messages perdus + broker déconnecte.

Q12 — Message retained

À quoi sert le flag retain dans mqtt.publish() ?

✅ Le broker conserve CE message. Tout nouveau subscriber le reçoit immédiatement — même si l'ESP32 est hors ligne. Idéal pour l'état ON/OFF d'un appareil.

Q13 — LWT

Que publie le broker quand un ESP32 se déconnecte sans envoyer DISCONNECT ?

✅ Le Last Will and Testament — le message prédéfini (ex: "offline") sur le topic configuré lors du connect(). Permet de détecter une panne automatiquement.

Q14 — CLIENT_ID unique

Que se passe-t-il si deux ESP32 utilisent le même CLIENT_ID MQTT ?

✅ Le broker déconnecte le premier dès que le second arrive. Ils se déconnectent mutuellement en boucle. Utiliser WiFi.macAddress() comme ID unique.

Q15 — ArduinoJson

Pourquoi StaticJsonDocument est préféré à DynamicJsonDocument sur ESP32 ?

✅ StaticJsonDocument alloue sur la pile (stack) — pas de malloc/free. Évite la fragmentation de la heap après des heures de fonctionnement en production.

Q16 — Broker rôle

Quelle est la différence entre publisher, broker et subscriber en MQTT ?

Publisher (ESP32) envoie. Broker (Mosquitto) reçoit et redistribue. Subscriber (Node-RED) reçoit. Publisher et subscriber ne se connaissent pas — le broker est l'intermédiaire.

Q17 — QoS 0 vs 1

Quand utiliser QoS 0 et quand utiliser QoS 1 en IoT maison ?

QoS 0 pour mesures fréquentes (température toutes les 30s — si un message est perdu, le suivant arrive). QoS 1 pour commandes importantes (allumer un relais).

Q18 — mosquitto_sub diagnostic

Quelle commande voir TOUS les messages MQTT en temps réel sur localhost ?

mosquitto_sub -h localhost -t "#" -v — -t "#" s'abonne à TOUS les topics, -v affiche aussi le nom du topic. Commande de débogage n°1.

Q19 — Node-RED function

Que fait un nœud function dans Node-RED et quel langage utilise-t-il ?

✅ Exécute du JavaScript arbitraire sur le message. Peut modifier msg.payload, créer des propriétés, retourner null pour bloquer, ou retourner un tableau pour plusieurs sorties.

Q20 — Deep sleep consommation

Quelle est la consommation typique de l'ESP32 en deep sleep et que reste-t-il actif ?

~10 µA. Seul le RTC (Real Time Clock) reste actif. CPU, RAM, WiFi, BT sont éteints. Au réveil → setup() recommence depuis le début.

Q21 — RTC_DATA_ATTR

Comment conserver une variable entre deux réveils du deep sleep ?

✅ Déclarer avec RTC_DATA_ATTR : RTC_DATA_ATTR int compteur = 0; — stockée dans la mémoire RTC (8KB) qui reste alimentée pendant le sleep.

Q22 — allow_anonymous Mosquitto

Que signifie allow_anonymous false dans la configuration Mosquitto ?

✅ Tout client doit fournir un username + password valides pour se connecter. Connexion anonyme refusée. Activer conjointement password_file.

Q23 — WiFiClientSecure

Quelle est la différence entre WiFiClient et WiFiClientSecure pour MQTT ?

✅ WiFiClient = TCP non chiffré (port 1883). WiFiClientSecure = TLS/SSL chiffré (port 8883) — nécessite un certificat CA du serveur. Utiliser dès que les données circulent sur Internet.

Q24 — FreeRTOS core allocation

Comment FreeRTOS gère-t-il le WiFi et ton code simultanément sur ESP32 ?

Core 0 : stack WiFi/BT (haute priorité). Core 1 : ta loop() Arduino. Les deux tournent en parallèle, FreeRTOS alloue le CPU selon les priorités et états des tâches.

Q25 — Node-RED switch

Comment déclencher une alerte email si la température dépasse 30°C dans Node-RED ?

✅ Nœud switch après function (Parser JSON) → Sortie 1 si msg.payload > 30 → nœud email. Sortie 2 si normal → nœud debug.

Q26 — Hystérésis

Pourquoi ajouter une hystérésis à une LED déclenchée par seuil ADC ?

✅ Sans hystérésis, si la mesure oscille autour du seuil, la LED clignote rapidement (oscillation). Deux seuils (allumage / extinction) créent une zone morte qui stabilise l'état.

Q27 — JSON IoT

Pourquoi publier {"temp":23.5,"humi":65} plutôt que "23.5,65" sur MQTT ?

✅ JSON est auto-descriptif (chaque valeur a un nom) et extensible (ajouter "pres" ne casse pas les abonnés qui ne lisent que "temp"). Toutes les langues ont des parseurs JSON.

Q28 — OTA

Comment mettre à jour le firmware d'un ESP32 déployé sans câble USB ?

OTA (Over-The-Air) : ArduinoOTA (UDP local port 3232) ou HTTP OTA (télécharge un .bin depuis HTTPS, écrit dans une partition alternative, redémarre dessus).

Q29 — Reconnexion MQTT robuste

Pourquoi éviter while(!mqtt.connected()) avec delay() dans loop() ?

✅ Bloque tout le code pendant la reconnexion — les capteurs ne sont pas lus, le watchdog peut déclencher. Utiliser millis() pour tenter toutes les 5s sans bloquer.

Q30 — Store-and-forward

Comment ne pas perdre de données quand le WiFi est temporairement absent ?

✅ Écrire dans LittleFS (Flash 4MB) en JSON ou CSV. Quand le WiFi revient : lire le fichier, publier chaque ligne MQTT, vider le fichier. Capacité : ~80 000 mesures.

Q31 — Dashboard bidirectionnel

Comment contrôler un relais ESP32 depuis un bouton Node-RED dashboard ?

ui_switch → change (formate ON/OFF) → mqtt out (publie sur maison/salon/relais/cmd). L'ESP32 : mqtt.subscribe() + callback qui actionne le relais. <100ms aller-retour.

Q32 — millis() pattern

Écrire le pattern millis() pour publier MQTT toutes les 30 secondes.

unsigned long last=0; dans loop() : if(millis()-last>=30000){last=millis(); mqtt.publish(...);} — non bloquant, tout continue de fonctionner.

Q33 — MQTT 5.0 nouveautés

Quels sont les 3 principaux apports de MQTT 5.0 par rapport à 3.1.1 ?

✅ (1) Reason codes dans chaque ACK, (2) Message expiry (TTL automatique), (3) Shared subscriptions (load balancing entre plusieurs subscribers).

Q34 — InfluxDB + Grafana

Pourquoi InfluxDB + Grafana plutôt que le seul dashboard Node-RED pour l'historique ?

✅ Node-RED dashboard est pour le temps réel. InfluxDB est une base time-series optimisée pour des semaines/mois de données. Grafana : alertes sophistiquées, multi-utilisateurs, multi-sources.

Q35 — ESP-IDF vs Arduino

Quand utiliser ESP-IDF plutôt qu'Arduino pour programmer l'ESP32 ?

ESP-IDF (framework officiel Espressif) quand tu as besoin d'accès direct aux registres hardware, de performances maximales, ou de fonctionnalités avancées de FreeRTOS (tâches, queues, sémaphores).

Q36 — Suréchantillonnage

Comment passer de 12 bits à 14 bits de résolution effective sans ADC externe ?

Suréchantillonnage ×16 : additionner 16 mesures successives et diviser par 16. +2 bits de résolution par facteur ×4. Coûte N× plus de temps CPU.

Q37 — LWT scénario

L'ESP32 plante à cause d'une exception. Que publie Mosquitto automatiquement ?

✅ Après 1.5 × keepalive sans PINGREQ, le broker publie le LWT (ex: "offline" sur maison/salon/status, retain=true). Node-RED reçoit "offline" et peut alerter.

Q38 — PWM fréquence

Pour un servo moteur RC (20ms = 50Hz), quelle fréquence configurer en ledcSetup() ?

ledcSetup(canal, 50, 16) — 50Hz, 16 bits de résolution. Rapport cyclique 1ms = ~3276 (5% de 65535), 2ms = ~6553 (10%). Position servo = valeur CCR.

Q39 — Débogage couche par couche

Node-RED ne reçoit aucune donnée de l'ESP32. Quelle est la première étape de débogage ?

mosquitto_sub -h localhost -t "#" -v — vérifier que le broker reçoit bien les messages. Si oui → problème Node-RED. Si non → problème ESP32 (vérifier Serial.println).

Q40 — Sécurité port 1883

Pourquoi ne jamais exposer le port 1883 directement sur Internet ?

✅ MQTT 1883 est en clair — mot de passe, topics et payloads visibles avec Wireshark. Sur Internet : toujours port 8883 + TLS + authentification. Pour réseau local : VPN ou tunnel SSH.
Module 13

Cheat Sheet & Conseils Pro

Référence rapide — ce qui distingue un projet IoT de production d'un prototype fragile.

GPIO Digital & PWM

FonctionUsage
pinMode(p, OUTPUT)LED, relais, buzzer
pinMode(p, INPUT_PULLUP)Bouton vers GND
digitalWrite(p, HIGH)Mettre à 3.3V
digitalRead(p)Lire HIGH ou LOW
ledcSetup(c,f,b)Config PWM canal
ledcWrite(c, duty)Rapport cyclique
🚫

Ne Jamais Faire

  • ❌ Appliquer > 3.3V sur GPIO
  • ❌ GPIO6–11 (Flash SPI interne)
  • ❌ ADC2 avec WiFi actif
  • ❌ delay() dans un projet MQTT
  • ❌ Same CLIENT_ID sur 2 ESP32
  • ❌ Buffer DMA sur la pile locale
  • ❌ Port 1883 exposé sur Internet
📡

MQTT — Guide rapide

ConceptUsage
Port 1883Réseau local non chiffré
Port 8883Internet TLS/SSL
QoS 0Mesures fréquentes
QoS 1Commandes importantes
retain=trueÉtat ON/OFF persistant
LWTDétection panne auto
mqtt.loop()Obligatoire dans loop()
🔍

Débogage Couche par Couche

1

Serial.println ESP32 — confirme que l'ESP32 publie

2

mosquitto_sub "#" -v — confirme que le broker reçoit

3

Nœud debug Node-RED — confirme la réception

4

Vérifier les topics — "Salon" ≠ "salon" (casse exacte)

Erreur fréquenteSymptômeSolution
delay() dans loop()MQTT se déconnecte, WiFi instableRemplacer par pattern millis()
ADC2 avec WiFianalogRead() retourne -1 ou valeur aléatoireUtiliser GPIO32–39 (ADC1 uniquement)
Oublier mqtt.loop()Messages jamais reçus, broker déconnecteAppeler mqtt.loop() à chaque itération
CLIENT_ID dupliquéESP32 se déconnecte en boucleUtiliser WiFi.macAddress() comme ID
Buffer MQTT trop petitmqtt.publish() retourne falsemqtt.setBufferSize(512) dans setup()
GPIO flottant en entréeLectures aléatoires HIGH/LOWINPUT_PULLUP ou INPUT_PULLDOWN
V_in GPIO > 3.3VGPIO grillé définitivementDiviseur de tension ou level-shifter
Topic avec faute de frappeAucune donnée dans Node-REDmosquitto_sub "#" -v pour voir les topics réels
ESP32 WiFi · BLE MQTT Mosquitto Node-RED IoT FreeRTOS Deep Sleep ArduinoJson
ESP32 Technical Reference Manual · ESP-IDF v5 · PubSubClient Nick O'Leary · Mosquitto Eclipse · Node-RED IBM