Associer ESP32, ArduinoJson et MQTT est la façon la plus courante d'envoyer des données de capteurs structurées depuis un microcontrôleur. Dans ce tutoriel, vous allez construire une télémétrie JSON avec ArduinoJson 7, la publier efficacement avec PubSubClient, analyser des commandes JSON dans le callback de message et découvrir les limites de tampon qui font disparaître des messages sans la moindre erreur.
Pourquoi des payloads JSON pour MQTT
MQTT se moque de ce que contient un payload ; il transporte des octets. Cette liberté vous oblige à choisir un format. JSON s'impose pour la plupart des projets ESP32, car tous les tableaux de bord, flux Node-RED, scripts Python et outils d'import en base de données savent le lire sans décodeur spécifique. Un message comme {"temp":22.4,"hum":51,"rssi":-63} se décrit tout seul, s'enrichit facilement de nouveaux champs et reste lisible dans n'importe quel client MQTT pendant le débogage.
Les coûts sont la taille et le travail d'analyse, tous deux négligeables aux cadences habituelles des capteurs sur un ESP32. Si vous envoyez des centaines de messages par seconde ou si votre budget batterie est serré, des clés compactes aident davantage qu'un changement de format.
ArduinoJson est la bibliothèque de référence pour cela. La version 7 supprime la nécessité de choisir la capacité d'un document : JsonDocument grandit selon les besoins sur le heap. Tout le code ci-dessous cible ArduinoJson 7 ; si vous êtes en version 6, remplacez JsonDocument par StaticJsonDocument<N> ou DynamicJsonDocument. La documentation officielle d'ArduinoJson détaille la migration.
Ce dont vous avez besoin
- Une carte ESP32 et l'Arduino IDE ou PlatformIO.
- Les bibliothèques PubSubClient de Nick O'Leary et ArduinoJson de Benoit Blanchon (7.x).
mosquitto_pubetmosquitto_subsur votre ordinateur pour envoyer des commandes et observer la sortie.
Les exemples utilisent la zone publique de broker.connectmq.com, le serveur MQTT public gratuit, sur le port 1883 avec le préfixe de topic connectmq/demo/a8e2d1. Remplacez ce préfixe par votre propre chaîne aléatoire, et n'oubliez pas que les topics publics sont lisibles par tous.
| Topic | Sens | Payload |
|---|---|---|
connectmq/demo/a8e2d1/telemetry |
L'ESP32 publie | {"device":"...","temp":22.4,"hum":51,"uptime":120} |
connectmq/demo/a8e2d1/cmd |
L'ESP32 s'abonne | {"action":"set_interval","value":10} |
connectmq/demo/a8e2d1/ack |
L'ESP32 publie | {"ok":true,"action":"set_interval"} |
Le sketch ESP32 ArduinoJson MQTT complet
Le sketch publie la télémétrie selon un intervalle configurable et accepte des commandes JSON pour modifier cet intervalle ou allumer et éteindre une LED.
#include <WiFi.h>
#include <PubSubClient.h>
#include <ArduinoJson.h>
const char* WIFI_SSID = "YOUR_WIFI";
const char* WIFI_PASS = "YOUR_WIFI_PASSWORD";
const char* TOPIC_TELEMETRY = "connectmq/demo/a8e2d1/telemetry";
const char* TOPIC_CMD = "connectmq/demo/a8e2d1/cmd";
const char* TOPIC_ACK = "connectmq/demo/a8e2d1/ack";
const int LED_PIN = 2;
WiFiClient net;
PubSubClient mqtt(net);
String deviceId;
unsigned long intervalMs = 5000;
unsigned long lastPublish = 0;
void publishAck(const char* action, bool ok, const char* error = nullptr) {
JsonDocument doc;
doc["ok"] = ok;
doc["action"] = action;
if (error) doc["error"] = error;
char buf[128];
size_t n = serializeJson(doc, buf, sizeof(buf));
mqtt.publish(TOPIC_ACK, (const uint8_t*)buf, n, false);
}
void onMessage(char* topic, byte* payload, unsigned int length) {
JsonDocument doc;
DeserializationError err = deserializeJson(doc, payload, length);
if (err) {
publishAck("unknown", false, err.c_str());
return;
}
const char* action = doc["action"] | "";
if (strcmp(action, "set_interval") == 0) {
int seconds = doc["value"] | 0;
if (seconds < 1 || seconds > 3600) {
publishAck(action, false, "value out of range");
return;
}
intervalMs = seconds * 1000UL;
publishAck(action, true);
} else if (strcmp(action, "led") == 0) {
bool on = doc["value"] | false;
digitalWrite(LED_PIN, on ? HIGH : LOW);
publishAck(action, true);
} else {
publishAck(action, false, "unknown action");
}
}
void publishTelemetry() {
JsonDocument doc;
doc["device"] = deviceId;
doc["temp"] = serialized(String(20.0 + random(0, 50) / 10.0, 1));
doc["hum"] = 45 + random(0, 15);
doc["uptime"] = millis() / 1000;
JsonObject network = doc["net"].to<JsonObject>();
network["rssi"] = WiFi.RSSI();
network["ip"] = WiFi.localIP().toString();
size_t len = measureJson(doc);
mqtt.beginPublish(TOPIC_TELEMETRY, len, false);
serializeJson(doc, mqtt); // écriture en flux directement dans le paquet MQTT
mqtt.endPublish();
}
void connectMqtt() {
while (!mqtt.connected()) {
if (mqtt.connect(deviceId.c_str())) {
mqtt.subscribe(TOPIC_CMD);
} else {
Serial.printf("MQTT failed, state=%d\n", mqtt.state());
delay(3000);
}
}
}
void setup() {
Serial.begin(115200);
pinMode(LED_PIN, OUTPUT);
WiFi.mode(WIFI_STA);
WiFi.begin(WIFI_SSID, WIFI_PASS);
while (WiFi.status() != WL_CONNECTED) delay(250);
deviceId = "esp32-json-" + String((uint32_t)ESP.getEfuseMac(), HEX);
mqtt.setServer("broker.connectmq.com", 1883);
mqtt.setBufferSize(1024); // la valeur par défaut, 256 octets, est trop petite pour la plupart des JSON
mqtt.setCallback(onMessage);
}
void loop() {
if (!mqtt.connected()) connectMqtt();
mqtt.loop();
if (millis() - lastPublish >= intervalMs) {
lastPublish = millis();
publishTelemetry();
}
}
Observez la sortie et envoyez des commandes depuis votre ordinateur :
mosquitto_sub -h broker.connectmq.com -p 1883 -i pc-a8e2d1 -t 'connectmq/demo/a8e2d1/#' -v
mosquitto_pub -h broker.connectmq.com -p 1883 -i pc-a8e2d1-pub \
-t connectmq/demo/a8e2d1/cmd -m '{"action":"set_interval","value":10}'
mosquitto_pub -h broker.connectmq.com -p 1883 -i pc-a8e2d1-pub \
-t connectmq/demo/a8e2d1/cmd -m '{"action":"led","value":true}'
Sérialisation : trois façons de mettre du JSON dans une publication
Dans un tampon fixe
serializeJson(doc, buf, sizeof(buf)) écrit dans un tableau de caractères et renvoie le nombre d'octets écrits. C'est la méthode la plus simple, et elle convient bien aux petits messages prévisibles comme l'acquittement ci-dessus. Passez la longueur renvoyée à publish(), en utilisant la surcharge qui prend un pointeur d'octets et une longueur, plutôt que de compter sur strlen().
En flux dans le client MQTT
PubSubClient hérite de la classe Print d'Arduino : ArduinoJson peut donc écrire directement dedans. measureJson(doc) donne d'abord la taille exacte, beginPublish() écrit l'en-tête MQTT avec cette taille, serializeJson(doc, mqtt) envoie le corps en flux et endPublish() termine le paquet. Aucun tampon intermédiaire n'est nécessaire : c'est l'option la plus économe en mémoire pour les documents volumineux. Vous avez tout de même besoin de setBufferSize() pour les appels publish() classiques et pour les messages entrants, qui sont toujours lus dans le tampon.
Dans une String
serializeJson(doc, someString) est pratique, mais alloue sur le heap à chaque message. Sur un appareil qui tourne pendant des semaines, des allocations répétées de String peuvent fragmenter la mémoire. Préférez les deux premières options dans un firmware destiné à fonctionner longtemps.
Maîtriser la précision des flottants
ArduinoJson affiche les flottants avec assez de chiffres pour qu'ils restent exacts à la relecture, ce qui peut donner des valeurs comme 22.399999. Envelopper une chaîne formatée dans serialized() l'insère telle quelle, comme dans la fonction de télémétrie ci-dessus. Vous pouvez aussi arrondir le nombre avant de l'affecter : doc["temp"] = round(t * 10) / 10.0;.
Analyser les commandes JSON en toute sécurité
Le callback MQTT reçoit un pointeur d'octets et une longueur, et le payload n'est pas terminé par un caractère nul. Passez toujours la longueur : deserializeJson(doc, payload, length). L'appeler avec seulement (char*)payload lit au-delà de la fin du message.
Trois autres habitudes rendent l'analyse robuste :
- Vérifiez l'erreur.
DeserializationErrorindique si l'entrée était vide, incomplète, invalide ou trop profonde. Renvoyez-la sur un topic d'acquittement, comme le fait le sketch, pour que l'expéditeur sache que la commande a échoué. - Utilisez des valeurs par défaut avec l'opérateur
|.doc["value"] | 0renvoie 0 si le champ est absent ou d'un mauvais type : une commande mal formée ne peut donc ni faire planter l'appareil ni laisser une variable non initialisée. - Validez les plages. Une analyse JSON réussie ne signifie pas que les valeurs ont un sens. Rejetez explicitement les intervalles de zéro seconde ou les luminosités de 5000.
Si vous n'avez besoin que de quelques champs d'un gros document entrant, utilisez un filtre pour qu'ArduinoJson ignore le reste :
JsonDocument filter;
filter["action"] = true;
filter["value"] = true;
JsonDocument doc;
deserializeJson(doc, payload, length, DeserializationOption::Filter(filter));
Pièges de tampon et de mémoire
| Symptôme | Cause | Solution |
|---|---|---|
publish() renvoie false, rien n'arrive |
Paquet plus grand que le tampon par défaut de 256 octets de PubSubClient | mqtt.setBufferSize(1024) avant connect() |
| Le JSON entrant n'atteint jamais le callback | Message entrant plus grand que le tampon | Idem : agrandir le tampon pour contenir la plus grosse commande attendue |
Erreurs IncompleteInput |
Analyse avec strlen() sur un payload non terminé |
Passer length à deserializeJson() |
| Redémarrages aléatoires au bout de plusieurs jours | Fragmentation du heap due à la sérialisation en String | Sérialiser dans des tampons fixes ou en flux dans le client |
| Valeurs affichées avec de longues décimales | Formatage par défaut des flottants | serialized() ou arrondi avant affectation |
Gardez aussi en tête les limites du broker. La zone publique de broker.connectmq.com accepte jusqu'à 64 Ko par message, et l'offre gratuite ConnectMQ Serverless jusqu'à 256 Ko, bien au-delà de ce qu'un sketch ESP32 devrait envoyer en un seul message. Si votre JSON devient volumineux, répartissez-le sur des topics comme .../telemetry et .../diagnostics. Le guide sur la taille du tampon PubSubClient détaille le calcul du tampon.
Concevoir le schéma JSON
Quelques conventions évitent bien des soucis dès que plusieurs appareils et plusieurs consommateurs entrent en jeu :
- Incluez un identifiant d'appareil ou placez-le dans le topic, mais pas les deux de manière incohérente.
- Indiquez les unités dans les noms de clés (
temp_c,power_w) ou documentez-les une fois pour toutes ; ne mélangez pas les unités d'un appareil à l'autre. - N'ajoutez un horodatage que si l'appareil a synchronisé son heure. Sinon, laissez le consommateur horodater l'arrivée ; consultez ajouter des horodatages NTP aux messages MQTT ESP32.
- Gardez commandes et télémétrie sur des topics séparés pour qu'un appareil n'analyse jamais ses propres messages.
- Versionnez le payload avec un champ comme
"v":1si le format est susceptible d'évoluer.
L'utiliser avec de vrais capteurs
Remplacez les valeurs aléatoires par de vraies mesures et le reste du sketch reste identique. Le tutoriel ESP32 DHT22 MQTT montre la partie capteur, et le tutoriel IoT ESP32 assemble un projet d'appareil complet autour du même schéma JSON.
Envoyez votre télémétrie JSON vers ConnectMQ
Tester avec le serveur MQTT public gratuit est le moyen le plus rapide d'itérer sur un format de payload. Quand les données comptent, passez à un broker privé : ConnectMQ est un broker MQTT managé dont le portail affiche vos messages JSON en direct dans l'explorateur de topics et vous permet de créer des règles d'alerte à seuil sur les champs numériques. Inscrivez-vous à l'offre gratuite Serverless sur app.connectmq.com, sans carte bancaire, puis basculez le sketch sur WiFiClientSecure sur le port 8883 avec YOUR_CLIENT_ID, YOUR_USERNAME et YOUR_PASSWORD.
Questions fréquentes
Pourquoi PubSubClient n'arrive-t-il pas à publier mon JSON ?
Le tampon par défaut fait 256 octets, topic et en-tête compris. Appelez mqtt.setBufferSize() avec une valeur plus grande avant la connexion, et vérifiez la valeur de retour de publish().
Faut-il utiliser ArduinoJson 6 ou 7 ?
Utilisez la version 7 pour les nouveaux projets. Elle remplace les documents à capacité fixe par un JsonDocument unique qui se dimensionne tout seul, ce qui élimine une source fréquente de sorties tronquées.
Le JSON est-il trop lourd pour un ESP32 en MQTT ?
Non. Aux cadences habituelles des capteurs, le coût d'analyse et de sérialisation est minime. Gardez des clés courtes et des payloads ciblés si vous êtes sur batterie ou si vous envoyez de nombreux messages par seconde.
Comment analyser le payload s'il n'est pas terminé par un caractère nul ?
Passez la longueur à l'analyseur : deserializeJson(doc, payload, length). ArduinoJson lit alors exactement les octets du message.



