DEV Community

Rebecca Anderson
Rebecca Anderson

Posted on

Reading Modbus RTU Data with Python

Most tutorials on reading Modbus RTU data with Python focus entirely on the software side. The standard advice is to run pip install pymodbus, plug a USB-to-RS485 adapter into your machine, and write a script using ModbusSerialClient.
It usually works perfectly on your desk. But when you deploy it to a production environment, the connection randomly drops, you get timeout errors, or the data reads as garbage.

Let's look at why this happens, and how to structure your Modbus integration so it actually stays up.

The Underlying Problem: Timing and Noise
Reading serial data directly via Python in a production setting introduces two major failure points:

  1. OS Timing Issues: Modbus RTU relies on strict timing gaps between data frames (typically 3.5 character times). Standard operating systems like Linux or Windows are not real-time OSs. If the CPU is busy and delays reading the serial buffer, the frame breaks, and your Python script throws a CRC error.

  2. Electrical Isolation: The floors of factories are electrically loud. A normal USB-to-RS485 dongle does not provide galvanic isolation. The ground potential changes when a big machine nearby starts up. That spike goes all the way up the RS485 cable, crashes the USB driver on your server, and you have to unplug and plug it back in to fix it.

The Fix: Decoupling the Architecture
The most reliable way to read Modbus RTU in Python is to avoid dealing with the serial port entirely.

Instead of letting your server handle the physical layer, decouple it using an industrial protocol gateway. You place a Modbus RTU-to-TCP converter (in my current setup, I use a Valtoris isolated Modbus gateway) right next to the physical sensors.

The hardware gateway handles the strict RS485 timing, the electrical isolation, and the continuous polling. It then exposes that data over standard Ethernet as Modbus TCP.

Your Python script no longer fights with ttyUSB0 or baud rates. It simply makes a standard TCP network request.

The Code: Switching to Modbus TCP
By shifting to TCP, the Python implementation becomes much cleaner and naturally handles network resilience. Here is the baseline structure using pymodbus:

from pymodbus.client import ModbusTcpClient
import time

Point this to the IP of your hardware gateway

GATEWAY_IP = '192.168.1.100'
GATEWAY_PORT = 502

def poll_sensor():
# Use TCP Client instead of Serial
client = ModbusTcpClient(GATEWAY_IP, port=GATEWAY_PORT, timeout=3)

try:
    if not client.connect():
        print("Connection failed. Check network or gateway.")
        return

    # Read 2 registers from Slave ID 1, starting at address 0
    response = client.read_holding_registers(address=0, count=2, slave=1)

    if response.isError():
        print(f"Modbus Error: {response}")
    else:
        raw_data = response.registers
        print(f"Raw Registers: {raw_data}")

        # Basic parsing example
        temp = raw_data[0] / 10.0
        print(f"Temperature: {temp} °C")

except Exception as e:
    print(f"Exception caught: {e}")
finally:
    client.close()
Enter fullscreen mode Exit fullscreen mode

if name == 'main':
while True:
poll_sensor()
time.sleep(5)

One Last Gotcha: Endianness
If your script connects but the data doesn't make sense (for example, your temperature says 18432 instead of 24.5), you haven't done anything wrong. There is a problem with the byte order.

The Modbus protocol specification does not strictly define how 32-bit floats or long integers should be ordered across 16-bit registers. Different sensor manufacturers use different byte orders (Big-Endian vs. Little-Endian).

If you come across this, don't try to figure out the bits by hand. To change the order of the bytes until the output matches what you expect, use the BinaryPayloadDecoder module that comes with pymodbus.

from pymodbus.payload import BinaryPayloadDecoder
from pymodbus.constants import Endian

Example: Decoding a 32-bit float

decoder = BinaryPayloadDecoder.fromRegisters(raw_data, byteorder=Endian.Big, wordorder=Endian.Little)
real_value = decoder.decode_32bit_float()

Top comments (0)