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 inTest 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
- Use the highest reliable baud rate your wiring supports
- Batch register reads instead of reading one at a time
- Use proper termination (120 ohm) to reduce CRC errors and retries
- Use shielded twisted pair cable for RS485
- Set a short but safe timeout (too low causes false timeouts, too high wastes time)