Skip to content

Commit 94fbbb3

Browse files
authored
Combine ModbusClientProtocol and ModbusUdpClientProtocol. (#1054)
1 parent 085edf7 commit 94fbbb3

File tree

7 files changed

+161
-356
lines changed

7 files changed

+161
-356
lines changed

pymodbus/client/async_serial.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ async def run():
3434

3535
from serial_asyncio import create_serial_connection
3636

37-
from pymodbus.client.helper_async import ModbusClientProtocol
37+
from pymodbus.client.base import ModbusClientProtocol
3838
from pymodbus.transaction import ModbusRtuFramer
3939
from pymodbus.client.base import ModbusBaseClient
4040

pymodbus/client/async_tcp.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ async def run():
2929

3030
from pymodbus.transaction import ModbusSocketFramer
3131
from pymodbus.client.base import ModbusBaseClient
32-
from pymodbus.client.helper_async import ModbusClientProtocol
32+
from pymodbus.client.base import ModbusClientProtocol
3333

3434
_logger = logging.getLogger(__name__)
3535

pymodbus/client/async_tls.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ async def run():
3434
from pymodbus.client.async_tcp import AsyncModbusTcpClient
3535
from pymodbus.transaction import ModbusTlsFramer, FifoTransactionManager
3636
from pymodbus.client.helper_tls import sslctx_provider
37-
from pymodbus.client.helper_async import ModbusClientProtocol
37+
from pymodbus.client.base import ModbusClientProtocol
3838

3939
_logger = logging.getLogger(__name__)
4040

pymodbus/client/async_udp.py

Lines changed: 2 additions & 152 deletions
Original file line numberDiff line numberDiff line change
@@ -31,163 +31,13 @@ async def run():
3131

3232
from pymodbus.client.base import ModbusBaseClient
3333
from pymodbus.transaction import ModbusSocketFramer
34-
# from pymodbus.client.helper_async import ModbusClientProtocol
35-
from pymodbus.exceptions import ConnectionException
36-
from pymodbus.utilities import hexlify_packets
34+
from pymodbus.client.base import ModbusClientProtocol
3735

3836
_logger = logging.getLogger(__name__)
3937

4038
DGRAM_TYPE = socket.SOCK_DGRAM
4139

4240

43-
class ModbusUdpClientProtocol(ModbusBaseClient, asyncio.DatagramProtocol):
44-
"""Asyncio specific implementation of asynchronous modbus udp client protocol."""
45-
46-
#: Factory that created this instance.
47-
factory = None
48-
transport = None
49-
50-
def __init__(
51-
self,
52-
host="127.0.0.1",
53-
port=502,
54-
framer=None,
55-
source_address=None,
56-
timeout=10,
57-
**kwargs,
58-
):
59-
"""Initialize a Modbus TCP/UDP asynchronous client
60-
61-
:param host: Host IP address
62-
:param port: Port
63-
:param framer: Framer to use
64-
:param source_address: Specific to underlying client being used
65-
:param timeout: Timeout in seconds
66-
:param kwargs: Extra arguments
67-
"""
68-
self.host = host
69-
self.port = port
70-
self.source_address = source_address or ("", 0)
71-
self._timeout = timeout
72-
self._connected = False
73-
super().__init__(framer=framer or ModbusSocketFramer, **kwargs)
74-
75-
def datagram_received(self, data, addr):
76-
"""Receive datagram."""
77-
self._data_received(data)
78-
79-
def write_transport(self, packet):
80-
"""Write transport."""
81-
return self.transport.sendto(packet)
82-
83-
async def execute(self, request=None): # pylint: disable=invalid-overridden-method
84-
"""Execute requests asynchronously."""
85-
req = self._execute(request)
86-
if self.params.broadcast_enable and not request.unit_id:
87-
resp = b"Broadcast write sent - no response expected"
88-
else:
89-
resp = await asyncio.wait_for(req, timeout=self._timeout)
90-
return resp
91-
92-
def connection_made(self, transport):
93-
"""Call when a connection is made.
94-
95-
The transport argument is the transport representing the connection.
96-
"""
97-
self.transport = transport
98-
self._connection_made()
99-
100-
if self.factory:
101-
self.factory.protocol_made_connection(self) # pylint: disable=no-member
102-
103-
def connection_lost(self, reason):
104-
"""Call when the connection is lost or closed."""
105-
self.transport = None
106-
self._connection_lost(reason)
107-
108-
if self.factory:
109-
self.factory.protocol_lost_connection(self) # pylint: disable=no-member
110-
111-
def data_received(self, data):
112-
"""Call when some data is received."""
113-
self._data_received(data)
114-
115-
def create_future(self):
116-
"""Create asyncio Future object."""
117-
return asyncio.Future()
118-
119-
def resolve_future(self, my_future, result):
120-
"""Resolve future."""
121-
if not my_future.done():
122-
my_future.set_result(result)
123-
124-
def raise_future(self, my_future, exc):
125-
"""Set exception of a future if not done."""
126-
if not my_future.done():
127-
my_future.set_exception(exc)
128-
129-
def _connection_made(self):
130-
"""Call upon a successful client connection."""
131-
_logger.debug("Client connected to modbus server")
132-
self._connected = True
133-
134-
def _connection_lost(self, reason):
135-
"""Call upon a client disconnect."""
136-
txt = f"Client disconnected from modbus server: {reason}"
137-
_logger.debug(txt)
138-
self._connected = False
139-
for tid in list(self.transaction):
140-
self.raise_future(
141-
self.transaction.getTransaction(tid),
142-
ConnectionException("Connection lost during request"),
143-
)
144-
145-
@property
146-
def connected(self):
147-
"""Return connection status."""
148-
return self._connected
149-
150-
def _execute(self, request, **kwargs): # NOSONAR pylint: disable=unused-argument
151-
"""Start the producer to send the next request to consumer.write(Frame(request))."""
152-
request.transaction_id = self.transaction.getNextTID()
153-
packet = self.framer.buildPacket(request)
154-
txt = f"send: {hexlify_packets(packet)}"
155-
_logger.debug(txt)
156-
self.write_transport(packet)
157-
return self._build_response(request.transaction_id)
158-
159-
def _data_received(self, data):
160-
"""Get response, check for valid message, decode result."""
161-
txt = f"recv: {hexlify_packets(data)}"
162-
_logger.debug(txt)
163-
unit = self.framer.decode_data(data).get("unit", 0)
164-
self.framer.processIncomingPacket(data, self._handle_response, unit=unit)
165-
166-
def _handle_response(self, reply, **kwargs): # pylint: disable=unused-argument
167-
"""Handle the processed response and link to correct deferred."""
168-
if reply is not None:
169-
tid = reply.transaction_id
170-
if handler := self.transaction.getTransaction(tid):
171-
self.resolve_future(handler, reply)
172-
else:
173-
txt = f"Unrequested message: {str(reply)}"
174-
_logger.debug(txt)
175-
176-
def _build_response(self, tid):
177-
"""Return a deferred response for the current request."""
178-
my_future = self.create_future()
179-
if not self._connected:
180-
self.raise_future(my_future, ConnectionException("Client is not connected"))
181-
else:
182-
self.transaction.addTransaction(my_future, tid)
183-
return my_future
184-
185-
def close(self):
186-
"""Close."""
187-
self.transport.close()
188-
self._connected = False
189-
190-
19141
class AsyncModbusUdpClient(ModbusBaseClient):
19242
r"""Modbus client for async UDP communication.
19343
@@ -258,7 +108,7 @@ async def aClose(self):
258108

259109
def _create_protocol(self, host=None, port=0):
260110
"""Create initialized protocol instance with factory function."""
261-
protocol = ModbusUdpClientProtocol(
111+
protocol = ModbusClientProtocol(
262112
use_udp=True,
263113
framer=self.params.framer,
264114
**self.params.kwargs

pymodbus/client/base.py

Lines changed: 155 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,10 @@ def run():
3030
"""
3131
from __future__ import annotations
3232
from dataclasses import dataclass
33+
import asyncio
3334
import logging
3435

36+
from pymodbus.utilities import hexlify_packets
3537
from pymodbus.factory import ClientDecoder
3638
from pymodbus.utilities import ModbusTransactionState
3739
from pymodbus.transaction import DictTransactionManager
@@ -67,7 +69,7 @@ class ModbusBaseClient(ModbusClientMixin):
6769
"""
6870

6971
@dataclass
70-
class _params:
72+
class _params: # pylint: disable=too-many-instance-attributes
7173
"""Common parameters."""
7274

7375
host: str = None
@@ -242,3 +244,155 @@ def __str__(self):
242244
:returns: The string representation
243245
"""
244246
return f"{self.__class__.__name__} {self.params.host}:{self.params.port}"
247+
248+
249+
class ModbusClientProtocol(
250+
ModbusBaseClient,
251+
asyncio.Protocol,
252+
asyncio.DatagramProtocol,
253+
):
254+
"""Asyncio specific implementation of asynchronous modbus client protocol."""
255+
256+
#: Factory that created this instance.
257+
factory = None
258+
transport = None
259+
260+
def __init__(
261+
self,
262+
host="127.0.0.1",
263+
port=502,
264+
source_address=None,
265+
use_udp=False,
266+
**kwargs
267+
):
268+
"""Initialize a Modbus TCP/UDP asynchronous client"""
269+
super().__init__(**kwargs)
270+
self.use_udp = use_udp
271+
self.params.host = host
272+
self.params.port = port
273+
self.params.source_address = source_address or ("", 0)
274+
275+
self._connected = False
276+
277+
def datagram_received(self, data, addr):
278+
"""Receive datagram."""
279+
self._data_received(data)
280+
281+
async def execute(self, request=None): # pylint: disable=invalid-overridden-method
282+
"""Execute requests asynchronously."""
283+
req = self._execute(request)
284+
if self.params.broadcast_enable and not request.unit_id:
285+
resp = b"Broadcast write sent - no response expected"
286+
else:
287+
resp = await asyncio.wait_for(req, timeout=self.params.timeout)
288+
return resp
289+
290+
def connection_made(self, transport):
291+
"""Call when a connection is made.
292+
293+
The transport argument is the transport representing the connection.
294+
"""
295+
self.transport = transport
296+
self._connection_made()
297+
298+
if self.factory:
299+
self.factory.protocol_made_connection(self) # pylint: disable=no-member,useless-suppression
300+
301+
def connection_lost(self, reason):
302+
"""Call when the connection is lost or closed.
303+
304+
The argument is either an exception object or None
305+
"""
306+
self.transport = None
307+
self._connection_lost(reason)
308+
309+
if self.factory:
310+
self.factory.protocol_lost_connection(self) # pylint: disable=no-member,useless-suppression
311+
312+
def data_received(self, data):
313+
"""Call when some data is received.
314+
315+
data is a non-empty bytes object containing the incoming data.
316+
"""
317+
self._data_received(data)
318+
319+
def create_future(self):
320+
"""Help function to create asyncio Future object."""
321+
return asyncio.Future()
322+
323+
def resolve_future(self, my_future, result):
324+
"""Resolve the completed future and sets the result."""
325+
if not my_future.done():
326+
my_future.set_result(result)
327+
328+
def raise_future(self, my_future, exc):
329+
"""Set exception of a future if not done."""
330+
if not my_future.done():
331+
my_future.set_exception(exc)
332+
333+
def _connection_made(self):
334+
"""Call upon a successful client connection."""
335+
_logger.debug("Client connected to modbus server")
336+
self._connected = True
337+
338+
def _connection_lost(self, reason):
339+
"""Call upon a client disconnect."""
340+
txt = f"Client disconnected from modbus server: {reason}"
341+
_logger.debug(txt)
342+
self._connected = False
343+
for tid in list(self.transaction):
344+
self.raise_future(
345+
self.transaction.getTransaction(tid),
346+
ConnectionException("Connection lost during request"),
347+
)
348+
349+
@property
350+
def connected(self):
351+
"""Return connection status."""
352+
return self._connected
353+
354+
def write_transport(self, packet):
355+
"""Write transport."""
356+
if self.use_udp:
357+
return self.transport.sendto(packet)
358+
return self.transport.write(packet)
359+
360+
def _execute(self, request, **kwargs): # pylint: disable=unused-argument
361+
"""Start the producer to send the next request to consumer.write(Frame(request))."""
362+
request.transaction_id = self.transaction.getNextTID()
363+
packet = self.framer.buildPacket(request)
364+
txt = f"send: {hexlify_packets(packet)}"
365+
_logger.debug(txt)
366+
self.write_transport(packet)
367+
return self._build_response(request.transaction_id)
368+
369+
def _data_received(self, data):
370+
"""Get response, check for valid message, decode result."""
371+
txt = f"recv: {hexlify_packets(data)}"
372+
_logger.debug(txt)
373+
unit = self.framer.decode_data(data).get("unit", 0)
374+
self.framer.processIncomingPacket(data, self._handle_response, unit=unit)
375+
376+
def _handle_response(self, reply, **kwargs): # pylint: disable=unused-argument
377+
"""Handle the processed response and link to correct deferred."""
378+
if reply is not None:
379+
tid = reply.transaction_id
380+
if handler := self.transaction.getTransaction(tid):
381+
self.resolve_future(handler, reply)
382+
else:
383+
txt = f"Unrequested message: {str(reply)}"
384+
_logger.debug(txt)
385+
386+
def _build_response(self, tid):
387+
"""Return a deferred response for the current request."""
388+
my_future = self.create_future()
389+
if not self._connected:
390+
self.raise_future(my_future, ConnectionException("Client is not connected"))
391+
else:
392+
self.transaction.addTransaction(my_future, tid)
393+
return my_future
394+
395+
async def aClose(self):
396+
"""Close."""
397+
self.transport.close()
398+
self._connected = False

0 commit comments

Comments
 (0)