ESP32 et Arduino

Tutoriel de la bibliothèque Arduino PubSubClient : configuration, callbacks, buffers et keep alive

Un tutoriel PubSubClient pratique pour ESP32 et ESP8266 : installer la bibliothèque, se connecter à un broker MQTT, gérer les callbacks, dimensionner le buffer, régler le keep alive et passer à TLS avec de vrais identifiants.

Carte microcontrôleur exécutant la bibliothèque PubSubClient et échangeant des messages MQTT avec un broker cloud
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

PubSubClient est la bibliothèque MQTT la plus utilisée pour les cartes compatibles Arduino, et elle est assez légère pour tourner sur presque tout ce qui dispose d'une pile réseau. Dans ce tutoriel, vous allez l'installer, connecter un ESP32 à un vrai broker, publier et vous abonner, et découvrir les trois réglages à l'origine de la plupart des problèmes sur le terrain : la taille du buffer, le keep alive et le timeout de socket. À la fin, vous disposerez d'un sketch robuste aux reconnexions, réutilisable dans tous vos projets.

Ce qu'est PubSubClient (et ce qu'il n'est pas)

PubSubClient, écrit par Nick O'Leary, est un client MQTT 3.1.1 léger. Il ne gère pas lui-même la connexion réseau. Il enveloppe à la place tout objet qui implémente l'interface Client d'Arduino : WiFiClient sur ESP32 et ESP8266, EthernetClient sur un shield filaire, ou WiFiClientSecure si vous voulez du TLS. C'est grâce à cette conception que la même bibliothèque fonctionne sur autant de cartes.

Elle est aussi volontairement minimaliste. Connaître ses limites d'emblée évite des heures de débogage :

Fonctionnalité Comportement de PubSubClient
Version du protocole MQTT 3.1.1 (MQTT 3.1 en option à la compilation), pas de MQTT 5
QoS en publication QoS 0 uniquement
QoS en abonnement QoS 0 ou QoS 1
Buffer de paquet par défaut 256 octets (topic + payload + en-tête)
Keep alive par défaut 15 secondes
Timeout de socket par défaut 15 secondes
Last Will Pris en charge dans connect()
Publication retained Prise en charge
Modèle d'exécution Monothread, vous devez appeler loop() souvent

Si vous avez besoin des propriétés MQTT 5 ou de la publication en QoS 2 depuis l'appareil, tournez-vous vers un autre client. Pour la grande majorité des capteurs et actionneurs, la télémétrie en QoS 0 et les abonnements aux commandes en QoS 1 sont exactement ce qu'il faut. Notre guide des niveaux de QoS MQTT explique pourquoi cette combinaison fonctionne bien.

Installer PubSubClient et préparer la carte

  1. Dans l'IDE Arduino, ouvrez Outils → Gérer les bibliothèques, recherchez « PubSubClient » et installez la bibliothèque de Nick O'Leary (version 2.8 ou plus récente).
  2. Installez le paquet de cartes nécessaire : « esp32 by Espressif Systems » pour les cartes ESP32 ou « esp8266 by ESP8266 Community » pour les cartes de type NodeMCU.
  3. Sélectionnez votre carte et votre port, puis téléversez une fois le sketch Blink vide pour vérifier que la chaîne de compilation fonctionne.
  4. Choisissez un identifiant d'appareil unique, par exemple pubsub-7f3a9c. Vous l'utiliserez à la fois comme client ID MQTT et comme préfixe de topic.

Pour le premier test, vous utiliserez l'espace public de broker.connectmq.com, le serveur MQTT public gratuit exploité par ConnectMQ. Il accepte les connexions anonymes sur le port 1883 tant que le client ID n'est pas vide. Tout ce qui y est publié est lisible par tous : n'y envoyez jamais de vrais identifiants ni de données personnelles.

L'exemple PubSubClient complet

Le sketch ci-dessous se connecte au Wi-Fi, se connecte au broker avec un Last Will, publie un message JSON toutes les cinq secondes, s'abonne à un topic de commande et se reconnecte sans bloquer le reste de votre code.

#include <WiFi.h>          // utilisez <ESP8266WiFi.h> sur ESP8266
#include <PubSubClient.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* DEVICE_ID = "pubsub-7f3a9c";   // rendez-le unique

char topicTelemetry[64];
char topicCommand[64];
char topicStatus[64];

WiFiClient netClient;
PubSubClient mqtt(netClient);

unsigned long lastPublish = 0;
unsigned long lastReconnectAttempt = 0;

void onMessage(char* topic, byte* payload, unsigned int length) {
  // le payload n'est PAS terminé par un caractère nul : copiez-le en utilisant la longueur
  char msg[64];
  unsigned int n = length < sizeof(msg) - 1 ? length : sizeof(msg) - 1;
  memcpy(msg, payload, n);
  msg[n] = '\0';

  Serial.printf("Message on %s: %s\n", topic, msg);

  if (strcmp(msg, "ping") == 0) {
    mqtt.publish(topicStatus, "pong");
  }
}

void connectWifi() {
  WiFi.mode(WIFI_STA);
  WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
  Serial.print("Connecting to Wi-Fi");
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }
  Serial.printf("\nIP address: %s\n", WiFi.localIP().toString().c_str());
}

bool connectMqtt() {
  // connect(clientId, user, pass, willTopic, willQos, willRetain, willMessage)
  bool ok = mqtt.connect(DEVICE_ID, nullptr, nullptr,
                         topicStatus, 1, true, "offline");
  if (ok) {
    mqtt.publish(topicStatus, "online", true);
    mqtt.subscribe(topicCommand, 1);
    Serial.println("MQTT connected");
  } else {
    Serial.printf("MQTT connect failed, rc=%d\n", mqtt.state());
  }
  return ok;
}

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

  snprintf(topicTelemetry, sizeof(topicTelemetry), "connectmq/demo/%s/telemetry", DEVICE_ID);
  snprintf(topicCommand,   sizeof(topicCommand),   "connectmq/demo/%s/cmd", DEVICE_ID);
  snprintf(topicStatus,    sizeof(topicStatus),    "connectmq/demo/%s/status", DEVICE_ID);

  connectWifi();

  mqtt.setServer(MQTT_HOST, MQTT_PORT);
  mqtt.setCallback(onMessage);
  mqtt.setBufferSize(512);   // de la place pour des payloads JSON plus volumineux
  mqtt.setKeepAlive(30);     // secondes
  mqtt.setSocketTimeout(10); // secondes
}

void loop() {
  if (WiFi.status() != WL_CONNECTED) {
    connectWifi();
  }

  if (!mqtt.connected()) {
    unsigned long now = millis();
    if (now - lastReconnectAttempt > 5000) {
      lastReconnectAttempt = now;
      connectMqtt();
    }
    return;
  }

  mqtt.loop();

  if (millis() - lastPublish > 5000) {
    lastPublish = millis();
    char payload[96];
    snprintf(payload, sizeof(payload),
             "{\"uptime\":%lu,\"rssi\":%d,\"heap\":%u}",
             millis() / 1000, WiFi.RSSI(), ESP.getFreeHeap());
    if (!mqtt.publish(topicTelemetry, payload)) {
      Serial.println("Publish failed");
    }
  }
}

Observez la sortie depuis votre ordinateur avec les outils en ligne de commande Mosquitto :

mosquitto_sub -h broker.connectmq.com -p 1883 -i watcher-7f3a9c -t "connectmq/demo/pubsub-7f3a9c/#" -v

Puis envoyez une commande à la carte :

mosquitto_pub -h broker.connectmq.com -p 1883 -i sender-7f3a9c -t "connectmq/demo/pubsub-7f3a9c/cmd" -m "ping"

Vous devriez voir pong apparaître sur le topic d'état en moins d'une seconde.

Comment fonctionnent les callbacks de PubSubClient

La signature du callback est fixe : void callback(char* topic, byte* payload, unsigned int length). Trois détails comptent.

Premièrement, le payload est un pointeur vers le buffer interne de la bibliothèque et il n'est pas terminé par un caractère nul. Le traiter comme une chaîne C avec Serial.println((char*)payload) affiche des caractères parasites après le vrai message. Copiez-le toujours en utilisant length, comme le fait l'exemple.

Deuxièmement, le callback s'exécute à l'intérieur de mqtt.loop(). Si vous publiez depuis le callback, la bibliothèque réutilise le même buffer, si bien que les pointeurs topic et payload d'origine ne sont plus valides ensuite. Copiez tout ce dont vous avez besoin avant d'appeler publish().

Troisièmement, gardez les callbacks courts. Un delay(2000) dans le callback empêche loop() d'entretenir la connexion et peut provoquer un dépassement du keep alive. Positionnez un indicateur ou stockez la valeur, puis agissez dans votre boucle principale.

La taille du buffer : le piège le plus courant de PubSubClient

Depuis la version 2.8, vous pouvez appeler setBufferSize() à l'exécution au lieu de modifier MQTT_MAX_PACKET_SIZE dans l'en-tête de la bibliothèque. Le buffer doit contenir le paquet MQTT entier : en-tête fixe, longueur du topic, topic et payload. Avec la valeur par défaut de 256 octets, un document JSON de 220 caractères sur un topic de 40 caractères échoue tout simplement, et publish() renvoie false sans plus d'explication. Les messages entrants trop volumineux sont silencieusement ignorés avant même que votre callback ne soit appelé.

Règles empiriques :

  • Dimensionnez le buffer pour votre plus long topic plus payload, plus une dizaine d'octets d'en-tête.
  • Appelez setBufferSize() avant connect() et vérifiez sa valeur de retour : elle vaut false si l'allocation a échoué.
  • Sur ESP8266, gardez un buffer modeste, car la RAM est partagée avec TLS et le Wi-Fi.

Nous entrons dans le détail, y compris l'envoi en flux de gros payloads avec beginPublish(), dans taille du buffer PubSubClient : corriger les échecs sur les gros messages.

Keep alive et timeout de socket

Le keep alive indique au broker combien de temps le client peut rester silencieux. PubSubClient envoie un PINGREQ lorsque rien d'autre n'a été envoyé pendant l'intervalle, mais seulement si loop() est appelé. Si votre code bloque plus d'environ une fois et demie l'intervalle de keep alive, le broker coupe la session et votre Last Will est déclenché.

  • setKeepAlive(30) est une valeur par défaut raisonnable pour des appareils alimentés sur secteur avec un Wi-Fi correct.
  • Montez plus haut (60 à 120 secondes) sur les liaisons cellulaires où chaque paquet coûte de l'énergie ou des données.
  • setSocketTimeout(10) limite la durée pendant laquelle connect() et les lectures peuvent bloquer. Des valeurs plus basses évitent qu'un réseau instable ne fige votre appareil 15 secondes à chaque fois.

Lire l'état de la connexion

Lorsque connect() échoue, mqtt.state() vous en donne la raison. Les valeurs négatives correspondent à des problèmes réseau ; les valeurs positives sont des refus du broker.

state() Signification
-4 Timeout de connexion, pas de CONNACK à temps
-3 Connexion perdue
-2 Échec de la connexion réseau (DNS, port, TLS, routage)
-1 Déconnecté proprement
0 Connecté
1 Mauvaise version de protocole
2 Client ID refusé
3 Serveur indisponible
4 Nom d'utilisateur ou mot de passe incorrect
5 Non autorisé

De loin le plus fréquent est -2. Notre guide dédié à PubSubClient rc=-2 et aux autres codes d'état passe en revue chaque valeur avec ses solutions.

Passer à TLS et à de vrais identifiants

Le serveur MQTT public gratuit est parfait pour apprendre, mais les appareils en production ont besoin de chiffrement et de leurs propres identifiants. Avec un broker MQTT managé de ConnectMQ, vous créez un broker dans le portail et recevez un nom d'utilisateur, un mot de passe et un client ID générés. La modification du sketch est minime :

#include <WiFiClientSecure.h>

WiFiClientSecure netClient;   // remplace WiFiClient
PubSubClient mqtt(netClient);

// ISRG Root X1, la racine Let's Encrypt utilisée par broker.connectmq.com
const char* ROOT_CA = R"EOF(
-----BEGIN CERTIFICATE-----
...paste ISRG Root X1 PEM here...
-----END CERTIFICATE-----
)EOF";

void setup() {
  // ... Wi-Fi comme précédemment ...
  netClient.setCACert(ROOT_CA);
  mqtt.setServer("broker.connectmq.com", 8883);
}

bool connectMqtt() {
  return mqtt.connect("YOUR_CLIENT_ID", "YOUR_USERNAME", "YOUR_PASSWORD",
                      topicStatus, 1, true, "offline");
}

ConnectMQ lie chaque client ID aux identifiants qu'il a émis : utilisez donc exactement le client ID fourni par le portail. Sur un broker privé, vous concevez librement vos préfixes de topic ; le préfixe public connectmq/demo/... n'est plus nécessaire. Pour d'autres pistes de durcissement, voir les bonnes pratiques de sécurité MQTT.

Checklist de dépannage

  1. Se connecte, puis décroche toutes les quelques secondes : deux appareils partagent un client ID. Le broker déconnecte l'ancienne session chaque fois que le second se connecte.
  2. publish() renvoie false : payload plus topic dépassent le buffer, ou le client n'est pas connecté.
  3. Le callback ne se déclenche jamais : vous avez oublié mqtt.loop(), l'abonnement a été fait avant que la connexion n'existe, ou le message est plus grand que le buffer.
  4. Déconnexions aléatoires sous charge : du code bloquant long dans loop() ; découpez-le ou déplacez-le dans un timer.
  5. Fonctionne sur 1883, échoue sur 8883 : mauvais certificat racine, ou horloge non réglée sur les cartes qui vérifient les dates de validité (fréquent sur ESP8266).

Si vous hésitez entre plusieurs bibliothèques, le guide de la bibliothèque ArduinoMqttClient et le client MQTT asynchrone pour ESP32 couvrent les principales alternatives, et le tutoriel IoT ESP32 assemble toutes les pièces dans un projet de capteur complet.

Connectez vos appareils PubSubClient à ConnectMQ

Une fois que votre sketch fonctionne avec le serveur public, le basculer vers un broker MQTT cloud privé ne demande de modifier que deux lignes. ConnectMQ vous offre MQTT sur TCP, TLS sur le port 8883 et WebSockets, un client MQTT dans le navigateur pour vos tests, un explorateur de topics en direct et des règles d'alerte à seuil. Créez un broker Serverless gratuit sur app.connectmq.com, sans carte bancaire, et vous aurez vos identifiants en quelques minutes.

Questions fréquentes

PubSubClient prend-il en charge MQTT 5 ?

Non. PubSubClient parle MQTT 3.1.1, que tous les brokers grand public, dont ConnectMQ, prennent toujours en charge aux côtés de MQTT 5. Si vous avez besoin des propriétés MQTT 5 sur l'appareil, choisissez une autre bibliothèque cliente.

PubSubClient peut-il publier en QoS 1 ?

Non. publish() envoie toujours en QoS 0. Vous pouvez en revanche vous abonner en QoS 1 : les commandes envoyées à l'appareil en QoS 1 sont alors livrées de manière fiable par le broker.

Pourquoi mon payload PubSubClient affiche-t-il des caractères étranges ?

Le buffer du payload n'est pas terminé par un caractère nul. Copiez length octets dans votre propre buffer et ajoutez un \0 final avant de le traiter comme une chaîne.

Quelle est la taille maximale d'un message avec PubSubClient ?

La taille du buffer est une valeur sur 16 bits : le plafond pratique est donc juste en dessous de 64 Ko, mais la vraie limite est la RAM. Gardez des messages courts sur les cartes contraintes et n'oubliez pas que votre plan de broker impose lui aussi une taille maximale de message.

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.