ESP32 et Arduino

ESP32 ArduinoJson MQTT : envoyer et analyser des payloads JSON correctement

Sérialisez des mesures de capteurs et analysez des commandes JSON entrantes avec ArduinoJson 7 sur un ESP32, publiez-les en MQTT avec PubSubClient et évitez les pièges de tampon et de mémoire qui font disparaître des messages sans bruit.

Carte ESP32 publiant un document JSON construit avec ArduinoJson dans un tutoriel ESP32 ArduinoJson MQTT
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

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_pub et mosquitto_sub sur 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 :

  1. Vérifiez l'erreur. DeserializationError indique 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é.
  2. Utilisez des valeurs par défaut avec l'opérateur |. doc["value"] | 0 renvoie 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.
  3. 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":1 si 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.

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.