PyModbus
PyModbusDocs

Setting up Modbus RTU Client

Configure Modbus RTU over serial RS485/RS232 - port settings, timing, wiring, troubleshooting.

Modbus RTU Client

Connect to Modbus devices over serial (RS485/RS232). Handle serial ports, baud rates, and timing.

Basic RTU Client

from pymodbus.client import ModbusSerialClient

client = ModbusSerialClient(
    port='/dev/ttyUSB0',     # Serial port (Linux)
    # port='COM3',            # Windows
    baudrate=9600,            # Baud rate
    bytesize=8,               # Data bits (7 or 8)
    parity='N',               # None, Even, Odd (N/E/O)
    stopbits=1,               # Stop bits (1 or 2)
    timeout=3,                # Response timeout
    strict=False              # Strict timing
)

if client.connect():
    # Read from slave ID 1
    result = client.read_holding_registers(0, 10, slave=1)
    print(result.registers)
    client.close()

Serial Port Discovery

import serial.tools.list_ports

def find_serial_ports():
    """Find available serial ports."""
    ports = serial.tools.list_ports.comports()
    
    for port in ports:
        print(f"Port: {port.device}")
        print(f"  Description: {port.description}")
        print(f"  Hardware ID: {port.hwid}")
        
        # Check if it's a USB serial adapter
        if 'USB' in port.description or 'USB' in port.hwid:
            print(f"  -> Likely USB-to-serial adapter")
    
    return [port.device for port in ports]

# Find ports
available_ports = find_serial_ports()
print(f"\nAvailable ports: {available_ports}")

Common Configurations

Standard Settings by Device Type

# Common industrial settings
COMMON_CONFIGS = {
    'standard': {
        'baudrate': 9600,
        'bytesize': 8,
        'parity': 'N',
        'stopbits': 1
    },
    'energy_meter': {
        'baudrate': 9600,
        'bytesize': 8,
        'parity': 'E',  # Even parity common for meters
        'stopbits': 1
    },
    'plc': {
        'baudrate': 19200,
        'bytesize': 8,
        'parity': 'E',
        'stopbits': 1
    },
    'high_speed': {
        'baudrate': 115200,
        'bytesize': 8,
        'parity': 'N',
        'stopbits': 1
    }
}

def create_rtu_client(port, device_type='standard'):
    """Create RTU client with preset configuration."""
    config = COMMON_CONFIGS.get(device_type, COMMON_CONFIGS['standard'])
    
    return ModbusSerialClient(
        port=port,
        **config,
        timeout=3
    )

# Use preset
client = create_rtu_client('/dev/ttyUSB0', 'energy_meter')

RTU Timing

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

def calculate_rtu_timing(baudrate):
    """Calculate RTU timing requirements."""
    # Time for one character (11 bits: 1 start, 8 data, 1 parity, 1 stop)
    bits_per_char = 11
    char_time_ms = (bits_per_char / baudrate) * 1000
    
    # RTU requirements
    inter_char_timeout = 1.5 * char_time_ms
    inter_frame_timeout = 3.5 * char_time_ms
    
    print(f"Baudrate: {baudrate}")
    print(f"Character time: {char_time_ms:.3f} ms")
    print(f"Inter-char timeout: {inter_char_timeout:.3f} ms")
    print(f"Inter-frame timeout: {inter_frame_timeout:.3f} ms")
    
    return inter_frame_timeout / 1000  # Return in seconds

# Calculate for different baud rates
for baud in [9600, 19200, 38400, 115200]:
    timeout = calculate_rtu_timing(baud)
    print(f"Minimum timeout for {baud}: {timeout:.4f}s\n")

Multi-Slave Communication

class MultiSlaveRTU:
    """Communicate with multiple slaves on same bus."""
    
    def __init__(self, port, baudrate=9600):
        self.client = ModbusSerialClient(
            port=port,
            baudrate=baudrate,
            timeout=1
        )
        self.slaves = {}
    
    def add_slave(self, name, slave_id, description=""):
        """Register a slave device."""
        self.slaves[name] = {
            'id': slave_id,
            'description': description
        }
    
    def read_slave(self, name, address, count):
        """Read from named slave."""
        if name not in self.slaves:
            return None
        
        slave_id = self.slaves[name]['id']
        result = self.client.read_holding_registers(
            address, count, slave=slave_id
        )
        
        if result.isError():
            print(f"Error reading {name} (slave {slave_id}): {result}")
            return None
        
        return result.registers
    
    def scan_slaves(self, max_id=247):
        """Scan for responding slaves."""
        print("Scanning for slaves...")
        found = []
        
        for slave_id in range(1, max_id + 1):
            try:
                result = self.client.read_holding_registers(
                    0, 1, slave=slave_id
                )
                if not result.isError():
                    found.append(slave_id)
                    print(f"  Found slave: {slave_id}")
            except:
                pass
        
        return found

# Use multi-slave
rtu = MultiSlaveRTU('/dev/ttyUSB0')
rtu.client.connect()

# Register slaves
rtu.add_slave('temperature', 1, "Temperature sensor")
rtu.add_slave('pressure', 2, "Pressure sensor")
rtu.add_slave('flow', 3, "Flow meter")

# Read from each
temp = rtu.read_slave('temperature', 100, 1)
pressure = rtu.read_slave('pressure', 200, 1)
flow = rtu.read_slave('flow', 300, 2)

rtu.client.close()

RS485 Hardware Setup

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

# Linux: Set up RS485 mode
import fcntl
import struct
import serial

def enable_rs485(port_name):
    """Enable RS485 mode on Linux."""
    # RS485 constants
    TIOCGRS485 = 0x542E
    TIOCSRS485 = 0x542F
    SER_RS485_ENABLED = 0x01
    SER_RS485_RTS_ON_SEND = 0x02
    SER_RS485_RTS_AFTER_SEND = 0x04
    
    ser = serial.Serial(port_name)
    
    # Create RS485 config struct
    rs485_config = struct.pack(
        'IIIIII',
        SER_RS485_ENABLED | SER_RS485_RTS_ON_SEND,
        0,  # Delay RTS before send (ms)
        0,  # Delay RTS after send (ms)
        0, 0, 0  # Padding
    )
    
    # Apply config
    fcntl.ioctl(ser.fileno(), TIOCSRS485, rs485_config)
    ser.close()
    
    print(f"RS485 mode enabled on {port_name}")

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

Error Recovery

class RobustRTUClient:
    """RTU client with error recovery."""
    
    def __init__(self, port, **kwargs):
        self.port = port
        self.kwargs = kwargs
        self.client = None
        self.connect()
    
    def connect(self):
        """Connect with retry."""
        for attempt in range(3):
            try:
                self.client = ModbusSerialClient(
                    self.port, **self.kwargs
                )
                if self.client.connect():
                    print(f"Connected to {self.port}")
                    return True
            except Exception as e:
                print(f"Connection attempt {attempt + 1} failed: {e}")
                time.sleep(1)
        
        return False
    
    def read_with_retry(self, address, count, slave, max_retries=3):
        """Read with automatic retry."""
        for attempt in range(max_retries):
            try:
                result = self.client.read_holding_registers(
                    address, count, slave
                )
                
                if not result.isError():
                    return result.registers
                
                print(f"Read error (attempt {attempt + 1}): {result}")
                
            except Exception as e:
                print(f"Exception (attempt {attempt + 1}): {e}")
                # Try reconnecting
                self.client.close()
                time.sleep(0.5)
                self.connect()
        
        return None

# Use robust client
client = RobustRTUClient(
    '/dev/ttyUSB0',
    baudrate=9600,
    timeout=1
)

data = client.read_with_retry(0, 10, slave=1)
if data:
    print(f"Data: {data}")

RTU over TCP

Some devices support RTU protocol over TCP:

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

# RTU over TCP client
client = ModbusTcpClient(
    '192.168.1.100',
    port=502,
    framer=ModbusRtuFramer  # Use RTU framing
)

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

Troubleshooting RTU

Permission Issues (Linux)

# Add user to dialout group
sudo usermod -a -G dialout $USER
# Logout and login again

# Or temporary fix
sudo chmod 666 /dev/ttyUSB0

Test Serial Port

def test_serial_port(port, baudrate=9600):
    """Test if serial port works."""
    import serial
    
    try:
        ser = serial.Serial(
            port=port,
            baudrate=baudrate,
            timeout=1
        )
        
        print(f"✓ Opened {port} at {baudrate} baud")
        
        # Send test data
        ser.write(b'\x01\x03\x00\x00\x00\x01\x84\x0A')
        time.sleep(0.1)
        
        # Check for response
        if ser.in_waiting > 0:
            data = ser.read(ser.in_waiting)
            print(f"✓ Received {len(data)} bytes: {data.hex()}")
        else:
            print("⚠ No response (device may not be connected)")
        
        ser.close()
        return True
        
    except Exception as e:
        print(f"✗ Error: {e}")
        return False

# Test port
test_serial_port('/dev/ttyUSB0')

Wrong Settings Diagnosis

def diagnose_rtu_settings(port):
    """Try different settings to find correct configuration."""
    bauds = [9600, 19200, 38400, 57600, 115200]
    parities = ['N', 'E', 'O']
    
    for baud in bauds:
        for parity in parities:
            print(f"Trying {baud} baud, parity {parity}...")
            
            client = ModbusSerialClient(
                port=port,
                baudrate=baud,
                parity=parity,
                timeout=0.5
            )
            
            if client.connect():
                # Try reading from common slave IDs
                for slave in [1, 2, 247]:
                    result = client.read_holding_registers(0, 1, slave)
                    if not result.isError():
                        print(f"✓ SUCCESS: {baud},{parity} slave {slave}")
                        client.close()
                        return
                
                client.close()
    
    print("✗ No working configuration found")

# Diagnose
diagnose_rtu_settings('/dev/ttyUSB0')

ASCII Mode

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

# ASCII mode client (less common)
client = ModbusSerialClient(
    port='/dev/ttyUSB0',
    framer=ModbusAsciiFramer,
    baudrate=9600,
    timeout=3
)

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

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

Performance Tips

  1. Use highest reliable baud rate - Faster = better performance
  2. Minimize timeout - But not too low (causes errors)
  3. Batch reads - Read multiple registers at once
  4. Proper termination - Reduces errors and retries
  5. Quality cables - Shielded twisted pair for RS485

Next Steps

How is this guide?