A Modbus RTU to MQTT gateway lets you take readings from RS-485 energy meters, flow meters, drives and sensors and publish them to a broker where dashboards and analytics can use them. This tutorial covers the physical layer that causes most problems (wiring, termination, serial settings), how to read a vendor register map, and a complete Python gateway that polls several devices on one bus and publishes their values over MQTT. A Raspberry Pi or any Linux box with a USB to RS-485 adapter is enough to follow along.
What Makes Modbus RTU Different from Modbus TCP
The Modbus data model is the same: coils, discrete inputs, holding registers and input registers. What changes is the transport. Modbus RTU runs over a serial line, almost always RS-485, as a multi-drop bus. One master polls up to 247 slave devices, each with a unique unit address. Frames are binary with a CRC, and frame boundaries are defined by silent gaps on the line.
That has practical consequences:
- Only one master per bus. If a building management system or HMI already polls the meters, your gateway cannot simply join the same wires. You need a separate bus, a device with a second port, or a gateway that sits in front of both.
- The bus is slow and shared. At 9600 baud, each request and response takes tens of milliseconds, and requests to different devices are serialised. Polling plans matter.
- Electrical problems look like software problems. Missing termination or a swapped pair produces timeouts and CRC errors that no amount of code will fix.
If your devices are on Ethernet, use our Modbus TCP to MQTT tutorial instead.
Hardware You Need
- A gateway computer: a Raspberry Pi, industrial PC or any Linux machine.
- A USB to RS-485 adapter, ideally an isolated one with selectable termination and bias resistors. Isolation protects the gateway from ground potential differences between cabinets.
- Twisted-pair cable for the bus, preferably shielded, with a third conductor for the signal common.
- The Modbus manual for each device. You need its register map and its default serial settings.
RS-485 Wiring Done Right
RS-485 is robust when wired as intended and frustrating when not. Follow these rules:
- Daisy chain, not star. Run the bus from device to device in a line. Short stubs are tolerable; long branches cause reflections.
- Terminate both ends. Place a 120 ohm resistor across the A/B pair at the two physical ends of the bus, and nowhere else. Many adapters and meters have a termination switch or jumper.
- Bias once. Bias (fail-safe) resistors keep the line in a defined state when nobody is talking. Enable them in one place, usually the master adapter.
- Connect the common. Despite the name "two-wire", connect the signal ground (often labelled GND, COM or C) between devices so the receivers share a reference.
- Watch the A/B labelling. Vendors disagree on whether A is positive or negative, and some use D+ and D-. If nothing answers, swapping A and B at the adapter is a legitimate first test.
- Keep it away from power cables. Route the bus separately from motor and VFD cables, and ground the shield at one end.
Serial Settings: Baud, Parity and Stop Bits
Every device on a bus must use the same settings, and every device needs a unique address.
| Setting | Common values | Notes |
|---|---|---|
| Baud rate | 9600, 19200, 38400 | 9600 and 19200 are the most common defaults; longer cables favour lower rates |
| Data bits | 8 | Modbus RTU always uses 8 data bits |
| Parity | Even, None, Odd | The Modbus specification defaults to even parity, but many devices ship with none |
| Stop bits | 1 or 2 | With no parity, the specification calls for 2 stop bits, though many devices accept 1 |
| Unit address | 1 to 247 | Set on the device front panel or by software; must be unique on the bus |
Write the settings down as a short code such as 9600 8E1 or 19200 8N2, and set every device to match before you connect anything.
Reading a Register Map
A device manual lists registers in a table with address, description, data type, unit and sometimes a scale factor. The example below is a hypothetical three-phase energy meter used for this tutorial; real meters differ, so always take addresses from your own manual.
| Description | Function | Address (0-based) | Type | Unit |
|---|---|---|---|---|
| Voltage L1-N | 04 input | 0 | float32 | V |
| Current L1 | 04 input | 6 | float32 | A |
| Active power total | 04 input | 52 | float32 | W |
| Frequency | 04 input | 70 | float32 | Hz |
| Imported active energy | 04 input | 72 | float32 | kWh |
Three things to check in every manual: whether addresses are zero-based or written as 30001 style (subtract the offset), whether 32-bit values use high-word-first or low-word-first order, and whether integer registers need a scale factor.
Prerequisites on the Gateway
sudo usermod -aG dialout $USER # allow serial port access, then log out and back in
ls -l /dev/serial/by-id/ # stable device path for your adapter
python3 -m venv ~/rtu-gw && source ~/rtu-gw/bin/activate
pip install "pymodbus[serial]>=3.8,<3.9" "paho-mqtt>=2.0"
Use the /dev/serial/by-id/... path rather than /dev/ttyUSB0, because USB device numbers can change after a reboot.
The Modbus RTU to MQTT Gateway Code
The gateway polls each meter in turn, reads each register block in one request where possible, and publishes one JSON message per meter per cycle. Bundling a meter's values into one message keeps the message rate low, which is what you want on a slow bus and on a metered uplink.
# rtu_mqtt_gateway.py - poll Modbus RTU meters over RS-485, publish to MQTT
import json, logging, struct, time, uuid
import paho.mqtt.client as mqtt
from pymodbus.client import ModbusSerialClient
logging.basicConfig(level=logging.INFO)
log = logging.getLogger("rtu-gw")
# Replace port with your /dev/serial/by-id/... path in production
SERIAL = dict(port="/dev/ttyUSB0", baudrate=9600, parity="E", stopbits=1, bytesize=8, timeout=0.5)
POLL_SECONDS = 10
TOPIC_ROOT = "connectmq/demo/e5a1/plant1/energy" # use your own random id
MQTT_HOST, MQTT_PORT = "broker.connectmq.com", 1883 # production: 8883 with TLS
MQTT_USER, MQTT_PASS = None, None # production: "YOUR_USERNAME", "YOUR_PASSWORD"
CLIENT_ID = f"rtu-gw-{uuid.uuid4().hex[:8]}" # production: "YOUR_CLIENT_ID"
# Meters on the bus: Modbus unit address -> topic name
METERS = [
{"unit": 1, "name": "mdb1_incomer"},
{"unit": 2, "name": "mdb1_compressors"},
]
def f32(regs, offset, big_endian_words=True):
words = regs[offset:offset + 2] if big_endian_words else regs[offset:offset + 2][::-1]
return struct.unpack(">f", struct.pack(">HH", *words))[0]
def read_meter(client, unit):
"""Read the hypothetical meter's float32 input registers in two block requests."""
block_a = client.read_input_registers(0, count=8, slave=unit) # voltage, current
block_b = client.read_input_registers(52, count=24, slave=unit) # power .. energy
for rr in (block_a, block_b):
if rr.isError():
raise IOError(str(rr))
a, b = block_a.registers, block_b.registers
return {
"voltage_l1": round(f32(a, 0), 1),
"current_l1": round(f32(a, 6), 2),
"power_total": round(f32(b, 0), 0),
"frequency": round(f32(b, 18), 2),
"energy_import": round(f32(b, 20), 1),
}
def main():
mq = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2, client_id=CLIENT_ID)
if MQTT_USER:
mq.username_pw_set(MQTT_USER, MQTT_PASS)
mq.tls_set()
mq.will_set(f"{TOPIC_ROOT}/gateway", "offline", qos=1, retain=True)
mq.on_connect = lambda c, u, f, rc, p: c.publish(f"{TOPIC_ROOT}/gateway", "online", qos=1, retain=True)
mq.connect(MQTT_HOST, MQTT_PORT, keepalive=60)
mq.loop_start()
bus = ModbusSerialClient(**SERIAL)
errors = {m["unit"]: 0 for m in METERS}
while True:
cycle_start = time.monotonic()
if not bus.connected and not bus.connect():
log.error("cannot open serial port %s", SERIAL["port"])
time.sleep(5)
continue
for meter in METERS:
topic = f"{TOPIC_ROOT}/{meter['name']}"
try:
values = read_meter(bus, meter["unit"])
errors[meter["unit"]] = 0
payload = {**values, "quality": "good",
"ts": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())}
mq.publish(topic, json.dumps(payload), qos=1, retain=True)
log.info("%s %s", meter["name"], values)
except Exception as exc:
errors[meter["unit"]] += 1
log.warning("unit %s: %s", meter["unit"], exc)
if errors[meter["unit"]] == 3: # publish bad quality once, after 3 misses
mq.publish(topic, json.dumps({"quality": "bad"}), qos=1, retain=True)
time.sleep(0.05) # small gap between devices on the bus
time.sleep(max(0, POLL_SECONDS - (time.monotonic() - cycle_start)))
if __name__ == "__main__":
main()
Note the offsets in read_meter: block B starts at register 52, so frequency at register 70 is offset 18 and imported energy at 72 is offset 20. Getting these offsets wrong is the most common bug when you batch reads, so print raw registers once while commissioning.
Run and Verify
Run the gateway, then watch the output from any machine:
python rtu_mqtt_gateway.py
mosquitto_sub -h broker.connectmq.com -p 1883 -i rtu-viewer-e5a1 -t "connectmq/demo/e5a1/#" -v
connectmq/demo/e5a1/plant1/energy/mdb1_incomer {"voltage_l1": 231.4, "current_l1": 18.72, "power_total": 12890.0, "frequency": 50.01, "energy_import": 48213.6, "quality": "good", "ts": "2026-10-16T10:05:00Z"}
The public MQTT test broker is readable by anyone, so use it only for test data. For a real site, create a private broker and switch to port 8883 with your credentials.
Troubleshooting Modbus RTU
| Symptom | Likely cause | What to try |
|---|---|---|
| No response from any device | A/B swapped, wrong baud or parity, wrong port | Swap A/B, confirm 8E1 vs 8N1, check /dev/serial/by-id |
| Some devices answer, others time out | Duplicate or wrong unit address, wiring break | Disconnect all but one device and test each |
| Intermittent CRC errors | Missing termination, star topology, noise from drives | Terminate both ends, re-route cable, use shielded twisted pair |
| Works at 9600, fails at higher rates | Long cable or poor wiring | Stay at the lower rate or fix the cabling |
| Exception code 2 (illegal data address) | Address beyond the device's map or block spans a gap | Split the block into smaller reads |
| Random timeouts while another system is running | Two masters on the same bus | Give the gateway its own bus or port |
Scaling Up
A single RS-485 port is fine for a handful of meters. When you have dozens, consider one adapter per panel or Modbus RTU to TCP converters that let one gateway reach several buses over Ethernet, then reuse the TCP bridge. Publishing on change rather than every cycle, as described in our report by exception guide, also keeps the uplink quiet. For remote sites on mobile data, see MQTT over cellular.
A hosted MQTT broker such as ConnectMQ removes the need to run a server for these gateways. The free Serverless plan supports up to 25 connections, enough for a pilot with several gateways, and the paid Serverless plan scales to 1,000. The portal's threshold alert rules can watch values such as power_total and log an alert when a limit is crossed. For the architecture behind it all, read the industrial IoT MQTT guide.
Get Started with ConnectMQ
Get your RS-485 meters online today. Sign up at app.connectmq.com, choose the free Serverless plan (no credit card), create a broker and copy the generated username, password and client ID into the gateway script. Your meter readings will show up in the ConnectMQ live topic explorer as soon as the first poll completes.
Frequently Asked Questions
How many Modbus RTU devices can one RS-485 bus support?
Modbus addresses allow up to 247 devices, but the electrical limit depends on transceiver load; standard RS-485 supports 32 unit loads, and many modern transceivers present fractional loads. In practice, polling time usually becomes the limit first.
Can I read Modbus RTU devices that a BMS already polls?
Not on the same bus, because Modbus RTU allows only one master. Use a second port on the device, a separate meter output, or ask the BMS integrator to expose the data another way.
Which baud rate should I use for Modbus RTU?
Use what every device on the bus supports, typically 9600 or 19200. Lower rates tolerate longer cables and noisier environments better.
Should a Modbus RTU gateway publish every poll?
Not necessarily. Bundling a device's values into one message per cycle, or publishing only on significant change, keeps message volume low without losing information.



