From 4ddb9b136838da6a35f755252f317e4a3df7fbe8 Mon Sep 17 00:00:00 2001 From: jan iversen Date: Tue, 17 Oct 2023 16:04:20 +0200 Subject: [PATCH] Client/Server framer as enum. (#1822) --- API_changes.rst | 1 + examples/client_custom_msg.py | 8 ++--- examples/client_performance.py | 6 ++-- examples/datastore_simulator.py | 5 ++- examples/helper.py | 22 ++----------- examples/server_hook.py | 5 ++- examples/simple_async_client.py | 21 +++--------- examples/simple_sync_client.py | 17 +++------- examples/simulator.py | 4 +-- pymodbus/__init__.py | 2 ++ pymodbus/client/base.py | 41 +++++++++++++----------- pymodbus/client/mixin.py | 22 +++++++------ pymodbus/client/serial.py | 26 +++++++-------- pymodbus/client/tcp.py | 28 +++++++++------- pymodbus/client/tls.py | 15 +++++---- pymodbus/client/udp.py | 32 +++++++++++------- pymodbus/framer/__init__.py | 25 ++++++++++++++- pymodbus/server/async_io.py | 27 +++++++--------- pymodbus/server/simulator/http_server.py | 16 +-------- test/sub_client/test_client.py | 39 ++++++++++------------ test/sub_client/test_client_sync.py | 17 +++++----- test/sub_examples/test_examples.py | 7 ++-- test/sub_server/test_server_asyncio.py | 8 ++--- test/test_framers.py | 3 +- 24 files changed, 189 insertions(+), 208 deletions(-) diff --git a/API_changes.rst b/API_changes.rst index 2ed0d7168..335ea73a4 100644 --- a/API_changes.rst +++ b/API_changes.rst @@ -4,6 +4,7 @@ Versions (X.Y.Z) where Z > 0 e.g. 3.0.1 do NOT have API changes! API changes 3.6.0 (future) -------------------------- +- framer= is an enum: pymodbus.Framer, but still accept a framer class API changes 3.5.0 diff --git a/examples/client_custom_msg.py b/examples/client_custom_msg.py index b8c3f403b..b42f809bd 100755 --- a/examples/client_custom_msg.py +++ b/examples/client_custom_msg.py @@ -14,14 +14,10 @@ import logging import struct +from pymodbus import Framer from pymodbus.bit_read_message import ReadCoilsRequest from pymodbus.client import AsyncModbusTcpClient as ModbusClient - -# --------------------------------------------------------------------------- # -# import the various server implementations -# --------------------------------------------------------------------------- # from pymodbus.pdu import ModbusExceptions, ModbusRequest, ModbusResponse -from pymodbus.transaction import ModbusSocketFramer # --------------------------------------------------------------------------- # @@ -130,7 +126,7 @@ def __init__(self, address, **kwargs): async def main(host="localhost", port=5020): """Run versions of read coil.""" - with ModbusClient(host=host, port=port, framer=ModbusSocketFramer) as client: + with ModbusClient(host=host, port=port, framer_name=Framer.SOCKET) as client: await client.connect() # new modbus function code. diff --git a/examples/client_performance.py b/examples/client_performance.py index 50a8b1e32..77f912dd9 100755 --- a/examples/client_performance.py +++ b/examples/client_performance.py @@ -16,8 +16,8 @@ import asyncio import time +from pymodbus import Framer from pymodbus.client import AsyncModbusSerialClient, ModbusSerialClient -from pymodbus.transaction import ModbusRtuFramer LOOP_COUNT = 1000 @@ -29,7 +29,7 @@ def run_sync_client_test(): print("--- Testing sync client v3.4.1") client = ModbusSerialClient( "/dev/ttys007", - framer=ModbusRtuFramer, + framer_name=Framer.RTU, baudrate=9600, ) client.connect() @@ -56,7 +56,7 @@ async def run_async_client_test(): print("--- Testing async client v3.4.1") client = AsyncModbusSerialClient( "/dev/ttys007", - framer=ModbusRtuFramer, + framer_name=Framer.RTU, baudrate=9600, ) await client.connect() diff --git a/examples/datastore_simulator.py b/examples/datastore_simulator.py index 63519ace0..e97ab2ebb 100755 --- a/examples/datastore_simulator.py +++ b/examples/datastore_simulator.py @@ -25,11 +25,10 @@ import asyncio import logging -from pymodbus import pymodbus_apply_logging_config +from pymodbus import Framer, pymodbus_apply_logging_config from pymodbus.datastore import ModbusServerContext, ModbusSimulatorContext from pymodbus.device import ModbusDeviceIdentification from pymodbus.server import StartAsyncTcpServer -from pymodbus.transaction import ModbusSocketFramer logging.basicConfig() @@ -141,7 +140,7 @@ def setup_simulator(setup=None, actions=None, cmdline=None): args = get_commandline(cmdline=cmdline) pymodbus_apply_logging_config(args.log.upper()) _logger.setLevel(args.log.upper()) - args.framer = ModbusSocketFramer + args.framer = Framer.SOCKET args.port = int(args.port) _logger.info("### Create datastore") diff --git a/examples/helper.py b/examples/helper.py index e43216c74..40741ae73 100755 --- a/examples/helper.py +++ b/examples/helper.py @@ -9,30 +9,11 @@ import os from pymodbus import pymodbus_apply_logging_config -from pymodbus.transaction import ( - ModbusAsciiFramer, - ModbusBinaryFramer, - ModbusRtuFramer, - ModbusSocketFramer, - ModbusTlsFramer, -) _logger = logging.getLogger(__file__) -def get_framer(framer): - """Convert framer name to framer class""" - framers = { - "ascii": ModbusAsciiFramer, - "binary": ModbusBinaryFramer, - "rtu": ModbusRtuFramer, - "socket": ModbusSocketFramer, - "tls": ModbusTlsFramer, - } - return framers[framer] - - def get_commandline(server=False, description=None, extras=None, cmdline=None): """Read and validate command line arguments""" parser = argparse.ArgumentParser(description=description) @@ -123,7 +104,8 @@ def get_commandline(server=False, description=None, extras=None, cmdline=None): } pymodbus_apply_logging_config(args.log.upper()) _logger.setLevel(args.log.upper()) - args.framer = get_framer(args.framer or comm_defaults[args.comm][0]) + if not args.framer: + args.framer = comm_defaults[args.comm][0] args.port = args.port or comm_defaults[args.comm][1] if args.comm != "serial" and args.port: args.port = int(args.port) diff --git a/examples/server_hook.py b/examples/server_hook.py index 199a9a5a3..865e5819a 100755 --- a/examples/server_hook.py +++ b/examples/server_hook.py @@ -7,14 +7,13 @@ import asyncio import logging -from pymodbus import pymodbus_apply_logging_config +from pymodbus import Framer, pymodbus_apply_logging_config from pymodbus.datastore import ( ModbusSequentialDataBlock, ModbusServerContext, ModbusSlaveContext, ) from pymodbus.server import ModbusTcpServer -from pymodbus.transaction import ModbusSocketFramer class Manipulator: @@ -64,7 +63,7 @@ async def setup(self): ) self.server = ModbusTcpServer( context, - ModbusSocketFramer, + Framer.SOCKET, None, ("127.0.0.1", 5020), request_tracer=self.server_request_tracer, diff --git a/examples/simple_async_client.py b/examples/simple_async_client.py index aa02bd621..12352e16a 100755 --- a/examples/simple_async_client.py +++ b/examples/simple_async_client.py @@ -11,11 +11,7 @@ """ import asyncio -from pymodbus import pymodbus_apply_logging_config - -# --------------------------------------------------------------------------- # -# import the various client implementations -# --------------------------------------------------------------------------- # +from pymodbus import Framer, pymodbus_apply_logging_config from pymodbus.client import ( AsyncModbusSerialClient, AsyncModbusTcpClient, @@ -24,16 +20,9 @@ ) from pymodbus.exceptions import ModbusException from pymodbus.pdu import ExceptionResponse -from pymodbus.transaction import ( - # ModbusAsciiFramer, - # ModbusBinaryFramer, - ModbusRtuFramer, - ModbusSocketFramer, - ModbusTlsFramer, -) -async def run_async_simple_client(comm, host, port, framer=ModbusSocketFramer): +async def run_async_simple_client(comm, host, port, framer=Framer.SOCKET): """Run async client.""" # activate debugging @@ -56,7 +45,7 @@ async def run_async_simple_client(comm, host, port, framer=ModbusSocketFramer): client = AsyncModbusUdpClient( host, port=port, - framer=ModbusSocketFramer, + framer=framer, # timeout=10, # retries=3, # retry_on_empty=False, @@ -67,7 +56,7 @@ async def run_async_simple_client(comm, host, port, framer=ModbusSocketFramer): elif comm == "serial": client = AsyncModbusSerialClient( port, - framer=ModbusRtuFramer, + framer=framer, # timeout=10, # retries=3, # retry_on_empty=False, @@ -83,7 +72,7 @@ async def run_async_simple_client(comm, host, port, framer=ModbusSocketFramer): client = AsyncModbusTlsClient( host, port=port, - framer=ModbusTlsFramer, + framer=Framer.TLS, # timeout=10, # retries=3, # retry_on_empty=False, diff --git a/examples/simple_sync_client.py b/examples/simple_sync_client.py index 3dd4184e7..b7792f560 100755 --- a/examples/simple_sync_client.py +++ b/examples/simple_sync_client.py @@ -13,7 +13,7 @@ # --------------------------------------------------------------------------- # # import the various client implementations # --------------------------------------------------------------------------- # -from pymodbus import pymodbus_apply_logging_config +from pymodbus import Framer, pymodbus_apply_logging_config from pymodbus.client import ( ModbusSerialClient, ModbusTcpClient, @@ -22,16 +22,9 @@ ) from pymodbus.exceptions import ModbusException from pymodbus.pdu import ExceptionResponse -from pymodbus.transaction import ( - # ModbusAsciiFramer, - # ModbusBinaryFramer, - ModbusRtuFramer, - ModbusSocketFramer, - ModbusTlsFramer, -) -def run_sync_simple_client(comm, host, port, framer=ModbusSocketFramer): +def run_sync_simple_client(comm, host, port, framer=Framer.SOCKET): """Run sync client.""" # activate debugging @@ -54,7 +47,7 @@ def run_sync_simple_client(comm, host, port, framer=ModbusSocketFramer): client = ModbusUdpClient( host, port=port, - framer=ModbusSocketFramer, + framer=framer, # timeout=10, # retries=3, # retry_on_empty=False, @@ -65,7 +58,7 @@ def run_sync_simple_client(comm, host, port, framer=ModbusSocketFramer): elif comm == "serial": client = ModbusSerialClient( port, - framer=ModbusRtuFramer, + framer=framer, # timeout=10, # retries=3, # retry_on_empty=False, @@ -81,7 +74,7 @@ def run_sync_simple_client(comm, host, port, framer=ModbusSocketFramer): client = ModbusTlsClient( host, port=port, - framer=ModbusTlsFramer, + framer=Framer.TLS, # timeout=10, # retries=3, # retry_on_empty=False, diff --git a/examples/simulator.py b/examples/simulator.py index ed64f2526..8897526b8 100755 --- a/examples/simulator.py +++ b/examples/simulator.py @@ -10,10 +10,10 @@ import asyncio import logging +from pymodbus import Framer from pymodbus.client import AsyncModbusTcpClient from pymodbus.datastore import ModbusSimulatorContext from pymodbus.server import ModbusSimulatorServer, get_simulator_commandline -from pymodbus.transaction import ModbusSocketFramer logging.basicConfig() @@ -75,7 +75,7 @@ async def run_simulator(): client = AsyncModbusTcpClient( "127.0.0.1", port=5020, - framer=ModbusSocketFramer, + framer=Framer.SOCKET, ) await client.connect() assert client.connected diff --git a/pymodbus/__init__.py b/pymodbus/__init__.py index 84a321fee..f52caa7bd 100644 --- a/pymodbus/__init__.py +++ b/pymodbus/__init__.py @@ -4,11 +4,13 @@ """ __all__ = [ + "Framer", "pymodbus_apply_logging_config", "__version__", "__version_full__", ] +from pymodbus.framer import Framer from pymodbus.logging import pymodbus_apply_logging_config diff --git a/pymodbus/client/base.py b/pymodbus/client/base.py index a00309cd0..a3b85ae59 100644 --- a/pymodbus/client/base.py +++ b/pymodbus/client/base.py @@ -4,12 +4,12 @@ import asyncio import socket from dataclasses import dataclass -from typing import Any, Callable +from typing import Any, Callable, Type, cast from pymodbus.client.mixin import ModbusClientMixin from pymodbus.exceptions import ConnectionException, ModbusIOException from pymodbus.factory import ClientDecoder -from pymodbus.framer import ModbusFramer +from pymodbus.framer import FRAMER_NAME_TO_CLASS, Framer, ModbusFramer from pymodbus.logging import Log from pymodbus.pdu import ModbusRequest, ModbusResponse from pymodbus.transaction import DictTransactionManager @@ -20,20 +20,23 @@ class ModbusBaseClient(ModbusClientMixin, ModbusProtocol): """**ModbusBaseClient** - **Parameters common to all clients**: - - :param framer: (optional) Modbus Framer class. - :param timeout: (optional) Timeout for a request, in seconds. - :param retries: (optional) Max number of retries per request. - :param retry_on_empty: (optional) Retry on empty response. - :param close_comm_on_error: (optional) Close connection on error. - :param strict: (optional) Strict timing, 1.5 character between requests. - :param broadcast_enable: (optional) True to treat id 0 as broadcast address. - :param reconnect_delay: (optional) Minimum delay in milliseconds before reconnecting. - :param reconnect_delay_max: (optional) Maximum delay in milliseconds before reconnecting. - :param on_reconnect_callback: (optional) Function that will be called just before a reconnection attempt. - :param no_resend_on_retry: (optional) Do not resend request when retrying due to missing response. - :param kwargs: (optional) Experimental parameters. + Fixed parameters: + + :param framer: Framer enum name + + Optional parameters: + + :param timeout: Timeout for a request, in seconds. + :param retries: Max number of retries per request. + :param retry_on_empty: Retry on empty response. + :param close_comm_on_error: Close connection on error. + :param strict: Strict timing, 1.5 character between requests. + :param broadcast_enable: True to treat id 0 as broadcast address. + :param reconnect_delay: Minimum delay in milliseconds before reconnecting. + :param reconnect_delay_max: Maximum delay in milliseconds before reconnecting. + :param on_reconnect_callback: Function that will be called just before a reconnection attempt. + :param no_resend_on_retry: Do not resend request when retrying due to missing response. + :param kwargs: Experimental parameters. .. tip:: **reconnect_delay** doubles automatically with each unsuccessful connect, from @@ -62,7 +65,7 @@ class _params: def __init__( # pylint: disable=too-many-arguments self, - framer: type[ModbusFramer] = None, + framer: Framer, timeout: float = 3, retries: int = 3, retry_on_empty: bool = False, @@ -114,7 +117,9 @@ def __init__( # pylint: disable=too-many-arguments self.slaves: list[int] = [] # Common variables. - self.framer = framer(ClientDecoder(), self) + self.framer = FRAMER_NAME_TO_CLASS.get( + framer, cast(Type[ModbusFramer], framer) + )(ClientDecoder(), self) self.transaction = DictTransactionManager( self, retries=retries, retry_on_empty=retry_on_empty, **kwargs ) diff --git a/pymodbus/client/mixin.py b/pymodbus/client/mixin.py index 64b41b6bb..f1bd823ee 100644 --- a/pymodbus/client/mixin.py +++ b/pymodbus/client/mixin.py @@ -1,7 +1,9 @@ """Modbus Client Common.""" +from __future__ import annotations + import struct from enum import Enum -from typing import Any, List, Tuple, Union +from typing import Any import pymodbus.bit_read_message as pdu_bit_read import pymodbus.bit_write_message as pdu_bit_write @@ -381,7 +383,7 @@ def diag_get_comm_event_log(self, **kwargs: Any) -> ModbusResponse: def write_coils( self, address: int, - values: Union[List[bool], bool], + values: list[bool] | bool, slave: int = 0, **kwargs: Any, ) -> ModbusResponse: @@ -398,7 +400,7 @@ def write_coils( ) def write_registers( - self, address: int, values: Union[List[int], int], slave: int = 0, **kwargs: Any + self, address: int, values: list[int] | int, slave: int = 0, **kwargs: Any ) -> ModbusResponse: """Write registers (code 0x10). @@ -423,7 +425,7 @@ def report_slave_id(self, slave: int = 0, **kwargs: Any) -> ModbusResponse: """ return self.execute(pdu_other_msg.ReportSlaveIdRequest(slave, **kwargs)) - def read_file_record(self, records: List[Tuple], **kwargs: Any) -> ModbusResponse: + def read_file_record(self, records: list[tuple], **kwargs: Any) -> ModbusResponse: """Read file record (code 0x14). :param records: List of (Reference type, File number, Record Number, Record Length) @@ -432,7 +434,7 @@ def read_file_record(self, records: List[Tuple], **kwargs: Any) -> ModbusRespons """ return self.execute(pdu_file_msg.ReadFileRecordRequest(records, **kwargs)) - def write_file_record(self, records: List[Tuple], **kwargs: Any) -> ModbusResponse: + def write_file_record(self, records: list[tuple], **kwargs: Any) -> ModbusResponse: """Write file record (code 0x15). :param records: List of (Reference type, File number, Record Number, Record Length) @@ -465,7 +467,7 @@ def readwrite_registers( read_address: int = 0, read_count: int = 0, write_address: int = 0, - values: Union[List[int], int] = 0, + values: list[int] | int = 0, slave: int = 0, **kwargs, ) -> ModbusResponse: @@ -534,8 +536,8 @@ class DATATYPE(Enum): @classmethod def convert_from_registers( - cls, registers: List[int], data_type: DATATYPE - ) -> Union[int, float, str]: + cls, registers: list[int], data_type: DATATYPE + ) -> int | float | str: """Convert registers to int/float/str. :param registers: list of registers received from e.g. read_holding_registers() @@ -558,8 +560,8 @@ def convert_from_registers( @classmethod def convert_to_registers( - cls, value: Union[int, float, str], data_type: DATATYPE - ) -> List[int]: + cls, value: int | float | str, data_type: DATATYPE + ) -> list[int]: """Convert int/float/str to registers (16/32/64 bit). :param value: value to be converted diff --git a/pymodbus/client/serial.py b/pymodbus/client/serial.py index 240b827d4..3321584fb 100644 --- a/pymodbus/client/serial.py +++ b/pymodbus/client/serial.py @@ -1,14 +1,15 @@ """Modbus client async serial communication.""" +from __future__ import annotations + import asyncio import time from contextlib import suppress from functools import partial -from typing import Any, Type +from typing import Any from pymodbus.client.base import ModbusBaseClient from pymodbus.exceptions import ConnectionException -from pymodbus.framer import ModbusFramer -from pymodbus.framer.rtu_framer import ModbusRtuFramer +from pymodbus.framer import Framer from pymodbus.logging import Log from pymodbus.transport import CommType from pymodbus.utilities import ModbusTransactionState @@ -27,7 +28,6 @@ class AsyncModbusSerialClient(ModbusBaseClient, asyncio.Protocol): Optional parameters: - :param framer: Framer class. :param baudrate: Bits per second. :param bytesize: Number of bits per byte 7-8. :param parity: 'E'ven, 'O'dd or 'N'one @@ -36,6 +36,7 @@ class AsyncModbusSerialClient(ModbusBaseClient, asyncio.Protocol): Common optional parameters: + :param framer: Framer enum name :param timeout: Timeout for a request, in seconds. :param retries: Max number of retries per request. :param retry_on_empty: Retry on empty response. @@ -65,7 +66,7 @@ async def run(): def __init__( self, port: str, - framer: Type[ModbusFramer] = ModbusRtuFramer, + framer: Framer = Framer.RTU, baudrate: int = 19200, bytesize: int = 8, parity: str = "N", @@ -76,7 +77,7 @@ def __init__( asyncio.Protocol.__init__(self) ModbusBaseClient.__init__( self, - framer=framer, + framer, CommType=CommType.SERIAL, host=port, baudrate=baudrate, @@ -106,7 +107,6 @@ class ModbusSerialClient(ModbusBaseClient): Optional parameters: - :param framer: Framer class. :param baudrate: Bits per second. :param bytesize: Number of bits per byte 7-8. :param parity: 'E'ven, 'O'dd or 'N'one @@ -115,6 +115,7 @@ class ModbusSerialClient(ModbusBaseClient): Common optional parameters: + :param framer: Framer enum name :param timeout: Timeout for a request, in seconds. :param retries: Max number of retries per request. :param retry_on_empty: Retry on empty response. @@ -150,7 +151,7 @@ def run(): def __init__( self, port: str, - framer: Type[ModbusFramer] = ModbusRtuFramer, + framer: Framer = Framer.RTU, baudrate: int = 19200, bytesize: int = 8, parity: str = "N", @@ -162,7 +163,7 @@ def __init__( kwargs["use_sync"] = True ModbusBaseClient.__init__( self, - framer=framer, + framer, CommType=CommType.SERIAL, host=port, baudrate=baudrate, @@ -207,10 +208,9 @@ def connect(self): # pylint: disable=invalid-overridden-method baudrate=self.comm_params.baudrate, parity=self.comm_params.parity, ) - if isinstance(self.framer, ModbusRtuFramer): - if self.params.strict: - self.socket.interCharTimeout = self.inter_char_timeout - self.last_frame_end = None + if self.params.strict: + self.socket.interCharTimeout = self.inter_char_timeout + self.last_frame_end = None except serial.SerialException as msg: Log.error("{}", msg) self.close() diff --git a/pymodbus/client/tcp.py b/pymodbus/client/tcp.py index 30347b4e5..4efb91d3b 100644 --- a/pymodbus/client/tcp.py +++ b/pymodbus/client/tcp.py @@ -1,14 +1,15 @@ """Modbus client async TCP communication.""" +from __future__ import annotations + import asyncio import select import socket import time -from typing import Any, Tuple, Type +from typing import Any from pymodbus.client.base import ModbusBaseClient from pymodbus.exceptions import ConnectionException -from pymodbus.framer import ModbusFramer -from pymodbus.framer.socket_framer import ModbusSocketFramer +from pymodbus.framer import Framer from pymodbus.logging import Log from pymodbus.transport import CommType from pymodbus.utilities import ModbusTransactionState @@ -24,11 +25,11 @@ class AsyncModbusTcpClient(ModbusBaseClient, asyncio.Protocol): Optional parameters: :param port: Port used for communication - :param framer: Framer class :param source_address: source address of client Common optional parameters: + :param framer: Framer enum name :param timeout: Timeout for a request, in seconds. :param retries: Max number of retries per request. :param retry_on_empty: Retry on empty response. @@ -59,8 +60,8 @@ def __init__( self, host: str, port: int = 502, - framer: Type[ModbusFramer] = ModbusSocketFramer, - source_address: Tuple[str, int] = None, + framer: Framer = Framer.SOCKET, + source_address: tuple[str, int] = None, **kwargs: Any, ) -> None: """Initialize Asyncio Modbus TCP Client.""" @@ -71,7 +72,7 @@ def __init__( kwargs["source_address"] = source_address ModbusBaseClient.__init__( self, - framer=framer, + framer, host=host, port=port, **kwargs, @@ -103,11 +104,11 @@ class ModbusTcpClient(ModbusBaseClient): Optional parameters: :param port: Port used for communication - :param framer: Framer class :param source_address: source address of client Common optional parameters: + :param framer: Framer enum name :param timeout: Timeout for a request, in seconds. :param retries: Max number of retries per request. :param retry_on_empty: Retry on empty response. @@ -140,8 +141,8 @@ def __init__( self, host: str, port: int = 502, - framer: Type[ModbusFramer] = ModbusSocketFramer, - source_address: Tuple[str, int] = None, + framer: Framer = Framer.SOCKET, + source_address: tuple[str, int] = None, **kwargs: Any, ) -> None: """Initialize Modbus TCP Client.""" @@ -149,7 +150,12 @@ def __init__( kwargs["CommType"] = CommType.TCP kwargs["use_sync"] = True self.transport = None - super().__init__(framer=framer, host=host, port=port, **kwargs) + super().__init__( + framer, + host=host, + port=port, + **kwargs, + ) self.params.source_address = source_address self.socket = None diff --git a/pymodbus/client/tls.py b/pymodbus/client/tls.py index 81a5d039a..3ad4fad47 100644 --- a/pymodbus/client/tls.py +++ b/pymodbus/client/tls.py @@ -1,11 +1,12 @@ """Modbus client async TLS communication.""" +from __future__ import annotations + import socket import ssl -from typing import Any, Type +from typing import Any from pymodbus.client.tcp import AsyncModbusTcpClient, ModbusTcpClient -from pymodbus.framer import ModbusFramer -from pymodbus.framer.tls_framer import ModbusTlsFramer +from pymodbus.framer import Framer from pymodbus.logging import Log from pymodbus.transport import CommParams, CommType @@ -20,7 +21,6 @@ class AsyncModbusTlsClient(AsyncModbusTcpClient): Optional parameters: :param port: Port used for communication - :param framer: Framer class :param source_address: Source address of client :param sslctx: SSLContext to use for TLS :param certfile: Cert file path for TLS server request @@ -30,6 +30,7 @@ class AsyncModbusTlsClient(AsyncModbusTcpClient): Common optional parameters: + :param framer: Framer enum name :param timeout: Timeout for a request, in seconds. :param retries: Max number of retries per request. :param retry_on_empty: Retry on empty response. @@ -60,7 +61,7 @@ def __init__( self, host: str, port: int = 802, - framer: Type[ModbusFramer] = ModbusTlsFramer, + framer: Framer = Framer.TLS, sslctx: ssl.SSLContext = None, certfile: str = None, keyfile: str = None, @@ -103,7 +104,6 @@ class ModbusTlsClient(ModbusTcpClient): Optional parameters: :param port: Port used for communication - :param framer: Framer class :param source_address: Source address of client :param sslctx: SSLContext to use for TLS :param certfile: Cert file path for TLS server request @@ -114,6 +114,7 @@ class ModbusTlsClient(ModbusTcpClient): Common optional parameters: + :param framer: Framer enum name :param timeout: Timeout for a request, in seconds. :param retries: Max number of retries per request. :param retry_on_empty: Retry on empty response. @@ -146,7 +147,7 @@ def __init__( self, host: str, port: int = 802, - framer: Type[ModbusFramer] = ModbusTlsFramer, + framer: Framer = Framer.TLS, sslctx: ssl.SSLContext = None, certfile: str = None, keyfile: str = None, diff --git a/pymodbus/client/udp.py b/pymodbus/client/udp.py index 8b60cbd2d..b130515be 100644 --- a/pymodbus/client/udp.py +++ b/pymodbus/client/udp.py @@ -1,12 +1,13 @@ """Modbus client async UDP communication.""" +from __future__ import annotations + import asyncio import socket -from typing import Any, Tuple, Type +from typing import Any from pymodbus.client.base import ModbusBaseClient from pymodbus.exceptions import ConnectionException -from pymodbus.framer import ModbusFramer -from pymodbus.framer.socket_framer import ModbusSocketFramer +from pymodbus.framer import Framer from pymodbus.logging import Log from pymodbus.transport import CommType @@ -26,11 +27,11 @@ class AsyncModbusUdpClient( Optional parameters: :param port: Port used for communication. - :param framer: Framer class. :param source_address: source address of client, Common optional parameters: + :param framer: Framer enum name :param timeout: Timeout for a request, in seconds. :param retries: Max number of retries per request. :param retry_on_empty: Retry on empty response. @@ -61,15 +62,20 @@ def __init__( self, host: str, port: int = 502, - framer: Type[ModbusFramer] = ModbusSocketFramer, - source_address: Tuple[str, int] = None, + framer: Framer = Framer.SOCKET, + source_address: tuple[str, int] = None, **kwargs: Any, ) -> None: """Initialize Asyncio Modbus UDP Client.""" asyncio.DatagramProtocol.__init__(self) asyncio.Protocol.__init__(self) ModbusBaseClient.__init__( - self, framer=framer, CommType=CommType.UDP, host=host, port=port, **kwargs + self, + framer, + CommType=CommType.UDP, + host=host, + port=port, + **kwargs, ) self.params.source_address = source_address @@ -102,11 +108,11 @@ class ModbusUdpClient(ModbusBaseClient): Optional parameters: :param port: Port used for communication. - :param framer: Framer class. :param source_address: source address of client, Common optional parameters: + :param framer: Framer enum name :param timeout: Timeout for a request, in seconds. :param retries: Max number of retries per request. :param retry_on_empty: Retry on empty response. @@ -139,15 +145,19 @@ def __init__( self, host: str, port: int = 502, - framer: Type[ModbusFramer] = ModbusSocketFramer, - source_address: Tuple[str, int] = None, + framer: Framer = Framer.SOCKET, + source_address: tuple[str, int] = None, **kwargs: Any, ) -> None: """Initialize Modbus UDP Client.""" kwargs["use_sync"] = True self.transport = None super().__init__( - framer=framer, port=port, host=host, CommType=CommType.UDP, **kwargs + framer, + port=port, + host=host, + CommType=CommType.UDP, + **kwargs, ) self.params.source_address = source_address diff --git a/pymodbus/framer/__init__.py b/pymodbus/framer/__init__.py index 94c840e5e..c2ac10902 100644 --- a/pymodbus/framer/__init__.py +++ b/pymodbus/framer/__init__.py @@ -1,6 +1,7 @@ """Framer""" - __all__ = [ + "Framer", + "FRAMER_NAME_TO_CLASS", "ModbusFramer", "ModbusAsciiFramer", "ModbusBinaryFramer", @@ -9,9 +10,31 @@ "ModbusTlsFramer", ] + +import enum + from pymodbus.framer.ascii_framer import ModbusAsciiFramer from pymodbus.framer.base import ModbusFramer from pymodbus.framer.binary_framer import ModbusBinaryFramer from pymodbus.framer.rtu_framer import ModbusRtuFramer from pymodbus.framer.socket_framer import ModbusSocketFramer from pymodbus.framer.tls_framer import ModbusTlsFramer + + +class Framer(str, enum.Enum): + """These represent the different framers.""" + + ASCII = "ascii" + BINARY = "binary" + RTU = "rtu" + SOCKET = "socket" + TLS = "tls" + + +FRAMER_NAME_TO_CLASS = { + Framer.ASCII: ModbusAsciiFramer, + Framer.BINARY: ModbusBinaryFramer, + Framer.RTU: ModbusRtuFramer, + Framer.SOCKET: ModbusSocketFramer, + Framer.TLS: ModbusTlsFramer, +} diff --git a/pymodbus/server/async_io.py b/pymodbus/server/async_io.py index 29e7f3902..a5bcb558b 100644 --- a/pymodbus/server/async_io.py +++ b/pymodbus/server/async_io.py @@ -11,15 +11,9 @@ from pymodbus.device import ModbusControlBlock, ModbusDeviceIdentification from pymodbus.exceptions import NoSuchSlaveException from pymodbus.factory import ServerDecoder -from pymodbus.framer import ModbusFramer +from pymodbus.framer import FRAMER_NAME_TO_CLASS, Framer, ModbusFramer from pymodbus.logging import Log from pymodbus.pdu import ModbusExceptions as merror -from pymodbus.transaction import ( - ModbusAsciiFramer, - ModbusRtuFramer, - ModbusSocketFramer, - ModbusTlsFramer, -) from pymodbus.transport import CommParams, CommType, ModbusProtocol @@ -283,7 +277,8 @@ def __init__( self.handle_local_echo = False if isinstance(identity, ModbusDeviceIdentification): self.control.Identity.update(identity) - self.framer = framer + + self.framer = FRAMER_NAME_TO_CLASS.get(framer, framer) self.serving: asyncio.Future = asyncio.Future() def callback_new_connection(self): @@ -323,7 +318,7 @@ class ModbusTcpServer(ModbusBaseServer): def __init__( self, context, - framer=ModbusSocketFramer, + framer=Framer.SOCKET, identity=None, address=("", 502), ignore_missing_slaves=False, @@ -383,7 +378,7 @@ class ModbusTlsServer(ModbusTcpServer): def __init__( # pylint: disable=too-many-arguments self, context, - framer=ModbusTlsFramer, + framer=Framer.TLS, identity=None, address=("", 502), sslctx=None, @@ -449,7 +444,7 @@ class ModbusUdpServer(ModbusBaseServer): def __init__( self, context, - framer=ModbusSocketFramer, + framer=Framer.SOCKET, identity=None, address=("", 502), ignore_missing_slaves=False, @@ -503,7 +498,7 @@ class ModbusSerialServer(ModbusBaseServer): """ def __init__( - self, context, framer=ModbusRtuFramer, identity=None, **kwargs + self, context, framer=Framer.RTU, identity=None, **kwargs ): # pragma: no cover """Initialize the socket server. @@ -625,7 +620,7 @@ async def StartAsyncTcpServer( # pylint: disable=invalid-name,dangerous-default """ kwargs.pop("host", None) server = ModbusTcpServer( - context, kwargs.pop("framer", ModbusSocketFramer), identity, address, **kwargs + context, kwargs.pop("framer", Framer.SOCKET), identity, address, **kwargs ) await _serverList.run(server, custom_functions) @@ -657,7 +652,7 @@ async def StartAsyncTlsServer( # pylint: disable=invalid-name,dangerous-default kwargs.pop("host", None) server = ModbusTlsServer( context, - kwargs.pop("framer", ModbusTlsFramer), + kwargs.pop("framer", Framer.TLS), identity, address, sslctx, @@ -687,7 +682,7 @@ async def StartAsyncUdpServer( # pylint: disable=invalid-name,dangerous-default """ kwargs.pop("host", None) server = ModbusUdpServer( - context, kwargs.pop("framer", ModbusSocketFramer), identity, address, **kwargs + context, kwargs.pop("framer", Framer.SOCKET), identity, address, **kwargs ) await _serverList.run(server, custom_functions) @@ -707,7 +702,7 @@ async def StartAsyncSerialServer( # pylint: disable=invalid-name,dangerous-defa :param kwargs: The rest """ server = ModbusSerialServer( - context, kwargs.pop("framer", ModbusAsciiFramer), identity=identity, **kwargs + context, kwargs.pop("framer", Framer.RTU), identity=identity, **kwargs ) await _serverList.run(server, custom_functions) diff --git a/pymodbus/server/simulator/http_server.py b/pymodbus/server/simulator/http_server.py index 08c005193..4ae7c40d1 100644 --- a/pymodbus/server/simulator/http_server.py +++ b/pymodbus/server/simulator/http_server.py @@ -24,13 +24,6 @@ ModbusTlsServer, ModbusUdpServer, ) -from pymodbus.transaction import ( - ModbusAsciiFramer, - ModbusBinaryFramer, - ModbusRtuFramer, - ModbusSocketFramer, - ModbusTlsFramer, -) MAX_FILTER = 1000 @@ -135,13 +128,6 @@ def __init__( "tls": ModbusTlsServer, "udp": ModbusUdpServer, } - framer_class = { - "ascii": ModbusAsciiFramer, - "binary": ModbusBinaryFramer, - "rtu": ModbusRtuFramer, - "socket": ModbusSocketFramer, - "tls": ModbusTlsFramer, - } if custom_actions_module: actions_module = importlib.import_module(custom_actions_module) custom_actions_dict = actions_module.custom_actions_dict @@ -158,7 +144,7 @@ def __init__( ) datastore = ModbusServerContext(slaves=self.datastore_context, single=True) comm = comm_class[server.pop("comm")] - framer = framer_class[server.pop("framer")] + framer = server.pop("framer") if "identity" in server: server["identity"] = ModbusDeviceIdentification( info_name=server["identity"] diff --git a/test/sub_client/test_client.py b/test/sub_client/test_client.py index fadcd4fd6..8824f4397 100755 --- a/test/sub_client/test_client.py +++ b/test/sub_client/test_client.py @@ -13,15 +13,12 @@ import pymodbus.other_message as pdu_other_msg import pymodbus.register_read_message as pdu_reg_read import pymodbus.register_write_message as pdu_req_write +from pymodbus import Framer from pymodbus.client.base import ModbusBaseClient from pymodbus.client.mixin import ModbusClientMixin from pymodbus.datastore import ModbusSlaveContext from pymodbus.datastore.store import ModbusSequentialDataBlock from pymodbus.exceptions import ConnectionException, ModbusIOException -from pymodbus.framer.ascii_framer import ModbusAsciiFramer -from pymodbus.framer.rtu_framer import ModbusRtuFramer -from pymodbus.framer.socket_framer import ModbusSocketFramer -from pymodbus.framer.tls_framer import ModbusTlsFramer from pymodbus.transport import CommType @@ -138,7 +135,7 @@ def fake_execute(_self, request): "serial": { "pos_arg": "/dev/tty", "opt_args": { - "framer": ModbusAsciiFramer, + "framer": Framer.ASCII, "baudrate": 19200 + 500, "bytesize": 8 - 1, "parity": "E", @@ -148,7 +145,7 @@ def fake_execute(_self, request): "defaults": { "host": None, "port": "/dev/tty", - "framer": ModbusRtuFramer, + "framer": Framer.RTU, "baudrate": 19200, "bytesize": 8, "parity": "N", @@ -160,13 +157,13 @@ def fake_execute(_self, request): "pos_arg": "192.168.1.2", "opt_args": { "port": 112, - "framer": ModbusAsciiFramer, + "framer": Framer.ASCII, "source_address": ("195.6.7.8", 1025), }, "defaults": { "host": "192.168.1.2", "port": 502, - "framer": ModbusSocketFramer, + "framer": Framer.SOCKET, "source_address": None, }, }, @@ -174,7 +171,7 @@ def fake_execute(_self, request): "pos_arg": "192.168.1.2", "opt_args": { "port": 211, - "framer": ModbusAsciiFramer, + "framer": Framer.ASCII, "source_address": ("195.6.7.8", 1025), "sslctx": None, "certfile": None, @@ -184,7 +181,7 @@ def fake_execute(_self, request): "defaults": { "host": "192.168.1.2", "port": 802, - "framer": ModbusTlsFramer, + "framer": Framer.TLS, "source_address": None, "sslctx": None, "certfile": None, @@ -196,13 +193,13 @@ def fake_execute(_self, request): "pos_arg": "192.168.1.2", "opt_args": { "port": 121, - "framer": ModbusAsciiFramer, + "framer": Framer.ASCII, "source_address": ("195.6.7.8", 1025), }, "defaults": { "host": "192.168.1.2", "port": 502, - "framer": ModbusSocketFramer, + "framer": Framer.SOCKET, "source_address": None, }, }, @@ -265,7 +262,7 @@ async def test_client_instanciate( async def test_client_modbusbaseclient(): """Test modbus base client class.""" client = ModbusBaseClient( - framer=ModbusAsciiFramer, + Framer.ASCII, host="localhost", port=BASE_PORT + 1, CommType=CommType.TCP, @@ -297,7 +294,7 @@ async def test_client_base_async(): p_close.return_value = asyncio.Future() p_close.return_value.set_result(True) async with ModbusBaseClient( - framer=ModbusAsciiFramer, + Framer.ASCII, host="localhost", port=BASE_PORT + 2, CommType=CommType.TCP, @@ -312,7 +309,7 @@ async def test_client_base_async(): @pytest.mark.skip() async def test_client_protocol_receiver(): """Test the client protocol data received""" - base = ModbusBaseClient(framer=ModbusSocketFramer) + base = ModbusBaseClient(Framer.SOCKET) transport = mock.MagicMock() base.connection_made(transport) assert base.transport == transport @@ -334,7 +331,7 @@ async def test_client_protocol_receiver(): @pytest.mark.skip() async def test_client_protocol_response(): """Test the udp client protocol builds responses""" - base = ModbusBaseClient(framer=ModbusSocketFramer) + base = ModbusBaseClient(Framer.SOCKET) response = base._build_response(0x00) # pylint: disable=protected-access excp = response.exception() assert isinstance(excp, ConnectionException) @@ -348,7 +345,7 @@ async def test_client_protocol_response(): async def test_client_protocol_handler(): """Test the client protocol handles responses""" base = ModbusBaseClient( - framer=ModbusAsciiFramer, host="localhost", port=+3, CommType=CommType.TCP + Framer.ASCII, host="localhost", port=+3, CommType=CommType.TCP ) transport = mock.MagicMock() base.connection_made(transport=transport) @@ -395,7 +392,7 @@ def close(self): async def test_client_protocol_execute(): """Test the client protocol execute method""" - base = ModbusBaseClient(host="127.0.0.1", framer=ModbusSocketFramer) + base = ModbusBaseClient(Framer.SOCKET, host="127.0.0.1") request = pdu_bit_read.ReadCoilsRequest(1, 1) transport = MockTransport(base, request) base.connection_made(transport=transport) @@ -407,7 +404,7 @@ async def test_client_protocol_execute(): async def test_client_protocol_retry(): """Test the client protocol execute method with retries""" - base = ModbusBaseClient(host="127.0.0.1", framer=ModbusSocketFramer, timeout=0.1) + base = ModbusBaseClient(Framer.SOCKET, host="127.0.0.1", timeout=0.1) request = pdu_bit_read.ReadCoilsRequest(1, 1) transport = MockTransport(base, request, retries=2) base.connection_made(transport=transport) @@ -420,9 +417,7 @@ async def test_client_protocol_retry(): async def test_client_protocol_timeout(): """Test the client protocol execute method with timeout""" - base = ModbusBaseClient( - host="127.0.0.1", framer=ModbusSocketFramer, timeout=0.1, retries=2 - ) + base = ModbusBaseClient(Framer.SOCKET, host="127.0.0.1", timeout=0.1, retries=2) # Avoid creating do_reconnect() task base.connection_lost = mock.MagicMock() request = pdu_bit_read.ReadCoilsRequest(1, 1) diff --git a/test/sub_client/test_client_sync.py b/test/sub_client/test_client_sync.py index b1649aa01..09f532e7c 100755 --- a/test/sub_client/test_client_sync.py +++ b/test/sub_client/test_client_sync.py @@ -6,6 +6,7 @@ import pytest import serial +from pymodbus import Framer from pymodbus.client import ( ModbusSerialClient, ModbusTcpClient, @@ -299,27 +300,27 @@ def test_sync_serial_client_instantiation(self): client = ModbusSerialClient("/dev/null") assert client assert isinstance( - ModbusSerialClient("/dev/null", framer=ModbusAsciiFramer).framer, + ModbusSerialClient("/dev/null", framer=Framer.ASCII).framer, ModbusAsciiFramer, ) assert isinstance( - ModbusSerialClient("/dev/null", framer=ModbusRtuFramer).framer, + ModbusSerialClient("/dev/null", framer=Framer.RTU).framer, ModbusRtuFramer, ) assert isinstance( - ModbusSerialClient("/dev/null", framer=ModbusBinaryFramer).framer, + ModbusSerialClient("/dev/null", framer=Framer.BINARY).framer, ModbusBinaryFramer, ) assert isinstance( - ModbusSerialClient("/dev/null", framer=ModbusSocketFramer).framer, + ModbusSerialClient("/dev/null", framer=Framer.SOCKET).framer, ModbusSocketFramer, ) def test_sync_serial_rtu_client_timeouts(self): """Test sync serial rtu.""" - client = ModbusSerialClient("/dev/null", framer=ModbusRtuFramer, baudrate=9600) + client = ModbusSerialClient("/dev/null", framer=Framer.RTU, baudrate=9600) assert client.silent_interval == round((3.5 * 10 / 9600), 6) - client = ModbusSerialClient("/dev/null", framer=ModbusRtuFramer, baudrate=38400) + client = ModbusSerialClient("/dev/null", framer=Framer.RTU, baudrate=38400) assert client.silent_interval == round((1.75 / 1000), 6) @mock.patch("serial.Serial") @@ -343,9 +344,7 @@ def test_basic_sync_serial_client(self, mock_serial): client.close() # rtu connect/disconnect - rtu_client = ModbusSerialClient( - "/dev/null", framer=ModbusRtuFramer, strict=True - ) + rtu_client = ModbusSerialClient("/dev/null", framer=Framer.RTU, strict=True) assert rtu_client.connect() assert rtu_client.socket.interCharTimeout == rtu_client.inter_char_timeout rtu_client.close() diff --git a/test/sub_examples/test_examples.py b/test/sub_examples/test_examples.py index 9a222b468..2f1fe82fc 100755 --- a/test/sub_examples/test_examples.py +++ b/test/sub_examples/test_examples.py @@ -18,7 +18,6 @@ from examples.client_custom_msg import main as main_custom_client from examples.client_payload import main as main_payload_calls from examples.datastore_simulator import main as main_datastore_simulator -from examples.helper import get_framer from examples.message_generator import generate_messages from examples.message_parser import main as main_parse_messages from examples.server_async import setup_server @@ -184,8 +183,7 @@ async def test_async_simple_client( return if use_comm == "serial": use_port = f"socket://{use_host}:{use_port}" - framer = get_framer(use_framer) - await run_async_simple_client(use_comm, use_host, use_port, framer=framer) + await run_async_simple_client(use_comm, use_host, use_port, framer=use_framer) @pytest.mark.parametrize("use_host", ["localhost"]) @@ -231,6 +229,5 @@ def test_sync_simple_client( sleep(1) if use_comm == "serial": use_port = f"socket://{use_host}:{use_port}" - framer = get_framer(use_framer) - run_sync_simple_client(use_comm, use_host, use_port, framer=framer) + run_sync_simple_client(use_comm, use_host, use_port, framer=use_framer) ServerStop() diff --git a/test/sub_server/test_server_asyncio.py b/test/sub_server/test_server_asyncio.py index 12e222394..e0ba7ccbc 100755 --- a/test/sub_server/test_server_asyncio.py +++ b/test/sub_server/test_server_asyncio.py @@ -8,6 +8,7 @@ import pytest +from pymodbus import Framer from pymodbus.datastore import ( ModbusSequentialDataBlock, ModbusServerContext, @@ -16,7 +17,6 @@ from pymodbus.device import ModbusDeviceIdentification from pymodbus.exceptions import NoSuchSlaveException from pymodbus.server import ModbusTcpServer, ModbusTlsServer, ModbusUdpServer -from pymodbus.transaction import ModbusSocketFramer, ModbusTlsFramer _logger = logging.getLogger() @@ -154,15 +154,15 @@ async def start_server( args["identity"] = self.identity if do_tls: self.server = ModbusTlsServer( - self.context, ModbusTlsFramer, self.identity, SERV_ADDR + self.context, Framer.TLS, self.identity, SERV_ADDR ) elif do_udp: self.server = ModbusUdpServer( - self.context, ModbusSocketFramer, self.identity, SERV_ADDR + self.context, Framer.SOCKET, self.identity, SERV_ADDR ) else: self.server = ModbusTcpServer( - self.context, ModbusSocketFramer, self.identity, SERV_ADDR + self.context, Framer.SOCKET, self.identity, SERV_ADDR ) assert self.server if do_forever: diff --git a/test/test_framers.py b/test/test_framers.py index 3070f8b12..8f00868ce 100644 --- a/test/test_framers.py +++ b/test/test_framers.py @@ -3,6 +3,7 @@ import pytest +from pymodbus import Framer from pymodbus.bit_read_message import ReadCoilsRequest from pymodbus.client import ModbusBaseClient from pymodbus.exceptions import ModbusIOException @@ -308,7 +309,7 @@ def test_send_packet(rtu_framer): """Test send packet.""" message = TEST_MESSAGE client = ModbusBaseClient( - framer=ModbusAsciiFramer, + Framer.ASCII, host="localhost", port=BASE_PORT + 1, CommType=CommType.TCP,