Skip to content

Commit a5358df

Browse files
committed
Merge branch 'dev'
2 parents 1b12541 + efbc6ec commit a5358df

18 files changed

+798
-845
lines changed

API_changes.rst

+6
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,12 @@
22
PyModbus - API changes.
33
=======================
44

5+
-------------
6+
Version 3.3.1
7+
-------------
8+
9+
No changes.
10+
511
-------------
612
Version 3.3.0
713
-------------

CHANGELOG.rst

+13
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,16 @@
1+
version 3.3.1
2+
----------------------------------------------------------
3+
* transport fixes and 100% test coverage. (#1580)
4+
* Delay self.loop until connect(). (#1579)
5+
* Added mechanism to determine if server did not start cleanly (#1539)
6+
* Proof transport reconnect works. (#1577)
7+
* Fix non-shared block doc in config.rst. (#1573)
8+
9+
Thanks to:
10+
Hayden Roche
11+
jan iversen
12+
Philip Couling
13+
114
version 3.3.0
215
----------------------------------------------------------
316
* Stabilize windows tests. (#1567)

MAKE_RELEASE.rst

+6-6
Original file line numberDiff line numberDiff line change
@@ -8,21 +8,21 @@ Making a release.
88
------------------------------------------------------------
99
Prepare/make release on dev.
1010
------------------------------------------------------------
11-
* Make pull request "prepare v3.3.x", with the following:
11+
* Make pull request "prepare v3.4.x", with the following:
1212
* Update pymodbus/__init__.py with version number (__version__ X.Y.Zpre)
1313
* Update README.rst "Supported versions"
1414
* Update CHANGELOG.rst
1515
* Add commits from last release, but selectively !
16-
git log --oneline v3.2.2..HEAD > commit.log
17-
git log v3.2.2..HEAD | grep Author > contributors.log
16+
git log --oneline v3.3.0..HEAD > commit.log
17+
git log v3.3.0..HEAD | grep Author > contributors.log
1818
* Commit, push and merge.
1919
* Checkout master locally
2020
* git merge dev
2121
* git push
2222
* wait for CI to complete on all branches
2323
* On github "prepare release"
24-
* Create tag e.g. v3.0.1dev0
25-
* Title "pymodbus v3.0.1dev0"
24+
* Create tag e.g. v3.4.0dev0
25+
* Title "pymodbus v3.4.0dev0"
2626
* do NOT generate release notes, but copy from CHANGELOG.rst
2727
* make release (remember to mark pre-release if so)
2828
* on local repo
@@ -40,4 +40,4 @@ Prepare release on dev for new commits.
4040
------------------------------------------------------------
4141
* git branch -D master
4242
* Make pull request "prepare dev", with the following:
43-
* Update pymodbus/version.py with version number (last line)
43+
* Update pymodbus/__init__.py with version number (__version__ X.Y.Zpre)

README.rst

+1-1
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ Supported versions
2222

2323
Version `2.5.3 <https://github.com/pymodbus-dev/pymodbus/releases/tag/v2.5.3>`_ is the last 2.x release (Supports python >= 2.7, no longer supported).
2424

25-
Version `3.3.0 <https://github.com/pymodbus-dev/pymodbus/releases/tag/v3.3.0>`_ is the current release (Supports Python >= 3.8).
25+
Version `3.3.0 <https://github.com/pymodbus-dev/pymodbus/releases/tag/v3.3.1>`_ is the current release (Supports Python >= 3.8).
2626

2727
.. important::
2828
All API changes after 3.0.0 are documented in `API_changes.rst <https://github.com/pymodbus-dev/pymodbus/blob/dev/API_changes.rst>`_

doc/source/library/simulator/config.rst

+2-2
Original file line numberDiff line numberDiff line change
@@ -258,8 +258,8 @@ Example "setup" configuration:
258258
assuming all sizes are set to 10, the addresses for configuration are as follows:
259259
- coils have addresses 0-9,
260260
- discrete_inputs have addresses 10-19,
261-
- holding_registers have addresses 20-29,
262-
- input_registers have addresses 30-39
261+
- input_registers have addresses 20-29,
262+
- holding_registers have addresses 30-39
263263

264264
when configuring the the datatypes (when calling each block start with 0).
265265

examples/client_async.py

-1
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,6 @@ async def run_async_client(client, modbus_calls=None):
121121
"""Run sync client."""
122122
_logger.info("### Client starting")
123123
await client.connect()
124-
print("jan " + str(client.connected))
125124
assert client.connected
126125
if modbus_calls:
127126
await modbus_calls(client)

pymodbus/__init__.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -12,5 +12,5 @@
1212
from pymodbus.logging import pymodbus_apply_logging_config
1313

1414

15-
__version__ = "3.3.0"
15+
__version__ = "3.3.1"
1616
__version_full__ = f"[pymodbus, version {__version__}]"

pymodbus/client/base.py

+5-5
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,11 @@
1414
from pymodbus.logging import Log
1515
from pymodbus.pdu import ModbusRequest, ModbusResponse
1616
from pymodbus.transaction import DictTransactionManager
17-
from pymodbus.transport import BaseTransport
17+
from pymodbus.transport.transport import Transport
1818
from pymodbus.utilities import ModbusTransactionState
1919

2020

21-
class ModbusBaseClient(ModbusClientMixin, BaseTransport):
21+
class ModbusBaseClient(ModbusClientMixin, Transport):
2222
"""**ModbusBaseClient**
2323
2424
**Parameters common to all clients**:
@@ -94,12 +94,12 @@ def __init__( # pylint: disable=too-many-arguments
9494
**kwargs: Any,
9595
) -> None:
9696
"""Initialize a client instance."""
97-
BaseTransport.__init__(
97+
Transport.__init__(
9898
self,
9999
"comm",
100-
(reconnect_delay * 1000, reconnect_delay_max * 1000),
100+
reconnect_delay * 1000,
101+
reconnect_delay_max * 1000,
101102
timeout * 1000,
102-
framer,
103103
lambda: None,
104104
self.cb_base_connection_lost,
105105
self.cb_base_handle_data,

pymodbus/server/async_io.py

+10
Original file line numberDiff line numberDiff line change
@@ -534,6 +534,7 @@ def __init__(
534534

535535
# asyncio future that will be done once server has started
536536
self.serving = asyncio.Future()
537+
self.serving_done = asyncio.Future()
537538
# constructors cannot be declared async, so we have to
538539
# defer the initialization of the server
539540
self.server = None
@@ -552,13 +553,15 @@ async def serve_forever(self):
552553
Log.info("Server(Unix) listening.")
553554
await self.server.serve_forever()
554555
except asyncio.exceptions.CancelledError:
556+
self.serving_done.set_result(True)
555557
raise
556558
except Exception as exc: # pylint: disable=broad-except
557559
Log.error("Server unexpected exception {}", exc)
558560
else:
559561
raise RuntimeError(
560562
"Can't call serve_forever on an already running server object"
561563
)
564+
self.serving_done.set_result(True)
562565
Log.info("Server graceful shutdown.")
563566

564567
async def shutdown(self):
@@ -641,6 +644,7 @@ def __init__(
641644

642645
# asyncio future that will be done once server has started
643646
self.serving = asyncio.Future()
647+
self.serving_done = asyncio.Future()
644648
# constructors cannot be declared async, so we have to
645649
# defer the initialization of the server
646650
self.server = None
@@ -663,13 +667,15 @@ async def serve_forever(self):
663667
try:
664668
await self.server.serve_forever()
665669
except asyncio.exceptions.CancelledError:
670+
self.serving_done.set_result(False)
666671
raise
667672
except Exception as exc: # pylint: disable=broad-except
668673
Log.error("Server unexpected exception {}", exc)
669674
else:
670675
raise RuntimeError(
671676
"Can't call serve_forever on an already running server object"
672677
)
678+
self.serving_done.set_result(True)
673679
Log.info("Server graceful shutdown.")
674680

675681
async def shutdown(self):
@@ -821,6 +827,7 @@ def __init__(
821827
self.stop_serving = self.loop.create_future()
822828
# asyncio future that will be done once server has started
823829
self.serving = asyncio.Future()
830+
self.serving_done = asyncio.Future()
824831
self.factory_parms = {
825832
"local_addr": self.address,
826833
"allow_broadcast": True,
@@ -836,9 +843,11 @@ async def serve_forever(self):
836843
**self.factory_parms,
837844
)
838845
except asyncio.exceptions.CancelledError:
846+
self.serving_done.set_result(False)
839847
raise
840848
except Exception as exc:
841849
Log.error("Server unexpected exception {}", exc)
850+
self.serving_done.set_result(False)
842851
raise RuntimeError(exc) from exc
843852
Log.info("Server(UDP) listening.")
844853
self.serving.set_result(True)
@@ -847,6 +856,7 @@ async def serve_forever(self):
847856
raise RuntimeError(
848857
"Can't call serve_forever on an already running server object"
849858
)
859+
self.serving_done.set_result(True)
850860

851861
async def shutdown(self):
852862
"""Shutdown server."""

pymodbus/transport/__init__.py

-6
Original file line numberDiff line numberDiff line change
@@ -1,7 +1 @@
11
"""Transport."""
2-
3-
__all__ = [
4-
"BaseTransport",
5-
]
6-
7-
from pymodbus.transport.transport import BaseTransport

pymodbus/transport/transport.py

+55-41
Original file line numberDiff line numberDiff line change
@@ -10,17 +10,22 @@
1010
from dataclasses import dataclass
1111
from typing import Any, Callable, Coroutine
1212

13-
from pymodbus.framer import ModbusFramer
1413
from pymodbus.logging import Log
1514
from pymodbus.transport.serial_asyncio import create_serial_connection
1615

1716

18-
class BaseTransport:
19-
"""Base class for transport types.
17+
class Transport:
18+
"""Transport layer.
2019
21-
BaseTransport contains functions common to all transport types and client/server.
20+
Contains pure transport methods needed to connect/listen, send/receive and close connections
21+
for unix socket, tcp, tls and serial communications.
2222
23-
This class is not available in the pymodbus API, and should not be referenced in Applications.
23+
Contains high level methods like reconnect.
24+
25+
This class is not available in the pymodbus API, and should not be referenced in Applications
26+
nor in the pymodbus documentation.
27+
28+
The class is designed to be an object in the message level class.
2429
"""
2530

2631
@dataclass
@@ -33,7 +38,6 @@ class CommParamsClass:
3338
reconnect_delay: float = None
3439
reconnect_delay_max: float = None
3540
timeout_connect: float = None
36-
framer: ModbusFramer = None
3741

3842
# tcp / tls / udp / serial
3943
host: str = None
@@ -60,19 +64,19 @@ def check_done(self):
6064
def __init__(
6165
self,
6266
comm_name: str,
63-
reconnect_delay: tuple[int, int],
67+
reconnect_delay: int,
68+
reconnect_max: int,
6469
timeout_connect: int,
65-
framer: ModbusFramer,
6670
callback_connected: Callable[[], None],
6771
callback_disconnected: Callable[[Exception], None],
6872
callback_data: Callable[[bytes], int],
6973
) -> None:
7074
"""Initialize a transport instance.
7175
7276
:param comm_name: name of this transport connection
73-
:param reconnect_delay: delay and max in milliseconds for first reconnect (0,0 for no reconnect)
77+
:param reconnect_delay: delay in milliseconds for first reconnect (0 for no reconnect)
78+
:param reconnect_delay: max reconnect delay in milliseconds
7479
:param timeout_connect: Max. time in milliseconds for connect to complete
75-
:param framer: Modbus framer to decode/encode messagees.
7680
:param callback_connected: Called when connection is established
7781
:param callback_disconnected: Called when connection is disconnected
7882
:param callback_data: Called when data is received
@@ -84,25 +88,25 @@ def __init__(
8488
# properties, can be read, but may not be mingled with
8589
self.comm_params = self.CommParamsClass(
8690
comm_name=comm_name,
87-
reconnect_delay=reconnect_delay[0] / 1000,
88-
reconnect_delay_max=reconnect_delay[1] / 1000,
91+
reconnect_delay=reconnect_delay / 1000,
92+
reconnect_delay_max=reconnect_max / 1000,
8993
timeout_connect=timeout_connect / 1000,
90-
framer=framer,
9194
)
9295

93-
self.reconnect_delay_current: float = 0
96+
self.reconnect_delay_current: float = 0.0
9497
self.transport: asyncio.BaseTransport | asyncio.Server = None
9598
self.protocol: asyncio.BaseProtocol = None
99+
self.loop: asyncio.AbstractEventLoop = None
96100
with suppress(RuntimeError):
97-
self.loop: asyncio.AbstractEventLoop = asyncio.get_running_loop()
98-
self.reconnect_timer: asyncio.TimerHandle = None
101+
self.loop = asyncio.get_running_loop()
102+
self.reconnect_task: asyncio.Task = None
99103
self.recv_buffer: bytes = b""
100104
self.call_connect_listen: Callable[[], Coroutine[Any, Any, Any]] = lambda: None
101105
self.use_udp = False
102106

103-
# ----------------------------- #
104-
# Transport specific parameters #
105-
# ----------------------------- #
107+
# ------------------------ #
108+
# Transport specific setup #
109+
# ------------------------ #
106110
def setup_unix(self, setup_server: bool, host: str):
107111
"""Prepare transport unix"""
108112
if sys.platform.startswith("win"):
@@ -263,6 +267,9 @@ def setup_serial(
263267
async def transport_connect(self):
264268
"""Handle generic connect and call on to specific transport connect."""
265269
Log.debug("Connecting {}", self.comm_params.comm_name)
270+
if not self.loop:
271+
self.loop = asyncio.get_running_loop()
272+
self.transport, self.protocol = None, None
266273
try:
267274
self.transport, self.protocol = await asyncio.wait_for(
268275
self.call_connect_listen(),
@@ -295,6 +302,8 @@ def connection_made(self, transport: asyncio.BaseTransport):
295302
:param transport: socket etc. representing the connection.
296303
"""
297304
Log.debug("Connected to {}", self.comm_params.comm_name)
305+
if not self.loop:
306+
self.loop = asyncio.get_running_loop()
298307
self.transport = transport
299308
self.reset_delay()
300309
self.cb_connection_made()
@@ -306,7 +315,9 @@ def connection_lost(self, reason: Exception):
306315
"""
307316
Log.debug("Connection lost {} due to {}", self.comm_params.comm_name, reason)
308317
self.cb_connection_lost(reason)
309-
self.close(reconnect=True)
318+
if self.transport:
319+
self.close()
320+
self.reconnect_task = asyncio.create_task(self.reconnect_connect())
310321

311322
def eof_received(self):
312323
"""Call when eof received (other end closed connection).
@@ -352,29 +363,11 @@ def close(self, reconnect: bool = False) -> None:
352363
self.transport.close()
353364
self.transport = None
354365
self.protocol = None
355-
if self.reconnect_timer:
356-
self.reconnect_timer.cancel()
357-
self.reconnect_timer = None
366+
if not reconnect and self.reconnect_task:
367+
self.reconnect_task.cancel()
368+
self.reconnect_task = None
358369
self.recv_buffer = b""
359370

360-
if not reconnect or not self.reconnect_delay_current:
361-
self.reconnect_delay_current = 0
362-
return
363-
364-
Log.debug(
365-
"Waiting {} {} ms reconnecting.",
366-
self.comm_params.comm_name,
367-
self.reconnect_delay_current * 1000,
368-
)
369-
self.reconnect_timer = self.loop.call_later(
370-
self.reconnect_delay_current,
371-
asyncio.create_task,
372-
self.transport_connect(),
373-
)
374-
self.reconnect_delay_current = min(
375-
2 * self.reconnect_delay_current, self.comm_params.reconnect_delay_max
376-
)
377-
378371
def reset_delay(self) -> None:
379372
"""Reset wait time before next reconnect to minimal period."""
380373
self.reconnect_delay_current = self.comm_params.reconnect_delay
@@ -386,6 +379,27 @@ def handle_listen(self):
386379
"""Handle incoming connect."""
387380
return self
388381

382+
async def reconnect_connect(self):
383+
"""Handle reconnect as a task."""
384+
try:
385+
self.reconnect_delay_current = self.comm_params.reconnect_delay
386+
transport = None
387+
while not transport:
388+
Log.debug(
389+
"Wait {} {} ms before reconnecting.",
390+
self.comm_params.comm_name,
391+
self.reconnect_delay_current * 1000,
392+
)
393+
await asyncio.sleep(self.reconnect_delay_current)
394+
transport, _protocol = await self.transport_connect()
395+
self.reconnect_delay_current = min(
396+
2 * self.reconnect_delay_current,
397+
self.comm_params.reconnect_delay_max,
398+
)
399+
except asyncio.CancelledError:
400+
pass
401+
self.reconnect_task = None
402+
389403
# ----------------- #
390404
# The magic methods #
391405
# ----------------- #

0 commit comments

Comments
 (0)