From b3883ce369ef1e07bd4be37892fed4bcde7ea15f Mon Sep 17 00:00:00 2001 From: jan iversen Date: Fri, 8 Mar 2024 10:50:20 +0100 Subject: [PATCH 01/88] prepare dev. --- pymodbus/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pymodbus/__init__.py b/pymodbus/__init__.py index 97558f6f2..28b87963c 100644 --- a/pymodbus/__init__.py +++ b/pymodbus/__init__.py @@ -18,5 +18,5 @@ from pymodbus.pdu import ExceptionResponse -__version__ = "3.6.6" +__version__ = "3.7.0dev" __version_full__ = f"[pymodbus, version {__version__}]" From 2e0cf9c0bd97edefa26dfcc804d34ad9b0048d13 Mon Sep 17 00:00:00 2001 From: jan iversen Date: Fri, 8 Mar 2024 14:17:24 +0100 Subject: [PATCH 02/88] Cancel send if no connection. (#2103) --- pymodbus/transport/transport.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pymodbus/transport/transport.py b/pymodbus/transport/transport.py index e643f0185..e3a4c0fb3 100644 --- a/pymodbus/transport/transport.py +++ b/pymodbus/transport/transport.py @@ -379,6 +379,9 @@ def send(self, data: bytes, addr: tuple | None = None) -> None: :param data: non-empty bytes object with data to send. :param addr: optional addr, only used for UDP server. """ + if not self.transport: + Log.error("Cancel send, because not connected!") + return Log.debug("send: {}", data, ":hex") if self.comm_params.handle_local_echo: self.sent_buffer += data From 579791c291db6fa2f00d3a653f5106c1314ab89b Mon Sep 17 00:00:00 2001 From: jan iversen Date: Sun, 17 Mar 2024 17:03:15 +0100 Subject: [PATCH 03/88] Bump dependencies. (#2108) --- pyproject.toml | 22 +++++++++++----------- test/conftest.py | 4 +--- test/transport/test_comm.py | 3 ++- 3 files changed, 14 insertions(+), 15 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index a2a0ed24f..c6654194d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -60,19 +60,19 @@ documentation = [ "sphinx-rtd-theme>=1.1.1" ] development = [ - "build>=1.0.3", - "codespell>=2.2.2", - "coverage>=7.4.0", - "mypy>=1.6.0", - "pylint>=3.0.0", - "pytest>=7.3.1", - "pytest-asyncio>=0.20.3", + "build>=1.1.1", + "codespell>=2.2.6", + "coverage>=7.4.3", + "mypy>=1.9.0", + "pylint>=3.1.0", + "pytest>=8.1.0", + "pytest-asyncio>=0.23.5.post1", "pytest-cov>=4.1.0", "pytest-profiling>=1.7.0", - "pytest-timeout>=2.2.0", - "pytest-xdist>=3.3.1", - "ruff>=0.2.0", - "twine>=4.0.2", + "pytest-timeout>=2.3.1", + "pytest-xdist>=3.5.0", + "ruff>=0.3.3", + "twine>=5.0.0", "types-Pygments", "types-pyserial" ] diff --git a/test/conftest.py b/test/conftest.py index c405d2490..ee499042a 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -24,8 +24,6 @@ def pytest_configure(): """Configure pytest.""" - pytest.IS_DARWIN = platform.system().lower() == "darwin" - pytest.IS_WINDOWS = platform.system().lower() == "windows" # -----------------------------------------------------------------------# @@ -93,7 +91,7 @@ def prepare_commparams_client(use_port, use_host, use_comm_type): """Prepare CommParamsClass object.""" if use_host == NULLMODEM_HOST and use_comm_type == CommType.SERIAL: use_host = f"{NULLMODEM_HOST}:{use_port}" - timeout = 10 if not pytest.IS_WINDOWS else 2 + timeout = 10 if platform.system().lower() != "windows" else 2 return CommParams( comm_name="test comm", comm_type=use_comm_type, diff --git a/test/transport/test_comm.py b/test/transport/test_comm.py index 918da70dc..4ddf7b742 100644 --- a/test/transport/test_comm.py +++ b/test/transport/test_comm.py @@ -1,5 +1,6 @@ """Test transport.""" import asyncio +import platform import time from unittest import mock @@ -13,7 +14,7 @@ from pymodbus.transport.serialtransport import SerialTransport -FACTOR = 1.2 if not pytest.IS_WINDOWS else 4.2 +FACTOR = 1.2 if platform.system().lower() != "windows" else 4.2 class TestTransportComm: From 92da0dba53cc51e6ff67e8f42b74dc951a65f6a1 Mon Sep 17 00:00:00 2001 From: jan iversen Date: Sun, 17 Mar 2024 17:28:02 +0100 Subject: [PATCH 04/88] update package_test_tool (add 4 test scenarios) (#2107) --- examples/package_test_tool.py | 90 ++++++++++++++++++++++++----------- 1 file changed, 63 insertions(+), 27 deletions(-) diff --git a/examples/package_test_tool.py b/examples/package_test_tool.py index 80221a5ff..b46040c51 100755 --- a/examples/package_test_tool.py +++ b/examples/package_test_tool.py @@ -31,13 +31,16 @@ Send raw data packets to the server (remark data is frame+request) -*** handle_client_data(transport, data) *** +*** simulate_server(transport, request) *** - Called when data is received from the client/server (remark data is frame+request) + Called when data is received from the client (remark data is frame+request) The function generates frame+response and sends it. -And one function which can be modified to test the server functionality: +*** simulate_client(transport, response) *** + + Called when data is received from the server (remark data is frame+request) + """ from __future__ import annotations @@ -46,7 +49,7 @@ import pymodbus.client as modbusClient import pymodbus.server as modbusServer -from pymodbus import Framer, pymodbus_apply_logging_config +from pymodbus import Framer, ModbusException, pymodbus_apply_logging_config from pymodbus.datastore import ( ModbusSequentialDataBlock, ModbusServerContext, @@ -69,6 +72,7 @@ def __init__( """Initialize a stub instance.""" self.stub_handle_data = handler super().__init__(params, is_server) + self.is_tcp = params.comm_type == CommType.TCP async def start_run(self): """Call need functions to start server/client.""" @@ -78,7 +82,7 @@ async def start_run(self): def callback_data(self, data: bytes, addr: tuple | None = None) -> int: """Handle received data.""" - self.stub_handle_data(self, data) + self.stub_handle_data(self, self.is_tcp, data) return len(data) def callback_connected(self) -> None: @@ -95,27 +99,33 @@ def callback_new_connection(self) -> ModbusProtocol: return new_stub +test_port = 5004 # pylint: disable=invalid-name + class ClientTester: # pylint: disable=too-few-public-methods """Main program.""" def __init__(self, comm: CommType): """Initialize runtime tester.""" + global test_port # pylint: disable=global-statement self.comm = comm + host = NULLMODEM_HOST if comm == CommType.TCP: self.client = modbusClient.AsyncModbusTcpClient( - NULLMODEM_HOST, - port=5004, + host, + port=test_port, ) elif comm == CommType.SERIAL: + host = f"{NULLMODEM_HOST}:{test_port}" self.client = modbusClient.AsyncModbusSerialClient( - f"{NULLMODEM_HOST}:5004", + host, ) else: raise RuntimeError("ERROR: CommType not implemented") server_params = self.client.comm_params.copy() - server_params.source_address = (f"{NULLMODEM_HOST}:5004", 5004) - self.stub = TransportStub(server_params, True, handle_client_data) + server_params.source_address = (host, test_port) + self.stub = TransportStub(server_params, True, simulate_server) + test_port += 1 async def run(self): @@ -135,6 +145,7 @@ class ServerTester: # pylint: disable=too-few-public-methods def __init__(self, comm: CommType): """Initialize runtime tester.""" + global test_port # pylint: disable=global-statement self.comm = comm self.store = ModbusSlaveContext( di=ModbusSequentialDataBlock(0, [17] * 100), @@ -151,14 +162,14 @@ def __init__(self, comm: CommType): self.context, framer=Framer.SOCKET, identity=self.identity, - address=(NULLMODEM_HOST, 5004), + address=(NULLMODEM_HOST, test_port), ) elif comm == CommType.SERIAL: self.server = modbusServer.ModbusSerialServer( self.context, framer=Framer.SOCKET, identity=self.identity, - port=f"{NULLMODEM_HOST}:5004", + port=f"{NULLMODEM_HOST}:{test_port}", ) else: raise RuntimeError("ERROR: CommType not implemented") @@ -166,7 +177,8 @@ def __init__(self, comm: CommType): client_params.host = client_params.source_address[0] client_params.port = client_params.source_address[1] client_params.timeout_connect = 1.0 - self.stub = TransportStub(client_params, False, handle_server_data) + self.stub = TransportStub(client_params, False, simulate_client) + test_port += 1 async def run(self): @@ -175,7 +187,7 @@ async def run(self): Log.debug("--> Start testing.") await self.server.listen() await self.stub.start_run() - await server_calls(self.stub) + await server_calls(self.stub, (self.comm == CommType.TCP)) Log.debug("--> Shutting down.") await self.server.shutdown() @@ -194,21 +206,43 @@ async def main(comm: CommType, use_server: bool): async def client_calls(client): """Test client API.""" Log.debug("--> Client calls starting.") - _resp = await client.read_holding_registers(address=124, count=4, slave=0) - + try: + resp = await client.read_holding_registers(address=124, count=4, slave=0) + except ModbusException as exc: + txt = f"ERROR: exception in pymodbus {exc}" + Log.error(txt) + return + if resp.isError(): + txt = "ERROR: pymodbus returned an error!" + Log.error(txt) + await asyncio.sleep(1) + client.close() + print("---> CLIENT all done") -async def server_calls(transport: ModbusProtocol): - """Test client API.""" +async def server_calls(transport: ModbusProtocol, is_tcp: bool): + """Test server functionality.""" Log.debug("--> Server calls starting.") - _resp = transport.send(b'\x00\x02\x00\x00\x00\x06\x01\x03\x00\x00\x00\x01' + - b'\x07\x00\x03\x00\x00\x06\x01\x03\x00\x00\x00\x01') + + if is_tcp: + request = b'\x00\x02\x00\x00\x00\x06\x01\x03\x00\x00\x00\x01' + else: + # 2 responses: + # response = b'\x00\x02\x00\x00\x00\x06\x01\x03\x00\x00\x00\x01' + + # b'\x07\x00\x03\x00\x00\x06\x01\x03\x00\x00\x00\x01') + # 1 response: + request = b'\x00\x02\x00\x00\x00\x06\x01\x03\x00\x00\x00\x01' + transport.send(request) await asyncio.sleep(1) - print("---> all done") + transport.close() + print("---> SERVER all done") -def handle_client_data(transport: ModbusProtocol, data: bytes): +def simulate_server(transport: ModbusProtocol, is_tcp: bool, request: bytes): """Respond to request at transport level.""" - Log.debug("--> stub called with request {}.", data, ":hex") - response = b'\x01\x03\x08\x00\x05\x00\x05\x00\x00\x00\x00\x0c\xd7' + Log.debug("--> Server simulator called with request {}.", request, ":hex") + if is_tcp: + response = b'\x00\x01\x00\x00\x00\x06\x00\x03\x00\x7c\x00\x04' + else: + response = b'\x01\x03\x08\x00\x05\x00\x05\x00\x00\x00\x00\x0c\xd7' # Multiple send is allowed, to test fragmentation # for data in response: @@ -217,12 +251,14 @@ def handle_client_data(transport: ModbusProtocol, data: bytes): transport.send(response) -def handle_server_data(_transport: ModbusProtocol, data: bytes): +def simulate_client(_transport: ModbusProtocol, _is_tcp: bool, response: bytes): """Respond to request at transport level.""" - Log.debug("--> stub called with response {}.", data, ":hex") + Log.debug("--> Client simulator called with response {}.", response, ":hex") if __name__ == "__main__": # True for Server test, False for Client test - # asyncio.run(main(CommType.SERIAL, False), debug=True) + asyncio.run(main(CommType.SERIAL, False), debug=True) + asyncio.run(main(CommType.SERIAL, True), debug=True) + asyncio.run(main(CommType.TCP, False), debug=True) asyncio.run(main(CommType.TCP, True), debug=True) From 66158d50cd6e7c74bc57d2e64a52566739576044 Mon Sep 17 00:00:00 2001 From: jan iversen Date: Thu, 21 Mar 2024 09:18:37 +0100 Subject: [PATCH 05/88] Bump github stale. (#2110) --- .github/workflows/stale_lock.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/stale_lock.yml b/.github/workflows/stale_lock.yml index 2c69132db..1320cbd71 100644 --- a/.github/workflows/stale_lock.yml +++ b/.github/workflows/stale_lock.yml @@ -16,7 +16,7 @@ jobs: stale: runs-on: ubuntu-latest steps: - - uses: actions/stale@v8 + - uses: actions/stale@v9 with: repo-token: ${{ secrets.GITHUB_TOKEN }} stale-issue-message: 'This issue is stale because it has been open 30 days with no activity. Remove stale label or comment or this will be closed in 5 days.' @@ -31,7 +31,7 @@ jobs: exempt-issue-labels: 'Bug,Enhancements,Investigating' stale-pr-label: 'no-pr-activity' remove-stale-when-updated: true - - uses: dessant/lock-threads@v4 + - uses: dessant/lock-threads@v5 with: github-token: ${{ github.token }} issue-inactive-days: '10' From 79287f0dc690fde718623fcb3e2bfaa7b8a5e29d Mon Sep 17 00:00:00 2001 From: jan iversen Date: Thu, 21 Mar 2024 12:33:51 +0100 Subject: [PATCH 06/88] Move on_reconnect to client level (#2111) --- pymodbus/client/base.py | 4 ++-- pymodbus/transport/transport.py | 3 --- test/transport/test_protocol.py | 1 - test/transport/test_reconnect.py | 2 -- 4 files changed, 2 insertions(+), 8 deletions(-) diff --git a/pymodbus/client/base.py b/pymodbus/client/base.py index 972ec7ef6..6c62a74d4 100644 --- a/pymodbus/client/base.py +++ b/pymodbus/client/base.py @@ -188,6 +188,8 @@ def callback_new_connection(self): def callback_connected(self) -> None: """Call when connection is succcesfull.""" + if self.on_reconnect_callback: + self.on_reconnect_callback() def callback_disconnected(self, exc: Exception | None) -> None: """Call when connection is lost.""" @@ -311,7 +313,6 @@ def __init__( broadcast_enable: bool = False, reconnect_delay: float = 0.1, reconnect_delay_max: float = 300.0, - on_reconnect_callback: Callable[[], None] | None = None, no_resend_on_retry: bool = False, **kwargs: Any, ) -> None: @@ -332,7 +333,6 @@ def __init__( parity=kwargs.get("parity", None), stopbits=kwargs.get("stopbits", None), handle_local_echo=kwargs.get("handle_local_echo", False), - on_reconnect_callback=on_reconnect_callback, ) self.params = self._params() self.params.retries = int(retries) diff --git a/pymodbus/transport/transport.py b/pymodbus/transport/transport.py index e3a4c0fb3..f15cf37fe 100644 --- a/pymodbus/transport/transport.py +++ b/pymodbus/transport/transport.py @@ -87,7 +87,6 @@ class CommParams: port: int = 0 source_address: tuple[str, int] | None = None handle_local_echo: bool = False - on_reconnect_callback: Callable[[], None] | None = None # tls sslctx: ssl.SSLContext | None = None @@ -471,8 +470,6 @@ async def do_reconnect(self) -> None: self.reconnect_delay_current * 1000, ) await asyncio.sleep(self.reconnect_delay_current) - if self.comm_params.on_reconnect_callback: - self.comm_params.on_reconnect_callback() if await self.connect(): break self.reconnect_delay_current = min( diff --git a/test/transport/test_protocol.py b/test/transport/test_protocol.py index 4e9b88eeb..e976c811a 100644 --- a/test/transport/test_protocol.py +++ b/test/transport/test_protocol.py @@ -301,7 +301,6 @@ async def test_do_reconnect(self, client): assert client.reconnect_delay_current == client.comm_params.reconnect_delay * 2 assert not client.reconnect_task client.connect.side_effect = asyncio.CancelledError("stop loop") - client.comm_params.on_reconnect_callback = mock.Mock() await client.do_reconnect() assert client.reconnect_delay_current == client.comm_params.reconnect_delay assert not client.reconnect_task diff --git a/test/transport/test_reconnect.py b/test/transport/test_reconnect.py index 9da9355b7..31d9960c6 100644 --- a/test/transport/test_reconnect.py +++ b/test/transport/test_reconnect.py @@ -28,7 +28,6 @@ async def test_no_reconnect_call(self, client): async def test_reconnect_call(self, client): """Test connection_lost().""" - client.comm_params.on_reconnect_callback = mock.MagicMock() client.loop = asyncio.get_running_loop() client.call_create = mock.AsyncMock(return_value=(None, None)) await client.connect() @@ -39,7 +38,6 @@ async def test_reconnect_call(self, client): assert client.reconnect_task assert client.call_create.call_count == 2 assert client.reconnect_delay_current == client.comm_params.reconnect_delay * 2 - assert client.comm_params.on_reconnect_callback.called client.close() async def test_multi_reconnect_call(self, client): From 58a1c37daa3c1ce3798f3d965c7e0c1c66221f2a Mon Sep 17 00:00:00 2001 From: jan iversen Date: Thu, 21 Mar 2024 13:05:46 +0100 Subject: [PATCH 07/88] Drop support for python 3.8 (#2112) --- .github/workflows/ci.yml | 4 ++-- README.rst | 6 +++--- examples/contrib/explain.py | 2 -- examples/package_test_tool.py | 2 +- pymodbus/client/base.py | 7 ++++--- pymodbus/datastore/simulator.py | 3 ++- pymodbus/datastore/store.py | 7 ++++--- pymodbus/factory.py | 8 ++++---- pymodbus/transport/transport.py | 3 ++- pyproject.toml | 6 +++--- test/test_network.py | 2 +- 11 files changed, 26 insertions(+), 24 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b8bdcb6cb..f1bec94bd 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -24,9 +24,9 @@ jobs: fail-fast: false matrix: os: [ubuntu-latest, macos-latest, windows-latest] - python: ['3.8', '3.9', '3.10', '3.11', '3.12'] + python: ['3.9', '3.10', '3.11', '3.12'] include: - - python: '3.8' + - python: '3.9' run_lint: true - python: '3.12' run_doc: true diff --git a/README.rst b/README.rst index 1f445e1ac..33911f0ef 100644 --- a/README.rst +++ b/README.rst @@ -40,9 +40,9 @@ Common features * support all standard frames: socket, rtu, rtu-over-tcp, tcp and ascii * does not have third party dependencies, apart from pyserial (optional) * very lightweight project -* requires Python >= 3.8 +* requires Python >= 3.9 * thorough test suite, that test all corners of the library -* automatically tested on Windows, Linux and MacOS combined with python 3.8 - 3.12 +* automatically tested on Windows, Linux and MacOS combined with python 3.9 - 3.12 * strongly typed API (py.typed present) The modbus protocol specification: Modbus_Application_Protocol_V1_1b3.pdf can be found on @@ -276,7 +276,7 @@ There are 2 bigger projects ongoing: Development instructions ------------------------ -The current code base is compatible with python >= 3.8. +The current code base is compatible with python >= 3.9. Here are some of the common commands to perform a range of activities:: diff --git a/examples/contrib/explain.py b/examples/contrib/explain.py index f91fe8964..64f10c83f 100644 --- a/examples/contrib/explain.py +++ b/examples/contrib/explain.py @@ -1,7 +1,5 @@ """ How to explain pymodbus logs using https://rapidscada.net/modbus/ and requests. - -Created on 7/19/2023 to support Python 3.8 to 3.11 on macOS, Ubuntu, or Windows. """ from __future__ import annotations diff --git a/examples/package_test_tool.py b/examples/package_test_tool.py index b46040c51..6f117a5eb 100755 --- a/examples/package_test_tool.py +++ b/examples/package_test_tool.py @@ -45,7 +45,7 @@ from __future__ import annotations import asyncio -from typing import Callable +from collections.abc import Callable import pymodbus.client as modbusClient import pymodbus.server as modbusServer diff --git a/pymodbus/client/base.py b/pymodbus/client/base.py index 6c62a74d4..e3199c98a 100644 --- a/pymodbus/client/base.py +++ b/pymodbus/client/base.py @@ -3,8 +3,9 @@ import asyncio import socket +from collections.abc import Awaitable, Callable from dataclasses import dataclass -from typing import Any, Awaitable, Callable, Type, cast +from typing import Any, cast from pymodbus.client.mixin import ModbusClientMixin from pymodbus.exceptions import ConnectionException, ModbusIOException @@ -90,7 +91,7 @@ def __init__( # Common variables. self.framer = FRAMER_NAME_TO_CLASS.get( - framer, cast(Type[ModbusFramer], framer) + framer, cast(type[ModbusFramer], framer) )(ClientDecoder(), self) self.transaction = ModbusTransactionManager( self, retries=retries, retry_on_empty=retry_on_empty, **kwargs @@ -344,7 +345,7 @@ def __init__( # Common variables. self.framer = FRAMER_NAME_TO_CLASS.get( - framer, cast(Type[ModbusFramer], framer) + framer, cast(type[ModbusFramer], framer) )(ClientDecoder(), self) self.transaction = ModbusTransactionManager( self, retries=retries, retry_on_empty=retry_on_empty, **kwargs diff --git a/pymodbus/datastore/simulator.py b/pymodbus/datastore/simulator.py index ee11b5198..924aacfaa 100644 --- a/pymodbus/datastore/simulator.py +++ b/pymodbus/datastore/simulator.py @@ -4,8 +4,9 @@ import dataclasses import random import struct +from collections.abc import Callable from datetime import datetime -from typing import Any, Callable +from typing import Any WORD_SIZE = 16 diff --git a/pymodbus/datastore/store.py b/pymodbus/datastore/store.py index d27252bd1..1a52c4429 100644 --- a/pymodbus/datastore/store.py +++ b/pymodbus/datastore/store.py @@ -48,7 +48,8 @@ from __future__ import annotations from abc import ABC, abstractmethod -from typing import Any, Dict, Generic, Iterable, TypeVar +from collections.abc import Iterable +from typing import Any, Generic, TypeVar from pymodbus.exceptions import ParameterException @@ -57,7 +58,7 @@ # Datablock Storage # ---------------------------------------------------------------------------# -V = TypeVar('V', list, Dict[int, Any]) +V = TypeVar('V', list, dict[int, Any]) class BaseModbusDataBlock(ABC, Generic[V]): """Base class for a modbus datastore. @@ -194,7 +195,7 @@ def setValues(self, address, values): self.values[start : start + len(values)] = values -class ModbusSparseDataBlock(BaseModbusDataBlock[Dict[int, Any]]): +class ModbusSparseDataBlock(BaseModbusDataBlock[dict[int, Any]]): """A sparse modbus datastore. E.g Usage. diff --git a/pymodbus/factory.py b/pymodbus/factory.py index 86a46db0c..c75766ed9 100644 --- a/pymodbus/factory.py +++ b/pymodbus/factory.py @@ -10,7 +10,7 @@ """ # pylint: disable=missing-type-doc -from typing import Callable, Dict +from collections.abc import Callable from pymodbus.bit_read_message import ( ReadCoilsRequest, @@ -163,7 +163,7 @@ class ServerDecoder: ] @classmethod - def getFCdict(cls) -> Dict[int, Callable]: + def getFCdict(cls) -> dict[int, Callable]: """Build function code - class list.""" return {f.function_code: f for f in cls.__function_table} # type: ignore[attr-defined] @@ -171,7 +171,7 @@ def __init__(self) -> None: """Initialize the client lookup tables.""" functions = {f.function_code for f in self.__function_table} # type: ignore[attr-defined] self.lookup = self.getFCdict() - self.__sub_lookup: Dict[int, Dict[int, Callable]] = {f: {} for f in functions} + self.__sub_lookup: dict[int, dict[int, Callable]] = {f: {} for f in functions} for f in self.__sub_function_table: self.__sub_lookup[f.function_code][f.sub_function_code] = f # type: ignore[attr-defined] @@ -300,7 +300,7 @@ def __init__(self) -> None: """Initialize the client lookup tables.""" functions = {f.function_code for f in self.function_table} # type: ignore[attr-defined] self.lookup = {f.function_code: f for f in self.function_table} # type: ignore[attr-defined] - self.__sub_lookup: Dict[int, Dict[int, Callable]] = {f: {} for f in functions} + self.__sub_lookup: dict[int, dict[int, Callable]] = {f: {} for f in functions} for f in self.__sub_function_table: self.__sub_lookup[f.function_code][f.sub_function_code] = f # type: ignore[attr-defined] diff --git a/pymodbus/transport/transport.py b/pymodbus/transport/transport.py index f15cf37fe..d248df459 100644 --- a/pymodbus/transport/transport.py +++ b/pymodbus/transport/transport.py @@ -52,10 +52,11 @@ import dataclasses import ssl from abc import abstractmethod +from collections.abc import Callable, Coroutine from contextlib import suppress from enum import Enum from functools import partial -from typing import Any, Callable, Coroutine +from typing import Any from pymodbus.logging import Log from pymodbus.transport.serialtransport import create_serial_connection diff --git a/pyproject.toml b/pyproject.toml index c6654194d..1d0200f47 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -22,7 +22,7 @@ classifiers = [ "Operating System :: MacOS :: MacOS X", "Operating System :: OS Independent", "Operating System :: Microsoft", - "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", @@ -30,7 +30,7 @@ classifiers = [ "Topic :: System :: Networking", "Topic :: Utilities", ] -requires-python = ">=3.8.0" +requires-python = ">=3.9.0" [project.urls] Homepage = "https://github.com/pymodbus-dev/pymodbus/" @@ -129,7 +129,7 @@ load-plugins = [ "pylint.extensions.typing" ] jobs = "0" -py-version = "3.8" +py-version = "3.9" [tool.pylint.messages_control] enable = "all" diff --git a/test/test_network.py b/test/test_network.py index 7697596d3..93844323f 100644 --- a/test/test_network.py +++ b/test/test_network.py @@ -2,7 +2,7 @@ from __future__ import annotations import asyncio -from typing import Callable +from collections.abc import Callable import pytest From 9c2c874a85c65e7433c73eb699fbb8798cb717ed Mon Sep 17 00:00:00 2001 From: John Miko Date: Thu, 21 Mar 2024 14:15:25 -0400 Subject: [PATCH 08/88] Add connection exception to list of exceptions catpured in retries (#2113) --- pymodbus/transaction.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pymodbus/transaction.py b/pymodbus/transaction.py index bd3def28a..accebc97f 100644 --- a/pymodbus/transaction.py +++ b/pymodbus/transaction.py @@ -18,6 +18,7 @@ from threading import RLock from pymodbus.exceptions import ( + ConnectionException, InvalidMessageReceivedException, ModbusIOException, ) @@ -325,7 +326,7 @@ def _transact(self, packet, response_length, full=False, broadcast=False): result = self._recv(response_length, full) # result2 = self._recv(response_length, full) Log.debug("RECV: {}", result, ":hex") - except (OSError, ModbusIOException, InvalidMessageReceivedException) as msg: + except (OSError, ModbusIOException, InvalidMessageReceivedException, ConnectionException) as msg: self.client.close() Log.debug("Transaction failed. ({}) ", msg) last_exception = msg From 6c192f6926d436a363e2c9d3c2b793ce3bbc454e Mon Sep 17 00:00:00 2001 From: jan iversen Date: Sun, 24 Mar 2024 13:28:00 +0100 Subject: [PATCH 09/88] Problem with stale CI. (#2117) --- .github/workflows/stale_lock.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/stale_lock.yml b/.github/workflows/stale_lock.yml index 1320cbd71..7221b0867 100644 --- a/.github/workflows/stale_lock.yml +++ b/.github/workflows/stale_lock.yml @@ -8,6 +8,7 @@ on: permissions: issues: write pull-requests: write + discussions: write concurrency: group: lock @@ -34,5 +35,5 @@ jobs: - uses: dessant/lock-threads@v5 with: github-token: ${{ github.token }} - issue-inactive-days: '10' - pr-inactive-days: '10' + discussion-inactive-days: '10' + process-only: 'discussions' From dfecd8c838938c60d3453a175e6173992a802628 Mon Sep 17 00:00:00 2001 From: jan iversen Date: Wed, 27 Mar 2024 10:56:58 +0100 Subject: [PATCH 10/88] add _legacy_decoder to message rtu (#2119) --- pymodbus/framer/rtu_framer.py | 4 +- pymodbus/message/base.py | 6 +- pymodbus/message/message.py | 4 +- pymodbus/message/rtu.py | 120 +++++++++++++++++++++++++++++++++- test/message/test_message.py | 7 +- 5 files changed, 129 insertions(+), 12 deletions(-) diff --git a/pymodbus/framer/rtu_framer.py b/pymodbus/framer/rtu_framer.py index 45f128cb5..dd6ec817c 100644 --- a/pymodbus/framer/rtu_framer.py +++ b/pymodbus/framer/rtu_framer.py @@ -3,9 +3,7 @@ import struct import time -from pymodbus.exceptions import ( - ModbusIOException, -) +from pymodbus.exceptions import ModbusIOException from pymodbus.framer.base import BYTE_ORDER, FRAME_HEADER, ModbusFramer from pymodbus.logging import Log from pymodbus.message.rtu import MessageRTU diff --git a/pymodbus/message/base.py b/pymodbus/message/base.py index d29366415..831cb57b2 100644 --- a/pymodbus/message/base.py +++ b/pymodbus/message/base.py @@ -16,7 +16,7 @@ class MessageBase: def __init__( self, - device_ids: list[int] | None, + device_ids: list[int], is_server: bool, ) -> None: """Initialize a message instance. @@ -25,10 +25,12 @@ def __init__( """ self.device_ids = device_ids self.is_server = is_server + self.broadcast: bool = (0 in device_ids) + def validate_device_id(self, dev_id: int) -> bool: """Check if device id is expected.""" - return not (self.device_ids and dev_id and dev_id not in self.device_ids) + return self.broadcast or (dev_id in self.device_ids) @abstractmethod def decode(self, _data: bytes) -> tuple[int, int, int, bytes]: diff --git a/pymodbus/message/message.py b/pymodbus/message/message.py index 48f9977f1..0f1967843 100644 --- a/pymodbus/message/message.py +++ b/pymodbus/message/message.py @@ -54,14 +54,14 @@ def __init__(self, message_type: MessageType, params: CommParams, is_server: bool, - device_ids: list[int] | None, + device_ids: list[int], ): """Initialize a message instance. :param message_type: Modbus message type :param params: parameter dataclass :param is_server: true if object act as a server (listen/connect) - :param device_ids: list of device id to accept (server only), None for all. + :param device_ids: list of device id to accept, 0 in list means broadcast. """ super().__init__(params, is_server) self.device_ids = device_ids diff --git a/pymodbus/message/rtu.py b/pymodbus/message/rtu.py index d7abc1099..fa5d7dad3 100644 --- a/pymodbus/message/rtu.py +++ b/pymodbus/message/rtu.py @@ -6,6 +6,11 @@ """ from __future__ import annotations +import struct + +from pymodbus.exceptions import ModbusIOException +from pymodbus.factory import ClientDecoder +from pymodbus.logging import Log from pymodbus.message.base import MessageBase @@ -40,6 +45,13 @@ class MessageRTU(MessageBase): neither when receiving nor when sending. """ + function_codes: list[int] = [] + + @classmethod + def set_legal_function_codes(cls, function_codes: list[int]): + """Set legal function codes.""" + cls.function_codes = function_codes + @classmethod def generate_crc16_table(cls) -> list[int]: """Generate a crc16 lookup table. @@ -59,10 +71,116 @@ def generate_crc16_table(cls) -> list[int]: return result crc16_table: list[int] = [0] - def decode(self, _data: bytes) -> tuple[int, int, int, bytes]: + def _legacy_decode(self, callback, slave): # noqa: C901 + """Process new packet pattern.""" + + def is_frame_ready(self): + """Check if we should continue decode logic.""" + size = self._header.get("len", 0) + if not size and len(self._buffer) > self._hsize: + try: + self._header["uid"] = int(self._buffer[0]) + self._header["tid"] = int(self._buffer[0]) + func_code = int(self._buffer[1]) + pdu_class = self.decoder.lookupPduClass(func_code) + size = pdu_class.calculateRtuFrameSize(self._buffer) + self._header["len"] = size + + if len(self._buffer) < size: + raise IndexError + self._header["crc"] = self._buffer[size - 2 : size] + except IndexError: + return False + return len(self._buffer) >= size if size > 0 else False + + def get_frame_start(self, slaves, broadcast, skip_cur_frame): + """Scan buffer for a relevant frame start.""" + start = 1 if skip_cur_frame else 0 + if (buf_len := len(self._buffer)) < 4: + return False + for i in range(start, buf_len - 3): # + if not broadcast and self._buffer[i] not in slaves: + continue + if ( + self._buffer[i + 1] not in self.function_codes + and (self._buffer[i + 1] - 0x80) not in self.function_codes + ): + continue + if i: + self._buffer = self._buffer[i:] # remove preceding trash. + return True + if buf_len > 3: + self._buffer = self._buffer[-3:] + return False + + def check_frame(self): + """Check if the next frame is available.""" + try: + self._header["uid"] = int(self._buffer[0]) + self._header["tid"] = int(self._buffer[0]) + func_code = int(self._buffer[1]) + pdu_class = self.decoder.lookupPduClass(func_code) + size = pdu_class.calculateRtuFrameSize(self._buffer) + self._header["len"] = size + + if len(self._buffer) < size: + raise IndexError + self._header["crc"] = self._buffer[size - 2 : size] + frame_size = self._header["len"] + data = self._buffer[: frame_size - 2] + crc = self._header["crc"] + crc_val = (int(crc[0]) << 8) + int(crc[1]) + return MessageRTU.check_CRC(data, crc_val) + except (IndexError, KeyError, struct.error): + return False + + self._buffer = b'' # pylint: disable=attribute-defined-outside-init + broadcast = not slave[0] + skip_cur_frame = False + while get_frame_start(self, slave, broadcast, skip_cur_frame): + self._header: dict = {"uid": 0x00, "len": 0, "crc": b"\x00\x00"} # pylint: disable=attribute-defined-outside-init + if not is_frame_ready(self): + Log.debug("Frame - not ready") + break + if not check_frame(self): + Log.debug("Frame check failed, ignoring!!") + # x = self._buffer + # self.resetFrame() + # self._buffer = x + skip_cur_frame = True + continue + start = 0x01 # self._hsize + end = self._header["len"] - 2 + buffer = self._buffer[start:end] + if end > 0: + Log.debug("Getting Frame - {}", buffer, ":hex") + data = buffer + else: + data = b"" + if (result := ClientDecoder().decode(data)) is None: + raise ModbusIOException("Unable to decode request") + result.slave_id = self._header["uid"] + result.transaction_id = self._header["tid"] + self._buffer = self._buffer[self._header["len"] :] # pylint: disable=attribute-defined-outside-init + Log.debug("Frame advanced, resetting header!!") + callback(result) # defer or push to a thread? + + + def decode(self, data: bytes) -> tuple[int, int, int, bytes]: """Decode message.""" + resp = None + if len(data) < 4: + return 0, 0, 0, b'' + + def callback(result): + """Set result.""" + nonlocal resp + resp = result + + self._legacy_decode(callback, self.device_ids) return 0, 0, 0, b'' + def encode(self, data: bytes, device_id: int, _tid: int) -> bytes: """Decode message.""" packet = device_id.to_bytes(1,'big') + data diff --git a/test/message/test_message.py b/test/message/test_message.py index 68dee5d37..22542ece1 100644 --- a/test/message/test_message.py +++ b/test/message/test_message.py @@ -68,8 +68,7 @@ async def test_message_build_send(self, msg): @pytest.mark.parametrize( ("dev_id", "res"), [ - (None, True), - (0, True), + (0, False), (1, True), (2, False), ]) @@ -220,7 +219,7 @@ def test_encode(self, frame, frame_expected, data, dev_id, tid, inx1, inx2, inx3 pytest.skip("Not supported") if frame == MessageTLS and (tid or dev_id): pytest.skip("Not supported") - frame_obj = frame(None, True) + frame_obj = frame([0], True) expected = frame_expected[inx1 + inx2 + inx3] encoded_data = frame_obj.encode(data, dev_id, tid) assert encoded_data == expected @@ -324,7 +323,7 @@ async def test_decode_bad_crc(self, frame, data, exp_len): """Test encode method.""" if frame == MessageRTU: pytest.skip("Waiting for implementation.") - frame_obj = frame(None, True) + frame_obj = frame([0], True) used_len, _, _, data = frame_obj.decode(data) assert used_len == exp_len assert not data From 3fc106bc6a8a69a587935b076e98e461585ea33e Mon Sep 17 00:00:00 2001 From: jan iversen Date: Wed, 27 Mar 2024 11:15:00 +0100 Subject: [PATCH 11/88] Add generate_ssl() to TLS client as helper. (#2120) --- API_changes.rst | 5 ++++ pymodbus/client/tls.py | 43 +++++++++++++++++++++++++++++++++ pymodbus/transport/transport.py | 2 +- 3 files changed, 49 insertions(+), 1 deletion(-) diff --git a/API_changes.rst b/API_changes.rst index 102bc3805..de3850124 100644 --- a/API_changes.rst +++ b/API_changes.rst @@ -3,6 +3,11 @@ API changes Versions (X.Y.Z) where Z > 0 e.g. 3.0.1 do NOT have API changes! +API changes 3.7.0 +----------------- +- class method generate_ssl() added to TLS client (sync/async). + + API changes 3.6.0 ----------------- - framer= is an enum: pymodbus.Framer, but still accept a framer class diff --git a/pymodbus/client/tls.py b/pymodbus/client/tls.py index 388ac8f78..d5b56f980 100644 --- a/pymodbus/client/tls.py +++ b/pymodbus/client/tls.py @@ -91,6 +91,27 @@ async def connect(self) -> bool: ) return await self.base_connect() + @classmethod + def generate_ssl( + cls, + certfile: str | None = None, + keyfile: str | None = None, + password: str | None = None, + ) -> ssl.SSLContext: + """Generate sslctx from cert/key/password. + + :param certfile: Cert file path for TLS server request + :param keyfile: Key file path for TLS server request + :param password: Password for for decrypting private key file + + Remark: + - MODBUS/TCP Security Protocol Specification demands TLSv2 at least + - verify_mode is set to ssl.NONE + """ + return CommParams.generate_ssl( + False, certfile=certfile, keyfile=keyfile, password=password + ) + class ModbusTlsClient(ModbusTcpClient): """**ModbusTlsClient**. @@ -160,6 +181,28 @@ def __init__( ) self.server_hostname = server_hostname + + @classmethod + def generate_ssl( + cls, + certfile: str | None = None, + keyfile: str | None = None, + password: str | None = None, + ) -> ssl.SSLContext: + """Generate sslctx from cert/key/password. + + :param certfile: Cert file path for TLS server request + :param keyfile: Key file path for TLS server request + :param password: Password for for decrypting private key file + + Remark: + - MODBUS/TCP Security Protocol Specification demands TLSv2 at least + - verify_mode is set to ssl.NONE + """ + return CommParams.generate_ssl( + False, certfile=certfile, keyfile=keyfile, password=password, + ) + @property def connected(self) -> bool: """Connect internal.""" diff --git a/pymodbus/transport/transport.py b/pymodbus/transport/transport.py index d248df459..265d98e1d 100644 --- a/pymodbus/transport/transport.py +++ b/pymodbus/transport/transport.py @@ -107,7 +107,7 @@ def generate_ssl( password: str | None = None, sslctx: ssl.SSLContext | None = None, ) -> ssl.SSLContext: - """Generate sslctx from cert/key/passwor. + """Generate sslctx from cert/key/password. MODBUS/TCP Security Protocol Specification demands TLSv2 at least """ From 2c36fd3f1a0587f0d9dd6583e8d82f766a4f2eb6 Mon Sep 17 00:00:00 2001 From: jan iversen Date: Wed, 27 Mar 2024 11:59:35 +0100 Subject: [PATCH 12/88] Remove certfile,keyfile,password from TLS client. (#2121) --- API_changes.rst | 1 + examples/client_async.py | 7 ++++--- examples/client_sync.py | 7 ++++--- pymodbus/client/tls.py | 24 ++++-------------------- test/sub_client/test_client.py | 24 ++++++++++++++++++++---- 5 files changed, 33 insertions(+), 30 deletions(-) diff --git a/API_changes.rst b/API_changes.rst index de3850124..a9ed32476 100644 --- a/API_changes.rst +++ b/API_changes.rst @@ -6,6 +6,7 @@ Versions (X.Y.Z) where Z > 0 e.g. 3.0.1 do NOT have API changes! API changes 3.7.0 ----------------- - class method generate_ssl() added to TLS client (sync/async). +- removed certfile, keyfile, password from TLS client, please use generate_ssl() API changes 3.6.0 diff --git a/examples/client_async.py b/examples/client_async.py index e5554b3b4..a5200fb05 100755 --- a/examples/client_async.py +++ b/examples/client_async.py @@ -97,10 +97,11 @@ def setup_async_client(description=None, cmdline=None): # retries=3, # retry_on_empty=False, # TLS setup parameters - # sslctx=sslctx, - certfile=helper.get_certificate("crt"), - keyfile=helper.get_certificate("key"), + sslctx=modbusClient.AsyncModbusTlsClient.generate_ssl( + certfile=helper.get_certificate("crt"), + keyfile=helper.get_certificate("key"), # password="none", + ), server_hostname="localhost", ) return client diff --git a/examples/client_sync.py b/examples/client_sync.py index ccb28ec8f..c074bb403 100755 --- a/examples/client_sync.py +++ b/examples/client_sync.py @@ -103,10 +103,11 @@ def setup_sync_client(description=None, cmdline=None): # retries=3, # retry_on_empty=False, # TLS setup parameters - # sslctx=None, - certfile=helper.get_certificate("crt"), - keyfile=helper.get_certificate("key"), + sslctx=modbusClient.ModbusTlsClient.generate_ssl( + certfile=helper.get_certificate("crt"), + keyfile=helper.get_certificate("key"), # password=None, + ), server_hostname="localhost", ) return client diff --git a/pymodbus/client/tls.py b/pymodbus/client/tls.py index d5b56f980..58f80e51e 100644 --- a/pymodbus/client/tls.py +++ b/pymodbus/client/tls.py @@ -23,9 +23,6 @@ class AsyncModbusTlsClient(AsyncModbusTcpClient): :param port: Port used for communication :param source_address: Source address of client :param sslctx: SSLContext to use for TLS - :param certfile: Cert file path for TLS server request - :param keyfile: Key file path for TLS server request - :param password: Password for for decrypting private key file :param server_hostname: Bind certificate to host Common optional parameters: @@ -60,10 +57,7 @@ def __init__( host: str, port: int = 802, framer: Framer = Framer.TLS, - sslctx: ssl.SSLContext | None = None, - certfile: str | None = None, - keyfile: str | None = None, - password: str | None = None, + sslctx: ssl.SSLContext = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT), server_hostname: str | None = None, **kwargs: Any, ): @@ -74,9 +68,7 @@ def __init__( port=port, framer=framer, CommType=CommType.TLS, - sslctx=CommParams.generate_ssl( - False, certfile, keyfile, password, sslctx=sslctx - ), + sslctx=sslctx, **kwargs, ) self.server_hostname = server_hostname @@ -125,9 +117,6 @@ class ModbusTlsClient(ModbusTcpClient): :param port: Port used for communication :param source_address: Source address of client :param sslctx: SSLContext to use for TLS - :param certfile: Cert file path for TLS server request - :param keyfile: Key file path for TLS server request - :param password: Password for decrypting private key file :param server_hostname: Bind certificate to host :param kwargs: Experimental parameters @@ -165,10 +154,7 @@ def __init__( host: str, port: int = 802, framer: Framer = Framer.TLS, - sslctx: ssl.SSLContext | None = None, - certfile: str | None = None, - keyfile: str | None = None, - password: str | None = None, + sslctx: ssl.SSLContext = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT), server_hostname: str | None = None, **kwargs: Any, ): @@ -176,9 +162,7 @@ def __init__( super().__init__( host, CommType=CommType.TLS, port=port, framer=framer, **kwargs ) - self.sslctx = CommParams.generate_ssl( - False, certfile, keyfile, password, sslctx=sslctx - ) + self.sslctx = sslctx self.server_hostname = server_hostname diff --git a/test/sub_client/test_client.py b/test/sub_client/test_client.py index 0af821878..43c858cfb 100755 --- a/test/sub_client/test_client.py +++ b/test/sub_client/test_client.py @@ -14,6 +14,7 @@ import pymodbus.other_message as pdu_other_msg import pymodbus.register_read_message as pdu_reg_read import pymodbus.register_write_message as pdu_req_write +from examples.helper import get_certificate from pymodbus import Framer from pymodbus.client.base import ModbusBaseClient from pymodbus.client.mixin import ModbusClientMixin @@ -516,25 +517,40 @@ def test_client_tcp_reuse(): def test_client_tls_connect(): """Test the tls client connection method.""" + sslctx=lib_client.ModbusTlsClient.generate_ssl( + certfile=get_certificate("crt"), + keyfile=get_certificate("key"), + ) with mock.patch.object(ssl.SSLSocket, "connect") as mock_method: - client = lib_client.ModbusTlsClient("127.0.0.1") + client = lib_client.ModbusTlsClient( + "127.0.0.1", + sslctx=sslctx, + ) assert client.connect() with mock.patch.object(socket, "create_connection") as mock_method: mock_method.side_effect = OSError() - client = lib_client.ModbusTlsClient("127.0.0.1") + client = lib_client.ModbusTlsClient("127.0.0.1", sslctx=sslctx) assert not client.connect() def test_client_tls_connect2(): """Test the tls client connection method.""" + sslctx=lib_client.ModbusTlsClient.generate_ssl( + certfile=get_certificate("crt"), + keyfile=get_certificate("key"), + ) with mock.patch.object(ssl.SSLSocket, "connect") as mock_method: - client = lib_client.ModbusTlsClient("127.0.0.1", source_address=("0.0.0.0", 0)) + client = lib_client.ModbusTlsClient( + "127.0.0.1", + sslctx=sslctx, + source_address=("0.0.0.0", 0) + ) assert client.connect() with mock.patch.object(socket, "create_connection") as mock_method: mock_method.side_effect = OSError() - client = lib_client.ModbusTlsClient("127.0.0.1") + client = lib_client.ModbusTlsClient("127.0.0.1", sslctx=sslctx) assert not client.connect() From 0803ff70271a4fd98377957bc1b51bcc270489f2 Mon Sep 17 00:00:00 2001 From: jan iversen Date: Wed, 27 Mar 2024 17:24:59 +0100 Subject: [PATCH 13/88] on_reconnect_callback --> on_connect_callback. (#2122) --- API_changes.rst | 2 ++ pymodbus/client/base.py | 13 +++++++------ 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/API_changes.rst b/API_changes.rst index a9ed32476..cae771306 100644 --- a/API_changes.rst +++ b/API_changes.rst @@ -7,6 +7,8 @@ API changes 3.7.0 ----------------- - class method generate_ssl() added to TLS client (sync/async). - removed certfile, keyfile, password from TLS client, please use generate_ssl() +- on_reconnect_callback() removed from clients (sync/async). +- on_connect_callback(true/false) added to async clients. API changes 3.6.0 diff --git a/pymodbus/client/base.py b/pymodbus/client/base.py index e3199c98a..fea561d1b 100644 --- a/pymodbus/client/base.py +++ b/pymodbus/client/base.py @@ -33,7 +33,7 @@ class ModbusBaseClient(ModbusClientMixin[Awaitable[ModbusResponse]], ModbusProto :param broadcast_enable: True to treat id 0 as broadcast address. :param reconnect_delay: Minimum delay in seconds.milliseconds before reconnecting. :param reconnect_delay_max: Maximum delay in seconds.milliseconds before reconnecting. - :param on_reconnect_callback: Function that will be called just before a reconnection attempt. + :param on_connect_callback: Will be called when connected/disconnected (bool parameter) :param no_resend_on_retry: Do not resend request when retrying due to missing response. :param kwargs: Experimental parameters. @@ -56,7 +56,7 @@ def __init__( broadcast_enable: bool = False, reconnect_delay: float = 0.1, reconnect_delay_max: float = 300, - on_reconnect_callback: Callable[[], None] | None = None, + on_connect_callback: Callable[[bool], None] | None = None, no_resend_on_retry: bool = False, **kwargs: Any, ) -> None: @@ -82,7 +82,7 @@ def __init__( ), False, ) - self.on_reconnect_callback = on_reconnect_callback + self.on_connect_callback = on_connect_callback self.retry_on_empty: int = 0 self.no_resend_on_retry = no_resend_on_retry self.slaves: list[int] = [] @@ -189,12 +189,14 @@ def callback_new_connection(self): def callback_connected(self) -> None: """Call when connection is succcesfull.""" - if self.on_reconnect_callback: - self.on_reconnect_callback() + if self.on_connect_callback: + self.loop.call_soon(self.on_connect_callback, True) def callback_disconnected(self, exc: Exception | None) -> None: """Call when connection is lost.""" Log.debug("callback_disconnected called: {}", exc) + if self.on_connect_callback: + self.loop.call_soon(self.on_connect_callback, False) def callback_data(self, data: bytes, addr: tuple | None = None) -> int: """Handle received data. @@ -281,7 +283,6 @@ class ModbusBaseSyncClient(ModbusClientMixin[ModbusResponse]): :param broadcast_enable: True to treat id 0 as broadcast address. :param reconnect_delay: Minimum delay in seconds.milliseconds before reconnecting. :param reconnect_delay_max: Maximum delay in seconds.milliseconds before reconnecting. - :param on_reconnect_callback: Function that will be called just before a reconnection attempt. :param no_resend_on_retry: Do not resend request when retrying due to missing response. :param kwargs: Experimental parameters. From 326913d0c19c908e0e520c738adb7c2f1295d5cc Mon Sep 17 00:00:00 2001 From: jan iversen Date: Thu, 28 Mar 2024 08:47:25 +0100 Subject: [PATCH 14/88] Wildcard exception catch from pyserial. (#2125) --- pymodbus/client/serial.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pymodbus/client/serial.py b/pymodbus/client/serial.py index 665f032fb..7726a3b33 100644 --- a/pymodbus/client/serial.py +++ b/pymodbus/client/serial.py @@ -218,7 +218,9 @@ def connect(self): if self.strict: self.socket.inter_byte_timeout = self.inter_byte_timeout self.last_frame_end = None - except serial.SerialException as msg: + # except serial.SerialException as msg: + # pyserial raises undocumented exceptions like termios + except Exception as msg: # pylint: disable=broad-exception-caught Log.error("{}", msg) self.close() return self.socket is not None From 3a3a63711022617639c9a50c3007c1eea5b4b4f5 Mon Sep 17 00:00:00 2001 From: jan iversen Date: Thu, 28 Mar 2024 14:01:20 +0100 Subject: [PATCH 15/88] ASCII framer using message decode() (#2128) --- pymodbus/framer/ascii_framer.py | 47 +++++-------------- ...r_multidrop.py => server_multidrop_tbd.py} | 0 .../test_all_messages.py | 0 .../test_bit_read_messages.py | 0 .../test_bit_write_messages.py | 0 .../test_diag_messages.py | 0 .../test_mei_messages.py | 0 .../test_other_messages.py | 0 .../test_register_read_messages.py | 0 .../test_register_write_messages.py | 0 test/transport/test_protocol.py | 6 +++ 11 files changed, 19 insertions(+), 34 deletions(-) rename test/message/{to_do_server_multidrop.py => server_multidrop_tbd.py} (100%) rename test/{sub_messages => sub_function_codes}/test_all_messages.py (100%) rename test/{sub_messages => sub_function_codes}/test_bit_read_messages.py (100%) rename test/{sub_messages => sub_function_codes}/test_bit_write_messages.py (100%) rename test/{sub_messages => sub_function_codes}/test_diag_messages.py (100%) rename test/{sub_messages => sub_function_codes}/test_mei_messages.py (100%) rename test/{sub_messages => sub_function_codes}/test_other_messages.py (100%) rename test/{sub_messages => sub_function_codes}/test_register_read_messages.py (100%) rename test/{sub_messages => sub_function_codes}/test_register_write_messages.py (100%) diff --git a/pymodbus/framer/ascii_framer.py b/pymodbus/framer/ascii_framer.py index c564085e5..8ce7a67b4 100644 --- a/pymodbus/framer/ascii_framer.py +++ b/pymodbus/framer/ascii_framer.py @@ -1,6 +1,5 @@ """Ascii_framer.""" # pylint: disable=missing-type-doc -from binascii import a2b_hex from pymodbus.exceptions import ModbusIOException from pymodbus.framer.base import BYTE_ORDER, FRAME_HEADER, ModbusFramer @@ -52,44 +51,24 @@ def decode_data(self, data): def frameProcessIncomingPacket(self, single, callback, slave, _tid=None, **kwargs): """Process new packet pattern.""" - def check_frame(self): - """Check and decode the next frame.""" - start = self._buffer.find(self._start) - if start == -1: - return False - if start > 0: # go ahead and skip old bad data - self._buffer = self._buffer[start:] - start = 0 - - if (end := self._buffer.find(self._end)) != -1: - self._header["len"] = end - self._header["uid"] = int(self._buffer[1:3], 16) - self._header["lrc"] = int(self._buffer[end - 2 : end], 16) - data = a2b_hex(self._buffer[start + 1 : end - 2]) - return MessageAscii.check_LRC(data, self._header["lrc"]) - return False - - while len(self._buffer) > 1: - if not check_frame(self): - break + while len(self._buffer): + used_len, _tid, dev_id, data = self.message_handler.decode(self._buffer) + if not data: + if not used_len: + return + self._buffer = self._buffer[used_len :] + continue + self._header["uid"] = dev_id if not self._validate_slave_id(slave, single): - header_txt = self._header["uid"] - Log.error("Not a valid slave id - {}, ignoring!!", header_txt) + Log.error("Not a valid slave id - {}, ignoring!!", dev_id) self.resetFrame() - continue + return - start = self._hsize + 1 - end = self._header["len"] - 2 - buffer = self._buffer[start:end] - if end > 0: - frame = a2b_hex(buffer) - else: - frame = b"" - if (result := self.decoder.decode(frame)) is None: + if (result := self.decoder.decode(data)) is None: raise ModbusIOException("Unable to decode response") self.populateResult(result) - self._buffer = self._buffer[self._header["len"] + 2 :] - self._header = {"lrc": "0000", "len": 0, "uid": 0x00} + self._buffer = self._buffer[used_len :] + self._header = {"uid": 0x00} callback(result) # defer this def buildPacket(self, message): diff --git a/test/message/to_do_server_multidrop.py b/test/message/server_multidrop_tbd.py similarity index 100% rename from test/message/to_do_server_multidrop.py rename to test/message/server_multidrop_tbd.py diff --git a/test/sub_messages/test_all_messages.py b/test/sub_function_codes/test_all_messages.py similarity index 100% rename from test/sub_messages/test_all_messages.py rename to test/sub_function_codes/test_all_messages.py diff --git a/test/sub_messages/test_bit_read_messages.py b/test/sub_function_codes/test_bit_read_messages.py similarity index 100% rename from test/sub_messages/test_bit_read_messages.py rename to test/sub_function_codes/test_bit_read_messages.py diff --git a/test/sub_messages/test_bit_write_messages.py b/test/sub_function_codes/test_bit_write_messages.py similarity index 100% rename from test/sub_messages/test_bit_write_messages.py rename to test/sub_function_codes/test_bit_write_messages.py diff --git a/test/sub_messages/test_diag_messages.py b/test/sub_function_codes/test_diag_messages.py similarity index 100% rename from test/sub_messages/test_diag_messages.py rename to test/sub_function_codes/test_diag_messages.py diff --git a/test/sub_messages/test_mei_messages.py b/test/sub_function_codes/test_mei_messages.py similarity index 100% rename from test/sub_messages/test_mei_messages.py rename to test/sub_function_codes/test_mei_messages.py diff --git a/test/sub_messages/test_other_messages.py b/test/sub_function_codes/test_other_messages.py similarity index 100% rename from test/sub_messages/test_other_messages.py rename to test/sub_function_codes/test_other_messages.py diff --git a/test/sub_messages/test_register_read_messages.py b/test/sub_function_codes/test_register_read_messages.py similarity index 100% rename from test/sub_messages/test_register_read_messages.py rename to test/sub_function_codes/test_register_read_messages.py diff --git a/test/sub_messages/test_register_write_messages.py b/test/sub_function_codes/test_register_write_messages.py similarity index 100% rename from test/sub_messages/test_register_write_messages.py rename to test/sub_function_codes/test_register_write_messages.py diff --git a/test/transport/test_protocol.py b/test/transport/test_protocol.py index e976c811a..7aafc3777 100644 --- a/test/transport/test_protocol.py +++ b/test/transport/test_protocol.py @@ -196,6 +196,12 @@ async def test_handle_local_echo_partial(self, client): assert client.recv_buffer == b"response" assert not client.sent_buffer + async def test_handle_no_transport(self, client): + """Test send().""" + client.transport = None + client.send(b"partial") + assert not client.sent_buffer + class TestTransportProtocol2: """Test transport module.""" From f23a71b93c00baa4817c01b689244fd9d6ed5fee Mon Sep 17 00:00:00 2001 From: jan iversen Date: Thu, 28 Mar 2024 14:29:37 +0100 Subject: [PATCH 16/88] SOCKET/TLS framer using message decode(). (#2129) --- pymodbus/framer/socket_framer.py | 38 +++++++---------------- pymodbus/framer/tls_framer.py | 52 +++++++++++++++----------------- 2 files changed, 35 insertions(+), 55 deletions(-) diff --git a/pymodbus/framer/socket_framer.py b/pymodbus/framer/socket_framer.py index 9137603f7..48616e52f 100644 --- a/pymodbus/framer/socket_framer.py +++ b/pymodbus/framer/socket_framer.py @@ -72,41 +72,25 @@ def frameProcessIncomingPacket(self, single, callback, slave, tid=None, **kwargs The processed and decoded messages are pushed to the callback function to process and send. """ - def check_frame(self): - """Check and decode the next frame.""" - if not len(self._buffer) > self._hsize: - return False - ( - self._header["tid"], - self._header["pid"], - self._header["len"], - self._header["uid"], - ) = struct.unpack(">HHHB", self._buffer[0 : self._hsize]) - if self._header["len"] < 2: - length = self._hsize + self._header["len"] -1 - self._buffer = self._buffer[length:] - self._header = {"tid": 0, "pid": 0, "len": 0, "uid": 0} - elif len(self._buffer) - self._hsize + 1 >= self._header["len"]: - return True - Log.debug("Frame check failed, missing part of message!!") - return False - while True: - if not check_frame(self): - return + used_len, use_tid, dev_id, data = self.message_handler.decode(self._buffer) + if not data: + if not used_len: + return + self._buffer = self._buffer[used_len :] + continue + self._header["uid"] = dev_id + self._header["tid"] = use_tid + self._header["pid"] = 0 if not self._validate_slave_id(slave, single): - header_txt = self._header["uid"] - Log.debug("Not a valid slave id - {}, ignoring!!", header_txt) + Log.debug("Not a valid slave id - {}, ignoring!!", dev_id) self.resetFrame() return - length = self._hsize + self._header["len"] -1 - data = self._buffer[self._hsize : length] if (result := self.decoder.decode(data)) is None: self.resetFrame() raise ModbusIOException("Unable to decode request") self.populateResult(result) - length = self._hsize + self._header["len"] -1 - self._buffer = self._buffer[length:] + self._buffer = self._buffer[used_len:] self._header = {"tid": 0, "pid": 0, "len": 0, "uid": 0} if tid and tid != result.transaction_id: self.resetFrame() diff --git a/pymodbus/framer/tls_framer.py b/pymodbus/framer/tls_framer.py index 8a8924012..5b88c1e34 100644 --- a/pymodbus/framer/tls_framer.py +++ b/pymodbus/framer/tls_framer.py @@ -35,7 +35,7 @@ def __init__(self, decoder, client=None): """ super().__init__(decoder, client) self._hsize = 0x0 - self.message_encoder = MessageTLS([0], True) + self.message_handler = MessageTLS([0], True) def decode_data(self, data): """Decode data.""" @@ -47,32 +47,28 @@ def decode_data(self, data): def frameProcessIncomingPacket(self, single, callback, slave, _tid=None, **kwargs): """Process new packet pattern.""" # no slave id for Modbus Security Application Protocol - def check_frame(self): - """Check and decode the next frame.""" - if len(self._buffer) > self._hsize: - # we have at least a complete message, continue - if len(self._buffer) - self._hsize >= 1: - return True - # we don't have enough of a message yet, wait - return False - - if not len(self._buffer) > self._hsize: - return - if not check_frame(self): - Log.debug("Frame check failed, ignoring!!") - self.resetFrame() - return - if not self._validate_slave_id(slave, single): - Log.debug("Not in valid slave id - {}, ignoring!!", slave) - self.resetFrame() - return - data = self._buffer[self._hsize :] - if (result := self.decoder.decode(data)) is None: - raise ModbusIOException("Unable to decode request") - self.populateResult(result) - self._buffer = b"" - self._header = {} - callback(result) # defer or push to a thread? + + while True: + used_len, use_tid, dev_id, data = self.message_handler.decode(self._buffer) + if not data: + if not used_len: + return + self._buffer = self._buffer[used_len :] + continue + self._header["uid"] = dev_id + self._header["tid"] = use_tid + self._header["pid"] = 0 + + if not self._validate_slave_id(slave, single): + Log.debug("Not in valid slave id - {}, ignoring!!", slave) + self.resetFrame() + return + if (result := self.decoder.decode(data)) is None: + raise ModbusIOException("Unable to decode request") + self.populateResult(result) + self._buffer = b"" + self._header = {} + callback(result) # defer or push to a thread? def buildPacket(self, message): """Create a ready to send modbus packet. @@ -80,5 +76,5 @@ def buildPacket(self, message): :param message: The populated request/response to send """ data = message.function_code.to_bytes(1,'big') + message.encode() - packet = self.message_encoder.encode(data, message.slave_id, message.transaction_id) + packet = self.message_handler.encode(data, message.slave_id, message.transaction_id) return packet From 86d5afe2e5f972d67c2dd20330523801f75295cb Mon Sep 17 00:00:00 2001 From: jan iversen Date: Thu, 28 Mar 2024 17:42:58 +0100 Subject: [PATCH 17/88] Remove binary_framer. (#2130) --- API_changes.rst | 1 + doc/Makefile | 14 -- doc/source/_static/examples.tgz | Bin 50483 -> 50801 bytes doc/source/_static/examples.zip | Bin 36925 -> 37218 bytes doc/source/library/framer.rst | 8 -- doc/source/library/simulator/config.rst | 9 +- examples/client_async.py | 4 +- examples/client_sync.py | 4 +- examples/helper.py | 2 +- examples/message_generator.py | 4 +- examples/message_parser.py | 4 +- examples/server_async.py | 4 +- examples/server_sync.py | 4 +- examples/server_updating.py | 4 +- pymodbus/framer/__init__.py | 4 - pymodbus/framer/binary_framer.py | 134 ------------------ pymodbus/transaction.py | 10 +- ...ver_multidrop_tbd.py => test_multidrop.py} | 0 test/message/test_rtu.py | 12 +- test/sub_client/test_client_sync.py | 5 - test/sub_examples/test_examples.py | 4 +- test/test_framers.py | 25 +--- test/test_network.py | 4 +- test/test_transaction.py | 85 ----------- 24 files changed, 34 insertions(+), 311 deletions(-) delete mode 100644 doc/Makefile delete mode 100644 pymodbus/framer/binary_framer.py rename test/message/{server_multidrop_tbd.py => test_multidrop.py} (100%) diff --git a/API_changes.rst b/API_changes.rst index cae771306..6e4fb08be 100644 --- a/API_changes.rst +++ b/API_changes.rst @@ -9,6 +9,7 @@ API changes 3.7.0 - removed certfile, keyfile, password from TLS client, please use generate_ssl() - on_reconnect_callback() removed from clients (sync/async). - on_connect_callback(true/false) added to async clients. +- binary framer no longer supported API changes 3.6.0 diff --git a/doc/Makefile b/doc/Makefile deleted file mode 100644 index 312a671da..000000000 --- a/doc/Makefile +++ /dev/null @@ -1,14 +0,0 @@ -# Minimal makefile for Sphinx documentation -# - -# You can set these variables from the command line. -SPHINXOPTS = -W --keep-going - -# Put it first so that "make" without argument is like "make help". -help: - sphinx-build -M help "." "_build" -W --keep-going - -.PHONY: help Makefile - -%: Makefile - sphinx-build -M $@ "." "_build" -W --keep-going diff --git a/doc/source/_static/examples.tgz b/doc/source/_static/examples.tgz index c0a001caa4e7b78db403ff617532d12776df400c..87b9d5fbc8bf2d9d48f4f18336b4b07133be0f5f 100644 GIT binary patch literal 50801 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 Wo_(Huo_)S(pZ_10IQaws)&l^Jwbg`sLi?1TGZG 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 T;cpHokZK78n08_RB% diff --git a/doc/source/library/framer.rst b/doc/source/library/framer.rst index c5d702db6..180b93346 100644 --- a/doc/source/library/framer.rst +++ b/doc/source/library/framer.rst @@ -9,14 +9,6 @@ pymodbus\.framer\.ascii_framer module :undoc-members: :show-inheritance: -pymodbus\.framer\.binary_framer module --------------------------------------- - -.. automodule:: pymodbus.framer.binary_framer - :members: - :undoc-members: - :show-inheritance: - pymodbus\.framer\.rtu_framer module ----------------------------------- diff --git a/doc/source/library/simulator/config.rst b/doc/source/library/simulator/config.rst index 0d6f31a3c..700f9a89f 100644 --- a/doc/source/library/simulator/config.rst +++ b/doc/source/library/simulator/config.rst @@ -63,11 +63,10 @@ The entry “comm” allows the following values: The entry “framer” allows the following values: -- “ascii” to use :class:`pymodbus.framer.ascii_framer.ModbusAsciiFramer`, -- "binary to use :class:`pymodbus.framer.ascii_framer.ModbusBinaryFramer`, -- “rtu” to use :class:`pymodbus.framer.ascii_framer.ModbusRtuFramer`, -- “tls” to use :class:`pymodbus.framer.ascii_framer.ModbusTlsFramer`, -- “socket” to use :class:`pymodbus.framer.ascii_framer.ModbusSocketFramer`. +- “ascii” to use :class:`pymodbus.framer.ModbusAsciiFramer`, +- “rtu” to use :class:`pymodbus.framer.ModbusRtuFramer`, +- “tls” to use :class:`pymodbus.framer.ModbusTlsFramer`, +- “socket” to use :class:`pymodbus.framer.ModbusSocketFramer`. .. warning:: diff --git a/examples/client_async.py b/examples/client_async.py index a5200fb05..6c4fa5804 100755 --- a/examples/client_async.py +++ b/examples/client_async.py @@ -4,7 +4,7 @@ usage:: client_async.py [-h] [-c {tcp,udp,serial,tls}] - [-f {ascii,binary,rtu,socket,tls}] + [-f {ascii,rtu,socket,tls}] [-l {critical,error,warning,info,debug}] [-p PORT] [--baudrate BAUDRATE] [--host HOST] @@ -12,7 +12,7 @@ show this help message and exit -c, -comm {tcp,udp,serial,tls} set communication, default is tcp - -f, --framer {ascii,binary,rtu,socket,tls} + -f, --framer {ascii,rtu,socket,tls} set framer, default depends on --comm -l, --log {critical,error,warning,info,debug} set log level, default is info diff --git a/examples/client_sync.py b/examples/client_sync.py index c074bb403..c236988ac 100755 --- a/examples/client_sync.py +++ b/examples/client_sync.py @@ -6,7 +6,7 @@ usage:: client_sync.py [-h] [-c {tcp,udp,serial,tls}] - [-f {ascii,binary,rtu,socket,tls}] + [-f {ascii,rtu,socket,tls}] [-l {critical,error,warning,info,debug}] [-p PORT] [--baudrate BAUDRATE] [--host HOST] @@ -14,7 +14,7 @@ show this help message and exit -c, --comm {tcp,udp,serial,tls} set communication, default is tcp - -f, --framer {ascii,binary,rtu,socket,tls} + -f, --framer {ascii,rtu,socket,tls} set framer, default depends on --comm -l, --log {critical,error,warning,info,debug} set log level, default is info diff --git a/examples/helper.py b/examples/helper.py index 4581014b0..af26468c8 100755 --- a/examples/helper.py +++ b/examples/helper.py @@ -29,7 +29,7 @@ def get_commandline(server=False, description=None, extras=None, cmdline=None): parser.add_argument( "-f", "--framer", - choices=["ascii", "binary", "rtu", "socket", "tls"], + choices=["ascii", "rtu", "socket", "tls"], help="set framer, default depends on --comm", dest="framer", type=str, diff --git a/examples/message_generator.py b/examples/message_generator.py index 4f49a67d9..eaf7f036e 100755 --- a/examples/message_generator.py +++ b/examples/message_generator.py @@ -14,7 +14,6 @@ import pymodbus.register_write_message as modbus_register_write from pymodbus.transaction import ( ModbusAsciiFramer, - ModbusBinaryFramer, ModbusRtuFramer, ModbusSocketFramer, ) @@ -121,7 +120,7 @@ def get_commandline(cmdline=None): parser = argparse.ArgumentParser() parser.add_argument( "--framer", - choices=["ascii", "binary", "rtu", "socket"], + choices=["ascii", "rtu", "socket"], help="set framer, default is rtu", dest="framer", default="rtu", @@ -200,7 +199,6 @@ def generate_messages(cmdline=None): } framer = { "ascii": ModbusAsciiFramer, - "binary": ModbusBinaryFramer, "rtu": ModbusRtuFramer, "socket": ModbusSocketFramer, }[args.framer](None) diff --git a/examples/message_parser.py b/examples/message_parser.py index e95c4a29e..67bf68191 100755 --- a/examples/message_parser.py +++ b/examples/message_parser.py @@ -15,7 +15,6 @@ from pymodbus.factory import ClientDecoder, ServerDecoder from pymodbus.transaction import ( ModbusAsciiFramer, - ModbusBinaryFramer, ModbusRtuFramer, ModbusSocketFramer, ) @@ -30,7 +29,7 @@ def get_commandline(cmdline): parser.add_argument( "--framer", - choices=["ascii", "binary", "rtu", "socket"], + choices=["ascii", "rtu", "socket"], help="set framer, default is rtu", type=str, default="rtu", @@ -148,7 +147,6 @@ def parse_messages(cmdline=None): framer = { "ascii": ModbusAsciiFramer, - "binary": ModbusBinaryFramer, "rtu": ModbusRtuFramer, "socket": ModbusSocketFramer, }[args.framer] diff --git a/examples/server_async.py b/examples/server_async.py index 668dcad0e..8cb2ad787 100755 --- a/examples/server_async.py +++ b/examples/server_async.py @@ -6,7 +6,7 @@ usage:: server_async.py [-h] [--comm {tcp,udp,serial,tls}] - [--framer {ascii,binary,rtu,socket,tls}] + [--framer {ascii,rtu,socket,tls}] [--log {critical,error,warning,info,debug}] [--port PORT] [--store {sequential,sparse,factory,none}] [--slaves SLAVES] @@ -15,7 +15,7 @@ show this help message and exit -c, --comm {tcp,udp,serial,tls} set communication, default is tcp - -f, --framer {ascii,binary,rtu,socket,tls} + -f, --framer {ascii,rtu,socket,tls} set framer, default depends on --comm -l, --log {critical,error,warning,info,debug} set log level, default is info diff --git a/examples/server_sync.py b/examples/server_sync.py index 54a2a1792..52192b4a0 100755 --- a/examples/server_sync.py +++ b/examples/server_sync.py @@ -6,7 +6,7 @@ usage:: server_sync.py [-h] [--comm {tcp,udp,serial,tls}] - [--framer {ascii,binary,rtu,socket,tls}] + [--framer {ascii,rtu,socket,tls}] [--log {critical,error,warning,info,debug}] [--port PORT] [--store {sequential,sparse,factory,none}] [--slaves SLAVES] @@ -15,7 +15,7 @@ show this help message and exit -c, --comm {tcp,udp,serial,tls} set communication, default is tcp - -f, --framer {ascii,binary,rtu,socket,tls} + -f, --framer {ascii,rtu,socket,tls} set framer, default depends on --comm -l, --log {critical,error,warning,info,debug} set log level, default is info diff --git a/examples/server_updating.py b/examples/server_updating.py index 84104a9eb..dae9e7f61 100755 --- a/examples/server_updating.py +++ b/examples/server_updating.py @@ -7,7 +7,7 @@ usage:: server_updating.py [-h] [--comm {tcp,udp,serial,tls}] - [--framer {ascii,binary,rtu,socket,tls}] + [--framer {ascii,rtu,socket,tls}] [--log {critical,error,warning,info,debug}] [--port PORT] [--store {sequential,sparse,factory,none}] [--slaves SLAVES] @@ -16,7 +16,7 @@ show this help message and exit -c, --comm {tcp,udp,serial,tls} set communication, default is tcp - -f, --framer {ascii,binary,rtu,socket,tls} + -f, --framer {ascii,rtu,socket,tls} set framer, default depends on --comm -l, --log {critical,error,warning,info,debug} set log level, default is info diff --git a/pymodbus/framer/__init__.py b/pymodbus/framer/__init__.py index 68d9aeb72..20b88e05b 100644 --- a/pymodbus/framer/__init__.py +++ b/pymodbus/framer/__init__.py @@ -4,7 +4,6 @@ "FRAMER_NAME_TO_CLASS", "ModbusFramer", "ModbusAsciiFramer", - "ModbusBinaryFramer", "ModbusRtuFramer", "ModbusSocketFramer", "ModbusTlsFramer", @@ -15,7 +14,6 @@ from pymodbus.framer.ascii_framer import ModbusAsciiFramer from pymodbus.framer.base import ModbusFramer -from pymodbus.framer.binary_framer import ModbusBinaryFramer from pymodbus.framer.rtu_framer import ModbusRtuFramer from pymodbus.framer.socket_framer import ModbusSocketFramer from pymodbus.framer.tls_framer import ModbusTlsFramer @@ -25,7 +23,6 @@ class Framer(str, enum.Enum): """These represent the different framers.""" ASCII = "ascii" - BINARY = "binary" RTU = "rtu" SOCKET = "socket" TLS = "tls" @@ -33,7 +30,6 @@ class Framer(str, enum.Enum): FRAMER_NAME_TO_CLASS = { Framer.ASCII: ModbusAsciiFramer, - Framer.BINARY: ModbusBinaryFramer, Framer.RTU: ModbusRtuFramer, Framer.SOCKET: ModbusSocketFramer, Framer.TLS: ModbusTlsFramer, diff --git a/pymodbus/framer/binary_framer.py b/pymodbus/framer/binary_framer.py deleted file mode 100644 index 746fe0f52..000000000 --- a/pymodbus/framer/binary_framer.py +++ /dev/null @@ -1,134 +0,0 @@ -"""Binary framer.""" -# pylint: disable=missing-type-doc -import struct - -from pymodbus.exceptions import ModbusIOException -from pymodbus.framer.base import BYTE_ORDER, FRAME_HEADER, ModbusFramer -from pymodbus.logging import Log -from pymodbus.message.rtu import MessageRTU - - -BINARY_FRAME_HEADER = BYTE_ORDER + FRAME_HEADER - -# --------------------------------------------------------------------------- # -# Modbus Binary Message -# --------------------------------------------------------------------------- # - - -class ModbusBinaryFramer(ModbusFramer): - """Modbus Binary Frame Controller. - - [ Start ][Address ][ Function ][ Data ][ CRC ][ End ] - 1b 1b 1b Nb 2b 1b - - * data can be 0 - 2x252 chars - * end is "}" - * start is "{" - - The idea here is that we implement the RTU protocol, however, - instead of using timing for message delimiting, we use start - and end of message characters (in this case { and }). Basically, - this is a binary framer. - - The only case we have to watch out for is when a message contains - the { or } characters. If we encounter these characters, we - simply duplicate them. Hopefully we will not encounter those - characters that often and will save a little bit of bandwitch - without a real-time system. - - Protocol defined by jamod.sourceforge.net. - """ - - method = "binary" - - def __init__(self, decoder, client=None): - """Initialize a new instance of the framer. - - :param decoder: The decoder implementation to use - """ - super().__init__(decoder, client) - # self._header.update({"crc": 0x0000}) - self._hsize = 0x01 - self._start = b"\x7b" # { - self._end = b"\x7d" # } - self._repeat = [b"}"[0], b"{"[0]] # python3 hack - - def decode_data(self, data): - """Decode data.""" - if len(data) > self._hsize: - uid = struct.unpack(">B", data[1:2])[0] - fcode = struct.unpack(">B", data[2:3])[0] - return {"slave": uid, "fcode": fcode} - return {} - - def frameProcessIncomingPacket(self, single, callback, slave, _tid=None, **kwargs): - """Process new packet pattern.""" - def check_frame(self) -> bool: - """Check and decode the next frame.""" - start = self._buffer.find(self._start) - if start == -1: - return False - if start > 0: # go ahead and skip old bad data - self._buffer = self._buffer[start:] - - if (end := self._buffer.find(self._end)) != -1: - self._header["len"] = end - self._header["uid"] = struct.unpack(">B", self._buffer[1:2])[0] - self._header["crc"] = struct.unpack(">H", self._buffer[end - 2 : end])[0] - data = self._buffer[1 : end - 2] - return MessageRTU.check_CRC(data, self._header["crc"]) - return False - - while len(self._buffer) > 1: - if not check_frame(self): - Log.debug("Frame check failed, ignoring!!") - break - if not self._validate_slave_id(slave, single): - header_txt = self._header["uid"] - Log.debug("Not a valid slave id - {}, ignoring!!", header_txt) - self.resetFrame() - break - start = self._hsize + 1 - end = self._header["len"] - 2 - buffer = self._buffer[start:end] - if end > 0: - frame = buffer - else: - frame = b"" - if (result := self.decoder.decode(frame)) is None: - raise ModbusIOException("Unable to decode response") - self.populateResult(result) - self._buffer = self._buffer[self._header["len"] + 2 :] - self._header = {"crc": 0x0000, "len": 0, "uid": 0x00} - callback(result) # defer or push to a thread? - - def buildPacket(self, message): - """Create a ready to send modbus packet. - - :param message: The request/response to send - :returns: The encoded packet - """ - data = self._preflight(message.encode()) - packet = ( - struct.pack(BINARY_FRAME_HEADER, message.slave_id, message.function_code) - + data - ) - packet += struct.pack(">H", MessageRTU.compute_CRC(packet)) - packet = self._start + packet + self._end - return packet - - def _preflight(self, data): - """Do preflight buffer test. - - This basically scans the buffer for start and end - tags and if found, escapes them. - - :param data: The message to escape - :returns: the escaped packet - """ - array = bytearray() - for item in data: - if item in self._repeat: - array.append(item) - array.append(item) - return bytes(array) diff --git a/pymodbus/transaction.py b/pymodbus/transaction.py index accebc97f..8ff1feeda 100644 --- a/pymodbus/transaction.py +++ b/pymodbus/transaction.py @@ -7,7 +7,6 @@ "ModbusTlsFramer", "ModbusRtuFramer", "ModbusAsciiFramer", - "ModbusBinaryFramer", ] # pylint: disable=missing-type-doc @@ -23,7 +22,6 @@ ModbusIOException, ) from pymodbus.framer.ascii_framer import ModbusAsciiFramer -from pymodbus.framer.binary_framer import ModbusBinaryFramer from pymodbus.framer.rtu_framer import ModbusRtuFramer from pymodbus.framer.socket_framer import ModbusSocketFramer from pymodbus.framer.tls_framer import ModbusTlsFramer @@ -87,8 +85,6 @@ def _set_adu_size(self): self.base_adu_size = 3 # address(1), CRC(2) elif isinstance(self.client.framer, ModbusAsciiFramer): self.base_adu_size = 7 # start(1)+ Address(2), LRC(2) + end(2) - elif isinstance(self.client.framer, ModbusBinaryFramer): - self.base_adu_size = 5 # start(1) + Address(1), CRC(2) + end(1) elif isinstance(self.client.framer, ModbusTlsFramer): self.base_adu_size = 0 # no header and footer else: @@ -106,7 +102,7 @@ def _calculate_exception_length(self): return self.base_adu_size + 2 # Fcode(1), ExceptionCode(1) if isinstance(self.client.framer, ModbusAsciiFramer): return self.base_adu_size + 4 # Fcode(2), ExceptionCode(2) - if isinstance(self.client.framer, (ModbusRtuFramer, ModbusBinaryFramer)): + if isinstance(self.client.framer, ModbusRtuFramer): return self.base_adu_size + 2 # Fcode(1), ExceptionCode(1) return None @@ -348,8 +344,6 @@ def _recv(self, expected_response_length, full): # noqa: C901 min_size = 4 elif isinstance(self.client.framer, ModbusAsciiFramer): min_size = 5 - elif isinstance(self.client.framer, ModbusBinaryFramer): - min_size = 3 else: min_size = expected_response_length @@ -367,8 +361,6 @@ def _recv(self, expected_response_length, full): # noqa: C901 func_code = int(read_min[1]) elif isinstance(self.client.framer, ModbusAsciiFramer): func_code = int(read_min[3:5], 16) - elif isinstance(self.client.framer, ModbusBinaryFramer): - func_code = int(read_min[-1]) else: func_code = -1 diff --git a/test/message/server_multidrop_tbd.py b/test/message/test_multidrop.py similarity index 100% rename from test/message/server_multidrop_tbd.py rename to test/message/test_multidrop.py diff --git a/test/message/test_rtu.py b/test/message/test_rtu.py index 0eb8e5a59..6892ab368 100644 --- a/test/message/test_rtu.py +++ b/test/message/test_rtu.py @@ -22,10 +22,10 @@ def prepare_frame(): (b'\x01\x03\x01\x00\x0a', 1, b'\x01\x01\x03\x01\x00\x0a\xed\x89'), ], ) - def xtest_roundtrip(self, frame, data, dev_id, res_msg): + def test_roundtrip(self, frame, data, dev_id, res_msg): """Test encode.""" - msg = frame.encode(data, dev_id, 0) - res_len, _, res_id, res_data = frame.decode(msg) - assert data == res_data - assert dev_id == res_id - assert res_len == len(res_msg) + # msg = frame.encode(data, dev_id, 0) + # res_len, _, res_id, res_data = frame.decode(msg) + # assert data == res_data + # assert dev_id == res_id + # assert res_len == len(res_msg) diff --git a/test/sub_client/test_client_sync.py b/test/sub_client/test_client_sync.py index 7d284f4a0..1782c8575 100755 --- a/test/sub_client/test_client_sync.py +++ b/test/sub_client/test_client_sync.py @@ -17,7 +17,6 @@ from pymodbus.exceptions import ConnectionException from pymodbus.transaction import ( ModbusAsciiFramer, - ModbusBinaryFramer, ModbusRtuFramer, ModbusSocketFramer, ModbusTlsFramer, @@ -314,10 +313,6 @@ def test_sync_serial_client_instantiation(self): ModbusSerialClient("/dev/null", framer=Framer.RTU).framer, ModbusRtuFramer, ) - assert isinstance( - ModbusSerialClient("/dev/null", framer=Framer.BINARY).framer, - ModbusBinaryFramer, - ) assert isinstance( ModbusSerialClient("/dev/null", framer=Framer.SOCKET).framer, ModbusSocketFramer, diff --git a/test/sub_examples/test_examples.py b/test/sub_examples/test_examples.py index 65442ed6d..1cf0ead7c 100755 --- a/test/sub_examples/test_examples.py +++ b/test/sub_examples/test_examples.py @@ -43,12 +43,12 @@ def get_port_in_class(base_ports): base_ports[__class__.__name__] += 1 return base_ports[__class__.__name__] - @pytest.mark.parametrize("framer", ["socket", "rtu", "ascii", "binary"]) + @pytest.mark.parametrize("framer", ["socket", "rtu", "ascii"]) def test_message_generator(self, framer): """Test all message generator.""" generate_messages(cmdline=["--framer", framer]) - @pytest.mark.parametrize("framer", ["socket", "rtu", "ascii", "binary"]) + @pytest.mark.parametrize("framer", ["socket", "rtu", "ascii"]) def test_message_parser(self, framer): """Test message parser.""" main_parse_messages(["--framer", framer, "-m", "000100000006010100200001"]) diff --git a/test/test_framers.py b/test/test_framers.py index 0eafac228..b43c3f08e 100644 --- a/test/test_framers.py +++ b/test/test_framers.py @@ -10,7 +10,6 @@ from pymodbus.factory import ClientDecoder from pymodbus.framer import ( ModbusAsciiFramer, - ModbusBinaryFramer, ModbusRtuFramer, ModbusSocketFramer, ) @@ -47,7 +46,7 @@ def fixture_ascii_framer(): [ ModbusRtuFramer, ModbusAsciiFramer, - ModbusBinaryFramer, + ModbusSocketFramer, ], ) def test_framer_initialization(self, framer): @@ -90,13 +89,7 @@ def test_framer_initialization(self, framer): "len": 0, "uid": 0x00, } - assert framer._hsize == 0x01 # pylint: disable=protected-access - assert framer._start == b"\x7b" # pylint: disable=protected-access - assert framer._end == b"\x7d" # pylint: disable=protected-access - assert framer._repeat == [ # pylint: disable=protected-access - b"}"[0], - b"{"[0], - ] + assert framer._hsize == 0x07 # pylint: disable=protected-access @pytest.mark.parametrize( @@ -212,7 +205,8 @@ def callback(data): rtu_framer.processIncomingPacket(data, callback, self.slaves) assert not count - + callback(b'') + assert count @pytest.mark.parametrize( ("data", "header"), @@ -371,6 +365,8 @@ def callback(data): data = TEST_MESSAGE rtu_framer.processIncomingPacket(data, callback, self.slaves) assert not count + callback(b'') + assert count @pytest.mark.parametrize(("slaves", "res"), [([16], 0), ([17], 1)]) def test_validate__slave_id(self,rtu_framer, slaves, res): @@ -453,7 +449,6 @@ def _handle_response(_reply): ("framer", "message"), [ (ModbusAsciiFramer, b':00010001000AF4\r\n',), - (ModbusBinaryFramer, b'{\x00\x01\x00\x01\x00\n\xec\x1c}',), (ModbusRtuFramer, b"\x00\x01\x00\x01\x00\n\xec\x1c",), (ModbusSocketFramer, b'\x00\x00\x00\x00\x00\x06\x00\x01\x00\x01\x00\n',), ] @@ -469,7 +464,6 @@ def test_build_packet(self, framer, message): ("framer", "message"), [ (ModbusAsciiFramer, b':01010001000AF3\r\n',), - (ModbusBinaryFramer, b'A{\x01\x01\x00\x01\x00\n\xed\xcd}',), (ModbusRtuFramer, b"\x01\x01\x03\x01\x00\n\xed\x89",), (ModbusSocketFramer, b'\x00\x00\x00\x00\x00\x06\x01\x01\x00\x01\x00\n',), ] @@ -485,7 +479,6 @@ def test_processincomingpacket_ok(self, framer, message, slave): ("framer", "message"), [ (ModbusAsciiFramer, b':01270001000ACD\r\n',), - (ModbusBinaryFramer, b'{\x01\x1a\x00\x01\x00\n\x89\xcf}',), (ModbusRtuFramer, b"\x01\x03\x03\x01\x00\n\x94\x49",), (ModbusSocketFramer, b'\x00\x00\x00\x00\x00\x06\x01\x27\x00\x01\x00\n',), ] @@ -500,7 +493,6 @@ def test_processincomingpacket_not_ok(self, framer, message): ("framer", "message"), [ (ModbusAsciiFramer, b':61620001000AF4\r\n',), - (ModbusBinaryFramer, b'{\x61\x62\x00\x01\x00\n\xec\x1c}',), (ModbusRtuFramer, b"\x61\x62\x00\x01\x00\n\xec\x1c",), (ModbusSocketFramer, b'\x00\x00\x00\x00\x00\x06\x61\x62\x00\x01\x00\n',), ] @@ -514,8 +506,3 @@ def test_decode_data(self, framer, message, expected): decoded = test_framer.decode_data(message) assert decoded["fcode"] == expected["fcode"] assert decoded["slave"] == expected["slave"] - - def test_binary_framer_preflight(self): - """Test binary framer _preflight.""" - test_framer = ModbusBinaryFramer(ClientDecoder()) - assert test_framer._preflight(b'A{B}C') == b'A{{B}}C' # pylint: disable=protected-access diff --git a/test/test_network.py b/test/test_network.py index 93844323f..bb2e032b2 100644 --- a/test/test_network.py +++ b/test/test_network.py @@ -27,9 +27,7 @@ def __init__( async def start_run(self): """Call need functions to start server/client.""" - if self.is_server: - return await self.listen() - return await self.connect() + return await self.listen() def callback_connected(self) -> None: diff --git a/test/test_transaction.py b/test/test_transaction.py index 97a53a1a3..0e237554e 100755 --- a/test/test_transaction.py +++ b/test/test_transaction.py @@ -9,7 +9,6 @@ from pymodbus.pdu import ModbusRequest from pymodbus.transaction import ( ModbusAsciiFramer, - ModbusBinaryFramer, ModbusRtuFramer, ModbusSocketFramer, ModbusTlsFramer, @@ -29,7 +28,6 @@ class TestTransaction: # pylint: disable=too-many-public-methods _tls = None _rtu = None _ascii = None - _binary = None _manager = None _tm = None @@ -44,7 +42,6 @@ def setup_method(self): self._tls = ModbusTlsFramer(decoder=self.decoder, client=None) self._rtu = ModbusRtuFramer(decoder=self.decoder, client=None) self._ascii = ModbusAsciiFramer(decoder=self.decoder, client=None) - self._binary = ModbusBinaryFramer(decoder=self.decoder, client=None) self._manager = ModbusTransactionManager(self.client) # ----------------------------------------------------------------------- # @@ -69,7 +66,6 @@ def test_calculate_exception_length(self): """Test calculate exception length.""" for framer, exception_length in ( ("ascii", 11), - ("binary", 7), ("rtu", 5), ("tcp", 9), ("tls", 2), @@ -78,8 +74,6 @@ def test_calculate_exception_length(self): self._manager.client = mock.MagicMock() if framer == "ascii": self._manager.client.framer = self._ascii - elif framer == "binary": - self._manager.client.framer = self._binary elif framer == "rtu": self._manager.client.framer = self._rtu elif framer == "tcp": @@ -724,82 +718,3 @@ def callback(data): msg = b":F7031389000A60\r\n" self._ascii.processIncomingPacket(msg, callback, [0,1]) assert result - - # ----------------------------------------------------------------------- # - # Binary tests - # ----------------------------------------------------------------------- # - def test_binary_framer_transaction_ready(self): - """Test a binary frame transaction.""" - count = 0 - result = None - def callback(data): - """Simulate callback.""" - nonlocal count, result - count += 1 - result = data - - msg = TEST_MESSAGE - self._binary.processIncomingPacket(msg, callback, [0,1]) - assert result - - def test_binary_framer_transaction_full(self): - """Test a full binary frame transaction.""" - count = 0 - result = None - def callback(data): - """Simulate callback.""" - nonlocal count, result - count += 1 - result = data - - msg = TEST_MESSAGE - self._binary.processIncomingPacket(msg, callback, [0,1]) - assert result - - def test_binary_framer_transaction_half(self): - """Test a half completed binary frame transaction.""" - count = 0 - result = None - def callback(data): - """Simulate callback.""" - nonlocal count, result - count += 1 - result = data - - msg_parts = (b"\x7b\x01\x03\x00", b"\x00\x00\x05\x85\xC9\x7d") - self._binary.processIncomingPacket(msg_parts[0], callback, [0,1]) - assert not result - self._binary.processIncomingPacket(msg_parts[1], callback, [0,1]) - assert result - - def test_binary_framer_populate(self): - """Test a binary frame packet build.""" - request = ModbusRequest() - self._binary.populateResult(request) - assert not request.slave_id - - def test_binary_framer_packet(self): - """Test a binary frame packet build.""" - old_encode = ModbusRequest.encode - ModbusRequest.encode = lambda self: b"" - message = ModbusRequest() - message.slave_id = 0xFF - message.function_code = 0x01 - expected = b"\x7b\xff\x01\x81\x80\x7d" - actual = self._binary.buildPacket(message) - assert expected == actual - ModbusRequest.encode = old_encode - - def test_binary_process_incoming_packet(self): - """Test binary process incoming packet.""" - mock_data = TEST_MESSAGE - slave = 0x00 - - def mock_callback(_mock_data): - pass - - self._binary.processIncomingPacket(mock_data, mock_callback, slave) - - # Test failure: - self._binary.checkFrame = mock.MagicMock(return_value=False) - self._binary.processIncomingPacket(mock_data, mock_callback, slave) From 331dc636b36349df7233d58252b58c6e68e32455 Mon Sep 17 00:00:00 2001 From: jan iversen Date: Thu, 28 Mar 2024 18:26:18 +0100 Subject: [PATCH 18/88] Coverage == 91%. (#2132) --- pymodbus/framer/socket_framer.py | 5 +---- pymodbus/framer/tls_framer.py | 12 ++---------- pymodbus/pdu.py | 3 +++ test/test_framers.py | 2 ++ 4 files changed, 8 insertions(+), 14 deletions(-) diff --git a/pymodbus/framer/socket_framer.py b/pymodbus/framer/socket_framer.py index 48616e52f..8460c8a15 100644 --- a/pymodbus/framer/socket_framer.py +++ b/pymodbus/framer/socket_framer.py @@ -75,10 +75,7 @@ def frameProcessIncomingPacket(self, single, callback, slave, tid=None, **kwargs while True: used_len, use_tid, dev_id, data = self.message_handler.decode(self._buffer) if not data: - if not used_len: - return - self._buffer = self._buffer[used_len :] - continue + return self._header["uid"] = dev_id self._header["tid"] = use_tid self._header["pid"] = 0 diff --git a/pymodbus/framer/tls_framer.py b/pymodbus/framer/tls_framer.py index 5b88c1e34..a39de0321 100644 --- a/pymodbus/framer/tls_framer.py +++ b/pymodbus/framer/tls_framer.py @@ -6,7 +6,6 @@ ModbusIOException, ) from pymodbus.framer.base import TLS_FRAME_HEADER, ModbusFramer -from pymodbus.logging import Log from pymodbus.message.tls import MessageTLS @@ -44,25 +43,18 @@ def decode_data(self, data): return {"fcode": fcode} return {} - def frameProcessIncomingPacket(self, single, callback, slave, _tid=None, **kwargs): + def frameProcessIncomingPacket(self, _single, callback, _slave, _tid=None, **kwargs): """Process new packet pattern.""" # no slave id for Modbus Security Application Protocol while True: used_len, use_tid, dev_id, data = self.message_handler.decode(self._buffer) if not data: - if not used_len: - return - self._buffer = self._buffer[used_len :] - continue + return self._header["uid"] = dev_id self._header["tid"] = use_tid self._header["pid"] = 0 - if not self._validate_slave_id(slave, single): - Log.debug("Not in valid slave id - {}, ignoring!!", slave) - self.resetFrame() - return if (result := self.decoder.decode(data)) is None: raise ModbusIOException("Unable to decode request") self.populateResult(result) diff --git a/pymodbus/pdu.py b/pymodbus/pdu.py index 64c48b1e1..13dd7981b 100644 --- a/pymodbus/pdu.py +++ b/pymodbus/pdu.py @@ -249,6 +249,9 @@ def __init__(self, function_code, **kwargs): def decode(self, _data): """Decode so this failure will run correctly.""" + def encode(self): + """Decode so this failure will run correctly.""" + def execute(self, _context): """Build an illegal function request error response. diff --git a/test/test_framers.py b/test/test_framers.py index b43c3f08e..280634d98 100644 --- a/test/test_framers.py +++ b/test/test_framers.py @@ -12,6 +12,7 @@ ModbusAsciiFramer, ModbusRtuFramer, ModbusSocketFramer, + ModbusTlsFramer, ) from pymodbus.transport import CommType from pymodbus.utilities import ModbusTransactionState @@ -481,6 +482,7 @@ def test_processincomingpacket_ok(self, framer, message, slave): (ModbusAsciiFramer, b':01270001000ACD\r\n',), (ModbusRtuFramer, b"\x01\x03\x03\x01\x00\n\x94\x49",), (ModbusSocketFramer, b'\x00\x00\x00\x00\x00\x06\x01\x27\x00\x01\x00\n',), + (ModbusTlsFramer, b'\x54\x00\x7c\x00\x02',), ] ) def test_processincomingpacket_not_ok(self, framer, message): From 5b1c1951f9e573fbe3548e2504c43a095a58f7bf Mon Sep 17 00:00:00 2001 From: jan iversen Date: Thu, 28 Mar 2024 21:18:05 +0100 Subject: [PATCH 19/88] Streamline message class. (#2133) --- pymodbus/framer/ascii_framer.py | 2 +- pymodbus/framer/rtu_framer.py | 2 +- pymodbus/framer/socket_framer.py | 2 +- pymodbus/framer/tls_framer.py | 2 +- pymodbus/message/base.py | 18 ++---------------- pymodbus/message/message.py | 18 ++++++++++++------ pymodbus/message/raw.py | 3 --- pymodbus/message/rtu.py | 2 +- test/message/conftest.py | 1 + test/message/test_ascii.py | 2 +- test/message/test_message.py | 17 ++++++++--------- test/message/test_rtu.py | 2 +- test/message/test_socket.py | 2 +- test/message/test_tls.py | 2 +- 14 files changed, 32 insertions(+), 43 deletions(-) diff --git a/pymodbus/framer/ascii_framer.py b/pymodbus/framer/ascii_framer.py index 8ce7a67b4..4d2fbc68a 100644 --- a/pymodbus/framer/ascii_framer.py +++ b/pymodbus/framer/ascii_framer.py @@ -39,7 +39,7 @@ def __init__(self, decoder, client=None): self._hsize = 0x02 self._start = b":" self._end = b"\r\n" - self.message_handler = MessageAscii([0], True) + self.message_handler = MessageAscii() def decode_data(self, data): """Decode data.""" diff --git a/pymodbus/framer/rtu_framer.py b/pymodbus/framer/rtu_framer.py index dd6ec817c..6455a28f8 100644 --- a/pymodbus/framer/rtu_framer.py +++ b/pymodbus/framer/rtu_framer.py @@ -61,7 +61,7 @@ def __init__(self, decoder, client=None): self._end = b"\x0d\x0a" self._min_frame_size = 4 self.function_codes = decoder.lookup.keys() if decoder else {} - self.message_handler = MessageRTU([0], True) + self.message_handler = MessageRTU() def decode_data(self, data): """Decode data.""" diff --git a/pymodbus/framer/socket_framer.py b/pymodbus/framer/socket_framer.py index 8460c8a15..582aa283a 100644 --- a/pymodbus/framer/socket_framer.py +++ b/pymodbus/framer/socket_framer.py @@ -43,7 +43,7 @@ def __init__(self, decoder, client=None): """ super().__init__(decoder, client) self._hsize = 0x07 - self.message_handler = MessageSocket([0], True) + self.message_handler = MessageSocket() def decode_data(self, data): """Decode data.""" diff --git a/pymodbus/framer/tls_framer.py b/pymodbus/framer/tls_framer.py index a39de0321..1b341b224 100644 --- a/pymodbus/framer/tls_framer.py +++ b/pymodbus/framer/tls_framer.py @@ -34,7 +34,7 @@ def __init__(self, decoder, client=None): """ super().__init__(decoder, client) self._hsize = 0x0 - self.message_handler = MessageTLS([0], True) + self.message_handler = MessageTLS() def decode_data(self, data): """Decode data.""" diff --git a/pymodbus/message/base.py b/pymodbus/message/base.py index 831cb57b2..3a41bb9eb 100644 --- a/pymodbus/message/base.py +++ b/pymodbus/message/base.py @@ -14,23 +14,9 @@ class MessageBase: EMPTY = b'' - def __init__( - self, - device_ids: list[int], - is_server: bool, - ) -> None: - """Initialize a message instance. - - :param device_ids: list of device id to accept (server only), None for all. - """ - self.device_ids = device_ids - self.is_server = is_server - self.broadcast: bool = (0 in device_ids) - + def __init__(self) -> None: + """Initialize a message instance.""" - def validate_device_id(self, dev_id: int) -> bool: - """Check if device id is expected.""" - return self.broadcast or (dev_id in self.device_ids) @abstractmethod def decode(self, _data: bytes) -> tuple[int, int, int, bytes]: diff --git a/pymodbus/message/message.py b/pymodbus/message/message.py index 0f1967843..20427bd23 100644 --- a/pymodbus/message/message.py +++ b/pymodbus/message/message.py @@ -65,15 +65,21 @@ def __init__(self, """ super().__init__(params, is_server) self.device_ids = device_ids - self.message_type = message_type + self.broadcast: bool = (0 in device_ids) self.msg_handle: MessageBase = { - MessageType.RAW: MessageRaw(device_ids, is_server), - MessageType.ASCII: MessageAscii(device_ids, is_server), - MessageType.RTU: MessageRTU(device_ids, is_server), - MessageType.SOCKET: MessageSocket(device_ids, is_server), - MessageType.TLS: MessageTLS(device_ids, is_server), + MessageType.RAW: MessageRaw(), + MessageType.ASCII: MessageAscii(), + MessageType.RTU: MessageRTU(), + MessageType.SOCKET: MessageSocket(), + MessageType.TLS: MessageTLS(), }[message_type] + + def validate_device_id(self, dev_id: int) -> bool: + """Check if device id is expected.""" + return self.broadcast or (dev_id in self.device_ids) + + def callback_data(self, data: bytes, addr: tuple | None = None) -> int: """Handle received data.""" tot_len = len(data) diff --git a/pymodbus/message/raw.py b/pymodbus/message/raw.py index e75324556..88627482b 100644 --- a/pymodbus/message/raw.py +++ b/pymodbus/message/raw.py @@ -23,9 +23,6 @@ def decode(self, data: bytes) -> tuple[int, int, int, bytes]: return 0, 0, 0, self.EMPTY dev_id = int(data[0]) tid = int(data[1]) - if not self.validate_device_id(dev_id): - Log.debug("Device id: {} in frame {} unknown, skipping.", dev_id, data, ":hex") - return len(data), dev_id, tid, data[2:] def encode(self, data: bytes, device_id: int, tid: int) -> bytes: diff --git a/pymodbus/message/rtu.py b/pymodbus/message/rtu.py index fa5d7dad3..2565fa5a9 100644 --- a/pymodbus/message/rtu.py +++ b/pymodbus/message/rtu.py @@ -177,7 +177,7 @@ def callback(result): nonlocal resp resp = result - self._legacy_decode(callback, self.device_ids) + self._legacy_decode(callback, [0]) return 0, 0, 0, b'' diff --git a/test/message/conftest.py b/test/message/conftest.py index cd5345d01..dcf2e3369 100644 --- a/test/message/conftest.py +++ b/test/message/conftest.py @@ -21,6 +21,7 @@ def __init__(self, """Initialize a message instance.""" super().__init__(message_type, params, is_server, device_ids) self.send = mock.Mock() + self.message_type = message_type def callback_new_connection(self) -> ModbusProtocol: """Call when listener receive new connection request.""" diff --git a/test/message/test_ascii.py b/test/message/test_ascii.py index 1235e646e..e166828c3 100644 --- a/test/message/test_ascii.py +++ b/test/message/test_ascii.py @@ -11,7 +11,7 @@ class TestMessageAscii: @pytest.fixture(name="frame") def prepare_frame(): """Return message object.""" - return MessageAscii([1], False) + return MessageAscii() @pytest.mark.parametrize( diff --git a/test/message/test_message.py b/test/message/test_message.py index 22542ece1..113ec8f8c 100644 --- a/test/message/test_message.py +++ b/test/message/test_message.py @@ -74,7 +74,7 @@ async def test_message_build_send(self, msg): ]) async def test_validate_id(self, msg, dev_id, res): """Test message type.""" - assert res == msg.msg_handle.validate_device_id(dev_id) + assert res == msg.validate_device_id(dev_id) @pytest.mark.parametrize( ("data", "res_len", "res_id", "res_tid", "res_data"), [ @@ -159,7 +159,7 @@ class TestMessages: b'\x11\x03\x00\x7c\x00\x02\x07\x43', b'\x11\x03\x04\x00\x8d\x00\x8e\xfb\xbd', b'\x11\x83\x02\xc1\x34', - b'\xff\x03\x00|\x00\x02\x10\x0d', + b'\xff\x03\x00\x7c\x00\x02\x10\x0d', b'\xff\x03\x04\x00\x8d\x00\x8e\xf5\xb3', b'\xff\x83\x02\xa1\x01', ]), @@ -215,11 +215,10 @@ class TestMessages: ) def test_encode(self, frame, frame_expected, data, dev_id, tid, inx1, inx2, inx3): """Test encode method.""" - if frame != MessageSocket and tid: - pytest.skip("Not supported") - if frame == MessageTLS and (tid or dev_id): - pytest.skip("Not supported") - frame_obj = frame([0], True) + if ((frame != MessageSocket and tid) or + (frame == MessageTLS and dev_id)): + return + frame_obj = frame() expected = frame_expected[inx1 + inx2 + inx3] encoded_data = frame_obj.encode(data, dev_id, tid) assert encoded_data == expected @@ -281,7 +280,7 @@ async def test_decode(self, dummy_message, msg_type, data, dev_id, tid, expected if msg_type == MessageType.RTU: pytest.skip("Waiting on implementation!") if msg_type == MessageType.TLS and split != "no": - pytest.skip("Not supported.") + return frame = dummy_message( msg_type, CommParams(), @@ -323,7 +322,7 @@ async def test_decode_bad_crc(self, frame, data, exp_len): """Test encode method.""" if frame == MessageRTU: pytest.skip("Waiting for implementation.") - frame_obj = frame([0], True) + frame_obj = frame() used_len, _, _, data = frame_obj.decode(data) assert used_len == exp_len assert not data diff --git a/test/message/test_rtu.py b/test/message/test_rtu.py index 6892ab368..e7c019b99 100644 --- a/test/message/test_rtu.py +++ b/test/message/test_rtu.py @@ -11,7 +11,7 @@ class TestMessageRTU: @pytest.fixture(name="frame") def prepare_frame(): """Return message object.""" - return MessageRTU([1], False) + return MessageRTU() @pytest.mark.parametrize( diff --git a/test/message/test_socket.py b/test/message/test_socket.py index 1c4e930ce..e58ba65cc 100644 --- a/test/message/test_socket.py +++ b/test/message/test_socket.py @@ -12,7 +12,7 @@ class TestMessageSocket: @pytest.fixture(name="frame") def prepare_frame(): """Return message object.""" - return MessageSocket([1], False) + return MessageSocket() @pytest.mark.parametrize( diff --git a/test/message/test_tls.py b/test/message/test_tls.py index d9140c2cb..194fda459 100644 --- a/test/message/test_tls.py +++ b/test/message/test_tls.py @@ -12,7 +12,7 @@ class TestMessageSocket: @pytest.fixture(name="frame") def prepare_frame(): """Return message object.""" - return MessageTLS([1], False) + return MessageTLS() @pytest.mark.parametrize( From 95877df2ae739488c80eabc9e4ddd8d0b89ee38c Mon Sep 17 00:00:00 2001 From: Dominique Martinet Date: Fri, 29 Mar 2024 18:39:19 +0900 Subject: [PATCH 20/88] Fix link to github in README (#2134) --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 33911f0ef..ba4abb10e 100644 --- a/README.rst +++ b/README.rst @@ -20,7 +20,7 @@ and all API changes are `documented `_ that helps make pymodbus a great project. -Source code on `github `_ +Source code on `github `_ Pymodbus in a nutshell ---------------------- From 2e31260abbff7dd27220fc0dd413b852699f5b0e Mon Sep 17 00:00:00 2001 From: jan iversen Date: Fri, 29 Mar 2024 13:49:28 +0100 Subject: [PATCH 21/88] Secure testing is done with pymodbus in PR. (#2136) --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f1bec94bd..781dabe78 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -73,7 +73,7 @@ jobs: run: | python -m venv ${{ env.VIRTUAL_ENV }} python -m pip install --upgrade pip - pip install ".[all]" + pip install -e ".[all]" - name: codespell if: matrix.run_doc == true From 9f736dfe61e5a7281591ebc948bcf5dcf3d97ff7 Mon Sep 17 00:00:00 2001 From: jan iversen Date: Fri, 29 Mar 2024 19:45:23 +0100 Subject: [PATCH 22/88] Merge new message layer and old framer directory. (#2135) * message ==> framers. * Rename message to framing. * Framer ==> FramerType. * Update doc. * Move complete. --- API_changes.rst | 1 + check_ci.sh | 2 +- doc/source/_static/examples.tgz | Bin 50801 -> 50758 bytes doc/source/_static/examples.zip | Bin 37218 -> 37192 bytes .../library/architecture/architecture.rst | 9 +- doc/source/library/framer.rst | 26 +- examples/client_custom_msg.py | 4 +- examples/client_performance.py | 6 +- examples/package_test_tool.py | 6 +- examples/server_hook.py | 4 +- examples/simple_async_client.py | 6 +- examples/simple_sync_client.py | 6 +- examples/simulator.py | 4 +- pymodbus/__init__.py | 4 +- pymodbus/client/base.py | 6 +- pymodbus/client/serial.py | 6 +- pymodbus/client/tcp.py | 6 +- pymodbus/client/tls.py | 6 +- pymodbus/client/udp.py | 6 +- pymodbus/framer/__init__.py | 33 +- pymodbus/{message => framer}/ascii.py | 4 +- pymodbus/framer/base.py | 160 +--- .../{message/message.py => framer/framer.py} | 51 +- .../{ascii_framer.py => old_framer_ascii.py} | 9 +- pymodbus/framer/old_framer_base.py | 150 ++++ .../{rtu_framer.py => old_framer_rtu.py} | 10 +- ...{socket_framer.py => old_framer_socket.py} | 8 +- .../{tls_framer.py => old_framer_tls.py} | 8 +- pymodbus/{message => framer}/raw.py | 10 +- pymodbus/{message => framer}/rtu.py | 37 +- pymodbus/{message => framer}/socket.py | 17 +- pymodbus/framer/tls.py | 20 + pymodbus/message/__init__.py | 7 - pymodbus/message/base.py | 38 - pymodbus/message/tls.py | 25 - pymodbus/server/async_io.py | 18 +- pymodbus/transaction.py | 10 +- test/conftest.py | 19 +- test/{message => framers}/__init__.py | 0 test/{message => framers}/conftest.py | 6 +- test/{message => framers}/generator.py | 0 test/{message => framers}/test_ascii.py | 10 +- .../test_framer.py} | 150 ++-- .../test_old_framers.py} | 4 +- test/{message => framers}/test_rtu.py | 10 +- test/{message => framers}/test_socket.py | 10 +- .../test_tbc_multidrop.py} | 2 +- test/framers/test_tbc_transaction.py | 721 ++++++++++++++++++ test/{message => framers}/test_tls.py | 10 +- test/sub_client/test_client.py | 38 +- test/sub_client/test_client_sync.py | 14 +- test/sub_server/test_server_asyncio.py | 8 +- test/sub_server/test_server_multidrop.py | 174 ----- test/test_transaction.py | 1 + 54 files changed, 1219 insertions(+), 681 deletions(-) rename pymodbus/{message => framer}/ascii.py (97%) rename pymodbus/{message/message.py => framer/framer.py} (72%) rename pymodbus/framer/{ascii_framer.py => old_framer_ascii.py} (93%) create mode 100644 pymodbus/framer/old_framer_base.py rename pymodbus/framer/{rtu_framer.py => old_framer_rtu.py} (97%) rename pymodbus/framer/{socket_framer.py => old_framer_socket.py} (94%) rename pymodbus/framer/{tls_framer.py => old_framer_tls.py} (92%) rename pymodbus/{message => framer}/raw.py (79%) rename pymodbus/{message => framer}/rtu.py (91%) rename pymodbus/{message => framer}/socket.py (72%) create mode 100644 pymodbus/framer/tls.py delete mode 100644 pymodbus/message/__init__.py delete mode 100644 pymodbus/message/base.py delete mode 100644 pymodbus/message/tls.py rename test/{message => framers}/__init__.py (100%) rename test/{message => framers}/conftest.py (91%) rename test/{message => framers}/generator.py (100%) rename test/{message => framers}/test_ascii.py (91%) rename test/{message/test_message.py => framers/test_framer.py} (63%) rename test/{test_framers.py => framers/test_old_framers.py} (99%) rename test/{message => framers}/test_rtu.py (84%) rename test/{message => framers}/test_socket.py (91%) rename test/{message/test_multidrop.py => framers/test_tbc_multidrop.py} (99%) create mode 100755 test/framers/test_tbc_transaction.py rename test/{message => framers}/test_tls.py (87%) delete mode 100644 test/sub_server/test_server_multidrop.py diff --git a/API_changes.rst b/API_changes.rst index 6e4fb08be..4ec98de3f 100644 --- a/API_changes.rst +++ b/API_changes.rst @@ -10,6 +10,7 @@ API changes 3.7.0 - on_reconnect_callback() removed from clients (sync/async). - on_connect_callback(true/false) added to async clients. - binary framer no longer supported +- Framer. renamed to FramerType. API changes 3.6.0 diff --git a/check_ci.sh b/check_ci.sh index 109b947ba..873c2311e 100755 --- a/check_ci.sh +++ b/check_ci.sh @@ -9,5 +9,5 @@ codespell ruff check --fix --exit-non-zero-on-fix . pylint --recursive=y examples pymodbus test mypy pymodbus -pytest --cov --numprocesses auto +pytest -x --cov --numprocesses auto echo "Ready to push" diff --git a/doc/source/_static/examples.tgz b/doc/source/_static/examples.tgz index 87b9d5fbc8bf2d9d48f4f18336b4b07133be0f5f..16fc99a5a226104b30f27b4b2f874bb3339a2b4e 100644 GIT binary patch literal 50758 zcmV)gK%~DPiwFS7xdvtc1MI!qavMpOAShMWZfCQ1XR2p*&H81`q9AK)Kna2X2$Cvp zqpWmcX3A12sYxnx+E_Gn5D1V_0uiW)fG8H5X7#pa+y5}XFt7V1`!Dk}doF$t4_qjc zsjNg~QUE;sa@_s+@#DwOCHfeiPexI;@!;;BX0y4owH46kHh(tT9sU$g(CTbATU(o* zomMkww%XgR=7V7C4q;NyG|R&jK>0&Bo|zA_4Tm${i+-rjpU4x*`R|Y7Xq@-@FdoTy z?>swgOfGM!joI_xYHn|~)%O#-aT1R=qVb1d za+#kc2^W9(&XW_{x>Ym407LQNY^UF!JK1lj&l}hF9<#{qVnP$Pk<+y*A zCgTLYKPN;A_5@-z*47TsqF|VeM#%+K5X4y!jst;rK{5=^l8Ycuf@u~7IpoZYxaNC_RKhwBI*Q;=D&qpGR31o<_33KBPTIX!fH&PNOV$iZReK^HYR`dw$mp=kD$Ijj6og+m&r5@m=*wFb%Z%P;L2H|G0ERyb4&rg2(RVl<_X$1w z$si(RA7S=OD0CWUd6Y&2MpDc?h%i?g48wk&q?dK5DL;cU7x8EmoXGbBoI)EWo`FX2 za)?6}sFeZP#?b{-a~{D$9x&8U;W)`*IMWoeK>t+sXi%r}=P=R=K!Qwk8)^#21Jxiy z9*Q;o!iK(a>oc^r)*po#kQ71$8~dvE&l)diSe4HO*6^}ZLxt;=vd9VQ(FApatu0FK zrTMgXa+ybvK81~Nl4S6*&1DABFzEH-F|5&EHH${Wdhj6}!2oq%C*!CdJbwKCB1})S znkFmI-b=_IhoksU+`4>Y1Kx1zRaL3{nBLz`vNrR(f4O}$F-cS`b414rKParJ! z#+u5on@0IG9cR0=6u6Q>FbVtbqg<^!qc)Rh0xV4F8koOY`R%hxJs3sfssV2;cpS8K zPlf=s!m@a5_DlmUtN+ri{~M@ZmaZ9)U=Of_9t~bLVn7FB9@=BVcw0IqlMrY$72{zM z!+{C7PjThmQPJoFg9DpI70VnokIUu~X3!j(7{??H$EOkOJZ5EI2JpLGv&YD&su4~m z(Rff5<24-{BSs*^!7kMPWzasZ5jg&ZMJv8){VHEp^m?&XHN-uym%%!K7f(u8R5%#G zlzh^XVxSgVQKC5jNqn?oTOll=ds7|Atz|NKf%f%Y+h9^ks zov>Yp$aMfLBj7bgSrVLuA0l8tqA_AeJSG<8&bD`Caa-FalUsGUUQLJajbgo!U;?!$ zlB06+GM}qaeTRY9p7RvuN zw|7?Zzx#MfPI0zcR;pIYL*aBSDWh=$~QE##6UqyW^< zXeWrrQ^#_*q+<;yG1H0E>tm9Ic-n!$0f&$8jCbGxaRN1L53)qvSNW>H`77{i`0OX_D4q zFCPO?^>{o?>VxQHdOEWT63e}Pvwuk71Nls&;3`96Ge%~mo&iIWMRh4Y8Urshvpyng zFp008e!q7h#0TqVb=VxwqR~VjCP*ZUEFmUKK>&??^f6{Ky57fPr6Xz_Ops?kKc9|a z7;rbK>WBdm47d9dooGNb52G_s(@ z++6VlD^{~(6~$C5fky@E`4IPksSc;G9|8VyMGH=&h z5|Hx>rCg4fyrKIhGFPch<{A#vAiaQW&w$%{U$>7h zC%0FWR+Srd&EoPTJ?SCIs@f-H==wXNN%BBQvMmHg93JR+MYOI+=1wAq#!-ovf(4Gp z{gfrUS#qxGkHY?0kYB)&BZ9gHnP>(I!P3P;984P=mLnkmR;N;0!kPY+Qnnxqhm1zy zX^6)j1W+>2(@d~_l0_Z%Vdi>g(MK4(O{TawyuEl3;IY5i>M=`?ar0+R8}Mo~1Wwhi|A_GZDJi4Q2v^v}j8&xoFt z!d$3o;ORv1A3a{w)&BWl6ptg4ZrUe8!P!tTbr9ssDMbx&XB%A_ zWSi)PfRv8B*aM`RN{lfO^~ftCQ@)|dC)T8Efh__TFeWxXe)UzIE}g{nS0bvz<=kZ2 zB%$F;Avl4LNhqgAGLizchUyY-q8U)9eu&q-=i&HL?2DNDEIx^I<~!cUP%nUU5}kz~ zVnCMa$uuWg6^=#<+n+NX#f_Y#!?-`T~#+O1M}BeYW|9iUnLcrTUnL&?XHrOp!j4Ib#Xy+aV5yq z8*78nP+*_J+Ch{7D@3!>CG7L?=8cLH0qZs;{)$gQoXi~(wzdM+8*(;n>zSu4}n zY5>30#i-nUFpF9fl)tNJ7GD+fer)f~*lI&=(KS^ThlKX_YDJz;tRqZ?vodA0#uB?m z->iPo9!6bqHg9UIkHW)IwN{iB`ANa^5KR@yW*P| zS>KI>6UY<;(Gv*xV;^Bx&nG;t{?{bD93|o4HpM^l^uIQnJ1GCz+HSVCw^}C zR`Q?wcovZV2sNU&j2VWUhqiM`Nl|8yNnNg!X^b~XT9}Uv!Wx{*CnJ;s01tK^ox|Za z7=@R(JM)DAm7+7#4@vqS*b#UGJOC2cljI}R3ykNP6g~+=74)p+07_@eeli+GB*<1; zSZ~q+D&o9|QBO;xi3Q+2mBLj&hBsf(K!4^rl_^uxLU7?XB5GXzk zBAS_ocl4QUoxm3<)Pe4Z6B6*xvVBYy%px-32n#_$+FTPt;S-c?B1et~nawtB*V~-{ z`By$vQ0Z3@U`!SQUlXa3zQ7v^7-`Ce+;n>rNZ-yA;>58ktV7XSsTA$d2kkPp> z5Bt;1tevm(j4Hld;-Lbt00+EEg5ckRXKEtg2fk_r;!7L82mzFSzV!akUlcR?4CV`; ze=~pmn-~=OLL>8U{sOP!JcntZe4CwCo4)Kk+1l0xWR_wJT(N4(U%+gED^^Y2Y*>{H z1+WIus>j$WlzsRIo{BZUUS&$tfW==A^!HkQW)7wto=p5=PEAfGt1{!#oX^CS^$MJj zwp~5h>ae152#(sCz8x7IKfbOM%MrGD${AziDW`u+a@6(G06ny{?&aFP9Kf)8NER-ibm0Wcfbma%UvVrxmFSG0x7QHH6DE#}enOQJqx@Gz_ znl!}K`tAg!E~o)$tneQq;(~aaRjunzrmltnPH;@XTECX80pZ+d<4+qw18oB3;fV3dT8MXwYd=ljsr~~OeOCZbdJ}I>9S(Cq-S=7_){Ic5}?g^Ln2g!XqMnhNRrc6Jfdt`yA!q{Ls2N)T9(?hp zL0sJ8rW~UVsURT@_;HSF*VfaQC&Mc&dj0$S>MZ)m>hLu*3y3qRtIdgZw7sbX(I^uN z6UEaaZ%j+x%i*}xo`=t-gVuuwe-B^3c)*{(`dfe2FT`Kr`GxrWJFE}NKX?#7$RG6Jzs7^f?Ss5} zR?q5LJ*#K+te#tV9`eT*CHwyBSv{-gXZ#?22*l^_#OI&+^PzbE3-S3&qs*_wXCOX* zCqDnopASX8Ux?3Niq9{^=dZ+PAU=O5KL5<0f@nPyhmVJ%<%i;g^6*#UGZ3G@TdK>e zXZ8H79z47MEuGop2V?8}e))2g^iM{~$pc=}PvNieKk1>8Tnh3%=syVI@9aT@DNd>X z?v(o9AZPR-g?8cxL-@n8IizHExrhJ7Dfb^C=Lqsg042g0KFFZuF{FB6efaNlN@J@d z_%HL8{vS^1zjI1Yp!6lAB=9-#)$of`r-uLF)Gz~f8t4CJl>KYSmr%JBYB=$X?~L3h z7uiJQ{(nymdfAhuv`Do+++V8y|osX0MhY?W{_6PsFgQ5Rk?A0Lak4^x} zhs9SP4$?82kU!u(>Td`wM=;EYbiT>{FU|n|=LhFd-{8T?gDFf?50Es0k{2-5DWsOw z^6yS9{}IY!-VgNeJaqjZPU*jMN-u_R|II=;yM|c^2PpLLKNw~IlT&sIWd^iNkwl!9 zjiOb@YX4t7tLHO+{)#{T9?$=K8hsai{R!73L4JmY2hqS&So!M<1j<<|jFU^Kb2JJP_PorI2~N<}O*lcj@p#Ps zLC^(z(Q%n@r6UVT&t)gk5Iu@TjnhWKi<$8AVR|vU1CO`DBpgBi5T>y)ly*>)+jp`o zT6P15%nS-*`(9Iiu%&ZXRsz-zXH8dYDsx?ovQRxmyXniGaJJU#C*xs!%C6X?Av?QQ zF;Sw5j{NF@c6%#5-g=zf+8&iqsaNXIDu+s8KaW44lQwc*aw;vOW`bC)yx4p8?(2%M zb`Z+Lr;${=6^h02kbE4H=4{@dRJ8iC(hN37yWxarv27)&zqbCfa`|f|i@^}S5Up2_ zf;>Ktl4;&;HQhI9l&5i&bvJ!4E_=zi7oAV?%kEd<2)}Pv;L~(^SD`a z4P&E`IX2V5qGR)J@DmuD*+WBd3}y}odxl(aL=GskZ%{sy-b2}b&PIZ^0d+VXpu@}V zZ?>CGlVr{i|0(J|aV8aLB(zg`U9r=%Jei<%Pq*c^ein`gKsX2)dr|)^ae?%We?H0< zU8?rUPQQ9u9apd)oSO7menin^6XV?#Rvg4B3OZd$Spy=|nqGZ*ip z%S8)A)`|<&B+N3jJoCUMwqzV!a&2ysn8JK!k?WCQSa~;oKTa;jaz?Lk!mlyj0$H+E zOrvD#-Y+i@ktt{m!>VL=o5O;uNFoHzrt!UO+V+hEE@^P z&2IH7O|NUqHfWghq-nj#ek=op8o2@}kAlN*ULFMS|LZr0!PDUFzZ`z^=5?_5U!U*2 zJ$(7*bv;0jgXE_r9!QV)Zb&4jKNcQgnhaO~5IATgOUyI8DdCKW%cuuNY&-aY!3HWz z+U2*lY{&~t6pVgttJ1nb{)sJ$rG8gVo|9Hh8pW^|mf)OO#>HZv#SS7)!#O$FLaw#*- zj7VTqlHH!zMd95I&S;NkHX5CUbxqIy)MuHE5d^QSYZ@;xXyZcl8kQ81@!FmU%S=XX*uV=?d|QY)%ou} zo(0Z-g6JY*kf(ga4JYQ74|#-tX!My&WhiI`5^?kqtz1I`#pInpMq|#Hu7$xltkHgK zL<}PzzoJ3r77iowaJv?ANDPizr9y=nRZ|R$%&LJSEXW%0BuPM0l&3rz*HJMUGYsm|Ka(+h*GdauCw=ZepKZNlJ-H6rMBP#54 zxE18V1qHMple^U5>0~1OH%oqoffGCm1DB`aI77y727G#iN94ebrT_yT;Rw4RA4KTG zC%oXe-W=mQlYv!4P1Q6y57YOYCC-@5{89ybWGLhF=?LRch`Kb#*kX&C zBj6?}B`)J}A}_+{`EqZBCw*BC;X^!|C8{|$MKx|V8C_6jW9s0S=omtqg1~0B1mwqv zb>=k5LFhz#T;c_#y&Pa`LlLCUDJHrh%e_k6t|;`8Szf<;_3FDfFZRCcee>qvupT^z z^TAt;o}BS_bNJp0LaG8nAIjPeVC7crOjO9 z3A|ksRP`>-#QNS1(Ar49;L|ZAOB6gJKh@bWyQzdvX3c{2-v%hVD33W!R7wS%F2r`n z*FA9-ro~3A&Wl@uVkWz!mg0yG}?4<`Qb+j14@*^)RL&GOco!8i&u{u4%TF zhLYr}J!D_89y^dW4g`B;)q9l+j?G|fIPyesMo>VBp|(z}BE*pl&I1{l$QMo=W)|nR z4nfE5O}1e0GBJD_$no&+z3(PR+W)`u3P$=S4z?mThNwik&Wo;b4`sM9G`pWMXY zEY~iaknCv(QX(R#RlH3TKo~>s>bgP@_(ozJUG#K2dN^##Q34>N5%G&OhZF(ZfZB{L zFDt+XX}xwGS|zX!mXTb0EHm<>L9I}sZ<%|Gx`-kx02)e6I1%A492A=!pIcWEs%;(5 zsFLcMJoT=x5HQH=!{}mtGCcv-eEmFvv%(;gjifaN^vKz4Fcud^2<3n~K@DHb1;e&JHLcCIy(KuPhY zNg8b`#g$8O=Xsg9(|=E4K1I=|`H&b?b>GgXk;)RuDwq%U_Fq1IWrHocKCE2Xy}#bQ zQmq)#5Et)hxoCZMXU$HS;zxYaFb1A>pV5P5Se3o~{Wtp>smN!w(yX-Pk>XUr%Tul{ zB{)0FZ}H!OIT-mqYN zlY%bFEE2L)51=P%r>ZKZvsZqp>hc}Q?7_xR}Nl42CKCK!;!%9Z#o!N1p{9kqR(717b`u3&jV1cEg9DG|&Gj1S`t^OQkljY*mbX({&h$G5(PnS{I@CXg*%O+5R5Rdy3L z8@<#$nUKI5`lqbdYh2+5VXol|M$w08GzawrP)RBTiF}nStGl|!7#reOJ*e!SMIW^q zY7se$IMedXiGc(=eet{F!%nwCIhjvHp)Kdr{DX=IJO9WF=~3|Abd<*wh8eFl@#+=h z#YTg=x)25?^HiCcic)mL&DV5e2L%bO^Q1@hby=(pKRNzP&B$USnJD36l0ZOluej#m zi@1X9%|Mu*i!lP$TSXbMla-;2m@A1hLXTS^&0MXYM+rxM=O@vMi{r*#%%qsR3Xay#j^UqvaD`W#)4`<9+WGZ-lwIo+xA`2c z4}+^P>&J0D&8PJ&LB*Vz`A5N3A4A0gwXPEvUB3uZRQ{=>kgQHb=NiG71aIH$AC^_K zF16`{XHVa~*nfJshh^4D@_^ElWA5VmSse~k7#CCv0a)=2c3lb=D)dR|U>JRjnUt^h zp%|TOeM4c$6LJzrO~*p&S689xp?WCEl?@RF78{iemQ|y$R!~(9q6ujO!6|5cT?u^G zM_B79IbCEtt@^PH?W`8Gn42q}V8yim;sjVlF_kkF$KT4xqZJ4R7R(!9W>Hqnl2wK; zWekv%4(-}~*^Y^yLi(cglMqo*?{GnNC9fWmbMgfSgoHdKp%m>kPNV!4rB!=9G$!cv zYBCpwr+bByX4qvhUseoTXGW_d3!K0oDGx2iL>H(Q#c74o@t?o}&~9z6=4rxD_mfGRopL)14c%@8gnZYnc3Tk;y0>MSu` zp?9oi2mBzcKi1+@t3W=FNDze~j3<1^W)+=?q5)euh47dv_E{xKs~5^3e%LumtY6Yd zh&+3PXcS($hC$A2V0AxwENevd^K58SdAluD=t~tgjVb zm{N}P6!?!UlE2X_Pah?QLBCA$p+p@(#(PAimAp&}J$mrUoBaY9*2tfq-xW#?;_%c{ z0Fw)ONj_4{i^+w&=TYp*3aN#hNqz=QN--~`7jny;rEFy6`|^TiV`OLWnQw){N~36t z_7o+I=9$QQP!dNql0nM!+QL$)+x5CjPIsg6^qhEGx~dlUfg9=~PmIy+y4;0^Wxdeh zBN`aZRy%Xi*oRsEhMAOo$y4l$+vp_=nQo;7qg>U?g9^Qffh&Nc!prewnyYq#Il088 zZGC~RP?6<2;ucw;8_$tfEQ4OWoDH<17mXkAUwL_MRlHmoAJ}F6_Eod+pv@_KQ#cMt zH(6G+3=nR~*%MeSi38}F%(9G2VB^gOcxKTVog;cqg%#(|vGm-|&B$nX&dUFFl%t4Z z?*lO7&ru&v&r&cy>FE@;eW`*}>TuJK zMmQMsLh)WtQbj5h8J8#hvjlegtb0@;9V7HYm*!LaU`?aSvEc|M8cn(tpSBVD3c^w< z>i~wVTPd_A@^>qCZuCde&2k}RSg0o>qj-f>n!KHoT29y@Qgp&ELe~ku;_V22lAPM# z*K*CLLVs;2RA-&MH|T9>_f}lSS6pzVv3j1MeNq9uVlfHgX4?TT9_LGd*SEnVW1a$V zlv)OwTSNl(=_)%qm3avLr~G*^Jm6x)@+=r4XdkY+Zd zbgNc?ky6W`bBinpm(V5?;L!&Hm{qMyE$I#jUMQj~?z*Lj?$gMw0HchQTfh{Q?nprZ z5*yPAd2JFlQF5HI;wwG6a&_*qc7yn73_q`tU8ztp9UHr;KveOPez-5O<|F+Yjo-aB zkyv^zFycyg21G2So?8)TQ`fBv0%_vBsM<&}GBWy!EcC;8Dcw85`{qaKUYqq^h32h2 zL$pKgmPyQcTwr6{^$X7#9^O@Vy`pq#{>V@~Ij$1cGTxy12%t@RiKaXfz4g^IzK+Kq z@g@2v8q*V}*=0D|Z@R^w?n{T2>M)jgx6S8oZ^%s;JE_qzd#K z(HQTEs=6B3W6z?GJ-R)r)Ee+FzOPL4;rf$`Q;l#Ad?gxb^m^o~qt|Q5>*w80=NjjR z>%KNB5Jqf^80!>C#b4$fwQK(Yw1{Lo=!5Q_;(vCwx0{9dpPg0wmwS22?|(_D$3O>E z4`L+QW$mXAS%cL&8p74Di~~dV5cqdK?Vkn15NS8%_L!9hV;2F_o*Yim_` zBUuanA!ztn)>qty)5Mp8q&$mD&{ zYC(x+kfAQ~AX|G1pwZ2pNIyp>zg!100j*Y}(-Mj5y1)V_{6u(~jm^y>G?ePV(rh$0 z3!s$SAD|}U+B|or&ZV$)_siKzv~lY}F%78et2b}n_MX3a_xcbWWPid2qP5 zZ=|}xKgD#T)=_o7ww<;}o`DUPgLczZk4K%jvN4E0Y~=Z626W0`N_mH4XFGx|slDLoeVlvl7xo%aDbIsAQGxgwNl>8c6aIJb^ zPlrb(t~5x1xeG5BD1ZhE0D5nYjG>l@m{9C^ux^YAkKcz=QanS)YpTpfU`JbSi~34}xZ7wCujTTtnCC1poNC2iNg%n1 zv-Ypd9+^25CGhEQ&RJsKKu?}E<{@$JBActYej->b6j-|Knbs1?<0t5rg=p zBO_@W@VBEV%urVymFmy~X7KoN9>GiU?g5z}KVCyh8U`{IO&`f>Itk9h_mQleE5_B< zKa2YB**{4<97ZX!l|ch3S{8`nnpkXqAa4ImM@3FWisVNKCy5N5b1~{KMU!00A1zj z3S%?2#@c!OF&@{co--jg6?IWAS=A_hAF;(bI*fd zCSPmQim{4!N zH47E({pJ9$^WelZpf(O!8YPx;=Q7bw{;0+^V>@i*_{*t9Fv5uAJ_aHph-BwF5vxS+Xq_tpNxJiwzB}_uC#X51lv1 zqi9C%-Gm7l0+Cq@q-G3_zl5fig}Qv$CtnirZ!Us&#s1vE=IyhkZ+d8^1;kOSx$x@o z_Tmm!lB%DUlK`-ceDI9=C$BD|Tt0sM{sPrFq&En^#*d?E(<)Q1c``$*rsY-hX>V;K zr}6~RrHQeGVc*$e+`*4yQ`5y5aCf?lajpjiU6IdYx)e}Hvj4~S(spI^URp<)7Fy7$ zCwm!P^IQ+y5na==ag(?S(MvDZ#ymY4DYq~VGSUF=ztJEtN~!p|vo--C&-WKAqx^Kf zG6AgKHntdb3nF!*1fLZjtBn$u9zoHI2e$;yBd1tK=71%PzB>E&INLqLKypE;@z{$# z(47H`2ddSQ)1+OH z_)KHa1xq!vRG&=idZhR%sQCb`>!LzQfh4cr90otU+5h(C>#uk1`(mnZh<0>64yw{i z#-mG5C06?=4ws8hI%O|(3(Zn^E#QE2_SyYX#Wbl|t}K&B3*{FsHBNd`nzwAp@coh{ zppTbJ4G7G5+Dq74XyJl!GFhe!&#G0xhlmz7xx|m_P`1WAoDMe45J|2PdEjd)l4ip3 z{xnUdlWdkC)6wPfItqIcodYO2`Q$hWGa@?ba;pL+W~y;v{(NB@8tn1@l@#|NMs<|@nb`_dhX-V{{MvQkR(Nsd~}QM{}!J`;U8h7SR7P_kWEZLX!qVO#u@(K_~lZ!0K7RyGk)Ost5>On`9XL0d-$c(_)fb zMCowKcSRgun5;owk{j6!I4;Z{N}ZW%IKCuP%o$+67L-gcyX%I#MjEj;jtw3btR#rcra2@C@ z-0wGBdy6;PbZyZ6G3e3q<74^s<@WqDIna(n1kiPvn z4!YJUll5Hb2ts6=jGgcnP=$_S9e}V^4=N||X{C1T2gw+F30Le3v^%3$o+?`A%r6xE z-4pnot03W@XMtBmy_*@lIs2FymvKv$o^VuBIbw^=k z$DIqZ$K$-1;o45p>W#-mTO^xK%3)p7S-L%iw=VS1_8&StL$-0dZOq~SiC?zQ|6A?t z%?H8O?SkQde&+nYo`0YAJPS5S{?qPsR`Y)!Pr?7cbgFZUQouR( zU$FEG{{LGm|Nr;%EbRZEJ;@2n7PihHg^tWQ;|p64LL;dtuRN`@0u&i_Oed2OvbSvW zi--T^;tKQu1UAIU+{2&rc>r9+MH)^@{2s{Py$~R|?1{p_=ZuH(sdF$JhJ9e=F0~;P zG9fR>O${7fm~|6BKS$5{GaL@yg|9&)-;eKX&~~VYPl=yFk5|HbQQkA;W8ZXDZh%+gvli=pnMk<_VHG*i;nO}c(oq(ftF>33JIO2@c^T+ zeiZH>%%@gJkxfI@SmR5;7c`c({m0dL+*#*R$YAWMge9d8wdMo=Bu&D>I%h481-3x8 z=B(qCRR?lcS6=MYin8+Yx+>%@TVTU@tX33TO=i}@lCsfvW4eje9F!=s9!QUO*DR`& zm@?bqQ^C%oIExy*MLZ%3pcc2*X0c#(yme6de^_sA;d=Xrtm2yly43+_*NFpMAFg9y zP+--}I{v{Y@j;yT$Z5*G|CPySYSw~$-y6hzK&lV)Y4}WpdN2hljA8CF*9D7hn*Kr5 zg8$Zv3r(y{F*FtEAU9jP=?H1)5jcb%KM{I{#1Hd~X|skJW?;S521tMVDYIxkFrzl8 zMM_{#4L%eX)-%Wk!`}!``i+-)lp<93HSlulgV)s~tp8(;CgYmvjM##lUaNzfYnRG-jplf#Tm&!)X}M7uycH< zk)B*W3m?ORfnfHvD-Tmuek=G3bmXgz1^Uu|uQ?v_NKo?}25gqMYSgxX?knCeO*m+= zEuttQo^di<>6gmK_5UTbI&QU%Ir86TyV-K&zgwN1mHyYgJPYW59T>N_!WY<{a{cwx z68?tbt{dn!s_aAqpYr(H+LTD-u4&Su)QOJP&yL}resGodC-v!IQqRbTWIZ2c*T-IV z;AnjqToG-o1NEyjg`1gw6kJJpT%8DWT}a~Uc;=~-4DK4im;`U%>>rj@vo4)c2G5?p zd$IrYa1YC@qbf=8&6@)#M+Mi<>Ok*tlhdLb5|g7g6eg<^7jDnd$C%Hl>wPSS8=H45 z3^7n{t6o{+K`6O%bX@cCPzj->nHsQ?ZwedXbxTQybpf~FyUmC9a)xdI}( z;9R{M{CwtWsi}JR;s#S?-|Li44^!*~XUOkd+A|xZz{q=n+B=q?oJYnCm7cFYn_1$) znLSa;mo`Bve|g#=t;_gNkr{GJy@bkRfU)wr;)O7opkEy`q?39nEG=mf_oDt;vRp?? zu?U!%MG5jK`?)Ui!&e8hu_Mby{rqEh)~Gd9m>kGl6osKqA+i%)b4S;DhSf?xg-x|6 zA$uQP&MXG+-C_b0v+N>C2M)Etr*NZ3JQz+fd7Jm@fLk}s(%ixqvchlHT4Z{BF7`dF zhhUA6mqg`&8G$6?q1B|uGc(mPK$g`Q{@;rb?6Cv3w=7pt93MF)?+v4i66t1F3`N%N z(w5srbltGWhTFi6m5}Wx@+kSAeJZ`xHs;9x+B-WDiKE!| zg(0sLIEI4vlQKqIsE$Us`+MI#-T(IS^Y3Bp^ON9yd-}eVCzQ08k8dw z?hp|WSYq^26j1EpiBw`VD>W@{l#Aq@QpjldfVh(EoEqmB*QH zn=b`c3?f7Au?7}YzuXA;;9p)p7rCSvFd}+l83UsNoI2!}+Wund9Pz9)ZtVt+F{LU< z!%mv4$dWr!XDDY6x_#>Pn{UGD^aVP_M#6yV7G{Zf5Rv0;E+T(Pkrg2Vr43zq>fy5& zY`^SDCqacR(GYSt8Dp8uM%m3ks!L|!JMPIaXJ*$1NBi%7RpNJGf+ zAPV^dX-*w9@w}$82ZeCdT%Q!rw5ST{&5MGep|Y0&my9Q}tW?@WQFZ(D3Am~j{*Q#K zE8P2>ajzh9hsjKU9Sqh7`2X9(f2qSTzwd73=aY>Ia5c?(8eY^%T-&V2u%|}@3dU$u zU_N#Qxm!WvslY~GbKCpTsO=|=JU&ZZNc?!{Z%FH9IfFw*Hjm;8c+09gf*n;>CQ0T{ zbXl1L+7-xqguMz?@WWZ$KMUT+EQ{oqzLrZaF%%4vr6t@;O3ji0ODH3-VTDi-u5`hwdM~ zTlPNYwm2}!;(jVkC^&(^+`!wS_!T`UNq8^LqEy7Rk#1=kE(+r^S1TMv;Dp45HGX3< zbaT?M`5AbK&mL4{%KLFPiBO~tzIVGYLQ0M4V>lBy$7o;Ss@$6r3@3Onb$D# zd|Wf6p-`K9u$B#(*&5l|G#?}vc#ksozQyXDx^_|We}Trg@BMYI{}1&0;>iCyoz6=B ze=iT<@i%OJF-+3HP`3-Byx&+`Lpljql+i2L&~%Vp^mk~e==DfPuSTTcLjntVFrAD@ ztPC5GFfI<+Eg{7Kh(^(eaGWc4jIzMe&!+veItRE#^~p82iCJzU90w3db|cphpXOJ= zQcettDv`p~)lB_Jii|a;*jyY0)AGomg4!(Zlc7X6av_WVzHRZ3pm-e|pqQ=BqKvva zRo0pL^SU#bdGkZN~x&^>1kRZ+X8Iv-&vqE@ZX?iW_$6vvWydJDRZO zZ>F1yg?c-&d+WG{)37=(7%C_62F)uSbZ?_27fha!KnaKgi1I+U7$A=Nk_Mt&r@b&9 zP91)?9KKN^PY_7@^g(YXyaHyrD{~YFHHPY&H(GDPjYv=t_iy+Wui#AFw0lkg-Qj$$ zuH~Gj6dIZ>SviWXN@~k>kT~Z`8G!BupfycR@^D-$fw}1cwBWOFj67qv@?}A-ZBh8e z5!%~~_n-a6JkM`3@Tn_g4)6e{ zKv=(lo?`#V6x*DXF2;f*y^n1*+f8FB@wLRP3{rLsdBmQ$0hErV#O9)82@vGMPpcZf zJ-;X8T;5v^Nt8}R^+9u_j+6xJ7$!u`0gI-(#&^LrdkeOXB?wv`_G>M2XZeDx$pK3= zgl6knLRRPzl53|RnKeNp6-|;^mS)@~nMYvZ-dXXoh72Q{4k=0vkU!0xehnBVRep@j ziW#LCSaD86TKWC&35+Cb;K7uRrQ^IJ;MoxVW-kVi6|!LDqKiSb*2v)GfaXM^qT12A z9!bg#)yRWu#iSJZ88+Ie2r^=1&!#86Sw$Kf@W0AXERTwh{&F)4NwoG9bW52OkF`Yi zr(>i~94Bp9dHk5ZJ$@WK563wFIPRl%h#)j zA3w)O>wm`Q(@_W;_w9>-=E#4xwstmI{3NU|KIl3O8#>n&m!?ZBxcz0Axf1% zg$GcnQl^Uw*+@xv?&E|+rkV0CG_Z>X^KDMtcORCpg&DNgd-v4I*-T3kMN`Xv1TiZiBU%T z0WDr9sg@Zkv&6c?g2Hy!?+-kx_z8;0ik2LD1D39y9HS7=RxNY-C$2%>C3}>qhm&Xk zY}PHuO`psJkH^XO!9f9sJ}RXbDIIlzN8aEGx%;A2SG=zySHS)hG4YC1uYKHzsDLLi z_@}O@3cm1EQdRu+8dGJp_?2@&wQl$j(||}!o7XGoEuvuF)ZcD9Q(Y|*Z|T+g8<6BgC;CWgINorhmD`~TIe zH&2%sfQ}phaRXqEK%>2AC2|bxN@x%o%ML?F4uh!79EXN85{+i9uvXPJQk|*TTU?y1 z-YcV7$y%g6BMUAp7p>?}nB@R0FEs_bA{cy*Qa?`tZlz+&CUnZNY*Sv=mhO*!?_sb{ zzb}$`p?~uDv5xJbX9J5gLaORy`UMGZakOu<`6!#-9LykEt+Zz&^Py>7<~< zMJEa64$a6k4&FTfcJI)X=USVtH^5OLwoyrcQY7a{$184d6nhtFm5+$H=JpMWGG(1t zR^UTnicqbs)DrgaEF8b5pf#``UoMTB=Z_oe6eka+ti1E-=eYV(!5|8!HZ!2Zfx1UReZyy?r2oEdy*9#xSuc#( zV`!f<|Hx+s1!Pb6-|4wf8}-xRIlV)>G7-~VnPFmDI*b#tb+7AITuThrSmd;)>XCjAc`sEXOhOSPyjbzgOt%kjr`gBc+w-+ zpQJpT#ix*$G-*X?LLQ82I#%%qWE zz`#wFrReNfI^DPHfBok5o>Q}Iv5}o6)6t+OjXS%{jn9K(T`m-^i{$xw61WP@b?9)P zghsA^_h^F4)nVJIH#4u_q>zXV|BN6DK;KS~z8NtP3$(9_0Rig4QETTocpS8vMtp+V z5ftr#VeQi*r7UXM4&tr?3B@m%&VGiS6{+} z%ctnc#1VJ$Ltfwaxcq;32M$1H^Z)Jk zPN(4iV|!egm&4#PnHn!)vs8FWW;3cLAOv`B!o=psufD27PHoXxb80gFapDeLA2{d#XPBPop z);xWV!t;|s80;={4E06)nc_tT?xgAEI@h@#W+=13TV+ES;OQl6rqAjjskl%lIk20a zdbSZpQhtVk`LM4Gch#nL)hq1hZqTGad|SIg>-v^EDi!bDo<2p9(HIue=(4Ufe`M5L zj_X6ant}b8LqYEP0!Zk>a01J1kPwr}{SL-|TlMJ7jDpev;1 zc9jOBL6k$QN^q1&Q5oNWxi8~E<{H31yVTrRE-4d9tq26v0haKQhCD!YscKW9N1aNm z+8h{mZ?aKBXONRi+^Ur2vv5&39EGRYe-ff0=j;v*4lzM= z$s%$LGKQX=0b*P;h zQxCd&Q@m6?$Y#J)ZLL44Ib)``_A4tLA6VqAT@M@Rk2_}y1gjoj)7;wpJf`rlEKfWq zk0%K_)Ba29%;rzCGx(^b=JBWR>F~AnOAr-TtY5r2D2Tlm7H3niVS7B>zB~Q2#oB&& zv9Edq0t!w6u?Ie9DGt-^6Wa?(z4E&vgy;OBjFfKAb0XL0a24{bro-3XQM-brA0*>{ zq);}z0VU}85}j&9qd|t7Qglhj0zp=pQz6Iv0f&?nyXbAhXZe;<^W{71YP`cSBQbZ^ zrn{Bzqwyd~Ut=`h-9X6(9m~1O+cX(W`}uQNM3n72Hv?DmE=8NM%2}RIvfYi1Q{X(N zCukm}tWws=j7IEjsMw}YY22=@y&Xki#x5NA#y5EUn9(VZAPYtb#UhU%lfQ>Bkg2%A z3ZD^pqiOm9$Y^!;&!YZ&`v!82l*DigAH=kSHdsrF8Na|flr>a##7pE4VLU>@=Kz`_A&%IbVogYfDKSb>4$XInikjh- z9c%-nL7^&)`I=OO8ajLDWSh^s|)cSpR^~NryVE zuVF^1mcaMi90yn;rmemwxWjV{re)~w3Bw%1Ii<_ja85H2WmxX8+{BG4NIIxM|M2FV#F#3rke&VWFihxMC5<8liYtty1LJwR|JA7a8= z7lct<5zL5sTl8kav2R;O=vx|Qaa`lbvKC^_3o|P(5cc->-|X*d-7h3CN|*BrlCL!% zD`%YutG+29uvOGru_8@BDsHQ?QPmE!XQ-Vz4yf{9FVX0%ZgYNdb@shPM4<%Z-alAJu_#DwzjD%1n8q0y1<7fpb*K^2cC<1Be6A7NGf){_nKgkvVxyzd5oo}!JO8ocG z(LrscIErv~$bvWDnuUr1qZa~W>+=Ak)xKjmTH6Z(vUA6Pv^!<)8yGu>c*NYu;Y7pd zz*umG2qvZg)s2Uj*5OKYC(}Im<1}QQ1jzs^LV<)N zicimkO8+BK98bPTe2AzBCV5C^$E2r%VrvWn-@txt);yDl=t~1IZzjudn^o+z(q7P7f5Ip} z8=n~k6mU;&fC9cUw}96;BnDtJN2YAU8pK&2@FwcTV?Y`2&K1Z!0p(d__p(dW*?pK8 z_uc0D&g^ZBh|KDAJ8Q7~B{aP))K!kBG7D`Y&BZaa z*ta`i4jk`(v>`T2sjdaeQLDM+n)CMP4%VNlpBAA2vy1@oO#3IbF5I>}e*FFdWs-7| zW$+Q(l{ocLETqaVOqKJBycxiYC&mjNSAQX*4&*Yty@lTE+UQFUO*ppTuf4_Cgddh0 z@xrjXQ)i4e`dC+;`TV9%fp#Rv;<%g?5Iveh2$g<_NKi)AMk#p| zVx*p-{we()6h~%{GY7oalghm+rMJ|jd9N2TLS8lQ!LBFe45QXekid;k2C)$kdW+C8Z-8y4=9c3v)J*r;nA@8z@$c( zff8`_9wEfFX^c+rC1$pL*?cM2Fy+f?wHb>SJ2XzRJnk>Cc6Fg;maexHkCv2q7V9(;!&6 z1)oP6EWu_u zCVJ!sbgANTGFiSP zQa*D6(|f9>iZ7ybfQXcO&|HtGnmPvB?zFjoGp;=fxqQ;(vP}LHd|B?&(`;FR5}vn^ zeGzuBw9t%gsOX8x?=NAtvI&xW<=of0>|oszG1HEp-`T1r0%0o!cM4R#xI#pO24Zn@ z8n(K)!C;i@Iw*J}_gJ5?ORS&Oqx652n+wA&b_P^B`gAp*+4?`NHvDJn|FoN(PJ5;Q zb05zF`ajx#kB+rc=KqUjMmnD^O9J^nWONA)1bZ0vvGj{5591N??wX`YdAztTB^6#$ z@{y+fLyUY8gt<_VkYP13w)iEqx;CHLFCLpUMlePO;fh@UCLb+R$b;TO9dN}esYjGB!$~TXr#K*=+c#Y zViYxIMXz4UdG#kbtoHfVS}gX^t$5G!n0_uEm1qF`t>~r_L05=bs_dE)>3$NRX$#7g z&A7UiL9EiZ%=EKVrnjy9u}-)0)2bh^pCS}g+dcy-q&=t(h47xWqzyPkyhsabdPDNw zLNdL=dm(t!{Mg)SJ}G)As7!&jwzh3qy7h9i4KFzYN-+b)Hl-YB0on27V0(46qyDds zZRE9MNFDY?zsq}3bZu+*Va9s`vgEz)MM(zLiQ;>s7vihrcq5^*7=wJYT@SX! zU4;1H`wgqWrcq$09y~E3q#m{Kn!`%L3;Oo9nX(Bfn{G2=G>@#Re`6H8s^A?9B5++J zSM%eO*0m?krje($~Dv zV5_{89AJfzN1n>IjmkD#?T)`AJ3O~K>(=(p6MxplkJN*!%(^5U?8ZnS>m&kb;Q zJa9K!PZk4RQ4t^H9e})JL0&n?Q!hFmbpY;8$IbiEZ7+=`QHWQlN4hACa%T%m<_Xn= zBxLi+@immW79Jmkr5SgqRqe&&kM*F}k47UcqBul8igS=)!c(%ncuClvL!o=G_GhlE z1wh*H)Xmpz_jOx$!*wTF;+vAxImV=1!rGvsOhBQg)Saplzi>Qlj^|9@8&5@^xAdlH z)Z;r0DW~H#qtcRqyqs5p+;LV_={%VXdhvQwKzUjoQR>X0^s4Lf3AD){-X( zG-K`a1oO!b)eN<^!@QpYBkJqp2_N zMoAdL=yf&MmC_PH2@SdUOy5La%XGcCgnhO)oj0uHYm&6y%H2WH5LJzfcgbp_)5193 z(D3vp73pI2_#QX@2j1wMg#BBM0XQ%IU%R>E*#B;Cwl`Pu|ND5BjQ{ssphs33><dMdytr>>N!C$+aINT;DwLCDYRxUiw9F zEg8yBjE}j}rtVtX#+}5MWdK_KIaUKWf!1<+@brCua_Z1Z-4((PW7NiST z5aL<@+MTjThT$}&=xviUK|j_Ay@WuAxa;-s(x%rF@$9Iyj1|uct#(|fm&%2w6%dL- zL2=Vvu0+3@63^IBEm@VD!S?d+DNTT)Jf~1j8;XM{H*Kq>nL?(nq$DocKnh57OHLd5 zh$U*cvX>l01O($rze=ttx4hCxzUeO2qBom-3hGO>EXozJTJt(3_ObQJa8nEZ& zlK@r-x;u!6=E(7po1M(h9x2u0O$JKIIPPnl9A;cncv&%(Z72TYm>_f;p*$3Jf^ zNP*B;-#Y5bRWBLNf5zWJi%&Z0FOo??X#8OZshF`$QNFyzuSAFgpHt>R+|}U(btpj{ zMv%VM{AW|&36t9LoG&eg-Z1&M+{_qa97i;zMpmAI3JO3v!iyO&r5N?Dpb_+G{%L&J zvN|<<9*@iaPr^%#nsBQT0O#@lo1Ko!|F_#K{hxbz7U%!pGG-X6KbHFx)F#QKHxDbw zks$KCO|6mh4Dzft5PWDb3NJCp597T z1Mf0cG+w$gr)QO77QCOY9HIP)3*h)%WUHFMOk(^B%6)S*6cj%GSeL` zMvm7N=87H&7Yix~NF*|TgPv0_A{F3EI7}m7Fc>(KxN?f?gihC{W9SSAPvTSKt%Gl! z1@8%MsWdHHpMBK?)V|695+@t+}CaCay zvXT99)YF2VTR8>*KfK-PlE&_`DG6L;GbX7 z@-7B86mEgnQ7L16ko4D-cRR_6|8W$K&>P>SKed4+(T6Ce(Ra$nQc#_1k|ZNTSvZgT zZz08-m3{xo%SZyMzG&7vBx`5SVC0I)Zc?Ce=bD`(f5^-)r=u@hR@CUD8B$-knEzNPYCa&BU}RuGW>X)tX3!uRm&vy89|V#5Zx<R!=mrzc-tVQuUbl>N)x|MXg=_Xg-~*AF z;NEqDq=-8$q5J1S4P+S$7dm5tM)svHe6}Tc4SEq9K@-f$(qS~@w=aVhhla3Xr&^Ix zRcWC2;!36>=jQ0@`Z&0{-o3i6_-3eZZ^L+C%`8;e2Rd3E_G`uBrB{dZvEggL>3^j+ z|7zVDYDV(2n|>IiG>(ku1_lb0-hzg^;g|ipooyKWb#C%z^L;AF&SF#TUXEMMFc|e< zIza^D!<6^B+_CZ`(SQi$EXmmY&p$UT5I|}_(A1yMVb;yhz1$7n(I=WfX1V16PfA5j z?iK;E1rPuo=?Fy!jRrupL?2WVK8zOpW(HI&f^`4Wn$0p=(EDJ3aCcLQD)3g$`PnQHEwoUDVI=9 zOw-s6-!p^RG>$pkig;y5x{;OJ`n*Q0Eh}9xoy1&suPPL=p@-tZUVet=*wO&fF%!gt z9-S1r`k3dBqUf;1MjrqRLS(k9W;flUlIu0f9b%H*XP#dV(#w{ece=9Y$!K~$u9Dd& zELM5CZ5;6ZK&w`pTNKWg(MPlFOW-L_qjY{rwi}vT(mfn2XjRjsr%vIGYUQ&)gVZp2 z%aSp-4kBj8hImsoxqZ2BsczJPS`LBQ$W6;-I4sVX+;6F77)1DvjaXxV2mWG2C#z#5 zqd+mEgkw&28oc1QT?y_&+5bsqhh2j8bk<#)-I`|xWH;%vK{r&rlgp~7g+`e5t2L8H zkh(|C+^Db{RA;R*jPm{&9x4j_#Zj7EG*}aj0y8BS#NAWH$Hq*OtX=IBp81q~1suJG z@yBkZ;?C_jO6Ac4g*Ci)sRNx?rRqrgLqOnY4C-3rF#;BlDwW%&y|r;Qjc_tSkI#h} zEZQ*R!{EaN@;cfrM;JRdueM!{01T|Rl(Gq=&tjhI5|k-XaqAIbE}NI0Pcf4e-AdHh zG{fm#vT#ap0kX%Ik5NHU(3r)jBruHe$9g=Gg#waj@SX0=(p(zDYcQM(jxaOc)*vrEUYe+U>{W_%(;PSfV2w#y#{KGw1y@FL zDR|_~w-hBnt^#~SHEN6P7j9XmA+`BC#?3KdF)Yv%?ud{U*~O(0>F2nW-@=A$(mv=U zdx%}~{-<#N*LZQzJHWI2?cFiWyZ>u8N&m0a+G=*%oz0-x+S=J{uI~Tt>3R5UI%qw3 z@GJQG#RL9)@PGTWej)w}&o5+M{0DV}4@M7q4~7qt2kD}4z~6sY=?Co-h9P#|2~cte(~L z89qoK0`d7f@%d-|d??=kLVW(xDDx}v8Hmr{iO)at=R=Y27vl4m;`0mf`77}mh|k}N z&p-30AX*Q_;p3rb`Jp(WJp7gT48-T}mg@5ASv^0S=dbwVSGfQ0Jt#f@ub$Pjdj5i+ zhrfJr@Z#A6bs#7_xYq{o_pfH55+>jmEG+zcBVQmsexxD!w-zi||11q@Dh zN}Oezr@{xC#gyN3Ud)6IRRpffE{PF}F1aLjwRTy>>O7oGP;vPuV~;niP#iH-;4V9ItYeO=D?_`_)X?txPqVYT(VQ<}arc*K zWOrAla`UFpYbmEJ*%q3?yRm5_UX5#HX6;U`+Ei=O)~3gdg+M~!cWk74dV)qmz%)Tt zntx!0OR)?}GX}sH$w@ipX!e@7^D*b|Llm{QUl|M%%rcZWcXNklDQ@fJ?~PWBS_nAL z0hTa=MQGHp2)YVmq-sQSwo!O~G6;hNR`{+_3dX4l@9Xr!(a)~*=alQ0lshXgmqxG- z&PvL~bIQfVa@WpCSY&SDk-$>hcTW0btav<+vnv%%^KZXd9M~QP^@Le$ZR;e-+joP??>{!1 zmEdtu)p`FRc;eVA(z_4lW)6gDroqFllU2^iEpleLr3#w_^iWO_Z^DAXyO(c6kp~lH z;gVB8uD&BMhUu5wM{oyGd1%b2)he0k^j)%X$WJKd=5Np2)qp&OLX}_Hx(G%w7AgnON;8=w6suXwGF~ zG0Drss=OX17TI#vW@DuL75MT(&|MWz9;aql`8C@#f?9__KSXZ{?e_U-ItVoqudan4jrW z^#2~EF>Ez}02d+pm%0T7u-W=w&8?lz7V3X(?d-Ign>*X6|J818t@OX{<8cB#!PM^` zzH1*oe;a%yF<@e-;R>gfWnxNT5ihf1)Yymo85{1wRT0uM5w4y_$r$9P^ z{?3jTB{kFHQ#ddb!bGtWjiqJA!||NnET)%ch5RTh(nzE8Bv&Q(>DLTxHWAl@gJZ;r z-J*(5J~vj>FdiAPqS7$VqTVU6@zHQP>O~*pyvnZ8wL;k57^9>F+1WH7Bo|}y!0wN$ zo<{vB{y@rLGBXb|@JJUzUx``_4lZF=J>P@oRH$1XW`%8ldfX!gM+sWWfb*q_XEW8! zGF8~xnnV;V;W(Y%Sy3;2Iz!WONsTy9aHJJTUk?nNRzKoNJrIc94d}8hIX_2lq3Dp) zkQ+!PuOezz&*{RrTS@b2#n9~1EqCP?U7w8Vi(y4!WCL9O$vC*eg4e;iDjHnLZ=R5$ zH2IsHOF)Klzrp=pjg=EXIGW}U$+qoRdJ-mH9t>??)bM(&Xj>`JQO z@$YJF;hJT@9yMV6fjdA%(LCLy0s}wstd#>uE?F$npI&Ax{u#{+I^+1gv&j;Y3o*ZV z#|YUglKvG|nxZ!QaO5GMy(+%0$QNKn+-#;O+Q}sBwDlkY$!)0dRAhNeZ>m{w?t!R|$pCxf0F#M#1`okPW zAktM+76VaNP0eYkqB#w#5hGEyfQ*Q!HOs36Z1UD3L;;kAcRc#4 zH(V}k?z(0Q(rcso`Ekb7UQx!f2x_FFB2gh?gc|%&6~7K%e*NaNi>rF2EVgC%qVB?m~7<1zP(s>Eae>?51tL{FzqG;lr%$0?f z_}vR9lhI{Q)PZhOhw<|KwqW9+;Z5*XKe*{!!dUoruw6t7r?j@Q88=@g8icYcn>N;JgC#iAfTLg9(t zorg4*-Qw^Xj5I8TWSVyk=fP+@aoJ18z363mylM^f=h8?c$(q24~4M)q39+07JwJ zI=zk(I5A#P_1C858}3r1j++S)f$@&|WwwmGX+Ioe+-I26aAXbMD9NJIq1%JA8nDZ6 z(*&5bb9H?5G_UO_kqR2IFVNOP8;apzq2QpdhfE`0b+g@UKB)&SLv}!EzbU4*nx3?6 zPuh+r&C^E9)5dlO*lj0m=SeZ`NwbjFY8A@vv|5{s)n}#|0)$ccr^_B4aeMOKtF@)x zUYr3PD(3lZ(6ogK7I!Y66p5R_2)y)!!ZFOPF;h|m)v_fHA`qxUXVF3y-W1!B7{&%H z!^+{_!QpP8=r)00y>dHoT??)tkGTL!$7CG_rH~7myLddcwG_p%>nzN|JWs1B^t7Ua zV^?a60zXdFZE02t|LsD40#l^pYKnfi^*IqfNc;>`1t|iVpI0m2Jl%g0?CtNr*{=uZ zD74Ja!ZDzfFj>ofU0ERbH1B6W+3U*!|G&TY^u>33jWo+|t&KVNe{cZUa^gR)`2YKQ zx-*`&=Zq6mw9b5KbG?nVnI+c_&!P;2=rMMY(P1_{IgfL!>*NykB~ny}(9CFH7g@t1 zuiqTP4%r_~2T)4H-wUUCavowebl%9OF^PJ{Xy64*I+B9ZDR5x0*GGdjIF{fB3@4Bu zQ0ff1!r&x64JwDbW>efFgc~g317RIb@a;M2(;9BA@XT z)_?SIG6Irzn-YLI@gFv~o4EeBfu(7}zuPGPX>YIMzu(J)eCapI1+4*|d42ST?LHHJ zKhE-elI?D6q~RnU053fV8{;V7;EWrz`h+GAEEBAg4w`T_oaWOMcTN>>9oDBfH%obd z_8ucsl1$y!pP{c7`74KG5S}*Il+8O`A!|P_Dv5KP<>#Y@xS$a^zB&Bv6<^G7mg)cB z-kXQFbzFI(7b`&!+`v_oEJ2hd5u!vA+^Nk0_kG_)M`1u*kOYZ^x&SwrN|eOip*)(l z?|py>j}Sz9&u^ylzRA4#gO-$O$M1df zy*gEQ1ujTRR^%mupyEZ{x^?T;J$34wQ@>M(af!CAVV^;KssxJ^!sUa#Sb9K$LBF-P zt#N#y!P;mYAL{P}#O==ARERG665qrkbd!L?L&B=EYvH(MG!Z8IvFpKUIad|G*L%us z^DLiodJtK9?2c0)!C^-2<5$hGnSz8#_xYK5JG=;Z&C^~Njnr75UjX7EHrx*_HsOa6 z_Hq%~2gaTBX~cmE3eDy4O!=pU-}A8OE-nCWw&Q?e7qXnbaRL)Sc@thQ(ZK=^#!F%u z0YDEIHmwkJr6_j!rFq26o&M!S=O>o^4p!z%P=XgwPn%!`;!+ERwmoTM6R)?spLs2L29wme`DeH%=fI zu{t!%ZpL8`f^j@x#_@Kq3BlG&)|yzfO`&5kXk0O3F^^@vD~^k=zKwf16mQX=7DDw&8nXn=H80%1nt2 zMcjk;l6s`ZP~gZV!uyhT3E)<-b6XL+2xrEm$hHd>!_hCYtBNSUe{e`rBj1U9b&rdpPSDn`1)5%Wmd(Tmr_5xe~wXNo>lNs#B;O z+f)7QCIWozvT;+u1UPrj+pd{JzN#T&A)vO@_H8&DT} zieTdQcrT$Y7^tw6iFhF?)=&OQaYm?7F;d0l7YG@n)Nu~CSTgFx5FS#&F(BqfCqt}{ z{3YzLHebf)fw|bGP=%syx?CuQH*v7C-kyO$(J6Dh!J7Tc_6XR%ILShIpUz1>6BZA! zhnv}S_?)S{tW3^Q!wXvRA^uPYpX2L7KjOpmLG9<_Zx>(C?VA$Hp*h><^~4vNrWv15 zY8uV#Xl{!yHaRC0nw(1%0FW!f4NmdhX*A+l=RYyHgGeaxj7kCknMBHNk)(1SmJuU8 zp7J1J`y35+dd55NJH@hF5_6uHf&d@|1;;0y4i{ZC$2h$VIVN*5JGyyZAs|EWy#@qu=zKDk_7D2@If}^8FjmcgViA$_Ol3=D73PzsA}kf6UzRfS#W*^Mm*}J{=ERBErbx%K zRDeDg-*W>oduA0%2=$yLh7WPtjZ95_?5X&0?0e}8^z@>>Ks_Vs3*jy>JN8Q)I18n< zlidvXGCsD@lkQeQiIeGw`els6L?>fo-tx-ExfkO&GK_e}wn%)^#N)e6JgQ@_!s;7$ z6?AC~(-01Mo^jF?v%YaL_I;5_t=sG8&8QAKW_p`MgOvzTw8@SdN2H3Egcot{GPbk4 zB;vE@E|(N?f(X&7SObe=oL@f&ouLS9zT%JDszR7|9HzT=sgT&`5-S zr~q}wDWvV#*`{Fy&cm96IeiG9!SWAWanj`QAj(98;T-SIZUbcLI62E1bV|ByZU;T+ z03!?15Svk!&@te5aISE%FupR-68;F+Q!Gv>?z-}es;Mt~_<3v+_ z1zN2aMmH zHR;$7k@n6(wH*DDo0(spnrJAA#kX zCL~Lsi6nkJn`p1yhuy|au$YQi9B>-gCX#W?gA$KKjS)0MdKbB^56OZ)-n9=iA0Ogh zPNaGRcPTLqcwsY3yi`NnZza3{4S1K!yNK7C*y`Li&paSzP2hip9WS72;6S*3T#A^R zOYGjcWymYy`y85l5|X1j+EN4Sztv@y>MBc3m8r~9Q(p5d{`XV)B*1^_kC+CW?s*rQ zB7G+RqE~E1B>Tb>h`H~y9>9{loSWupQ=@ar>9@H|Z9S%<#Z4;sC6>1z4fA(WSSf{I~ih(*9F_<3m==w$F_|&&CR>#N=$8RoebHP0r*LY zq+yoRrV$%RxY*6bgwc7XqLI!nGb_Jx{D9k_XHis|U3kU~7j_0PwfUvZa(e7wLO>lx z;Ic?;0>2!@ao|~zV$Yt%^9sscgdH@%;0z{E^w{tyM_mD-FlcpE(RB_RC0yf2gOYGjqxFp66QS7{kn?TwEc3<{~-D}n_ zy4>SVX4kvP^w&jl@s9V!HIrMQ&Jeuv=I8MK^X!yF=2s9uL{22w(~ff`AxKJ#XxFDB zrXgI1WPE@sh39WH-a$-(VZv@*QN)f^7+#pZ1-S;-FwVU|(%KSXM|2Co5yhK-LD!MW zeSAks%a@B9Dz3fwoJmYxvSm)4Ghx)K=q~8yV3Uf?wOlj{$9qv!^rGawBI8<{Mw_a%-$ zm3X|1NS;J|xwA`5O-SR9J@H^}uHlW<@jmW+i>Wd!*x)Lh+lNfl-M)}GArs80qjTl8cW?#T#e!VOB$OpbR+?zhtgcu=s_$2SkW zdgnl2V3l*}7@4xc!e)^`6(>dgcv9Tjlai*GcZG|Jz4S26f(>J0eu{;GJazeRNB=+H zC6_1?n4JiRrz$KKq!`%ueve(^e^pde5&6HuQeIVE3H!fPmQ__glmDN_=SQijnCLDI z`y8Qt6!l-kCpGNf2XCU}CPh?Xr)UrroexdZq=At0eLEy#;qOG;$yWd$*mjH z;bV;}*{vVb<72HW#cdcfC@4?L5z3MJJjJLK)CJ0;UZj>(7pO(3Gs%_aP9ICh<#nzM zcjj0o#071k=;W6vTJK2zp^Bn@tm2wDW~7w^lsP4k9UI1Mps%y!S5TzetcdCd0Gwm* z^?F_CSH_UXi3BdhJYgq8@X=O8)ksJmM}L!%Dnw%s9`qwOLC`EkRMqL7k_-VQ*H_cH zuN)NWA!ga6rbc(TLT93}^=@gWv z&^dG}$}`Yu=vOM8j?y$b1EuM7CQ37CBYN)KX!^hfBOG{6?9d0kP zeA=vwCXG8hXufeolgE)3YlG5J?Kr6=WnAz(imH%Q#u(7EFli`>>c{DcaY7YlRa6Zi zil_!V@JF?z;XZgzs8JuEDIEfIb}3?srHo_F>nr72R~qjwESVzamZLfjvl7UUp-djl zl85f&o1q~3@3x-GKd`F4W{B*24*f}s!*XI4WazTaDF4oO^xUbucp6}zNUUtzux$ES4dYL)|Cgj?*n+L z5|V~vc;E*>0XNC(Rt7FT^;l+sY}T|QiClJzx}{)ppr4?fCI(_CqgPKtum$qsdlsK~ z9xl*x9h-41n0}Ngh<$K1_uv8i;Ajl+r`BboKGTXHqWmB8=M*rulyW6eT`JKrKWb&i zmNH;@WWJ zJGw)+b%)l~A>Gli?r2bVbZ4T%eful8nO)4Fv|I=O0qI~g2S8U}R{BNVJ0mbm<%(iO zxuW_Zs^iCeJv(*sx=J=b<~89uC37X}bd@a^6?hg{PA;$?*N(|i$6XrF9l0|g_4*nyQEq2YSp{p> z8^xnUe{rjFJ}>cc(ryDX9kFdX4P$ zpRiuD+6Ds07bBD(Tb>v}PICrf$CA@0-3^{yHlZhC$=oMH=G|tkh|X0mhX;u}g*Qgza#9#T|t^UiGe)3XC z*AUh<1a%FOl+0~Pm0TM!WW3t>N@vUfsSX)VhYhEL-1h@is_m7w>-5dp*JjrXZ+Kt# zt}kwCLx!5Lp(eYYb!f3TWQASfp-_8-(K-{&+o=lKCX=YlY<8dYatTKSck3EUIffoo}fg+d}?8 zMrj3=5-&s&jF}9F!8#-v|M-yLG$4RB)*54rbCr@S_*={~&e;9Xo1>ZbX@`Aw+y`b+ z|1?0IK?il%r@c`FtFjVRGs2cX8!yF^3!N|#|29Ncq3U)A8MAgu|ZgWOd zk91I*3*EuJ98F~{z(hx8pd%URov5x0Da)XM6a7Imh!tjSgV#0h_C$543b-BOBLi4@ zizc-pCNbq;7#JT65N=dU>W(I{EoO`;*Q_EOiPu0lEgJJ5tepf3;?y->6(55u{woEn z=hp@*zc8Fv8Zl%?GIJvZr#9w;`>O8cnzoZQ`T7qjlstq4v}IJb;Q8>rlN*EYd^xo5 zba>xsP@YlQaPF~)Aro<>Yu)z@xnaX|Ym+~ie%rfg3mvNsAFB-+>cWOP_RQ1|mcqwt zLWbI~p;k8Qz}oQo$Yxd0P)Dx&Ff}O;wE-odqa#+CoO&(gj;`Rgu3)Y5&9fn0QCL?L z)DhN(;kA#MBhC${osy zY)rG7y_H*0fjEPN8pJqear~<%4>y>oq$K8vX(ec+OQ!<|w zwe*^G*%b%rT`p_DfVrY+jL86kKz+Z*0=pUs>?L@VN{kHfKw0Sjg98-2)G7417n3h5 zRusZR-%wsd+=_t$w-n4d^vA4>Doe_uD#p9$gSW;GqL1}w!OZ{`Y|aOY;>Hq^Y)j6% zymqA3oi7neUPf2?fY`23!Ss{lyJo+xdOex{iD}WC!j5jA!v%vHUO-e~XU?F%3<%3n zofkHF9G_T@s%$K@*r0kgGvD0J9v` z68N)EOR-W4%O*}yAQ#C!8Uv5?MjFS%I3mbdWb zG7le)YKaG$Sx07p5lBWMslYTvLT)f)k{WzE{_S&Os-6vD45q61*@8d!b-3C=h#@m; zReSHi(e=sDfm7iFr?#o2x1lqwc#5*>mw0#FcQjsetmLd z{w+@=d;i+dc9J^Nh{ym40wvo*l163ZuBOJbY7O_^zDf~Ry)O4E;2j`&^c|&?FeC4InlU_F`7xk;DLFXdW~x@t9%2Si&gvz`nHNnHLfRN#_kG+bcqjQJ|X%T`X0&%qFpiW!6) z9hwEO;RV0rTS>A5EQuYY%mv88ZyTmf@z`R@DOui%hSPVTJTO@Fs#DTmJ@d+$>x=8! z4Q(i;G@MepO{tU1Be?}Py{~!Kr#2Qg7ecw`!@1{ITO!%{uXo(ZK6X3%SSY(FoL#iq z6v^2a&N+G~=hW?-QyY%WkxiiC3 z(7#sfP9u19N%TAX=7nip);1@7^YYX|tKNwjnTSF(^41T3il|+py9<;dXkB96FIMB@ zMKrk@criv&gI2BvDUHTGg-F+17qwjU4Dxi&>vI16qOu9-+?*6hA{LR7sB&o>aV=OV z#eARXq=3?w#<6&iv_$;D%xS_fU@9q&vNem-4#t6{5vC6kJPSd$kcosJC=7rS@EBpH zC^`DKphRZHh|F^T94`6S6dnS`+?%DZm98J(sNSr8@A>PcA!AF}*s_`w5#)|g#`EEf z=kH_`-_9uBJRZy_4rQDUXPic|K}OEal-E)ua>);zo4U}ky6`a&JOqK{2g{rLLdVa9 zkDs}nappsf3Z@?w$Riq%M>JR-F;K=_k*u+q7s{*)XV!s)k#p_qKRB{J`Bw4U#kbQ> z1$C!(7)rfgOpQV`JQA#N$4dD+xl*wcfj>)BL(C9eQSk(>3?y^wN9@e3C&b$eNescp z(CQGy(dsZt;>6&iD2cbz@yjjHSl`2uGQ)98SPsNX7%d{dw3cH)p*3X>Zi;kfUxGKJeh`cuy(aH;P@+;iN&h`KkZ?%h_WXezcTJ}J;> zj%`zXlBR+ae4^KDUf8CjNxoil8ji$CeyXN+o02Ba=W5EgDLyGn(VT)?vPnUTCKryx zNkNiEk0e?yF(zqp;D}9(DVh=}$0h}O>^c*o5>3^np2xfw`Xf5=x>Og3YLe7=ASP z57Pu^(?w^&AcgHn(M(aU5B}J&aVVN5&cEa$8st7{W{D6@95eA~N*jjBV;7;QF&?GQ zB;jWACs>prv61WJ-}T43vxK_NGoVz#!1eu4lx(XM3Ppq}2vP;xYMmlEqRt6&--!Bn zQ2LFiPXwjkh`Kx|{YKPBg3@n9T@j4?eiTVEZc_?HvXDg7x+{&)pyZD}`e<8ADXN21 zmG~Q>OhM^)+h9{F(3_M`gSjU^#0L)(6!5{^{c_=P>PMPV)t@U$WksIf>Ca=~Kg*ve z{iu7iDZ&v2YO;y5uP_cMay zJVV8xitg%CUQNC#AUVSi1cUU&qn1@m2xtXy!^zItSrJ(PGP}FOBwY>bd?NdP>j8t zG0L3R@WWWSr}{+r%bD!>^Lwl%X^*w=SWFIy$qjD~e;I?m%#W{YW`BI%&hJE#;QY*U zGS~1m3Rui#{TB;~3GCyGj>P;q1$^isATjUcm4;%$n0Iqc50hC4=0kvB-r09D1SrhE z`0}kUZxIkCDsw0ISct=6zS@1Y`>rAPj^ViwfHB{m{*m`B@1||*SnZafPB=03_R^i> zHKF4*TZY<1xhxzrY^ZyXD#l>GnsOCTm?1!6e*f$nXTunQ2}gGTfLXc)0L=GygkGLd zKAOHL5&-0{ksCL1Tv0Jw^hkBc8FrMjU;gBENETrWgymO`8vu}VMh46M$?hv>zT$X^ z=+4wkMtr%MtoXbYHFwoaPU4yY(a}h~s+%(mAR}PK&b*B zEn*xGhJ6A{FdLo>ev*LelJ*XwBskGHxWg_`%D<=B<>Q+vH(&udlu>vWFCfG3o2fTa zN#6QY2rnE9xko~K?$V@&lC^S9v3C&Hw$E-gxQzm*2R2=TLbF!z)9F zs<#f+Zn411JDG<=c+EKkYtCDE!8w#!w}nxaM~nXx!LHs+{=?+k7*+Wj^an4W|Ly6l z#tgMQx^j++M^_30dn0x6C`AdlO1W{q8>&*6E3X|_Q@;&FC5968Q?ZE3K}ijt5K%cm z@rcU1fT-L$aO!>xlJe^ANXlOWl5$%GFwBhI@RQvX%Wq}iC>(l{<3M8Eq%gi|)odd{ zOAb&mXh}ROcSTF`9V?(Ep;I3REm=eHXvrHqT5=1Zk`D<~@}FR!B!)?DtJKN*Z7pTY z-3=@G-*@RFLCIrmE3&hcv=ru}GB`FlMODNpW#=tfwN!)N4lA)rabvDvl0mK7D4jsu4g9PYEKvl}3BVnKrs1!z= z*{H`uNB$+nqa$yK(2-j#G*XZwwlFd>#3CcNcv$3(6gdgwZRe(L>ljE3aRSBL%bWYQ zj-Lr-oOuB7$OkF`6e*PxHuJVvP~@v$zxwsJkE~DrNpWa5P~;!Pfg(lk{F$szu{=a> zl*uhN3leu`{8?ch5x|uz{1{(RvExDlsl?Ma3#sg&I7p>Hw!6v$;WN?xXZuI{}L zILrHhuDlPJ$_Id?d;rkN2Y{8l58%lA0Ev76K*;-me7p~M$NPYCd{7XJV{}u$3pmC_ zip4RuQ!I}0=PZcv7wVjQ>f`s+C+?}s@2QX6Q&;?2M=7fBQKp9m78m$yjQ{&+JBQL` z-3RdD&(%5%J^a`Qqxj{*^VGl5ShcF3Y7{7qAGyCPpPli4UYCt|{Cyv}&o1%5t1Bz2 zV)lP5uPA?p|9dJQZvV&e5B}ECnjC}wy*p(r1yFttg)qN5Ql+j2V`+}Gb(xu#f~$BtSgb!M#?WPA44u)D zMeB*X0%Jh;%9J`fpt8yVirqD`yO#@cGqCI-+qR?Q2A31STuo$bl<4(cUK-+O(HvyK zD#s%aHF%e0bnqM>hsQQI=UN`;c8j&+{mW5-bOI0>w9k>d3Rz7{ zb_WiciL#9BroUc@IMU3ZNsN&U#3c4@Jz(N?3h|3{2tl8S?LWUrdP=a}V%#=xV*f7y z^!Y_H6F~Of(f$z4AwTBj$IA&fWsH48duUBkTx-PsVY(;re01_ZzodRi`5A(Up%00Bgu%i4P_0N=X?H6fdEAekn^z5;F5-F%_=TX1Q$g_{t z@2PRUl!iu9dLXlZ-ea;l>iT03OJ2i?**WaoIf1|Dj-a(Hk_Y zqiT>QqS{GJ;?X2Fv}805FdNI`UeCA#;vfdHMOCvS&_VwSDD783;=ckR`ByL?0;yaa zCzsbdX9^hM!X^(5h>%{!JB9AkZB8WuP!c;hqngI1`k@viBtiyih8p&bCUQ6yXZUp| zEKg}L9x1AFdi>E8JY|kU*atukxx7BWUusFeMD>^}jKf25up$nHS@9?+Ogt^mEl1U` zwKQ`GRRN-t?>Pks>%UYV{=xCLPleQFVRczhU3O2M^6k!Vb_O$#h1AEx*v;c!>Vb%^ zAd;H#%k;yMjLb;h{_WJHH0_5JN*+RTrFlDp(xiO5`t9aLxY^&AhWi^A%n zpt|UuI(hZ*H(FnAz0xXkmq@1ZO8dRc-0%3-%GdR8yb#JO4(AnbOoZ~vHZ7sN%1~xi zII{{oiwbbEs=232f7S4cA((S=eF!D!8`7N$>p-Esr^~wD9MbI%>-GnA`|s&eS7$=H zePP|cApSJ#tXkxvwJ!g+d`0w)+Q;Kv!(a1{Rg{xgreGl=t6%FDA>bM<(RJ1yP z_`zk>1;pttYrdwrHc0+1DmZMsUu3UZQPas%zgFM#duW3mw;|=9#qFIS-2d8j)F&!m zx@0CWNXvgyaqUlO9pVwGf!sluJB1k&<7a#&<}|a?sk@B27#fX^lSRjR(Q$_8I8$_N z6dh-Yj=yx7U39*sbar#cD8E;9%!rPEbDcfLetbCj75)hwM=HfrJ5rYeIfG3DgQran zZjv^+Y=PyXfYNO0jwbU_2?P10n8zzbDkLR8R+s~aTdn<#rl$V>p8k_2H$am7(>9OE zQpG~le22~2s2&#S$8k&?V&MYKlEW}2P1}4nzn_U35Ss(OLSjy788E^{)qpm_SlMU> z+;rRlXGxUBsFGo#D%5vY>`?v&@TwgY3D%D81~^{QjrL>)L7 z)a6Ig4z6e3PAd$i6>cjvsrw>1`|so&yPb1v-M=yP?%`n0u~5$0aL(E5%InIH?qwCC z@t(R*NbY6i-AsKgHF&TmSltZi!JeNTA%8;|z2S`BU`B5w!?bPf31A&rBGIJIICD%b7!!w4O09KWpsrzx`G*9(n?{)KK;-(rATg6 z+%p~w9;y!+8^XqhprHY&jwo~cjL-o-cnvr5PbmLXZ#|;<$x-V;%})<%&>`3#$d>=+ zY{AZeS3TA6hRCGBy8w`>C!JT2$y7mUlp_5~ z8vKh;xk2go1FAeI{X+4RL8@5%jZmdQ={G`E1f}0?EtPU$^>|pnf2}p7KN{8_y^<86 zlI~C$x2cTld9Obgq7H?rLqY1$wl-IBe4FBva+3nynJ8^mP-??!;I?XCP_+*`C4Qs% z<>o8+-?oZU7ocq155EJSN7DZv(eID@?4tiyRiPi?|5sjBh4lVPqW?dO|L{~kod4f{ z*8jI1^#A`zJ#qbCX649{%1I4i1DH7^Q$jIgiq92L2ys?`BE-yeU<2T^90j$kTvmP& zHGe8b^B3e!xm9nW&jg)M*7Z*086-$~Qf;5;`kbDBIe}ZA@CJ6dW0L5O!WyRTaat_Z z;067i)82VBg-CA~;bY1$Ab#h&vHH5Gb-bp&LdiYJil}|p%GJzwJoPT z)5AXtdic*M5Sjl$efaI;KRWf+DWrzq1&#C@pdBOC_%5e?{{yFeKTH+t+~4R%3O9cR zQl^XUf}Z_ytE3jJUEj^jeVx^=x5TRTR%X?GqEg=lmHMjT`sMYZ^%kU1BW?OFsM1J} zz6(n9`?`HdenuL&4(^R=f4Q5#&C&OFRGCWzm3fQ>l8KdOfG1g#cLF&fg;@l0;)xs7 zRZdm)9TF5(S(pZ|q>5nMf_mBn>M5hR)+b_J2Cn2c6>4f(CsxprdVWHzp3B}*qMFOy zl~>ANj#bK^slcC075Lv#yb64O2jy3!`G!QA?-r=N5B02WiWJkkpp)K64Vr2}C#`M& z*-FYkkLdA~eHjr_S<_j^Qh{M@OH zJL`WA#%Xhnqd9lc1{m(gTngS<|Fe`MtuHqIAIf^B|2>TlxBmM-um4-eFH>UlKTh&| z@H2&s{-<;)$FO&t3u%G)uX;@Vs5`+R-ApC;RXEaO)`HXNB-r%??+b%;yI4Cm()IML zu9w2D`2Iositku@r>AcD{h%OWYCnm)d6eN zN1jMce0)V%^)>|lO3;1r2=5;pDu>AK&D=oO>*sy8DTnE_o(%|Ziem+H%cfJOkX&xU zGCDfOFZ)F~1|b=)~;Dzwy- z`;(;NPWymtaCStbyQ<6L<-e9E z*&A3#Yu`zV*;!Zb7kSgI=vUM$s!8QLy10OzLfn?*74`R6h3|2ng-%(~?`1Pou;@JA z9mE?S$cJE%-sxLNA$>_TOx;xdKJ24r!0V)GV8apUGOA+y^I*PkI}kIbS=)?`W37P{ zG%ifQ&5;TgFqlr6c&5wiYRzUPB5lk~(O<-GTn==O+v6mV0yhFK1T|<$c_ksRvA#9(Nz3Az;}>1&zMsP zz7{K6I7&c@XbOPc1K6p8T+A77lH91pXmUbN1_0Bj!pS?j_<-n4!iS7o6*KP%97s51 z(OH20`YO>|{IBRAVsmw^p}O{PU3*AZjTF7o7#JsM>eW9{qIc&GkZdrJ>krr zRc*w0c3mAbo<-lyBJvjkM`0Ah^^uV7xv=iJpzgUyh7r8{@?dH43{F<9_wvje$KF*0 z^G=8IPKWbOuQpvje6@A8^`7x4QXH#-Mr+7u4I8bfzTBe`Lslf`;Cf0h`{cGpm8buZ zQb8mZ73&48qq1IDPYxQ-qwnT^@>jMU#xomve{t|n4~EQjVRK#3cm{oC3#J*b&E82n zemm`WD9s#BGY54Ezwt~>MX^i`LJSh7CWh=YXLjF_Ic|_Bgh8?g=t3NP3iv+e{i;Cv zfZsbO=K296%qeVudZ*6>v#?oBR9_u8EsMw-v{3Q9phk2!7kDlqfx&4&ll#Eig;3ZB zDe2b`At;keBgWjWkqM$+Md(V&sVa9^{PK695VM9rf`)+Gc9k?R= z8oJF%j|P7iHuM1G`z>~NE$l*{!6h&>eJzx9^I(AgY^#-;{pjXgqBCkvY*Tzvq}ISD z`f&-l2h=RTkn!>k!Orl;_mX#eIbkSDS~Z2HzCT8a2dxzY+t5@bfh8smW=ZSlWLht| zHf67YFlhswN~igCk9BQ2ok3?l=CwvTi_U(`Hs{c};=9s$@8ru|D;p#tW;bz#7z~KyQ;TY2An*xu8YKa}pt}VOSioR^Ijv-3 z*qvTsSy4+=?0D-nHgU^2F=1o(zW5U*3^pzhvv)p!o=|%UzVW!zOKjh=25u}>05>Aj zEhd?h6e~1GwF7M}ZQX;y1Wi0y`8ir->mH_9TQ|&`1fr!BL$orkq{F`mmBwMU_=D6d z_}>RqNl^NY#G$t$RBcfDjZl?A=~oV^wXIbta<(ZxF&t7LSs*_75R!*NUZbKwF`(F{ zMCoZUb}RL0F!g9C^>{e-_?2e#z?vfwUCyfZTI%)5*WJORwIN+ySO**6MRZxKN!L=Y zH{Wc3t^G#l>zyIp;jr#-PWJrp4tDO#^*om$^QS>zg_!( zL43O$X*?DzZViC54@s>5fb^OaBfWU5zl^;H<(`nq%@B4j##Z-}GG(%7PQ~ZO?%A~C z^`(2c9#CpWK5;EP@Zg!vf3G%w5H-a%|J}CB7OW9p1Uo<20|2_jB7n-mYRlc-Ec}Jk z?iT)K&6Bt98>x8vKHf&>=~(xZDc-vOPj|BH!`f-AWuF_Vi7fh@J)eh1?O@55-Dp>Y zq2%rNsK3M(d}6=n!zyr=`)sv%UmI+>=XZP&+3W?2y(lol9&GiywH7S&JME+qYnk7z zz7^RWcjWfY|IZXyPCr7NsjhV^ z#x8%Cv&$2UeDpXkzH~=&G_oY z{w`QZ5$t(cv8+JDWIrAzpQe?)mZKFmrfbBQd$afk*w{nHV_U|eJJ_Or7cA;m!J_^? zSkWm%Uc^vD_6GUp39DZ5j>4)_|Jea_^xLzk!w7@jVT$g9A^kp>&mR=+!9c#ShkdK@ zo@i`|%Lz97mp70jey`zPYJ%%x%;N8)>=sra))>BrD`HIGd$=OT@V$pCB<5}~c1y01 z+2fDj&q-w7mfaOfvDY12<7IYqt;}w02@Kpa$fOln-x`!Fqh+Do1G2d?EqG=Ue`XSY zvL^8YY!d&6)SNr1&xf#8Tx1UqVS9L#vxMJeE#VKv_V18j|Nc8-|9&X8dWSfx_kRSd zcbKzxhd6t87~8wUoTWR&S-Qj6(jDe(+#$}!{nwn0`=Qvf9ZET{1!nDstXVq*X6>7! zH%4!M<;GXGa*l?vJsW1CXzn4_ko{`i)w=8SP4^A=`rw_TRUxov|EsV4<=5Wx|NTop zeP+{+wQ2ubio51?TGPsK2QzY}o%#pI}YJmSDjutPxp-wZtmSc?P#o zVC|)K4lN)5)3B^MLRpY9u&9Lip9=7sG_!Wi;LjZL= zxT21w;?SRIIP_;aoepMjIs>~n>&J|!tWi`pi_S!6vS}kqbLcFT=F-_H&0Eef=S4I7 zp<5(DL(k)&p-xe}P%LV2+q8?j(`3UzLQz*>6DDB>Vzm@>6Ofl5+{aykBuE)p7tjuG zOHw8%qoRc!hE)m5Abmw-b>d4TD4itiU@IHLP`RQKtXQIYdDVAkT>z=R$!2}v)+2-t zo%Z_J@MLg=1G^B1Ky(;e7Q{zV7F&LHE{iwEhai>Y2qZ?_m|4fN5ON9$#ptflWS197 z+xQS0t(^Z)zOcc*C`2i1+c7H{=mb2mcbl~&;3yVRG?+x98cbm%s7o5V!@k=#H-{(& z{E2Z0Qa1LeP!k^z?c;NgVAqbFmn4Bm3*so9x7%V9HyDerP)LN#$bB_%HL!MUy?86V zWK|8jD3jmMZ>1kczjF`VoVhWxp1*N=E4La8Zd}egRey2j)|q!3-}8ryJGXMX@JXHF zj&6TQxBq?JbMa2IywTc3aYk!7;|y3gF+ly(h*XOEO&X}wWLDr8Dh*c(4k$vn(tUFM zjZb0I9a&M>)>Fn~>-3Li-MF4EAm2%34d&79SDzr@sFDphI_aAC;cz7T zW1g3dFq#+twD<&wv)Fz|VvORDJO@I$1Mlk&k_VLy;>gBnee58PN=iPa!ERw3p+&eR z_AbvNiavHkQLHAhuE=Syc{bj30H#XcBZU>kL?(n4mEmICInbqi_0(Phi&m`q@1z%o z(hJuse{|-pGaJmGybwyS*lc-sXe+%TsB8F~kmm1flu_WIfh;~0XdK~TiwhzXDA{qf|*pRC=PzK(AgH%2xcVcprsp)Y>-i0?iIeeE1~RGBzP^^WmE zV=0cr3=RDIFGj#nmWF)sGz3w$1tP*xKhq%aOAIBM3HK6$*m7Y%W2wux6Ls+?!Kc#K zK(R8IJazrV{_e4VF&oy^M|3Aw)kLgXYy5u48yy?U@AZUqClLWozOOq+$V*HvDvrE} z+hHOp3)a_>tfj)TR=_%e#1$KosL>F>eU8+7D&-Z0KszvgjD2Arnp;E@ZcsD1fG+TwS?0U^uf1}bghBP%7bOE>(8y()>Uis z!Stg+-O+fNix0Q-M=1F=>ata8Qb8+T*5SWO_OFWltA1HWYu;2}6(KHgzLq_o#QxQ> zf0Oz03Q2iAdp?EzYheGT^5yxLiZ?YnR1fL|9(w$Uf|=C>3I^a3l`)S5D=iNI;`JTH zP3o#rUK?EjCcp=?zNPGrD(G7Zq)Nanj%eVPDyr~8EITawk{yDs;J5Oz?ySq^o}g{# z1LZQWM56qZoW+Wk?|cb&4TO=v-l-p5q1Nc{yWencG;C&WTDJ~XU!me@`thHKIZv^R z1UPDlPCVruoTs>sKZ1L`)a2JK+}y zu{@aeC)9epaD@9B7zd*AC-@BZC*Ic#$RQ;7&68^;{p6e3DDKoJOTdmN-`X~)t@p`x z>g1DeCQqq8LaDb@BuJQIA)X^YcDtQVzTKE+afHg@C)UiFC*Mq-W_>KpKDpM;KKa&S znjL$xo$8V_v)in~k)+6~BwlRgjX+-|H^M=rO}pI)hmwZSU1)xPprwp8zsRg6-w%^D zhxDuJux|f@v`5H)U$-B(@K<1?`dC5z3bZJBwf$q1d|QzyUgA|=>3KyViYlsgE7;1aEPP3{YqNJZ!{8!W~8XDKYm2DKTHQGmrxyeU1yqvU>grm>?t0yka zlaeG5F)7UIWNaQrV1l}4vibL9BY@Q;q6^q)K~IB@kcj5cvk>j@0T_I~qu{`P7gGR# zbXAG87>Z~yf26pnP*B%W)zq>A;CHu_I7(voEec7n%91i>1pUI%6Qx3I^4Tee2LgJX z4^$>lEKpO~=&+*E!(DX+8V5tFM=MACc&Y@ zLr0TXdrDM?-priW<8zRpz+Yv17wb~#G0zk@yz67nL$6(;@K{FrYI@e4w1c6vgKNI6 zv?HskyV(V6)$5f%I{()Bt?cqmODMZ)HTkYFe=T?Y=o<$&vM~nKSo>~f2q2*U#n|#r z&c^b)$KSL5FU4Et<}eN+iS)EHH_qJ2Iugn{vfi?lb#he~NzYqNkGGxhx*BgEPq4G5 zIOr{6r0N9)qr^dvK;c#YUl~$EuZ@^Frko27))ER9Ui&=5I6QvWGFTI^y%(>m3L~g( z(q!{2bIS>!wwj3EYwGrT948^y!&{57?eeoc{JIC5ktPly!f$B=x+9VHFjNI-KolGq zPY1BUpzL}Rt3bXC*HlwjAvaRl`FFC8hq8`us5XW-hqkiLtm?M4R7Un%Z7BT+1V_%~ zgCpmW;K+$7V3i`;`qe$r0H@Qhs6}j6O)KR?ehYzNMfNt0kh`KmwX+jI0kO~i3D{@R z1Ams^a2$*p^Hxx>M7%W{cALlH_KCIZJNr(A_MOZ3 z=O7NI3KMfwJ>zwf9X7f6pfYqF)uP@;?Z*3{epN)^U5N;sZ7bkHe4w2X?kGdyzsNeb z*&5C|hnQMs*3G;dd29JwnT4y`yP1dAk8Nd|arD~jlWPkbnm4`{OfL!QN_JS3C|3g6 ze_Qcwy%_&27=xuU%GEtdClaYf4Me@C>$DLB zMNg~bWSpydUS`u@)gyMUETC4BW>Vuz()`fHPod|Yht?F}9CTl}vl7g8LL_pSAQC(= z+g=gV&|Q^{b1RlBv9y;Yz5`+yT3VMt+Dn$(pM52HMHd&RIvL$t2JbAld-_VU$T2}< zP7Zu;;L~DIVne-Pq?cIe<0QW)$eoxal*ueZ@)EUYR^&^_ivFCEvLfS|4cm3g?JV<} z`cA$%lyBb3G9zK`S?24DCb2>c;lgCO;>zPcJ46Ea9pj(H7)&J0*CD|xi&(kGV!WDm+Wk>gz&Xds?dW#ql-PL&1AOsh0}xBb3q>_}EJQVv zPLIvS%t8L|!BYVP{OVV^q46d1bVF2vX^4Hgw{U-;UjFzB^)QXf%)Z|IhbgO?N0ZL) zW*l5w*vdGzs`2M?|t3j9cIt# z)Lv}^n1>^QQL6FN$^<-IEpG>qUFcVf1xYMC{&k$j&g~QbIZ)f5*4R#@prfuzte`_6 z3WGF!vO`o~k7fe$^J+6?Q%4D1B}QRZKTN5=Mm7&Q>~#!ILM zqNjxDNj|VFCQ<_<=~-*FK@!tGar0Lmq4}Wcy{fS*rzpD;JAR|VM#P2{S$xF}B{+VA z6Z(o;s@>U@3iSW7W)aP=fep~}xY7Ap9+v2}!Rtb58BiIkmhGScF)KVNi(YJBq8H1O zZv-ytp;#<~+>5O|`=8>JwNUnnASgsAK@_{DUC-S(`qsfEO+FeZlw52%IH!>{#zr%H~&#J)3yUj^Nj1#!|w;=(W5#j4sDLK9_h-tvwWBEGs zBDI4Mh8hQ+BR*K0`A8y!MbeDBb8}9vY2L~!-ZGv{DAxcR##2@Pqq$^*LoD%F#NKFT zNU%$qh3_Z4-2s+)VHgmZ7wmi327)EfXfkFdV6A3egG@Xj0Q1L;0(TI3v)C;XcUm%+ zfYR*y21OjcTkoq=Gzae&Xf&1g)0CP+4|IA>qvAn6r1$faHRtai&}xp|&&bl~x67!^ ztSfDitbJ=2HZtG17|JTX(s^H#29FioSsZ%WaW-Y>}06mHkb7SyAHsNUzHTFZmn z6ip#%9d?vWWndS9sGh}wyPOlmT`;B14ITkFkPh+MOXxtD!Jl9~5eXnxC_nFax}1Ke zgN@vi#P^D{r$lal-=|o&Kj>QQ_V;t@z%QvoA8ILW+Uo3fdOrNwT|<{*Hbg)Bo>L z!oTe1|5s(Hs)>#NQc?XZ{^wKq{Af@9e;ZU{|3B%ryzyRuvC&mLDcGA&y{IHUe4J0; zX@>sgk?3fHZGoo$XhVMRU&^uvAYrR6O`5$SG&k(a4*|rLxfWF zBA$5Eb@Bq0@8*R9x%{*@9_ z(MqBSI5DBGgvB^qr-VN~^D^Ag%Mizo{A_s{wo)-(VE1$>*90HKknT)acP7Ys8it7* z*dL$SVBUJ+?H9tt5iFG65Z=w#@Uuv$zq3AvmO&^%#zrn(V>Dx?0sQ;5eaR5wtNu>sTf~GG1dK*{oP}K zIs22@kgh(gs}HgsiDBZ7_}z{TniehXP-o{PJR}B66rMbmLAtDv7F-k9@gdf1MB%9wQM|f??kAE^-laQ z>zx=nSRLNQLGiI4?Nf79`~)xK({xz;1kL^oIW9ilQ+(PEjE~vsPs@?9+<2TW`cDE;&DJeVh;z2ALd@JmGQ(B z`(jG3Y>W-*sTHF<GcYHqG zJb`;-W=%Gm;F)zQxKYh%#V%AV!%m|?QGQA;ogi;FMVO47Q_wus;_D!n$=YgAlEV*gRzgH-4*$#cc=Djn(-nf*QF zB;}zZTa!LG}%44W4zdeUjLE~T@POKf#j$J2n8YH^$)~#?KG5eEG&4Q-sU_{L&no#a6XNQ$bPd1B_ZP(!58(l<7>x5#=@}3 z9d)fJOdL}G*tJ;`I(8-;=abqz9A@28x0MNq=pN^)`gdOmRSkx#1~JWj2&W!W60cQ> z$`#q03U$wz`yK4c8p=Bn&N~q@7Kd4fRzN%D-v+l2K1e(H?jRz(O+BjdCEmqV?Bu%3 z)S?y}ToT|-;soz6uDk1#=^5#z^J8Q z{wZ__c*#DNV=NHQ4jT5}d}Cn(d9n(%*bg>F5mtVF{9xm@6~QYJ0J4FVh$(io#)^73 zl-CRcWSE*2QXkrQdGBNWSqN2V_qylK2TBqXym%FYEB^shc9>8z^y9tF9zKA@49%i7hw0$L=mul}4hb(|LHLf9Y>6lD<4jJRtk)dMD-P!shm0q~tdsA~vL3kmLQnj*+Cp>=Em zzQ}OICg7C}ZDkYiPll>v6N%6A8`NjdXL+4seU|kK)@M0F9SBkfKA;W-rQdDvV!krE zIvdgygf#`&JsBqvb;gxW_y?C~3i;UC`5`6AlS_~)Dtivxqk~kH_#2^2LFqR_y%3c9 zZex$?ZAv~3=9hek4<05cfTNzit~sjuf!dt(j>7!R^ZK*)5xnCZjNduwwA=g+!FjI4 zj-{4Gq@mwuXkj25a$XeV}}j8SZMdcGpkMUYeeDw$v?_)m!^~&DO^H z(XKw zmXx1bY}4CYUH*ykekVQJwbV3d?X92c9f2fFGjM;iy*mb)5f z7ke6;EZvP$f$m{1%IjLzLuU6NpSd_3XzJ{;&XPM%cQy16qlz1?J*48PuKF^1`NBxm z^jJ$1&exL@cIgSeC0=Pu`kNLT7xD8qHd?0_9V3lz{qCBjiOJs9=ArJ+hFYJ$56R2= z>H7M{q528iDD9qHoT}=oY+LMbG&yD$dhE>|6aIQf`B-bSbJ0C;p>m+Vre>z2CNS(N zwT!eax&|(_IotG|wmN@h+e}T}Na<)<|H!35TSe1Sf9rI8S$jvf+f&}(G|@dV-Dm0d zbzbPIn(X&Y40TmmYz@;3bMvjffpWcjdAip%dTF+_qNcQ~cDTE9seOLBCr~_4?OYrf z^7YWxMVDu^z1dya&{a1%GIgnVW~r*DvcLC2*X)Inwq^ZL$MC|CtHajobd1e1U37(Y zva#8IVXU;8u`Vvp4$r!3rluPv=NBs~rYsEuJxlFq<3@i+{SS<8pv&}!IZ@))rrFAJ_eAk@V{PCfAX!W-CqIjq$AJ(`JLe zv!7k&f25y(@_Xzu|5ub(mBr2f6*bT1|EKX0&HugqZNq3lnmU>;@biDuLSI|GHEzCd zLJV`Dp}xbuI5jmeW1T`w)r-pYH4HXgC|~r}4b9ItdAszp6N8P74VLMO((bMyciZAj zMOW{@aFrF!gw>8>&!USSDIT?tc25mWRCYCu&DV}p1eTVD8wVB_Iy;9tDiF)+>+7>v zN6K9lW1iZ!p241mt_II^>s)8RQ$OIY=^ORC`T%H{c5%UEgMu(PXu z=n|T`oDJm*6OM+q;?m&-OI_3OrP*@-QrA#_Wgp!zFf%q+H{0J57^xiU9-3$6#|Jf6nI}tQxMK>#Zv;4fHk-4%7}zcn5u@v-4hGldH4Q)7RBL zW?AkZo43yNj#|7O3*K7C;C$0yd1HG^vs2&YYH&?h+6UY-l^*w4N1&BnY+YO$ZE2Y- zzO>lYHP<}Wj;4V=PmQm;uClvtcE&d_)Df`N4h`4M+uEm!^`5cXUZ>wN3DSwx+5|#^ zwE@BeJ^Q_XsMI>yS2tVdtM2IRT3m3pR9Dw5RN3nnm&&XYoeSj`8fWLLt*+{6H1kxC z&JMK%s+s1=VfSp=T-9)Se@jp4Qhn8Ar?0iTt#zQyUo}_ibo4fqEtmH-w2pSx)YlAD zwv9G4P7lpC=u7SHrnbJpDrV?Hc~@PFqqC~Ic%rhRy2axupSO7jO6S{3oxYK-%HeV} z^NzO9*ga(vv%{66m6vL|{bLpS<*~_`*0xJslRfh_rJgpQy}YKw;%=@O@s12ThgygG zyC&>4?xmr+_KLEl#qRz|+P^eEINa*7+KOjAiw?b`u5Pq(%D+@R+FaUY8*1vm(6>0T zU}>-Kurh7!Q!@jV#WQ0S_0u)3noA4S(|+HuqkF7jzPfa2W_HTCVAZ!-8kZ}}YTBD? zXNMP-#s&ub9aD8xRrF-jOrLw8qo=&hv|ncO_P^=b;b0kzM*Pxtk|+VVWZu%PFLGx!@$T~Ps_mY>_C~5>74frO%GRGn(OOE z%^dA-nz`hwZ7QoAUYvC|p}BXgYg%vV8|azn=s;3l#q31q;8Oo!xovj2dguaaa{a(a zPxENq{9tXZ)3-2CHD|A$x!~_<9ij&o+Gnhz_4?XCPisZ#Sj|H9=wg}GF*?~_$_#W) z&}B0tBhKlnzOm){ioPkg-|4EaSe|IEYFqAGw0Dfm*@h}-2mAxg`hcyk$I5v6XWG4^ zRjtnM!9n|g%hT_eY3priu}?O(msYu(#^^vz8=~o%*`>OPr76cyZ984#^!G1cXw^3j z&6U@cHZrA)(By`uTCZ=mcDk!^y0O#O-|1*6ot<1Pt8Mpn*IcqKbj*|%x0LpD)-RVe z&bBSg(QT#r>9MJWs->AFZ*i@qZN6us!BsX=QQ_)tY3XMMTFN@+M?7AK(>K)G(l;~F z-#l-v7;b8^E!EMZh{cUMCiTN3eHGq@j)@C(ZOe@_W6s&YVwc5N@13CQEBdivd%YLRoD+i5NHo7{R+P!n7G!RMYjA{>ce%vG;8-WYI4%r2sw%qmL#?)&sou)6ON$o9G1}8p z+uK+?-&N(Qa@EW>O?I@oYR8z_{_=&^(e|#vrjfeAOY=hvJu~K*>S-LUTjYo0VvgXo`^0w0QI@_4l+hT3HH0c_w8+1)obvKt) zFL~=nr#tjMr?bDVvCiR|3Jfim&QuHzw@DRoUGO{n-6JEu_TdW+BYJyP-DE{w8`IWYYFU_Qs=Tm>_?@l7MK|PE-r=noX{?$@dSO-9XvL-ZhT>sQV6wl;z1Z06w)jK> fV%%KxY0s0-KF>bSKF>a1w9o$!^?))$0Mr8j1C9#r literal 50801 zcmV)oK%BoHiwFQWc?D(w1MI!qaw9pGAQ(|yyPd7xovEJLHS3o#^I}+AlaUlfQj`>> zZI)GBl$l`}84)chGN(-?W4lPF-~b%% zBo&l0vXY*uAPL9i05~`}I5?N!%0Hisf@I^-{XNZQb7yPIqt9*rY_>c6DITxY*>1Ml zJDtw6=JWsL*BjF$-Z%-z z8^QR(n_Q=7(Rg$1349xc<8;>>go%GL3cBZEl7!>a_4IlYtPi68TBTBXe|;VePNs=> za6Rsy#nCu|?=K0Fygh+fjkUGIv%nihqfvAT6?kFd`D0Juofi$gv*^-GBX61nUJ5xA ztu9eLvzJ}dLfTB2sN&)0Bl0TC)p7tS! z0iP#ceii%YL2SNjoP=qQnmrGa#6JyW=6y(eiID6Ef1CzMY87LEW#;Dy1^4{0KM}J4 z{nZD)`@2Ve6aB!2lND(b(Ka&Z(4c68GMb?-1cu%0dK8!&cc-PT1_LvTN zVnFVpu^dt+h+fy8@8L{%hyd`7IuB^+!%&Vc{p*C@CSGfsasYNe@eaaqpHX)>9rp<} z`_UjEL?2=HYbbOYCTS1{14dBHJP0sX><#^X8pYRjs3|>zGMC|K?f{A@q^Jk40Gpxv$0&955siDHPN?GIt^=N{+-qsc+ z_u_QgJGo8+NT0$cIEfN?+2%5XVCeOF;TYCvubKp-Vcom%M=(I$x6wGLdrzN!y!7MK zq?VBsXzw-T5B*X2CvIK7F#&H_^{T2=eoSw0a{!U}62;!paRyv4#@C$~7>5wes5+mAW?y##Ig8n)lRe zXFVAL)C$YOvEH)`Xj%Q2cKzQ#{jzjTg9LkkC9=`rWg`Z3;HSPhCXBa*V>0o9K2tFs z7BL){fO`~I?i`g2ePD24qo`t;qvmnZT*3_MLlfed#Qyj+fL%wg>`M=Rw`=AYX;n4+ z$s`yLs$#sRV{OC;gwWfC+Q0PL$29`Sy|8G-SB+oA%Zgslx2lG?-}Mq$2k_!q;fnGH z1DKLeT2c(ud@D*c2Ox=W=4&~Pt?jv2S;Jf*gqIcy5t zDapn;sj|5o_ZBWuZSwwnm@o&_4tpd7yqeyw6Vs$G5p)*LuKRO-OF$H z4xNWw|I&vs5+__SlGR#<-A@SZ#RhL2V;U-tIv+C|R&^07otiO9b)AgHW*cbW2xhF? z(u;_bxB;!)ohFu+AWm-Z8uxnTdkcf zl>c>_&6WJ`A)W&HpDx_op$Stdi7Q^}Cm*H254OzbhIr2(n>8?^Aqijexg-fH zUoz8;XsH5RWWWCXuUW9t*uS*tSTjdc|`V(*3XXNpY?upe(v3* z{YiZ~nA9O7Z2I+dl-wR0tfBTmAttomji2&xSM0kD^oWrVsQ8 zx3YQ=$5C8|ZG8-|)x+^Hst1k;dB%yo%ZvT*a0n{`Oyqg4R&lvfZdIG#j64a$6 zX$)*pX?;ZHU@qT0|9ab6q1*1uJt{}-Qa)+2KWdbw>!Bxmqb-j^(waqi)M#Wqpt8ozEkr=kwv4D*$A7SNlPKQxH zmKg;X!N>p_b92QLtXNHwQ4~{+1Rfly=OXL_o9$0wmjZ0&isqbKqe48*aWNFl%X;(~ zHnNYW=O-|@urNg)U|v}oGJvlvtnWv$NW#@ExUxPhWMIRH(WNs1Z()LBh+I8f>9FKV z#N#lSr%>&h5|{IOrCgTKypi=yWUf-1%u5`oL3#z*UI72~v2MJ^dS43uQh$N{R_VA) z_Yfu+l&%C0&vI`lttz+gn!)!;>e533RyEJe(De^Qq2v*fq+JM%I8M-kizr{7=$!-> z#iJ6h1ydZ3`!UOUljvO4ANc(Ep5J1UNPZPlqN~U$#hnef01y?Y5n@oRkczfZ%LqR~Z$u%i?OY^G5 zNm4=~L@ZATIYoy6CI1w}ytwRR_K~drSUYelD6|t4HPF@0*qP|9Ng}rkoET0#79iOz zB&98=Lpc^sudp}OOsH2eaAa35oLw&zr%gP)2mz>f!(V=#+M}H*+$nI*(Q1+Af-bv_5i7-5^M}aJ@V2>mv|`hi8W=lz!rfE7!#XUUwu`lYbbFc z7K!R`IX9U$Noe?*2+rhV64R-X45Ub{p}K^dXadx!@8jj~xj()Z`y%E(3s1t7*^iGQ z)C=I81ZVz52*^@BnWjXm{Lx5Z`%9*yxRKLkvhQWvsCPpBL)aoHdg@OS1_Q^G-*t<( zl`4)y-8*XS9D7ea=y6S_7gCG2x>>WzvL3TJIf{1u;qIO#hiY;8HL*CcO% z*#{&tGghXy)c}60t5Ui9U>3C|D1TScEWSME{n*@{vDJp$qHC%w4hik;)rvf=7zdgP zXJyK0j3suBz8U?ZJ&d|!ZQj&aAH|8IYAr84a+89W(K#@_0n-7#hnD*h>pVDz)-O93iHr=%aE_#-eN@CJ5$B&R3Q71Rr?=9v^e2}BhXtl-E=r^$Xa8U-ZKR$5r^ z;sGk-yb4iIOQeYf;5>)I?tvFVsj6TPj>i7jR;LmpB=WohHnJTnsfa<>6 zf3>&Yd-eL|A<*=j%0E?hy~>-{hlg+WVEe877Jt5Y{dMIwx5vtHzNU_7K@_goU6Wjjjoy@CnK`kq5`4%VwK4=-v?{(_;GqJr00+EEJn!GU7iuEl2fk`~;!7L82$56veChm=eNpV^3z#o_{!Rb& zZ(>mB3ysXbxeL4r(-fwG@@;lnZThnFY->9!AhYCK;EGjK?gDxXT(N5EZo{giCV(}F zRz1d6q42{$@En|R=vAgP4OskjFZ*7rm*!x~;mE`ALz4s8czJc4q>Ul|H}jC25Q+9C&X3qz@G8^sHic3N=VXzN#vb>pJBE zI+9e6;Mm%#d;G8ZqHCq;MV_G|G7jcCu8Wd9Q_NJHr?stBge$Eqo2qNG!T5Yu(MK$W#)=Tnn%bc%0U$*sdbA^9D)-wynENhuQwI($I zb#_~Vau(DA)K>Tp0dYaR&8pV3PR6#T0OizrsYI13Nh%%CzZygdaJDSuisMF@?BPvS zRi`%ZV&p3_aL}qkXz>b`kIBPbpc5kouzd_CJ+77x_zo3`B1kn*m#2J_)oF3CtTst3 ztYr6D%|@afY^y#DSO}XEpE^kANUKm{iB!zTk)?}IY{mzkG#HH<_u46eyZb_m%!&B& z-UPL+1;n+I878xmvuK1Bx+a>cyCVvUH5fPMT!wOByUs&U0}oTG%jPz%w^1+Hk==W_ zbD#ds{|E=xX&l(+439J+#|gFsqkIyim#7EnJWC+U?>x*|!)NwHj6tKPI)OqqNtU!! zFK>c1iKCP>;MV16}ylUra6NDq<7e3VrQzz6%7Vz<> zWN#3`GZv>-t2?Sd9w%P`aX#(>&!BVp0*db`C^RiDH7zdI+IAMpn4_X%?&dUZ$z5=> z?JSqEPesMt?P>f}5umiy7eUH=KxxtTUCM5@3!2sWtJ0#5W@ne(?l?y&W7dj_xkp!H z!`ubK({U6BT0z>0kv=5VXGLuljEv5hl@@i3Oj+4u2L%#@|COK#c!#EK0eJAmy9IGo zi<@$Yx}<`HG~mZMu3b}CU!Dwau;}gY)0?y4idEukXc`bEQeT@B>u7&d^MX+#R3!4J zMc$Yeyo=#`9oKz~kaKnoA)>f*TlgyzYu@9=NIDh@31~7|L9TpD1Fp}{~C`b%LjS&te(}g zdREWsSv^a59`nZ+1^fQ$Sv{-gXZ#?2@Wkiu#OI&+^RamU3-S3&t<0~)rzbvtCqDno zpN~bpUx?3Niq9{^=dZ-4Cq92CKL5<0f@nP!hmXgi<;UWL^7vQc(-WV+Td2#cXZ8H7 z9z47MEuGoJM`Ppse*JnB^-o68$s=CU&*87}Kk1>8Tnh3%>Ob<~@9a^4DORcfZk77q zAZPF>hIYb7L-@n8DWoK3xyS#-D)%2D=Lqr#042icKT4qGF{CO=-HJmucw zt$R1WX*c2f=JxIF-+b|ZKH2!c_In>k=i})AVMLUK{n7t!Vd%f-do@V{OBC&8$3FBG=*vE0g^^g@)D*xhSZ{3{@tqO zKSEi|dqMxsLf8LcmHs=c^n3{S-wcE^YbZlFK%vL~K`Z;8tg=%mGoWRPB;vGa6pcDo z`~T`$J)imWSN!q!c>dojmjADw)w6p3f}h8~e0A{Z#UmvU%ssf*dhqwJvcRKqLBM0} z!f?e1te(}gdREWp^JM=2$T56R(;i!z4fqWIe_K1v9V`CFPIG&ErT_B~&m8vOdBsF& z^qu$hC!CRZ=@}Xx1OrE5<*&~ZC}*fJPOhQO(a4L~`!Yu-I6>z&{sisD!!dgVK{xDq zS7pL^jw~d7m7N4b^dS~BP8&JDWx}(E?zik7yxt0va0LCrk3(%J&7daBceE^8wu6S0 z1_m*`ttnsF(#b0;0khLt-Laa=SQjHK)K1Z6`no4vtM&TPco?3t<27l-&dyYHbf}`+ zzPgvWy_FtsEoZl;?<7>_mAbRab&}st!wYoBMs7+@rA1UJc-6|Qy%!(8t_Uj!p)`CN zNQGOWP#g})sUd01ru|7JQ(IOV!TKmStne(RjRf`8)O%L$evM=?5JPPOc7K~FmBuNwZ2n>E`YHW=w+ zGabx3HXjB*fw3tc8j4|1Ivnf+a?TMspv|4RG^K}PUUUIOi$8if>u4)$)Bxr5sfJTqiHNxl97v3&F7RVf=9%U5@2!-r!{4Vw?rCUaRPa$mku- zpzFdRI(s`oI7D{0zjSRgKsyk~jt)--nV}0zj}~4mE(k4#hWLgpEj!4w_fp)c;Z457 z`$0blFMzwT3RG_(`)!TGqm9Co*pIJu`%%#+^qybMjS7B+m7<$_&TLsW;*m4m>P;Nq z))sA0GvSHjdYhu^$D@ZkTq?+(4^-ur(!{N~+TZ|}dp+-tg^ENLGIgJi;^?r~n|a&`6e;Wo%Qx5fO(`_q2F+@B@PkR6fBL$3Fl0 z_izB5eg5P4k6WEKoclU>{%dcp>^~mlS=9gg&*A`Rc@g`(&rqg2Fn>u$DgU!SOS$4* z{&Wuw%Rf(PQGnUK_MHj z0*=jf)T3~6y07guUaz0V|0^Pc{#ao|dbs9#X%vl?=K{L?{MT-^;AN|`-E1|R@b5PI zf8O5etj>QA@yv1l69gC$gM8y7Zdft5T*xE*L!-}JDnnr^kf5WFXyqCjs3-3VG9q)r zG|u};#tY(^^?>Kf~J-9$xrf3Md#)|q-26x3#FgV5I@DCV{QVx~sr3O`q0hA{rA1gY+ zaPvtzJ)unsGmWRC5mXJF$vr6hQsBe{z=R?_blzU-;n& z-H6rMBP#5AxGkjKB?Yw~le<*!`D7yeH%s1zffM`+1DD4BI6(%m1ioy9N94ebrT{}6 z;RxFx9|Y*b$G_y*-W=yUk)c#XP1QI!_v4S8B}|yz{89ydWGKV)=?EiFh)@a&(7MFC z0*Ga$vE1?q@3nEB)p|DYB3$QbFb-mhj3={-Am<$C3B7EGX~P0r>^KJ4VtZZ`?m0&d zr3qA~*BefOU%(AhN?gX{NM3}`^5x#}Px`VP!iRWPCaO8tMKyLd8C_6jW9s0R=m?=r zPGD0m1^F=|n^{eA$T`s-mv}{KuLs!LP(a&IELD++aFmbV|?y!r0ktG(}f z-@H3Gta~rvobVo_Cnx;<@Ol!+;d?KLs0s*uC~G@_Ra-SPWg;q>g04Jq$&dS^DG2}- zFa`0T73)RumNWj7z}wY9Rqw(?tnFP7Ese4ld_sm~iGD}qqdGZe7nShIESk6eTMy+I z#WAOeLaCtBx!CUbx+l)#w5qzY?JVDPg6^cVc+wLw;0pShStq0elL<9(#s(hddKf_v znO8YWjl-Ak*E05%hLU8eJ!E6C9y5?O4g`B-&Gso39Gk(|uw;tjte}7rLv5T~ML;7N ziU%?>fzO>h^eon`9fFQq$q7(;8blFs8^7-Id~qPLT`!%YH2dFZ&R~GZ863`Gs7F)= z_`jh!$Os`;HGA?zyV@i$5V^jf15AFA?||CPcoLi#Mw3A#J3G7}Jx->5bmfWrwYf;L z;RzG7fjXU5vlE;coWZgj?1O12+X#QXH`jcb$)u+RS1~l^NK81c4 z5MD+j9G+c8UTQ@Y5vU7F?ueDe2xQvupN^svU>dWII)LRo>p*r=Qmr!=BnvA&6&DsC zl!D<>H+HTyoj^$ur%oD8D#evck>`1l$kTmSp+9-?r~Z&ARCV9Xmm!rQmQ^qx?CrmP z{>B7bbbVO4F?)Zzd!t&>Vjs@m(_-;@d1v)b=we8GVm}0acAwFMWm%QI{rz|Q8B&qI zYNb_a$R)*zf|sXUT}ptc7A}0BTvrQbPk2^0j}lou z8M1$g?&$3u*}WI}oC&y$(c`L-vW|Z9^MbZQm87Ofm`1JWhEp zcZ!6%j!Z)8n^k0-U9|#*z`ukI$O7AFM@jOSBhuUaWFiCx*6_NT%Hd>=tLog<0qPhDXHIEAl3m&+%aI2`W zxHXpN3XB2MXxfC)v8x-kWbD5wYWt~f>pXk8gZCmkzb4OB1?pZ^+k#BOR4az~Y?_jY zeGpxa%OOr6E9P>>-Zl1K3L9<6dghJC3*SMOJ^%spUViiZ?bmw;);qo9E}6gT=Am}u{`~!G)xjJqMLGDkoMv1@98>){NG6NL zr0KTMQ4q(C@5tNq=0dmWGU~e^SlQ(DKFoN&8jkxaC6*~0Bb&1Xm%Jx(f zTsRm|XeKQ4>(cgkkmN;iG75r8wdK5RrnN4@@6mj`dA;}ckTe(usQ6LSC)|CHvTI35 zNK%;gCpO{}MmVpK3UGt5-GLi&5JfxM5OYxZu6Oc}zrSiW;jb+|<=-~^wcu~lN^kw6 zVXy*XDEy3#^RfwRn!$i&}x-&0kWE;G<8D^`SI zKwG{C@}grH;a1V@S2=pAyD=eo)pSo;msh*C_53u0&l?37!Dt5R3811_2oU-DR#ta& zi*Yr?uew*+JqxZf71TU(=5eOwm=g^NcKYIX$Az7Cg>o{Vi9%b}r~U^O4|e{M7t#~& zyXhzmDfBX4W#V-!#)u6Db#)004CbjaEfuBcR-3Qp#uf?^TBlKu>g%#N8-8-knVOcx zL^4sr#w3A&;x2K`!WVJv*egMpo{cdA)}^A1*vX1eM$DDO8KJwakY=XV&l3{u(3lJ4 zObP3VHKgR1nRZ3S|LKudtn-N|YiI)1<}68h7byrl*j?JAVvfdsY-N|#lY25HQ_~1z znQfHof3=$J_O{Uf>a@3Z(Eev@yR*5{|9Xh$lkI;DJunN2yrcEAWB8}<-QYFnbTFy2 zmVP}QCAZn>TYrw$hu)2!^uw^uQB3P`I;|%W>gSXe8+kWTc+gBP!1`yf z5mVGqp<+sB#o#Jr>b~BGVszAXjff^Y*f8HbfX$ic~&kM#aL0 zLDe`2CZrn#=cDy?B?4X_VdbOfbe>@~>c=v)=Ndp`ZmxKO71Pd)^I#OkRL+#21|uU6 znkQ6RFt3N1MOigjMj5`Q(Lhq#wQCP$NhW>@xs1|QLVQ6j#5t9hoLWiNnHX3T6916g zQnc4N4bnH1R_*oB%%In+$y^w$?hQ_wX062>TRw=L9=ne0ay)n3JhUSdU7%VN59Y_G zC>7*xBUC*^JG|hAN^}(+98Y0^VMNT+zL$l_lYK+nt5%*odE&i{1H>r-Rdn))ps(4V zAzVrjRb+&=;KeT0SzypY?^x*$I7L{0tkbDhfqb5jhzbK5Px$!FN;?6?1~zgEkup{6 zvQ?7yFqA?3Fmn{x#l(RShV}-*$iKGDg{;@W1YbF9ZbbF7tZRUM6mqt#oB5jVVnAn3 zaDf$F(F7M*(akr(KiYh%QFj_U%{e0952ajF-8?w&`Z;K`mh4%=D8e~vAl!%5WJ9(iO;rxEHZJ#cd%S^pfbGV`t*15mZ#_@ zArI*uoLQow_ny^PDh@XTy&>vdmfRs_N|Z2z9}_h!dFMDq)|8v=yg8*dDySYC#|m1gn?H7pu|+I{TC=7Uwfyu)#{M_ z=gvz0_aINP{7+b5e<#FPUn{)Of1_8PK1vL|evzz02{(Xn_lPblL75bD z^x%~<`z5fbfjd9H%at00{;8t?Cg<{!L?oXVlXH2`gV2!`Qgb<@^bFXNd|pb=<(7L% z(a6a6#RZGT$jsm};qry0M&2InIf@qZzKX1)kvN=@)KR9_=9WV(*Xu4h-3`XmbK+*{ z+FINVZm3H=F-FUExeE=;dZEKdG%y(>ZOuhvA7=R*W=r-Z7qKsHrWY)vyOk2GaWz{W zROnR*d;lB{UXLf!RJG&H$R#dt>vMF4iY(REhc(ZWN-$09_AvVA zX)p~+21ey@jtej>O!L|10M7sMxz8?o%oDNRxLH9qz@;%|kggh+o0iag?NCf7V>jC^UDwFVFn(X+W$PZJMuwQFtECYPh6S z35rqCucEjgydiz>cjM7Duy;8`E6!Jp6)#7D9}CKqBaW^db72Gy?~7?7PAgP+jtR+? zSP%=Kyn#)m7`#B^g@qS~Lv1FD!r&~pVEaVM7aV&>nY*<5Hj?EQYB*PH)@snpKL6He ztFR4~Zx)TD>@XMCbJeS5oO^A^fr{jeNt+@PxGxs{0EZBao#H{%u zBI0T!PikBhyQ)Mk2=fa?)lr_eE6k=UJr-&ew$g)J?)x5vE5hF~<@GR=^o^_pz$Akq zp|_C7G@i$&Q`G#W3Sz0xO+Om`V9@i$`)rabQjisxGO8SFZc&2Kq+4-m8ll%9ETyszVDP$?T)QHFw_@f-?o4ve#$|@2rUg)H8t**SRGSvAlX_MVw7tway8oj`N~wElJDB z=qIw!5ACIN^a$^pSJL4&>%0o(TXTkJuiPz?o%6WB#eT#^rgU;# zC9GweHtR8EZ!F*uU(t z>qNA-WIOhgX*e3Zr^KqL%`Bt}^c%q#Z;Yy0HL%B?1y?<~N2=5s@GrivOw-}|vx-%X za2b3f8fo-;}SIP6H~Wc-6oMI9g3lZ67tXk&HfitrnDMdI@ST50bU#02GL*57J1IxUf?t_uus{7;0Z+1T97Lqn++EX_u9GY3ks#R2Lbu9dknwJwCEt6$Di zpnF^Q@@YU%-@JSGzW4Iohqs65I0JgUw!ioF>x09+eJ#}v{wby!b&RUBb?Y+AJbo9aq+*6n*HoDePa6z&+|*RLW#3UYA`5R=gP>tlHt!X=Y;UsK zZ}sJSV^(sTU(7cW#N9@Fcq^B6ML$)E;Z!UAMgqx2tTlh5_ejs7D1b{-bH?&=26}R= zF9&I}7uZY{{iUz6JLt|XbFJldOjj%J=eXkLjpt~rLV=~3B5c8Gz2DP@&Hu}QH_L2e zhW_VHdx!Y{?PhCxtF^Pm_TO78{{JDKIqbiM*@F0$MM=tRz~7GoKSAwuRH{QSnBLQ; zX#g+D(+6aJ`g9Gcs_)5EGBu|xKL)aLt{7Kd|19W#WM3uWa2UkMS`w{8%Ys43 z;y1S2W&5M2n#*w_7o3dF)q@nzE=CjN-UE&#YYD7cwVFx92-F6Rd&vf&*5F{2Dv*T* zXdD1B2$Fz;TD~57*U^*{`NYC8K%`TW^vtI_0d$*}RT%2AHP+6z_n>_9s@b}uS}qhi zoM)w15$Fx-BrLTj8gUs$=*%b^*#ro}xnI8IX>Tr|>}Yp5Gx5%%lk*0jQ7mrvZnofW>JAjJ+410lRmR5581B}s_%Jhw9h z148XPwuq=J3~U*zY|KTiybV$wFV~%`x22FTTJP7dilB|z06{`XADSHI`BF5&c9+4S5_G`1|m^egJ z8h}}|SA@gto)3^!s>-z1pD`*I;Vn1pLKe&_F634Sk~-2n!&VW;u5p zPv-F`&pf-1S)xwqa(s82>pMl;iwO;__hpZlgU;*YkvAN7Zo-5Np2(aDr%Hy#T_U5F zxw>4~CtnirZ_a~v`TpF)=IyecZ#rnE0mMCWYhuPe}jRil~VC_%QgWa+jkc$qWpBe(gCb48(Xxx1(7;Yg3pSJ)kcX+ zkD%y!ug?BG&i24CkX%seIrf4Jx-(#r+_E9bpl|_5jm`%jVB-VA zhAU1R7w=2VEWK;UB(X9nUs9`COLCy>#1>eQS)oN1q_Y%{P^JoaQhE1n#XZ!2M9V#n zYk8iWc14-xlXV45SHflR5(G=k_I*9Wb85BVG-(zjK2sZX!BUl$>XT_*kJLQ{H6NgL zU63m&kmT*VL+^)o``^BP`}MAQcTDvS(T=Xi0akj^cy#Tk#AqMI;d1dw2kp6Tp;-#A z1srhBUfwTNEF(3Gm1Xj1p#0pW#!*j7^A;`Xe_XHx^znM30f8Bhdl6d;&0R2zCX1Be zSv3l{5YfOUoA_}Z%GQ{N)54|_k>nbY2fmgfX(k-+PvdAhNy-G7mOhu$HQ1}*96(9Q zC&!7O5YbU5C%$^loLV{L_4LLi4%J_{kFR=;!ULV2sem<<1Rs|#63}6IwL2FtRa&rw z;)}iE&fdY4dX6r7XD?y8cQ18P$axFPyZ?504x{2aHgh|c>Vw;#o{2PbC{(b^u)iQJ z>>uw@`X4eX<&t$j@M$)isN!9u|J`hEw!KzobEnzrv^u2!-Pvic^uHhE0V?ra=^{vE zAlmU`L$-Pz;>rB~3D+S}j3W79iSGYq+kb4gEd8(Tt*zE-{XfJrhyIto|7YkSG-)vG z6fkiUbgLhGteyqDs}y6Sih$6yiH6=EQ1=BjEhf=r5D%w(SHv-f$r|J}IgU+$VIJk6Gf#J6N_%x7l^31}jd8Pe?eV9&AyCESaTMdFJDB$K+xlK_LTCzZ0TYC|u zXR=79xtseOAzeCT=AJ(N;raesIHf#&>KV#cggQycCPUF^%F!H14lsy3JOE2{ zW4Wr5PuwB0MwxXBtD&gy4OU(aL-p)N#OI1_=dS`|G&^35SQj_JZdo1_x zZj?{<+qzV?L-gcyX%I%ijPsJMOo2$ttt@{W0Xon`C-{Go;^*Ir+TeKZKrMdv6Wd7VEqM zCvUYvYEq^!iXvLS0h5SqrLs8z6T#}-0)umM@~}XJ9y6drPJrOMIS6gFK__EB=^h!g zo{v9(PD8TQXvG7-=O&jz#jqQdU9WbfHIP|B(e`NH&(+#ti(>lfUCHS{Yin}1Npn>10>fyQ5g7~@i08K4rW8Y53JmEW(b8$$SZPA14kET-Net& z(X)Pu!@>LTHK^tLoSY4s4)yRU@H6P}O8C=sHaL%C!K}j>ymR&T-0ZDb<2kQyNG_!E zhNKH~O^O*lkU=yXF`T=VxPV(*BgaslSfSc&H2rQc!jj)AuMhhHtY4%X6BMY$(R9p5 zW`@$S=+SJhdfWXamc;sh8lPH^U75rMly@V;TX=ULBBB+6(2JB zmc3qB%-98ri)DeP$(ot7qqsxD=63jSBauLU=G~j!Iz6IQ*6l&S$1qq_y&-W=SK%8R zGyMUd_p2yZm|VjGN_SCpAMXdd=m?L*SnGZtXjzi0kkDxw4ln}im2e87Keb$nY#OS@ z8eapxpxLzPKd#Q>&f1qk8e>)^Y$~%bYcB9l;>aJYbJqM=U<+h*&e~2%bs+b5<;6~| zC?g-Qt6c7)1vU)FYDKaAWMV8VDJA_draRe;ml8$RJ?Zi8mW6f_Q_5{W73@5UWz^t3 z;t^2*wYW3A76VqxhXU%j#*42^$Sr8{#(m0G_f+p&{Uv< z+-&WxBa}gpz#;VbiO@47ZkS7^%@}H!f%R4!ApPy9%%c9ljGCb4DS{;L{I##J`{$NBzY4&tcMSWX;G8u(9@{jhr5%{xvP z_pY(Txk52>=CRg(>{-$!>T|%n3<{yaDi<(t76L|0Kk<)cxW zs9#2~?jblD9`FRO7foZORK4VO`0U9{`3zUfuOi@vS9GdN)K7 z>nt&>1L>0C$VfOuYB+_797yS(pAPy%*0vyxM<$ zxQAudQKQ8B=G_65qk`*abs+Y*!)0+4uT13r$u7{8&^KU%27Bu$G95x7Yj7G;t5tv(sP^$ zqbR0wru;k@8F|p8*B;F4VP;WQ&6ZIHQw@+>?VUyw{%d`zi!ZU#EN!~AGDlxhnaz&< zD>eRPtY4QNo2k8~~0zK@OdI^<>0AuBC#R*|FK`%RcSSR&T*j>^h?gjm`Xt9nKVi7Pi z^AhAy@^f9}hi?wbu_H-F{q(9^HfjwODhD#>#bKyni0njH-_bLz!)m1;!={>-l6?%W zON+sKyO_YlB)N>@fkkcb$=&J^4~A1rKIokq;l^#VG`?^Jv2Yu?=9wOs<9!F~Ay^~i zB~d}3B)mj8G@8utOiwk;k!3ZSXZSn>d+d(wJ=DWmpAzC)V+3b)h_2%JEfUgdM0rjIm@eS=7#mtEWFWwh9YbCY1wTf zde*SRqFcj_m7wjjeU$vqJcll|jT!R4&1MtzKeu+;o#xhNr%m#|&6WP=gFFk$|5#P# zj`BZ7FRxNv7<@u{@!=b_rHDm)O*c}vH{Kn`w?T?C@o$eFVJ6m*t(Xm z2Ia`0+ebVEmKc2&c@%$mA{80+3J5o8YA8Yjyh9JP$(j#S2tz+YlkmnI;!p8Q50J3j z>v6sxmrCT&iwii?W-O9N7R%~dKUsR)Wj4Xg%#qI6MdU9jvLZyFw4p6e zJ$&(st(hI^B&aYY8bS^y<1CXADH~vmB&!-$i|(lqJKdvWD&$j?4xu!C#yzS{7F8jAdr_D)RQS^1lF>yLl}hUh2HuA|9^k@FLfB^kKK**e6leCuBKU!{mVLuYn$~D_Vi#t zVH&jx^vA3qwJV4m6`1I&Z+kx)wf%&V$7i7ni60OBHEF#pr*Wvr=24UZXIXVeu%pV# zB*`3#E-T|evjTaKuvei9emD#JXWqwC%8<*53l9=wbrj zeZyhF=$(h}WbZy4yjV}8_5Fj+vn^P_{si#vr$t`$TNeCH9!Mw+F&)ojUfODG(GUt_ z(fz{@i{8g9ivyD^?x)g(oJ$zY4ZJOiU(uhEg!l3+h(&}O>7=G%qcARWwZc&ZPDq?v z<98-QcP9-CZo;2NNoHOAjgei}g`NjRyyBgx?mOC{U$RqJa6?_1F|=oU}pAs%KnM<-3TtRs~&zS9kBBdFv&s zl&MskutSTlC#f{Yp2Xn=QbpPHPm5B0G9x5z^%Ci1i!_?<;5vuyJy>nY2;e9#Wtc#f zUc<=qaZQtrLT&cJS~jF-Yb0mWbP!$QJ<81c7Nd8oYnPwxqwaqM;aa};-Kx!06=v7$Cd%A|SPnp< z&%ii|d<*lZjG)$qunf*Q^5lcPWzb}l~;ebdhag5q>* zfr8aK3$N-*j!BqKIkzzLn(id#>fg}p->3K`DeLXbd$4i`BzNrG zXXlc{bTnejWT~5yxq3S>duurf%wV-%FjQ9J9hz762hK(dE*BjmfzmGv5ao+-K0qwh zwG4=2dxqS2SarDFvbaWxJb@q)%ntdb@Cumep43tr%`h~(b*1$t+{kzparcT}@%mNA zO|$0+DavNlm8{j2LPNbJBS+pMCb(y6IZ1fYHSYfMvx!6s*dU-b)=-1$1ovk4p=PN)xHb<*;%l4EJ4U*p})?q zTUfRvYqG!+4WZkDOgftD5t3`GAepp5BbAIKvm8~sN7Th1!qK)A+BZm32cVJkYNNYAj*Mn#YjEqgLO>6H~}Y{362L%uvJy}HYl6q0D| zC}@|`DIRMG?@z}_pEzRPu=4aNeS7-Ud+CpH{>h_|HUU(1_XgUQ9-_nq4=XqL>eh7j zt5i*Hc$RNhv0r{ZPp1DHo=-15VOhit9(j!m;;)q5_^o z@1MG&D)_=vNmcRNTTGSJ;#bZA)!LzBbaOE=ZBB0hW;{?w2xAnJ0>2UrHo|c~CY9Wc z7*HJ+48w`X{ct!8VmjhsJR*6qX4ba8{#)$iukZSk6A;fNe?W{WY*FNPf1()_cD z2oL7kkbP>kI?u>-jc-WBg4k@gT06D*YG`jZxAHY?HN7vzfX!XawpNW;jGZmD6t}j- zd9ip9DAtLrq%e;a6$@zk2iT z`2qvbkpm#4|N00t+VfT-$H1(F2BER&Fm&WFh|2VFXjmiBXx4ITRc#~HQZ?@U;$&rR z8Oco6BCS_hRAD&fMHkY1@@EODF3RPUUW#%)M*(i7VoD>jlw;YZyqYiEAKm`qV4wb& zC-Xwv=;_leu1hu>SfnAszhF?FDn>Kw(U|CAfQp8VXU{P81pr)3g(mfNNpeX831uu< zl2GMgCZ=)l?&Y_8hr0CE*l?YJt#UD~3i=WwDMuP!v4i56J3y;=G=xHfYe1CI@~pA~ z7jn~s3XFv|&xdFJ_#*`?g1z{%QCTo(kF|7AlzTEg6;uCXv;4+m+kc)#(Z`3i|7>=) zHh1jv-`3{V>iqW*&ph^@OvHb{Lz_^g+E5WGfpmrwAvn-Nf*J?HEb0gH#3`~=b~S12 z_o0n6T2mF~Tz+n-Q=B}Qvbxr%o#W~Y1p>*P+Vo%^2kPbswM$=&qW;Ia@mdQJXS~qj z&Y?Bd>?5BZ5RzjXe3#9G+Nhs`o0oI3yjZ%b!)IMpTla#H!o5au2nz*p12#z+Nmk3Bd<-W& za`sB9;z@W4c}Z_tlqTfCh_+)DaYDA?9At4WCP)1yTA9~r=3E@SfA{uak0yibwxcHG z>jMnjR9W)wt!0=6v;Md5-tJj7%N85SSu`CDdeW4)%iQ=ZDAwgd;krnIpG^W+k#Qbb zs8>QG+oOLl!R2bP?bMsn3pXhwA~R1dtOn4x6Qpli{L38et6~U={ zqC9%0IWVkioTrp|o$5i@Re+&jLH~6Y>%T(nmr;%F7$}R}>AE$iNmDwy79DdMBAC@X zm~i>zeHNM4f5_BZz?uZIs{v=xPP)UnGRs!(3^&t{}+F52QdDpd4TyO7cigd17@jCU{+pW zR$gG1}OcfLYpZ*_lFoli8JuT&`T9qwh>fIAiwNkW#%t+BLe? z7A8A9Vgp4wbPH>jP(^zjXm!0Fde_lZdkI^o!c#J$Q8fV}z>^dvHm|<=st!3bi$<-= z<`tZ=yvuMjQc$s?#n_`;pKzdzmjmgOvSDsPf1pQt@~b<+2gY&_9lXcPW?$@ z+FI-9(UE_CGVs0KMGmH}s83V8-oOntzFy}#*Zl;g7At z*|}Mz#%K`b$do5IQmCkm@5Stw;UKXcAiTKFxUp1HCX!tcDDLB|0BF{Z<G1BpYMc&$Vuz~K_gSv>Y>hLwrtconr%Qp!6_*Ez~wB*V!B;|d?CA6es_d8oj;VJ((*iKa(xz8A|3nw5&wn%@shL@sEJHuoh)eh0Rp7i!zd92aG<@sg5Rj}~sjl2v-k&Q~ za}LYU+ND>i3YFlx`@*;Y>sL-$S6KYhbjoUD^2$H!Ps=~+vrqKT`gY}?_2=TB^&z;w zl^fTU8`rtrxbnwex+7P1Y_xLadN)_D+0om-i$_=Uiugc8VQe&{A*tcUMNic2qH#^RS~ zY!JlBKG%1*qQY!LP+$fONTREOs-)Ax^h-r*4X%Rz6t-AVUdblI1Uu%B`}j(lKRDah zRL+4yqp|=-8pG&hr)rin8xvu!Cbd_msB5EBLDWSh^fT1vSmS`uDGP2}U&D-25rJ>E zIr6haOjBV`@Pp?VK26iu6UI4&b4q)!VXI~@ptq-Y=kB^=T97YNSA_C->$-Nguxn80 zaGsT4MW8nbbXa;%G?Funh%r_podAJ2_v?3q#^w(UT9pfGe1PCe&O?W{E(oKz6ex*v zocCzL(T-bMuwxo#aX{l(!Ukf_3M?$I4EFZ--|g>a+FnRtl=kKgB;RJ7tDJQrtl8ZF zfvw^cixsK+Qn6c=jjDE-AwzxCu}78vdVxmEy3P5;W!d-ARWjaH=I%`t!qHGG5`b1= z965e6$o@$H?+ocBHTqa;U-Hi`L3IL(_7E-9Z)B zwhO>nlj;Q1EpG;Hlk5iwu5F`Vv2n7<7AUr)2O8iem zaXjlH@gbrjnB*au9+PGY3a(j)>;%Ol1Jv>(b`N!U5>Zn%jV}FoKxNOM2``EV=ns_z z)~S$x3k^8z5%8laysaLQL<9S^S#wMxqAv}=teGsrZC0ww)bza8`ZGrHa(reKP{2LA z0}8mxECH`^NHoAoN2X}Q8iYw7@FwVmV?Y__&gIEG9_5*5_cBY=*@c)G_uc0DPWd)v z!bFr|*(2!KDT+oY?+4Ghy%WNCBC|T(mJOD>L`H9Obrs{O%tD(;bAAlX_w62-1Ixo7 zZHUc6s%wC9)M_rc=A1pchxMoGr$s2hEFwTW)9y*F3%4y#pMJbVDWsfa8GOWaAWnT0 z3#qaTQ)Rs(j|TAKnf8Lm)m@0F133zBZ=nafHu}>;qm3>2Yi}_&;fLWoJU8t2)M=xQ zp4C-nKEJ6`pdHDvI4&jyM2}_=0w#HL5rEi{y__=I0Nld>3CgJ2C?txJQnFX2GS4w38_RS|oQbyM(Np1ikN z5>QX9VoBtHp~MzF&itc5Lb6Y3zSs*cP#V!^vE#Dg$zXATNsTT7CE)4(!q>@To(vqFn3U1upCEh+QHb@T4qBCo_W_)svu z?%{EydY+tiMakuF5(=cQgwDaS5Ja%6qFA|S*z{KmPQb3>#PMp=AXvFM%J$FDq^?Ka z0mR-wAsm8Sd4Wi8-yM2Cyxafw_1mv^9rXbf14_w?4LvlwbyVVj009YuX^ zUOPA)Txg!2s=^tux=RgAY>nlzVA*k$`RTb+dH>^`%0b_+?=W0|A#o3l^2iR)&zs9W4?9>`sAL<; zd!%xENGMk?L6WZ={2G@XtW6?j+Vb^Vu3RDzwvuz8K;`o*L^P-&7B`o{Ru?xIjB;HI z1@Gi0>nGqE>u2#O{U7DnLUWCs0F@3tT@9#Q|EJZ4|4jX#cC*u2#s7VfXAb?J%wta$ zYo*Bl7t4&Yd|6o%$p0asOK2e2L%)xuUj?Zjj*xfHNSc(#i|bNS+9f3)Y0N*w=og-! z3Iz#iIH>ehaKnQN22nq0gc0;>19!G0-AJM-Fp?Xi@Fe!*>n!Ba279|~oTcZZC*15h zki9qz63bUv*(xE&Wr{}I&!>7A6Ln->#>#SGZv`3uMCW0j?vab@nc;v-*Ecr6&vJ5f z9~m`>+!x481)jcX%+|dNImp7Qi~M7$U_;U~4l;+NVw($%R96~ZS>=uxMUArP)eAYV z{v@Z>E^k`%#UEOVzbuEb=lqe02EgBnZYmLUg_xzvt}c=8M;;oppj=sxrdt_=Dt${& zKZ|90+sGeg=|+B9^#k@ygo0|*OF)IR2i2hv-m_k`28W0eX+TYHNZy-ErdN0`1aF#G z&7J16yibD46liN}+mxjnFE`uplB1yHGf+fR%7HeI9Y+qfP)Ez@|7ux7-dd*9eqZ#v zxEFaBw`Lzo-V=}o@3USMWKf;Rzt?&pzKV~E@Sue^5-N)}$Vc0CZ(H0&h!4KsFbZsH z1$OG*Gc7LaQ46m*j1;_}Z*S`}+=0&MFadg>@CwrAjN#j7A5xic84>RtS0I zsBBxSY_rwwxI41LbDL$|+TMBQ&iWge^roJ*)!E)|8BnTdS1w2i^Bs^fIvJx`2yc6n zF>eld+t9U51Kv#$WN99Nv$g7in9w~3al?&LN1L0xI82}$t>3tF1Kb@4+>O?=`9N1x z#07Z=AnzEER}RwHiH=7dfVS?yYQEqQ`K!__`dFrVyD z%`nq;m=SCpbLGQl3sW!^0g8SLN2+3R+|8}3ih%)%ndUWDuW`nDUeTUr(JKJZm#W^| zqP1r9`5xo6mjpeh_c@7Be)vE`hqW935W(_JWW4E4nwCkaECY+cQC zp0q$W#>0i=HhmVUe8~%WHP=HAz~Ra(9q7Jyql4T(X&&X>J_vXlnYCjCiqn z^v90>fj2rQet)Sk0B6PjYd7uqf7_ew&6WKBA)W=}|GgCGkrW2|0|H1ASDBGr2*g6! z#*Ex|iB5HW5v&<%LWv3bu0Yq&X=4F9N8Lhl?Z*h&H%olU^t8k;zX-4;!}-Z^&YXD{ z!kcXu&o#HzB#su2>QQ7p4ld<5kG*r?{Nn4n2p@<0ii#A6cB=sk(uMN}aV-GtPT2>; za2ittw@DnK=W2vrLXd^I>-F%`rq>fO?WnYj7tacYtdanoI{BzrX_ zp0S}?GAh>t?xo*TngB)lN}-%46bC`7+g3|6g+yIRNnA346p&;sS#4xTEMb!LtVZ&-hzt@k!VGc`_*ojXU%p71Nd}%9pqJl?ZC!a>G0byIDxVESz8#N|3(Q+-FnQ z36t7#oG%TAUNiYO9Ls2897{B%MpmAIatc6MOcy;wN zpZM1pCt;}(0B7<4o1Ko$|F_#K{htSU=I8(4GiGS2KZg4h)Fw%!HxDDMks$KCO|6mh z4DzKm5PYaN@~<(h59gcHU->l4=-McjK-^110-0d=T;p3si*GUm?=n_2-n>3M ze6v^g(7&GJMNO}iE*$t4uv+m?`Y;8jXO(;wyq~Wgu@?dUSAA(7=d(Y9{5=$VuQ#t+ z?Sl7s(OY%DZ*6N8;0O?sB~f;`+B=V{N* zvR;s9yQB5%)!y@0FZTAnD#~W{ZO-g6oA`WHlwA(VW}~xJo@ZTVx~Ij+5xV?T(F5UP zL52MYM8SL$V;LV5{@Y2*tALuC?IPLZ6@>DqJ*o#Ajvd}>@e_{LeFINTeoG&!_s zq?ruVJ|os&_gc9a+b<_=VXpsvE+Go`AID9^y__{xR}zaong7oS6`oHvl0S}mnV@GW z#{l4mw>zCe{r}Dm>;G?dwzoE0+b#6}xz$|B{~zQ*W>7J|%z+>L^BY>;`A~+!5%4-H zWvmaP{+jY`Cpqyyj{FgN{_w(20)4ZwN(p{nmI0ps6CBYMZJ->K6dPZ5&3uh<@W+v{pSpXl>61~&M)Jyy zs{Wb*3dMRH@gb793rh6+cmsj_N=UW=EZ{4Q*`3yQW&7~pa7 zU2hM82}YzNNVj29Ekv>(dj00TMlTqj;?CM7O=Rocd64?lN9Ws={KYz7WnM|0?`27b zACHsO3Z@gLhdnG@y%gJOc3M(c|gWZjdja@$0@7k601}8lcC^dtmZZ=TU zCk_^|m^bHTg)ai80rMnWC;GbikYIN7nPV;sChhrLMV&JzIJJt1s6couSCNKxq)F_A z9W~t0yx&AH5sovAmEs=XzT|Ji6lOFAaWv_nxvm{tVe!GX?mmY7S@$2x-===?A^o@} zQzUlgMM!P|4g0$sc|f>o#)AdA!GyE-N9nNFE+buaanw}dj6E0XKx8Jkcdf7};!aEG z{y9(sS;pLj&X}N)eW?qdZ4q9BUWQtj1bwoyz!~z}mtKp5K^W0ejo7HFG|+o-B~y`e zb98fi?A_e%-rQDPGnBixVLUKq7OLz69jy-gwS4iytHasY@HOD{ztWq3HEs-jvLJ|7VTLG?;8qk;eE%EKH*fTu(tZ=LXs*!P&88eh}cq%lFon2PSB~%mBIJ5)ylrWpx zF^5|bugussvT{?OSBtJ?qzk5#nCtFMg(5ceP(0X6&(IuO8bDfRf_TuQlR`H;=DFi0 zS}d{F2fzX!neD3HO}nV%dbM(gm}K^u=huPsvSsUCR@uvFG(8_z$?Ovrt32H{4)}hc zRjbV`3TMmUN-z5wc*@fto?VjdhGv$u5622x)imj;Q$V9y@hs3FHFVyxV9bq!h@P<_ z-c)sNU+!C~8+D+TL!dTt)3O;3gEJ=gTdElb5x!$1#u(s%KOe)%=orZ;P|PUdn30_Z z&--mxf;(6Cf0Ef@mtZ}eb=zjQ=9mH5P5Nxm4OQ>ty6R}5;V1oSP3IA$?vXV&D(nW; zS!)b~w10+&iX4A&6i1g0)XZ z9qkq)jG3EP+ir#c46L)1vI(Tme4g6^lqpbg>k(ltnwOqSF_RSC3e?y#hSRxZ;gnzl zWR5K#qrAMJF$qygU>M?$^>DbJiQx5I2%6({ zjZwh(ZM>9tNN3i8Pj*MdWwoVJP~Ir+qH^IFT?1BY4t$=u+SSdFU0Oq)2CQ?YP(Ttj zzSEvrnoDhXHHLG}5oX5Q803Y=DL>H{YLtT9PTxL;kd;EG5tg^RrVmZAj6 zRe+DEMs2?R!Y<1+q&9oUxH~4yhXvV$TOy=+c5x|0`Z;dpOW2Tg+6SFv53vj0|K#rf z8m|s|2Y8lW-W}7d`@d$B^#59|t!Ag)+4P#Nt)0#0>i+M6p2shygVv)*zk;t{JmSws z|F=8q7veAX{6f~ne^7`2X!NM}X!t046weC>{Qc)g$)f;Dghu&G_!U1I(3{4i$)oGn zqo{u}icSQ${~i7s|C641JH2{V&+1t{t7rAB9@=~1`QmSXTrgJ8>RCOX;e+(S6Q92m zpMU1h$Kw4j#OE)yGQSd^p7{Ko`1~_}J{I|YAwGX8KEDv3zY?FG`23yt{4;+FqV-rD zJ|2sfABz*p<6ntSPkjDvp)RkU)$_A?{)#_-h5P^Bqr&t5>RCOj=P&qq{L5DduU)v#1rYzg^4lVEQ$MjBG-tFUXE^d1lYG!w2 zvaVEcG~7V@P2WRrPMK|gsmLJsqP&WrQOd1YLolDwoVgyvvE zI}cR{5txs`b)v(nzEk_A5u9*eo^wl%kMLrxXBVE*w>NiwV=2rE7_97+SZ152!UyWb zl;3k+%!Cb91g^_2i4lq}xg@r=c3H*h+@DNParq}>k2kDPA5D8a??xM;+Xu1E6TugU zqiG_Pc2uJ)8@s8# zDW@#i=96t%bCXbcj}GL+YMbBkxmZ|mgmjaH0W2sqCkme7JlWT;^f zbQQ)()rjV7Bmewl;Cm6Q@LjDGj8hff*Xf0&pIzzCDAzA2cUD|34qzRe6_g8Slne9a zZmp3p$lTl`fu-&;5~|#SBZ1}aG7_rX93zn|G7=ZNS}A=bF3LwDU#?$J?yR_+J`%Zd z;f!)&zTEA-lRg&4ut`7<MI184pw^C>$404OJh011vkGTfZL8p8MAxgrc7Yqj z?#;z*g--Mr>V#BPkkwUlzl9Ommo^JH>+&P}AMH?;?87gBM!_e}b4=uEVp3ltQ zX?PI`nDK``+ z!gGwRJwK+E5FI{0$>tD;ltQb8+T81T$ncO2;$)J8bj*nyc;q_?)1SwDsj^jQE!I}N zq|Ib`tPi}yqN-A9{2>a-HmWF#E#Q@QRF0($$wy+ab}dVL<4B zv{rOf6E=TN!_SAK^0F+Ob9XIqy=jX`P&2u_3kqgMXJVkp>MAQSuq2E4cGTQuNRD9y z2tIiTtxVJ5`m;(6M@Hne>pgzaHYT0zJXh?;n0>AHIC=eI+qq zZEZ>sKzG+fbSTe(Bf#H|qEnB;n8J5be5q_;(D5|A{&4UD7}53h6Ms4&&tAXTh9O#C z7ZD)BiC1a;rrl_5KWnra?VU>Yit5<{bigDEq2aZ>wzp|Shl)>ubOQY?j}|30)523Y zFyz8Su@a4;WyZttlHSaxmu7_gC@Rv3gYzg=CHLu9i8h;v>%qY>!pLk<#V4N`D{2^y zv{+HGA0|QX6xjG+I34wZt1zvyYxGPZY;TNFQoQ7Bnhv7NF?nEj$5oGmeh^-eGMLQF z!wfvqrO;QR7QKUO*j3N>pg9%lmWNqk8=xNdNWoEnmNMXcq2gJox>=$M8(Wizf+ZZM z(>o*TrAudMIxeUY=LwFq0_p3XhSTatIH`LAvAZ5!wngXX=q(f-a%yq|spOSM&FVQ_ z7Cbo-9F%9^Fkof~V_N z^{LFJ*y%!3A9u`)YX-+%RdLHYgEv}5bC0}U`3Tup3b~EkBO}<2RKerl)!N)O%YZ#< z!1x1qfPkWT+Dione&Sdw2asH_SfoF_L|gnN%?mnX`MopA5|Rrszj((8*(;L%hJAm8I-&vURpS>RkvRG6+;te?C+W;ojv{Z!JO;Kv{Ulqp#V9%Z2q_S5HBD ztyMoe&Y0ROidYswjZ{=5DnyJ>gFmX`*TL(rU%x%9yVx*|=xSlj1lhx{V$ewLJ<=L- zWG5V<9wzoPM|F^TV`N-Q^Qv0RP z&;0zCKmSJ~e;J2?6k@Mp`A*;y(jjIg6ul1mF3{p~Zj( zwmBT43bN4l2VMes&LBSbG?%7p`gvU*$my7yhUe-ioQ#5CVx7#Dg_ii;^Cy$hbx+iR zZc~Th>3sx76YZuXhK)n)d?qB&uJ@9qw>?`j0jCtFSc0R{Ay8)iJ(+IhPh)?a_^c3X z3NoVp#~}SGhOKaZ$wF?omzhZ@Bsp&m(3!E@aC#P9G-FPePPkolp7($9?p$L4JV3lWcb$(pYx$!>cjU zuoR+c+SQx~qwU0XFBOs;|Z?m?GgSP3NMbkLb`!)a=B3{txbrivg@rJ6u z)h*v}mm+oCM2HBqchoPvW#mo!{utvv!<_mfWAH{%5)=;I9-P&HU49=&z@(k4+76oJgotCeq_@4xc)_V?fI*S&KTTBc|I7*I-> ztR=s$%n^K=_p_hu^<|F#-`{)w>bt#0oTN)@W5)d-900bg_|Gf;|Dm34$+Py7abk+r znXgT*x3N}Qa_#UeNHB;VV;316CexGiFvYq~u2EkiMsbCw2 zeYMD6|9^XL9^Te*<%wRb1VL~ES5dMAQIF$}%_szWb0U|s?5a~U?na=wr^X3m) zQl=fh_s#d}RNWQ0ASqdqmk5H27j^5_ty}lhsdG;KP8~l|gFxHV^FHr*30c_3h0h`- zru}YL3A;eUmS`R9>Ld#@q{uwu5^Y<>wb430 z)ZYn++nu|q5MA^ozKKQXCIN?sgjHqN!g0%JB24yU*Mrk?t}1@7_mtb_Sw7|TAhPt> z9j8Eo!;IR;ubN{s1qqYx^E2~ycoFcLr@by3sj)u40K`LVxF1?TDam|{GJz&G!9+Zbnn?9*X3ojQ+! zZE=uI8(VTYcGD6JUPL;CZ31=~T9&#OvzMGRy;LalNFnQmDdb!hOSkx(w5wo!4rV%N zddzYGIluLj3b+ zbi9avog|qeg0NJTl%3S$S1&Unxe!OY_a*HTz^!8Ewjy>B&WuTsZ5J$tqhDlK6;Xcw;E<$7z7zTCoRg9Un0&TnbXilw z@JV5J7E!$eP2G(`jeN)VaMm$4$ApHL-OTT}1dJ7PC4SYD*pw|*r%*Yzr~27V1o+y? z+aXL2_mqf@aSuqieF^{8SmO_{TlBh{z_128pf#h*VR9%EW{4QKhp8z<*#HQD{egsA zB9##?F+n^cfpv1+Bz7w-9B2&*oBnb8w2fgLlfKxv+GKd)<bQ$yGo<=80sXSE za$gj=$$n{$@uI5z%aTUYT=-l}@|Z)Sad$B`My6=@6SzY9pd`fcIA0YBlZ*#2t~H*A zuzEWbD*;cgxgr^x1G#I0hvNcvimTw<)bVBnmq@K2dSyVH|GMMd{d$MqSy|LH)LUDhY}<9Tg5Hd!o;~Z|WWYmixJfwnSK+KI!hFBl@OW0v;zKqWU zbFodK3Ps&?xljsk;$UUHJp+THQ|5SsHT#$C5wLx6l7;X-os)bfEFNGFH?!&RIa7IA znVhAD7qsF-{Gkp$$Jd2^#E0pF+Rw$`F211KHzkxqbGFaxi7zxwGd`izG@9Ab+!kMK za!x2TIhQB^AXkJNoZ`FFXvDM5e`0V4kx=3pl>`7XiIm+UN##5&BSv~WEbo0DIK!)IZ!-z-Or$x_=xd)tl z5f4`^f{5z{M@NguZG#|8c5DM4jzpLtlHWN~8NRhsZ2n_!4Fe(ZQ<+6$`$bW`7X{8h zng+kdiCZxD4uzdS3Z)V=lI!B0qHs{W=td){5!O!`aV*b5B45j1-APV~OS96qZ1Euv zxM<1Wg%{FguWT0Ylq0!LBDV@ZNQuia!r1tvhgAfK#08?+NfVa8&xtRN>(>`W?IYel zh=s)03(Wb&A|mOT%9hG1%qLAnSSm!nEM?}4adZ$b(Meg%i4(C+k&b1l0DUgL=LTZ- z%qo-+>N!gcAL6tdnVS08Q}N;0_tF>W=|z2kdPdY2!d+l??3Xxj7D{U;yBY9hd~Bg7 z-K~NWC({x2%NU1=PR7Q(<&}+dFUE0X81am4k@%#E$9I`{RL5S0)i>@c=+YRbAsq5N zq{-nyl!*qzIo_S!2FTKJa+WjblyuwN4tmf5Mi!(YHlr+| zW5Dm=T;XD2d}W|3xEy{K*)514NEpF45G}uh3Fjn%-Acm2vrRF_iKhGtw1}k_!nFhj zj9!E@er*vx+|5fC87I0y;g>xz#1T^f>NCgP9ki&^{QctZ!fMYB7{5Df(y<>R?VW>a zIr=3xGrwrL$Vt)h;!XTOiFpp$n@tuDfX$t6?P*xZWREYSp4sUl)?6UIq(m^znBnr> znu*^d(o~;XPm-JM+>^Lr!l)KZ7>S<)K6~r#evWBBVvlpzeS%Iu0?Rc`NR~hoN&I*= z(O$a`yN#P*F%_{m;54vJB;%L|B_4?yBWQ;7E^=ERk_COdYaeDlKE%JANc9HpQeqnL z!e*9usfM`UN_YVp@Gh5k5wA6|)wyk+c|gpX!2b$6UO?5rfpGn}6frlK*u8VhkXOX_ zIW+ksE+^9X1jBU+zrQ&=Dq{1^Xs)8gyPV@U=)=BW3o-S6s*A^Q>pwJ_sys+H9QQfh zPiPTnH~eR{r3Ti2tII6aRhF76Q<suh@!6 z_Jt=9bKhw_fF*l5H_g+gM(32%Z*!U2dQ3%)Z9Qf(RBAttYi>qRfgG&yW`GQ=3K3%1J_J~oq&Z5wTxn|IHZnA+Gn8L}$_@RJfr!z`ywBQ}t5 zv73ttqw`EfBb{AlR(|F90k=WVqNp^x@QfQS>Whj(%h9=^+IyuNp+t9QI-XpqgQ)$2LTCz9Nq_68OQieHd`OGOp*AW@ihddF#} z$Tu+VA&V?0O)ui9;-cvUY>ESy*u9T&NsJq!*m)5*fwTqezU&XX*Q{T3xyPN%u6L8^ zuZ!g39q)^4CbvMHA$aA@&*A;&*(r(4uONPioJg>z9p_3ykdzkDu1`lyL%0sf_yAQ3 z&);aggO~!tgx$KLh#jdgyfA$Wat*FwoO^+!wI#xi=oWw@iZ}m)t|OKE_>Pp8FBdgb zTzm03lbF0@%bYrA!l+fzUC_jOB{bH@pu=JJc;;n zXP20okmSkV;qpllN%_zjQ9}U?sh9d@vt3~KFYV|R%s(OtfTHJmfOqfWdI}ge(xegJ zoGp@JXGOvoM7KOC+Sa_NunfEFfzKk$ie~oBO8SELNXILM;z^Y}w8zA6eo)B1t35oK zh&Qm3D$4k+J(=ZEpA25M=*cYHlM&8@8J0JpkS+yZytE{&VjzbD(BKM zGG&8>%_4y+PKx^Rq`0*wB~3B!3Ktc7>0z1$8^*-^6bl1+>hj-?{(rtpE>R>fI}r{~ zRah)YF|hCb9=pWc5Cj zYS_OI-bBewil)Y>WQtbMN|$0xf&VJqsxh?!@>DL3TRW!3$7)xSTQ{b|#~N3%TR*19 z$68m4+c0KOP@a?{lq2WD!k}J)fK9-Kl>s%S`%&|;}3)(`_ z$uCp1-jV)86-E76#WiuvNGk^@b4nmPHjLRoUuVg$ph&k_5!DX>ILF@W^}5inj3JK` z30#PI!cKZ1 zjHzhl%hZ^fR-sfwt5K?@vuN$him@d0HwpdK(b?$f6l2M>9-Y%f3+fflwAt;z z+?}wU9}esV;%A9n903*rs;0xk{Wn4giWO>{KZ$=SB)^oBU;N8jQLgjR;l&?XNvnQ{ zuK6*49lA=h>s4x66L*zXI9^YZyK04+(TI-u^3s}s{80C50~a45Oc@Elb=uSfUF#rb zeiBiQkP$E2^A~nUHGWu=jp{fpDyoA!fJ7ZIBKGU_J5HCDmY#My++Jq+v{@HT8h3cm zeB+2Fk0UMC2Bo9gaZ*dlxZrmbRUxU2F`#E*(ohoBkJA(5geuIcs2V^NQ4MzBk7`N7 zeej-8qdq=UIt1wKQp6HV8ONO0SIV`nG~QiUGDXZSM|B)#C6FIOnLL^$58cN%LqYW4 zZ9SELU{!t15ZU(}`jZyPEkJ)#BKr@ms>8bcNY4J7W3P>^Cv6mjaw@|)mFT2Cl7H~~ z`tR!3PX|jILivs1{6>_U8qpbEO@AeQP5q{Rz47g?kghzeD-Uwt2k=rQBn`*#zz=`| zZj#rn3|xBZvCIP5tZ79Ox$G8oOTpwoKS4W948%}IubzZp3*^Q3EI#o(T%hMVHse?@ z{U}oq``~Ks!2|fg(HP)Qt;k)XI)6Wx(>t;nup% z8U}(nqfx?m=f=^MOg2zBPv*>XF@2`Nq22;Hu|0;fi=l8e?ynDYhHGhebcb&14y~(0 zx}#y;(V*_=&P0X#_E&H-yO=>~xeopV(!ppBfUdr*^ozQ8MqrrA6~&5jMfF2e$B+4X zcIxDHm27^@Yr=I(=1SD*DqAip@GP*LTwpn-5cN^O7#|AGtY&nWmawFb7%x{OswHrp zTWUO3qsmEpG>Ltns0KfR58f!Q9h0MuyEL9Va%Vv5^)+Im+|Hn~3f8PQidU2F=~AvG z2aU%zlp8IZLm}Onu%-dlC`(mk!KXrB| z@+#UW+?d*&59!W@b?1V*b3k6puPm=+e}DgX_pkTAap=uM8^<@RLg{tk^tzy~4wKiQ zT_bwYI*z`O_`hfR@{4LfbRnUXILcEqClgW`B`+%OPJL)nQVqcM8rkbVVZCOx4Frxa zMkqhFJTZct<_yA)C8tli8$7#gLQlk!xle}7yUkh=ovU094-$9CEikKY;` zncI{qxi(_Rc(wDD&X@sG9WtB_8%_tg?+2(<+beC?>6^2!&8`>T@V@R{U)}Z!jJ-`a4P&F8hs~*V~f==Nj;(xg76kY)gom~a;>0j zDa2}(qztcw(Q1FDC_kCc*Tjr>iLjWUF0DU9l+T;Z_H=F1p03Rk${vH^GV#-3dyrch5Uhx(h4diUWgFly4*Tr556q(eX@EL|4(hN^ zd!q(cWhJU;@|@S}!p<2{HS9-+IYv~2k&{sk9_vvR>|m~ls-bz^=8UKw>7X_jx`TT; zn#x*$iH^)bM>5bmQC$~OmO%k0`h#W=E6mykuWR1ziRw@la680D2C(uLO=?3-V#>iV zFg_R{+^Ck+9Zh0e%otIwSw%P!uYqt{H0D28I|&rTscXC{J_c9(R|;6quMJdwVK}ce zV#tnU=0*xmZOjMvRo%-qZ6|B;^&e6wc?bz;%cyL@^Wl9bHwNGNa%kV_@V?WaJfpJV z++z_#CgMuhy6+is!-nV9CVw#fws+GOI#wG#RvR+Zg$;G=nW-Nvg^$;S47FiHt!&nT zwc+)V&8ncGj$HR)YEm9*14=+gN31eA^;*guUBPW#!CK{;XG6N8u&yYmD?-})n`iIr zFTK6LG_>Cm-fzLWd=P)*1yW!Hn92RQVsHAWL|71&7KV+8sVQcaJCqaIm}WJ5E4QKo zaRv!Bh;hu~_QkLT$#A}`UQw^}OhWwG?N0L6E9uaWyB9U1Z3`HC5L?;F$3MC!*7R3@3*u>2TWpFNmNUMCNL);SA%o_W;v=Q@MoWvVx<(8 zO`M`YE|PmR1|H*G{1_i(WWSzrC!_FoM&Ug}Mg)Gno*K?5T%Wp~af<(WWPKs#=ceYZ zuf3}X9k08cQTJhzIyLPfg^~|d+DtTGJLw&s8Hr8h5!50&3JRDJtU zb?8uS_)u*qqb{6Lx2lO`=7cj3-^n!J&NOcv4P};vGt2H|*4@sm3uT@OXP#NrMsT;T z52GZMelm=H=}0F>DT$825BU|AIxcI(b9Jl+&`4c0e;Ei7foY0_++fBeHTZP=+vmhoJsZLpOjYr-1%K}AaJ7RFLuS^h_TGV`>yx1a zr@{wLZBt3fX*jvsx!QESDq=9c+Vx7;2iXN{!#8@?MexQcxw-GPeQP!A&6|g>?+ayChBGTywZGO<8Cln>ufBBcrM13SzP|eP zZ55SjTx~+dQI{s`cj&i|ckQ=W{gJV+B$x)}O5-Vjmto~~{V6f;_r=-7n=9M$o7uU5L+E7YqIHh!( zQYV*3atm&HU-Pa{Z7ggqgmTY^bI-4~M6&Z=@3@nF>~{9CPKy%RGs5rt^vtsnjrQM*KU7bru}y2QF)tj5QSXmU01VvM8) zty~RK8jX7jk*>EcYPskcUF~Uqya`bOOiOh-- zndSaDT=K6eJOqrnH%nhDT|d52y;=R<^Vds5#+IuXb)jQ*;bS0p2m;9umN)l>j-Lr1KXW_d%!e8k zOg}1+M>HUhXs|qDpp3a9Sz|LVlvx+ftOE%n=i1kQaAbY*t>U+fZ>OIM>Q3!2lzP9I z8iiRh_@G#7=n$V)gg+b)nSyx ziNQxv5^tyDms_B*z8lnUT^5$mo}8R}FWtz_|Bl<~M}oQ|myLG333Z)kK&gU(>-(Q5*;XkOiU?H@qzbmxIz@6sofG7~5%uw)^czv12ui;Z zb$L+wji`?VrQe9UA{h7mD3WB{rWA@~A&IDUR~n%~$sc|6(YBUSR0pXl@i#)5g3|A{ z!KPH8Hz}V6b5DMV4<05c;DfpQ<-+6Ck2IyKKUb8>iafv5pU1*~mOoMaXL)5=HQxWf ztg^DYvdV(-pOw$z|2>t@f7lcL^EK+p;y?F-^W+4eG2^mQj269ih(n8hmQbIQ)E-cu z8MFqYJ~J@-Q%7fF^k*`y!>G?>jQTXvdW`x^!KhCIMt!DY)FgHLVeq7A?BXq{o*QD|(V#Lwqh*tAbiqJVSf__RyYRruK^V zyn)f4TOz3E8wBdPCBk`LOTC2%;67s;7_uq$R^gW*JMo;3Rw`gX*&a^E^SZFwa-ht^&|A1VGO)3-OGj zryX!}SExj|rWqNl8;76cEOk5duZ$|c!oL?>2m9up;W&51abiU7X9UN2hKfNI-PNVM zntWA2a)wz9=T(g1d}DT_@Ov1+xwRX7^Rq#Qzq5eNBh*s`Y$m277P1*;!J1bwSo4kj z>;2!uP|Ym?rzsE9{68Lpjs$xtq6W6Sh_ckokpy%^$w_ze)P*Jw6)G=dsDIR2o`EmL zB>R~JU&6B94&?n4-@DcS;-#>z;pZut7;$;EQ$pp{@3R=os~BT>V|Kmp4>7{>{T)%2 zUyeal$`QOAy7C>Kjmo1cd$58{QoLG6sE_A79tZ{`kC|--#l@`I+ZruHkDGu$asG zFBTFL*vA(giTQI1_|QW@V&2Is4aI^n@8+5wCbJOChXBF6v+raGP?&%5i47G`J zSvY3cQ1>8JjKO?0evvdmpnD6fhy*#0OG<{Jd0LWh> zH*Vy(qGGn_k?N2$>?miy{K@N(EW#ED%dZ?a03hd#43_(o-B-?h#qko+ovE3O_;NE@ z@p&t1?y8xb#5Dt=qmg=5YgbNDcICx17Uz%~f5i>uwG1^Xb6GopQUyL*#5f)d`vjI? zHar>pBmvhY?Hxo(aH4T=hh3nQe^0T?$2U`Mzyfk8qwp?XK!)EpQ*We_y!ELNUN{zV zkAz_TSU#6uRSsp;J%Cl^2P!eH@w7hsn1ws`5AJ4_-e1+tXQ%8EScS@a^rkARHZOiUOTR)ejA8N3?=BNViA>tk{UiCqH=)Z5tVlVQMq;C z)cqJF<<;Ghl)nZf<+ci7m>Ij_C%Y+@-^##IIP@gPfyB5;VSLl7*+znv9H3&*l6X|^ zik9R%RzOQar#=o^vWDW(k~es?ka@6tzt zlE>IqWM?U9Da=P@aBOmls)$p{&Reo-sRq3rR$`Un#)OZ6loZAx%zy*LQp%$vFH$U= zQ3f5^LnTB<#tar79SHQk5jEcO$q9V6gNaW3q8vu$7WgpvO z5s`Otj)pKOGDJX;TPz~-<_kAoc-y{l>`!Jx93pZD+2r3VcB2D4TG(r)2w2e@lQDL2 zZ&{8400+s>z-YddGdT$O#{~D3p#V8ONYKp$J&cu$_FAiqPA95WE3TEJQPI=MYF{&X z@p*AHE~9A)D9KnFkH!jhzIr1R(JK$3a`ZK|Nh@ zIhMkulJH%?T_Rz^5t5=ViNrH#Rz!bmkW0*k@U;9WkjrM2yiEOE-FqK!miGZ&c^@#9 z4**H|0HBi(04sSPz>)U>68QjtkoN)kcpvbN_W|Yjpdc2<=%#)baEyx-i(_o3SRCWe zSrFqd)H(Om$M306+*6m|Qy;mfuK2Z%QdHlgOb-n#F7VeF|M$^$4yDVw58%U}t92N9 z_^}U0@ymthsehxfYE?hgC{P+da(`DoJLCVnE*tas`#y4?UE+UNS5{QT?EhF^QT`17 z_f$UI{*U7y{H>!kIR^j7jf)4jDGvW9LjS2S`cK6o{?u;Gn1)6CY28UGwn#kBF(d)asG{n!MImm*^<%Rv?9Sp2B za~LdCjz=D9@Gi^f;5j}Hk8N(wwLH%47Hh}*m!ksd1RykMpCfk_vYM9c4jeQSWf|E` zf4vZKq?tjJ7$X^oN$lHtz{Kqo;uq-=;X@^u~Up+WK*T}D~do$Y(LWa0D4+F z1=2Vl=5v>oduWAWMfpAIpDE+oFVeXCJHIQ{#Fm4UMGq zKxY5E$7Cl1$LH|R&y@tSjvP6{jU{dYJdoiQZC_pDvU!~UL&5H%H)vKz)gVnowUd~{ zqe*ON$!Hp2HkQY|o^c1nK@4Pzs%A%^gZ>pz+OL4be+5GFuV6p~Qn@%zF0Xga6fnYt zO&%H$A-#-u3f-sMoJs_sBzAB{HH}U6LoG;1gbdUSHS8NrM>Ushlk=|MH~vV;!#kTcv_xYj;dj6Y32~B0z@a@ za|#gFf2ltFgX3?X3aQJ&>aw7^?4CO1+nwL+3}zk+sgH-To5#J>0})+8BsJrg>4zg3 znUTEx+o?%u+7Bs|JcQ&*^L7TMN%?m7H@idX?65jJsLtl=IUZ6Mh1Eqtb>D=sT(~1iuX+aC&@c| zysO{Q#Cl_U&K{BQ-{&c&6zPJZk(Iy;SG%J69^!E;8pI3KaXSR5XmtSbgUhN5h|^ux zd`)w0ko;X#aM*ai$X>Ogrjw<9t-j~?&;~tjL&`sk+dDzH|F!F=PgK5i$xL97mj9;W z+Mm)o#3NDzxq~ow3Nt3g&-hBrX=bHUcNujtG#VWzi;nf8;|$Sprs&uxI?fUuf9W#2 z=zL4*?Bbw)t#+KNB?|HV1r##GKMHV1$dR0d0h_ve68<>9_;Vk|>K& zCBsBjsQF^M09J(eaS157zHH1?w$&U;4z>_QqRe}B@ z9WZbp{(L?R5M>PA9@C5t&C8t- zSAzke2G1M#Jqs3-TNGoWSc8a26F)<-n)v@sv6}d{TCGrTQ;_H>Mf#OA_!psagVOH@ zRC!SPh2kfJRI&IQp-O|&Z-lA{O26A$D&@fH@vwgXT5CvuG^{^*B`HEB-JvpWQyJIu zUVkn`9ST#2g4CgHZLZ?@HpM69CI!4RQQEAa)P~i-ZPmV@Y9Dq={6_Q3%~$ZhZ55?1 zK-st-eg{5}r2jvn-yiqcMgOm=LO;O&ue_=X>HU>N|9=+$;i-H$|G)pN|8G0!|NoJC z;`+bL%8?_LlN!JVFmp(zgkr`NpDUmc;;aBgh?(iY2Eb`K3Tj!oto$Ns{#1z&9mNRaZR+CI_sIX(Y!0=GQj4eWBqB+(s(HB8;(v{tC}cujqUl6#UBQTwiytD(2{#n-#%-bxi}TTXkXhkq9I@Sjm2 zGXI16@Y~0Kbn2~BNDaRW8tFGcJ4UMUT~7P{2TuEbm@3w}ztN2pZvG0SOc&h+J^STW zNiA5rzMGl*I;&l8iB;>Z%&PlDrM?R)^;N_5%j-kyEl8n8+VoverI8+e7nJDtb^DO~ zj5Kf^+#A*YayNgQqwnviGM5M{^B4;x6D!RCPqHTO1ad+Ovk2tG6E~=RH_sDW-QpC%usxG}VGmTHF4!!$>6!%P6DK z^h0;C8oGsb&=1eSy+3+bL}eej56UfHQ5C4oRW0u^KRfcX9HcadCF=6sjQsx>>&PGb zU~0ONZ9S~~>EY!1Y|T$knCjCtf0M33$6sW%736lTwtfg|>jThO?@_tGq{@E{+UPy1 z^d7HZ-qG(5fsVN)D3~EoFmFD0<2j^X-UsFKetC=<`E#}I_lV~Bxl(gN{c^_comcY;B>nM&}haHPeo1*g+Vu*-ltFNIz4{e$=w z-?8juxQ|-UgU~Ur3pg@j!&9NMO2He#k-aYC8L{(L+%k2@pec8&1JRV zZ3z68p!?zx-aj~04w2oPxq+_N&--js4%2Bp8xY`Q4pON_r}i2l zO}xDFO@MZ1H+it&Ey8fioEW!ELV7@3T5@x!-*QoZ+e6GCrQPf_5s=C zW)=OzZi$k{g1q!CXsi}By94!_6uTiE)nC>S73;FhG}%-{U?DowB0e%Vwxx(RsW(h&Miv55XY4 z)3=a9`jTpxx~cko*hkHP*GbdBh9l5rRK@t`!F=I%AZARnwizAAS_3I)T$q5HBNZ%Q zFr6~-OqbWyn$1c?+L)W7zlh(s9OxXk$4MXsZUkHiYS5DK+<|O}rQpCe8^71Yvf^&D zKAMh1b)-SL-Qd5>JNIHlCpa`DSFduDeD(Dwf8cQY*cOf-$Vcl~<-E)x)BY64c!P4RxoUB^!<(W5*y{ic3oet%l4(FX- zZMuH=YU^t2J>yZNI93IX){xN}Hd;}Axkn?0tVqtm^^{=t$!(1)PyZpMf=DbX)(coi zWxcST95kLs-_8BxuWUPvXEyTw;^3bi44Lb~=DMKq4Eo9zOfz1ay_0tQcG~e!nmL?i z4(bwqemhLRxn-PWKwa7FkvbeoeN4gM}{ z=mE(0TkP&y*o8iWOJHdFS}5t}!2ti+Rx368(apI;XVje7rud{tt$|JS;}UWYs9Am? z)LcWgU)=+YmIako&A_?&Y^R~cct^*$(OlSHb_LwZsH0t7&O5y<%->+7459l zpzC?}9$)e_Meq9)BnFN1dFXtK=-41SP8A)eiH`YeCqW`D2o&`@Y&71hgY$|pvGbk@ zCO#9Azvp4!M_(YjL7eX;MlUqQrqB+bopIvd0S(re%$leUW%-?Un=7ip_Q+@wVj3{h zVh+PvHUkG?+dNRQTwwh=$?uYfcXUIOD7MC`(d;+vb=uLyuAX+d=Aub#F|ut(Kua{U zKspEsMDWM+d%z%8)yyiU7S+Z;;1lLFN&fWiK9UO5WOITC#>z|P#bX6KuT zLk3brx?X^t%S~;)fn-a0O-UL0X9?&y1uw7d?748{>0C)2!^9>4l$6)PN+NaqewJRX#sM)`|4EIM$=c-f+CJ zF{(m6AJxpy&7n7heN=ckg}4ggt=^GTHAn8C#=00AwhA-QLtKJ)@VY?2nW;J9)aQei zmSAdYD77`5+IpoqqSoJ0=iOH4MRe(r3~La7BdMn%8M*gTGqzJGP5y_JR-=6g$u>^X zC`}4?O-3-UXtU(s`LFi&I_sg!|MK^x}U%ETKb0j zy}|c;2k_a!uzE149u&@|Zgjl2|NVhse0C(P9to;PM6EUk_f`I{74O+)TcD#1#ycqo zZl@dwr5p^W9K6!_Yt$&i9d-6?b@uha*GF%D<+ZPba*l>`1hi&^OpgkUDUy5Wy7Rgd z?-GhK>#ozUyVnQ9N0A`*ldrw!|GSrhDexd<(^J8MozIj~oQqVF&A05aJH5iPqL!%G z@z!f>;+At_!p81>@h3_cY+N8_?|l9|q4pAd<8h~#*uG^A+*qmrZbYVAOfn}aR%niD z2ijWNx(9^`ns~DEbF|3TJxsB-ZkRU-L`x}#Xk}bUhkp?&jl*j32dP)^zYnOAp!6Gw zLvKZ>+Mx6sp(=yYuN+cqTdPv!Y*TzQK(S4U($iw>R_f7U z>d{c@@o?(#E6wPEHAf=4oK@|$)a#S4yMsq-L%O=K4mQAx=(1LmuBBXWzS;g-`;E@m zJ43p|Vcp@N?(hfvnU2>wZgjog719-kb%jCh`w>#y6P^e3VJZQP;jQL>dZ@Pk4D~l> zo^LqxIq@lbT>Jl{K*4sg|68hREV28)S5;Izv;RMh&wtpH{r|6jyY~Nr_;xwccq~}l z8USY>l34!%=`|@vdhu3&8G8@PJt32uA?#d?t?nmf%4E-+iqDPRvuVfcOZRd;pwy0h z;#zp%!84oxUTyv$YKm?CyKR>(SR=j&c7Cu20Cb5(0F{N+mb<%I_zS7sE&R)xCvV?3 zQt|eEyp7J&vF;~RymkMd?qu1AwbNM3J~vVmS@b!3J`a!D!ICe#(XI$X$=mNye~B&l z#D33*Rp2c5*=q5=HrR5{@Ax9J*$WnXQDBBW*y?v{Em-Jx+DRkUGQV4WE3!N8$nBlS z-GNx-De8}uH&wr{(Eu+t)KJ$nf2P6!u4lISPr)|-pDD1MeuOwvUF%kiUH&d-mnRna z>_hA7wT1OBZx(G9U~@c&fE)=6hWPu$27i~>;92YYE3M$=bLJn20Un#)?@CPX4>f|p zJ;$`3y!O(@vCaLPso2^sxlMKv*x7F#+%ne%i%w%ByX1VD@zvR@U|&zef;B9=fA-|ze@wWwv29x@4_iNFyyls7rlC!M~AEtKiTl|PfUm+#^U9gZM*z>Yt zS%HShemqP*O)GmXM=NYh*N8FqX7LTMv4@Puwv0u0utohYSk$kAMg4uSqEm*vh@pt= z4f4$sR=wgKg;l5ivjgbpw`Ws_5eB=%6x|0y`h75;KPcLRfqY>P`&Q#U(by7~6KwV` zZy-nfUcoSn zId@W@4`Hjg$Q~ZT_V6fY3BSu)!XJw5-yy;N{ddIv{ZMT64sllR{|Hv^FlX-$arW*o zws(g)OLvH~bceB}JIvX*L!6EKuQ?m{L$PH$lyYDT%-RoGvvvr~+BZjUjNbgpjjwFw z91UZ8Hq1oP+(WD(`_;Oub=T>e?i=p)!8=E*LSWDSS6}DR`0b2eP-*y^`b(Z4S9^TRZ%Q$GK?~_LW-P!(IQC?A96>I;ksJ1+_|2~b6IR59=!bJAp z*D0y}*R32=5*w$3cf6{i*f^kI1y;d7!J3LK!Gcv-BeDu>iB*{M3~r&o+Dq#kT0Z`# zVOe#AvLIz(Q3>%s72r2%X6>5Ef!x>#pqOY$3{WI&=peQa=+WJ~2KGcVSK6G-K07|) z^phZe(TsVIG$+nxh`Z-aY7ZFKTsHV;E*tz4jM~!BpLz)U=`hfS0P1*fMIB4Up+D1b z=+AUI9n9c#26l1Qj~P)}qo`~aor%t5(?*o$&{-(WrL$3*x13|ni)Qviw@8A9p2tB$ zouYW5Sk&OQX%}~=$%cc3qOQOuOu`JrYANU@ATK|-kGld%kTS3?pdH?pq)bpoMGHF& zs}ht!`ijWv#Ft1=I!V~URyKyAaz!Oru|)Oqs_)La08)LE&HBKtM+hA{?e($Y$>0bF zb|DUd=rFb{h>xT!w*2f|7H^IZK`O}+NQ}5KvyNpUVeYb6H4p9vF6XOu1Z0u2? zCO#nA$LAiwt{posNdl1;#8EnLx5XxIFcx2-kO-NP`)c57VC~p?@m6}tsv34tCcmHG zNm|hXoRbb^ozLUrr%%j_{K0&}yB^z*b(lzhH;Yjw!JTDtzG%x;X z@d*%TvHgz37{wuZ4uo_E-q#%@4=NkPk&V;(*g+hXlzdEs-NHCRi*QZsU7kf0ee8&$ zSWRMGk<(!FY`p0JOqISz3M-0y0ec%U;OS7-+c`F+Bxp1GI5aV9pi<@QXGjH8u<5L zjDVpm4f*0}2%>BYM1-S$ra|DB7)mk|?j;1V<-&f(QkQQh>f%p=Po=McVr4LS>iUWO z-DCe^Hms|U=uWPxiCDGP`2CJIIyRKw>j~*jA_AOzUw4j>mzZ2s9C;D9!$eXRtgjfuiqalF%9I1N=-z*ZPNw1>=PRx#pvM3$s(sEdIyk&!+(|RUlsdT{j!eMys5q_LR{c{EqgwR{i|dDCiCSLlJa`? zdpO~@)K#UtHo5{# zfDdMUOW7S&(6 zixn;3`4a9L2qS^LQ$M;wteFc`)rysP%Z^ z2=_HG4n*Zo@EPn+yssIMLrCzOC)Z5+$v3l6+^J8NfE`c1wQW#a@00D+$tT}To>F~; zQg5k9kTAtUJV$=)b~~SZyD`n;2$jQ6teG=UzL`AD`dFHMa;=?x@~y=*JN9Hd)g@_W zw^@ZFNs(1ayx7Vcfxb#^go8+%cDoS{B@Lmw(ER>DOBri^ky%Z?A0}%K=~vZZ-Tnt@ zkC6YqZa;3}ufRt2v4Z*)Xi@TN`^PBxwjxoy#H+m0^NK?L;*hd2 z)b@0pW>rB%Nk6anuc%ivG_HXw+bCdbw2u&TlaFk8IcX&cN1y#yPh6TOB}pJ+Qkd1r z*gTBD1a-}1^Y6(<0INwv7qHQSo(3Hu5zV1zA==>sF!+2&!GZlQrU3rvsuF226wzY- zNO4o4psuB=sbvMg?`|n^l*H~^6p~<-C1uPA`h}w>N`=_uvr`Ta1oS!|s7#<(pr*3X zVMU{dyXp!w4u;4_7;ksm*s^ZDZofXXc6iOYR`L4Tkg?@A58x@jm6@%;dZ<~81k(foVk;AB$Rbzy=5!wZCITswPB@`^Y_IZYJc>JzquqI%8FJ4y_Mo`v$+mJ>j2 zH4(kn)a~^+PC~ASw-#gDLbq_WpO&mgm-_i(lM}?t$cSVD0XD5IHVxRpJu+O3g{w%-YI2bkN zt)O6ucxyKB))mE)!e>x`WD?&$6w7UM5x1ot9z18BvOqUhs}pI9K(& z%%;DpN9Rrq{f$|`Js!SLeD)9ttr4c=)Q1gC7A1kNaQd`qMoqTgB-@KJ& zM#9c-U{7g^`5N3nGw+`~RofkBcE(Q#y7G6F{1fI9*%nP;imNKQ=OdF2`;_^Tr$qBN zTwzwT%-0u9VucvOg~@WomB)W}hy?CC#y|UgN|~vTlv!3=u3Og>YmMLUexo~-e``=hFWbB>eS(e2DBvGWWD_~OY1AeM|5ifZs!h-xOC9-E7qgZ$rv zrve7})vt0x<4fe}hNuM75c_m*;r>9q{P7j)VH%a0eZBb)Q&u&PCY|5SIJma3m2qrU z^MNs!cnhujHkvn^wv4A&lYU`5vsn{{auH+p&Eq$YuN~hq7Q(N6NZn};8jq|ye&l+~ zwOO-u>VS9Pq2A7czz^g99d0zs6o|>_+b8~Wpte7)v7JakM_rXzL5DyT5(T|JRza`- zjcD|%NzijVzA|+VUMm%D%PK5W)|0*vtvP!nlu7paqP~<&|T@#j7vVUQ$MRA9^&k8P5mrxBvPYKbJd|+8j zqy|RPv({>ZB&L1h=C3?L^Fh;lRby99QFbME{6>R~hz%>U_=+1!aQp@*^cA&KyR$15 z=>KKSBAQ4{1D0WS|p1X1Mt%JezvY@W)H}_Rk_IWHhtmW0I<@xykP7yvX zuK}&dLL_CmyP)19O(7w7*9e-9X!ih(0GJVRbV}GlPx#2`DuO|Qh1;wTe5$0iyO{oI zOL3@gWLW%vhwEUURe_Ipo0EtbCvfv`LjpDnHWjvWst^qcTr>gu%bIAsWSmLpWz0u5&V3#xt-%ohE z11$5xFd#B7*!Qpv1WTaNWXw#!TFtx$nRr3~=8qW#?jZ7Jv0EhWv}7&;rP=olia30? z-dCq+4&E=&Xe#fgDK&>4==7RK#e;lE@8>6L&fh?oVc zz%Bw&J&Of*IVXs_U`m@CJOXea9pbf@(19?6Kf!t;5Hnl& zcxbq&QWOUBOMXqk!9V7q)7wz?W4v3-4b6GgztLEeWOpO|9sN9}|KFp8f7#9dugX$Y z6C3}fqWW3<&!_VF(VqPOHmJn@f6{GvP>+2AQ_w>h90od7+;|Uy z5uNuQdki zJV3GTgxA2C@HY4p9t`Ua2DvZ6gh8e9c7i{k61mt&LMpJXTd^DcD9F_-n*AAaTztHz__Q4uAG6h;mLub1HuqC@Xnf38e_D=>pOlh6 zZ3o9sLermuqvI!~=}*Vu@srT>=g9F9&`yBk0>{T-W;OXnxPu*zx53$Qo8mknx6?lD z3WE5%SILU_mIB0AMvObw-ub*AB zLQ3+)UxL9fQJ-7yOh7YD`afpoHkzv?CEX5o`$kchW7$L{SrmA{&l&L!y-7P z++Xay=}|oM-rU7|6M&o&?@c_-35d4G@!qVV#IW7l9J(`%+O)_(5jw zZtk0#6z9JAD8wLkLS{7`0L82ml&fxl-c(;SX<`OG~P-p%Xt zhq7Lu@do)=+34yXrJ@*#it_>8%e7jGBo*79q^k)Q0f;}SbbL#_LZ#yyd_(0WuB-YO zsgm^(6_4GOq;;8X5y4+ndTp%NsJM>A{-eAHsnB1N=ZfQ1I@BvN`+LYq$nn0UVsFx2 zRpv{bmhHLdG+KFY0T@*N%sbg<+99>RM5lIHdlu zYqKVF>`XY$C$)Jv%(|s+D-#gWJYg$8 zJJ^*qly@SWcOqmg4zmuefOg8i4Q?NNkaqIjK}2|)dQ{^}yo;;Y$#s{hMJ+bCB*2@* z3Ep2^ch@J$Qy$;ZRbu(553>>+1z7 zu>~U{YVe=&j#DACHEgz`tdOyu^M`$Oiv$OZT$dx@9W4^rG1fhWEE<$A8d>wto;1=!NzSXf>$B{WCJS^Q|xGs74>c?uNel&Ff}Wr zKD6=j-pBg05USAbbsTQa@fiHNh4s3`FK2lN7l1#!pg$^b0f9MX8nVdLTuQ`-g9L_5a8Bc~;C*PfAJ#hDhp7?FK`(lj)?~Q~tj$Pb+@ig}+ z?!L_mnAy005p`=&-MS5)zuOd_fDiCC#V6nmjD&A4!G6KpR9u>$C_h~~D@iJS-WHh3UI>(~T*k>Q9h54E1^=IuPc*i*yzjM-QxA`4{^IVA?OIx4tv$Owq zS$TOG_bu(7 z8arkh8m!Z84Gnz_*5c9nK=~vy+|_99uAiE{G(GEVsaq_oxAys(t&R1gU46dAhQ14p z!+m`%O^fZ#`k_EmUzfGA#cCO9YOpT0_E)q|Pq>?BFANI#^^21H{w`}RDL=K?rnk4c z{1fH(fK}a13Ip^ws-ZnVPzh($TX1kxPTNil(Lh*6I4P_Kt41r@X&uqI+Vx&(iPfywFuO+3%Ye z>Z-EX8m1TK=39LO<$Cw>bgygl(rjr(O=(x{aChfY`}}lIpm?C#xi~Q7>!GcSF3)It zv%9jPt8Q{+>QeE{QdLi7fA58^*$X3W%le^?;e{brhppG?7@K9f=nCs(W3&CjSZOt5 zU0j|Wo^{nsO*c%=FIH4cSsDg)jTycyxip-)Xz^AyF5L0{z=DJbqj4D>TMseO$WSl zjk7hK^_`tvRjsYVjYHmsE`L)!JytPSW@&2~SQu`xSm?=Sn}1B-G*jcf(B87(uI;T0 zlvjGnJ3AK~m2DGEvz6uUiQ?(T+Q5a%#`?y}k(Mrp%WgK9`t(d&>tcD?fUmT^Z`L(5H|3o6R5rGD_4w!K2Ls&~>KKdff~#?$d8*d8 z+{HM$7s{)eE6OU0-Az3sUCZ?|dZyMfuHJ#+Dl3`^ zs~yFjMHf9%JZc~9o*J5{>}ndDuN|ofEG-Q;4lFKob`Et^AePnF*JrVgl)Ea%Jhg2- zgFOvh4W8-Nxz2#6e!yMRH|lryFJ17p&$@e>W*xXIJ~sB{X$88_E|Z z91U&7rNaxBx~Abvv*rG!uA%-O<^#xZrH5uC7_Avez#zm02e`7s@X*&dyg`UDeZQ=BXZ?9cl?wGtHC3 z?%A@rs^RkfmY&k3`l`uJUu$(+>p+{oYOd7j=xr!lF7In-9qp{CuNkOp8*ONu9-3{? zm)hM;ZGD4P%+Q7MuDTXSXH|9aL}f*Fi^o$wZ}Se6&bO61eIs3!!{un^9c`bnd&(we zhbu=bFV%GW$13#8W0NzjZI`+xd**9OJ#9XFc}<7K-CQx^9T|2GwGQ`pP1tMPOG9<- z6=h3{-Tjlae`$VjxYc8|70-GW9ePJy-Du;Kf2nx1xwOkR)YN~WZ*gM5(q7+TW!l=O zW(F#YXT~b(r)yj_mlmq0{k~yG_gKSxb?MT~?38oCs&BJ2E?1V-v^Uqz4lgW?4Gj1@ zrs}Gy=*gy;KKDRJPkEcm;lEVb)7RHgKh@XRuspa_-Z|PkIWsrZJk)NT(6^O&I)}$b z{LMB;^-|AtU)cpD`b^9;+k0&8irRTkOI5Ld%w09zS6*J)S{3kCFHf}A^*WlGCNKHw zis?~(L)G9|v1NI}M!RR7uC~dBfswhMmVx2ffifr4Iqw;o9IO}dgbMIK!wBFJ;&@<7|fuy{O*@@1mGSh?w0lRZTAkg4gZ2TJ zr{6Kt*4xrzpKNR|t#UVw(Se#aMAI{~OLY@VQ;wn9cDlyt?_a*qs&5*aE3YeUWJ(vI z$qh}lUf*o(bXVhaW2djb)6r5oJGod^+wSYGxnx`Dm?Y7UZ)T#udEQzv+|*=S zs-s5{iyL)J>W4@AD!dIH6Bp{*mK$froU?(&E{m_;J3-f1^mhk(+%-+3_O9u=kuGM? z)5Em(dM}hYC#s!o(8(>{8oj61-(hQPbagbfd*@2aT{dg)V84HU*?ptIAX5 zs+nz?>}YePkU(5vq$ud~$FEG)Fv)>Zo^ z0;5X{!wcO@Bkc{#oBaf(luB&=$fkPZZ4}{^45<|cj$di zXMbH|ox?R17+Nl!sTdq?pK$kfd7RxXuGX%Zy7s2w-r7Zvr)$*PQ`=l`ZLTi6;CJ}D zM@D?@!xtJx^!BQ{$%?u*rmeZuvM|w9d0`RpJ6nZ|Zs_Z3E?-=#UT$t@YCR3iTxn&; zh4!um*Ie_+;MDTG!&@`bST&FI!m6&(ic9ki#lxP!WPg==v9Z@}@reY)xVh-lo+qDu Wo_(Huo_)S(pZ_10IQaws)&l^Jk(+3sVp^d5c{_>B0W zZEYkL*_zulf2NmL^ru2~v`Csx^RdFgHRiXTUu(7?giAB^yEGQ&YDEgCL$$#))7VL+ z`f$CVps%RliKluAf<@k0Ggq;?$K-N9bGkVL_I?OPy>NC2;K`n%J*nI-PPr}^Y+|%L z1^YLKba7>n*&Jsw37@xjEk%}5Sv2M&xG5q^*AY^A5R$1`Y66VNhSy@4B3{!wp#b#j z;X*G1Tkt|c6ZADqtIB#i1cI*BG9JObAoHUw$yXJ1U_1la!EVRYd~dipv_v|;Uv}2P z4t>-yJMn@xAowh?l9^|H5A)5|Cqx_MXY(v3?cX+4Tx`<{1f9KpJCt?u)?3BvMfP}0 zz1(WZkadLkJdA5pyJ1!8+?D~xM{UgY3g$_LPWJ=LGPMqNI~ZzrIawI$VizbWdJm9Ve!YNA+ZrS#|^V2VM6nA!X}f zu&OB&AQDUAv-9$=DQl}TelcBFAKMYVhs`6MnXT}rhemY$wp-tE@X4^qUb{1i$({!! zdmW@5BUEyC3%}#(Lti@vL#eW+D-@eOGuLx7s8kt=&fSS=tI$tDSs0TU=LCK|WJNVOC=rM7h z=hc^FkVAQns2AdZfc_MO`yQMMybAXrmqe`dwmL&!_ShRq!Qyro_Ox9HIfNvxn?}7uZA6ihN;LW9f$kAn zi+6I1F4v#XS2}E?Xm>dNmFMnucK5s@jK0v7!kU;oE_DA#wtX-Kqsdf5yQ(%j6B9pv zFvXEmbO((UTBL^Ix;98OcHUqz8ME?uON+g10Ti@!hE_Eg%qfRoZjIion8uzY2O#GG z>aC9ZCvq7_Pe!GkNuMuFyCN)+f29^JOq=|T-%NpM8>FwLIA>aLfw19(D%Si%e7Epb zGv&s(WS{VM9aETcB#CWX;#D3gZ|qDB4Om-%4!fft7kB!^uarFmz3$aRitKIFGF3_> z4*6>-Xlac5iR9#(@gy_n$BIra;(fdtAA1u@ayPxEij z1c>@S&`bwbVA8;(v_YAt{;P&fOaf48A6H`lzn>|kOEIUaO@01Tn8NGTdj6Z}cXcU8 zupOZ*!{0vAyON%n2^3g9mlYClNHe9Sj;_Ti7kQZrLw+QKprdEClve2i7%QLp*oRF{HX`^uSK>gJM?KCxr>TsO`hFSxQ4^_PIXQmO zi<{#fTVdM`gu&qhtp|$XNJ6rqM%TCk6L17+W8F78b%J;=p?ws*j(oc2=b^KLQ^agv ze6#uRqbS5{FL2HM@*^~rI=z4&IIN&24UD*Wpg+j7lq~4UXIA}S!DBB1q(Zd<27tPF zG@yCFFg#KK&|zVYQ*6n`k9&~k>Dge`<~z1euafr1D_{;O`D}}5rDuEdhQNI+IX9=j z5U&%5ieW$j|7n0?2hVI?yf_W$M~8Vsr@j?+Y}K^7VcVHuL~w}RG{ok`9se@Btf8vN zl{#V4Q|5tAxva`!D)V!y6QZf)86Q+-(3ZR2X`B^Hlio1VESX)4BfZ7Qf>kXkf*>n_ z{2i-70DlAOzqHZOj?d zg_{(s0h20Jg>hHj-Z+5U1Fj_|O+&BSD>(K#ol=pwtX?;&w5md2HU&n)L2+*D*V-A|FSg7d$G*VPpmf~Q zyB5Y?9LxRj$6uKlo%yhy@`AZaYUWL-94ufdTmstBLzlLV6DY0ul%mp(Oi%(Har3OIu() zWh7v|xlCz@f7RlM?zwBWKMKt0mrc>ah^`D9KZbMJJR0WJIds}w?S%s8g9!6TbSHzG zePCBts}I11rH4H0poum4^X&}tDRcjh$BYYJZDB99$$O9(cEB=y$&i|yK7}GoMYBfz zk9OXmWcvF0Prpv1r<67CN6@=rMt}6b<)Jk&!f1 z0iU&4X_9sqc`mr`eAzB&d0-RyRUjUiePS~sIJjE+ld!@MW|YS{;lI#IAY@wN{l8jd z7KHxs2g<2weFcR(-st={T;_T0V5>y>Io|a>&;zX&m?jW&+iC{&Pq1WZ@ZdA+ac95} zIQ~cV*)>dG(Jsu$^|q*AS|qlO1S(De*I{y&V`VouA?E)5EoP02<<}vaitiJ#j6ImM zicX?RgI0KrJR8@XdVyWeF0dyuN}wmeszvl*XE877{g)Jno399}h=9F3!=~7WARm_m zGz84R4T_Ia^1{l}328~kg0Z$}FmG(1h4I-ZLQ`qf+$P-`1nr|P$^tG7Tn*LexkQ z*;$~?V9iP3iz{%8h7Qn2E6n;Z58lSwV-1|CvCpYNGAZx(pURHL?Jl5D zoN`2$&#pZLYoy?bf7&_aqDmfAC5eQOWmw%&&!cS!fBZB4jM;d^=Tjh|h#v zMqPxXpiqu4EFk@4@NiqsL0m>OqBSGt7WHSm!wqU_v#C<>Om>FykutUchApJ=s|hEd};t<(h=fh;R}b)ccB#YL{o^j z^Dbwj-F0QQR`kxfHOSG4K>wHgaGGS0|NU=J5rA2&gn<9(52}CV<_Ql13h@LA0`hOB zK|r!JWE>7SP`WQPWY?lEDeD`Abl3uresOFOVn!QBKPMm}Xy@CyQz%o=IK&Wtcl}({ zQIE0BgrE$6cs=gh3t@Qvg35ynTBZqWC$-780kJu&V3)W^QS zoZm1|MLVcd(_yep0_mmgB-g4BhojtcF1CJ8vg_Cd6aQ=vxYs;&)zPvNa^whi zb5<-!N7*s_a-OJI+-1mO*WLkVbMMUsuj>zKi5+z4sa*L%tlV}3TEF7H?ZSgiaeBU0 zGlvVPmfvTYn+tllI=4+Ug>t*-xL%#!w`0~RnPSF0fNR?Cfh$UvH|`SFHskx%M#g|a z3cy=FUUdCIh0TW?Sb*Hkj^|Sj--xOMt~> zs6PX8MO*wy4PkEb{KJRN-xr;rTdM(Sv-A7pc;RZx=k=o|1KDFm2+1uzOdRd;*&x0Z zD-#<_9Z_y~;muF&(3HgW$@`BKqY~m|G@xm=`WP(pD7}Nmu=4X=>qcoo^T28qK>^9Y zdE#NKhKSLyt#U;Jxe0QdwEHIZ&o?Ox9>)&|d9uN0ldPb1&VpOx&H}mh4nR zkOneC$Zr_ILA}+#F)LQ-z19eOp-3H{WfF_kR6(M`zlPtFFCEZpyDxlT$#l&Uv2dsB z`?=EQRKprYhr$Wy1s+b$pZN9^C$13UM9Dg)1!1T=imO+Ga2w9*pY=Qg<(sL z*FnvcV@h@xjYiR160Yo#*1zTi!}E$oe@qA@WaBNdq10AFW%XZ;Sq`Riuo|{3xx2`d zYwtOXIMUwSx@D&Dk}W1b#q6qDuihb~U}w)H<$t+K1Z$<%9nIGy%d{Rv-Sgx~Rc8MJ z>pXvjwG(U@V(!E7$uBrr3RtVwww+VSIRG5y#@QqD058KLSfv$XvD0Ppe*wMJEhw^0-h}@b@ zI>359s$as4AM%rWx{gLV@5nP*ge~QaT@q*Ro+sUS-}d$m%SVXzqp-!Xn|u2Wf@}KM zavAo+L~S-Lf9Hu0tmCKruk)nL25#{y04mIVp9VJc$6DyNyUnhWUE{pT)^iHDehWrAFVWpc z(ul3ZY$}(fPUfHHAd)4O+SDCfr8|mQ%B*H{=uJhGj*4GKU>1rv&ubFK9_8ih?-3&6 ztM0h5TTqmd+oh`ZVw;M0o`o!ij5itW=D;h}gOSrH$H=%%!A*%({8eUH2H5Q05V3Du zWEJY6t0}YPri&ctf1G53C5&v^O~cQr`x4lX(P~?g5PdESf_PPYo5ILUk77}_$hMCJ zXKBx?D+Mpy$diHz+HB*gIeu-Y8ggiTRel@{G39$2uGf?(q2tWTCKPG#>-_wjtEBMK zze_*jo=>VIy@hhM!Cg;g2RLXcvmK{1#z53|Io009l99b;M@+ABiQEtu-N8pa3XIzy zwmb9^atNcLaLt=F(z~3)RPKv!`r3aI&V)4?lX>k z+k7RVN^3WRkI=|%^sCCDtMM2LEZl}q{+z&5p56{&suJ?Fau5$l&I&$}#U^c^OJ~m# zht}G!W$oYx&1R_bK|l7Jr~rWkuaer!Mw5W})n)Xtxp>P(0MK+x5V0JZNc}76-yxt| zQ<-zydP_A6ME&{;hB=)87qE>O4Sg&dRGwA%Q#rNVo|HK&YYTJn>tW+MyhB16imdW&Bsw*+|Gj-hNFERv{D0q z1HDI*m;jq&%T9gYk6d%Q<~$37xw#k_{uQC(k1$Ac2N-iYB_G$rrvG#@QzGD=LJ~2o`P*3+3d1AnH7NXz+;hEdGGDM zWm8n3hgxOG?R9E)1l%k4Mdj160-A{0z}&f(+w>J4=S95ixZU>~<=S#S{Y>ofit6yN zW)Tl2dr@MyBhY5L*=_<;R!UvM1p65ILdwY*`%b&-W{Ho|##X8p_$ zcSB3ZQoYQt7{l!jBX6hs0x)I19UQUBzaN(K)u7lrFY7JUJ%3zJwh+ieFZ6(A{C_3? zWMf11Cv&DbKI|~!j&J#cQ?BRH_GVaS&SIea{;asy+M*VtIy@E*M;O_OQdUmb6K4v3Jx<#B0 z5&5KF7jD46{a7q5`pxoDlx2@7RsZL76s<=>TNNtb4=tiKG*)7@WQRd#Zd{hv*QF5R z6Ts2$t_-`K0l@}UEXWQgD;zZ~9MHr=uS4gLJN`UG!GJoBPLN~!R=J{>m7Lm398hmB|NuO%D{Y82k1bo+!k-vrrH`i!xsn6zx_|w+w(*9CdptytidMH*VG4Zi>d8FAqFgFR5xZxT#4pKtGfsK^qllC>D4FuiD z-hA64#OW>()vUG|PL^BTHF~Ip3lB43Z=J7(p4(s2?&o8{AtE5CQ)w;Dv?O!qvkQpB zhSF~cZll|A1N(u-T)0Ue48Vx~)p6uW|C*d|I(zRC?`GMh*^pJuli{ev;UBfAE&T@u zb0r#*k+C{QFb-tiFjF)9ESI9KRRFY!PsaqmwhC0vUzt>oB4wr46U zL*bu94Rz+)Wk%CqgRA37u3yP-#DN^OO=vp)G)+c8wWH4Lht4JJQ$Y6P^#K3xwbrTh z1GAfH@Tp*@!gA)ZpL|&jn5k8gbkwn7!5p294}?70LjyvZx5i0M&3u{4l%>-VUA$h8 z*`J|-^Tne=ZQxWcGRac#jiS0V6v~}aQ})MJ)2&U;1#rYGnT+@R%F4@aD#Ei`C<%tV z#Xalet}XU@P(%XZ*ge>2`!$DzmH_3$x&Qvi$LgOCWso1!n z*yTTJjm2BEM?O&V_coDE1Doj&wP9Lhff-4?#QErwp^u`-S&9#HV0R!GP?v}>)pRfk zJlF^ts{a0MB2y~Tc)%GA2Ui~tS6=^81Z-NI1|`WZ;@lv|PXLHh)OztaukDi7TuA}n zibi2;EkE%Z4I8zKsdE=5lnlIa9;NvjE`&$e;u8L-3ASGWSmIU%-r)eVH?1Ih2|#{uK%^_?3oIu~tW=jATJ5AG2)N47L`0hG*SkxY|9^ zY*(0UM=vfp>XR`>Uh(`7C0t)p*AR_%yNlT3Zvm}ILb)!CfTq6KAxn?$@HTn=tE7Xt z;piY8Lm3lDp>{*$V`N2~mV>EeQakIih&5i%^vp>@1pqXUP0e$LP8J%(+nBQg4%ga- ziW#fDun%kObARExe`-}5k@z-vg9U8J+g^`UD$#dp|85B^yFt-5)$Pc(iX?4aThrzsjABAv^J1hU z%bwz|OaR$wXwMjx>2iUQn$nB4;z33~r^|P?i-=T?)suxpET5i`V~r6(S2F1gy}q=u zgxxR6if(08v9n!mXDGHG_HzzXL}@b0D)16R9*^x+J9*P+kSng6GaUy8q2rBSkh}*J zHs-+x{*o~Ucv)&?yrIOTwJ9PX3ddDrPCgg9R)Ca|5xi2qatu#(C*7BgpQpm<;RoO& z2yL(fV&4EGA5N8hu@MXKwap!|Ex;D5p%VlhWfK!~*Rrhiq5w8$M`uyOxPx^I|j zzY&k{`*dB9;)SbWZz^G1H&U1e__*Qrk|xVsKp>6WQg)O4JW$;y%Lt{i?G*IZ?h3<# z;RGFeB3ZA^vtSAi*Sa@Fl_3q{e6kg|+@x>qd+;#%6>?b6>9!kw??0{tyYJu1J$6Z* zSAnKAg4su#>e0}q+y1@gCsA@Dbk8fvlLCwthhK!3>SufB;zl(xM}{JR`Hh-~a~IVQ z&?0yvoo*%wYftt{jZXW+EZDxlNnjfKh4K)uEbc`rE}qSrNfhEgixAQT=6eRZ^flYo zsIk-Qnvvvo#VJHUv_meP)=F>v3TJX$WW}mB8~sq*$mht`HG}4Ba~SB@d_N4AcmP2D z^@S2mx{}1!!dFDy@J&vmV1nRS4TH) zQc6B9X8Ni|K40}4D?a2}#`JFIt6kF-##PV{%wAIHhQx6e*fDT(lic6a5A)a$(ct@I z{>h6g;%L5=$JLc#icx`MeK#nIzc@_UmP2oNbk{u=)sk>1btk!D{kS{uqEP723US z;lQ}PonxnK-(&Ek(F6IhzW57rGj;N}U>8srlrgC{Wi?h)L&r!ob)6=#d|`kD+boz8 zRVe-n+OS2r56hy0X@)q*%MPH|FBR(a>d$iBy%@~C!xgo05andO*(&H}IV~a@wn(he z%J6nNw8ovG%B2kPQpcgN#SW0i`N;mEo8xZsy)?@wD9BqivSs&7T>pgfBXVv>>=w>W z-sD5z4%zs{OBk%5gv97#!fen3Ut`_$4*NUba1*6!C6W+vU(DLw?B^_FZ08_^Hqn=$ zxuvwKz}!%G>XdMd{0C;J=J>v+VKsHRG7}y33!_K;0RN;-tYEKVJIDZhUez6mz>Wy(qYx~xL2OBD^3gl!K zA;@OO#s?+tHMg6YZZbb*L3I5Woib0C0${SfBIy9r&C zNzWwO)h{LZaXZKcu;2}(Ec&HQG+hd>ti`PUuo?A3au6(<(q|OaLUOCJNDnLpQ{C}M zE-(3ro1%V)CX%GHm-XFDiBs#(#A+_}g19VA=a{C_snjkZ-73$_@Q~>Ibvq_$ezSUM zDYU$`{163Ak}WU&R-wn~x~pM5iss}y?0OHK+~Dw$B~R8D04Zk$z+cU|LE#&{`yES( zwF>cM(Iv9z3|CB&wsCFChO|?JJ|%c$Dp^I4#~>%*ZPnP6Y`(UqptFkprx}!+X6h{W z4@aVVNkR%AlHhAbP3O4cv1$AVI>HXHCh!D&*pi8GzW4=}s(Z|-Zmbi*xqxyitlnCW zanks~5zwLq_%MR7<&5qAw&7FW?O28k?UR9>zPq`;S+*FJ8&2ac)kb$ZhU^36%kR#V zr*c437`8<|hM^Gm|8mV>iketXN|{sSL&l}^&^@>#l6;ZaaKLx4AX*_J{61Z6V{di& zh;8-tc!|YgiWYgxh!1c3Z>C&`5k= z=YDu}dT<7$6%HH@>g`%r*jyqIAPTZA*j6?0b5t_j)O;SFZwog}eNN?ET+LoC@7|7i zx;{OB+WyL^P=f1W*k+uDBV-&?tLbg{Ow(7j2H<5u_PC#V9Lk12wzuW>+t_IN2h2P& zwiAo0JWsY>wXpp)wKr&US57FLa5UBZO?6c5 zichfRmRjK;Xg6}n;NZC@B)mDA%Yu%3wkdcy5x#cuf?ZlamiEU9F?#jZE4S6hORY9b z{};jfoPMHMar7M;FW&_cd(sAeKr6x<8iwfZ-*BvCn(=`*6xjkYS!tKT7~bZTP(b`U z+45q6)`$)xZ`@x$<<+$Mo8&4=r^{hDVbw*OnK@77ZBByW;HRY7uF}&~BP2I?SZTX< zpR=Sxwfo)5LosOjU8<6PtINuCijf}*i{a#~O!zF(6)|*vy}2ICa;RG&t%QB+yI76s ztFYL4HX70}R*?b9$a}CR&@U4T!WFW=ocZ`)x^YfWV@9}{%H_untwXOdpd=9}I1gnF zSqFq*E|!#)J63}6=}mZly|gQ#wzzWIFgA#d`Hjz01k3U^_KXPLu(K_{WvgLO&_z=nkWsigeM$>Zg9c{5t9HM4!E5zp2re4eo*02Mh@c2Cn$Bm z$JSG+x(wV7-8D4ahzR$dgX_tpm_@dDk3P=F_%a$BD%h;m3xuW>K9BFmM>{fOzg~u( zWoXP~=%J$bYsw!fc)7`0{%{^ZjLD#q-oQg%S{E-86y1ym!ucXL;gS3%5ha9=YcDs^ z3R?o0&W0Ibi_c~;pQC4Ocm-nI!AN(SL7H_S1dD{q5n-E~$TdU^l~AB@Buwbkh<5p|;V6K(^1yj0OeqF=Jo zMx|+zd@RF1^&Tu6Vj#i_V{}7M5YoG{MLl+pwd?4`l;vAZAA&}9SYoPz>sJdkYZ4<6 zkpARxZ0Pie@_MQ#>Mn5zxT4U7-7xZ{71kE7D_uOEJKt)5XgUIj|Lr@V%Ce`k&KNizpPH=Kd0N$0G3`H?1^t^I z%@z(H$WLDWG1MVnT%2`%=&&VVXKU&6-aJ+0Xoqv4^G%?V>B9Yt#g#&R~Mia`u1{@k0px~zaGnf$jDS6Tat!wxp!FSvL^%hKI%QY zysV*tk{ImC1GmxH40jH)$rr{_cwe$0dTthWP7mbu(coU$)M!o2^v$R2ppiA(R^xK7 zrArxXvHn|Nb$U>?2X4@hY?6>@(}9xu$3LiGjFcoza~2EH*Ykqz5AzborIf^$S0PMD z7`dTKif;pu2H|ctMtL}uv`GWNW~tm99+}Kh<8p7c$!^BXt zy|mch*og`&$Q^O&2zK+CeQu5%v&kq+!k_2*9XUUJ_1g_tXevx8CDPTk0SN&6--Z?= zptI2Up8+F+1OkHhUq5=Baftu@qxT=P9VlbUhDVB47N2OsdHUVWn~-1STQ!gzzFK(g z|MJQE223|41MCG|FswMz*}@+v{-isEqW{Tx>pt?Hci@%K8Q=Q}stPVUjT$3iSTCQt z(#X(|D`AzZj0P%!e(~JPL%}J&h!6Q1pWZToXsgBP91JBfz~S2@jn*s3q$xjrG(cD_ zzhU#&L%XayoL2{NO%oXh4sO}3`R7h`)-f5NbkTCT0IpH6R<|BPqp6<>0f? z42^qEQ%w+zw%}*O;_JCo(~0W56i}ruASiDfxTFw zRrX;m06_nHR|>@=t!jnG5UjUr>ly-HOV8$Tg?%`deu^@A1(d(_n%N1B?IucG!94L^ zFb_~={dVx;d`K?jzZL4OW#jGL$ol5{OeRLkn9 z50ETChW(!N8wf^WR1iY!B50DrCiDA(aDFp==YXr^Zku$(oq$v={Y4eA;5}kSYbmHp ztu#=x3D#T@R>I@bnl?jKm2IR7djqRRlFSx~>T0mJcDMy~gP8;ov9eP95{8Wzx9}0A z@WYn2Qg`j1fbyovLRZ>>#=4VhBP+|R5YRpx#(I68)Kk6~;R*ToGK=473J0t|`R{lW zQMIF*kmUVR2q0@jX#P};BW=7hu>aFwI=*^3R|&US6&v71pxf@y$7j=b!RQC#E-_c= zw46RvDWSfwTRB^vTZ{n(zfYp>c+%6!yaPhVR&>GW+h>zwM>Lun+DL=@I5OMS1|VYu z5Gdnrgbuf=j5l1^(K%unoX6MXw_GMia+S56aS_eufK7kvgnCgl-;#BDcjA7xk%pJ^ zm5Rwu_AvF4L3)t$bUcMY%=h!yIZ zLR5g^$3CQ;6p!J(utszUOUhEu_a;hpSG)?*3C5Akp|Ow3V~FSnW{>%;Mw4$vu{Rg` z+_a)bL|<2WO*DgLQ0T=iJ7HY3uU?&GOIXGKP{`{DXNR-FB6=%UbQ^#=0Bq%EzFQ0} z*iaPlnL0k5r`WuLGVdR;7~r$TVajMnlNv8z^w& z<672K*LgH%GAwVl7@c;nHnDTQ(jJy8w_f`kZt)qI0!5%RRKCMzgycu?&@?uJt1Y}u zkhc7Pg>dZ@N>o67Y`CGh0iGNEB*~{kfJ~$odqM}}PamG$S!31$Ri|c)MTxCj*^I?I zR1G=I@J#1vPRTNGQ+^pkNA9WcXWRGg!?#yu`+7VxN^u`gu(Vtn{9kYf+0dK$;uS3J zC!n}PuQ?|vw_f<;CZTD!qx^HdkJI2{pwac@oDawP z*Z;`Z_y?Wx^MCSn8n|jj2l#K<`u~Y9EvawV|BEmAt!T(5Vd0@1dSZ1k+;bNBzEUTns|!)F zaJEL=*yXKv^Y>z!_R9oTSQ7$>s^-|FlhLse&I!)DfOWFGCG6_bWQOfhWtkqRvp=K} z8Q)McY2&CFitG%yH0EmU!APw@0&!lK;2A%8${WjH#8EOS1y=X-gbZe5XO;~2hi=#_ zlxo^3D?hHDhovMNcUUas$l7&Fr^2MC;3??3rSh%N#ROES>%g|g2}c2cVK$}m&Upf> zSUEsbG{BPLvIvwV9cmZ>`jy4}d?5pJbUp}S!`!x+*9e+MZ@;w)Q!kcJ9*$J0?&6|} zuKlBEj2tSsk1!l&!>*+E4m-h8=uL43n=SVR2woso8^{(H;8Tp4v@qnDGB5o$4=Mx_ zxFA@O2jaPe=D+bPrh5VRxN0^HZ*Q6fbJo8peLfQMCz2-X>}^xJEQ~Gx_6?V+W2H9K z7n<1COVO+Lg~)Kr@A`T(-^~5G!_R5<#Sri~`X5!{USs8 z<6zW~WG_+9ZX+Y-VD$XSc~bD_Vmf~hVEDXM+92x^^@Hiaq^%B6uH|A=|Hk#6JeF{c z3ZQqPZ??`nl##3L?>ryEIL_fpow=@GVruzT_lTjs6j}bpr-NT?-Un;olCv<9PEP~g zM`0~;{p6iQx07Ia3)$Lnqg?RJ)EJ0q(`cVGmb#g2-TIt$0g>sa_wm2TWLw_NOZu$R z8}hgZs=hc|UKIc?{{FC|tIZ9>t9E`t38_k!&=80Pmohh08zoC^@EHy_`U6!k#wLqj z?lB8WMLSnfcgc@v$L3D4fh29N6(PeffbI#^B550p4FgIVbAT6^^V(DZ z^D)mxa8^)AaFS;=5}G#oBew?CHhhhb`0V7}bCehNSGo!y!l72Eo9vm@Ih8NrZ2_Hc z3&po8?K&G&Ab>crZ%m4)KV*sL$ULuU9(aaDhWrbpYoK6RVPOU8a1cH+b_|S4o2tb3 zlwIpGo>%G!L7hlo8_X|7gRV8;Vn>6hI=S}s%Gs&w-Lpy7aBH3>fZ#`Gh|r>SI-RJ5 zpvKJHm|MN(-lY;0$>SZ8wPKp=d^`H>F3aL#v{Ef@x6fhTJ|1HB| z!R39@>h9?cFCU8k?cRmtd}l=v^nXKv28*RY=RYV|0OHvHPg)!(V=n+;BiL|WFBW39 zZ3Kxv^!vJY^(eTZ%$fsIr2I(4v-Iguzc)i4xii}-wO+$v?xJ7OmJ+)u+NEaLWw6l> z)I#q{?m#TX#l~jTc(h-^)e2oAn;cxkY3|n$y?Bkfa_YmkT7D*1fl;hFlE$^+b;1N8u2L1Eqv zdrY&AEh^+gdoKVWPCoOd4SM&KE1>R^w;MO`2(f(nxZ&(pykH4m?(4oTx?8cnx46mg zD-1TuO^}$ie1QV9vi!8;rxzN$8tb0zZc!a!-Q2#}l)ebdd&O z!PdwY`;ig4KKmm=Z*iUqokdev?RdHjS{hdG*Y^hgnMeRtMxW@g@8GwY;9TWca{fCn zUtjx5>y*uOUyj9Y^ZNY$Jz!n0b5)qQKv)mPaam3|^Gy72Xg8a?lcydGY%*ydUP*}? zW@{anK(9=(^W{PnEPtHbU(sRuchQJ$_>i%P}NfDbJ&D zievI@s22e0X7UM7cRW$yJGBRDmkVeQRUMcNF0}BJd-U!_BVYNI>I+4DpjzK=A;NX3 zR(W|mFbZN6?i!!cRI|X?{)QF>oAGMm(J6^V(^lp&dPwgWL%d0gKBgVZxOh((XG}S?k^)?OYBZ7za zPF#-~NeNB@C8w5gzMD6Xzi%`K@@Q5nYtYcfzjWoy+Yp%kz|l^03)Ttq_l2#0o2Q3U zc7J_A?tCkyC!`Uhb!qj&={q;z_x)(rFG;L=!$0D@za#J&;Zlyp_H2}u*kUH~VM5P- z0^{j&Sd}9znEc0!A2ZsfkQcC2lpkRK_wa5Zdubs4iy`{}X`M;_pH&Z_it{@__j0+B zDUP2>|7zLr%Y-7@*3ZHW8j5>PH1MLa&gnc(PSPXD=H1uwD45so-ZE6Au@JXqao2wJ zSAd*vz#opl;oQ%$YcXuB%0g^pp_%$1XKi9BsoxZOtVLz2N&&kE`2?g1Cyx4*)(wVm zKOZudoJGbT8+KhIXoq>>?>FfIL4C1eL887D-1qbrh4dysJhFsgnR*IY&Lv|#tw>hr z9$v<$PDP5fSvDL^nEU|d*w)zq%%5&dMStRfb^U3%Y+{iv!@j+J>HU4VJK48>e+R~` z%1-N!&jyDS*y&tslWL`mNA?)q2R3qtAV*>b?0o>bGu)1RahKK+34VNl!)>dz^O7>k5-9~1Jt#XO_T97n)mC3PvABW^n8 zE)>W@Rj z8M#z7RE)aXt7!h7*T_j*~#RxkIizkTol$hR-ftS7a4fPRg67P^Vq(9!tb!@C8j^{vu(L4r*Y7`(L_M2+od#U7_9*_R=0Pr=Qes_^>nU@8b@}FXPmT$ZJ4Q!Th`DD* z%a_|$?^OzWD`co~KYOZb!VA#*z-L%vnrF&*ou*@0v*hNH%lfjlu&mk2D? zFbCM-oY@2)$!ukDNb43b{7 zX71kipzO?TF+Pa`aYQGSW3P6^dD}J!Y^Z>4vHHd%FYH`p^V;er)07~;Jca;I*tC!{ z3E~fzbg>=rWfQhr@?dQ@sw!wJ&xwqWfof_04AEGDp}Y9&tf5l7cv18(qzw0hW8Rte zYPQ=lDMo;RV9)E{yGPR3$2W5hV=BO!w;zV^q4ZM=PB)#NB*94Jd@cQ2WwJXs-AYqr zn2Iwdo~j;T!7+M5U(^aiCA@r%KLj^^#23_2MwB#INDN1iLIJDbn3n3HxVUVpp>XlS zXZ(1UavhpZQ-@}|n&kCp!WUlV_9EiUfRvzqK(KB?_SR|uwmB=1|IhBBK_}oEJd&D% z1E+qIZDC*}a2|7Z{N84ZK`%)J6xTbppbg2>JQnHjcCWCUAdxZ@ z`pu9?)sXVhjw*!?^7hk?0Zahos{FCy6-|WcR|M^n>X8kEoj{oDIIRNrN0KuXXg|?7 z2?BI-OAa-(9gC=v0+qrAdqQr^>Kz3n^ir@Mbo%e&gxP3jX{aPSxmZ6iYiBJnr9qj? zwP#YR0bpWt5bA-)Tq1juJoJf|_d;dk0#Xq~)O@FQd$9hJDN0<)_u=CGE&=AJ?E8a~v> z@*JTF#uP~+!Pv^`eHCORhmhxO3ByrX9D&q!rm=RC1P$Cj^`ML3Z|7(#VTs>>@;x)R6_yd1eqdx`6t zp%M?i@g@PGCuIy!0n%Y;`|z{30B&9@095&=Kn%H7)iv*y$F%Qy8>Wkv3DUDTVacWB@3 zyG7fM{NyMOVnuI5{_19oM<<8$9@Ra;Acc<#e+>}K9!gQRsA zyWTLF-pT*CYEsD^xpe7|JC3SOP!iqx!)#afi_7coHoI4R%Afml3%kFAu|Lba@>5#N z4~H6m3kc!~(>}-`RJ=%G>ZzFOij@VfUB0u3^%s1cv#9s!+*>zaG#!<8k~#Zo z^4;nke-q@r3RV>#xNAFk<>a1w6`m_!J>0N*!?pVBD+12gu?2P`;n=hZ1Jp8%WLRdy9QDx8nxu>=i2RW&?8J*gFK;7lb zKEGSdysh8la;`XDsp=89oXE{m@bA_8^2dfX_g~%r^_%^N--F-lit9hEN>`QLu>I|9 z*Ux32^PafQx*;9sEK=ifg#GW2yXBn{QbKCImiyCHZY3Age1B}FpD?c>{PZ^d#DM6! z#!pE~JsY;Z_r3A>L)N{?FNJT2_C zC%?yWas83WyW0%wlX_P!3tyi(E4RWXD~{vvgq=rj`s}@T@x)Q{JFiUVzHpe3pu4%@ z$yO(;3Ox8?#K9z^)07aD+>FP-0Nj?x@V0S+*kr>_VUg0}lKkBG+~Rb-g38bk zR`A*lpm7S5uXRc>+D`u1=_n5}2fmjY*e;MmF^6w*V3!(b#RpKAj6BNbT>%CLaTK+B zlec!sLmezO`E8d9bD=f^uz@c!S)<#5c@L0%M_XxfZ?_WI9QfWs;LL{*iaE`b&vi>d z&4Jhn-}WPo?$6lCYCWnbYT;X2gc%s5P|WR`+}Wc9RjV-hT#ppjBQp{{o43n*+ zWGAcjL4pCX!~k7y!sO#Xy^#2q0Xq+6vIxa_FOny#_QU)pqlhy2$<4qZkD_>fCd1@= znM!Q%gW-V9YjkJ6M{hRL&Q6=1g6OjKYiZU?1o01JM97XSbN delta 18242 zcmZ6yQ;;QGu&rCRZQHi3F59-%g@+eB2q?r8C9>KG?fO?dB*ipUZDu=4W2AcST3Xh6%NhfWS<@(B|Bsgdb0J(@R z)QjW>XQHHvQ#7W&z69z(g3-}5*~RkZg3c%I_n*rO#*W6C_QgOfN#Odh-``7x;T$C^ z`R68Nb!u0Z(zAXRAvQ z7jNP$Rx$ZC?j}K9KHd+ZB1JaFd;Z`2w&RFS=F=nXY`moB}`^7T?@Ksn~&^8Dx!be8VTnqNLLF_vox04$i`LE#J9 zL;~pLj2lztj*UBhEcb?6LNiTe3(ZX5IblgY;3Ya&^|{Mw;t*yz$22wziZKS_>RH2M ze(BR8BGhYy$GPkw!Tdm1@S+Jg&y#6nQeHt3a|j=ug`0`wGAqZP-7vxCV}_s7#5txm z>~LvZEZ=f8?K@@WZtrBy0~7>DfPWhg77v!)rBxGSZm1SbX9nwaw6Q)V*_U54vo zJ55H;=-gNLT&00Xx}=PIo!r5S1C`snUhLZG7#L__&uRb8SS@`{0(h~d)vn-`76)tD z-f{otdd?V{fBVyRJ?VyJmNA5_XB8|fBqRBpH=BGsch zOtlF3*+g?;fNd{y%FP#cF&-8hwu8Aih}GDJbNSeH6(pX6yj*21`m}e*f^$7<+ynv? z##c&_lQWOen#H*A19-ZowG8W#SI)aSA6ng3dBJb<88TaRU*c+`kMN5VPC zaPa1UJZ8Auk+c%GrAx2!Y#-`+JwaTj;vkO)hfywL#^YNsEU5n|;(BNJ!)U|p>-yjf zjkTEe%KwYT%rZ z9CrZ;xY0Z$KqVH1|Mgpf7t3ARMGK~b6-jB1f&n+ywi9Yes``?@sX{)Xr3WiN^|~uR zuTdQ;DZ+f7vb&*_vSZ#h?CG#^H~7TQh0vslNb6f|LW$5MbMHHz zE#wCh?9>v#_pM+xnVqxAv9SZ87@gqp#^gzN4O-(sYVN8pd*C0+?1_?|xlO|g%P;rq z_Uh?1lE-id{Yb|zH|vG|J{Q|_B=+4z9Yvi{J1bL-*!B9_b()rMdLWEwJA$oIL4Q?k z{^gbGEE{p*)Tn_F${z*sXO@+AspD3>+P`+11T4X#h1rKYxA-p_Rj$$}bF;|lzRD@^C_xzVW? z%zp{(EA4&c+ciHAofU#DY5(GvEkF=WDOG!cXX&3GsjbrK0er`01Knw0#KQ+gC(BZ? zVxX8=^@jzIy9ksG(+La#>fzIZ1_8tH$pC6j3v=9(OLqRey?js4MzeO`aeW4rbm*^u zy@>J0>x2s{wv(7IpmdUhqvEs}1?W|!-Af0K z?WtNQ*Vt9l^v=vdU+aqtS{j1MgSwqXemHcyavZjzG;_n?b+w=IfwZR07;88dxY!I> zEaPkwsSNm28;R^`<#L1ZaXou>s=OlW)4&qRy+XHE*rk^yk&=`V82#Klmr z?E#uXMO{NVkDasw!>?*?YGdY&DO36g@?wpUKNad?cq?yjRKQⅆkH}m7r~8e$3I< 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 None: @@ -139,7 +139,7 @@ def __init__( self, host: str, port: int = 502, - framer: Framer = Framer.SOCKET, + framer: FramerType = FramerType.SOCKET, source_address: tuple[str, int] | None = None, **kwargs: Any, ) -> None: diff --git a/pymodbus/client/tls.py b/pymodbus/client/tls.py index 58f80e51e..d7adb9861 100644 --- a/pymodbus/client/tls.py +++ b/pymodbus/client/tls.py @@ -6,7 +6,7 @@ from typing import Any from pymodbus.client.tcp import AsyncModbusTcpClient, ModbusTcpClient -from pymodbus.framer import Framer +from pymodbus.framer import FramerType from pymodbus.logging import Log from pymodbus.transport import CommParams, CommType @@ -56,7 +56,7 @@ def __init__( self, host: str, port: int = 802, - framer: Framer = Framer.TLS, + framer: FramerType = FramerType.TLS, sslctx: ssl.SSLContext = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT), server_hostname: str | None = None, **kwargs: Any, @@ -153,7 +153,7 @@ def __init__( self, host: str, port: int = 802, - framer: Framer = Framer.TLS, + framer: FramerType = FramerType.TLS, sslctx: ssl.SSLContext = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT), server_hostname: str | None = None, **kwargs: Any, diff --git a/pymodbus/client/udp.py b/pymodbus/client/udp.py index 03115143d..48cb8bcc1 100644 --- a/pymodbus/client/udp.py +++ b/pymodbus/client/udp.py @@ -7,7 +7,7 @@ from pymodbus.client.base import ModbusBaseClient, ModbusBaseSyncClient from pymodbus.exceptions import ConnectionException -from pymodbus.framer import Framer +from pymodbus.framer import FramerType from pymodbus.logging import Log from pymodbus.transport import CommType @@ -60,7 +60,7 @@ def __init__( self, host: str, port: int = 502, - framer: Framer = Framer.SOCKET, + framer: FramerType = FramerType.SOCKET, source_address: tuple[str, int] | None = None, **kwargs: Any, ) -> None: @@ -143,7 +143,7 @@ def __init__( self, host: str, port: int = 502, - framer: Framer = Framer.SOCKET, + framer: FramerType = FramerType.SOCKET, source_address: tuple[str, int] | None = None, **kwargs: Any, ) -> None: diff --git a/pymodbus/framer/__init__.py b/pymodbus/framer/__init__.py index 20b88e05b..32c61d817 100644 --- a/pymodbus/framer/__init__.py +++ b/pymodbus/framer/__init__.py @@ -7,30 +7,21 @@ "ModbusRtuFramer", "ModbusSocketFramer", "ModbusTlsFramer", + "Framer", + "FramerType", ] - -import enum - -from pymodbus.framer.ascii_framer import ModbusAsciiFramer -from pymodbus.framer.base import ModbusFramer -from pymodbus.framer.rtu_framer import ModbusRtuFramer -from pymodbus.framer.socket_framer import ModbusSocketFramer -from pymodbus.framer.tls_framer import ModbusTlsFramer - - -class Framer(str, enum.Enum): - """These represent the different framers.""" - - ASCII = "ascii" - RTU = "rtu" - SOCKET = "socket" - TLS = "tls" +from pymodbus.framer.framer import Framer, FramerType +from pymodbus.framer.old_framer_ascii import ModbusAsciiFramer +from pymodbus.framer.old_framer_base import ModbusFramer +from pymodbus.framer.old_framer_rtu import ModbusRtuFramer +from pymodbus.framer.old_framer_socket import ModbusSocketFramer +from pymodbus.framer.old_framer_tls import ModbusTlsFramer FRAMER_NAME_TO_CLASS = { - Framer.ASCII: ModbusAsciiFramer, - Framer.RTU: ModbusRtuFramer, - Framer.SOCKET: ModbusSocketFramer, - Framer.TLS: ModbusTlsFramer, + FramerType.ASCII: ModbusAsciiFramer, + FramerType.RTU: ModbusRtuFramer, + FramerType.SOCKET: ModbusSocketFramer, + FramerType.TLS: ModbusTlsFramer, } diff --git a/pymodbus/message/ascii.py b/pymodbus/framer/ascii.py similarity index 97% rename from pymodbus/message/ascii.py rename to pymodbus/framer/ascii.py index b42460717..16485c25f 100644 --- a/pymodbus/message/ascii.py +++ b/pymodbus/framer/ascii.py @@ -8,11 +8,11 @@ from binascii import a2b_hex, b2a_hex +from pymodbus.framer.base import FramerBase from pymodbus.logging import Log -from pymodbus.message.base import MessageBase -class MessageAscii(MessageBase): +class FramerAscii(FramerBase): r"""Modbus ASCII Frame Controller. [ Start ][Address ][ Function ][ Data ][ LRC ][ End ] diff --git a/pymodbus/framer/base.py b/pymodbus/framer/base.py index 27ac9724c..9eaf1075f 100644 --- a/pymodbus/framer/base.py +++ b/pymodbus/framer/base.py @@ -1,150 +1,38 @@ -"""Framer start.""" -# pylint: disable=missing-type-doc -from __future__ import annotations - -from typing import Any - -from pymodbus.factory import ClientDecoder, ServerDecoder -from pymodbus.logging import Log - - -# Unit ID, Function Code -BYTE_ORDER = ">" -FRAME_HEADER = "BB" - -# Transaction Id, Protocol ID, Length, Unit ID, Function Code -SOCKET_FRAME_HEADER = BYTE_ORDER + "HHH" + FRAME_HEADER - -# Function Code -TLS_FRAME_HEADER = BYTE_ORDER + "B" - - -class ModbusFramer: - """Base Framer class.""" - - name = "" - - def __init__( - self, - decoder: ClientDecoder | ServerDecoder, - client, - ) -> None: - """Initialize a new instance of the framer. - - :param decoder: The decoder implementation to use - """ - self.decoder = decoder - self.client = client - self._header: dict[str, Any] = { - "lrc": "0000", - "len": 0, - "uid": 0x00, - "tid": 0, - "pid": 0, - "crc": b"\x00\x00", - } - self._buffer = b"" +"""Framer implementations. - def _validate_slave_id(self, slaves: list, single: bool) -> bool: - """Validate if the received data is valid for the client. +The implementation is responsible for encoding/decoding requests/responses. - :param slaves: list of slave id for which the transaction is valid - :param single: Set to true to treat this as a single context - :return: - """ - if single: - return True - if 0 in slaves or 0xFF in slaves: - # Handle Modbus TCP slave identifier (0x00 0r 0xFF) - # in asynchronous requests - return True - return self._header["uid"] in slaves - - def sendPacket(self, message): - """Send packets on the bus. - - With 3.5char delay between frames - :param message: Message to be sent over the bus - :return: - """ - return self.client.send(message) - - def recvPacket(self, size): - """Receive packet from the bus. - - With specified len - :param size: Number of bytes to read - :return: - """ - return self.client.recv(size) - - def resetFrame(self): - """Reset the entire message frame. +According to the selected type of modbus frame a prefix/suffix is added/removed +""" +from __future__ import annotations - This allows us to skip ovver errors that may be in the stream. - It is hard to know if we are simply out of sync or if there is - an error in the stream as we have no way to check the start or - end of the message (python just doesn't have the resolution to - check for millisecond delays). - """ - Log.debug( - "Resetting frame - Current Frame in buffer - {}", self._buffer, ":hex" - ) - self._buffer = b"" - self._header = { - "lrc": "0000", - "crc": b"\x00\x00", - "len": 0, - "uid": 0x00, - "pid": 0, - "tid": 0, - } +from abc import abstractmethod - def populateResult(self, result): - """Populate the modbus result header. - The serial packets do not have any header information - that is copied. +class FramerBase: + """Intern base.""" - :param result: The response packet - """ - result.slave_id = self._header.get("uid", 0) - result.transaction_id = self._header.get("tid", 0) - result.protocol_id = self._header.get("pid", 0) + EMPTY = b'' - def processIncomingPacket(self, data, callback, slave, **kwargs): - """Process new packet pattern. + def __init__(self) -> None: + """Initialize a message instance.""" - This takes in a new request packet, adds it to the current - packet stream, and performs framing on it. That is, checks - for complete messages, and once found, will process all that - exist. This handles the case when we read N + 1 or 1 // N - messages at a time instead of 1. - The processed and decoded messages are pushed to the callback - function to process and send. + @abstractmethod + def decode(self, data: bytes) -> tuple[int, int, int, bytes]: + """Decode message. - :param data: The new packet data - :param callback: The function to send results to - :param slave: Process if slave id matches, ignore otherwise (could be a - list of slave ids (server) or single slave id(client/server)) - :param kwargs: - :raises ModbusIOException: + return: + used_len (int) or 0 to read more + transaction_id (int) or 0 + device_id (int) or 0 + modbus request/response (bytes) """ - Log.debug("Processing: {}", data, ":hex") - self._buffer += data - if not isinstance(slave, (list, tuple)): - slave = [slave] - single = kwargs.pop("single", False) - self.frameProcessIncomingPacket(single, callback, slave, **kwargs) - - def frameProcessIncomingPacket( - self, _single, _callback, _slave, _tid=None, **kwargs - ) -> None: - """Process new packet pattern.""" - def buildPacket(self, message) -> bytes: # type:ignore[empty-body] - """Create a ready to send modbus packet. + @abstractmethod + def encode(self, pdu: bytes, device_id: int, tid: int) -> bytes: + """Decode message. - :param message: The populated request/response to send + return: + modbus message (bytes) """ diff --git a/pymodbus/message/message.py b/pymodbus/framer/framer.py similarity index 72% rename from pymodbus/message/message.py rename to pymodbus/framer/framer.py index 20427bd23..93b6d4324 100644 --- a/pymodbus/message/message.py +++ b/pymodbus/framer/framer.py @@ -1,24 +1,27 @@ -"""ModbusMessage layer. +"""Framing layer. -The message layer is responsible for encoding/decoding requests/responses. +The framer layer is responsible for isolating/generating the request/request from +the frame (prefix - postfix) According to the selected type of modbus frame a prefix/suffix is added/removed + +This layer is also responsible for discarding invalid frames and frames for other slaves. """ from __future__ import annotations from abc import abstractmethod from enum import Enum -from pymodbus.message.ascii import MessageAscii -from pymodbus.message.base import MessageBase -from pymodbus.message.raw import MessageRaw -from pymodbus.message.rtu import MessageRTU -from pymodbus.message.socket import MessageSocket -from pymodbus.message.tls import MessageTLS +from pymodbus.framer.ascii import FramerAscii +from pymodbus.framer.base import FramerBase +from pymodbus.framer.raw import FramerRaw +from pymodbus.framer.rtu import FramerRTU +from pymodbus.framer.socket import FramerSocket +from pymodbus.framer.tls import FramerTLS from pymodbus.transport.transport import CommParams, ModbusProtocol -class MessageType(str, Enum): +class FramerType(str, Enum): """Type of Modbus frame.""" RAW = "raw" # only used for testing @@ -28,15 +31,13 @@ class MessageType(str, Enum): TLS = "tls" -class Message(ModbusProtocol): - """Message layer extending transport layer. - - extends the ModbusProtocol to handle receiving and sending of complete modbus messsagees. +class Framer(ModbusProtocol): + """Framer layer extending transport layer. - Message is the prefix / suffix around the response/request + extends the ModbusProtocol to handle receiving and sending of complete modbus PDU. When receiving: - - Secures full valid Modbus message is received (across multiple callbacks) + - Secures full valid Modbus PDU is received (across multiple callbacks) - Validates and removes Modbus prefix/suffix (CRC for serial, MBAP for others) - Callback with pure request/response - Skips invalid messagees @@ -51,14 +52,14 @@ class Message(ModbusProtocol): """ def __init__(self, - message_type: MessageType, + framer_type: FramerType, params: CommParams, is_server: bool, device_ids: list[int], ): - """Initialize a message instance. + """Initialize a framer instance. - :param message_type: Modbus message type + :param framer_type: Modbus message type :param params: parameter dataclass :param is_server: true if object act as a server (listen/connect) :param device_ids: list of device id to accept, 0 in list means broadcast. @@ -66,13 +67,13 @@ def __init__(self, super().__init__(params, is_server) self.device_ids = device_ids self.broadcast: bool = (0 in device_ids) - self.msg_handle: MessageBase = { - MessageType.RAW: MessageRaw(), - MessageType.ASCII: MessageAscii(), - MessageType.RTU: MessageRTU(), - MessageType.SOCKET: MessageSocket(), - MessageType.TLS: MessageTLS(), - }[message_type] + self.msg_handle: FramerBase = { + FramerType.RAW: FramerRaw(), + FramerType.ASCII: FramerAscii(), + FramerType.RTU: FramerRTU(), + FramerType.SOCKET: FramerSocket(), + FramerType.TLS: FramerTLS(), + }[framer_type] def validate_device_id(self, dev_id: int) -> bool: diff --git a/pymodbus/framer/ascii_framer.py b/pymodbus/framer/old_framer_ascii.py similarity index 93% rename from pymodbus/framer/ascii_framer.py rename to pymodbus/framer/old_framer_ascii.py index 4d2fbc68a..534c4fdc2 100644 --- a/pymodbus/framer/ascii_framer.py +++ b/pymodbus/framer/old_framer_ascii.py @@ -2,16 +2,17 @@ # pylint: disable=missing-type-doc from pymodbus.exceptions import ModbusIOException -from pymodbus.framer.base import BYTE_ORDER, FRAME_HEADER, ModbusFramer +from pymodbus.framer.old_framer_base import BYTE_ORDER, FRAME_HEADER, ModbusFramer from pymodbus.logging import Log -from pymodbus.message.ascii import MessageAscii + +from .ascii import FramerAscii ASCII_FRAME_HEADER = BYTE_ORDER + FRAME_HEADER # --------------------------------------------------------------------------- # -# Modbus ASCII Message +# Modbus ASCII olf framer # --------------------------------------------------------------------------- # class ModbusAsciiFramer(ModbusFramer): r"""Modbus ASCII Frame Controller. @@ -39,7 +40,7 @@ def __init__(self, decoder, client=None): self._hsize = 0x02 self._start = b":" self._end = b"\r\n" - self.message_handler = MessageAscii() + self.message_handler = FramerAscii() def decode_data(self, data): """Decode data.""" diff --git a/pymodbus/framer/old_framer_base.py b/pymodbus/framer/old_framer_base.py new file mode 100644 index 000000000..27ac9724c --- /dev/null +++ b/pymodbus/framer/old_framer_base.py @@ -0,0 +1,150 @@ +"""Framer start.""" +# pylint: disable=missing-type-doc +from __future__ import annotations + +from typing import Any + +from pymodbus.factory import ClientDecoder, ServerDecoder +from pymodbus.logging import Log + + +# Unit ID, Function Code +BYTE_ORDER = ">" +FRAME_HEADER = "BB" + +# Transaction Id, Protocol ID, Length, Unit ID, Function Code +SOCKET_FRAME_HEADER = BYTE_ORDER + "HHH" + FRAME_HEADER + +# Function Code +TLS_FRAME_HEADER = BYTE_ORDER + "B" + + +class ModbusFramer: + """Base Framer class.""" + + name = "" + + def __init__( + self, + decoder: ClientDecoder | ServerDecoder, + client, + ) -> None: + """Initialize a new instance of the framer. + + :param decoder: The decoder implementation to use + """ + self.decoder = decoder + self.client = client + self._header: dict[str, Any] = { + "lrc": "0000", + "len": 0, + "uid": 0x00, + "tid": 0, + "pid": 0, + "crc": b"\x00\x00", + } + self._buffer = b"" + + def _validate_slave_id(self, slaves: list, single: bool) -> bool: + """Validate if the received data is valid for the client. + + :param slaves: list of slave id for which the transaction is valid + :param single: Set to true to treat this as a single context + :return: + """ + if single: + return True + if 0 in slaves or 0xFF in slaves: + # Handle Modbus TCP slave identifier (0x00 0r 0xFF) + # in asynchronous requests + return True + return self._header["uid"] in slaves + + def sendPacket(self, message): + """Send packets on the bus. + + With 3.5char delay between frames + :param message: Message to be sent over the bus + :return: + """ + return self.client.send(message) + + def recvPacket(self, size): + """Receive packet from the bus. + + With specified len + :param size: Number of bytes to read + :return: + """ + return self.client.recv(size) + + def resetFrame(self): + """Reset the entire message frame. + + This allows us to skip ovver errors that may be in the stream. + It is hard to know if we are simply out of sync or if there is + an error in the stream as we have no way to check the start or + end of the message (python just doesn't have the resolution to + check for millisecond delays). + """ + Log.debug( + "Resetting frame - Current Frame in buffer - {}", self._buffer, ":hex" + ) + self._buffer = b"" + self._header = { + "lrc": "0000", + "crc": b"\x00\x00", + "len": 0, + "uid": 0x00, + "pid": 0, + "tid": 0, + } + + def populateResult(self, result): + """Populate the modbus result header. + + The serial packets do not have any header information + that is copied. + + :param result: The response packet + """ + result.slave_id = self._header.get("uid", 0) + result.transaction_id = self._header.get("tid", 0) + result.protocol_id = self._header.get("pid", 0) + + def processIncomingPacket(self, data, callback, slave, **kwargs): + """Process new packet pattern. + + This takes in a new request packet, adds it to the current + packet stream, and performs framing on it. That is, checks + for complete messages, and once found, will process all that + exist. This handles the case when we read N + 1 or 1 // N + messages at a time instead of 1. + + The processed and decoded messages are pushed to the callback + function to process and send. + + :param data: The new packet data + :param callback: The function to send results to + :param slave: Process if slave id matches, ignore otherwise (could be a + list of slave ids (server) or single slave id(client/server)) + :param kwargs: + :raises ModbusIOException: + """ + Log.debug("Processing: {}", data, ":hex") + self._buffer += data + if not isinstance(slave, (list, tuple)): + slave = [slave] + single = kwargs.pop("single", False) + self.frameProcessIncomingPacket(single, callback, slave, **kwargs) + + def frameProcessIncomingPacket( + self, _single, _callback, _slave, _tid=None, **kwargs + ) -> None: + """Process new packet pattern.""" + + def buildPacket(self, message) -> bytes: # type:ignore[empty-body] + """Create a ready to send modbus packet. + + :param message: The populated request/response to send + """ diff --git a/pymodbus/framer/rtu_framer.py b/pymodbus/framer/old_framer_rtu.py similarity index 97% rename from pymodbus/framer/rtu_framer.py rename to pymodbus/framer/old_framer_rtu.py index 6455a28f8..0f43e2501 100644 --- a/pymodbus/framer/rtu_framer.py +++ b/pymodbus/framer/old_framer_rtu.py @@ -4,9 +4,9 @@ import time from pymodbus.exceptions import ModbusIOException -from pymodbus.framer.base import BYTE_ORDER, FRAME_HEADER, ModbusFramer +from pymodbus.framer.old_framer_base import BYTE_ORDER, FRAME_HEADER, ModbusFramer +from pymodbus.framer.rtu import FramerRTU from pymodbus.logging import Log -from pymodbus.message.rtu import MessageRTU from pymodbus.utilities import ModbusTransactionState @@ -14,7 +14,7 @@ # --------------------------------------------------------------------------- # -# Modbus RTU Message +# Modbus RTU old Framer # --------------------------------------------------------------------------- # class ModbusRtuFramer(ModbusFramer): """Modbus RTU Frame controller. @@ -61,7 +61,7 @@ def __init__(self, decoder, client=None): self._end = b"\x0d\x0a" self._min_frame_size = 4 self.function_codes = decoder.lookup.keys() if decoder else {} - self.message_handler = MessageRTU() + self.message_handler = FramerRTU() def decode_data(self, data): """Decode data.""" @@ -131,7 +131,7 @@ def check_frame(self): data = self._buffer[: frame_size - 2] crc = self._header["crc"] crc_val = (int(crc[0]) << 8) + int(crc[1]) - return MessageRTU.check_CRC(data, crc_val) + return FramerRTU.check_CRC(data, crc_val) except (IndexError, KeyError, struct.error): return False diff --git a/pymodbus/framer/socket_framer.py b/pymodbus/framer/old_framer_socket.py similarity index 94% rename from pymodbus/framer/socket_framer.py rename to pymodbus/framer/old_framer_socket.py index 582aa283a..e7fde0e16 100644 --- a/pymodbus/framer/socket_framer.py +++ b/pymodbus/framer/old_framer_socket.py @@ -5,13 +5,13 @@ from pymodbus.exceptions import ( ModbusIOException, ) -from pymodbus.framer.base import SOCKET_FRAME_HEADER, ModbusFramer +from pymodbus.framer.old_framer_base import SOCKET_FRAME_HEADER, ModbusFramer +from pymodbus.framer.socket import FramerSocket from pymodbus.logging import Log -from pymodbus.message.socket import MessageSocket # --------------------------------------------------------------------------- # -# Modbus TCP Message +# Modbus TCP old framer # --------------------------------------------------------------------------- # @@ -43,7 +43,7 @@ def __init__(self, decoder, client=None): """ super().__init__(decoder, client) self._hsize = 0x07 - self.message_handler = MessageSocket() + self.message_handler = FramerSocket() def decode_data(self, data): """Decode data.""" diff --git a/pymodbus/framer/tls_framer.py b/pymodbus/framer/old_framer_tls.py similarity index 92% rename from pymodbus/framer/tls_framer.py rename to pymodbus/framer/old_framer_tls.py index 1b341b224..ed6f610ee 100644 --- a/pymodbus/framer/tls_framer.py +++ b/pymodbus/framer/old_framer_tls.py @@ -5,12 +5,12 @@ from pymodbus.exceptions import ( ModbusIOException, ) -from pymodbus.framer.base import TLS_FRAME_HEADER, ModbusFramer -from pymodbus.message.tls import MessageTLS +from pymodbus.framer.old_framer_base import TLS_FRAME_HEADER, ModbusFramer +from pymodbus.framer.tls import FramerTLS # --------------------------------------------------------------------------- # -# Modbus TLS Message +# Modbus TLS old framer # --------------------------------------------------------------------------- # @@ -34,7 +34,7 @@ def __init__(self, decoder, client=None): """ super().__init__(decoder, client) self._hsize = 0x0 - self.message_handler = MessageTLS() + self.message_handler = FramerTLS() def decode_data(self, data): """Decode data.""" diff --git a/pymodbus/message/raw.py b/pymodbus/framer/raw.py similarity index 79% rename from pymodbus/message/raw.py rename to pymodbus/framer/raw.py index 88627482b..1d32ec20a 100644 --- a/pymodbus/message/raw.py +++ b/pymodbus/framer/raw.py @@ -1,11 +1,11 @@ -"""ModbusMessage layer.""" +"""Modbus Raw (passthrough) implementation.""" from __future__ import annotations +from pymodbus.framer.base import FramerBase from pymodbus.logging import Log -from pymodbus.message.base import MessageBase -class MessageRaw(MessageBase): +class FramerRaw(FramerBase): r"""Modbus RAW Frame Controller. [ Device id ][Transaction id ][ Data ] @@ -25,6 +25,6 @@ def decode(self, data: bytes) -> tuple[int, int, int, bytes]: tid = int(data[1]) return len(data), dev_id, tid, data[2:] - def encode(self, data: bytes, device_id: int, tid: int) -> bytes: + def encode(self, pdu: bytes, device_id: int, tid: int) -> bytes: """Decode message.""" - return device_id.to_bytes(1, 'big') + tid.to_bytes(1, 'big') + data + return device_id.to_bytes(1, 'big') + tid.to_bytes(1, 'big') + pdu diff --git a/pymodbus/message/rtu.py b/pymodbus/framer/rtu.py similarity index 91% rename from pymodbus/message/rtu.py rename to pymodbus/framer/rtu.py index 2565fa5a9..7cad7f8e6 100644 --- a/pymodbus/message/rtu.py +++ b/pymodbus/framer/rtu.py @@ -1,20 +1,15 @@ -"""ModbusMessage layer. - -is extending ModbusProtocol to handle receiving and sending of messsagees. - -ModbusMessage provides a unified interface to send/receive Modbus requests/responses. -""" +"""Modbus RTU frame implementation.""" from __future__ import annotations import struct from pymodbus.exceptions import ModbusIOException from pymodbus.factory import ClientDecoder +from pymodbus.framer.base import FramerBase from pymodbus.logging import Log -from pymodbus.message.base import MessageBase -class MessageRTU(MessageBase): +class FramerRTU(FramerBase): """Modbus RTU frame type. [ Start Wait ] [Address ][ Function Code] [ Data ][ CRC ][ End Wait ] @@ -130,7 +125,7 @@ def check_frame(self): data = self._buffer[: frame_size - 2] crc = self._header["crc"] crc_val = (int(crc[0]) << 8) + int(crc[1]) - return MessageRTU.check_CRC(data, crc_val) + return FramerRTU.check_CRC(data, crc_val) except (IndexError, KeyError, struct.error): return False @@ -165,13 +160,21 @@ def check_frame(self): Log.debug("Frame advanced, resetting header!!") callback(result) # defer or push to a thread? + def assemble_frame(self, _data_len: int, _data: bytes) -> int: + """Collect frame, until CRC matches.""" + return 0 def decode(self, data: bytes) -> tuple[int, int, int, bytes]: """Decode message.""" - resp = None - if len(data) < 4: + if (data_len := len(data)) < 6: # + return 0, 0, 0, b'' + + if not (_frame_len := self.assemble_frame(data_len, data)): return 0, 0, 0, b'' + + + resp = None def callback(result): """Set result.""" nonlocal resp @@ -180,11 +183,10 @@ def callback(result): self._legacy_decode(callback, [0]) return 0, 0, 0, b'' - - def encode(self, data: bytes, device_id: int, _tid: int) -> bytes: + def encode(self, pdu: bytes, device_id: int, _tid: int) -> bytes: """Decode message.""" - packet = device_id.to_bytes(1,'big') + data - return packet + MessageRTU.compute_CRC(packet).to_bytes(2,'big') + packet = device_id.to_bytes(1,'big') + pdu + return packet + FramerRTU.compute_CRC(packet).to_bytes(2,'big') @classmethod def check_CRC(cls, data: bytes, check: int) -> bool: @@ -200,9 +202,6 @@ def check_CRC(cls, data: bytes, check: int) -> bool: def compute_CRC(cls, data: bytes) -> int: """Compute a crc16 on the passed in bytes. - For modbus, this is only used on the binary serial protocols (in this - case RTU). - The difference between modbus's crc16 and a normal crc16 is that modbus starts the crc value out at 0xffff. @@ -216,4 +215,4 @@ def compute_CRC(cls, data: bytes) -> int: swapped = ((crc << 8) & 0xFF00) | ((crc >> 8) & 0x00FF) return swapped -MessageRTU.crc16_table = MessageRTU.generate_crc16_table() +FramerRTU.crc16_table = FramerRTU.generate_crc16_table() diff --git a/pymodbus/message/socket.py b/pymodbus/framer/socket.py similarity index 72% rename from pymodbus/message/socket.py rename to pymodbus/framer/socket.py index 53e88ef9c..1c99837bd 100644 --- a/pymodbus/message/socket.py +++ b/pymodbus/framer/socket.py @@ -1,16 +1,11 @@ -"""ModbusMessage layer. - -is extending ModbusProtocol to handle receiving and sending of messsagees. - -ModbusMessage provides a unified interface to send/receive Modbus requests/responses. -""" +"""Modbus Socket frame implementation.""" from __future__ import annotations +from pymodbus.framer.base import FramerBase from pymodbus.logging import Log -from pymodbus.message.base import MessageBase -class MessageSocket(MessageBase): +class FramerSocket(FramerBase): """Modbus Socket frame type. [ MBAP Header ] [ Function Code] [ Data ] @@ -33,13 +28,13 @@ def decode(self, data: bytes) -> tuple[int, int, int, bytes]: return 0, 0, 0, self.EMPTY return msg_len, msg_tid, msg_dev, data[7:msg_len] - def encode(self, data: bytes, device_id: int, tid: int) -> bytes: + def encode(self, pdu: bytes, device_id: int, tid: int) -> bytes: """Decode message.""" packet = ( tid.to_bytes(2, 'big') + b'\x00\x00' + - (len(data) + 1).to_bytes(2, 'big') + + (len(pdu) + 1).to_bytes(2, 'big') + device_id.to_bytes(1, 'big') + - data + pdu ) return packet diff --git a/pymodbus/framer/tls.py b/pymodbus/framer/tls.py new file mode 100644 index 000000000..8256038c3 --- /dev/null +++ b/pymodbus/framer/tls.py @@ -0,0 +1,20 @@ +"""Modbus TLS frame implementation.""" +from __future__ import annotations + +from pymodbus.framer.base import FramerBase + + +class FramerTLS(FramerBase): + """Modbus TLS frame type. + + [ Function Code] [ Data ] + 1b Nb + """ + + def decode(self, data: bytes) -> tuple[int, int, int, bytes]: + """Decode message.""" + return len(data), 0, 0, data + + def encode(self, pdu: bytes, _device_id: int, _tid: int) -> bytes: + """Decode message.""" + return pdu diff --git a/pymodbus/message/__init__.py b/pymodbus/message/__init__.py deleted file mode 100644 index 5e35bc814..000000000 --- a/pymodbus/message/__init__.py +++ /dev/null @@ -1,7 +0,0 @@ -"""Message.""" -__all__ = [ - "Message", - "MessageType", -] - -from pymodbus.message.message import Message, MessageType diff --git a/pymodbus/message/base.py b/pymodbus/message/base.py deleted file mode 100644 index 3a41bb9eb..000000000 --- a/pymodbus/message/base.py +++ /dev/null @@ -1,38 +0,0 @@ -"""ModbusMessage layer. - -The message layer is responsible for encoding/decoding requests/responses. - -According to the selected type of modbus frame a prefix/suffix is added/removed -""" -from __future__ import annotations - -from abc import abstractmethod - - -class MessageBase: - """Intern base.""" - - EMPTY = b'' - - def __init__(self) -> None: - """Initialize a message instance.""" - - - @abstractmethod - def decode(self, _data: bytes) -> tuple[int, int, int, bytes]: - """Decode message. - - return: - used_len (int) or 0 to read more - transaction_id (int) or 0 - device_id (int) or 0 - modbus request/response (bytes) - """ - - @abstractmethod - def encode(self, data: bytes, device_id: int, tid: int) -> bytes: - """Decode message. - - return: - modbus message (bytes) - """ diff --git a/pymodbus/message/tls.py b/pymodbus/message/tls.py deleted file mode 100644 index d89e769e1..000000000 --- a/pymodbus/message/tls.py +++ /dev/null @@ -1,25 +0,0 @@ -"""ModbusMessage layer. - -is extending ModbusProtocol to handle receiving and sending of messsagees. - -ModbusMessage provides a unified interface to send/receive Modbus requests/responses. -""" -from __future__ import annotations - -from pymodbus.message.base import MessageBase - - -class MessageTLS(MessageBase): - """Modbus TLS frame type. - - [ Function Code] [ Data ] - 1b Nb - """ - - def decode(self, data: bytes) -> tuple[int, int, int, bytes]: - """Decode message.""" - return len(data), 0, 0, data - - def encode(self, data: bytes, _device_id: int, _tid: int) -> bytes: - """Decode message.""" - return data diff --git a/pymodbus/server/async_io.py b/pymodbus/server/async_io.py index 4453d50dc..449580ff0 100644 --- a/pymodbus/server/async_io.py +++ b/pymodbus/server/async_io.py @@ -11,7 +11,7 @@ from pymodbus.device import ModbusControlBlock, ModbusDeviceIdentification from pymodbus.exceptions import NoSuchSlaveException from pymodbus.factory import ServerDecoder -from pymodbus.framer import FRAMER_NAME_TO_CLASS, Framer, ModbusFramer +from pymodbus.framer import FRAMER_NAME_TO_CLASS, FramerType, ModbusFramer from pymodbus.logging import Log from pymodbus.pdu import ModbusExceptions as merror from pymodbus.transport import CommParams, CommType, ModbusProtocol @@ -316,7 +316,7 @@ class ModbusTcpServer(ModbusBaseServer): def __init__( self, context, - framer=Framer.SOCKET, + framer=FramerType.SOCKET, identity=None, address=("", 502), ignore_missing_slaves=False, @@ -376,7 +376,7 @@ class ModbusTlsServer(ModbusTcpServer): def __init__( # pylint: disable=too-many-arguments self, context, - framer=Framer.TLS, + framer=FramerType.TLS, identity=None, address=("", 502), sslctx=None, @@ -442,7 +442,7 @@ class ModbusUdpServer(ModbusBaseServer): def __init__( self, context, - framer=Framer.SOCKET, + framer=FramerType.SOCKET, identity=None, address=("", 502), ignore_missing_slaves=False, @@ -496,7 +496,7 @@ class ModbusSerialServer(ModbusBaseServer): """ def __init__( - self, context, framer=Framer.RTU, identity=None, **kwargs + self, context, framer=FramerType.RTU, identity=None, **kwargs ): """Initialize the socket server. @@ -611,7 +611,7 @@ async def StartAsyncTcpServer( # pylint: disable=invalid-name,dangerous-default """ kwargs.pop("host", None) server = ModbusTcpServer( - context, kwargs.pop("framer", Framer.SOCKET), identity, address, **kwargs + context, kwargs.pop("framer", FramerType.SOCKET), identity, address, **kwargs ) await _serverList.run(server, custom_functions) @@ -643,7 +643,7 @@ async def StartAsyncTlsServer( # pylint: disable=invalid-name,dangerous-default kwargs.pop("host", None) server = ModbusTlsServer( context, - kwargs.pop("framer", Framer.TLS), + kwargs.pop("framer", FramerType.TLS), identity, address, sslctx, @@ -673,7 +673,7 @@ async def StartAsyncUdpServer( # pylint: disable=invalid-name,dangerous-default """ kwargs.pop("host", None) server = ModbusUdpServer( - context, kwargs.pop("framer", Framer.SOCKET), identity, address, **kwargs + context, kwargs.pop("framer", FramerType.SOCKET), identity, address, **kwargs ) await _serverList.run(server, custom_functions) @@ -693,7 +693,7 @@ async def StartAsyncSerialServer( # pylint: disable=invalid-name,dangerous-defa :param kwargs: The rest """ server = ModbusSerialServer( - context, kwargs.pop("framer", Framer.RTU), identity=identity, **kwargs + context, kwargs.pop("framer", FramerType.RTU), identity=identity, **kwargs ) await _serverList.run(server, custom_functions) diff --git a/pymodbus/transaction.py b/pymodbus/transaction.py index 8ff1feeda..157537cdb 100644 --- a/pymodbus/transaction.py +++ b/pymodbus/transaction.py @@ -21,10 +21,12 @@ InvalidMessageReceivedException, ModbusIOException, ) -from pymodbus.framer.ascii_framer import ModbusAsciiFramer -from pymodbus.framer.rtu_framer import ModbusRtuFramer -from pymodbus.framer.socket_framer import ModbusSocketFramer -from pymodbus.framer.tls_framer import ModbusTlsFramer +from pymodbus.framer import ( + ModbusAsciiFramer, + ModbusRtuFramer, + ModbusSocketFramer, + ModbusTlsFramer, +) from pymodbus.logging import Log from pymodbus.utilities import ModbusTransactionState, hexlify_packets diff --git a/test/conftest.py b/test/conftest.py index ee499042a..3cc85989f 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -286,14 +286,17 @@ def recv(self, size): """Receive.""" if not self.packets or not size: return b"" - if not self.buffer: - self.buffer = self.packets.popleft() - if size >= len(self.buffer): - retval = self.buffer - self.buffer = None - else: - retval = self.buffer[0:size] - self.buffer = self.buffer[size] + # if not self.buffer: + # self.buffer = self.packets.popleft() + # if size >= len(self.buffer): + # retval = self.buffer + # self.buffer = None + # else: + # retval = self.buffer[0:size] + # self.buffer = self.buffer[size] + self.buffer = self.packets.popleft() + retval = self.buffer + self.buffer = None self.in_waiting -= len(retval) return retval diff --git a/test/message/__init__.py b/test/framers/__init__.py similarity index 100% rename from test/message/__init__.py rename to test/framers/__init__.py diff --git a/test/message/conftest.py b/test/framers/conftest.py similarity index 91% rename from test/message/conftest.py rename to test/framers/conftest.py index dcf2e3369..5592ac994 100644 --- a/test/message/conftest.py +++ b/test/framers/conftest.py @@ -5,15 +5,15 @@ import pytest -from pymodbus.message import Message, MessageType +from pymodbus.framer import Framer, FramerType from pymodbus.transport import CommParams, ModbusProtocol -class DummyMessage(Message): +class DummyMessage(Framer): """Implement use of ModbusProtocol.""" def __init__(self, - message_type: MessageType, + message_type: FramerType, params: CommParams, is_server: bool, device_ids: list[int] | None, diff --git a/test/message/generator.py b/test/framers/generator.py similarity index 100% rename from test/message/generator.py rename to test/framers/generator.py diff --git a/test/message/test_ascii.py b/test/framers/test_ascii.py similarity index 91% rename from test/message/test_ascii.py rename to test/framers/test_ascii.py index e166828c3..d78d2e87b 100644 --- a/test/message/test_ascii.py +++ b/test/framers/test_ascii.py @@ -1,17 +1,17 @@ -"""Test transport.""" +"""Test framer.""" import pytest -from pymodbus.message.ascii import MessageAscii +from pymodbus.framer.ascii import FramerAscii -class TestMessageAscii: - """Test message module.""" +class TestFramerAscii: + """Test module.""" @staticmethod @pytest.fixture(name="frame") def prepare_frame(): """Return message object.""" - return MessageAscii() + return FramerAscii() @pytest.mark.parametrize( diff --git a/test/message/test_message.py b/test/framers/test_framer.py similarity index 63% rename from test/message/test_message.py rename to test/framers/test_framer.py index 113ec8f8c..0114053ff 100644 --- a/test/message/test_message.py +++ b/test/framers/test_framer.py @@ -1,33 +1,33 @@ -"""Test transport.""" +"""Test framer.""" from unittest import mock import pytest -from pymodbus.message import MessageType -from pymodbus.message.ascii import MessageAscii -from pymodbus.message.rtu import MessageRTU -from pymodbus.message.socket import MessageSocket -from pymodbus.message.tls import MessageTLS +from pymodbus.framer import FramerType +from pymodbus.framer.ascii import FramerAscii +from pymodbus.framer.rtu import FramerRTU +from pymodbus.framer.socket import FramerSocket +from pymodbus.framer.tls import FramerTLS from pymodbus.transport import CommParams -class TestMessage: - """Test message module.""" +class TestFramer: + """Test module.""" @staticmethod @pytest.fixture(name="msg") async def prepare_message(dummy_message): """Return message object.""" return dummy_message( - MessageType.RAW, + FramerType.RAW, CommParams(), False, [1], ) - @pytest.mark.parametrize(("entry"), list(MessageType)) + @pytest.mark.parametrize(("entry"), list(FramerType)) async def test_message_init(self, entry, dummy_message): """Test message type.""" msg = dummy_message(entry.value, @@ -102,12 +102,12 @@ async def test_encode(self, msg, data, dev_id, tid, res_data): @pytest.mark.parametrize( ("func", "lrc", "expect"), - [(MessageAscii.check_LRC, 0x1c, True), - (MessageAscii.check_LRC, 0x0c, False), - (MessageAscii.compute_LRC, None, 0x1c), - (MessageRTU.check_CRC, 0xE2DB, True), - (MessageRTU.check_CRC, 0xDBE2, False), - (MessageRTU.compute_CRC, None, 0xE2DB), + [(FramerAscii.check_LRC, 0x1c, True), + (FramerAscii.check_LRC, 0x0c, False), + (FramerAscii.compute_LRC, None, 0x1c), + (FramerRTU.check_CRC, 0xE2DB, True), + (FramerRTU.check_CRC, 0xDBE2, False), + (FramerRTU.compute_CRC, None, 0xE2DB), ] ) def test_LRC_CRC(self, func, lrc, expect): @@ -118,30 +118,30 @@ def test_LRC_CRC(self, func, lrc, expect): def test_roundtrip_LRC(self): """Test combined compute/check LRC.""" data = b'\x12\x34\x23\x45\x34\x56\x45\x67' - assert MessageAscii.compute_LRC(data) == 0x1c - assert MessageAscii.check_LRC(data, 0x1C) + assert FramerAscii.compute_LRC(data) == 0x1c + assert FramerAscii.check_LRC(data, 0x1C) def test_crc16_table(self): """Test the crc16 table is prefilled.""" - assert len(MessageRTU.crc16_table) == 256 - assert isinstance(MessageRTU.crc16_table[0], int) - assert isinstance(MessageRTU.crc16_table[255], int) + assert len(FramerRTU.crc16_table) == 256 + assert isinstance(FramerRTU.crc16_table[0], int) + assert isinstance(FramerRTU.crc16_table[255], int) def test_roundtrip_CRC(self): """Test combined compute/check CRC.""" data = b'\x12\x34\x23\x45\x34\x56\x45\x67' - assert MessageRTU.compute_CRC(data) == 0xE2DB - assert MessageRTU.check_CRC(data, 0xE2DB) + assert FramerRTU.compute_CRC(data) == 0xE2DB + assert FramerRTU.check_CRC(data, 0xE2DB) -class TestMessages: - """Test message classes.""" +class TestFramer2: + """Test classes.""" @pytest.mark.parametrize( ("frame", "frame_expected"), [ - (MessageAscii, [ + (FramerAscii, [ b':0003007C00027F\r\n', b':000304008D008EDE\r\n', b':0083027B\r\n', @@ -152,7 +152,7 @@ class TestMessages: b':FF0304008D008EDF\r\n', b':FF83027C\r\n', ]), - (MessageRTU, [ + (FramerRTU, [ b'\x00\x03\x00\x7c\x00\x02\x04\x02', b'\x00\x03\x04\x00\x8d\x00\x8e\xfa\xbc', b'\x00\x83\x02\x91\x31', @@ -163,7 +163,7 @@ class TestMessages: b'\xff\x03\x04\x00\x8d\x00\x8e\xf5\xb3', b'\xff\x83\x02\xa1\x01', ]), - (MessageSocket, [ + (FramerSocket, [ b'\x00\x00\x00\x00\x00\x06\x00\x03\x00\x7c\x00\x02', b'\x00\x00\x00\x00\x00\x07\x00\x03\x04\x00\x8d\x00\x8e', b'\x00\x00\x00\x00\x00\x03\x00\x83\x02', @@ -183,7 +183,7 @@ class TestMessages: b'\x0c\x05\x00\x00\x00\x07\xff\x03\x04\x00\x8d\x00\x8e', b'\x0c\x05\x00\x00\x00\x03\xff\x83\x02', ]), - (MessageTLS, [ + (FramerTLS, [ b'\x03\x00\x7c\x00\x02', b'\x03\x04\x00\x8d\x00\x8e', b'\x83\x02', @@ -215,8 +215,8 @@ class TestMessages: ) def test_encode(self, frame, frame_expected, data, dev_id, tid, inx1, inx2, inx3): """Test encode method.""" - if ((frame != MessageSocket and tid) or - (frame == MessageTLS and dev_id)): + if ((frame != FramerSocket and tid) or + (frame == FramerTLS and dev_id)): return frame_obj = frame() expected = frame_expected[inx1 + inx2 + inx3] @@ -226,45 +226,45 @@ def test_encode(self, frame, frame_expected, data, dev_id, tid, inx1, inx2, inx3 @pytest.mark.parametrize( ("msg_type", "data", "dev_id", "tid", "expected"), [ - (MessageType.ASCII, b':0003007C00027F\r\n', 0, 0, b"\x03\x00\x7c\x00\x02",), # Request - (MessageType.ASCII, b':000304008D008EDE\r\n', 0, 0, b"\x03\x04\x00\x8d\x00\x8e",), # Response - (MessageType.ASCII, b':0083027B\r\n', 0, 0, b'\x83\x02',), # Exception - (MessageType.ASCII, b':1103007C00026E\r\n', 17, 0, b"\x03\x00\x7c\x00\x02",), # Request - (MessageType.ASCII, b':110304008D008ECD\r\n', 17, 0, b"\x03\x04\x00\x8d\x00\x8e",), # Response - (MessageType.ASCII, b':1183026A\r\n', 17, 0, b'\x83\x02',), # Exception - (MessageType.ASCII, b':FF03007C000280\r\n', 255, 0, b"\x03\x00\x7c\x00\x02",), # Request - (MessageType.ASCII, b':FF0304008D008EDF\r\n', 255, 0, b"\x03\x04\x00\x8d\x00\x8e",), # Response - (MessageType.ASCII, b':FF83027C\r\n', 255, 0, b'\x83\x02',), # Exception - (MessageType.RTU, b'\x00\x03\x00\x7c\x00\x02\x04\x02', 0, 0, b"\x03\x00\x7c\x00\x02",), # Request - (MessageType.RTU, b'\x00\x03\x04\x00\x8d\x00\x8e\xfa\xbc', 0, 0, b"\x03\x04\x00\x8d\x00\x8e",), # Response - (MessageType.RTU, b'\x00\x83\x02\x91\x31', 0, 0, b'\x83\x02',), # Exception - (MessageType.RTU, b'\x11\x03\x00\x7c\x00\x02\x07\x43', 17, 0, b"\x03\x00\x7c\x00\x02",), # Request - (MessageType.RTU, b'\x11\x03\x04\x00\x8d\x00\x8e\xfb\xbd', 17, 0, b"\x03\x04\x00\x8d\x00\x8e",), # Response - (MessageType.RTU, b'\x11\x83\x02\xc1\x34', 17, 0, b'\x83\x02',), # Exception - (MessageType.RTU, b'\xff\x03\x00|\x00\x02\x10\x0d', 255, 0, b"\x03\x00\x7c\x00\x02",), # Request - (MessageType.RTU, b'\xff\x03\x04\x00\x8d\x00\x8e\xf5\xb3', 255, 0, b"\x03\x04\x00\x8d\x00\x8e",), # Response - (MessageType.RTU, b'\xff\x83\x02\xa1\x01', 255, 0, b'\x83\x02',), # Exception - (MessageType.SOCKET, b'\x00\x00\x00\x00\x00\x06\x00\x03\x00\x7c\x00\x02', 0, 0, b"\x03\x00\x7c\x00\x02",), # Request - (MessageType.SOCKET, b'\x00\x00\x00\x00\x00\x07\x00\x03\x04\x00\x8d\x00\x8e', 0, 0, b"\x03\x04\x00\x8d\x00\x8e",), # Response - (MessageType.SOCKET, b'\x00\x00\x00\x00\x00\x03\x00\x83\x02', 0, 0, b'\x83\x02',), # Exception - (MessageType.SOCKET, b'\x00\x00\x00\x00\x00\x06\x11\x03\x00\x7c\x00\x02', 17, 0, b"\x03\x00\x7c\x00\x02",), # Request - (MessageType.SOCKET, b'\x00\x00\x00\x00\x00\x07\x11\x03\x04\x00\x8d\x00\x8e', 17, 0, b"\x03\x04\x00\x8d\x00\x8e",), # Response - (MessageType.SOCKET, b'\x00\x00\x00\x00\x00\x03\x11\x83\x02', 17, 0, b'\x83\x02',), # Exception - (MessageType.SOCKET, b'\x00\x00\x00\x00\x00\x06\xff\x03\x00\x7c\x00\x02', 255, 0, b"\x03\x00\x7c\x00\x02",), # Request - (MessageType.SOCKET, b'\x00\x00\x00\x00\x00\x07\xff\x03\x04\x00\x8d\x00\x8e', 255, 0, b"\x03\x04\x00\x8d\x00\x8e",), # Response - (MessageType.SOCKET, b'\x00\x00\x00\x00\x00\x03\xff\x83\x02', 255, 0, b'\x83\x02',), # Exception - (MessageType.SOCKET, b'\x0c\x05\x00\x00\x00\x06\x00\x03\x00\x7c\x00\x02', 0, 3077, b"\x03\x00\x7c\x00\x02",), # Request - (MessageType.SOCKET, b'\x0c\x05\x00\x00\x00\x07\x00\x03\x04\x00\x8d\x00\x8e', 0, 3077, b"\x03\x04\x00\x8d\x00\x8e",), # Response - (MessageType.SOCKET, b'\x0c\x05\x00\x00\x00\x03\x00\x83\x02', 0, 3077, b'\x83\x02',), # Exception - (MessageType.SOCKET, b'\x0c\x05\x00\x00\x00\x06\x11\x03\x00\x7c\x00\x02', 17, 3077, b"\x03\x00\x7c\x00\x02",), # Request - (MessageType.SOCKET, b'\x0c\x05\x00\x00\x00\x07\x11\x03\x04\x00\x8d\x00\x8e', 17, 3077, b"\x03\x04\x00\x8d\x00\x8e",), # Response - (MessageType.SOCKET, b'\x0c\x05\x00\x00\x00\x03\x11\x83\x02', 17, 3077, b'\x83\x02',), # Exception - (MessageType.SOCKET, b'\x0c\x05\x00\x00\x00\x06\xff\x03\x00\x7c\x00\x02', 255, 3077, b"\x03\x00\x7c\x00\x02",), # Request - (MessageType.SOCKET, b'\x0c\x05\x00\x00\x00\x07\xff\x03\x04\x00\x8d\x00\x8e', 255, 3077, b"\x03\x04\x00\x8d\x00\x8e",), # Response - (MessageType.SOCKET, b'\x0c\x05\x00\x00\x00\x03\xff\x83\x02', 255, 3077, b'\x83\x02',), # Exception - (MessageType.TLS, b'\x03\x00\x7c\x00\x02', 0, 0, b"\x03\x00\x7c\x00\x02",), # Request - (MessageType.TLS, b'\x03\x04\x00\x8d\x00\x8e', 0, 0, b"\x03\x04\x00\x8d\x00\x8e",), # Response - (MessageType.TLS, b'\x83\x02', 0, 0, b'\x83\x02',), # Exception + (FramerType.ASCII, b':0003007C00027F\r\n', 0, 0, b"\x03\x00\x7c\x00\x02",), # Request + (FramerType.ASCII, b':000304008D008EDE\r\n', 0, 0, b"\x03\x04\x00\x8d\x00\x8e",), # Response + (FramerType.ASCII, b':0083027B\r\n', 0, 0, b'\x83\x02',), # Exception + (FramerType.ASCII, b':1103007C00026E\r\n', 17, 0, b"\x03\x00\x7c\x00\x02",), # Request + (FramerType.ASCII, b':110304008D008ECD\r\n', 17, 0, b"\x03\x04\x00\x8d\x00\x8e",), # Response + (FramerType.ASCII, b':1183026A\r\n', 17, 0, b'\x83\x02',), # Exception + (FramerType.ASCII, b':FF03007C000280\r\n', 255, 0, b"\x03\x00\x7c\x00\x02",), # Request + (FramerType.ASCII, b':FF0304008D008EDF\r\n', 255, 0, b"\x03\x04\x00\x8d\x00\x8e",), # Response + (FramerType.ASCII, b':FF83027C\r\n', 255, 0, b'\x83\x02',), # Exception + (FramerType.RTU, b'\x00\x03\x00\x7c\x00\x02\x04\x02', 0, 0, b"\x03\x00\x7c\x00\x02",), # Request + (FramerType.RTU, b'\x00\x03\x04\x00\x8d\x00\x8e\xfa\xbc', 0, 0, b"\x03\x04\x00\x8d\x00\x8e",), # Response + (FramerType.RTU, b'\x00\x83\x02\x91\x31', 0, 0, b'\x83\x02',), # Exception + (FramerType.RTU, b'\x11\x03\x00\x7c\x00\x02\x07\x43', 17, 0, b"\x03\x00\x7c\x00\x02",), # Request + (FramerType.RTU, b'\x11\x03\x04\x00\x8d\x00\x8e\xfb\xbd', 17, 0, b"\x03\x04\x00\x8d\x00\x8e",), # Response + (FramerType.RTU, b'\x11\x83\x02\xc1\x34', 17, 0, b'\x83\x02',), # Exception + (FramerType.RTU, b'\xff\x03\x00|\x00\x02\x10\x0d', 255, 0, b"\x03\x00\x7c\x00\x02",), # Request + (FramerType.RTU, b'\xff\x03\x04\x00\x8d\x00\x8e\xf5\xb3', 255, 0, b"\x03\x04\x00\x8d\x00\x8e",), # Response + (FramerType.RTU, b'\xff\x83\x02\xa1\x01', 255, 0, b'\x83\x02',), # Exception + (FramerType.SOCKET, b'\x00\x00\x00\x00\x00\x06\x00\x03\x00\x7c\x00\x02', 0, 0, b"\x03\x00\x7c\x00\x02",), # Request + (FramerType.SOCKET, b'\x00\x00\x00\x00\x00\x07\x00\x03\x04\x00\x8d\x00\x8e', 0, 0, b"\x03\x04\x00\x8d\x00\x8e",), # Response + (FramerType.SOCKET, b'\x00\x00\x00\x00\x00\x03\x00\x83\x02', 0, 0, b'\x83\x02',), # Exception + (FramerType.SOCKET, b'\x00\x00\x00\x00\x00\x06\x11\x03\x00\x7c\x00\x02', 17, 0, b"\x03\x00\x7c\x00\x02",), # Request + (FramerType.SOCKET, b'\x00\x00\x00\x00\x00\x07\x11\x03\x04\x00\x8d\x00\x8e', 17, 0, b"\x03\x04\x00\x8d\x00\x8e",), # Response + (FramerType.SOCKET, b'\x00\x00\x00\x00\x00\x03\x11\x83\x02', 17, 0, b'\x83\x02',), # Exception + (FramerType.SOCKET, b'\x00\x00\x00\x00\x00\x06\xff\x03\x00\x7c\x00\x02', 255, 0, b"\x03\x00\x7c\x00\x02",), # Request + (FramerType.SOCKET, b'\x00\x00\x00\x00\x00\x07\xff\x03\x04\x00\x8d\x00\x8e', 255, 0, b"\x03\x04\x00\x8d\x00\x8e",), # Response + (FramerType.SOCKET, b'\x00\x00\x00\x00\x00\x03\xff\x83\x02', 255, 0, b'\x83\x02',), # Exception + (FramerType.SOCKET, b'\x0c\x05\x00\x00\x00\x06\x00\x03\x00\x7c\x00\x02', 0, 3077, b"\x03\x00\x7c\x00\x02",), # Request + (FramerType.SOCKET, b'\x0c\x05\x00\x00\x00\x07\x00\x03\x04\x00\x8d\x00\x8e', 0, 3077, b"\x03\x04\x00\x8d\x00\x8e",), # Response + (FramerType.SOCKET, b'\x0c\x05\x00\x00\x00\x03\x00\x83\x02', 0, 3077, b'\x83\x02',), # Exception + (FramerType.SOCKET, b'\x0c\x05\x00\x00\x00\x06\x11\x03\x00\x7c\x00\x02', 17, 3077, b"\x03\x00\x7c\x00\x02",), # Request + (FramerType.SOCKET, b'\x0c\x05\x00\x00\x00\x07\x11\x03\x04\x00\x8d\x00\x8e', 17, 3077, b"\x03\x04\x00\x8d\x00\x8e",), # Response + (FramerType.SOCKET, b'\x0c\x05\x00\x00\x00\x03\x11\x83\x02', 17, 3077, b'\x83\x02',), # Exception + (FramerType.SOCKET, b'\x0c\x05\x00\x00\x00\x06\xff\x03\x00\x7c\x00\x02', 255, 3077, b"\x03\x00\x7c\x00\x02",), # Request + (FramerType.SOCKET, b'\x0c\x05\x00\x00\x00\x07\xff\x03\x04\x00\x8d\x00\x8e', 255, 3077, b"\x03\x04\x00\x8d\x00\x8e",), # Response + (FramerType.SOCKET, b'\x0c\x05\x00\x00\x00\x03\xff\x83\x02', 255, 3077, b'\x83\x02',), # Exception + (FramerType.TLS, b'\x03\x00\x7c\x00\x02', 0, 0, b"\x03\x00\x7c\x00\x02",), # Request + (FramerType.TLS, b'\x03\x04\x00\x8d\x00\x8e', 0, 0, b"\x03\x04\x00\x8d\x00\x8e",), # Response + (FramerType.TLS, b'\x83\x02', 0, 0, b'\x83\x02',), # Exception ] ) @pytest.mark.parametrize( @@ -277,9 +277,9 @@ def test_encode(self, frame, frame_expected, data, dev_id, tid, inx1, inx2, inx3 ) async def test_decode(self, dummy_message, msg_type, data, dev_id, tid, expected, split): """Test encode method.""" - if msg_type == MessageType.RTU: + if msg_type == FramerType.RTU: pytest.skip("Waiting on implementation!") - if msg_type == MessageType.TLS and split != "no": + if msg_type == FramerType.TLS and split != "no": return frame = dummy_message( msg_type, @@ -308,11 +308,11 @@ async def test_decode(self, dummy_message, msg_type, data, dev_id, tid, expected @pytest.mark.parametrize( ("frame", "data", "exp_len"), [ - (MessageAscii, b':0003007C00017F\r\n', 17), # bad crc + (FramerAscii, b':0003007C00017F\r\n', 17), # bad crc # (MessageAscii, b'abc:0003007C00027F\r\n', 3), # garble in front # (MessageAscii, b':0003007C00017F\r\nabc', 17), # bad crc, garble after # (MessageAscii, b':0003007C00017F\r\n:0003', 17), # part second message - (MessageRTU, b'\x00\x83\x02\x91\x31', 0), # bad crc + (FramerRTU, b'\x00\x83\x02\x91\x31', 0), # bad crc # (MessageRTU, b'\x00\x83\x02\x91\x31', 0), # garble in front # (MessageRTU, b'\x00\x83\x02\x91\x31', 0), # garble after # (MessageRTU, b'\x00\x83\x02\x91\x31', 0), # part second message @@ -320,7 +320,7 @@ async def test_decode(self, dummy_message, msg_type, data, dev_id, tid, expected ) async def test_decode_bad_crc(self, frame, data, exp_len): """Test encode method.""" - if frame == MessageRTU: + if frame == FramerRTU: pytest.skip("Waiting for implementation.") frame_obj = frame() used_len, _, _, data = frame_obj.decode(data) diff --git a/test/test_framers.py b/test/framers/test_old_framers.py similarity index 99% rename from test/test_framers.py rename to test/framers/test_old_framers.py index 280634d98..4059ca766 100644 --- a/test/test_framers.py +++ b/test/framers/test_old_framers.py @@ -3,7 +3,7 @@ import pytest -from pymodbus import Framer +from pymodbus import FramerType from pymodbus.bit_read_message import ReadCoilsRequest from pymodbus.client.base import ModbusBaseClient from pymodbus.exceptions import ModbusIOException @@ -328,7 +328,7 @@ async def test_send_packet(self, rtu_framer): """Test send packet.""" message = TEST_MESSAGE client = ModbusBaseClient( - Framer.ASCII, + FramerType.ASCII, host="localhost", port=BASE_PORT + 1, CommType=CommType.TCP, diff --git a/test/message/test_rtu.py b/test/framers/test_rtu.py similarity index 84% rename from test/message/test_rtu.py rename to test/framers/test_rtu.py index e7c019b99..dd30f8218 100644 --- a/test/message/test_rtu.py +++ b/test/framers/test_rtu.py @@ -1,17 +1,17 @@ -"""Test transport.""" +"""Test framer.""" import pytest -from pymodbus.message.rtu import MessageRTU +from pymodbus.framer.rtu import FramerRTU -class TestMessageRTU: - """Test message module.""" +class TestFramerRTU: + """Test module.""" @staticmethod @pytest.fixture(name="frame") def prepare_frame(): """Return message object.""" - return MessageRTU() + return FramerRTU() @pytest.mark.parametrize( diff --git a/test/message/test_socket.py b/test/framers/test_socket.py similarity index 91% rename from test/message/test_socket.py rename to test/framers/test_socket.py index e58ba65cc..d716d4c7d 100644 --- a/test/message/test_socket.py +++ b/test/framers/test_socket.py @@ -1,18 +1,18 @@ -"""Test transport.""" +"""Test framer.""" import pytest -from pymodbus.message.socket import MessageSocket +from pymodbus.framer.socket import FramerSocket -class TestMessageSocket: - """Test message module.""" +class TestFramerSocket: + """Test module.""" @staticmethod @pytest.fixture(name="frame") def prepare_frame(): """Return message object.""" - return MessageSocket() + return FramerSocket() @pytest.mark.parametrize( diff --git a/test/message/test_multidrop.py b/test/framers/test_tbc_multidrop.py similarity index 99% rename from test/message/test_multidrop.py rename to test/framers/test_tbc_multidrop.py index 74711ab5c..e2a21bd14 100644 --- a/test/message/test_multidrop.py +++ b/test/framers/test_tbc_multidrop.py @@ -3,7 +3,7 @@ import pytest -from pymodbus.framer.rtu_framer import ModbusRtuFramer +from pymodbus.framer import ModbusRtuFramer from pymodbus.server.async_io import ServerDecoder diff --git a/test/framers/test_tbc_transaction.py b/test/framers/test_tbc_transaction.py new file mode 100755 index 000000000..b072909f8 --- /dev/null +++ b/test/framers/test_tbc_transaction.py @@ -0,0 +1,721 @@ +"""Test transaction.""" +from itertools import count +from unittest import mock + +from pymodbus.exceptions import ( + ModbusIOException, +) +from pymodbus.factory import ServerDecoder +from pymodbus.pdu import ModbusRequest +from pymodbus.transaction import ( + ModbusAsciiFramer, + ModbusRtuFramer, + ModbusSocketFramer, + ModbusTlsFramer, + ModbusTransactionManager, +) + + +TEST_MESSAGE = b"\x7b\x01\x03\x00\x00\x00\x05\x85\xC9\x7d" + + +class TestTransaction: # pylint: disable=too-many-public-methods + """Unittest for the pymodbus.transaction module.""" + + client = None + decoder = None + _tcp = None + _tls = None + _rtu = None + _ascii = None + _manager = None + _tm = None + + # ----------------------------------------------------------------------- # + # Test Construction + # ----------------------------------------------------------------------- # + def setup_method(self): + """Set up the test environment.""" + self.client = None + self.decoder = ServerDecoder() + self._tcp = ModbusSocketFramer(decoder=self.decoder, client=None) + self._tls = ModbusTlsFramer(decoder=self.decoder, client=None) + self._rtu = ModbusRtuFramer(decoder=self.decoder, client=None) + self._ascii = ModbusAsciiFramer(decoder=self.decoder, client=None) + self._manager = ModbusTransactionManager(self.client) + + # ----------------------------------------------------------------------- # + # Modbus transaction manager + # ----------------------------------------------------------------------- # + + def test_calculate_expected_response_length(self): + """Test calculate expected response length.""" + self._manager.client = mock.MagicMock() + self._manager.client.framer = mock.MagicMock() + self._manager._set_adu_size() # pylint: disable=protected-access + assert not self._manager._calculate_response_length( # pylint: disable=protected-access + 0 + ) + self._manager.base_adu_size = 10 + assert ( + self._manager._calculate_response_length(5) # pylint: disable=protected-access + == 15 + ) + + def test_calculate_exception_length(self): + """Test calculate exception length.""" + for framer, exception_length in ( + ("ascii", 11), + ("rtu", 5), + ("tcp", 9), + ("tls", 2), + ("dummy", None), + ): + self._manager.client = mock.MagicMock() + if framer == "ascii": + self._manager.client.framer = self._ascii + elif framer == "rtu": + self._manager.client.framer = self._rtu + elif framer == "tcp": + self._manager.client.framer = self._tcp + elif framer == "tls": + self._manager.client.framer = self._tls + else: + self._manager.client.framer = mock.MagicMock() + + self._manager._set_adu_size() # pylint: disable=protected-access + assert ( + self._manager._calculate_exception_length() # pylint: disable=protected-access + == exception_length + ) + + @mock.patch("pymodbus.transaction.time") + def test_execute(self, mock_time): + """Test execute.""" + mock_time.time.side_effect = count() + + client = mock.MagicMock() + client.framer = self._ascii + client.framer._buffer = b"deadbeef" # pylint: disable=protected-access + client.framer.processIncomingPacket = mock.MagicMock() + client.framer.processIncomingPacket.return_value = None + client.framer.buildPacket = mock.MagicMock() + client.framer.buildPacket.return_value = b"deadbeef" + client.framer.sendPacket = mock.MagicMock() + client.framer.sendPacket.return_value = len(b"deadbeef") + client.framer.decode_data = mock.MagicMock() + client.framer.decode_data.return_value = { + "slave": 1, + "fcode": 222, + "length": 27, + } + request = mock.MagicMock() + request.get_response_pdu_size.return_value = 10 + request.slave_id = 1 + request.function_code = 222 + trans = ModbusTransactionManager(client) + trans._recv = mock.MagicMock( # pylint: disable=protected-access + return_value=b"abcdef" + ) + assert trans.retries == 3 + assert not trans.retry_on_empty + + trans.getTransaction = mock.MagicMock() + trans.getTransaction.return_value = "response" + response = trans.execute(request) + assert response == "response" + # No response + trans._recv = mock.MagicMock( # pylint: disable=protected-access + return_value=b"abcdef" + ) + trans.transactions = {} + trans.getTransaction = mock.MagicMock() + trans.getTransaction.return_value = None + response = trans.execute(request) + assert isinstance(response, ModbusIOException) + + # No response with retries + trans.retry_on_empty = True + trans._recv = mock.MagicMock( # pylint: disable=protected-access + side_effect=iter([b"", b"abcdef"]) + ) + response = trans.execute(request) + assert isinstance(response, ModbusIOException) + + # wrong handle_local_echo + trans._recv = mock.MagicMock( # pylint: disable=protected-access + side_effect=iter([b"abcdef", b"deadbe", b"123456"]) + ) + client.comm_params.handle_local_echo = True + trans.retry_on_empty = False + trans.retry_on_invalid = False + assert trans.execute(request).message == "[Input/Output] Wrong local echo" + client.comm_params.handle_local_echo = False + + # retry on invalid response + trans.retry_on_invalid = True + trans._recv = mock.MagicMock( # pylint: disable=protected-access + side_effect=iter([b"", b"abcdef", b"deadbe", b"123456"]) + ) + response = trans.execute(request) + assert isinstance(response, ModbusIOException) + + # Unable to decode response + trans._recv = mock.MagicMock( # pylint: disable=protected-access + side_effect=ModbusIOException() + ) + client.framer.processIncomingPacket.side_effect = mock.MagicMock( + side_effect=ModbusIOException() + ) + assert isinstance(trans.execute(request), ModbusIOException) + + # Broadcast + client.params.broadcast_enable = True + request.slave_id = 0 + response = trans.execute(request) + assert response == b"Broadcast write sent - no response expected" + + # Broadcast w/ Local echo + client.comm_params.handle_local_echo = True + client.params.broadcast_enable = True + recv = mock.MagicMock(return_value=b"deadbeef") + trans._recv = recv # pylint: disable=protected-access + request.slave_id = 0 + response = trans.execute(request) + assert response == b"Broadcast write sent - no response expected" + recv.assert_called_once_with(8, False) + client.comm_params.handle_local_echo = False + + def test_transaction_manager_tid(self): + """Test the transaction manager TID.""" + for tid in range(1, self._manager.getNextTID() + 10): + assert tid + 1 == self._manager.getNextTID() + self._manager.reset() + assert self._manager.getNextTID() == 1 + + def test_get_transaction_manager_transaction(self): + """Test the getting a transaction from the transaction manager.""" + + class Request: # pylint: disable=too-few-public-methods + """Request.""" + + self._manager.reset() + handle = Request() + handle.transaction_id = ( # pylint: disable=attribute-defined-outside-init + self._manager.getNextTID() + ) + handle.message = b"testing" # pylint: disable=attribute-defined-outside-init + self._manager.addTransaction(handle) + result = self._manager.getTransaction(handle.transaction_id) + assert handle.message == result.message + + def test_delete_transaction_manager_transaction(self): + """Test deleting a transaction from the dict transaction manager.""" + + class Request: # pylint: disable=too-few-public-methods + """Request.""" + + self._manager.reset() + handle = Request() + handle.transaction_id = ( # pylint: disable=attribute-defined-outside-init + self._manager.getNextTID() + ) + handle.message = b"testing" # pylint: disable=attribute-defined-outside-init + + self._manager.addTransaction(handle) + self._manager.delTransaction(handle.transaction_id) + assert not self._manager.getTransaction(handle.transaction_id) + + # ----------------------------------------------------------------------- # + # TCP tests + # ----------------------------------------------------------------------- # + def test_tcp_framer_transaction_ready(self): + """Test a tcp frame transaction.""" + count = 0 + result = None + def callback(data): + """Simulate callback.""" + nonlocal count, result + count += 1 + result = data + + msg = b"\x00\x01\x12\x34\x00\x06\xff\x02\x01\x02\x00\x08" + self._tcp.processIncomingPacket(msg, callback, [1]) + self._tcp._buffer = msg # pylint: disable=protected-access + callback(b'') + + def test_tcp_framer_transaction_full(self): + """Test a full tcp frame transaction.""" + count = 0 + result = None + def callback(data): + """Simulate callback.""" + nonlocal count, result + count += 1 + result = data + + msg = b"\x00\x01\x12\x34\x00\x06\xff\x02\x01\x02\x00\x08" + self._tcp.processIncomingPacket(msg, callback, [0, 1]) + assert result.function_code.to_bytes(1,'big') + result.encode() == msg[7:] + + def test_tcp_framer_transaction_half(self): + """Test a half completed tcp frame transaction.""" + count = 0 + result = None + def callback(data): + """Simulate callback.""" + nonlocal count, result + count += 1 + result = data + + msg1 = b"\x00\x01\x12\x34\x00" + msg2 = b"\x06\xff\x02\x01\x02\x00\x08" + self._tcp.processIncomingPacket(msg1, callback, [0, 1]) + assert not result + self._tcp.processIncomingPacket(msg2, callback, [0, 1]) + assert result + assert result.function_code.to_bytes(1,'big') + result.encode() == msg2[2:] + + def test_tcp_framer_transaction_half2(self): + """Test a half completed tcp frame transaction.""" + count = 0 + result = None + def callback(data): + """Simulate callback.""" + nonlocal count, result + count += 1 + result = data + + msg1 = b"\x00\x01\x12\x34\x00\x06\xff" + msg2 = b"\x02\x01\x02\x00\x08" + self._tcp.processIncomingPacket(msg1, callback, [0, 1]) + assert not result + self._tcp.processIncomingPacket(msg2, callback, [0, 1]) + assert result + assert result.function_code.to_bytes(1,'big') + result.encode() == msg2 + + def test_tcp_framer_transaction_half3(self): + """Test a half completed tcp frame transaction.""" + count = 0 + result = None + def callback(data): + """Simulate callback.""" + nonlocal count, result + count += 1 + result = data + + msg1 = b"\x00\x01\x12\x34\x00\x06\xff\x02\x01\x02\x00" + msg2 = b"\x08" + self._tcp.processIncomingPacket(msg1, callback, [0, 1]) + assert not result + self._tcp.processIncomingPacket(msg2, callback, [0, 1]) + assert result + assert result.function_code.to_bytes(1,'big') + result.encode() == msg1[7:] + msg2 + + def test_tcp_framer_transaction_short(self): + """Test that we can get back on track after an invalid message.""" + count = 0 + result = None + def callback(data): + """Simulate callback.""" + nonlocal count, result + count += 1 + result = data + + # msg1 = b"\x99\x99\x99\x99\x00\x01\x00\x17" + msg1 = b'' + msg2 = b"\x00\x01\x12\x34\x00\x06\xff\x02\x01\x02\x00\x08" + self._tcp.processIncomingPacket(msg1, callback, [0, 1]) + assert not result + self._tcp.processIncomingPacket(msg2, callback, [0, 1]) + assert result + assert result.function_code.to_bytes(1,'big') + result.encode() == msg2[7:] + + def test_tcp_framer_populate(self): + """Test a tcp frame packet build.""" + count = 0 + result = None + def callback(data): + """Simulate callback.""" + nonlocal count, result + count += 1 + result = data + + expected = ModbusRequest() + expected.transaction_id = 0x0001 + expected.protocol_id = 0x1234 + expected.slave_id = 0xFF + msg = b"\x00\x01\x12\x34\x00\x06\xff\x02\x12\x34\x01\x02" + self._tcp.processIncomingPacket(msg, callback, [0, 1]) + # assert self._tcp.checkFrame() + # actual = ModbusRequest() + # self._tcp.populateResult(actual) + # for name in ("transaction_id", "protocol_id", "slave_id"): + # assert getattr(expected, name) == getattr(actual, name) + + def test_tcp_framer_packet(self): + """Test a tcp frame packet build.""" + old_encode = ModbusRequest.encode + ModbusRequest.encode = lambda self: b"" + message = ModbusRequest() + message.transaction_id = 0x0001 + message.protocol_id = 0x0000 + message.slave_id = 0xFF + message.function_code = 0x01 + expected = b"\x00\x01\x00\x00\x00\x02\xff\x01" + actual = self._tcp.buildPacket(message) + assert expected == actual + ModbusRequest.encode = old_encode + + # ----------------------------------------------------------------------- # + # TLS tests + # ----------------------------------------------------------------------- # + def test_framer_tls_framer_transaction_ready(self): + """Test a tls frame transaction.""" + count = 0 + result = None + def callback(data): + """Simulate callback.""" + nonlocal count, result + count += 1 + result = data + + msg = b"\x00\x01\x12\x34\x00\x06\xff\x02\x12\x34\x01\x02" + self._tcp.processIncomingPacket(msg[0:4], callback, [0, 1]) + assert not result + self._tcp.processIncomingPacket(msg[4:], callback, [0, 1]) + assert result + + def test_framer_tls_framer_transaction_full(self): + """Test a full tls frame transaction.""" + count = 0 + result = None + def callback(data): + """Simulate callback.""" + nonlocal count, result + count += 1 + result = data + + msg = b"\x00\x01\x12\x34\x00\x06\xff\x02\x12\x34\x01\x02" + self._tcp.processIncomingPacket(msg, callback, [0, 1]) + assert result + + def test_framer_tls_framer_transaction_half(self): + """Test a half completed tls frame transaction.""" + count = 0 + result = None + def callback(data): + """Simulate callback.""" + nonlocal count, result + count += 1 + result = data + + msg = b"\x00\x01\x12\x34\x00\x06\xff\x02\x12\x34\x01\x02" + self._tcp.processIncomingPacket(msg[0:8], callback, [0, 1]) + assert not result + self._tcp.processIncomingPacket(msg[8:], callback, [0, 1]) + assert result + + def test_framer_tls_framer_transaction_short(self): + """Test that we can get back on track after an invalid message.""" + count = 0 + result = None + def callback(data): + """Simulate callback.""" + nonlocal count, result + count += 1 + result = data + + msg = b"\x00\x01\x12\x34\x00\x06\xff\x02\x12\x34\x01\x02" + self._tcp.processIncomingPacket(msg[0:2], callback, [0, 1]) + assert not result + self._tcp.processIncomingPacket(msg[2:], callback, [0, 1]) + assert result + + def test_framer_tls_framer_decode(self): + """Testmessage decoding.""" + msg1 = b"" + msg2 = b"\x01\x12\x34\x00\x08" + result = self._tls.decode_data(msg1) + assert not result + result = self._tls.decode_data(msg2) + assert result == {"fcode": 1} + + def test_framer_tls_incoming_packet(self): + """Framer tls incoming packet.""" + msg = b"\x00\x01\x12\x34\x00\x06\xff\x02\x12\x34\x01\x02" + + slave = 0x01 + msg_result = None + + def mock_callback(result): + """Mock callback.""" + nonlocal msg_result + + msg_result = result.encode() + + self._tls.processIncomingPacket(msg, mock_callback, slave) + # assert msg == msg_result + + # self._tls.isFrameReady = mock.MagicMock(return_value=True) + # x = mock.MagicMock(return_value=False) + # self._tls._validate_slave_id = x + # self._tls.processIncomingPacket(msg, mock_callback, slave) + # assert not self._tls._buffer + # self._tls.advanceFrame() + # x = mock.MagicMock(return_value=True) + # self._tls._validate_slave_id = x + # self._tls.processIncomingPacket(msg, mock_callback, slave) + # assert msg[1:] == msg_result + # self._tls.advanceFrame() + + def test_framer_tls_process(self): + """Framer tls process.""" + # class MockResult: + # """Mock result.""" + + # def __init__(self, code): + # """Init.""" + # self.function_code = code + + # def mock_callback(_arg): + # """Mock callback.""" + + # self._tls.decoder.decode = mock.MagicMock(return_value=None) + # with pytest.raises(ModbusIOException): + # self._tls._process(mock_callback) + + # result = MockResult(0x01) + # self._tls.decoder.decode = mock.MagicMock(return_value=result) + # with pytest.raises(InvalidMessageReceivedException): + # self._tls._process( + # mock_callback, error=True + # ) + # self._tls._process(mock_callback) + # assert not self._tls._buffer + + def test_framer_tls_framer_populate(self): + """Test a tls frame packet build.""" + count = 0 + result = None + def callback(data): + """Simulate callback.""" + nonlocal count, result + count += 1 + result = data + + msg = b"\x00\x01\x12\x34\x00\x06\xff\x02\x12\x34\x01\x02" + self._tcp.processIncomingPacket(msg, callback, [0, 1]) + assert result + + def test_framer_tls_framer_packet(self): + """Test a tls frame packet build.""" + old_encode = ModbusRequest.encode + ModbusRequest.encode = lambda self: b"" + message = ModbusRequest() + message.function_code = 0x01 + expected = b"\x01" + actual = self._tls.buildPacket(message) + assert expected == actual + ModbusRequest.encode = old_encode + + # ----------------------------------------------------------------------- # + # RTU tests + # ----------------------------------------------------------------------- # + def test_rtu_framer_transaction_ready(self): + """Test if the checks for a complete frame work.""" + count = 0 + result = None + def callback(data): + """Simulate callback.""" + nonlocal count, result + count += 1 + result = data + + msg_parts = [b"\x00\x01\x00", b"\x00\x00\x01\xfc\x1b"] + self._rtu.processIncomingPacket(msg_parts[0], callback, [0, 1]) + assert not result + self._rtu.processIncomingPacket(msg_parts[1], callback, [0, 1]) + assert result + + def test_rtu_framer_transaction_full(self): + """Test a full rtu frame transaction.""" + count = 0 + result = None + def callback(data): + """Simulate callback.""" + nonlocal count, result + count += 1 + result = data + + msg = b"\x00\x01\x00\x00\x00\x01\xfc\x1b" + self._rtu.processIncomingPacket(msg, callback, [0, 1]) + assert result + + def test_rtu_framer_transaction_half(self): + """Test a half completed rtu frame transaction.""" + count = 0 + result = None + def callback(data): + """Simulate callback.""" + nonlocal count, result + count += 1 + result = data + + msg_parts = [b"\x00\x01\x00", b"\x00\x00\x01\xfc\x1b"] + self._rtu.processIncomingPacket(msg_parts[0], callback, [0, 1]) + assert not result + self._rtu.processIncomingPacket(msg_parts[1], callback, [0, 1]) + assert result + + def test_rtu_framer_populate(self): + """Test a rtu frame packet build.""" + count = 0 + result = None + def callback(data): + """Simulate callback.""" + nonlocal count, result + count += 1 + result = data + + msg = b"\x00\x01\x00\x00\x00\x01\xfc\x1b" + self._rtu.processIncomingPacket(msg, callback, [0, 1]) + header_dict = self._rtu._header # pylint: disable=protected-access + assert len(msg) == header_dict["len"] + assert int(msg[0]) == header_dict["uid"] + assert msg[-2:] == header_dict["crc"] + + def test_rtu_framer_packet(self): + """Test a rtu frame packet build.""" + old_encode = ModbusRequest.encode + ModbusRequest.encode = lambda self: b"" + message = ModbusRequest() + message.slave_id = 0xFF + message.function_code = 0x01 + expected = b"\xff\x01\x81\x80" # only header + CRC - no data + actual = self._rtu.buildPacket(message) + assert expected == actual + ModbusRequest.encode = old_encode + + def test_rtu_decode_exception(self): + """Test that the RTU framer can decode errors.""" + count = 0 + result = None + def callback(data): + """Simulate callback.""" + nonlocal count, result + count += 1 + result = data + + msg = b"\x00\x90\x02\x9c\x01" + self._rtu.processIncomingPacket(msg, callback, [0, 1]) + assert result + + def test_process(self): + """Test process.""" + count = 0 + result = None + def callback(data): + """Simulate callback.""" + nonlocal count, result + count += 1 + result = data + + msg = b"\x00\x01\x00\x00\x00\x01\xfc\x1b" + self._rtu.processIncomingPacket(msg, callback, [0, 1]) + assert result + + def test_rtu_process_incoming_packets(self): + """Test rtu process incoming packets.""" + count = 0 + result = None + def callback(data): + """Simulate callback.""" + nonlocal count, result + count += 1 + result = data + + msg = b"\x00\x01\x00\x00\x00\x01\xfc\x1b" + slave = 0x00 + + self._rtu.processIncomingPacket(msg, callback, slave) + assert result + + # ----------------------------------------------------------------------- # + # ASCII tests + # ----------------------------------------------------------------------- # + def test_ascii_framer_transaction_ready(self): + """Test a ascii frame transaction.""" + count = 0 + result = None + def callback(data): + """Simulate callback.""" + nonlocal count, result + count += 1 + result = data + + msg = b":F7031389000A60\r\n" + self._ascii.processIncomingPacket(msg, callback, [0,1]) + assert result + + def test_ascii_framer_transaction_full(self): + """Test a full ascii frame transaction.""" + count = 0 + result = None + def callback(data): + """Simulate callback.""" + nonlocal count, result + count += 1 + result = data + + msg = b"sss:F7031389000A60\r\n" + self._ascii.processIncomingPacket(msg, callback, [0,1]) + assert result + + def test_ascii_framer_transaction_half(self): + """Test a half completed ascii frame transaction.""" + count = 0 + result = None + def callback(data): + """Simulate callback.""" + nonlocal count, result + count += 1 + result = data + + msg_parts = (b"sss:F7031389", b"000A60\r\n") + self._ascii.processIncomingPacket(msg_parts[0], callback, [0,1]) + assert not result + self._ascii.processIncomingPacket(msg_parts[1], callback, [0,1]) + assert result + + def test_ascii_framer_populate(self): + """Test a ascii frame packet build.""" + request = ModbusRequest() + self._ascii.populateResult(request) + assert not request.slave_id + + def test_ascii_framer_packet(self): + """Test a ascii frame packet build.""" + old_encode = ModbusRequest.encode + ModbusRequest.encode = lambda self: b"" + message = ModbusRequest() + message.slave_id = 0xFF + message.function_code = 0x01 + expected = b":FF0100\r\n" + actual = self._ascii.buildPacket(message) + assert expected == actual + ModbusRequest.encode = old_encode + + def test_ascii_process_incoming_packets(self): + """Test ascii process incoming packet.""" + count = 0 + result = None + def callback(data): + """Simulate callback.""" + nonlocal count, result + count += 1 + result = data + + msg = b":F7031389000A60\r\n" + self._ascii.processIncomingPacket(msg, callback, [0,1]) + assert result diff --git a/test/message/test_tls.py b/test/framers/test_tls.py similarity index 87% rename from test/message/test_tls.py rename to test/framers/test_tls.py index 194fda459..50ccce800 100644 --- a/test/message/test_tls.py +++ b/test/framers/test_tls.py @@ -1,18 +1,18 @@ -"""Test transport.""" +"""Test framer.""" import pytest -from pymodbus.message.tls import MessageTLS +from pymodbus.framer.tls import FramerTLS -class TestMessageSocket: - """Test message module.""" +class TestMFramerTLS: + """Test module.""" @staticmethod @pytest.fixture(name="frame") def prepare_frame(): """Return message object.""" - return MessageTLS() + return FramerTLS() @pytest.mark.parametrize( diff --git a/test/sub_client/test_client.py b/test/sub_client/test_client.py index 43c858cfb..f62cf29c9 100755 --- a/test/sub_client/test_client.py +++ b/test/sub_client/test_client.py @@ -15,7 +15,7 @@ import pymodbus.register_read_message as pdu_reg_read import pymodbus.register_write_message as pdu_req_write from examples.helper import get_certificate -from pymodbus import Framer +from pymodbus import FramerType from pymodbus.client.base import ModbusBaseClient from pymodbus.client.mixin import ModbusClientMixin from pymodbus.datastore import ModbusSlaveContext @@ -141,7 +141,7 @@ def fake_execute(_self, request): "serial": { "pos_arg": "/dev/tty", "opt_args": { - "framer": Framer.ASCII, + "framer": FramerType.ASCII, "baudrate": 19200 + 500, "bytesize": 8 - 1, "parity": "E", @@ -151,7 +151,7 @@ def fake_execute(_self, request): "defaults": { "host": None, "port": "/dev/tty", - "framer": Framer.RTU, + "framer": FramerType.RTU, "baudrate": 19200, "bytesize": 8, "parity": "N", @@ -163,13 +163,13 @@ def fake_execute(_self, request): "pos_arg": "192.168.1.2", "opt_args": { "port": 112, - "framer": Framer.ASCII, + "framer": FramerType.ASCII, "source_address": ("195.6.7.8", 1025), }, "defaults": { "host": "192.168.1.2", "port": 502, - "framer": Framer.SOCKET, + "framer": FramerType.SOCKET, "source_address": None, }, }, @@ -177,7 +177,7 @@ def fake_execute(_self, request): "pos_arg": "192.168.1.2", "opt_args": { "port": 211, - "framer": Framer.ASCII, + "framer": FramerType.ASCII, "source_address": ("195.6.7.8", 1025), "sslctx": None, "certfile": None, @@ -187,7 +187,7 @@ def fake_execute(_self, request): "defaults": { "host": "192.168.1.2", "port": 802, - "framer": Framer.TLS, + "framer": FramerType.TLS, "source_address": None, "sslctx": None, "certfile": None, @@ -199,13 +199,13 @@ def fake_execute(_self, request): "pos_arg": "192.168.1.2", "opt_args": { "port": 121, - "framer": Framer.ASCII, + "framer": FramerType.ASCII, "source_address": ("195.6.7.8", 1025), }, "defaults": { "host": "192.168.1.2", "port": 502, - "framer": Framer.SOCKET, + "framer": FramerType.SOCKET, "source_address": None, }, }, @@ -277,7 +277,7 @@ async def test_serial_not_installed(): async def test_client_modbusbaseclient(): """Test modbus base client class.""" client = ModbusBaseClient( - Framer.ASCII, + FramerType.ASCII, host="localhost", port=BASE_PORT + 1, CommType=CommType.TCP, @@ -312,7 +312,7 @@ async def test_client_base_async(): p_close.return_value = asyncio.Future() p_close.return_value.set_result(True) async with ModbusBaseClient( - Framer.ASCII, + FramerType.ASCII, host="localhost", port=BASE_PORT + 2, CommType=CommType.TCP, @@ -327,7 +327,7 @@ async def test_client_base_async(): @pytest.mark.skip() async def test_client_protocol_receiver(): """Test the client protocol data received.""" - base = ModbusBaseClient(Framer.SOCKET) + base = ModbusBaseClient(FramerType.SOCKET) transport = mock.MagicMock() base.connection_made(transport) assert base.transport == transport @@ -349,7 +349,7 @@ async def test_client_protocol_receiver(): @pytest.mark.skip() async def test_client_protocol_response(): """Test the udp client protocol builds responses.""" - base = ModbusBaseClient(Framer.SOCKET) + base = ModbusBaseClient(FramerType.SOCKET) response = base.build_response(0x00) # pylint: disable=protected-access excp = response.exception() assert isinstance(excp, ConnectionException) @@ -363,7 +363,7 @@ async def test_client_protocol_response(): async def test_client_protocol_handler(): """Test the client protocol handles responses.""" base = ModbusBaseClient( - Framer.ASCII, host="localhost", port=+3, CommType=CommType.TCP + FramerType.ASCII, host="localhost", port=+3, CommType=CommType.TCP ) transport = mock.MagicMock() base.connection_made(transport=transport) @@ -411,7 +411,7 @@ def close(self): async def test_client_protocol_execute(): """Test the client protocol execute method.""" - base = ModbusBaseClient(Framer.SOCKET, host="127.0.0.1") + base = ModbusBaseClient(FramerType.SOCKET, host="127.0.0.1") request = pdu_bit_read.ReadCoilsRequest(1, 1) transport = MockTransport(base, request) base.connection_made(transport=transport) @@ -422,7 +422,7 @@ async def test_client_protocol_execute(): async def test_client_execute_broadcast(): """Test the client protocol execute method.""" - base = ModbusBaseClient(Framer.SOCKET, host="127.0.0.1") + base = ModbusBaseClient(FramerType.SOCKET, host="127.0.0.1") base.broadcast_enable = True request = pdu_bit_read.ReadCoilsRequest(1, 1) transport = MockTransport(base, request) @@ -432,7 +432,7 @@ async def test_client_execute_broadcast(): async def test_client_protocol_retry(): """Test the client protocol execute method with retries.""" - base = ModbusBaseClient(Framer.SOCKET, host="127.0.0.1", timeout=0.1) + base = ModbusBaseClient(FramerType.SOCKET, host="127.0.0.1", timeout=0.1) request = pdu_bit_read.ReadCoilsRequest(1, 1) transport = MockTransport(base, request, retries=2) base.connection_made(transport=transport) @@ -445,7 +445,7 @@ async def test_client_protocol_retry(): async def test_client_protocol_timeout(): """Test the client protocol execute method with timeout.""" - base = ModbusBaseClient(Framer.SOCKET, host="127.0.0.1", timeout=0.1, retries=2) + base = ModbusBaseClient(FramerType.SOCKET, host="127.0.0.1", timeout=0.1, retries=2) # Avoid creating do_reconnect() task base.connection_lost = mock.MagicMock() request = pdu_bit_read.ReadCoilsRequest(1, 1) @@ -611,7 +611,7 @@ def test_client_mixin_convert_fail(): async def test_client_build_response(): """Test fail of build_response.""" - client = ModbusBaseClient(Framer.RTU) + client = ModbusBaseClient(FramerType.RTU) with pytest.raises(ConnectionException): await client.build_response(0) diff --git a/test/sub_client/test_client_sync.py b/test/sub_client/test_client_sync.py index 1782c8575..4be8bdba2 100755 --- a/test/sub_client/test_client_sync.py +++ b/test/sub_client/test_client_sync.py @@ -7,7 +7,7 @@ import pytest import serial -from pymodbus import Framer +from pymodbus import FramerType from pymodbus.client import ( ModbusSerialClient, ModbusTcpClient, @@ -306,23 +306,23 @@ def test_sync_serial_client_instantiation(self): client = ModbusSerialClient("/dev/null") assert client assert isinstance( - ModbusSerialClient("/dev/null", framer=Framer.ASCII).framer, + ModbusSerialClient("/dev/null", framer=FramerType.ASCII).framer, ModbusAsciiFramer, ) assert isinstance( - ModbusSerialClient("/dev/null", framer=Framer.RTU).framer, + ModbusSerialClient("/dev/null", framer=FramerType.RTU).framer, ModbusRtuFramer, ) assert isinstance( - ModbusSerialClient("/dev/null", framer=Framer.SOCKET).framer, + ModbusSerialClient("/dev/null", framer=FramerType.SOCKET).framer, ModbusSocketFramer, ) def test_sync_serial_rtu_client_timeouts(self): """Test sync serial rtu.""" - client = ModbusSerialClient("/dev/null", framer=Framer.RTU, baudrate=9600) + client = ModbusSerialClient("/dev/null", framer=FramerType.RTU, baudrate=9600) assert client.silent_interval == round((3.5 * 10 / 9600), 6) - client = ModbusSerialClient("/dev/null", framer=Framer.RTU, baudrate=38400) + client = ModbusSerialClient("/dev/null", framer=FramerType.RTU, baudrate=38400) assert client.silent_interval == round((1.75 / 1000), 6) @mock.patch("serial.Serial") @@ -347,7 +347,7 @@ def test_basic_sync_serial_client(self, mock_serial): client.close() # rtu connect/disconnect - rtu_client = ModbusSerialClient("/dev/null", framer=Framer.RTU, strict=True) + rtu_client = ModbusSerialClient("/dev/null", framer=FramerType.RTU, strict=True) assert rtu_client.connect() assert rtu_client.socket.inter_byte_timeout == rtu_client.inter_byte_timeout rtu_client.close() diff --git a/test/sub_server/test_server_asyncio.py b/test/sub_server/test_server_asyncio.py index 9ae9b6af8..34c16616b 100755 --- a/test/sub_server/test_server_asyncio.py +++ b/test/sub_server/test_server_asyncio.py @@ -8,7 +8,7 @@ import pytest -from pymodbus import Framer +from pymodbus import FramerType from pymodbus.datastore import ( ModbusSequentialDataBlock, ModbusServerContext, @@ -154,15 +154,15 @@ async def start_server( args["identity"] = self.identity if do_tls: self.server = ModbusTlsServer( - self.context, Framer.TLS, self.identity, SERV_ADDR + self.context, FramerType.TLS, self.identity, SERV_ADDR ) elif do_udp: self.server = ModbusUdpServer( - self.context, Framer.SOCKET, self.identity, SERV_ADDR + self.context, FramerType.SOCKET, self.identity, SERV_ADDR ) else: self.server = ModbusTcpServer( - self.context, Framer.SOCKET, self.identity, SERV_ADDR + self.context, FramerType.SOCKET, self.identity, SERV_ADDR ) assert self.server if do_forever: diff --git a/test/sub_server/test_server_multidrop.py b/test/sub_server/test_server_multidrop.py deleted file mode 100644 index 74711ab5c..000000000 --- a/test/sub_server/test_server_multidrop.py +++ /dev/null @@ -1,174 +0,0 @@ -"""Test server working as slave on a multidrop RS485 line.""" -from unittest import mock - -import pytest - -from pymodbus.framer.rtu_framer import ModbusRtuFramer -from pymodbus.server.async_io import ServerDecoder - - -class TestMultidrop: - """Test that server works on a multidrop line.""" - - slaves = [2] - - good_frame = b"\x02\x03\x00\x01\x00}\xd4\x18" - - @pytest.fixture(name="framer") - def fixture_framer(self): - """Prepare framer.""" - return ModbusRtuFramer(ServerDecoder()) - - @pytest.fixture(name="callback") - def fixture_callback(self): - """Prepare dummy callback.""" - return mock.Mock() - - def test_ok_frame(self, framer, callback): - """Test ok frame.""" - serial_event = self.good_frame - framer.processIncomingPacket(serial_event, callback, self.slaves) - callback.assert_called_once() - - def test_ok_2frame(self, framer, callback): - """Test ok frame.""" - serial_event = self.good_frame + self.good_frame - framer.processIncomingPacket(serial_event, callback, self.slaves) - assert callback.call_count == 2 - - def test_bad_crc(self, framer, callback): - """Test bad crc.""" - serial_event = b"\x02\x03\x00\x01\x00}\xd4\x19" # Manually mangled crc - framer.processIncomingPacket(serial_event, callback, self.slaves) - callback.assert_not_called() - - def test_wrong_id(self, framer, callback): - """Test frame wrong id.""" - serial_event = b"\x01\x03\x00\x01\x00}\xd4+" # Frame with good CRC but other id - framer.processIncomingPacket(serial_event, callback, self.slaves) - callback.assert_not_called() - - def test_big_split_response_frame_from_other_id(self, framer, callback): - """Test split response.""" - # This is a single *response* from device id 1 after being queried for 125 holding register values - # Because the response is so long it spans several serial events - serial_events = [ - b"\x01\x03\xfa\xc4y\xc0\x00\xc4y\xc0\x00\xc4y\xc0\x00\xc4y\xc0\x00\xc4y\xc0\x00Dz\x00\x00C\x96\x00\x00", - b"?\x05\x1e\xb8DH\x00\x00D\x96\x00\x00D\xfa\x00\x00DH\x00\x00D\x96\x00\x00D\xfa\x00\x00DH\x00", - b"\x00D\x96\x00\x00D\xfa\x00\x00B\x96\x00\x00B\xb4\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00", - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00", - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00", - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00", - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00", - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00", - b"\x00\x00\x00\x00\x00\x00\x00N,", - ] - for serial_event in serial_events: - framer.processIncomingPacket(serial_event, callback, self.slaves) - callback.assert_not_called() - - def test_split_frame(self, framer, callback): - """Test split frame.""" - serial_events = [self.good_frame[:5], self.good_frame[5:]] - for serial_event in serial_events: - framer.processIncomingPacket(serial_event, callback, self.slaves) - callback.assert_called_once() - - def test_complete_frame_trailing_data_without_id(self, framer, callback): - """Test trailing data.""" - garbage = b"\x05\x04\x03" # without id - serial_event = garbage + self.good_frame - framer.processIncomingPacket(serial_event, callback, self.slaves) - callback.assert_called_once() - - def test_complete_frame_trailing_data_with_id(self, framer, callback): - """Test trailing data.""" - garbage = b"\x05\x04\x03\x02\x01\x00" # with id - serial_event = garbage + self.good_frame - framer.processIncomingPacket(serial_event, callback, self.slaves) - callback.assert_called_once() - - def test_split_frame_trailing_data_with_id(self, framer, callback): - """Test split frame.""" - garbage = b"\x05\x04\x03\x02\x01\x00" - serial_events = [garbage + self.good_frame[:5], self.good_frame[5:]] - for serial_event in serial_events: - framer.processIncomingPacket(serial_event, callback, self.slaves) - callback.assert_called_once() - - def test_coincidental_1(self, framer, callback): - """Test conincidental.""" - garbage = b"\x02\x90\x07" - serial_events = [garbage, self.good_frame[:5], self.good_frame[5:]] - for serial_event in serial_events: - framer.processIncomingPacket(serial_event, callback, self.slaves) - callback.assert_called_once() - - def test_coincidental_2(self, framer, callback): - """Test conincidental.""" - garbage = b"\x02\x10\x07" - serial_events = [garbage, self.good_frame[:5], self.good_frame[5:]] - for serial_event in serial_events: - framer.processIncomingPacket(serial_event, callback, self.slaves) - callback.assert_called_once() - - def test_coincidental_3(self, framer, callback): - """Test conincidental.""" - garbage = b"\x02\x10\x07\x10" - serial_events = [garbage, self.good_frame[:5], self.good_frame[5:]] - for serial_event in serial_events: - framer.processIncomingPacket(serial_event, callback, self.slaves) - callback.assert_called_once() - - def test_wrapped_frame(self, framer, callback): - """Test wrapped frame.""" - garbage = b"\x05\x04\x03\x02\x01\x00" - serial_event = garbage + self.good_frame + garbage - framer.processIncomingPacket(serial_event, callback, self.slaves) - - # We probably should not respond in this case; in this case we've likely become desynchronized - # i.e. this probably represents a case where a command came for us, but we didn't get - # to the serial buffer in time (some other co-routine or perhaps a block on the USB bus) - # and the master moved on and queried another device - callback.assert_called_once() - - def test_frame_with_trailing_data(self, framer, callback): - """Test trailing data.""" - garbage = b"\x05\x04\x03\x02\x01\x00" - serial_event = self.good_frame + garbage - framer.processIncomingPacket(serial_event, callback, self.slaves) - - # We should not respond in this case for identical reasons as test_wrapped_frame - callback.assert_called_once() - - def test_getFrameStart(self, framer): - """Test getFrameStart.""" - result = None - count = 0 - def test_callback(data): - """Check callback.""" - nonlocal result, count - count += 1 - result = data.function_code.to_bytes(1,'big')+data.encode() - - framer_ok = b"\x02\x03\x00\x01\x00\x7d\xd4\x18" - framer.processIncomingPacket(framer_ok, test_callback, self.slaves) - assert framer_ok[1:-2] == result - - count = 0 - framer_2ok = framer_ok + framer_ok - framer.processIncomingPacket(framer_2ok, test_callback, self.slaves) - assert count == 2 - assert not framer._buffer # pylint: disable=protected-access - - framer._buffer = framer_ok[:2] # pylint: disable=protected-access - framer.processIncomingPacket(b'', test_callback, self.slaves) - assert framer_ok[:2] == framer._buffer # pylint: disable=protected-access - - framer._buffer = framer_ok[:3] # pylint: disable=protected-access - framer.processIncomingPacket(b'', test_callback, self.slaves) - assert framer_ok[:3] == framer._buffer # pylint: disable=protected-access - - framer_ok = b"\xF0\x03\x00\x01\x00}\xd4\x18" - framer.processIncomingPacket(framer_ok, test_callback, self.slaves) - assert framer._buffer == framer_ok[-3:] # pylint: disable=protected-access diff --git a/test/test_transaction.py b/test/test_transaction.py index 0e237554e..b072909f8 100755 --- a/test/test_transaction.py +++ b/test/test_transaction.py @@ -242,6 +242,7 @@ def callback(data): msg = b"\x00\x01\x12\x34\x00\x06\xff\x02\x01\x02\x00\x08" self._tcp.processIncomingPacket(msg, callback, [1]) self._tcp._buffer = msg # pylint: disable=protected-access + callback(b'') def test_tcp_framer_transaction_full(self): """Test a full tcp frame transaction.""" From 9e9e50e247d4d1bd69fbd2f09b02b00ee5a4742f Mon Sep 17 00:00:00 2001 From: jan iversen Date: Sat, 30 Mar 2024 14:03:10 +0100 Subject: [PATCH 23/88] Dislodge client classes from modbusProtocol. (#2137) --- examples/package_test_tool.py | 2 +- pymodbus/client/base.py | 101 ++++++++---------------- pymodbus/client/modbusclientprotocol.py | 91 +++++++++++++++++++++ pymodbus/client/serial.py | 10 +-- pymodbus/client/tcp.py | 14 +--- pymodbus/client/tls.py | 10 --- pymodbus/client/udp.py | 22 +----- pymodbus/framer/old_framer_rtu.py | 5 +- test/framers/test_old_framers.py | 2 +- test/sub_client/test_client.py | 28 +++---- test/test_network.py | 2 +- 11 files changed, 147 insertions(+), 140 deletions(-) create mode 100644 pymodbus/client/modbusclientprotocol.py diff --git a/examples/package_test_tool.py b/examples/package_test_tool.py index e46433863..142379f17 100755 --- a/examples/package_test_tool.py +++ b/examples/package_test_tool.py @@ -122,7 +122,7 @@ def __init__(self, comm: CommType): ) else: raise RuntimeError("ERROR: CommType not implemented") - server_params = self.client.comm_params.copy() + server_params = self.client.ctx.comm_params.copy() server_params.source_address = (host, test_port) self.stub = TransportStub(server_params, True, simulate_server) test_port += 1 diff --git a/pymodbus/client/base.py b/pymodbus/client/base.py index cefd2060d..0a9a6f6c5 100644 --- a/pymodbus/client/base.py +++ b/pymodbus/client/base.py @@ -8,17 +8,18 @@ from typing import Any, cast from pymodbus.client.mixin import ModbusClientMixin +from pymodbus.client.modbusclientprotocol import ModbusClientProtocol from pymodbus.exceptions import ConnectionException, ModbusIOException from pymodbus.factory import ClientDecoder from pymodbus.framer import FRAMER_NAME_TO_CLASS, FramerType, ModbusFramer from pymodbus.logging import Log from pymodbus.pdu import ModbusRequest, ModbusResponse from pymodbus.transaction import ModbusTransactionManager -from pymodbus.transport import CommParams, ModbusProtocol +from pymodbus.transport import CommParams from pymodbus.utilities import ModbusTransactionState -class ModbusBaseClient(ModbusClientMixin[Awaitable[ModbusResponse]], ModbusProtocol): +class ModbusBaseClient(ModbusClientMixin[Awaitable[ModbusResponse]]): """**ModbusBaseClient**. Fixed parameters: @@ -62,8 +63,8 @@ def __init__( ) -> None: """Initialize a client instance.""" ModbusClientMixin.__init__(self) # type: ignore[arg-type] - ModbusProtocol.__init__( - self, + self.ctx = ModbusClientProtocol( + framer, CommParams( comm_type=kwargs.get("CommType"), comm_name="comm", @@ -80,22 +81,15 @@ def __init__( stopbits=kwargs.get("stopbits", None), handle_local_echo=kwargs.get("handle_local_echo", False), ), - False, + retries, + retry_on_empty, + on_connect_callback, ) - self.on_connect_callback = on_connect_callback - self.retry_on_empty: int = 0 self.no_resend_on_retry = no_resend_on_retry - self.slaves: list[int] = [] - self.retries: int = retries self.broadcast_enable = broadcast_enable + self.retries = retries # Common variables. - self.framer = FRAMER_NAME_TO_CLASS.get( - framer, cast(type[ModbusFramer], framer) - )(ClientDecoder(), self) - self.transaction = ModbusTransactionManager( - self, retries=retries, retry_on_empty=retry_on_empty, **kwargs - ) self.use_udp = False self.state = ModbusTransactionState.IDLE self.last_frame_end: float | None = 0 @@ -107,11 +101,17 @@ def __init__( @property def connected(self) -> bool: """Return state of connection.""" - return self.is_active() + return self.ctx.is_active() - async def base_connect(self) -> bool: + async def connect(self) -> bool: """Call transport connect.""" - return await super().connect() + self.ctx.reset_delay() + Log.debug( + "Connecting to {}:{}.", + self.ctx.comm_params.host, + self.ctx.comm_params.port, + ) + return await self.ctx.connect() def register(self, custom_response_class: ModbusResponse) -> None: @@ -123,14 +123,14 @@ def register(self, custom_response_class: ModbusResponse) -> None: Use register() to add non-standard responses (like e.g. a login prompt) and have them interpreted automatically. """ - self.framer.decoder.register(custom_response_class) + self.ctx.framer.decoder.register(custom_response_class) def close(self, reconnect: bool = False) -> None: """Close connection.""" if reconnect: - self.connection_lost(asyncio.TimeoutError("Server not responding")) + self.ctx.connection_lost(asyncio.TimeoutError("Server not responding")) else: - super().close() + self.ctx.close() def idle_time(self) -> float: """Time before initiating next transaction (call **sync**). @@ -149,7 +149,7 @@ def execute(self, request: ModbusRequest | None = None): :returns: The result of the request execution :raises ConnectionException: Check exception text. """ - if not self.transport: + if not self.ctx.transport: raise ConnectionException(f"Not connected[{self!s}]") return self.async_execute(request) @@ -158,20 +158,20 @@ def execute(self, request: ModbusRequest | None = None): # ----------------------------------------------------------------------- # async def async_execute(self, request) -> ModbusResponse: """Execute requests asynchronously.""" - request.transaction_id = self.transaction.getNextTID() - packet = self.framer.buildPacket(request) + request.transaction_id = self.ctx.transaction.getNextTID() + packet = self.ctx.framer.buildPacket(request) count = 0 while count <= self.retries: req = self.build_response(request.transaction_id) if not count or not self.no_resend_on_retry: - self.send(packet) + self.ctx.send(packet) if self.broadcast_enable and not request.slave_id: resp = None break try: resp = await asyncio.wait_for( - req, timeout=self.comm_params.timeout_connect + req, timeout=self.ctx.comm_params.timeout_connect ) break except asyncio.exceptions.TimeoutError: @@ -184,53 +184,14 @@ async def async_execute(self, request) -> ModbusResponse: return resp # type: ignore[return-value] - def callback_new_connection(self): - """Call when listener receive new connection request.""" - - def callback_connected(self) -> None: - """Call when connection is succcesfull.""" - if self.on_connect_callback: - self.loop.call_soon(self.on_connect_callback, True) - - def callback_disconnected(self, exc: Exception | None) -> None: - """Call when connection is lost.""" - Log.debug("callback_disconnected called: {}", exc) - if self.on_connect_callback: - self.loop.call_soon(self.on_connect_callback, False) - - def callback_data(self, data: bytes, addr: tuple | None = None) -> int: - """Handle received data. - - returns number of bytes consumed - """ - self.framer.processIncomingPacket(data, self._handle_response, slave=0) - return len(data) - - async def connect(self) -> bool: # type: ignore[empty-body] - """Connect to the modbus remote host.""" - - def raise_future(self, my_future, exc): - """Set exception of a future if not done.""" - if not my_future.done(): - my_future.set_exception(exc) - - def _handle_response(self, reply, **_kwargs): - """Handle the processed response and link to correct deferred.""" - if reply is not None: - tid = reply.transaction_id - if handler := self.transaction.getTransaction(tid): - if not handler.done(): - handler.set_result(reply) - else: - Log.debug("Unrequested message: {}", reply, ":str") - def build_response(self, tid): """Return a deferred response for the current request.""" my_future: asyncio.Future = asyncio.Future() - if not self.transport: - self.raise_future(my_future, ConnectionException("Client is not connected")) + if not self.ctx.transport: + if not my_future.done(): + my_future.set_exception(ConnectionException("Client is not connected")) else: - self.transaction.addTransaction(my_future, tid) + self.ctx.transaction.addTransaction(my_future, tid) return my_future # ----------------------------------------------------------------------- # @@ -264,7 +225,7 @@ def __str__(self): :returns: The string representation """ return ( - f"{self.__class__.__name__} {self.comm_params.host}:{self.comm_params.port}" + f"{self.__class__.__name__} {self.ctx.comm_params.host}:{self.ctx.comm_params.port}" ) diff --git a/pymodbus/client/modbusclientprotocol.py b/pymodbus/client/modbusclientprotocol.py new file mode 100644 index 000000000..929d6bd45 --- /dev/null +++ b/pymodbus/client/modbusclientprotocol.py @@ -0,0 +1,91 @@ +"""ModbusProtocol implementation for all clients.""" +from __future__ import annotations + +from collections.abc import Callable +from typing import cast + +from pymodbus.factory import ClientDecoder +from pymodbus.framer import FRAMER_NAME_TO_CLASS, FramerType, ModbusFramer +from pymodbus.logging import Log +from pymodbus.transaction import ModbusTransactionManager +from pymodbus.transport import CommParams, ModbusProtocol + + +class ModbusClientProtocol(ModbusProtocol): + """**ModbusClientProtocol**. + + Fixed parameters: + + :param framer: Framer enum name + :param params: Comm parameters for transport + :param retries: Max number of retries per request. + :param retry_on_empty: Retry on empty response. + :param on_connect_callback: Will be called when connected/disconnected (bool parameter) + + :mod:`ModbusClientProtocol` is normally not referenced outside :mod:`pymodbus`. + """ + + def __init__( + self, + framer: FramerType, + params: CommParams, + retries: int, + retry_on_empty: bool, + on_connect_callback: Callable[[bool], None] | None = None, + ) -> None: + """Initialize a client instance.""" + ModbusProtocol.__init__( + self, + params, + False, + ) + self.on_connect_callback = on_connect_callback + + # Common variables. + self.framer = FRAMER_NAME_TO_CLASS.get( + framer, cast(type[ModbusFramer], framer) + )(ClientDecoder(), self) + self.transaction = ModbusTransactionManager( + self, retries=retries, retry_on_empty=retry_on_empty + ) + + def _handle_response(self, reply, **_kwargs): + """Handle the processed response and link to correct deferred.""" + if reply is not None: + tid = reply.transaction_id + if handler := self.transaction.getTransaction(tid): + if not handler.done(): + handler.set_result(reply) + else: + Log.debug("Unrequested message: {}", reply, ":str") + + def callback_new_connection(self): + """Call when listener receive new connection request.""" + + def callback_connected(self) -> None: + """Call when connection is succcesfull.""" + if self.on_connect_callback: + self.loop.call_soon(self.on_connect_callback, True) + + def callback_disconnected(self, exc: Exception | None) -> None: + """Call when connection is lost.""" + Log.debug("callback_disconnected called: {}", exc) + if self.on_connect_callback: + self.loop.call_soon(self.on_connect_callback, False) + + def callback_data(self, data: bytes, addr: tuple | None = None) -> int: + """Handle received data. + + returns number of bytes consumed + """ + self.framer.processIncomingPacket(data, self._handle_response, slave=0) + return len(data) + + def __str__(self): + """Build a string representation of the connection. + + :returns: The string representation + """ + return ( + f"{self.__class__.__name__} {self.comm_params.host}:{self.comm_params.port}" + ) diff --git a/pymodbus/client/serial.py b/pymodbus/client/serial.py index 216a6308a..0fb4c6034 100644 --- a/pymodbus/client/serial.py +++ b/pymodbus/client/serial.py @@ -1,7 +1,6 @@ """Modbus client async serial communication.""" from __future__ import annotations -import asyncio import time from functools import partial from typing import TYPE_CHECKING, Any @@ -24,7 +23,7 @@ # type checkers do not understand the Raise RuntimeError in __init__() import serial -class AsyncModbusSerialClient(ModbusBaseClient, asyncio.Protocol): +class AsyncModbusSerialClient(ModbusBaseClient): """**AsyncModbusSerialClient**. Fixed parameters: @@ -82,7 +81,6 @@ def __init__( "Serial client requires pyserial " 'Please install with "pip install pyserial" and try again.' ) - asyncio.Protocol.__init__(self) ModbusBaseClient.__init__( self, framer, @@ -95,12 +93,6 @@ def __init__( **kwargs, ) - async def connect(self) -> bool: - """Connect Async client.""" - self.reset_delay() - Log.debug("Connecting to {}.", self.comm_params.host) - return await self.base_connect() - def close(self, reconnect: bool = False) -> None: """Close connection.""" super().close(reconnect=reconnect) diff --git a/pymodbus/client/tcp.py b/pymodbus/client/tcp.py index 1684cfb5f..aa71c9e40 100644 --- a/pymodbus/client/tcp.py +++ b/pymodbus/client/tcp.py @@ -1,7 +1,6 @@ """Modbus client async TCP communication.""" from __future__ import annotations -import asyncio import select import socket import time @@ -14,7 +13,7 @@ from pymodbus.transport import CommType -class AsyncModbusTcpClient(ModbusBaseClient, asyncio.Protocol): +class AsyncModbusTcpClient(ModbusBaseClient): """**AsyncModbusTcpClient**. Fixed parameters: @@ -64,7 +63,6 @@ def __init__( **kwargs: Any, ) -> None: """Initialize Asyncio Modbus TCP Client.""" - asyncio.Protocol.__init__(self) if "CommType" not in kwargs: kwargs["CommType"] = CommType.TCP if source_address: @@ -77,16 +75,6 @@ def __init__( **kwargs, ) - async def connect(self) -> bool: - """Initiate connection to start client.""" - self.reset_delay() - Log.debug( - "Connecting to {}:{}.", - self.comm_params.host, - self.comm_params.port, - ) - return await self.base_connect() - def close(self, reconnect: bool = False) -> None: """Close connection.""" super().close(reconnect=reconnect) diff --git a/pymodbus/client/tls.py b/pymodbus/client/tls.py index d7adb9861..e8bfce508 100644 --- a/pymodbus/client/tls.py +++ b/pymodbus/client/tls.py @@ -73,16 +73,6 @@ def __init__( ) self.server_hostname = server_hostname - async def connect(self) -> bool: - """Initiate connection to start client.""" - self.reset_delay() - Log.debug( - "Connecting to {}:{}.", - self.comm_params.host, - self.comm_params.port, - ) - return await self.base_connect() - @classmethod def generate_ssl( cls, diff --git a/pymodbus/client/udp.py b/pymodbus/client/udp.py index 48cb8bcc1..4f2f3d2df 100644 --- a/pymodbus/client/udp.py +++ b/pymodbus/client/udp.py @@ -1,7 +1,6 @@ """Modbus client async UDP communication.""" from __future__ import annotations -import asyncio import socket from typing import Any @@ -15,9 +14,7 @@ DGRAM_TYPE = socket.SOCK_DGRAM -class AsyncModbusUdpClient( - ModbusBaseClient, asyncio.Protocol, asyncio.DatagramProtocol -): +class AsyncModbusUdpClient(ModbusBaseClient): """**AsyncModbusUdpClient**. Fixed parameters: @@ -65,8 +62,6 @@ def __init__( **kwargs: Any, ) -> None: """Initialize Asyncio Modbus UDP Client.""" - asyncio.DatagramProtocol.__init__(self) - asyncio.Protocol.__init__(self) ModbusBaseClient.__init__( self, framer, @@ -80,20 +75,7 @@ def __init__( @property def connected(self): """Return true if connected.""" - return self.is_active() - - async def connect(self) -> bool: - """Start reconnecting asynchronous udp client. - - :meta private: - """ - self.reset_delay() - Log.debug( - "Connecting to {}:{}.", - self.comm_params.host, - self.comm_params.port, - ) - return await self.base_connect() + return self.ctx.is_active() class ModbusUdpClient(ModbusBaseSyncClient): diff --git a/pymodbus/framer/old_framer_rtu.py b/pymodbus/framer/old_framer_rtu.py index 0f43e2501..b4fedae7c 100644 --- a/pymodbus/framer/old_framer_rtu.py +++ b/pymodbus/framer/old_framer_rtu.py @@ -186,7 +186,10 @@ def sendPacket(self, message): """ super().resetFrame() start = time.time() - timeout = start + self.client.comm_params.timeout_connect + if hasattr(self.client,"ctx"): + timeout = start + self.client.ctx.comm_params.timeout_connect + else: + timeout = start + self.client.comm_params.timeout_connect while self.client.state != ModbusTransactionState.IDLE: if self.client.state == ModbusTransactionState.TRANSACTION_COMPLETE: timestamp = round(time.time(), 6) diff --git a/test/framers/test_old_framers.py b/test/framers/test_old_framers.py index 4059ca766..14ecedd31 100644 --- a/test/framers/test_old_framers.py +++ b/test/framers/test_old_framers.py @@ -336,7 +336,7 @@ async def test_send_packet(self, rtu_framer): client.state = ModbusTransactionState.TRANSACTION_COMPLETE client.silent_interval = 1 client.last_frame_end = 1 - client.comm_params.timeout_connect = 0.25 + client.ctx.comm_params.timeout_connect = 0.25 client.idle_time = mock.Mock(return_value=1) client.send = mock.Mock(return_value=len(message)) rtu_framer.client = client diff --git a/test/sub_client/test_client.py b/test/sub_client/test_client.py index f62cf29c9..40fc0e86f 100755 --- a/test/sub_client/test_client.py +++ b/test/sub_client/test_client.py @@ -294,7 +294,7 @@ async def test_client_connection_made(): transport = mock.AsyncMock() transport.close = lambda : () - client.connection_made(transport) + client.ctx.connection_made(transport) # assert await client.connected client.close() @@ -329,7 +329,7 @@ async def test_client_protocol_receiver(): """Test the client protocol data received.""" base = ModbusBaseClient(FramerType.SOCKET) transport = mock.MagicMock() - base.connection_made(transport) + base.ctx.connection_made(transport) assert base.transport == transport assert base.transport data = b"\x00\x00\x12\x34\x00\x06\xff\x01\x01\x02\x00\x04" @@ -337,7 +337,7 @@ async def test_client_protocol_receiver(): # setup existing request assert not list(base.transaction) response = base.build_response(0x00) # pylint: disable=protected-access - base.data_received(data) + base.ctx.data_received(data) result = response.result() assert isinstance(result, pdu_bit_read.ReadCoilsResponse) @@ -366,13 +366,13 @@ async def test_client_protocol_handler(): FramerType.ASCII, host="localhost", port=+3, CommType=CommType.TCP ) transport = mock.MagicMock() - base.connection_made(transport=transport) + base.ctx.connection_made(transport=transport) reply = pdu_bit_read.ReadCoilsRequest(1, 1) reply.transaction_id = 0x00 - base._handle_response(None) # pylint: disable=protected-access - base._handle_response(reply) # pylint: disable=protected-access + base.ctx._handle_response(None) # pylint: disable=protected-access + base.ctx._handle_response(reply) # pylint: disable=protected-access response = base.build_response(0x00) # pylint: disable=protected-access - base._handle_response(reply) # pylint: disable=protected-access + base.ctx._handle_response(reply) # pylint: disable=protected-access result = response.result() assert result == reply @@ -393,8 +393,8 @@ async def delayed_resp(self): """Send a response to a received packet.""" await asyncio.sleep(0.05) resp = self.req.execute(self.ctx) - pkt = self.base.framer.buildPacket(resp) - self.base.data_received(pkt) + pkt = self.base.ctx.framer.buildPacket(resp) + self.base.ctx.data_received(pkt) def write(self, data, addr=None): """Write data to the transport, start a task to send the response.""" @@ -414,7 +414,7 @@ async def test_client_protocol_execute(): base = ModbusBaseClient(FramerType.SOCKET, host="127.0.0.1") request = pdu_bit_read.ReadCoilsRequest(1, 1) transport = MockTransport(base, request) - base.connection_made(transport=transport) + base.ctx.connection_made(transport=transport) response = await base.async_execute(request) assert not response.isError() @@ -426,7 +426,7 @@ async def test_client_execute_broadcast(): base.broadcast_enable = True request = pdu_bit_read.ReadCoilsRequest(1, 1) transport = MockTransport(base, request) - base.connection_made(transport=transport) + base.ctx.connection_made(transport=transport) assert not await base.async_execute(request) @@ -435,7 +435,7 @@ async def test_client_protocol_retry(): base = ModbusBaseClient(FramerType.SOCKET, host="127.0.0.1", timeout=0.1) request = pdu_bit_read.ReadCoilsRequest(1, 1) transport = MockTransport(base, request, retries=2) - base.connection_made(transport=transport) + base.ctx.connection_made(transport=transport) response = await base.async_execute(request) assert transport.retries == 0 @@ -447,10 +447,10 @@ async def test_client_protocol_timeout(): """Test the client protocol execute method with timeout.""" base = ModbusBaseClient(FramerType.SOCKET, host="127.0.0.1", timeout=0.1, retries=2) # Avoid creating do_reconnect() task - base.connection_lost = mock.MagicMock() + base.ctx.connection_lost = mock.MagicMock() request = pdu_bit_read.ReadCoilsRequest(1, 1) transport = MockTransport(base, request, retries=4) - base.connection_made(transport=transport) + base.ctx.connection_made(transport=transport) with pytest.raises(ModbusIOException): await base.async_execute(request) diff --git a/test/test_network.py b/test/test_network.py index bb2e032b2..24da82c67 100644 --- a/test/test_network.py +++ b/test/test_network.py @@ -74,7 +74,7 @@ async def test_stub(self, use_port, use_cls): assert await stub.start_run() assert await client.connect() test_data = b"Data got echoed." - client.transport.write(test_data) + client.ctx.transport.write(test_data) client.close() stub.close() From 6b728ec908e68c0c7f16d42fe4abee297c9f10b3 Mon Sep 17 00:00:00 2001 From: jan iversen Date: Mon, 1 Apr 2024 13:05:34 +0200 Subject: [PATCH 24/88] streamline imports in Factory.py (#2140) --- pymodbus/factory.py | 264 +++++++++++++++----------------------------- 1 file changed, 89 insertions(+), 175 deletions(-) diff --git a/pymodbus/factory.py b/pymodbus/factory.py index c75766ed9..c351fa687 100644 --- a/pymodbus/factory.py +++ b/pymodbus/factory.py @@ -12,103 +12,17 @@ # pylint: disable=missing-type-doc from collections.abc import Callable -from pymodbus.bit_read_message import ( - ReadCoilsRequest, - ReadCoilsResponse, - ReadDiscreteInputsRequest, - ReadDiscreteInputsResponse, -) -from pymodbus.bit_write_message import ( - WriteMultipleCoilsRequest, - WriteMultipleCoilsResponse, - WriteSingleCoilRequest, - WriteSingleCoilResponse, -) -from pymodbus.diag_message import ( - ChangeAsciiInputDelimiterRequest, - ChangeAsciiInputDelimiterResponse, - ClearCountersRequest, - ClearCountersResponse, - ClearOverrunCountRequest, - ClearOverrunCountResponse, - DiagnosticStatusRequest, - DiagnosticStatusResponse, - ForceListenOnlyModeRequest, - ForceListenOnlyModeResponse, - GetClearModbusPlusRequest, - GetClearModbusPlusResponse, - RestartCommunicationsOptionRequest, - RestartCommunicationsOptionResponse, - ReturnBusCommunicationErrorCountRequest, - ReturnBusCommunicationErrorCountResponse, - ReturnBusExceptionErrorCountRequest, - ReturnBusExceptionErrorCountResponse, - ReturnBusMessageCountRequest, - ReturnBusMessageCountResponse, - ReturnDiagnosticRegisterRequest, - ReturnDiagnosticRegisterResponse, - ReturnIopOverrunCountRequest, - ReturnIopOverrunCountResponse, - ReturnQueryDataRequest, - ReturnQueryDataResponse, - ReturnSlaveBusCharacterOverrunCountRequest, - ReturnSlaveBusCharacterOverrunCountResponse, - ReturnSlaveBusyCountRequest, - ReturnSlaveBusyCountResponse, - ReturnSlaveMessageCountRequest, - ReturnSlaveMessageCountResponse, - ReturnSlaveNAKCountRequest, - ReturnSlaveNAKCountResponse, - ReturnSlaveNoResponseCountRequest, - ReturnSlaveNoResponseCountResponse, -) +from pymodbus import bit_read_message as bit_r_msg +from pymodbus import bit_write_message as bit_w_msg +from pymodbus import diag_message as diag_msg +from pymodbus import file_message as file_msg +from pymodbus import mei_message as mei_msg +from pymodbus import other_message as o_msg +from pymodbus import pdu +from pymodbus import register_read_message as reg_r_msg +from pymodbus import register_write_message as reg_w_msg from pymodbus.exceptions import MessageRegisterException, ModbusException -from pymodbus.file_message import ( - ReadFifoQueueRequest, - ReadFifoQueueResponse, - ReadFileRecordRequest, - ReadFileRecordResponse, - WriteFileRecordRequest, - WriteFileRecordResponse, -) from pymodbus.logging import Log -from pymodbus.mei_message import ( - ReadDeviceInformationRequest, - ReadDeviceInformationResponse, -) -from pymodbus.other_message import ( - GetCommEventCounterRequest, - GetCommEventCounterResponse, - GetCommEventLogRequest, - GetCommEventLogResponse, - ReadExceptionStatusRequest, - ReadExceptionStatusResponse, - ReportSlaveIdRequest, - ReportSlaveIdResponse, -) -from pymodbus.pdu import ( - ExceptionResponse, - IllegalFunctionRequest, - ModbusRequest, - ModbusResponse, -) -from pymodbus.pdu import ModbusExceptions as ecode -from pymodbus.register_read_message import ( - ReadHoldingRegistersRequest, - ReadHoldingRegistersResponse, - ReadInputRegistersRequest, - ReadInputRegistersResponse, - ReadWriteMultipleRegistersRequest, - ReadWriteMultipleRegistersResponse, -) -from pymodbus.register_write_message import ( - MaskWriteRegisterRequest, - MaskWriteRegisterResponse, - WriteMultipleRegistersRequest, - WriteMultipleRegistersResponse, - WriteSingleRegisterRequest, - WriteSingleRegisterResponse, -) # --------------------------------------------------------------------------- # @@ -121,45 +35,45 @@ class ServerDecoder: """ __function_table = [ - ReadHoldingRegistersRequest, - ReadDiscreteInputsRequest, - ReadInputRegistersRequest, - ReadCoilsRequest, - WriteMultipleCoilsRequest, - WriteMultipleRegistersRequest, - WriteSingleRegisterRequest, - WriteSingleCoilRequest, - ReadWriteMultipleRegistersRequest, - DiagnosticStatusRequest, - ReadExceptionStatusRequest, - GetCommEventCounterRequest, - GetCommEventLogRequest, - ReportSlaveIdRequest, - ReadFileRecordRequest, - WriteFileRecordRequest, - MaskWriteRegisterRequest, - ReadFifoQueueRequest, - ReadDeviceInformationRequest, + reg_r_msg.ReadHoldingRegistersRequest, + bit_r_msg.ReadDiscreteInputsRequest, + reg_r_msg.ReadInputRegistersRequest, + bit_r_msg.ReadCoilsRequest, + bit_w_msg.WriteMultipleCoilsRequest, + reg_w_msg.WriteMultipleRegistersRequest, + reg_w_msg.WriteSingleRegisterRequest, + bit_w_msg.WriteSingleCoilRequest, + reg_r_msg.ReadWriteMultipleRegistersRequest, + diag_msg.DiagnosticStatusRequest, + o_msg.ReadExceptionStatusRequest, + o_msg.GetCommEventCounterRequest, + o_msg.GetCommEventLogRequest, + o_msg.ReportSlaveIdRequest, + file_msg.ReadFileRecordRequest, + file_msg.WriteFileRecordRequest, + reg_w_msg.MaskWriteRegisterRequest, + file_msg.ReadFifoQueueRequest, + mei_msg.ReadDeviceInformationRequest, ] __sub_function_table = [ - ReturnQueryDataRequest, - RestartCommunicationsOptionRequest, - ReturnDiagnosticRegisterRequest, - ChangeAsciiInputDelimiterRequest, - ForceListenOnlyModeRequest, - ClearCountersRequest, - ReturnBusMessageCountRequest, - ReturnBusCommunicationErrorCountRequest, - ReturnBusExceptionErrorCountRequest, - ReturnSlaveMessageCountRequest, - ReturnSlaveNoResponseCountRequest, - ReturnSlaveNAKCountRequest, - ReturnSlaveBusyCountRequest, - ReturnSlaveBusCharacterOverrunCountRequest, - ReturnIopOverrunCountRequest, - ClearOverrunCountRequest, - GetClearModbusPlusRequest, - ReadDeviceInformationRequest, + diag_msg.ReturnQueryDataRequest, + diag_msg.RestartCommunicationsOptionRequest, + diag_msg.ReturnDiagnosticRegisterRequest, + diag_msg.ChangeAsciiInputDelimiterRequest, + diag_msg.ForceListenOnlyModeRequest, + diag_msg.ClearCountersRequest, + diag_msg.ReturnBusMessageCountRequest, + diag_msg.ReturnBusCommunicationErrorCountRequest, + diag_msg.ReturnBusExceptionErrorCountRequest, + diag_msg.ReturnSlaveMessageCountRequest, + diag_msg.ReturnSlaveNoResponseCountRequest, + diag_msg.ReturnSlaveNAKCountRequest, + diag_msg.ReturnSlaveBusyCountRequest, + diag_msg.ReturnSlaveBusCharacterOverrunCountRequest, + diag_msg.ReturnIopOverrunCountRequest, + diag_msg.ClearOverrunCountRequest, + diag_msg.GetClearModbusPlusRequest, + mei_msg.ReadDeviceInformationRequest, ] @classmethod @@ -193,7 +107,7 @@ def lookupPduClass(self, function_code): :param function_code: The function code specified in a frame. :returns: The class of the PDU that has a matching `function_code`. """ - return self.lookup.get(function_code, ExceptionResponse) + return self.lookup.get(function_code, pdu.ExceptionResponse) def _helper(self, data: str): """Generate the correct request object from a valid request packet. @@ -206,7 +120,7 @@ def _helper(self, data: str): function_code = int(data[0]) if not (request := self.lookup.get(function_code, lambda: None)()): Log.debug("Factory Request[{}]", function_code) - request = IllegalFunctionRequest(function_code) + request = pdu.IllegalFunctionRequest(function_code) else: fc_string = "{}: {}".format( # pylint: disable=consider-using-f-string str(self.lookup[function_code]) # pylint: disable=use-maxsplit-arg @@ -230,7 +144,7 @@ def register(self, function): :param function: Custom function class to register :raises MessageRegisterException: """ - if not issubclass(function, ModbusRequest): + if not issubclass(function, pdu.ModbusRequest): raise MessageRegisterException( f'"{function.__class__.__name__}" is Not a valid Modbus Message' ". Class needs to be derived from " @@ -255,45 +169,45 @@ class ClientDecoder: """ function_table = [ - ReadHoldingRegistersResponse, - ReadDiscreteInputsResponse, - ReadInputRegistersResponse, - ReadCoilsResponse, - WriteMultipleCoilsResponse, - WriteMultipleRegistersResponse, - WriteSingleRegisterResponse, - WriteSingleCoilResponse, - ReadWriteMultipleRegistersResponse, - DiagnosticStatusResponse, - ReadExceptionStatusResponse, - GetCommEventCounterResponse, - GetCommEventLogResponse, - ReportSlaveIdResponse, - ReadFileRecordResponse, - WriteFileRecordResponse, - MaskWriteRegisterResponse, - ReadFifoQueueResponse, - ReadDeviceInformationResponse, + reg_r_msg.ReadHoldingRegistersResponse, + bit_r_msg.ReadDiscreteInputsResponse, + reg_r_msg.ReadInputRegistersResponse, + bit_r_msg.ReadCoilsResponse, + bit_w_msg.WriteMultipleCoilsResponse, + reg_w_msg.WriteMultipleRegistersResponse, + reg_w_msg.WriteSingleRegisterResponse, + bit_w_msg.WriteSingleCoilResponse, + reg_r_msg.ReadWriteMultipleRegistersResponse, + diag_msg.DiagnosticStatusResponse, + o_msg.ReadExceptionStatusResponse, + o_msg.GetCommEventCounterResponse, + o_msg.GetCommEventLogResponse, + o_msg.ReportSlaveIdResponse, + file_msg.ReadFileRecordResponse, + file_msg.WriteFileRecordResponse, + reg_w_msg.MaskWriteRegisterResponse, + file_msg.ReadFifoQueueResponse, + mei_msg.ReadDeviceInformationResponse, ] __sub_function_table = [ - ReturnQueryDataResponse, - RestartCommunicationsOptionResponse, - ReturnDiagnosticRegisterResponse, - ChangeAsciiInputDelimiterResponse, - ForceListenOnlyModeResponse, - ClearCountersResponse, - ReturnBusMessageCountResponse, - ReturnBusCommunicationErrorCountResponse, - ReturnBusExceptionErrorCountResponse, - ReturnSlaveMessageCountResponse, - ReturnSlaveNoResponseCountResponse, - ReturnSlaveNAKCountResponse, - ReturnSlaveBusyCountResponse, - ReturnSlaveBusCharacterOverrunCountResponse, - ReturnIopOverrunCountResponse, - ClearOverrunCountResponse, - GetClearModbusPlusResponse, - ReadDeviceInformationResponse, + diag_msg.ReturnQueryDataResponse, + diag_msg.RestartCommunicationsOptionResponse, + diag_msg.ReturnDiagnosticRegisterResponse, + diag_msg.ChangeAsciiInputDelimiterResponse, + diag_msg.ForceListenOnlyModeResponse, + diag_msg.ClearCountersResponse, + diag_msg.ReturnBusMessageCountResponse, + diag_msg.ReturnBusCommunicationErrorCountResponse, + diag_msg.ReturnBusExceptionErrorCountResponse, + diag_msg.ReturnSlaveMessageCountResponse, + diag_msg.ReturnSlaveNoResponseCountResponse, + diag_msg.ReturnSlaveNAKCountResponse, + diag_msg.ReturnSlaveBusyCountResponse, + diag_msg.ReturnSlaveBusCharacterOverrunCountResponse, + diag_msg.ReturnIopOverrunCountResponse, + diag_msg.ClearOverrunCountResponse, + diag_msg.GetClearModbusPlusResponse, + mei_msg.ReadDeviceInformationResponse, ] def __init__(self) -> None: @@ -310,7 +224,7 @@ def lookupPduClass(self, function_code): :param function_code: The function code specified in a frame. :returns: The class of the PDU that has a matching `function_code`. """ - return self.lookup.get(function_code, ExceptionResponse) + return self.lookup.get(function_code, pdu.ExceptionResponse) def decode(self, message): """Decode a response packet. @@ -348,7 +262,7 @@ def _helper(self, data: str): response = self.lookup.get(function_code, lambda: None)() if function_code > 0x80: code = function_code & 0x7F # strip error portion - response = ExceptionResponse(code, ecode.IllegalFunction) + response = pdu.ExceptionResponse(code, pdu.ModbusExceptions.IllegalFunction) if not response: raise ModbusException(f"Unknown response {function_code}") response.decode(data[1:]) @@ -362,7 +276,7 @@ def _helper(self, data: str): def register(self, function): """Register a function and sub function class with the decoder.""" - if function and not issubclass(function, ModbusResponse): + if function and not issubclass(function, pdu.ModbusResponse): raise MessageRegisterException( f'"{function.__class__.__name__}" is Not a valid Modbus Message' ". Class needs to be derived from " From a2e82d5e4bee035f7851e6405f5a131abdbee95c Mon Sep 17 00:00:00 2001 From: Ilkka Ollakka <14361597+ilkka-ollakka@users.noreply.github.com> Date: Tue, 2 Apr 2024 10:10:14 +0300 Subject: [PATCH 25/88] test_simulator: use unused_tcp_port fixture (#2141) --- test/sub_server/test_simulator.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/test/sub_server/test_simulator.py b/test/sub_server/test_simulator.py index 00d64697b..46288e57f 100644 --- a/test/sub_server/test_simulator.py +++ b/test/sub_server/test_simulator.py @@ -1,4 +1,5 @@ """Test datastore.""" + import asyncio import copy import json @@ -582,9 +583,9 @@ def test_simulator_action_random(self, celltype, minval, maxval): ) ), ) - async def test_simulator_server_tcp(self): + async def test_simulator_server_tcp(self, unused_tcp_port): """Test init simulator server.""" - task = ModbusSimulatorServer() + task = ModbusSimulatorServer(http_port=unused_tcp_port) await task.run_forever(only_start=True) await asyncio.sleep(0.5) await task.stop() From 24c200c0fc71a48d2cd9dabab14c4f52a3ce4bdf Mon Sep 17 00:00:00 2001 From: Ilkka Ollakka <14361597+ilkka-ollakka@users.noreply.github.com> Date: Tue, 2 Apr 2024 10:53:22 +0300 Subject: [PATCH 26/88] modbus_server: call execute in a way that those can be either coroutines or normal methods (#2139) --- pymodbus/server/async_io.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/pymodbus/server/async_io.py b/pymodbus/server/async_io.py index 449580ff0..f717345c1 100644 --- a/pymodbus/server/async_io.py +++ b/pymodbus/server/async_io.py @@ -49,6 +49,7 @@ def __init__(self, owner): self.receive_queue: asyncio.Queue = asyncio.Queue() self.handler_task = None # coroutine to be run on asyncio loop self.framer: ModbusFramer + self.loop = asyncio.get_running_loop() def _log_exception(self): """Show log exception.""" @@ -173,6 +174,9 @@ def execute(self, request, *addr): if self.server.request_tracer: self.server.request_tracer(request, *addr) + asyncio.run_coroutine_threadsafe(self._async_execute(request, *addr), self.loop) + + async def _async_execute(self, request, *addr): broadcast = False try: if self.server.broadcast_enable and not request.slave_id: @@ -181,9 +185,17 @@ def execute(self, request, *addr): # note response will be ignored for slave_id in self.server.context.slaves(): response = request.execute(self.server.context[slave_id]) + # Temporary check while we move execute to async method + if asyncio.iscoroutine(response): + response = await response else: context = self.server.context[request.slave_id] response = request.execute(context) + + # Temporary check while we move execute to async method + if asyncio.iscoroutine(response): + response = await response + except NoSuchSlaveException: Log.error("requested slave does not exist: {}", request.slave_id) if self.server.ignore_missing_slaves: From c4c14cab294e166b1efb1b7733b14fb3dcb019a3 Mon Sep 17 00:00:00 2001 From: jan iversen Date: Tue, 2 Apr 2024 11:40:36 +0200 Subject: [PATCH 27/88] RTU decode hunt part. (#2138) --- pymodbus/framer/ascii.py | 13 +++-- pymodbus/framer/base.py | 21 +++---- pymodbus/framer/framer.py | 19 +++---- pymodbus/framer/old_framer_ascii.py | 12 ---- pymodbus/framer/old_framer_base.py | 12 +++- pymodbus/framer/old_framer_rtu.py | 17 +----- pymodbus/framer/old_framer_socket.py | 10 ---- pymodbus/framer/old_framer_tls.py | 15 +---- pymodbus/framer/raw.py | 12 ++-- pymodbus/framer/rtu.py | 83 +++++++++++++--------------- pymodbus/framer/socket.py | 8 ++- pymodbus/framer/tls.py | 4 +- pymodbus/transaction.py | 4 -- test/framers/test_framer.py | 14 ++--- test/framers/test_old_framers.py | 2 - 15 files changed, 98 insertions(+), 148 deletions(-) diff --git a/pymodbus/framer/ascii.py b/pymodbus/framer/ascii.py index 16485c25f..03746b87e 100644 --- a/pymodbus/framer/ascii.py +++ b/pymodbus/framer/ascii.py @@ -15,10 +15,10 @@ class FramerAscii(FramerBase): r"""Modbus ASCII Frame Controller. - [ Start ][Address ][ Function ][ Data ][ LRC ][ End ] - 1c 2c 2c Nc 1c 2c + [ Start ][ Dev id ][ Function ][ Data ][ LRC ][ End ] + 1c 2c 2c N*2c 1c 2c - * data can be 0 - 2x252 chars + * data can be 1 - 2x252 chars * end is "\\r\\n" (Carriage return line feed), however the line feed character can be changed via a special command * start is ":" @@ -29,11 +29,12 @@ class FramerAscii(FramerBase): START = b':' END = b'\r\n' + MIN_SIZE = 10 def decode(self, data: bytes) -> tuple[int, int, int, bytes]: - """Decode message.""" - if (used_len := len(data)) < 10: + """Decode ADU.""" + if (used_len := len(data)) < self.MIN_SIZE: Log.debug("Short frame: {} wait for more data", data, ":hex") return 0, 0, 0, self.EMPTY if data[0:1] != self.START: @@ -54,7 +55,7 @@ def decode(self, data: bytes) -> tuple[int, int, int, bytes]: return used_len+2, 0, dev_id, msg[1:] def encode(self, data: bytes, device_id: int, _tid: int) -> bytes: - """Decode message.""" + """Encode ADU.""" dev_id = device_id.to_bytes(1,'big') checksum = self.compute_LRC(dev_id + data) packet = ( diff --git a/pymodbus/framer/base.py b/pymodbus/framer/base.py index 9eaf1075f..a7e0ba146 100644 --- a/pymodbus/framer/base.py +++ b/pymodbus/framer/base.py @@ -6,8 +6,6 @@ """ from __future__ import annotations -from abc import abstractmethod - class FramerBase: """Intern base.""" @@ -15,24 +13,23 @@ class FramerBase: EMPTY = b'' def __init__(self) -> None: - """Initialize a message instance.""" - + """Initialize a ADU instance.""" - @abstractmethod def decode(self, data: bytes) -> tuple[int, int, int, bytes]: - """Decode message. + """Decode ADU. - return: + returns: used_len (int) or 0 to read more transaction_id (int) or 0 device_id (int) or 0 modbus request/response (bytes) """ + raise RuntimeError("NOT IMPLEMENTED!") - @abstractmethod - def encode(self, pdu: bytes, device_id: int, tid: int) -> bytes: - """Decode message. + def encode(self, pdu: bytes, dev_id: int, tid: int) -> bytes: + """Encode ADU. - return: - modbus message (bytes) + returns: + modbus ADU (bytes) """ + raise RuntimeError("NOT IMPLEMENTED!") diff --git a/pymodbus/framer/framer.py b/pymodbus/framer/framer.py index 93b6d4324..9dc9eabd0 100644 --- a/pymodbus/framer/framer.py +++ b/pymodbus/framer/framer.py @@ -13,7 +13,6 @@ from enum import Enum from pymodbus.framer.ascii import FramerAscii -from pymodbus.framer.base import FramerBase from pymodbus.framer.raw import FramerRaw from pymodbus.framer.rtu import FramerRTU from pymodbus.framer.socket import FramerSocket @@ -67,13 +66,13 @@ def __init__(self, super().__init__(params, is_server) self.device_ids = device_ids self.broadcast: bool = (0 in device_ids) - self.msg_handle: FramerBase = { - FramerType.RAW: FramerRaw(), - FramerType.ASCII: FramerAscii(), - FramerType.RTU: FramerRTU(), - FramerType.SOCKET: FramerSocket(), - FramerType.TLS: FramerTLS(), - }[framer_type] + self.handle = { + FramerType.RAW: FramerRaw, + FramerType.ASCII: FramerAscii, + FramerType.RTU: FramerRTU, + FramerType.SOCKET: FramerSocket, + FramerType.TLS: FramerTLS, + }[framer_type]() def validate_device_id(self, dev_id: int) -> bool: @@ -86,7 +85,7 @@ def callback_data(self, data: bytes, addr: tuple | None = None) -> int: tot_len = len(data) start = 0 while True: - used_len, tid, device_id, msg = self.msg_handle.decode(data[start:]) + used_len, tid, device_id, msg = self.handle.decode(data[start:]) if msg: self.callback_request_response(msg, device_id, tid) if not used_len: @@ -110,5 +109,5 @@ def build_send(self, data: bytes, device_id: int, tid: int, addr: tuple | None = :param tid: transaction id (0 if not used). :param addr: optional addr, only used for UDP server. """ - send_data = self.msg_handle.encode(data, device_id, tid) + send_data = self.handle.encode(data, device_id, tid) self.send(send_data, addr) diff --git a/pymodbus/framer/old_framer_ascii.py b/pymodbus/framer/old_framer_ascii.py index 534c4fdc2..7dc0cf4ed 100644 --- a/pymodbus/framer/old_framer_ascii.py +++ b/pymodbus/framer/old_framer_ascii.py @@ -1,6 +1,4 @@ """Ascii_framer.""" -# pylint: disable=missing-type-doc - from pymodbus.exceptions import ModbusIOException from pymodbus.framer.old_framer_base import BYTE_ORDER, FRAME_HEADER, ModbusFramer from pymodbus.logging import Log @@ -71,13 +69,3 @@ def frameProcessIncomingPacket(self, single, callback, slave, _tid=None, **kwarg self._buffer = self._buffer[used_len :] self._header = {"uid": 0x00} callback(result) # defer this - - def buildPacket(self, message): - """Create a ready to send modbus packet. - - :param message: The request/response to send - :return: The encoded packet - """ - data = message.function_code.to_bytes(1,'big') + message.encode() - packet = self.message_handler.encode(data, message.slave_id, message.transaction_id) - return packet diff --git a/pymodbus/framer/old_framer_base.py b/pymodbus/framer/old_framer_base.py index 27ac9724c..b35da6d53 100644 --- a/pymodbus/framer/old_framer_base.py +++ b/pymodbus/framer/old_framer_base.py @@ -2,9 +2,11 @@ # pylint: disable=missing-type-doc from __future__ import annotations +import time from typing import Any from pymodbus.factory import ClientDecoder, ServerDecoder +from pymodbus.framer.base import FramerBase from pymodbus.logging import Log @@ -44,6 +46,7 @@ def __init__( "crc": b"\x00\x00", } self._buffer = b"" + self.message_handler: FramerBase def _validate_slave_id(self, slaves: list, single: bool) -> bool: """Validate if the received data is valid for the client. @@ -76,7 +79,9 @@ def recvPacket(self, size): :param size: Number of bytes to read :return: """ - return self.client.recv(size) + packet = self.client.recv(size) + self.client.last_frame_end = round(time.time(), 6) + return packet def resetFrame(self): """Reset the entire message frame. @@ -143,8 +148,11 @@ def frameProcessIncomingPacket( ) -> None: """Process new packet pattern.""" - def buildPacket(self, message) -> bytes: # type:ignore[empty-body] + def buildPacket(self, message) -> bytes: """Create a ready to send modbus packet. :param message: The populated request/response to send """ + data = message.function_code.to_bytes(1,'big') + message.encode() + packet = self.message_handler.encode(data, message.slave_id, message.transaction_id) + return packet diff --git a/pymodbus/framer/old_framer_rtu.py b/pymodbus/framer/old_framer_rtu.py index b4fedae7c..997448af4 100644 --- a/pymodbus/framer/old_framer_rtu.py +++ b/pymodbus/framer/old_framer_rtu.py @@ -58,8 +58,6 @@ def __init__(self, decoder, client=None): """ super().__init__(decoder, client) self._hsize = 0x01 - self._end = b"\x0d\x0a" - self._min_frame_size = 4 self.function_codes = decoder.lookup.keys() if decoder else {} self.message_handler = FramerRTU() @@ -170,12 +168,11 @@ def buildPacket(self, message): :param message: The populated request/response to send """ - data = message.function_code.to_bytes(1, 'big') + message.encode() - packet = self.message_handler.encode(data, message.slave_id, message.transaction_id) + packet = super().buildPacket(message) # Ensure that transaction is actually the slave id for serial comms if message.slave_id: - message.transaction_id = message.slave_id + message.transaction_id = message.slave_id return packet def sendPacket(self, message): @@ -227,13 +224,3 @@ def sendPacket(self, message): size = self.client.send(message) self.client.last_frame_end = round(time.time(), 6) return size - - def recvPacket(self, size): - """Receive packet from the bus with specified len. - - :param size: Number of bytes to read - :return: - """ - result = self.client.recv(size) - self.client.last_frame_end = round(time.time(), 6) - return result diff --git a/pymodbus/framer/old_framer_socket.py b/pymodbus/framer/old_framer_socket.py index e7fde0e16..4bd36f3f7 100644 --- a/pymodbus/framer/old_framer_socket.py +++ b/pymodbus/framer/old_framer_socket.py @@ -1,5 +1,4 @@ """Socket framer.""" -# pylint: disable=missing-type-doc import struct from pymodbus.exceptions import ( @@ -93,12 +92,3 @@ def frameProcessIncomingPacket(self, single, callback, slave, tid=None, **kwargs self.resetFrame() else: callback(result) # defer or push to a thread? - - def buildPacket(self, message): - """Create a ready to send modbus packet. - - :param message: The populated request/response to send - """ - data = message.function_code.to_bytes(1, 'big') + message.encode() - packet = self.message_handler.encode(data, message.slave_id, message.transaction_id) - return packet diff --git a/pymodbus/framer/old_framer_tls.py b/pymodbus/framer/old_framer_tls.py index ed6f610ee..9ebb44227 100644 --- a/pymodbus/framer/old_framer_tls.py +++ b/pymodbus/framer/old_framer_tls.py @@ -1,5 +1,4 @@ """TLS framer.""" -# pylint: disable=missing-type-doc import struct from pymodbus.exceptions import ( @@ -56,17 +55,9 @@ def frameProcessIncomingPacket(self, _single, callback, _slave, _tid=None, **kwa self._header["pid"] = 0 if (result := self.decoder.decode(data)) is None: + self.resetFrame() raise ModbusIOException("Unable to decode request") self.populateResult(result) - self._buffer = b"" - self._header = {} + self._buffer = self._buffer[used_len:] + self._header = {"tid": 0, "pid": 0, "len": 0, "uid": 0} callback(result) # defer or push to a thread? - - def buildPacket(self, message): - """Create a ready to send modbus packet. - - :param message: The populated request/response to send - """ - data = message.function_code.to_bytes(1,'big') + message.encode() - packet = self.message_handler.encode(data, message.slave_id, message.transaction_id) - return packet diff --git a/pymodbus/framer/raw.py b/pymodbus/framer/raw.py index 1d32ec20a..54a9a0ac1 100644 --- a/pymodbus/framer/raw.py +++ b/pymodbus/framer/raw.py @@ -9,16 +9,18 @@ class FramerRaw(FramerBase): r"""Modbus RAW Frame Controller. [ Device id ][Transaction id ][ Data ] - 1c 2c Nc + 1b 2b Nb - * data can be 1 - X chars + * data can be 0 - X bytes This framer is used for non modbus communication and testing purposes. """ + MIN_SIZE = 3 + def decode(self, data: bytes) -> tuple[int, int, int, bytes]: - """Decode message.""" - if len(data) < 3: + """Decode ADU.""" + if len(data) < self.MIN_SIZE: Log.debug("Short frame: {} wait for more data", data, ":hex") return 0, 0, 0, self.EMPTY dev_id = int(data[0]) @@ -26,5 +28,5 @@ def decode(self, data: bytes) -> tuple[int, int, int, bytes]: return len(data), dev_id, tid, data[2:] def encode(self, pdu: bytes, device_id: int, tid: int) -> bytes: - """Decode message.""" + """Encode ADU.""" return device_id.to_bytes(1, 'big') + tid.to_bytes(1, 'big') + pdu diff --git a/pymodbus/framer/rtu.py b/pymodbus/framer/rtu.py index 7cad7f8e6..bfda58365 100644 --- a/pymodbus/framer/rtu.py +++ b/pymodbus/framer/rtu.py @@ -12,40 +12,32 @@ class FramerRTU(FramerBase): """Modbus RTU frame type. - [ Start Wait ] [Address ][ Function Code] [ Data ][ CRC ][ End Wait ] - 3.5 chars 1b 1b Nb 2b 3.5 chars - - Wait refers to the amount of time required to transmit at least x many - characters. In this case it is 3.5 characters. Also, if we receive a - wait of 1.5 characters at any point, we must trigger an error message. - Also, it appears as though this message is little endian. The logic is - simplified as the following:: - - The following table is a listing of the baud wait times for the specified - baud rates:: - - ------------------------------------------------------------------ - Baud 1.5c (18 bits) 3.5c (38 bits) - ------------------------------------------------------------------ - 1200 15,000 ms 31,667 ms - 4800 3,750 ms 7,917 ms - 9600 1,875 ms 3,958 ms - 19200 0,938 ms 1,979 ms - 38400 0,469 ms 0,989 ms - 115200 0,156 ms 0,329 ms - ------------------------------------------------------------------ - 1 Byte = 8 bits + 1 bit parity + 2 stop bit = 11 bits + [ Start Wait ] [Address ][ Function Code] [ Data ][ CRC ] + 3.5 chars 1b 1b Nb 2b * Note: due to the USB converter and the OS drivers, timing cannot be quaranteed neither when receiving nor when sending. """ - function_codes: list[int] = [] + MIN_SIZE = 5 + + def __init__(self) -> None: + """Initialize a ADU instance.""" + super().__init__() + self.broadcast: bool = False + self.dev_ids: list[int] + self.fc_calc: dict[int, int] + + def set_dev_ids(self, dev_ids: list[int]): + """Set/update allowed device ids.""" + if 0 in dev_ids: + self.broadcast = True + self.dev_ids = dev_ids + + def set_fc_calc(self, fc: int, msg_size: int, count_pos: int): + """Set/Update function code information.""" + self.fc_calc[fc] = msg_size if not count_pos else -count_pos - @classmethod - def set_legal_function_codes(cls, function_codes: list[int]): - """Set legal function codes.""" - cls.function_codes = function_codes @classmethod def generate_crc16_table(cls) -> list[int]: @@ -160,31 +152,30 @@ def check_frame(self): Log.debug("Frame advanced, resetting header!!") callback(result) # defer or push to a thread? - def assemble_frame(self, _data_len: int, _data: bytes) -> int: - """Collect frame, until CRC matches.""" - return 0 - def decode(self, data: bytes) -> tuple[int, int, int, bytes]: - """Decode message.""" - if (data_len := len(data)) < 6: # - return 0, 0, 0, b'' + def hunt_frame_start(self, skip_cur_frame: bool, data: bytes) -> int: + """Scan buffer for a relevant frame start.""" + buf_len = len(data) + for i in range(1 if skip_cur_frame else 0, buf_len - self.MIN_SIZE): + if not (self.broadcast or data[i] in self.dev_ids): + continue + if (_fc := data[i + 1]) not in self.fc_calc: + continue + return i + return -i - if not (_frame_len := self.assemble_frame(data_len, data)): + def decode(self, data: bytes) -> tuple[int, int, int, bytes]: + """Decode ADU.""" + if len(data) < self.MIN_SIZE: return 0, 0, 0, b'' + while (i := self.hunt_frame_start(False, data)) > 0: + pass + return -i, 0, 0, b'' - resp = None - def callback(result): - """Set result.""" - nonlocal resp - resp = result - - self._legacy_decode(callback, [0]) - return 0, 0, 0, b'' - def encode(self, pdu: bytes, device_id: int, _tid: int) -> bytes: - """Decode message.""" + """Encode ADU.""" packet = device_id.to_bytes(1,'big') + pdu return packet + FramerRTU.compute_CRC(packet).to_bytes(2,'big') diff --git a/pymodbus/framer/socket.py b/pymodbus/framer/socket.py index 1c99837bd..6e081b03a 100644 --- a/pymodbus/framer/socket.py +++ b/pymodbus/framer/socket.py @@ -15,9 +15,11 @@ class FramerSocket(FramerBase): * length = uid + function code + data """ + MIN_SIZE = 9 + def decode(self, data: bytes) -> tuple[int, int, int, bytes]: - """Decode message.""" - if (used_len := len(data)) < 9: + """Decode ADU.""" + if (used_len := len(data)) < self.MIN_SIZE: Log.debug("Very short frame (NO MBAP): {} wait for more data", data, ":hex") return 0, 0, 0, self.EMPTY msg_tid = int.from_bytes(data[0:2], 'big') @@ -29,7 +31,7 @@ def decode(self, data: bytes) -> tuple[int, int, int, bytes]: return msg_len, msg_tid, msg_dev, data[7:msg_len] def encode(self, pdu: bytes, device_id: int, tid: int) -> bytes: - """Decode message.""" + """Encode ADU.""" packet = ( tid.to_bytes(2, 'big') + b'\x00\x00' + diff --git a/pymodbus/framer/tls.py b/pymodbus/framer/tls.py index 8256038c3..a4e83973b 100644 --- a/pymodbus/framer/tls.py +++ b/pymodbus/framer/tls.py @@ -12,9 +12,9 @@ class FramerTLS(FramerBase): """ def decode(self, data: bytes) -> tuple[int, int, int, bytes]: - """Decode message.""" + """Decode ADU.""" return len(data), 0, 0, data def encode(self, pdu: bytes, _device_id: int, _tid: int) -> bytes: - """Decode message.""" + """Encode ADU.""" return pdu diff --git a/pymodbus/transaction.py b/pymodbus/transaction.py index 157537cdb..04dcce829 100644 --- a/pymodbus/transaction.py +++ b/pymodbus/transaction.py @@ -1,7 +1,6 @@ """Collection of transaction based abstractions.""" __all__ = [ - "DictTransactionManager", "ModbusTransactionManager", "ModbusSocketFramer", "ModbusTlsFramer", @@ -481,6 +480,3 @@ def reset(self): """Reset the transaction identifier.""" self.tid = 0 self.transactions = {} - -class DictTransactionManager(ModbusTransactionManager): - """Old alias for ModbusTransactionManager.""" diff --git a/test/framers/test_framer.py b/test/framers/test_framer.py index 0114053ff..0236fcb34 100644 --- a/test/framers/test_framer.py +++ b/test/framers/test_framer.py @@ -35,7 +35,7 @@ async def test_message_init(self, entry, dummy_message): False, [1], ) - assert msg.msg_handle + assert msg.handle @pytest.mark.parametrize(("data", "res_len", "cx", "rc"), [ (b'12345', 5, 1, [(5, 0, 0, b'12345')]), # full frame @@ -50,7 +50,7 @@ async def test_message_init(self, entry, dummy_message): async def test_message_callback(self, msg, data, res_len, cx, rc): """Test message type.""" msg.callback_request_response = mock.Mock() - msg.msg_handle.decode = mock.MagicMock(side_effect=iter(rc)) + msg.handle.decode = mock.MagicMock(side_effect=iter(rc)) assert msg.callback_data(data) == res_len assert msg.callback_request_response.call_count == cx if cx: @@ -60,9 +60,9 @@ async def test_message_callback(self, msg, data, res_len, cx, rc): async def test_message_build_send(self, msg): """Test message type.""" - msg.msg_handle.encode = mock.MagicMock(return_value=(b'decode')) + msg.handle.encode = mock.MagicMock(return_value=(b'decode')) msg.build_send(b'decode', 1, 0) - msg.msg_handle.encode.assert_called_once() + msg.handle.encode.assert_called_once() msg.send.assert_called_once() msg.send.assert_called_with(b'decode', None) @@ -84,7 +84,7 @@ async def test_validate_id(self, msg, dev_id, res): ]) async def test_decode(self, msg, data, res_id, res_tid, res_len, res_data): """Test decode method in all types.""" - t_len, t_id, t_tid, t_data = msg.msg_handle.decode(data) + t_len, t_id, t_tid, t_data = msg.handle.decode(data) assert res_len == t_len assert res_id == t_id assert res_tid == t_tid @@ -97,7 +97,7 @@ async def test_decode(self, msg, data, res_id, res_tid, res_len, res_data): ]) async def test_encode(self, msg, data, dev_id, tid, res_data): """Test decode method in all types.""" - t_data = msg.msg_handle.encode(data, dev_id, tid) + t_data = msg.handle.encode(data, dev_id, tid) assert res_data == t_data @pytest.mark.parametrize( @@ -275,7 +275,7 @@ def test_encode(self, frame, frame_expected, data, dev_id, tid, inx1, inx2, inx3 "single", ] ) - async def test_decode(self, dummy_message, msg_type, data, dev_id, tid, expected, split): + async def test_decode2(self, dummy_message, msg_type, data, dev_id, tid, expected, split): """Test encode method.""" if msg_type == FramerType.RTU: pytest.skip("Waiting on implementation!") diff --git a/test/framers/test_old_framers.py b/test/framers/test_old_framers.py index 14ecedd31..6d6aa3e68 100644 --- a/test/framers/test_old_framers.py +++ b/test/framers/test_old_framers.py @@ -79,8 +79,6 @@ def test_framer_initialization(self, framer): "crc": b"\x00\x00", } assert framer._hsize == 0x01 # pylint: disable=protected-access - assert framer._end == b"\x0d\x0a" # pylint: disable=protected-access - assert framer._min_frame_size == 4 # pylint: disable=protected-access else: assert framer._header == { # pylint: disable=protected-access "tid": 0, From 210ad31827ec45b07828794d9dc0b30f4c8805d2 Mon Sep 17 00:00:00 2001 From: jan iversen Date: Wed, 3 Apr 2024 09:51:29 +0200 Subject: [PATCH 28/88] Clean datastore setValues. (#2145) --- pymodbus/bit_write_message.py | 11 ++++++----- pymodbus/datastore/remote.py | 5 ++--- pymodbus/datastore/simulator.py | 4 +++- test/test_remote_datastore.py | 7 ++++--- 4 files changed, 15 insertions(+), 12 deletions(-) diff --git a/pymodbus/bit_write_message.py b/pymodbus/bit_write_message.py index 7e634f204..0af65992d 100644 --- a/pymodbus/bit_write_message.py +++ b/pymodbus/bit_write_message.py @@ -90,12 +90,13 @@ def execute(self, context): if not context.validate(self.function_code, self.address, 1): return self.doException(merror.IllegalAddress) - result = context.setValues(self.function_code, self.address, [self.value]) - if isinstance(result, ExceptionResponse): - return result + context.setValues(self.function_code, self.address, [self.value]) + # result = context.setValues(self.function_code, self.address, [self.value]) + # if isinstance(result, ExceptionResponse): + # return result values = context.getValues(self.function_code, self.address, 1) - if isinstance(values, ExceptionResponse): - return values + # if isinstance(values, ExceptionResponse): + # return values return WriteSingleCoilResponse(self.address, values[0]) def get_response_pdu_size(self): diff --git a/pymodbus/datastore/remote.py b/pymodbus/datastore/remote.py index 2ae76409b..919da6c50 100644 --- a/pymodbus/datastore/remote.py +++ b/pymodbus/datastore/remote.py @@ -57,9 +57,8 @@ def setValues(self, fc_as_hex, address, values): self.result = func_fc(address, values) else: self.result = func_fc(address, values[0]) - if self.result.isError(): - return self.result - return None + # if self.result.isError(): + # return self.result def __str__(self): """Return a string representation of the context. diff --git a/pymodbus/datastore/simulator.py b/pymodbus/datastore/simulator.py index 924aacfaa..4c73a86cc 100644 --- a/pymodbus/datastore/simulator.py +++ b/pymodbus/datastore/simulator.py @@ -8,6 +8,8 @@ from datetime import datetime from typing import Any +from pymodbus.datastore.context import ModbusBaseSlaveContext + WORD_SIZE = 16 @@ -371,7 +373,7 @@ def setup(self, config, custom_actions) -> None: raise RuntimeError(f"INVALID key in setup: {self.config}") -class ModbusSimulatorContext: +class ModbusSimulatorContext(ModbusBaseSlaveContext): """Modbus simulator. :param config: A dict with structure as shown below. diff --git a/test/test_remote_datastore.py b/test/test_remote_datastore.py index d9ab9d247..1f59c62b8 100644 --- a/test/test_remote_datastore.py +++ b/test/test_remote_datastore.py @@ -29,9 +29,10 @@ def test_remote_slave_set_values(self): context = RemoteSlaveContext(client) context.setValues(0x0F, 0, [1]) - result = context.setValues(0x10, 1, [1]) - assert result.exception_code == 0x02 - assert result.function_code == 0x90 + # result = context.setValues(0x10, 1, [1]) + context.setValues(0x10, 1, [1]) + # assert result.exception_code == 0x02 + # assert result.function_code == 0x90 def test_remote_slave_get_values(self): """Test getting values from a remote slave context.""" From a6b43dd714e290080c112a3001037187bdb3a5f5 Mon Sep 17 00:00:00 2001 From: jan iversen Date: Fri, 5 Apr 2024 14:52:41 +0200 Subject: [PATCH 29/88] Speed up no data detection. (#2150) --- pymodbus/framer/old_framer_base.py | 2 ++ pymodbus/framer/old_framer_socket.py | 2 ++ 2 files changed, 4 insertions(+) diff --git a/pymodbus/framer/old_framer_base.py b/pymodbus/framer/old_framer_base.py index b35da6d53..4922be1c4 100644 --- a/pymodbus/framer/old_framer_base.py +++ b/pymodbus/framer/old_framer_base.py @@ -138,6 +138,8 @@ def processIncomingPacket(self, data, callback, slave, **kwargs): """ Log.debug("Processing: {}", data, ":hex") self._buffer += data + if self._buffer == b'': + return if not isinstance(slave, (list, tuple)): slave = [slave] single = kwargs.pop("single", False) diff --git a/pymodbus/framer/old_framer_socket.py b/pymodbus/framer/old_framer_socket.py index 4bd36f3f7..bc515ab2a 100644 --- a/pymodbus/framer/old_framer_socket.py +++ b/pymodbus/framer/old_framer_socket.py @@ -72,6 +72,8 @@ def frameProcessIncomingPacket(self, single, callback, slave, tid=None, **kwargs function to process and send. """ while True: + if self._buffer == b'': + return used_len, use_tid, dev_id, data = self.message_handler.decode(self._buffer) if not data: return From e8063fa54a02234f0ae6cb4da9c9383189921d83 Mon Sep 17 00:00:00 2001 From: jan iversen Date: Tue, 9 Apr 2024 17:39:15 +0200 Subject: [PATCH 30/88] Merge 3.6.7 back into dev. (#2156) * Add lock to async requests, correct logging and length calc. * Prepare v3.6.7. * Prepare v3.6.7. * Correct change.log. * prepare dev. * Doc update. --- AUTHORS.rst | 3 +++ CHANGELOG.rst | 17 +++++++++++++ MAKE_RELEASE.rst | 1 - README.rst | 2 +- doc/Makefile | 14 ++++++++++ doc/source/_static/examples.tgz | Bin 50758 -> 50710 bytes doc/source/_static/examples.zip | Bin 37192 -> 37195 bytes .../library/architecture/architecture.rst | 9 +++---- doc/source/library/framer.rst | 24 +++++++++--------- pymodbus/__init__.py | 2 +- pymodbus/client/base.py | 2 ++ pymodbus/client/modbusclientprotocol.py | 1 + test/test_network.py | 9 ++++--- 13 files changed, 61 insertions(+), 23 deletions(-) create mode 100644 doc/Makefile diff --git a/AUTHORS.rst b/AUTHORS.rst index e9a2b0747..713a481dc 100644 --- a/AUTHORS.rst +++ b/AUTHORS.rst @@ -29,6 +29,7 @@ Thanks to - dhoomakethu - doelki - DominicDataP +- Dominique Martinet - Dries - duc996 - Farzad Panahi @@ -38,6 +39,7 @@ Thanks to - Hangyu Fan - Hayden Roche - Iktek +- Ilkka Ollakka - Jakob Ruhe - Jakob Schlyter - James Braza @@ -45,6 +47,7 @@ Thanks to - jan iversen - Jerome Velociter - Joe Burmeister +- John Miko - Jonathan Reichelt Gjertsen - julian - Justin Standring diff --git a/CHANGELOG.rst b/CHANGELOG.rst index a72222dd9..db96ff69e 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -7,6 +7,23 @@ helps make pymodbus a better product. :ref:`Authors`: contains a complete list of volunteers have contributed to each major version. +Version 3.6.7 +------------- +* Add lock to async requests, correct logging and length calc. (FIX, not on dev) +* test_simulator: use unused_tcp_port fixture (#2141) +* streamline imports in Factory.py (#2140) +* Secure testing is done with pymodbus in PR. (#2136) +* Fix link to github in README (#2134) +* Wildcard exception catch from pyserial. (#2125) +* Problem with stale CI. (#2117) +* Add connection exception to list of exceptions catpured in retries (#2113) +* Move on_reconnect to client level (#2111) +* Bump github stale. (#2110) +* update package_test_tool (add 4 test scenarios) (#2107) +* Bump dependencies. (#2108) +* Cancel send if no connection. (#2103) + + Version 3.6.6 ------------- * Solve transport close() as not inherited method. (#2098) diff --git a/MAKE_RELEASE.rst b/MAKE_RELEASE.rst index 7a5e3cc9c..a919eb9d4 100644 --- a/MAKE_RELEASE.rst +++ b/MAKE_RELEASE.rst @@ -19,7 +19,6 @@ Prepare/make release on dev. update AUTHORS.rst and CHANGELOG.rst cd doc; ./build_html * rm -rf build/* dist/* - * git checkout build * python3 -m build * twine check dist/* * Commit, push and merge. diff --git a/README.rst b/README.rst index ba4abb10e..fbfe1c89c 100644 --- a/README.rst +++ b/README.rst @@ -11,7 +11,7 @@ PyModbus - A Python Modbus Stack Pymodbus is a full Modbus protocol implementation offering client/server with synchronous/asynchronous API a well as simulators. -Current release is `3.6.6 `_. +Current release is `3.6.7 `_. Bleeding edge (not released) is `dev `_. diff --git a/doc/Makefile b/doc/Makefile new file mode 100644 index 000000000..312a671da --- /dev/null +++ b/doc/Makefile @@ -0,0 +1,14 @@ +# Minimal makefile for Sphinx documentation +# + +# You can set these variables from the command line. +SPHINXOPTS = -W --keep-going + +# Put it first so that "make" without argument is like "make help". +help: + sphinx-build -M help "." "_build" -W --keep-going + +.PHONY: help Makefile + +%: Makefile + sphinx-build -M $@ "." "_build" -W --keep-going diff --git a/doc/source/_static/examples.tgz b/doc/source/_static/examples.tgz index 16fc99a5a226104b30f27b4b2f874bb3339a2b4e..722d25b18df64101be33e6559c2da0358074cd17 100644 GIT binary patch delta 49402 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 )78^CKRw!|5ApZRwkt=x?5{s~B!-vMtrp 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⁣IuBz77;l?3v zLzlm)o*t{1E3>q<3@i+{SS<8pv&}!IZ@)wiKf}g za`!~>bYpGcLSEa&)0cs^-Nb==V;Xk;~tx}FEj)u z2L=`{(Vg9;qaD8Hmae+dGJ2_HzPZ#fI_FxRs&P+L57jhujx4s6EqZG!TRnYxrmc0c zyllW%THiP88k(DOPJ1dFTf2IH{PXjJf$j@+jKz1s)i}^RRqI>sVjSHIYlxW5o@D39F-fx@o4$J>NDuJTtmnHauhTdmUBu@Fm*c z;OX}V$|lAd7JZDTYGCTzIXz}8P2G+0tme~ZgTJ$%UFLtJpMUau>@xp+7>vN6K9lW1iZ!p241mt_II^>s)8R zQ$OIY=^ORC`T%H{c5%UEgMu(PXu=n|T`oDJm*6OM+q;?m&-OI_3O zrP*@-QrA#_Wgp!zFf%q+H{0J57^xiU9-3${?uK zwp3TwEL7R+7njPc6P*j?7aC{htF5l;X*BaxkIoLY1ge?l$zk_w*<96dd4Ee!=~8{w zWT&sSx~+Af&0jTF>U8urlr5L{HMEX)*3{PwRJM&aG)@nH%{J&u?e3<&ay0Xfw$Io-WfQZ*m7|rHYP$Vn75e3| z$(h!+OI?#a^EIWOHlMw`ro-ZHt{Cx-3_FKfhx@xG>^1JCp}O{pvZclD{z=-uG(R}p z>ap62XFZF54!xtUZnSaAzf?ThT-s$DYU;nxw>Yt2X|M0FGHvZsGXs^yGh-F?(>1P| zOAFQ0e&4X8d#qu;x^!t~cFMV6)wfw1mn+L^+M8=FevLpXzIDSRPy|?;P!&oS7SH9%{FLPUzdpJe|X1BmQQaqk5@ly07d4 z5`89Sn(aL{cSY^Ir=_acKjyBQ?kg`ZZLJD;tCuHQ>v|ncO_P^=b;b0kzM*Pxtk|+V zVWZu%PFLGx!@$T~Ps_mY>_C~5>74frO%GRGn(OOE%^dA-nz`hwZ7QoAUYvC|p}BXg zYg%uA=^N;o=;%OFUd8N0=ipNRV7YB}xq9dVX>$F*NKf-<-TYu}t<$$KP&H?-p1I)f zX&s^m7TRa5qxJgQKu>E$=~&G|_2^=m)iFBRU&;)0PS9mDBO}h~s=l%1`ij0Ox8Lci zuUMXFu4-HETeNqK%-M!2X9xTP&H8|?ugA)Nc=~7By`xpF&hEiM`+&>S@0e-pZE3Mj zHnx{mxtqr5KusH>>6zK3x{0MJ$53rMUE}okFJEZYHx12|*OfLhrHjzyhNfDtZ?<;2 zt8u!q)7RhWXephYTr8_?_jT7?vMqGXloq#?_H@=Smo?6|EzHqvrTXcysfDVgnI&(3 zajm6ozGtGrRW?#l;p%Q_>1PI7$~xvpJYI*>H`LnFH#5=SJa4TSZfdeE)zPDf#f>^9 z^}{2572bx9i3@dY%Z)Q*&e_0Xm&I4_ouKP0`nv-??wY1id)IW`NEb8c>0w%Xy%)-y z6V=W(=;Rh}jowr1@31vCx;mQLy>q31G!RMYjA z{>ce%vG;8-WYI4%r2sw%qmL#?)&sou)6ON$o9G1}8p+uK+?-&N(Qa@EW>O?I@o zYR8z_{_=&^(e|#vrjfeAOY=hvJu~K*>S-LUTjz z?xm6ThU%XFma^v3j`FtB@;cj?)!Slix-{t;tQ&MqRdqL)RWEt#N2fdVKBu$4uCdPH znhFdpm(ElS4!2LZd%HZ&?iN>T*Gye|({OL?qQ}!U>g}m*uD3Q9VNYPPzskMX*z30VL;_;mT=Z$rlg~cSKF>bS TK3}xY{}1(mGC}~m!pr~wZA_>_ diff --git a/doc/source/_static/examples.zip b/doc/source/_static/examples.zip index f582b5053177099c59826791db05f6b926f01642..5da9c1f1451dc851c327268c09c12edde69f4df7 100644 GIT binary patch delta 1984 zcmZ8ic{J2}6dz`>lfslWoRBRu6o#=M>xhPKM$RY@rdCpW&KHqeP7k&qA#AkM(CwQ7(*dqm+=IteU@nC2E3}|M~{^HzD za}FT{)Q^YugSn-2Fao6~FqL2~b(`0JI(73><57dL0_p?oL7x5vKLlTufj^!nwKQli z9~=*Y@IE-1FU)JL@_g>b9aLb>-bRz%LW|7E#qstl7Vh=E&6`p$?Y0WGfx`5 zd_s47nHzi_`g!3?c zMIRV1C>|NCoJe{I8HyoA_RISJ@R3TQzs|AQHklbb5#OS$4ZyGp?N<}lTb>qCNldx= zGig5MlV-C^A*cFhiG(ct&o?J&t&x_MaC`=xa_MLt1ra6Y2S@6{f={<$v%GyXUF)TG z8U|M+>VHPDI=)n{r!yTfLsf0kI1Pp4aVq`wYOBJ`mD1;lDU6IPPZ@L-0O)nE_zrzi zh}jQ6{Gk@OHXzv%0?z}-H-;{C4Yq%y*5B*E?Vh^-6GOyJgv%}tOjpqiUa!Ye@yMMe zeZQ(-B?cyHqn3e^iAlv^Q>^@(7RATSV-xDlFM}e6_l%UsWG+~)>dg78886>G`gG7Q z?k>r6Epv5UEWF2=Ku*_UX2M%bt$LMaK54I^Xx;YGu-DW13THJ8ko#1;d? zc)8-D=ISo2eGcx2s%2w0Fx6j(#tUahY0R`8#+a)-$*xFF_kH3e2BTzdgBBkd`;_9| zo&7uVDyHU|$vyG15#=ZLQ%$i+8n4~0R8Fmp&Oe-M3$J6T*o!5>GXM5e1bQIen`jG7>I_CsA}=s|_cq)vQaXgm)uNYCT7ORY>np zaSZqHncWWmOZU^U!Quh3;judPS6!u61N3C3%pwC)h!W`^2+KmXj+5_Xl&DJAik~Nt z#hcsw8YDFPtSw7Cpe{K;Ou(C{Y4@JIj#9w&JUz})5z5 z^yoK>!d>C#bn@yX@OBO?y#n;d17}$bM-_*%7Ey%Ca~sgN^0in~)uyGgwX%93js^RM`PXz#V}uDO+rmsPABt8bc6#V*#X~~Hbl5Xe4y_0E^{u%F&8b~vSb}H2W%Y^ zi+pxrWee=tC|qZcR->r*%3u;&x_k_qWpyoh7`NY+ME>wxKckGGKU!@`5RU#-RN9$L zn!FNXFulf5GFf)IV1%8uz}7@FYErUXJlrL>;ED4KD^}q+Xm9IkUf^=Y-1%7Bi>t%8 zyqrb8=ehfmu51+|BWe-aOX^m?*GM8)Z93!3*C|xxrh&)5@PBZs}mFRvd zRLFhL)twCt4N=$?=Sx#gh!3AL5>bj$6N@e@fOqAbTB*l{Ig`BbMV;&jxS8+QYZcG% z+0?T+O`^sh-R5Pz)1D52sA0+6i$FCaT7o&}pese0yI*b-?ZE8)!nc>%{tcV6{g07s zH}PvpR-61v*X{Uz4j;H~@vp5QCJy5KIB5U%y&Ej5QAz-J+h(Th+rYT?Op2fqC+6@C zE54s|vuHPLHdU4lBf?S^3RWfuB=vI@OwAk7IkJ*>AEd(>8ay zonp@SbM%E#TsG?eZ%L0+Om}n6@RdjjkPu`+2n2%2u?KU&4GQB7NRj>3v$r`enPdF} DQP_sS delta 2058 zcmZuyc{G%39G2N6QnJ-xI*BPV7+cO5VJIz@C=@YGmgr(amIl++DOo08eJv_8GP>51 z8Z|0~hH|NUb99*@OVN#qtk;s{`aZ|FQ};XP`{y~&`~05g_nz}Uja=D!t}O5z5yDEi zP)ES%a!^5f926L#hvlg*Q$`^?;v7MJw3%F@xH|CE{Lc$QW8`3zzLKzn=C2&bATEU` zf^0QY%+*KepsYt|UaJ~?L134r@e(yz^9mZKVs*?UR8O4`vXBRmq-VHjf)}BufI?t{ z8DNqD9&>g%GALs?l6TwSJsM^L#7!_0&q493h=oWP47D_sfVV7%Q80LwY%J0IgWSL1 z=`n@41|9_5{NURzLB%ol?+acx>(Y_EQ5(bCDmKLQS-m)+r5osV{nn&W1 z@Ob|i5(c^{!Th3fV_u^b=dZ;3W2YCRV!4b=Jjh7gEICt>Sczf=Z1J5UbvI{*9RTX{;aoVKSoeqhkSpa3<4&2i2S-|=Ort{~RB_|`#+6YUOn z$BzAi6L}6e`@K(dx0muOII9oj4T;@7m6ks>bAI(%t>ew5Zidn7Oj7dg*^-*F)~Pi| zZMArY#O8%l_7Hcz8CTN-W z)rqBP>O?)G!vpr)>uxG$Jkp8Ut>DqmUsn5@;S~Vo`>De!d%`cWynnyJ1Bc zV`lQSKlNCS>Q%Z)7I93R`*LC^;>bzY0UsaQ-F~Y0B){+mvqm&Xjn;6R8q{ukGMzBm z+AJajYN>R|Hxbx`=g-M-0qXi;qwGvgmNq|YE>hd=F+ zc_9iH7}H9bO=$zN=IpJLkV}pMfSjhe@PbCVeEE%lgP}6ZIw6VeK_vi@^>? z#Vi%Ki(VX6-9ZM6z>LngokimJWhG@Tt%Yu{tkIRm7iR@|U-tpyBEDCC_NOlnWbv!w z@)qU_jd5eGtjsZ9bK_dU7;EbqP36NIE9ck^l@uQQk_m+qO;DCTH!u&!1afAEImSj<>>uj6$gmZFm2 zREwFN>WEF%Sf{MVvZ@N+*mi`TFxg{aMCC9~c{M&%I_&0?^y?^XLq^$yA!lLFaA+sk zzecDQ-l}p*7*}PUoKJ4ev)-< zyth`SCdJ9WYKRi-MPLDuaM%d^=_>yJVfI_&$o&v$5jtOY(tL-Qz?#SV#E>@#&`yxO@L>no$H}Qs6oON$0GQ zP#=IbDXJiF4h_FB*Us4^p%+F9oSkP%flw3pq}m~&CXix2OSSo#shwso&4h{|IgPY9 z9SfI0SDHBzDuG2k@P&ag=$P&SzpyvbEf(1eULlFGbi1EH&kP%B5Nf!G8Me~4pyL{> z|H&W2+9u6}qPDU~(g7e%v-3Yhg)}dnm!w+qIdXoRv?WN>J(D7>34#2~A2tPnSink} f2~Cdyc1rpDp$R?%mWcVw0d+5|M@OZ|%zys^6w ModbusResponse: while count <= self.retries: req = self.build_response(request.transaction_id) if not count or not self.no_resend_on_retry: + self.ctx.framer.resetFrame() self.ctx.send(packet) if self.broadcast_enable and not request.slave_id: resp = None diff --git a/pymodbus/client/modbusclientprotocol.py b/pymodbus/client/modbusclientprotocol.py index 929d6bd45..f95fff75d 100644 --- a/pymodbus/client/modbusclientprotocol.py +++ b/pymodbus/client/modbusclientprotocol.py @@ -66,6 +66,7 @@ def callback_connected(self) -> None: """Call when connection is succcesfull.""" if self.on_connect_callback: self.loop.call_soon(self.on_connect_callback, True) + self.framer.resetFrame() def callback_disconnected(self, exc: Exception | None) -> None: """Call when connection is lost.""" diff --git a/test/test_network.py b/test/test_network.py index 24da82c67..97bfcb0a1 100644 --- a/test/test_network.py +++ b/test/test_network.py @@ -78,10 +78,10 @@ async def test_stub(self, use_port, use_cls): client.close() stub.close() - async def test_double_packet(self, use_port, use_cls): + async def test_parallel_requests(self, use_port, use_cls): """Test double packet on network.""" old_data = b'' - client = AsyncModbusTcpClient(NULLMODEM_HOST, port=use_port, retries=0) + client = AsyncModbusTcpClient(NULLMODEM_HOST, port=use_port, retries=0, timeout=30) def local_handle_data(data: bytes) -> bytes | None: """Handle server side for this test case.""" @@ -126,6 +126,9 @@ async def local_call(addr: int) -> bool: await stub.start_run() assert await client.connect() - await asyncio.gather(*[local_call(x) for x in range(1, 10)]) + try: + await asyncio.gather(*[local_call(1) for x in range(1, 10)]) + except Exception as exc: # pylint: disable=broad-exception-caught + pytest.fail(exc) client.close() stub.close() From 8a836b20105124ed9b834dd485cf9cc9c096681f Mon Sep 17 00:00:00 2001 From: jan iversen Date: Fri, 12 Apr 2024 19:38:35 +0200 Subject: [PATCH 31/88] PDU classes --> pymodbus/pdu. (#2160) --- API_changes.rst | 1 + doc/source/library/pymodbus.rst | 39 ++++++++++--------- examples/client_custom_msg.py | 2 +- examples/message_generator.py | 16 ++++---- pymodbus/client/mixin.py | 16 ++++---- pymodbus/factory.py | 18 ++++----- pymodbus/pdu/__init__.py | 18 +++++++++ pymodbus/{ => pdu}/bit_read_message.py | 0 pymodbus/{ => pdu}/bit_write_message.py | 0 pymodbus/{ => pdu}/diag_message.py | 0 pymodbus/{ => pdu}/file_message.py | 0 pymodbus/{ => pdu}/mei_message.py | 0 pymodbus/{ => pdu}/other_message.py | 0 pymodbus/{ => pdu}/pdu.py | 0 pymodbus/{ => pdu}/register_read_message.py | 0 pymodbus/{ => pdu}/register_write_message.py | 0 test/framers/generator.py | 2 +- test/framers/test_old_framers.py | 2 +- test/sub_client/test_client.py | 14 +++---- test/sub_function_codes/test_all_messages.py | 8 ++-- .../test_bit_read_messages.py | 4 +- .../test_bit_write_messages.py | 4 +- test/sub_function_codes/test_diag_messages.py | 4 +- test/sub_function_codes/test_mei_messages.py | 2 +- .../sub_function_codes/test_other_messages.py | 6 +-- .../test_register_read_messages.py | 2 +- .../test_register_write_messages.py | 2 +- test/sub_server/test_server_asyncio.py | 2 +- test/test_file_message.py | 4 +- test/test_remote_datastore.py | 6 +-- 30 files changed, 97 insertions(+), 75 deletions(-) create mode 100644 pymodbus/pdu/__init__.py rename pymodbus/{ => pdu}/bit_read_message.py (100%) rename pymodbus/{ => pdu}/bit_write_message.py (100%) rename pymodbus/{ => pdu}/diag_message.py (100%) rename pymodbus/{ => pdu}/file_message.py (100%) rename pymodbus/{ => pdu}/mei_message.py (100%) rename pymodbus/{ => pdu}/other_message.py (100%) rename pymodbus/{ => pdu}/pdu.py (100%) rename pymodbus/{ => pdu}/register_read_message.py (100%) rename pymodbus/{ => pdu}/register_write_message.py (100%) diff --git a/API_changes.rst b/API_changes.rst index 4ec98de3f..8c3fd1de8 100644 --- a/API_changes.rst +++ b/API_changes.rst @@ -11,6 +11,7 @@ API changes 3.7.0 - on_connect_callback(true/false) added to async clients. - binary framer no longer supported - Framer. renamed to FramerType. +- PDU classes moved to pymodbus/pdu API changes 3.6.0 diff --git a/doc/source/library/pymodbus.rst b/doc/source/library/pymodbus.rst index 9875fd1ed..cdda34602 100644 --- a/doc/source/library/pymodbus.rst +++ b/doc/source/library/pymodbus.rst @@ -6,84 +6,87 @@ Extra functions :undoc-members: :show-inheritance: - -.. automodule:: pymodbus.bit_read_message +.. automodule:: pymodbus.device :members: :undoc-members: :show-inheritance: -.. automodule:: pymodbus.bit_write_message +.. automodule:: pymodbus.events :members: :undoc-members: :show-inheritance: -.. automodule:: pymodbus.device +.. automodule:: pymodbus.exceptions :members: :undoc-members: :show-inheritance: -.. automodule:: pymodbus.diag_message +.. automodule:: pymodbus.factory :members: :undoc-members: :show-inheritance: -.. automodule:: pymodbus.events +.. automodule:: pymodbus.payload :members: :undoc-members: :show-inheritance: -.. automodule:: pymodbus.exceptions +.. automodule:: pymodbus.transaction :members: :undoc-members: :show-inheritance: -.. automodule:: pymodbus.factory +.. automodule:: pymodbus.utilities :members: :undoc-members: :show-inheritance: -.. automodule:: pymodbus.file_message + +PDU classes +=========== + +.. automodule:: pymodbus.pdu.bit_read_message :members: :undoc-members: :show-inheritance: -.. automodule:: pymodbus.mei_message +.. automodule:: pymodbus.pdu.bit_write_message :members: :undoc-members: :show-inheritance: -.. automodule:: pymodbus.other_message +.. automodule:: pymodbus.pdu.diag_message :members: :undoc-members: :show-inheritance: -.. automodule:: pymodbus.payload +.. automodule:: pymodbus.pdu.file_message :members: :undoc-members: :show-inheritance: -.. automodule:: pymodbus.pdu +.. automodule:: pymodbus.pdu.mei_message :members: :undoc-members: :show-inheritance: - :noindex: -.. automodule:: pymodbus.register_read_message +.. automodule:: pymodbus.pdu.other_message :members: :undoc-members: :show-inheritance: -.. automodule:: pymodbus.register_write_message +.. automodule:: pymodbus.pdu.pdu :members: :undoc-members: :show-inheritance: + :noindex: -.. automodule:: pymodbus.transaction +.. automodule:: pymodbus.pdu.register_read_message :members: :undoc-members: :show-inheritance: -.. automodule:: pymodbus.utilities +.. automodule:: pymodbus.pdu.register_write_message :members: :undoc-members: :show-inheritance: diff --git a/examples/client_custom_msg.py b/examples/client_custom_msg.py index 01edf40f2..e4af4f4f1 100755 --- a/examples/client_custom_msg.py +++ b/examples/client_custom_msg.py @@ -14,9 +14,9 @@ import struct from pymodbus import FramerType -from pymodbus.bit_read_message import ReadCoilsRequest from pymodbus.client import AsyncModbusTcpClient as ModbusClient from pymodbus.pdu import ModbusExceptions, ModbusRequest, ModbusResponse +from pymodbus.pdu.bit_read_message import ReadCoilsRequest # --------------------------------------------------------------------------- # diff --git a/examples/message_generator.py b/examples/message_generator.py index eaf7f036e..cf885b36c 100755 --- a/examples/message_generator.py +++ b/examples/message_generator.py @@ -4,14 +4,14 @@ import codecs as c import logging -import pymodbus.bit_read_message as modbus_bit -import pymodbus.bit_write_message as modbus_bit_write -import pymodbus.diag_message as modbus_diag -import pymodbus.file_message as modbus_file -import pymodbus.mei_message as modbus_mei -import pymodbus.other_message as modbus_other -import pymodbus.register_read_message as modbus_register -import pymodbus.register_write_message as modbus_register_write +import pymodbus.pdu.bit_read_message as modbus_bit +import pymodbus.pdu.bit_write_message as modbus_bit_write +import pymodbus.pdu.diag_message as modbus_diag +import pymodbus.pdu.file_message as modbus_file +import pymodbus.pdu.mei_message as modbus_mei +import pymodbus.pdu.other_message as modbus_other +import pymodbus.pdu.register_read_message as modbus_register +import pymodbus.pdu.register_write_message as modbus_register_write from pymodbus.transaction import ( ModbusAsciiFramer, ModbusRtuFramer, diff --git a/pymodbus/client/mixin.py b/pymodbus/client/mixin.py index f686761aa..bb5346d25 100644 --- a/pymodbus/client/mixin.py +++ b/pymodbus/client/mixin.py @@ -5,14 +5,14 @@ from enum import Enum from typing import Any, Generic, TypeVar -import pymodbus.bit_read_message as pdu_bit_read -import pymodbus.bit_write_message as pdu_bit_write -import pymodbus.diag_message as pdu_diag -import pymodbus.file_message as pdu_file_msg -import pymodbus.mei_message as pdu_mei -import pymodbus.other_message as pdu_other_msg -import pymodbus.register_read_message as pdu_reg_read -import pymodbus.register_write_message as pdu_req_write +import pymodbus.pdu.bit_read_message as pdu_bit_read +import pymodbus.pdu.bit_write_message as pdu_bit_write +import pymodbus.pdu.diag_message as pdu_diag +import pymodbus.pdu.file_message as pdu_file_msg +import pymodbus.pdu.mei_message as pdu_mei +import pymodbus.pdu.other_message as pdu_other_msg +import pymodbus.pdu.register_read_message as pdu_reg_read +import pymodbus.pdu.register_write_message as pdu_req_write from pymodbus.exceptions import ModbusException from pymodbus.pdu import ModbusRequest diff --git a/pymodbus/factory.py b/pymodbus/factory.py index c351fa687..50302b50c 100644 --- a/pymodbus/factory.py +++ b/pymodbus/factory.py @@ -12,17 +12,17 @@ # pylint: disable=missing-type-doc from collections.abc import Callable -from pymodbus import bit_read_message as bit_r_msg -from pymodbus import bit_write_message as bit_w_msg -from pymodbus import diag_message as diag_msg -from pymodbus import file_message as file_msg -from pymodbus import mei_message as mei_msg -from pymodbus import other_message as o_msg -from pymodbus import pdu -from pymodbus import register_read_message as reg_r_msg -from pymodbus import register_write_message as reg_w_msg from pymodbus.exceptions import MessageRegisterException, ModbusException from pymodbus.logging import Log +from pymodbus.pdu import bit_read_message as bit_r_msg +from pymodbus.pdu import bit_write_message as bit_w_msg +from pymodbus.pdu import diag_message as diag_msg +from pymodbus.pdu import file_message as file_msg +from pymodbus.pdu import mei_message as mei_msg +from pymodbus.pdu import other_message as o_msg +from pymodbus.pdu import pdu +from pymodbus.pdu import register_read_message as reg_r_msg +from pymodbus.pdu import register_write_message as reg_w_msg # --------------------------------------------------------------------------- # diff --git a/pymodbus/pdu/__init__.py b/pymodbus/pdu/__init__.py new file mode 100644 index 000000000..da3d86f8f --- /dev/null +++ b/pymodbus/pdu/__init__.py @@ -0,0 +1,18 @@ +"""Framer.""" +__all__ = [ + "ExceptionResponse", + "IllegalFunctionRequest", + "ModbusExceptions", + "ModbusPDU", + "ModbusRequest", + "ModbusResponse", +] + +from pymodbus.pdu.pdu import ( + ExceptionResponse, + IllegalFunctionRequest, + ModbusExceptions, + ModbusPDU, + ModbusRequest, + ModbusResponse, +) diff --git a/pymodbus/bit_read_message.py b/pymodbus/pdu/bit_read_message.py similarity index 100% rename from pymodbus/bit_read_message.py rename to pymodbus/pdu/bit_read_message.py diff --git a/pymodbus/bit_write_message.py b/pymodbus/pdu/bit_write_message.py similarity index 100% rename from pymodbus/bit_write_message.py rename to pymodbus/pdu/bit_write_message.py diff --git a/pymodbus/diag_message.py b/pymodbus/pdu/diag_message.py similarity index 100% rename from pymodbus/diag_message.py rename to pymodbus/pdu/diag_message.py diff --git a/pymodbus/file_message.py b/pymodbus/pdu/file_message.py similarity index 100% rename from pymodbus/file_message.py rename to pymodbus/pdu/file_message.py diff --git a/pymodbus/mei_message.py b/pymodbus/pdu/mei_message.py similarity index 100% rename from pymodbus/mei_message.py rename to pymodbus/pdu/mei_message.py diff --git a/pymodbus/other_message.py b/pymodbus/pdu/other_message.py similarity index 100% rename from pymodbus/other_message.py rename to pymodbus/pdu/other_message.py diff --git a/pymodbus/pdu.py b/pymodbus/pdu/pdu.py similarity index 100% rename from pymodbus/pdu.py rename to pymodbus/pdu/pdu.py diff --git a/pymodbus/register_read_message.py b/pymodbus/pdu/register_read_message.py similarity index 100% rename from pymodbus/register_read_message.py rename to pymodbus/pdu/register_read_message.py diff --git a/pymodbus/register_write_message.py b/pymodbus/pdu/register_write_message.py similarity index 100% rename from pymodbus/register_write_message.py rename to pymodbus/pdu/register_write_message.py diff --git a/test/framers/generator.py b/test/framers/generator.py index 038f0d2b6..04344a823 100755 --- a/test/framers/generator.py +++ b/test/framers/generator.py @@ -9,7 +9,7 @@ ModbusTlsFramer, ) from pymodbus.pdu import ModbusExceptions as merror -from pymodbus.register_read_message import ( +from pymodbus.pdu.register_read_message import ( ReadHoldingRegistersRequest, ReadHoldingRegistersResponse, ) diff --git a/test/framers/test_old_framers.py b/test/framers/test_old_framers.py index 6d6aa3e68..7101137fa 100644 --- a/test/framers/test_old_framers.py +++ b/test/framers/test_old_framers.py @@ -4,7 +4,6 @@ import pytest from pymodbus import FramerType -from pymodbus.bit_read_message import ReadCoilsRequest from pymodbus.client.base import ModbusBaseClient from pymodbus.exceptions import ModbusIOException from pymodbus.factory import ClientDecoder @@ -14,6 +13,7 @@ ModbusSocketFramer, ModbusTlsFramer, ) +from pymodbus.pdu.bit_read_message import ReadCoilsRequest from pymodbus.transport import CommType from pymodbus.utilities import ModbusTransactionState diff --git a/test/sub_client/test_client.py b/test/sub_client/test_client.py index 40fc0e86f..b853e8ab0 100755 --- a/test/sub_client/test_client.py +++ b/test/sub_client/test_client.py @@ -6,14 +6,14 @@ import pytest -import pymodbus.bit_read_message as pdu_bit_read -import pymodbus.bit_write_message as pdu_bit_write import pymodbus.client as lib_client -import pymodbus.diag_message as pdu_diag -import pymodbus.file_message as pdu_file_msg -import pymodbus.other_message as pdu_other_msg -import pymodbus.register_read_message as pdu_reg_read -import pymodbus.register_write_message as pdu_req_write +import pymodbus.pdu.bit_read_message as pdu_bit_read +import pymodbus.pdu.bit_write_message as pdu_bit_write +import pymodbus.pdu.diag_message as pdu_diag +import pymodbus.pdu.file_message as pdu_file_msg +import pymodbus.pdu.other_message as pdu_other_msg +import pymodbus.pdu.register_read_message as pdu_reg_read +import pymodbus.pdu.register_write_message as pdu_req_write from examples.helper import get_certificate from pymodbus import FramerType from pymodbus.client.base import ModbusBaseClient diff --git a/test/sub_function_codes/test_all_messages.py b/test/sub_function_codes/test_all_messages.py index 2ab64d879..bfb669df9 100644 --- a/test/sub_function_codes/test_all_messages.py +++ b/test/sub_function_codes/test_all_messages.py @@ -1,17 +1,17 @@ """Test all messages.""" -from pymodbus.bit_read_message import ( +from pymodbus.pdu.bit_read_message import ( ReadCoilsRequest, ReadCoilsResponse, ReadDiscreteInputsRequest, ReadDiscreteInputsResponse, ) -from pymodbus.bit_write_message import ( +from pymodbus.pdu.bit_write_message import ( WriteMultipleCoilsRequest, WriteMultipleCoilsResponse, WriteSingleCoilRequest, WriteSingleCoilResponse, ) -from pymodbus.register_read_message import ( +from pymodbus.pdu.register_read_message import ( ReadHoldingRegistersRequest, ReadHoldingRegistersResponse, ReadInputRegistersRequest, @@ -19,7 +19,7 @@ ReadWriteMultipleRegistersRequest, ReadWriteMultipleRegistersResponse, ) -from pymodbus.register_write_message import ( +from pymodbus.pdu.register_write_message import ( WriteMultipleRegistersRequest, WriteMultipleRegistersResponse, WriteSingleRegisterRequest, diff --git a/test/sub_function_codes/test_bit_read_messages.py b/test/sub_function_codes/test_bit_read_messages.py index 21817d0d7..5d190f023 100644 --- a/test/sub_function_codes/test_bit_read_messages.py +++ b/test/sub_function_codes/test_bit_read_messages.py @@ -9,13 +9,13 @@ import struct from test.conftest import MockContext -from pymodbus.bit_read_message import ( +from pymodbus.pdu import ModbusExceptions +from pymodbus.pdu.bit_read_message import ( ReadBitsRequestBase, ReadBitsResponseBase, ReadCoilsRequest, ReadDiscreteInputsRequest, ) -from pymodbus.pdu import ModbusExceptions res = [True] * 21 diff --git a/test/sub_function_codes/test_bit_write_messages.py b/test/sub_function_codes/test_bit_write_messages.py index 7552cd4e5..94e2066f8 100644 --- a/test/sub_function_codes/test_bit_write_messages.py +++ b/test/sub_function_codes/test_bit_write_messages.py @@ -8,13 +8,13 @@ """ from test.conftest import FakeList, MockContext -from pymodbus.bit_write_message import ( +from pymodbus.pdu import ModbusExceptions +from pymodbus.pdu.bit_write_message import ( WriteMultipleCoilsRequest, WriteMultipleCoilsResponse, WriteSingleCoilRequest, WriteSingleCoilResponse, ) -from pymodbus.pdu import ModbusExceptions # ---------------------------------------------------------------------------# diff --git a/test/sub_function_codes/test_diag_messages.py b/test/sub_function_codes/test_diag_messages.py index 5db301894..673222c93 100644 --- a/test/sub_function_codes/test_diag_messages.py +++ b/test/sub_function_codes/test_diag_messages.py @@ -2,7 +2,8 @@ import pytest from pymodbus.constants import ModbusPlusOperation -from pymodbus.diag_message import ( +from pymodbus.exceptions import NotImplementedException +from pymodbus.pdu.diag_message import ( ChangeAsciiInputDelimiterRequest, ChangeAsciiInputDelimiterResponse, ClearCountersRequest, @@ -42,7 +43,6 @@ ReturnSlaveNoResponseCountRequest, ReturnSlaveNoResponseCountResponse, ) -from pymodbus.exceptions import NotImplementedException class TestDataStore: diff --git a/test/sub_function_codes/test_mei_messages.py b/test/sub_function_codes/test_mei_messages.py index b0db6111a..d553e3461 100644 --- a/test/sub_function_codes/test_mei_messages.py +++ b/test/sub_function_codes/test_mei_messages.py @@ -7,7 +7,7 @@ from pymodbus.constants import DeviceInformation from pymodbus.device import ModbusControlBlock -from pymodbus.mei_message import ( +from pymodbus.pdu.mei_message import ( ReadDeviceInformationRequest, ReadDeviceInformationResponse, ) diff --git a/test/sub_function_codes/test_other_messages.py b/test/sub_function_codes/test_other_messages.py index 5b87c8154..eb43ad21e 100644 --- a/test/sub_function_codes/test_other_messages.py +++ b/test/sub_function_codes/test_other_messages.py @@ -1,7 +1,7 @@ """Test other messages.""" from unittest import mock -import pymodbus.other_message as pymodbus_message +import pymodbus.pdu.other_message as pymodbus_message class TestOtherMessage: @@ -86,7 +86,7 @@ def test_get_comm_event_log_with_events(self): def test_report_slave_id_request(self): """Test report slave id request.""" - with mock.patch("pymodbus.other_message.DeviceInformationFactory") as dif: + with mock.patch("pymodbus.pdu.other_message.DeviceInformationFactory") as dif: # First test regular identity strings identity = { 0x00: "VN", # VendorName @@ -126,7 +126,7 @@ def test_report_slave_id_request(self): def test_report_slave_id(self): """Test report slave id.""" - with mock.patch("pymodbus.other_message.DeviceInformationFactory") as dif: + with mock.patch("pymodbus.pdu.other_message.DeviceInformationFactory") as dif: dif.get.return_value = {} request = pymodbus_message.ReportSlaveIdRequest() request.decode(b"\x12") diff --git a/test/sub_function_codes/test_register_read_messages.py b/test/sub_function_codes/test_register_read_messages.py index dcb1ef6c5..6b0072c21 100644 --- a/test/sub_function_codes/test_register_read_messages.py +++ b/test/sub_function_codes/test_register_read_messages.py @@ -2,7 +2,7 @@ from test.conftest import FakeList, MockContext from pymodbus.pdu import ModbusExceptions -from pymodbus.register_read_message import ( +from pymodbus.pdu.register_read_message import ( ReadHoldingRegistersRequest, ReadHoldingRegistersResponse, ReadInputRegistersRequest, diff --git a/test/sub_function_codes/test_register_write_messages.py b/test/sub_function_codes/test_register_write_messages.py index 2327d090b..e89e97802 100644 --- a/test/sub_function_codes/test_register_write_messages.py +++ b/test/sub_function_codes/test_register_write_messages.py @@ -3,7 +3,7 @@ from pymodbus.payload import BinaryPayloadBuilder, Endian from pymodbus.pdu import ModbusExceptions -from pymodbus.register_write_message import ( +from pymodbus.pdu.register_write_message import ( MaskWriteRegisterRequest, MaskWriteRegisterResponse, WriteMultipleRegistersRequest, diff --git a/test/sub_server/test_server_asyncio.py b/test/sub_server/test_server_asyncio.py index 34c16616b..e3de50956 100755 --- a/test/sub_server/test_server_asyncio.py +++ b/test/sub_server/test_server_asyncio.py @@ -266,7 +266,7 @@ async def test_async_tcp_server_modbus_error(self): BasicClient.data = TEST_DATA await self.start_server() with mock.patch( - "pymodbus.register_read_message.ReadHoldingRegistersRequest.execute", + "pymodbus.pdu.register_read_message.ReadHoldingRegistersRequest.execute", side_effect=NoSuchSlaveException, ): await self.connect_server() diff --git a/test/test_file_message.py b/test/test_file_message.py index ee71ba1ac..e210f6bcb 100644 --- a/test/test_file_message.py +++ b/test/test_file_message.py @@ -8,7 +8,8 @@ """ from test.conftest import MockContext -from pymodbus.file_message import ( +from pymodbus.pdu import ModbusExceptions +from pymodbus.pdu.file_message import ( FileRecord, ReadFifoQueueRequest, ReadFifoQueueResponse, @@ -17,7 +18,6 @@ WriteFileRecordRequest, WriteFileRecordResponse, ) -from pymodbus.pdu import ModbusExceptions TEST_MESSAGE = b"\x00\n\x00\x08\x00\x01\x00\x02\x00\x03\x00\x04" diff --git a/test/test_remote_datastore.py b/test/test_remote_datastore.py index 1f59c62b8..b12d792d4 100644 --- a/test/test_remote_datastore.py +++ b/test/test_remote_datastore.py @@ -3,12 +3,12 @@ import pytest -from pymodbus.bit_read_message import ReadCoilsResponse -from pymodbus.bit_write_message import WriteMultipleCoilsResponse from pymodbus.datastore.remote import RemoteSlaveContext from pymodbus.exceptions import NotImplementedException from pymodbus.pdu import ExceptionResponse -from pymodbus.register_read_message import ReadInputRegistersResponse +from pymodbus.pdu.bit_read_message import ReadCoilsResponse +from pymodbus.pdu.bit_write_message import WriteMultipleCoilsResponse +from pymodbus.pdu.register_read_message import ReadInputRegistersResponse class TestRemoteDataStore: From 9fa913afc0b6560a2834060bb8c35b60d444c8da Mon Sep 17 00:00:00 2001 From: sumguytho <48988983+sumguytho@users.noreply.github.com> Date: Sat, 13 Apr 2024 21:52:05 +0300 Subject: [PATCH 32/88] fixed kwargs not being expanded for actions on bit registers, adjusted tests to catch this issue (#2161) --- pymodbus/datastore/simulator.py | 5 +++-- test/sub_server/test_simulator.py | 14 ++++++++++++-- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/pymodbus/datastore/simulator.py b/pymodbus/datastore/simulator.py index 4c73a86cc..31b61d856 100644 --- a/pymodbus/datastore/simulator.py +++ b/pymodbus/datastore/simulator.py @@ -435,7 +435,7 @@ class ModbusSimulatorContext(ModbusBaseSlaveContext): "write": [ --> allow write, efault is ReadOnly [5, 5] --> start, end bytes, repeated as needed ], - "bits": [ --> Define bits (1 register == 1 byte) + "bits": [ --> Define bits (1 register == 2 bytes) [30, 31], --> start, end registers, repeated as needed {"addr": [32, 34], "value": 0xF1}, --> with value {"addr": [35, 36], "action": "increment"}, --> with action @@ -602,8 +602,9 @@ def getValues(self, func_code, address, count=1): for i in range(real_address, real_address + reg_count): reg = self.registers[i] if reg.action: + kwargs = reg.action_kwargs or {} self.action_methods[reg.action]( - self.registers, i, reg, reg.action_kwargs + self.registers, i, reg, **kwargs ) self.registers[i].count_read += 1 while count and bit_index < 16: diff --git a/test/sub_server/test_simulator.py b/test/sub_server/test_simulator.py index 46288e57f..aeefed8b7 100644 --- a/test/sub_server/test_simulator.py +++ b/test/sub_server/test_simulator.py @@ -528,7 +528,12 @@ def test_simulator_action_increment( exc_simulator.registers[30].value = regs[0] exc_simulator.registers[31].value = regs[1] for expect_value in expected: - regs = exc_simulator.getValues(FX_READ_REG, 30, reg_count) + if celltype != CellType.BITS: + regs = exc_simulator.getValues(FX_READ_REG, 30, reg_count) + else: + reg_bits = exc_simulator.getValues(FX_READ_BIT, 30 * 16, 16) + reg_value = sum([ bit * 2 ** i for i, bit in enumerate(reg_bits)]) + regs = [reg_value] if reg_count == 1: assert expect_value == regs[0], f"type({celltype})" else: @@ -563,7 +568,12 @@ def test_simulator_action_random(self, celltype, minval, maxval): is_int = celltype != CellType.FLOAT32 reg_count = 1 if celltype in (CellType.BITS, CellType.UINT16) else 2 for _i in range(100): - regs = exc_simulator.getValues(FX_READ_REG, 30, reg_count) + if celltype != CellType.BITS: + regs = exc_simulator.getValues(FX_READ_REG, 30, reg_count) + else: + reg_bits = exc_simulator.getValues(FX_READ_BIT, 30 * 16, 16) + reg_value = sum([ bit * 2 ** i for i, bit in enumerate(reg_bits)]) + regs = [reg_value] if reg_count == 1: new_value = regs[0] else: From e9c187e9cafaf92901750c8a522b9325dba1d830 Mon Sep 17 00:00:00 2001 From: jan iversen Date: Sun, 14 Apr 2024 12:27:03 +0200 Subject: [PATCH 33/88] Merge 3.6.8 effects. --- pymodbus/client/base.py | 6 +++--- pymodbus/framer/socket.py | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/pymodbus/client/base.py b/pymodbus/client/base.py index b6ba1f174..95dcaf1a4 100644 --- a/pymodbus/client/base.py +++ b/pymodbus/client/base.py @@ -167,14 +167,14 @@ async def async_execute(self, request) -> ModbusResponse: async with self._lock: req = self.build_response(request.transaction_id) if not count or not self.no_resend_on_retry: - self.framer.resetFrame() - self.send(packet) + self.ctx.framer.resetFrame() + self.ctx.send(packet) if self.broadcast_enable and not request.slave_id: resp = None break try: resp = await asyncio.wait_for( - req, timeout=self.comm_params.timeout_connect + req, timeout=self.ctx.comm_params.timeout_connect ) break except asyncio.exceptions.TimeoutError: diff --git a/pymodbus/framer/socket.py b/pymodbus/framer/socket.py index 5b1081049..793e37f8e 100644 --- a/pymodbus/framer/socket.py +++ b/pymodbus/framer/socket.py @@ -15,7 +15,7 @@ class FramerSocket(FramerBase): * length = uid + function code + data """ - MIN_SIZE = 9 + MIN_SIZE = 8 def decode(self, data: bytes) -> tuple[int, int, int, bytes]: """Decode ADU.""" @@ -28,8 +28,8 @@ def decode(self, data: bytes) -> tuple[int, int, int, bytes]: if used_len < msg_len: Log.debug("Short frame: {} wait for more data", data, ":hex") return 0, 0, 0, self.EMPTY - if msg_len == 2 and used_len >= self.MIN_SIZE: - msg_len = 3 + if msg_len == 8 and used_len == 9: + msg_len = 9 return msg_len, msg_tid, msg_dev, data[7:msg_len] def encode(self, pdu: bytes, device_id: int, tid: int) -> bytes: From d1aed6f3b9f46b7e7fa9ef1f4cd935c60102b412 Mon Sep 17 00:00:00 2001 From: jan iversen Date: Mon, 15 Apr 2024 21:23:20 +0200 Subject: [PATCH 34/88] datastore: add async_setValues/getValues methods (#2165) Co-authored-by: Ilkka Ollakka --- pymodbus/datastore/context.py | 47 ++++++++++++++++++++++++++++++++--- pymodbus/datastore/store.py | 25 ++++++++++++++++++- test/test_remote_datastore.py | 37 +++++++++++++++++++++++++++ test/test_sparse_datastore.py | 23 +++++++++++++++++ 4 files changed, 128 insertions(+), 4 deletions(-) diff --git a/pymodbus/datastore/context.py b/pymodbus/datastore/context.py index 859af60c6..d7915c0bc 100644 --- a/pymodbus/datastore/context.py +++ b/pymodbus/datastore/context.py @@ -1,18 +1,21 @@ """Context for datastore.""" + +from __future__ import annotations + # pylint: disable=missing-type-doc from pymodbus.datastore.store import ModbusSequentialDataBlock from pymodbus.exceptions import NoSuchSlaveException from pymodbus.logging import Log -class ModbusBaseSlaveContext: # pylint: disable=too-few-public-methods +class ModbusBaseSlaveContext: """Interface for a modbus slave data context. Derived classes must implemented the following methods: reset(self) validate(self, fx, address, count=1) - getValues(self, fx, address, count=1) - setValues(self, fx, address, values) + getValues/async_getValues(self, fc_as_hex, address, count=1) + setValues/async_setValues(self, fc_as_hex, address, values) """ _fx_mapper = {2: "d", 4: "i"} @@ -27,6 +30,44 @@ def decode(self, fx): """ return self._fx_mapper[fx] + async def async_getValues(self, fc_as_hex: int, address: int, count: int = 1) -> list[int | bool | None]: + """Get `count` values from datastore. + + :param fc_as_hex: The function we are working with + :param address: The starting address + :param count: The number of values to retrieve + :returns: The requested values from a:a+c + """ + return self.getValues(fc_as_hex, address, count) + + async def async_setValues(self, fc_as_hex: int, address: int, values: list[int | bool]) -> None: + """Set the datastore with the supplied values. + + :param fc_as_hex: The function we are working with + :param address: The starting address + :param values: The new values to be set + """ + self.setValues(fc_as_hex, address, values) + + def getValues(self, fc_as_hex: int, address: int, count: int = 1) -> list[int | bool | None]: + """Get `count` values from datastore. + + :param fc_as_hex: The function we are working with + :param address: The starting address + :param count: The number of values to retrieve + :returns: The requested values from a:a+c + """ + Log.error("getValues({},{},{}) not implemented!", fc_as_hex, address, count) + return [] + + def setValues(self, fc_as_hex: int, address: int, values: list[int | bool]) -> None: + """Set the datastore with the supplied values. + + :param fc_as_hex: The function we are working with + :param address: The starting address + :param values: The new values to be set + """ + # ---------------------------------------------------------------------------# # Slave Contexts diff --git a/pymodbus/datastore/store.py b/pymodbus/datastore/store.py index 1a52c4429..51cfbef86 100644 --- a/pymodbus/datastore/store.py +++ b/pymodbus/datastore/store.py @@ -72,6 +72,11 @@ class BaseModbusDataBlock(ABC, Generic[V]): getValues(self, address, count=1) setValues(self, address, values) reset(self) + + Derived classes can implemented the following async methods: + async_getValues(self, address, count=1) + async_setValues(self, address, values) + but are not needed since these standard call the sync. methods. """ values: V @@ -87,6 +92,15 @@ def validate(self, address:int, count=1) -> bool: :raises TypeError: """ + async def async_getValues(self, address: int, count=1) -> Iterable: + """Return the requested values from the datastore. + + :param address: The starting address + :param count: The number of values to retrieve + :raises TypeError: + """ + return self.getValues(address, count) + @abstractmethod def getValues(self, address:int, count=1) -> Iterable: """Return the requested values from the datastore. @@ -96,9 +110,18 @@ def getValues(self, address:int, count=1) -> Iterable: :raises TypeError: """ + async def async_setValues(self, address: int, values: list[int|bool]) -> None: + """Set the requested values in the datastore. + + :param address: The starting address + :param values: The values to store + :raises TypeError: + """ + self.setValues(address, values) + @abstractmethod def setValues(self, address:int, values) -> None: - """Return the requested values from the datastore. + """Set the requested values in the datastore. :param address: The starting address :param values: The values to store diff --git a/test/test_remote_datastore.py b/test/test_remote_datastore.py index b12d792d4..0b6c5d217 100644 --- a/test/test_remote_datastore.py +++ b/test/test_remote_datastore.py @@ -1,4 +1,5 @@ """Test remote datastore.""" + from unittest import mock import pytest @@ -34,6 +35,18 @@ def test_remote_slave_set_values(self): # assert result.exception_code == 0x02 # assert result.function_code == 0x90 + async def test_remote_slave_async_set_values(self): + """Test setting values against a remote slave context.""" + client = mock.MagicMock() + client.write_coils = mock.MagicMock(return_value=WriteMultipleCoilsResponse()) + client.write_registers = mock.MagicMock( + return_value=ExceptionResponse(0x10, 0x02) + ) + + context = RemoteSlaveContext(client) + await context.async_setValues(0x0F, 0, [1]) + await context.async_setValues(0x10, 1, [1]) + def test_remote_slave_get_values(self): """Test getting values from a remote slave context.""" client = mock.MagicMock() @@ -54,6 +67,30 @@ def test_remote_slave_get_values(self): result = context.getValues(3, 0, 10) assert result != [10] * 10 + async def test_remote_slave_async_get_values(self): + """Test getting values from a remote slave context.""" + client = mock.MagicMock() + client.read_coils = mock.MagicMock(return_value=ReadCoilsResponse([1] * 10)) + client.read_input_registers = mock.MagicMock( + return_value=ReadInputRegistersResponse([10] * 10) + ) + client.read_holding_registers = mock.MagicMock( + return_value=ExceptionResponse(0x15) + ) + + context = RemoteSlaveContext(client) + context.validate(1, 0, 10) + result = await context.async_getValues(1, 0, 10) + assert result == [1] * 10 + + context.validate(4, 0, 10) + result = await context.async_getValues(4, 0, 10) + assert result == [10] * 10 + + context.validate(3, 0, 10) + result = await context.async_getValues(3, 0, 10) + assert result != [10] * 10 + def test_remote_slave_validate_values(self): """Test validating against a remote slave context.""" client = mock.MagicMock() diff --git a/test/test_sparse_datastore.py b/test/test_sparse_datastore.py index 409fc3e01..250489bb8 100644 --- a/test/test_sparse_datastore.py +++ b/test/test_sparse_datastore.py @@ -1,9 +1,32 @@ """Test framers.""" +import pytest from pymodbus.datastore import ModbusSparseDataBlock +@pytest.mark.asyncio() +async def test_check_async_sparsedatastore(): + """Test check frame.""" + data_in_block = { + 1: 6720, + 2: 130, + 30: [0x0D, 0xFE], + 105: [1, 2, 3, 4], + 20000: [45, 241, 48], + 20008: 38, + 48140: [0x4208, 0xCCCD], + } + datablock = ModbusSparseDataBlock(data_in_block) + for key, entry in data_in_block.items(): + if isinstance(entry, int): + entry = [entry] + for value in entry: + assert datablock.validate(key, 1) + assert await datablock.async_getValues(key, 1) == [value] + key += 1 + + def test_check_sparsedatastore(): """Test check frame.""" data_in_block = { From 9c4466b22384009eced59be916698b716df2ab0c Mon Sep 17 00:00:00 2001 From: Ilkka Ollakka <14361597+ilkka-ollakka@users.noreply.github.com> Date: Tue, 16 Apr 2024 12:00:44 +0300 Subject: [PATCH 35/88] Request/Response: change execute to be async method (#2142) --- pymodbus/pdu/bit_read_message.py | 4 +-- pymodbus/pdu/bit_write_message.py | 4 +-- pymodbus/pdu/diag_message.py | 36 +++++++++---------- pymodbus/pdu/mei_message.py | 2 +- pymodbus/pdu/other_message.py | 8 ++--- pymodbus/pdu/pdu.py | 2 +- pymodbus/pdu/register_read_message.py | 6 ++-- pymodbus/pdu/register_write_message.py | 6 ++-- pymodbus/server/async_io.py | 11 ++---- test/sub_client/test_client.py | 2 +- .../test_bit_read_messages.py | 12 +++---- .../test_bit_write_messages.py | 18 +++++----- test/sub_function_codes/test_diag_messages.py | 14 ++++---- test/sub_function_codes/test_mei_messages.py | 16 ++++----- .../sub_function_codes/test_other_messages.py | 24 ++++++------- .../test_register_read_messages.py | 24 ++++++------- .../test_register_write_messages.py | 32 ++++++++--------- test/test_factory.py | 6 ++-- test/test_pdu.py | 4 +-- 19 files changed, 112 insertions(+), 119 deletions(-) diff --git a/pymodbus/pdu/bit_read_message.py b/pymodbus/pdu/bit_read_message.py index b5569fc4a..7d6b4c6a5 100644 --- a/pymodbus/pdu/bit_read_message.py +++ b/pymodbus/pdu/bit_read_message.py @@ -155,7 +155,7 @@ def __init__(self, address=None, count=None, slave=0, **kwargs): """ ReadBitsRequestBase.__init__(self, address, count, slave, **kwargs) - def execute(self, context): + async def execute(self, context): """Run a read coils request against a datastore. Before running the request, we make sure that the request is in @@ -223,7 +223,7 @@ def __init__(self, address=None, count=None, slave=0, **kwargs): """ ReadBitsRequestBase.__init__(self, address, count, slave, **kwargs) - def execute(self, context): + async def execute(self, context): """Run a read discrete input request against a datastore. Before running the request, we make sure that the request is in diff --git a/pymodbus/pdu/bit_write_message.py b/pymodbus/pdu/bit_write_message.py index 0af65992d..f3ea83c2e 100644 --- a/pymodbus/pdu/bit_write_message.py +++ b/pymodbus/pdu/bit_write_message.py @@ -79,7 +79,7 @@ def decode(self, data): self.address, value = struct.unpack(">HH", data) self.value = value == ModbusStatus.ON - def execute(self, context): + async def execute(self, context): """Run a write coil request against a datastore. :param context: The datastore to request from @@ -213,7 +213,7 @@ def decode(self, data): values = unpack_bitstring(data[5:]) self.values = values[:count] - def execute(self, context): + async def execute(self, context): """Run a write coils request against a datastore. :param context: The datastore to request from diff --git a/pymodbus/pdu/diag_message.py b/pymodbus/pdu/diag_message.py index a35fe0e75..e484ecd74 100644 --- a/pymodbus/pdu/diag_message.py +++ b/pymodbus/pdu/diag_message.py @@ -199,7 +199,7 @@ def __init__(self, data=0x0000, **kwargs): DiagnosticStatusRequest.__init__(self, **kwargs) self.message = data - def execute(self, *args): + async def execute(self, *args): """Raise if not implemented.""" raise NotImplementedException("Diagnostic Message Has No Execute Method") @@ -245,7 +245,7 @@ def __init__(self, message=b"\x00\x00", slave=None, **kwargs): raise ModbusException(f"message({type(message)}) must be bytes") self.message = message - def execute(self, *_args): + async def execute(self, *_args): """Execute the loopback request (builds the response). :returns: The populated loopback response message @@ -301,7 +301,7 @@ def __init__(self, toggle=False, slave=None, **kwargs): else: self.message = [ModbusStatus.OFF] - def execute(self, *_args): + async def execute(self, *_args): """Clear event log and restart. :returns: The initialized response message @@ -343,7 +343,7 @@ class ReturnDiagnosticRegisterRequest(DiagnosticStatusSimpleRequest): sub_function_code = 0x0002 - def execute(self, *args): + async def execute(self, *args): """Execute the diagnostic request on the given device. :returns: The initialized response message @@ -377,7 +377,7 @@ class ChangeAsciiInputDelimiterRequest(DiagnosticStatusSimpleRequest): sub_function_code = 0x0003 - def execute(self, *args): + async def execute(self, *args): """Execute the diagnostic request on the given device. :returns: The initialized response message @@ -412,7 +412,7 @@ class ForceListenOnlyModeRequest(DiagnosticStatusSimpleRequest): sub_function_code = 0x0004 - def execute(self, *args): + async def execute(self, *args): """Execute the diagnostic request on the given device. :returns: The initialized response message @@ -451,7 +451,7 @@ class ClearCountersRequest(DiagnosticStatusSimpleRequest): sub_function_code = 0x000A - def execute(self, *args): + async def execute(self, *args): """Execute the diagnostic request on the given device. :returns: The initialized response message @@ -482,7 +482,7 @@ class ReturnBusMessageCountRequest(DiagnosticStatusSimpleRequest): sub_function_code = 0x000B - def execute(self, *args): + async def execute(self, *args): """Execute the diagnostic request on the given device. :returns: The initialized response message @@ -515,7 +515,7 @@ class ReturnBusCommunicationErrorCountRequest(DiagnosticStatusSimpleRequest): sub_function_code = 0x000C - def execute(self, *args): + async def execute(self, *args): """Execute the diagnostic request on the given device. :returns: The initialized response message @@ -548,7 +548,7 @@ class ReturnBusExceptionErrorCountRequest(DiagnosticStatusSimpleRequest): sub_function_code = 0x000D - def execute(self, *args): + async def execute(self, *args): """Execute the diagnostic request on the given device. :returns: The initialized response message @@ -581,7 +581,7 @@ class ReturnSlaveMessageCountRequest(DiagnosticStatusSimpleRequest): sub_function_code = 0x000E - def execute(self, *args): + async def execute(self, *args): """Execute the diagnostic request on the given device. :returns: The initialized response message @@ -614,7 +614,7 @@ class ReturnSlaveNoResponseCountRequest(DiagnosticStatusSimpleRequest): sub_function_code = 0x000F - def execute(self, *args): + async def execute(self, *args): """Execute the diagnostic request on the given device. :returns: The initialized response message @@ -648,7 +648,7 @@ class ReturnSlaveNAKCountRequest(DiagnosticStatusSimpleRequest): sub_function_code = 0x0010 - def execute(self, *args): + async def execute(self, *args): """Execute the diagnostic request on the given device. :returns: The initialized response message @@ -682,7 +682,7 @@ class ReturnSlaveBusyCountRequest(DiagnosticStatusSimpleRequest): sub_function_code = 0x0011 - def execute(self, *args): + async def execute(self, *args): """Execute the diagnostic request on the given device. :returns: The initialized response message @@ -717,7 +717,7 @@ class ReturnSlaveBusCharacterOverrunCountRequest(DiagnosticStatusSimpleRequest): sub_function_code = 0x0012 - def execute(self, *args): + async def execute(self, *args): """Execute the diagnostic request on the given device. :returns: The initialized response message @@ -750,7 +750,7 @@ class ReturnIopOverrunCountRequest(DiagnosticStatusSimpleRequest): sub_function_code = 0x0013 - def execute(self, *args): + async def execute(self, *args): """Execute the diagnostic request on the given device. :returns: The initialized response message @@ -783,7 +783,7 @@ class ClearOverrunCountRequest(DiagnosticStatusSimpleRequest): sub_function_code = 0x0014 - def execute(self, *args): + async def execute(self, *args): """Execute the diagnostic request on the given device. :returns: The initialized response message @@ -834,7 +834,7 @@ def get_response_pdu_size(self): data = 0 return 1 + 2 + 2 + 2 + data - def execute(self, *args): + async def execute(self, *args): """Execute the diagnostic request on the given device. :returns: The initialized response message diff --git a/pymodbus/pdu/mei_message.py b/pymodbus/pdu/mei_message.py index 8d94d3fc7..4eb2e596f 100644 --- a/pymodbus/pdu/mei_message.py +++ b/pymodbus/pdu/mei_message.py @@ -84,7 +84,7 @@ def decode(self, data): params = struct.unpack(">BBB", data) self.sub_function_code, self.read_code, self.object_id = params - def execute(self, _context): + async def execute(self, _context): """Run a read exception status request against the store. :returns: The populated response diff --git a/pymodbus/pdu/other_message.py b/pymodbus/pdu/other_message.py index 906d6673a..ec3e35110 100644 --- a/pymodbus/pdu/other_message.py +++ b/pymodbus/pdu/other_message.py @@ -54,7 +54,7 @@ def decode(self, data): :param data: The incoming data """ - def execute(self, _context=None): + async def execute(self, _context=None): """Run a read exception status request against the store. :returns: The populated response @@ -159,7 +159,7 @@ def decode(self, data): :param data: The incoming data """ - def execute(self, _context=None): + async def execute(self, _context=None): """Run a read exception status request against the store. :returns: The populated response @@ -270,7 +270,7 @@ def decode(self, data): :param data: The incoming data """ - def execute(self, _context=None): + async def execute(self, _context=None): """Run a read exception status request against the store. :returns: The populated response @@ -395,7 +395,7 @@ def decode(self, data): :param data: The incoming data """ - def execute(self, context=None): + async def execute(self, context=None): """Run a report slave id request against the store. :returns: The populated response diff --git a/pymodbus/pdu/pdu.py b/pymodbus/pdu/pdu.py index 13dd7981b..c50cf281a 100644 --- a/pymodbus/pdu/pdu.py +++ b/pymodbus/pdu/pdu.py @@ -252,7 +252,7 @@ def decode(self, _data): def encode(self): """Decode so this failure will run correctly.""" - def execute(self, _context): + async def execute(self, _context): """Build an illegal function request error response. :returns: The error response packet diff --git a/pymodbus/pdu/register_read_message.py b/pymodbus/pdu/register_read_message.py index 5d7369140..842e2a6b0 100644 --- a/pymodbus/pdu/register_read_message.py +++ b/pymodbus/pdu/register_read_message.py @@ -139,7 +139,7 @@ def __init__(self, address=None, count=None, slave=0, **kwargs): """ super().__init__(address, count, slave, **kwargs) - def execute(self, context): + async def execute(self, context): """Run a read holding request against a datastore. :param context: The datastore to request from @@ -200,7 +200,7 @@ def __init__(self, address=None, count=None, slave=0, **kwargs): """ super().__init__(address, count, slave, **kwargs) - def execute(self, context): + async def execute(self, context): """Run a read input request against a datastore. :param context: The datastore to request from @@ -310,7 +310,7 @@ def decode(self, data): register = struct.unpack(">H", data[i : i + 2])[0] self.write_registers.append(register) - def execute(self, context): + async def execute(self, context): """Run a write single register request against a datastore. :param context: The datastore to request from diff --git a/pymodbus/pdu/register_write_message.py b/pymodbus/pdu/register_write_message.py index 756b44055..e0970bbac 100644 --- a/pymodbus/pdu/register_write_message.py +++ b/pymodbus/pdu/register_write_message.py @@ -57,7 +57,7 @@ def decode(self, data): """ self.address, self.value = struct.unpack(">HH", data) - def execute(self, context): + async def execute(self, context): """Run a write single register request against a datastore. :param context: The datastore to request from @@ -200,7 +200,7 @@ def decode(self, data): for idx in range(5, (self.count * 2) + 5, 2): self.values.append(struct.unpack(">H", data[idx : idx + 2])[0]) - def execute(self, context): + async def execute(self, context): """Run a write single register request against a datastore. :param context: The datastore to request from @@ -321,7 +321,7 @@ def decode(self, data): """ self.address, self.and_mask, self.or_mask = struct.unpack(">HHH", data) - def execute(self, context): + async def execute(self, context): """Run a mask write register request against the store. :param context: The datastore to request from diff --git a/pymodbus/server/async_io.py b/pymodbus/server/async_io.py index f717345c1..d0c511bf4 100644 --- a/pymodbus/server/async_io.py +++ b/pymodbus/server/async_io.py @@ -184,17 +184,10 @@ async def _async_execute(self, request, *addr): # if broadcasting then execute on all slave contexts, # note response will be ignored for slave_id in self.server.context.slaves(): - response = request.execute(self.server.context[slave_id]) - # Temporary check while we move execute to async method - if asyncio.iscoroutine(response): - response = await response + response = await request.execute(self.server.context[slave_id]) else: context = self.server.context[request.slave_id] - response = request.execute(context) - - # Temporary check while we move execute to async method - if asyncio.iscoroutine(response): - response = await response + response = await request.execute(context) except NoSuchSlaveException: Log.error("requested slave does not exist: {}", request.slave_id) diff --git a/test/sub_client/test_client.py b/test/sub_client/test_client.py index b853e8ab0..360d22fb9 100755 --- a/test/sub_client/test_client.py +++ b/test/sub_client/test_client.py @@ -392,7 +392,7 @@ def __init__(self, base, req, retries=0): async def delayed_resp(self): """Send a response to a received packet.""" await asyncio.sleep(0.05) - resp = self.req.execute(self.ctx) + resp = await self.req.execute(self.ctx) pkt = self.base.ctx.framer.buildPacket(resp) self.base.ctx.data_received(pkt) diff --git a/test/sub_function_codes/test_bit_read_messages.py b/test/sub_function_codes/test_bit_read_messages.py index 5d190f023..e2c5a0b8a 100644 --- a/test/sub_function_codes/test_bit_read_messages.py +++ b/test/sub_function_codes/test_bit_read_messages.py @@ -85,7 +85,7 @@ def test_bit_read_base_requests(self): for request, expected in iter(messages.items()): assert request.encode() == expected - def test_bit_read_message_execute_value_errors(self): + async def test_bit_read_message_execute_value_errors(self): """Test bit read request encoding.""" context = MockContext() requests = [ @@ -93,10 +93,10 @@ def test_bit_read_message_execute_value_errors(self): ReadDiscreteInputsRequest(1, 0x800), ] for request in requests: - result = request.execute(context) + result = await request.execute(context) assert ModbusExceptions.IllegalValue == result.exception_code - def test_bit_read_message_execute_address_errors(self): + async def test_bit_read_message_execute_address_errors(self): """Test bit read request encoding.""" context = MockContext() requests = [ @@ -104,10 +104,10 @@ def test_bit_read_message_execute_address_errors(self): ReadDiscreteInputsRequest(1, 5), ] for request in requests: - result = request.execute(context) + result = await request.execute(context) assert ModbusExceptions.IllegalAddress == result.exception_code - def test_bit_read_message_execute_success(self): + async def test_bit_read_message_execute_success(self): """Test bit read request encoding.""" context = MockContext() context.validate = lambda a, b, c: True @@ -116,7 +116,7 @@ def test_bit_read_message_execute_success(self): ReadDiscreteInputsRequest(1, 5), ] for request in requests: - result = request.execute(context) + result = await request.execute(context) assert result.bits == [True] * 5 def test_bit_read_message_get_response_pdu(self): diff --git a/test/sub_function_codes/test_bit_write_messages.py b/test/sub_function_codes/test_bit_write_messages.py index 94e2066f8..8adf01352 100644 --- a/test/sub_function_codes/test_bit_write_messages.py +++ b/test/sub_function_codes/test_bit_write_messages.py @@ -81,45 +81,45 @@ def test_write_single_coil_request_encode(self): request = WriteSingleCoilRequest(1, False) assert request.encode() == b"\x00\x01\x00\x00" - def test_write_single_coil_execute(self): + async def test_write_single_coil_execute(self): """Test write single coil.""" context = MockContext(False, default=True) request = WriteSingleCoilRequest(2, True) - result = request.execute(context) + result = await request.execute(context) assert result.exception_code == ModbusExceptions.IllegalAddress context.valid = True - result = request.execute(context) + result = await request.execute(context) assert result.encode() == b"\x00\x02\xff\x00" context = MockContext(True, default=False) request = WriteSingleCoilRequest(2, False) - result = request.execute(context) + result = await request.execute(context) assert result.encode() == b"\x00\x02\x00\x00" - def test_write_multiple_coils_execute(self): + async def test_write_multiple_coils_execute(self): """Test write multiple coils.""" context = MockContext(False) # too many values request = WriteMultipleCoilsRequest(2, FakeList(0x123456)) - result = request.execute(context) + result = await request.execute(context) assert result.exception_code == ModbusExceptions.IllegalValue # bad byte count request = WriteMultipleCoilsRequest(2, [0x00] * 4) request.byte_count = 0x00 - result = request.execute(context) + result = await request.execute(context) assert result.exception_code == ModbusExceptions.IllegalValue # does not validate context.valid = False request = WriteMultipleCoilsRequest(2, [0x00] * 4) - result = request.execute(context) + result = await request.execute(context) assert result.exception_code == ModbusExceptions.IllegalAddress # validated request context.valid = True - result = request.execute(context) + result = await request.execute(context) assert result.encode() == b"\x00\x02\x00\x04" def test_write_multiple_coils_response(self): diff --git a/test/sub_function_codes/test_diag_messages.py b/test/sub_function_codes/test_diag_messages.py index 673222c93..d5caa11b6 100644 --- a/test/sub_function_codes/test_diag_messages.py +++ b/test/sub_function_codes/test_diag_messages.py @@ -137,12 +137,12 @@ def test_diagnostic_requests_decode(self): encoded = handle.encode() assert enc == encoded - def test_diagnostic_simple_requests(self): + async def test_diagnostic_simple_requests(self): """Testing diagnostic request messages encoding.""" request = DiagnosticStatusSimpleRequest(b"\x12\x34") request.sub_function_code = 0x1234 with pytest.raises(NotImplementedException): - request.execute() + await request.execute() assert request.encode() == b"\x12\x34\x12\x34" DiagnosticStatusSimpleResponse() @@ -158,10 +158,10 @@ def test_diagnostic_requests_encode(self): for msg, enc, _ in self.requests: assert msg().encode() == enc - def test_diagnostic_execute(self): + async def test_diagnostic_execute(self): """Testing diagnostic message execution.""" for message, encoded, executed in self.requests: - encoded = message().execute().encode() + encoded = (await message().execute()).encode() assert encoded == executed def test_return_query_data_request(self): @@ -190,13 +190,13 @@ def test_restart_communications_option(self): response = RestartCommunicationsOptionResponse(False) assert response.encode() == b"\x00\x01\x00\x00" - def test_get_clear_modbus_plus_request_execute(self): + async def test_get_clear_modbus_plus_request_execute(self): """Testing diagnostic message execution.""" request = GetClearModbusPlusRequest(data=ModbusPlusOperation.CLEAR_STATISTICS) - response = request.execute() + response = await request.execute() assert response.message == ModbusPlusOperation.CLEAR_STATISTICS request = GetClearModbusPlusRequest(data=ModbusPlusOperation.GET_STATISTICS) - response = request.execute() + response = await request.execute() resp = [ModbusPlusOperation.GET_STATISTICS] assert response.message == resp + [0x00] * 55 diff --git a/test/sub_function_codes/test_mei_messages.py b/test/sub_function_codes/test_mei_messages.py index d553e3461..371b23e17 100644 --- a/test/sub_function_codes/test_mei_messages.py +++ b/test/sub_function_codes/test_mei_messages.py @@ -43,7 +43,7 @@ def test_read_device_information_request_decode(self): assert handle.read_code == DeviceInformation.BASIC assert not handle.object_id - def test_read_device_information_request(self): + async def test_read_device_information_request(self): """Test basic bit message encoding/decoding.""" context = None control = ModbusControlBlock() @@ -53,7 +53,7 @@ def test_read_device_information_request(self): control.Identity.update({0x81: ["Test", "Repeated"]}) handle = ReadDeviceInformationRequest() - result = handle.execute(context) + result = await handle.execute(context) assert isinstance(result, ReadDeviceInformationResponse) assert result.information[0x00] == "Company" assert result.information[0x01] == "Product" @@ -64,20 +64,20 @@ def test_read_device_information_request(self): handle = ReadDeviceInformationRequest( read_code=DeviceInformation.EXTENDED, object_id=0x80 ) - result = handle.execute(context) + result = await handle.execute(context) assert result.information[0x81] == ["Test", "Repeated"] - def test_read_device_information_request_error(self): + async def test_read_device_information_request_error(self): """Test basic bit message encoding/decoding.""" handle = ReadDeviceInformationRequest() handle.read_code = -1 - assert handle.execute(None).function_code == 0xAB + assert (await handle.execute(None)).function_code == 0xAB handle.read_code = 0x05 - assert handle.execute(None).function_code == 0xAB + assert (await handle.execute(None)).function_code == 0xAB handle.object_id = -1 - assert handle.execute(None).function_code == 0xAB + assert (await handle.execute(None)).function_code == 0xAB handle.object_id = 0x100 - assert handle.execute(None).function_code == 0xAB + assert (await handle.execute(None)).function_code == 0xAB def test_read_device_information_encode(self): """Test that the read fifo queue response can encode.""" diff --git a/test/sub_function_codes/test_other_messages.py b/test/sub_function_codes/test_other_messages.py index eb43ad21e..f5c51d6d4 100644 --- a/test/sub_function_codes/test_other_messages.py +++ b/test/sub_function_codes/test_other_messages.py @@ -28,24 +28,24 @@ def test_other_messages_to_string(self): for message in self.responses: assert str(message) - def test_read_exception_status(self): + async def test_read_exception_status(self): """Test read exception status.""" request = pymodbus_message.ReadExceptionStatusRequest() request.decode(b"\x12") assert not request.encode() - assert request.execute().function_code == 0x07 + assert (await request.execute()).function_code == 0x07 response = pymodbus_message.ReadExceptionStatusResponse(0x12) assert response.encode() == b"\x12" response.decode(b"\x12") assert response.status == 0x12 - def test_get_comm_event_counter(self): + async def test_get_comm_event_counter(self): """Test get comm event counter.""" request = pymodbus_message.GetCommEventCounterRequest() request.decode(b"\x12") assert not request.encode() - assert request.execute().function_code == 0x0B + assert (await request.execute()).function_code == 0x0B response = pymodbus_message.GetCommEventCounterResponse(0x12) assert response.encode() == b"\x00\x00\x00\x12" @@ -56,12 +56,12 @@ def test_get_comm_event_counter(self): response.status = False assert response.encode() == b"\xFF\xFF\x00\x12" - def test_get_comm_event_log(self): + async def test_get_comm_event_log(self): """Test get comm event log.""" request = pymodbus_message.GetCommEventLogRequest() request.decode(b"\x12") assert not request.encode() - assert request.execute().function_code == 0x0C + assert (await request.execute()).function_code == 0x0C response = pymodbus_message.GetCommEventLogResponse() assert response.encode() == b"\x06\x00\x00\x00\x00\x00\x00" @@ -84,7 +84,7 @@ def test_get_comm_event_log_with_events(self): assert response.event_count == 0x12 assert response.events == [0x12, 0x34, 0x56] - def test_report_slave_id_request(self): + async def test_report_slave_id_request(self): """Test report slave id request.""" with mock.patch("pymodbus.pdu.other_message.DeviceInformationFactory") as dif: # First test regular identity strings @@ -103,7 +103,7 @@ def test_report_slave_id_request(self): expected_identity = "-".join(identity.values()).encode() request = pymodbus_message.ReportSlaveIdRequest() - response = request.execute() + response = await request.execute() assert response.identifier == expected_identity # Change to byte strings and test again (final result should be the same) @@ -121,20 +121,20 @@ def test_report_slave_id_request(self): dif.get.return_value = identity request = pymodbus_message.ReportSlaveIdRequest() - response = request.execute() + response = await request.execute() assert response.identifier == expected_identity - def test_report_slave_id(self): + async def test_report_slave_id(self): """Test report slave id.""" with mock.patch("pymodbus.pdu.other_message.DeviceInformationFactory") as dif: dif.get.return_value = {} request = pymodbus_message.ReportSlaveIdRequest() request.decode(b"\x12") assert not request.encode() - assert request.execute().function_code == 0x11 + assert (await request.execute()).function_code == 0x11 response = pymodbus_message.ReportSlaveIdResponse( - request.execute().identifier, True + (await request.execute()).identifier, True ) assert response.encode() == b"\tPymodbus\xff" diff --git a/test/sub_function_codes/test_register_read_messages.py b/test/sub_function_codes/test_register_read_messages.py index 6b0072c21..416ceb5b7 100644 --- a/test/sub_function_codes/test_register_read_messages.py +++ b/test/sub_function_codes/test_register_read_messages.py @@ -100,7 +100,7 @@ def test_register_read_response_decode(self): request.decode(response) assert request.registers == register - def test_register_read_requests_count_errors(self): + async def test_register_read_requests_count_errors(self): """This tests that the register request messages. will break on counts that are out of range @@ -117,10 +117,10 @@ def test_register_read_requests_count_errors(self): ), ] for request in requests: - result = request.execute(None) + result = await request.execute(None) assert ModbusExceptions.IllegalValue == result.exception_code - def test_register_read_requests_validate_errors(self): + async def test_register_read_requests_validate_errors(self): """This tests that the register request messages. will break on counts that are out of range @@ -133,10 +133,10 @@ def test_register_read_requests_validate_errors(self): # ReadWriteMultipleRegistersRequest(1,5,-1,5), ] for request in requests: - result = request.execute(context) + result = await request.execute(context) assert ModbusExceptions.IllegalAddress == result.exception_code - def test_register_read_requests_execute(self): + async def test_register_read_requests_execute(self): """This tests that the register request messages. will break on counts that are out of range @@ -147,34 +147,34 @@ def test_register_read_requests_execute(self): ReadInputRegistersRequest(-1, 5), ] for request in requests: - response = request.execute(context) + response = await request.execute(context) assert request.function_code == response.function_code - def test_read_write_multiple_registers_request(self): + async def test_read_write_multiple_registers_request(self): """Test read/write multiple registers.""" context = MockContext(True) request = ReadWriteMultipleRegistersRequest( read_address=1, read_count=10, write_address=1, write_registers=[0x00] ) - response = request.execute(context) + response = await request.execute(context) assert request.function_code == response.function_code - def test_read_write_multiple_registers_validate(self): + async def test_read_write_multiple_registers_validate(self): """Test read/write multiple registers.""" context = MockContext() context.validate = lambda f, a, c: a == 1 request = ReadWriteMultipleRegistersRequest( read_address=1, read_count=10, write_address=2, write_registers=[0x00] ) - response = request.execute(context) + response = await request.execute(context) assert response.exception_code == ModbusExceptions.IllegalAddress context.validate = lambda f, a, c: a == 2 - response = request.execute(context) + response = await request.execute(context) assert response.exception_code == ModbusExceptions.IllegalAddress request.write_byte_count = 0x100 - response = request.execute(context) + response = await request.execute(context) assert response.exception_code == ModbusExceptions.IllegalValue def test_read_write_multiple_registers_request_decode(self): diff --git a/test/sub_function_codes/test_register_write_messages.py b/test/sub_function_codes/test_register_write_messages.py index e89e97802..7697d56de 100644 --- a/test/sub_function_codes/test_register_write_messages.py +++ b/test/sub_function_codes/test_register_write_messages.py @@ -84,43 +84,43 @@ def test_serializing_to_string(self): for request in iter(self.write.keys()): assert str(request) - def test_write_single_register_request(self): + async def test_write_single_register_request(self): """Test write single register request.""" context = MockContext() request = WriteSingleRegisterRequest(0x00, 0xF0000) - result = request.execute(context) + result = await request.execute(context) assert result.exception_code == ModbusExceptions.IllegalValue request.value = 0x00FF - result = request.execute(context) + result = await request.execute(context) assert result.exception_code == ModbusExceptions.IllegalAddress context.valid = True - result = request.execute(context) + result = await request.execute(context) assert result.function_code == request.function_code - def test_write_multiple_register_request(self): + async def test_write_multiple_register_request(self): """Test write multiple register request.""" context = MockContext() request = WriteMultipleRegistersRequest(0x00, [0x00] * 10) - result = request.execute(context) + result = await request.execute(context) assert result.exception_code == ModbusExceptions.IllegalAddress request.count = 0x05 # bytecode != code * 2 - result = request.execute(context) + result = await request.execute(context) assert result.exception_code == ModbusExceptions.IllegalValue request.count = 0x800 # outside of range - result = request.execute(context) + result = await request.execute(context) assert result.exception_code == ModbusExceptions.IllegalValue context.valid = True request = WriteMultipleRegistersRequest(0x00, [0x00] * 10) - result = request.execute(context) + result = await request.execute(context) assert result.function_code == request.function_code request = WriteMultipleRegistersRequest(0x00, 0x00) - result = request.execute(context) + result = await request.execute(context) assert result.function_code == request.function_code # -----------------------------------------------------------------------# @@ -142,7 +142,7 @@ def test_mask_write_register_request_decode(self): assert handle.and_mask == 0x00F2 assert handle.or_mask == 0x0025 - def test_mask_write_register_request_execute(self): + async def test_mask_write_register_request_execute(self): """Test write register request valid execution.""" # The test uses the 4 nibbles of the 16-bit values to test # the combinations: @@ -152,23 +152,23 @@ def test_mask_write_register_request_execute(self): # and_mask=F, or_mask=F context = MockLastValuesContext(valid=True, default=0xAA55) handle = MaskWriteRegisterRequest(0x0000, 0x0F0F, 0x00FF) - result = handle.execute(context) + result = await handle.execute(context) assert isinstance(result, MaskWriteRegisterResponse) assert context.last_values == [0x0AF5] - def test_mask_write_register_request_invalid_execute(self): + async def test_mask_write_register_request_invalid_execute(self): """Test write register request execute with invalid data.""" context = MockContext(valid=False, default=0x0000) handle = MaskWriteRegisterRequest(0x0000, -1, 0x1010) - result = handle.execute(context) + result = await handle.execute(context) assert ModbusExceptions.IllegalValue == result.exception_code handle = MaskWriteRegisterRequest(0x0000, 0x0101, -1) - result = handle.execute(context) + result = await handle.execute(context) assert ModbusExceptions.IllegalValue == result.exception_code handle = MaskWriteRegisterRequest(0x0000, 0x0101, 0x1010) - result = handle.execute(context) + result = await handle.execute(context) assert ModbusExceptions.IllegalAddress == result.exception_code # -----------------------------------------------------------------------# diff --git a/test/test_factory.py b/test/test_factory.py index dc8311fd8..da27dd679 100644 --- a/test/test_factory.py +++ b/test/test_factory.py @@ -204,11 +204,11 @@ class NoCustomResponse: # pylint: disable=too-few-public-methods # since the high bit is set, it will simply echo the resulting message # ---------------------------------------------------------------------------# - def test_request_errors(self): + async def test_request_errors(self): """Test a request factory decoder exceptions.""" for func, msg in self.bad: result = self.server.decode(msg) assert result.ErrorCode == 1, "Failed to decode invalid requests" assert ( - result.execute(None).function_code == func - ), "Failed to create correct response message" + await result.execute(None) + ).function_code == func, "Failed to create correct response message" diff --git a/test/test_pdu.py b/test/test_pdu.py index 080af8a5a..44f44f874 100644 --- a/test/test_pdu.py +++ b/test/test_pdu.py @@ -31,10 +31,10 @@ def test_not_impelmented(self): with pytest.raises(NotImplementedException): request.decode(None) - def test_error_methods(self): + async def test_error_methods(self): """Test all error methods.""" self.illegal.decode("12345") - self.illegal.execute(None) + await self.illegal.execute(None) result = self.exception.encode() self.exception.decode(result) From 08ecd8c076dbd128f168ba23e6934380a46c57bf Mon Sep 17 00:00:00 2001 From: jan iversen Date: Tue, 16 Apr 2024 11:38:28 +0200 Subject: [PATCH 36/88] Bump actions CI. (#2166) --- .github/workflows/ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 781dabe78..3f4a888ef 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -43,7 +43,7 @@ jobs: uses: actions/checkout@v4.1.1 - name: Set up Python ${{ matrix.python }} - uses: actions/setup-python@v5.0.0 + uses: actions/setup-python@v5.1.0 with: python-version: ${{ matrix.python }} @@ -61,7 +61,7 @@ jobs: - name: Restore base Python virtual environment id: cache-venv - uses: actions/cache@v4.0.1 + uses: actions/cache@v4.0.2 with: path: ${{ env.VIRTUAL_ENV }} key: >- From 5678740a520a0298209c6d922d6c72a900a186bd Mon Sep 17 00:00:00 2001 From: Yohrog <69543220+Yohrog@users.noreply.github.com> Date: Wed, 17 Apr 2024 17:36:59 +0200 Subject: [PATCH 37/88] Fix usage of AsyncModbusTcpClient in client docs page (#2169) --- doc/source/client.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/doc/source/client.rst b/doc/source/client.rst index 1e6e89c21..826d14dcb 100644 --- a/doc/source/client.rst +++ b/doc/source/client.rst @@ -132,16 +132,16 @@ is done in a few simple steps, like the following synchronous example:: and a asynchronous example:: - from pymodbus.client import ModbusAsyncTcpClient + from pymodbus.client import AsyncModbusTcpClient - client = ModbusAsyncTcpClient('MyDevice.lan') # Create client object + client = AsyncModbusTcpClient('MyDevice.lan') # Create client object await client.connect() # connect to device, reconnect automatically await client.write_coil(1, True, slave=1) # set information in device result = await client.read_coils(2, 3, slave=1) # get information from device print(result.bits[0]) # use information client.close() # Disconnect device -The line :mod:`client = ModbusAsyncTcpClient('MyDevice.lan')` only creates the object it does not activate +The line :mod:`client = AsyncModbusTcpClient('MyDevice.lan')` only creates the object it does not activate anything. The line :mod:`await client.connect()` connects to the device (or comm port), if this cannot connect successfully within From 2eadaf67e572541fb9969896a8c3635d812235f4 Mon Sep 17 00:00:00 2001 From: jan iversen Date: Wed, 17 Apr 2024 19:30:52 +0200 Subject: [PATCH 38/88] Sphinx: do not turn warnings into errors. --- doc/build_html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/build_html b/doc/build_html index 0fa42b93d..2b73403e5 100755 --- a/doc/build_html +++ b/doc/build_html @@ -5,4 +5,4 @@ zip -9 doc/source/_static/examples.zip examples/* tar -czf doc/source/_static/examples.tgz examples popd -sphinx-build -M html "." "../build/html" -W --keep-going +sphinx-build -M html "." "../build/html" From 79bd86df5c6ea84783d8802768f1d5cdba7eb2d7 Mon Sep 17 00:00:00 2001 From: jan iversen Date: Fri, 19 Apr 2024 16:15:54 +0200 Subject: [PATCH 39/88] Add minimal devcontainer. (#2172) --- .devcontainer/devcontainer.json | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 .devcontainer/devcontainer.json diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 000000000..8c6f3fb3d --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,22 @@ +// For format details, see https://aka.ms/devcontainer.json. For config options, see the +// README at: https://github.com/devcontainers/templates/tree/main/src/python +{ + "name": "Python 3", + // Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile + "image": "mcr.microsoft.com/devcontainers/python:1-3.12-bullseye" + + // Features to add to the dev container. More info: https://containers.dev/features. + // "features": {}, + + // Use 'forwardPorts' to make a list of ports inside the container available locally. + // "forwardPorts": [], + + // Use 'postCreateCommand' to run commands after the container is created. + // "postCreateCommand": "pip3 install --user -r requirements.txt", + + // Configure tool-specific properties. + // "customizations": {}, + + // Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root. + // "remoteUser": "root" +} From a48aeeb48e503a1f6a424cddc9439cc211b15072 Mon Sep 17 00:00:00 2001 From: Martyy <48990925+martyy665@users.noreply.github.com> Date: Tue, 23 Apr 2024 08:12:52 +0200 Subject: [PATCH 40/88] Update transaction.py (#2174) --- pymodbus/transaction.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pymodbus/transaction.py b/pymodbus/transaction.py index 04dcce829..3e328bebe 100644 --- a/pymodbus/transaction.py +++ b/pymodbus/transaction.py @@ -451,7 +451,7 @@ def getTransaction(self, tid): """ Log.debug("Getting transaction {}", tid) - if not tid: + if tid is None: if self.transactions: return self.transactions.popitem()[1] return None From 39cc9ffc01d58a3b9b7b97e9b508ddfb0779271e Mon Sep 17 00:00:00 2001 From: jan iversen Date: Tue, 23 Apr 2024 08:37:59 +0200 Subject: [PATCH 41/88] Transaction id overrun. --- pymodbus/transaction.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/pymodbus/transaction.py b/pymodbus/transaction.py index 3e328bebe..13f84e31a 100644 --- a/pymodbus/transaction.py +++ b/pymodbus/transaction.py @@ -451,7 +451,7 @@ def getTransaction(self, tid): """ Log.debug("Getting transaction {}", tid) - if tid is None: + if not tid: if self.transactions: return self.transactions.popitem()[1] return None @@ -473,7 +473,10 @@ def getNextTID(self): :returns: The next unique transaction identifier """ - self.tid = (self.tid + 1) & 0xFFFF + if self.tid < 65000: + self.tid += 1 + else: + self.tid = 1 return self.tid def reset(self): From bf3b21e6b4aa2c24d68d4e3d6a0f5a5bef2c676c Mon Sep 17 00:00:00 2001 From: Ilkka Ollakka <14361597+ilkka-ollakka@users.noreply.github.com> Date: Tue, 23 Apr 2024 11:32:56 +0300 Subject: [PATCH 42/88] call async datastore from modbus server (#2144) --- pymodbus/pdu/bit_read_message.py | 8 ++++++-- pymodbus/pdu/bit_write_message.py | 15 ++++++--------- pymodbus/pdu/register_read_message.py | 14 ++++++++------ pymodbus/pdu/register_write_message.py | 22 +++++++++++----------- 4 files changed, 31 insertions(+), 28 deletions(-) diff --git a/pymodbus/pdu/bit_read_message.py b/pymodbus/pdu/bit_read_message.py index 7d6b4c6a5..9deb7009b 100644 --- a/pymodbus/pdu/bit_read_message.py +++ b/pymodbus/pdu/bit_read_message.py @@ -169,7 +169,9 @@ async def execute(self, context): return self.doException(merror.IllegalValue) if not context.validate(self.function_code, self.address, self.count): return self.doException(merror.IllegalAddress) - values = context.getValues(self.function_code, self.address, self.count) + values = await context.async_getValues( + self.function_code, self.address, self.count + ) if isinstance(values, ExceptionResponse): return values return ReadCoilsResponse(values) @@ -237,7 +239,9 @@ async def execute(self, context): return self.doException(merror.IllegalValue) if not context.validate(self.function_code, self.address, self.count): return self.doException(merror.IllegalAddress) - values = context.getValues(self.function_code, self.address, self.count) + values = await context.async_getValues( + self.function_code, self.address, self.count + ) if isinstance(values, ExceptionResponse): return values return ReadDiscreteInputsResponse(values) diff --git a/pymodbus/pdu/bit_write_message.py b/pymodbus/pdu/bit_write_message.py index f3ea83c2e..d9a9fb323 100644 --- a/pymodbus/pdu/bit_write_message.py +++ b/pymodbus/pdu/bit_write_message.py @@ -14,8 +14,8 @@ import struct from pymodbus.constants import ModbusStatus -from pymodbus.pdu import ExceptionResponse, ModbusRequest, ModbusResponse from pymodbus.pdu import ModbusExceptions as merror +from pymodbus.pdu import ModbusRequest, ModbusResponse from pymodbus.utilities import pack_bitstring, unpack_bitstring @@ -90,11 +90,8 @@ async def execute(self, context): if not context.validate(self.function_code, self.address, 1): return self.doException(merror.IllegalAddress) - context.setValues(self.function_code, self.address, [self.value]) - # result = context.setValues(self.function_code, self.address, [self.value]) - # if isinstance(result, ExceptionResponse): - # return result - values = context.getValues(self.function_code, self.address, 1) + await context.async_setValues(self.function_code, self.address, [self.value]) + values = await context.async_getValues(self.function_code, self.address, 1) # if isinstance(values, ExceptionResponse): # return values return WriteSingleCoilResponse(self.address, values[0]) @@ -227,9 +224,9 @@ async def execute(self, context): if not context.validate(self.function_code, self.address, count): return self.doException(merror.IllegalAddress) - result = context.setValues(self.function_code, self.address, self.values) - if isinstance(result, ExceptionResponse): - return result + await context.async_setValues( + self.function_code, self.address, self.values + ) return WriteMultipleCoilsResponse(self.address, count) def __str__(self): diff --git a/pymodbus/pdu/register_read_message.py b/pymodbus/pdu/register_read_message.py index 842e2a6b0..a6f1e3f75 100644 --- a/pymodbus/pdu/register_read_message.py +++ b/pymodbus/pdu/register_read_message.py @@ -149,7 +149,9 @@ async def execute(self, context): return self.doException(merror.IllegalValue) if not context.validate(self.function_code, self.address, self.count): return self.doException(merror.IllegalAddress) - values = context.getValues(self.function_code, self.address, self.count) + values = await context.async_getValues( + self.function_code, self.address, self.count + ) if isinstance(values, ExceptionResponse): return values @@ -210,7 +212,9 @@ async def execute(self, context): return self.doException(merror.IllegalValue) if not context.validate(self.function_code, self.address, self.count): return self.doException(merror.IllegalAddress) - values = context.getValues(self.function_code, self.address, self.count) + values = await context.async_getValues( + self.function_code, self.address, self.count + ) if isinstance(values, ExceptionResponse): return values return ReadInputRegistersResponse(values) @@ -328,12 +332,10 @@ async def execute(self, context): return self.doException(merror.IllegalAddress) if not context.validate(self.function_code, self.read_address, self.read_count): return self.doException(merror.IllegalAddress) - result = context.setValues( + await context.async_setValues( self.function_code, self.write_address, self.write_registers ) - if isinstance(result, ExceptionResponse): - return result - registers = context.getValues( + registers = await context.async_getValues( self.function_code, self.read_address, self.read_count ) return ReadWriteMultipleRegistersResponse(registers) diff --git a/pymodbus/pdu/register_write_message.py b/pymodbus/pdu/register_write_message.py index e0970bbac..eca9035a2 100644 --- a/pymodbus/pdu/register_write_message.py +++ b/pymodbus/pdu/register_write_message.py @@ -68,10 +68,10 @@ async def execute(self, context): if not context.validate(self.function_code, self.address, 1): return self.doException(merror.IllegalAddress) - result = context.setValues(self.function_code, self.address, [self.value]) - if isinstance(result, ExceptionResponse): - return result - values = context.getValues(self.function_code, self.address, 1) + await context.async_setValues( + self.function_code, self.address, [self.value] + ) + values = await context.async_getValues(self.function_code, self.address, 1) return WriteSingleRegisterResponse(self.address, values[0]) def get_response_pdu_size(self): @@ -213,9 +213,9 @@ async def execute(self, context): if not context.validate(self.function_code, self.address, self.count): return self.doException(merror.IllegalAddress) - result = context.setValues(self.function_code, self.address, self.values) - if isinstance(result, ExceptionResponse): - return result + await context.async_setValues( + self.function_code, self.address, self.values + ) return WriteMultipleRegistersResponse(self.address, self.count) def get_response_pdu_size(self): @@ -333,13 +333,13 @@ async def execute(self, context): return self.doException(merror.IllegalValue) if not context.validate(self.function_code, self.address, 1): return self.doException(merror.IllegalAddress) - values = context.getValues(self.function_code, self.address, 1)[0] + values = (await context.async_getValues(self.function_code, self.address, 1))[0] if isinstance(values, ExceptionResponse): return values values = (values & self.and_mask) | (self.or_mask & ~self.and_mask) - result = context.setValues(self.function_code, self.address, [values]) - if isinstance(result, ExceptionResponse): - return result + await context.async_setValues( + self.function_code, self.address, [values] + ) return MaskWriteRegisterResponse(self.address, self.and_mask, self.or_mask) From 559e516a96e263f4b87623a99143bd7cc3ab529b Mon Sep 17 00:00:00 2001 From: jan iversen Date: Tue, 23 Apr 2024 10:47:53 +0200 Subject: [PATCH 43/88] Datastore will not return ExceptionResponse. (#2175) --- pymodbus/pdu/bit_read_message.py | 6 +----- pymodbus/pdu/bit_write_message.py | 2 -- pymodbus/pdu/register_read_message.py | 13 ++++--------- pymodbus/pdu/register_write_message.py | 4 +--- 4 files changed, 6 insertions(+), 19 deletions(-) diff --git a/pymodbus/pdu/bit_read_message.py b/pymodbus/pdu/bit_read_message.py index 9deb7009b..6796eeffc 100644 --- a/pymodbus/pdu/bit_read_message.py +++ b/pymodbus/pdu/bit_read_message.py @@ -11,8 +11,8 @@ # pylint: disable=missing-type-doc import struct -from pymodbus.pdu import ExceptionResponse, ModbusRequest, ModbusResponse from pymodbus.pdu import ModbusExceptions as merror +from pymodbus.pdu import ModbusRequest, ModbusResponse from pymodbus.utilities import pack_bitstring, unpack_bitstring @@ -172,8 +172,6 @@ async def execute(self, context): values = await context.async_getValues( self.function_code, self.address, self.count ) - if isinstance(values, ExceptionResponse): - return values return ReadCoilsResponse(values) @@ -242,8 +240,6 @@ async def execute(self, context): values = await context.async_getValues( self.function_code, self.address, self.count ) - if isinstance(values, ExceptionResponse): - return values return ReadDiscreteInputsResponse(values) diff --git a/pymodbus/pdu/bit_write_message.py b/pymodbus/pdu/bit_write_message.py index d9a9fb323..6c4fafa27 100644 --- a/pymodbus/pdu/bit_write_message.py +++ b/pymodbus/pdu/bit_write_message.py @@ -92,8 +92,6 @@ async def execute(self, context): await context.async_setValues(self.function_code, self.address, [self.value]) values = await context.async_getValues(self.function_code, self.address, 1) - # if isinstance(values, ExceptionResponse): - # return values return WriteSingleCoilResponse(self.address, values[0]) def get_response_pdu_size(self): diff --git a/pymodbus/pdu/register_read_message.py b/pymodbus/pdu/register_read_message.py index a6f1e3f75..9088f501e 100644 --- a/pymodbus/pdu/register_read_message.py +++ b/pymodbus/pdu/register_read_message.py @@ -13,8 +13,8 @@ # pylint: disable=missing-type-doc import struct -from pymodbus.pdu import ExceptionResponse, ModbusRequest, ModbusResponse from pymodbus.pdu import ModbusExceptions as merror +from pymodbus.pdu import ModbusRequest, ModbusResponse class ReadRegistersRequestBase(ModbusRequest): @@ -143,7 +143,7 @@ async def execute(self, context): """Run a read holding request against a datastore. :param context: The datastore to request from - :returns: An initialized :py:class:`~pymodbus.register_read_message.ReadHoldingRegistersResponse`, or an :py:class:`~pymodbus.pdu.ExceptionResponse` if an error occurred + :returns: An initialized :py:class:`~pymodbus.register_read_message.ReadHoldingRegistersResponse` """ if not (1 <= self.count <= 0x7D): return self.doException(merror.IllegalValue) @@ -152,9 +152,6 @@ async def execute(self, context): values = await context.async_getValues( self.function_code, self.address, self.count ) - if isinstance(values, ExceptionResponse): - return values - return ReadHoldingRegistersResponse(values) @@ -206,7 +203,7 @@ async def execute(self, context): """Run a read input request against a datastore. :param context: The datastore to request from - :returns: An initialized :py:class:`~pymodbus.register_read_message.ReadInputRegistersResponse`, or an :py:class:`~pymodbus.pdu.ExceptionResponse` if an error occurred + :returns: An initialized :py:class:`~pymodbus.register_read_message.ReadInputRegistersResponse` """ if not (1 <= self.count <= 0x7D): return self.doException(merror.IllegalValue) @@ -215,8 +212,6 @@ async def execute(self, context): values = await context.async_getValues( self.function_code, self.address, self.count ) - if isinstance(values, ExceptionResponse): - return values return ReadInputRegistersResponse(values) @@ -318,7 +313,7 @@ async def execute(self, context): """Run a write single register request against a datastore. :param context: The datastore to request from - :returns: An initialized :py:class:`~pymodbus.register_read_message.ReadWriteMultipleRegistersResponse`, or an :py:class:`~pymodbus.pdu.ExceptionResponse` if an error occurred + :returns: An initialized :py:class:`~pymodbus.register_read_message.ReadWriteMultipleRegistersResponse` """ if not (1 <= self.read_count <= 0x07D): return self.doException(merror.IllegalValue) diff --git a/pymodbus/pdu/register_write_message.py b/pymodbus/pdu/register_write_message.py index eca9035a2..753c2ce62 100644 --- a/pymodbus/pdu/register_write_message.py +++ b/pymodbus/pdu/register_write_message.py @@ -12,8 +12,8 @@ # pylint: disable=missing-type-doc import struct -from pymodbus.pdu import ExceptionResponse, ModbusRequest, ModbusResponse from pymodbus.pdu import ModbusExceptions as merror +from pymodbus.pdu import ModbusRequest, ModbusResponse class WriteSingleRegisterRequest(ModbusRequest): @@ -334,8 +334,6 @@ async def execute(self, context): if not context.validate(self.function_code, self.address, 1): return self.doException(merror.IllegalAddress) values = (await context.async_getValues(self.function_code, self.address, 1))[0] - if isinstance(values, ExceptionResponse): - return values values = (values & self.and_mask) | (self.or_mask & ~self.and_mask) await context.async_setValues( self.function_code, self.address, [values] From c91b2182f4b39d50a142ebe6372c227e29d588ca Mon Sep 17 00:00:00 2001 From: jan iversen Date: Sun, 5 May 2024 01:01:51 +0200 Subject: [PATCH 44/88] Sync TLS needs time before reading frame (#2186) --- pymodbus/framer/old_framer_tls.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/pymodbus/framer/old_framer_tls.py b/pymodbus/framer/old_framer_tls.py index 9ebb44227..a6a4df892 100644 --- a/pymodbus/framer/old_framer_tls.py +++ b/pymodbus/framer/old_framer_tls.py @@ -1,5 +1,6 @@ """TLS framer.""" import struct +from time import sleep from pymodbus.exceptions import ( ModbusIOException, @@ -42,6 +43,11 @@ def decode_data(self, data): return {"fcode": fcode} return {} + def recvPacket(self, size): + """Receive packet from the bus.""" + sleep(0.5) + return super().recvPacket(size) + def frameProcessIncomingPacket(self, _single, callback, _slave, _tid=None, **kwargs): """Process new packet pattern.""" # no slave id for Modbus Security Application Protocol From 100538f17b697e0ab844e9eadb810b0fe574ace1 Mon Sep 17 00:00:00 2001 From: James Cameron <90580716+jcameron-sso@users.noreply.github.com> Date: Wed, 8 May 2024 16:01:51 +1000 Subject: [PATCH 45/88] Describe zero_mode in ModbusSlaveContext.__init__ (#2187) --- pymodbus/datastore/context.py | 26 +++++++++++++++++++------- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/pymodbus/datastore/context.py b/pymodbus/datastore/context.py index d7915c0bc..f2d8c948b 100644 --- a/pymodbus/datastore/context.py +++ b/pymodbus/datastore/context.py @@ -73,17 +73,29 @@ def setValues(self, fc_as_hex: int, address: int, values: list[int | bool]) -> N # Slave Contexts # ---------------------------------------------------------------------------# class ModbusSlaveContext(ModbusBaseSlaveContext): - """This creates a modbus data model with each data access stored in a block.""" + """ + Create a modbus data model with each data access stored in a + block. + + :param di: discrete inputs initializer ModbusDataBlock + :param co: coils initializer ModbusDataBlock + :param hr: holding register initializer ModbusDataBlock + :param ir: input registers initializer ModbusDataBlock + :param zero_mode: Not add one to address + + When True, a request for address zero to n will map to + datastore address zero to n. + + When False, a request for address zero to n will map to + datastore address one to n+1, based on section 4.4 of + specification. + + Default is False. + """ def __init__(self, *_args, **kwargs): """Initialize the datastores. - :param kwargs: Each element is a ModbusDataBlock - - "di" - Discrete Inputs initializer - "co" - Coils initializer - "hr" - Holding Register initializer - "ir" - Input Registers iniatializer """ self.store = {} self.store["d"] = kwargs.get("di", ModbusSequentialDataBlock.create()) From bdd679d4250db0b2fafce9d665c6b4043cdc6abf Mon Sep 17 00:00:00 2001 From: jan iversen Date: Wed, 8 May 2024 08:39:12 +0200 Subject: [PATCH 46/88] Solve pylint error. --- pymodbus/datastore/context.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/pymodbus/datastore/context.py b/pymodbus/datastore/context.py index f2d8c948b..d2296a270 100644 --- a/pymodbus/datastore/context.py +++ b/pymodbus/datastore/context.py @@ -73,9 +73,7 @@ def setValues(self, fc_as_hex: int, address: int, values: list[int | bool]) -> N # Slave Contexts # ---------------------------------------------------------------------------# class ModbusSlaveContext(ModbusBaseSlaveContext): - """ - Create a modbus data model with each data access stored in a - block. + """Create a modbus data model with data stored in a block. :param di: discrete inputs initializer ModbusDataBlock :param co: coils initializer ModbusDataBlock @@ -91,12 +89,11 @@ class ModbusSlaveContext(ModbusBaseSlaveContext): specification. Default is False. + """ def __init__(self, *_args, **kwargs): - """Initialize the datastores. - - """ + """Initialize the datastores.""" self.store = {} self.store["d"] = kwargs.get("di", ModbusSequentialDataBlock.create()) self.store["c"] = kwargs.get("co", ModbusSequentialDataBlock.create()) From f4fb933e3f37aee8dc95a709564aad8b0b39c640 Mon Sep 17 00:00:00 2001 From: jan iversen Date: Wed, 8 May 2024 12:16:29 +0200 Subject: [PATCH 47/88] Show error if example is run without support files. (#2189) --- examples/client_async.py | 10 +++++++++- examples/client_async_calls.py | 9 ++++++++- examples/client_calls.py | 11 ++++++++++- examples/client_payload.py | 10 +++++++++- examples/client_sync.py | 10 +++++++++- examples/modbus_forwarder.py | 10 +++++++++- examples/server_async.py | 10 +++++++++- examples/server_callback.py | 10 +++++++++- examples/server_payload.py | 11 ++++++++++- examples/server_sync.py | 12 ++++++++++-- examples/server_updating.py | 10 +++++++++- 11 files changed, 101 insertions(+), 12 deletions(-) diff --git a/examples/client_async.py b/examples/client_async.py index 6c4fa5804..cc6bfae0f 100755 --- a/examples/client_async.py +++ b/examples/client_async.py @@ -28,8 +28,16 @@ """ import asyncio import logging +import sys -import helper + +try: + import helper +except ImportError: + print("*** ERROR --> THIS EXAMPLE needs the example directory, please see \n\ + https://pymodbus.readthedocs.io/en/latest/source/examples.html\n\ + for more information.") + sys.exit(-1) import pymodbus.client as modbusClient from pymodbus import ModbusException diff --git a/examples/client_async_calls.py b/examples/client_async_calls.py index 2f84c57b0..482a9ec40 100755 --- a/examples/client_async_calls.py +++ b/examples/client_async_calls.py @@ -31,9 +31,16 @@ """ import asyncio import logging +import sys -import client_async +try: + import client_async +except ImportError: + print("*** ERROR --> THIS EXAMPLE needs the example directory, please see \n\ + https://pymodbus.readthedocs.io/en/latest/source/examples.html\n\ + for more information.") + sys.exit(-1) _logger = logging.getLogger(__file__) _logger.setLevel("DEBUG") diff --git a/examples/client_calls.py b/examples/client_calls.py index ab9d740d4..50a58bc3d 100755 --- a/examples/client_calls.py +++ b/examples/client_calls.py @@ -30,8 +30,17 @@ ./server_async.py """ import logging +import sys + + +try: + import client_sync +except ImportError: + print("*** ERROR --> THIS EXAMPLE needs the example directory, please see \n\ + https://pymodbus.readthedocs.io/en/latest/source/examples.html\n\ + for more information.") + sys.exit(-1) -import client_sync _logger = logging.getLogger(__file__) diff --git a/examples/client_payload.py b/examples/client_payload.py index c5d82b082..70eb53ac6 100755 --- a/examples/client_payload.py +++ b/examples/client_payload.py @@ -7,9 +7,17 @@ Works out of the box together with payload_server.py """ import asyncio +import sys from collections import OrderedDict -import client_async + +try: + import client_async +except ImportError: + print("*** ERROR --> THIS EXAMPLE needs the example directory, please see \n\ + https://pymodbus.readthedocs.io/en/latest/source/examples.html\n\ + for more information.") + sys.exit(-1) from pymodbus.constants import Endian from pymodbus.payload import BinaryPayloadBuilder, BinaryPayloadDecoder diff --git a/examples/client_sync.py b/examples/client_sync.py index c236988ac..55f03935b 100755 --- a/examples/client_sync.py +++ b/examples/client_sync.py @@ -31,8 +31,16 @@ """ import logging +import sys -import helper + +try: + import helper +except ImportError: + print("*** ERROR --> THIS EXAMPLE needs the example directory, please see \n\ + https://pymodbus.readthedocs.io/en/latest/source/examples.html\n\ + for more information.") + sys.exit(-1) import pymodbus.client as modbusClient from pymodbus import ModbusException diff --git a/examples/modbus_forwarder.py b/examples/modbus_forwarder.py index 9ada899de..b29070f25 100755 --- a/examples/modbus_forwarder.py +++ b/examples/modbus_forwarder.py @@ -18,8 +18,16 @@ """ import asyncio import logging +import sys -import helper + +try: + import helper +except ImportError: + print("*** ERROR --> THIS EXAMPLE needs the example directory, please see \n\ + https://pymodbus.readthedocs.io/en/latest/source/examples.html\n\ + for more information.") + sys.exit(-1) from pymodbus.client import ModbusTcpClient from pymodbus.datastore import ModbusServerContext diff --git a/examples/server_async.py b/examples/server_async.py index 8cb2ad787..676fa8ba5 100755 --- a/examples/server_async.py +++ b/examples/server_async.py @@ -34,8 +34,16 @@ """ import asyncio import logging +import sys -import helper + +try: + import helper +except ImportError: + print("*** ERROR --> THIS EXAMPLE needs the example directory, please see \n\ + https://pymodbus.readthedocs.io/en/latest/source/examples.html\n\ + for more information.") + sys.exit(-1) from pymodbus import __version__ as pymodbus_version from pymodbus.datastore import ( diff --git a/examples/server_callback.py b/examples/server_callback.py index f5cd574bd..6be7da107 100755 --- a/examples/server_callback.py +++ b/examples/server_callback.py @@ -6,8 +6,16 @@ """ import asyncio import logging +import sys -import server_async + +try: + import server_async +except ImportError: + print("*** ERROR --> THIS EXAMPLE needs the example directory, please see \n\ + https://pymodbus.readthedocs.io/en/latest/source/examples.html\n\ + for more information.") + sys.exit(-1) from pymodbus.datastore import ( ModbusSequentialDataBlock, diff --git a/examples/server_payload.py b/examples/server_payload.py index c56d5a035..abd0b444e 100755 --- a/examples/server_payload.py +++ b/examples/server_payload.py @@ -6,8 +6,17 @@ """ import asyncio import logging +import sys + + +try: + import server_async +except ImportError: + print("*** ERROR --> THIS EXAMPLE needs the example directory, please see \n\ + https://pymodbus.readthedocs.io/en/latest/source/examples.html\n\ + for more information.") + sys.exit(-1) -import server_async from pymodbus.constants import Endian from pymodbus.datastore import ( diff --git a/examples/server_sync.py b/examples/server_sync.py index 52192b4a0..0b6d3bf6d 100755 --- a/examples/server_sync.py +++ b/examples/server_sync.py @@ -35,9 +35,17 @@ a lot slower. """ import logging +import sys -import helper -import server_async + +try: + import helper + import server_async +except ImportError: + print("*** ERROR --> THIS EXAMPLE needs the example directory, please see \n\ + https://pymodbus.readthedocs.io/en/latest/source/examples.html\n\ + for more information.") + sys.exit(-1) # --------------------------------------------------------------------------- # # import the various client implementations diff --git a/examples/server_updating.py b/examples/server_updating.py index dae9e7f61..5ece64f23 100755 --- a/examples/server_updating.py +++ b/examples/server_updating.py @@ -33,8 +33,16 @@ """ import asyncio import logging +import sys -import server_async + +try: + import server_async +except ImportError: + print("*** ERROR --> THIS EXAMPLE needs the example directory, please see \n\ + https://pymodbus.readthedocs.io/en/latest/source/examples.html\n\ + for more information.") + sys.exit(-1) from pymodbus.datastore import ( ModbusSequentialDataBlock, From f92440c091be38bea4d00d3ba858cc9a03e9a91d Mon Sep 17 00:00:00 2001 From: James Cameron <90580716+jcameron-sso@users.noreply.github.com> Date: Mon, 13 May 2024 15:45:04 +1000 Subject: [PATCH 48/88] Fix usage file names (#2194) --- examples/simple_async_client.py | 2 +- examples/simple_sync_client.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/simple_async_client.py b/examples/simple_async_client.py index 5030bfc5a..500112799 100755 --- a/examples/simple_async_client.py +++ b/examples/simple_async_client.py @@ -3,7 +3,7 @@ An example of a single threaded synchronous client. -usage: simple_client_async.py +usage: simple_async_client.py All options must be adapted in the code The corresponding server must be started before e.g. as: diff --git a/examples/simple_sync_client.py b/examples/simple_sync_client.py index 80a8b9e77..2cd363d8b 100755 --- a/examples/simple_sync_client.py +++ b/examples/simple_sync_client.py @@ -3,7 +3,7 @@ An example of a single threaded synchronous client. -usage: simple_client_async.py +usage: simple_sync_client.py All options must be adapted in the code The corresponding server must be started before e.g. as: From a0e3044103d881b59feb93ba1d6191486371154d Mon Sep 17 00:00:00 2001 From: Qi Li <160646648+qili-eaton@users.noreply.github.com> Date: Mon, 20 May 2024 13:08:53 -0600 Subject: [PATCH 49/88] Update client.rst (#2199) --- doc/source/client.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/source/client.rst b/doc/source/client.rst index 826d14dcb..9b3a6f9c4 100644 --- a/doc/source/client.rst +++ b/doc/source/client.rst @@ -15,7 +15,7 @@ It is allowed to have multiple client objects that e.g. each communicate with a Client performance ------------------ -There are currently a big performance gab between the 2 clients +There are currently a big performance gap between the 2 clients (try it on your computer :github:`examples/client_performance.py`). This is due to a rather old implementation of the synchronous client, we are currently working to update the client code. Our aim is to achieve a similar data rate with both clients and at least double the data rate while keeping the stability. From 45c5116ec25b11c4b2c46fa8b786c69543eaddec Mon Sep 17 00:00:00 2001 From: jan iversen Date: Thu, 30 May 2024 19:39:15 +0200 Subject: [PATCH 50/88] Transaction_id for serial == 0. (#2208) --- pymodbus/framer/old_framer_rtu.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/pymodbus/framer/old_framer_rtu.py b/pymodbus/framer/old_framer_rtu.py index 997448af4..a57fd75da 100644 --- a/pymodbus/framer/old_framer_rtu.py +++ b/pymodbus/framer/old_framer_rtu.py @@ -158,7 +158,7 @@ def check_frame(self): if (result := self.decoder.decode(data)) is None: raise ModbusIOException("Unable to decode request") result.slave_id = self._header["uid"] - result.transaction_id = self._header["tid"] + result.transaction_id = 0 self._buffer = self._buffer[self._header["len"] :] Log.debug("Frame advanced, resetting header!!") callback(result) # defer or push to a thread? @@ -171,8 +171,7 @@ def buildPacket(self, message): packet = super().buildPacket(message) # Ensure that transaction is actually the slave id for serial comms - if message.slave_id: - message.transaction_id = message.slave_id + message.transaction_id = 0 return packet def sendPacket(self, message): From d940e929a958f818f7400ec2cf67e8bc14bb94f6 Mon Sep 17 00:00:00 2001 From: jan iversen Date: Thu, 30 May 2024 23:02:03 +0200 Subject: [PATCH 51/88] Remember to remove serial writer. (#2209) --- pymodbus/transport/serialtransport.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pymodbus/transport/serialtransport.py b/pymodbus/transport/serialtransport.py index c783187ad..c86494c97 100644 --- a/pymodbus/transport/serialtransport.py +++ b/pymodbus/transport/serialtransport.py @@ -46,6 +46,7 @@ def close(self, exc: Exception | None = None) -> None: self.poll_task = None else: self.async_loop.remove_reader(self.sync_serial.fileno()) + self.async_loop.remove_writer(self.sync_serial.fileno()) self.sync_serial.close() self.sync_serial = None # type: ignore[assignment] if exc: From 45a998b03080dd0c64909747e7a2cec6fbae2e3e Mon Sep 17 00:00:00 2001 From: jan iversen Date: Thu, 30 May 2024 23:18:15 +0200 Subject: [PATCH 52/88] Delay callback_disconnected() until reconnect_delay_max. (#2210) --- pymodbus/transport/transport.py | 9 +++++---- test/transport/test_reconnect.py | 4 ++-- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/pymodbus/transport/transport.py b/pymodbus/transport/transport.py index 265d98e1d..e6d32cb9e 100644 --- a/pymodbus/transport/transport.py +++ b/pymodbus/transport/transport.py @@ -473,10 +473,11 @@ async def do_reconnect(self) -> None: await asyncio.sleep(self.reconnect_delay_current) if await self.connect(): break - self.reconnect_delay_current = min( - 2 * self.reconnect_delay_current, - self.comm_params.reconnect_delay_max, - ) + self.reconnect_delay_current = 2 * self.reconnect_delay_current + if self.reconnect_delay_current > self.comm_params.reconnect_delay_max: + self.callback_disconnected(Exception("Retry connect timeout.")) + self.reconnect_delay_current = self.comm_params.reconnect_delay_max + break except asyncio.CancelledError: pass self.reconnect_task = None diff --git a/test/transport/test_reconnect.py b/test/transport/test_reconnect.py index 31d9960c6..e97a62119 100644 --- a/test/transport/test_reconnect.py +++ b/test/transport/test_reconnect.py @@ -52,9 +52,9 @@ async def test_multi_reconnect_call(self, client): assert client.reconnect_delay_current == client.comm_params.reconnect_delay * 2 await asyncio.sleep(client.reconnect_delay_current * 1.8) assert client.call_create.call_count == 3 - assert client.reconnect_delay_current == client.comm_params.reconnect_delay_max + assert client.reconnect_delay_current >= client.comm_params.reconnect_delay_max await asyncio.sleep(client.reconnect_delay_current * 1.8) - assert client.call_create.call_count >= 4 + assert client.call_create.call_count >= 3 assert client.reconnect_delay_current == client.comm_params.reconnect_delay_max client.close() From 450b96f77262aab93c0ca8e5f69e125cd7859968 Mon Sep 17 00:00:00 2001 From: jan iversen Date: Fri, 31 May 2024 10:57:26 +0200 Subject: [PATCH 53/88] Revert "Delay callback_disconnected() until reconnect_delay_max. (#2210)" This reverts commit 45a998b03080dd0c64909747e7a2cec6fbae2e3e. --- pymodbus/transport/transport.py | 9 ++++----- test/transport/test_reconnect.py | 4 ++-- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/pymodbus/transport/transport.py b/pymodbus/transport/transport.py index e6d32cb9e..265d98e1d 100644 --- a/pymodbus/transport/transport.py +++ b/pymodbus/transport/transport.py @@ -473,11 +473,10 @@ async def do_reconnect(self) -> None: await asyncio.sleep(self.reconnect_delay_current) if await self.connect(): break - self.reconnect_delay_current = 2 * self.reconnect_delay_current - if self.reconnect_delay_current > self.comm_params.reconnect_delay_max: - self.callback_disconnected(Exception("Retry connect timeout.")) - self.reconnect_delay_current = self.comm_params.reconnect_delay_max - break + self.reconnect_delay_current = min( + 2 * self.reconnect_delay_current, + self.comm_params.reconnect_delay_max, + ) except asyncio.CancelledError: pass self.reconnect_task = None diff --git a/test/transport/test_reconnect.py b/test/transport/test_reconnect.py index e97a62119..31d9960c6 100644 --- a/test/transport/test_reconnect.py +++ b/test/transport/test_reconnect.py @@ -52,9 +52,9 @@ async def test_multi_reconnect_call(self, client): assert client.reconnect_delay_current == client.comm_params.reconnect_delay * 2 await asyncio.sleep(client.reconnect_delay_current * 1.8) assert client.call_create.call_count == 3 - assert client.reconnect_delay_current >= client.comm_params.reconnect_delay_max + assert client.reconnect_delay_current == client.comm_params.reconnect_delay_max await asyncio.sleep(client.reconnect_delay_current * 1.8) - assert client.call_create.call_count >= 3 + assert client.call_create.call_count >= 4 assert client.reconnect_delay_current == client.comm_params.reconnect_delay_max client.close() From df0c09e901feaf538fa1ec87fec36625807e2842 Mon Sep 17 00:00:00 2001 From: andrew-harness <75149647+andrew-harness@users.noreply.github.com> Date: Fri, 31 May 2024 13:34:48 -0400 Subject: [PATCH 54/88] Fix writing to serial (rs485) on windows os. (#2191) Co-authored-by: jan iversen --- pymodbus/transport/serialtransport.py | 6 +++--- test/transport/test_comm.py | 1 - test/transport/test_serial.py | 1 + 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/pymodbus/transport/serialtransport.py b/pymodbus/transport/serialtransport.py index c86494c97..6d632fcae 100644 --- a/pymodbus/transport/serialtransport.py +++ b/pymodbus/transport/serialtransport.py @@ -13,7 +13,7 @@ class SerialTransport(asyncio.Transport): """An asyncio serial transport.""" - force_poll: bool = False + force_poll: bool = os.name == "nt" def __init__(self, loop, protocol, *args, **kwargs) -> None: """Initialize.""" @@ -29,7 +29,7 @@ def __init__(self, loop, protocol, *args, **kwargs) -> None: def setup(self) -> None: """Prepare to read/write.""" - if os.name == "nt" or self.force_poll: + if self.force_poll: self.poll_task = asyncio.create_task(self.polling_task()) self.poll_task.set_name("SerialTransport poll") else: @@ -56,7 +56,7 @@ def close(self, exc: Exception | None = None) -> None: def write(self, data) -> None: """Write some data to the transport.""" self.intern_write_buffer.append(data) - if not self.poll_task: + if not self.force_poll: self.async_loop.add_writer(self.sync_serial.fileno(), self.intern_write_ready) def flush(self) -> None: diff --git a/test/transport/test_comm.py b/test/transport/test_comm.py index 4ddf7b742..8b175bc09 100644 --- a/test/transport/test_comm.py +++ b/test/transport/test_comm.py @@ -179,7 +179,6 @@ async def test_serial_poll(self, client, server, use_port): SerialTransport.force_poll = True assert await client.connect() await asyncio.sleep(0.5) - SerialTransport.force_poll = False assert len(server.active_connections) == 1 server_connected = list(server.active_connections.values())[0] test_data = b"abcd" * 1000 diff --git a/test/transport/test_serial.py b/test/transport/test_serial.py index 62f54e8b4..c39e11e4c 100644 --- a/test/transport/test_serial.py +++ b/test/transport/test_serial.py @@ -100,6 +100,7 @@ async def test_write_force_poll(self): ) await asyncio.sleep(0) transport.write(b"abcd") + await asyncio.sleep(0.5) transport.close() SerialTransport.force_poll = False From 5bddf9e8830cf339165ea31f5c16443c1b9b75f0 Mon Sep 17 00:00:00 2001 From: jan iversen Date: Tue, 11 Jun 2024 14:42:00 +0200 Subject: [PATCH 55/88] Update third party versions. (#2216) --- examples/client_async.py | 1 + examples/client_sync.py | 1 + examples/server_async.py | 2 ++ examples/server_sync.py | 1 + pyproject.toml | 28 +++++++++---------- test/sub_client/test_client_sync.py | 2 +- .../test_bit_read_messages.py | 2 +- .../test_bit_write_messages.py | 3 +- .../test_register_read_messages.py | 3 +- .../test_register_write_messages.py | 3 +- test/test_file_message.py | 4 +-- 11 files changed, 26 insertions(+), 24 deletions(-) diff --git a/examples/client_async.py b/examples/client_async.py index cc6bfae0f..06da1c40e 100755 --- a/examples/client_async.py +++ b/examples/client_async.py @@ -53,6 +53,7 @@ def setup_async_client(description=None, cmdline=None): server=False, description=description, cmdline=cmdline ) _logger.info("### Create client object") + client = None if args.comm == "tcp": client = modbusClient.AsyncModbusTcpClient( args.host, diff --git a/examples/client_sync.py b/examples/client_sync.py index 55f03935b..58bf19966 100755 --- a/examples/client_sync.py +++ b/examples/client_sync.py @@ -58,6 +58,7 @@ def setup_sync_client(description=None, cmdline=None): cmdline=cmdline, ) _logger.info("### Create client object") + client = None if args.comm == "tcp": client = modbusClient.ModbusTcpClient( args.host, diff --git a/examples/server_async.py b/examples/server_async.py index 676fa8ba5..a4f71cb71 100755 --- a/examples/server_async.py +++ b/examples/server_async.py @@ -70,6 +70,7 @@ def setup_server(description=None, context=None, cmdline=None): args = helper.get_commandline(server=True, description=description, cmdline=cmdline) if context: args.context = context + datablock = None if not args.context: _logger.info("### Create datastore") # The datastores only respond to the addresses that are initialized @@ -153,6 +154,7 @@ async def run_async_server(args): """Run server.""" txt = f"### start ASYNC server, listening on {args.port} - {args.comm}" _logger.info(txt) + server = None if args.comm == "tcp": address = (args.host if args.host else "", args.port if args.port else None) server = await StartAsyncTcpServer( diff --git a/examples/server_sync.py b/examples/server_sync.py index 0b6d3bf6d..d60fd9d05 100755 --- a/examples/server_sync.py +++ b/examples/server_sync.py @@ -66,6 +66,7 @@ def run_sync_server(args): """Run server.""" txt = f"### start SYNC server, listening on {args.port} - {args.comm}" _logger.info(txt) + server = None if args.comm == "tcp": address = ("", args.port) if args.port else None server = StartTcpServer( diff --git a/pyproject.toml b/pyproject.toml index 1d0200f47..bbc81d515 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -52,27 +52,27 @@ repl = [ simulator = [ "aiohttp>=3.8.6;python_version<'3.12'", - "aiohttp>=3.9.0b0;python_version=='3.12'" + "aiohttp>=3.9.5;python_version=='3.12'" ] documentation = [ "recommonmark>=0.7.1", - "Sphinx>=5.3.0", - "sphinx-rtd-theme>=1.1.1" + "Sphinx>=7.3.7", + "sphinx-rtd-theme>=2.0.0" ] development = [ - "build>=1.1.1", - "codespell>=2.2.6", - "coverage>=7.4.3", - "mypy>=1.9.0", - "pylint>=3.1.0", - "pytest>=8.1.0", - "pytest-asyncio>=0.23.5.post1", - "pytest-cov>=4.1.0", + "build>=1.2.1", + "codespell>=2.3.0", + "coverage>=7.5.3", + "mypy>=1.10.0", + "pylint>=3.2.3", + "pytest>=8.2.2", + "pytest-asyncio>=0.23.7", + "pytest-cov>=5.0.0", "pytest-profiling>=1.7.0", "pytest-timeout>=2.3.1", - "pytest-xdist>=3.5.0", - "ruff>=0.3.3", - "twine>=5.0.0", + "pytest-xdist>=3.6.1", + "ruff>=0.4.8", + "twine>=5.1.0", "types-Pygments", "types-pyserial" ] diff --git a/test/sub_client/test_client_sync.py b/test/sub_client/test_client_sync.py index 4be8bdba2..08635de89 100755 --- a/test/sub_client/test_client_sync.py +++ b/test/sub_client/test_client_sync.py @@ -1,7 +1,6 @@ """Test client sync.""" import socket from itertools import count -from test.conftest import mockSocket from unittest import mock import pytest @@ -21,6 +20,7 @@ ModbusSocketFramer, ModbusTlsFramer, ) +from test.conftest import mockSocket # ---------------------------------------------------------------------------# diff --git a/test/sub_function_codes/test_bit_read_messages.py b/test/sub_function_codes/test_bit_read_messages.py index e2c5a0b8a..c0c7a5890 100644 --- a/test/sub_function_codes/test_bit_read_messages.py +++ b/test/sub_function_codes/test_bit_read_messages.py @@ -7,7 +7,6 @@ * Read Coils """ import struct -from test.conftest import MockContext from pymodbus.pdu import ModbusExceptions from pymodbus.pdu.bit_read_message import ( @@ -16,6 +15,7 @@ ReadCoilsRequest, ReadDiscreteInputsRequest, ) +from test.conftest import MockContext res = [True] * 21 diff --git a/test/sub_function_codes/test_bit_write_messages.py b/test/sub_function_codes/test_bit_write_messages.py index 8adf01352..54cae8b41 100644 --- a/test/sub_function_codes/test_bit_write_messages.py +++ b/test/sub_function_codes/test_bit_write_messages.py @@ -6,8 +6,6 @@ * Read/Write Discretes * Read Coils """ -from test.conftest import FakeList, MockContext - from pymodbus.pdu import ModbusExceptions from pymodbus.pdu.bit_write_message import ( WriteMultipleCoilsRequest, @@ -15,6 +13,7 @@ WriteSingleCoilRequest, WriteSingleCoilResponse, ) +from test.conftest import FakeList, MockContext # ---------------------------------------------------------------------------# diff --git a/test/sub_function_codes/test_register_read_messages.py b/test/sub_function_codes/test_register_read_messages.py index 416ceb5b7..9b8bafef8 100644 --- a/test/sub_function_codes/test_register_read_messages.py +++ b/test/sub_function_codes/test_register_read_messages.py @@ -1,6 +1,4 @@ """Test register read messages.""" -from test.conftest import FakeList, MockContext - from pymodbus.pdu import ModbusExceptions from pymodbus.pdu.register_read_message import ( ReadHoldingRegistersRequest, @@ -12,6 +10,7 @@ ReadWriteMultipleRegistersRequest, ReadWriteMultipleRegistersResponse, ) +from test.conftest import FakeList, MockContext TEST_MESSAGE = b"\x06\x00\x0a\x00\x0b\x00\x0c" diff --git a/test/sub_function_codes/test_register_write_messages.py b/test/sub_function_codes/test_register_write_messages.py index 7697d56de..1ec6f4202 100644 --- a/test/sub_function_codes/test_register_write_messages.py +++ b/test/sub_function_codes/test_register_write_messages.py @@ -1,6 +1,4 @@ """Test register write messages.""" -from test.conftest import MockContext, MockLastValuesContext - from pymodbus.payload import BinaryPayloadBuilder, Endian from pymodbus.pdu import ModbusExceptions from pymodbus.pdu.register_write_message import ( @@ -11,6 +9,7 @@ WriteSingleRegisterRequest, WriteSingleRegisterResponse, ) +from test.conftest import MockContext, MockLastValuesContext # ---------------------------------------------------------------------------# diff --git a/test/test_file_message.py b/test/test_file_message.py index e210f6bcb..6882c6b89 100644 --- a/test/test_file_message.py +++ b/test/test_file_message.py @@ -6,8 +6,6 @@ * Read/Write Discretes * Read Coils """ -from test.conftest import MockContext - from pymodbus.pdu import ModbusExceptions from pymodbus.pdu.file_message import ( FileRecord, @@ -19,6 +17,8 @@ WriteFileRecordResponse, ) +from .conftest import MockContext + TEST_MESSAGE = b"\x00\n\x00\x08\x00\x01\x00\x02\x00\x03\x00\x04" From 98d1d4a655baa605d6ad557ab014a7ae1db0b7c0 Mon Sep 17 00:00:00 2001 From: jan iversen Date: Tue, 11 Jun 2024 20:29:44 +0200 Subject: [PATCH 56/88] test convert registers with 1234.... (#2217) --- test/sub_client/test_client.py | 38 ++++++++++++++++++++++++++++++++-- 1 file changed, 36 insertions(+), 2 deletions(-) diff --git a/test/sub_client/test_client.py b/test/sub_client/test_client.py index 360d22fb9..04ea7828f 100755 --- a/test/sub_client/test_client.py +++ b/test/sub_client/test_client.py @@ -592,12 +592,46 @@ def test_client_tls_connect2(): ) def test_client_mixin_convert(datatype, registers, value): """Test converter methods.""" - regs = ModbusClientMixin.convert_to_registers(value, datatype) - result = ModbusClientMixin.convert_from_registers(regs, datatype) + result = ModbusClientMixin.convert_from_registers(registers, datatype) if datatype == ModbusClientMixin.DATATYPE.FLOAT32: result = round(result, 6) + assert result == value + regs = ModbusClientMixin.convert_to_registers(value, datatype) assert regs == registers + + +@pytest.mark.parametrize( + ("datatype", "value", "registers"), + [ + (ModbusClientMixin.DATATYPE.STRING, "0123", [b'\x30\x31', b'\x32\x33']), + (ModbusClientMixin.DATATYPE.UINT16, 258, [b'\x01\x02']), + (ModbusClientMixin.DATATYPE.INT16, -32510, [b'\x81\x02']), + (ModbusClientMixin.DATATYPE.UINT32, 16909060, [b'\x01\x02', b'\x03\x04']), + (ModbusClientMixin.DATATYPE.INT32, -2130574588, [b'\x81\x02', b'\x03\x04']), + ( + ModbusClientMixin.DATATYPE.UINT64, + 72623859790382856, + [b'\x01\x02', b'\x03\x04', b'\x05\x06', b'\x07\x08'], + ), + ( + ModbusClientMixin.DATATYPE.INT64, + -9150748177064392952, + [b'\x81\x02', b'\x03\x04', b'\x05\x06', b'\x07\x08'], + ), + (ModbusClientMixin.DATATYPE.FLOAT32, 8.125736, [b'\x41\x02', b'\x03\x04']), + (ModbusClientMixin.DATATYPE.FLOAT64, 147552.502453, [b'\x41\x02', b'\x03\x04', b'\x05\x06', b'\x14\x16']), + ], +) +def test_client_mixin_convert_1234(datatype, registers, value): + """Test converter methods.""" + for i in range(0, len(registers)): + registers[i] = int.from_bytes(registers[i], "big") + regs = ModbusClientMixin.convert_to_registers(value, datatype) + result = ModbusClientMixin.convert_from_registers(regs, datatype) + if datatype == ModbusClientMixin.DATATYPE.FLOAT32 or datatype == ModbusClientMixin.DATATYPE.FLOAT64: + result = round(result, 6) assert result == value + assert regs == registers def test_client_mixin_convert_fail(): From 14ec87e3e9852933af7dce78ed69cedf8f404110 Mon Sep 17 00:00:00 2001 From: jan iversen Date: Fri, 14 Jun 2024 13:21:58 +0200 Subject: [PATCH 57/88] Solve serial unrequested frame. (#2219) --- pymodbus/framer/old_framer_rtu.py | 2 ++ pymodbus/transaction.py | 4 +++- test/framers/test_old_framers.py | 4 ++-- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/pymodbus/framer/old_framer_rtu.py b/pymodbus/framer/old_framer_rtu.py index a57fd75da..024dba269 100644 --- a/pymodbus/framer/old_framer_rtu.py +++ b/pymodbus/framer/old_framer_rtu.py @@ -80,6 +80,7 @@ def is_frame_ready(self): try: self._header["uid"] = int(self._buffer[0]) self._header["tid"] = int(self._buffer[0]) + self._header["tid"] = 0 # fix for now func_code = int(self._buffer[1]) pdu_class = self.decoder.lookupPduClass(func_code) size = pdu_class.calculateRtuFrameSize(self._buffer) @@ -117,6 +118,7 @@ def check_frame(self): try: self._header["uid"] = int(self._buffer[0]) self._header["tid"] = int(self._buffer[0]) + self._header["tid"] = 0 # fix for now func_code = int(self._buffer[1]) pdu_class = self.decoder.lookupPduClass(func_code) size = pdu_class.calculateRtuFrameSize(self._buffer) diff --git a/pymodbus/transaction.py b/pymodbus/transaction.py index 13f84e31a..9aa614e82 100644 --- a/pymodbus/transaction.py +++ b/pymodbus/transaction.py @@ -453,7 +453,9 @@ def getTransaction(self, tid): Log.debug("Getting transaction {}", tid) if not tid: if self.transactions: - return self.transactions.popitem()[1] + ret = self.transactions.popitem()[1] + self.transactions.clear() + return ret return None return self.transactions.pop(tid, None) diff --git a/test/framers/test_old_framers.py b/test/framers/test_old_framers.py index e7b394701..39bc5035b 100644 --- a/test/framers/test_old_framers.py +++ b/test/framers/test_old_framers.py @@ -216,7 +216,7 @@ def callback(data): "crc": b"\x49\xAD", "uid": 17, "len": 11, - "tid": 17, + "tid": 0, }, ), ( @@ -225,7 +225,7 @@ def callback(data): "crc": b"\x49\xAD", "uid": 17, "len": 11, - "tid": 17, + "tid": 0, }, ), ], From a1696f79868b77259329f14d8f6112233b265a03 Mon Sep 17 00:00:00 2001 From: jan iversen Date: Fri, 14 Jun 2024 13:56:19 +0200 Subject: [PATCH 58/88] Log comm retries. (#2220) --- pymodbus/transaction.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pymodbus/transaction.py b/pymodbus/transaction.py index 9aa614e82..a537f120b 100644 --- a/pymodbus/transaction.py +++ b/pymodbus/transaction.py @@ -220,6 +220,7 @@ def execute(self, request): # noqa: C901 else: break # full = False + Log.debug("Retry getting response: - {}", _buffer) addTransaction = partial( # pylint: disable=invalid-name self.addTransaction, tid=request.transaction_id, From ae415ad0ae128f661b2d4ea1a674a38bc21d769c Mon Sep 17 00:00:00 2001 From: jan iversen Date: Thu, 27 Jun 2024 11:58:25 +0200 Subject: [PATCH 59/88] Merge v3.6.9 back to dev. (#2226) * add _legacy_decoder to message rtu (#2119) * Add generate_ssl() to TLS client as helper. (#2120) * ASCII framer using message decode() (#2128) * SOCKET/TLS framer using message decode(). (#2129) * Fix decode for wrong mdap len. * Streamline message class. (#2133) * modbus_server: call execute in a way that those can be either coroutines or normal methods (#2139) * Clean datastore setValues. (#2145) * fixed kwargs not being expanded for actions on bit registers, adjusted tests to catch this issue (#2161) * datastore: add async_setValues/getValues methods (#2165) Co-authored-by: Ilkka Ollakka * Request/Response: change execute to be async method (#2142) * Bump actions CI. (#2166) * Fix usage of AsyncModbusTcpClient in client docs page (#2169) * Sphinx: do not turn warnings into errors. * Add minimal devcontainer. (#2172) * Transaction id overrun. * call async datastore from modbus server (#2144) * Datastore will not return ExceptionResponse. (#2175) * Describe zero_mode in ModbusSlaveContext.__init__ (#2187) * Solve pylint error. * Show error if example is run without support files. (#2189) * Fix usage file names (#2194) * Update client.rst (#2199) * Transaction_id for serial == 0. (#2208) * Remember to remove serial writer. (#2209) * Fix writing to serial (rs485) on windows os. (#2191) Co-authored-by: jan iversen * test convert registers with 1234.... (#2217) * Solve serial unrequested frame. (#2219) * Log comm retries. (#2220) * prepare v3.6.9. * pylint. * Remove python 3.8 from CI. --------- Co-authored-by: Ilkka Ollakka <14361597+ilkka-ollakka@users.noreply.github.com> Co-authored-by: sumguytho <48988983+sumguytho@users.noreply.github.com> Co-authored-by: Ilkka Ollakka Co-authored-by: Yohrog <69543220+Yohrog@users.noreply.github.com> Co-authored-by: James Cameron <90580716+jcameron-sso@users.noreply.github.com> Co-authored-by: Qi Li <160646648+qili-eaton@users.noreply.github.com> Co-authored-by: andrew-harness <75149647+andrew-harness@users.noreply.github.com> --- AUTHORS.rst | 5 +++ CHANGELOG.rst | 34 +++++++++++++++++++ README.rst | 2 +- pymodbus/__init__.py | 2 +- pymodbus/transaction.py | 2 +- ...c_multidrop.py => server_multidrop_tbd.py} | 0 test/test_file_message.py | 3 +- v3.6.9..dev | 20 +++++++++++ 8 files changed, 63 insertions(+), 5 deletions(-) rename test/framers/{test_tbc_multidrop.py => server_multidrop_tbd.py} (100%) create mode 100644 v3.6.9..dev diff --git a/AUTHORS.rst b/AUTHORS.rst index 713a481dc..6d015f1e9 100644 --- a/AUTHORS.rst +++ b/AUTHORS.rst @@ -18,6 +18,7 @@ Thanks to - Alois Hockenschlohe - Arjan - André Srinivasan +- andrew-harness - banana-sun - Blaise Thompson - CapraTheBest @@ -43,6 +44,7 @@ Thanks to - Jakob Ruhe - Jakob Schlyter - James Braza +- James Cameron - James Hilliard - jan iversen - Jerome Velociter @@ -63,13 +65,16 @@ Thanks to - Pavel Kostromitinov - peufeu2 - Philip Couling +- Qi Li - Sebastian Machuca - Sefa Keleş - Steffen Beyer +- sumguytho - Thijs W - Totally a booplicate - WouterTuinstra - wriswith +- Yohrog - yyokusa diff --git a/CHANGELOG.rst b/CHANGELOG.rst index f5a45789c..2c3347d78 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -7,6 +7,40 @@ helps make pymodbus a better product. :ref:`Authors`: contains a complete list of volunteers have contributed to each major version. + +Version 3.6.9 +------------- +* Remove python 3.8 from CI +* Log comm retries. (#2220) +* Solve serial unrequested frame. (#2219) +* test convert registers with 1234.... (#2217) +* Fix writing to serial (rs485) on windows os. (#2191) +* Remember to remove serial writer. (#2209) +* Update client.rst (#2199) +* Fix usage file names (#2194) +* Show error if example is run without support files. (#2189) +* Solve pylint error. +* Describe zero_mode in ModbusSlaveContext.__init__ (#2187) +* Datastore will not return ExceptionResponse. (#2175) +* call async datastore from modbus server (#2144) +* Transaction id overrun. +* Add minimal devcontainer. (#2172) +* Sphinx: do not turn warnings into errors. +* Fix usage of AsyncModbusTcpClient in client docs page (#2169) +* Bump actions CI. (#2166) +* Request/Response: change execute to be async method (#2142) +* datastore: add async_setValues/getValues methods (#2165) +* fixed kwargs not being expanded for actions on bit registers, adjusted tests to catch this issue (#2161) +* Clean datastore setValues. (#2145) +* modbus_server: call execute in a way that those can be either coroutines or normal methods (#2139) +* Streamline message class. (#2133) +* Fix decode for wrong mdap len. +* SOCKET/TLS framer using message decode(). (#2129) +* ASCII framer using message decode() (#2128) +* Add generate_ssl() to TLS client as helper. (#2120) +* add _legacy_decoder to message rtu (#2119) + + Version 3.6.8 ------------- * Allow socket exception response with wrong length diff --git a/README.rst b/README.rst index bb9cb3316..f78889652 100644 --- a/README.rst +++ b/README.rst @@ -11,7 +11,7 @@ PyModbus - A Python Modbus Stack Pymodbus is a full Modbus protocol implementation offering client/server with synchronous/asynchronous API a well as simulators. -Current release is `3.6.8 `_. +Current release is `3.6.9 `_. Bleeding edge (not released) is `dev `_. diff --git a/pymodbus/__init__.py b/pymodbus/__init__.py index 920380d17..3a67cada4 100644 --- a/pymodbus/__init__.py +++ b/pymodbus/__init__.py @@ -18,5 +18,5 @@ from pymodbus.pdu import ExceptionResponse -__version__ = "3.7.0dev2" +__version__ = "3.6.9" __version_full__ = f"[pymodbus, version {__version__}]" diff --git a/pymodbus/transaction.py b/pymodbus/transaction.py index a537f120b..09f648270 100644 --- a/pymodbus/transaction.py +++ b/pymodbus/transaction.py @@ -454,7 +454,7 @@ def getTransaction(self, tid): Log.debug("Getting transaction {}", tid) if not tid: if self.transactions: - ret = self.transactions.popitem()[1] + ret = self.transactions.popitem()[1] self.transactions.clear() return ret return None diff --git a/test/framers/test_tbc_multidrop.py b/test/framers/server_multidrop_tbd.py similarity index 100% rename from test/framers/test_tbc_multidrop.py rename to test/framers/server_multidrop_tbd.py diff --git a/test/test_file_message.py b/test/test_file_message.py index 6882c6b89..36ef6ee0e 100644 --- a/test/test_file_message.py +++ b/test/test_file_message.py @@ -16,8 +16,7 @@ WriteFileRecordRequest, WriteFileRecordResponse, ) - -from .conftest import MockContext +from test.conftest import MockContext # pylint: disable=wrong-import-order TEST_MESSAGE = b"\x00\n\x00\x08\x00\x01\x00\x02\x00\x03\x00\x04" diff --git a/v3.6.9..dev b/v3.6.9..dev new file mode 100644 index 000000000..befbfe6df --- /dev/null +++ b/v3.6.9..dev @@ -0,0 +1,20 @@ + +V3.7.0 (NOT INCLUDED in 3.6.7-8-9) +14ec87e3 Solve serial unrequested frame. (#2219) +5bddf9e8 Update third party versions. (#2216) +45c5116e Transaction_id for serial == 0. (#2208) +c91b2182 Sync TLS needs time before reading frame (#2186) +a48aeeb4 Update transaction.py (#2174) +e9c187e9 Merge 3.6.8 effects. +681fd2c8 Merge master v3.6.8 into dev +8a836b20 PDU classes --> pymodbus/pdu. (#2160) +e8063fa5 Merge 3.6.7 back into dev. (#2156) +a6b43dd7 Speed up no data detection. (#2150) +c4c14cab RTU decode hunt part. (#2138) +9e9e50e2 Dislodge client classes from modbusProtocol. (#2137) +9f736dfe Merge new message layer and old framer directory. (#2135) +331dc636 Coverage == 91%. (#2132) +86d5afe2 Remove binary_framer. (#2130) +0803ff70 on_reconnect_callback --> on_connect_callback. (#2122) +2c36fd3f Remove certfile,keyfile,password from TLS client. (#2121) +58a1c37d Drop support for python 3.8 (#2112) From 2e12ef161cd2f130dedcf95233e5ba241caf092e Mon Sep 17 00:00:00 2001 From: jan iversen Date: Thu, 27 Jun 2024 12:49:43 +0200 Subject: [PATCH 60/88] Prepare dev. --- .pypirc | 3 +++ pymodbus/__init__.py | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) create mode 100644 .pypirc diff --git a/.pypirc b/.pypirc new file mode 100644 index 000000000..47296d878 --- /dev/null +++ b/.pypirc @@ -0,0 +1,3 @@ +[pypi] + username = __token__ + password = pypi-AgEIcHlwaS5vcmcCJDg0ZWExZmE1LTRjYzQtNDI3Ni04YWVlLTlmMDAwMWZlNGJkZQACKlszLCI0YTM4N2JjMC0yMDAwLTRmYmEtYTI4NC0wNGQ1MzBkNjA3NjAiXQAABiCkDfmYJ4z05OKQau3wmoiMqpIl1Z9dB0scS247w3Kecw diff --git a/pymodbus/__init__.py b/pymodbus/__init__.py index 3a67cada4..d238f3d6f 100644 --- a/pymodbus/__init__.py +++ b/pymodbus/__init__.py @@ -18,5 +18,5 @@ from pymodbus.pdu import ExceptionResponse -__version__ = "3.6.9" +__version__ = "3.7.0dev3" __version_full__ = f"[pymodbus, version {__version__}]" From 94e5e6d8acef486261aaaeb63af72cc7f895d6a0 Mon Sep 17 00:00:00 2001 From: jan iversen Date: Thu, 27 Jun 2024 12:56:40 +0200 Subject: [PATCH 61/88] ups. --- .gitignore | 1 + .pypirc | 3 --- 2 files changed, 1 insertion(+), 3 deletions(-) delete mode 100644 .pypirc diff --git a/.gitignore b/.gitignore index 965c21c94..c276f7914 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,7 @@ __pycache__/ .idea/ .noseids .pymodhis +.pypirc .venv .vscode .vscode/ diff --git a/.pypirc b/.pypirc deleted file mode 100644 index 47296d878..000000000 --- a/.pypirc +++ /dev/null @@ -1,3 +0,0 @@ -[pypi] - username = __token__ - password = pypi-AgEIcHlwaS5vcmcCJDg0ZWExZmE1LTRjYzQtNDI3Ni04YWVlLTlmMDAwMWZlNGJkZQACKlszLCI0YTM4N2JjMC0yMDAwLTRmYmEtYTI4NC0wNGQ1MzBkNjA3NjAiXQAABiCkDfmYJ4z05OKQau3wmoiMqpIl1Z9dB0scS247w3Kecw From 5e146c08fcb9b406362691ecfc758ef08f947ee1 Mon Sep 17 00:00:00 2001 From: James Hilliard Date: Fri, 28 Jun 2024 05:35:54 -0600 Subject: [PATCH 62/88] Simplify some BinaryPayload pack operations (#2224) --- pymodbus/payload.py | 55 +++++++++++++++++++++------------------------ 1 file changed, 26 insertions(+), 29 deletions(-) diff --git a/pymodbus/payload.py b/pymodbus/payload.py index 583aa3b30..3f20ad46d 100644 --- a/pymodbus/payload.py +++ b/pymodbus/payload.py @@ -11,6 +11,8 @@ "BinaryPayloadDecoder", ] +from array import array + # pylint: disable=missing-type-doc from struct import pack, unpack @@ -23,9 +25,6 @@ ) -WC = {"b": 1, "h": 2, "e": 2, "i": 4, "l": 4, "q": 8, "f": 4, "d": 8} - - class BinaryPayloadBuilder: """A utility that helps build payload messages to be written with the various modbus messages. @@ -69,15 +68,14 @@ def _pack_words(self, fstring: str, value) -> bytes: :return: """ value = pack(f"!{fstring}", value) - wordorder = WC.get(fstring.lower()) // 2 # type: ignore[operator] - upperbyte = f"!{wordorder}H" - payload = unpack(upperbyte, value) - - if self._wordorder == Endian.LITTLE: - payload = payload[::-1] - - fstring = self._byteorder + "H" - return b"".join(pack(fstring, word) for word in payload) + if Endian.LITTLE in {self._byteorder, self._wordorder}: + value = array("H", value) + if self._byteorder == Endian.LITTLE: + value.byteswap() + if self._wordorder == Endian.LITTLE: + value.reverse() + value = value.tobytes() + return value def encode(self) -> bytes: """Get the payload buffer encoded in bytes.""" @@ -295,7 +293,7 @@ def fromRegisters( """ Log.debug("{}", registers) if isinstance(registers, list): # repack into flat binary - payload = b"".join(pack("!H", x) for x in registers) + payload = pack(f"!{len(registers)}H", *registers) return cls(payload, byteorder, wordorder) raise ParameterException("Invalid collection of registers supplied") @@ -324,7 +322,7 @@ def fromCoils( return cls(payload, byteorder) raise ParameterException("Invalid collection of coils supplied") - def _unpack_words(self, fstring: str, handle) -> bytes: + def _unpack_words(self, handle) -> bytes: """Unpack words based on the word order and byte order. # ---------------------------------------------- # @@ -336,15 +334,14 @@ def _unpack_words(self, fstring: str, handle) -> bytes: :param handle: Value to be unpacked :return: """ - wc_value = WC.get(fstring.lower()) // 2 # type: ignore[operator] - handle = unpack(f"!{wc_value}H", handle) - if self._wordorder == Endian.LITTLE: - handle = list(reversed(handle)) - - # Repack as unsigned Integer - handle = [pack(self._byteorder + "H", p) for p in handle] + if Endian.LITTLE in {self._byteorder, self._wordorder}: + handle = array("H", handle) + if self._byteorder == Endian.LITTLE: + handle.byteswap() + if self._wordorder == Endian.LITTLE: + handle.reverse() + handle = handle.tobytes() Log.debug("handle: {}", handle) - handle = b"".join(handle) return handle def reset(self): @@ -377,7 +374,7 @@ def decode_32bit_uint(self): self._pointer += 4 fstring = "I" handle = self._payload[self._pointer - 4 : self._pointer] - handle = self._unpack_words(fstring, handle) + handle = self._unpack_words(handle) return unpack("!" + fstring, handle)[0] def decode_64bit_uint(self): @@ -385,7 +382,7 @@ def decode_64bit_uint(self): self._pointer += 8 fstring = "Q" handle = self._payload[self._pointer - 8 : self._pointer] - handle = self._unpack_words(fstring, handle) + handle = self._unpack_words(handle) return unpack("!" + fstring, handle)[0] def decode_8bit_int(self): @@ -407,7 +404,7 @@ def decode_32bit_int(self): self._pointer += 4 fstring = "i" handle = self._payload[self._pointer - 4 : self._pointer] - handle = self._unpack_words(fstring, handle) + handle = self._unpack_words(handle) return unpack("!" + fstring, handle)[0] def decode_64bit_int(self): @@ -415,7 +412,7 @@ def decode_64bit_int(self): self._pointer += 8 fstring = "q" handle = self._payload[self._pointer - 8 : self._pointer] - handle = self._unpack_words(fstring, handle) + handle = self._unpack_words(handle) return unpack("!" + fstring, handle)[0] def decode_16bit_float(self): @@ -423,7 +420,7 @@ def decode_16bit_float(self): self._pointer += 2 fstring = "e" handle = self._payload[self._pointer - 2 : self._pointer] - handle = self._unpack_words(fstring, handle) + handle = self._unpack_words(handle) return unpack("!" + fstring, handle)[0] def decode_32bit_float(self): @@ -431,7 +428,7 @@ def decode_32bit_float(self): self._pointer += 4 fstring = "f" handle = self._payload[self._pointer - 4 : self._pointer] - handle = self._unpack_words(fstring, handle) + handle = self._unpack_words(handle) return unpack("!" + fstring, handle)[0] def decode_64bit_float(self): @@ -439,7 +436,7 @@ def decode_64bit_float(self): self._pointer += 8 fstring = "d" handle = self._payload[self._pointer - 8 : self._pointer] - handle = self._unpack_words(fstring, handle) + handle = self._unpack_words(handle) return unpack("!" + fstring, handle)[0] def decode_string(self, size=1): From d7e3e407381a02d942044119ea1791d7f97bb8ac Mon Sep 17 00:00:00 2001 From: James Hilliard Date: Thu, 18 Jul 2024 00:46:32 -0600 Subject: [PATCH 63/88] Refactor transaction handling to better separate async and sync code. (#2232) --- pymodbus/client/base.py | 44 +++--- pymodbus/client/modbusclientprotocol.py | 11 +- pymodbus/client/serial.py | 16 +- pymodbus/client/tcp.py | 9 +- pymodbus/client/udp.py | 9 +- pymodbus/framer/old_framer_base.py | 26 ++-- pymodbus/framer/old_framer_rtu.py | 4 +- pymodbus/framer/old_framer_socket.py | 4 +- pymodbus/framer/old_framer_tls.py | 4 +- pymodbus/pdu/pdu.py | 4 +- pymodbus/transaction.py | 188 +++++++++++++----------- pymodbus/transport/transport.py | 2 +- test/framers/test_tbc_transaction.py | 6 +- test/sub_client/test_client.py | 6 +- test/test_transaction.py | 6 +- 15 files changed, 179 insertions(+), 160 deletions(-) diff --git a/pymodbus/client/base.py b/pymodbus/client/base.py index 95dcaf1a4..90cbecc2d 100644 --- a/pymodbus/client/base.py +++ b/pymodbus/client/base.py @@ -3,6 +3,7 @@ import asyncio import socket +from abc import abstractmethod from collections.abc import Awaitable, Callable from dataclasses import dataclass from typing import Any, cast @@ -14,7 +15,7 @@ from pymodbus.framer import FRAMER_NAME_TO_CLASS, FramerType, ModbusFramer from pymodbus.logging import Log from pymodbus.pdu import ModbusRequest, ModbusResponse -from pymodbus.transaction import ModbusTransactionManager +from pymodbus.transaction import SyncModbusTransactionManager from pymodbus.transport import CommParams from pymodbus.utilities import ModbusTransactionState @@ -53,7 +54,6 @@ def __init__( framer: FramerType, timeout: float = 3, retries: int = 3, - retry_on_empty: bool = False, broadcast_enable: bool = False, reconnect_delay: float = 0.1, reconnect_delay_max: float = 300, @@ -81,8 +81,6 @@ def __init__( stopbits=kwargs.get("stopbits", None), handle_local_echo=kwargs.get("handle_local_echo", False), ), - retries, - retry_on_empty, on_connect_callback, ) self.no_resend_on_retry = no_resend_on_retry @@ -143,7 +141,7 @@ def idle_time(self) -> float: return 0 return self.last_frame_end + self.silent_interval - def execute(self, request: ModbusRequest | None = None): + def execute(self, request: ModbusRequest): """Execute request and get response (call **sync/async**). :param request: The request to process @@ -165,7 +163,7 @@ async def async_execute(self, request) -> ModbusResponse: count = 0 while count <= self.retries: async with self._lock: - req = self.build_response(request.transaction_id) + req = self.build_response(request) if not count or not self.no_resend_on_retry: self.ctx.framer.resetFrame() self.ctx.send(packet) @@ -187,25 +185,17 @@ async def async_execute(self, request) -> ModbusResponse: return resp # type: ignore[return-value] - def build_response(self, tid): + def build_response(self, request: ModbusRequest): """Return a deferred response for the current request.""" my_future: asyncio.Future = asyncio.Future() + request.fut = my_future if not self.ctx.transport: if not my_future.done(): my_future.set_exception(ConnectionException("Client is not connected")) else: - self.ctx.transaction.addTransaction(my_future, tid) + self.ctx.transaction.addTransaction(request) return my_future - # ----------------------------------------------------------------------- # - # Internal methods - # ----------------------------------------------------------------------- # - def recv(self, size): - """Receive data. - - :meta private: - """ - # ----------------------------------------------------------------------- # # The magic methods # ----------------------------------------------------------------------- # @@ -309,10 +299,10 @@ def __init__( self.slaves: list[int] = [] # Common variables. - self.framer = FRAMER_NAME_TO_CLASS.get( + self.framer: ModbusFramer = FRAMER_NAME_TO_CLASS.get( framer, cast(type[ModbusFramer], framer) )(ClientDecoder(), self) - self.transaction = ModbusTransactionManager( + self.transaction = SyncModbusTransactionManager( self, retries=retries, retry_on_empty=retry_on_empty, **kwargs ) self.reconnect_delay_current = self.params.reconnect_delay or 0 @@ -346,7 +336,7 @@ def idle_time(self) -> float: return 0 return self.last_frame_end + self.silent_interval - def execute(self, request: ModbusRequest | None = None) -> ModbusResponse: + def execute(self, request: ModbusRequest) -> ModbusResponse: """Execute request and get response (call **sync/async**). :param request: The request to process @@ -360,7 +350,7 @@ def execute(self, request: ModbusRequest | None = None) -> ModbusResponse: # ----------------------------------------------------------------------- # # Internal methods # ----------------------------------------------------------------------- # - def send(self, request): + def _start_send(self): """Send request. :meta private: @@ -368,14 +358,20 @@ def send(self, request): if self.state != ModbusTransactionState.RETRYING: Log.debug('New Transaction state "SENDING"') self.state = ModbusTransactionState.SENDING - return request - def recv(self, size): + @abstractmethod + def send(self, request: bytes) -> int: + """Send request. + + :meta private: + """ + + @abstractmethod + def recv(self, size: int | None) -> bytes: """Receive data. :meta private: """ - return size @classmethod def get_address_family(cls, address): diff --git a/pymodbus/client/modbusclientprotocol.py b/pymodbus/client/modbusclientprotocol.py index f95fff75d..c5d935076 100644 --- a/pymodbus/client/modbusclientprotocol.py +++ b/pymodbus/client/modbusclientprotocol.py @@ -29,8 +29,6 @@ def __init__( self, framer: FramerType, params: CommParams, - retries: int, - retry_on_empty: bool, on_connect_callback: Callable[[bool], None] | None = None, ) -> None: """Initialize a client instance.""" @@ -45,17 +43,16 @@ def __init__( self.framer = FRAMER_NAME_TO_CLASS.get( framer, cast(type[ModbusFramer], framer) )(ClientDecoder(), self) - self.transaction = ModbusTransactionManager( - self, retries=retries, retry_on_empty=retry_on_empty - ) + self.transaction = ModbusTransactionManager() def _handle_response(self, reply, **_kwargs): """Handle the processed response and link to correct deferred.""" if reply is not None: tid = reply.transaction_id if handler := self.transaction.getTransaction(tid): - if not handler.done(): - handler.set_result(reply) + reply.request = handler + if not handler.fut.done(): + handler.fut.set_result(reply) else: Log.debug("Unrequested message: {}", reply, ":str") diff --git a/pymodbus/client/serial.py b/pymodbus/client/serial.py index 0fb4c6034..2872b7d57 100644 --- a/pymodbus/client/serial.py +++ b/pymodbus/client/serial.py @@ -193,7 +193,7 @@ def connected(self): """Connect internal.""" return self.connect() - def connect(self): + def connect(self) -> bool: """Connect to the modbus serial server.""" if self.socket: return True @@ -227,25 +227,26 @@ def _in_waiting(self): """Return waiting bytes.""" return getattr(self.socket, "in_waiting") if hasattr(self.socket, "in_waiting") else getattr(self.socket, "inWaiting")() - def send(self, request): + def send(self, request: bytes) -> int: """Send data on the underlying socket. If receive buffer still holds some data then flush it. Sleep if last send finished less than 3.5 character times ago. """ - super().send(request) + super()._start_send() if not self.socket: raise ConnectionException(str(self)) if request: if waitingbytes := self._in_waiting(): result = self.socket.read(waitingbytes) Log.warning("Cleanup recv buffer before send: {}", result, ":hex") - size = self.socket.write(request) + if (size := self.socket.write(request)) is None: + size = 0 return size return 0 - def _wait_for_data(self): + def _wait_for_data(self) -> int: """Wait for data.""" size = 0 more_data = False @@ -264,9 +265,8 @@ def _wait_for_data(self): time.sleep(self._recv_interval) return size - def recv(self, size): + def recv(self, size: int | None) -> bytes: """Read data from the underlying descriptor.""" - super().recv(size) if not self.socket: raise ConnectionException(str(self)) if size is None: @@ -276,7 +276,7 @@ def recv(self, size): result = self.socket.read(size) return result - def is_socket_open(self): + def is_socket_open(self) -> bool: """Check if socket is open.""" if self.socket: return self.socket.is_open diff --git a/pymodbus/client/tcp.py b/pymodbus/client/tcp.py index aa71c9e40..d975482eb 100644 --- a/pymodbus/client/tcp.py +++ b/pymodbus/client/tcp.py @@ -180,16 +180,15 @@ def close(self): def send(self, request): """Send data on the underlying socket.""" - super().send(request) + super()._start_send() if not self.socket: raise ConnectionException(str(self)) if request: return self.socket.send(request) return 0 - def recv(self, size): + def recv(self, size: int | None) -> bytes: """Read data from the underlying descriptor.""" - super().recv(size) if not self.socket: raise ConnectionException(str(self)) @@ -241,7 +240,7 @@ def recv(self, size): return b"".join(data) - def _handle_abrupt_socket_close(self, size, data, duration): + def _handle_abrupt_socket_close(self, size: int | None, data: list[bytes], duration: float) -> bytes: """Handle unexpected socket close by remote end. Intended to be invoked after determining that the remote end @@ -271,7 +270,7 @@ def _handle_abrupt_socket_close(self, size, data, duration): msg += " without response from slave before it closed connection" raise ConnectionException(msg) - def is_socket_open(self): + def is_socket_open(self) -> bool: """Check if socket is open.""" return self.socket is not None diff --git a/pymodbus/client/udp.py b/pymodbus/client/udp.py index 4f2f3d2df..acadf1ab5 100644 --- a/pymodbus/client/udp.py +++ b/pymodbus/client/udp.py @@ -169,12 +169,12 @@ def close(self): """ self.socket = None - def send(self, request): + def send(self, request: bytes) -> int: """Send data on the underlying socket. :meta private: """ - super().send(request) + super()._start_send() if not self.socket: raise ConnectionException(str(self)) if request: @@ -183,14 +183,15 @@ def send(self, request): ) return 0 - def recv(self, size): + def recv(self, size: int | None) -> bytes: """Read data from the underlying descriptor. :meta private: """ - super().recv(size) if not self.socket: raise ConnectionException(str(self)) + if size is None: + size = 0 return self.socket.recvfrom(size)[0] def is_socket_open(self): diff --git a/pymodbus/framer/old_framer_base.py b/pymodbus/framer/old_framer_base.py index 4922be1c4..8f5a9be2a 100644 --- a/pymodbus/framer/old_framer_base.py +++ b/pymodbus/framer/old_framer_base.py @@ -3,13 +3,17 @@ from __future__ import annotations import time -from typing import Any +from typing import TYPE_CHECKING, Any from pymodbus.factory import ClientDecoder, ServerDecoder from pymodbus.framer.base import FramerBase from pymodbus.logging import Log +from pymodbus.pdu import ModbusRequest +if TYPE_CHECKING: + from pymodbus.client.base import ModbusBaseSyncClient + # Unit ID, Function Code BYTE_ORDER = ">" FRAME_HEADER = "BB" @@ -29,7 +33,7 @@ class ModbusFramer: def __init__( self, decoder: ClientDecoder | ServerDecoder, - client, + client: ModbusBaseSyncClient, ) -> None: """Initialize a new instance of the framer. @@ -37,7 +41,13 @@ def __init__( """ self.decoder = decoder self.client = client - self._header: dict[str, Any] = { + self._header: dict[str, Any] + self._reset_header() + self._buffer = b"" + self.message_handler: FramerBase + + def _reset_header(self) -> None: + self._header = { "lrc": "0000", "len": 0, "uid": 0x00, @@ -45,8 +55,6 @@ def __init__( "pid": 0, "crc": b"\x00\x00", } - self._buffer = b"" - self.message_handler: FramerBase def _validate_slave_id(self, slaves: list, single: bool) -> bool: """Validate if the received data is valid for the client. @@ -63,7 +71,7 @@ def _validate_slave_id(self, slaves: list, single: bool) -> bool: return True return self._header["uid"] in slaves - def sendPacket(self, message): + def sendPacket(self, message: bytes): """Send packets on the bus. With 3.5char delay between frames @@ -72,7 +80,7 @@ def sendPacket(self, message): """ return self.client.send(message) - def recvPacket(self, size): + def recvPacket(self, size: int) -> bytes: """Receive packet from the bus. With specified len @@ -117,7 +125,7 @@ def populateResult(self, result): result.transaction_id = self._header.get("tid", 0) result.protocol_id = self._header.get("pid", 0) - def processIncomingPacket(self, data, callback, slave, **kwargs): + def processIncomingPacket(self, data: bytes, callback, slave, **kwargs): """Process new packet pattern. This takes in a new request packet, adds it to the current @@ -150,7 +158,7 @@ def frameProcessIncomingPacket( ) -> None: """Process new packet pattern.""" - def buildPacket(self, message) -> bytes: + def buildPacket(self, message: ModbusRequest) -> bytes: """Create a ready to send modbus packet. :param message: The populated request/response to send diff --git a/pymodbus/framer/old_framer_rtu.py b/pymodbus/framer/old_framer_rtu.py index 024dba269..ccd293352 100644 --- a/pymodbus/framer/old_framer_rtu.py +++ b/pymodbus/framer/old_framer_rtu.py @@ -146,7 +146,7 @@ def check_frame(self): Log.debug("Frame check failed, ignoring!!") x = self._buffer self.resetFrame() - self._buffer = x + self._buffer: bytes = x skip_cur_frame = True continue start = self._hsize @@ -176,7 +176,7 @@ def buildPacket(self, message): message.transaction_id = 0 return packet - def sendPacket(self, message): + def sendPacket(self, message: bytes) -> int: """Send packets on the bus with 3.5char delay between frames. :param message: Message to be sent over the bus diff --git a/pymodbus/framer/old_framer_socket.py b/pymodbus/framer/old_framer_socket.py index bc515ab2a..e812fdda9 100644 --- a/pymodbus/framer/old_framer_socket.py +++ b/pymodbus/framer/old_framer_socket.py @@ -88,8 +88,8 @@ def frameProcessIncomingPacket(self, single, callback, slave, tid=None, **kwargs self.resetFrame() raise ModbusIOException("Unable to decode request") self.populateResult(result) - self._buffer = self._buffer[used_len:] - self._header = {"tid": 0, "pid": 0, "len": 0, "uid": 0} + self._buffer: bytes = self._buffer[used_len:] + self._reset_header() if tid and tid != result.transaction_id: self.resetFrame() else: diff --git a/pymodbus/framer/old_framer_tls.py b/pymodbus/framer/old_framer_tls.py index a6a4df892..71e0a5a9d 100644 --- a/pymodbus/framer/old_framer_tls.py +++ b/pymodbus/framer/old_framer_tls.py @@ -64,6 +64,6 @@ def frameProcessIncomingPacket(self, _single, callback, _slave, _tid=None, **kwa self.resetFrame() raise ModbusIOException("Unable to decode request") self.populateResult(result) - self._buffer = self._buffer[used_len:] - self._header = {"tid": 0, "pid": 0, "len": 0, "uid": 0} + self._buffer: bytes = self._buffer[used_len:] + self._reset_header() callback(result) # defer or push to a thread? diff --git a/pymodbus/pdu/pdu.py b/pymodbus/pdu/pdu.py index c50cf281a..b430d5740 100644 --- a/pymodbus/pdu/pdu.py +++ b/pymodbus/pdu/pdu.py @@ -102,12 +102,13 @@ class ModbusRequest(ModbusPDU): function_code = -1 - def __init__(self, slave=0, **kwargs): # pylint: disable=useless-parent-delegation + def __init__(self, slave=0, **kwargs): """Proxy to the lower level initializer. :param slave: Modbus slave slave ID """ super().__init__(slave, **kwargs) + self.fut = None def doException(self, exception): """Build an error response based on the function. @@ -146,6 +147,7 @@ def __init__(self, slave=0, **kwargs): super().__init__(slave, **kwargs) self.bits = [] self.registers = [] + self.request = None def isError(self) -> bool: """Check if the error is a success or failure.""" diff --git a/pymodbus/transaction.py b/pymodbus/transaction.py index 09f648270..914edb7bb 100644 --- a/pymodbus/transaction.py +++ b/pymodbus/transaction.py @@ -1,4 +1,6 @@ """Collection of transaction based abstractions.""" +from __future__ import annotations + __all__ = [ "ModbusTransactionManager", @@ -6,14 +8,14 @@ "ModbusTlsFramer", "ModbusRtuFramer", "ModbusAsciiFramer", + "SyncModbusTransactionManager", ] -# pylint: disable=missing-type-doc import struct import time from contextlib import suppress -from functools import partial from threading import RLock +from typing import TYPE_CHECKING from pymodbus.exceptions import ( ConnectionException, @@ -27,15 +29,96 @@ ModbusTlsFramer, ) from pymodbus.logging import Log +from pymodbus.pdu import ModbusRequest +from pymodbus.transport import CommType from pymodbus.utilities import ModbusTransactionState, hexlify_packets +if TYPE_CHECKING: + from pymodbus.client.base import ModbusBaseSyncClient + + # --------------------------------------------------------------------------- # # The Global Transaction Manager # --------------------------------------------------------------------------- # class ModbusTransactionManager: """Implement a transaction for a manager. + Results are keyed based on the supplied transaction id. + """ + + def __init__(self): + """Initialize an instance of the ModbusTransactionManager.""" + self.tid = 0 + self.transactions: dict[int, ModbusRequest] = {} + + def __iter__(self): + """Iterate over the current managed transactions. + + :returns: An iterator of the managed transactions + """ + return iter(self.transactions.keys()) + + def addTransaction(self, request: ModbusRequest): + """Add a transaction to the handler. + + This holds the request in case it needs to be resent. + After being sent, the request is removed. + + :param request: The request to hold on to + """ + tid = request.transaction_id + Log.debug("Adding transaction {}", tid) + self.transactions[tid] = request + + def getTransaction(self, tid: int): + """Return a transaction matching the referenced tid. + + If the transaction does not exist, None is returned + + :param tid: The transaction to retrieve + + """ + Log.debug("Getting transaction {}", tid) + if not tid: + if self.transactions: + ret = self.transactions.popitem()[1] + self.transactions.clear() + return ret + return None + return self.transactions.pop(tid, None) + + def delTransaction(self, tid: int): + """Remove a transaction matching the referenced tid. + + :param tid: The transaction to remove + """ + Log.debug("deleting transaction {}", tid) + self.transactions.pop(tid, None) + + def getNextTID(self) -> int: + """Retrieve the next unique transaction identifier. + + This handles incrementing the identifier after + retrieval + + :returns: The next unique transaction identifier + """ + if self.tid < 65000: + self.tid += 1 + else: + self.tid = 1 + return self.tid + + def reset(self): + """Reset the transaction identifier.""" + self.tid = 0 + self.transactions = {} + + +class SyncModbusTransactionManager(ModbusTransactionManager): + """Implement a transaction for a manager. + The transaction protocol can be represented by the following pseudo code:: count = 0 @@ -51,32 +134,24 @@ class ModbusTransactionManager: Results are keyed based on the supplied transaction id. """ - def __init__(self, client, **kwargs): + def __init__(self, client: ModbusBaseSyncClient, **kwargs): """Initialize an instance of the ModbusTransactionManager. :param client: The client socket wrapper :param retry_on_empty: Should the client retry on empty :param retries: The number of retries to allow """ - self.tid = 0 - self.client = client + super().__init__() + self.client: ModbusBaseSyncClient = client self.backoff = kwargs.get("backoff", 0.3) self.retry_on_empty = kwargs.get("retry_on_empty", False) self.retry_on_invalid = kwargs.get("retry_on_invalid", False) self.retries = kwargs.get("retries", 3) - self.transactions = {} self._transaction_lock = RLock() - self._no_response_devices = [] + self._no_response_devices: list[int] = [] if client: self._set_adu_size() - def __iter__(self): - """Iterate over the current managed transactions. - - :returns: An iterator of the managed transactions - """ - return iter(self.transactions.keys()) - def _set_adu_size(self): """Set adu size.""" # base ADU size of modbus frame in bytes @@ -107,7 +182,7 @@ def _calculate_exception_length(self): return self.base_adu_size + 2 # Fcode(1), ExceptionCode(1) return None - def _validate_response(self, request, response, exp_resp_len, is_udp=False): + def _validate_response(self, request: ModbusRequest, response, exp_resp_len, is_udp=False): """Validate Incoming response against request. :param request: Request sent @@ -118,7 +193,10 @@ def _validate_response(self, request, response, exp_resp_len, is_udp=False): if not response: return False - mbap = self.client.framer.decode_data(response) + if hasattr(self.client.framer, "decode_data"): + mbap = self.client.framer.decode_data(response) + else: + mbap = {} if ( mbap.get("slave") != request.slave_id or mbap.get("fcode") & 0x7F != request.function_code @@ -129,7 +207,7 @@ def _validate_response(self, request, response, exp_resp_len, is_udp=False): return mbap.get("length") == exp_resp_len return True - def execute(self, request): # noqa: C901 + def execute(self, request: ModbusRequest): # noqa: C901 """Start the producer to send the next request to consumer.write(Frame(request)).""" with self._transaction_lock: try: @@ -167,9 +245,8 @@ def execute(self, request): # noqa: C901 full = True else: full = False - c_str = str(self.client) is_udp = False - if "modbusudpclient" in c_str.lower().strip(): + if self.client.comm_params.comm_type == CommType.UDP: is_udp = True full = True if not expected_response_length: @@ -221,13 +298,9 @@ def execute(self, request): # noqa: C901 break # full = False Log.debug("Retry getting response: - {}", _buffer) - addTransaction = partial( # pylint: disable=invalid-name - self.addTransaction, - tid=request.transaction_id, - ) self.client.framer.processIncomingPacket( response, - addTransaction, + self.addTransaction, request.slave_id, tid=request.transaction_id, ) @@ -278,7 +351,7 @@ def _retry_transaction(self, retries, reason, packet, response_length, full=Fals return result, None return self._transact(packet, response_length, full=full) - def _transact(self, packet, response_length, full=False, broadcast=False): + def _transact(self, request: ModbusRequest, response_length, full=False, broadcast=False): """Do a Write and Read transaction. :param packet: packet to be sent @@ -291,7 +364,7 @@ def _transact(self, packet, response_length, full=False, broadcast=False): last_exception = None try: self.client.connect() - packet = self.client.framer.buildPacket(packet) + packet = self.client.framer.buildPacket(request) Log.debug("SEND: {}", packet, ":hex") size = self._send(packet) if ( @@ -331,11 +404,11 @@ def _transact(self, packet, response_length, full=False, broadcast=False): result = b"" return result, last_exception - def _send(self, packet, _retrying=False): + def _send(self, packet: bytes, _retrying=False): """Send.""" return self.client.framer.sendPacket(packet) - def _recv(self, expected_response_length, full): # noqa: C901 + def _recv(self, expected_response_length, full) -> bytes: # noqa: C901 """Receive.""" total = None if not full: @@ -419,7 +492,7 @@ def _recv(self, expected_response_length, full): # noqa: C901 self.client.state = ModbusTransactionState.PROCESSING_REPLY return result - def _get_expected_response_length(self, data): + def _get_expected_response_length(self, data) -> int: """Get the expected response length. :param data: Message data read so far @@ -429,60 +502,3 @@ def _get_expected_response_length(self, data): func_code = int(data[1]) pdu_class = self.client.framer.decoder.lookupPduClass(func_code) return pdu_class.calculateRtuFrameSize(data) - - def addTransaction(self, request, tid=None): - """Add a transaction to the handler. - - This holds the request in case it needs to be resent. - After being sent, the request is removed. - - :param request: The request to hold on to - :param tid: The overloaded transaction id to use - """ - tid = tid if tid is not None else request.transaction_id - Log.debug("Adding transaction {}", tid) - self.transactions[tid] = request - - def getTransaction(self, tid): - """Return a transaction matching the referenced tid. - - If the transaction does not exist, None is returned - - :param tid: The transaction to retrieve - - """ - Log.debug("Getting transaction {}", tid) - if not tid: - if self.transactions: - ret = self.transactions.popitem()[1] - self.transactions.clear() - return ret - return None - return self.transactions.pop(tid, None) - - def delTransaction(self, tid): - """Remove a transaction matching the referenced tid. - - :param tid: The transaction to remove - """ - Log.debug("deleting transaction {}", tid) - self.transactions.pop(tid, None) - - def getNextTID(self): - """Retrieve the next unique transaction identifier. - - This handles incrementing the identifier after - retrieval - - :returns: The next unique transaction identifier - """ - if self.tid < 65000: - self.tid += 1 - else: - self.tid = 1 - return self.tid - - def reset(self): - """Reset the transaction identifier.""" - self.tid = 0 - self.transactions = {} diff --git a/pymodbus/transport/transport.py b/pymodbus/transport/transport.py index 265d98e1d..ee0cf64f5 100644 --- a/pymodbus/transport/transport.py +++ b/pymodbus/transport/transport.py @@ -83,7 +83,7 @@ class CommParams: comm_type: CommType | None = None reconnect_delay: float | None = None reconnect_delay_max: float = 0.0 - timeout_connect: float | None = None + timeout_connect: float = 0.0 host: str = "localhost" # On some machines this will now be ::1 port: int = 0 source_address: tuple[str, int] | None = None diff --git a/test/framers/test_tbc_transaction.py b/test/framers/test_tbc_transaction.py index b072909f8..2dd0405dc 100755 --- a/test/framers/test_tbc_transaction.py +++ b/test/framers/test_tbc_transaction.py @@ -12,7 +12,7 @@ ModbusRtuFramer, ModbusSocketFramer, ModbusTlsFramer, - ModbusTransactionManager, + SyncModbusTransactionManager, ) @@ -42,7 +42,7 @@ def setup_method(self): self._tls = ModbusTlsFramer(decoder=self.decoder, client=None) self._rtu = ModbusRtuFramer(decoder=self.decoder, client=None) self._ascii = ModbusAsciiFramer(decoder=self.decoder, client=None) - self._manager = ModbusTransactionManager(self.client) + self._manager = SyncModbusTransactionManager(self.client) # ----------------------------------------------------------------------- # # Modbus transaction manager @@ -113,7 +113,7 @@ def test_execute(self, mock_time): request.get_response_pdu_size.return_value = 10 request.slave_id = 1 request.function_code = 222 - trans = ModbusTransactionManager(client) + trans = SyncModbusTransactionManager(client) trans._recv = mock.MagicMock( # pylint: disable=protected-access return_value=b"abcdef" ) diff --git a/test/sub_client/test_client.py b/test/sub_client/test_client.py index 04ea7828f..a6f07fe04 100755 --- a/test/sub_client/test_client.py +++ b/test/sub_client/test_client.py @@ -262,7 +262,7 @@ async def test_client_instanciate( client.connect = lambda: False client.transport = None with pytest.raises(ConnectionException): - client.execute() + client.execute(ModbusRequest()) async def test_serial_not_installed(): """Try to instantiate clients.""" @@ -371,7 +371,7 @@ async def test_client_protocol_handler(): reply.transaction_id = 0x00 base.ctx._handle_response(None) # pylint: disable=protected-access base.ctx._handle_response(reply) # pylint: disable=protected-access - response = base.build_response(0x00) # pylint: disable=protected-access + response = base.build_response(reply) # pylint: disable=protected-access base.ctx._handle_response(reply) # pylint: disable=protected-access result = response.result() assert result == reply @@ -647,7 +647,7 @@ async def test_client_build_response(): """Test fail of build_response.""" client = ModbusBaseClient(FramerType.RTU) with pytest.raises(ConnectionException): - await client.build_response(0) + await client.build_response(ModbusRequest(transaction=0)) async def test_client_mixin_execute(): diff --git a/test/test_transaction.py b/test/test_transaction.py index b072909f8..2dd0405dc 100755 --- a/test/test_transaction.py +++ b/test/test_transaction.py @@ -12,7 +12,7 @@ ModbusRtuFramer, ModbusSocketFramer, ModbusTlsFramer, - ModbusTransactionManager, + SyncModbusTransactionManager, ) @@ -42,7 +42,7 @@ def setup_method(self): self._tls = ModbusTlsFramer(decoder=self.decoder, client=None) self._rtu = ModbusRtuFramer(decoder=self.decoder, client=None) self._ascii = ModbusAsciiFramer(decoder=self.decoder, client=None) - self._manager = ModbusTransactionManager(self.client) + self._manager = SyncModbusTransactionManager(self.client) # ----------------------------------------------------------------------- # # Modbus transaction manager @@ -113,7 +113,7 @@ def test_execute(self, mock_time): request.get_response_pdu_size.return_value = 10 request.slave_id = 1 request.function_code = 222 - trans = ModbusTransactionManager(client) + trans = SyncModbusTransactionManager(client) trans._recv = mock.MagicMock( # pylint: disable=protected-access return_value=b"abcdef" ) From 97250ae548cc2a680f37b67d8041c4f0ceb45dfa Mon Sep 17 00:00:00 2001 From: jan iversen Date: Thu, 18 Jul 2024 12:39:07 +0200 Subject: [PATCH 64/88] Simulator config, kwargs -> parameters. (#2235) --- API_changes.rst | 1 + doc/source/library/simulator/config.rst | 4 +-- pymodbus/datastore/simulator.py | 48 ++++++++++++------------- pymodbus/server/simulator/setup.json | 12 +++---- test/sub_server/test_simulator.py | 18 +++++----- 5 files changed, 42 insertions(+), 41 deletions(-) diff --git a/API_changes.rst b/API_changes.rst index 8c3fd1de8..bcbd9dfb8 100644 --- a/API_changes.rst +++ b/API_changes.rst @@ -12,6 +12,7 @@ API changes 3.7.0 - binary framer no longer supported - Framer. renamed to FramerType. - PDU classes moved to pymodbus/pdu +- Simulator config custom actions kwargs -> parameters API changes 3.6.0 diff --git a/doc/source/library/simulator/config.rst b/doc/source/library/simulator/config.rst index 700f9a89f..99766f2e0 100644 --- a/doc/source/library/simulator/config.rst +++ b/doc/source/library/simulator/config.rst @@ -288,8 +288,8 @@ In case of **"increment"**, the counter is reset to the minimum value, if the ma .. code-block:: - {"addr": 9, "value": 7, "action": "random", "kwargs": {"minval": 0, "maxval": 12} }, - {"addr": 10, "value": 100, "action": "increment", "kwargs": {"minval": 50} } + {"addr": 9, "value": 7, "action": "random", "parameters": {"minval": 0, "maxval": 12} }, + {"addr": 10, "value": 100, "action": "increment", "parameters": {"minval": 50} } Invalid section diff --git a/pymodbus/datastore/simulator.py b/pymodbus/datastore/simulator.py index 31b61d856..69ecb2a26 100644 --- a/pymodbus/datastore/simulator.py +++ b/pymodbus/datastore/simulator.py @@ -35,7 +35,7 @@ class Cell: access: bool = False value: int = 0 action: int = 0 - action_kwargs: dict[str, Any] | None = None + action_parameters: dict[str, Any] | None = None count_read: int = 0 count_write: int = 0 @@ -47,7 +47,7 @@ class TextCell: # pylint: disable=too-few-public-methods access: str value: str action: str - action_kwargs: str + action_parameters: str count_read: str count_write: str @@ -69,7 +69,7 @@ class Label: # pylint: disable=too-many-instance-attributes increment: str = "increment" invalid: str = "invalid" ir_size: str = "ir size" - kwargs: str = "kwargs" + parameters: str = "parameters" method: str = "method" next: str = "next" none: str = "none" @@ -148,7 +148,7 @@ def __init__(self, runtime): }, } - def handle_type_bits(self, start, stop, value, action, action_kwargs): + def handle_type_bits(self, start, stop, value, action, action_parameters): """Handle type bits.""" for reg in self.runtime.registers[start:stop]: if reg.type != CellType.INVALID: @@ -156,9 +156,9 @@ def handle_type_bits(self, start, stop, value, action, action_kwargs): reg.value = value reg.type = CellType.BITS reg.action = action - reg.action_kwargs = action_kwargs + reg.action_parameters = action_parameters - def handle_type_uint16(self, start, stop, value, action, action_kwargs): + def handle_type_uint16(self, start, stop, value, action, action_parameters): """Handle type uint16.""" for reg in self.runtime.registers[start:stop]: if reg.type != CellType.INVALID: @@ -166,9 +166,9 @@ def handle_type_uint16(self, start, stop, value, action, action_kwargs): reg.value = value reg.type = CellType.UINT16 reg.action = action - reg.action_kwargs = action_kwargs + reg.action_parameters = action_parameters - def handle_type_uint32(self, start, stop, value, action, action_kwargs): + def handle_type_uint32(self, start, stop, value, action, action_parameters): """Handle type uint32.""" regs_value = ModbusSimulatorContext.build_registers_from_value(value, True) for i in range(start, stop, 2): @@ -178,11 +178,11 @@ def handle_type_uint32(self, start, stop, value, action, action_kwargs): regs[0].value = regs_value[0] regs[0].type = CellType.UINT32 regs[0].action = action - regs[0].action_kwargs = action_kwargs + regs[0].action_parameters = action_parameters regs[1].value = regs_value[1] regs[1].type = CellType.NEXT - def handle_type_float32(self, start, stop, value, action, action_kwargs): + def handle_type_float32(self, start, stop, value, action, action_parameters): """Handle type uint32.""" regs_value = ModbusSimulatorContext.build_registers_from_value(value, False) for i in range(start, stop, 2): @@ -192,11 +192,11 @@ def handle_type_float32(self, start, stop, value, action, action_kwargs): regs[0].value = regs_value[0] regs[0].type = CellType.FLOAT32 regs[0].action = action - regs[0].action_kwargs = action_kwargs + regs[0].action_parameters = action_parameters regs[1].value = regs_value[1] regs[1].type = CellType.NEXT - def handle_type_string(self, start, stop, value, action, action_kwargs): + def handle_type_string(self, start, stop, value, action, action_parameters): """Handle type string.""" regs = stop - start reg_len = regs * 2 @@ -214,7 +214,7 @@ def handle_type_string(self, start, stop, value, action, action_kwargs): reg.type = CellType.NEXT self.runtime.registers[start].type = CellType.STRING self.runtime.registers[start].action = action - self.runtime.registers[start].action_kwargs = action_kwargs + self.runtime.registers[start].action_parameters = action_parameters def handle_setup_section(self): """Load setup section.""" @@ -305,7 +305,7 @@ def handle_types(self): self.runtime.action_name_to_id[ entry.get(Label.action, type_entry[Label.action]) ], - entry.get(Label.kwargs, None), + entry.get(Label.parameters, None), ) del self.config[section] @@ -440,7 +440,7 @@ class ModbusSimulatorContext(ModbusBaseSlaveContext): {"addr": [32, 34], "value": 0xF1}, --> with value {"addr": [35, 36], "action": "increment"}, --> with action {"addr": [37, 38], "action": "increment", "value": 0xF1} --> with action and value - {"addr": [37, 38], "action": "increment", "kwargs": {"min": 0, "max": 100}} --> with action with arguments + {"addr": [37, 38], "action": "increment", "parameters": {"min": 0, "max": 100}} --> with action with arguments ], "uint16": [ --> Define uint16 (1 register == 2 bytes) --> same as type_bits @@ -495,8 +495,8 @@ def get_text_register(self, register): text_cell.count_read = str(reg.count_read) text_cell.count_write = str(reg.count_write) text_cell.action = self.action_id_to_name[reg.action] - if reg.action_kwargs: - text_cell.action = f"{text_cell.action}({reg.action_kwargs})" + if reg.action_parameters: + text_cell.action = f"{text_cell.action}({reg.action_parameters})" if reg.type in (CellType.INVALID, CellType.UINT16, CellType.NEXT): text_cell.value = str(reg.value) build_len = 0 @@ -589,9 +589,9 @@ def getValues(self, func_code, address, count=1): real_address = self.fc_offset[func_code] + address for i in range(real_address, real_address + count): reg = self.registers[i] - kwargs = reg.action_kwargs if reg.action_kwargs else {} + parameters = reg.action_parameters if reg.action_parameters else {} if reg.action: - self.action_methods[reg.action](self.registers, i, reg, **kwargs) + self.action_methods[reg.action](self.registers, i, reg, **parameters) self.registers[i].count_read += 1 result.append(reg.value) else: @@ -602,9 +602,9 @@ def getValues(self, func_code, address, count=1): for i in range(real_address, real_address + reg_count): reg = self.registers[i] if reg.action: - kwargs = reg.action_kwargs or {} + parameters = reg.action_parameters or {} self.action_methods[reg.action]( - self.registers, i, reg, **kwargs + self.registers, i, reg, **parameters ) self.registers[i].count_read += 1 while count and bit_index < 16: @@ -707,7 +707,7 @@ def action_increment(cls, registers, inx, cell, minval=None, maxval=None): reg2.value = new_regs[1] @classmethod - def action_timestamp(cls, registers, inx, _cell, **_kwargs): + def action_timestamp(cls, registers, inx, _cell, **_parameters): """Set current time. :meta private: @@ -722,7 +722,7 @@ def action_timestamp(cls, registers, inx, _cell, **_kwargs): registers[inx + 6].value = system_time.second @classmethod - def action_reset(cls, _registers, _inx, _cell, **_kwargs): + def action_reset(cls, _registers, _inx, _cell, **_parameters): """Reboot server. :meta private: @@ -730,7 +730,7 @@ def action_reset(cls, _registers, _inx, _cell, **_kwargs): raise RuntimeError("RESET server") @classmethod - def action_uptime(cls, registers, inx, cell, **_kwargs): + def action_uptime(cls, registers, inx, cell, **_parameters): """Return uptime in seconds. :meta private: diff --git a/pymodbus/server/simulator/setup.json b/pymodbus/server/simulator/setup.json index ea3213b62..94c63f4fb 100644 --- a/pymodbus/server/simulator/setup.json +++ b/pymodbus/server/simulator/setup.json @@ -171,12 +171,12 @@ {"addr": 2305, "value": 50, "action": "increment", - "kwargs": {"minval": 45, "maxval": 155} + "parameters": {"minval": 45, "maxval": 155} }, {"addr": 2306, "value": 50, "action": "random", - "kwargs": {"minval": 45, "maxval": 55} + "parameters": {"minval": 45, "maxval": 55} } ], "uint32": [ @@ -190,12 +190,12 @@ {"addr": [3876, 3877], "value": 50000, "action": "increment", - "kwargs": {"minval": 45000, "maxval": 55000} + "parameters": {"minval": 45000, "maxval": 55000} }, {"addr": [3878, 3879], "value": 50000, "action": "random", - "kwargs": {"minval": 45000, "maxval": 55000} + "parameters": {"minval": 45000, "maxval": 55000} } ], "float32": [ @@ -209,12 +209,12 @@ {"addr": [4876, 4877], "value": 50000.0, "action": "increment", - "kwargs": {"minval": 45000.0, "maxval": 55000.0} + "parameters": {"minval": 45000.0, "maxval": 55000.0} }, {"addr": [4878, 48779], "value": 50000.0, "action": "random", - "kwargs": {"minval": 45000.0, "maxval": 55000.0} + "parameters": {"minval": 45000.0, "maxval": 55000.0} } ], "string": [ diff --git a/test/sub_server/test_simulator.py b/test/sub_server/test_simulator.py index aeefed8b7..a870d942e 100644 --- a/test/sub_server/test_simulator.py +++ b/test/sub_server/test_simulator.py @@ -85,7 +85,7 @@ class TestSimulator: "addr": [31, 32], "value": 50, "action": "random", - "kwargs": {"minval": 10, "maxval": 80}, + "parameters": {"minval": 10, "maxval": 80}, }, ], "float32": [ @@ -151,7 +151,7 @@ class TestSimulator: Cell(type=CellType.UINT32, value=5, action=1), Cell(type=CellType.NEXT, value=17320), # 30 Cell( - type=CellType.UINT32, action=2, action_kwargs={"minval": 10, "maxval": 80} + type=CellType.UINT32, action=2, action_parameters={"minval": 10, "maxval": 80} ), Cell(type=CellType.NEXT, value=50), Cell(type=CellType.FLOAT32, access=True, value=17731), @@ -215,7 +215,7 @@ def test_simulator_config_verify(self): assert reg.value == test_cell.value, f"at index {i} - {offset}" assert reg.action == test_cell.action, f"at index {i} - {offset}" assert ( - reg.action_kwargs == test_cell.action_kwargs + reg.action_parameters == test_cell.action_parameters ), f"at index {i} - {offset}" assert ( reg.count_read == test_cell.count_read @@ -404,10 +404,10 @@ def test_simulator_set_values(self): exc_simulator.setValues(FX_WRITE_BIT, 84, [True]) exc_simulator.setValues(FX_WRITE_BIT, 86, [True, False, True]) result = exc_simulator.getValues(FX_READ_BIT, 80, 8) - assert [True, False] * 4 == result + assert result == [True, False] * 4 exc_simulator.setValues(FX_WRITE_BIT, 88, [False]) result = exc_simulator.getValues(FX_READ_BIT, 86, 3) - assert [True, False, False] == result + assert result == [True, False, False] exc_simulator.setValues(FX_WRITE_BIT, 80, [True] * 17) def test_simulator_get_text(self): @@ -509,13 +509,13 @@ def test_simulator_action_increment( exc_setup = copy.deepcopy(self.default_config) exc_simulator = ModbusSimulatorContext(exc_setup, None) action = exc_simulator.action_name_to_id[Label.increment] - kwargs = { + parameters = { "minval": minval, "maxval": maxval, } exc_simulator.registers[30].type = celltype exc_simulator.registers[30].action = action - exc_simulator.registers[30].action_kwargs = kwargs + exc_simulator.registers[30].action_parameters = parameters exc_simulator.registers[31].type = CellType.NEXT is_int = celltype != CellType.FLOAT32 @@ -557,13 +557,13 @@ def test_simulator_action_random(self, celltype, minval, maxval): exc_setup = copy.deepcopy(self.default_config) exc_simulator = ModbusSimulatorContext(exc_setup, None) action = exc_simulator.action_name_to_id[Label.random] - kwargs = { + parameters = { "minval": minval, "maxval": maxval, } exc_simulator.registers[30].type = celltype exc_simulator.registers[30].action = action - exc_simulator.registers[30].action_kwargs = kwargs + exc_simulator.registers[30].action_parameters = parameters exc_simulator.registers[31].type = CellType.NEXT is_int = celltype != CellType.FLOAT32 reg_count = 1 if celltype in (CellType.BITS, CellType.UINT16) else 2 From 0e2c7e96c2bf447aae2eddad823694b4e2a73e87 Mon Sep 17 00:00:00 2001 From: jan iversen Date: Thu, 18 Jul 2024 14:53:43 +0200 Subject: [PATCH 65/88] Remove kwargs. (#2236) --- examples/contrib/redis_datastore.py | 9 ++++---- examples/contrib/sql_datastore.py | 9 ++++---- pymodbus/datastore/context.py | 17 +++++++++----- pymodbus/datastore/remote.py | 28 +++++++++++------------ pymodbus/device.py | 2 +- pymodbus/events.py | 22 +++++++++--------- pymodbus/framer/old_framer_ascii.py | 4 ++-- pymodbus/framer/old_framer_base.py | 10 ++++----- pymodbus/framer/old_framer_rtu.py | 2 +- pymodbus/framer/old_framer_socket.py | 2 +- pymodbus/framer/old_framer_tls.py | 2 +- pymodbus/transaction.py | 11 ++++----- pymodbus/transport/serialtransport.py | 20 +++++++++++++---- pymodbus/transport/transport.py | 1 - test/transport/test_serial.py | 32 +++++++++++++-------------- 15 files changed, 94 insertions(+), 77 deletions(-) diff --git a/examples/contrib/redis_datastore.py b/examples/contrib/redis_datastore.py index 14d96d633..ca974426f 100644 --- a/examples/contrib/redis_datastore.py +++ b/examples/contrib/redis_datastore.py @@ -17,17 +17,16 @@ class RedisSlaveContext(ModbusBaseSlaveContext): """This is a modbus slave context using redis as a backing store.""" - def __init__(self, **kwargs): + def __init__(self, host="localhost", port=6379, prefix="pymodbus", client=None): """Initialize the datastores. :param host: The host to connect to :param port: The port to connect to :param prefix: A prefix for the keys + :param client: redis client """ - host = kwargs.get("host", "localhost") - port = kwargs.get("port", 6379) - self.prefix = kwargs.get("prefix", "pymodbus") - self.client = kwargs.get("client", redis.Redis(host=host, port=port)) + self.prefix = prefix + self.client = client if client else redis.Redis(host=host, port=port) self._build_mapping() def __str__(self): diff --git a/examples/contrib/sql_datastore.py b/examples/contrib/sql_datastore.py index b882b7f11..2d6387a8b 100644 --- a/examples/contrib/sql_datastore.py +++ b/examples/contrib/sql_datastore.py @@ -20,17 +20,18 @@ class SqlSlaveContext(ModbusBaseSlaveContext): """This creates a modbus data model with each data access in its a block.""" - def __init__(self, *_args, **kwargs): + def __init__(self, *_args, table="pymodbus", database=None): """Initialize the datastores. - :param kwargs: Each element is a ModbusDataBlock + :param table: table name + :param database: database """ self._engine = None self._metadata = None self._table = None self._connection = None - self.table = kwargs.get("table", "pymodbus") - self.database = kwargs.get("database", "sqlite:///:memory:") + self.table = table + self.database = database if database else "sqlite:///:memory:" self._db_create(self.table, self.database) def __str__(self): diff --git a/pymodbus/datastore/context.py b/pymodbus/datastore/context.py index d2296a270..ea76b7ffa 100644 --- a/pymodbus/datastore/context.py +++ b/pymodbus/datastore/context.py @@ -92,14 +92,19 @@ class ModbusSlaveContext(ModbusBaseSlaveContext): """ - def __init__(self, *_args, **kwargs): + def __init__(self, *_args, + di=ModbusSequentialDataBlock.create(), + co=ModbusSequentialDataBlock.create(), + ir=ModbusSequentialDataBlock.create(), + hr=ModbusSequentialDataBlock.create(), + zero_mode=False): """Initialize the datastores.""" self.store = {} - self.store["d"] = kwargs.get("di", ModbusSequentialDataBlock.create()) - self.store["c"] = kwargs.get("co", ModbusSequentialDataBlock.create()) - self.store["i"] = kwargs.get("ir", ModbusSequentialDataBlock.create()) - self.store["h"] = kwargs.get("hr", ModbusSequentialDataBlock.create()) - self.zero_mode = kwargs.get("zero_mode", False) + self.store["d"] = di + self.store["c"] = co + self.store["i"] = ir + self.store["h"] = hr + self.zero_mode = zero_mode def __str__(self): """Return a string representation of the context. diff --git a/pymodbus/datastore/remote.py b/pymodbus/datastore/remote.py index 919da6c50..85452be51 100644 --- a/pymodbus/datastore/remote.py +++ b/pymodbus/datastore/remote.py @@ -69,47 +69,47 @@ def __str__(self): def __build_mapping(self): """Build the function code mapper.""" - kwargs = {} + params = {} if self.slave: - kwargs["slave"] = self.slave + params["slave"] = self.slave self.__get_callbacks = { "d": lambda a, c: self._client.read_discrete_inputs( - a, c, **kwargs + a, c, **params ), "c": lambda a, c: self._client.read_coils( - a, c, **kwargs + a, c, **params ), "h": lambda a, c: self._client.read_holding_registers( - a, c, **kwargs + a, c, **params ), "i": lambda a, c: self._client.read_input_registers( - a, c, **kwargs + a, c, **params ), } self.__set_callbacks = { "d5": lambda a, v: self._client.write_coil( - a, v, **kwargs + a, v, **params ), "d15": lambda a, v: self._client.write_coils( - a, v, **kwargs + a, v, **params ), "c5": lambda a, v: self._client.write_coil( - a, v, **kwargs + a, v, **params ), "c15": lambda a, v: self._client.write_coils( - a, v, **kwargs + a, v, **params ), "h6": lambda a, v: self._client.write_register( - a, v, **kwargs + a, v, **params ), "h16": lambda a, v: self._client.write_registers( - a, v, **kwargs + a, v, **params ), "i6": lambda a, v: self._client.write_register( - a, v, **kwargs + a, v, **params ), "i16": lambda a, v: self._client.write_registers( - a, v, **kwargs + a, v, **params ), } self._write_fc = (0x05, 0x06, 0x0F, 0x10) diff --git a/pymodbus/device.py b/pymodbus/device.py index 6ea7038f7..adcc58296 100644 --- a/pymodbus/device.py +++ b/pymodbus/device.py @@ -470,7 +470,7 @@ def __iter__(self): """ return self.__counters.__iter__() - def __new__(cls, *_args, **_kwargs): + def __new__(cls): """Create a new instance.""" if "_inst" not in vars(cls): cls._inst = object.__new__(cls) diff --git a/pymodbus/events.py b/pymodbus/events.py index fcd5abf42..848e4fa4b 100644 --- a/pymodbus/events.py +++ b/pymodbus/events.py @@ -48,11 +48,11 @@ class RemoteReceiveEvent(ModbusEvent): 7 1 """ - def __init__(self, **kwargs): + def __init__(self, overrun=False, listen=False, broadcast=False): """Initialize a new event instance.""" - self.overrun = kwargs.get("overrun", False) - self.listen = kwargs.get("listen", False) - self.broadcast = kwargs.get("broadcast", False) + self.overrun = overrun + self.listen = listen + self.broadcast = broadcast def encode(self) -> bytes: """Encode the status bits to an event message. @@ -98,14 +98,14 @@ class RemoteSendEvent(ModbusEvent): 7 0 """ - def __init__(self, **kwargs): + def __init__(self, read=False, slave_abort=False, slave_busy=False, slave_nak=False, write_timeout=False, listen=False): """Initialize a new event instance.""" - self.read = kwargs.get("read", False) - self.slave_abort = kwargs.get("slave_abort", False) - self.slave_busy = kwargs.get("slave_busy", False) - self.slave_nak = kwargs.get("slave_nak", False) - self.write_timeout = kwargs.get("write_timeout", False) - self.listen = kwargs.get("listen", False) + self.read = read + self.slave_abort = slave_abort + self.slave_busy = slave_busy + self.slave_nak = slave_nak + self.write_timeout = write_timeout + self.listen = listen def encode(self): """Encode the status bits to an event message. diff --git a/pymodbus/framer/old_framer_ascii.py b/pymodbus/framer/old_framer_ascii.py index 7dc0cf4ed..1773e4592 100644 --- a/pymodbus/framer/old_framer_ascii.py +++ b/pymodbus/framer/old_framer_ascii.py @@ -48,10 +48,10 @@ def decode_data(self, data): return {"slave": uid, "fcode": fcode} return {} - def frameProcessIncomingPacket(self, single, callback, slave, _tid=None, **kwargs): + def frameProcessIncomingPacket(self, single, callback, slave, tid=None): """Process new packet pattern.""" while len(self._buffer): - used_len, _tid, dev_id, data = self.message_handler.decode(self._buffer) + used_len, tid, dev_id, data = self.message_handler.decode(self._buffer) if not data: if not used_len: return diff --git a/pymodbus/framer/old_framer_base.py b/pymodbus/framer/old_framer_base.py index 8f5a9be2a..4982d27a9 100644 --- a/pymodbus/framer/old_framer_base.py +++ b/pymodbus/framer/old_framer_base.py @@ -125,7 +125,7 @@ def populateResult(self, result): result.transaction_id = self._header.get("tid", 0) result.protocol_id = self._header.get("pid", 0) - def processIncomingPacket(self, data: bytes, callback, slave, **kwargs): + def processIncomingPacket(self, data: bytes, callback, slave, single=False, tid=None): """Process new packet pattern. This takes in a new request packet, adds it to the current @@ -141,7 +141,8 @@ def processIncomingPacket(self, data: bytes, callback, slave, **kwargs): :param callback: The function to send results to :param slave: Process if slave id matches, ignore otherwise (could be a list of slave ids (server) or single slave id(client/server)) - :param kwargs: + :param single: multiple slave ? + :param tid: transaction id :raises ModbusIOException: """ Log.debug("Processing: {}", data, ":hex") @@ -150,11 +151,10 @@ def processIncomingPacket(self, data: bytes, callback, slave, **kwargs): return if not isinstance(slave, (list, tuple)): slave = [slave] - single = kwargs.pop("single", False) - self.frameProcessIncomingPacket(single, callback, slave, **kwargs) + self.frameProcessIncomingPacket(single, callback, slave, tid=tid) def frameProcessIncomingPacket( - self, _single, _callback, _slave, _tid=None, **kwargs + self, _single, _callback, _slave, tid=None ) -> None: """Process new packet pattern.""" diff --git a/pymodbus/framer/old_framer_rtu.py b/pymodbus/framer/old_framer_rtu.py index ccd293352..88478b0bd 100644 --- a/pymodbus/framer/old_framer_rtu.py +++ b/pymodbus/framer/old_framer_rtu.py @@ -70,7 +70,7 @@ def decode_data(self, data): return {} - def frameProcessIncomingPacket(self, _single, callback, slave, _tid=None, **kwargs): # noqa: C901 + def frameProcessIncomingPacket(self, _single, callback, slave, tid=None): # noqa: C901 """Process new packet pattern.""" def is_frame_ready(self): diff --git a/pymodbus/framer/old_framer_socket.py b/pymodbus/framer/old_framer_socket.py index e812fdda9..65bc9ba4e 100644 --- a/pymodbus/framer/old_framer_socket.py +++ b/pymodbus/framer/old_framer_socket.py @@ -59,7 +59,7 @@ def decode_data(self, data): } return {} - def frameProcessIncomingPacket(self, single, callback, slave, tid=None, **kwargs): + def frameProcessIncomingPacket(self, single, callback, slave, tid=None): """Process new packet pattern. This takes in a new request packet, adds it to the current diff --git a/pymodbus/framer/old_framer_tls.py b/pymodbus/framer/old_framer_tls.py index 71e0a5a9d..c34470f4e 100644 --- a/pymodbus/framer/old_framer_tls.py +++ b/pymodbus/framer/old_framer_tls.py @@ -48,7 +48,7 @@ def recvPacket(self, size): sleep(0.5) return super().recvPacket(size) - def frameProcessIncomingPacket(self, _single, callback, _slave, _tid=None, **kwargs): + def frameProcessIncomingPacket(self, _single, callback, _slave, tid=None): """Process new packet pattern.""" # no slave id for Modbus Security Application Protocol diff --git a/pymodbus/transaction.py b/pymodbus/transaction.py index 914edb7bb..bbb187efe 100644 --- a/pymodbus/transaction.py +++ b/pymodbus/transaction.py @@ -134,19 +134,20 @@ class SyncModbusTransactionManager(ModbusTransactionManager): Results are keyed based on the supplied transaction id. """ - def __init__(self, client: ModbusBaseSyncClient, **kwargs): + def __init__(self, client: ModbusBaseSyncClient, backoff=0.3, retry_on_empty=False, retry_on_invalid=False, retries=3, **_kwargs): """Initialize an instance of the ModbusTransactionManager. :param client: The client socket wrapper + :param backoff: 0.3 :param retry_on_empty: Should the client retry on empty :param retries: The number of retries to allow """ super().__init__() self.client: ModbusBaseSyncClient = client - self.backoff = kwargs.get("backoff", 0.3) - self.retry_on_empty = kwargs.get("retry_on_empty", False) - self.retry_on_invalid = kwargs.get("retry_on_invalid", False) - self.retries = kwargs.get("retries", 3) + self.backoff = backoff + self.retry_on_empty = retry_on_empty + self.retry_on_invalid = retry_on_invalid + self.retries = retries self._transaction_lock = RLock() self._no_response_devices: list[int] = [] if client: diff --git a/pymodbus/transport/serialtransport.py b/pymodbus/transport/serialtransport.py index 6d632fcae..111116b1b 100644 --- a/pymodbus/transport/serialtransport.py +++ b/pymodbus/transport/serialtransport.py @@ -15,12 +15,14 @@ class SerialTransport(asyncio.Transport): force_poll: bool = os.name == "nt" - def __init__(self, loop, protocol, *args, **kwargs) -> None: + def __init__(self, loop, protocol, url, baudrate, bytesize, parity, stopbits, timeout) -> None: """Initialize.""" super().__init__() self.async_loop = loop self.intern_protocol: asyncio.BaseProtocol = protocol - self.sync_serial = serial.serial_for_url(*args, **kwargs) + self.sync_serial = serial.serial_for_url(url, exclusive=True, + baudrate=baudrate, bytesize=bytesize, parity=parity, stopbits=stopbits, timeout=timeout +) self.intern_write_buffer: list[bytes] = [] self.poll_task: asyncio.Task | None = None self._poll_wait_time = 0.0005 @@ -154,10 +156,20 @@ async def polling_task(self): self.intern_read_ready() async def create_serial_connection( - loop, protocol_factory, *args, **kwargs + loop, protocol_factory, url, + baudrate=None, + bytesize=None, + parity=None, + stopbits=None, + timeout=None, ) -> tuple[asyncio.Transport, asyncio.BaseProtocol]: """Create a connection to a new serial port instance.""" protocol = protocol_factory() - transport = SerialTransport(loop, protocol, *args, **kwargs) + transport = SerialTransport(loop, protocol, url, + baudrate, + bytesize, + parity, + stopbits, + timeout) loop.call_soon(transport.setup) return transport, protocol diff --git a/pymodbus/transport/transport.py b/pymodbus/transport/transport.py index ee0cf64f5..8ec1216e5 100644 --- a/pymodbus/transport/transport.py +++ b/pymodbus/transport/transport.py @@ -202,7 +202,6 @@ def init_setup_connect_listen(self, host: str, port: int) -> None: parity=self.comm_params.parity, stopbits=self.comm_params.stopbits, timeout=self.comm_params.timeout_connect, - exclusive=True, ) return if self.comm_params.comm_type == CommType.UDP: diff --git a/test/transport/test_serial.py b/test/transport/test_serial.py index c39e11e4c..f40eb805d 100644 --- a/test/transport/test_serial.py +++ b/test/transport/test_serial.py @@ -22,17 +22,17 @@ class TestTransportSerial: async def test_init(self): """Test null modem init.""" - SerialTransport(asyncio.get_running_loop(), mock.Mock(), "dummy") + SerialTransport(asyncio.get_running_loop(), mock.Mock(), "dummy", None, None, None, None, None) async def test_loop(self): """Test asyncio abstract methods.""" - comm = SerialTransport(asyncio.get_running_loop(), mock.Mock(), "dummy") + comm = SerialTransport(asyncio.get_running_loop(), mock.Mock(), "dummy", None, None, None, None, None) assert comm.loop @pytest.mark.parametrize("inx", range(0, 11)) async def test_abstract_methods(self, inx): """Test asyncio abstract methods.""" - comm = SerialTransport(asyncio.get_running_loop(), mock.Mock(), "dummy") + comm = SerialTransport(asyncio.get_running_loop(), mock.Mock(), "dummy", None, None, None, None, None) methods = [ partial(comm.get_protocol), partial(comm.set_protocol, None), @@ -51,7 +51,7 @@ async def test_abstract_methods(self, inx): @pytest.mark.parametrize("inx", range(0, 4)) async def test_external_methods(self, inx): """Test external methods.""" - comm = SerialTransport(mock.MagicMock(), mock.Mock(), "dummy") + comm = SerialTransport(mock.MagicMock(), mock.Mock(), "dummy", None, None, None, None, None) comm.sync_serial.read = mock.MagicMock(return_value="abcd") comm.sync_serial.write = mock.MagicMock(return_value=4) comm.sync_serial.fileno = mock.MagicMock(return_value=2) @@ -73,7 +73,7 @@ async def test_external_methods(self, inx): async def test_create_serial(self): """Test external methods.""" transport, protocol = await create_serial_connection( - asyncio.get_running_loop(), mock.Mock, url="dummy" + asyncio.get_running_loop(), mock.Mock, "dummy" ) assert transport assert protocol @@ -83,7 +83,7 @@ async def test_force_poll(self): """Test external methods.""" SerialTransport.force_poll = True transport, protocol = await create_serial_connection( - asyncio.get_running_loop(), mock.Mock, url="dummy" + asyncio.get_running_loop(), mock.Mock, "dummy" ) await asyncio.sleep(0) assert transport @@ -96,7 +96,7 @@ async def test_write_force_poll(self): """Test write with poll.""" SerialTransport.force_poll = True transport, protocol = await create_serial_connection( - asyncio.get_running_loop(), mock.Mock, url="dummy" + asyncio.get_running_loop(), mock.Mock, "dummy" ) await asyncio.sleep(0) transport.write(b"abcd") @@ -106,14 +106,14 @@ async def test_write_force_poll(self): async def test_close(self): """Test close.""" - comm = SerialTransport(asyncio.get_running_loop(), mock.Mock(), "dummy") + comm = SerialTransport(asyncio.get_running_loop(), mock.Mock(), "dummy", None, None, None, None, None) comm.sync_serial = None comm.close() @pytest.mark.skipif(os.name == "nt", reason="Windows not supported") async def test_polling(self): """Test polling.""" - comm = SerialTransport(asyncio.get_running_loop(), mock.Mock(), "dummy") + comm = SerialTransport(asyncio.get_running_loop(), mock.Mock(), "dummy", None, None, None, None, None) comm.sync_serial = mock.MagicMock() comm.sync_serial.read.side_effect = asyncio.CancelledError("test") with contextlib.suppress(asyncio.CancelledError): @@ -122,7 +122,7 @@ async def test_polling(self): @pytest.mark.skipif(os.name == "nt", reason="Windows not supported") async def test_poll_task(self): """Test polling.""" - comm = SerialTransport(asyncio.get_running_loop(), mock.Mock(), "dummy") + comm = SerialTransport(asyncio.get_running_loop(), mock.Mock(), "dummy", None, None, None, None, None) comm.sync_serial = mock.MagicMock() comm.sync_serial.read.side_effect = serial.SerialException("test") await comm.polling_task() @@ -130,7 +130,7 @@ async def test_poll_task(self): @pytest.mark.skipif(os.name == "nt", reason="Windows not supported") async def test_poll_task2(self): """Test polling.""" - comm = SerialTransport(asyncio.get_running_loop(), mock.Mock(), "dummy") + comm = SerialTransport(asyncio.get_running_loop(), mock.Mock(), "dummy", None, None, None, None, None) comm.sync_serial = mock.MagicMock() comm.sync_serial = mock.MagicMock() comm.sync_serial.write.return_value = 4 @@ -142,7 +142,7 @@ async def test_poll_task2(self): @pytest.mark.skipif(os.name == "nt", reason="Windows not supported") async def test_write_exception(self): """Test write exception.""" - comm = SerialTransport(asyncio.get_running_loop(), mock.Mock(), "dummy") + comm = SerialTransport(asyncio.get_running_loop(), mock.Mock(), "dummy", None, None, None, None, None) comm.sync_serial = mock.MagicMock() comm.sync_serial.write.side_effect = BlockingIOError("test") comm.intern_write_ready() @@ -152,7 +152,7 @@ async def test_write_exception(self): @pytest.mark.skipif(os.name == "nt", reason="Windows not supported") async def test_write_ok(self): """Test write exception.""" - comm = SerialTransport(asyncio.get_running_loop(), mock.Mock(), "dummy") + comm = SerialTransport(asyncio.get_running_loop(), mock.Mock(), "dummy", None, None, None, None, None) comm.sync_serial = mock.MagicMock() comm.sync_serial.write.return_value = 4 comm.intern_write_buffer.append(b"abcd") @@ -161,7 +161,7 @@ async def test_write_ok(self): @pytest.mark.skipif(os.name == "nt", reason="Windows not supported") async def test_write_len(self): """Test write exception.""" - comm = SerialTransport(asyncio.get_running_loop(), mock.Mock(), "dummy") + comm = SerialTransport(asyncio.get_running_loop(), mock.Mock(), "dummy", None, None, None, None, None) comm.sync_serial = mock.MagicMock() comm.sync_serial.write.return_value = 3 comm.async_loop.add_writer = mock.Mock() @@ -171,7 +171,7 @@ async def test_write_len(self): @pytest.mark.skipif(os.name == "nt", reason="Windows not supported") async def test_write_force(self): """Test write exception.""" - comm = SerialTransport(asyncio.get_running_loop(), mock.Mock(), "dummy") + comm = SerialTransport(asyncio.get_running_loop(), mock.Mock(), "dummy", None, None, None, None, None) comm.poll_task = True comm.sync_serial = mock.MagicMock() comm.sync_serial.write.return_value = 3 @@ -181,7 +181,7 @@ async def test_write_force(self): @pytest.mark.skipif(os.name == "nt", reason="Windows not supported") async def test_read_ready(self): """Test polling.""" - comm = SerialTransport(asyncio.get_running_loop(), mock.Mock(), "dummy") + comm = SerialTransport(asyncio.get_running_loop(), mock.Mock(), "dummy", None, None, None, None, None) comm.sync_serial = mock.MagicMock() comm.intern_protocol = mock.Mock() comm.sync_serial.read = mock.Mock() From 52dc16b28df783ce2541f33b210ed3e9b5d393b9 Mon Sep 17 00:00:00 2001 From: jan iversen Date: Thu, 18 Jul 2024 19:59:18 +0200 Subject: [PATCH 66/88] remove kwargs II. (#2237) --- API_changes.rst | 1 + examples/client_custom_msg.py | 8 ++--- pymodbus/client/base.py | 6 +++- pymodbus/factory.py | 2 +- pymodbus/pdu/bit_read_message.py | 24 +++++++-------- pymodbus/pdu/bit_write_message.py | 16 +++++----- pymodbus/pdu/diag_message.py | 8 ++--- pymodbus/pdu/file_message.py | 24 +++++++-------- pymodbus/pdu/mei_message.py | 8 ++--- pymodbus/pdu/other_message.py | 32 ++++++++++---------- pymodbus/pdu/pdu.py | 24 +++++++-------- pymodbus/pdu/register_read_message.py | 16 +++++----- pymodbus/pdu/register_write_message.py | 24 +++++++-------- pymodbus/transaction.py | 2 +- test/framers/test_tbc_transaction.py | 18 +++++------ test/sub_client/test_client.py | 8 ++--- test/sub_function_codes/test_all_messages.py | 4 +-- test/test_pdu.py | 10 +++--- test/test_transaction.py | 16 +++++----- 19 files changed, 128 insertions(+), 123 deletions(-) diff --git a/API_changes.rst b/API_changes.rst index bcbd9dfb8..82e913b9b 100644 --- a/API_changes.rst +++ b/API_changes.rst @@ -13,6 +13,7 @@ API changes 3.7.0 - Framer. renamed to FramerType. - PDU classes moved to pymodbus/pdu - Simulator config custom actions kwargs -> parameters +- Non defined parameters (kwargs) no longer valid API changes 3.6.0 diff --git a/examples/client_custom_msg.py b/examples/client_custom_msg.py index e4af4f4f1..e86f4793b 100755 --- a/examples/client_custom_msg.py +++ b/examples/client_custom_msg.py @@ -36,9 +36,9 @@ class CustomModbusResponse(ModbusResponse): function_code = 55 _rtu_byte_count_pos = 2 - def __init__(self, values=None, **kwargs): + def __init__(self, values=None, slave=0, transaction=0, protocol=0, skip_encode=False, **_kwargs): """Initialize.""" - ModbusResponse.__init__(self, **kwargs) + ModbusResponse.__init__(self, slave, transaction, protocol, skip_encode) self.values = values or [] def encode(self): @@ -68,9 +68,9 @@ class CustomModbusRequest(ModbusRequest): function_code = 55 _rtu_frame_size = 8 - def __init__(self, address=None, **kwargs): + def __init__(self, address=None, slave=0, transaction=0, protocol=0, skip_encode=False, **_kwargs): """Initialize.""" - ModbusRequest.__init__(self, **kwargs) + ModbusRequest.__init__(self, slave, transaction, protocol, skip_encode) self.address = address self.count = 16 diff --git a/pymodbus/client/base.py b/pymodbus/client/base.py index 90cbecc2d..5475546e8 100644 --- a/pymodbus/client/base.py +++ b/pymodbus/client/base.py @@ -303,7 +303,11 @@ def __init__( framer, cast(type[ModbusFramer], framer) )(ClientDecoder(), self) self.transaction = SyncModbusTransactionManager( - self, retries=retries, retry_on_empty=retry_on_empty, **kwargs + self, + kwargs.get("backoff", 0.3), + retry_on_empty, + kwargs.get("retry_on_invalid", False), + retries, ) self.reconnect_delay_current = self.params.reconnect_delay or 0 self.use_udp = False diff --git a/pymodbus/factory.py b/pymodbus/factory.py index 50302b50c..2d69e403e 100644 --- a/pymodbus/factory.py +++ b/pymodbus/factory.py @@ -120,7 +120,7 @@ def _helper(self, data: str): function_code = int(data[0]) if not (request := self.lookup.get(function_code, lambda: None)()): Log.debug("Factory Request[{}]", function_code) - request = pdu.IllegalFunctionRequest(function_code) + request = pdu.IllegalFunctionRequest(function_code, 0, 0, 0, False) else: fc_string = "{}: {}".format( # pylint: disable=consider-using-f-string str(self.lookup[function_code]) # pylint: disable=use-maxsplit-arg diff --git a/pymodbus/pdu/bit_read_message.py b/pymodbus/pdu/bit_read_message.py index 6796eeffc..c7a54e679 100644 --- a/pymodbus/pdu/bit_read_message.py +++ b/pymodbus/pdu/bit_read_message.py @@ -21,14 +21,14 @@ class ReadBitsRequestBase(ModbusRequest): _rtu_frame_size = 8 - def __init__(self, address, count, slave=0, **kwargs): + def __init__(self, address, count, slave=0, transaction=0, protocol=0, skip_encode=False, **_kwargs): """Initialize the read request data. :param address: The start address to read from :param count: The number of bits after "address" to read :param slave: Modbus slave slave ID """ - ModbusRequest.__init__(self, slave, **kwargs) + ModbusRequest.__init__(self, slave, transaction, protocol, skip_encode) self.address = address self.count = count @@ -75,13 +75,13 @@ class ReadBitsResponseBase(ModbusResponse): _rtu_byte_count_pos = 2 - def __init__(self, values, slave=0, **kwargs): + def __init__(self, values, slave=0, transaction=0, protocol=0, skip_encode=False, **_kwargs): """Initialize a new instance. :param values: The requested values to be returned :param slave: Modbus slave slave ID """ - ModbusResponse.__init__(self, slave, **kwargs) + ModbusResponse.__init__(self, slave, transaction, protocol, skip_encode) #: A list of booleans representing bit values self.bits = values or [] @@ -146,14 +146,14 @@ class ReadCoilsRequest(ReadBitsRequestBase): function_code = 1 function_code_name = "read_coils" - def __init__(self, address=None, count=None, slave=0, **kwargs): + def __init__(self, address=None, count=None, slave=0, transaction=0, protocol=0, skip_encode=False, **_kwargs): """Initialize a new instance. :param address: The address to start reading from :param count: The number of bits to read :param slave: Modbus slave slave ID """ - ReadBitsRequestBase.__init__(self, address, count, slave, **kwargs) + ReadBitsRequestBase.__init__(self, address, count, slave, transaction, protocol, skip_encode) async def execute(self, context): """Run a read coils request against a datastore. @@ -193,13 +193,13 @@ class ReadCoilsResponse(ReadBitsResponseBase): function_code = 1 - def __init__(self, values=None, slave=0, **kwargs): + def __init__(self, values=None, slave=0, transaction=0, protocol=0, skip_encode=False, **_kwargs): """Initialize a new instance. :param values: The request values to respond with :param slave: Modbus slave slave ID """ - ReadBitsResponseBase.__init__(self, values, slave, **kwargs) + ReadBitsResponseBase.__init__(self, values, slave, transaction, protocol, skip_encode) class ReadDiscreteInputsRequest(ReadBitsRequestBase): @@ -214,14 +214,14 @@ class ReadDiscreteInputsRequest(ReadBitsRequestBase): function_code = 2 function_code_name = "read_discrete_input" - def __init__(self, address=None, count=None, slave=0, **kwargs): + def __init__(self, address=None, count=None, slave=0, transaction=0, protocol=0, skip_encode=False, **_kwargs): """Initialize a new instance. :param address: The address to start reading from :param count: The number of bits to read :param slave: Modbus slave slave ID """ - ReadBitsRequestBase.__init__(self, address, count, slave, **kwargs) + ReadBitsRequestBase.__init__(self, address, count, slave, transaction, protocol, skip_encode) async def execute(self, context): """Run a read discrete input request against a datastore. @@ -261,10 +261,10 @@ class ReadDiscreteInputsResponse(ReadBitsResponseBase): function_code = 2 - def __init__(self, values=None, slave=0, **kwargs): + def __init__(self, values=None, slave=0, transaction=0, protocol=0, skip_encode=False, **_kwargs): """Initialize a new instance. :param values: The request values to respond with :param slave: Modbus slave slave ID """ - ReadBitsResponseBase.__init__(self, values, slave, **kwargs) + ReadBitsResponseBase.__init__(self, values, slave, transaction, protocol, skip_encode) diff --git a/pymodbus/pdu/bit_write_message.py b/pymodbus/pdu/bit_write_message.py index 6c4fafa27..3297299d0 100644 --- a/pymodbus/pdu/bit_write_message.py +++ b/pymodbus/pdu/bit_write_message.py @@ -49,13 +49,13 @@ class WriteSingleCoilRequest(ModbusRequest): _rtu_frame_size = 8 - def __init__(self, address=None, value=None, slave=None, **kwargs): + def __init__(self, address=None, value=None, slave=None, transaction=0, protocol=0, skip_encode=0, **_kwargs): """Initialize a new instance. :param address: The variable address to write :param value: The value to write at address """ - ModbusRequest.__init__(self, slave=slave, **kwargs) + ModbusRequest.__init__(self, slave, transaction, protocol, skip_encode) self.address = address self.value = bool(value) @@ -119,13 +119,13 @@ class WriteSingleCoilResponse(ModbusResponse): function_code = 5 _rtu_frame_size = 8 - def __init__(self, address=None, value=None, **kwargs): + def __init__(self, address=None, value=None, slave=0, transaction=0, protocol=0, skip_encode=False, **_kwargs): """Initialize a new instance. :param address: The variable address written to :param value: The value written at address """ - ModbusResponse.__init__(self, **kwargs) + ModbusResponse.__init__(self, slave, transaction, protocol, skip_encode) self.address = address self.value = value @@ -173,13 +173,13 @@ class WriteMultipleCoilsRequest(ModbusRequest): function_code_name = "write_coils" _rtu_byte_count_pos = 6 - def __init__(self, address=None, values=None, slave=None, **kwargs): + def __init__(self, address=None, values=None, slave=None, transaction=0, protocol=0, skip_encode=0, **_kwargs): """Initialize a new instance. :param address: The starting request address :param values: The values to write """ - ModbusRequest.__init__(self, slave=slave, **kwargs) + ModbusRequest.__init__(self, slave, transaction, protocol, skip_encode) self.address = address if values is None: values = [] @@ -256,13 +256,13 @@ class WriteMultipleCoilsResponse(ModbusResponse): function_code = 15 _rtu_frame_size = 8 - def __init__(self, address=None, count=None, **kwargs): + def __init__(self, address=None, count=None, slave=0, transaction=0, protocol=0, skip_encode=False, **_kwargs): """Initialize a new instance. :param address: The starting variable address written to :param count: The number of values written """ - ModbusResponse.__init__(self, **kwargs) + ModbusResponse.__init__(self, slave, transaction, protocol, skip_encode) self.address = address self.count = count diff --git a/pymodbus/pdu/diag_message.py b/pymodbus/pdu/diag_message.py index e484ecd74..8d507068c 100644 --- a/pymodbus/pdu/diag_message.py +++ b/pymodbus/pdu/diag_message.py @@ -69,9 +69,9 @@ class DiagnosticStatusRequest(ModbusRequest): function_code_name = "diagnostic_status" _rtu_frame_size = 8 - def __init__(self, **kwargs): + def __init__(self, slave=0, transaction=0, protocol=0, skip_encode=False, **_kwargs): """Initialize a diagnostic request.""" - ModbusRequest.__init__(self, **kwargs) + ModbusRequest.__init__(self, slave, transaction, protocol, skip_encode) self.message = None def encode(self): @@ -131,9 +131,9 @@ class DiagnosticStatusResponse(ModbusResponse): function_code = 0x08 _rtu_frame_size = 8 - def __init__(self, **kwargs): + def __init__(self, slave=0, transaction=0, protocol=0, skip_encode=False, **_kwargs): """Initialize a diagnostic response.""" - ModbusResponse.__init__(self, **kwargs) + ModbusResponse.__init__(self, slave, transaction, protocol, skip_encode) self.message = None def encode(self): diff --git a/pymodbus/pdu/file_message.py b/pymodbus/pdu/file_message.py index e08e8f99d..51b4ecf50 100644 --- a/pymodbus/pdu/file_message.py +++ b/pymodbus/pdu/file_message.py @@ -100,12 +100,12 @@ class ReadFileRecordRequest(ModbusRequest): function_code_name = "read_file_record" _rtu_byte_count_pos = 2 - def __init__(self, records=None, **kwargs): + def __init__(self, records=None, slave=0, transaction=0, protocol=0, skip_encode=False, **_kwargs): """Initialize a new instance. :param records: The file record requests to be read """ - ModbusRequest.__init__(self, **kwargs) + ModbusRequest.__init__(self, slave, transaction, protocol, skip_encode) self.records = records or [] def encode(self): @@ -165,12 +165,12 @@ class ReadFileRecordResponse(ModbusResponse): function_code = 0x14 _rtu_byte_count_pos = 2 - def __init__(self, records=None, **kwargs): + def __init__(self, records=None, slave=0, transaction=0, protocol=0, skip_encode=False, **_kwargs): """Initialize a new instance. :param records: The requested file records """ - ModbusResponse.__init__(self, **kwargs) + ModbusResponse.__init__(self, slave, transaction, protocol, skip_encode) self.records = records or [] def encode(self): @@ -218,12 +218,12 @@ class WriteFileRecordRequest(ModbusRequest): function_code_name = "write_file_record" _rtu_byte_count_pos = 2 - def __init__(self, records=None, **kwargs): + def __init__(self, records=None, slave=0, transaction=0, protocol=0, skip_encode=False, **_kwargs): """Initialize a new instance. :param records: The file record requests to be read """ - ModbusRequest.__init__(self, **kwargs) + ModbusRequest.__init__(self, slave, transaction, protocol, skip_encode) self.records = records or [] def encode(self): @@ -282,12 +282,12 @@ class WriteFileRecordResponse(ModbusResponse): function_code = 0x15 _rtu_byte_count_pos = 2 - def __init__(self, records=None, **kwargs): + def __init__(self, records=None, slave=0, transaction=0, protocol=0, skip_encode=False, **_kwargs): """Initialize a new instance. :param records: The file record requests to be read """ - ModbusResponse.__init__(self, **kwargs) + ModbusResponse.__init__(self, slave, transaction, protocol, skip_encode) self.records = records or [] def encode(self): @@ -347,12 +347,12 @@ class ReadFifoQueueRequest(ModbusRequest): function_code_name = "read_fifo_queue" _rtu_frame_size = 6 - def __init__(self, address=0x0000, **kwargs): + def __init__(self, address=0x0000, slave=0, transaction=0, protocol=0, skip_encode=False, **_kwargs): """Initialize a new instance. :param address: The fifo pointer address (0x0000 to 0xffff) """ - ModbusRequest.__init__(self, **kwargs) + ModbusRequest.__init__(self, slave, transaction, protocol, skip_encode) self.address = address self.values = [] # this should be added to the context @@ -408,12 +408,12 @@ def calculateRtuFrameSize(cls, buffer): lo_byte = int(buffer[3]) return (hi_byte << 16) + lo_byte + 6 - def __init__(self, values=None, **kwargs): + def __init__(self, values=None, slave=0, transaction=0, protocol=0, skip_encode=False, **_kwargs): """Initialize a new instance. :param values: The list of values of the fifo to return """ - ModbusResponse.__init__(self, **kwargs) + ModbusResponse.__init__(self, slave, transaction, protocol, skip_encode) self.values = values or [] def encode(self): diff --git a/pymodbus/pdu/mei_message.py b/pymodbus/pdu/mei_message.py index 4eb2e596f..c1aeac445 100644 --- a/pymodbus/pdu/mei_message.py +++ b/pymodbus/pdu/mei_message.py @@ -56,13 +56,13 @@ class ReadDeviceInformationRequest(ModbusRequest): function_code_name = "read_device_information" _rtu_frame_size = 7 - def __init__(self, read_code=None, object_id=0x00, **kwargs): + def __init__(self, read_code=None, object_id=0x00, slave=0, transaction=0, protocol=0, skip_encode=False, **_kwargs): """Initialize a new instance. :param read_code: The device information read code :param object_id: The object to read from """ - ModbusRequest.__init__(self, **kwargs) + ModbusRequest.__init__(self, slave, transaction, protocol, skip_encode) self.read_code = read_code or DeviceInformation.BASIC self.object_id = object_id @@ -134,13 +134,13 @@ def calculateRtuFrameSize(cls, buffer): except struct.error as exc: raise IndexError from exc - def __init__(self, read_code=None, information=None, **kwargs): + def __init__(self, read_code=None, information=None, slave=0, transaction=0, protocol=0, skip_encode=False, **_kwargs): """Initialize a new instance. :param read_code: The device information read code :param information: The requested information request """ - ModbusResponse.__init__(self, **kwargs) + ModbusResponse.__init__(self, slave, transaction, protocol, skip_encode) self.read_code = read_code or DeviceInformation.BASIC self.information = information or {} self.number_of_objects = 0 diff --git a/pymodbus/pdu/other_message.py b/pymodbus/pdu/other_message.py index ec3e35110..a6b51a793 100644 --- a/pymodbus/pdu/other_message.py +++ b/pymodbus/pdu/other_message.py @@ -40,9 +40,9 @@ class ReadExceptionStatusRequest(ModbusRequest): function_code_name = "read_exception_status" _rtu_frame_size = 4 - def __init__(self, slave=None, **kwargs): + def __init__(self, slave=None, transaction=0, protocol=0, skip_encode=0, **_kwargs): """Initialize a new instance.""" - ModbusRequest.__init__(self, slave=slave, **kwargs) + ModbusRequest.__init__(self, slave, transaction, protocol, skip_encode) def encode(self): """Encode the message.""" @@ -82,12 +82,12 @@ class ReadExceptionStatusResponse(ModbusResponse): function_code = 0x07 _rtu_frame_size = 5 - def __init__(self, status=0x00, **kwargs): + def __init__(self, status=0x00, slave=0, transaction=0, protocol=0, skip_encode=False, **_kwargs): """Initialize a new instance. :param status: The status response to report """ - ModbusResponse.__init__(self, **kwargs) + ModbusResponse.__init__(self, slave, transaction, protocol, skip_encode) self.status = status if status < 256 else 255 def encode(self): @@ -145,9 +145,9 @@ class GetCommEventCounterRequest(ModbusRequest): function_code_name = "get_event_counter" _rtu_frame_size = 4 - def __init__(self, **kwargs): + def __init__(self, slave=0, transaction=0, protocol=0, skip_encode=False, **_kwargs): """Initialize a new instance.""" - ModbusRequest.__init__(self, **kwargs) + ModbusRequest.__init__(self, slave, transaction, protocol, skip_encode) def encode(self): """Encode the message.""" @@ -188,12 +188,12 @@ class GetCommEventCounterResponse(ModbusResponse): function_code = 0x0B _rtu_frame_size = 8 - def __init__(self, count=0x0000, **kwargs): + def __init__(self, count=0x0000, slave=0, transaction=0, protocol=0, skip_encode=False, **_kwargs): """Initialize a new instance. :param count: The current event counter value """ - ModbusResponse.__init__(self, **kwargs) + ModbusResponse.__init__(self, slave, transaction, protocol, skip_encode) self.count = count self.status = True # this means we are ready, not waiting @@ -256,9 +256,9 @@ class GetCommEventLogRequest(ModbusRequest): function_code_name = "get_event_log" _rtu_frame_size = 4 - def __init__(self, **kwargs): + def __init__(self, slave=0, transaction=0, protocol=0, skip_encode=False, **_kwargs): """Initialize a new instance.""" - ModbusRequest.__init__(self, **kwargs) + ModbusRequest.__init__(self, slave, transaction, protocol, skip_encode) def encode(self): """Encode the message.""" @@ -303,7 +303,7 @@ class GetCommEventLogResponse(ModbusResponse): function_code = 0x0C _rtu_byte_count_pos = 2 - def __init__(self, **kwargs): + def __init__(self, slave=0, transaction=0, protocol=0, skip_encode=False, **kwargs): """Initialize a new instance. :param status: The status response to report @@ -311,7 +311,7 @@ def __init__(self, **kwargs): :param event_count: The current event count :param events: The collection of events to send """ - ModbusResponse.__init__(self, **kwargs) + ModbusResponse.__init__(self, slave, transaction, protocol, skip_encode) self.status = kwargs.get("status", True) self.message_count = kwargs.get("message_count", 0) self.event_count = kwargs.get("event_count", 0) @@ -377,13 +377,13 @@ class ReportSlaveIdRequest(ModbusRequest): function_code_name = "report_slave_id" _rtu_frame_size = 4 - def __init__(self, slave=0, **kwargs): + def __init__(self, slave=0, transaction=0, protocol=0, skip_encode=False, **_kwargs): """Initialize a new instance. :param slave: Modbus slave slave ID """ - ModbusRequest.__init__(self, slave, **kwargs) + ModbusRequest.__init__(self, slave, transaction, protocol, skip_encode) def encode(self): """Encode the message.""" @@ -436,13 +436,13 @@ class ReportSlaveIdResponse(ModbusResponse): function_code = 0x11 _rtu_byte_count_pos = 2 - def __init__(self, identifier=b"\x00", status=True, **kwargs): + def __init__(self, identifier=b"\x00", status=True, slave=0, transaction=0, protocol=0, skip_encode=False, **_kwargs): """Initialize a new instance. :param identifier: The identifier of the slave :param status: The status response to report """ - ModbusResponse.__init__(self, **kwargs) + ModbusResponse.__init__(self, slave, transaction, protocol, skip_encode) self.identifier = identifier self.status = status self.byte_count = None diff --git a/pymodbus/pdu/pdu.py b/pymodbus/pdu/pdu.py index b430d5740..f11f681c6 100644 --- a/pymodbus/pdu/pdu.py +++ b/pymodbus/pdu/pdu.py @@ -53,16 +53,16 @@ class ModbusPDU: of encoding it again. """ - def __init__(self, slave=0, **kwargs): + def __init__(self, slave, transaction, protocol, skip_encode): """Initialize the base data for a modbus request. :param slave: Modbus slave slave ID """ - self.transaction_id = kwargs.get("transaction", 0) - self.protocol_id = kwargs.get("protocol", 0) + self.transaction_id = transaction + self.protocol_id = protocol self.slave_id = slave - self.skip_encode = kwargs.get("skip_encode", False) + self.skip_encode = skip_encode self.check = 0x0000 def encode(self): @@ -102,12 +102,12 @@ class ModbusRequest(ModbusPDU): function_code = -1 - def __init__(self, slave=0, **kwargs): + def __init__(self, slave, transaction, protocol, skip_encode): """Proxy to the lower level initializer. :param slave: Modbus slave slave ID """ - super().__init__(slave, **kwargs) + super().__init__(slave, transaction, protocol, skip_encode) self.fut = None def doException(self, exception): @@ -138,13 +138,13 @@ class ModbusResponse(ModbusPDU): should_respond = True function_code = 0x00 - def __init__(self, slave=0, **kwargs): + def __init__(self, slave, transaction, protocol, skip_encode): """Proxy the lower level initializer. :param slave: Modbus slave slave ID """ - super().__init__(slave, **kwargs) + super().__init__(slave, transaction, protocol, skip_encode) self.bits = [] self.registers = [] self.request = None @@ -191,13 +191,13 @@ class ExceptionResponse(ModbusResponse): ExceptionOffset = 0x80 _rtu_frame_size = 5 - def __init__(self, function_code, exception_code=None, **kwargs): + def __init__(self, function_code, exception_code=None, slave=0, transaction=0, protocol=0, skip_encode=False): """Initialize the modbus exception response. :param function_code: The function to build an exception response for :param exception_code: The specific modbus exception to return """ - super().__init__(**kwargs) + super().__init__(slave, transaction, protocol, skip_encode) self.original_code = function_code self.function_code = function_code | self.ExceptionOffset self.exception_code = exception_code @@ -240,12 +240,12 @@ class IllegalFunctionRequest(ModbusRequest): ErrorCode = 1 - def __init__(self, function_code, **kwargs): + def __init__(self, function_code, xslave, xtransaction, xprotocol, xskip_encode): """Initialize a IllegalFunctionRequest. :param function_code: The function we are erroring on """ - super().__init__(**kwargs) + super().__init__(xslave, xtransaction, xprotocol, xskip_encode) self.function_code = function_code def decode(self, _data): diff --git a/pymodbus/pdu/register_read_message.py b/pymodbus/pdu/register_read_message.py index 9088f501e..58d8f9b71 100644 --- a/pymodbus/pdu/register_read_message.py +++ b/pymodbus/pdu/register_read_message.py @@ -22,14 +22,14 @@ class ReadRegistersRequestBase(ModbusRequest): _rtu_frame_size = 8 - def __init__(self, address, count, slave=0, **kwargs): + def __init__(self, address, count, slave=0, transaction=0, protocol=0, skip_encode=False, **_kwargs): """Initialize a new instance. :param address: The address to start the read from :param count: The number of registers to read :param slave: Modbus slave slave ID """ - super().__init__(slave, **kwargs) + super().__init__(slave, transaction, protocol, skip_encode) self.address = address self.count = count @@ -70,13 +70,13 @@ class ReadRegistersResponseBase(ModbusResponse): _rtu_byte_count_pos = 2 - def __init__(self, values, slave=0, **kwargs): + def __init__(self, values, slave=0, transaction=0, protocol=0, skip_encode=False, **_kwargs): """Initialize a new instance. :param values: The values to write to :param slave: Modbus slave slave ID """ - super().__init__(slave, **kwargs) + super().__init__(slave, transaction, protocol, skip_encode) #: A list of register values self.registers = values or [] @@ -257,7 +257,7 @@ class ReadWriteMultipleRegistersRequest(ModbusRequest): function_code_name = "read_write_multiple_registers" _rtu_byte_count_pos = 10 - def __init__(self, **kwargs): + def __init__(self, slave=0, transaction=0, protocol=0, skip_encode=False, **kwargs): """Initialize a new request message. :param read_address: The address to start reading from @@ -265,7 +265,7 @@ def __init__(self, **kwargs): :param write_address: The address to start writing to :param write_registers: The registers to write to the specified address """ - super().__init__(**kwargs) + super().__init__(slave, transaction, protocol, skip_encode) self.read_address = kwargs.get("read_address", 0x00) self.read_count = kwargs.get("read_count", 0) self.write_address = kwargs.get("write_address", 0x00) @@ -373,12 +373,12 @@ class ReadWriteMultipleRegistersResponse(ModbusResponse): function_code = 23 _rtu_byte_count_pos = 2 - def __init__(self, values=None, **kwargs): + def __init__(self, values=None, slave=0, transaction=0, protocol=0, skip_encode=False, **_kwargs): """Initialize a new instance. :param values: The register values to write """ - super().__init__(**kwargs) + super().__init__(slave, transaction, protocol, skip_encode) self.registers = values or [] def encode(self): diff --git a/pymodbus/pdu/register_write_message.py b/pymodbus/pdu/register_write_message.py index 753c2ce62..32521acc7 100644 --- a/pymodbus/pdu/register_write_message.py +++ b/pymodbus/pdu/register_write_message.py @@ -28,13 +28,13 @@ class WriteSingleRegisterRequest(ModbusRequest): function_code_name = "write_register" _rtu_frame_size = 8 - def __init__(self, address=None, value=None, slave=None, **kwargs): + def __init__(self, address=None, value=None, slave=None, transaction=0, protocol=0, skip_encode=0, **_kwargs): """Initialize a new instance. :param address: The address to start writing add :param value: The values to write """ - super().__init__(slave=slave, **kwargs) + super().__init__(slave, transaction, protocol, skip_encode) self.address = address self.value = value @@ -99,13 +99,13 @@ class WriteSingleRegisterResponse(ModbusResponse): function_code = 6 _rtu_frame_size = 8 - def __init__(self, address=None, value=None, **kwargs): + def __init__(self, address=None, value=None, slave=0, transaction=0, protocol=0, skip_encode=False, **_kwargs): """Initialize a new instance. :param address: The address to start writing add :param value: The values to write """ - super().__init__(**kwargs) + super().__init__(slave, transaction, protocol, skip_encode) self.address = address self.value = value @@ -160,13 +160,13 @@ class WriteMultipleRegistersRequest(ModbusRequest): _rtu_byte_count_pos = 6 _pdu_length = 5 # func + adress1 + adress2 + outputQuant1 + outputQuant2 - def __init__(self, address=None, values=None, slave=None, **kwargs): + def __init__(self, address=None, values=None, slave=None, transaction=0, protocol=0, skip_encode=0, **_kwargs): """Initialize a new instance. :param address: The address to start writing to :param values: The values to write """ - super().__init__(slave=slave, **kwargs) + super().__init__(slave, transaction, protocol, skip_encode) self.address = address if values is None: values = [] @@ -247,13 +247,13 @@ class WriteMultipleRegistersResponse(ModbusResponse): function_code = 16 _rtu_frame_size = 8 - def __init__(self, address=None, count=None, **kwargs): + def __init__(self, address=None, count=None, slave=0, transaction=0, protocol=0, skip_encode=False, **_kwargs): """Initialize a new instance. :param address: The address to start writing to :param count: The number of registers to write to """ - super().__init__(**kwargs) + super().__init__(slave, transaction, protocol, skip_encode) self.address = address self.count = count @@ -295,14 +295,14 @@ class MaskWriteRegisterRequest(ModbusRequest): function_code_name = "mask_write_register" _rtu_frame_size = 10 - def __init__(self, address=0x0000, and_mask=0xFFFF, or_mask=0x0000, **kwargs): + def __init__(self, address=0x0000, and_mask=0xFFFF, or_mask=0x0000, slave=0, transaction=0, protocol=0, skip_encode=False, **_kwargs): """Initialize a new instance. :param address: The mask pointer address (0x0000 to 0xffff) :param and_mask: The and bitmask to apply to the register address :param or_mask: The or bitmask to apply to the register address """ - super().__init__(**kwargs) + super().__init__(slave, transaction, protocol, skip_encode) self.address = address self.and_mask = and_mask self.or_mask = or_mask @@ -350,14 +350,14 @@ class MaskWriteRegisterResponse(ModbusResponse): function_code = 0x16 _rtu_frame_size = 10 - def __init__(self, address=0x0000, and_mask=0xFFFF, or_mask=0x0000, **kwargs): + def __init__(self, address=0x0000, and_mask=0xFFFF, or_mask=0x0000, slave=0, transaction=0, protocol=0, skip_encode=False, **_kwargs): """Initialize new instance. :param address: The mask pointer address (0x0000 to 0xffff) :param and_mask: The and bitmask applied to the register address :param or_mask: The or bitmask applied to the register address """ - super().__init__(**kwargs) + super().__init__(slave, transaction, protocol, skip_encode) self.address = address self.and_mask = and_mask self.or_mask = or_mask diff --git a/pymodbus/transaction.py b/pymodbus/transaction.py index bbb187efe..fd3f6e329 100644 --- a/pymodbus/transaction.py +++ b/pymodbus/transaction.py @@ -134,7 +134,7 @@ class SyncModbusTransactionManager(ModbusTransactionManager): Results are keyed based on the supplied transaction id. """ - def __init__(self, client: ModbusBaseSyncClient, backoff=0.3, retry_on_empty=False, retry_on_invalid=False, retries=3, **_kwargs): + def __init__(self, client: ModbusBaseSyncClient, backoff, retry_on_empty, retry_on_invalid, retries): """Initialize an instance of the ModbusTransactionManager. :param client: The client socket wrapper diff --git a/test/framers/test_tbc_transaction.py b/test/framers/test_tbc_transaction.py index 2dd0405dc..cd78d1e75 100755 --- a/test/framers/test_tbc_transaction.py +++ b/test/framers/test_tbc_transaction.py @@ -42,7 +42,7 @@ def setup_method(self): self._tls = ModbusTlsFramer(decoder=self.decoder, client=None) self._rtu = ModbusRtuFramer(decoder=self.decoder, client=None) self._ascii = ModbusAsciiFramer(decoder=self.decoder, client=None) - self._manager = SyncModbusTransactionManager(self.client) + self._manager = SyncModbusTransactionManager(self.client, 0.3, False, False, 3) # ----------------------------------------------------------------------- # # Modbus transaction manager @@ -113,7 +113,7 @@ def test_execute(self, mock_time): request.get_response_pdu_size.return_value = 10 request.slave_id = 1 request.function_code = 222 - trans = SyncModbusTransactionManager(client) + trans = SyncModbusTransactionManager(client, 0.3, False, False, 3) trans._recv = mock.MagicMock( # pylint: disable=protected-access return_value=b"abcdef" ) @@ -341,14 +341,14 @@ def callback(data): count += 1 result = data - expected = ModbusRequest() + expected = ModbusRequest(0, 0, 0, False) expected.transaction_id = 0x0001 expected.protocol_id = 0x1234 expected.slave_id = 0xFF msg = b"\x00\x01\x12\x34\x00\x06\xff\x02\x12\x34\x01\x02" self._tcp.processIncomingPacket(msg, callback, [0, 1]) # assert self._tcp.checkFrame() - # actual = ModbusRequest() + # actual = ModbusRequest(0, 0, 0, False) # self._tcp.populateResult(actual) # for name in ("transaction_id", "protocol_id", "slave_id"): # assert getattr(expected, name) == getattr(actual, name) @@ -357,7 +357,7 @@ def test_tcp_framer_packet(self): """Test a tcp frame packet build.""" old_encode = ModbusRequest.encode ModbusRequest.encode = lambda self: b"" - message = ModbusRequest() + message = ModbusRequest(0, 0, 0, False) message.transaction_id = 0x0001 message.protocol_id = 0x0000 message.slave_id = 0xFF @@ -512,7 +512,7 @@ def test_framer_tls_framer_packet(self): """Test a tls frame packet build.""" old_encode = ModbusRequest.encode ModbusRequest.encode = lambda self: b"" - message = ModbusRequest() + message = ModbusRequest(0, 0, 0, False) message.function_code = 0x01 expected = b"\x01" actual = self._tls.buildPacket(message) @@ -589,7 +589,7 @@ def test_rtu_framer_packet(self): """Test a rtu frame packet build.""" old_encode = ModbusRequest.encode ModbusRequest.encode = lambda self: b"" - message = ModbusRequest() + message = ModbusRequest(0, 0, 0, False) message.slave_id = 0xFF message.function_code = 0x01 expected = b"\xff\x01\x81\x80" # only header + CRC - no data @@ -690,7 +690,7 @@ def callback(data): def test_ascii_framer_populate(self): """Test a ascii frame packet build.""" - request = ModbusRequest() + request = ModbusRequest(0, 0, 0, False) self._ascii.populateResult(request) assert not request.slave_id @@ -698,7 +698,7 @@ def test_ascii_framer_packet(self): """Test a ascii frame packet build.""" old_encode = ModbusRequest.encode ModbusRequest.encode = lambda self: b"" - message = ModbusRequest() + message = ModbusRequest(0, 0, 0, False) message.slave_id = 0xFF message.function_code = 0x01 expected = b":FF0100\r\n" diff --git a/test/sub_client/test_client.py b/test/sub_client/test_client.py index a6f07fe04..ec9405009 100755 --- a/test/sub_client/test_client.py +++ b/test/sub_client/test_client.py @@ -262,7 +262,7 @@ async def test_client_instanciate( client.connect = lambda: False client.transport = None with pytest.raises(ConnectionException): - client.execute(ModbusRequest()) + client.execute(ModbusRequest(0, 0, 0, False)) async def test_serial_not_installed(): """Try to instantiate clients.""" @@ -647,13 +647,13 @@ async def test_client_build_response(): """Test fail of build_response.""" client = ModbusBaseClient(FramerType.RTU) with pytest.raises(ConnectionException): - await client.build_response(ModbusRequest(transaction=0)) + await client.build_response(ModbusRequest(0, 0, 0, False)) async def test_client_mixin_execute(): """Test dummy execute for both sync and async.""" client = ModbusClientMixin() with pytest.raises(NotImplementedError): - client.execute(ModbusRequest()) + client.execute(ModbusRequest(0, 0, 0, False)) with pytest.raises(NotImplementedError): - await client.execute(ModbusRequest()) + await client.execute(ModbusRequest(0, 0, 0, False)) diff --git a/test/sub_function_codes/test_all_messages.py b/test/sub_function_codes/test_all_messages.py index bfb669df9..9e20ea5ed 100644 --- a/test/sub_function_codes/test_all_messages.py +++ b/test/sub_function_codes/test_all_messages.py @@ -82,8 +82,8 @@ def test_initializing_slave_address_response(self): response = factory(slave_id) assert response.slave_id == slave_id - def test_forwarding_kwargs_to_pdu(self): - """Test that the kwargs are forwarded to the pdu correctly.""" + def test_forwarding_to_pdu(self): + """Test that parameters are forwarded to the pdu correctly.""" request = ReadCoilsRequest(1, 5, slave=0x12, transaction=0x12, protocol=0x12) assert request.slave_id == 0x12 assert request.transaction_id == 0x12 diff --git a/test/test_pdu.py b/test/test_pdu.py index 44f44f874..aa18b2d6c 100644 --- a/test/test_pdu.py +++ b/test/test_pdu.py @@ -15,11 +15,11 @@ class TestPdu: """Unittest for the pymod.pdu module.""" bad_requests = ( - ModbusRequest(), - ModbusResponse(), + ModbusRequest(0, 0, 0, False), + ModbusResponse(0, 0, 0, False), ) - illegal = IllegalFunctionRequest(1) - exception = ExceptionResponse(1, 1) + illegal = IllegalFunctionRequest(1, 0, 0, 0, False) + exception = ExceptionResponse(1, 1, 0, 0, 0, False) def test_not_impelmented(self): """Test a base classes for not implemented functions.""" @@ -43,7 +43,7 @@ async def test_error_methods(self): def test_request_exception_factory(self): """Test all error methods.""" - request = ModbusRequest() + request = ModbusRequest(0, 0, 0, False) request.function_code = 1 errors = {ModbusExceptions.decode(c): c for c in range(1, 20)} for error, code in iter(errors.items()): diff --git a/test/test_transaction.py b/test/test_transaction.py index 2dd0405dc..546807f64 100755 --- a/test/test_transaction.py +++ b/test/test_transaction.py @@ -42,7 +42,7 @@ def setup_method(self): self._tls = ModbusTlsFramer(decoder=self.decoder, client=None) self._rtu = ModbusRtuFramer(decoder=self.decoder, client=None) self._ascii = ModbusAsciiFramer(decoder=self.decoder, client=None) - self._manager = SyncModbusTransactionManager(self.client) + self._manager = SyncModbusTransactionManager(self.client, 0.3, False, False, 3) # ----------------------------------------------------------------------- # # Modbus transaction manager @@ -113,7 +113,7 @@ def test_execute(self, mock_time): request.get_response_pdu_size.return_value = 10 request.slave_id = 1 request.function_code = 222 - trans = SyncModbusTransactionManager(client) + trans = SyncModbusTransactionManager(client, 0.3, False, False, 3) trans._recv = mock.MagicMock( # pylint: disable=protected-access return_value=b"abcdef" ) @@ -341,7 +341,7 @@ def callback(data): count += 1 result = data - expected = ModbusRequest() + expected = ModbusRequest(0, 0, 0, False) expected.transaction_id = 0x0001 expected.protocol_id = 0x1234 expected.slave_id = 0xFF @@ -357,7 +357,7 @@ def test_tcp_framer_packet(self): """Test a tcp frame packet build.""" old_encode = ModbusRequest.encode ModbusRequest.encode = lambda self: b"" - message = ModbusRequest() + message = ModbusRequest(0, 0, 0, False) message.transaction_id = 0x0001 message.protocol_id = 0x0000 message.slave_id = 0xFF @@ -512,7 +512,7 @@ def test_framer_tls_framer_packet(self): """Test a tls frame packet build.""" old_encode = ModbusRequest.encode ModbusRequest.encode = lambda self: b"" - message = ModbusRequest() + message = ModbusRequest(0, 0, 0, False) message.function_code = 0x01 expected = b"\x01" actual = self._tls.buildPacket(message) @@ -589,7 +589,7 @@ def test_rtu_framer_packet(self): """Test a rtu frame packet build.""" old_encode = ModbusRequest.encode ModbusRequest.encode = lambda self: b"" - message = ModbusRequest() + message = ModbusRequest(0, 0, 0, False) message.slave_id = 0xFF message.function_code = 0x01 expected = b"\xff\x01\x81\x80" # only header + CRC - no data @@ -690,7 +690,7 @@ def callback(data): def test_ascii_framer_populate(self): """Test a ascii frame packet build.""" - request = ModbusRequest() + request = ModbusRequest(0, 0, 0, False) self._ascii.populateResult(request) assert not request.slave_id @@ -698,7 +698,7 @@ def test_ascii_framer_packet(self): """Test a ascii frame packet build.""" old_encode = ModbusRequest.encode ModbusRequest.encode = lambda self: b"" - message = ModbusRequest() + message = ModbusRequest(0, 0, 0, False) message.slave_id = 0xFF message.function_code = 0x01 expected = b":FF0100\r\n" From a8de61dc44b486c3a6045c5073d3893851c11d7d Mon Sep 17 00:00:00 2001 From: James Hilliard Date: Fri, 19 Jul 2024 00:44:49 -0600 Subject: [PATCH 67/88] Fix ruff check in CI (#2242) --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3f4a888ef..5fdfd762f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -98,7 +98,7 @@ jobs: - name: ruff if: matrix.run_lint == true run: | - ruff . + ruff check . - name: pytest if: ${{ (matrix.os != 'ubuntu-latest') || (matrix.python != '3.12') }} From 3dea7ac54f4bd631515ed9a907ecdaf3191d4c68 Mon Sep 17 00:00:00 2001 From: James Hilliard Date: Fri, 19 Jul 2024 00:53:03 -0600 Subject: [PATCH 68/88] Update dev dependencies (#2241) Co-authored-by: jan iversen --- pyproject.toml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index bbc81d515..99182bdb5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -62,17 +62,17 @@ documentation = [ development = [ "build>=1.2.1", "codespell>=2.3.0", - "coverage>=7.5.3", - "mypy>=1.10.0", - "pylint>=3.2.3", + "coverage>=7.6.0", + "mypy>=1.10.1", + "pylint>=3.2.5", "pytest>=8.2.2", - "pytest-asyncio>=0.23.7", + "pytest-asyncio>=0.23.8", "pytest-cov>=5.0.0", "pytest-profiling>=1.7.0", "pytest-timeout>=2.3.1", "pytest-xdist>=3.6.1", - "ruff>=0.4.8", - "twine>=5.1.0", + "ruff>=0.5.3", + "twine>=5.1.1", "types-Pygments", "types-pyserial" ] From 18c839bad9429d970e0d0b28abf6713449ec17af Mon Sep 17 00:00:00 2001 From: jan iversen Date: Fri, 19 Jul 2024 14:10:47 +0200 Subject: [PATCH 69/88] Remove message_generator example (not part of API). (#2239) --- doc/source/_static/examples.tgz | Bin 50710 -> 42372 bytes doc/source/_static/examples.zip | Bin 37195 -> 38790 bytes doc/source/examples.rst | 9 - examples/client_async.py | 2 + examples/client_custom_msg.py | 3 + examples/helper.py | 2 +- examples/message_generator.py | 216 ------------------ test/sub_examples/test_client_server_async.py | 12 + test/sub_examples/test_examples.py | 6 - 9 files changed, 18 insertions(+), 232 deletions(-) delete mode 100755 examples/message_generator.py diff --git a/doc/source/_static/examples.tgz b/doc/source/_static/examples.tgz index 722d25b18df64101be33e6559c2da0358074cd17..be0a609ef10a30d4b1d5da98ed7e5509a9c9d324 100644 GIT binary patch literal 42372 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 M2RkiO#sDq@0KJ6VhyVZp literal 50710 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 diff --git a/doc/source/_static/examples.zip b/doc/source/_static/examples.zip index 5da9c1f1451dc851c327268c09c12edde69f4df7..777f6fbec4055718a22123ee489d0ebd83fd411d 100644 GIT binary patch delta 23682 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 CM4#3G delta 21709 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 Cq7)_o diff --git a/doc/source/examples.rst b/doc/source/examples.rst index 74b01b811..1ec97e8cf 100644 --- a/doc/source/examples.rst +++ b/doc/source/examples.rst @@ -177,15 +177,6 @@ Source: :github:`examples/datastore_simulator_share.py` :noindex: -Message generator -^^^^^^^^^^^^^^^^^ -Source: :github:`examples/message_generator.py` - -.. automodule:: examples.message_generator - :undoc-members: - :noindex: - - Message Parser ^^^^^^^^^^^^^^ Source: :github:`examples/message_parser.py` diff --git a/examples/client_async.py b/examples/client_async.py index 06da1c40e..081770e87 100755 --- a/examples/client_async.py +++ b/examples/client_async.py @@ -113,6 +113,8 @@ def setup_async_client(description=None, cmdline=None): ), server_hostname="localhost", ) + else: + raise RuntimeError(f"Unknown commtype {args.comm}") return client diff --git a/examples/client_custom_msg.py b/examples/client_custom_msg.py index e86f4793b..17b643c64 100755 --- a/examples/client_custom_msg.py +++ b/examples/client_custom_msg.py @@ -121,6 +121,9 @@ async def main(host="localhost", port=5020): async with ModbusClient(host=host, port=port, framer_name=FramerType.SOCKET) as client: await client.connect() + # create a response object to control it works + CustomModbusResponse() + # new modbus function code. client.register(CustomModbusResponse) request = CustomModbusRequest(32, slave=1) diff --git a/examples/helper.py b/examples/helper.py index af26468c8..d1d93b64d 100755 --- a/examples/helper.py +++ b/examples/helper.py @@ -20,7 +20,7 @@ def get_commandline(server=False, description=None, extras=None, cmdline=None): parser.add_argument( "-c", "--comm", - choices=["tcp", "udp", "serial", "tls"], + choices=["tcp", "udp", "serial", "tls", "unknown"], help="set communication, default is tcp", dest="comm", default="tcp", diff --git a/examples/message_generator.py b/examples/message_generator.py deleted file mode 100755 index cf885b36c..000000000 --- a/examples/message_generator.py +++ /dev/null @@ -1,216 +0,0 @@ -#!/usr/bin/env python3 -"""Modbus Message Generator.""" -import argparse -import codecs as c -import logging - -import pymodbus.pdu.bit_read_message as modbus_bit -import pymodbus.pdu.bit_write_message as modbus_bit_write -import pymodbus.pdu.diag_message as modbus_diag -import pymodbus.pdu.file_message as modbus_file -import pymodbus.pdu.mei_message as modbus_mei -import pymodbus.pdu.other_message as modbus_other -import pymodbus.pdu.register_read_message as modbus_register -import pymodbus.pdu.register_write_message as modbus_register_write -from pymodbus.transaction import ( - ModbusAsciiFramer, - ModbusRtuFramer, - ModbusSocketFramer, -) - - -_logger = logging.getLogger(__file__) - - -# -------------------------------------------------------------------------- # -# enumerate all request/response messages -# -------------------------------------------------------------------------- # -messages = [ - ( - modbus_register.ReadHoldingRegistersRequest, - modbus_register.ReadHoldingRegistersResponse, - ), - (modbus_bit.ReadDiscreteInputsRequest, modbus_bit.ReadDiscreteInputsResponse), - ( - modbus_register.ReadInputRegistersRequest, - modbus_register.ReadInputRegistersResponse, - ), - (modbus_bit.ReadCoilsRequest, modbus_bit.ReadCoilsResponse), - ( - modbus_bit_write.WriteMultipleCoilsRequest, - modbus_bit_write.WriteMultipleCoilsResponse, - ), - ( - modbus_register_write.WriteMultipleRegistersRequest, - modbus_register_write.WriteMultipleRegistersResponse, - ), - ( - modbus_register_write.WriteSingleRegisterRequest, - modbus_register_write.WriteSingleRegisterResponse, - ), - (modbus_bit_write.WriteSingleCoilRequest, modbus_bit_write.WriteSingleCoilResponse), - ( - modbus_register.ReadWriteMultipleRegistersRequest, - modbus_register.ReadWriteMultipleRegistersResponse, - ), - (modbus_other.ReadExceptionStatusRequest, modbus_other.ReadExceptionStatusResponse), - (modbus_other.GetCommEventCounterRequest, modbus_other.GetCommEventCounterResponse), - (modbus_other.GetCommEventLogRequest, modbus_other.GetCommEventLogResponse), - (modbus_other.ReportSlaveIdRequest, modbus_other.ReportSlaveIdResponse), - (modbus_file.ReadFileRecordRequest, modbus_file.ReadFileRecordResponse), - (modbus_file.WriteFileRecordRequest, modbus_file.WriteFileRecordResponse), - ( - modbus_register_write.MaskWriteRegisterRequest, - modbus_register_write.MaskWriteRegisterResponse, - ), - (modbus_file.ReadFifoQueueRequest, modbus_file.ReadFifoQueueResponse), - (modbus_mei.ReadDeviceInformationRequest, modbus_mei.ReadDeviceInformationResponse), - (modbus_diag.ReturnQueryDataRequest, modbus_diag.ReturnQueryDataResponse), - ( - modbus_diag.RestartCommunicationsOptionRequest, - modbus_diag.RestartCommunicationsOptionResponse, - ), - ( - modbus_diag.ReturnDiagnosticRegisterRequest, - modbus_diag.ReturnDiagnosticRegisterResponse, - ), - ( - modbus_diag.ChangeAsciiInputDelimiterRequest, - modbus_diag.ChangeAsciiInputDelimiterResponse, - ), - (modbus_diag.ForceListenOnlyModeRequest, modbus_diag.ForceListenOnlyModeResponse), - (modbus_diag.ClearCountersRequest, modbus_diag.ClearCountersResponse), - ( - modbus_diag.ReturnBusMessageCountRequest, - modbus_diag.ReturnBusMessageCountResponse, - ), - ( - modbus_diag.ReturnBusCommunicationErrorCountRequest, - modbus_diag.ReturnBusCommunicationErrorCountResponse, - ), - ( - modbus_diag.ReturnBusExceptionErrorCountRequest, - modbus_diag.ReturnBusExceptionErrorCountResponse, - ), - ( - modbus_diag.ReturnSlaveMessageCountRequest, - modbus_diag.ReturnSlaveMessageCountResponse, - ), - ( - modbus_diag.ReturnSlaveNoResponseCountRequest, - modbus_diag.ReturnSlaveNoResponseCountResponse, - ), - (modbus_diag.ReturnSlaveNAKCountRequest, modbus_diag.ReturnSlaveNAKCountResponse), - (modbus_diag.ReturnSlaveBusyCountRequest, modbus_diag.ReturnSlaveBusyCountResponse), - ( - modbus_diag.ReturnSlaveBusCharacterOverrunCountRequest, - modbus_diag.ReturnSlaveBusCharacterOverrunCountResponse, - ), - ( - modbus_diag.ReturnIopOverrunCountRequest, - modbus_diag.ReturnIopOverrunCountResponse, - ), - (modbus_diag.ClearOverrunCountRequest, modbus_diag.ClearOverrunCountResponse), - (modbus_diag.GetClearModbusPlusRequest, modbus_diag.GetClearModbusPlusResponse), -] - - -def get_commandline(cmdline=None): - """Parse the command line options.""" - parser = argparse.ArgumentParser() - parser.add_argument( - "--framer", - choices=["ascii", "rtu", "socket"], - help="set framer, default is rtu", - dest="framer", - default="rtu", - type=str, - ) - parser.add_argument( - "-l", - "--log", - choices=["critical", "error", "warning", "info", "debug"], - help="set log level, default is info", - dest="log", - default="info", - type=str, - ) - parser.add_argument( - "-a", - "--address", - help="address to use", - dest="address", - default=32, - type=int, - ) - parser.add_argument( - "-c", - "--count", - help="count to use", - dest="count", - default=8, - type=int, - ) - parser.add_argument( - "-v", - "--value", - help="value to use", - dest="value", - default=1, - type=int, - ) - parser.add_argument( - "-t", - "--transaction", - help="transaction to use", - dest="transaction", - default=1, - type=int, - ) - parser.add_argument( - "-s", - "--slave", - help="slave to use", - dest="slave", - default=1, - type=int, - ) - args = parser.parse_args(cmdline) - return args - - -def generate_messages(cmdline=None): - """Parse the command line options.""" - args = get_commandline(cmdline=cmdline) - _logger.setLevel(args.log.upper()) - - arguments = { - "address": args.address, - "count": args.count, - "value": args.value, - "values": [args.value] * args.count, - "read_address": args.address, - "read_count": args.count, - "write_address": args.address, - "write_registers": [args.value] * args.count, - "transaction": args.transaction, - "slave": args.slave, - "protocol": 0x00, - } - framer = { - "ascii": ModbusAsciiFramer, - "rtu": ModbusRtuFramer, - "socket": ModbusSocketFramer, - }[args.framer](None) - - for entry in messages: - for inx in (0, 1): - message = entry[inx](**arguments) - raw_packet = framer.buildPacket(message) - packet = c.encode(raw_packet, "hex_codec").decode("utf-8") - print(f"{message.__class__.__name__:44} = {packet}") - print("") - - -if __name__ == "__main__": - generate_messages() diff --git a/test/sub_examples/test_client_server_async.py b/test/sub_examples/test_client_server_async.py index 674b4fa66..92446785d 100755 --- a/test/sub_examples/test_client_server_async.py +++ b/test/sub_examples/test_client_server_async.py @@ -56,6 +56,12 @@ async def test_client_exception(self, mock_server, mock_clc): ) await run_async_client(test_client, modbus_calls=run_a_few_calls) + async def test_client_no_calls(self, mock_server, mock_clc): + """Run async client and server.""" + assert mock_server + test_client = setup_async_client(cmdline=mock_clc) + await run_async_client(test_client, modbus_calls=None) + async def test_server_no_client(self, mock_server): """Run async server without client.""" assert mock_server @@ -76,3 +82,9 @@ async def test_client_no_server(self, mock_clc): test_client = setup_async_client(cmdline=mock_clc) with pytest.raises((AssertionError, asyncio.TimeoutError)): await run_async_client(test_client, modbus_calls=run_a_few_calls) + +async def test_illegal_commtype(): + """Run async client and server.""" + with pytest.raises(RuntimeError): + setup_async_client(cmdline=["--comm", "unknown", "--framer", "rtu", "--port", "5912"] +) diff --git a/test/sub_examples/test_examples.py b/test/sub_examples/test_examples.py index 1cf0ead7c..446190a75 100755 --- a/test/sub_examples/test_examples.py +++ b/test/sub_examples/test_examples.py @@ -18,7 +18,6 @@ from examples.client_custom_msg import main as main_custom_client from examples.client_payload import main as main_payload_calls from examples.datastore_simulator_share import main as main_datastore_simulator_share -from examples.message_generator import generate_messages from examples.message_parser import main as main_parse_messages from examples.server_async import setup_server from examples.server_callback import run_callback_server @@ -43,11 +42,6 @@ def get_port_in_class(base_ports): base_ports[__class__.__name__] += 1 return base_ports[__class__.__name__] - @pytest.mark.parametrize("framer", ["socket", "rtu", "ascii"]) - def test_message_generator(self, framer): - """Test all message generator.""" - generate_messages(cmdline=["--framer", framer]) - @pytest.mark.parametrize("framer", ["socket", "rtu", "ascii"]) def test_message_parser(self, framer): """Test message parser.""" From 552c56e8ded17f384737981df81ba6247a538b57 Mon Sep 17 00:00:00 2001 From: jan iversen Date: Fri, 19 Jul 2024 15:35:19 +0200 Subject: [PATCH 70/88] remove kwargs PDU messagees. (#2240) --- examples/client_async_calls.py | 2 +- examples/client_calls.py | 2 +- examples/client_custom_msg.py | 13 ++++----- pymodbus/client/mixin.py | 21 ++++++--------- pymodbus/pdu/bit_read_message.py | 12 ++++----- pymodbus/pdu/bit_write_message.py | 8 +++--- pymodbus/pdu/diag_message.py | 37 +++++++++++++------------- pymodbus/pdu/file_message.py | 26 +++++++++--------- pymodbus/pdu/mei_message.py | 4 +-- pymodbus/pdu/other_message.py | 24 ++++++++--------- pymodbus/pdu/register_read_message.py | 32 +++++++++++----------- pymodbus/pdu/register_write_message.py | 12 ++++----- 12 files changed, 95 insertions(+), 98 deletions(-) diff --git a/examples/client_async_calls.py b/examples/client_async_calls.py index 482a9ec40..bf4d66b43 100755 --- a/examples/client_async_calls.py +++ b/examples/client_async_calls.py @@ -137,7 +137,7 @@ async def async_handle_holding_registers(client): assert not rr.isError() # test that call was OK assert rr.registers == [10] * 8 - _logger.info("### write read holding registers, using **kwargs") + _logger.info("### write read holding registers") arguments = { "read_address": 1, "read_count": 8, diff --git a/examples/client_calls.py b/examples/client_calls.py index 50a58bc3d..c1de95f93 100755 --- a/examples/client_calls.py +++ b/examples/client_calls.py @@ -138,7 +138,7 @@ def handle_holding_registers(client): assert not rr.isError() # test that call was OK assert rr.registers == [10] * 8 - _logger.info("### write read holding registers, using **kwargs") + _logger.info("### write read holding registers") arguments = { "read_address": 1, "read_count": 8, diff --git a/examples/client_custom_msg.py b/examples/client_custom_msg.py index 17b643c64..8fb6cb3a5 100755 --- a/examples/client_custom_msg.py +++ b/examples/client_custom_msg.py @@ -36,7 +36,7 @@ class CustomModbusResponse(ModbusResponse): function_code = 55 _rtu_byte_count_pos = 2 - def __init__(self, values=None, slave=0, transaction=0, protocol=0, skip_encode=False, **_kwargs): + def __init__(self, values=None, slave=0, transaction=0, protocol=0, skip_encode=False): """Initialize.""" ModbusResponse.__init__(self, slave, transaction, protocol, skip_encode) self.values = values or [] @@ -68,7 +68,7 @@ class CustomModbusRequest(ModbusRequest): function_code = 55 _rtu_frame_size = 8 - def __init__(self, address=None, slave=0, transaction=0, protocol=0, skip_encode=False, **_kwargs): + def __init__(self, address=None, slave=0, transaction=0, protocol=0, skip_encode=False): """Initialize.""" ModbusRequest.__init__(self, slave, transaction, protocol, skip_encode) self.address = address @@ -100,12 +100,12 @@ def execute(self, context): class Read16CoilsRequest(ReadCoilsRequest): """Read 16 coils in one request.""" - def __init__(self, address, **kwargs): + def __init__(self, address, count=None, slave=0, transaction=0, protocol=0, skip_encode=False): """Initialize a new instance. :param address: The address to start reading from """ - ReadCoilsRequest.__init__(self, address, 16, **kwargs) + ReadCoilsRequest.__init__(self, address, count=16, slave=slave, transaction=transaction, protocol=protocol, skip_encode=skip_encode) # --------------------------------------------------------------------------- # @@ -126,12 +126,13 @@ async def main(host="localhost", port=5020): # new modbus function code. client.register(CustomModbusResponse) - request = CustomModbusRequest(32, slave=1) + slave=1 + request = CustomModbusRequest(32, slave=slave) result = await client.execute(request) print(result) # inherited request - request = Read16CoilsRequest(32, slave=1) + request = Read16CoilsRequest(32, slave) result = await client.execute(request) print(result) diff --git a/pymodbus/client/mixin.py b/pymodbus/client/mixin.py index bb5346d25..40391de61 100644 --- a/pymodbus/client/mixin.py +++ b/pymodbus/client/mixin.py @@ -64,48 +64,42 @@ def execute(self, _request: ModbusRequest) -> T: ) def read_coils( - self, address: int, count: int = 1, slave: int = 0, **kwargs: Any - ) -> T: + self, address: int, count: int = 1, slave: int = 0) -> T: """Read coils (code 0x01). :param address: Start address to read from :param count: (optional) Number of coils to read :param slave: (optional) Modbus slave ID - :param kwargs: (optional) Experimental parameters. :raises ModbusException: """ return self.execute( - pdu_bit_read.ReadCoilsRequest(address, count, slave, **kwargs) + pdu_bit_read.ReadCoilsRequest(address, count, slave) ) def read_discrete_inputs( - self, address: int, count: int = 1, slave: int = 0, **kwargs: Any - ) -> T: + self, address: int, count: int = 1, slave: int = 0) -> T: """Read discrete inputs (code 0x02). :param address: Start address to read from :param count: (optional) Number of coils to read :param slave: (optional) Modbus slave ID - :param kwargs: (optional) Experimental parameters. :raises ModbusException: """ return self.execute( - pdu_bit_read.ReadDiscreteInputsRequest(address, count, slave, **kwargs) + pdu_bit_read.ReadDiscreteInputsRequest(address, count, slave) ) def read_holding_registers( - self, address: int, count: int = 1, slave: int = 0, **kwargs: Any - ) -> T: + self, address: int, count: int = 1, slave: int = 0) -> T: """Read holding registers (code 0x03). :param address: Start address to read from :param count: (optional) Number of coils to read :param slave: (optional) Modbus slave ID - :param kwargs: (optional) Experimental parameters. :raises ModbusException: """ return self.execute( - pdu_reg_read.ReadHoldingRegistersRequest(address, count, slave, **kwargs) + pdu_reg_read.ReadHoldingRegistersRequest(address, count, slave) ) def read_input_registers( @@ -457,6 +451,7 @@ def readwrite_registers( :param kwargs: :raises ModbusException: """ + read_address = kwargs.pop("address", read_address) return self.execute( pdu_reg_read.ReadWriteMultipleRegistersRequest( read_address=read_address, @@ -464,7 +459,7 @@ def readwrite_registers( write_address=write_address, write_registers=values, slave=slave, - **kwargs, + **kwargs ) ) diff --git a/pymodbus/pdu/bit_read_message.py b/pymodbus/pdu/bit_read_message.py index c7a54e679..a7a8877a5 100644 --- a/pymodbus/pdu/bit_read_message.py +++ b/pymodbus/pdu/bit_read_message.py @@ -21,7 +21,7 @@ class ReadBitsRequestBase(ModbusRequest): _rtu_frame_size = 8 - def __init__(self, address, count, slave=0, transaction=0, protocol=0, skip_encode=False, **_kwargs): + def __init__(self, address, count, slave=0, transaction=0, protocol=0, skip_encode=False): """Initialize the read request data. :param address: The start address to read from @@ -75,7 +75,7 @@ class ReadBitsResponseBase(ModbusResponse): _rtu_byte_count_pos = 2 - def __init__(self, values, slave=0, transaction=0, protocol=0, skip_encode=False, **_kwargs): + def __init__(self, values, slave=0, transaction=0, protocol=0, skip_encode=False): """Initialize a new instance. :param values: The requested values to be returned @@ -146,7 +146,7 @@ class ReadCoilsRequest(ReadBitsRequestBase): function_code = 1 function_code_name = "read_coils" - def __init__(self, address=None, count=None, slave=0, transaction=0, protocol=0, skip_encode=False, **_kwargs): + def __init__(self, address=None, count=None, slave=0, transaction=0, protocol=0, skip_encode=False): """Initialize a new instance. :param address: The address to start reading from @@ -193,7 +193,7 @@ class ReadCoilsResponse(ReadBitsResponseBase): function_code = 1 - def __init__(self, values=None, slave=0, transaction=0, protocol=0, skip_encode=False, **_kwargs): + def __init__(self, values=None, slave=0, transaction=0, protocol=0, skip_encode=False): """Initialize a new instance. :param values: The request values to respond with @@ -214,7 +214,7 @@ class ReadDiscreteInputsRequest(ReadBitsRequestBase): function_code = 2 function_code_name = "read_discrete_input" - def __init__(self, address=None, count=None, slave=0, transaction=0, protocol=0, skip_encode=False, **_kwargs): + def __init__(self, address=None, count=None, slave=0, transaction=0, protocol=0, skip_encode=False): """Initialize a new instance. :param address: The address to start reading from @@ -261,7 +261,7 @@ class ReadDiscreteInputsResponse(ReadBitsResponseBase): function_code = 2 - def __init__(self, values=None, slave=0, transaction=0, protocol=0, skip_encode=False, **_kwargs): + def __init__(self, values=None, slave=0, transaction=0, protocol=0, skip_encode=False): """Initialize a new instance. :param values: The request values to respond with diff --git a/pymodbus/pdu/bit_write_message.py b/pymodbus/pdu/bit_write_message.py index 3297299d0..04c2d5c6a 100644 --- a/pymodbus/pdu/bit_write_message.py +++ b/pymodbus/pdu/bit_write_message.py @@ -49,7 +49,7 @@ class WriteSingleCoilRequest(ModbusRequest): _rtu_frame_size = 8 - def __init__(self, address=None, value=None, slave=None, transaction=0, protocol=0, skip_encode=0, **_kwargs): + def __init__(self, address=None, value=None, slave=None, transaction=0, protocol=0, skip_encode=0): """Initialize a new instance. :param address: The variable address to write @@ -119,7 +119,7 @@ class WriteSingleCoilResponse(ModbusResponse): function_code = 5 _rtu_frame_size = 8 - def __init__(self, address=None, value=None, slave=0, transaction=0, protocol=0, skip_encode=False, **_kwargs): + def __init__(self, address=None, value=None, slave=0, transaction=0, protocol=0, skip_encode=False): """Initialize a new instance. :param address: The variable address written to @@ -173,7 +173,7 @@ class WriteMultipleCoilsRequest(ModbusRequest): function_code_name = "write_coils" _rtu_byte_count_pos = 6 - def __init__(self, address=None, values=None, slave=None, transaction=0, protocol=0, skip_encode=0, **_kwargs): + def __init__(self, address=None, values=None, slave=None, transaction=0, protocol=0, skip_encode=0): """Initialize a new instance. :param address: The starting request address @@ -256,7 +256,7 @@ class WriteMultipleCoilsResponse(ModbusResponse): function_code = 15 _rtu_frame_size = 8 - def __init__(self, address=None, count=None, slave=0, transaction=0, protocol=0, skip_encode=False, **_kwargs): + def __init__(self, address=None, count=None, slave=0, transaction=0, protocol=0, skip_encode=False): """Initialize a new instance. :param address: The starting variable address written to diff --git a/pymodbus/pdu/diag_message.py b/pymodbus/pdu/diag_message.py index 8d507068c..68f5ee0d4 100644 --- a/pymodbus/pdu/diag_message.py +++ b/pymodbus/pdu/diag_message.py @@ -69,7 +69,7 @@ class DiagnosticStatusRequest(ModbusRequest): function_code_name = "diagnostic_status" _rtu_frame_size = 8 - def __init__(self, slave=0, transaction=0, protocol=0, skip_encode=False, **_kwargs): + def __init__(self, slave=0, transaction=0, protocol=0, skip_encode=False): """Initialize a diagnostic request.""" ModbusRequest.__init__(self, slave, transaction, protocol, skip_encode) self.message = None @@ -131,7 +131,7 @@ class DiagnosticStatusResponse(ModbusResponse): function_code = 0x08 _rtu_frame_size = 8 - def __init__(self, slave=0, transaction=0, protocol=0, skip_encode=False, **_kwargs): + def __init__(self, slave=0, transaction=0, protocol=0, skip_encode=False): """Initialize a diagnostic response.""" ModbusResponse.__init__(self, slave, transaction, protocol, skip_encode) self.message = None @@ -188,7 +188,7 @@ class DiagnosticStatusSimpleRequest(DiagnosticStatusRequest): the execute method """ - def __init__(self, data=0x0000, **kwargs): + def __init__(self, data=0x0000, slave=0, transaction=0, protocol=0, skip_encode=False): """Initialize a simple diagnostic request. The data defaults to 0x0000 if not provided as over half @@ -196,7 +196,7 @@ def __init__(self, data=0x0000, **kwargs): :param data: The data to send along with the request """ - DiagnosticStatusRequest.__init__(self, **kwargs) + DiagnosticStatusRequest.__init__(self, slave=slave, transaction=transaction, protocol=protocol, skip_encode=skip_encode) self.message = data async def execute(self, *args): @@ -213,12 +213,12 @@ class DiagnosticStatusSimpleResponse(DiagnosticStatusResponse): 2 bytes of data. """ - def __init__(self, data=0x0000, **kwargs): + def __init__(self, data=0x0000, slave=0, transaction=0, protocol=0, skip_encode=False): """Return a simple diagnostic response. :param data: The resulting data to return to the client """ - DiagnosticStatusResponse.__init__(self, **kwargs) + DiagnosticStatusResponse.__init__(self, slave=slave, transaction=transaction, protocol=protocol, skip_encode=skip_encode) self.message = data @@ -235,12 +235,12 @@ class ReturnQueryDataRequest(DiagnosticStatusRequest): sub_function_code = 0x0000 - def __init__(self, message=b"\x00\x00", slave=None, **kwargs): + def __init__(self, message=b"\x00\x00", slave=0, transaction=0, protocol=0, skip_encode=False): """Initialize a new instance of the request. :param message: The message to send to loopback """ - DiagnosticStatusRequest.__init__(self, slave=slave, **kwargs) + DiagnosticStatusRequest.__init__(self, slave=slave, transaction=transaction, protocol=protocol, skip_encode=skip_encode) if not isinstance(message, bytes): raise ModbusException(f"message({type(message)}) must be bytes") self.message = message @@ -263,12 +263,12 @@ class ReturnQueryDataResponse(DiagnosticStatusResponse): sub_function_code = 0x0000 - def __init__(self, message=b"\x00\x00", **kwargs): + def __init__(self, message=b"\x00\x00", slave=0, transaction=0, protocol=0, skip_encode=False): """Initialize a new instance of the response. :param message: The message to loopback """ - DiagnosticStatusResponse.__init__(self, **kwargs) + DiagnosticStatusResponse.__init__(self, slave=slave, transaction=transaction, protocol=protocol, skip_encode=skip_encode) if not isinstance(message, bytes): raise ModbusException(f"message({type(message)}) must be bytes") self.message = message @@ -290,12 +290,12 @@ class RestartCommunicationsOptionRequest(DiagnosticStatusRequest): sub_function_code = 0x0001 - def __init__(self, toggle=False, slave=None, **kwargs): + def __init__(self, toggle=False, slave=0, transaction=0, protocol=0, skip_encode=False): """Initialize a new request. :param toggle: Set to True to toggle, False otherwise """ - DiagnosticStatusRequest.__init__(self, slave=slave, **kwargs) + DiagnosticStatusRequest.__init__(self, slave=slave, transaction=transaction, protocol=protocol, skip_encode=skip_encode) if toggle: self.message = [ModbusStatus.ON] else: @@ -323,12 +323,12 @@ class RestartCommunicationsOptionResponse(DiagnosticStatusResponse): sub_function_code = 0x0001 - def __init__(self, toggle=False, **kwargs): + def __init__(self, toggle=False, slave=0, transaction=0, protocol=0, skip_encode=False): """Initialize a new response. :param toggle: Set to True if we toggled, False otherwise """ - DiagnosticStatusResponse.__init__(self, **kwargs) + DiagnosticStatusResponse.__init__(self, slave=slave, transaction=transaction, protocol=protocol, skip_encode=skip_encode) if toggle: self.message = [ModbusStatus.ON] else: @@ -434,9 +434,9 @@ class ForceListenOnlyModeResponse(DiagnosticStatusResponse): sub_function_code = 0x0004 should_respond = False - def __init__(self, **kwargs): + def __init__(self, slave=0, transaction=0, protocol=0, skip_encode=False): """Initialize to block a return response.""" - DiagnosticStatusResponse.__init__(self, **kwargs) + DiagnosticStatusResponse.__init__(self, slave=slave, transaction=transaction, protocol=protocol, skip_encode=skip_encode) self.message = [] @@ -816,9 +816,10 @@ class GetClearModbusPlusRequest(DiagnosticStatusSimpleRequest): sub_function_code = 0x0015 - def __init__(self, slave=None, **kwargs): + def __init__(self, data=0, slave=0, transaction=0, protocol=0, skip_encode=False): """Initialize.""" - super().__init__(slave=slave, **kwargs) + super().__init__(slave=slave, transaction=transaction, protocol=protocol, skip_encode=skip_encode) + self.message=data def get_response_pdu_size(self): """Return a series of 54 16-bit words (108 bytes) in the data field of the response. diff --git a/pymodbus/pdu/file_message.py b/pymodbus/pdu/file_message.py index 51b4ecf50..43a94afb1 100644 --- a/pymodbus/pdu/file_message.py +++ b/pymodbus/pdu/file_message.py @@ -28,7 +28,7 @@ class FileRecord: # pylint: disable=eq-without-hash """Represents a file record and its relevant data.""" - def __init__(self, **kwargs): + def __init__(self, reference_type=0x06, file_number=0x00, record_number=0x00, record_data="", record_length=None, response_length=None): """Initialize a new instance. :params reference_type: must be 0x06 @@ -38,13 +38,13 @@ def __init__(self, **kwargs): :params record_length: The length in registers of the record :params response_length: The length in bytes of the record """ - self.reference_type = kwargs.get("reference_type", 0x06) - self.file_number = kwargs.get("file_number", 0x00) - self.record_number = kwargs.get("record_number", 0x00) - self.record_data = kwargs.get("record_data", "") + self.reference_type = reference_type + self.file_number = file_number + self.record_number = record_number + self.record_data = record_data - self.record_length = kwargs.get("record_length", len(self.record_data) // 2) - self.response_length = kwargs.get("response_length", len(self.record_data) + 1) + self.record_length = record_length if record_length else len(self.record_data) // 2 + self.response_length = response_length if response_length else len(self.record_data) + 1 def __eq__(self, relf): """Compare the left object to the right.""" @@ -100,7 +100,7 @@ class ReadFileRecordRequest(ModbusRequest): function_code_name = "read_file_record" _rtu_byte_count_pos = 2 - def __init__(self, records=None, slave=0, transaction=0, protocol=0, skip_encode=False, **_kwargs): + def __init__(self, records=None, slave=0, transaction=0, protocol=0, skip_encode=False): """Initialize a new instance. :param records: The file record requests to be read @@ -165,7 +165,7 @@ class ReadFileRecordResponse(ModbusResponse): function_code = 0x14 _rtu_byte_count_pos = 2 - def __init__(self, records=None, slave=0, transaction=0, protocol=0, skip_encode=False, **_kwargs): + def __init__(self, records=None, slave=0, transaction=0, protocol=0, skip_encode=False): """Initialize a new instance. :param records: The requested file records @@ -218,7 +218,7 @@ class WriteFileRecordRequest(ModbusRequest): function_code_name = "write_file_record" _rtu_byte_count_pos = 2 - def __init__(self, records=None, slave=0, transaction=0, protocol=0, skip_encode=False, **_kwargs): + def __init__(self, records=None, slave=0, transaction=0, protocol=0, skip_encode=False): """Initialize a new instance. :param records: The file record requests to be read @@ -282,7 +282,7 @@ class WriteFileRecordResponse(ModbusResponse): function_code = 0x15 _rtu_byte_count_pos = 2 - def __init__(self, records=None, slave=0, transaction=0, protocol=0, skip_encode=False, **_kwargs): + def __init__(self, records=None, slave=0, transaction=0, protocol=0, skip_encode=False): """Initialize a new instance. :param records: The file record requests to be read @@ -347,7 +347,7 @@ class ReadFifoQueueRequest(ModbusRequest): function_code_name = "read_fifo_queue" _rtu_frame_size = 6 - def __init__(self, address=0x0000, slave=0, transaction=0, protocol=0, skip_encode=False, **_kwargs): + def __init__(self, address=0x0000, slave=0, transaction=0, protocol=0, skip_encode=False): """Initialize a new instance. :param address: The fifo pointer address (0x0000 to 0xffff) @@ -408,7 +408,7 @@ def calculateRtuFrameSize(cls, buffer): lo_byte = int(buffer[3]) return (hi_byte << 16) + lo_byte + 6 - def __init__(self, values=None, slave=0, transaction=0, protocol=0, skip_encode=False, **_kwargs): + def __init__(self, values=None, slave=0, transaction=0, protocol=0, skip_encode=False): """Initialize a new instance. :param values: The list of values of the fifo to return diff --git a/pymodbus/pdu/mei_message.py b/pymodbus/pdu/mei_message.py index c1aeac445..aaedea198 100644 --- a/pymodbus/pdu/mei_message.py +++ b/pymodbus/pdu/mei_message.py @@ -56,7 +56,7 @@ class ReadDeviceInformationRequest(ModbusRequest): function_code_name = "read_device_information" _rtu_frame_size = 7 - def __init__(self, read_code=None, object_id=0x00, slave=0, transaction=0, protocol=0, skip_encode=False, **_kwargs): + def __init__(self, read_code=None, object_id=0x00, slave=0, transaction=0, protocol=0, skip_encode=False): """Initialize a new instance. :param read_code: The device information read code @@ -134,7 +134,7 @@ def calculateRtuFrameSize(cls, buffer): except struct.error as exc: raise IndexError from exc - def __init__(self, read_code=None, information=None, slave=0, transaction=0, protocol=0, skip_encode=False, **_kwargs): + def __init__(self, read_code=None, information=None, slave=0, transaction=0, protocol=0, skip_encode=False): """Initialize a new instance. :param read_code: The device information read code diff --git a/pymodbus/pdu/other_message.py b/pymodbus/pdu/other_message.py index a6b51a793..0b251f467 100644 --- a/pymodbus/pdu/other_message.py +++ b/pymodbus/pdu/other_message.py @@ -40,7 +40,7 @@ class ReadExceptionStatusRequest(ModbusRequest): function_code_name = "read_exception_status" _rtu_frame_size = 4 - def __init__(self, slave=None, transaction=0, protocol=0, skip_encode=0, **_kwargs): + def __init__(self, slave=None, transaction=0, protocol=0, skip_encode=0): """Initialize a new instance.""" ModbusRequest.__init__(self, slave, transaction, protocol, skip_encode) @@ -82,7 +82,7 @@ class ReadExceptionStatusResponse(ModbusResponse): function_code = 0x07 _rtu_frame_size = 5 - def __init__(self, status=0x00, slave=0, transaction=0, protocol=0, skip_encode=False, **_kwargs): + def __init__(self, status=0x00, slave=0, transaction=0, protocol=0, skip_encode=False): """Initialize a new instance. :param status: The status response to report @@ -145,7 +145,7 @@ class GetCommEventCounterRequest(ModbusRequest): function_code_name = "get_event_counter" _rtu_frame_size = 4 - def __init__(self, slave=0, transaction=0, protocol=0, skip_encode=False, **_kwargs): + def __init__(self, slave=0, transaction=0, protocol=0, skip_encode=False): """Initialize a new instance.""" ModbusRequest.__init__(self, slave, transaction, protocol, skip_encode) @@ -188,7 +188,7 @@ class GetCommEventCounterResponse(ModbusResponse): function_code = 0x0B _rtu_frame_size = 8 - def __init__(self, count=0x0000, slave=0, transaction=0, protocol=0, skip_encode=False, **_kwargs): + def __init__(self, count=0x0000, slave=0, transaction=0, protocol=0, skip_encode=False): """Initialize a new instance. :param count: The current event counter value @@ -256,7 +256,7 @@ class GetCommEventLogRequest(ModbusRequest): function_code_name = "get_event_log" _rtu_frame_size = 4 - def __init__(self, slave=0, transaction=0, protocol=0, skip_encode=False, **_kwargs): + def __init__(self, slave=0, transaction=0, protocol=0, skip_encode=False): """Initialize a new instance.""" ModbusRequest.__init__(self, slave, transaction, protocol, skip_encode) @@ -303,7 +303,7 @@ class GetCommEventLogResponse(ModbusResponse): function_code = 0x0C _rtu_byte_count_pos = 2 - def __init__(self, slave=0, transaction=0, protocol=0, skip_encode=False, **kwargs): + def __init__(self, status=True, message_count=0, event_count=0, events=None, slave=0, transaction=0, protocol=0, skip_encode=False): """Initialize a new instance. :param status: The status response to report @@ -312,10 +312,10 @@ def __init__(self, slave=0, transaction=0, protocol=0, skip_encode=False, **kwar :param events: The collection of events to send """ ModbusResponse.__init__(self, slave, transaction, protocol, skip_encode) - self.status = kwargs.get("status", True) - self.message_count = kwargs.get("message_count", 0) - self.event_count = kwargs.get("event_count", 0) - self.events = kwargs.get("events", []) + self.status = status + self.message_count = message_count + self.event_count = event_count + self.events = events if events else [] def encode(self): """Encode the response. @@ -377,7 +377,7 @@ class ReportSlaveIdRequest(ModbusRequest): function_code_name = "report_slave_id" _rtu_frame_size = 4 - def __init__(self, slave=0, transaction=0, protocol=0, skip_encode=False, **_kwargs): + def __init__(self, slave=0, transaction=0, protocol=0, skip_encode=False): """Initialize a new instance. :param slave: Modbus slave slave ID @@ -436,7 +436,7 @@ class ReportSlaveIdResponse(ModbusResponse): function_code = 0x11 _rtu_byte_count_pos = 2 - def __init__(self, identifier=b"\x00", status=True, slave=0, transaction=0, protocol=0, skip_encode=False, **_kwargs): + def __init__(self, identifier=b"\x00", status=True, slave=0, transaction=0, protocol=0, skip_encode=False): """Initialize a new instance. :param identifier: The identifier of the slave diff --git a/pymodbus/pdu/register_read_message.py b/pymodbus/pdu/register_read_message.py index 58d8f9b71..c03877f94 100644 --- a/pymodbus/pdu/register_read_message.py +++ b/pymodbus/pdu/register_read_message.py @@ -22,7 +22,7 @@ class ReadRegistersRequestBase(ModbusRequest): _rtu_frame_size = 8 - def __init__(self, address, count, slave=0, transaction=0, protocol=0, skip_encode=False, **_kwargs): + def __init__(self, address, count, slave=0, transaction=0, protocol=0, skip_encode=False): """Initialize a new instance. :param address: The address to start the read from @@ -70,7 +70,7 @@ class ReadRegistersResponseBase(ModbusResponse): _rtu_byte_count_pos = 2 - def __init__(self, values, slave=0, transaction=0, protocol=0, skip_encode=False, **_kwargs): + def __init__(self, values, slave=0, transaction=0, protocol=0, skip_encode=False): """Initialize a new instance. :param values: The values to write to @@ -130,14 +130,14 @@ class ReadHoldingRegistersRequest(ReadRegistersRequestBase): function_code = 3 function_code_name = "read_holding_registers" - def __init__(self, address=None, count=None, slave=0, **kwargs): + def __init__(self, address=None, count=None, slave=0, transaction=0, protocol=0, skip_encode=0): """Initialize a new instance of the request. :param address: The starting address to read from :param count: The number of registers to read from address :param slave: Modbus slave slave ID """ - super().__init__(address, count, slave, **kwargs) + super().__init__(address, count, slave, transaction, protocol, skip_encode) async def execute(self, context): """Run a read holding request against a datastore. @@ -169,12 +169,12 @@ class ReadHoldingRegistersResponse(ReadRegistersResponseBase): function_code = 3 - def __init__(self, values=None, **kwargs): + def __init__(self, values=None, slave=None, transaction=0, protocol=0, skip_encode=0): """Initialize a new response instance. :param values: The resulting register values """ - super().__init__(values, **kwargs) + super().__init__(values, slave, transaction, protocol, skip_encode) class ReadInputRegistersRequest(ReadRegistersRequestBase): @@ -190,14 +190,14 @@ class ReadInputRegistersRequest(ReadRegistersRequestBase): function_code = 4 function_code_name = "read_input_registers" - def __init__(self, address=None, count=None, slave=0, **kwargs): + def __init__(self, address=None, count=None, slave=0, transaction=0, protocol=0, skip_encode=0): """Initialize a new instance of the request. :param address: The starting address to read from :param count: The number of registers to read from address :param slave: Modbus slave slave ID """ - super().__init__(address, count, slave, **kwargs) + super().__init__(address, count, slave, transaction, protocol, skip_encode) async def execute(self, context): """Run a read input request against a datastore. @@ -229,12 +229,12 @@ class ReadInputRegistersResponse(ReadRegistersResponseBase): function_code = 4 - def __init__(self, values=None, **kwargs): + def __init__(self, values=None, slave=None, transaction=0, protocol=0, skip_encode=0): """Initialize a new response instance. :param values: The resulting register values """ - super().__init__(values, **kwargs) + super().__init__(values, slave, transaction, protocol, skip_encode) class ReadWriteMultipleRegistersRequest(ModbusRequest): @@ -257,7 +257,7 @@ class ReadWriteMultipleRegistersRequest(ModbusRequest): function_code_name = "read_write_multiple_registers" _rtu_byte_count_pos = 10 - def __init__(self, slave=0, transaction=0, protocol=0, skip_encode=False, **kwargs): + def __init__(self, read_address=0x00, read_count=0, write_address=0x00, write_registers=None, slave=0, transaction=0, protocol=0, skip_encode=False): """Initialize a new request message. :param read_address: The address to start reading from @@ -266,10 +266,10 @@ def __init__(self, slave=0, transaction=0, protocol=0, skip_encode=False, **kwar :param write_registers: The registers to write to the specified address """ super().__init__(slave, transaction, protocol, skip_encode) - self.read_address = kwargs.get("read_address", 0x00) - self.read_count = kwargs.get("read_count", 0) - self.write_address = kwargs.get("write_address", 0x00) - self.write_registers = kwargs.get("write_registers", None) + self.read_address = read_address + self.read_count = read_count + self.write_address = write_address + self.write_registers = write_registers if not hasattr(self.write_registers, "__iter__"): self.write_registers = [self.write_registers] self.write_count = len(self.write_registers) @@ -373,7 +373,7 @@ class ReadWriteMultipleRegistersResponse(ModbusResponse): function_code = 23 _rtu_byte_count_pos = 2 - def __init__(self, values=None, slave=0, transaction=0, protocol=0, skip_encode=False, **_kwargs): + def __init__(self, values=None, slave=0, transaction=0, protocol=0, skip_encode=False): """Initialize a new instance. :param values: The register values to write diff --git a/pymodbus/pdu/register_write_message.py b/pymodbus/pdu/register_write_message.py index 32521acc7..18b12a51b 100644 --- a/pymodbus/pdu/register_write_message.py +++ b/pymodbus/pdu/register_write_message.py @@ -28,7 +28,7 @@ class WriteSingleRegisterRequest(ModbusRequest): function_code_name = "write_register" _rtu_frame_size = 8 - def __init__(self, address=None, value=None, slave=None, transaction=0, protocol=0, skip_encode=0, **_kwargs): + def __init__(self, address=None, value=None, slave=None, transaction=0, protocol=0, skip_encode=0): """Initialize a new instance. :param address: The address to start writing add @@ -99,7 +99,7 @@ class WriteSingleRegisterResponse(ModbusResponse): function_code = 6 _rtu_frame_size = 8 - def __init__(self, address=None, value=None, slave=0, transaction=0, protocol=0, skip_encode=False, **_kwargs): + def __init__(self, address=None, value=None, slave=0, transaction=0, protocol=0, skip_encode=False): """Initialize a new instance. :param address: The address to start writing add @@ -160,7 +160,7 @@ class WriteMultipleRegistersRequest(ModbusRequest): _rtu_byte_count_pos = 6 _pdu_length = 5 # func + adress1 + adress2 + outputQuant1 + outputQuant2 - def __init__(self, address=None, values=None, slave=None, transaction=0, protocol=0, skip_encode=0, **_kwargs): + def __init__(self, address=None, values=None, slave=None, transaction=0, protocol=0, skip_encode=0): """Initialize a new instance. :param address: The address to start writing to @@ -247,7 +247,7 @@ class WriteMultipleRegistersResponse(ModbusResponse): function_code = 16 _rtu_frame_size = 8 - def __init__(self, address=None, count=None, slave=0, transaction=0, protocol=0, skip_encode=False, **_kwargs): + def __init__(self, address=None, count=None, slave=0, transaction=0, protocol=0, skip_encode=False): """Initialize a new instance. :param address: The address to start writing to @@ -295,7 +295,7 @@ class MaskWriteRegisterRequest(ModbusRequest): function_code_name = "mask_write_register" _rtu_frame_size = 10 - def __init__(self, address=0x0000, and_mask=0xFFFF, or_mask=0x0000, slave=0, transaction=0, protocol=0, skip_encode=False, **_kwargs): + def __init__(self, address=0x0000, and_mask=0xFFFF, or_mask=0x0000, slave=0, transaction=0, protocol=0, skip_encode=False): """Initialize a new instance. :param address: The mask pointer address (0x0000 to 0xffff) @@ -350,7 +350,7 @@ class MaskWriteRegisterResponse(ModbusResponse): function_code = 0x16 _rtu_frame_size = 10 - def __init__(self, address=0x0000, and_mask=0xFFFF, or_mask=0x0000, slave=0, transaction=0, protocol=0, skip_encode=False, **_kwargs): + def __init__(self, address=0x0000, and_mask=0xFFFF, or_mask=0x0000, slave=0, transaction=0, protocol=0, skip_encode=False): """Initialize new instance. :param address: The mask pointer address (0x0000 to 0xffff) From 72e3399d41c20719ac3e63412fd1be2b8cca3840 Mon Sep 17 00:00:00 2001 From: jan iversen Date: Fri, 19 Jul 2024 20:00:24 +0200 Subject: [PATCH 71/88] Remove kwargs client. (#2243) --- pymodbus/client/base.py | 20 +- pymodbus/client/mixin.py | 212 +++++++----------- pymodbus/client/modbusclientprotocol.py | 2 +- pymodbus/client/serial.py | 4 +- pymodbus/client/tcp.py | 8 +- pymodbus/client/tls.py | 4 +- pymodbus/client/udp.py | 4 +- pymodbus/pdu/bit_read_message.py | 12 +- pymodbus/pdu/bit_write_message.py | 6 - pymodbus/pdu/diag_message.py | 38 ---- pymodbus/pdu/file_message.py | 11 - pymodbus/pdu/mei_message.py | 4 - pymodbus/pdu/other_message.py | 10 - pymodbus/pdu/pdu.py | 7 - pymodbus/pdu/register_read_message.py | 9 - pymodbus/pdu/register_write_message.py | 8 - .../test_bit_read_messages.py | 38 ++-- 17 files changed, 125 insertions(+), 272 deletions(-) diff --git a/pymodbus/client/base.py b/pymodbus/client/base.py index 5475546e8..51602641b 100644 --- a/pymodbus/client/base.py +++ b/pymodbus/client/base.py @@ -16,7 +16,7 @@ from pymodbus.logging import Log from pymodbus.pdu import ModbusRequest, ModbusResponse from pymodbus.transaction import SyncModbusTransactionManager -from pymodbus.transport import CommParams +from pymodbus.transport import CommParams, CommType from pymodbus.utilities import ModbusTransactionState @@ -37,6 +37,7 @@ class ModbusBaseClient(ModbusClientMixin[Awaitable[ModbusResponse]]): :param reconnect_delay_max: Maximum delay in seconds.milliseconds before reconnecting. :param on_connect_callback: Will be called when connected/disconnected (bool parameter) :param no_resend_on_retry: Do not resend request when retrying due to missing response. + :param comm_type: Type of communication (set by interface class) :param kwargs: Experimental parameters. .. tip:: @@ -49,7 +50,7 @@ class ModbusBaseClient(ModbusClientMixin[Awaitable[ModbusResponse]]): **Application methods, common to all clients**: """ - def __init__( + def __init__( # pylint: disable=too-many-arguments self, framer: FramerType, timeout: float = 3, @@ -59,6 +60,8 @@ def __init__( reconnect_delay_max: float = 300, on_connect_callback: Callable[[bool], None] | None = None, no_resend_on_retry: bool = False, + comm_type: CommType | None = None, + source_address: tuple[str, int] | None = None, **kwargs: Any, ) -> None: """Initialize a client instance.""" @@ -66,9 +69,9 @@ def __init__( self.ctx = ModbusClientProtocol( framer, CommParams( - comm_type=kwargs.get("CommType"), + comm_type=comm_type, comm_name="comm", - source_address=kwargs.get("source_address", None), + source_address=source_address, reconnect_delay=reconnect_delay, reconnect_delay_max=reconnect_delay_max, timeout_connect=timeout, @@ -238,6 +241,7 @@ class ModbusBaseSyncClient(ModbusClientMixin[ModbusResponse]): :param reconnect_delay: Minimum delay in seconds.milliseconds before reconnecting. :param reconnect_delay_max: Maximum delay in seconds.milliseconds before reconnecting. :param no_resend_on_retry: Do not resend request when retrying due to missing response. + :param source_address: source address of client :param kwargs: Experimental parameters. .. tip:: @@ -260,7 +264,7 @@ class _params: reconnect_delay: int | None = None source_address: tuple[str, int] | None = None - def __init__( + def __init__( # pylint: disable=too-many-arguments self, framer: FramerType, timeout: float = 3, @@ -270,14 +274,16 @@ def __init__( reconnect_delay: float = 0.1, reconnect_delay_max: float = 300.0, no_resend_on_retry: bool = False, + comm_type: CommType | None = None, + source_address: tuple[str, int] | None = None, **kwargs: Any, ) -> None: """Initialize a client instance.""" ModbusClientMixin.__init__(self) # type: ignore[arg-type] self.comm_params = CommParams( - comm_type=kwargs.get("CommType"), + comm_type=comm_type, comm_name="comm", - source_address=kwargs.get("source_address", None), + source_address=source_address, reconnect_delay=reconnect_delay, reconnect_delay_max=reconnect_delay_max, timeout_connect=timeout, diff --git a/pymodbus/client/mixin.py b/pymodbus/client/mixin.py index 40391de61..7f0d710a6 100644 --- a/pymodbus/client/mixin.py +++ b/pymodbus/client/mixin.py @@ -3,7 +3,7 @@ import struct from enum import Enum -from typing import Any, Generic, TypeVar +from typing import Generic, TypeVar import pymodbus.pdu.bit_read_message as pdu_bit_read import pymodbus.pdu.bit_write_message as pdu_bit_write @@ -59,12 +59,9 @@ def execute(self, _request: ModbusRequest) -> T: .. tip:: Response is not interpreted. """ - raise NotImplementedError( - "The execute method of ModbusClientMixin needs to be overridden and cannot be used directly" - ) + raise NotImplementedError("execute of ModbusClientMixin needs to be overridden") - def read_coils( - self, address: int, count: int = 1, slave: int = 0) -> T: + def read_coils(self, address: int, count: int = 1, slave: int = 0) -> T: """Read coils (code 0x01). :param address: Start address to read from @@ -72,12 +69,9 @@ def read_coils( :param slave: (optional) Modbus slave ID :raises ModbusException: """ - return self.execute( - pdu_bit_read.ReadCoilsRequest(address, count, slave) - ) + return self.execute(pdu_bit_read.ReadCoilsRequest(address, count, slave=slave)) - def read_discrete_inputs( - self, address: int, count: int = 1, slave: int = 0) -> T: + def read_discrete_inputs(self, address: int, count: int = 1, slave: int = 0) -> T: """Read discrete inputs (code 0x02). :param address: Start address to read from @@ -85,12 +79,9 @@ def read_discrete_inputs( :param slave: (optional) Modbus slave ID :raises ModbusException: """ - return self.execute( - pdu_bit_read.ReadDiscreteInputsRequest(address, count, slave) - ) + return self.execute(pdu_bit_read.ReadDiscreteInputsRequest(address, count, slave=slave)) - def read_holding_registers( - self, address: int, count: int = 1, slave: int = 0) -> T: + def read_holding_registers(self, address: int, count: int = 1, slave: int = 0) -> T: """Read holding registers (code 0x03). :param address: Start address to read from @@ -98,338 +89,295 @@ def read_holding_registers( :param slave: (optional) Modbus slave ID :raises ModbusException: """ - return self.execute( - pdu_reg_read.ReadHoldingRegistersRequest(address, count, slave) - ) + return self.execute(pdu_reg_read.ReadHoldingRegistersRequest(address, count, slave=slave)) - def read_input_registers( - self, address: int, count: int = 1, slave: int = 0, **kwargs: Any - ) -> T: + def read_input_registers(self, address: int, count: int = 1, slave: int = 0) -> T: """Read input registers (code 0x04). :param address: Start address to read from :param count: (optional) Number of coils to read :param slave: (optional) Modbus slave ID - :param kwargs: (optional) Experimental parameters. :raises ModbusException: """ - return self.execute( - pdu_reg_read.ReadInputRegistersRequest(address, count, slave, **kwargs) - ) + return self.execute(pdu_reg_read.ReadInputRegistersRequest(address, count, slave=slave)) - def write_coil(self, address: int, value: bool, slave: int = 0, **kwargs: Any) -> T: + def write_coil(self, address: int, value: bool, slave: int = 0) -> T: """Write single coil (code 0x05). :param address: Address to write to :param value: Boolean to write :param slave: (optional) Modbus slave ID - :param kwargs: (optional) Experimental parameters. :raises ModbusException: """ - return self.execute( - pdu_bit_write.WriteSingleCoilRequest(address, value, slave, **kwargs) - ) + return self.execute(pdu_bit_write.WriteSingleCoilRequest(address, value, slave=slave)) - def write_register( - self, address: int, value: int, slave: int = 0, **kwargs: Any - ) -> T: + def write_register(self, address: int, value: int, slave: int = 0) -> T: """Write register (code 0x06). :param address: Address to write to :param value: Value to write :param slave: (optional) Modbus slave ID - :param kwargs: (optional) Experimental parameters. :raises ModbusException: """ - return self.execute( - pdu_req_write.WriteSingleRegisterRequest(address, value, slave, **kwargs) - ) + return self.execute(pdu_req_write.WriteSingleRegisterRequest(address, value, slave=slave)) - def read_exception_status(self, slave: int = 0, **kwargs: Any) -> T: + def read_exception_status(self, slave: int = 0) -> T: """Read Exception Status (code 0x07). :param slave: (optional) Modbus slave ID - :param kwargs: (optional) Experimental parameters. :raises ModbusException: """ - return self.execute(pdu_other_msg.ReadExceptionStatusRequest(slave, **kwargs)) + return self.execute(pdu_other_msg.ReadExceptionStatusRequest(slave=slave)) def diag_query_data( - self, msg: bytes, slave: int = 0, **kwargs: Any - ) -> T: + self, msg: bytes, slave: int = 0) -> T: """Diagnose query data (code 0x08 sub 0x00). :param msg: Message to be returned :param slave: (optional) Modbus slave ID - :param kwargs: (optional) Experimental parameters. :raises ModbusException: """ - return self.execute(pdu_diag.ReturnQueryDataRequest(msg, slave=slave, **kwargs)) + return self.execute(pdu_diag.ReturnQueryDataRequest(msg, slave=slave)) def diag_restart_communication( - self, toggle: bool, slave: int = 0, **kwargs: Any - ) -> T: + self, toggle: bool, slave: int = 0) -> T: """Diagnose restart communication (code 0x08 sub 0x01). :param toggle: True if toggled. :param slave: (optional) Modbus slave ID - :param kwargs: (optional) Experimental parameters. :raises ModbusException: """ return self.execute( - pdu_diag.RestartCommunicationsOptionRequest(toggle, slave=slave, **kwargs) + pdu_diag.RestartCommunicationsOptionRequest(toggle, slave=slave) ) - def diag_read_diagnostic_register(self, slave: int = 0, **kwargs: Any) -> T: + def diag_read_diagnostic_register(self, slave: int = 0) -> T: """Diagnose read diagnostic register (code 0x08 sub 0x02). :param slave: (optional) Modbus slave ID - :param kwargs: (optional) Experimental parameters. :raises ModbusException: """ return self.execute( - pdu_diag.ReturnDiagnosticRegisterRequest(slave=slave, **kwargs) + pdu_diag.ReturnDiagnosticRegisterRequest(slave=slave) ) - def diag_change_ascii_input_delimeter(self, slave: int = 0, **kwargs: Any) -> T: + def diag_change_ascii_input_delimeter(self, slave: int = 0) -> T: """Diagnose change ASCII input delimiter (code 0x08 sub 0x03). :param slave: (optional) Modbus slave ID - :param kwargs: (optional) Experimental parameters. :raises ModbusException: """ return self.execute( - pdu_diag.ChangeAsciiInputDelimiterRequest(slave=slave, **kwargs) + pdu_diag.ChangeAsciiInputDelimiterRequest(slave=slave) ) - def diag_force_listen_only(self, slave: int = 0, **kwargs: Any) -> T: + def diag_force_listen_only(self, slave: int = 0) -> T: """Diagnose force listen only (code 0x08 sub 0x04). :param slave: (optional) Modbus slave ID - :param kwargs: (optional) Experimental parameters. :raises ModbusException: """ - return self.execute(pdu_diag.ForceListenOnlyModeRequest(slave=slave, **kwargs)) + return self.execute(pdu_diag.ForceListenOnlyModeRequest(slave=slave)) - def diag_clear_counters(self, slave: int = 0, **kwargs: Any) -> T: + def diag_clear_counters(self, slave: int = 0) -> T: """Diagnose clear counters (code 0x08 sub 0x0A). :param slave: (optional) Modbus slave ID - :param kwargs: (optional) Experimental parameters. :raises ModbusException: """ - return self.execute(pdu_diag.ClearCountersRequest(slave=slave, **kwargs)) + return self.execute(pdu_diag.ClearCountersRequest(slave=slave)) - def diag_read_bus_message_count(self, slave: int = 0, **kwargs: Any) -> T: + def diag_read_bus_message_count(self, slave: int = 0) -> T: """Diagnose read bus message count (code 0x08 sub 0x0B). :param slave: (optional) Modbus slave ID - :param kwargs: (optional) Experimental parameters. :raises ModbusException: """ return self.execute( - pdu_diag.ReturnBusMessageCountRequest(slave=slave, **kwargs) + pdu_diag.ReturnBusMessageCountRequest(slave=slave) ) - def diag_read_bus_comm_error_count(self, slave: int = 0, **kwargs: Any) -> T: + def diag_read_bus_comm_error_count(self, slave: int = 0) -> T: """Diagnose read Bus Communication Error Count (code 0x08 sub 0x0C). :param slave: (optional) Modbus slave ID - :param kwargs: (optional) Experimental parameters. :raises ModbusException: """ return self.execute( - pdu_diag.ReturnBusCommunicationErrorCountRequest(slave=slave, **kwargs) + pdu_diag.ReturnBusCommunicationErrorCountRequest(slave=slave) ) - def diag_read_bus_exception_error_count(self, slave: int = 0, **kwargs: Any) -> T: + def diag_read_bus_exception_error_count(self, slave: int = 0) -> T: """Diagnose read Bus Exception Error Count (code 0x08 sub 0x0D). :param slave: (optional) Modbus slave ID - :param kwargs: (optional) Experimental parameters. :raises ModbusException: """ return self.execute( - pdu_diag.ReturnBusExceptionErrorCountRequest(slave=slave, **kwargs) + pdu_diag.ReturnBusExceptionErrorCountRequest(slave=slave) ) - def diag_read_slave_message_count(self, slave: int = 0, **kwargs: Any) -> T: + def diag_read_slave_message_count(self, slave: int = 0) -> T: """Diagnose read Slave Message Count (code 0x08 sub 0x0E). :param slave: (optional) Modbus slave ID - :param kwargs: (optional) Experimental parameters. :raises ModbusException: """ return self.execute( - pdu_diag.ReturnSlaveMessageCountRequest(slave=slave, **kwargs) + pdu_diag.ReturnSlaveMessageCountRequest(slave=slave) ) - def diag_read_slave_no_response_count(self, slave: int = 0, **kwargs: Any) -> T: + def diag_read_slave_no_response_count(self, slave: int = 0) -> T: """Diagnose read Slave No Response Count (code 0x08 sub 0x0F). :param slave: (optional) Modbus slave ID - :param kwargs: (optional) Experimental parameters. :raises ModbusException: """ return self.execute( - pdu_diag.ReturnSlaveNoResponseCountRequest(slave=slave, **kwargs) + pdu_diag.ReturnSlaveNoResponseCountRequest(slave=slave) ) - def diag_read_slave_nak_count(self, slave: int = 0, **kwargs: Any) -> T: + def diag_read_slave_nak_count(self, slave: int = 0) -> T: """Diagnose read Slave NAK Count (code 0x08 sub 0x10). :param slave: (optional) Modbus slave ID - :param kwargs: (optional) Experimental parameters. :raises ModbusException: """ - return self.execute(pdu_diag.ReturnSlaveNAKCountRequest(slave=slave, **kwargs)) + return self.execute(pdu_diag.ReturnSlaveNAKCountRequest(slave=slave)) - def diag_read_slave_busy_count(self, slave: int = 0, **kwargs: Any) -> T: + def diag_read_slave_busy_count(self, slave: int = 0) -> T: """Diagnose read Slave Busy Count (code 0x08 sub 0x11). :param slave: (optional) Modbus slave ID - :param kwargs: (optional) Experimental parameters. :raises ModbusException: """ - return self.execute(pdu_diag.ReturnSlaveBusyCountRequest(slave=slave, **kwargs)) + return self.execute(pdu_diag.ReturnSlaveBusyCountRequest(slave=slave)) - def diag_read_bus_char_overrun_count(self, slave: int = 0, **kwargs: Any) -> T: + def diag_read_bus_char_overrun_count(self, slave: int = 0) -> T: """Diagnose read Bus Character Overrun Count (code 0x08 sub 0x12). :param slave: (optional) Modbus slave ID - :param kwargs: (optional) Experimental parameters. :raises ModbusException: """ return self.execute( - pdu_diag.ReturnSlaveBusCharacterOverrunCountRequest(slave=slave, **kwargs) + pdu_diag.ReturnSlaveBusCharacterOverrunCountRequest(slave=slave) ) - def diag_read_iop_overrun_count(self, slave: int = 0, **kwargs: Any) -> T: + def diag_read_iop_overrun_count(self, slave: int = 0) -> T: """Diagnose read Iop overrun count (code 0x08 sub 0x13). :param slave: (optional) Modbus slave ID - :param kwargs: (optional) Experimental parameters. :raises ModbusException: """ return self.execute( - pdu_diag.ReturnIopOverrunCountRequest(slave=slave, **kwargs) + pdu_diag.ReturnIopOverrunCountRequest(slave=slave) ) - def diag_clear_overrun_counter(self, slave: int = 0, **kwargs: Any) -> T: + def diag_clear_overrun_counter(self, slave: int = 0) -> T: """Diagnose Clear Overrun Counter and Flag (code 0x08 sub 0x14). :param slave: (optional) Modbus slave ID - :param kwargs: (optional) Experimental parameters. :raises ModbusException: """ - return self.execute(pdu_diag.ClearOverrunCountRequest(slave=slave, **kwargs)) + return self.execute(pdu_diag.ClearOverrunCountRequest(slave=slave)) - def diag_getclear_modbus_response(self, slave: int = 0, **kwargs: Any) -> T: + def diag_getclear_modbus_response(self, slave: int = 0) -> T: """Diagnose Get/Clear modbus plus (code 0x08 sub 0x15). :param slave: (optional) Modbus slave ID - :param kwargs: (optional) Experimental parameters. :raises ModbusException: """ - return self.execute(pdu_diag.GetClearModbusPlusRequest(slave=slave, **kwargs)) + return self.execute(pdu_diag.GetClearModbusPlusRequest(slave=slave)) - def diag_get_comm_event_counter(self, **kwargs: Any) -> T: + def diag_get_comm_event_counter(self, slave: int = 0) -> T: """Diagnose get event counter (code 0x0B). - :param kwargs: (optional) Experimental parameters. :raises ModbusException: """ - return self.execute(pdu_other_msg.GetCommEventCounterRequest(**kwargs)) + return self.execute(pdu_other_msg.GetCommEventCounterRequest(slave=slave)) - def diag_get_comm_event_log(self, **kwargs: Any) -> T: + def diag_get_comm_event_log(self, slave: int = 0) -> T: """Diagnose get event counter (code 0x0C). - :param kwargs: (optional) Experimental parameters. :raises ModbusException: """ - return self.execute(pdu_other_msg.GetCommEventLogRequest(**kwargs)) + return self.execute(pdu_other_msg.GetCommEventLogRequest(slave=slave)) def write_coils( self, address: int, values: list[bool] | bool, slave: int = 0, - **kwargs: Any, ) -> T: """Write coils (code 0x0F). :param address: Start address to write to :param values: List of booleans to write, or a single boolean to write :param slave: (optional) Modbus slave ID - :param kwargs: (optional) Experimental parameters. :raises ModbusException: """ return self.execute( - pdu_bit_write.WriteMultipleCoilsRequest(address, values, slave, **kwargs) + pdu_bit_write.WriteMultipleCoilsRequest(address, values, slave) ) def write_registers( - self, address: int, values: list[int] | int, slave: int = 0, **kwargs: Any - ) -> T: + self, address: int, values: list[int] | int, slave: int = 0, skip_encode: bool = False) -> T: """Write registers (code 0x10). :param address: Start address to write to :param values: List of values to write, or a single value to write :param slave: (optional) Modbus slave ID - :param kwargs: (optional) Experimental parameters. + :param skip_encode: (optional) do not encode values :raises ModbusException: """ return self.execute( - pdu_req_write.WriteMultipleRegistersRequest( - address, values, slave, **kwargs - ) + pdu_req_write.WriteMultipleRegistersRequest(address, values, slave=slave, skip_encode=skip_encode) ) - def report_slave_id(self, slave: int = 0, **kwargs: Any) -> T: + def report_slave_id(self, slave: int = 0) -> T: """Report slave ID (code 0x11). :param slave: (optional) Modbus slave ID - :param kwargs: (optional) Experimental parameters. :raises ModbusException: """ - return self.execute(pdu_other_msg.ReportSlaveIdRequest(slave, **kwargs)) + return self.execute(pdu_other_msg.ReportSlaveIdRequest(slave=slave)) - def read_file_record(self, records: list[tuple], **kwargs: Any) -> T: + def read_file_record(self, records: list[tuple], slave: int = 0) -> T: """Read file record (code 0x14). :param records: List of (Reference type, File number, Record Number, Record Length) - :param kwargs: (optional) Experimental parameters. + :param slave: device id :raises ModbusException: """ - return self.execute(pdu_file_msg.ReadFileRecordRequest(records, **kwargs)) + return self.execute(pdu_file_msg.ReadFileRecordRequest(records, slave=slave)) - def write_file_record(self, records: list[tuple], **kwargs: Any) -> T: + def write_file_record(self, records: list[tuple], slave: int = 0) -> T: """Write file record (code 0x15). :param records: List of (Reference type, File number, Record Number, Record Length) - :param kwargs: (optional) Experimental parameters. + :param slave: (optional) Device id :raises ModbusException: """ - return self.execute(pdu_file_msg.WriteFileRecordRequest(records, **kwargs)) + return self.execute(pdu_file_msg.WriteFileRecordRequest(records, slave=slave)) def mask_write_register( self, address: int = 0x0000, and_mask: int = 0xFFFF, or_mask: int = 0x0000, - **kwargs: Any, + slave: int = 0, ) -> T: """Mask write register (code 0x16). :param address: The mask pointer address (0x0000 to 0xffff) :param and_mask: The and bitmask to apply to the register address :param or_mask: The or bitmask to apply to the register address - :param kwargs: (optional) Experimental parameters. + :param slave: (optional) device id :raises ModbusException: """ return self.execute( - pdu_req_write.MaskWriteRegisterRequest(address, and_mask, or_mask, **kwargs) + pdu_req_write.MaskWriteRegisterRequest(address, and_mask, or_mask, slave=slave) ) def readwrite_registers( @@ -437,21 +385,23 @@ def readwrite_registers( read_address: int = 0, read_count: int = 0, write_address: int = 0, + address: int | None = None, values: list[int] | int = 0, slave: int = 0, - **kwargs, ) -> T: """Read/Write registers (code 0x17). :param read_address: The address to start reading from :param read_count: The number of registers to read from address :param write_address: The address to start writing to + :param address: (optional) use as read/write address :param values: List of values to write, or a single value to write :param slave: (optional) Modbus slave ID - :param kwargs: :raises ModbusException: """ - read_address = kwargs.pop("address", read_address) + if address: + read_address = address + write_address = address return self.execute( pdu_reg_read.ReadWriteMultipleRegistersRequest( read_address=read_address, @@ -459,33 +409,31 @@ def readwrite_registers( write_address=write_address, write_registers=values, slave=slave, - **kwargs ) ) - def read_fifo_queue(self, address: int = 0x0000, **kwargs: Any) -> T: + def read_fifo_queue(self, address: int = 0x0000, slave: int = 0) -> T: """Read FIFO queue (code 0x18). :param address: The address to start reading from - :param kwargs: + :param slave: (optional) device id :raises ModbusException: """ - return self.execute(pdu_file_msg.ReadFifoQueueRequest(address, **kwargs)) + return self.execute(pdu_file_msg.ReadFifoQueueRequest(address, slave=slave)) # code 0x2B sub 0x0D: CANopen General Reference Request and Response, NOT IMPLEMENTED def read_device_information( - self, read_code: int | None = None, object_id: int = 0x00, **kwargs: Any - ) -> T: + self, read_code: int | None = None, object_id: int = 0x00, slave: int = 0) -> T: """Read FIFO queue (code 0x2B sub 0x0E). :param read_code: The device information read code :param object_id: The object to read from - :param kwargs: + :param slave: (optional) Device id :raises ModbusException: """ return self.execute( - pdu_mei.ReadDeviceInformationRequest(read_code, object_id, **kwargs) + pdu_mei.ReadDeviceInformationRequest(read_code, object_id, slave=slave) ) # ------------------ diff --git a/pymodbus/client/modbusclientprotocol.py b/pymodbus/client/modbusclientprotocol.py index c5d935076..4412462fa 100644 --- a/pymodbus/client/modbusclientprotocol.py +++ b/pymodbus/client/modbusclientprotocol.py @@ -45,7 +45,7 @@ def __init__( )(ClientDecoder(), self) self.transaction = ModbusTransactionManager() - def _handle_response(self, reply, **_kwargs): + def _handle_response(self, reply): """Handle the processed response and link to correct deferred.""" if reply is not None: tid = reply.transaction_id diff --git a/pymodbus/client/serial.py b/pymodbus/client/serial.py index 2872b7d57..2972bcde0 100644 --- a/pymodbus/client/serial.py +++ b/pymodbus/client/serial.py @@ -84,7 +84,7 @@ def __init__( ModbusBaseClient.__init__( self, framer, - CommType=CommType.SERIAL, + comm_type=CommType.SERIAL, host=port, baudrate=baudrate, bytesize=bytesize, @@ -161,7 +161,7 @@ def __init__( """Initialize Modbus Serial Client.""" super().__init__( framer, - CommType=CommType.SERIAL, + comm_type=CommType.SERIAL, host=port, baudrate=baudrate, bytesize=bytesize, diff --git a/pymodbus/client/tcp.py b/pymodbus/client/tcp.py index d975482eb..e6ff7bf1f 100644 --- a/pymodbus/client/tcp.py +++ b/pymodbus/client/tcp.py @@ -63,8 +63,8 @@ def __init__( **kwargs: Any, ) -> None: """Initialize Asyncio Modbus TCP Client.""" - if "CommType" not in kwargs: - kwargs["CommType"] = CommType.TCP + if "comm_type" not in kwargs: + kwargs["comm_type"] = CommType.TCP if source_address: kwargs["source_address"] = source_address ModbusBaseClient.__init__( @@ -132,8 +132,8 @@ def __init__( **kwargs: Any, ) -> None: """Initialize Modbus TCP Client.""" - if "CommType" not in kwargs: - kwargs["CommType"] = CommType.TCP + if "comm_type" not in kwargs: + kwargs["comm_type"] = CommType.TCP super().__init__( framer, host=host, diff --git a/pymodbus/client/tls.py b/pymodbus/client/tls.py index e8bfce508..e90640004 100644 --- a/pymodbus/client/tls.py +++ b/pymodbus/client/tls.py @@ -67,7 +67,7 @@ def __init__( host, port=port, framer=framer, - CommType=CommType.TLS, + comm_type=CommType.TLS, sslctx=sslctx, **kwargs, ) @@ -150,7 +150,7 @@ def __init__( ): """Initialize Modbus TLS Client.""" super().__init__( - host, CommType=CommType.TLS, port=port, framer=framer, **kwargs + host, comm_type=CommType.TLS, port=port, framer=framer, **kwargs ) self.sslctx = sslctx self.server_hostname = server_hostname diff --git a/pymodbus/client/udp.py b/pymodbus/client/udp.py index acadf1ab5..4d71e240e 100644 --- a/pymodbus/client/udp.py +++ b/pymodbus/client/udp.py @@ -65,7 +65,7 @@ def __init__( ModbusBaseClient.__init__( self, framer, - CommType=CommType.UDP, + comm_type=CommType.UDP, host=host, port=port, **kwargs, @@ -134,7 +134,7 @@ def __init__( framer, port=port, host=host, - CommType=CommType.UDP, + comm_type=CommType.UDP, **kwargs, ) self.params.source_address = source_address diff --git a/pymodbus/pdu/bit_read_message.py b/pymodbus/pdu/bit_read_message.py index a7a8877a5..3e22de6e9 100644 --- a/pymodbus/pdu/bit_read_message.py +++ b/pymodbus/pdu/bit_read_message.py @@ -1,13 +1,5 @@ """Bit Reading Request/Response messages.""" -__all__ = [ - "ReadBitsResponseBase", - "ReadCoilsRequest", - "ReadCoilsResponse", - "ReadDiscreteInputsRequest", - "ReadDiscreteInputsResponse", -] - # pylint: disable=missing-type-doc import struct @@ -21,7 +13,7 @@ class ReadBitsRequestBase(ModbusRequest): _rtu_frame_size = 8 - def __init__(self, address, count, slave=0, transaction=0, protocol=0, skip_encode=False): + def __init__(self, address, count, slave, transaction, protocol, skip_encode): """Initialize the read request data. :param address: The start address to read from @@ -75,7 +67,7 @@ class ReadBitsResponseBase(ModbusResponse): _rtu_byte_count_pos = 2 - def __init__(self, values, slave=0, transaction=0, protocol=0, skip_encode=False): + def __init__(self, values, slave, transaction, protocol, skip_encode): """Initialize a new instance. :param values: The requested values to be returned diff --git a/pymodbus/pdu/bit_write_message.py b/pymodbus/pdu/bit_write_message.py index 04c2d5c6a..7a7e04522 100644 --- a/pymodbus/pdu/bit_write_message.py +++ b/pymodbus/pdu/bit_write_message.py @@ -3,12 +3,6 @@ TODO write mask request/response """ -__all__ = [ - "WriteSingleCoilRequest", - "WriteSingleCoilResponse", - "WriteMultipleCoilsRequest", - "WriteMultipleCoilsResponse", -] # pylint: disable=missing-type-doc import struct diff --git a/pymodbus/pdu/diag_message.py b/pymodbus/pdu/diag_message.py index 68f5ee0d4..1966fc85b 100644 --- a/pymodbus/pdu/diag_message.py +++ b/pymodbus/pdu/diag_message.py @@ -4,44 +4,6 @@ or linked to the appropriate data """ -__all__ = [ - "DiagnosticStatusRequest", - "DiagnosticStatusResponse", - "ReturnQueryDataRequest", - "ReturnQueryDataResponse", - "RestartCommunicationsOptionRequest", - "RestartCommunicationsOptionResponse", - "ReturnDiagnosticRegisterRequest", - "ReturnDiagnosticRegisterResponse", - "ChangeAsciiInputDelimiterRequest", - "ChangeAsciiInputDelimiterResponse", - "ForceListenOnlyModeRequest", - "ForceListenOnlyModeResponse", - "ClearCountersRequest", - "ClearCountersResponse", - "ReturnBusMessageCountRequest", - "ReturnBusMessageCountResponse", - "ReturnBusCommunicationErrorCountRequest", - "ReturnBusCommunicationErrorCountResponse", - "ReturnBusExceptionErrorCountRequest", - "ReturnBusExceptionErrorCountResponse", - "ReturnSlaveMessageCountRequest", - "ReturnSlaveMessageCountResponse", - "ReturnSlaveNoResponseCountRequest", - "ReturnSlaveNoResponseCountResponse", - "ReturnSlaveNAKCountRequest", - "ReturnSlaveNAKCountResponse", - "ReturnSlaveBusyCountRequest", - "ReturnSlaveBusyCountResponse", - "ReturnSlaveBusCharacterOverrunCountRequest", - "ReturnSlaveBusCharacterOverrunCountResponse", - "ReturnIopOverrunCountRequest", - "ReturnIopOverrunCountResponse", - "ClearOverrunCountRequest", - "ClearOverrunCountResponse", - "GetClearModbusPlusRequest", - "GetClearModbusPlusResponse", -] # pylint: disable=missing-type-doc import struct diff --git a/pymodbus/pdu/file_message.py b/pymodbus/pdu/file_message.py index 43a94afb1..70f40e43c 100644 --- a/pymodbus/pdu/file_message.py +++ b/pymodbus/pdu/file_message.py @@ -4,17 +4,6 @@ """ from __future__ import annotations - -__all__ = [ - "FileRecord", - "ReadFileRecordRequest", - "ReadFileRecordResponse", - "WriteFileRecordRequest", - "WriteFileRecordResponse", - "ReadFifoQueueRequest", - "ReadFifoQueueResponse", -] - # pylint: disable=missing-type-doc import struct diff --git a/pymodbus/pdu/mei_message.py b/pymodbus/pdu/mei_message.py index aaedea198..60eb28435 100644 --- a/pymodbus/pdu/mei_message.py +++ b/pymodbus/pdu/mei_message.py @@ -1,9 +1,5 @@ """Encapsulated Interface (MEI) Transport Messages.""" -__all__ = [ - "ReadDeviceInformationRequest", - "ReadDeviceInformationResponse", -] # pylint: disable=missing-type-doc import struct diff --git a/pymodbus/pdu/other_message.py b/pymodbus/pdu/other_message.py index 0b251f467..60e55b7f1 100644 --- a/pymodbus/pdu/other_message.py +++ b/pymodbus/pdu/other_message.py @@ -3,16 +3,6 @@ Currently not all implemented """ -__all__ = [ - "ReadExceptionStatusRequest", - "ReadExceptionStatusResponse", - "GetCommEventCounterRequest", - "GetCommEventCounterResponse", - "GetCommEventLogRequest", - "GetCommEventLogResponse", - "ReportSlaveIdRequest", - "ReportSlaveIdResponse", -] # pylint: disable=missing-type-doc import struct diff --git a/pymodbus/pdu/pdu.py b/pymodbus/pdu/pdu.py index f11f681c6..47c15c8b9 100644 --- a/pymodbus/pdu/pdu.py +++ b/pymodbus/pdu/pdu.py @@ -1,12 +1,5 @@ """Contains base classes for modbus request/response/error packets.""" -__all__ = [ - "ModbusRequest", - "ModbusResponse", - "ModbusExceptions", - "ExceptionResponse", - "IllegalFunctionRequest", -] # pylint: disable=missing-type-doc import struct diff --git a/pymodbus/pdu/register_read_message.py b/pymodbus/pdu/register_read_message.py index c03877f94..8fd335b32 100644 --- a/pymodbus/pdu/register_read_message.py +++ b/pymodbus/pdu/register_read_message.py @@ -1,14 +1,5 @@ """Register Reading Request/Response.""" -__all__ = [ - "ReadHoldingRegistersRequest", - "ReadHoldingRegistersResponse", - "ReadInputRegistersRequest", - "ReadInputRegistersResponse", - "ReadRegistersResponseBase", - "ReadWriteMultipleRegistersRequest", - "ReadWriteMultipleRegistersResponse", -] # pylint: disable=missing-type-doc import struct diff --git a/pymodbus/pdu/register_write_message.py b/pymodbus/pdu/register_write_message.py index 18b12a51b..b0b67d5ea 100644 --- a/pymodbus/pdu/register_write_message.py +++ b/pymodbus/pdu/register_write_message.py @@ -1,13 +1,5 @@ """Register Writing Request/Response Messages.""" -__all__ = [ - "WriteSingleRegisterRequest", - "WriteSingleRegisterResponse", - "WriteMultipleRegistersRequest", - "WriteMultipleRegistersResponse", - "MaskWriteRegisterRequest", - "MaskWriteRegisterResponse", -] # pylint: disable=missing-type-doc import struct diff --git a/test/sub_function_codes/test_bit_read_messages.py b/test/sub_function_codes/test_bit_read_messages.py index c0c7a5890..99c095560 100644 --- a/test/sub_function_codes/test_bit_read_messages.py +++ b/test/sub_function_codes/test_bit_read_messages.py @@ -40,17 +40,17 @@ def tearDown(self): def test_read_bit_base_class_methods(self): """Test basic bit message encoding/decoding.""" - handle = ReadBitsRequestBase(1, 1) + handle = ReadBitsRequestBase(1, 1, 0, 0, 0, False) msg = "ReadBitRequest(1,1)" assert msg == str(handle) - handle = ReadBitsResponseBase([1, 1]) + handle = ReadBitsResponseBase([1, 1], 0, 0, 0, False) msg = "ReadBitsResponseBase(2)" assert msg == str(handle) def test_bit_read_base_request_encoding(self): """Test basic bit message encoding/decoding.""" for i in range(20): - handle = ReadBitsRequestBase(i, i) + handle = ReadBitsRequestBase(i, i, 0, 0, 0, False) result = struct.pack(">HH", i, i) assert handle.encode() == result handle.decode(result) @@ -60,7 +60,7 @@ def test_bit_read_base_response_encoding(self): """Test basic bit message encoding/decoding.""" for i in range(20): data = [True] * i - handle = ReadBitsResponseBase(data) + handle = ReadBitsResponseBase(data, 0, 0, 0, False) result = handle.encode() handle.decode(result) assert handle.bits[:i] == data @@ -68,7 +68,7 @@ def test_bit_read_base_response_encoding(self): def test_bit_read_base_response_helper_methods(self): """Test the extra methods on a ReadBitsResponseBase.""" data = [False] * 8 - handle = ReadBitsResponseBase(data) + handle = ReadBitsResponseBase(data, 0, 0, 0, False) for i in (1, 3, 5): handle.setBit(i, True) for i in (1, 3, 5): @@ -79,8 +79,8 @@ def test_bit_read_base_response_helper_methods(self): def test_bit_read_base_requests(self): """Test bit read request encoding.""" messages = { - ReadBitsRequestBase(12, 14): b"\x00\x0c\x00\x0e", - ReadBitsResponseBase([1, 0, 1, 1, 0]): b"\x01\x0d", + ReadBitsRequestBase(12, 14, 0, 0, 0, False): b"\x00\x0c\x00\x0e", + ReadBitsResponseBase([1, 0, 1, 1, 0], 0, 0, 0, False): b"\x01\x0d", } for request, expected in iter(messages.items()): assert request.encode() == expected @@ -89,8 +89,8 @@ async def test_bit_read_message_execute_value_errors(self): """Test bit read request encoding.""" context = MockContext() requests = [ - ReadCoilsRequest(1, 0x800), - ReadDiscreteInputsRequest(1, 0x800), + ReadCoilsRequest(1, 0x800, 0, 0, 0, False), + ReadDiscreteInputsRequest(1, 0x800, 0, 0, 0, False), ] for request in requests: result = await request.execute(context) @@ -100,8 +100,8 @@ async def test_bit_read_message_execute_address_errors(self): """Test bit read request encoding.""" context = MockContext() requests = [ - ReadCoilsRequest(1, 5), - ReadDiscreteInputsRequest(1, 5), + ReadCoilsRequest(1, 5, 0, 0, 0, False), + ReadDiscreteInputsRequest(1, 5, 0, 0, 0, False), ] for request in requests: result = await request.execute(context) @@ -112,8 +112,8 @@ async def test_bit_read_message_execute_success(self): context = MockContext() context.validate = lambda a, b, c: True requests = [ - ReadCoilsRequest(1, 5), - ReadDiscreteInputsRequest(1, 5), + ReadCoilsRequest(1, 5, 0, 0, 0, False), + ReadDiscreteInputsRequest(1, 5, 0, 0, 0, False), ] for request in requests: result = await request.execute(context) @@ -122,12 +122,12 @@ async def test_bit_read_message_execute_success(self): def test_bit_read_message_get_response_pdu(self): """Test bit read message get response pdu.""" requests = { - ReadCoilsRequest(1, 5): 3, - ReadCoilsRequest(1, 8): 3, - ReadCoilsRequest(0, 16): 4, - ReadDiscreteInputsRequest(1, 21): 5, - ReadDiscreteInputsRequest(1, 24): 5, - ReadDiscreteInputsRequest(1, 1900): 240, + ReadCoilsRequest(1, 5, 0, 0, 0, False): 3, + ReadCoilsRequest(1, 8, 0, 0, 0, False): 3, + ReadCoilsRequest(0, 16, 0, 0, 0, False): 4, + ReadDiscreteInputsRequest(1, 21, 0, 0, 0, False): 5, + ReadDiscreteInputsRequest(1, 24, 0, 0, 0, False): 5, + ReadDiscreteInputsRequest(1, 1900, 0, 0, 0, False): 240, } for request, expected in iter(requests.items()): pdu_len = request.get_response_pdu_size() From dbcb4a4d185d6159232b0da30e6884f040574a30 Mon Sep 17 00:00:00 2001 From: jan iversen Date: Sat, 20 Jul 2024 09:04:29 +0200 Subject: [PATCH 72/88] Block draft PRs to use CPU minutes. (#2245) --- .github/workflows/ci.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5fdfd762f..024ea1405 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -10,6 +10,7 @@ on: pull_request: branches: - dev + types: [opened, synchronize, reopened, ready_for_review] schedule: # Sunday at 02:10 UTC. - cron: '10 2 * * 0' @@ -18,6 +19,7 @@ on: jobs: testing: name: ${{ matrix.os }} - ${{ matrix.python }} + if: github.event.pull_request.draft == false runs-on: ${{ matrix.os }} timeout-minutes: 20 strategy: @@ -114,6 +116,7 @@ jobs: analyze: name: Analyze Python + if: github.event.pull_request.draft == false runs-on: ubuntu-22.04 timeout-minutes: 10 steps: From e6e9e184f0ff19fc5dca147b99ed5b8eb6e11aaf Mon Sep 17 00:00:00 2001 From: jan iversen Date: Sat, 20 Jul 2024 09:37:38 +0200 Subject: [PATCH 73/88] CI, fail draft PR. (#2246) --- .github/workflows/ci.yml | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 024ea1405..e166ff9e2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -17,6 +17,15 @@ on: workflow_dispatch: jobs: + faildraft: + name: fail draft + if: github.event.pull_request.draft == true + runs-on: ubuntu-latest + steps: + - name: fail draft + run: | + exit 1 + testing: name: ${{ matrix.os }} - ${{ matrix.python }} if: github.event.pull_request.draft == false From 063f50406491c7ed703a4099167134f8accb8c3f Mon Sep 17 00:00:00 2001 From: jan iversen Date: Sat, 20 Jul 2024 13:50:23 +0200 Subject: [PATCH 74/88] Logging 100% coverage. (#2248) --- test/test_logging.py | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/test/test_logging.py b/test/test_logging.py index a8730190d..eda278e5f 100644 --- a/test/test_logging.py +++ b/test/test_logging.py @@ -4,7 +4,7 @@ import pytest -from pymodbus.logging import Log +from pymodbus.logging import Log, pymodbus_apply_logging_config class TestLogging: @@ -32,6 +32,7 @@ def test_log_simple(self): [ ("string {} {} {}", "string 101 102 103", (101, 102, 103)), ("string {}", "string 0x41 0x42 0x43 0x44", (b"ABCD", ":hex")), + ("string {}", "string b'41424344'", (b"ABCD", ":b2a")), ("string {}", "string 125", (125, ":str")), ], ) @@ -39,3 +40,18 @@ def test_log_parms(self, txt, result, params): """Test string with parameters (old f-string).""" log_txt = Log.build_msg(txt, *params) assert log_txt == result + + def test_apply_logging(self): + """Test pymodbus_apply_logging_config.""" + + pymodbus_apply_logging_config("debug") + pymodbus_apply_logging_config(logging.NOTSET) + pymodbus_apply_logging_config("debug", "pymodbus.log") + pymodbus_apply_logging_config("info") + Log.info("test") + pymodbus_apply_logging_config("warning") + Log.warning("test") + pymodbus_apply_logging_config("critical") + Log.critical("test") + pymodbus_apply_logging_config("error") + Log.error("test") From 978ad88b2eb7482d681bbfcef46a8f0f05547f84 Mon Sep 17 00:00:00 2001 From: jan iversen Date: Sat, 20 Jul 2024 13:58:43 +0200 Subject: [PATCH 75/88] Update check for windows platform (#2247) --- test/test_logging.py | 1 - test/transport/test_comm.py | 5 +++++ test/transport/test_serial.py | 4 ++++ 3 files changed, 9 insertions(+), 1 deletion(-) diff --git a/test/test_logging.py b/test/test_logging.py index eda278e5f..3ce498133 100644 --- a/test/test_logging.py +++ b/test/test_logging.py @@ -43,7 +43,6 @@ def test_log_parms(self, txt, result, params): def test_apply_logging(self): """Test pymodbus_apply_logging_config.""" - pymodbus_apply_logging_config("debug") pymodbus_apply_logging_config(logging.NOTSET) pymodbus_apply_logging_config("debug", "pymodbus.log") diff --git a/test/transport/test_comm.py b/test/transport/test_comm.py index 8b175bc09..35b25c52c 100644 --- a/test/transport/test_comm.py +++ b/test/transport/test_comm.py @@ -174,6 +174,10 @@ async def test_split_serial_packet(self, client, server, use_port): ) async def test_serial_poll(self, client, server, use_port): """Test connection and data exchange.""" + if SerialTransport.force_poll: + client.close() + server.close() + return Log.debug("test_serial_poll {}", use_port) assert await server.listen() SerialTransport.force_poll = True @@ -188,6 +192,7 @@ async def test_serial_poll(self, client, server, use_port): assert not client.recv_buffer client.close() server.close() + SerialTransport.force_poll = False @pytest.mark.parametrize( ("use_comm_type", "use_host"), diff --git a/test/transport/test_serial.py b/test/transport/test_serial.py index f40eb805d..702c0d441 100644 --- a/test/transport/test_serial.py +++ b/test/transport/test_serial.py @@ -81,6 +81,8 @@ async def test_create_serial(self): async def test_force_poll(self): """Test external methods.""" + if SerialTransport.force_poll: + return SerialTransport.force_poll = True transport, protocol = await create_serial_connection( asyncio.get_running_loop(), mock.Mock, "dummy" @@ -94,6 +96,8 @@ async def test_force_poll(self): async def test_write_force_poll(self): """Test write with poll.""" + if SerialTransport.force_poll: + return SerialTransport.force_poll = True transport, protocol = await create_serial_connection( asyncio.get_running_loop(), mock.Mock, "dummy" From ef4886b68c4470b0f437ff00a3e3d647336b3b15 Mon Sep 17 00:00:00 2001 From: Alex <52292902+alexrudd2@users.noreply.github.com> Date: Sat, 20 Jul 2024 15:11:02 -0500 Subject: [PATCH 76/88] Fix some mypy type checking errors in test_transaction.py (#2250) --- test/test_transaction.py | 28 +++++++--------------------- 1 file changed, 7 insertions(+), 21 deletions(-) diff --git a/test/test_transaction.py b/test/test_transaction.py index 546807f64..2508c34ff 100755 --- a/test/test_transaction.py +++ b/test/test_transaction.py @@ -120,10 +120,9 @@ def test_execute(self, mock_time): assert trans.retries == 3 assert not trans.retry_on_empty - trans.getTransaction = mock.MagicMock() - trans.getTransaction.return_value = "response" + trans.getTransaction = mock.MagicMock(return_value = b"response") response = trans.execute(request) - assert response == "response" + assert response == b"response" # No response trans._recv = mock.MagicMock( # pylint: disable=protected-access return_value=b"abcdef" @@ -195,33 +194,20 @@ def test_transaction_manager_tid(self): def test_get_transaction_manager_transaction(self): """Test the getting a transaction from the transaction manager.""" - - class Request: # pylint: disable=too-few-public-methods - """Request.""" - self._manager.reset() - handle = Request() - handle.transaction_id = ( # pylint: disable=attribute-defined-outside-init - self._manager.getNextTID() + handle = ModbusRequest( + 0, self._manager.getNextTID(), 0, False ) - handle.message = b"testing" # pylint: disable=attribute-defined-outside-init self._manager.addTransaction(handle) result = self._manager.getTransaction(handle.transaction_id) - assert handle.message == result.message + assert handle is result def test_delete_transaction_manager_transaction(self): """Test deleting a transaction from the dict transaction manager.""" - - class Request: # pylint: disable=too-few-public-methods - """Request.""" - self._manager.reset() - handle = Request() - handle.transaction_id = ( # pylint: disable=attribute-defined-outside-init - self._manager.getNextTID() + handle = ModbusRequest( + 0, self._manager.getNextTID(), 0, False ) - handle.message = b"testing" # pylint: disable=attribute-defined-outside-init - self._manager.addTransaction(handle) self._manager.delTransaction(handle.transaction_id) assert not self._manager.getTransaction(handle.transaction_id) From fbaf05d5d3ab4f274de829cac74eb829a3b6ebf9 Mon Sep 17 00:00:00 2001 From: James Hilliard Date: Sun, 21 Jul 2024 00:24:26 -0600 Subject: [PATCH 77/88] Use mock.patch.object to avoid protected access errors. (#2251) --- test/test_transaction.py | 55 +++++++++++++++++++--------------------- 1 file changed, 26 insertions(+), 29 deletions(-) diff --git a/test/test_transaction.py b/test/test_transaction.py index 2508c34ff..3d2311241 100755 --- a/test/test_transaction.py +++ b/test/test_transaction.py @@ -12,6 +12,7 @@ ModbusRtuFramer, ModbusSocketFramer, ModbusTlsFramer, + ModbusTransactionManager, SyncModbusTransactionManager, ) @@ -90,7 +91,9 @@ def test_calculate_exception_length(self): ) @mock.patch("pymodbus.transaction.time") - def test_execute(self, mock_time): + @mock.patch.object(SyncModbusTransactionManager, "_recv") + @mock.patch.object(ModbusTransactionManager, "getTransaction") + def test_execute(self, mock_get_transaction, mock_recv, mock_time): """Test execute.""" mock_time.time.side_effect = count() @@ -114,35 +117,34 @@ def test_execute(self, mock_time): request.slave_id = 1 request.function_code = 222 trans = SyncModbusTransactionManager(client, 0.3, False, False, 3) - trans._recv = mock.MagicMock( # pylint: disable=protected-access + mock_recv.reset_mock( return_value=b"abcdef" ) assert trans.retries == 3 assert not trans.retry_on_empty - trans.getTransaction = mock.MagicMock(return_value = b"response") + mock_get_transaction.return_value = b"response" response = trans.execute(request) assert response == b"response" # No response - trans._recv = mock.MagicMock( # pylint: disable=protected-access + mock_recv.reset_mock( return_value=b"abcdef" ) trans.transactions = {} - trans.getTransaction = mock.MagicMock() - trans.getTransaction.return_value = None + mock_get_transaction.return_value = None response = trans.execute(request) assert isinstance(response, ModbusIOException) # No response with retries trans.retry_on_empty = True - trans._recv = mock.MagicMock( # pylint: disable=protected-access + mock_recv.reset_mock( side_effect=iter([b"", b"abcdef"]) ) response = trans.execute(request) assert isinstance(response, ModbusIOException) # wrong handle_local_echo - trans._recv = mock.MagicMock( # pylint: disable=protected-access + mock_recv.reset_mock( side_effect=iter([b"abcdef", b"deadbe", b"123456"]) ) client.comm_params.handle_local_echo = True @@ -153,14 +155,14 @@ def test_execute(self, mock_time): # retry on invalid response trans.retry_on_invalid = True - trans._recv = mock.MagicMock( # pylint: disable=protected-access + mock_recv.reset_mock( side_effect=iter([b"", b"abcdef", b"deadbe", b"123456"]) ) response = trans.execute(request) assert isinstance(response, ModbusIOException) # Unable to decode response - trans._recv = mock.MagicMock( # pylint: disable=protected-access + mock_recv.reset_mock( side_effect=ModbusIOException() ) client.framer.processIncomingPacket.side_effect = mock.MagicMock( @@ -177,12 +179,11 @@ def test_execute(self, mock_time): # Broadcast w/ Local echo client.comm_params.handle_local_echo = True client.params.broadcast_enable = True - recv = mock.MagicMock(return_value=b"deadbeef") - trans._recv = recv # pylint: disable=protected-access + mock_recv.reset_mock(return_value=b"deadbeef") request.slave_id = 0 response = trans.execute(request) assert response == b"Broadcast write sent - no response expected" - recv.assert_called_once_with(8, False) + mock_recv.assert_called_once_with(8, False) client.comm_params.handle_local_echo = False def test_transaction_manager_tid(self): @@ -339,19 +340,18 @@ def callback(data): # for name in ("transaction_id", "protocol_id", "slave_id"): # assert getattr(expected, name) == getattr(actual, name) - def test_tcp_framer_packet(self): + @mock.patch.object(ModbusRequest, "encode") + def test_tcp_framer_packet(self, mock_encode): """Test a tcp frame packet build.""" - old_encode = ModbusRequest.encode - ModbusRequest.encode = lambda self: b"" message = ModbusRequest(0, 0, 0, False) message.transaction_id = 0x0001 message.protocol_id = 0x0000 message.slave_id = 0xFF message.function_code = 0x01 expected = b"\x00\x01\x00\x00\x00\x02\xff\x01" + mock_encode.return_value = b"" actual = self._tcp.buildPacket(message) assert expected == actual - ModbusRequest.encode = old_encode # ----------------------------------------------------------------------- # # TLS tests @@ -494,16 +494,15 @@ def callback(data): self._tcp.processIncomingPacket(msg, callback, [0, 1]) assert result - def test_framer_tls_framer_packet(self): + @mock.patch.object(ModbusRequest, "encode") + def test_framer_tls_framer_packet(self, mock_encode): """Test a tls frame packet build.""" - old_encode = ModbusRequest.encode - ModbusRequest.encode = lambda self: b"" message = ModbusRequest(0, 0, 0, False) message.function_code = 0x01 expected = b"\x01" + mock_encode.return_value = b"" actual = self._tls.buildPacket(message) assert expected == actual - ModbusRequest.encode = old_encode # ----------------------------------------------------------------------- # # RTU tests @@ -571,17 +570,16 @@ def callback(data): assert int(msg[0]) == header_dict["uid"] assert msg[-2:] == header_dict["crc"] - def test_rtu_framer_packet(self): + @mock.patch.object(ModbusRequest, "encode") + def test_rtu_framer_packet(self, mock_encode): """Test a rtu frame packet build.""" - old_encode = ModbusRequest.encode - ModbusRequest.encode = lambda self: b"" message = ModbusRequest(0, 0, 0, False) message.slave_id = 0xFF message.function_code = 0x01 expected = b"\xff\x01\x81\x80" # only header + CRC - no data + mock_encode.return_value = b"" actual = self._rtu.buildPacket(message) assert expected == actual - ModbusRequest.encode = old_encode def test_rtu_decode_exception(self): """Test that the RTU framer can decode errors.""" @@ -680,17 +678,16 @@ def test_ascii_framer_populate(self): self._ascii.populateResult(request) assert not request.slave_id - def test_ascii_framer_packet(self): + @mock.patch.object(ModbusRequest, "encode") + def test_ascii_framer_packet(self, mock_encode): """Test a ascii frame packet build.""" - old_encode = ModbusRequest.encode - ModbusRequest.encode = lambda self: b"" message = ModbusRequest(0, 0, 0, False) message.slave_id = 0xFF message.function_code = 0x01 expected = b":FF0100\r\n" + mock_encode.return_value = b"" actual = self._ascii.buildPacket(message) assert expected == actual - ModbusRequest.encode = old_encode def test_ascii_process_incoming_packets(self): """Test ascii process incoming packet.""" From 385bbc5ef6d2189f7426961e7461e6330ff0208c Mon Sep 17 00:00:00 2001 From: jan iversen Date: Sun, 21 Jul 2024 14:03:49 +0200 Subject: [PATCH 78/88] Framer optimization (apart from RTU). (#2146) --- pymodbus/framer/ascii.py | 42 ++--- pymodbus/framer/base.py | 12 +- pymodbus/framer/framer.py | 38 ++--- pymodbus/framer/raw.py | 4 +- pymodbus/framer/rtu.py | 194 ++++++++------------- test/framers/conftest.py | 43 +++-- test/framers/test_ascii.py | 2 +- test/framers/test_framer.py | 326 +++++++++++++++++++++--------------- test/framers/test_rtu.py | 23 +++ 9 files changed, 371 insertions(+), 313 deletions(-) diff --git a/pymodbus/framer/ascii.py b/pymodbus/framer/ascii.py index 03746b87e..0229ed19f 100644 --- a/pymodbus/framer/ascii.py +++ b/pymodbus/framer/ascii.py @@ -34,25 +34,29 @@ class FramerAscii(FramerBase): def decode(self, data: bytes) -> tuple[int, int, int, bytes]: """Decode ADU.""" - if (used_len := len(data)) < self.MIN_SIZE: - Log.debug("Short frame: {} wait for more data", data, ":hex") - return 0, 0, 0, self.EMPTY - if data[0:1] != self.START: - if (start := data.find(self.START)) != -1: - used_len = start - Log.debug("Garble data before frame: {}, skip until start of frame", data, ":hex") - return used_len, 0, 0, self.EMPTY - if (used_len := data.find(self.END)) == -1: - Log.debug("Incomplete frame: {} wait for more data", data, ":hex") - return 0, 0, 0, self.EMPTY - - dev_id = int(data[1:3], 16) - lrc = int(data[used_len - 2: used_len], 16) - msg = a2b_hex(data[1 : used_len - 2]) - if not self.check_LRC(msg, lrc): - Log.debug("LRC wrong in frame: {} skipping", data, ":hex") - return used_len+2, 0, 0, self.EMPTY - return used_len+2, 0, dev_id, msg[1:] + buf_len = len(data) + used_len = 0 + while True: + if buf_len - used_len < self.MIN_SIZE: + return used_len, 0, 0, self.EMPTY + buffer = data[used_len:] + if buffer[0:1] != self.START: + if (i := buffer.find(self.START)) == -1: + Log.debug("No frame start in data: {}, wait for data", data, ":hex") + return buf_len, 0, 0, self.EMPTY + used_len += i + continue + if (end := buffer.find(self.END)) == -1: + Log.debug("Incomplete frame: {} wait for more data", data, ":hex") + return used_len, 0, 0, self.EMPTY + dev_id = int(buffer[1:3], 16) + lrc = int(buffer[end - 2: end], 16) + msg = a2b_hex(buffer[1 : end - 2]) + used_len += end + 2 + if not self.check_LRC(msg, lrc): + Log.debug("LRC wrong in frame: {} skipping", data, ":hex") + continue + return used_len, dev_id, dev_id, msg[1:] def encode(self, data: bytes, device_id: int, _tid: int) -> bytes: """Encode ADU.""" diff --git a/pymodbus/framer/base.py b/pymodbus/framer/base.py index a7e0ba146..e0c1595f0 100644 --- a/pymodbus/framer/base.py +++ b/pymodbus/framer/base.py @@ -6,6 +6,8 @@ """ from __future__ import annotations +from abc import abstractmethod + class FramerBase: """Intern base.""" @@ -15,6 +17,13 @@ class FramerBase: def __init__(self) -> None: """Initialize a ADU instance.""" + def set_dev_ids(self, _dev_ids: list[int]): + """Set/update allowed device ids.""" + + def set_fc_calc(self, _fc: int, _msg_size: int, _count_pos: int): + """Set/Update function code information.""" + + @abstractmethod def decode(self, data: bytes) -> tuple[int, int, int, bytes]: """Decode ADU. @@ -24,12 +33,11 @@ def decode(self, data: bytes) -> tuple[int, int, int, bytes]: device_id (int) or 0 modbus request/response (bytes) """ - raise RuntimeError("NOT IMPLEMENTED!") + @abstractmethod def encode(self, pdu: bytes, dev_id: int, tid: int) -> bytes: """Encode ADU. returns: modbus ADU (bytes) """ - raise RuntimeError("NOT IMPLEMENTED!") diff --git a/pymodbus/framer/framer.py b/pymodbus/framer/framer.py index 9dc9eabd0..b2f8be800 100644 --- a/pymodbus/framer/framer.py +++ b/pymodbus/framer/framer.py @@ -66,37 +66,31 @@ def __init__(self, super().__init__(params, is_server) self.device_ids = device_ids self.broadcast: bool = (0 in device_ids) - self.handle = { - FramerType.RAW: FramerRaw, - FramerType.ASCII: FramerAscii, - FramerType.RTU: FramerRTU, - FramerType.SOCKET: FramerSocket, - FramerType.TLS: FramerTLS, - }[framer_type]() - - def validate_device_id(self, dev_id: int) -> bool: - """Check if device id is expected.""" - return self.broadcast or (dev_id in self.device_ids) + self.handle = { + FramerType.RAW: FramerRaw(), + FramerType.ASCII: FramerAscii(), + FramerType.RTU: FramerRTU(), + FramerType.SOCKET: FramerSocket(), + FramerType.TLS: FramerTLS(), + }[framer_type] def callback_data(self, data: bytes, addr: tuple | None = None) -> int: """Handle received data.""" - tot_len = len(data) - start = 0 + tot_len = 0 + buf_len = len(data) while True: - used_len, tid, device_id, msg = self.handle.decode(data[start:]) + used_len, tid, device_id, msg = self.handle.decode(data[tot_len:]) + tot_len += used_len if msg: - self.callback_request_response(msg, device_id, tid) - if not used_len: - return start - start += used_len - if start == tot_len: + if self.broadcast or device_id in self.device_ids: + self.callback_request_response(msg, device_id, tid) + if tot_len == buf_len: + return tot_len + else: return tot_len - # --------------------- # - # callbacks and helpers # - # --------------------- # @abstractmethod def callback_request_response(self, data: bytes, device_id: int, tid: int) -> None: """Handle received modbus request/response.""" diff --git a/pymodbus/framer/raw.py b/pymodbus/framer/raw.py index 54a9a0ac1..96ca1bc2e 100644 --- a/pymodbus/framer/raw.py +++ b/pymodbus/framer/raw.py @@ -27,6 +27,6 @@ def decode(self, data: bytes) -> tuple[int, int, int, bytes]: tid = int(data[1]) return len(data), dev_id, tid, data[2:] - def encode(self, pdu: bytes, device_id: int, tid: int) -> bytes: + def encode(self, pdu: bytes, dev_id: int, tid: int) -> bytes: """Encode ADU.""" - return device_id.to_bytes(1, 'big') + tid.to_bytes(1, 'big') + pdu + return dev_id.to_bytes(1, 'big') + tid.to_bytes(1, 'big') + pdu diff --git a/pymodbus/framer/rtu.py b/pymodbus/framer/rtu.py index bfda58365..9233245a1 100644 --- a/pymodbus/framer/rtu.py +++ b/pymodbus/framer/rtu.py @@ -1,10 +1,8 @@ """Modbus RTU frame implementation.""" from __future__ import annotations -import struct +from collections import namedtuple -from pymodbus.exceptions import ModbusIOException -from pymodbus.factory import ClientDecoder from pymodbus.framer.base import FramerBase from pymodbus.logging import Log @@ -17,26 +15,53 @@ class FramerRTU(FramerBase): * Note: due to the USB converter and the OS drivers, timing cannot be quaranteed neither when receiving nor when sending. + + Decoding is a complicated process because the RTU frame does not have a fixed prefix + only suffix, therefore it is necessary to decode the content (PDU) to get length etc. + + There are some protocol restrictions that help with the detection. + + For client: + - a request causes 1 response ! + - Multiple requests are NOT allowed (master-slave protocol) + - the server will not retransmit responses + this means decoding is always exactly 1 frame (response) + + For server (Single device) + - only 1 request allowed (master-slave) protocol + - the client (master) may retransmit but in larger time intervals + this means decoding is always exactly 1 frame (request) + + For server (Multidrop line --> devices in parallel) + - only 1 request allowed (master-slave) protocol + - other devices will send responses + - the client (master) may retransmit but in larger time intervals + this means decoding is always exactly 1 frame request, however some requests + will be for unknown slaves, which must be ignored together with the + response from the unknown slave. + + Recovery from bad cabling and unstable USB etc is important, + the following scenarios is possible: + - garble data before frame + - garble data in frame + - garble data after frame + - data in frame garbled (wrong CRC) + decoding assumes the frame is sound, and if not enters a hunting mode. + + The 3.5 byte transmission time at the slowest speed 1.200Bps is 31ms. + Device drivers will typically flush buffer after 10ms of silence. + If no data is received for 50ms the transmission / frame can be considered + complete. """ MIN_SIZE = 5 + FC_LEN = namedtuple("FC_LEN", "req_len req_bytepos resp_len resp_bytepos") + def __init__(self) -> None: """Initialize a ADU instance.""" super().__init__() - self.broadcast: bool = False - self.dev_ids: list[int] - self.fc_calc: dict[int, int] - - def set_dev_ids(self, dev_ids: list[int]): - """Set/update allowed device ids.""" - if 0 in dev_ids: - self.broadcast = True - self.dev_ids = dev_ids - - def set_fc_calc(self, fc: int, msg_size: int, count_pos: int): - """Set/Update function code information.""" - self.fc_calc[fc] = msg_size if not count_pos else -count_pos + self.fc_len: dict[int, FramerRTU.FC_LEN] = {} @classmethod @@ -58,120 +83,39 @@ def generate_crc16_table(cls) -> list[int]: return result crc16_table: list[int] = [0] - def _legacy_decode(self, callback, slave): # noqa: C901 - """Process new packet pattern.""" - - def is_frame_ready(self): - """Check if we should continue decode logic.""" - size = self._header.get("len", 0) - if not size and len(self._buffer) > self._hsize: - try: - self._header["uid"] = int(self._buffer[0]) - self._header["tid"] = int(self._buffer[0]) - func_code = int(self._buffer[1]) - pdu_class = self.decoder.lookupPduClass(func_code) - size = pdu_class.calculateRtuFrameSize(self._buffer) - self._header["len"] = size - - if len(self._buffer) < size: - raise IndexError - self._header["crc"] = self._buffer[size - 2 : size] - except IndexError: - return False - return len(self._buffer) >= size if size > 0 else False - - def get_frame_start(self, slaves, broadcast, skip_cur_frame): - """Scan buffer for a relevant frame start.""" - start = 1 if skip_cur_frame else 0 - if (buf_len := len(self._buffer)) < 4: - return False - for i in range(start, buf_len - 3): # - if not broadcast and self._buffer[i] not in slaves: - continue - if ( - self._buffer[i + 1] not in self.function_codes - and (self._buffer[i + 1] - 0x80) not in self.function_codes - ): - continue - if i: - self._buffer = self._buffer[i:] # remove preceding trash. - return True - if buf_len > 3: - self._buffer = self._buffer[-3:] - return False - - def check_frame(self): - """Check if the next frame is available.""" - try: - self._header["uid"] = int(self._buffer[0]) - self._header["tid"] = int(self._buffer[0]) - func_code = int(self._buffer[1]) - pdu_class = self.decoder.lookupPduClass(func_code) - size = pdu_class.calculateRtuFrameSize(self._buffer) - self._header["len"] = size - - if len(self._buffer) < size: - raise IndexError - self._header["crc"] = self._buffer[size - 2 : size] - frame_size = self._header["len"] - data = self._buffer[: frame_size - 2] - crc = self._header["crc"] - crc_val = (int(crc[0]) << 8) + int(crc[1]) - return FramerRTU.check_CRC(data, crc_val) - except (IndexError, KeyError, struct.error): - return False - - self._buffer = b'' # pylint: disable=attribute-defined-outside-init - broadcast = not slave[0] - skip_cur_frame = False - while get_frame_start(self, slave, broadcast, skip_cur_frame): - self._header: dict = {"uid": 0x00, "len": 0, "crc": b"\x00\x00"} # pylint: disable=attribute-defined-outside-init - if not is_frame_ready(self): - Log.debug("Frame - not ready") - break - if not check_frame(self): - Log.debug("Frame check failed, ignoring!!") - # x = self._buffer - # self.resetFrame() - # self._buffer = x - skip_cur_frame = True - continue - start = 0x01 # self._hsize - end = self._header["len"] - 2 - buffer = self._buffer[start:end] - if end > 0: - Log.debug("Getting Frame - {}", buffer, ":hex") - data = buffer - else: - data = b"" - if (result := ClientDecoder().decode(data)) is None: - raise ModbusIOException("Unable to decode request") - result.slave_id = self._header["uid"] - result.transaction_id = self._header["tid"] - self._buffer = self._buffer[self._header["len"] :] # pylint: disable=attribute-defined-outside-init - Log.debug("Frame advanced, resetting header!!") - callback(result) # defer or push to a thread? - - - def hunt_frame_start(self, skip_cur_frame: bool, data: bytes) -> int: - """Scan buffer for a relevant frame start.""" - buf_len = len(data) - for i in range(1 if skip_cur_frame else 0, buf_len - self.MIN_SIZE): - if not (self.broadcast or data[i] in self.dev_ids): - continue - if (_fc := data[i + 1]) not in self.fc_calc: - continue - return i - return -i + + def setup_fc_len(self, _fc: int, + _req_len: int, _req_byte_pos: int, + _resp_len: int, _resp_byte_pos: int + ): + """Define request/response lengths pr function code.""" + return def decode(self, data: bytes) -> tuple[int, int, int, bytes]: """Decode ADU.""" - if len(data) < self.MIN_SIZE: + if (buf_len := len(data)) < self.MIN_SIZE: + Log.debug("Short frame: {} wait for more data", data, ":hex") return 0, 0, 0, b'' - while (i := self.hunt_frame_start(False, data)) > 0: - pass - return -i, 0, 0, b'' + i = -1 + try: + while True: + i += 1 + if i > buf_len - self.MIN_SIZE + 1: + break + dev_id = int(data[i]) + fc_len = 5 + msg_len = fc_len -2 if fc_len > 0 else int(data[i-fc_len])-fc_len+1 + if msg_len + i + 2 > buf_len: + break + crc_val = (int(data[i+msg_len]) << 8) + int(data[i+msg_len+1]) + if not self.check_CRC(data[i:i+msg_len], crc_val): + Log.debug("Skipping frame CRC with len {} at index {}!", msg_len, i) + raise KeyError + return i+msg_len+2, dev_id, dev_id, data[i+1:i+msg_len] + except KeyError: + i = buf_len + return i, 0, 0, b'' def encode(self, pdu: bytes, device_id: int, _tid: int) -> bytes: diff --git a/test/framers/conftest.py b/test/framers/conftest.py index 5592ac994..078599c99 100644 --- a/test/framers/conftest.py +++ b/test/framers/conftest.py @@ -5,27 +5,28 @@ import pytest +from pymodbus.factory import ClientDecoder, ServerDecoder from pymodbus.framer import Framer, FramerType from pymodbus.transport import CommParams, ModbusProtocol -class DummyMessage(Framer): +class DummyFramer(Framer): """Implement use of ModbusProtocol.""" def __init__(self, - message_type: FramerType, + framer_type: FramerType, params: CommParams, is_server: bool, device_ids: list[int] | None, ): - """Initialize a message instance.""" - super().__init__(message_type, params, is_server, device_ids) + """Initialize a frame instance.""" + super().__init__(framer_type, params, is_server, device_ids) self.send = mock.Mock() - self.message_type = message_type + self.framer_type = framer_type def callback_new_connection(self) -> ModbusProtocol: """Call when listener receive new connection request.""" - return DummyMessage(self.message_type, self.comm_params, self.is_server, self.device_ids) # pragma: no cover + return DummyFramer(self.framer_type, self.comm_params, self.is_server, self.device_ids) # pragma: no cover def callback_connected(self) -> None: """Call when connection is succcesfull.""" @@ -37,7 +38,29 @@ def callback_request_response(self, data: bytes, device_id: int, tid: int) -> No """Handle received modbus request/response.""" -@pytest.fixture(name="dummy_message") -async def prepare_dummy_message(): - """Return message object.""" - return DummyMessage +@pytest.fixture(name="entry") +def prepare_entry(): + """Return framer_type.""" + return FramerType.RAW + +@pytest.fixture(name="is_server") +def prepare_is_server(): + """Return client/server.""" + return False + +@pytest.fixture(name="dummy_framer") +async def prepare_test_framer(entry, is_server): + """Return framer object.""" + framer = DummyFramer( + entry, + CommParams(), + is_server, + [0, 1], + ) + if entry == FramerType.RTU: + func_table = (ServerDecoder if is_server else ClientDecoder)().lookup + for key, ent in func_table.items(): + fix_len = ent._rtu_frame_size if hasattr(ent, "_rtu_frame_size") else 0 # pylint: disable=protected-access + cnt_pos = ent. _rtu_byte_count_pos if hasattr(ent, "_rtu_byte_count_pos") else 0 # pylint: disable=protected-access + framer.handle.set_fc_calc(key, fix_len, cnt_pos) + return framer diff --git a/test/framers/test_ascii.py b/test/framers/test_ascii.py index d78d2e87b..d3b4ef74c 100644 --- a/test/framers/test_ascii.py +++ b/test/framers/test_ascii.py @@ -34,7 +34,7 @@ def test_decode(self, frame, packet, used_len, res_id, res): res_len, tid, dev_id, data = frame.decode(packet) assert res_len == used_len assert data == res - assert not tid + assert tid == res_id assert dev_id == res_id @pytest.mark.parametrize( diff --git a/test/framers/test_framer.py b/test/framers/test_framer.py index 0236fcb34..59e6cdda1 100644 --- a/test/framers/test_framer.py +++ b/test/framers/test_framer.py @@ -9,33 +9,15 @@ from pymodbus.framer.rtu import FramerRTU from pymodbus.framer.socket import FramerSocket from pymodbus.framer.tls import FramerTLS -from pymodbus.transport import CommParams class TestFramer: """Test module.""" - @staticmethod - @pytest.fixture(name="msg") - async def prepare_message(dummy_message): - """Return message object.""" - return dummy_message( - FramerType.RAW, - CommParams(), - False, - [1], - ) - - @pytest.mark.parametrize(("entry"), list(FramerType)) - async def test_message_init(self, entry, dummy_message): - """Test message type.""" - msg = dummy_message(entry.value, - CommParams(), - False, - [1], - ) - assert msg.handle + async def test_framer_init(self, dummy_framer): + """Test framer type.""" + assert dummy_framer.handle @pytest.mark.parametrize(("data", "res_len", "cx", "rc"), [ (b'12345', 5, 1, [(5, 0, 0, b'12345')]), # full frame @@ -43,38 +25,39 @@ async def test_message_init(self, entry, dummy_message): (b'12345', 5, 0, [(5, 0, 0, b'')]), # faulty frame, skipped (b'1234512345', 10, 2, [(5, 0, 0, b'12345'), (5, 0, 0, b'12345')]), # 2 full frames (b'12345678', 5, 1, [(5, 0, 0, b'12345'), (0, 0, 0, b'')]), # full frame, not full frame - (b'67812345', 8, 1, [(3, 0, 0, b''), (5, 0, 0, b'12345')]), # garble first, full frame next - (b'12345678', 5, 0, [(5, 0, 0, b''), (0, 0, 0, b'')]), # garble first, not full frame - (b'12345678', 8, 0, [(5, 0, 0, b''), (3, 0, 0, b'')]), # garble first, faulty frame + (b'67812345', 8, 1, [(8, 0, 0, b'12345')]), # garble first, full frame next + (b'12345678', 5, 0, [(5, 0, 0, b'')]), # garble first, not full frame + (b'12345678', 8, 0, [(8, 0, 0, b'')]), # garble first, faulty frame ]) - async def test_message_callback(self, msg, data, res_len, cx, rc): - """Test message type.""" - msg.callback_request_response = mock.Mock() - msg.handle.decode = mock.MagicMock(side_effect=iter(rc)) - assert msg.callback_data(data) == res_len - assert msg.callback_request_response.call_count == cx + async def test_framer_callback(self, dummy_framer, data, res_len, cx, rc): + """Test framer type.""" + dummy_framer.callback_request_response = mock.Mock() + dummy_framer.handle.decode = mock.MagicMock(side_effect=iter(rc)) + assert dummy_framer.callback_data(data) == res_len + assert dummy_framer.callback_request_response.call_count == cx if cx: - msg.callback_request_response.assert_called_with(b'12345', 0, 0) + dummy_framer.callback_request_response.assert_called_with(b'12345', 0, 0) else: - msg.callback_request_response.assert_not_called() + dummy_framer.callback_request_response.assert_not_called() - async def test_message_build_send(self, msg): - """Test message type.""" - msg.handle.encode = mock.MagicMock(return_value=(b'decode')) - msg.build_send(b'decode', 1, 0) - msg.handle.encode.assert_called_once() - msg.send.assert_called_once() - msg.send.assert_called_with(b'decode', None) + @pytest.mark.parametrize(("data", "res_len", "rc"), [ + (b'12345', 5, [(5, 0, 17, b'12345'), (0, 0, 0, b'')]), # full frame, wrong dev_id + ]) + async def test_framer_callback_wrong_id(self, dummy_framer, data, res_len, rc): + """Test framer type.""" + dummy_framer.callback_request_response = mock.Mock() + dummy_framer.handle.decode = mock.MagicMock(side_effect=iter(rc)) + dummy_framer.broadcast = False + assert dummy_framer.callback_data(data) == res_len + dummy_framer.callback_request_response.assert_not_called() - @pytest.mark.parametrize( - ("dev_id", "res"), [ - (0, False), - (1, True), - (2, False), - ]) - async def test_validate_id(self, msg, dev_id, res): - """Test message type.""" - assert res == msg.validate_device_id(dev_id) + async def test_framer_build_send(self, dummy_framer): + """Test framer type.""" + dummy_framer.handle.encode = mock.MagicMock(return_value=(b'decode')) + dummy_framer.build_send(b'decode', 1, 0) + dummy_framer.handle.encode.assert_called_once() + dummy_framer.send.assert_called_once() + dummy_framer.send.assert_called_with(b'decode', None) @pytest.mark.parametrize( ("data", "res_len", "res_id", "res_tid", "res_data"), [ @@ -82,9 +65,9 @@ async def test_validate_id(self, msg, dev_id, res): (b'\x01\x02\x03', 3, 1, 2, b'\x03'), (b'\x04\x05\x06\x07\x08\x09\x00\x01\x02\x03', 10, 4, 5, b'\x06\x07\x08\x09\x00\x01\x02\x03'), ]) - async def test_decode(self, msg, data, res_id, res_tid, res_len, res_data): + async def test_framer_decode(self, dummy_framer, data, res_id, res_tid, res_len, res_data): """Test decode method in all types.""" - t_len, t_id, t_tid, t_data = msg.handle.decode(data) + t_len, t_id, t_tid, t_data = dummy_framer.handle.decode(data) assert res_len == t_len assert res_id == t_id assert res_tid == t_tid @@ -95,9 +78,9 @@ async def test_decode(self, msg, data, res_id, res_tid, res_len, res_data): (b'\x01\x02', 5, 6, b'\x05\x06\x01\x02'), (b'\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09', 17, 25, b'\x11\x19\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09'), ]) - async def test_encode(self, msg, data, dev_id, tid, res_data): + async def test_framer_encode(self, dummy_framer, data, dev_id, tid, res_data): """Test decode method in all types.""" - t_data = msg.handle.encode(data, dev_id, tid) + t_data = dummy_framer.handle.encode(data, dev_id, tid) assert res_data == t_data @pytest.mark.parametrize( @@ -135,7 +118,7 @@ def test_roundtrip_CRC(self): -class TestFramer2: +class TestFramerType: """Test classes.""" @pytest.mark.parametrize( @@ -151,6 +134,24 @@ class TestFramer2: b':FF03007C000280\r\n', b':FF0304008D008EDF\r\n', b':FF83027C\r\n', + b':0003007C00027F\r\n', + b':000304008D008EDE\r\n', + b':0083027B\r\n', + b':1103007C00026E\r\n', + b':110304008D008ECD\r\n', + b':1183026A\r\n', + b':FF03007C000280\r\n', + b':FF0304008D008EDF\r\n', + b':FF83027C\r\n', + b':0003007C00027F\r\n', + b':000304008D008EDE\r\n', + b':0083027B\r\n', + b':1103007C00026E\r\n', + b':110304008D008ECD\r\n', + b':1183026A\r\n', + b':FF03007C000280\r\n', + b':FF0304008D008EDF\r\n', + b':FF83027C\r\n', ]), (FramerRTU, [ b'\x00\x03\x00\x7c\x00\x02\x04\x02', @@ -162,6 +163,24 @@ class TestFramer2: b'\xff\x03\x00\x7c\x00\x02\x10\x0d', b'\xff\x03\x04\x00\x8d\x00\x8e\xf5\xb3', b'\xff\x83\x02\xa1\x01', + b'\x00\x03\x00\x7c\x00\x02\x04\x02', + b'\x00\x03\x04\x00\x8d\x00\x8e\xfa\xbc', + b'\x00\x83\x02\x91\x31', + b'\x11\x03\x00\x7c\x00\x02\x07\x43', + b'\x11\x03\x04\x00\x8d\x00\x8e\xfb\xbd', + b'\x11\x83\x02\xc1\x34', + b'\xff\x03\x00\x7c\x00\x02\x10\x0d', + b'\xff\x03\x04\x00\x8d\x00\x8e\xf5\xb3', + b'\xff\x83\x02\xa1\x01', + b'\x00\x03\x00\x7c\x00\x02\x04\x02', + b'\x00\x03\x04\x00\x8d\x00\x8e\xfa\xbc', + b'\x00\x83\x02\x91\x31', + b'\x11\x03\x00\x7c\x00\x02\x07\x43', + b'\x11\x03\x04\x00\x8d\x00\x8e\xfb\xbd', + b'\x11\x83\x02\xc1\x34', + b'\xff\x03\x00\x7c\x00\x02\x10\x0d', + b'\xff\x03\x04\x00\x8d\x00\x8e\xf5\xb3', + b'\xff\x83\x02\xa1\x01', ]), (FramerSocket, [ b'\x00\x00\x00\x00\x00\x06\x00\x03\x00\x7c\x00\x02', @@ -213,10 +232,9 @@ class TestFramer2: (9, 3077), ] ) - def test_encode(self, frame, frame_expected, data, dev_id, tid, inx1, inx2, inx3): + def test_encode_type(self, frame, frame_expected, data, dev_id, tid, inx1, inx2, inx3): """Test encode method.""" - if ((frame != FramerSocket and tid) or - (frame == FramerTLS and dev_id)): + if frame == FramerTLS and dev_id + tid: return frame_obj = frame() expected = frame_expected[inx1 + inx2 + inx3] @@ -224,47 +242,47 @@ def test_encode(self, frame, frame_expected, data, dev_id, tid, inx1, inx2, inx3 assert encoded_data == expected @pytest.mark.parametrize( - ("msg_type", "data", "dev_id", "tid", "expected"), + ("entry", "is_server", "data", "dev_id", "tid", "expected"), [ - (FramerType.ASCII, b':0003007C00027F\r\n', 0, 0, b"\x03\x00\x7c\x00\x02",), # Request - (FramerType.ASCII, b':000304008D008EDE\r\n', 0, 0, b"\x03\x04\x00\x8d\x00\x8e",), # Response - (FramerType.ASCII, b':0083027B\r\n', 0, 0, b'\x83\x02',), # Exception - (FramerType.ASCII, b':1103007C00026E\r\n', 17, 0, b"\x03\x00\x7c\x00\x02",), # Request - (FramerType.ASCII, b':110304008D008ECD\r\n', 17, 0, b"\x03\x04\x00\x8d\x00\x8e",), # Response - (FramerType.ASCII, b':1183026A\r\n', 17, 0, b'\x83\x02',), # Exception - (FramerType.ASCII, b':FF03007C000280\r\n', 255, 0, b"\x03\x00\x7c\x00\x02",), # Request - (FramerType.ASCII, b':FF0304008D008EDF\r\n', 255, 0, b"\x03\x04\x00\x8d\x00\x8e",), # Response - (FramerType.ASCII, b':FF83027C\r\n', 255, 0, b'\x83\x02',), # Exception - (FramerType.RTU, b'\x00\x03\x00\x7c\x00\x02\x04\x02', 0, 0, b"\x03\x00\x7c\x00\x02",), # Request - (FramerType.RTU, b'\x00\x03\x04\x00\x8d\x00\x8e\xfa\xbc', 0, 0, b"\x03\x04\x00\x8d\x00\x8e",), # Response - (FramerType.RTU, b'\x00\x83\x02\x91\x31', 0, 0, b'\x83\x02',), # Exception - (FramerType.RTU, b'\x11\x03\x00\x7c\x00\x02\x07\x43', 17, 0, b"\x03\x00\x7c\x00\x02",), # Request - (FramerType.RTU, b'\x11\x03\x04\x00\x8d\x00\x8e\xfb\xbd', 17, 0, b"\x03\x04\x00\x8d\x00\x8e",), # Response - (FramerType.RTU, b'\x11\x83\x02\xc1\x34', 17, 0, b'\x83\x02',), # Exception - (FramerType.RTU, b'\xff\x03\x00|\x00\x02\x10\x0d', 255, 0, b"\x03\x00\x7c\x00\x02",), # Request - (FramerType.RTU, b'\xff\x03\x04\x00\x8d\x00\x8e\xf5\xb3', 255, 0, b"\x03\x04\x00\x8d\x00\x8e",), # Response - (FramerType.RTU, b'\xff\x83\x02\xa1\x01', 255, 0, b'\x83\x02',), # Exception - (FramerType.SOCKET, b'\x00\x00\x00\x00\x00\x06\x00\x03\x00\x7c\x00\x02', 0, 0, b"\x03\x00\x7c\x00\x02",), # Request - (FramerType.SOCKET, b'\x00\x00\x00\x00\x00\x07\x00\x03\x04\x00\x8d\x00\x8e', 0, 0, b"\x03\x04\x00\x8d\x00\x8e",), # Response - (FramerType.SOCKET, b'\x00\x00\x00\x00\x00\x03\x00\x83\x02', 0, 0, b'\x83\x02',), # Exception - (FramerType.SOCKET, b'\x00\x00\x00\x00\x00\x06\x11\x03\x00\x7c\x00\x02', 17, 0, b"\x03\x00\x7c\x00\x02",), # Request - (FramerType.SOCKET, b'\x00\x00\x00\x00\x00\x07\x11\x03\x04\x00\x8d\x00\x8e', 17, 0, b"\x03\x04\x00\x8d\x00\x8e",), # Response - (FramerType.SOCKET, b'\x00\x00\x00\x00\x00\x03\x11\x83\x02', 17, 0, b'\x83\x02',), # Exception - (FramerType.SOCKET, b'\x00\x00\x00\x00\x00\x06\xff\x03\x00\x7c\x00\x02', 255, 0, b"\x03\x00\x7c\x00\x02",), # Request - (FramerType.SOCKET, b'\x00\x00\x00\x00\x00\x07\xff\x03\x04\x00\x8d\x00\x8e', 255, 0, b"\x03\x04\x00\x8d\x00\x8e",), # Response - (FramerType.SOCKET, b'\x00\x00\x00\x00\x00\x03\xff\x83\x02', 255, 0, b'\x83\x02',), # Exception - (FramerType.SOCKET, b'\x0c\x05\x00\x00\x00\x06\x00\x03\x00\x7c\x00\x02', 0, 3077, b"\x03\x00\x7c\x00\x02",), # Request - (FramerType.SOCKET, b'\x0c\x05\x00\x00\x00\x07\x00\x03\x04\x00\x8d\x00\x8e', 0, 3077, b"\x03\x04\x00\x8d\x00\x8e",), # Response - (FramerType.SOCKET, b'\x0c\x05\x00\x00\x00\x03\x00\x83\x02', 0, 3077, b'\x83\x02',), # Exception - (FramerType.SOCKET, b'\x0c\x05\x00\x00\x00\x06\x11\x03\x00\x7c\x00\x02', 17, 3077, b"\x03\x00\x7c\x00\x02",), # Request - (FramerType.SOCKET, b'\x0c\x05\x00\x00\x00\x07\x11\x03\x04\x00\x8d\x00\x8e', 17, 3077, b"\x03\x04\x00\x8d\x00\x8e",), # Response - (FramerType.SOCKET, b'\x0c\x05\x00\x00\x00\x03\x11\x83\x02', 17, 3077, b'\x83\x02',), # Exception - (FramerType.SOCKET, b'\x0c\x05\x00\x00\x00\x06\xff\x03\x00\x7c\x00\x02', 255, 3077, b"\x03\x00\x7c\x00\x02",), # Request - (FramerType.SOCKET, b'\x0c\x05\x00\x00\x00\x07\xff\x03\x04\x00\x8d\x00\x8e', 255, 3077, b"\x03\x04\x00\x8d\x00\x8e",), # Response - (FramerType.SOCKET, b'\x0c\x05\x00\x00\x00\x03\xff\x83\x02', 255, 3077, b'\x83\x02',), # Exception - (FramerType.TLS, b'\x03\x00\x7c\x00\x02', 0, 0, b"\x03\x00\x7c\x00\x02",), # Request - (FramerType.TLS, b'\x03\x04\x00\x8d\x00\x8e', 0, 0, b"\x03\x04\x00\x8d\x00\x8e",), # Response - (FramerType.TLS, b'\x83\x02', 0, 0, b'\x83\x02',), # Exception + (FramerType.ASCII, True, b':0003007C00027F\r\n', 0, 0, b"\x03\x00\x7c\x00\x02",), # Request + (FramerType.ASCII, False, b':000304008D008EDE\r\n', 0, 0, b"\x03\x04\x00\x8d\x00\x8e",), # Response + (FramerType.ASCII, False, b':0083027B\r\n', 0, 0, b'\x83\x02',), # Exception + (FramerType.ASCII, True, b':1103007C00026E\r\n', 17, 17, b"\x03\x00\x7c\x00\x02",), # Request + (FramerType.ASCII, False, b':110304008D008ECD\r\n', 17, 17, b"\x03\x04\x00\x8d\x00\x8e",), # Response + (FramerType.ASCII, False, b':1183026A\r\n', 17, 17, b'\x83\x02',), # Exception + (FramerType.ASCII, True, b':FF03007C000280\r\n', 255, 255, b"\x03\x00\x7c\x00\x02",), # Request + (FramerType.ASCII, False, b':FF0304008D008EDF\r\n', 255, 255, b"\x03\x04\x00\x8d\x00\x8e",), # Response + (FramerType.ASCII, False, b':FF83027C\r\n', 255, 255, b'\x83\x02',), # Exception + (FramerType.RTU, True, b'\x00\x03\x00\x7c\x00\x02\x04\x02', 0, 0, b"\x03\x00\x7c\x00\x02",), # Request + (FramerType.RTU, False, b'\x00\x03\x04\x00\x8d\x00\x8e\xfa\xbc', 0, 0, b"\x03\x04\x00\x8d\x00\x8e",), # Response + (FramerType.RTU, False, b'\x00\x83\x02\x91\x31', 0, 0, b'\x83\x02',), # Exception + (FramerType.RTU, True, b'\x11\x03\x00\x7c\x00\x02\x07\x43', 17, 17, b"\x03\x00\x7c\x00\x02",), # Request + (FramerType.RTU, False, b'\x11\x03\x04\x00\x8d\x00\x8e\xfb\xbd', 17, 17, b"\x03\x04\x00\x8d\x00\x8e",), # Response + (FramerType.RTU, False, b'\x11\x83\x02\xc1\x34', 17, 17, b'\x83\x02',), # Exception + (FramerType.RTU, True, b'\xff\x03\x00|\x00\x02\x10\x0d', 255, 255, b"\x03\x00\x7c\x00\x02",), # Request + (FramerType.RTU, False, b'\xff\x03\x04\x00\x8d\x00\x8e\xf5\xb3', 255, 255, b"\x03\x04\x00\x8d\x00\x8e",), # Response + (FramerType.RTU, False, b'\xff\x83\x02\xa1\x01', 255, 255, b'\x83\x02',), # Exception + (FramerType.SOCKET, True, b'\x00\x00\x00\x00\x00\x06\x00\x03\x00\x7c\x00\x02', 0, 0, b"\x03\x00\x7c\x00\x02",), # Request + (FramerType.SOCKET, False, b'\x00\x00\x00\x00\x00\x07\x00\x03\x04\x00\x8d\x00\x8e', 0, 0, b"\x03\x04\x00\x8d\x00\x8e",), # Response + (FramerType.SOCKET, False, b'\x00\x00\x00\x00\x00\x03\x00\x83\x02', 0, 0, b'\x83\x02',), # Exception + (FramerType.SOCKET, True, b'\x00\x00\x00\x00\x00\x06\x11\x03\x00\x7c\x00\x02', 17, 0, b"\x03\x00\x7c\x00\x02",), # Request + (FramerType.SOCKET, False, b'\x00\x00\x00\x00\x00\x07\x11\x03\x04\x00\x8d\x00\x8e', 17, 0, b"\x03\x04\x00\x8d\x00\x8e",), # Response + (FramerType.SOCKET, False, b'\x00\x00\x00\x00\x00\x03\x11\x83\x02', 17, 0, b'\x83\x02',), # Exception + (FramerType.SOCKET, True, b'\x00\x00\x00\x00\x00\x06\xff\x03\x00\x7c\x00\x02', 255, 0, b"\x03\x00\x7c\x00\x02",), # Request + (FramerType.SOCKET, False, b'\x00\x00\x00\x00\x00\x07\xff\x03\x04\x00\x8d\x00\x8e', 255, 0, b"\x03\x04\x00\x8d\x00\x8e",), # Response + (FramerType.SOCKET, False, b'\x00\x00\x00\x00\x00\x03\xff\x83\x02', 255, 0, b'\x83\x02',), # Exception + (FramerType.SOCKET, True, b'\x0c\x05\x00\x00\x00\x06\x00\x03\x00\x7c\x00\x02', 0, 3077, b"\x03\x00\x7c\x00\x02",), # Request + (FramerType.SOCKET, False, b'\x0c\x05\x00\x00\x00\x07\x00\x03\x04\x00\x8d\x00\x8e', 0, 3077, b"\x03\x04\x00\x8d\x00\x8e",), # Response + (FramerType.SOCKET, False, b'\x0c\x05\x00\x00\x00\x03\x00\x83\x02', 0, 3077, b'\x83\x02',), # Exception + (FramerType.SOCKET, True, b'\x0c\x05\x00\x00\x00\x06\x11\x03\x00\x7c\x00\x02', 17, 3077, b"\x03\x00\x7c\x00\x02",), # Request + (FramerType.SOCKET, False, b'\x0c\x05\x00\x00\x00\x07\x11\x03\x04\x00\x8d\x00\x8e', 17, 3077, b"\x03\x04\x00\x8d\x00\x8e",), # Response + (FramerType.SOCKET, False, b'\x0c\x05\x00\x00\x00\x03\x11\x83\x02', 17, 3077, b'\x83\x02',), # Exception + (FramerType.SOCKET, True, b'\x0c\x05\x00\x00\x00\x06\xff\x03\x00\x7c\x00\x02', 255, 3077, b"\x03\x00\x7c\x00\x02",), # Request + (FramerType.SOCKET, False, b'\x0c\x05\x00\x00\x00\x07\xff\x03\x04\x00\x8d\x00\x8e', 255, 3077, b"\x03\x04\x00\x8d\x00\x8e",), # Response + (FramerType.SOCKET, False, b'\x0c\x05\x00\x00\x00\x03\xff\x83\x02', 255, 3077, b'\x83\x02',), # Exception + (FramerType.TLS, True, b'\x03\x00\x7c\x00\x02', 0, 0, b"\x03\x00\x7c\x00\x02",), # Request + (FramerType.TLS, False, b'\x03\x04\x00\x8d\x00\x8e', 0, 0, b"\x03\x04\x00\x8d\x00\x8e",), # Response + (FramerType.TLS, False, b'\x83\x02', 0, 0, b'\x83\x02',), # Exception ] ) @pytest.mark.parametrize( @@ -275,54 +293,98 @@ def test_encode(self, frame, frame_expected, data, dev_id, tid, inx1, inx2, inx3 "single", ] ) - async def test_decode2(self, dummy_message, msg_type, data, dev_id, tid, expected, split): + async def test_decode_type(self, entry, dummy_framer, data, dev_id, tid, expected, split): """Test encode method.""" - if msg_type == FramerType.RTU: - pytest.skip("Waiting on implementation!") - if msg_type == FramerType.TLS and split != "no": + if entry == FramerType.TLS and split != "no": + return + if entry == FramerType.RTU: return - frame = dummy_message( - msg_type, - CommParams(), - False, - [1], - ) - frame.callback_request_response = mock.Mock() + dummy_framer.callback_request_response = mock.MagicMock() if split == "no": - used_len = frame.callback_data(data) - + used_len = dummy_framer.callback_data(data) elif split == "half": split_len = int(len(data) / 2) - assert not frame.callback_data(data[0:split_len]) - frame.callback_request_response.assert_not_called() - used_len = frame.callback_data(data) + assert not dummy_framer.callback_data(data[0:split_len]) + dummy_framer.callback_request_response.assert_not_called() + used_len = dummy_framer.callback_data(data) else: last = len(data) for i in range(0, last -1): - assert not frame.callback_data(data[0:i+1]) - frame.callback_request_response.assert_not_called() - used_len = frame.callback_data(data) + assert not dummy_framer.callback_data(data[0:i+1]) + dummy_framer.callback_request_response.assert_not_called() + used_len = dummy_framer.callback_data(data) assert used_len == len(data) - frame.callback_request_response.assert_called_with(expected, dev_id, tid) + dummy_framer.callback_request_response.assert_called_with(expected, dev_id, tid) @pytest.mark.parametrize( - ("frame", "data", "exp_len"), + ("entry", "data", "exp"), [ - (FramerAscii, b':0003007C00017F\r\n', 17), # bad crc - # (MessageAscii, b'abc:0003007C00027F\r\n', 3), # garble in front - # (MessageAscii, b':0003007C00017F\r\nabc', 17), # bad crc, garble after - # (MessageAscii, b':0003007C00017F\r\n:0003', 17), # part second message - (FramerRTU, b'\x00\x83\x02\x91\x31', 0), # bad crc - # (MessageRTU, b'\x00\x83\x02\x91\x31', 0), # garble in front - # (MessageRTU, b'\x00\x83\x02\x91\x31', 0), # garble after - # (MessageRTU, b'\x00\x83\x02\x91\x31', 0), # part second message + (FramerType.ASCII, b':0003007C00017F\r\n', [ # bad crc + (17, b''), + ]), + (FramerType.ASCII, b':0003007C00027F\r\n:0003007C00027F\r\n', [ # double good crc + (17, b'\x03\x00\x7c\x00\x02'), + (17, b'\x03\x00\x7c\x00\x02'), + ]), + (FramerType.ASCII, b':0003007C00017F\r\n:0003007C00027F\r\n', [ # bad crc + good CRC + (34, b'\x03\x00\x7c\x00\x02'), + ]), + (FramerType.ASCII, b'abc:0003007C00027F\r\n', [ # garble in front + (20, b'\x03\x00\x7c\x00\x02'), + ]), + (FramerType.ASCII, b':0003007C00017F\r\nabc', [ # bad crc, garble after + (17, b''), + ]), + (FramerType.ASCII, b':0003007C00017F\r\nabcdefghijkl', [ # bad crc, garble after + (29, b''), + ]), + (FramerType.ASCII, b':0003007C00027F\r\nabc', [ # good crc, garble after + (17, b'\x03\x00\x7c\x00\x02'), + ]), + (FramerType.ASCII, b':0003007C00017F\r\n:0003', [ # bad crc, part second framer + (17, b''), + ]), + (FramerType.SOCKET, b'\x00\x00\x00\x00\x00\x06\x00\x03\x00\x7c\x00\x02\x00\x00\x00\x00\x00\x06\x00\x03\x00\x7c\x00\x02', [ # double good crc + (12, b"\x03\x00\x7c\x00\x02"), + (12, b"\x03\x00\x7c\x00\x02"), + ]), + (FramerType.RTU, b'\x00\x83\x02\x91\x21', [ # bad crc + (5, b''), + ]), + #(FramerType.RTU, b'\x00\x83\x02\xf0\x91\x31', [ # dummy char in stream, bad crc + # (5, b''), + #]), + # (FramerType.RTU, b'\x00\x83\x02\x91\x21\x00\x83\x02\x91\x31', [ # bad crc + good CRC + # (10, b'\x83\x02'), + #]), + #(FramerType.RTU, b'\x00\x83\x02\xf0\x91\x31\x00\x83\x02\x91\x31', [ # dummy char in stream, bad crc + good CRC + # (11, b''), + #]), + + # (FramerType.RTU, b'\x00\x83\x02\x91\x31', 0), # garble in front + # (FramerType.ASCII, b'abc:0003007C00027F\r\n', [ # garble in front + # (20, b'\x03\x00\x7c\x00\x02'), + # ]), + + # (FramerType.RTU, b'\x00\x83\x02\x91\x31', 0), # garble after + # (FramerType.ASCII, b':0003007C00017F\r\nabc', [ # bad crc, garble after + # (17, b''), + # ]), + # (FramerType.ASCII, b':0003007C00017F\r\nabcdefghijkl', [ # bad crc, garble after + # (29, b''), + # ]), + # (FramerType.ASCII, b':0003007C00027F\r\nabc', [ # good crc, garble after + # (17, b'\x03\x00\x7c\x00\x02'), + # ]), + # (FramerType.RTU, b'\x00\x83\x02\x91\x31', 0), # part second framer + # (FramerType.ASCII, b':0003007C00017F\r\n:0003', [ # bad crc, part second framer + # (17, b''), + # ]), ] ) - async def test_decode_bad_crc(self, frame, data, exp_len): + async def test_decode_complicated(self, dummy_framer, data, exp): """Test encode method.""" - if frame == FramerRTU: - pytest.skip("Waiting for implementation.") - frame_obj = frame() - used_len, _, _, data = frame_obj.decode(data) - assert used_len == exp_len - assert not data + for ent in exp: + used_len, _, _, res_data = dummy_framer.handle.decode(data) + assert used_len == ent[0] + assert res_data == ent[1] diff --git a/test/framers/test_rtu.py b/test/framers/test_rtu.py index dd30f8218..13bdd8770 100644 --- a/test/framers/test_rtu.py +++ b/test/framers/test_rtu.py @@ -13,6 +13,29 @@ def prepare_frame(): """Return message object.""" return FramerRTU() + @pytest.mark.skip() + @pytest.mark.parametrize( + ("packet", "used_len", "res_id", "res"), + [ + (b':010100010001FC\r\n', 17, 1, b'\x01\x00\x01\x00\x01'), + (b':00010001000AF4\r\n', 17, 0, b'\x01\x00\x01\x00\x0a'), + (b':01010001000AF3\r\n', 17, 1, b'\x01\x00\x01\x00\x0a'), + (b':61620001000A32\r\n', 17, 97, b'\x62\x00\x01\x00\x0a'), + (b':01270001000ACD\r\n', 17, 1, b'\x27\x00\x01\x00\x0a'), + (b':010100', 0, 0, b''), # short frame + (b':00010001000AF4', 0, 0, b''), + (b'abc:00010001000AF4', 3, 0, b''), # garble before frame + (b'abc00010001000AF4', 17, 0, b''), # only garble + (b':01010001000A00\r\n', 17, 0, b''), + ], + ) + def test_decode(self, frame, packet, used_len, res_id, res): + """Test decode.""" + res_len, tid, dev_id, data = frame.decode(packet) + assert res_len == used_len + assert data == res + assert tid == res_id + assert dev_id == res_id @pytest.mark.parametrize( ("data", "dev_id", "res_msg"), From f0d68422cbf923ec68fa4057a11f0c4ead06182e Mon Sep 17 00:00:00 2001 From: jan iversen Date: Mon, 22 Jul 2024 21:53:10 +0200 Subject: [PATCH 79/88] remove kwargs client. (#2244) --- .gitignore | 1 + examples/client_async.py | 2 - examples/client_custom_msg.py | 2 +- examples/client_sync.py | 5 - examples/server_async.py | 1 - examples/server_hook.py | 6 +- examples/server_sync.py | 1 - examples/simple_async_client.py | 2 - examples/simple_sync_client.py | 2 - pymodbus/client/base.py | 147 ++++----------------------- pymodbus/client/serial.py | 110 ++++++++++++++------ pymodbus/client/tcp.py | 115 ++++++++++++++------- pymodbus/client/tls.py | 138 ++++++++++++++++--------- pymodbus/client/udp.py | 108 ++++++++++++++------ pymodbus/transaction.py | 21 +--- test/framers/test_old_framers.py | 10 +- test/framers/test_tbc_transaction.py | 12 +-- test/sub_client/test_client.py | 81 +++++++++------ test/sub_client/test_client_sync.py | 6 +- test/test_transaction.py | 12 +-- 20 files changed, 417 insertions(+), 365 deletions(-) diff --git a/.gitignore b/.gitignore index c276f7914..c2a1d0c33 100644 --- a/.gitignore +++ b/.gitignore @@ -19,3 +19,4 @@ prof/ /pymodbus.egg-info/ venv downloaded_files/ +pymodbus.log diff --git a/examples/client_async.py b/examples/client_async.py index 081770e87..16ed44c8d 100755 --- a/examples/client_async.py +++ b/examples/client_async.py @@ -94,7 +94,6 @@ def setup_async_client(description=None, cmdline=None): # parity="N", # stopbits=1, # handle_local_echo=False, - # strict=True, ) elif args.comm == "tls": client = modbusClient.AsyncModbusTlsClient( @@ -111,7 +110,6 @@ def setup_async_client(description=None, cmdline=None): keyfile=helper.get_certificate("key"), # password="none", ), - server_hostname="localhost", ) else: raise RuntimeError(f"Unknown commtype {args.comm}") diff --git a/examples/client_custom_msg.py b/examples/client_custom_msg.py index 8fb6cb3a5..b125d02e6 100755 --- a/examples/client_custom_msg.py +++ b/examples/client_custom_msg.py @@ -118,7 +118,7 @@ def __init__(self, address, count=None, slave=0, transaction=0, protocol=0, skip async def main(host="localhost", port=5020): """Run versions of read coil.""" - async with ModbusClient(host=host, port=port, framer_name=FramerType.SOCKET) as client: + async with ModbusClient(host=host, port=port, framer=FramerType.SOCKET) as client: await client.connect() # create a response object to control it works diff --git a/examples/client_sync.py b/examples/client_sync.py index 58bf19966..15f5b0ae1 100755 --- a/examples/client_sync.py +++ b/examples/client_sync.py @@ -68,7 +68,6 @@ def setup_sync_client(description=None, cmdline=None): timeout=args.timeout, # retries=3, # retry_on_empty=False,y - # strict=True, # TCP setup parameters # source_address=("localhost", 0), ) @@ -81,7 +80,6 @@ def setup_sync_client(description=None, cmdline=None): timeout=args.timeout, # retries=3, # retry_on_empty=False, - # strict=True, # UDP setup parameters # source_address=None, ) @@ -93,14 +91,12 @@ def setup_sync_client(description=None, cmdline=None): timeout=args.timeout, # retries=3, # retry_on_empty=False, - # strict=True, # Serial setup parameters baudrate=args.baudrate, # bytesize=8, # parity="N", # stopbits=1, # handle_local_echo=False, - # strict=True, ) elif args.comm == "tls": client = modbusClient.ModbusTlsClient( @@ -117,7 +113,6 @@ def setup_sync_client(description=None, cmdline=None): keyfile=helper.get_certificate("key"), # password=None, ), - server_hostname="localhost", ) return client diff --git a/examples/server_async.py b/examples/server_async.py index a4f71cb71..6835426d5 100755 --- a/examples/server_async.py +++ b/examples/server_async.py @@ -201,7 +201,6 @@ async def run_async_server(args): # handle_local_echo=False, # Handle local echo of the USB-to-RS485 adaptor # ignore_missing_slaves=True, # ignore request to a missing slave # broadcast_enable=False, # treat slave_id 0 as broadcast address, - # strict=True, # use strict timing, t1.5 for Modbus RTU ) elif args.comm == "tls": address = (args.host if args.host else "", args.port if args.port else None) diff --git a/examples/server_hook.py b/examples/server_hook.py index 89a8baecd..a50035efb 100755 --- a/examples/server_hook.py +++ b/examples/server_hook.py @@ -17,11 +17,7 @@ class Manipulator: - """A Class to run the server. - - Using a class allows the easy use of global variables, but - are not strictly needed - """ + """A Class to run the server.""" message_count: int = 1 server: ModbusTcpServer = None diff --git a/examples/server_sync.py b/examples/server_sync.py index d60fd9d05..66e2d7f4a 100755 --- a/examples/server_sync.py +++ b/examples/server_sync.py @@ -110,7 +110,6 @@ def run_sync_server(args): # handle_local_echo=False, # Handle local echo of the USB-to-RS485 adaptor # ignore_missing_slaves=True, # ignore request to a missing slave # broadcast_enable=False, # treat slave_id 0 as broadcast address, - # strict=True, # use strict timing, t1.5 for Modbus RTU ) elif args.comm == "tls": address = ("", args.port) if args.port else None diff --git a/examples/simple_async_client.py b/examples/simple_async_client.py index 500112799..2b08686a0 100755 --- a/examples/simple_async_client.py +++ b/examples/simple_async_client.py @@ -53,7 +53,6 @@ async def run_async_simple_client(comm, host, port, framer=FramerType.SOCKET): # timeout=10, # retries=3, # retry_on_empty=False, - # strict=True, baudrate=9600, bytesize=8, parity="N", @@ -72,7 +71,6 @@ async def run_async_simple_client(comm, host, port, framer=FramerType.SOCKET): certfile="../examples/certificates/pymodbus.crt", keyfile="../examples/certificates/pymodbus.key", # password="none", - server_hostname="localhost", ) else: print(f"Unknown client {comm} selected") diff --git a/examples/simple_sync_client.py b/examples/simple_sync_client.py index 2cd363d8b..d2dfd3a57 100755 --- a/examples/simple_sync_client.py +++ b/examples/simple_sync_client.py @@ -55,7 +55,6 @@ def run_sync_simple_client(comm, host, port, framer=FramerType.SOCKET): # timeout=10, # retries=3, # retry_on_empty=False, - # strict=True, baudrate=9600, bytesize=8, parity="N", @@ -74,7 +73,6 @@ def run_sync_simple_client(comm, host, port, framer=FramerType.SOCKET): certfile="../examples/certificates/pymodbus.crt", keyfile="../examples/certificates/pymodbus.key", # password=None, - server_hostname="localhost", ) else: print(f"Unknown client {comm} selected") diff --git a/pymodbus/client/base.py b/pymodbus/client/base.py index 51602641b..91412901b 100644 --- a/pymodbus/client/base.py +++ b/pymodbus/client/base.py @@ -5,8 +5,7 @@ import socket from abc import abstractmethod from collections.abc import Awaitable, Callable -from dataclasses import dataclass -from typing import Any, cast +from typing import cast from pymodbus.client.mixin import ModbusClientMixin from pymodbus.client.modbusclientprotocol import ModbusClientProtocol @@ -16,79 +15,39 @@ from pymodbus.logging import Log from pymodbus.pdu import ModbusRequest, ModbusResponse from pymodbus.transaction import SyncModbusTransactionManager -from pymodbus.transport import CommParams, CommType +from pymodbus.transport import CommParams from pymodbus.utilities import ModbusTransactionState class ModbusBaseClient(ModbusClientMixin[Awaitable[ModbusResponse]]): """**ModbusBaseClient**. - Fixed parameters: - - :param framer: Framer enum name - - Optional parameters: - - :param timeout: Timeout for a request, in seconds. - :param retries: Max number of retries per request. - :param retry_on_empty: Retry on empty response. - :param broadcast_enable: True to treat id 0 as broadcast address. - :param reconnect_delay: Minimum delay in seconds.milliseconds before reconnecting. - :param reconnect_delay_max: Maximum delay in seconds.milliseconds before reconnecting. - :param on_connect_callback: Will be called when connected/disconnected (bool parameter) - :param no_resend_on_retry: Do not resend request when retrying due to missing response. - :param comm_type: Type of communication (set by interface class) - :param kwargs: Experimental parameters. - - .. tip:: - **reconnect_delay** doubles automatically with each unsuccessful connect, from - **reconnect_delay** to **reconnect_delay_max**. - Set `reconnect_delay=0` to avoid automatic reconnection. - :mod:`ModbusBaseClient` is normally not referenced outside :mod:`pymodbus`. - - **Application methods, common to all clients**: """ - def __init__( # pylint: disable=too-many-arguments + def __init__( self, framer: FramerType, - timeout: float = 3, retries: int = 3, + comm_params: CommParams | None = None, + retry_on_empty: bool = False, broadcast_enable: bool = False, - reconnect_delay: float = 0.1, - reconnect_delay_max: float = 300, - on_connect_callback: Callable[[bool], None] | None = None, no_resend_on_retry: bool = False, - comm_type: CommType | None = None, - source_address: tuple[str, int] | None = None, - **kwargs: Any, + on_connect_callback: Callable[[bool], None] | None = None, ) -> None: """Initialize a client instance.""" ModbusClientMixin.__init__(self) # type: ignore[arg-type] + if comm_params: + self.comm_params = comm_params + self.retries = retries + self.retry_on_empty = retry_on_empty self.ctx = ModbusClientProtocol( framer, - CommParams( - comm_type=comm_type, - comm_name="comm", - source_address=source_address, - reconnect_delay=reconnect_delay, - reconnect_delay_max=reconnect_delay_max, - timeout_connect=timeout, - host=kwargs.get("host", None), - port=kwargs.get("port", 0), - sslctx=kwargs.get("sslctx", None), - baudrate=kwargs.get("baudrate", None), - bytesize=kwargs.get("bytesize", None), - parity=kwargs.get("parity", None), - stopbits=kwargs.get("stopbits", None), - handle_local_echo=kwargs.get("handle_local_echo", False), - ), + self.comm_params, on_connect_callback, ) self.no_resend_on_retry = no_resend_on_retry self.broadcast_enable = broadcast_enable - self.retries = retries # Common variables. self.use_udp = False @@ -97,9 +56,6 @@ def __init__( # pylint: disable=too-many-arguments self.silent_interval: float = 0 self._lock = asyncio.Lock() - # ----------------------------------------------------------------------- # - # Client external interface - # ----------------------------------------------------------------------- # @property def connected(self) -> bool: """Return state of connection.""" @@ -115,7 +71,6 @@ async def connect(self) -> bool: ) return await self.ctx.connect() - def register(self, custom_response_class: ModbusResponse) -> None: """Register a custom response class with the decoder (call **sync**). @@ -155,9 +110,6 @@ def execute(self, request: ModbusRequest): raise ConnectionException(f"Not connected[{self!s}]") return self.async_execute(request) - # ----------------------------------------------------------------------- # - # Merged client methods - # ----------------------------------------------------------------------- # async def async_execute(self, request) -> ModbusResponse: """Execute requests asynchronously.""" request.transaction_id = self.ctx.transaction.getNextTID() @@ -199,9 +151,6 @@ def build_response(self, request: ModbusRequest): self.ctx.transaction.addTransaction(request) return my_future - # ----------------------------------------------------------------------- # - # The magic methods - # ----------------------------------------------------------------------- # async def __aenter__(self): """Implement the client with enter block. @@ -228,79 +177,25 @@ def __str__(self): class ModbusBaseSyncClient(ModbusClientMixin[ModbusResponse]): """**ModbusBaseClient**. - Fixed parameters: - - :param framer: Framer enum name - - Optional parameters: - - :param timeout: Timeout for a request, in seconds. - :param retries: Max number of retries per request. - :param retry_on_empty: Retry on empty response. - :param broadcast_enable: True to treat id 0 as broadcast address. - :param reconnect_delay: Minimum delay in seconds.milliseconds before reconnecting. - :param reconnect_delay_max: Maximum delay in seconds.milliseconds before reconnecting. - :param no_resend_on_retry: Do not resend request when retrying due to missing response. - :param source_address: source address of client - :param kwargs: Experimental parameters. - - .. tip:: - **reconnect_delay** doubles automatically with each unsuccessful connect, from - **reconnect_delay** to **reconnect_delay_max**. - Set `reconnect_delay=0` to avoid automatic reconnection. - :mod:`ModbusBaseClient` is normally not referenced outside :mod:`pymodbus`. - - **Application methods, common to all clients**: """ - @dataclass - class _params: - """Parameter class.""" - - retries: int | None = None - retry_on_empty: bool | None = None - broadcast_enable: bool | None = None - reconnect_delay: int | None = None - source_address: tuple[str, int] | None = None - - def __init__( # pylint: disable=too-many-arguments + def __init__( self, framer: FramerType, - timeout: float = 3, retries: int = 3, + comm_params: CommParams | None = None, retry_on_empty: bool = False, broadcast_enable: bool = False, - reconnect_delay: float = 0.1, - reconnect_delay_max: float = 300.0, no_resend_on_retry: bool = False, - comm_type: CommType | None = None, - source_address: tuple[str, int] | None = None, - **kwargs: Any, ) -> None: """Initialize a client instance.""" ModbusClientMixin.__init__(self) # type: ignore[arg-type] - self.comm_params = CommParams( - comm_type=comm_type, - comm_name="comm", - source_address=source_address, - reconnect_delay=reconnect_delay, - reconnect_delay_max=reconnect_delay_max, - timeout_connect=timeout, - host=kwargs.get("host", None), - port=kwargs.get("port", 0), - sslctx=kwargs.get("sslctx", None), - baudrate=kwargs.get("baudrate", None), - bytesize=kwargs.get("bytesize", None), - parity=kwargs.get("parity", None), - stopbits=kwargs.get("stopbits", None), - handle_local_echo=kwargs.get("handle_local_echo", False), - ) - self.params = self._params() - self.params.retries = int(retries) - self.params.retry_on_empty = bool(retry_on_empty) - self.params.broadcast_enable = bool(broadcast_enable) - self.retry_on_empty: int = 0 + if comm_params: + self.comm_params = comm_params + self.retries = retries + self.broadcast_enable = bool(broadcast_enable) + self.retry_on_empty = retry_on_empty self.no_resend_on_retry = no_resend_on_retry self.slaves: list[int] = [] @@ -310,12 +205,10 @@ def __init__( # pylint: disable=too-many-arguments )(ClientDecoder(), self) self.transaction = SyncModbusTransactionManager( self, - kwargs.get("backoff", 0.3), retry_on_empty, - kwargs.get("retry_on_invalid", False), - retries, + self.retries, ) - self.reconnect_delay_current = self.params.reconnect_delay or 0 + self.reconnect_delay_current = self.comm_params.reconnect_delay or 0 self.use_udp = False self.state = ModbusTransactionState.IDLE self.last_frame_end: float | None = 0 diff --git a/pymodbus/client/serial.py b/pymodbus/client/serial.py index 2972bcde0..089d3a017 100644 --- a/pymodbus/client/serial.py +++ b/pymodbus/client/serial.py @@ -2,6 +2,7 @@ from __future__ import annotations import time +from collections.abc import Callable from functools import partial from typing import TYPE_CHECKING, Any @@ -9,7 +10,7 @@ from pymodbus.exceptions import ConnectionException from pymodbus.framer import FramerType from pymodbus.logging import Log -from pymodbus.transport import CommType +from pymodbus.transport import CommParams, CommType from pymodbus.utilities import ModbusTransactionState @@ -32,24 +33,26 @@ class AsyncModbusSerialClient(ModbusBaseClient): Optional parameters: + :param framer: Framer name, default FramerType.RTU :param baudrate: Bits per second. :param bytesize: Number of bits per byte 7-8. :param parity: 'E'ven, 'O'dd or 'N'one :param stopbits: Number of stop bits 1, 1.5, 2. :param handle_local_echo: Discard local echo from dongle. - - Common optional parameters: - - :param framer: Framer enum name - :param timeout: Timeout for a request, in seconds. + :param name: Set communication name, used in logging + :param reconnect_delay: Minimum delay in seconds.milliseconds before reconnecting. + :param reconnect_delay_max: Maximum delay in seconds.milliseconds before reconnecting. + :param timeout: Timeout for a connection request, in seconds. :param retries: Max number of retries per request. :param retry_on_empty: Retry on empty response. :param broadcast_enable: True to treat id 0 as broadcast address. - :param reconnect_delay: Minimum delay in seconds.milliseconds before reconnecting. - :param reconnect_delay_max: Maximum delay in seconds.milliseconds before reconnecting. - :param on_reconnect_callback: Function that will be called just before a reconnection attempt. :param no_resend_on_retry: Do not resend request when retrying due to missing response. - :param kwargs: Experimental parameters. + :param on_reconnect_callback: Function that will be called just before a reconnection attempt. + + .. tip:: + **reconnect_delay** doubles automatically with each unsuccessful connect, from + **reconnect_delay** to **reconnect_delay_max**. + Set `reconnect_delay=0` to avoid automatic reconnection. Example:: @@ -65,7 +68,7 @@ async def run(): Please refer to :ref:`Pymodbus internals` for advanced usage. """ - def __init__( + def __init__( # pylint: disable=too-many-arguments self, port: str, framer: FramerType = FramerType.RTU, @@ -73,6 +76,18 @@ def __init__( bytesize: int = 8, parity: str = "N", stopbits: int = 1, + handle_local_echo: bool = False, + name: str = "comm", + reconnect_delay: float = 0.1, + reconnect_delay_max: float = 300, + timeout: float = 3, + retries: int = 3, + retry_on_empty: bool = False, + broadcast_enable: bool = False, + no_resend_on_retry: bool = False, + on_connect_callback: Callable[[bool], None] | None = None, + + # ----- OLD ------ **kwargs: Any, ) -> None: """Initialize Asyncio Modbus Serial Client.""" @@ -81,15 +96,27 @@ def __init__( "Serial client requires pyserial " 'Please install with "pip install pyserial" and try again.' ) - ModbusBaseClient.__init__( - self, - framer, + self.comm_params = CommParams( comm_type=CommType.SERIAL, host=port, baudrate=baudrate, bytesize=bytesize, parity=parity, stopbits=stopbits, + handle_local_echo=handle_local_echo, + comm_name=name, + reconnect_delay=reconnect_delay, + reconnect_delay_max=reconnect_delay_max, + timeout_connect=timeout, + ) + ModbusBaseClient.__init__( + self, + framer, + retries=retries, + retry_on_empty=retry_on_empty, + broadcast_enable=broadcast_enable, + no_resend_on_retry=no_resend_on_retry, + on_connect_callback=on_connect_callback, **kwargs, ) @@ -107,25 +134,25 @@ class ModbusSerialClient(ModbusBaseSyncClient): Optional parameters: + :param framer: Framer name, default FramerType.RTU :param baudrate: Bits per second. :param bytesize: Number of bits per byte 7-8. :param parity: 'E'ven, 'O'dd or 'N'one :param stopbits: Number of stop bits 0-2. :param handle_local_echo: Discard local echo from dongle. - - Common optional parameters: - - :param framer: Framer enum name - :param timeout: Timeout for a request, in seconds. + :param name: Set communication name, used in logging + :param reconnect_delay: Minimum delay in seconds.milliseconds before reconnecting. + :param reconnect_delay_max: Maximum delay in seconds.milliseconds before reconnecting. + :param timeout: Timeout for a connection request, in seconds. :param retries: Max number of retries per request. :param retry_on_empty: Retry on empty response. - :param strict: Strict timing, 1.5 character between requests. :param broadcast_enable: True to treat id 0 as broadcast address. - :param reconnect_delay: Minimum delay in seconds.milliseconds before reconnecting. - :param reconnect_delay_max: Maximum delay in seconds.milliseconds before reconnecting. - :param on_reconnect_callback: Function that will be called just before a reconnection attempt. :param no_resend_on_retry: Do not resend request when retrying due to missing response. - :param kwargs: Experimental parameters. + + .. tip:: + **reconnect_delay** doubles automatically with each unsuccessful connect, from + **reconnect_delay** to **reconnect_delay_max**. + Set `reconnect_delay=0` to avoid automatic reconnection. Example:: @@ -147,7 +174,7 @@ def run(): inter_byte_timeout: float = 0 silent_interval: float = 0 - def __init__( + def __init__( # pylint: disable=too-many-arguments self, port: str, framer: FramerType = FramerType.RTU, @@ -155,25 +182,43 @@ def __init__( bytesize: int = 8, parity: str = "N", stopbits: int = 1, - strict: bool = True, + handle_local_echo: bool = False, + name: str = "comm", + reconnect_delay: float = 0.1, + reconnect_delay_max: float = 300, + timeout: float = 3, + retries: int = 3, + retry_on_empty: bool = False, + broadcast_enable: bool = False, + no_resend_on_retry: bool = False, + + # ----- OLD ------ **kwargs: Any, ) -> None: """Initialize Modbus Serial Client.""" - super().__init__( - framer, + self.comm_params = CommParams( comm_type=CommType.SERIAL, host=port, baudrate=baudrate, bytesize=bytesize, parity=parity, stopbits=stopbits, + handle_local_echo=handle_local_echo, + comm_name=name, + reconnect_delay=reconnect_delay, + reconnect_delay_max=reconnect_delay_max, + timeout_connect=timeout, + ) + super().__init__( + framer, + retries=retries, + retry_on_empty=retry_on_empty, + broadcast_enable=broadcast_enable, + no_resend_on_retry=no_resend_on_retry, **kwargs, ) self.socket: serial.Serial | None = None - self.strict = bool(strict) - self.last_frame_end = None - self._t0 = float(1 + bytesize + stopbits) / baudrate # Check every 4 bytes / 2 registers if the reading is ready @@ -207,8 +252,7 @@ def connect(self) -> bool: parity=self.comm_params.parity, exclusive=True, ) - if self.strict: - self.socket.inter_byte_timeout = self.inter_byte_timeout + self.socket.inter_byte_timeout = self.inter_byte_timeout self.last_frame_end = None # except serial.SerialException as msg: # pyserial raises undocumented exceptions like termios diff --git a/pymodbus/client/tcp.py b/pymodbus/client/tcp.py index e6ff7bf1f..b1ed98572 100644 --- a/pymodbus/client/tcp.py +++ b/pymodbus/client/tcp.py @@ -4,13 +4,14 @@ import select import socket import time +from collections.abc import Callable from typing import Any from pymodbus.client.base import ModbusBaseClient, ModbusBaseSyncClient from pymodbus.exceptions import ConnectionException from pymodbus.framer import FramerType from pymodbus.logging import Log -from pymodbus.transport import CommType +from pymodbus.transport import CommParams, CommType class AsyncModbusTcpClient(ModbusBaseClient): @@ -22,21 +23,23 @@ class AsyncModbusTcpClient(ModbusBaseClient): Optional parameters: + :param framer: Framer name, default FramerType.SOCKET :param port: Port used for communication + :param name: Set communication name, used in logging :param source_address: source address of client - - Common optional parameters: - - :param framer: Framer enum name - :param timeout: Timeout for a request, in seconds. + :param reconnect_delay: Minimum delay in seconds.milliseconds before reconnecting. + :param reconnect_delay_max: Maximum delay in seconds.milliseconds before reconnecting. + :param timeout: Timeout for a connection request, in seconds. :param retries: Max number of retries per request. :param retry_on_empty: Retry on empty response. :param broadcast_enable: True to treat id 0 as broadcast address. - :param reconnect_delay: Minimum delay in seconds.milliseconds before reconnecting. - :param reconnect_delay_max: Maximum delay in seconds.milliseconds before reconnecting. - :param on_reconnect_callback: Function that will be called just before a reconnection attempt. :param no_resend_on_retry: Do not resend request when retrying due to missing response. - :param kwargs: Experimental parameters. + :param on_reconnect_callback: Function that will be called just before a reconnection attempt. + + .. tip:: + **reconnect_delay** doubles automatically with each unsuccessful connect, from + **reconnect_delay** to **reconnect_delay_max**. + Set `reconnect_delay=0` to avoid automatic reconnection. Example:: @@ -54,24 +57,45 @@ async def run(): socket: socket.socket | None - def __init__( + def __init__( # pylint: disable=too-many-arguments self, host: str, - port: int = 502, framer: FramerType = FramerType.SOCKET, + port: int = 502, + name: str = "comm", source_address: tuple[str, int] | None = None, + reconnect_delay: float = 0.1, + reconnect_delay_max: float = 300, + timeout: float = 3, + retries: int = 3, + retry_on_empty: bool = False, + broadcast_enable: bool = False, + no_resend_on_retry: bool = False, + on_connect_callback: Callable[[bool], None] | None = None, + + # ----- OLD ------ **kwargs: Any, ) -> None: """Initialize Asyncio Modbus TCP Client.""" - if "comm_type" not in kwargs: - kwargs["comm_type"] = CommType.TCP - if source_address: - kwargs["source_address"] = source_address + if not hasattr(self,"comm_params"): + self.comm_params = CommParams( + comm_type=CommType.TCP, + host=host, + port=port, + comm_name=name, + source_address=source_address, + reconnect_delay=reconnect_delay, + reconnect_delay_max=reconnect_delay_max, + timeout_connect=timeout, + ) ModbusBaseClient.__init__( self, framer, - host=host, - port=port, + retries=retries, + retry_on_empty=retry_on_empty, + broadcast_enable=broadcast_enable, + no_resend_on_retry=no_resend_on_retry, + on_connect_callback=on_connect_callback, **kwargs, ) @@ -89,21 +113,22 @@ class ModbusTcpClient(ModbusBaseSyncClient): Optional parameters: + :param framer: Framer name, default FramerType.SOCKET :param port: Port used for communication + :param name: Set communication name, used in logging :param source_address: source address of client - - Common optional parameters: - - :param framer: Framer enum name - :param timeout: Timeout for a request, in seconds. + :param reconnect_delay: Minimum delay in seconds.milliseconds before reconnecting. + :param reconnect_delay_max: Maximum delay in seconds.milliseconds before reconnecting. + :param timeout: Timeout for a connection request, in seconds. :param retries: Max number of retries per request. :param retry_on_empty: Retry on empty response. :param broadcast_enable: True to treat id 0 as broadcast address. - :param reconnect_delay: Minimum delay in seconds.milliseconds before reconnecting. - :param reconnect_delay_max: Maximum delay in seconds.milliseconds before reconnecting. - :param on_reconnect_callback: Function that will be called just before a reconnection attempt. :param no_resend_on_retry: Do not resend request when retrying due to missing response. - :param kwargs: Experimental parameters. + + .. tip:: + **reconnect_delay** doubles automatically with each unsuccessful connect, from + **reconnect_delay** to **reconnect_delay_max**. + Set `reconnect_delay=0` to avoid automatic reconnection. Example:: @@ -123,24 +148,44 @@ async def run(): socket: socket.socket | None - def __init__( + def __init__( # pylint: disable=too-many-arguments self, host: str, - port: int = 502, framer: FramerType = FramerType.SOCKET, + port: int = 502, + name: str = "comm", source_address: tuple[str, int] | None = None, + reconnect_delay: float = 0.1, + reconnect_delay_max: float = 300, + timeout: float = 3, + retries: int = 3, + retry_on_empty: bool = False, + broadcast_enable: bool = False, + no_resend_on_retry: bool = False, + + # ----- OLD ------ **kwargs: Any, ) -> None: """Initialize Modbus TCP Client.""" - if "comm_type" not in kwargs: - kwargs["comm_type"] = CommType.TCP + if not hasattr(self,"comm_params"): + self.comm_params = CommParams( + comm_type=CommType.TCP, + host=host, + port=port, + comm_name=name, + source_address=source_address, + reconnect_delay=reconnect_delay, + reconnect_delay_max=reconnect_delay_max, + timeout_connect=timeout, + ) super().__init__( framer, - host=host, - port=port, + retries=retries, + retry_on_empty=retry_on_empty, + broadcast_enable=broadcast_enable, + no_resend_on_retry=no_resend_on_retry, **kwargs, ) - self.params.source_address = source_address self.socket = None @property @@ -156,7 +201,7 @@ def connect(self): self.socket = socket.create_connection( (self.comm_params.host, self.comm_params.port), timeout=self.comm_params.timeout_connect, - source_address=self.params.source_address, + source_address=self.comm_params.source_address, ) Log.debug( "Connection to Modbus server established. Socket {}", diff --git a/pymodbus/client/tls.py b/pymodbus/client/tls.py index e90640004..22eb1899b 100644 --- a/pymodbus/client/tls.py +++ b/pymodbus/client/tls.py @@ -3,6 +3,7 @@ import socket import ssl +from collections.abc import Callable from typing import Any from pymodbus.client.tcp import AsyncModbusTcpClient, ModbusTcpClient @@ -20,23 +21,24 @@ class AsyncModbusTlsClient(AsyncModbusTcpClient): Optional parameters: + :param sslctx: SSLContext to use for TLS + :param framer: Framer name, default FramerType.TLS :param port: Port used for communication + :param name: Set communication name, used in logging :param source_address: Source address of client - :param sslctx: SSLContext to use for TLS - :param server_hostname: Bind certificate to host - - Common optional parameters: - - :param framer: Framer enum name - :param timeout: Timeout for a request, in seconds. + :param reconnect_delay: Minimum delay in seconds.milliseconds before reconnecting. + :param reconnect_delay_max: Maximum delay in seconds.milliseconds before reconnecting. + :param timeout: Timeout for a connection request, in seconds. :param retries: Max number of retries per request. :param retry_on_empty: Retry on empty response. :param broadcast_enable: True to treat id 0 as broadcast address. - :param reconnect_delay: Minimum delay in seconds.milliseconds before reconnecting. - :param reconnect_delay_max: Maximum delay in seconds.milliseconds before reconnecting. - :param on_reconnect_callback: Function that will be called just before a reconnection attempt. :param no_resend_on_retry: Do not resend request when retrying due to missing response. - :param kwargs: Experimental parameters. + :param on_reconnect_callback: Function that will be called just before a reconnection attempt. + + .. tip:: + **reconnect_delay** doubles automatically with each unsuccessful connect, from + **reconnect_delay** to **reconnect_delay_max**. + Set `reconnect_delay=0` to avoid automatic reconnection. Example:: @@ -52,26 +54,49 @@ async def run(): Please refer to :ref:`Pymodbus internals` for advanced usage. """ - def __init__( + def __init__( # pylint: disable=too-many-arguments self, host: str, - port: int = 802, - framer: FramerType = FramerType.TLS, sslctx: ssl.SSLContext = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT), - server_hostname: str | None = None, + framer: FramerType = FramerType.TLS, + port: int = 802, + name: str = "comm", + source_address: tuple[str, int] | None = None, + reconnect_delay: float = 0.1, + reconnect_delay_max: float = 300, + timeout: float = 3, + retries: int = 3, + retry_on_empty: bool = False, + broadcast_enable: bool = False, + no_resend_on_retry: bool = False, + on_connect_callback: Callable[[bool], None] | None = None, + + # ----- OLD ------ **kwargs: Any, ): """Initialize Asyncio Modbus TLS Client.""" + self.comm_params = CommParams( + comm_type=CommType.TLS, + host=host, + sslctx=sslctx, + port=port, + comm_name=name, + source_address=source_address, + reconnect_delay=reconnect_delay, + reconnect_delay_max=reconnect_delay_max, + timeout_connect=timeout, + ) AsyncModbusTcpClient.__init__( self, - host, - port=port, + "", framer=framer, - comm_type=CommType.TLS, - sslctx=sslctx, + retries=retries, + retry_on_empty=retry_on_empty, + broadcast_enable=broadcast_enable, + no_resend_on_retry=no_resend_on_retry, + on_connect_callback=on_connect_callback, **kwargs, ) - self.server_hostname = server_hostname @classmethod def generate_ssl( @@ -104,24 +129,23 @@ class ModbusTlsClient(ModbusTcpClient): Optional parameters: + :param sslctx: SSLContext to use for TLS + :param framer: Framer name, default FramerType.TLS :param port: Port used for communication + :param name: Set communication name, used in logging :param source_address: Source address of client - :param sslctx: SSLContext to use for TLS - :param server_hostname: Bind certificate to host - :param kwargs: Experimental parameters - - Common optional parameters: - - :param framer: Framer enum name - :param timeout: Timeout for a request, in seconds. + :param reconnect_delay: Minimum delay in seconds.milliseconds before reconnecting. + :param reconnect_delay_max: Maximum delay in seconds.milliseconds before reconnecting. + :param timeout: Timeout for a connection request, in seconds. :param retries: Max number of retries per request. :param retry_on_empty: Retry on empty response. :param broadcast_enable: True to treat id 0 as broadcast address. - :param reconnect_delay: Minimum delay in seconds.milliseconds before reconnecting. - :param reconnect_delay_max: Maximum delay in seconds.milliseconds before reconnecting. - :param on_reconnect_callback: Function that will be called just before a reconnection attempt. :param no_resend_on_retry: Do not resend request when retrying due to missing response. - :param kwargs: Experimental parameters. + + .. tip:: + **reconnect_delay** doubles automatically with each unsuccessful connect, from + **reconnect_delay** to **reconnect_delay_max**. + Set `reconnect_delay=0` to avoid automatic reconnection. Example:: @@ -139,22 +163,46 @@ async def run(): Remark: There are no automatic reconnect as with AsyncModbusTlsClient """ - def __init__( + def __init__( # pylint: disable=too-many-arguments self, host: str, - port: int = 802, - framer: FramerType = FramerType.TLS, sslctx: ssl.SSLContext = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT), - server_hostname: str | None = None, + framer: FramerType = FramerType.TLS, + port: int = 802, + name: str = "comm", + source_address: tuple[str, int] | None = None, + reconnect_delay: float = 0.1, + reconnect_delay_max: float = 300, + timeout: float = 3, + retries: int = 3, + retry_on_empty: bool = False, + broadcast_enable: bool = False, + no_resend_on_retry: bool = False, + + # ----- OLD ------ **kwargs: Any, ): """Initialize Modbus TLS Client.""" + self.comm_params = CommParams( + comm_type=CommType.TLS, + host=host, + sslctx=sslctx, + port=port, + comm_name=name, + source_address=source_address, + reconnect_delay=reconnect_delay, + reconnect_delay_max=reconnect_delay_max, + timeout_connect=timeout, + ) super().__init__( - host, comm_type=CommType.TLS, port=port, framer=framer, **kwargs + "", + framer=framer, + retries=retries, + retry_on_empty=retry_on_empty, + broadcast_enable=broadcast_enable, + no_resend_on_retry=no_resend_on_retry, + **kwargs ) - self.sslctx = sslctx - self.server_hostname = server_hostname - @classmethod def generate_ssl( @@ -188,11 +236,9 @@ def connect(self): return True try: sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - if self.params.source_address: - sock.bind(self.params.source_address) - self.socket = self.sslctx.wrap_socket( - sock, server_side=False, server_hostname=self.server_hostname - ) + if self.comm_params.source_address: + sock.bind(self.comm_params.source_address) + self.socket = self.comm_params.sslctx.wrap_socket(sock, server_side=False) # type: ignore[union-attr] self.socket.settimeout(self.comm_params.timeout_connect) self.socket.connect((self.comm_params.host, self.comm_params.port)) except OSError as msg: @@ -209,6 +255,6 @@ def __repr__(self): """Return string representation.""" return ( f"<{self.__class__.__name__} at {hex(id(self))} socket={self.socket}, " - f"ipaddr={self.comm_params.host}, port={self.comm_params.port}, sslctx={self.sslctx}, " + f"ipaddr={self.comm_params.host}, port={self.comm_params.port}, sslctx={self.comm_params.sslctx}, " f"timeout={self.comm_params.timeout_connect}>" ) diff --git a/pymodbus/client/udp.py b/pymodbus/client/udp.py index 4d71e240e..6038e3faf 100644 --- a/pymodbus/client/udp.py +++ b/pymodbus/client/udp.py @@ -2,13 +2,14 @@ from __future__ import annotations import socket +from collections.abc import Callable from typing import Any from pymodbus.client.base import ModbusBaseClient, ModbusBaseSyncClient from pymodbus.exceptions import ConnectionException from pymodbus.framer import FramerType from pymodbus.logging import Log -from pymodbus.transport import CommType +from pymodbus.transport import CommParams, CommType DGRAM_TYPE = socket.SOCK_DGRAM @@ -23,21 +24,23 @@ class AsyncModbusUdpClient(ModbusBaseClient): Optional parameters: + :param framer: Framer name, default FramerType.SOCKET :param port: Port used for communication. + :param name: Set communication name, used in logging :param source_address: source address of client, - - Common optional parameters: - - :param framer: Framer enum name - :param timeout: Timeout for a request, in seconds. + :param reconnect_delay: Minimum delay in seconds.milliseconds before reconnecting. + :param reconnect_delay_max: Maximum delay in seconds.milliseconds before reconnecting. + :param timeout: Timeout for a connection request, in seconds. :param retries: Max number of retries per request. :param retry_on_empty: Retry on empty response. :param broadcast_enable: True to treat id 0 as broadcast address. - :param reconnect_delay: Minimum delay in seconds.milliseconds before reconnecting. - :param reconnect_delay_max: Maximum delay in seconds.milliseconds before reconnecting. - :param on_reconnect_callback: Function that will be called just before a reconnection attempt. :param no_resend_on_retry: Do not resend request when retrying due to missing response. - :param kwargs: Experimental parameters. + :param on_reconnect_callback: Function that will be called just before a reconnection attempt. + + .. tip:: + **reconnect_delay** doubles automatically with each unsuccessful connect, from + **reconnect_delay** to **reconnect_delay_max**. + Set `reconnect_delay=0` to avoid automatic reconnection. Example:: @@ -53,21 +56,44 @@ async def run(): Please refer to :ref:`Pymodbus internals` for advanced usage. """ - def __init__( + def __init__( # pylint: disable=too-many-arguments self, host: str, - port: int = 502, framer: FramerType = FramerType.SOCKET, + port: int = 502, + name: str = "comm", source_address: tuple[str, int] | None = None, + reconnect_delay: float = 0.1, + reconnect_delay_max: float = 300, + timeout: float = 3, + retries: int = 3, + retry_on_empty: bool = False, + broadcast_enable: bool = False, + no_resend_on_retry: bool = False, + on_connect_callback: Callable[[bool], None] | None = None, + + # ----- OLD ------ **kwargs: Any, ) -> None: """Initialize Asyncio Modbus UDP Client.""" - ModbusBaseClient.__init__( - self, - framer, + self.comm_params = CommParams( comm_type=CommType.UDP, host=host, port=port, + comm_name=name, + source_address=source_address, + reconnect_delay=reconnect_delay, + reconnect_delay_max=reconnect_delay_max, + timeout_connect=timeout, + ) + ModbusBaseClient.__init__( + self, + framer, + retries=retries, + retry_on_empty=retry_on_empty, + broadcast_enable=broadcast_enable, + no_resend_on_retry=no_resend_on_retry, + on_connect_callback=on_connect_callback, **kwargs, ) self.source_address = source_address @@ -87,21 +113,22 @@ class ModbusUdpClient(ModbusBaseSyncClient): Optional parameters: + :param framer: Framer name, default FramerType.SOCKET :param port: Port used for communication. + :param name: Set communication name, used in logging :param source_address: source address of client, - - Common optional parameters: - - :param framer: Framer enum name - :param timeout: Timeout for a request, in seconds. + :param reconnect_delay: Minimum delay in seconds.milliseconds before reconnecting. + :param reconnect_delay_max: Maximum delay in seconds.milliseconds before reconnecting. + :param timeout: Timeout for a connection request, in seconds. :param retries: Max number of retries per request. :param retry_on_empty: Retry on empty response. :param broadcast_enable: True to treat id 0 as broadcast address. - :param reconnect_delay: Minimum delay in seconds.milliseconds before reconnecting. - :param reconnect_delay_max: Maximum delay in seconds.milliseconds before reconnecting. - :param on_reconnect_callback: Function that will be called just before a reconnection attempt. :param no_resend_on_retry: Do not resend request when retrying due to missing response. - :param kwargs: Experimental parameters. + + .. tip:: + **reconnect_delay** doubles automatically with each unsuccessful connect, from + **reconnect_delay** to **reconnect_delay_max**. + Set `reconnect_delay=0` to avoid automatic reconnection. Example:: @@ -121,24 +148,43 @@ async def run(): socket: socket.socket | None - def __init__( + def __init__( # pylint: disable=too-many-arguments self, host: str, - port: int = 502, framer: FramerType = FramerType.SOCKET, + port: int = 502, + name: str = "comm", source_address: tuple[str, int] | None = None, + reconnect_delay: float = 0.1, + reconnect_delay_max: float = 300, + timeout: float = 3, + retries: int = 3, + retry_on_empty: bool = False, + broadcast_enable: bool = False, + no_resend_on_retry: bool = False, + + # ----- OLD ------ **kwargs: Any, ) -> None: """Initialize Modbus UDP Client.""" + self.comm_params = CommParams( + comm_type=CommType.UDP, + host=host, + port=port, + comm_name=name, + source_address=source_address, + reconnect_delay=reconnect_delay, + reconnect_delay_max=reconnect_delay_max, + timeout_connect=timeout, + ) super().__init__( framer, - port=port, - host=host, - comm_type=CommType.UDP, + retries=retries, + retry_on_empty=retry_on_empty, + broadcast_enable=broadcast_enable, + no_resend_on_retry=no_resend_on_retry, **kwargs, ) - self.params.source_address = source_address - self.socket = None @property diff --git a/pymodbus/transaction.py b/pymodbus/transaction.py index fd3f6e329..b64c17932 100644 --- a/pymodbus/transaction.py +++ b/pymodbus/transaction.py @@ -12,7 +12,6 @@ ] import struct -import time from contextlib import suppress from threading import RLock from typing import TYPE_CHECKING @@ -134,19 +133,16 @@ class SyncModbusTransactionManager(ModbusTransactionManager): Results are keyed based on the supplied transaction id. """ - def __init__(self, client: ModbusBaseSyncClient, backoff, retry_on_empty, retry_on_invalid, retries): + def __init__(self, client: ModbusBaseSyncClient, retry_on_empty, retries): """Initialize an instance of the ModbusTransactionManager. :param client: The client socket wrapper - :param backoff: 0.3 :param retry_on_empty: Should the client retry on empty :param retries: The number of retries to allow """ super().__init__() self.client: ModbusBaseSyncClient = client - self.backoff = backoff self.retry_on_empty = retry_on_empty - self.retry_on_invalid = retry_on_invalid self.retries = retries self._transaction_lock = RLock() self._no_response_devices: list[int] = [] @@ -225,7 +221,7 @@ def execute(self, request: ModbusRequest): # noqa: C901 Log.debug("Clearing current Frame: - {}", _buffer) self.client.framer.resetFrame() if broadcast := ( - self.client.params.broadcast_enable and not request.slave_id + self.client.broadcast_enable and not request.slave_id ): self._transact(request, None, broadcast=True) response = b"Broadcast write sent - no response expected" @@ -286,15 +282,6 @@ def execute(self, request: ModbusRequest): # noqa: C901 else: # No response received and retries not enabled break - elif self.retry_on_invalid: - response, last_exception = self._retry_transaction( - retries, - "invalid", - request, - expected_response_length, - full=full, - ) - retries -= 1 else: break # full = False @@ -338,10 +325,6 @@ def _retry_transaction(self, retries, reason, packet, response_length, full=Fals Log.debug("Retry on {} response - {}", reason, retries) Log.debug('Changing transaction state from "WAITING_FOR_REPLY" to "RETRYING"') self.client.state = ModbusTransactionState.RETRYING - if self.backoff: - delay = 2 ** (self.retries - retries) * self.backoff - time.sleep(delay) - Log.debug("Sleeping {}", delay) self.client.connect() if hasattr(self.client, "_in_waiting"): if ( diff --git a/test/framers/test_old_framers.py b/test/framers/test_old_framers.py index 39bc5035b..5bb2c0ca1 100644 --- a/test/framers/test_old_framers.py +++ b/test/framers/test_old_framers.py @@ -14,7 +14,7 @@ ModbusTlsFramer, ) from pymodbus.pdu.bit_read_message import ReadCoilsRequest -from pymodbus.transport import CommType +from pymodbus.transport import CommParams, CommType from pymodbus.utilities import ModbusTransactionState @@ -327,9 +327,11 @@ async def test_send_packet(self, rtu_framer): message = TEST_MESSAGE client = ModbusBaseClient( FramerType.ASCII, - host="localhost", - port=BASE_PORT + 1, - CommType=CommType.TCP, + comm_params=CommParams( + comm_type=CommType.TCP, + host="localhost", + port=BASE_PORT + 1, + ), ) client.state = ModbusTransactionState.TRANSACTION_COMPLETE client.silent_interval = 1 diff --git a/test/framers/test_tbc_transaction.py b/test/framers/test_tbc_transaction.py index cd78d1e75..90f93c8d5 100755 --- a/test/framers/test_tbc_transaction.py +++ b/test/framers/test_tbc_transaction.py @@ -1,5 +1,4 @@ """Test transaction.""" -from itertools import count from unittest import mock from pymodbus.exceptions import ( @@ -42,7 +41,7 @@ def setup_method(self): self._tls = ModbusTlsFramer(decoder=self.decoder, client=None) self._rtu = ModbusRtuFramer(decoder=self.decoder, client=None) self._ascii = ModbusAsciiFramer(decoder=self.decoder, client=None) - self._manager = SyncModbusTransactionManager(self.client, 0.3, False, False, 3) + self._manager = SyncModbusTransactionManager(self.client, False, 3) # ----------------------------------------------------------------------- # # Modbus transaction manager @@ -89,11 +88,8 @@ def test_calculate_exception_length(self): == exception_length ) - @mock.patch("pymodbus.transaction.time") - def test_execute(self, mock_time): + def test_execute(self): """Test execute.""" - mock_time.time.side_effect = count() - client = mock.MagicMock() client.framer = self._ascii client.framer._buffer = b"deadbeef" # pylint: disable=protected-access @@ -113,7 +109,7 @@ def test_execute(self, mock_time): request.get_response_pdu_size.return_value = 10 request.slave_id = 1 request.function_code = 222 - trans = SyncModbusTransactionManager(client, 0.3, False, False, 3) + trans = SyncModbusTransactionManager(client, False, 3) trans._recv = mock.MagicMock( # pylint: disable=protected-access return_value=b"abcdef" ) @@ -148,12 +144,10 @@ def test_execute(self, mock_time): ) client.comm_params.handle_local_echo = True trans.retry_on_empty = False - trans.retry_on_invalid = False assert trans.execute(request).message == "[Input/Output] Wrong local echo" client.comm_params.handle_local_echo = False # retry on invalid response - trans.retry_on_invalid = True trans._recv = mock.MagicMock( # pylint: disable=protected-access side_effect=iter([b"", b"abcdef", b"deadbe", b"123456"]) ) diff --git a/test/sub_client/test_client.py b/test/sub_client/test_client.py index ec9405009..2356c5634 100755 --- a/test/sub_client/test_client.py +++ b/test/sub_client/test_client.py @@ -22,7 +22,7 @@ from pymodbus.datastore.store import ModbusSequentialDataBlock from pymodbus.exceptions import ConnectionException, ModbusException, ModbusIOException from pymodbus.pdu import ModbusRequest -from pymodbus.transport import CommType +from pymodbus.transport import CommParams, CommType BASE_PORT = 6500 @@ -123,7 +123,6 @@ def fake_execute(_self, request): "timeout": 3 + 2, "retries": 3 + 2, "retry_on_empty": True, - "strict": False, "broadcast_enable": not False, "reconnect_delay": 117, "reconnect_delay_max": 250, @@ -132,7 +131,6 @@ def fake_execute(_self, request): "timeout": 3, "retries": 3, "retry_on_empty": False, - "strict": True, "broadcast_enable": False, "reconnect_delay": 100, "reconnect_delay_max": 1000 * 60 * 5, @@ -149,7 +147,6 @@ def fake_execute(_self, request): "handle_local_echo": True, }, "defaults": { - "host": None, "port": "/dev/tty", "framer": FramerType.RTU, "baudrate": 19200, @@ -167,7 +164,6 @@ def fake_execute(_self, request): "source_address": ("195.6.7.8", 1025), }, "defaults": { - "host": "192.168.1.2", "port": 502, "framer": FramerType.SOCKET, "source_address": None, @@ -180,19 +176,12 @@ def fake_execute(_self, request): "framer": FramerType.ASCII, "source_address": ("195.6.7.8", 1025), "sslctx": None, - "certfile": None, - "keyfile": None, - "password": None, }, "defaults": { - "host": "192.168.1.2", "port": 802, "framer": FramerType.TLS, "source_address": None, "sslctx": None, - "certfile": None, - "keyfile": None, - "password": None, }, }, "udp": { @@ -203,7 +192,6 @@ def fake_execute(_self, request): "source_address": ("195.6.7.8", 1025), }, "defaults": { - "host": "192.168.1.2", "port": 502, "framer": FramerType.SOCKET, "source_address": None, @@ -236,15 +224,12 @@ async def test_client_instanciate( cur_args = arg_list[type_args] if test_default: client = clientclass(cur_args["pos_arg"]) - to_test = dict(arg_list["fix"]["defaults"], **cur_args["defaults"]) else: client = clientclass( cur_args["pos_arg"], **arg_list["fix"]["opt_args"], **cur_args["opt_args"], ) - to_test = dict(arg_list["fix"]["opt_args"], **cur_args["opt_args"]) - to_test["host"] = cur_args["defaults"]["host"] # Test information methods client.last_frame_end = 2 @@ -278,9 +263,11 @@ async def test_client_modbusbaseclient(): """Test modbus base client class.""" client = ModbusBaseClient( FramerType.ASCII, - host="localhost", - port=BASE_PORT + 1, - CommType=CommType.TCP, + comm_params=CommParams( + host="localhost", + port=BASE_PORT + 1, + comm_type=CommType.TCP, + ), ) client.register(pdu_bit_read.ReadCoilsResponse) assert str(client) @@ -313,9 +300,11 @@ async def test_client_base_async(): p_close.return_value.set_result(True) async with ModbusBaseClient( FramerType.ASCII, - host="localhost", - port=BASE_PORT + 2, - CommType=CommType.TCP, + comm_params=CommParams( + host="localhost", + port=BASE_PORT + 2, + comm_type=CommType.TCP, + ), ) as client: str(client) p_connect.return_value = asyncio.Future() @@ -327,7 +316,8 @@ async def test_client_base_async(): @pytest.mark.skip() async def test_client_protocol_receiver(): """Test the client protocol data received.""" - base = ModbusBaseClient(FramerType.SOCKET) + base = ModbusBaseClient(FramerType.SOCKET, comm_params=CommParams(), +) transport = mock.MagicMock() base.ctx.connection_made(transport) assert base.transport == transport @@ -349,7 +339,8 @@ async def test_client_protocol_receiver(): @pytest.mark.skip() async def test_client_protocol_response(): """Test the udp client protocol builds responses.""" - base = ModbusBaseClient(FramerType.SOCKET) + base = ModbusBaseClient(FramerType.SOCKET, comm_params=CommParams(), +) response = base.build_response(0x00) # pylint: disable=protected-access excp = response.exception() assert isinstance(excp, ConnectionException) @@ -363,7 +354,12 @@ async def test_client_protocol_response(): async def test_client_protocol_handler(): """Test the client protocol handles responses.""" base = ModbusBaseClient( - FramerType.ASCII, host="localhost", port=+3, CommType=CommType.TCP + FramerType.ASCII, + comm_params=CommParams( + host="localhost", + port=BASE_PORT + 3, + comm_type=CommType.TCP, + ), ) transport = mock.MagicMock() base.ctx.connection_made(transport=transport) @@ -411,7 +407,13 @@ def close(self): async def test_client_protocol_execute(): """Test the client protocol execute method.""" - base = ModbusBaseClient(FramerType.SOCKET, host="127.0.0.1") + base = ModbusBaseClient( + FramerType.SOCKET, + comm_params=CommParams( + host="127.0.0.1", + timeout_connect=3, + ), + ) request = pdu_bit_read.ReadCoilsRequest(1, 1) transport = MockTransport(base, request) base.ctx.connection_made(transport=transport) @@ -422,7 +424,12 @@ async def test_client_protocol_execute(): async def test_client_execute_broadcast(): """Test the client protocol execute method.""" - base = ModbusBaseClient(FramerType.SOCKET, host="127.0.0.1") + base = ModbusBaseClient( + FramerType.SOCKET, + comm_params=CommParams( + host="127.0.0.1", + ), + ) base.broadcast_enable = True request = pdu_bit_read.ReadCoilsRequest(1, 1) transport = MockTransport(base, request) @@ -432,7 +439,13 @@ async def test_client_execute_broadcast(): async def test_client_protocol_retry(): """Test the client protocol execute method with retries.""" - base = ModbusBaseClient(FramerType.SOCKET, host="127.0.0.1", timeout=0.1) + base = ModbusBaseClient( + FramerType.SOCKET, + comm_params=CommParams( + host="127.0.0.1", + timeout_connect=0.1, + ), + ) request = pdu_bit_read.ReadCoilsRequest(1, 1) transport = MockTransport(base, request, retries=2) base.ctx.connection_made(transport=transport) @@ -445,7 +458,14 @@ async def test_client_protocol_retry(): async def test_client_protocol_timeout(): """Test the client protocol execute method with timeout.""" - base = ModbusBaseClient(FramerType.SOCKET, host="127.0.0.1", timeout=0.1, retries=2) + base = ModbusBaseClient( + FramerType.SOCKET, + retries=2, + comm_params=CommParams( + host="127.0.0.1", + timeout_connect=0.1, + ), + ) # Avoid creating do_reconnect() task base.ctx.connection_lost = mock.MagicMock() request = pdu_bit_read.ReadCoilsRequest(1, 1) @@ -645,7 +665,8 @@ def test_client_mixin_convert_fail(): async def test_client_build_response(): """Test fail of build_response.""" - client = ModbusBaseClient(FramerType.RTU) + client = ModbusBaseClient(FramerType.RTU, comm_params=CommParams(), +) with pytest.raises(ConnectionException): await client.build_response(ModbusRequest(0, 0, 0, False)) diff --git a/test/sub_client/test_client_sync.py b/test/sub_client/test_client_sync.py index 08635de89..90f5ee862 100755 --- a/test/sub_client/test_client_sync.py +++ b/test/sub_client/test_client_sync.py @@ -218,7 +218,7 @@ def test_syn_tls_client_instantiation(self): client = ModbusTlsClient("127.0.0.1") assert client assert isinstance(client.framer, ModbusTlsFramer) - assert client.sslctx + assert client.comm_params.sslctx @mock.patch("pymodbus.client.tcp.select") def test_basic_syn_tls_client(self, mock_select): @@ -280,7 +280,7 @@ def test_tls_client_repr(self): client = ModbusTlsClient("127.0.0.1") rep = ( f"<{client.__class__.__name__} at {hex(id(client))} socket={client.socket}, " - f"ipaddr={client.comm_params.host}, port={client.comm_params.port}, sslctx={client.sslctx}, " + f"ipaddr={client.comm_params.host}, port={client.comm_params.port}, sslctx={client.comm_params.sslctx}, " f"timeout={client.comm_params.timeout_connect}>" ) assert repr(client) == rep @@ -347,7 +347,7 @@ def test_basic_sync_serial_client(self, mock_serial): client.close() # rtu connect/disconnect - rtu_client = ModbusSerialClient("/dev/null", framer=FramerType.RTU, strict=True) + rtu_client = ModbusSerialClient("/dev/null", framer=FramerType.RTU) assert rtu_client.connect() assert rtu_client.socket.inter_byte_timeout == rtu_client.inter_byte_timeout rtu_client.close() diff --git a/test/test_transaction.py b/test/test_transaction.py index 3d2311241..404e87fa6 100755 --- a/test/test_transaction.py +++ b/test/test_transaction.py @@ -1,5 +1,4 @@ """Test transaction.""" -from itertools import count from unittest import mock from pymodbus.exceptions import ( @@ -43,7 +42,7 @@ def setup_method(self): self._tls = ModbusTlsFramer(decoder=self.decoder, client=None) self._rtu = ModbusRtuFramer(decoder=self.decoder, client=None) self._ascii = ModbusAsciiFramer(decoder=self.decoder, client=None) - self._manager = SyncModbusTransactionManager(self.client, 0.3, False, False, 3) + self._manager = SyncModbusTransactionManager(self.client, False, 3) # ----------------------------------------------------------------------- # # Modbus transaction manager @@ -90,13 +89,10 @@ def test_calculate_exception_length(self): == exception_length ) - @mock.patch("pymodbus.transaction.time") @mock.patch.object(SyncModbusTransactionManager, "_recv") @mock.patch.object(ModbusTransactionManager, "getTransaction") - def test_execute(self, mock_get_transaction, mock_recv, mock_time): + def test_execute(self, mock_get_transaction, mock_recv): """Test execute.""" - mock_time.time.side_effect = count() - client = mock.MagicMock() client.framer = self._ascii client.framer._buffer = b"deadbeef" # pylint: disable=protected-access @@ -116,7 +112,7 @@ def test_execute(self, mock_get_transaction, mock_recv, mock_time): request.get_response_pdu_size.return_value = 10 request.slave_id = 1 request.function_code = 222 - trans = SyncModbusTransactionManager(client, 0.3, False, False, 3) + trans = SyncModbusTransactionManager(client, False, 3) mock_recv.reset_mock( return_value=b"abcdef" ) @@ -149,12 +145,10 @@ def test_execute(self, mock_get_transaction, mock_recv, mock_time): ) client.comm_params.handle_local_echo = True trans.retry_on_empty = False - trans.retry_on_invalid = False assert trans.execute(request).message == "[Input/Output] Wrong local echo" client.comm_params.handle_local_echo = False # retry on invalid response - trans.retry_on_invalid = True mock_recv.reset_mock( side_effect=iter([b"", b"abcdef", b"deadbe", b"123456"]) ) From 1b2cb7eb6651c2cb9ec0eb8323e645b0758fc26b Mon Sep 17 00:00:00 2001 From: jan iversen Date: Mon, 22 Jul 2024 22:28:44 +0200 Subject: [PATCH 80/88] cleanup kwargs sideeffects. (#2257) --- examples/client_performance.py | 4 +- examples/simple_async_client.py | 13 ------ examples/simple_sync_client.py | 13 ------ pymodbus/client/base.py | 18 ++++----- pymodbus/client/serial.py | 28 +++++-------- pymodbus/client/tcp.py | 27 +++++-------- pymodbus/client/tls.py | 9 ----- pymodbus/client/udp.py | 9 ----- test/framers/test_old_framers.py | 5 +++ test/sub_client/test_client.py | 69 ++++++++++++++++++++++++++++---- 10 files changed, 97 insertions(+), 98 deletions(-) diff --git a/examples/client_performance.py b/examples/client_performance.py index 4cc9e014d..43a6f9a4e 100755 --- a/examples/client_performance.py +++ b/examples/client_performance.py @@ -29,7 +29,7 @@ def run_sync_client_test(): print("--- Testing sync client v3.4.1") client = ModbusSerialClient( "/dev/ttys007", - framer_name=FramerType.RTU, + framer=FramerType.RTU, baudrate=9600, ) client.connect() @@ -56,7 +56,7 @@ async def run_async_client_test(): print("--- Testing async client v3.4.1") client = AsyncModbusSerialClient( "/dev/ttys007", - framer_name=FramerType.RTU, + framer=FramerType.RTU, baudrate=9600, ) await client.connect() diff --git a/examples/simple_async_client.py b/examples/simple_async_client.py index 2b08686a0..24c95beaa 100755 --- a/examples/simple_async_client.py +++ b/examples/simple_async_client.py @@ -59,19 +59,6 @@ async def run_async_simple_client(comm, host, port, framer=FramerType.SOCKET): stopbits=1, # handle_local_echo=False, ) - elif comm == "tls": - client = ModbusClient.AsyncModbusTlsClient( - host, - port=port, - framer=FramerType.TLS, - # timeout=10, - # retries=3, - # retry_on_empty=False, - # sslctx=sslctx, - certfile="../examples/certificates/pymodbus.crt", - keyfile="../examples/certificates/pymodbus.key", - # password="none", - ) else: print(f"Unknown client {comm} selected") return diff --git a/examples/simple_sync_client.py b/examples/simple_sync_client.py index d2dfd3a57..d2519d864 100755 --- a/examples/simple_sync_client.py +++ b/examples/simple_sync_client.py @@ -61,19 +61,6 @@ def run_sync_simple_client(comm, host, port, framer=FramerType.SOCKET): stopbits=1, # handle_local_echo=False, ) - elif comm == "tls": - client = ModbusClient.ModbusTlsClient( - host, - port=port, - framer=FramerType.TLS, - # timeout=10, - # retries=3, - # retry_on_empty=False, - # sslctx=None, - certfile="../examples/certificates/pymodbus.crt", - keyfile="../examples/certificates/pymodbus.key", - # password=None, - ) else: print(f"Unknown client {comm} selected") return diff --git a/pymodbus/client/base.py b/pymodbus/client/base.py index 91412901b..0e8d5e9fa 100644 --- a/pymodbus/client/base.py +++ b/pymodbus/client/base.py @@ -28,12 +28,12 @@ class ModbusBaseClient(ModbusClientMixin[Awaitable[ModbusResponse]]): def __init__( self, framer: FramerType, - retries: int = 3, + retries: int, + retry_on_empty: bool, + broadcast_enable: bool, + no_resend_on_retry: bool, + on_connect_callback: Callable[[bool], None] | None, comm_params: CommParams | None = None, - retry_on_empty: bool = False, - broadcast_enable: bool = False, - no_resend_on_retry: bool = False, - on_connect_callback: Callable[[bool], None] | None = None, ) -> None: """Initialize a client instance.""" ModbusClientMixin.__init__(self) # type: ignore[arg-type] @@ -183,11 +183,11 @@ class ModbusBaseSyncClient(ModbusClientMixin[ModbusResponse]): def __init__( self, framer: FramerType, - retries: int = 3, + retries: int, + retry_on_empty: bool, + broadcast_enable: bool, + no_resend_on_retry: bool, comm_params: CommParams | None = None, - retry_on_empty: bool = False, - broadcast_enable: bool = False, - no_resend_on_retry: bool = False, ) -> None: """Initialize a client instance.""" ModbusClientMixin.__init__(self) # type: ignore[arg-type] diff --git a/pymodbus/client/serial.py b/pymodbus/client/serial.py index 089d3a017..a53fa305c 100644 --- a/pymodbus/client/serial.py +++ b/pymodbus/client/serial.py @@ -4,7 +4,7 @@ import time from collections.abc import Callable from functools import partial -from typing import TYPE_CHECKING, Any +from typing import TYPE_CHECKING from pymodbus.client.base import ModbusBaseClient, ModbusBaseSyncClient from pymodbus.exceptions import ConnectionException @@ -86,9 +86,6 @@ def __init__( # pylint: disable=too-many-arguments broadcast_enable: bool = False, no_resend_on_retry: bool = False, on_connect_callback: Callable[[bool], None] | None = None, - - # ----- OLD ------ - **kwargs: Any, ) -> None: """Initialize Asyncio Modbus Serial Client.""" if PYSERIAL_MISSING: @@ -112,12 +109,11 @@ def __init__( # pylint: disable=too-many-arguments ModbusBaseClient.__init__( self, framer, - retries=retries, - retry_on_empty=retry_on_empty, - broadcast_enable=broadcast_enable, - no_resend_on_retry=no_resend_on_retry, - on_connect_callback=on_connect_callback, - **kwargs, + retries, + retry_on_empty, + broadcast_enable, + no_resend_on_retry, + on_connect_callback, ) def close(self, reconnect: bool = False) -> None: @@ -191,9 +187,6 @@ def __init__( # pylint: disable=too-many-arguments retry_on_empty: bool = False, broadcast_enable: bool = False, no_resend_on_retry: bool = False, - - # ----- OLD ------ - **kwargs: Any, ) -> None: """Initialize Modbus Serial Client.""" self.comm_params = CommParams( @@ -211,11 +204,10 @@ def __init__( # pylint: disable=too-many-arguments ) super().__init__( framer, - retries=retries, - retry_on_empty=retry_on_empty, - broadcast_enable=broadcast_enable, - no_resend_on_retry=no_resend_on_retry, - **kwargs, + retries, + retry_on_empty, + broadcast_enable, + no_resend_on_retry, ) self.socket: serial.Serial | None = None self.last_frame_end = None diff --git a/pymodbus/client/tcp.py b/pymodbus/client/tcp.py index b1ed98572..8fcba4396 100644 --- a/pymodbus/client/tcp.py +++ b/pymodbus/client/tcp.py @@ -5,7 +5,6 @@ import socket import time from collections.abc import Callable -from typing import Any from pymodbus.client.base import ModbusBaseClient, ModbusBaseSyncClient from pymodbus.exceptions import ConnectionException @@ -72,9 +71,6 @@ def __init__( # pylint: disable=too-many-arguments broadcast_enable: bool = False, no_resend_on_retry: bool = False, on_connect_callback: Callable[[bool], None] | None = None, - - # ----- OLD ------ - **kwargs: Any, ) -> None: """Initialize Asyncio Modbus TCP Client.""" if not hasattr(self,"comm_params"): @@ -91,12 +87,11 @@ def __init__( # pylint: disable=too-many-arguments ModbusBaseClient.__init__( self, framer, - retries=retries, - retry_on_empty=retry_on_empty, - broadcast_enable=broadcast_enable, - no_resend_on_retry=no_resend_on_retry, - on_connect_callback=on_connect_callback, - **kwargs, + retries, + retry_on_empty, + broadcast_enable, + no_resend_on_retry, + on_connect_callback, ) def close(self, reconnect: bool = False) -> None: @@ -162,9 +157,6 @@ def __init__( # pylint: disable=too-many-arguments retry_on_empty: bool = False, broadcast_enable: bool = False, no_resend_on_retry: bool = False, - - # ----- OLD ------ - **kwargs: Any, ) -> None: """Initialize Modbus TCP Client.""" if not hasattr(self,"comm_params"): @@ -180,11 +172,10 @@ def __init__( # pylint: disable=too-many-arguments ) super().__init__( framer, - retries=retries, - retry_on_empty=retry_on_empty, - broadcast_enable=broadcast_enable, - no_resend_on_retry=no_resend_on_retry, - **kwargs, + retries, + retry_on_empty, + broadcast_enable, + no_resend_on_retry, ) self.socket = None diff --git a/pymodbus/client/tls.py b/pymodbus/client/tls.py index 22eb1899b..4fac9fae7 100644 --- a/pymodbus/client/tls.py +++ b/pymodbus/client/tls.py @@ -4,7 +4,6 @@ import socket import ssl from collections.abc import Callable -from typing import Any from pymodbus.client.tcp import AsyncModbusTcpClient, ModbusTcpClient from pymodbus.framer import FramerType @@ -70,9 +69,6 @@ def __init__( # pylint: disable=too-many-arguments broadcast_enable: bool = False, no_resend_on_retry: bool = False, on_connect_callback: Callable[[bool], None] | None = None, - - # ----- OLD ------ - **kwargs: Any, ): """Initialize Asyncio Modbus TLS Client.""" self.comm_params = CommParams( @@ -95,7 +91,6 @@ def __init__( # pylint: disable=too-many-arguments broadcast_enable=broadcast_enable, no_resend_on_retry=no_resend_on_retry, on_connect_callback=on_connect_callback, - **kwargs, ) @classmethod @@ -178,9 +173,6 @@ def __init__( # pylint: disable=too-many-arguments retry_on_empty: bool = False, broadcast_enable: bool = False, no_resend_on_retry: bool = False, - - # ----- OLD ------ - **kwargs: Any, ): """Initialize Modbus TLS Client.""" self.comm_params = CommParams( @@ -201,7 +193,6 @@ def __init__( # pylint: disable=too-many-arguments retry_on_empty=retry_on_empty, broadcast_enable=broadcast_enable, no_resend_on_retry=no_resend_on_retry, - **kwargs ) @classmethod diff --git a/pymodbus/client/udp.py b/pymodbus/client/udp.py index 6038e3faf..3b19708c0 100644 --- a/pymodbus/client/udp.py +++ b/pymodbus/client/udp.py @@ -3,7 +3,6 @@ import socket from collections.abc import Callable -from typing import Any from pymodbus.client.base import ModbusBaseClient, ModbusBaseSyncClient from pymodbus.exceptions import ConnectionException @@ -71,9 +70,6 @@ def __init__( # pylint: disable=too-many-arguments broadcast_enable: bool = False, no_resend_on_retry: bool = False, on_connect_callback: Callable[[bool], None] | None = None, - - # ----- OLD ------ - **kwargs: Any, ) -> None: """Initialize Asyncio Modbus UDP Client.""" self.comm_params = CommParams( @@ -94,7 +90,6 @@ def __init__( # pylint: disable=too-many-arguments broadcast_enable=broadcast_enable, no_resend_on_retry=no_resend_on_retry, on_connect_callback=on_connect_callback, - **kwargs, ) self.source_address = source_address @@ -162,9 +157,6 @@ def __init__( # pylint: disable=too-many-arguments retry_on_empty: bool = False, broadcast_enable: bool = False, no_resend_on_retry: bool = False, - - # ----- OLD ------ - **kwargs: Any, ) -> None: """Initialize Modbus UDP Client.""" self.comm_params = CommParams( @@ -183,7 +175,6 @@ def __init__( # pylint: disable=too-many-arguments retry_on_empty=retry_on_empty, broadcast_enable=broadcast_enable, no_resend_on_retry=no_resend_on_retry, - **kwargs, ) self.socket = None diff --git a/test/framers/test_old_framers.py b/test/framers/test_old_framers.py index 5bb2c0ca1..165f7fc6b 100644 --- a/test/framers/test_old_framers.py +++ b/test/framers/test_old_framers.py @@ -327,6 +327,11 @@ async def test_send_packet(self, rtu_framer): message = TEST_MESSAGE client = ModbusBaseClient( FramerType.ASCII, + 3, + False, + False, + False, + None, comm_params=CommParams( comm_type=CommType.TCP, host="localhost", diff --git a/test/sub_client/test_client.py b/test/sub_client/test_client.py index 2356c5634..48a042964 100755 --- a/test/sub_client/test_client.py +++ b/test/sub_client/test_client.py @@ -263,6 +263,11 @@ async def test_client_modbusbaseclient(): """Test modbus base client class.""" client = ModbusBaseClient( FramerType.ASCII, + 3, + False, + False, + False, + None, comm_params=CommParams( host="localhost", port=BASE_PORT + 1, @@ -300,6 +305,11 @@ async def test_client_base_async(): p_close.return_value.set_result(True) async with ModbusBaseClient( FramerType.ASCII, + 3, + False, + False, + False, + None, comm_params=CommParams( host="localhost", port=BASE_PORT + 2, @@ -316,8 +326,15 @@ async def test_client_base_async(): @pytest.mark.skip() async def test_client_protocol_receiver(): """Test the client protocol data received.""" - base = ModbusBaseClient(FramerType.SOCKET, comm_params=CommParams(), -) + base = ModbusBaseClient( + FramerType.SOCKET, + 3, + False, + False, + False, + None, + comm_params=CommParams(), + ) transport = mock.MagicMock() base.ctx.connection_made(transport) assert base.transport == transport @@ -339,8 +356,15 @@ async def test_client_protocol_receiver(): @pytest.mark.skip() async def test_client_protocol_response(): """Test the udp client protocol builds responses.""" - base = ModbusBaseClient(FramerType.SOCKET, comm_params=CommParams(), -) + base = ModbusBaseClient( + FramerType.SOCKET, + 3, + False, + False, + False, + None, + comm_params=CommParams(), + ) response = base.build_response(0x00) # pylint: disable=protected-access excp = response.exception() assert isinstance(excp, ConnectionException) @@ -355,6 +379,11 @@ async def test_client_protocol_handler(): """Test the client protocol handles responses.""" base = ModbusBaseClient( FramerType.ASCII, + 3, + False, + False, + False, + None, comm_params=CommParams( host="localhost", port=BASE_PORT + 3, @@ -409,6 +438,11 @@ async def test_client_protocol_execute(): """Test the client protocol execute method.""" base = ModbusBaseClient( FramerType.SOCKET, + 3, + False, + False, + False, + None, comm_params=CommParams( host="127.0.0.1", timeout_connect=3, @@ -426,6 +460,11 @@ async def test_client_execute_broadcast(): """Test the client protocol execute method.""" base = ModbusBaseClient( FramerType.SOCKET, + 3, + False, + False, + False, + None, comm_params=CommParams( host="127.0.0.1", ), @@ -441,6 +480,11 @@ async def test_client_protocol_retry(): """Test the client protocol execute method with retries.""" base = ModbusBaseClient( FramerType.SOCKET, + 3, + False, + False, + False, + None, comm_params=CommParams( host="127.0.0.1", timeout_connect=0.1, @@ -460,7 +504,11 @@ async def test_client_protocol_timeout(): """Test the client protocol execute method with timeout.""" base = ModbusBaseClient( FramerType.SOCKET, - retries=2, + 2, + False, + False, + False, + None, comm_params=CommParams( host="127.0.0.1", timeout_connect=0.1, @@ -665,8 +713,15 @@ def test_client_mixin_convert_fail(): async def test_client_build_response(): """Test fail of build_response.""" - client = ModbusBaseClient(FramerType.RTU, comm_params=CommParams(), -) + client = ModbusBaseClient( + FramerType.RTU, + 3, + False, + False, + False, + None, + comm_params=CommParams(), + ) with pytest.raises(ConnectionException): await client.build_response(ModbusRequest(0, 0, 0, False)) From d32947517087214d28d71cdcbf3c02355526ab68 Mon Sep 17 00:00:00 2001 From: Esco441-91 <56118718+Esco441-91@users.noreply.github.com> Date: Tue, 23 Jul 2024 23:44:30 +0300 Subject: [PATCH 81/88] Feature/simulator addressing (#2258) --- doc/source/library/simulator/config.rst | 3 +++ pymodbus/server/simulator/http_server.py | 9 ++++++++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/doc/source/library/simulator/config.rst b/doc/source/library/simulator/config.rst index 99766f2e0..9349a313c 100644 --- a/doc/source/library/simulator/config.rst +++ b/doc/source/library/simulator/config.rst @@ -68,6 +68,9 @@ The entry “framer” allows the following values: - “tls” to use :class:`pymodbus.framer.ModbusTlsFramer`, - “socket” to use :class:`pymodbus.framer.ModbusSocketFramer`. +Optional entry "device_id" will limit server to only accept a single id. If +not set, the server will accept all device id. + .. warning:: not all "framer" types can be used with all “comm” types. diff --git a/pymodbus/server/simulator/http_server.py b/pymodbus/server/simulator/http_server.py index 4766b73a9..673ba70a6 100644 --- a/pymodbus/server/simulator/http_server.py +++ b/pymodbus/server/simulator/http_server.py @@ -154,7 +154,14 @@ def __init__( self.datastore_context = ModbusSimulatorContext( device, custom_actions_dict or {} ) - datastore = ModbusServerContext(slaves=self.datastore_context, single=True) + datastore = None + if "device_id" in server: + # Designated ModBus unit address. Will only serve data if the address matches + datastore = ModbusServerContext(slaves={int(server["device_id"]): self.datastore_context}, single=True) + else: + # Will server any request regardless of addressing + datastore = ModbusServerContext(slaves=self.datastore_context, single=True) + comm = comm_class[server.pop("comm")] framer = server.pop("framer") if "identity" in server: From 371e168a0193d5eb98e41e4c92c862b94c1ead0d Mon Sep 17 00:00:00 2001 From: Alex <52292902+alexrudd2@users.noreply.github.com> Date: Wed, 24 Jul 2024 01:27:56 -0500 Subject: [PATCH 82/88] Cleanup ReadWriteMultipleRegistersResponse and testing (#2261) --- pymodbus/pdu/register_read_message.py | 37 +------------------ .../test_register_read_messages.py | 25 +++---------- 2 files changed, 7 insertions(+), 55 deletions(-) diff --git a/pymodbus/pdu/register_read_message.py b/pymodbus/pdu/register_read_message.py index 8fd335b32..9bcd113da 100644 --- a/pymodbus/pdu/register_read_message.py +++ b/pymodbus/pdu/register_read_message.py @@ -351,7 +351,7 @@ def __str__(self): ) -class ReadWriteMultipleRegistersResponse(ModbusResponse): +class ReadWriteMultipleRegistersResponse(ReadHoldingRegistersResponse): """Read/write multiple registers. The normal response contains the data from the group of registers that @@ -362,38 +362,3 @@ class ReadWriteMultipleRegistersResponse(ModbusResponse): """ function_code = 23 - _rtu_byte_count_pos = 2 - - def __init__(self, values=None, slave=0, transaction=0, protocol=0, skip_encode=False): - """Initialize a new instance. - - :param values: The register values to write - """ - super().__init__(slave, transaction, protocol, skip_encode) - self.registers = values or [] - - def encode(self): - """Encode the response packet. - - :returns: The encoded packet - """ - result = struct.pack(">B", len(self.registers) * 2) - for register in self.registers: - result += struct.pack(">H", register) - return result - - def decode(self, data): - """Decode the register response packet. - - :param data: The response to decode - """ - bytecount = int(data[0]) - for i in range(1, bytecount, 2): - self.registers.append(struct.unpack(">H", data[i : i + 2])[0]) - - def __str__(self): - """Return a string representation of the instance. - - :returns: A string representation of the instance - """ - return f"ReadWriteNRegisterResponse ({len(self.registers)})" diff --git a/test/sub_function_codes/test_register_read_messages.py b/test/sub_function_codes/test_register_read_messages.py index 9b8bafef8..ce5504e65 100644 --- a/test/sub_function_codes/test_register_read_messages.py +++ b/test/sub_function_codes/test_register_read_messages.py @@ -30,10 +30,9 @@ class TestReadRegisterMessages: * Read Holding Registers """ - value = None - values = None - request_read = None - response_read = None + values: list + request_read: dict + response_read: dict def setup_method(self): """Initialize the test environment and builds request/result encoding pairs.""" @@ -42,7 +41,6 @@ def setup_method(self): "read_count": 5, "write_address": 1, } - self.value = 0xABCD self.values = [0xA, 0xB, 0xC] self.request_read = { ReadRegistersRequestBase(1, 5): b"\x00\x01\x00\x05", @@ -84,20 +82,9 @@ def test_register_read_responses(self): def test_register_read_response_decode(self): """Test register read response.""" - registers = [ - [0x0A, 0x0B, 0x0C], - [0x0A, 0x0B, 0x0C], - [0x0A, 0x0B, 0x0C], - [0x0A, 0x0B, 0x0C, 0x0A, 0x0B, 0x0C], - ] - values = sorted( - self.response_read.items(), - key=lambda x: str(x), # pylint: disable=unnecessary-lambda - ) - for packet, register in zip(values, registers): - request, response = packet - request.decode(response) - assert request.registers == register + for response, packet in self.response_read.items(): + response.decode(packet) + assert response.registers == self.values async def test_register_read_requests_count_errors(self): """This tests that the register request messages. From 4dbefa26cccd1b1e4899954cd45022a2cdfd8dd0 Mon Sep 17 00:00:00 2001 From: Alex <52292902+alexrudd2@users.noreply.github.com> Date: Wed, 24 Jul 2024 01:31:30 -0500 Subject: [PATCH 83/88] More descriptive decoder exceptions (#2260) --- pymodbus/factory.py | 2 -- pymodbus/pdu/register_read_message.py | 3 +++ test/test_factory.py | 19 ++++++------------- 3 files changed, 9 insertions(+), 15 deletions(-) diff --git a/pymodbus/factory.py b/pymodbus/factory.py index 2d69e403e..1e4b04a06 100644 --- a/pymodbus/factory.py +++ b/pymodbus/factory.py @@ -236,8 +236,6 @@ def decode(self, message): return self._helper(message) except ModbusException as exc: Log.error("Unable to decode response {}", exc) - except Exception as exc: # pylint: disable=broad-except - Log.error("General exception: {}", exc) return None def _helper(self, data: str): diff --git a/pymodbus/pdu/register_read_message.py b/pymodbus/pdu/register_read_message.py index 9bcd113da..a4280379a 100644 --- a/pymodbus/pdu/register_read_message.py +++ b/pymodbus/pdu/register_read_message.py @@ -4,6 +4,7 @@ # pylint: disable=missing-type-doc import struct +from pymodbus.exceptions import ModbusIOException from pymodbus.pdu import ModbusExceptions as merror from pymodbus.pdu import ModbusRequest, ModbusResponse @@ -88,6 +89,8 @@ def decode(self, data): :param data: The request to decode """ byte_count = int(data[0]) + if byte_count < 2 or byte_count > 246 or byte_count % 2 == 1 or byte_count != len(data) - 1: + raise ModbusIOException(f"Invalid response {data} has byte count of {byte_count}") self.registers = [] for i in range(1, byte_count + 1, 2): self.registers.append(struct.unpack(">H", data[i : i + 2])[0]) diff --git a/test/test_factory.py b/test/test_factory.py index da27dd679..52d415794 100644 --- a/test/test_factory.py +++ b/test/test_factory.py @@ -6,16 +6,11 @@ from pymodbus.pdu import ModbusRequest, ModbusResponse -def _raise_exception(_): - """Raise exception.""" - raise ModbusException("something") - - class TestFactory: """Unittest for the pymod.exceptions module.""" - client = None - server = None + client: ClientDecoder + server: ServerDecoder request = ( (0x01, b"\x01\x00\x01\x00\x01"), # read coils (0x02, b"\x02\x00\x01\x00\x01"), # read discrete inputs @@ -139,15 +134,13 @@ def test_requests_working(self): def test_client_factory_fails(self): """Tests that a client factory will fail to decode a bad message.""" - self.client._helper = _raise_exception # pylint: disable=protected-access - actual = self.client.decode(None) - assert not actual + with pytest.raises(TypeError): + self.client.decode(None) def test_server_factory_fails(self): """Tests that a server factory will fail to decode a bad message.""" - self.server._helper = _raise_exception # pylint: disable=protected-access - actual = self.server.decode(None) - assert not actual + with pytest.raises(TypeError): + self.server.decode(None) def test_server_register_custom_request(self): """Test server register custom request.""" From 109ea695670ed969a6457e2b43a8a1d627b077ac Mon Sep 17 00:00:00 2001 From: Alex <52292902+alexrudd2@users.noreply.github.com> Date: Wed, 24 Jul 2024 01:32:59 -0500 Subject: [PATCH 84/88] buildPacket can be used for Request and Response (#2262) --- pymodbus/framer/old_framer_base.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pymodbus/framer/old_framer_base.py b/pymodbus/framer/old_framer_base.py index 4982d27a9..b737a1b47 100644 --- a/pymodbus/framer/old_framer_base.py +++ b/pymodbus/framer/old_framer_base.py @@ -8,7 +8,7 @@ from pymodbus.factory import ClientDecoder, ServerDecoder from pymodbus.framer.base import FramerBase from pymodbus.logging import Log -from pymodbus.pdu import ModbusRequest +from pymodbus.pdu import ModbusRequest, ModbusResponse if TYPE_CHECKING: @@ -158,7 +158,7 @@ def frameProcessIncomingPacket( ) -> None: """Process new packet pattern.""" - def buildPacket(self, message: ModbusRequest) -> bytes: + def buildPacket(self, message: ModbusRequest | ModbusResponse) -> bytes: """Create a ready to send modbus packet. :param message: The populated request/response to send From 30e4aa759024ee3faec1bc54ff2e689be9c86448 Mon Sep 17 00:00:00 2001 From: efdx <150683046+efdx@users.noreply.github.com> Date: Thu, 25 Jul 2024 15:02:03 +0300 Subject: [PATCH 85/88] simulator: Fix context single parameter (#2264) --- pymodbus/server/simulator/http_server.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pymodbus/server/simulator/http_server.py b/pymodbus/server/simulator/http_server.py index 673ba70a6..39cc843c2 100644 --- a/pymodbus/server/simulator/http_server.py +++ b/pymodbus/server/simulator/http_server.py @@ -157,7 +157,7 @@ def __init__( datastore = None if "device_id" in server: # Designated ModBus unit address. Will only serve data if the address matches - datastore = ModbusServerContext(slaves={int(server["device_id"]): self.datastore_context}, single=True) + datastore = ModbusServerContext(slaves={int(server["device_id"]): self.datastore_context}, single=False) else: # Will server any request regardless of addressing datastore = ModbusServerContext(slaves=self.datastore_context, single=True) From d1fe94a162292efde9c1ae6ebf823e7d3145fd7f Mon Sep 17 00:00:00 2001 From: jan iversen Date: Mon, 29 Jul 2024 19:13:01 +0200 Subject: [PATCH 86/88] Remove unneeded parameters. (#2272) * remove no_resend_on_retry. * Default slave = 1. * remove retry_on_empty. * remove broadcast_enable. --- API_changes.rst | 2 + doc/source/client.rst | 3 +- examples/client_async.py | 5 -- examples/client_custom_msg.py | 6 +- examples/client_sync.py | 4 -- examples/contrib/solar.py | 1 - examples/package_test_tool.py | 2 +- examples/simple_async_client.py | 3 - examples/simple_sync_client.py | 3 - pymodbus/client/base.py | 20 +----- pymodbus/client/mixin.py | 70 ++++++++++---------- pymodbus/client/modbusclientprotocol.py | 10 +-- pymodbus/client/serial.py | 18 ----- pymodbus/client/tcp.py | 25 +------ pymodbus/client/tls.py | 18 ----- pymodbus/client/udp.py | 29 ++------ pymodbus/pdu/bit_read_message.py | 8 +-- pymodbus/pdu/bit_write_message.py | 4 +- pymodbus/pdu/diag_message.py | 20 +++--- pymodbus/pdu/file_message.py | 12 ++-- pymodbus/pdu/mei_message.py | 4 +- pymodbus/pdu/other_message.py | 14 ++-- pymodbus/pdu/pdu.py | 2 +- pymodbus/pdu/register_read_message.py | 10 +-- pymodbus/pdu/register_write_message.py | 8 +-- pymodbus/transaction.py | 32 ++------- test/framers/test_old_framers.py | 19 +++--- test/framers/test_tbc_transaction.py | 9 +-- test/sub_client/test_client.py | 38 +---------- test/sub_function_codes/test_all_messages.py | 4 +- test/test_transaction.py | 9 +-- 31 files changed, 115 insertions(+), 297 deletions(-) diff --git a/API_changes.rst b/API_changes.rst index 82e913b9b..50ecc7867 100644 --- a/API_changes.rst +++ b/API_changes.rst @@ -5,6 +5,8 @@ Versions (X.Y.Z) where Z > 0 e.g. 3.0.1 do NOT have API changes! API changes 3.7.0 ----------------- +- default slave changed to 1 from 0 (which is broadcast). +- broadcast_enable, retry_on_empty, no_resend_on_retry parameters removed. - class method generate_ssl() added to TLS client (sync/async). - removed certfile, keyfile, password from TLS client, please use generate_ssl() - on_reconnect_callback() removed from clients (sync/async). diff --git a/doc/source/client.rst b/doc/source/client.rst index 9b3a6f9c4..cec456a5b 100644 --- a/doc/source/client.rst +++ b/doc/source/client.rst @@ -177,7 +177,8 @@ The physical devices are addressed with the :mod:`slave=` parameter. :mod:`slave=0` is used as broadcast in order to address all devices. However experience shows that modern devices do not allow broadcast, mostly because it is -inheriently dangerous. With :mod:`slave=0` the application can get upto 254 responses on a single request! +inheriently dangerous. With :mod:`slave=0` the application can get upto 254 responses on a single request, +and this is not handled with the normal API calls! The simple request calls (mixin) do NOT support broadcast, if an application wants to use broadcast it must call :mod:`client.execute` and deal with the responses. diff --git a/examples/client_async.py b/examples/client_async.py index 16ed44c8d..281b7792d 100755 --- a/examples/client_async.py +++ b/examples/client_async.py @@ -64,8 +64,6 @@ def setup_async_client(description=None, cmdline=None): retries=3, reconnect_delay=1, reconnect_delay_max=10, - # retry_on_empty=False, - # TCP setup parameters # source_address=("localhost", 0), ) elif args.comm == "udp": @@ -76,7 +74,6 @@ def setup_async_client(description=None, cmdline=None): framer=args.framer, timeout=args.timeout, # retries=3, - # retry_on_empty=False, # UDP setup parameters # source_address=None, ) @@ -87,7 +84,6 @@ def setup_async_client(description=None, cmdline=None): # framer=ModbusRtuFramer, timeout=args.timeout, # retries=3, - # retry_on_empty=False, # Serial setup parameters baudrate=args.baudrate, # bytesize=8, @@ -103,7 +99,6 @@ def setup_async_client(description=None, cmdline=None): framer=args.framer, timeout=args.timeout, # retries=3, - # retry_on_empty=False, # TLS setup parameters sslctx=modbusClient.AsyncModbusTlsClient.generate_ssl( certfile=helper.get_certificate("crt"), diff --git a/examples/client_custom_msg.py b/examples/client_custom_msg.py index b125d02e6..bc44658df 100755 --- a/examples/client_custom_msg.py +++ b/examples/client_custom_msg.py @@ -36,7 +36,7 @@ class CustomModbusResponse(ModbusResponse): function_code = 55 _rtu_byte_count_pos = 2 - def __init__(self, values=None, slave=0, transaction=0, protocol=0, skip_encode=False): + def __init__(self, values=None, slave=1, transaction=0, protocol=0, skip_encode=False): """Initialize.""" ModbusResponse.__init__(self, slave, transaction, protocol, skip_encode) self.values = values or [] @@ -68,7 +68,7 @@ class CustomModbusRequest(ModbusRequest): function_code = 55 _rtu_frame_size = 8 - def __init__(self, address=None, slave=0, transaction=0, protocol=0, skip_encode=False): + def __init__(self, address=None, slave=1, transaction=0, protocol=0, skip_encode=False): """Initialize.""" ModbusRequest.__init__(self, slave, transaction, protocol, skip_encode) self.address = address @@ -100,7 +100,7 @@ def execute(self, context): class Read16CoilsRequest(ReadCoilsRequest): """Read 16 coils in one request.""" - def __init__(self, address, count=None, slave=0, transaction=0, protocol=0, skip_encode=False): + def __init__(self, address, count=None, slave=1, transaction=0, protocol=0, skip_encode=False): """Initialize a new instance. :param address: The address to start reading from diff --git a/examples/client_sync.py b/examples/client_sync.py index 15f5b0ae1..6367306d0 100755 --- a/examples/client_sync.py +++ b/examples/client_sync.py @@ -67,7 +67,6 @@ def setup_sync_client(description=None, cmdline=None): framer=args.framer, timeout=args.timeout, # retries=3, - # retry_on_empty=False,y # TCP setup parameters # source_address=("localhost", 0), ) @@ -79,7 +78,6 @@ def setup_sync_client(description=None, cmdline=None): framer=args.framer, timeout=args.timeout, # retries=3, - # retry_on_empty=False, # UDP setup parameters # source_address=None, ) @@ -90,7 +88,6 @@ def setup_sync_client(description=None, cmdline=None): # framer=ModbusRtuFramer, timeout=args.timeout, # retries=3, - # retry_on_empty=False, # Serial setup parameters baudrate=args.baudrate, # bytesize=8, @@ -106,7 +103,6 @@ def setup_sync_client(description=None, cmdline=None): framer=args.framer, timeout=args.timeout, # retries=3, - # retry_on_empty=False, # TLS setup parameters sslctx=modbusClient.ModbusTlsClient.generate_ssl( certfile=helper.get_certificate("crt"), diff --git a/examples/contrib/solar.py b/examples/contrib/solar.py index 406f978fe..a2e951f96 100755 --- a/examples/contrib/solar.py +++ b/examples/contrib/solar.py @@ -31,7 +31,6 @@ def main(): # Common optional parameters: framer=ModbusSocketFramer, timeout=1, - retry_on_empty=True, ) client.connect() _logger.info("### Client connected") diff --git a/examples/package_test_tool.py b/examples/package_test_tool.py index 142379f17..9e1020c94 100755 --- a/examples/package_test_tool.py +++ b/examples/package_test_tool.py @@ -207,7 +207,7 @@ async def client_calls(client): """Test client API.""" Log.debug("--> Client calls starting.") try: - resp = await client.read_holding_registers(address=124, count=4, slave=0) + resp = await client.read_holding_registers(address=124, count=4, slave=1) except ModbusException as exc: txt = f"ERROR: exception in pymodbus {exc}" Log.error(txt) diff --git a/examples/simple_async_client.py b/examples/simple_async_client.py index 24c95beaa..ba377c27d 100755 --- a/examples/simple_async_client.py +++ b/examples/simple_async_client.py @@ -33,7 +33,6 @@ async def run_async_simple_client(comm, host, port, framer=FramerType.SOCKET): framer=framer, # timeout=10, # retries=3, - # retry_on_empty=False, # source_address=("localhost", 0), ) elif comm == "udp": @@ -43,7 +42,6 @@ async def run_async_simple_client(comm, host, port, framer=FramerType.SOCKET): framer=framer, # timeout=10, # retries=3, - # retry_on_empty=False, # source_address=None, ) elif comm == "serial": @@ -52,7 +50,6 @@ async def run_async_simple_client(comm, host, port, framer=FramerType.SOCKET): framer=framer, # timeout=10, # retries=3, - # retry_on_empty=False, baudrate=9600, bytesize=8, parity="N", diff --git a/examples/simple_sync_client.py b/examples/simple_sync_client.py index d2519d864..43dabd0c5 100755 --- a/examples/simple_sync_client.py +++ b/examples/simple_sync_client.py @@ -35,7 +35,6 @@ def run_sync_simple_client(comm, host, port, framer=FramerType.SOCKET): framer=framer, # timeout=10, # retries=3, - # retry_on_empty=False,y # source_address=("localhost", 0), ) elif comm == "udp": @@ -45,7 +44,6 @@ def run_sync_simple_client(comm, host, port, framer=FramerType.SOCKET): framer=framer, # timeout=10, # retries=3, - # retry_on_empty=False, # source_address=None, ) elif comm == "serial": @@ -54,7 +52,6 @@ def run_sync_simple_client(comm, host, port, framer=FramerType.SOCKET): framer=framer, # timeout=10, # retries=3, - # retry_on_empty=False, baudrate=9600, bytesize=8, parity="N", diff --git a/pymodbus/client/base.py b/pymodbus/client/base.py index 0e8d5e9fa..26d7871b8 100644 --- a/pymodbus/client/base.py +++ b/pymodbus/client/base.py @@ -29,9 +29,6 @@ def __init__( self, framer: FramerType, retries: int, - retry_on_empty: bool, - broadcast_enable: bool, - no_resend_on_retry: bool, on_connect_callback: Callable[[bool], None] | None, comm_params: CommParams | None = None, ) -> None: @@ -40,14 +37,11 @@ def __init__( if comm_params: self.comm_params = comm_params self.retries = retries - self.retry_on_empty = retry_on_empty self.ctx = ModbusClientProtocol( framer, self.comm_params, on_connect_callback, ) - self.no_resend_on_retry = no_resend_on_retry - self.broadcast_enable = broadcast_enable # Common variables. self.use_udp = False @@ -119,10 +113,9 @@ async def async_execute(self, request) -> ModbusResponse: while count <= self.retries: async with self._lock: req = self.build_response(request) - if not count or not self.no_resend_on_retry: - self.ctx.framer.resetFrame() - self.ctx.send(packet) - if self.broadcast_enable and not request.slave_id: + self.ctx.framer.resetFrame() + self.ctx.send(packet) + if not request.slave_id: resp = None break try: @@ -184,9 +177,6 @@ def __init__( self, framer: FramerType, retries: int, - retry_on_empty: bool, - broadcast_enable: bool, - no_resend_on_retry: bool, comm_params: CommParams | None = None, ) -> None: """Initialize a client instance.""" @@ -194,9 +184,6 @@ def __init__( if comm_params: self.comm_params = comm_params self.retries = retries - self.broadcast_enable = bool(broadcast_enable) - self.retry_on_empty = retry_on_empty - self.no_resend_on_retry = no_resend_on_retry self.slaves: list[int] = [] # Common variables. @@ -205,7 +192,6 @@ def __init__( )(ClientDecoder(), self) self.transaction = SyncModbusTransactionManager( self, - retry_on_empty, self.retries, ) self.reconnect_delay_current = self.comm_params.reconnect_delay or 0 diff --git a/pymodbus/client/mixin.py b/pymodbus/client/mixin.py index 7f0d710a6..e7e5c137b 100644 --- a/pymodbus/client/mixin.py +++ b/pymodbus/client/mixin.py @@ -61,7 +61,7 @@ def execute(self, _request: ModbusRequest) -> T: """ raise NotImplementedError("execute of ModbusClientMixin needs to be overridden") - def read_coils(self, address: int, count: int = 1, slave: int = 0) -> T: + def read_coils(self, address: int, count: int = 1, slave: int = 1) -> T: """Read coils (code 0x01). :param address: Start address to read from @@ -71,7 +71,7 @@ def read_coils(self, address: int, count: int = 1, slave: int = 0) -> T: """ return self.execute(pdu_bit_read.ReadCoilsRequest(address, count, slave=slave)) - def read_discrete_inputs(self, address: int, count: int = 1, slave: int = 0) -> T: + def read_discrete_inputs(self, address: int, count: int = 1, slave: int = 1) -> T: """Read discrete inputs (code 0x02). :param address: Start address to read from @@ -81,7 +81,7 @@ def read_discrete_inputs(self, address: int, count: int = 1, slave: int = 0) -> """ return self.execute(pdu_bit_read.ReadDiscreteInputsRequest(address, count, slave=slave)) - def read_holding_registers(self, address: int, count: int = 1, slave: int = 0) -> T: + def read_holding_registers(self, address: int, count: int = 1, slave: int = 1) -> T: """Read holding registers (code 0x03). :param address: Start address to read from @@ -91,7 +91,7 @@ def read_holding_registers(self, address: int, count: int = 1, slave: int = 0) - """ return self.execute(pdu_reg_read.ReadHoldingRegistersRequest(address, count, slave=slave)) - def read_input_registers(self, address: int, count: int = 1, slave: int = 0) -> T: + def read_input_registers(self, address: int, count: int = 1, slave: int = 1) -> T: """Read input registers (code 0x04). :param address: Start address to read from @@ -101,7 +101,7 @@ def read_input_registers(self, address: int, count: int = 1, slave: int = 0) -> """ return self.execute(pdu_reg_read.ReadInputRegistersRequest(address, count, slave=slave)) - def write_coil(self, address: int, value: bool, slave: int = 0) -> T: + def write_coil(self, address: int, value: bool, slave: int = 1) -> T: """Write single coil (code 0x05). :param address: Address to write to @@ -111,7 +111,7 @@ def write_coil(self, address: int, value: bool, slave: int = 0) -> T: """ return self.execute(pdu_bit_write.WriteSingleCoilRequest(address, value, slave=slave)) - def write_register(self, address: int, value: int, slave: int = 0) -> T: + def write_register(self, address: int, value: int, slave: int = 1) -> T: """Write register (code 0x06). :param address: Address to write to @@ -121,7 +121,7 @@ def write_register(self, address: int, value: int, slave: int = 0) -> T: """ return self.execute(pdu_req_write.WriteSingleRegisterRequest(address, value, slave=slave)) - def read_exception_status(self, slave: int = 0) -> T: + def read_exception_status(self, slave: int = 1) -> T: """Read Exception Status (code 0x07). :param slave: (optional) Modbus slave ID @@ -131,7 +131,7 @@ def read_exception_status(self, slave: int = 0) -> T: def diag_query_data( - self, msg: bytes, slave: int = 0) -> T: + self, msg: bytes, slave: int = 1) -> T: """Diagnose query data (code 0x08 sub 0x00). :param msg: Message to be returned @@ -141,7 +141,7 @@ def diag_query_data( return self.execute(pdu_diag.ReturnQueryDataRequest(msg, slave=slave)) def diag_restart_communication( - self, toggle: bool, slave: int = 0) -> T: + self, toggle: bool, slave: int = 1) -> T: """Diagnose restart communication (code 0x08 sub 0x01). :param toggle: True if toggled. @@ -152,7 +152,7 @@ def diag_restart_communication( pdu_diag.RestartCommunicationsOptionRequest(toggle, slave=slave) ) - def diag_read_diagnostic_register(self, slave: int = 0) -> T: + def diag_read_diagnostic_register(self, slave: int = 1) -> T: """Diagnose read diagnostic register (code 0x08 sub 0x02). :param slave: (optional) Modbus slave ID @@ -162,7 +162,7 @@ def diag_read_diagnostic_register(self, slave: int = 0) -> T: pdu_diag.ReturnDiagnosticRegisterRequest(slave=slave) ) - def diag_change_ascii_input_delimeter(self, slave: int = 0) -> T: + def diag_change_ascii_input_delimeter(self, slave: int = 1) -> T: """Diagnose change ASCII input delimiter (code 0x08 sub 0x03). :param slave: (optional) Modbus slave ID @@ -172,7 +172,7 @@ def diag_change_ascii_input_delimeter(self, slave: int = 0) -> T: pdu_diag.ChangeAsciiInputDelimiterRequest(slave=slave) ) - def diag_force_listen_only(self, slave: int = 0) -> T: + def diag_force_listen_only(self, slave: int = 1) -> T: """Diagnose force listen only (code 0x08 sub 0x04). :param slave: (optional) Modbus slave ID @@ -180,7 +180,7 @@ def diag_force_listen_only(self, slave: int = 0) -> T: """ return self.execute(pdu_diag.ForceListenOnlyModeRequest(slave=slave)) - def diag_clear_counters(self, slave: int = 0) -> T: + def diag_clear_counters(self, slave: int = 1) -> T: """Diagnose clear counters (code 0x08 sub 0x0A). :param slave: (optional) Modbus slave ID @@ -188,7 +188,7 @@ def diag_clear_counters(self, slave: int = 0) -> T: """ return self.execute(pdu_diag.ClearCountersRequest(slave=slave)) - def diag_read_bus_message_count(self, slave: int = 0) -> T: + def diag_read_bus_message_count(self, slave: int = 1) -> T: """Diagnose read bus message count (code 0x08 sub 0x0B). :param slave: (optional) Modbus slave ID @@ -198,7 +198,7 @@ def diag_read_bus_message_count(self, slave: int = 0) -> T: pdu_diag.ReturnBusMessageCountRequest(slave=slave) ) - def diag_read_bus_comm_error_count(self, slave: int = 0) -> T: + def diag_read_bus_comm_error_count(self, slave: int = 1) -> T: """Diagnose read Bus Communication Error Count (code 0x08 sub 0x0C). :param slave: (optional) Modbus slave ID @@ -208,7 +208,7 @@ def diag_read_bus_comm_error_count(self, slave: int = 0) -> T: pdu_diag.ReturnBusCommunicationErrorCountRequest(slave=slave) ) - def diag_read_bus_exception_error_count(self, slave: int = 0) -> T: + def diag_read_bus_exception_error_count(self, slave: int = 1) -> T: """Diagnose read Bus Exception Error Count (code 0x08 sub 0x0D). :param slave: (optional) Modbus slave ID @@ -218,7 +218,7 @@ def diag_read_bus_exception_error_count(self, slave: int = 0) -> T: pdu_diag.ReturnBusExceptionErrorCountRequest(slave=slave) ) - def diag_read_slave_message_count(self, slave: int = 0) -> T: + def diag_read_slave_message_count(self, slave: int = 1) -> T: """Diagnose read Slave Message Count (code 0x08 sub 0x0E). :param slave: (optional) Modbus slave ID @@ -228,7 +228,7 @@ def diag_read_slave_message_count(self, slave: int = 0) -> T: pdu_diag.ReturnSlaveMessageCountRequest(slave=slave) ) - def diag_read_slave_no_response_count(self, slave: int = 0) -> T: + def diag_read_slave_no_response_count(self, slave: int = 1) -> T: """Diagnose read Slave No Response Count (code 0x08 sub 0x0F). :param slave: (optional) Modbus slave ID @@ -238,7 +238,7 @@ def diag_read_slave_no_response_count(self, slave: int = 0) -> T: pdu_diag.ReturnSlaveNoResponseCountRequest(slave=slave) ) - def diag_read_slave_nak_count(self, slave: int = 0) -> T: + def diag_read_slave_nak_count(self, slave: int = 1) -> T: """Diagnose read Slave NAK Count (code 0x08 sub 0x10). :param slave: (optional) Modbus slave ID @@ -246,7 +246,7 @@ def diag_read_slave_nak_count(self, slave: int = 0) -> T: """ return self.execute(pdu_diag.ReturnSlaveNAKCountRequest(slave=slave)) - def diag_read_slave_busy_count(self, slave: int = 0) -> T: + def diag_read_slave_busy_count(self, slave: int = 1) -> T: """Diagnose read Slave Busy Count (code 0x08 sub 0x11). :param slave: (optional) Modbus slave ID @@ -254,7 +254,7 @@ def diag_read_slave_busy_count(self, slave: int = 0) -> T: """ return self.execute(pdu_diag.ReturnSlaveBusyCountRequest(slave=slave)) - def diag_read_bus_char_overrun_count(self, slave: int = 0) -> T: + def diag_read_bus_char_overrun_count(self, slave: int = 1) -> T: """Diagnose read Bus Character Overrun Count (code 0x08 sub 0x12). :param slave: (optional) Modbus slave ID @@ -264,7 +264,7 @@ def diag_read_bus_char_overrun_count(self, slave: int = 0) -> T: pdu_diag.ReturnSlaveBusCharacterOverrunCountRequest(slave=slave) ) - def diag_read_iop_overrun_count(self, slave: int = 0) -> T: + def diag_read_iop_overrun_count(self, slave: int = 1) -> T: """Diagnose read Iop overrun count (code 0x08 sub 0x13). :param slave: (optional) Modbus slave ID @@ -274,7 +274,7 @@ def diag_read_iop_overrun_count(self, slave: int = 0) -> T: pdu_diag.ReturnIopOverrunCountRequest(slave=slave) ) - def diag_clear_overrun_counter(self, slave: int = 0) -> T: + def diag_clear_overrun_counter(self, slave: int = 1) -> T: """Diagnose Clear Overrun Counter and Flag (code 0x08 sub 0x14). :param slave: (optional) Modbus slave ID @@ -282,7 +282,7 @@ def diag_clear_overrun_counter(self, slave: int = 0) -> T: """ return self.execute(pdu_diag.ClearOverrunCountRequest(slave=slave)) - def diag_getclear_modbus_response(self, slave: int = 0) -> T: + def diag_getclear_modbus_response(self, slave: int = 1) -> T: """Diagnose Get/Clear modbus plus (code 0x08 sub 0x15). :param slave: (optional) Modbus slave ID @@ -290,14 +290,14 @@ def diag_getclear_modbus_response(self, slave: int = 0) -> T: """ return self.execute(pdu_diag.GetClearModbusPlusRequest(slave=slave)) - def diag_get_comm_event_counter(self, slave: int = 0) -> T: + def diag_get_comm_event_counter(self, slave: int = 1) -> T: """Diagnose get event counter (code 0x0B). :raises ModbusException: """ return self.execute(pdu_other_msg.GetCommEventCounterRequest(slave=slave)) - def diag_get_comm_event_log(self, slave: int = 0) -> T: + def diag_get_comm_event_log(self, slave: int = 1) -> T: """Diagnose get event counter (code 0x0C). :raises ModbusException: @@ -308,7 +308,7 @@ def write_coils( self, address: int, values: list[bool] | bool, - slave: int = 0, + slave: int = 1, ) -> T: """Write coils (code 0x0F). @@ -322,7 +322,7 @@ def write_coils( ) def write_registers( - self, address: int, values: list[int] | int, slave: int = 0, skip_encode: bool = False) -> T: + self, address: int, values: list[int] | int, slave: int = 1, skip_encode: bool = False) -> T: """Write registers (code 0x10). :param address: Start address to write to @@ -335,7 +335,7 @@ def write_registers( pdu_req_write.WriteMultipleRegistersRequest(address, values, slave=slave, skip_encode=skip_encode) ) - def report_slave_id(self, slave: int = 0) -> T: + def report_slave_id(self, slave: int = 1) -> T: """Report slave ID (code 0x11). :param slave: (optional) Modbus slave ID @@ -343,7 +343,7 @@ def report_slave_id(self, slave: int = 0) -> T: """ return self.execute(pdu_other_msg.ReportSlaveIdRequest(slave=slave)) - def read_file_record(self, records: list[tuple], slave: int = 0) -> T: + def read_file_record(self, records: list[tuple], slave: int = 1) -> T: """Read file record (code 0x14). :param records: List of (Reference type, File number, Record Number, Record Length) @@ -352,7 +352,7 @@ def read_file_record(self, records: list[tuple], slave: int = 0) -> T: """ return self.execute(pdu_file_msg.ReadFileRecordRequest(records, slave=slave)) - def write_file_record(self, records: list[tuple], slave: int = 0) -> T: + def write_file_record(self, records: list[tuple], slave: int = 1) -> T: """Write file record (code 0x15). :param records: List of (Reference type, File number, Record Number, Record Length) @@ -366,7 +366,7 @@ def mask_write_register( address: int = 0x0000, and_mask: int = 0xFFFF, or_mask: int = 0x0000, - slave: int = 0, + slave: int = 1, ) -> T: """Mask write register (code 0x16). @@ -387,7 +387,7 @@ def readwrite_registers( write_address: int = 0, address: int | None = None, values: list[int] | int = 0, - slave: int = 0, + slave: int = 1, ) -> T: """Read/Write registers (code 0x17). @@ -412,7 +412,7 @@ def readwrite_registers( ) ) - def read_fifo_queue(self, address: int = 0x0000, slave: int = 0) -> T: + def read_fifo_queue(self, address: int = 0x0000, slave: int = 1) -> T: """Read FIFO queue (code 0x18). :param address: The address to start reading from @@ -424,7 +424,7 @@ def read_fifo_queue(self, address: int = 0x0000, slave: int = 0) -> T: # code 0x2B sub 0x0D: CANopen General Reference Request and Response, NOT IMPLEMENTED def read_device_information( - self, read_code: int | None = None, object_id: int = 0x00, slave: int = 0) -> T: + self, read_code: int | None = None, object_id: int = 0x00, slave: int = 1) -> T: """Read FIFO queue (code 0x2B sub 0x0E). :param read_code: The device information read code diff --git a/pymodbus/client/modbusclientprotocol.py b/pymodbus/client/modbusclientprotocol.py index 4412462fa..fffe822e2 100644 --- a/pymodbus/client/modbusclientprotocol.py +++ b/pymodbus/client/modbusclientprotocol.py @@ -14,14 +14,6 @@ class ModbusClientProtocol(ModbusProtocol): """**ModbusClientProtocol**. - Fixed parameters: - - :param framer: Framer enum name - :param params: Comm parameters for transport - :param retries: Max number of retries per request. - :param retry_on_empty: Retry on empty response. - :param on_connect_callback: Will be called when connected/disconnected (bool parameter) - :mod:`ModbusClientProtocol` is normally not referenced outside :mod:`pymodbus`. """ @@ -76,7 +68,7 @@ def callback_data(self, data: bytes, addr: tuple | None = None) -> int: returns number of bytes consumed """ - self.framer.processIncomingPacket(data, self._handle_response, slave=0) + self.framer.processIncomingPacket(data, self._handle_response, 0) return len(data) def __str__(self): diff --git a/pymodbus/client/serial.py b/pymodbus/client/serial.py index a53fa305c..833c7fd41 100644 --- a/pymodbus/client/serial.py +++ b/pymodbus/client/serial.py @@ -44,9 +44,6 @@ class AsyncModbusSerialClient(ModbusBaseClient): :param reconnect_delay_max: Maximum delay in seconds.milliseconds before reconnecting. :param timeout: Timeout for a connection request, in seconds. :param retries: Max number of retries per request. - :param retry_on_empty: Retry on empty response. - :param broadcast_enable: True to treat id 0 as broadcast address. - :param no_resend_on_retry: Do not resend request when retrying due to missing response. :param on_reconnect_callback: Function that will be called just before a reconnection attempt. .. tip:: @@ -82,9 +79,6 @@ def __init__( # pylint: disable=too-many-arguments reconnect_delay_max: float = 300, timeout: float = 3, retries: int = 3, - retry_on_empty: bool = False, - broadcast_enable: bool = False, - no_resend_on_retry: bool = False, on_connect_callback: Callable[[bool], None] | None = None, ) -> None: """Initialize Asyncio Modbus Serial Client.""" @@ -110,9 +104,6 @@ def __init__( # pylint: disable=too-many-arguments self, framer, retries, - retry_on_empty, - broadcast_enable, - no_resend_on_retry, on_connect_callback, ) @@ -141,9 +132,6 @@ class ModbusSerialClient(ModbusBaseSyncClient): :param reconnect_delay_max: Maximum delay in seconds.milliseconds before reconnecting. :param timeout: Timeout for a connection request, in seconds. :param retries: Max number of retries per request. - :param retry_on_empty: Retry on empty response. - :param broadcast_enable: True to treat id 0 as broadcast address. - :param no_resend_on_retry: Do not resend request when retrying due to missing response. .. tip:: **reconnect_delay** doubles automatically with each unsuccessful connect, from @@ -184,9 +172,6 @@ def __init__( # pylint: disable=too-many-arguments reconnect_delay_max: float = 300, timeout: float = 3, retries: int = 3, - retry_on_empty: bool = False, - broadcast_enable: bool = False, - no_resend_on_retry: bool = False, ) -> None: """Initialize Modbus Serial Client.""" self.comm_params = CommParams( @@ -205,9 +190,6 @@ def __init__( # pylint: disable=too-many-arguments super().__init__( framer, retries, - retry_on_empty, - broadcast_enable, - no_resend_on_retry, ) self.socket: serial.Serial | None = None self.last_frame_end = None diff --git a/pymodbus/client/tcp.py b/pymodbus/client/tcp.py index 8fcba4396..ca40ae92a 100644 --- a/pymodbus/client/tcp.py +++ b/pymodbus/client/tcp.py @@ -30,9 +30,6 @@ class AsyncModbusTcpClient(ModbusBaseClient): :param reconnect_delay_max: Maximum delay in seconds.milliseconds before reconnecting. :param timeout: Timeout for a connection request, in seconds. :param retries: Max number of retries per request. - :param retry_on_empty: Retry on empty response. - :param broadcast_enable: True to treat id 0 as broadcast address. - :param no_resend_on_retry: Do not resend request when retrying due to missing response. :param on_reconnect_callback: Function that will be called just before a reconnection attempt. .. tip:: @@ -67,9 +64,6 @@ def __init__( # pylint: disable=too-many-arguments reconnect_delay_max: float = 300, timeout: float = 3, retries: int = 3, - retry_on_empty: bool = False, - broadcast_enable: bool = False, - no_resend_on_retry: bool = False, on_connect_callback: Callable[[bool], None] | None = None, ) -> None: """Initialize Asyncio Modbus TCP Client.""" @@ -88,9 +82,6 @@ def __init__( # pylint: disable=too-many-arguments self, framer, retries, - retry_on_empty, - broadcast_enable, - no_resend_on_retry, on_connect_callback, ) @@ -116,9 +107,6 @@ class ModbusTcpClient(ModbusBaseSyncClient): :param reconnect_delay_max: Maximum delay in seconds.milliseconds before reconnecting. :param timeout: Timeout for a connection request, in seconds. :param retries: Max number of retries per request. - :param retry_on_empty: Retry on empty response. - :param broadcast_enable: True to treat id 0 as broadcast address. - :param no_resend_on_retry: Do not resend request when retrying due to missing response. .. tip:: **reconnect_delay** doubles automatically with each unsuccessful connect, from @@ -143,7 +131,7 @@ async def run(): socket: socket.socket | None - def __init__( # pylint: disable=too-many-arguments + def __init__( self, host: str, framer: FramerType = FramerType.SOCKET, @@ -154,9 +142,6 @@ def __init__( # pylint: disable=too-many-arguments reconnect_delay_max: float = 300, timeout: float = 3, retries: int = 3, - retry_on_empty: bool = False, - broadcast_enable: bool = False, - no_resend_on_retry: bool = False, ) -> None: """Initialize Modbus TCP Client.""" if not hasattr(self,"comm_params"): @@ -170,13 +155,7 @@ def __init__( # pylint: disable=too-many-arguments reconnect_delay_max=reconnect_delay_max, timeout_connect=timeout, ) - super().__init__( - framer, - retries, - retry_on_empty, - broadcast_enable, - no_resend_on_retry, - ) + super().__init__(framer, retries) self.socket = None @property diff --git a/pymodbus/client/tls.py b/pymodbus/client/tls.py index 4fac9fae7..deddd6bc2 100644 --- a/pymodbus/client/tls.py +++ b/pymodbus/client/tls.py @@ -29,9 +29,6 @@ class AsyncModbusTlsClient(AsyncModbusTcpClient): :param reconnect_delay_max: Maximum delay in seconds.milliseconds before reconnecting. :param timeout: Timeout for a connection request, in seconds. :param retries: Max number of retries per request. - :param retry_on_empty: Retry on empty response. - :param broadcast_enable: True to treat id 0 as broadcast address. - :param no_resend_on_retry: Do not resend request when retrying due to missing response. :param on_reconnect_callback: Function that will be called just before a reconnection attempt. .. tip:: @@ -65,9 +62,6 @@ def __init__( # pylint: disable=too-many-arguments reconnect_delay_max: float = 300, timeout: float = 3, retries: int = 3, - retry_on_empty: bool = False, - broadcast_enable: bool = False, - no_resend_on_retry: bool = False, on_connect_callback: Callable[[bool], None] | None = None, ): """Initialize Asyncio Modbus TLS Client.""" @@ -87,9 +81,6 @@ def __init__( # pylint: disable=too-many-arguments "", framer=framer, retries=retries, - retry_on_empty=retry_on_empty, - broadcast_enable=broadcast_enable, - no_resend_on_retry=no_resend_on_retry, on_connect_callback=on_connect_callback, ) @@ -133,9 +124,6 @@ class ModbusTlsClient(ModbusTcpClient): :param reconnect_delay_max: Maximum delay in seconds.milliseconds before reconnecting. :param timeout: Timeout for a connection request, in seconds. :param retries: Max number of retries per request. - :param retry_on_empty: Retry on empty response. - :param broadcast_enable: True to treat id 0 as broadcast address. - :param no_resend_on_retry: Do not resend request when retrying due to missing response. .. tip:: **reconnect_delay** doubles automatically with each unsuccessful connect, from @@ -170,9 +158,6 @@ def __init__( # pylint: disable=too-many-arguments reconnect_delay_max: float = 300, timeout: float = 3, retries: int = 3, - retry_on_empty: bool = False, - broadcast_enable: bool = False, - no_resend_on_retry: bool = False, ): """Initialize Modbus TLS Client.""" self.comm_params = CommParams( @@ -190,9 +175,6 @@ def __init__( # pylint: disable=too-many-arguments "", framer=framer, retries=retries, - retry_on_empty=retry_on_empty, - broadcast_enable=broadcast_enable, - no_resend_on_retry=no_resend_on_retry, ) @classmethod diff --git a/pymodbus/client/udp.py b/pymodbus/client/udp.py index 3b19708c0..48b71edab 100644 --- a/pymodbus/client/udp.py +++ b/pymodbus/client/udp.py @@ -31,9 +31,6 @@ class AsyncModbusUdpClient(ModbusBaseClient): :param reconnect_delay_max: Maximum delay in seconds.milliseconds before reconnecting. :param timeout: Timeout for a connection request, in seconds. :param retries: Max number of retries per request. - :param retry_on_empty: Retry on empty response. - :param broadcast_enable: True to treat id 0 as broadcast address. - :param no_resend_on_retry: Do not resend request when retrying due to missing response. :param on_reconnect_callback: Function that will be called just before a reconnection attempt. .. tip:: @@ -66,9 +63,6 @@ def __init__( # pylint: disable=too-many-arguments reconnect_delay_max: float = 300, timeout: float = 3, retries: int = 3, - retry_on_empty: bool = False, - broadcast_enable: bool = False, - no_resend_on_retry: bool = False, on_connect_callback: Callable[[bool], None] | None = None, ) -> None: """Initialize Asyncio Modbus UDP Client.""" @@ -85,11 +79,8 @@ def __init__( # pylint: disable=too-many-arguments ModbusBaseClient.__init__( self, framer, - retries=retries, - retry_on_empty=retry_on_empty, - broadcast_enable=broadcast_enable, - no_resend_on_retry=no_resend_on_retry, - on_connect_callback=on_connect_callback, + retries, + on_connect_callback, ) self.source_address = source_address @@ -116,9 +107,6 @@ class ModbusUdpClient(ModbusBaseSyncClient): :param reconnect_delay_max: Maximum delay in seconds.milliseconds before reconnecting. :param timeout: Timeout for a connection request, in seconds. :param retries: Max number of retries per request. - :param retry_on_empty: Retry on empty response. - :param broadcast_enable: True to treat id 0 as broadcast address. - :param no_resend_on_retry: Do not resend request when retrying due to missing response. .. tip:: **reconnect_delay** doubles automatically with each unsuccessful connect, from @@ -143,7 +131,7 @@ async def run(): socket: socket.socket | None - def __init__( # pylint: disable=too-many-arguments + def __init__( self, host: str, framer: FramerType = FramerType.SOCKET, @@ -154,9 +142,6 @@ def __init__( # pylint: disable=too-many-arguments reconnect_delay_max: float = 300, timeout: float = 3, retries: int = 3, - retry_on_empty: bool = False, - broadcast_enable: bool = False, - no_resend_on_retry: bool = False, ) -> None: """Initialize Modbus UDP Client.""" self.comm_params = CommParams( @@ -169,13 +154,7 @@ def __init__( # pylint: disable=too-many-arguments reconnect_delay_max=reconnect_delay_max, timeout_connect=timeout, ) - super().__init__( - framer, - retries=retries, - retry_on_empty=retry_on_empty, - broadcast_enable=broadcast_enable, - no_resend_on_retry=no_resend_on_retry, - ) + super().__init__(framer, retries) self.socket = None @property diff --git a/pymodbus/pdu/bit_read_message.py b/pymodbus/pdu/bit_read_message.py index 3e22de6e9..4a116dc42 100644 --- a/pymodbus/pdu/bit_read_message.py +++ b/pymodbus/pdu/bit_read_message.py @@ -138,7 +138,7 @@ class ReadCoilsRequest(ReadBitsRequestBase): function_code = 1 function_code_name = "read_coils" - def __init__(self, address=None, count=None, slave=0, transaction=0, protocol=0, skip_encode=False): + def __init__(self, address=None, count=None, slave=1, transaction=0, protocol=0, skip_encode=False): """Initialize a new instance. :param address: The address to start reading from @@ -185,7 +185,7 @@ class ReadCoilsResponse(ReadBitsResponseBase): function_code = 1 - def __init__(self, values=None, slave=0, transaction=0, protocol=0, skip_encode=False): + def __init__(self, values=None, slave=1, transaction=0, protocol=0, skip_encode=False): """Initialize a new instance. :param values: The request values to respond with @@ -206,7 +206,7 @@ class ReadDiscreteInputsRequest(ReadBitsRequestBase): function_code = 2 function_code_name = "read_discrete_input" - def __init__(self, address=None, count=None, slave=0, transaction=0, protocol=0, skip_encode=False): + def __init__(self, address=None, count=None, slave=1, transaction=0, protocol=0, skip_encode=False): """Initialize a new instance. :param address: The address to start reading from @@ -253,7 +253,7 @@ class ReadDiscreteInputsResponse(ReadBitsResponseBase): function_code = 2 - def __init__(self, values=None, slave=0, transaction=0, protocol=0, skip_encode=False): + def __init__(self, values=None, slave=1, transaction=0, protocol=0, skip_encode=False): """Initialize a new instance. :param values: The request values to respond with diff --git a/pymodbus/pdu/bit_write_message.py b/pymodbus/pdu/bit_write_message.py index 7a7e04522..9b531d92c 100644 --- a/pymodbus/pdu/bit_write_message.py +++ b/pymodbus/pdu/bit_write_message.py @@ -113,7 +113,7 @@ class WriteSingleCoilResponse(ModbusResponse): function_code = 5 _rtu_frame_size = 8 - def __init__(self, address=None, value=None, slave=0, transaction=0, protocol=0, skip_encode=False): + def __init__(self, address=None, value=None, slave=1, transaction=0, protocol=0, skip_encode=False): """Initialize a new instance. :param address: The variable address written to @@ -250,7 +250,7 @@ class WriteMultipleCoilsResponse(ModbusResponse): function_code = 15 _rtu_frame_size = 8 - def __init__(self, address=None, count=None, slave=0, transaction=0, protocol=0, skip_encode=False): + def __init__(self, address=None, count=None, slave=1, transaction=0, protocol=0, skip_encode=False): """Initialize a new instance. :param address: The starting variable address written to diff --git a/pymodbus/pdu/diag_message.py b/pymodbus/pdu/diag_message.py index 1966fc85b..de78c2def 100644 --- a/pymodbus/pdu/diag_message.py +++ b/pymodbus/pdu/diag_message.py @@ -31,7 +31,7 @@ class DiagnosticStatusRequest(ModbusRequest): function_code_name = "diagnostic_status" _rtu_frame_size = 8 - def __init__(self, slave=0, transaction=0, protocol=0, skip_encode=False): + def __init__(self, slave=1, transaction=0, protocol=0, skip_encode=False): """Initialize a diagnostic request.""" ModbusRequest.__init__(self, slave, transaction, protocol, skip_encode) self.message = None @@ -93,7 +93,7 @@ class DiagnosticStatusResponse(ModbusResponse): function_code = 0x08 _rtu_frame_size = 8 - def __init__(self, slave=0, transaction=0, protocol=0, skip_encode=False): + def __init__(self, slave=1, transaction=0, protocol=0, skip_encode=False): """Initialize a diagnostic response.""" ModbusResponse.__init__(self, slave, transaction, protocol, skip_encode) self.message = None @@ -150,7 +150,7 @@ class DiagnosticStatusSimpleRequest(DiagnosticStatusRequest): the execute method """ - def __init__(self, data=0x0000, slave=0, transaction=0, protocol=0, skip_encode=False): + def __init__(self, data=0x0000, slave=1, transaction=0, protocol=0, skip_encode=False): """Initialize a simple diagnostic request. The data defaults to 0x0000 if not provided as over half @@ -175,7 +175,7 @@ class DiagnosticStatusSimpleResponse(DiagnosticStatusResponse): 2 bytes of data. """ - def __init__(self, data=0x0000, slave=0, transaction=0, protocol=0, skip_encode=False): + def __init__(self, data=0x0000, slave=1, transaction=0, protocol=0, skip_encode=False): """Return a simple diagnostic response. :param data: The resulting data to return to the client @@ -197,7 +197,7 @@ class ReturnQueryDataRequest(DiagnosticStatusRequest): sub_function_code = 0x0000 - def __init__(self, message=b"\x00\x00", slave=0, transaction=0, protocol=0, skip_encode=False): + def __init__(self, message=b"\x00\x00", slave=1, transaction=0, protocol=0, skip_encode=False): """Initialize a new instance of the request. :param message: The message to send to loopback @@ -225,7 +225,7 @@ class ReturnQueryDataResponse(DiagnosticStatusResponse): sub_function_code = 0x0000 - def __init__(self, message=b"\x00\x00", slave=0, transaction=0, protocol=0, skip_encode=False): + def __init__(self, message=b"\x00\x00", slave=1, transaction=0, protocol=0, skip_encode=False): """Initialize a new instance of the response. :param message: The message to loopback @@ -252,7 +252,7 @@ class RestartCommunicationsOptionRequest(DiagnosticStatusRequest): sub_function_code = 0x0001 - def __init__(self, toggle=False, slave=0, transaction=0, protocol=0, skip_encode=False): + def __init__(self, toggle=False, slave=1, transaction=0, protocol=0, skip_encode=False): """Initialize a new request. :param toggle: Set to True to toggle, False otherwise @@ -285,7 +285,7 @@ class RestartCommunicationsOptionResponse(DiagnosticStatusResponse): sub_function_code = 0x0001 - def __init__(self, toggle=False, slave=0, transaction=0, protocol=0, skip_encode=False): + def __init__(self, toggle=False, slave=1, transaction=0, protocol=0, skip_encode=False): """Initialize a new response. :param toggle: Set to True if we toggled, False otherwise @@ -396,7 +396,7 @@ class ForceListenOnlyModeResponse(DiagnosticStatusResponse): sub_function_code = 0x0004 should_respond = False - def __init__(self, slave=0, transaction=0, protocol=0, skip_encode=False): + def __init__(self, slave=1, transaction=0, protocol=0, skip_encode=False): """Initialize to block a return response.""" DiagnosticStatusResponse.__init__(self, slave=slave, transaction=transaction, protocol=protocol, skip_encode=skip_encode) self.message = [] @@ -778,7 +778,7 @@ class GetClearModbusPlusRequest(DiagnosticStatusSimpleRequest): sub_function_code = 0x0015 - def __init__(self, data=0, slave=0, transaction=0, protocol=0, skip_encode=False): + def __init__(self, data=0, slave=1, transaction=0, protocol=0, skip_encode=False): """Initialize.""" super().__init__(slave=slave, transaction=transaction, protocol=protocol, skip_encode=skip_encode) self.message=data diff --git a/pymodbus/pdu/file_message.py b/pymodbus/pdu/file_message.py index 70f40e43c..37adbd5b6 100644 --- a/pymodbus/pdu/file_message.py +++ b/pymodbus/pdu/file_message.py @@ -89,7 +89,7 @@ class ReadFileRecordRequest(ModbusRequest): function_code_name = "read_file_record" _rtu_byte_count_pos = 2 - def __init__(self, records=None, slave=0, transaction=0, protocol=0, skip_encode=False): + def __init__(self, records=None, slave=1, transaction=0, protocol=0, skip_encode=False): """Initialize a new instance. :param records: The file record requests to be read @@ -154,7 +154,7 @@ class ReadFileRecordResponse(ModbusResponse): function_code = 0x14 _rtu_byte_count_pos = 2 - def __init__(self, records=None, slave=0, transaction=0, protocol=0, skip_encode=False): + def __init__(self, records=None, slave=1, transaction=0, protocol=0, skip_encode=False): """Initialize a new instance. :param records: The requested file records @@ -207,7 +207,7 @@ class WriteFileRecordRequest(ModbusRequest): function_code_name = "write_file_record" _rtu_byte_count_pos = 2 - def __init__(self, records=None, slave=0, transaction=0, protocol=0, skip_encode=False): + def __init__(self, records=None, slave=1, transaction=0, protocol=0, skip_encode=False): """Initialize a new instance. :param records: The file record requests to be read @@ -271,7 +271,7 @@ class WriteFileRecordResponse(ModbusResponse): function_code = 0x15 _rtu_byte_count_pos = 2 - def __init__(self, records=None, slave=0, transaction=0, protocol=0, skip_encode=False): + def __init__(self, records=None, slave=1, transaction=0, protocol=0, skip_encode=False): """Initialize a new instance. :param records: The file record requests to be read @@ -336,7 +336,7 @@ class ReadFifoQueueRequest(ModbusRequest): function_code_name = "read_fifo_queue" _rtu_frame_size = 6 - def __init__(self, address=0x0000, slave=0, transaction=0, protocol=0, skip_encode=False): + def __init__(self, address=0x0000, slave=1, transaction=0, protocol=0, skip_encode=False): """Initialize a new instance. :param address: The fifo pointer address (0x0000 to 0xffff) @@ -397,7 +397,7 @@ def calculateRtuFrameSize(cls, buffer): lo_byte = int(buffer[3]) return (hi_byte << 16) + lo_byte + 6 - def __init__(self, values=None, slave=0, transaction=0, protocol=0, skip_encode=False): + def __init__(self, values=None, slave=1, transaction=0, protocol=0, skip_encode=False): """Initialize a new instance. :param values: The list of values of the fifo to return diff --git a/pymodbus/pdu/mei_message.py b/pymodbus/pdu/mei_message.py index 60eb28435..71396bbc8 100644 --- a/pymodbus/pdu/mei_message.py +++ b/pymodbus/pdu/mei_message.py @@ -52,7 +52,7 @@ class ReadDeviceInformationRequest(ModbusRequest): function_code_name = "read_device_information" _rtu_frame_size = 7 - def __init__(self, read_code=None, object_id=0x00, slave=0, transaction=0, protocol=0, skip_encode=False): + def __init__(self, read_code=None, object_id=0x00, slave=1, transaction=0, protocol=0, skip_encode=False): """Initialize a new instance. :param read_code: The device information read code @@ -130,7 +130,7 @@ def calculateRtuFrameSize(cls, buffer): except struct.error as exc: raise IndexError from exc - def __init__(self, read_code=None, information=None, slave=0, transaction=0, protocol=0, skip_encode=False): + def __init__(self, read_code=None, information=None, slave=1, transaction=0, protocol=0, skip_encode=False): """Initialize a new instance. :param read_code: The device information read code diff --git a/pymodbus/pdu/other_message.py b/pymodbus/pdu/other_message.py index 60e55b7f1..408fb0042 100644 --- a/pymodbus/pdu/other_message.py +++ b/pymodbus/pdu/other_message.py @@ -72,7 +72,7 @@ class ReadExceptionStatusResponse(ModbusResponse): function_code = 0x07 _rtu_frame_size = 5 - def __init__(self, status=0x00, slave=0, transaction=0, protocol=0, skip_encode=False): + def __init__(self, status=0x00, slave=1, transaction=0, protocol=0, skip_encode=False): """Initialize a new instance. :param status: The status response to report @@ -135,7 +135,7 @@ class GetCommEventCounterRequest(ModbusRequest): function_code_name = "get_event_counter" _rtu_frame_size = 4 - def __init__(self, slave=0, transaction=0, protocol=0, skip_encode=False): + def __init__(self, slave=1, transaction=0, protocol=0, skip_encode=False): """Initialize a new instance.""" ModbusRequest.__init__(self, slave, transaction, protocol, skip_encode) @@ -178,7 +178,7 @@ class GetCommEventCounterResponse(ModbusResponse): function_code = 0x0B _rtu_frame_size = 8 - def __init__(self, count=0x0000, slave=0, transaction=0, protocol=0, skip_encode=False): + def __init__(self, count=0x0000, slave=1, transaction=0, protocol=0, skip_encode=False): """Initialize a new instance. :param count: The current event counter value @@ -246,7 +246,7 @@ class GetCommEventLogRequest(ModbusRequest): function_code_name = "get_event_log" _rtu_frame_size = 4 - def __init__(self, slave=0, transaction=0, protocol=0, skip_encode=False): + def __init__(self, slave=1, transaction=0, protocol=0, skip_encode=False): """Initialize a new instance.""" ModbusRequest.__init__(self, slave, transaction, protocol, skip_encode) @@ -293,7 +293,7 @@ class GetCommEventLogResponse(ModbusResponse): function_code = 0x0C _rtu_byte_count_pos = 2 - def __init__(self, status=True, message_count=0, event_count=0, events=None, slave=0, transaction=0, protocol=0, skip_encode=False): + def __init__(self, status=True, message_count=0, event_count=0, events=None, slave=1, transaction=0, protocol=0, skip_encode=False): """Initialize a new instance. :param status: The status response to report @@ -367,7 +367,7 @@ class ReportSlaveIdRequest(ModbusRequest): function_code_name = "report_slave_id" _rtu_frame_size = 4 - def __init__(self, slave=0, transaction=0, protocol=0, skip_encode=False): + def __init__(self, slave=1, transaction=0, protocol=0, skip_encode=False): """Initialize a new instance. :param slave: Modbus slave slave ID @@ -426,7 +426,7 @@ class ReportSlaveIdResponse(ModbusResponse): function_code = 0x11 _rtu_byte_count_pos = 2 - def __init__(self, identifier=b"\x00", status=True, slave=0, transaction=0, protocol=0, skip_encode=False): + def __init__(self, identifier=b"\x00", status=True, slave=1, transaction=0, protocol=0, skip_encode=False): """Initialize a new instance. :param identifier: The identifier of the slave diff --git a/pymodbus/pdu/pdu.py b/pymodbus/pdu/pdu.py index 47c15c8b9..67f486839 100644 --- a/pymodbus/pdu/pdu.py +++ b/pymodbus/pdu/pdu.py @@ -184,7 +184,7 @@ class ExceptionResponse(ModbusResponse): ExceptionOffset = 0x80 _rtu_frame_size = 5 - def __init__(self, function_code, exception_code=None, slave=0, transaction=0, protocol=0, skip_encode=False): + def __init__(self, function_code, exception_code=None, slave=1, transaction=0, protocol=0, skip_encode=False): """Initialize the modbus exception response. :param function_code: The function to build an exception response for diff --git a/pymodbus/pdu/register_read_message.py b/pymodbus/pdu/register_read_message.py index a4280379a..ee1fd0e1a 100644 --- a/pymodbus/pdu/register_read_message.py +++ b/pymodbus/pdu/register_read_message.py @@ -14,7 +14,7 @@ class ReadRegistersRequestBase(ModbusRequest): _rtu_frame_size = 8 - def __init__(self, address, count, slave=0, transaction=0, protocol=0, skip_encode=False): + def __init__(self, address, count, slave=1, transaction=0, protocol=0, skip_encode=False): """Initialize a new instance. :param address: The address to start the read from @@ -62,7 +62,7 @@ class ReadRegistersResponseBase(ModbusResponse): _rtu_byte_count_pos = 2 - def __init__(self, values, slave=0, transaction=0, protocol=0, skip_encode=False): + def __init__(self, values, slave=1, transaction=0, protocol=0, skip_encode=False): """Initialize a new instance. :param values: The values to write to @@ -124,7 +124,7 @@ class ReadHoldingRegistersRequest(ReadRegistersRequestBase): function_code = 3 function_code_name = "read_holding_registers" - def __init__(self, address=None, count=None, slave=0, transaction=0, protocol=0, skip_encode=0): + def __init__(self, address=None, count=None, slave=1, transaction=0, protocol=0, skip_encode=0): """Initialize a new instance of the request. :param address: The starting address to read from @@ -184,7 +184,7 @@ class ReadInputRegistersRequest(ReadRegistersRequestBase): function_code = 4 function_code_name = "read_input_registers" - def __init__(self, address=None, count=None, slave=0, transaction=0, protocol=0, skip_encode=0): + def __init__(self, address=None, count=None, slave=1, transaction=0, protocol=0, skip_encode=0): """Initialize a new instance of the request. :param address: The starting address to read from @@ -251,7 +251,7 @@ class ReadWriteMultipleRegistersRequest(ModbusRequest): function_code_name = "read_write_multiple_registers" _rtu_byte_count_pos = 10 - def __init__(self, read_address=0x00, read_count=0, write_address=0x00, write_registers=None, slave=0, transaction=0, protocol=0, skip_encode=False): + def __init__(self, read_address=0x00, read_count=0, write_address=0x00, write_registers=None, slave=1, transaction=0, protocol=0, skip_encode=False): """Initialize a new request message. :param read_address: The address to start reading from diff --git a/pymodbus/pdu/register_write_message.py b/pymodbus/pdu/register_write_message.py index b0b67d5ea..5ee9a1c40 100644 --- a/pymodbus/pdu/register_write_message.py +++ b/pymodbus/pdu/register_write_message.py @@ -91,7 +91,7 @@ class WriteSingleRegisterResponse(ModbusResponse): function_code = 6 _rtu_frame_size = 8 - def __init__(self, address=None, value=None, slave=0, transaction=0, protocol=0, skip_encode=False): + def __init__(self, address=None, value=None, slave=1, transaction=0, protocol=0, skip_encode=False): """Initialize a new instance. :param address: The address to start writing add @@ -239,7 +239,7 @@ class WriteMultipleRegistersResponse(ModbusResponse): function_code = 16 _rtu_frame_size = 8 - def __init__(self, address=None, count=None, slave=0, transaction=0, protocol=0, skip_encode=False): + def __init__(self, address=None, count=None, slave=1, transaction=0, protocol=0, skip_encode=False): """Initialize a new instance. :param address: The address to start writing to @@ -287,7 +287,7 @@ class MaskWriteRegisterRequest(ModbusRequest): function_code_name = "mask_write_register" _rtu_frame_size = 10 - def __init__(self, address=0x0000, and_mask=0xFFFF, or_mask=0x0000, slave=0, transaction=0, protocol=0, skip_encode=False): + def __init__(self, address=0x0000, and_mask=0xFFFF, or_mask=0x0000, slave=1, transaction=0, protocol=0, skip_encode=False): """Initialize a new instance. :param address: The mask pointer address (0x0000 to 0xffff) @@ -342,7 +342,7 @@ class MaskWriteRegisterResponse(ModbusResponse): function_code = 0x16 _rtu_frame_size = 10 - def __init__(self, address=0x0000, and_mask=0xFFFF, or_mask=0x0000, slave=0, transaction=0, protocol=0, skip_encode=False): + def __init__(self, address=0x0000, and_mask=0xFFFF, or_mask=0x0000, slave=1, transaction=0, protocol=0, skip_encode=False): """Initialize new instance. :param address: The mask pointer address (0x0000 to 0xffff) diff --git a/pymodbus/transaction.py b/pymodbus/transaction.py index b64c17932..ce8048604 100644 --- a/pymodbus/transaction.py +++ b/pymodbus/transaction.py @@ -133,16 +133,10 @@ class SyncModbusTransactionManager(ModbusTransactionManager): Results are keyed based on the supplied transaction id. """ - def __init__(self, client: ModbusBaseSyncClient, retry_on_empty, retries): - """Initialize an instance of the ModbusTransactionManager. - - :param client: The client socket wrapper - :param retry_on_empty: Should the client retry on empty - :param retries: The number of retries to allow - """ + def __init__(self, client: ModbusBaseSyncClient, retries): + """Initialize an instance of the ModbusTransactionManager.""" super().__init__() self.client: ModbusBaseSyncClient = client - self.retry_on_empty = retry_on_empty self.retries = retries self._transaction_lock = RLock() self._no_response_devices: list[int] = [] @@ -220,9 +214,7 @@ def execute(self, request: ModbusRequest): # noqa: C901 ): Log.debug("Clearing current Frame: - {}", _buffer) self.client.framer.resetFrame() - if broadcast := ( - self.client.broadcast_enable and not request.slave_id - ): + if broadcast := not request.slave_id: self._transact(request, None, broadcast=True) response = b"Broadcast write sent - no response expected" else: @@ -270,22 +262,8 @@ def execute(self, request: ModbusRequest): # noqa: C901 if not response: if request.slave_id not in self._no_response_devices: self._no_response_devices.append(request.slave_id) - if self.retry_on_empty: - response, last_exception = self._retry_transaction( - retries, - "empty", - request, - expected_response_length, - full=full, - ) - retries -= 1 - else: - # No response received and retries not enabled - break - else: - break - # full = False - Log.debug("Retry getting response: - {}", _buffer) + # No response received and retries not enabled + break self.client.framer.processIncomingPacket( response, self.addTransaction, diff --git a/test/framers/test_old_framers.py b/test/framers/test_old_framers.py index 165f7fc6b..56758bcb3 100644 --- a/test/framers/test_old_framers.py +++ b/test/framers/test_old_framers.py @@ -328,9 +328,6 @@ async def test_send_packet(self, rtu_framer): client = ModbusBaseClient( FramerType.ASCII, 3, - False, - False, - False, None, comm_params=CommParams( comm_type=CommType.TCP, @@ -423,9 +420,9 @@ def _handle_response(_reply): response_ok = False framer = ModbusSocketFramer(ClientDecoder()) if i: - framer.processIncomingPacket(part1, _handle_response, slave=0) + framer.processIncomingPacket(part1, _handle_response, 0) assert not response_ok, "Response should not be accepted" - framer.processIncomingPacket(part2, _handle_response, slave=0) + framer.processIncomingPacket(part2, _handle_response, 0) assert response_ok, "Response is valid, but not accepted" @@ -441,13 +438,13 @@ def _handle_response(_reply): message = bytearray(b"\x00\x02\x00\x00\x00\x03\x01\x84\x02") response_ok = False framer = ModbusSocketFramer(ClientDecoder()) - framer.processIncomingPacket(message, _handle_response, slave=0) + framer.processIncomingPacket(message, _handle_response, 0) assert response_ok, "Response is valid, but not accepted" message = bytearray(b"\x00\x01\x00\x00\x00\x0b\x01\x03\x08\x00\xb5\x12\x2f\x37\x21\x00\x03") response_ok = False framer = ModbusSocketFramer(ClientDecoder()) - framer.processIncomingPacket(message, _handle_response, slave=0) + framer.processIncomingPacket(message, _handle_response, 0) assert response_ok, "Response is valid, but not accepted" def test_recv_socket_exception_faulty(self): @@ -462,16 +459,16 @@ def _handle_response(_reply): message = bytearray(b"\x00\x02\x00\x00\x00\x02\x01\x84\x02") response_ok = False framer = ModbusSocketFramer(ClientDecoder()) - framer.processIncomingPacket(message, _handle_response, slave=0) + framer.processIncomingPacket(message, _handle_response, 0) assert response_ok, "Response is valid, but not accepted" # ---- 100% coverage @pytest.mark.parametrize( ("framer", "message"), [ - (ModbusAsciiFramer, b':00010001000AF4\r\n',), - (ModbusRtuFramer, b"\x00\x01\x00\x01\x00\n\xec\x1c",), - (ModbusSocketFramer, b'\x00\x00\x00\x00\x00\x06\x00\x01\x00\x01\x00\n',), + (ModbusAsciiFramer, b':01010001000AF3\r\n',), + (ModbusRtuFramer, b"\x01\x01\x00\x01\x00\n\xed\xcd",), + (ModbusSocketFramer, b'\x00\x00\x00\x00\x00\x06\x01\x01\x00\x01\x00\n',), ] ) def test_build_packet(self, framer, message): diff --git a/test/framers/test_tbc_transaction.py b/test/framers/test_tbc_transaction.py index 90f93c8d5..eb918727a 100755 --- a/test/framers/test_tbc_transaction.py +++ b/test/framers/test_tbc_transaction.py @@ -41,7 +41,7 @@ def setup_method(self): self._tls = ModbusTlsFramer(decoder=self.decoder, client=None) self._rtu = ModbusRtuFramer(decoder=self.decoder, client=None) self._ascii = ModbusAsciiFramer(decoder=self.decoder, client=None) - self._manager = SyncModbusTransactionManager(self.client, False, 3) + self._manager = SyncModbusTransactionManager(self.client, 3) # ----------------------------------------------------------------------- # # Modbus transaction manager @@ -109,12 +109,11 @@ def test_execute(self): request.get_response_pdu_size.return_value = 10 request.slave_id = 1 request.function_code = 222 - trans = SyncModbusTransactionManager(client, False, 3) + trans = SyncModbusTransactionManager(client, 3) trans._recv = mock.MagicMock( # pylint: disable=protected-access return_value=b"abcdef" ) assert trans.retries == 3 - assert not trans.retry_on_empty trans.getTransaction = mock.MagicMock() trans.getTransaction.return_value = "response" @@ -131,7 +130,6 @@ def test_execute(self): assert isinstance(response, ModbusIOException) # No response with retries - trans.retry_on_empty = True trans._recv = mock.MagicMock( # pylint: disable=protected-access side_effect=iter([b"", b"abcdef"]) ) @@ -143,7 +141,6 @@ def test_execute(self): side_effect=iter([b"abcdef", b"deadbe", b"123456"]) ) client.comm_params.handle_local_echo = True - trans.retry_on_empty = False assert trans.execute(request).message == "[Input/Output] Wrong local echo" client.comm_params.handle_local_echo = False @@ -164,14 +161,12 @@ def test_execute(self): assert isinstance(trans.execute(request), ModbusIOException) # Broadcast - client.params.broadcast_enable = True request.slave_id = 0 response = trans.execute(request) assert response == b"Broadcast write sent - no response expected" # Broadcast w/ Local echo client.comm_params.handle_local_echo = True - client.params.broadcast_enable = True recv = mock.MagicMock(return_value=b"deadbeef") trans._recv = recv # pylint: disable=protected-access request.slave_id = 0 diff --git a/test/sub_client/test_client.py b/test/sub_client/test_client.py index 48a042964..6111dfb59 100755 --- a/test/sub_client/test_client.py +++ b/test/sub_client/test_client.py @@ -122,16 +122,12 @@ def fake_execute(_self, request): "opt_args": { "timeout": 3 + 2, "retries": 3 + 2, - "retry_on_empty": True, - "broadcast_enable": not False, "reconnect_delay": 117, "reconnect_delay_max": 250, }, "defaults": { "timeout": 3, "retries": 3, - "retry_on_empty": False, - "broadcast_enable": False, "reconnect_delay": 100, "reconnect_delay_max": 1000 * 60 * 5, }, @@ -264,9 +260,6 @@ async def test_client_modbusbaseclient(): client = ModbusBaseClient( FramerType.ASCII, 3, - False, - False, - False, None, comm_params=CommParams( host="localhost", @@ -306,9 +299,6 @@ async def test_client_base_async(): async with ModbusBaseClient( FramerType.ASCII, 3, - False, - False, - False, None, comm_params=CommParams( host="localhost", @@ -329,9 +319,6 @@ async def test_client_protocol_receiver(): base = ModbusBaseClient( FramerType.SOCKET, 3, - False, - False, - False, None, comm_params=CommParams(), ) @@ -359,9 +346,6 @@ async def test_client_protocol_response(): base = ModbusBaseClient( FramerType.SOCKET, 3, - False, - False, - False, None, comm_params=CommParams(), ) @@ -380,9 +364,6 @@ async def test_client_protocol_handler(): base = ModbusBaseClient( FramerType.ASCII, 3, - False, - False, - False, None, comm_params=CommParams( host="localhost", @@ -439,9 +420,6 @@ async def test_client_protocol_execute(): base = ModbusBaseClient( FramerType.SOCKET, 3, - False, - False, - False, None, comm_params=CommParams( host="127.0.0.1", @@ -461,29 +439,23 @@ async def test_client_execute_broadcast(): base = ModbusBaseClient( FramerType.SOCKET, 3, - False, - False, - False, None, comm_params=CommParams( host="127.0.0.1", ), ) - base.broadcast_enable = True request = pdu_bit_read.ReadCoilsRequest(1, 1) transport = MockTransport(base, request) base.ctx.connection_made(transport=transport) - assert not await base.async_execute(request) + with pytest.raises(ModbusIOException): + assert not await base.async_execute(request) async def test_client_protocol_retry(): """Test the client protocol execute method with retries.""" base = ModbusBaseClient( FramerType.SOCKET, 3, - False, - False, - False, None, comm_params=CommParams( host="127.0.0.1", @@ -505,9 +477,6 @@ async def test_client_protocol_timeout(): base = ModbusBaseClient( FramerType.SOCKET, 2, - False, - False, - False, None, comm_params=CommParams( host="127.0.0.1", @@ -716,9 +685,6 @@ async def test_client_build_response(): client = ModbusBaseClient( FramerType.RTU, 3, - False, - False, - False, None, comm_params=CommParams(), ) diff --git a/test/sub_function_codes/test_all_messages.py b/test/sub_function_codes/test_all_messages.py index 9e20ea5ed..3454cf191 100644 --- a/test/sub_function_codes/test_all_messages.py +++ b/test/sub_function_codes/test_all_messages.py @@ -84,12 +84,12 @@ def test_initializing_slave_address_response(self): def test_forwarding_to_pdu(self): """Test that parameters are forwarded to the pdu correctly.""" - request = ReadCoilsRequest(1, 5, slave=0x12, transaction=0x12, protocol=0x12) + request = ReadCoilsRequest(1, 5, slave=18, transaction=0x12, protocol=0x12) assert request.slave_id == 0x12 assert request.transaction_id == 0x12 assert request.protocol_id == 0x12 request = ReadCoilsRequest(1, 5) - assert not request.slave_id + assert request.slave_id == 1 assert not request.transaction_id assert not request.protocol_id diff --git a/test/test_transaction.py b/test/test_transaction.py index 404e87fa6..e7cdc23f2 100755 --- a/test/test_transaction.py +++ b/test/test_transaction.py @@ -42,7 +42,7 @@ def setup_method(self): self._tls = ModbusTlsFramer(decoder=self.decoder, client=None) self._rtu = ModbusRtuFramer(decoder=self.decoder, client=None) self._ascii = ModbusAsciiFramer(decoder=self.decoder, client=None) - self._manager = SyncModbusTransactionManager(self.client, False, 3) + self._manager = SyncModbusTransactionManager(self.client, 3) # ----------------------------------------------------------------------- # # Modbus transaction manager @@ -112,12 +112,11 @@ def test_execute(self, mock_get_transaction, mock_recv): request.get_response_pdu_size.return_value = 10 request.slave_id = 1 request.function_code = 222 - trans = SyncModbusTransactionManager(client, False, 3) + trans = SyncModbusTransactionManager(client, 3) mock_recv.reset_mock( return_value=b"abcdef" ) assert trans.retries == 3 - assert not trans.retry_on_empty mock_get_transaction.return_value = b"response" response = trans.execute(request) @@ -132,7 +131,6 @@ def test_execute(self, mock_get_transaction, mock_recv): assert isinstance(response, ModbusIOException) # No response with retries - trans.retry_on_empty = True mock_recv.reset_mock( side_effect=iter([b"", b"abcdef"]) ) @@ -144,7 +142,6 @@ def test_execute(self, mock_get_transaction, mock_recv): side_effect=iter([b"abcdef", b"deadbe", b"123456"]) ) client.comm_params.handle_local_echo = True - trans.retry_on_empty = False assert trans.execute(request).message == "[Input/Output] Wrong local echo" client.comm_params.handle_local_echo = False @@ -165,14 +162,12 @@ def test_execute(self, mock_get_transaction, mock_recv): assert isinstance(trans.execute(request), ModbusIOException) # Broadcast - client.params.broadcast_enable = True request.slave_id = 0 response = trans.execute(request) assert response == b"Broadcast write sent - no response expected" # Broadcast w/ Local echo client.comm_params.handle_local_echo = True - client.params.broadcast_enable = True mock_recv.reset_mock(return_value=b"deadbeef") request.slave_id = 0 response = trans.execute(request) From 97681ec5c44803438017c04cf22786b0ad326910 Mon Sep 17 00:00:00 2001 From: jan iversen Date: Wed, 31 Jul 2024 21:50:21 +0200 Subject: [PATCH 87/88] Prepare v3.7.0. (#2211) --- API_changes.rst | 1 + AUTHORS.rst | 2 ++ CHANGELOG.rst | 41 ++++++++++++++++++++++++++++++++ MAKE_RELEASE.rst | 12 +++++----- README.rst | 5 +++- doc/index.rst | 1 + doc/source/_static/examples.tgz | Bin 42372 -> 43223 bytes doc/source/_static/examples.zip | Bin 38790 -> 38433 bytes pymodbus/__init__.py | 2 +- 9 files changed, 56 insertions(+), 8 deletions(-) diff --git a/API_changes.rst b/API_changes.rst index 50ecc7867..fef2c8cc0 100644 --- a/API_changes.rst +++ b/API_changes.rst @@ -16,6 +16,7 @@ API changes 3.7.0 - PDU classes moved to pymodbus/pdu - Simulator config custom actions kwargs -> parameters - Non defined parameters (kwargs) no longer valid +- Drop support for Python 3.8 (its no longer tested, but will probably work) API changes 3.6.0 diff --git a/AUTHORS.rst b/AUTHORS.rst index 6d015f1e9..78fad7682 100644 --- a/AUTHORS.rst +++ b/AUTHORS.rst @@ -33,6 +33,7 @@ Thanks to - Dominique Martinet - Dries - duc996 +- Esco441-91 - Farzad Panahi - Fredo70 - Gao Fang @@ -54,6 +55,7 @@ Thanks to - julian - Justin Standring - Kenny Johansson +- Martyy - Matthias Straka - laund - Logan Gunthorpe diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 2c3347d78..1cae14415 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -8,6 +8,47 @@ helps make pymodbus a better product. :ref:`Authors`: contains a complete list of volunteers have contributed to each major version. +Version 3.7.0 +------------- +* Remove unneeded client parameters. (#2272) +* simulator: Fix context single parameter (#2264) +* buildPacket can be used for Request and Response (#2262) +* More descriptive decoder exceptions (#2260) +* Cleanup ReadWriteMultipleRegistersResponse and testing (#2261) +* Feature/simulator addressing (#2258) +* Framer optimization (apart from RTU). (#2146) +* Use mock.patch.object to avoid protected access errors. (#2251) +* Fix some mypy type checking errors in test_transaction.py (#2250) +* Update check for windows platform (#2247) +* Logging 100% coverage. (#2248) +* CI, Block draft PRs to use CPU minutes. (#2245, #2246) +* Remove kwargs client. (#2243, #2244, #2257) +* remove kwargs PDU messagees. (#2240) +* Remove message_generator example (not part of API). (#2239) +* Update dev dependencies (#2241) +* Fix ruff check in CI (#2242) +* Remove kwargs. (#2236, #2237) +* Simulator config, kwargs -> parameters. (#2235) +* Refactor transaction handling to better separate async and sync code. (#2232) +* Simplify some BinaryPayload pack operations (#2224) +* Fix writing to serial (rs485) on windows os. (#2191) +* Remember to remove serial writer. (#2209) +* Transaction_id for serial == 0. (#2208) +* Solve pylint error. +* Sync TLS needs time before reading frame (#2186) +* Update transaction.py (#2174) +* PDU classes --> pymodbus/pdu. (#2160) +* Speed up no data detection. (#2150) +* RTU decode hunt part. (#2138) +* Dislodge client classes from modbusProtocol. (#2137) +* Merge new message layer and old framer directory. (#2135) +* Coverage == 91%. (#2132) +* Remove binary_framer. (#2130) +* on_reconnect_callback --> on_connect_callback. (#2122) +* Remove certfile,keyfile,password from TLS client. (#2121) +* Drop support for python 3.8 (#2112) + + Version 3.6.9 ------------- * Remove python 3.8 from CI diff --git a/MAKE_RELEASE.rst b/MAKE_RELEASE.rst index a919eb9d4..9bed9c862 100644 --- a/MAKE_RELEASE.rst +++ b/MAKE_RELEASE.rst @@ -8,14 +8,14 @@ Making a release. ------------------------------------------------------------ Prepare/make release on dev. ------------------------------------------------------------ -* Make pull request "prepare v3.6.x", with the following: +* Make pull request "prepare v3.7.x", with the following: * Update pymodbus/__init__.py with version number (__version__ X.Y.Zpre) * Update README.rst "Supported versions" * Control / Update API_changes.rst * Update CHANGELOG.rst * Add commits from last release, but selectively ! - git log --oneline v3.6.6..HEAD > commit.log - git log --pretty="%an" v3.6.6..HEAD | sort -uf > authors.log + git log --oneline v3.7.0..HEAD > commit.log + git log --pretty="%an" v3.7.0..HEAD | sort -uf > authors.log update AUTHORS.rst and CHANGELOG.rst cd doc; ./build_html * rm -rf build/* dist/* @@ -30,13 +30,13 @@ Prepare/make release on dev. * git branch -D master * wait for CI to complete on all branches * On github "prepare release" - * Create tag e.g. v3.4.0dev0 - * Title "pymodbus v3.4.0dev0" + * Create tag e.g. v3.7.0dev0 + * Title "pymodbus v3.7.0dev0" * do NOT generate release notes, but copy from CHANGELOG.rst * make release (remember to mark pre-release if so) * on local repo * git pull, check release tag is pulled - * git checkout v3.0.0dev0 + * git checkout v3.7.0dev0 * rm -rf build/* dist/* * python3 -m build * twine upload dist/* (upload to pypi) diff --git a/README.rst b/README.rst index f78889652..30a21ee60 100644 --- a/README.rst +++ b/README.rst @@ -11,10 +11,13 @@ PyModbus - A Python Modbus Stack Pymodbus is a full Modbus protocol implementation offering client/server with synchronous/asynchronous API a well as simulators. -Current release is `3.6.9 `_. +Current release is `3.7.0 `_. Bleeding edge (not released) is `dev `_. +Waiting for v3.8.0 (not released) is `wait3.8.0 `_. This contains +dev + merged pull requests that have API changes, and thus have to wait. + All changes are described in `release notes `_ and all API changes are `documented `_ diff --git a/doc/index.rst b/doc/index.rst index 8c226ea1a..433f78f4b 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -9,6 +9,7 @@ Please select a topic in the left hand column. :hidden: source/readme + source/api_changes source/client source/server source/repl diff --git a/doc/source/_static/examples.tgz b/doc/source/_static/examples.tgz index be0a609ef10a30d4b1d5da98ed7e5509a9c9d324..65294ff1e53b22de591aec1515db7f75ce6be235 100644 GIT binary patch literal 43223 zcmV)WK(4b1%MRD?r8xtY64a3 znk!eXysgYeKl&72PRCL9)q{PS&F0S5RzQEZ`ERq`<-h#B6STV9TkWmpR;$|#nyvQE zR_Cjr`EWzhpIMfNDGcRr;bd+(C^j0+dCyY`)Zc%U&%gUm|Ng6g{_3mm!$I)&DEM
#N|? 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

M2OGTuqiH)Y*dZ> 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 M2RkiO#sDq@0KJ6VhyVZp diff --git a/doc/source/_static/examples.zip b/doc/source/_static/examples.zip index 777f6fbec4055718a22123ee489d0ebd83fd411d..b853a45dc77339f4d76bc68460e99033b4675d6a 100644 GIT binary patch delta 21469 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 delta 21933 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 Date: Wed, 31 Jul 2024 22:43:27 +0200 Subject: [PATCH 88/88] Remove 3.6.9 monitor file. --- v3.6.9..dev | 20 -------------------- 1 file changed, 20 deletions(-) delete mode 100644 v3.6.9..dev diff --git a/v3.6.9..dev b/v3.6.9..dev deleted file mode 100644 index befbfe6df..000000000 --- a/v3.6.9..dev +++ /dev/null @@ -1,20 +0,0 @@ - -V3.7.0 (NOT INCLUDED in 3.6.7-8-9) -14ec87e3 Solve serial unrequested frame. (#2219) -5bddf9e8 Update third party versions. (#2216) -45c5116e Transaction_id for serial == 0. (#2208) -c91b2182 Sync TLS needs time before reading frame (#2186) -a48aeeb4 Update transaction.py (#2174) -e9c187e9 Merge 3.6.8 effects. -681fd2c8 Merge master v3.6.8 into dev -8a836b20 PDU classes --> pymodbus/pdu. (#2160) -e8063fa5 Merge 3.6.7 back into dev. (#2156) -a6b43dd7 Speed up no data detection. (#2150) -c4c14cab RTU decode hunt part. (#2138) -9e9e50e2 Dislodge client classes from modbusProtocol. (#2137) -9f736dfe Merge new message layer and old framer directory. (#2135) -331dc636 Coverage == 91%. (#2132) -86d5afe2 Remove binary_framer. (#2130) -0803ff70 on_reconnect_callback --> on_connect_callback. (#2122) -2c36fd3f Remove certfile,keyfile,password from TLS client. (#2121) -58a1c37d Drop support for python 3.8 (#2112)