diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3f4a888ef..e166ff9e2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -10,14 +10,25 @@ on: pull_request: branches: - dev + types: [opened, synchronize, reopened, ready_for_review] schedule: # Sunday at 02:10 UTC. - cron: '10 2 * * 0' workflow_dispatch: jobs: + faildraft: + name: fail draft + if: github.event.pull_request.draft == true + runs-on: ubuntu-latest + steps: + - name: fail draft + run: | + exit 1 + testing: name: ${{ matrix.os }} - ${{ matrix.python }} + if: github.event.pull_request.draft == false runs-on: ${{ matrix.os }} timeout-minutes: 20 strategy: @@ -98,7 +109,7 @@ jobs: - name: ruff if: matrix.run_lint == true run: | - ruff . + ruff check . - name: pytest if: ${{ (matrix.os != 'ubuntu-latest') || (matrix.python != '3.12') }} @@ -114,6 +125,7 @@ jobs: analyze: name: Analyze Python + if: github.event.pull_request.draft == false runs-on: ubuntu-22.04 timeout-minutes: 10 steps: diff --git a/.gitignore b/.gitignore index 965c21c94..c2a1d0c33 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,7 @@ __pycache__/ .idea/ .noseids .pymodhis +.pypirc .venv .vscode .vscode/ @@ -18,3 +19,4 @@ prof/ /pymodbus.egg-info/ venv downloaded_files/ +pymodbus.log diff --git a/API_changes.rst b/API_changes.rst index 102bc3805..fef2c8cc0 100644 --- a/API_changes.rst +++ b/API_changes.rst @@ -3,6 +3,22 @@ API changes Versions (X.Y.Z) where Z > 0 e.g. 3.0.1 do NOT have API changes! +API changes 3.7.0 +----------------- +- default slave changed to 1 from 0 (which is broadcast). +- broadcast_enable, retry_on_empty, no_resend_on_retry parameters removed. +- class method generate_ssl() added to TLS client (sync/async). +- removed certfile, keyfile, password from TLS client, please use generate_ssl() +- on_reconnect_callback() removed from clients (sync/async). +- on_connect_callback(true/false) added to async clients. +- binary framer no longer supported +- Framer. renamed to FramerType. +- PDU classes moved to pymodbus/pdu +- Simulator config custom actions kwargs -> parameters +- Non defined parameters (kwargs) no longer valid +- Drop support for Python 3.8 (its no longer tested, but will probably work) + + API changes 3.6.0 ----------------- - framer= is an enum: pymodbus.Framer, but still accept a framer class diff --git a/AUTHORS.rst b/AUTHORS.rst index 6d015f1e9..78fad7682 100644 --- a/AUTHORS.rst +++ b/AUTHORS.rst @@ -33,6 +33,7 @@ Thanks to - Dominique Martinet - Dries - duc996 +- Esco441-91 - Farzad Panahi - Fredo70 - Gao Fang @@ -54,6 +55,7 @@ Thanks to - julian - Justin Standring - Kenny Johansson +- Martyy - Matthias Straka - laund - Logan Gunthorpe diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 2c3347d78..1cae14415 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -8,6 +8,47 @@ helps make pymodbus a better product. :ref:`Authors`: contains a complete list of volunteers have contributed to each major version. +Version 3.7.0 +------------- +* Remove unneeded client parameters. (#2272) +* simulator: Fix context single parameter (#2264) +* buildPacket can be used for Request and Response (#2262) +* More descriptive decoder exceptions (#2260) +* Cleanup ReadWriteMultipleRegistersResponse and testing (#2261) +* Feature/simulator addressing (#2258) +* Framer optimization (apart from RTU). (#2146) +* Use mock.patch.object to avoid protected access errors. (#2251) +* Fix some mypy type checking errors in test_transaction.py (#2250) +* Update check for windows platform (#2247) +* Logging 100% coverage. (#2248) +* CI, Block draft PRs to use CPU minutes. (#2245, #2246) +* Remove kwargs client. (#2243, #2244, #2257) +* remove kwargs PDU messagees. (#2240) +* Remove message_generator example (not part of API). (#2239) +* Update dev dependencies (#2241) +* Fix ruff check in CI (#2242) +* Remove kwargs. (#2236, #2237) +* Simulator config, kwargs -> parameters. (#2235) +* Refactor transaction handling to better separate async and sync code. (#2232) +* Simplify some BinaryPayload pack operations (#2224) +* Fix writing to serial (rs485) on windows os. (#2191) +* Remember to remove serial writer. (#2209) +* Transaction_id for serial == 0. (#2208) +* Solve pylint error. +* Sync TLS needs time before reading frame (#2186) +* Update transaction.py (#2174) +* PDU classes --> pymodbus/pdu. (#2160) +* Speed up no data detection. (#2150) +* RTU decode hunt part. (#2138) +* Dislodge client classes from modbusProtocol. (#2137) +* Merge new message layer and old framer directory. (#2135) +* Coverage == 91%. (#2132) +* Remove binary_framer. (#2130) +* on_reconnect_callback --> on_connect_callback. (#2122) +* Remove certfile,keyfile,password from TLS client. (#2121) +* Drop support for python 3.8 (#2112) + + Version 3.6.9 ------------- * Remove python 3.8 from CI diff --git a/MAKE_RELEASE.rst b/MAKE_RELEASE.rst index a919eb9d4..9bed9c862 100644 --- a/MAKE_RELEASE.rst +++ b/MAKE_RELEASE.rst @@ -8,14 +8,14 @@ Making a release. ------------------------------------------------------------ Prepare/make release on dev. ------------------------------------------------------------ -* Make pull request "prepare v3.6.x", with the following: +* Make pull request "prepare v3.7.x", with the following: * Update pymodbus/__init__.py with version number (__version__ X.Y.Zpre) * Update README.rst "Supported versions" * Control / Update API_changes.rst * Update CHANGELOG.rst * Add commits from last release, but selectively ! - git log --oneline v3.6.6..HEAD > commit.log - git log --pretty="%an" v3.6.6..HEAD | sort -uf > authors.log + git log --oneline v3.7.0..HEAD > commit.log + git log --pretty="%an" v3.7.0..HEAD | sort -uf > authors.log update AUTHORS.rst and CHANGELOG.rst cd doc; ./build_html * rm -rf build/* dist/* @@ -30,13 +30,13 @@ Prepare/make release on dev. * git branch -D master * wait for CI to complete on all branches * On github "prepare release" - * Create tag e.g. v3.4.0dev0 - * Title "pymodbus v3.4.0dev0" + * Create tag e.g. v3.7.0dev0 + * Title "pymodbus v3.7.0dev0" * do NOT generate release notes, but copy from CHANGELOG.rst * make release (remember to mark pre-release if so) * on local repo * git pull, check release tag is pulled - * git checkout v3.0.0dev0 + * git checkout v3.7.0dev0 * rm -rf build/* dist/* * python3 -m build * twine upload dist/* (upload to pypi) diff --git a/README.rst b/README.rst index 6b3792cba..30a21ee60 100644 --- a/README.rst +++ b/README.rst @@ -11,10 +11,13 @@ PyModbus - A Python Modbus Stack Pymodbus is a full Modbus protocol implementation offering client/server with synchronous/asynchronous API a well as simulators. -Current release is `3.6.9 `_. +Current release is `3.7.0 `_. Bleeding edge (not released) is `dev `_. +Waiting for v3.8.0 (not released) is `wait3.8.0 `_. This contains +dev + merged pull requests that have API changes, and thus have to wait. + All changes are described in `release notes `_ and all API changes are `documented `_ @@ -40,9 +43,9 @@ Common features * support all standard frames: socket, rtu, rtu-over-tcp, tcp and ascii * does not have third party dependencies, apart from pyserial (optional) * very lightweight project -* requires Python >= 3.8 +* 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.8 - 3.12 +* automatically tested on Windows, Linux and MacOS combined with python 3.9 - 3.12 * strongly typed API (py.typed present) The modbus protocol specification: Modbus_Application_Protocol_V1_1b3.pdf can be found on @@ -276,7 +279,7 @@ There are 2 bigger projects ongoing: Development instructions ------------------------ -The current code base is compatible with python >= 3.8. +The current code base is compatible with python >= 3.9. Here are some of the common commands to perform a range of activities:: diff --git a/check_ci.sh b/check_ci.sh index 109b947ba..873c2311e 100755 --- a/check_ci.sh +++ b/check_ci.sh @@ -9,5 +9,5 @@ codespell ruff check --fix --exit-non-zero-on-fix . pylint --recursive=y examples pymodbus test mypy pymodbus -pytest --cov --numprocesses auto +pytest -x --cov --numprocesses auto echo "Ready to push" diff --git a/doc/index.rst b/doc/index.rst index 8c226ea1a..433f78f4b 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -9,6 +9,7 @@ Please select a topic in the left hand column. :hidden: source/readme + source/api_changes source/client source/server source/repl diff --git a/doc/source/_static/examples.tgz b/doc/source/_static/examples.tgz index 6859dac96..65294ff1e 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 220ca22db..b853a45dc 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 9b3a6f9c4..cec456a5b 100644 --- a/doc/source/client.rst +++ b/doc/source/client.rst @@ -177,7 +177,8 @@ 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! +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! 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. diff --git a/doc/source/examples.rst b/doc/source/examples.rst index 74b01b811..1ec97e8cf 100644 --- a/doc/source/examples.rst +++ b/doc/source/examples.rst @@ -177,15 +177,6 @@ Source: :github:`examples/datastore_simulator_share.py` :noindex: -Message generator -^^^^^^^^^^^^^^^^^ -Source: :github:`examples/message_generator.py` - -.. automodule:: examples.message_generator - :undoc-members: - :noindex: - - Message Parser ^^^^^^^^^^^^^^ Source: :github:`examples/message_parser.py` diff --git a/doc/source/library/framer.rst b/doc/source/library/framer.rst index c5d702db6..32204cfaf 100644 --- a/doc/source/library/framer.rst +++ b/doc/source/library/framer.rst @@ -1,34 +1,34 @@ Framer ====== -pymodbus\.framer\.ascii_framer module -------------------------------------- +pymodbus\.framer\.ModbusAsciiFramer module +------------------------------------------ -.. automodule:: pymodbus.framer.ascii_framer +.. automodule:: pymodbus.framer.ModbusAsciiFramer :members: :undoc-members: :show-inheritance: -pymodbus\.framer\.binary_framer module --------------------------------------- +pymodbus\.framer\.ModbusRtuFramer module +---------------------------------------- -.. automodule:: pymodbus.framer.binary_framer +.. automodule:: pymodbus.framer.ModbusRtuFramer :members: :undoc-members: :show-inheritance: -pymodbus\.framer\.rtu_framer module ------------------------------------ +pymodbus\.framer\.ModbusSocketFramer module +------------------------------------------- -.. automodule:: pymodbus.framer.rtu_framer +.. automodule:: pymodbus.framer.ModbusSocketFramer :members: :undoc-members: :show-inheritance: -pymodbus\.framer\.socket_framer module --------------------------------------- +pymodbus\.framer\.ModbusTlsFramer module +---------------------------------------- -.. automodule:: pymodbus.framer.socket_framer +.. automodule:: pymodbus.framer.ModbusTlsFramer :members: :undoc-members: :show-inheritance: diff --git a/doc/source/library/pymodbus.rst b/doc/source/library/pymodbus.rst index 9875fd1ed..cdda34602 100644 --- a/doc/source/library/pymodbus.rst +++ b/doc/source/library/pymodbus.rst @@ -6,84 +6,87 @@ Extra functions :undoc-members: :show-inheritance: - -.. automodule:: pymodbus.bit_read_message +.. automodule:: pymodbus.device :members: :undoc-members: :show-inheritance: -.. automodule:: pymodbus.bit_write_message +.. automodule:: pymodbus.events :members: :undoc-members: :show-inheritance: -.. automodule:: pymodbus.device +.. automodule:: pymodbus.exceptions :members: :undoc-members: :show-inheritance: -.. automodule:: pymodbus.diag_message +.. automodule:: pymodbus.factory :members: :undoc-members: :show-inheritance: -.. automodule:: pymodbus.events +.. automodule:: pymodbus.payload :members: :undoc-members: :show-inheritance: -.. automodule:: pymodbus.exceptions +.. automodule:: pymodbus.transaction :members: :undoc-members: :show-inheritance: -.. automodule:: pymodbus.factory +.. automodule:: pymodbus.utilities :members: :undoc-members: :show-inheritance: -.. automodule:: pymodbus.file_message + +PDU classes +=========== + +.. automodule:: pymodbus.pdu.bit_read_message :members: :undoc-members: :show-inheritance: -.. automodule:: pymodbus.mei_message +.. automodule:: pymodbus.pdu.bit_write_message :members: :undoc-members: :show-inheritance: -.. automodule:: pymodbus.other_message +.. automodule:: pymodbus.pdu.diag_message :members: :undoc-members: :show-inheritance: -.. automodule:: pymodbus.payload +.. automodule:: pymodbus.pdu.file_message :members: :undoc-members: :show-inheritance: -.. automodule:: pymodbus.pdu +.. automodule:: pymodbus.pdu.mei_message :members: :undoc-members: :show-inheritance: - :noindex: -.. automodule:: pymodbus.register_read_message +.. automodule:: pymodbus.pdu.other_message :members: :undoc-members: :show-inheritance: -.. automodule:: pymodbus.register_write_message +.. automodule:: pymodbus.pdu.pdu :members: :undoc-members: :show-inheritance: + :noindex: -.. automodule:: pymodbus.transaction +.. automodule:: pymodbus.pdu.register_read_message :members: :undoc-members: :show-inheritance: -.. automodule:: pymodbus.utilities +.. automodule:: pymodbus.pdu.register_write_message :members: :undoc-members: :show-inheritance: diff --git a/doc/source/library/simulator/config.rst b/doc/source/library/simulator/config.rst index 0d6f31a3c..9349a313c 100644 --- a/doc/source/library/simulator/config.rst +++ b/doc/source/library/simulator/config.rst @@ -63,11 +63,13 @@ The entry “comm” allows the following values: The entry “framer” allows the following values: -- “ascii” to use :class:`pymodbus.framer.ascii_framer.ModbusAsciiFramer`, -- "binary to use :class:`pymodbus.framer.ascii_framer.ModbusBinaryFramer`, -- “rtu” to use :class:`pymodbus.framer.ascii_framer.ModbusRtuFramer`, -- “tls” to use :class:`pymodbus.framer.ascii_framer.ModbusTlsFramer`, -- “socket” to use :class:`pymodbus.framer.ascii_framer.ModbusSocketFramer`. +- “ascii” to use :class:`pymodbus.framer.ModbusAsciiFramer`, +- “rtu” to use :class:`pymodbus.framer.ModbusRtuFramer`, +- “tls” to use :class:`pymodbus.framer.ModbusTlsFramer`, +- “socket” to use :class:`pymodbus.framer.ModbusSocketFramer`. + +Optional entry "device_id" will limit server to only accept a single id. If +not set, the server will accept all device id. .. warning:: @@ -289,8 +291,8 @@ In case of **"increment"**, the counter is reset to the minimum value, if the ma .. code-block:: - {"addr": 9, "value": 7, "action": "random", "kwargs": {"minval": 0, "maxval": 12} }, - {"addr": 10, "value": 100, "action": "increment", "kwargs": {"minval": 50} } + {"addr": 9, "value": 7, "action": "random", "parameters": {"minval": 0, "maxval": 12} }, + {"addr": 10, "value": 100, "action": "increment", "parameters": {"minval": 50} } Invalid section diff --git a/examples/client_async.py b/examples/client_async.py index 105de9a75..281b7792d 100755 --- a/examples/client_async.py +++ b/examples/client_async.py @@ -4,7 +4,7 @@ usage:: client_async.py [-h] [-c {tcp,udp,serial,tls}] - [-f {ascii,binary,rtu,socket,tls}] + [-f {ascii,rtu,socket,tls}] [-l {critical,error,warning,info,debug}] [-p PORT] [--baudrate BAUDRATE] [--host HOST] @@ -12,7 +12,7 @@ show this help message and exit -c, -comm {tcp,udp,serial,tls} set communication, default is tcp - -f, --framer {ascii,binary,rtu,socket,tls} + -f, --framer {ascii,rtu,socket,tls} set framer, default depends on --comm -l, --log {critical,error,warning,info,debug} set log level, default is info @@ -64,8 +64,6 @@ def setup_async_client(description=None, cmdline=None): retries=3, reconnect_delay=1, reconnect_delay_max=10, - # retry_on_empty=False, - # TCP setup parameters # source_address=("localhost", 0), ) elif args.comm == "udp": @@ -76,7 +74,6 @@ def setup_async_client(description=None, cmdline=None): framer=args.framer, timeout=args.timeout, # retries=3, - # retry_on_empty=False, # UDP setup parameters # source_address=None, ) @@ -87,14 +84,12 @@ def setup_async_client(description=None, cmdline=None): # framer=ModbusRtuFramer, timeout=args.timeout, # retries=3, - # retry_on_empty=False, # Serial setup parameters baudrate=args.baudrate, # bytesize=8, # parity="N", # stopbits=1, # handle_local_echo=False, - # strict=True, ) elif args.comm == "tls": client = modbusClient.AsyncModbusTlsClient( @@ -104,14 +99,15 @@ def setup_async_client(description=None, cmdline=None): framer=args.framer, timeout=args.timeout, # retries=3, - # retry_on_empty=False, # TLS setup parameters - # sslctx=sslctx, - certfile=helper.get_certificate("crt"), - keyfile=helper.get_certificate("key"), + sslctx=modbusClient.AsyncModbusTlsClient.generate_ssl( + certfile=helper.get_certificate("crt"), + keyfile=helper.get_certificate("key"), # password="none", - server_hostname="localhost", + ), ) + else: + raise RuntimeError(f"Unknown commtype {args.comm}") return client diff --git a/examples/client_async_calls.py b/examples/client_async_calls.py index 482a9ec40..bf4d66b43 100755 --- a/examples/client_async_calls.py +++ b/examples/client_async_calls.py @@ -137,7 +137,7 @@ async def async_handle_holding_registers(client): assert not rr.isError() # test that call was OK assert rr.registers == [10] * 8 - _logger.info("### write read holding registers, using **kwargs") + _logger.info("### write read holding registers") arguments = { "read_address": 1, "read_count": 8, diff --git a/examples/client_calls.py b/examples/client_calls.py index 50a58bc3d..c1de95f93 100755 --- a/examples/client_calls.py +++ b/examples/client_calls.py @@ -138,7 +138,7 @@ def handle_holding_registers(client): assert not rr.isError() # test that call was OK assert rr.registers == [10] * 8 - _logger.info("### write read holding registers, using **kwargs") + _logger.info("### write read holding registers") arguments = { "read_address": 1, "read_count": 8, diff --git a/examples/client_custom_msg.py b/examples/client_custom_msg.py index 8b353e342..bc44658df 100755 --- a/examples/client_custom_msg.py +++ b/examples/client_custom_msg.py @@ -13,10 +13,10 @@ import asyncio import struct -from pymodbus import Framer -from pymodbus.bit_read_message import ReadCoilsRequest +from pymodbus import FramerType from pymodbus.client import AsyncModbusTcpClient as ModbusClient from pymodbus.pdu import ModbusExceptions, ModbusRequest, ModbusResponse +from pymodbus.pdu.bit_read_message import ReadCoilsRequest # --------------------------------------------------------------------------- # @@ -36,9 +36,9 @@ class CustomModbusResponse(ModbusResponse): function_code = 55 _rtu_byte_count_pos = 2 - def __init__(self, values=None, **kwargs): + def __init__(self, values=None, slave=1, transaction=0, protocol=0, skip_encode=False): """Initialize.""" - ModbusResponse.__init__(self, **kwargs) + ModbusResponse.__init__(self, slave, transaction, protocol, skip_encode) self.values = values or [] def encode(self): @@ -68,9 +68,9 @@ class CustomModbusRequest(ModbusRequest): function_code = 55 _rtu_frame_size = 8 - def __init__(self, address=None, **kwargs): + def __init__(self, address=None, slave=1, transaction=0, protocol=0, skip_encode=False): """Initialize.""" - ModbusRequest.__init__(self, **kwargs) + ModbusRequest.__init__(self, slave, transaction, protocol, skip_encode) self.address = address self.count = 16 @@ -100,12 +100,12 @@ def execute(self, context): class Read16CoilsRequest(ReadCoilsRequest): """Read 16 coils in one request.""" - def __init__(self, address, **kwargs): + def __init__(self, address, count=None, slave=1, transaction=0, protocol=0, skip_encode=False): """Initialize a new instance. :param address: The address to start reading from """ - ReadCoilsRequest.__init__(self, address, 16, **kwargs) + ReadCoilsRequest.__init__(self, address, count=16, slave=slave, transaction=transaction, protocol=protocol, skip_encode=skip_encode) # --------------------------------------------------------------------------- # @@ -118,17 +118,21 @@ def __init__(self, address, **kwargs): async def main(host="localhost", port=5020): """Run versions of read coil.""" - async with ModbusClient(host=host, port=port, framer_name=Framer.SOCKET) as client: + async with ModbusClient(host=host, port=port, framer=FramerType.SOCKET) as client: await client.connect() + # create a response object to control it works + CustomModbusResponse() + # new modbus function code. client.register(CustomModbusResponse) - request = CustomModbusRequest(32, slave=1) + slave=1 + request = CustomModbusRequest(32, slave=slave) result = await client.execute(request) print(result) # inherited request - request = Read16CoilsRequest(32, slave=1) + request = Read16CoilsRequest(32, slave) result = await client.execute(request) print(result) diff --git a/examples/client_performance.py b/examples/client_performance.py index 09cf25572..43a6f9a4e 100755 --- a/examples/client_performance.py +++ b/examples/client_performance.py @@ -16,7 +16,7 @@ import asyncio import time -from pymodbus import Framer +from pymodbus import FramerType from pymodbus.client import AsyncModbusSerialClient, ModbusSerialClient @@ -29,7 +29,7 @@ def run_sync_client_test(): print("--- Testing sync client v3.4.1") client = ModbusSerialClient( "/dev/ttys007", - framer_name=Framer.RTU, + framer=FramerType.RTU, baudrate=9600, ) client.connect() @@ -56,7 +56,7 @@ async def run_async_client_test(): print("--- Testing async client v3.4.1") client = AsyncModbusSerialClient( "/dev/ttys007", - framer_name=Framer.RTU, + framer=FramerType.RTU, baudrate=9600, ) await client.connect() diff --git a/examples/client_sync.py b/examples/client_sync.py index eb634baff..6367306d0 100755 --- a/examples/client_sync.py +++ b/examples/client_sync.py @@ -6,7 +6,7 @@ usage:: client_sync.py [-h] [-c {tcp,udp,serial,tls}] - [-f {ascii,binary,rtu,socket,tls}] + [-f {ascii,rtu,socket,tls}] [-l {critical,error,warning,info,debug}] [-p PORT] [--baudrate BAUDRATE] [--host HOST] @@ -14,7 +14,7 @@ show this help message and exit -c, --comm {tcp,udp,serial,tls} set communication, default is tcp - -f, --framer {ascii,binary,rtu,socket,tls} + -f, --framer {ascii,rtu,socket,tls} set framer, default depends on --comm -l, --log {critical,error,warning,info,debug} set log level, default is info @@ -67,8 +67,6 @@ def setup_sync_client(description=None, cmdline=None): framer=args.framer, timeout=args.timeout, # retries=3, - # retry_on_empty=False,y - # strict=True, # TCP setup parameters # source_address=("localhost", 0), ) @@ -80,8 +78,6 @@ def setup_sync_client(description=None, cmdline=None): framer=args.framer, timeout=args.timeout, # retries=3, - # retry_on_empty=False, - # strict=True, # UDP setup parameters # source_address=None, ) @@ -92,15 +88,12 @@ def setup_sync_client(description=None, cmdline=None): # framer=ModbusRtuFramer, timeout=args.timeout, # retries=3, - # retry_on_empty=False, - # strict=True, # Serial setup parameters baudrate=args.baudrate, # bytesize=8, # parity="N", # stopbits=1, # handle_local_echo=False, - # strict=True, ) elif args.comm == "tls": client = modbusClient.ModbusTlsClient( @@ -110,13 +103,12 @@ def setup_sync_client(description=None, cmdline=None): framer=args.framer, timeout=args.timeout, # retries=3, - # retry_on_empty=False, # TLS setup parameters - # sslctx=None, - certfile=helper.get_certificate("crt"), - keyfile=helper.get_certificate("key"), + sslctx=modbusClient.ModbusTlsClient.generate_ssl( + certfile=helper.get_certificate("crt"), + keyfile=helper.get_certificate("key"), # password=None, - server_hostname="localhost", + ), ) return client diff --git a/examples/contrib/explain.py b/examples/contrib/explain.py index f91fe8964..64f10c83f 100644 --- a/examples/contrib/explain.py +++ b/examples/contrib/explain.py @@ -1,7 +1,5 @@ """ How to explain pymodbus logs using https://rapidscada.net/modbus/ and requests. - -Created on 7/19/2023 to support Python 3.8 to 3.11 on macOS, Ubuntu, or Windows. """ from __future__ import annotations diff --git a/examples/contrib/redis_datastore.py b/examples/contrib/redis_datastore.py index 14d96d633..ca974426f 100644 --- a/examples/contrib/redis_datastore.py +++ b/examples/contrib/redis_datastore.py @@ -17,17 +17,16 @@ class RedisSlaveContext(ModbusBaseSlaveContext): """This is a modbus slave context using redis as a backing store.""" - def __init__(self, **kwargs): + 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 """ - host = kwargs.get("host", "localhost") - port = kwargs.get("port", 6379) - self.prefix = kwargs.get("prefix", "pymodbus") - self.client = kwargs.get("client", redis.Redis(host=host, port=port)) + self.prefix = prefix + self.client = client if client else redis.Redis(host=host, port=port) self._build_mapping() def __str__(self): diff --git a/examples/contrib/solar.py b/examples/contrib/solar.py index 406f978fe..a2e951f96 100755 --- a/examples/contrib/solar.py +++ b/examples/contrib/solar.py @@ -31,7 +31,6 @@ def main(): # Common optional parameters: framer=ModbusSocketFramer, timeout=1, - retry_on_empty=True, ) client.connect() _logger.info("### Client connected") diff --git a/examples/contrib/sql_datastore.py b/examples/contrib/sql_datastore.py index b882b7f11..2d6387a8b 100644 --- a/examples/contrib/sql_datastore.py +++ b/examples/contrib/sql_datastore.py @@ -20,17 +20,18 @@ class SqlSlaveContext(ModbusBaseSlaveContext): """This creates a modbus data model with each data access in its a block.""" - def __init__(self, *_args, **kwargs): + def __init__(self, *_args, table="pymodbus", database=None): """Initialize the datastores. - :param kwargs: Each element is a ModbusDataBlock + :param table: table name + :param database: database """ self._engine = None self._metadata = None self._table = None self._connection = None - self.table = kwargs.get("table", "pymodbus") - self.database = kwargs.get("database", "sqlite:///:memory:") + self.table = table + self.database = database if database else "sqlite:///:memory:" self._db_create(self.table, self.database) def __str__(self): diff --git a/examples/helper.py b/examples/helper.py index 4581014b0..d1d93b64d 100755 --- a/examples/helper.py +++ b/examples/helper.py @@ -20,7 +20,7 @@ def get_commandline(server=False, description=None, extras=None, cmdline=None): parser.add_argument( "-c", "--comm", - choices=["tcp", "udp", "serial", "tls"], + choices=["tcp", "udp", "serial", "tls", "unknown"], help="set communication, default is tcp", dest="comm", default="tcp", @@ -29,7 +29,7 @@ def get_commandline(server=False, description=None, extras=None, cmdline=None): parser.add_argument( "-f", "--framer", - choices=["ascii", "binary", "rtu", "socket", "tls"], + choices=["ascii", "rtu", "socket", "tls"], help="set framer, default depends on --comm", dest="framer", type=str, diff --git a/examples/message_generator.py b/examples/message_generator.py deleted file mode 100755 index 4f49a67d9..000000000 --- a/examples/message_generator.py +++ /dev/null @@ -1,218 +0,0 @@ -#!/usr/bin/env python3 -"""Modbus Message Generator.""" -import argparse -import codecs as c -import logging - -import pymodbus.bit_read_message as modbus_bit -import pymodbus.bit_write_message as modbus_bit_write -import pymodbus.diag_message as modbus_diag -import pymodbus.file_message as modbus_file -import pymodbus.mei_message as modbus_mei -import pymodbus.other_message as modbus_other -import pymodbus.register_read_message as modbus_register -import pymodbus.register_write_message as modbus_register_write -from pymodbus.transaction import ( - ModbusAsciiFramer, - ModbusBinaryFramer, - ModbusRtuFramer, - ModbusSocketFramer, -) - - -_logger = logging.getLogger(__file__) - - -# -------------------------------------------------------------------------- # -# enumerate all request/response messages -# -------------------------------------------------------------------------- # -messages = [ - ( - modbus_register.ReadHoldingRegistersRequest, - modbus_register.ReadHoldingRegistersResponse, - ), - (modbus_bit.ReadDiscreteInputsRequest, modbus_bit.ReadDiscreteInputsResponse), - ( - modbus_register.ReadInputRegistersRequest, - modbus_register.ReadInputRegistersResponse, - ), - (modbus_bit.ReadCoilsRequest, modbus_bit.ReadCoilsResponse), - ( - modbus_bit_write.WriteMultipleCoilsRequest, - modbus_bit_write.WriteMultipleCoilsResponse, - ), - ( - modbus_register_write.WriteMultipleRegistersRequest, - modbus_register_write.WriteMultipleRegistersResponse, - ), - ( - modbus_register_write.WriteSingleRegisterRequest, - modbus_register_write.WriteSingleRegisterResponse, - ), - (modbus_bit_write.WriteSingleCoilRequest, modbus_bit_write.WriteSingleCoilResponse), - ( - modbus_register.ReadWriteMultipleRegistersRequest, - modbus_register.ReadWriteMultipleRegistersResponse, - ), - (modbus_other.ReadExceptionStatusRequest, modbus_other.ReadExceptionStatusResponse), - (modbus_other.GetCommEventCounterRequest, modbus_other.GetCommEventCounterResponse), - (modbus_other.GetCommEventLogRequest, modbus_other.GetCommEventLogResponse), - (modbus_other.ReportSlaveIdRequest, modbus_other.ReportSlaveIdResponse), - (modbus_file.ReadFileRecordRequest, modbus_file.ReadFileRecordResponse), - (modbus_file.WriteFileRecordRequest, modbus_file.WriteFileRecordResponse), - ( - modbus_register_write.MaskWriteRegisterRequest, - modbus_register_write.MaskWriteRegisterResponse, - ), - (modbus_file.ReadFifoQueueRequest, modbus_file.ReadFifoQueueResponse), - (modbus_mei.ReadDeviceInformationRequest, modbus_mei.ReadDeviceInformationResponse), - (modbus_diag.ReturnQueryDataRequest, modbus_diag.ReturnQueryDataResponse), - ( - modbus_diag.RestartCommunicationsOptionRequest, - modbus_diag.RestartCommunicationsOptionResponse, - ), - ( - modbus_diag.ReturnDiagnosticRegisterRequest, - modbus_diag.ReturnDiagnosticRegisterResponse, - ), - ( - modbus_diag.ChangeAsciiInputDelimiterRequest, - modbus_diag.ChangeAsciiInputDelimiterResponse, - ), - (modbus_diag.ForceListenOnlyModeRequest, modbus_diag.ForceListenOnlyModeResponse), - (modbus_diag.ClearCountersRequest, modbus_diag.ClearCountersResponse), - ( - modbus_diag.ReturnBusMessageCountRequest, - modbus_diag.ReturnBusMessageCountResponse, - ), - ( - modbus_diag.ReturnBusCommunicationErrorCountRequest, - modbus_diag.ReturnBusCommunicationErrorCountResponse, - ), - ( - modbus_diag.ReturnBusExceptionErrorCountRequest, - modbus_diag.ReturnBusExceptionErrorCountResponse, - ), - ( - modbus_diag.ReturnSlaveMessageCountRequest, - modbus_diag.ReturnSlaveMessageCountResponse, - ), - ( - modbus_diag.ReturnSlaveNoResponseCountRequest, - modbus_diag.ReturnSlaveNoResponseCountResponse, - ), - (modbus_diag.ReturnSlaveNAKCountRequest, modbus_diag.ReturnSlaveNAKCountResponse), - (modbus_diag.ReturnSlaveBusyCountRequest, modbus_diag.ReturnSlaveBusyCountResponse), - ( - modbus_diag.ReturnSlaveBusCharacterOverrunCountRequest, - modbus_diag.ReturnSlaveBusCharacterOverrunCountResponse, - ), - ( - modbus_diag.ReturnIopOverrunCountRequest, - modbus_diag.ReturnIopOverrunCountResponse, - ), - (modbus_diag.ClearOverrunCountRequest, modbus_diag.ClearOverrunCountResponse), - (modbus_diag.GetClearModbusPlusRequest, modbus_diag.GetClearModbusPlusResponse), -] - - -def get_commandline(cmdline=None): - """Parse the command line options.""" - parser = argparse.ArgumentParser() - parser.add_argument( - "--framer", - choices=["ascii", "binary", "rtu", "socket"], - help="set framer, default is rtu", - dest="framer", - default="rtu", - type=str, - ) - parser.add_argument( - "-l", - "--log", - choices=["critical", "error", "warning", "info", "debug"], - help="set log level, default is info", - dest="log", - default="info", - type=str, - ) - parser.add_argument( - "-a", - "--address", - help="address to use", - dest="address", - default=32, - type=int, - ) - parser.add_argument( - "-c", - "--count", - help="count to use", - dest="count", - default=8, - type=int, - ) - parser.add_argument( - "-v", - "--value", - help="value to use", - dest="value", - default=1, - type=int, - ) - parser.add_argument( - "-t", - "--transaction", - help="transaction to use", - dest="transaction", - default=1, - type=int, - ) - parser.add_argument( - "-s", - "--slave", - help="slave to use", - dest="slave", - default=1, - type=int, - ) - args = parser.parse_args(cmdline) - return args - - -def generate_messages(cmdline=None): - """Parse the command line options.""" - args = get_commandline(cmdline=cmdline) - _logger.setLevel(args.log.upper()) - - arguments = { - "address": args.address, - "count": args.count, - "value": args.value, - "values": [args.value] * args.count, - "read_address": args.address, - "read_count": args.count, - "write_address": args.address, - "write_registers": [args.value] * args.count, - "transaction": args.transaction, - "slave": args.slave, - "protocol": 0x00, - } - framer = { - "ascii": ModbusAsciiFramer, - "binary": ModbusBinaryFramer, - "rtu": ModbusRtuFramer, - "socket": ModbusSocketFramer, - }[args.framer](None) - - for entry in messages: - for inx in (0, 1): - message = entry[inx](**arguments) - raw_packet = framer.buildPacket(message) - packet = c.encode(raw_packet, "hex_codec").decode("utf-8") - print(f"{message.__class__.__name__:44} = {packet}") - print("") - - -if __name__ == "__main__": - generate_messages() diff --git a/examples/message_parser.py b/examples/message_parser.py index e95c4a29e..67bf68191 100755 --- a/examples/message_parser.py +++ b/examples/message_parser.py @@ -15,7 +15,6 @@ from pymodbus.factory import ClientDecoder, ServerDecoder from pymodbus.transaction import ( ModbusAsciiFramer, - ModbusBinaryFramer, ModbusRtuFramer, ModbusSocketFramer, ) @@ -30,7 +29,7 @@ def get_commandline(cmdline): parser.add_argument( "--framer", - choices=["ascii", "binary", "rtu", "socket"], + choices=["ascii", "rtu", "socket"], help="set framer, default is rtu", type=str, default="rtu", @@ -148,7 +147,6 @@ def parse_messages(cmdline=None): framer = { "ascii": ModbusAsciiFramer, - "binary": ModbusBinaryFramer, "rtu": ModbusRtuFramer, "socket": ModbusSocketFramer, }[args.framer] diff --git a/examples/package_test_tool.py b/examples/package_test_tool.py index b46040c51..9e1020c94 100755 --- a/examples/package_test_tool.py +++ b/examples/package_test_tool.py @@ -45,11 +45,11 @@ from __future__ import annotations import asyncio -from typing import Callable +from collections.abc import Callable import pymodbus.client as modbusClient import pymodbus.server as modbusServer -from pymodbus import Framer, ModbusException, pymodbus_apply_logging_config +from pymodbus import FramerType, ModbusException, pymodbus_apply_logging_config from pymodbus.datastore import ( ModbusSequentialDataBlock, ModbusServerContext, @@ -122,7 +122,7 @@ def __init__(self, comm: CommType): ) else: raise RuntimeError("ERROR: CommType not implemented") - server_params = self.client.comm_params.copy() + server_params = self.client.ctx.comm_params.copy() server_params.source_address = (host, test_port) self.stub = TransportStub(server_params, True, simulate_server) test_port += 1 @@ -160,14 +160,14 @@ def __init__(self, comm: CommType): if comm == CommType.TCP: self.server = modbusServer.ModbusTcpServer( self.context, - framer=Framer.SOCKET, + framer=FramerType.SOCKET, identity=self.identity, address=(NULLMODEM_HOST, test_port), ) elif comm == CommType.SERIAL: self.server = modbusServer.ModbusSerialServer( self.context, - framer=Framer.SOCKET, + framer=FramerType.SOCKET, identity=self.identity, port=f"{NULLMODEM_HOST}:{test_port}", ) @@ -207,7 +207,7 @@ async def client_calls(client): """Test client API.""" Log.debug("--> Client calls starting.") try: - resp = await client.read_holding_registers(address=124, count=4, slave=0) + resp = await client.read_holding_registers(address=124, count=4, slave=1) except ModbusException as exc: txt = f"ERROR: exception in pymodbus {exc}" Log.error(txt) diff --git a/examples/server_async.py b/examples/server_async.py index 859f5cd6b..6835426d5 100755 --- a/examples/server_async.py +++ b/examples/server_async.py @@ -6,7 +6,7 @@ usage:: server_async.py [-h] [--comm {tcp,udp,serial,tls}] - [--framer {ascii,binary,rtu,socket,tls}] + [--framer {ascii,rtu,socket,tls}] [--log {critical,error,warning,info,debug}] [--port PORT] [--store {sequential,sparse,factory,none}] [--slaves SLAVES] @@ -15,7 +15,7 @@ show this help message and exit -c, --comm {tcp,udp,serial,tls} set communication, default is tcp - -f, --framer {ascii,binary,rtu,socket,tls} + -f, --framer {ascii,rtu,socket,tls} set framer, default depends on --comm -l, --log {critical,error,warning,info,debug} set log level, default is info @@ -201,7 +201,6 @@ async def run_async_server(args): # handle_local_echo=False, # Handle local echo of the USB-to-RS485 adaptor # ignore_missing_slaves=True, # ignore request to a missing slave # broadcast_enable=False, # treat slave_id 0 as broadcast address, - # strict=True, # use strict timing, t1.5 for Modbus RTU ) elif args.comm == "tls": address = (args.host if args.host else "", args.port if args.port else None) diff --git a/examples/server_hook.py b/examples/server_hook.py index 755b8f933..a50035efb 100755 --- a/examples/server_hook.py +++ b/examples/server_hook.py @@ -7,7 +7,7 @@ import asyncio import logging -from pymodbus import Framer, pymodbus_apply_logging_config +from pymodbus import FramerType, pymodbus_apply_logging_config from pymodbus.datastore import ( ModbusSequentialDataBlock, ModbusServerContext, @@ -17,11 +17,7 @@ class Manipulator: - """A Class to run the server. - - Using a class allows the easy use of global variables, but - are not strictly needed - """ + """A Class to run the server.""" message_count: int = 1 server: ModbusTcpServer = None @@ -63,7 +59,7 @@ async def setup(self): ) self.server = ModbusTcpServer( context, - Framer.SOCKET, + FramerType.SOCKET, None, ("127.0.0.1", 5020), request_tracer=self.server_request_tracer, diff --git a/examples/server_sync.py b/examples/server_sync.py index b34d36ce4..66e2d7f4a 100755 --- a/examples/server_sync.py +++ b/examples/server_sync.py @@ -6,7 +6,7 @@ usage:: server_sync.py [-h] [--comm {tcp,udp,serial,tls}] - [--framer {ascii,binary,rtu,socket,tls}] + [--framer {ascii,rtu,socket,tls}] [--log {critical,error,warning,info,debug}] [--port PORT] [--store {sequential,sparse,factory,none}] [--slaves SLAVES] @@ -15,7 +15,7 @@ show this help message and exit -c, --comm {tcp,udp,serial,tls} set communication, default is tcp - -f, --framer {ascii,binary,rtu,socket,tls} + -f, --framer {ascii,rtu,socket,tls} set framer, default depends on --comm -l, --log {critical,error,warning,info,debug} set log level, default is info @@ -110,7 +110,6 @@ def run_sync_server(args): # handle_local_echo=False, # Handle local echo of the USB-to-RS485 adaptor # ignore_missing_slaves=True, # ignore request to a missing slave # broadcast_enable=False, # treat slave_id 0 as broadcast address, - # strict=True, # use strict timing, t1.5 for Modbus RTU ) elif args.comm == "tls": address = ("", args.port) if args.port else None diff --git a/examples/server_updating.py b/examples/server_updating.py index 10e0ce3cb..5ece64f23 100755 --- a/examples/server_updating.py +++ b/examples/server_updating.py @@ -7,7 +7,7 @@ usage:: server_updating.py [-h] [--comm {tcp,udp,serial,tls}] - [--framer {ascii,binary,rtu,socket,tls}] + [--framer {ascii,rtu,socket,tls}] [--log {critical,error,warning,info,debug}] [--port PORT] [--store {sequential,sparse,factory,none}] [--slaves SLAVES] @@ -16,7 +16,7 @@ show this help message and exit -c, --comm {tcp,udp,serial,tls} set communication, default is tcp - -f, --framer {ascii,binary,rtu,socket,tls} + -f, --framer {ascii,rtu,socket,tls} set framer, default depends on --comm -l, --log {critical,error,warning,info,debug} set log level, default is info diff --git a/examples/simple_async_client.py b/examples/simple_async_client.py index 722dc54f4..ba377c27d 100755 --- a/examples/simple_async_client.py +++ b/examples/simple_async_client.py @@ -14,13 +14,13 @@ import pymodbus.client as ModbusClient from pymodbus import ( ExceptionResponse, - Framer, + FramerType, ModbusException, pymodbus_apply_logging_config, ) -async def run_async_simple_client(comm, host, port, framer=Framer.SOCKET): +async def run_async_simple_client(comm, host, port, framer=FramerType.SOCKET): """Run async client.""" # activate debugging pymodbus_apply_logging_config("DEBUG") @@ -33,7 +33,6 @@ async def run_async_simple_client(comm, host, port, framer=Framer.SOCKET): framer=framer, # timeout=10, # retries=3, - # retry_on_empty=False, # source_address=("localhost", 0), ) elif comm == "udp": @@ -43,7 +42,6 @@ async def run_async_simple_client(comm, host, port, framer=Framer.SOCKET): framer=framer, # timeout=10, # retries=3, - # retry_on_empty=False, # source_address=None, ) elif comm == "serial": @@ -52,28 +50,12 @@ async def run_async_simple_client(comm, host, port, framer=Framer.SOCKET): framer=framer, # timeout=10, # retries=3, - # retry_on_empty=False, - # strict=True, baudrate=9600, bytesize=8, parity="N", stopbits=1, # handle_local_echo=False, ) - elif comm == "tls": - client = ModbusClient.AsyncModbusTlsClient( - host, - port=port, - framer=Framer.TLS, - # timeout=10, - # retries=3, - # retry_on_empty=False, - # sslctx=sslctx, - certfile="../examples/certificates/pymodbus.crt", - keyfile="../examples/certificates/pymodbus.key", - # password="none", - server_hostname="localhost", - ) else: print(f"Unknown client {comm} selected") return diff --git a/examples/simple_sync_client.py b/examples/simple_sync_client.py index 9e7b9d749..43dabd0c5 100755 --- a/examples/simple_sync_client.py +++ b/examples/simple_sync_client.py @@ -16,13 +16,13 @@ import pymodbus.client as ModbusClient from pymodbus import ( ExceptionResponse, - Framer, + FramerType, ModbusException, pymodbus_apply_logging_config, ) -def run_sync_simple_client(comm, host, port, framer=Framer.SOCKET): +def run_sync_simple_client(comm, host, port, framer=FramerType.SOCKET): """Run sync client.""" # activate debugging pymodbus_apply_logging_config("DEBUG") @@ -35,7 +35,6 @@ def run_sync_simple_client(comm, host, port, framer=Framer.SOCKET): framer=framer, # timeout=10, # retries=3, - # retry_on_empty=False,y # source_address=("localhost", 0), ) elif comm == "udp": @@ -45,7 +44,6 @@ def run_sync_simple_client(comm, host, port, framer=Framer.SOCKET): framer=framer, # timeout=10, # retries=3, - # retry_on_empty=False, # source_address=None, ) elif comm == "serial": @@ -54,28 +52,12 @@ def run_sync_simple_client(comm, host, port, framer=Framer.SOCKET): framer=framer, # timeout=10, # retries=3, - # retry_on_empty=False, - # strict=True, baudrate=9600, bytesize=8, parity="N", stopbits=1, # handle_local_echo=False, ) - elif comm == "tls": - client = ModbusClient.ModbusTlsClient( - host, - port=port, - framer=Framer.TLS, - # timeout=10, - # retries=3, - # retry_on_empty=False, - # sslctx=None, - certfile="../examples/certificates/pymodbus.crt", - keyfile="../examples/certificates/pymodbus.key", - # password=None, - server_hostname="localhost", - ) else: print(f"Unknown client {comm} selected") return diff --git a/examples/simulator.py b/examples/simulator.py index b75c92f7d..58f7ce6fb 100755 --- a/examples/simulator.py +++ b/examples/simulator.py @@ -10,7 +10,7 @@ import asyncio import logging -from pymodbus import Framer +from pymodbus import FramerType from pymodbus.client import AsyncModbusTcpClient from pymodbus.datastore import ModbusSimulatorContext from pymodbus.server import ModbusSimulatorServer, get_simulator_commandline @@ -74,7 +74,7 @@ async def run_simulator(): client = AsyncModbusTcpClient( "127.0.0.1", port=5020, - framer=Framer.SOCKET, + framer=FramerType.SOCKET, ) await client.connect() assert client.connected diff --git a/pymodbus/__init__.py b/pymodbus/__init__.py index 9500ad306..2a5de0535 100644 --- a/pymodbus/__init__.py +++ b/pymodbus/__init__.py @@ -5,7 +5,7 @@ __all__ = [ "ExceptionResponse", - "Framer", + "FramerType", "ModbusException", "pymodbus_apply_logging_config", "__version__", @@ -13,10 +13,10 @@ ] from pymodbus.exceptions import ModbusException -from pymodbus.framer import Framer +from pymodbus.framer import FramerType from pymodbus.logging import pymodbus_apply_logging_config from pymodbus.pdu import ExceptionResponse -__version__ = "3.6.9" +__version__ = "3.7.0" __version_full__ = f"[pymodbus, version {__version__}]" diff --git a/pymodbus/client/base.py b/pymodbus/client/base.py index 0aa15d147..26d7871b8 100644 --- a/pymodbus/client/base.py +++ b/pymodbus/client/base.py @@ -3,116 +3,67 @@ import asyncio import socket -from dataclasses import dataclass -from typing import Any, Awaitable, Callable, Type, cast +from abc import abstractmethod +from collections.abc import Awaitable, Callable +from typing import cast 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, Framer, ModbusFramer +from pymodbus.framer import FRAMER_NAME_TO_CLASS, FramerType, ModbusFramer from pymodbus.logging import Log from pymodbus.pdu import ModbusRequest, ModbusResponse -from pymodbus.transaction import ModbusTransactionManager -from pymodbus.transport import CommParams, ModbusProtocol +from pymodbus.transaction import SyncModbusTransactionManager +from pymodbus.transport import CommParams from pymodbus.utilities import ModbusTransactionState -class ModbusBaseClient(ModbusClientMixin[Awaitable[ModbusResponse]], ModbusProtocol): +class ModbusBaseClient(ModbusClientMixin[Awaitable[ModbusResponse]]): """**ModbusBaseClient**. - Fixed parameters: - - :param framer: Framer enum name - - Optional parameters: - - :param timeout: Timeout for a request, in seconds. - :param retries: Max number of retries per request. - :param retry_on_empty: Retry on empty response. - :param broadcast_enable: True to treat id 0 as broadcast address. - :param reconnect_delay: Minimum delay in seconds.milliseconds before reconnecting. - :param reconnect_delay_max: Maximum delay in seconds.milliseconds before reconnecting. - :param on_reconnect_callback: Function that will be called just before a reconnection attempt. - :param no_resend_on_retry: Do not resend request when retrying due to missing response. - :param kwargs: Experimental parameters. - - .. tip:: - **reconnect_delay** doubles automatically with each unsuccessful connect, from - **reconnect_delay** to **reconnect_delay_max**. - Set `reconnect_delay=0` to avoid automatic reconnection. - :mod:`ModbusBaseClient` is normally not referenced outside :mod:`pymodbus`. - - **Application methods, common to all clients**: """ def __init__( self, - framer: Framer, - timeout: float = 3, - retries: int = 3, - retry_on_empty: bool = False, - broadcast_enable: bool = False, - reconnect_delay: float = 0.1, - reconnect_delay_max: float = 300, - on_reconnect_callback: Callable[[], None] | None = None, - no_resend_on_retry: bool = False, - **kwargs: Any, + framer: FramerType, + retries: int, + on_connect_callback: Callable[[bool], None] | None, + comm_params: CommParams | None = None, ) -> None: """Initialize a client instance.""" ModbusClientMixin.__init__(self) # type: ignore[arg-type] - ModbusProtocol.__init__( - self, - CommParams( - comm_type=kwargs.get("CommType"), - comm_name="comm", - source_address=kwargs.get("source_address", None), - reconnect_delay=reconnect_delay, - reconnect_delay_max=reconnect_delay_max, - timeout_connect=timeout, - host=kwargs.get("host", None), - port=kwargs.get("port", 0), - sslctx=kwargs.get("sslctx", None), - baudrate=kwargs.get("baudrate", None), - bytesize=kwargs.get("bytesize", None), - parity=kwargs.get("parity", None), - stopbits=kwargs.get("stopbits", None), - handle_local_echo=kwargs.get("handle_local_echo", False), - ), - False, + if comm_params: + self.comm_params = comm_params + self.retries = retries + self.ctx = ModbusClientProtocol( + framer, + self.comm_params, + on_connect_callback, ) - self.on_reconnect_callback = on_reconnect_callback - self.retry_on_empty: int = 0 - self.no_resend_on_retry = no_resend_on_retry - self.slaves: list[int] = [] - self.retries: int = retries - self.broadcast_enable = broadcast_enable # Common variables. - self.framer = FRAMER_NAME_TO_CLASS.get( - framer, cast(Type[ModbusFramer], framer) - )(ClientDecoder(), self) - self.transaction = ModbusTransactionManager( - self, retries=retries, retry_on_empty=retry_on_empty, **kwargs - ) self.use_udp = False self.state = ModbusTransactionState.IDLE self.last_frame_end: float | None = 0 self.silent_interval: float = 0 self._lock = asyncio.Lock() - # ----------------------------------------------------------------------- # - # Client external interface - # ----------------------------------------------------------------------- # @property def connected(self) -> bool: """Return state of connection.""" - return self.is_active() + return self.ctx.is_active() - async def base_connect(self) -> bool: + async def connect(self) -> bool: """Call transport connect.""" - return await super().connect() - + self.ctx.reset_delay() + Log.debug( + "Connecting to {}:{}.", + self.ctx.comm_params.host, + self.ctx.comm_params.port, + ) + return await self.ctx.connect() def register(self, custom_response_class: ModbusResponse) -> None: """Register a custom response class with the decoder (call **sync**). @@ -123,14 +74,14 @@ def register(self, custom_response_class: ModbusResponse) -> None: Use register() to add non-standard responses (like e.g. a login prompt) and have them interpreted automatically. """ - self.framer.decoder.register(custom_response_class) + self.ctx.framer.decoder.register(custom_response_class) def close(self, reconnect: bool = False) -> None: """Close connection.""" if reconnect: - self.connection_lost(asyncio.TimeoutError("Server not responding")) + self.ctx.connection_lost(asyncio.TimeoutError("Server not responding")) else: - super().close() + self.ctx.close() def idle_time(self) -> float: """Time before initiating next transaction (call **sync**). @@ -142,38 +93,34 @@ def idle_time(self) -> float: return 0 return self.last_frame_end + self.silent_interval - def execute(self, request: ModbusRequest | None = None): + def execute(self, request: ModbusRequest): """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. """ - if not self.transport: + if not self.ctx.transport: raise ConnectionException(f"Not connected[{self!s}]") return self.async_execute(request) - # ----------------------------------------------------------------------- # - # Merged client methods - # ----------------------------------------------------------------------- # async def async_execute(self, request) -> ModbusResponse: """Execute requests asynchronously.""" - request.transaction_id = self.transaction.getNextTID() - packet = self.framer.buildPacket(request) + request.transaction_id = self.ctx.transaction.getNextTID() + packet = self.ctx.framer.buildPacket(request) count = 0 while count <= self.retries: async with self._lock: - req = self.build_response(request.transaction_id) - if not count or not self.no_resend_on_retry: - self.framer.resetFrame() - self.send(packet) - if self.broadcast_enable and not request.slave_id: + req = self.build_response(request) + self.ctx.framer.resetFrame() + self.ctx.send(packet) + if not request.slave_id: resp = None break try: resp = await asyncio.wait_for( - req, timeout=self.comm_params.timeout_connect + req, timeout=self.ctx.comm_params.timeout_connect ) break except asyncio.exceptions.TimeoutError: @@ -186,66 +133,17 @@ async def async_execute(self, request) -> ModbusResponse: return resp # type: ignore[return-value] - def callback_new_connection(self): - """Call when listener receive new connection request.""" - - def callback_connected(self) -> None: - """Call when connection is succcesfull.""" - if self.on_reconnect_callback: - self.on_reconnect_callback() - self.framer.resetFrame() - - def callback_disconnected(self, exc: Exception | None) -> None: - """Call when connection is lost.""" - Log.debug("callback_disconnected called: {}", exc) - - def callback_data(self, data: bytes, addr: tuple | None = None) -> int: - """Handle received data. - - returns number of bytes consumed - """ - self.framer.processIncomingPacket(data, self._handle_response, slave=0) - return len(data) - - async def connect(self) -> bool: # type: ignore[empty-body] - """Connect to the modbus remote host.""" - - def raise_future(self, my_future, exc): - """Set exception of a future if not done.""" - if not my_future.done(): - my_future.set_exception(exc) - - def _handle_response(self, reply, **_kwargs): - """Handle the processed response and link to correct deferred.""" - if reply is not None: - tid = reply.transaction_id - if handler := self.transaction.getTransaction(tid): - if not handler.done(): - handler.set_result(reply) - else: - Log.debug("Unrequested message: {}", reply, ":str") - - def build_response(self, tid): + def build_response(self, request: ModbusRequest): """Return a deferred response for the current request.""" my_future: asyncio.Future = asyncio.Future() - if not self.transport: - self.raise_future(my_future, ConnectionException("Client is not connected")) + request.fut = my_future + if not self.ctx.transport: + if not my_future.done(): + my_future.set_exception(ConnectionException("Client is not connected")) else: - self.transaction.addTransaction(my_future, tid) + self.ctx.transaction.addTransaction(request) return my_future - # ----------------------------------------------------------------------- # - # Internal methods - # ----------------------------------------------------------------------- # - def recv(self, size): - """Receive data. - - :meta private: - """ - - # ----------------------------------------------------------------------- # - # The magic methods - # ----------------------------------------------------------------------- # async def __aenter__(self): """Implement the client with enter block. @@ -265,95 +163,38 @@ def __str__(self): :returns: The string representation """ return ( - f"{self.__class__.__name__} {self.comm_params.host}:{self.comm_params.port}" + f"{self.__class__.__name__} {self.ctx.comm_params.host}:{self.ctx.comm_params.port}" ) class ModbusBaseSyncClient(ModbusClientMixin[ModbusResponse]): """**ModbusBaseClient**. - Fixed parameters: - - :param framer: Framer enum name - - Optional parameters: - - :param timeout: Timeout for a request, in seconds. - :param retries: Max number of retries per request. - :param retry_on_empty: Retry on empty response. - :param broadcast_enable: True to treat id 0 as broadcast address. - :param reconnect_delay: Minimum delay in seconds.milliseconds before reconnecting. - :param reconnect_delay_max: Maximum delay in seconds.milliseconds before reconnecting. - :param on_reconnect_callback: Function that will be called just before a reconnection attempt. - :param no_resend_on_retry: Do not resend request when retrying due to missing response. - :param kwargs: Experimental parameters. - - .. tip:: - **reconnect_delay** doubles automatically with each unsuccessful connect, from - **reconnect_delay** to **reconnect_delay_max**. - Set `reconnect_delay=0` to avoid automatic reconnection. - :mod:`ModbusBaseClient` is normally not referenced outside :mod:`pymodbus`. - - **Application methods, common to all clients**: """ - @dataclass - class _params: - """Parameter class.""" - - retries: int | None = None - retry_on_empty: bool | None = None - broadcast_enable: bool | None = None - reconnect_delay: int | None = None - source_address: tuple[str, int] | None = None - def __init__( self, - framer: Framer, - timeout: float = 3, - retries: int = 3, - retry_on_empty: bool = False, - broadcast_enable: bool = False, - reconnect_delay: float = 0.1, - reconnect_delay_max: float = 300.0, - no_resend_on_retry: bool = False, - **kwargs: Any, + framer: FramerType, + retries: int, + comm_params: CommParams | None = None, ) -> None: """Initialize a client instance.""" ModbusClientMixin.__init__(self) # type: ignore[arg-type] - self.comm_params = CommParams( - comm_type=kwargs.get("CommType"), - comm_name="comm", - source_address=kwargs.get("source_address", None), - reconnect_delay=reconnect_delay, - reconnect_delay_max=reconnect_delay_max, - timeout_connect=timeout, - host=kwargs.get("host", None), - port=kwargs.get("port", 0), - sslctx=kwargs.get("sslctx", None), - baudrate=kwargs.get("baudrate", None), - bytesize=kwargs.get("bytesize", None), - parity=kwargs.get("parity", None), - stopbits=kwargs.get("stopbits", None), - handle_local_echo=kwargs.get("handle_local_echo", False), - ) - self.params = self._params() - self.params.retries = int(retries) - self.params.retry_on_empty = bool(retry_on_empty) - self.params.broadcast_enable = bool(broadcast_enable) - self.retry_on_empty: int = 0 - self.no_resend_on_retry = no_resend_on_retry + if comm_params: + self.comm_params = comm_params + self.retries = retries self.slaves: list[int] = [] # Common variables. - self.framer = FRAMER_NAME_TO_CLASS.get( - framer, cast(Type[ModbusFramer], framer) + self.framer: ModbusFramer = FRAMER_NAME_TO_CLASS.get( + framer, cast(type[ModbusFramer], framer) )(ClientDecoder(), self) - self.transaction = ModbusTransactionManager( - self, retries=retries, retry_on_empty=retry_on_empty, **kwargs + self.transaction = SyncModbusTransactionManager( + self, + self.retries, ) - self.reconnect_delay_current = self.params.reconnect_delay or 0 + self.reconnect_delay_current = self.comm_params.reconnect_delay or 0 self.use_udp = False self.state = ModbusTransactionState.IDLE self.last_frame_end: float | None = 0 @@ -384,7 +225,7 @@ def idle_time(self) -> float: return 0 return self.last_frame_end + self.silent_interval - def execute(self, request: ModbusRequest | None = None) -> ModbusResponse: + def execute(self, request: ModbusRequest) -> ModbusResponse: """Execute request and get response (call **sync/async**). :param request: The request to process @@ -398,7 +239,7 @@ def execute(self, request: ModbusRequest | None = None) -> ModbusResponse: # ----------------------------------------------------------------------- # # Internal methods # ----------------------------------------------------------------------- # - def send(self, request): + def _start_send(self): """Send request. :meta private: @@ -406,14 +247,20 @@ def send(self, request): if self.state != ModbusTransactionState.RETRYING: Log.debug('New Transaction state "SENDING"') self.state = ModbusTransactionState.SENDING - return request - def recv(self, size): + @abstractmethod + def send(self, request: bytes) -> int: + """Send request. + + :meta private: + """ + + @abstractmethod + def recv(self, size: int | None) -> bytes: """Receive data. :meta private: """ - return size @classmethod def get_address_family(cls, address): diff --git a/pymodbus/client/mixin.py b/pymodbus/client/mixin.py index f686761aa..e7e5c137b 100644 --- a/pymodbus/client/mixin.py +++ b/pymodbus/client/mixin.py @@ -3,16 +3,16 @@ import struct from enum import Enum -from typing import Any, Generic, TypeVar - -import pymodbus.bit_read_message as pdu_bit_read -import pymodbus.bit_write_message as pdu_bit_write -import pymodbus.diag_message as pdu_diag -import pymodbus.file_message as pdu_file_msg -import pymodbus.mei_message as pdu_mei -import pymodbus.other_message as pdu_other_msg -import pymodbus.register_read_message as pdu_reg_read -import pymodbus.register_write_message as pdu_req_write +from typing import Generic, TypeVar + +import pymodbus.pdu.bit_read_message as pdu_bit_read +import pymodbus.pdu.bit_write_message as pdu_bit_write +import pymodbus.pdu.diag_message as pdu_diag +import pymodbus.pdu.file_message as pdu_file_msg +import pymodbus.pdu.mei_message as pdu_mei +import pymodbus.pdu.other_message as pdu_other_msg +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 @@ -59,383 +59,325 @@ def execute(self, _request: ModbusRequest) -> T: .. tip:: Response is not interpreted. """ - raise NotImplementedError( - "The execute method of ModbusClientMixin needs to be overridden and cannot be used directly" - ) + raise NotImplementedError("execute of ModbusClientMixin needs to be overridden") - def read_coils( - self, address: int, count: int = 1, slave: int = 0, **kwargs: Any - ) -> T: + def read_coils(self, address: int, count: int = 1, slave: int = 1) -> 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 kwargs: (optional) Experimental parameters. :raises ModbusException: """ - return self.execute( - pdu_bit_read.ReadCoilsRequest(address, count, slave, **kwargs) - ) + return self.execute(pdu_bit_read.ReadCoilsRequest(address, count, slave=slave)) - def read_discrete_inputs( - self, address: int, count: int = 1, slave: int = 0, **kwargs: Any - ) -> T: + def read_discrete_inputs(self, address: int, count: int = 1, slave: int = 1) -> 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 kwargs: (optional) Experimental parameters. :raises ModbusException: """ - return self.execute( - pdu_bit_read.ReadDiscreteInputsRequest(address, count, slave, **kwargs) - ) + return self.execute(pdu_bit_read.ReadDiscreteInputsRequest(address, count, slave=slave)) - def read_holding_registers( - self, address: int, count: int = 1, slave: int = 0, **kwargs: Any - ) -> T: + def read_holding_registers(self, address: int, count: int = 1, slave: int = 1) -> 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 kwargs: (optional) Experimental parameters. :raises ModbusException: """ - return self.execute( - pdu_reg_read.ReadHoldingRegistersRequest(address, count, slave, **kwargs) - ) + return self.execute(pdu_reg_read.ReadHoldingRegistersRequest(address, count, slave=slave)) - def read_input_registers( - self, address: int, count: int = 1, slave: int = 0, **kwargs: Any - ) -> T: + def read_input_registers(self, address: int, count: int = 1, slave: int = 1) -> 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 kwargs: (optional) Experimental parameters. :raises ModbusException: """ - return self.execute( - pdu_reg_read.ReadInputRegistersRequest(address, count, slave, **kwargs) - ) + return self.execute(pdu_reg_read.ReadInputRegistersRequest(address, count, slave=slave)) - def write_coil(self, address: int, value: bool, slave: int = 0, **kwargs: Any) -> T: + def write_coil(self, address: int, value: bool, slave: int = 1) -> T: """Write single coil (code 0x05). :param address: Address to write to :param value: Boolean to write :param slave: (optional) Modbus slave ID - :param kwargs: (optional) Experimental parameters. :raises ModbusException: """ - return self.execute( - pdu_bit_write.WriteSingleCoilRequest(address, value, slave, **kwargs) - ) + return self.execute(pdu_bit_write.WriteSingleCoilRequest(address, value, slave=slave)) - def write_register( - self, address: int, value: int, slave: int = 0, **kwargs: Any - ) -> T: + def write_register(self, address: int, value: int, slave: int = 1) -> T: """Write register (code 0x06). :param address: Address to write to :param value: Value to write :param slave: (optional) Modbus slave ID - :param kwargs: (optional) Experimental parameters. :raises ModbusException: """ - return self.execute( - pdu_req_write.WriteSingleRegisterRequest(address, value, slave, **kwargs) - ) + return self.execute(pdu_req_write.WriteSingleRegisterRequest(address, value, slave=slave)) - def read_exception_status(self, slave: int = 0, **kwargs: Any) -> T: + def read_exception_status(self, slave: int = 1) -> T: """Read Exception Status (code 0x07). :param slave: (optional) Modbus slave ID - :param kwargs: (optional) Experimental parameters. :raises ModbusException: """ - return self.execute(pdu_other_msg.ReadExceptionStatusRequest(slave, **kwargs)) + return self.execute(pdu_other_msg.ReadExceptionStatusRequest(slave=slave)) def diag_query_data( - self, msg: bytes, slave: int = 0, **kwargs: Any - ) -> T: + self, msg: bytes, slave: int = 1) -> T: """Diagnose query data (code 0x08 sub 0x00). :param msg: Message to be returned :param slave: (optional) Modbus slave ID - :param kwargs: (optional) Experimental parameters. :raises ModbusException: """ - return self.execute(pdu_diag.ReturnQueryDataRequest(msg, slave=slave, **kwargs)) + return self.execute(pdu_diag.ReturnQueryDataRequest(msg, slave=slave)) def diag_restart_communication( - self, toggle: bool, slave: int = 0, **kwargs: Any - ) -> T: + self, toggle: bool, slave: int = 1) -> T: """Diagnose restart communication (code 0x08 sub 0x01). :param toggle: True if toggled. :param slave: (optional) Modbus slave ID - :param kwargs: (optional) Experimental parameters. :raises ModbusException: """ return self.execute( - pdu_diag.RestartCommunicationsOptionRequest(toggle, slave=slave, **kwargs) + pdu_diag.RestartCommunicationsOptionRequest(toggle, slave=slave) ) - def diag_read_diagnostic_register(self, slave: int = 0, **kwargs: Any) -> T: + def diag_read_diagnostic_register(self, slave: int = 1) -> T: """Diagnose read diagnostic register (code 0x08 sub 0x02). :param slave: (optional) Modbus slave ID - :param kwargs: (optional) Experimental parameters. :raises ModbusException: """ return self.execute( - pdu_diag.ReturnDiagnosticRegisterRequest(slave=slave, **kwargs) + pdu_diag.ReturnDiagnosticRegisterRequest(slave=slave) ) - def diag_change_ascii_input_delimeter(self, slave: int = 0, **kwargs: Any) -> T: + def diag_change_ascii_input_delimeter(self, slave: int = 1) -> T: """Diagnose change ASCII input delimiter (code 0x08 sub 0x03). :param slave: (optional) Modbus slave ID - :param kwargs: (optional) Experimental parameters. :raises ModbusException: """ return self.execute( - pdu_diag.ChangeAsciiInputDelimiterRequest(slave=slave, **kwargs) + pdu_diag.ChangeAsciiInputDelimiterRequest(slave=slave) ) - def diag_force_listen_only(self, slave: int = 0, **kwargs: Any) -> T: + def diag_force_listen_only(self, slave: int = 1) -> T: """Diagnose force listen only (code 0x08 sub 0x04). :param slave: (optional) Modbus slave ID - :param kwargs: (optional) Experimental parameters. :raises ModbusException: """ - return self.execute(pdu_diag.ForceListenOnlyModeRequest(slave=slave, **kwargs)) + return self.execute(pdu_diag.ForceListenOnlyModeRequest(slave=slave)) - def diag_clear_counters(self, slave: int = 0, **kwargs: Any) -> T: + def diag_clear_counters(self, slave: int = 1) -> T: """Diagnose clear counters (code 0x08 sub 0x0A). :param slave: (optional) Modbus slave ID - :param kwargs: (optional) Experimental parameters. :raises ModbusException: """ - return self.execute(pdu_diag.ClearCountersRequest(slave=slave, **kwargs)) + return self.execute(pdu_diag.ClearCountersRequest(slave=slave)) - def diag_read_bus_message_count(self, slave: int = 0, **kwargs: Any) -> T: + def diag_read_bus_message_count(self, slave: int = 1) -> T: """Diagnose read bus message count (code 0x08 sub 0x0B). :param slave: (optional) Modbus slave ID - :param kwargs: (optional) Experimental parameters. :raises ModbusException: """ return self.execute( - pdu_diag.ReturnBusMessageCountRequest(slave=slave, **kwargs) + pdu_diag.ReturnBusMessageCountRequest(slave=slave) ) - def diag_read_bus_comm_error_count(self, slave: int = 0, **kwargs: Any) -> T: + def diag_read_bus_comm_error_count(self, slave: int = 1) -> T: """Diagnose read Bus Communication Error Count (code 0x08 sub 0x0C). :param slave: (optional) Modbus slave ID - :param kwargs: (optional) Experimental parameters. :raises ModbusException: """ return self.execute( - pdu_diag.ReturnBusCommunicationErrorCountRequest(slave=slave, **kwargs) + pdu_diag.ReturnBusCommunicationErrorCountRequest(slave=slave) ) - def diag_read_bus_exception_error_count(self, slave: int = 0, **kwargs: Any) -> T: + def diag_read_bus_exception_error_count(self, slave: int = 1) -> T: """Diagnose read Bus Exception Error Count (code 0x08 sub 0x0D). :param slave: (optional) Modbus slave ID - :param kwargs: (optional) Experimental parameters. :raises ModbusException: """ return self.execute( - pdu_diag.ReturnBusExceptionErrorCountRequest(slave=slave, **kwargs) + pdu_diag.ReturnBusExceptionErrorCountRequest(slave=slave) ) - def diag_read_slave_message_count(self, slave: int = 0, **kwargs: Any) -> T: + def diag_read_slave_message_count(self, slave: int = 1) -> T: """Diagnose read Slave Message Count (code 0x08 sub 0x0E). :param slave: (optional) Modbus slave ID - :param kwargs: (optional) Experimental parameters. :raises ModbusException: """ return self.execute( - pdu_diag.ReturnSlaveMessageCountRequest(slave=slave, **kwargs) + pdu_diag.ReturnSlaveMessageCountRequest(slave=slave) ) - def diag_read_slave_no_response_count(self, slave: int = 0, **kwargs: Any) -> T: + def diag_read_slave_no_response_count(self, slave: int = 1) -> T: """Diagnose read Slave No Response Count (code 0x08 sub 0x0F). :param slave: (optional) Modbus slave ID - :param kwargs: (optional) Experimental parameters. :raises ModbusException: """ return self.execute( - pdu_diag.ReturnSlaveNoResponseCountRequest(slave=slave, **kwargs) + pdu_diag.ReturnSlaveNoResponseCountRequest(slave=slave) ) - def diag_read_slave_nak_count(self, slave: int = 0, **kwargs: Any) -> T: + def diag_read_slave_nak_count(self, slave: int = 1) -> T: """Diagnose read Slave NAK Count (code 0x08 sub 0x10). :param slave: (optional) Modbus slave ID - :param kwargs: (optional) Experimental parameters. :raises ModbusException: """ - return self.execute(pdu_diag.ReturnSlaveNAKCountRequest(slave=slave, **kwargs)) + return self.execute(pdu_diag.ReturnSlaveNAKCountRequest(slave=slave)) - def diag_read_slave_busy_count(self, slave: int = 0, **kwargs: Any) -> T: + def diag_read_slave_busy_count(self, slave: int = 1) -> T: """Diagnose read Slave Busy Count (code 0x08 sub 0x11). :param slave: (optional) Modbus slave ID - :param kwargs: (optional) Experimental parameters. :raises ModbusException: """ - return self.execute(pdu_diag.ReturnSlaveBusyCountRequest(slave=slave, **kwargs)) + return self.execute(pdu_diag.ReturnSlaveBusyCountRequest(slave=slave)) - def diag_read_bus_char_overrun_count(self, slave: int = 0, **kwargs: Any) -> T: + def diag_read_bus_char_overrun_count(self, slave: int = 1) -> T: """Diagnose read Bus Character Overrun Count (code 0x08 sub 0x12). :param slave: (optional) Modbus slave ID - :param kwargs: (optional) Experimental parameters. :raises ModbusException: """ return self.execute( - pdu_diag.ReturnSlaveBusCharacterOverrunCountRequest(slave=slave, **kwargs) + pdu_diag.ReturnSlaveBusCharacterOverrunCountRequest(slave=slave) ) - def diag_read_iop_overrun_count(self, slave: int = 0, **kwargs: Any) -> T: + def diag_read_iop_overrun_count(self, slave: int = 1) -> T: """Diagnose read Iop overrun count (code 0x08 sub 0x13). :param slave: (optional) Modbus slave ID - :param kwargs: (optional) Experimental parameters. :raises ModbusException: """ return self.execute( - pdu_diag.ReturnIopOverrunCountRequest(slave=slave, **kwargs) + pdu_diag.ReturnIopOverrunCountRequest(slave=slave) ) - def diag_clear_overrun_counter(self, slave: int = 0, **kwargs: Any) -> T: + def diag_clear_overrun_counter(self, slave: int = 1) -> T: """Diagnose Clear Overrun Counter and Flag (code 0x08 sub 0x14). :param slave: (optional) Modbus slave ID - :param kwargs: (optional) Experimental parameters. :raises ModbusException: """ - return self.execute(pdu_diag.ClearOverrunCountRequest(slave=slave, **kwargs)) + return self.execute(pdu_diag.ClearOverrunCountRequest(slave=slave)) - def diag_getclear_modbus_response(self, slave: int = 0, **kwargs: Any) -> T: + def diag_getclear_modbus_response(self, slave: int = 1) -> T: """Diagnose Get/Clear modbus plus (code 0x08 sub 0x15). :param slave: (optional) Modbus slave ID - :param kwargs: (optional) Experimental parameters. :raises ModbusException: """ - return self.execute(pdu_diag.GetClearModbusPlusRequest(slave=slave, **kwargs)) + return self.execute(pdu_diag.GetClearModbusPlusRequest(slave=slave)) - def diag_get_comm_event_counter(self, **kwargs: Any) -> T: + def diag_get_comm_event_counter(self, slave: int = 1) -> T: """Diagnose get event counter (code 0x0B). - :param kwargs: (optional) Experimental parameters. :raises ModbusException: """ - return self.execute(pdu_other_msg.GetCommEventCounterRequest(**kwargs)) + return self.execute(pdu_other_msg.GetCommEventCounterRequest(slave=slave)) - def diag_get_comm_event_log(self, **kwargs: Any) -> T: + def diag_get_comm_event_log(self, slave: int = 1) -> T: """Diagnose get event counter (code 0x0C). - :param kwargs: (optional) Experimental parameters. :raises ModbusException: """ - return self.execute(pdu_other_msg.GetCommEventLogRequest(**kwargs)) + return self.execute(pdu_other_msg.GetCommEventLogRequest(slave=slave)) def write_coils( self, address: int, values: list[bool] | bool, - slave: int = 0, - **kwargs: Any, + slave: int = 1, ) -> 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 kwargs: (optional) Experimental parameters. :raises ModbusException: """ return self.execute( - pdu_bit_write.WriteMultipleCoilsRequest(address, values, slave, **kwargs) + pdu_bit_write.WriteMultipleCoilsRequest(address, values, slave) ) def write_registers( - self, address: int, values: list[int] | int, slave: int = 0, **kwargs: Any - ) -> T: + self, address: int, values: list[int] | int, slave: int = 1, skip_encode: bool = False) -> T: """Write registers (code 0x10). :param address: Start address to write to :param values: List of values to write, or a single value to write :param slave: (optional) Modbus slave ID - :param kwargs: (optional) Experimental parameters. + :param skip_encode: (optional) do not encode values :raises ModbusException: """ return self.execute( - pdu_req_write.WriteMultipleRegistersRequest( - address, values, slave, **kwargs - ) + pdu_req_write.WriteMultipleRegistersRequest(address, values, slave=slave, skip_encode=skip_encode) ) - def report_slave_id(self, slave: int = 0, **kwargs: Any) -> T: + def report_slave_id(self, slave: int = 1) -> T: """Report slave ID (code 0x11). :param slave: (optional) Modbus slave ID - :param kwargs: (optional) Experimental parameters. :raises ModbusException: """ - return self.execute(pdu_other_msg.ReportSlaveIdRequest(slave, **kwargs)) + return self.execute(pdu_other_msg.ReportSlaveIdRequest(slave=slave)) - def read_file_record(self, records: list[tuple], **kwargs: Any) -> T: + def read_file_record(self, records: list[tuple], slave: int = 1) -> T: """Read file record (code 0x14). :param records: List of (Reference type, File number, Record Number, Record Length) - :param kwargs: (optional) Experimental parameters. + :param slave: device id :raises ModbusException: """ - return self.execute(pdu_file_msg.ReadFileRecordRequest(records, **kwargs)) + return self.execute(pdu_file_msg.ReadFileRecordRequest(records, slave=slave)) - def write_file_record(self, records: list[tuple], **kwargs: Any) -> T: + def write_file_record(self, records: list[tuple], slave: int = 1) -> T: """Write file record (code 0x15). :param records: List of (Reference type, File number, Record Number, Record Length) - :param kwargs: (optional) Experimental parameters. + :param slave: (optional) Device id :raises ModbusException: """ - return self.execute(pdu_file_msg.WriteFileRecordRequest(records, **kwargs)) + return self.execute(pdu_file_msg.WriteFileRecordRequest(records, slave=slave)) def mask_write_register( self, address: int = 0x0000, and_mask: int = 0xFFFF, or_mask: int = 0x0000, - **kwargs: Any, + slave: int = 1, ) -> T: """Mask write register (code 0x16). :param address: The mask pointer address (0x0000 to 0xffff) :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 kwargs: (optional) Experimental parameters. + :param slave: (optional) device id :raises ModbusException: """ return self.execute( - pdu_req_write.MaskWriteRegisterRequest(address, and_mask, or_mask, **kwargs) + pdu_req_write.MaskWriteRegisterRequest(address, and_mask, or_mask, slave=slave) ) def readwrite_registers( @@ -443,20 +385,23 @@ def readwrite_registers( read_address: int = 0, read_count: int = 0, write_address: int = 0, + address: int | None = None, values: list[int] | int = 0, - slave: int = 0, - **kwargs, + slave: int = 1, ) -> T: """Read/Write registers (code 0x17). :param read_address: The address to start reading from :param read_count: The number of registers to read from address :param write_address: The address to start writing to + :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 kwargs: :raises ModbusException: """ + if address: + read_address = address + write_address = address return self.execute( pdu_reg_read.ReadWriteMultipleRegistersRequest( read_address=read_address, @@ -464,33 +409,31 @@ def readwrite_registers( write_address=write_address, write_registers=values, slave=slave, - **kwargs, ) ) - def read_fifo_queue(self, address: int = 0x0000, **kwargs: Any) -> T: + def read_fifo_queue(self, address: int = 0x0000, slave: int = 1) -> T: """Read FIFO queue (code 0x18). :param address: The address to start reading from - :param kwargs: + :param slave: (optional) device id :raises ModbusException: """ - return self.execute(pdu_file_msg.ReadFifoQueueRequest(address, **kwargs)) + return self.execute(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, **kwargs: Any - ) -> T: + self, read_code: int | None = None, object_id: int = 0x00, slave: int = 1) -> 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 kwargs: + :param slave: (optional) Device id :raises ModbusException: """ return self.execute( - pdu_mei.ReadDeviceInformationRequest(read_code, object_id, **kwargs) + pdu_mei.ReadDeviceInformationRequest(read_code, object_id, slave=slave) ) # ------------------ diff --git a/pymodbus/client/modbusclientprotocol.py b/pymodbus/client/modbusclientprotocol.py new file mode 100644 index 000000000..fffe822e2 --- /dev/null +++ b/pymodbus/client/modbusclientprotocol.py @@ -0,0 +1,81 @@ +"""ModbusProtocol implementation for all clients.""" +from __future__ import annotations + +from collections.abc import Callable +from typing import cast + +from pymodbus.factory import ClientDecoder +from pymodbus.framer import FRAMER_NAME_TO_CLASS, FramerType, ModbusFramer +from pymodbus.logging import Log +from pymodbus.transaction import ModbusTransactionManager +from pymodbus.transport import CommParams, ModbusProtocol + + +class ModbusClientProtocol(ModbusProtocol): + """**ModbusClientProtocol**. + + :mod:`ModbusClientProtocol` is normally not referenced outside :mod:`pymodbus`. + """ + + def __init__( + self, + framer: FramerType, + params: CommParams, + on_connect_callback: Callable[[bool], None] | None = None, + ) -> None: + """Initialize a client instance.""" + ModbusProtocol.__init__( + self, + params, + False, + ) + self.on_connect_callback = on_connect_callback + + # Common variables. + self.framer = FRAMER_NAME_TO_CLASS.get( + framer, cast(type[ModbusFramer], framer) + )(ClientDecoder(), self) + self.transaction = ModbusTransactionManager() + + def _handle_response(self, reply): + """Handle the processed response and link to correct deferred.""" + if reply is not None: + tid = reply.transaction_id + if handler := self.transaction.getTransaction(tid): + reply.request = handler + if not handler.fut.done(): + handler.fut.set_result(reply) + else: + Log.debug("Unrequested message: {}", reply, ":str") + + def callback_new_connection(self): + """Call when listener receive new connection request.""" + + def callback_connected(self) -> None: + """Call when connection is succcesfull.""" + if self.on_connect_callback: + self.loop.call_soon(self.on_connect_callback, True) + self.framer.resetFrame() + + def callback_disconnected(self, exc: Exception | None) -> None: + """Call when connection is lost.""" + Log.debug("callback_disconnected called: {}", exc) + if self.on_connect_callback: + self.loop.call_soon(self.on_connect_callback, False) + + def callback_data(self, data: bytes, addr: tuple | None = None) -> int: + """Handle received data. + + returns number of bytes consumed + """ + self.framer.processIncomingPacket(data, self._handle_response, 0) + return len(data) + + def __str__(self): + """Build a string representation of the connection. + + :returns: The string representation + """ + return ( + f"{self.__class__.__name__} {self.comm_params.host}:{self.comm_params.port}" + ) diff --git a/pymodbus/client/serial.py b/pymodbus/client/serial.py index 7726a3b33..833c7fd41 100644 --- a/pymodbus/client/serial.py +++ b/pymodbus/client/serial.py @@ -1,16 +1,16 @@ """Modbus client async serial communication.""" from __future__ import annotations -import asyncio import time +from collections.abc import Callable from functools import partial -from typing import TYPE_CHECKING, Any +from typing import TYPE_CHECKING from pymodbus.client.base import ModbusBaseClient, ModbusBaseSyncClient from pymodbus.exceptions import ConnectionException -from pymodbus.framer import Framer +from pymodbus.framer import FramerType from pymodbus.logging import Log -from pymodbus.transport import CommType +from pymodbus.transport import CommParams, CommType from pymodbus.utilities import ModbusTransactionState @@ -24,7 +24,7 @@ # type checkers do not understand the Raise RuntimeError in __init__() import serial -class AsyncModbusSerialClient(ModbusBaseClient, asyncio.Protocol): +class AsyncModbusSerialClient(ModbusBaseClient): """**AsyncModbusSerialClient**. Fixed parameters: @@ -33,24 +33,23 @@ class AsyncModbusSerialClient(ModbusBaseClient, asyncio.Protocol): Optional parameters: + :param framer: Framer name, default FramerType.RTU :param baudrate: Bits per second. :param bytesize: Number of bits per byte 7-8. :param parity: 'E'ven, 'O'dd or 'N'one :param stopbits: Number of stop bits 1, 1.5, 2. :param handle_local_echo: Discard local echo from dongle. - - Common optional parameters: - - :param framer: Framer enum name - :param timeout: Timeout for a request, in seconds. - :param retries: Max number of retries per request. - :param retry_on_empty: Retry on empty response. - :param broadcast_enable: True to treat id 0 as broadcast address. + :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 retries: Max number of retries per request. :param on_reconnect_callback: Function that will be called just before a reconnection attempt. - :param no_resend_on_retry: Do not resend request when retrying due to missing response. - :param kwargs: Experimental parameters. + + .. tip:: + **reconnect_delay** doubles automatically with each unsuccessful connect, from + **reconnect_delay** to **reconnect_delay_max**. + Set `reconnect_delay=0` to avoid automatic reconnection. Example:: @@ -66,15 +65,21 @@ async def run(): Please refer to :ref:`Pymodbus internals` for advanced usage. """ - def __init__( + def __init__( # pylint: disable=too-many-arguments self, port: str, - framer: Framer = Framer.RTU, + framer: FramerType = FramerType.RTU, baudrate: int = 19200, bytesize: int = 8, parity: str = "N", stopbits: int = 1, - **kwargs: Any, + handle_local_echo: bool = False, + name: str = "comm", + reconnect_delay: float = 0.1, + reconnect_delay_max: float = 300, + timeout: float = 3, + retries: int = 3, + on_connect_callback: Callable[[bool], None] | None = None, ) -> None: """Initialize Asyncio Modbus Serial Client.""" if PYSERIAL_MISSING: @@ -82,24 +87,25 @@ def __init__( "Serial client requires pyserial " 'Please install with "pip install pyserial" and try again.' ) - asyncio.Protocol.__init__(self) - ModbusBaseClient.__init__( - self, - framer, - CommType=CommType.SERIAL, + self.comm_params = CommParams( + comm_type=CommType.SERIAL, host=port, baudrate=baudrate, bytesize=bytesize, parity=parity, stopbits=stopbits, - **kwargs, + handle_local_echo=handle_local_echo, + comm_name=name, + reconnect_delay=reconnect_delay, + reconnect_delay_max=reconnect_delay_max, + timeout_connect=timeout, + ) + ModbusBaseClient.__init__( + self, + framer, + retries, + on_connect_callback, ) - - async def connect(self) -> bool: - """Connect Async client.""" - self.reset_delay() - Log.debug("Connecting to {}.", self.comm_params.host) - return await self.base_connect() def close(self, reconnect: bool = False) -> None: """Close connection.""" @@ -115,25 +121,22 @@ class ModbusSerialClient(ModbusBaseSyncClient): Optional parameters: + :param framer: Framer name, default FramerType.RTU :param baudrate: Bits per second. :param bytesize: Number of bits per byte 7-8. :param parity: 'E'ven, 'O'dd or 'N'one :param stopbits: Number of stop bits 0-2. :param handle_local_echo: Discard local echo from dongle. - - Common optional parameters: - - :param framer: Framer enum name - :param timeout: Timeout for a request, in seconds. - :param retries: Max number of retries per request. - :param retry_on_empty: Retry on empty response. - :param strict: Strict timing, 1.5 character between requests. - :param broadcast_enable: True to treat id 0 as broadcast address. + :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 on_reconnect_callback: Function that will be called just before a reconnection attempt. - :param no_resend_on_retry: Do not resend request when retrying due to missing response. - :param kwargs: Experimental parameters. + :param timeout: Timeout for a connection request, in seconds. + :param retries: Max number of retries per request. + + .. tip:: + **reconnect_delay** doubles automatically with each unsuccessful connect, from + **reconnect_delay** to **reconnect_delay_max**. + Set `reconnect_delay=0` to avoid automatic reconnection. Example:: @@ -155,33 +158,41 @@ def run(): inter_byte_timeout: float = 0 silent_interval: float = 0 - def __init__( + def __init__( # pylint: disable=too-many-arguments self, port: str, - framer: Framer = Framer.RTU, + framer: FramerType = FramerType.RTU, baudrate: int = 19200, bytesize: int = 8, parity: str = "N", stopbits: int = 1, - strict: bool = True, - **kwargs: Any, + handle_local_echo: bool = False, + name: str = "comm", + reconnect_delay: float = 0.1, + reconnect_delay_max: float = 300, + timeout: float = 3, + retries: int = 3, ) -> None: """Initialize Modbus Serial Client.""" - super().__init__( - framer, - CommType=CommType.SERIAL, + self.comm_params = CommParams( + comm_type=CommType.SERIAL, host=port, baudrate=baudrate, bytesize=bytesize, parity=parity, stopbits=stopbits, - **kwargs, + handle_local_echo=handle_local_echo, + comm_name=name, + reconnect_delay=reconnect_delay, + reconnect_delay_max=reconnect_delay_max, + timeout_connect=timeout, + ) + super().__init__( + framer, + retries, ) self.socket: serial.Serial | None = None - self.strict = bool(strict) - self.last_frame_end = None - self._t0 = float(1 + bytesize + stopbits) / baudrate # Check every 4 bytes / 2 registers if the reading is ready @@ -201,7 +212,7 @@ def connected(self): """Connect internal.""" return self.connect() - def connect(self): + def connect(self) -> bool: """Connect to the modbus serial server.""" if self.socket: return True @@ -215,8 +226,7 @@ def connect(self): parity=self.comm_params.parity, exclusive=True, ) - if self.strict: - self.socket.inter_byte_timeout = self.inter_byte_timeout + self.socket.inter_byte_timeout = self.inter_byte_timeout self.last_frame_end = None # except serial.SerialException as msg: # pyserial raises undocumented exceptions like termios @@ -235,25 +245,26 @@ def _in_waiting(self): """Return waiting bytes.""" return getattr(self.socket, "in_waiting") if hasattr(self.socket, "in_waiting") else getattr(self.socket, "inWaiting")() - def send(self, request): + def send(self, request: bytes) -> int: """Send data on the underlying socket. If receive buffer still holds some data then flush it. Sleep if last send finished less than 3.5 character times ago. """ - super().send(request) + super()._start_send() if not self.socket: raise ConnectionException(str(self)) if request: if waitingbytes := self._in_waiting(): result = self.socket.read(waitingbytes) Log.warning("Cleanup recv buffer before send: {}", result, ":hex") - size = self.socket.write(request) + if (size := self.socket.write(request)) is None: + size = 0 return size return 0 - def _wait_for_data(self): + def _wait_for_data(self) -> int: """Wait for data.""" size = 0 more_data = False @@ -272,9 +283,8 @@ def _wait_for_data(self): time.sleep(self._recv_interval) return size - def recv(self, size): + def recv(self, size: int | None) -> bytes: """Read data from the underlying descriptor.""" - super().recv(size) if not self.socket: raise ConnectionException(str(self)) if size is None: @@ -284,7 +294,7 @@ def recv(self, size): result = self.socket.read(size) return result - def is_socket_open(self): + def is_socket_open(self) -> bool: """Check if socket is open.""" if self.socket: return self.socket.is_open diff --git a/pymodbus/client/tcp.py b/pymodbus/client/tcp.py index cc6ce6c84..ca40ae92a 100644 --- a/pymodbus/client/tcp.py +++ b/pymodbus/client/tcp.py @@ -1,20 +1,19 @@ """Modbus client async TCP communication.""" from __future__ import annotations -import asyncio import select import socket import time -from typing import Any +from collections.abc import Callable from pymodbus.client.base import ModbusBaseClient, ModbusBaseSyncClient from pymodbus.exceptions import ConnectionException -from pymodbus.framer import Framer +from pymodbus.framer import FramerType from pymodbus.logging import Log -from pymodbus.transport import CommType +from pymodbus.transport import CommParams, CommType -class AsyncModbusTcpClient(ModbusBaseClient, asyncio.Protocol): +class AsyncModbusTcpClient(ModbusBaseClient): """**AsyncModbusTcpClient**. Fixed parameters: @@ -23,21 +22,20 @@ class AsyncModbusTcpClient(ModbusBaseClient, asyncio.Protocol): Optional parameters: + :param framer: Framer name, default FramerType.SOCKET :param port: Port used for communication + :param name: Set communication name, used in logging :param source_address: source address of client - - Common optional parameters: - - :param framer: Framer enum name - :param timeout: Timeout for a request, in seconds. - :param retries: Max number of retries per request. - :param retry_on_empty: Retry on empty response. - :param broadcast_enable: True to treat id 0 as broadcast address. :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 retries: Max number of retries per request. :param on_reconnect_callback: Function that will be called just before a reconnection attempt. - :param no_resend_on_retry: Do not resend request when retrying due to missing response. - :param kwargs: Experimental parameters. + + .. tip:: + **reconnect_delay** doubles automatically with each unsuccessful connect, from + **reconnect_delay** to **reconnect_delay_max**. + Set `reconnect_delay=0` to avoid automatic reconnection. Example:: @@ -55,37 +53,37 @@ async def run(): socket: socket.socket | None - def __init__( + def __init__( # pylint: disable=too-many-arguments self, host: str, + framer: FramerType = FramerType.SOCKET, port: int = 502, - framer: Framer = Framer.SOCKET, + name: str = "comm", source_address: tuple[str, int] | None = None, - **kwargs: Any, + reconnect_delay: float = 0.1, + reconnect_delay_max: float = 300, + timeout: float = 3, + retries: int = 3, + on_connect_callback: Callable[[bool], None] | None = None, ) -> None: """Initialize Asyncio Modbus TCP Client.""" - asyncio.Protocol.__init__(self) - if "CommType" not in kwargs: - kwargs["CommType"] = CommType.TCP - if source_address: - kwargs["source_address"] = source_address + if not hasattr(self,"comm_params"): + self.comm_params = CommParams( + comm_type=CommType.TCP, + host=host, + port=port, + comm_name=name, + source_address=source_address, + reconnect_delay=reconnect_delay, + reconnect_delay_max=reconnect_delay_max, + timeout_connect=timeout, + ) ModbusBaseClient.__init__( self, framer, - host=host, - port=port, - **kwargs, - ) - - async def connect(self) -> bool: - """Initiate connection to start client.""" - self.reset_delay() - Log.debug( - "Connecting to {}:{}.", - self.comm_params.host, - self.comm_params.port, + retries, + on_connect_callback, ) - return await self.base_connect() def close(self, reconnect: bool = False) -> None: """Close connection.""" @@ -101,21 +99,19 @@ class ModbusTcpClient(ModbusBaseSyncClient): Optional parameters: + :param framer: Framer name, default FramerType.SOCKET :param port: Port used for communication + :param name: Set communication name, used in logging :param source_address: source address of client - - Common optional parameters: - - :param framer: Framer enum name - :param timeout: Timeout for a request, in seconds. - :param retries: Max number of retries per request. - :param retry_on_empty: Retry on empty response. - :param broadcast_enable: True to treat id 0 as broadcast address. :param reconnect_delay: Minimum delay in seconds.milliseconds before reconnecting. :param reconnect_delay_max: Maximum delay in seconds.milliseconds before reconnecting. - :param on_reconnect_callback: Function that will be called just before a reconnection attempt. - :param no_resend_on_retry: Do not resend request when retrying due to missing response. - :param kwargs: Experimental parameters. + :param timeout: Timeout for a connection request, in seconds. + :param retries: Max number of retries per request. + + .. tip:: + **reconnect_delay** doubles automatically with each unsuccessful connect, from + **reconnect_delay** to **reconnect_delay_max**. + Set `reconnect_delay=0` to avoid automatic reconnection. Example:: @@ -138,21 +134,28 @@ async def run(): def __init__( self, host: str, + framer: FramerType = FramerType.SOCKET, port: int = 502, - framer: Framer = Framer.SOCKET, + name: str = "comm", source_address: tuple[str, int] | None = None, - **kwargs: Any, + reconnect_delay: float = 0.1, + reconnect_delay_max: float = 300, + timeout: float = 3, + retries: int = 3, ) -> None: """Initialize Modbus TCP Client.""" - if "CommType" not in kwargs: - kwargs["CommType"] = CommType.TCP - super().__init__( - framer, - host=host, - port=port, - **kwargs, - ) - self.params.source_address = source_address + if not hasattr(self,"comm_params"): + self.comm_params = CommParams( + comm_type=CommType.TCP, + host=host, + port=port, + comm_name=name, + source_address=source_address, + reconnect_delay=reconnect_delay, + reconnect_delay_max=reconnect_delay_max, + timeout_connect=timeout, + ) + super().__init__(framer, retries) self.socket = None @property @@ -168,7 +171,7 @@ def connect(self): self.socket = socket.create_connection( (self.comm_params.host, self.comm_params.port), timeout=self.comm_params.timeout_connect, - source_address=self.params.source_address, + source_address=self.comm_params.source_address, ) Log.debug( "Connection to Modbus server established. Socket {}", @@ -192,16 +195,15 @@ def close(self): def send(self, request): """Send data on the underlying socket.""" - super().send(request) + super()._start_send() if not self.socket: raise ConnectionException(str(self)) if request: return self.socket.send(request) return 0 - def recv(self, size): + def recv(self, size: int | None) -> bytes: """Read data from the underlying descriptor.""" - super().recv(size) if not self.socket: raise ConnectionException(str(self)) @@ -253,7 +255,7 @@ def recv(self, size): return b"".join(data) - def _handle_abrupt_socket_close(self, size, data, duration): + def _handle_abrupt_socket_close(self, size: int | None, data: list[bytes], duration: float) -> bytes: """Handle unexpected socket close by remote end. Intended to be invoked after determining that the remote end @@ -283,7 +285,7 @@ def _handle_abrupt_socket_close(self, size, data, duration): msg += " without response from slave before it closed connection" raise ConnectionException(msg) - def is_socket_open(self): + def is_socket_open(self) -> bool: """Check if socket is open.""" return self.socket is not None diff --git a/pymodbus/client/tls.py b/pymodbus/client/tls.py index d5b56f980..7c77d93e8 100644 --- a/pymodbus/client/tls.py +++ b/pymodbus/client/tls.py @@ -3,10 +3,10 @@ import socket import ssl -from typing import Any +from collections.abc import Callable from pymodbus.client.tcp import AsyncModbusTcpClient, ModbusTcpClient -from pymodbus.framer import Framer +from pymodbus.framer import FramerType from pymodbus.logging import Log from pymodbus.transport import CommParams, CommType @@ -20,26 +20,21 @@ class AsyncModbusTlsClient(AsyncModbusTcpClient): Optional parameters: + :param sslctx: SSLContext to use for TLS + :param framer: Framer name, default FramerType.TLS :param port: Port used for communication + :param name: Set communication name, used in logging :param source_address: Source address of client - :param sslctx: SSLContext to use for TLS - :param certfile: Cert file path for TLS server request - :param keyfile: Key file path for TLS server request - :param password: Password for for decrypting private key file - :param server_hostname: Bind certificate to host - - Common optional parameters: - - :param framer: Framer enum name - :param timeout: Timeout for a request, in seconds. - :param retries: Max number of retries per request. - :param retry_on_empty: Retry on empty response. - :param broadcast_enable: True to treat id 0 as broadcast address. :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 retries: Max number of retries per request. :param on_reconnect_callback: Function that will be called just before a reconnection attempt. - :param no_resend_on_retry: Do not resend request when retrying due to missing response. - :param kwargs: Experimental parameters. + + .. tip:: + **reconnect_delay** doubles automatically with each unsuccessful connect, from + **reconnect_delay** to **reconnect_delay_max**. + Set `reconnect_delay=0` to avoid automatic reconnection. Example:: @@ -55,41 +50,39 @@ async def run(): Please refer to :ref:`Pymodbus internals` for advanced usage. """ - def __init__( + def __init__( # pylint: disable=too-many-arguments self, host: str, + sslctx: ssl.SSLContext = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT), + framer: FramerType = FramerType.TLS, port: int = 802, - framer: Framer = Framer.TLS, - sslctx: ssl.SSLContext | None = None, - certfile: str | None = None, - keyfile: str | None = None, - password: str | None = None, - server_hostname: str | None = None, - **kwargs: Any, + name: str = "comm", + source_address: tuple[str, int] | None = None, + reconnect_delay: float = 0.1, + reconnect_delay_max: float = 300, + timeout: float = 3, + retries: int = 3, + on_connect_callback: Callable[[bool], None] | None = None, ): """Initialize Asyncio Modbus TLS Client.""" + self.comm_params = CommParams( + comm_type=CommType.TLS, + host=host, + sslctx=sslctx, + port=port, + comm_name=name, + source_address=source_address, + reconnect_delay=reconnect_delay, + reconnect_delay_max=reconnect_delay_max, + timeout_connect=timeout, + ) AsyncModbusTcpClient.__init__( self, - host, - port=port, + "", framer=framer, - CommType=CommType.TLS, - sslctx=CommParams.generate_ssl( - False, certfile, keyfile, password, sslctx=sslctx - ), - **kwargs, - ) - self.server_hostname = server_hostname - - async def connect(self) -> bool: - """Initiate connection to start client.""" - self.reset_delay() - Log.debug( - "Connecting to {}:{}.", - self.comm_params.host, - self.comm_params.port, + retries=retries, + on_connect_callback=on_connect_callback, ) - return await self.base_connect() @classmethod def generate_ssl( @@ -112,7 +105,6 @@ def generate_ssl( False, certfile=certfile, keyfile=keyfile, password=password ) - class ModbusTlsClient(ModbusTcpClient): """**ModbusTlsClient**. @@ -122,27 +114,20 @@ class ModbusTlsClient(ModbusTcpClient): Optional parameters: + :param sslctx: SSLContext to use for TLS + :param framer: Framer name, default FramerType.TLS :param port: Port used for communication + :param name: Set communication name, used in logging :param source_address: Source address of client - :param sslctx: SSLContext to use for TLS - :param certfile: Cert file path for TLS server request - :param keyfile: Key file path for TLS server request - :param password: Password for decrypting private key file - :param server_hostname: Bind certificate to host - :param kwargs: Experimental parameters - - Common optional parameters: - - :param framer: Framer enum name - :param timeout: Timeout for a request, in seconds. - :param retries: Max number of retries per request. - :param retry_on_empty: Retry on empty response. - :param broadcast_enable: True to treat id 0 as broadcast address. :param reconnect_delay: Minimum delay in seconds.milliseconds before reconnecting. :param reconnect_delay_max: Maximum delay in seconds.milliseconds before reconnecting. - :param on_reconnect_callback: Function that will be called just before a reconnection attempt. - :param no_resend_on_retry: Do not resend request when retrying due to missing response. - :param kwargs: Experimental parameters. + :param timeout: Timeout for a connection request, in seconds. + :param retries: Max number of retries per request. + + .. tip:: + **reconnect_delay** doubles automatically with each unsuccessful connect, from + **reconnect_delay** to **reconnect_delay_max**. + Set `reconnect_delay=0` to avoid automatic reconnection. Example:: @@ -160,27 +145,36 @@ async def run(): Remark: There are no automatic reconnect as with AsyncModbusTlsClient """ - def __init__( + def __init__( # pylint: disable=too-many-arguments self, host: str, + sslctx: ssl.SSLContext = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT), + framer: FramerType = FramerType.TLS, port: int = 802, - framer: Framer = Framer.TLS, - sslctx: ssl.SSLContext | None = None, - certfile: str | None = None, - keyfile: str | None = None, - password: str | None = None, - server_hostname: str | None = None, - **kwargs: Any, + name: str = "comm", + source_address: tuple[str, int] | None = None, + reconnect_delay: float = 0.1, + reconnect_delay_max: float = 300, + timeout: float = 3, + retries: int = 3, ): """Initialize Modbus TLS Client.""" - super().__init__( - host, CommType=CommType.TLS, port=port, framer=framer, **kwargs + self.comm_params = CommParams( + comm_type=CommType.TLS, + host=host, + sslctx=sslctx, + port=port, + comm_name=name, + source_address=source_address, + reconnect_delay=reconnect_delay, + reconnect_delay_max=reconnect_delay_max, + timeout_connect=timeout, ) - self.sslctx = CommParams.generate_ssl( - False, certfile, keyfile, password, sslctx=sslctx + super().__init__( + "", + framer=framer, + retries=retries, ) - self.server_hostname = server_hostname - @classmethod def generate_ssl( @@ -214,11 +208,9 @@ def connect(self): return True try: sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - if self.params.source_address: - sock.bind(self.params.source_address) - self.socket = self.sslctx.wrap_socket( - sock, server_side=False, server_hostname=self.server_hostname - ) + if self.comm_params.source_address: + sock.bind(self.comm_params.source_address) + self.socket = self.comm_params.sslctx.wrap_socket(sock, server_side=False) # type: ignore[union-attr] self.socket.settimeout(self.comm_params.timeout_connect) self.socket.connect((self.comm_params.host, self.comm_params.port)) except OSError as msg: @@ -235,6 +227,6 @@ def __repr__(self): """Return string representation.""" return ( f"<{self.__class__.__name__} at {hex(id(self))} socket={self.socket}, " - f"ipaddr={self.comm_params.host}, port={self.comm_params.port}, sslctx={self.sslctx}, " + f"ipaddr={self.comm_params.host}, port={self.comm_params.port}, sslctx={self.comm_params.sslctx}, " f"timeout={self.comm_params.timeout_connect}>" ) diff --git a/pymodbus/client/udp.py b/pymodbus/client/udp.py index 03115143d..48b71edab 100644 --- a/pymodbus/client/udp.py +++ b/pymodbus/client/udp.py @@ -1,23 +1,20 @@ """Modbus client async UDP communication.""" from __future__ import annotations -import asyncio import socket -from typing import Any +from collections.abc import Callable from pymodbus.client.base import ModbusBaseClient, ModbusBaseSyncClient from pymodbus.exceptions import ConnectionException -from pymodbus.framer import Framer +from pymodbus.framer import FramerType from pymodbus.logging import Log -from pymodbus.transport import CommType +from pymodbus.transport import CommParams, CommType DGRAM_TYPE = socket.SOCK_DGRAM -class AsyncModbusUdpClient( - ModbusBaseClient, asyncio.Protocol, asyncio.DatagramProtocol -): +class AsyncModbusUdpClient(ModbusBaseClient): """**AsyncModbusUdpClient**. Fixed parameters: @@ -26,21 +23,20 @@ class AsyncModbusUdpClient( Optional parameters: + :param framer: Framer name, default FramerType.SOCKET :param port: Port used for communication. + :param name: Set communication name, used in logging :param source_address: source address of client, - - Common optional parameters: - - :param framer: Framer enum name - :param timeout: Timeout for a request, in seconds. - :param retries: Max number of retries per request. - :param retry_on_empty: Retry on empty response. - :param broadcast_enable: True to treat id 0 as broadcast address. :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 retries: Max number of retries per request. :param on_reconnect_callback: Function that will be called just before a reconnection attempt. - :param no_resend_on_retry: Do not resend request when retrying due to missing response. - :param kwargs: Experimental parameters. + + .. tip:: + **reconnect_delay** doubles automatically with each unsuccessful connect, from + **reconnect_delay** to **reconnect_delay_max**. + Set `reconnect_delay=0` to avoid automatic reconnection. Example:: @@ -56,44 +52,42 @@ async def run(): Please refer to :ref:`Pymodbus internals` for advanced usage. """ - def __init__( + def __init__( # pylint: disable=too-many-arguments self, host: str, + framer: FramerType = FramerType.SOCKET, port: int = 502, - framer: Framer = Framer.SOCKET, + name: str = "comm", source_address: tuple[str, int] | None = None, - **kwargs: Any, + reconnect_delay: float = 0.1, + reconnect_delay_max: float = 300, + timeout: float = 3, + retries: int = 3, + on_connect_callback: Callable[[bool], None] | None = None, ) -> None: """Initialize Asyncio Modbus UDP Client.""" - asyncio.DatagramProtocol.__init__(self) - asyncio.Protocol.__init__(self) + self.comm_params = CommParams( + comm_type=CommType.UDP, + host=host, + port=port, + comm_name=name, + source_address=source_address, + reconnect_delay=reconnect_delay, + reconnect_delay_max=reconnect_delay_max, + timeout_connect=timeout, + ) ModbusBaseClient.__init__( self, framer, - CommType=CommType.UDP, - host=host, - port=port, - **kwargs, + retries, + on_connect_callback, ) self.source_address = source_address @property def connected(self): """Return true if connected.""" - return self.is_active() - - async def connect(self) -> bool: - """Start reconnecting asynchronous udp client. - - :meta private: - """ - self.reset_delay() - Log.debug( - "Connecting to {}:{}.", - self.comm_params.host, - self.comm_params.port, - ) - return await self.base_connect() + return self.ctx.is_active() class ModbusUdpClient(ModbusBaseSyncClient): @@ -105,21 +99,19 @@ class ModbusUdpClient(ModbusBaseSyncClient): Optional parameters: + :param framer: Framer name, default FramerType.SOCKET :param port: Port used for communication. + :param name: Set communication name, used in logging :param source_address: source address of client, - - Common optional parameters: - - :param framer: Framer enum name - :param timeout: Timeout for a request, in seconds. - :param retries: Max number of retries per request. - :param retry_on_empty: Retry on empty response. - :param broadcast_enable: True to treat id 0 as broadcast address. :param reconnect_delay: Minimum delay in seconds.milliseconds before reconnecting. :param reconnect_delay_max: Maximum delay in seconds.milliseconds before reconnecting. - :param on_reconnect_callback: Function that will be called just before a reconnection attempt. - :param no_resend_on_retry: Do not resend request when retrying due to missing response. - :param kwargs: Experimental parameters. + :param timeout: Timeout for a connection request, in seconds. + :param retries: Max number of retries per request. + + .. tip:: + **reconnect_delay** doubles automatically with each unsuccessful connect, from + **reconnect_delay** to **reconnect_delay_max**. + Set `reconnect_delay=0` to avoid automatic reconnection. Example:: @@ -142,21 +134,27 @@ async def run(): def __init__( self, host: str, + framer: FramerType = FramerType.SOCKET, port: int = 502, - framer: Framer = Framer.SOCKET, + name: str = "comm", source_address: tuple[str, int] | None = None, - **kwargs: Any, + reconnect_delay: float = 0.1, + reconnect_delay_max: float = 300, + timeout: float = 3, + retries: int = 3, ) -> None: """Initialize Modbus UDP Client.""" - super().__init__( - framer, - port=port, + self.comm_params = CommParams( + comm_type=CommType.UDP, host=host, - CommType=CommType.UDP, - **kwargs, + port=port, + comm_name=name, + source_address=source_address, + reconnect_delay=reconnect_delay, + reconnect_delay_max=reconnect_delay_max, + timeout_connect=timeout, ) - self.params.source_address = source_address - + super().__init__(framer, retries) self.socket = None @property @@ -187,12 +185,12 @@ def close(self): """ self.socket = None - def send(self, request): + def send(self, request: bytes) -> int: """Send data on the underlying socket. :meta private: """ - super().send(request) + super()._start_send() if not self.socket: raise ConnectionException(str(self)) if request: @@ -201,14 +199,15 @@ def send(self, request): ) return 0 - def recv(self, size): + def recv(self, size: int | None) -> bytes: """Read data from the underlying descriptor. :meta private: """ - super().recv(size) if not self.socket: raise ConnectionException(str(self)) + if size is None: + size = 0 return self.socket.recvfrom(size)[0] def is_socket_open(self): diff --git a/pymodbus/datastore/context.py b/pymodbus/datastore/context.py index d2296a270..ea76b7ffa 100644 --- a/pymodbus/datastore/context.py +++ b/pymodbus/datastore/context.py @@ -92,14 +92,19 @@ class ModbusSlaveContext(ModbusBaseSlaveContext): """ - def __init__(self, *_args, **kwargs): + def __init__(self, *_args, + di=ModbusSequentialDataBlock.create(), + co=ModbusSequentialDataBlock.create(), + ir=ModbusSequentialDataBlock.create(), + hr=ModbusSequentialDataBlock.create(), + zero_mode=False): """Initialize the datastores.""" self.store = {} - self.store["d"] = kwargs.get("di", ModbusSequentialDataBlock.create()) - self.store["c"] = kwargs.get("co", ModbusSequentialDataBlock.create()) - self.store["i"] = kwargs.get("ir", ModbusSequentialDataBlock.create()) - self.store["h"] = kwargs.get("hr", ModbusSequentialDataBlock.create()) - self.zero_mode = kwargs.get("zero_mode", False) + self.store["d"] = di + self.store["c"] = co + self.store["i"] = ir + self.store["h"] = hr + self.zero_mode = zero_mode def __str__(self): """Return a string representation of the context. diff --git a/pymodbus/datastore/remote.py b/pymodbus/datastore/remote.py index 919da6c50..85452be51 100644 --- a/pymodbus/datastore/remote.py +++ b/pymodbus/datastore/remote.py @@ -69,47 +69,47 @@ def __str__(self): def __build_mapping(self): """Build the function code mapper.""" - kwargs = {} + params = {} if self.slave: - kwargs["slave"] = self.slave + params["slave"] = self.slave self.__get_callbacks = { "d": lambda a, c: self._client.read_discrete_inputs( - a, c, **kwargs + a, c, **params ), "c": lambda a, c: self._client.read_coils( - a, c, **kwargs + a, c, **params ), "h": lambda a, c: self._client.read_holding_registers( - a, c, **kwargs + a, c, **params ), "i": lambda a, c: self._client.read_input_registers( - a, c, **kwargs + a, c, **params ), } self.__set_callbacks = { "d5": lambda a, v: self._client.write_coil( - a, v, **kwargs + a, v, **params ), "d15": lambda a, v: self._client.write_coils( - a, v, **kwargs + a, v, **params ), "c5": lambda a, v: self._client.write_coil( - a, v, **kwargs + a, v, **params ), "c15": lambda a, v: self._client.write_coils( - a, v, **kwargs + a, v, **params ), "h6": lambda a, v: self._client.write_register( - a, v, **kwargs + a, v, **params ), "h16": lambda a, v: self._client.write_registers( - a, v, **kwargs + a, v, **params ), "i6": lambda a, v: self._client.write_register( - a, v, **kwargs + a, v, **params ), "i16": lambda a, v: self._client.write_registers( - a, v, **kwargs + a, v, **params ), } self._write_fc = (0x05, 0x06, 0x0F, 0x10) diff --git a/pymodbus/datastore/simulator.py b/pymodbus/datastore/simulator.py index 81fabeadf..69ecb2a26 100644 --- a/pymodbus/datastore/simulator.py +++ b/pymodbus/datastore/simulator.py @@ -4,8 +4,9 @@ import dataclasses import random import struct +from collections.abc import Callable from datetime import datetime -from typing import Any, Callable +from typing import Any from pymodbus.datastore.context import ModbusBaseSlaveContext @@ -34,7 +35,7 @@ class Cell: access: bool = False value: int = 0 action: int = 0 - action_kwargs: dict[str, Any] | None = None + action_parameters: dict[str, Any] | None = None count_read: int = 0 count_write: int = 0 @@ -46,7 +47,7 @@ class TextCell: # pylint: disable=too-few-public-methods access: str value: str action: str - action_kwargs: str + action_parameters: str count_read: str count_write: str @@ -68,7 +69,7 @@ class Label: # pylint: disable=too-many-instance-attributes increment: str = "increment" invalid: str = "invalid" ir_size: str = "ir size" - kwargs: str = "kwargs" + parameters: str = "parameters" method: str = "method" next: str = "next" none: str = "none" @@ -147,7 +148,7 @@ def __init__(self, runtime): }, } - def handle_type_bits(self, start, stop, value, action, action_kwargs): + def handle_type_bits(self, start, stop, value, action, action_parameters): """Handle type bits.""" for reg in self.runtime.registers[start:stop]: if reg.type != CellType.INVALID: @@ -155,9 +156,9 @@ def handle_type_bits(self, start, stop, value, action, action_kwargs): reg.value = value reg.type = CellType.BITS reg.action = action - reg.action_kwargs = action_kwargs + reg.action_parameters = action_parameters - def handle_type_uint16(self, start, stop, value, action, action_kwargs): + def handle_type_uint16(self, start, stop, value, action, action_parameters): """Handle type uint16.""" for reg in self.runtime.registers[start:stop]: if reg.type != CellType.INVALID: @@ -165,9 +166,9 @@ def handle_type_uint16(self, start, stop, value, action, action_kwargs): reg.value = value reg.type = CellType.UINT16 reg.action = action - reg.action_kwargs = action_kwargs + reg.action_parameters = action_parameters - def handle_type_uint32(self, start, stop, value, action, action_kwargs): + def handle_type_uint32(self, start, stop, value, action, action_parameters): """Handle type uint32.""" regs_value = ModbusSimulatorContext.build_registers_from_value(value, True) for i in range(start, stop, 2): @@ -177,11 +178,11 @@ def handle_type_uint32(self, start, stop, value, action, action_kwargs): regs[0].value = regs_value[0] regs[0].type = CellType.UINT32 regs[0].action = action - regs[0].action_kwargs = action_kwargs + regs[0].action_parameters = action_parameters regs[1].value = regs_value[1] regs[1].type = CellType.NEXT - def handle_type_float32(self, start, stop, value, action, action_kwargs): + def handle_type_float32(self, start, stop, value, action, action_parameters): """Handle type uint32.""" regs_value = ModbusSimulatorContext.build_registers_from_value(value, False) for i in range(start, stop, 2): @@ -191,11 +192,11 @@ def handle_type_float32(self, start, stop, value, action, action_kwargs): regs[0].value = regs_value[0] regs[0].type = CellType.FLOAT32 regs[0].action = action - regs[0].action_kwargs = action_kwargs + regs[0].action_parameters = action_parameters regs[1].value = regs_value[1] regs[1].type = CellType.NEXT - def handle_type_string(self, start, stop, value, action, action_kwargs): + def handle_type_string(self, start, stop, value, action, action_parameters): """Handle type string.""" regs = stop - start reg_len = regs * 2 @@ -213,7 +214,7 @@ def handle_type_string(self, start, stop, value, action, action_kwargs): reg.type = CellType.NEXT self.runtime.registers[start].type = CellType.STRING self.runtime.registers[start].action = action - self.runtime.registers[start].action_kwargs = action_kwargs + self.runtime.registers[start].action_parameters = action_parameters def handle_setup_section(self): """Load setup section.""" @@ -304,7 +305,7 @@ def handle_types(self): self.runtime.action_name_to_id[ entry.get(Label.action, type_entry[Label.action]) ], - entry.get(Label.kwargs, None), + entry.get(Label.parameters, None), ) del self.config[section] @@ -439,7 +440,7 @@ class ModbusSimulatorContext(ModbusBaseSlaveContext): {"addr": [32, 34], "value": 0xF1}, --> with value {"addr": [35, 36], "action": "increment"}, --> with action {"addr": [37, 38], "action": "increment", "value": 0xF1} --> with action and value - {"addr": [37, 38], "action": "increment", "kwargs": {"min": 0, "max": 100}} --> with action with arguments + {"addr": [37, 38], "action": "increment", "parameters": {"min": 0, "max": 100}} --> with action with arguments ], "uint16": [ --> Define uint16 (1 register == 2 bytes) --> same as type_bits @@ -494,8 +495,8 @@ def get_text_register(self, register): text_cell.count_read = str(reg.count_read) text_cell.count_write = str(reg.count_write) text_cell.action = self.action_id_to_name[reg.action] - if reg.action_kwargs: - text_cell.action = f"{text_cell.action}({reg.action_kwargs})" + if reg.action_parameters: + text_cell.action = f"{text_cell.action}({reg.action_parameters})" if reg.type in (CellType.INVALID, CellType.UINT16, CellType.NEXT): text_cell.value = str(reg.value) build_len = 0 @@ -588,9 +589,9 @@ def getValues(self, func_code, address, count=1): real_address = self.fc_offset[func_code] + address for i in range(real_address, real_address + count): reg = self.registers[i] - kwargs = reg.action_kwargs if reg.action_kwargs else {} + parameters = reg.action_parameters if reg.action_parameters else {} if reg.action: - self.action_methods[reg.action](self.registers, i, reg, **kwargs) + self.action_methods[reg.action](self.registers, i, reg, **parameters) self.registers[i].count_read += 1 result.append(reg.value) else: @@ -601,9 +602,9 @@ def getValues(self, func_code, address, count=1): for i in range(real_address, real_address + reg_count): reg = self.registers[i] if reg.action: - kwargs = reg.action_kwargs or {} + parameters = reg.action_parameters or {} self.action_methods[reg.action]( - self.registers, i, reg, **kwargs + self.registers, i, reg, **parameters ) self.registers[i].count_read += 1 while count and bit_index < 16: @@ -706,7 +707,7 @@ def action_increment(cls, registers, inx, cell, minval=None, maxval=None): reg2.value = new_regs[1] @classmethod - def action_timestamp(cls, registers, inx, _cell, **_kwargs): + def action_timestamp(cls, registers, inx, _cell, **_parameters): """Set current time. :meta private: @@ -721,7 +722,7 @@ def action_timestamp(cls, registers, inx, _cell, **_kwargs): registers[inx + 6].value = system_time.second @classmethod - def action_reset(cls, _registers, _inx, _cell, **_kwargs): + def action_reset(cls, _registers, _inx, _cell, **_parameters): """Reboot server. :meta private: @@ -729,7 +730,7 @@ def action_reset(cls, _registers, _inx, _cell, **_kwargs): raise RuntimeError("RESET server") @classmethod - def action_uptime(cls, registers, inx, cell, **_kwargs): + def action_uptime(cls, registers, inx, cell, **_parameters): """Return uptime in seconds. :meta private: diff --git a/pymodbus/datastore/store.py b/pymodbus/datastore/store.py index a028d9a4a..51cfbef86 100644 --- a/pymodbus/datastore/store.py +++ b/pymodbus/datastore/store.py @@ -48,7 +48,8 @@ from __future__ import annotations from abc import ABC, abstractmethod -from typing import Any, Dict, Generic, Iterable, TypeVar +from collections.abc import Iterable +from typing import Any, Generic, TypeVar from pymodbus.exceptions import ParameterException @@ -57,7 +58,7 @@ # Datablock Storage # ---------------------------------------------------------------------------# -V = TypeVar('V', list, Dict[int, Any]) +V = TypeVar('V', list, dict[int, Any]) class BaseModbusDataBlock(ABC, Generic[V]): """Base class for a modbus datastore. @@ -217,7 +218,7 @@ def setValues(self, address, values): self.values[start : start + len(values)] = values -class ModbusSparseDataBlock(BaseModbusDataBlock[Dict[int, Any]]): +class ModbusSparseDataBlock(BaseModbusDataBlock[dict[int, Any]]): """A sparse modbus datastore. E.g Usage. diff --git a/pymodbus/device.py b/pymodbus/device.py index 6ea7038f7..adcc58296 100644 --- a/pymodbus/device.py +++ b/pymodbus/device.py @@ -470,7 +470,7 @@ def __iter__(self): """ return self.__counters.__iter__() - def __new__(cls, *_args, **_kwargs): + def __new__(cls): """Create a new instance.""" if "_inst" not in vars(cls): cls._inst = object.__new__(cls) diff --git a/pymodbus/events.py b/pymodbus/events.py index fcd5abf42..848e4fa4b 100644 --- a/pymodbus/events.py +++ b/pymodbus/events.py @@ -48,11 +48,11 @@ class RemoteReceiveEvent(ModbusEvent): 7 1 """ - def __init__(self, **kwargs): + def __init__(self, overrun=False, listen=False, broadcast=False): """Initialize a new event instance.""" - self.overrun = kwargs.get("overrun", False) - self.listen = kwargs.get("listen", False) - self.broadcast = kwargs.get("broadcast", False) + self.overrun = overrun + self.listen = listen + self.broadcast = broadcast def encode(self) -> bytes: """Encode the status bits to an event message. @@ -98,14 +98,14 @@ class RemoteSendEvent(ModbusEvent): 7 0 """ - def __init__(self, **kwargs): + def __init__(self, read=False, slave_abort=False, slave_busy=False, slave_nak=False, write_timeout=False, listen=False): """Initialize a new event instance.""" - self.read = kwargs.get("read", False) - self.slave_abort = kwargs.get("slave_abort", False) - self.slave_busy = kwargs.get("slave_busy", False) - self.slave_nak = kwargs.get("slave_nak", False) - self.write_timeout = kwargs.get("write_timeout", False) - self.listen = kwargs.get("listen", False) + self.read = read + self.slave_abort = slave_abort + self.slave_busy = slave_busy + self.slave_nak = slave_nak + self.write_timeout = write_timeout + self.listen = listen def encode(self): """Encode the status bits to an event message. diff --git a/pymodbus/factory.py b/pymodbus/factory.py index 5a58c6d0c..1e4b04a06 100644 --- a/pymodbus/factory.py +++ b/pymodbus/factory.py @@ -10,19 +10,19 @@ """ # pylint: disable=missing-type-doc -from typing import Callable, Dict - -from pymodbus import bit_read_message as bit_r_msg -from pymodbus import bit_write_message as bit_w_msg -from pymodbus import diag_message as diag_msg -from pymodbus import file_message as file_msg -from pymodbus import mei_message as mei_msg -from pymodbus import other_message as o_msg -from pymodbus import pdu -from pymodbus import register_read_message as reg_r_msg -from pymodbus import register_write_message as reg_w_msg +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 # --------------------------------------------------------------------------- # @@ -77,7 +77,7 @@ class ServerDecoder: ] @classmethod - def getFCdict(cls) -> Dict[int, Callable]: + 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] @@ -85,7 +85,7 @@ 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} + 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] @@ -120,7 +120,7 @@ def _helper(self, data: str): 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) + request = pdu.IllegalFunctionRequest(function_code, 0, 0, 0, False) else: fc_string = "{}: {}".format( # pylint: disable=consider-using-f-string str(self.lookup[function_code]) # pylint: disable=use-maxsplit-arg @@ -214,7 +214,7 @@ 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} + 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] @@ -236,8 +236,6 @@ def decode(self, message): return self._helper(message) except ModbusException as exc: Log.error("Unable to decode response {}", exc) - except Exception as exc: # pylint: disable=broad-except - Log.error("General exception: {}", exc) return None def _helper(self, data: str): diff --git a/pymodbus/framer/__init__.py b/pymodbus/framer/__init__.py index 68d9aeb72..32c61d817 100644 --- a/pymodbus/framer/__init__.py +++ b/pymodbus/framer/__init__.py @@ -4,37 +4,24 @@ "FRAMER_NAME_TO_CLASS", "ModbusFramer", "ModbusAsciiFramer", - "ModbusBinaryFramer", "ModbusRtuFramer", "ModbusSocketFramer", "ModbusTlsFramer", + "Framer", + "FramerType", ] - -import enum - -from pymodbus.framer.ascii_framer import ModbusAsciiFramer -from pymodbus.framer.base import ModbusFramer -from pymodbus.framer.binary_framer import ModbusBinaryFramer -from pymodbus.framer.rtu_framer import ModbusRtuFramer -from pymodbus.framer.socket_framer import ModbusSocketFramer -from pymodbus.framer.tls_framer import ModbusTlsFramer - - -class Framer(str, enum.Enum): - """These represent the different framers.""" - - ASCII = "ascii" - BINARY = "binary" - RTU = "rtu" - SOCKET = "socket" - TLS = "tls" +from pymodbus.framer.framer import Framer, FramerType +from pymodbus.framer.old_framer_ascii import ModbusAsciiFramer +from pymodbus.framer.old_framer_base import ModbusFramer +from pymodbus.framer.old_framer_rtu import ModbusRtuFramer +from pymodbus.framer.old_framer_socket import ModbusSocketFramer +from pymodbus.framer.old_framer_tls import ModbusTlsFramer FRAMER_NAME_TO_CLASS = { - Framer.ASCII: ModbusAsciiFramer, - Framer.BINARY: ModbusBinaryFramer, - Framer.RTU: ModbusRtuFramer, - Framer.SOCKET: ModbusSocketFramer, - Framer.TLS: ModbusTlsFramer, + FramerType.ASCII: ModbusAsciiFramer, + FramerType.RTU: ModbusRtuFramer, + FramerType.SOCKET: ModbusSocketFramer, + FramerType.TLS: ModbusTlsFramer, } diff --git a/pymodbus/message/ascii.py b/pymodbus/framer/ascii.py similarity index 53% rename from pymodbus/message/ascii.py rename to pymodbus/framer/ascii.py index b42460717..0229ed19f 100644 --- a/pymodbus/message/ascii.py +++ b/pymodbus/framer/ascii.py @@ -8,17 +8,17 @@ from binascii import a2b_hex, b2a_hex +from pymodbus.framer.base import FramerBase from pymodbus.logging import Log -from pymodbus.message.base import MessageBase -class MessageAscii(MessageBase): +class FramerAscii(FramerBase): r"""Modbus ASCII Frame Controller. - [ Start ][Address ][ Function ][ Data ][ LRC ][ End ] - 1c 2c 2c Nc 1c 2c + [ Start ][ Dev id ][ Function ][ Data ][ LRC ][ End ] + 1c 2c 2c N*2c 1c 2c - * data can be 0 - 2x252 chars + * data can be 1 - 2x252 chars * end is "\\r\\n" (Carriage return line feed), however the line feed character can be changed via a special command * start is ":" @@ -29,32 +29,37 @@ class MessageAscii(MessageBase): START = b':' END = b'\r\n' + MIN_SIZE = 10 def decode(self, data: bytes) -> tuple[int, int, int, bytes]: - """Decode message.""" - if (used_len := len(data)) < 10: - Log.debug("Short frame: {} wait for more data", data, ":hex") - return 0, 0, 0, self.EMPTY - if data[0:1] != self.START: - if (start := data.find(self.START)) != -1: - used_len = start - Log.debug("Garble data before frame: {}, skip until start of frame", data, ":hex") - return used_len, 0, 0, self.EMPTY - if (used_len := data.find(self.END)) == -1: - Log.debug("Incomplete frame: {} wait for more data", data, ":hex") - return 0, 0, 0, self.EMPTY - - dev_id = int(data[1:3], 16) - lrc = int(data[used_len - 2: used_len], 16) - msg = a2b_hex(data[1 : used_len - 2]) - if not self.check_LRC(msg, lrc): - Log.debug("LRC wrong in frame: {} skipping", data, ":hex") - return used_len+2, 0, 0, self.EMPTY - return used_len+2, 0, dev_id, msg[1:] + """Decode ADU.""" + buf_len = len(data) + used_len = 0 + while True: + if buf_len - used_len < self.MIN_SIZE: + 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 buf_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, 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, dev_id, dev_id, msg[1:] def encode(self, data: bytes, device_id: int, _tid: int) -> bytes: - """Decode message.""" + """Encode ADU.""" dev_id = device_id.to_bytes(1,'big') checksum = self.compute_LRC(dev_id + data) packet = ( diff --git a/pymodbus/framer/base.py b/pymodbus/framer/base.py index 334814347..e0c1595f0 100644 --- a/pymodbus/framer/base.py +++ b/pymodbus/framer/base.py @@ -1,150 +1,43 @@ -"""Framer start.""" -# pylint: disable=missing-type-doc -from __future__ import annotations - -from typing import Any - -from pymodbus.factory import ClientDecoder, ServerDecoder -from pymodbus.logging import Log - - -# Unit ID, Function Code -BYTE_ORDER = ">" -FRAME_HEADER = "BB" - -# Transaction Id, Protocol ID, Length, Unit ID, Function Code -SOCKET_FRAME_HEADER = BYTE_ORDER + "HHH" + FRAME_HEADER - -# Function Code -TLS_FRAME_HEADER = BYTE_ORDER + "B" - - -class ModbusFramer: - """Base Framer class.""" - - name = "" - - def __init__( - self, - decoder: ClientDecoder | ServerDecoder, - client, - ) -> None: - """Initialize a new instance of the framer. - - :param decoder: The decoder implementation to use - """ - self.decoder = decoder - self.client = client - self._header: dict[str, Any] = { - "lrc": "0000", - "len": 0, - "uid": 0x00, - "tid": 0, - "pid": 0, - "crc": b"\x00\x00", - } - self._buffer = b"" - - def _validate_slave_id(self, slaves: list, single: bool) -> bool: - """Validate if the received data is valid for the client. - - :param slaves: list of slave id for which the transaction is valid - :param single: Set to true to treat this as a single context - :return: - """ - if single: - return True - if 0 in slaves or 0xFF in slaves: - # Handle Modbus TCP slave identifier (0x00 0r 0xFF) - # in asynchronous requests - return True - return self._header["uid"] in slaves - - def sendPacket(self, message): - """Send packets on the bus. - - With 3.5char delay between frames - :param message: Message to be sent over the bus - :return: - """ - return self.client.send(message) +"""Framer implementations. - def recvPacket(self, size): - """Receive packet from the bus. +The implementation is responsible for encoding/decoding requests/responses. - With specified len - :param size: Number of bytes to read - :return: - """ - return self.client.recv(size) +According to the selected type of modbus frame a prefix/suffix is added/removed +""" +from __future__ import annotations - def resetFrame(self): - """Reset the entire message frame. +from abc import abstractmethod - This allows us to skip ovver errors that may be in the stream. - It is hard to know if we are simply out of sync or if there is - an error in the stream as we have no way to check the start or - end of the message (python just doesn't have the resolution to - check for millisecond delays). - """ - Log.debug( - "Resetting frame - Current Frame in buffer - {}", self._buffer, ":hex" - ) - self._buffer = b"" - self._header = { - "lrc": "0000", - "crc": b"\x00\x00", - "len": 0, - "uid": 0x00, - "pid": 0, - "tid": 0, - } - def populateResult(self, result): - """Populate the modbus result header. +class FramerBase: + """Intern base.""" - The serial packets do not have any header information - that is copied. + EMPTY = b'' - :param result: The response packet - """ - result.slave_id = self._header.get("uid", 0) - result.transaction_id = self._header.get("tid", 0) - result.protocol_id = self._header.get("pid", 0) + def __init__(self) -> None: + """Initialize a ADU instance.""" - def processIncomingPacket(self, data, callback, slave, **kwargs): - """Process new packet pattern. + def set_dev_ids(self, _dev_ids: list[int]): + """Set/update allowed device ids.""" - 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. + def set_fc_calc(self, _fc: int, _msg_size: int, _count_pos: int): + """Set/Update function code information.""" - The processed and decoded messages are pushed to the callback - function to process and send. + @abstractmethod + def decode(self, data: bytes) -> tuple[int, int, int, bytes]: + """Decode ADU. - :param data: The new packet data - :param callback: The function to send results to - :param slave: Process if slave id matches, ignore otherwise (could be a - list of slave ids (server) or single slave id(client/server)) - :param kwargs: - :raises ModbusIOException: + returns: + used_len (int) or 0 to read more + transaction_id (int) or 0 + device_id (int) or 0 + modbus request/response (bytes) """ - self._buffer += data - Log.debug("Processing: {}", self._buffer, ":hex") - if not isinstance(slave, (list, tuple)): - slave = [slave] - single = kwargs.pop("single", False) - self.frameProcessIncomingPacket(single, callback, slave, **kwargs) - - def frameProcessIncomingPacket( - self, _single, _callback, _slave, _tid=None, **kwargs - ) -> None: - """Process new packet pattern.""" - def buildPacket(self, message) -> bytes: # type:ignore[empty-body] - """Create a ready to send modbus packet. + @abstractmethod + def encode(self, pdu: bytes, dev_id: int, tid: int) -> bytes: + """Encode ADU. - :param message: The populated request/response to send + returns: + modbus ADU (bytes) """ diff --git a/pymodbus/framer/binary_framer.py b/pymodbus/framer/binary_framer.py deleted file mode 100644 index 746fe0f52..000000000 --- a/pymodbus/framer/binary_framer.py +++ /dev/null @@ -1,134 +0,0 @@ -"""Binary framer.""" -# pylint: disable=missing-type-doc -import struct - -from pymodbus.exceptions import ModbusIOException -from pymodbus.framer.base import BYTE_ORDER, FRAME_HEADER, ModbusFramer -from pymodbus.logging import Log -from pymodbus.message.rtu import MessageRTU - - -BINARY_FRAME_HEADER = BYTE_ORDER + FRAME_HEADER - -# --------------------------------------------------------------------------- # -# Modbus Binary Message -# --------------------------------------------------------------------------- # - - -class ModbusBinaryFramer(ModbusFramer): - """Modbus Binary Frame Controller. - - [ Start ][Address ][ Function ][ Data ][ CRC ][ End ] - 1b 1b 1b Nb 2b 1b - - * data can be 0 - 2x252 chars - * end is "}" - * start is "{" - - The idea here is that we implement the RTU protocol, however, - instead of using timing for message delimiting, we use start - and end of message characters (in this case { and }). Basically, - this is a binary framer. - - The only case we have to watch out for is when a message contains - the { or } characters. If we encounter these characters, we - simply duplicate them. Hopefully we will not encounter those - characters that often and will save a little bit of bandwitch - without a real-time system. - - Protocol defined by jamod.sourceforge.net. - """ - - method = "binary" - - def __init__(self, decoder, client=None): - """Initialize a new instance of the framer. - - :param decoder: The decoder implementation to use - """ - super().__init__(decoder, client) - # self._header.update({"crc": 0x0000}) - self._hsize = 0x01 - self._start = b"\x7b" # { - self._end = b"\x7d" # } - self._repeat = [b"}"[0], b"{"[0]] # python3 hack - - def decode_data(self, data): - """Decode data.""" - if len(data) > self._hsize: - uid = struct.unpack(">B", data[1:2])[0] - fcode = struct.unpack(">B", data[2:3])[0] - return {"slave": uid, "fcode": fcode} - return {} - - def frameProcessIncomingPacket(self, single, callback, slave, _tid=None, **kwargs): - """Process new packet pattern.""" - def check_frame(self) -> bool: - """Check and decode the next frame.""" - start = self._buffer.find(self._start) - if start == -1: - return False - if start > 0: # go ahead and skip old bad data - self._buffer = self._buffer[start:] - - if (end := self._buffer.find(self._end)) != -1: - self._header["len"] = end - self._header["uid"] = struct.unpack(">B", self._buffer[1:2])[0] - self._header["crc"] = struct.unpack(">H", self._buffer[end - 2 : end])[0] - data = self._buffer[1 : end - 2] - return MessageRTU.check_CRC(data, self._header["crc"]) - return False - - while len(self._buffer) > 1: - if not check_frame(self): - Log.debug("Frame check failed, ignoring!!") - break - if not self._validate_slave_id(slave, single): - header_txt = self._header["uid"] - Log.debug("Not a valid slave id - {}, ignoring!!", header_txt) - self.resetFrame() - break - start = self._hsize + 1 - end = self._header["len"] - 2 - buffer = self._buffer[start:end] - if end > 0: - frame = buffer - else: - frame = b"" - if (result := self.decoder.decode(frame)) is None: - raise ModbusIOException("Unable to decode response") - self.populateResult(result) - self._buffer = self._buffer[self._header["len"] + 2 :] - self._header = {"crc": 0x0000, "len": 0, "uid": 0x00} - callback(result) # defer or push to a thread? - - def buildPacket(self, message): - """Create a ready to send modbus packet. - - :param message: The request/response to send - :returns: The encoded packet - """ - data = self._preflight(message.encode()) - packet = ( - struct.pack(BINARY_FRAME_HEADER, message.slave_id, message.function_code) - + data - ) - packet += struct.pack(">H", MessageRTU.compute_CRC(packet)) - packet = self._start + packet + self._end - return packet - - def _preflight(self, data): - """Do preflight buffer test. - - This basically scans the buffer for start and end - tags and if found, escapes them. - - :param data: The message to escape - :returns: the escaped packet - """ - array = bytearray() - for item in data: - if item in self._repeat: - array.append(item) - array.append(item) - return bytes(array) diff --git a/pymodbus/message/message.py b/pymodbus/framer/framer.py similarity index 60% rename from pymodbus/message/message.py rename to pymodbus/framer/framer.py index 20427bd23..5ab1194ce 100644 --- a/pymodbus/message/message.py +++ b/pymodbus/framer/framer.py @@ -1,24 +1,26 @@ -"""ModbusMessage layer. +"""Framing layer. -The message layer is responsible for encoding/decoding requests/responses. +The framer layer is responsible for isolating/generating the request/request from +the frame (prefix - postfix) According to the selected type of modbus frame a prefix/suffix is added/removed + +This layer is also responsible for discarding invalid frames and frames for other slaves. """ from __future__ import annotations from abc import abstractmethod from enum import Enum -from pymodbus.message.ascii import MessageAscii -from pymodbus.message.base import MessageBase -from pymodbus.message.raw import MessageRaw -from pymodbus.message.rtu import MessageRTU -from pymodbus.message.socket import MessageSocket -from pymodbus.message.tls import MessageTLS +from pymodbus.framer.ascii import FramerAscii +from pymodbus.framer.raw import FramerRaw +from pymodbus.framer.rtu import FramerRTU +from pymodbus.framer.socket import FramerSocket +from pymodbus.framer.tls import FramerTLS from pymodbus.transport.transport import CommParams, ModbusProtocol -class MessageType(str, Enum): +class FramerType(str, Enum): """Type of Modbus frame.""" RAW = "raw" # only used for testing @@ -28,15 +30,13 @@ class MessageType(str, Enum): TLS = "tls" -class Message(ModbusProtocol): - """Message layer extending transport layer. - - extends the ModbusProtocol to handle receiving and sending of complete modbus messsagees. +class Framer(ModbusProtocol): + """Framer layer extending transport layer. - Message is the prefix / suffix around the response/request + extends the ModbusProtocol to handle receiving and sending of complete modbus PDU. When receiving: - - Secures full valid Modbus message is received (across multiple callbacks) + - Secures full valid Modbus PDU is received (across multiple callbacks) - Validates and removes Modbus prefix/suffix (CRC for serial, MBAP for others) - Callback with pure request/response - Skips invalid messagees @@ -51,14 +51,14 @@ class Message(ModbusProtocol): """ def __init__(self, - message_type: MessageType, + framer_type: FramerType, params: CommParams, is_server: bool, device_ids: list[int], ): - """Initialize a message instance. + """Initialize a framer instance. - :param message_type: Modbus message type + :param framer_type: Modbus message type :param params: parameter dataclass :param is_server: true if object act as a server (listen/connect) :param device_ids: list of device id to accept, 0 in list means broadcast. @@ -66,13 +66,15 @@ def __init__(self, super().__init__(params, is_server) self.device_ids = device_ids self.broadcast: bool = (0 in device_ids) - self.msg_handle: MessageBase = { - MessageType.RAW: MessageRaw(), - MessageType.ASCII: MessageAscii(), - MessageType.RTU: MessageRTU(), - MessageType.SOCKET: MessageSocket(), - MessageType.TLS: MessageTLS(), - }[message_type] + + self.handle = { + FramerType.RAW: FramerRaw(), + FramerType.ASCII: FramerAscii(), + FramerType.RTU: FramerRTU(), + FramerType.SOCKET: FramerSocket(), + FramerType.TLS: FramerTLS(), + }[framer_type] + def validate_device_id(self, dev_id: int) -> bool: @@ -82,21 +84,19 @@ def validate_device_id(self, dev_id: int) -> bool: def callback_data(self, data: bytes, addr: tuple | None = None) -> int: """Handle received data.""" - tot_len = len(data) - start = 0 + tot_len = 0 + buf_len = len(data) while True: - used_len, tid, device_id, msg = self.msg_handle.decode(data[start:]) + used_len, tid, device_id, msg = self.handle.decode(data[tot_len:]) + tot_len += used_len if msg: - self.callback_request_response(msg, device_id, tid) - if not used_len: - return start - start += used_len - if start == tot_len: + if self.broadcast or device_id in self.device_ids: + self.callback_request_response(msg, device_id, tid) + if tot_len == buf_len: + return tot_len + else: return tot_len - # --------------------- # - # callbacks and helpers # - # --------------------- # @abstractmethod def callback_request_response(self, data: bytes, device_id: int, tid: int) -> None: """Handle received modbus request/response.""" @@ -109,5 +109,5 @@ def build_send(self, data: bytes, device_id: int, tid: int, addr: tuple | None = :param tid: transaction id (0 if not used). :param addr: optional addr, only used for UDP server. """ - send_data = self.msg_handle.encode(data, device_id, tid) + send_data = self.handle.encode(data, device_id, tid) self.send(send_data, addr) diff --git a/pymodbus/framer/ascii_framer.py b/pymodbus/framer/old_framer_ascii.py similarity index 76% rename from pymodbus/framer/ascii_framer.py rename to pymodbus/framer/old_framer_ascii.py index 4d2fbc68a..1773e4592 100644 --- a/pymodbus/framer/ascii_framer.py +++ b/pymodbus/framer/old_framer_ascii.py @@ -1,17 +1,16 @@ """Ascii_framer.""" -# pylint: disable=missing-type-doc - from pymodbus.exceptions import ModbusIOException -from pymodbus.framer.base import BYTE_ORDER, FRAME_HEADER, ModbusFramer +from pymodbus.framer.old_framer_base import BYTE_ORDER, FRAME_HEADER, ModbusFramer from pymodbus.logging import Log -from pymodbus.message.ascii import MessageAscii + +from .ascii import FramerAscii ASCII_FRAME_HEADER = BYTE_ORDER + FRAME_HEADER # --------------------------------------------------------------------------- # -# Modbus ASCII Message +# Modbus ASCII olf framer # --------------------------------------------------------------------------- # class ModbusAsciiFramer(ModbusFramer): r"""Modbus ASCII Frame Controller. @@ -39,7 +38,7 @@ def __init__(self, decoder, client=None): self._hsize = 0x02 self._start = b":" self._end = b"\r\n" - self.message_handler = MessageAscii() + self.message_handler = FramerAscii() def decode_data(self, data): """Decode data.""" @@ -49,10 +48,10 @@ def decode_data(self, data): return {"slave": uid, "fcode": fcode} return {} - def frameProcessIncomingPacket(self, single, callback, slave, _tid=None, **kwargs): + def frameProcessIncomingPacket(self, single, callback, slave, tid=None): """Process new packet pattern.""" while len(self._buffer): - used_len, _tid, dev_id, data = self.message_handler.decode(self._buffer) + used_len, tid, dev_id, data = self.message_handler.decode(self._buffer) if not data: if not used_len: return @@ -70,13 +69,3 @@ def frameProcessIncomingPacket(self, single, callback, slave, _tid=None, **kwarg self._buffer = self._buffer[used_len :] self._header = {"uid": 0x00} callback(result) # defer this - - def buildPacket(self, message): - """Create a ready to send modbus packet. - - :param message: The request/response to send - :return: The encoded packet - """ - data = message.function_code.to_bytes(1,'big') + message.encode() - packet = self.message_handler.encode(data, message.slave_id, message.transaction_id) - return packet diff --git a/pymodbus/framer/old_framer_base.py b/pymodbus/framer/old_framer_base.py new file mode 100644 index 000000000..b737a1b47 --- /dev/null +++ b/pymodbus/framer/old_framer_base.py @@ -0,0 +1,168 @@ +"""Framer start.""" +# pylint: disable=missing-type-doc +from __future__ import annotations + +import time +from typing import TYPE_CHECKING, Any + +from pymodbus.factory import ClientDecoder, ServerDecoder +from pymodbus.framer.base import FramerBase +from pymodbus.logging import Log +from pymodbus.pdu import ModbusRequest, ModbusResponse + + +if TYPE_CHECKING: + from pymodbus.client.base import ModbusBaseSyncClient + +# Unit ID, Function Code +BYTE_ORDER = ">" +FRAME_HEADER = "BB" + +# Transaction Id, Protocol ID, Length, Unit ID, Function Code +SOCKET_FRAME_HEADER = BYTE_ORDER + "HHH" + FRAME_HEADER + +# Function Code +TLS_FRAME_HEADER = BYTE_ORDER + "B" + + +class ModbusFramer: + """Base Framer class.""" + + name = "" + + def __init__( + self, + decoder: ClientDecoder | ServerDecoder, + client: ModbusBaseSyncClient, + ) -> None: + """Initialize a new instance of the framer. + + :param decoder: The decoder implementation to use + """ + self.decoder = decoder + self.client = client + self._header: dict[str, Any] + self._reset_header() + self._buffer = b"" + self.message_handler: FramerBase + + def _reset_header(self) -> None: + self._header = { + "lrc": "0000", + "len": 0, + "uid": 0x00, + "tid": 0, + "pid": 0, + "crc": b"\x00\x00", + } + + def _validate_slave_id(self, slaves: list, single: bool) -> bool: + """Validate if the received data is valid for the client. + + :param slaves: list of slave id for which the transaction is valid + :param single: Set to true to treat this as a single context + :return: + """ + if single: + return True + if 0 in slaves or 0xFF in slaves: + # Handle Modbus TCP slave identifier (0x00 0r 0xFF) + # in asynchronous requests + return True + return self._header["uid"] in slaves + + def sendPacket(self, message: bytes): + """Send packets on the bus. + + With 3.5char delay between frames + :param message: Message to be sent over the bus + :return: + """ + return self.client.send(message) + + def recvPacket(self, size: int) -> bytes: + """Receive packet from the bus. + + With specified len + :param size: Number of bytes to read + :return: + """ + packet = self.client.recv(size) + self.client.last_frame_end = round(time.time(), 6) + return packet + + def resetFrame(self): + """Reset the entire message frame. + + This allows us to skip ovver errors that may be in the stream. + It is hard to know if we are simply out of sync or if there is + an error in the stream as we have no way to check the start or + end of the message (python just doesn't have the resolution to + check for millisecond delays). + """ + Log.debug( + "Resetting frame - Current Frame in buffer - {}", self._buffer, ":hex" + ) + self._buffer = b"" + self._header = { + "lrc": "0000", + "crc": b"\x00\x00", + "len": 0, + "uid": 0x00, + "pid": 0, + "tid": 0, + } + + def populateResult(self, result): + """Populate the modbus result header. + + The serial packets do not have any header information + that is copied. + + :param result: The response packet + """ + result.slave_id = self._header.get("uid", 0) + result.transaction_id = self._header.get("tid", 0) + result.protocol_id = self._header.get("pid", 0) + + def processIncomingPacket(self, data: bytes, callback, slave, single=False, tid=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. + + The processed and decoded messages are pushed to the callback + function to process and send. + + :param data: The new packet data + :param callback: The function to send results to + :param slave: Process if slave id matches, ignore otherwise (could be a + list of slave ids (server) or single slave id(client/server)) + :param single: multiple slave ? + :param tid: transaction id + :raises ModbusIOException: + """ + Log.debug("Processing: {}", data, ":hex") + self._buffer += data + if self._buffer == b'': + return + if not isinstance(slave, (list, tuple)): + slave = [slave] + self.frameProcessIncomingPacket(single, callback, slave, tid=tid) + + def frameProcessIncomingPacket( + self, _single, _callback, _slave, tid=None + ) -> None: + """Process new packet pattern.""" + + def buildPacket(self, message: ModbusRequest | ModbusResponse) -> 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.message_handler.encode(data, message.slave_id, message.transaction_id) + return packet diff --git a/pymodbus/framer/rtu_framer.py b/pymodbus/framer/old_framer_rtu.py similarity index 90% rename from pymodbus/framer/rtu_framer.py rename to pymodbus/framer/old_framer_rtu.py index c68196632..88478b0bd 100644 --- a/pymodbus/framer/rtu_framer.py +++ b/pymodbus/framer/old_framer_rtu.py @@ -4,9 +4,9 @@ import time from pymodbus.exceptions import ModbusIOException -from pymodbus.framer.base import BYTE_ORDER, FRAME_HEADER, ModbusFramer +from pymodbus.framer.old_framer_base import BYTE_ORDER, FRAME_HEADER, ModbusFramer +from pymodbus.framer.rtu import FramerRTU from pymodbus.logging import Log -from pymodbus.message.rtu import MessageRTU from pymodbus.utilities import ModbusTransactionState @@ -14,7 +14,7 @@ # --------------------------------------------------------------------------- # -# Modbus RTU Message +# Modbus RTU old Framer # --------------------------------------------------------------------------- # class ModbusRtuFramer(ModbusFramer): """Modbus RTU Frame controller. @@ -58,10 +58,8 @@ def __init__(self, decoder, client=None): """ super().__init__(decoder, client) self._hsize = 0x01 - self._end = b"\x0d\x0a" - self._min_frame_size = 4 self.function_codes = decoder.lookup.keys() if decoder else {} - self.message_handler = MessageRTU() + self.message_handler = FramerRTU() def decode_data(self, data): """Decode data.""" @@ -72,7 +70,7 @@ def decode_data(self, data): return {} - def frameProcessIncomingPacket(self, _single, callback, slave, _tid=None, **kwargs): # noqa: C901 + def frameProcessIncomingPacket(self, _single, callback, slave, tid=None): # noqa: C901 """Process new packet pattern.""" def is_frame_ready(self): @@ -133,7 +131,7 @@ def check_frame(self): data = self._buffer[: frame_size - 2] crc = self._header["crc"] crc_val = (int(crc[0]) << 8) + int(crc[1]) - return MessageRTU.check_CRC(data, crc_val) + return FramerRTU.check_CRC(data, crc_val) except (IndexError, KeyError, struct.error): return False @@ -148,7 +146,7 @@ def check_frame(self): Log.debug("Frame check failed, ignoring!!") x = self._buffer self.resetFrame() - self._buffer = x + self._buffer: bytes = x skip_cur_frame = True continue start = self._hsize @@ -172,14 +170,13 @@ def buildPacket(self, message): :param message: The populated request/response to send """ - data = message.function_code.to_bytes(1, 'big') + message.encode() - packet = self.message_handler.encode(data, message.slave_id, message.transaction_id) + packet = super().buildPacket(message) # Ensure that transaction is actually the slave id for serial comms message.transaction_id = 0 return packet - def sendPacket(self, message): + def sendPacket(self, message: bytes) -> int: """Send packets on the bus with 3.5char delay between frames. :param message: Message to be sent over the bus @@ -187,7 +184,10 @@ def sendPacket(self, message): """ super().resetFrame() start = time.time() - timeout = start + self.client.comm_params.timeout_connect + if hasattr(self.client,"ctx"): + timeout = start + self.client.ctx.comm_params.timeout_connect + else: + timeout = start + self.client.comm_params.timeout_connect while self.client.state != ModbusTransactionState.IDLE: if self.client.state == ModbusTransactionState.TRANSACTION_COMPLETE: timestamp = round(time.time(), 6) @@ -225,13 +225,3 @@ def sendPacket(self, message): size = self.client.send(message) self.client.last_frame_end = round(time.time(), 6) return size - - def recvPacket(self, size): - """Receive packet from the bus with specified len. - - :param size: Number of bytes to read - :return: - """ - result = self.client.recv(size) - self.client.last_frame_end = round(time.time(), 6) - return result diff --git a/pymodbus/framer/socket_framer.py b/pymodbus/framer/old_framer_socket.py similarity index 78% rename from pymodbus/framer/socket_framer.py rename to pymodbus/framer/old_framer_socket.py index 4b82e9bb7..65bc9ba4e 100644 --- a/pymodbus/framer/socket_framer.py +++ b/pymodbus/framer/old_framer_socket.py @@ -1,17 +1,16 @@ """Socket framer.""" -# pylint: disable=missing-type-doc import struct from pymodbus.exceptions import ( ModbusIOException, ) -from pymodbus.framer.base import SOCKET_FRAME_HEADER, ModbusFramer +from pymodbus.framer.old_framer_base import SOCKET_FRAME_HEADER, ModbusFramer +from pymodbus.framer.socket import FramerSocket from pymodbus.logging import Log -from pymodbus.message.socket import MessageSocket # --------------------------------------------------------------------------- # -# Modbus TCP Message +# Modbus TCP old framer # --------------------------------------------------------------------------- # @@ -43,7 +42,7 @@ def __init__(self, decoder, client=None): """ super().__init__(decoder, client) self._hsize = 0x07 - self.message_handler = MessageSocket() + self.message_handler = FramerSocket() def decode_data(self, data): """Decode data.""" @@ -60,7 +59,7 @@ def decode_data(self, data): } return {} - def frameProcessIncomingPacket(self, single, callback, slave, tid=None, **kwargs): + def frameProcessIncomingPacket(self, single, callback, slave, tid=None): """Process new packet pattern. This takes in a new request packet, adds it to the current @@ -73,12 +72,11 @@ def frameProcessIncomingPacket(self, single, callback, slave, tid=None, **kwargs function to process and send. """ while True: + if self._buffer == b'': + return used_len, use_tid, dev_id, data = self.message_handler.decode(self._buffer) if not data: - if not used_len: - return - self._buffer = self._buffer[used_len :] - continue + return self._header["uid"] = dev_id self._header["tid"] = use_tid self._header["pid"] = 0 @@ -90,18 +88,9 @@ def frameProcessIncomingPacket(self, single, callback, slave, tid=None, **kwargs self.resetFrame() raise ModbusIOException("Unable to decode request") self.populateResult(result) - self._buffer = self._buffer[used_len:] - self._header = {"tid": 0, "pid": 0, "len": 0, "uid": 0} + self._buffer: bytes = self._buffer[used_len:] + self._reset_header() if tid and tid != result.transaction_id: self.resetFrame() else: callback(result) # defer or push to a thread? - - def buildPacket(self, message): - """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.message_handler.encode(data, message.slave_id, message.transaction_id) - return packet diff --git a/pymodbus/framer/tls_framer.py b/pymodbus/framer/old_framer_tls.py similarity index 63% rename from pymodbus/framer/tls_framer.py rename to pymodbus/framer/old_framer_tls.py index 9e2473581..c34470f4e 100644 --- a/pymodbus/framer/tls_framer.py +++ b/pymodbus/framer/old_framer_tls.py @@ -1,17 +1,16 @@ """TLS framer.""" -# pylint: disable=missing-type-doc import struct +from time import sleep from pymodbus.exceptions import ( ModbusIOException, ) -from pymodbus.framer.base import TLS_FRAME_HEADER, ModbusFramer -from pymodbus.logging import Log -from pymodbus.message.tls import MessageTLS +from pymodbus.framer.old_framer_base import TLS_FRAME_HEADER, ModbusFramer +from pymodbus.framer.tls import FramerTLS # --------------------------------------------------------------------------- # -# Modbus TLS Message +# Modbus TLS old framer # --------------------------------------------------------------------------- # @@ -35,7 +34,7 @@ def __init__(self, decoder, client=None): """ super().__init__(decoder, client) self._hsize = 0x0 - self.message_handler = MessageTLS() + self.message_handler = FramerTLS() def decode_data(self, data): """Decode data.""" @@ -44,37 +43,27 @@ def decode_data(self, data): return {"fcode": fcode} return {} - def frameProcessIncomingPacket(self, single, callback, slave, _tid=None, **kwargs): + def recvPacket(self, size): + """Receive packet from the bus.""" + sleep(0.5) + return super().recvPacket(size) + + def frameProcessIncomingPacket(self, _single, callback, _slave, tid=None): """Process new packet pattern.""" # no slave id for Modbus Security Application Protocol while True: used_len, use_tid, dev_id, data = self.message_handler.decode(self._buffer) if not data: - if not used_len: - return - self._buffer = self._buffer[used_len :] - continue + return self._header["uid"] = dev_id self._header["tid"] = use_tid self._header["pid"] = 0 - if not self._validate_slave_id(slave, single): - Log.debug("Not in valid slave id - {}, ignoring!!", slave) - self.resetFrame() - return if (result := self.decoder.decode(data)) is None: + self.resetFrame() raise ModbusIOException("Unable to decode request") self.populateResult(result) - self._buffer = b"" - self._header = {} + self._buffer: bytes = self._buffer[used_len:] + self._reset_header() callback(result) # defer or push to a thread? - - def buildPacket(self, message): - """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.message_handler.encode(data, message.slave_id, message.transaction_id) - return packet diff --git a/pymodbus/message/raw.py b/pymodbus/framer/raw.py similarity index 54% rename from pymodbus/message/raw.py rename to pymodbus/framer/raw.py index 88627482b..96ca1bc2e 100644 --- a/pymodbus/message/raw.py +++ b/pymodbus/framer/raw.py @@ -1,30 +1,32 @@ -"""ModbusMessage layer.""" +"""Modbus Raw (passthrough) implementation.""" from __future__ import annotations +from pymodbus.framer.base import FramerBase from pymodbus.logging import Log -from pymodbus.message.base import MessageBase -class MessageRaw(MessageBase): +class FramerRaw(FramerBase): r"""Modbus RAW Frame Controller. [ Device id ][Transaction id ][ Data ] - 1c 2c Nc + 1b 2b Nb - * data can be 1 - X chars + * data can be 0 - X bytes This framer is used for non modbus communication and testing purposes. """ + MIN_SIZE = 3 + def decode(self, data: bytes) -> tuple[int, int, int, bytes]: - """Decode message.""" - if len(data) < 3: + """Decode ADU.""" + if len(data) < self.MIN_SIZE: Log.debug("Short frame: {} wait for more data", data, ":hex") return 0, 0, 0, self.EMPTY dev_id = int(data[0]) tid = int(data[1]) return len(data), dev_id, tid, data[2:] - def encode(self, data: bytes, device_id: int, tid: int) -> bytes: - """Decode message.""" - return device_id.to_bytes(1, 'big') + tid.to_bytes(1, 'big') + data + def encode(self, pdu: bytes, dev_id: int, tid: int) -> bytes: + """Encode ADU.""" + return dev_id.to_bytes(1, 'big') + tid.to_bytes(1, 'big') + pdu diff --git a/pymodbus/framer/rtu.py b/pymodbus/framer/rtu.py new file mode 100644 index 000000000..9233245a1 --- /dev/null +++ b/pymodbus/framer/rtu.py @@ -0,0 +1,153 @@ +"""Modbus RTU frame implementation.""" +from __future__ import annotations + +from collections import namedtuple + +from pymodbus.framer.base import FramerBase +from pymodbus.logging import Log + + +class FramerRTU(FramerBase): + """Modbus RTU frame type. + + [ 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. + + 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) + 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. + Device drivers will typically flush buffer after 10ms of silence. + If no data is received for 50ms the transmission / frame can be considered + complete. + """ + + MIN_SIZE = 5 + + FC_LEN = namedtuple("FC_LEN", "req_len req_bytepos resp_len resp_bytepos") + + def __init__(self) -> None: + """Initialize a ADU instance.""" + super().__init__() + self.fc_len: dict[int, FramerRTU.FC_LEN] = {} + + + @classmethod + def generate_crc16_table(cls) -> list[int]: + """Generate a crc16 lookup table. + + .. note:: This will only be generated once + """ + result = [] + for byte in range(256): + crc = 0x0000 + for _ in range(8): + if (byte ^ crc) & 0x0001: + crc = (crc >> 1) ^ 0xA001 + else: + crc >>= 1 + byte >>= 1 + result.append(crc) + return result + crc16_table: list[int] = [0] + + + def setup_fc_len(self, _fc: int, + _req_len: int, _req_byte_pos: int, + _resp_len: int, _resp_byte_pos: int + ): + """Define request/response lengths pr function code.""" + return + + def decode(self, data: bytes) -> tuple[int, int, int, bytes]: + """Decode ADU.""" + if (buf_len := len(data)) < self.MIN_SIZE: + Log.debug("Short frame: {} wait for more data", data, ":hex") + return 0, 0, 0, b'' + + i = -1 + try: + while True: + i += 1 + if i > buf_len - self.MIN_SIZE + 1: + break + dev_id = int(data[i]) + fc_len = 5 + msg_len = fc_len -2 if fc_len > 0 else int(data[i-fc_len])-fc_len+1 + if msg_len + i + 2 > buf_len: + break + crc_val = (int(data[i+msg_len]) << 8) + int(data[i+msg_len+1]) + if not self.check_CRC(data[i:i+msg_len], crc_val): + Log.debug("Skipping frame CRC with len {} at index {}!", msg_len, i) + raise KeyError + return i+msg_len+2, dev_id, dev_id, data[i+1:i+msg_len] + except KeyError: + i = buf_len + return i, 0, 0, b'' + + + 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') + + @classmethod + def check_CRC(cls, data: bytes, check: int) -> bool: + """Check if the data matches the passed in CRC. + + :param data: The data to create a crc16 of + :param check: The CRC to validate + :returns: True if matched, False otherwise + """ + return cls.compute_CRC(data) == check + + @classmethod + def compute_CRC(cls, data: bytes) -> int: + """Compute a crc16 on the passed in bytes. + + The difference between modbus's crc16 and a normal crc16 + is that modbus starts the crc value out at 0xffff. + + :param data: The data to create a crc16 of + :returns: The calculated CRC + """ + crc = 0xFFFF + for data_byte in data: + idx = cls.crc16_table[(crc ^ int(data_byte)) & 0xFF] + crc = ((crc >> 8) & 0xFF) ^ idx + swapped = ((crc << 8) & 0xFF00) | ((crc >> 8) & 0x00FF) + return swapped + +FramerRTU.crc16_table = FramerRTU.generate_crc16_table() diff --git a/pymodbus/message/socket.py b/pymodbus/framer/socket.py similarity index 67% rename from pymodbus/message/socket.py rename to pymodbus/framer/socket.py index e895c74f9..793e37f8e 100644 --- a/pymodbus/message/socket.py +++ b/pymodbus/framer/socket.py @@ -1,16 +1,11 @@ -"""ModbusMessage layer. - -is extending ModbusProtocol to handle receiving and sending of messsagees. - -ModbusMessage provides a unified interface to send/receive Modbus requests/responses. -""" +"""Modbus Socket frame implementation.""" from __future__ import annotations +from pymodbus.framer.base import FramerBase from pymodbus.logging import Log -from pymodbus.message.base import MessageBase -class MessageSocket(MessageBase): +class FramerSocket(FramerBase): """Modbus Socket frame type. [ MBAP Header ] [ Function Code] [ Data ] @@ -20,9 +15,11 @@ class MessageSocket(MessageBase): * length = uid + function code + data """ + MIN_SIZE = 8 + def decode(self, data: bytes) -> tuple[int, int, int, bytes]: - """Decode message.""" - if (used_len := len(data)) < 9: + """Decode ADU.""" + if (used_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 msg_tid = int.from_bytes(data[0:2], 'big') @@ -35,13 +32,13 @@ def decode(self, data: bytes) -> tuple[int, int, int, bytes]: msg_len = 9 return msg_len, msg_tid, msg_dev, data[7:msg_len] - def encode(self, data: bytes, device_id: int, tid: int) -> bytes: - """Decode message.""" + def encode(self, pdu: bytes, device_id: int, tid: int) -> bytes: + """Encode ADU.""" packet = ( tid.to_bytes(2, 'big') + b'\x00\x00' + - (len(data) + 1).to_bytes(2, 'big') + + (len(pdu) + 1).to_bytes(2, 'big') + device_id.to_bytes(1, 'big') + - data + pdu ) return packet diff --git a/pymodbus/framer/tls.py b/pymodbus/framer/tls.py new file mode 100644 index 000000000..a4e83973b --- /dev/null +++ b/pymodbus/framer/tls.py @@ -0,0 +1,20 @@ +"""Modbus TLS frame implementation.""" +from __future__ import annotations + +from pymodbus.framer.base import FramerBase + + +class FramerTLS(FramerBase): + """Modbus TLS frame type. + + [ Function Code] [ Data ] + 1b Nb + """ + + def decode(self, data: bytes) -> tuple[int, int, int, bytes]: + """Decode ADU.""" + return len(data), 0, 0, data + + def encode(self, pdu: bytes, _device_id: int, _tid: int) -> bytes: + """Encode ADU.""" + return pdu diff --git a/pymodbus/message/__init__.py b/pymodbus/message/__init__.py deleted file mode 100644 index 5e35bc814..000000000 --- a/pymodbus/message/__init__.py +++ /dev/null @@ -1,7 +0,0 @@ -"""Message.""" -__all__ = [ - "Message", - "MessageType", -] - -from pymodbus.message.message import Message, MessageType diff --git a/pymodbus/message/base.py b/pymodbus/message/base.py deleted file mode 100644 index 3a41bb9eb..000000000 --- a/pymodbus/message/base.py +++ /dev/null @@ -1,38 +0,0 @@ -"""ModbusMessage layer. - -The message layer is responsible for encoding/decoding requests/responses. - -According to the selected type of modbus frame a prefix/suffix is added/removed -""" -from __future__ import annotations - -from abc import abstractmethod - - -class MessageBase: - """Intern base.""" - - EMPTY = b'' - - def __init__(self) -> None: - """Initialize a message instance.""" - - - @abstractmethod - def decode(self, _data: bytes) -> tuple[int, int, int, bytes]: - """Decode message. - - return: - used_len (int) or 0 to read more - transaction_id (int) or 0 - device_id (int) or 0 - modbus request/response (bytes) - """ - - @abstractmethod - def encode(self, data: bytes, device_id: int, tid: int) -> bytes: - """Decode message. - - return: - modbus message (bytes) - """ diff --git a/pymodbus/message/rtu.py b/pymodbus/message/rtu.py deleted file mode 100644 index 2565fa5a9..000000000 --- a/pymodbus/message/rtu.py +++ /dev/null @@ -1,219 +0,0 @@ -"""ModbusMessage layer. - -is extending ModbusProtocol to handle receiving and sending of messsagees. - -ModbusMessage provides a unified interface to send/receive Modbus requests/responses. -""" -from __future__ import annotations - -import struct - -from pymodbus.exceptions import ModbusIOException -from pymodbus.factory import ClientDecoder -from pymodbus.logging import Log -from pymodbus.message.base import MessageBase - - -class MessageRTU(MessageBase): - """Modbus RTU frame type. - - [ Start Wait ] [Address ][ Function Code] [ Data ][ CRC ][ End Wait ] - 3.5 chars 1b 1b Nb 2b 3.5 chars - - Wait refers to the amount of time required to transmit at least x many - characters. In this case it is 3.5 characters. Also, if we receive a - wait of 1.5 characters at any point, we must trigger an error message. - Also, it appears as though this message is little endian. The logic is - simplified as the following:: - - The following table is a listing of the baud wait times for the specified - baud rates:: - - ------------------------------------------------------------------ - Baud 1.5c (18 bits) 3.5c (38 bits) - ------------------------------------------------------------------ - 1200 15,000 ms 31,667 ms - 4800 3,750 ms 7,917 ms - 9600 1,875 ms 3,958 ms - 19200 0,938 ms 1,979 ms - 38400 0,469 ms 0,989 ms - 115200 0,156 ms 0,329 ms - ------------------------------------------------------------------ - 1 Byte = 8 bits + 1 bit parity + 2 stop bit = 11 bits - - * Note: due to the USB converter and the OS drivers, timing cannot be quaranteed - neither when receiving nor when sending. - """ - - function_codes: list[int] = [] - - @classmethod - def set_legal_function_codes(cls, function_codes: list[int]): - """Set legal function codes.""" - cls.function_codes = function_codes - - @classmethod - def generate_crc16_table(cls) -> list[int]: - """Generate a crc16 lookup table. - - .. note:: This will only be generated once - """ - result = [] - for byte in range(256): - crc = 0x0000 - for _ in range(8): - if (byte ^ crc) & 0x0001: - crc = (crc >> 1) ^ 0xA001 - else: - crc >>= 1 - byte >>= 1 - result.append(crc) - return result - crc16_table: list[int] = [0] - - def _legacy_decode(self, callback, slave): # noqa: C901 - """Process new packet pattern.""" - - def is_frame_ready(self): - """Check if we should continue decode logic.""" - size = self._header.get("len", 0) - if not size and len(self._buffer) > self._hsize: - try: - self._header["uid"] = int(self._buffer[0]) - self._header["tid"] = int(self._buffer[0]) - func_code = int(self._buffer[1]) - pdu_class = self.decoder.lookupPduClass(func_code) - size = pdu_class.calculateRtuFrameSize(self._buffer) - self._header["len"] = size - - if len(self._buffer) < size: - raise IndexError - self._header["crc"] = self._buffer[size - 2 : size] - except IndexError: - return False - return len(self._buffer) >= size if size > 0 else False - - def get_frame_start(self, slaves, broadcast, skip_cur_frame): - """Scan buffer for a relevant frame start.""" - start = 1 if skip_cur_frame else 0 - if (buf_len := len(self._buffer)) < 4: - return False - for i in range(start, buf_len - 3): # - if not broadcast and self._buffer[i] not in slaves: - continue - if ( - self._buffer[i + 1] not in self.function_codes - and (self._buffer[i + 1] - 0x80) not in self.function_codes - ): - continue - if i: - self._buffer = self._buffer[i:] # remove preceding trash. - return True - if buf_len > 3: - self._buffer = self._buffer[-3:] - return False - - def check_frame(self): - """Check if the next frame is available.""" - try: - self._header["uid"] = int(self._buffer[0]) - self._header["tid"] = int(self._buffer[0]) - func_code = int(self._buffer[1]) - pdu_class = self.decoder.lookupPduClass(func_code) - size = pdu_class.calculateRtuFrameSize(self._buffer) - self._header["len"] = size - - if len(self._buffer) < size: - raise IndexError - self._header["crc"] = self._buffer[size - 2 : size] - frame_size = self._header["len"] - data = self._buffer[: frame_size - 2] - crc = self._header["crc"] - crc_val = (int(crc[0]) << 8) + int(crc[1]) - return MessageRTU.check_CRC(data, crc_val) - except (IndexError, KeyError, struct.error): - return False - - self._buffer = b'' # pylint: disable=attribute-defined-outside-init - broadcast = not slave[0] - skip_cur_frame = False - while get_frame_start(self, slave, broadcast, skip_cur_frame): - self._header: dict = {"uid": 0x00, "len": 0, "crc": b"\x00\x00"} # pylint: disable=attribute-defined-outside-init - if not is_frame_ready(self): - Log.debug("Frame - not ready") - break - if not check_frame(self): - Log.debug("Frame check failed, ignoring!!") - # x = self._buffer - # self.resetFrame() - # self._buffer = x - skip_cur_frame = True - continue - start = 0x01 # self._hsize - end = self._header["len"] - 2 - buffer = self._buffer[start:end] - if end > 0: - Log.debug("Getting Frame - {}", buffer, ":hex") - data = buffer - else: - data = b"" - if (result := ClientDecoder().decode(data)) is None: - raise ModbusIOException("Unable to decode request") - result.slave_id = self._header["uid"] - result.transaction_id = self._header["tid"] - self._buffer = self._buffer[self._header["len"] :] # pylint: disable=attribute-defined-outside-init - Log.debug("Frame advanced, resetting header!!") - callback(result) # defer or push to a thread? - - - def decode(self, data: bytes) -> tuple[int, int, int, bytes]: - """Decode message.""" - resp = None - if len(data) < 4: - return 0, 0, 0, b'' - - def callback(result): - """Set result.""" - nonlocal resp - resp = result - - self._legacy_decode(callback, [0]) - return 0, 0, 0, b'' - - - def encode(self, data: bytes, device_id: int, _tid: int) -> bytes: - """Decode message.""" - packet = device_id.to_bytes(1,'big') + data - return packet + MessageRTU.compute_CRC(packet).to_bytes(2,'big') - - @classmethod - def check_CRC(cls, data: bytes, check: int) -> bool: - """Check if the data matches the passed in CRC. - - :param data: The data to create a crc16 of - :param check: The CRC to validate - :returns: True if matched, False otherwise - """ - return cls.compute_CRC(data) == check - - @classmethod - def compute_CRC(cls, data: bytes) -> int: - """Compute a crc16 on the passed in bytes. - - For modbus, this is only used on the binary serial protocols (in this - case RTU). - - The difference between modbus's crc16 and a normal crc16 - is that modbus starts the crc value out at 0xffff. - - :param data: The data to create a crc16 of - :returns: The calculated CRC - """ - crc = 0xFFFF - for data_byte in data: - idx = cls.crc16_table[(crc ^ int(data_byte)) & 0xFF] - crc = ((crc >> 8) & 0xFF) ^ idx - swapped = ((crc << 8) & 0xFF00) | ((crc >> 8) & 0x00FF) - return swapped - -MessageRTU.crc16_table = MessageRTU.generate_crc16_table() diff --git a/pymodbus/message/tls.py b/pymodbus/message/tls.py deleted file mode 100644 index d89e769e1..000000000 --- a/pymodbus/message/tls.py +++ /dev/null @@ -1,25 +0,0 @@ -"""ModbusMessage layer. - -is extending ModbusProtocol to handle receiving and sending of messsagees. - -ModbusMessage provides a unified interface to send/receive Modbus requests/responses. -""" -from __future__ import annotations - -from pymodbus.message.base import MessageBase - - -class MessageTLS(MessageBase): - """Modbus TLS frame type. - - [ Function Code] [ Data ] - 1b Nb - """ - - def decode(self, data: bytes) -> tuple[int, int, int, bytes]: - """Decode message.""" - return len(data), 0, 0, data - - def encode(self, data: bytes, _device_id: int, _tid: int) -> bytes: - """Decode message.""" - return data diff --git a/pymodbus/payload.py b/pymodbus/payload.py index 583aa3b30..3f20ad46d 100644 --- a/pymodbus/payload.py +++ b/pymodbus/payload.py @@ -11,6 +11,8 @@ "BinaryPayloadDecoder", ] +from array import array + # pylint: disable=missing-type-doc from struct import pack, unpack @@ -23,9 +25,6 @@ ) -WC = {"b": 1, "h": 2, "e": 2, "i": 4, "l": 4, "q": 8, "f": 4, "d": 8} - - class BinaryPayloadBuilder: """A utility that helps build payload messages to be written with the various modbus messages. @@ -69,15 +68,14 @@ def _pack_words(self, fstring: str, value) -> bytes: :return: """ value = pack(f"!{fstring}", value) - wordorder = WC.get(fstring.lower()) // 2 # type: ignore[operator] - upperbyte = f"!{wordorder}H" - payload = unpack(upperbyte, value) - - if self._wordorder == Endian.LITTLE: - payload = payload[::-1] - - fstring = self._byteorder + "H" - return b"".join(pack(fstring, word) for word in payload) + if Endian.LITTLE in {self._byteorder, self._wordorder}: + value = array("H", value) + if self._byteorder == Endian.LITTLE: + value.byteswap() + if self._wordorder == Endian.LITTLE: + value.reverse() + value = value.tobytes() + return value def encode(self) -> bytes: """Get the payload buffer encoded in bytes.""" @@ -295,7 +293,7 @@ def fromRegisters( """ Log.debug("{}", registers) if isinstance(registers, list): # repack into flat binary - payload = b"".join(pack("!H", x) for x in registers) + payload = pack(f"!{len(registers)}H", *registers) return cls(payload, byteorder, wordorder) raise ParameterException("Invalid collection of registers supplied") @@ -324,7 +322,7 @@ def fromCoils( return cls(payload, byteorder) raise ParameterException("Invalid collection of coils supplied") - def _unpack_words(self, fstring: str, handle) -> bytes: + def _unpack_words(self, handle) -> bytes: """Unpack words based on the word order and byte order. # ---------------------------------------------- # @@ -336,15 +334,14 @@ def _unpack_words(self, fstring: str, handle) -> bytes: :param handle: Value to be unpacked :return: """ - wc_value = WC.get(fstring.lower()) // 2 # type: ignore[operator] - handle = unpack(f"!{wc_value}H", handle) - if self._wordorder == Endian.LITTLE: - handle = list(reversed(handle)) - - # Repack as unsigned Integer - handle = [pack(self._byteorder + "H", p) for p in handle] + if Endian.LITTLE in {self._byteorder, self._wordorder}: + handle = array("H", handle) + if self._byteorder == Endian.LITTLE: + handle.byteswap() + if self._wordorder == Endian.LITTLE: + handle.reverse() + handle = handle.tobytes() Log.debug("handle: {}", handle) - handle = b"".join(handle) return handle def reset(self): @@ -377,7 +374,7 @@ def decode_32bit_uint(self): self._pointer += 4 fstring = "I" handle = self._payload[self._pointer - 4 : self._pointer] - handle = self._unpack_words(fstring, handle) + handle = self._unpack_words(handle) return unpack("!" + fstring, handle)[0] def decode_64bit_uint(self): @@ -385,7 +382,7 @@ def decode_64bit_uint(self): self._pointer += 8 fstring = "Q" handle = self._payload[self._pointer - 8 : self._pointer] - handle = self._unpack_words(fstring, handle) + handle = self._unpack_words(handle) return unpack("!" + fstring, handle)[0] def decode_8bit_int(self): @@ -407,7 +404,7 @@ def decode_32bit_int(self): self._pointer += 4 fstring = "i" handle = self._payload[self._pointer - 4 : self._pointer] - handle = self._unpack_words(fstring, handle) + handle = self._unpack_words(handle) return unpack("!" + fstring, handle)[0] def decode_64bit_int(self): @@ -415,7 +412,7 @@ def decode_64bit_int(self): self._pointer += 8 fstring = "q" handle = self._payload[self._pointer - 8 : self._pointer] - handle = self._unpack_words(fstring, handle) + handle = self._unpack_words(handle) return unpack("!" + fstring, handle)[0] def decode_16bit_float(self): @@ -423,7 +420,7 @@ def decode_16bit_float(self): self._pointer += 2 fstring = "e" handle = self._payload[self._pointer - 2 : self._pointer] - handle = self._unpack_words(fstring, handle) + handle = self._unpack_words(handle) return unpack("!" + fstring, handle)[0] def decode_32bit_float(self): @@ -431,7 +428,7 @@ def decode_32bit_float(self): self._pointer += 4 fstring = "f" handle = self._payload[self._pointer - 4 : self._pointer] - handle = self._unpack_words(fstring, handle) + handle = self._unpack_words(handle) return unpack("!" + fstring, handle)[0] def decode_64bit_float(self): @@ -439,7 +436,7 @@ def decode_64bit_float(self): self._pointer += 8 fstring = "d" handle = self._payload[self._pointer - 8 : self._pointer] - handle = self._unpack_words(fstring, handle) + handle = self._unpack_words(handle) return unpack("!" + fstring, handle)[0] def decode_string(self, size=1): diff --git a/pymodbus/pdu/__init__.py b/pymodbus/pdu/__init__.py new file mode 100644 index 000000000..da3d86f8f --- /dev/null +++ b/pymodbus/pdu/__init__.py @@ -0,0 +1,18 @@ +"""Framer.""" +__all__ = [ + "ExceptionResponse", + "IllegalFunctionRequest", + "ModbusExceptions", + "ModbusPDU", + "ModbusRequest", + "ModbusResponse", +] + +from pymodbus.pdu.pdu import ( + ExceptionResponse, + IllegalFunctionRequest, + ModbusExceptions, + ModbusPDU, + ModbusRequest, + ModbusResponse, +) diff --git a/pymodbus/bit_read_message.py b/pymodbus/pdu/bit_read_message.py similarity index 89% rename from pymodbus/bit_read_message.py rename to pymodbus/pdu/bit_read_message.py index 6796eeffc..4a116dc42 100644 --- a/pymodbus/bit_read_message.py +++ b/pymodbus/pdu/bit_read_message.py @@ -1,13 +1,5 @@ """Bit Reading Request/Response messages.""" -__all__ = [ - "ReadBitsResponseBase", - "ReadCoilsRequest", - "ReadCoilsResponse", - "ReadDiscreteInputsRequest", - "ReadDiscreteInputsResponse", -] - # pylint: disable=missing-type-doc import struct @@ -21,14 +13,14 @@ class ReadBitsRequestBase(ModbusRequest): _rtu_frame_size = 8 - def __init__(self, address, count, slave=0, **kwargs): + def __init__(self, address, count, slave, transaction, protocol, skip_encode): """Initialize the read request data. :param address: The start address to read from :param count: The number of bits after "address" to read :param slave: Modbus slave slave ID """ - ModbusRequest.__init__(self, slave, **kwargs) + ModbusRequest.__init__(self, slave, transaction, protocol, skip_encode) self.address = address self.count = count @@ -75,13 +67,13 @@ class ReadBitsResponseBase(ModbusResponse): _rtu_byte_count_pos = 2 - def __init__(self, values, slave=0, **kwargs): + def __init__(self, values, slave, transaction, protocol, skip_encode): """Initialize a new instance. :param values: The requested values to be returned :param slave: Modbus slave slave ID """ - ModbusResponse.__init__(self, slave, **kwargs) + ModbusResponse.__init__(self, slave, transaction, protocol, skip_encode) #: A list of booleans representing bit values self.bits = values or [] @@ -146,14 +138,14 @@ class ReadCoilsRequest(ReadBitsRequestBase): function_code = 1 function_code_name = "read_coils" - def __init__(self, address=None, count=None, slave=0, **kwargs): + def __init__(self, address=None, count=None, slave=1, transaction=0, protocol=0, skip_encode=False): """Initialize a new instance. :param address: The address to start reading from :param count: The number of bits to read :param slave: Modbus slave slave ID """ - ReadBitsRequestBase.__init__(self, address, count, slave, **kwargs) + ReadBitsRequestBase.__init__(self, address, count, slave, transaction, protocol, skip_encode) async def execute(self, context): """Run a read coils request against a datastore. @@ -193,13 +185,13 @@ class ReadCoilsResponse(ReadBitsResponseBase): function_code = 1 - def __init__(self, values=None, slave=0, **kwargs): + def __init__(self, values=None, slave=1, transaction=0, protocol=0, skip_encode=False): """Initialize a new instance. :param values: The request values to respond with :param slave: Modbus slave slave ID """ - ReadBitsResponseBase.__init__(self, values, slave, **kwargs) + ReadBitsResponseBase.__init__(self, values, slave, transaction, protocol, skip_encode) class ReadDiscreteInputsRequest(ReadBitsRequestBase): @@ -214,14 +206,14 @@ class ReadDiscreteInputsRequest(ReadBitsRequestBase): function_code = 2 function_code_name = "read_discrete_input" - def __init__(self, address=None, count=None, slave=0, **kwargs): + def __init__(self, address=None, count=None, slave=1, transaction=0, protocol=0, skip_encode=False): """Initialize a new instance. :param address: The address to start reading from :param count: The number of bits to read :param slave: Modbus slave slave ID """ - ReadBitsRequestBase.__init__(self, address, count, slave, **kwargs) + ReadBitsRequestBase.__init__(self, address, count, slave, transaction, protocol, skip_encode) async def execute(self, context): """Run a read discrete input request against a datastore. @@ -261,10 +253,10 @@ class ReadDiscreteInputsResponse(ReadBitsResponseBase): function_code = 2 - def __init__(self, values=None, slave=0, **kwargs): + def __init__(self, values=None, slave=1, transaction=0, protocol=0, skip_encode=False): """Initialize a new instance. :param values: The request values to respond with :param slave: Modbus slave slave ID """ - ReadBitsResponseBase.__init__(self, values, slave, **kwargs) + ReadBitsResponseBase.__init__(self, values, slave, transaction, protocol, skip_encode) diff --git a/pymodbus/bit_write_message.py b/pymodbus/pdu/bit_write_message.py similarity index 92% rename from pymodbus/bit_write_message.py rename to pymodbus/pdu/bit_write_message.py index 6c4fafa27..9b531d92c 100644 --- a/pymodbus/bit_write_message.py +++ b/pymodbus/pdu/bit_write_message.py @@ -3,12 +3,6 @@ TODO write mask request/response """ -__all__ = [ - "WriteSingleCoilRequest", - "WriteSingleCoilResponse", - "WriteMultipleCoilsRequest", - "WriteMultipleCoilsResponse", -] # pylint: disable=missing-type-doc import struct @@ -49,13 +43,13 @@ class WriteSingleCoilRequest(ModbusRequest): _rtu_frame_size = 8 - def __init__(self, address=None, value=None, slave=None, **kwargs): + def __init__(self, address=None, value=None, slave=None, transaction=0, protocol=0, skip_encode=0): """Initialize a new instance. :param address: The variable address to write :param value: The value to write at address """ - ModbusRequest.__init__(self, slave=slave, **kwargs) + ModbusRequest.__init__(self, slave, transaction, protocol, skip_encode) self.address = address self.value = bool(value) @@ -119,13 +113,13 @@ class WriteSingleCoilResponse(ModbusResponse): function_code = 5 _rtu_frame_size = 8 - def __init__(self, address=None, value=None, **kwargs): + def __init__(self, address=None, value=None, slave=1, transaction=0, protocol=0, skip_encode=False): """Initialize a new instance. :param address: The variable address written to :param value: The value written at address """ - ModbusResponse.__init__(self, **kwargs) + ModbusResponse.__init__(self, slave, transaction, protocol, skip_encode) self.address = address self.value = value @@ -173,13 +167,13 @@ class WriteMultipleCoilsRequest(ModbusRequest): function_code_name = "write_coils" _rtu_byte_count_pos = 6 - def __init__(self, address=None, values=None, slave=None, **kwargs): + def __init__(self, address=None, values=None, slave=None, transaction=0, protocol=0, skip_encode=0): """Initialize a new instance. :param address: The starting request address :param values: The values to write """ - ModbusRequest.__init__(self, slave=slave, **kwargs) + ModbusRequest.__init__(self, slave, transaction, protocol, skip_encode) self.address = address if values is None: values = [] @@ -256,13 +250,13 @@ class WriteMultipleCoilsResponse(ModbusResponse): function_code = 15 _rtu_frame_size = 8 - def __init__(self, address=None, count=None, **kwargs): + def __init__(self, address=None, count=None, slave=1, transaction=0, protocol=0, skip_encode=False): """Initialize a new instance. :param address: The starting variable address written to :param count: The number of values written """ - ModbusResponse.__init__(self, **kwargs) + ModbusResponse.__init__(self, slave, transaction, protocol, skip_encode) self.address = address self.count = count diff --git a/pymodbus/diag_message.py b/pymodbus/pdu/diag_message.py similarity index 92% rename from pymodbus/diag_message.py rename to pymodbus/pdu/diag_message.py index e484ecd74..de78c2def 100644 --- a/pymodbus/diag_message.py +++ b/pymodbus/pdu/diag_message.py @@ -4,44 +4,6 @@ or linked to the appropriate data """ -__all__ = [ - "DiagnosticStatusRequest", - "DiagnosticStatusResponse", - "ReturnQueryDataRequest", - "ReturnQueryDataResponse", - "RestartCommunicationsOptionRequest", - "RestartCommunicationsOptionResponse", - "ReturnDiagnosticRegisterRequest", - "ReturnDiagnosticRegisterResponse", - "ChangeAsciiInputDelimiterRequest", - "ChangeAsciiInputDelimiterResponse", - "ForceListenOnlyModeRequest", - "ForceListenOnlyModeResponse", - "ClearCountersRequest", - "ClearCountersResponse", - "ReturnBusMessageCountRequest", - "ReturnBusMessageCountResponse", - "ReturnBusCommunicationErrorCountRequest", - "ReturnBusCommunicationErrorCountResponse", - "ReturnBusExceptionErrorCountRequest", - "ReturnBusExceptionErrorCountResponse", - "ReturnSlaveMessageCountRequest", - "ReturnSlaveMessageCountResponse", - "ReturnSlaveNoResponseCountRequest", - "ReturnSlaveNoResponseCountResponse", - "ReturnSlaveNAKCountRequest", - "ReturnSlaveNAKCountResponse", - "ReturnSlaveBusyCountRequest", - "ReturnSlaveBusyCountResponse", - "ReturnSlaveBusCharacterOverrunCountRequest", - "ReturnSlaveBusCharacterOverrunCountResponse", - "ReturnIopOverrunCountRequest", - "ReturnIopOverrunCountResponse", - "ClearOverrunCountRequest", - "ClearOverrunCountResponse", - "GetClearModbusPlusRequest", - "GetClearModbusPlusResponse", -] # pylint: disable=missing-type-doc import struct @@ -69,9 +31,9 @@ class DiagnosticStatusRequest(ModbusRequest): function_code_name = "diagnostic_status" _rtu_frame_size = 8 - def __init__(self, **kwargs): + def __init__(self, slave=1, transaction=0, protocol=0, skip_encode=False): """Initialize a diagnostic request.""" - ModbusRequest.__init__(self, **kwargs) + ModbusRequest.__init__(self, slave, transaction, protocol, skip_encode) self.message = None def encode(self): @@ -131,9 +93,9 @@ class DiagnosticStatusResponse(ModbusResponse): function_code = 0x08 _rtu_frame_size = 8 - def __init__(self, **kwargs): + def __init__(self, slave=1, transaction=0, protocol=0, skip_encode=False): """Initialize a diagnostic response.""" - ModbusResponse.__init__(self, **kwargs) + ModbusResponse.__init__(self, slave, transaction, protocol, skip_encode) self.message = None def encode(self): @@ -188,7 +150,7 @@ class DiagnosticStatusSimpleRequest(DiagnosticStatusRequest): the execute method """ - def __init__(self, data=0x0000, **kwargs): + def __init__(self, data=0x0000, slave=1, transaction=0, protocol=0, skip_encode=False): """Initialize a simple diagnostic request. The data defaults to 0x0000 if not provided as over half @@ -196,7 +158,7 @@ def __init__(self, data=0x0000, **kwargs): :param data: The data to send along with the request """ - DiagnosticStatusRequest.__init__(self, **kwargs) + DiagnosticStatusRequest.__init__(self, slave=slave, transaction=transaction, protocol=protocol, skip_encode=skip_encode) self.message = data async def execute(self, *args): @@ -213,12 +175,12 @@ class DiagnosticStatusSimpleResponse(DiagnosticStatusResponse): 2 bytes of data. """ - def __init__(self, data=0x0000, **kwargs): + def __init__(self, data=0x0000, slave=1, transaction=0, protocol=0, skip_encode=False): """Return a simple diagnostic response. :param data: The resulting data to return to the client """ - DiagnosticStatusResponse.__init__(self, **kwargs) + DiagnosticStatusResponse.__init__(self, slave=slave, transaction=transaction, protocol=protocol, skip_encode=skip_encode) self.message = data @@ -235,12 +197,12 @@ class ReturnQueryDataRequest(DiagnosticStatusRequest): sub_function_code = 0x0000 - def __init__(self, message=b"\x00\x00", slave=None, **kwargs): + def __init__(self, message=b"\x00\x00", slave=1, transaction=0, protocol=0, skip_encode=False): """Initialize a new instance of the request. :param message: The message to send to loopback """ - DiagnosticStatusRequest.__init__(self, slave=slave, **kwargs) + DiagnosticStatusRequest.__init__(self, slave=slave, transaction=transaction, protocol=protocol, skip_encode=skip_encode) if not isinstance(message, bytes): raise ModbusException(f"message({type(message)}) must be bytes") self.message = message @@ -263,12 +225,12 @@ class ReturnQueryDataResponse(DiagnosticStatusResponse): sub_function_code = 0x0000 - def __init__(self, message=b"\x00\x00", **kwargs): + def __init__(self, message=b"\x00\x00", slave=1, transaction=0, protocol=0, skip_encode=False): """Initialize a new instance of the response. :param message: The message to loopback """ - DiagnosticStatusResponse.__init__(self, **kwargs) + DiagnosticStatusResponse.__init__(self, slave=slave, transaction=transaction, protocol=protocol, skip_encode=skip_encode) if not isinstance(message, bytes): raise ModbusException(f"message({type(message)}) must be bytes") self.message = message @@ -290,12 +252,12 @@ class RestartCommunicationsOptionRequest(DiagnosticStatusRequest): sub_function_code = 0x0001 - def __init__(self, toggle=False, slave=None, **kwargs): + def __init__(self, toggle=False, slave=1, transaction=0, protocol=0, skip_encode=False): """Initialize a new request. :param toggle: Set to True to toggle, False otherwise """ - DiagnosticStatusRequest.__init__(self, slave=slave, **kwargs) + DiagnosticStatusRequest.__init__(self, slave=slave, transaction=transaction, protocol=protocol, skip_encode=skip_encode) if toggle: self.message = [ModbusStatus.ON] else: @@ -323,12 +285,12 @@ class RestartCommunicationsOptionResponse(DiagnosticStatusResponse): sub_function_code = 0x0001 - def __init__(self, toggle=False, **kwargs): + def __init__(self, toggle=False, slave=1, transaction=0, protocol=0, skip_encode=False): """Initialize a new response. :param toggle: Set to True if we toggled, False otherwise """ - DiagnosticStatusResponse.__init__(self, **kwargs) + DiagnosticStatusResponse.__init__(self, slave=slave, transaction=transaction, protocol=protocol, skip_encode=skip_encode) if toggle: self.message = [ModbusStatus.ON] else: @@ -434,9 +396,9 @@ class ForceListenOnlyModeResponse(DiagnosticStatusResponse): sub_function_code = 0x0004 should_respond = False - def __init__(self, **kwargs): + def __init__(self, slave=1, transaction=0, protocol=0, skip_encode=False): """Initialize to block a return response.""" - DiagnosticStatusResponse.__init__(self, **kwargs) + DiagnosticStatusResponse.__init__(self, slave=slave, transaction=transaction, protocol=protocol, skip_encode=skip_encode) self.message = [] @@ -816,9 +778,10 @@ class GetClearModbusPlusRequest(DiagnosticStatusSimpleRequest): sub_function_code = 0x0015 - def __init__(self, slave=None, **kwargs): + def __init__(self, data=0, slave=1, transaction=0, protocol=0, skip_encode=False): """Initialize.""" - super().__init__(slave=slave, **kwargs) + super().__init__(slave=slave, transaction=transaction, protocol=protocol, skip_encode=skip_encode) + self.message=data def get_response_pdu_size(self): """Return a series of 54 16-bit words (108 bytes) in the data field of the response. diff --git a/pymodbus/file_message.py b/pymodbus/pdu/file_message.py similarity index 89% rename from pymodbus/file_message.py rename to pymodbus/pdu/file_message.py index e08e8f99d..37adbd5b6 100644 --- a/pymodbus/file_message.py +++ b/pymodbus/pdu/file_message.py @@ -4,17 +4,6 @@ """ from __future__ import annotations - -__all__ = [ - "FileRecord", - "ReadFileRecordRequest", - "ReadFileRecordResponse", - "WriteFileRecordRequest", - "WriteFileRecordResponse", - "ReadFifoQueueRequest", - "ReadFifoQueueResponse", -] - # pylint: disable=missing-type-doc import struct @@ -28,7 +17,7 @@ class FileRecord: # pylint: disable=eq-without-hash """Represents a file record and its relevant data.""" - def __init__(self, **kwargs): + def __init__(self, reference_type=0x06, file_number=0x00, record_number=0x00, record_data="", record_length=None, response_length=None): """Initialize a new instance. :params reference_type: must be 0x06 @@ -38,13 +27,13 @@ def __init__(self, **kwargs): :params record_length: The length in registers of the record :params response_length: The length in bytes of the record """ - self.reference_type = kwargs.get("reference_type", 0x06) - self.file_number = kwargs.get("file_number", 0x00) - self.record_number = kwargs.get("record_number", 0x00) - self.record_data = kwargs.get("record_data", "") + self.reference_type = reference_type + self.file_number = file_number + self.record_number = record_number + self.record_data = record_data - self.record_length = kwargs.get("record_length", len(self.record_data) // 2) - self.response_length = kwargs.get("response_length", len(self.record_data) + 1) + self.record_length = record_length if record_length else len(self.record_data) // 2 + self.response_length = response_length if response_length else len(self.record_data) + 1 def __eq__(self, relf): """Compare the left object to the right.""" @@ -100,12 +89,12 @@ class ReadFileRecordRequest(ModbusRequest): function_code_name = "read_file_record" _rtu_byte_count_pos = 2 - def __init__(self, records=None, **kwargs): + def __init__(self, records=None, slave=1, transaction=0, protocol=0, skip_encode=False): """Initialize a new instance. :param records: The file record requests to be read """ - ModbusRequest.__init__(self, **kwargs) + ModbusRequest.__init__(self, slave, transaction, protocol, skip_encode) self.records = records or [] def encode(self): @@ -165,12 +154,12 @@ class ReadFileRecordResponse(ModbusResponse): function_code = 0x14 _rtu_byte_count_pos = 2 - def __init__(self, records=None, **kwargs): + def __init__(self, records=None, slave=1, transaction=0, protocol=0, skip_encode=False): """Initialize a new instance. :param records: The requested file records """ - ModbusResponse.__init__(self, **kwargs) + ModbusResponse.__init__(self, slave, transaction, protocol, skip_encode) self.records = records or [] def encode(self): @@ -218,12 +207,12 @@ class WriteFileRecordRequest(ModbusRequest): function_code_name = "write_file_record" _rtu_byte_count_pos = 2 - def __init__(self, records=None, **kwargs): + def __init__(self, records=None, slave=1, transaction=0, protocol=0, skip_encode=False): """Initialize a new instance. :param records: The file record requests to be read """ - ModbusRequest.__init__(self, **kwargs) + ModbusRequest.__init__(self, slave, transaction, protocol, skip_encode) self.records = records or [] def encode(self): @@ -282,12 +271,12 @@ class WriteFileRecordResponse(ModbusResponse): function_code = 0x15 _rtu_byte_count_pos = 2 - def __init__(self, records=None, **kwargs): + def __init__(self, records=None, slave=1, transaction=0, protocol=0, skip_encode=False): """Initialize a new instance. :param records: The file record requests to be read """ - ModbusResponse.__init__(self, **kwargs) + ModbusResponse.__init__(self, slave, transaction, protocol, skip_encode) self.records = records or [] def encode(self): @@ -347,12 +336,12 @@ class ReadFifoQueueRequest(ModbusRequest): function_code_name = "read_fifo_queue" _rtu_frame_size = 6 - def __init__(self, address=0x0000, **kwargs): + def __init__(self, address=0x0000, slave=1, transaction=0, protocol=0, skip_encode=False): """Initialize a new instance. :param address: The fifo pointer address (0x0000 to 0xffff) """ - ModbusRequest.__init__(self, **kwargs) + ModbusRequest.__init__(self, slave, transaction, protocol, skip_encode) self.address = address self.values = [] # this should be added to the context @@ -408,12 +397,12 @@ def calculateRtuFrameSize(cls, buffer): lo_byte = int(buffer[3]) return (hi_byte << 16) + lo_byte + 6 - def __init__(self, values=None, **kwargs): + def __init__(self, values=None, slave=1, transaction=0, protocol=0, skip_encode=False): """Initialize a new instance. :param values: The list of values of the fifo to return """ - ModbusResponse.__init__(self, **kwargs) + ModbusResponse.__init__(self, slave, transaction, protocol, skip_encode) self.values = values or [] def encode(self): diff --git a/pymodbus/mei_message.py b/pymodbus/pdu/mei_message.py similarity index 95% rename from pymodbus/mei_message.py rename to pymodbus/pdu/mei_message.py index 4eb2e596f..71396bbc8 100644 --- a/pymodbus/mei_message.py +++ b/pymodbus/pdu/mei_message.py @@ -1,9 +1,5 @@ """Encapsulated Interface (MEI) Transport Messages.""" -__all__ = [ - "ReadDeviceInformationRequest", - "ReadDeviceInformationResponse", -] # pylint: disable=missing-type-doc import struct @@ -56,13 +52,13 @@ class ReadDeviceInformationRequest(ModbusRequest): function_code_name = "read_device_information" _rtu_frame_size = 7 - def __init__(self, read_code=None, object_id=0x00, **kwargs): + def __init__(self, read_code=None, object_id=0x00, slave=1, transaction=0, protocol=0, skip_encode=False): """Initialize a new instance. :param read_code: The device information read code :param object_id: The object to read from """ - ModbusRequest.__init__(self, **kwargs) + ModbusRequest.__init__(self, slave, transaction, protocol, skip_encode) self.read_code = read_code or DeviceInformation.BASIC self.object_id = object_id @@ -134,13 +130,13 @@ def calculateRtuFrameSize(cls, buffer): except struct.error as exc: raise IndexError from exc - def __init__(self, read_code=None, information=None, **kwargs): + def __init__(self, read_code=None, information=None, slave=1, transaction=0, protocol=0, skip_encode=False): """Initialize a new instance. :param read_code: The device information read code :param information: The requested information request """ - ModbusResponse.__init__(self, **kwargs) + ModbusResponse.__init__(self, slave, transaction, protocol, skip_encode) self.read_code = read_code or DeviceInformation.BASIC self.information = information or {} self.number_of_objects = 0 diff --git a/pymodbus/other_message.py b/pymodbus/pdu/other_message.py similarity index 90% rename from pymodbus/other_message.py rename to pymodbus/pdu/other_message.py index ec3e35110..408fb0042 100644 --- a/pymodbus/other_message.py +++ b/pymodbus/pdu/other_message.py @@ -3,16 +3,6 @@ Currently not all implemented """ -__all__ = [ - "ReadExceptionStatusRequest", - "ReadExceptionStatusResponse", - "GetCommEventCounterRequest", - "GetCommEventCounterResponse", - "GetCommEventLogRequest", - "GetCommEventLogResponse", - "ReportSlaveIdRequest", - "ReportSlaveIdResponse", -] # pylint: disable=missing-type-doc import struct @@ -40,9 +30,9 @@ class ReadExceptionStatusRequest(ModbusRequest): function_code_name = "read_exception_status" _rtu_frame_size = 4 - def __init__(self, slave=None, **kwargs): + def __init__(self, slave=None, transaction=0, protocol=0, skip_encode=0): """Initialize a new instance.""" - ModbusRequest.__init__(self, slave=slave, **kwargs) + ModbusRequest.__init__(self, slave, transaction, protocol, skip_encode) def encode(self): """Encode the message.""" @@ -82,12 +72,12 @@ class ReadExceptionStatusResponse(ModbusResponse): function_code = 0x07 _rtu_frame_size = 5 - def __init__(self, status=0x00, **kwargs): + def __init__(self, status=0x00, slave=1, transaction=0, protocol=0, skip_encode=False): """Initialize a new instance. :param status: The status response to report """ - ModbusResponse.__init__(self, **kwargs) + ModbusResponse.__init__(self, slave, transaction, protocol, skip_encode) self.status = status if status < 256 else 255 def encode(self): @@ -145,9 +135,9 @@ class GetCommEventCounterRequest(ModbusRequest): function_code_name = "get_event_counter" _rtu_frame_size = 4 - def __init__(self, **kwargs): + def __init__(self, slave=1, transaction=0, protocol=0, skip_encode=False): """Initialize a new instance.""" - ModbusRequest.__init__(self, **kwargs) + ModbusRequest.__init__(self, slave, transaction, protocol, skip_encode) def encode(self): """Encode the message.""" @@ -188,12 +178,12 @@ class GetCommEventCounterResponse(ModbusResponse): function_code = 0x0B _rtu_frame_size = 8 - def __init__(self, count=0x0000, **kwargs): + def __init__(self, count=0x0000, slave=1, transaction=0, protocol=0, skip_encode=False): """Initialize a new instance. :param count: The current event counter value """ - ModbusResponse.__init__(self, **kwargs) + ModbusResponse.__init__(self, slave, transaction, protocol, skip_encode) self.count = count self.status = True # this means we are ready, not waiting @@ -256,9 +246,9 @@ class GetCommEventLogRequest(ModbusRequest): function_code_name = "get_event_log" _rtu_frame_size = 4 - def __init__(self, **kwargs): + def __init__(self, slave=1, transaction=0, protocol=0, skip_encode=False): """Initialize a new instance.""" - ModbusRequest.__init__(self, **kwargs) + ModbusRequest.__init__(self, slave, transaction, protocol, skip_encode) def encode(self): """Encode the message.""" @@ -303,7 +293,7 @@ class GetCommEventLogResponse(ModbusResponse): function_code = 0x0C _rtu_byte_count_pos = 2 - def __init__(self, **kwargs): + def __init__(self, status=True, message_count=0, event_count=0, events=None, slave=1, transaction=0, protocol=0, skip_encode=False): """Initialize a new instance. :param status: The status response to report @@ -311,11 +301,11 @@ def __init__(self, **kwargs): :param event_count: The current event count :param events: The collection of events to send """ - ModbusResponse.__init__(self, **kwargs) - self.status = kwargs.get("status", True) - self.message_count = kwargs.get("message_count", 0) - self.event_count = kwargs.get("event_count", 0) - self.events = kwargs.get("events", []) + ModbusResponse.__init__(self, slave, transaction, protocol, skip_encode) + self.status = status + self.message_count = message_count + self.event_count = event_count + self.events = events if events else [] def encode(self): """Encode the response. @@ -377,13 +367,13 @@ class ReportSlaveIdRequest(ModbusRequest): function_code_name = "report_slave_id" _rtu_frame_size = 4 - def __init__(self, slave=0, **kwargs): + def __init__(self, slave=1, transaction=0, protocol=0, skip_encode=False): """Initialize a new instance. :param slave: Modbus slave slave ID """ - ModbusRequest.__init__(self, slave, **kwargs) + ModbusRequest.__init__(self, slave, transaction, protocol, skip_encode) def encode(self): """Encode the message.""" @@ -436,13 +426,13 @@ class ReportSlaveIdResponse(ModbusResponse): function_code = 0x11 _rtu_byte_count_pos = 2 - def __init__(self, identifier=b"\x00", status=True, **kwargs): + def __init__(self, identifier=b"\x00", status=True, slave=1, transaction=0, protocol=0, skip_encode=False): """Initialize a new instance. :param identifier: The identifier of the slave :param status: The status response to report """ - ModbusResponse.__init__(self, **kwargs) + ModbusResponse.__init__(self, slave, transaction, protocol, skip_encode) self.identifier = identifier self.status = status self.byte_count = None diff --git a/pymodbus/pdu.py b/pymodbus/pdu/pdu.py similarity index 89% rename from pymodbus/pdu.py rename to pymodbus/pdu/pdu.py index c50cf281a..67f486839 100644 --- a/pymodbus/pdu.py +++ b/pymodbus/pdu/pdu.py @@ -1,12 +1,5 @@ """Contains base classes for modbus request/response/error packets.""" -__all__ = [ - "ModbusRequest", - "ModbusResponse", - "ModbusExceptions", - "ExceptionResponse", - "IllegalFunctionRequest", -] # pylint: disable=missing-type-doc import struct @@ -53,16 +46,16 @@ class ModbusPDU: of encoding it again. """ - def __init__(self, slave=0, **kwargs): + def __init__(self, slave, transaction, protocol, skip_encode): """Initialize the base data for a modbus request. :param slave: Modbus slave slave ID """ - self.transaction_id = kwargs.get("transaction", 0) - self.protocol_id = kwargs.get("protocol", 0) + self.transaction_id = transaction + self.protocol_id = protocol self.slave_id = slave - self.skip_encode = kwargs.get("skip_encode", False) + self.skip_encode = skip_encode self.check = 0x0000 def encode(self): @@ -102,12 +95,13 @@ class ModbusRequest(ModbusPDU): function_code = -1 - def __init__(self, slave=0, **kwargs): # pylint: disable=useless-parent-delegation + def __init__(self, slave, transaction, protocol, skip_encode): """Proxy to the lower level initializer. :param slave: Modbus slave slave ID """ - super().__init__(slave, **kwargs) + super().__init__(slave, transaction, protocol, skip_encode) + self.fut = None def doException(self, exception): """Build an error response based on the function. @@ -137,15 +131,16 @@ class ModbusResponse(ModbusPDU): should_respond = True function_code = 0x00 - def __init__(self, slave=0, **kwargs): + def __init__(self, slave, transaction, protocol, skip_encode): """Proxy the lower level initializer. :param slave: Modbus slave slave ID """ - super().__init__(slave, **kwargs) + super().__init__(slave, transaction, protocol, skip_encode) self.bits = [] self.registers = [] + self.request = None def isError(self) -> bool: """Check if the error is a success or failure.""" @@ -189,13 +184,13 @@ class ExceptionResponse(ModbusResponse): ExceptionOffset = 0x80 _rtu_frame_size = 5 - def __init__(self, function_code, exception_code=None, **kwargs): + def __init__(self, function_code, exception_code=None, slave=1, transaction=0, protocol=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__(**kwargs) + super().__init__(slave, transaction, protocol, skip_encode) self.original_code = function_code self.function_code = function_code | self.ExceptionOffset self.exception_code = exception_code @@ -238,12 +233,12 @@ class IllegalFunctionRequest(ModbusRequest): ErrorCode = 1 - def __init__(self, function_code, **kwargs): + def __init__(self, function_code, xslave, xtransaction, xprotocol, xskip_encode): """Initialize a IllegalFunctionRequest. :param function_code: The function we are erroring on """ - super().__init__(**kwargs) + super().__init__(xslave, xtransaction, xprotocol, xskip_encode) self.function_code = function_code def decode(self, _data): diff --git a/pymodbus/register_read_message.py b/pymodbus/pdu/register_read_message.py similarity index 83% rename from pymodbus/register_read_message.py rename to pymodbus/pdu/register_read_message.py index 9088f501e..ee1fd0e1a 100644 --- a/pymodbus/register_read_message.py +++ b/pymodbus/pdu/register_read_message.py @@ -1,18 +1,10 @@ """Register Reading Request/Response.""" -__all__ = [ - "ReadHoldingRegistersRequest", - "ReadHoldingRegistersResponse", - "ReadInputRegistersRequest", - "ReadInputRegistersResponse", - "ReadRegistersResponseBase", - "ReadWriteMultipleRegistersRequest", - "ReadWriteMultipleRegistersResponse", -] # pylint: disable=missing-type-doc import struct +from pymodbus.exceptions import ModbusIOException from pymodbus.pdu import ModbusExceptions as merror from pymodbus.pdu import ModbusRequest, ModbusResponse @@ -22,14 +14,14 @@ class ReadRegistersRequestBase(ModbusRequest): _rtu_frame_size = 8 - def __init__(self, address, count, slave=0, **kwargs): + def __init__(self, address, count, slave=1, transaction=0, protocol=0, skip_encode=False): """Initialize a new instance. :param address: The address to start the read from :param count: The number of registers to read :param slave: Modbus slave slave ID """ - super().__init__(slave, **kwargs) + super().__init__(slave, transaction, protocol, skip_encode) self.address = address self.count = count @@ -70,13 +62,13 @@ class ReadRegistersResponseBase(ModbusResponse): _rtu_byte_count_pos = 2 - def __init__(self, values, slave=0, **kwargs): + def __init__(self, values, slave=1, transaction=0, protocol=0, skip_encode=False): """Initialize a new instance. :param values: The values to write to :param slave: Modbus slave slave ID """ - super().__init__(slave, **kwargs) + super().__init__(slave, transaction, protocol, skip_encode) #: A list of register values self.registers = values or [] @@ -97,6 +89,8 @@ def decode(self, data): :param data: The request to decode """ byte_count = int(data[0]) + if byte_count < 2 or byte_count > 246 or byte_count % 2 == 1 or byte_count != len(data) - 1: + raise ModbusIOException(f"Invalid response {data} has byte count of {byte_count}") self.registers = [] for i in range(1, byte_count + 1, 2): self.registers.append(struct.unpack(">H", data[i : i + 2])[0]) @@ -130,14 +124,14 @@ class ReadHoldingRegistersRequest(ReadRegistersRequestBase): function_code = 3 function_code_name = "read_holding_registers" - def __init__(self, address=None, count=None, slave=0, **kwargs): + def __init__(self, address=None, count=None, slave=1, transaction=0, protocol=0, skip_encode=0): """Initialize a new instance of the request. :param address: The starting address to read from :param count: The number of registers to read from address :param slave: Modbus slave slave ID """ - super().__init__(address, count, slave, **kwargs) + super().__init__(address, count, slave, transaction, protocol, skip_encode) async def execute(self, context): """Run a read holding request against a datastore. @@ -169,12 +163,12 @@ class ReadHoldingRegistersResponse(ReadRegistersResponseBase): function_code = 3 - def __init__(self, values=None, **kwargs): + def __init__(self, values=None, slave=None, transaction=0, protocol=0, skip_encode=0): """Initialize a new response instance. :param values: The resulting register values """ - super().__init__(values, **kwargs) + super().__init__(values, slave, transaction, protocol, skip_encode) class ReadInputRegistersRequest(ReadRegistersRequestBase): @@ -190,14 +184,14 @@ class ReadInputRegistersRequest(ReadRegistersRequestBase): function_code = 4 function_code_name = "read_input_registers" - def __init__(self, address=None, count=None, slave=0, **kwargs): + def __init__(self, address=None, count=None, slave=1, transaction=0, protocol=0, skip_encode=0): """Initialize a new instance of the request. :param address: The starting address to read from :param count: The number of registers to read from address :param slave: Modbus slave slave ID """ - super().__init__(address, count, slave, **kwargs) + super().__init__(address, count, slave, transaction, protocol, skip_encode) async def execute(self, context): """Run a read input request against a datastore. @@ -229,12 +223,12 @@ class ReadInputRegistersResponse(ReadRegistersResponseBase): function_code = 4 - def __init__(self, values=None, **kwargs): + def __init__(self, values=None, slave=None, transaction=0, protocol=0, skip_encode=0): """Initialize a new response instance. :param values: The resulting register values """ - super().__init__(values, **kwargs) + super().__init__(values, slave, transaction, protocol, skip_encode) class ReadWriteMultipleRegistersRequest(ModbusRequest): @@ -257,7 +251,7 @@ class ReadWriteMultipleRegistersRequest(ModbusRequest): function_code_name = "read_write_multiple_registers" _rtu_byte_count_pos = 10 - def __init__(self, **kwargs): + def __init__(self, read_address=0x00, read_count=0, write_address=0x00, write_registers=None, slave=1, transaction=0, protocol=0, skip_encode=False): """Initialize a new request message. :param read_address: The address to start reading from @@ -265,11 +259,11 @@ def __init__(self, **kwargs): :param write_address: The address to start writing to :param write_registers: The registers to write to the specified address """ - super().__init__(**kwargs) - self.read_address = kwargs.get("read_address", 0x00) - self.read_count = kwargs.get("read_count", 0) - self.write_address = kwargs.get("write_address", 0x00) - self.write_registers = kwargs.get("write_registers", None) + super().__init__(slave, transaction, protocol, skip_encode) + self.read_address = read_address + self.read_count = read_count + self.write_address = write_address + self.write_registers = write_registers if not hasattr(self.write_registers, "__iter__"): self.write_registers = [self.write_registers] self.write_count = len(self.write_registers) @@ -360,7 +354,7 @@ def __str__(self): ) -class ReadWriteMultipleRegistersResponse(ModbusResponse): +class ReadWriteMultipleRegistersResponse(ReadHoldingRegistersResponse): """Read/write multiple registers. The normal response contains the data from the group of registers that @@ -371,38 +365,3 @@ class ReadWriteMultipleRegistersResponse(ModbusResponse): """ function_code = 23 - _rtu_byte_count_pos = 2 - - def __init__(self, values=None, **kwargs): - """Initialize a new instance. - - :param values: The register values to write - """ - super().__init__(**kwargs) - self.registers = values or [] - - def encode(self): - """Encode the response packet. - - :returns: The encoded packet - """ - result = struct.pack(">B", len(self.registers) * 2) - for register in self.registers: - result += struct.pack(">H", register) - return result - - def decode(self, data): - """Decode the register response packet. - - :param data: The response to decode - """ - bytecount = int(data[0]) - for i in range(1, bytecount, 2): - self.registers.append(struct.unpack(">H", data[i : i + 2])[0]) - - def __str__(self): - """Return a string representation of the instance. - - :returns: A string representation of the instance - """ - return f"ReadWriteNRegisterResponse ({len(self.registers)})" diff --git a/pymodbus/register_write_message.py b/pymodbus/pdu/register_write_message.py similarity index 92% rename from pymodbus/register_write_message.py rename to pymodbus/pdu/register_write_message.py index 753c2ce62..5ee9a1c40 100644 --- a/pymodbus/register_write_message.py +++ b/pymodbus/pdu/register_write_message.py @@ -1,13 +1,5 @@ """Register Writing Request/Response Messages.""" -__all__ = [ - "WriteSingleRegisterRequest", - "WriteSingleRegisterResponse", - "WriteMultipleRegistersRequest", - "WriteMultipleRegistersResponse", - "MaskWriteRegisterRequest", - "MaskWriteRegisterResponse", -] # pylint: disable=missing-type-doc import struct @@ -28,13 +20,13 @@ class WriteSingleRegisterRequest(ModbusRequest): function_code_name = "write_register" _rtu_frame_size = 8 - def __init__(self, address=None, value=None, slave=None, **kwargs): + def __init__(self, address=None, value=None, slave=None, transaction=0, protocol=0, skip_encode=0): """Initialize a new instance. :param address: The address to start writing add :param value: The values to write """ - super().__init__(slave=slave, **kwargs) + super().__init__(slave, transaction, protocol, skip_encode) self.address = address self.value = value @@ -99,13 +91,13 @@ class WriteSingleRegisterResponse(ModbusResponse): function_code = 6 _rtu_frame_size = 8 - def __init__(self, address=None, value=None, **kwargs): + def __init__(self, address=None, value=None, slave=1, transaction=0, protocol=0, skip_encode=False): """Initialize a new instance. :param address: The address to start writing add :param value: The values to write """ - super().__init__(**kwargs) + super().__init__(slave, transaction, protocol, skip_encode) self.address = address self.value = value @@ -160,13 +152,13 @@ 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, **kwargs): + def __init__(self, address=None, values=None, slave=None, transaction=0, protocol=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=slave, **kwargs) + super().__init__(slave, transaction, protocol, skip_encode) self.address = address if values is None: values = [] @@ -247,13 +239,13 @@ class WriteMultipleRegistersResponse(ModbusResponse): function_code = 16 _rtu_frame_size = 8 - def __init__(self, address=None, count=None, **kwargs): + def __init__(self, address=None, count=None, slave=1, transaction=0, protocol=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__(**kwargs) + super().__init__(slave, transaction, protocol, skip_encode) self.address = address self.count = count @@ -295,14 +287,14 @@ class MaskWriteRegisterRequest(ModbusRequest): function_code_name = "mask_write_register" _rtu_frame_size = 10 - def __init__(self, address=0x0000, and_mask=0xFFFF, or_mask=0x0000, **kwargs): + def __init__(self, address=0x0000, and_mask=0xFFFF, or_mask=0x0000, slave=1, transaction=0, protocol=0, skip_encode=False): """Initialize a new instance. :param address: The mask pointer address (0x0000 to 0xffff) :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__(**kwargs) + super().__init__(slave, transaction, protocol, skip_encode) self.address = address self.and_mask = and_mask self.or_mask = or_mask @@ -350,14 +342,14 @@ class MaskWriteRegisterResponse(ModbusResponse): function_code = 0x16 _rtu_frame_size = 10 - def __init__(self, address=0x0000, and_mask=0xFFFF, or_mask=0x0000, **kwargs): + def __init__(self, address=0x0000, and_mask=0xFFFF, or_mask=0x0000, slave=1, transaction=0, protocol=0, skip_encode=False): """Initialize new instance. :param address: The mask pointer address (0x0000 to 0xffff) :param and_mask: The and bitmask applied to the register address :param or_mask: The or bitmask applied to the register address """ - super().__init__(**kwargs) + super().__init__(slave, transaction, protocol, 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 94b850db7..d0c511bf4 100644 --- a/pymodbus/server/async_io.py +++ b/pymodbus/server/async_io.py @@ -11,7 +11,7 @@ from pymodbus.device import ModbusControlBlock, ModbusDeviceIdentification from pymodbus.exceptions import NoSuchSlaveException from pymodbus.factory import ServerDecoder -from pymodbus.framer import FRAMER_NAME_TO_CLASS, Framer, ModbusFramer +from pymodbus.framer import FRAMER_NAME_TO_CLASS, FramerType, ModbusFramer from pymodbus.logging import Log from pymodbus.pdu import ModbusExceptions as merror from pymodbus.transport import CommParams, CommType, ModbusProtocol @@ -321,7 +321,7 @@ class ModbusTcpServer(ModbusBaseServer): def __init__( self, context, - framer=Framer.SOCKET, + framer=FramerType.SOCKET, identity=None, address=("", 502), ignore_missing_slaves=False, @@ -381,7 +381,7 @@ class ModbusTlsServer(ModbusTcpServer): def __init__( # pylint: disable=too-many-arguments self, context, - framer=Framer.TLS, + framer=FramerType.TLS, identity=None, address=("", 502), sslctx=None, @@ -447,7 +447,7 @@ class ModbusUdpServer(ModbusBaseServer): def __init__( self, context, - framer=Framer.SOCKET, + framer=FramerType.SOCKET, identity=None, address=("", 502), ignore_missing_slaves=False, @@ -501,7 +501,7 @@ class ModbusSerialServer(ModbusBaseServer): """ def __init__( - self, context, framer=Framer.RTU, identity=None, **kwargs + self, context, framer=FramerType.RTU, identity=None, **kwargs ): """Initialize the socket server. @@ -616,7 +616,7 @@ async def StartAsyncTcpServer( # pylint: disable=invalid-name,dangerous-default """ kwargs.pop("host", None) server = ModbusTcpServer( - context, kwargs.pop("framer", Framer.SOCKET), identity, address, **kwargs + context, kwargs.pop("framer", FramerType.SOCKET), identity, address, **kwargs ) await _serverList.run(server, custom_functions) @@ -648,7 +648,7 @@ async def StartAsyncTlsServer( # pylint: disable=invalid-name,dangerous-default kwargs.pop("host", None) server = ModbusTlsServer( context, - kwargs.pop("framer", Framer.TLS), + kwargs.pop("framer", FramerType.TLS), identity, address, sslctx, @@ -678,7 +678,7 @@ async def StartAsyncUdpServer( # pylint: disable=invalid-name,dangerous-default """ kwargs.pop("host", None) server = ModbusUdpServer( - context, kwargs.pop("framer", Framer.SOCKET), identity, address, **kwargs + context, kwargs.pop("framer", FramerType.SOCKET), identity, address, **kwargs ) await _serverList.run(server, custom_functions) @@ -698,7 +698,7 @@ async def StartAsyncSerialServer( # pylint: disable=invalid-name,dangerous-defa :param kwargs: The rest """ server = ModbusSerialServer( - context, kwargs.pop("framer", Framer.RTU), identity=identity, **kwargs + context, kwargs.pop("framer", FramerType.RTU), identity=identity, **kwargs ) await _serverList.run(server, custom_functions) diff --git a/pymodbus/server/simulator/http_server.py b/pymodbus/server/simulator/http_server.py index 4766b73a9..39cc843c2 100644 --- a/pymodbus/server/simulator/http_server.py +++ b/pymodbus/server/simulator/http_server.py @@ -154,7 +154,14 @@ def __init__( self.datastore_context = ModbusSimulatorContext( device, custom_actions_dict or {} ) - datastore = ModbusServerContext(slaves=self.datastore_context, single=True) + datastore = None + if "device_id" in server: + # Designated ModBus unit address. Will only serve data if the address matches + datastore = ModbusServerContext(slaves={int(server["device_id"]): self.datastore_context}, single=False) + else: + # Will server any request regardless of addressing + datastore = ModbusServerContext(slaves=self.datastore_context, single=True) + comm = comm_class[server.pop("comm")] framer = server.pop("framer") if "identity" in server: diff --git a/pymodbus/server/simulator/setup.json b/pymodbus/server/simulator/setup.json index ea3213b62..94c63f4fb 100644 --- a/pymodbus/server/simulator/setup.json +++ b/pymodbus/server/simulator/setup.json @@ -171,12 +171,12 @@ {"addr": 2305, "value": 50, "action": "increment", - "kwargs": {"minval": 45, "maxval": 155} + "parameters": {"minval": 45, "maxval": 155} }, {"addr": 2306, "value": 50, "action": "random", - "kwargs": {"minval": 45, "maxval": 55} + "parameters": {"minval": 45, "maxval": 55} } ], "uint32": [ @@ -190,12 +190,12 @@ {"addr": [3876, 3877], "value": 50000, "action": "increment", - "kwargs": {"minval": 45000, "maxval": 55000} + "parameters": {"minval": 45000, "maxval": 55000} }, {"addr": [3878, 3879], "value": 50000, "action": "random", - "kwargs": {"minval": 45000, "maxval": 55000} + "parameters": {"minval": 45000, "maxval": 55000} } ], "float32": [ @@ -209,12 +209,12 @@ {"addr": [4876, 4877], "value": 50000.0, "action": "increment", - "kwargs": {"minval": 45000.0, "maxval": 55000.0} + "parameters": {"minval": 45000.0, "maxval": 55000.0} }, {"addr": [4878, 48779], "value": 50000.0, "action": "random", - "kwargs": {"minval": 45000.0, "maxval": 55000.0} + "parameters": {"minval": 45000.0, "maxval": 55000.0} } ], "string": [ diff --git a/pymodbus/transaction.py b/pymodbus/transaction.py index ea74b3658..ce8048604 100644 --- a/pymodbus/transaction.py +++ b/pymodbus/transaction.py @@ -1,42 +1,123 @@ """Collection of transaction based abstractions.""" +from __future__ import annotations + __all__ = [ - "DictTransactionManager", "ModbusTransactionManager", "ModbusSocketFramer", "ModbusTlsFramer", "ModbusRtuFramer", "ModbusAsciiFramer", - "ModbusBinaryFramer", + "SyncModbusTransactionManager", ] -# pylint: disable=missing-type-doc import struct -import time from contextlib import suppress -from functools import partial from threading import RLock +from typing import TYPE_CHECKING from pymodbus.exceptions import ( ConnectionException, InvalidMessageReceivedException, ModbusIOException, ) -from pymodbus.framer.ascii_framer import ModbusAsciiFramer -from pymodbus.framer.binary_framer import ModbusBinaryFramer -from pymodbus.framer.rtu_framer import ModbusRtuFramer -from pymodbus.framer.socket_framer import ModbusSocketFramer -from pymodbus.framer.tls_framer import ModbusTlsFramer +from pymodbus.framer import ( + ModbusAsciiFramer, + ModbusRtuFramer, + ModbusSocketFramer, + ModbusTlsFramer, +) from pymodbus.logging import Log +from pymodbus.pdu import ModbusRequest +from pymodbus.transport import CommType from pymodbus.utilities import ModbusTransactionState, hexlify_packets +if TYPE_CHECKING: + from pymodbus.client.base import ModbusBaseSyncClient + + # --------------------------------------------------------------------------- # # The Global Transaction Manager # --------------------------------------------------------------------------- # class ModbusTransactionManager: """Implement a transaction for a manager. + Results are keyed based on the supplied transaction id. + """ + + def __init__(self): + """Initialize an instance of the ModbusTransactionManager.""" + self.tid = 0 + self.transactions: dict[int, ModbusRequest] = {} + + def __iter__(self): + """Iterate over the current managed transactions. + + :returns: An iterator of the managed transactions + """ + return iter(self.transactions.keys()) + + def addTransaction(self, request: ModbusRequest): + """Add a transaction to the handler. + + This holds the request in case it needs to be resent. + After being sent, the request is removed. + + :param request: The request to hold on to + """ + tid = request.transaction_id + Log.debug("Adding transaction {}", tid) + self.transactions[tid] = request + + def getTransaction(self, tid: int): + """Return a transaction matching the referenced tid. + + If the transaction does not exist, None is returned + + :param tid: The transaction to retrieve + + """ + Log.debug("Getting transaction {}", tid) + if not tid: + if self.transactions: + ret = self.transactions.popitem()[1] + self.transactions.clear() + return ret + return None + return self.transactions.pop(tid, None) + + def delTransaction(self, tid: int): + """Remove a transaction matching the referenced tid. + + :param tid: The transaction to remove + """ + Log.debug("deleting transaction {}", tid) + self.transactions.pop(tid, None) + + def getNextTID(self) -> int: + """Retrieve the next unique transaction identifier. + + This handles incrementing the identifier after + retrieval + + :returns: The next unique transaction identifier + """ + if self.tid < 65000: + self.tid += 1 + else: + self.tid = 1 + return self.tid + + def reset(self): + """Reset the transaction identifier.""" + self.tid = 0 + self.transactions = {} + + +class SyncModbusTransactionManager(ModbusTransactionManager): + """Implement a transaction for a manager. + The transaction protocol can be represented by the following pseudo code:: count = 0 @@ -52,32 +133,16 @@ class ModbusTransactionManager: Results are keyed based on the supplied transaction id. """ - def __init__(self, client, **kwargs): - """Initialize an instance of the ModbusTransactionManager. - - :param client: The client socket wrapper - :param retry_on_empty: Should the client retry on empty - :param retries: The number of retries to allow - """ - self.tid = 0 - self.client = client - self.backoff = kwargs.get("backoff", 0.3) - self.retry_on_empty = kwargs.get("retry_on_empty", False) - self.retry_on_invalid = kwargs.get("retry_on_invalid", False) - self.retries = kwargs.get("retries", 3) - self.transactions = {} + def __init__(self, client: ModbusBaseSyncClient, retries): + """Initialize an instance of the ModbusTransactionManager.""" + super().__init__() + self.client: ModbusBaseSyncClient = client + self.retries = retries self._transaction_lock = RLock() - self._no_response_devices = [] + self._no_response_devices: list[int] = [] if client: self._set_adu_size() - def __iter__(self): - """Iterate over the current managed transactions. - - :returns: An iterator of the managed transactions - """ - return iter(self.transactions.keys()) - def _set_adu_size(self): """Set adu size.""" # base ADU size of modbus frame in bytes @@ -87,8 +152,6 @@ def _set_adu_size(self): self.base_adu_size = 3 # address(1), CRC(2) elif isinstance(self.client.framer, ModbusAsciiFramer): self.base_adu_size = 7 # start(1)+ Address(2), LRC(2) + end(2) - elif isinstance(self.client.framer, ModbusBinaryFramer): - self.base_adu_size = 5 # start(1) + Address(1), CRC(2) + end(1) elif isinstance(self.client.framer, ModbusTlsFramer): self.base_adu_size = 0 # no header and footer else: @@ -106,11 +169,11 @@ def _calculate_exception_length(self): return self.base_adu_size + 2 # Fcode(1), ExceptionCode(1) if isinstance(self.client.framer, ModbusAsciiFramer): return self.base_adu_size + 4 # Fcode(2), ExceptionCode(2) - if isinstance(self.client.framer, (ModbusRtuFramer, ModbusBinaryFramer)): + if isinstance(self.client.framer, ModbusRtuFramer): return self.base_adu_size + 2 # Fcode(1), ExceptionCode(1) return None - def _validate_response(self, request, response, exp_resp_len, is_udp=False): + def _validate_response(self, request: ModbusRequest, response, exp_resp_len, is_udp=False): """Validate Incoming response against request. :param request: Request sent @@ -121,7 +184,10 @@ def _validate_response(self, request, response, exp_resp_len, is_udp=False): if not response: return False - mbap = self.client.framer.decode_data(response) + if hasattr(self.client.framer, "decode_data"): + mbap = self.client.framer.decode_data(response) + else: + mbap = {} if ( mbap.get("slave") != request.slave_id or mbap.get("fcode") & 0x7F != request.function_code @@ -132,7 +198,7 @@ def _validate_response(self, request, response, exp_resp_len, is_udp=False): return mbap.get("length") == exp_resp_len return True - def execute(self, request): # noqa: C901 + def execute(self, request: ModbusRequest): # noqa: C901 """Start the producer to send the next request to consumer.write(Frame(request)).""" with self._transaction_lock: try: @@ -148,9 +214,7 @@ def execute(self, request): # noqa: C901 ): Log.debug("Clearing current Frame: - {}", _buffer) self.client.framer.resetFrame() - if broadcast := ( - self.client.params.broadcast_enable and not request.slave_id - ): + if broadcast := not request.slave_id: self._transact(request, None, broadcast=True) response = b"Broadcast write sent - no response expected" else: @@ -170,9 +234,8 @@ def execute(self, request): # noqa: C901 full = True else: full = False - c_str = str(self.client) is_udp = False - if "modbusudpclient" in c_str.lower().strip(): + if self.client.comm_params.comm_type == CommType.UDP: is_udp = True full = True if not expected_response_length: @@ -199,38 +262,11 @@ def execute(self, request): # noqa: C901 if not response: if request.slave_id not in self._no_response_devices: self._no_response_devices.append(request.slave_id) - if self.retry_on_empty: - response, last_exception = self._retry_transaction( - retries, - "empty", - request, - expected_response_length, - full=full, - ) - retries -= 1 - else: - # No response received and retries not enabled - break - elif self.retry_on_invalid: - response, last_exception = self._retry_transaction( - retries, - "invalid", - request, - expected_response_length, - full=full, - ) - retries -= 1 - else: - break - # full = False - Log.debug("Retry getting response: - {}", _buffer) - addTransaction = partial( # pylint: disable=invalid-name - self.addTransaction, - tid=request.transaction_id, - ) + # No response received and retries not enabled + break self.client.framer.processIncomingPacket( response, - addTransaction, + self.addTransaction, request.slave_id, tid=request.transaction_id, ) @@ -267,10 +303,6 @@ def _retry_transaction(self, retries, reason, packet, response_length, full=Fals Log.debug("Retry on {} response - {}", reason, retries) Log.debug('Changing transaction state from "WAITING_FOR_REPLY" to "RETRYING"') self.client.state = ModbusTransactionState.RETRYING - if self.backoff: - delay = 2 ** (self.retries - retries) * self.backoff - time.sleep(delay) - Log.debug("Sleeping {}", delay) self.client.connect() if hasattr(self.client, "_in_waiting"): if ( @@ -281,7 +313,7 @@ def _retry_transaction(self, retries, reason, packet, response_length, full=Fals return result, None return self._transact(packet, response_length, full=full) - def _transact(self, packet, response_length, full=False, broadcast=False): + def _transact(self, request: ModbusRequest, response_length, full=False, broadcast=False): """Do a Write and Read transaction. :param packet: packet to be sent @@ -294,7 +326,7 @@ def _transact(self, packet, response_length, full=False, broadcast=False): last_exception = None try: self.client.connect() - packet = self.client.framer.buildPacket(packet) + packet = self.client.framer.buildPacket(request) Log.debug("SEND: {}", packet, ":hex") size = self._send(packet) if ( @@ -334,11 +366,11 @@ def _transact(self, packet, response_length, full=False, broadcast=False): result = b"" return result, last_exception - def _send(self, packet, _retrying=False): + def _send(self, packet: bytes, _retrying=False): """Send.""" return self.client.framer.sendPacket(packet) - def _recv(self, expected_response_length, full): # noqa: C901 + def _recv(self, expected_response_length, full) -> bytes: # noqa: C901 """Receive.""" total = None if not full: @@ -349,8 +381,6 @@ def _recv(self, expected_response_length, full): # noqa: C901 min_size = 4 elif isinstance(self.client.framer, ModbusAsciiFramer): min_size = 5 - elif isinstance(self.client.framer, ModbusBinaryFramer): - min_size = 3 else: min_size = expected_response_length @@ -368,8 +398,6 @@ def _recv(self, expected_response_length, full): # noqa: C901 func_code = int(read_min[1]) elif isinstance(self.client.framer, ModbusAsciiFramer): func_code = int(read_min[3:5], 16) - elif isinstance(self.client.framer, ModbusBinaryFramer): - func_code = int(read_min[-1]) else: func_code = -1 @@ -426,7 +454,7 @@ def _recv(self, expected_response_length, full): # noqa: C901 self.client.state = ModbusTransactionState.PROCESSING_REPLY return result - def _get_expected_response_length(self, data): + def _get_expected_response_length(self, data) -> int: """Get the expected response length. :param data: Message data read so far @@ -436,63 +464,3 @@ def _get_expected_response_length(self, data): func_code = int(data[1]) pdu_class = self.client.framer.decoder.lookupPduClass(func_code) return pdu_class.calculateRtuFrameSize(data) - - def addTransaction(self, request, tid=None): - """Add a transaction to the handler. - - This holds the request in case it needs to be resent. - After being sent, the request is removed. - - :param request: The request to hold on to - :param tid: The overloaded transaction id to use - """ - tid = tid if tid is not None else request.transaction_id - Log.debug("Adding transaction {}", tid) - self.transactions[tid] = request - - def getTransaction(self, tid): - """Return a transaction matching the referenced tid. - - If the transaction does not exist, None is returned - - :param tid: The transaction to retrieve - - """ - Log.debug("Getting transaction {}", tid) - if not tid: - if self.transactions: - ret = self.transactions.popitem()[1] - self.transactions.clear() - return ret - return None - return self.transactions.pop(tid, None) - - def delTransaction(self, tid): - """Remove a transaction matching the referenced tid. - - :param tid: The transaction to remove - """ - Log.debug("deleting transaction {}", tid) - self.transactions.pop(tid, None) - - def getNextTID(self): - """Retrieve the next unique transaction identifier. - - This handles incrementing the identifier after - retrieval - - :returns: The next unique transaction identifier - """ - if self.tid < 65000: - self.tid += 1 - else: - self.tid = 1 - return self.tid - - def reset(self): - """Reset the transaction identifier.""" - self.tid = 0 - self.transactions = {} - -class DictTransactionManager(ModbusTransactionManager): - """Old alias for ModbusTransactionManager.""" diff --git a/pymodbus/transport/serialtransport.py b/pymodbus/transport/serialtransport.py index 6d632fcae..111116b1b 100644 --- a/pymodbus/transport/serialtransport.py +++ b/pymodbus/transport/serialtransport.py @@ -15,12 +15,14 @@ class SerialTransport(asyncio.Transport): force_poll: bool = os.name == "nt" - def __init__(self, loop, protocol, *args, **kwargs) -> None: + def __init__(self, loop, protocol, url, baudrate, bytesize, parity, stopbits, timeout) -> None: """Initialize.""" super().__init__() self.async_loop = loop self.intern_protocol: asyncio.BaseProtocol = protocol - self.sync_serial = serial.serial_for_url(*args, **kwargs) + self.sync_serial = serial.serial_for_url(url, exclusive=True, + baudrate=baudrate, bytesize=bytesize, parity=parity, stopbits=stopbits, timeout=timeout +) self.intern_write_buffer: list[bytes] = [] self.poll_task: asyncio.Task | None = None self._poll_wait_time = 0.0005 @@ -154,10 +156,20 @@ async def polling_task(self): self.intern_read_ready() async def create_serial_connection( - loop, protocol_factory, *args, **kwargs + loop, protocol_factory, url, + baudrate=None, + bytesize=None, + parity=None, + stopbits=None, + timeout=None, ) -> tuple[asyncio.Transport, asyncio.BaseProtocol]: """Create a connection to a new serial port instance.""" protocol = protocol_factory() - transport = SerialTransport(loop, protocol, *args, **kwargs) + transport = SerialTransport(loop, protocol, url, + baudrate, + bytesize, + parity, + stopbits, + timeout) loop.call_soon(transport.setup) return transport, protocol diff --git a/pymodbus/transport/transport.py b/pymodbus/transport/transport.py index 1be0d2bb1..8ec1216e5 100644 --- a/pymodbus/transport/transport.py +++ b/pymodbus/transport/transport.py @@ -52,10 +52,11 @@ import dataclasses import ssl from abc import abstractmethod +from collections.abc import Callable, Coroutine from contextlib import suppress from enum import Enum from functools import partial -from typing import Any, Callable, Coroutine +from typing import Any from pymodbus.logging import Log from pymodbus.transport.serialtransport import create_serial_connection @@ -82,7 +83,7 @@ class CommParams: comm_type: CommType | None = None reconnect_delay: float | None = None reconnect_delay_max: float = 0.0 - timeout_connect: float | None = None + timeout_connect: float = 0.0 host: str = "localhost" # On some machines this will now be ::1 port: int = 0 source_address: tuple[str, int] | None = None @@ -201,7 +202,6 @@ def init_setup_connect_listen(self, host: str, port: int) -> None: parity=self.comm_params.parity, stopbits=self.comm_params.stopbits, timeout=self.comm_params.timeout_connect, - exclusive=True, ) return if self.comm_params.comm_type == CommType.UDP: diff --git a/pyproject.toml b/pyproject.toml index c6654194d..99182bdb5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -22,7 +22,7 @@ classifiers = [ "Operating System :: MacOS :: MacOS X", "Operating System :: OS Independent", "Operating System :: Microsoft", - "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", @@ -30,7 +30,7 @@ classifiers = [ "Topic :: System :: Networking", "Topic :: Utilities", ] -requires-python = ">=3.8.0" +requires-python = ">=3.9.0" [project.urls] Homepage = "https://github.com/pymodbus-dev/pymodbus/" @@ -52,27 +52,27 @@ repl = [ simulator = [ "aiohttp>=3.8.6;python_version<'3.12'", - "aiohttp>=3.9.0b0;python_version=='3.12'" + "aiohttp>=3.9.5;python_version=='3.12'" ] documentation = [ "recommonmark>=0.7.1", - "Sphinx>=5.3.0", - "sphinx-rtd-theme>=1.1.1" + "Sphinx>=7.3.7", + "sphinx-rtd-theme>=2.0.0" ] development = [ - "build>=1.1.1", - "codespell>=2.2.6", - "coverage>=7.4.3", - "mypy>=1.9.0", - "pylint>=3.1.0", - "pytest>=8.1.0", - "pytest-asyncio>=0.23.5.post1", - "pytest-cov>=4.1.0", + "build>=1.2.1", + "codespell>=2.3.0", + "coverage>=7.6.0", + "mypy>=1.10.1", + "pylint>=3.2.5", + "pytest>=8.2.2", + "pytest-asyncio>=0.23.8", + "pytest-cov>=5.0.0", "pytest-profiling>=1.7.0", "pytest-timeout>=2.3.1", - "pytest-xdist>=3.5.0", - "ruff>=0.3.3", - "twine>=5.0.0", + "pytest-xdist>=3.6.1", + "ruff>=0.5.3", + "twine>=5.1.1", "types-Pygments", "types-pyserial" ] @@ -129,7 +129,7 @@ load-plugins = [ "pylint.extensions.typing" ] jobs = "0" -py-version = "3.8" +py-version = "3.9" [tool.pylint.messages_control] enable = "all" diff --git a/test/conftest.py b/test/conftest.py index ee499042a..3cc85989f 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -286,14 +286,17 @@ def recv(self, size): """Receive.""" if not self.packets or not size: return b"" - if not self.buffer: - self.buffer = self.packets.popleft() - if size >= len(self.buffer): - retval = self.buffer - self.buffer = None - else: - retval = self.buffer[0:size] - self.buffer = self.buffer[size] + # if not self.buffer: + # self.buffer = self.packets.popleft() + # if size >= len(self.buffer): + # retval = self.buffer + # self.buffer = None + # else: + # retval = self.buffer[0:size] + # self.buffer = self.buffer[size] + self.buffer = self.packets.popleft() + retval = self.buffer + self.buffer = None self.in_waiting -= len(retval) return retval diff --git a/test/message/__init__.py b/test/framers/__init__.py similarity index 100% rename from test/message/__init__.py rename to test/framers/__init__.py diff --git a/test/framers/conftest.py b/test/framers/conftest.py new file mode 100644 index 000000000..078599c99 --- /dev/null +++ b/test/framers/conftest.py @@ -0,0 +1,66 @@ +"""Configure pytest.""" +from __future__ import annotations + +from unittest import mock + +import pytest + +from pymodbus.factory import ClientDecoder, ServerDecoder +from pymodbus.framer import Framer, FramerType +from pymodbus.transport import CommParams, ModbusProtocol + + +class DummyFramer(Framer): + """Implement use of ModbusProtocol.""" + + def __init__(self, + framer_type: FramerType, + params: CommParams, + is_server: bool, + device_ids: list[int] | None, + ): + """Initialize a frame instance.""" + super().__init__(framer_type, params, is_server, device_ids) + self.send = mock.Mock() + self.framer_type = framer_type + + def callback_new_connection(self) -> ModbusProtocol: + """Call when listener receive new connection request.""" + return DummyFramer(self.framer_type, self.comm_params, self.is_server, self.device_ids) # pragma: no cover + + def callback_connected(self) -> None: + """Call when connection is succcesfull.""" + + def callback_disconnected(self, exc: Exception | None) -> None: + """Call when connection is lost.""" + + def callback_request_response(self, data: bytes, device_id: int, tid: int) -> None: + """Handle received modbus request/response.""" + + +@pytest.fixture(name="entry") +def prepare_entry(): + """Return framer_type.""" + return FramerType.RAW + +@pytest.fixture(name="is_server") +def prepare_is_server(): + """Return client/server.""" + return False + +@pytest.fixture(name="dummy_framer") +async def prepare_test_framer(entry, is_server): + """Return framer object.""" + framer = DummyFramer( + entry, + CommParams(), + is_server, + [0, 1], + ) + if entry == FramerType.RTU: + func_table = (ServerDecoder if is_server else ClientDecoder)().lookup + for key, ent in func_table.items(): + fix_len = ent._rtu_frame_size if hasattr(ent, "_rtu_frame_size") else 0 # pylint: disable=protected-access + cnt_pos = ent. _rtu_byte_count_pos if hasattr(ent, "_rtu_byte_count_pos") else 0 # pylint: disable=protected-access + framer.handle.set_fc_calc(key, fix_len, cnt_pos) + return framer diff --git a/test/message/generator.py b/test/framers/generator.py similarity index 97% rename from test/message/generator.py rename to test/framers/generator.py index 038f0d2b6..04344a823 100755 --- a/test/message/generator.py +++ b/test/framers/generator.py @@ -9,7 +9,7 @@ ModbusTlsFramer, ) from pymodbus.pdu import ModbusExceptions as merror -from pymodbus.register_read_message import ( +from pymodbus.pdu.register_read_message import ( ReadHoldingRegistersRequest, ReadHoldingRegistersResponse, ) diff --git a/test/message/server_multidrop_tbd.py b/test/framers/server_multidrop_tbd.py similarity index 99% rename from test/message/server_multidrop_tbd.py rename to test/framers/server_multidrop_tbd.py index 74711ab5c..e2a21bd14 100644 --- a/test/message/server_multidrop_tbd.py +++ b/test/framers/server_multidrop_tbd.py @@ -3,7 +3,7 @@ import pytest -from pymodbus.framer.rtu_framer import ModbusRtuFramer +from pymodbus.framer import ModbusRtuFramer from pymodbus.server.async_io import ServerDecoder diff --git a/test/message/test_ascii.py b/test/framers/test_ascii.py similarity index 90% rename from test/message/test_ascii.py rename to test/framers/test_ascii.py index e166828c3..d3b4ef74c 100644 --- a/test/message/test_ascii.py +++ b/test/framers/test_ascii.py @@ -1,17 +1,17 @@ -"""Test transport.""" +"""Test framer.""" import pytest -from pymodbus.message.ascii import MessageAscii +from pymodbus.framer.ascii import FramerAscii -class TestMessageAscii: - """Test message module.""" +class TestFramerAscii: + """Test module.""" @staticmethod @pytest.fixture(name="frame") def prepare_frame(): """Return message object.""" - return MessageAscii() + return FramerAscii() @pytest.mark.parametrize( @@ -34,7 +34,7 @@ def test_decode(self, frame, packet, used_len, res_id, res): res_len, tid, dev_id, data = frame.decode(packet) assert res_len == used_len assert data == res - assert not tid + assert tid == res_id assert dev_id == res_id @pytest.mark.parametrize( diff --git a/test/framers/test_framer.py b/test/framers/test_framer.py new file mode 100644 index 000000000..59e6cdda1 --- /dev/null +++ b/test/framers/test_framer.py @@ -0,0 +1,390 @@ +"""Test framer.""" + +from unittest import mock + +import pytest + +from pymodbus.framer import FramerType +from pymodbus.framer.ascii import FramerAscii +from pymodbus.framer.rtu import FramerRTU +from pymodbus.framer.socket import FramerSocket +from pymodbus.framer.tls import FramerTLS + + +class TestFramer: + """Test module.""" + + @pytest.mark.parametrize(("entry"), list(FramerType)) + async def test_framer_init(self, dummy_framer): + """Test framer type.""" + assert dummy_framer.handle + + @pytest.mark.parametrize(("data", "res_len", "cx", "rc"), [ + (b'12345', 5, 1, [(5, 0, 0, b'12345')]), # full frame + (b'12345', 0, 0, [(0, 0, 0, b'')]), # not full frame, need more data + (b'12345', 5, 0, [(5, 0, 0, b'')]), # faulty frame, skipped + (b'1234512345', 10, 2, [(5, 0, 0, b'12345'), (5, 0, 0, b'12345')]), # 2 full frames + (b'12345678', 5, 1, [(5, 0, 0, b'12345'), (0, 0, 0, b'')]), # full frame, not full frame + (b'67812345', 8, 1, [(8, 0, 0, b'12345')]), # garble first, full frame next + (b'12345678', 5, 0, [(5, 0, 0, b'')]), # garble first, not full frame + (b'12345678', 8, 0, [(8, 0, 0, b'')]), # garble first, faulty frame + ]) + async def test_framer_callback(self, dummy_framer, data, res_len, cx, rc): + """Test framer type.""" + dummy_framer.callback_request_response = mock.Mock() + dummy_framer.handle.decode = mock.MagicMock(side_effect=iter(rc)) + assert dummy_framer.callback_data(data) == res_len + assert dummy_framer.callback_request_response.call_count == cx + if cx: + dummy_framer.callback_request_response.assert_called_with(b'12345', 0, 0) + else: + dummy_framer.callback_request_response.assert_not_called() + + @pytest.mark.parametrize(("data", "res_len", "rc"), [ + (b'12345', 5, [(5, 0, 17, b'12345'), (0, 0, 0, b'')]), # full frame, wrong dev_id + ]) + async def test_framer_callback_wrong_id(self, dummy_framer, data, res_len, rc): + """Test framer type.""" + dummy_framer.callback_request_response = mock.Mock() + dummy_framer.handle.decode = mock.MagicMock(side_effect=iter(rc)) + dummy_framer.broadcast = False + assert dummy_framer.callback_data(data) == res_len + dummy_framer.callback_request_response.assert_not_called() + + async def test_framer_build_send(self, dummy_framer): + """Test framer type.""" + dummy_framer.handle.encode = mock.MagicMock(return_value=(b'decode')) + dummy_framer.build_send(b'decode', 1, 0) + dummy_framer.handle.encode.assert_called_once() + dummy_framer.send.assert_called_once() + dummy_framer.send.assert_called_with(b'decode', None) + + @pytest.mark.parametrize( + ("data", "res_len", "res_id", "res_tid", "res_data"), [ + (b'\x00\x01', 0, 0, 0, b''), + (b'\x01\x02\x03', 3, 1, 2, b'\x03'), + (b'\x04\x05\x06\x07\x08\x09\x00\x01\x02\x03', 10, 4, 5, b'\x06\x07\x08\x09\x00\x01\x02\x03'), + ]) + async def test_framer_decode(self, dummy_framer, data, res_id, res_tid, res_len, res_data): + """Test decode method in all types.""" + t_len, t_id, t_tid, t_data = dummy_framer.handle.decode(data) + assert res_len == t_len + assert res_id == t_id + assert res_tid == t_tid + assert res_data == t_data + + @pytest.mark.parametrize( + ("data", "dev_id", "tid", "res_data"), [ + (b'\x01\x02', 5, 6, b'\x05\x06\x01\x02'), + (b'\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09', 17, 25, b'\x11\x19\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09'), + ]) + async def test_framer_encode(self, dummy_framer, data, dev_id, tid, res_data): + """Test decode method in all types.""" + t_data = dummy_framer.handle.encode(data, dev_id, tid) + assert res_data == t_data + + @pytest.mark.parametrize( + ("func", "lrc", "expect"), + [(FramerAscii.check_LRC, 0x1c, True), + (FramerAscii.check_LRC, 0x0c, False), + (FramerAscii.compute_LRC, None, 0x1c), + (FramerRTU.check_CRC, 0xE2DB, True), + (FramerRTU.check_CRC, 0xDBE2, False), + (FramerRTU.compute_CRC, None, 0xE2DB), + ] + ) + def test_LRC_CRC(self, func, lrc, expect): + """Test check_LRC.""" + data = b'\x12\x34\x23\x45\x34\x56\x45\x67' + assert expect == func(data, lrc) if lrc else func(data) + + def test_roundtrip_LRC(self): + """Test combined compute/check LRC.""" + data = b'\x12\x34\x23\x45\x34\x56\x45\x67' + assert FramerAscii.compute_LRC(data) == 0x1c + assert FramerAscii.check_LRC(data, 0x1C) + + def test_crc16_table(self): + """Test the crc16 table is prefilled.""" + assert len(FramerRTU.crc16_table) == 256 + assert isinstance(FramerRTU.crc16_table[0], int) + assert isinstance(FramerRTU.crc16_table[255], int) + + def test_roundtrip_CRC(self): + """Test combined compute/check CRC.""" + data = b'\x12\x34\x23\x45\x34\x56\x45\x67' + assert FramerRTU.compute_CRC(data) == 0xE2DB + assert FramerRTU.check_CRC(data, 0xE2DB) + + + +class TestFramerType: + """Test classes.""" + + @pytest.mark.parametrize( + ("frame", "frame_expected"), + [ + (FramerAscii, [ + b':0003007C00027F\r\n', + b':000304008D008EDE\r\n', + b':0083027B\r\n', + b':1103007C00026E\r\n', + b':110304008D008ECD\r\n', + b':1183026A\r\n', + b':FF03007C000280\r\n', + b':FF0304008D008EDF\r\n', + b':FF83027C\r\n', + b':0003007C00027F\r\n', + b':000304008D008EDE\r\n', + b':0083027B\r\n', + b':1103007C00026E\r\n', + b':110304008D008ECD\r\n', + b':1183026A\r\n', + b':FF03007C000280\r\n', + b':FF0304008D008EDF\r\n', + b':FF83027C\r\n', + b':0003007C00027F\r\n', + b':000304008D008EDE\r\n', + b':0083027B\r\n', + b':1103007C00026E\r\n', + b':110304008D008ECD\r\n', + b':1183026A\r\n', + b':FF03007C000280\r\n', + b':FF0304008D008EDF\r\n', + b':FF83027C\r\n', + ]), + (FramerRTU, [ + b'\x00\x03\x00\x7c\x00\x02\x04\x02', + b'\x00\x03\x04\x00\x8d\x00\x8e\xfa\xbc', + b'\x00\x83\x02\x91\x31', + b'\x11\x03\x00\x7c\x00\x02\x07\x43', + b'\x11\x03\x04\x00\x8d\x00\x8e\xfb\xbd', + b'\x11\x83\x02\xc1\x34', + b'\xff\x03\x00\x7c\x00\x02\x10\x0d', + b'\xff\x03\x04\x00\x8d\x00\x8e\xf5\xb3', + b'\xff\x83\x02\xa1\x01', + b'\x00\x03\x00\x7c\x00\x02\x04\x02', + b'\x00\x03\x04\x00\x8d\x00\x8e\xfa\xbc', + b'\x00\x83\x02\x91\x31', + b'\x11\x03\x00\x7c\x00\x02\x07\x43', + b'\x11\x03\x04\x00\x8d\x00\x8e\xfb\xbd', + b'\x11\x83\x02\xc1\x34', + b'\xff\x03\x00\x7c\x00\x02\x10\x0d', + b'\xff\x03\x04\x00\x8d\x00\x8e\xf5\xb3', + b'\xff\x83\x02\xa1\x01', + b'\x00\x03\x00\x7c\x00\x02\x04\x02', + b'\x00\x03\x04\x00\x8d\x00\x8e\xfa\xbc', + b'\x00\x83\x02\x91\x31', + b'\x11\x03\x00\x7c\x00\x02\x07\x43', + b'\x11\x03\x04\x00\x8d\x00\x8e\xfb\xbd', + b'\x11\x83\x02\xc1\x34', + b'\xff\x03\x00\x7c\x00\x02\x10\x0d', + b'\xff\x03\x04\x00\x8d\x00\x8e\xf5\xb3', + b'\xff\x83\x02\xa1\x01', + ]), + (FramerSocket, [ + b'\x00\x00\x00\x00\x00\x06\x00\x03\x00\x7c\x00\x02', + b'\x00\x00\x00\x00\x00\x07\x00\x03\x04\x00\x8d\x00\x8e', + b'\x00\x00\x00\x00\x00\x03\x00\x83\x02', + b'\x00\x00\x00\x00\x00\x06\x11\x03\x00\x7c\x00\x02', + b'\x00\x00\x00\x00\x00\x07\x11\x03\x04\x00\x8d\x00\x8e', + b'\x00\x00\x00\x00\x00\x03\x11\x83\x02', + b'\x00\x00\x00\x00\x00\x06\xff\x03\x00\x7c\x00\x02', + b'\x00\x00\x00\x00\x00\x07\xff\x03\x04\x00\x8d\x00\x8e', + b'\x00\x00\x00\x00\x00\x03\xff\x83\x02', + b'\x0c\x05\x00\x00\x00\x06\x00\x03\x00\x7c\x00\x02', + b'\x0c\x05\x00\x00\x00\x07\x00\x03\x04\x00\x8d\x00\x8e', + b'\x0c\x05\x00\x00\x00\x03\x00\x83\x02', + b'\x0c\x05\x00\x00\x00\x06\x11\x03\x00\x7c\x00\x02', + b'\x0c\x05\x00\x00\x00\x07\x11\x03\x04\x00\x8d\x00\x8e', + b'\x0c\x05\x00\x00\x00\x03\x11\x83\x02', + b'\x0c\x05\x00\x00\x00\x06\xff\x03\x00\x7c\x00\x02', + b'\x0c\x05\x00\x00\x00\x07\xff\x03\x04\x00\x8d\x00\x8e', + b'\x0c\x05\x00\x00\x00\x03\xff\x83\x02', + ]), + (FramerTLS, [ + b'\x03\x00\x7c\x00\x02', + b'\x03\x04\x00\x8d\x00\x8e', + b'\x83\x02', + ]), + ] + ) + @pytest.mark.parametrize( + ("inx1", "data"), + [ + (0, b"\x03\x00\x7c\x00\x02",), # Request + (1, b"\x03\x04\x00\x8d\x00\x8e",), # Response + (2, b'\x83\x02',), # Exception + ] + ) + @pytest.mark.parametrize( + ("inx2", "dev_id"), + [ + (0, 0), + (3, 17), + (6, 255), + ] + ) + @pytest.mark.parametrize( + ("inx3", "tid"), + [ + (0, 0), + (9, 3077), + ] + ) + def test_encode_type(self, frame, frame_expected, data, dev_id, tid, inx1, inx2, inx3): + """Test encode method.""" + if frame == FramerTLS and dev_id + tid: + return + frame_obj = frame() + expected = frame_expected[inx1 + inx2 + inx3] + encoded_data = frame_obj.encode(data, dev_id, tid) + assert encoded_data == expected + + @pytest.mark.parametrize( + ("entry", "is_server", "data", "dev_id", "tid", "expected"), + [ + (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.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.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 + (FramerType.SOCKET, True, b'\x00\x00\x00\x00\x00\x06\x11\x03\x00\x7c\x00\x02', 17, 0, b"\x03\x00\x7c\x00\x02",), # Request + (FramerType.SOCKET, False, b'\x00\x00\x00\x00\x00\x07\x11\x03\x04\x00\x8d\x00\x8e', 17, 0, b"\x03\x04\x00\x8d\x00\x8e",), # Response + (FramerType.SOCKET, False, b'\x00\x00\x00\x00\x00\x03\x11\x83\x02', 17, 0, b'\x83\x02',), # Exception + (FramerType.SOCKET, True, b'\x00\x00\x00\x00\x00\x06\xff\x03\x00\x7c\x00\x02', 255, 0, b"\x03\x00\x7c\x00\x02",), # Request + (FramerType.SOCKET, False, b'\x00\x00\x00\x00\x00\x07\xff\x03\x04\x00\x8d\x00\x8e', 255, 0, b"\x03\x04\x00\x8d\x00\x8e",), # Response + (FramerType.SOCKET, False, b'\x00\x00\x00\x00\x00\x03\xff\x83\x02', 255, 0, b'\x83\x02',), # Exception + (FramerType.SOCKET, True, b'\x0c\x05\x00\x00\x00\x06\x00\x03\x00\x7c\x00\x02', 0, 3077, b"\x03\x00\x7c\x00\x02",), # Request + (FramerType.SOCKET, False, b'\x0c\x05\x00\x00\x00\x07\x00\x03\x04\x00\x8d\x00\x8e', 0, 3077, b"\x03\x04\x00\x8d\x00\x8e",), # Response + (FramerType.SOCKET, False, b'\x0c\x05\x00\x00\x00\x03\x00\x83\x02', 0, 3077, b'\x83\x02',), # Exception + (FramerType.SOCKET, True, b'\x0c\x05\x00\x00\x00\x06\x11\x03\x00\x7c\x00\x02', 17, 3077, b"\x03\x00\x7c\x00\x02",), # Request + (FramerType.SOCKET, False, b'\x0c\x05\x00\x00\x00\x07\x11\x03\x04\x00\x8d\x00\x8e', 17, 3077, b"\x03\x04\x00\x8d\x00\x8e",), # Response + (FramerType.SOCKET, False, b'\x0c\x05\x00\x00\x00\x03\x11\x83\x02', 17, 3077, b'\x83\x02',), # Exception + (FramerType.SOCKET, True, b'\x0c\x05\x00\x00\x00\x06\xff\x03\x00\x7c\x00\x02', 255, 3077, b"\x03\x00\x7c\x00\x02",), # Request + (FramerType.SOCKET, False, b'\x0c\x05\x00\x00\x00\x07\xff\x03\x04\x00\x8d\x00\x8e', 255, 3077, b"\x03\x04\x00\x8d\x00\x8e",), # Response + (FramerType.SOCKET, False, b'\x0c\x05\x00\x00\x00\x03\xff\x83\x02', 255, 3077, b'\x83\x02',), # Exception + (FramerType.TLS, True, b'\x03\x00\x7c\x00\x02', 0, 0, b"\x03\x00\x7c\x00\x02",), # Request + (FramerType.TLS, False, b'\x03\x04\x00\x8d\x00\x8e', 0, 0, b"\x03\x04\x00\x8d\x00\x8e",), # Response + (FramerType.TLS, False, b'\x83\x02', 0, 0, b'\x83\x02',), # Exception + ] + ) + @pytest.mark.parametrize( + ("split"), + [ + "no", + "half", + "single", + ] + ) + async def test_decode_type(self, entry, dummy_framer, data, dev_id, tid, expected, split): + """Test encode method.""" + if entry == FramerType.TLS and split != "no": + return + if entry == FramerType.RTU: + return + dummy_framer.callback_request_response = mock.MagicMock() + if split == "no": + used_len = dummy_framer.callback_data(data) + elif split == "half": + split_len = int(len(data) / 2) + assert not dummy_framer.callback_data(data[0:split_len]) + dummy_framer.callback_request_response.assert_not_called() + used_len = dummy_framer.callback_data(data) + else: + last = len(data) + for i in range(0, last -1): + assert not dummy_framer.callback_data(data[0:i+1]) + dummy_framer.callback_request_response.assert_not_called() + used_len = dummy_framer.callback_data(data) + assert used_len == len(data) + dummy_framer.callback_request_response.assert_called_with(expected, dev_id, tid) + + @pytest.mark.parametrize( + ("entry", "data", "exp"), + [ + (FramerType.ASCII, b':0003007C00017F\r\n', [ # bad crc + (17, b''), + ]), + (FramerType.ASCII, b':0003007C00027F\r\n:0003007C00027F\r\n', [ # double good crc + (17, b'\x03\x00\x7c\x00\x02'), + (17, b'\x03\x00\x7c\x00\x02'), + ]), + (FramerType.ASCII, b':0003007C00017F\r\n:0003007C00027F\r\n', [ # bad crc + good CRC + (34, b'\x03\x00\x7c\x00\x02'), + ]), + (FramerType.ASCII, b'abc:0003007C00027F\r\n', [ # garble in front + (20, b'\x03\x00\x7c\x00\x02'), + ]), + (FramerType.ASCII, b':0003007C00017F\r\nabc', [ # bad crc, garble after + (17, b''), + ]), + (FramerType.ASCII, b':0003007C00017F\r\nabcdefghijkl', [ # bad crc, garble after + (29, b''), + ]), + (FramerType.ASCII, b':0003007C00027F\r\nabc', [ # good crc, garble after + (17, b'\x03\x00\x7c\x00\x02'), + ]), + (FramerType.ASCII, b':0003007C00017F\r\n:0003', [ # bad crc, part second framer + (17, b''), + ]), + (FramerType.SOCKET, b'\x00\x00\x00\x00\x00\x06\x00\x03\x00\x7c\x00\x02\x00\x00\x00\x00\x00\x06\x00\x03\x00\x7c\x00\x02', [ # double good crc + (12, b"\x03\x00\x7c\x00\x02"), + (12, b"\x03\x00\x7c\x00\x02"), + ]), + (FramerType.RTU, b'\x00\x83\x02\x91\x21', [ # bad crc + (5, b''), + ]), + #(FramerType.RTU, b'\x00\x83\x02\xf0\x91\x31', [ # dummy char in stream, bad crc + # (5, b''), + #]), + # (FramerType.RTU, b'\x00\x83\x02\x91\x21\x00\x83\x02\x91\x31', [ # bad crc + good CRC + # (10, b'\x83\x02'), + #]), + #(FramerType.RTU, b'\x00\x83\x02\xf0\x91\x31\x00\x83\x02\x91\x31', [ # dummy char in stream, bad crc + good CRC + # (11, b''), + #]), + + # (FramerType.RTU, b'\x00\x83\x02\x91\x31', 0), # garble in front + # (FramerType.ASCII, b'abc:0003007C00027F\r\n', [ # garble in front + # (20, b'\x03\x00\x7c\x00\x02'), + # ]), + + # (FramerType.RTU, b'\x00\x83\x02\x91\x31', 0), # garble after + # (FramerType.ASCII, b':0003007C00017F\r\nabc', [ # bad crc, garble after + # (17, b''), + # ]), + # (FramerType.ASCII, b':0003007C00017F\r\nabcdefghijkl', [ # bad crc, garble after + # (29, b''), + # ]), + # (FramerType.ASCII, b':0003007C00027F\r\nabc', [ # good crc, garble after + # (17, b'\x03\x00\x7c\x00\x02'), + # ]), + # (FramerType.RTU, b'\x00\x83\x02\x91\x31', 0), # part second framer + # (FramerType.ASCII, b':0003007C00017F\r\n:0003', [ # bad crc, part second framer + # (17, b''), + # ]), + ] + ) + async def test_decode_complicated(self, dummy_framer, data, exp): + """Test encode method.""" + for ent in exp: + used_len, _, _, res_data = dummy_framer.handle.decode(data) + assert used_len == ent[0] + assert res_data == ent[1] diff --git a/test/test_framers.py b/test/framers/test_old_framers.py similarity index 90% rename from test/test_framers.py rename to test/framers/test_old_framers.py index f430e7450..56758bcb3 100644 --- a/test/test_framers.py +++ b/test/framers/test_old_framers.py @@ -3,18 +3,18 @@ import pytest -from pymodbus import Framer -from pymodbus.bit_read_message import ReadCoilsRequest +from pymodbus import FramerType from pymodbus.client.base import ModbusBaseClient from pymodbus.exceptions import ModbusIOException from pymodbus.factory import ClientDecoder from pymodbus.framer import ( ModbusAsciiFramer, - ModbusBinaryFramer, ModbusRtuFramer, ModbusSocketFramer, + ModbusTlsFramer, ) -from pymodbus.transport import CommType +from pymodbus.pdu.bit_read_message import ReadCoilsRequest +from pymodbus.transport import CommParams, CommType from pymodbus.utilities import ModbusTransactionState @@ -47,7 +47,7 @@ def fixture_ascii_framer(): [ ModbusRtuFramer, ModbusAsciiFramer, - ModbusBinaryFramer, + ModbusSocketFramer, ], ) def test_framer_initialization(self, framer): @@ -79,8 +79,6 @@ def test_framer_initialization(self, framer): "crc": b"\x00\x00", } assert framer._hsize == 0x01 # pylint: disable=protected-access - assert framer._end == b"\x0d\x0a" # pylint: disable=protected-access - assert framer._min_frame_size == 4 # pylint: disable=protected-access else: assert framer._header == { # pylint: disable=protected-access "tid": 0, @@ -90,13 +88,7 @@ def test_framer_initialization(self, framer): "len": 0, "uid": 0x00, } - assert framer._hsize == 0x01 # pylint: disable=protected-access - assert framer._start == b"\x7b" # pylint: disable=protected-access - assert framer._end == b"\x7d" # pylint: disable=protected-access - assert framer._repeat == [ # pylint: disable=protected-access - b"}"[0], - b"{"[0], - ] + assert framer._hsize == 0x07 # pylint: disable=protected-access @pytest.mark.parametrize( @@ -212,7 +204,8 @@ def callback(data): rtu_framer.processIncomingPacket(data, callback, self.slaves) assert not count - + callback(b'') + assert count @pytest.mark.parametrize( ("data", "header"), @@ -333,15 +326,19 @@ async def test_send_packet(self, rtu_framer): """Test send packet.""" message = TEST_MESSAGE client = ModbusBaseClient( - Framer.ASCII, - host="localhost", - port=BASE_PORT + 1, - CommType=CommType.TCP, + FramerType.ASCII, + 3, + None, + comm_params=CommParams( + comm_type=CommType.TCP, + host="localhost", + port=BASE_PORT + 1, + ), ) client.state = ModbusTransactionState.TRANSACTION_COMPLETE client.silent_interval = 1 client.last_frame_end = 1 - client.comm_params.timeout_connect = 0.25 + client.ctx.comm_params.timeout_connect = 0.25 client.idle_time = mock.Mock(return_value=1) client.send = mock.Mock(return_value=len(message)) rtu_framer.client = client @@ -371,6 +368,8 @@ def callback(data): data = TEST_MESSAGE rtu_framer.processIncomingPacket(data, callback, self.slaves) assert not count + callback(b'') + assert count @pytest.mark.parametrize(("slaves", "res"), [([16], 0), ([17], 1)]) def test_validate__slave_id(self,rtu_framer, slaves, res): @@ -421,9 +420,9 @@ def _handle_response(_reply): response_ok = False framer = ModbusSocketFramer(ClientDecoder()) if i: - framer.processIncomingPacket(part1, _handle_response, slave=0) + framer.processIncomingPacket(part1, _handle_response, 0) assert not response_ok, "Response should not be accepted" - framer.processIncomingPacket(part2, _handle_response, slave=0) + framer.processIncomingPacket(part2, _handle_response, 0) assert response_ok, "Response is valid, but not accepted" @@ -439,13 +438,13 @@ def _handle_response(_reply): message = bytearray(b"\x00\x02\x00\x00\x00\x03\x01\x84\x02") response_ok = False framer = ModbusSocketFramer(ClientDecoder()) - framer.processIncomingPacket(message, _handle_response, slave=0) + framer.processIncomingPacket(message, _handle_response, 0) assert response_ok, "Response is valid, but not accepted" message = bytearray(b"\x00\x01\x00\x00\x00\x0b\x01\x03\x08\x00\xb5\x12\x2f\x37\x21\x00\x03") response_ok = False framer = ModbusSocketFramer(ClientDecoder()) - framer.processIncomingPacket(message, _handle_response, slave=0) + framer.processIncomingPacket(message, _handle_response, 0) assert response_ok, "Response is valid, but not accepted" def test_recv_socket_exception_faulty(self): @@ -460,17 +459,16 @@ def _handle_response(_reply): message = bytearray(b"\x00\x02\x00\x00\x00\x02\x01\x84\x02") response_ok = False framer = ModbusSocketFramer(ClientDecoder()) - framer.processIncomingPacket(message, _handle_response, slave=0) + framer.processIncomingPacket(message, _handle_response, 0) assert response_ok, "Response is valid, but not accepted" # ---- 100% coverage @pytest.mark.parametrize( ("framer", "message"), [ - (ModbusAsciiFramer, b':00010001000AF4\r\n',), - (ModbusBinaryFramer, b'{\x00\x01\x00\x01\x00\n\xec\x1c}',), - (ModbusRtuFramer, b"\x00\x01\x00\x01\x00\n\xec\x1c",), - (ModbusSocketFramer, b'\x00\x00\x00\x00\x00\x06\x00\x01\x00\x01\x00\n',), + (ModbusAsciiFramer, b':01010001000AF3\r\n',), + (ModbusRtuFramer, b"\x01\x01\x00\x01\x00\n\xed\xcd",), + (ModbusSocketFramer, b'\x00\x00\x00\x00\x00\x06\x01\x01\x00\x01\x00\n',), ] ) def test_build_packet(self, framer, message): @@ -484,7 +482,6 @@ def test_build_packet(self, framer, message): ("framer", "message"), [ (ModbusAsciiFramer, b':01010001000AF3\r\n',), - (ModbusBinaryFramer, b'A{\x01\x01\x00\x01\x00\n\xed\xcd}',), (ModbusRtuFramer, b"\x01\x01\x03\x01\x00\n\xed\x89",), (ModbusSocketFramer, b'\x00\x00\x00\x00\x00\x06\x01\x01\x00\x01\x00\n',), ] @@ -500,9 +497,9 @@ def test_processincomingpacket_ok(self, framer, message, slave): ("framer", "message"), [ (ModbusAsciiFramer, b':01270001000ACD\r\n',), - (ModbusBinaryFramer, b'{\x01\x1a\x00\x01\x00\n\x89\xcf}',), (ModbusRtuFramer, b"\x01\x03\x03\x01\x00\n\x94\x49",), (ModbusSocketFramer, b'\x00\x00\x00\x00\x00\x06\x01\x27\x00\x01\x00\n',), + (ModbusTlsFramer, b'\x54\x00\x7c\x00\x02',), ] ) def test_processincomingpacket_not_ok(self, framer, message): @@ -515,7 +512,6 @@ def test_processincomingpacket_not_ok(self, framer, message): ("framer", "message"), [ (ModbusAsciiFramer, b':61620001000AF4\r\n',), - (ModbusBinaryFramer, b'{\x61\x62\x00\x01\x00\n\xec\x1c}',), (ModbusRtuFramer, b"\x61\x62\x00\x01\x00\n\xec\x1c",), (ModbusSocketFramer, b'\x00\x00\x00\x00\x00\x06\x61\x62\x00\x01\x00\n',), ] @@ -529,8 +525,3 @@ def test_decode_data(self, framer, message, expected): decoded = test_framer.decode_data(message) assert decoded["fcode"] == expected["fcode"] assert decoded["slave"] == expected["slave"] - - def test_binary_framer_preflight(self): - """Test binary framer _preflight.""" - test_framer = ModbusBinaryFramer(ClientDecoder()) - assert test_framer._preflight(b'A{B}C') == b'A{{B}}C' # pylint: disable=protected-access diff --git a/test/framers/test_rtu.py b/test/framers/test_rtu.py new file mode 100644 index 000000000..13bdd8770 --- /dev/null +++ b/test/framers/test_rtu.py @@ -0,0 +1,54 @@ +"""Test framer.""" +import pytest + +from pymodbus.framer.rtu import FramerRTU + + +class TestFramerRTU: + """Test module.""" + + @staticmethod + @pytest.fixture(name="frame") + def prepare_frame(): + """Return message object.""" + return FramerRTU() + + @pytest.mark.skip() + @pytest.mark.parametrize( + ("packet", "used_len", "res_id", "res"), + [ + (b':010100010001FC\r\n', 17, 1, b'\x01\x00\x01\x00\x01'), + (b':00010001000AF4\r\n', 17, 0, b'\x01\x00\x01\x00\x0a'), + (b':01010001000AF3\r\n', 17, 1, b'\x01\x00\x01\x00\x0a'), + (b':61620001000A32\r\n', 17, 97, b'\x62\x00\x01\x00\x0a'), + (b':01270001000ACD\r\n', 17, 1, b'\x27\x00\x01\x00\x0a'), + (b':010100', 0, 0, b''), # short frame + (b':00010001000AF4', 0, 0, b''), + (b'abc:00010001000AF4', 3, 0, b''), # garble before frame + (b'abc00010001000AF4', 17, 0, b''), # only garble + (b':01010001000A00\r\n', 17, 0, b''), + ], + ) + def test_decode(self, frame, packet, used_len, res_id, res): + """Test decode.""" + res_len, tid, dev_id, data = frame.decode(packet) + assert res_len == used_len + assert data == res + assert tid == res_id + assert dev_id == res_id + + @pytest.mark.parametrize( + ("data", "dev_id", "res_msg"), + [ + (b'\x01\x01\x00', 2, b'\x02\x01\x01\x00\x51\xcc'), + (b'\x03\x06\xAE\x41\x56\x52\x43\x40', 17, b'\x11\x03\x06\xAE\x41\x56\x52\x43\x40\x49\xAD'), + (b'\x01\x03\x01\x00\x0a', 1, b'\x01\x01\x03\x01\x00\x0a\xed\x89'), + ], + ) + def test_roundtrip(self, frame, data, dev_id, res_msg): + """Test encode.""" + # msg = frame.encode(data, dev_id, 0) + # res_len, _, res_id, res_data = frame.decode(msg) + # assert data == res_data + # assert dev_id == res_id + # assert res_len == len(res_msg) diff --git a/test/message/test_socket.py b/test/framers/test_socket.py similarity index 91% rename from test/message/test_socket.py rename to test/framers/test_socket.py index e58ba65cc..d716d4c7d 100644 --- a/test/message/test_socket.py +++ b/test/framers/test_socket.py @@ -1,18 +1,18 @@ -"""Test transport.""" +"""Test framer.""" import pytest -from pymodbus.message.socket import MessageSocket +from pymodbus.framer.socket import FramerSocket -class TestMessageSocket: - """Test message module.""" +class TestFramerSocket: + """Test module.""" @staticmethod @pytest.fixture(name="frame") def prepare_frame(): """Return message object.""" - return MessageSocket() + return FramerSocket() @pytest.mark.parametrize( diff --git a/test/framers/test_tbc_transaction.py b/test/framers/test_tbc_transaction.py new file mode 100755 index 000000000..eb918727a --- /dev/null +++ b/test/framers/test_tbc_transaction.py @@ -0,0 +1,710 @@ +"""Test transaction.""" +from unittest import mock + +from pymodbus.exceptions import ( + ModbusIOException, +) +from pymodbus.factory import ServerDecoder +from pymodbus.pdu import ModbusRequest +from pymodbus.transaction import ( + ModbusAsciiFramer, + ModbusRtuFramer, + ModbusSocketFramer, + ModbusTlsFramer, + SyncModbusTransactionManager, +) + + +TEST_MESSAGE = b"\x7b\x01\x03\x00\x00\x00\x05\x85\xC9\x7d" + + +class TestTransaction: # pylint: disable=too-many-public-methods + """Unittest for the pymodbus.transaction module.""" + + client = None + decoder = None + _tcp = None + _tls = None + _rtu = None + _ascii = None + _manager = None + _tm = None + + # ----------------------------------------------------------------------- # + # Test Construction + # ----------------------------------------------------------------------- # + def setup_method(self): + """Set up the test environment.""" + self.client = None + self.decoder = ServerDecoder() + self._tcp = ModbusSocketFramer(decoder=self.decoder, client=None) + self._tls = ModbusTlsFramer(decoder=self.decoder, client=None) + self._rtu = ModbusRtuFramer(decoder=self.decoder, client=None) + self._ascii = ModbusAsciiFramer(decoder=self.decoder, client=None) + self._manager = SyncModbusTransactionManager(self.client, 3) + + # ----------------------------------------------------------------------- # + # Modbus transaction manager + # ----------------------------------------------------------------------- # + + def test_calculate_expected_response_length(self): + """Test calculate expected response length.""" + self._manager.client = mock.MagicMock() + self._manager.client.framer = mock.MagicMock() + self._manager._set_adu_size() # pylint: disable=protected-access + assert not self._manager._calculate_response_length( # pylint: disable=protected-access + 0 + ) + self._manager.base_adu_size = 10 + assert ( + self._manager._calculate_response_length(5) # pylint: disable=protected-access + == 15 + ) + + def test_calculate_exception_length(self): + """Test calculate exception length.""" + for framer, exception_length in ( + ("ascii", 11), + ("rtu", 5), + ("tcp", 9), + ("tls", 2), + ("dummy", None), + ): + self._manager.client = mock.MagicMock() + if framer == "ascii": + self._manager.client.framer = self._ascii + elif framer == "rtu": + self._manager.client.framer = self._rtu + elif framer == "tcp": + self._manager.client.framer = self._tcp + elif framer == "tls": + self._manager.client.framer = self._tls + else: + self._manager.client.framer = mock.MagicMock() + + self._manager._set_adu_size() # pylint: disable=protected-access + assert ( + self._manager._calculate_exception_length() # pylint: disable=protected-access + == exception_length + ) + + def test_execute(self): + """Test execute.""" + client = mock.MagicMock() + client.framer = self._ascii + client.framer._buffer = b"deadbeef" # pylint: disable=protected-access + client.framer.processIncomingPacket = mock.MagicMock() + client.framer.processIncomingPacket.return_value = None + client.framer.buildPacket = mock.MagicMock() + client.framer.buildPacket.return_value = b"deadbeef" + client.framer.sendPacket = mock.MagicMock() + client.framer.sendPacket.return_value = len(b"deadbeef") + client.framer.decode_data = mock.MagicMock() + client.framer.decode_data.return_value = { + "slave": 1, + "fcode": 222, + "length": 27, + } + request = mock.MagicMock() + request.get_response_pdu_size.return_value = 10 + request.slave_id = 1 + request.function_code = 222 + trans = SyncModbusTransactionManager(client, 3) + trans._recv = mock.MagicMock( # pylint: disable=protected-access + return_value=b"abcdef" + ) + assert trans.retries == 3 + + trans.getTransaction = mock.MagicMock() + trans.getTransaction.return_value = "response" + response = trans.execute(request) + assert response == "response" + # No response + trans._recv = mock.MagicMock( # pylint: disable=protected-access + return_value=b"abcdef" + ) + trans.transactions = {} + trans.getTransaction = mock.MagicMock() + trans.getTransaction.return_value = None + response = trans.execute(request) + assert isinstance(response, ModbusIOException) + + # No response with retries + trans._recv = mock.MagicMock( # pylint: disable=protected-access + side_effect=iter([b"", b"abcdef"]) + ) + response = trans.execute(request) + assert isinstance(response, ModbusIOException) + + # wrong handle_local_echo + trans._recv = mock.MagicMock( # pylint: disable=protected-access + side_effect=iter([b"abcdef", b"deadbe", b"123456"]) + ) + client.comm_params.handle_local_echo = True + assert trans.execute(request).message == "[Input/Output] Wrong local echo" + client.comm_params.handle_local_echo = False + + # retry on invalid response + trans._recv = mock.MagicMock( # pylint: disable=protected-access + side_effect=iter([b"", b"abcdef", b"deadbe", b"123456"]) + ) + response = trans.execute(request) + assert isinstance(response, ModbusIOException) + + # Unable to decode response + trans._recv = mock.MagicMock( # pylint: disable=protected-access + side_effect=ModbusIOException() + ) + client.framer.processIncomingPacket.side_effect = mock.MagicMock( + side_effect=ModbusIOException() + ) + assert isinstance(trans.execute(request), ModbusIOException) + + # Broadcast + request.slave_id = 0 + response = trans.execute(request) + assert response == b"Broadcast write sent - no response expected" + + # Broadcast w/ Local echo + client.comm_params.handle_local_echo = True + recv = mock.MagicMock(return_value=b"deadbeef") + trans._recv = recv # pylint: disable=protected-access + request.slave_id = 0 + response = trans.execute(request) + assert response == b"Broadcast write sent - no response expected" + recv.assert_called_once_with(8, False) + client.comm_params.handle_local_echo = False + + def test_transaction_manager_tid(self): + """Test the transaction manager TID.""" + for tid in range(1, self._manager.getNextTID() + 10): + assert tid + 1 == self._manager.getNextTID() + self._manager.reset() + assert self._manager.getNextTID() == 1 + + def test_get_transaction_manager_transaction(self): + """Test the getting a transaction from the transaction manager.""" + + class Request: # pylint: disable=too-few-public-methods + """Request.""" + + self._manager.reset() + handle = Request() + handle.transaction_id = ( # pylint: disable=attribute-defined-outside-init + self._manager.getNextTID() + ) + handle.message = b"testing" # pylint: disable=attribute-defined-outside-init + self._manager.addTransaction(handle) + result = self._manager.getTransaction(handle.transaction_id) + assert handle.message == result.message + + def test_delete_transaction_manager_transaction(self): + """Test deleting a transaction from the dict transaction manager.""" + + class Request: # pylint: disable=too-few-public-methods + """Request.""" + + self._manager.reset() + handle = Request() + handle.transaction_id = ( # pylint: disable=attribute-defined-outside-init + self._manager.getNextTID() + ) + handle.message = b"testing" # pylint: disable=attribute-defined-outside-init + + self._manager.addTransaction(handle) + self._manager.delTransaction(handle.transaction_id) + assert not self._manager.getTransaction(handle.transaction_id) + + # ----------------------------------------------------------------------- # + # TCP tests + # ----------------------------------------------------------------------- # + def test_tcp_framer_transaction_ready(self): + """Test a tcp frame transaction.""" + count = 0 + result = None + def callback(data): + """Simulate callback.""" + nonlocal count, result + count += 1 + result = data + + msg = b"\x00\x01\x12\x34\x00\x06\xff\x02\x01\x02\x00\x08" + self._tcp.processIncomingPacket(msg, callback, [1]) + self._tcp._buffer = msg # pylint: disable=protected-access + callback(b'') + + def test_tcp_framer_transaction_full(self): + """Test a full tcp frame transaction.""" + count = 0 + result = None + def callback(data): + """Simulate callback.""" + nonlocal count, result + count += 1 + result = data + + msg = b"\x00\x01\x12\x34\x00\x06\xff\x02\x01\x02\x00\x08" + self._tcp.processIncomingPacket(msg, callback, [0, 1]) + assert result.function_code.to_bytes(1,'big') + result.encode() == msg[7:] + + def test_tcp_framer_transaction_half(self): + """Test a half completed tcp frame transaction.""" + count = 0 + result = None + def callback(data): + """Simulate callback.""" + nonlocal count, result + count += 1 + result = data + + msg1 = b"\x00\x01\x12\x34\x00" + msg2 = b"\x06\xff\x02\x01\x02\x00\x08" + self._tcp.processIncomingPacket(msg1, callback, [0, 1]) + assert not result + self._tcp.processIncomingPacket(msg2, callback, [0, 1]) + assert result + assert result.function_code.to_bytes(1,'big') + result.encode() == msg2[2:] + + def test_tcp_framer_transaction_half2(self): + """Test a half completed tcp frame transaction.""" + count = 0 + result = None + def callback(data): + """Simulate callback.""" + nonlocal count, result + count += 1 + result = data + + msg1 = b"\x00\x01\x12\x34\x00\x06\xff" + msg2 = b"\x02\x01\x02\x00\x08" + self._tcp.processIncomingPacket(msg1, callback, [0, 1]) + assert not result + self._tcp.processIncomingPacket(msg2, callback, [0, 1]) + assert result + assert result.function_code.to_bytes(1,'big') + result.encode() == msg2 + + def test_tcp_framer_transaction_half3(self): + """Test a half completed tcp frame transaction.""" + count = 0 + result = None + def callback(data): + """Simulate callback.""" + nonlocal count, result + count += 1 + result = data + + msg1 = b"\x00\x01\x12\x34\x00\x06\xff\x02\x01\x02\x00" + msg2 = b"\x08" + self._tcp.processIncomingPacket(msg1, callback, [0, 1]) + assert not result + self._tcp.processIncomingPacket(msg2, callback, [0, 1]) + assert result + assert result.function_code.to_bytes(1,'big') + result.encode() == msg1[7:] + msg2 + + def test_tcp_framer_transaction_short(self): + """Test that we can get back on track after an invalid message.""" + count = 0 + result = None + def callback(data): + """Simulate callback.""" + nonlocal count, result + count += 1 + result = data + + # msg1 = b"\x99\x99\x99\x99\x00\x01\x00\x17" + msg1 = b'' + msg2 = b"\x00\x01\x12\x34\x00\x06\xff\x02\x01\x02\x00\x08" + self._tcp.processIncomingPacket(msg1, callback, [0, 1]) + assert not result + self._tcp.processIncomingPacket(msg2, callback, [0, 1]) + assert result + assert result.function_code.to_bytes(1,'big') + result.encode() == msg2[7:] + + def test_tcp_framer_populate(self): + """Test a tcp frame packet build.""" + count = 0 + result = None + def callback(data): + """Simulate callback.""" + nonlocal count, result + count += 1 + result = data + + expected = ModbusRequest(0, 0, 0, False) + expected.transaction_id = 0x0001 + expected.protocol_id = 0x1234 + expected.slave_id = 0xFF + msg = b"\x00\x01\x12\x34\x00\x06\xff\x02\x12\x34\x01\x02" + self._tcp.processIncomingPacket(msg, callback, [0, 1]) + # assert self._tcp.checkFrame() + # actual = ModbusRequest(0, 0, 0, False) + # self._tcp.populateResult(actual) + # for name in ("transaction_id", "protocol_id", "slave_id"): + # assert getattr(expected, name) == getattr(actual, name) + + def test_tcp_framer_packet(self): + """Test a tcp frame packet build.""" + old_encode = ModbusRequest.encode + ModbusRequest.encode = lambda self: b"" + message = ModbusRequest(0, 0, 0, False) + message.transaction_id = 0x0001 + message.protocol_id = 0x0000 + message.slave_id = 0xFF + message.function_code = 0x01 + expected = b"\x00\x01\x00\x00\x00\x02\xff\x01" + actual = self._tcp.buildPacket(message) + assert expected == actual + ModbusRequest.encode = old_encode + + # ----------------------------------------------------------------------- # + # TLS tests + # ----------------------------------------------------------------------- # + def test_framer_tls_framer_transaction_ready(self): + """Test a tls frame transaction.""" + count = 0 + result = None + def callback(data): + """Simulate callback.""" + nonlocal count, result + count += 1 + result = data + + msg = b"\x00\x01\x12\x34\x00\x06\xff\x02\x12\x34\x01\x02" + self._tcp.processIncomingPacket(msg[0:4], callback, [0, 1]) + assert not result + self._tcp.processIncomingPacket(msg[4:], callback, [0, 1]) + assert result + + def test_framer_tls_framer_transaction_full(self): + """Test a full tls frame transaction.""" + count = 0 + result = None + def callback(data): + """Simulate callback.""" + nonlocal count, result + count += 1 + result = data + + msg = b"\x00\x01\x12\x34\x00\x06\xff\x02\x12\x34\x01\x02" + self._tcp.processIncomingPacket(msg, callback, [0, 1]) + assert result + + def test_framer_tls_framer_transaction_half(self): + """Test a half completed tls frame transaction.""" + count = 0 + result = None + def callback(data): + """Simulate callback.""" + nonlocal count, result + count += 1 + result = data + + msg = b"\x00\x01\x12\x34\x00\x06\xff\x02\x12\x34\x01\x02" + self._tcp.processIncomingPacket(msg[0:8], callback, [0, 1]) + assert not result + self._tcp.processIncomingPacket(msg[8:], callback, [0, 1]) + assert result + + def test_framer_tls_framer_transaction_short(self): + """Test that we can get back on track after an invalid message.""" + count = 0 + result = None + def callback(data): + """Simulate callback.""" + nonlocal count, result + count += 1 + result = data + + msg = b"\x00\x01\x12\x34\x00\x06\xff\x02\x12\x34\x01\x02" + self._tcp.processIncomingPacket(msg[0:2], callback, [0, 1]) + assert not result + self._tcp.processIncomingPacket(msg[2:], callback, [0, 1]) + assert result + + def test_framer_tls_framer_decode(self): + """Testmessage decoding.""" + msg1 = b"" + msg2 = b"\x01\x12\x34\x00\x08" + result = self._tls.decode_data(msg1) + assert not result + result = self._tls.decode_data(msg2) + assert result == {"fcode": 1} + + def test_framer_tls_incoming_packet(self): + """Framer tls incoming packet.""" + msg = b"\x00\x01\x12\x34\x00\x06\xff\x02\x12\x34\x01\x02" + + slave = 0x01 + msg_result = None + + def mock_callback(result): + """Mock callback.""" + nonlocal msg_result + + msg_result = result.encode() + + self._tls.processIncomingPacket(msg, mock_callback, slave) + # assert msg == msg_result + + # self._tls.isFrameReady = mock.MagicMock(return_value=True) + # x = mock.MagicMock(return_value=False) + # self._tls._validate_slave_id = x + # self._tls.processIncomingPacket(msg, mock_callback, slave) + # assert not self._tls._buffer + # self._tls.advanceFrame() + # x = mock.MagicMock(return_value=True) + # self._tls._validate_slave_id = x + # self._tls.processIncomingPacket(msg, mock_callback, slave) + # assert msg[1:] == msg_result + # self._tls.advanceFrame() + + def test_framer_tls_process(self): + """Framer tls process.""" + # class MockResult: + # """Mock result.""" + + # def __init__(self, code): + # """Init.""" + # self.function_code = code + + # def mock_callback(_arg): + # """Mock callback.""" + + # self._tls.decoder.decode = mock.MagicMock(return_value=None) + # with pytest.raises(ModbusIOException): + # self._tls._process(mock_callback) + + # result = MockResult(0x01) + # self._tls.decoder.decode = mock.MagicMock(return_value=result) + # with pytest.raises(InvalidMessageReceivedException): + # self._tls._process( + # mock_callback, error=True + # ) + # self._tls._process(mock_callback) + # assert not self._tls._buffer + + def test_framer_tls_framer_populate(self): + """Test a tls frame packet build.""" + count = 0 + result = None + def callback(data): + """Simulate callback.""" + nonlocal count, result + count += 1 + result = data + + msg = b"\x00\x01\x12\x34\x00\x06\xff\x02\x12\x34\x01\x02" + self._tcp.processIncomingPacket(msg, callback, [0, 1]) + assert result + + def test_framer_tls_framer_packet(self): + """Test a tls frame packet build.""" + old_encode = ModbusRequest.encode + ModbusRequest.encode = lambda self: b"" + message = ModbusRequest(0, 0, 0, False) + message.function_code = 0x01 + expected = b"\x01" + actual = self._tls.buildPacket(message) + assert expected == actual + ModbusRequest.encode = old_encode + + # ----------------------------------------------------------------------- # + # RTU tests + # ----------------------------------------------------------------------- # + def test_rtu_framer_transaction_ready(self): + """Test if the checks for a complete frame work.""" + count = 0 + result = None + def callback(data): + """Simulate callback.""" + nonlocal count, result + count += 1 + result = data + + msg_parts = [b"\x00\x01\x00", b"\x00\x00\x01\xfc\x1b"] + self._rtu.processIncomingPacket(msg_parts[0], callback, [0, 1]) + assert not result + self._rtu.processIncomingPacket(msg_parts[1], callback, [0, 1]) + assert result + + def test_rtu_framer_transaction_full(self): + """Test a full rtu frame transaction.""" + count = 0 + result = None + def callback(data): + """Simulate callback.""" + nonlocal count, result + count += 1 + result = data + + msg = b"\x00\x01\x00\x00\x00\x01\xfc\x1b" + self._rtu.processIncomingPacket(msg, callback, [0, 1]) + assert result + + def test_rtu_framer_transaction_half(self): + """Test a half completed rtu frame transaction.""" + count = 0 + result = None + def callback(data): + """Simulate callback.""" + nonlocal count, result + count += 1 + result = data + + msg_parts = [b"\x00\x01\x00", b"\x00\x00\x01\xfc\x1b"] + self._rtu.processIncomingPacket(msg_parts[0], callback, [0, 1]) + assert not result + self._rtu.processIncomingPacket(msg_parts[1], callback, [0, 1]) + assert result + + def test_rtu_framer_populate(self): + """Test a rtu frame packet build.""" + count = 0 + result = None + def callback(data): + """Simulate callback.""" + nonlocal count, result + count += 1 + result = data + + msg = b"\x00\x01\x00\x00\x00\x01\xfc\x1b" + self._rtu.processIncomingPacket(msg, callback, [0, 1]) + header_dict = self._rtu._header # pylint: disable=protected-access + assert len(msg) == header_dict["len"] + assert int(msg[0]) == header_dict["uid"] + assert msg[-2:] == header_dict["crc"] + + def test_rtu_framer_packet(self): + """Test a rtu frame packet build.""" + old_encode = ModbusRequest.encode + ModbusRequest.encode = lambda self: b"" + message = ModbusRequest(0, 0, 0, False) + message.slave_id = 0xFF + message.function_code = 0x01 + expected = b"\xff\x01\x81\x80" # only header + CRC - no data + actual = self._rtu.buildPacket(message) + assert expected == actual + ModbusRequest.encode = old_encode + + def test_rtu_decode_exception(self): + """Test that the RTU framer can decode errors.""" + count = 0 + result = None + def callback(data): + """Simulate callback.""" + nonlocal count, result + count += 1 + result = data + + msg = b"\x00\x90\x02\x9c\x01" + self._rtu.processIncomingPacket(msg, callback, [0, 1]) + assert result + + def test_process(self): + """Test process.""" + count = 0 + result = None + def callback(data): + """Simulate callback.""" + nonlocal count, result + count += 1 + result = data + + msg = b"\x00\x01\x00\x00\x00\x01\xfc\x1b" + self._rtu.processIncomingPacket(msg, callback, [0, 1]) + assert result + + def test_rtu_process_incoming_packets(self): + """Test rtu process incoming packets.""" + count = 0 + result = None + def callback(data): + """Simulate callback.""" + nonlocal count, result + count += 1 + result = data + + msg = b"\x00\x01\x00\x00\x00\x01\xfc\x1b" + slave = 0x00 + + self._rtu.processIncomingPacket(msg, callback, slave) + assert result + + # ----------------------------------------------------------------------- # + # ASCII tests + # ----------------------------------------------------------------------- # + def test_ascii_framer_transaction_ready(self): + """Test a ascii frame transaction.""" + count = 0 + result = None + def callback(data): + """Simulate callback.""" + nonlocal count, result + count += 1 + result = data + + msg = b":F7031389000A60\r\n" + self._ascii.processIncomingPacket(msg, callback, [0,1]) + assert result + + def test_ascii_framer_transaction_full(self): + """Test a full ascii frame transaction.""" + count = 0 + result = None + def callback(data): + """Simulate callback.""" + nonlocal count, result + count += 1 + result = data + + msg = b"sss:F7031389000A60\r\n" + self._ascii.processIncomingPacket(msg, callback, [0,1]) + assert result + + def test_ascii_framer_transaction_half(self): + """Test a half completed ascii frame transaction.""" + count = 0 + result = None + def callback(data): + """Simulate callback.""" + nonlocal count, result + count += 1 + result = data + + msg_parts = (b"sss:F7031389", b"000A60\r\n") + self._ascii.processIncomingPacket(msg_parts[0], callback, [0,1]) + assert not result + self._ascii.processIncomingPacket(msg_parts[1], callback, [0,1]) + assert result + + def test_ascii_framer_populate(self): + """Test a ascii frame packet build.""" + request = ModbusRequest(0, 0, 0, False) + self._ascii.populateResult(request) + assert not request.slave_id + + def test_ascii_framer_packet(self): + """Test a ascii frame packet build.""" + old_encode = ModbusRequest.encode + ModbusRequest.encode = lambda self: b"" + message = ModbusRequest(0, 0, 0, False) + message.slave_id = 0xFF + message.function_code = 0x01 + expected = b":FF0100\r\n" + actual = self._ascii.buildPacket(message) + assert expected == actual + ModbusRequest.encode = old_encode + + def test_ascii_process_incoming_packets(self): + """Test ascii process incoming packet.""" + count = 0 + result = None + def callback(data): + """Simulate callback.""" + nonlocal count, result + count += 1 + result = data + + msg = b":F7031389000A60\r\n" + self._ascii.processIncomingPacket(msg, callback, [0,1]) + assert result diff --git a/test/message/test_tls.py b/test/framers/test_tls.py similarity index 87% rename from test/message/test_tls.py rename to test/framers/test_tls.py index 194fda459..50ccce800 100644 --- a/test/message/test_tls.py +++ b/test/framers/test_tls.py @@ -1,18 +1,18 @@ -"""Test transport.""" +"""Test framer.""" import pytest -from pymodbus.message.tls import MessageTLS +from pymodbus.framer.tls import FramerTLS -class TestMessageSocket: - """Test message module.""" +class TestMFramerTLS: + """Test module.""" @staticmethod @pytest.fixture(name="frame") def prepare_frame(): """Return message object.""" - return MessageTLS() + return FramerTLS() @pytest.mark.parametrize( diff --git a/test/message/conftest.py b/test/message/conftest.py deleted file mode 100644 index dcf2e3369..000000000 --- a/test/message/conftest.py +++ /dev/null @@ -1,43 +0,0 @@ -"""Configure pytest.""" -from __future__ import annotations - -from unittest import mock - -import pytest - -from pymodbus.message import Message, MessageType -from pymodbus.transport import CommParams, ModbusProtocol - - -class DummyMessage(Message): - """Implement use of ModbusProtocol.""" - - def __init__(self, - message_type: MessageType, - params: CommParams, - is_server: bool, - device_ids: list[int] | None, - ): - """Initialize a message instance.""" - super().__init__(message_type, params, is_server, device_ids) - self.send = mock.Mock() - self.message_type = message_type - - def callback_new_connection(self) -> ModbusProtocol: - """Call when listener receive new connection request.""" - return DummyMessage(self.message_type, self.comm_params, self.is_server, self.device_ids) # pragma: no cover - - def callback_connected(self) -> None: - """Call when connection is succcesfull.""" - - def callback_disconnected(self, exc: Exception | None) -> None: - """Call when connection is lost.""" - - def callback_request_response(self, data: bytes, device_id: int, tid: int) -> None: - """Handle received modbus request/response.""" - - -@pytest.fixture(name="dummy_message") -async def prepare_dummy_message(): - """Return message object.""" - return DummyMessage diff --git a/test/message/test_message.py b/test/message/test_message.py deleted file mode 100644 index 113ec8f8c..000000000 --- a/test/message/test_message.py +++ /dev/null @@ -1,328 +0,0 @@ -"""Test transport.""" - -from unittest import mock - -import pytest - -from pymodbus.message import MessageType -from pymodbus.message.ascii import MessageAscii -from pymodbus.message.rtu import MessageRTU -from pymodbus.message.socket import MessageSocket -from pymodbus.message.tls import MessageTLS -from pymodbus.transport import CommParams - - -class TestMessage: - """Test message module.""" - - @staticmethod - @pytest.fixture(name="msg") - async def prepare_message(dummy_message): - """Return message object.""" - return dummy_message( - MessageType.RAW, - CommParams(), - False, - [1], - ) - - - @pytest.mark.parametrize(("entry"), list(MessageType)) - async def test_message_init(self, entry, dummy_message): - """Test message type.""" - msg = dummy_message(entry.value, - CommParams(), - False, - [1], - ) - assert msg.msg_handle - - @pytest.mark.parametrize(("data", "res_len", "cx", "rc"), [ - (b'12345', 5, 1, [(5, 0, 0, b'12345')]), # full frame - (b'12345', 0, 0, [(0, 0, 0, b'')]), # not full frame, need more data - (b'12345', 5, 0, [(5, 0, 0, b'')]), # faulty frame, skipped - (b'1234512345', 10, 2, [(5, 0, 0, b'12345'), (5, 0, 0, b'12345')]), # 2 full frames - (b'12345678', 5, 1, [(5, 0, 0, b'12345'), (0, 0, 0, b'')]), # full frame, not full frame - (b'67812345', 8, 1, [(3, 0, 0, b''), (5, 0, 0, b'12345')]), # garble first, full frame next - (b'12345678', 5, 0, [(5, 0, 0, b''), (0, 0, 0, b'')]), # garble first, not full frame - (b'12345678', 8, 0, [(5, 0, 0, b''), (3, 0, 0, b'')]), # garble first, faulty frame - ]) - async def test_message_callback(self, msg, data, res_len, cx, rc): - """Test message type.""" - msg.callback_request_response = mock.Mock() - msg.msg_handle.decode = mock.MagicMock(side_effect=iter(rc)) - assert msg.callback_data(data) == res_len - assert msg.callback_request_response.call_count == cx - if cx: - msg.callback_request_response.assert_called_with(b'12345', 0, 0) - else: - msg.callback_request_response.assert_not_called() - - async def test_message_build_send(self, msg): - """Test message type.""" - msg.msg_handle.encode = mock.MagicMock(return_value=(b'decode')) - msg.build_send(b'decode', 1, 0) - msg.msg_handle.encode.assert_called_once() - msg.send.assert_called_once() - msg.send.assert_called_with(b'decode', None) - - @pytest.mark.parametrize( - ("dev_id", "res"), [ - (0, False), - (1, True), - (2, False), - ]) - async def test_validate_id(self, msg, dev_id, res): - """Test message type.""" - assert res == msg.validate_device_id(dev_id) - - @pytest.mark.parametrize( - ("data", "res_len", "res_id", "res_tid", "res_data"), [ - (b'\x00\x01', 0, 0, 0, b''), - (b'\x01\x02\x03', 3, 1, 2, b'\x03'), - (b'\x04\x05\x06\x07\x08\x09\x00\x01\x02\x03', 10, 4, 5, b'\x06\x07\x08\x09\x00\x01\x02\x03'), - ]) - async def test_decode(self, msg, data, res_id, res_tid, res_len, res_data): - """Test decode method in all types.""" - t_len, t_id, t_tid, t_data = msg.msg_handle.decode(data) - assert res_len == t_len - assert res_id == t_id - assert res_tid == t_tid - assert res_data == t_data - - @pytest.mark.parametrize( - ("data", "dev_id", "tid", "res_data"), [ - (b'\x01\x02', 5, 6, b'\x05\x06\x01\x02'), - (b'\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09', 17, 25, b'\x11\x19\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09'), - ]) - async def test_encode(self, msg, data, dev_id, tid, res_data): - """Test decode method in all types.""" - t_data = msg.msg_handle.encode(data, dev_id, tid) - assert res_data == t_data - - @pytest.mark.parametrize( - ("func", "lrc", "expect"), - [(MessageAscii.check_LRC, 0x1c, True), - (MessageAscii.check_LRC, 0x0c, False), - (MessageAscii.compute_LRC, None, 0x1c), - (MessageRTU.check_CRC, 0xE2DB, True), - (MessageRTU.check_CRC, 0xDBE2, False), - (MessageRTU.compute_CRC, None, 0xE2DB), - ] - ) - def test_LRC_CRC(self, func, lrc, expect): - """Test check_LRC.""" - data = b'\x12\x34\x23\x45\x34\x56\x45\x67' - assert expect == func(data, lrc) if lrc else func(data) - - def test_roundtrip_LRC(self): - """Test combined compute/check LRC.""" - data = b'\x12\x34\x23\x45\x34\x56\x45\x67' - assert MessageAscii.compute_LRC(data) == 0x1c - assert MessageAscii.check_LRC(data, 0x1C) - - def test_crc16_table(self): - """Test the crc16 table is prefilled.""" - assert len(MessageRTU.crc16_table) == 256 - assert isinstance(MessageRTU.crc16_table[0], int) - assert isinstance(MessageRTU.crc16_table[255], int) - - def test_roundtrip_CRC(self): - """Test combined compute/check CRC.""" - data = b'\x12\x34\x23\x45\x34\x56\x45\x67' - assert MessageRTU.compute_CRC(data) == 0xE2DB - assert MessageRTU.check_CRC(data, 0xE2DB) - - - -class TestMessages: - """Test message classes.""" - - @pytest.mark.parametrize( - ("frame", "frame_expected"), - [ - (MessageAscii, [ - b':0003007C00027F\r\n', - b':000304008D008EDE\r\n', - b':0083027B\r\n', - b':1103007C00026E\r\n', - b':110304008D008ECD\r\n', - b':1183026A\r\n', - b':FF03007C000280\r\n', - b':FF0304008D008EDF\r\n', - b':FF83027C\r\n', - ]), - (MessageRTU, [ - b'\x00\x03\x00\x7c\x00\x02\x04\x02', - b'\x00\x03\x04\x00\x8d\x00\x8e\xfa\xbc', - b'\x00\x83\x02\x91\x31', - b'\x11\x03\x00\x7c\x00\x02\x07\x43', - b'\x11\x03\x04\x00\x8d\x00\x8e\xfb\xbd', - b'\x11\x83\x02\xc1\x34', - b'\xff\x03\x00\x7c\x00\x02\x10\x0d', - b'\xff\x03\x04\x00\x8d\x00\x8e\xf5\xb3', - b'\xff\x83\x02\xa1\x01', - ]), - (MessageSocket, [ - b'\x00\x00\x00\x00\x00\x06\x00\x03\x00\x7c\x00\x02', - b'\x00\x00\x00\x00\x00\x07\x00\x03\x04\x00\x8d\x00\x8e', - b'\x00\x00\x00\x00\x00\x03\x00\x83\x02', - b'\x00\x00\x00\x00\x00\x06\x11\x03\x00\x7c\x00\x02', - b'\x00\x00\x00\x00\x00\x07\x11\x03\x04\x00\x8d\x00\x8e', - b'\x00\x00\x00\x00\x00\x03\x11\x83\x02', - b'\x00\x00\x00\x00\x00\x06\xff\x03\x00\x7c\x00\x02', - b'\x00\x00\x00\x00\x00\x07\xff\x03\x04\x00\x8d\x00\x8e', - b'\x00\x00\x00\x00\x00\x03\xff\x83\x02', - b'\x0c\x05\x00\x00\x00\x06\x00\x03\x00\x7c\x00\x02', - b'\x0c\x05\x00\x00\x00\x07\x00\x03\x04\x00\x8d\x00\x8e', - b'\x0c\x05\x00\x00\x00\x03\x00\x83\x02', - b'\x0c\x05\x00\x00\x00\x06\x11\x03\x00\x7c\x00\x02', - b'\x0c\x05\x00\x00\x00\x07\x11\x03\x04\x00\x8d\x00\x8e', - b'\x0c\x05\x00\x00\x00\x03\x11\x83\x02', - b'\x0c\x05\x00\x00\x00\x06\xff\x03\x00\x7c\x00\x02', - b'\x0c\x05\x00\x00\x00\x07\xff\x03\x04\x00\x8d\x00\x8e', - b'\x0c\x05\x00\x00\x00\x03\xff\x83\x02', - ]), - (MessageTLS, [ - b'\x03\x00\x7c\x00\x02', - b'\x03\x04\x00\x8d\x00\x8e', - b'\x83\x02', - ]), - ] - ) - @pytest.mark.parametrize( - ("inx1", "data"), - [ - (0, b"\x03\x00\x7c\x00\x02",), # Request - (1, b"\x03\x04\x00\x8d\x00\x8e",), # Response - (2, b'\x83\x02',), # Exception - ] - ) - @pytest.mark.parametrize( - ("inx2", "dev_id"), - [ - (0, 0), - (3, 17), - (6, 255), - ] - ) - @pytest.mark.parametrize( - ("inx3", "tid"), - [ - (0, 0), - (9, 3077), - ] - ) - def test_encode(self, frame, frame_expected, data, dev_id, tid, inx1, inx2, inx3): - """Test encode method.""" - if ((frame != MessageSocket and tid) or - (frame == MessageTLS and dev_id)): - return - frame_obj = frame() - expected = frame_expected[inx1 + inx2 + inx3] - encoded_data = frame_obj.encode(data, dev_id, tid) - assert encoded_data == expected - - @pytest.mark.parametrize( - ("msg_type", "data", "dev_id", "tid", "expected"), - [ - (MessageType.ASCII, b':0003007C00027F\r\n', 0, 0, b"\x03\x00\x7c\x00\x02",), # Request - (MessageType.ASCII, b':000304008D008EDE\r\n', 0, 0, b"\x03\x04\x00\x8d\x00\x8e",), # Response - (MessageType.ASCII, b':0083027B\r\n', 0, 0, b'\x83\x02',), # Exception - (MessageType.ASCII, b':1103007C00026E\r\n', 17, 0, b"\x03\x00\x7c\x00\x02",), # Request - (MessageType.ASCII, b':110304008D008ECD\r\n', 17, 0, b"\x03\x04\x00\x8d\x00\x8e",), # Response - (MessageType.ASCII, b':1183026A\r\n', 17, 0, b'\x83\x02',), # Exception - (MessageType.ASCII, b':FF03007C000280\r\n', 255, 0, b"\x03\x00\x7c\x00\x02",), # Request - (MessageType.ASCII, b':FF0304008D008EDF\r\n', 255, 0, b"\x03\x04\x00\x8d\x00\x8e",), # Response - (MessageType.ASCII, b':FF83027C\r\n', 255, 0, b'\x83\x02',), # Exception - (MessageType.RTU, b'\x00\x03\x00\x7c\x00\x02\x04\x02', 0, 0, b"\x03\x00\x7c\x00\x02",), # Request - (MessageType.RTU, b'\x00\x03\x04\x00\x8d\x00\x8e\xfa\xbc', 0, 0, b"\x03\x04\x00\x8d\x00\x8e",), # Response - (MessageType.RTU, b'\x00\x83\x02\x91\x31', 0, 0, b'\x83\x02',), # Exception - (MessageType.RTU, b'\x11\x03\x00\x7c\x00\x02\x07\x43', 17, 0, b"\x03\x00\x7c\x00\x02",), # Request - (MessageType.RTU, b'\x11\x03\x04\x00\x8d\x00\x8e\xfb\xbd', 17, 0, b"\x03\x04\x00\x8d\x00\x8e",), # Response - (MessageType.RTU, b'\x11\x83\x02\xc1\x34', 17, 0, b'\x83\x02',), # Exception - (MessageType.RTU, b'\xff\x03\x00|\x00\x02\x10\x0d', 255, 0, b"\x03\x00\x7c\x00\x02",), # Request - (MessageType.RTU, b'\xff\x03\x04\x00\x8d\x00\x8e\xf5\xb3', 255, 0, b"\x03\x04\x00\x8d\x00\x8e",), # Response - (MessageType.RTU, b'\xff\x83\x02\xa1\x01', 255, 0, b'\x83\x02',), # Exception - (MessageType.SOCKET, b'\x00\x00\x00\x00\x00\x06\x00\x03\x00\x7c\x00\x02', 0, 0, b"\x03\x00\x7c\x00\x02",), # Request - (MessageType.SOCKET, b'\x00\x00\x00\x00\x00\x07\x00\x03\x04\x00\x8d\x00\x8e', 0, 0, b"\x03\x04\x00\x8d\x00\x8e",), # Response - (MessageType.SOCKET, b'\x00\x00\x00\x00\x00\x03\x00\x83\x02', 0, 0, b'\x83\x02',), # Exception - (MessageType.SOCKET, b'\x00\x00\x00\x00\x00\x06\x11\x03\x00\x7c\x00\x02', 17, 0, b"\x03\x00\x7c\x00\x02",), # Request - (MessageType.SOCKET, b'\x00\x00\x00\x00\x00\x07\x11\x03\x04\x00\x8d\x00\x8e', 17, 0, b"\x03\x04\x00\x8d\x00\x8e",), # Response - (MessageType.SOCKET, b'\x00\x00\x00\x00\x00\x03\x11\x83\x02', 17, 0, b'\x83\x02',), # Exception - (MessageType.SOCKET, b'\x00\x00\x00\x00\x00\x06\xff\x03\x00\x7c\x00\x02', 255, 0, b"\x03\x00\x7c\x00\x02",), # Request - (MessageType.SOCKET, b'\x00\x00\x00\x00\x00\x07\xff\x03\x04\x00\x8d\x00\x8e', 255, 0, b"\x03\x04\x00\x8d\x00\x8e",), # Response - (MessageType.SOCKET, b'\x00\x00\x00\x00\x00\x03\xff\x83\x02', 255, 0, b'\x83\x02',), # Exception - (MessageType.SOCKET, b'\x0c\x05\x00\x00\x00\x06\x00\x03\x00\x7c\x00\x02', 0, 3077, b"\x03\x00\x7c\x00\x02",), # Request - (MessageType.SOCKET, b'\x0c\x05\x00\x00\x00\x07\x00\x03\x04\x00\x8d\x00\x8e', 0, 3077, b"\x03\x04\x00\x8d\x00\x8e",), # Response - (MessageType.SOCKET, b'\x0c\x05\x00\x00\x00\x03\x00\x83\x02', 0, 3077, b'\x83\x02',), # Exception - (MessageType.SOCKET, b'\x0c\x05\x00\x00\x00\x06\x11\x03\x00\x7c\x00\x02', 17, 3077, b"\x03\x00\x7c\x00\x02",), # Request - (MessageType.SOCKET, b'\x0c\x05\x00\x00\x00\x07\x11\x03\x04\x00\x8d\x00\x8e', 17, 3077, b"\x03\x04\x00\x8d\x00\x8e",), # Response - (MessageType.SOCKET, b'\x0c\x05\x00\x00\x00\x03\x11\x83\x02', 17, 3077, b'\x83\x02',), # Exception - (MessageType.SOCKET, b'\x0c\x05\x00\x00\x00\x06\xff\x03\x00\x7c\x00\x02', 255, 3077, b"\x03\x00\x7c\x00\x02",), # Request - (MessageType.SOCKET, b'\x0c\x05\x00\x00\x00\x07\xff\x03\x04\x00\x8d\x00\x8e', 255, 3077, b"\x03\x04\x00\x8d\x00\x8e",), # Response - (MessageType.SOCKET, b'\x0c\x05\x00\x00\x00\x03\xff\x83\x02', 255, 3077, b'\x83\x02',), # Exception - (MessageType.TLS, b'\x03\x00\x7c\x00\x02', 0, 0, b"\x03\x00\x7c\x00\x02",), # Request - (MessageType.TLS, b'\x03\x04\x00\x8d\x00\x8e', 0, 0, b"\x03\x04\x00\x8d\x00\x8e",), # Response - (MessageType.TLS, b'\x83\x02', 0, 0, b'\x83\x02',), # Exception - ] - ) - @pytest.mark.parametrize( - ("split"), - [ - "no", - "half", - "single", - ] - ) - async def test_decode(self, dummy_message, msg_type, data, dev_id, tid, expected, split): - """Test encode method.""" - if msg_type == MessageType.RTU: - pytest.skip("Waiting on implementation!") - if msg_type == MessageType.TLS and split != "no": - return - frame = dummy_message( - msg_type, - CommParams(), - False, - [1], - ) - frame.callback_request_response = mock.Mock() - if split == "no": - used_len = frame.callback_data(data) - - elif split == "half": - split_len = int(len(data) / 2) - assert not frame.callback_data(data[0:split_len]) - frame.callback_request_response.assert_not_called() - used_len = frame.callback_data(data) - else: - last = len(data) - for i in range(0, last -1): - assert not frame.callback_data(data[0:i+1]) - frame.callback_request_response.assert_not_called() - used_len = frame.callback_data(data) - assert used_len == len(data) - frame.callback_request_response.assert_called_with(expected, dev_id, tid) - - @pytest.mark.parametrize( - ("frame", "data", "exp_len"), - [ - (MessageAscii, b':0003007C00017F\r\n', 17), # bad crc - # (MessageAscii, b'abc:0003007C00027F\r\n', 3), # garble in front - # (MessageAscii, b':0003007C00017F\r\nabc', 17), # bad crc, garble after - # (MessageAscii, b':0003007C00017F\r\n:0003', 17), # part second message - (MessageRTU, b'\x00\x83\x02\x91\x31', 0), # bad crc - # (MessageRTU, b'\x00\x83\x02\x91\x31', 0), # garble in front - # (MessageRTU, b'\x00\x83\x02\x91\x31', 0), # garble after - # (MessageRTU, b'\x00\x83\x02\x91\x31', 0), # part second message - ] - ) - async def test_decode_bad_crc(self, frame, data, exp_len): - """Test encode method.""" - if frame == MessageRTU: - pytest.skip("Waiting for implementation.") - frame_obj = frame() - used_len, _, _, data = frame_obj.decode(data) - assert used_len == exp_len - assert not data diff --git a/test/message/test_rtu.py b/test/message/test_rtu.py deleted file mode 100644 index 369fe3624..000000000 --- a/test/message/test_rtu.py +++ /dev/null @@ -1,31 +0,0 @@ -"""Test transport.""" -import pytest - -from pymodbus.message.rtu import MessageRTU - - -class TestMessageRTU: - """Test message module.""" - - @staticmethod - @pytest.fixture(name="frame") - def prepare_frame(): - """Return message object.""" - return MessageRTU() - - - @pytest.mark.parametrize( - ("data", "dev_id", "res_msg"), - [ - (b'\x01\x01\x00', 2, b'\x02\x01\x01\x00\x51\xcc'), - (b'\x03\x06\xAE\x41\x56\x52\x43\x40', 17, b'\x11\x03\x06\xAE\x41\x56\x52\x43\x40\x49\xAD'), - (b'\x01\x03\x01\x00\x0a', 1, b'\x01\x01\x03\x01\x00\x0a\xed\x89'), - ], - ) - def xtest_roundtrip(self, frame, data, dev_id, res_msg): - """Test encode.""" - msg = frame.encode(data, dev_id, 0) - res_len, _, res_id, res_data = frame.decode(msg) - assert data == res_data - assert dev_id == res_id - assert res_len == len(res_msg) diff --git a/test/sub_client/test_client.py b/test/sub_client/test_client.py index 6e0ee174c..6111dfb59 100755 --- a/test/sub_client/test_client.py +++ b/test/sub_client/test_client.py @@ -6,22 +6,23 @@ import pytest -import pymodbus.bit_read_message as pdu_bit_read -import pymodbus.bit_write_message as pdu_bit_write import pymodbus.client as lib_client -import pymodbus.diag_message as pdu_diag -import pymodbus.file_message as pdu_file_msg -import pymodbus.other_message as pdu_other_msg -import pymodbus.register_read_message as pdu_reg_read -import pymodbus.register_write_message as pdu_req_write -from pymodbus import Framer +import pymodbus.pdu.bit_read_message as pdu_bit_read +import pymodbus.pdu.bit_write_message as pdu_bit_write +import pymodbus.pdu.diag_message as pdu_diag +import pymodbus.pdu.file_message as pdu_file_msg +import pymodbus.pdu.other_message as pdu_other_msg +import pymodbus.pdu.register_read_message as pdu_reg_read +import pymodbus.pdu.register_write_message as pdu_req_write +from examples.helper import get_certificate +from pymodbus import FramerType from pymodbus.client.base import ModbusBaseClient from pymodbus.client.mixin import ModbusClientMixin from pymodbus.datastore import ModbusSlaveContext from pymodbus.datastore.store import ModbusSequentialDataBlock from pymodbus.exceptions import ConnectionException, ModbusException, ModbusIOException from pymodbus.pdu import ModbusRequest -from pymodbus.transport import CommType +from pymodbus.transport import CommParams, CommType BASE_PORT = 6500 @@ -121,18 +122,12 @@ def fake_execute(_self, request): "opt_args": { "timeout": 3 + 2, "retries": 3 + 2, - "retry_on_empty": True, - "strict": False, - "broadcast_enable": not False, "reconnect_delay": 117, "reconnect_delay_max": 250, }, "defaults": { "timeout": 3, "retries": 3, - "retry_on_empty": False, - "strict": True, - "broadcast_enable": False, "reconnect_delay": 100, "reconnect_delay_max": 1000 * 60 * 5, }, @@ -140,7 +135,7 @@ def fake_execute(_self, request): "serial": { "pos_arg": "/dev/tty", "opt_args": { - "framer": Framer.ASCII, + "framer": FramerType.ASCII, "baudrate": 19200 + 500, "bytesize": 8 - 1, "parity": "E", @@ -148,9 +143,8 @@ def fake_execute(_self, request): "handle_local_echo": True, }, "defaults": { - "host": None, "port": "/dev/tty", - "framer": Framer.RTU, + "framer": FramerType.RTU, "baudrate": 19200, "bytesize": 8, "parity": "N", @@ -162,13 +156,12 @@ def fake_execute(_self, request): "pos_arg": "192.168.1.2", "opt_args": { "port": 112, - "framer": Framer.ASCII, + "framer": FramerType.ASCII, "source_address": ("195.6.7.8", 1025), }, "defaults": { - "host": "192.168.1.2", "port": 502, - "framer": Framer.SOCKET, + "framer": FramerType.SOCKET, "source_address": None, }, }, @@ -176,35 +169,27 @@ def fake_execute(_self, request): "pos_arg": "192.168.1.2", "opt_args": { "port": 211, - "framer": Framer.ASCII, + "framer": FramerType.ASCII, "source_address": ("195.6.7.8", 1025), "sslctx": None, - "certfile": None, - "keyfile": None, - "password": None, }, "defaults": { - "host": "192.168.1.2", "port": 802, - "framer": Framer.TLS, + "framer": FramerType.TLS, "source_address": None, "sslctx": None, - "certfile": None, - "keyfile": None, - "password": None, }, }, "udp": { "pos_arg": "192.168.1.2", "opt_args": { "port": 121, - "framer": Framer.ASCII, + "framer": FramerType.ASCII, "source_address": ("195.6.7.8", 1025), }, "defaults": { - "host": "192.168.1.2", "port": 502, - "framer": Framer.SOCKET, + "framer": FramerType.SOCKET, "source_address": None, }, }, @@ -235,15 +220,12 @@ async def test_client_instanciate( cur_args = arg_list[type_args] if test_default: client = clientclass(cur_args["pos_arg"]) - to_test = dict(arg_list["fix"]["defaults"], **cur_args["defaults"]) else: client = clientclass( cur_args["pos_arg"], **arg_list["fix"]["opt_args"], **cur_args["opt_args"], ) - to_test = dict(arg_list["fix"]["opt_args"], **cur_args["opt_args"]) - to_test["host"] = cur_args["defaults"]["host"] # Test information methods client.last_frame_end = 2 @@ -261,7 +243,7 @@ async def test_client_instanciate( client.connect = lambda: False client.transport = None with pytest.raises(ConnectionException): - client.execute() + client.execute(ModbusRequest(0, 0, 0, False)) async def test_serial_not_installed(): """Try to instantiate clients.""" @@ -276,10 +258,14 @@ async def test_serial_not_installed(): async def test_client_modbusbaseclient(): """Test modbus base client class.""" client = ModbusBaseClient( - Framer.ASCII, - host="localhost", - port=BASE_PORT + 1, - CommType=CommType.TCP, + FramerType.ASCII, + 3, + None, + comm_params=CommParams( + host="localhost", + port=BASE_PORT + 1, + comm_type=CommType.TCP, + ), ) client.register(pdu_bit_read.ReadCoilsResponse) assert str(client) @@ -293,7 +279,7 @@ async def test_client_connection_made(): transport = mock.AsyncMock() transport.close = lambda : () - client.connection_made(transport) + client.ctx.connection_made(transport) # assert await client.connected client.close() @@ -311,10 +297,14 @@ async def test_client_base_async(): p_close.return_value = asyncio.Future() p_close.return_value.set_result(True) async with ModbusBaseClient( - Framer.ASCII, - host="localhost", - port=BASE_PORT + 2, - CommType=CommType.TCP, + FramerType.ASCII, + 3, + None, + comm_params=CommParams( + host="localhost", + port=BASE_PORT + 2, + comm_type=CommType.TCP, + ), ) as client: str(client) p_connect.return_value = asyncio.Future() @@ -326,9 +316,14 @@ async def test_client_base_async(): @pytest.mark.skip() async def test_client_protocol_receiver(): """Test the client protocol data received.""" - base = ModbusBaseClient(Framer.SOCKET) + base = ModbusBaseClient( + FramerType.SOCKET, + 3, + None, + comm_params=CommParams(), + ) transport = mock.MagicMock() - base.connection_made(transport) + base.ctx.connection_made(transport) assert base.transport == transport assert base.transport data = b"\x00\x00\x12\x34\x00\x06\xff\x01\x01\x02\x00\x04" @@ -336,7 +331,7 @@ async def test_client_protocol_receiver(): # setup existing request assert not list(base.transaction) response = base.build_response(0x00) # pylint: disable=protected-access - base.data_received(data) + base.ctx.data_received(data) result = response.result() assert isinstance(result, pdu_bit_read.ReadCoilsResponse) @@ -348,7 +343,12 @@ async def test_client_protocol_receiver(): @pytest.mark.skip() async def test_client_protocol_response(): """Test the udp client protocol builds responses.""" - base = ModbusBaseClient(Framer.SOCKET) + base = ModbusBaseClient( + FramerType.SOCKET, + 3, + None, + comm_params=CommParams(), + ) response = base.build_response(0x00) # pylint: disable=protected-access excp = response.exception() assert isinstance(excp, ConnectionException) @@ -362,16 +362,23 @@ async def test_client_protocol_response(): async def test_client_protocol_handler(): """Test the client protocol handles responses.""" base = ModbusBaseClient( - Framer.ASCII, host="localhost", port=+3, CommType=CommType.TCP + FramerType.ASCII, + 3, + None, + comm_params=CommParams( + host="localhost", + port=BASE_PORT + 3, + comm_type=CommType.TCP, + ), ) transport = mock.MagicMock() - base.connection_made(transport=transport) + base.ctx.connection_made(transport=transport) reply = pdu_bit_read.ReadCoilsRequest(1, 1) reply.transaction_id = 0x00 - base._handle_response(None) # pylint: disable=protected-access - base._handle_response(reply) # pylint: disable=protected-access - response = base.build_response(0x00) # pylint: disable=protected-access - base._handle_response(reply) # pylint: disable=protected-access + base.ctx._handle_response(None) # pylint: disable=protected-access + base.ctx._handle_response(reply) # pylint: disable=protected-access + response = base.build_response(reply) # pylint: disable=protected-access + base.ctx._handle_response(reply) # pylint: disable=protected-access result = response.result() assert result == reply @@ -392,8 +399,8 @@ 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.framer.buildPacket(resp) - self.base.data_received(pkt) + pkt = self.base.ctx.framer.buildPacket(resp) + self.base.ctx.data_received(pkt) def write(self, data, addr=None): """Write data to the transport, start a task to send the response.""" @@ -410,10 +417,18 @@ def close(self): async def test_client_protocol_execute(): """Test the client protocol execute method.""" - base = ModbusBaseClient(Framer.SOCKET, host="127.0.0.1") + base = ModbusBaseClient( + FramerType.SOCKET, + 3, + None, + comm_params=CommParams( + host="127.0.0.1", + timeout_connect=3, + ), + ) request = pdu_bit_read.ReadCoilsRequest(1, 1) transport = MockTransport(base, request) - base.connection_made(transport=transport) + base.ctx.connection_made(transport=transport) response = await base.async_execute(request) assert not response.isError() @@ -421,20 +436,35 @@ async def test_client_protocol_execute(): async def test_client_execute_broadcast(): """Test the client protocol execute method.""" - base = ModbusBaseClient(Framer.SOCKET, host="127.0.0.1") - base.broadcast_enable = True + base = ModbusBaseClient( + FramerType.SOCKET, + 3, + None, + comm_params=CommParams( + host="127.0.0.1", + ), + ) request = pdu_bit_read.ReadCoilsRequest(1, 1) transport = MockTransport(base, request) - base.connection_made(transport=transport) + base.ctx.connection_made(transport=transport) - assert not await base.async_execute(request) + with pytest.raises(ModbusIOException): + assert not await base.async_execute(request) async def test_client_protocol_retry(): """Test the client protocol execute method with retries.""" - base = ModbusBaseClient(Framer.SOCKET, host="127.0.0.1", timeout=0.1) + base = ModbusBaseClient( + FramerType.SOCKET, + 3, + None, + comm_params=CommParams( + host="127.0.0.1", + timeout_connect=0.1, + ), + ) request = pdu_bit_read.ReadCoilsRequest(1, 1) transport = MockTransport(base, request, retries=2) - base.connection_made(transport=transport) + base.ctx.connection_made(transport=transport) response = await base.async_execute(request) assert transport.retries == 0 @@ -444,12 +474,20 @@ async def test_client_protocol_retry(): async def test_client_protocol_timeout(): """Test the client protocol execute method with timeout.""" - base = ModbusBaseClient(Framer.SOCKET, host="127.0.0.1", timeout=0.1, retries=2) + base = ModbusBaseClient( + FramerType.SOCKET, + 2, + None, + comm_params=CommParams( + host="127.0.0.1", + timeout_connect=0.1, + ), + ) # Avoid creating do_reconnect() task - base.connection_lost = mock.MagicMock() + base.ctx.connection_lost = mock.MagicMock() request = pdu_bit_read.ReadCoilsRequest(1, 1) transport = MockTransport(base, request, retries=4) - base.connection_made(transport=transport) + base.ctx.connection_made(transport=transport) with pytest.raises(ModbusIOException): await base.async_execute(request) @@ -516,25 +554,40 @@ def test_client_tcp_reuse(): def test_client_tls_connect(): """Test the tls client connection method.""" + sslctx=lib_client.ModbusTlsClient.generate_ssl( + certfile=get_certificate("crt"), + keyfile=get_certificate("key"), + ) with mock.patch.object(ssl.SSLSocket, "connect") as mock_method: - client = lib_client.ModbusTlsClient("127.0.0.1") + client = lib_client.ModbusTlsClient( + "127.0.0.1", + sslctx=sslctx, + ) assert client.connect() with mock.patch.object(socket, "create_connection") as mock_method: mock_method.side_effect = OSError() - client = lib_client.ModbusTlsClient("127.0.0.1") + client = lib_client.ModbusTlsClient("127.0.0.1", sslctx=sslctx) assert not client.connect() def test_client_tls_connect2(): """Test the tls client connection method.""" + sslctx=lib_client.ModbusTlsClient.generate_ssl( + certfile=get_certificate("crt"), + keyfile=get_certificate("key"), + ) with mock.patch.object(ssl.SSLSocket, "connect") as mock_method: - client = lib_client.ModbusTlsClient("127.0.0.1", source_address=("0.0.0.0", 0)) + client = lib_client.ModbusTlsClient( + "127.0.0.1", + sslctx=sslctx, + source_address=("0.0.0.0", 0) + ) assert client.connect() with mock.patch.object(socket, "create_connection") as mock_method: mock_method.side_effect = OSError() - client = lib_client.ModbusTlsClient("127.0.0.1") + client = lib_client.ModbusTlsClient("127.0.0.1", sslctx=sslctx) assert not client.connect() @@ -629,15 +682,20 @@ def test_client_mixin_convert_fail(): async def test_client_build_response(): """Test fail of build_response.""" - client = ModbusBaseClient(Framer.RTU) + client = ModbusBaseClient( + FramerType.RTU, + 3, + None, + comm_params=CommParams(), + ) with pytest.raises(ConnectionException): - await client.build_response(0) + await client.build_response(ModbusRequest(0, 0, 0, False)) async def test_client_mixin_execute(): """Test dummy execute for both sync and async.""" client = ModbusClientMixin() with pytest.raises(NotImplementedError): - client.execute(ModbusRequest()) + client.execute(ModbusRequest(0, 0, 0, False)) with pytest.raises(NotImplementedError): - await client.execute(ModbusRequest()) + await client.execute(ModbusRequest(0, 0, 0, False)) diff --git a/test/sub_client/test_client_sync.py b/test/sub_client/test_client_sync.py index b2c413358..90f5ee862 100755 --- a/test/sub_client/test_client_sync.py +++ b/test/sub_client/test_client_sync.py @@ -6,7 +6,7 @@ import pytest import serial -from pymodbus import Framer +from pymodbus import FramerType from pymodbus.client import ( ModbusSerialClient, ModbusTcpClient, @@ -16,7 +16,6 @@ from pymodbus.exceptions import ConnectionException from pymodbus.transaction import ( ModbusAsciiFramer, - ModbusBinaryFramer, ModbusRtuFramer, ModbusSocketFramer, ModbusTlsFramer, @@ -219,7 +218,7 @@ def test_syn_tls_client_instantiation(self): client = ModbusTlsClient("127.0.0.1") assert client assert isinstance(client.framer, ModbusTlsFramer) - assert client.sslctx + assert client.comm_params.sslctx @mock.patch("pymodbus.client.tcp.select") def test_basic_syn_tls_client(self, mock_select): @@ -281,7 +280,7 @@ def test_tls_client_repr(self): client = ModbusTlsClient("127.0.0.1") rep = ( f"<{client.__class__.__name__} at {hex(id(client))} socket={client.socket}, " - f"ipaddr={client.comm_params.host}, port={client.comm_params.port}, sslctx={client.sslctx}, " + f"ipaddr={client.comm_params.host}, port={client.comm_params.port}, sslctx={client.comm_params.sslctx}, " f"timeout={client.comm_params.timeout_connect}>" ) assert repr(client) == rep @@ -307,27 +306,23 @@ def test_sync_serial_client_instantiation(self): client = ModbusSerialClient("/dev/null") assert client assert isinstance( - ModbusSerialClient("/dev/null", framer=Framer.ASCII).framer, + ModbusSerialClient("/dev/null", framer=FramerType.ASCII).framer, ModbusAsciiFramer, ) assert isinstance( - ModbusSerialClient("/dev/null", framer=Framer.RTU).framer, + ModbusSerialClient("/dev/null", framer=FramerType.RTU).framer, ModbusRtuFramer, ) assert isinstance( - ModbusSerialClient("/dev/null", framer=Framer.BINARY).framer, - ModbusBinaryFramer, - ) - assert isinstance( - ModbusSerialClient("/dev/null", framer=Framer.SOCKET).framer, + ModbusSerialClient("/dev/null", framer=FramerType.SOCKET).framer, ModbusSocketFramer, ) def test_sync_serial_rtu_client_timeouts(self): """Test sync serial rtu.""" - client = ModbusSerialClient("/dev/null", framer=Framer.RTU, baudrate=9600) + client = ModbusSerialClient("/dev/null", framer=FramerType.RTU, baudrate=9600) assert client.silent_interval == round((3.5 * 10 / 9600), 6) - client = ModbusSerialClient("/dev/null", framer=Framer.RTU, baudrate=38400) + client = ModbusSerialClient("/dev/null", framer=FramerType.RTU, baudrate=38400) assert client.silent_interval == round((1.75 / 1000), 6) @mock.patch("serial.Serial") @@ -352,7 +347,7 @@ def test_basic_sync_serial_client(self, mock_serial): client.close() # rtu connect/disconnect - rtu_client = ModbusSerialClient("/dev/null", framer=Framer.RTU, strict=True) + rtu_client = ModbusSerialClient("/dev/null", framer=FramerType.RTU) assert rtu_client.connect() assert rtu_client.socket.inter_byte_timeout == rtu_client.inter_byte_timeout rtu_client.close() diff --git a/test/sub_examples/test_client_server_async.py b/test/sub_examples/test_client_server_async.py index 674b4fa66..92446785d 100755 --- a/test/sub_examples/test_client_server_async.py +++ b/test/sub_examples/test_client_server_async.py @@ -56,6 +56,12 @@ async def test_client_exception(self, mock_server, mock_clc): ) await run_async_client(test_client, modbus_calls=run_a_few_calls) + async def test_client_no_calls(self, mock_server, mock_clc): + """Run async client and server.""" + assert mock_server + test_client = setup_async_client(cmdline=mock_clc) + await run_async_client(test_client, modbus_calls=None) + async def test_server_no_client(self, mock_server): """Run async server without client.""" assert mock_server @@ -76,3 +82,9 @@ async def test_client_no_server(self, mock_clc): test_client = setup_async_client(cmdline=mock_clc) with pytest.raises((AssertionError, asyncio.TimeoutError)): await run_async_client(test_client, modbus_calls=run_a_few_calls) + +async def test_illegal_commtype(): + """Run async client and server.""" + with pytest.raises(RuntimeError): + setup_async_client(cmdline=["--comm", "unknown", "--framer", "rtu", "--port", "5912"] +) diff --git a/test/sub_examples/test_examples.py b/test/sub_examples/test_examples.py index 65442ed6d..446190a75 100755 --- a/test/sub_examples/test_examples.py +++ b/test/sub_examples/test_examples.py @@ -18,7 +18,6 @@ from examples.client_custom_msg import main as main_custom_client from examples.client_payload import main as main_payload_calls from examples.datastore_simulator_share import main as main_datastore_simulator_share -from examples.message_generator import generate_messages from examples.message_parser import main as main_parse_messages from examples.server_async import setup_server from examples.server_callback import run_callback_server @@ -43,12 +42,7 @@ def get_port_in_class(base_ports): base_ports[__class__.__name__] += 1 return base_ports[__class__.__name__] - @pytest.mark.parametrize("framer", ["socket", "rtu", "ascii", "binary"]) - def test_message_generator(self, framer): - """Test all message generator.""" - generate_messages(cmdline=["--framer", framer]) - - @pytest.mark.parametrize("framer", ["socket", "rtu", "ascii", "binary"]) + @pytest.mark.parametrize("framer", ["socket", "rtu", "ascii"]) def test_message_parser(self, framer): """Test message parser.""" main_parse_messages(["--framer", framer, "-m", "000100000006010100200001"]) diff --git a/test/sub_function_codes/test_all_messages.py b/test/sub_function_codes/test_all_messages.py index 2ab64d879..3454cf191 100644 --- a/test/sub_function_codes/test_all_messages.py +++ b/test/sub_function_codes/test_all_messages.py @@ -1,17 +1,17 @@ """Test all messages.""" -from pymodbus.bit_read_message import ( +from pymodbus.pdu.bit_read_message import ( ReadCoilsRequest, ReadCoilsResponse, ReadDiscreteInputsRequest, ReadDiscreteInputsResponse, ) -from pymodbus.bit_write_message import ( +from pymodbus.pdu.bit_write_message import ( WriteMultipleCoilsRequest, WriteMultipleCoilsResponse, WriteSingleCoilRequest, WriteSingleCoilResponse, ) -from pymodbus.register_read_message import ( +from pymodbus.pdu.register_read_message import ( ReadHoldingRegistersRequest, ReadHoldingRegistersResponse, ReadInputRegistersRequest, @@ -19,7 +19,7 @@ ReadWriteMultipleRegistersRequest, ReadWriteMultipleRegistersResponse, ) -from pymodbus.register_write_message import ( +from pymodbus.pdu.register_write_message import ( WriteMultipleRegistersRequest, WriteMultipleRegistersResponse, WriteSingleRegisterRequest, @@ -82,14 +82,14 @@ def test_initializing_slave_address_response(self): response = factory(slave_id) assert response.slave_id == slave_id - def test_forwarding_kwargs_to_pdu(self): - """Test that the kwargs are forwarded to the pdu correctly.""" - request = ReadCoilsRequest(1, 5, slave=0x12, transaction=0x12, protocol=0x12) + def test_forwarding_to_pdu(self): + """Test that parameters are forwarded to the pdu correctly.""" + request = ReadCoilsRequest(1, 5, slave=18, transaction=0x12, protocol=0x12) assert request.slave_id == 0x12 assert request.transaction_id == 0x12 assert request.protocol_id == 0x12 request = ReadCoilsRequest(1, 5) - assert not request.slave_id + assert request.slave_id == 1 assert not request.transaction_id assert not request.protocol_id diff --git a/test/sub_function_codes/test_bit_read_messages.py b/test/sub_function_codes/test_bit_read_messages.py index fc0b4de76..99c095560 100644 --- a/test/sub_function_codes/test_bit_read_messages.py +++ b/test/sub_function_codes/test_bit_read_messages.py @@ -8,13 +8,13 @@ """ import struct -from pymodbus.bit_read_message import ( +from pymodbus.pdu import ModbusExceptions +from pymodbus.pdu.bit_read_message import ( ReadBitsRequestBase, ReadBitsResponseBase, ReadCoilsRequest, ReadDiscreteInputsRequest, ) -from pymodbus.pdu import ModbusExceptions from test.conftest import MockContext @@ -40,17 +40,17 @@ def tearDown(self): def test_read_bit_base_class_methods(self): """Test basic bit message encoding/decoding.""" - handle = ReadBitsRequestBase(1, 1) + handle = ReadBitsRequestBase(1, 1, 0, 0, 0, False) msg = "ReadBitRequest(1,1)" assert msg == str(handle) - handle = ReadBitsResponseBase([1, 1]) + handle = ReadBitsResponseBase([1, 1], 0, 0, 0, False) msg = "ReadBitsResponseBase(2)" assert msg == str(handle) def test_bit_read_base_request_encoding(self): """Test basic bit message encoding/decoding.""" for i in range(20): - handle = ReadBitsRequestBase(i, i) + handle = ReadBitsRequestBase(i, i, 0, 0, 0, False) result = struct.pack(">HH", i, i) assert handle.encode() == result handle.decode(result) @@ -60,7 +60,7 @@ def test_bit_read_base_response_encoding(self): """Test basic bit message encoding/decoding.""" for i in range(20): data = [True] * i - handle = ReadBitsResponseBase(data) + handle = ReadBitsResponseBase(data, 0, 0, 0, False) result = handle.encode() handle.decode(result) assert handle.bits[:i] == data @@ -68,7 +68,7 @@ def test_bit_read_base_response_encoding(self): def test_bit_read_base_response_helper_methods(self): """Test the extra methods on a ReadBitsResponseBase.""" data = [False] * 8 - handle = ReadBitsResponseBase(data) + handle = ReadBitsResponseBase(data, 0, 0, 0, False) for i in (1, 3, 5): handle.setBit(i, True) for i in (1, 3, 5): @@ -79,8 +79,8 @@ def test_bit_read_base_response_helper_methods(self): def test_bit_read_base_requests(self): """Test bit read request encoding.""" messages = { - ReadBitsRequestBase(12, 14): b"\x00\x0c\x00\x0e", - ReadBitsResponseBase([1, 0, 1, 1, 0]): b"\x01\x0d", + ReadBitsRequestBase(12, 14, 0, 0, 0, False): b"\x00\x0c\x00\x0e", + ReadBitsResponseBase([1, 0, 1, 1, 0], 0, 0, 0, False): b"\x01\x0d", } for request, expected in iter(messages.items()): assert request.encode() == expected @@ -89,8 +89,8 @@ async def test_bit_read_message_execute_value_errors(self): """Test bit read request encoding.""" context = MockContext() requests = [ - ReadCoilsRequest(1, 0x800), - ReadDiscreteInputsRequest(1, 0x800), + ReadCoilsRequest(1, 0x800, 0, 0, 0, False), + ReadDiscreteInputsRequest(1, 0x800, 0, 0, 0, False), ] for request in requests: result = await request.execute(context) @@ -100,8 +100,8 @@ async def test_bit_read_message_execute_address_errors(self): """Test bit read request encoding.""" context = MockContext() requests = [ - ReadCoilsRequest(1, 5), - ReadDiscreteInputsRequest(1, 5), + ReadCoilsRequest(1, 5, 0, 0, 0, False), + ReadDiscreteInputsRequest(1, 5, 0, 0, 0, False), ] for request in requests: result = await request.execute(context) @@ -112,8 +112,8 @@ async def test_bit_read_message_execute_success(self): context = MockContext() context.validate = lambda a, b, c: True requests = [ - ReadCoilsRequest(1, 5), - ReadDiscreteInputsRequest(1, 5), + ReadCoilsRequest(1, 5, 0, 0, 0, False), + ReadDiscreteInputsRequest(1, 5, 0, 0, 0, False), ] for request in requests: result = await request.execute(context) @@ -122,12 +122,12 @@ async def test_bit_read_message_execute_success(self): def test_bit_read_message_get_response_pdu(self): """Test bit read message get response pdu.""" requests = { - ReadCoilsRequest(1, 5): 3, - ReadCoilsRequest(1, 8): 3, - ReadCoilsRequest(0, 16): 4, - ReadDiscreteInputsRequest(1, 21): 5, - ReadDiscreteInputsRequest(1, 24): 5, - ReadDiscreteInputsRequest(1, 1900): 240, + ReadCoilsRequest(1, 5, 0, 0, 0, False): 3, + ReadCoilsRequest(1, 8, 0, 0, 0, False): 3, + ReadCoilsRequest(0, 16, 0, 0, 0, False): 4, + ReadDiscreteInputsRequest(1, 21, 0, 0, 0, False): 5, + ReadDiscreteInputsRequest(1, 24, 0, 0, 0, False): 5, + ReadDiscreteInputsRequest(1, 1900, 0, 0, 0, False): 240, } for request, expected in iter(requests.items()): pdu_len = request.get_response_pdu_size() diff --git a/test/sub_function_codes/test_bit_write_messages.py b/test/sub_function_codes/test_bit_write_messages.py index 4fbb081ef..54cae8b41 100644 --- a/test/sub_function_codes/test_bit_write_messages.py +++ b/test/sub_function_codes/test_bit_write_messages.py @@ -6,13 +6,13 @@ * Read/Write Discretes * Read Coils """ -from pymodbus.bit_write_message import ( +from pymodbus.pdu import ModbusExceptions +from pymodbus.pdu.bit_write_message import ( WriteMultipleCoilsRequest, WriteMultipleCoilsResponse, WriteSingleCoilRequest, WriteSingleCoilResponse, ) -from pymodbus.pdu import ModbusExceptions from test.conftest import FakeList, MockContext diff --git a/test/sub_function_codes/test_diag_messages.py b/test/sub_function_codes/test_diag_messages.py index 9ed59ee75..d5caa11b6 100644 --- a/test/sub_function_codes/test_diag_messages.py +++ b/test/sub_function_codes/test_diag_messages.py @@ -2,7 +2,8 @@ import pytest from pymodbus.constants import ModbusPlusOperation -from pymodbus.diag_message import ( +from pymodbus.exceptions import NotImplementedException +from pymodbus.pdu.diag_message import ( ChangeAsciiInputDelimiterRequest, ChangeAsciiInputDelimiterResponse, ClearCountersRequest, @@ -42,7 +43,6 @@ ReturnSlaveNoResponseCountRequest, ReturnSlaveNoResponseCountResponse, ) -from pymodbus.exceptions import NotImplementedException class TestDataStore: diff --git a/test/sub_function_codes/test_mei_messages.py b/test/sub_function_codes/test_mei_messages.py index 4b54337d0..371b23e17 100644 --- a/test/sub_function_codes/test_mei_messages.py +++ b/test/sub_function_codes/test_mei_messages.py @@ -7,7 +7,7 @@ from pymodbus.constants import DeviceInformation from pymodbus.device import ModbusControlBlock -from pymodbus.mei_message import ( +from pymodbus.pdu.mei_message import ( ReadDeviceInformationRequest, ReadDeviceInformationResponse, ) diff --git a/test/sub_function_codes/test_other_messages.py b/test/sub_function_codes/test_other_messages.py index 97305988d..f5c51d6d4 100644 --- a/test/sub_function_codes/test_other_messages.py +++ b/test/sub_function_codes/test_other_messages.py @@ -1,7 +1,7 @@ """Test other messages.""" from unittest import mock -import pymodbus.other_message as pymodbus_message +import pymodbus.pdu.other_message as pymodbus_message class TestOtherMessage: @@ -86,7 +86,7 @@ def test_get_comm_event_log_with_events(self): async def test_report_slave_id_request(self): """Test report slave id request.""" - with mock.patch("pymodbus.other_message.DeviceInformationFactory") as dif: + with mock.patch("pymodbus.pdu.other_message.DeviceInformationFactory") as dif: # First test regular identity strings identity = { 0x00: "VN", # VendorName @@ -126,7 +126,7 @@ async def test_report_slave_id_request(self): async def test_report_slave_id(self): """Test report slave id.""" - with mock.patch("pymodbus.other_message.DeviceInformationFactory") as dif: + with mock.patch("pymodbus.pdu.other_message.DeviceInformationFactory") as dif: dif.get.return_value = {} request = pymodbus_message.ReportSlaveIdRequest() request.decode(b"\x12") diff --git a/test/sub_function_codes/test_register_read_messages.py b/test/sub_function_codes/test_register_read_messages.py index 14b9d6505..ce5504e65 100644 --- a/test/sub_function_codes/test_register_read_messages.py +++ b/test/sub_function_codes/test_register_read_messages.py @@ -1,6 +1,6 @@ """Test register read messages.""" from pymodbus.pdu import ModbusExceptions -from pymodbus.register_read_message import ( +from pymodbus.pdu.register_read_message import ( ReadHoldingRegistersRequest, ReadHoldingRegistersResponse, ReadInputRegistersRequest, @@ -30,10 +30,9 @@ class TestReadRegisterMessages: * Read Holding Registers """ - value = None - values = None - request_read = None - response_read = None + values: list + request_read: dict + response_read: dict def setup_method(self): """Initialize the test environment and builds request/result encoding pairs.""" @@ -42,7 +41,6 @@ def setup_method(self): "read_count": 5, "write_address": 1, } - self.value = 0xABCD self.values = [0xA, 0xB, 0xC] self.request_read = { ReadRegistersRequestBase(1, 5): b"\x00\x01\x00\x05", @@ -84,20 +82,9 @@ def test_register_read_responses(self): def test_register_read_response_decode(self): """Test register read response.""" - registers = [ - [0x0A, 0x0B, 0x0C], - [0x0A, 0x0B, 0x0C], - [0x0A, 0x0B, 0x0C], - [0x0A, 0x0B, 0x0C, 0x0A, 0x0B, 0x0C], - ] - values = sorted( - self.response_read.items(), - key=lambda x: str(x), # pylint: disable=unnecessary-lambda - ) - for packet, register in zip(values, registers): - request, response = packet - request.decode(response) - assert request.registers == register + for response, packet in self.response_read.items(): + response.decode(packet) + assert response.registers == self.values async def test_register_read_requests_count_errors(self): """This tests that the register request messages. diff --git a/test/sub_function_codes/test_register_write_messages.py b/test/sub_function_codes/test_register_write_messages.py index 8f1b6c48f..1ec6f4202 100644 --- a/test/sub_function_codes/test_register_write_messages.py +++ b/test/sub_function_codes/test_register_write_messages.py @@ -1,7 +1,7 @@ """Test register write messages.""" from pymodbus.payload import BinaryPayloadBuilder, Endian from pymodbus.pdu import ModbusExceptions -from pymodbus.register_write_message import ( +from pymodbus.pdu.register_write_message import ( MaskWriteRegisterRequest, MaskWriteRegisterResponse, WriteMultipleRegistersRequest, diff --git a/test/sub_server/test_server_asyncio.py b/test/sub_server/test_server_asyncio.py index 9ae9b6af8..e3de50956 100755 --- a/test/sub_server/test_server_asyncio.py +++ b/test/sub_server/test_server_asyncio.py @@ -8,7 +8,7 @@ import pytest -from pymodbus import Framer +from pymodbus import FramerType from pymodbus.datastore import ( ModbusSequentialDataBlock, ModbusServerContext, @@ -154,15 +154,15 @@ async def start_server( args["identity"] = self.identity if do_tls: self.server = ModbusTlsServer( - self.context, Framer.TLS, self.identity, SERV_ADDR + self.context, FramerType.TLS, self.identity, SERV_ADDR ) elif do_udp: self.server = ModbusUdpServer( - self.context, Framer.SOCKET, self.identity, SERV_ADDR + self.context, FramerType.SOCKET, self.identity, SERV_ADDR ) else: self.server = ModbusTcpServer( - self.context, Framer.SOCKET, self.identity, SERV_ADDR + self.context, FramerType.SOCKET, self.identity, SERV_ADDR ) assert self.server if do_forever: @@ -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.register_read_message.ReadHoldingRegistersRequest.execute", + "pymodbus.pdu.register_read_message.ReadHoldingRegistersRequest.execute", side_effect=NoSuchSlaveException, ): await self.connect_server() diff --git a/test/sub_server/test_server_multidrop.py b/test/sub_server/test_server_multidrop.py deleted file mode 100644 index 74711ab5c..000000000 --- a/test/sub_server/test_server_multidrop.py +++ /dev/null @@ -1,174 +0,0 @@ -"""Test server working as slave on a multidrop RS485 line.""" -from unittest import mock - -import pytest - -from pymodbus.framer.rtu_framer import ModbusRtuFramer -from pymodbus.server.async_io import ServerDecoder - - -class TestMultidrop: - """Test that server works on a multidrop line.""" - - slaves = [2] - - good_frame = b"\x02\x03\x00\x01\x00}\xd4\x18" - - @pytest.fixture(name="framer") - def fixture_framer(self): - """Prepare framer.""" - return ModbusRtuFramer(ServerDecoder()) - - @pytest.fixture(name="callback") - def fixture_callback(self): - """Prepare dummy callback.""" - return mock.Mock() - - def test_ok_frame(self, framer, callback): - """Test ok frame.""" - serial_event = self.good_frame - framer.processIncomingPacket(serial_event, callback, self.slaves) - callback.assert_called_once() - - def test_ok_2frame(self, framer, callback): - """Test ok frame.""" - serial_event = self.good_frame + self.good_frame - framer.processIncomingPacket(serial_event, callback, self.slaves) - assert callback.call_count == 2 - - def test_bad_crc(self, framer, callback): - """Test bad crc.""" - serial_event = b"\x02\x03\x00\x01\x00}\xd4\x19" # Manually mangled crc - framer.processIncomingPacket(serial_event, callback, self.slaves) - callback.assert_not_called() - - def test_wrong_id(self, framer, callback): - """Test frame wrong id.""" - serial_event = b"\x01\x03\x00\x01\x00}\xd4+" # Frame with good CRC but other id - framer.processIncomingPacket(serial_event, callback, self.slaves) - callback.assert_not_called() - - def test_big_split_response_frame_from_other_id(self, framer, callback): - """Test split response.""" - # This is a single *response* from device id 1 after being queried for 125 holding register values - # Because the response is so long it spans several serial events - serial_events = [ - b"\x01\x03\xfa\xc4y\xc0\x00\xc4y\xc0\x00\xc4y\xc0\x00\xc4y\xc0\x00\xc4y\xc0\x00Dz\x00\x00C\x96\x00\x00", - b"?\x05\x1e\xb8DH\x00\x00D\x96\x00\x00D\xfa\x00\x00DH\x00\x00D\x96\x00\x00D\xfa\x00\x00DH\x00", - b"\x00D\x96\x00\x00D\xfa\x00\x00B\x96\x00\x00B\xb4\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00", - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00", - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00", - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00", - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00", - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00", - b"\x00\x00\x00\x00\x00\x00\x00N,", - ] - for serial_event in serial_events: - framer.processIncomingPacket(serial_event, callback, self.slaves) - callback.assert_not_called() - - def test_split_frame(self, framer, callback): - """Test split frame.""" - serial_events = [self.good_frame[:5], self.good_frame[5:]] - for serial_event in serial_events: - framer.processIncomingPacket(serial_event, callback, self.slaves) - callback.assert_called_once() - - def test_complete_frame_trailing_data_without_id(self, framer, callback): - """Test trailing data.""" - garbage = b"\x05\x04\x03" # without id - serial_event = garbage + self.good_frame - framer.processIncomingPacket(serial_event, callback, self.slaves) - callback.assert_called_once() - - def test_complete_frame_trailing_data_with_id(self, framer, callback): - """Test trailing data.""" - garbage = b"\x05\x04\x03\x02\x01\x00" # with id - serial_event = garbage + self.good_frame - framer.processIncomingPacket(serial_event, callback, self.slaves) - callback.assert_called_once() - - def test_split_frame_trailing_data_with_id(self, framer, callback): - """Test split frame.""" - garbage = b"\x05\x04\x03\x02\x01\x00" - serial_events = [garbage + self.good_frame[:5], self.good_frame[5:]] - for serial_event in serial_events: - framer.processIncomingPacket(serial_event, callback, self.slaves) - callback.assert_called_once() - - def test_coincidental_1(self, framer, callback): - """Test conincidental.""" - garbage = b"\x02\x90\x07" - serial_events = [garbage, self.good_frame[:5], self.good_frame[5:]] - for serial_event in serial_events: - framer.processIncomingPacket(serial_event, callback, self.slaves) - callback.assert_called_once() - - def test_coincidental_2(self, framer, callback): - """Test conincidental.""" - garbage = b"\x02\x10\x07" - serial_events = [garbage, self.good_frame[:5], self.good_frame[5:]] - for serial_event in serial_events: - framer.processIncomingPacket(serial_event, callback, self.slaves) - callback.assert_called_once() - - def test_coincidental_3(self, framer, callback): - """Test conincidental.""" - garbage = b"\x02\x10\x07\x10" - serial_events = [garbage, self.good_frame[:5], self.good_frame[5:]] - for serial_event in serial_events: - framer.processIncomingPacket(serial_event, callback, self.slaves) - callback.assert_called_once() - - def test_wrapped_frame(self, framer, callback): - """Test wrapped frame.""" - garbage = b"\x05\x04\x03\x02\x01\x00" - serial_event = garbage + self.good_frame + garbage - framer.processIncomingPacket(serial_event, callback, self.slaves) - - # We probably should not respond in this case; in this case we've likely become desynchronized - # i.e. this probably represents a case where a command came for us, but we didn't get - # to the serial buffer in time (some other co-routine or perhaps a block on the USB bus) - # and the master moved on and queried another device - callback.assert_called_once() - - def test_frame_with_trailing_data(self, framer, callback): - """Test trailing data.""" - garbage = b"\x05\x04\x03\x02\x01\x00" - serial_event = self.good_frame + garbage - framer.processIncomingPacket(serial_event, callback, self.slaves) - - # We should not respond in this case for identical reasons as test_wrapped_frame - callback.assert_called_once() - - def test_getFrameStart(self, framer): - """Test getFrameStart.""" - result = None - count = 0 - def test_callback(data): - """Check callback.""" - nonlocal result, count - count += 1 - result = data.function_code.to_bytes(1,'big')+data.encode() - - framer_ok = b"\x02\x03\x00\x01\x00\x7d\xd4\x18" - framer.processIncomingPacket(framer_ok, test_callback, self.slaves) - assert framer_ok[1:-2] == result - - count = 0 - framer_2ok = framer_ok + framer_ok - framer.processIncomingPacket(framer_2ok, test_callback, self.slaves) - assert count == 2 - assert not framer._buffer # pylint: disable=protected-access - - framer._buffer = framer_ok[:2] # pylint: disable=protected-access - framer.processIncomingPacket(b'', test_callback, self.slaves) - assert framer_ok[:2] == framer._buffer # pylint: disable=protected-access - - framer._buffer = framer_ok[:3] # pylint: disable=protected-access - framer.processIncomingPacket(b'', test_callback, self.slaves) - assert framer_ok[:3] == framer._buffer # pylint: disable=protected-access - - framer_ok = b"\xF0\x03\x00\x01\x00}\xd4\x18" - framer.processIncomingPacket(framer_ok, test_callback, self.slaves) - assert framer._buffer == framer_ok[-3:] # pylint: disable=protected-access diff --git a/test/sub_server/test_simulator.py b/test/sub_server/test_simulator.py index aeefed8b7..a870d942e 100644 --- a/test/sub_server/test_simulator.py +++ b/test/sub_server/test_simulator.py @@ -85,7 +85,7 @@ class TestSimulator: "addr": [31, 32], "value": 50, "action": "random", - "kwargs": {"minval": 10, "maxval": 80}, + "parameters": {"minval": 10, "maxval": 80}, }, ], "float32": [ @@ -151,7 +151,7 @@ class TestSimulator: Cell(type=CellType.UINT32, value=5, action=1), Cell(type=CellType.NEXT, value=17320), # 30 Cell( - type=CellType.UINT32, action=2, action_kwargs={"minval": 10, "maxval": 80} + type=CellType.UINT32, action=2, action_parameters={"minval": 10, "maxval": 80} ), Cell(type=CellType.NEXT, value=50), Cell(type=CellType.FLOAT32, access=True, value=17731), @@ -215,7 +215,7 @@ def test_simulator_config_verify(self): assert reg.value == test_cell.value, f"at index {i} - {offset}" assert reg.action == test_cell.action, f"at index {i} - {offset}" assert ( - reg.action_kwargs == test_cell.action_kwargs + reg.action_parameters == test_cell.action_parameters ), f"at index {i} - {offset}" assert ( reg.count_read == test_cell.count_read @@ -404,10 +404,10 @@ def test_simulator_set_values(self): exc_simulator.setValues(FX_WRITE_BIT, 84, [True]) exc_simulator.setValues(FX_WRITE_BIT, 86, [True, False, True]) result = exc_simulator.getValues(FX_READ_BIT, 80, 8) - assert [True, False] * 4 == result + assert result == [True, False] * 4 exc_simulator.setValues(FX_WRITE_BIT, 88, [False]) result = exc_simulator.getValues(FX_READ_BIT, 86, 3) - assert [True, False, False] == result + assert result == [True, False, False] exc_simulator.setValues(FX_WRITE_BIT, 80, [True] * 17) def test_simulator_get_text(self): @@ -509,13 +509,13 @@ def test_simulator_action_increment( exc_setup = copy.deepcopy(self.default_config) exc_simulator = ModbusSimulatorContext(exc_setup, None) action = exc_simulator.action_name_to_id[Label.increment] - kwargs = { + parameters = { "minval": minval, "maxval": maxval, } exc_simulator.registers[30].type = celltype exc_simulator.registers[30].action = action - exc_simulator.registers[30].action_kwargs = kwargs + exc_simulator.registers[30].action_parameters = parameters exc_simulator.registers[31].type = CellType.NEXT is_int = celltype != CellType.FLOAT32 @@ -557,13 +557,13 @@ def test_simulator_action_random(self, celltype, minval, maxval): exc_setup = copy.deepcopy(self.default_config) exc_simulator = ModbusSimulatorContext(exc_setup, None) action = exc_simulator.action_name_to_id[Label.random] - kwargs = { + parameters = { "minval": minval, "maxval": maxval, } exc_simulator.registers[30].type = celltype exc_simulator.registers[30].action = action - exc_simulator.registers[30].action_kwargs = kwargs + exc_simulator.registers[30].action_parameters = parameters exc_simulator.registers[31].type = CellType.NEXT is_int = celltype != CellType.FLOAT32 reg_count = 1 if celltype in (CellType.BITS, CellType.UINT16) else 2 diff --git a/test/test_factory.py b/test/test_factory.py index da27dd679..52d415794 100644 --- a/test/test_factory.py +++ b/test/test_factory.py @@ -6,16 +6,11 @@ from pymodbus.pdu import ModbusRequest, ModbusResponse -def _raise_exception(_): - """Raise exception.""" - raise ModbusException("something") - - class TestFactory: """Unittest for the pymod.exceptions module.""" - client = None - server = None + client: ClientDecoder + server: ServerDecoder request = ( (0x01, b"\x01\x00\x01\x00\x01"), # read coils (0x02, b"\x02\x00\x01\x00\x01"), # read discrete inputs @@ -139,15 +134,13 @@ def test_requests_working(self): def test_client_factory_fails(self): """Tests that a client factory will fail to decode a bad message.""" - self.client._helper = _raise_exception # pylint: disable=protected-access - actual = self.client.decode(None) - assert not actual + 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.""" - self.server._helper = _raise_exception # pylint: disable=protected-access - actual = self.server.decode(None) - assert not actual + with pytest.raises(TypeError): + self.server.decode(None) def test_server_register_custom_request(self): """Test server register custom request.""" diff --git a/test/test_file_message.py b/test/test_file_message.py index 9e14ab9c7..36ef6ee0e 100644 --- a/test/test_file_message.py +++ b/test/test_file_message.py @@ -6,7 +6,8 @@ * Read/Write Discretes * Read Coils """ -from pymodbus.file_message import ( +from pymodbus.pdu import ModbusExceptions +from pymodbus.pdu.file_message import ( FileRecord, ReadFifoQueueRequest, ReadFifoQueueResponse, @@ -15,7 +16,6 @@ WriteFileRecordRequest, WriteFileRecordResponse, ) -from pymodbus.pdu import ModbusExceptions from test.conftest import MockContext # pylint: disable=wrong-import-order diff --git a/test/test_logging.py b/test/test_logging.py index a8730190d..3ce498133 100644 --- a/test/test_logging.py +++ b/test/test_logging.py @@ -4,7 +4,7 @@ import pytest -from pymodbus.logging import Log +from pymodbus.logging import Log, pymodbus_apply_logging_config class TestLogging: @@ -32,6 +32,7 @@ def test_log_simple(self): [ ("string {} {} {}", "string 101 102 103", (101, 102, 103)), ("string {}", "string 0x41 0x42 0x43 0x44", (b"ABCD", ":hex")), + ("string {}", "string b'41424344'", (b"ABCD", ":b2a")), ("string {}", "string 125", (125, ":str")), ], ) @@ -39,3 +40,17 @@ def test_log_parms(self, txt, result, params): """Test string with parameters (old f-string).""" log_txt = Log.build_msg(txt, *params) assert log_txt == result + + def test_apply_logging(self): + """Test pymodbus_apply_logging_config.""" + pymodbus_apply_logging_config("debug") + pymodbus_apply_logging_config(logging.NOTSET) + pymodbus_apply_logging_config("debug", "pymodbus.log") + pymodbus_apply_logging_config("info") + Log.info("test") + pymodbus_apply_logging_config("warning") + Log.warning("test") + pymodbus_apply_logging_config("critical") + Log.critical("test") + pymodbus_apply_logging_config("error") + Log.error("test") diff --git a/test/test_network.py b/test/test_network.py index 0131fedf7..97bfcb0a1 100644 --- a/test/test_network.py +++ b/test/test_network.py @@ -2,7 +2,7 @@ from __future__ import annotations import asyncio -from typing import Callable +from collections.abc import Callable import pytest @@ -27,9 +27,7 @@ def __init__( async def start_run(self): """Call need functions to start server/client.""" - if self.is_server: - return await self.listen() - return await self.connect() + return await self.listen() def callback_connected(self) -> None: @@ -76,7 +74,7 @@ async def test_stub(self, use_port, use_cls): assert await stub.start_run() assert await client.connect() test_data = b"Data got echoed." - client.transport.write(test_data) + client.ctx.transport.write(test_data) client.close() stub.close() diff --git a/test/test_pdu.py b/test/test_pdu.py index 44f44f874..aa18b2d6c 100644 --- a/test/test_pdu.py +++ b/test/test_pdu.py @@ -15,11 +15,11 @@ class TestPdu: """Unittest for the pymod.pdu module.""" bad_requests = ( - ModbusRequest(), - ModbusResponse(), + ModbusRequest(0, 0, 0, False), + ModbusResponse(0, 0, 0, False), ) - illegal = IllegalFunctionRequest(1) - exception = ExceptionResponse(1, 1) + illegal = IllegalFunctionRequest(1, 0, 0, 0, False) + exception = ExceptionResponse(1, 1, 0, 0, 0, False) def test_not_impelmented(self): """Test a base classes for not implemented functions.""" @@ -43,7 +43,7 @@ async def test_error_methods(self): def test_request_exception_factory(self): """Test all error methods.""" - request = ModbusRequest() + request = ModbusRequest(0, 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()): diff --git a/test/test_remote_datastore.py b/test/test_remote_datastore.py index a7f2a5c60..0b6c5d217 100644 --- a/test/test_remote_datastore.py +++ b/test/test_remote_datastore.py @@ -4,12 +4,12 @@ import pytest -from pymodbus.bit_read_message import ReadCoilsResponse -from pymodbus.bit_write_message import WriteMultipleCoilsResponse from pymodbus.datastore.remote import RemoteSlaveContext from pymodbus.exceptions import NotImplementedException from pymodbus.pdu import ExceptionResponse -from pymodbus.register_read_message import ReadInputRegistersResponse +from pymodbus.pdu.bit_read_message import ReadCoilsResponse +from pymodbus.pdu.bit_write_message import WriteMultipleCoilsResponse +from pymodbus.pdu.register_read_message import ReadInputRegistersResponse class TestRemoteDataStore: diff --git a/test/test_transaction.py b/test/test_transaction.py index 97a53a1a3..e7cdc23f2 100755 --- a/test/test_transaction.py +++ b/test/test_transaction.py @@ -1,5 +1,4 @@ """Test transaction.""" -from itertools import count from unittest import mock from pymodbus.exceptions import ( @@ -9,11 +8,11 @@ from pymodbus.pdu import ModbusRequest from pymodbus.transaction import ( ModbusAsciiFramer, - ModbusBinaryFramer, ModbusRtuFramer, ModbusSocketFramer, ModbusTlsFramer, ModbusTransactionManager, + SyncModbusTransactionManager, ) @@ -29,7 +28,6 @@ class TestTransaction: # pylint: disable=too-many-public-methods _tls = None _rtu = None _ascii = None - _binary = None _manager = None _tm = None @@ -44,8 +42,7 @@ def setup_method(self): self._tls = ModbusTlsFramer(decoder=self.decoder, client=None) self._rtu = ModbusRtuFramer(decoder=self.decoder, client=None) self._ascii = ModbusAsciiFramer(decoder=self.decoder, client=None) - self._binary = ModbusBinaryFramer(decoder=self.decoder, client=None) - self._manager = ModbusTransactionManager(self.client) + self._manager = SyncModbusTransactionManager(self.client, 3) # ----------------------------------------------------------------------- # # Modbus transaction manager @@ -69,7 +66,6 @@ def test_calculate_exception_length(self): """Test calculate exception length.""" for framer, exception_length in ( ("ascii", 11), - ("binary", 7), ("rtu", 5), ("tcp", 9), ("tls", 2), @@ -78,8 +74,6 @@ def test_calculate_exception_length(self): self._manager.client = mock.MagicMock() if framer == "ascii": self._manager.client.framer = self._ascii - elif framer == "binary": - self._manager.client.framer = self._binary elif framer == "rtu": self._manager.client.framer = self._rtu elif framer == "tcp": @@ -95,11 +89,10 @@ def test_calculate_exception_length(self): == exception_length ) - @mock.patch("pymodbus.transaction.time") - def test_execute(self, mock_time): + @mock.patch.object(SyncModbusTransactionManager, "_recv") + @mock.patch.object(ModbusTransactionManager, "getTransaction") + def test_execute(self, mock_get_transaction, mock_recv): """Test execute.""" - mock_time.time.side_effect = count() - client = mock.MagicMock() client.framer = self._ascii client.framer._buffer = b"deadbeef" # pylint: disable=protected-access @@ -119,55 +112,48 @@ def test_execute(self, mock_time): request.get_response_pdu_size.return_value = 10 request.slave_id = 1 request.function_code = 222 - trans = ModbusTransactionManager(client) - trans._recv = mock.MagicMock( # pylint: disable=protected-access + trans = SyncModbusTransactionManager(client, 3) + mock_recv.reset_mock( return_value=b"abcdef" ) assert trans.retries == 3 - assert not trans.retry_on_empty - trans.getTransaction = mock.MagicMock() - trans.getTransaction.return_value = "response" + mock_get_transaction.return_value = b"response" response = trans.execute(request) - assert response == "response" + assert response == b"response" # No response - trans._recv = mock.MagicMock( # pylint: disable=protected-access + mock_recv.reset_mock( return_value=b"abcdef" ) trans.transactions = {} - trans.getTransaction = mock.MagicMock() - trans.getTransaction.return_value = None + mock_get_transaction.return_value = None response = trans.execute(request) assert isinstance(response, ModbusIOException) # No response with retries - trans.retry_on_empty = True - trans._recv = mock.MagicMock( # pylint: disable=protected-access + mock_recv.reset_mock( side_effect=iter([b"", b"abcdef"]) ) response = trans.execute(request) assert isinstance(response, ModbusIOException) # wrong handle_local_echo - trans._recv = mock.MagicMock( # pylint: disable=protected-access + mock_recv.reset_mock( side_effect=iter([b"abcdef", b"deadbe", b"123456"]) ) client.comm_params.handle_local_echo = True - trans.retry_on_empty = False - trans.retry_on_invalid = False assert trans.execute(request).message == "[Input/Output] Wrong local echo" client.comm_params.handle_local_echo = False # retry on invalid response - trans.retry_on_invalid = True - trans._recv = mock.MagicMock( # pylint: disable=protected-access + mock_recv.reset_mock( side_effect=iter([b"", b"abcdef", b"deadbe", b"123456"]) ) response = trans.execute(request) assert isinstance(response, ModbusIOException) # Unable to decode response - trans._recv = mock.MagicMock( # pylint: disable=protected-access + mock_recv.reset_mock( side_effect=ModbusIOException() ) client.framer.processIncomingPacket.side_effect = mock.MagicMock( @@ -176,20 +162,17 @@ def test_execute(self, mock_time): assert isinstance(trans.execute(request), ModbusIOException) # Broadcast - client.params.broadcast_enable = True request.slave_id = 0 response = trans.execute(request) assert response == b"Broadcast write sent - no response expected" # Broadcast w/ Local echo client.comm_params.handle_local_echo = True - client.params.broadcast_enable = True - recv = mock.MagicMock(return_value=b"deadbeef") - trans._recv = recv # pylint: disable=protected-access + mock_recv.reset_mock(return_value=b"deadbeef") request.slave_id = 0 response = trans.execute(request) assert response == b"Broadcast write sent - no response expected" - recv.assert_called_once_with(8, False) + mock_recv.assert_called_once_with(8, False) client.comm_params.handle_local_echo = False def test_transaction_manager_tid(self): @@ -201,33 +184,20 @@ def test_transaction_manager_tid(self): def test_get_transaction_manager_transaction(self): """Test the getting a transaction from the transaction manager.""" - - class Request: # pylint: disable=too-few-public-methods - """Request.""" - self._manager.reset() - handle = Request() - handle.transaction_id = ( # pylint: disable=attribute-defined-outside-init - self._manager.getNextTID() + handle = ModbusRequest( + 0, self._manager.getNextTID(), 0, False ) - handle.message = b"testing" # pylint: disable=attribute-defined-outside-init self._manager.addTransaction(handle) result = self._manager.getTransaction(handle.transaction_id) - assert handle.message == result.message + assert handle is result def test_delete_transaction_manager_transaction(self): """Test deleting a transaction from the dict transaction manager.""" - - class Request: # pylint: disable=too-few-public-methods - """Request.""" - self._manager.reset() - handle = Request() - handle.transaction_id = ( # pylint: disable=attribute-defined-outside-init - self._manager.getNextTID() + handle = ModbusRequest( + 0, self._manager.getNextTID(), 0, False ) - handle.message = b"testing" # pylint: disable=attribute-defined-outside-init - self._manager.addTransaction(handle) self._manager.delTransaction(handle.transaction_id) assert not self._manager.getTransaction(handle.transaction_id) @@ -248,6 +218,7 @@ def callback(data): msg = b"\x00\x01\x12\x34\x00\x06\xff\x02\x01\x02\x00\x08" self._tcp.processIncomingPacket(msg, callback, [1]) self._tcp._buffer = msg # pylint: disable=protected-access + callback(b'') def test_tcp_framer_transaction_full(self): """Test a full tcp frame transaction.""" @@ -346,7 +317,7 @@ def callback(data): count += 1 result = data - expected = ModbusRequest() + expected = ModbusRequest(0, 0, 0, False) expected.transaction_id = 0x0001 expected.protocol_id = 0x1234 expected.slave_id = 0xFF @@ -358,19 +329,18 @@ def callback(data): # for name in ("transaction_id", "protocol_id", "slave_id"): # assert getattr(expected, name) == getattr(actual, name) - def test_tcp_framer_packet(self): + @mock.patch.object(ModbusRequest, "encode") + def test_tcp_framer_packet(self, mock_encode): """Test a tcp frame packet build.""" - old_encode = ModbusRequest.encode - ModbusRequest.encode = lambda self: b"" - message = ModbusRequest() + message = ModbusRequest(0, 0, 0, False) message.transaction_id = 0x0001 message.protocol_id = 0x0000 message.slave_id = 0xFF message.function_code = 0x01 expected = b"\x00\x01\x00\x00\x00\x02\xff\x01" + mock_encode.return_value = b"" actual = self._tcp.buildPacket(message) assert expected == actual - ModbusRequest.encode = old_encode # ----------------------------------------------------------------------- # # TLS tests @@ -513,16 +483,15 @@ def callback(data): self._tcp.processIncomingPacket(msg, callback, [0, 1]) assert result - def test_framer_tls_framer_packet(self): + @mock.patch.object(ModbusRequest, "encode") + def test_framer_tls_framer_packet(self, mock_encode): """Test a tls frame packet build.""" - old_encode = ModbusRequest.encode - ModbusRequest.encode = lambda self: b"" - message = ModbusRequest() + message = ModbusRequest(0, 0, 0, False) message.function_code = 0x01 expected = b"\x01" + mock_encode.return_value = b"" actual = self._tls.buildPacket(message) assert expected == actual - ModbusRequest.encode = old_encode # ----------------------------------------------------------------------- # # RTU tests @@ -590,17 +559,16 @@ def callback(data): assert int(msg[0]) == header_dict["uid"] assert msg[-2:] == header_dict["crc"] - def test_rtu_framer_packet(self): + @mock.patch.object(ModbusRequest, "encode") + def test_rtu_framer_packet(self, mock_encode): """Test a rtu frame packet build.""" - old_encode = ModbusRequest.encode - ModbusRequest.encode = lambda self: b"" - message = ModbusRequest() + message = ModbusRequest(0, 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 - ModbusRequest.encode = old_encode def test_rtu_decode_exception(self): """Test that the RTU framer can decode errors.""" @@ -695,21 +663,20 @@ def callback(data): def test_ascii_framer_populate(self): """Test a ascii frame packet build.""" - request = ModbusRequest() + request = ModbusRequest(0, 0, 0, False) self._ascii.populateResult(request) assert not request.slave_id - def test_ascii_framer_packet(self): + @mock.patch.object(ModbusRequest, "encode") + def test_ascii_framer_packet(self, mock_encode): """Test a ascii frame packet build.""" - old_encode = ModbusRequest.encode - ModbusRequest.encode = lambda self: b"" - message = ModbusRequest() + message = ModbusRequest(0, 0, 0, False) message.slave_id = 0xFF message.function_code = 0x01 expected = b":FF0100\r\n" + mock_encode.return_value = b"" actual = self._ascii.buildPacket(message) assert expected == actual - ModbusRequest.encode = old_encode def test_ascii_process_incoming_packets(self): """Test ascii process incoming packet.""" @@ -724,82 +691,3 @@ def callback(data): msg = b":F7031389000A60\r\n" self._ascii.processIncomingPacket(msg, callback, [0,1]) assert result - - # ----------------------------------------------------------------------- # - # Binary tests - # ----------------------------------------------------------------------- # - def test_binary_framer_transaction_ready(self): - """Test a binary frame transaction.""" - count = 0 - result = None - def callback(data): - """Simulate callback.""" - nonlocal count, result - count += 1 - result = data - - msg = TEST_MESSAGE - self._binary.processIncomingPacket(msg, callback, [0,1]) - assert result - - def test_binary_framer_transaction_full(self): - """Test a full binary frame transaction.""" - count = 0 - result = None - def callback(data): - """Simulate callback.""" - nonlocal count, result - count += 1 - result = data - - msg = TEST_MESSAGE - self._binary.processIncomingPacket(msg, callback, [0,1]) - assert result - - def test_binary_framer_transaction_half(self): - """Test a half completed binary frame transaction.""" - count = 0 - result = None - def callback(data): - """Simulate callback.""" - nonlocal count, result - count += 1 - result = data - - msg_parts = (b"\x7b\x01\x03\x00", b"\x00\x00\x05\x85\xC9\x7d") - self._binary.processIncomingPacket(msg_parts[0], callback, [0,1]) - assert not result - self._binary.processIncomingPacket(msg_parts[1], callback, [0,1]) - assert result - - def test_binary_framer_populate(self): - """Test a binary frame packet build.""" - request = ModbusRequest() - self._binary.populateResult(request) - assert not request.slave_id - - def test_binary_framer_packet(self): - """Test a binary frame packet build.""" - old_encode = ModbusRequest.encode - ModbusRequest.encode = lambda self: b"" - message = ModbusRequest() - message.slave_id = 0xFF - message.function_code = 0x01 - expected = b"\x7b\xff\x01\x81\x80\x7d" - actual = self._binary.buildPacket(message) - assert expected == actual - ModbusRequest.encode = old_encode - - def test_binary_process_incoming_packet(self): - """Test binary process incoming packet.""" - mock_data = TEST_MESSAGE - slave = 0x00 - - def mock_callback(_mock_data): - pass - - self._binary.processIncomingPacket(mock_data, mock_callback, slave) - - # Test failure: - self._binary.checkFrame = mock.MagicMock(return_value=False) - self._binary.processIncomingPacket(mock_data, mock_callback, slave) diff --git a/test/transport/test_comm.py b/test/transport/test_comm.py index 8b175bc09..35b25c52c 100644 --- a/test/transport/test_comm.py +++ b/test/transport/test_comm.py @@ -174,6 +174,10 @@ async def test_split_serial_packet(self, client, server, use_port): ) async def test_serial_poll(self, client, server, use_port): """Test connection and data exchange.""" + if SerialTransport.force_poll: + client.close() + server.close() + return Log.debug("test_serial_poll {}", use_port) assert await server.listen() SerialTransport.force_poll = True @@ -188,6 +192,7 @@ async def test_serial_poll(self, client, server, use_port): assert not client.recv_buffer client.close() server.close() + SerialTransport.force_poll = False @pytest.mark.parametrize( ("use_comm_type", "use_host"), diff --git a/test/transport/test_serial.py b/test/transport/test_serial.py index c39e11e4c..702c0d441 100644 --- a/test/transport/test_serial.py +++ b/test/transport/test_serial.py @@ -22,17 +22,17 @@ class TestTransportSerial: async def test_init(self): """Test null modem init.""" - SerialTransport(asyncio.get_running_loop(), mock.Mock(), "dummy") + SerialTransport(asyncio.get_running_loop(), mock.Mock(), "dummy", None, None, None, None, None) async def test_loop(self): """Test asyncio abstract methods.""" - comm = SerialTransport(asyncio.get_running_loop(), mock.Mock(), "dummy") + comm = SerialTransport(asyncio.get_running_loop(), mock.Mock(), "dummy", None, None, None, None, None) assert comm.loop @pytest.mark.parametrize("inx", range(0, 11)) async def test_abstract_methods(self, inx): """Test asyncio abstract methods.""" - comm = SerialTransport(asyncio.get_running_loop(), mock.Mock(), "dummy") + comm = SerialTransport(asyncio.get_running_loop(), mock.Mock(), "dummy", None, None, None, None, None) methods = [ partial(comm.get_protocol), partial(comm.set_protocol, None), @@ -51,7 +51,7 @@ async def test_abstract_methods(self, inx): @pytest.mark.parametrize("inx", range(0, 4)) async def test_external_methods(self, inx): """Test external methods.""" - comm = SerialTransport(mock.MagicMock(), mock.Mock(), "dummy") + comm = SerialTransport(mock.MagicMock(), mock.Mock(), "dummy", None, None, None, None, None) comm.sync_serial.read = mock.MagicMock(return_value="abcd") comm.sync_serial.write = mock.MagicMock(return_value=4) comm.sync_serial.fileno = mock.MagicMock(return_value=2) @@ -73,7 +73,7 @@ async def test_external_methods(self, inx): async def test_create_serial(self): """Test external methods.""" transport, protocol = await create_serial_connection( - asyncio.get_running_loop(), mock.Mock, url="dummy" + asyncio.get_running_loop(), mock.Mock, "dummy" ) assert transport assert protocol @@ -81,9 +81,11 @@ async def test_create_serial(self): async def test_force_poll(self): """Test external methods.""" + if SerialTransport.force_poll: + return SerialTransport.force_poll = True transport, protocol = await create_serial_connection( - asyncio.get_running_loop(), mock.Mock, url="dummy" + asyncio.get_running_loop(), mock.Mock, "dummy" ) await asyncio.sleep(0) assert transport @@ -94,9 +96,11 @@ async def test_force_poll(self): async def test_write_force_poll(self): """Test write with poll.""" + if SerialTransport.force_poll: + return SerialTransport.force_poll = True transport, protocol = await create_serial_connection( - asyncio.get_running_loop(), mock.Mock, url="dummy" + asyncio.get_running_loop(), mock.Mock, "dummy" ) await asyncio.sleep(0) transport.write(b"abcd") @@ -106,14 +110,14 @@ async def test_write_force_poll(self): async def test_close(self): """Test close.""" - comm = SerialTransport(asyncio.get_running_loop(), mock.Mock(), "dummy") + comm = SerialTransport(asyncio.get_running_loop(), mock.Mock(), "dummy", None, None, None, None, None) comm.sync_serial = None comm.close() @pytest.mark.skipif(os.name == "nt", reason="Windows not supported") async def test_polling(self): """Test polling.""" - comm = SerialTransport(asyncio.get_running_loop(), mock.Mock(), "dummy") + comm = SerialTransport(asyncio.get_running_loop(), mock.Mock(), "dummy", None, None, None, None, None) comm.sync_serial = mock.MagicMock() comm.sync_serial.read.side_effect = asyncio.CancelledError("test") with contextlib.suppress(asyncio.CancelledError): @@ -122,7 +126,7 @@ async def test_polling(self): @pytest.mark.skipif(os.name == "nt", reason="Windows not supported") async def test_poll_task(self): """Test polling.""" - comm = SerialTransport(asyncio.get_running_loop(), mock.Mock(), "dummy") + comm = SerialTransport(asyncio.get_running_loop(), mock.Mock(), "dummy", None, None, None, None, None) comm.sync_serial = mock.MagicMock() comm.sync_serial.read.side_effect = serial.SerialException("test") await comm.polling_task() @@ -130,7 +134,7 @@ async def test_poll_task(self): @pytest.mark.skipif(os.name == "nt", reason="Windows not supported") async def test_poll_task2(self): """Test polling.""" - comm = SerialTransport(asyncio.get_running_loop(), mock.Mock(), "dummy") + comm = SerialTransport(asyncio.get_running_loop(), mock.Mock(), "dummy", None, None, None, None, None) comm.sync_serial = mock.MagicMock() comm.sync_serial = mock.MagicMock() comm.sync_serial.write.return_value = 4 @@ -142,7 +146,7 @@ async def test_poll_task2(self): @pytest.mark.skipif(os.name == "nt", reason="Windows not supported") async def test_write_exception(self): """Test write exception.""" - comm = SerialTransport(asyncio.get_running_loop(), mock.Mock(), "dummy") + comm = SerialTransport(asyncio.get_running_loop(), mock.Mock(), "dummy", None, None, None, None, None) comm.sync_serial = mock.MagicMock() comm.sync_serial.write.side_effect = BlockingIOError("test") comm.intern_write_ready() @@ -152,7 +156,7 @@ async def test_write_exception(self): @pytest.mark.skipif(os.name == "nt", reason="Windows not supported") async def test_write_ok(self): """Test write exception.""" - comm = SerialTransport(asyncio.get_running_loop(), mock.Mock(), "dummy") + comm = SerialTransport(asyncio.get_running_loop(), mock.Mock(), "dummy", None, None, None, None, None) comm.sync_serial = mock.MagicMock() comm.sync_serial.write.return_value = 4 comm.intern_write_buffer.append(b"abcd") @@ -161,7 +165,7 @@ async def test_write_ok(self): @pytest.mark.skipif(os.name == "nt", reason="Windows not supported") async def test_write_len(self): """Test write exception.""" - comm = SerialTransport(asyncio.get_running_loop(), mock.Mock(), "dummy") + comm = SerialTransport(asyncio.get_running_loop(), mock.Mock(), "dummy", None, None, None, None, None) comm.sync_serial = mock.MagicMock() comm.sync_serial.write.return_value = 3 comm.async_loop.add_writer = mock.Mock() @@ -171,7 +175,7 @@ async def test_write_len(self): @pytest.mark.skipif(os.name == "nt", reason="Windows not supported") async def test_write_force(self): """Test write exception.""" - comm = SerialTransport(asyncio.get_running_loop(), mock.Mock(), "dummy") + comm = SerialTransport(asyncio.get_running_loop(), mock.Mock(), "dummy", None, None, None, None, None) comm.poll_task = True comm.sync_serial = mock.MagicMock() comm.sync_serial.write.return_value = 3 @@ -181,7 +185,7 @@ async def test_write_force(self): @pytest.mark.skipif(os.name == "nt", reason="Windows not supported") async def test_read_ready(self): """Test polling.""" - comm = SerialTransport(asyncio.get_running_loop(), mock.Mock(), "dummy") + comm = SerialTransport(asyncio.get_running_loop(), mock.Mock(), "dummy", None, None, None, None, None) comm.sync_serial = mock.MagicMock() comm.intern_protocol = mock.Mock() comm.sync_serial.read = mock.Mock()