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)

On this page

Deploy your PyModbus test scripts with TofuPilot

Trusted by Lyft, Cobot, Pacific Defense, and more.

TofuPilot provides dashboards and analytics to track serial device testing at scale.

TofuPilot
Get Started