ESP32 et Arduino

Mise à jour OTA ESP32 via MQTT : déclencher des mises à jour HTTPS et suivre leur statut

Déclenchez les mises à jour du firmware ESP32 par une commande MQTT, téléchargez le binaire en HTTPS avec HTTPUpdate, et publiez la progression et le résultat sur un topic de statut retenu pour toujours savoir quelle version tourne sur chaque appareil.

Appareils ESP32 recevant une commande de firmware dans un flux de mise à jour OTA ESP32 MQTT avec un serveur de téléchargement HTTPS
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

Une mise à jour OTA ESP32 via MQTT vous permet de mettre à niveau des appareils sur le terrain sans ouvrir le boîtier : vous publiez une commande, l'appareil télécharge le nouveau firmware en HTTPS et rend compte de sa progression sur un topic de statut. Ce tutoriel construit ce flux pas à pas avec la bibliothèque HTTPUpdate d'Arduino-ESP32 et présente les garde-fous indispensables avant de l'utiliser sur du vrai matériel.

Pourquoi combiner MQTT et HTTPS pour l'OTA

Il est tentant de pousser le binaire du firmware lui-même via MQTT, mais c'est rarement une bonne idée. Les images de firmware pèsent de quelques centaines de kilo-octets à quelques mégaoctets, de nombreux brokers et bibliothèques clientes plafonnent la taille des messages bien en dessous, et découper un binaire en morceaux implique d'écrire votre propre logique de réassemblage, d'ordonnancement et de somme de contrôle.

La répartition la plus propre consiste à laisser chaque protocole faire ce qu'il fait le mieux :

  • MQTT transporte les petits messages de contrôle : « mets-toi à jour en version 1.4.0 depuis cette URL », ainsi que les rapports de progression et de résultat. Les appareils sont déjà connectés : la commande arrive donc instantanément, même derrière un NAT.
  • HTTPS transporte le gros binaire depuis un serveur web ou un stockage objet, avec le TLS, la gestion des plages et la mise en cache déjà réglés.

L'appareil n'a jamais besoin d'un port entrant ouvert, et vous conservez sur le broker une trace en direct de chaque tentative de mise à jour.

La conception des topics

Topic Sens Payload Retenu
connectmq/demo/e47b10/<device>/ota/cmd L'opérateur publie {"version":"1.4.0","url":"https://..."} Non
connectmq/demo/e47b10/<device>/ota/status L'ESP32 publie {"state":"downloading","progress":42} Oui
connectmq/demo/e47b10/<device>/info L'ESP32 publie {"version":"1.3.2"} Oui

Retenir les topics de statut et d'info signifie que tout outil qui s'abonne plus tard, comme un tableau de bord ou un script qui vérifie un déploiement, voit immédiatement la version actuelle et le dernier résultat OTA de chaque appareil.

Prérequis

  • Un ESP32 avec au moins 4 Mo de flash et un schéma de partitions comportant deux emplacements d'application OTA. Le schéma Arduino par défaut les a ; « Huge APP (no OTA) » ne les a pas.
  • Le core Arduino-ESP32 2.x ou 3.x, qui inclut HTTPUpdate.
  • Les bibliothèques PubSubClient et ArduinoJson.
  • Un serveur HTTPS qui sert votre fichier .bin. N'importe quel hébergement de fichiers statiques convient.

Exportez le binaire avec Croquis > Exporter les binaires compilées dans l'Arduino IDE, ou récupérez .pio/build/<env>/firmware.bin dans PlatformIO.

Le sketch complet de mise à jour OTA ESP32 MQTT

#include <WiFi.h>
#include <WiFiClientSecure.h>
#include <HTTPUpdate.h>
#include <PubSubClient.h>
#include <ArduinoJson.h>

#define FW_VERSION "1.3.2"

const char* WIFI_SSID = "YOUR_WIFI";
const char* WIFI_PASS = "YOUR_WIFI_PASSWORD";
const char* ALLOWED_HOST = "https://firmware.example.com/";   // n'accepter que les URL sous ce préfixe

// CA racine de votre hébergeur de firmware au format PEM
const char* FIRMWARE_CA = R"EOF(
-----BEGIN CERTIFICATE-----
...paste the root CA of your download server here...
-----END CERTIFICATE-----
)EOF";

WiFiClient mqttNet;
PubSubClient mqtt(mqttNet);
String deviceId, topicCmd, topicStatus, topicInfo;

volatile bool otaRequested = false;
String otaUrl, otaVersion;
int lastReported = -1;

void publishStatus(const char* state, int progress = -1, const char* error = nullptr) {
  JsonDocument doc;
  doc["state"] = state;
  doc["from"] = FW_VERSION;
  if (otaVersion.length()) doc["to"] = otaVersion;
  if (progress >= 0) doc["progress"] = progress;
  if (error) doc["error"] = error;
  char buf[192];
  size_t n = serializeJson(doc, buf, sizeof(buf));
  mqtt.publish(topicStatus.c_str(), (const uint8_t*)buf, n, true);
}

void onMessage(char* topic, byte* payload, unsigned int length) {
  JsonDocument doc;
  if (deserializeJson(doc, payload, length)) return;
  String url = doc["url"] | "";
  String version = doc["version"] | "";
  if (!url.startsWith(ALLOWED_HOST) || version.length() == 0) {
    publishStatus("rejected", -1, "url or version invalid");
    return;
  }
  if (version == FW_VERSION) {
    publishStatus("up_to_date");
    return;
  }
  otaUrl = url;
  otaVersion = version;
  otaRequested = true;              // faire le travail dans loop(), pas ici
}

void onProgress(int current, int total) {
  int pct = total > 0 ? (current * 100) / total : 0;
  if (pct / 10 != lastReported / 10) {   // rapport tous les 10 %
    lastReported = pct;
    publishStatus("downloading", pct);
    mqtt.loop();
  }
}

void runOta() {
  otaRequested = false;
  lastReported = -1;
  publishStatus("starting");

  WiFiClientSecure https;
  https.setCACert(FIRMWARE_CA);

  httpUpdate.rebootOnUpdate(false);
  httpUpdate.setFollowRedirects(HTTPC_STRICT_FOLLOW_REDIRECTS);
  httpUpdate.onProgress(onProgress);

  t_httpUpdate_return result = httpUpdate.update(https, otaUrl);

  switch (result) {
    case HTTP_UPDATE_OK:
      publishStatus("success", 100);
      mqtt.loop();
      delay(500);
      ESP.restart();
      break;
    case HTTP_UPDATE_NO_UPDATES:
      publishStatus("no_update");
      break;
    case HTTP_UPDATE_FAILED:
    default:
      publishStatus("failed", -1, httpUpdate.getLastErrorString().c_str());
      break;
  }
}

void connectMqtt() {
  while (!mqtt.connected()) {
    if (mqtt.connect(deviceId.c_str(), topicStatus.c_str(), 1, true,
                     "{\"state\":\"offline\"}")) {
      mqtt.subscribe(topicCmd.c_str());
      String info = String("{\"version\":\"") + FW_VERSION + "\"}";
      mqtt.publish(topicInfo.c_str(), info.c_str(), true);
    } else {
      delay(3000);
    }
  }
}

void setup() {
  Serial.begin(115200);
  WiFi.mode(WIFI_STA);
  WiFi.begin(WIFI_SSID, WIFI_PASS);
  while (WiFi.status() != WL_CONNECTED) delay(250);

  deviceId = "esp32-ota-" + String((uint32_t)ESP.getEfuseMac(), HEX);
  String base = "connectmq/demo/e47b10/" + deviceId;
  topicCmd = base + "/ota/cmd";
  topicStatus = base + "/ota/status";
  topicInfo = base + "/info";

  mqtt.setServer("broker.connectmq.com", 1883);
  mqtt.setBufferSize(512);
  mqtt.setCallback(onMessage);
  Serial.printf("Device %s running %s\n", deviceId.c_str(), FW_VERSION);
}

void loop() {
  if (!mqtt.connected()) connectMqtt();
  mqtt.loop();
  if (otaRequested) runOta();
}

Après le premier flashage par USB, relevez l'identifiant de l'appareil sur le moniteur série et déclenchez une mise à jour :

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

mosquitto_pub -h broker.connectmq.com -p 1883 -i pc-e47b10-pub \
  -t connectmq/demo/e47b10/esp32-ota-1a2b3c4d/ota/cmd \
  -m '{"version":"1.4.0","url":"https://firmware.example.com/sensor-1.4.0.bin"}'

Vous verrez starting, une série de messages downloading, success, puis un message info avec la nouvelle version une fois que l'appareil aura redémarré et se sera reconnecté.

Comment les éléments s'articulent

Le callback se contente d'enregistrer la demande. httpUpdate.update() bloque pendant toute la durée du téléchargement. L'exécuter dans le callback MQTT bloquerait mqtt.loop() de l'intérieur : le sketch positionne donc un flag et laisse loop() lancer la mise à jour. Cela évite aussi à l'appareil de déclencher le task watchdog ; les resets du task watchdog ESP32 dans les projets MQTT explique pourquoi.

La progression maintient la connexion MQTT en vie. Pendant un long téléchargement, le callback de progression publie tous les 10 % et appelle mqtt.loop(), ce qui envoie aussi les pings de keepalive pour que le broker ne coupe pas la session en cours de route.

rebootOnUpdate(false) vous laisse le dernier mot. Par défaut, HTTPUpdate redémarre dès que l'image est écrite. Désactiver ce comportement permet à l'appareil de publier d'abord success, puis de redémarrer de lui-même.

Le Last Will signale les plantages. L'appel de connexion enregistre un message de will offline retenu sur le topic de statut : un appareil qui meurt en pleine mise à jour apparaît donc hors ligne plutôt que bloqué sur « downloading ». Le guide ESP32 MQTT Last Will détaille ce schéma.

Les garde-fous de sécurité à ne pas négliger

Les mises à jour à distance sont l'action la plus puissante que votre appareil puisse effectuer, ce qui fait de la commande de mise à jour la cible la plus attrayante. Avant que cela n'approche un vrai déploiement :

  1. Ne déclenchez jamais d'OTA depuis un topic public en production. La zone publique du serveur MQTT public gratuit de ConnectMQ est lisible et modifiable par tous, ce qui convient pour la démo avec une URL fictive, mais pas pour de vrais appareils. Utilisez un broker privé avec des identifiants.
  2. Mettez l'hôte de téléchargement sur liste blanche. Le sketch rejette toute URL qui ne commence pas par votre propre préfixe HTTPS : une commande malveillante ne peut donc pas diriger l'appareil vers un binaire arbitraire.
  3. Validez le certificat TLS. Chargez la CA racine de votre hébergeur de firmware avec setCACert(). setInsecure() permettrait à n'importe qui sur le chemin réseau de substituer l'image.
  4. Signez vos images. Pour des garanties plus fortes, ESP-IDF prend en charge le secure boot et la vérification des applications signées : le bootloader rejette alors tout firmware que vous n'avez pas signé. Cela nécessite une compilation ESP-IDF ou une configuration Arduino personnalisée.
  5. Vérifiez les versions. Ignorez les commandes visant la version déjà installée, et envisagez de refuser les rétrogradations sauf si elles sont explicitement signalées.

Déployer sur une flotte

Étape Que faire
1 Téléverser le nouveau binaire sous un nom de fichier versionné, sans jamais écraser l'ancien
2 Mettre à jour un appareil de test et surveiller son topic de statut jusqu'à ce que info affiche la nouvelle version
3 Mettre à jour un petit groupe et repérer les appareils qui restent offline
4 Déployer sur le reste par lots, pas tous en même temps
5 Garder le binaire précédent disponible pour pouvoir le repousser si nécessaire

Envoyer les commandes appareil par appareil, plutôt que sur un topic partagé auquel tous les appareils sont abonnés, vous donne ce contrôle. Un petit script Python ou Node.js qui lit une liste d'identifiants d'appareils, publie la commande et attend chaque success est généralement le seul outillage nécessaire.

Dépannage

  • HTTP_UPDATE_FAILED avec « Not Enough Space » : le binaire est plus grand que l'emplacement OTA. Choisissez un schéma de partitions avec des partitions d'application plus grandes.
  • « Wrong HTTP Code » avec une redirection : certains hébergeurs redirigent vers un CDN. Le sketch suit les redirections strictes ; assurez-vous que la CA chargée correspond aussi à l'hôte final.
  • Le handshake TLS échoue : mauvaise CA ou heap insuffisant. Libérez les gros tampons avant d'appeler runOta().
  • L'appareil redémarre sur l'ancien firmware : la nouvelle image n'a pas réussi à démarrer. Testez d'abord le binaire par USB.
  • Le broker déconnecte l'appareil pendant le téléchargement : le callback de progression ne s'exécute pas assez souvent pour appeler mqtt.loop(). Réduisez le pas de rapport.

Faites tourner votre flux OTA sur ConnectMQ

Testez l'ensemble du flux sur le serveur MQTT public gratuit broker.connectmq.com, puis placez les topics de commande derrière des identifiants ConnectMQ. ConnectMQ est un broker MQTT managé avec isolation multi-tenant, client ID liés aux identifiants émis et TLS sur 8883 : seuls vos outils peuvent envoyer des commandes de mise à jour à vos appareils. Créez un broker avec l'offre gratuite Serverless sur app.connectmq.com, sans carte bancaire, et utilisez l'explorateur de topics en direct pour suivre le topic de statut de chaque appareil pendant un déploiement. Pour le projet d'appareil complet autour de ce flux, consultez le tutoriel IoT ESP32.

Questions fréquentes

Puis-je envoyer le binaire du firmware lui-même via MQTT ?

C'est possible, mais cela en vaut rarement la peine. Les binaires sont volumineux, la taille des messages est limitée, et il vous faudrait votre propre découpage et votre propre vérification. Envoyez plutôt une URL via MQTT et téléchargez le binaire en HTTPS.

Que se passe-t-il en cas de coupure de courant pendant une mise à jour OTA ?

L'ESP32 écrit la nouvelle image dans l'emplacement OTA inactif et ne bascule la partition de démarrage qu'après une écriture complète et vérifiée. Une coupure en plein téléchargement laisse l'ancien firmware en place.

Pourquoi mon appareil affiche-t-il « downloading » indéfiniment ?

Il a probablement planté ou perdu son alimentation sans déconnexion propre. Avec un Last Will retenu sur le topic de statut, le broker remplace cet état par « offline » après l'expiration du keepalive.

Faut-il la QoS 1 pour la commande OTA ?

La QoS 1 est un bon choix pour les commandes, car elle garantit la livraison tant que l'appareil est connecté. Rendez le gestionnaire idempotent en ignorant les commandes visant la version déjà installé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.