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
- Use highest reliable baud rate - Faster = better performance
- Minimize timeout - But not too low (causes errors)
- Batch reads - Read multiple registers at once
- Proper termination - Reduces errors and retries
- Quality cables - Shielded twisted pair for RS485
Next Steps
- Serial Communication - RS485/RS232 setup
- Async Client - Async serial operations
- Troubleshooting - Fix connection issues
How is this guide?