Skip to content

Commit 2df9599

Browse files
authored
Transport integrated in async clients. (#1541)
1 parent c19b503 commit 2df9599

File tree

17 files changed

+1246
-337
lines changed

17 files changed

+1246
-337
lines changed

examples/client_async.py

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,21 @@ async def run_async_client(client, modbus_calls=None):
131131

132132
async def helper():
133133
"""Combine the setup and run"""
134-
testclient = setup_async_client(description="Run asynchronous client.")
134+
args = [
135+
"--comm",
136+
"udp",
137+
"--host",
138+
"127.0.0.1",
139+
"--port",
140+
"5020",
141+
"--framer",
142+
"socket",
143+
"--log",
144+
"debug",
145+
]
146+
testclient = setup_async_client(
147+
description="Run asynchronous client.", cmdline=args
148+
)
135149
await run_async_client(testclient)
136150

137151

pymodbus/client/base.py

Lines changed: 23 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88

99
from pymodbus.client.mixin import ModbusClientMixin
1010
from pymodbus.constants import Defaults
11-
from pymodbus.exceptions import ConnectionException, NotImplementedException
11+
from pymodbus.exceptions import ConnectionException
1212
from pymodbus.factory import ClientDecoder
1313
from pymodbus.framer import ModbusFramer
1414
from pymodbus.logging import Log
@@ -95,8 +95,16 @@ def __init__( # pylint: disable=too-many-arguments
9595
) -> None:
9696
"""Initialize a client instance."""
9797
BaseTransport.__init__(
98-
self, "comm", framer, reconnect_delay, reconnect_delay_max, timeout, timeout
98+
self,
99+
"comm",
100+
(reconnect_delay * 1000, reconnect_delay_max * 1000),
101+
timeout * 1000,
102+
framer,
103+
lambda: None,
104+
self.cb_base_connection_lost,
105+
self.cb_base_handle_data,
99106
)
107+
self.framer = framer
100108
self.params = self._params()
101109
self.params.framer = framer
102110
self.params.timeout = float(timeout)
@@ -122,12 +130,11 @@ def __init__( # pylint: disable=too-many-arguments
122130
)
123131
self.reconnect_delay = self.params.reconnect_delay
124132
self.reconnect_delay_current = self.params.reconnect_delay
125-
self.use_protocol = False
133+
self.use_sync = False
126134
self.use_udp = False
127135
self.state = ModbusTransactionState.IDLE
128136
self.last_frame_end: float = 0
129137
self.silent_interval: float = 0
130-
self._reconnect_task: asyncio.Task = None
131138

132139
# Initialize mixin
133140
ModbusClientMixin.__init__(self)
@@ -146,10 +153,6 @@ def register(self, custom_response_class: ModbusResponse) -> None:
146153
"""
147154
self.framer.decoder.register(custom_response_class)
148155

149-
def is_socket_open(self) -> bool:
150-
"""Return whether socket/serial is open or not (call **sync**)."""
151-
raise NotImplementedException
152-
153156
def idle_time(self) -> float:
154157
"""Time before initiating next transaction (call **sync**).
155158
@@ -167,13 +170,13 @@ def execute(self, request: ModbusRequest = None) -> ModbusResponse:
167170
:returns: The result of the request execution
168171
:raises ConnectionException: Check exception text.
169172
"""
170-
if self.use_protocol:
171-
if not self.transport:
172-
raise ConnectionException(f"Not connected[{str(self)}]")
173-
return self.async_execute(request)
174-
if not self.connect():
175-
raise ConnectionException(f"Failed to connect[{str(self)}]")
176-
return self.transaction.execute(request)
173+
if self.use_sync:
174+
if not self.connect():
175+
raise ConnectionException(f"Failed to connect[{str(self)}]")
176+
return self.transaction.execute(request)
177+
if not self.transport:
178+
raise ConnectionException(f"Not connected[{str(self)}]")
179+
return self.async_execute(request)
177180

178181
# ----------------------------------------------------------------------- #
179182
# Merged client methods
@@ -198,24 +201,16 @@ async def async_execute(self, request=None):
198201
raise
199202
return resp
200203

201-
def data_received(self, data):
202-
"""Call when some data is received.
203-
204-
data is a non-empty bytes object containing the incoming data.
205-
"""
206-
Log.debug("recv: {}", data, ":hex")
207-
self.framer.processIncomingPacket(data, self._handle_response, slave=0)
208-
209-
def cb_handle_data(self, _data: bytes) -> int:
204+
def cb_base_handle_data(self, data: bytes) -> int:
210205
"""Handle received data
211206
212207
returns number of bytes consumed
213208
"""
209+
Log.debug("recv: {}", data, ":hex")
210+
self.framer.processIncomingPacket(data, self._handle_response, slave=0)
211+
return len(data)
214212

215-
def cb_connection_made(self) -> None:
216-
"""Handle new connection"""
217-
218-
def cb_connection_lost(self, _reason: Exception) -> None:
213+
def cb_base_connection_lost(self, _reason: Exception) -> None:
219214
"""Handle lost connection"""
220215
for tid in list(self.transaction):
221216
self.raise_future(

pymodbus/client/serial.py

Lines changed: 16 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
from typing import Any, Type
77

88
from pymodbus.client.base import ModbusBaseClient
9-
from pymodbus.client.serial_asyncio import create_serial_connection
109
from pymodbus.constants import Defaults
1110
from pymodbus.exceptions import ConnectionException
1211
from pymodbus.framer import ModbusFramer
@@ -60,18 +59,15 @@ def __init__(
6059
**kwargs: Any,
6160
) -> None:
6261
"""Initialize Asyncio Modbus Serial Client."""
63-
super().__init__(framer=framer, **kwargs)
64-
self.use_protocol = True
62+
asyncio.Protocol.__init__(self)
63+
ModbusBaseClient.__init__(self, framer=framer, **kwargs)
6564
self.params.port = port
6665
self.params.baudrate = baudrate
6766
self.params.bytesize = bytesize
6867
self.params.parity = parity
6968
self.params.stopbits = stopbits
7069
self.params.handle_local_echo = handle_local_echo
71-
72-
def _create_protocol(self):
73-
"""Create a protocol instance."""
74-
return self
70+
self.setup_serial(False, port, baudrate, bytesize, parity, stopbits)
7571

7672
@property
7773
def connected(self):
@@ -80,29 +76,13 @@ def connected(self):
8076

8177
async def connect(self):
8278
"""Connect Async client."""
83-
# get current loop, if there are no loop a RuntimeError will be raised
84-
Log.debug("Starting serial connection")
85-
try:
86-
await asyncio.wait_for(
87-
create_serial_connection(
88-
self.loop,
89-
self._create_protocol,
90-
self.params.port,
91-
baudrate=self.params.baudrate,
92-
bytesize=self.params.bytesize,
93-
stopbits=self.params.stopbits,
94-
parity=self.params.parity,
95-
timeout=self.params.timeout,
96-
**self.params.kwargs,
97-
),
98-
timeout=self.params.timeout,
99-
)
100-
Log.info("Connected to {}", self.params.port)
101-
except Exception as exc: # pylint: disable=broad-except
102-
Log.warning("Failed to connect: {}", exc)
103-
self.close(reconnect=True)
79+
# if reconnect_delay_current was set to 0 by close(), we need to set it back again
80+
# so this instance will work
10481
self.reset_delay()
105-
return self.connected
82+
83+
# force reconnect if required:
84+
Log.debug("Connecting to {}.", self.params.host)
85+
return await self.transport_connect()
10686

10787

10888
class ModbusSerialClient(ModbusBaseClient):
@@ -158,6 +138,7 @@ def __init__(
158138
self.params.stopbits = stopbits
159139
self.params.handle_local_echo = handle_local_echo
160140
self.socket = None
141+
self.use_sync = True
161142

162143
self.last_frame_end = None
163144

@@ -173,13 +154,12 @@ def __init__(
173154
else 0.05
174155
)
175156

176-
if isinstance(self.framer, ModbusRtuFramer):
177-
if self.params.baudrate > 19200:
178-
self.silent_interval = 1.75 / 1000 # ms
179-
else:
180-
self.inter_char_timeout = 1.5 * self._t0
181-
self.silent_interval = 3.5 * self._t0
182-
self.silent_interval = round(self.silent_interval, 6)
157+
if self.params.baudrate > 19200:
158+
self.silent_interval = 1.75 / 1000 # ms
159+
else:
160+
self.inter_char_timeout = 1.5 * self._t0
161+
self.silent_interval = 3.5 * self._t0
162+
self.silent_interval = round(self.silent_interval, 6)
183163

184164
@property
185165
def connected(self):

pymodbus/client/tcp.py

Lines changed: 10 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -46,11 +46,17 @@ def __init__(
4646
**kwargs: Any,
4747
) -> None:
4848
"""Initialize Asyncio Modbus TCP Client."""
49-
super().__init__(framer=framer, **kwargs)
50-
self.use_protocol = True
49+
asyncio.Protocol.__init__(self)
50+
ModbusBaseClient.__init__(self, framer=framer, **kwargs)
5151
self.params.host = host
5252
self.params.port = port
5353
self.params.source_address = source_address
54+
if "internal_no_setup" in kwargs:
55+
return
56+
if host.startswith("unix:"):
57+
self.setup_unix(False, host[5:])
58+
else:
59+
self.setup_tcp(False, host, port)
5460

5561
async def connect(self):
5662
"""Initiate connection to start client."""
@@ -61,45 +67,13 @@ async def connect(self):
6167

6268
# force reconnect if required:
6369
Log.debug("Connecting to {}:{}.", self.params.host, self.params.port)
64-
return await self._connect()
70+
return await self.transport_connect()
6571

6672
@property
6773
def connected(self):
6874
"""Return true if connected."""
6975
return self.transport is not None
7076

71-
def _create_protocol(self):
72-
"""Create initialized protocol instance with function."""
73-
return self
74-
75-
async def _connect(self):
76-
"""Connect."""
77-
Log.debug("Connecting.")
78-
try:
79-
if self.params.host.startswith("unix:"):
80-
transport, protocol = await asyncio.wait_for(
81-
self.loop.create_unix_connection(
82-
self._create_protocol, path=self.params.host[5:]
83-
),
84-
timeout=self.params.timeout,
85-
)
86-
else:
87-
transport, protocol = await asyncio.wait_for(
88-
self.loop.create_connection(
89-
self._create_protocol,
90-
host=self.params.host,
91-
port=self.params.port,
92-
),
93-
timeout=self.params.timeout,
94-
)
95-
except Exception as exc: # pylint: disable=broad-except
96-
Log.warning("Failed to connect: {}", exc)
97-
self.close(reconnect=True)
98-
else:
99-
Log.info("Connected to {}:{}.", self.params.host, self.params.port)
100-
self.reset_delay()
101-
return transport, protocol
102-
10377

10478
class ModbusTcpClient(ModbusBaseClient):
10579
"""**ModbusTcpClient**.
@@ -140,6 +114,7 @@ def __init__(
140114
self.params.port = port
141115
self.params.source_address = source_address
142116
self.socket = None
117+
self.use_sync = True
143118

144119
@property
145120
def connected(self):

pymodbus/client/tls.py

Lines changed: 17 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
"""Modbus client async TLS communication."""
2-
import asyncio
32
import socket
43
import ssl
54
from typing import Any, Type
@@ -40,7 +39,7 @@ def sslctx_provider(
4039
return sslctx
4140

4241

43-
class AsyncModbusTlsClient(AsyncModbusTcpClient, asyncio.Protocol):
42+
class AsyncModbusTlsClient(AsyncModbusTcpClient):
4443
"""**AsyncModbusTlsClient**.
4544
4645
:param host: Host IP address or host name
@@ -74,41 +73,37 @@ def __init__(
7473
host: str,
7574
port: int = Defaults.TlsPort,
7675
framer: Type[ModbusFramer] = ModbusTlsFramer,
77-
sslctx: str = None,
76+
sslctx: ssl.SSLContext = None,
7877
certfile: str = None,
7978
keyfile: str = None,
8079
password: str = None,
8180
server_hostname: str = None,
8281
**kwargs: Any,
8382
):
8483
"""Initialize Asyncio Modbus TLS Client."""
85-
super().__init__(host, port=port, framer=framer, **kwargs)
84+
AsyncModbusTcpClient.__init__(
85+
self, host, port=port, framer=framer, internal_no_setup=True, **kwargs
86+
)
8687
self.sslctx = sslctx_provider(sslctx, certfile, keyfile, password)
87-
self.params.sslctx = sslctx
8888
self.params.certfile = certfile
8989
self.params.keyfile = keyfile
9090
self.params.password = password
9191
self.params.server_hostname = server_hostname
92-
AsyncModbusTcpClient.__init__(self, host, port=port, framer=framer, **kwargs)
92+
self.setup_tls(
93+
False, host, port, sslctx, certfile, keyfile, password, server_hostname
94+
)
9395

94-
async def _connect(self):
95-
"""Connect to server."""
96-
Log.debug("Connecting tls.")
97-
try:
98-
return await self.loop.create_connection(
99-
self._create_protocol,
100-
self.params.host,
101-
self.params.port,
102-
ssl=self.sslctx,
103-
server_hostname=self.params.server_hostname,
104-
)
105-
except Exception as exc: # pylint: disable=broad-except
106-
Log.warning("Failed to connect: {}", exc)
107-
self.close(reconnect=True)
108-
return
109-
Log.info("Connected to {}:{}.", self.params.host, self.params.port)
96+
async def connect(self):
97+
"""Initiate connection to start client."""
98+
99+
# if reconnect_delay_current was set to 0 by close(), we need to set it back again
100+
# so this instance will work
110101
self.reset_delay()
111102

103+
# force reconnect if required:
104+
Log.debug("Connecting to {}:{}.", self.params.host, self.params.port)
105+
return await self.transport_connect()
106+
112107

113108
class ModbusTlsClient(ModbusTcpClient):
114109
"""**ModbusTlsClient**.

0 commit comments

Comments
 (0)