diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0335f5ce6..a9b61a027 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -36,11 +36,11 @@ jobs: fail-fast: false matrix: os: [ubuntu-latest, macos-latest, windows-latest] - python: ['3.9', '3.10', '3.11', '3.12'] + python: ['3.9', '3.10', '3.11', '3.12', "3.13"] include: - python: '3.9' run_lint: true - - python: '3.12' + - python: '3.13' run_doc: true run_lint: true - os: macos-latest @@ -113,13 +113,13 @@ jobs: ruff check . - name: pytest - if: ${{ (matrix.os != 'ubuntu-latest') || (matrix.python != '3.12') }} + if: ${{ (matrix.os != 'ubuntu-latest') || (matrix.python != '3.13') }} run: | env pytest - name: pytest coverage - if: ${{ (matrix.os == 'ubuntu-latest') && (matrix.python == '3.12') }} + if: ${{ (matrix.os == 'ubuntu-latest') && (matrix.python == '3.13') }} run: | env pytest --cov diff --git a/AUTHORS.rst b/AUTHORS.rst index 57b03d7c4..9f1a8e91a 100644 --- a/AUTHORS.rst +++ b/AUTHORS.rst @@ -10,6 +10,7 @@ Pymodbus version 3 family ------------------------- Thanks to +- ahcm-dev - AKJ7 - Alex - Alex Ruddick @@ -58,13 +59,12 @@ Thanks to - julian - Justin Standring - Kenny Johansson -- Martyy -- Matthias Straka +- Kürşat Aktaş - laund - Logan Gunthorpe - Marko Luther -- Logan Gunthorpe -- Marko Luther +- Martyy +- Máté Szabó - Matthias Straka - Matthias Urlichs - Michel F diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 511831a37..12a727467 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -7,6 +7,40 @@ helps make pymodbus a better product. :ref:`Authors`: contains a complete list of volunteers have contributed to each major version. +Version 3.7.4 +------------- +* Clean PDU init. (#2399) +* Wrong close, when transaction do not match. (#2401) +* Remove unmaintained (not working) example contributions. (#2400) +* All pdu (incl. function code) tests to pdu directory. (#2397) +* Add `no_response_expected` argument to requests (#2385) +* Resubmit: Don't close/reopen tcp connection on single modbus message timeout (#2350) +* 100% test coverage for PDU. (#2394) +* Type DecodePDU. (#2392) +* Update to use DecodePDU. (#2391) +* Client/Server decoder renamed and moved to pdu. (#2390) +* Move client/server decoder to pdu. (#2388) +* Introducing PyModbus Guru on Gurubase.io (#2387) +* Remove IllegalFunctionRequest. (#2384) +* remove ModbusResponse. (#2383) +* Add typing to pdu base classes. (#2380) +* Updated roadmap. +* remove databuffer from framer. (#2379) +* Improve retries for sync client. (#2377) +* Move process test to framer tests (#2376) +* Framer do not check ids (#2375) +* Remove callback from framer. (#2374) +* Auto fill device ids for clients. (#2372) +* Reenable multidrop tests. (#2370) +* write_register/s accept bytes or int. (#2369) +* roadmap corrections. +* Added roadmap (not written in stone). (#2367) +* Update README to show python 3.13. +* Test on Python 3.13 (#2366) +* Use @abstractmethod (#2365) +* Corrected smaller documentation bugs. (#2364) +* README as landing page in readthedocs. (#2363) + Version 3.7.3 ------------- * 100% test coverage of framers (#2359) @@ -44,7 +78,6 @@ Version 3.7.3 * fixed type hints for write_register and write_registers (#2309) * Remove _header from framers. (#2305) - Version 3.7.2 ------------- * Correct README diff --git a/MAKE_RELEASE.rst b/MAKE_RELEASE.rst index d3f4bc5f9..c2d52c0f5 100644 --- a/MAKE_RELEASE.rst +++ b/MAKE_RELEASE.rst @@ -14,9 +14,10 @@ Prepare/make release on dev. * Control / Update API_changes.rst * Update CHANGELOG.rst * Add commits from last release, but selectively ! - git log --oneline v3.7.3..HEAD > commit.log - git log --pretty="%an" v3.7.3..HEAD | sort -uf > authors.log + git log --oneline v3.7.4..HEAD > commit.log + git log --pretty="%an" v3.7.4..HEAD | sort -uf > authors.log update AUTHORS.rst and CHANGELOG.rst + update roadmap.rst cd doc; ./build_html * rm -rf build/* dist/* * python3 -m build diff --git a/README.rst b/README.rst index 5cca41287..15873f5ae 100644 --- a/README.rst +++ b/README.rst @@ -8,8 +8,11 @@ PyModbus - A Python Modbus Stack .. image:: https://pepy.tech/badge/pymodbus :target: https://pepy.tech/project/pymodbus :alt: Downloads +.. image:: https://img.shields.io/badge/Gurubase-Ask%20PyModbus%20Guru-006BFF + :target: https://gurubase.io/g/pymodbus + :alt: PyModbus Guru -Pymodbus is a full Modbus protocol implementation offering client/server with synchronous/asynchronous API a well as simulators. +Pymodbus is a full Modbus protocol implementation offering client/server with synchronous/asynchronous API and simulators. Our releases is defined as X.Y.Z, and we have strict rules what to release when: @@ -23,7 +26,7 @@ Upgrade examples: - 3.6.1 -> 3.7.0: Smaller changes to the pymodbus calls might be needed - 2.5.4 -> 3.0.0: Major changes in the application might be needed -Current release is `3.7.3 `_. +Current release is `3.7.4 `_. Bleeding edge (not released) is `dev `_. @@ -57,7 +60,7 @@ Common features * very lightweight project * requires Python >= 3.9 * thorough test suite, that test all corners of the library -* automatically tested on Windows, Linux and MacOS combined with python 3.9 - 3.12 +* automatically tested on Windows, Linux and MacOS combined with python 3.9 - 3.13 * strongly typed API (py.typed present) The modbus protocol specification: Modbus_Application_Protocol_V1_1b3.pdf can be found on diff --git a/doc/index.rst b/doc/index.rst index 433f78f4b..c2073f1f9 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -8,7 +8,6 @@ Please select a topic in the left hand column. :caption: Contents: :hidden: - source/readme source/api_changes source/client source/server @@ -17,5 +16,7 @@ Please select a topic in the left hand column. source/examples source/authors source/changelog - source/api_changes source/internals + source/roadmap + +.. include:: ../README.rst \ No newline at end of file diff --git a/doc/source/README.rst b/doc/source/README.rst deleted file mode 100644 index a6210d3d8..000000000 --- a/doc/source/README.rst +++ /dev/null @@ -1 +0,0 @@ -.. include:: ../../README.rst diff --git a/doc/source/_static/examples.tgz b/doc/source/_static/examples.tgz index 862407c19..6f5117edd 100644 Binary files a/doc/source/_static/examples.tgz and b/doc/source/_static/examples.tgz differ diff --git a/doc/source/_static/examples.zip b/doc/source/_static/examples.zip index 402b6c3c8..32350499a 100644 Binary files a/doc/source/_static/examples.zip and b/doc/source/_static/examples.zip differ diff --git a/doc/source/client.rst b/doc/source/client.rst index ae4a8404e..99d115c1a 100644 --- a/doc/source/client.rst +++ b/doc/source/client.rst @@ -166,6 +166,14 @@ The line :mod:`result = await client.read_coils(2, 3, slave=1)` is an example of The last line :mod:`client.close()` closes the connection and render the object inactive. +Retry logic for async clients +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +If no response is received to a request (call), it is retried (parameter retries) times, if not successful +an exception response is returned, BUT the connection is not touched. + +If 3 consequitve requests (calls) do not receive a response, the connection is terminated. + Development notes ^^^^^^^^^^^^^^^^^ @@ -191,14 +199,12 @@ The logical devices represented by the device is addressed with the :mod:`slave= With **Serial**, the comm port is defined when creating the object. The physical devices are addressed with the :mod:`slave=` parameter. -:mod:`slave=0` is used as broadcast in order to address all devices. -However experience shows that modern devices do not allow broadcast, mostly because it is -inheriently dangerous. With :mod:`slave=0` the application can get upto 254 responses on a single request, -and this is not handled with the normal API calls! +:mod:`slave=0` is defined as broadcast in the modbus standard, but pymodbus treats is a normal device. -The simple request calls (mixin) do NOT support broadcast, if an application wants to use broadcast -it must call :mod:`client.execute` and deal with the responses. +If an application is expecting multiple responses to a broadcast request, it must call :mod:`client.execute` and deal with the responses. +If no response is expected to a request, the :mod:`no_response_expected=True` argument can be used +in the normal API calls, this will cause the call to return imidiatble with :mod:`None` Client response handling @@ -227,6 +233,7 @@ And in case of read retrieve the data depending on type of request - :mod:`rr.bits` is set for coils / input_register requests - :mod:`rr.registers` is set for other requests +Remark if using :mod:`no_response_expected=True` rr will always be None. Client interface classes ------------------------ diff --git a/doc/source/library/pymodbus.rst b/doc/source/library/pymodbus.rst index cdda34602..610a440d1 100644 --- a/doc/source/library/pymodbus.rst +++ b/doc/source/library/pymodbus.rst @@ -21,11 +21,6 @@ Extra functions :undoc-members: :show-inheritance: -.. automodule:: pymodbus.factory - :members: - :undoc-members: - :show-inheritance: - .. automodule:: pymodbus.payload :members: :undoc-members: @@ -45,6 +40,11 @@ Extra functions PDU classes =========== +.. automodule:: pymodbus.pdu.decoders + :members: + :undoc-members: + :show-inheritance: + .. automodule:: pymodbus.pdu.bit_read_message :members: :undoc-members: diff --git a/doc/source/repl.rst b/doc/source/repl.rst index a598c7904..28ad16284 100644 --- a/doc/source/repl.rst +++ b/doc/source/repl.rst @@ -3,7 +3,8 @@ Pymodbus REPL (Read Evaluate Print Loop) .. raw:: html -

Warning: The Pymodbus REPL documentation is not updated.

+

Warning: The Pymodbus REPL documentation is not updated, + because it lives in a different repo.

Installation ------------ diff --git a/doc/source/roadmap.rst b/doc/source/roadmap.rst new file mode 100644 index 000000000..0d7fa3276 --- /dev/null +++ b/doc/source/roadmap.rst @@ -0,0 +1,40 @@ +Roadmap +======= + +The roadmap is not a finite plan, but merely an expression of intentions ! + +Pymodbus development is mainly driven by contributors, who have an itch, and provide a solution for the community. +The maintainers are very open to these pull request, and ONLY work to secure that: + +- it does not break existing usage/functionality (PR put on hold for next API change release) +- it is a generic feature (e.g. not just for serial 9.600 bps) +- it have proper test cases, to ensure against side effects. + +It is important to note the maintainer do NOT reject ANY pull request that emcompases the above criteria. +It is the community that decides how pymodbus evolves NOT the maintainers ! + +The following bullet points are what the maintainers focus on: + +- 3.7.5, bug fix release, hopefully with: + - Simplify PDU classes + - Simplify transaction manager (central control point) + - Remove ModbusControlBlock +- 3.7.6, bug fix release, with: + - Not planned +- 3.8.0, with: + - new transaction handling + - transaction 100% coverage + - skip_encode, zero_mode parameters removed +- 4.0.0, with: + - client async with sync/async API + - Only one datastore, but with different API`s + - Simulator standard in server + - GUI client, to analyze devices + - GUI server, to simulate devices + +All contributions are WELCOME, and we (the maintainers) are always open to talk about ideas, +best way is via `discussions `_ on github. + +We have lately decided, that we do strictly follow the `modbus org `_ standard, +but we also accept vendor specific (like Huawei) pull requests, as long as they extend the standard or are actitvated with +a specific argument like --huawei. diff --git a/examples/client_custom_msg.py b/examples/client_custom_msg.py index 67f39cce0..0820cf161 100755 --- a/examples/client_custom_msg.py +++ b/examples/client_custom_msg.py @@ -15,7 +15,7 @@ from pymodbus import FramerType from pymodbus.client import AsyncModbusTcpClient as ModbusClient -from pymodbus.pdu import ModbusExceptions, ModbusRequest, ModbusResponse +from pymodbus.pdu import ModbusExceptions, ModbusPDU from pymodbus.pdu.bit_read_message import ReadCoilsRequest @@ -26,11 +26,11 @@ # Since the function code is already registered with the decoder factory, # this will be decoded as a read coil response. If you implement a new # method that is not currently implemented, you must register the request -# and response with a ClientDecoder factory. +# and response with the active DecodePDU object. # --------------------------------------------------------------------------- # -class CustomModbusResponse(ModbusResponse): +class CustomModbusPDU(ModbusPDU): """Custom modbus response.""" function_code = 55 @@ -38,7 +38,8 @@ class CustomModbusResponse(ModbusResponse): def __init__(self, values=None, slave=1, transaction=0, skip_encode=False): """Initialize.""" - ModbusResponse.__init__(self, slave, transaction, skip_encode) + super().__init__() + super().setData(slave, transaction, skip_encode) self.values = values or [] def encode(self): @@ -62,7 +63,7 @@ def decode(self, data): self.values.append(struct.unpack(">H", data[i : i + 2])[0]) -class CustomModbusRequest(ModbusRequest): +class CustomRequest(ModbusPDU): """Custom modbus request.""" function_code = 55 @@ -70,7 +71,8 @@ class CustomModbusRequest(ModbusRequest): def __init__(self, address=None, slave=1, transaction=0, skip_encode=False): """Initialize.""" - ModbusRequest.__init__(self, slave, transaction, skip_encode) + super().__init__() + super().setData(slave, transaction, skip_encode) self.address = address self.count = 16 @@ -89,7 +91,7 @@ def execute(self, context): if not context.validate(self.function_code, self.address, self.count): return self.doException(ModbusExceptions.IllegalAddress) values = context.getValues(self.function_code, self.address, self.count) - return CustomModbusResponse(values) + return CustomModbusPDU(values) # --------------------------------------------------------------------------- # @@ -122,18 +124,18 @@ async def main(host="localhost", port=5020): await client.connect() # create a response object to control it works - CustomModbusResponse() + CustomModbusPDU() # new modbus function code. - client.register(CustomModbusResponse) + client.register(CustomModbusPDU) slave=1 - request = CustomModbusRequest(32, slave=slave) - result = await client.execute(request) + request = CustomRequest(32, slave=slave) + result = await client.execute(False, request) print(result) # inherited request request = Read16CoilsRequest(32, slave) - result = await client.execute(request) + result = await client.execute(False, request) print(result) diff --git a/examples/contrib/explain.py b/examples/contrib/explain.py deleted file mode 100644 index 64f10c83f..000000000 --- a/examples/contrib/explain.py +++ /dev/null @@ -1,168 +0,0 @@ -""" -How to explain pymodbus logs using https://rapidscada.net/modbus/ and requests. -""" -from __future__ import annotations - -import contextlib -import os -import shutil -import tempfile -from dataclasses import dataclass -from html.parser import HTMLParser -from urllib import request -from urllib.error import HTTPError - - -RAPID_SCADA_URL = "https://rapidscada.net/modbus/" - - -@dataclass(frozen=True) -class ParsedModbusResult: # pylint: disable=too-many-instance-attributes - """Simple data structure to hold post response of Rapid SCADA.""" - - transaction_id: int - length: int - unit_id: int - func_code: int - is_receive: bool - zero_index_reg: int | None = None - quantity: int | None = None - byte_count: int | None = None - registers: list[int] | None = None - - def summarize(self) -> dict: - """Get a summary representation for readability.""" - summary = {"is_receive": self.is_receive} - if self.zero_index_reg is not None: - summary["one_index_reg"] = self.zero_index_reg + 1 - if self.registers is not None: - summary["registers"] = self.registers - return summary - - -def explain_with_rapid_scada( - packet: str, - is_modbus_tcp: bool = True, - is_receive: bool = False, - timeout: float | tuple[float, float] | None = 15.0, -) -> ParsedModbusResult: - """ - Explain a Modbus packet using https://rapidscada.net/modbus/. - - Args: - packet: Packet from pymodbus logs. - is_modbus_tcp: Set True (default) for Modbus TCP or False for Modbus RTU. - is_receive: Set True if pymodbus log says RECV, otherwise False for SEND. - timeout: Optional timeout (sec) for the HTTP post, defaulted to 15-sec. - - Returns: - Parsed data from Rapid SCADA Modbus Parser. - """ - - class NonEmptyDataFromHTML(HTMLParser): - """Aggregate all data from an HTML blob.""" - - def __init__(self, *, convert_charrefs=True): - super().__init__(convert_charrefs=convert_charrefs) - self._data = [] - - @property - def data(self) -> list[str]: - return self._data - - def handle_data(self, data: str) -> None: - if not data.strip(): - return - self._data.append(data.strip()) - - data_packet = "+".join( - [f"{int(hex_str, base=16):02X}" for hex_str in packet.split(" ")], - ) - with request.urlopen( # noqa: S310 - request.Request( - f"{RAPID_SCADA_URL}?ModbusMode={int(is_modbus_tcp)}" - f"&DataDirection={int(is_receive)}&DataPackage={data_packet}", - method="POST", - ), - timeout=timeout, - ) as response: - if response.getcode() != 200: - raise HTTPError( - url=response.url, - code=response.getcode(), - msg=response.reason, - hdrs=response.headers, - fp=response.fp, - ) - response_data = response.read().decode() - parser = NonEmptyDataFromHTML() - parser.feed(response_data) - - # pylint: disable-next=dangerous-default-value - def get_next_field(prior_field: str, data: list[str] = parser.data) -> str: - return data[data.index(prior_field) + 1] - - def parse_next_field(prior_field: str, split_index: int = 0) -> int: - return int(get_next_field(prior_field).split(" ")[split_index], base=16) - - base_result_data = { - "transaction_id": parse_next_field("Transaction identifier"), - "length": parse_next_field("Length"), - "unit_id": parse_next_field("Unit identifier"), - "func_code": parse_next_field("Function code"), - "is_receive": is_receive, - } - is_receive_fn_code: tuple[bool, int] = is_receive, base_result_data["func_code"] - if is_receive_fn_code in [(False, 0x03), (True, 0x10)]: - return ParsedModbusResult( - **base_result_data, - zero_index_reg=parse_next_field("Starting address", split_index=1), - quantity=parse_next_field("Quantity"), - ) - if is_receive_fn_code in [(False, 0x10), (True, 0x03)]: - next_field = "Register value" if is_receive else "Registers value" - return ParsedModbusResult( - **base_result_data, - byte_count=parse_next_field("Byte count"), - registers=[ - int(raw_value.split(" ")[0], base=16) - for raw_value in get_next_field(next_field).split(", ") - ], - ) - raise NotImplementedError( - f"Unhandled case with {is_receive=} and {parser.data=}.", - ) - - -def annotate_pymodbus_logs(file: str | os.PathLike) -> None: - """Annotate a pymodbus log file in-place with explanations.""" - with open(file, encoding="utf-8") as in_file, tempfile.NamedTemporaryFile( - mode="w", encoding="utf-8", delete=False - ) as out_file: - for i, line in enumerate(in_file): - if "Running transaction" in line and i > 0: - out_file.write("\n") - out_file.write(line) - if "SEND:" in line: - explained = explain_with_rapid_scada( - packet=line.split("SEND:")[1].strip(), - ) - out_file.write( - f"Send explained: {explained}\n" - f"Send summary: {explained.summarize()}\n", - ) - if "RECV:" in line: - explained = explain_with_rapid_scada( - packet=line.split("RECV:")[1].strip(), - is_receive=True, - ) - out_file.write( - f"Receive explained: {explained}\n" - f"Receive summary: {explained.summarize()}\n", - ) - # NOTE: per NamedTemporaryFile docs, the name cannot be reused on Windows - # while the file is still open. So we have to use delete=False followed by - # manually removing the temp file - shutil.copyfile(out_file.name, file) - with contextlib.suppress(FileNotFoundError): - os.remove(out_file.name) diff --git a/examples/contrib/redis_datastore.py b/examples/contrib/redis_datastore.py deleted file mode 100644 index ca974426f..000000000 --- a/examples/contrib/redis_datastore.py +++ /dev/null @@ -1,237 +0,0 @@ -"""Datastore using redis.""" -# pylint: disable=missing-type-doc -from contextlib import suppress - - -with suppress(ImportError): - import redis - -from pymodbus.datastore import ModbusBaseSlaveContext -from pymodbus.logging import Log -from pymodbus.utilities import pack_bitstring, unpack_bitstring - - -# ---------------------------------------------------------------------------# -# Context -# ---------------------------------------------------------------------------# -class RedisSlaveContext(ModbusBaseSlaveContext): - """This is a modbus slave context using redis as a backing store.""" - - def __init__(self, host="localhost", port=6379, prefix="pymodbus", client=None): - """Initialize the datastores. - - :param host: The host to connect to - :param port: The port to connect to - :param prefix: A prefix for the keys - :param client: redis client - """ - self.prefix = prefix - self.client = client if client else redis.Redis(host=host, port=port) - self._build_mapping() - - def __str__(self): - """Return a string representation of the context. - - :returns: A string representation of the context - """ - return f"Redis Slave Context {self.client}" - - def reset(self): - """Reset all the datastores to their default values.""" - self.client.flushall() - - def validate(self, fc, address, count=1): - """Validate the request to make sure it is in range. - - :param fc: The function we are working with - :param address: The starting address - :param count: The number of values to test - :returns: True if the request in within range, False otherwise - """ - address = address + 1 # section 4.4 of specification - Log.debug("validate[{}] {}:{}", fc, address, count) - return self._val_callbacks[self.decode(fc)](address, count) - - def getValues(self, fc, address, count=1): - """Get `count` values from datastore. - - :param fc: The function we are working with - :param address: The starting address - :param count: The number of values to retrieve - :returns: The requested values from a:a+c - """ - address = address + 1 # section 4.4 of specification - Log.debug("getValues[{}] {}:{}", fc, address, count) - return self._get_callbacks[self.decode(fc)](address, count) - - def setValues(self, fc, address, values): - """Set the datastore with the supplied values. - - :param fc: The function we are working with - :param address: The starting address - :param values: The new values to be set - """ - address = address + 1 # section 4.4 of specification - Log.debug("setValues[{}] {}:{}", fc, address, len(values)) - self._set_callbacks[self.decode(fc)](address, values) - - # --------------------------------------------------------------------------# - # Redis Helper Methods - # --------------------------------------------------------------------------# - def _get_prefix(self, key): - """Abstract getting bit values. - - :param key: The key prefix to use - :returns: The key prefix to redis - """ - return f"{self.prefix}:{key}" - - def _build_mapping(self): - """Build the function code mapper.""" - self._val_callbacks = { - "d": lambda o, c: self._val_bit("d", o, c), - "c": lambda o, c: self._val_bit("c", o, c), - "h": lambda o, c: self._val_reg("h", o, c), - "i": lambda o, c: self._val_reg("i", o, c), - } - self._get_callbacks = { - "d": lambda o, c: self._get_bit("d", o, c), - "c": lambda o, c: self._get_bit("c", o, c), - "h": lambda o, c: self._get_reg("h", o, c), - "i": lambda o, c: self._get_reg("i", o, c), - } - self._set_callbacks = { - "d": lambda o, v: self._set_bit("d", o, v), - "c": lambda o, v: self._set_bit("c", o, v), - "h": lambda o, v: self._set_reg("h", o, v), - "i": lambda o, v: self._set_reg("i", o, v), - } - - # --------------------------------------------------------------------------# - # Redis discrete implementation - # --------------------------------------------------------------------------# - _bit_size = 16 - _bit_default = "\x00" * (_bit_size % 8) - - def _get_bit_values(self, key, offset, count): - """Abstract getting bit values. - - :param key: The key prefix to use - :param offset: The address offset to start at - :param count: The number of bits to read - """ - key = self._get_prefix(key) - bit_start = divmod(offset, self._bit_size)[0] - bit_end = divmod(offset + count, self._bit_size)[0] - - request = (f"{key}:{v}" for v in range(bit_start, bit_end + 1)) - response = self.client.mget(request) - return response - - def _val_bit(self, key, offset, count): - """Validate that the given range is currently set in redis. - - If any of the keys return None, then it is invalid. - - :param key: The key prefix to use - :param offset: The address offset to start at - :param count: The number of bits to read - """ - response = self._get_bit_values(key, offset, count) - return ( - True # pylint: disable=simplifiable-if-expression - if None not in response - else False - ) - - def _get_bit(self, key, offset, count): - """Get bit. - - :param key: The key prefix to use - :param offset: The address offset to start at - :param count: The number of bits to read - """ - response = self._get_bit_values(key, offset, count) - response = (r or self._bit_default for r in response) - result = "".join(response) - result = unpack_bitstring(result) - return result[offset : offset + count] - - def _set_bit(self, key, offset, values): - """Set bit. - - :param key: The key prefix to use - :param offset: The address offset to start at - :param values: The values to set - """ - count = len(values) - bit_start = divmod(offset, self._bit_size)[0] - bit_end = divmod(offset + count, self._bit_size)[0] - value = pack_bitstring(values) - - current = self._get_bit_values(key, offset, count) - current = (r or self._bit_default for r in current) - current = "".join(current) - current = current[0:offset] + value.decode("utf-8") + current[offset + count :] - final = ( - current[s : s + self._bit_size] for s in range(0, count, self._bit_size) - ) - - key = self._get_prefix(key) - request = (f"{key}:{v}" for v in range(bit_start, bit_end + 1)) - request = dict(zip(request, final)) - self.client.mset(request) - - # --------------------------------------------------------------------------# - # Redis register implementation - # --------------------------------------------------------------------------# - _reg_size = 16 - _reg_default = "\x00" * (_reg_size % 8) - - def _get_reg_values(self, key, offset, count): - """Abstract getting register values. - - :param key: The key prefix to use - :param offset: The address offset to start at - :param count: The number of bits to read - """ - key = self._get_prefix(key) - request = (f"{key}:{v}" for v in range(offset, count + 1)) - response = self.client.mget(request) - return response - - def _val_reg(self, key, offset, count): - """Validate that the given range is currently set in redis. - - If any of the keys return None, then it is invalid. - - :param key: The key prefix to use - :param offset: The address offset to start at - :param count: The number of bits to read - """ - response = self._get_reg_values(key, offset, count) - return None not in response - - def _get_reg(self, key, offset, count): - """Get register. - - :param key: The key prefix to use - :param offset: The address offset to start at - :param count: The number of bits to read - """ - response = self._get_reg_values(key, offset, count) - response = [r or self._reg_default for r in response] - return response[offset : offset + count] - - def _set_reg(self, key, offset, values): - """Set register. - - :param key: The key prefix to use - :param offset: The address offset to start at - :param values: The values to set - """ - count = len(values) - key = self._get_prefix(key) - request = (f"{key}:{v}" for v in range(offset, count + 1)) - request = dict(zip(request, values)) - self.client.mset(request) diff --git a/examples/contrib/sql_datastore.py b/examples/contrib/sql_datastore.py deleted file mode 100644 index 2d6387a8b..000000000 --- a/examples/contrib/sql_datastore.py +++ /dev/null @@ -1,182 +0,0 @@ -"""Datastore using SQL.""" -# pylint: disable=missing-type-doc -try: - import sqlalchemy - import sqlalchemy.types as sqltypes - from sqlalchemy.pool import StaticPool - from sqlalchemy.schema import UniqueConstraint - from sqlalchemy.sql import and_ - from sqlalchemy.sql.expression import bindparam -except ImportError: - pass - -from pymodbus.datastore import ModbusBaseSlaveContext -from pymodbus.logging import Log - - -# --------------------------------------------------------------------------- # -# Context -# --------------------------------------------------------------------------- # -class SqlSlaveContext(ModbusBaseSlaveContext): - """This creates a modbus data model with each data access in its a block.""" - - def __init__(self, *_args, table="pymodbus", database=None): - """Initialize the datastores. - - :param table: table name - :param database: database - """ - self._engine = None - self._metadata = None - self._table = None - self._connection = None - self.table = table - self.database = database if database else "sqlite:///:memory:" - self._db_create(self.table, self.database) - - def __str__(self): - """Return a string representation of the context. - - :returns: A string representation of the context - """ - return "Modbus Slave Context" - - def reset(self): - """Reset all the datastores to their default values.""" - self._metadata.drop_all(None) - self._db_create(self.table, self.database) - - def validate(self, fc, address, count=1): - """Validate the request to make sure it is in range. - - :param fc: The function we are working with - :param address: The starting address - :param count: The number of values to test - :returns: True if the request in within range, False otherwise - """ - address = address + 1 # section 4.4 of specification - Log.debug("validate[{}] {}:{}", fc, address, count) - return self._validate(self.decode(fc), address, count) - - def getValues(self, fc, address, count=1): - """Get `count` values from datastore. - - :param fc: The function we are working with - :param address: The starting address - :param count: The number of values to retrieve - :returns: The requested values from a:a+c - """ - address = address + 1 # section 4.4 of specification - Log.debug("get-values[{}] {}:{}", fc, address, count) - return self._get(self.decode(fc), address, count) - - def setValues(self, fc, address, values, update=True): - """Set the datastore with the supplied values. - - :param fc: The function we are working with - :param address: The starting address - :param values: The new values to be set - :param update: Update existing register in the db - """ - address = address + 1 # section 4.4 of specification - Log.debug("set-values[{}] {}:{}", fc, address, len(values)) - if update: - self._update(self.decode(fc), address, values) - else: - self._set(self.decode(fc), address, values) - - # ----------------------------------------------------------------------- # - # Sqlite Helper Methods - # ----------------------------------------------------------------------- # - def _db_create(self, table, database): - """Initialize the database and handles. - - :param table: The table name to create - :param database: The database uri to use - """ - self._engine = sqlalchemy.create_engine( - database, - echo=False, - connect_args={"check_same_thread": False}, - poolclass=StaticPool, - ) - self._metadata = sqlalchemy.MetaData(self._engine) - self._table = sqlalchemy.Table( - table, - self._metadata, - sqlalchemy.Column("type", sqltypes.String(1)), - sqlalchemy.Column("index", sqltypes.Integer), - sqlalchemy.Column("value", sqltypes.Integer), - UniqueConstraint("type", "index", name="key"), - ) - self._table.create(self._engine) - self._connection = self._engine.connect() - - def _get(self, sqltype, offset, count): - """Get.""" - query = self._table.select( - and_( - self._table.c.type == sqltype, - self._table.c.index >= offset, - self._table.c.index <= offset + count - 1, - ) - ) - query = query.order_by(self._table.c.index.asc()) - result = self._connection.execute(query).fetchall() - return [row.value for row in result] - - def _build_set(self, sqltype, offset, values, prefix=""): - """Generate the sql update context.""" - result = [] - for index, value in enumerate(values): - result.append( - { - prefix + "type": sqltype, - prefix + "index": offset + index, - "value": value, - } - ) - return result - - def _check(self, sqltype, offset, _values): - """Check.""" - result = self._get(sqltype, offset, count=1) - return ( - False # pylint: disable=simplifiable-if-expression - if len(result) > 0 - else True - ) - - def _set(self, sqltype, offset, values): - """Set.""" - if self._check(sqltype, offset, values): - context = self._build_set(sqltype, offset, values) - query = self._table.insert() - result = self._connection.execute(query, context) - return result.rowcount == len(values) - return False - - def _update(self, sqltype, offset, values): - """Update.""" - context = self._build_set(sqltype, offset, values, prefix="x_") - query = self._table.update().values(value="value") - query = query.where( - and_( - self._table.c.type == bindparam("x_type"), - self._table.c.index == bindparam("x_index"), - ) - ) - result = self._connection.execute(query, context) - return result.rowcount == len(values) - - def _validate(self, sqltype, offset, count): - """Validate.""" - query = self._table.select( - and_( - self._table.c.type == sqltype, - self._table.c.index >= offset, - self._table.c.index <= offset + count - 1, - ) - ) - result = self._connection.execute(query).fetchall() - return len(result) == count diff --git a/examples/message_parser.py b/examples/message_parser.py index 659d2747a..6f6851799 100755 --- a/examples/message_parser.py +++ b/examples/message_parser.py @@ -12,12 +12,12 @@ import textwrap from pymodbus import pymodbus_apply_logging_config -from pymodbus.factory import ClientDecoder, ServerDecoder from pymodbus.framer import ( FramerAscii, FramerRTU, FramerSocket, ) +from pymodbus.pdu import DecodePDU _logger = logging.getLogger(__file__) @@ -73,14 +73,15 @@ def decode(self, message): print(f"Decoding Message {value}") print("=" * 80) decoders = [ - self.framer(ServerDecoder(), []), - self.framer(ClientDecoder(), []), + self.framer(DecodePDU(True)), + self.framer(DecodePDU(False)), ] for decoder in decoders: print(f"{decoder.decoder.__class__.__name__}") print("-" * 80) try: - decoder.processIncomingPacket(message, self.report) + _, pdu = decoder.processIncomingFrame(message) + self.report(pdu) except Exception: # pylint: disable=broad-except self.check_errors(decoder, message) diff --git a/examples/server_hook.py b/examples/server_hook.py index a50035efb..371e79d6f 100755 --- a/examples/server_hook.py +++ b/examples/server_hook.py @@ -43,7 +43,6 @@ def server_response_manipulator(self, response): self.message_count = 3 else: print("---> RESPONSE: NONE") - response.should_respond = False self.message_count -= 1 return response, False diff --git a/pymodbus/__init__.py b/pymodbus/__init__.py index 279621c57..c2fed39e0 100644 --- a/pymodbus/__init__.py +++ b/pymodbus/__init__.py @@ -18,5 +18,5 @@ from pymodbus.pdu import ExceptionResponse -__version__ = "3.7.3" +__version__ = "3.7.4" __version_full__ = f"[pymodbus, version {__version__}]" diff --git a/pymodbus/client/base.py b/pymodbus/client/base.py index 34ebd1953..884d8f017 100644 --- a/pymodbus/client/base.py +++ b/pymodbus/client/base.py @@ -9,16 +9,15 @@ from pymodbus.client.mixin import ModbusClientMixin from pymodbus.client.modbusclientprotocol import ModbusClientProtocol from pymodbus.exceptions import ConnectionException, ModbusIOException -from pymodbus.factory import ClientDecoder from pymodbus.framer import FRAMER_NAME_TO_CLASS, FramerBase, FramerType from pymodbus.logging import Log -from pymodbus.pdu import ModbusRequest, ModbusResponse +from pymodbus.pdu import DecodePDU, ExceptionResponse, ModbusPDU from pymodbus.transaction import SyncModbusTransactionManager from pymodbus.transport import CommParams from pymodbus.utilities import ModbusTransactionState -class ModbusBaseClient(ModbusClientMixin[Awaitable[ModbusResponse]]): +class ModbusBaseClient(ModbusClientMixin[Awaitable[ModbusPDU]]): """**ModbusBaseClient**. :mod:`ModbusBaseClient` is normally not referenced outside :mod:`pymodbus`. @@ -51,6 +50,8 @@ def __init__( self.last_frame_end: float | None = 0 self.silent_interval: float = 0 self._lock = asyncio.Lock() + self.accept_no_response_limit = 3 + self.count_no_responses = 0 @property def connected(self) -> bool: @@ -67,7 +68,7 @@ async def connect(self) -> bool: ) return await self.ctx.connect() - def register(self, custom_response_class: ModbusResponse) -> 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. @@ -82,33 +83,29 @@ def close(self) -> None: """Close connection.""" self.ctx.close() - def execute(self, request: ModbusRequest): + def execute(self, no_response_expected: bool, request: ModbusPDU): """Execute request and get response (call **sync/async**). - :param request: The request to process - :returns: The result of the request execution - :raises ConnectionException: Check exception text. - :meta private: """ if not self.ctx.transport: raise ConnectionException(f"Not connected[{self!s}]") - return self.async_execute(request) + return self.async_execute(no_response_expected, request) - async def async_execute(self, request) -> ModbusResponse: + async def async_execute(self, no_response_expected: bool, request) -> ModbusPDU | None: """Execute requests asynchronously. :meta private: """ request.transaction_id = self.ctx.transaction.getNextTID() - packet = self.ctx.framer.buildPacket(request) + packet = self.ctx.framer.buildFrame(request) count = 0 while count <= self.retries: async with self._lock: req = self.build_response(request) self.ctx.send(packet) - if not request.slave_id: + if no_response_expected: resp = None break try: @@ -119,14 +116,19 @@ async def async_execute(self, request) -> ModbusResponse: except asyncio.exceptions.TimeoutError: count += 1 if count > self.retries: - self.ctx.connection_lost(asyncio.TimeoutError("Server not responding")) - raise ModbusIOException( - f"ERROR: No response received after {self.retries} retries" - ) - - return resp # type: ignore[return-value] - - def build_response(self, request: ModbusRequest): + if self.count_no_responses >= self.accept_no_response_limit: + self.ctx.connection_lost(asyncio.TimeoutError("Server not responding")) + raise ModbusIOException( + f"ERROR: No response received of the last {self.accept_no_response_limit} request, CLOSING CONNECTION." + ) + self.count_no_responses += 1 + Log.error(f"No response received after {self.retries} retries, continue with next request") + return ExceptionResponse(request.function_code) + + self.count_no_responses = 0 + return resp + + def build_response(self, request: ModbusPDU): """Return a deferred response for the current request. :meta private: @@ -163,7 +165,7 @@ def __str__(self): ) -class ModbusBaseSyncClient(ModbusClientMixin[ModbusResponse]): +class ModbusBaseSyncClient(ModbusClientMixin[ModbusPDU]): """**ModbusBaseClient**. :mod:`ModbusBaseClient` is normally not referenced outside :mod:`pymodbus`. @@ -186,7 +188,7 @@ def __init__( self.slaves: list[int] = [] # Common variables. - self.framer: FramerBase = (FRAMER_NAME_TO_CLASS[framer])(ClientDecoder(), [0]) + self.framer: FramerBase = (FRAMER_NAME_TO_CLASS[framer])(DecodePDU(False)) self.transaction = SyncModbusTransactionManager( self, self.retries, @@ -201,7 +203,7 @@ def __init__( # ----------------------------------------------------------------------- # # Client external interface # ----------------------------------------------------------------------- # - def register(self, custom_response_class: ModbusResponse) -> 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. @@ -222,9 +224,10 @@ def idle_time(self) -> float: return 0 return self.last_frame_end + self.silent_interval - def execute(self, request: ModbusRequest) -> ModbusResponse: + 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 :param request: The request to process :returns: The result of the request execution :raises ConnectionException: Check exception text. @@ -233,7 +236,7 @@ def execute(self, request: ModbusRequest) -> ModbusResponse: """ if not self.connect(): raise ConnectionException(f"Failed to connect[{self!s}]") - return self.transaction.execute(request) + return self.transaction.execute(no_response_expected, request) # ----------------------------------------------------------------------- # # Internal methods diff --git a/pymodbus/client/mixin.py b/pymodbus/client/mixin.py index 995d72110..b31e1ef87 100644 --- a/pymodbus/client/mixin.py +++ b/pymodbus/client/mixin.py @@ -14,7 +14,7 @@ import pymodbus.pdu.register_read_message as pdu_reg_read import pymodbus.pdu.register_write_message as pdu_req_write from pymodbus.exceptions import ModbusException -from pymodbus.pdu import ModbusRequest +from pymodbus.pdu import ModbusPDU T = TypeVar("T", covariant=False) @@ -49,7 +49,7 @@ class ModbusClientMixin(Generic[T]): # pylint: disable=too-many-public-methods def __init__(self): """Initialize.""" - def execute(self, _request: ModbusRequest) -> T: + def execute(self, _no_response_expected: bool, _request: ModbusPDU,) -> T: """Execute request (code ???). :raises ModbusException: @@ -61,305 +61,331 @@ def execute(self, _request: ModbusRequest) -> T: """ raise NotImplementedError("execute of ModbusClientMixin needs to be overridden") - def read_coils(self, address: int, count: int = 1, slave: int = 1) -> T: + def read_coils(self, address: int, count: int = 1, slave: int = 1, no_response_expected: bool = False) -> T: """Read coils (code 0x01). :param address: Start address to read from :param count: (optional) Number of coils to read :param slave: (optional) Modbus slave ID + :param no_response_expected: (optional) The client will not expect a response to the request :raises ModbusException: """ - return self.execute(pdu_bit_read.ReadCoilsRequest(address, count, slave=slave)) + return self.execute(no_response_expected, pdu_bit_read.ReadCoilsRequest(address=address, count=count, slave=slave)) - def read_discrete_inputs(self, address: int, count: int = 1, slave: int = 1) -> T: + def read_discrete_inputs(self, + address: int, + count: int = 1, + slave: int = 1, + no_response_expected: bool = False) -> T: """Read discrete inputs (code 0x02). :param address: Start address to read from :param count: (optional) Number of coils to read :param slave: (optional) Modbus slave ID + :param no_response_expected: (optional) The client will not expect a response to the request :raises ModbusException: """ - return self.execute(pdu_bit_read.ReadDiscreteInputsRequest(address, count, slave=slave)) + return self.execute(no_response_expected, pdu_bit_read.ReadDiscreteInputsRequest(address=address, count=count, slave=slave, )) - def read_holding_registers(self, address: int, count: int = 1, slave: int = 1) -> T: + def read_holding_registers(self, + address: int, + count: int = 1, + slave: int = 1, + no_response_expected: bool = False) -> T: """Read holding registers (code 0x03). :param address: Start address to read from :param count: (optional) Number of coils to read :param slave: (optional) Modbus slave ID + :param no_response_expected: (optional) The client will not expect a response to the request :raises ModbusException: """ - return self.execute(pdu_reg_read.ReadHoldingRegistersRequest(address, count, slave=slave)) + return self.execute(no_response_expected, pdu_reg_read.ReadHoldingRegistersRequest(address=address, count=count, slave=slave)) - def read_input_registers(self, address: int, count: int = 1, slave: int = 1) -> T: + def read_input_registers(self, + address: int, + count: int = 1, + slave: int = 1, + no_response_expected: bool = False) -> T: """Read input registers (code 0x04). :param address: Start address to read from :param count: (optional) Number of coils to read :param slave: (optional) Modbus slave ID + :param no_response_expected: (optional) The client will not expect a response to the request :raises ModbusException: """ - return self.execute(pdu_reg_read.ReadInputRegistersRequest(address, count, slave=slave)) + return self.execute(no_response_expected, pdu_reg_read.ReadInputRegistersRequest(address, count, slave=slave)) - def write_coil(self, address: int, value: bool, slave: int = 1) -> T: + def write_coil(self, address: int, value: bool, slave: int = 1, no_response_expected: bool = False) -> T: """Write single coil (code 0x05). :param address: Address to write to :param value: Boolean to write :param slave: (optional) Modbus slave ID + :param no_response_expected: (optional) The client will not expect a response to the request :raises ModbusException: """ - return self.execute(pdu_bit_write.WriteSingleCoilRequest(address, value, slave=slave)) + return self.execute(no_response_expected, pdu_bit_write.WriteSingleCoilRequest(address, value, slave=slave)) - def write_register(self, address: int, value: bytes, slave: int = 1) -> T: + def write_register(self, address: int, value: bytes | int, slave: int = 1, no_response_expected: bool = False) -> T: """Write register (code 0x06). :param address: Address to write to :param value: Value to write :param slave: (optional) Modbus slave ID + :param no_response_expected: (optional) The client will not expect a response to the request :raises ModbusException: """ - return self.execute(pdu_req_write.WriteSingleRegisterRequest(address, value, slave=slave)) + return self.execute(no_response_expected, pdu_req_write.WriteSingleRegisterRequest(address, value, slave=slave)) - def read_exception_status(self, slave: int = 1) -> T: + def read_exception_status(self, slave: int = 1, no_response_expected: bool = False) -> T: """Read Exception Status (code 0x07). :param slave: (optional) Modbus slave ID + :param no_response_expected: (optional) The client will not expect a response to the request :raises ModbusException: """ - return self.execute(pdu_other_msg.ReadExceptionStatusRequest(slave=slave)) + return self.execute(no_response_expected, pdu_other_msg.ReadExceptionStatusRequest(slave=slave)) - - def diag_query_data( - self, msg: bytes, slave: int = 1) -> T: + def diag_query_data(self, msg: bytes, slave: int = 1, no_response_expected: bool = False) -> T: """Diagnose query data (code 0x08 sub 0x00). :param msg: Message to be returned :param slave: (optional) Modbus slave ID + :param no_response_expected: (optional) The client will not expect a response to the request :raises ModbusException: """ - return self.execute(pdu_diag.ReturnQueryDataRequest(msg, slave=slave)) + return self.execute(no_response_expected, pdu_diag.ReturnQueryDataRequest(msg, slave=slave)) - def diag_restart_communication( - self, toggle: bool, slave: int = 1) -> T: + def diag_restart_communication(self, toggle: bool, slave: int = 1, no_response_expected: bool = False) -> T: """Diagnose restart communication (code 0x08 sub 0x01). :param toggle: True if toggled. :param slave: (optional) Modbus slave ID + :param no_response_expected: (optional) The client will not expect a response to the request :raises ModbusException: """ - return self.execute( - pdu_diag.RestartCommunicationsOptionRequest(toggle, slave=slave) - ) + return self.execute(no_response_expected, pdu_diag.RestartCommunicationsOptionRequest(toggle, slave=slave)) - def diag_read_diagnostic_register(self, slave: int = 1) -> T: + def diag_read_diagnostic_register(self, slave: int = 1, no_response_expected: bool = False) -> T: """Diagnose read diagnostic register (code 0x08 sub 0x02). :param slave: (optional) Modbus slave ID + :param no_response_expected: (optional) The client will not expect a response to the request :raises ModbusException: """ - return self.execute( - pdu_diag.ReturnDiagnosticRegisterRequest(slave=slave) - ) + return self.execute(no_response_expected, pdu_diag.ReturnDiagnosticRegisterRequest(slave=slave)) - def diag_change_ascii_input_delimeter(self, slave: int = 1) -> T: + def diag_change_ascii_input_delimeter(self, slave: int = 1, no_response_expected: bool = False) -> T: """Diagnose change ASCII input delimiter (code 0x08 sub 0x03). :param slave: (optional) Modbus slave ID + :param no_response_expected: (optional) The client will not expect a response to the request :raises ModbusException: """ - return self.execute( - pdu_diag.ChangeAsciiInputDelimiterRequest(slave=slave) - ) + return self.execute(no_response_expected, pdu_diag.ChangeAsciiInputDelimiterRequest(slave=slave)) - def diag_force_listen_only(self, slave: int = 1) -> T: + def diag_force_listen_only(self, slave: int = 1, no_response_expected: bool = False) -> T: """Diagnose force listen only (code 0x08 sub 0x04). :param slave: (optional) Modbus slave ID + :param no_response_expected: (optional) The client will not expect a response to the request :raises ModbusException: """ - return self.execute(pdu_diag.ForceListenOnlyModeRequest(slave=slave)) + return self.execute(no_response_expected, pdu_diag.ForceListenOnlyModeRequest(slave=slave)) - def diag_clear_counters(self, slave: int = 1) -> T: + def diag_clear_counters(self, slave: int = 1, no_response_expected: bool = False) -> T: """Diagnose clear counters (code 0x08 sub 0x0A). :param slave: (optional) Modbus slave ID + :param no_response_expected: (optional) The client will not expect a response to the request :raises ModbusException: """ - return self.execute(pdu_diag.ClearCountersRequest(slave=slave)) + return self.execute(no_response_expected, pdu_diag.ClearCountersRequest(slave=slave)) - def diag_read_bus_message_count(self, slave: int = 1) -> T: + def diag_read_bus_message_count(self, slave: int = 1, no_response_expected: bool = False) -> T: """Diagnose read bus message count (code 0x08 sub 0x0B). :param slave: (optional) Modbus slave ID + :param no_response_expected: (optional) The client will not expect a response to the request :raises ModbusException: """ - return self.execute( - pdu_diag.ReturnBusMessageCountRequest(slave=slave) - ) + return self.execute(no_response_expected, pdu_diag.ReturnBusMessageCountRequest(slave=slave)) - def diag_read_bus_comm_error_count(self, slave: int = 1) -> T: + def diag_read_bus_comm_error_count(self, slave: int = 1, no_response_expected: bool = False) -> T: """Diagnose read Bus Communication Error Count (code 0x08 sub 0x0C). :param slave: (optional) Modbus slave ID + :param no_response_expected: (optional) The client will not expect a response to the request :raises ModbusException: """ - return self.execute( - pdu_diag.ReturnBusCommunicationErrorCountRequest(slave=slave) - ) + return self.execute(no_response_expected, pdu_diag.ReturnBusCommunicationErrorCountRequest(slave=slave)) - def diag_read_bus_exception_error_count(self, slave: int = 1) -> T: + def diag_read_bus_exception_error_count(self, slave: int = 1, no_response_expected: bool = False) -> T: """Diagnose read Bus Exception Error Count (code 0x08 sub 0x0D). :param slave: (optional) Modbus slave ID + :param no_response_expected: (optional) The client will not expect a response to the request :raises ModbusException: """ - return self.execute( - pdu_diag.ReturnBusExceptionErrorCountRequest(slave=slave) - ) + return self.execute(no_response_expected, pdu_diag.ReturnBusExceptionErrorCountRequest(slave=slave)) - def diag_read_slave_message_count(self, slave: int = 1) -> T: + def diag_read_slave_message_count(self, slave: int = 1, no_response_expected: bool = False) -> T: """Diagnose read Slave Message Count (code 0x08 sub 0x0E). :param slave: (optional) Modbus slave ID + :param no_response_expected: (optional) The client will not expect a response to the request :raises ModbusException: """ - return self.execute( - pdu_diag.ReturnSlaveMessageCountRequest(slave=slave) - ) + return self.execute(no_response_expected, pdu_diag.ReturnSlaveMessageCountRequest(slave=slave)) - def diag_read_slave_no_response_count(self, slave: int = 1) -> T: + def diag_read_slave_no_response_count(self, slave: int = 1, no_response_expected: bool = False) -> T: """Diagnose read Slave No Response Count (code 0x08 sub 0x0F). :param slave: (optional) Modbus slave ID + :param no_response_expected: (optional) The client will not expect a response to the request :raises ModbusException: """ - return self.execute( - pdu_diag.ReturnSlaveNoResponseCountRequest(slave=slave) - ) + return self.execute(no_response_expected, pdu_diag.ReturnSlaveNoResponseCountRequest(slave=slave)) - def diag_read_slave_nak_count(self, slave: int = 1) -> T: + def diag_read_slave_nak_count(self, slave: int = 1, no_response_expected: bool = False) -> T: """Diagnose read Slave NAK Count (code 0x08 sub 0x10). :param slave: (optional) Modbus slave ID + :param no_response_expected: (optional) The client will not expect a response to the request :raises ModbusException: """ - return self.execute(pdu_diag.ReturnSlaveNAKCountRequest(slave=slave)) + return self.execute(no_response_expected, pdu_diag.ReturnSlaveNAKCountRequest(slave=slave)) - def diag_read_slave_busy_count(self, slave: int = 1) -> T: + def diag_read_slave_busy_count(self, slave: int = 1, no_response_expected: bool = False) -> T: """Diagnose read Slave Busy Count (code 0x08 sub 0x11). :param slave: (optional) Modbus slave ID + :param no_response_expected: (optional) The client will not expect a response to the request :raises ModbusException: """ - return self.execute(pdu_diag.ReturnSlaveBusyCountRequest(slave=slave)) + return self.execute(no_response_expected, pdu_diag.ReturnSlaveBusyCountRequest(slave=slave)) - def diag_read_bus_char_overrun_count(self, slave: int = 1) -> T: + def diag_read_bus_char_overrun_count(self, slave: int = 1, no_response_expected: bool = False) -> T: """Diagnose read Bus Character Overrun Count (code 0x08 sub 0x12). :param slave: (optional) Modbus slave ID + :param no_response_expected: (optional) The client will not expect a response to the request :raises ModbusException: """ - return self.execute( - pdu_diag.ReturnSlaveBusCharacterOverrunCountRequest(slave=slave) - ) + return self.execute(no_response_expected, pdu_diag.ReturnSlaveBusCharacterOverrunCountRequest(slave=slave)) - def diag_read_iop_overrun_count(self, slave: int = 1) -> T: + def diag_read_iop_overrun_count(self, slave: int = 1, no_response_expected: bool = False) -> T: """Diagnose read Iop overrun count (code 0x08 sub 0x13). :param slave: (optional) Modbus slave ID + :param no_response_expected: (optional) The client will not expect a response to the request :raises ModbusException: """ - return self.execute( - pdu_diag.ReturnIopOverrunCountRequest(slave=slave) - ) + return self.execute(no_response_expected, pdu_diag.ReturnIopOverrunCountRequest(slave=slave)) - def diag_clear_overrun_counter(self, slave: int = 1) -> T: + def diag_clear_overrun_counter(self, slave: int = 1, no_response_expected: bool = False) -> T: """Diagnose Clear Overrun Counter and Flag (code 0x08 sub 0x14). :param slave: (optional) Modbus slave ID + :param no_response_expected: (optional) The client will not expect a response to the request :raises ModbusException: """ - return self.execute(pdu_diag.ClearOverrunCountRequest(slave=slave)) + return self.execute(no_response_expected, pdu_diag.ClearOverrunCountRequest(slave=slave)) - def diag_getclear_modbus_response(self, slave: int = 1) -> T: + def diag_getclear_modbus_response(self, slave: int = 1, no_response_expected: bool = False) -> T: """Diagnose Get/Clear modbus plus (code 0x08 sub 0x15). :param slave: (optional) Modbus slave ID + :param no_response_expected: (optional) The client will not expect a response to the request :raises ModbusException: """ - return self.execute(pdu_diag.GetClearModbusPlusRequest(slave=slave)) + return self.execute(no_response_expected, pdu_diag.GetClearModbusPlusRequest(slave=slave)) - def diag_get_comm_event_counter(self, slave: int = 1) -> T: + def diag_get_comm_event_counter(self, slave: int = 1, no_response_expected: bool = False) -> T: """Diagnose get event counter (code 0x0B). + :param slave: (optional) Modbus slave ID + :param no_response_expected: (optional) The client will not expect a response to the request :raises ModbusException: """ - return self.execute(pdu_other_msg.GetCommEventCounterRequest(slave=slave)) + return self.execute(no_response_expected, pdu_other_msg.GetCommEventCounterRequest(slave=slave)) - def diag_get_comm_event_log(self, slave: int = 1) -> T: + def diag_get_comm_event_log(self, slave: int = 1, no_response_expected: bool = False) -> T: """Diagnose get event counter (code 0x0C). + :param slave: (optional) Modbus slave ID + :param no_response_expected: (optional) The client will not expect a response to the request :raises ModbusException: """ - return self.execute(pdu_other_msg.GetCommEventLogRequest(slave=slave)) + return self.execute(no_response_expected, pdu_other_msg.GetCommEventLogRequest(slave=slave)) def write_coils( self, address: int, values: list[bool] | bool, slave: int = 1, + no_response_expected: bool = False ) -> T: """Write coils (code 0x0F). :param address: Start address to write to :param values: List of booleans to write, or a single boolean to write :param slave: (optional) Modbus slave ID + :param no_response_expected: (optional) The client will not expect a response to the request :raises ModbusException: """ - return self.execute( - pdu_bit_write.WriteMultipleCoilsRequest(address, values, slave) - ) + return self.execute(no_response_expected, pdu_bit_write.WriteMultipleCoilsRequest(address, values=values, slave=slave)) def write_registers( - self, address: int, values: list[bytes], slave: int = 1, skip_encode: bool = False) -> T: + self, + address: int, + values: list[bytes | int], + slave: int = 1, + skip_encode: bool = False, + no_response_expected: bool = False + ) -> T: """Write registers (code 0x10). :param address: Start address to write to :param values: List of values to write :param slave: (optional) Modbus slave ID :param skip_encode: (optional) do not encode values + :param no_response_expected: (optional) The client will not expect a response to the request :raises ModbusException: """ - return self.execute( - pdu_req_write.WriteMultipleRegistersRequest(address, values, slave=slave, skip_encode=skip_encode) - ) + return self.execute(no_response_expected, pdu_req_write.WriteMultipleRegistersRequest(address, values,slave=slave,skip_encode=skip_encode)) - def report_slave_id(self, slave: int = 1) -> T: + def report_slave_id(self, slave: int = 1, no_response_expected: bool = False) -> T: """Report slave ID (code 0x11). :param slave: (optional) Modbus slave ID + :param no_response_expected: (optional) The client will not expect a response to the request :raises ModbusException: """ - return self.execute(pdu_other_msg.ReportSlaveIdRequest(slave=slave)) + return self.execute(no_response_expected, pdu_other_msg.ReportSlaveIdRequest(slave=slave)) - def read_file_record(self, records: list[tuple], slave: int = 1) -> T: + def read_file_record(self, records: list[tuple], slave: int = 1, no_response_expected: bool = False) -> T: """Read file record (code 0x14). :param records: List of (Reference type, File number, Record Number, Record Length) :param slave: device id + :param no_response_expected: (optional) The client will not expect a response to the request :raises ModbusException: """ - return self.execute(pdu_file_msg.ReadFileRecordRequest(records, slave=slave)) + return self.execute(no_response_expected, pdu_file_msg.ReadFileRecordRequest(records, slave=slave)) - def write_file_record(self, records: list[tuple], slave: int = 1) -> T: + def write_file_record(self, records: list[tuple], slave: int = 1, no_response_expected: bool = False) -> T: """Write file record (code 0x15). :param records: List of (Reference type, File number, Record Number, Record Length) :param slave: (optional) Device id + :param no_response_expected: (optional) The client will not expect a response to the request + :param no_response_expected: (optional) The client will not expect a response to the request :raises ModbusException: """ - return self.execute(pdu_file_msg.WriteFileRecordRequest(records, slave=slave)) + return self.execute(no_response_expected, pdu_file_msg.WriteFileRecordRequest(records,slave=slave)) def mask_write_register( self, @@ -367,6 +393,7 @@ def mask_write_register( and_mask: int = 0xFFFF, or_mask: int = 0x0000, slave: int = 1, + no_response_expected: bool = False ) -> T: """Mask write register (code 0x16). @@ -374,11 +401,10 @@ def mask_write_register( :param and_mask: The and bitmask to apply to the register address :param or_mask: The or bitmask to apply to the register address :param slave: (optional) device id + :param no_response_expected: (optional) The client will not expect a response to the request :raises ModbusException: """ - return self.execute( - pdu_req_write.MaskWriteRegisterRequest(address, and_mask, or_mask, slave=slave) - ) + return self.execute(no_response_expected, pdu_req_write.MaskWriteRegisterRequest(address, and_mask, or_mask, slave=slave)) def readwrite_registers( self, @@ -388,6 +414,7 @@ def readwrite_registers( address: int | None = None, values: list[int] | int = 0, slave: int = 1, + no_response_expected: bool = False ) -> T: """Read/Write registers (code 0x17). @@ -397,44 +424,39 @@ def readwrite_registers( :param address: (optional) use as read/write address :param values: List of values to write, or a single value to write :param slave: (optional) Modbus slave ID + :param no_response_expected: (optional) The client will not expect a response to the request :raises ModbusException: """ if address: read_address = address write_address = address - return self.execute( - pdu_reg_read.ReadWriteMultipleRegistersRequest( - read_address=read_address, - read_count=read_count, - write_address=write_address, - write_registers=values, - slave=slave, - ) - ) + return self.execute(no_response_expected, pdu_reg_read.ReadWriteMultipleRegistersRequest( read_address=read_address, read_count=read_count, write_address=write_address, write_registers=values,slave=slave)) - def read_fifo_queue(self, address: int = 0x0000, slave: int = 1) -> T: + def read_fifo_queue(self, address: int = 0x0000, slave: int = 1, no_response_expected: bool = False) -> T: """Read FIFO queue (code 0x18). :param address: The address to start reading from :param slave: (optional) device id + :param no_response_expected: (optional) The client will not expect a response to the request :raises ModbusException: """ - return self.execute(pdu_file_msg.ReadFifoQueueRequest(address, slave=slave)) + return self.execute(no_response_expected, pdu_file_msg.ReadFifoQueueRequest(address, slave=slave)) # code 0x2B sub 0x0D: CANopen General Reference Request and Response, NOT IMPLEMENTED - def read_device_information( - self, read_code: int | None = None, object_id: int = 0x00, slave: int = 1) -> T: + def read_device_information(self, read_code: int | None = None, + object_id: int = 0x00, + slave: int = 1, + no_response_expected: bool = False) -> T: """Read FIFO queue (code 0x2B sub 0x0E). :param read_code: The device information read code :param object_id: The object to read from :param slave: (optional) Device id + :param no_response_expected: (optional) The client will not expect a response to the request :raises ModbusException: """ - return self.execute( - pdu_mei.ReadDeviceInformationRequest(read_code, object_id, slave=slave) - ) + return self.execute(no_response_expected, pdu_mei.ReadDeviceInformationRequest(read_code, object_id, slave=slave)) # ------------------ # Converter methods diff --git a/pymodbus/client/modbusclientprotocol.py b/pymodbus/client/modbusclientprotocol.py index a06025f4d..49c788b38 100644 --- a/pymodbus/client/modbusclientprotocol.py +++ b/pymodbus/client/modbusclientprotocol.py @@ -3,13 +3,13 @@ from collections.abc import Callable -from pymodbus.factory import ClientDecoder from pymodbus.framer import ( FRAMER_NAME_TO_CLASS, FramerBase, FramerType, ) from pymodbus.logging import Log +from pymodbus.pdu import DecodePDU from pymodbus.transaction import ModbusTransactionManager from pymodbus.transport import CommParams, ModbusProtocol @@ -35,7 +35,7 @@ def __init__( self.on_connect_callback = on_connect_callback # Common variables. - self.framer: FramerBase = (FRAMER_NAME_TO_CLASS[framer])(ClientDecoder(), []) + self.framer: FramerBase = (FRAMER_NAME_TO_CLASS[framer])(DecodePDU(False)) self.transaction = ModbusTransactionManager() def _handle_response(self, reply): @@ -68,8 +68,10 @@ def callback_data(self, data: bytes, addr: tuple | None = None) -> int: returns number of bytes consumed """ - self.framer.processIncomingPacket(data, self._handle_response) - return len(data) + used_len, pdu = self.framer.processIncomingFrame(data) + if pdu: + self._handle_response(pdu) + return used_len def __str__(self): """Build a string representation of the connection. diff --git a/pymodbus/client/serial.py b/pymodbus/client/serial.py index fe74fb99f..a94502624 100644 --- a/pymodbus/client/serial.py +++ b/pymodbus/client/serial.py @@ -37,7 +37,7 @@ class AsyncModbusSerialClient(ModbusBaseClient): :param name: Set communication name, used in logging :param reconnect_delay: Minimum delay in seconds.milliseconds before reconnecting. :param reconnect_delay_max: Maximum delay in seconds.milliseconds before reconnecting. - :param timeout: Timeout for a connection request, in seconds. + :param timeout: Timeout for connecting and receiving data, in seconds. :param retries: Max number of retries per request. :param on_connect_callback: Function that will be called just before a connection attempt. @@ -121,7 +121,7 @@ class ModbusSerialClient(ModbusBaseSyncClient): :param name: Set communication name, used in logging :param reconnect_delay: Not used in the sync client :param reconnect_delay_max: Not used in the sync client - :param timeout: Timeout for a connection request, in seconds. + :param timeout: Timeout for connecting and receiving data, in seconds. :param retries: Max number of retries per request. Note that unlike the async client, the sync client does not perform diff --git a/pymodbus/client/tcp.py b/pymodbus/client/tcp.py index 360a80e0f..0983f2c19 100644 --- a/pymodbus/client/tcp.py +++ b/pymodbus/client/tcp.py @@ -28,7 +28,7 @@ class AsyncModbusTcpClient(ModbusBaseClient): :param source_address: source address of client :param reconnect_delay: Minimum delay in seconds.milliseconds before reconnecting. :param reconnect_delay_max: Maximum delay in seconds.milliseconds before reconnecting. - :param timeout: Timeout for a connection request, in seconds. + :param timeout: Timeout for connecting and receiving data, in seconds. :param retries: Max number of retries per request. :param on_connect_callback: Function that will be called just before a connection attempt. @@ -99,7 +99,7 @@ class ModbusTcpClient(ModbusBaseSyncClient): :param source_address: source address of client :param reconnect_delay: Not used in the sync client :param reconnect_delay_max: Not used in the sync client - :param timeout: Timeout for a connection request, in seconds. + :param timeout: Timeout for connecting and receiving data, in seconds. :param retries: Max number of retries per request. .. tip:: diff --git a/pymodbus/client/tls.py b/pymodbus/client/tls.py index c2310ab98..8b368d47a 100644 --- a/pymodbus/client/tls.py +++ b/pymodbus/client/tls.py @@ -27,7 +27,7 @@ class AsyncModbusTlsClient(AsyncModbusTcpClient): :param source_address: Source address of client :param reconnect_delay: Minimum delay in seconds.milliseconds before reconnecting. :param reconnect_delay_max: Maximum delay in seconds.milliseconds before reconnecting. - :param timeout: Timeout for a connection request, in seconds. + :param timeout: Timeout for connecting and receiving data, in seconds. :param retries: Max number of retries per request. :param on_connect_callback: Function that will be called just before a connection attempt. @@ -121,7 +121,7 @@ class ModbusTlsClient(ModbusTcpClient): :param source_address: Source address of client :param reconnect_delay: Not used in the sync client :param reconnect_delay_max: Not used in the sync client - :param timeout: Timeout for a connection request, in seconds. + :param timeout: Timeout for connecting and receiving data, in seconds. :param retries: Max number of retries per request. .. tip:: diff --git a/pymodbus/client/udp.py b/pymodbus/client/udp.py index d3c68612c..86c05895f 100644 --- a/pymodbus/client/udp.py +++ b/pymodbus/client/udp.py @@ -30,7 +30,7 @@ class AsyncModbusUdpClient(ModbusBaseClient): :param source_address: source address of client, :param reconnect_delay: Minimum delay in seconds.milliseconds before reconnecting. :param reconnect_delay_max: Maximum delay in seconds.milliseconds before reconnecting. - :param timeout: Timeout for a connection request, in seconds. + :param timeout: Timeout for connecting and receiving data, in seconds. :param retries: Max number of retries per request. :param on_connect_callback: Function that will be called just before a connection attempt. @@ -101,7 +101,7 @@ class ModbusUdpClient(ModbusBaseSyncClient): :param source_address: source address of client, :param reconnect_delay: Not used in the sync client :param reconnect_delay_max: Not used in the sync client - :param timeout: Timeout for a connection request, in seconds. + :param timeout: Timeout for connecting and receiving data, in seconds. :param retries: Max number of retries per request. .. tip:: diff --git a/pymodbus/device.py b/pymodbus/device.py index 61d97e8db..136ac0c82 100644 --- a/pymodbus/device.py +++ b/pymodbus/device.py @@ -233,7 +233,7 @@ def __str__(self): class DeviceInformationFactory: # pylint: disable=too-few-public-methods - """This is a helper factory. + """This is a helper. That really just hides some of the complexity of processing the device information @@ -323,25 +323,22 @@ class ModbusCountersHandler: 0x0D 3 Return Slave Exception Error Count Quantity of MODBUS exception error detected by the remote device - since its last restart, clear counters operation, or power-up. It - comprises also the error detected in broadcast messages even if an - exception message is not returned in this case. + since its last restart, clear counters operation, or power-up. Exception errors are described and listed in "MODBUS Application Protocol Specification" document. 0xOE 4 Return Slave Message Count - Quantity of messages addressed to the remote device, including - broadcast messages, that the remote device has processed since its - last restart, clear counters operation, or power-up. + Quantity of messages addressed to the remote device that the remote + device has processed since its last restart, clear counters operation, + or power-up. 0x0F 5 Return Slave No Response Count Quantity of messages received by the remote device for which it returned no response (neither a normal response nor an exception response), since its last restart, clear counters operation, or - power-up. Then, this counter counts the number of broadcast - messages it has received. + power-up. 0x10 6 Return Slave NAK Count diff --git a/pymodbus/events.py b/pymodbus/events.py index 848e4fa4b..aa576a07f 100644 --- a/pymodbus/events.py +++ b/pymodbus/events.py @@ -5,27 +5,25 @@ (the high-order bit) in each byte. It may be further defined by bit 6. """ # pylint: disable=missing-type-doc -from pymodbus.exceptions import NotImplementedException, ParameterException +from abc import ABC, abstractmethod + +from pymodbus.exceptions import ParameterException from pymodbus.utilities import pack_bitstring, unpack_bitstring -class ModbusEvent: +class ModbusEvent(ABC): """Define modbus events.""" - def encode(self): - """Encode the status bits to an event message. - - :raises NotImplementedException: - """ - raise NotImplementedException + @abstractmethod + def encode(self) -> bytes: + """Encode the status bits to an event message.""" + @abstractmethod def decode(self, event): """Decode the event message to its status bits. :param event: The event to decode - :raises NotImplementedException: """ - raise NotImplementedException class RemoteReceiveEvent(ModbusEvent): diff --git a/pymodbus/factory.py b/pymodbus/factory.py deleted file mode 100644 index ea5fff7a5..000000000 --- a/pymodbus/factory.py +++ /dev/null @@ -1,289 +0,0 @@ -"""Modbus Request/Response Decoder Factories. - -The following factories make it easy to decode request/response messages. -To add a new request/response pair to be decodeable by the library, simply -add them to the respective function lookup table (order doesn't matter, but -it does help keep things organized). - -Regardless of how many functions are added to the lookup, O(1) behavior is -kept as a result of a pre-computed lookup dictionary. -""" - -# pylint: disable=missing-type-doc -from collections.abc import Callable - -from pymodbus.exceptions import MessageRegisterException, ModbusException -from pymodbus.logging import Log -from pymodbus.pdu import bit_read_message as bit_r_msg -from pymodbus.pdu import bit_write_message as bit_w_msg -from pymodbus.pdu import diag_message as diag_msg -from pymodbus.pdu import file_message as file_msg -from pymodbus.pdu import mei_message as mei_msg -from pymodbus.pdu import other_message as o_msg -from pymodbus.pdu import pdu -from pymodbus.pdu import register_read_message as reg_r_msg -from pymodbus.pdu import register_write_message as reg_w_msg - - -# --------------------------------------------------------------------------- # -# Server Decoder -# --------------------------------------------------------------------------- # -class ServerDecoder: - """Request Message Factory (Server). - - To add more implemented functions, simply add them to the list - """ - - __function_table = [ - reg_r_msg.ReadHoldingRegistersRequest, - bit_r_msg.ReadDiscreteInputsRequest, - reg_r_msg.ReadInputRegistersRequest, - bit_r_msg.ReadCoilsRequest, - bit_w_msg.WriteMultipleCoilsRequest, - reg_w_msg.WriteMultipleRegistersRequest, - reg_w_msg.WriteSingleRegisterRequest, - bit_w_msg.WriteSingleCoilRequest, - reg_r_msg.ReadWriteMultipleRegistersRequest, - diag_msg.DiagnosticStatusRequest, - o_msg.ReadExceptionStatusRequest, - o_msg.GetCommEventCounterRequest, - o_msg.GetCommEventLogRequest, - o_msg.ReportSlaveIdRequest, - file_msg.ReadFileRecordRequest, - file_msg.WriteFileRecordRequest, - reg_w_msg.MaskWriteRegisterRequest, - file_msg.ReadFifoQueueRequest, - mei_msg.ReadDeviceInformationRequest, - ] - __sub_function_table = [ - diag_msg.ReturnQueryDataRequest, - diag_msg.RestartCommunicationsOptionRequest, - diag_msg.ReturnDiagnosticRegisterRequest, - diag_msg.ChangeAsciiInputDelimiterRequest, - diag_msg.ForceListenOnlyModeRequest, - diag_msg.ClearCountersRequest, - diag_msg.ReturnBusMessageCountRequest, - diag_msg.ReturnBusCommunicationErrorCountRequest, - diag_msg.ReturnBusExceptionErrorCountRequest, - diag_msg.ReturnSlaveMessageCountRequest, - diag_msg.ReturnSlaveNoResponseCountRequest, - diag_msg.ReturnSlaveNAKCountRequest, - diag_msg.ReturnSlaveBusyCountRequest, - diag_msg.ReturnSlaveBusCharacterOverrunCountRequest, - diag_msg.ReturnIopOverrunCountRequest, - diag_msg.ClearOverrunCountRequest, - diag_msg.GetClearModbusPlusRequest, - mei_msg.ReadDeviceInformationRequest, - ] - - @classmethod - def getFCdict(cls) -> dict[int, Callable]: - """Build function code - class list.""" - return {f.function_code: f for f in cls.__function_table} # type: ignore[attr-defined] - - def __init__(self) -> None: - """Initialize the client lookup tables.""" - functions = {f.function_code for f in self.__function_table} # type: ignore[attr-defined] - self.lookup = self.getFCdict() - self.__sub_lookup: dict[int, dict[int, Callable]] = {f: {} for f in functions} - for f in self.__sub_function_table: - self.__sub_lookup[f.function_code][f.sub_function_code] = f # type: ignore[attr-defined] - - def decode(self, message): - """Decode a request packet. - - :param message: The raw modbus request packet - :return: The decoded modbus message or None if error - """ - try: - return self._helper(message) - except ModbusException as exc: - Log.warning("Unable to decode request {}", exc) - return None - - def lookupPduClass(self, function_code): - """Use `function_code` to determine the class of the PDU. - - :param function_code: The function code specified in a frame. - :returns: The class of the PDU that has a matching `function_code`. - """ - return self.lookup.get(function_code, pdu.ExceptionResponse) - - def _helper(self, data: str): - """Generate the correct request object from a valid request packet. - - This decodes from a list of the currently implemented request types. - - :param data: The request packet to decode - :returns: The decoded request or illegal function request object - """ - function_code = int(data[0]) - if not (request := self.lookup.get(function_code, lambda: None)()): - Log.debug("Factory Request[{}]", function_code) - request = pdu.IllegalFunctionRequest(function_code, 0, 0, False) - else: - fc_string = "{}: {}".format( # pylint: disable=consider-using-f-string - str(self.lookup[function_code]) # pylint: disable=use-maxsplit-arg - .split(".")[-1] - .rstrip('">"'), - function_code, - ) - Log.debug("Factory Request[{}]", fc_string) - request.decode(data[1:]) - - if hasattr(request, "sub_function_code"): - lookup = self.__sub_lookup.get(request.function_code, {}) - if subtype := lookup.get(request.sub_function_code, None): - request.__class__ = subtype - - return request - - def register(self, function): - """Register a function and sub function class with the decoder. - - :param function: Custom function class to register - :raises MessageRegisterException: - """ - if not issubclass(function, pdu.ModbusRequest): - raise MessageRegisterException( - f'"{function.__class__.__name__}" is Not a valid Modbus Message' - ". Class needs to be derived from " - "`pymodbus.pdu.ModbusRequest` " - ) - self.lookup[function.function_code] = function - if hasattr(function, "sub_function_code"): - if function.function_code not in self.__sub_lookup: - self.__sub_lookup[function.function_code] = {} - self.__sub_lookup[function.function_code][ - function.sub_function_code - ] = function - - -# --------------------------------------------------------------------------- # -# Client Decoder -# --------------------------------------------------------------------------- # -class ClientDecoder: - """Response Message Factory (Client). - - To add more implemented functions, simply add them to the list - """ - - function_table = [ - reg_r_msg.ReadHoldingRegistersResponse, - bit_r_msg.ReadDiscreteInputsResponse, - reg_r_msg.ReadInputRegistersResponse, - bit_r_msg.ReadCoilsResponse, - bit_w_msg.WriteMultipleCoilsResponse, - reg_w_msg.WriteMultipleRegistersResponse, - reg_w_msg.WriteSingleRegisterResponse, - bit_w_msg.WriteSingleCoilResponse, - reg_r_msg.ReadWriteMultipleRegistersResponse, - diag_msg.DiagnosticStatusResponse, - o_msg.ReadExceptionStatusResponse, - o_msg.GetCommEventCounterResponse, - o_msg.GetCommEventLogResponse, - o_msg.ReportSlaveIdResponse, - file_msg.ReadFileRecordResponse, - file_msg.WriteFileRecordResponse, - reg_w_msg.MaskWriteRegisterResponse, - file_msg.ReadFifoQueueResponse, - mei_msg.ReadDeviceInformationResponse, - ] - __sub_function_table = [ - diag_msg.ReturnQueryDataResponse, - diag_msg.RestartCommunicationsOptionResponse, - diag_msg.ReturnDiagnosticRegisterResponse, - diag_msg.ChangeAsciiInputDelimiterResponse, - diag_msg.ForceListenOnlyModeResponse, - diag_msg.ClearCountersResponse, - diag_msg.ReturnBusMessageCountResponse, - diag_msg.ReturnBusCommunicationErrorCountResponse, - diag_msg.ReturnBusExceptionErrorCountResponse, - diag_msg.ReturnSlaveMessageCountResponse, - diag_msg.ReturnSlaveNoResponseCountResponse, - diag_msg.ReturnSlaveNAKCountResponse, - diag_msg.ReturnSlaveBusyCountResponse, - diag_msg.ReturnSlaveBusCharacterOverrunCountResponse, - diag_msg.ReturnIopOverrunCountResponse, - diag_msg.ClearOverrunCountResponse, - diag_msg.GetClearModbusPlusResponse, - mei_msg.ReadDeviceInformationResponse, - ] - - def __init__(self) -> None: - """Initialize the client lookup tables.""" - functions = {f.function_code for f in self.function_table} # type: ignore[attr-defined] - self.lookup = {f.function_code: f for f in self.function_table} # type: ignore[attr-defined] - self.__sub_lookup: dict[int, dict[int, Callable]] = {f: {} for f in functions} - for f in self.__sub_function_table: - self.__sub_lookup[f.function_code][f.sub_function_code] = f # type: ignore[attr-defined] - - def lookupPduClass(self, function_code): - """Use `function_code` to determine the class of the PDU. - - :param function_code: The function code specified in a frame. - :returns: The class of the PDU that has a matching `function_code`. - """ - return self.lookup.get(function_code, pdu.ExceptionResponse) - - def decode(self, message): - """Decode a response packet. - - :param message: The raw packet to decode - :return: The decoded modbus message or None if error - """ - try: - return self._helper(message) - except ModbusException as exc: - Log.error("Unable to decode response {}", exc) - return None - - def _helper(self, data: str): - """Generate the correct response object from a valid response packet. - - This decodes from a list of the currently implemented request types. - - :param data: The response packet to decode - :returns: The decoded request or an exception response object - :raises ModbusException: - """ - fc_string = data[0] - function_code = int(fc_string) - if function_code in self.lookup: # pylint: disable=consider-using-assignment-expr - fc_string = "{}: {}".format( # pylint: disable=consider-using-f-string - str(self.lookup[function_code]) # pylint: disable=use-maxsplit-arg - .split(".")[-1] - .rstrip('">"'), - function_code, - ) - Log.debug("Factory Response[{}]", fc_string) - response = self.lookup.get(function_code, lambda: None)() - if function_code > 0x80: - code = function_code & 0x7F # strip error portion - response = pdu.ExceptionResponse(code, pdu.ModbusExceptions.IllegalFunction) - if not response: - raise ModbusException(f"Unknown response {function_code}") - response.decode(data[1:]) - - if hasattr(response, "sub_function_code"): - lookup = self.__sub_lookup.get(response.function_code, {}) - if subtype := lookup.get(response.sub_function_code, None): - response.__class__ = subtype - - return response - - def register(self, function): - """Register a function and sub function class with the decoder.""" - if function and not issubclass(function, pdu.ModbusResponse): - raise MessageRegisterException( - f'"{function.__class__.__name__}" is Not a valid Modbus Message' - ". Class needs to be derived from " - "`pymodbus.pdu.ModbusResponse` " - ) - self.lookup[function.function_code] = function - if hasattr(function, "sub_function_code"): - if function.function_code not in self.__sub_lookup: - self.__sub_lookup[function.function_code] = {} - self.__sub_lookup[function.function_code][ - function.sub_function_code - ] = function diff --git a/pymodbus/framer/ascii.py b/pymodbus/framer/ascii.py index 48fbb67eb..cc8a2f3a5 100644 --- a/pymodbus/framer/ascii.py +++ b/pymodbus/framer/ascii.py @@ -15,6 +15,7 @@ class FramerAscii(FramerBase): r"""Modbus ASCII Frame Controller. + Layout:: [ Start ][ Dev id ][ Function ][ Data ][ LRC ][ End ] 1c 2c 2c N*2c 1c 2c @@ -32,44 +33,45 @@ class FramerAscii(FramerBase): MIN_SIZE = 10 - def specific_decode(self, data: bytes, data_len: int) -> tuple[int, bytes]: + def decode(self, data: bytes) -> tuple[int, int, int, bytes]: """Decode ADU.""" used_len = 0 + data_len = len(data) while True: if data_len - used_len < self.MIN_SIZE: - return used_len, self.EMPTY + Log.debug("Short frame: {} wait for more data", data, ":hex") + return used_len, 0, 0, self.EMPTY buffer = data[used_len:] if buffer[0:1] != self.START: if (i := buffer.find(self.START)) == -1: Log.debug("No frame start in data: {}, wait for data", data, ":hex") - return data_len, self.EMPTY + return data_len, 0, 0, self.EMPTY used_len += i continue if (end := buffer.find(self.END)) == -1: Log.debug("Incomplete frame: {} wait for more data", data, ":hex") - return used_len, self.EMPTY - self.incoming_dev_id = int(buffer[1:3], 16) - self.incoming_tid = self.incoming_dev_id + return used_len, 0, 0, self.EMPTY + dev_id = int(buffer[1:3], 16) lrc = int(buffer[end - 2: end], 16) msg = a2b_hex(buffer[1 : end - 2]) used_len += end + 2 if not self.check_LRC(msg, lrc): Log.debug("LRC wrong in frame: {} skipping", data, ":hex") continue - return used_len, msg[1:] + return used_len, dev_id, 0, msg[1:] def encode(self, data: bytes, device_id: int, _tid: int) -> bytes: """Encode ADU.""" dev_id = device_id.to_bytes(1,'big') checksum = self.compute_LRC(dev_id + data) - packet = ( + frame = ( self.START + f"{device_id:02x}".encode() + b2a_hex(data) + f"{checksum:02x}".encode() + self.END ).upper() - return packet + return frame @classmethod def compute_LRC(cls, data: bytes) -> int: diff --git a/pymodbus/framer/base.py b/pymodbus/framer/base.py index bad9c71b4..ffdbf52ca 100644 --- a/pymodbus/framer/base.py +++ b/pymodbus/framer/base.py @@ -9,9 +9,8 @@ from enum import Enum from pymodbus.exceptions import ModbusIOException -from pymodbus.factory import ClientDecoder, ServerDecoder from pymodbus.logging import Log -from pymodbus.pdu import ModbusRequest, ModbusResponse +from pymodbus.pdu import DecodePDU, ModbusPDU class FramerType(str, Enum): @@ -31,94 +30,74 @@ class FramerBase: def __init__( self, - decoder: ClientDecoder | ServerDecoder, - dev_ids: list[int], + decoder: DecodePDU, ) -> None: """Initialize a ADU (framer) instance.""" self.decoder = decoder - if 0 in dev_ids: - dev_ids = [] - self.dev_ids = dev_ids - self.incoming_dev_id = 0 - self.incoming_tid = 0 self.databuffer = b"" - def decode(self, data: bytes) -> tuple[int, bytes]: + def decode(self, _data: bytes) -> tuple[int, int, int, bytes]: """Decode ADU. returns: used_len (int) or 0 to read more + dev_id, + tid, modbus request/response (bytes) """ - if (data_len := len(data)) < self.MIN_SIZE: - Log.debug("Very short frame (NO MBAP): {} wait for more data", data, ":hex") - return 0, self.EMPTY - used_len, res_data = self.specific_decode(data, data_len) - if not res_data: - self.incoming_dev_id = 0 - self.incoming_tid = 0 - return used_len, res_data - - def specific_decode(self, data: bytes, data_len: int) -> tuple[int, bytes]: - """Decode ADU. - - returns: - used_len (int) or 0 to read more - modbus request/response (bytes) - """ - return data_len, data - + return 0, 0, 0, self.EMPTY - def encode(self, pdu: bytes, _dev_id: int, _tid: int) -> bytes: + def encode(self, data: bytes, _dev_id: int, _tid: int) -> bytes: """Encode ADU. returns: modbus ADU (bytes) """ - return pdu + return data - def buildPacket(self, message: ModbusRequest | ModbusResponse) -> bytes: + def buildFrame(self, message: ModbusPDU) -> bytes: """Create a ready to send modbus packet. :param message: The populated request/response to send """ data = message.function_code.to_bytes(1,'big') + message.encode() - packet = self.encode(data, message.slave_id, message.transaction_id) - return packet + frame = self.encode(data, message.slave_id, message.transaction_id) + return frame - def processIncomingPacket(self, data: bytes, callback, tid=None): + def processIncomingFrame(self, data: bytes) -> tuple[int, ModbusPDU | None]: """Process new packet pattern. This takes in a new request packet, adds it to the current packet stream, and performs framing on it. That is, checks for complete messages, and once found, will process all that - exist. This handles the case when we read N + 1 or 1 // N - messages at a time instead of 1. + exist. + """ + used_len = 0 + while True: + data_len, pdu = self._processIncomingFrame(data[used_len:]) + used_len += data_len + if not data_len: + return used_len, None + if pdu: + return used_len, pdu + + def _processIncomingFrame(self, data: bytes) -> tuple[int, ModbusPDU | None]: + """Process new packet pattern. - The processed and decoded messages are pushed to the callback - function to process and send. + This takes in a new request packet, adds it to the current + packet stream, and performs framing on it. That is, checks + for complete messages, and once found, will process all that + exist. """ Log.debug("Processing: {}", data, ":hex") - self.databuffer += data - while True: - if self.databuffer == b'': - return - used_len, data = self.decode(self.databuffer) - self.databuffer = self.databuffer[used_len:] - if not data: - return - if self.dev_ids and self.incoming_dev_id not in self.dev_ids: - Log.debug("Not a valid slave id - {}, ignoring!!", self.incoming_dev_id) - self.databuffer = b'' - continue - if (result := self.decoder.decode(data)) is None: - self.databuffer = b'' - raise ModbusIOException("Unable to decode request") - result.slave_id = self.incoming_dev_id - result.transaction_id = self.incoming_tid - Log.debug("Frame advanced, resetting header!!") - self.databuffer = self.databuffer[used_len:] - if tid and result.transaction_id and tid != result.transaction_id: - self.databuffer = b'' - else: - callback(result) # defer or push to a thread? + if not data: + return 0, None + used_len, dev_id, tid, frame_data = self.decode(data) + if not frame_data: + return used_len, None + if (result := self.decoder.decode(frame_data)) is None: + raise ModbusIOException("Unable to decode request") + result.slave_id = dev_id + result.transaction_id = tid + Log.debug("Frame advanced, resetting header!!") + return used_len, result diff --git a/pymodbus/framer/rtu.py b/pymodbus/framer/rtu.py index 05417013a..76fe07675 100644 --- a/pymodbus/framer/rtu.py +++ b/pymodbus/framer/rtu.py @@ -8,43 +8,50 @@ class FramerRTU(FramerBase): """Modbus RTU frame type. - [ Start Wait ] [Address ][ Function Code] [ Data ][ CRC ] - 3.5 chars 1b 1b Nb 2b + Layout:: - * Note: due to the USB converter and the OS drivers, timing cannot be quaranteed - neither when receiving nor when sending. + [ Start Wait ] [Address ][ Function Code] [ Data ][ CRC ] + 3.5 chars 1b 1b Nb 2b + + .. note:: + + due to the USB converter and the OS drivers, timing cannot be quaranteed + neither when receiving nor when sending. Decoding is a complicated process because the RTU frame does not have a fixed prefix only suffix, therefore it is necessary to decode the content (PDU) to get length etc. - There are some protocol restrictions that help with the detection. For client: - a request causes 1 response ! - Multiple requests are NOT allowed (master-slave protocol) - the server will not retransmit responses + this means decoding is always exactly 1 frame (response) For server (Single device) - only 1 request allowed (master-slave) protocol - the client (master) may retransmit but in larger time intervals + this means decoding is always exactly 1 frame (request) For server (Multidrop line --> devices in parallel) - only 1 request allowed (master-slave) protocol - other devices will send responses - the client (master) may retransmit but in larger time intervals + this means decoding is always exactly 1 frame request, however some requests will be for unknown slaves, which must be ignored together with the response from the unknown slave. - >>>>> NOT IMPLEMENTED <<<<< Recovery from bad cabling and unstable USB etc is important, the following scenarios is possible: - - garble data before frame - - garble data in frame - - garble data after frame - - data in frame garbled (wrong CRC) + + - garble data before frame + - garble data in frame + - garble data after frame + - data in frame garbled (wrong CRC) + decoding assumes the frame is sound, and if not enters a hunting mode. The 3.5 byte transmission time at the slowest speed 1.200Bps is 31ms. @@ -52,7 +59,7 @@ class FramerRTU(FramerBase): If no data is received for 50ms the transmission / frame can be considered complete. - The following table is a listing of the baud wait times for the specified + The following table is a listing of the baud wait times for the specified baud rates:: ------------------------------------------------------------------ @@ -63,12 +70,9 @@ class FramerRTU(FramerBase): 9600 1666.7 us 3958.3 us 19200 833.3 us 1979.2 us 38400 416.7 us 989.6 us - ... ------------------------------------------------------------------ 1 Byte = start + 8 bits + parity + stop = 11 bits (1/Baud)(bits) = delay seconds - - >>>>> NOT IMPLEMENTED <<<<< """ MIN_SIZE = 4 # @@ -93,43 +97,37 @@ def generate_crc16_table(cls) -> list[int]: crc16_table: list[int] = [0] - def specific_decode(self, data: bytes, data_len: int) -> tuple[int, bytes]: + def decode(self, data: bytes) -> tuple[int, int, int, bytes]: """Decode ADU.""" - for used_len in range(data_len): # pragma: no cover + data_len = len(data) + for used_len in range(data_len): if data_len - used_len < self.MIN_SIZE: Log.debug("Short frame: {} wait for more data", data, ":hex") - return used_len, self.EMPTY - self.incoming_dev_id = int(data[used_len]) + return used_len, 0, 0, self.EMPTY + dev_id = int(data[used_len]) func_code = int(data[used_len + 1]) - if (self.dev_ids and self.incoming_dev_id not in self.dev_ids) or func_code & 0x7F not in self.decoder.lookup: + if func_code & 0x7F not in self.decoder.lookup: continue - if data_len - used_len < self.MIN_SIZE: # pragma: no cover - Log.debug("Garble in front {}, then short frame: {} wait for more data", used_len, data, ":hex") - return used_len, self.EMPTY pdu_class = self.decoder.lookupPduClass(func_code) - try: - size = pdu_class.calculateRtuFrameSize(data[used_len:]) - except IndexError: # pragma: no cover + if not (size := pdu_class.calculateRtuFrameSize(data[used_len:])): size = data_len +1 if data_len < used_len +size: Log.debug("Frame - not ready") - if used_len: # pragma: no cover - continue - return used_len, self.EMPTY # pragma: no cover + return used_len, dev_id, 0, self.EMPTY start_crc = used_len + size -2 crc = data[start_crc : start_crc + 2] crc_val = (int(crc[0]) << 8) + int(crc[1]) if not FramerRTU.check_CRC(data[used_len : start_crc], crc_val): Log.debug("Frame check failed, ignoring!!") continue - return start_crc + 2, data[used_len + 1 : start_crc] - return used_len, self.EMPTY # pragma: no cover + return start_crc + 2, dev_id, 0, data[used_len + 1 : start_crc] + return 0, 0, 0, self.EMPTY def encode(self, pdu: bytes, device_id: int, _tid: int) -> bytes: """Encode ADU.""" - packet = device_id.to_bytes(1,'big') + pdu - return packet + FramerRTU.compute_CRC(packet).to_bytes(2,'big') + frame = device_id.to_bytes(1,'big') + pdu + return frame + FramerRTU.compute_CRC(frame).to_bytes(2,'big') @classmethod def check_CRC(cls, data: bytes, check: int) -> bool: diff --git a/pymodbus/framer/socket.py b/pymodbus/framer/socket.py index 53de210fc..eb2617683 100644 --- a/pymodbus/framer/socket.py +++ b/pymodbus/framer/socket.py @@ -8,34 +8,39 @@ class FramerSocket(FramerBase): """Modbus Socket frame type. - [ MBAP Header ] [ Function Code] [ Data ] - [ tid ][ pid ][ length ][ uid ] - 2b 2b 2b 1b 1b Nb + Layout:: - * length = uid + function code + data + [ MBAP Header ] [ Function Code] [ Data ] + [ tid ][ pid ][ length ][ uid ] + 2b 2b 2b 1b 1b Nb + + length = uid + function code + data """ MIN_SIZE = 8 - def specific_decode(self, data: bytes, data_len: int) -> tuple[int, bytes]: + def decode(self, data: bytes) -> tuple[int, int, int, bytes]: """Decode ADU.""" - self.incoming_tid = int.from_bytes(data[0:2], 'big') + if (data_len := len(data)) < self.MIN_SIZE: + Log.debug("Very short frame (NO MBAP): {} wait for more data", data, ":hex") + return 0, 0, 0, self.EMPTY + tid = int.from_bytes(data[0:2], 'big') msg_len = int.from_bytes(data[4:6], 'big') + 6 - self.incoming_dev_id = int(data[6]) + dev_id = int(data[6]) if data_len < msg_len: Log.debug("Short frame: {} wait for more data", data, ":hex") - return 0, self.EMPTY + return 0, 0, 0, self.EMPTY if msg_len == 8 and data_len == 9: msg_len = 9 - return msg_len, data[7:msg_len] + return msg_len, dev_id, tid, data[7:msg_len] def encode(self, pdu: bytes, device_id: int, tid: int) -> bytes: """Encode ADU.""" - packet = ( + frame = ( tid.to_bytes(2, 'big') + b'\x00\x00' + (len(pdu) + 1).to_bytes(2, 'big') + device_id.to_bytes(1, 'big') + pdu ) - return packet + return frame diff --git a/pymodbus/framer/tls.py b/pymodbus/framer/tls.py index 4565a6811..dd10f1f6e 100644 --- a/pymodbus/framer/tls.py +++ b/pymodbus/framer/tls.py @@ -7,13 +7,14 @@ class FramerTLS(FramerBase): """Modbus TLS frame type. - [ Function Code] [ Data ] - 1b Nb + Layout:: + [ Function Code] [ Data ] + 1b Nb """ - def specific_decode(self, data: bytes, data_len: int) -> tuple[int, bytes]: + def decode(self, data: bytes) -> tuple[int, int, int, bytes]: """Decode ADU.""" - return data_len, data + return len(data), 0, 0, data def encode(self, pdu: bytes, _device_id: int, _tid: int) -> bytes: """Encode ADU.""" diff --git a/pymodbus/pdu/__init__.py b/pymodbus/pdu/__init__.py index da3d86f8f..60ba42e58 100644 --- a/pymodbus/pdu/__init__.py +++ b/pymodbus/pdu/__init__.py @@ -1,18 +1,11 @@ """Framer.""" __all__ = [ + "DecodePDU", + "ExceptionResponse", "ExceptionResponse", - "IllegalFunctionRequest", "ModbusExceptions", "ModbusPDU", - "ModbusRequest", - "ModbusResponse", ] -from pymodbus.pdu.pdu import ( - ExceptionResponse, - IllegalFunctionRequest, - ModbusExceptions, - ModbusPDU, - ModbusRequest, - ModbusResponse, -) +from pymodbus.pdu.decoders import DecodePDU +from pymodbus.pdu.pdu import ExceptionResponse, ModbusExceptions, ModbusPDU diff --git a/pymodbus/pdu/bit_read_message.py b/pymodbus/pdu/bit_read_message.py index a277a10eb..e37a6985e 100644 --- a/pymodbus/pdu/bit_read_message.py +++ b/pymodbus/pdu/bit_read_message.py @@ -3,12 +3,12 @@ # pylint: disable=missing-type-doc import struct -from pymodbus.pdu import ModbusExceptions as merror -from pymodbus.pdu import ModbusRequest, ModbusResponse +from pymodbus.pdu.pdu import ModbusExceptions as merror +from pymodbus.pdu.pdu import ModbusPDU from pymodbus.utilities import pack_bitstring, unpack_bitstring -class ReadBitsRequestBase(ModbusRequest): +class ReadBitsRequestBase(ModbusPDU): """Base class for Messages Requesting bit values.""" _rtu_frame_size = 8 @@ -20,7 +20,8 @@ def __init__(self, address, count, slave, transaction, skip_encode): :param count: The number of bits after "address" to read :param slave: Modbus slave slave ID """ - ModbusRequest.__init__(self, slave, transaction, skip_encode) + super().__init__() + super().setData(slave, transaction, skip_encode) self.address = address self.count = count @@ -46,20 +47,17 @@ def get_response_pdu_size(self): :return: """ count = self.count // 8 - if self.count % 8: + if self.count % 8: # pragma: no cover count += 1 return 1 + 1 + count def __str__(self): - """Return a string representation of the instance. - - :returns: A string representation of the instance - """ + """Return a string representation of the instance.""" return f"ReadBitRequest({self.address},{self.count})" -class ReadBitsResponseBase(ModbusResponse): +class ReadBitsResponseBase(ModbusPDU): """Base class for Messages responding to bit-reading values. The requested bits can be found in the .bits list. @@ -73,7 +71,8 @@ def __init__(self, values, slave, transaction, skip_encode): :param values: The requested values to be returned :param slave: Modbus slave slave ID """ - ModbusResponse.__init__(self, slave, transaction, skip_encode) + super().__init__() + super().setData(slave, transaction, skip_encode) #: A list of booleans representing bit values self.bits = values or [] @@ -119,10 +118,7 @@ def getBit(self, address): return self.bits[address] def __str__(self): - """Return a string representation of the instance. - - :returns: A string representation of the instance - """ + """Return a string representation of the instance.""" return f"{self.__class__.__name__}({len(self.bits)})" @@ -147,7 +143,7 @@ def __init__(self, address=None, count=None, slave=1, transaction=0, skip_encode """ ReadBitsRequestBase.__init__(self, address, count, slave, transaction, skip_encode) - async def execute(self, context): + async def update_datastore(self, context): # pragma: no cover """Run a read coils request against a datastore. Before running the request, we make sure that the request is in @@ -215,7 +211,7 @@ def __init__(self, address=None, count=None, slave=1, transaction=0, skip_encode """ ReadBitsRequestBase.__init__(self, address, count, slave, transaction, skip_encode) - async def execute(self, context): + async def update_datastore(self, context): # pragma: no cover """Run a read discrete input request against a datastore. Before running the request, we make sure that the request is in diff --git a/pymodbus/pdu/bit_write_message.py b/pymodbus/pdu/bit_write_message.py index 627e7abe5..ad995f9a7 100644 --- a/pymodbus/pdu/bit_write_message.py +++ b/pymodbus/pdu/bit_write_message.py @@ -8,8 +8,8 @@ import struct from pymodbus.constants import ModbusStatus -from pymodbus.pdu import ModbusExceptions as merror -from pymodbus.pdu import ModbusRequest, ModbusResponse +from pymodbus.pdu.pdu import ModbusExceptions as merror +from pymodbus.pdu.pdu import ModbusPDU from pymodbus.utilities import pack_bitstring, unpack_bitstring @@ -22,7 +22,7 @@ _turn_coil_off = struct.pack(">H", ModbusStatus.OFF) -class WriteSingleCoilRequest(ModbusRequest): +class WriteSingleCoilRequest(ModbusPDU): """This function code is used to write a single output to either ON or OFF in a remote device. The requested ON/OFF state is specified by a constant in the request @@ -49,7 +49,8 @@ def __init__(self, address=None, value=None, slave=None, transaction=0, skip_enc :param address: The variable address to write :param value: The value to write at address """ - ModbusRequest.__init__(self, slave, transaction, skip_encode) + super().__init__() + super().setData(slave, transaction, skip_encode) self.address = address self.value = bool(value) @@ -59,10 +60,10 @@ def encode(self): :returns: The byte encoded message """ result = struct.pack(">H", self.address) - if self.value: + if self.value: # pragma: no cover result += _turn_coil_on else: - result += _turn_coil_off + result += _turn_coil_off # pragma: no cover return result def decode(self, data): @@ -73,7 +74,7 @@ def decode(self, data): self.address, value = struct.unpack(">HH", data) self.value = value == ModbusStatus.ON - async def execute(self, context): + async def update_datastore(self, context): # pragma: no cover """Run a write coil request against a datastore. :param context: The datastore to request from @@ -97,14 +98,11 @@ def get_response_pdu_size(self): return 1 + 2 + 2 def __str__(self): - """Return a string representation of the instance. - - :return: A string representation of the instance - """ + """Return a string representation of the instance.""" return f"WriteCoilRequest({self.address}, {self.value}) => " -class WriteSingleCoilResponse(ModbusResponse): +class WriteSingleCoilResponse(ModbusPDU): """The normal response is an echo of the request. Returned after the coil state has been written. @@ -119,7 +117,8 @@ def __init__(self, address=None, value=None, slave=1, transaction=0, skip_encode :param address: The variable address written to :param value: The value written at address """ - ModbusResponse.__init__(self, slave, transaction, skip_encode) + super().__init__() + super().setData(slave, transaction, skip_encode) self.address = address self.value = value @@ -129,10 +128,10 @@ def encode(self): :return: The byte encoded message """ result = struct.pack(">H", self.address) - if self.value: + if self.value: # pragma: no cover result += _turn_coil_on else: - result += _turn_coil_off + result += _turn_coil_off # pragma: no cover return result def decode(self, data): @@ -151,7 +150,7 @@ def __str__(self): return f"WriteCoilResponse({self.address}) => {self.value}" -class WriteMultipleCoilsRequest(ModbusRequest): +class WriteMultipleCoilsRequest(ModbusPDU): """This function code is used to forcea sequence of coils. To either ON or OFF in a remote device. The Request PDU specifies the coil @@ -167,15 +166,16 @@ class WriteMultipleCoilsRequest(ModbusRequest): function_code_name = "write_coils" _rtu_byte_count_pos = 6 - def __init__(self, address=None, values=None, slave=None, transaction=0, skip_encode=0): + def __init__(self, address=0, values=None, slave=None, transaction=0, skip_encode=0): """Initialize a new instance. :param address: The starting request address :param values: The values to write """ - ModbusRequest.__init__(self, slave, transaction, skip_encode) + super().__init__() + super().setData(slave, transaction, skip_encode) self.address = address - if values is None: + if values is None: # pragma: no cover values = [] elif not hasattr(values, "__iter__"): values = [values] @@ -202,7 +202,7 @@ def decode(self, data): values = unpack_bitstring(data[5:]) self.values = values[:count] - async def execute(self, context): + async def update_datastore(self, context): # pragma: no cover """Run a write coils request against a datastore. :param context: The datastore to request from @@ -222,15 +222,8 @@ async def execute(self, context): return WriteMultipleCoilsResponse(self.address, count) def __str__(self): - """Return a string representation of the instance. - - :returns: A string representation of the instance - """ - params = (self.address, len(self.values)) - return ( - "WriteNCoilRequest (%d) => %d " # pylint: disable=consider-using-f-string - % params - ) + """Return a string representation of the instance.""" + return f"WriteNCoilRequest ({self.address}) => {len(self.values)}" def get_response_pdu_size(self): """Get response pdu size. @@ -241,7 +234,7 @@ def get_response_pdu_size(self): return 1 + 2 + 2 -class WriteMultipleCoilsResponse(ModbusResponse): +class WriteMultipleCoilsResponse(ModbusPDU): """The normal response returns the function code. Starting address, and quantity of coils forced. @@ -256,7 +249,8 @@ def __init__(self, address=None, count=None, slave=1, transaction=0, skip_encode :param address: The starting variable address written to :param count: The number of values written """ - ModbusResponse.__init__(self, slave, transaction, skip_encode) + super().__init__() + super().setData(slave, transaction, skip_encode) self.address = address self.count = count @@ -275,8 +269,5 @@ def decode(self, data): self.address, self.count = struct.unpack(">HH", data) def __str__(self): - """Return a string representation of the instance. - - :returns: A string representation of the instance - """ + """Return a string representation of the instance.""" return f"WriteNCoilResponse({self.address}, {self.count})" diff --git a/pymodbus/pdu/decoders.py b/pymodbus/pdu/decoders.py new file mode 100644 index 000000000..6df645b8e --- /dev/null +++ b/pymodbus/pdu/decoders.py @@ -0,0 +1,113 @@ +"""Modbus Request/Response Decoders.""" +from __future__ import annotations + +import pymodbus.pdu.bit_read_message as bit_r_msg +import pymodbus.pdu.bit_write_message as bit_w_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.pdu as base +import pymodbus.pdu.register_read_message as reg_r_msg +import pymodbus.pdu.register_write_message as reg_w_msg +from pymodbus.exceptions import MessageRegisterException, ModbusException +from pymodbus.logging import Log + + +class DecodePDU: + """Decode pdu requests/responses (server/client).""" + + _pdu_class_table: set[tuple[type[base.ModbusPDU], type[base.ModbusPDU]]] = { + (reg_r_msg.ReadHoldingRegistersRequest, reg_r_msg.ReadHoldingRegistersResponse), + (bit_r_msg.ReadDiscreteInputsRequest, bit_r_msg.ReadDiscreteInputsResponse), + (reg_r_msg.ReadInputRegistersRequest, reg_r_msg.ReadInputRegistersResponse), + (bit_r_msg.ReadCoilsRequest, bit_r_msg.ReadCoilsResponse), + (bit_w_msg.WriteMultipleCoilsRequest, bit_w_msg.WriteMultipleCoilsResponse), + (reg_w_msg.WriteMultipleRegistersRequest, reg_w_msg.WriteMultipleRegistersResponse), + (reg_w_msg.WriteSingleRegisterRequest, reg_w_msg.WriteSingleRegisterResponse), + (bit_w_msg.WriteSingleCoilRequest, bit_w_msg.WriteSingleCoilResponse), + (reg_r_msg.ReadWriteMultipleRegistersRequest, reg_r_msg.ReadWriteMultipleRegistersResponse), + (diag_msg.DiagnosticStatusRequest, diag_msg.DiagnosticStatusResponse), + (o_msg.ReadExceptionStatusRequest, o_msg.ReadExceptionStatusResponse), + (o_msg.GetCommEventCounterRequest, o_msg.GetCommEventCounterResponse), + (o_msg.GetCommEventLogRequest, o_msg.GetCommEventLogResponse), + (o_msg.ReportSlaveIdRequest, o_msg.ReportSlaveIdResponse), + (file_msg.ReadFileRecordRequest, file_msg.ReadFileRecordResponse), + (file_msg.WriteFileRecordRequest, file_msg.WriteFileRecordResponse), + (reg_w_msg.MaskWriteRegisterRequest, reg_w_msg.MaskWriteRegisterResponse), + (file_msg.ReadFifoQueueRequest, file_msg.ReadFifoQueueResponse), + (mei_msg.ReadDeviceInformationRequest, mei_msg.ReadDeviceInformationResponse), + } + + _pdu_sub_class_table: set[tuple[type[base.ModbusPDU], type[base.ModbusPDU]]] = { + (diag_msg.ReturnQueryDataRequest, diag_msg.ReturnQueryDataResponse), + (diag_msg.RestartCommunicationsOptionRequest, diag_msg.RestartCommunicationsOptionResponse), + (diag_msg.ReturnDiagnosticRegisterRequest, diag_msg.ReturnDiagnosticRegisterResponse), + (diag_msg.ChangeAsciiInputDelimiterRequest, diag_msg.ChangeAsciiInputDelimiterResponse), + (diag_msg.ForceListenOnlyModeRequest, diag_msg.ForceListenOnlyModeResponse), + (diag_msg.ClearCountersRequest, diag_msg.ClearCountersResponse), + (diag_msg.ReturnBusMessageCountRequest, diag_msg.ReturnBusMessageCountResponse), + (diag_msg.ReturnBusCommunicationErrorCountRequest, diag_msg.ReturnBusCommunicationErrorCountResponse), + (diag_msg.ReturnBusExceptionErrorCountRequest, diag_msg.ReturnBusExceptionErrorCountResponse), + (diag_msg.ReturnSlaveMessageCountRequest, diag_msg.ReturnSlaveMessageCountResponse), + (diag_msg.ReturnSlaveNoResponseCountRequest, diag_msg.ReturnSlaveNoResponseCountResponse), + (diag_msg.ReturnSlaveNAKCountRequest, diag_msg.ReturnSlaveNAKCountResponse), + (diag_msg.ReturnSlaveBusyCountRequest, diag_msg.ReturnSlaveBusyCountResponse), + (diag_msg.ReturnSlaveBusCharacterOverrunCountRequest, diag_msg.ReturnSlaveBusCharacterOverrunCountResponse), + (diag_msg.ReturnIopOverrunCountRequest, diag_msg.ReturnIopOverrunCountResponse), + (diag_msg.ClearOverrunCountRequest, diag_msg.ClearOverrunCountResponse), + (diag_msg.GetClearModbusPlusRequest, diag_msg.GetClearModbusPlusResponse), + (mei_msg.ReadDeviceInformationRequest, mei_msg.ReadDeviceInformationResponse), + } + + def __init__(self, is_server: bool) -> None: + """Initialize function_tables.""" + inx = 0 if is_server else 1 + self.lookup: dict[int, type[base.ModbusPDU]] = {cl[inx].function_code: cl[inx] for cl in self._pdu_class_table} + self.sub_lookup: dict[int, dict[int, type[base.ModbusPDU]]] = {f: {} for f in self.lookup} + for f in self._pdu_sub_class_table: + self.sub_lookup[f[inx].function_code][f[inx].sub_function_code] = f[inx] + + def lookupPduClass(self, function_code: int) -> type[base.ModbusPDU]: + """Use `function_code` to determine the class of the PDU.""" + return self.lookup.get(function_code, base.ExceptionResponse) + + def register(self, custom_class: type[base.ModbusPDU]) -> None: + """Register a function and sub function class with the decoder.""" + if not issubclass(custom_class, base.ModbusPDU): + raise MessageRegisterException( + f'"{custom_class.__class__.__name__}" is Not a valid Modbus Message' + ". Class needs to be derived from " + "`pymodbus.pdu.ModbusPDU` " + ) + self.lookup[custom_class.function_code] = custom_class + if custom_class.sub_function_code >= 0: + if custom_class.function_code not in self.sub_lookup: + self.sub_lookup[custom_class.function_code] = {} + self.sub_lookup[custom_class.function_code][ + custom_class.sub_function_code + ] = custom_class + + def decode(self, frame: bytes) -> base.ModbusPDU | None: + """Decode a frame.""" + try: + if (function_code := int(frame[0])) > 0x80: + pdu_exp = base.ExceptionResponse(function_code & 0x7F) + pdu_exp.decode(frame[1:]) + return pdu_exp + if not (pdu_type := self.lookup.get(function_code, None)): + Log.debug("decode PDU failed for function code {}", function_code) + raise ModbusException(f"Unknown response {function_code}") + pdu = pdu_type() + pdu.setData(0, 0, False) + Log.debug("decode PDU for {}", function_code) + pdu.decode(frame[1:]) + + if pdu.sub_function_code >= 0: + lookup = self.sub_lookup.get(pdu.function_code, {}) + if subtype := lookup.get(pdu.sub_function_code, None): + pdu.__class__ = subtype + return pdu + except (ModbusException, ValueError, IndexError) as exc: + Log.warning("Unable to decode frame {}", exc) + return None diff --git a/pymodbus/pdu/diag_message.py b/pymodbus/pdu/diag_message.py index 22e735e32..3fa580334 100644 --- a/pymodbus/pdu/diag_message.py +++ b/pymodbus/pdu/diag_message.py @@ -11,7 +11,7 @@ from pymodbus.constants import ModbusPlusOperation, ModbusStatus from pymodbus.device import ModbusControlBlock from pymodbus.exceptions import ModbusException, NotImplementedException -from pymodbus.pdu import ModbusRequest, ModbusResponse +from pymodbus.pdu.pdu import ModbusPDU from pymodbus.utilities import pack_bitstring @@ -24,16 +24,18 @@ # ---------------------------------------------------------------------------# # TODO Make sure all the data is decoded from the response # pylint: disable=fixme # ---------------------------------------------------------------------------# -class DiagnosticStatusRequest(ModbusRequest): +class DiagnosticStatusRequest(ModbusPDU): """This is a base class for all of the diagnostic request functions.""" function_code = 0x08 function_code_name = "diagnostic_status" + sub_function_code = 9999 _rtu_frame_size = 8 def __init__(self, slave=1, transaction=0, skip_encode=False): """Initialize a diagnostic request.""" - ModbusRequest.__init__(self, slave, transaction, skip_encode) + super().__init__() + super().setData(slave, transaction, skip_encode) self.message = None def encode(self): @@ -45,14 +47,14 @@ def encode(self): """ packet = struct.pack(">H", self.sub_function_code) if self.message is not None: - if isinstance(self.message, str): + if isinstance(self.message, str): # pragma: no cover packet += self.message.encode() elif isinstance(self.message, bytes): packet += self.message elif isinstance(self.message, (list, tuple)): for piece in self.message: packet += struct.pack(">H", piece) - elif isinstance(self.message, int): + elif isinstance(self.message, int): # pragma: no cover packet += struct.pack(">H", self.message) return packet @@ -61,13 +63,11 @@ def decode(self, data): :param data: The data to decode into the function code """ - ( - self.sub_function_code, # pylint: disable=attribute-defined-outside-init - ) = struct.unpack(">H", data[:2]) - if self.sub_function_code == ReturnQueryDataRequest.sub_function_code: + (self.sub_function_code, ) = struct.unpack(">H", data[:2]) + if self.sub_function_code == ReturnQueryDataRequest.sub_function_code: # pragma: no cover self.message = data[2:] else: - (self.message,) = struct.unpack(">H", data[2:]) + (self.message,) = struct.unpack(">H", data[2:]) # pragma: no cover def get_response_pdu_size(self): """Get response pdu size. @@ -80,22 +80,24 @@ def get_response_pdu_size(self): return 1 + 2 + 2 * len(self.message) -class DiagnosticStatusResponse(ModbusResponse): +class DiagnosticStatusResponse(ModbusPDU): """Diagnostic status. This is a base class for all of the diagnostic response functions It works by performing all of the encoding and decoding of variable data and lets the higher classes define what extra data to append - and how to execute a request + and how to update_datastore a request """ function_code = 0x08 + sub_function_code = 9999 _rtu_frame_size = 8 def __init__(self, slave=1, transaction=0, skip_encode=False): """Initialize a diagnostic response.""" - ModbusResponse.__init__(self, slave, transaction, skip_encode) + super().__init__() + super().setData(slave, transaction, skip_encode) self.message = None def encode(self): @@ -107,14 +109,14 @@ def encode(self): """ packet = struct.pack(">H", self.sub_function_code) if self.message is not None: - if isinstance(self.message, str): + if isinstance(self.message, str): # pragma: no cover packet += self.message.encode() elif isinstance(self.message, bytes): packet += self.message elif isinstance(self.message, (list, tuple)): for piece in self.message: packet += struct.pack(">H", piece) - elif isinstance(self.message, int): + elif isinstance(self.message, int): # pragma: no cover packet += struct.pack(">H", self.message) return packet @@ -123,15 +125,13 @@ def decode(self, data): :param data: The data to decode into the function code """ - ( - self.sub_function_code, # pylint: disable=attribute-defined-outside-init - ) = struct.unpack(">H", data[:2]) + (self.sub_function_code, ) = struct.unpack(">H", data[:2]) data = data[2:] if self.sub_function_code == ReturnQueryDataRequest.sub_function_code: self.message = data else: word_len = len(data) // 2 - if len(data) % 2: + if len(data) % 2: # pragma: no cover word_len += 1 data += b"0" data = struct.unpack(">" + "H" * word_len, data) @@ -147,7 +147,7 @@ class DiagnosticStatusSimpleRequest(DiagnosticStatusRequest): 2 bytes of data. If a function inherits this, they only need to implement - the execute method + the update_datastore method """ def __init__(self, data=0x0000, slave=1, transaction=0, skip_encode=False): @@ -161,9 +161,9 @@ def __init__(self, data=0x0000, slave=1, transaction=0, skip_encode=False): DiagnosticStatusRequest.__init__(self, slave=slave, transaction=transaction, skip_encode=skip_encode) self.message = data - async def execute(self, *args): + async def update_datastore(self, *args): # pragma: no cover """Raise if not implemented.""" - raise NotImplementedException("Diagnostic Message Has No Execute Method") + raise NotImplementedException("Diagnostic Message Has No update_datastore Method") class DiagnosticStatusSimpleResponse(DiagnosticStatusResponse): @@ -203,12 +203,12 @@ def __init__(self, message=b"\x00\x00", slave=1, transaction=0, skip_encode=Fals :param message: The message to send to loopback """ DiagnosticStatusRequest.__init__(self, slave=slave, transaction=transaction, skip_encode=skip_encode) - if not isinstance(message, bytes): + if not isinstance(message, bytes): # pragma: no cover raise ModbusException(f"message({type(message)}) must be bytes") self.message = message - async def execute(self, *_args): - """Execute the loopback request (builds the response). + async def update_datastore(self, *_args): # pragma: no cover + """update_datastore the loopback request (builds the response). :returns: The populated loopback response message """ @@ -231,7 +231,7 @@ def __init__(self, message=b"\x00\x00", slave=1, transaction=0, skip_encode=Fals :param message: The message to loopback """ DiagnosticStatusResponse.__init__(self, slave=slave, transaction=transaction, skip_encode=skip_encode) - if not isinstance(message, bytes): + if not isinstance(message, bytes): # pragma: no cover raise ModbusException(f"message({type(message)}) must be bytes") self.message = message @@ -247,7 +247,7 @@ class RestartCommunicationsOptionRequest(DiagnosticStatusRequest): currently in Listen Only Mode, no response is returned. This function is 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. This - occurs before the restart is executed. + occurs before the restart is update_datastored. """ sub_function_code = 0x0001 @@ -258,12 +258,12 @@ def __init__(self, toggle=False, slave=1, transaction=0, skip_encode=False): :param toggle: Set to True to toggle, False otherwise """ DiagnosticStatusRequest.__init__(self, slave=slave, transaction=transaction, skip_encode=skip_encode) - if toggle: + if toggle: # pragma: no cover self.message = [ModbusStatus.ON] else: - self.message = [ModbusStatus.OFF] + self.message = [ModbusStatus.OFF] # pragma: no cover - async def execute(self, *_args): + async def update_datastore(self, *_args): # pragma: no cover """Clear event log and restart. :returns: The initialized response message @@ -280,7 +280,7 @@ class RestartCommunicationsOptionResponse(DiagnosticStatusResponse): currently in Listen Only Mode, no response is returned. This function is 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. This - occurs before the restart is executed. + occurs before the restart is update_datastored. """ sub_function_code = 0x0001 @@ -291,10 +291,10 @@ def __init__(self, toggle=False, slave=1, transaction=0, skip_encode=False): :param toggle: Set to True if we toggled, False otherwise """ DiagnosticStatusResponse.__init__(self, slave=slave, transaction=transaction, skip_encode=skip_encode) - if toggle: + if toggle: # pragma: no cover self.message = [ModbusStatus.ON] else: - self.message = [ModbusStatus.OFF] + self.message = [ModbusStatus.OFF] # pragma: no cover # ---------------------------------------------------------------------------# @@ -305,8 +305,8 @@ class ReturnDiagnosticRegisterRequest(DiagnosticStatusSimpleRequest): sub_function_code = 0x0002 - async def execute(self, *args): - """Execute the diagnostic request on the given device. + async def update_datastore(self, *args): # pragma: no cover + """update_datastore the diagnostic request on the given device. :returns: The initialized response message """ @@ -339,8 +339,8 @@ class ChangeAsciiInputDelimiterRequest(DiagnosticStatusSimpleRequest): sub_function_code = 0x0003 - async def execute(self, *args): - """Execute the diagnostic request on the given device. + async def update_datastore(self, *args): # pragma: no cover + """update_datastore the diagnostic request on the given device. :returns: The initialized response message """ @@ -374,8 +374,8 @@ class ForceListenOnlyModeRequest(DiagnosticStatusSimpleRequest): sub_function_code = 0x0004 - async def execute(self, *args): - """Execute the diagnostic request on the given device. + async def update_datastore(self, *args): # pragma: no cover + """update_datastore the diagnostic request on the given device. :returns: The initialized response message """ @@ -394,7 +394,6 @@ class ForceListenOnlyModeResponse(DiagnosticStatusResponse): """ sub_function_code = 0x0004 - should_respond = False def __init__(self, slave=1, transaction=0, skip_encode=False): """Initialize to block a return response.""" @@ -413,8 +412,8 @@ class ClearCountersRequest(DiagnosticStatusSimpleRequest): sub_function_code = 0x000A - async def execute(self, *args): - """Execute the diagnostic request on the given device. + async def update_datastore(self, *args): # pragma: no cover + """update_datastore the diagnostic request on the given device. :returns: The initialized response message """ @@ -444,8 +443,8 @@ class ReturnBusMessageCountRequest(DiagnosticStatusSimpleRequest): sub_function_code = 0x000B - async def execute(self, *args): - """Execute the diagnostic request on the given device. + async def update_datastore(self, *args): # pragma: no cover + """update_datastore the diagnostic request on the given device. :returns: The initialized response message """ @@ -477,8 +476,8 @@ class ReturnBusCommunicationErrorCountRequest(DiagnosticStatusSimpleRequest): sub_function_code = 0x000C - async def execute(self, *args): - """Execute the diagnostic request on the given device. + async def update_datastore(self, *args): # pragma: no cover + """update_datastore the diagnostic request on the given device. :returns: The initialized response message """ @@ -510,8 +509,8 @@ class ReturnBusExceptionErrorCountRequest(DiagnosticStatusSimpleRequest): sub_function_code = 0x000D - async def execute(self, *args): - """Execute the diagnostic request on the given device. + async def update_datastore(self, *args): # pragma: no cover + """update_datastore the diagnostic request on the given device. :returns: The initialized response message """ @@ -537,14 +536,14 @@ class ReturnSlaveMessageCountRequest(DiagnosticStatusSimpleRequest): """Return slave message count. The response data field returns the quantity of messages addressed to the - remote device, or broadcast, that the remote device has processed since + remote device, that the remote device has processed since its last restart, clear counters operation, or power-up """ sub_function_code = 0x000E - async def execute(self, *args): - """Execute the diagnostic request on the given device. + async def update_datastore(self, *args): # pragma: no cover + """update_datastore the diagnostic request on the given device. :returns: The initialized response message """ @@ -556,7 +555,7 @@ class ReturnSlaveMessageCountResponse(DiagnosticStatusSimpleResponse): """Return slave message count. The response data field returns the quantity of messages addressed to the - remote device, or broadcast, that the remote device has processed since + remote device, that the remote device has processed since its last restart, clear counters operation, or power-up """ @@ -570,14 +569,14 @@ class ReturnSlaveNoResponseCountRequest(DiagnosticStatusSimpleRequest): """Return slave no response. The response data field returns the quantity of messages addressed to the - remote device, or broadcast, that the remote device has processed since + remote device, that the remote device has processed since its last restart, clear counters operation, or power-up """ sub_function_code = 0x000F - async def execute(self, *args): - """Execute the diagnostic request on the given device. + async def update_datastore(self, *args): # pragma: no cover + """update_datastore the diagnostic request on the given device. :returns: The initialized response message """ @@ -589,7 +588,7 @@ class ReturnSlaveNoResponseCountResponse(DiagnosticStatusSimpleResponse): """Return slave no response. The response data field returns the quantity of messages addressed to the - remote device, or broadcast, that the remote device has processed since + remote device, that the remote device has processed since its last restart, clear counters operation, or power-up """ @@ -610,8 +609,8 @@ class ReturnSlaveNAKCountRequest(DiagnosticStatusSimpleRequest): sub_function_code = 0x0010 - async def execute(self, *args): - """Execute the diagnostic request on the given device. + async def update_datastore(self, *args): # pragma: no cover + """update_datastore the diagnostic request on the given device. :returns: The initialized response message """ @@ -644,8 +643,8 @@ class ReturnSlaveBusyCountRequest(DiagnosticStatusSimpleRequest): sub_function_code = 0x0011 - async def execute(self, *args): - """Execute the diagnostic request on the given device. + async def update_datastore(self, *args): # pragma: no cover + """update_datastore the diagnostic request on the given device. :returns: The initialized response message """ @@ -679,8 +678,8 @@ class ReturnSlaveBusCharacterOverrunCountRequest(DiagnosticStatusSimpleRequest): sub_function_code = 0x0012 - async def execute(self, *args): - """Execute the diagnostic request on the given device. + async def update_datastore(self, *args): # pragma: no cover + """update_datastore the diagnostic request on the given device. :returns: The initialized response message """ @@ -712,8 +711,8 @@ class ReturnIopOverrunCountRequest(DiagnosticStatusSimpleRequest): sub_function_code = 0x0013 - async def execute(self, *args): - """Execute the diagnostic request on the given device. + async def update_datastore(self, *args): # pragma: no cover + """update_datastore the diagnostic request on the given device. :returns: The initialized response message """ @@ -745,8 +744,8 @@ class ClearOverrunCountRequest(DiagnosticStatusSimpleRequest): sub_function_code = 0x0014 - async def execute(self, *args): - """Execute the diagnostic request on the given device. + async def update_datastore(self, *args): # pragma: no cover + """update_datastore the diagnostic request on the given device. :returns: The initialized response message """ @@ -791,14 +790,14 @@ def get_response_pdu_size(self): Func_code (1 byte) + Sub function code (2 byte) + Operation (2 byte) + Data (108 bytes) :return: """ - if self.message == ModbusPlusOperation.GET_STATISTICS: + if self.message == ModbusPlusOperation.GET_STATISTICS: # pragma: no cover data = 2 + 108 # byte count(2) + data (54*2) else: data = 0 return 1 + 2 + 2 + 2 + data - async def execute(self, *args): - """Execute the diagnostic request on the given device. + async def update_datastore(self, *args): # pragma: no cover + """update_datastore the diagnostic request on the given device. :returns: The initialized response message """ diff --git a/pymodbus/pdu/file_message.py b/pymodbus/pdu/file_message.py index 3c3a01b31..163375792 100644 --- a/pymodbus/pdu/file_message.py +++ b/pymodbus/pdu/file_message.py @@ -7,8 +7,8 @@ # pylint: disable=missing-type-doc import struct -from pymodbus.pdu import ModbusExceptions as merror -from pymodbus.pdu import ModbusRequest, ModbusResponse +from pymodbus.pdu.pdu import ModbusExceptions as merror +from pymodbus.pdu.pdu import ModbusPDU # ---------------------------------------------------------------------------# @@ -17,7 +17,7 @@ class FileRecord: # pylint: disable=eq-without-hash """Represents a file record and its relevant data.""" - def __init__(self, reference_type=0x06, file_number=0x00, record_number=0x00, record_data="", record_length=None, response_length=None): + def __init__(self, reference_type=0x06, file_number=0x00, record_number=0x00, record_data=b'', record_length=None, response_length=None): """Initialize a new instance. :params reference_type: must be 0x06 @@ -37,7 +37,7 @@ def __init__(self, reference_type=0x06, file_number=0x00, record_number=0x00, re def __eq__(self, relf): """Compare the left object to the right.""" - return ( + return ( # pragma: no cover self.reference_type == relf.reference_type and self.file_number == relf.file_number and self.record_number == relf.record_number @@ -47,9 +47,9 @@ def __eq__(self, relf): def __ne__(self, relf): """Compare the left object to the right.""" - return not self.__eq__(relf) + return not self.__eq__(relf) # pragma: no cover - def __repr__(self): + def __repr__(self): # pragma: no cover """Give a representation of the file record.""" params = (self.file_number, self.record_number, self.record_length) return ( @@ -61,7 +61,7 @@ def __repr__(self): # ---------------------------------------------------------------------------# # File Requests/Responses # ---------------------------------------------------------------------------# -class ReadFileRecordRequest(ModbusRequest): +class ReadFileRecordRequest(ModbusPDU): """Read file record request. This function code is used to perform a file record read. All request @@ -94,7 +94,8 @@ def __init__(self, records=None, slave=1, transaction=0, skip_encode=False): :param records: The file record requests to be read """ - ModbusRequest.__init__(self, slave, transaction, skip_encode) + super().__init__() + super().setData(slave, transaction, skip_encode) self.records = records or [] def encode(self): @@ -127,10 +128,10 @@ def decode(self, data): record_number=decoded[2], record_length=decoded[3], ) - if decoded[0] == 0x06: + if decoded[0] == 0x06: # pragma: no cover self.records.append(record) - def execute(self, _context): + def update_datastore(self, _context): # pragma: no cover """Run a read exception status request against the store. :returns: The populated response @@ -142,7 +143,7 @@ def execute(self, _context): return ReadFileRecordResponse(files) -class ReadFileRecordResponse(ModbusResponse): +class ReadFileRecordResponse(ModbusPDU): """Read file record response. The normal response is a series of "sub-responses," one for each @@ -159,7 +160,8 @@ def __init__(self, records=None, slave=1, transaction=0, skip_encode=False): :param records: The requested file records """ - ModbusResponse.__init__(self, slave, transaction, skip_encode) + super().__init__() + super().setData(slave, transaction, skip_encode) self.records = records or [] def encode(self): @@ -193,11 +195,11 @@ def decode(self, data): record_data=data[count : count + record_length], ) count += record_length - if reference_type == 0x06: + if reference_type == 0x06: # pragma: no cover self.records.append(record) -class WriteFileRecordRequest(ModbusRequest): +class WriteFileRecordRequest(ModbusPDU): """Write file record request. This function code is used to perform a file record write. All @@ -215,7 +217,8 @@ def __init__(self, records=None, slave=1, transaction=0, skip_encode=False): :param records: The file record requests to be read """ - ModbusRequest.__init__(self, slave, transaction, skip_encode) + super().__init__() + super().setData(slave, transaction, skip_encode) self.records = records or [] def encode(self): @@ -254,10 +257,10 @@ def decode(self, data): record_number=decoded[2], record_data=data[count - response_length : count], ) - if decoded[0] == 0x06: + if decoded[0] == 0x06: # pragma: no cover self.records.append(record) - def execute(self, _context): + def update_datastore(self, _context): # pragma: no cover """Run the write file record request against the context. :returns: The populated response @@ -268,7 +271,7 @@ def execute(self, _context): return WriteFileRecordResponse(self.records) -class WriteFileRecordResponse(ModbusResponse): +class WriteFileRecordResponse(ModbusPDU): """The normal response is an echo of the request.""" function_code = 0x15 @@ -279,7 +282,8 @@ def __init__(self, records=None, slave=1, transaction=0, skip_encode=False): :param records: The file record requests to be read """ - ModbusResponse.__init__(self, slave, transaction, skip_encode) + super().__init__() + super().setData(slave, transaction, skip_encode) self.records = records or [] def encode(self): @@ -317,11 +321,11 @@ def decode(self, data): record_number=decoded[2], record_data=data[count - response_length : count], ) - if decoded[0] == 0x06: + if decoded[0] == 0x06: # pragma: no cover self.records.append(record) -class ReadFifoQueueRequest(ModbusRequest): +class ReadFifoQueueRequest(ModbusPDU): """Read fifo queue request. This function code allows to read the contents of a First-In-First-Out @@ -344,7 +348,8 @@ def __init__(self, address=0x0000, slave=1, transaction=0, skip_encode=False): :param address: The fifo pointer address (0x0000 to 0xffff) """ - ModbusRequest.__init__(self, slave, transaction, skip_encode) + super().__init__() + super().setData(slave, transaction, skip_encode) self.address = address self.values = [] # this should be added to the context @@ -362,7 +367,7 @@ def decode(self, data): """ self.address = struct.unpack(">H", data)[0] - def execute(self, _context): + def update_datastore(self, _context): # pragma: no cover """Run a read exception status request against the store. :returns: The populated response @@ -375,7 +380,7 @@ def execute(self, _context): return ReadFifoQueueResponse(self.values) -class ReadFifoQueueResponse(ModbusResponse): +class ReadFifoQueueResponse(ModbusPDU): """Read Fifo queue response. In a normal response, the byte count shows the quantity of bytes to @@ -390,7 +395,7 @@ class ReadFifoQueueResponse(ModbusResponse): function_code = 0x18 @classmethod - def calculateRtuFrameSize(cls, buffer): + def calculateRtuFrameSize(cls, buffer): # pragma: no cover """Calculate the size of the message. :param buffer: A buffer containing the data that have been received. @@ -405,7 +410,8 @@ def __init__(self, values=None, slave=1, transaction=0, skip_encode=False): :param values: The list of values of the fifo to return """ - ModbusResponse.__init__(self, slave, transaction, skip_encode) + super().__init__() + super().setData(slave, transaction, skip_encode) self.values = values or [] def encode(self): @@ -426,6 +432,6 @@ def decode(self, data): """ self.values = [] _, count = struct.unpack(">HH", data[0:4]) - for index in range(0, count - 4): + for index in range(0, count - 4): # pragma: no cover idx = 4 + index * 2 self.values.append(struct.unpack(">H", data[idx : idx + 2])[0]) diff --git a/pymodbus/pdu/mei_message.py b/pymodbus/pdu/mei_message.py index 0fc327520..0085f23dc 100644 --- a/pymodbus/pdu/mei_message.py +++ b/pymodbus/pdu/mei_message.py @@ -6,8 +6,8 @@ from pymodbus.constants import DeviceInformation, MoreData from pymodbus.device import DeviceInformationFactory, ModbusControlBlock -from pymodbus.pdu import ModbusExceptions as merror -from pymodbus.pdu import ModbusRequest, ModbusResponse +from pymodbus.pdu.pdu import ModbusExceptions as merror +from pymodbus.pdu.pdu import ModbusPDU _MCB = ModbusControlBlock() @@ -27,7 +27,7 @@ class _OutOfSpaceException(Exception): # # See Page 5/50 of MODBUS Application Protocol Specification V1.1b3. - def __init__(self, oid): + def __init__(self, oid): # pragma: no cover self.oid = oid super().__init__() @@ -35,7 +35,7 @@ def __init__(self, oid): # ---------------------------------------------------------------------------# # Read Device Information # ---------------------------------------------------------------------------# -class ReadDeviceInformationRequest(ModbusRequest): +class ReadDeviceInformationRequest(ModbusPDU): """Read device information. This function code allows reading the identification and additional @@ -58,7 +58,8 @@ def __init__(self, read_code=None, object_id=0x00, slave=1, transaction=0, skip_ :param read_code: The device information read code :param object_id: The object to read from """ - ModbusRequest.__init__(self, slave, transaction, skip_encode) + super().__init__() + super().setData(slave, transaction, skip_encode) self.read_code = read_code or DeviceInformation.BASIC self.object_id = object_id @@ -80,7 +81,7 @@ def decode(self, data): params = struct.unpack(">BBB", data) self.sub_function_code, self.read_code, self.object_id = params - async def execute(self, _context): + async def update_datastore(self, _context): # pragma: no cover """Run a read exception status request against the store. :returns: The populated response @@ -105,14 +106,14 @@ def __str__(self): ) -class ReadDeviceInformationResponse(ModbusResponse): +class ReadDeviceInformationResponse(ModbusPDU): """Read device information response.""" function_code = 0x2B sub_function_code = 0x0E @classmethod - def calculateRtuFrameSize(cls, buffer): + def calculateRtuFrameSize(cls, buffer): # pragma: no cover """Calculate the size of the message. :param buffer: A buffer containing the data that have been received. @@ -136,7 +137,8 @@ def __init__(self, read_code=None, information=None, slave=1, transaction=0, ski :param read_code: The device information read code :param information: The requested information request """ - ModbusResponse.__init__(self, slave, transaction, skip_encode) + super().__init__() + super().setData(slave, transaction, skip_encode) self.read_code = read_code or DeviceInformation.BASIC self.information = information or {} self.number_of_objects = 0 @@ -145,7 +147,7 @@ def __init__(self, read_code=None, information=None, slave=1, transaction=0, ski self.more_follows = MoreData.NOTHING self.space_left = 253 - 6 - def _encode_object(self, object_id, data): + def _encode_object(self, object_id, data): # pragma: no cover """Encode object.""" self.space_left -= 2 + len(data) if self.space_left <= 0: @@ -167,14 +169,14 @@ def encode(self): ">BBB", self.sub_function_code, self.read_code, self.conformity ) objects = b"" - try: + try: # pragma: no cover for object_id, data in iter(self.information.items()): if isinstance(data, list): for item in data: objects += self._encode_object(object_id, item) else: objects += self._encode_object(object_id, data) - except _OutOfSpaceException as exc: + except _OutOfSpaceException as exc: # pragma: no cover self.next_object_id = exc.oid self.more_follows = MoreData.KEEP_READING @@ -198,19 +200,16 @@ def decode(self, data): while count < len(data): object_id, object_length = struct.unpack(">BB", data[count : count + 2]) count += object_length + 2 - if object_id not in self.information: + if object_id not in self.information: # pragma: no cover self.information[object_id] = data[count - object_length : count] - elif isinstance(self.information[object_id], list): + elif isinstance(self.information[object_id], list): # pragma: no cover self.information[object_id].append(data[count - object_length : count]) else: - self.information[object_id] = [ + self.information[object_id] = [ # pragma: no cover self.information[object_id], data[count - object_length : count], ] def __str__(self): - """Build a representation of the response. - - :returns: The string representation of the response - """ + """Build a representation of the response.""" return f"ReadDeviceInformationResponse({self.read_code})" diff --git a/pymodbus/pdu/other_message.py b/pymodbus/pdu/other_message.py index 18423c8a4..597a37bde 100644 --- a/pymodbus/pdu/other_message.py +++ b/pymodbus/pdu/other_message.py @@ -9,7 +9,7 @@ from pymodbus.constants import ModbusStatus from pymodbus.device import DeviceInformationFactory, ModbusControlBlock -from pymodbus.pdu import ModbusRequest, ModbusResponse +from pymodbus.pdu.pdu import ModbusPDU _MCB = ModbusControlBlock() @@ -18,7 +18,7 @@ # ---------------------------------------------------------------------------# # TODO Make these only work on serial # pylint: disable=fixme # ---------------------------------------------------------------------------# -class ReadExceptionStatusRequest(ModbusRequest): +class ReadExceptionStatusRequest(ModbusPDU): """This function code is used to read the contents of eight Exception Status outputs in a remote device. The function provides a simple method for @@ -32,7 +32,8 @@ class ReadExceptionStatusRequest(ModbusRequest): def __init__(self, slave=None, transaction=0, skip_encode=0): """Initialize a new instance.""" - ModbusRequest.__init__(self, slave, transaction, skip_encode) + super().__init__() + super().setData(slave, transaction, skip_encode) def encode(self): """Encode the message.""" @@ -44,7 +45,7 @@ def decode(self, data): :param data: The incoming data """ - async def execute(self, _context=None): + async def update_datastore(self, _context=None): # pragma: no cover """Run a read exception status request against the store. :returns: The populated response @@ -53,14 +54,11 @@ async def execute(self, _context=None): return ReadExceptionStatusResponse(status) def __str__(self): - """Build a representation of the request. - - :returns: The string representation of the request - """ + """Build a representation of the request.""" return f"ReadExceptionStatusRequest({self.function_code})" -class ReadExceptionStatusResponse(ModbusResponse): +class ReadExceptionStatusResponse(ModbusPDU): """The normal response contains the status of the eight Exception Status outputs. The outputs are packed into one data byte, with one bit @@ -77,7 +75,8 @@ def __init__(self, status=0x00, slave=1, transaction=0, skip_encode=False): :param status: The status response to report """ - ModbusResponse.__init__(self, slave, transaction, skip_encode) + super().__init__() + super().setData(slave, transaction, skip_encode) self.status = status if status < 256 else 255 def encode(self): @@ -95,10 +94,7 @@ def decode(self, data): self.status = int(data[0]) def __str__(self): - """Build a representation of the response. - - :returns: The string representation of the response - """ + """Build a representation of the response.""" arguments = (self.function_code, self.status) return ( "ReadExceptionStatusResponse(%d, %s)" # pylint: disable=consider-using-f-string @@ -113,7 +109,7 @@ def __str__(self): # ---------------------------------------------------------------------------# # TODO Make these only work on serial # pylint: disable=fixme # ---------------------------------------------------------------------------# -class GetCommEventCounterRequest(ModbusRequest): +class GetCommEventCounterRequest(ModbusPDU): """This function code is used to get a status word. And an event count from the remote device's communication event counter. @@ -137,7 +133,8 @@ class GetCommEventCounterRequest(ModbusRequest): def __init__(self, slave=1, transaction=0, skip_encode=False): """Initialize a new instance.""" - ModbusRequest.__init__(self, slave, transaction, skip_encode) + super().__init__() + super().setData(slave, transaction, skip_encode) def encode(self): """Encode the message.""" @@ -149,7 +146,7 @@ def decode(self, data): :param data: The incoming data """ - async def execute(self, _context=None): + async def update_datastore(self, _context=None): # pragma: no cover """Run a read exception status request against the store. :returns: The populated response @@ -158,14 +155,11 @@ async def execute(self, _context=None): return GetCommEventCounterResponse(status) def __str__(self): - """Build a representation of the request. - - :returns: The string representation of the request - """ + """Build a representation of the request.""" return f"GetCommEventCounterRequest({self.function_code})" -class GetCommEventCounterResponse(ModbusResponse): +class GetCommEventCounterResponse(ModbusPDU): """Get comm event counter response. The normal response contains a two-byte status word, and a two-byte @@ -183,7 +177,8 @@ def __init__(self, count=0x0000, slave=1, transaction=0, skip_encode=False): :param count: The current event counter value """ - ModbusResponse.__init__(self, slave, transaction, skip_encode) + super().__init__() + super().setData(slave, transaction, skip_encode) self.count = count self.status = True # this means we are ready, not waiting @@ -192,10 +187,10 @@ def encode(self): :returns: The byte encoded message """ - if self.status: + if self.status: # pragma: no cover ready = ModbusStatus.READY else: - ready = ModbusStatus.WAITING + ready = ModbusStatus.WAITING # pragma: no cover return struct.pack(">HH", ready, self.count) def decode(self, data): @@ -207,10 +202,7 @@ def decode(self, data): self.status = ready == ModbusStatus.READY def __str__(self): - """Build a representation of the response. - - :returns: The string representation of the response - """ + """Build a representation of the response.""" arguments = (self.function_code, self.count, self.status) return ( "GetCommEventCounterResponse(%d, %d, %d)" # pylint: disable=consider-using-f-string @@ -221,7 +213,7 @@ def __str__(self): # ---------------------------------------------------------------------------# # TODO Make these only work on serial # pylint: disable=fixme # ---------------------------------------------------------------------------# -class GetCommEventLogRequest(ModbusRequest): +class GetCommEventLogRequest(ModbusPDU): """This function code is used to get a status word. Event count, message count, and a field of event bytes from the remote device. @@ -248,7 +240,8 @@ class GetCommEventLogRequest(ModbusRequest): def __init__(self, slave=1, transaction=0, skip_encode=False): """Initialize a new instance.""" - ModbusRequest.__init__(self, slave, transaction, skip_encode) + super().__init__() + super().setData(slave, transaction, skip_encode) def encode(self): """Encode the message.""" @@ -260,7 +253,7 @@ def decode(self, data): :param data: The incoming data """ - async def execute(self, _context=None): + async def update_datastore(self, _context=None): # pragma: no cover """Run a read exception status request against the store. :returns: The populated response @@ -281,7 +274,7 @@ def __str__(self): return f"GetCommEventLogRequest({self.function_code})" -class GetCommEventLogResponse(ModbusResponse): +class GetCommEventLogResponse(ModbusPDU): """Get Comm event log response. The normal response contains a two-byte status word field, @@ -301,7 +294,8 @@ def __init__(self, status=True, message_count=0, event_count=0, events=None, sla :param event_count: The current event count :param events: The collection of events to send """ - ModbusResponse.__init__(self, slave, transaction, skip_encode) + super().__init__() + super().setData(slave, transaction, skip_encode) self.status = status self.message_count = message_count self.event_count = event_count @@ -312,10 +306,10 @@ def encode(self): :returns: The byte encoded message """ - if self.status: + if self.status: # pragma: no cover ready = ModbusStatus.READY else: - ready = ModbusStatus.WAITING + ready = ModbusStatus.WAITING # pragma: no cover packet = struct.pack(">B", 6 + len(self.events)) packet += struct.pack(">H", ready) packet += struct.pack(">HH", self.event_count, self.message_count) @@ -357,7 +351,7 @@ def __str__(self): # ---------------------------------------------------------------------------# # TODO Make these only work on serial # pylint: disable=fixme # ---------------------------------------------------------------------------# -class ReportSlaveIdRequest(ModbusRequest): +class ReportSlaveIdRequest(ModbusPDU): """This function code is used to read the description of the type. The current status, and other information specific to a remote device. @@ -373,7 +367,8 @@ def __init__(self, slave=1, transaction=0, skip_encode=False): :param slave: Modbus slave slave ID """ - ModbusRequest.__init__(self, slave, transaction, skip_encode) + super().__init__() + super().setData(slave, transaction, skip_encode) def encode(self): """Encode the message.""" @@ -385,7 +380,7 @@ def decode(self, data): :param data: The incoming data """ - async def execute(self, context=None): + async def update_datastore(self, context=None): # pragma: no cover """Run a report slave id request against the store. :returns: The populated response @@ -417,7 +412,7 @@ def __str__(self): return f"ReportSlaveIdRequest({self.function_code})" -class ReportSlaveIdResponse(ModbusResponse): +class ReportSlaveIdResponse(ModbusPDU): """Show response. The data contents are specific to each type of device. @@ -432,7 +427,8 @@ def __init__(self, identifier=b"\x00", status=True, slave=1, transaction=0, skip :param identifier: The identifier of the slave :param status: The status response to report """ - ModbusResponse.__init__(self, slave, transaction, skip_encode) + super().__init__() + super().setData(slave, transaction, skip_encode) self.identifier = identifier self.status = status self.byte_count = None @@ -442,10 +438,10 @@ def encode(self): :returns: The byte encoded message """ - if self.status: + if self.status: # pragma: no cover status = ModbusStatus.SLAVE_ON else: - status = ModbusStatus.SLAVE_OFF + status = ModbusStatus.SLAVE_OFF # pragma: no cover length = len(self.identifier) + 1 packet = struct.pack(">B", length) packet += self.identifier # we assume it is already encoded diff --git a/pymodbus/pdu/pdu.py b/pymodbus/pdu/pdu.py index 8bf5781a7..5a07014ed 100644 --- a/pymodbus/pdu/pdu.py +++ b/pymodbus/pdu/pdu.py @@ -1,149 +1,74 @@ """Contains base classes for modbus request/response/error packets.""" +from __future__ import annotations - -# pylint: disable=missing-type-doc +import asyncio import struct +from abc import abstractmethod from pymodbus.exceptions import NotImplementedException from pymodbus.logging import Log -from pymodbus.utilities import rtuFrameSize -# --------------------------------------------------------------------------- # -# Base PDUs -# --------------------------------------------------------------------------- # class ModbusPDU: - """Base class for all Modbus messages. - - .. attribute:: transaction_id - - This value is used to uniquely identify a request - response pair. It can be implemented as a simple counter - - .. attribute:: slave_id - - This is used to route the request to the correct child. In - the TCP modbus, it is used for routing (or not used at all. However, - for the serial versions, it is used to specify which child to perform - the requests against. The value 0x00 represents the broadcast address - (also 0xff). - - .. attribute:: check - - This is used for LRC/CRC in the serial modbus protocols - - .. attribute:: skip_encode - - This is used when the message payload has already been encoded. - Generally this will occur when the PayloadBuilder is being used - to create a complicated message. By setting this to True, the - request will pass the currently encoded message through instead - of encoding it again. - """ - - def __init__(self, slave, transaction, skip_encode): - """Initialize the base data for a modbus request. - - :param slave: Modbus slave slave ID - - """ + """Base class for all Modbus messages.""" + + function_code: int = 0 + sub_function_code: int = -1 + _rtu_frame_size: int = 0 + _rtu_byte_count_pos: int = 0 + + def __init__(self) -> None: + """Initialize the base data for a modbus request.""" + self.transaction_id: int + self.slave_id: int + self.skip_encode: bool + self.bits: list[bool] + self.registers: list[int] + self.fut: asyncio.Future + + def setData(self, slave: int, transaction: int, skip_encode: bool) -> None: + """Set data common for all PDU.""" self.transaction_id = transaction self.slave_id = slave self.skip_encode = skip_encode - self.check = 0x0000 - - def encode(self): - """Encode the message. - - :raises: A not implemented exception - """ - raise NotImplementedException() - - def decode(self, data): - """Decode data part of the message. - :param data: is a string object - :raises NotImplementedException: - """ - raise NotImplementedException() - - @classmethod - def calculateRtuFrameSize(cls, buffer): - """Calculate the size of a PDU. - - :param buffer: A buffer containing the data that have been received. - :returns: The number of bytes in the PDU. - :raises NotImplementedException: - """ - if hasattr(cls, "_rtu_frame_size"): - return cls._rtu_frame_size - if hasattr(cls, "_rtu_byte_count_pos"): - return rtuFrameSize(buffer, cls._rtu_byte_count_pos) - raise NotImplementedException( - f"Cannot determine RTU frame size for {cls.__name__}" - ) - - -class ModbusRequest(ModbusPDU): - """Base class for a modbus request PDU.""" - - function_code = -1 - - def __init__(self, slave, transaction, skip_encode): - """Proxy to the lower level initializer. - - :param slave: Modbus slave slave ID - """ - super().__init__(slave, transaction, skip_encode) - self.fut = None - - def doException(self, exception): - """Build an error response based on the function. - - :param exception: The exception to return - :raises: An exception response - """ + def doException(self, exception: int) -> ExceptionResponse: + """Build an error response based on the function.""" exc = ExceptionResponse(self.function_code, exception) Log.error("Exception response {}", exc) return exc + def isError(self) -> bool: + """Check if the error is a success or failure.""" + return self.function_code > 0x80 -class ModbusResponse(ModbusPDU): - """Base class for a modbus response PDU. - - .. attribute:: should_respond - - A flag that indicates if this response returns a result back - to the client issuing the request - - .. attribute:: _rtu_frame_size - - Indicates the size of the modbus rtu response used for - calculating how much to read. - """ - - should_respond = True - function_code = 0x00 + def get_response_pdu_size(self) -> int: + """Calculate response pdu size.""" + return 0 - def __init__(self, slave, transaction, skip_encode): - """Proxy the lower level initializer. + @abstractmethod + def encode(self) -> bytes: + """Encode the message.""" - :param slave: Modbus slave slave ID + @abstractmethod + def decode(self, data: bytes) -> None: + """Decode data part of the message.""" - """ - super().__init__(slave, transaction, skip_encode) - self.bits = [] - self.registers = [] - self.request = None - def isError(self) -> bool: - """Check if the error is a success or failure.""" - return self.function_code > 0x80 + @classmethod + def calculateRtuFrameSize(cls, data: bytes) -> int: + """Calculate the size of a PDU.""" + if cls._rtu_frame_size: + return cls._rtu_frame_size + if cls._rtu_byte_count_pos: + if len(data) < cls._rtu_byte_count_pos +1: + return 0 + return int(data[cls._rtu_byte_count_pos]) + cls._rtu_byte_count_pos + 3 + raise NotImplementedException( + f"Cannot determine RTU frame size for {cls.__name__}" + ) -# --------------------------------------------------------------------------- # -# Exception PDUs -# --------------------------------------------------------------------------- # class ModbusExceptions: # pylint: disable=too-few-public-methods """An enumeration of the valid modbus exceptions.""" @@ -159,11 +84,8 @@ class ModbusExceptions: # pylint: disable=too-few-public-methods GatewayNoResponse = 0x0B @classmethod - def decode(cls, code): - """Give an error code, translate it to a string error name. - - :param code: The code number to translate - """ + def decode(cls, code: int) -> str | None: + """Give an error code, translate it to a string error name.""" values = { v: k for k, v in iter(cls.__dict__.items()) @@ -172,78 +94,35 @@ def decode(cls, code): return values.get(code, None) -class ExceptionResponse(ModbusResponse): +class ExceptionResponse(ModbusPDU): """Base class for a modbus exception PDU.""" - ExceptionOffset = 0x80 _rtu_frame_size = 5 - def __init__(self, function_code, exception_code=None, slave=1, transaction=0, skip_encode=False): - """Initialize the modbus exception response. - - :param function_code: The function to build an exception response for - :param exception_code: The specific modbus exception to return - """ - super().__init__(slave, transaction, skip_encode) - self.original_code = function_code - self.function_code = function_code | self.ExceptionOffset + def __init__( + self, + function_code: int, + exception_code: int = 0, + slave: int = 1, + transaction: int = 0, + skip_encode: bool = False) -> None: + """Initialize the modbus exception response.""" + super().__init__() + super().setData(slave, transaction, skip_encode) + self.function_code = function_code | 0x80 self.exception_code = exception_code - def encode(self): - """Encode a modbus exception response. - - :returns: The encoded exception packet - """ + def encode(self) -> bytes: + """Encode a modbus exception response.""" return struct.pack(">B", self.exception_code) - def decode(self, data): - """Decode a modbus exception response. - - :param data: The packet data to decode - """ + def decode(self, data: bytes) -> None: + """Decode a modbus exception response.""" self.exception_code = int(data[0]) - def __str__(self): - """Build a representation of an exception response. - - :returns: The string representation of an exception response - """ + def __str__(self) -> str: + """Build a representation of an exception response.""" message = ModbusExceptions.decode(self.exception_code) - parameters = (self.function_code, self.original_code, message) return ( - "Exception Response(%d, %d, %s)" # pylint: disable=consider-using-f-string - % parameters + f"Exception Response({self.function_code}, {self.function_code - 0x80}, {message})" ) - - -class IllegalFunctionRequest(ModbusRequest): - """Define the Modbus slave exception type "Illegal Function". - - This exception code is returned if the slave:: - - - does not implement the function code **or** - - is not in a state that allows it to process the function - """ - - ErrorCode = 1 - - def __init__(self, function_code, slave, transaction, xskip_encode): - """Initialize a IllegalFunctionRequest. - - :param function_code: The function we are erroring on - """ - super().__init__(slave, transaction, xskip_encode) - self.function_code = function_code - - def decode(self, _data): - """Decode so this failure will run correctly.""" - - def encode(self): - """Decode so this failure will run correctly.""" - - async def execute(self, _context): - """Build an illegal function request error response. - - :returns: The error response packet - """ - return ExceptionResponse(self.function_code, self.ErrorCode) diff --git a/pymodbus/pdu/register_read_message.py b/pymodbus/pdu/register_read_message.py index f8a3c465b..524f05091 100644 --- a/pymodbus/pdu/register_read_message.py +++ b/pymodbus/pdu/register_read_message.py @@ -5,11 +5,11 @@ import struct from pymodbus.exceptions import ModbusIOException -from pymodbus.pdu import ExceptionResponse, ModbusRequest, ModbusResponse -from pymodbus.pdu import ModbusExceptions as merror +from pymodbus.pdu.pdu import ExceptionResponse, ModbusPDU +from pymodbus.pdu.pdu import ModbusExceptions as merror -class ReadRegistersRequestBase(ModbusRequest): +class ReadRegistersRequestBase(ModbusPDU): """Base class for reading a modbus register.""" _rtu_frame_size = 8 @@ -21,7 +21,8 @@ def __init__(self, address, count, slave=1, transaction=0, skip_encode=False): :param count: The number of registers to read :param slave: Modbus slave slave ID """ - super().__init__(slave, transaction, skip_encode) + super().__init__() + super().setData(slave, transaction, skip_encode) self.address = address self.count = count @@ -54,7 +55,7 @@ def __str__(self): return f"{self.__class__.__name__} ({self.address},{self.count})" -class ReadRegistersResponseBase(ModbusResponse): +class ReadRegistersResponseBase(ModbusPDU): """Base class for responding to a modbus register read. The requested registers can be found in the .registers list. @@ -68,7 +69,8 @@ def __init__(self, values, slave=1, transaction=0, skip_encode=False): :param values: The values to write to :param slave: Modbus slave slave ID """ - super().__init__(slave, transaction, skip_encode) + super().__init__() + super().setData(slave, transaction, skip_encode) #: A list of register values self.registers = values or [] @@ -89,8 +91,8 @@ def decode(self, data): :param data: The request to decode """ byte_count = int(data[0]) - if byte_count < 2 or byte_count > 252 or byte_count % 2 == 1 or byte_count != len(data) - 1: - raise ModbusIOException(f"Invalid response {data} has byte count of {byte_count}") + if byte_count < 2 or byte_count > 252 or byte_count % 2 == 1 or byte_count != len(data) - 1: # pragma: no cover + raise ModbusIOException(f"Invalid response {data} has byte count of {byte_count}") # pragma: no cover self.registers = [] for i in range(1, byte_count + 1, 2): self.registers.append(struct.unpack(">H", data[i : i + 2])[0]) @@ -101,7 +103,7 @@ def getRegister(self, index): :param index: The indexed register to retrieve :returns: The request register """ - return self.registers[index] + return self.registers[index] # pragma: no cover def __str__(self): """Return a string representation of the instance. @@ -133,7 +135,7 @@ def __init__(self, address=None, count=None, slave=1, transaction=0, skip_encode """ super().__init__(address, count, slave, transaction, skip_encode) - async def execute(self, context): + async def update_datastore(self, context): # pragma: no cover """Run a read holding request against a datastore. :param context: The datastore to request from @@ -195,7 +197,7 @@ def __init__(self, address=None, count=None, slave=1, transaction=0, skip_encode """ super().__init__(address, count, slave, transaction, skip_encode) - async def execute(self, context): + async def update_datastore(self, context): # pragma: no cover """Run a read input request against a datastore. :param context: The datastore to request from @@ -235,7 +237,7 @@ def __init__(self, values=None, slave=None, transaction=0, skip_encode=0): super().__init__(values, slave, transaction, skip_encode) -class ReadWriteMultipleRegistersRequest(ModbusRequest): +class ReadWriteMultipleRegistersRequest(ModbusPDU): """Read/write multiple registers. This function code performs a combination of one read operation and one @@ -263,7 +265,8 @@ def __init__(self, read_address=0x00, read_count=0, write_address=0x00, write_re :param write_address: The address to start writing to :param write_registers: The registers to write to the specified address """ - super().__init__(slave, transaction, skip_encode) + super().__init__() + super().setData(slave, transaction, skip_encode) self.read_address = read_address self.read_count = read_count self.write_address = write_address @@ -307,7 +310,7 @@ def decode(self, data): register = struct.unpack(">H", data[i : i + 2])[0] self.write_registers.append(register) - async def execute(self, context): + async def update_datastore(self, context): # pragma: no cover """Run a write single register request against a datastore. :param context: The datastore to request from diff --git a/pymodbus/pdu/register_write_message.py b/pymodbus/pdu/register_write_message.py index 6a1abb3aa..bb94bad18 100644 --- a/pymodbus/pdu/register_write_message.py +++ b/pymodbus/pdu/register_write_message.py @@ -4,11 +4,11 @@ # pylint: disable=missing-type-doc import struct -from pymodbus.pdu import ModbusExceptions as merror -from pymodbus.pdu import ModbusRequest, ModbusResponse +from pymodbus.pdu.pdu import ModbusExceptions as merror +from pymodbus.pdu.pdu import ModbusPDU -class WriteSingleRegisterRequest(ModbusRequest): +class WriteSingleRegisterRequest(ModbusPDU): """This function code is used to write a single holding register in a remote device. The Request PDU specifies the address of the register to @@ -26,7 +26,8 @@ def __init__(self, address=None, value=None, slave=None, transaction=0, skip_enc :param address: The address to start writing add :param value: The values to write """ - super().__init__(slave, transaction, skip_encode) + super().__init__() + super().setData(slave, transaction, skip_encode) self.address = address self.value = value @@ -36,8 +37,8 @@ def encode(self): :returns: The encoded packet """ packet = struct.pack(">H", self.address) - if self.skip_encode: - packet += self.value + if self.skip_encode or isinstance(self.value, bytes): # pragma: no cover + packet += self.value # pragma: no cover else: packet += struct.pack(">H", self.value) return packet @@ -49,7 +50,7 @@ def decode(self, data): """ self.address, self.value = struct.unpack(">HH", data) - async def execute(self, context): + async def update_datastore(self, context): # pragma: no cover """Run a write single register request against a datastore. :param context: The datastore to request from @@ -82,7 +83,7 @@ def __str__(self): return f"WriteRegisterRequest {self.address}" -class WriteSingleRegisterResponse(ModbusResponse): +class WriteSingleRegisterResponse(ModbusPDU): """The normal response is an echo of the request. Returned after the register contents have been written. @@ -91,13 +92,14 @@ class WriteSingleRegisterResponse(ModbusResponse): function_code = 6 _rtu_frame_size = 8 - def __init__(self, address=None, value=None, slave=1, transaction=0, skip_encode=False): + def __init__(self, address=0, value=0, slave=1, transaction=0, skip_encode=False): """Initialize a new instance. :param address: The address to start writing add :param value: The values to write """ - super().__init__(slave, transaction, skip_encode) + super().__init__() + super().setData(slave, transaction, skip_encode) self.address = address self.value = value @@ -138,7 +140,7 @@ def __str__(self): # ---------------------------------------------------------------------------# # Write Multiple Registers # ---------------------------------------------------------------------------# -class WriteMultipleRegistersRequest(ModbusRequest): +class WriteMultipleRegistersRequest(ModbusPDU): """This function code is used to write a block. Of contiguous registers (1 to approx. 120 registers) in a remote device. @@ -152,13 +154,14 @@ class WriteMultipleRegistersRequest(ModbusRequest): _rtu_byte_count_pos = 6 _pdu_length = 5 # func + adress1 + adress2 + outputQuant1 + outputQuant2 - def __init__(self, address=None, values=None, slave=None, transaction=0, skip_encode=0): + def __init__(self, address=0, values=None, slave=None, transaction=0, skip_encode=0): """Initialize a new instance. :param address: The address to start writing to :param values: The values to write """ - super().__init__(slave, transaction, skip_encode) + super().__init__() + super().setData(slave, transaction, skip_encode) self.address = address if values is None: values = [] @@ -174,11 +177,14 @@ def encode(self): :returns: The encoded packet """ packet = struct.pack(">HHB", self.address, self.count, self.byte_count) - if self.skip_encode: - return packet + b"".join(self.values) + if self.skip_encode: # pragma: no cover + return packet + b"".join(self.values) # pragma: no cover for value in self.values: - packet += struct.pack(">H", value) + if isinstance(value, bytes): # pragma: no cover + packet += value # pragma: no cover + else: + packet += struct.pack(">H", value) return packet @@ -192,7 +198,7 @@ def decode(self, data): for idx in range(5, (self.count * 2) + 5, 2): self.values.append(struct.unpack(">H", data[idx : idx + 2])[0]) - async def execute(self, context): + async def update_datastore(self, context): # pragma: no cover """Run a write single register request against a datastore. :param context: The datastore to request from @@ -230,7 +236,7 @@ def __str__(self): ) -class WriteMultipleRegistersResponse(ModbusResponse): +class WriteMultipleRegistersResponse(ModbusPDU): """The normal response returns the function code. Starting address, and quantity of registers written. @@ -239,13 +245,14 @@ class WriteMultipleRegistersResponse(ModbusResponse): function_code = 16 _rtu_frame_size = 8 - def __init__(self, address=None, count=None, slave=1, transaction=0, skip_encode=False): + def __init__(self, address=0, count=0, slave=1, transaction=0, skip_encode=False): """Initialize a new instance. :param address: The address to start writing to :param count: The number of registers to write to """ - super().__init__(slave, transaction, skip_encode) + super().__init__() + super().setData(slave, transaction, skip_encode) self.address = address self.count = count @@ -275,7 +282,7 @@ def __str__(self): ) -class MaskWriteRegisterRequest(ModbusRequest): +class MaskWriteRegisterRequest(ModbusPDU): """This function code is used to modify the contents. Of a specified holding register using a combination of an AND mask, @@ -294,7 +301,8 @@ def __init__(self, address=0x0000, and_mask=0xFFFF, or_mask=0x0000, slave=1, tra :param and_mask: The and bitmask to apply to the register address :param or_mask: The or bitmask to apply to the register address """ - super().__init__(slave, transaction, skip_encode) + super().__init__() + super().setData(slave, transaction, skip_encode) self.address = address self.and_mask = and_mask self.or_mask = or_mask @@ -313,7 +321,7 @@ def decode(self, data): """ self.address, self.and_mask, self.or_mask = struct.unpack(">HHH", data) - async def execute(self, context): + async def update_datastore(self, context): # pragma: no cover """Run a mask write register request against the store. :param context: The datastore to request from @@ -333,7 +341,7 @@ async def execute(self, context): return MaskWriteRegisterResponse(self.address, self.and_mask, self.or_mask) -class MaskWriteRegisterResponse(ModbusResponse): +class MaskWriteRegisterResponse(ModbusPDU): """The normal response is an echo of the request. The response is returned after the register has been written. @@ -349,7 +357,8 @@ def __init__(self, address=0x0000, and_mask=0xFFFF, or_mask=0x0000, slave=1, tra :param and_mask: The and bitmask applied to the register address :param or_mask: The or bitmask applied to the register address """ - super().__init__(slave, transaction, skip_encode) + super().__init__() + super().setData(slave, transaction, skip_encode) self.address = address self.and_mask = and_mask self.or_mask = or_mask diff --git a/pymodbus/server/async_io.py b/pymodbus/server/async_io.py index 7ad9d19d4..d06154603 100644 --- a/pymodbus/server/async_io.py +++ b/pymodbus/server/async_io.py @@ -9,11 +9,12 @@ from pymodbus.datastore import ModbusServerContext from pymodbus.device import ModbusControlBlock, ModbusDeviceIdentification -from pymodbus.exceptions import NoSuchSlaveException -from pymodbus.factory import ServerDecoder +from pymodbus.exceptions import ModbusException, NoSuchSlaveException from pymodbus.framer import FRAMER_NAME_TO_CLASS, FramerBase, FramerType from pymodbus.logging import Log +from pymodbus.pdu import DecodePDU from pymodbus.pdu import ModbusExceptions as merror +from pymodbus.pdu.pdu import ExceptionResponse from pymodbus.transport import CommParams, CommType, ModbusProtocol @@ -48,6 +49,7 @@ def __init__(self, owner): self.running = False self.receive_queue: asyncio.Queue = asyncio.Queue() self.handler_task = None # coroutine to be run on asyncio loop + self.databuffer = b'' self.framer: FramerBase self.loop = asyncio.get_running_loop() @@ -68,14 +70,9 @@ def callback_connected(self) -> None: if self.server.broadcast_enable: if 0 not in slaves: slaves.append(0) - if 0 in slaves: - slaves = [] try: self.running = True - self.framer = self.server.framer( - self.server.decoder, - slaves, - ) + self.framer = self.server.framer(self.server.decoder) # schedule the connection handler on the event loop self.handler_task = asyncio.create_task(self.handle()) @@ -122,11 +119,21 @@ async def inner_handle(self): # if broadcast is enabled make sure to # process requests to address 0 - Log.debug("Handling data: {}", data, ":hex") - self.framer.processIncomingPacket( - data=data, - callback=lambda x: self.execute(x, *addr), - ) + self.databuffer += data + Log.debug("Handling data: {}", self.databuffer, ":hex") + try: + used_len, pdu = self.framer.processIncomingFrame(self.databuffer) + except ModbusException: + pdu = ExceptionResponse( + 40, + exception_code=merror.IllegalFunction + ) + self.server_send(pdu, 0) + pdu = None + used_len = len(self.databuffer) + self.databuffer = self.databuffer[used_len:] + if pdu: + self.execute(pdu, *addr) async def handle(self) -> None: """Coroutine which represents a single master <=> slave conversation. @@ -152,7 +159,7 @@ async def handle(self) -> None: self._log_exception() self.running = False except Exception as exc: # pylint: disable=broad-except - # force TCP socket termination as processIncomingPacket + # force TCP socket termination as framer # should handle application layer errors Log.error( 'Unknown exception "{}" on stream {} forcing disconnect', @@ -181,10 +188,10 @@ async def _async_execute(self, request, *addr): # if broadcasting then execute on all slave contexts, # note response will be ignored for slave_id in self.server.context.slaves(): - response = await request.execute(self.server.context[slave_id]) + response = await request.update_datastore(self.server.context[slave_id]) else: context = self.server.context[request.slave_id] - response = await request.execute(context) + response = await request.update_datastore(context) except NoSuchSlaveException: Log.error("requested slave does not exist: {}", request.slave_id) @@ -211,11 +218,11 @@ def server_send(self, message, addr, **kwargs): """Send message.""" if kwargs.get("skip_encoding", False): self.send(message, addr=addr) - elif message.should_respond: - pdu = self.framer.buildPacket(message) - self.send(pdu, addr=addr) - else: + if not message: Log.debug("Skipping sending response!!") + else: + pdu = self.framer.buildFrame(message) + self.send(pdu, addr=addr) async def _recv_(self): """Receive data from the network.""" @@ -260,7 +267,7 @@ def __init__( True, ) self.loop = asyncio.get_running_loop() - self.decoder = ServerDecoder() + self.decoder = DecodePDU(True) self.context = context or ModbusServerContext() self.control = ModbusControlBlock() self.ignore_missing_slaves = ignore_missing_slaves diff --git a/pymodbus/server/simulator/http_server.py b/pymodbus/server/simulator/http_server.py index 6bf170716..aba2f3432 100644 --- a/pymodbus/server/simulator/http_server.py +++ b/pymodbus/server/simulator/http_server.py @@ -24,9 +24,8 @@ from pymodbus.datastore import ModbusServerContext, ModbusSimulatorContext from pymodbus.datastore.simulator import Label from pymodbus.device import ModbusDeviceIdentification -from pymodbus.factory import ServerDecoder from pymodbus.logging import Log -from pymodbus.pdu import ExceptionResponse +from pymodbus.pdu import DecodePDU, ExceptionResponse from pymodbus.server.async_io import ( ModbusSerialServer, ModbusTcpServer, @@ -215,7 +214,7 @@ def __init__( self.refresh_rate = 0 self.register_filter: list[int] = [] self.call_list: list[CallTracer] = [] - self.request_lookup = ServerDecoder.getFCdict() + self.request_lookup = DecodePDU(True).lookup self.call_monitor = CallTypeMonitor() self.call_response = CallTypeResponse() app_key = getattr(web, 'AppKey', str) # fall back to str for aiohttp < 3.9.0 @@ -391,7 +390,7 @@ def build_html_calls(self, params: dict, html: str) -> str: for function in self.request_lookup.values(): selected = ( "selected" - if function.function_code == self.call_monitor.function #type: ignore[attr-defined] + if function.function_code == self.call_monitor.function else "" ) function_codes += f"" #type: ignore[attr-defined] @@ -557,9 +556,9 @@ def build_json_calls(self, params: dict) -> dict: function_codes = [] for function in self.request_lookup.values(): function_codes.append({ - "value": function.function_code, # type: ignore[attr-defined] + "value": function.function_code, "text": function.function_code_name, # type: ignore[attr-defined] - "selected": function.function_code == self.call_monitor.function # type: ignore[attr-defined] + "selected": function.function_code == self.call_monitor.function }) simulation_action = "ACTIVE" if self.call_response.active != RESPONSE_INACTIVE else "" @@ -756,8 +755,8 @@ def server_response_manipulator(self, response): skip_encoding = False if self.call_response.active == RESPONSE_EMPTY: Log.warning("Sending empty response") - response.should_respond = False - elif self.call_response.active == RESPONSE_NORMAL: + return None, False + if self.call_response.active == RESPONSE_NORMAL: if self.call_response.delay: Log.warning( "Delaying response by {}s for all incoming requests", diff --git a/pymodbus/transaction.py b/pymodbus/transaction.py index 327f84657..1b2f9e612 100644 --- a/pymodbus/transaction.py +++ b/pymodbus/transaction.py @@ -8,6 +8,7 @@ ] import struct +import time from contextlib import suppress from threading import RLock from typing import TYPE_CHECKING @@ -24,7 +25,7 @@ FramerTLS, ) from pymodbus.logging import Log -from pymodbus.pdu import ModbusRequest +from pymodbus.pdu import ModbusPDU from pymodbus.transport import CommType from pymodbus.utilities import ModbusTransactionState, hexlify_packets @@ -45,7 +46,7 @@ class ModbusTransactionManager: def __init__(self): """Initialize an instance of the ModbusTransactionManager.""" self.tid = 0 - self.transactions: dict[int, ModbusRequest] = {} + self.transactions: dict[int, ModbusPDU] = {} def __iter__(self): """Iterate over the current managed transactions. @@ -54,7 +55,7 @@ def __iter__(self): """ return iter(self.transactions.keys()) - def addTransaction(self, request: ModbusRequest): + def addTransaction(self, request: ModbusPDU): """Add a transaction to the handler. This holds the request in case it needs to be resent. @@ -136,6 +137,7 @@ def __init__(self, client: ModbusBaseSyncClient, retries): self.retries = retries self._transaction_lock = RLock() self._no_response_devices: list[int] = [] + self.databuffer = b'' if client: self._set_adu_size() @@ -175,7 +177,7 @@ def _validate_response(self, response): return False return True - def execute(self, request: ModbusRequest): # noqa: C901 + def execute(self, no_response_expected: bool, request: ModbusPDU): # noqa: C901 """Start the producer to send the next request to consumer.write(Frame(request)).""" with self._transaction_lock: try: @@ -184,24 +186,25 @@ def execute(self, request: ModbusRequest): # noqa: C901 ModbusTransactionState.to_string(self.client.state), ) retries = self.retries - request.transaction_id = self.getNextTID() + if isinstance(self.client.framer, FramerSocket): + request.transaction_id = self.getNextTID() + else: + request.transaction_id = 0 Log.debug("Running transaction {}", request.transaction_id) if _buffer := hexlify_packets( self.client.framer.databuffer ): Log.debug("Clearing current Frame: - {}", _buffer) self.client.framer.databuffer = b'' - broadcast = not request.slave_id expected_response_length = None if not isinstance(self.client.framer, FramerSocket): - if hasattr(request, "get_response_pdu_size"): - response_pdu_size = request.get_response_pdu_size() - if isinstance(self.client.framer, FramerAscii): - response_pdu_size *= 2 - if response_pdu_size: - expected_response_length = ( - self._calculate_response_length(response_pdu_size) - ) + response_pdu_size = request.get_response_pdu_size() + if isinstance(self.client.framer, FramerAscii): + response_pdu_size *= 2 + if response_pdu_size: + expected_response_length = ( + self._calculate_response_length(response_pdu_size) + ) if ( # pylint: disable=simplifiable-if-statement request.slave_id in self._no_response_devices ): @@ -213,11 +216,13 @@ def execute(self, request: ModbusRequest): # noqa: C901 if not expected_response_length: expected_response_length = 1024 response, last_exception = self._transact( + no_response_expected, request, expected_response_length, full=full, - broadcast=broadcast, ) + if no_response_expected: + return None while retries > 0: if self._validate_response(response): if ( @@ -232,23 +237,23 @@ def execute(self, request: ModbusRequest): # noqa: C901 self._no_response_devices.append(request.slave_id) # No response received and retries not enabled break - self.client.framer.processIncomingPacket( - response, - self.addTransaction, - tid=request.transaction_id, - ) - if not (response := self.getTransaction(request.transaction_id)): + self.databuffer += response + used_len, pdu = self.client.framer.processIncomingFrame(self.databuffer) + self.databuffer = self.databuffer[used_len:] + if pdu: + self.addTransaction(pdu) + if not (result := self.getTransaction(request.transaction_id)): if len(self.transactions): - response = self.getTransaction(tid=0) + result = self.getTransaction(0) else: last_exception = last_exception or ( "No Response received from the remote slave" "/Unable to decode response" ) - response = ModbusIOException( + result = ModbusIOException( last_exception, request.function_code ) - self.client.close() + self.client.close() if hasattr(self.client, "state"): Log.debug( "Changing transaction state from " @@ -256,7 +261,7 @@ def execute(self, request: ModbusRequest): # noqa: C901 '"TRANSACTION_COMPLETE"' ) self.client.state = ModbusTransactionState.TRANSACTION_COMPLETE - return response + return result except ModbusIOException as exc: # Handle decode errors method Log.error("Modbus IO exception {}", exc) @@ -264,7 +269,7 @@ def execute(self, request: ModbusRequest): # noqa: C901 self.client.close() return exc - def _retry_transaction(self, retries, reason, packet, response_length, full=False): + def _retry_transaction(self, no_response_expected, retries, reason, packet, response_length, full=False): """Retry transaction.""" Log.debug("Retry on {} response - {}", reason, retries) Log.debug('Changing transaction state from "WAITING_FOR_REPLY" to "RETRYING"') @@ -277,22 +282,14 @@ def _retry_transaction(self, retries, reason, packet, response_length, full=Fals if response_length == in_waiting: result = self._recv(response_length, full) return result, None - return self._transact(packet, response_length, full=full) + return self._transact(no_response_expected, packet, response_length, full=full) - def _transact(self, request: ModbusRequest, response_length, full=False, broadcast=False): - """Do a Write and Read transaction. - - :param packet: packet to be sent - :param response_length: Expected response length - :param full: the target device was notorious for its no response. Dont - waste time this time by partial querying - :param broadcast: - :return: response - """ + def _transact(self, no_response_expected: bool, request: ModbusPDU, response_length, full=False): + """Do a Write and Read transaction.""" last_exception = None try: self.client.connect() - packet = self.client.framer.buildPacket(request) + packet = self.client.framer.buildFrame(request) Log.debug("SEND: {}", packet, ":hex") size = self._send(packet) if ( @@ -308,7 +305,7 @@ def _transact(self, request: ModbusRequest, response_length, full=False, broadca if self.client.comm_params.handle_local_echo is True: if self._recv(size, full) != packet: return b"", "Wrong local echo" - if broadcast: + if no_response_expected: if size: Log.debug( 'Changing transaction state from "SENDING" ' @@ -390,11 +387,21 @@ def _recv(self, expected_response_length, full) -> bytes: # noqa: C901 total = expected_response_length + min_size else: total = expected_response_length + retries = 0 + missing_len = expected_response_length + result = read_min + while missing_len and retries < self.retries: + if retries: + time.sleep(0.1) + data = self.client.recv(expected_response_length) + result += data + missing_len -= len(data) + retries += 1 else: read_min = b"" total = expected_response_length - result = self.client.recv(expected_response_length) - result = read_min + result + result = self.client.recv(expected_response_length) + result = read_min + result actual = len(result) if total is not None and actual != total: msg_start = "Incomplete message" if actual else "No response" diff --git a/pymodbus/utilities.py b/pymodbus/utilities.py index b0274116d..1bbbc8f3d 100644 --- a/pymodbus/utilities.py +++ b/pymodbus/utilities.py @@ -10,7 +10,6 @@ "pack_bitstring", "unpack_bitstring", "default", - "rtuFrameSize", ] # pylint: disable=missing-type-doc @@ -150,29 +149,6 @@ def unpack_bitstring(data: bytes) -> list[bool]: # --------------------------------------------------------------------------- # -def rtuFrameSize(data, byte_count_pos): # pylint: disable=invalid-name - """Calculate the size of the frame based on the byte count. - - :param data: The buffer containing the frame. - :param byte_count_pos: The index of the byte count in the buffer. - :returns: The size of the frame. - - The structure of frames with a byte count field is always the - same: - - - first, there are some header fields - - then the byte count field - - then as many data bytes as indicated by the byte count, - - finally the CRC (two bytes). - - To calculate the frame size, it is therefore sufficient to extract - the contents of the byte count field, add the position of this - field, and finally increment the sum by three (one byte for the - byte count field, two for the CRC). - """ - return int(data[byte_count_pos]) + byte_count_pos + 3 - - def hexlify_packets(packet): """Return hex representation of bytestring received. diff --git a/pyproject.toml b/pyproject.toml index bfb3ce760..2d3137d14 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -26,6 +26,7 @@ classifiers = [ "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", "Topic :: System :: Networking", "Topic :: Utilities", ] @@ -51,7 +52,7 @@ repl = [ simulator = [ "aiohttp>=3.8.6;python_version<'3.12'", - "aiohttp>=3.10.5;python_version=='3.12'" + "aiohttp>=3.10.5;python_version>='3.12'" ] documentation = [ "recommonmark>=0.7.1", @@ -67,7 +68,7 @@ development = [ "pytest>=8.3.3", "pytest-asyncio>=0.24.0", "pytest-cov>=5.0.0", - "pytest-profiling>=1.7.0", + "pytest-profiling>=1.7.0;python_version<'3.13'", "pytest-timeout>=2.3.1", "pytest-xdist>=3.6.1", "pytest-aiohttp>=1.0.5", diff --git a/test/conftest.py b/test/conftest.py index 7becad60d..834c09c95 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -287,17 +287,7 @@ def recv(self, size): """Receive.""" if not self.packets or not size: return b"" - # if not self.buffer: - # self.buffer = self.packets.popleft() - # if size >= len(self.buffer): - # retval = self.buffer - # self.buffer = None - # else: - # retval = self.buffer[0:size] - # self.buffer = self.buffer[size] - self.buffer = self.packets.popleft() - retval = self.buffer - self.buffer = None + retval = self.packets.popleft() self.in_waiting -= len(retval) return retval @@ -309,6 +299,10 @@ def recvfrom(self, size): """Receive from.""" return [self.recv(size)] + def write(self, msg): + """Write.""" + return self.send(msg) + def send(self, msg): """Send.""" if not self.copy_send: diff --git a/test/framers/__init__.py b/test/framer/__init__.py similarity index 100% rename from test/framers/__init__.py rename to test/framer/__init__.py diff --git a/test/framers/conftest.py b/test/framer/conftest.py similarity index 54% rename from test/framers/conftest.py rename to test/framer/conftest.py index 18720c6b7..b5f28f20c 100644 --- a/test/framers/conftest.py +++ b/test/framer/conftest.py @@ -3,8 +3,8 @@ import pytest -from pymodbus.factory import ClientDecoder, ServerDecoder from pymodbus.framer import FRAMER_NAME_TO_CLASS, FramerType +from pymodbus.pdu import DecodePDU @pytest.fixture(name="entry") @@ -17,15 +17,7 @@ def prepare_is_server(): """Return client/server.""" return False -@pytest.fixture(name="dev_ids") -def prepare_dev_ids(): - """Return list of device ids.""" - return [0, 17] - @pytest.fixture(name="test_framer") -async def prepare_test_framer(entry, is_server, dev_ids): +async def prepare_test_framer(entry, is_server): """Return framer object.""" - return FRAMER_NAME_TO_CLASS[entry]( - (ServerDecoder if is_server else ClientDecoder)(), - dev_ids, - ) + return FRAMER_NAME_TO_CLASS[entry](DecodePDU(is_server)) diff --git a/test/framers/generator.py b/test/framer/generator.py similarity index 81% rename from test/framers/generator.py rename to test/framer/generator.py index 37e4e46c8..f546433a2 100755 --- a/test/framers/generator.py +++ b/test/framer/generator.py @@ -1,13 +1,13 @@ #!/usr/bin/env python3 """Build framer encode responses.""" -from pymodbus.factory import ClientDecoder, ServerDecoder from pymodbus.framer import ( FramerAscii, FramerRTU, FramerSocket, FramerTLS, ) +from pymodbus.pdu import DecodePDU from pymodbus.pdu import ModbusExceptions as merror from pymodbus.pdu.register_read_message import ( ReadHoldingRegistersRequest, @@ -23,23 +23,23 @@ def set_calls(): print(f" dev_id --> {dev_id}") for tid in (0, 3077): print(f" tid --> {tid}") - client = framer(ClientDecoder(), [0]) + client = framer(DecodePDU(False)) request = ReadHoldingRegistersRequest(124, 2, dev_id) request.transaction_id = tid - result = client.buildPacket(request) + result = client.buildFrame(request) print(f" request --> {result}") print(f" request --> {result.hex()}") - server = framer(ServerDecoder(), [0]) + server = framer(DecodePDU(True)) response = ReadHoldingRegistersResponse([141,142]) response.slave_id = dev_id response.transaction_id = tid - result = server.buildPacket(response) + result = server.buildFrame(response) print(f" response --> {result}") print(f" response --> {result.hex()}") exception = request.doException(merror.IllegalAddress) exception.transaction_id = tid exception.slave_id = dev_id - result = server.buildPacket(exception) + result = server.buildFrame(exception) print(f" exception --> {result}") print(f" exception --> {result.hex()}") diff --git a/test/framer/test_extras.py b/test/framer/test_extras.py new file mode 100755 index 000000000..b6e605fbb --- /dev/null +++ b/test/framer/test_extras.py @@ -0,0 +1,98 @@ +"""Test transaction.""" + +from pymodbus.framer import ( + FramerAscii, + FramerRTU, + FramerSocket, + FramerTLS, +) +from pymodbus.pdu import DecodePDU + + +TEST_MESSAGE = b"\x7b\x01\x03\x00\x00\x00\x05\x85\xC9\x7d" + + +class TestExtas: + """Unittest for the pymodbus.transaction module.""" + + client = None + decoder = None + _tcp = None + _tls = None + _rtu = None + _ascii = None + _manager = None + _tm = None + + # ----------------------------------------------------------------------- # + # Test Construction + # ----------------------------------------------------------------------- # + def setup_method(self): + """Set up the test environment.""" + self.client = None + self.decoder = DecodePDU(True) + self._tcp = FramerSocket(self.decoder) + self._tls = FramerTLS(self.decoder) + self._rtu = FramerRTU(self.decoder) + self._ascii = FramerAscii(self.decoder) + + + def test_tcp_framer_transaction_half2(self): + """Test a half completed tcp frame transaction.""" + msg1 = b"\x00\x01\x12\x34\x00\x06\xff" + msg2 = b"\x02\x01\x02\x00\x08" + used_len, pdu = self._tcp.processIncomingFrame(msg1) + assert not pdu + assert not used_len + used_len, pdu = self._tcp.processIncomingFrame(msg1+msg2) + assert pdu + assert used_len == len(msg1) + len(msg2) + assert pdu.function_code.to_bytes(1,'big') + pdu.encode() == msg2 + + def test_tcp_framer_transaction_half3(self): + """Test a half completed tcp frame transaction.""" + msg1 = b"\x00\x01\x12\x34\x00\x06\xff\x02\x01\x02\x00" + msg2 = b"\x08" + used_len, pdu = self._tcp.processIncomingFrame(msg1) + assert not pdu + assert not used_len + used_len, pdu = self._tcp.processIncomingFrame(msg1+msg2) + assert pdu + assert used_len == len(msg1) + len(msg2) + assert pdu.function_code.to_bytes(1,'big') + pdu.encode() == msg1[7:] + msg2 + + def test_tcp_framer_transaction_short(self): + """Test that we can get back on track after an invalid message.""" + msg1 = b'' + msg2 = b"\x00\x01\x12\x34\x00\x06\xff\x02\x01\x02\x00\x08" + used_len, pdu = self._tcp.processIncomingFrame(msg1) + assert not pdu + assert not used_len + used_len, pdu = self._tcp.processIncomingFrame(msg1+msg2) + assert pdu + assert used_len == len(msg1) + len(msg2) + assert pdu.function_code.to_bytes(1,'big') + pdu.encode() == msg2[7:] + + def test_tls_incoming_packet(self): + """Framer tls incoming packet.""" + msg = b"\x01\x12\x34\x00\x06" + _, pdu = self._tls.processIncomingFrame(msg) + assert pdu + + def test_rtu_process_incoming_packets(self): + """Test rtu process incoming packets.""" + msg = b"\x00\x01\x00\x00\x00\x01\xfc\x1b" + _, pdu = self._rtu.processIncomingFrame(msg) + assert pdu + + def test_ascii_process_incoming_packets(self): + """Test ascii process incoming packet.""" + msg = b":F7031389000A60\r\n" + _, pdu = self._ascii.processIncomingFrame(msg) + assert pdu + + def test_rtu_decode_exception(self): + """Test that the RTU framer can decode errors.""" + msg = b"\x00\x90\x02\x9c\x01" + _, pdu = self._rtu.processIncomingFrame(msg) + assert pdu diff --git a/test/framers/test_framer.py b/test/framer/test_framer.py similarity index 74% rename from test/framers/test_framer.py rename to test/framer/test_framer.py index 0a331caf1..cce0d98bd 100644 --- a/test/framers/test_framer.py +++ b/test/framer/test_framer.py @@ -1,9 +1,8 @@ """Test framer.""" - +from unittest import mock import pytest -from pymodbus.factory import ClientDecoder from pymodbus.framer import ( FramerAscii, FramerBase, @@ -12,6 +11,7 @@ FramerTLS, FramerType, ) +from pymodbus.pdu import DecodePDU, ModbusPDU from .generator import set_calls @@ -19,24 +19,23 @@ class TestFramer: """Test module.""" - def test_setup(self, entry, is_server, dev_ids): + def test_setup(self, entry, is_server): """Test conftest.""" assert entry == FramerType.RTU assert not is_server - assert dev_ids == [0, 17] set_calls() def test_base(self): """Test FramerBase.""" - framer = FramerBase(ClientDecoder(), []) + framer = FramerBase(DecodePDU(False)) framer.decode(b'') framer.encode(b'', 0, 0) + framer.encode(b'', 2, 0) @pytest.mark.parametrize(("entry"), list(FramerType)) async def test_framer_init(self, test_framer): """Test framer type.""" - test_framer.incomming_dev_id = 1 - assert test_framer.incomming_dev_id + assert test_framer @pytest.mark.parametrize( ("func", "test_compare", "expect"), @@ -190,7 +189,7 @@ def test_encode_type(self, frame, frame_expected, data, dev_id, tr_id, inx1, inx """Test encode method.""" if frame == FramerTLS and dev_id + tr_id: return - frame_obj = frame(ClientDecoder(), [0]) + frame_obj = frame(DecodePDU(False)) expected = frame_expected[inx1 + inx2 + inx3] encoded_data = frame_obj.encode(data, dev_id, tr_id) assert encoded_data == expected @@ -201,21 +200,21 @@ def test_encode_type(self, frame, frame_expected, data, dev_id, tr_id, inx1, inx (FramerType.ASCII, True, b':0003007C00027F\r\n', 0, 0, b"\x03\x00\x7c\x00\x02",), # Request (FramerType.ASCII, False, b':000304008D008EDE\r\n', 0, 0, b"\x03\x04\x00\x8d\x00\x8e",), # Response (FramerType.ASCII, False, b':0083027B\r\n', 0, 0, b'\x83\x02',), # Exception - (FramerType.ASCII, True, b':1103007C00026E\r\n', 17, 17, b"\x03\x00\x7c\x00\x02",), # Request - (FramerType.ASCII, False, b':110304008D008ECD\r\n', 17, 17, b"\x03\x04\x00\x8d\x00\x8e",), # Response - (FramerType.ASCII, False, b':1183026A\r\n', 17, 17, b'\x83\x02',), # Exception - (FramerType.ASCII, True, b':FF03007C000280\r\n', 255, 255, b"\x03\x00\x7c\x00\x02",), # Request - (FramerType.ASCII, False, b':FF0304008D008EDF\r\n', 255, 255, b"\x03\x04\x00\x8d\x00\x8e",), # Response - (FramerType.ASCII, False, b':FF83027C\r\n', 255, 255, b'\x83\x02',), # Exception + (FramerType.ASCII, True, b':1103007C00026E\r\n', 17, 0, b"\x03\x00\x7c\x00\x02",), # Request + (FramerType.ASCII, False, b':110304008D008ECD\r\n', 17, 0, b"\x03\x04\x00\x8d\x00\x8e",), # Response + (FramerType.ASCII, False, b':1183026A\r\n', 17, 0, b'\x83\x02',), # Exception + (FramerType.ASCII, True, b':FF03007C000280\r\n', 255, 0, b"\x03\x00\x7c\x00\x02",), # Request + (FramerType.ASCII, False, b':FF0304008D008EDF\r\n', 255, 0, b"\x03\x04\x00\x8d\x00\x8e",), # Response + (FramerType.ASCII, False, b':FF83027C\r\n', 255, 0, b'\x83\x02',), # Exception (FramerType.RTU, True, b'\x00\x03\x00\x7c\x00\x02\x04\x02', 0, 0, b"\x03\x00\x7c\x00\x02",), # Request (FramerType.RTU, False, b'\x00\x03\x04\x00\x8d\x00\x8e\xfa\xbc', 0, 0, b"\x03\x04\x00\x8d\x00\x8e",), # Response (FramerType.RTU, False, b'\x00\x83\x02\x91\x31', 0, 0, b'\x83\x02',), # Exception - (FramerType.RTU, True, b'\x11\x03\x00\x7c\x00\x02\x07\x43', 17, 17, b"\x03\x00\x7c\x00\x02",), # Request - (FramerType.RTU, False, b'\x11\x03\x04\x00\x8d\x00\x8e\xfb\xbd', 17, 17, b"\x03\x04\x00\x8d\x00\x8e",), # Response - (FramerType.RTU, False, b'\x11\x83\x02\xc1\x34', 17, 17, b'\x83\x02',), # Exception - (FramerType.RTU, True, b'\xff\x03\x00|\x00\x02\x10\x0d', 255, 255, b"\x03\x00\x7c\x00\x02",), # Request - (FramerType.RTU, False, b'\xff\x03\x04\x00\x8d\x00\x8e\xf5\xb3', 255, 255, b"\x03\x04\x00\x8d\x00\x8e",), # Response - (FramerType.RTU, False, b'\xff\x83\x02\xa1\x01', 255, 255, b'\x83\x02',), # Exception + (FramerType.RTU, True, b'\x11\x03\x00\x7c\x00\x02\x07\x43', 17, 0, b"\x03\x00\x7c\x00\x02",), # Request + (FramerType.RTU, False, b'\x11\x03\x04\x00\x8d\x00\x8e\xfb\xbd', 17, 0, b"\x03\x04\x00\x8d\x00\x8e",), # Response + (FramerType.RTU, False, b'\x11\x83\x02\xc1\x34', 17, 0, b'\x83\x02',), # Exception + (FramerType.RTU, True, b'\xff\x03\x00|\x00\x02\x10\x0d', 255, 0, b"\x03\x00\x7c\x00\x02",), # Request + (FramerType.RTU, False, b'\xff\x03\x04\x00\x8d\x00\x8e\xf5\xb3', 255, 0, b"\x03\x04\x00\x8d\x00\x8e",), # Response + (FramerType.RTU, False, b'\xff\x83\x02\xa1\x01', 255, 0, b'\x83\x02',), # Exception (FramerType.SOCKET, True, b'\x00\x00\x00\x00\x00\x06\x00\x03\x00\x7c\x00\x02', 0, 0, b"\x03\x00\x7c\x00\x02",), # Request (FramerType.SOCKET, False, b'\x00\x00\x00\x00\x00\x07\x00\x03\x04\x00\x8d\x00\x8e', 0, 0, b"\x03\x04\x00\x8d\x00\x8e",), # Response (FramerType.SOCKET, False, b'\x00\x00\x00\x00\x00\x03\x00\x83\x02', 0, 0, b'\x83\x02',), # Exception @@ -254,28 +253,28 @@ async def test_decode_type(self, entry, test_framer, data, dev_id, tr_id, expect if entry == FramerType.RTU: return if split == "no": - used_len, res_data = test_framer.decode(data) + used_len, res_dev_id, res_tid, res_data = test_framer.decode(data) elif split == "half": split_len = int(len(data) / 2) - used_len, res_data = test_framer.decode(data[0:split_len]) + used_len, res_dev_id, res_tid, res_data = test_framer.decode(data[0:split_len]) assert not used_len assert not res_data - assert not test_framer.incoming_dev_id - assert not test_framer.incoming_tid - used_len, res_data = test_framer.decode(data) + assert not res_dev_id + assert not res_tid + used_len, res_dev_id, res_tid, res_data = test_framer.decode(data) else: last = len(data) for i in range(0, last -1): - used_len, res_data = test_framer.decode(data[0:i+1]) + used_len, res_dev_id, res_tid, res_data = test_framer.decode(data[0:i+1]) assert not used_len assert not res_data - assert not test_framer.incoming_dev_id - assert not test_framer.incoming_tid - used_len, res_data = test_framer.decode(data) + assert not res_dev_id + assert not res_tid + used_len, res_dev_id, res_tid, res_data = test_framer.decode(data) assert used_len == len(data) assert res_data == expected - assert dev_id == test_framer.incoming_dev_id - assert tr_id == test_framer.incoming_tid + assert dev_id == res_dev_id + assert tr_id == res_tid @pytest.mark.parametrize( ("entry", "data", "exp"), @@ -309,25 +308,32 @@ async def test_decode_type(self, entry, test_framer, data, dev_id, tr_id, expect (12, b"\x03\x00\x7c\x00\x02"), (12, b"\x03\x00\x7c\x00\x02"), ]), - (FramerType.SOCKET, b'\x0c\x05\x00\x00\x00\x02\xff\x83\x02', [(9, b'\x83\x02')],), # Exception + (FramerType.SOCKET, b'\x0c\x05\x00\x00\x00\x02\xff\x83\x02', [ # Exception + (9, b'\x83\x02'), + ]), (FramerType.RTU, b'\x00\x83\x02\x91\x21', [ # bad crc - (2, b''), + (1, b''), + (0, b''), ]), (FramerType.RTU, b'\x00\x83\x02\xf0\x91\x31', [ # dummy char in stream, bad crc - (3, b''), + (1, b''), + (0, b''), ]), (FramerType.RTU, b'\x00\x83\x02\x91\x21\x00\x83\x02\x91\x31', [ # bad crc + good CRC - (10, b'\x83\x02'), + (1, b''), + (0, b''), ]), (FramerType.RTU, b'\x00\x83\x02\xf0\x91\x31\x00\x83\x02\x91\x31', [ # dummy char in stream, bad crc + good CRC - (11, b'\x83\x02'), + (1, b''), + (0, b''), ]), ] ) async def test_decode_complicated(self, test_framer, data, exp): """Test encode method.""" for ent in exp: - used_len, res_data = test_framer.decode(data) + used_len, _, _, res_data = test_framer.decode(data) + data = data[used_len:] assert used_len == ent[0] assert res_data == ent[1] @@ -354,7 +360,87 @@ async def test_decode_complicated(self, test_framer, data, exp): def test_roundtrip(self, test_framer, data, dev_id, res_msg): """Test encode.""" msg = test_framer.encode(data, dev_id, 0) - res_len, res_data = test_framer.decode(msg) + res_len, res_dev_id, _, res_data = test_framer.decode(msg) assert data == res_data - assert dev_id == test_framer.incoming_dev_id + assert dev_id == res_dev_id assert res_len == len(res_msg) + + @pytest.mark.parametrize(("entry"), [FramerType.RTU]) + def test_framer_decode(self, test_framer): + """Test dummy decode.""" + msg = b'' + res_len, _, _, res_data = test_framer.decode(msg) + assert not res_len + assert not res_data + + @pytest.mark.parametrize(("is_server"), [True]) + async def test_processIncomingFrame1(self, test_framer): + """Test processIncomingFrame.""" + msg = b"\x00\x01\x00\x00\x00\x01\xfc\x1b" + _, pdu = test_framer.processIncomingFrame(msg) + assert pdu + + @pytest.mark.parametrize(("is_server"), [True]) + @pytest.mark.parametrize(("entry", "msg"), [ + (FramerType.SOCKET, b"\x00\x01\x12\x34\x00\x06\xff\x02\x01\x02\x00\x08"), + (FramerType.TLS, b"\x02\x01\x02\x00\x08"), + (FramerType.RTU, b"\x00\x01\x00\x00\x00\x01\xfc\x1b"), + (FramerType.ASCII, b":F7031389000A60\r\n"), + ]) + def test_processIncomingFrame2(self, test_framer, msg): + """Test a tcp frame transaction.""" + used_len, pdu = test_framer.processIncomingFrame(msg) + assert pdu + assert used_len == len(msg) + + @pytest.mark.parametrize(("is_server"), [True]) + @pytest.mark.parametrize(("half"), [False, True]) + @pytest.mark.parametrize(("entry", "msg", "dev_id", "tid"), [ + (FramerType.SOCKET, b"\x00\x01\x00\x00\x00\x06\xff\x02\x01\x02\x00\x08", 0xff, 1), + (FramerType.TLS, b"\x02\x01\x02\x00\x08", 0, 0), + (FramerType.RTU, b"\x00\x01\x00\x00\x00\x01\xfc\x1b", 0, 0), + (FramerType.ASCII, b":F7031389000A60\r\n", 0xf7, 0), + ]) + def test_processIncomingFrame_roundtrip(self, entry, test_framer, msg, dev_id, tid, half): + """Test a tcp frame transaction.""" + if half and entry != FramerType.TLS: + data_len = int(len(msg) / 2) + used_len, pdu = test_framer.processIncomingFrame(msg[:data_len]) + assert not pdu + assert not used_len + used_len, result = test_framer.processIncomingFrame(msg) + else: + used_len, result = test_framer.processIncomingFrame(msg) + assert used_len == len(msg) + assert result + assert result.slave_id == dev_id + assert result.transaction_id == tid + assert not test_framer.databuffer + expected = test_framer.encode( + result.function_code.to_bytes(1,'big') + result.encode(), + dev_id, 1) + assert msg == expected + + @pytest.mark.parametrize(("is_server"), [True]) + @pytest.mark.parametrize(("entry", "msg"), [ + (FramerType.SOCKET, b"\x00\x01\x00\x00\x00\x02\xff\x01"), + (FramerType.TLS, b"\x01"), + (FramerType.RTU, b"\xff\x01\x81\x80"), + (FramerType.ASCII, b":FF0100\r\n"), + ]) + def test_framer_encode(self, test_framer, msg): + """Test a tcp frame transaction.""" + with mock.patch.object(ModbusPDU, "encode") as mock_encode: + message = ModbusPDU() + message.setData(0, 0, False) + message.transaction_id = 0x0001 + message.slave_id = 0xFF + message.function_code = 0x01 + mock_encode.return_value = b"" + + actual = test_framer.buildFrame(message) + assert msg == actual + + + +# @pytest.mark.parametrize(("entry"), list(FramerType)) diff --git a/test/framer/test_multidrop.py b/test/framer/test_multidrop.py new file mode 100644 index 000000000..cfbcb96b3 --- /dev/null +++ b/test/framer/test_multidrop.py @@ -0,0 +1,173 @@ +"""Test server working as slave on a multidrop RS485 line.""" +from unittest import mock + +import pytest + +from pymodbus.exceptions import ModbusIOException +from pymodbus.framer import FramerAscii, FramerRTU +from pymodbus.pdu import DecodePDU + + +class TestMultidrop: + """Test that server works on a multidrop line.""" + + good_frame = b"\x02\x03\x00\x01\x00}\xd4\x18" + + @pytest.fixture(name="framer") + def fixture_framer(self): + """Prepare framer.""" + return FramerRTU(DecodePDU(True)) + + @pytest.fixture(name="callback") + def fixture_callback(self): + """Prepare dummy callback.""" + return mock.Mock() + + def test_ok_frame(self, framer): + """Test ok frame.""" + serial_event = self.good_frame + used_len, pdu = framer.processIncomingFrame(serial_event) + assert pdu + assert used_len == len(serial_event) + + def test_ok_2frame(self, framer): + """Test ok frame.""" + serial_event = self.good_frame + self.good_frame + used_len, pdu = framer.processIncomingFrame(serial_event) + assert pdu + assert used_len == len(self.good_frame) + used_len, pdu = framer.processIncomingFrame(serial_event[used_len:]) + assert pdu + assert used_len == len(self.good_frame) + + def test_bad_crc(self, framer): + """Test bad crc.""" + serial_event = b"\x02\x03\x00\x01\x00}\xd4\x19" # Manually mangled crc + _, pdu = framer.processIncomingFrame(serial_event) + assert not pdu + + def test_big_split_response_frame_from_other_id(self, framer): + """Test split response.""" + # This is a single *response* from device id 1 after being queried for 125 holding register values + # Because the response is so long it spans several serial events + framer = FramerRTU(DecodePDU(False)) + serial_events = [ + b'\x01\x03\xfa\x00\x11\x00\x11\x00\x11\x00\x11\x00\x11\x00\x11\x00\x11\x00\x11\x00\x11\x00', + b'\x11\x00\x11\x00\x11\x00\x11\x00\x11\x00\x11\x00\x11\x00\x11\x00\x11\x00\x11\x00\x11\x00', + b'\x11\x00\x11\x00\x11\x00\x11\x00\x11\x00\x11\x00\x11\x00\x11\x00\x11\x00\x11\x00\x11\x00', + b'\x11\x00\x11\x00\x11\x00\x11\x00\x11\x00\x11\x00\x11\x00\x11\x00\x11\x00\x11\x00\x11\x00', + b'\x11\x00\x11\x00\x11\x00\x11\x00\x11\x00\x11\x00\x11\x00\x11\x00\x11\x00\x11\x00\x11\x00', + b'\x11\x00\x11\x00\x11\x00\x11\x00\x11\x00\x11\x00\x11\x00\x11\x00\x11\x00\x11\x00\x11\x00', + b'\x11\x00\x11\x00\x11\x00\x11\x00\x11\x00\x11\x00\x11\x00\x11\x00\x11\x00\x11\x00\x11\x00', + b'\x11\x00\x11\x00\x11\x00\x11\x00\x11\x00\x11\x00\x11\x00\x11\x00\x11\x00\x11\x00\x11\x00', + b'\x11\x00\x11\x00\x11\x00\x11\x00\x11\x00\x11\x00\x11\x00\x11\x00\x11\x00\x11\x00\x11\x00', + b'\x11\x00\x11\x00\x11\x00\x11\x00\x11\x00\x11\x00\x11\x00\x11\x00\x11\x00\x11\x00\x11\x00', + b'\x11\x00\x11\x00\x11\x00\x11\x00\x11\x00\x11\x00\x11\x00\x11\x00\x11\x00\x11\x00\x11\x00', + ] + final = b'\x11\x00\x11\x00\x11\x00\x11\x00\x11\x00\x11\xa5\x8f' + data = b'' + for serial_event in serial_events: + data += serial_event + used_len, pdu = framer.processIncomingFrame(data) + assert not pdu + assert not used_len + used_len, pdu = framer.processIncomingFrame(data + final) + assert pdu + assert used_len == len(data + final) + + def test_split_frame(self, framer): + """Test split frame.""" + used_len, pdu = framer.processIncomingFrame(self.good_frame[:5]) + assert not pdu + assert not used_len + used_len, pdu = framer.processIncomingFrame(self.good_frame) + assert pdu + assert used_len == len(self.good_frame) + + def test_complete_frame_trailing_data_without_id(self, framer): + """Test trailing data.""" + garbage = b"\x05\x04\x03" # without id + serial_event = garbage + self.good_frame + used_len, pdu = framer.processIncomingFrame(serial_event) + assert pdu + assert used_len == len(serial_event) + + def test_complete_frame_trailing_data_with_id(self, framer): + """Test trailing data.""" + garbage = b"\x05\x04\x03\x02\x01\x00" # with id + serial_event = garbage + self.good_frame + used_len, pdu = framer.processIncomingFrame(serial_event) + assert pdu + assert used_len == len(serial_event) + + def test_split_frame_trailing_data_with_id(self, framer): + """Test split frame.""" + garbage = b"ABCDEF" + serial_events = garbage + self.good_frame + used_len, pdu = framer.processIncomingFrame(serial_events[:11]) + assert not pdu + serial_events = serial_events[used_len:] + used_len, pdu = framer.processIncomingFrame(serial_events) + assert pdu + assert used_len == len(serial_events) + + @pytest.mark.parametrize( + ("garbage"), [ + b"\x02\x90\x07", + b"\x02\x10\x07", + b"\x02\x10\x07\x10", + ]) + def test_coincidental(self, garbage, framer): + """Test conincidental.""" + serial_events = garbage + self.good_frame + used_len, pdu = framer.processIncomingFrame(serial_events[:5]) + assert not pdu + serial_events = serial_events[used_len:] + used_len, pdu = framer.processIncomingFrame(serial_events) + assert pdu + assert used_len == len(serial_events) + + def test_wrapped_frame(self, framer): + """Test wrapped frame.""" + garbage = b"\x05\x04\x03\x02\x01\x00" + serial_event = garbage + self.good_frame + garbage + # We probably should not respond in this case; in this case we've likely become desynchronized + # i.e. this probably represents a case where a command came for us, but we didn't get + # to the serial buffer in time (some other co-routine or perhaps a block on the USB bus) + # and the master moved on and queried another device + _, pdu = framer.processIncomingFrame(serial_event) + assert pdu + + def test_frame_with_trailing_data(self, framer): + """Test trailing data.""" + garbage = b"\x05\x04\x03\x02\x01\x00" + serial_event = self.good_frame + garbage + # We should not respond in this case for identical reasons as test_wrapped_frame + _, pdu = framer.processIncomingFrame(serial_event) + assert pdu + + def test_wrong_class(self): + """Test conincidental.""" + + def return_none(_data): + """Return none.""" + return None + + framer = FramerAscii(DecodePDU(True)) + framer.decoder.decode = return_none + with pytest.raises(ModbusIOException): + framer.processIncomingFrame(b':1103007C00026E\r\n') + + def test_getFrameStart(self, framer): + """Test getFrameStart.""" + framer_ok = b"\x02\x03\x00\x01\x00\x7d\xd4\x18" + _, pdu = framer.processIncomingFrame(framer_ok) + assert framer_ok[1:-2] == pdu.function_code.to_bytes(1,'big')+pdu.encode() + + framer_2ok = framer_ok + framer_ok + used_len, pdu = framer.processIncomingFrame(framer_2ok) + assert pdu + framer_2ok = framer_2ok[used_len:] + used_len, pdu = framer.processIncomingFrame(framer_2ok) + assert pdu + assert used_len == len(framer_2ok) diff --git a/test/framers/test_multidrop.py b/test/framers/test_multidrop.py deleted file mode 100644 index 1baa87604..000000000 --- a/test/framers/test_multidrop.py +++ /dev/null @@ -1,179 +0,0 @@ -"""Test server working as slave on a multidrop RS485 line.""" -from unittest import mock - -import pytest - -from pymodbus.framer import FramerRTU -from pymodbus.server.async_io import ServerDecoder - - -class TestMultidrop: - """Test that server works on a multidrop line.""" - - good_frame = b"\x02\x03\x00\x01\x00}\xd4\x18" - - @pytest.fixture(name="framer") - def fixture_framer(self): - """Prepare framer.""" - return FramerRTU(ServerDecoder(), [2]) - - @pytest.fixture(name="callback") - def fixture_callback(self): - """Prepare dummy callback.""" - return mock.Mock() - - def test_ok_frame(self, framer, callback): - """Test ok frame.""" - serial_event = self.good_frame - framer.processIncomingPacket(serial_event, callback) - callback.assert_called_once() - - @pytest.mark.skip - def test_ok_2frame(self, framer, callback): # pragma: no cover - """Test ok frame.""" - serial_event = self.good_frame + self.good_frame - framer.processIncomingPacket(serial_event, callback) - assert callback.call_count == 2 - - def test_bad_crc(self, framer, callback): - """Test bad crc.""" - serial_event = b"\x02\x03\x00\x01\x00}\xd4\x19" # Manually mangled crc - framer.processIncomingPacket(serial_event, callback) - callback.assert_not_called() - - def test_wrong_id(self, framer, callback): - """Test frame wrong id.""" - serial_event = b"\x01\x03\x00\x01\x00}\xd4+" # Frame with good CRC but other id - framer.processIncomingPacket(serial_event, callback) - callback.assert_not_called() - - def test_big_split_response_frame_from_other_id(self, framer, callback): - """Test split response.""" - # This is a single *response* from device id 1 after being queried for 125 holding register values - # Because the response is so long it spans several serial events - serial_events = [ - b"\x01\x03\xfa\xc4y\xc0\x00\xc4y\xc0\x00\xc4y\xc0\x00\xc4y\xc0\x00\xc4y\xc0\x00Dz\x00\x00C\x96\x00\x00", - b"?\x05\x1e\xb8DH\x00\x00D\x96\x00\x00D\xfa\x00\x00DH\x00\x00D\x96\x00\x00D\xfa\x00\x00DH\x00", - b"\x00D\x96\x00\x00D\xfa\x00\x00B\x96\x00\x00B\xb4\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00", - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00", - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00", - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00", - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00", - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00", - b"\x00\x00\x00\x00\x00\x00\x00N,", - ] - for serial_event in serial_events: - framer.processIncomingPacket(serial_event, callback) - callback.assert_not_called() - - @pytest.mark.skip - def test_split_frame(self, framer, callback): # pragma: no cover - """Test split frame.""" - serial_events = [self.good_frame[:5], self.good_frame[5:]] - for serial_event in serial_events: - framer.processIncomingPacket(serial_event, callback) - callback.assert_called_once() - - def test_complete_frame_trailing_data_without_id(self, framer, callback): - """Test trailing data.""" - garbage = b"\x05\x04\x03" # without id - serial_event = garbage + self.good_frame - framer.processIncomingPacket(serial_event, callback) - callback.assert_called_once() - - def test_complete_frame_trailing_data_with_id(self, framer, callback): - """Test trailing data.""" - garbage = b"\x05\x04\x03\x02\x01\x00" # with id - serial_event = garbage + self.good_frame - framer.processIncomingPacket(serial_event, callback) - callback.assert_called_once() - - @pytest.mark.skip - def test_split_frame_trailing_data_with_id(self, framer, callback): # pragma: no cover - """Test split frame.""" - garbage = b"\x05\x04\x03\x02\x01\x00" - serial_events = [garbage + self.good_frame[:5], self.good_frame[5:]] - for serial_event in serial_events: - framer.processIncomingPacket(serial_event, callback) - callback.assert_called_once() - - @pytest.mark.skip - def test_coincidental_1(self, framer, callback): # pragma: no cover - """Test conincidental.""" - garbage = b"\x02\x90\x07" - serial_events = [garbage, self.good_frame[:5], self.good_frame[5:]] - for serial_event in serial_events: - framer.processIncomingPacket(serial_event, callback) - callback.assert_called_once() - - @pytest.mark.skip - def test_coincidental_2(self, framer, callback): # pragma: no cover - """Test conincidental.""" - garbage = b"\x02\x10\x07" - serial_events = [garbage, self.good_frame[:5], self.good_frame[5:]] - for serial_event in serial_events: - framer.processIncomingPacket(serial_event, callback) - callback.assert_called_once() - - @pytest.mark.skip - def test_coincidental_3(self, framer, callback): # pragma: no cover - """Test conincidental.""" - garbage = b"\x02\x10\x07\x10" - serial_events = [garbage, self.good_frame[:5], self.good_frame[5:]] - for serial_event in serial_events: - framer.processIncomingPacket(serial_event, callback) - callback.assert_called_once() - - def test_wrapped_frame(self, framer, callback): - """Test wrapped frame.""" - garbage = b"\x05\x04\x03\x02\x01\x00" - serial_event = garbage + self.good_frame + garbage - framer.processIncomingPacket(serial_event, callback) - - # We probably should not respond in this case; in this case we've likely become desynchronized - # i.e. this probably represents a case where a command came for us, but we didn't get - # to the serial buffer in time (some other co-routine or perhaps a block on the USB bus) - # and the master moved on and queried another device - callback.assert_called_once() - - def test_frame_with_trailing_data(self, framer, callback): - """Test trailing data.""" - garbage = b"\x05\x04\x03\x02\x01\x00" - serial_event = self.good_frame + garbage - framer.processIncomingPacket(serial_event, callback) - - # We should not respond in this case for identical reasons as test_wrapped_frame - callback.assert_called_once() - - @pytest.mark.skip - def test_getFrameStart(self, framer): # pragma: no cover - """Test getFrameStart.""" - result = None - count = 0 - def test_callback(data): - """Check callback.""" - nonlocal result, count - count += 1 - result = data.function_code.to_bytes(1,'big')+data.encode() - - framer_ok = b"\x02\x03\x00\x01\x00\x7d\xd4\x18" - framer.processIncomingPacket(framer_ok, test_callback) - assert framer_ok[1:-2] == result - - count = 0 - framer_2ok = framer_ok + framer_ok - framer.processIncomingPacket(framer_2ok, test_callback) - assert count == 2 - assert not framer._buffer # pylint: disable=protected-access - - framer._buffer = framer_ok[:2] # pylint: disable=protected-access - framer.processIncomingPacket(b'', test_callback) - assert framer_ok[:2] == framer._buffer # pylint: disable=protected-access - - framer._buffer = framer_ok[:3] # pylint: disable=protected-access - framer.processIncomingPacket(b'', test_callback) - assert framer_ok[:3] == framer._buffer # pylint: disable=protected-access - - framer_ok = b"\xF0\x03\x00\x01\x00}\xd4\x18" - framer.processIncomingPacket(framer_ok, test_callback) - assert framer._buffer == framer_ok[-3:] # pylint: disable=protected-access diff --git a/test/pdu/__init__.py b/test/pdu/__init__.py new file mode 100644 index 000000000..6120f1a8f --- /dev/null +++ b/test/pdu/__init__.py @@ -0,0 +1 @@ +"""Test of message layer.""" diff --git a/test/sub_function_codes/test_bit_read_messages.py b/test/pdu/test_bit_read_messages.py similarity index 91% rename from test/sub_function_codes/test_bit_read_messages.py rename to test/pdu/test_bit_read_messages.py index 85f807e37..b93c7b193 100644 --- a/test/sub_function_codes/test_bit_read_messages.py +++ b/test/pdu/test_bit_read_messages.py @@ -15,7 +15,8 @@ ReadCoilsRequest, ReadDiscreteInputsRequest, ) -from test.conftest import MockContext + +from ..conftest import MockContext res = [True] * 21 @@ -85,7 +86,7 @@ def test_bit_read_base_requests(self): for request, expected in iter(messages.items()): assert request.encode() == expected - async def test_bit_read_message_execute_value_errors(self): + async def test_bit_read_message_update_datastore_value_errors(self): """Test bit read request encoding.""" context = MockContext() requests = [ @@ -93,10 +94,10 @@ async def test_bit_read_message_execute_value_errors(self): ReadDiscreteInputsRequest(1, 0x800, 0, 0, False), ] for request in requests: - result = await request.execute(context) + result = await request.update_datastore(context) assert ModbusExceptions.IllegalValue == result.exception_code - async def test_bit_read_message_execute_address_errors(self): + async def test_bit_read_message_update_datastore_address_errors(self): """Test bit read request encoding.""" context = MockContext() requests = [ @@ -104,10 +105,10 @@ async def test_bit_read_message_execute_address_errors(self): ReadDiscreteInputsRequest(1, 5, 0, 0, False), ] for request in requests: - result = await request.execute(context) + result = await request.update_datastore(context) assert ModbusExceptions.IllegalAddress == result.exception_code - async def test_bit_read_message_execute_success(self): + async def test_bit_read_message_update_datastore_success(self): """Test bit read request encoding.""" context = MockContext() context.validate = lambda a, b, c: True @@ -116,7 +117,7 @@ async def test_bit_read_message_execute_success(self): ReadDiscreteInputsRequest(1, 5, 0, False), ] for request in requests: - result = await request.execute(context) + result = await request.update_datastore(context) assert result.bits == [True] * 5 def test_bit_read_message_get_response_pdu(self): diff --git a/test/sub_function_codes/test_bit_write_messages.py b/test/pdu/test_bit_write_messages.py similarity index 89% rename from test/sub_function_codes/test_bit_write_messages.py rename to test/pdu/test_bit_write_messages.py index 54cae8b41..931d42637 100644 --- a/test/sub_function_codes/test_bit_write_messages.py +++ b/test/pdu/test_bit_write_messages.py @@ -13,7 +13,8 @@ WriteSingleCoilRequest, WriteSingleCoilResponse, ) -from test.conftest import FakeList, MockContext + +from ..conftest import FakeList, MockContext # ---------------------------------------------------------------------------# @@ -80,45 +81,45 @@ def test_write_single_coil_request_encode(self): request = WriteSingleCoilRequest(1, False) assert request.encode() == b"\x00\x01\x00\x00" - async def test_write_single_coil_execute(self): + async def test_write_single_coil_update_datastore(self): """Test write single coil.""" context = MockContext(False, default=True) request = WriteSingleCoilRequest(2, True) - result = await request.execute(context) + result = await request.update_datastore(context) assert result.exception_code == ModbusExceptions.IllegalAddress context.valid = True - result = await request.execute(context) + result = await request.update_datastore(context) assert result.encode() == b"\x00\x02\xff\x00" context = MockContext(True, default=False) request = WriteSingleCoilRequest(2, False) - result = await request.execute(context) + result = await request.update_datastore(context) assert result.encode() == b"\x00\x02\x00\x00" - async def test_write_multiple_coils_execute(self): + async def test_write_multiple_coils_update_datastore(self): """Test write multiple coils.""" context = MockContext(False) # too many values request = WriteMultipleCoilsRequest(2, FakeList(0x123456)) - result = await request.execute(context) + result = await request.update_datastore(context) assert result.exception_code == ModbusExceptions.IllegalValue # bad byte count request = WriteMultipleCoilsRequest(2, [0x00] * 4) request.byte_count = 0x00 - result = await request.execute(context) + result = await request.update_datastore(context) assert result.exception_code == ModbusExceptions.IllegalValue # does not validate context.valid = False request = WriteMultipleCoilsRequest(2, [0x00] * 4) - result = await request.execute(context) + result = await request.update_datastore(context) assert result.exception_code == ModbusExceptions.IllegalAddress # validated request context.valid = True - result = await request.execute(context) + result = await request.update_datastore(context) assert result.encode() == b"\x00\x02\x00\x04" def test_write_multiple_coils_response(self): diff --git a/test/pdu/test_decoders.py b/test/pdu/test_decoders.py new file mode 100644 index 000000000..7e571859a --- /dev/null +++ b/test/pdu/test_decoders.py @@ -0,0 +1,162 @@ +"""Test factory.""" +import pytest + +from pymodbus.exceptions import MessageRegisterException +from pymodbus.pdu import ModbusPDU +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 slave 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 slave 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"), # slave 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.""" + assert frame + assert self.client.lookupPduClass(code) + + @pytest.mark.parametrize(("code", "frame"), list(requests)) + def test_server_lookup(self, code, frame): + """Test lookup for requests.""" + assert frame + assert self.server.lookupPduClass(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.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.function_code == 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.""" + assert not decoder.decode(frame) + + def test_decode_unknown_sub(self): + """Test for unknown sub code.""" + assert self.client.decode(b"\x08\x00\xF0\xF0\x00") + + @pytest.mark.parametrize(("decoder"), [server, client]) + def test_register_custom_request(self, decoder): + """Test server register custom request.""" + + class CustomRequestResponse(ModbusPDU): + """Custom request.""" + + function_code = 0xF0 + + def encode(self): + """Encode.""" + + def decode(self, _data): + """Decode.""" + + class NoCustomRequestResponse: + """Custom request.""" + + function_code = 0xF0 + + def encode(self): + """Encode.""" + + def decode(self, _data): + """Decode.""" + + decoder.register(CustomRequestResponse) + assert decoder.lookupPduClass(CustomRequestResponse.function_code) + CustomRequestResponse.sub_function_code = 0xF7 + decoder.register(CustomRequestResponse) + CustomRequestResponse.sub_function_code = 0xF4 + decoder.register(CustomRequestResponse) + assert self.server.lookupPduClass(CustomRequestResponse.function_code) + with pytest.raises(MessageRegisterException): + decoder.register(NoCustomRequestResponse) diff --git a/test/sub_function_codes/test_diag_messages.py b/test/pdu/test_diag_messages.py similarity index 94% rename from test/sub_function_codes/test_diag_messages.py rename to test/pdu/test_diag_messages.py index d5caa11b6..d20eaf7df 100644 --- a/test/sub_function_codes/test_diag_messages.py +++ b/test/pdu/test_diag_messages.py @@ -142,7 +142,7 @@ async def test_diagnostic_simple_requests(self): request = DiagnosticStatusSimpleRequest(b"\x12\x34") request.sub_function_code = 0x1234 with pytest.raises(NotImplementedException): - await request.execute() + await request.update_datastore() assert request.encode() == b"\x12\x34\x12\x34" DiagnosticStatusSimpleResponse() @@ -158,11 +158,11 @@ def test_diagnostic_requests_encode(self): for msg, enc, _ in self.requests: assert msg().encode() == enc - async def test_diagnostic_execute(self): + async def test_diagnostic_update_datastore(self): """Testing diagnostic message execution.""" - for message, encoded, executed in self.requests: - encoded = (await message().execute()).encode() - assert encoded == executed + for message, encoded, update_datastored in self.requests: + encoded = (await message().update_datastore()).encode() + assert encoded == update_datastored def test_return_query_data_request(self): """Testing diagnostic message execution.""" @@ -190,13 +190,13 @@ def test_restart_communications_option(self): response = RestartCommunicationsOptionResponse(False) assert response.encode() == b"\x00\x01\x00\x00" - async def test_get_clear_modbus_plus_request_execute(self): + async def test_get_clear_modbus_plus_request_update_datastore(self): """Testing diagnostic message execution.""" request = GetClearModbusPlusRequest(data=ModbusPlusOperation.CLEAR_STATISTICS) - response = await request.execute() + response = await request.update_datastore() assert response.message == ModbusPlusOperation.CLEAR_STATISTICS request = GetClearModbusPlusRequest(data=ModbusPlusOperation.GET_STATISTICS) - response = await request.execute() + response = await request.update_datastore() resp = [ModbusPlusOperation.GET_STATISTICS] assert response.message == resp + [0x00] * 55 diff --git a/test/sub_current/test_file_message.py b/test/pdu/test_file_message.py similarity index 96% rename from test/sub_current/test_file_message.py rename to test/pdu/test_file_message.py index ab2aae876..fb18e96f3 100644 --- a/test/sub_current/test_file_message.py +++ b/test/pdu/test_file_message.py @@ -45,15 +45,15 @@ def test_read_fifo_queue_request(self): """Test basic bit message encoding/decoding.""" context = MockContext() handle = ReadFifoQueueRequest(0x1234) - result = handle.execute(context) + result = handle.update_datastore(context) assert isinstance(result, ReadFifoQueueResponse) handle.address = -1 - result = handle.execute(context) + result = handle.update_datastore(context) assert ModbusExceptions.IllegalValue == result.exception_code handle.values = [0x00] * 33 - result = handle.execute(context) + result = handle.update_datastore(context) assert ModbusExceptions.IllegalValue == result.exception_code def test_read_fifo_queue_request_error(self): @@ -61,7 +61,7 @@ def test_read_fifo_queue_request_error(self): context = MockContext() handle = ReadFifoQueueRequest(0x1234) handle.values = [0x00] * 32 - result = handle.execute(context) + result = handle.update_datastore(context) assert result.function_code == 0x98 def test_read_fifo_queue_response_encode(self): @@ -148,10 +148,10 @@ def test_read_file_record_request_rtu_frame_size(self): size = handle.calculateRtuFrameSize(request) assert size == 0x0E + 5 - def test_read_file_record_request_execute(self): + def test_read_file_record_request_update_datastore(self): """Test basic bit message encoding/decoding.""" handle = ReadFileRecordRequest() - result = handle.execute(None) + result = handle.update_datastore(None) assert isinstance(result, ReadFileRecordResponse) # -----------------------------------------------------------------------# @@ -221,10 +221,10 @@ def test_write_file_record_request_rtu_frame_size(self): size = handle.calculateRtuFrameSize(request) assert size == 0x0D + 5 - def test_write_file_record_request_execute(self): + def test_write_file_record_request_update_datastore(self): """Test basic bit message encoding/decoding.""" handle = WriteFileRecordRequest() - result = handle.execute(None) + result = handle.update_datastore(None) assert isinstance(result, WriteFileRecordResponse) # -----------------------------------------------------------------------# diff --git a/test/sub_function_codes/test_mei_messages.py b/test/pdu/test_mei_messages.py similarity index 93% rename from test/sub_function_codes/test_mei_messages.py rename to test/pdu/test_mei_messages.py index 371b23e17..2c81a8538 100644 --- a/test/sub_function_codes/test_mei_messages.py +++ b/test/pdu/test_mei_messages.py @@ -53,7 +53,7 @@ async def test_read_device_information_request(self): control.Identity.update({0x81: ["Test", "Repeated"]}) handle = ReadDeviceInformationRequest() - result = await handle.execute(context) + result = await handle.update_datastore(context) assert isinstance(result, ReadDeviceInformationResponse) assert result.information[0x00] == "Company" assert result.information[0x01] == "Product" @@ -64,20 +64,20 @@ async def test_read_device_information_request(self): handle = ReadDeviceInformationRequest( read_code=DeviceInformation.EXTENDED, object_id=0x80 ) - result = await handle.execute(context) + result = await handle.update_datastore(context) assert result.information[0x81] == ["Test", "Repeated"] async def test_read_device_information_request_error(self): """Test basic bit message encoding/decoding.""" handle = ReadDeviceInformationRequest() handle.read_code = -1 - assert (await handle.execute(None)).function_code == 0xAB + assert (await handle.update_datastore(None)).function_code == 0xAB handle.read_code = 0x05 - assert (await handle.execute(None)).function_code == 0xAB + assert (await handle.update_datastore(None)).function_code == 0xAB handle.object_id = -1 - assert (await handle.execute(None)).function_code == 0xAB + assert (await handle.update_datastore(None)).function_code == 0xAB handle.object_id = 0x100 - assert (await handle.execute(None)).function_code == 0xAB + assert (await handle.update_datastore(None)).function_code == 0xAB def test_read_device_information_encode(self): """Test that the read fifo queue response can encode.""" diff --git a/test/sub_function_codes/test_other_messages.py b/test/pdu/test_other_messages.py similarity index 91% rename from test/sub_function_codes/test_other_messages.py rename to test/pdu/test_other_messages.py index f5c51d6d4..9fe83bdbc 100644 --- a/test/sub_function_codes/test_other_messages.py +++ b/test/pdu/test_other_messages.py @@ -33,7 +33,7 @@ async def test_read_exception_status(self): request = pymodbus_message.ReadExceptionStatusRequest() request.decode(b"\x12") assert not request.encode() - assert (await request.execute()).function_code == 0x07 + assert (await request.update_datastore()).function_code == 0x07 response = pymodbus_message.ReadExceptionStatusResponse(0x12) assert response.encode() == b"\x12" @@ -45,7 +45,7 @@ async def test_get_comm_event_counter(self): request = pymodbus_message.GetCommEventCounterRequest() request.decode(b"\x12") assert not request.encode() - assert (await request.execute()).function_code == 0x0B + assert (await request.update_datastore()).function_code == 0x0B response = pymodbus_message.GetCommEventCounterResponse(0x12) assert response.encode() == b"\x00\x00\x00\x12" @@ -61,7 +61,7 @@ async def test_get_comm_event_log(self): request = pymodbus_message.GetCommEventLogRequest() request.decode(b"\x12") assert not request.encode() - assert (await request.execute()).function_code == 0x0C + assert (await request.update_datastore()).function_code == 0x0C response = pymodbus_message.GetCommEventLogResponse() assert response.encode() == b"\x06\x00\x00\x00\x00\x00\x00" @@ -103,7 +103,7 @@ async def test_report_slave_id_request(self): expected_identity = "-".join(identity.values()).encode() request = pymodbus_message.ReportSlaveIdRequest() - response = await request.execute() + response = await request.update_datastore() assert response.identifier == expected_identity # Change to byte strings and test again (final result should be the same) @@ -121,7 +121,7 @@ async def test_report_slave_id_request(self): dif.get.return_value = identity request = pymodbus_message.ReportSlaveIdRequest() - response = await request.execute() + response = await request.update_datastore() assert response.identifier == expected_identity async def test_report_slave_id(self): @@ -131,10 +131,10 @@ async def test_report_slave_id(self): request = pymodbus_message.ReportSlaveIdRequest() request.decode(b"\x12") assert not request.encode() - assert (await request.execute()).function_code == 0x11 + assert (await request.update_datastore()).function_code == 0x11 response = pymodbus_message.ReportSlaveIdResponse( - (await request.execute()).identifier, True + (await request.update_datastore()).identifier, True ) assert response.encode() == b"\tPymodbus\xff" diff --git a/test/pdu/test_pdu.py b/test/pdu/test_pdu.py new file mode 100644 index 000000000..9b9344218 --- /dev/null +++ b/test/pdu/test_pdu.py @@ -0,0 +1,72 @@ +"""Test pdu.""" +import pytest + +from pymodbus.exceptions import NotImplementedException +from pymodbus.pdu import ( + ExceptionResponse, + ModbusExceptions, + ModbusPDU, +) + + +class TestPdu: + """Test modbus PDU.""" + + exception = ExceptionResponse(1, 1, 0, 0, False) + + async def test_error_methods(self): + """Test all error methods.""" + result = self.exception.encode() + self.exception.decode(result) + assert result == b"\x01" + assert self.exception.exception_code == 1 + + async def test_get_pdu_size(self): + """Test get pdu size.""" + assert not self.exception.get_response_pdu_size() + + async def test_is_error(self): + """Test is_error.""" + assert self.exception.isError() + + def test_request_exception(self): + """Test request exception.""" + request = ModbusPDU() + request.setData(0, 0, False) + request.function_code = 1 + errors = {ModbusExceptions.decode(c): c for c in range(1, 20)} + for error, code in iter(errors.items()): + result = request.doException(code) + assert str(result) == f"Exception Response(129, 1, {error})" + + def test_calculate_rtu_frame_size(self): + """Test the calculation of Modbus frame sizes.""" + with pytest.raises(NotImplementedException): + ModbusPDU.calculateRtuFrameSize(b"") + ModbusPDU._rtu_frame_size = 5 # pylint: disable=protected-access + assert ModbusPDU.calculateRtuFrameSize(b"") == 5 + ModbusPDU._rtu_frame_size = None # pylint: disable=protected-access + + ModbusPDU._rtu_byte_count_pos = 2 # pylint: disable=protected-access + assert ( + ModbusPDU.calculateRtuFrameSize( + b"\x11\x01\x05\xcd\x6b\xb2\x0e\x1b\x45\xe6" + ) + == 0x05 + 5 + ) + assert not ModbusPDU.calculateRtuFrameSize(b"\x11") + ModbusPDU._rtu_byte_count_pos = None # pylint: disable=protected-access + + with pytest.raises(NotImplementedException): + ModbusPDU.calculateRtuFrameSize(b"") + ModbusPDU._rtu_frame_size = 12 # pylint: disable=protected-access + assert ModbusPDU.calculateRtuFrameSize(b"") == 12 + ModbusPDU._rtu_frame_size = None # pylint: disable=protected-access + ModbusPDU._rtu_byte_count_pos = 2 # pylint: disable=protected-access + assert ( + ModbusPDU.calculateRtuFrameSize( + b"\x11\x01\x05\xcd\x6b\xb2\x0e\x1b\x45\xe6" + ) + == 0x05 + 5 + ) + ModbusPDU._rtu_byte_count_pos = None # pylint: disable=protected-access diff --git a/test/pdu/test_pdutype.py b/test/pdu/test_pdutype.py new file mode 100644 index 000000000..38a16cfb9 --- /dev/null +++ b/test/pdu/test_pdutype.py @@ -0,0 +1,145 @@ +"""Test pdu.""" +import pytest + +import pymodbus.pdu.bit_read_message as bit_r_msg +import pymodbus.pdu.bit_write_message as bit_w_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_read_message as reg_r_msg +import pymodbus.pdu.register_write_message as reg_w_msg + + +class TestPduType: + """Test all PDU types requests/responses.""" + + requests = [ + (bit_r_msg.ReadCoilsRequest, {"address": 117, "count": 3}, b''), + (bit_r_msg.ReadDiscreteInputsRequest, {"address": 117, "count": 3}, b''), + (bit_w_msg.WriteSingleCoilRequest, {"address": 117, "value": True}, b''), + (bit_w_msg.WriteMultipleCoilsRequest, {"address": 117, "values": [True, False, True]}, b''), + (diag_msg.DiagnosticStatusRequest, {}, b''), + (diag_msg.DiagnosticStatusSimpleRequest, {"data": 0x1010}, b''), + (diag_msg.ReturnQueryDataRequest, {"message": b'\x10\x01'}, b''), + (diag_msg.RestartCommunicationsOptionRequest, {"toggle": True}, b''), + (diag_msg.ReturnDiagnosticRegisterRequest, {"data": 0x1010}, b''), + (diag_msg.ChangeAsciiInputDelimiterRequest, {"data": 0x1010}, b''), + (diag_msg.ForceListenOnlyModeRequest, {}, b''), + (diag_msg.ClearCountersRequest, {"data": 0x1010}, b''), + (diag_msg.ReturnBusMessageCountRequest, {"data": 0x1010}, b''), + (diag_msg.ReturnBusCommunicationErrorCountRequest, {"data": 0x1010}, b''), + (diag_msg.ReturnBusExceptionErrorCountRequest, {"data": 0x1010}, b''), + (diag_msg.ReturnSlaveMessageCountRequest, {"data": 0x1010}, b''), + (diag_msg.ReturnSlaveNoResponseCountRequest, {"data": 0x1010}, b''), + (diag_msg.ReturnSlaveNAKCountRequest, {"data": 0x1010}, b''), + (diag_msg.ReturnSlaveBusyCountRequest, {"data": 0x1010}, b''), + (diag_msg.ReturnSlaveBusCharacterOverrunCountRequest, {"data": 0x1010}, b''), + (diag_msg.ReturnIopOverrunCountRequest, {"data": 0x1010}, b''), + (diag_msg.ClearOverrunCountRequest, {"data": 0x1010}, b''), + (diag_msg.GetClearModbusPlusRequest, {"data": 0x1010}, b''), + (file_msg.ReadFileRecordRequest, {"records": [file_msg.FileRecord(), file_msg.FileRecord()]}, b''), + (file_msg.WriteFileRecordRequest, {"records": [file_msg.FileRecord(), file_msg.FileRecord()]}, b''), + (file_msg.ReadFifoQueueRequest, {"address": 117}, b''), + (mei_msg.ReadDeviceInformationRequest, {"read_code": 0x17, "object_id": 0x29}, b''), + (o_msg.ReadExceptionStatusRequest, {}, b''), + (o_msg.GetCommEventCounterRequest, {}, b''), + (o_msg.GetCommEventLogRequest, {}, b''), + (o_msg.ReportSlaveIdRequest, {}, b''), + (reg_r_msg.ReadHoldingRegistersRequest, {"address": 117, "count": 3}, b''), + (reg_r_msg.ReadInputRegistersRequest, {"address": 117, "count": 3}, b''), + (reg_r_msg.ReadWriteMultipleRegistersRequest, {"read_address": 17, "read_count": 2, "write_address": 25, "write_registers": [111, 112]}, b''), + (reg_w_msg.WriteMultipleRegistersRequest, {"address": 117, "values": [111, 121, 131]}, b''), + (reg_w_msg.WriteSingleRegisterRequest, {"address": 117, "value": 112}, b''), + (reg_w_msg.MaskWriteRegisterRequest, {"address": 0x0104, "and_mask": 0xE1D2, "or_mask": 0x1234}, b''), + ] + + responses = [ + (bit_r_msg.ReadCoilsResponse, {"values": [3, 17]}, b''), + (bit_r_msg.ReadDiscreteInputsResponse, {"values": [3, 17]}, b''), + (bit_w_msg.WriteSingleCoilResponse, {"address": 117, "value": True}, b''), + (bit_w_msg.WriteMultipleCoilsResponse, {"address": 117, "count": 3}, b''), + (diag_msg.DiagnosticStatusResponse, {}, b''), + (diag_msg.DiagnosticStatusSimpleResponse, {"data": 0x1010}, b''), + (diag_msg.ReturnQueryDataResponse, {"message": b'AB'}, b''), + (diag_msg.RestartCommunicationsOptionResponse, {"toggle": True}, b''), + (diag_msg.ReturnDiagnosticRegisterResponse, {"data": 0x1010}, b''), + (diag_msg.ChangeAsciiInputDelimiterResponse, {"data": 0x1010}, b''), + (diag_msg.ForceListenOnlyModeResponse, {}, b''), + (diag_msg.ClearCountersResponse, {"data": 0x1010}, b''), + (diag_msg.ReturnBusMessageCountResponse, {"data": 0x1010}, b''), + (diag_msg.ReturnBusCommunicationErrorCountResponse, {"data": 0x1010}, b''), + (diag_msg.ReturnBusExceptionErrorCountResponse, {"data": 0x1010}, b''), + (diag_msg.ReturnSlaveMessageCountResponse, {"data": 0x1010}, b''), + (diag_msg.ReturnSlaveNoResponseCountResponse, {"data": 0x1010}, b''), + (diag_msg.ReturnSlaveNAKCountResponse, {"data": 0x1010}, b''), + (diag_msg.ReturnSlaveBusyCountResponse, {"data": 0x1010}, b''), + (diag_msg.ReturnSlaveBusCharacterOverrunCountResponse, {"data": 0x1010}, b''), + (diag_msg.ReturnIopOverrunCountResponse, {"data": 0x1010}, b''), + (diag_msg.ClearOverrunCountResponse, {"data": 0x1010}, b''), + (diag_msg.GetClearModbusPlusResponse, {"data": 0x1010}, b''), + (file_msg.ReadFileRecordResponse, {"records": [file_msg.FileRecord(), file_msg.FileRecord()]}, b''), + (file_msg.WriteFileRecordResponse, {"records": [file_msg.FileRecord(), file_msg.FileRecord()]}, b''), + (file_msg.ReadFifoQueueResponse, {"values": [123, 456]}, b''), + (mei_msg.ReadDeviceInformationResponse, {"read_code": 0x17}, b''), + (o_msg.ReadExceptionStatusResponse, {"status": 0x23}, b''), + (o_msg.GetCommEventCounterResponse, {"count": 123}, b''), + (o_msg.GetCommEventLogResponse, {"status": True, "message_count": 12, "event_count": 7, "events": [12, 14]}, b''), + (o_msg.ReportSlaveIdResponse, {"identifier": b'\x12', "status": True}, b''), + (reg_r_msg.ReadHoldingRegistersResponse, {"values": [3, 17]}, b''), + (reg_r_msg.ReadInputRegistersResponse, {"values": [3, 17]}, b''), + (reg_r_msg.ReadWriteMultipleRegistersResponse, {"values": [1, 2]}, b''), + (reg_w_msg.WriteSingleRegisterResponse, {"address": 117, "value": 112}, b''), + (reg_w_msg.WriteMultipleRegistersResponse, {"address": 117, "count": 3}, b''), + (reg_w_msg.MaskWriteRegisterResponse, {"address": 0x0104, "and_mask": 0xE1D2, "or_mask": 0x1234}, b''), + ] + + + @pytest.mark.parametrize(("pdutype", "kwargs", "framer"), requests) + @pytest.mark.usefixtures("kwargs", "framer") + def test_pdu_instance(self, pdutype): + """Test that all PDU types can be created.""" + pdu = pdutype() + assert pdu + assert str(pdu) + + @pytest.mark.parametrize(("pdutype", "kwargs", "framer"), requests + responses) + @pytest.mark.usefixtures("framer") + def test_pdu_instance_args(self, pdutype, kwargs): + """Test that all PDU types can be created.""" + pdu = pdutype(**kwargs) + assert pdu + assert str(pdu) + + @pytest.mark.parametrize(("pdutype", "kwargs", "framer"), requests + responses) + @pytest.mark.usefixtures("framer") + def test_pdu_instance_extras(self, pdutype, kwargs): + """Test that all PDU types can be created.""" + tid = 9112 + slave_id = 63 + pdu = pdutype(transaction=tid, slave=slave_id, **kwargs) + assert pdu + assert str(pdu) + assert pdu.slave_id == slave_id + assert pdu.transaction_id == tid + assert pdu.function_code > 0 + + @pytest.mark.parametrize(("pdutype", "kwargs", "framer"), requests + responses) + @pytest.mark.usefixtures("framer") + def test_pdu_instance_encode(self, pdutype, kwargs): + """Test that all PDU types can be created.""" + pdutype(**kwargs).encode() + # Fix Check frame against test case + + @pytest.mark.parametrize(("pdutype", "kwargs", "framer"), requests + responses) + @pytest.mark.usefixtures("framer") + def test_pdu_special_methods(self, pdutype, kwargs): + """Test that all PDU types can be created.""" + pdu = pdutype(**kwargs) + pdu.get_response_pdu_size() + if hasattr(pdu, "setBit"): + pdu.setBit(0) + if hasattr(pdu, "resetBit"): + pdu.resetBit(0) + if hasattr(pdu, "getBit"): + pdu.getBit(0) diff --git a/test/sub_function_codes/test_register_read_messages.py b/test/pdu/test_register_read_messages.py similarity index 92% rename from test/sub_function_codes/test_register_read_messages.py rename to test/pdu/test_register_read_messages.py index ce5504e65..b25c2b122 100644 --- a/test/sub_function_codes/test_register_read_messages.py +++ b/test/pdu/test_register_read_messages.py @@ -10,7 +10,8 @@ ReadWriteMultipleRegistersRequest, ReadWriteMultipleRegistersResponse, ) -from test.conftest import FakeList, MockContext + +from ..conftest import FakeList, MockContext TEST_MESSAGE = b"\x06\x00\x0a\x00\x0b\x00\x0c" @@ -103,7 +104,7 @@ async def test_register_read_requests_count_errors(self): ), ] for request in requests: - result = await request.execute(None) + result = await request.update_datastore(None) assert ModbusExceptions.IllegalValue == result.exception_code async def test_register_read_requests_validate_errors(self): @@ -119,10 +120,10 @@ async def test_register_read_requests_validate_errors(self): # ReadWriteMultipleRegistersRequest(1,5,-1,5), ] for request in requests: - result = await request.execute(context) + result = await request.update_datastore(context) assert ModbusExceptions.IllegalAddress == result.exception_code - async def test_register_read_requests_execute(self): + async def test_register_read_requests_update_datastore(self): """This tests that the register request messages. will break on counts that are out of range @@ -133,7 +134,7 @@ async def test_register_read_requests_execute(self): ReadInputRegistersRequest(-1, 5), ] for request in requests: - response = await request.execute(context) + response = await request.update_datastore(context) assert request.function_code == response.function_code async def test_read_write_multiple_registers_request(self): @@ -142,7 +143,7 @@ async def test_read_write_multiple_registers_request(self): request = ReadWriteMultipleRegistersRequest( read_address=1, read_count=10, write_address=1, write_registers=[0x00] ) - response = await request.execute(context) + response = await request.update_datastore(context) assert request.function_code == response.function_code async def test_read_write_multiple_registers_validate(self): @@ -152,15 +153,15 @@ async def test_read_write_multiple_registers_validate(self): request = ReadWriteMultipleRegistersRequest( read_address=1, read_count=10, write_address=2, write_registers=[0x00] ) - response = await request.execute(context) + response = await request.update_datastore(context) assert response.exception_code == ModbusExceptions.IllegalAddress context.validate = lambda f, a, c: a == 2 - response = await request.execute(context) + response = await request.update_datastore(context) assert response.exception_code == ModbusExceptions.IllegalAddress request.write_byte_count = 0x100 - response = await request.execute(context) + response = await request.update_datastore(context) assert response.exception_code == ModbusExceptions.IllegalValue def test_read_write_multiple_registers_request_decode(self): diff --git a/test/sub_function_codes/test_register_write_messages.py b/test/pdu/test_register_write_messages.py similarity index 87% rename from test/sub_function_codes/test_register_write_messages.py rename to test/pdu/test_register_write_messages.py index 8d702fb77..40d64ab3d 100644 --- a/test/sub_function_codes/test_register_write_messages.py +++ b/test/pdu/test_register_write_messages.py @@ -9,7 +9,8 @@ WriteSingleRegisterRequest, WriteSingleRegisterResponse, ) -from test.conftest import MockContext, MockLastValuesContext + +from ..conftest import MockContext, MockLastValuesContext # ---------------------------------------------------------------------------# @@ -87,43 +88,43 @@ async def test_write_single_register_request(self): """Test write single register request.""" context = MockContext() request = WriteSingleRegisterRequest(0x00, 0xF0000) - result = await request.execute(context) + result = await request.update_datastore(context) assert result.exception_code == ModbusExceptions.IllegalValue request.value = 0x00FF - result = await request.execute(context) + result = await request.update_datastore(context) assert result.exception_code == ModbusExceptions.IllegalAddress context.valid = True - result = await request.execute(context) + result = await request.update_datastore(context) assert result.function_code == request.function_code async def test_write_multiple_register_request(self): """Test write multiple register request.""" context = MockContext() request = WriteMultipleRegistersRequest(0x00, [0x00] * 10) - result = await request.execute(context) + result = await request.update_datastore(context) assert result.exception_code == ModbusExceptions.IllegalAddress request.count = 0x05 # bytecode != code * 2 - result = await request.execute(context) + result = await request.update_datastore(context) assert result.exception_code == ModbusExceptions.IllegalValue request.count = 0x800 # outside of range - result = await request.execute(context) + result = await request.update_datastore(context) assert result.exception_code == ModbusExceptions.IllegalValue context.valid = True request = WriteMultipleRegistersRequest(0x00, [0x00] * 10) - result = await request.execute(context) + result = await request.update_datastore(context) assert result.function_code == request.function_code request = WriteMultipleRegistersRequest(0x00, 0x00) - result = await request.execute(context) + result = await request.update_datastore(context) assert result.function_code == request.function_code request = WriteMultipleRegistersRequest(0x00, [0x00]) - result = await request.execute(context) + result = await request.update_datastore(context) assert result.function_code == request.function_code # -----------------------------------------------------------------------# @@ -145,7 +146,7 @@ def test_mask_write_register_request_decode(self): assert handle.and_mask == 0x00F2 assert handle.or_mask == 0x0025 - async def test_mask_write_register_request_execute(self): + async def test_mask_write_register_request_update_datastore(self): """Test write register request valid execution.""" # The test uses the 4 nibbles of the 16-bit values to test # the combinations: @@ -155,23 +156,23 @@ async def test_mask_write_register_request_execute(self): # and_mask=F, or_mask=F context = MockLastValuesContext(valid=True, default=0xAA55) handle = MaskWriteRegisterRequest(0x0000, 0x0F0F, 0x00FF) - result = await handle.execute(context) + result = await handle.update_datastore(context) assert isinstance(result, MaskWriteRegisterResponse) assert context.last_values == [0x0AF5] - async def test_mask_write_register_request_invalid_execute(self): - """Test write register request execute with invalid data.""" + async def test_mask_write_register_request_invalid_update_datastore(self): + """Test write register request update_datastore with invalid data.""" context = MockContext(valid=False, default=0x0000) handle = MaskWriteRegisterRequest(0x0000, -1, 0x1010) - result = await handle.execute(context) + result = await handle.update_datastore(context) assert ModbusExceptions.IllegalValue == result.exception_code handle = MaskWriteRegisterRequest(0x0000, 0x0101, -1) - result = await handle.execute(context) + result = await handle.update_datastore(context) assert ModbusExceptions.IllegalValue == result.exception_code handle = MaskWriteRegisterRequest(0x0000, 0x0101, 0x1010) - result = await handle.execute(context) + result = await handle.update_datastore(context) assert ModbusExceptions.IllegalAddress == result.exception_code # -----------------------------------------------------------------------# diff --git a/test/sub_client/test_client.py b/test/sub_client/test_client.py index 894cf4a70..44482e241 100755 --- a/test/sub_client/test_client.py +++ b/test/sub_client/test_client.py @@ -20,8 +20,8 @@ from pymodbus.client.mixin import ModbusClientMixin from pymodbus.datastore import ModbusSlaveContext from pymodbus.datastore.store import ModbusSequentialDataBlock -from pymodbus.exceptions import ConnectionException, ModbusException, ModbusIOException -from pymodbus.pdu import ModbusRequest +from pymodbus.exceptions import ConnectionException, ModbusException +from pymodbus.pdu import ExceptionResponse, ModbusPDU from pymodbus.transport import CommParams, CommType @@ -104,7 +104,7 @@ def test_client_mixin(arglist, method, arg, pdu_request): """Test mixin responses.""" pdu_to_call = None - def fake_execute(_self, request): + def fake_execute(_self, _no_response_expected, request): """Set PDU request.""" nonlocal pdu_to_call pdu_to_call = request @@ -240,8 +240,10 @@ async def test_client_instanciate( # a unsuccessful connect client.connect = lambda: False client.transport = None + pdu = ModbusPDU() + pdu.setData(0, 0, False) with pytest.raises(ConnectionException): - client.execute(ModbusRequest(0, 0, False)) + client.execute(False, pdu) async def test_client_modbusbaseclient(): """Test modbus base client class.""" @@ -386,8 +388,8 @@ def __init__(self, base, req, retries=0): async def delayed_resp(self): """Send a response to a received packet.""" await asyncio.sleep(0.05) - resp = await self.req.execute(self.ctx) - pkt = self.base.ctx.framer.buildPacket(resp) + resp = await self.req.update_datastore(self.ctx) + pkt = self.base.ctx.framer.buildFrame(resp) self.base.ctx.data_received(pkt) def write(self, data, addr=None): @@ -418,7 +420,7 @@ async def test_client_protocol_execute(): transport = MockTransport(base, request) base.ctx.connection_made(transport=transport) - response = await base.async_execute(request) + response = await base.async_execute(False, request) assert not response.isError() assert isinstance(response, pdu_bit_read.ReadCoilsResponse) @@ -432,12 +434,26 @@ async def test_client_execute_broadcast(): host="127.0.0.1", ), ) - request = pdu_bit_read.ReadCoilsRequest(1, 1) + request = pdu_bit_read.ReadCoilsRequest(1, 1, slave=0) transport = MockTransport(base, request) base.ctx.connection_made(transport=transport) + assert await base.async_execute(False, request) + - with pytest.raises(ModbusIOException): - assert not await base.async_execute(request) +async def test_client_execute_broadcast_no(): + """Test the client protocol execute method.""" + base = ModbusBaseClient( + FramerType.SOCKET, + 3, + None, + comm_params=CommParams( + host="127.0.0.1", + ), + ) + request = pdu_bit_read.ReadCoilsRequest(1, 1, slave=0) + transport = MockTransport(base, request) + base.ctx.connection_made(transport=transport) + assert not await base.async_execute(True, request) async def test_client_protocol_retry(): """Test the client protocol execute method with retries.""" @@ -454,7 +470,7 @@ async def test_client_protocol_retry(): transport = MockTransport(base, request, retries=2) base.ctx.connection_made(transport=transport) - response = await base.async_execute(request) + response = await base.async_execute(False, request) assert transport.retries == 0 assert not response.isError() assert isinstance(response, pdu_bit_read.ReadCoilsResponse) @@ -477,8 +493,8 @@ async def test_client_protocol_timeout(): transport = MockTransport(base, request, retries=4) base.ctx.connection_made(transport=transport) - with pytest.raises(ModbusIOException): - await base.async_execute(request) + pdu = await base.async_execute(False, request) + assert isinstance(pdu, ExceptionResponse) assert transport.retries == 1 @@ -676,14 +692,18 @@ async def test_client_build_response(): None, comm_params=CommParams(), ) + pdu = ModbusPDU() + pdu.setData(0, 0, False) with pytest.raises(ConnectionException): - await client.build_response(ModbusRequest(0, 0, False)) + await client.build_response(pdu) async def test_client_mixin_execute(): """Test dummy execute for both sync and async.""" client = ModbusClientMixin() + pdu = ModbusPDU() + pdu.setData(0, 0, False) with pytest.raises(NotImplementedError): - client.execute(ModbusRequest(0, 0, False)) + client.execute(False, pdu) with pytest.raises(NotImplementedError): - await client.execute(ModbusRequest(0, 0, False)) + await client.execute(False, pdu) diff --git a/test/sub_client/test_client_faulty_response.py b/test/sub_client/test_client_faulty_response.py index a14e1890f..8beaf4c6d 100644 --- a/test/sub_client/test_client_faulty_response.py +++ b/test/sub_client/test_client_faulty_response.py @@ -1,11 +1,10 @@ """Test server working as slave on a multidrop RS485 line.""" -from unittest import mock import pytest from pymodbus.exceptions import ModbusIOException -from pymodbus.factory import ClientDecoder from pymodbus.framer import FramerRTU, FramerSocket +from pymodbus.pdu import DecodePDU class TestFaultyResponses: @@ -16,30 +15,27 @@ class TestFaultyResponses: @pytest.fixture(name="framer") def fixture_framer(self): """Prepare framer.""" - return FramerSocket(ClientDecoder(), []) + return FramerSocket(DecodePDU(False)) - @pytest.fixture(name="callback") - def fixture_callback(self): - """Prepare dummy callback.""" - return mock.Mock() - - def test_ok_frame(self, framer, callback): + def test_ok_frame(self, framer): """Test ok frame.""" - framer.processIncomingPacket(self.good_frame, callback) - callback.assert_called_once() + used_len, pdu = framer.processIncomingFrame(self.good_frame) + assert pdu + assert used_len == len(self.good_frame) - def test_1917_frame(self, callback): + def test_1917_frame(self): """Test invalid frame in issue 1917.""" recv = b"\x01\x86\x02\x00\x01" - framer = FramerRTU(ClientDecoder(), [0]) - framer.processIncomingPacket(recv, callback) - callback.assert_not_called() + framer = FramerRTU(DecodePDU(False)) + used_len, pdu = framer.processIncomingFrame(recv) + assert not pdu + assert used_len - def test_faulty_frame1(self, framer, callback): + def test_faulty_frame1(self, framer): """Test ok frame.""" faulty_frame = b"\x00\x04\x00\x00\x00\x05\x00\x03\x0a\x00\x04" with pytest.raises(ModbusIOException): - framer.processIncomingPacket(faulty_frame, callback) - callback.assert_not_called() - framer.processIncomingPacket(self.good_frame, callback) - callback.assert_called_once() + framer.processIncomingFrame(faulty_frame) + used_len, pdu = framer.processIncomingFrame(self.good_frame) + assert pdu + assert used_len == len(self.good_frame) diff --git a/test/sub_client/test_client_sync.py b/test/sub_client/test_client_sync.py index 68d0080aa..529c18787 100755 --- a/test/sub_client/test_client_sync.py +++ b/test/sub_client/test_client_sync.py @@ -88,8 +88,8 @@ def test_udp_client_recv_duplicate(self): client.socket.mock_prepare_receive(test_msg) reply_ok = client.read_input_registers(0x820, 1, 1) assert not reply_ok.isError() - reply_none = client.read_input_registers(0x40, 10, 1) - assert reply_none.isError() + reply_ok = client.read_input_registers(0x40, 10, 1) + assert not reply_ok.isError() client.close() def test_udp_client_repr(self): @@ -422,6 +422,21 @@ def test_serial_client_recv(self): client.socket.timeout = 0 assert client.recv(0) == b"" + def test_serial_client_recv_split(self): + """Test the serial client receive method.""" + client = ModbusSerialClient("/dev/null") + with pytest.raises(ConnectionException): + client.recv(1024) + client.socket = mockSocket(copy_send=False) + client.socket.mock_prepare_receive(b'') + client.socket.mock_prepare_receive(b'\x11\x03\x06\xAE') + client.socket.mock_prepare_receive(b'\x41\x56\x52\x43\x40\x49') + client.socket.mock_prepare_receive(b'\xAD') + reply_ok = client.read_input_registers(0x820, 3, slave=17) + assert not reply_ok.isError() + client.close() + + def test_serial_client_repr(self): """Test serial client.""" client = ModbusSerialClient("/dev/null") diff --git a/test/sub_current/test_device.py b/test/sub_current/test_device.py index e150068d8..66c1fd09b 100644 --- a/test/sub_current/test_device.py +++ b/test/sub_current/test_device.py @@ -6,7 +6,7 @@ ModbusDeviceIdentification, ModbusPlusStatistics, ) -from pymodbus.events import ModbusEvent, RemoteReceiveEvent +from pymodbus.events import RemoteReceiveEvent # ---------------------------------------------------------------------------# @@ -281,7 +281,7 @@ def test_modbus_control_block_invalid_diagnostic(self): def test_clearing_control_events(self): """Test adding and clearing modbus events.""" assert self.control.Events == [] - event = ModbusEvent() + event = RemoteReceiveEvent() self.control.addEvent(event) assert self.control.Events == [event] assert self.control.Counter.Event == 1 diff --git a/test/sub_current/test_events.py b/test/sub_current/test_events.py index bfe68bda9..3cec6672d 100644 --- a/test/sub_current/test_events.py +++ b/test/sub_current/test_events.py @@ -4,24 +4,15 @@ from pymodbus.events import ( CommunicationRestartEvent, EnteredListenModeEvent, - ModbusEvent, RemoteReceiveEvent, RemoteSendEvent, ) -from pymodbus.exceptions import NotImplementedException, ParameterException +from pymodbus.exceptions import ParameterException class TestEvents: """Unittest for the pymodbus.device module.""" - def test_modbus_event_base_class(self): - """Test modbus event base class.""" - event = ModbusEvent() - with pytest.raises(NotImplementedException): - event.encode() - with pytest.raises(NotImplementedException): - event.decode(None) - def test_remote_receive_event(self): """Test remove receive event.""" event = RemoteReceiveEvent() diff --git a/test/sub_current/test_factory.py b/test/sub_current/test_factory.py deleted file mode 100644 index 52d415794..000000000 --- a/test/sub_current/test_factory.py +++ /dev/null @@ -1,207 +0,0 @@ -"""Test factory.""" -import pytest - -from pymodbus.exceptions import MessageRegisterException, ModbusException -from pymodbus.factory import ClientDecoder, ServerDecoder -from pymodbus.pdu import ModbusRequest, ModbusResponse - - -class TestFactory: - """Unittest for the pymod.exceptions module.""" - - client: ClientDecoder - server: ServerDecoder - request = ( - (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 slave 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 - ) - - response = ( - (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 slave 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 - ) - - exception = ( - (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"), # slave 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.fixture(autouse=True) - def _setup(self): - """Do common setup function.""" - self.client = ClientDecoder() - self.server = ServerDecoder() - - def test_exception_lookup(self): - """Test that we can look up exception messages.""" - for func, _ in self.exception: - response = self.client.lookupPduClass(func) - assert response - - def test_response_lookup(self): - """Test a working response factory lookup.""" - for func, _ in self.response: - response = self.client.lookupPduClass(func) - assert response - - def test_request_lookup(self): - """Test a working request factory lookup.""" - for func, _ in self.request: - request = self.client.lookupPduClass(func) - assert request - - def test_response_working(self): - """Test a working response factory decoders.""" - for _func, msg in self.response: - self.client.decode(msg) - - def test_response_errors(self): - """Test a response factory decoder exceptions.""" - with pytest.raises(ModbusException): - self.client._helper(self.bad[0][1]) # pylint: disable=protected-access - assert ( - self.client.decode(self.bad[1][1]).function_code == self.bad[1][0] - ), "Failed to decode error PDU" - - def test_requests_working(self): - """Test a working request factory decoders.""" - for _func, msg in self.request: - self.server.decode(msg) - - def test_client_factory_fails(self): - """Tests that a client factory will fail to decode a bad message.""" - with pytest.raises(TypeError): - self.client.decode(None) - - def test_server_factory_fails(self): - """Tests that a server factory will fail to decode a bad message.""" - with pytest.raises(TypeError): - self.server.decode(None) - - def test_server_register_custom_request(self): - """Test server register custom request.""" - - class CustomRequest(ModbusRequest): - """Custom request.""" - - function_code = 0xFF - - class NoCustomRequest: # pylint: disable=too-few-public-methods - """Custom request.""" - - function_code = 0xFF - - self.server.register(CustomRequest) - assert self.client.lookupPduClass(CustomRequest.function_code) - CustomRequest.sub_function_code = 0xFF - self.server.register(CustomRequest) - assert self.server.lookupPduClass(CustomRequest.function_code) - try: - func_raised = False - self.server.register(NoCustomRequest) - except MessageRegisterException: - func_raised = True - assert func_raised - - def test_client_register_custom_response(self): - """Test client register custom response.""" - - class CustomResponse(ModbusResponse): - """Custom response.""" - - function_code = 0xFF - - class NoCustomResponse: # pylint: disable=too-few-public-methods - """Custom request.""" - - function_code = 0xFF - - self.client.register(CustomResponse) - assert self.client.lookupPduClass(CustomResponse.function_code) - CustomResponse.sub_function_code = 0xFF - self.client.register(CustomResponse) - assert self.client.lookupPduClass(CustomResponse.function_code) - try: - func_raised = False - self.client.register(NoCustomResponse) - except MessageRegisterException: - func_raised = True - assert func_raised - - # ---------------------------------------------------------------------------# - # I don't actually know what is supposed to be returned here, I assume that - # since the high bit is set, it will simply echo the resulting message - # ---------------------------------------------------------------------------# - - async def test_request_errors(self): - """Test a request factory decoder exceptions.""" - for func, msg in self.bad: - result = self.server.decode(msg) - assert result.ErrorCode == 1, "Failed to decode invalid requests" - assert ( - await result.execute(None) - ).function_code == func, "Failed to create correct response message" diff --git a/test/sub_current/test_pdu.py b/test/sub_current/test_pdu.py deleted file mode 100644 index 5c17b4088..000000000 --- a/test/sub_current/test_pdu.py +++ /dev/null @@ -1,82 +0,0 @@ -"""Test pdu.""" -import pytest - -from pymodbus.exceptions import NotImplementedException -from pymodbus.pdu import ( - ExceptionResponse, - IllegalFunctionRequest, - ModbusExceptions, - ModbusRequest, - ModbusResponse, -) - - -class TestPdu: - """Unittest for the pymod.pdu module.""" - - bad_requests = ( - ModbusRequest(0, 0, False), - ModbusResponse(0, 0, False), - ) - illegal = IllegalFunctionRequest(1, 0, 0, False) - exception = ExceptionResponse(1, 1, 0, 0, False) - - def test_not_impelmented(self): - """Test a base classes for not implemented functions.""" - for request in self.bad_requests: - with pytest.raises(NotImplementedException): - request.encode() - - for request in self.bad_requests: - with pytest.raises(NotImplementedException): - request.decode(None) - - async def test_error_methods(self): - """Test all error methods.""" - self.illegal.decode("12345") - await self.illegal.execute(None) - - result = self.exception.encode() - self.exception.decode(result) - assert result == b"\x01" - assert self.exception.exception_code == 1 - - def test_request_exception_factory(self): - """Test all error methods.""" - request = ModbusRequest(0, 0, False) - request.function_code = 1 - errors = {ModbusExceptions.decode(c): c for c in range(1, 20)} - for error, code in iter(errors.items()): - result = request.doException(code) - assert str(result) == f"Exception Response(129, 1, {error})" - - def test_calculate_rtu_frame_size(self): - """Test the calculation of Modbus/RTU frame sizes.""" - with pytest.raises(NotImplementedException): - ModbusRequest.calculateRtuFrameSize(b"") - ModbusRequest._rtu_frame_size = 5 # pylint: disable=protected-access - assert ModbusRequest.calculateRtuFrameSize(b"") == 5 - del ModbusRequest._rtu_frame_size - - ModbusRequest._rtu_byte_count_pos = 2 # pylint: disable=protected-access - assert ( - ModbusRequest.calculateRtuFrameSize( - b"\x11\x01\x05\xcd\x6b\xb2\x0e\x1b\x45\xe6" - ) - == 0x05 + 5 - ) - del ModbusRequest._rtu_byte_count_pos - - with pytest.raises(NotImplementedException): - ModbusResponse.calculateRtuFrameSize(b"") - ModbusResponse._rtu_frame_size = 12 # pylint: disable=protected-access - assert ModbusResponse.calculateRtuFrameSize(b"") == 12 - del ModbusResponse._rtu_frame_size - ModbusResponse._rtu_byte_count_pos = 2 # pylint: disable=protected-access - assert ( - ModbusResponse.calculateRtuFrameSize( - b"\x11\x01\x05\xcd\x6b\xb2\x0e\x1b\x45\xe6" - ) - == 0x05 + 5 - ) - del ModbusResponse._rtu_byte_count_pos diff --git a/test/sub_current/test_transaction.py b/test/sub_current/test_transaction.py index bef0eda22..c24c28d79 100755 --- a/test/sub_current/test_transaction.py +++ b/test/sub_current/test_transaction.py @@ -4,14 +4,13 @@ from pymodbus.exceptions import ( ModbusIOException, ) -from pymodbus.factory import ServerDecoder from pymodbus.framer import ( FramerAscii, FramerRTU, FramerSocket, FramerTLS, ) -from pymodbus.pdu import ModbusRequest +from pymodbus.pdu import DecodePDU, ModbusPDU from pymodbus.transaction import ( ModbusTransactionManager, SyncModbusTransactionManager, @@ -38,13 +37,12 @@ class TestTransaction: # pylint: disable=too-many-public-methods # ----------------------------------------------------------------------- # def setup_method(self): """Set up the test environment.""" - self.client = None - self.decoder = ServerDecoder() - self._tcp = FramerSocket(self.decoder, []) - self._tls = FramerTLS(self.decoder, []) - self._rtu = FramerRTU(self.decoder, []) - self._ascii = FramerAscii(self.decoder, []) - self._manager = SyncModbusTransactionManager(self.client, 3) + self.decoder = DecodePDU(True) + self._tcp = FramerSocket(self.decoder) + self._tls = FramerTLS(self.decoder) + self._rtu = FramerRTU(self.decoder) + self._ascii = FramerAscii(self.decoder) + self._manager = SyncModbusTransactionManager(None, 3) # ----------------------------------------------------------------------- # # Modbus transaction manager @@ -98,10 +96,10 @@ def test_execute(self, mock_get_transaction, mock_recv): client = mock.MagicMock() client.framer = self._ascii client.framer._buffer = b"deadbeef" # pylint: disable=protected-access - client.framer.processIncomingPacket = mock.MagicMock() - client.framer.processIncomingPacket.return_value = None - client.framer.buildPacket = mock.MagicMock() - client.framer.buildPacket.return_value = b"deadbeef" + client.framer.processIncomingFrame = mock.MagicMock() + client.framer.processIncomingFrame.return_value = 0, None + client.framer.buildFrame = mock.MagicMock() + client.framer.buildFrame.return_value = b"deadbeef" client.send = mock.MagicMock() client.send.return_value = len(b"deadbeef") request = mock.MagicMock() @@ -115,7 +113,7 @@ def test_execute(self, mock_get_transaction, mock_recv): assert trans.retries == 3 mock_get_transaction.return_value = b"response" - response = trans.execute(request) + response = trans.execute(False, request) assert response == b"response" # No response mock_recv.reset_mock( @@ -123,14 +121,14 @@ def test_execute(self, mock_get_transaction, mock_recv): ) trans.transactions = {} mock_get_transaction.return_value = None - response = trans.execute(request) + response = trans.execute(False, request) assert isinstance(response, ModbusIOException) # No response with retries mock_recv.reset_mock( side_effect=iter([b"", b"abcdef"]) ) - response = trans.execute(request) + response = trans.execute(False, request) assert isinstance(response, ModbusIOException) # wrong handle_local_echo @@ -138,24 +136,24 @@ def test_execute(self, mock_get_transaction, mock_recv): side_effect=iter([b"abcdef", b"deadbe", b"123456"]) ) client.comm_params.handle_local_echo = True - assert trans.execute(request).message == "[Input/Output] Wrong local echo" + assert trans.execute(False, request).message == "[Input/Output] Wrong local echo" client.comm_params.handle_local_echo = False # retry on invalid response mock_recv.reset_mock( side_effect=iter([b"", b"abcdef", b"deadbe", b"123456"]) ) - response = trans.execute(request) + response = trans.execute(False, request) assert isinstance(response, ModbusIOException) # Unable to decode response mock_recv.reset_mock( side_effect=ModbusIOException() ) - client.framer.processIncomingPacket.side_effect = mock.MagicMock( + client.framer.processIncomingFrame.side_effect = mock.MagicMock( side_effect=ModbusIOException() ) - assert isinstance(trans.execute(request), ModbusIOException) + assert isinstance(trans.execute(False, request), ModbusIOException) def test_transaction_manager_tid(self): """Test the transaction manager TID.""" @@ -167,9 +165,8 @@ def test_transaction_manager_tid(self): def test_get_transaction_manager_transaction(self): """Test the getting a transaction from the transaction manager.""" self._manager.reset() - handle = ModbusRequest( - 0, self._manager.getNextTID(), False - ) + handle = ModbusPDU() + handle.setData(0, self._manager.getNextTID(), False) self._manager.addTransaction(handle) result = self._manager.getTransaction(handle.transaction_id) assert handle is result @@ -177,422 +174,8 @@ def test_get_transaction_manager_transaction(self): def test_delete_transaction_manager_transaction(self): """Test deleting a transaction from the dict transaction manager.""" self._manager.reset() - handle = ModbusRequest( - 0, self._manager.getNextTID(), False - ) + handle = ModbusPDU() + handle.setData(0, self._manager.getNextTID(), False) self._manager.addTransaction(handle) self._manager.delTransaction(handle.transaction_id) assert not self._manager.getTransaction(handle.transaction_id) - - # ----------------------------------------------------------------------- # - # TCP tests - # ----------------------------------------------------------------------- # - def test_tcp_framer_transaction_ready(self): - """Test a tcp frame transaction.""" - count = 0 - result = None - def callback(data): - """Simulate callback.""" - nonlocal count, result - count += 1 - result = data - - msg = b"\x00\x01\x12\x34\x00\x06\xff\x02\x01\x02\x00\x08" - self._tcp.processIncomingPacket(msg, callback) - self._tcp._buffer = msg # pylint: disable=protected-access - callback(b'') - - def test_tcp_framer_transaction_full(self): - """Test a full tcp frame transaction.""" - count = 0 - result = None - def callback(data): - """Simulate callback.""" - nonlocal count, result - count += 1 - result = data - - msg = b"\x00\x01\x12\x34\x00\x06\xff\x02\x01\x02\x00\x08" - self._tcp.processIncomingPacket(msg, callback) - assert result.function_code.to_bytes(1,'big') + result.encode() == msg[7:] - - def test_tcp_framer_transaction_half(self): - """Test a half completed tcp frame transaction.""" - count = 0 - result = None - def callback(data): - """Simulate callback.""" - nonlocal count, result - count += 1 - result = data - - msg1 = b"\x00\x01\x12\x34\x00" - msg2 = b"\x06\xff\x02\x01\x02\x00\x08" - self._tcp.processIncomingPacket(msg1, callback) - assert not result - self._tcp.processIncomingPacket(msg2, callback) - assert result - assert result.function_code.to_bytes(1,'big') + result.encode() == msg2[2:] - - def test_tcp_framer_transaction_half2(self): - """Test a half completed tcp frame transaction.""" - count = 0 - result = None - def callback(data): - """Simulate callback.""" - nonlocal count, result - count += 1 - result = data - - msg1 = b"\x00\x01\x12\x34\x00\x06\xff" - msg2 = b"\x02\x01\x02\x00\x08" - self._tcp.processIncomingPacket(msg1, callback) - assert not result - self._tcp.processIncomingPacket(msg2, callback) - assert result - assert result.function_code.to_bytes(1,'big') + result.encode() == msg2 - - def test_tcp_framer_transaction_half3(self): - """Test a half completed tcp frame transaction.""" - count = 0 - result = None - def callback(data): - """Simulate callback.""" - nonlocal count, result - count += 1 - result = data - - msg1 = b"\x00\x01\x12\x34\x00\x06\xff\x02\x01\x02\x00" - msg2 = b"\x08" - self._tcp.processIncomingPacket(msg1, callback) - assert not result - self._tcp.processIncomingPacket(msg2, callback) - assert result - assert result.function_code.to_bytes(1,'big') + result.encode() == msg1[7:] + msg2 - - def test_tcp_framer_transaction_short(self): - """Test that we can get back on track after an invalid message.""" - count = 0 - result = None - def callback(data): - """Simulate callback.""" - nonlocal count, result - count += 1 - result = data - - # msg1 = b"\x99\x99\x99\x99\x00\x01\x00\x17" - msg1 = b'' - msg2 = b"\x00\x01\x12\x34\x00\x06\xff\x02\x01\x02\x00\x08" - self._tcp.processIncomingPacket(msg1, callback) - assert not result - self._tcp.processIncomingPacket(msg2, callback) - assert result - assert result.function_code.to_bytes(1,'big') + result.encode() == msg2[7:] - - def test_tcp_framer_populate(self): - """Test a tcp frame packet build.""" - count = 0 - result = None - def callback(data): - """Simulate callback.""" - nonlocal count, result - count += 1 - result = data - - expected = ModbusRequest(0, 0, False) - expected.transaction_id = 0x0001 - expected.slave_id = 0xFF - msg = b"\x00\x01\x12\x34\x00\x06\xff\x02\x12\x34\x01\x02" - self._tcp.processIncomingPacket(msg, callback) - - @mock.patch.object(ModbusRequest, "encode") - def test_tcp_framer_packet(self, mock_encode): - """Test a tcp frame packet build.""" - message = ModbusRequest(0, 0, False) - message.transaction_id = 0x0001 - message.slave_id = 0xFF - message.function_code = 0x01 - expected = b"\x00\x01\x00\x00\x00\x02\xff\x01" - mock_encode.return_value = b"" - actual = self._tcp.buildPacket(message) - assert expected == actual - - # ----------------------------------------------------------------------- # - # TLS tests - # ----------------------------------------------------------------------- # - def test_framer_tls_framer_transaction_ready(self): - """Test a tls frame transaction.""" - count = 0 - result = None - def callback(data): - """Simulate callback.""" - nonlocal count, result - count += 1 - result = data - - msg = b"\x00\x01\x12\x34\x00\x06\xff\x02\x12\x34\x01\x02" - self._tcp.processIncomingPacket(msg[0:4], callback) - assert not result - self._tcp.processIncomingPacket(msg[4:], callback) - assert result - - def test_framer_tls_framer_transaction_full(self): - """Test a full tls frame transaction.""" - count = 0 - result = None - def callback(data): - """Simulate callback.""" - nonlocal count, result - count += 1 - result = data - - msg = b"\x00\x01\x12\x34\x00\x06\xff\x02\x12\x34\x01\x02" - self._tcp.processIncomingPacket(msg, callback) - assert result - - def test_framer_tls_framer_transaction_half(self): - """Test a half completed tls frame transaction.""" - count = 0 - result = None - def callback(data): - """Simulate callback.""" - nonlocal count, result - count += 1 - result = data - - msg = b"\x00\x01\x12\x34\x00\x06\xff\x02\x12\x34\x01\x02" - self._tcp.processIncomingPacket(msg[0:8], callback) - assert not result - self._tcp.processIncomingPacket(msg[8:], callback) - assert result - - def test_framer_tls_framer_transaction_short(self): - """Test that we can get back on track after an invalid message.""" - count = 0 - result = None - def callback(data): - """Simulate callback.""" - nonlocal count, result - count += 1 - result = data - - msg = b"\x00\x01\x12\x34\x00\x06\xff\x02\x12\x34\x01\x02" - self._tcp.processIncomingPacket(msg[0:2], callback) - assert not result - self._tcp.processIncomingPacket(msg[2:], callback) - assert result - - def test_framer_tls_incoming_packet(self): - """Framer tls incoming packet.""" - msg = b"\x00\x01\x12\x34\x00\x06\xff\x02\x12\x34\x01\x02" - msg_result = None - - def mock_callback(result): - """Mock callback.""" - nonlocal msg_result - - msg_result = result.encode() - - self._tls.processIncomingPacket(msg, mock_callback) - # assert msg == msg_result - - def test_framer_tls_framer_populate(self): - """Test a tls frame packet build.""" - count = 0 - result = None - def callback(data): - """Simulate callback.""" - nonlocal count, result - count += 1 - result = data - - msg = b"\x00\x01\x12\x34\x00\x06\xff\x02\x12\x34\x01\x02" - self._tcp.processIncomingPacket(msg, callback) - assert result - - @mock.patch.object(ModbusRequest, "encode") - def test_framer_tls_framer_packet(self, mock_encode): - """Test a tls frame packet build.""" - message = ModbusRequest(0, 0, False) - message.function_code = 0x01 - expected = b"\x01" - mock_encode.return_value = b"" - actual = self._tls.buildPacket(message) - assert expected == actual - - # ----------------------------------------------------------------------- # - # RTU tests - # ----------------------------------------------------------------------- # - def test_rtu_framer_transaction_ready(self): - """Test if the checks for a complete frame work.""" - count = 0 - result = None - def callback(data): - """Simulate callback.""" - nonlocal count, result - count += 1 - result = data - - msg_parts = [b"\x00\x01\x00", b"\x00\x00\x01\xfc\x1b"] - self._rtu.processIncomingPacket(msg_parts[0], callback) - assert not result - self._rtu.processIncomingPacket(msg_parts[1], callback) - assert result - - def test_rtu_framer_transaction_full(self): - """Test a full rtu frame transaction.""" - count = 0 - result = None - def callback(data): - """Simulate callback.""" - nonlocal count, result - count += 1 - result = data - - msg = b"\x00\x01\x00\x00\x00\x01\xfc\x1b" - self._rtu.processIncomingPacket(msg, callback) - assert result - - def test_rtu_framer_transaction_half(self): - """Test a half completed rtu frame transaction.""" - count = 0 - result = None - def callback(data): - """Simulate callback.""" - nonlocal count, result - count += 1 - result = data - - msg_parts = [b"\x00\x01\x00", b"\x00\x00\x01\xfc\x1b"] - self._rtu.processIncomingPacket(msg_parts[0], callback) - assert not result - self._rtu.processIncomingPacket(msg_parts[1], callback) - assert result - - def test_rtu_framer_populate(self): - """Test a rtu frame packet build.""" - count = 0 - result = None - def callback(data): - """Simulate callback.""" - nonlocal count, result - count += 1 - result = data - - msg = b"\x00\x01\x00\x00\x00\x01\xfc\x1b" - self._rtu.processIncomingPacket(msg, callback) - assert int(msg[0]) == self._rtu.incoming_dev_id - - @mock.patch.object(ModbusRequest, "encode") - def test_rtu_framer_packet(self, mock_encode): - """Test a rtu frame packet build.""" - message = ModbusRequest(0, 0, False) - message.slave_id = 0xFF - message.function_code = 0x01 - expected = b"\xff\x01\x81\x80" # only header + CRC - no data - mock_encode.return_value = b"" - actual = self._rtu.buildPacket(message) - assert expected == actual - - def test_rtu_decode_exception(self): - """Test that the RTU framer can decode errors.""" - count = 0 - result = None - def callback(data): - """Simulate callback.""" - nonlocal count, result - count += 1 - result = data - - msg = b"\x00\x90\x02\x9c\x01" - self._rtu.processIncomingPacket(msg, callback) - assert result - - def test_process(self): - """Test process.""" - count = 0 - result = None - def callback(data): - """Simulate callback.""" - nonlocal count, result - count += 1 - result = data - - msg = b"\x00\x01\x00\x00\x00\x01\xfc\x1b" - self._rtu.processIncomingPacket(msg, callback) - assert result - - def test_rtu_process_incoming_packets(self): - """Test rtu process incoming packets.""" - count = 0 - result = None - def callback(data): - """Simulate callback.""" - nonlocal count, result - count += 1 - result = data - - msg = b"\x00\x01\x00\x00\x00\x01\xfc\x1b" - self._rtu.processIncomingPacket(msg, callback) - assert result - - # ----------------------------------------------------------------------- # - # ASCII tests - # ----------------------------------------------------------------------- # - def test_ascii_framer_transaction_ready(self): - """Test a ascii frame transaction.""" - count = 0 - result = None - def callback(data): - """Simulate callback.""" - nonlocal count, result - count += 1 - result = data - - msg = b":F7031389000A60\r\n" - self._ascii.processIncomingPacket(msg, callback) - assert result - - def test_ascii_framer_transaction_full(self): - """Test a full ascii frame transaction.""" - count = 0 - result = None - def callback(data): - """Simulate callback.""" - nonlocal count, result - count += 1 - result = data - - msg = b"sss:F7031389000A60\r\n" - self._ascii.processIncomingPacket(msg, callback) - assert result - - def test_ascii_framer_transaction_half(self): - """Test a half completed ascii frame transaction.""" - count = 0 - result = None - def callback(data): - """Simulate callback.""" - nonlocal count, result - count += 1 - result = data - - msg_parts = (b"sss:F7031389", b"000A60\r\n") - self._ascii.processIncomingPacket(msg_parts[0], callback) - assert not result - self._ascii.processIncomingPacket(msg_parts[1], callback) - assert result - - def test_ascii_process_incoming_packets(self): - """Test ascii process incoming packet.""" - count = 0 - result = None - def callback(data): - """Simulate callback.""" - nonlocal count, result - count += 1 - result = data - - msg = b":F7031389000A60\r\n" - self._ascii.processIncomingPacket(msg, callback) - assert result diff --git a/test/sub_function_codes/test_all_messages.py b/test/sub_function_codes/test_all_messages.py deleted file mode 100644 index e2581e9d4..000000000 --- a/test/sub_function_codes/test_all_messages.py +++ /dev/null @@ -1,93 +0,0 @@ -"""Test all messages.""" -from pymodbus.pdu.bit_read_message import ( - ReadCoilsRequest, - ReadCoilsResponse, - ReadDiscreteInputsRequest, - ReadDiscreteInputsResponse, -) -from pymodbus.pdu.bit_write_message import ( - WriteMultipleCoilsRequest, - WriteMultipleCoilsResponse, - WriteSingleCoilRequest, - WriteSingleCoilResponse, -) -from pymodbus.pdu.register_read_message import ( - ReadHoldingRegistersRequest, - ReadHoldingRegistersResponse, - ReadInputRegistersRequest, - ReadInputRegistersResponse, - ReadWriteMultipleRegistersRequest, - ReadWriteMultipleRegistersResponse, -) -from pymodbus.pdu.register_write_message import ( - WriteMultipleRegistersRequest, - WriteMultipleRegistersResponse, - WriteSingleRegisterRequest, - WriteSingleRegisterResponse, -) - - -# ---------------------------------------------------------------------------# -# Fixture -# ---------------------------------------------------------------------------# - - -class TestAllMessages: - """All messages tests.""" - - # -----------------------------------------------------------------------# - # Setup/TearDown - # -----------------------------------------------------------------------# - - requests = [ - lambda slave: ReadCoilsRequest(1, 5, slave=slave), - lambda slave: ReadDiscreteInputsRequest(1, 5, slave=slave), - lambda slave: WriteSingleCoilRequest(1, 1, slave=slave), - lambda slave: WriteMultipleCoilsRequest(1, [1], slave=slave), - lambda slave: ReadHoldingRegistersRequest(1, 5, slave=slave), - lambda slave: ReadInputRegistersRequest(1, 5, slave=slave), - lambda slave: ReadWriteMultipleRegistersRequest( - slave=slave, - read_address=1, - read_count=1, - write_address=1, - write_registers=1, - ), - lambda slave: WriteSingleRegisterRequest(1, 1, slave=slave), - lambda slave: WriteMultipleRegistersRequest(1, [1], slave=slave), - ] - responses = [ - lambda slave: ReadCoilsResponse([1], slave=slave), - lambda slave: ReadDiscreteInputsResponse([1], slave=slave), - lambda slave: WriteSingleCoilResponse(1, 1, slave=slave), - lambda slave: WriteMultipleCoilsResponse(1, [1], slave=slave), - lambda slave: ReadHoldingRegistersResponse([1], slave=slave), - lambda slave: ReadInputRegistersResponse([1], slave=slave), - lambda slave: ReadWriteMultipleRegistersResponse([1], slave=slave), - lambda slave: WriteSingleRegisterResponse(1, 1, slave=slave), - lambda slave: WriteMultipleRegistersResponse(1, 1, slave=slave), - ] - - def test_initializing_slave_address_request(self): - """Test that every request can initialize the slave id.""" - slave_id = 0x12 - for factory in self.requests: - request = factory(slave_id) - assert request.slave_id == slave_id - - def test_initializing_slave_address_response(self): - """Test that every response can initialize the slave id.""" - slave_id = 0x12 - for factory in self.responses: - response = factory(slave_id) - assert response.slave_id == slave_id - - def test_forwarding_to_pdu(self): - """Test that parameters are forwarded to the pdu correctly.""" - request = ReadCoilsRequest(1, 5, slave=18, transaction=0x12,) - assert request.slave_id == 0x12 - assert request.transaction_id == 0x12 - - request = ReadCoilsRequest(1, 5) - assert request.slave_id == 1 - assert not request.transaction_id diff --git a/test/sub_server/test_server_asyncio.py b/test/sub_server/test_server_asyncio.py index fb9d8dbfc..2f40a8fce 100755 --- a/test/sub_server/test_server_asyncio.py +++ b/test/sub_server/test_server_asyncio.py @@ -215,12 +215,12 @@ async def test_async_tcp_server_receive_data(self): BasicClient.data = b"\x01\x00\x00\x00\x00\x06\x01\x03\x00\x00\x00\x19" await self.start_server() with mock.patch( - "pymodbus.framer.FramerSocket.processIncomingPacket", + "pymodbus.framer.FramerSocket.processIncomingFrame", new_callable=mock.Mock, ) as process: await self.connect_server() process.assert_called_once() - assert process.call_args[1]["data"] == BasicClient.data + assert process.call_args[0][0] == BasicClient.data async def test_async_tcp_server_roundtrip(self): """Test sending and receiving data on tcp socket.""" @@ -266,7 +266,7 @@ async def test_async_tcp_server_modbus_error(self): BasicClient.data = TEST_DATA await self.start_server() with mock.patch( - "pymodbus.pdu.register_read_message.ReadHoldingRegistersRequest.execute", + "pymodbus.pdu.register_read_message.ReadHoldingRegistersRequest.update_datastore", side_effect=NoSuchSlaveException, ): await self.connect_server() @@ -345,7 +345,7 @@ async def test_async_udp_server_exception(self): BasicClient.done = asyncio.Future() await self.start_server(do_udp=True) with mock.patch( - "pymodbus.framer.FramerSocket.processIncomingPacket", + "pymodbus.framer.FramerSocket.processIncomingFrame", new_callable=lambda: mock.Mock(side_effect=Exception), ): # get the random server port pylint: disable=protected-access @@ -361,7 +361,7 @@ async def test_async_tcp_server_exception(self): BasicClient.data = b"\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF" await self.start_server() with mock.patch( - "pymodbus.framer.FramerSocket.processIncomingPacket", + "pymodbus.framer.FramerSocket.processIncomingFrame", new_callable=lambda: mock.Mock(side_effect=Exception), ): await self.connect_server()