ESP32 et Arduino

Passerelle ESP32 Modbus RS485 vers MQTT : interroger des équipements RTU et publier les valeurs de registres

Transformez un ESP32 et un transceiver RS485 en 3,3 V en petit maître Modbus RTU qui interroge compteurs et capteurs, met à l'échelle les valeurs de registres et les publie en JSON via MQTT.

Passerelle ESP32 Modbus RS485 MQTT reliée à un compteur d'énergie et publiant les valeurs de registres vers un broker cloud
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 passerelle ESP32 Modbus RS485 MQTT permet d'extraire les données de compteurs d'énergie, de variateurs de fréquence, de transmetteurs de température et d'autres équipements Modbus RTU pour les envoyer en Wi-Fi, sans automate ni PC industriel. Dans ce tutoriel, vous allez câbler un transceiver RS485 sur un ESP32, interroger les registres d'un ou plusieurs esclaves avec la bibliothèque ModbusMaster, mettre à l'échelle les valeurs brutes et les publier en JSON sur un broker MQTT.

Rôle de la passerelle

Modbus RTU est un protocole maître-esclave par interrogation : rien ne circule sur le bus tant qu'un maître ne demande pas un bloc de registres précis à un esclave précis. MQTT est l'inverse, un protocole publish-subscribe piloté par les événements. L'ESP32 fait le lien entre les deux :

  1. Il joue le rôle de maître Modbus sur le bus RS485 et interroge chaque équipement configuré à intervalle fixe.
  2. Il convertit les registres bruts de 16 bits en valeurs physiques (volts, degrés, kilowattheures) à l'aide d'une table de registres.
  3. Il joue le rôle de client MQTT et publie un message JSON par équipement sur un topic comme connectmq/demo/mb-4d1e/meter1.

Plusieurs équipements peuvent partager une même paire RS485 (chacun avec sa propre adresse esclave) : un seul ESP32 peut donc desservir une petite armoire remplie d'instruments.

Matériel

Composant Remarques
ESP32 DevKit Utilise l'UART2 pour le RS485
Transceiver RS485 3,3 V (MAX3485, SP3485) Le MAX485 classique est un composant 5 V ; préférez une puce 3,3 V
Équipement Modbus RTU Compteur d'énergie, transmetteur température/humidité, variateur...
Résistance de 120 Ω Terminaison à l'extrémité des bus longs
Câble à paire torsadée Paire torsadée blindée pour tout ce qui dépasse l'essai sur table

Certaines cartes transceiver basculent automatiquement le sens de transmission et n'exposent que RX et TX. Elles fonctionnent aussi : laissez simplement la broche DE/RE inutilisée dans le code ci-dessous.

Câblage

Broche du transceiver Broche ESP32 Rôle
VCC 3V3 Alimentation
GND GND Masse, à relier aussi à la masse de signal de l'équipement si elle existe
RO GPIO 16 (RX2) Données du bus vers l'ESP32
DI GPIO 17 (TX2) Données de l'ESP32 vers le bus
DE et RE (reliées) GPIO 4 Niveau haut pour émettre, bas pour recevoir
A / B A / B de l'équipement La paire du bus

L'erreur de câblage la plus fréquente est l'inversion des lignes A et B. Les fabricants ne s'accordent pas sur celle qui est le « plus » : si vous n'obtenez que des timeouts, inversez-les avant de chercher plus loin. Les résistances de terminaison se placent aux deux extrémités physiques du bus, pas sur chaque équipement.

Connaître sa table de registres

Chaque équipement Modbus documente ses registres dans un tableau : adresse, code fonction, type de donnée et facteur d'échelle. Lisez-le attentivement, car il répond aux questions à l'origine de la plupart des bugs de passerelle :

  • Code fonction : les holding registers se lisent avec la fonction 03, les input registers avec la fonction 04. Utiliser la mauvaise renvoie une exception ou rien du tout.
  • Décalage d'adresse : une documentation qui indique « 30001 » ou « 40001 » désigne généralement l'adresse de registre 0 avec la fonction 04 ou 03. Certains manuels listent des adresses commençant à zéro, d'autres à un.
  • Facteur d'échelle : beaucoup d'équipements envoient 235 pour 23,5 degrés (échelle 0,1).
  • Type de donnée : 16 bits non signé, 16 bits signé, ou valeurs 32 bits réparties sur deux registres, parfois avec un ordre des mots inversé.
  • Paramètres série : débit, parité et bits de stop doivent correspondre exactement. Le 9600 8N1 est courant, mais le 9600 8E1 est la valeur par défaut prévue par la norme Modbus.

Le sketch ESP32 Modbus RS485 MQTT

Installez ModbusMaster de Doc Walker et PubSubClient de Nick O'Leary depuis le gestionnaire de bibliothèques. La table de registres en haut du sketch est la seule partie à modifier pour vos équipements. L'exemple interroge deux équipements : un transmetteur température/humidité à l'adresse 1 et un compteur d'énergie à l'adresse 2.

#include <WiFi.h>
#include <PubSubClient.h>
#include <ModbusMaster.h>

const char* WIFI_SSID     = "YOUR_WIFI";
const char* WIFI_PASSWORD = "YOUR_WIFI_PASSWORD";
const char* MQTT_HOST     = "broker.connectmq.com";
const uint16_t MQTT_PORT  = 1883;
const char* CLIENT_ID     = "mb-4d1e-gateway";
const char* TOPIC_PREFIX  = "connectmq/demo/mb-4d1e";

const int RS485_RX = 16;
const int RS485_TX = 17;
const int RS485_DE = 4;
const uint32_t BAUD = 9600;
const uint32_t POLL_INTERVAL_MS = 10000;

struct Point {
  const char* device;    // devient le niveau de topic
  uint8_t     slave;     // adresse Modbus
  uint8_t     fc;        // 3 = holding, 4 = input
  uint16_t    reg;       // adresse de registre (base zéro)
  uint8_t     words;     // 1 = 16 bits, 2 = 32 bits (mot de poids fort en premier)
  bool        isSigned;
  float       scale;
  const char* name;      // clé JSON
};

// Modifiez ce tableau selon la documentation de vos équipements
Point points[] = {
  {"climate1", 1, 4, 0x0001, 1, true,  0.1,  "temperature_c"},
  {"climate1", 1, 4, 0x0002, 1, false, 0.1,  "humidity_pct"},
  {"meter1",   2, 3, 0x0000, 1, false, 0.1,  "voltage_v"},
  {"meter1",   2, 3, 0x0001, 1, false, 0.01, "current_a"},
  {"meter1",   2, 3, 0x0010, 2, false, 0.01, "energy_kwh"},
};
const size_t NUM_POINTS = sizeof(points) / sizeof(points[0]);

ModbusMaster node;
WiFiClient net;
PubSubClient mqtt(net);
uint32_t lastPoll = 0;

void preTx()  { digitalWrite(RS485_DE, HIGH); }
void postTx() { digitalWrite(RS485_DE, LOW); }

bool readPoint(const Point& p, float& out) {
  node.begin(p.slave, Serial2);
  uint8_t rc = (p.fc == 4) ? node.readInputRegisters(p.reg, p.words)
                           : node.readHoldingRegisters(p.reg, p.words);
  if (rc != node.ku8MBSuccess) {
    Serial.printf("slave %u reg 0x%04X error 0x%02X\n", p.slave, p.reg, rc);
    return false;
  }
  if (p.words == 2) {
    uint32_t raw = ((uint32_t)node.getResponseBuffer(0) << 16) | node.getResponseBuffer(1);
    out = p.isSigned ? (int32_t)raw * p.scale : raw * p.scale;
  } else {
    uint16_t raw = node.getResponseBuffer(0);
    out = p.isSigned ? (int16_t)raw * p.scale : raw * p.scale;
  }
  return true;
}

void connectWifi() {
  WiFi.mode(WIFI_STA);
  WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
  while (WiFi.status() != WL_CONNECTED) delay(500);
}

void connectMqtt() {
  char statusTopic[96];
  snprintf(statusTopic, sizeof(statusTopic), "%s/status", TOPIC_PREFIX);
  while (!mqtt.connected()) {
    if (mqtt.connect(CLIENT_ID, statusTopic, 1, true, "offline")) {
      mqtt.publish(statusTopic, "online", true);
    } else {
      delay(5000);
    }
  }
}

void pollAndPublish() {
  // Regroupe les points consécutifs d'un même équipement dans un seul message JSON
  size_t i = 0;
  while (i < NUM_POINTS) {
    const char* dev = points[i].device;
    String json = "{";
    int ok = 0, failed = 0;
    while (i < NUM_POINTS && strcmp(points[i].device, dev) == 0) {
      float v;
      if (readPoint(points[i], v)) {
        if (ok > 0) json += ",";
        json += "\"" + String(points[i].name) + "\":" + String(v, 2);
        ok++;
      } else {
        failed++;
      }
      delay(20);                      // pause entre trames, aide les équipements lents
      i++;
    }
    if (ok > 0) json += ",";
    json += "\"errors\":" + String(failed) + "}";

    char topic[96];
    snprintf(topic, sizeof(topic), "%s/%s", TOPIC_PREFIX, dev);
    mqtt.publish(topic, json.c_str(), true);
    Serial.printf("%s %s\n", topic, json.c_str());
  }
}

void setup() {
  Serial.begin(115200);
  pinMode(RS485_DE, OUTPUT);
  digitalWrite(RS485_DE, LOW);
  Serial2.begin(BAUD, SERIAL_8N1, RS485_RX, RS485_TX);
  node.preTransmission(preTx);
  node.postTransmission(postTx);

  connectWifi();
  mqtt.setServer(MQTT_HOST, MQTT_PORT);
  mqtt.setBufferSize(512);
  connectMqtt();
}

void loop() {
  if (WiFi.status() != WL_CONNECTED) connectWifi();
  if (!mqtt.connected()) connectMqtt();
  mqtt.loop();
  if (millis() - lastPoll >= POLL_INTERVAL_MS) {
    lastPoll = millis();
    pollAndPublish();
  }
}

Quelques détails de ce code comptent plus qu'il n'y paraît :

  • preTransmission et postTransmission pilotent la broche DE pour que le transceiver ne prenne la parole que pendant les émissions de l'ESP32. Sans eux, le transceiver ne libère jamais le bus et chaque lecture échoue en timeout.
  • Chaque message publié contient un compteur errors. Un tableau de bord peut alors afficher « équipement présent mais certains registres en échec » au lieu de rester silencieusement figé sur d'anciennes valeurs.
  • Les messages sont retenus : un tableau de bord qui vient de se connecter voit immédiatement les dernières valeurs.
  • setBufferSize(512) laisse de la place pour un JSON plus long lorsque vous ajoutez des points.

Vérifiez la sortie :

mosquitto_sub -h broker.connectmq.com -p 1883 -i mb-4d1e-viewer -t "connectmq/demo/mb-4d1e/#" -v

Interpréter les codes d'erreur de ModbusMaster

Code Signification Cause typique
0xE2 Délai de réponse dépassé Polarité A/B inversée, mauvais débit ou parité, mauvaise adresse esclave, broche DE qui ne bascule pas
0xE0 ID d'esclave invalide dans la réponse Deux équipements avec la même adresse, ou bruit
0xE3 CRC invalide Terminaison absente, long câble non blindé, différence de potentiel de masse
0x02 Adresse de donnée illégale Le registre n'existe pas ; vérifiez l'adressage base zéro ou base un
0x01 Fonction illégale Utilisation de la fonction 03 pour un registre qui ne répond qu'à la 04, ou l'inverse

Check-list de dépannage ESP32 Modbus RS485 MQTT

  1. Confirmez les paramètres série dans le menu ou le manuel de l'équipement : débit, parité, bits de stop, adresse.
  2. Testez un équipement avec un adaptateur USB-RS485 et un outil Modbus sur PC avant d'impliquer l'ESP32. Si le PC n'arrive pas à le lire, l'ESP32 n'y arrivera pas non plus.
  3. Inversez A et B si toutes les requêtes échouent en timeout.
  4. Ajoutez une terminaison de 120 Ω aux deux extrémités des bus de plus de quelques mètres et reliez la masse de signal.
  5. Gardez un intervalle d'interrogation réaliste. Une douzaine de registres à 9600 bauds prennent une fraction de seconde non négligeable ; n'interrogez pas plus vite que le bus ne peut répondre.
  6. Lisez les registres contigus en une seule requête quand c'est possible. Une requête pour dix registres est bien plus rapide que dix requêtes pour un registre.

Publier moins, pas plus

Interroger toutes les dix secondes et tout publier est la conception la plus simple, mais les compteurs évoluent lentement et le trafic cloud s'accumule. Deux améliorations s'intègrent facilement au sketch : ne publier un point que lorsqu'il varie de plus qu'une bande morte, et envoyer un instantané complet toutes les quelques minutes en guise de heartbeat. Ce schéma est expliqué dans l'article sur le report by exception. Pour une vue du même problème au niveau passerelle, y compris les passerelles sur PC et les réseaux RTU plus importants, consultez Modbus RTU vers MQTT.

Si MQTT est nouveau pour vous, commencez par le rôle d'un broker MQTT ; pour construire le JSON avec une bibliothèque plutôt que par concaténation de chaînes, le guide ArduinoJson MQTT est un bon complément. Le tutoriel IoT ESP32 couvre les bases du Wi-Fi et de la reconnexion utilisées ici.

Durcissement pour la production

Une passerelle fixée au mur d'une usine fonctionne pendant des années : traitez-la comme une infrastructure.

  • TLS et identifiants. Utilisez le port 8883 avec WiFiClientSecure et des identifiants émis pour cette passerelle. Ne publiez jamais de données d'usine sur un topic de test public.
  • Watchdog. Activez le watchdog de tâches pour qu'une transaction Modbus ou une pile Wi-Fi bloquée redémarre la carte.
  • Isolation. Dans les armoires électriquement bruyantes, un transceiver RS485 isolé évite que des boucles de masse n'endommagent l'ESP32.
  • Lecture seule par défaut. Écrire des registres via MQTT peut démarrer des moteurs ou modifier des consignes. Si vous ajoutez des écritures, limitez-les à une liste blanche de registres et validez les plages de valeurs.
  • Choix du broker. Un broker MQTT managé retire de votre liste de tâches les mises à jour du broker et le renouvellement des certificats, et l'explorateur de topics de ConnectMQ permet de vérifier rapidement que chaque équipement remonte bien ses données. Les valeurs de registres ainsi publiées peuvent ensuite alimenter un jumeau numérique de l'installation.

Démarrer avec ConnectMQ

Prototypez sur la zone publique de broker.connectmq.com, le serveur MQTT public gratuit réservé aux tests, puis donnez à votre passerelle un broker privé. ConnectMQ fournit des points de connexion TLS, des identifiants par broker, un explorateur de topics en direct et une vue des équipements organisée selon les niveaux ISA-95 pour les topics industriels. Créez un broker Serverless gratuit sur app.connectmq.com, sans carte bancaire, et pointez le sketch dessus.

Questions fréquentes

Un seul ESP32 peut-il lire plusieurs équipements Modbus ?

Oui. Attribuez à chaque équipement une adresse esclave unique sur la même paire RS485 et ajoutez ses registres à la table de points. L'ESP32 les interroge les uns après les autres.

Faut-il un MAX485 ou un MAX3485 ?

L'ESP32 fonctionne en logique 3,3 V : un transceiver 3,3 V comme le MAX3485 ou le SP3485 est donc le choix propre. Une carte MAX485 en 5 V peut fonctionner, mais demande de faire attention aux niveaux sur la ligne RO.

Pourquoi n'obtiens-je que des erreurs de timeout ?

C'est presque toujours le câblage ou les paramètres : A/B inversés, mauvais débit ou parité, mauvaise adresse esclave, ou broche DE/RE qui ne bascule pas. Testez d'abord l'équipement avec un outil sur PC.

La passerelle peut-elle écrire dans des registres Modbus depuis MQTT ?

ModbusMaster prend en charge l'écriture, c'est donc possible, mais limitez les écritures à des registres précis et validez les valeurs. Des écritures à distance non contrôlées sur des équipements industriels représentent un risque pour la sécurité.

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.