From 496db2540fffe945d26c8e8a169db9972027c283 Mon Sep 17 00:00:00 2001 From: jan iversen Date: Thu, 28 Mar 2024 21:19:45 +0100 Subject: [PATCH 1/5] message ==> framers. --- .../library/architecture/architecture.rst | 9 +- pymodbus/framer/__init__.py | 13 +- pymodbus/{message => framer}/ascii.py | 2 +- pymodbus/framer/base.py | 160 +--- pymodbus/{message => framer}/message.py | 12 +- .../{ascii_framer.py => old_framer_ascii.py} | 5 +- pymodbus/framer/old_framer_base.py | 150 ++++ .../{rtu_framer.py => old_framer_rtu.py} | 4 +- ...{socket_framer.py => old_framer_socket.py} | 4 +- .../{tls_framer.py => old_framer_tls.py} | 4 +- pymodbus/{message => framer}/raw.py | 2 +- pymodbus/{message => framer}/rtu.py | 18 +- pymodbus/{message => framer}/socket.py | 2 +- pymodbus/{message => framer}/tls.py | 2 +- pymodbus/message/__init__.py | 7 - pymodbus/message/base.py | 38 - pymodbus/transaction.py | 10 +- test/conftest.py | 19 +- test/{message => framers}/__init__.py | 0 test/{message => framers}/conftest.py | 2 +- test/{message => framers}/generator.py | 0 test/{message => framers}/test_ascii.py | 2 +- test/{message => framers}/test_message.py | 10 +- test/{message => framers}/test_multidrop.py | 2 +- .../test_old_framers.py} | 0 test/{message => framers}/test_rtu.py | 2 +- test/{message => framers}/test_socket.py | 2 +- .../test_tbc_multidrop.py} | 2 +- test/framers/test_tbc_transaction.py | 721 ++++++++++++++++++ test/{message => framers}/test_tls.py | 2 +- test/test_transaction.py | 1 + 31 files changed, 968 insertions(+), 239 deletions(-) rename pymodbus/{message => framer}/ascii.py (98%) rename pymodbus/{message => framer}/message.py (93%) rename pymodbus/framer/{ascii_framer.py => old_framer_ascii.py} (95%) create mode 100644 pymodbus/framer/old_framer_base.py rename pymodbus/framer/{rtu_framer.py => old_framer_rtu.py} (98%) rename pymodbus/framer/{socket_framer.py => old_framer_socket.py} (96%) rename pymodbus/framer/{tls_framer.py => old_framer_tls.py} (95%) rename pymodbus/{message => framer}/raw.py (95%) rename pymodbus/{message => framer}/rtu.py (95%) rename pymodbus/{message => framer}/socket.py (96%) rename pymodbus/{message => framer}/tls.py (93%) delete mode 100644 pymodbus/message/__init__.py delete mode 100644 pymodbus/message/base.py rename test/{message => framers}/__init__.py (100%) rename test/{message => framers}/conftest.py (96%) rename test/{message => framers}/generator.py (100%) rename test/{message => framers}/test_ascii.py (97%) rename test/{message => framers}/test_message.py (98%) rename test/{message => framers}/test_multidrop.py (99%) rename test/{test_framers.py => framers/test_old_framers.py} (100%) rename test/{message => framers}/test_rtu.py (95%) rename test/{message => framers}/test_socket.py (97%) rename test/{sub_server/test_server_multidrop.py => framers/test_tbc_multidrop.py} (99%) create mode 100755 test/framers/test_tbc_transaction.py rename test/{message => framers}/test_tls.py (96%) diff --git a/doc/source/library/architecture/architecture.rst b/doc/source/library/architecture/architecture.rst index f91e74d87..e38f46754 100644 --- a/doc/source/library/architecture/architecture.rst +++ b/doc/source/library/architecture/architecture.rst @@ -5,14 +5,16 @@ The internal structure of pymodbus is a bit complicated, mostly due to the mixtu The overall architecture can be viewed as: + Client classes (interface to applications) mixin (interface with all requests defined as methods) - transaction (handles transactions and allow concurrent calls) - framers (add pre/post headers to make a valid package) - transport (handles actual transportation) + Lower levels are Common Server classes (interface to applications) datastores (handles registers/values to be returned) + Lower levels are Common + + pdu (build/create response/request class) transaction (handles transactions and allow concurrent calls) framers (add pre/post headers to make a valid package) transport (handles actual transportation) @@ -25,4 +27,3 @@ In detail the packages can viewed as: In detail the classes can be viewed as: .. image:: classes.png - diff --git a/pymodbus/framer/__init__.py b/pymodbus/framer/__init__.py index 20b88e05b..614b440bc 100644 --- a/pymodbus/framer/__init__.py +++ b/pymodbus/framer/__init__.py @@ -7,16 +7,19 @@ "ModbusRtuFramer", "ModbusSocketFramer", "ModbusTlsFramer", + "Message", + "MessageType", ] import enum -from pymodbus.framer.ascii_framer import ModbusAsciiFramer -from pymodbus.framer.base import ModbusFramer -from pymodbus.framer.rtu_framer import ModbusRtuFramer -from pymodbus.framer.socket_framer import ModbusSocketFramer -from pymodbus.framer.tls_framer import ModbusTlsFramer +from pymodbus.framer.message import Message, MessageType +from pymodbus.framer.old_framer_ascii import ModbusAsciiFramer +from pymodbus.framer.old_framer_base import ModbusFramer +from pymodbus.framer.old_framer_rtu import ModbusRtuFramer +from pymodbus.framer.old_framer_socket import ModbusSocketFramer +from pymodbus.framer.old_framer_tls import ModbusTlsFramer class Framer(str, enum.Enum): diff --git a/pymodbus/message/ascii.py b/pymodbus/framer/ascii.py similarity index 98% rename from pymodbus/message/ascii.py rename to pymodbus/framer/ascii.py index b42460717..c2304eae3 100644 --- a/pymodbus/message/ascii.py +++ b/pymodbus/framer/ascii.py @@ -8,8 +8,8 @@ from binascii import a2b_hex, b2a_hex +from pymodbus.framer.base import MessageBase from pymodbus.logging import Log -from pymodbus.message.base import MessageBase class MessageAscii(MessageBase): diff --git a/pymodbus/framer/base.py b/pymodbus/framer/base.py index 27ac9724c..3a41bb9eb 100644 --- a/pymodbus/framer/base.py +++ b/pymodbus/framer/base.py @@ -1,150 +1,38 @@ -"""Framer start.""" -# pylint: disable=missing-type-doc -from __future__ import annotations - -from typing import Any - -from pymodbus.factory import ClientDecoder, ServerDecoder -from pymodbus.logging import Log - - -# Unit ID, Function Code -BYTE_ORDER = ">" -FRAME_HEADER = "BB" - -# Transaction Id, Protocol ID, Length, Unit ID, Function Code -SOCKET_FRAME_HEADER = BYTE_ORDER + "HHH" + FRAME_HEADER - -# Function Code -TLS_FRAME_HEADER = BYTE_ORDER + "B" - - -class ModbusFramer: - """Base Framer class.""" - - name = "" - - def __init__( - self, - decoder: ClientDecoder | ServerDecoder, - client, - ) -> None: - """Initialize a new instance of the framer. - - :param decoder: The decoder implementation to use - """ - self.decoder = decoder - self.client = client - self._header: dict[str, Any] = { - "lrc": "0000", - "len": 0, - "uid": 0x00, - "tid": 0, - "pid": 0, - "crc": b"\x00\x00", - } - self._buffer = b"" +"""ModbusMessage layer. - def _validate_slave_id(self, slaves: list, single: bool) -> bool: - """Validate if the received data is valid for the client. +The message layer is responsible for encoding/decoding requests/responses. - :param slaves: list of slave id for which the transaction is valid - :param single: Set to true to treat this as a single context - :return: - """ - if single: - return True - if 0 in slaves or 0xFF in slaves: - # Handle Modbus TCP slave identifier (0x00 0r 0xFF) - # in asynchronous requests - return True - return self._header["uid"] in slaves - - def sendPacket(self, message): - """Send packets on the bus. - - With 3.5char delay between frames - :param message: Message to be sent over the bus - :return: - """ - return self.client.send(message) - - def recvPacket(self, size): - """Receive packet from the bus. - - With specified len - :param size: Number of bytes to read - :return: - """ - return self.client.recv(size) - - def resetFrame(self): - """Reset the entire message frame. +According to the selected type of modbus frame a prefix/suffix is added/removed +""" +from __future__ import annotations - This allows us to skip ovver errors that may be in the stream. - It is hard to know if we are simply out of sync or if there is - an error in the stream as we have no way to check the start or - end of the message (python just doesn't have the resolution to - check for millisecond delays). - """ - Log.debug( - "Resetting frame - Current Frame in buffer - {}", self._buffer, ":hex" - ) - self._buffer = b"" - self._header = { - "lrc": "0000", - "crc": b"\x00\x00", - "len": 0, - "uid": 0x00, - "pid": 0, - "tid": 0, - } +from abc import abstractmethod - def populateResult(self, result): - """Populate the modbus result header. - The serial packets do not have any header information - that is copied. +class MessageBase: + """Intern base.""" - :param result: The response packet - """ - result.slave_id = self._header.get("uid", 0) - result.transaction_id = self._header.get("tid", 0) - result.protocol_id = self._header.get("pid", 0) + EMPTY = b'' - def processIncomingPacket(self, data, callback, slave, **kwargs): - """Process new packet pattern. + def __init__(self) -> None: + """Initialize a message instance.""" - This takes in a new request packet, adds it to the current - packet stream, and performs framing on it. That is, checks - for complete messages, and once found, will process all that - exist. This handles the case when we read N + 1 or 1 // N - messages at a time instead of 1. - The processed and decoded messages are pushed to the callback - function to process and send. + @abstractmethod + def decode(self, _data: bytes) -> tuple[int, int, int, bytes]: + """Decode message. - :param data: The new packet data - :param callback: The function to send results to - :param slave: Process if slave id matches, ignore otherwise (could be a - list of slave ids (server) or single slave id(client/server)) - :param kwargs: - :raises ModbusIOException: + return: + used_len (int) or 0 to read more + transaction_id (int) or 0 + device_id (int) or 0 + modbus request/response (bytes) """ - Log.debug("Processing: {}", data, ":hex") - self._buffer += data - if not isinstance(slave, (list, tuple)): - slave = [slave] - single = kwargs.pop("single", False) - self.frameProcessIncomingPacket(single, callback, slave, **kwargs) - - def frameProcessIncomingPacket( - self, _single, _callback, _slave, _tid=None, **kwargs - ) -> None: - """Process new packet pattern.""" - def buildPacket(self, message) -> bytes: # type:ignore[empty-body] - """Create a ready to send modbus packet. + @abstractmethod + def encode(self, data: bytes, device_id: int, tid: int) -> bytes: + """Decode message. - :param message: The populated request/response to send + return: + modbus message (bytes) """ diff --git a/pymodbus/message/message.py b/pymodbus/framer/message.py similarity index 93% rename from pymodbus/message/message.py rename to pymodbus/framer/message.py index 20427bd23..a8f314d03 100644 --- a/pymodbus/message/message.py +++ b/pymodbus/framer/message.py @@ -9,12 +9,12 @@ from abc import abstractmethod from enum import Enum -from pymodbus.message.ascii import MessageAscii -from pymodbus.message.base import MessageBase -from pymodbus.message.raw import MessageRaw -from pymodbus.message.rtu import MessageRTU -from pymodbus.message.socket import MessageSocket -from pymodbus.message.tls import MessageTLS +from pymodbus.framer.ascii import MessageAscii +from pymodbus.framer.base import MessageBase +from pymodbus.framer.raw import MessageRaw +from pymodbus.framer.rtu import MessageRTU +from pymodbus.framer.socket import MessageSocket +from pymodbus.framer.tls import MessageTLS from pymodbus.transport.transport import CommParams, ModbusProtocol diff --git a/pymodbus/framer/ascii_framer.py b/pymodbus/framer/old_framer_ascii.py similarity index 95% rename from pymodbus/framer/ascii_framer.py rename to pymodbus/framer/old_framer_ascii.py index 4d2fbc68a..2d0969f68 100644 --- a/pymodbus/framer/ascii_framer.py +++ b/pymodbus/framer/old_framer_ascii.py @@ -2,9 +2,10 @@ # pylint: disable=missing-type-doc from pymodbus.exceptions import ModbusIOException -from pymodbus.framer.base import BYTE_ORDER, FRAME_HEADER, ModbusFramer +from pymodbus.framer.old_framer_base import BYTE_ORDER, FRAME_HEADER, ModbusFramer from pymodbus.logging import Log -from pymodbus.message.ascii import MessageAscii + +from .ascii import MessageAscii ASCII_FRAME_HEADER = BYTE_ORDER + FRAME_HEADER diff --git a/pymodbus/framer/old_framer_base.py b/pymodbus/framer/old_framer_base.py new file mode 100644 index 000000000..27ac9724c --- /dev/null +++ b/pymodbus/framer/old_framer_base.py @@ -0,0 +1,150 @@ +"""Framer start.""" +# pylint: disable=missing-type-doc +from __future__ import annotations + +from typing import Any + +from pymodbus.factory import ClientDecoder, ServerDecoder +from pymodbus.logging import Log + + +# Unit ID, Function Code +BYTE_ORDER = ">" +FRAME_HEADER = "BB" + +# Transaction Id, Protocol ID, Length, Unit ID, Function Code +SOCKET_FRAME_HEADER = BYTE_ORDER + "HHH" + FRAME_HEADER + +# Function Code +TLS_FRAME_HEADER = BYTE_ORDER + "B" + + +class ModbusFramer: + """Base Framer class.""" + + name = "" + + def __init__( + self, + decoder: ClientDecoder | ServerDecoder, + client, + ) -> None: + """Initialize a new instance of the framer. + + :param decoder: The decoder implementation to use + """ + self.decoder = decoder + self.client = client + self._header: dict[str, Any] = { + "lrc": "0000", + "len": 0, + "uid": 0x00, + "tid": 0, + "pid": 0, + "crc": b"\x00\x00", + } + self._buffer = b"" + + def _validate_slave_id(self, slaves: list, single: bool) -> bool: + """Validate if the received data is valid for the client. + + :param slaves: list of slave id for which the transaction is valid + :param single: Set to true to treat this as a single context + :return: + """ + if single: + return True + if 0 in slaves or 0xFF in slaves: + # Handle Modbus TCP slave identifier (0x00 0r 0xFF) + # in asynchronous requests + return True + return self._header["uid"] in slaves + + def sendPacket(self, message): + """Send packets on the bus. + + With 3.5char delay between frames + :param message: Message to be sent over the bus + :return: + """ + return self.client.send(message) + + def recvPacket(self, size): + """Receive packet from the bus. + + With specified len + :param size: Number of bytes to read + :return: + """ + return self.client.recv(size) + + def resetFrame(self): + """Reset the entire message frame. + + This allows us to skip ovver errors that may be in the stream. + It is hard to know if we are simply out of sync or if there is + an error in the stream as we have no way to check the start or + end of the message (python just doesn't have the resolution to + check for millisecond delays). + """ + Log.debug( + "Resetting frame - Current Frame in buffer - {}", self._buffer, ":hex" + ) + self._buffer = b"" + self._header = { + "lrc": "0000", + "crc": b"\x00\x00", + "len": 0, + "uid": 0x00, + "pid": 0, + "tid": 0, + } + + def populateResult(self, result): + """Populate the modbus result header. + + The serial packets do not have any header information + that is copied. + + :param result: The response packet + """ + result.slave_id = self._header.get("uid", 0) + result.transaction_id = self._header.get("tid", 0) + result.protocol_id = self._header.get("pid", 0) + + def processIncomingPacket(self, data, callback, slave, **kwargs): + """Process new packet pattern. + + This takes in a new request packet, adds it to the current + packet stream, and performs framing on it. That is, checks + for complete messages, and once found, will process all that + exist. This handles the case when we read N + 1 or 1 // N + messages at a time instead of 1. + + The processed and decoded messages are pushed to the callback + function to process and send. + + :param data: The new packet data + :param callback: The function to send results to + :param slave: Process if slave id matches, ignore otherwise (could be a + list of slave ids (server) or single slave id(client/server)) + :param kwargs: + :raises ModbusIOException: + """ + Log.debug("Processing: {}", data, ":hex") + self._buffer += data + if not isinstance(slave, (list, tuple)): + slave = [slave] + single = kwargs.pop("single", False) + self.frameProcessIncomingPacket(single, callback, slave, **kwargs) + + def frameProcessIncomingPacket( + self, _single, _callback, _slave, _tid=None, **kwargs + ) -> None: + """Process new packet pattern.""" + + def buildPacket(self, message) -> bytes: # type:ignore[empty-body] + """Create a ready to send modbus packet. + + :param message: The populated request/response to send + """ diff --git a/pymodbus/framer/rtu_framer.py b/pymodbus/framer/old_framer_rtu.py similarity index 98% rename from pymodbus/framer/rtu_framer.py rename to pymodbus/framer/old_framer_rtu.py index 6455a28f8..ec9351ea9 100644 --- a/pymodbus/framer/rtu_framer.py +++ b/pymodbus/framer/old_framer_rtu.py @@ -4,9 +4,9 @@ import time from pymodbus.exceptions import ModbusIOException -from pymodbus.framer.base import BYTE_ORDER, FRAME_HEADER, ModbusFramer +from pymodbus.framer.old_framer_base import BYTE_ORDER, FRAME_HEADER, ModbusFramer +from pymodbus.framer.rtu import MessageRTU from pymodbus.logging import Log -from pymodbus.message.rtu import MessageRTU from pymodbus.utilities import ModbusTransactionState diff --git a/pymodbus/framer/socket_framer.py b/pymodbus/framer/old_framer_socket.py similarity index 96% rename from pymodbus/framer/socket_framer.py rename to pymodbus/framer/old_framer_socket.py index 582aa283a..33ae3a538 100644 --- a/pymodbus/framer/socket_framer.py +++ b/pymodbus/framer/old_framer_socket.py @@ -5,9 +5,9 @@ from pymodbus.exceptions import ( ModbusIOException, ) -from pymodbus.framer.base import SOCKET_FRAME_HEADER, ModbusFramer +from pymodbus.framer.old_framer_base import SOCKET_FRAME_HEADER, ModbusFramer +from pymodbus.framer.socket import MessageSocket from pymodbus.logging import Log -from pymodbus.message.socket import MessageSocket # --------------------------------------------------------------------------- # diff --git a/pymodbus/framer/tls_framer.py b/pymodbus/framer/old_framer_tls.py similarity index 95% rename from pymodbus/framer/tls_framer.py rename to pymodbus/framer/old_framer_tls.py index 1b341b224..91b699968 100644 --- a/pymodbus/framer/tls_framer.py +++ b/pymodbus/framer/old_framer_tls.py @@ -5,8 +5,8 @@ from pymodbus.exceptions import ( ModbusIOException, ) -from pymodbus.framer.base import TLS_FRAME_HEADER, ModbusFramer -from pymodbus.message.tls import MessageTLS +from pymodbus.framer.old_framer_base import TLS_FRAME_HEADER, ModbusFramer +from pymodbus.framer.tls import MessageTLS # --------------------------------------------------------------------------- # diff --git a/pymodbus/message/raw.py b/pymodbus/framer/raw.py similarity index 95% rename from pymodbus/message/raw.py rename to pymodbus/framer/raw.py index 88627482b..ab932826e 100644 --- a/pymodbus/message/raw.py +++ b/pymodbus/framer/raw.py @@ -1,8 +1,8 @@ """ModbusMessage layer.""" from __future__ import annotations +from pymodbus.framer.base import MessageBase from pymodbus.logging import Log -from pymodbus.message.base import MessageBase class MessageRaw(MessageBase): diff --git a/pymodbus/message/rtu.py b/pymodbus/framer/rtu.py similarity index 95% rename from pymodbus/message/rtu.py rename to pymodbus/framer/rtu.py index 2565fa5a9..fcd0ec804 100644 --- a/pymodbus/message/rtu.py +++ b/pymodbus/framer/rtu.py @@ -10,8 +10,8 @@ from pymodbus.exceptions import ModbusIOException from pymodbus.factory import ClientDecoder +from pymodbus.framer.base import MessageBase from pymodbus.logging import Log -from pymodbus.message.base import MessageBase class MessageRTU(MessageBase): @@ -165,13 +165,21 @@ def check_frame(self): Log.debug("Frame advanced, resetting header!!") callback(result) # defer or push to a thread? + def assemble_frame(self, _data_len: int, _data: bytes) -> int: + """Collect frame, until CRC matches.""" + return 0 def decode(self, data: bytes) -> tuple[int, int, int, bytes]: """Decode message.""" - resp = None - if len(data) < 4: + if (data_len := len(data)) < 6: # + return 0, 0, 0, b'' + + if not (_frame_len := self.assemble_frame(data_len, data)): return 0, 0, 0, b'' + + + resp = None def callback(result): """Set result.""" nonlocal resp @@ -180,7 +188,6 @@ def callback(result): self._legacy_decode(callback, [0]) return 0, 0, 0, b'' - def encode(self, data: bytes, device_id: int, _tid: int) -> bytes: """Decode message.""" packet = device_id.to_bytes(1,'big') + data @@ -200,9 +207,6 @@ def check_CRC(cls, data: bytes, check: int) -> bool: def compute_CRC(cls, data: bytes) -> int: """Compute a crc16 on the passed in bytes. - For modbus, this is only used on the binary serial protocols (in this - case RTU). - The difference between modbus's crc16 and a normal crc16 is that modbus starts the crc value out at 0xffff. diff --git a/pymodbus/message/socket.py b/pymodbus/framer/socket.py similarity index 96% rename from pymodbus/message/socket.py rename to pymodbus/framer/socket.py index 53e88ef9c..f3bca1a8f 100644 --- a/pymodbus/message/socket.py +++ b/pymodbus/framer/socket.py @@ -6,8 +6,8 @@ """ from __future__ import annotations +from pymodbus.framer.base import MessageBase from pymodbus.logging import Log -from pymodbus.message.base import MessageBase class MessageSocket(MessageBase): diff --git a/pymodbus/message/tls.py b/pymodbus/framer/tls.py similarity index 93% rename from pymodbus/message/tls.py rename to pymodbus/framer/tls.py index d89e769e1..e9845b1a5 100644 --- a/pymodbus/message/tls.py +++ b/pymodbus/framer/tls.py @@ -6,7 +6,7 @@ """ from __future__ import annotations -from pymodbus.message.base import MessageBase +from pymodbus.framer.base import MessageBase class MessageTLS(MessageBase): diff --git a/pymodbus/message/__init__.py b/pymodbus/message/__init__.py deleted file mode 100644 index 5e35bc814..000000000 --- a/pymodbus/message/__init__.py +++ /dev/null @@ -1,7 +0,0 @@ -"""Message.""" -__all__ = [ - "Message", - "MessageType", -] - -from pymodbus.message.message import Message, MessageType diff --git a/pymodbus/message/base.py b/pymodbus/message/base.py deleted file mode 100644 index 3a41bb9eb..000000000 --- a/pymodbus/message/base.py +++ /dev/null @@ -1,38 +0,0 @@ -"""ModbusMessage layer. - -The message layer is responsible for encoding/decoding requests/responses. - -According to the selected type of modbus frame a prefix/suffix is added/removed -""" -from __future__ import annotations - -from abc import abstractmethod - - -class MessageBase: - """Intern base.""" - - EMPTY = b'' - - def __init__(self) -> None: - """Initialize a message instance.""" - - - @abstractmethod - def decode(self, _data: bytes) -> tuple[int, int, int, bytes]: - """Decode message. - - return: - used_len (int) or 0 to read more - transaction_id (int) or 0 - device_id (int) or 0 - modbus request/response (bytes) - """ - - @abstractmethod - def encode(self, data: bytes, device_id: int, tid: int) -> bytes: - """Decode message. - - return: - modbus message (bytes) - """ diff --git a/pymodbus/transaction.py b/pymodbus/transaction.py index 8ff1feeda..157537cdb 100644 --- a/pymodbus/transaction.py +++ b/pymodbus/transaction.py @@ -21,10 +21,12 @@ InvalidMessageReceivedException, ModbusIOException, ) -from pymodbus.framer.ascii_framer import ModbusAsciiFramer -from pymodbus.framer.rtu_framer import ModbusRtuFramer -from pymodbus.framer.socket_framer import ModbusSocketFramer -from pymodbus.framer.tls_framer import ModbusTlsFramer +from pymodbus.framer import ( + ModbusAsciiFramer, + ModbusRtuFramer, + ModbusSocketFramer, + ModbusTlsFramer, +) from pymodbus.logging import Log from pymodbus.utilities import ModbusTransactionState, hexlify_packets diff --git a/test/conftest.py b/test/conftest.py index ee499042a..3cc85989f 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -286,14 +286,17 @@ def recv(self, size): """Receive.""" if not self.packets or not size: return b"" - if not self.buffer: - self.buffer = self.packets.popleft() - if size >= len(self.buffer): - retval = self.buffer - self.buffer = None - else: - retval = self.buffer[0:size] - self.buffer = self.buffer[size] + # if not self.buffer: + # self.buffer = self.packets.popleft() + # if size >= len(self.buffer): + # retval = self.buffer + # self.buffer = None + # else: + # retval = self.buffer[0:size] + # self.buffer = self.buffer[size] + self.buffer = self.packets.popleft() + retval = self.buffer + self.buffer = None self.in_waiting -= len(retval) return retval diff --git a/test/message/__init__.py b/test/framers/__init__.py similarity index 100% rename from test/message/__init__.py rename to test/framers/__init__.py diff --git a/test/message/conftest.py b/test/framers/conftest.py similarity index 96% rename from test/message/conftest.py rename to test/framers/conftest.py index dcf2e3369..d16b22e18 100644 --- a/test/message/conftest.py +++ b/test/framers/conftest.py @@ -5,7 +5,7 @@ import pytest -from pymodbus.message import Message, MessageType +from pymodbus.framer import Message, MessageType from pymodbus.transport import CommParams, ModbusProtocol diff --git a/test/message/generator.py b/test/framers/generator.py similarity index 100% rename from test/message/generator.py rename to test/framers/generator.py diff --git a/test/message/test_ascii.py b/test/framers/test_ascii.py similarity index 97% rename from test/message/test_ascii.py rename to test/framers/test_ascii.py index e166828c3..86f63e2d6 100644 --- a/test/message/test_ascii.py +++ b/test/framers/test_ascii.py @@ -1,7 +1,7 @@ """Test transport.""" import pytest -from pymodbus.message.ascii import MessageAscii +from pymodbus.framer.ascii import MessageAscii class TestMessageAscii: diff --git a/test/message/test_message.py b/test/framers/test_message.py similarity index 98% rename from test/message/test_message.py rename to test/framers/test_message.py index 113ec8f8c..3d515fd80 100644 --- a/test/message/test_message.py +++ b/test/framers/test_message.py @@ -4,11 +4,11 @@ import pytest -from pymodbus.message import MessageType -from pymodbus.message.ascii import MessageAscii -from pymodbus.message.rtu import MessageRTU -from pymodbus.message.socket import MessageSocket -from pymodbus.message.tls import MessageTLS +from pymodbus.framer import MessageType +from pymodbus.framer.ascii import MessageAscii +from pymodbus.framer.rtu import MessageRTU +from pymodbus.framer.socket import MessageSocket +from pymodbus.framer.tls import MessageTLS from pymodbus.transport import CommParams diff --git a/test/message/test_multidrop.py b/test/framers/test_multidrop.py similarity index 99% rename from test/message/test_multidrop.py rename to test/framers/test_multidrop.py index 74711ab5c..e2a21bd14 100644 --- a/test/message/test_multidrop.py +++ b/test/framers/test_multidrop.py @@ -3,7 +3,7 @@ import pytest -from pymodbus.framer.rtu_framer import ModbusRtuFramer +from pymodbus.framer import ModbusRtuFramer from pymodbus.server.async_io import ServerDecoder diff --git a/test/test_framers.py b/test/framers/test_old_framers.py similarity index 100% rename from test/test_framers.py rename to test/framers/test_old_framers.py diff --git a/test/message/test_rtu.py b/test/framers/test_rtu.py similarity index 95% rename from test/message/test_rtu.py rename to test/framers/test_rtu.py index e7c019b99..3af18c695 100644 --- a/test/message/test_rtu.py +++ b/test/framers/test_rtu.py @@ -1,7 +1,7 @@ """Test transport.""" import pytest -from pymodbus.message.rtu import MessageRTU +from pymodbus.framer.rtu import MessageRTU class TestMessageRTU: diff --git a/test/message/test_socket.py b/test/framers/test_socket.py similarity index 97% rename from test/message/test_socket.py rename to test/framers/test_socket.py index e58ba65cc..77d972ea1 100644 --- a/test/message/test_socket.py +++ b/test/framers/test_socket.py @@ -2,7 +2,7 @@ import pytest -from pymodbus.message.socket import MessageSocket +from pymodbus.framer.socket import MessageSocket class TestMessageSocket: diff --git a/test/sub_server/test_server_multidrop.py b/test/framers/test_tbc_multidrop.py similarity index 99% rename from test/sub_server/test_server_multidrop.py rename to test/framers/test_tbc_multidrop.py index 74711ab5c..e2a21bd14 100644 --- a/test/sub_server/test_server_multidrop.py +++ b/test/framers/test_tbc_multidrop.py @@ -3,7 +3,7 @@ import pytest -from pymodbus.framer.rtu_framer import ModbusRtuFramer +from pymodbus.framer import ModbusRtuFramer from pymodbus.server.async_io import ServerDecoder diff --git a/test/framers/test_tbc_transaction.py b/test/framers/test_tbc_transaction.py new file mode 100755 index 000000000..b072909f8 --- /dev/null +++ b/test/framers/test_tbc_transaction.py @@ -0,0 +1,721 @@ +"""Test transaction.""" +from itertools import count +from unittest import mock + +from pymodbus.exceptions import ( + ModbusIOException, +) +from pymodbus.factory import ServerDecoder +from pymodbus.pdu import ModbusRequest +from pymodbus.transaction import ( + ModbusAsciiFramer, + ModbusRtuFramer, + ModbusSocketFramer, + ModbusTlsFramer, + ModbusTransactionManager, +) + + +TEST_MESSAGE = b"\x7b\x01\x03\x00\x00\x00\x05\x85\xC9\x7d" + + +class TestTransaction: # pylint: disable=too-many-public-methods + """Unittest for the pymodbus.transaction module.""" + + client = None + decoder = None + _tcp = None + _tls = None + _rtu = None + _ascii = None + _manager = None + _tm = None + + # ----------------------------------------------------------------------- # + # Test Construction + # ----------------------------------------------------------------------- # + def setup_method(self): + """Set up the test environment.""" + self.client = None + self.decoder = ServerDecoder() + self._tcp = ModbusSocketFramer(decoder=self.decoder, client=None) + self._tls = ModbusTlsFramer(decoder=self.decoder, client=None) + self._rtu = ModbusRtuFramer(decoder=self.decoder, client=None) + self._ascii = ModbusAsciiFramer(decoder=self.decoder, client=None) + self._manager = ModbusTransactionManager(self.client) + + # ----------------------------------------------------------------------- # + # Modbus transaction manager + # ----------------------------------------------------------------------- # + + def test_calculate_expected_response_length(self): + """Test calculate expected response length.""" + self._manager.client = mock.MagicMock() + self._manager.client.framer = mock.MagicMock() + self._manager._set_adu_size() # pylint: disable=protected-access + assert not self._manager._calculate_response_length( # pylint: disable=protected-access + 0 + ) + self._manager.base_adu_size = 10 + assert ( + self._manager._calculate_response_length(5) # pylint: disable=protected-access + == 15 + ) + + def test_calculate_exception_length(self): + """Test calculate exception length.""" + for framer, exception_length in ( + ("ascii", 11), + ("rtu", 5), + ("tcp", 9), + ("tls", 2), + ("dummy", None), + ): + self._manager.client = mock.MagicMock() + if framer == "ascii": + self._manager.client.framer = self._ascii + elif framer == "rtu": + self._manager.client.framer = self._rtu + elif framer == "tcp": + self._manager.client.framer = self._tcp + elif framer == "tls": + self._manager.client.framer = self._tls + else: + self._manager.client.framer = mock.MagicMock() + + self._manager._set_adu_size() # pylint: disable=protected-access + assert ( + self._manager._calculate_exception_length() # pylint: disable=protected-access + == exception_length + ) + + @mock.patch("pymodbus.transaction.time") + def test_execute(self, mock_time): + """Test execute.""" + mock_time.time.side_effect = count() + + client = mock.MagicMock() + client.framer = self._ascii + client.framer._buffer = b"deadbeef" # pylint: disable=protected-access + client.framer.processIncomingPacket = mock.MagicMock() + client.framer.processIncomingPacket.return_value = None + client.framer.buildPacket = mock.MagicMock() + client.framer.buildPacket.return_value = b"deadbeef" + client.framer.sendPacket = mock.MagicMock() + client.framer.sendPacket.return_value = len(b"deadbeef") + client.framer.decode_data = mock.MagicMock() + client.framer.decode_data.return_value = { + "slave": 1, + "fcode": 222, + "length": 27, + } + request = mock.MagicMock() + request.get_response_pdu_size.return_value = 10 + request.slave_id = 1 + request.function_code = 222 + trans = ModbusTransactionManager(client) + trans._recv = mock.MagicMock( # pylint: disable=protected-access + return_value=b"abcdef" + ) + assert trans.retries == 3 + assert not trans.retry_on_empty + + trans.getTransaction = mock.MagicMock() + trans.getTransaction.return_value = "response" + response = trans.execute(request) + assert response == "response" + # No response + trans._recv = mock.MagicMock( # pylint: disable=protected-access + return_value=b"abcdef" + ) + trans.transactions = {} + trans.getTransaction = mock.MagicMock() + trans.getTransaction.return_value = None + response = trans.execute(request) + assert isinstance(response, ModbusIOException) + + # No response with retries + trans.retry_on_empty = True + trans._recv = mock.MagicMock( # pylint: disable=protected-access + side_effect=iter([b"", b"abcdef"]) + ) + response = trans.execute(request) + assert isinstance(response, ModbusIOException) + + # wrong handle_local_echo + trans._recv = mock.MagicMock( # pylint: disable=protected-access + side_effect=iter([b"abcdef", b"deadbe", b"123456"]) + ) + client.comm_params.handle_local_echo = True + trans.retry_on_empty = False + trans.retry_on_invalid = False + assert trans.execute(request).message == "[Input/Output] Wrong local echo" + client.comm_params.handle_local_echo = False + + # retry on invalid response + trans.retry_on_invalid = True + trans._recv = mock.MagicMock( # pylint: disable=protected-access + side_effect=iter([b"", b"abcdef", b"deadbe", b"123456"]) + ) + response = trans.execute(request) + assert isinstance(response, ModbusIOException) + + # Unable to decode response + trans._recv = mock.MagicMock( # pylint: disable=protected-access + side_effect=ModbusIOException() + ) + client.framer.processIncomingPacket.side_effect = mock.MagicMock( + side_effect=ModbusIOException() + ) + assert isinstance(trans.execute(request), ModbusIOException) + + # Broadcast + client.params.broadcast_enable = True + request.slave_id = 0 + response = trans.execute(request) + assert response == b"Broadcast write sent - no response expected" + + # Broadcast w/ Local echo + client.comm_params.handle_local_echo = True + client.params.broadcast_enable = True + recv = mock.MagicMock(return_value=b"deadbeef") + trans._recv = recv # pylint: disable=protected-access + request.slave_id = 0 + response = trans.execute(request) + assert response == b"Broadcast write sent - no response expected" + recv.assert_called_once_with(8, False) + client.comm_params.handle_local_echo = False + + def test_transaction_manager_tid(self): + """Test the transaction manager TID.""" + for tid in range(1, self._manager.getNextTID() + 10): + assert tid + 1 == self._manager.getNextTID() + self._manager.reset() + assert self._manager.getNextTID() == 1 + + def test_get_transaction_manager_transaction(self): + """Test the getting a transaction from the transaction manager.""" + + class Request: # pylint: disable=too-few-public-methods + """Request.""" + + self._manager.reset() + handle = Request() + handle.transaction_id = ( # pylint: disable=attribute-defined-outside-init + self._manager.getNextTID() + ) + handle.message = b"testing" # pylint: disable=attribute-defined-outside-init + self._manager.addTransaction(handle) + result = self._manager.getTransaction(handle.transaction_id) + assert handle.message == result.message + + def test_delete_transaction_manager_transaction(self): + """Test deleting a transaction from the dict transaction manager.""" + + class Request: # pylint: disable=too-few-public-methods + """Request.""" + + self._manager.reset() + handle = Request() + handle.transaction_id = ( # pylint: disable=attribute-defined-outside-init + self._manager.getNextTID() + ) + handle.message = b"testing" # pylint: disable=attribute-defined-outside-init + + self._manager.addTransaction(handle) + self._manager.delTransaction(handle.transaction_id) + assert not self._manager.getTransaction(handle.transaction_id) + + # ----------------------------------------------------------------------- # + # TCP tests + # ----------------------------------------------------------------------- # + def test_tcp_framer_transaction_ready(self): + """Test a tcp frame transaction.""" + count = 0 + result = None + def callback(data): + """Simulate callback.""" + nonlocal count, result + count += 1 + result = data + + msg = b"\x00\x01\x12\x34\x00\x06\xff\x02\x01\x02\x00\x08" + self._tcp.processIncomingPacket(msg, callback, [1]) + self._tcp._buffer = msg # pylint: disable=protected-access + callback(b'') + + def test_tcp_framer_transaction_full(self): + """Test a full tcp frame transaction.""" + count = 0 + result = None + def callback(data): + """Simulate callback.""" + nonlocal count, result + count += 1 + result = data + + msg = b"\x00\x01\x12\x34\x00\x06\xff\x02\x01\x02\x00\x08" + self._tcp.processIncomingPacket(msg, callback, [0, 1]) + assert result.function_code.to_bytes(1,'big') + result.encode() == msg[7:] + + def test_tcp_framer_transaction_half(self): + """Test a half completed tcp frame transaction.""" + count = 0 + result = None + def callback(data): + """Simulate callback.""" + nonlocal count, result + count += 1 + result = data + + msg1 = b"\x00\x01\x12\x34\x00" + msg2 = b"\x06\xff\x02\x01\x02\x00\x08" + self._tcp.processIncomingPacket(msg1, callback, [0, 1]) + assert not result + self._tcp.processIncomingPacket(msg2, callback, [0, 1]) + assert result + assert result.function_code.to_bytes(1,'big') + result.encode() == msg2[2:] + + def test_tcp_framer_transaction_half2(self): + """Test a half completed tcp frame transaction.""" + count = 0 + result = None + def callback(data): + """Simulate callback.""" + nonlocal count, result + count += 1 + result = data + + msg1 = b"\x00\x01\x12\x34\x00\x06\xff" + msg2 = b"\x02\x01\x02\x00\x08" + self._tcp.processIncomingPacket(msg1, callback, [0, 1]) + assert not result + self._tcp.processIncomingPacket(msg2, callback, [0, 1]) + assert result + assert result.function_code.to_bytes(1,'big') + result.encode() == msg2 + + def test_tcp_framer_transaction_half3(self): + """Test a half completed tcp frame transaction.""" + count = 0 + result = None + def callback(data): + """Simulate callback.""" + nonlocal count, result + count += 1 + result = data + + msg1 = b"\x00\x01\x12\x34\x00\x06\xff\x02\x01\x02\x00" + msg2 = b"\x08" + self._tcp.processIncomingPacket(msg1, callback, [0, 1]) + assert not result + self._tcp.processIncomingPacket(msg2, callback, [0, 1]) + assert result + assert result.function_code.to_bytes(1,'big') + result.encode() == msg1[7:] + msg2 + + def test_tcp_framer_transaction_short(self): + """Test that we can get back on track after an invalid message.""" + count = 0 + result = None + def callback(data): + """Simulate callback.""" + nonlocal count, result + count += 1 + result = data + + # msg1 = b"\x99\x99\x99\x99\x00\x01\x00\x17" + msg1 = b'' + msg2 = b"\x00\x01\x12\x34\x00\x06\xff\x02\x01\x02\x00\x08" + self._tcp.processIncomingPacket(msg1, callback, [0, 1]) + assert not result + self._tcp.processIncomingPacket(msg2, callback, [0, 1]) + assert result + assert result.function_code.to_bytes(1,'big') + result.encode() == msg2[7:] + + def test_tcp_framer_populate(self): + """Test a tcp frame packet build.""" + count = 0 + result = None + def callback(data): + """Simulate callback.""" + nonlocal count, result + count += 1 + result = data + + expected = ModbusRequest() + expected.transaction_id = 0x0001 + expected.protocol_id = 0x1234 + expected.slave_id = 0xFF + msg = b"\x00\x01\x12\x34\x00\x06\xff\x02\x12\x34\x01\x02" + self._tcp.processIncomingPacket(msg, callback, [0, 1]) + # assert self._tcp.checkFrame() + # actual = ModbusRequest() + # self._tcp.populateResult(actual) + # for name in ("transaction_id", "protocol_id", "slave_id"): + # assert getattr(expected, name) == getattr(actual, name) + + def test_tcp_framer_packet(self): + """Test a tcp frame packet build.""" + old_encode = ModbusRequest.encode + ModbusRequest.encode = lambda self: b"" + message = ModbusRequest() + message.transaction_id = 0x0001 + message.protocol_id = 0x0000 + message.slave_id = 0xFF + message.function_code = 0x01 + expected = b"\x00\x01\x00\x00\x00\x02\xff\x01" + actual = self._tcp.buildPacket(message) + assert expected == actual + ModbusRequest.encode = old_encode + + # ----------------------------------------------------------------------- # + # TLS tests + # ----------------------------------------------------------------------- # + def test_framer_tls_framer_transaction_ready(self): + """Test a tls frame transaction.""" + count = 0 + result = None + def callback(data): + """Simulate callback.""" + nonlocal count, result + count += 1 + result = data + + msg = b"\x00\x01\x12\x34\x00\x06\xff\x02\x12\x34\x01\x02" + self._tcp.processIncomingPacket(msg[0:4], callback, [0, 1]) + assert not result + self._tcp.processIncomingPacket(msg[4:], callback, [0, 1]) + assert result + + def test_framer_tls_framer_transaction_full(self): + """Test a full tls frame transaction.""" + count = 0 + result = None + def callback(data): + """Simulate callback.""" + nonlocal count, result + count += 1 + result = data + + msg = b"\x00\x01\x12\x34\x00\x06\xff\x02\x12\x34\x01\x02" + self._tcp.processIncomingPacket(msg, callback, [0, 1]) + assert result + + def test_framer_tls_framer_transaction_half(self): + """Test a half completed tls frame transaction.""" + count = 0 + result = None + def callback(data): + """Simulate callback.""" + nonlocal count, result + count += 1 + result = data + + msg = b"\x00\x01\x12\x34\x00\x06\xff\x02\x12\x34\x01\x02" + self._tcp.processIncomingPacket(msg[0:8], callback, [0, 1]) + assert not result + self._tcp.processIncomingPacket(msg[8:], callback, [0, 1]) + assert result + + def test_framer_tls_framer_transaction_short(self): + """Test that we can get back on track after an invalid message.""" + count = 0 + result = None + def callback(data): + """Simulate callback.""" + nonlocal count, result + count += 1 + result = data + + msg = b"\x00\x01\x12\x34\x00\x06\xff\x02\x12\x34\x01\x02" + self._tcp.processIncomingPacket(msg[0:2], callback, [0, 1]) + assert not result + self._tcp.processIncomingPacket(msg[2:], callback, [0, 1]) + assert result + + def test_framer_tls_framer_decode(self): + """Testmessage decoding.""" + msg1 = b"" + msg2 = b"\x01\x12\x34\x00\x08" + result = self._tls.decode_data(msg1) + assert not result + result = self._tls.decode_data(msg2) + assert result == {"fcode": 1} + + def test_framer_tls_incoming_packet(self): + """Framer tls incoming packet.""" + msg = b"\x00\x01\x12\x34\x00\x06\xff\x02\x12\x34\x01\x02" + + slave = 0x01 + msg_result = None + + def mock_callback(result): + """Mock callback.""" + nonlocal msg_result + + msg_result = result.encode() + + self._tls.processIncomingPacket(msg, mock_callback, slave) + # assert msg == msg_result + + # self._tls.isFrameReady = mock.MagicMock(return_value=True) + # x = mock.MagicMock(return_value=False) + # self._tls._validate_slave_id = x + # self._tls.processIncomingPacket(msg, mock_callback, slave) + # assert not self._tls._buffer + # self._tls.advanceFrame() + # x = mock.MagicMock(return_value=True) + # self._tls._validate_slave_id = x + # self._tls.processIncomingPacket(msg, mock_callback, slave) + # assert msg[1:] == msg_result + # self._tls.advanceFrame() + + def test_framer_tls_process(self): + """Framer tls process.""" + # class MockResult: + # """Mock result.""" + + # def __init__(self, code): + # """Init.""" + # self.function_code = code + + # def mock_callback(_arg): + # """Mock callback.""" + + # self._tls.decoder.decode = mock.MagicMock(return_value=None) + # with pytest.raises(ModbusIOException): + # self._tls._process(mock_callback) + + # result = MockResult(0x01) + # self._tls.decoder.decode = mock.MagicMock(return_value=result) + # with pytest.raises(InvalidMessageReceivedException): + # self._tls._process( + # mock_callback, error=True + # ) + # self._tls._process(mock_callback) + # assert not self._tls._buffer + + def test_framer_tls_framer_populate(self): + """Test a tls frame packet build.""" + count = 0 + result = None + def callback(data): + """Simulate callback.""" + nonlocal count, result + count += 1 + result = data + + msg = b"\x00\x01\x12\x34\x00\x06\xff\x02\x12\x34\x01\x02" + self._tcp.processIncomingPacket(msg, callback, [0, 1]) + assert result + + def test_framer_tls_framer_packet(self): + """Test a tls frame packet build.""" + old_encode = ModbusRequest.encode + ModbusRequest.encode = lambda self: b"" + message = ModbusRequest() + message.function_code = 0x01 + expected = b"\x01" + actual = self._tls.buildPacket(message) + assert expected == actual + ModbusRequest.encode = old_encode + + # ----------------------------------------------------------------------- # + # RTU tests + # ----------------------------------------------------------------------- # + def test_rtu_framer_transaction_ready(self): + """Test if the checks for a complete frame work.""" + count = 0 + result = None + def callback(data): + """Simulate callback.""" + nonlocal count, result + count += 1 + result = data + + msg_parts = [b"\x00\x01\x00", b"\x00\x00\x01\xfc\x1b"] + self._rtu.processIncomingPacket(msg_parts[0], callback, [0, 1]) + assert not result + self._rtu.processIncomingPacket(msg_parts[1], callback, [0, 1]) + assert result + + def test_rtu_framer_transaction_full(self): + """Test a full rtu frame transaction.""" + count = 0 + result = None + def callback(data): + """Simulate callback.""" + nonlocal count, result + count += 1 + result = data + + msg = b"\x00\x01\x00\x00\x00\x01\xfc\x1b" + self._rtu.processIncomingPacket(msg, callback, [0, 1]) + assert result + + def test_rtu_framer_transaction_half(self): + """Test a half completed rtu frame transaction.""" + count = 0 + result = None + def callback(data): + """Simulate callback.""" + nonlocal count, result + count += 1 + result = data + + msg_parts = [b"\x00\x01\x00", b"\x00\x00\x01\xfc\x1b"] + self._rtu.processIncomingPacket(msg_parts[0], callback, [0, 1]) + assert not result + self._rtu.processIncomingPacket(msg_parts[1], callback, [0, 1]) + assert result + + def test_rtu_framer_populate(self): + """Test a rtu frame packet build.""" + count = 0 + result = None + def callback(data): + """Simulate callback.""" + nonlocal count, result + count += 1 + result = data + + msg = b"\x00\x01\x00\x00\x00\x01\xfc\x1b" + self._rtu.processIncomingPacket(msg, callback, [0, 1]) + header_dict = self._rtu._header # pylint: disable=protected-access + assert len(msg) == header_dict["len"] + assert int(msg[0]) == header_dict["uid"] + assert msg[-2:] == header_dict["crc"] + + def test_rtu_framer_packet(self): + """Test a rtu frame packet build.""" + old_encode = ModbusRequest.encode + ModbusRequest.encode = lambda self: b"" + message = ModbusRequest() + message.slave_id = 0xFF + message.function_code = 0x01 + expected = b"\xff\x01\x81\x80" # only header + CRC - no data + actual = self._rtu.buildPacket(message) + assert expected == actual + ModbusRequest.encode = old_encode + + def test_rtu_decode_exception(self): + """Test that the RTU framer can decode errors.""" + count = 0 + result = None + def callback(data): + """Simulate callback.""" + nonlocal count, result + count += 1 + result = data + + msg = b"\x00\x90\x02\x9c\x01" + self._rtu.processIncomingPacket(msg, callback, [0, 1]) + assert result + + def test_process(self): + """Test process.""" + count = 0 + result = None + def callback(data): + """Simulate callback.""" + nonlocal count, result + count += 1 + result = data + + msg = b"\x00\x01\x00\x00\x00\x01\xfc\x1b" + self._rtu.processIncomingPacket(msg, callback, [0, 1]) + assert result + + def test_rtu_process_incoming_packets(self): + """Test rtu process incoming packets.""" + count = 0 + result = None + def callback(data): + """Simulate callback.""" + nonlocal count, result + count += 1 + result = data + + msg = b"\x00\x01\x00\x00\x00\x01\xfc\x1b" + slave = 0x00 + + self._rtu.processIncomingPacket(msg, callback, slave) + assert result + + # ----------------------------------------------------------------------- # + # ASCII tests + # ----------------------------------------------------------------------- # + def test_ascii_framer_transaction_ready(self): + """Test a ascii frame transaction.""" + count = 0 + result = None + def callback(data): + """Simulate callback.""" + nonlocal count, result + count += 1 + result = data + + msg = b":F7031389000A60\r\n" + self._ascii.processIncomingPacket(msg, callback, [0,1]) + assert result + + def test_ascii_framer_transaction_full(self): + """Test a full ascii frame transaction.""" + count = 0 + result = None + def callback(data): + """Simulate callback.""" + nonlocal count, result + count += 1 + result = data + + msg = b"sss:F7031389000A60\r\n" + self._ascii.processIncomingPacket(msg, callback, [0,1]) + assert result + + def test_ascii_framer_transaction_half(self): + """Test a half completed ascii frame transaction.""" + count = 0 + result = None + def callback(data): + """Simulate callback.""" + nonlocal count, result + count += 1 + result = data + + msg_parts = (b"sss:F7031389", b"000A60\r\n") + self._ascii.processIncomingPacket(msg_parts[0], callback, [0,1]) + assert not result + self._ascii.processIncomingPacket(msg_parts[1], callback, [0,1]) + assert result + + def test_ascii_framer_populate(self): + """Test a ascii frame packet build.""" + request = ModbusRequest() + self._ascii.populateResult(request) + assert not request.slave_id + + def test_ascii_framer_packet(self): + """Test a ascii frame packet build.""" + old_encode = ModbusRequest.encode + ModbusRequest.encode = lambda self: b"" + message = ModbusRequest() + message.slave_id = 0xFF + message.function_code = 0x01 + expected = b":FF0100\r\n" + actual = self._ascii.buildPacket(message) + assert expected == actual + ModbusRequest.encode = old_encode + + def test_ascii_process_incoming_packets(self): + """Test ascii process incoming packet.""" + count = 0 + result = None + def callback(data): + """Simulate callback.""" + nonlocal count, result + count += 1 + result = data + + msg = b":F7031389000A60\r\n" + self._ascii.processIncomingPacket(msg, callback, [0,1]) + assert result diff --git a/test/message/test_tls.py b/test/framers/test_tls.py similarity index 96% rename from test/message/test_tls.py rename to test/framers/test_tls.py index 194fda459..15eb4a450 100644 --- a/test/message/test_tls.py +++ b/test/framers/test_tls.py @@ -2,7 +2,7 @@ import pytest -from pymodbus.message.tls import MessageTLS +from pymodbus.framer.tls import MessageTLS class TestMessageSocket: diff --git a/test/test_transaction.py b/test/test_transaction.py index 0e237554e..b072909f8 100755 --- a/test/test_transaction.py +++ b/test/test_transaction.py @@ -242,6 +242,7 @@ def callback(data): msg = b"\x00\x01\x12\x34\x00\x06\xff\x02\x01\x02\x00\x08" self._tcp.processIncomingPacket(msg, callback, [1]) self._tcp._buffer = msg # pylint: disable=protected-access + callback(b'') def test_tcp_framer_transaction_full(self): """Test a full tcp frame transaction.""" From 5968f6ae9cc5f045f760ee97d208c88966967882 Mon Sep 17 00:00:00 2001 From: jan iversen Date: Fri, 29 Mar 2024 12:56:41 +0100 Subject: [PATCH 2/5] Rename message to framing. --- pymodbus/framer/__init__.py | 17 ++--- pymodbus/framer/{message.py => framer.py} | 31 ++++---- test/framers/conftest.py | 6 +- test/framers/test_message.py | 88 +++++++++++------------ 4 files changed, 68 insertions(+), 74 deletions(-) rename pymodbus/framer/{message.py => framer.py} (81%) diff --git a/pymodbus/framer/__init__.py b/pymodbus/framer/__init__.py index 614b440bc..c9eda8a1e 100644 --- a/pymodbus/framer/__init__.py +++ b/pymodbus/framer/__init__.py @@ -7,14 +7,11 @@ "ModbusRtuFramer", "ModbusSocketFramer", "ModbusTlsFramer", - "Message", - "MessageType", + "Framing", + "FramerType", ] - -import enum - -from pymodbus.framer.message import Message, MessageType +from pymodbus.framer.framer import FramerType, Framing from pymodbus.framer.old_framer_ascii import ModbusAsciiFramer from pymodbus.framer.old_framer_base import ModbusFramer from pymodbus.framer.old_framer_rtu import ModbusRtuFramer @@ -22,13 +19,7 @@ from pymodbus.framer.old_framer_tls import ModbusTlsFramer -class Framer(str, enum.Enum): - """These represent the different framers.""" - - ASCII = "ascii" - RTU = "rtu" - SOCKET = "socket" - TLS = "tls" +Framer = FramerType FRAMER_NAME_TO_CLASS = { diff --git a/pymodbus/framer/message.py b/pymodbus/framer/framer.py similarity index 81% rename from pymodbus/framer/message.py rename to pymodbus/framer/framer.py index a8f314d03..690190dfb 100644 --- a/pymodbus/framer/message.py +++ b/pymodbus/framer/framer.py @@ -1,8 +1,11 @@ -"""ModbusMessage layer. +"""Framing layer. -The message layer is responsible for encoding/decoding requests/responses. +The framing layer is responsible for isolating/generating the request/request from +the frame (prefix - postfix) According to the selected type of modbus frame a prefix/suffix is added/removed + +This layer is also responsible for discarding invalid frames and frames for other slaves. """ from __future__ import annotations @@ -18,7 +21,7 @@ from pymodbus.transport.transport import CommParams, ModbusProtocol -class MessageType(str, Enum): +class FramerType(str, Enum): """Type of Modbus frame.""" RAW = "raw" # only used for testing @@ -28,15 +31,15 @@ class MessageType(str, Enum): TLS = "tls" -class Message(ModbusProtocol): - """Message layer extending transport layer. +class Framing(ModbusProtocol): + """Framing layer extending transport layer. - extends the ModbusProtocol to handle receiving and sending of complete modbus messsagees. + extends the ModbusProtocol to handle receiving and sending of complete modbus PDU. - Message is the prefix / suffix around the response/request + Framing is the prefix / suffix around the response/request When receiving: - - Secures full valid Modbus message is received (across multiple callbacks) + - Secures full valid Modbus PDU is received (across multiple callbacks) - Validates and removes Modbus prefix/suffix (CRC for serial, MBAP for others) - Callback with pure request/response - Skips invalid messagees @@ -51,7 +54,7 @@ class Message(ModbusProtocol): """ def __init__(self, - message_type: MessageType, + message_type: FramerType, params: CommParams, is_server: bool, device_ids: list[int], @@ -67,11 +70,11 @@ def __init__(self, self.device_ids = device_ids self.broadcast: bool = (0 in device_ids) self.msg_handle: MessageBase = { - MessageType.RAW: MessageRaw(), - MessageType.ASCII: MessageAscii(), - MessageType.RTU: MessageRTU(), - MessageType.SOCKET: MessageSocket(), - MessageType.TLS: MessageTLS(), + FramerType.RAW: MessageRaw(), + FramerType.ASCII: MessageAscii(), + FramerType.RTU: MessageRTU(), + FramerType.SOCKET: MessageSocket(), + FramerType.TLS: MessageTLS(), }[message_type] diff --git a/test/framers/conftest.py b/test/framers/conftest.py index d16b22e18..5a708c30c 100644 --- a/test/framers/conftest.py +++ b/test/framers/conftest.py @@ -5,15 +5,15 @@ import pytest -from pymodbus.framer import Message, MessageType +from pymodbus.framer import FramerType, Framing from pymodbus.transport import CommParams, ModbusProtocol -class DummyMessage(Message): +class DummyMessage(Framing): """Implement use of ModbusProtocol.""" def __init__(self, - message_type: MessageType, + message_type: FramerType, params: CommParams, is_server: bool, device_ids: list[int] | None, diff --git a/test/framers/test_message.py b/test/framers/test_message.py index 3d515fd80..b8493d988 100644 --- a/test/framers/test_message.py +++ b/test/framers/test_message.py @@ -4,7 +4,7 @@ import pytest -from pymodbus.framer import MessageType +from pymodbus.framer import FramerType from pymodbus.framer.ascii import MessageAscii from pymodbus.framer.rtu import MessageRTU from pymodbus.framer.socket import MessageSocket @@ -20,14 +20,14 @@ class TestMessage: async def prepare_message(dummy_message): """Return message object.""" return dummy_message( - MessageType.RAW, + FramerType.RAW, CommParams(), False, [1], ) - @pytest.mark.parametrize(("entry"), list(MessageType)) + @pytest.mark.parametrize(("entry"), list(FramerType)) async def test_message_init(self, entry, dummy_message): """Test message type.""" msg = dummy_message(entry.value, @@ -226,45 +226,45 @@ def test_encode(self, frame, frame_expected, data, dev_id, tid, inx1, inx2, inx3 @pytest.mark.parametrize( ("msg_type", "data", "dev_id", "tid", "expected"), [ - (MessageType.ASCII, b':0003007C00027F\r\n', 0, 0, b"\x03\x00\x7c\x00\x02",), # Request - (MessageType.ASCII, b':000304008D008EDE\r\n', 0, 0, b"\x03\x04\x00\x8d\x00\x8e",), # Response - (MessageType.ASCII, b':0083027B\r\n', 0, 0, b'\x83\x02',), # Exception - (MessageType.ASCII, b':1103007C00026E\r\n', 17, 0, b"\x03\x00\x7c\x00\x02",), # Request - (MessageType.ASCII, b':110304008D008ECD\r\n', 17, 0, b"\x03\x04\x00\x8d\x00\x8e",), # Response - (MessageType.ASCII, b':1183026A\r\n', 17, 0, b'\x83\x02',), # Exception - (MessageType.ASCII, b':FF03007C000280\r\n', 255, 0, b"\x03\x00\x7c\x00\x02",), # Request - (MessageType.ASCII, b':FF0304008D008EDF\r\n', 255, 0, b"\x03\x04\x00\x8d\x00\x8e",), # Response - (MessageType.ASCII, b':FF83027C\r\n', 255, 0, b'\x83\x02',), # Exception - (MessageType.RTU, b'\x00\x03\x00\x7c\x00\x02\x04\x02', 0, 0, b"\x03\x00\x7c\x00\x02",), # Request - (MessageType.RTU, b'\x00\x03\x04\x00\x8d\x00\x8e\xfa\xbc', 0, 0, b"\x03\x04\x00\x8d\x00\x8e",), # Response - (MessageType.RTU, b'\x00\x83\x02\x91\x31', 0, 0, b'\x83\x02',), # Exception - (MessageType.RTU, b'\x11\x03\x00\x7c\x00\x02\x07\x43', 17, 0, b"\x03\x00\x7c\x00\x02",), # Request - (MessageType.RTU, b'\x11\x03\x04\x00\x8d\x00\x8e\xfb\xbd', 17, 0, b"\x03\x04\x00\x8d\x00\x8e",), # Response - (MessageType.RTU, b'\x11\x83\x02\xc1\x34', 17, 0, b'\x83\x02',), # Exception - (MessageType.RTU, b'\xff\x03\x00|\x00\x02\x10\x0d', 255, 0, b"\x03\x00\x7c\x00\x02",), # Request - (MessageType.RTU, b'\xff\x03\x04\x00\x8d\x00\x8e\xf5\xb3', 255, 0, b"\x03\x04\x00\x8d\x00\x8e",), # Response - (MessageType.RTU, b'\xff\x83\x02\xa1\x01', 255, 0, b'\x83\x02',), # Exception - (MessageType.SOCKET, b'\x00\x00\x00\x00\x00\x06\x00\x03\x00\x7c\x00\x02', 0, 0, b"\x03\x00\x7c\x00\x02",), # Request - (MessageType.SOCKET, b'\x00\x00\x00\x00\x00\x07\x00\x03\x04\x00\x8d\x00\x8e', 0, 0, b"\x03\x04\x00\x8d\x00\x8e",), # Response - (MessageType.SOCKET, b'\x00\x00\x00\x00\x00\x03\x00\x83\x02', 0, 0, b'\x83\x02',), # Exception - (MessageType.SOCKET, b'\x00\x00\x00\x00\x00\x06\x11\x03\x00\x7c\x00\x02', 17, 0, b"\x03\x00\x7c\x00\x02",), # Request - (MessageType.SOCKET, b'\x00\x00\x00\x00\x00\x07\x11\x03\x04\x00\x8d\x00\x8e', 17, 0, b"\x03\x04\x00\x8d\x00\x8e",), # Response - (MessageType.SOCKET, b'\x00\x00\x00\x00\x00\x03\x11\x83\x02', 17, 0, b'\x83\x02',), # Exception - (MessageType.SOCKET, b'\x00\x00\x00\x00\x00\x06\xff\x03\x00\x7c\x00\x02', 255, 0, b"\x03\x00\x7c\x00\x02",), # Request - (MessageType.SOCKET, b'\x00\x00\x00\x00\x00\x07\xff\x03\x04\x00\x8d\x00\x8e', 255, 0, b"\x03\x04\x00\x8d\x00\x8e",), # Response - (MessageType.SOCKET, b'\x00\x00\x00\x00\x00\x03\xff\x83\x02', 255, 0, b'\x83\x02',), # Exception - (MessageType.SOCKET, b'\x0c\x05\x00\x00\x00\x06\x00\x03\x00\x7c\x00\x02', 0, 3077, b"\x03\x00\x7c\x00\x02",), # Request - (MessageType.SOCKET, b'\x0c\x05\x00\x00\x00\x07\x00\x03\x04\x00\x8d\x00\x8e', 0, 3077, b"\x03\x04\x00\x8d\x00\x8e",), # Response - (MessageType.SOCKET, b'\x0c\x05\x00\x00\x00\x03\x00\x83\x02', 0, 3077, b'\x83\x02',), # Exception - (MessageType.SOCKET, b'\x0c\x05\x00\x00\x00\x06\x11\x03\x00\x7c\x00\x02', 17, 3077, b"\x03\x00\x7c\x00\x02",), # Request - (MessageType.SOCKET, b'\x0c\x05\x00\x00\x00\x07\x11\x03\x04\x00\x8d\x00\x8e', 17, 3077, b"\x03\x04\x00\x8d\x00\x8e",), # Response - (MessageType.SOCKET, b'\x0c\x05\x00\x00\x00\x03\x11\x83\x02', 17, 3077, b'\x83\x02',), # Exception - (MessageType.SOCKET, b'\x0c\x05\x00\x00\x00\x06\xff\x03\x00\x7c\x00\x02', 255, 3077, b"\x03\x00\x7c\x00\x02",), # Request - (MessageType.SOCKET, b'\x0c\x05\x00\x00\x00\x07\xff\x03\x04\x00\x8d\x00\x8e', 255, 3077, b"\x03\x04\x00\x8d\x00\x8e",), # Response - (MessageType.SOCKET, b'\x0c\x05\x00\x00\x00\x03\xff\x83\x02', 255, 3077, b'\x83\x02',), # Exception - (MessageType.TLS, b'\x03\x00\x7c\x00\x02', 0, 0, b"\x03\x00\x7c\x00\x02",), # Request - (MessageType.TLS, b'\x03\x04\x00\x8d\x00\x8e', 0, 0, b"\x03\x04\x00\x8d\x00\x8e",), # Response - (MessageType.TLS, b'\x83\x02', 0, 0, b'\x83\x02',), # Exception + (FramerType.ASCII, b':0003007C00027F\r\n', 0, 0, b"\x03\x00\x7c\x00\x02",), # Request + (FramerType.ASCII, b':000304008D008EDE\r\n', 0, 0, b"\x03\x04\x00\x8d\x00\x8e",), # Response + (FramerType.ASCII, b':0083027B\r\n', 0, 0, b'\x83\x02',), # Exception + (FramerType.ASCII, b':1103007C00026E\r\n', 17, 0, b"\x03\x00\x7c\x00\x02",), # Request + (FramerType.ASCII, b':110304008D008ECD\r\n', 17, 0, b"\x03\x04\x00\x8d\x00\x8e",), # Response + (FramerType.ASCII, b':1183026A\r\n', 17, 0, b'\x83\x02',), # Exception + (FramerType.ASCII, b':FF03007C000280\r\n', 255, 0, b"\x03\x00\x7c\x00\x02",), # Request + (FramerType.ASCII, b':FF0304008D008EDF\r\n', 255, 0, b"\x03\x04\x00\x8d\x00\x8e",), # Response + (FramerType.ASCII, b':FF83027C\r\n', 255, 0, b'\x83\x02',), # Exception + (FramerType.RTU, b'\x00\x03\x00\x7c\x00\x02\x04\x02', 0, 0, b"\x03\x00\x7c\x00\x02",), # Request + (FramerType.RTU, b'\x00\x03\x04\x00\x8d\x00\x8e\xfa\xbc', 0, 0, b"\x03\x04\x00\x8d\x00\x8e",), # Response + (FramerType.RTU, b'\x00\x83\x02\x91\x31', 0, 0, b'\x83\x02',), # Exception + (FramerType.RTU, b'\x11\x03\x00\x7c\x00\x02\x07\x43', 17, 0, b"\x03\x00\x7c\x00\x02",), # Request + (FramerType.RTU, b'\x11\x03\x04\x00\x8d\x00\x8e\xfb\xbd', 17, 0, b"\x03\x04\x00\x8d\x00\x8e",), # Response + (FramerType.RTU, b'\x11\x83\x02\xc1\x34', 17, 0, b'\x83\x02',), # Exception + (FramerType.RTU, b'\xff\x03\x00|\x00\x02\x10\x0d', 255, 0, b"\x03\x00\x7c\x00\x02",), # Request + (FramerType.RTU, b'\xff\x03\x04\x00\x8d\x00\x8e\xf5\xb3', 255, 0, b"\x03\x04\x00\x8d\x00\x8e",), # Response + (FramerType.RTU, b'\xff\x83\x02\xa1\x01', 255, 0, b'\x83\x02',), # Exception + (FramerType.SOCKET, b'\x00\x00\x00\x00\x00\x06\x00\x03\x00\x7c\x00\x02', 0, 0, b"\x03\x00\x7c\x00\x02",), # Request + (FramerType.SOCKET, b'\x00\x00\x00\x00\x00\x07\x00\x03\x04\x00\x8d\x00\x8e', 0, 0, b"\x03\x04\x00\x8d\x00\x8e",), # Response + (FramerType.SOCKET, b'\x00\x00\x00\x00\x00\x03\x00\x83\x02', 0, 0, b'\x83\x02',), # Exception + (FramerType.SOCKET, b'\x00\x00\x00\x00\x00\x06\x11\x03\x00\x7c\x00\x02', 17, 0, b"\x03\x00\x7c\x00\x02",), # Request + (FramerType.SOCKET, b'\x00\x00\x00\x00\x00\x07\x11\x03\x04\x00\x8d\x00\x8e', 17, 0, b"\x03\x04\x00\x8d\x00\x8e",), # Response + (FramerType.SOCKET, b'\x00\x00\x00\x00\x00\x03\x11\x83\x02', 17, 0, b'\x83\x02',), # Exception + (FramerType.SOCKET, b'\x00\x00\x00\x00\x00\x06\xff\x03\x00\x7c\x00\x02', 255, 0, b"\x03\x00\x7c\x00\x02",), # Request + (FramerType.SOCKET, b'\x00\x00\x00\x00\x00\x07\xff\x03\x04\x00\x8d\x00\x8e', 255, 0, b"\x03\x04\x00\x8d\x00\x8e",), # Response + (FramerType.SOCKET, b'\x00\x00\x00\x00\x00\x03\xff\x83\x02', 255, 0, b'\x83\x02',), # Exception + (FramerType.SOCKET, b'\x0c\x05\x00\x00\x00\x06\x00\x03\x00\x7c\x00\x02', 0, 3077, b"\x03\x00\x7c\x00\x02",), # Request + (FramerType.SOCKET, b'\x0c\x05\x00\x00\x00\x07\x00\x03\x04\x00\x8d\x00\x8e', 0, 3077, b"\x03\x04\x00\x8d\x00\x8e",), # Response + (FramerType.SOCKET, b'\x0c\x05\x00\x00\x00\x03\x00\x83\x02', 0, 3077, b'\x83\x02',), # Exception + (FramerType.SOCKET, b'\x0c\x05\x00\x00\x00\x06\x11\x03\x00\x7c\x00\x02', 17, 3077, b"\x03\x00\x7c\x00\x02",), # Request + (FramerType.SOCKET, b'\x0c\x05\x00\x00\x00\x07\x11\x03\x04\x00\x8d\x00\x8e', 17, 3077, b"\x03\x04\x00\x8d\x00\x8e",), # Response + (FramerType.SOCKET, b'\x0c\x05\x00\x00\x00\x03\x11\x83\x02', 17, 3077, b'\x83\x02',), # Exception + (FramerType.SOCKET, b'\x0c\x05\x00\x00\x00\x06\xff\x03\x00\x7c\x00\x02', 255, 3077, b"\x03\x00\x7c\x00\x02",), # Request + (FramerType.SOCKET, b'\x0c\x05\x00\x00\x00\x07\xff\x03\x04\x00\x8d\x00\x8e', 255, 3077, b"\x03\x04\x00\x8d\x00\x8e",), # Response + (FramerType.SOCKET, b'\x0c\x05\x00\x00\x00\x03\xff\x83\x02', 255, 3077, b'\x83\x02',), # Exception + (FramerType.TLS, b'\x03\x00\x7c\x00\x02', 0, 0, b"\x03\x00\x7c\x00\x02",), # Request + (FramerType.TLS, b'\x03\x04\x00\x8d\x00\x8e', 0, 0, b"\x03\x04\x00\x8d\x00\x8e",), # Response + (FramerType.TLS, b'\x83\x02', 0, 0, b'\x83\x02',), # Exception ] ) @pytest.mark.parametrize( @@ -277,9 +277,9 @@ def test_encode(self, frame, frame_expected, data, dev_id, tid, inx1, inx2, inx3 ) async def test_decode(self, dummy_message, msg_type, data, dev_id, tid, expected, split): """Test encode method.""" - if msg_type == MessageType.RTU: + if msg_type == FramerType.RTU: pytest.skip("Waiting on implementation!") - if msg_type == MessageType.TLS and split != "no": + if msg_type == FramerType.TLS and split != "no": return frame = dummy_message( msg_type, From 0c9a80021aa0934dd20150a780bf8c0b53d451d6 Mon Sep 17 00:00:00 2001 From: jan iversen Date: Fri, 29 Mar 2024 13:25:13 +0100 Subject: [PATCH 3/5] Framer ==> FramerType. --- API_changes.rst | 1 + check_ci.sh | 2 +- examples/client_custom_msg.py | 4 +-- examples/client_performance.py | 6 ++-- examples/package_test_tool.py | 6 ++-- examples/server_hook.py | 4 +-- examples/simple_async_client.py | 6 ++-- examples/simple_sync_client.py | 6 ++-- examples/simulator.py | 4 +-- pymodbus/__init__.py | 4 +-- pymodbus/client/base.py | 6 ++-- pymodbus/client/serial.py | 6 ++-- pymodbus/client/tcp.py | 6 ++-- pymodbus/client/tls.py | 6 ++-- pymodbus/client/udp.py | 6 ++-- pymodbus/framer/__init__.py | 15 ++++------ pymodbus/framer/framer.py | 2 +- pymodbus/server/async_io.py | 18 ++++++------ test/framers/conftest.py | 4 +-- test/framers/test_old_framers.py | 4 +-- test/sub_client/test_client.py | 38 +++++++++++++------------- test/sub_client/test_client_sync.py | 14 +++++----- test/sub_server/test_server_asyncio.py | 8 +++--- 23 files changed, 87 insertions(+), 89 deletions(-) diff --git a/API_changes.rst b/API_changes.rst index 6e4fb08be..4ec98de3f 100644 --- a/API_changes.rst +++ b/API_changes.rst @@ -10,6 +10,7 @@ API changes 3.7.0 - on_reconnect_callback() removed from clients (sync/async). - on_connect_callback(true/false) added to async clients. - binary framer no longer supported +- Framer. renamed to FramerType. API changes 3.6.0 diff --git a/check_ci.sh b/check_ci.sh index 109b947ba..873c2311e 100755 --- a/check_ci.sh +++ b/check_ci.sh @@ -9,5 +9,5 @@ codespell ruff check --fix --exit-non-zero-on-fix . pylint --recursive=y examples pymodbus test mypy pymodbus -pytest --cov --numprocesses auto +pytest -x --cov --numprocesses auto echo "Ready to push" diff --git a/examples/client_custom_msg.py b/examples/client_custom_msg.py index 8b353e342..01edf40f2 100755 --- a/examples/client_custom_msg.py +++ b/examples/client_custom_msg.py @@ -13,7 +13,7 @@ import asyncio import struct -from pymodbus import Framer +from pymodbus import FramerType from pymodbus.bit_read_message import ReadCoilsRequest from pymodbus.client import AsyncModbusTcpClient as ModbusClient from pymodbus.pdu import ModbusExceptions, ModbusRequest, ModbusResponse @@ -118,7 +118,7 @@ def __init__(self, address, **kwargs): async def main(host="localhost", port=5020): """Run versions of read coil.""" - async with ModbusClient(host=host, port=port, framer_name=Framer.SOCKET) as client: + async with ModbusClient(host=host, port=port, framer_name=FramerType.SOCKET) as client: await client.connect() # new modbus function code. diff --git a/examples/client_performance.py b/examples/client_performance.py index 09cf25572..4cc9e014d 100755 --- a/examples/client_performance.py +++ b/examples/client_performance.py @@ -16,7 +16,7 @@ import asyncio import time -from pymodbus import Framer +from pymodbus import FramerType from pymodbus.client import AsyncModbusSerialClient, ModbusSerialClient @@ -29,7 +29,7 @@ def run_sync_client_test(): print("--- Testing sync client v3.4.1") client = ModbusSerialClient( "/dev/ttys007", - framer_name=Framer.RTU, + framer_name=FramerType.RTU, baudrate=9600, ) client.connect() @@ -56,7 +56,7 @@ async def run_async_client_test(): print("--- Testing async client v3.4.1") client = AsyncModbusSerialClient( "/dev/ttys007", - framer_name=Framer.RTU, + framer_name=FramerType.RTU, baudrate=9600, ) await client.connect() diff --git a/examples/package_test_tool.py b/examples/package_test_tool.py index 6f117a5eb..e46433863 100755 --- a/examples/package_test_tool.py +++ b/examples/package_test_tool.py @@ -49,7 +49,7 @@ import pymodbus.client as modbusClient import pymodbus.server as modbusServer -from pymodbus import Framer, ModbusException, pymodbus_apply_logging_config +from pymodbus import FramerType, ModbusException, pymodbus_apply_logging_config from pymodbus.datastore import ( ModbusSequentialDataBlock, ModbusServerContext, @@ -160,14 +160,14 @@ def __init__(self, comm: CommType): if comm == CommType.TCP: self.server = modbusServer.ModbusTcpServer( self.context, - framer=Framer.SOCKET, + framer=FramerType.SOCKET, identity=self.identity, address=(NULLMODEM_HOST, test_port), ) elif comm == CommType.SERIAL: self.server = modbusServer.ModbusSerialServer( self.context, - framer=Framer.SOCKET, + framer=FramerType.SOCKET, identity=self.identity, port=f"{NULLMODEM_HOST}:{test_port}", ) diff --git a/examples/server_hook.py b/examples/server_hook.py index 755b8f933..89a8baecd 100755 --- a/examples/server_hook.py +++ b/examples/server_hook.py @@ -7,7 +7,7 @@ import asyncio import logging -from pymodbus import Framer, pymodbus_apply_logging_config +from pymodbus import FramerType, pymodbus_apply_logging_config from pymodbus.datastore import ( ModbusSequentialDataBlock, ModbusServerContext, @@ -63,7 +63,7 @@ async def setup(self): ) self.server = ModbusTcpServer( context, - Framer.SOCKET, + FramerType.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 91f9665af..5030bfc5a 100755 --- a/examples/simple_async_client.py +++ b/examples/simple_async_client.py @@ -14,13 +14,13 @@ import pymodbus.client as ModbusClient from pymodbus import ( ExceptionResponse, - Framer, + FramerType, ModbusException, pymodbus_apply_logging_config, ) -async def run_async_simple_client(comm, host, port, framer=Framer.SOCKET): +async def run_async_simple_client(comm, host, port, framer=FramerType.SOCKET): """Run async client.""" # activate debugging pymodbus_apply_logging_config("DEBUG") @@ -64,7 +64,7 @@ async def run_async_simple_client(comm, host, port, framer=Framer.SOCKET): client = ModbusClient.AsyncModbusTlsClient( host, port=port, - framer=Framer.TLS, + framer=FramerType.TLS, # timeout=10, # retries=3, # retry_on_empty=False, diff --git a/examples/simple_sync_client.py b/examples/simple_sync_client.py index 4189c9e25..80a8b9e77 100755 --- a/examples/simple_sync_client.py +++ b/examples/simple_sync_client.py @@ -16,13 +16,13 @@ import pymodbus.client as ModbusClient from pymodbus import ( ExceptionResponse, - Framer, + FramerType, ModbusException, pymodbus_apply_logging_config, ) -def run_sync_simple_client(comm, host, port, framer=Framer.SOCKET): +def run_sync_simple_client(comm, host, port, framer=FramerType.SOCKET): """Run sync client.""" # activate debugging pymodbus_apply_logging_config("DEBUG") @@ -66,7 +66,7 @@ def run_sync_simple_client(comm, host, port, framer=Framer.SOCKET): client = ModbusClient.ModbusTlsClient( host, port=port, - framer=Framer.TLS, + framer=FramerType.TLS, # timeout=10, # retries=3, # retry_on_empty=False, diff --git a/examples/simulator.py b/examples/simulator.py index b75c92f7d..58f7ce6fb 100755 --- a/examples/simulator.py +++ b/examples/simulator.py @@ -10,7 +10,7 @@ import asyncio import logging -from pymodbus import Framer +from pymodbus import FramerType from pymodbus.client import AsyncModbusTcpClient from pymodbus.datastore import ModbusSimulatorContext from pymodbus.server import ModbusSimulatorServer, get_simulator_commandline @@ -74,7 +74,7 @@ async def run_simulator(): client = AsyncModbusTcpClient( "127.0.0.1", port=5020, - framer=Framer.SOCKET, + framer=FramerType.SOCKET, ) await client.connect() assert client.connected diff --git a/pymodbus/__init__.py b/pymodbus/__init__.py index 28b87963c..3de59b0ff 100644 --- a/pymodbus/__init__.py +++ b/pymodbus/__init__.py @@ -5,7 +5,7 @@ __all__ = [ "ExceptionResponse", - "Framer", + "FramerType", "ModbusException", "pymodbus_apply_logging_config", "__version__", @@ -13,7 +13,7 @@ ] from pymodbus.exceptions import ModbusException -from pymodbus.framer import Framer +from pymodbus.framer import FramerType from pymodbus.logging import pymodbus_apply_logging_config from pymodbus.pdu import ExceptionResponse diff --git a/pymodbus/client/base.py b/pymodbus/client/base.py index fea561d1b..cefd2060d 100644 --- a/pymodbus/client/base.py +++ b/pymodbus/client/base.py @@ -10,7 +10,7 @@ from pymodbus.client.mixin import ModbusClientMixin from pymodbus.exceptions import ConnectionException, ModbusIOException from pymodbus.factory import ClientDecoder -from pymodbus.framer import FRAMER_NAME_TO_CLASS, Framer, ModbusFramer +from pymodbus.framer import FRAMER_NAME_TO_CLASS, FramerType, ModbusFramer from pymodbus.logging import Log from pymodbus.pdu import ModbusRequest, ModbusResponse from pymodbus.transaction import ModbusTransactionManager @@ -49,7 +49,7 @@ class ModbusBaseClient(ModbusClientMixin[Awaitable[ModbusResponse]], ModbusProto def __init__( self, - framer: Framer, + framer: FramerType, timeout: float = 3, retries: int = 3, retry_on_empty: bool = False, @@ -308,7 +308,7 @@ class _params: def __init__( self, - framer: Framer, + framer: FramerType, timeout: float = 3, retries: int = 3, retry_on_empty: bool = False, diff --git a/pymodbus/client/serial.py b/pymodbus/client/serial.py index 7726a3b33..216a6308a 100644 --- a/pymodbus/client/serial.py +++ b/pymodbus/client/serial.py @@ -8,7 +8,7 @@ from pymodbus.client.base import ModbusBaseClient, ModbusBaseSyncClient from pymodbus.exceptions import ConnectionException -from pymodbus.framer import Framer +from pymodbus.framer import FramerType from pymodbus.logging import Log from pymodbus.transport import CommType from pymodbus.utilities import ModbusTransactionState @@ -69,7 +69,7 @@ async def run(): def __init__( self, port: str, - framer: Framer = Framer.RTU, + framer: FramerType = FramerType.RTU, baudrate: int = 19200, bytesize: int = 8, parity: str = "N", @@ -158,7 +158,7 @@ def run(): def __init__( self, port: str, - framer: Framer = Framer.RTU, + framer: FramerType = FramerType.RTU, baudrate: int = 19200, bytesize: int = 8, parity: str = "N", diff --git a/pymodbus/client/tcp.py b/pymodbus/client/tcp.py index cc6ce6c84..1684cfb5f 100644 --- a/pymodbus/client/tcp.py +++ b/pymodbus/client/tcp.py @@ -9,7 +9,7 @@ from pymodbus.client.base import ModbusBaseClient, ModbusBaseSyncClient from pymodbus.exceptions import ConnectionException -from pymodbus.framer import Framer +from pymodbus.framer import FramerType from pymodbus.logging import Log from pymodbus.transport import CommType @@ -59,7 +59,7 @@ def __init__( self, host: str, port: int = 502, - framer: Framer = Framer.SOCKET, + framer: FramerType = FramerType.SOCKET, source_address: tuple[str, int] | None = None, **kwargs: Any, ) -> None: @@ -139,7 +139,7 @@ def __init__( self, host: str, port: int = 502, - framer: Framer = Framer.SOCKET, + framer: FramerType = FramerType.SOCKET, source_address: tuple[str, int] | None = None, **kwargs: Any, ) -> None: diff --git a/pymodbus/client/tls.py b/pymodbus/client/tls.py index 58f80e51e..d7adb9861 100644 --- a/pymodbus/client/tls.py +++ b/pymodbus/client/tls.py @@ -6,7 +6,7 @@ from typing import Any from pymodbus.client.tcp import AsyncModbusTcpClient, ModbusTcpClient -from pymodbus.framer import Framer +from pymodbus.framer import FramerType from pymodbus.logging import Log from pymodbus.transport import CommParams, CommType @@ -56,7 +56,7 @@ def __init__( self, host: str, port: int = 802, - framer: Framer = Framer.TLS, + framer: FramerType = FramerType.TLS, sslctx: ssl.SSLContext = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT), server_hostname: str | None = None, **kwargs: Any, @@ -153,7 +153,7 @@ def __init__( self, host: str, port: int = 802, - framer: Framer = Framer.TLS, + framer: FramerType = FramerType.TLS, sslctx: ssl.SSLContext = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT), server_hostname: str | None = None, **kwargs: Any, diff --git a/pymodbus/client/udp.py b/pymodbus/client/udp.py index 03115143d..48cb8bcc1 100644 --- a/pymodbus/client/udp.py +++ b/pymodbus/client/udp.py @@ -7,7 +7,7 @@ from pymodbus.client.base import ModbusBaseClient, ModbusBaseSyncClient from pymodbus.exceptions import ConnectionException -from pymodbus.framer import Framer +from pymodbus.framer import FramerType from pymodbus.logging import Log from pymodbus.transport import CommType @@ -60,7 +60,7 @@ def __init__( self, host: str, port: int = 502, - framer: Framer = Framer.SOCKET, + framer: FramerType = FramerType.SOCKET, source_address: tuple[str, int] | None = None, **kwargs: Any, ) -> None: @@ -143,7 +143,7 @@ def __init__( self, host: str, port: int = 502, - framer: Framer = Framer.SOCKET, + framer: FramerType = FramerType.SOCKET, source_address: tuple[str, int] | None = None, **kwargs: Any, ) -> None: diff --git a/pymodbus/framer/__init__.py b/pymodbus/framer/__init__.py index c9eda8a1e..32c61d817 100644 --- a/pymodbus/framer/__init__.py +++ b/pymodbus/framer/__init__.py @@ -7,11 +7,11 @@ "ModbusRtuFramer", "ModbusSocketFramer", "ModbusTlsFramer", - "Framing", + "Framer", "FramerType", ] -from pymodbus.framer.framer import FramerType, Framing +from pymodbus.framer.framer import Framer, FramerType from pymodbus.framer.old_framer_ascii import ModbusAsciiFramer from pymodbus.framer.old_framer_base import ModbusFramer from pymodbus.framer.old_framer_rtu import ModbusRtuFramer @@ -19,12 +19,9 @@ from pymodbus.framer.old_framer_tls import ModbusTlsFramer -Framer = FramerType - - FRAMER_NAME_TO_CLASS = { - Framer.ASCII: ModbusAsciiFramer, - Framer.RTU: ModbusRtuFramer, - Framer.SOCKET: ModbusSocketFramer, - Framer.TLS: ModbusTlsFramer, + FramerType.ASCII: ModbusAsciiFramer, + FramerType.RTU: ModbusRtuFramer, + FramerType.SOCKET: ModbusSocketFramer, + FramerType.TLS: ModbusTlsFramer, } diff --git a/pymodbus/framer/framer.py b/pymodbus/framer/framer.py index 690190dfb..d93cd8dd2 100644 --- a/pymodbus/framer/framer.py +++ b/pymodbus/framer/framer.py @@ -31,7 +31,7 @@ class FramerType(str, Enum): TLS = "tls" -class Framing(ModbusProtocol): +class Framer(ModbusProtocol): """Framing layer extending transport layer. extends the ModbusProtocol to handle receiving and sending of complete modbus PDU. diff --git a/pymodbus/server/async_io.py b/pymodbus/server/async_io.py index 4453d50dc..449580ff0 100644 --- a/pymodbus/server/async_io.py +++ b/pymodbus/server/async_io.py @@ -11,7 +11,7 @@ from pymodbus.device import ModbusControlBlock, ModbusDeviceIdentification from pymodbus.exceptions import NoSuchSlaveException from pymodbus.factory import ServerDecoder -from pymodbus.framer import FRAMER_NAME_TO_CLASS, Framer, ModbusFramer +from pymodbus.framer import FRAMER_NAME_TO_CLASS, FramerType, ModbusFramer from pymodbus.logging import Log from pymodbus.pdu import ModbusExceptions as merror from pymodbus.transport import CommParams, CommType, ModbusProtocol @@ -316,7 +316,7 @@ class ModbusTcpServer(ModbusBaseServer): def __init__( self, context, - framer=Framer.SOCKET, + framer=FramerType.SOCKET, identity=None, address=("", 502), ignore_missing_slaves=False, @@ -376,7 +376,7 @@ class ModbusTlsServer(ModbusTcpServer): def __init__( # pylint: disable=too-many-arguments self, context, - framer=Framer.TLS, + framer=FramerType.TLS, identity=None, address=("", 502), sslctx=None, @@ -442,7 +442,7 @@ class ModbusUdpServer(ModbusBaseServer): def __init__( self, context, - framer=Framer.SOCKET, + framer=FramerType.SOCKET, identity=None, address=("", 502), ignore_missing_slaves=False, @@ -496,7 +496,7 @@ class ModbusSerialServer(ModbusBaseServer): """ def __init__( - self, context, framer=Framer.RTU, identity=None, **kwargs + self, context, framer=FramerType.RTU, identity=None, **kwargs ): """Initialize the socket server. @@ -611,7 +611,7 @@ async def StartAsyncTcpServer( # pylint: disable=invalid-name,dangerous-default """ kwargs.pop("host", None) server = ModbusTcpServer( - context, kwargs.pop("framer", Framer.SOCKET), identity, address, **kwargs + context, kwargs.pop("framer", FramerType.SOCKET), identity, address, **kwargs ) await _serverList.run(server, custom_functions) @@ -643,7 +643,7 @@ async def StartAsyncTlsServer( # pylint: disable=invalid-name,dangerous-default kwargs.pop("host", None) server = ModbusTlsServer( context, - kwargs.pop("framer", Framer.TLS), + kwargs.pop("framer", FramerType.TLS), identity, address, sslctx, @@ -673,7 +673,7 @@ async def StartAsyncUdpServer( # pylint: disable=invalid-name,dangerous-default """ kwargs.pop("host", None) server = ModbusUdpServer( - context, kwargs.pop("framer", Framer.SOCKET), identity, address, **kwargs + context, kwargs.pop("framer", FramerType.SOCKET), identity, address, **kwargs ) await _serverList.run(server, custom_functions) @@ -693,7 +693,7 @@ async def StartAsyncSerialServer( # pylint: disable=invalid-name,dangerous-defa :param kwargs: The rest """ server = ModbusSerialServer( - context, kwargs.pop("framer", Framer.RTU), identity=identity, **kwargs + context, kwargs.pop("framer", FramerType.RTU), identity=identity, **kwargs ) await _serverList.run(server, custom_functions) diff --git a/test/framers/conftest.py b/test/framers/conftest.py index 5a708c30c..5592ac994 100644 --- a/test/framers/conftest.py +++ b/test/framers/conftest.py @@ -5,11 +5,11 @@ import pytest -from pymodbus.framer import FramerType, Framing +from pymodbus.framer import Framer, FramerType from pymodbus.transport import CommParams, ModbusProtocol -class DummyMessage(Framing): +class DummyMessage(Framer): """Implement use of ModbusProtocol.""" def __init__(self, diff --git a/test/framers/test_old_framers.py b/test/framers/test_old_framers.py index 280634d98..4059ca766 100644 --- a/test/framers/test_old_framers.py +++ b/test/framers/test_old_framers.py @@ -3,7 +3,7 @@ import pytest -from pymodbus import Framer +from pymodbus import FramerType from pymodbus.bit_read_message import ReadCoilsRequest from pymodbus.client.base import ModbusBaseClient from pymodbus.exceptions import ModbusIOException @@ -328,7 +328,7 @@ async def test_send_packet(self, rtu_framer): """Test send packet.""" message = TEST_MESSAGE client = ModbusBaseClient( - Framer.ASCII, + FramerType.ASCII, host="localhost", port=BASE_PORT + 1, CommType=CommType.TCP, diff --git a/test/sub_client/test_client.py b/test/sub_client/test_client.py index 43c858cfb..f62cf29c9 100755 --- a/test/sub_client/test_client.py +++ b/test/sub_client/test_client.py @@ -15,7 +15,7 @@ import pymodbus.register_read_message as pdu_reg_read import pymodbus.register_write_message as pdu_req_write from examples.helper import get_certificate -from pymodbus import Framer +from pymodbus import FramerType from pymodbus.client.base import ModbusBaseClient from pymodbus.client.mixin import ModbusClientMixin from pymodbus.datastore import ModbusSlaveContext @@ -141,7 +141,7 @@ def fake_execute(_self, request): "serial": { "pos_arg": "/dev/tty", "opt_args": { - "framer": Framer.ASCII, + "framer": FramerType.ASCII, "baudrate": 19200 + 500, "bytesize": 8 - 1, "parity": "E", @@ -151,7 +151,7 @@ def fake_execute(_self, request): "defaults": { "host": None, "port": "/dev/tty", - "framer": Framer.RTU, + "framer": FramerType.RTU, "baudrate": 19200, "bytesize": 8, "parity": "N", @@ -163,13 +163,13 @@ def fake_execute(_self, request): "pos_arg": "192.168.1.2", "opt_args": { "port": 112, - "framer": Framer.ASCII, + "framer": FramerType.ASCII, "source_address": ("195.6.7.8", 1025), }, "defaults": { "host": "192.168.1.2", "port": 502, - "framer": Framer.SOCKET, + "framer": FramerType.SOCKET, "source_address": None, }, }, @@ -177,7 +177,7 @@ def fake_execute(_self, request): "pos_arg": "192.168.1.2", "opt_args": { "port": 211, - "framer": Framer.ASCII, + "framer": FramerType.ASCII, "source_address": ("195.6.7.8", 1025), "sslctx": None, "certfile": None, @@ -187,7 +187,7 @@ def fake_execute(_self, request): "defaults": { "host": "192.168.1.2", "port": 802, - "framer": Framer.TLS, + "framer": FramerType.TLS, "source_address": None, "sslctx": None, "certfile": None, @@ -199,13 +199,13 @@ def fake_execute(_self, request): "pos_arg": "192.168.1.2", "opt_args": { "port": 121, - "framer": Framer.ASCII, + "framer": FramerType.ASCII, "source_address": ("195.6.7.8", 1025), }, "defaults": { "host": "192.168.1.2", "port": 502, - "framer": Framer.SOCKET, + "framer": FramerType.SOCKET, "source_address": None, }, }, @@ -277,7 +277,7 @@ async def test_serial_not_installed(): async def test_client_modbusbaseclient(): """Test modbus base client class.""" client = ModbusBaseClient( - Framer.ASCII, + FramerType.ASCII, host="localhost", port=BASE_PORT + 1, CommType=CommType.TCP, @@ -312,7 +312,7 @@ async def test_client_base_async(): p_close.return_value = asyncio.Future() p_close.return_value.set_result(True) async with ModbusBaseClient( - Framer.ASCII, + FramerType.ASCII, host="localhost", port=BASE_PORT + 2, CommType=CommType.TCP, @@ -327,7 +327,7 @@ async def test_client_base_async(): @pytest.mark.skip() async def test_client_protocol_receiver(): """Test the client protocol data received.""" - base = ModbusBaseClient(Framer.SOCKET) + base = ModbusBaseClient(FramerType.SOCKET) transport = mock.MagicMock() base.connection_made(transport) assert base.transport == transport @@ -349,7 +349,7 @@ async def test_client_protocol_receiver(): @pytest.mark.skip() async def test_client_protocol_response(): """Test the udp client protocol builds responses.""" - base = ModbusBaseClient(Framer.SOCKET) + base = ModbusBaseClient(FramerType.SOCKET) response = base.build_response(0x00) # pylint: disable=protected-access excp = response.exception() assert isinstance(excp, ConnectionException) @@ -363,7 +363,7 @@ async def test_client_protocol_response(): async def test_client_protocol_handler(): """Test the client protocol handles responses.""" base = ModbusBaseClient( - Framer.ASCII, host="localhost", port=+3, CommType=CommType.TCP + FramerType.ASCII, host="localhost", port=+3, CommType=CommType.TCP ) transport = mock.MagicMock() base.connection_made(transport=transport) @@ -411,7 +411,7 @@ def close(self): async def test_client_protocol_execute(): """Test the client protocol execute method.""" - base = ModbusBaseClient(Framer.SOCKET, host="127.0.0.1") + base = ModbusBaseClient(FramerType.SOCKET, host="127.0.0.1") request = pdu_bit_read.ReadCoilsRequest(1, 1) transport = MockTransport(base, request) base.connection_made(transport=transport) @@ -422,7 +422,7 @@ async def test_client_protocol_execute(): async def test_client_execute_broadcast(): """Test the client protocol execute method.""" - base = ModbusBaseClient(Framer.SOCKET, host="127.0.0.1") + base = ModbusBaseClient(FramerType.SOCKET, host="127.0.0.1") base.broadcast_enable = True request = pdu_bit_read.ReadCoilsRequest(1, 1) transport = MockTransport(base, request) @@ -432,7 +432,7 @@ async def test_client_execute_broadcast(): async def test_client_protocol_retry(): """Test the client protocol execute method with retries.""" - base = ModbusBaseClient(Framer.SOCKET, host="127.0.0.1", timeout=0.1) + base = ModbusBaseClient(FramerType.SOCKET, host="127.0.0.1", timeout=0.1) request = pdu_bit_read.ReadCoilsRequest(1, 1) transport = MockTransport(base, request, retries=2) base.connection_made(transport=transport) @@ -445,7 +445,7 @@ async def test_client_protocol_retry(): async def test_client_protocol_timeout(): """Test the client protocol execute method with timeout.""" - base = ModbusBaseClient(Framer.SOCKET, host="127.0.0.1", timeout=0.1, retries=2) + base = ModbusBaseClient(FramerType.SOCKET, host="127.0.0.1", timeout=0.1, retries=2) # Avoid creating do_reconnect() task base.connection_lost = mock.MagicMock() request = pdu_bit_read.ReadCoilsRequest(1, 1) @@ -611,7 +611,7 @@ def test_client_mixin_convert_fail(): async def test_client_build_response(): """Test fail of build_response.""" - client = ModbusBaseClient(Framer.RTU) + client = ModbusBaseClient(FramerType.RTU) with pytest.raises(ConnectionException): await client.build_response(0) diff --git a/test/sub_client/test_client_sync.py b/test/sub_client/test_client_sync.py index 1782c8575..4be8bdba2 100755 --- a/test/sub_client/test_client_sync.py +++ b/test/sub_client/test_client_sync.py @@ -7,7 +7,7 @@ import pytest import serial -from pymodbus import Framer +from pymodbus import FramerType from pymodbus.client import ( ModbusSerialClient, ModbusTcpClient, @@ -306,23 +306,23 @@ def test_sync_serial_client_instantiation(self): client = ModbusSerialClient("/dev/null") assert client assert isinstance( - ModbusSerialClient("/dev/null", framer=Framer.ASCII).framer, + ModbusSerialClient("/dev/null", framer=FramerType.ASCII).framer, ModbusAsciiFramer, ) assert isinstance( - ModbusSerialClient("/dev/null", framer=Framer.RTU).framer, + ModbusSerialClient("/dev/null", framer=FramerType.RTU).framer, ModbusRtuFramer, ) assert isinstance( - ModbusSerialClient("/dev/null", framer=Framer.SOCKET).framer, + ModbusSerialClient("/dev/null", framer=FramerType.SOCKET).framer, ModbusSocketFramer, ) def test_sync_serial_rtu_client_timeouts(self): """Test sync serial rtu.""" - client = ModbusSerialClient("/dev/null", framer=Framer.RTU, baudrate=9600) + client = ModbusSerialClient("/dev/null", framer=FramerType.RTU, baudrate=9600) assert client.silent_interval == round((3.5 * 10 / 9600), 6) - client = ModbusSerialClient("/dev/null", framer=Framer.RTU, baudrate=38400) + client = ModbusSerialClient("/dev/null", framer=FramerType.RTU, baudrate=38400) assert client.silent_interval == round((1.75 / 1000), 6) @mock.patch("serial.Serial") @@ -347,7 +347,7 @@ def test_basic_sync_serial_client(self, mock_serial): client.close() # rtu connect/disconnect - rtu_client = ModbusSerialClient("/dev/null", framer=Framer.RTU, strict=True) + rtu_client = ModbusSerialClient("/dev/null", framer=FramerType.RTU, strict=True) assert rtu_client.connect() assert rtu_client.socket.inter_byte_timeout == rtu_client.inter_byte_timeout rtu_client.close() diff --git a/test/sub_server/test_server_asyncio.py b/test/sub_server/test_server_asyncio.py index 9ae9b6af8..34c16616b 100755 --- a/test/sub_server/test_server_asyncio.py +++ b/test/sub_server/test_server_asyncio.py @@ -8,7 +8,7 @@ import pytest -from pymodbus import Framer +from pymodbus import FramerType from pymodbus.datastore import ( ModbusSequentialDataBlock, ModbusServerContext, @@ -154,15 +154,15 @@ async def start_server( args["identity"] = self.identity if do_tls: self.server = ModbusTlsServer( - self.context, Framer.TLS, self.identity, SERV_ADDR + self.context, FramerType.TLS, self.identity, SERV_ADDR ) elif do_udp: self.server = ModbusUdpServer( - self.context, Framer.SOCKET, self.identity, SERV_ADDR + self.context, FramerType.SOCKET, self.identity, SERV_ADDR ) else: self.server = ModbusTcpServer( - self.context, Framer.SOCKET, self.identity, SERV_ADDR + self.context, FramerType.SOCKET, self.identity, SERV_ADDR ) assert self.server if do_forever: From 5405d7f5c0bda5674b85aa1ef137aac0be8a5e6d Mon Sep 17 00:00:00 2001 From: jan iversen Date: Fri, 29 Mar 2024 13:34:52 +0100 Subject: [PATCH 4/5] Update doc. --- doc/source/_static/examples.tgz | Bin 50801 -> 50758 bytes doc/source/_static/examples.zip | Bin 37218 -> 37192 bytes doc/source/library/framer.rst | 26 +++++++++++++++++--------- pymodbus/framer/framer.py | 14 ++++++-------- 4 files changed, 23 insertions(+), 17 deletions(-) diff --git a/doc/source/_static/examples.tgz b/doc/source/_static/examples.tgz index 87b9d5fbc8bf2d9d48f4f18336b4b07133be0f5f..16fc99a5a226104b30f27b4b2f874bb3339a2b4e 100644 GIT binary patch literal 50758 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 literal 50801 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 delta 18242 zcmZ6yQ;;QGu&rCRZQHi3F59-%g@+eB2q?r8C9>KG?fO?dB*ipUZDu=4W2AcST3Xh6%NhfWS<@(B|Bsgdb0J(@R z)QjW>XQHHvQ#7W&z69z(g3-}5*~RkZg3c%I_n*rO#*W6C_QgOfN#Odh-``7x;T$C^ z`R68Nb!u0Z(zAXRAvQ z7jNP$Rx$ZC?j}K9KHd+ZB1JaFd;Z`2w&RFS=F=nXY`moB}`^7T?@Ksn~&^8Dx!be8VTnqNLLF_vox04$i`LE#J9 zL;~pLj2lztj*UBhEcb?6LNiTe3(ZX5IblgY;3Ya&^|{Mw;t*yz$22wziZKS_>RH2M ze(BR8BGhYy$GPkw!Tdm1@S+Jg&y#6nQeHt3a|j=ug`0`wGAqZP-7vxCV}_s7#5txm z>~LvZEZ=f8?K@@WZtrBy0~7>DfPWhg77v!)rBxGSZm1SbX9nwaw6Q)V*_U54vo zJ55H;=-gNLT&00Xx}=PIo!r5S1C`snUhLZG7#L__&uRb8SS@`{0(h~d)vn-`76)tD z-f{otdd?V{fBVyRJ?VyJmNA5_XB8|fBqRBpH=BGsch zOtlF3*+g?;fNd{y%FP#cF&-8hwu8Aih}GDJbNSeH6(pX6yj*21`m}e*f^$7<+ynv? z##c&_lQWOen#H*A19-ZowG8W#SI)aSA6ng3dBJb<88TaRU*c+`kMN5VPC zaPa1UJZ8Auk+c%GrAx2!Y#-`+JwaTj;vkO)hfywL#^YNsEU5n|;(BNJ!)U|p>-yjf zjkTEe%KwYT%rZ z9CrZ;xY0Z$KqVH1|Mgpf7t3ARMGK~b6-jB1f&n+ywi9Yes``?@sX{)Xr3WiN^|~uR zuTdQ;DZ+f7vb&*_vSZ#h?CG#^H~7TQh0vslNb6f|LW$5MbMHHz zE#wCh?9>v#_pM+xnVqxAv9SZ87@gqp#^gzN4O-(sYVN8pd*C0+?1_?|xlO|g%P;rq z_Uh?1lE-id{Yb|zH|vG|J{Q|_B=+4z9Yvi{J1bL-*!B9_b()rMdLWEwJA$oIL4Q?k z{^gbGEE{p*)Tn_F${z*sXO@+AspD3>+P`+11T4X#h1rKYxA-p_Rj$$}bF;|lzRD@^C_xzVW? z%zp{(EA4&c+ciHAofU#DY5(GvEkF=WDOG!cXX&3GsjbrK0er`01Knw0#KQ+gC(BZ? zVxX8=^@jzIy9ksG(+La#>fzIZ1_8tH$pC6j3v=9(OLqRey?js4MzeO`aeW4rbm*^u zy@>J0>x2s{wv(7IpmdUhqvEs}1?W|!-Af0K z?WtNQ*Vt9l^v=vdU+aqtS{j1MgSwqXemHcyavZjzG;_n?b+w=IfwZR07;88dxY!I> zEaPkwsSNm28;R^`<#L1ZaXou>s=OlW)4&qRy+XHE*rk^yk&=`V82#Klmr z?E#uXMO{NVkDasw!>?*?YGdY&DO36g@?wpUKNad?cq?yjRKQⅆkH}m7r~8e$3I< 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 bool: From 6bb73afe8a438aafe21dc127116bb215c73882be Mon Sep 17 00:00:00 2001 From: jan iversen Date: Fri, 29 Mar 2024 16:56:02 +0100 Subject: [PATCH 5/5] Move complete. --- pymodbus/framer/ascii.py | 4 +- pymodbus/framer/base.py | 10 +- pymodbus/framer/framer.py | 24 +-- pymodbus/framer/old_framer_ascii.py | 6 +- pymodbus/framer/old_framer_rtu.py | 8 +- pymodbus/framer/old_framer_socket.py | 6 +- pymodbus/framer/old_framer_tls.py | 6 +- pymodbus/framer/raw.py | 10 +- pymodbus/framer/rtu.py | 21 +-- pymodbus/framer/socket.py | 17 +- pymodbus/framer/tls.py | 15 +- test/framers/test_ascii.py | 10 +- .../{test_message.py => test_framer.py} | 62 +++---- test/framers/test_multidrop.py | 174 ------------------ test/framers/test_rtu.py | 10 +- test/framers/test_socket.py | 10 +- test/framers/test_tls.py | 10 +- 17 files changed, 107 insertions(+), 296 deletions(-) rename test/framers/{test_message.py => test_framer.py} (91%) delete mode 100644 test/framers/test_multidrop.py diff --git a/pymodbus/framer/ascii.py b/pymodbus/framer/ascii.py index c2304eae3..16485c25f 100644 --- a/pymodbus/framer/ascii.py +++ b/pymodbus/framer/ascii.py @@ -8,11 +8,11 @@ from binascii import a2b_hex, b2a_hex -from pymodbus.framer.base import MessageBase +from pymodbus.framer.base import FramerBase from pymodbus.logging import Log -class MessageAscii(MessageBase): +class FramerAscii(FramerBase): r"""Modbus ASCII Frame Controller. [ Start ][Address ][ Function ][ Data ][ LRC ][ End ] diff --git a/pymodbus/framer/base.py b/pymodbus/framer/base.py index 3a41bb9eb..9eaf1075f 100644 --- a/pymodbus/framer/base.py +++ b/pymodbus/framer/base.py @@ -1,6 +1,6 @@ -"""ModbusMessage layer. +"""Framer implementations. -The message layer is responsible for encoding/decoding requests/responses. +The implementation is responsible for encoding/decoding requests/responses. According to the selected type of modbus frame a prefix/suffix is added/removed """ @@ -9,7 +9,7 @@ from abc import abstractmethod -class MessageBase: +class FramerBase: """Intern base.""" EMPTY = b'' @@ -19,7 +19,7 @@ def __init__(self) -> None: @abstractmethod - def decode(self, _data: bytes) -> tuple[int, int, int, bytes]: + def decode(self, data: bytes) -> tuple[int, int, int, bytes]: """Decode message. return: @@ -30,7 +30,7 @@ def decode(self, _data: bytes) -> tuple[int, int, int, bytes]: """ @abstractmethod - def encode(self, data: bytes, device_id: int, tid: int) -> bytes: + def encode(self, pdu: bytes, device_id: int, tid: int) -> bytes: """Decode message. return: diff --git a/pymodbus/framer/framer.py b/pymodbus/framer/framer.py index adfc0cf06..93b6d4324 100644 --- a/pymodbus/framer/framer.py +++ b/pymodbus/framer/framer.py @@ -12,12 +12,12 @@ from abc import abstractmethod from enum import Enum -from pymodbus.framer.ascii import MessageAscii -from pymodbus.framer.base import MessageBase -from pymodbus.framer.raw import MessageRaw -from pymodbus.framer.rtu import MessageRTU -from pymodbus.framer.socket import MessageSocket -from pymodbus.framer.tls import MessageTLS +from pymodbus.framer.ascii import FramerAscii +from pymodbus.framer.base import FramerBase +from pymodbus.framer.raw import FramerRaw +from pymodbus.framer.rtu import FramerRTU +from pymodbus.framer.socket import FramerSocket +from pymodbus.framer.tls import FramerTLS from pymodbus.transport.transport import CommParams, ModbusProtocol @@ -67,12 +67,12 @@ def __init__(self, super().__init__(params, is_server) self.device_ids = device_ids self.broadcast: bool = (0 in device_ids) - self.msg_handle: MessageBase = { - FramerType.RAW: MessageRaw(), - FramerType.ASCII: MessageAscii(), - FramerType.RTU: MessageRTU(), - FramerType.SOCKET: MessageSocket(), - FramerType.TLS: MessageTLS(), + self.msg_handle: FramerBase = { + FramerType.RAW: FramerRaw(), + FramerType.ASCII: FramerAscii(), + FramerType.RTU: FramerRTU(), + FramerType.SOCKET: FramerSocket(), + FramerType.TLS: FramerTLS(), }[framer_type] diff --git a/pymodbus/framer/old_framer_ascii.py b/pymodbus/framer/old_framer_ascii.py index 2d0969f68..534c4fdc2 100644 --- a/pymodbus/framer/old_framer_ascii.py +++ b/pymodbus/framer/old_framer_ascii.py @@ -5,14 +5,14 @@ from pymodbus.framer.old_framer_base import BYTE_ORDER, FRAME_HEADER, ModbusFramer from pymodbus.logging import Log -from .ascii import MessageAscii +from .ascii import FramerAscii ASCII_FRAME_HEADER = BYTE_ORDER + FRAME_HEADER # --------------------------------------------------------------------------- # -# Modbus ASCII Message +# Modbus ASCII olf framer # --------------------------------------------------------------------------- # class ModbusAsciiFramer(ModbusFramer): r"""Modbus ASCII Frame Controller. @@ -40,7 +40,7 @@ def __init__(self, decoder, client=None): self._hsize = 0x02 self._start = b":" self._end = b"\r\n" - self.message_handler = MessageAscii() + self.message_handler = FramerAscii() def decode_data(self, data): """Decode data.""" diff --git a/pymodbus/framer/old_framer_rtu.py b/pymodbus/framer/old_framer_rtu.py index ec9351ea9..0f43e2501 100644 --- a/pymodbus/framer/old_framer_rtu.py +++ b/pymodbus/framer/old_framer_rtu.py @@ -5,7 +5,7 @@ from pymodbus.exceptions import ModbusIOException from pymodbus.framer.old_framer_base import BYTE_ORDER, FRAME_HEADER, ModbusFramer -from pymodbus.framer.rtu import MessageRTU +from pymodbus.framer.rtu import FramerRTU from pymodbus.logging import Log from pymodbus.utilities import ModbusTransactionState @@ -14,7 +14,7 @@ # --------------------------------------------------------------------------- # -# Modbus RTU Message +# Modbus RTU old Framer # --------------------------------------------------------------------------- # class ModbusRtuFramer(ModbusFramer): """Modbus RTU Frame controller. @@ -61,7 +61,7 @@ def __init__(self, decoder, client=None): self._end = b"\x0d\x0a" self._min_frame_size = 4 self.function_codes = decoder.lookup.keys() if decoder else {} - self.message_handler = MessageRTU() + self.message_handler = FramerRTU() def decode_data(self, data): """Decode data.""" @@ -131,7 +131,7 @@ def check_frame(self): data = self._buffer[: frame_size - 2] crc = self._header["crc"] crc_val = (int(crc[0]) << 8) + int(crc[1]) - return MessageRTU.check_CRC(data, crc_val) + return FramerRTU.check_CRC(data, crc_val) except (IndexError, KeyError, struct.error): return False diff --git a/pymodbus/framer/old_framer_socket.py b/pymodbus/framer/old_framer_socket.py index 33ae3a538..e7fde0e16 100644 --- a/pymodbus/framer/old_framer_socket.py +++ b/pymodbus/framer/old_framer_socket.py @@ -6,12 +6,12 @@ ModbusIOException, ) from pymodbus.framer.old_framer_base import SOCKET_FRAME_HEADER, ModbusFramer -from pymodbus.framer.socket import MessageSocket +from pymodbus.framer.socket import FramerSocket from pymodbus.logging import Log # --------------------------------------------------------------------------- # -# Modbus TCP Message +# Modbus TCP old framer # --------------------------------------------------------------------------- # @@ -43,7 +43,7 @@ def __init__(self, decoder, client=None): """ super().__init__(decoder, client) self._hsize = 0x07 - self.message_handler = MessageSocket() + self.message_handler = FramerSocket() def decode_data(self, data): """Decode data.""" diff --git a/pymodbus/framer/old_framer_tls.py b/pymodbus/framer/old_framer_tls.py index 91b699968..ed6f610ee 100644 --- a/pymodbus/framer/old_framer_tls.py +++ b/pymodbus/framer/old_framer_tls.py @@ -6,11 +6,11 @@ ModbusIOException, ) from pymodbus.framer.old_framer_base import TLS_FRAME_HEADER, ModbusFramer -from pymodbus.framer.tls import MessageTLS +from pymodbus.framer.tls import FramerTLS # --------------------------------------------------------------------------- # -# Modbus TLS Message +# Modbus TLS old framer # --------------------------------------------------------------------------- # @@ -34,7 +34,7 @@ def __init__(self, decoder, client=None): """ super().__init__(decoder, client) self._hsize = 0x0 - self.message_handler = MessageTLS() + self.message_handler = FramerTLS() def decode_data(self, data): """Decode data.""" diff --git a/pymodbus/framer/raw.py b/pymodbus/framer/raw.py index ab932826e..1d32ec20a 100644 --- a/pymodbus/framer/raw.py +++ b/pymodbus/framer/raw.py @@ -1,11 +1,11 @@ -"""ModbusMessage layer.""" +"""Modbus Raw (passthrough) implementation.""" from __future__ import annotations -from pymodbus.framer.base import MessageBase +from pymodbus.framer.base import FramerBase from pymodbus.logging import Log -class MessageRaw(MessageBase): +class FramerRaw(FramerBase): r"""Modbus RAW Frame Controller. [ Device id ][Transaction id ][ Data ] @@ -25,6 +25,6 @@ def decode(self, data: bytes) -> tuple[int, int, int, bytes]: tid = int(data[1]) return len(data), dev_id, tid, data[2:] - def encode(self, data: bytes, device_id: int, tid: int) -> bytes: + def encode(self, pdu: bytes, device_id: int, tid: int) -> bytes: """Decode message.""" - return device_id.to_bytes(1, 'big') + tid.to_bytes(1, 'big') + data + return device_id.to_bytes(1, 'big') + tid.to_bytes(1, 'big') + pdu diff --git a/pymodbus/framer/rtu.py b/pymodbus/framer/rtu.py index fcd0ec804..7cad7f8e6 100644 --- a/pymodbus/framer/rtu.py +++ b/pymodbus/framer/rtu.py @@ -1,20 +1,15 @@ -"""ModbusMessage layer. - -is extending ModbusProtocol to handle receiving and sending of messsagees. - -ModbusMessage provides a unified interface to send/receive Modbus requests/responses. -""" +"""Modbus RTU frame implementation.""" from __future__ import annotations import struct from pymodbus.exceptions import ModbusIOException from pymodbus.factory import ClientDecoder -from pymodbus.framer.base import MessageBase +from pymodbus.framer.base import FramerBase from pymodbus.logging import Log -class MessageRTU(MessageBase): +class FramerRTU(FramerBase): """Modbus RTU frame type. [ Start Wait ] [Address ][ Function Code] [ Data ][ CRC ][ End Wait ] @@ -130,7 +125,7 @@ def check_frame(self): data = self._buffer[: frame_size - 2] crc = self._header["crc"] crc_val = (int(crc[0]) << 8) + int(crc[1]) - return MessageRTU.check_CRC(data, crc_val) + return FramerRTU.check_CRC(data, crc_val) except (IndexError, KeyError, struct.error): return False @@ -188,10 +183,10 @@ def callback(result): self._legacy_decode(callback, [0]) return 0, 0, 0, b'' - def encode(self, data: bytes, device_id: int, _tid: int) -> bytes: + def encode(self, pdu: bytes, device_id: int, _tid: int) -> bytes: """Decode message.""" - packet = device_id.to_bytes(1,'big') + data - return packet + MessageRTU.compute_CRC(packet).to_bytes(2,'big') + packet = device_id.to_bytes(1,'big') + pdu + return packet + FramerRTU.compute_CRC(packet).to_bytes(2,'big') @classmethod def check_CRC(cls, data: bytes, check: int) -> bool: @@ -220,4 +215,4 @@ def compute_CRC(cls, data: bytes) -> int: swapped = ((crc << 8) & 0xFF00) | ((crc >> 8) & 0x00FF) return swapped -MessageRTU.crc16_table = MessageRTU.generate_crc16_table() +FramerRTU.crc16_table = FramerRTU.generate_crc16_table() diff --git a/pymodbus/framer/socket.py b/pymodbus/framer/socket.py index f3bca1a8f..1c99837bd 100644 --- a/pymodbus/framer/socket.py +++ b/pymodbus/framer/socket.py @@ -1,16 +1,11 @@ -"""ModbusMessage layer. - -is extending ModbusProtocol to handle receiving and sending of messsagees. - -ModbusMessage provides a unified interface to send/receive Modbus requests/responses. -""" +"""Modbus Socket frame implementation.""" from __future__ import annotations -from pymodbus.framer.base import MessageBase +from pymodbus.framer.base import FramerBase from pymodbus.logging import Log -class MessageSocket(MessageBase): +class FramerSocket(FramerBase): """Modbus Socket frame type. [ MBAP Header ] [ Function Code] [ Data ] @@ -33,13 +28,13 @@ def decode(self, data: bytes) -> tuple[int, int, int, bytes]: return 0, 0, 0, self.EMPTY return msg_len, msg_tid, msg_dev, data[7:msg_len] - def encode(self, data: bytes, device_id: int, tid: int) -> bytes: + def encode(self, pdu: bytes, device_id: int, tid: int) -> bytes: """Decode message.""" packet = ( tid.to_bytes(2, 'big') + b'\x00\x00' + - (len(data) + 1).to_bytes(2, 'big') + + (len(pdu) + 1).to_bytes(2, 'big') + device_id.to_bytes(1, 'big') + - data + pdu ) return packet diff --git a/pymodbus/framer/tls.py b/pymodbus/framer/tls.py index e9845b1a5..8256038c3 100644 --- a/pymodbus/framer/tls.py +++ b/pymodbus/framer/tls.py @@ -1,15 +1,10 @@ -"""ModbusMessage layer. - -is extending ModbusProtocol to handle receiving and sending of messsagees. - -ModbusMessage provides a unified interface to send/receive Modbus requests/responses. -""" +"""Modbus TLS frame implementation.""" from __future__ import annotations -from pymodbus.framer.base import MessageBase +from pymodbus.framer.base import FramerBase -class MessageTLS(MessageBase): +class FramerTLS(FramerBase): """Modbus TLS frame type. [ Function Code] [ Data ] @@ -20,6 +15,6 @@ def decode(self, data: bytes) -> tuple[int, int, int, bytes]: """Decode message.""" return len(data), 0, 0, data - def encode(self, data: bytes, _device_id: int, _tid: int) -> bytes: + def encode(self, pdu: bytes, _device_id: int, _tid: int) -> bytes: """Decode message.""" - return data + return pdu diff --git a/test/framers/test_ascii.py b/test/framers/test_ascii.py index 86f63e2d6..d78d2e87b 100644 --- a/test/framers/test_ascii.py +++ b/test/framers/test_ascii.py @@ -1,17 +1,17 @@ -"""Test transport.""" +"""Test framer.""" import pytest -from pymodbus.framer.ascii import MessageAscii +from pymodbus.framer.ascii import FramerAscii -class TestMessageAscii: - """Test message module.""" +class TestFramerAscii: + """Test module.""" @staticmethod @pytest.fixture(name="frame") def prepare_frame(): """Return message object.""" - return MessageAscii() + return FramerAscii() @pytest.mark.parametrize( diff --git a/test/framers/test_message.py b/test/framers/test_framer.py similarity index 91% rename from test/framers/test_message.py rename to test/framers/test_framer.py index b8493d988..0114053ff 100644 --- a/test/framers/test_message.py +++ b/test/framers/test_framer.py @@ -1,19 +1,19 @@ -"""Test transport.""" +"""Test framer.""" from unittest import mock import pytest from pymodbus.framer import FramerType -from pymodbus.framer.ascii import MessageAscii -from pymodbus.framer.rtu import MessageRTU -from pymodbus.framer.socket import MessageSocket -from pymodbus.framer.tls import MessageTLS +from pymodbus.framer.ascii import FramerAscii +from pymodbus.framer.rtu import FramerRTU +from pymodbus.framer.socket import FramerSocket +from pymodbus.framer.tls import FramerTLS from pymodbus.transport import CommParams -class TestMessage: - """Test message module.""" +class TestFramer: + """Test module.""" @staticmethod @pytest.fixture(name="msg") @@ -102,12 +102,12 @@ async def test_encode(self, msg, data, dev_id, tid, res_data): @pytest.mark.parametrize( ("func", "lrc", "expect"), - [(MessageAscii.check_LRC, 0x1c, True), - (MessageAscii.check_LRC, 0x0c, False), - (MessageAscii.compute_LRC, None, 0x1c), - (MessageRTU.check_CRC, 0xE2DB, True), - (MessageRTU.check_CRC, 0xDBE2, False), - (MessageRTU.compute_CRC, None, 0xE2DB), + [(FramerAscii.check_LRC, 0x1c, True), + (FramerAscii.check_LRC, 0x0c, False), + (FramerAscii.compute_LRC, None, 0x1c), + (FramerRTU.check_CRC, 0xE2DB, True), + (FramerRTU.check_CRC, 0xDBE2, False), + (FramerRTU.compute_CRC, None, 0xE2DB), ] ) def test_LRC_CRC(self, func, lrc, expect): @@ -118,30 +118,30 @@ def test_LRC_CRC(self, func, lrc, expect): def test_roundtrip_LRC(self): """Test combined compute/check LRC.""" data = b'\x12\x34\x23\x45\x34\x56\x45\x67' - assert MessageAscii.compute_LRC(data) == 0x1c - assert MessageAscii.check_LRC(data, 0x1C) + assert FramerAscii.compute_LRC(data) == 0x1c + assert FramerAscii.check_LRC(data, 0x1C) def test_crc16_table(self): """Test the crc16 table is prefilled.""" - assert len(MessageRTU.crc16_table) == 256 - assert isinstance(MessageRTU.crc16_table[0], int) - assert isinstance(MessageRTU.crc16_table[255], int) + assert len(FramerRTU.crc16_table) == 256 + assert isinstance(FramerRTU.crc16_table[0], int) + assert isinstance(FramerRTU.crc16_table[255], int) def test_roundtrip_CRC(self): """Test combined compute/check CRC.""" data = b'\x12\x34\x23\x45\x34\x56\x45\x67' - assert MessageRTU.compute_CRC(data) == 0xE2DB - assert MessageRTU.check_CRC(data, 0xE2DB) + assert FramerRTU.compute_CRC(data) == 0xE2DB + assert FramerRTU.check_CRC(data, 0xE2DB) -class TestMessages: - """Test message classes.""" +class TestFramer2: + """Test classes.""" @pytest.mark.parametrize( ("frame", "frame_expected"), [ - (MessageAscii, [ + (FramerAscii, [ b':0003007C00027F\r\n', b':000304008D008EDE\r\n', b':0083027B\r\n', @@ -152,7 +152,7 @@ class TestMessages: b':FF0304008D008EDF\r\n', b':FF83027C\r\n', ]), - (MessageRTU, [ + (FramerRTU, [ b'\x00\x03\x00\x7c\x00\x02\x04\x02', b'\x00\x03\x04\x00\x8d\x00\x8e\xfa\xbc', b'\x00\x83\x02\x91\x31', @@ -163,7 +163,7 @@ class TestMessages: b'\xff\x03\x04\x00\x8d\x00\x8e\xf5\xb3', b'\xff\x83\x02\xa1\x01', ]), - (MessageSocket, [ + (FramerSocket, [ b'\x00\x00\x00\x00\x00\x06\x00\x03\x00\x7c\x00\x02', b'\x00\x00\x00\x00\x00\x07\x00\x03\x04\x00\x8d\x00\x8e', b'\x00\x00\x00\x00\x00\x03\x00\x83\x02', @@ -183,7 +183,7 @@ class TestMessages: b'\x0c\x05\x00\x00\x00\x07\xff\x03\x04\x00\x8d\x00\x8e', b'\x0c\x05\x00\x00\x00\x03\xff\x83\x02', ]), - (MessageTLS, [ + (FramerTLS, [ b'\x03\x00\x7c\x00\x02', b'\x03\x04\x00\x8d\x00\x8e', b'\x83\x02', @@ -215,8 +215,8 @@ class TestMessages: ) def test_encode(self, frame, frame_expected, data, dev_id, tid, inx1, inx2, inx3): """Test encode method.""" - if ((frame != MessageSocket and tid) or - (frame == MessageTLS and dev_id)): + if ((frame != FramerSocket and tid) or + (frame == FramerTLS and dev_id)): return frame_obj = frame() expected = frame_expected[inx1 + inx2 + inx3] @@ -308,11 +308,11 @@ async def test_decode(self, dummy_message, msg_type, data, dev_id, tid, expected @pytest.mark.parametrize( ("frame", "data", "exp_len"), [ - (MessageAscii, b':0003007C00017F\r\n', 17), # bad crc + (FramerAscii, b':0003007C00017F\r\n', 17), # bad crc # (MessageAscii, b'abc:0003007C00027F\r\n', 3), # garble in front # (MessageAscii, b':0003007C00017F\r\nabc', 17), # bad crc, garble after # (MessageAscii, b':0003007C00017F\r\n:0003', 17), # part second message - (MessageRTU, b'\x00\x83\x02\x91\x31', 0), # bad crc + (FramerRTU, b'\x00\x83\x02\x91\x31', 0), # bad crc # (MessageRTU, b'\x00\x83\x02\x91\x31', 0), # garble in front # (MessageRTU, b'\x00\x83\x02\x91\x31', 0), # garble after # (MessageRTU, b'\x00\x83\x02\x91\x31', 0), # part second message @@ -320,7 +320,7 @@ async def test_decode(self, dummy_message, msg_type, data, dev_id, tid, expected ) async def test_decode_bad_crc(self, frame, data, exp_len): """Test encode method.""" - if frame == MessageRTU: + if frame == FramerRTU: pytest.skip("Waiting for implementation.") frame_obj = frame() used_len, _, _, data = frame_obj.decode(data) diff --git a/test/framers/test_multidrop.py b/test/framers/test_multidrop.py deleted file mode 100644 index e2a21bd14..000000000 --- a/test/framers/test_multidrop.py +++ /dev/null @@ -1,174 +0,0 @@ -"""Test server working as slave on a multidrop RS485 line.""" -from unittest import mock - -import pytest - -from pymodbus.framer import ModbusRtuFramer -from pymodbus.server.async_io import ServerDecoder - - -class TestMultidrop: - """Test that server works on a multidrop line.""" - - slaves = [2] - - good_frame = b"\x02\x03\x00\x01\x00}\xd4\x18" - - @pytest.fixture(name="framer") - def fixture_framer(self): - """Prepare framer.""" - return ModbusRtuFramer(ServerDecoder()) - - @pytest.fixture(name="callback") - def fixture_callback(self): - """Prepare dummy callback.""" - return mock.Mock() - - def test_ok_frame(self, framer, callback): - """Test ok frame.""" - serial_event = self.good_frame - framer.processIncomingPacket(serial_event, callback, self.slaves) - callback.assert_called_once() - - def test_ok_2frame(self, framer, callback): - """Test ok frame.""" - serial_event = self.good_frame + self.good_frame - framer.processIncomingPacket(serial_event, callback, self.slaves) - assert callback.call_count == 2 - - def test_bad_crc(self, framer, callback): - """Test bad crc.""" - serial_event = b"\x02\x03\x00\x01\x00}\xd4\x19" # Manually mangled crc - framer.processIncomingPacket(serial_event, callback, self.slaves) - callback.assert_not_called() - - def test_wrong_id(self, framer, callback): - """Test frame wrong id.""" - serial_event = b"\x01\x03\x00\x01\x00}\xd4+" # Frame with good CRC but other id - framer.processIncomingPacket(serial_event, callback, self.slaves) - callback.assert_not_called() - - def test_big_split_response_frame_from_other_id(self, framer, callback): - """Test split response.""" - # This is a single *response* from device id 1 after being queried for 125 holding register values - # Because the response is so long it spans several serial events - serial_events = [ - b"\x01\x03\xfa\xc4y\xc0\x00\xc4y\xc0\x00\xc4y\xc0\x00\xc4y\xc0\x00\xc4y\xc0\x00Dz\x00\x00C\x96\x00\x00", - b"?\x05\x1e\xb8DH\x00\x00D\x96\x00\x00D\xfa\x00\x00DH\x00\x00D\x96\x00\x00D\xfa\x00\x00DH\x00", - b"\x00D\x96\x00\x00D\xfa\x00\x00B\x96\x00\x00B\xb4\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00", - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00", - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00", - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00", - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00", - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00", - b"\x00\x00\x00\x00\x00\x00\x00N,", - ] - for serial_event in serial_events: - framer.processIncomingPacket(serial_event, callback, self.slaves) - callback.assert_not_called() - - def test_split_frame(self, framer, callback): - """Test split frame.""" - serial_events = [self.good_frame[:5], self.good_frame[5:]] - for serial_event in serial_events: - framer.processIncomingPacket(serial_event, callback, self.slaves) - callback.assert_called_once() - - def test_complete_frame_trailing_data_without_id(self, framer, callback): - """Test trailing data.""" - garbage = b"\x05\x04\x03" # without id - serial_event = garbage + self.good_frame - framer.processIncomingPacket(serial_event, callback, self.slaves) - callback.assert_called_once() - - def test_complete_frame_trailing_data_with_id(self, framer, callback): - """Test trailing data.""" - garbage = b"\x05\x04\x03\x02\x01\x00" # with id - serial_event = garbage + self.good_frame - framer.processIncomingPacket(serial_event, callback, self.slaves) - callback.assert_called_once() - - def test_split_frame_trailing_data_with_id(self, framer, callback): - """Test split frame.""" - garbage = b"\x05\x04\x03\x02\x01\x00" - serial_events = [garbage + self.good_frame[:5], self.good_frame[5:]] - for serial_event in serial_events: - framer.processIncomingPacket(serial_event, callback, self.slaves) - callback.assert_called_once() - - def test_coincidental_1(self, framer, callback): - """Test conincidental.""" - garbage = b"\x02\x90\x07" - serial_events = [garbage, self.good_frame[:5], self.good_frame[5:]] - for serial_event in serial_events: - framer.processIncomingPacket(serial_event, callback, self.slaves) - callback.assert_called_once() - - def test_coincidental_2(self, framer, callback): - """Test conincidental.""" - garbage = b"\x02\x10\x07" - serial_events = [garbage, self.good_frame[:5], self.good_frame[5:]] - for serial_event in serial_events: - framer.processIncomingPacket(serial_event, callback, self.slaves) - callback.assert_called_once() - - def test_coincidental_3(self, framer, callback): - """Test conincidental.""" - garbage = b"\x02\x10\x07\x10" - serial_events = [garbage, self.good_frame[:5], self.good_frame[5:]] - for serial_event in serial_events: - framer.processIncomingPacket(serial_event, callback, self.slaves) - callback.assert_called_once() - - def test_wrapped_frame(self, framer, callback): - """Test wrapped frame.""" - garbage = b"\x05\x04\x03\x02\x01\x00" - serial_event = garbage + self.good_frame + garbage - framer.processIncomingPacket(serial_event, callback, self.slaves) - - # We probably should not respond in this case; in this case we've likely become desynchronized - # i.e. this probably represents a case where a command came for us, but we didn't get - # to the serial buffer in time (some other co-routine or perhaps a block on the USB bus) - # and the master moved on and queried another device - callback.assert_called_once() - - def test_frame_with_trailing_data(self, framer, callback): - """Test trailing data.""" - garbage = b"\x05\x04\x03\x02\x01\x00" - serial_event = self.good_frame + garbage - framer.processIncomingPacket(serial_event, callback, self.slaves) - - # We should not respond in this case for identical reasons as test_wrapped_frame - callback.assert_called_once() - - def test_getFrameStart(self, framer): - """Test getFrameStart.""" - result = None - count = 0 - def test_callback(data): - """Check callback.""" - nonlocal result, count - count += 1 - result = data.function_code.to_bytes(1,'big')+data.encode() - - framer_ok = b"\x02\x03\x00\x01\x00\x7d\xd4\x18" - framer.processIncomingPacket(framer_ok, test_callback, self.slaves) - assert framer_ok[1:-2] == result - - count = 0 - framer_2ok = framer_ok + framer_ok - framer.processIncomingPacket(framer_2ok, test_callback, self.slaves) - assert count == 2 - assert not framer._buffer # pylint: disable=protected-access - - framer._buffer = framer_ok[:2] # pylint: disable=protected-access - framer.processIncomingPacket(b'', test_callback, self.slaves) - assert framer_ok[:2] == framer._buffer # pylint: disable=protected-access - - framer._buffer = framer_ok[:3] # pylint: disable=protected-access - framer.processIncomingPacket(b'', test_callback, self.slaves) - assert framer_ok[:3] == framer._buffer # pylint: disable=protected-access - - framer_ok = b"\xF0\x03\x00\x01\x00}\xd4\x18" - framer.processIncomingPacket(framer_ok, test_callback, self.slaves) - assert framer._buffer == framer_ok[-3:] # pylint: disable=protected-access diff --git a/test/framers/test_rtu.py b/test/framers/test_rtu.py index 3af18c695..dd30f8218 100644 --- a/test/framers/test_rtu.py +++ b/test/framers/test_rtu.py @@ -1,17 +1,17 @@ -"""Test transport.""" +"""Test framer.""" import pytest -from pymodbus.framer.rtu import MessageRTU +from pymodbus.framer.rtu import FramerRTU -class TestMessageRTU: - """Test message module.""" +class TestFramerRTU: + """Test module.""" @staticmethod @pytest.fixture(name="frame") def prepare_frame(): """Return message object.""" - return MessageRTU() + return FramerRTU() @pytest.mark.parametrize( diff --git a/test/framers/test_socket.py b/test/framers/test_socket.py index 77d972ea1..d716d4c7d 100644 --- a/test/framers/test_socket.py +++ b/test/framers/test_socket.py @@ -1,18 +1,18 @@ -"""Test transport.""" +"""Test framer.""" import pytest -from pymodbus.framer.socket import MessageSocket +from pymodbus.framer.socket import FramerSocket -class TestMessageSocket: - """Test message module.""" +class TestFramerSocket: + """Test module.""" @staticmethod @pytest.fixture(name="frame") def prepare_frame(): """Return message object.""" - return MessageSocket() + return FramerSocket() @pytest.mark.parametrize( diff --git a/test/framers/test_tls.py b/test/framers/test_tls.py index 15eb4a450..50ccce800 100644 --- a/test/framers/test_tls.py +++ b/test/framers/test_tls.py @@ -1,18 +1,18 @@ -"""Test transport.""" +"""Test framer.""" import pytest -from pymodbus.framer.tls import MessageTLS +from pymodbus.framer.tls import FramerTLS -class TestMessageSocket: - """Test message module.""" +class TestMFramerTLS: + """Test module.""" @staticmethod @pytest.fixture(name="frame") def prepare_frame(): """Return message object.""" - return MessageTLS() + return FramerTLS() @pytest.mark.parametrize(