diff --git a/doc/index.rst b/doc/index.rst index af14c6c653..4efeab5de2 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -12,6 +12,7 @@ Welcome to PyModbus's documentation! readme.rst changelog.rst + source/library/transport.rst source/library/client.rst source/library/server.rst source/library/simulator/simulator diff --git a/doc/source/library/client.rst b/doc/source/library/client.rst index 156aec77f6..b4ff6ea27a 100644 --- a/doc/source/library/client.rst +++ b/doc/source/library/client.rst @@ -7,12 +7,8 @@ Pymodbus offers clients with transport protocols for - *TCP* - *TLS* - *UDP* -- possibility to add a custom transport protocol -communication in 2 versions: - -- :mod:`synchronous client`, -- :mod:`asynchronous client` using asyncio. +communication can be either using a :mod:`synchronous client` or a :mod:`asynchronous client` using asyncio. Using pymodbus client to set/get information from a device (server) is done in a few simple steps, like the following synchronous example:: diff --git a/doc/source/library/transport.rst b/doc/source/library/transport.rst new file mode 100644 index 0000000000..b764e0dcb7 --- /dev/null +++ b/doc/source/library/transport.rst @@ -0,0 +1,10 @@ +Transport +========= + +Pymodbus uses a common transport layer, that handles reconnect, timeout etc. + + +.. autoclass:: pymodbus.transport.base.BaseTransport + :members: + :member-order: bysource + :show-inheritance: diff --git a/pymodbus/client/base.py b/pymodbus/client/base.py index 16abe2a038..e6b7a4f58b 100644 --- a/pymodbus/client/base.py +++ b/pymodbus/client/base.py @@ -43,23 +43,7 @@ class ModbusBaseClient(ModbusClientMixin): **reconnect_delay** to **reconnect_delay_max**. Set `reconnect_delay=0` to avoid automatic reconnection. - :mod:`ModbusBaseClient` is normally not referenced outside :mod:`pymodbus`, - unless you want to make a custom client. - - Custom client class **must** inherit :mod:`ModbusBaseClient`, example:: - - from pymodbus.client import ModbusBaseClient - - class myOwnClient(ModbusBaseClient): - - def __init__(self, **kwargs): - super().__init__(kwargs) - - def run(): - client = myOwnClient(...) - client.connect() - rr = client.read_coils(0x01) - client.close() + :mod:`ModbusBaseClient` is normally not referenced outside :mod:`pymodbus`. **Application methods, common to all clients**: """ diff --git a/pymodbus/client/tls.py b/pymodbus/client/tls.py index b7c684c850..e2adbcb79f 100644 --- a/pymodbus/client/tls.py +++ b/pymodbus/client/tls.py @@ -54,6 +54,9 @@ class AsyncModbusTlsClient(AsyncModbusTcpClient, asyncio.Protocol): :param server_hostname: (optional) Bind certificate to host :param kwargs: (optional) Experimental parameters + ..tip:: + See ModbusBaseClient for common parameters. + Example:: from pymodbus.client import AsyncModbusTlsClient @@ -122,6 +125,9 @@ class ModbusTlsClient(ModbusTcpClient): :param server_hostname: (optional) Bind certificate to host :param kwargs: (optional) Experimental parameters + ..tip:: + See ModbusBaseClient for common parameters. + Example:: from pymodbus.client import ModbusTlsClient diff --git a/pymodbus/client/udp.py b/pymodbus/client/udp.py index 2abdc475e8..5cef1ee868 100644 --- a/pymodbus/client/udp.py +++ b/pymodbus/client/udp.py @@ -25,6 +25,9 @@ class AsyncModbusUdpClient( :param source_address: (optional) source address of client, :param kwargs: (optional) Experimental parameters + ..tip:: + See ModbusBaseClient for common parameters. + Example:: from pymodbus.client import AsyncModbusUdpClient @@ -164,6 +167,9 @@ class ModbusUdpClient(ModbusBaseClient): :param source_address: (optional) source address of client, :param kwargs: (optional) Experimental parameters + ..tip:: + See ModbusBaseClient for common parameters. + Example:: from pymodbus.client import ModbusUdpClient diff --git a/pymodbus/transport/__init__.py b/pymodbus/transport/__init__.py new file mode 100644 index 0000000000..d96b477712 --- /dev/null +++ b/pymodbus/transport/__init__.py @@ -0,0 +1 @@ +"""Transport.""" diff --git a/pymodbus/transport/base.py b/pymodbus/transport/base.py new file mode 100644 index 0000000000..269a64b11c --- /dev/null +++ b/pymodbus/transport/base.py @@ -0,0 +1,140 @@ +"""Base for all transport types.""" +from __future__ import annotations + +from abc import abstractmethod +from typing import Any + +from pymodbus.framer import ModbusFramer +from pymodbus.logging import Log + + +class BaseTransport: + """Base class for transport types. + + :param framer: framer used to encode/decode data + :param slaves: list of acceptable slaves (0 for accept all) + :param comm_name: name of this transport connection + :param reconnect_delay: delay in milliseconds for first reconnect (0 for no reconnect) + :param reconnect_delay_max: max delay in milliseconds for next reconnect, resets to reconnect_delay + :param retries_send: number of times to retry a send operation + :param retry_on_empty: retry read on nothing + :param timeout_connect: Max. time in milliseconds for connect to complete + :param timeout_comm: Max. time in milliseconds for recv/send to complete + + :property reconnect_delay_current: current delay in milliseconds for next reconnect (doubles with every try) + :property transport: current transport class (none if not connected) + + BaseTransport contains functions common to all transport types and client/server. + + This class is not available in the pymodbus API, and should not be referenced in Applications. + """ + + def __init__( + self, + framer: ModbusFramer, + slaves: list[int], + comm_name: str = "NO NAME", + reconnect_delay: int = 0, + reconnect_delay_max: int = 0, + retries_send: int = 0, + retry_on_empty: bool = False, + timeout_connect: int = 10, + timeout_comm: int = 5, + ) -> None: + """Initialize a transport instance.""" + # parameter variables + self.framer = framer + self.slaves = slaves + self.comm_name = comm_name + self.reconnect_delay = reconnect_delay + self.reconnect_delay_max = reconnect_delay_max + self.retries_send = retries_send + self.retry_on_empty = retry_on_empty + self.timeout_connect = timeout_connect + self.timeout_comm = timeout_comm + + # local variables + self.reconnect_delay_current: int = 0 + self.transport: Any = None + + # -------------------------- # + # Transport external methods # + # -------------------------- # + def connection_made(self, transport): + """Call when a connection is made. + + The transport argument is the transport representing the connection. + """ + self.transport = transport + Log.debug("Connected on transport {}", transport) + self.cb_connection_made() + + def connection_lost(self, reason): + """Call when the connection is lost or closed. + + The argument is either an exception object or None + """ + self.transport = None + if reason: + Log.debug( + "Connection lost due to {} on transport {}", reason, self.transport + ) + self.cb_connection_lost(reason) + + def data_received(self, data): + """Call when some data is received. + + data is a non-empty bytes object containing the incoming data. + """ + Log.debug("recv: {}", data, ":hex") + # self.framer.processIncomingPacket(data, self._handle_response, unit=0) + + def send(self, request: bytes) -> bool: + """Send request.""" + return self.cb_send(request) + + def close(self) -> None: + """Close the underlying socket connection (call **sync/async**).""" + # raise NotImplementedException + + # -------------------------- # + # Transport callback methods # + # -------------------------- # + @abstractmethod + def cb_connection_made(self) -> bool: + """Handle low level.""" + + @abstractmethod + def cb_connection_lost(self, _reason) -> bool: + """Handle low level.""" + + @abstractmethod + def cb_send(self, _request) -> bool: + """Handle low level.""" + + @abstractmethod + def cb_close(self) -> bool: + """Handle low level.""" + + # ----------------------------------------------------------------------- # + # The magic methods + # ----------------------------------------------------------------------- # + def __enter__(self) -> BaseTransport: + """Implement the client with enter block.""" + return self + + async def __aenter__(self): + """Implement the client with enter block.""" + return self + + def __exit__(self, _class, _value, _traceback) -> None: + """Implement the client with exit block.""" + self.close() + + async def __aexit__(self, _class, _value, _traceback) -> None: + """Implement the client with exit block.""" + self.close() + + def __str__(self) -> str: + """Build a string representation of the connection.""" + return f"{self.__class__.__name__}({self.comm_name})" diff --git a/test/test_transport.py b/test/test_transport.py new file mode 100644 index 0000000000..f034db4bbc --- /dev/null +++ b/test/test_transport.py @@ -0,0 +1,16 @@ +"""Test transport.""" +# import pytest + +# from pymodbus.transport.base import BaseTransport + + +class TestTransport: # pylint: disable=too-few-public-methods + """Unittest for the transport module.""" + + def test_base_properties(self): + """Test properties.""" + # transport = BaseTransport(None, "test", None) + # assert not transport.ps_close_comm_on_error + + # with pytest.raises(RuntimeError): + # transport.connection_made(None)