Tutoriels

Tutoriel MQTT ESP32 avec Arduino (PubSubClient)

Tutoriel MQTT ESP32 avec Arduino et PubSubClient : connexion WiFi et broker, publication de JSON, abonnement aux commandes, reconnexion fiable et TLS.

Mis à jour · 8 min de lecture

L’ESP32 est le microcontrôleur de référence pour les projets MQTT : WiFi intégré, assez de RAM pour TLS et un support Arduino de premier ordre. Ce tutoriel construit un sketch complet avec la bibliothèque PubSubClient qui publie des mesures de capteurs en JSON, écoute un topic de commande, se reconnecte tout seul et — à la dernière étape — passe en TLS.

Ce dont vous avez besoin

  • Une carte de développement ESP32 et un câble USB.
  • Arduino IDE 2.x (ou PlatformIO) avec le paquet de cartes esp32 by Espressif Systems installé via le gestionnaire de cartes.
  • La bibliothèque PubSubClient de Nick O’Leary, installée via le gestionnaire de bibliothèques.
  • Un broker. Nous utilisons le broker public HiveMQ sur le port 1883 pour les tests — suffisant pour une démo, mais n’importe qui peut lire les topics.

Organisation des topics

Une structure de topics propre facilite la gestion des appareils. Chaque carte reçoit son propre préfixe, basé sur un identifiant d’appareil unique :

TopicSensRôle
testmqtt/esp32/<id>/telemetryESP32 vers brokerJSON des capteurs toutes les 10 secondes
testmqtt/esp32/<id>/statusESP32 vers brokeronline / offline (retenu, Last Will)
testmqtt/esp32/<id>/cmdBroker vers ESP32Commandes comme led:on

Voir topics et jokers MQTT pour les conventions de nommage.

Le sketch complet

esp32_mqtt.ino
#include <WiFi.h>
#include <PubSubClient.h>

const char* WIFI_SSID     = "your-ssid";
const char* WIFI_PASSWORD = "your-wifi-password";

const char* MQTT_HOST = "broker.hivemq.com";
const uint16_t MQTT_PORT = 1883;
const char* MQTT_USER = nullptr;   // set for brokers that require auth
const char* MQTT_PASS = nullptr;

const int LED_PIN = 2;             // on-board LED on many dev boards

WiFiClient net;
PubSubClient mqtt(net);

char deviceId[24];
char topicTelemetry[64];
char topicStatus[64];
char topicCmd[64];

unsigned long lastReconnectAttempt = 0;
unsigned long 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("Message on %s: %s\n", topic, msg.c_str());

  if (msg == "led:on")  digitalWrite(LED_PIN, HIGH);
  if (msg == "led:off") digitalWrite(LED_PIN, LOW);
}

void connectWiFi() {
  WiFi.mode(WIFI_STA);
  WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
  Serial.print("Connecting to WiFi");
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }
  Serial.printf("\nWiFi connected, IP %s\n", WiFi.localIP().toString().c_str());
}

bool connectMqtt() {
  // Last Will: broker publishes "offline" (retained) if we drop unexpectedly
  bool ok = mqtt.connect(deviceId, MQTT_USER, MQTT_PASS,
                         topicStatus, 1, true, "offline");
  if (!ok) {
    Serial.printf("MQTT connect failed, state=%d\n", mqtt.state());
    return false;
  }
  Serial.println("MQTT connected");
  mqtt.publish(topicStatus, "online", true);
  mqtt.subscribe(topicCmd, 1);
  return true;
}

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

  // Unique client ID from the chip's MAC address
  uint64_t mac = ESP.getEfuseMac();
  snprintf(deviceId, sizeof(deviceId), "esp32-%04X%08X",
           (uint16_t)(mac >> 32), (uint32_t)mac);
  snprintf(topicTelemetry, sizeof(topicTelemetry), "testmqtt/esp32/%s/telemetry", deviceId);
  snprintf(topicStatus, sizeof(topicStatus), "testmqtt/esp32/%s/status", deviceId);
  snprintf(topicCmd, sizeof(topicCmd), "testmqtt/esp32/%s/cmd", deviceId);
  Serial.printf("Device ID: %s\n", deviceId);

  connectWiFi();

  mqtt.setServer(MQTT_HOST, MQTT_PORT);
  mqtt.setCallback(onMessage);
  mqtt.setBufferSize(512);   // default is 256 bytes for the whole packet
  mqtt.setKeepAlive(30);
}

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

  if (!mqtt.connected()) {
    // Non-blocking reconnect: try every 5 seconds without stalling loop()
    unsigned long now = millis();
    if (now - lastReconnectAttempt > 5000) {
      lastReconnectAttempt = now;
      if (connectMqtt()) lastReconnectAttempt = 0;
    }
    return;
  }

  mqtt.loop();   // must run often: handles keep-alive and incoming messages

  unsigned long now = millis();
  if (now - lastPublish > 10000) {
    lastPublish = now;
    float temperature = temperatureRead();   // ESP32 internal sensor (rough)
    char payload[128];
    snprintf(payload, sizeof(payload),
             "{\"device\":\"%s\",\"temp\":%.1f,\"rssi\":%d,\"uptime\":%lu}",
             deviceId, temperature, WiFi.RSSI(), now / 1000);
    bool sent = mqtt.publish(topicTelemetry, payload);
    Serial.printf("Publish %s: %s\n", sent ? "ok" : "FAILED", payload);
  }
}

Ouvrez le moniteur série à 115200 bauds pour voir l’ID de l’appareil, puis regardez la télémétrie arriver dans le navigateur en vous abonnant à testmqtt/esp32/#. Publiez led:on sur le topic cmd et la LED s’allume.

Voir la télémétrie de votre ESP32 en direct

Comment fonctionne le sketch

Des client ID uniques

Les brokers MQTT n’autorisent qu’une connexion par client ID. Si deux cartes sont flashées avec le même ID codé en dur, comme ESP32Client, elles se déconnectent mutuellement en boucle. Dériver l’ID de l’adresse MAC eFuse le garde unique et stable d’un redémarrage à l’autre. Le générateur de client ID explique les règles si vous voulez un autre schéma.

Une boucle de reconnexion non bloquante

Beaucoup d’exemples utilisent une boucle while (!client.connected()) avec delay(5000). Cela bloque tout le reste sur la carte. Le motif à base de millis() ci-dessus réessaie toutes les cinq secondes pendant que le reste de loop() continue de tourner. mqtt.state() vous indique pourquoi une connexion a échoué : -2 pour une erreur réseau, 4 pour de mauvais identifiants et 5 pour un accès non autorisé. Comparez avec la liste complète de notre guide des erreurs de connexion MQTT.

Taille du buffer et MQTT_MAX_PACKET_SIZE

Le buffer de PubSubClient doit contenir le paquet entier : en-tête fixe, topic et payload. Par défaut, il fait 256 octets, et un payload JSON avec un topic long le dépasse vite — publish() renvoie alors silencieusement false. Les anciens tutoriels vous disent de modifier MQTT_MAX_PACKET_SIZE dans l’en-tête de la bibliothèque ; depuis la version 2.8, appelez simplement setBufferSize() avant de vous connecter. Le calculateur de taille de paquet MQTT vous indique la taille réelle de vos paquets.

Les limites de PubSubClient

  • MQTT 3.1.1 uniquement — pas de propriétés MQTT 5.
  • Publication en QoS 0 uniquement ; les abonnements peuvent utiliser la QoS 0 ou 1.
  • Les publications retenues et le Last Will sont pris en charge, comme montré ci-dessus.

Statut en ligne avec Last Will et messages retenus

L’appel à connect() enregistre un Last Will : si la carte perd l’alimentation ou le WiFi sans déconnexion propre, le broker publie offline sur le topic de statut après le délai de keep-alive. Juste après la connexion, le sketch publie online sur ce même topic. Les deux sont retenus : un tableau de bord qui s’abonne plus tard voit donc immédiatement l’état courant. Ce motif est expliqué dans Last Will and Testament et les messages retenus.

Tester en ligne de commande

Inutile d’avoir un second appareil pour tester le topic de commande. Avec les clients Mosquitto installés, envoyez une commande et observez la télémétrie (remplacez l’ID par celui affiché dans le moniteur série) :

bash
# watch everything your board publishes
mosquitto_sub -h broker.hivemq.com -t 'testmqtt/esp32/#' -v

# switch the LED on
mosquitto_pub -h broker.hivemq.com -t 'testmqtt/esp32/esp32-ABCD12345678/cmd' -m 'led:on'

D’autres recettes en ligne de commande dans exemples mosquitto_pub et mosquitto_sub.

Construire du JSON avec ArduinoJson

snprintf convient pour quelques champs, mais pour des données imbriquées ou l’analyse de commandes JSON, la bibliothèque ArduinoJson (version 7) est plus sûre :

cpp
#include <ArduinoJson.h>

void publishTelemetry() {
  JsonDocument doc;
  doc["device"] = deviceId;
  doc["temp"] = temperatureRead();
  doc["rssi"] = WiFi.RSSI();

  char payload[256];
  size_t n = serializeJson(doc, payload, sizeof(payload));
  mqtt.publish(topicTelemetry, (const uint8_t*)payload, n, false);
}

Gardez des payloads petits et plats : des clés courtes et des nombres plutôt que des chaînes économisent la RAM, le temps d’antenne et la bande passante du broker.

Dépannage des codes d’état

mqtt.state()SignificationSolution habituelle
-4Timeout de connexionVérifiez le keep-alive et que loop() s’exécute souvent
-2Échec de la connexion réseauMauvais hôte ou port, pare-feu, ou échec du handshake TLS
2Client ID refuséUtilisez un client ID plus court ou unique
4Nom d’utilisateur ou mot de passe incorrectVérifiez les identifiants, y compris la casse
5Non autoriséLes règles d’accès du broker refusent ce client

Ajouter TLS avec WiFiClientSecure

Le port 1883 en clair transmet vos identifiants et vos données sans chiffrement. Passer en TLS sur le port 8883 demande trois modifications : inclure WiFiClientSecure.h, remplacer le client réseau et lui fournir le certificat de la CA de votre broker :

Modifications pour TLS
#include <WiFiClientSecure.h>

// PEM of the root CA that signed your broker's certificate
static const char ROOT_CA[] PROGMEM = R"EOF(
-----BEGIN CERTIFICATE-----
...paste the root CA certificate here...
-----END CERTIFICATE-----
)EOF";

WiFiClientSecure net;
PubSubClient mqtt(net);

const uint16_t MQTT_PORT = 8883;

// in setup(), before mqtt.setServer():
net.setCACert(ROOT_CA);
// net.setInsecure();  // skips verification - local testing only!

De nombreux brokers publics utilisent des certificats Let’s Encrypt, dont la racine est ISRG Root X1 ; vérifiez la bonne racine dans la documentation de votre broker. Dans certaines configurations, TLS a aussi besoin de l’heure exacte pour vérifier la validité des certificats, et il consomme nettement plus de RAM — un ESP32 s’en sort très bien, un ESP8266 peine. Pour une vue d’ensemble, lisez nos bonnes pratiques de sécurité MQTT.

Vous voulez ce sketch avec l’hôte, le port et les identifiants de votre broker déjà renseignés ? Le générateur de code MQTT le produit pour vos propres paramètres de broker.

Questions fréquentes

Pourquoi PubSubClient ne publie-t-il pas de messages de plus de 256 octets ?

PubSubClient utilise par défaut un buffer de 256 octets, qui doit contenir tout le paquet MQTT, topic compris. publish() renvoie false quand le paquet ne tient pas. Appelez client.setBufferSize(1024) ou plus avant de vous connecter.

Pourquoi mon ESP32 se déconnecte-t-il sans cesse du broker MQTT ?

Les causes les plus courantes sont un client ID utilisé aussi par un autre appareil, de longs appels à delay() qui empêchent client.loop() de s’exécuter et font manquer les pings de keep-alive, et un WiFi faible. Utilisez un client ID unique par appareil et gardez loop() non bloquante.

PubSubClient prend-il en charge la QoS 1 et MQTT 5 ?

PubSubClient ne parle que MQTT 3.1.1. Il peut s’abonner en QoS 0 ou 1 mais ne publie qu’en QoS 0. Si vous avez besoin de publier en QoS 1 ou des fonctionnalités MQTT 5, tournez-vous vers des bibliothèques comme AsyncMqttClient ou le composant esp-mqtt d’ESP-IDF.