ESP32 et Arduino

ESP32 deep sleep MQTT : des capteurs sur batterie qui publient puis s'endorment

Construisez un capteur ESP32 économe en batterie qui se réveille sur minuterie, se connecte rapidement, publie une mesure retenue, vérifie qu'elle a atteint le broker et retourne en deep sleep.

Nœud capteur ESP32 sur batterie se réveillant pour publier dans un cycle ESP32 deep sleep MQTT
Essayez-le sur votre propre broker MQTTObtenez un broker MQTT privé et managé avec l’offre Free et partez d’exemples de code prêts à l’emploi.
Essayer gratuitement Exemples de code Aucune carte bancaire requise

ESP32 deep sleep MQTT, c'est ce qui permet de tenir des mois au lieu de quelques jours avec un capteur alimenté sur batterie. Dans ce tutoriel, vous allez construire un nœud qui se réveille sur minuterie, se reconnecte au Wi-Fi le plus vite possible, publie une mesure en retained message sur broker.connectmq.com, vérifie que le message a bien atteint le broker, puis se rendort. Comme la mesure est retenue, les tableaux de bord affichent toujours la dernière valeur, même si l'appareil est hors ligne presque tout le temps.

Le cycle réveil, publication, veille

En deep sleep, l'ESP32 coupe ses CPU, la majeure partie de sa RAM et la radio. Seul le domaine RTC reste actif, avec une infime fraction du courant consommé en fonctionnement. Quand la minuterie se déclenche, la puce ne reprend pas là où elle s'était arrêtée : elle redémarre et exécute setup() depuis le début. Dans cette conception, loop() ne s'exécute jamais.

Chaque cycle de réveil suit les mêmes étapes :

  1. Démarrer et lire le capteur immédiatement, avant que la radio ne chauffe la carte.
  2. Se connecter au Wi-Fi en réutilisant le canal et le point d'accès enregistrés lors du cycle précédent.
  3. Se connecter au broker MQTT et publier la mesure en retained message.
  4. Confirmer que le broker l'a reçue.
  5. Se déconnecter proprement, couper la radio et entrer en deep sleep.

Le temps passé entre les étapes 2 et 5 domine la consommation d'énergie, car la radio Wi-Fi est de loin ce qui tire le plus de courant. Toutes les optimisations de cet article visent à raccourcir cette fenêtre, ou à la sauter lorsqu'elle échouerait de toute façon.

Phase Consommation relative Comment la raccourcir
Démarrage et lecture du capteur Faible à modérée Lire le capteur en premier, garder un code compact
Association Wi-Fi et DHCP Élevée Réutiliser canal et BSSID, IP statique en option
Connexion MQTT et publication Élevée Connexion simple ou légère en session, une seule publication
Attente de confirmation Élevée Timeout court, abandonner rapidement
Deep sleep Très faible Conception de la carte : régulateur, LED, puce USB

La dernière ligne réserve souvent une surprise : beaucoup de cartes de développement gaspillent bien plus dans leur régulateur de tension, leur LED d'alimentation et leur puce USB-série que l'ESP32 lui-même en deep sleep. Pour une vraie autonomie, mesurez la carte, pas la puce. L'article complémentaire sur les capteurs ESP32 sur batterie détaille les choix matériels et les mesures de courant.

Pourquoi les retained messages sont indispensables ici

Un capteur endormi publie une fois puis disparaît. Si un tableau de bord ou une application mobile s'abonne une minute plus tard, un message normal est perdu. Avec le flag retain, le broker conserve le dernier message du topic et le transmet immédiatement à chaque nouvel abonné. Le tableau de bord affiche la mesure la plus récente dès son ouverture, quel que soit le moment du dernier réveil de l'appareil.

Ajoutez un horodatage ou un numéro de séquence dans le payload pour que les consommateurs sachent quel âge a la valeur retenue. Une mesure vieille de quatre heures qui semble actuelle est trompeuse, et pour un appareil en veille, « ancien » est l'état normal. Si les retained messages ne vous sont pas familiers, la présentation de ce que fait un broker MQTT explique comment le broker les stocke.

Confirmer la livraison sans publication en QoS 1

PubSubClient ne peut publier qu'en QoS 0, ce qui signifie « envoyé », pas « reçu ». Sur un appareil toujours allumé, c'est généralement acceptable. Sur un appareil qui dort, c'est un vrai problème : si vous appelez esp_deep_sleep_start() juste après publish(), la radio peut s'éteindre alors que le paquet est encore dans un tampon, et le message disparaît sans bruit.

Le sketch ci-dessous utilise une astuce simple et fiable. Il s'abonne à son propre topic d'état avant de publier, puis attend que le broker lui renvoie le nouveau message. Voir son propre message revenir prouve que le broker l'a reçu et routé. Un numéro de séquence stocké en mémoire RTC distingue le nouveau message de l'ancien message retenu, que le broker transmet aussi lors de l'abonnement.

Le sketch ESP32 deep sleep MQTT complet

#include <WiFi.h>
#include <PubSubClient.h>
#include "esp_sleep.h"

const char* WIFI_SSID     = "YOUR_WIFI";
const char* WIFI_PASSWORD = "YOUR_WIFI_PASSWORD";
const char* MQTT_HOST     = "broker.connectmq.com";
const uint16_t MQTT_PORT  = 1883;
const char* CLIENT_ID     = "sleepy-3e7f10";                 // unique
const char* TOPIC_STATE   = "connectmq/demo/sleepy-3e7f10/state";

const uint64_t SLEEP_SECONDS   = 300;                       // 5 minutes
const uint32_t WIFI_TIMEOUT_MS = 8000;
const uint32_t ACK_TIMEOUT_MS  = 3000;

// Survit au deep sleep (perdu en cas de coupure d'alimentation ou de reset)
RTC_DATA_ATTR uint32_t seq = 0;
RTC_DATA_ATTR uint32_t failures = 0;
RTC_DATA_ATTR uint8_t savedBssid[6];
RTC_DATA_ATTR int32_t savedChannel = 0;
RTC_DATA_ATTR bool haveSaved = false;

WiFiClient net;
PubSubClient mqtt(net);
volatile bool acked = false;
char expected[24];

void goToSleep() {
  mqtt.disconnect();
  net.stop();
  WiFi.disconnect(true);
  WiFi.mode(WIFI_OFF);
  // Espacer les réveils quand les échecs se répètent, pour qu'un réseau mort ne vide pas la batterie
  uint64_t seconds = SLEEP_SECONDS * (failures > 3 ? 4 : 1);
  Serial.printf("Sleeping %llu s (failures=%lu)\n", seconds, (unsigned long)failures);
  Serial.flush();
  esp_sleep_enable_timer_wakeup(seconds * 1000000ULL);
  esp_deep_sleep_start();
}

float readSensor() {
  // Remplacez par votre capteur ; une valeur simulée garde l'exemple autonome
  return 20.0 + (esp_random() % 500) / 100.0;
}

void onMessage(char* topic, byte* payload, unsigned int length) {
  // Notre payload contient "seq":N ; on cherche celui que l'on vient d'envoyer
  String msg;
  for (unsigned int i = 0; i < length; i++) msg += (char)payload[i];
  if (msg.indexOf(expected) >= 0) acked = true;
}

bool connectWifi() {
  WiFi.mode(WIFI_STA);
  WiFi.persistent(false);
  if (haveSaved) {
    WiFi.begin(WIFI_SSID, WIFI_PASSWORD, savedChannel, savedBssid, true);   // chemin rapide
  } else {
    WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
  }
  unsigned long start = millis();
  while (WiFi.status() != WL_CONNECTED) {
    if (millis() - start > WIFI_TIMEOUT_MS) {
      haveSaved = false;              // l'AP a pu changer de canal : scan complet la prochaine fois
      return false;
    }
    delay(20);
  }
  memcpy(savedBssid, WiFi.BSSID(), 6);
  savedChannel = WiFi.channel();
  haveSaved = true;
  return true;
}

void setup() {
  unsigned long t0 = millis();
  Serial.begin(115200);
  float temperature = readSensor();                 // lire avant que la radio ne chauffe la carte
  seq++;

  if (!connectWifi()) { failures++; goToSleep(); }

  mqtt.setServer(MQTT_HOST, MQTT_PORT);
  mqtt.setCallback(onMessage);
  mqtt.setSocketTimeout(4);
  if (!mqtt.connect(CLIENT_ID)) { failures++; goToSleep(); }

  mqtt.subscribe(TOPIC_STATE, 0);
  snprintf(expected, sizeof(expected), "\"seq\":%lu,", (unsigned long)seq);

  char payload[128];
  snprintf(payload, sizeof(payload),
           "{\"seq\":%lu,\"temperature\":%.2f,\"rssi\":%d,\"awakeMs\":%lu,\"failures\":%lu}",
           (unsigned long)seq, temperature, WiFi.RSSI(),
           millis() - t0, (unsigned long)failures);
  mqtt.publish(TOPIC_STATE, payload, true);         // retenu

  unsigned long start = millis();
  while (!acked && millis() - start < ACK_TIMEOUT_MS) {
    mqtt.loop();
    delay(5);
  }
  if (acked) failures = 0; else failures++;
  Serial.printf("seq=%lu acked=%d awake=%lu ms\n", (unsigned long)seq, acked, millis() - t0);
  goToSleep();
}

void loop() {
  // Jamais atteint : l'ESP32 redémarre dans setup() après chaque réveil
}

Ce que fait chaque optimisation

Mise en cache du canal et du BSSID. Un WiFi.begin() normal scanne tous les canaux pour trouver le point d'accès. Passer le canal et le BSSID enregistrés en mémoire RTC évite ce scan, ce qui constitue généralement le gain le plus important. Si le routeur change de canal, le chemin rapide expire, le cache est vidé et le réveil suivant effectue un scan complet.

Lire le capteur en premier. La radio chauffe sensiblement la carte, ce qui fausse les mesures de température. Échantillonner avant la connexion évite ce biais.

Espacement en cas d'échec. Quand le réseau est en panne, chaque réveil gaspille de l'énergie dans des tentatives vouées à l'échec. Après plusieurs échecs, le nœud dort quatre fois plus longtemps jusqu'à ce qu'une publication réussisse de nouveau.

awakeMs dans le payload. Indiquer combien de temps l'appareil est resté éveillé lors de la phase précédente vous donne une vue continue et gratuite du coût énergétique. Si la valeur augmente, quelque chose a changé sur le réseau.

Pas de Last Will. Un appareil en veille se déconnecte volontairement à chaque cycle : un Last Will offline n'aurait donc aucun sens. Jugez plutôt de sa vitalité à l'âge de la dernière mesure retenue.

En option, une IP statique permet aussi d'économiser l'échange DHCP. Appelez WiFi.config(ip, gateway, subnet, dns) avant WiFi.begin(), mais uniquement sur des réseaux où vous maîtrisez l'attribution des adresses.

Vérifier que tout fonctionne

Abonnez-vous depuis votre ordinateur :

mosquitto_sub -h broker.connectmq.com -p 1883 -i watch-3e7f10 -v -t "connectmq/demo/sleepy-3e7f10/#"

Réduisez SLEEP_SECONDS à 20 pour les tests. Vous devriez voir un message par cycle :

connectmq/demo/sleepy-3e7f10/state {"seq":7,"temperature":22.81,"rssi":-61,"awakeMs":1180,"failures":0}

Arrêtez maintenant l'abonné, puis relancez-le pendant que l'ESP32 dort. La dernière mesure apparaît immédiatement, exactement comme le vivra un tableau de bord. Coupez ensuite votre routeur pendant quelques cycles : le moniteur série affiche un compteur failures croissant, et le premier message après le rétablissement indique combien de cycles ont été manqués.

La zone publique de broker.connectmq.com est un serveur MQTT public gratuit adapté à ce genre de test. Ses sessions expirent au bout de 30 minutes au maximum, ce qui n'a pas d'importance ici puisque le sketch se connecte avec une nouvelle session à chaque réveil. Lorsque vous passez à un broker ConnectMQ privé, le sketch n'a besoin que d'identifiants (et du TLS si vous le choisissez) ; le seul point à surveiller est la limite de 25 connexions de l'offre gratuite, qu'une flotte de capteurs en veille atteint rarement puisque chacun n'est connecté qu'un instant par cycle.

Liste de contrôle pour l'autonomie

  1. Retirez ou désactivez la LED d'alimentation et utilisez une carte avec un régulateur à faible courant de repos, ou alimentez directement le module.
  2. Mesurez le courant en deep sleep avec un multimètre en série ; il doit être bien inférieur au milliampère sur une carte bien conçue.
  3. Gardez la fenêtre d'éveil courte : Wi-Fi en cache, timeouts courts, une seule publication.
  4. Choisissez un intervalle raisonnable. Diviser l'intervalle par deux double à peu près l'énergie consommée par la radio ; la plupart des mesures environnementales n'ont pas besoin d'une résolution à la minute.
  5. Alimentez le capteur depuis un GPIO et coupez-le avant la mise en veille, pour qu'il ne vide pas la batterie en arrière-plan.
  6. Évitez le TLS à chaque réveil si l'autonomie est critique et que le modèle de menace le permet sur un réseau privé ; si vous avez besoin du chiffrement, prévoyez le temps de handshake supplémentaire et suivez ESP32 MQTT SSL avec WiFiClientSecure.

Dépannage

  • Les messages n'arrivent que de temps en temps. Vous vous endormez avant que la publication ne quitte la radio. Conservez la confirmation par écho et ne réduisez pas trop ACK_TIMEOUT_MS.
  • seq revient à 1 de façon inattendue. La mémoire RTC survit au deep sleep, mais pas à une coupure de courant, à un brownout ni à un appui sur reset. Une tension de batterie en baisse qui provoque des brownouts est une cause fréquente.
  • L'appareil ne se réveille jamais. Vérifiez que vous appelez esp_sleep_enable_timer_wakeup() avec des microsecondes et une valeur 64 bits ; un débordement 32 bits produit un intervalle bien plus court ou erroné.
  • La sortie série est tronquée. Appelez Serial.flush() avant la mise en veille, comme le fait le sketch.
  • Durées d'éveil longues sur certains cycles. Le chemin Wi-Fi rapide a échoué et un scan complet a eu lieu. Vérifiez que votre routeur utilise un canal fixe.

Faire tourner des capteurs en veille en production

Les valeurs retenues ne sont fiables que si le broker qui les conserve l'est aussi. Un broker MQTT managé garde ce point d'accès disponible pendant que vos capteurs dorment, et les mêmes topics retenus alimentent vos tableaux de bord, scripts et applications. Avec ConnectMQ, vous créez des identifiants par appareil dans le portail, voyez la dernière valeur retenue de chaque capteur dans l'explorateur de topics en direct et définissez des règles d'alerte à seuil sur des mesures comme la température. Un broker MQTT cloud permet aussi à des capteurs installés dans un champ, une cave et un entrepôt de remonter vers un même point d'accès sans aucun serveur local. Vous découvrez la carte ? Commencez par le tutoriel IoT ESP32.

Démarrez avec ConnectMQ

Créez gratuitement un broker Serverless sur app.connectmq.com, sans carte bancaire. Connectez-y vos capteurs en veille avec leurs propres identifiants et regardez les mesures retenues apparaître dans le portail à chaque réveil.

Questions fréquentes

Pourquoi mon ESP32 perd-il des messages MQTT avant le deep sleep ?

Une publication en QoS 0 rend la main dès que les données sont mises en file d'attente : s'endormir immédiatement peut donc les couper. Attendez une confirmation, par exemple avec la technique d'écho de ce sketch, puis déconnectez-vous et dormez.

Un capteur en deep sleep doit-il conserver une session MQTT ?

En général, non. Une nouvelle connexion à chaque réveil est plus simple et fonctionne avec n'importe quel broker. Les sessions persistantes ne sont utiles que si l'appareil doit recevoir des commandes mises en file pendant son sommeil.

Comment un ESP32 en veille peut-il recevoir des commandes ?

Publiez les commandes en retained messages sur un topic auquel l'appareil s'abonne à son réveil. Il reçoit immédiatement la dernière commande, l'exécute, puis l'efface en publiant un message retenu vide.

Quel est un bon intervalle de publication pour un capteur sur batterie ?

Cela dépend de la vitesse à laquelle la valeur mesurée évolue. Les capteurs environnementaux fonctionnent souvent bien avec des intervalles de cinq à quinze minutes ; des intervalles plus courts coûtent proportionnellement plus d'énergie.

Articles liés

Exemple MQTT ESP-IDF : construire un client esp-mqtt à partir de zéro

Construisez un projet ESP-IDF à partir d'un dossier vide avec le composant esp-mqtt : configuration du Wi-Fi en mode station, gestionnaire d'événements MQTT, publication et abonnement, propriétés MQTT 5 et TLS avec le bundle de certificats.

9 min de lecture

Horodatage NTP ESP32 MQTT : ajouter une heure précise à chaque message

Synchronisez l'horloge de l'ESP32 avec NTP, formatez des horodatages ISO 8601 et epoch en millisecondes, et joignez-les aux payloads MQTT pour que les mesures conservent leur véritable heure de capture même lorsqu'elles sont publiées en retard.

8 min de lecture

Tous les articles →

Votre broker MQTT privé, à quelques clics

Créez un compte, récupérez les identifiants de votre broker et connectez votre premier appareil — aucun serveur à installer, mettre à jour ou dimensionner.