PyModbus
PyModbusDocs

Setting up Modbus RTU Client

Configure Modbus RTU over serial RS485 and RS232 with PyModbus. Port settings, timing, wiring diagrams, and 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