ESP32 et Arduino

Projet ESP32 PlatformIO MQTT : une structure propre avec des paramètres de broker par environnement

Structurez un projet ESP32 PlatformIO MQTT avec des environnements de développement et de production séparés, des paramètres de broker injectés à la compilation, des secrets hors du contrôle de version et des versions de bibliothèques figées.

VS Code avec PlatformIO affichant les environnements dev et production d'un projet ESP32 PlatformIO 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

Un projet ESP32 PlatformIO MQTT peut être bien plus qu'un simple sketch avec des identifiants codés en dur. Dans ce guide, vous allez configurer un projet PlatformIO avec un environnement de développement pointant vers broker.connectmq.com et un environnement de production utilisant TLS et des identifiants privés, avec des secrets tenus hors de Git et des versions de bibliothèques figées pour des builds reproductibles.

Pourquoi PlatformIO pour un firmware MQTT

L'IDE Arduino convient pour un premier sketch. Dès qu'un projet compte plus d'un fichier, plus d'une cible ou plus d'un développeur, PlatformIO devient rentable :

  • Builds reproductibles. Les versions de plateforme et de bibliothèques sont définies dans platformio.ini : un collègue ou un serveur de CI compile exactement ce que vous avez compilé.
  • Plusieurs environnements. Un même projet peut produire, à partir des mêmes sources, un firmware pour un broker de test et un autre pour un broker de production, ou pour différentes cartes.
  • Une vraie structure de projet. Les en-têtes dans include/, le code réparti en plusieurs fichiers dans src/, les modules réutilisables dans lib/ et les tests dans test/.
  • Builds en ligne de commande. pio run fonctionne de la même façon dans VS Code, dans un terminal et dans un pipeline de CI.

Organisation du projet

Créez un nouveau projet pour un ESP32 DevKit avec le framework Arduino, puis organisez-le ainsi :

esp32-mqtt/
  platformio.ini
  secrets.ini            (not committed)
  secrets.example.ini    (committed template)
  .gitignore
  include/
    config.h
  src/
    main.cpp
    mqtt_link.cpp
    mqtt_link.h

Le .gitignore tient les secrets locaux et les fichiers de build hors du dépôt :

.pio/
secrets.ini

Le platformio.ini avec environnements dev et production

[platformio]
default_envs = dev
extra_configs = secrets.ini

[env]
platform = espressif32
board = esp32dev
framework = arduino
monitor_speed = 115200
monitor_filters = esp32_exception_decoder
lib_deps =
    knolleary/PubSubClient @ ^2.8
    bblanchon/ArduinoJson @ ^7.0.0
build_flags =
    -D WIFI_SSID=\"${secrets.wifi_ssid}\"
    -D WIFI_PASS=\"${secrets.wifi_pass}\"
    -D FW_VERSION=\"1.0.0\"

[env:dev]
build_flags =
    ${env.build_flags}
    -D MQTT_HOST=\"broker.connectmq.com\"
    -D MQTT_PORT=1883
    -D MQTT_USE_TLS=0
    -D TOPIC_PREFIX=\"connectmq/demo/6d1f3c\"
    -D CORE_DEBUG_LEVEL=3

[env:prod]
build_flags =
    ${env.build_flags}
    -D MQTT_HOST=\"broker.connectmq.com\"
    -D MQTT_PORT=8883
    -D MQTT_USE_TLS=1
    -D MQTT_CLIENT_ID=\"${secrets.mqtt_client_id}\"
    -D MQTT_USER=\"${secrets.mqtt_user}\"
    -D MQTT_PASS=\"${secrets.mqtt_pass}\"
    -D TOPIC_PREFIX=\"factory1/line1/sensor01\"
    -D CORE_DEBUG_LEVEL=1

La section partagée [env] regroupe tout ce qui est commun. Chaque environnement nommé étend build_flags avec ses propres paramètres de broker. extra_configs = secrets.ini fusionne un second fichier dans la configuration, de sorte que ${secrets.wifi_ssid} se résout en une valeur qui n'entre jamais dans le contrôle de version.

Le fichier secrets.ini :

[secrets]
wifi_ssid = YOUR_WIFI
wifi_pass = YOUR_WIFI_PASSWORD
mqtt_client_id = YOUR_CLIENT_ID
mqtt_user = YOUR_USERNAME
mqtt_pass = YOUR_PASSWORD

Commitez un secrets.example.ini avec les mêmes clés et des valeurs fictives pour que les nouveaux contributeurs sachent quoi renseigner. Les valeurs de production proviennent du portail ConnectMQ, où chaque broker reçoit un client ID, un nom d'utilisateur et un mot de passe générés.

En CI, vous pouvez vous passer du fichier et lire des variables d'environnement avec ${sysenv.MQTT_PASS}. Gardez à l'esprit que les build flags se retrouvent sous forme de chaînes dans le binaire : toute personne disposant du fichier de firmware peut les extraire. Pour des appareils distribués à des tiers, préférez des identifiants propres à chaque appareil, provisionnés à l'exécution, par exemple avec le portail ESP32 WiFiManager MQTT.

config.h : un seul endroit pour les valeurs par défaut

L'en-tête attribue à chaque paramètre une valeur par défaut sûre et fait échouer la compilation s'il manque un élément essentiel :

#pragma once

#ifndef MQTT_HOST
#define MQTT_HOST "broker.connectmq.com"
#endif

#ifndef MQTT_PORT
#define MQTT_PORT 1883
#endif

#ifndef MQTT_USE_TLS
#define MQTT_USE_TLS 0
#endif

#ifndef TOPIC_PREFIX
#error "TOPIC_PREFIX must be set in platformio.ini"
#endif

#if MQTT_USE_TLS && !defined(MQTT_PASS)
#error "Production builds need MQTT credentials from secrets.ini"
#endif

#ifndef FW_VERSION
#define FW_VERSION "dev"
#endif

Un #error vaut bien mieux qu'un firmware qui compile, part en production puis se connecte discrètement au mauvais endroit.

Le module MQTT

Isoler la logique MQTT dans son propre fichier permet de garder main.cpp court et de réutiliser le module d'un projet à l'autre.

src/mqtt_link.h :

#pragma once
#include <Arduino.h>

namespace mqtt_link {
void begin();
void loop();
bool publish(const char* subtopic, const char* payload, bool retain = false);
bool connected();
}

src/mqtt_link.cpp :

#include "mqtt_link.h"
#include "config.h"
#include <WiFi.h>
#include <PubSubClient.h>
#if MQTT_USE_TLS
#include <WiFiClientSecure.h>
#include "isrg_root_x1.h"   // const char ISRG_ROOT_X1[] = R"EOF(...PEM...)EOF";
#endif

namespace {
#if MQTT_USE_TLS
WiFiClientSecure net;
#else
WiFiClient net;
#endif
PubSubClient client(net);
String clientId;
String statusTopic;
unsigned long nextAttempt = 0;
unsigned long backoffMs = 1000;

void onMessage(char* topic, byte* payload, unsigned int length) {
  Serial.printf("[mqtt] %s: %.*s\n", topic, (int)length, (const char*)payload);
}

void tryConnect() {
  bool ok;
#if MQTT_USE_TLS
  ok = client.connect(MQTT_CLIENT_ID, MQTT_USER, MQTT_PASS,
                      statusTopic.c_str(), 1, true, "offline");
#else
  ok = client.connect(clientId.c_str(), statusTopic.c_str(), 1, true, "offline");
#endif
  if (ok) {
    client.publish(statusTopic.c_str(), "online", true);
    client.subscribe((String(TOPIC_PREFIX) + "/cmd").c_str());
    backoffMs = 1000;
    Serial.println("[mqtt] connected");
  } else {
    Serial.printf("[mqtt] failed, state=%d\n", client.state());
    nextAttempt = millis() + backoffMs;
    backoffMs = min(backoffMs * 2, 60000UL);
  }
}
}  // namespace

namespace mqtt_link {

void begin() {
#if MQTT_USE_TLS
  net.setCACert(ISRG_ROOT_X1);
  clientId = MQTT_CLIENT_ID;
#else
  clientId = "esp32-pio-" + String((uint32_t)ESP.getEfuseMac(), HEX);
#endif
  statusTopic = String(TOPIC_PREFIX) + "/status";
  client.setServer(MQTT_HOST, MQTT_PORT);
  client.setBufferSize(512);
  client.setSocketTimeout(5);
  client.setCallback(onMessage);
}

void loop() {
  if (WiFi.status() != WL_CONNECTED) return;
  if (client.connected()) {
    client.loop();
  } else if (millis() >= nextAttempt) {
    tryConnect();
  }
}

bool publish(const char* subtopic, const char* payload, bool retain) {
  String topic = String(TOPIC_PREFIX) + "/" + subtopic;
  return client.publish(topic.c_str(), payload, retain);
}

bool connected() { return client.connected(); }

}  // namespace mqtt_link

Créez include/isrg_root_x1.h contenant le certificat ISRG Root X1 de Let's Encrypt sous forme de chaîne PEM. Il n'est compilé que dans les builds de production.

src/main.cpp :

#include <Arduino.h>
#include <WiFi.h>
#include <ArduinoJson.h>
#include "config.h"
#include "mqtt_link.h"

unsigned long lastPublish = 0;

void setup() {
  Serial.begin(115200);
  Serial.printf("Firmware %s, broker %s:%d\n", FW_VERSION, MQTT_HOST, MQTT_PORT);

  WiFi.mode(WIFI_STA);
  WiFi.setAutoReconnect(true);
  WiFi.begin(WIFI_SSID, WIFI_PASS);

  mqtt_link::begin();
}

void loop() {
  mqtt_link::loop();

  if (mqtt_link::connected() && millis() - lastPublish > 10000) {
    lastPublish = millis();
    JsonDocument doc;
    doc["fw"] = FW_VERSION;
    doc["uptime"] = millis() / 1000;
    doc["rssi"] = WiFi.RSSI();
    char buf[128];
    serializeJson(doc, buf, sizeof(buf));
    mqtt_link::publish("telemetry", buf);
  }
}

Compiler, flasher et surveiller

# build de développement vers la zone de test publique
pio run -e dev -t upload
pio device monitor -e dev

# build de production avec TLS et identifiants du portail
pio run -e prod -t upload

# compiler tous les environnements, comme le ferait la CI
pio run

Pendant l'exécution du build dev, observez ses topics depuis votre ordinateur :

mosquitto_sub -h broker.connectmq.com -p 1883 -i pc-6d1f3c -t 'connectmq/demo/6d1f3c/#' -v

Les paramètres d'environnement en un coup d'œil

Paramètre dev prod
Broker Zone publique de broker.connectmq.com Votre broker ConnectMQ privé
Port et transport 1883, TCP en clair 8883, TLS avec ISRG Root X1
Client ID Dérivé de l'adresse MAC Client ID émis par le portail
Identifiants Aucun Nom d'utilisateur et mot de passe issus de secrets.ini
Préfixe de topic connectmq/demo/<random> Votre propre hiérarchie, par exemple un chemin ISA-95
Niveau de log Info Erreurs uniquement

Pourquoi cette structure est rentable

Découper le code ainsi n'est pas une affaire de rangement pour le plaisir. Chaque élément a un rôle qui fait une vraie différence dès que le projet quitte votre bureau.

Les environnements traduisent l'intention. Personne n'a besoin de penser à changer un port, à remplacer WiFiClient par WiFiClientSecure et à coller des identifiants avant une release. Choisir prod fait tout cela de manière cohérente, et les vérifications #error de config.h arrêtent un build à moitié configuré avant qu'il n'atteigne un appareil. Le principe s'étend naturellement : ajoutez un environnement staging pointant vers un second broker privé, ou un environnement par type de carte avec une ligne board = différente.

Le module MQTT masque le transport. main.cpp ne sait jamais s'il communique en TCP en clair ou en TLS, ni quel client ID est utilisé. Lorsque vous remplacerez plus tard PubSubClient par un client asynchrone, ou déplacerez les topics vers une nouvelle hiérarchie, la modification restera confinée à un seul fichier. Le module gère aussi le backoff de reconnexion : chaque projet qui le réutilise bénéficie gratuitement d'une reconnexion non bloquante.

Le topic de statut et le Last Will sont intégrés. Comme tryConnect() enregistre toujours un will offline en retained message et publie online après la connexion, chaque appareil construit à partir de ce modèle signale sa disponibilité sans code supplémentaire.

Des dépendances figées donnent du sens à la CI. Un pipeline qui exécute pio run à chaque commit ne prouve quelque chose que s'il compile avec les mêmes versions de bibliothèques que les développeurs. Avec des plages caret comme ^2.8, vous recevez les correctifs mais pas les versions majeures incompatibles.

Si vous souhaitez des vérifications automatisées au-delà de la compilation, le lanceur de tests unitaires de PlatformIO peut exécuter des tests sur la carte elle-même ou sur l'hôte avec un environnement native. La logique pure, comme la construction de topics ou le formatage de payloads, se teste facilement en natif une fois isolée dans son propre module.

Pièges courants avec PlatformIO

  1. Macros de chaîne sans guillemets échappés. -D MQTT_HOST=broker.connectmq.com définit un simple jeton, pas une chaîne, et la compilation échoue. Utilisez \"...\" comme indiqué.
  2. Écraser les flags au lieu de les étendre. Écrire build_flags = dans un environnement sans ${env.build_flags} supprime les flags partagés, y compris les identifiants Wi-Fi.
  3. Bibliothèques non figées. Un nom de bibliothèque seul récupère la version la plus récente, et une mise à jour majeure comme le passage d'ArduinoJson 6 à 7 peut casser votre build du jour au lendemain. Figez avec @ ^x.y.z.
  4. secrets.ini absent. Les références à une section non définie échouent de manière déroutante. Le fichier d'exemple commité évite ce problème.
  5. Builds obsolètes après modification des flags. PlatformIO recompile généralement quand les flags changent, mais si le comportement semble anormal, lancez pio run -t clean.
  6. Décodage des plantages. Conservez monitor_filters = esp32_exception_decoder pour que les backtraces indiquent le fichier et la ligne, ce qui est précieux pour des problèmes comme les réinitialisations du task watchdog de l'ESP32.

Étapes suivantes

Une fois cette structure propre en place, ajouter des fonctionnalités devient simple. Placez la gestion du JSON dans son propre module en suivant le guide ESP32 ArduinoJson MQTT, ajoutez la livraison de firmware à distance avec les mises à jour OTA de l'ESP32 via MQTT, et consultez le tutoriel IoT ESP32 pour le projet complet de l'appareil au tableau de bord. Pour comprendre pourquoi les brokers dev et prod se comportent différemment, lisez l'article sur le serveur MQTT public gratuit et ses limites.

Livrez votre firmware PlatformIO avec ConnectMQ

L'environnement dev fonctionne immédiatement avec le serveur MQTT public gratuit. Pour l'environnement prod, créez un broker privé sur ConnectMQ, un broker MQTT managé avec MQTT 3.1.1 et 5.0, TLS sur le port 8883 et un certificat Let's Encrypt. Inscrivez-vous à l'offre gratuite Serverless sur app.connectmq.com, sans carte bancaire, copiez les identifiants générés dans secrets.ini et lancez pio run -e prod -t upload.

Questions fréquentes

Comment garder les mots de passe MQTT hors de Git avec PlatformIO ?

Placez-les dans un fichier secrets.ini listé dans .gitignore et fusionné via extra_configs, ou lisez-les depuis des variables d'environnement avec ${sysenv.NAME} en CI.

Un même projet PlatformIO peut-il compiler pour plusieurs brokers ?

Oui. Définissez un environnement par broker ou par étape de déploiement, chacun étendant les build_flags partagés avec son propre hôte, son port et ses identifiants, puis sélectionnez-le avec pio run -e <name>.

Les identifiants passés en build flags sont-ils sûrs dans le firmware ?

Ils sont compilés dans le binaire sous forme de chaînes en clair : toute personne disposant du fichier peut les extraire. Pour les appareils que vous remettez à d'autres, provisionnez les identifiants à l'exécution et stockez-les plutôt dans la NVS.

Quelles bibliothèques utiliser pour MQTT avec PlatformIO ?

PubSubClient est le client synchrone le plus répandu, et AsyncMqttClient est une alternative événementielle. Ajoutez l'un ou l'autre à lib_deps avec une version figée.

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.