Ce tutoriel ESP32 MQTT vous fait passer d'un sketch Arduino vide à une carte qui publie de la télémétrie JSON, écoute des commandes et allume ou éteint sa LED intégrée. Vous vous connecterez d'abord à la zone publique gratuite de broker.connectmq.com, puis vous ferez évoluer le même sketch vers le TLS sur le port 8883 avec les identifiants du portail ConnectMQ. Chaque étape est vérifiée depuis votre ordinateur avec mosquitto_sub et mosquitto_pub.
Ce que vous allez construire
L'appareil final fait trois choses :
- Il se connecte au Wi-Fi et à un broker MQTT, avec reconnexion automatique.
- Il publie toutes les cinq secondes un message JSON tel que
{"temperature":23.41,"uptime":125}. - Il s'abonne à un topic de commande et allume ou éteint la LED intégrée lorsqu'il reçoit
ON,OFFouTOGGLE.
L'organisation des topics regroupe tout sous un préfixe unique :
| Topic | Sens | Exemple de payload |
|---|---|---|
connectmq/demo/<deviceId>/telemetry |
L'ESP32 publie | {"temperature":23.41,"uptime":125} |
connectmq/demo/<deviceId>/led/set |
L'ESP32 s'abonne | ON, OFF, TOGGLE |
connectmq/demo/<deviceId>/led/state |
L'ESP32 publie (retenu) | ON ou OFF |
Si les termes MQTT comme topic, QoS ou retained message sont nouveaux pour vous, lisez d'abord Qu'est-ce qu'un broker MQTT ?.
Prérequis
- Une carte de développement ESP32 (n'importe quel ESP32 DevKit convient ; la plupart ont une LED bleue sur le GPIO 2).
- L'Arduino IDE 2.x avec le paquet de cartes esp32 installé via le gestionnaire de cartes.
- La bibliothèque PubSubClient de Nick O'Leary, installée via le gestionnaire de bibliothèques.
- Un réseau Wi-Fi 2,4 GHz (l'ESP32 ne prend pas en charge le 5 GHz).
- Facultatif : les clients en ligne de commande Mosquitto (
mosquitto_sub,mosquitto_pub) sur votre ordinateur pour les tests. - Facultatif : un capteur DHT22. Le sketch simule la température par défaut, aucun matériel supplémentaire n'est donc nécessaire.
Endpoints du broker pour l'ESP32 MQTT
broker.connectmq.com expose quatre listeners. L'ESP32 utilise les deux premiers ; les ports WebSocket sont destinés aux navigateurs.
| Protocole | Port | Usage dans ce tutoriel |
|---|---|---|
| MQTT (TCP) | 1883 | Partie 1, zone publique anonyme |
| MQTT sur TLS | 8883 | Partie 2, identifiants du portail |
| WebSocket | 8083 | Non utilisé (clients navigateur) |
| WebSocket sécurisé | 8084 | Non utilisé (clients navigateur) |
La zone publique ne nécessite aucun compte : connectez-vous sans nom d'utilisateur ni mot de passe, avec un client ID unique et non vide. Gardez à l'esprit que tout ce qui y est publié est lisible par n'importe qui : n'envoyez que des données de démonstration. La zone publique autorise 64 Ko par message, 10 messages par seconde et 10 abonnements par client, et jusqu'à 8 niveaux de topic, ce qui est largement suffisant pour ce projet. Consultez le guide du serveur MQTT public gratuit pour plus de détails.
Partie 1 : ESP32 MQTT en TCP simple (port 1883)
Générer un client ID unique à partir de l'adresse MAC
Les client IDs doivent être uniques : si deux clients se connectent avec le même ID, le broker déconnecte le plus ancien. Chaque ESP32 possède une adresse MAC programmée en usine, source idéale pour un ID stable et unique. Le sketch construit un identifiant du type esp32-a1b2c3d4e5f6 et le réutilise dans le préfixe des topics.
Le sketch complet
Créez un nouveau sketch, collez le code ci-dessous et renseignez le SSID et le mot de passe de votre Wi-Fi.
#include <WiFi.h>
#include <PubSubClient.h>
// ---- Wi-Fi ----
const char* WIFI_SSID = "YOUR_WIFI_SSID";
const char* WIFI_PASSWORD = "YOUR_WIFI_PASSWORD";
// ---- MQTT ----
const char* MQTT_HOST = "broker.connectmq.com";
const uint16_t MQTT_PORT = 1883;
const int LED_PIN = 2; // LED intégrée sur la plupart des ESP32 DevKit
const unsigned long PUBLISH_INTERVAL_MS = 5000;
WiFiClient netClient;
PubSubClient mqtt(netClient);
String deviceId;
String topicTelemetry, topicLedSet, topicLedState;
unsigned long lastPublish = 0;
unsigned long lastReconnectAttempt = 0;
bool ledOn = false;
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.print("\nWi-Fi connected, IP: ");
Serial.println(WiFi.localIP());
}
String makeDeviceId() {
uint8_t mac[6];
WiFi.macAddress(mac);
char buf[20];
snprintf(buf, sizeof(buf), "esp32-%02x%02x%02x%02x%02x%02x",
mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]);
return String(buf);
}
void publishLedState() {
mqtt.publish(topicLedState.c_str(), ledOn ? "ON" : "OFF", true); // retenu
}
void onMessage(char* topic, byte* payload, unsigned int length) {
String msg;
for (unsigned int i = 0; i < length; i++) msg += (char)payload[i];
msg.trim();
Serial.printf("Message on %s: %s\n", topic, msg.c_str());
if (String(topic) != topicLedSet) return;
if (msg.equalsIgnoreCase("ON")) ledOn = true;
else if (msg.equalsIgnoreCase("OFF")) ledOn = false;
else if (msg.equalsIgnoreCase("TOGGLE")) ledOn = !ledOn;
else return; // ignorer tout le reste
digitalWrite(LED_PIN, ledOn ? HIGH : LOW);
publishLedState();
}
bool connectMqtt() {
Serial.printf("Connecting to MQTT as %s ... ", deviceId.c_str());
if (mqtt.connect(deviceId.c_str())) { // sans nom d'utilisateur/mot de passe : zone publique
Serial.println("connected");
mqtt.subscribe(topicLedSet.c_str(), 1);
publishLedState();
return true;
}
Serial.printf("failed, state=%d\n", mqtt.state());
return false;
}
float readTemperature() {
// Capteur simulé : 22-25 °C avec un peu de bruit
return 23.5 + 1.5 * sin(millis() / 60000.0) + random(-20, 20) / 100.0;
}
void setup() {
Serial.begin(115200);
pinMode(LED_PIN, OUTPUT);
digitalWrite(LED_PIN, LOW);
connectWiFi();
deviceId = makeDeviceId();
String base = "connectmq/demo/" + deviceId;
topicTelemetry = base + "/telemetry";
topicLedSet = base + "/led/set";
topicLedState = base + "/led/state";
Serial.println("Topic prefix: " + base);
mqtt.setServer(MQTT_HOST, MQTT_PORT);
mqtt.setCallback(onMessage);
mqtt.setBufferSize(512); // 256 octets par défaut, voir plus bas
mqtt.setKeepAlive(30);
}
void loop() {
if (WiFi.status() != WL_CONNECTED) {
connectWiFi();
}
if (!mqtt.connected()) {
unsigned long now = millis();
if (now - lastReconnectAttempt > 5000) { // nouvelle tentative non bloquante toutes les 5 s
lastReconnectAttempt = now;
if (connectMqtt()) lastReconnectAttempt = 0;
}
return;
}
mqtt.loop(); // traiter les messages entrants et le keepalive
if (millis() - lastPublish >= PUBLISH_INTERVAL_MS) {
lastPublish = millis();
char json[96];
snprintf(json, sizeof(json), "{\"temperature\":%.2f,\"uptime\":%lu}",
readTemperature(), millis() / 1000);
mqtt.publish(topicTelemetry.c_str(), json);
Serial.printf("Published %s\n", json);
}
}
Sélectionnez votre carte et votre port, téléversez le sketch et ouvrez le moniteur série à 115200 bauds. Vous devriez voir l'adresse IP, le préfixe des topics et une ligne Published toutes les cinq secondes. Copiez l'identifiant de l'appareil depuis le préfixe des topics : vous en aurez besoin à l'étape suivante.
Fonctionnement de la logique de reconnexion
Le sketch ne reste jamais bloqué indéfiniment à attendre le broker. Si la connexion tombe, loop() réessaie toutes les cinq secondes pendant que le reste du programme continue de tourner. Deux détails comptent :
mqtt.loop()doit s'exécuter souvent. Il lit les paquets entrants et envoie les pings de keepalive ; si vous appelezdelay(10000)dans votre boucle, le broker considérera le client comme mort.- Après une reconnexion, le sketch se réabonne. Avec la clean session par défaut, les abonnements ne survivent pas à une déconnexion : se réabonner dans
connectMqtt()est donc indispensable.
mqtt.state() renvoie un code numérique en cas d'échec : -2 signale généralement un problème réseau (mauvais hôte, pas d'accès Internet), tandis que les valeurs positives proviennent du CONNACK du broker, par exemple 2 pour un identifiant refusé.
Utiliser un vrai capteur DHT22
Pour utiliser des données réelles, installez la « DHT sensor library » d'Adafruit, reliez la broche de données du capteur au GPIO 4 et remplacez readTemperature() :
#include <DHT.h>
DHT dht(4, DHT22); // appeler dht.begin() dans setup()
float readTemperature() {
float t = dht.readTemperature();
return isnan(t) ? -127.0 : t; // envoyer une valeur sentinelle plutôt que NaN
}
Vérifier avec mosquitto_sub et mosquitto_pub
Ouvrez un terminal et abonnez-vous à tout ce qui se trouve sous le préfixe de votre appareil. Remplacez esp32-a1b2c3d4e5f6 par l'identifiant affiché dans le moniteur série.
mosquitto_sub -h broker.connectmq.com -p 1883 -i laptop-watch-81f3 \
-t "connectmq/demo/esp32-a1b2c3d4e5f6/#" -v
La sortie ressemble à ceci :
connectmq/demo/esp32-a1b2c3d4e5f6/led/state OFF
connectmq/demo/esp32-a1b2c3d4e5f6/telemetry {"temperature":23.41,"uptime":125}
connectmq/demo/esp32-a1b2c3d4e5f6/telemetry {"temperature":23.47,"uptime":130}
Commandez maintenant la LED depuis un second terminal :
mosquitto_pub -h broker.connectmq.com -p 1883 -i laptop-cmd-81f3 \
-t "connectmq/demo/esp32-a1b2c3d4e5f6/led/set" -q 1 -m "TOGGLE"
La LED change d'état et l'abonné affiche le nouveau led/state retenu. Comme l'état est retenu, tout tableau de bord qui s'abonne plus tard reçoit immédiatement la valeur courante. L'abonnement utilise la QoS 1 pour que la commande soit acquittée par le broker ; Les niveaux de QoS MQTT expliqués détaille les compromis.
Vous pouvez aussi suivre les connexions et déconnexions de l'appareil : abonnez-vous à $SYS/broker/connection/esp32-a1b2c3d4e5f6/state et vous verrez 1 lorsqu'il se connecte et 0 lorsqu'il se déconnecte.
La taille du buffer de PubSubClient
PubSubClient alloue un buffer unique pour les paquets entrants et sortants, et il ne fait par défaut que 256 octets. Ce buffer doit contenir le paquet MQTT entier : en-tête fixe, nom du topic et payload. Un topic long associé à un document JSON de taille moyenne le dépasse rapidement ; dans ce cas, publish() renvoie silencieusement false et les messages entrants trop gros sont abandonnés.
Appelez setBufferSize() avant la connexion et vérifiez la valeur de retour de publish() :
| Taille du payload (approx.) | setBufferSize() conseillé |
|---|---|
| Jusqu'à ~150 octets | 256 (par défaut) |
| Jusqu'à ~400 octets | 512 |
| Jusqu'à ~1 800 octets | 2048 |
| Documents plus volumineux | Découpez le message ou n'envoyez que les champs modifiés |
Chaque octet supplémentaire est pris sur la RAM de l'ESP32 : dimensionnez donc le buffer selon vos messages réels plutôt que selon le maximum du broker.
Partie 2 : sécuriser l'ESP32 MQTT avec TLS sur le port 8883
La zone publique est parfaite pour apprendre, mais les appareils de production doivent utiliser TLS et leurs propres identifiants. Connectez-vous à https://app.connectmq.com, créez un broker et copiez le nom d'utilisateur, le mot de passe et le client ID générés. Les clients authentifiés arrivent dans votre propre tenant isolé et non dans la zone publique.
Sur les connexions authentifiées, les client IDs sont limités à l'ID émis et à ses variantes _suffix. Donnez à chaque appareil son propre suffixe, par exemple YOUR_CLIENT_ID_esp32a, et un autre à votre ordinateur portable.
Modifications du sketch
Seuls le client réseau, le port, les identifiants et les topics changent. Téléchargez le certificat ISRG Root X1 (isrgrootx1.pem) depuis Let's Encrypt ; la chaîne du certificat du broker remonte jusqu'à lui. Collez son contenu dans le sketch :
#include <WiFi.h>
#include <WiFiClientSecure.h>
#include <PubSubClient.h>
#include <time.h>
const char* MQTT_HOST = "broker.connectmq.com";
const uint16_t MQTT_PORT = 8883;
const char* MQTT_USER = "YOUR_USERNAME";
const char* MQTT_PASS = "YOUR_PASSWORD";
const char* MQTT_CLIENT_ID = "YOUR_CLIENT_ID_esp32a";
// ISRG Root X1 (collez le PEM complet issu de isrgrootx1.pem)
static const char ROOT_CA[] PROGMEM = R"EOF(
-----BEGIN CERTIFICATE-----
...paste the certificate body here...
-----END CERTIFICATE-----
)EOF";
WiFiClientSecure netClient;
PubSubClient mqtt(netClient);
void syncClock() {
configTime(0, 0, "pool.ntp.org", "time.nist.gov");
time_t now = time(nullptr);
while (now < 1700000000) { delay(250); now = time(nullptr); }
}
// dans setup(), après connectWiFi() :
// syncClock();
// netClient.setCACert(ROOT_CA);
// mqtt.setServer(MQTT_HOST, MQTT_PORT);
// base des topics : "plant1/" + String(MQTT_CLIENT_ID) au lieu de "connectmq/demo/..."
bool connectMqtt() {
if (mqtt.connect(MQTT_CLIENT_ID, MQTT_USER, MQTT_PASS)) {
mqtt.subscribe(topicLedSet.c_str(), 1);
publishLedState();
return true;
}
return false;
}
Quelques remarques :
setCACert()permet à l'ESP32 de vérifier qu'il dialogue bien avecbroker.connectmq.com. Synchroniser d'abord l'horloge via NTP permet à la pile TLS de contrôler les dates de validité du certificat.setInsecure()désactive entièrement la vérification. C'est pratique pour un test de cinq minutes, mais cela supprime la protection contre les attaques de l'homme du milieu : ne le livrez jamais.- TLS consomme sensiblement plus de RAM et la première négociation prend un moment. C'est normal sur l'ESP32.
- Gardez les identifiants hors de votre dépôt Git, par exemple dans un fichier
secrets.hséparé listé dans.gitignore. La checklist de sécurité MQTT traite du stockage des secrets sur les appareils et d'autres mesures de durcissement.
Vérifier la connexion sécurisée
Abonnez-vous avec vos propres identifiants et un autre suffixe. Sous Linux, vous pouvez utiliser le magasin de CA du système avec --capath /etc/ssl/certs ; sur n'importe quel OS, vous pouvez passer le certificat racine téléchargé :
mosquitto_sub -h broker.connectmq.com -p 8883 --cafile isrgrootx1.pem \
-u YOUR_USERNAME -P YOUR_PASSWORD -i YOUR_CLIENT_ID_laptop \
-t "plant1/#" -v
Envoyez une commande de la même façon :
mosquitto_pub -h broker.connectmq.com -p 8883 --cafile isrgrootx1.pem \
-u YOUR_USERNAME -P YOUR_PASSWORD -i YOUR_CLIENT_ID_cli \
-t "plant1/YOUR_CLIENT_ID_esp32a/led/set" -q 1 -m "ON"
Vous pouvez aussi ouvrir le client MQTT intégré au navigateur ou l'explorateur de topics en temps réel du portail ConnectMQ pour suivre la télémétrie sans rien installer.
Dépannage
| Symptôme | Cause probable | Solution |
|---|---|---|
state=-2 en boucle |
Pas d'accès Internet, mauvais hôte ou port bloqué | Vérifiez le Wi-Fi, essayez le port 1883, vérifiez le pare-feu |
state=2 |
Client ID refusé | Utilisez un ID unique et non vide ; avec des identifiants, utilisez l'ID émis ou une variante _suffix |
state=4 ou 5 |
Identifiants incorrects | Recopiez le nom d'utilisateur et le mot de passe depuis le portail |
| Les publications s'arrêtent sans erreur affichée | Message plus gros que le buffer | Augmentez setBufferSize(), vérifiez la valeur de retour de publish() |
| Déconnexions incessantes | mqtt.loop() pas appelé assez souvent, ou autre client avec le même ID |
Supprimez les longs appels à delay(), donnez à chaque client son propre ID |
| TLS échoue, le TCP simple fonctionne | Mauvaise CA, horloge non réglée | Utilisez ISRG Root X1, synchronisez l'heure via NTP avant de vous connecter |
Démarrer avec ConnectMQ
Vous disposez maintenant d'un ESP32 qui envoie de la télémétrie et reçoit des commandes via MQTT, d'abord sur la zone publique, puis en TLS avec ses propres identifiants. Quand vous êtes prêt à dépasser les données de démonstration, passez à un broker MQTT managé : créez un broker avec l'offre Serverless gratuite sur https://app.connectmq.com (sans carte bancaire) et utilisez le tableau de bord, la vue des appareils et les alertes à seuil du portail pour garder un œil sur votre flotte.
Questions fréquentes
Quelle bibliothèque MQTT utiliser sur l'ESP32 avec l'Arduino IDE ?
PubSubClient est le choix le plus répandu : elle est légère, stable et fonctionne aussi bien avec WiFiClient qu'avec WiFiClientSecure. Elle prend en charge MQTT 3.1.1 avec publication en QoS 0 et abonnements en QoS 0 ou 1, ce qui couvre la plupart des projets de capteurs.
Pourquoi la publication de mon ESP32 échoue-t-elle sans erreur ?
Le plus souvent, le paquet dépasse le buffer de 256 octets par défaut de PubSubClient. Appelez mqtt.setBufferSize(512) ou plus avant la connexion et vérifiez le booléen renvoyé par publish().
Ai-je besoin d'un compte pour tester l'ESP32 MQTT avec broker.connectmq.com ?
Non. Connectez-vous au port 1883 ou 8883 sans nom d'utilisateur et avec un client ID unique pour utiliser la zone publique. Pour des données réelles, créez un compte gratuit et utilisez les identifiants générés.
Puis-je utiliser setInsecure() avec WiFiClientSecure ?
Uniquement pour des tests rapides. Le trafic est chiffré, mais la vérification du serveur est ignorée : un attaquant pourrait se faire passer pour le broker. Utilisez setCACert() avec ISRG Root X1 dans tout ce que vous déployez.
Comment donner à plusieurs cartes ESP32 leur propre client ID sur un même broker ?
Ajoutez un suffixe au client ID émis dans le portail, par exemple YOUR_CLIENT_ID_esp32a et YOUR_CLIENT_ID_esp32b. ConnectMQ accepte l'ID émis et ses variantes _suffix sur les connexions authentifiées.



