ESP32 y Arduino

Gateway ESP32 Modbus RS485 a MQTT: consulta dispositivos RTU y publica valores de registros

Convierte un ESP32 y un transceptor RS485 de 3,3 V en un pequeño maestro Modbus RTU que consulta contadores y sensores, escala los valores de los registros y los publica como JSON por MQTT.

Gateway ESP32 Modbus RS485 MQTT conectado a un contador de energía que publica valores de registros en un broker en la nube
Pruébalo en tu propio broker MQTTConsigue un broker MQTT privado y gestionado con el plan Free y empieza con ejemplos de código listos para usar.
Pruébalo gratis Ejemplos de código Sin tarjeta

Un gateway ESP32 Modbus RS485 MQTT te permite extraer datos de contadores de energía, variadores de frecuencia, transmisores de temperatura y otros dispositivos Modbus RTU y llevarlos a Wi-Fi sin un PLC ni un PC industrial. En este tutorial conectarás un transceptor RS485 a un ESP32, consultarás registros de uno o varios esclavos con la librería ModbusMaster, escalarás los valores en bruto y los publicarás como JSON en un broker MQTT.

Qué hace el gateway

Modbus RTU es un protocolo maestro-esclavo por sondeo: no ocurre nada en el bus hasta que un maestro pide a un esclavo concreto un bloque concreto de registros. MQTT es lo contrario, un protocolo publicación-suscripción orientado a eventos. El ESP32 se sitúa entre ambos:

  1. Actúa como maestro Modbus en el bus RS485 y consulta cada dispositivo configurado con un intervalo fijo.
  2. Convierte los registros en bruto de 16 bits en valores de ingeniería (voltios, grados, kilovatios hora) mediante un mapa de registros.
  3. Actúa como cliente MQTT y publica un mensaje JSON por dispositivo en un topic como connectmq/demo/mb-4d1e/meter1.

Muchos dispositivos pueden compartir un mismo par RS485 (cada uno con su propia dirección de esclavo), así que un solo ESP32 puede atender un pequeño tablero eléctrico lleno de instrumentos, por ejemplo en una planta de agroindustria o en una estación de bombeo.

Hardware

Componente Notas
ESP32 DevKit Usa la UART2 para RS485
Transceptor RS485 de 3,3 V (MAX3485, SP3485) El clásico MAX485 es de 5 V; mejor un chip de 3,3 V
Dispositivo Modbus RTU Contador de energía, transmisor de temperatura/humedad, variador...
Resistencia de 120 Ω Terminación en el extremo lejano de buses largos
Cable de par trenzado Par trenzado apantallado para cualquier cosa más allá de una prueba en banco

Algunas placas transceptoras cambian de dirección automáticamente y solo exponen RX y TX. También funcionan: simplemente deja sin usar el pin DE/RE en el código siguiente.

Cableado

Pin del transceptor Pin del ESP32 Función
VCC 3V3 Alimentación
GND GND Masa; conéctala también a la masa de señal del dispositivo si existe
RO GPIO 16 (RX2) Datos del bus al ESP32
DI GPIO 17 (TX2) Datos del ESP32 al bus
DE y RE (unidos) GPIO 4 Alto para transmitir, bajo para recibir
A / B A / B del dispositivo El par del bus

El error de cableado más habitual es intercambiar las líneas A y B. Los fabricantes no se ponen de acuerdo sobre cuál es el "positivo", así que si solo obtienes timeouts, intercámbialas antes de depurar cualquier otra cosa. Las resistencias de terminación van en los dos extremos físicos del bus, no en cada dispositivo.

Conoce tu mapa de registros

Cada dispositivo Modbus documenta sus registros en una tabla: dirección, código de función, tipo de dato y escala. Léela con atención, porque responde a las preguntas que causan la mayoría de los fallos de un gateway:

  • Código de función: los holding registers se leen con la función 03 y los input registers con la función 04. Pedirlos con la función equivocada devuelve una excepción o nada.
  • Desplazamiento de dirección: una documentación que dice "30001" o "40001" suele significar la dirección de registro 0 con la función 04 o 03. Algunos manuales usan direcciones basadas en cero y otros basadas en uno.
  • Escala: muchos dispositivos envían 235 para indicar 23,5 grados (escala 0,1).
  • Tipo de dato: 16 bits sin signo, 16 bits con signo o valores de 32 bits repartidos en dos registros, a veces con el orden de palabras invertido.
  • Parámetros serie: velocidad en baudios, paridad y bits de parada deben coincidir exactamente. 9600 8N1 es habitual, pero 9600 8E1 es el valor por defecto que fija el estándar Modbus.

El sketch ESP32 Modbus RS485 MQTT

Instala ModbusMaster de Doc Walker y PubSubClient de Nick O'Leary desde el Gestor de librerías. El mapa de registros del principio es lo único que tienes que cambiar para tus dispositivos. El ejemplo consulta dos dispositivos: un transmisor de temperatura/humedad en la dirección 1 y un contador de energía en la dirección 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;    // se convierte en el nivel del topic
  uint8_t     slave;     // dirección Modbus
  uint8_t     fc;        // 3 = holding, 4 = input
  uint16_t    reg;       // dirección del registro (basada en cero)
  uint8_t     words;     // 1 = 16 bits, 2 = 32 bits (palabra alta primero)
  bool        isSigned;
  float       scale;
  const char* name;      // clave JSON
};

// Edita esta tabla según la documentación de tus dispositivos
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() {
  // Agrupa los puntos consecutivos del mismo dispositivo en un único mensaje 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);                      // pausa entre tramas, ayuda a los dispositivos lentos
      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();
  }
}

Algunos detalles de este código importan más de lo que parece:

  • preTransmission y postTransmission controlan el pin DE para que el transceptor solo hable mientras el ESP32 transmite. Sin ellos, el transceptor nunca libera el bus y todas las lecturas acaban en timeout.
  • Cada mensaje publicado incluye un contador errors. Así, un panel puede mostrar "dispositivo presente pero algunos registros fallan" en lugar de quedarse congelado en silencio con valores antiguos.
  • Los mensajes son retenidos, por lo que un panel recién conectado ve de inmediato los últimos valores.
  • setBufferSize(512) deja espacio para un JSON más largo cuando añadas más puntos.

Comprueba la salida:

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

Interpretar los códigos de error de ModbusMaster

Código Significado Causa típica
0xE2 Tiempo de respuesta agotado Polaridad A/B invertida, baudios o paridad incorrectos, dirección de esclavo errónea, pin DE que no conmuta
0xE0 ID de esclavo no válido en la respuesta Dos dispositivos con la misma dirección, o ruido
0xE3 CRC no válido Falta la terminación, cable largo sin apantallar, diferencia de potencial de masa
0x02 Dirección de datos ilegal El registro no existe; revisa el direccionamiento basado en cero frente a basado en uno
0x01 Función ilegal Usar la función 03 para un registro que solo responde a la 04, o viceversa

Lista de comprobación para resolver problemas de ESP32 Modbus RS485 MQTT

  1. Confirma los parámetros serie en el menú o el manual del dispositivo: baudios, paridad, bits de parada y dirección.
  2. Prueba un dispositivo con un adaptador USB-RS485 y una herramienta Modbus de PC antes de meter al ESP32 en juego. Si el PC no puede leerlo, el ESP32 tampoco.
  3. Intercambia A y B si todas las peticiones acaban en timeout.
  4. Añade terminación de 120 Ω en ambos extremos de buses de más de unos pocos metros y conecta la masa de señal.
  5. Mantén un intervalo de sondeo realista. Una docena de registros a 9600 baudios tarda una fracción apreciable de segundo; no consultes más rápido de lo que el bus puede responder.
  6. Lee registros contiguos en una sola petición siempre que puedas. Una petición de diez registros es mucho más rápida que diez peticiones de uno.

Publicar menos, no más

Consultar cada diez segundos y publicarlo todo es el diseño más sencillo, pero los contadores cambian despacio y el tráfico a la nube se acumula. Dos mejoras encajan fácilmente en el sketch: publicar un punto solo cuando cambie más allá de una banda muerta y enviar una instantánea completa cada pocos minutos como heartbeat. Ese patrón se explica en report by exception. Para ver el mismo problema desde el nivel del gateway, incluidos los gateways basados en PC y las redes RTU más grandes, consulta Modbus RTU a MQTT.

Si MQTT en sí es nuevo para ti, empieza por qué hace un broker MQTT, y para construir JSON con una librería en lugar de concatenar cadenas, la guía de ArduinoJson con MQTT es un buen complemento. El tutorial de IoT con ESP32 cubre las bases de Wi-Fi y reconexión que se usan aquí.

Robustez para producción

Un gateway en la pared de una fábrica funciona durante años, así que trátalo como infraestructura:

  • TLS y credenciales. Usa el puerto 8883 con WiFiClientSecure y credenciales emitidas para este gateway. Nunca publiques datos de planta en un topic de pruebas público.
  • Watchdog. Activa el watchdog de tareas para que una transacción Modbus colgada o una pila Wi-Fi bloqueada reinicien la placa.
  • Aislamiento. En tableros con mucho ruido eléctrico, un transceptor RS485 aislado evita que los bucles de masa dañen el ESP32.
  • Solo lectura por defecto. Escribir registros por MQTT puede arrancar motores o cambiar consignas. Si añades escrituras, limítalas a una lista de registros permitidos y valida los rangos.
  • Elección del broker. Un broker MQTT gestionado quita de tu lista de tareas los parches del broker y la renovación de certificados, y el explorador de topics de ConnectMQ es una forma rápida de confirmar que todos los dispositivos están reportando.

Empieza con ConnectMQ

Haz el prototipo contra el servidor MQTT público gratuito broker.connectmq.com, pensado solo para pruebas, y después dale a tu gateway un broker privado. ConnectMQ ofrece endpoints TLS, credenciales por broker, un explorador de topics en vivo y una vista de dispositivos organizada por niveles ISA-95 para topics industriales. Crea un broker Serverless gratuito en app.connectmq.com, sin tarjeta de crédito, y apunta el sketch hacia él.

Preguntas frecuentes

¿Puede un ESP32 leer varios dispositivos Modbus?

Sí. Asigna a cada dispositivo una dirección de esclavo única en el mismo par RS485 y añade sus registros a la tabla de puntos. El ESP32 los consulta uno tras otro.

¿Necesito un MAX485 o un MAX3485?

El ESP32 usa lógica de 3,3 V, así que un transceptor de 3,3 V como el MAX3485 o el SP3485 es la opción limpia. Una placa MAX485 de 5 V puede funcionar, pero requiere cuidado con los niveles en la línea RO.

¿Por qué solo obtengo errores de timeout?

Casi siempre es el cableado o la configuración: A/B intercambiados, baudios o paridad incorrectos, dirección de esclavo errónea o el pin DE/RE que no conmuta. Prueba antes el dispositivo con una herramienta de PC.

¿Puede el gateway escribir en registros Modbus desde MQTT?

ModbusMaster admite escrituras, así que es posible, pero limita las escrituras a registros concretos y valida los valores. Las escrituras remotas sin control sobre equipos industriales son un riesgo de seguridad.

Artículos relacionados

Ejemplo de ESP-IDF MQTT: crea un cliente esp-mqtt desde cero

Construye un proyecto ESP-IDF desde una carpeta vacía con el componente esp-mqtt: configuración de Wi-Fi en modo estación, el manejador de eventos MQTT, publicación y suscripción, propiedades de MQTT 5 y TLS con el bundle de certificados.

9 min de lectura

Todos los artículos →

Tu broker MQTT privado, a unos pocos clics

Crea una cuenta, obtén las credenciales de tu broker y conecta tu primer dispositivo, sin servidores que instalar, parchear ni escalar.