Tutoriels

Tutoriel MQTT pour ESP8266 avec PubSubClient

Connectez un NodeMCU ou Wemos D1 mini à un broker MQTT avec PubSubClient : sketch complet, reconnexion non bloquante, Last Will et TLS avec empreinte.

Mis à jour · 9 min de lecture

Tutoriel MQTT pour ESP8266 avec PubSubClient

L'ESP8266 reste l'un des moyens les moins chers de mettre un capteur sur le Wi-Fi, et PubSubClient est la bibliothèque MQTT utilisée par la plupart des sketches ESP8266. Ce tutoriel vous donne un sketch complet pour NodeMCU ou Wemos D1 mini qui se connecte à un broker MQTT, publie de la télémétrie, reçoit des commandes, se reconnecte sans bloquer et s'annonce avec un Last Will — puis passe la connexion en TLS avec l'empreinte (fingerprint) du certificat et explique quand préférer une CA racine.

Configuration testée : Arduino IDE 2.x ou arduino-cli, core ESP8266 3.1.x, PubSubClient 2.8.

Installer la chaîne d'outils

  1. Dans l'Arduino IDE, ouvrez Préférences et ajoutez cette URL du gestionnaire de cartes : https://arduino.esp8266.com/stable/package_esp8266com_index.json.
  2. Gestionnaire de cartes → installez esp8266 by ESP8266 Community.
  3. Gestionnaire de bibliothèques → installez PubSubClient de Nick O'Leary.
  4. Choisissez NodeMCU 1.0 (ESP-12E Module) ou LOLIN(WEMOS) D1 R2 & mini comme carte.

Avec arduino-cli, les mêmes étapes donnent :

Configuration avec arduino-cli
arduino-cli config add board_manager.additional_urls \
  https://arduino.esp8266.com/stable/package_esp8266com_index.json
arduino-cli core update-index
arduino-cli core install esp8266:esp8266
arduino-cli lib install PubSubClient

L'ESP8266 ne gère que le Wi-Fi 2,4 GHz. Si votre routeur diffuse un SSID unique pour les deux bandes, cela fonctionne en général ; un réseau uniquement 5 GHz ne fonctionnera jamais.

Le sketch complet

esp8266_mqtt.ino
#include <ESP8266WiFi.h>
#include <PubSubClient.h>

const char* WIFI_SSID = "your-ssid";
const char* WIFI_PASS = "your-wifi-password";
const char* MQTT_HOST = "broker.connectmq.com";
const uint16_t MQTT_PORT = 1883;
const char* MQTT_USER = "username";
const char* MQTT_PASS = "password";

WiFiClient net;
PubSubClient mqtt(net);

String clientId, baseTopic;
unsigned long lastAttempt = 0, retryDelay = 1000, lastPublish = 0;

void onMessage(char* topic, byte* payload, unsigned int length) {
  String msg;
  for (unsigned int i = 0; i < length; i++) msg += (char)payload[i];
  Serial.printf("[%s] %s\n", topic, msg.c_str());
  if (msg == "on")  digitalWrite(LED_BUILTIN, LOW);    // la LED intégrée est active à l'état bas
  if (msg == "off") digitalWrite(LED_BUILTIN, HIGH);
}

bool connectMqtt() {
  String status = baseTopic + "/status";
  // connect(id, user, pass, willTopic, willQos, willRetain, willMessage)
  if (!mqtt.connect(clientId.c_str(), MQTT_USER, MQTT_PASS,
                    status.c_str(), 1, true, "offline")) {
    Serial.printf("MQTT connect failed, state=%d\n", mqtt.state());
    return false;
  }
  mqtt.publish(status.c_str(), "online", true);              // retenu
  mqtt.subscribe((baseTopic + "/led").c_str(), 1);
  Serial.println("MQTT connected as " + clientId);
  return true;
}

void setup() {
  Serial.begin(115200);
  pinMode(LED_BUILTIN, OUTPUT);
  digitalWrite(LED_BUILTIN, HIGH);

  WiFi.persistent(false);              // ne pas réécrire la flash à chaque démarrage
  WiFi.mode(WIFI_STA);
  WiFi.begin(WIFI_SSID, WIFI_PASS);

  clientId  = "esp8266-" + String(ESP.getChipId(), HEX);
  baseTopic = "testmqtt/esp8266/" + clientId;

  mqtt.setServer(MQTT_HOST, MQTT_PORT);
  mqtt.setCallback(onMessage);
  mqtt.setBufferSize(512);             // 256 octets par défaut, topic et en-tête compris
  mqtt.setKeepAlive(30);
  mqtt.setSocketTimeout(5);            // un connect raté bloque au maximum ~5 s
}

void loop() {
  if (WiFi.status() != WL_CONNECTED) {
    delay(10);                         // le core reconnecte le Wi-Fi tout seul
    return;
  }

  if (!mqtt.connected()) {
    unsigned long now = millis();
    if (now - lastAttempt >= retryDelay) {
      lastAttempt = now;
      retryDelay = connectMqtt() ? 1000 : min(retryDelay * 2, 60000UL);
    }
    return;
  }

  mqtt.loop();                         // doit tourner souvent : reçoit les messages, envoie les pings

  if (millis() - lastPublish >= 10000) {
    lastPublish = millis();
    char payload[96];
    snprintf(payload, sizeof(payload),
             "{\"rssi\":%d,\"heap\":%u,\"uptime\":%lu}",
             WiFi.RSSI(), ESP.getFreeHeap(), millis() / 1000);
    mqtt.publish((baseTopic + "/telemetry").c_str(), payload);
  }
}

Pourquoi il est écrit ainsi

  • Reconnexion non bloquante. L'exemple classique utilise while (!client.connected()) { ...; delay(5000); }, qui fige tout le reste — boutons, capteurs, et même la pile Wi-Fi — tant que le broker est injoignable. Ici, loop() rend la main immédiatement et réessaie avec un backoff exponentiel (1 s, 2 s, 4 s… jusqu'à 60 s), pour qu'une flotte ne martèle pas un broker en train de redémarrer.
  • Client ID stable dérivé de ESP.getChipId(). Deux cartes avec le même ID s'éjectent l'une l'autre en boucle ; voir les bonnes pratiques pour le client ID.
  • Last Will + statut retenu. Si la carte perd son alimentation, le broker publie offline sur .../status à l'expiration du keep-alive (1,5 × 30 s ici). Tout abonné arrivant plus tard voit immédiatement l'état actuel.
  • S'abonner après chaque connexion. PubSubClient utilise une clean session : les abonnements ne survivent pas à une reconnexion. Les endpoints de test partagés comme broker.connectmq.com ne conservent de toute façon pas les sessions, c'est pourquoi le sketch se réabonne toujours dans connectMqtt().
  • setBufferSize(512) : le buffer par défaut de PubSubClient fait 256 octets pour le paquet entier, topic compris. Un topic long plus un payload JSON le dépassent vite, et publish() renvoie alors false sans autre indice. Le calculateur de taille de paquet montre la taille réelle de votre paquet.
  • setSocketTimeout(5) : connect() est lui-même bloquant. Le timeout par défaut de 15 secondes suffit à déclencher des resets du watchdog ou une interface figée.

Tester le sketch

Flashez la carte et ouvrez le moniteur série à 115200 bauds. Vous devriez voir MQTT connected as esp8266-xxxxxx. Si state=-2 ou un autre code apparaît à la place, reportez-vous au tableau des erreurs plus bas.

Ouvrez ensuite le client MQTT en ligne, connectez-vous à broker.connectmq.com et abonnez-vous à testmqtt/esp8266/#. Le statut online retenu apparaît d'abord, puis un JSON de télémétrie toutes les 10 secondes. Publiez on sur testmqtt/esp8266/<client-id>/led et la LED s'allume.

Voir votre ESP8266 dans le navigateur

Pour tester le Last Will, débranchez la carte et attendez environ 45 secondes : le topic de statut passe à offline. Rebranchez-la et il revient à online : votre chemin de reconnexion fonctionne.

Ajouter TLS avec une empreinte

Le port 1883 transmet le mot de passe MQTT en clair. Le core ESP8266 inclut BearSSL, donc TLS sur le port 8883 ne demande qu'une autre classe de client. La façon la plus légère de vérifier le serveur est l'épinglage du certificat par empreinte SHA-1.

Récupérez l'empreinte du certificat actuel du broker depuis votre ordinateur :

Lire l'empreinte SHA-1
openssl s_client -connect broker.connectmq.com:8883 \
  -servername broker.connectmq.com </dev/null 2>/dev/null \
  | openssl x509 -noout -fingerprint -sha1 \
  | sed 's/.*=//; s/:/ /g'

Puis modifiez trois choses dans le sketch :

Modifications pour TLS
#include <WiFiClientSecure.h>

const uint16_t MQTT_PORT = 8883;
const char* FINGERPRINT = "AA BB CC DD EE FF 00 11 22 33 44 55 66 77 88 99 AA BB CC DD";

BearSSL::WiFiClientSecure net;         // remplace WiFiClient
PubSubClient mqtt(net);

// dans setup(), avant mqtt.setServer() :
net.setFingerprint(FINGERPRINT);
mqtt.setSocketTimeout(10);             // le handshake TLS est lent sur une puce à 80 MHz

Le handshake prend nettement plus de temps qu'un connect TCP simple et mobilise une bonne partie de la RAM limitée de l'ESP8266. Surveillez ESP.getFreeHeap() dans la télémétrie ; si cela devient juste, BearSSL peut utiliser des buffers plus petits avec net.setBufferSizes(1024, 1024), mais seulement si le broker prend en charge l'extension TLS max fragment length — vérifiez d'abord avec BearSSL::WiFiClientSecure::probeMaxFragmentLength(MQTT_HOST, 8883, 1024).

Empreinte ou CA racine

MéthodeCommentCompromis
setFingerprint()Épingle le SHA-1 du certificat finalLe plus léger et le plus rapide, mais casse à chaque renouvellement du certificat
setTrustAnchors()Épingle la CA racine (BearSSL::X509List)Survit aux renouvellements ; nécessite l'heure exacte via configTime()
setInsecure()Aucune vérificationChiffré mais accepte n'importe quel serveur : débogage uniquement

Les certificats des CA publiques sont renouvelés régulièrement, souvent tous les quelques mois. Une flotte avec empreinte épinglée cesse de se connecter le jour du renouvellement avec state=-2, et il faut reflasher chaque appareil. Pour tout ce qui doit durer, épinglez plutôt la CA racine :

CA racine au lieu d'une empreinte
#include <time.h>
static const char ROOT_CA[] PROGMEM = R"EOF(
-----BEGIN CERTIFICATE-----
...certificat racine de la CA qui signe le certificat de votre broker...
-----END CERTIFICATE-----
)EOF";
BearSSL::X509List trustAnchor(ROOT_CA);

// dans setup(), une fois le Wi-Fi connecté :
configTime(0, 0, "pool.ntp.org", "time.nist.gov");
while (time(nullptr) < 1700000000) delay(200);   // attendre NTP
net.setTrustAnchors(&trustAnchor);

Sans horloge valide, BearSSL considère tout certificat comme expiré ou pas encore valide. Plus de détails sur TLS, y compris les certificats client, dans le guide TLS pour ESP32 : l'API BearSSL diffère, mais les concepts sont les mêmes.

Codes d'erreur

mqtt.state() renvoie ces valeurs :

stateConstanteCause typique
-4MQTT_CONNECTION_TIMEOUTPas de réponse au ping dans le délai keep-alive ; Wi-Fi faible
-3MQTT_CONNECTION_LOSTRéseau perdu ou socket fermé par le broker
-2MQTT_CONNECT_FAILEDÉchec TCP/TLS : mauvais hôte ou port, DNS, pare-feu, empreinte périmée
-1MQTT_DISCONNECTEDDéconnexion propre, ou jamais connecté
1MQTT_CONNECT_BAD_PROTOCOLLe broker refuse MQTT 3.1.1
2MQTT_CONNECT_BAD_CLIENT_IDClient ID rejeté
3MQTT_CONNECT_UNAVAILABLEBroker indisponible
4MQTT_CONNECT_BAD_CREDENTIALSNom d'utilisateur ou mot de passe incorrect
5MQTT_CONNECT_UNAUTHORIZEDAuthentifié mais pas autorisé à se connecter

Les valeurs positives sont des codes de retour CONNACK du broker — consultez-les dans l'outil des reason codes. Les valeurs négatives viennent de la bibliothèque elle-même et signalent presque toujours un problème réseau. La méthode pas à pas de tester la connexion d'un ESP32 au broker s'applique de la même façon à un ESP8266.

Pour aller plus loin

  • Vous passez à une puce plus récente ? Le tutoriel ESP32 avec Arduino utilise la même API PubSubClient.
  • Besoin de publier en QoS 1 ou d'un autre style d'API ? Comparez avec la bibliothèque arduino-mqtt.
  • Des payloads plus gros ? ArduinoJson avec MQTT montre comment dimensionner correctement les buffers.

Un nœud MQTT fiable sur ESP8266 tient à quelques habitudes : ne jamais bloquer loop(), se reconnecter avec backoff, se réabonner à chaque connexion, dimensionner le buffer pour vos vrais paquets et épingler une CA racine plutôt qu'une empreinte sur les appareils qui doivent continuer à fonctionner après le prochain renouvellement de certificat.

Questions fréquentes

Pourquoi la connexion TLS de mon ESP8266 cesse-t-elle de fonctionner au bout de quelques semaines ?

Si vous avez épinglé le certificat avec setFingerprint(), l'empreinte change à chaque renouvellement du certificat du broker, et le handshake échoue alors avec state -2. Épinglez plutôt la CA racine avec setTrustAnchors() : elle survit aux renouvellements tant que la même CA signe le nouveau certificat.

PubSubClient peut-il publier en QoS 1 sur l'ESP8266 ?

Non. PubSubClient ne publie qu'en QoS 0. Il peut s'abonner en QoS 0 ou 1, donc les messages entrants peuvent être acquittés, mais les messages sortants sont en fire-and-forget. Si vous avez besoin de publier en QoS 1, utilisez une bibliothèque comme arduino-mqtt ou ajoutez un acquittement applicatif.

Que signifie le state -2 de PubSubClient ?

MQTT_CONNECT_FAILED : la connexion TCP ou TLS vers le broker n'a pas pu être ouverte. Le problème se situe sous MQTT : Wi-Fi, DNS, mauvais port, pare-feu ou échec du handshake TLS, par exemple une empreinte périmée ou un WiFiClient simple sur le port 8883.