Async Client
Use PyModbus with asyncio for concurrent Modbus reads across multiple devices, parallel operations, and non-blocking communication.
Async Modbus Client
PyModbus supports Python's asyncio for non-blocking Modbus communication. You get concurrent reads across multiple devices without threads.
Connect and Read
import asyncio
from pymodbus.client import AsyncModbusTcpClient
async def read_data():
async with AsyncModbusTcpClient('192.168.1.100') as client:
result = await client.read_holding_registers(0, 10, slave=1)
if not result.isError():
print(f"Data: {result.registers}")
return result.registers
data = asyncio.run(read_data())Log Modbus data automatically
TofuPilot records test results from your PyModbus scripts, tracks pass/fail rates, and generates compliance reports. Free to start.
Parallel Reads with gather
Read from multiple devices at the same time. Each device gets its own connection and the reads run concurrently.
import asyncio
from pymodbus.client import AsyncModbusTcpClient
async def read_device(host, slave):
async with AsyncModbusTcpClient(host) as client:
result = await client.read_holding_registers(0, 10, slave)
return host, result.registers if not result.isError() else None
async def read_multiple_devices():
devices = [
('192.168.1.10', 1),
('192.168.1.11', 1),
('192.168.1.12', 1),
]
tasks = [read_device(host, slave) for host, slave in devices]
results = await asyncio.gather(*tasks)
for host, data in results:
print(f"{host}: {data}")
return results
asyncio.run(read_multiple_devices())Async Serial RTU
Async works with serial connections too. Useful when polling multiple slaves on the same bus.
import asyncio
from pymodbus.client import AsyncModbusSerialClient
async def async_rtu_example():
client = AsyncModbusSerialClient(
port='/dev/ttyUSB0',
baudrate=9600,
timeout=1
)
await client.connect()
for slave_id in range(1, 5):
result = await client.read_holding_registers(0, 10, slave=slave_id)
if not result.isError():
print(f"Slave {slave_id}: {result.registers}")
client.close()
asyncio.run(async_rtu_example())Async Context Manager
The AsyncModbusTcpClient context manager handles connect and close for you. Prefer this pattern to avoid leaked connections.
import asyncio
from pymodbus.client import AsyncModbusTcpClient
async def safe_read(host, address, count):
async with AsyncModbusTcpClient(host, timeout=5) as client:
result = await client.read_holding_registers(address, count)
if not result.isError():
return result.registers
return None
asyncio.run(safe_read('192.168.1.100', 0, 10))Retry with Timeout
Combine asyncio.wait_for with a retry loop for unreliable connections.
import asyncio
from pymodbus.client import AsyncModbusTcpClient
async def read_with_retry(host, address, count, max_retries=3):
for attempt in range(max_retries):
try:
async with AsyncModbusTcpClient(host, timeout=5) as client:
result = await asyncio.wait_for(
client.read_holding_registers(address, count),
timeout=3.0
)
if not result.isError():
return result.registers
print(f"Attempt {attempt + 1} error: {result}")
except asyncio.TimeoutError:
print(f"Attempt {attempt + 1} timeout")
except Exception as e:
print(f"Attempt {attempt + 1} exception: {e}")
if attempt < max_retries - 1:
await asyncio.sleep(1)
return NonePerformance Comparison
Async shines when you're reading from multiple devices or making many requests through a single connection.
import asyncio
import time
from pymodbus.client import AsyncModbusTcpClient, ModbusTcpClient
async def benchmark_async(host, num_requests=100):
start = time.time()
async with AsyncModbusTcpClient(host) as client:
tasks = [
client.read_holding_registers(0, 10)
for _ in range(num_requests)
]
results = await asyncio.gather(*tasks)
elapsed = time.time() - start
successful = sum(1 for r in results if not r.isError())
print(f"Async: {num_requests} requests in {elapsed:.2f}s ({num_requests/elapsed:.1f} req/s, {successful} ok)")
def benchmark_sync(host, num_requests=100):
start = time.time()
client = ModbusTcpClient(host)
client.connect()
successful = 0
for _ in range(num_requests):
result = client.read_holding_registers(0, 10)
if not result.isError():
successful += 1
client.close()
elapsed = time.time() - start
print(f"Sync: {num_requests} requests in {elapsed:.2f}s ({num_requests/elapsed:.1f} req/s, {successful} ok)")
asyncio.run(benchmark_async('192.168.1.100'))
benchmark_sync('192.168.1.100')Async is most useful when polling multiple devices, reading at high frequency, or integrating Modbus into an async application (web server, event loop).