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/ttyUSB0Test 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