PyModbus
PyModbusDocs

Setting up Modbus RTU Client

Configure PyModbus RTU over serial RS485 and RS232. Port settings, timing, multi-slave polling, RS485 hardware setup, and troubleshooting.

Connect to Modbus devices over serial (RS485/RS232). Configure baud rate, parity, and timing.

Basic RTU Connection

from pymodbus.client import ModbusSerialClient

client = ModbusSerialClient(
    port='/dev/ttyUSB0',      # Linux (use 'COM3' on Windows)
    baudrate=9600,
    bytesize=8,
    parity='N',               # N=None, E=Even, O=Odd
    stopbits=1,
    timeout=3
)

if client.connect():
    result = client.read_holding_registers(0, 10, slave=1)
    if not result.isError():
        print(result.registers)
    client.close()

Serial Port Discovery

import serial.tools.list_ports

def find_serial_ports():
    """List available serial ports."""
    ports = serial.tools.list_ports.comports()

    for port in ports:
        print(f"{port.device}: {port.description}")
        if 'USB' in port.description or 'USB' in port.hwid:
            print(f"  (USB-to-serial adapter)")

    return [port.device for port in ports]

available = find_serial_ports()

Log Modbus data automatically

TofuPilot records test results from your PyModbus scripts, tracks pass/fail rates, and generates compliance reports. Free to start.

Common Configurations

Most devices use one of these settings. Check your device manual.

from pymodbus.client import ModbusSerialClient

CONFIGS = {
    'standard':     {'baudrate': 9600,   'parity': 'N', 'stopbits': 1},
    'energy_meter': {'baudrate': 9600,   'parity': 'E', 'stopbits': 1},
    'plc':          {'baudrate': 19200,  'parity': 'E', 'stopbits': 1},
    'high_speed':   {'baudrate': 115200, 'parity': 'N', 'stopbits': 1},
}

def create_rtu_client(port, preset='standard', timeout=3):
    config = CONFIGS.get(preset, CONFIGS['standard'])
    return ModbusSerialClient(port=port, bytesize=8, timeout=timeout, **config)

client = create_rtu_client('/dev/ttyUSB0', 'energy_meter')

RTU Timing

RTU uses precise timing. The inter-frame delay must be at least 3.5 character times:

def calculate_rtu_timing(baudrate):
    """Calculate minimum inter-frame delay for a given baud rate."""
    bits_per_char = 11  # 1 start + 8 data + 1 parity + 1 stop
    char_time_ms = (bits_per_char / baudrate) * 1000
    inter_frame_ms = 3.5 * char_time_ms

    print(f"{baudrate} baud: char={char_time_ms:.3f}ms, inter-frame={inter_frame_ms:.3f}ms")
    return inter_frame_ms / 1000

for baud in [9600, 19200, 38400, 115200]:
    calculate_rtu_timing(baud)

Multi-Slave Polling

On a single RS485 bus, you can poll multiple slave devices using their IDs:

import time
from pymodbus.client import ModbusSerialClient

def poll_slaves(port, slaves, address, count, interval=0.5):
    """Poll multiple slaves on the same serial bus.

    Args:
        port: Serial port path
        slaves: Dict of {name: slave_id}
        address: Starting register address
        count: Number of registers to read
        interval: Delay between polls in seconds
    """
    client = ModbusSerialClient(port=port, baudrate=9600, timeout=1)

    if not client.connect():
        print("Connection failed")
        return

    try:
        for name, slave_id in slaves.items():
            result = client.read_holding_registers(address, count, slave=slave_id)

            if not result.isError():
                print(f"{name} (slave {slave_id}): {result.registers}")
            else:
                print(f"{name} (slave {slave_id}): error - {result}")

            time.sleep(interval)  # inter-device delay
    finally:
        client.close()

slaves = {
    'temperature_sensor': 1,
    'pressure_sensor': 2,
    'flow_meter': 3,
}

poll_slaves('/dev/ttyUSB0', slaves, address=0, count=2)

Slave Scan

Find which slave IDs respond on the bus:

import time
from pymodbus.client import ModbusSerialClient

def scan_slaves(port, baudrate=9600, max_id=32):
    """Scan for responding slave devices."""
    client = ModbusSerialClient(port=port, baudrate=baudrate, timeout=0.3)

    if not client.connect():
        print("Connection failed")
        return []

    found = []
    for slave_id in range(1, max_id + 1):
        try:
            result = client.read_holding_registers(0, 1, slave=slave_id)
            if not result.isError():
                found.append(slave_id)
                print(f"  Found slave {slave_id}")
        except Exception:
            pass
        time.sleep(0.05)

    client.close()
    print(f"Found {len(found)} devices: {found}")
    return found

scan_slaves('/dev/ttyUSB0')

RS485 Hardware Setup

RS485 requires proper termination and biasing. Use 120 ohm termination resistors at both ends of the bus.

On Linux, you can enable kernel-level RS485 mode:

import fcntl
import struct
import serial

def enable_rs485(port_name):
    """Enable RS485 mode on a Linux serial port."""
    TIOCSRS485 = 0x542F
    SER_RS485_ENABLED = 0x01
    SER_RS485_RTS_ON_SEND = 0x02

    ser = serial.Serial(port_name)

    rs485_config = struct.pack(
        'IIIIII',
        SER_RS485_ENABLED | SER_RS485_RTS_ON_SEND,
        0, 0, 0, 0, 0  # delays and padding
    )

    fcntl.ioctl(ser.fileno(), TIOCSRS485, rs485_config)
    ser.close()
    print(f"RS485 mode enabled on {port_name}")

# Linux only
# enable_rs485('/dev/ttyUSB0')

RTU over TCP

Some serial-to-Ethernet gateways forward raw RTU frames over TCP:

from pymodbus.client import ModbusTcpClient
from pymodbus.framer import ModbusRtuFramer

client = ModbusTcpClient(
    '192.168.1.100',
    port=502,
    framer=ModbusRtuFramer
)

if client.connect():
    result = client.read_holding_registers(0, 10, slave=1)
    if not result.isError():
        print(result.registers)
    client.close()

ASCII Mode

For legacy devices that use ASCII framing instead of binary RTU:

from pymodbus.client import ModbusSerialClient
from pymodbus.framer import ModbusAsciiFramer

client = ModbusSerialClient(
    port='/dev/ttyUSB0',
    framer=ModbusAsciiFramer,
    baudrate=9600,
    timeout=3
)

if client.connect():
    result = client.read_holding_registers(0, 10, slave=1)
    if not result.isError():
        print(result.registers)
    client.close()

RTU is binary and more efficient than ASCII. Only use ASCII for legacy devices that require it.

Troubleshooting

Permission Denied (Linux)

# Add your user to the dialout group
sudo usermod -a -G dialout $USER
# Log out and back in

Test Serial Port

import time
import serial

def test_serial_port(port, baudrate=9600):
    """Verify the serial port opens and can send/receive."""
    try:
        ser = serial.Serial(port=port, baudrate=baudrate, timeout=1)
        print(f"Opened {port} at {baudrate} baud")

        # Send a Modbus read request for slave 1, register 0, count 1
        ser.write(b'\x01\x03\x00\x00\x00\x01\x84\x0A')
        time.sleep(0.1)

        if ser.in_waiting > 0:
            data = ser.read(ser.in_waiting)
            print(f"Received {len(data)} bytes: {data.hex()}")
        else:
            print("No response (check wiring and slave power)")

        ser.close()
        return True

    except Exception as e:
        print(f"Error: {e}")
        return False

test_serial_port('/dev/ttyUSB0')

Auto-Detect Settings

from pymodbus.client import ModbusSerialClient

def detect_settings(port):
    """Try common baud/parity combinations to find a responding slave."""
    bauds = [9600, 19200, 38400, 57600, 115200]
    parities = ['N', 'E', 'O']

    for baud in bauds:
        for parity in parities:
            client = ModbusSerialClient(
                port=port, baudrate=baud, parity=parity, timeout=0.5
            )

            if not client.connect():
                continue

            for slave in [1, 2, 247]:
                result = client.read_holding_registers(0, 1, slave=slave)
                if not result.isError():
                    print(f"Found: {baud} baud, parity={parity}, slave={slave}")
                    client.close()
                    return baud, parity, slave

            client.close()

    print("No working configuration found")
    return None

detect_settings('/dev/ttyUSB0')

Performance Tips

  1. Use the highest reliable baud rate your wiring supports
  2. Batch register reads instead of reading one at a time
  3. Use proper termination (120 ohm) to reduce CRC errors and retries
  4. Use shielded twisted pair cable for RS485
  5. Set a short but safe timeout (too low causes false timeouts, too high wastes time)