From d1b79fd27d30653c22f6ac1947402a07533b8969 Mon Sep 17 00:00:00 2001 From: jan iversen Date: Fri, 16 Oct 2026 17:57:59 +0200 Subject: [PATCH] PDU classes refactored. --- examples/client_async_calls.py | 12 +- examples/client_calls.py | 12 +- examples/custom_msg.py | 109 --- examples/server_hook.py | 4 +- pymodbus/__init__.py | 5 +- pymodbus/client/base.py | 22 +- pymodbus/client/mixin.py | 104 ++- pymodbus/client/serial.py | 6 +- pymodbus/client/tcp.py | 6 +- pymodbus/client/tls.py | 6 +- pymodbus/client/udp.py | 6 +- pymodbus/constants.py | 18 +- pymodbus/framer/ascii.py | 2 +- pymodbus/framer/base.py | 6 +- pymodbus/pdu/__init__.py | 40 +- pymodbus/pdu/bit_message.py | 361 +++------ pymodbus/pdu/decoders.py | 105 --- pymodbus/pdu/device.py | 4 +- pymodbus/pdu/diag_message.py | 546 ++++---------- pymodbus/pdu/events.py | 20 +- pymodbus/pdu/exceptionresponse.py | 56 -- pymodbus/pdu/file_message.py | 384 ++++------ pymodbus/pdu/mei_message.py | 206 +++-- pymodbus/pdu/other_message.py | 354 ++++----- pymodbus/pdu/pdu.py | 263 +++++-- pymodbus/pdu/register_message.py | 677 +++++++---------- pymodbus/server/base.py | 16 +- pymodbus/server/requesthandler.py | 27 +- pymodbus/server/server.py | 27 +- pymodbus/server/simulator/http_server.py | 7 +- pymodbus/transaction/transaction.py | 24 +- test/client/test_client.py | 84 +-- test/conftest.py | 26 - test/examples/test_examples.py | 10 +- test/framer/generator.py | 17 +- test/framer/test_extras.py | 6 +- test/framer/test_framer.py | 19 +- test/pdu/conftest.py | 550 ++++++++++++++ test/pdu/test_bit.py | 257 +++---- test/pdu/test_decoders.py | 192 ----- test/pdu/test_device.py | 6 +- test/pdu/test_diag_messages.py | 224 ------ test/pdu/test_events.py | 20 +- test/pdu/test_file_message.py | 185 ++--- test/pdu/test_mei_messages.py | 149 ++-- test/pdu/test_other_messages.py | 166 ++--- test/pdu/test_pdu.py | 912 ++++++++--------------- test/pdu/test_register_read_messages.py | 291 +++----- test/pdu/test_register_write_messages.py | 163 ++-- test/server/test_base.py | 12 +- test/server/test_requesthandler.py | 16 +- test/server/test_server_asyncio.py | 2 +- test/transaction/test_transaction.py | 147 ++-- 53 files changed, 2897 insertions(+), 3992 deletions(-) delete mode 100644 pymodbus/pdu/decoders.py delete mode 100644 pymodbus/pdu/exceptionresponse.py create mode 100644 test/pdu/conftest.py delete mode 100644 test/pdu/test_decoders.py delete mode 100644 test/pdu/test_diag_messages.py diff --git a/examples/client_async_calls.py b/examples/client_async_calls.py index 2b6c4a6d9d..9a2f8b4a4b 100755 --- a/examples/client_async_calls.py +++ b/examples/client_async_calls.py @@ -11,16 +11,8 @@ All available modbus calls are present. If you are performing a request that is not available in the client -mixin, you have to perform the request like this instead:: - - from pymodbus.pdu.diag_message import ClearCountersRequest - from pymodbus.pdu.diag_message import ClearCountersResponse - - request = ClearCountersRequest() - response = client.execute(request) - if isinstance(response, ClearCountersResponse): - ... do something with the response - +mixin, you can use client.execute directly, but the easiest way it to add +a custom PDU class. This example uses client_async.py and client_sync.py to handle connection, and have the same options. diff --git a/examples/client_calls.py b/examples/client_calls.py index d0ff92ff79..a7f23b9663 100755 --- a/examples/client_calls.py +++ b/examples/client_calls.py @@ -11,16 +11,8 @@ All available modbus calls are present. If you are performing a request that is not available in the client -mixin, you have to perform the request like this instead:: - - from pymodbus.pdu.diag_message import ClearCountersRequest - from pymodbus.pdu.diag_message import ClearCountersResponse - - request = ClearCountersRequest() - response = client.execute(request) - if isinstance(response, ClearCountersResponse): - ... do something with the response - +mixin, you can use client.execute directly, but the easiest way it to add +a custom PDU class. This example uses client_async.py and client_sync.py to handle connection, and have the same options. diff --git a/examples/custom_msg.py b/examples/custom_msg.py index 59cdd327db..d6ac4e9f2b 100755 --- a/examples/custom_msg.py +++ b/examples/custom_msg.py @@ -1,5 +1,4 @@ #!/usr/bin/env python3 -# pylint: disable=missing-type-doc """Pymodbus Synchronous Client Examples. The following is an example of how to use the synchronous modbus client @@ -12,14 +11,9 @@ """ import asyncio -import struct from pymodbus import FramerType from pymodbus.client import AsyncModbusTcpClient as ModbusClient -from pymodbus.datastore import ModbusServerContext -from pymodbus.exceptions import ModbusIOException -from pymodbus.pdu import ModbusPDU -from pymodbus.pdu.bit_message import ReadCoilsRequest from pymodbus.server import ServerAsyncStop, StartAsyncTcpServer from pymodbus.simulator import DataType, SimData, SimDevice @@ -35,92 +29,6 @@ # --------------------------------------------------------------------------- # -class CustomModbusResponse(ModbusPDU): - """Custom modbus response.""" - - function_code = 55 - - @classmethod - def x_calculateRtuFrameSize(cls, data: bytes) -> int: - """Calculate the size of a PDU.""" - return int(data[2]) + 2 + 3 - - def __init__(self, values: list[int] | None = None, device_id=1, transaction=0): - """Initialize.""" - super().__init__(dev_id=device_id, transaction_id=transaction) - self.values: list[int] = values or [] - - def x_encode(self): - """Encode response pdu. - - :returns: The encoded packet message - """ - res = struct.pack(">B", len(self.values) * 2) - for register in self.values: # pragma: no cover - res += struct.pack(">H", register) - return res - - def x_decode(self, data): # pragma: no cover - """Decode response pdu. - - :param data: The packet data to decode - """ - byte_count = int(data[0]) - self.values = [] - for i in range(1, byte_count + 1, 2): - self.values.append(struct.unpack(">H", data[i : i + 2])[0]) - - -class CustomRequest(ModbusPDU): - """Custom modbus request.""" - - function_code = 55 - - @classmethod - def x_calculateRtuFrameSize(cls, data: bytes) -> int: - """Calculate the size of a PDU.""" - return 8 - - def __init__(self, address=None, device_id=1, transaction=0): - """Initialize.""" - super().__init__(dev_id=device_id, transaction_id=transaction) - self.address = address - self.count = 2 - - def x_encode(self): - """Encode.""" - return struct.pack(">HH", self.address, self.count) - - def x_decode(self, data): - """Decode.""" - self.address, self.count = struct.unpack(">HH", data) - - async def datastore_update( - self, context: ModbusServerContext, device_id: int - ) -> ModbusPDU: - """Update diagnostic request on the given device.""" - _ = context, device_id - return CustomModbusResponse() - - -# --------------------------------------------------------------------------- # -# This could also have been defined as -# --------------------------------------------------------------------------- # - - -class Read16CoilsRequest(ReadCoilsRequest): - """Read 16 coils in one request.""" - - def __init__(self, address, device_id=1, transaction=0): - """Initialize a new instance. - - :param address: The address to start reading from - """ - super().__init__( - address=address, count=16, dev_id=device_id, transaction_id=transaction - ) - - # --------------------------------------------------------------------------- # # execute the request with your client # --------------------------------------------------------------------------- # @@ -137,7 +45,6 @@ async def main(host="localhost", port=5020): 0, SimData(0, datatype=DataType.REGISTERS, values=[17] * 100) ), address=(host, port), - custom_pdu=[CustomRequest], ) ) await asyncio.sleep(0.1) @@ -145,22 +52,6 @@ async def main(host="localhost", port=5020): await client.connect() # add new modbus function code. - assert CustomModbusResponse.x_calculateRtuFrameSize(b"\x00\x00\x07\x00") == 12 - client.register(CustomModbusResponse) - device_id = 1 - assert CustomRequest.x_calculateRtuFrameSize(b"") == 8 - request1 = CustomRequest(32, device_id=device_id) - try: - result = await client.execute(False, request1) - except ModbusIOException: # pragma: no cover - print("Server do not support CustomRequest.") - else: - print(result) # pragma: no cover - - # inherited request - request2 = Read16CoilsRequest(32, device_id) - result = await client.execute(False, request2) - print(result) await ServerAsyncStop() task.cancel() await task diff --git a/examples/server_hook.py b/examples/server_hook.py index 6449e90f38..d198e03a92 100755 --- a/examples/server_hook.py +++ b/examples/server_hook.py @@ -12,7 +12,7 @@ import sys from pymodbus import FramerType, pymodbus_apply_logging_config -from pymodbus.pdu import ModbusPDU +from pymodbus.pdu import ModbusPdu from pymodbus.server import ModbusTcpServer from pymodbus.simulator import DataType, SimData, SimDevice @@ -41,7 +41,7 @@ def trace_packet(self, sending: bool, data: bytes) -> bytes: return data - def trace_pdu(self, sending: bool, pdu: ModbusPDU) -> ModbusPDU: + def trace_pdu(self, sending: bool, pdu: ModbusPdu) -> ModbusPdu: """Do dummy trace.""" txt = "REQUEST pdu" if sending else "RESPONSE pdu" print(f"---> {txt}: {pdu}") diff --git a/pymodbus/__init__.py b/pymodbus/__init__.py index 812f44ab2b..86b1da1a6c 100644 --- a/pymodbus/__init__.py +++ b/pymodbus/__init__.py @@ -4,11 +4,11 @@ """ __all__ = [ - "ExceptionResponse", "FramerType", "Log", "ModbusDeviceIdentification", "ModbusException", + "ModbusPdu", "__version__", "__version_full__", "pymodbus_apply_logging_config", @@ -17,8 +17,7 @@ from .exceptions import ModbusException from .framer import FramerType from .logging import Log, pymodbus_apply_logging_config -from .pdu import ExceptionResponse -from .pdu.device import ModbusDeviceIdentification +from .pdu import ModbusDeviceIdentification, ModbusPdu __version__ = "4.0.0dev16" diff --git a/pymodbus/client/base.py b/pymodbus/client/base.py index ea6c8e9561..65b6f2d370 100644 --- a/pymodbus/client/base.py +++ b/pymodbus/client/base.py @@ -8,13 +8,13 @@ from ..exceptions import ConnectionException from ..framer import FRAMER_NAME_TO_CLASS, FramerBase, FramerType from ..logging import Log -from ..pdu import DecodePDU, ModbusPDU +from ..pdu import DecodePDU, ModbusPdu from ..transaction import TransactionManager from ..transport import CommParams from .mixin import ModbusClientMixin -class ModbusBaseClient(ModbusClientMixin[Awaitable[ModbusPDU]]): +class ModbusBaseClient(ModbusClientMixin[Awaitable[ModbusPdu]]): """**ModbusBaseClient**. :mod:`ModbusBaseClient` is normally not referenced outside :mod:`pymodbus`. @@ -26,7 +26,7 @@ def __init__( retries: int, comm_params: CommParams, trace_packet: Callable[[bool, bytes], bytes] | None, - trace_pdu: Callable[[bool, ModbusPDU], ModbusPDU] | None, + trace_pdu: Callable[[bool, ModbusPdu], ModbusPdu] | None, trace_connect: Callable[[bool], None] | None, ) -> None: """Initialize a client instance. @@ -61,7 +61,7 @@ async def connect(self) -> bool: rc = await self.ctx.connect() return rc - def register(self, custom_response_class: type[ModbusPDU]) -> None: + def register(self, custom_response_class: type[ModbusPdu]) -> None: """Register a custom response class with the decoder (call **sync**). :param custom_response_class: (optional) Modbus response class. @@ -70,13 +70,13 @@ def register(self, custom_response_class: type[ModbusPDU]) -> None: Use register() to add non-standard responses (like e.g. a login prompt) and have them interpreted automatically. """ - self.ctx.framer.decoder.register(custom_response_class) + DecodePDU.add_pdu(custom_response_class) def close(self) -> None: """Close connection.""" self.ctx.close() - def execute(self, no_response_expected: bool, request: ModbusPDU): + def execute(self, no_response_expected: bool, request: ModbusPdu): """Execute request and get response (call **sync/async**). :meta private: @@ -120,7 +120,7 @@ def __str__(self): return f"{self.__class__.__name__} {self.ctx.comm_params.host}:{self.ctx.comm_params.port}" -class ModbusBaseSyncClient(ModbusClientMixin[ModbusPDU]): +class ModbusBaseSyncClient(ModbusClientMixin[ModbusPdu]): """**ModbusBaseClient**. :mod:`ModbusBaseClient` is normally not referenced outside :mod:`pymodbus`. @@ -132,7 +132,7 @@ def __init__( retries: int, comm_params: CommParams, trace_packet: Callable[[bool, bytes], bytes] | None, - trace_pdu: Callable[[bool, ModbusPDU], ModbusPDU] | None, + trace_pdu: Callable[[bool, ModbusPdu], ModbusPdu] | None, trace_connect: Callable[[bool], None] | None, ) -> None: """Initialize a client instance. @@ -161,7 +161,7 @@ def __init__( # ----------------------------------------------------------------------- # # Client external interface # ----------------------------------------------------------------------- # - def register(self, custom_response_class: type[ModbusPDU]) -> None: + def register(self, custom_response_class: type[ModbusPdu]) -> None: """Register a custom response class with the decoder. :param custom_response_class: (optional) Modbus response class. @@ -170,9 +170,9 @@ def register(self, custom_response_class: type[ModbusPDU]) -> None: Use register() to add non-standard responses (like e.g. a login prompt) and have them interpreted automatically. """ - self.framer.decoder.register(custom_response_class) + DecodePDU.add_pdu(custom_response_class) - def execute(self, no_response_expected: bool, request: ModbusPDU) -> ModbusPDU: + def execute(self, no_response_expected: bool, request: ModbusPdu) -> ModbusPdu: """Execute request and get response (call **sync/async**). :param no_response_expected: The client will not expect a response to the request diff --git a/pymodbus/client/mixin.py b/pymodbus/client/mixin.py index c495be6730..d5fd62c5e3 100644 --- a/pymodbus/client/mixin.py +++ b/pymodbus/client/mixin.py @@ -7,16 +7,14 @@ from collections.abc import Sequence from typing import Generic, Literal, TypeVar, cast -from ..constants import ModbusStatus from ..exceptions import ModbusException +from ..pdu import ModbusPdu, pack_bitstring, unpack_bitstring from ..pdu import bit_message as pdu_bit from ..pdu import diag_message as pdu_diag from ..pdu import file_message as pdu_file_msg from ..pdu import mei_message as pdu_mei from ..pdu import other_message as pdu_other_msg from ..pdu import register_message as pdu_reg -from ..pdu.pdu import ModbusPDU -from ..pdu.utils import pack_bitstring, unpack_bitstring T = TypeVar("T", covariant=False) @@ -51,7 +49,7 @@ class ModbusClientMixin(Generic[T]): # pylint: disable=too-many-public-methods def __init__(self): """Initialize.""" - def execute(self, no_response_expected: bool, request: ModbusPDU) -> T: + def execute(self, no_response_expected: bool, request: ModbusPdu) -> T: """Execute request.""" _ = no_response_expected, request return cast(T, None) @@ -78,7 +76,7 @@ def read_coils( """ return self.execute( no_response_expected, - pdu_bit.ReadCoilsRequest(address=address, count=count, dev_id=device_id), + pdu_bit.PDUreadCoils(device_id, address=address, count=count), ) def read_discrete_inputs( @@ -101,9 +99,7 @@ def read_discrete_inputs( Discrete Inputs are addressed as 0-N (Note some device manuals uses 1-N, assuming 1==0). """ - pdu = pdu_bit.ReadDiscreteInputsRequest( - address=address, count=count, dev_id=device_id - ) + pdu = pdu_bit.PDUreadDiscreteInputs(device_id, address=address, count=count) return self.execute(no_response_expected, pdu) def read_holding_registers( @@ -131,9 +127,7 @@ def read_holding_registers( """ return self.execute( no_response_expected, - pdu_reg.ReadHoldingRegistersRequest( - address=address, count=count, dev_id=device_id - ), + pdu_reg.PDUreadHoldingRegisters(device_id, address=address, count=count), ) def read_input_registers( @@ -161,9 +155,7 @@ def read_input_registers( """ return self.execute( no_response_expected, - pdu_reg.ReadInputRegistersRequest( - address=address, count=count, dev_id=device_id - ), + pdu_reg.PDUreadInputRegisters(device_id, address=address, count=count), ) def write_coil( @@ -186,9 +178,7 @@ def write_coil( Coils are addressed as 0-N (Note some device manuals uses 1-N, assuming 1==0). """ - pdu = pdu_bit.WriteSingleCoilRequest( - address=address, bits=[value], dev_id=device_id - ) + pdu = pdu_bit.PDUwriteSingleCoil(device_id, address=address, bits=[value]) return self.execute(no_response_expected, pdu) def write_register( @@ -216,8 +206,8 @@ def write_register( """ return self.execute( no_response_expected, - pdu_reg.WriteSingleRegisterRequest( - address=address, registers=[value], dev_id=device_id + pdu_reg.PDUwriteSingleRegister( + device_id, address=address, registers=[value] ), ) @@ -238,7 +228,7 @@ def read_exception_status( """ return self.execute( no_response_expected, - pdu_other_msg.ReadExceptionStatusRequest(dev_id=device_id), + pdu_other_msg.PDUreadExceptionStatus(device_id), ) def diag_query_data( @@ -256,7 +246,7 @@ def diag_query_data( request. """ return self.execute( - no_response_expected, pdu_diag.ReturnQueryDataRequest(msg, dev_id=device_id) + no_response_expected, pdu_diag.PDUreturnQueryData(device_id, message=msg) ) def diag_restart_communication( @@ -275,10 +265,10 @@ def diag_restart_communication( the only one that brings the port out of Listen Only Mode. If the port is not currently in Listen Only Mode, a normal response is returned. """ - msg = ModbusStatus.ON if toggle else ModbusStatus.OFF + msg = b"\xff\x00" if toggle else b"\x00\x00" return self.execute( no_response_expected, - pdu_diag.RestartCommunicationsOptionRequest(message=msg, dev_id=device_id), + pdu_diag.PDUrestartCommunicationsOption(device_id, message=msg), ) def diag_read_diagnostic_register( @@ -294,7 +284,7 @@ def diag_read_diagnostic_register( """ return self.execute( no_response_expected, - pdu_diag.ReturnDiagnosticRegisterRequest(dev_id=device_id), + pdu_diag.PDUreturnDiagnosticRegister(device_id), ) def diag_change_ascii_input_delimeter( @@ -318,9 +308,7 @@ def diag_change_ascii_input_delimeter( """ return self.execute( no_response_expected, - pdu_diag.ChangeAsciiInputDelimiterRequest( - message=delimiter, dev_id=device_id - ), + pdu_diag.PDUchangeAsciiInputDelimiter(device_id, message=delimiter), ) def diag_force_listen_only( @@ -339,7 +327,7 @@ def diag_force_listen_only( addressed remote device. No response is returned. """ return self.execute( - no_response_expected, pdu_diag.ForceListenOnlyModeRequest(dev_id=device_id) + no_response_expected, pdu_diag.PDUforceListenOnlyMode(device_id) ) def diag_clear_counters( @@ -353,9 +341,7 @@ def diag_clear_counters( Clear ll counters and the diagnostic register. Also, counters are cleared upon power-up """ - return self.execute( - no_response_expected, pdu_diag.ClearCountersRequest(dev_id=device_id) - ) + return self.execute(no_response_expected, pdu_diag.PDUclearCounters(device_id)) def diag_read_bus_message_count( self, *, device_id: int = 1, no_response_expected: bool = False @@ -372,7 +358,7 @@ def diag_read_bus_message_count( """ return self.execute( no_response_expected, - pdu_diag.ReturnBusMessageCountRequest(dev_id=device_id), + pdu_diag.PDUreturnBusMessageCount(device_id), ) def diag_read_bus_comm_error_count( @@ -390,7 +376,7 @@ def diag_read_bus_comm_error_count( """ return self.execute( no_response_expected, - pdu_diag.ReturnBusCommunicationErrorCountRequest(dev_id=device_id), + pdu_diag.PDUreturnBusCommunicationErrorCount(device_id), ) def diag_read_bus_exception_error_count( @@ -408,7 +394,7 @@ def diag_read_bus_exception_error_count( """ return self.execute( no_response_expected, - pdu_diag.ReturnBusExceptionErrorCountRequest(dev_id=device_id), + pdu_diag.PDUreturnBusExceptionErrorCount(device_id), ) def diag_read_device_message_count( @@ -426,7 +412,7 @@ def diag_read_device_message_count( """ return self.execute( no_response_expected, - pdu_diag.ReturnDeviceMessageCountRequest(dev_id=device_id), + pdu_diag.PDUreturnDeviceMessageCount(device_id), ) def diag_read_device_no_response_count( @@ -444,7 +430,7 @@ def diag_read_device_no_response_count( """ return self.execute( no_response_expected, - pdu_diag.ReturnDeviceNoResponseCountRequest(dev_id=device_id), + pdu_diag.PDUreturnDeviceNoResponseCount(device_id), ) def diag_read_device_nak_count( @@ -462,7 +448,7 @@ def diag_read_device_nak_count( Exception responses are described and listed in section 7 . """ return self.execute( - no_response_expected, pdu_diag.ReturnDeviceNAKCountRequest(dev_id=device_id) + no_response_expected, pdu_diag.PDUreturnDeviceNAKCount(device_id) ) def diag_read_device_busy_count( @@ -480,7 +466,7 @@ def diag_read_device_busy_count( """ return self.execute( no_response_expected, - pdu_diag.ReturnDeviceBusyCountRequest(dev_id=device_id), + pdu_diag.PDUreturnDeviceBusyCount(device_id), ) def diag_read_bus_char_overrun_count( @@ -500,7 +486,7 @@ def diag_read_bus_char_overrun_count( """ return self.execute( no_response_expected, - pdu_diag.ReturnDeviceBusCharacterOverrunCountRequest(dev_id=device_id), + pdu_diag.PDUreturnDeviceBusCharacterOverrunCount(device_id), ) def diag_read_iop_overrun_count( @@ -518,7 +504,7 @@ def diag_read_iop_overrun_count( """ return self.execute( no_response_expected, - pdu_diag.ReturnIopOverrunCountRequest(dev_id=device_id), + pdu_diag.PDUreturnIopOverrunCount(device_id), ) def diag_clear_overrun_counter( @@ -534,7 +520,7 @@ def diag_clear_overrun_counter( specification mentions is, so it is ignored. """ return self.execute( - no_response_expected, pdu_diag.ClearOverrunCountRequest(dev_id=device_id) + no_response_expected, pdu_diag.PDUclearOverrunCount(device_id) ) def diag_getclear_modbus_response( @@ -557,7 +543,7 @@ def diag_getclear_modbus_response( """ return self.execute( no_response_expected, - pdu_diag.GetClearModbusPlusRequest(message=data, dev_id=device_id), + pdu_diag.PDUgetClearModbusPlus(device_id, message=data), ) def diag_get_comm_event_counter( @@ -584,7 +570,7 @@ def diag_get_comm_event_counter( """ return self.execute( no_response_expected, - pdu_other_msg.GetCommEventCounterRequest(dev_id=device_id), + pdu_other_msg.PDUgetCommEventCounter(device_id), ) def diag_get_comm_event_log( @@ -615,7 +601,7 @@ def diag_get_comm_event_log( flushes the oldest byte from the field. """ return self.execute( - no_response_expected, pdu_other_msg.GetCommEventLogRequest(dev_id=device_id) + no_response_expected, pdu_other_msg.PDUgetCommEventLog(device_id) ) def write_coils( @@ -638,9 +624,7 @@ def write_coils( Coils are addressed as 0-N (Note some device manuals uses 1-N, assuming 1==0). """ - pdu = pdu_bit.WriteMultipleCoilsRequest( - address=address, bits=values, dev_id=device_id - ) + pdu = pdu_bit.PDUwriteMultipleCoils(device_id, address=address, bits=values) return self.execute(no_response_expected, pdu) def write_registers( @@ -664,8 +648,8 @@ def write_registers( """ return self.execute( no_response_expected, - pdu_reg.WriteMultipleRegistersRequest( - address=address, registers=values, dev_id=device_id + pdu_reg.PDUwriteMultipleRegisters( + device_id, address=address, registers=values ), ) @@ -682,7 +666,7 @@ def report_device_id( and other information specific to a remote device. """ return self.execute( - no_response_expected, pdu_other_msg.ReportDeviceIdRequest(dev_id=device_id) + no_response_expected, pdu_other_msg.PDUreportDeviceId(device_id) ) def read_file_record( @@ -721,7 +705,7 @@ def read_file_record( """ return self.execute( no_response_expected, - pdu_file_msg.ReadFileRecordRequest(records, dev_id=device_id), + pdu_file_msg.PDUreadFileRecord(records, dev_id=device_id), ) def write_file_record( @@ -745,7 +729,7 @@ def write_file_record( """ return self.execute( no_response_expected, - pdu_file_msg.WriteFileRecordRequest(records=records, dev_id=device_id), + pdu_file_msg.PDUwriteFileRecord(records=records, dev_id=device_id), ) def mask_write_register( @@ -773,8 +757,8 @@ def mask_write_register( """ return self.execute( no_response_expected, - pdu_reg.MaskWriteRegisterRequest( - address=address, and_mask=and_mask, or_mask=or_mask, dev_id=device_id + pdu_reg.PDUmaskWriteRegister( + device_id, address=address, and_mask=and_mask, or_mask=or_mask ), ) @@ -814,12 +798,12 @@ def readwrite_registers( write_address = address return self.execute( no_response_expected, - pdu_reg.ReadWriteMultipleRegistersRequest( + pdu_reg.PDUreadWriteMultipleRegisters( + device_id, read_address=read_address, - read_count=read_count, + count=read_count, write_address=write_address, write_registers=values, - dev_id=device_id, ), ) @@ -849,7 +833,7 @@ def read_fifo_queue( """ return self.execute( no_response_expected, - pdu_file_msg.ReadFifoQueueRequest(address, dev_id=device_id), + pdu_file_msg.PDUreadFifoQueue(address, dev_id=device_id), ) # code 0x2B sub 0x0D: CANopen General Reference Request and Response, NOT IMPLEMENTED @@ -880,8 +864,8 @@ def read_device_information( """ return self.execute( no_response_expected, - pdu_mei.ReadDeviceInformationRequest( - read_code, object_id, dev_id=device_id + pdu_mei.PDUreadDeviceInformation( + device_id, read_code=read_code, object_id=object_id ), ) diff --git a/pymodbus/client/serial.py b/pymodbus/client/serial.py index 3038b20781..7666796c70 100644 --- a/pymodbus/client/serial.py +++ b/pymodbus/client/serial.py @@ -11,7 +11,7 @@ from ..exceptions import ConnectionException from ..framer import FramerType from ..logging import Log -from ..pdu import ModbusPDU +from ..pdu import ModbusPdu from ..transport import CommParams, CommType from .base import ModbusBaseClient, ModbusBaseSyncClient @@ -82,7 +82,7 @@ def __init__( timeout: float = 3, retries: int = 3, trace_packet: Callable[[bool, bytes], bytes] | None = None, - trace_pdu: Callable[[bool, ModbusPDU], ModbusPDU] | None = None, + trace_pdu: Callable[[bool, ModbusPdu], ModbusPdu] | None = None, trace_connect: Callable[[bool], None] | None = None, ) -> None: """Initialize Asyncio Modbus Serial Client.""" @@ -174,7 +174,7 @@ def __init__( timeout: float = 3, retries: int = 3, trace_packet: Callable[[bool, bytes], bytes] | None = None, - trace_pdu: Callable[[bool, ModbusPDU], ModbusPDU] | None = None, + trace_pdu: Callable[[bool, ModbusPdu], ModbusPdu] | None = None, trace_connect: Callable[[bool], None] | None = None, ) -> None: """Initialize Modbus Serial Client.""" diff --git a/pymodbus/client/tcp.py b/pymodbus/client/tcp.py index cfc92fc8cb..b0c8341944 100644 --- a/pymodbus/client/tcp.py +++ b/pymodbus/client/tcp.py @@ -11,7 +11,7 @@ from ..exceptions import ConnectionException from ..framer import FramerType from ..logging import Log -from ..pdu import ModbusPDU +from ..pdu import ModbusPdu from ..transport import CommParams, CommType from .base import ModbusBaseClient, ModbusBaseSyncClient @@ -72,7 +72,7 @@ def __init__( timeout: float = 3, retries: int = 3, trace_packet: Callable[[bool, bytes], bytes] | None = None, - trace_pdu: Callable[[bool, ModbusPDU], ModbusPDU] | None = None, + trace_pdu: Callable[[bool, ModbusPdu], ModbusPdu] | None = None, trace_connect: Callable[[bool], None] | None = None, ) -> None: """Initialize Asyncio Modbus TCP Client.""" @@ -151,7 +151,7 @@ def __init__( timeout: float = 3, retries: int = 3, trace_packet: Callable[[bool, bytes], bytes] | None = None, - trace_pdu: Callable[[bool, ModbusPDU], ModbusPDU] | None = None, + trace_pdu: Callable[[bool, ModbusPdu], ModbusPdu] | None = None, trace_connect: Callable[[bool], None] | None = None, ) -> None: """Initialize Modbus TCP Client.""" diff --git a/pymodbus/client/tls.py b/pymodbus/client/tls.py index fe4cc0dfbe..01ea99742f 100644 --- a/pymodbus/client/tls.py +++ b/pymodbus/client/tls.py @@ -8,7 +8,7 @@ from ..framer import FramerType from ..logging import Log -from ..pdu import ModbusPDU +from ..pdu import ModbusPdu from ..transport import CommParams, CommType from .tcp import AsyncModbusTcpClient, ModbusTcpClient @@ -71,7 +71,7 @@ def __init__( timeout: float = 3, retries: int = 3, trace_packet: Callable[[bool, bytes], bytes] | None = None, - trace_pdu: Callable[[bool, ModbusPDU], ModbusPDU] | None = None, + trace_pdu: Callable[[bool, ModbusPdu], ModbusPdu] | None = None, trace_connect: Callable[[bool], None] | None = None, ): """Initialize Asyncio Modbus TLS Client.""" @@ -152,7 +152,7 @@ def __init__( timeout: float = 3, retries: int = 3, trace_packet: Callable[[bool, bytes], bytes] | None = None, - trace_pdu: Callable[[bool, ModbusPDU], ModbusPDU] | None = None, + trace_pdu: Callable[[bool, ModbusPdu], ModbusPdu] | None = None, trace_connect: Callable[[bool], None] | None = None, ): """Initialize Modbus TLS Client.""" diff --git a/pymodbus/client/udp.py b/pymodbus/client/udp.py index 5b50a22cfd..ef89bbe16f 100644 --- a/pymodbus/client/udp.py +++ b/pymodbus/client/udp.py @@ -8,7 +8,7 @@ from ..exceptions import ConnectionException from ..framer import FramerType from ..logging import Log -from ..pdu import ModbusPDU +from ..pdu import ModbusPdu from ..transport import CommParams, CommType from .base import ModbusBaseClient, ModbusBaseSyncClient @@ -72,7 +72,7 @@ def __init__( timeout: float = 3, retries: int = 3, trace_packet: Callable[[bool, bytes], bytes] | None = None, - trace_pdu: Callable[[bool, ModbusPDU], ModbusPDU] | None = None, + trace_pdu: Callable[[bool, ModbusPdu], ModbusPdu] | None = None, trace_connect: Callable[[bool], None] | None = None, ) -> None: """Initialize Asyncio Modbus UDP Client.""" @@ -151,7 +151,7 @@ def __init__( timeout: float = 3, retries: int = 3, trace_packet: Callable[[bool, bytes], bytes] | None = None, - trace_pdu: Callable[[bool, ModbusPDU], ModbusPDU] | None = None, + trace_pdu: Callable[[bool, ModbusPdu], ModbusPdu] | None = None, trace_connect: Callable[[bool], None] | None = None, ) -> None: """Initialize Modbus UDP Client.""" diff --git a/pymodbus/constants.py b/pymodbus/constants.py index 27e1c55247..46078e683a 100644 --- a/pymodbus/constants.py +++ b/pymodbus/constants.py @@ -55,22 +55,8 @@ class ExcCodes(enum.IntEnum): GATEWAY_NO_RESPONSE = 0x0B -class ModbusPlusOperation(enum.IntEnum): - """Represents the type of modbus plus request. - - .. attribute:: GET_STATISTICS - - Operation requesting that the current modbus plus statistics - be returned in the response. - - .. attribute:: CLEAR_STATISTICS - - Operation requesting that the current modbus plus statistics - be cleared and not returned in the response. - """ - - GET_STATISTICS = 0x0003 - CLEAR_STATISTICS = 0x0004 +GET_STATISTICS = b"\x00\x03" +CLEAR_STATISTICS = b"\x00\x04" class DeviceInformation(enum.IntEnum): diff --git a/pymodbus/framer/ascii.py b/pymodbus/framer/ascii.py index 9ee0de3ed9..f156cf11b9 100644 --- a/pymodbus/framer/ascii.py +++ b/pymodbus/framer/ascii.py @@ -10,7 +10,7 @@ from binascii import a2b_hex, b2a_hex from ..logging import Log -from ..pdu.device import ModbusControlBlock +from ..pdu import ModbusControlBlock from .base import FramerBase diff --git a/pymodbus/framer/base.py b/pymodbus/framer/base.py index e0d8a5690f..e7888f9b6b 100644 --- a/pymodbus/framer/base.py +++ b/pymodbus/framer/base.py @@ -11,7 +11,7 @@ from ..exceptions import ModbusIOException from ..logging import Log -from ..pdu import DecodePDU, ModbusPDU +from ..pdu import DecodePDU, ModbusPdu class FramerType(str, Enum): @@ -56,7 +56,7 @@ def encode(self, payload: bytes, _dev_id: int, _tid: int) -> bytes: """ return payload - def buildFrame(self, message: ModbusPDU) -> bytes: + def buildFrame(self, message: ModbusPdu) -> bytes: """Create a ready to send modbus packet. :param message: The populated request/response to send @@ -67,7 +67,7 @@ def buildFrame(self, message: ModbusPDU) -> bytes: def handleFrame( self, data: bytes, exp_devid: int, exp_tid: int - ) -> tuple[int, ModbusPDU | None]: + ) -> tuple[int, ModbusPdu | None]: """Process incoming data.""" used_len = 0 while True: diff --git a/pymodbus/pdu/__init__.py b/pymodbus/pdu/__init__.py index 45112f9e1d..2c5556fc19 100644 --- a/pymodbus/pdu/__init__.py +++ b/pymodbus/pdu/__init__.py @@ -2,22 +2,32 @@ __all__ = [ "DecodePDU", - "DiagnosticBase", - "ExceptionResponse", "FileRecord", - "ModbusPDU", - "ReadCoilsRequest", - "ReadDeviceInformationRequest", - "ReadExceptionStatusRequest", - "ReadHoldingRegistersRequest", + "ModbusControlBlock", + "ModbusDeviceIdentification", + "ModbusPdu", + "PDUexception", + "StoreGet", + "StoreSet", + "pack_bitstring", + "unpack_bitstring", ] -from .bit_message import ReadCoilsRequest -from .decoders import DecodePDU -from .diag_message import DiagnosticBase -from .exceptionresponse import ExceptionResponse +from .bit_message import PDUreadCoils +from .device import ModbusControlBlock, ModbusDeviceIdentification +from .diag_message import PDUdiagnostic +from .events import ModbusEvent from .file_message import FileRecord -from .mei_message import ReadDeviceInformationRequest -from .other_message import ReadExceptionStatusRequest -from .pdu import ModbusPDU -from .register_message import ReadHoldingRegistersRequest +from .mei_message import PDUreadDeviceInformation +from .other_message import PDUreadExceptionStatus +from .pdu import DecodePDU, ModbusPdu, PDUexception, StoreGet, StoreSet +from .register_message import PDUreadHoldingRegisters +from .utils import pack_bitstring, unpack_bitstring + + +_ = PDUreadCoils +_ = PDUdiagnostic +_ = ModbusEvent +_ = PDUreadDeviceInformation +_ = PDUreadExceptionStatus +_ = PDUreadHoldingRegisters diff --git a/pymodbus/pdu/bit_message.py b/pymodbus/pdu/bit_message.py index 49bb68f0ee..74adaea0bd 100644 --- a/pymodbus/pdu/bit_message.py +++ b/pymodbus/pdu/bit_message.py @@ -2,299 +2,172 @@ import struct from collections.abc import Sequence +from copy import copy from typing import cast from ..constants import ExcCodes, ModbusStatus -from ..datastore import ModbusServerContext -from .decoders import DecodePDU -from .exceptionresponse import ExceptionResponse -from .pdu import ModbusPDU +from .pdu import DecodePDU, ModbusPdu, PDUexception, StoreGet, StoreSet from .utils import pack_bitstring, unpack_bitstring -class ReadCoilsRequest(ModbusPDU): - """ReadCoilsRequest.""" +class PDUreadCoils(ModbusPdu): + """ReadCoils.""" function_code = 1 MAX_COUNT = 2000 - @classmethod - def x_calculateRtuFrameSize(cls, data: bytes) -> int: - """Calculate the size of a PDU.""" - return 8 - - def x_encode(self) -> bytes: - """Encode a request pdu.""" - self.verifyCount(self.MAX_COUNT) - self.verifyAddress() - return struct.pack(">HH", self.address, self.count) - - def x_decode(self, data: bytes) -> None: - """Decode a request pdu.""" - self.address, self.count = struct.unpack(">HH", data[:4]) - - async def datastore_update( - self, context: ModbusServerContext, device_id: int - ) -> ModbusPDU: - """Run request against a datastore.""" - if not 1 <= self.count <= self.MAX_COUNT: - return ExceptionResponse(self.function_code, ExcCodes.ILLEGAL_VALUE) - values = await context.async_getValues( - device_id, self.function_code, self.address, self.count - ) - if isinstance(values, ExcCodes): - return ExceptionResponse(self.function_code, values) - return ReadCoilsResponse( - dev_id=self.dev_id, - transaction_id=self.transaction_id, - bits=cast(list[bool], values), - ) - + def __init__( + self, + *args, + bits: Sequence[bool] | None = None, + **kwargs, + ) -> None: + """Initialize.""" + if bits: + kwargs |= {"count": kwargs.get("count", len(bits))} + super().__init__(*args, **kwargs) + self.bits = copy(bits) if bits else [] -class ReadDiscreteInputsRequest(ModbusPDU): - """ReadDiscreteInputsRequest.""" - - function_code = 2 - MAX_COUNT = 2000 + def __str__(self) -> str: + """Build representation of pdu.""" + return super().__str__() + f", bits={self.bits}" @classmethod - def x_calculateRtuFrameSize(cls, data: bytes) -> int: + def calculatePduSize(cls, is_request: bool, offset: int, data: bytes) -> int: """Calculate the size of a PDU.""" - return 8 + return 8 if is_request else int(data[offset + 2]) + 2 + 3 - def x_encode(self) -> bytes: - """Encode a request pdu.""" - self.verifyCount(self.MAX_COUNT) - self.verifyAddress() - return struct.pack(">HH", self.address, self.count) + def to_bytes(self) -> bytes: + """Encode the message.""" + if self.is_request: + return struct.pack(">HH", self.address, self.count) + result = pack_bitstring(self.bits) + return struct.pack(">B", len(result)) + result - def x_decode(self, data: bytes) -> None: - """Decode a request pdu.""" - self.address, self.count = struct.unpack(">HH", data[:4]) + def from_bytes(self, offset: int, data: bytes) -> None: + """Decode payload.""" + if self.is_request: + self.address, self.count = struct.unpack(">HH", data[offset : offset + 4]) + else: + self.bits = unpack_bitstring(data[offset + 1 :]) + self.count = len(self.bits) - async def datastore_update( - self, context: ModbusServerContext, device_id: int - ) -> ModbusPDU: - """Run request against a datastore.""" - if not 1 <= self.count <= self.MAX_COUNT: - return ExceptionResponse(self.function_code, ExcCodes.ILLEGAL_VALUE) - values = await context.async_getValues( - device_id, self.function_code, self.address, self.count + async def build_response( + self, get_values: StoreGet, set_values: StoreSet + ) -> ModbusPdu: + """Get bits from datastore.""" + values = await get_values( + self.dev_id, self.function_code, self.address, self.count ) if isinstance(values, ExcCodes): - return ExceptionResponse(self.function_code, values) - return ReadDiscreteInputsResponse( - dev_id=self.dev_id, - transaction_id=self.transaction_id, - bits=cast(list[bool], values), - ) - - -class ReadCoilsResponse(ModbusPDU): - """ReadCoilsResponse.""" - - function_code = 1 - - @classmethod - def x_calculateRtuFrameSize(cls, data: bytes) -> int: - """Calculate the size of a PDU.""" - return int(data[2]) + 2 + 3 + return PDUexception(self.dev_id, self.function_code, values) + self.is_request = False + self.bits = cast(list[bool], values) + return self - def x_encode(self) -> bytes: - """Encode response pdu.""" - result = pack_bitstring(self.bits) - return struct.pack(">B", len(result)) + result - - def x_decode(self, data: bytes) -> None: - """Decode response pdu.""" - self.bits = unpack_bitstring(data[1:]) - -class ReadDiscreteInputsResponse(ModbusPDU): - """ReadDiscreteInputsResponse.""" +class PDUreadDiscreteInputs(PDUreadCoils): + """ReadDiscreteInputsRequest.""" function_code = 2 - @classmethod - def x_calculateRtuFrameSize(cls, data: bytes) -> int: - """Calculate the size of a PDU.""" - return int(data[2]) + 2 + 3 - - def x_encode(self) -> bytes: - """Encode response pdu.""" - result = pack_bitstring(self.bits) - return struct.pack(">B", len(result)) + result - - def x_decode(self, data: bytes) -> None: - """Decode response pdu.""" - self.bits = unpack_bitstring(data[1:]) - -class WriteSingleCoilResponse(ModbusPDU): - """WriteSingleCoilResponse.""" - - function_code = 5 - - @classmethod - def x_calculateRtuFrameSize(cls, data: bytes) -> int: - """Calculate the size of a PDU.""" - return 8 - - def x_encode(self) -> bytes: - """Encode write coil request.""" - self.verifyAddress() - val = ModbusStatus.ON if self.bits[0] else ModbusStatus.OFF - return struct.pack(">HH", self.address, val) - - def x_decode(self, data: bytes) -> None: - """Decode a write coil request.""" - self.address, value = struct.unpack(">HH", data[:4]) - self.bits = [bool(value)] - - -class WriteSingleCoilRequest(ModbusPDU): - """WriteSingleCoilRequest.""" +class PDUwriteSingleCoil(PDUreadCoils): + """WriteSingleCoil.""" function_code = 5 - _value: int | None = None @classmethod - def x_calculateRtuFrameSize(cls, data: bytes) -> int: + def calculatePduSize(cls, is_request: bool, offset: int, data: bytes) -> int: """Calculate the size of a PDU.""" return 8 - def x_encode(self) -> bytes: - """Encode write coil request.""" - self.verifyAddress() + def to_bytes(self) -> bytes: + """Encode the message.""" val = ModbusStatus.ON if self.bits[0] else ModbusStatus.OFF return struct.pack(">HH", self.address, val) - def x_decode(self, data: bytes) -> None: - """Decode a write coil request.""" - self.address, self._value = struct.unpack(">HH", data[:4]) - self.bits = [bool(self._value)] + def from_bytes(self, offset: int, data: bytes) -> None: + """Decode payload.""" + self.address, value = struct.unpack(">HH", data[offset : offset + 4]) + if value in (ModbusStatus.ON, ModbusStatus.OFF): + self.bits = [bool(value)] + else: + self.bits = [] + self.count = len(self.bits) - async def datastore_update( - self, context: ModbusServerContext, device_id: int - ) -> ModbusPDU: - """Run a request against a datastore.""" - if self._value not in (None, ModbusStatus.ON, ModbusStatus.OFF): - return ExceptionResponse(self.function_code, ExcCodes.ILLEGAL_VALUE) - if rc := await context.async_setValues( - device_id, self.function_code, self.address, self.bits + async def build_response( + self, get_values: StoreGet, set_values: StoreSet + ) -> ModbusPdu: + """Get bits from datastore.""" + if not self.bits: + return PDUexception(self.dev_id, self.function_code, ExcCodes.ILLEGAL_VALUE) + if rc := await set_values( + self.dev_id, self.function_code, self.address, self.bits ): - return ExceptionResponse(self.function_code, rc) - values = await context.async_getValues( - device_id, self.function_code, self.address, 1 + return PDUexception(self.dev_id, self.function_code, rc) + + values = await get_values( + self.dev_id, self.function_code, self.address, self.count ) if isinstance(values, ExcCodes): - return ExceptionResponse(self.function_code, values) - return WriteSingleCoilResponse( - address=self.address, - bits=cast(list[bool], values), - dev_id=self.dev_id, - transaction_id=self.transaction_id, - ) + return PDUexception(self.dev_id, self.function_code, values) + self.is_request = False + self.bits = cast(list[bool], values) + return self -class WriteMultipleCoilsRequest(ModbusPDU): - """WriteMultipleCoilsRequest.""" +class PDUwriteMultipleCoils(PDUreadCoils): + """WriteMultipleCoils.""" function_code = 15 - count: int - byte_count: int | None - data_byte_count: int MAX_COUNT = 2000 @classmethod - def x_calculateRtuFrameSize(cls, data: bytes) -> int: + def calculatePduSize(cls, is_request: bool, offset: int, data: bytes) -> int: """Calculate the size of a PDU.""" - return int(data[6]) + 6 + 3 - - def __init__( - self, - address: int = 0, - bits: Sequence[bool] | None = None, - dev_id: int = 0, - transaction_id: int = 0, - ) -> None: - """Initialize a write multiple coils request.""" - super().__init__( - address=address, - bits=bits, - dev_id=dev_id, - transaction_id=transaction_id, - ) - self.count = len(self.bits) - self.byte_count = None - self.data_byte_count = 0 - - def x_encode(self) -> bytes: - """Encode write coils request.""" - self.count = len(self.bits) - self.verifyAddress() - self.verifyCount(self.MAX_COUNT) - byte_count = (self.count + 7) // 8 - return struct.pack( - ">HHB", self.address, self.count, byte_count - ) + pack_bitstring(self.bits) - - def x_decode(self, data: bytes) -> None: - """Decode a write coils request.""" - self.address, self.count, self.byte_count = struct.unpack(">HHB", data[0:5]) - self.verifyCount(self.MAX_COUNT) - self.data_byte_count = len(data) - 5 - self.bits = unpack_bitstring(data[5 : 5 + cast(int, self.byte_count)])[ - : self.count - ] + return int(data[offset + 6]) + 6 + 3 if is_request else 8 + + def to_bytes(self) -> bytes: + """Encode the message.""" + if self.is_request: + byte_count = (self.count + 7) // 8 + return struct.pack( + ">HHB", self.address, self.count, byte_count + ) + pack_bitstring(self.bits) + return struct.pack(">HH", self.address, self.count) - async def datastore_update( - self, context: ModbusServerContext, device_id: int - ) -> ModbusPDU: - """Run a request against a datastore.""" - if self.byte_count is None: - self.count = len(self.bits) + def from_bytes(self, offset: int, data: bytes) -> None: + """Decode payload.""" + if self.is_request: + self.address, self.count, byte_count = struct.unpack( + ">HHB", data[offset : offset + 5] + ) + self.bits = unpack_bitstring( + data[offset + 5 : offset + 5 + cast(int, byte_count)] + )[: self.count] else: - expected_byte_count = (self.count + 7) // 8 - if ( - self.byte_count != expected_byte_count - or self.data_byte_count != self.byte_count - ): - return ExceptionResponse(self.function_code, ExcCodes.ILLEGAL_VALUE) - rc = await context.async_setValues( - device_id, self.function_code, self.address, self.bits - ) - if rc: - return ExceptionResponse(self.function_code, rc) - - return WriteMultipleCoilsResponse( - address=self.address, - count=self.count, - dev_id=self.dev_id, - transaction_id=self.transaction_id, + self.address, self.count = struct.unpack(">HH", data[offset : offset + 4]) + + async def build_response( + self, get_values: StoreGet, set_values: StoreSet + ) -> ModbusPdu: + """Get bits from datastore.""" + if rc := await set_values( + self.dev_id, self.function_code, self.address, self.bits + ): + return PDUexception(self.dev_id, self.function_code, rc) + values = await get_values( + self.dev_id, self.function_code, self.address, self.count ) + if isinstance(values, ExcCodes): + return PDUexception(self.dev_id, self.function_code, values) + self.is_request = False + self.bits = cast(list[bool], values) + return self -class WriteMultipleCoilsResponse(ModbusPDU): - """WriteMultipleCoilsResponse.""" - - function_code = 15 - - @classmethod - def x_calculateRtuFrameSize(cls, data: bytes) -> int: - """Calculate the size of a PDU.""" - return 8 - - def x_encode(self) -> bytes: - """Encode write coils response.""" - return struct.pack(">HH", self.address, self.count) - - def x_decode(self, data: bytes) -> None: - """Decode a write coils response.""" - self.address, self.count = struct.unpack(">HH", data[:4]) - - -DecodePDU.add_pdu(ReadCoilsRequest, ReadCoilsResponse) -DecodePDU.add_pdu(ReadDiscreteInputsRequest, ReadDiscreteInputsResponse) -DecodePDU.add_pdu(WriteSingleCoilRequest, WriteSingleCoilResponse) -DecodePDU.add_pdu(WriteMultipleCoilsRequest, WriteMultipleCoilsResponse) +DecodePDU.add_pdu(PDUreadCoils) +DecodePDU.add_pdu(PDUreadDiscreteInputs) +DecodePDU.add_pdu(PDUwriteSingleCoil) +DecodePDU.add_pdu(PDUwriteMultipleCoils) diff --git a/pymodbus/pdu/decoders.py b/pymodbus/pdu/decoders.py deleted file mode 100644 index d0114684b3..0000000000 --- a/pymodbus/pdu/decoders.py +++ /dev/null @@ -1,105 +0,0 @@ -"""Modbus Request/Response Decoders.""" - -from __future__ import annotations - -import copy -import struct - -from ..exceptions import MessageRegisterException, ModbusIOException -from ..logging import Log -from .exceptionresponse import ExceptionResponse -from .pdu import ModbusPDU - - -class DecodePDU: - """Decode pdu requests/responses (server/client).""" - - pdu_table: dict[int, tuple[type[ModbusPDU], type[ModbusPDU]]] = {} - pdu_sub_table: dict[int, dict[int, tuple[type[ModbusPDU], type[ModbusPDU]]]] = {} - - def __init__(self, is_server: bool) -> None: - """Initialize function_tables.""" - self.pdu_inx = 0 if is_server else 1 - self.is_server = is_server - - def _lookupPduClass(self, data: bytes) -> type[ModbusPDU] | None: - """Use `function_code` to determine the class of the PDU.""" - if (func_code := int(data[1])) > 0x80: - return ExceptionResponse - if not (pdu := self.pdu_table.get(func_code, (None, None))[self.pdu_inx]): - return None - if (sub_func_code := pdu.decode_sub_function_code(data)) < 0: - return pdu - return self.pdu_sub_table[func_code].get(sub_func_code, (None, None))[ - self.pdu_inx - ] - - def list_function_codes(self): - """Return list of function codes.""" - return list(self.pdu_table) - - @classmethod - def add_pdu(cls, req: type[ModbusPDU], resp: type[ModbusPDU]): - """Register request/response.""" - cls.pdu_table[req.function_code] = (req, resp) - - @classmethod - def add_sub_pdu(cls, req: type[ModbusPDU], resp: type[ModbusPDU]): - """Register request/response.""" - if req.function_code not in cls.pdu_sub_table: - cls.pdu_sub_table[req.function_code] = {} - cls.pdu_sub_table[req.function_code][req.sub_function_code] = (req, resp) - - def register(self, custom_class: type[ModbusPDU]) -> None: - """Register a function and sub function class with the decoder.""" - if not issubclass(custom_class, ModbusPDU): - raise MessageRegisterException( - f'"{custom_class.__class__.__name__}" is Not a valid Modbus Message' - ". Class needs to be derived from `ModbusPDU` " - ) - if "pdu_table" not in self.__dict__: - self.pdu_table = copy.deepcopy(DecodePDU.pdu_table) - self.pdu_table[custom_class.function_code] = (custom_class, custom_class) - - def decode(self, frame: bytes) -> ModbusPDU: - """Decode a frame.""" - if not frame: - raise ModbusIOException("Cannot decode empty frame") - function_code = 0 - try: - if (function_code := int(frame[0])) > 0x80: - if self.is_server: - raise ModbusIOException( - f"Reserved function code {function_code}", - function_code=function_code, - ) - pdu_exp = ExceptionResponse(function_code & 0x7F) - pdu_exp.from_frame(False, frame) - return pdu_exp - if not (pdu_class := self._lookupPduClass(b"\x00" + frame)): - raise ModbusIOException( - f"Unknown response {function_code}", function_code=function_code - ) - pdu = pdu_class() - pdu.x_decode(frame[1:]) - Log.debug( - "decoded PDU function_code({} sub {}) -> {} ", - pdu.function_code, - pdu.sub_function_code, - str(pdu), - ) - return pdu - except (ValueError, IndexError, struct.error) as exc: - raise ModbusIOException( - f"Unable to decode frame {exc}", function_code=function_code - ) from exc - - def calculateRtuFrameSize(self, data: bytes) -> int: - """Calculate the size of a PDU.""" - if not (pdu_class := self._lookupPduClass(data)): - return 0 - return pdu_class.x_calculateRtuFrameSize(data) - - def encode(self, pdu: ModbusPDU) -> bytes: - """Calculate the size of a PDU.""" - return pdu.x_encode() diff --git a/pymodbus/pdu/device.py b/pymodbus/pdu/device.py index 5f98dcd297..4d5c499b4a 100644 --- a/pymodbus/pdu/device.py +++ b/pymodbus/pdu/device.py @@ -105,7 +105,7 @@ def summary(self): """ return iter(self.stat_data.values()) - def x_encode(self) -> list[int]: + def encode(self) -> list[int]: """Return a summary of the modbus plus statistics. :returns: An iterator over lists of 8-bit integers representing each statistic @@ -487,7 +487,7 @@ def getEvents(self): :returns: The encoded events packet """ - events = [event.x_encode() for event in self._events] + events = [event.encode() for event in self._events] return b"".join(events) def clearEvents(self): diff --git a/pymodbus/pdu/diag_message.py b/pymodbus/pdu/diag_message.py index aa17c82c3a..f7303c79ca 100644 --- a/pymodbus/pdu/diag_message.py +++ b/pymodbus/pdu/diag_message.py @@ -3,504 +3,270 @@ from __future__ import annotations import struct -from typing import cast -from ..constants import ModbusPlusOperation -from ..datastore import ModbusServerContext -from .decoders import DecodePDU +from ..constants import CLEAR_STATISTICS from .device import ModbusControlBlock -from .pdu import ModbusPDU +from .pdu import DecodePDU, ModbusPdu, StoreGet, StoreSet from .utils import pack_bitstring _MCB = ModbusControlBlock() -class DiagnosticBase(ModbusPDU): - """DiagnosticBase.""" +class PDUdiagnostic(ModbusPdu): + """Diagnostic base.""" function_code = 0x08 - sub_function_code: int = 9999 - - @classmethod - def x_calculateRtuFrameSize(cls, data: bytes) -> int: - """Calculate the size of a PDU.""" - return 8 def __init__( self, - message: bytes | int | list | tuple | None = 0, - dev_id: int = 1, - transaction_id: int = 0, + *args, + message: bytes | int | list | tuple | None = None, + **kwargs, ) -> None: - """Initialize a diagnostic request.""" - super().__init__(transaction_id=transaction_id, dev_id=dev_id) - self.message: bytes | int | list | tuple | None = message + """Initialize.""" + super().__init__(*args, **kwargs) + if isinstance(message, int): + self.message = message.to_bytes(2, "big") + elif isinstance(message, (list, tuple)): + packet = b"" + for piece in message: + packet += struct.pack(">H", piece) + self.message = packet + else: + self.message = message or b"\x00\x00" - def x_encode(self) -> bytes: - """Encode a diagnostic response.""" - packet = struct.pack(">H", self.sub_function_code) - if self.message is not None: - if isinstance(self.message, bytes): - packet += self.message - return packet - if isinstance(self.message, int): - packet += struct.pack(">H", self.message) - return packet - if isinstance(self.message, (list, tuple)): - for piece in self.message: - packet += struct.pack(">H", piece) - return packet - raise TypeError(f"UNKNOWN DIAG message type: {type(self.message)}") - return packet + def __str__(self) -> str: + """Build representation of pdu.""" + msg = "" + return super().__str__() + f", message={msg}" @classmethod - def decode_sub_function_code(cls, data: bytes) -> int: - """Decode sub function code (2 bytes).""" - return int.from_bytes(data[2:4], "big") - - def x_decode(self, data: bytes) -> None: - """Decode a diagnostic request.""" - (self.sub_function_code,) = struct.unpack(">H", data[:2]) - data = data[2:] - if self.sub_function_code == ReturnQueryDataRequest.sub_function_code: - self.message = data - elif data_len := len(data): - if data_len % 2: - data_len += 1 - data += b"0" - if (word_len := data_len // 2) == 1: - (self.message,) = struct.unpack(">H", data[:2]) - else: - self.message = struct.unpack(">" + "H" * word_len, data[: 2 * word_len]) - - async def datastore_update( - self, context: ModbusServerContext, device_id: int - ) -> ModbusPDU: - """Implement dummy.""" - _ = context - response = { - DiagnosticBase.sub_function_code: DiagnosticBase, - ReturnQueryDataResponse.sub_function_code: ReturnQueryDataResponse, - RestartCommunicationsOptionResponse.sub_function_code: RestartCommunicationsOptionResponse, - }[self.sub_function_code] - return response( - message=self.message, dev_id=device_id, transaction_id=self.transaction_id - ) - - -class ReturnQueryDataRequest(DiagnosticBase): - """ReturnQueryDataRequest.""" + def calculatePduSize(cls, is_request: bool, offset: int, data: bytes) -> int: + """Calculate the size of a PDU.""" + return 8 - sub_function_code = 0x0000 + @classmethod + def decode_sub_function_code(cls, offset: int, data: bytes) -> int: + """Decode sub function code.""" + return int.from_bytes(data[offset + 2 : offset + 4], "big") + def to_bytes(self) -> bytes: + """Encode the message.""" + return struct.pack(">H", self.sub_function_code) + self.message -class ReturnQueryDataResponse(DiagnosticBase): - """ReturnQueryDataResponse.""" + def from_bytes(self, offset: int, data: bytes) -> None: + """Decode payload.""" + (self.sub_function_code,) = struct.unpack(">H", data[offset : offset + 2]) + self.message = data[offset + 2 :] - sub_function_code = 0x0000 + def sub_build_response(self) -> None: + """get/set info in _MCB.""" + return + async def build_response( + self, get_values: StoreGet, set_values: StoreSet + ) -> ModbusPdu: + """Update internal data.""" + self.is_request = False + self.sub_build_response() + return self -class RestartCommunicationsOptionRequest(DiagnosticBase): - """RestartCommunicationsOptionRequest.""" - sub_function_code = 0x0001 +class PDUreturnQueryData(PDUdiagnostic): + """ReturnQueryData.""" + + sub_function_code = 0x0000 -class RestartCommunicationsOptionResponse(DiagnosticBase): - """RestartCommunicationsOptionResponse.""" +class PDUrestartCommunicationsOption(PDUdiagnostic): + """RestartCommunicationsOption.""" sub_function_code = 0x0001 -class ReturnDiagnosticRegisterRequest(DiagnosticBase): - """ReturnDiagnosticRegisterRequest.""" +class PDUreturnDiagnosticRegister(PDUdiagnostic): + """ReturnDiagnosticRegister.""" sub_function_code = 0x0002 - async def datastore_update( - self, context: ModbusServerContext, device_id: int - ) -> ModbusPDU: - """Update diagnostic request on the given device.""" - _ = context - register = pack_bitstring(_MCB.getDiagnosticRegister()) - return ReturnDiagnosticRegisterResponse( - message=register, dev_id=device_id, transaction_id=self.transaction_id - ) - - -class ReturnDiagnosticRegisterResponse(DiagnosticBase): - """ReturnDiagnosticRegisterResponse.""" - - sub_function_code = 0x0002 + def sub_build_response(self) -> None: + """Get/set info in _MCB.""" + self.message = pack_bitstring(_MCB.getDiagnosticRegister()) -class ChangeAsciiInputDelimiterRequest(DiagnosticBase): - """ChangeAsciiInputDelimiterRequest.""" +class PDUchangeAsciiInputDelimiter(PDUdiagnostic): + """ChangeAsciiInputDelimiter.""" sub_function_code = 0x0003 - async def datastore_update( - self, context: ModbusServerContext, device_id: int - ) -> ModbusPDU: - """Update diagnostic request on the given device.""" - _ = context - char = (cast(int, self.message) & 0xFF00) >> 8 + def sub_build_response(self) -> None: + """Get/set info in _MCB.""" + char = self.message[0] _MCB.Delimiter = char - return ChangeAsciiInputDelimiterResponse( - message=self.message, dev_id=device_id, transaction_id=self.transaction_id - ) - - -class ChangeAsciiInputDelimiterResponse(DiagnosticBase): - """ChangeAsciiInputDelimiterResponse.""" - - sub_function_code = 0x0003 -class ForceListenOnlyModeRequest(DiagnosticBase): - """ForceListenOnlyModeRequest.""" +class PDUforceListenOnlyMode(PDUdiagnostic): + """ForceListenOnlyMode.""" sub_function_code = 0x0004 - async def datastore_update( - self, context: ModbusServerContext, device_id: int - ) -> ModbusPDU: - """Update diagnostic request on the given device.""" - _ = context + def sub_build_response(self) -> None: + """Get/set info in _MCB.""" _MCB.ListenOnly = True - return ForceListenOnlyModeResponse( - dev_id=device_id, transaction_id=self.transaction_id - ) - - -class ForceListenOnlyModeResponse(DiagnosticBase): - """ForceListenOnlyModeResponse. - - This does not send a response - """ - - sub_function_code = 0x0004 - def __init__(self, dev_id=1, transaction_id=0): - """Initialize to block a return response.""" - DiagnosticBase.__init__(self, dev_id=dev_id, transaction_id=transaction_id) - self.message = [] - -class ClearCountersRequest(DiagnosticBase): - """ClearCountersRequest.""" +class PDUclearCounters(PDUdiagnostic): + """ClearCounters.""" sub_function_code = 0x000A - async def datastore_update( - self, context: ModbusServerContext, device_id: int - ) -> ModbusPDU: - """Update diagnostic request on the given device.""" - _ = context + def sub_build_response(self) -> None: + """Get/set info in _MCB.""" _MCB.reset() - return ClearCountersResponse( - dev_id=device_id, transaction_id=self.transaction_id - ) - - -class ClearCountersResponse(DiagnosticBase): - """ClearCountersResponse.""" - - sub_function_code = 0x000A -class ReturnBusMessageCountRequest(DiagnosticBase): - """ReturnBusMessageCountRequest.""" +class PDUreturnBusMessageCount(PDUdiagnostic): + """ReturnBusMessageCount.""" sub_function_code = 0x000B - async def datastore_update( - self, context: ModbusServerContext, device_id: int - ) -> ModbusPDU: - """Update diagnostic request on the given device.""" - _ = context - count = _MCB.Counter.BusMessage - return ReturnBusMessageCountResponse( - message=count, dev_id=device_id, transaction_id=self.transaction_id - ) + def sub_build_response(self) -> None: + """Get/set info in _MCB.""" + self.message = _MCB.Counter.BusMessage -class ReturnBusMessageCountResponse(DiagnosticBase): - """ReturnBusMessageCountResponse.""" - - sub_function_code = 0x000B - - -class ReturnBusCommunicationErrorCountRequest(DiagnosticBase): - """ReturnBusCommunicationErrorCountRequest.""" +class PDUreturnBusCommunicationErrorCount(PDUdiagnostic): + """ReturnBusCommunicationErrorCount.""" sub_function_code = 0x000C - async def datastore_update( - self, context: ModbusServerContext, device_id: int - ) -> ModbusPDU: - """Update diagnostic request on the given device.""" - _ = context - count = _MCB.Counter.BusCommunicationError - return ReturnBusCommunicationErrorCountResponse( - message=count, dev_id=device_id, transaction_id=self.transaction_id - ) - + def sub_build_response(self) -> None: + """Get/set info in _MCB.""" + self.message = _MCB.Counter.BusCommunicationError -class ReturnBusCommunicationErrorCountResponse(DiagnosticBase): - """ReturnBusCommunicationErrorCountResponse.""" - sub_function_code = 0x000C - - -class ReturnBusExceptionErrorCountRequest(DiagnosticBase): - """ReturnBusExceptionErrorCountRequest.""" +class PDUreturnBusExceptionErrorCount(PDUdiagnostic): + """ReturnBusExceptionErrorCount.""" sub_function_code = 0x000D - async def datastore_update( - self, context: ModbusServerContext, device_id: int - ) -> ModbusPDU: - """Update diagnostic request on the given device.""" - _ = context - count = _MCB.Counter.BusExceptionError - return ReturnBusExceptionErrorCountResponse( - message=count, dev_id=device_id, transaction_id=self.transaction_id - ) - - -class ReturnBusExceptionErrorCountResponse(DiagnosticBase): - """ReturnBusExceptionErrorCountResponse.""" - - sub_function_code = 0x000D + def sub_build_response(self) -> None: + """Get/set info in _MCB.""" + self.message = _MCB.Counter.BusExceptionError -class ReturnDeviceMessageCountRequest(DiagnosticBase): - """ReturnDeviceMessageCountRequest.""" +class PDUreturnDeviceMessageCount(PDUdiagnostic): + """ReturnDeviceMessageCount.""" sub_function_code = 0x000E - async def datastore_update( - self, context: ModbusServerContext, device_id: int - ) -> ModbusPDU: - """Update diagnostic request on the given device.""" - _ = context - count = _MCB.Counter.DeviceMessage - return ReturnDeviceMessageCountResponse( - message=count, dev_id=device_id, transaction_id=self.transaction_id - ) - - -class ReturnDeviceMessageCountResponse(DiagnosticBase): - """ReturnDeviceMessageCountResponse.""" - - sub_function_code = 0x000E + def sub_build_response(self) -> None: + """Get/set info in _MCB.""" + self.message = _MCB.Counter.DeviceMessage -class ReturnDeviceNoResponseCountRequest(DiagnosticBase): - """ReturnDeviceNoResponseCountRequest.""" +class PDUreturnDeviceNoResponseCount(PDUdiagnostic): + """ReturnDeviceNoResponseCount.""" sub_function_code = 0x000F - async def datastore_update( - self, context: ModbusServerContext, device_id: int - ) -> ModbusPDU: - """Update diagnostic request on the given device.""" - _ = context - count = _MCB.Counter.DeviceNoResponse - return ReturnDeviceNoResponseCountResponse( - message=count, dev_id=device_id, transaction_id=self.transaction_id - ) + def sub_build_response(self) -> None: + """Get/set info in _MCB.""" + self.message = _MCB.Counter.DeviceNoResponse -class ReturnDeviceNoResponseCountResponse(DiagnosticBase): - """ReturnDeviceNoResponseCountResponse.""" - - sub_function_code = 0x000F - - -class ReturnDeviceNAKCountRequest(DiagnosticBase): - """ReturnDeviceNAKCountRequest.""" +class PDUreturnDeviceNAKCount(PDUdiagnostic): + """ReturnDeviceNAKCount.""" sub_function_code = 0x0010 - async def datastore_update( - self, context: ModbusServerContext, device_id: int - ) -> ModbusPDU: - """Update diagnostic request on the given device.""" - _ = context - count = _MCB.Counter.DeviceNAK - return ReturnDeviceNAKCountResponse( - message=count, dev_id=device_id, transaction_id=self.transaction_id - ) + def sub_build_response(self) -> None: + """Get/set info in _MCB.""" + self.message = _MCB.Counter.DeviceNAK -class ReturnDeviceNAKCountResponse(DiagnosticBase): - """ReturnDeviceNAKCountResponse.""" - - sub_function_code = 0x0010 - - -class ReturnDeviceBusyCountRequest(DiagnosticBase): - """ReturnDeviceBusyCountRequest.""" +class PDUreturnDeviceBusyCount(PDUdiagnostic): + """ReturnDeviceBusyCount.""" sub_function_code = 0x0011 - async def datastore_update( - self, context: ModbusServerContext, device_id: int - ) -> ModbusPDU: - """Update diagnostic request on the given device.""" - _ = context - count = _MCB.Counter.DEVICE_BUSY - return ReturnDeviceBusyCountResponse( - message=count, dev_id=device_id, transaction_id=self.transaction_id - ) - - -class ReturnDeviceBusyCountResponse(DiagnosticBase): - """ReturnDeviceBusyCountResponse.""" - - sub_function_code = 0x0011 + def sub_build_response(self) -> None: + """Get/set info in _MCB.""" + self.message = _MCB.Counter.DEVICE_BUSY -class ReturnDeviceBusCharacterOverrunCountRequest(DiagnosticBase): - """ReturnDeviceBusCharacterOverrunCountRequest.""" +class PDUreturnDeviceBusCharacterOverrunCount(PDUdiagnostic): + """ReturnDeviceBusCharacterOverrunCount.""" sub_function_code = 0x0012 - async def datastore_update( - self, context: ModbusServerContext, device_id: int - ) -> ModbusPDU: - """Update diagnostic request on the given device.""" - _ = context - count = _MCB.Counter.BusCharacterOverrun - return ReturnDeviceBusCharacterOverrunCountResponse( - message=count, dev_id=device_id, transaction_id=self.transaction_id - ) - - -class ReturnDeviceBusCharacterOverrunCountResponse(DiagnosticBase): - """ReturnDeviceBusCharacterOverrunCountResponse.""" - - sub_function_code = 0x0012 + def sub_build_response(self) -> None: + """Get/set info in _MCB.""" + self.message = _MCB.Counter.BusCharacterOverrun -class ReturnIopOverrunCountRequest(DiagnosticBase): - """ReturnIopOverrunCountRequest.""" +class PDUreturnIopOverrunCount(PDUdiagnostic): + """ReturnIopOverrunCount.""" sub_function_code = 0x0013 - async def datastore_update( - self, context: ModbusServerContext, device_id: int - ) -> ModbusPDU: - """Update diagnostic request on the given device.""" - _ = context - count = _MCB.Counter.BusCharacterOverrun - return ReturnIopOverrunCountResponse( - message=count, dev_id=device_id, transaction_id=self.transaction_id - ) - - -class ReturnIopOverrunCountResponse(DiagnosticBase): - """ReturnIopOverrunCountResponse.""" - - sub_function_code = 0x0013 + def sub_build_response(self) -> None: + """Get/set info in _MCB.""" + self.message = _MCB.Counter.BusCharacterOverrun -class ClearOverrunCountRequest(DiagnosticBase): - """ClearOverrunCountRequest.""" +class PDUclearOverrunCount(PDUdiagnostic): + """ClearOverrunCount.""" sub_function_code = 0x0014 - async def datastore_update( - self, context: ModbusServerContext, device_id: int - ) -> ModbusPDU: - """Update diagnostic request on the given device.""" - _ = context + def sub_build_response(self) -> None: + """Get/set info in _MCB.""" _MCB.Counter.BusCharacterOverrun = 0x0000 - return ClearOverrunCountResponse( - dev_id=device_id, transaction_id=self.transaction_id - ) -class ClearOverrunCountResponse(DiagnosticBase): - """ClearOverrunCountResponse.""" - - sub_function_code = 0x0014 - - -class GetClearModbusPlusRequest(DiagnosticBase): - """GetClearModbusPlusRequest.""" +class PDUgetClearModbusPlus(PDUdiagnostic): + """GetClearModbusPlus.""" sub_function_code = 0x0015 - async def datastore_update( - self, context: ModbusServerContext, device_id: int - ) -> ModbusPDU: - """Update diagnostic request on the given device.""" - _ = context - message: int | list | None = None # the clear operation does not return info - if self.message == ModbusPlusOperation.CLEAR_STATISTICS: + @classmethod + def calculatePduSize(cls, is_request: bool, offset: int, data: bytes) -> int: + """Calculate the size of a PDU.""" + size_data = 111 if len(data) > 7 else 0 + return 8 + size_data + + def sub_build_response(self) -> None: + """Get/set info in _MCB.""" + if self.message == CLEAR_STATISTICS: _MCB.Plus.reset() - message = ModbusPlusOperation.CLEAR_STATISTICS else: - message = [self.message] - message += _MCB.Plus.x_encode() - return GetClearModbusPlusResponse( - message=message, dev_id=device_id, transaction_id=self.transaction_id - ) + packet = b"" + for piece in _MCB.Plus.encode(): + packet += struct.pack(">H", piece) + self.message = packet - def x_encode(self): + def to_bytes(self): """Encode a diagnostic response.""" - packet = struct.pack(">H", self.sub_function_code) - packet += struct.pack(">H", self.message) - return packet - - -class GetClearModbusPlusResponse(DiagnosticBase): - """GetClearModbusPlusResponse.""" - - sub_function_code = 0x0015 - - @classmethod - def x_calculateRtuFrameSize(cls, data: bytes) -> int: - """Calculate the size of the message. - - Func_code (1 byte) + Sub function code (2 byte) + Operation (2 byte) + Data (108 bytes) - """ - size_data = 111 if len(data) > 7 else 0 - return 1 + 2 + 2 + 2 + size_data - - -DecodePDU.add_pdu(DiagnosticBase, DiagnosticBase) -DecodePDU.add_sub_pdu(ReturnQueryDataRequest, ReturnQueryDataResponse) -DecodePDU.add_sub_pdu( - RestartCommunicationsOptionRequest, RestartCommunicationsOptionResponse -) -DecodePDU.add_sub_pdu(ReturnDiagnosticRegisterRequest, ReturnDiagnosticRegisterResponse) -DecodePDU.add_sub_pdu( - ChangeAsciiInputDelimiterRequest, ChangeAsciiInputDelimiterResponse -) -DecodePDU.add_sub_pdu(ForceListenOnlyModeRequest, ForceListenOnlyModeResponse) -DecodePDU.add_sub_pdu(ClearCountersRequest, ClearCountersResponse) -DecodePDU.add_sub_pdu(ReturnBusMessageCountRequest, ReturnBusMessageCountResponse) -DecodePDU.add_sub_pdu( - ReturnBusCommunicationErrorCountRequest, ReturnBusCommunicationErrorCountResponse -) -DecodePDU.add_sub_pdu( - ReturnBusExceptionErrorCountRequest, ReturnBusExceptionErrorCountResponse -) -DecodePDU.add_sub_pdu(ReturnDeviceMessageCountRequest, ReturnDeviceMessageCountResponse) -DecodePDU.add_sub_pdu( - ReturnDeviceNoResponseCountRequest, ReturnDeviceNoResponseCountResponse -) -DecodePDU.add_sub_pdu(ReturnDeviceNAKCountRequest, ReturnDeviceNAKCountResponse) -DecodePDU.add_sub_pdu(ReturnDeviceBusyCountRequest, ReturnDeviceBusyCountResponse) -DecodePDU.add_sub_pdu( - ReturnDeviceBusCharacterOverrunCountRequest, - ReturnDeviceBusCharacterOverrunCountResponse, -) -DecodePDU.add_sub_pdu(ReturnIopOverrunCountRequest, ReturnIopOverrunCountResponse) -DecodePDU.add_sub_pdu(ClearOverrunCountRequest, ClearOverrunCountResponse) -DecodePDU.add_sub_pdu(GetClearModbusPlusRequest, GetClearModbusPlusResponse) + return struct.pack(">H", self.sub_function_code) + self.message + + +DecodePDU.add_pdu(PDUdiagnostic) +DecodePDU.add_pdu(PDUreturnQueryData) +DecodePDU.add_pdu(PDUrestartCommunicationsOption) +DecodePDU.add_pdu(PDUreturnDiagnosticRegister) +DecodePDU.add_pdu(PDUchangeAsciiInputDelimiter) +DecodePDU.add_pdu(PDUforceListenOnlyMode) +DecodePDU.add_pdu(PDUclearCounters) +DecodePDU.add_pdu(PDUreturnBusMessageCount) +DecodePDU.add_pdu(PDUreturnBusCommunicationErrorCount) +DecodePDU.add_pdu(PDUreturnBusExceptionErrorCount) +DecodePDU.add_pdu(PDUreturnDeviceMessageCount) +DecodePDU.add_pdu(PDUreturnDeviceNoResponseCount) +DecodePDU.add_pdu(PDUreturnDeviceNAKCount) +DecodePDU.add_pdu(PDUreturnDeviceBusyCount) +DecodePDU.add_pdu(PDUreturnDeviceBusCharacterOverrunCount) +DecodePDU.add_pdu(PDUreturnIopOverrunCount) +DecodePDU.add_pdu(PDUclearOverrunCount) +DecodePDU.add_pdu(PDUgetClearModbusPlus) diff --git a/pymodbus/pdu/events.py b/pymodbus/pdu/events.py index c402463055..1f9d3a8f5f 100644 --- a/pymodbus/pdu/events.py +++ b/pymodbus/pdu/events.py @@ -16,11 +16,11 @@ class ModbusEvent(ABC): """Define modbus events.""" @abstractmethod - def x_encode(self) -> bytes: + def encode(self) -> bytes: """Encode the status bits to an event message.""" @abstractmethod - def x_decode(self, event): + def decode(self, event): """Decode the event message to its status bits. :param event: The event to decode @@ -53,7 +53,7 @@ def __init__(self, overrun=False, listen=False, broadcast=False): self.listen = listen self.broadcast = broadcast - def x_encode(self) -> bytes: + def encode(self) -> bytes: """Encode the status bits to an event message. :returns: The encoded event message @@ -63,7 +63,7 @@ def x_encode(self) -> bytes: packet = pack_bitstring(bits) return packet - def x_decode(self, event: bytes) -> None: + def decode(self, event: bytes) -> None: """Decode the event message to its status bits. :param event: The event to decode @@ -114,7 +114,7 @@ def __init__( self.write_timeout = write_timeout self.listen = listen - def x_encode(self): + def encode(self): """Encode the status bits to an event message. :returns: The encoded event message @@ -131,7 +131,7 @@ def x_encode(self): packet = pack_bitstring(bits) return packet - def x_decode(self, event): + def decode(self, event): """Decode the event message to its status bits. :param event: The event to decode @@ -156,14 +156,14 @@ class EnteredListenModeEvent(ModbusEvent): value = 0x04 __encoded = b"\x04" - def x_encode(self): + def encode(self): """Encode the status bits to an event message. :returns: The encoded event message """ return self.__encoded - def x_decode(self, event): + def decode(self, event): """Decode the event message to its status bits. :param event: The event to decode @@ -193,14 +193,14 @@ class CommunicationRestartEvent(ModbusEvent): value = 0x00 __encoded = b"\x00" - def x_encode(self): + def encode(self): """Encode the status bits to an event message. :returns: The encoded event message """ return self.__encoded - def x_decode(self, event): + def decode(self, event): """Decode the event message to its status bits. :param event: The event to decode diff --git a/pymodbus/pdu/exceptionresponse.py b/pymodbus/pdu/exceptionresponse.py deleted file mode 100644 index 8374543145..0000000000 --- a/pymodbus/pdu/exceptionresponse.py +++ /dev/null @@ -1,56 +0,0 @@ -"""Contains exceptionResponse class for modbus.""" - -from __future__ import annotations - -import struct - -from .pdu import ModbusPDU - - -class ExceptionResponse(ModbusPDU): - """Base class for a modbus exception PDU.""" - - @classmethod - def x_calculateRtuFrameSize(cls, data: bytes) -> int: - """Calculate the size of a PDU.""" - return 5 - - def __init__( - self, - function_code: int, - exception_code: int = 0, - device_id: int = 1, - transaction: int = 0, - ) -> None: - """Initialize the modbus exception response.""" - super().__init__(transaction_id=transaction, dev_id=device_id) - self.function_code = function_code | 0x80 - self.exception_code = exception_code - - def __str__(self) -> str: - """Build a representation of an exception response.""" - return ( - f"{self.__class__.__name__}(" - f"dev_id={self.dev_id}, " - f"function_code={self.function_code}, " - f"exception_code={self.exception_code})" - ) - - def x_encode(self) -> bytes: - """Encode a modbus exception response.""" - return struct.pack(">B", self.exception_code) - - def x_decode(self, data: bytes) -> None: - """Decode data part of the message.""" - _ = data - - def isError(self) -> bool: - """Check if the error is a success or failure.""" - return True - - def from_frame(self, is_request: bool, data: bytes) -> int: - """Decode a modbus exception response.""" - _ = is_request - self.function_code = int(data[0]) - self.exception_code = int(data[1]) - return 5 diff --git a/pymodbus/pdu/file_message.py b/pymodbus/pdu/file_message.py index fa2fa26893..cefe8347d4 100644 --- a/pymodbus/pdu/file_message.py +++ b/pymodbus/pdu/file_message.py @@ -3,12 +3,11 @@ from __future__ import annotations import struct +from copy import deepcopy from dataclasses import dataclass -from ..datastore import ModbusServerContext from ..exceptions import ModbusException, ModbusIOException -from .decoders import DecodePDU -from .pdu import ModbusPDU +from .pdu import DecodePDU, ModbusPdu, StoreGet, StoreSet @dataclass @@ -31,139 +30,126 @@ def __post_init__(self) -> None: self.record_length //= 2 -class ReadFileRecordRequest(ModbusPDU): - """ReadFileRecordRequest.""" +class PDUreadFileRecord(ModbusPdu): + """ReadFileRecord.""" function_code = 0x14 - @classmethod - def x_calculateRtuFrameSize(cls, data: bytes) -> int: - """Calculate the size of a PDU.""" - return int(data[2]) + 2 + 3 - def __init__( self, + *args, records: list[FileRecord] | None = None, - dev_id: int = 1, - transaction_id: int = 0, + **kwargs, ) -> None: - """Initialize a new instance.""" - super().__init__(transaction_id=transaction_id, dev_id=dev_id) - self.records: list[FileRecord] = records or [] + """Initialize.""" + super().__init__(*args, **kwargs) + self.records: list[FileRecord] = [] + if records: + self.records = deepcopy(records) - def x_encode(self) -> bytes: - """Encode the request packet.""" - packet = struct.pack("B", len(self.records) * 7) - for record in self.records: - packet += struct.pack( - ">BHHH", - 0x06, - record.file_number, - record.record_number, - record.record_length, - ) - return packet + def __str__(self) -> str: + """Build representation of pdu.""" + return super().__str__() + f", records={self.records}" - def x_decode(self, data: bytes) -> None: - """Decode the incoming request.""" - self.records = [] - if (byte_count := int(data[0])) > len(data) - 1: - raise ModbusIOException( - f"Invalid byte count: {byte_count}", function_code=self.function_code - ) - for count in range(1, byte_count, 7): - decoded = struct.unpack(">BHHH", data[count : count + 7]) - record = FileRecord( - file_number=decoded[1], - record_number=decoded[2], - record_length=decoded[3], - ) - self.records.append(record) + @classmethod + def calculatePduSize(cls, is_request: bool, offset: int, data: bytes) -> int: + """Calculate the size of a PDU.""" + return int(data[offset + 2]) + 2 + 3 + + def to_bytes(self) -> bytes: + """Encode the message.""" + if self.is_request: + packet = struct.pack("B", len(self.records) * 7) + for record in self.records: + packet += struct.pack( + ">BHHH", + 0x06, + record.file_number, + record.record_number, + record.record_length, + ) + else: + total = sum(len(record.record_data) + 1 + 1 for record in self.records) + packet = struct.pack("B", total) + for record in self.records: + packet += struct.pack(">BB", len(record.record_data) + 1, 0x06) + packet += record.record_data + return packet - async def datastore_update( - self, context: ModbusServerContext, device_id: int - ) -> ModbusPDU: - """Update diagnostic request on the given device.""" - _ = context + def from_bytes(self, offset: int, data: bytes) -> None: + """Decode payload.""" + if self.is_request: + self.records = [] + if (byte_count := int(data[offset + 0])) > len(data) - 1 - offset: + raise ModbusIOException(f"Invalid byte count: {byte_count}") + for count in range(1, byte_count, 7): + decoded = struct.unpack( + ">BHHH", data[count + offset : count + 7 + offset] + ) + record = FileRecord( + file_number=decoded[1], + record_number=decoded[2], + record_length=decoded[3], + ) + self.records.append(record) + else: + count = 1 + self.records.clear() + byte_count = int(data[offset + 0]) + while count < byte_count: + calc_length, _ = struct.unpack( + ">BB", data[count + offset : count + 2 + offset] + ) + count += 2 + + record_length = ( + calc_length - 1 + ) # response length includes the type byte + record = FileRecord( + record_data=data[count + offset : count + record_length + offset], + ) + count += record_length + self.records.append(record) + + async def build_response( + self, get_values: StoreGet, set_values: StoreSet + ) -> ModbusPdu: + """Get bits from datastore.""" for record in self.records: record.record_data = b"SERVER DUMMY RECORD." record.record_length = len(record.record_data) // 2 - return ReadFileRecordResponse( - records=self.records, dev_id=device_id, transaction_id=self.transaction_id - ) + self.is_request = False + return self -class ReadFileRecordResponse(ModbusPDU): - """ReadFileRecordResponse.""" - - function_code = 0x14 +class PDUwriteFileRecord(ModbusPdu): + """WriteFileRecord.""" - @classmethod - def x_calculateRtuFrameSize(cls, data: bytes) -> int: - """Calculate the size of a PDU.""" - return int(data[2]) + 2 + 3 + function_code = 0x15 def __init__( self, + *args, records: list[FileRecord] | None = None, - dev_id: int = 1, - transaction_id: int = 0, + **kwargs, ) -> None: - """Initialize a new instance.""" - super().__init__(transaction_id=transaction_id, dev_id=dev_id) + """Initialize.""" + super().__init__(*args, **kwargs) self.records: list[FileRecord] = records or [] - def x_encode(self) -> bytes: - """Encode the response.""" - total = sum(len(record.record_data) + 1 + 1 for record in self.records) - packet = struct.pack("B", total) - for record in self.records: - packet += struct.pack(">BB", len(record.record_data) + 1, 0x06) - packet += record.record_data - return packet - - def x_decode(self, data: bytes) -> None: - """Decode the response.""" - count = 1 - self.records.clear() - byte_count = int(data[0]) - while count < byte_count: - calc_length, _ = struct.unpack(">BB", data[count : count + 2]) - count += 2 - - record_length = calc_length - 1 # response length includes the type byte - record = FileRecord( - record_data=data[count : count + record_length], - ) - count += record_length - self.records.append(record) - - -class WriteFileRecordRequest(ModbusPDU): - """WriteFileRecordRequest.""" - - function_code = 0x15 + def __str__(self) -> str: + """Build representation of pdu.""" + return super().__str__() + f", records={self.records}" @classmethod - def x_calculateRtuFrameSize(cls, data: bytes) -> int: + def calculatePduSize(cls, is_request: bool, offset: int, data: bytes) -> int: """Calculate the size of a PDU.""" - return int(data[2]) + 2 + 3 - - def __init__( - self, - records: list[FileRecord] | None = None, - dev_id: int = 1, - transaction_id: int = 0, - ) -> None: - """Initialize a new instance.""" - super().__init__(transaction_id=transaction_id, dev_id=dev_id) - self.records: list[FileRecord] = records or [] + return int(data[offset + 2]) + 2 + 3 - def x_encode(self) -> bytes: - """Encode the request packet.""" + def to_bytes(self) -> bytes: + """Encode the message.""" total_length = sum((record.record_length * 2) + 7 for record in self.records) packet = struct.pack("B", total_length) - for record in self.records: packet += struct.pack( ">BHHH", @@ -175,16 +161,14 @@ def x_encode(self) -> bytes: packet += record.record_data return packet - def x_decode(self, data: bytes) -> None: - """Decode the incoming request.""" - if (byte_count := int(data[0])) > len(data) - 1: - raise ModbusIOException( - f"Invalid byte count: {byte_count}", function_code=self.function_code - ) + def from_bytes(self, offset: int, data: bytes) -> None: + """Decode payload.""" count = 1 self.records.clear() + if (byte_count := int(data[offset + 0])) > len(data) - 1 - offset: + raise ModbusIOException(f"Invalid byte count: {byte_count}") while count < byte_count: - decoded = struct.unpack(">BHHH", data[count : count + 7]) + decoded = struct.unpack(">BHHH", data[count + offset : count + 7 + offset]) calc_length = decoded[3] * 2 count += calc_length + 7 record = FileRecord( @@ -195,146 +179,76 @@ def x_decode(self, data: bytes) -> None: record.record_length = decoded[3] self.records.append(record) - async def datastore_update( - self, context: ModbusServerContext, device_id: int - ) -> ModbusPDU: - """Update diagnostic request on the given device.""" - _ = context - return WriteFileRecordResponse( - records=self.records, dev_id=device_id, transaction_id=self.transaction_id - ) + async def build_response( + self, get_values: StoreGet, set_values: StoreSet + ) -> ModbusPdu: + """Get bits from datastore.""" + self.is_request = False + return self -class WriteFileRecordResponse(ModbusPDU): - """The normal response is an echo of the request.""" +class PDUreadFifoQueue(ModbusPdu): + """ReadFifoQueue.""" - function_code = 0x15 - - @classmethod - def x_calculateRtuFrameSize(cls, data: bytes) -> int: - """Calculate the size of a PDU.""" - return int(data[2]) + 2 + 3 + function_code = 0x18 def __init__( self, - records: list[FileRecord] | None = None, - dev_id: int = 1, - transaction_id: int = 0, + *args, + values: list[int] | None = None, + **kwargs, ) -> None: - """Initialize a new instance.""" - super().__init__(transaction_id=transaction_id, dev_id=dev_id) - self.records: list[FileRecord] = records or [] + """Initialize.""" + super().__init__(*args, **kwargs) + self.values: list[int] = values or [] - def x_encode(self) -> bytes: - """Encode the response.""" - total_length = sum((record.record_length * 2) + 7 for record in self.records) - packet = struct.pack("B", total_length) - for record in self.records: - packet += struct.pack( - ">BHHH", - 0x06, - record.file_number, - record.record_number, - record.record_length, - ) - packet += record.record_data - return packet - - def x_decode(self, data: bytes) -> None: - """Decode the incoming request.""" - count = 1 - self.records.clear() - byte_count = int(data[0]) - while count < byte_count: - decoded = struct.unpack(">BHHH", data[count : count + 7]) - calc_length = decoded[3] * 2 - count += calc_length + 7 - record = FileRecord( - file_number=decoded[1], - record_number=decoded[2], - record_data=data[count - calc_length : count], - ) - record.record_length = decoded[3] - self.records.append(record) - - -class ReadFifoQueueRequest(ModbusPDU): - """ReadFifoQueueRequest.""" - - function_code = 0x18 + def __str__(self) -> str: + """Build representation of pdu.""" + return super().__str__() + f", values={self.values}" @classmethod - def x_calculateRtuFrameSize(cls, data: bytes) -> int: + def calculatePduSize(cls, is_request: bool, offset: int, data: bytes) -> int: """Calculate the size of a PDU.""" - return 6 - - def __init__( - self, address: int = 0, dev_id: int = 1, transaction_id: int = 0 - ) -> None: - """Initialize a new instance.""" - super().__init__(transaction_id=transaction_id, dev_id=dev_id) - self.address = address - self.verifyAddress() - - def x_encode(self) -> bytes: - """Encode the request packet.""" - return struct.pack(">H", self.address) - - def x_decode(self, data: bytes) -> None: - """Decode the incoming request.""" - self.address = struct.unpack(">H", data[:2])[0] - - async def datastore_update( - self, context: ModbusServerContext, device_id: int - ) -> ModbusPDU: - """Update diagnostic request on the given device.""" - _ = context - values = [0, 1, 2, 3] # server dummy response (should be in datastore) - return ReadFifoQueueResponse( - values=values, dev_id=device_id, transaction_id=self.transaction_id - ) - - -class ReadFifoQueueResponse(ModbusPDU): - """ReadFifoQueueResponse.""" - - function_code = 0x18 - - @classmethod - def x_calculateRtuFrameSize(cls, data: bytes) -> int: - """Calculate the size of the message.""" - hi_byte = int(data[2]) - lo_byte = int(data[3]) - return ((hi_byte << 8) + lo_byte) + 4 - - def __init__( - self, values: list[int] | None = None, dev_id: int = 1, transaction_id: int = 0 - ) -> None: - """Initialize a new instance.""" - super().__init__(transaction_id=transaction_id, dev_id=dev_id) - self.values = values or [] - - def x_encode(self) -> bytes: - """Encode the response.""" + if is_request: + return 6 + hi_byte = int(data[offset + 2]) + lo_byte = int(data[offset + 3]) + return ((hi_byte << 8) + lo_byte) + 6 + + def to_bytes(self) -> bytes: + """Encode the message.""" + if self.is_request: + return struct.pack(">H", self.address) length = len(self.values) packet = struct.pack(">HH", 2 + length * 2, length) for value in self.values: packet += struct.pack(">H", value) return packet - def x_decode(self, data: bytes) -> None: - """Decode a the response.""" + def from_bytes(self, offset: int, data: bytes) -> None: + """Decode payload.""" + if self.is_request: + self.address = struct.unpack(">H", data[offset : offset + 2])[0] + return self.values = [] - _, count = struct.unpack(">HH", data[0:4]) - if 4 + count * 2 > len(data): - raise ModbusIOException( - f"Invalid fifo count: {count}", function_code=self.function_code - ) + _, count = struct.unpack(">HH", data[offset + 0 : offset + 4]) + if 4 + count * 2 > len(data) - offset: + raise ModbusIOException(f"Invalid fifo count: {count}") for index in range(0, count): idx = 4 + index * 2 - self.values.append(struct.unpack(">H", data[idx : idx + 2])[0]) + self.values.append( + struct.unpack(">H", data[offset + idx : offset + idx + 2])[0] + ) + + async def build_response( + self, get_values: StoreGet, set_values: StoreSet + ) -> ModbusPdu: + """Get bits from datastore.""" + self.is_request = False + self.values = [0, 1, 2, 3] # server dummy response (should be in datastore) + return self -DecodePDU.add_pdu(ReadFileRecordRequest, ReadFileRecordResponse) -DecodePDU.add_pdu(WriteFileRecordRequest, WriteFileRecordResponse) -DecodePDU.add_pdu(ReadFifoQueueRequest, ReadFifoQueueResponse) +DecodePDU.add_pdu(PDUreadFileRecord) +DecodePDU.add_pdu(PDUwriteFileRecord) +DecodePDU.add_pdu(PDUreadFifoQueue) diff --git a/pymodbus/pdu/mei_message.py b/pymodbus/pdu/mei_message.py index fed229f324..6d47627089 100644 --- a/pymodbus/pdu/mei_message.py +++ b/pymodbus/pdu/mei_message.py @@ -3,15 +3,11 @@ from __future__ import annotations import struct -from abc import abstractmethod from typing import Any from ..constants import DeviceInformation, ExcCodes, MoreData -from ..datastore import ModbusServerContext -from .decoders import DecodePDU from .device import DeviceInformationFactory, ModbusControlBlock -from .exceptionresponse import ExceptionResponse -from .pdu import ModbusPDU +from .pdu import DecodePDU, ModbusPdu, PDUexception, StoreGet, StoreSet _MCB = ModbusControlBlock() @@ -32,123 +28,79 @@ def __init__(self, oid: int) -> None: self.oid = oid -class EncapsulatedInterfaceTransportBase(ModbusPDU): +class PDUreadDeviceInformation(ModbusPdu): """Base PDU for dispatching Encapsulated Interface Transport messages.""" function_code = 0x2B - - @classmethod - def decode_sub_function_code(cls, data: bytes) -> int: - """Decode the MEI type.""" - return int(data[2]) - - @abstractmethod - def x_encode(self) -> bytes: - """Encode the request packet.""" - - -class ReadDeviceInformationRequest(EncapsulatedInterfaceTransportBase): - """ReadDeviceInformationRequest.""" - sub_function_code = 0x0E - @classmethod - def x_calculateRtuFrameSize(cls, data: bytes) -> int: - """Calculate the size of a PDU.""" - return 7 - def __init__( self, + *args, read_code: int | None = None, object_id: int = 0, - dev_id: int = 1, - transaction_id: int = 0, + information: dict[int, Any] | None = None, + **kwargs, ) -> None: - """Initialize a new instance.""" - super().__init__(transaction_id=transaction_id, dev_id=dev_id) - self.read_code = read_code or DeviceInformation.BASIC + """Initialize.""" + super().__init__(*args, **kwargs) + self.read_code = read_code or int(DeviceInformation.BASIC) self.object_id = object_id + self.information: dict[int, Any] = information or {} + self.number_of_objects = 0 + self.conformity = 0x83 # I support everything right now + self.next_object_id = 0x00 + self.more_follows = MoreData.NOTHING + self.space_left = 253 - 6 - def x_encode(self) -> bytes: - """Encode the request packet.""" - packet = struct.pack( - ">BBB", self.sub_function_code, self.read_code, self.object_id - ) - return packet - - def x_decode(self, data: bytes) -> None: - """Decode data part of the message.""" - self.sub_function_code, self.read_code, self.object_id = struct.unpack( - ">BBB", data[:3] - ) - - async def datastore_update( - self, context: ModbusServerContext, device_id: int - ) -> ModbusPDU: - """Update diagnostic request on the given device.""" - _ = context - if not 0x00 <= self.object_id <= 0xFF: - return ExceptionResponse(self.function_code, ExcCodes.ILLEGAL_VALUE) - if not 0x00 <= self.read_code <= 0x04: - return ExceptionResponse(self.function_code, ExcCodes.ILLEGAL_VALUE) - - information = DeviceInformationFactory.get(_MCB, self.read_code, self.object_id) - if self.read_code == DeviceInformation.SPECIFIC and ( - 0x07 <= self.object_id < 0x80 or not information.get(self.object_id) - ): - return ExceptionResponse(self.function_code, ExcCodes.ILLEGAL_ADDRESS) - return ReadDeviceInformationResponse( - read_code=self.read_code, - information=information, - dev_id=device_id, - transaction_id=self.transaction_id, + def __str__(self) -> str: + """Build representation of pdu.""" + return super().__str__() + ( + ", " + + f"read_code={self.read_code}, " + + f"object_id={self.object_id}, " + + f"information={self.information}, " + + f"number_of_objects={self.number_of_objects}, " + + f"conformity={self.conformity}, " + + f"next_object_id={self.next_object_id}, " + + f"more_follows={self.more_follows}, " + + f"space_left={self.space_left}, " ) - -class ReadDeviceInformationResponse(EncapsulatedInterfaceTransportBase): - """ReadDeviceInformationResponse.""" - - sub_function_code = 0x0E - @classmethod - def x_calculateRtuFrameSize(cls, data: bytes) -> int: - """Calculate the size of the message.""" + def calculatePduSize(cls, is_request: bool, offset: int, data: bytes) -> int: + """Calculate the size of a PDU.""" + data_len = len(data) - offset + if is_request: + if data_len != 5: + return 999 + return 5 + 2 size = 8 # skip the header information - if (data_len := len(data)) < size: + if data_len < size: return 999 - count = int(data[7]) + count = int(data[offset + 7]) while count > 0: if data_len < size + 2: return 998 - _, object_length = struct.unpack(">BB", data[size : size + 2]) + _, object_length = struct.unpack( + ">BB", data[offset + size : offset + size + 2] + ) size += object_length + 2 count -= 1 return size + 2 - def __init__( - self, - read_code: int | None = None, - information: dict[int, Any] | None = None, - dev_id: int = 1, - transaction_id: int = 0, - ) -> None: - """Initialize a new instance.""" - super().__init__(transaction_id=transaction_id, dev_id=dev_id) - self.read_code = read_code or DeviceInformation.BASIC - self.information: dict[int, Any] = information or {} - self.number_of_objects = 0 - self.conformity = 0x83 # I support everything right now - self.next_object_id = 0x00 - self.more_follows = MoreData.NOTHING - self.space_left = 253 - 6 + @classmethod + def decode_sub_function_code(cls, offset: int, data: bytes) -> int: + """Decode sub function code.""" + return int.from_bytes(data[offset + 2 : offset + 3]) - def _encode_object(self, object_id: int, data: str | bytes | ModbusPDU) -> bytes: + def _encode_object(self, object_id: int, data: str | bytes | ModbusPdu) -> bytes: """Encode object.""" if isinstance(data, str): data = data.encode() elif not isinstance(data, bytes): - data = data.x_encode() + data = data.to_bytes() data_len = len(data) self.space_left -= 2 + data_len if self.space_left <= 0: @@ -158,8 +110,13 @@ def _encode_object(self, object_id: int, data: str | bytes | ModbusPDU) -> bytes self.number_of_objects += 1 return encoded_obj - def x_encode(self) -> bytes: - """Encode the response.""" + def to_bytes(self) -> bytes: + """Encode the message.""" + if self.is_request: + packet = struct.pack( + ">BBB", self.sub_function_code, self.read_code, self.object_id + ) + return packet packet = struct.pack( ">BBB", self.sub_function_code, self.read_code, self.conformity ) @@ -181,30 +138,63 @@ def x_encode(self) -> bytes: packet += objects return packet - def x_decode(self, data: bytes) -> None: - """Decode a the response.""" - params = struct.unpack(">BBBBBB", data[0:6]) - self.sub_function_code, self.read_code = params[0:2] - self.conformity, self.more_follows = params[2:4] - self.next_object_id, self.number_of_objects = params[4:6] + def from_bytes(self, offset: int, data: bytes) -> None: + """Decode payload.""" + if self.is_request: + self.sub_function_code, self.read_code, self.object_id = struct.unpack( + ">BBB", data[offset : offset + 3] + ) + return + ( + self.sub_function_code, + self.read_code, + self.conformity, + self.more_follows, + self.next_object_id, + self.number_of_objects, + ) = struct.unpack(">BBBBBB", data[offset + 0 : offset + 6]) count = 6 # skip the header information self.information.clear() - while count < len(data): - object_id, object_length = struct.unpack(">BB", data[count : count + 2]) + while count < len(data) - offset: + object_id, object_length = struct.unpack( + ">BB", data[offset + count : offset + count + 2] + ) count += object_length + 2 if object_id not in self.information: - self.information[object_id] = data[count - object_length : count] + self.information[object_id] = data[ + offset + count - object_length : offset + count + ] elif isinstance(self.information[object_id], list): - self.information[object_id].append(data[count - object_length : count]) + self.information[object_id].append( + data[offset + count - object_length : offset + count] + ) else: self.information[object_id] = [ self.information[object_id], data[count - object_length : count], ] + async def build_response( + self, get_values: StoreGet, set_values: StoreSet + ) -> ModbusPdu: + """Update diagnostic request on the given device.""" + if not 0x00 <= self.object_id <= 0xFF: + return PDUexception(self.dev_id, self.function_code, ExcCodes.ILLEGAL_VALUE) + if not 0x00 <= self.read_code <= 0x04: + return PDUexception(self.dev_id, self.function_code, ExcCodes.ILLEGAL_VALUE) + + self.information = DeviceInformationFactory.get( + _MCB, self.read_code, self.object_id + ) + if self.read_code == DeviceInformation.SPECIFIC and ( + 0x07 <= self.object_id < 0x80 or not self.information.get(self.object_id) + ): + return PDUexception( + self.dev_id, self.function_code, ExcCodes.ILLEGAL_ADDRESS + ) + self.is_request = False + return self + -DecodePDU.add_pdu( - EncapsulatedInterfaceTransportBase, EncapsulatedInterfaceTransportBase -) -DecodePDU.add_sub_pdu(ReadDeviceInformationRequest, ReadDeviceInformationResponse) +DecodePDU.add_pdu(PDUreadDeviceInformation) diff --git a/pymodbus/pdu/other_message.py b/pymodbus/pdu/other_message.py index 3a7d649128..66ebfa02ec 100644 --- a/pymodbus/pdu/other_message.py +++ b/pymodbus/pdu/other_message.py @@ -5,263 +5,208 @@ import struct from ..constants import ModbusStatus -from ..datastore import ModbusServerContext from ..exceptions import ModbusIOException -from .decoders import DecodePDU from .device import DeviceInformationFactory, ModbusControlBlock -from .pdu import ModbusPDU +from .pdu import DecodePDU, ModbusPdu, StoreGet, StoreSet _MCB = ModbusControlBlock() -class ReadExceptionStatusRequest(ModbusPDU): - """ReadExceptionStatusRequest.""" +class PDUreadExceptionStatus(ModbusPdu): + """ReadExceptionStatus.""" function_code = 0x07 - @classmethod - def x_calculateRtuFrameSize(cls, data: bytes) -> int: - """Calculate the size of a PDU.""" - return 4 - - def x_encode(self) -> bytes: - """Encode the message.""" - return b"" - - def x_decode(self, data: bytes) -> None: - """Decode data part of the message.""" - - async def datastore_update( - self, context: ModbusServerContext, device_id: int - ) -> ModbusPDU: - """Update diagnostic request on the given device.""" - _ = context - status = _MCB.Counter.summary() - return ReadExceptionStatusResponse( - status=status, dev_id=device_id, transaction_id=self.transaction_id - ) - - -class ReadExceptionStatusResponse(ModbusPDU): - """ReadExceptionStatusResponse.""" + def __init__( + self, + *args, + status: int = 0, + **kwargs, + ) -> None: + """Initialize.""" + super().__init__(*args, **kwargs) + self.status: int = status or 0 - function_code = 0x07 + def __str__(self) -> str: + """Build representation of pdu.""" + return super().__str__() + f", status={self.status}" @classmethod - def x_calculateRtuFrameSize(cls, data: bytes) -> int: + def calculatePduSize(cls, is_request: bool, offset: int, data: bytes) -> int: """Calculate the size of a PDU.""" - return 5 + return 4 if is_request else 5 - def x_encode(self) -> bytes: - """Encode the response.""" - return struct.pack(">B", self.status) - - def x_decode(self, data: bytes) -> None: - """Decode a the response.""" - self.status = int(data[0]) + def to_bytes(self) -> bytes: + """Encode the message.""" + return b"" if self.is_request else struct.pack(">B", self.status) + def from_bytes(self, offset: int, data: bytes) -> None: + """Decode payload.""" + if not self.is_request: + self.status = int(data[offset + 0]) -# Encapsulate interface transport 43, 14 -# CANopen general reference 43, 13 + async def build_response( + self, get_values: StoreGet, set_values: StoreSet + ) -> ModbusPdu: + """Update internal data.""" + self.status = _MCB.Counter.summary() + self.is_request = False + return self -class GetCommEventCounterRequest(ModbusPDU): - """GetCommEventCounterRequest.""" +class PDUgetCommEventCounter(ModbusPdu): + """GetCommEventCounter.""" function_code = 0x0B - @classmethod - def x_calculateRtuFrameSize(cls, data: bytes) -> int: - """Calculate the size of a PDU.""" - return 4 - - def x_encode(self) -> bytes: - """Encode the message.""" - return b"" - - def x_decode(self, data: bytes) -> None: - """Decode data part of the message.""" - - async def datastore_update( - self, context: ModbusServerContext, device_id: int - ) -> ModbusPDU: - """Update diagnostic request on the given device.""" - _ = context - count = _MCB.Counter.Event - return GetCommEventCounterResponse( - count=count, dev_id=device_id, transaction_id=self.transaction_id - ) - - -class GetCommEventCounterResponse(ModbusPDU): - """GetCommEventCounterRequest.""" + def __init__( + self, + *args, + status: int = 0, + **kwargs, + ) -> None: + """Initialize.""" + super().__init__(*args, **kwargs) + self.status: int = status or True - function_code = 0x0B + def __str__(self) -> str: + """Build representation of pdu.""" + return super().__str__() + f", status={self.status}" @classmethod - def x_calculateRtuFrameSize(cls, data: bytes) -> int: + def calculatePduSize(cls, is_request: bool, offset: int, data: bytes) -> int: """Calculate the size of a PDU.""" - return 8 + return 4 if is_request else 8 - def x_encode(self) -> bytes: - """Encode the response.""" + def to_bytes(self) -> bytes: + """Encode the message.""" + if self.is_request: + return b"" ready = ModbusStatus.READY if self.status else ModbusStatus.WAITING return struct.pack(">HH", ready, self.count) - def x_decode(self, data: bytes) -> None: - """Decode a the response.""" - ready, self.count = struct.unpack(">HH", data[:4]) - self.status = ready == ModbusStatus.READY - + def from_bytes(self, offset: int, data: bytes) -> None: + """Decode payload.""" + if not self.is_request: + ready, self.count = struct.unpack(">HH", data[offset : offset + 4]) + self.status = ready == ModbusStatus.READY -class GetCommEventLogRequest(ModbusPDU): - """GetCommEventLogRequest.""" + async def build_response( + self, get_values: StoreGet, set_values: StoreSet + ) -> ModbusPdu: + """Update internal data.""" + self.count = _MCB.Counter.Event + self.is_request = False + return self - function_code = 0x0C - @classmethod - def x_calculateRtuFrameSize(cls, data: bytes) -> int: - """Calculate the size of a PDU.""" - return 4 - - def x_encode(self) -> bytes: - """Encode the message.""" - return b"" - - def x_decode(self, data: bytes) -> None: - """Decode data part of the message.""" - - async def datastore_update( - self, context: ModbusServerContext, device_id: int - ) -> ModbusPDU: - """Update diagnostic request on the given device.""" - _ = context - return GetCommEventLogResponse( - status=True, - message_count=_MCB.Counter.BusMessage, - event_count=_MCB.Counter.Event, - events=list(_MCB.getEvents()), - dev_id=device_id, - transaction_id=self.transaction_id, - ) - - -class GetCommEventLogResponse(ModbusPDU): - """GetCommEventLogRequest.""" +class PDUgetCommEventLog(ModbusPdu): + """GetCommEventLog.""" function_code = 0x0C - @classmethod - def x_calculateRtuFrameSize(cls, data: bytes) -> int: - """Calculate the size of a PDU.""" - return int(data[2]) + 2 + 3 - def __init__( self, + *args, status: bool = True, message_count: int = 0, event_count: int = 0, events: list[int] | None = None, - dev_id: int = 1, - transaction_id: int = 0, + **kwargs, ) -> None: - """Initialize a new instance.""" - super().__init__(transaction_id=transaction_id, dev_id=dev_id, status=status) + """Initialize.""" + super().__init__(*args, **kwargs) + self.status = status self.message_count = message_count self.event_count = event_count self.events = events or [] - def x_encode(self) -> bytes: - """Encode the response.""" - if self.status: - ready = ModbusStatus.READY - else: - ready = ModbusStatus.WAITING + def __str__(self) -> str: + """Build representation of pdu.""" + return ( + super().__str__() + + f", status={self.status}, message_count={self.message_count}" + + f", event_count={self.event_count}, events={self.events}" + ) + + @classmethod + def calculatePduSize(cls, is_request: bool, offset: int, data: bytes) -> int: + """Calculate the size of a PDU.""" + return 4 if is_request else int(data[offset + 2]) + 2 + 3 + + def to_bytes(self) -> bytes: + """Encode the message.""" + if self.is_request: + return b"" + ready = ModbusStatus.READY if self.status else ModbusStatus.WAITING packet = struct.pack(">B", 6 + len(self.events)) packet += struct.pack(">H", ready) packet += struct.pack(">HH", self.event_count, self.message_count) packet += b"".join(struct.pack(">B", e) for e in self.events) return packet - def x_decode(self, data: bytes) -> None: - """Decode a the response.""" - length = int(data[0]) - status = struct.unpack(">H", data[1:3])[0] + def from_bytes(self, offset: int, data: bytes) -> None: + """Decode payload.""" + if self.is_request: + return + length = int(data[offset]) + status = struct.unpack(">H", data[offset + 1 : offset + 3])[0] self.status = status == ModbusStatus.READY - self.event_count = struct.unpack(">H", data[3:5])[0] - self.message_count = struct.unpack(">H", data[5:7])[0] + self.event_count = struct.unpack(">H", data[offset + 3 : offset + 5])[0] + self.message_count = struct.unpack(">H", data[offset + 5 : offset + 7])[0] self.events = [] for i in range(7, length + 1): - self.events.append(int(data[i])) - - -class ReportDeviceIdRequest(ModbusPDU): - """ReportDeviceIdRequest.""" - - function_code = 0x11 - - @classmethod - def x_calculateRtuFrameSize(cls, data: bytes) -> int: - """Calculate the size of a PDU.""" - return 4 - - def x_encode(self) -> bytes: - """Encode the message.""" - return b"" + self.events.append(int(data[offset + i])) - def x_decode(self, data: bytes) -> None: - """Decode data part of the message.""" - - async def datastore_update( - self, context: ModbusServerContext, device_id: int - ) -> ModbusPDU: - """Update diagnostic request on the given device.""" - _ = context - information = DeviceInformationFactory.get(_MCB) - id_data = [] - for v_item in information.values(): - if isinstance(v_item, bytes): - id_data.append(v_item) - else: - id_data.append(v_item.encode()) - - identifier = b"-".join(id_data) - identifier = identifier or b"Pymodbus" - return ReportDeviceIdResponse( - identifier=identifier, dev_id=device_id, transaction_id=self.transaction_id - ) + async def build_response( + self, get_values: StoreGet, set_values: StoreSet + ) -> ModbusPdu: + """Update internal data.""" + self.status = True + self.message_count = _MCB.Counter.BusMessage + self.event_count = _MCB.Counter.Event + self.events = list(_MCB.getEvents()) + return self ID_ON = 0xFF ID_OFF = 0x00 -class ReportDeviceIdResponse(ModbusPDU): - """ReportDeviceIdRequeste.""" +class PDUreportDeviceId(ModbusPdu): + """ReportDeviceId.""" function_code = 0x11 - @classmethod - def x_calculateRtuFrameSize(cls, data: bytes) -> int: - """Calculate the size of a PDU.""" - return int(data[2]) + 2 + 3 - def __init__( self, + *args, identifier: bytes = b"\x00", status: bool = True, - dev_id: int = 1, - transaction_id: int = 0, + **kwargs, ) -> None: - """Initialize a new instance.""" - super().__init__(transaction_id=transaction_id, dev_id=dev_id, status=status) + """Initialize.""" + super().__init__(*args, **kwargs) + self.status = status self.identifier = identifier - self.byte_count = 0 - def x_encode(self) -> bytes: - """Encode the response.""" + def __str__(self) -> str: + """Build representation of pdu.""" + return ( + super().__str__() + f", status={self.status}, identifier={self.identifier}" + ) + + @classmethod + def calculatePduSize(cls, is_request: bool, offset: int, data: bytes) -> int: + """Calculate the size of a PDU.""" + return 4 if is_request else int(data[offset + 2]) + 2 + 3 + + def to_bytes(self) -> bytes: + """Encode the message.""" + if self.is_request: + return b"" status = ID_ON if self.status else ID_OFF length = len(self.identifier) + 1 packet = struct.pack(">B", length) @@ -269,25 +214,40 @@ def x_encode(self) -> bytes: packet += struct.pack(">B", status) return packet - def x_decode(self, data: bytes) -> None: - """Decode a the response. + def from_bytes(self, offset: int, data: bytes) -> None: + """Decode payload.""" + if self.is_request: + return - Since the identifier is device dependent, we just return the - raw value that a user can decode to whatever it should be. - """ - self.byte_count = int(data[0]) - if not 1 <= self.byte_count <= len(data) - 1: + byte_count = int(data[offset]) + if not 1 <= byte_count <= len(data) - 1 - offset: raise ModbusIOException( - f"byte_count {self.byte_count} outside 1..{len(data) - 1} " + f"byte_count {byte_count} outside 1..{len(data) - 1 - offset} " f"for packet of length {len(data)}", function_code=self.function_code, ) - self.identifier = data[1 : self.byte_count] - status = int(data[self.byte_count]) + self.identifier = data[offset + 1 : offset + byte_count] + status = int(data[offset + byte_count]) self.status = status == ID_ON + async def build_response( + self, get_values: StoreGet, set_values: StoreSet + ) -> ModbusPdu: + """Update internal data.""" + information = DeviceInformationFactory.get(_MCB) + id_data = [] + for v_item in information.values(): + if isinstance(v_item, bytes): + id_data.append(v_item) + else: + id_data.append(v_item.encode()) + + identifier = b"-".join(id_data) + self.identifier = identifier or b"Pymodbus" + return self + -DecodePDU.add_pdu(ReadExceptionStatusRequest, ReadExceptionStatusResponse) -DecodePDU.add_pdu(GetCommEventCounterRequest, GetCommEventCounterResponse) -DecodePDU.add_pdu(GetCommEventLogRequest, GetCommEventLogResponse) -DecodePDU.add_pdu(ReportDeviceIdRequest, ReportDeviceIdResponse) +DecodePDU.add_pdu(PDUreadExceptionStatus) +DecodePDU.add_pdu(PDUgetCommEventCounter) +DecodePDU.add_pdu(PDUgetCommEventLog) +DecodePDU.add_pdu(PDUreportDeviceId) diff --git a/pymodbus/pdu/pdu.py b/pymodbus/pdu/pdu.py index 19efc00478..afdf861462 100644 --- a/pymodbus/pdu/pdu.py +++ b/pymodbus/pdu/pdu.py @@ -2,104 +2,233 @@ from __future__ import annotations -import asyncio +import struct from abc import abstractmethod -from collections.abc import Sequence +from collections.abc import Awaitable, Callable, Sequence +from typing import TypeAlias -from ..datastore import ModbusServerContext -from ..exceptions import ModbusIOException, NotImplementedException +from ..constants import ExcCodes +from ..exceptions import ( + ModbusIOException, +) +from ..logging import Log -class ModbusPDU: - """Base class for all Modbus messages.""" +StoreGet: TypeAlias = Callable[ + [int, int, int, int], Awaitable[list[int] | list[bool] | ExcCodes] +] +StoreSet: TypeAlias = Callable[ + [int, int, int, Sequence[int] | Sequence[bool]], Awaitable[ExcCodes | None] +] + + +class ModbusPdu: + """Base class for all Modbus messages. + + The expected format as parameter and as result are: + + parameter uses an extra "offset" to identify the start (avoids slicing the data) + """ function_code: int = 0 sub_function_code: int = -1 + MAX_COUNT: int = 2001 def __init__( self, - dev_id: int = 0, + dev_id: int, + is_request: bool = True, transaction_id: int = 0, - address: int = 0, - count: int = 0, - bits: Sequence[bool] | None = None, - registers: Sequence[int] | None = None, - status: int = 1, + address: int | None = None, + count: int | None = None, ) -> None: """Initialize the base data for a modbus request.""" - self.dev_id: int = dev_id - if dev_id > 255: + if not 0 <= dev_id <= 255: raise ModbusIOException( - f"Invalid ID {dev_id}", - transaction_id=transaction_id, + "Invalid device id", dev_id=dev_id, + function_code=self.function_code, + transaction_id=transaction_id, ) + self.dev_id: int = dev_id + self.is_request: bool = is_request self.transaction_id: int = transaction_id - self.address: int = address - self.bits: Sequence[bool] = bits or [] - self.registers: Sequence[int] = registers or [] - self.count: int = count or len(self.registers) - self.status: int = status - self.exception_code: int = 0 - self.fut: asyncio.Future - self.retries: int = 0 + if address is not None: + if not 0 <= address <= 65535: + raise ModbusIOException( + f"Invalid address({address})", + dev_id=dev_id, + function_code=self.function_code, + transaction_id=transaction_id, + ) + self.address: int = address + else: + self.address = 0 + if count is not None: + if not 0 < count <= self.MAX_COUNT: + raise ModbusIOException( + "Invalid count({count})", + dev_id=dev_id, + function_code=self.function_code, + transaction_id=transaction_id, + ) + self.count: int = count + else: + self.count = 0 + self.registers: Sequence[int] = [] + self.exception_code = 0 + self.bits: Sequence[bool] = [] + + def isError(self) -> bool: + """Check if the error is a success or failure.""" + return False + + def __str__(self) -> str: + """Build representation of pdu.""" + x_type = "request" if self.is_request else "response" + x_str = ( + f"{self.__class__.__name__}-{x_type} " + f"(dev_id={self.dev_id}, function_code={self.function_code}, transaction_id={self.transaction_id}" + ) + if self.address: + x_str += f", address={self.address}" + if self.count: + x_str += f", count={self.count}" + return x_str @classmethod - def decode_sub_function_code(cls, data: bytes) -> int: + def decode_sub_function_code(cls, offset: int, data: bytes) -> int: """Decode sub function code.""" - _ = data + _ = offset, data return -1 - def isError(self) -> bool: - """Check if the error is a success or failure.""" - return False + @classmethod + @abstractmethod + def calculatePduSize(cls, is_request: bool, offset: int, data: bytes) -> int: + """Calculate the size of a PDU.""" + + @abstractmethod + def to_bytes(self) -> bytes: + """Encode the message.""" + + @abstractmethod + def from_bytes(self, offset: int, data: bytes) -> None: + """Decode message.""" + + @abstractmethod + async def build_response( + self, get_values: StoreGet, set_values: StoreSet + ) -> ModbusPdu: + """Get/Set values from datastore.""" - def verifyCount(self, max_count: int, count: int = -1) -> None: - """Validate API supplied count.""" - if count == -1: - count = self.count - if not 1 <= count <= max_count: - raise ValueError(f"1 <= count {count} <= {max_count} !") - def verifyAddress(self, address: int = -1) -> None: - """Validate API supplied address.""" - if address == -1: - address = self.address - if not 0 <= address <= 65535: - raise ValueError(f"0 <= address {address} <= 65535 !") +class PDUexception(ModbusPdu): + """Base class for a modbus exception PDU.""" + + def __init__( + self, + dev_id: int, + function_code: int, + exception_code: int, + **kwargs, + ) -> None: + """Initialize.""" + super().__init__(dev_id, **kwargs) + self.function_code = function_code | 0x80 or 0 + self.exception_code = exception_code or 0 + + def isError(self) -> bool: + """Check if the error is a success or failure.""" + return True def __str__(self) -> str: - """Build a representation of a Modbus response.""" + """Build representation of pdu.""" return ( - f"{self.__class__.__name__}(" - f"dev_id={self.dev_id}, " - f"transaction_id={self.transaction_id}, " - f"address={self.address}, " - f"count={self.count}, " - f"bits={self.bits!s}, " - f"registers={self.registers!s}, " - f"status={self.status!s}, " - f"retries={self.retries})" + super().__str__() + + f", function_code={self.function_code}, exception_code={self.exception_code}" ) - @abstractmethod - def x_encode(self) -> bytes: - """Encode the message.""" + @classmethod + def calculatePduSize(cls, is_request: bool, offset: int, data: bytes) -> int: + """Calculate the size of a PDU.""" + return 5 - @abstractmethod - def x_decode(self, data: bytes) -> None: - """Decode data part of the message.""" - - async def datastore_update( - self, context: ModbusServerContext, device_id: int - ) -> ModbusPDU: - """Update diagnostic request on the given device.""" - _ = context, device_id - raise NotImplementedException( - f"datastore_update() not implemented {self.__class__.__name__}" + def to_bytes(self) -> bytes: + """Encode the message.""" + return struct.pack(">B", self.exception_code) + + def from_bytes(self, offset: int, data: bytes) -> None: + """Decode payload.""" + self.function_code = int(data[offset + 0]) + self.exception_code = int(data[offset + 1]) + + async def build_response( + self, get_values: StoreGet, set_values: StoreSet + ) -> ModbusPdu: + """Get from datastore.""" + return PDUexception( + self.dev_id, + self.function_code, + ExcCodes.ILLEGAL_FUNCTION, + is_request=self.is_request, ) + +class DecodePDU: + """Decode pdu requests/responses (server/client).""" + + pdu_table: dict[int, type[ModbusPdu]] = {} + pdu_sub_table: dict[int, dict[int, type[ModbusPdu]]] = {} + + def __init__(self, is_server: bool) -> None: + """Initialize function_tables.""" + self.is_server = is_server + @classmethod - @abstractmethod - def x_calculateRtuFrameSize(cls, data: bytes) -> int: + def _lookupPduClass(cls, data: bytes) -> type[ModbusPdu] | None: + """Use `function_code` to determine the class of the PDU.""" + if (func_code := int(data[1])) > 0x80: + return PDUexception + if not (pdu := cls.pdu_table.get(func_code, None)): + return None + if (sub_func_code := pdu.decode_sub_function_code(0, data)) < 0: + return pdu + return cls.pdu_sub_table[func_code].get(sub_func_code, None) + + @classmethod + def add_pdu(cls, pdu: type[ModbusPdu]): + """Register request/response.""" + cls.pdu_table[pdu.function_code] = pdu + if pdu.sub_function_code >= 0: + if pdu.function_code not in cls.pdu_sub_table: + cls.pdu_sub_table[pdu.function_code] = {} + cls.pdu_sub_table[pdu.function_code][pdu.sub_function_code] = pdu + + def decode(self, frame: bytes) -> ModbusPdu: + """Decode a frame.""" + if not frame: + raise ModbusIOException("Cannot decode empty frame") + function_code = 0 + try: + if not (pdu_class := self._lookupPduClass(b"\x00" + frame)): + raise ModbusIOException( + f"Unknown response {function_code}", function_code=function_code + ) + pdu = pdu_class(0, is_request=self.is_server) # type: ignore[call-arg] + pdu.from_bytes(1, frame) # type: ignore[attr-defined] + Log.debug("decoded PDU " + str(pdu)) + return pdu + except (ValueError, IndexError, struct.error) as exc: + raise ModbusIOException( + f"Unable to decode frame {exc}", function_code=function_code + ) from exc + + def calculateRtuFrameSize(self, data: bytes) -> int: """Calculate the size of a PDU.""" + if not (pdu_class := self._lookupPduClass(data)): + return 0 + return pdu_class.calculatePduSize(self.is_server, 0, data) + + def encode(self, pdu: ModbusPdu) -> bytes: + """Create message from PDU.""" + return pdu.to_bytes() diff --git a/pymodbus/pdu/register_message.py b/pymodbus/pdu/register_message.py index 9475286cdc..41d3f1114e 100644 --- a/pymodbus/pdu/register_message.py +++ b/pymodbus/pdu/register_message.py @@ -7,504 +7,383 @@ from typing import cast from ..constants import ExcCodes -from ..datastore import ModbusServerContext from ..exceptions import ModbusIOException -from .decoders import DecodePDU -from .exceptionresponse import ExceptionResponse -from .pdu import ModbusPDU +from .pdu import DecodePDU, ModbusPdu, PDUexception, StoreGet, StoreSet -class ReadHoldingRegistersRequest(ModbusPDU): - """ReadHoldingRegistersRequest.""" +class PDUreadHoldingRegisters(ModbusPdu): + """ReadHoldingRegisters.""" function_code = 3 MAX_COUNT = 125 - @classmethod - def x_calculateRtuFrameSize(cls, data: bytes) -> int: - """Calculate the size of a PDU.""" - return 8 - - def x_encode(self) -> bytes: - """Encode the request packet.""" - self.verifyAddress() - self.verifyCount(self.MAX_COUNT) - return struct.pack(">HH", self.address, self.count) - - def x_decode(self, data: bytes) -> None: - """Decode a register request packet.""" - self.address, self.count = struct.unpack(">HH", data[:4]) - - async def datastore_update( - self, context: ModbusServerContext, device_id: int - ) -> ModbusPDU: - """Update diagnostic request on the given device.""" - if not 1 <= self.count <= self.MAX_COUNT: - return ExceptionResponse(self.function_code, ExcCodes.ILLEGAL_VALUE) - values = await context.async_getValues( - device_id, self.function_code, self.address, self.count - ) - if isinstance(values, ExcCodes): - return ExceptionResponse(self.function_code, values) - return ReadHoldingRegistersResponse( - registers=cast(list[int], values), - dev_id=device_id, - transaction_id=self.transaction_id, - ) - + def __init__( + self, + *args, + registers: Sequence[int] | None = None, + **kwargs, + ) -> None: + """Initialize.""" + if registers: + kwargs |= {"count": kwargs.get("count", len(registers))} + super().__init__(*args, **kwargs) + self.registers = registers or [] -class ReadHoldingRegistersResponse(ModbusPDU): - """ReadHoldingRegistersResponse.""" - - function_code = 3 + def __str__(self) -> str: + """Build representation of pdu.""" + return super().__str__() + f", registers={self.registers}" @classmethod - def x_calculateRtuFrameSize(cls, data: bytes) -> int: + def calculatePduSize(cls, is_request: bool, offset: int, data: bytes) -> int: """Calculate the size of a PDU.""" - return int(data[2]) + 2 + 3 + return 8 if is_request else int(data[offset + 2]) + 2 + 3 - def x_encode(self) -> bytes: - """Encode the response packet.""" + def to_bytes(self) -> bytes: + """Encode the message.""" + if self.is_request: + return struct.pack(">HH", self.address, self.count) result = struct.pack(">B", len(self.registers) * 2) for register in self.registers: result += struct.pack(">H", register) return result - def x_decode(self, data: bytes) -> None: - """Decode a register response packet.""" - self.registers = [] - if (data_len := int(data[0])) >= len(data): - raise ModbusIOException( - f"byte_count {data_len} > length of packet {len(data)}", - function_code=self.function_code, - ) - for i in range(1, data_len, 2): - self.registers.append(struct.unpack(">H", data[i : i + 2])[0]) - - -class ReadInputRegistersRequest(ModbusPDU): - """ReadInputRegistersRequest.""" - - function_code = 4 - MAX_COUNT = 125 - - @classmethod - def x_calculateRtuFrameSize(cls, data: bytes) -> int: - """Calculate the size of a PDU.""" - return 8 - - def x_encode(self) -> bytes: - """Encode the request packet.""" - self.verifyAddress() - self.verifyCount(self.MAX_COUNT) - return struct.pack(">HH", self.address, self.count) - - def x_decode(self, data: bytes) -> None: - """Decode a register request packet.""" - self.address, self.count = struct.unpack(">HH", data[:4]) - - async def datastore_update( - self, context: ModbusServerContext, device_id: int - ) -> ModbusPDU: - """Update diagnostic request on the given device.""" + def from_bytes(self, offset: int, data: bytes) -> None: + """Decode payload.""" + if self.is_request: + self.address, self.count = struct.unpack(">HH", data[offset : offset + 4]) + else: + self.registers = [] + if (data_len := int(data[offset])) >= len(data) - offset: + raise ModbusIOException( + f"byte_count {data_len} > length of packet {len(data) - offset}", + function_code=self.function_code, + ) + for i in range(1, data_len, 2): + self.registers.append( + struct.unpack(">H", data[offset + i : offset + i + 2])[0] + ) + self.count = len(self.registers) + + async def build_response( + self, get_values: StoreGet, set_values: StoreSet + ) -> ModbusPdu: + """Get bits from datastore.""" if not 1 <= self.count <= self.MAX_COUNT: - return ExceptionResponse(self.function_code, ExcCodes.ILLEGAL_VALUE) - values = await context.async_getValues( - device_id, self.function_code, self.address, self.count + return PDUexception(self.dev_id, self.function_code, ExcCodes.ILLEGAL_VALUE) + values = await get_values( + self.dev_id, self.function_code, self.address, self.count ) if isinstance(values, ExcCodes): - return ExceptionResponse(self.function_code, values) - return ReadInputRegistersResponse( - registers=cast(list[int], values), - dev_id=device_id, - transaction_id=self.transaction_id, - ) + return PDUexception(self.dev_id, self.function_code, values) + self.is_request = False + return self -class ReadInputRegistersResponse(ModbusPDU): - """ReadInputRegistersResponse.""" +class PDUreadInputRegisters(PDUreadHoldingRegisters): + """ReadInputRegisters.""" function_code = 4 + MAX_COUNT = 125 @classmethod - def x_calculateRtuFrameSize(cls, data: bytes) -> int: + def calculatePduSize(cls, is_request: bool, offset: int, data: bytes) -> int: """Calculate the size of a PDU.""" - return int(data[2]) + 2 + 3 + return 8 if is_request else int(data[offset + 2]) + 2 + 3 - def x_encode(self) -> bytes: - """Encode the response packet.""" + def to_bytes(self) -> bytes: + """Encode the message.""" + if self.is_request: + return struct.pack(">HH", self.address, self.count) result = struct.pack(">B", len(self.registers) * 2) for register in self.registers: result += struct.pack(">H", register) return result - def x_decode(self, data: bytes) -> None: - """Decode a register response packet.""" - self.registers = [] - if (data_len := int(data[0])) >= len(data): - raise ModbusIOException( - f"byte_count {data_len} > length of packet {len(data)}", - function_code=self.function_code, - ) - for i in range(1, data_len, 2): - self.registers.append(struct.unpack(">H", data[i : i + 2])[0]) + def from_bytes(self, offset: int, data: bytes) -> None: + """Decode payload.""" + if self.is_request: + self.address, self.count = struct.unpack(">HH", data[offset : offset + 4]) + else: + self.registers = [] + if (data_len := int(data[offset])) >= len(data) - offset: + raise ModbusIOException( + f"byte_count {data_len} > length of packet {len(data) - offset}", + function_code=self.function_code, + ) + for i in range(1, data_len, 2): + self.registers.append( + struct.unpack(">H", data[offset + i : offset + i + 2])[0] + ) + self.count = len(self.registers) + + async def build_response( + self, get_values: StoreGet, set_values: StoreSet + ) -> ModbusPdu: + """Get bits from datastore.""" + if not 1 <= self.count <= self.MAX_COUNT: + return PDUexception(self.dev_id, self.function_code, ExcCodes.ILLEGAL_VALUE) + values = await get_values( + self.dev_id, self.function_code, self.address, self.count + ) + if isinstance(values, ExcCodes): + return PDUexception(self.dev_id, self.function_code, values) + self.is_request = False + return self -class ReadWriteMultipleRegistersRequest(ModbusPDU): - """ReadWriteMultipleRegistersRequest.""" +class PDUreadWriteMultipleRegisters(PDUreadHoldingRegisters): + """ReadWriteMultipleRegisters.""" function_code = 23 MAX_READ_COUNT = 125 MAX_WRITE_COUNT = 121 - @classmethod - def x_calculateRtuFrameSize(cls, data: bytes) -> int: - """Calculate the size of a PDU.""" - return int(data[10]) + 10 + 3 - def __init__( self, - read_address: int = 0x00, - read_count: int = 0, + *args, write_address: int = 0x00, write_registers: Sequence[int] | None = None, - dev_id: int = 1, - transaction_id: int = 0, + **kwargs, ) -> None: - """Initialize a new request message.""" - if not write_registers: - write_registers = [] - super().__init__(transaction_id=transaction_id, dev_id=dev_id) - self.read_address = read_address - self.read_count = read_count - self.write_address = write_address - self.write_registers = write_registers - self.write_count = len(self.write_registers) - self.write_byte_count = self.write_count * 2 - self._payload_byte_count: int | None = None - - def x_encode(self) -> bytes: - """Encode the request packet.""" - self.verifyAddress(address=self.read_address) - self.verifyAddress(address=self.write_address) - self.verifyCount(self.MAX_READ_COUNT, count=self.read_count) - self.verifyCount(self.MAX_WRITE_COUNT, count=self.write_count) - result = struct.pack( - ">HHHHB", - self.read_address, - self.read_count, - self.write_address, - self.write_count, - self.write_byte_count, + """Initialize.""" + super().__init__(*args, **kwargs) + self.write_registers: Sequence[int] = write_registers or [] + self.write_address: int = write_address + self.write_count: int = len(write_registers) if write_registers else 0 + + def __str__(self) -> str: + """Build representation of pdu.""" + return ( + super().__str__() + + f", write_address={self.write_address}, write_count={self.write_count}, write_registers={self.write_registers}" ) - for register in self.write_registers: - result += struct.pack(">H", register) - return result - - def x_decode(self, data: bytes) -> None: - """Decode the register request packet.""" - ( - self.read_address, - self.read_count, - self.write_address, - self.write_count, - self.write_byte_count, - ) = struct.unpack(">HHHHB", data[:9]) - self.verifyCount(self.MAX_READ_COUNT, count=self.write_count) - self._payload_byte_count = len(data) - 9 - self.write_registers = [ - struct.unpack(">H", data[i : i + 2])[0] - for i in range(9, min(len(data), self.write_byte_count + 9) - 1, 2) - ] - - async def datastore_update( - self, context: ModbusServerContext, device_id: int - ) -> ModbusPDU: - """Update diagnostic request on the given device.""" - if not (1 <= self.read_count <= 0x07D): - return ExceptionResponse(self.function_code, ExcCodes.ILLEGAL_VALUE) - if not 1 <= self.write_count <= 0x079: - return ExceptionResponse(self.function_code, ExcCodes.ILLEGAL_VALUE) - if self.write_byte_count != self.write_count * 2 or ( - self._payload_byte_count is not None - and self._payload_byte_count != self.write_byte_count - ): - return ExceptionResponse(self.function_code, ExcCodes.ILLEGAL_VALUE) - rc = await context.async_setValues( - device_id, self.function_code, self.write_address, self.write_registers - ) - if rc: - return ExceptionResponse(self.function_code, rc) - registers = await context.async_getValues( - device_id, self.function_code, self.read_address, self.read_count - ) - if isinstance(registers, ExcCodes): - return ExceptionResponse(self.function_code, registers) - return ReadWriteMultipleRegistersResponse( - registers=cast(list[int], registers), - dev_id=device_id, - transaction_id=self.transaction_id, - ) - - -class ReadWriteMultipleRegistersResponse(ModbusPDU): - """ReadWriteMultipleRegistersResponse.""" - - function_code = 23 @classmethod - def x_calculateRtuFrameSize(cls, data: bytes) -> int: + def calculatePduSize(cls, is_request: bool, offset: int, data: bytes) -> int: """Calculate the size of a PDU.""" - return int(data[2]) + 2 + 3 + return ( + int(data[offset + 10]) + 10 + 3 + if is_request + else int(data[offset + 2]) + 2 + 3 + ) - def x_encode(self) -> bytes: - """Encode the response packet.""" + def to_bytes(self) -> bytes: + """Encode the message.""" + if self.is_request: + result = struct.pack( + ">HHHHB", + self.address, + self.count, + self.write_address, + self.write_count, + self.write_count * 2, + ) + for register in self.write_registers: + result += struct.pack(">H", register) + return result result = struct.pack(">B", len(self.registers) * 2) for register in self.registers: result += struct.pack(">H", register) return result - def x_decode(self, data: bytes) -> None: - """Decode a register response packet.""" - self.registers = [] - if (data_len := int(data[0])) >= len(data): - raise ModbusIOException( - f"byte_count {data_len} > length of packet {len(data)}", - function_code=self.function_code, - ) - for i in range(1, data_len, 2): - self.registers.append(struct.unpack(">H", data[i : i + 2])[0]) - - -class WriteSingleRegisterResponse(ModbusPDU): - """WriteSingleRegisterResponse.""" - - function_code = 6 - - @classmethod - def x_calculateRtuFrameSize(cls, data: bytes) -> int: - """Calculate the size of a PDU.""" - return 8 - - def x_encode(self) -> bytes: - """Encode a write single register packet packet request.""" - return struct.pack(">HH", self.address, self.registers[0]) + def from_bytes(self, offset: int, data: bytes) -> None: + """Decode payload.""" + if self.is_request: + ( + self.address, + self.count, + self.write_address, + self.write_count, + _, + ) = struct.unpack(">HHHHB", data[offset : offset + 9]) + self.write_registers = [ + struct.unpack(">H", data[offset + i : offset + i + 2])[0] + for i in range( + 9, min(len(data) - offset, self.write_count * 2 + 9) - 1, 2 + ) + ] + else: + self.registers = [] + if (data_len := int(data[offset])) >= len(data) - offset: + raise ModbusIOException( + f"byte_count {data_len} > length of packet {len(data) - offset}", + function_code=self.function_code, + ) + for i in range(1, data_len, 2): + self.registers.append( + struct.unpack(">H", data[offset + i : offset + i + 2])[0] + ) + self.count = len(self.registers) + + async def build_response( + self, get_values: StoreGet, set_values: StoreSet + ) -> ModbusPdu: + """Get bits from datastore.""" + if not (1 <= self.count <= 0x07D): + return PDUexception(self.dev_id, self.function_code, ExcCodes.ILLEGAL_VALUE) + if not 1 <= self.write_count <= 0x079: + return PDUexception(self.dev_id, self.function_code, ExcCodes.ILLEGAL_VALUE) + rc = await set_values( + self.dev_id, self.function_code, self.write_address, self.write_registers + ) + if rc: + return PDUexception(self.dev_id, self.function_code, ExcCodes.ILLEGAL_VALUE) - def x_decode(self, data: bytes) -> None: - """Decode a write single register packet packet request.""" - self.address, register = struct.unpack(">HH", data[:4]) - self.registers = [register] + registers = await get_values( + self.dev_id, self.function_code, self.address, self.count + ) + if isinstance(registers, ExcCodes): + return PDUexception(self.dev_id, self.function_code, registers) + self.is_request = False + return self -class WriteSingleRegisterRequest(ModbusPDU): - """WriteSingleRegisterRequest.""" +class PDUwriteSingleRegister(PDUreadHoldingRegisters): + """WriteSingleRegister.""" function_code = 6 @classmethod - def x_calculateRtuFrameSize(cls, data: bytes) -> int: + def calculatePduSize(cls, is_request: bool, offset: int, data: bytes) -> int: """Calculate the size of a PDU.""" return 8 - def x_encode(self) -> bytes: - """Encode a write single register packet packet request.""" + def to_bytes(self) -> bytes: + """Encode the message.""" return struct.pack(">HH", self.address, self.registers[0]) - def x_decode(self, data: bytes) -> None: - """Decode a write single register packet packet request.""" - self.address, register = struct.unpack(">HH", data[:4]) + def from_bytes(self, offset: int, data: bytes) -> None: + """Decode payload.""" + self.address, register = struct.unpack(">HH", data[offset : offset + 4]) self.registers = [register] - async def datastore_update( - self, context: ModbusServerContext, device_id: int - ) -> ModbusPDU: - """Update diagnostic request on the given device.""" + async def build_response( + self, get_values: StoreGet, set_values: StoreSet + ) -> ModbusPdu: + """Get bits from datastore.""" if not 0 <= self.registers[0] <= 0xFFFF: - return ExceptionResponse(self.function_code, ExcCodes.ILLEGAL_VALUE) - rc = await context.async_setValues( - device_id, self.function_code, self.address, self.registers + return PDUexception(self.dev_id, self.function_code, ExcCodes.ILLEGAL_VALUE) + rc = await set_values( + self.dev_id, self.function_code, self.address, self.registers ) if rc: - return ExceptionResponse(self.function_code, rc) - values = await context.async_getValues( - device_id, self.function_code, self.address, 1 - ) - if isinstance(values, ExcCodes): - return ExceptionResponse(self.function_code, values) - return WriteSingleRegisterResponse( - address=self.address, registers=cast(list[int], values) - ) - + return PDUexception(self.dev_id, self.function_code, ExcCodes.ILLEGAL_VALUE) -class WriteMultipleRegistersRequest(ModbusPDU): - """WriteMultipleRegistersRequest.""" - - function_code = 16 - _pdu_length = 5 # func + adress1 + adress2 + outputQuant1 + outputQuant2 - byte_count: int | None = None - _payload_byte_count: int | None = None - - @classmethod - def x_calculateRtuFrameSize(cls, data: bytes) -> int: - """Calculate the size of a PDU.""" - return int(data[6]) + 6 + 3 - - def x_encode(self) -> bytes: - """Encode a write single register packet packet request.""" - packet = struct.pack(">HHB", self.address, self.count, self.count * 2) - for value in self.registers: - packet += struct.pack(">H", value) - return packet - - def x_decode(self, data: bytes) -> None: - """Decode a write single register packet packet request.""" - self.address, self.count, self.byte_count = struct.unpack(">HHB", data[:5]) - self._payload_byte_count = len(data) - 5 - self.registers = [ - struct.unpack(">H", data[idx : idx + 2])[0] - for idx in range(5, min(len(data), self.byte_count + 5) - 1, 2) - ] - - async def datastore_update( - self, context: ModbusServerContext, device_id: int - ) -> ModbusPDU: - """Update diagnostic request on the given device.""" - if not 1 <= self.count <= 0x07B: - return ExceptionResponse(self.function_code, ExcCodes.ILLEGAL_VALUE) - if self.byte_count is not None and ( - self.byte_count != self.count * 2 - or self._payload_byte_count != self.byte_count - ): - return ExceptionResponse(self.function_code, ExcCodes.ILLEGAL_VALUE) - rc = await context.async_setValues( - device_id, self.function_code, self.address, self.registers - ) - if rc: - return ExceptionResponse(self.function_code, rc) - return WriteMultipleRegistersResponse( - address=self.address, - count=self.count, - dev_id=device_id, - transaction_id=self.transaction_id, - ) + registers = await get_values(self.dev_id, self.function_code, self.address, 1) + if isinstance(registers, ExcCodes): + return PDUexception(self.dev_id, self.function_code, registers) + self.is_request = False + return self -class WriteMultipleRegistersResponse(ModbusPDU): - """WriteMultipleRegistersResponse.""" +class PDUwriteMultipleRegisters(PDUreadHoldingRegisters): + """WriteMultipleRegisters.""" function_code = 16 @classmethod - def x_calculateRtuFrameSize(cls, data: bytes) -> int: + def calculatePduSize(cls, is_request: bool, offset: int, data: bytes) -> int: """Calculate the size of a PDU.""" - return 8 - - def x_encode(self) -> bytes: - """Encode a write single register packet packet request.""" + return int(data[offset + 6]) + 6 + 3 if is_request else 8 + + def to_bytes(self) -> bytes: + """Encode the message.""" + if self.is_request: + packet = struct.pack(">HHB", self.address, self.count, self.count * 2) + for value in self.registers: + packet += struct.pack(">H", value) + return packet return struct.pack(">HH", self.address, self.count) - def x_decode(self, data: bytes) -> None: - """Decode a write single register packet packet request.""" - self.address, self.count = struct.unpack(">HH", data[:4]) + def from_bytes(self, offset: int, data: bytes) -> None: + """Decode payload.""" + if self.is_request: + self.address, self.count, byte_count = struct.unpack( + ">HHB", data[offset : offset + 5] + ) + self.registers = [ + struct.unpack(">H", data[offset + idx : offset + idx + 2])[0] + for idx in range(5, min(len(data) - offset, byte_count + 5) - 1, 2) + ] + self.count = len(self.registers) + else: + self.address, self.count = struct.unpack(">HH", data[offset : offset + 4]) + + async def build_response( + self, get_values: StoreGet, set_values: StoreSet + ) -> ModbusPdu: + """Get bits from datastore.""" + if not 1 <= self.count <= 0x07B: + return PDUexception(self.dev_id, self.function_code, ExcCodes.ILLEGAL_VALUE) + rc = await set_values( + self.dev_id, self.function_code, self.address, self.registers + ) + if rc: + return PDUexception(self.dev_id, self.function_code, ExcCodes.ILLEGAL_VALUE) + self.is_request = False + return self -class MaskWriteRegisterRequest(ModbusPDU): - """MaskWriteRegisterRequest.""" +class PDUmaskWriteRegister(PDUreadHoldingRegisters): + """MaskWriteRegister.""" function_code = 0x16 - @classmethod - def x_calculateRtuFrameSize(cls, data: bytes) -> int: - """Calculate the size of a PDU.""" - return 10 - def __init__( self, - address=0x0000, - and_mask=0xFFFF, - or_mask=0x0000, - dev_id=1, - transaction_id=0, + *args, + and_mask: int = 0xFFFF, + or_mask: int = 0x0000, + **kwargs, ) -> None: - """Initialize a new instance.""" - super().__init__(transaction_id=transaction_id, dev_id=dev_id, address=address) + """Initialize.""" + super().__init__(*args, **kwargs) self.and_mask = and_mask self.or_mask = or_mask - def x_encode(self) -> bytes: - """Encode the request packet.""" - return struct.pack(">HHH", self.address, self.and_mask, self.or_mask) - - def x_decode(self, data: bytes) -> None: - """Decode the incoming request.""" - self.address, self.and_mask, self.or_mask = struct.unpack(">HHH", data[:6]) - - async def datastore_update( - self, context: ModbusServerContext, device_id: int - ) -> ModbusPDU: - """Update diagnostic request on the given device.""" - if not 0x0000 <= self.and_mask <= 0xFFFF: - return ExceptionResponse(self.function_code, ExcCodes.ILLEGAL_VALUE) - if not 0x0000 <= self.or_mask <= 0xFFFF: - return ExceptionResponse(self.function_code, ExcCodes.ILLEGAL_VALUE) - values = await context.async_getValues( - device_id, self.function_code, self.address, 1 - ) - if isinstance(values, ExcCodes): - return ExceptionResponse(self.function_code, values) - values = (cast(Sequence[int | bool], values)[0] & self.and_mask) | ( - self.or_mask & ~self.and_mask - ) - rc = await context.async_setValues( - device_id, self.function_code, self.address, cast(list[int], [values]) - ) - if rc: - return ExceptionResponse(self.function_code, rc) - return MaskWriteRegisterResponse( - address=self.address, - and_mask=self.and_mask, - or_mask=self.or_mask, - dev_id=device_id, - transaction_id=self.transaction_id, - ) - - -class MaskWriteRegisterResponse(ModbusPDU): - """MaskWriteRegisterResponse.""" - - function_code = 0x16 + def __str__(self) -> str: + """Build representation of pdu.""" + return super().__str__() + f", and_mask={self.and_mask}, or_mask={self.or_mask}" @classmethod - def x_calculateRtuFrameSize(cls, data: bytes) -> int: + def calculatePduSize(cls, is_request: bool, offset: int, data: bytes) -> int: """Calculate the size of a PDU.""" return 10 - def __init__( - self, - address=0x0000, - and_mask=0xFFFF, - or_mask=0x0000, - dev_id=1, - transaction_id=0, - ) -> None: - """Initialize new instance.""" - super().__init__(transaction_id=transaction_id, dev_id=dev_id, address=address) - self.and_mask = and_mask - self.or_mask = or_mask - - def x_encode(self) -> bytes: - """Encode the response.""" - self.verifyAddress() + def to_bytes(self) -> bytes: + """Encode the message.""" return struct.pack(">HHH", self.address, self.and_mask, self.or_mask) - def x_decode(self, data: bytes) -> None: - """Decode a the response.""" - self.address, self.and_mask, self.or_mask = struct.unpack(">HHH", data[:6]) + def from_bytes(self, offset: int, data: bytes) -> None: + """Decode payload.""" + self.address, self.and_mask, self.or_mask = struct.unpack( + ">HHH", data[offset : offset + 6] + ) + + async def build_response( + self, get_values: StoreGet, set_values: StoreSet + ) -> ModbusPdu: + """Get bits from datastore.""" + if not 0x0000 <= self.and_mask <= 0xFFFF: + return PDUexception(self.dev_id, self.function_code, ExcCodes.ILLEGAL_VALUE) + if not 0x0000 <= self.or_mask <= 0xFFFF: + return PDUexception(self.dev_id, self.function_code, ExcCodes.ILLEGAL_VALUE) + if not 1 <= self.count <= 0x07B: + return PDUexception(self.dev_id, self.function_code, ExcCodes.ILLEGAL_VALUE) + if values := await get_values(self.dev_id, self.function_code, self.address, 1): + return PDUexception(self.dev_id, self.function_code, ExcCodes.ILLEGAL_VALUE) + rc = await set_values( + self.dev_id, self.function_code, self.address, cast(list[int], [values]) + ) + if rc: + return PDUexception(self.dev_id, self.function_code, ExcCodes.ILLEGAL_VALUE) + self.is_request = False + return self -DecodePDU.add_pdu(ReadHoldingRegistersRequest, ReadHoldingRegistersResponse) -DecodePDU.add_pdu(ReadInputRegistersRequest, ReadInputRegistersResponse) -DecodePDU.add_pdu(WriteMultipleRegistersRequest, WriteMultipleRegistersResponse) -DecodePDU.add_pdu(WriteSingleRegisterRequest, WriteSingleRegisterResponse) -DecodePDU.add_pdu(ReadWriteMultipleRegistersRequest, ReadWriteMultipleRegistersResponse) -DecodePDU.add_pdu(MaskWriteRegisterRequest, MaskWriteRegisterResponse) +DecodePDU.add_pdu(PDUreadHoldingRegisters) +DecodePDU.add_pdu(PDUreadInputRegisters) +DecodePDU.add_pdu(PDUwriteMultipleRegisters) +DecodePDU.add_pdu(PDUwriteSingleRegister) +DecodePDU.add_pdu(PDUreadWriteMultipleRegisters) +DecodePDU.add_pdu(PDUmaskWriteRegister) diff --git a/pymodbus/server/base.py b/pymodbus/server/base.py index 5613f10dc5..d5a5cf6f71 100644 --- a/pymodbus/server/base.py +++ b/pymodbus/server/base.py @@ -10,8 +10,12 @@ from ..exceptions import ParameterException from ..framer import FRAMER_NAME_TO_CLASS, FramerType from ..logging import Log -from ..pdu import DecodePDU, ModbusPDU -from ..pdu.device import ModbusControlBlock, ModbusDeviceIdentification +from ..pdu import ( + DecodePDU, + ModbusControlBlock, + ModbusDeviceIdentification, + ModbusPdu, +) from ..simulator import SimDevice from ..simulator.simcore import SimCore from ..transport import CommParams, ModbusProtocol @@ -32,9 +36,9 @@ def __init__( identity: ModbusDeviceIdentification | None, framer: FramerType, trace_packet: Callable[[bool, bytes], bytes] | None, - trace_pdu: Callable[[bool, ModbusPDU], ModbusPDU] | None, + trace_pdu: Callable[[bool, ModbusPdu], ModbusPdu] | None, trace_connect: Callable[[bool], None] | None, - custom_pdu: list[type[ModbusPDU]] | None, + custom_pdu: list[type[ModbusPdu]] | None, ) -> None: """Initialize base server.""" super().__init__( @@ -44,8 +48,8 @@ def __init__( self.loop = asyncio.get_running_loop() self.decoder = DecodePDU(True) if custom_pdu: - for func in custom_pdu: - self.decoder.register(func) + for new_pdu in custom_pdu: + DecodePDU.add_pdu(new_pdu) self.context: ModbusServerContext | SimCore if not isinstance(context, ModbusServerContext): self.context = SimCore(context) diff --git a/pymodbus/server/requesthandler.py b/pymodbus/server/requesthandler.py index de130f3fa0..2c52117b78 100644 --- a/pymodbus/server/requesthandler.py +++ b/pymodbus/server/requesthandler.py @@ -9,7 +9,7 @@ from ..constants import ExcCodes from ..exceptions import ModbusIOException, NoSuchIdException from ..logging import Log -from ..pdu import ExceptionResponse, ModbusPDU +from ..pdu import ModbusPdu, PDUexception from ..transaction import TransactionManager from ..transport import CommParams @@ -68,10 +68,10 @@ def callback_data(self, data: bytes, addr: tuple | None = None) -> int: new_len = super().callback_data(data[used_len:], addr) except ModbusIOException as exc: function_code = 0x00 if exc.fcode is None else exc.fcode - response = ExceptionResponse( + response = PDUexception( + exc.dev_id, function_code, - exception_code=ExcCodes.ILLEGAL_FUNCTION, - device_id=exc.dev_id, + ExcCodes.ILLEGAL_FUNCTION, transaction=exc.transaction_id, ) new_len = exc.used_len @@ -83,36 +83,39 @@ def callback_data(self, data: bytes, addr: tuple | None = None) -> int: break return used_len - def handle_later(self, pdu: ModbusPDU) -> None: + def handle_later(self, pdu: ModbusPdu) -> None: """Change sync (async not allowed in call_soon) to async.""" asyncio.run_coroutine_threadsafe(self.handle_request(pdu), self.loop) - async def handle_request(self, pdu: ModbusPDU) -> None: + async def handle_request(self, pdu: ModbusPdu) -> None: """Handle request.""" try: + get_v = self.server.context.async_getValues + set_v = self.server.context.async_setValues if self.server.broadcast_enable and not pdu.dev_id: # if broadcasting then execute on all device contexts, # note response will be ignored + remember = pdu.dev_id for dev_id in self.server.context.device_ids(): - await pdu.datastore_update(self.server.context, dev_id) + pdu.dev_id = dev_id + await pdu.build_response(get_v, set_v) + pdu.dev_id = remember return - response = await pdu.datastore_update(self.server.context, pdu.dev_id) + response = await pdu.build_response(get_v, set_v) except NoSuchIdException: if self.server.ignore_missing_devices: Log.debug("ignoring request for unknown device id: {}", pdu.dev_id) return # the client will simply timeout waiting for a response Log.error("requested device id does not exist: {}", pdu.dev_id) - response = ExceptionResponse( - pdu.function_code, ExcCodes.GATEWAY_NO_RESPONSE - ) + response = PDUexception(0, pdu.function_code, ExcCodes.GATEWAY_NO_RESPONSE) except Exception as exc: # pylint: disable=broad-except Log.error( "Datastore unable to fulfill request: {}; {}", exc, traceback.format_exc(), ) - response = ExceptionResponse(pdu.function_code, ExcCodes.DEVICE_FAILURE) + response = PDUexception(0, pdu.function_code, ExcCodes.DEVICE_FAILURE) response.transaction_id = pdu.transaction_id response.dev_id = pdu.dev_id self.server_send(response, self.last_addr) diff --git a/pymodbus/server/server.py b/pymodbus/server/server.py index c2694d0455..17ccc4fc40 100644 --- a/pymodbus/server/server.py +++ b/pymodbus/server/server.py @@ -6,8 +6,7 @@ from ..datastore import ModbusServerContext from ..framer import FramerType -from ..pdu import ModbusPDU -from ..pdu.device import ModbusDeviceIdentification +from ..pdu import ModbusDeviceIdentification, ModbusPdu from ..simulator import SimDevice from ..transport import CommParams, CommType from .base import ModbusBaseServer @@ -30,9 +29,9 @@ def __init__( ignore_missing_devices: bool = False, broadcast_enable: bool = False, trace_packet: Callable[[bool, bytes], bytes] | None = None, - trace_pdu: Callable[[bool, ModbusPDU], ModbusPDU] | None = None, + trace_pdu: Callable[[bool, ModbusPdu], ModbusPdu] | None = None, trace_connect: Callable[[bool], None] | None = None, - custom_pdu: list[type[ModbusPDU]] | None = None, + custom_pdu: list[type[ModbusPdu]] | None = None, ): """Initialize the socket server. @@ -49,7 +48,7 @@ def __init__( :param trace_packet: Called with bytestream received/to be sent :param trace_pdu: Called with PDU received/to be sent :param trace_connect: Called when connected/disconnected - :param custom_pdu: list of ModbusPDU custom classes + :param custom_pdu: list of ModbusPdu custom classes """ params = getattr( self, @@ -95,9 +94,9 @@ def __init__( ignore_missing_devices=False, broadcast_enable=False, trace_packet: Callable[[bool, bytes], bytes] | None = None, - trace_pdu: Callable[[bool, ModbusPDU], ModbusPDU] | None = None, + trace_pdu: Callable[[bool, ModbusPdu], ModbusPdu] | None = None, trace_connect: Callable[[bool], None] | None = None, - custom_pdu: list[type[ModbusPDU]] | None = None, + custom_pdu: list[type[ModbusPdu]] | None = None, ): """Overloaded initializer for the socket server. @@ -115,7 +114,7 @@ def __init__( :param trace_packet: Called with bytestream received/to be sent :param trace_pdu: Called with PDU received/to be sent :param trace_connect: Called when connected/disconnected - :param custom_pdu: list of ModbusPDU custom classes + :param custom_pdu: list of ModbusPdu custom classes """ if not sslctx: raise TypeError( @@ -160,9 +159,9 @@ def __init__( ignore_missing_devices: bool = False, broadcast_enable: bool = False, trace_packet: Callable[[bool, bytes], bytes] | None = None, - trace_pdu: Callable[[bool, ModbusPDU], ModbusPDU] | None = None, + trace_pdu: Callable[[bool, ModbusPdu], ModbusPdu] | None = None, trace_connect: Callable[[bool], None] | None = None, - custom_pdu: list[type[ModbusPDU]] | None = None, + custom_pdu: list[type[ModbusPdu]] | None = None, ): """Overloaded initializer for the socket server. @@ -179,7 +178,7 @@ def __init__( :param trace_packet: Called with bytestream received/to be sent :param trace_pdu: Called with PDU received/to be sent :param trace_connect: Called when connected/disconnected - :param custom_pdu: list of ModbusPDU custom classes + :param custom_pdu: list of ModbusPdu custom classes """ # ---------------- params = CommParams( @@ -228,9 +227,9 @@ def __init__( broadcast_enable: bool = False, reconnect_delay: float = 2, trace_packet: Callable[[bool, bytes], bytes] | None = None, - trace_pdu: Callable[[bool, ModbusPDU], ModbusPDU] | None = None, + trace_pdu: Callable[[bool, ModbusPdu], ModbusPdu] | None = None, trace_connect: Callable[[bool], None] | None = None, - custom_pdu: list[type[ModbusPDU]] | None = None, + custom_pdu: list[type[ModbusPdu]] | None = None, ): """Initialize the socket server. @@ -253,7 +252,7 @@ def __init__( :param trace_packet: Called with bytestream received/to be sent :param trace_pdu: Called with PDU received/to be sent :param trace_connect: Called when connected/disconnected - :param custom_pdu: list of ModbusPDU custom classes + :param custom_pdu: list of ModbusPdu custom classes """ params = CommParams( comm_type=CommType.SERIAL, diff --git a/pymodbus/server/simulator/http_server.py b/pymodbus/server/simulator/http_server.py index 8157f99d4d..54b24210b8 100644 --- a/pymodbus/server/simulator/http_server.py +++ b/pymodbus/server/simulator/http_server.py @@ -17,8 +17,7 @@ from ...datastore import ModbusServerContext, ModbusSimulatorContext from ...datastore.simulator import CellType, Label from ...logging import Log -from ...pdu import DecodePDU -from ...pdu.device import ModbusDeviceIdentification +from ...pdu import DecodePDU, ModbusDeviceIdentification from ..server import ( ModbusSerialServer, ModbusTcpServer, @@ -377,7 +376,7 @@ def build_html_calls(self, params: dict, html: str) -> str: # pragma: no cover else "" ) function_codes = "" - for function in DecodePDU(True).list_function_codes(): + for function in DecodePDU.pdu_table: selected = "selected" if function == self.call_monitor.function else "" function_codes += ( f"" @@ -546,7 +545,7 @@ def build_json_calls(self, params: dict) -> dict: ) function_codes = [] - for function in DecodePDU(True).list_function_codes(): + for function in DecodePDU.pdu_table: function_codes.append( { "value": function, diff --git a/pymodbus/transaction/transaction.py b/pymodbus/transaction/transaction.py index bbd49a7d43..d4f0420938 100644 --- a/pymodbus/transaction/transaction.py +++ b/pymodbus/transaction/transaction.py @@ -10,7 +10,7 @@ from ..exceptions import ConnectionException, ModbusIOException from ..framer import FramerAscii, FramerBase, FramerRTU from ..logging import Log -from ..pdu import ModbusPDU +from ..pdu import ModbusPdu from ..transport import CommParams, CommType, ModbusProtocol @@ -40,7 +40,7 @@ def __init__( retries: int, is_server: bool, trace_packet: Callable[[bool, bytes], bytes] | None, - trace_pdu: Callable[[bool, ModbusPDU], ModbusPDU] | None, + trace_pdu: Callable[[bool, ModbusPdu], ModbusPdu] | None, trace_connect: Callable[[bool], None] | None, sync_client=None, ) -> None: @@ -69,7 +69,7 @@ def __init__( self.cancelled_requests: dict[ tuple[int, int], tuple[asyncio.Future, float] ] = {} - self.last_pdu: ModbusPDU | None = None + self.last_pdu: ModbusPdu | None = None self.last_addr: tuple | None = None def dummy_trace_packet(self, sending: bool, data: bytes) -> bytes: @@ -77,7 +77,7 @@ def dummy_trace_packet(self, sending: bool, data: bytes) -> bytes: _ = sending return data - def dummy_trace_pdu(self, sending: bool, pdu: ModbusPDU) -> ModbusPDU: + def dummy_trace_pdu(self, sending: bool, pdu: ModbusPdu) -> ModbusPdu: """Do dummy trace.""" _ = sending return pdu @@ -86,7 +86,7 @@ def dummy_trace_connect(self, connect: bool) -> None: """Do dummy trace.""" _ = connect - def sync_get_response(self, dev_id, tid) -> ModbusPDU: + def sync_get_response(self, dev_id, tid) -> ModbusPdu: """Receive until PDU is correct or timeout.""" databuffer = b"" deadline = monotonic() + self.comm_params.timeout_connect @@ -127,7 +127,7 @@ def sync_get_response(self, dev_id, tid) -> ModbusPDU: @staticmethod def _io_exception_from_request( - message: str, request: ModbusPDU + message: str, request: ModbusPdu ) -> ModbusIOException: """Build ModbusIOException from an outstanding request.""" return ModbusIOException( @@ -137,7 +137,7 @@ def _io_exception_from_request( dev_id=request.dev_id, ) - def sync_execute(self, no_response_expected: bool, request: ModbusPDU) -> ModbusPDU: + def sync_execute(self, no_response_expected: bool, request: ModbusPdu) -> ModbusPdu: """Execute requests asynchronously. REMARK: this method is identical to execute, apart from the lock and sync_receive. @@ -169,7 +169,6 @@ def sync_execute(self, no_response_expected: bool, request: ModbusPDU) -> Modbus f"ERROR: request uses transaction id={request.transaction_id} but received {response.transaction_id}.", request, ) - response.retries = count_retries return response except asyncio.exceptions.TimeoutError: count_retries += 1 @@ -186,8 +185,8 @@ def sync_execute(self, no_response_expected: bool, request: ModbusPDU) -> Modbus raise self._io_exception_from_request(txt, request) async def execute( # noqa: C901 - self, no_response_expected: bool, request: ModbusPDU - ) -> ModbusPDU: + self, no_response_expected: bool, request: ModbusPdu + ) -> ModbusPdu: """Execute requests asynchronously. REMARK: this method is identical to sync_execute, apart from the lock, try/except @@ -213,7 +212,7 @@ async def execute( # noqa: C901 if no_response_expected: return None # type: ignore[return-value] try: - response: ModbusPDU = await asyncio.wait_for( + response: ModbusPdu = await asyncio.wait_for( self.response_future, timeout=self.comm_params.timeout_connect ) self.count_until_disconnect = self.max_until_disconnect @@ -232,7 +231,6 @@ async def execute( # noqa: C901 f"ERROR: request uses transaction id={request.transaction_id} but received {response.transaction_id}.", request, ) - response.retries = count_retries return response except asyncio.exceptions.TimeoutError: count_retries += 1 @@ -261,7 +259,7 @@ async def execute( # noqa: C901 Log.error(txt) raise self._io_exception_from_request(txt, request) - def pdu_send(self, pdu: ModbusPDU, addr: tuple | None = None) -> None: + def pdu_send(self, pdu: ModbusPdu, addr: tuple | None = None) -> None: """Build byte stream and send.""" if not self.is_server: self.request_dev_id = pdu.dev_id diff --git a/test/client/test_client.py b/test/client/test_client.py index b3bd82a588..03ba3d5875 100755 --- a/test/client/test_client.py +++ b/test/client/test_client.py @@ -19,7 +19,7 @@ from pymodbus.client.base import ModbusBaseClient from pymodbus.client.mixin import ModbusClientMixin from pymodbus.exceptions import ConnectionException, ModbusException -from pymodbus.pdu import DecodePDU, ModbusPDU +from pymodbus.pdu import DecodePDU, ModbusPdu from pymodbus.transport import CommParams, CommType @@ -47,78 +47,78 @@ class TestMixin: @pytest.mark.parametrize( ("method", "arg", "pdu_request"), [ - ("read_coils", 1, pdu_bit.ReadCoilsRequest), - ("read_discrete_inputs", 1, pdu_bit.ReadDiscreteInputsRequest), - ("read_holding_registers", 1, pdu_reg.ReadHoldingRegistersRequest), - ("read_input_registers", 1, pdu_reg.ReadInputRegistersRequest), - ("write_coil", 2, pdu_bit.WriteSingleCoilRequest), - ("write_register", 2, pdu_reg.WriteSingleRegisterRequest), - ("read_exception_status", 0, pdu_other_msg.ReadExceptionStatusRequest), - ("diag_query_data", 3, pdu_diag.ReturnQueryDataRequest), + ("read_coils", 1, pdu_bit.PDUreadCoils), + ("read_discrete_inputs", 1, pdu_bit.PDUreadDiscreteInputs), + ("read_holding_registers", 1, pdu_reg.PDUreadHoldingRegisters), + ("read_input_registers", 1, pdu_reg.PDUreadInputRegisters), + ("write_coil", 2, pdu_bit.PDUwriteSingleCoil), + ("write_register", 2, pdu_reg.PDUwriteSingleRegister), + ("read_exception_status", 0, pdu_other_msg.PDUreadExceptionStatus), + ("diag_query_data", 3, pdu_diag.PDUreturnQueryData), ( "diag_restart_communication", 4, - pdu_diag.RestartCommunicationsOptionRequest, + pdu_diag.PDUrestartCommunicationsOption, ), ( "diag_read_diagnostic_register", 0, - pdu_diag.ReturnDiagnosticRegisterRequest, + pdu_diag.PDUreturnDiagnosticRegister, ), ( "diag_change_ascii_input_delimeter", 0, - pdu_diag.ChangeAsciiInputDelimiterRequest, + pdu_diag.PDUchangeAsciiInputDelimiter, ), - ("diag_force_listen_only", 0, pdu_diag.ForceListenOnlyModeRequest), - ("diag_clear_counters", 0, pdu_diag.ClearCountersRequest), - ("diag_read_bus_message_count", 0, pdu_diag.ReturnBusMessageCountRequest), + ("diag_force_listen_only", 0, pdu_diag.PDUforceListenOnlyMode), + ("diag_clear_counters", 0, pdu_diag.PDUclearCounters), + ("diag_read_bus_message_count", 0, pdu_diag.PDUreturnBusMessageCount), ( "diag_read_bus_comm_error_count", 0, - pdu_diag.ReturnBusCommunicationErrorCountRequest, + pdu_diag.PDUreturnBusCommunicationErrorCount, ), ( "diag_read_bus_exception_error_count", 0, - pdu_diag.ReturnBusExceptionErrorCountRequest, + pdu_diag.PDUreturnBusExceptionErrorCount, ), ( "diag_read_device_message_count", 0, - pdu_diag.ReturnDeviceMessageCountRequest, + pdu_diag.PDUreturnDeviceMessageCount, ), ( "diag_read_device_no_response_count", 0, - pdu_diag.ReturnDeviceNoResponseCountRequest, + pdu_diag.PDUreturnDeviceNoResponseCount, ), - ("diag_read_device_nak_count", 0, pdu_diag.ReturnDeviceNAKCountRequest), - ("diag_read_device_busy_count", 0, pdu_diag.ReturnDeviceBusyCountRequest), + ("diag_read_device_nak_count", 0, pdu_diag.PDUreturnDeviceNAKCount), + ("diag_read_device_busy_count", 0, pdu_diag.PDUreturnDeviceBusyCount), ( "diag_read_bus_char_overrun_count", 0, - pdu_diag.ReturnDeviceBusCharacterOverrunCountRequest, + pdu_diag.PDUreturnDeviceBusCharacterOverrunCount, ), - ("diag_read_iop_overrun_count", 0, pdu_diag.ReturnIopOverrunCountRequest), - ("diag_clear_overrun_counter", 0, pdu_diag.ClearOverrunCountRequest), - ("diag_getclear_modbus_response", 0, pdu_diag.GetClearModbusPlusRequest), + ("diag_read_iop_overrun_count", 0, pdu_diag.PDUreturnIopOverrunCount), + ("diag_clear_overrun_counter", 0, pdu_diag.PDUclearOverrunCount), + ("diag_getclear_modbus_response", 0, pdu_diag.PDUgetClearModbusPlus), ( "diag_get_comm_event_counter", 0, - pdu_other_msg.GetCommEventCounterRequest, - ), - ("diag_get_comm_event_log", 0, pdu_other_msg.GetCommEventLogRequest), - ("write_coils", 5, pdu_bit.WriteMultipleCoilsRequest), - ("write_registers", 6, pdu_reg.WriteMultipleRegistersRequest), - ("readwrite_registers", 0, pdu_reg.ReadWriteMultipleRegistersRequest), - ("readwrite_registers", 6, pdu_reg.ReadWriteMultipleRegistersRequest), - ("mask_write_register", 1, pdu_reg.MaskWriteRegisterRequest), - ("report_device_id", 0, pdu_other_msg.ReportDeviceIdRequest), - ("read_file_record", 7, pdu_file_msg.ReadFileRecordRequest), - ("write_file_record", 7, pdu_file_msg.WriteFileRecordRequest), - ("read_fifo_queue", 1, pdu_file_msg.ReadFifoQueueRequest), - ("read_device_information", 0, pdu_mei.ReadDeviceInformationRequest), + pdu_other_msg.PDUgetCommEventCounter, + ), + ("diag_get_comm_event_log", 0, pdu_other_msg.PDUgetCommEventLog), + ("write_coils", 5, pdu_bit.PDUwriteMultipleCoils), + ("write_registers", 6, pdu_reg.PDUwriteMultipleRegisters), + ("readwrite_registers", 0, pdu_reg.PDUreadWriteMultipleRegisters), + ("readwrite_registers", 6, pdu_reg.PDUreadWriteMultipleRegisters), + ("mask_write_register", 1, pdu_reg.PDUmaskWriteRegister), + ("report_device_id", 0, pdu_other_msg.PDUreportDeviceId), + ("read_file_record", 7, pdu_file_msg.PDUreadFileRecord), + ("write_file_record", 7, pdu_file_msg.PDUwriteFileRecord), + ("read_fifo_queue", 1, pdu_file_msg.PDUreadFifoQueue), + ("read_device_information", 0, pdu_mei.PDUreadDeviceInformation), ], ) def test_client_mixin(self, arglist, method, arg, pdu_request): @@ -136,7 +136,7 @@ def fake_execute(_self, _no_response_expected, request): def test_client_mixin_write_sequence(self): """Write methods must accept any Sequence of values, e.g. a tuple (#2957).""" - requests: list[ModbusPDU] = [] + requests: list[ModbusPdu] = [] def fake_execute(_self, _no_response_expected, request): """Collect PDU requests.""" @@ -475,7 +475,7 @@ def test_client_mixin_convert_datatype_fail(self): def test_client_mixin_execute(self): """Test mixin execute.""" a = ModbusClientMixin() - a.execute(False, cast(ModbusPDU, None)) + a.execute(False, cast(ModbusPdu, None)) class TestClientBase: @@ -620,7 +620,7 @@ async def test_client_modbusbaseclient(self): None, None, ) - client.register(pdu_bit.ReadCoilsResponse) + client.register(pdu_bit.PDUreadCoils) assert str(client) client.set_max_no_responses(110) client.close() @@ -794,7 +794,7 @@ def test_client_tls_connect2(self): def test_tcp_client_register(self): """Test tcp client.""" - class CustomRequest(ModbusPDU): # pylint: disable=too-few-public-methods + class CustomRequest(ModbusPdu): # pylint: disable=too-few-public-methods """Dummy custom request.""" function_code = 79 diff --git a/test/conftest.py b/test/conftest.py index 32ec0bbf22..333ebd8b09 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -11,7 +11,6 @@ import pytest_asyncio from pymodbus.datastore import ModbusDeviceContext, ModbusServerContext -from pymodbus.pdu import ModbusPDU from pymodbus.server import ServerAsyncStop from pymodbus.transport import NULLMODEM_HOST, CommParams, CommType from pymodbus.transport.transport import NullModem @@ -327,28 +326,3 @@ def setblocking(self, _flag): return None return DummySocket - - -@pytest.fixture(name="mockPDU") -def define_dummyPDU(): - """Define mockSocket class.""" - - class DummyPDU(ModbusPDU): # pragma: no cover - """Custom request.""" - - function_code = 0x70 - - @classmethod - def x_calculateRtuFrameSize(cls, data: bytes) -> int: - """Calculate the size of a PDU.""" - return 5 - - def x_encode(self) -> bytes: - """Encode the message.""" - return self.function_code.to_bytes(1, "big") + b"123" - - def x_decode(self, data): - """Decode.""" - _ = data - - return DummyPDU diff --git a/test/examples/test_examples.py b/test/examples/test_examples.py index a31ed73251..d8abd592b6 100755 --- a/test/examples/test_examples.py +++ b/test/examples/test_examples.py @@ -30,7 +30,7 @@ from examples.simple_sync_client import run_sync_simple_client from examples.simulator import run_simulator as run_simulator3 from pymodbus.exceptions import ModbusException -from pymodbus.pdu import ExceptionResponse +from pymodbus.pdu import PDUexception from pymodbus.server import ServerAsyncStop, ServerStop @@ -142,7 +142,9 @@ async def test_client_async_calls_errors(self, mock_server): with pytest.raises(ModbusException): await run_async_client(client, modbus_calls=async_template_call) client.close() - client.read_coils = mock.AsyncMock(return_value=ExceptionResponse(0x05, 0x10)) # type: ignore[method-assign] + client.read_coils = mock.AsyncMock( # type: ignore[method-assign] + return_value=PDUexception(0, 0x05, 0x10) + ) # type: ignore[method-assign] with pytest.raises(ModbusException): await run_async_client(client, modbus_calls=async_template_call) client.close() @@ -154,7 +156,9 @@ async def test_client_calls_errors(self, mock_server): with pytest.raises(ModbusException): await run_async_client(client, modbus_calls=async_template_call) client.close() - client.read_coils = mock.Mock(return_value=ExceptionResponse(0x05, 0x10)) # type: ignore[method-assign] + client.read_coils = mock.Mock( # type: ignore[method-assign] + return_value=PDUexception(0, 0x05, 0x10) + ) # type: ignore[method-assign] with pytest.raises(ModbusException): await run_async_client(client, modbus_calls=template_call) client.close() diff --git a/test/framer/generator.py b/test/framer/generator.py index c28f8f0e0a..fa5cc36264 100755 --- a/test/framer/generator.py +++ b/test/framer/generator.py @@ -8,11 +8,8 @@ FramerSocket, FramerTLS, ) -from pymodbus.pdu import DecodePDU, ExceptionResponse -from pymodbus.pdu.register_message import ( - ReadHoldingRegistersRequest, - ReadHoldingRegistersResponse, -) +from pymodbus.pdu import DecodePDU, PDUexception +from pymodbus.pdu.register_message import PDUreadHoldingRegisters def set_calls(): @@ -24,22 +21,20 @@ def set_calls(): for tid in (0, 3077): print(f" tid --> {tid}") client = framer(DecodePDU(False)) - request = ReadHoldingRegistersRequest( - address=124, count=2, dev_id=dev_id - ) + request = PDUreadHoldingRegisters(address=124, count=2, dev_id=dev_id) request.transaction_id = tid result = client.buildFrame(request) print(f" request --> {result}") print(f" request --> {result.hex()}") server = framer(DecodePDU(True)) - response = ReadHoldingRegistersResponse(registers=[141, 142]) + response = PDUreadHoldingRegisters(0, registers=[141, 142]) response.dev_id = dev_id response.transaction_id = tid result = server.buildFrame(response) print(f" response --> {result}") print(f" response --> {result.hex()}") - exception = ExceptionResponse( - request.function_code, ExcCodes.ILLEGAL_ADDRESS + exception = PDUexception( + 0, request.function_code, ExcCodes.ILLEGAL_ADDRESS ) exception.transaction_id = tid exception.dev_id = dev_id diff --git a/test/framer/test_extras.py b/test/framer/test_extras.py index c74dc48b9f..7307e32c72 100755 --- a/test/framer/test_extras.py +++ b/test/framer/test_extras.py @@ -11,8 +11,7 @@ FramerSocket, FramerTLS, ) -from pymodbus.pdu import DecodePDU -from pymodbus.pdu.device import ModbusControlBlock +from pymodbus.pdu import DecodePDU, ModbusControlBlock TEST_MESSAGE = b"\x7b\x01\x03\x00\x00\x00\x05\x85\xc9\x7d" @@ -120,7 +119,8 @@ def test_rtu_calculate(self): """Test rtu process incoming packets.""" msg = b"\x00\x01\x00\x00\x00\x01\xfc\x1b" with patch( - "pymodbus.pdu.ReadCoilsRequest.x_calculateRtuFrameSize", return_value=0 + "pymodbus.pdu.bit_message.PDUreadCoils.calculatePduSize", + return_value=0, ): used_len, pdu = self._rtu.handleFrame(msg, 0, 0) assert not used_len diff --git a/test/framer/test_framer.py b/test/framer/test_framer.py index ece10034c4..9544c13696 100644 --- a/test/framer/test_framer.py +++ b/test/framer/test_framer.py @@ -13,12 +13,11 @@ FramerTLS, FramerType, ) -from pymodbus.pdu import DecodePDU -from pymodbus.pdu.bit_message import WriteMultipleCoilsRequest -from pymodbus.pdu.device import ModbusControlBlock +from pymodbus.pdu import DecodePDU, ModbusControlBlock +from pymodbus.pdu.bit_message import PDUwriteMultipleCoils from pymodbus.pdu.register_message import ( - ReadWriteMultipleRegistersRequest, - WriteMultipleRegistersRequest, + PDUreadWriteMultipleRegisters, + PDUwriteMultipleRegisters, ) from test.framer.generator import set_calls @@ -863,11 +862,11 @@ def test_handleFrame2(self, test_framer, msg): @pytest.mark.parametrize( "request_pdu", [ - WriteMultipleCoilsRequest(address=1, bits=[True, False, True], dev_id=1), - WriteMultipleRegistersRequest(address=1, registers=[10, 11], dev_id=1), - ReadWriteMultipleRegistersRequest( + PDUwriteMultipleCoils(1, address=1, bits=[True, False, True]), + PDUwriteMultipleRegisters(address=1, registers=[10, 11], dev_id=1), + PDUreadWriteMultipleRegisters( read_address=1, - read_count=2, + count=2, write_address=3, write_registers=[4, 5], dev_id=1, @@ -947,7 +946,7 @@ def test_handleFrame_roundtrip(self, entry, test_framer, msg, dev_id, tid, half) ) def test_framer_encode(self, test_framer, msg, mockPDU): """Test a tcp frame transaction.""" - with mock.patch.object(mockPDU, "x_encode") as mock_encode: + with mock.patch.object(mockPDU, "to_bytes") as mock_encode: message = mockPDU() message.transaction_id = 0x0001 message.dev_id = 0xFF diff --git a/test/pdu/conftest.py b/test/pdu/conftest.py new file mode 100644 index 0000000000..68d607cc3d --- /dev/null +++ b/test/pdu/conftest.py @@ -0,0 +1,550 @@ +"""Configure pytest.""" + +import pytest + +import pymodbus.pdu.bit_message as pdu_bit +import pymodbus.pdu.diag_message as pdu_diag +import pymodbus.pdu.file_message as pdu_file +import pymodbus.pdu.mei_message as pdu_mei +import pymodbus.pdu.other_message as pdu_other +import pymodbus.pdu.register_message as pdu_reg + + +@pytest.fixture( + params=[ + ( # 0 + pdu_bit.PDUreadCoils, + "read_coils", + ( + {"address": 117, "count": 3}, + b"\x01\x00\x75\x00\x03", + {"bits": [True, True] + [False] * 6}, + b"\x01\x01\x03", + ), + ), + ( # 1 + pdu_bit.PDUwriteSingleCoil, + "write_coil", + ( + {"address": 117, "bits": [True]}, + b"\x05\x00\x75\xff\x00", + {"address": 117, "bits": [True]}, + b"\x05\x00\x75\xff\x00", + ), + ), + ( # 2 + pdu_bit.PDUwriteMultipleCoils, + "write_coils", + ( + {"address": 117, "bits": [True, False, True]}, + b"\x0f\x00\x75\x00\x03\x01\x05", + {"address": 117, "count": 3}, + b"\x0f\x00\x75\x00\x03", + ), + ), + ( # 3 + pdu_bit.PDUreadDiscreteInputs, + "read_discrete_inputs", + ( + {"address": 117, "count": 3}, + b"\x02\x00\x75\x00\x03", + {"bits": [True, True]}, + b"\x02\x01\x03", + ), + ), + ( # 4 + pdu_reg.PDUreadHoldingRegisters, + "read_holding_registers", + ( + {"address": 117, "count": 3}, + b"\x03\x00\x75\x00\x03", + {"registers": [3, 17]}, + b"\x03\x04\x00\x03\x00\x11", + ), + ), + ( # 5 + pdu_reg.PDUreadInputRegisters, + "read_input_registers", + ( + {"address": 117, "count": 3}, + b"\x04\x00\x75\x00\x03", + {"registers": [3, 17]}, + b"\x04\x04\x00\x03\x00\x11", + ), + ), + ( # 6 + pdu_reg.PDUwriteSingleRegister, + "write_register", + ( + {"address": 117, "registers": [112]}, + b"\x06\x00\x75\x00\x70", + {"address": 117, "registers": [112]}, + b"\x06\x00\x75\x00\x70", + ), + ), + ( # 7 + pdu_reg.PDUwriteMultipleRegisters, + "write_registers", + ( + {"address": 117, "registers": [111, 121, 131]}, + b"\x10\x00\x75\x00\x03\x06\x00\x6f\x00\x79\x00\x83", + {"address": 117, "count": 3}, + b"\x10\x00\x75\x00\x03", + ), + ), + ( # 8 + pdu_reg.PDUreadWriteMultipleRegisters, + "readwrite_registers", + ( + { + "address": 17, + "count": 2, + "write_address": 25, + "write_registers": [111, 112], + }, + b"\x17\x00\x11\x00\x02\x00\x19\x00\x02\x04\x00\x6f\x00\x70", + {"registers": [1, 2]}, + b"\x17\x04\x00\x01\x00\x02", + ), + ), + ( # 9 + pdu_reg.PDUmaskWriteRegister, + "mask_write_register", + ( + {"address": 0x0104, "and_mask": 0xE1D2, "or_mask": 0x1234}, + b"\x16\x01\x04\xe1\xd2\x12\x34", + {"address": 0x0104, "and_mask": 0xE1D2, "or_mask": 0x1234}, + b"\x16\x01\x04\xe1\xd2\x12\x34", + ), + ), + ( # 10 + pdu_file.PDUreadFileRecord, + "read_file_record", + ( + {"records": [pdu_file.FileRecord(), pdu_file.FileRecord()]}, + b"\x14\x0e\x06\x00\x00\x00\x00\x00\x00\x06\x00\x00\x00\x00\x00\x00", + {"records": [pdu_file.FileRecord(), pdu_file.FileRecord()]}, + b"\x14\x04\x01\x06\x01\x06", + ), + ), + ( # 11 + pdu_file.PDUwriteFileRecord, + "write_file_record", + ( + {"records": [pdu_file.FileRecord(), pdu_file.FileRecord()]}, + b"\x15\x0e\x06\x00\x00\x00\x00\x00\x00\x06\x00\x00\x00\x00\x00\x00", + {"records": [pdu_file.FileRecord(), pdu_file.FileRecord()]}, + b"\x15\x0e\x06\x00\x00\x00\x00\x00\x00\x06\x00\x00\x00\x00\x00\x00", + ), + ), + ( # 12 + pdu_file.PDUreadFifoQueue, + "read_fifo_queue", + ( + {"address": 117}, + b"\x18\x00\x75", + {"values": [123, 456]}, + b"\x18\x00\x06\x00\x02\x00\x7b\x01\xc8", + ), + ), + ( # 13 + pdu_mei.PDUreadDeviceInformation, + "read_device_information", + ( + {"read_code": 0x01, "object_id": 0x02}, + b"\x2b\x0e\x01\x02", + {"read_code": 0x01}, + b"\x2b\x0e\x01\x83\x00\x00\x00", + ), + ), + ( # 14 + pdu_other.PDUreadExceptionStatus, + "read_exception_status", + ({}, b"\x07", {"status": 0x23}, b"\x07\x23"), + ), + ( # 15 + pdu_other.PDUgetCommEventCounter, + "diag_get_comm_event_counter", + ({}, b"\x0b", {"status": 1, "count": 1}, b"\x0b\x00\x00\x00\x01"), + ), + ( # 16 + pdu_other.PDUgetCommEventLog, + "diag_get_comm_event_log", + ( + {}, + b"\x0c", + { + "status": True, + "message_count": 12, + "event_count": 7, + "events": [12, 14], + }, + b"\x0c\x08\x00\x00\x00\x07\x00\x0c\x0c\x0e", + ), + ), + ( # 17 + pdu_other.PDUreportDeviceId, + "report_device_id", + ( + {}, + b"\x11", + {"identifier": b"\x12", "status": True}, + b"\x11\x02\x12\xff", + ), + ), + ( # 18 + pdu_diag.PDUreturnQueryData, + "diag_query_data", + ( + {}, + b"\x08\x00\x00\x00\x00", + {}, + b"\x08\x00\x00\x00\x00", + ), + ), + ( # 19 + pdu_diag.PDUreturnQueryData, + "diag_query_data", + ( + {"message": b"AB"}, + b"\x08\x00\x00\x41\x42", + {"message": b"AB"}, + b"\x08\x00\x00\x41\x42", + ), + ), + ( # 20 + pdu_diag.PDUrestartCommunicationsOption, + "diag_restart_communication", + ( + {}, + b"\x08\x00\x01\x00\x00", + {}, + b"\x08\x00\x01\x00\x00", + ), + ), + ( # 21 + pdu_diag.PDUrestartCommunicationsOption, + "diag_restart_communication", + ( + {"message": b"\xff\x00"}, + b"\x08\x00\x01\xff\x00", + {"message": b"\xff\x00"}, + b"\x08\x00\x01\xff\x00", + ), + ), + ( # 22 + pdu_diag.PDUrestartCommunicationsOption, + "diag_restart_communication", + ( + {"message": b"\x00\x00"}, + b"\x08\x00\x01\x00\x00", + {"message": b"\x00\x00"}, + b"\x08\x00\x01\x00\x00", + ), + ), + ( # 23 + pdu_diag.PDUreturnDiagnosticRegister, + "diag_read_diagnostic_register", + ( + {}, + b"\x08\x00\x02\x00\x00", + {}, + b"\x08\x00\x02\x00\x00", + ), + ), + ( # 24 + pdu_diag.PDUreturnDiagnosticRegister, + "diag_read_diagnostic_register", + ( + {"message": b"\x10\x10"}, + b"\x08\x00\x02\x10\x10", + {"message": b"\x10\x10"}, + b"\x08\x00\x02\x10\x10", + ), + ), + ( # 25 + pdu_diag.PDUchangeAsciiInputDelimiter, + "diag_change_ascii_input_delimeter", + ( + {}, + b"\x08\x00\x03\x00\x00", + {}, + b"\x08\x00\x03\x00\x00", + ), + ), + ( # 26 + pdu_diag.PDUchangeAsciiInputDelimiter, + "diag_change_ascii_input_delimeter", + ( + {"message": b"A\x00"}, + b"\x08\x00\x03\x41\x00", + {"message": b"A\x00"}, + b"\x08\x00\x03\x41\x00", + ), + ), + ( # 27 + pdu_diag.PDUchangeAsciiInputDelimiter, + "diag_change_ascii_input_delimeter", + ( + {"message": b"\n\x00"}, + b"\x08\x00\x03\x0a\x00", + {"message": b"\n\x00"}, + b"\x08\x00\x03\x0a\x00", + ), + ), + ( # 28 + pdu_diag.PDUforceListenOnlyMode, + "diag_force_listen_only", + ({}, b"\x08\x00\x04\x00\x00", {}, b"\x08\x00\x04\x00\x00"), + ), + ( # 29 + pdu_diag.PDUforceListenOnlyMode, + "diag_force_listen_only", + ( + {"message": b"\x10\x10"}, + b"\x08\x00\x04\x10\x10", + {"message": b"\x10\x10"}, + b"\x08\x00\x04\x10\x10", + ), + ), + ( # 30 + pdu_diag.PDUclearCounters, + "diag_clear_counters", + ({}, b"\x08\x00\x0a\x00\x00", {}, b"\x08\x00\n\x00\x00"), + ), + ( # 31 + pdu_diag.PDUclearCounters, + "diag_clear_counters", + ( + {"message": b"\x10\x10"}, + b"\x08\x00\x0a\x10\x10", + {"message": b"\x10\x10"}, + b"\x08\x00\n\x10\x10", + ), + ), + ( # 32 + pdu_diag.PDUreturnBusMessageCount, + "diag_read_bus_message_count", + ( + {}, + b"\x08\x00\x0b\x00\x00", + {}, + b"\x08\x00\x0b\x00\x00", + ), + ), + ( # 33 + pdu_diag.PDUreturnBusMessageCount, + "diag_read_bus_message_count", + ( + {"message": b"\x10\x10"}, + b"\x08\x00\x0b\x10\x10", + {"message": b"\x10\x10"}, + b"\x08\x00\x0b\x10\x10", + ), + ), + ( # 34 + pdu_diag.PDUreturnBusCommunicationErrorCount, + "diag_read_bus_comm_error_count", + ( + {}, + b"\x08\x00\x0c\x00\x00", + {}, + b"\x08\x00\x0c\x00\x00", + ), + ), + ( # 35 + pdu_diag.PDUreturnBusCommunicationErrorCount, + "diag_read_bus_comm_error_count", + ( + {"message": b"\x10\x10"}, + b"\x08\x00\x0c\x10\x10", + {"message": b"\x10\x10"}, + b"\x08\x00\x0c\x10\x10", + ), + ), + ( # 36 + pdu_diag.PDUreturnBusExceptionErrorCount, + "diag_read_bus_exception_error_count", + ( + {}, + b"\x08\x00\x0d\x00\x00", + {}, + b"\x08\x00\x0d\x00\x00", + ), + ), + ( # 37 + pdu_diag.PDUreturnBusExceptionErrorCount, + "diag_read_bus_exception_error_count", + ( + {"message": b"\x10\x10"}, + b"\x08\x00\x0d\x10\x10", + {"message": b"\x10\x10"}, + b"\x08\x00\x0d\x10\x10", + ), + ), + ( # 38 + pdu_diag.PDUreturnDeviceMessageCount, + "diag_read_device_message_count", + ( + {}, + b"\x08\x00\x0e\x00\x00", + {}, + b"\x08\x00\x0e\x00\x00", + ), + ), + ( # 39 + pdu_diag.PDUreturnDeviceMessageCount, + "diag_read_device_message_count", + ( + {"message": b"\x10\x10"}, + b"\x08\x00\x0e\x10\x10", + {"message": b"\x10\x10"}, + b"\x08\x00\x0e\x10\x10", + ), + ), + ( # 40 + pdu_diag.PDUreturnDeviceNoResponseCount, + "diag_read_device_no_response_count", + ( + {}, + b"\x08\x00\x0f\x00\x00", + {}, + b"\x08\x00\x0f\x00\x00", + ), + ), + ( # 41 + pdu_diag.PDUreturnDeviceNoResponseCount, + "diag_read_device_no_response_count", + ( + {"message": b"\x10\x10"}, + b"\x08\x00\x0f\x10\x10", + {"message": b"\x10\x10"}, + b"\x08\x00\x0f\x10\x10", + ), + ), + ( # 42 + pdu_diag.PDUreturnDeviceNAKCount, + "diag_read_device_nak_count", + ( + {}, + b"\x08\x00\x10\x00\x00", + {}, + b"\x08\x00\x10\x00\x00", + ), + ), + ( # 43 + pdu_diag.PDUreturnDeviceNAKCount, + "diag_read_device_nak_count", + ( + {"message": b"\x10\x10"}, + b"\x08\x00\x10\x10\x10", + {"message": b"\x10\x10"}, + b"\x08\x00\x10\x10\x10", + ), + ), + ( # 44 + pdu_diag.PDUreturnDeviceBusyCount, + "diag_read_device_busy_count", + ( + {}, + b"\x08\x00\x11\x00\x00", + {}, + b"\x08\x00\x11\x00\x00", + ), + ), + ( # 45 + pdu_diag.PDUreturnDeviceBusyCount, + "diag_read_device_busy_count", + ( + {"message": b"\x10\x10"}, + b"\x08\x00\x11\x10\x10", + {"message": b"\x10\x10"}, + b"\x08\x00\x11\x10\x10", + ), + ), + ( # 46 + pdu_diag.PDUreturnDeviceBusCharacterOverrunCount, + "diag_read_bus_char_overrun_count", + ( + {}, + b"\x08\x00\x12\x00\x00", + {}, + b"\x08\x00\x12\x00\x00", + ), + ), + ( # 47 + pdu_diag.PDUreturnDeviceBusCharacterOverrunCount, + "diag_read_bus_char_overrun_count", + ( + {"message": b"\x10\x10"}, + b"\x08\x00\x12\x10\x10", + {"message": b"\x10\x10"}, + b"\x08\x00\x12\x10\x10", + ), + ), + ( # 48 + pdu_diag.PDUreturnIopOverrunCount, + "diag_read_iop_overrun_count", + ( + {}, + b"\x08\x00\x13\x00\x00", + {}, + b"\x08\x00\x13\x00\x00", + ), + ), + ( # 49 + pdu_diag.PDUreturnIopOverrunCount, + "diag_read_iop_overrun_count", + ( + {"message": b"\x10\x10"}, + b"\x08\x00\x13\x10\x10", + {"message": b"\x10\x10"}, + b"\x08\x00\x13\x10\x10", + ), + ), + ( # 50 + pdu_diag.PDUclearOverrunCount, + "diag_clear_overrun_counter", + ( + {}, + b"\x08\x00\x14\x00\x00", + {}, + b"\x08\x00\x14\x00\x00", + ), + ), + ( # 51 + pdu_diag.PDUclearOverrunCount, + "diag_clear_overrun_counter", + ( + {"message": b"\x10\x10"}, + b"\x08\x00\x14\x10\x10", + {"message": b"\x10\x10"}, + b"\x08\x00\x14\x10\x10", + ), + ), + ( # 52 + pdu_diag.PDUgetClearModbusPlus, + "diag_getclear_modbus_response", + ( + {}, + b"\x08\x00\x15\x00\x00", + {}, + b"\x08\x00\x15\x00\x00", + ), + ), + ( # 53 + pdu_diag.PDUgetClearModbusPlus, + "diag_getclear_modbus_response", + ( + {"message": b"\x10\x10"}, + b"\x08\x00\x15\x10\x10", + {"message": b"\x10\x10"}, + b"\x08\x00\x15\x10\x10", + ), + ), + ], +) +def paramPDU(request): + """Define all requests/responses for use in general tests.""" + pdu, api, entry = request.param + return pdu, api, entry[0], entry[1], entry[2], entry[3] diff --git a/test/pdu/test_bit.py b/test/pdu/test_bit.py index ff8748b98c..c779344f68 100644 --- a/test/pdu/test_bit.py +++ b/test/pdu/test_bit.py @@ -6,7 +6,8 @@ import pymodbus.pdu.bit_message as bit_msg from pymodbus.constants import ExcCodes -from pymodbus.pdu.decoders import DecodePDU +from pymodbus.exceptions import ModbusIOException +from pymodbus.pdu import DecodePDU class TestModbusBitMessage: @@ -17,75 +18,67 @@ def test_bit_read_base_response_encoding(self): for i in range(1, 20): data = [True] * i expected = data + [False] * (-len(data) % 8) - pdu = bit_msg.ReadCoilsResponse(bits=data) - pdu.x_decode(pdu.x_encode()) + pdu = bit_msg.PDUreadCoils(0, is_request=False, bits=data) + pdu.from_bytes(0, pdu.to_bytes()) assert pdu.bits == expected def test_bit_read_base_requests(self): """Test bit read request encoding.""" for pdu, expected in ( - (bit_msg.ReadCoilsRequest(address=12, count=14), b"\x00\x0c\x00\x0e"), + (bit_msg.PDUreadCoils(0, address=12, count=14), b"\x00\x0c\x00\x0e"), ( - bit_msg.ReadCoilsResponse(bits=[True, False, True, True, False]), + bit_msg.PDUreadCoils( + 0, is_request=False, bits=[True, False, True, True, False] + ), b"\x01\x0d", ), ): - assert pdu.x_encode() == expected + assert pdu.to_bytes() == expected - async def test_bit_read_datastore_update_value_errors(self, mock_server_context): + async def test_bit_read_build_response_value_errors(self): """Test bit read request encoding.""" - context = mock_server_context() for pdu in ( - (bit_msg.ReadCoilsRequest(address=1, count=0x800)), - (bit_msg.ReadDiscreteInputsRequest(address=1, count=0x800)), + (bit_msg.PDUreadCoils(1, address=1, count=1999)), + (bit_msg.PDUreadDiscreteInputs(0, address=1, count=1999)), ): - await pdu.datastore_update(context, 1) + await pdu.build_response( + mock.AsyncMock(return_value=[True] * 1999), + mock.AsyncMock(return_value=0), + ) @pytest.mark.parametrize( ("request_class", "function_code"), [ - (bit_msg.ReadCoilsRequest, 1), - (bit_msg.ReadDiscreteInputsRequest, 2), + (bit_msg.PDUreadCoils, 1), + (bit_msg.PDUreadDiscreteInputs, 2), ], ) async def test_bit_read_count_above_max_returns_illegal_value( - self, request_class, function_code, mock_server_context + self, request_class, function_code ): """An out-of-range bit read quantity must answer 0x03, not silence.""" - context = mock_server_context() - context.async_getValues = mock.AsyncMock(return_value=[True] * 10) - request = request_class(address=0, count=request_class.MAX_COUNT + 1) - result = await request.datastore_update(context, 1) - assert result.function_code == function_code | 0x80 - assert result.exception_code == ExcCodes.ILLEGAL_VALUE - context.async_getValues.assert_not_awaited() + with pytest.raises(ModbusIOException): + request_class(0, address=0, count=request_class.MAX_COUNT + 1) @pytest.mark.parametrize( "request_class", - [bit_msg.ReadCoilsRequest, bit_msg.ReadDiscreteInputsRequest], + [bit_msg.PDUreadCoils, bit_msg.PDUreadDiscreteInputs], ) - async def test_bit_read_count_zero_returns_illegal_value( - self, request_class, mock_server_context - ): + async def test_bit_read_count_zero_returns_illegal_value(self, request_class): """A zero bit read quantity must answer 0x03.""" - context = mock_server_context() - context.async_getValues = mock.AsyncMock(return_value=[True] * 10) - result = await request_class(address=0, count=0).datastore_update(context, 1) - assert result.function_code == request_class.function_code | 0x80 - assert result.exception_code == ExcCodes.ILLEGAL_VALUE - context.async_getValues.assert_not_awaited() + with pytest.raises(ModbusIOException): + request_class(1, address=0, count=0) @pytest.mark.parametrize( "request_class", - [bit_msg.ReadCoilsRequest, bit_msg.ReadDiscreteInputsRequest], + [bit_msg.PDUreadCoils, bit_msg.PDUreadDiscreteInputs], ) - async def test_bit_read_count_at_max_returns_normal_response( - self, request_class, mock_server_context - ): + async def test_bit_read_count_at_max_returns_normal_response(self, request_class): """A count exactly at the maximum must be accepted, not rejected.""" - context = mock_server_context() - request = request_class(address=0, count=request_class.MAX_COUNT) - result = await request.datastore_update(context, 1) + request = request_class(1, address=0, count=request_class.MAX_COUNT) + result = await request.build_response( + mock.AsyncMock(return_value=0), mock.AsyncMock(return_value=0) + ) assert result.function_code == request_class.function_code def test_bit_read_decode_accepts_count_above_max(self): @@ -98,78 +91,70 @@ def test_bit_read_decode_accepts_count_above_max(self): pdu = decoder.decode(frame) assert pdu.count == expected_count - async def test_bit_datastore_exceptions(self, mock_server_context): + async def test_bit_datastore_exceptions(self): """Test bit exception response from datastore.""" - context = mock_server_context() - context.async_getValues = mock.AsyncMock(return_value=ExcCodes.ILLEGAL_VALUE) for pdu in ( - (bit_msg.ReadCoilsRequest(address=1, count=5)), - (bit_msg.ReadDiscreteInputsRequest(address=1, count=5)), - (bit_msg.WriteSingleCoilRequest(address=1, bits=[True])), - (bit_msg.WriteMultipleCoilsRequest(address=1, bits=[True] * 5)), + (bit_msg.PDUreadCoils(0, address=1, count=5)), + (bit_msg.PDUreadDiscreteInputs(0, address=1, count=5)), + (bit_msg.PDUwriteSingleCoil(0, address=1, bits=[True])), + (bit_msg.PDUwriteMultipleCoils(0, address=1, bits=[True] * 5)), ): - await pdu.datastore_update(context, 0) - - async def test_bit_read_datastore_update_address_errors(self, mock_server_context): - """Test bit read request encoding.""" - context = mock_server_context() - for pdu in ( - (bit_msg.ReadCoilsRequest(address=1, count=0x800)), - (bit_msg.ReadDiscreteInputsRequest(address=1, count=0x800)), - ): - await pdu.datastore_update(context, 1) + await pdu.build_response( + mock.AsyncMock(return_value=ExcCodes.ILLEGAL_VALUE), + mock.AsyncMock(return_value=0), + ) def test_bit_write_base_requests(self): """Test bit write base.""" for pdu, expected in ( ( - bit_msg.WriteSingleCoilRequest(address=1, bits=[True]), + bit_msg.PDUwriteSingleCoil(0, address=1, bits=[True]), b"\x00\x01\xff\x00", ), ( - bit_msg.WriteMultipleCoilsRequest(address=1, bits=[True] * 5), + bit_msg.PDUwriteMultipleCoils(0, address=1, bits=[True] * 5), b"\x00\x01\x00\x05\x01\x1f", ), ( - bit_msg.WriteMultipleCoilsRequest(address=1, bits=[True]), + bit_msg.PDUwriteMultipleCoils(0, address=1, bits=[True]), b"\x00\x01\x00\x01\x01\x01", ), ( - bit_msg.WriteMultipleCoilsResponse(address=1, count=5), + bit_msg.PDUwriteMultipleCoils(0, is_request=False, address=1, count=5), b"\x00\x01\x00\x05", ), ( - bit_msg.WriteMultipleCoilsResponse(address=1, count=1), + bit_msg.PDUwriteMultipleCoils(0, is_request=False, address=1, count=1), b"\x00\x01\x00\x01", ), ): - assert pdu.x_encode() == expected + assert pdu.to_bytes() == expected def test_write_message_coil(self): """Test bit write message.""" - _ = bit_msg.WriteSingleCoilRequest(address=1, bits=[True]) + _ = bit_msg.PDUwriteSingleCoil(0, address=1, bits=[True]) def test_write_multiple_coils_request(self): """Test write multiple coils.""" for request, frame, values in ( ( - bit_msg.WriteMultipleCoilsRequest(address=1, bits=[True] * 5), + bit_msg.PDUwriteMultipleCoils(0, address=1, bits=[True] * 5), b"\x00\x01\x00\x05\x01\x1f", [True] * 5, ), ( - bit_msg.WriteMultipleCoilsRequest(address=1, bits=[True]), + bit_msg.PDUwriteMultipleCoils(0, address=1, bits=[True]), b"\x00\x01\x00\x01\x01\x01", [True], ), ): - request.x_decode(frame) + request.from_bytes(0, frame) assert request.address == 1 assert request.bits == values def test_invalid_write_multiple_coils_request(self): """Test write invalid multiple coils.""" - request = bit_msg.WriteMultipleCoilsRequest(address=1, bits=None) + request = bit_msg.PDUwriteMultipleCoils(0, address=1, bits=None) assert not request.bits @pytest.mark.parametrize( @@ -181,131 +166,119 @@ def test_invalid_write_multiple_coils_request(self): b"\x00\x01\x00\x08\x01\xff\x00", ], ) - async def test_write_multiple_coils_rejects_invalid_byte_count( - self, frame, mock_server_context - ): + async def test_write_multiple_coils_rejects_invalid_byte_count(self, frame): """Test write multiple coils rejects inconsistent byte counts.""" - request = bit_msg.WriteMultipleCoilsRequest() - request.x_decode(frame) - context = mock_server_context() - context.async_setValues = mock.AsyncMock() - - result = await request.datastore_update(context, 0) - + request = bit_msg.PDUwriteMultipleCoils(0) + request.from_bytes(0, frame) + mx = mock.AsyncMock(return_value=ExcCodes.ILLEGAL_VALUE) + result = await request.build_response(mx, mx) assert result.exception_code == ExcCodes.ILLEGAL_VALUE - context.async_setValues.assert_not_awaited() async def test_write_multiple_coils_accepts_valid_byte_count( - self, mock_server_context + self, ): """Test write multiple coils accepts a consistent byte count.""" - request = bit_msg.WriteMultipleCoilsRequest() - request.x_decode(b"\x00\x01\x00\x09\x02\xff\x01") - context = mock_server_context() - context.async_setValues = mock.AsyncMock(return_value=0) - - result = await request.datastore_update(context, 0) - - assert result.count == 9 - context.async_setValues.assert_awaited_once_with( - 0, request.function_code, 1, [True] * 9 + request = bit_msg.PDUwriteMultipleCoils(0) + request.from_bytes(0, b"\x00\x01\x00\x09\x02\xff\x01") + result = await request.build_response( + mock.AsyncMock(return_value=[True]), mock.AsyncMock(return_value=0) ) + assert result.count == 9 def test_write_single_coil_request_encode(self): """Test write single coil.""" - request = bit_msg.WriteSingleCoilRequest(address=1, bits=[False]) - assert request.x_encode() == b"\x00\x01\x00\x00" + request = bit_msg.PDUwriteSingleCoil(0, address=1, bits=[False]) + assert request.to_bytes() == b"\x00\x01\x00\x00" - async def test_write_single_coil_datastore_update(self, mock_server_context): + async def test_write_single_coil_build_response(self): """Test write single coil.""" - context = mock_server_context(False, default=True) - request = bit_msg.WriteSingleCoilRequest(address=2, bits=[True]) - result = await request.datastore_update(context, 1) - - context.valid = True - result = await request.datastore_update(context, 0) - assert result.x_encode() == b"\x00\x02\xff\x00" + request = bit_msg.PDUwriteSingleCoil(0, address=2, bits=[True]) + result = await request.build_response( + mock.AsyncMock(return_value=[True]), mock.AsyncMock(return_value=0) + ) + assert result.to_bytes() == b"\x00\x02\xff\x00" - context = mock_server_context(True, default=False) - request = bit_msg.WriteSingleCoilRequest(address=2, bits=[False]) - result = await request.datastore_update(context, 0) - assert result.x_encode() == b"\x00\x02\x00\x00" + request = bit_msg.PDUwriteSingleCoil(0, address=2, bits=[False]) + result = await request.build_response( + mock.AsyncMock(return_value=[False]), mock.AsyncMock(return_value=0) + ) + assert result.to_bytes() == b"\x00\x02\x00\x00" @pytest.mark.parametrize("value", [b"\x00\x01", b"\x00\xff", b"\xff\xff"]) - async def test_write_single_coil_illegal_value(self, value, mock_server_context): + async def test_write_single_coil_illegal_value(self, value): """A value other than 0x0000 or 0xFF00 must answer 0x03 and leave the coil.""" - context = mock_server_context() - context.async_setValues = mock.AsyncMock(return_value=0) - request = bit_msg.WriteSingleCoilRequest() - request.x_decode(b"\x00\x02" + value) - result = await request.datastore_update(context, 1) + request = bit_msg.PDUwriteSingleCoil(0) + request.from_bytes(0, b"\x00\x02" + value) + result = await request.build_response( + mock.AsyncMock(return_value=[True]), mock.AsyncMock(return_value=0) + ) assert result.function_code == 0x85 assert result.exception_code == ExcCodes.ILLEGAL_VALUE - context.async_setValues.assert_not_awaited() @pytest.mark.parametrize( ("value", "bit"), [(b"\xff\x00", True), (b"\x00\x00", False)] ) - async def test_write_single_coil_legal_value(self, value, bit, mock_server_context): + async def test_write_single_coil_legal_value(self, value, bit): """0xFF00 and 0x0000 are written as ON and OFF.""" - context = mock_server_context() - context.async_setValues = mock.AsyncMock(return_value=0) - context.async_getValues = mock.AsyncMock(return_value=[bit]) - request = bit_msg.WriteSingleCoilRequest() - request.x_decode(b"\x00\x02" + value) - result = await request.datastore_update(context, 1) - assert result.x_encode() == b"\x00\x02" + value - context.async_setValues.assert_awaited_once_with(1, 5, 2, [bit]) - - async def test_write_multiple_coils_datastore_update(self, mock_server_context): + request = bit_msg.PDUwriteSingleCoil(1) + request.from_bytes(0, b"\x00\x02" + value) + x_set = mock.AsyncMock(return_value=0) + result = await request.build_response(mock.AsyncMock(return_value=[bit]), x_set) + assert result.to_bytes() == b"\x00\x02" + value + x_set.assert_awaited_once_with(1, 5, 2, [bit]) + + async def test_write_multiple_coils_build_response(self): """Test write multiple coils.""" - context = mock_server_context(False) # too many values - request = bit_msg.WriteMultipleCoilsRequest(address=2, bits=[]) - result = await request.datastore_update(context, 0) + request = bit_msg.PDUwriteMultipleCoils(0, address=2, bits=[]) + result = await request.build_response( + mock.AsyncMock(return_value=[True]), mock.AsyncMock(return_value=0) + ) # does not verify - context.valid = False - request = bit_msg.WriteMultipleCoilsRequest(address=2, bits=[False] * 4) - result = await request.datastore_update(context, 0) + request = bit_msg.PDUwriteMultipleCoils(0, address=2, bits=[False] * 4) + result = await request.build_response( + mock.AsyncMock(return_value=[True]), mock.AsyncMock(return_value=0) + ) # verified request - context.valid = True - result = await request.datastore_update(context, 0) - assert result.x_encode() == b"\x00\x02\x00\x04" + result = await request.build_response( + mock.AsyncMock(return_value=[True]), mock.AsyncMock(return_value=0) + ) + assert result.to_bytes() == b"\x00\x02\x00\x04" @pytest.mark.parametrize( ("request_pdu"), [ - bit_msg.WriteSingleCoilRequest(address=2, bits=[True]), - bit_msg.WriteMultipleCoilsRequest(address=2, bits=[]), + bit_msg.PDUwriteSingleCoil(1, address=2, bits=[True]), + bit_msg.PDUwriteMultipleCoils(1, address=2, bits=[True]), ], ) - async def test_write_coil_exception(self, request_pdu, mock_server_context): + async def test_write_coil_exception(self, request_pdu): """Test write single coil.""" - context = mock_server_context(True, default=True) - context.async_setValues = mock.AsyncMock(return_value=1) - result = await request_pdu.datastore_update(context, 1) + result = await request_pdu.build_response( + mock.AsyncMock(return_value=1), mock.AsyncMock(return_value=1) + ) assert result.exception_code == 1 def test_write_multiple_coils_response(self): """Test write multiple coils.""" - response = bit_msg.WriteMultipleCoilsResponse() - response.x_decode(b"\x00\x80\x00\x08") + response = bit_msg.PDUwriteMultipleCoils(0, is_request=False) + response.from_bytes(0, b"\x00\x80\x00\x08") assert response.address == 0x80 assert response.count == 0x08 def test_serializing_to_string(self): """Test serializing to string.""" for pdu in ( - bit_msg.WriteSingleCoilRequest(address=1, bits=[True]), - bit_msg.WriteSingleCoilResponse(address=1, bits=[True]), - bit_msg.WriteMultipleCoilsRequest(address=1, bits=[True] * 5), - bit_msg.WriteMultipleCoilsResponse(address=1, count=5), + bit_msg.PDUwriteSingleCoil(0, address=1, bits=[True]), + bit_msg.PDUwriteSingleCoil(0, is_request=False, address=1, bits=[True]), + bit_msg.PDUwriteMultipleCoils(0, address=1, bits=[True] * 5), + bit_msg.PDUwriteMultipleCoils(0, is_request=False, address=1, count=5), ): assert str(pdu) def test_pass_falsy_value_in_write_multiple_coils_request(self): """Test pass falsy value to write multiple coils.""" - request = bit_msg.WriteMultipleCoilsRequest(address=1, bits=[False]) + request = bit_msg.PDUwriteMultipleCoils(0, address=1, bits=[False]) assert request.bits == [False] diff --git a/test/pdu/test_decoders.py b/test/pdu/test_decoders.py deleted file mode 100644 index 0208cf9940..0000000000 --- a/test/pdu/test_decoders.py +++ /dev/null @@ -1,192 +0,0 @@ -"""Test decoders.""" - -import pytest - -from pymodbus.exceptions import MessageRegisterException, ModbusIOException -from pymodbus.pdu.decoders import DecodePDU - - -class TestModbusPDU: - """Test ModbusPDU.""" - - client = DecodePDU(False) - server = DecodePDU(True) - requests = ( - (0x01, b"\x01\x00\x01\x00\x01"), # read coils - (0x02, b"\x02\x00\x01\x00\x01"), # read discrete inputs - (0x03, b"\x03\x00\x01\x00\x01"), # read holding registers - (0x04, b"\x04\x00\x01\x00\x01"), # read input registers - (0x05, b"\x05\x00\x01\x00\x01"), # write single coil - (0x06, b"\x06\x00\x01\x00\x01"), # write single register - (0x07, b"\x07"), # read exception status - (0x08, b"\x08\x00\x00\x00\x00"), # read diagnostic - (0x0B, b"\x0b"), # get comm event counters - (0x0C, b"\x0c"), # get comm event log - (0x0F, b"\x0f\x00\x01\x00\x08\x01\x00\xff"), # write multiple coils - (0x10, b"\x10\x00\x01\x00\x02\x04\0xff\xff"), # write multiple registers - (0x11, b"\x11"), # report device id - ( - 0x14, - b"\x14\x0e\x06\x00\x04\x00\x01\x00\x02\x06\x00\x03\x00\x09\x00\x02", - ), # read file record - ( - 0x15, - b"\x15\x0d\x06\x00\x04\x00\x07\x00\x03\x06\xaf\x04\xbe\x10\x0d", - ), # write file record - (0x16, b"\x16\x00\x01\x00\xff\xff\x00"), # mask write register - ( - 0x17, - b"\x17\x00\x01\x00\x01\x00\x01\x00\x01\x02\x12\x34", - ), # r/w multiple regs - (0x18, b"\x18\x00\x01"), # read fifo queue - (0x2B, b"\x2b\x0e\x01\x00"), # read device identification - ) - - responses = ( - (0x01, b"\x01\x01\x01"), # read coils - (0x02, b"\x02\x01\x01"), # read discrete inputs - (0x03, b"\x03\x02\x01\x01"), # read holding registers - (0x04, b"\x04\x02\x01\x01"), # read input registers - (0x05, b"\x05\x00\x01\x00\x01"), # write single coil - (0x06, b"\x06\x00\x01\x00\x01"), # write single register - (0x07, b"\x07\x00"), # read exception status - (0x08, b"\x08\x00\x00\x00\x00"), # read diagnostic - (0x0B, b"\x0b\x00\x00\x00\x00"), # get comm event counters - (0x0C, b"\x0c\x08\x00\x00\x01\x08\x01\x21\x20\x00"), # get comm event log - (0x0F, b"\x0f\x00\x01\x00\x08"), # write multiple coils - (0x10, b"\x10\x00\x01\x00\x02"), # write multiple registers - (0x11, b"\x11\x03\x05\x01\x54"), # report device id (device specific) - ( - 0x14, - b"\x14\x0c\x05\x06\x0d\xfe\x00\x20\x05\x06\x33\xcd\x00\x40", - ), # read file record - ( - 0x15, - b"\x15\x0d\x06\x00\x04\x00\x07\x00\x03\x06\xaf\x04\xbe\x10\x0d", - ), # write file record - (0x16, b"\x16\x00\x01\x00\xff\xff\x00"), # mask write register - (0x17, b"\x17\x02\x12\x34"), # read/write multiple registers - (0x18, b"\x18\x00\x01\x00\x01\x00\x00"), # read fifo queue - ( - 0x2B, - b"\x2b\x0e\x01\x01\x00\x00\x01\x00\x01\x77", - ), # read device identification - ) - - exceptions = ( - (0x81, b"\x81\x01\xd0\x50"), # illegal function exception - (0x82, b"\x82\x02\x90\xa1"), # illegal data address exception - (0x83, b"\x83\x03\x50\xf1"), # illegal data value exception - (0x84, b"\x84\x04\x13\x03"), # skave device failure exception - (0x85, b"\x85\x05\xd3\x53"), # acknowledge exception - (0x86, b"\x86\x06\x93\xa2"), # device busy exception - (0x87, b"\x87\x08\x53\xf2"), # memory parity exception - (0x88, b"\x88\x0a\x16\x06"), # gateway path unavailable exception - (0x89, b"\x89\x0b\xd6\x56"), # gateway target failed exception - ) - - bad = ( - (0x80, b"\x80\x00\x00\x00"), # Unknown Function - (0x81, b"\x81\x00\x00\x00"), # error message - ) - - @pytest.mark.parametrize(("code", "frame"), list(responses) + list(exceptions)) - def test_client_lookup(self, code, frame): - """Test lookup for responses.""" - data = b"\x01" + frame - pdu = self.client._lookupPduClass(data) - assert pdu - if not code & 0x80: - assert pdu.function_code == code - - def test_client_lookup_no_fc(self): - """Test lookup for responses.""" - data = b"\x01\x70" - pdu = self.client._lookupPduClass(data) - assert not pdu - - def test_list_function_codes(self): - """Test lookup for responses.""" - fc_list = self.client.list_function_codes() - assert fc_list - - @pytest.mark.parametrize(("code", "frame"), list(requests)) - def test_server_lookup(self, code, frame): - """Test lookup for requests.""" - data = b"\x01" + frame - pdu = self.client._lookupPduClass(data) - assert pdu - assert pdu.function_code == code - - @pytest.mark.parametrize(("code", "frame"), list(responses) + list(exceptions)) - def test_client_decode(self, code, frame): - """Test lookup for responses.""" - pdu = self.client.decode(frame) - assert pdu - assert pdu.function_code == code - - @pytest.mark.parametrize(("code", "frame"), list(requests)) - def test_server_decode(self, code, frame): - """Test lookup for requests.""" - pdu = self.server.decode(frame) - assert pdu - assert pdu.function_code == code - - @pytest.mark.parametrize(("code", "frame"), list(exceptions)) - def test_server_decode_reserved_function_code(self, code, frame): - """A request may not use a function code reserved for exception responses.""" - with pytest.raises(ModbusIOException) as exc: - self.server.decode(frame) - assert exc.value.fcode == code - - @pytest.mark.parametrize(("frame"), [b"", b"NO FRAME"]) - @pytest.mark.parametrize(("decoder"), [server, client]) - def test_decode_bad_frame(self, decoder, frame): - """Test lookup bad frames.""" - with pytest.raises(ModbusIOException): - decoder.decode(frame) - - @pytest.mark.parametrize( - ("decoder", "frame"), - [ - (server, b"\x14\x10"), # read file record, byte count past the frame - (server, b"\x15\x10"), # write file record, byte count past the frame - (client, b"\x18\x00\x06\x00\x05\x00\x01"), # fifo count past the frame - ], - ) - def test_decode_bad_file_record_frame(self, decoder, frame): - """Test bad file record frames fail like other undecodable frames.""" - with pytest.raises(ModbusIOException): - decoder.decode(frame) - - def test_decode_unknown_sub(self): - """Test for unknown sub code.""" - with pytest.raises(ModbusIOException): - self.client.decode(b"\x08\x00\xf0\xf0\x00") - - @pytest.mark.parametrize(("decoder"), [server, client]) - def test_register_custom_request(self, decoder, mockPDU): - """Test server register custom request.""" - - class NoCustomRequestResponse: - """Custom request.""" - - function_code = 0x70 - - def x_encode(self): - """Encode.""" - - def x_decode(self, _data): - """Decode.""" - - decoder.register(mockPDU) - data = b"\x01" + mockPDU().x_encode() - assert decoder._lookupPduClass(data) - mockPDU.sub_function_code = 0xF7 - decoder.register(mockPDU) - mockPDU.sub_function_code = 0xF4 - decoder.register(mockPDU) - data = b"\x01" + mockPDU().x_encode() - assert self.server._lookupPduClass(data) - with pytest.raises(MessageRegisterException): - decoder.register(NoCustomRequestResponse) diff --git a/test/pdu/test_device.py b/test/pdu/test_device.py index c4878577ae..b9984e1561 100644 --- a/test/pdu/test_device.py +++ b/test/pdu/test_device.py @@ -320,10 +320,10 @@ def test_modbus_plus_statistics(self): """Test device identification reading.""" default = [0x0000] * 55 statistics = ModbusPlusStatistics() - assert default == statistics.x_encode() + assert default == statistics.encode() statistics.reset() - assert default == statistics.x_encode() - assert default == self.control.Plus.x_encode() + assert default == statistics.encode() + assert default == self.control.Plus.encode() def test_modbus_plus_statistics_helpers(self): """Test modbus plus statistics helper methods.""" diff --git a/test/pdu/test_diag_messages.py b/test/pdu/test_diag_messages.py deleted file mode 100644 index 1d9149ef40..0000000000 --- a/test/pdu/test_diag_messages.py +++ /dev/null @@ -1,224 +0,0 @@ -"""Test diag messages.""" - -from typing import cast - -import pytest - -from pymodbus.constants import ModbusPlusOperation, ModbusStatus -from pymodbus.datastore import ModbusServerContext -from pymodbus.pdu.device import ModbusControlBlock -from pymodbus.pdu.diag_message import ( - ChangeAsciiInputDelimiterRequest, - ChangeAsciiInputDelimiterResponse, - ClearCountersRequest, - ClearCountersResponse, - ClearOverrunCountRequest, - ClearOverrunCountResponse, - DiagnosticBase, - ForceListenOnlyModeRequest, - ForceListenOnlyModeResponse, - GetClearModbusPlusRequest, - GetClearModbusPlusResponse, - RestartCommunicationsOptionRequest, - RestartCommunicationsOptionResponse, - ReturnBusCommunicationErrorCountRequest, - ReturnBusCommunicationErrorCountResponse, - ReturnBusExceptionErrorCountRequest, - ReturnBusExceptionErrorCountResponse, - ReturnBusMessageCountRequest, - ReturnBusMessageCountResponse, - ReturnDeviceBusCharacterOverrunCountRequest, - ReturnDeviceBusCharacterOverrunCountResponse, - ReturnDeviceBusyCountRequest, - ReturnDeviceBusyCountResponse, - ReturnDeviceMessageCountRequest, - ReturnDeviceMessageCountResponse, - ReturnDeviceNAKCountRequest, - ReturnDeviceNAKCountResponse, - ReturnDeviceNoResponseCountRequest, - ReturnDeviceNoResponseCountResponse, - ReturnDiagnosticRegisterRequest, - ReturnDiagnosticRegisterResponse, - ReturnIopOverrunCountRequest, - ReturnIopOverrunCountResponse, - ReturnQueryDataRequest, - ReturnQueryDataResponse, -) - - -class TestDataStore: - """Unittest for the pymodbus.diag_message module.""" - - requests = [ - ( - RestartCommunicationsOptionRequest, - b"\x00\x01\x00\x00", - b"\x00\x01\x00\x00", - ), - (ReturnDiagnosticRegisterRequest, b"\x00\x02\x00\x00", b"\x00\x02\x00\x00"), - ( - ChangeAsciiInputDelimiterRequest, - b"\x00\x03\x00\x00", - b"\x00\x03\x00\x00", - ), - (ForceListenOnlyModeRequest, b"\x00\x04\x00\x00", b"\x00\x04"), - (ReturnQueryDataRequest, b"\x00\x00\x00\x00", b"\x00\x00\x00\x00"), - (ClearCountersRequest, b"\x00\x0a\x00\x00", b"\x00\x0a\x00\x00"), - (ReturnBusMessageCountRequest, b"\x00\x0b\x00\x00", b"\x00\x0b\x00\x00"), - ( - ReturnBusCommunicationErrorCountRequest, - b"\x00\x0c\x00\x00", - b"\x00\x0c\x00\x00", - ), - ( - ReturnBusExceptionErrorCountRequest, - b"\x00\x0d\x00\x00", - b"\x00\x0d\x00\x00", - ), - (ReturnDeviceMessageCountRequest, b"\x00\x0e\x00\x00", b"\x00\x0e\x00\x00"), - ( - ReturnDeviceNoResponseCountRequest, - b"\x00\x0f\x00\x00", - b"\x00\x0f\x00\x00", - ), - (ReturnDeviceNAKCountRequest, b"\x00\x10\x00\x00", b"\x00\x10\x00\x00"), - (ReturnDeviceBusyCountRequest, b"\x00\x11\x00\x00", b"\x00\x11\x00\x00"), - ( - ReturnDeviceBusCharacterOverrunCountRequest, - b"\x00\x12\x00\x00", - b"\x00\x12\x00\x00", - ), - (ReturnIopOverrunCountRequest, b"\x00\x13\x00\x00", b"\x00\x13\x00\x00"), - (ClearOverrunCountRequest, b"\x00\x14\x00\x00", b"\x00\x14\x00\x00"), - ( - GetClearModbusPlusRequest, - b"\x00\x15\x00\x00", - b"\x00\x15\x00\x00" + b"\x00\x00" * 55, - ), - ] - - responses = [ - (DiagnosticBase, b"\x00\x00\x00\x00"), - (DiagnosticBase, b"\x00\x00\x00\x00"), - (ReturnQueryDataResponse, b"\x00\x00\x00\x00"), - (RestartCommunicationsOptionResponse, b"\x00\x01\x00\x00"), - (ReturnDiagnosticRegisterResponse, b"\x00\x02\x00\x00"), - (ChangeAsciiInputDelimiterResponse, b"\x00\x03\x00\x00"), - (ForceListenOnlyModeResponse, b"\x00\x04"), - (ReturnQueryDataResponse, b"\x00\x00\x00\x00"), - (ClearCountersResponse, b"\x00\x0a\x00\x00"), - (ReturnBusMessageCountResponse, b"\x00\x0b\x00\x00"), - (ReturnBusCommunicationErrorCountResponse, b"\x00\x0c\x00\x00"), - (ReturnBusExceptionErrorCountResponse, b"\x00\x0d\x00\x00"), - (ReturnDeviceMessageCountResponse, b"\x00\x0e\x00\x00"), - (ReturnDeviceNoResponseCountResponse, b"\x00\x0f\x00\x00"), - (ReturnDeviceNAKCountResponse, b"\x00\x10\x00\x00"), - (ReturnDeviceBusyCountResponse, b"\x00\x11\x00\x00"), - (ReturnDeviceBusCharacterOverrunCountResponse, b"\x00\x12\x00\x00"), - (ReturnIopOverrunCountResponse, b"\x00\x13\x00\x00"), - (ClearOverrunCountResponse, b"\x00\x14\x00\x00"), - (GetClearModbusPlusResponse, b"\x00\x15\x00\x04" + b"\x00\x00" * 55), - ] - - def test_diagnostic_encode_decode(self): - """Testing diagnostic request/response can be decoded and encoded.""" - msg_obj = DiagnosticBase() - data = b"\x00\x01\x02\x03" - msg_obj.x_decode(data) - result = msg_obj.x_encode() - assert data == result - - def test_diagnostic_encode_error(self): - """Testing diagnostic request/response can be decoded and encoded.""" - msg_obj = DiagnosticBase() - msg_obj.message = "not allowed" # type: ignore[assignment] - with pytest.raises(TypeError): - msg_obj.x_encode() - - def test_diagnostic_decode_error(self): - """Testing diagnostic request/response can be decoded and encoded.""" - msg_obj = DiagnosticBase() - data = b"\x00\x01\x02\x03a" - msg_obj.x_decode(data) - - def test_diagnostic_requests_decode(self): - """Testing diagnostic request messages encoding.""" - for msg, enc, _ in self.requests: - handle = DiagnosticBase() - handle.x_decode(enc) - assert handle.sub_function_code == msg.sub_function_code - encoded = handle.x_encode() - assert enc == encoded - - async def test_diagnostic_simple_requests(self): - """Testing diagnostic request messages encoding.""" - request = DiagnosticBase(message=b"\x12\x34") - request.sub_function_code = 0x1234 - assert request.x_encode() == b"\x12\x34\x12\x34" - DiagnosticBase() - - def test_diagnostic_response_decode(self): - """Testing diagnostic request messages encoding.""" - for msg, enc, _ in self.requests: - handle = DiagnosticBase() - handle.x_decode(enc) - assert handle.sub_function_code == msg.sub_function_code - - def test_diagnostic_requests_encode(self): - """Testing diagnostic request messages encoding.""" - for msg, enc, _ in self.requests: - assert msg().x_encode() == enc - - async def test_diagnostic_datastore_update(self): - """Testing diagnostic message execution.""" - control = ModbusControlBlock() - try: - for message, encoded, datastore_updated in self.requests: - encoded = ( - await message().datastore_update(cast(ModbusServerContext, None), 1) - ).x_encode() - assert encoded == datastore_updated - finally: - control.Delimiter = b"\n" - - def test_return_query_data_request(self): - """Testing diagnostic message execution.""" - message = ReturnQueryDataRequest(b"\x00\x00\x00\x00") - assert message.x_encode() == b"\x00\x00\x00\x00\x00\x00" - message = ReturnQueryDataRequest(b"\x00\x00") - assert message.x_encode() == b"\x00\x00\x00\x00" - - def test_return_query_data_response(self): - """Testing diagnostic message execution.""" - message = ReturnQueryDataResponse(b"\x00\x00\x00\x00") - assert message.x_encode() == b"\x00\x00\x00\x00\x00\x00" - message = ReturnQueryDataResponse(b"\x00\x00") - assert message.x_encode() == b"\x00\x00\x00\x00" - - def test_restart_communications_option(self): - """Testing diagnostic message execution.""" - request = RestartCommunicationsOptionRequest(message=ModbusStatus.ON) - assert request.x_encode() == b"\x00\x01\xff\x00" - request = RestartCommunicationsOptionRequest(message=ModbusStatus.OFF) - assert request.x_encode() == b"\x00\x01\x00\x00" - - response = RestartCommunicationsOptionResponse(message=ModbusStatus.ON) - assert response.x_encode() == b"\x00\x01\xff\x00" - response = RestartCommunicationsOptionResponse(message=ModbusStatus.OFF) - assert response.x_encode() == b"\x00\x01\x00\x00" - - async def test_get_clear_modbus_plus_request_datastore_update(self): - """Testing diagnostic message execution.""" - request = GetClearModbusPlusRequest( - message=ModbusPlusOperation.CLEAR_STATISTICS - ) - response = await request.datastore_update(cast(ModbusServerContext, None), 0) - assert ( - cast(GetClearModbusPlusResponse, response).message - == ModbusPlusOperation.CLEAR_STATISTICS - ) - - request = GetClearModbusPlusRequest(message=ModbusPlusOperation.GET_STATISTICS) - response = await request.datastore_update(cast(ModbusServerContext, None), 0) - resp = [ModbusPlusOperation.GET_STATISTICS] - assert cast(GetClearModbusPlusResponse, response).message == resp + [0x00] * 55 diff --git a/test/pdu/test_events.py b/test/pdu/test_events.py index df80cac149..2e2e81c698 100644 --- a/test/pdu/test_events.py +++ b/test/pdu/test_events.py @@ -17,7 +17,7 @@ class TestEvents: def test_remote_receive_event(self): """Test remove receive event.""" event = RemoteReceiveEvent() - event.x_decode(b"\x70") + event.decode(b"\x70") assert event.overrun assert event.listen assert event.broadcast @@ -25,9 +25,9 @@ def test_remote_receive_event(self): def test_remote_sent_event(self): """Test remote sent event.""" event = RemoteSendEvent() - result = event.x_encode() + result = event.encode() assert result == b"\x40" - event.x_decode(b"\x7f") + event.decode(b"\x7f") assert event.read assert event.device_abort assert event.device_busy @@ -46,25 +46,25 @@ def test_remote_sent_event_encode(self): "listen": True, } event = RemoteSendEvent(**arguments) - result = event.x_encode() + result = event.encode() assert result == b"\x7f" def test_entered_listen_mode_event(self): """Test entered listen mode event.""" event = EnteredListenModeEvent() - result = event.x_encode() + result = event.encode() assert result == b"\x04" - event.x_decode(b"\x04") + event.decode(b"\x04") assert event.value == 0x04 with pytest.raises(ParameterException): - event.x_decode(b"\x00") + event.decode(b"\x00") def test_communication_restart_event(self): """Test communication restart event.""" event = CommunicationRestartEvent() - result = event.x_encode() + result = event.encode() assert result == b"\x00" - event.x_decode(b"\x00") + event.decode(b"\x00") assert not event.value with pytest.raises(ParameterException): - event.x_decode(b"\x04") + event.decode(b"\x04") diff --git a/test/pdu/test_file_message.py b/test/pdu/test_file_message.py index c962bfa314..144674928a 100644 --- a/test/pdu/test_file_message.py +++ b/test/pdu/test_file_message.py @@ -7,74 +7,21 @@ * Read Coils """ -import pytest - -from pymodbus.exceptions import ModbusException -from pymodbus.pdu.file_message import ( - FileRecord, - ReadFifoQueueRequest, - ReadFifoQueueResponse, - ReadFileRecordRequest, - ReadFileRecordResponse, - WriteFileRecordRequest, - WriteFileRecordResponse, -) +from unittest import mock +import pytest -TEST_MESSAGE = b"\x00\n\x00\x04\x00\x01\x00\x02\x00\x03\x00\x04" +import pymodbus.pdu.file_message as file_msg +from pymodbus.exceptions import ModbusException, ModbusIOException +from pymodbus.pdu import DecodePDU -class TestBitMessage: +class TestFileMessage: """Modbus bit message tests.""" - def test_read_fifo_queue_request_encode(self): - """Test basic bit message encoding/decoding.""" - handle = ReadFifoQueueRequest(0x1234) - result = handle.x_encode() - assert result == b"\x12\x34" - - def test_read_fifo_queue_request_decode(self): - """Test basic bit message encoding/decoding.""" - handle = ReadFifoQueueRequest(0x0000) - handle.x_decode(b"\x12\x34") - assert handle.address == 0x1234 - - async def test_read_fifo_queue_request(self, mock_server_context): - """Test basic bit message encoding/decoding.""" - context = mock_server_context() - handle = ReadFifoQueueRequest(0x1234) - result = await handle.datastore_update(context, 0) - assert isinstance(result, ReadFifoQueueResponse) - - def test_read_fifo_queue_response_encode(self): - """Test that the read fifo queue response can encode.""" - message = TEST_MESSAGE - handle = ReadFifoQueueResponse([1, 2, 3, 4]) - result = handle.x_encode() - assert result == message - - def test_read_fifo_queue_response_decode(self): - """Test that the read fifo queue response can decode.""" - message = TEST_MESSAGE - handle = ReadFifoQueueResponse([1, 2, 3, 4]) - handle.x_decode(message) - assert handle.values == [1, 2, 3, 4] - - def test_frame_size(self): - """Test that the read fifo queue response can decode.""" - message = TEST_MESSAGE - assert ( - ReadFifoQueueResponse.x_calculateRtuFrameSize(b"\x00\x18" + message) == 14 - ) - assert ReadFifoQueueRequest.x_calculateRtuFrameSize(b"") == 6 - - # -----------------------------------------------------------------------# - # File Record - # -----------------------------------------------------------------------# - def test_file_record_length(self): """Test file record length generation.""" - record = FileRecord( + record = file_msg.FileRecord( file_number=0x01, record_number=0x02, record_data=b"\x00\x01\x02\x04" ) assert record.record_length == 0x02 @@ -82,39 +29,37 @@ def test_file_record_length(self): def test_file_record_errors(self): """Test file record length generation.""" with pytest.raises(ModbusException): - FileRecord(record_length=12, record_data=b"\x00\x01\x02\x04") + file_msg.FileRecord(record_length=12, record_data=b"\x00\x01\x02\x04") with pytest.raises(ModbusException): - FileRecord(record_length=11) + file_msg.FileRecord(record_length=11) def test_read_file_record_request_encode(self): """Test basic bit message encoding/decoding.""" - records = [FileRecord(file_number=0x01, record_number=0x02)] - handle = ReadFileRecordRequest(records=records) - result = handle.x_encode() + records = [file_msg.FileRecord(file_number=0x01, record_number=0x02)] + handle = file_msg.PDUreadFileRecord(0, records=records) + result = handle.to_bytes() assert result == b"\x07\x06\x00\x01\x00\x02\x00\x00" def test_read_file_record_request_decode(self): """Test basic bit message encoding/decoding.""" - # record = FileRecord(file_number=0x04, record_number=0x01, record_length=0x02) request = b"\x0e\x06\x00\x04\x00\x01\x00\x02\x06\x00\x03\x00\x09\x00\x02" - handle = ReadFileRecordRequest() - handle.x_decode(request) - # assert handle.records[0] == record + handle = file_msg.PDUreadFileRecord(0) + handle.from_bytes(0, request) def test_read_file_record_request_frame_size(self): """Test basic bit message encoding/decoding.""" request = ( b"\x00\x00\x0e\x06\x00\x04\x00\x01\x00\x02\x06\x00\x03\x00\x09\x00\x02" ) - handle = ReadFileRecordRequest() - size = handle.x_calculateRtuFrameSize(request) + handle = file_msg.PDUreadFileRecord(0) + size = handle.calculatePduSize(True, 0, request) assert size == 0x0E + 5 - async def test_read_file_record_request_datastore_update(self, mock_server_context): + async def test_read_file_record_request_build_response(self): """Test basic bit message encoding/decoding.""" - handle = ReadFileRecordRequest() - result = await handle.datastore_update(mock_server_context(), 0) - assert isinstance(result, ReadFileRecordResponse) + handle = file_msg.PDUreadFileRecord(0) + result = await handle.build_response(mock.AsyncMock(), mock.AsyncMock()) + assert isinstance(result, file_msg.PDUreadFileRecord) # -----------------------------------------------------------------------# # Read File Record Response @@ -122,9 +67,9 @@ async def test_read_file_record_request_datastore_update(self, mock_server_conte def test_read_file_record_response_encode(self): """Test basic bit message encoding/decoding.""" - records = [FileRecord(record_data=b"\x00\x01\x02\x03\x04\x05")] - handle = ReadFileRecordResponse(records) - result = handle.x_encode() + records = [file_msg.FileRecord(record_data=b"\x00\x01\x02\x03\x04\x05")] + handle = file_msg.PDUreadFileRecord(0, is_request=False, records=records) + result = handle.to_bytes() assert result == b"\x08\x07\x06\x00\x01\x02\x03\x04\x05" def test_read_file_record_response_decode(self): @@ -136,8 +81,8 @@ def test_read_file_record_response_decode(self): # file_number=0x00, record_number=0x00, record_data=b"\x33\xcd\x00\x40" # ) response = b"\x0c\x05\x06\x0d\xfe\x00\x20\x05\x06\x33\xcd\x00\x40" - handle = ReadFileRecordResponse() - handle.x_decode(response) + handle = file_msg.PDUreadFileRecord(0, is_request=False) + handle.from_bytes(0, response) # assert handle.records[0] == record1 # ssert handle.records[1] == record2 @@ -145,8 +90,8 @@ def test_read_file_record_response_decode(self): def test_read_file_record_response_frame_size(self): """Test basic bit message encoding/decoding.""" request = b"\x00\x00\x0c\x05\x06\x0d\xfe\x00\x20\x05\x05\x06\x33\xcd\x00\x40" - handle = ReadFileRecordResponse() - size = handle.x_calculateRtuFrameSize(request) + handle = file_msg.PDUreadFileRecord(0) + size = handle.calculatePduSize(True, 0, request) assert size == 0x0C + 5 # -----------------------------------------------------------------------# @@ -156,12 +101,12 @@ def test_read_file_record_response_frame_size(self): def test_write_file_record_request_encode(self): """Test basic bit message encoding/decoding.""" records = [ - FileRecord( + file_msg.FileRecord( file_number=0x01, record_number=0x02, record_data=b"\x00\x01\x02\x03" ) ] - handle = WriteFileRecordRequest(records) - result = handle.x_encode() + handle = file_msg.PDUwriteFileRecord(0, is_request=False, records=records) + result = handle.to_bytes() assert result == b"\x0b\x06\x00\x01\x00\x02\x00\x02\x00\x01\x02\x03" def test_write_file_record_request_decode(self): @@ -172,24 +117,22 @@ def test_write_file_record_request_decode(self): # record_data=b"\x06\xaf\x04\xbe\x10\x0d", # ) request = b"\x0d\x06\x00\x04\x00\x07\x00\x03\x06\xaf\x04\xbe\x10\x0d" - handle = WriteFileRecordRequest() - handle.x_decode(request) + handle = file_msg.PDUwriteFileRecord(0) + handle.from_bytes(0, request) # assert handle.records[0] == record def test_write_file_record_request_frame_size(self): """Test write file record request rtu frame size calculation.""" request = b"\x00\x00\x0d\x06\x00\x04\x00\x07\x00\x03\x06\xaf\x04\xbe\x10\x0d" - handle = WriteFileRecordRequest() - size = handle.x_calculateRtuFrameSize(request) + handle = file_msg.PDUwriteFileRecord(0) + size = handle.calculatePduSize(True, 0, request) assert size == 0x0D + 5 - async def test_write_file_record_request_datastore_update( - self, mock_server_context - ): + async def test_write_file_record_request_build_response(self): """Test basic bit message encoding/decoding.""" - handle = WriteFileRecordRequest() - result = await handle.datastore_update(mock_server_context(), 0) - assert isinstance(result, WriteFileRecordResponse) + handle = file_msg.PDUwriteFileRecord(0) + result = await handle.build_response(mock.AsyncMock(), mock.AsyncMock()) + assert isinstance(result, file_msg.PDUwriteFileRecord) # -----------------------------------------------------------------------# # Write File Record Response @@ -198,12 +141,12 @@ async def test_write_file_record_request_datastore_update( def test_write_file_record_response_encode(self): """Test basic bit message encoding/decoding.""" records = [ - FileRecord( + file_msg.FileRecord( file_number=0x01, record_number=0x02, record_data=b"\x00\x01\x02\x03" ) ] - handle = WriteFileRecordResponse(records) - result = handle.x_encode() + handle = file_msg.PDUwriteFileRecord(0, is_request=False, records=records) + result = handle.to_bytes() assert result == b"\x0b\x06\x00\x01\x00\x02\x00\x02\x00\x01\x02\x03" def test_write_file_record_response_decode(self): @@ -214,33 +157,51 @@ def test_write_file_record_response_decode(self): # record_data=b"\x06\xaf\x04\xbe\x10\x0d", # ) request = b"\x0d\x06\x00\x04\x00\x07\x00\x03\x06\xaf\x04\xbe\x10\x0d" - handle = WriteFileRecordResponse() - handle.x_decode(request) + handle = file_msg.PDUwriteFileRecord(0) + handle.from_bytes(0, request) # assert handle.records[0] == record def test_read_file_record_request_decode_invalid_byte_count(self): - """Test ReadFileRecordRequest raises ModbusException on oversized byte_count.""" - handle = ReadFileRecordRequest() + """Test file_msg.PDUreadFileRecord raises ModbusException on oversized byte_count.""" + handle = file_msg.PDUreadFileRecord(0) with pytest.raises(ModbusException): - handle.x_decode(bytes([0xFF, 0x06, 0x00, 0x01, 0x00, 0x01, 0x00, 0x01])) + handle.from_bytes( + 0, bytes([0xFF, 0x06, 0x00, 0x01, 0x00, 0x01, 0x00, 0x01]) + ) def test_write_file_record_request_decode_invalid_byte_count(self): - """Test WriteFileRecordRequest raises ModbusException on oversized byte_count.""" - handle = WriteFileRecordRequest() + """Test file_msg.PDUwriteFileRecord raises ModbusException on oversized byte_count.""" + handle = file_msg.PDUwriteFileRecord(0) with pytest.raises(ModbusException): - handle.x_decode( - bytes([0xFF, 0x06, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00]) + handle.from_bytes( + 0, bytes([0xFF, 0x06, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00]) ) def test_read_fifo_queue_response_decode_invalid_count(self): - """Test ReadFifoQueueResponse raises ModbusException on oversized count.""" - handle = ReadFifoQueueResponse() + """Test file_msg.PDUreadFifoQueue raises ModbusException on oversized count.""" + handle = file_msg.PDUreadFifoQueue(0, is_request=False) with pytest.raises(ModbusException): - handle.x_decode(bytes([0x00, 0x00, 0xFF, 0xFF])) + handle.from_bytes(0, bytes([0x00, 0x00, 0xFF, 0xFF])) def test_write_file_record_response_frame_size(self): """Test write file record response rtu frame size calculation.""" request = b"\x00\x00\x0d\x06\x00\x04\x00\x07\x00\x03\x06\xaf\x04\xbe\x10\x0d" - handle = WriteFileRecordResponse() - size = handle.x_calculateRtuFrameSize(request) + handle = file_msg.PDUwriteFileRecord(0) + size = handle.calculatePduSize(True, 0, request) assert size == 0x0D + 5 + + server = DecodePDU(True) + client = DecodePDU(False) + + @pytest.mark.parametrize( + ("decoder", "frame"), + [ + (server, b"\x14\x10"), # read file record, byte count past the frame + (server, b"\x15\x10"), # write file record, byte count past the frame + (client, b"\x18\x00\x06\x00\x05\x00\x01"), # fifo count past the frame + ], + ) + def test_decode_bad_file_record_frame(self, decoder, frame): + """Test bad file record frames fail like other undecodable frames.""" + with pytest.raises(ModbusIOException): + decoder.decode(frame) diff --git a/test/pdu/test_mei_messages.py b/test/pdu/test_mei_messages.py index a4d4bf2e55..7d0fcc7ac8 100644 --- a/test/pdu/test_mei_messages.py +++ b/test/pdu/test_mei_messages.py @@ -5,17 +5,13 @@ """ from typing import cast +from unittest import mock import pytest from pymodbus.constants import DeviceInformation, ExcCodes -from pymodbus.pdu import ExceptionResponse -from pymodbus.pdu.decoders import DecodePDU -from pymodbus.pdu.device import ModbusControlBlock -from pymodbus.pdu.mei_message import ( - ReadDeviceInformationRequest, - ReadDeviceInformationResponse, -) +from pymodbus.pdu import DecodePDU, ModbusControlBlock, PDUexception +from pymodbus.pdu.mei_message import PDUreadDeviceInformation TEST_VERSION = b"v2.1.12" @@ -31,97 +27,109 @@ class TestMeiMessage: def test_read_device_information_request_encode(self): """Test basic bit message encoding/decoding.""" - params = {"read_code": DeviceInformation.BASIC, "object_id": 0x00} - handle = ReadDeviceInformationRequest(**params) - result = handle.x_encode() + params = {"read_code": int(DeviceInformation.BASIC), "object_id": 0x00} + handle = PDUreadDeviceInformation(0, *params) + result = handle.to_bytes() assert result == b"\x0e\x01\x00" - assert "ReadDeviceInformationRequest" in str(handle) + assert "PDUreadDeviceInformation" in str(handle) def test_read_device_information_request_decode(self): """Test basic bit message encoding/decoding.""" - handle = ReadDeviceInformationRequest() - handle.x_decode(b"\x0e\x01\x00") + handle = PDUreadDeviceInformation(0) + handle.from_bytes(0, b"\x0e\x01\x00") assert handle.read_code == DeviceInformation.BASIC assert not handle.object_id def test_device_information_mei_type_is_routed(self): """Test MEI type 0x0E is routed to device identification.""" pdu = DecodePDU(True).decode(b"\x2b\x0e\x01\x00") - assert isinstance(pdu, ReadDeviceInformationRequest) + assert isinstance(pdu, PDUreadDeviceInformation) - async def test_read_device_information_request(self, mock_server_context): + async def test_read_device_information_request(self): """Test basic bit message encoding/decoding.""" - context = mock_server_context() control = ModbusControlBlock() control.Identity.VendorName = "Company" control.Identity.ProductCode = "Product" control.Identity.MajorMinorRevision = TEST_VERSION control.Identity.update({0x81: ["Test", "Repeated"]}) - handle = ReadDeviceInformationRequest() - result = await handle.datastore_update(context, 0) - assert isinstance(result, ReadDeviceInformationResponse) + handle = PDUreadDeviceInformation(0) + result = await handle.build_response(mock.AsyncMock(), mock.AsyncMock()) + assert isinstance(result, PDUreadDeviceInformation) assert result.information[0x00] == "Company" assert result.information[0x01] == "Product" assert result.information[0x02] == TEST_VERSION with pytest.raises(KeyError): _ = result.information[0x81] - handle = ReadDeviceInformationRequest( - read_code=DeviceInformation.SPECIFIC, object_id=0x00 + handle = PDUreadDeviceInformation( + 0, read_code=DeviceInformation.SPECIFIC, object_id=0x00 ) - result = await handle.datastore_update(context, 0) - assert cast(ReadDeviceInformationResponse, result).information == { - 0x00: "Company" - } + result = await handle.build_response(mock.AsyncMock(), mock.AsyncMock()) + assert cast(PDUreadDeviceInformation, result).information == {0x00: "Company"} - handle = ReadDeviceInformationRequest( - read_code=DeviceInformation.EXTENDED, object_id=0x80 + handle = PDUreadDeviceInformation( + 0, read_code=DeviceInformation.EXTENDED, object_id=0x80 ) - result = await handle.datastore_update(context, 0) - assert cast(ReadDeviceInformationResponse, result).information[0x81] == [ + result = await handle.build_response(mock.AsyncMock(), mock.AsyncMock()) + assert cast(PDUreadDeviceInformation, result).information[0x81] == [ "Test", "Repeated", ] - async def test_read_device_information_request_error(self, mock_server_context): + async def test_read_device_information_request_error(self): """Test basic bit message encoding/decoding.""" - context = mock_server_context() - handle = ReadDeviceInformationRequest() + handle = PDUreadDeviceInformation(0) handle.read_code = -1 - assert (await handle.datastore_update(context, 0)).function_code == 0xAB + assert ( + await handle.build_response(mock.AsyncMock(), mock.AsyncMock()) + ).function_code == 0xAB handle.read_code = 0x05 - assert (await handle.datastore_update(context, 0)).function_code == 0xAB + assert ( + await handle.build_response(mock.AsyncMock(), mock.AsyncMock()) + ).function_code == 0xAB handle.object_id = -1 - assert (await handle.datastore_update(context, 0)).function_code == 0xAB + assert ( + await handle.build_response(mock.AsyncMock(), mock.AsyncMock()) + ).function_code == 0xAB handle.object_id = 0x100 - assert (await handle.datastore_update(context, 0)).function_code == 0xAB - handle = ReadDeviceInformationRequest( - read_code=DeviceInformation.SPECIFIC, object_id=0x54 + assert ( + await handle.build_response(mock.AsyncMock(), mock.AsyncMock()) + ).function_code == 0xAB + handle = PDUreadDeviceInformation( + 0, read_code=DeviceInformation.SPECIFIC, object_id=0x54 ) - result = await handle.datastore_update(context, 0) + result = await handle.build_response(mock.AsyncMock(), mock.AsyncMock()) assert result.exception_code == ExcCodes.ILLEGAL_ADDRESS ModbusControlBlock().Identity[0xFE] = "" - handle = ReadDeviceInformationRequest( - read_code=DeviceInformation.SPECIFIC, object_id=0xFE + handle = PDUreadDeviceInformation( + 0, read_code=DeviceInformation.SPECIFIC, object_id=0xFE ) - result = await handle.datastore_update(context, 0) + result = await handle.build_response(mock.AsyncMock(), mock.AsyncMock()) assert result.exception_code == ExcCodes.ILLEGAL_ADDRESS def test_read_device_information_calc1(self): - """Test x_calculateRtuFrameSize, short buffer.""" - handle = ReadDeviceInformationResponse() - assert handle.x_calculateRtuFrameSize(b"\x0e\x01\x83") == 999 + """Test calculatePduSize, short buffer.""" assert ( - handle.x_calculateRtuFrameSize(b"\x0e\x01\x83\x00\x00\x03\x01\x03") == 998 + PDUreadDeviceInformation.calculatePduSize(True, 0, b"\x0e\x01\x83") == 999 + ) + assert ( + PDUreadDeviceInformation.calculatePduSize(False, 0, b"\x0e\x01\x83\x00\x00") + == 999 + ) + assert ( + PDUreadDeviceInformation.calculatePduSize( + False, 0, b"\x0e\x01\x83\x00\x00\x03\x01\x03" + ) + == 998 ) def test_read_device_information_sub_fc(self): - """Test x_calculateRtuFrameSize, short buffer.""" - handle = ReadDeviceInformationResponse() - assert handle.decode_sub_function_code(b"\x0e\x01\x83") == 0x83 - handle2 = ReadDeviceInformationRequest() - assert handle2.decode_sub_function_code(b"\x0e\x01\x83") == 0x83 + """Test calculatePduSize, short buffer.""" + handle = PDUreadDeviceInformation(0) + assert handle.decode_sub_function_code(0, b"\x0e\x01\x83") == 0x83 + handle2 = PDUreadDeviceInformation(0) + assert handle2.decode_sub_function_code(0, b"\x0e\x01\x83") == 0x83 def test_read_device_information_encode(self): """Test that the read fifo queue response can encode.""" @@ -132,27 +140,30 @@ def test_read_device_information_encode(self): 0x01: "Product", 0x02: TEST_VERSION, } - handle = ReadDeviceInformationResponse( - read_code=DeviceInformation.BASIC, information=dataset + handle = PDUreadDeviceInformation( + 0, is_request=False, read_code=DeviceInformation.BASIC, information=dataset ) - result = handle.x_encode() + result = handle.to_bytes() assert result == message - assert "ReadDeviceInformationResponse" in str(handle) + assert "PDUreadDeviceInformation" in str(handle) dataset = { 0x00: "Company", 0x01: "Product", 0x02: TEST_VERSION, 0x81: ["Test", "Repeated"], - 0x82: ExceptionResponse(function_code=0x01), + 0x82: PDUexception(0, 0x01, 0), } message = b"\x0e\x03\x83\x00\x00\x06" message += TEST_MESSAGE message += b"\x81\x04Test\x81\x08Repeated\x82\x01\x00" - handle = ReadDeviceInformationResponse( - read_code=DeviceInformation.EXTENDED, information=dataset + handle = PDUreadDeviceInformation( + 0, + is_request=False, + read_code=DeviceInformation.EXTENDED, + information=dataset, ) - result = handle.x_encode() + result = handle.to_bytes() assert result == message def test_read_device_information_encode_long(self): @@ -173,20 +184,22 @@ def test_read_device_information_encode_long(self): 0x02: TEST_VERSION, 0x80: longstring, } - handle = ReadDeviceInformationResponse( - read_code=DeviceInformation.BASIC, information=dataset + handle = PDUreadDeviceInformation( + 0, is_request=False, read_code=DeviceInformation.BASIC, information=dataset ) - result = handle.x_encode() + result = handle.to_bytes() assert result == message - assert "ReadDeviceInformationResponse" in str(handle) + assert "PDUreadDeviceInformation" in str(handle) def test_read_device_information_decode(self): """Test that the read device information response can decode.""" message = b"\x0e\x01\x01\x00\x00\x05" message += TEST_MESSAGE message += b"\x81\x04Test\x81\x08Repeated\x81\x07Another" - handle = ReadDeviceInformationResponse(read_code=0x00, information={}) - handle.x_decode(message) + handle = PDUreadDeviceInformation( + 0, is_request=False, read_code=0x00, information={} + ) + handle.from_bytes(0, message) assert handle.read_code == DeviceInformation.BASIC assert handle.conformity == 0x01 assert handle.information[0x00] == b"Company" @@ -199,8 +212,8 @@ def test_frame_size(self): message = ( b"\x04\x2b\x0e\x01\x81\x00\x01\x01\x00\x06\x66\x6f\x6f\x62\x61\x72\xd7\x3b" ) - result = ReadDeviceInformationResponse.x_calculateRtuFrameSize(message) + result = PDUreadDeviceInformation.calculatePduSize(False, 0, message) assert result == 18 - message = b"\x00\x2b\x0e\x02\x00\x4d\x47" - result = ReadDeviceInformationRequest.x_calculateRtuFrameSize(message) + message = b"\x00\x2b\x0e\x02\x00" + result = PDUreadDeviceInformation.calculatePduSize(True, 0, message) assert result == 7 diff --git a/test/pdu/test_other_messages.py b/test/pdu/test_other_messages.py index e1c24f65ab..d6b45d10c8 100644 --- a/test/pdu/test_other_messages.py +++ b/test/pdu/test_other_messages.py @@ -12,89 +12,76 @@ class TestOtherMessage: """Unittest for the pymodbus.other_message module.""" - requests = [ - pymodbus_message.ReadExceptionStatusRequest, - pymodbus_message.GetCommEventCounterRequest, - pymodbus_message.GetCommEventLogRequest, - pymodbus_message.ReportDeviceIdRequest, - ] - - responses = [ - pymodbus_message.ReadExceptionStatusResponse(0x12), - pymodbus_message.GetCommEventCounterResponse(0x12), - pymodbus_message.GetCommEventLogResponse, - pymodbus_message.ReportDeviceIdResponse(b"\x12"), - ] - - def test_other_messages_to_string(self): - """Test other messages to string.""" - for message in self.requests: - assert str(message) - for message2 in self.responses: - assert str(message2) - - async def test_read_exception_status(self, mock_server_context): + async def test_read_exception_status(self): """Test read exception status.""" - context = mock_server_context() - request = pymodbus_message.ReadExceptionStatusRequest() - request.x_decode(b"\x12") - assert not request.x_encode() - assert (await request.datastore_update(context, 0)).function_code == 0x07 - - response = pymodbus_message.ReadExceptionStatusResponse(status=0x12) - assert response.x_encode() == b"\x12" - response.x_decode(b"\x12") + request = pymodbus_message.PDUreadExceptionStatus(0) + request.from_bytes(0, b"\x12") + assert not request.to_bytes() + assert ( + await request.build_response(mock.AsyncMock(), mock.AsyncMock()) + ).function_code == 0x07 + + response = pymodbus_message.PDUreadExceptionStatus( + 0, is_request=False, status=0x12 + ) + assert response.to_bytes() == b"\x12" + response.from_bytes(0, b"\x12") assert response.status == 0x12 - async def test_get_comm_event_counter(self, mock_server_context): + async def test_get_comm_event_counter(self): """Test get comm event counter.""" - context = mock_server_context() - request = pymodbus_message.GetCommEventCounterRequest() - request.x_decode(b"\x12") - assert not request.x_encode() - assert (await request.datastore_update(context, 0)).function_code == 0x0B - - response = pymodbus_message.GetCommEventCounterResponse(count=0x12) - assert response.x_encode() == b"\x00\x00\x00\x12" - response.x_decode(b"\x00\x00\x00\x12") + request = pymodbus_message.PDUgetCommEventCounter(0) + request.from_bytes(1, b"\x0b\x00\x00\x00\x12") + assert not request.to_bytes() + assert ( + await request.build_response(mock.AsyncMock(), mock.AsyncMock()) + ).function_code == 0x0B + + response = pymodbus_message.PDUgetCommEventCounter( + 0, is_request=False, count=0x12 + ) + assert response.to_bytes() == b"\x00\x00\x00\x12" + response.from_bytes(0, b"\x00\x00\x00\x12") assert response.status assert response.count == 0x12 response.status = False - assert response.x_encode() == b"\xff\xff\x00\x12" + assert response.to_bytes() == b"\xff\xff\x00\x12" - async def test_get_comm_event_log(self, mock_server_context): + async def test_get_comm_event_log(self): """Test get comm event log.""" - context = mock_server_context() - request = pymodbus_message.GetCommEventLogRequest() - request.x_decode(b"\x12") - assert not request.x_encode() - assert (await request.datastore_update(context, 0)).function_code == 0x0C - - response = pymodbus_message.GetCommEventLogResponse() - assert response.x_encode() == b"\x06\x00\x00\x00\x00\x00\x00" - response.x_decode(b"\x06\x00\x00\x00\x12\x00\x12") + request = pymodbus_message.PDUgetCommEventLog(0) + request.from_bytes(0, b"\x12") + assert not request.to_bytes() + assert ( + await request.build_response(mock.AsyncMock(), mock.AsyncMock()) + ).function_code == 0x0C + + response = pymodbus_message.PDUgetCommEventLog(0, is_request=False) + assert response.to_bytes() == b"\x06\x00\x00\x00\x00\x00\x00" + response.from_bytes(0, b"\x06\x00\x00\x00\x12\x00\x12") assert response.status assert response.message_count == 0x12 assert response.event_count == 0x12 assert not response.events response.status = False - assert response.x_encode() == b"\x06\xff\xff\x00\x12\x00\x12" + assert response.to_bytes() == b"\x06\xff\xff\x00\x12\x00\x12" def test_get_comm_event_log_with_events(self): """Test get comm event log with events.""" - response = pymodbus_message.GetCommEventLogResponse(events=[0x12, 0x34, 0x56]) - assert response.x_encode() == b"\x09\x00\x00\x00\x00\x00\x00\x12\x34\x56" - response.x_decode(b"\x09\x00\x00\x00\x12\x00\x12\x12\x34\x56") + response = pymodbus_message.PDUgetCommEventLog( + 0, is_request=False, events=[0x12, 0x34, 0x56] + ) + assert response.to_bytes() == b"\x09\x00\x00\x00\x00\x00\x00\x12\x34\x56" + response.from_bytes(0, b"\x09\x00\x00\x00\x12\x00\x12\x12\x34\x56") assert response.status assert response.message_count == 0x12 assert response.event_count == 0x12 assert response.events == [0x12, 0x34, 0x56] - async def test_report_device_id_request(self, mock_server_context): + async def test_report_device_id_request(self): """Test report device_id request.""" - context = mock_server_context() with mock.patch("pymodbus.pdu.other_message.DeviceInformationFactory") as dif: # First test regular identity strings identity = { @@ -111,10 +98,10 @@ async def test_report_device_id_request(self, mock_server_context): dif.get.return_value = identity expected_identity = "-".join(identity.values()).encode() - request = pymodbus_message.ReportDeviceIdRequest() - response = await request.datastore_update(context, 1) + request = pymodbus_message.PDUreportDeviceId(0) + response = await request.build_response(mock.AsyncMock(), mock.AsyncMock()) assert ( - cast(pymodbus_message.ReportDeviceIdResponse, response).identifier + cast(pymodbus_message.PDUreportDeviceId, response).identifier == expected_identity ) @@ -132,54 +119,57 @@ async def test_report_device_id_request(self, mock_server_context): } dif.get.return_value = identity2 - request = pymodbus_message.ReportDeviceIdRequest() - response = await request.datastore_update(context, 0) + request = pymodbus_message.PDUreportDeviceId(0) + response = await request.build_response(mock.AsyncMock(), mock.AsyncMock()) assert ( - cast(pymodbus_message.ReportDeviceIdResponse, response).identifier + cast(pymodbus_message.PDUreportDeviceId, response).identifier == expected_identity ) - async def test_report_device_id(self, mock_server_context): + async def test_report_device_id(self): """Test report device_id.""" - context = mock_server_context() with mock.patch("pymodbus.pdu.other_message.DeviceInformationFactory") as dif: dif.get.return_value = {} - request = pymodbus_message.ReportDeviceIdRequest() - request.x_decode(b"\x12") - assert not request.x_encode() - assert (await request.datastore_update(context, 0)).function_code == 0x11 - - response = pymodbus_message.ReportDeviceIdResponse( - cast( - pymodbus_message.ReportDeviceIdResponse, - await request.datastore_update(context, 0), + request = pymodbus_message.PDUreportDeviceId(0) + request.from_bytes(0, b"\x12") + assert not request.to_bytes() + assert ( + await request.build_response(mock.AsyncMock(), mock.AsyncMock()) + ).function_code == 0x11 + + response = pymodbus_message.PDUreportDeviceId( + 0, + is_request=False, + identifier=cast( + pymodbus_message.PDUreportDeviceId, + await request.build_response(mock.AsyncMock(), mock.AsyncMock()), ).identifier, - True, + status=True, ) - assert response.x_encode() == b"\tPymodbus\xff" - response.x_decode(b"\tPymodbus\xff") + assert response.to_bytes() == b"\tPymodbus\xff" + response.from_bytes(0, b"\tPymodbus\xff") assert response.status assert response.identifier == b"Pymodbus" - response.x_decode(b"\x03\x12\x00\x00") - assert not response.status + response.from_bytes(0, b"\x03\x12\x00\x00") assert response.identifier == b"\x12\x00" - response.status = False - assert response.x_encode() == b"\x03\x12\x00\x00" + assert response.to_bytes() == b"\x03\x12\x00\x00" def test_report_device_id_response_roundtrip(self): - """Test decode( x_encode()) keeps identifier and status unchanged.""" + """Test decode( to_bytes()) keeps identifier and status unchanged.""" for identifier, status in ((b"Pymodbus", True), (b"\x12", False)): - sent = pymodbus_message.ReportDeviceIdResponse(identifier, status) - received = pymodbus_message.ReportDeviceIdResponse() - received.x_decode(sent.x_encode()) + sent = pymodbus_message.PDUreportDeviceId( + 0, is_request=False, identifier=identifier, status=status + ) + received = pymodbus_message.PDUreportDeviceId(0, is_request=False) + received.from_bytes(0, sent.to_bytes()) assert received.identifier == identifier assert received.status == status def test_report_device_id_response_invalid_byte_count(self): """Test byte count pointing outside the received data raises.""" - response = pymodbus_message.ReportDeviceIdResponse() + response = pymodbus_message.PDUreportDeviceId(0, is_request=False) for frame in (b"\x00\x12\xff", b"\x04\x12\xff"): with pytest.raises(ModbusIOException): - response.x_decode(frame) + response.from_bytes(0, frame) diff --git a/test/pdu/test_pdu.py b/test/pdu/test_pdu.py index a5d2fb2632..9ef973822b 100644 --- a/test/pdu/test_pdu.py +++ b/test/pdu/test_pdu.py @@ -1,549 +1,238 @@ """Test pdu.""" -from typing import Any +from copy import copy +from unittest import mock import pytest -import pymodbus.pdu.bit_message as bit_msg -import pymodbus.pdu.diag_message as diag_msg -import pymodbus.pdu.file_message as file_msg -import pymodbus.pdu.mei_message as mei_msg -import pymodbus.pdu.other_message as o_msg import pymodbus.pdu.register_message as reg_msg -from pymodbus.constants import ModbusStatus +from pymodbus.constants import CLEAR_STATISTICS, ExcCodes from pymodbus.exceptions import ModbusIOException from pymodbus.pdu import ( DecodePDU, - ExceptionResponse, - ModbusPDU, + PDUexception, ) -from pymodbus.pdu.device import ModbusControlBlock +from pymodbus.pdu.diag_message import PDUgetClearModbusPlus, PDUreturnQueryData from pymodbus.pdu.utils import pack_bitstring, unpack_bitstring class TestPdu: """Test modbus PDU.""" - exception = ExceptionResponse(1, 1, 0, 0) - - async def test_error_methods(self): - """Test all error methods.""" - result = self.exception.x_encode() - assert result == b"\x01" - DecodePDU(False).decode(b"\x81" + result) - assert self.exception.exception_code == 1 - self.exception.x_decode(b"") - - async def test_pdu_id(self, mockPDU): - """Test set illegal pdu id.""" - with pytest.raises(ModbusIOException) as exc_info: - mockPDU(256, transaction_id=0x42) - assert exc_info.value.dev_id == 256 - assert exc_info.value.transaction_id == 0x42 - - async def test_is_error(self): - """Test is_error.""" - assert self.exception.isError() - - requests: list[tuple[type[ModbusPDU], tuple[()], dict[str, Any], bytes]] = [ - ( - bit_msg.ReadCoilsRequest, - (), - {"address": 117, "count": 3}, - b"\x01\x00\x75\x00\x03", - ), - ( - bit_msg.ReadDiscreteInputsRequest, - (), - {"address": 117, "count": 3}, - b"\x02\x00\x75\x00\x03", - ), - ( - bit_msg.WriteSingleCoilRequest, - (), - {"address": 117, "bits": [True]}, - b"\x05\x00\x75\xff\x00", - ), - ( - bit_msg.WriteMultipleCoilsRequest, - (), - {"address": 117, "bits": [True, False, True]}, - b"\x0f\x00\x75\x00\x03\x01\x05", - ), - (diag_msg.DiagnosticBase, (), {"message": None}, b"\x08\x27\x0f"), - (diag_msg.DiagnosticBase, (), {"message": 0x1010}, b"\x08\x27\x0f\x10\x10"), - ( - diag_msg.ReturnQueryDataRequest, - (), - {"message": b"\x10\x01"}, - b"\x08\x00\x00\x10\x01", - ), - ( - diag_msg.RestartCommunicationsOptionRequest, - (), - {"message": ModbusStatus.ON}, - b"\x08\x00\x01\xff\x00", - ), - ( - diag_msg.ReturnDiagnosticRegisterRequest, - (), - {"message": 0x1010}, - b"\x08\x00\x02\x10\x10", - ), - ( - diag_msg.ChangeAsciiInputDelimiterRequest, - (), - {"message": 0x1010}, - b"\x08\x00\x03\x10\x10", - ), - (diag_msg.ForceListenOnlyModeRequest, (), {}, b"\x08\x00\x04\x00\x00"), - (diag_msg.ClearCountersRequest, (), {"message": 0x1010}, b"\x08\x00\n\x10\x10"), - ( - diag_msg.ReturnBusMessageCountRequest, - (), - {"message": 0x1010}, - b"\x08\x00\x0b\x10\x10", - ), - ( - diag_msg.ReturnBusCommunicationErrorCountRequest, - (), - {"message": 0x1010}, - b"\x08\x00\x0c\x10\x10", - ), - ( - diag_msg.ReturnBusExceptionErrorCountRequest, - (), - {"message": 0x1010}, - b"\x08\x00\x0d\x10\x10", - ), - ( - diag_msg.ReturnDeviceMessageCountRequest, - (), - {"message": 0x1010}, - b"\x08\x00\x0e\x10\x10", - ), - ( - diag_msg.ReturnDeviceNoResponseCountRequest, - (), - {"message": 0x1010}, - b"\x08\x00\x0f\x10\x10", - ), - ( - diag_msg.ReturnDeviceNAKCountRequest, - (), - {"message": 0x1010}, - b"\x08\x00\x10\x10\x10", - ), - ( - diag_msg.ReturnDeviceBusyCountRequest, - (), - {"message": 0x1010}, - b"\x08\x00\x11\x10\x10", - ), - ( - diag_msg.ReturnDeviceBusCharacterOverrunCountRequest, - (), - {"message": 0x1010}, - b"\x08\x00\x12\x10\x10", - ), - ( - diag_msg.ReturnIopOverrunCountRequest, - (), - {"message": 0x1010}, - b"\x08\x00\x13\x10\x10", - ), - ( - diag_msg.ClearOverrunCountRequest, - (), - {"message": 0x1010}, - b"\x08\x00\x14\x10\x10", - ), - ( - diag_msg.GetClearModbusPlusRequest, - (), - {"message": 0x1010}, - b"\x08\x00\x15\x10\x10", - ), - ( - file_msg.ReadFileRecordRequest, - (), - {"records": [file_msg.FileRecord(), file_msg.FileRecord()]}, - b"\x14\x0e\x06\x00\x00\x00\x00\x00\x00\x06\x00\x00\x00\x00\x00\x00", - ), - ( - file_msg.WriteFileRecordRequest, - (), - {"records": [file_msg.FileRecord(), file_msg.FileRecord()]}, - b"\x15\x0e\x06\x00\x00\x00\x00\x00\x00\x06\x00\x00\x00\x00\x00\x00", - ), - (file_msg.ReadFifoQueueRequest, (), {"address": 117}, b"\x18\x00\x75"), - ( - mei_msg.ReadDeviceInformationRequest, - (), - {"read_code": 0x17, "object_id": 0x29}, - b"\x2b\x0e\x17\x29", - ), - (o_msg.ReadExceptionStatusRequest, (), {}, b"\x07"), - (o_msg.GetCommEventCounterRequest, (), {}, b"\x0b"), - (o_msg.GetCommEventLogRequest, (), {}, b"\x0c"), - (o_msg.ReportDeviceIdRequest, (), {}, b"\x11"), - ( - reg_msg.ReadHoldingRegistersRequest, - (), - {"address": 117, "count": 3}, - b"\x03\x00\x75\x00\x03", - ), - ( - reg_msg.ReadInputRegistersRequest, - (), - {"address": 117, "count": 3}, - b"\x04\x00\x75\x00\x03", - ), - ( - reg_msg.ReadWriteMultipleRegistersRequest, - (), - { - "read_address": 17, - "read_count": 2, - "write_address": 25, - "write_registers": [111, 112], - }, - b"\x17\x00\x11\x00\x02\x00\x19\x00\x02\x04\x00\x6f\x00\x70", - ), - ( - reg_msg.WriteMultipleRegistersRequest, - (), - {"address": 117, "registers": [111, 121, 131]}, - b"\x10\x00\x75\x00\x03\x06\x00\x6f\x00\x79\x00\x83", - ), - ( - reg_msg.WriteSingleRegisterRequest, - (), - {"address": 117, "registers": [112]}, - b"\x06\x00\x75\x00\x70", - ), - ( - reg_msg.MaskWriteRegisterRequest, - (), - {"address": 0x0104, "and_mask": 0xE1D2, "or_mask": 0x1234}, - b"\x16\x01\x04\xe1\xd2\x12\x34", - ), - ] - - responses: list[tuple[type[ModbusPDU], tuple[()], dict[str, Any], bytes]] = [ - ( - bit_msg.ReadCoilsResponse, - (), - {"bits": [True, True] + [False] * 6, "address": 17}, - b"\x01\x01\x03", - ), - ( - bit_msg.ReadDiscreteInputsResponse, - (), - {"bits": [True, True], "address": 17}, - b"\x02\x01\x03", - ), - ( - bit_msg.WriteSingleCoilResponse, - (), - {"address": 117, "bits": [True]}, - b"\x05\x00\x75\xff\x00", - ), - ( - bit_msg.WriteMultipleCoilsResponse, - (), - {"address": 117, "count": 3}, - b"\x0f\x00\x75\x00\x03", - ), - (diag_msg.DiagnosticBase, (), {"message": None}, b"\x08\x27\x0f"), - (diag_msg.DiagnosticBase, (), {"message": 0x1010}, b"\x08\x27\x0f\x10\x10"), - ( - diag_msg.ReturnQueryDataResponse, - (), - {"message": b"AB"}, - b"\x08\x00\x00\x41\x42", - ), - ( - diag_msg.RestartCommunicationsOptionResponse, - (), - {"message": ModbusStatus.ON}, - b"\x08\x00\x01\xff\x00", - ), - ( - diag_msg.ReturnDiagnosticRegisterResponse, - (), - {"message": 0x1010}, - b"\x08\x00\x02\x10\x10", - ), - ( - diag_msg.ChangeAsciiInputDelimiterResponse, - (), - {"message": 0x1010}, - b"\x08\x00\x03\x10\x10", - ), - (diag_msg.ForceListenOnlyModeResponse, (), {}, b"\x08\x00\x04"), - ( - diag_msg.ClearCountersResponse, - (), - {"message": 0x1010}, - b"\x08\x00\n\x10\x10", - ), - ( - diag_msg.ReturnBusMessageCountResponse, - (), - {"message": 0x1010}, - b"\x08\x00\x0b\x10\x10", - ), - ( - diag_msg.ReturnBusCommunicationErrorCountResponse, - (), - {"message": 0x1010}, - b"\x08\x00\x0c\x10\x10", - ), - ( - diag_msg.ReturnBusExceptionErrorCountResponse, - (), - {"message": 0x1010}, - b"\x08\x00\x0d\x10\x10", - ), - ( - diag_msg.ReturnDeviceMessageCountResponse, - (), - {"message": 0x1010}, - b"\x08\x00\x0e\x10\x10", - ), - ( - diag_msg.ReturnDeviceNoResponseCountResponse, - (), - {"message": 0x1010}, - b"\x08\x00\x0f\x10\x10", - ), - ( - diag_msg.ReturnDeviceNAKCountResponse, - (), - {"message": 0x1010}, - b"\x08\x00\x10\x10\x10", - ), - ( - diag_msg.ReturnDeviceBusyCountResponse, - (), - {"message": 0x1010}, - b"\x08\x00\x11\x10\x10", - ), - ( - diag_msg.ReturnDeviceBusCharacterOverrunCountResponse, - (), - {"message": 0x1010}, - b"\x08\x00\x12\x10\x10", - ), - ( - diag_msg.ReturnIopOverrunCountResponse, - (), - {"message": 0x1010}, - b"\x08\x00\x13\x10\x10", - ), - ( - diag_msg.ClearOverrunCountResponse, - (), - {"message": 0x1010}, - b"\x08\x00\x14\x10\x10", - ), - ( - diag_msg.GetClearModbusPlusResponse, - (), - {"message": 0x1010}, - b"\x08\x00\x15\x10\x10", - ), - ( - file_msg.ReadFileRecordResponse, - (), - {"records": [file_msg.FileRecord(), file_msg.FileRecord()]}, - b"\x14\x04\x01\x06\x01\x06", - ), - ( - file_msg.WriteFileRecordResponse, - (), - {"records": [file_msg.FileRecord(), file_msg.FileRecord()]}, - b"\x15\x0e\x06\x00\x00\x00\x00\x00\x00\x06\x00\x00\x00\x00\x00\x00", - ), - ( - file_msg.ReadFifoQueueResponse, - (), - {"values": [123, 456]}, - b"\x18\x00\x06\x00\x02\x00{\x01\xc8", - ), - ( - mei_msg.ReadDeviceInformationResponse, - (), - {"read_code": 0x17}, - b"\x2b\x0e\x17\x83\x00\x00\x00", - ), - (o_msg.ReadExceptionStatusResponse, (), {"status": 0x23}, b"\x07\x23"), - ( - o_msg.GetCommEventCounterResponse, - (), - {"count": 123}, - b"\x0b\x00\x00\x00\x7b", - ), - ( - o_msg.GetCommEventLogResponse, - (), - {"status": True, "message_count": 12, "event_count": 7, "events": [12, 14]}, - b"\x0c\x08\x00\x00\x00\x07\x00\x0c\x0c\x0e", - ), - ( - o_msg.ReportDeviceIdResponse, - (), - {"identifier": b"\x12", "status": True}, - b"\x11\x02\x12\xff", - ), - ( - reg_msg.ReadHoldingRegistersResponse, - (), - {"registers": [3, 17]}, - b"\x03\x04\x00\x03\x00\x11", - ), - ( - reg_msg.ReadInputRegistersResponse, - (), - {"registers": [3, 17]}, - b"\x04\x04\x00\x03\x00\x11", - ), - ( - reg_msg.ReadWriteMultipleRegistersResponse, - (), - {"registers": [1, 2]}, - b"\x17\x04\x00\x01\x00\x02", - ), - ( - reg_msg.WriteSingleRegisterResponse, - (), - {"address": 117, "registers": [112]}, - b"\x06\x00\x75\x00\x70", - ), - ( - reg_msg.WriteMultipleRegistersResponse, - (), - {"address": 117, "count": 3}, - b"\x10\x00\x75\x00\x03", - ), - ( - reg_msg.MaskWriteRegisterResponse, - (), - {"address": 0x0104, "and_mask": 0xE1D2, "or_mask": 0x1234}, - b"\x16\x01\x04\xe1\xd2\x12\x34", - ), - ] - - @pytest.mark.parametrize(("pdutype", "args", "kwargs", "frame"), requests) - @pytest.mark.usefixtures("kwargs", "args", "frame") - def test_pdu_instance(self, pdutype): - """Test that all PDU types can be created.""" - pdu = pdutype() - assert pdu - @pytest.mark.parametrize( - ("pdutype", "args", "kwargs", "frame"), requests + responses + "cases", + ["id", "params", "wrong_addr", "wrong_count"], ) - @pytest.mark.usefixtures("frame", "args") - def test_pdu_instance_args(self, pdutype, kwargs): - """Test that all PDU types can be created.""" - pdu = pdutype(**kwargs) - assert pdu - assert pdutype.__name__ in str(pdu) + async def test_init1(self, cases, paramPDU): + """Ensure that __init__ runs correctly.""" + pdu, _api, params, _msg, params2, _msg = paramPDU + match cases: + case "id": + with pytest.raises(TypeError): + pdu() + assert pdu(0) + assert pdu(1) + assert pdu(1, is_request=False) + assert pdu(1, is_request=True) + with pytest.raises(ModbusIOException): + pdu(-1) + with pytest.raises(ModbusIOException): + pdu(256) + case "params": + assert pdu(1, **params) + assert pdu(1, **params2) + assert pdu(1, **(params | params2)) + dev_id = 63 + params_comb = params | { + "transaction_id": 9112, + "address": 517, + "count": 68, + } + pdu = pdu(dev_id, **params_comb) + assert pdu + assert str(pdu) + assert 0 < pdu.function_code < 0x80 + for key, value in params_comb.items(): + assert getattr(pdu, key) == value + case "wrong_addr": + z_param = params | {"address": -1} + with pytest.raises(ModbusIOException): + pdu(0, **z_param) + z_param = params | {"address": 70000} + with pytest.raises(ModbusIOException): + pdu(0, **z_param) + case "wrong_count": + z_param = params | {"count": -1} + with pytest.raises(ModbusIOException): + pdu(0, **z_param) + z_param = params | {"count": 3000} + with pytest.raises(ModbusIOException): + pdu(0, **z_param) + case _: # pragma: no cover + raise TypeError("Test case not handled!") @pytest.mark.parametrize( - ("pdutype", "args", "kwargs", "frame"), requests + responses + "cases", + ["isError", "sub_func", "store", "str"], ) - @pytest.mark.usefixtures("frame", "args") - def test_pdu_instance_extras(self, pdutype, kwargs): - """Test that all PDU types can be created.""" - tid = 9112 - dev_id = 63 - pdu = pdutype(transaction_id=tid, dev_id=dev_id, **kwargs) - assert pdu - assert str(pdu) - assert pdu.dev_id == dev_id - assert pdu.transaction_id == tid - assert pdu.function_code > 0 - - def test_pdu_register_as_byte(self): - """Test verify functions.""" - registers = [int.from_bytes(b"ab", "big"), int.from_bytes(b"cd", "big")] - # NOT ALLOWED, NO conversion. - req = reg_msg.ReadHoldingRegistersRequest( - address=117, registers=registers, count=3 - ) - assert len(req.registers) == 2 - assert req.registers[0] == 24930 - assert req.registers[1] == 25444 - - def test_pdu_verify_address(self): - """Test verify functions.""" - req = reg_msg.ReadHoldingRegistersRequest(address=10, count=3) - req.address = -1 - with pytest.raises(ValueError): # noqa: PT011 - req.verifyAddress() - req.address = 66000 - with pytest.raises(ValueError): # noqa: PT011 - req.verifyAddress() + async def test_init(self, cases, paramPDU): + """Ensure that __init__ runs correctly.""" + pdu, _api, params, msg, _params2, _msg = paramPDU + match cases: + case "isError": + assert not pdu(0).isError() + case "sub_func": + x = pdu(0) + x_fc = x.sub_function_code + assert x_fc == x.decode_sub_function_code(0, b"\x01" + msg) + case "store": + x = pdu(0, **params) + assert await x.build_response(mock.AsyncMock(), mock.AsyncMock()) + case "str": + x_str = str(pdu(0, **params)) + for x in (pdu.__name__, "dev_id", "function_code", "transaction_id"): + assert x in x_str + for key in params: + assert key in x_str + case _: # pragma: no cover + raise TypeError("Test case not handled!") - def test_pdu_verify_count(self): - """Test verify functions.""" - req = reg_msg.ReadHoldingRegistersRequest(address=2, count=0) - req.count = 0 - with pytest.raises(ValueError): # noqa: PT011 - req.verifyCount(100) - req.count = 101 - with pytest.raises(ValueError): # noqa: PT011 - req.verifyCount(100) - with pytest.raises(ValueError): # noqa: PT011 - req.verifyCount(100, count=0) + @pytest.mark.parametrize("is_request", [True, False]) + @pytest.mark.parametrize("cases", ["to_bytes", "from_bytes", "calc"]) + def test_to_from_bytes(self, is_request, cases, paramPDU): + """Ensure that __init__ runs correctly.""" + if is_request: + pdu, _api, params, msg, params2, _msg = paramPDU + else: + pdu, _api, params2, _msg, params, msg = paramPDU + params_comb = params | params2 + match cases: + case "to_bytes": + x = pdu(0, is_request=is_request, **params) + fc = x.function_code.to_bytes(1, "big") + assert msg == fc + x.to_bytes() + x = pdu(0, is_request=is_request, **params_comb) + assert msg == fc + x.to_bytes() + case "from_bytes": + result = pdu(0, is_request=is_request, **params) + x = pdu(0, is_request=is_request) + x.from_bytes(1, msg) + if "bits" in params: + for entry in (result, x): + if j := len(entry.bits) % 8: + entry.bits += [False] * (8 - j) + entry.count = len(result.bits) + for key in params: + assert getattr(result, key) == getattr(x, key) + result_str = str(result) + x_str = str(x) + for entry in params: + assert entry in result_str + assert entry in x_str + case "calc": + x = pdu(0, is_request=is_request) + x.from_bytes(1, msg) + assert len(msg) + 3 == x.calculatePduSize(is_request, 0, b"\x01" + msg) + case _: # pragma: no cover + raise TypeError("Test case not handled!") + @pytest.mark.parametrize("is_request", [True, False]) @pytest.mark.parametrize( - ("pdutype", "args", "kwargs", "frame"), requests + responses + "cases", ["isError", "to_bytes", "from_bytes", "build", "calc", "str"] ) - @pytest.mark.usefixtures("args") - def test_pdu_instance_encode(self, pdutype, kwargs, frame): - """Test that all PDU types can be created.""" - pdu = pdutype(**kwargs) - res_frame = pdutype.function_code.to_bytes(1, "big") + pdu.x_encode() - assert res_frame == frame - - @pytest.mark.parametrize( - ("pdutype", "args", "kwargs", "frame"), requests + responses - ) - @pytest.mark.usefixtures("args") - def test_pdu_decode(self, pdutype, kwargs, frame): - """Test pdu decode.""" - pdu = pdutype(**kwargs) - pdu.x_decode(frame[1:]) - - @pytest.mark.parametrize(("pdutype", "args", "kwargs", "frame"), requests) - @pytest.mark.usefixtures("frame", "args") - async def test_pdu_datastore(self, pdutype, kwargs, mock_server_context): - """Test that all PDU types can be created.""" - control = ModbusControlBlock() - try: - pdu = pdutype(**kwargs) - assert await pdu.datastore_update(mock_server_context(), 1) - finally: - control.Delimiter = b"\n" + async def test_PDUexception(self, is_request, cases): + """Ensure that PDUexception is correct.""" + pdu = PDUexception(2, 0x01, 0x02, is_request=is_request) + match cases: + case "isError": + assert pdu.isError() + case "to_bytes": + assert pdu.to_bytes() == b"\x02" + case "from_bytes": + pdu2 = PDUexception(2, 0x01, 0x02, is_request=is_request) + pdu2.from_bytes(0, b"\x84\x04") + assert pdu2.function_code == 0x84 + assert pdu2.exception_code == 0x04 + case "calc": + assert pdu.calculatePduSize(is_request, 0, b"\x81\x05") == 5 + case "build": + if is_request: + x = await pdu.build_response(mock.AsyncMock(), mock.AsyncMock()) + assert isinstance(x, PDUexception) + assert x.exception_code == ExcCodes.ILLEGAL_FUNCTION + case "str": + x_str = str(pdu) + for x in ("PDUexception", "dev_id", "function_code", "transaction_id"): + assert x in x_str + case _: # pragma: no cover + raise TypeError("Test case not handled!") + @pytest.mark.parametrize("pack", ["pack", "pack8", "unpack"]) @pytest.mark.parametrize( - ("bytestream", "bitlist"), + ("pack_run", "bytestream", "bitlist", "padding"), [ - (b"\x01\x00", [True] + [False] * 15), - (b"\x00\x80", [False] * 15 + [True]), - (b"\x00\x01", [False] * 8 + [True] + [False] * 7), - (b"\x01\x80", [True] + [False] * 14 + [True]), - (b"\x00\x05", [False] * 8 + [True, False, True] + [False] * 5), - (b"\x01\x05", [True] + [False] * 7 + [True, False, True] + [False] * 5), - ( + ( # 0 + 1, + b"\x01", + [True], + 7, + ), + ( # 1 + 1, + b"\x05", + [True, False, True], + 5, + ), + ( # 2 + 2, + b"\x01\x00", + [True] + [False] * 15, + 0, + ), + ( # 3 + 2, + b"\x01\x00", + [True], + 15, + ), + ( # 4 + 3, + b"\x00\x80", + [False] * 15 + [True], + 0, + ), + ( # 5 + 3, + b"\x00\x01", + [False] * 8 + [True] + [False] * 7, + 0, + ), + ( # 6 + 3, + b"\x01\x80", + [True] + [False] * 14 + [True], + 0, + ), + ( # 7 + 3, + b"\x00\x05", + [False] * 8 + [True, False, True] + [False] * 5, + 0, + ), + ( # 8 + 3, + b"\x01\x05", + [True] + [False] * 7 + [True, False, True] + [False] * 5, + 0, + ), + ( # 9 + 3, b"\x81\x05", [True] + [False] * 6 + [True, True, False, True] + [False] * 5, + 0, ), - ( + ( # 10 + 3, b"\x00\x01\x81\x05", [False] * 8 + [True] @@ -552,13 +241,34 @@ async def test_pdu_datastore(self, pdutype, kwargs, mock_server_context): + [False] * 6 + [True, True, False, True] + [False] * 5, + 0, ), - (b"\x01\x00", [True]), - (b"\x00\x01", [False] * 8 + [True]), - (b"\x00\x05", [False] * 8 + [True, False, True]), - (b"\x01\x05", [True] + [False] * 7 + [True, False, True]), - (b"\x81\x05", [True] + [False] * 6 + [True, True, False, True]), - ( + ( # 11 + 3, + b"\x00\x01", + [False] * 8 + [True], + 7, + ), + ( # 12 + 3, + b"\x00\x05", + [False] * 8 + [True, False, True], + 5, + ), + ( # 13 + 3, + b"\x01\x05", + [True] + [False] * 7 + [True, False, True], + 5, + ), + ( # 14 + 3, + b"\x81\x05", + [True] + [False] * 6 + [True, True, False, True], + 5, + ), + ( # 15 + 3, b"\x00\x01\x81\x05", [False] * 8 + [True] @@ -566,78 +276,108 @@ async def test_pdu_datastore(self, pdutype, kwargs, mock_server_context): + [True] + [False] * 6 + [True, True, False, True], + 5, ), ], ) - def test_bit_packing(self, bytestream, bitlist): + def test_bit_packing(self, pack, bytestream, pack_run, bitlist, padding): """Test all string <=> bit packing functions.""" - assert pack_bitstring(bitlist, align_byte=False) == bytestream + original = copy(bitlist) + match pack: + case "pack": + if pack_run in (2, 3): + assert pack_bitstring(bitlist, align_byte=False) == bytestream + case "pack8": + if pack_run in (1, 3): + original = copy(bitlist) + assert pack_bitstring(bitlist, align_byte=True) == bytestream + assert original == bitlist + case "unpack": + original_b = copy(bytestream) + assert unpack_bitstring(bytestream) == bitlist + [False] * padding + assert original_b == bytestream + case _: # pragma: no cover + raise TypeError("Test case not handled!") + assert original == bitlist - @pytest.mark.parametrize( - ("bytestream", "bitlist"), - [ - (b"\x01", [True]), - (b"\x00\x01", [False] * 8 + [True]), - (b"\x05", [True, False, True]), - (b"\x01\x05", [True] + [False] * 7 + [True, False, True]), - ], - ) - def test_bit_packing8(self, bytestream, bitlist): - """Test all string <=> bit packing functions.""" - assert pack_bitstring(bitlist) == bytestream + def test_pdu_register_as_byte(self): + """Test verify functions.""" + registers = [int.from_bytes(b"ab", "big"), int.from_bytes(b"cd", "big")] + # NOT ALLOWED, NO conversion. + req = reg_msg.PDUreadHoldingRegisters( + 0, address=117, registers=registers, count=3 + ) + assert len(req.registers) == 2 + assert req.registers[0] == 24930 + assert req.registers[1] == 25444 - @pytest.mark.parametrize( - ("align_byte", "expected"), - [ - (True, b"\x05"), - (False, b"\x05\x00"), - ], - ) - def test_pack_bitstring_does_not_mutate_input( - self, - align_byte, - expected, - ): - """Test that pack_bitstring leaves the input list unchanged.""" - bits = [True, False, True] - original = bits.copy() + @pytest.mark.parametrize("cases", ["lookup"]) + def test_decoder1(self, cases, paramPDU): + """Test decoder.""" + pdu, _api, _params, msg, _params2, _msg = paramPDU + match cases: + case "lookup": + result = DecodePDU._lookupPduClass(b"\x01" + msg) + assert result == pdu + case _: # pragma: no cover + raise TypeError("Test case not handled!") - result = pack_bitstring(bits, align_byte=align_byte) + @pytest.mark.parametrize("is_request", [True, False]) + @pytest.mark.parametrize("cases", ["decode", "encode", "calc"]) + def test_decoder2(self, is_request, cases, paramPDU): + """Test decoder.""" + if is_request: + pdu, _api, params, msg, _params2, _msg = paramPDU + else: + pdu, _api, _params2, _msg, params, msg = paramPDU + match cases: + case "decode": + x_pdu = DecodePDU(is_request).decode(msg) + assert isinstance(x_pdu, pdu) + case "encode": + x_pdu = pdu(0, is_request=is_request, **params) + x_msg = DecodePDU(is_request).encode(x_pdu) + assert x_msg == msg[1:] + case "calc": + x = pdu(0, is_request=is_request) + x.from_bytes(1, msg) + assert len(msg) + 3 == DecodePDU(is_request).calculateRtuFrameSize( + b"\x01" + msg + ) + case _: # pragma: no cover + raise TypeError("Test case not handled!") - assert result == expected - assert bits == original + def test_decoder_no_fc(self): + """Test lookup for responses.""" + msg = b"\x01\x7f\x70" + pdu = DecodePDU._lookupPduClass(msg) + assert not pdu + pdu = DecodePDU(True).calculateRtuFrameSize(msg) + assert not pdu - @pytest.mark.parametrize( - ("bytestream", "bitlist"), - [ - (b"\x01", [True] + [False] * 7), - (b"\x01\x00", [True] + [False] * 15), - (b"\x00\x01", [False] * 8 + [True] + [False] * 7), - (b"\x00\x80", [False] * 15 + [True]), - (b"\x01\x80", [True] + [False] * 14 + [True]), - (b"\x00\x05", [False] * 8 + [True, False, True] + [False] * 5), - (b"\x01\x05", [True] + [False] * 7 + [True, False, True] + [False] * 5), - ( - b"\x81\x05", - [True] + [False] * 6 + [True, True, False, True] + [False] * 5, - ), - ( - b"\x05\x81\x01\x00", - [True, False, True] - + [False] * 5 - + [True] - + [False] * 6 - + [True] - + [True] - + [False] * 7 - + [False] * 8, - ), - ], - ) - def test_bit_unpacking(self, bytestream, bitlist): - """Test all string <=> bit packing functions.""" - assert unpack_bitstring(bytestream) == bitlist + def test_decoder_no_sub(self): + """Test for unknown sub code.""" + with pytest.raises(ModbusIOException): + DecodePDU(True).decode(b"\x08\x00\xf0\xf0\x00") + + def test_decoder_exc(self): + """Test lookup for exception response.""" + pdu = DecodePDU._lookupPduClass(b"\x01\x81\x04") + assert pdu == PDUexception + + @pytest.mark.parametrize(("frame"), [b"", b"\x01", b"NO FRAME"]) + def test_decoder_bad_frame(self, frame): + """Test lookup bad frames.""" + with pytest.raises(ModbusIOException): + DecodePDU(True).decode(frame) + + @pytest.mark.parametrize(("message"), [b"\x0c", 12, [12], [12, 24]]) + def test_diag_message(self, message): + """Test diag message.""" + PDUreturnQueryData(0, message=message) - def test_ExceptionResponse_str(self): - """Test string conversion of ExceptionResponse.""" - assert str(self.exception) != "" + @pytest.mark.parametrize(("message"), [b"\x0c", 12, [12], [12, 24]]) + async def test_diag_message2(self, message): + """Test diag message.""" + pdu = PDUgetClearModbusPlus(0, message=CLEAR_STATISTICS) + assert await pdu.build_response(mock.AsyncMock(), mock.AsyncMock()) diff --git a/test/pdu/test_register_read_messages.py b/test/pdu/test_register_read_messages.py index 1bd10986ef..69309838ea 100644 --- a/test/pdu/test_register_read_messages.py +++ b/test/pdu/test_register_read_messages.py @@ -5,17 +5,10 @@ import pytest +import pymodbus.pdu.register_message as pdu_reg from pymodbus.constants import ExcCodes from pymodbus.exceptions import ModbusIOException -from pymodbus.pdu.decoders import DecodePDU -from pymodbus.pdu.register_message import ( - ReadHoldingRegistersRequest, - ReadHoldingRegistersResponse, - ReadInputRegistersRequest, - ReadInputRegistersResponse, - ReadWriteMultipleRegistersRequest, - ReadWriteMultipleRegistersResponse, -) +from pymodbus.pdu import DecodePDU TEST_MESSAGE = b"\x06\x00\x0a\x00\x0b\x00\x0c" @@ -31,73 +24,25 @@ class TestReadRegisterMessages: * Read Holding Registers """ - values: list - request_read: dict - response_read: dict - - def setup_method(self): - """Initialize the test environment and builds request/result encoding pairs.""" - arguments = { - "read_address": 1, - "read_count": 5, - "write_address": 1, - } - self.values = [0xA, 0xB, 0xC] - self.request_read = { - ReadHoldingRegistersRequest(address=1, count=5): b"\x00\x01\x00\x05", - ReadInputRegistersRequest(address=1, count=5): b"\x00\x01\x00\x05", - ReadWriteMultipleRegistersRequest( - write_registers=[0x00] * 5, - **arguments, - ): b"\x00\x01\x00\x05\x00\x01\x00" - b"\x05\x0a\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00", - ReadWriteMultipleRegistersRequest( - write_registers=[0xAB], - **arguments, - ): b"\x00\x01\x00\x05\x00\x01\x00\x01\x02\x00\xab", - } - self.response_read = { - ReadHoldingRegistersResponse(registers=self.values): TEST_MESSAGE, - ReadInputRegistersResponse(registers=self.values): TEST_MESSAGE, - ReadWriteMultipleRegistersResponse(registers=self.values): TEST_MESSAGE, - } - - def test_register_read_requests(self): - """Test register read requests.""" - for request, response in iter(self.request_read.items()): - assert request.x_encode() == response - - def test_register_read_responses(self): - """Test register read response.""" - for request, response in iter(self.response_read.items()): - assert request.x_encode() == response - - def test_register_read_response_decode(self): - """Test register read response.""" - for response, packet in self.response_read.items(): - response.x_decode(packet) - assert response.registers == self.values - def test_register_read_response_decode_error(self): """Test register read response.""" - reg = ReadHoldingRegistersResponse(count=5) + reg = pdu_reg.PDUreadHoldingRegisters(0, is_request=False, count=5) with pytest.raises(ModbusIOException) as exc_info: - reg.x_decode(b"\x14\x00\x03\x00\x11") + reg.from_bytes(0, b"\x14\x00\x03\x00\x11") assert exc_info.value.fcode == reg.function_code + 0x80 def test_register_input_response_decode_error(self): """Test register read response.""" - reg = ReadInputRegistersResponse(count=5) + reg = pdu_reg.PDUreadHoldingRegisters(0, is_request=False, count=5) with pytest.raises(ModbusIOException) as exc_info: - reg.x_decode(b"\x14\x00\x03\x00\x11") + reg.from_bytes(0, b"\x14\x00\x03\x00\x11") assert exc_info.value.fcode == reg.function_code + 0x80 def test_readwrite_response_decode_error(self): """Test register read response.""" - reg = ReadWriteMultipleRegistersResponse(count=5) + reg = pdu_reg.PDUreadWriteMultipleRegisters(0, is_request=False, count=5) with pytest.raises(ModbusIOException) as exc_info: - reg.x_decode(b"\x14\x00\x03\x00\x11") + reg.from_bytes(0, b"\x14\x00\x03\x00\x11") assert exc_info.value.fcode == reg.function_code + 0x80 def test_readwrite_encode_write_count_limit(self): @@ -108,127 +53,121 @@ def test_readwrite_encode_write_count_limit(self): """ def build(count): - return ReadWriteMultipleRegistersRequest( + return pdu_reg.PDUreadWriteMultipleRegisters( + 0, read_address=1, - read_count=1, + count=1, write_address=1, write_registers=[0] * count, ) - assert len(build(121).x_encode()) + 1 <= 253 + assert len(build(121).to_bytes()) + 1 <= 253 with pytest.raises(ValueError): # noqa: PT011 - build(122).x_encode() + build(122).to_bytes() def test_readwrite_decode_write_count_above_limit(self): """Decoding must accept 122..125 so the server can answer ILLEGAL_VALUE. Raising here would make the server drop the frame without a response. """ - request = ReadWriteMultipleRegistersRequest() - request.x_decode(struct.pack(">HHHHB", 1, 1, 1, 122, 244) + b"\x00\x00" * 122) + request = pdu_reg.PDUreadWriteMultipleRegisters(0) + request.from_bytes( + 0, struct.pack(">HHHHB", 1, 1, 1, 122, 244) + b"\x00\x00" * 122 + ) assert request.write_count == 122 - def test_readwrite_decode_read_count_above_limit(self): - """Decoding must accept read_count above 125 so the server can answer 0x03.""" - request = ReadWriteMultipleRegistersRequest() - request.x_decode(struct.pack(">HHHHB", 1, 126, 1, 1, 2) + b"\x00\x00") - assert request.read_count == 126 + def test_readwrite_decode_count_above_limit(self): + """Decoding must accept count above 125 so the server can answer 0x03.""" + request = pdu_reg.PDUreadWriteMultipleRegisters(0) + request.from_bytes(0, struct.pack(">HHHHB", 1, 126, 1, 1, 2) + b"\x00\x00") + assert request.count == 126 - async def test_readwrite_read_count_above_limit_returns_illegal_value( - self, mock_server_context - ): + async def test_readwrite_count_above_limit_returns_illegal_value(self): """An out-of-range FC23 read count must answer 0x03.""" - context = mock_server_context() - context.async_getValues = mock.AsyncMock() - context.async_setValues = mock.AsyncMock() - request = ReadWriteMultipleRegistersRequest( - read_address=1, read_count=126, write_address=1, write_registers=[5] + getV = mock.AsyncMock() + setV = mock.AsyncMock() + request = pdu_reg.PDUreadWriteMultipleRegisters( + 0, read_address=1, count=126, write_address=1, write_registers=[5] ) - result = await request.datastore_update(context, 1) + result = await request.build_response(getV, setV) assert result.function_code == 0x97 assert result.exception_code == ExcCodes.ILLEGAL_VALUE - context.async_getValues.assert_not_awaited() - context.async_setValues.assert_not_awaited() + getV.assert_not_awaited() + setV.assert_not_awaited() - async def test_readwrite_read_count_at_max_returns_normal_response( - self, mock_server_context - ): + async def test_readwrite_count_at_max_returns_normal_response(self): """A read count exactly at MAX_READ_COUNT must be accepted.""" - context = mock_server_context() - request = ReadWriteMultipleRegistersRequest( + request = pdu_reg.PDUreadWriteMultipleRegisters( + 0, read_address=1, - read_count=ReadWriteMultipleRegistersRequest.MAX_READ_COUNT, + count=pdu_reg.PDUreadWriteMultipleRegisters.MAX_READ_COUNT, write_address=1, write_registers=[5], ) - result = await request.datastore_update(context, 1) + result = await request.build_response(mock.AsyncMock(), mock.AsyncMock()) assert result.function_code == request.function_code - async def test_register_read_requests_count_errors(self, mock_server_context): + async def test_register_read_requests_count_errors(self): """This tests that the register request messages. will break on counts that are out of range """ - context = mock_server_context() requests = [ - ReadHoldingRegistersRequest(address=1, count=0x800), - ReadInputRegistersRequest(address=1, count=0x800), - ReadWriteMultipleRegistersRequest( - read_address=1, read_count=0x800, write_address=1, write_registers=[5] + pdu_reg.PDUreadHoldingRegisters(0, address=1, count=0x800), + pdu_reg.PDUreadHoldingRegisters(0, address=1, count=0x800), + pdu_reg.PDUreadWriteMultipleRegisters( + 0, read_address=1, count=0x800, write_address=1, write_registers=[5] ), - ReadWriteMultipleRegistersRequest( - read_address=1, read_count=5, write_address=1, write_registers=[] + pdu_reg.PDUreadWriteMultipleRegisters( + 0, read_address=1, count=5, write_address=1, write_registers=[] ), ] for request in requests: - result = await request.datastore_update(context, 0) + result = await request.build_response(mock.AsyncMock(), mock.AsyncMock()) assert result.exception_code == ExcCodes.ILLEGAL_VALUE @pytest.mark.parametrize( ("request_class", "function_code"), [ - (ReadHoldingRegistersRequest, 3), - (ReadInputRegistersRequest, 4), + (pdu_reg.PDUreadHoldingRegisters, 3), + (pdu_reg.PDUreadHoldingRegisters, 4), ], ) async def test_read_register_count_above_max_returns_illegal_value( - self, request_class, function_code, mock_server_context + self, request_class, function_code ): """An out-of-range read quantity must answer 0x03, not silence.""" - context = mock_server_context() - context.async_getValues = mock.AsyncMock(return_value=[1] * 10) - request = request_class(address=0, count=request_class.MAX_COUNT + 1) - result = await request.datastore_update(context, 1) + getV = mock.AsyncMock(return_value=[1] * 10) + request = request_class(0, address=0, count=request_class.MAX_COUNT + 1) + result = await request.build_response(getV, mock.AsyncMock()) assert result.function_code == function_code | 0x80 assert result.exception_code == ExcCodes.ILLEGAL_VALUE - context.async_getValues.assert_not_awaited() + getV.assert_not_awaited() @pytest.mark.parametrize( "request_class", - [ReadHoldingRegistersRequest, ReadInputRegistersRequest], + [pdu_reg.PDUreadHoldingRegisters], ) - async def test_read_register_count_zero_returns_illegal_value( - self, request_class, mock_server_context - ): + async def test_read_register_count_zero_returns_illegal_value(self, request_class): """A zero read quantity must answer 0x03.""" - context = mock_server_context() - context.async_getValues = mock.AsyncMock(return_value=[1] * 10) - result = await request_class(address=0, count=0).datastore_update(context, 1) + getV = mock.AsyncMock(return_value=[1] * 10) + result = await request_class(0, address=0, count=0).build_response( + getV, mock.AsyncMock() + ) assert result.function_code == request_class.function_code | 0x80 assert result.exception_code == ExcCodes.ILLEGAL_VALUE - context.async_getValues.assert_not_awaited() + getV.assert_not_awaited() @pytest.mark.parametrize( "request_class", - [ReadHoldingRegistersRequest, ReadInputRegistersRequest], + [pdu_reg.PDUreadHoldingRegisters], ) async def test_read_register_count_at_max_returns_normal_response( - self, request_class, mock_server_context + self, request_class ): """A count exactly at the maximum must be accepted, not rejected.""" - context = mock_server_context() - request = request_class(address=0, count=request_class.MAX_COUNT) - result = await request.datastore_update(context, 1) + request = request_class(0, address=0, count=request_class.MAX_COUNT) + result = await request.build_response(mock.AsyncMock(), mock.AsyncMock()) assert result.function_code == request_class.function_code def test_read_register_decode_accepts_count_above_max(self): @@ -245,42 +184,39 @@ def test_read_register_decode_accepts_count_above_max(self): pdu = decoder.decode(frame) assert pdu.count == expected_count - async def test_register_read_requests_verify_errors(self, mock_server_context): + async def test_register_read_requests_verify_errors(self): """This tests that the register request messages. will break on counts that are out of range """ - context = mock_server_context() requests = [ - ReadHoldingRegistersRequest(address=-1, count=5), - ReadInputRegistersRequest(address=-1, count=5), - ReadWriteMultipleRegistersRequest(-1, 5, 1, [5]), - ReadWriteMultipleRegistersRequest(1, 5, -1, [5]), + pdu_reg.PDUreadHoldingRegisters(0, address=-1, count=5), + pdu_reg.PDUreadHoldingRegisters(0, address=-1, count=5), + pdu_reg.PDUreadWriteMultipleRegisters(0, -1, 5, 1, [5]), + pdu_reg.PDUreadWriteMultipleRegisters(0, 1, 5, -1, [5]), ] for request in requests: - await request.datastore_update(context, 0) + await request.build_response(mock.AsyncMock(), mock.AsyncMock()) - async def test_register_read_requests_datastore_update(self, mock_server_context): + async def test_register_read_requests_build_response(self): """This tests that the register request messages. will break on counts that are out of range """ - context = mock_server_context(True) requests = [ - ReadHoldingRegistersRequest(address=-1, count=5), - ReadInputRegistersRequest(address=-1, count=5), + pdu_reg.PDUreadHoldingRegisters(0, address=-1, count=5), + pdu_reg.PDUreadHoldingRegisters(0, address=-1, count=5), ] for request in requests: - response = await request.datastore_update(context, 1) + response = await request.build_response(mock.AsyncMock(), mock.AsyncMock()) assert request.function_code == response.function_code - async def test_read_write_multiple_registers_request(self, mock_server_context): + async def test_read_write_multiple_registers_request(self): """Test read/write multiple registers.""" - context = mock_server_context(True) - request = ReadWriteMultipleRegistersRequest( - read_address=1, read_count=10, write_address=1, write_registers=[0x00] + request = pdu_reg.PDUreadWriteMultipleRegisters( + 0, read_address=1, count=10, write_address=1, write_registers=[0x00] ) - response = await request.datastore_update(context, 0) + response = await request.build_response(mock.AsyncMock(), mock.AsyncMock()) assert request.function_code == response.function_code @pytest.mark.parametrize( @@ -294,80 +230,65 @@ async def test_read_write_multiple_registers_request(self, mock_server_context): ], ) async def test_read_write_multiple_registers_rejects_invalid_byte_count( - self, frame, mock_server_context + self, frame ): """Test inconsistent write byte counts are rejected before writing.""" - request = ReadWriteMultipleRegistersRequest() - request.x_decode(frame) - context = mock_server_context() - context.async_setValues = mock.AsyncMock() + request = pdu_reg.PDUreadWriteMultipleRegisters(0) + request.from_bytes(0, frame) + setV = mock.AsyncMock() - result = await request.datastore_update(context, 1) + result = await request.build_response(mock.AsyncMock(), setV) assert result.exception_code == ExcCodes.ILLEGAL_VALUE - context.async_setValues.assert_not_awaited() + setV.assert_not_awaited() - async def test_read_write_multiple_registers_accepts_valid_byte_count( - self, mock_server_context - ): + async def test_read_write_multiple_registers_accepts_valid_byte_count(self): """Test a consistent write byte count reaches the datastore.""" - request = ReadWriteMultipleRegistersRequest() - request.x_decode(b"\x00\x01\x00\x01\x00\x02\x00\x02\x04\x00\x0a\x00\x0b") - context = mock_server_context() - context.async_setValues = mock.AsyncMock(return_value=0) + request = pdu_reg.PDUreadWriteMultipleRegisters(0) + request.from_bytes(0, b"\x00\x01\x00\x01\x00\x02\x00\x02\x04\x00\x0a\x00\x0b") + setV = mock.AsyncMock(return_value=0) - result = await request.datastore_update(context, 1) + result = await request.build_response(mock.AsyncMock(), setV) assert result.function_code == request.function_code - context.async_setValues.assert_awaited_once_with( - 1, request.function_code, 2, [0x0A, 0x0B] - ) + setV.assert_awaited_once_with(1, request.function_code, 2, [0x0A, 0x0B]) - async def test_read_write_multiple_registers_verify(self, mock_server_context): + async def test_read_write_multiple_registers_verify(self): """Test read/write multiple registers.""" - context = mock_server_context() - request = ReadWriteMultipleRegistersRequest( - read_address=1, read_count=0x200, write_address=2, write_registers=[0x00] + request = pdu_reg.PDUreadWriteMultipleRegisters( + 0, read_address=1, count=0x200, write_address=2, write_registers=[0x00] ) - response = await request.datastore_update(context, 0) + response = await request.build_response(mock.AsyncMock(), mock.AsyncMock()) assert response.exception_code == ExcCodes.ILLEGAL_VALUE - await request.datastore_update(context, 0) + await request.build_response(mock.AsyncMock(), mock.AsyncMock()) assert response.exception_code == ExcCodes.ILLEGAL_VALUE - request.write_byte_count = 0x100 - await request.datastore_update(context, 0) + # request.write_byte_count = 0x100 + await request.build_response(mock.AsyncMock(), mock.AsyncMock()) assert response.exception_code == ExcCodes.ILLEGAL_VALUE - async def test_register_datastore_exceptions(self, mock_server_context): + async def test_register_datastore_exceptions(self): """Test exception response from datastore.""" - context = mock_server_context() - context.async_getValues = mock.AsyncMock(return_value=ExcCodes.ILLEGAL_VALUE) + getV = mock.AsyncMock(return_value=ExcCodes.ILLEGAL_VALUE) for pdu in ( - ReadHoldingRegistersRequest(address=-1, count=5), - ReadInputRegistersRequest(address=-1, count=5), - ReadWriteMultipleRegistersRequest(-1, 5, 1, [5], 1), + pdu_reg.PDUreadHoldingRegisters(0, address=-1, count=5), + pdu_reg.PDUreadHoldingRegisters(0, address=-1, count=5), + pdu_reg.PDUreadWriteMultipleRegisters(-1, 5, 1, [5], 1), ): - await pdu.datastore_update(context, 1) - - def test_serializing_to_string(self): - """Test serializing to string.""" - for request in iter(self.request_read.keys()): - assert str(request) - for request in iter(self.response_read.keys()): - assert str(request) + await pdu.build_response(getV, mock.AsyncMock()) @pytest.mark.parametrize( ("request_pdu"), [ - ReadWriteMultipleRegistersRequest( - read_address=1, read_count=5, write_address=1, write_registers=[5] + pdu_reg.PDUreadWriteMultipleRegisters( + 0, 0, address=1, count=5, write_address=1, write_registers=[5] ), ], ) - async def test_register_read_exception(self, request_pdu, mock_server_context): + async def test_register_read_exception(self, request_pdu): """Test write single coil.""" - context = mock_server_context(True, default=True) - context.async_setValues = mock.AsyncMock(return_value=1) - result = await request_pdu.datastore_update(context, 1) + result = await request_pdu.build_response( + mock.AsyncMock(), mock.AsyncMock(return_value=1) + ) assert result.exception_code == 1 diff --git a/test/pdu/test_register_write_messages.py b/test/pdu/test_register_write_messages.py index 7a6d518e85..d5171a6d55 100644 --- a/test/pdu/test_register_write_messages.py +++ b/test/pdu/test_register_write_messages.py @@ -4,15 +4,8 @@ import pytest +import pymodbus.pdu.register_message as pdu_reg from pymodbus.constants import ExcCodes -from pymodbus.pdu.register_message import ( - MaskWriteRegisterRequest, - MaskWriteRegisterResponse, - WriteMultipleRegistersRequest, - WriteMultipleRegistersResponse, - WriteSingleRegisterRequest, - WriteSingleRegisterResponse, -) # ---------------------------------------------------------------------------# @@ -35,25 +28,25 @@ def setup_method(self): self.value = 0xABCD self.values = [0xA, 0xB, 0xC] self.write = { - WriteSingleRegisterRequest( + pdu_reg.PDUwriteSingleRegister( address=1, registers=[self.value] ): b"\x00\x01\xab\xcd", - WriteSingleRegisterResponse( + pdu_reg.PDUwriteSingleRegister( address=1, registers=[self.value] ): b"\x00\x01\xab\xcd", - WriteMultipleRegistersRequest( + pdu_reg.PDUwriteMultipleRegisters( address=1, registers=self.values ): b"\x00\x01\x00\x03\x06\x00\x0a\x00\x0b\x00\x0c", - WriteMultipleRegistersRequest( + pdu_reg.PDUwriteMultipleRegisters( address=1, registers=[0xD] ): b"\x00\x01\x00\x01\x02\x00\x0d", - WriteMultipleRegistersResponse(address=1, count=5): b"\x00\x01\x00\x05", + pdu_reg.PDUwriteMultipleRegisters(address=1, count=5): b"\x00\x01\x00\x05", } def test_register_write_requests_encode(self): """Test register write requests encode.""" for request, response in iter(self.write.items()): - assert request.x_encode() == response + assert request.to_bytes() == response def test_register_write_requests_decode(self): """Test register write requests decode.""" @@ -64,12 +57,12 @@ def test_register_write_requests_decode(self): ) for packet, address in zip(values, addresses): request, response = packet - request.x_decode(response) + request.from_bytes(0, response) assert request.address == address def test_invalid_write_multiple_registers_request(self): """Test invalid write multiple registers request.""" - request = WriteMultipleRegistersRequest(address=0, registers=None) + request = pdu_reg.PDUwriteMultipleRegisters(address=0, registers=None) assert not request.registers @pytest.mark.parametrize( @@ -82,102 +75,89 @@ def test_invalid_write_multiple_registers_request(self): b"\x00\x01\x00\x01\x01\x00", ], ) - async def test_write_multiple_registers_rejects_invalid_byte_count( - self, frame, mock_server_context - ): + async def test_write_multiple_registers_rejects_invalid_byte_count(self, frame): """Test inconsistent byte counts are rejected before writing.""" - request = WriteMultipleRegistersRequest() - request.x_decode(frame) - context = mock_server_context() - context.async_setValues = mock.AsyncMock() - - result = await request.datastore_update(context, 1) + request = pdu_reg.PDUwriteMultipleRegisters() + request.from_bytes(0, frame) + setValues = mock.AsyncMock() + result = await request.build_response(mock.AsyncMock(), setValues) assert result.exception_code == ExcCodes.ILLEGAL_VALUE - context.async_setValues.assert_not_awaited() + setValues.assert_not_awaited() - async def test_write_multiple_registers_accepts_valid_byte_count( - self, mock_server_context - ): + async def test_write_multiple_registers_accepts_valid_byte_count(self): """Test a consistent byte count reaches the datastore.""" - request = WriteMultipleRegistersRequest() - request.x_decode(b"\x00\x01\x00\x02\x04\x00\x0a\x00\x0b") - context = mock_server_context() - context.async_setValues = mock.AsyncMock(return_value=0) - - result = await request.datastore_update(context, 1) + request = pdu_reg.PDUwriteMultipleRegisters() + request.from_bytes(0, b"\x00\x01\x00\x02\x04\x00\x0a\x00\x0b") + setValues = mock.AsyncMock(return_value=0) + result = await request.build_response(mock.AsyncMock(), setValues) assert result.count == 2 - context.async_setValues.assert_awaited_once_with( - 1, request.function_code, 1, [0x0A, 0x0B] - ) + setValues.assert_awaited_once_with(1, request.function_code, 1, [0x0A, 0x0B]) def test_serializing_to_string(self): """Test serializing to string.""" for request in iter(self.write.keys()): assert str(request) - async def test_write_single_register_request(self, mock_server_context): + async def test_write_single_register_request(self): """Test write single register request.""" - context = mock_server_context() - request = WriteSingleRegisterRequest(address=0x00, registers=[0xF0000]) - result = await request.datastore_update(context, 1) + request = pdu_reg.PDUwriteSingleRegister(address=0x00, registers=[0xF0000]) + result = await request.build_response(mock.AsyncMock(), mock.AsyncMock()) assert result.exception_code == ExcCodes.ILLEGAL_VALUE request.registers = [0x00FF] - result = await request.datastore_update(context, 1) + result = await request.build_response(mock.AsyncMock(), mock.AsyncMock()) # assert result.exception_code == ExcCodes.ILLEGAL_ADDRESS - context.valid = True - result = await request.datastore_update(context, 1) + # context.valid = True + result = await request.build_response(mock.AsyncMock(), mock.AsyncMock()) assert result.function_code == request.function_code - async def test_write_multiple_register_request(self, mock_server_context): + async def test_write_multiple_register_request(self): """Test write multiple register request.""" - context = mock_server_context() - request = WriteMultipleRegistersRequest(address=0x00, registers=[0x00] * 10) - result = await request.datastore_update(context, 1) + request = pdu_reg.PDUwriteMultipleRegisters(address=0x00, registers=[0x00] * 10) + result = await request.build_response(mock.AsyncMock(), mock.AsyncMock()) request.count = 0x800 # outside of range - result = await request.datastore_update(context, 1) + result = await request.build_response(mock.AsyncMock(), mock.AsyncMock()) assert result.exception_code == ExcCodes.ILLEGAL_VALUE - context.valid = True - request = WriteMultipleRegistersRequest(address=0x00, registers=[0x00] * 10) - result = await request.datastore_update(context, 1) + # context.valid = True + request = pdu_reg.PDUwriteMultipleRegisters(address=0x00, registers=[0x00] * 10) + result = await request.build_response(mock.AsyncMock(), mock.AsyncMock()) assert result.function_code == request.function_code - request = WriteMultipleRegistersRequest(address=0x00, registers=[0x00]) - result = await request.datastore_update(context, 1) + request = pdu_reg.PDUwriteMultipleRegisters(address=0x00, registers=[0x00]) + result = await request.build_response(mock.AsyncMock(), mock.AsyncMock()) assert result.function_code == request.function_code - async def test_register_write_datastore_exceptions(self, mock_server_context): + async def test_register_write_datastore_exceptions(self): """Test exception response from datastore.""" - context = mock_server_context() - context.async_getValues = mock.AsyncMock(return_value=ExcCodes.ILLEGAL_VALUE) + getV = mock.AsyncMock(return_value=ExcCodes.ILLEGAL_VALUE) for pdu in ( - WriteSingleRegisterRequest(address=0x00, registers=[0xF000]), - WriteMultipleRegistersRequest(address=0x00, registers=[0x00] * 10), - MaskWriteRegisterRequest(0x0000, 0x0101, 0x1010), + pdu_reg.PDUwriteSingleRegister(address=0x00, registers=[0xF000]), + pdu_reg.PDUwriteMultipleRegisters(address=0x00, registers=[0x00] * 10), + pdu_reg.PDUmaskWriteRegister(0x0000, 0x0101, 0x1010), ): - await pdu.datastore_update(context, 1) + await pdu.build_response(getV, mock.AsyncMock()) def test_mask_write_register_request_encode(self): """Test basic bit message encoding/decoding.""" - handle = MaskWriteRegisterRequest(0x0000, 0x0101, 0x1010) - result = handle.x_encode() + handle = pdu_reg.PDUmaskWriteRegister(0x0000, 0x0101, 0x1010) + result = handle.to_bytes() assert result == b"\x00\x00\x01\x01\x10\x10" def test_mask_write_register_request_decode(self): """Test basic bit message encoding/decoding.""" request = b"\x00\x04\x00\xf2\x00\x25" - handle = MaskWriteRegisterRequest() - handle.x_decode(request) + handle = pdu_reg.PDUmaskWriteRegister() + handle.from_bytes(0, request) assert handle.address == 0x0004 assert handle.and_mask == 0x00F2 assert handle.or_mask == 0x0025 - async def test_mask_write_register_request_datastore_update( + async def test_mask_write_register_request_build_response( self, MockLastValuesContext ): """Test write register request valid execution.""" @@ -188,26 +168,23 @@ async def test_mask_write_register_request_datastore_update( # and_mask=0, or_mask=F # and_mask=F, or_mask=F context = MockLastValuesContext(valid=True, default=0xAA55) - handle = MaskWriteRegisterRequest(0x0000, 0x0F0F, 0x00FF) - result = await handle.datastore_update(context, 1) - assert isinstance(result, MaskWriteRegisterResponse) + handle = pdu_reg.PDUmaskWriteRegister(0x0000, 0x0F0F, 0x00FF) + result = await handle.build_response(mock.AsyncMock(), mock.AsyncMock()) + assert isinstance(result, pdu_reg.PDUmaskWriteRegister) assert context.last_values == [0x0AF5] - async def test_mask_write_register_request_invalid_datastore_update( - self, mock_server_context - ): - """Test write register request datastore_update with invalid data.""" - context = mock_server_context(valid=False, default=0x0000) - handle = MaskWriteRegisterRequest(0x0000, -1, 0x1010) - result = await handle.datastore_update(context, 1) + async def test_mask_write_register_request_invalid_build_response(self): + """Test write register request build_response with invalid data.""" + handle = pdu_reg.PDUmaskWriteRegister(0x0000, -1, 0x1010) + result = await handle.build_response(mock.AsyncMock(), mock.AsyncMock()) assert result.exception_code == ExcCodes.ILLEGAL_VALUE - handle = MaskWriteRegisterRequest(0x0000, 0x0101, -1) - result = await handle.datastore_update(context, 1) + handle = pdu_reg.PDUmaskWriteRegister(0x0000, 0x0101, -1) + result = await handle.build_response(mock.AsyncMock(), mock.AsyncMock()) assert result.exception_code == ExcCodes.ILLEGAL_VALUE - handle = MaskWriteRegisterRequest(0x0000, 0x0101, 0x1010) - result = await handle.datastore_update(context, 1) + handle = pdu_reg.PDUmaskWriteRegister(0x0000, 0x0101, 0x1010) + result = await handle.build_response(mock.AsyncMock(), mock.AsyncMock()) # -----------------------------------------------------------------------# # Mask Write Register Response @@ -215,20 +192,20 @@ async def test_mask_write_register_request_invalid_datastore_update( def test_mask_write_register_response_encode(self): """Test basic bit message encoding/decoding.""" - handle = MaskWriteRegisterResponse(0x0000, 0x0101, 0x1010) - result = handle.x_encode() + handle = pdu_reg.PDUmaskWriteRegister(0x0000, 0x0101, 0x1010) + result = handle.to_bytes() assert result == b"\x00\x00\x01\x01\x10\x10" def test_mask_write_register_frame_size(self): """Test that the read fifo queue response can decode.""" - assert MaskWriteRegisterRequest.x_calculateRtuFrameSize(b"") == 10 - assert MaskWriteRegisterResponse.x_calculateRtuFrameSize(b"") == 10 + assert pdu_reg.PDUmaskWriteRegister.calculatePduSize(True, 0, b"") == 10 + assert pdu_reg.PDUmaskWriteRegister.calculatePduSize(False, 0, b"") == 10 def test_mask_write_register_response_decode(self): """Test basic bit message encoding/decoding.""" request = b"\x00\x04\x00\xf2\x00\x25" - handle = MaskWriteRegisterResponse() - handle.x_decode(request) + handle = pdu_reg.PDUmaskWriteRegister() + handle.from_bytes(0, request) assert handle.address == 0x0004 assert handle.and_mask == 0x00F2 assert handle.or_mask == 0x0025 @@ -236,14 +213,14 @@ def test_mask_write_register_response_decode(self): @pytest.mark.parametrize( ("request_pdu"), [ - WriteSingleRegisterRequest(address=0x00, registers=[5]), - WriteMultipleRegistersRequest(address=0x00, registers=[0x00] * 10), - MaskWriteRegisterRequest(0x0000, 0x01, 0x1010), + pdu_reg.PDUwriteSingleRegister(0, address=0x00, registers=[5]), + pdu_reg.PDUwriteMultipleRegisters(0, address=0x00, registers=[0x00] * 10), + pdu_reg.PDUmaskWriteRegister(0, 0x0000, 0x01, 0x1010), ], ) async def test_register_write_exception(self, request_pdu, mock_server_context): """Test write single coil.""" - context = mock_server_context(True, default=True) - context.async_setValues = mock.AsyncMock(return_value=1) - result = await request_pdu.datastore_update(context, 1) + result = await request_pdu.build_response( + mock.AsyncMock(), mock.AsyncMock(return_value=1) + ) assert result.exception_code == 1 diff --git a/test/server/test_base.py b/test/server/test_base.py index 35e8b29cf0..f0d7897fa6 100755 --- a/test/server/test_base.py +++ b/test/server/test_base.py @@ -7,7 +7,7 @@ from pymodbus.datastore import ModbusDeviceContext, ModbusServerContext from pymodbus.exceptions import ParameterException from pymodbus.framer import FramerType -from pymodbus.pdu import ReadHoldingRegistersRequest +from pymodbus.pdu.register_message import PDUreadHoldingRegisters from pymodbus.server import ModbusBaseServer from pymodbus.simulator import DataType, SimData, SimDevice from pymodbus.transport import CommParams, CommType @@ -35,7 +35,7 @@ async def baseserver(self): None, None, None, - [ReadHoldingRegistersRequest], + [PDUreadHoldingRegisters], ) return server @@ -60,7 +60,7 @@ async def test_base_simcore(self): None, None, None, - [ReadHoldingRegistersRequest], + [PDUreadHoldingRegisters], ) async def test_base_getValues(self): @@ -81,7 +81,7 @@ async def test_base_getValues(self): None, None, None, - [ReadHoldingRegistersRequest], + [PDUreadHoldingRegisters], ) result = await bs.async_getValues(0, 0x03, 0) assert result == [15] @@ -106,7 +106,7 @@ async def test_base_setValues(self): None, None, None, - [ReadHoldingRegistersRequest], + [PDUreadHoldingRegisters], ) await bs.async_setValues(1, 0x10, 1, [1]) result = await bs.async_getValues(1, 0x03, 1) @@ -163,5 +163,5 @@ async def test_base_old_context(self): None, None, None, - [ReadHoldingRegistersRequest], + [PDUreadHoldingRegisters], ) diff --git a/test/server/test_requesthandler.py b/test/server/test_requesthandler.py index 48481d8e96..49e981fc37 100755 --- a/test/server/test_requesthandler.py +++ b/test/server/test_requesthandler.py @@ -7,7 +7,7 @@ from pymodbus.exceptions import ModbusIOException, NoSuchIdException from pymodbus.framer import FramerType -from pymodbus.pdu import ExceptionResponse +from pymodbus.pdu import PDUexception from pymodbus.server import ModbusBaseServer from pymodbus.simulator import DataType, SimData, SimDevice from pymodbus.transport import CommParams, CommType @@ -74,7 +74,7 @@ async def test_rh_callback_data_undecodable_echoes_identity(self, requesthandler requesthandler.pdu_send.assert_called_once() response = requesthandler.pdu_send.call_args.args[0] - assert isinstance(response, ExceptionResponse) + assert isinstance(response, PDUexception) assert response.transaction_id == 0x000A assert response.dev_id == 7 assert response.function_code == 0x80 @@ -107,7 +107,7 @@ async def test_rh_callback_data_undecodable_real_socket_frame(self, requesthandl assert len(frame) == requesthandler.callback_data(frame, None) requesthandler.pdu_send.assert_called_once() response = requesthandler.pdu_send.call_args.args[0] - assert isinstance(response, ExceptionResponse) + assert isinstance(response, PDUexception) assert response.function_code == 0x89 assert response.exception_code == 0x01 assert response.transaction_id == 0x1234 @@ -142,7 +142,7 @@ async def test_rh_callback_exc( assert len(data) == requesthandler.callback_data(data, None) await asyncio.sleep(0.1) response = requesthandler.pdu_send.call_args.args[0] - assert isinstance(response, ExceptionResponse) + assert isinstance(response, PDUexception) assert response.function_code == 0xC1 assert response.exception_code == 0x01 assert response.transaction_id == 1 @@ -150,13 +150,13 @@ async def test_rh_callback_exc( async def test_rh_handle_request(self, requesthandler): """Test __init__.""" - requesthandler.last_pdu = ExceptionResponse(17) + requesthandler.last_pdu = PDUexception(0, 17, 0) await requesthandler.handle_request(requesthandler.last_pdu) - requesthandler.last_pdu.datastore_update = mock.AsyncMock() # type: ignore[method-assign] + requesthandler.last_pdu.build_response = mock.AsyncMock() # type: ignore[method-assign] requesthandler.server.broadcast_enable = True requesthandler.last_pdu.dev_id = 0 await requesthandler.handle_request(requesthandler.last_pdu) - requesthandler.last_pdu.datastore_update.side_effect = NoSuchIdException + requesthandler.last_pdu.build_response.side_effect = NoSuchIdException await requesthandler.handle_request(requesthandler.last_pdu) requesthandler.server.ignore_missing_devices = True await requesthandler.handle_request(requesthandler.last_pdu) @@ -164,4 +164,4 @@ async def test_rh_handle_request(self, requesthandler): async def test_rh_server_send(self, requesthandler): """Test __init__.""" requesthandler.server_send(None, None) - requesthandler.server_send(ExceptionResponse(17), None) + requesthandler.server_send(PDUexception(0, 17, 0), None) diff --git a/test/server/test_server_asyncio.py b/test/server/test_server_asyncio.py index 84437d240e..3bf8852a2d 100755 --- a/test/server/test_server_asyncio.py +++ b/test/server/test_server_asyncio.py @@ -330,7 +330,7 @@ async def test_async_tcp_server_modbus_error(self): BasicClient.data = TEST_DATA await self.start_server() with mock.patch( - "pymodbus.pdu.register_message.ReadHoldingRegistersRequest.datastore_update", + "pymodbus.pdu.register_message.ReadHoldingRegistersRequest.build_response", side_effect=NoSuchIdException, ): await self.connect_server() diff --git a/test/transaction/test_transaction.py b/test/transaction/test_transaction.py index 5dffb56db6..ea317cdc18 100755 --- a/test/transaction/test_transaction.py +++ b/test/transaction/test_transaction.py @@ -9,14 +9,9 @@ from pymodbus.client import ModbusBaseSyncClient from pymodbus.exceptions import ConnectionException, ModbusIOException from pymodbus.framer import FramerRTU, FramerSocket, FramerType -from pymodbus.pdu import DecodePDU, ExceptionResponse -from pymodbus.pdu.bit_message import ReadCoilsRequest, ReadCoilsResponse -from pymodbus.pdu.register_message import ( - ReadHoldingRegistersRequest, - ReadHoldingRegistersResponse, - ReadInputRegistersRequest, - ReadInputRegistersResponse, -) +from pymodbus.pdu import DecodePDU, PDUexception +from pymodbus.pdu.bit_message import PDUreadCoils +from pymodbus.pdu.register_message import PDUreadHoldingRegisters, PDUreadInputRegisters from pymodbus.transaction import TransactionManager from pymodbus.transport import CommType @@ -92,7 +87,7 @@ async def test_transaction_sync_pdu_send(self, use_clc): ) transact.is_sync = True transact.comm_params.handle_local_echo = True - transact.pdu_send(ExceptionResponse(0xFF), (0, 0)) + transact.pdu_send(PDUexception(0, 0xFF, 0), (0, 0)) assert transact.sent_buffer == b"\x01\xff\x00a\xf0" async def test_transaction_connect(self, use_clc): @@ -138,7 +133,7 @@ async def test_transaction_disconnect(self, use_clc): ) async def test_transaction_data(self, use_clc, test, is_server): """Test tracers in disconnect.""" - pdu = ExceptionResponse(0xFF) + pdu = PDUexception(0, 0xFF, 0) pdu.dev_id = 0 packet = b"\x00\x03\x00\x7c\x00\x02\x04\x02" transact = TransactionManager( @@ -172,7 +167,7 @@ async def test_transaction_data(self, use_clc, test, is_server): @pytest.mark.parametrize("test", [True, False]) async def test_transaction_data_2(self, use_clc, test): """Test tracers in disconnect.""" - pdu = ExceptionResponse(0xFF) + pdu = PDUexception(0, 0xFF, 0) packet = b"\x00\x03\x00\x7c\x00\x02\x04\x02" transact = TransactionManager( use_clc, @@ -195,10 +190,12 @@ async def test_transaction_data_2(self, use_clc, test): async def test_transaction_discard(self, use_clc): """Test next request does not get the response of a cancelled request.""" - req_org = ReadHoldingRegistersRequest(dev_id=1, count=4) - resp_org = ReadHoldingRegistersResponse(dev_id=1, registers=[1, 2, 3, 4]) - resp_fc = ReadInputRegistersResponse(dev_id=1, registers=[1, 2, 3, 4, 5]) - resp_id = ReadHoldingRegistersResponse(dev_id=2, registers=[1, 2, 3, 4, 5]) + req_org = PDUreadHoldingRegisters(1, count=4) + resp_org = PDUreadHoldingRegisters(1, is_request=False, registers=[1, 2, 3, 4]) + resp_fc = PDUreadInputRegisters(1, is_request=False, registers=[1, 2, 3, 4, 5]) + resp_id = PDUreadHoldingRegisters( + dev_id=2, is_request=False, registers=[1, 2, 3, 4, 5] + ) transact = TransactionManager( use_clc, FramerRTU(DecodePDU(False)), @@ -242,8 +239,10 @@ async def test_transaction_execute(self, use_clc, scenario): # noqa: C901 None, ) transact.send = mock.Mock() # type: ignore[method-assign] - request = ReadCoilsRequest(address=117, count=5, dev_id=1) - response = ReadCoilsResponse(bits=[True, False, True, True, False], dev_id=1) + request = PDUreadCoils(1, address=117, count=5) + response = PDUreadCoils( + 1, is_request=False, bits=[True, False, True, True, False] + ) transact.retries = 0 transact.connection_made(mock.AsyncMock()) transact.transport.write = mock.Mock() # type: ignore[method-assign, attr-defined] @@ -355,14 +354,18 @@ async def _cancel_send(self, transact, transport, delay_cond, pdu): ) async def test_transaction_execute_cancel(self, use_clc, cases): """Test next request does not get the response of a cancelled request.""" - req_org = ReadHoldingRegistersRequest(dev_id=1, count=4) - resp_org = ReadHoldingRegistersResponse(dev_id=1, registers=[1, 2, 3, 4]) - req_org2 = ReadHoldingRegistersRequest(dev_id=1, count=5) - resp_org2 = ReadHoldingRegistersResponse(dev_id=1, registers=[1, 2, 3, 4, 5]) - req_fc = ReadInputRegistersRequest(dev_id=1, count=5) - resp_fc = ReadInputRegistersResponse(dev_id=1, registers=[1, 2, 3, 4, 5]) - req_id = ReadHoldingRegistersRequest(dev_id=2, count=5) - resp_id = ReadHoldingRegistersResponse(dev_id=2, registers=[1, 2, 3, 4, 5]) + req_org = PDUreadHoldingRegisters(1, count=4) + resp_org = PDUreadHoldingRegisters(1, is_request=False, registers=[1, 2, 3, 4]) + req_org2 = PDUreadHoldingRegisters(1, is_request=False, count=5) + resp_org2 = PDUreadHoldingRegisters( + 1, is_request=False, registers=[1, 2, 3, 4, 5] + ) + req_fc = PDUreadInputRegisters(1, count=5) + resp_fc = PDUreadInputRegisters(1, is_request=False, registers=[1, 2, 3, 4, 5]) + req_id = PDUreadHoldingRegisters(2, count=5) + resp_id = PDUreadHoldingRegisters( + 2, is_request=False, registers=[1, 2, 3, 4, 5] + ) transact = TransactionManager( use_clc, FramerRTU(DecodePDU(False)), @@ -429,13 +432,13 @@ async def test_transaction_execute_cancel(self, use_clc, cases): @pytest.mark.parametrize( ("framer", "dev_id", "request_type", "response_type"), [ - (FramerRTU, 2, ReadHoldingRegistersRequest, ReadHoldingRegistersResponse), - (FramerRTU, 1, ReadInputRegistersRequest, ReadInputRegistersResponse), + (FramerRTU, 2, PDUreadHoldingRegisters, PDUreadHoldingRegisters), + (FramerRTU, 1, PDUreadInputRegisters, PDUreadInputRegisters), ( FramerSocket, 1, - ReadHoldingRegistersRequest, - ReadHoldingRegistersResponse, + PDUreadHoldingRegisters, + PDUreadHoldingRegisters, ), ], ) @@ -460,23 +463,23 @@ async def test_transaction_execute_cancel_no_wait( transact.connection_made(transport) transact.comm_params.timeout_connect = 5 resp = asyncio.create_task( - transact.execute(False, ReadHoldingRegistersRequest(dev_id=1, count=4)) + transact.execute(False, PDUreadHoldingRegisters(1, count=4)) ) await asyncio.sleep(0.1) resp.cancel() with pytest.raises(asyncio.CancelledError): await resp resp = asyncio.create_task( - transact.execute(False, request_type(dev_id=dev_id, count=5)) + transact.execute(False, request_type(dev_id, count=5)) ) await asyncio.sleep(0.1) assert transport.write.call_count == 2 - late = ReadHoldingRegistersResponse( - dev_id=1, transaction_id=1, registers=[1, 2, 3, 4] + late = PDUreadHoldingRegisters( + 1, is_request=False, transaction_id=1, registers=[1, 2, 3, 4] ) transact.data_received(transact.framer.buildFrame(late)) response = response_type( - dev_id=dev_id, transaction_id=2, registers=[1, 2, 3, 4, 5] + dev_id, is_request=False, transaction_id=2, registers=[1, 2, 3, 4, 5] ) transact.data_received(transact.framer.buildFrame(response)) assert (await resp).registers == response.registers @@ -493,14 +496,16 @@ async def test_transaction_receiver(self, use_clc): None, ) transact.send = mock.Mock() # type: ignore[method-assign] - response = ReadCoilsResponse(bits=[True, False, True, True, False], dev_id=0) + response = PDUreadCoils( + 0, is_request=False, bits=[True, False, True, True, False] + ) transact.retries = 0 transact.connection_made(mock.AsyncMock()) data = b"\x00\x00\x00\x00\x00\x06\x00\x01\x01\x02\x00\x04" transact.data_received(data) response = await transact.response_future - assert isinstance(response, ReadCoilsResponse) + assert isinstance(response, PDUreadCoils) @pytest.mark.parametrize("no_resp", [False, True]) async def test_client_protocol_execute_outside(self, use_clc, no_resp): @@ -516,7 +521,7 @@ async def test_client_protocol_execute_outside(self, use_clc, no_resp): ) transact.send = mock.Mock() # type: ignore[method-assign] transact.comm_params.timeout_connect = 0.1 - request = ReadCoilsRequest(address=117, count=5, dev_id=1) + request = PDUreadCoils(1, address=117, count=5) transact.retries = 0 transact.connection_made(mock.AsyncMock()) transact.transport.write = mock.Mock() # type: ignore[attr-defined] @@ -599,9 +604,11 @@ async def test_sync_transaction_execute(self, use_clc, scenario): # noqa: C901 ) transact.send = mock.Mock() # type: ignore[method-assign] transact.sync_client.connect = mock.Mock(return_value=True) - request = ReadCoilsRequest(address=117, count=5, dev_id=1) - response = ReadCoilsResponse( - bits=[True, False, True, True, False, False, False, False], dev_id=1 + request = PDUreadCoils(1, address=117, count=5) + response = PDUreadCoils( + 1, + is_request=False, + bits=[True, False, True, True, False, False, False, False], ) transact.retries = 0 if scenario == 0: # transport not ok and no connect @@ -703,9 +710,11 @@ def test_sync_transaction_receiver(self, use_clc): ) transact.sync_client.connect = mock.Mock(return_value=True) transact.sync_client.send = mock.Mock() - request = ReadCoilsRequest(address=117, count=5, dev_id=1) - response = ReadCoilsResponse( - bits=[True, False, True, True, False, False, False, False], dev_id=1 + request = PDUreadCoils(1, address=117, count=5) + response = PDUreadCoils( + 1, + is_request=False, + bits=[True, False, True, True, False, False, False, False], ) transact.retries = 0 transact.transport = 1 # type: ignore[assignment] @@ -730,9 +739,11 @@ def test_sync_execute_resets_count_until_disconnect(self, use_clc): sync_client=client, ) transact.sync_client.connect = mock.Mock(return_value=True) - request = ReadCoilsRequest(address=117, count=5, dev_id=1) - response = ReadCoilsResponse( - bits=[True, False, True, True, False, False, False, False], dev_id=1 + request = PDUreadCoils(1, address=117, count=5) + response = PDUreadCoils( + 1, + is_request=False, + bits=[True, False, True, True, False, False, False, False], ) transact.retries = 0 transact.transport = 1 # type: ignore[assignment] @@ -760,9 +771,11 @@ def test_sync_client_protocol_execute_outside(self, use_clc, no_resp): sync_client=client, ) transact.sync_client.connect = mock.Mock(return_value=True) - request = ReadCoilsRequest(address=117, count=5, dev_id=1) - response = ReadCoilsResponse( - bits=[True, False, True, True, False, False, False, False], dev_id=1 + request = PDUreadCoils(1, address=117, count=5) + response = PDUreadCoils( + 1, + is_request=False, + bits=[True, False, True, True, False, False, False, False], ) transact.retries = 0 transact.transport = 1 # type: ignore[assignment] @@ -774,7 +787,7 @@ def test_sync_client_protocol_execute_outside(self, use_clc, no_resp): assert not result else: assert not result.isError() - assert isinstance(response, ReadCoilsResponse) + assert isinstance(response, PDUreadCoils) def test_sync_client_protocol_execute_no_pdu(self, use_clc): """Test the transaction execute method.""" @@ -790,9 +803,11 @@ def test_sync_client_protocol_execute_no_pdu(self, use_clc): sync_client=client, ) transact.sync_client.connect = mock.Mock(return_value=True) - request = ReadCoilsRequest(address=117, count=5, dev_id=1) - response = ReadCoilsResponse( - bits=[True, False, True, True, False, False, False, False], dev_id=1 + request = PDUreadCoils(1, address=117, count=5) + response = PDUreadCoils( + 1, + is_request=False, + bits=[True, False, True, True, False, False, False, False], ) transact.retries = 0 transact.transport = 1 # type: ignore[assignment] @@ -819,19 +834,23 @@ def test_transaction_sync_id0(self, use_clc): transact.sync_client.send = mock.Mock() transact.retries = 0 transact.transport = 1 # type: ignore[assignment] - response = ReadCoilsResponse( - bits=[True, False, True, True, False, False, False, False], dev_id=2 + response = PDUreadCoils( + 2, + is_request=False, + bits=[True, False, True, True, False, False, False, False], ) resp_bytes = transact.framer.buildFrame(response) transact.sync_client.recv = mock.Mock() transact.sync_client.recv.side_effect = [resp_bytes, None] transact.sync_client.send = mock.Mock() transact.comm_params.timeout_connect = 0.2 - request = ReadCoilsRequest(address=117, count=5, dev_id=1) + request = PDUreadCoils(1, address=117, count=5) with pytest.raises(ModbusIOException): transact.sync_execute(False, request) - response = ReadCoilsResponse( - bits=[True, False, True, True, False, False, False, False], dev_id=1 + response = PDUreadCoils( + 1, + is_request=False, + bits=[True, False, True, True, False, False, False, False], ) resp_bytes = transact.framer.buildFrame(response) transact.sync_client.recv = mock.Mock(return_value=resp_bytes) @@ -852,24 +871,22 @@ def test_transaction_sync_get_response(self, use_clc): sync_client=client, ) client.recv = mock.Mock() # type: ignore[method-assign] - request = transact.framer.buildFrame( - ReadCoilsRequest(address=117, count=5, dev_id=1) - ) + request = transact.framer.buildFrame(PDUreadCoils(1, address=117, count=5)) response = transact.framer.buildFrame( - ReadCoilsResponse(bits=[True] * 8, dev_id=1) + PDUreadCoils(1, is_request=False, bits=[True] * 8) ) transact.sent_buffer = request client.recv.side_effect = [request, response] pdu = transact.sync_get_response(1, 0) - assert isinstance(pdu, ReadCoilsResponse) + assert isinstance(pdu, PDUreadCoils) transact.sent_buffer = request client.recv.side_effect = [request[:3], request[3:], response] pdu = transact.sync_get_response(1, 0) - assert isinstance(pdu, ReadCoilsResponse) + assert isinstance(pdu, PDUreadCoils) transact.sent_buffer = request client.recv.side_effect = [response] pdu = transact.sync_get_response(1, 0) - assert isinstance(pdu, ReadCoilsResponse) + assert isinstance(pdu, PDUreadCoils) def test_transaction_sync_get_response_garbage_timeout(self, use_clc): """Test continuous invalid input cannot bypass the response timeout."""