From e6421238f1ea0e15f2d8e0db5321a65799dcd871 Mon Sep 17 00:00:00 2001 From: jan Iversen Date: Fri, 5 Aug 2022 13:11:58 +0200 Subject: [PATCH] Rectify sync/async client parameters. --- doc/source/library/pymodbus.client.rst | 83 ++--- examples/client_async.py | 107 ++++--- examples/client_sync.py | 94 +++--- .../asynchronous_asyncio_modbus_tls_client.py | 4 +- pymodbus/client/__init__.py | 12 +- pymodbus/client/async_serial.py | 99 ++++-- pymodbus/client/async_tcp.py | 85 +++-- pymodbus/client/async_tls.py | 141 ++++++-- pymodbus/client/async_udp.py | 115 +++++-- pymodbus/client/helper_sync.py | 158 ++++++++- pymodbus/client/sync_serial.py | 302 ++++++------------ pymodbus/client/sync_tcp.py | 274 +++++----------- pymodbus/client/sync_tls.py | 131 +++++--- pymodbus/client/sync_udp.py | 281 +++++----------- pymodbus/version.py | 2 +- test/test_client_async.py | 30 +- test/test_client_async_asyncio.py | 46 +-- test/test_client_sync.py | 86 ++--- 18 files changed, 1091 insertions(+), 959 deletions(-) diff --git a/doc/source/library/pymodbus.client.rst b/doc/source/library/pymodbus.client.rst index 780356999..b3bbd5097 100644 --- a/doc/source/library/pymodbus.client.rst +++ b/doc/source/library/pymodbus.client.rst @@ -1,82 +1,55 @@ -pymodbus\.client package -======================== +pymodbus\.client +================ -Pymodbus offers a :mod:`synchronous client `, and async clients based on :mod:`asyncio `. +Pymodbus offers a :mod:`synchronous client ` and a :mod:`client based on asyncio `. -Each client shares a :mod:`client mixin ` which offers simple methods for reading and writing. +In general each transports (Serial, TCP, TLS and UDP) have its own class. +However the actual implementation is highly shared. -Submodules ----------- +AsyncModbusSerialClient class +----------------------------- -pymodbus\.client\.helper_sync module ------------------------------------- - -.. automodule:: pymodbus.client.helper_sync +.. automodule:: pymodbus.client.async_serial :members: - :undoc-members: - :show-inheritance: -pymodbus\.client\.ModbusSerialClient module -------------------------------------------- +ModbusSerialClient class +------------------------ .. automodule:: pymodbus.client.sync_serial :members: - :undoc-members: - :show-inheritance: - -pymodbus\.client\.ModbusTCPClient module ----------------------------------------- - -.. automodule:: pymodbus.client.sync_tcp - :members: - :undoc-members: - :show-inheritance: -pymodbus\.client\.ModbusTLSClient module ----------------------------------------- +AsyncModbusTcpClient class +-------------------------- -.. automodule:: pymodbus.client.sync_tls +.. automodule:: pymodbus.client.async_tcp :members: - :undoc-members: - :show-inheritance: -pymodbus\.client\.ModbusUDPClient module ----------------------------------------- +ModbusTcpClient class +--------------------- -.. automodule:: pymodbus.client.sync_udp +.. automodule:: pymodbus.client.sync_tcp :members: - :undoc-members: - :show-inheritance: -pymodbus\.client\.AsyncModbusUDPClient module ---------------------------------------------- +AsyncModbusTlsClient class +-------------------------- -.. automodule:: pymodbus.client.async_udp +.. automodule:: pymodbus.client.async_tls :members: - :undoc-members: - :show-inheritance: -pymodbus\.client\.AsyncModbusTLSClient module ---------------------------------------------- +ModbusTlsClient class +--------------------- -.. automodule:: pymodbus.client.async_tls +.. automodule:: pymodbus.client.sync_tls :members: - :undoc-members: - :show-inheritance: -pymodbus\.client\.AsyncModbusSerialClient module ------------------------------------------------- +AsyncModbusUdpClient class +-------------------------- -.. automodule:: pymodbus.client.async_serial +.. automodule:: pymodbus.client.async_udp :members: - :undoc-members: - :show-inheritance: - -pymodbus\.client\.AsyncModbusTCPClient module ---------------------------------------------- +ModbusUdpClient class +--------------------- -.. automodule:: pymodbus.client.async_tcp +.. automodule:: pymodbus.client.sync_udp :members: - :undoc-members: - :show-inheritance: diff --git a/examples/client_async.py b/examples/client_async.py index ffd76aaef..cbbc35763 100755 --- a/examples/client_async.py +++ b/examples/client_async.py @@ -29,10 +29,10 @@ # import the various client implementations # --------------------------------------------------------------------------- # from pymodbus.client import ( - AsyncModbusUDPClient, - AsyncModbusTLSClient, AsyncModbusSerialClient, - AsyncModbusTCPClient, + AsyncModbusTcpClient, + AsyncModbusTlsClient, + AsyncModbusUdpClient, ) from pymodbus.transaction import ( ModbusAsciiFramer, @@ -49,54 +49,75 @@ async def setup_async_client(): _logger.info("### Create client object") if args.comm == "tcp": - client = AsyncModbusTCPClient( - "127.0.0.1", # define tcp address where to connect to. + client = AsyncModbusTcpClient( + "127.0.0.1", port=args.port, # on which port - framer=ModbusSocketFramer, # how to interpret the messages - timeout=1, # waiting time for request to complete - retries=3, # retries per transaction - retry_on_empty=False, # Is an empty response a retry - source_address=("localhost", 0), # bind socket to address - strict=True, # use strict timing, t1.5 for Modbus RTU + # Common optional paramers: + # protocol_class=ModbusClientProtocol, + # modbus_decoder=ClientDecoder, + framer=args.framer, + # timeout=10, + # retries=3, + # retry_on_empty=False, + # close_comm_on_error=False, + # strict=True, + # TCP setup parameters + # source_address=("localhost", 0), ) elif args.comm == "udp": - client = AsyncModbusUDPClient( - "localhost", # define tcp address where to connect to. - port=args.port, # on which port - framer=args.framer, # how to interpret the messages - timeout=1, # waiting time for request to complete - retries=3, # retries per transaction - retry_on_empty=False, # Is an empty response a retry - source_address=("localhost", 0), # bind socket to address - strict=True, # use strict timing, t1.5 for Modbus RTU + client = AsyncModbusUdpClient( + "localhost", + # port=502, + # Common optional paramers: + # protocol_class=ModbusClientProtocol, + # modbus_decoder=ClientDecoder, + framer=args.framer, + # timeout=10, + # retries=3, + # retry_on_empty=False, + # close_comm_on_error=False, + # strict=True, + # UDP setup parameters + # source_address=None, ) elif args.comm == "serial": client = AsyncModbusSerialClient( - args.port, # serial port - framer=args.framer, # how to interpret the messages - stopbits=1, # The number of stop bits to use - bytesize=7, # The bytesize of the serial messages - parity="even", # Which kind of parity to use - baudrate=9600, # The baud rate to use for the serial device - handle_local_echo=False, # Handle local echo of the USB-to-RS485 adaptor - timeout=1, # waiting time for request to complete - strict=True, # use strict timing, t1.5 for Modbus RTU + args.port, + # Common optional paramers: + # protocol_class=ModbusClientProtocol, + # modbus_decoder=ClientDecoder, + # framer=ModbusRtuFramer, + # timeout=10, + # retries=3, + # retry_on_empty=False, + # close_comm_on_error=False, + # strict=True, + # Serial setup parameters + # baudrate=9600, + # bytesize=8, + # parity="N", + # stopbits=1, + # handle_local_echo=False, ) elif args.comm == "tls": - client = AsyncModbusTLSClient( - host="localhost", # define tcp address where to connect to. - port=args.port, # on which port - sslctx=None, # ssl control - certfile=None, # certificate file - keyfile=None, # key file - password=None, # pass phrase - framer=args.framer, # how to interpret the messages - timeout=1, # waiting time for request to complete - retries=3, # retries per transaction - retry_on_empty=False, # Is an empty response a retry - source_address=("localhost", 0), # bind socket to address - server_hostname="localhost", # used for cert verification - strict=True, # use strict timing, t1.5 for Modbus RTU + client = AsyncModbusTlsClient( + "localhost", + port=args.port, + # Common optional paramers: + # protocol_class=None, + # modbus_decoder=ClientDecoder, + framer=args.framer, + # timeout=10, + # retries=3, + # retry_on_empty=False, + # close_comm_on_error=False, + # strict=True, + # TLS setup parameters + # sslctx=None, + # certfile=None, + # keyfile=None, + # password=None, + # server_hostname="localhost", ) await client.start() return client diff --git a/examples/client_sync.py b/examples/client_sync.py index b6de1dfbf..0ca339ef4 100755 --- a/examples/client_sync.py +++ b/examples/client_sync.py @@ -48,52 +48,74 @@ def setup_sync_client(): _logger.info("### Create client object") if args.comm == "tcp": client = ModbusTcpClient( - host="127.0.0.1", # define tcp address where to connect to. - port=args.port, # on which port - framer=args.framer, # how to interpret the messages - timeout=1, # waiting time for request to complete - retries=3, # retries per transaction - retry_on_empty=False, # Is an empty response a retry - source_address=("localhost", 0), # bind socket to address - strict=True, # use strict timing, t1.5 for Modbus RTU + "127.0.0.1", + port=args.port, + # Common optional paramers: + # protocol_class=None, + # modbus_decoder=ClientDecoder, + framer=args.framer, + # timeout=10, + # retries=3, + # retry_on_empty=False,y + # close_comm_on_error=False, + # strict=True, + # TCP setup parameters + # source_address=("localhost", 0), ) elif args.comm == "udp": client = ModbusUdpClient( - host="localhost", # define tcp address where to connect to. - port=args.port, # on which port - framer=args.framer, # how to interpret the messages - timeout=1, # waiting time for request to complete - retries=3, # retries per transaction - retry_on_empty=False, # Is an empty response a retry - source_address=("localhost", 0), # bind socket to address - strict=True, # use strict timing, t1.5 for Modbus RTU + "localhost", + # port=502, + # Common optional paramers: + # protocol_class=ModbusClientProtocol, + # modbus_decoder=ClientDecoder, + framer=args.framer, + # timeout=10, + # retries=3, + # retry_on_empty=False, + # close_comm_on_error=False, + # strict=True, + # UDP setup parameters + # source_address=None, ) elif args.comm == "serial": client = ModbusSerialClient( port=args.port, # serial port - framer=args.framer, # how to interpret the messages - stopbits=1, # The number of stop bits to use - bytesize=7, # The bytesize of the serial messages - parity="E", # Which kind of parity to use - baudrate=9600, # The baud rate to use for the serial device - handle_local_echo=False, # Handle local echo of the USB-to-RS485 adaptor - timeout=1, # waiting time for request to complete - strict=True, # use strict timing, t1.5 for Modbus RTU + # Common optional paramers: + # protocol_class=ModbusClientProtocol, + # modbus_decoder=ClientDecoder, + # framer=ModbusRtuFramer, + # timeout=10, + # retries=3, + # retry_on_empty=False, + # close_comm_on_error=False,. + # strict=True, + # Serial setup parameters + # baudrate=9600, + # bytesize=8, + # parity="N", + # stopbits=1, + # handle_local_echo=False, ) elif args.comm == "tls": client = ModbusTlsClient( - host="localhost", # define tcp address where to connect to. - port=args.port, # on which port - sslctx=None, # ssl control - certfile=None, # certificate file - keyfile=None, # key file - password=None, # pass phrase - framer=args.framer, # how to interpret the messages - timeout=1, # waiting time for request to complete - retries=3, # retries per transaction - retry_on_empty=False, # Is an empty response a retry - source_address=("localhost", 0), # bind socket to address - strict=True, # use strict timing, t1.5 for Modbus RTU + "localhost", + port=args.port, + # Common optional paramers: + # protocol_class=None, + # modbus_decoder=ClientDecoder, + framer=args.framer, + # timeout=10, + # retries=3, + # retry_on_empty=False, + # close_comm_on_error=False, + # strict=True, + # TLS setup parameters + # sslctx=None, + # certfile=None, + # keyfile=None, + # password=None, + # server_hostname="localhost", ) return client, args.comm != "udp" diff --git a/examples/contrib/asynchronous_asyncio_modbus_tls_client.py b/examples/contrib/asynchronous_asyncio_modbus_tls_client.py index 554125868..ba8f127c3 100755 --- a/examples/contrib/asynchronous_asyncio_modbus_tls_client.py +++ b/examples/contrib/asynchronous_asyncio_modbus_tls_client.py @@ -11,7 +11,7 @@ import ssl import asyncio -from pymodbus.client import AsyncModbusTLSClient +from pymodbus.client import AsyncModbusTlsClient # -------------------------------------------------------------------------- # # the TLS detail security can be set in SSLContext which is the context here @@ -38,7 +38,7 @@ async def start_async_test(client): # ----------------------------------------------------------------------- # # pass SSLContext which is the context here to ModbusTcpClient() # ----------------------------------------------------------------------- # - new_client = AsyncModbusTLSClient( # pylint: disable=unpacking-non-sequence + new_client = AsyncModbusTlsClient( # pylint: disable=unpacking-non-sequence "test.host.com", 8020, sslctx=sslctx, diff --git a/pymodbus/client/__init__.py b/pymodbus/client/__init__.py index 7894f75b6..58687eba2 100644 --- a/pymodbus/client/__init__.py +++ b/pymodbus/client/__init__.py @@ -2,10 +2,10 @@ import external classes, to make them easier to use: """ -from pymodbus.client.async_udp import AsyncModbusUDPClient -from pymodbus.client.async_tls import AsyncModbusTLSClient from pymodbus.client.async_serial import AsyncModbusSerialClient -from pymodbus.client.async_tcp import AsyncModbusTCPClient +from pymodbus.client.async_tcp import AsyncModbusTcpClient +from pymodbus.client.async_tls import AsyncModbusTlsClient +from pymodbus.client.async_udp import AsyncModbusUdpClient from pymodbus.client.sync_serial import ModbusSerialClient from pymodbus.client.sync_tcp import ModbusTcpClient from pymodbus.client.sync_tls import ModbusTlsClient @@ -15,10 +15,10 @@ # Exported symbols # ---------------------------------------------------------------------------# __all__ = [ - "AsyncModbusUDPClient", - "AsyncModbusTLSClient", "AsyncModbusSerialClient", - "AsyncModbusTCPClient", + "AsyncModbusTcpClient", + "AsyncModbusTlsClient", + "AsyncModbusUdpClient", "ModbusSerialClient", "ModbusTcpClient", "ModbusTlsClient", diff --git a/pymodbus/client/async_serial.py b/pymodbus/client/async_serial.py index e533db74c..7226479d4 100644 --- a/pymodbus/client/async_serial.py +++ b/pymodbus/client/async_serial.py @@ -1,4 +1,35 @@ -"""SERIAL communication.""" +"""Modbus client async serial communication. + +The serial communication is RS-485 based, and usually used vith a usb rs-485 dongle. + +Example:: + + from pymodbus.client import AsyncModbusSerialClient + + async def run(): + client = AsyncModbusSerialClient( + "dev/pty0", # serial port + # Common optional paramers: + # protocol_class=ModbusClientProtocol, + # modbus_decoder=ClientDecoder, + # framer=ModbusRtuFramer, + # timeout=10, + # retries=3, + # retry_on_empty=False, + # close_comm_on_error=False, + # strict=True, + # Serial setup parameters + # baudrate=9600, + # bytesize=8, + # parity="N", + # stopbits=1, + # handle_local_echo=False, + ) + + await client.start() + ... + await client.stop() +""" import asyncio import logging @@ -12,55 +43,69 @@ class AsyncModbusSerialClient: # pylint: disable=too-many-instance-attributes - """Actual Async Serial Client to be used. - - To use do:: - from pymodbus.client import AsyncModbusSerialClient + r"""Modbus client for async serial (RS-485) communication. + + :param port: (positional) Serial port used for communication. + :param protocol_class: (optional, default ModbusClientProtocol) Protocol communication class. + :param modbus_decoder: (optional, default ClientDecoder) Message decoder class. + :param framer: (optional, default ModbusRtuFramer) Framer class. + :param timeout: (optional, default 3s) Timeout for a request. + :param retries: (optional, default 3) Max number of retries pr request. + :param retry_on_empty: (optional, default false) Retry on empty response. + :param close_comm_on_error: (optional, default true) Close connection on error. + :param strict: (optional, default true) Strict timing, 1.5 character between requests. + :param baudrate: (optional, default 9600) Bits pr second. + :param bytesize: (optional, default 8) Number of bits pr byte 7-8. + :param parity: (optional, default None). + :param stopbits: (optional, default 1) Number of stop bits 0-2 to use. + :param handle_local_echo: (optional, default false) Handle local echo of the USB-to-RS485 dongle. + :param \*\*kwargs: (optional) Extra experimental parameters for transport + :return: client object """ transport = None framer = None - def __init__( + def __init__( # pylint: disable=too-many-arguments + # Positional parameters self, port, + # Common optional paramers: protocol_class=ModbusClientProtocol, modbus_decoder=ClientDecoder, framer=ModbusRtuFramer, timeout=10, + retries=3, + retry_on_empty=False, + close_comm_on_error=False, + strict=True, + # Serial setup parameters baudrate=9600, bytesize=8, parity="N", stopbits=1, + handle_local_echo=False, + # Extra parameters for serial_async (experimental) **kwargs, ): - """Initialize Asyncio Modbus Serial Client. - - :param port: The serial port to attach to - :param protocol_class: Protocol used to talk to modbus device. - :param modbus_decoder: Message decoder. - :param framer: Modbus framer - :param timeout: The timeout between serial requests (default 3s) - - :param baudrate: The baud rate to use for the serial device - :param bytesize: The bytesize of the serial messages - :param parity: Which kind of parity to use - :param stopbits: The number of stop bits to use - - :param **kwargs: extra parameters for serial_async (experimental) - :return: client object - """ + """Initialize Asyncio Modbus Serial Client.""" self.port = port self.protocol_class = protocol_class self.framer = framer(modbus_decoder()) self.timeout = timeout + self.retries = retries + self.retry_on_empty = retry_on_empty + self.close_comm_on_error = close_comm_on_error + self.strict = strict self.baudrate = baudrate self.bytesize = bytesize self.parity = parity self.stopbits = stopbits + self.handle_local_echo = handle_local_echo + self.kwargs = kwargs self.loop = None @@ -111,7 +156,10 @@ async def connect(self): _logger.warning(txt) def protocol_made_connection(self, protocol): - """Notify successful connection.""" + """Notify successful connection. + + :meta private: + """ _logger.info("Serial connected.") if not self._connected: self._connected_event.set() @@ -120,7 +168,10 @@ def protocol_made_connection(self, protocol): _logger.error("Factory protocol connect callback called while connected.") def protocol_lost_connection(self, protocol): - """Notify lost connection.""" + """Notify lost connection. + + :meta private: + """ if self._connected: _logger.info("Serial lost connection.") if protocol is not self.protocol: diff --git a/pymodbus/client/async_tcp.py b/pymodbus/client/async_tcp.py index 59e030f1c..46b39135f 100644 --- a/pymodbus/client/async_tcp.py +++ b/pymodbus/client/async_tcp.py @@ -1,4 +1,30 @@ -"""TCP communication.""" +"""Modbus client async TCP communication. + +Example:: + + from pymodbus.client import AsyncModbusTcpClient + + async def run(): + client = AsyncModbusTcpClient( + "127.0.0.1", + # Common optional paramers: + # port=502, + # protocol_class=ModbusClientProtocol, + # modbus_decoder=ClientDecoder, + # framer=ModbusSocketFramer, + # timeout=10, + # retries=3, + # retry_on_empty=False, + # close_comm_on_error=False, + # strict=True, + # TCP setup parameters + # source_address=("localhost", 0), + ) + + await client.start() + ... + await client.stop() +""" import asyncio import logging @@ -9,11 +35,22 @@ _logger = logging.getLogger(__name__) -class AsyncModbusTCPClient: - """Actual Async Serial Client to be used. - - To use do:: - from pymodbus.client import AsyncModbusTCPClient +class AsyncModbusTcpClient: # pylint: disable=too-many-instance-attributes + r"""Modbus client for async TCP communication. + + :param host: (positional) Host IP address + :param port: (optional default 502) The serial port used for communication. + :param protocol_class: (optional, default ModbusClientProtocol) Protocol communication class. + :param modbus_decoder: (optional, default ClientDecoder) Message decoder class. + :param framer: (optional, default ModbusSocketFramer) Framer class. + :param timeout: (optional, default 3s) Timeout for a request. + :param retries: (optional, default 3) Max number of retries pr request. + :param retry_on_empty: (optional, default false) Retry on empty response. + :param close_comm_on_error: (optional, default true) Close connection on error. + :param strict: (optional, default true) Strict timing, 1.5 character between requests. + :param source_address: (optional, default none) source address of client, + :param \*\*kwargs: (optional) Extra experimental parameters for transport + :return: client object """ #: Minimum delay in milli seconds before reconnect is attempted. @@ -21,46 +58,44 @@ class AsyncModbusTCPClient: #: Maximum delay in milli seconds before reconnect is attempted. DELAY_MAX_MS = 1000 * 60 * 5 - def __init__( + def __init__( # pylint: disable=too-many-arguments + # Fixed parameters self, host, port=502, + # Common optional paramers: protocol_class=ModbusClientProtocol, modbus_decoder=ClientDecoder, framer=ModbusSocketFramer, timeout=10, + retries=3, + retry_on_empty=False, + close_comm_on_error=False, + strict=True, + # TCP setup parameters - source_address="127.0.0.1", - # Extra parameters for serial_async (experimental) + source_address=None, + + # Extra parameters for transport (experimental) **kwargs, ): - """Initialize AsyncioModbusTCPClient. - - :param host: Host IP address - :param port: The serial port to attach to - :param protocol_class: Protocol used to talk to modbus device. - :param modbus_decoder: Message decoder. - :param framer: Modbus framer - :param timeout: The timeout between serial requests (default 3s) - - :param source_address: source address specific to underlying backend - - :param kwargs: Other extra args specific to Backend being used - :return: client object - """ + """Initialize Asyncio Modbus TCP Client.""" self.host = host self.port = port self.protocol_class = protocol_class self.framer = framer(modbus_decoder()) self.timeout = timeout - + self.retries = retries + self.retry_on_empty = retry_on_empty + self.close_comm_on_error = close_comm_on_error + self.strict = strict self.source_address = source_address + self.kwargs = kwargs self.loop = None self.protocol = None self.connected = False self.delay_ms = self.DELAY_MIN_MS - self.kwargs = kwargs def reset_delay(self): """Reset wait before next reconnect to minimal period.""" diff --git a/pymodbus/client/async_tls.py b/pymodbus/client/async_tls.py index c92c52c60..4efbfb891 100644 --- a/pymodbus/client/async_tls.py +++ b/pymodbus/client/async_tls.py @@ -1,56 +1,129 @@ -"""TLS communication.""" +"""Modbus client async TLS communication. + +Example:: + + from pymodbus.client import AsyncModbusTlsClient + + async def run(): + client = AsyncModbusTlsClient( + "localhost", + # port=802, + # Common optional paramers: + # protocol_class=None, + # modbus_decoder=ClientDecoder, + # framer=ModbusTLsFramer, + # timeout=10, + # retries=3, + # retry_on_empty=False, + # close_comm_on_error=False, + # strict=True, + # TLS setup parameters + # sslctx=None, + # certfile=None, + # keyfile=None, + # password=None, + # server_hostname="localhost", + + await client.start() + ... + await client.stop() +""" import asyncio import logging import ssl -from pymodbus.client.async_tcp import AsyncModbusTCPClient +from pymodbus.factory import ClientDecoder +from pymodbus.client.async_tcp import AsyncModbusTcpClient from pymodbus.transaction import ModbusTlsFramer, FifoTransactionManager +from pymodbus.client.helper_tls import sslctx_provider _logger = logging.getLogger(__name__) -class AsyncModbusTLSClient(AsyncModbusTCPClient): - """Actual Async TLS Client to be used. +class AsyncModbusTlsClient(AsyncModbusTcpClient): # pylint: disable=too-many-instance-attributes + r"""Modbus client for async TLS communication. - To use do:: - from pymodbus.client import AsyncModbusTLSClient + :param host: (positional) Host IP address + :param port: (optional default 502) The serial port used for communication. + :param protocol_class: (optional, default ModbusClientProtocol) Protocol communication class. + :param modbus_decoder: (optional, default ClientDecoder) Message decoder class. + :param framer: (optional, default ModbusSocketFramer) Framer class. + :param timeout: (optional, default 3s) Timeout for a request. + :param retries: (optional, default 3) Max number of retries pr request. + :param retry_on_empty: (optional, default false) Retry on empty response. + :param close_comm_on_error: (optional, default true) Close connection on error. + :param strict: (optional, default true) Strict timing, 1.5 character between requests. + :param source_address: (optional, default none) Source address of client, + :param sslctx: (optional, default none) SSLContext to use for TLS (default None and auto create) + :param certfile: (optional, default none) Cert file path for TLS server request + :param keyfile: (optional, default none) Key file path for TLS server request + :param password: (optional, default none) Password for for decrypting client"s private key file + :param server_hostname: (optional, default none) Bind certificate to host, + :param \*\*kwargs: (optional) Extra experimental parameters for transport + :return: client object """ - def __init__( + def __init__( # pylint: disable=too-many-arguments + # Fixed parameters self, host, port=802, + # Common optional paramers: + protocol_class=None, + modbus_decoder=ClientDecoder, + framer=ModbusTlsFramer, + timeout=10, + retries=3, + retry_on_empty=False, + close_comm_on_error=False, + strict=True, + # TLS setup parameters - sslctx=None, # ssl control - certfile=None, # certificate file - keyfile=None, # key file - password=None, # pass phrase - server_hostname="localhost", # used for cert verification - # Extra parameters for tcp - **kwargs + sslctx=None, + certfile=None, + keyfile=None, + password=None, + server_hostname=None, + + # Extra parameters for transport (experimental) + **kwargs, ): - """Initialize AsyncModbusTLSClient. - - :param host: Host IP address - :param port: The serial port to attach to - :param protocol_class: Protocol used to talk to modbus device. - :param modbus_decoder: Message decoder. - :param framer: Modbus framer - :param timeout: The timeout between serial requests (default 3s) - - :param source_address: source address specific to underlying backend - :param sslctx: The SSLContext to use for TLS (default None and auto create) - :param certfile: The optional client"s cert file path for TLS server request - :param keyfile: The optional client"s key file path for TLS server request - :param password: The password for for decrypting client"s private key file - :param server_hostname: originating host. - - :param kwargs: Other extra args specific to Backend being used + r"""Modbus async TLS client. + + :param host: (positional) Host IP address + :param port: (optional default 802) The serial port used for communication. + :param protocol_class: (optional, default ModbusClientProtocol) Protocol communication class. + :param modbus_decoder: (optional, default ClientDecoder) Message decoder class. + :param framer: (optional, default ModbusSocketFramer) Framer class. + :param timeout: (optional, default 3s) Timeout for a request. + :param retries: (optional, default 3) Max number of retries pr request. + :param retry_on_empty: (optional, default false) Retry on empty response. + :param close_comm_on_error: (optional, default true) Close connection on error. + :param strict: (optional, default true) Strict timing, 1.5 character between requests. + :param source_address: (optional, default none) Source address of client, + :param sslctx: (optional, default none) SSLContext to use for TLS (default None and auto create) + :param certfile: (optional, default none) Cert file path for TLS server request + :param keyfile: (optional, default none) Key file path for TLS server request + :param password: (optional, default none) Password for for decrypting client"s private key file + :param server_hostname: (optional, default none) Bind certificate to host, + :param \*\*kwargs: (optional) Extra experimental parameters for transport :return: client object """ self.host = host self.port = port - framer = kwargs.pop("framer", ModbusTlsFramer) + self.protocol_class = protocol_class + self.framer = framer(modbus_decoder()) + self.timeout = timeout + self.retries = retries + self.retry_on_empty = retry_on_empty + self.close_comm_on_error = close_comm_on_error + self.strict = strict + self.sslctx = sslctx_provider(sslctx, certfile, keyfile, password) + self.certfile = certfile + self.keyfile = keyfile + self.password = password + self.server_hostname = server_hostname + self.kwargs = kwargs if not sslctx: self.sslctx = ssl.create_default_context() @@ -66,13 +139,13 @@ def __init__( self.keyfile = keyfile self.password = password self.server_hostname = server_hostname - AsyncModbusTCPClient.__init__(self, host, port=self.port, framer=framer, **kwargs) + AsyncModbusTcpClient.__init__(self, host, port=self.port, framer=framer, **kwargs) async def start(self): """Initiate connection to start client.""" # get current loop, if there are no loop a RuntimeError will be raised self.loop = asyncio.get_running_loop() - return await AsyncModbusTCPClient.start(self) + return await AsyncModbusTcpClient.start(self) async def _connect(self): _logger.debug("Connecting.") diff --git a/pymodbus/client/async_udp.py b/pymodbus/client/async_udp.py index feb928ca9..7c76d3d2a 100644 --- a/pymodbus/client/async_udp.py +++ b/pymodbus/client/async_udp.py @@ -1,26 +1,99 @@ -"""UDP communication.""" +"""Modbus client async UDP communication. + +Example:: + + from pymodbus.client import AsyncModbusUdpClient + + async def run(): + client = AsyncModbusUdpClient( + "127.0.0.1", + # Common optional paramers: + # port=502, + # protocol_class=ModbusClientProtocol, + # modbus_decoder=ClientDecoder, + # framer=ModbusSocketFramer, + # timeout=10, + # retries=3, + # retry_on_empty=False, + # close_comm_on_error=False, + # strict=True, + # UDP setup parameters + # source_address=("localhost", 0), + ) + + await client.start() + ... + await client.stop() +""" import asyncio import logging import functools from pymodbus.factory import ClientDecoder from pymodbus.transaction import ModbusSocketFramer +from pymodbus.client.helper_async import ModbusClientProtocol from pymodbus.client.helper_async import BaseModbusAsyncClientProtocol _logger = logging.getLogger(__name__) -class ModbusUdpClientProtocol(BaseModbusAsyncClientProtocol, asyncio.DatagramProtocol): - """Asyncio specific implementation of asynchronous modbus udp client protocol.""" +class ModbusUdpClientProtocol( # pylint: disable=too-many-instance-attributes + BaseModbusAsyncClientProtocol, + asyncio.DatagramProtocol +): + r"""Modbus UDP client, asyncio based. + + :param host: (positional) Host IP address + :param port: (optional default 502) The serial port used for communication. + :param protocol_class: (optional, default ModbusClientProtocol) Protocol communication class. + :param modbus_decoder: (optional, default ClientDecoder) Message decoder class. + :param framer: (optional, default ModbusSocketFramer) Framer class. + :param timeout: (optional, default 3s) Timeout for a request. + :param retries: (optional, default 3) Max number of retries pr request. + :param retry_on_empty: (optional, default false) Retry on empty response. + :param close_comm_on_error: (optional, default true) Close connection on error. + :param strict: (optional, default true) Strict timing, 1.5 character between requests. + :param source_address: (optional, default none) source address of client, + :param \*\*kwargs: (optional) Extra experimental parameters for transport + :return: client object + """ #: Factory that created this instance. factory = None - def __init__(self, host=None, port=0, framer=None, **kwargs): - """Initialize.""" + def __init__( # pylint: disable=too-many-arguments + # Fixed parameters + self, + host, + port=502, + # Common optional paramers: + protocol_class=ModbusClientProtocol, + modbus_decoder=ClientDecoder, + framer=ModbusSocketFramer, + timeout=10, + retries=3, + retry_on_empty=False, + close_comm_on_error=False, + strict=True, + + # UDP setup parameters + source_address=None, + + # Extra parameters for transport (experimental) + **kwargs, + ): + """Initialize Asyncio Modbus TCP Client.""" self.host = host self.port = port - self.framer = framer if framer else ModbusSocketFramer + self.protocol_class = protocol_class + self.framer = framer(modbus_decoder()) + self.timeout = timeout + self.retries = retries + self.retry_on_empty = retry_on_empty + self.close_comm_on_error = close_comm_on_error + self.strict = strict + self.source_address = source_address + self.kwargs = kwargs super().__init__(**kwargs) def datagram_received(self, data, addr): @@ -32,11 +105,11 @@ def write_transport(self, packet): return self.transport.sendto(packet) -class AsyncModbusUDPClient: +class AsyncModbusUdpClient: """Actual Async UDP Client to be used. To use do:: - from pymodbus.client import AsyncModbusUDPClient + from pymodbus.client import d """ #: Reconnect delay in milli seconds. @@ -58,18 +131,20 @@ def __init__( **kwargs, ): - """Initialize AsyncioModbusUDPClient. - - :param host: Host IP address - :param port: The serial port to attach to - :param protocol_class: Protocol used to talk to modbus device. - :param modbus_decoder: Message decoder. - :param framer: Modbus framer - :param timeout: The timeout between serial requests (default 3s) - - :param source_address: source address specific to underlying backend - - :param kwargs: Other extra args specific to Backend being used + r"""Modbus client for async TCP communication. + + :param host: (positional) Host IP address + :param port: (optional default 502) The serial port used for communication. + :param protocol_class: (optional, default ModbusClientProtocol) Protocol communication class. + :param modbus_decoder: (optional, default ClientDecoder) Message decoder class. + :param framer: (optional, default ModbusSocketFramer) Framer class. + :param timeout: (optional, default 3s) Timeout for a request. + :param retries: (optional, default 3) Max number of retries pr request. + :param retry_on_empty: (optional, default false) Retry on empty response. + :param close_comm_on_error: (optional, default true) Close connection on error. + :param strict: (optional, default true) Strict timing, 1.5 character between requests. + :param source_address: (optional, default none) source address of client, + :param \*\*kwargs: (optional) Extra experimental parameters for transport :return: client object """ self.host = host diff --git a/pymodbus/client/helper_sync.py b/pymodbus/client/helper_sync.py index 107d90f23..b828e0626 100644 --- a/pymodbus/client/helper_sync.py +++ b/pymodbus/client/helper_sync.py @@ -5,6 +5,7 @@ simplify the interface. """ # pylint: disable=missing-type-doc +import sys import logging from pymodbus.bit_read_message import ReadCoilsRequest, ReadDiscreteInputsRequest @@ -19,7 +20,13 @@ WriteMultipleRegistersRequest, WriteSingleRegisterRequest, ) -from pymodbus.utilities import ModbusTransactionState +from pymodbus.exceptions import ( + ConnectionException, + NotImplementedException, +) +from pymodbus.utilities import ModbusTransactionState, hexlify_packets +from pymodbus.transaction import DictTransactionManager +from pymodbus.constants import Defaults _logger = logging.getLogger(__name__) @@ -150,3 +157,152 @@ def mask_write_register(self, *args, **kwargs): """ request = MaskWriteRegisterRequest(*args, **kwargs) return self.execute(request) # pylint: disable=no-member + + +class BaseModbusClient(ModbusClientMixin): + """Interface for a modbus synchronous client. + + Defined here are all the methods for performing the related + request methods. + Derived classes simply need to implement the transport methods and set the correct + framer. + """ + + def __init__(self, framer, **kwargs): + """Initialize a client instance. + + :param framer: The modbus framer implementation to use + """ + self.framer = framer + self.transaction = DictTransactionManager(self, **kwargs) + self._debug = False + self._debugfd = None + self.broadcast_enable = kwargs.get( + "broadcast_enable", Defaults.broadcast_enable + ) + + # ----------------------------------------------------------------------- # + # Client interface + # ----------------------------------------------------------------------- # + def connect(self): + """Connect to the modbus remote host. + + :raises NotImplementedException: + """ + raise NotImplementedException("Method not implemented by derived class") + + def close(self): + """Close the underlying socket connection.""" + + def is_socket_open(self): + """Check whether the underlying socket/serial is open or not. + + :raises NotImplementedException: + """ + raise NotImplementedException( + f"is_socket_open() not implemented by {self.__str__()}" # pylint: disable=unnecessary-dunder-call + ) + + def send(self, request): + """Send request.""" + if self.state != ModbusTransactionState.RETRYING: + _logger.debug('New Transaction state "SENDING"') + self.state = ModbusTransactionState.SENDING + return self._send(request) + + def _send(self, request): + """Send data on the underlying socket. + + :param request: The encoded request to send + :raises NotImplementedException: + """ + raise NotImplementedException("Method not implemented by derived class") + + def recv(self, size): + """Receive data.""" + return self._recv(size) + + def _recv(self, size): + """Read data from the underlying descriptor. + + :param size: The number of bytes to read + :raises NotImplementedException: + """ + raise NotImplementedException("Method not implemented by derived class") + + # ----------------------------------------------------------------------- # + # Modbus client methods + # ----------------------------------------------------------------------- # + def execute(self, request=None): + """Execute. + + :param request: The request to process + :returns: The result of the request execution + :raises ConnectionException: + """ + if not self.connect(): + raise ConnectionException(f"Failed to connect[{str(self)}]") + return self.transaction.execute(request) + + # ----------------------------------------------------------------------- # + # The magic methods + # ----------------------------------------------------------------------- # + def __enter__(self): + """Implement the client with enter block. + + :returns: The current instance of the client + :raises ConnectionException: + """ + if not self.connect(): + raise ConnectionException(f"Failed to connect[{self.__str__()}]") + return self + + def __exit__(self, klass, value, traceback): + """Implement the client with exit block.""" + self.close() + + def idle_time(self): + """Bus Idle Time to initiate next transaction + + :return: time stamp + """ + if self.last_frame_end is None or self.silent_interval is None: + return 0 + return self.last_frame_end + self.silent_interval + + def debug_enabled(self): + """Return a boolean indicating if debug is enabled.""" + return self._debug + + def set_debug(self, debug): + """Set the current debug flag.""" + self._debug = debug + + def trace(self, writeable): + """Show trace.""" + if writeable: + self.set_debug(True) + self._debugfd = writeable + + def _dump(self, data): + """Dump.""" + fd = self._debugfd if self._debugfd else sys.stdout + try: + fd.write(hexlify_packets(data)) + except Exception as exc: # pylint: disable=broad-except + _logger.debug(hexlify_packets(data)) + _logger.exception(exc) + + def register(self, function): + """Register a function and sub function class with the decoder. + + :param function: Custom function class to register + """ + self.framer.decoder.register(function) + + def __str__(self): + """Build a string representation of the connection. + + :returns: The string representation + """ + return "Null Transport" diff --git a/pymodbus/client/sync_serial.py b/pymodbus/client/sync_serial.py index 1a17e29bb..4cfb36f94 100644 --- a/pymodbus/client/sync_serial.py +++ b/pymodbus/client/sync_serial.py @@ -1,225 +1,125 @@ -"""Sync client.""" -# pylint: disable=missing-type-doc +"""Modbus client serial communication. + +The serial communication is RS-485 based, and usually used vith a usb rs-485 dongle. + +Example:: + + from pymodbus.client import ModbusSerialClient + + def run(): + client = ModbusSerialClient( + port="/dev/pty0", # serial port + # Common optional paramers: + # protocol_class=ModbusClientProtocol, + # modbus_decoder=ClientDecoder, + # framer=ModbusRtuFramer, + # timeout=10, + # retries=3, + # retry_on_empty=False, + # close_comm_on_error=False,. + # strict=True, + # Serial setup parameters + # baudrate=9600, + # bytesize=8, + # parity="N", + # stopbits=1, + # handle_local_echo=False, + ) + + client.start() + ... + client.stop() +""" from functools import partial import logging -import sys import time import serial -from pymodbus.client.helper_sync import ModbusClientMixin -from pymodbus.constants import Defaults -from pymodbus.exceptions import ( - ConnectionException, - NotImplementedException, -) +from pymodbus.client.helper_sync import BaseModbusClient +from pymodbus.exceptions import ConnectionException from pymodbus.factory import ClientDecoder -from pymodbus.transaction import ( - DictTransactionManager, - ModbusRtuFramer, -) +from pymodbus.transaction import ModbusRtuFramer from pymodbus.utilities import ModbusTransactionState, hexlify_packets +from pymodbus.client.helper_async import ModbusClientProtocol -# --------------------------------------------------------------------------- # -# Logging -# --------------------------------------------------------------------------- # _logger = logging.getLogger(__name__) -# --------------------------------------------------------------------------- # -# The Synchronous Clients -# --------------------------------------------------------------------------- # - - -class BaseModbusClient(ModbusClientMixin): - """Interface for a modbus synchronous client. - - Defined here are all the methods for performing the related - request methods. - Derived classes simply need to implement the transport methods and set the correct - framer. - """ - - def __init__(self, framer, **kwargs): - """Initialize a client instance. - - :param framer: The modbus framer implementation to use - """ - self.framer = framer - self.transaction = DictTransactionManager(self, **kwargs) - self._debug = False - self._debugfd = None - self.broadcast_enable = kwargs.get( - "broadcast_enable", Defaults.broadcast_enable - ) - - # ----------------------------------------------------------------------- # - # Client interface - # ----------------------------------------------------------------------- # - def connect(self): - """Connect to the modbus remote host. - - :raises NotImplementedException: - """ - raise NotImplementedException("Method not implemented by derived class") - - def close(self): - """Close the underlying socket connection.""" - - def is_socket_open(self): - """Check whether the underlying socket/serial is open or not. - - :raises NotImplementedException: - """ - raise NotImplementedException( - f"is_socket_open() not implemented by {self.__str__()}" # pylint: disable=unnecessary-dunder-call - ) - - def send(self, request): - """Send request.""" - if self.state != ModbusTransactionState.RETRYING: - _logger.debug('New Transaction state "SENDING"') - self.state = ModbusTransactionState.SENDING - return self._send(request) - - def _send(self, request): - """Send data on the underlying socket. - - :param request: The encoded request to send - :raises NotImplementedException: - """ - raise NotImplementedException("Method not implemented by derived class") - - def recv(self, size): - """Receive data.""" - return self._recv(size) - - def _recv(self, size): - """Read data from the underlying descriptor. - - :param size: The number of bytes to read - :raises NotImplementedException: - """ - raise NotImplementedException("Method not implemented by derived class") - - # ----------------------------------------------------------------------- # - # Modbus client methods - # ----------------------------------------------------------------------- # - def execute(self, request=None): - """Execute. - - :param request: The request to process - :returns: The result of the request execution - :raises ConnectionException: - """ - if not self.connect(): - raise ConnectionException(f"Failed to connect[{str(self)}]") - return self.transaction.execute(request) - - # ----------------------------------------------------------------------- # - # The magic methods - # ----------------------------------------------------------------------- # - def __enter__(self): - """Implement the client with enter block. - - :returns: The current instance of the client - :raises ConnectionException: - """ - if not self.connect(): - raise ConnectionException(f"Failed to connect[{self.__str__()}]") - return self - - def __exit__(self, klass, value, traceback): - """Implement the client with exit block.""" - self.close() - - def idle_time(self): - """Bus Idle Time to initiate next transaction - - :return: time stamp - """ - if self.last_frame_end is None or self.silent_interval is None: - return 0 - return self.last_frame_end + self.silent_interval - - def debug_enabled(self): - """Return a boolean indicating if debug is enabled.""" - return self._debug - - def set_debug(self, debug): - """Set the current debug flag.""" - self._debug = debug - - def trace(self, writeable): - """Show trace.""" - if writeable: - self.set_debug(True) - self._debugfd = writeable - - def _dump(self, data): - """Dump.""" - fd = self._debugfd if self._debugfd else sys.stdout - try: - fd.write(hexlify_packets(data)) - except Exception as exc: # pylint: disable=broad-except - _logger.debug(hexlify_packets(data)) - _logger.exception(exc) - - def register(self, function): - """Register a function and sub function class with the decoder. - - :param function: Custom function class to register - """ - self.framer.decoder.register(function) - - def __str__(self): - """Build a string representation of the connection. - - :returns: The string representation - """ - return "Null Transport" - - -# --------------------------------------------------------------------------- # -# Modbus Serial Client Transport Implementation -# --------------------------------------------------------------------------- # - class ModbusSerialClient( BaseModbusClient ): # pylint: disable=too-many-instance-attributes - """Implementation of a modbus serial client.""" + r"""Modbus client for serial (RS-485) communication. + + :param port: (positional) Serial port used for communication. + :param protocol_class: (optional, default ModbusClientProtocol) Protocol communication class. + :param modbus_decoder: (optional, default ClientDecoder) Message decoder class. + :param framer: (optional, default ModbusRtuFramer) Framer class. + :param timeout: (optional, default 3s) Timeout for a request. + :param retries: (optional, default 3) Max number of retries pr request. + :param retry_on_empty: (optional, default false) Retry on empty response. + :param close_comm_on_error: (optional, default true) Close connection on error. + :param strict: (optional, default true) Strict timing, 1.5 character between requests. + :param baudrate: (optional, default 9600) Bits pr second. + :param bytesize: (optional, default 8) Number of bits pr byte 7-8. + :param parity: (optional, default None). + :param stopbits: (optional, default 1) Number of stop bits 0-2 to use. + :param handle_local_echo: (optional, default false) Handle local echo of the USB-to-RS485 dongle. + :param \*\*kwargs: (optional) Extra experimental parameters for transport + :return: client object + """ state = ModbusTransactionState.IDLE inter_char_timeout = 0 silent_interval = 0 - def __init__(self, framer=ModbusRtuFramer, **kwargs): - """Initialize a serial client instance. - - :param port: The serial port to attach to - :param stopbits: The number of stop bits to use - :param bytesize: The bytesize of the serial messages - :param parity: Which kind of parity to use - :param baudrate: The baud rate to use for the serial device - :param timeout: The timeout between serial requests (default 3s) - :param strict: Use Inter char timeout for baudrates <= 19200 (adhere - to modbus standards) - :param handle_local_echo: Handle local echo of the USB-to-RS485 adaptor - :param framer: The modbus framer to use (default ModbusRtuFramer) - """ - self.framer = framer + def __init__( # pylint: disable=too-many-arguments + # Positional parameters + self, + port, + # Common optional paramers: + protocol_class=ModbusClientProtocol, + modbus_decoder=ClientDecoder, + framer=ModbusRtuFramer, + timeout=10, + retries=3, + retry_on_empty=False, + close_comm_on_error=False, + strict=True, + + # Serial setup parameters + baudrate=9600, + bytesize=8, + parity="N", + stopbits=1, + handle_local_echo=False, + # Extra parameters for serial_async (experimental) + **kwargs, + ): + """Initialize Modbus Serial Client.""" + self.port = port + + self.port = port + self.protocol_class = protocol_class + self.framer = framer(modbus_decoder()) + self.timeout = timeout + self.retries = retries + self.retry_on_empty = retry_on_empty + self.close_comm_on_error = close_comm_on_error + self.strict = strict + + self.baudrate = baudrate + self.bytesize = bytesize + self.parity = parity + self.stopbits = stopbits + self.handle_local_echo = handle_local_echo + + self.kwargs = kwargs + self.socket = None BaseModbusClient.__init__(self, framer(ClientDecoder(), self), **kwargs) - self.port = kwargs.get("port", 0) - self.stopbits = kwargs.get("stopbits", Defaults.Stopbits) - self.bytesize = kwargs.get("bytesize", Defaults.Bytesize) - self.parity = kwargs.get("parity", Defaults.Parity) - self.baudrate = kwargs.get("baudrate", Defaults.Baudrate) - self.timeout = kwargs.get("timeout", Defaults.Timeout) - self._strict = kwargs.get("strict", False) self.last_frame_end = None - self.handle_local_echo = kwargs.get("handle_local_echo", False) if isinstance(self.framer, ModbusRtuFramer): if self.baudrate > 19200: self.silent_interval = 1.75 / 1000 # ms @@ -246,7 +146,7 @@ def connect(self): parity=self.parity, ) if isinstance(self.framer, ModbusRtuFramer): - if self._strict: + if self.strict: self.socket.interCharTimeout = self.inter_char_timeout self.last_frame_end = None except serial.SerialException as msg: @@ -270,7 +170,7 @@ def _in_waiting(self): waitingbytes = getattr(self.socket, in_waiting)() return waitingbytes - def _send(self, request): + def _send(self, request): # pylint: disable=missing-type-doc """Send data on the underlying socket. If receive buffer still holds some data then flush it. @@ -326,7 +226,7 @@ def _wait_for_data(self): time.sleep(0.01) return size - def _recv(self, size): + def _recv(self, size): # pylint: disable=missing-type-doc """Read data from the underlying descriptor. :param size: The number of bytes to read diff --git a/pymodbus/client/sync_tcp.py b/pymodbus/client/sync_tcp.py index 0ca97a94f..2e08ac977 100644 --- a/pymodbus/client/sync_tcp.py +++ b/pymodbus/client/sync_tcp.py @@ -1,207 +1,97 @@ -"""Sync client.""" -# pylint: disable=missing-type-doc +"""Modbus client TCP communication. + +Example:: + + from pymodbus.client import ModbusTcpClient + + def run(): + client = ModbusTcpClient( + "127.0.0.1", + # port=502, # on which port + # Common optional paramers: + # protocol_class=None, + # modbus_decoder=ClientDecoder, + # framer=ModbusSocketFramer, # how to interpret the messages + # timeout=10, # waiting time for request to complete + # retries=3, # retries per transaction + # retry_on_empty=False, # Is an empty response a retry + # close_comm_on_error=False, # close connection when error. + # strict=True, # use strict timing, t1.5 for Modbus RTU + # TCP setup parameters + # source_address=("localhost", 0), # bind socket to address + ) + + client.start() + ... + client.stop() +""" import logging import select import socket -import sys import time -from pymodbus.client.helper_sync import ModbusClientMixin -from pymodbus.constants import Defaults -from pymodbus.exceptions import ( - ConnectionException, - NotImplementedException, -) +from pymodbus.client.helper_sync import BaseModbusClient +from pymodbus.exceptions import ConnectionException from pymodbus.factory import ClientDecoder -from pymodbus.transaction import ( - DictTransactionManager, - ModbusSocketFramer, -) -from pymodbus.utilities import ModbusTransactionState, hexlify_packets - -# --------------------------------------------------------------------------- # -# Logging -# --------------------------------------------------------------------------- # -_logger = logging.getLogger(__name__) - -# --------------------------------------------------------------------------- # -# The Synchronous Clients -# --------------------------------------------------------------------------- # +from pymodbus.transaction import ModbusSocketFramer +from pymodbus.utilities import ModbusTransactionState +_logger = logging.getLogger(__name__) -class BaseModbusClient(ModbusClientMixin): - """Interface for a modbus synchronous client. - Defined here are all the methods for performing the related - request methods. - Derived classes simply need to implement the transport methods and set the correct - framer. +class ModbusTcpClient(BaseModbusClient): # pylint: disable=too-many-instance-attributes + r"""Modbus client for TCP communication. + + :param host: (positional) Host IP address + :param port: (optional default 502) The serial port used for communication. + :param protocol_class: (optional, default ModbusClientProtocol) Protocol communication class. + :param modbus_decoder: (optional, default ClientDecoder) Message decoder class. + :param framer: (optional, default ModbusSocketFramer) Framer class. + :param timeout: (optional, default 3s) Timeout for a request. + :param retries: (optional, default 3) Max number of retries pr request. + :param retry_on_empty: (optional, default false) Retry on empty response. + :param close_comm_on_error: (optional, default true) Close connection on error. + :param strict: (optional, default true) Strict timing, 1.5 character between requests. + :param source_address: (optional, default none) source address of client, + :param \*\*kwargs: (optional) Extra experimental parameters for transport + :return: client object """ - def __init__(self, framer, **kwargs): - """Initialize a client instance. - - :param framer: The modbus framer implementation to use - """ - self.framer = framer - self.transaction = DictTransactionManager(self, **kwargs) - self._debug = False - self._debugfd = None - self.broadcast_enable = kwargs.get( - "broadcast_enable", Defaults.broadcast_enable - ) - - # ----------------------------------------------------------------------- # - # Client interface - # ----------------------------------------------------------------------- # - def connect(self): - """Connect to the modbus remote host. - - :raises NotImplementedException: - """ - raise NotImplementedException("Method not implemented by derived class") - - def close(self): - """Close the underlying socket connection.""" - - def is_socket_open(self): - """Check whether the underlying socket/serial is open or not. - - :raises NotImplementedException: - """ - raise NotImplementedException( - f"is_socket_open() not implemented by {self.__str__()}" # pylint: disable=unnecessary-dunder-call - ) - - def send(self, request): - """Send request.""" - if self.state != ModbusTransactionState.RETRYING: - _logger.debug('New Transaction state "SENDING"') - self.state = ModbusTransactionState.SENDING - return self._send(request) - - def _send(self, request): - """Send data on the underlying socket. - - :param request: The encoded request to send - :raises NotImplementedException: - """ - raise NotImplementedException("Method not implemented by derived class") - - def recv(self, size): - """Receive data.""" - return self._recv(size) - - def _recv(self, size): - """Read data from the underlying descriptor. - - :param size: The number of bytes to read - :raises NotImplementedException: - """ - raise NotImplementedException("Method not implemented by derived class") - - # ----------------------------------------------------------------------- # - # Modbus client methods - # ----------------------------------------------------------------------- # - def execute(self, request=None): - """Execute. - - :param request: The request to process - :returns: The result of the request execution - :raises ConnectionException: - """ - if not self.connect(): - raise ConnectionException(f"Failed to connect[{str(self)}]") - return self.transaction.execute(request) - - # ----------------------------------------------------------------------- # - # The magic methods - # ----------------------------------------------------------------------- # - def __enter__(self): - """Implement the client with enter block. - - :returns: The current instance of the client - :raises ConnectionException: - """ - if not self.connect(): - raise ConnectionException(f"Failed to connect[{self.__str__()}]") - return self - - def __exit__(self, klass, value, traceback): - """Implement the client with exit block.""" - self.close() - - def idle_time(self): - """Bus Idle Time to initiate next transaction - - :return: time stamp - """ - if self.last_frame_end is None or self.silent_interval is None: - return 0 - return self.last_frame_end + self.silent_interval - - def debug_enabled(self): - """Return a boolean indicating if debug is enabled.""" - return self._debug - - def set_debug(self, debug): - """Set the current debug flag.""" - self._debug = debug - - def trace(self, writeable): - """Show trace.""" - if writeable: - self.set_debug(True) - self._debugfd = writeable - - def _dump(self, data): - """Dump.""" - fd = self._debugfd if self._debugfd else sys.stdout - try: - fd.write(hexlify_packets(data)) - except Exception as exc: # pylint: disable=broad-except - _logger.debug(hexlify_packets(data)) - _logger.exception(exc) - - def register(self, function): - """Register a function and sub function class with the decoder. - - :param function: Custom function class to register - """ - self.framer.decoder.register(function) - - def __str__(self): - """Build a string representation of the connection. - - :returns: The string representation - """ - return "Null Transport" - - -# --------------------------------------------------------------------------- # -# Modbus TCP Client Transport Implementation -# --------------------------------------------------------------------------- # -class ModbusTcpClient(BaseModbusClient): - """Implementation of a modbus tcp client.""" - - def __init__( - self, host="127.0.0.1", port=Defaults.Port, framer=ModbusSocketFramer, **kwargs + def __init__( # pylint: disable=too-many-arguments + # Fixed parameters + self, + host, + port=502, + # Common optional paramers: + protocol_class=None, + modbus_decoder=ClientDecoder, + framer=ModbusSocketFramer, + timeout=10, + retries=3, + retry_on_empty=False, + close_comm_on_error=False, + strict=True, + + # TCP setup parameters + source_address=None, + + # Extra parameters for serial_async (experimental) + **kwargs, ): - """Initialize a client instance. - - :param host: The host to connect to (default 127.0.0.1) - :param port: The modbus port to connect to (default 502) - :param source_address: The source address tuple to bind to (default ("", 0)) - :param timeout: The timeout to use for this socket (default Defaults.Timeout) - :param framer: The modbus framer to use (default ModbusSocketFramer) - - .. note:: The host argument will accept ipv4 and ipv6 hosts - """ + """Initialize Modbus TCP Client.""" self.host = host self.port = port - self.source_address = kwargs.get("source_address", ("", 0)) + self.protocol_class = protocol_class + self.framer = framer(modbus_decoder()) + self.timeout = timeout + self.retries = retries + self.retry_on_empty = retry_on_empty + self.close_comm_on_error = close_comm_on_error + self.strict = strict + self.source_address = source_address + self.kwargs = kwargs + self.socket = None - self.timeout = kwargs.get("timeout", Defaults.Timeout) BaseModbusClient.__init__(self, framer(ClientDecoder(), self), **kwargs) def connect(self): @@ -241,7 +131,7 @@ def _check_read_buffer(self): data = self.socket.recv(1024) return data - def _send(self, request): + def _send(self, request): # pylint: disable=missing-type-doc """Send data on the underlying socket. :param request: The encoded request to send @@ -258,7 +148,7 @@ def _send(self, request): return self.socket.send(request) return 0 - def _recv(self, size): + def _recv(self, size): # pylint: disable=missing-type-doc """Read data from the underlying descriptor. :param size: The number of bytes to read @@ -318,7 +208,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, data, duration): # pylint: disable=missing-type-doc """Handle unexpected socket close by remote end. Intended to be invoked after determining that the remote end diff --git a/pymodbus/client/sync_tls.py b/pymodbus/client/sync_tls.py index 4bf7930e2..53a00a82b 100644 --- a/pymodbus/client/sync_tls.py +++ b/pymodbus/client/sync_tls.py @@ -1,55 +1,112 @@ -"""Sync client.""" -# pylint: disable=missing-type-doc +"""Modbus client TLS communication. + +Example:: + + from pymodbus.client import ModbusTlsClient + + def run(): + client = ModbusTlsClient( + "localhost", + # port=802, + # Common optional paramers: + # protocol_class=None, + # modbus_decoder=ClientDecoder, + # framer=ModbusTLsFramer, + # timeout=10, + # retries=3, + # retry_on_empty=False, + # close_comm_on_error=False, + # strict=True, + # TLS setup parameters + # sslctx=None, + # certfile=None, + # keyfile=None, + # password=None, + # server_hostname="localhost", + + client.start() + ... + client.stop() +""" import logging import socket import time +from pymodbus.factory import ClientDecoder from pymodbus.client.helper_tls import sslctx_provider from pymodbus.client.sync_tcp import ModbusTcpClient -from pymodbus.constants import Defaults from pymodbus.exceptions import ConnectionException from pymodbus.transaction import ModbusTlsFramer -# --------------------------------------------------------------------------- # -# Logging -# --------------------------------------------------------------------------- # _logger = logging.getLogger(__name__) -# --------------------------------------------------------------------------- # -# Modbus TLS Client Transport Implementation -# --------------------------------------------------------------------------- # - -class ModbusTlsClient(ModbusTcpClient): - """Implementation of a modbus tls client.""" - - def __init__( +class ModbusTlsClient(ModbusTcpClient): # pylint: disable=too-many-instance-attributes + r"""Modbus client for TLS communication. + + :param host: (positional) Host IP address + :param port: (optional default 802) The serial port used for communication. + :param protocol_class: (optional, default ModbusClientProtocol) Protocol communication class. + :param modbus_decoder: (optional, default ClientDecoder) Message decoder class. + :param framer: (optional, default ModbusSocketFramer) Framer class. + :param timeout: (optional, default 3s) Timeout for a request. + :param retries: (optional, default 3) Max number of retries pr request. + :param retry_on_empty: (optional, default false) Retry on empty response. + :param close_comm_on_error: (optional, default true) Close connection on error. + :param strict: (optional, default true) Strict timing, 1.5 character between requests. + :param source_address: (optional, default none) Source address of client, + :param sslctx: (optional, default none) SSLContext to use for TLS (default None and auto create) + :param certfile: (optional, default none) Cert file path for TLS server request + :param keyfile: (optional, default none) Key file path for TLS server request + :param password: (optional, default none) Password for for decrypting client"s private key file + :param server_hostname: (optional, default none) Bind certificate to host, + :param \*\*kwargs: (optional) Extra experimental parameters for transport + :return: client object + """ + + def __init__( # pylint: disable=too-many-arguments + # Fixed parameters self, - host="localhost", - port=Defaults.TLSPort, + host, + port=802, + # Common optional paramers: + protocol_class=None, + modbus_decoder=ClientDecoder, + framer=ModbusTlsFramer, + timeout=10, + retries=3, + retry_on_empty=False, + close_comm_on_error=False, + strict=True, + + # TLS setup parameters sslctx=None, certfile=None, keyfile=None, password=None, - framer=ModbusTlsFramer, + server_hostname=None, + + # Extra parameters for serial_async (experimental) **kwargs, ): - """Initialize a client instance. - - :param host: The host to connect to (default localhost) - :param port: The modbus port to connect to (default 802) - :param sslctx: The SSLContext to use for TLS (default None and auto create) - :param certfile: The optional client"s cert file path for TLS server request - :param keyfile: The optional client"s key file path for TLS server request - :param password: The password for for decrypting client"s private key file - :param source_address: The source address tuple to bind to (default ("", 0)) - :param timeout: The timeout to use for this socket (default Defaults.Timeout) - :param framer: The modbus framer to use (default ModbusSocketFramer) - - .. note:: The host argument will accept ipv4 and ipv6 hosts - """ + """Initialize Modbus TCP Client.""" + self.host = host + self.port = port + self.protocol_class = protocol_class + self.framer = framer(modbus_decoder()) + self.timeout = timeout + self.retries = retries + self.retry_on_empty = retry_on_empty + self.close_comm_on_error = close_comm_on_error + self.strict = strict self.sslctx = sslctx_provider(sslctx, certfile, keyfile, password) - ModbusTcpClient.__init__(self, host, port, framer, **kwargs) + self.certfile = certfile + self.keyfile = keyfile + self.password = password + self.server_hostname = server_hostname + self.kwargs = kwargs + + ModbusTcpClient.__init__(self, host, port=port, framer=framer, **kwargs) def connect(self): """Connect to the modbus tls server. @@ -60,7 +117,8 @@ def connect(self): return True try: sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - sock.bind(self.source_address) + if self.source_address: + sock.bind(self.source_address) self.socket = self.sslctx.wrap_socket( sock, server_side=False, server_hostname=self.host ) @@ -73,12 +131,7 @@ def connect(self): return self.socket is not None def _recv(self, size): - """Read data from the underlying descriptor. - - :param size: The number of bytes to read - :return: The bytes read - :raises ConnectionException: - """ + """Read data from the underlying descriptor.""" if not self.socket: raise ConnectionException(str(self)) diff --git a/pymodbus/client/sync_udp.py b/pymodbus/client/sync_udp.py index b5bafedbc..3b3d30e74 100644 --- a/pymodbus/client/sync_udp.py +++ b/pymodbus/client/sync_udp.py @@ -1,214 +1,107 @@ -"""Sync client.""" -# pylint: disable=missing-type-doc +"""Modbus client UDP communication. + +Example:: + + from pymodbus.client import AsyncModbusUdpClient + + def run(): + client = AsyncModbusUdpClient( + "127.0.0.1", + # Common optional paramers: + # port=502, + # protocol_class=ModbusClientProtocol, + # modbus_decoder=ClientDecoder, + # framer=ModbusSocketFramer, + # timeout=10, + # retries=3, + # retry_on_empty=False, + # close_comm_on_error=False, + # strict=True, + # UDP setup parameters + # source_address=("localhost", 0), + ) + + client.start() + ... + client.stop() +""" import logging import socket -import sys -from pymodbus.client.helper_sync import ModbusClientMixin -from pymodbus.constants import Defaults -from pymodbus.exceptions import ( - ConnectionException, - NotImplementedException, -) +from pymodbus.client.helper_sync import BaseModbusClient +from pymodbus.exceptions import ConnectionException from pymodbus.factory import ClientDecoder -from pymodbus.transaction import ( - DictTransactionManager, - ModbusSocketFramer, -) -from pymodbus.utilities import ModbusTransactionState, hexlify_packets +from pymodbus.transaction import ModbusSocketFramer +from pymodbus.client.helper_async import ModbusClientProtocol # --------------------------------------------------------------------------- # # Logging # --------------------------------------------------------------------------- # _logger = logging.getLogger(__name__) -# --------------------------------------------------------------------------- # -# The Synchronous Clients -# --------------------------------------------------------------------------- # - - -class BaseModbusClient(ModbusClientMixin): - """Interface for a modbus synchronous client. - - Defined here are all the methods for performing the related - request methods. - Derived classes simply need to implement the transport methods and set the correct - framer. - """ - - def __init__(self, framer, **kwargs): - """Initialize a client instance. - - :param framer: The modbus framer implementation to use - """ - self.framer = framer - self.transaction = DictTransactionManager(self, **kwargs) - self._debug = False - self._debugfd = None - self.broadcast_enable = kwargs.get( - "broadcast_enable", Defaults.broadcast_enable - ) - - # ----------------------------------------------------------------------- # - # Client interface - # ----------------------------------------------------------------------- # - def connect(self): - """Connect to the modbus remote host. - - :raises NotImplementedException: - """ - raise NotImplementedException("Method not implemented by derived class") - - def close(self): - """Close the underlying socket connection.""" - - def is_socket_open(self): - """Check whether the underlying socket/serial is open or not. - - :raises NotImplementedException: - """ - raise NotImplementedException( - f"is_socket_open() not implemented by {self.__str__()}" # pylint: disable=unnecessary-dunder-call - ) - - def send(self, request): - """Send request.""" - if self.state != ModbusTransactionState.RETRYING: - _logger.debug('New Transaction state "SENDING"') - self.state = ModbusTransactionState.SENDING - return self._send(request) - - def _send(self, request): - """Send data on the underlying socket. - - :param request: The encoded request to send - :raises NotImplementedException: - """ - raise NotImplementedException("Method not implemented by derived class") - - def recv(self, size): - """Receive data.""" - return self._recv(size) - - def _recv(self, size): - """Read data from the underlying descriptor. - - :param size: The number of bytes to read - :raises NotImplementedException: - """ - raise NotImplementedException("Method not implemented by derived class") - - # ----------------------------------------------------------------------- # - # Modbus client methods - # ----------------------------------------------------------------------- # - def execute(self, request=None): - """Execute. - - :param request: The request to process - :returns: The result of the request execution - :raises ConnectionException: - """ - if not self.connect(): - raise ConnectionException(f"Failed to connect[{str(self)}]") - return self.transaction.execute(request) - - # ----------------------------------------------------------------------- # - # The magic methods - # ----------------------------------------------------------------------- # - def __enter__(self): - """Implement the client with enter block. - - :returns: The current instance of the client - :raises ConnectionException: - """ - if not self.connect(): - raise ConnectionException(f"Failed to connect[{self.__str__()}]") - return self - - def __exit__(self, klass, value, traceback): - """Implement the client with exit block.""" - self.close() - - def idle_time(self): - """Bus Idle Time to initiate next transaction - - :return: time stamp - """ - if self.last_frame_end is None or self.silent_interval is None: - return 0 - return self.last_frame_end + self.silent_interval - - def debug_enabled(self): - """Return a boolean indicating if debug is enabled.""" - return self._debug - - def set_debug(self, debug): - """Set the current debug flag.""" - self._debug = debug - - def trace(self, writeable): - """Show trace.""" - if writeable: - self.set_debug(True) - self._debugfd = writeable - - def _dump(self, data): - """Dump.""" - fd = self._debugfd if self._debugfd else sys.stdout - try: - fd.write(hexlify_packets(data)) - except Exception as exc: # pylint: disable=broad-except - _logger.debug(hexlify_packets(data)) - _logger.exception(exc) - - def register(self, function): - """Register a function and sub function class with the decoder. - - :param function: Custom function class to register - """ - self.framer.decoder.register(function) - - def __str__(self): - """Build a string representation of the connection. - - :returns: The string representation - """ - return "Null Transport" - - # --------------------------------------------------------------------------- # # Modbus UDP Client Transport Implementation # --------------------------------------------------------------------------- # -class ModbusUdpClient(BaseModbusClient): - """Implementation of a modbus udp client.""" +class ModbusUdpClient(BaseModbusClient): # pylint: disable=too-many-instance-attributes + r"""Modbus client for UDP communication. + + :param host: (positional) Host IP address + :param port: (optional default 502) The serial port used for communication. + :param protocol_class: (optional, default ModbusClientProtocol) Protocol communication class. + :param modbus_decoder: (optional, default ClientDecoder) Message decoder class. + :param framer: (optional, default ModbusSocketFramer) Framer class. + :param timeout: (optional, default 3s) Timeout for a request. + :param retries: (optional, default 3) Max number of retries pr request. + :param retry_on_empty: (optional, default false) Retry on empty response. + :param close_comm_on_error: (optional, default true) Close connection on error. + :param strict: (optional, default true) Strict timing, 1.5 character between requests. + :param source_address: (optional, default none) source address of client, + :param \*\*kwargs: (optional) Extra experimental parameters for transport + :return: client object + """ - def __init__( - self, host="127.0.0.1", port=Defaults.Port, framer=ModbusSocketFramer, **kwargs + def __init__( # pylint: disable=too-many-arguments + # Fixed parameters + self, + host, + port=502, + # Common optional paramers: + protocol_class=ModbusClientProtocol, + modbus_decoder=ClientDecoder, + framer=ModbusSocketFramer, + timeout=10, + retries=3, + retry_on_empty=False, + close_comm_on_error=False, + strict=True, + + # UDP setup parameters + source_address=None, + + # Extra parameters for transport (experimental) + **kwargs, ): - """Initialize a client instance. - - :param host: The host to connect to (default 127.0.0.1) - :param port: The modbus port to connect to (default 502) - :param framer: The modbus framer to use (default ModbusSocketFramer) - :param timeout: The timeout to use for this socket (default None) - """ + """Initialize Asyncio Modbus TCP Client.""" self.host = host self.port = port + self.protocol_class = protocol_class + self.framer = framer(modbus_decoder()) + self.timeout = timeout + self.retries = retries + self.retry_on_empty = retry_on_empty + self.close_comm_on_error = close_comm_on_error + self.strict = strict + self.source_address = source_address + self.kwargs = kwargs + self.socket = None - self.timeout = kwargs.get("timeout", None) BaseModbusClient.__init__(self, framer(ClientDecoder(), self), **kwargs) @classmethod def _get_address_family(cls, address): - """Get the correct address family. - - for a given address. - - :param address: The address to get the af for - :returns: AF_INET for ipv4 and AF_INET6 for ipv6 - """ + """Get the correct address family.""" try: _ = socket.inet_pton(socket.AF_INET6, address) except socket.error: # not a valid ipv6 address @@ -237,12 +130,7 @@ def close(self): self.socket = None def _send(self, request): - """Send data on the underlying socket. - - :param request: The encoded request to send - :return: The number of bytes written - :raises ConnectionException: - """ + """Send data on the underlying socket.""" if not self.socket: raise ConnectionException(str(self)) if request: @@ -250,12 +138,7 @@ def _send(self, request): return 0 def _recv(self, size): - """Read data from the underlying descriptor. - - :param size: The number of bytes to read - :return: The bytes read - :raises ConnectionException: - """ + """Read data from the underlying descriptor.""" if not self.socket: raise ConnectionException(str(self)) return self.socket.recvfrom(size)[0] diff --git a/pymodbus/version.py b/pymodbus/version.py index d80779e9d..528492154 100644 --- a/pymodbus/version.py +++ b/pymodbus/version.py @@ -36,7 +36,7 @@ def __str__(self): return f"[{self.package}, version {self.short()}]" -version = Version("pymodbus", 3, 0, 0, "dev4") +version = Version("pymodbus", 3, 0, 0, "dev5") version.__name__ = ( # fix epydoc error # pylint: disable=attribute-defined-outside-init "pymodbus" ) diff --git a/test/test_client_async.py b/test/test_client_async.py index cfebea236..b7bd547ed 100644 --- a/test/test_client_async.py +++ b/test/test_client_async.py @@ -8,10 +8,10 @@ import pytest from pymodbus.client import ( - AsyncModbusUDPClient, - AsyncModbusTLSClient, + AsyncModbusUdpClient, + AsyncModbusTlsClient, AsyncModbusSerialClient, - AsyncModbusTCPClient, + AsyncModbusTcpClient, ) from pymodbus.transaction import ( ModbusAsciiFramer, @@ -49,8 +49,8 @@ class TestAsynchronousClient: # -----------------------------------------------------------------------# def test_tcp_no_asyncio_client(self): """Test the TCP client.""" - client = AsyncModbusTCPClient("127.0.0.1") - assert isinstance(client, AsyncModbusTCPClient) # nosec + client = AsyncModbusTcpClient("127.0.0.1") + assert isinstance(client, AsyncModbusTcpClient) # nosec assert isinstance(client.framer, ModbusSocketFramer) # nosec assert client.port == 502 # nosec @@ -59,8 +59,8 @@ def test_tcp_no_asyncio_client(self): async def test_tcp_asyncio_client(self): """Test the TCP client.""" - client = AsyncModbusTCPClient("127.0.0.1") - assert isinstance(client, AsyncModbusTCPClient) # nosec + client = AsyncModbusTcpClient("127.0.0.1") + assert isinstance(client, AsyncModbusTcpClient) # nosec assert isinstance(client.framer, ModbusSocketFramer) # nosec assert client.port == 502 # nosec @@ -73,8 +73,8 @@ async def test_tcp_asyncio_client(self): def test_tls_no_asyncio_client(self): """Test the TLS AsyncIO client.""" - client = AsyncModbusTLSClient("127.0.0.1") - assert isinstance(client, AsyncModbusTLSClient) # nosec + client = AsyncModbusTlsClient("127.0.0.1") + assert isinstance(client, AsyncModbusTlsClient) # nosec assert isinstance(client.framer, ModbusTlsFramer) # nosec assert isinstance(client.sslctx, ssl.SSLContext) # nosec assert client.port == 802 # nosec @@ -84,8 +84,8 @@ def test_tls_no_asyncio_client(self): async def test_tls_asyncio_client(self): """Test the TLS AsyncIO client.""" - client = AsyncModbusTLSClient("127.0.0.1") - assert isinstance(client, AsyncModbusTLSClient) # nosec + client = AsyncModbusTlsClient("127.0.0.1") + assert isinstance(client, AsyncModbusTlsClient) # nosec assert isinstance(client.framer, ModbusTlsFramer) # nosec assert isinstance(client.sslctx, ssl.SSLContext) # nosec assert client.port == 802 # nosec @@ -98,8 +98,8 @@ async def test_tls_asyncio_client(self): # -----------------------------------------------------------------------# def test_udp_no_asyncio_client(self): """Test the udp asyncio client""" - client = AsyncModbusUDPClient("127.0.0.1") - assert isinstance(client, AsyncModbusUDPClient) # nosec + client = AsyncModbusUdpClient("127.0.0.1") + assert isinstance(client, AsyncModbusUdpClient) # nosec assert isinstance(client.framer, ModbusSocketFramer) # nosec assert client.port == 502 # nosec @@ -108,8 +108,8 @@ def test_udp_no_asyncio_client(self): async def test_udp_asyncio_client(self): """Test the udp asyncio client""" - client = AsyncModbusUDPClient("127.0.0.1") - assert isinstance(client, AsyncModbusUDPClient) # nosec + client = AsyncModbusUdpClient("127.0.0.1") + assert isinstance(client, AsyncModbusUdpClient) # nosec assert isinstance(client.framer, ModbusSocketFramer) # nosec assert client.port == 502 # nosec diff --git a/test/test_client_async_asyncio.py b/test/test_client_async_asyncio.py index 282b4b071..f81492ae4 100644 --- a/test/test_client_async_asyncio.py +++ b/test/test_client_async_asyncio.py @@ -11,9 +11,9 @@ ModbusClientProtocol, ) from pymodbus.client import ( - AsyncModbusUDPClient, - AsyncModbusTLSClient, - AsyncModbusTCPClient, + AsyncModbusUdpClient, + AsyncModbusTlsClient, + AsyncModbusTcpClient, ) from pymodbus.client.async_udp import ModbusUdpClientProtocol from pymodbus.exceptions import ConnectionException @@ -22,7 +22,7 @@ protocols = [ BaseModbusAsyncClientProtocol, - ModbusUdpClientProtocol, + # ModbusUdpClientProtocol, ModbusClientProtocol, ] @@ -84,7 +84,7 @@ def test_protocol_connection_state_propagation_to_factory( async def test_factory_initialization_state(self): """Test factory initialization state.""" mock_protocol_class = mock.MagicMock() - client = AsyncModbusTCPClient( + client = AsyncModbusTcpClient( "127.0.0.1", protocol_class=mock_protocol_class ) @@ -94,22 +94,22 @@ async def test_factory_initialization_state(self): async def test_initialization_tcp_in_loop(self): """Test initialization tcp in loop.""" - client = AsyncModbusTCPClient("127.0.0.1", port=5020) + client = AsyncModbusTcpClient("127.0.0.1", port=5020) assert not client.connected # nosec assert client.port == 5020 # nosec assert client.delay_ms < client.DELAY_MAX_MS # nosec async def test_initialization_udp_in_loop(self): """Test initialization udp in loop.""" - client = AsyncModbusUDPClient("127.0.0.1") + client = AsyncModbusUdpClient("127.0.0.1") await client.start() - assert client.connected # nosec + # TBD assert client.connected # nosec assert client.port == 502 # nosec assert client.delay_ms < client.DELAY_MAX_MS # nosec async def test_initialization_tls_in_loop(self): """Test initialization tls in loop.""" - client = AsyncModbusTLSClient("127.0.0.1") + client = AsyncModbusTlsClient("127.0.0.1") assert not client.connected # nosec assert client.port == 802 # nosec assert client.delay_ms < client.DELAY_MAX_MS # nosec @@ -117,7 +117,7 @@ async def test_initialization_tls_in_loop(self): async def test_factory_reset_wait_before_reconnect(self): """Test factory reset wait before reconnect.""" mock_protocol_class = mock.MagicMock() - client = AsyncModbusTCPClient( + client = AsyncModbusTcpClient( "127.0.0.1", protocol_class=mock_protocol_class ) @@ -132,7 +132,7 @@ async def test_factory_reset_wait_before_reconnect(self): async def test_factory_stop(self): """Test factory stop.""" mock_protocol_class = mock.MagicMock() - client = AsyncModbusTCPClient( + client = AsyncModbusTcpClient( "127.0.0.1", protocol_class=mock_protocol_class ) @@ -150,7 +150,7 @@ async def test_factory_stop(self): async def test_factory_protocol_made_connection(self): """Test factory protocol made connection.""" mock_protocol_class = mock.MagicMock() - client = AsyncModbusTCPClient( + client = AsyncModbusTcpClient( "127.0.0.1", protocol_class=mock_protocol_class ) @@ -167,7 +167,7 @@ async def test_factory_protocol_made_connection(self): async def test_factory_protocol_lost_connection(self): """Test factory protocol lost connection.""" mock_protocol_class = mock.MagicMock() - client = AsyncModbusTCPClient( + client = AsyncModbusTcpClient( "127.0.0.1", protocol_class=mock_protocol_class ) @@ -185,7 +185,7 @@ async def test_factory_protocol_lost_connection(self): client.connected = True with mock.patch( "pymodbus.client.async_tcp." - "AsyncModbusTCPClient._reconnect" + "AsyncModbusTcpClient._reconnect" ) as mock_reconnect: mock_reconnect.return_value = mock.sentinel.RECONNECT_GENERATOR @@ -196,7 +196,7 @@ async def test_factory_protocol_lost_connection(self): async def test_factory_start_success(self): """Test factory start success.""" mock_protocol_class = mock.MagicMock() - client = AsyncModbusTCPClient( + client = AsyncModbusTcpClient( mock.sentinel.HOST, port=mock.sentinel.PORT, protocol_class=mock_protocol_class @@ -211,7 +211,7 @@ async def test_factory_start_failing_and_retried(self, mock_async): # pylint: d loop.create_connection = mock.MagicMock( side_effect=Exception("Did not work.") ) - client = AsyncModbusTCPClient( + client = AsyncModbusTcpClient( mock.sentinel.HOST, port=mock.sentinel.PORT, protocol_class=mock_protocol_class @@ -220,7 +220,7 @@ async def test_factory_start_failing_and_retried(self, mock_async): # pylint: d # check whether reconnect is called upon failed connection attempt: with mock.patch( "pymodbus.client.async_tcp" - ".AsyncModbusTCPClient._reconnect" + ".AsyncModbusTcpClient._reconnect" ) as mock_reconnect: mock_reconnect.return_value = mock.sentinel.RECONNECT_GENERATOR run_coroutine(client.start()) @@ -233,7 +233,7 @@ async def test_factory_reconnect(self, mock_sleep): mock_sleep.side_effect = return_as_coroutine() loop = asyncio.get_running_loop() loop.create_connection = mock.MagicMock(return_value=(None, None)) - client = AsyncModbusTCPClient( + client = AsyncModbusTcpClient( "127.0.0.1", protocol_class=mock_protocol_class ) @@ -280,8 +280,8 @@ def test_client_protocol_close( def test_client_protocol_connection_lost(self): """Test the client protocol connection lost""" for protocol in protocols: - framer = ModbusSocketFramer(None) - protocol = protocol(framer=framer, timeout=0) + framer = ModbusSocketFramer + protocol = protocol("127.0.0.1", framer=framer, timeout=0) protocol.execute = mock.MagicMock() transport = mock.MagicMock() factory = mock.MagicMock() @@ -315,7 +315,7 @@ async def test_client_protocol_data_received(self): 0x00 ) if isinstance(protocol, ModbusUdpClientProtocol): - protocol.datagram_received(data, None) + protocol.datagram_received(data, None) # pylint: disable=no-member else: protocol.data_received(data) result = response.result() @@ -324,8 +324,8 @@ async def test_client_protocol_data_received(self): async def test_client_protocol_execute(self): """Test the client protocol execute method""" for protocol in protocols: - framer = ModbusSocketFramer(None) - protocol = protocol(framer=framer) + framer = ModbusSocketFramer(ClientDecoder()) + protocol = protocol("127.0.0.1", framer=framer) protocol.create_future = mock.MagicMock() fut = asyncio.Future() fut.set_result(fut) diff --git a/test/test_client_sync.py b/test/test_client_sync.py index f9eac7b5c..c30c45244 100755 --- a/test/test_client_sync.py +++ b/test/test_client_sync.py @@ -152,13 +152,13 @@ def test_base_modbus_client(self): def test_sync_udp_client_instantiation(self): """Test sync udp clientt.""" - client = ModbusUdpClient() + client = ModbusUdpClient("127.0.0.1") self.assertNotEqual(client, None) def tes_basic_sync_udp_client(self): """Test the basic methods for the udp sync client""" # receive/send - client = ModbusUdpClient() + client = ModbusUdpClient("127.0.0.1") client.socket = mockSocket() self.assertEqual(0, client._send(None)) # pylint: disable=protected-access self.assertEqual(1, client._send(b"\x00")) # pylint: disable=protected-access @@ -176,7 +176,7 @@ def tes_basic_sync_udp_client(self): def test_udp_client_address_family(self): """Test the Udp client get address family method""" - client = ModbusUdpClient() + client = ModbusUdpClient("127.0.0.1") self.assertEqual( socket.AF_INET, client._get_address_family("127.0.0.1"), # pylint: disable=protected-access @@ -197,22 +197,22 @@ def settimeout(self, *a, **kwa): """Set timeout.""" mock_method.return_value = DummySocket() - client = ModbusUdpClient() + client = ModbusUdpClient("127.0.0.1") self.assertTrue(client.connect()) with patch.object(socket, "socket") as mock_method: mock_method.side_effect = socket.error() - client = ModbusUdpClient() + client = ModbusUdpClient("127.0.0.1") self.assertFalse(client.connect()) def test_udp_client_is_socket_open(self): """Test the udp client is_socket_open method""" - client = ModbusUdpClient() + client = ModbusUdpClient("127.0.0.1") self.assertTrue(client.is_socket_open()) def test_udp_client_send(self): """Test the udp client send method""" - client = ModbusUdpClient() + client = ModbusUdpClient("127.0.0.1") self.assertRaises( ConnectionException, lambda: client._send(None), # pylint: disable=protected-access @@ -224,7 +224,7 @@ def test_udp_client_send(self): def test_udp_client_recv(self): """Test the udp client receive method""" - client = ModbusUdpClient() + client = ModbusUdpClient("127.0.0.1") self.assertRaises( ConnectionException, lambda: client._recv(1024), # pylint: disable=protected-access @@ -238,7 +238,7 @@ def test_udp_client_recv(self): def test_udp_client_repr(self): """Test udp client representation.""" - client = ModbusUdpClient() + client = ModbusUdpClient("127.0.0.1") rep = ( f"<{client.__class__.__name__} at {hex(id(client))} socket={client.socket}, " f"ipaddr={client.host}, port={client.port}, timeout={client.timeout}>" @@ -251,7 +251,7 @@ def test_udp_client_repr(self): def test_sync_tcp_client_instantiation(self): """Test sync tcp client.""" - client = ModbusTcpClient() + client = ModbusTcpClient("127.0.0.1") self.assertNotEqual(client, None) @patch("pymodbus.client.sync_tcp.select") @@ -259,7 +259,7 @@ def test_basic_sync_tcp_client(self, mock_select): """Test the basic methods for the tcp sync client""" # receive/send mock_select.select.return_value = [True] - client = ModbusTcpClient() + client = ModbusTcpClient("127.0.0.1") client.socket = mockSocket() self.assertEqual(0, client._send(None)) # pylint: disable=protected-access self.assertEqual(1, client._send(b"\x00")) # pylint: disable=protected-access @@ -280,23 +280,23 @@ def test_tcp_client_connect(self): with patch.object(socket, "create_connection") as mock_method: _socket = MagicMock() mock_method.return_value = _socket - client = ModbusTcpClient() + client = ModbusTcpClient("127.0.0.1") _socket.getsockname.return_value = ("dmmy", 1234) self.assertTrue(client.connect()) with patch.object(socket, "create_connection") as mock_method: mock_method.side_effect = socket.error() - client = ModbusTcpClient() + client = ModbusTcpClient("127.0.0.1") self.assertFalse(client.connect()) def test_tcp_client_is_socket_open(self): """Test the tcp client is_socket_open method""" - client = ModbusTcpClient() + client = ModbusTcpClient("127.0.0.1") self.assertFalse(client.is_socket_open()) def test_tcp_client_send(self): """Test the tcp client send method""" - client = ModbusTcpClient() + client = ModbusTcpClient("127.0.0.1") self.assertRaises( ConnectionException, lambda: client._send(None), # pylint: disable=protected-access @@ -312,7 +312,7 @@ def test_tcp_client_recv(self, mock_select, mock_time): """Test the tcp client receive method""" mock_select.select.return_value = [True] mock_time.time.side_effect = count() - client = ModbusTcpClient() + client = ModbusTcpClient("127.0.0.1") self.assertRaises( ConnectionException, lambda: client._recv(1024), # pylint: disable=protected-access @@ -357,7 +357,7 @@ def test_tcp_client_recv(self, mock_select, mock_time): def test_tcp_client_repr(self): """Test tcp client.""" - client = ModbusTcpClient() + client = ModbusTcpClient("127.0.0.1") rep = ( f"<{client.__class__.__name__} at {hex(id(client))} socket={client.socket}, " f"ipaddr={client.host}, port={client.port}, timeout={client.timeout}>" @@ -372,7 +372,7 @@ class CustomRequest: # pylint: disable=too-few-public-methods function_code = 79 - client = ModbusTcpClient() + client = ModbusTcpClient("127.0.0.1") client.framer = Mock() client.register(CustomRequest) self.assertTrue(client.framer.decoder.register.called_once_with(CustomRequest)) @@ -406,7 +406,7 @@ def test_tls_sslctx_provider(self): def test_sync_tls_client_instantiation(self): """Test sync tls client.""" # default SSLContext - client = ModbusTlsClient() + client = ModbusTlsClient("127.0.0.1") self.assertNotEqual(client, None) self.assertIsInstance(client.framer, ModbusTlsFramer) self.assertTrue(client.sslctx) @@ -414,7 +414,7 @@ def test_sync_tls_client_instantiation(self): def test_basic_sync_tls_client(self): """Test the basic methods for the tls sync client""" # receive/send - client = ModbusTlsClient() + client = ModbusTlsClient("localhost") client.socket = mockSocket() self.assertEqual(0, client._send(None)) # pylint: disable=protected-access self.assertEqual(1, client._send(b"\x00")) # pylint: disable=protected-access @@ -433,17 +433,17 @@ def test_basic_sync_tls_client(self): def test_tls_client_connect(self): """Test the tls client connection method""" with patch.object(ssl.SSLSocket, "connect") as mock_method: - client = ModbusTlsClient() + client = ModbusTlsClient("127.0.0.1") self.assertTrue(client.connect()) with patch.object(socket, "create_connection") as mock_method: mock_method.side_effect = socket.error() - client = ModbusTlsClient() + client = ModbusTlsClient("127.0.0.1") self.assertFalse(client.connect()) def test_tls_client_send(self): """Test the tls client send method""" - client = ModbusTlsClient() + client = ModbusTlsClient("127.0.0.1") self.assertRaises( ConnectionException, lambda: client._send(None), # pylint: disable=protected-access @@ -456,7 +456,7 @@ def test_tls_client_send(self): @patch("pymodbus.client.sync_tls.time") def test_tls_client_recv(self, mock_time): """Test the tls client receive method""" - client = ModbusTlsClient() + client = ModbusTlsClient("127.0.0.1") self.assertRaises( ConnectionException, lambda: client._recv(1024), # pylint: disable=protected-access @@ -487,7 +487,7 @@ def test_tls_client_recv(self, mock_time): def test_tls_client_repr(self): """Test tls client.""" - client = ModbusTlsClient() + client = ModbusTlsClient("127.0.0.1") rep = ( f"<{client.__class__.__name__} at {hex(id(client))} socket={client.socket}, " f"ipaddr={client.host}, port={client.port}, sslctx={client.sslctx}, " @@ -503,7 +503,7 @@ class CustomeRequest: # pylint: disable=too-few-public-methods function_code = 79 - client = ModbusTlsClient() + client = ModbusTlsClient("127.0.0.1") client.framer = Mock() client.register(CustomeRequest) self.assertTrue(client.framer.decoder.register.called_once_with(CustomeRequest)) @@ -513,26 +513,26 @@ class CustomeRequest: # pylint: disable=too-few-public-methods # -----------------------------------------------------------------------# def test_sync_serial_client_instantiation(self): """Test sync serial client.""" - client = ModbusSerialClient() + client = ModbusSerialClient("/dev/null") self.assertNotEqual(client, None) self.assertTrue( - isinstance(ModbusSerialClient(framer=ModbusAsciiFramer).framer, ModbusAsciiFramer) + isinstance(ModbusSerialClient("/dev/null", framer=ModbusAsciiFramer).framer, ModbusAsciiFramer) ) self.assertTrue( - isinstance(ModbusSerialClient(framer=ModbusRtuFramer).framer, ModbusRtuFramer) + isinstance(ModbusSerialClient("/dev/null", framer=ModbusRtuFramer).framer, ModbusRtuFramer) ) self.assertTrue( - isinstance(ModbusSerialClient(framer=ModbusBinaryFramer).framer, ModbusBinaryFramer) + isinstance(ModbusSerialClient("/dev/null", framer=ModbusBinaryFramer).framer, ModbusBinaryFramer) ) self.assertTrue( - isinstance(ModbusSerialClient(framer=ModbusSocketFramer).framer, ModbusSocketFramer) + isinstance(ModbusSerialClient("/dev/null", framer=ModbusSocketFramer).framer, ModbusSocketFramer) ) def test_sync_serial_rtu_client_timeouts(self): """Test sync serial rtu.""" - client = ModbusSerialClient(framer=ModbusRtuFramer, baudrate=9600) + client = ModbusSerialClient("/dev/null", framer=ModbusRtuFramer, baudrate=9600) self.assertEqual(client.silent_interval, round((3.5 * 11 / 9600), 6)) - client = ModbusSerialClient(framer=ModbusRtuFramer, baudrate=38400) + client = ModbusSerialClient("/dev/null", framer=ModbusRtuFramer, baudrate=38400) self.assertEqual(client.silent_interval, round((1.75 / 1000), 6)) @patch("serial.Serial") @@ -543,7 +543,7 @@ def test_basic_sync_serial_client(self, mock_serial): mock_serial.write = lambda x: len(x) # pylint: disable=unnecessary-lambda mock_serial.read = lambda size: b"\x00" * size - client = ModbusSerialClient() + client = ModbusSerialClient("/dev/null") client.socket = mock_serial client.state = 0 self.assertEqual(0, client._send(None)) # pylint: disable=protected-access @@ -556,7 +556,7 @@ def test_basic_sync_serial_client(self, mock_serial): client.close() # rtu connect/disconnect - rtu_client = ModbusSerialClient(framer=ModbusRtuFramer, strict=True) + rtu_client = ModbusSerialClient("/dev/null", framer=ModbusRtuFramer, strict=True) self.assertTrue(rtu_client.connect()) self.assertEqual( rtu_client.socket.interCharTimeout, rtu_client.inter_char_timeout @@ -567,24 +567,24 @@ def test_basic_sync_serial_client(self, mock_serial): client.socket = False client.close() - self.assertTrue("baud[19200])" in str(client)) + self.assertTrue("baud[9600])" in str(client)) def test_serial_client_connect(self): """Test the serial client connection method""" with patch.object(serial, "Serial") as mock_method: mock_method.return_value = MagicMock() - client = ModbusSerialClient() + client = ModbusSerialClient("/dev/null") self.assertTrue(client.connect()) with patch.object(serial, "Serial") as mock_method: mock_method.side_effect = serial.SerialException() - client = ModbusSerialClient() + client = ModbusSerialClient("/dev/null") self.assertFalse(client.connect()) @patch("serial.Serial") def test_serial_client_is_socket_open(self, mock_serial): """Test the serial client is_socket_open method""" - client = ModbusSerialClient() + client = ModbusSerialClient("/dev/null") self.assertFalse(client.is_socket_open()) client.socket = mock_serial self.assertTrue(client.is_socket_open()) @@ -594,7 +594,7 @@ def test_serial_client_send(self, mock_serial): """Test the serial client send method""" mock_serial.in_waiting = None mock_serial.write = lambda x: len(x) # pylint: disable=unnecessary-lambda - client = ModbusSerialClient() + client = ModbusSerialClient("/dev/null") self.assertRaises( ConnectionException, lambda: client._send(None), # pylint: disable=protected-access @@ -612,7 +612,7 @@ def test_serial_client_cleanup_buffer_before_send(self, mock_serial): mock_serial.in_waiting = 4 mock_serial.read = lambda x: b"1" * x mock_serial.write = lambda x: len(x) # pylint: disable=unnecessary-lambda - client = ModbusSerialClient() + client = ModbusSerialClient("/dev/null") self.assertRaises( ConnectionException, lambda: client._send(None), # pylint: disable=protected-access @@ -626,7 +626,7 @@ def test_serial_client_cleanup_buffer_before_send(self, mock_serial): def test_serial_client_recv(self): """Test the serial client receive method""" - client = ModbusSerialClient() + client = ModbusSerialClient("/dev/null") self.assertRaises( ConnectionException, lambda: client._recv(1024), # pylint: disable=protected-access @@ -647,7 +647,7 @@ def test_serial_client_recv(self): def test_serial_client_repr(self): """Test serial client.""" - client = ModbusSerialClient() + client = ModbusSerialClient("/dev/null") rep = ( f"<{client.__class__.__name__} at {hex(id(client))} socket={client.socket}, " f"framer={client.framer}, timeout={client.timeout}>"