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:
- Actúa como maestro Modbus en el bus RS485 y consulta cada dispositivo configurado con un intervalo fijo.
- Convierte los registros en bruto de 16 bits en valores de ingeniería (voltios, grados, kilovatios hora) mediante un mapa de registros.
- 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
235para 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:
preTransmissionypostTransmissioncontrolan 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
- Confirma los parámetros serie en el menú o el manual del dispositivo: baudios, paridad, bits de parada y dirección.
- 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.
- Intercambia A y B si todas las peticiones acaban en timeout.
- 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.
- 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.
- 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
WiFiClientSecurey 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.



