ESP32 and Arduino

ESP32 Modbus RS485 to MQTT Gateway: Poll RTU Devices and Publish Register Values

Turn an ESP32 and a 3.3 V RS485 transceiver into a small Modbus RTU master that polls meters and sensors, scales register values and publishes them as JSON over MQTT.

ESP32 modbus rs485 mqtt gateway wired to an energy meter and publishing register values to a cloud broker
Run this on your own MQTT brokerGet a private, managed MQTT broker on the Free plan and start from ready-made code samples.
Try it free Code samples No card required

An ESP32 Modbus RS485 MQTT gateway lets you pull data out of energy meters, VFDs, temperature transmitters and other Modbus RTU devices and put it on Wi-Fi without a PLC or an industrial PC. In this tutorial you will wire an RS485 transceiver to an ESP32, poll registers from one or more slaves with the ModbusMaster library, scale the raw values and publish them as JSON to an MQTT broker.

What the Gateway Does

Modbus RTU is a polled, master-slave protocol: nothing happens on the bus until a master asks a specific slave for a specific block of registers. MQTT is the opposite, an event-driven publish-subscribe protocol. The ESP32 sits between the two:

  1. It acts as the Modbus master on the RS485 bus, polling each configured device on a fixed interval.
  2. It converts raw 16-bit registers into engineering values (volts, degrees, kilowatt-hours) using a register map.
  3. It acts as an MQTT client, publishing one JSON message per device to a topic such as connectmq/demo/mb-4d1e/meter1.

Many devices can share one RS485 pair (each with its own slave address), so one ESP32 can serve a small panel full of instruments.

Hardware

Part Notes
ESP32 DevKit Uses UART2 for RS485
3.3 V RS485 transceiver (MAX3485, SP3485) The classic MAX485 is a 5 V part; prefer a 3.3 V chip
Modbus RTU device Energy meter, temperature/humidity transmitter, VFD...
120 Ω resistor Termination at the far end of long buses
Twisted-pair cable Shielded twisted pair for anything beyond a bench test

Some transceiver boards switch direction automatically and expose only RX and TX. Those work too: just leave the DE/RE pin unused in the code below.

Wiring

Transceiver pin ESP32 pin Purpose
VCC 3V3 Power
GND GND Ground, also connect to the device's signal ground if available
RO GPIO 16 (RX2) Data from bus to ESP32
DI GPIO 17 (TX2) Data from ESP32 to bus
DE and RE (tied) GPIO 4 High to transmit, low to receive
A / B Device A / B The bus pair

The most common wiring mistake is swapped A and B lines. Vendors disagree on which one is "plus", so if you get only timeouts, swap them before debugging anything else. Termination resistors belong at the two physical ends of the bus, not on every device.

Know Your Register Map

Every Modbus device documents its registers in a table: address, function code, data type and scale. Read it carefully, because it answers the questions that cause most gateway bugs:

  • Function code: holding registers are read with function 03, input registers with function 04. Asking with the wrong one returns an exception or nothing.
  • Address offset: documentation that says "30001" or "40001" usually means register address 0 with function 04 or 03. Some manuals list zero-based addresses, others one-based.
  • Scale: many devices send 235 meaning 23.5 degrees (scale 0.1).
  • Data type: 16-bit unsigned, 16-bit signed, or 32-bit values spread across two registers, sometimes with swapped word order.
  • Serial settings: baud rate, parity and stop bits must match exactly. 9600 8N1 is common, but 9600 8E1 is the Modbus default in the standard.

The ESP32 Modbus RS485 MQTT Sketch

Install ModbusMaster by Doc Walker and PubSubClient by Nick O'Leary from the Library Manager. The register map at the top is the only part you need to change for your devices. The example polls two devices: a temperature/humidity transmitter at address 1 and an energy meter at address 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;    // becomes the topic level
  uint8_t     slave;     // Modbus address
  uint8_t     fc;        // 3 = holding, 4 = input
  uint16_t    reg;       // register address (zero-based)
  uint8_t     words;     // 1 = 16 bit, 2 = 32 bit (high word first)
  bool        isSigned;
  float       scale;
  const char* name;      // JSON key
};

// Edit this table to match your devices' documentation
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() {
  // Group consecutive points that belong to the same device into one JSON message
  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);                      // inter-frame gap, helps slow devices
      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();
  }
}

A few details in this code matter more than they look:

  • preTransmission and postTransmission drive the DE pin so the transceiver only talks while the ESP32 transmits. Without them, the transceiver never releases the bus and every read times out.
  • Each published message carries an errors count. A dashboard can then show "device present but some registers failing" instead of silently freezing on old values.
  • Messages are retained, so a newly connected dashboard immediately sees the last values.
  • setBufferSize(512) gives room for longer JSON when you add more points.

Check the output:

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

Reading ModbusMaster Error Codes

Code Meaning Typical cause
0xE2 Response timed out Wrong A/B polarity, wrong baud or parity, wrong slave address, DE pin not toggling
0xE0 Invalid slave ID in response Two devices with the same address, or noise
0xE3 Invalid CRC Missing termination, long unshielded cable, ground offset
0x02 Illegal data address Register does not exist; check zero-based vs one-based addressing
0x01 Illegal function Using function 03 for a register that only answers to 04, or vice versa

Troubleshooting Checklist for ESP32 Modbus RS485 MQTT

  1. Confirm serial settings from the device menu or manual: baud, parity, stop bits, address.
  2. Test one device with a USB-RS485 adapter and a PC Modbus tool before involving the ESP32. If the PC cannot read it, the ESP32 will not either.
  3. Swap A and B if every request times out.
  4. Add 120 Ω termination at both ends of buses longer than a few metres, and connect signal ground.
  5. Keep the poll interval realistic. A dozen registers at 9600 baud take a noticeable fraction of a second; do not poll faster than the bus can answer.
  6. Read contiguous registers in one request when you can. One request for ten registers is far faster than ten requests for one.

Publishing Less, Not More

Polling every ten seconds and publishing everything is the simplest design, but meters change slowly and cloud traffic adds up. Two improvements fit easily into the sketch: publish a point only when it changes by more than a deadband, and send a full snapshot every few minutes as a heartbeat. That pattern is explained in report by exception. For a gateway-level view of the same problem, including PC-based gateways and larger RTU networks, see Modbus RTU to MQTT.

If MQTT itself is new, start with what an MQTT broker does, and for building JSON with a library rather than string concatenation, the ArduinoJson MQTT guide is a good companion. The ESP32 IoT tutorial covers the Wi-Fi and reconnect foundations used here.

Production Hardening

A gateway on a factory wall runs for years, so treat it like infrastructure:

  • TLS and credentials. Use port 8883 with WiFiClientSecure and credentials issued for this gateway. Never publish plant data to a public test topic.
  • Watchdog. Enable the task watchdog so a hung Modbus transaction or Wi-Fi stack reboots the board.
  • Isolation. In electrically noisy panels, an isolated RS485 transceiver prevents ground loops from damaging the ESP32.
  • Read-only by default. Writing registers over MQTT can start motors or change setpoints. If you add writes, restrict them to an allowlist of registers and validate ranges.
  • Broker choice. A fully managed MQTT service removes broker patching and certificate renewal from your to-do list, and ConnectMQ's topic explorer is a quick way to confirm every device is reporting.

Get Started with ConnectMQ

Prototype against the public area of broker.connectmq.com, then give your gateway a private broker. ConnectMQ provides TLS endpoints, per-broker credentials, a live topic explorer and a devices view organised by ISA-95 levels for industrial topics. Create a free Serverless broker at app.connectmq.com with no credit card and point the sketch at it.

Frequently Asked Questions

Can one ESP32 read several Modbus devices?

Yes. Give each device a unique slave address on the same RS485 pair and add its registers to the point table. The ESP32 polls them one after another.

Do I need a MAX485 or a MAX3485?

The ESP32 uses 3.3 V logic, so a 3.3 V transceiver such as the MAX3485 or SP3485 is the clean choice. A 5 V MAX485 board can work but needs level care on the RO line.

Why do I only get timeout errors?

Almost always wiring or settings: swapped A/B, wrong baud or parity, wrong slave address, or the DE/RE pin not switching. Test the device with a PC tool first.

Can the gateway write to Modbus registers from MQTT?

ModbusMaster supports writes, so it is possible, but restrict writes to specific registers and validate values. Unchecked remote writes to industrial equipment are a safety risk.

Related articles

ESP-IDF MQTT Example: Build an esp-mqtt Client from Scratch

Build an ESP-IDF project from an empty folder with the esp-mqtt component: Wi-Fi station setup, the MQTT event handler, publish and subscribe, MQTT 5 properties and TLS with the certificate bundle.

8 min read

Using esp-mqtt MQTT 5 Features on ESP32 with ESP-IDF

A hands-on ESP-IDF project that enables MQTT 5.0 in esp-mqtt and uses user properties, message expiry, topic aliases, session expiry and response topics against a real MQTT 5 broker.

7 min read

All articles →

Your private MQTT broker is a few clicks away

Create an account, get your broker credentials and connect your first device — no servers to install, patch or scale.